github.com/mweagle/Sparta@v1.15.0/docs_source/content/reference/decorators/dynamic_infrastructure.md (about)

     1  ---
     2  date: 2018-12-01 06:28:42
     3  title: Dynamic Infrastructure
     4  weight: 500
     5  ---
     6  
     7  In addition to provisioning AWS Lambda functions, Sparta supports the creation of
     8  other [CloudFormation Resources](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html).
     9  This enables a service to move towards [immutable infrastructure](https://fugue.co/oreilly/),
    10  where the service and its infrastructure requirements are treated as a logical unit.
    11  
    12  For instance, consider the case where two developers are working in the same AWS account.
    13  
    14  - Developer 1 is working on analyzing text documents.
    15    - Their lambda code is triggered in response to uploading sample text documents to S3.
    16  - Developer 2 is working on image recognition.
    17    - Their lambda code is triggered in response to uploading sample images to S3.
    18  
    19  {{< mermaid >}}
    20  graph LR
    21    sharedBucket[S3 Bucket]
    22  
    23    dev1Lambda[Dev1 LambdaCode]
    24    dev2Lambda[Dev2 LambdaCode]
    25  
    26    sharedBucket --> dev1Lambda
    27    sharedBucket --> dev2Lambda
    28  {{< /mermaid >}}
    29  
    30  Using a shared, externally provisioned S3 bucket has several impacts:
    31  
    32    * Adding conditionals in each lambda codebase to scope valid processing targets.
    33    * Ambiguity regarding which codebase handled an event.
    34    * Infrastructure ownership/lifespan management.  When a service is decommissioned, its infrastructure requirements may be automatically decommissioned as well.
    35      - Eg, "Is this S3 bucket in use by any service?".
    36    * Overly permissive IAM roles due to static Arns.
    37      - Eg, "Arn hugging".
    38    * Contention updating the shared bucket's [notification configuration](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putBucketNotificationConfiguration-property).
    39  
    40  Alternatively, each developer could provision and manage disjoint topologies:
    41  
    42  {{< mermaid >}}
    43  graph LR
    44    dev1S3Bucket[Dev1 S3 Bucket]
    45    dev1Lambda[Dev1 LambdaCode]
    46  
    47    dev2S3Bucket[Dev2 S3 Bucket]
    48    dev2Lambda[Dev2 LambdaCode]
    49  
    50    dev1S3Bucket --> dev1Lambda
    51    dev2S3Bucket --> dev2Lambda
    52  {{< /mermaid >}}
    53  
    54  Enabling each developer to create other AWS resources also means more complex topologies can be expressed.  These topologies can benefit from CloudWatch monitoring (eg, [per-Lambda Metrics](http://docs.aws.amazon.com/lambda/latest/dg/monitoring-functions-metrics.html) ) without the need to add [custom metrics](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/publishingMetrics.html).
    55  
    56  {{< mermaid >}}
    57  graph LR
    58    dev1S3Bucket[Dev1 S3 Bucket]
    59    dev1Lambda[Dev1 LambdaCode]
    60  
    61    dev2S3Bucket[Dev2 S3 Images Bucket]
    62    dev2PNGLambda[Dev2 PNG LambdaCode]
    63    dev2JPGLambda[Dev2 JPEG LambdaCode]
    64    dev2TIFFLambda[Dev2 TIFF LambdaCode]
    65    dev2S3VideoBucket[Dev2 VideoBucket]
    66    dev2VideoLambda[Dev2 Video LambdaCode]
    67  
    68    dev1S3Bucket --> dev1Lambda
    69    dev2S3Bucket -->|SuffixFilter=*.PNG|dev2PNGLambda
    70    dev2S3Bucket -->|SuffixFilter=*.JPEG,*.JPG|dev2JPGLambda
    71    dev2S3Bucket -->|SuffixFilter=*.TIFF|dev2TIFFLambda
    72    dev2S3VideoBucket -->dev2VideoLambda
    73  {{< /mermaid >}}
    74  
    75  Sparta supports Dynamic Resources via [TemplateDecoratorHandler](https://godoc.org/github.com/mweagle/Sparta#TemplateDecoratorHandler) satisfying
    76  types.
    77  
    78  # Template Decorator Handler
    79  
    80  A template decorator is a **go** interface:
    81  
    82  ```go
    83  type TemplateDecoratorHandler interface {
    84  	DecorateTemplate(serviceName string,
    85  		lambdaResourceName string,
    86  		lambdaResource gocf.LambdaFunction,
    87  		resourceMetadata map[string]interface{},
    88  		S3Bucket string,
    89  		S3Key string,
    90  		buildID string,
    91  		template *gocf.Template,
    92  		context map[string]interface{},
    93  		logger *logrus.Logger) error
    94  }
    95  ```
    96  
    97  Clients use [go-cloudformation](https://godoc.org/github.com/mweagle/go-cloudformation)
    98  types for CloudFormation resources and  `template.AddResource` to add them to
    99  the `*template` parameter.  After a decorator is invoked, Sparta also verifies that
   100  the user-supplied function did not add entities that that collide with the
   101  internally-generated ones.
   102  
   103  ## Unique Resource Names
   104  
   105  CloudFormation uses [Logical IDs](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html)
   106  as resource key names.
   107  
   108  To minimize collision likelihood, Sparta publishes [CloudFormationResourceName(prefix, ...parts)](https://godoc.org/github.com/mweagle/Sparta#CloudFormationResourceName) to generate compliant identifiers.  To produce content-based hash values, callers can provide a non-empty set of values as the `...parts` variadic argument.  This produces stable identifiers across Sparta execution (which may affect availability during updates).
   109  
   110  When called with only a single value (eg: `CloudFormationResourceName("myResource")`), Sparta will return a random resource name that is **NOT** stable across executions.
   111  
   112  # Example - S3 Bucket
   113  
   114  Let's work through an example to make things a bit more concrete.  We have the following requirements:
   115  
   116    * Our lambda function needs a immutable-infrastructure compliant S3 bucket
   117    * Our lambda function should be notified when items are created or deleted from the bucket
   118    * Our lambda function must be able to access the contents in the bucket (not shown below)
   119  
   120  ## Lambda Function
   121  
   122  To start with, we'll need a Sparta lambda function to expose:
   123  
   124  ```go
   125  import (
   126    awsLambdaEvents "github.com/aws/aws-lambda-go/events"
   127  )
   128  func echoS3DynamicBucketEvent(ctx context.Context,
   129    s3Event awsLambdaEvents.S3Event) (*awsLambdaEvents.S3Event, error) {
   130  	logger, _ := ctx.Value(sparta.ContextKeyRequestLogger).(*logrus.Entry)
   131  	discoveryInfo, discoveryInfoErr := sparta.Discover()
   132  	logger.WithFields(logrus.Fields{
   133  		"Event":        s3Event,
   134  		"Discovery":    discoveryInfo,
   135  		"DiscoveryErr": discoveryInfoErr,
   136  	}).Info("Event received")
   137  	return &s3Event, nil
   138  }
   139  ```
   140  
   141  For brevity our demo function doesn't access the S3 bucket objects.
   142  To support that please see the [sparta.Discover](/reference/discovery) functionality.
   143  
   144  ## S3 Resource Name
   145  
   146  The next thing we need is a _Logical ID_ for our bucket:
   147  
   148  ```go
   149  s3BucketResourceName := sparta.CloudFormationResourceName("S3DynamicBucket", "myServiceBucket")
   150  ```
   151  
   152  ## Sparta Integration
   153  
   154  With these two values we're ready to get started building up the lambda function:
   155  
   156  ```go
   157  lambdaFn, _ := sparta.NewAWSLambda(sparta.LambdaName(echoS3DynamicBucketEvent),
   158    echoS3DynamicBucketEvent,
   159    sparta.IAMRoleDefinition{})
   160  ```
   161  
   162  The open issue is how to publish the CloudFormation-defined S3 Arn to the `compile`-time application.  Our lambda function needs to provide both:
   163  
   164    * [IAMRolePrivilege](https://godoc.org/github.com/mweagle/Sparta#IAMRolePrivilege) values that reference the (as yet) undefined Arn.
   165    * [S3Permission](https://godoc.org/github.com/mweagle/Sparta#S3Permission) values to configure our lambda's event triggers on the (as yet) undefined Arn.
   166  
   167  The missing piece is [gocf.Ref()](https://godoc.org/github.com/crewjam/go-cloudformation#Ref), whose single argument is the _Logical ID_ of the S3 resource we'll be inserting in the decorator call.
   168  
   169  ### Dynamic IAM Role Privilege
   170  
   171  The `IAMRolePrivilege` struct references the dynamically assigned S3 Arn as follows:
   172  
   173  ```go
   174  
   175  lambdaFn.Permissions = append(lambdaFn.Permissions, sparta.S3Permission{
   176    BasePermission: sparta.BasePermission{
   177      SourceArn: gocf.Ref(s3BucketResourceName),
   178    },
   179    Events: []string{"s3:ObjectCreated:*", "s3:ObjectRemoved:*"},
   180  })
   181  lambdaFn.DependsOn = append(lambdaFn.DependsOn, s3BucketResourceName)
   182  ```
   183  
   184  ### Dynamic S3 Permissions
   185  
   186  The `S3Permission` struct also requires the dynamic Arn, to which it will append `"/*"` to enable object read access.
   187  
   188  ```go
   189  
   190  lambdaFn.RoleDefinition.Privileges = append(lambdaFn.RoleDefinition.Privileges,
   191    sparta.IAMRolePrivilege{
   192      Actions:  []string{"s3:GetObject", "s3:HeadObject"},
   193      Resource: spartaCF.S3AllKeysArnForBucket(gocf.Ref(s3BucketResourceName)),
   194    })
   195  ```
   196  
   197  The `spartaCF.S3AllKeysArnForBucket` call is a convenience wrapper around [gocf.Join](https://godoc.org/github.com/crewjam/go-cloudformation#Join) to generate the concatenated, dynamic Arn expression.
   198  
   199  ## S3 Resource Insertion
   200  
   201  All that's left to do is actually insert the S3 resource in our decorator:
   202  
   203  ```go
   204  s3Decorator := func(serviceName string,
   205    lambdaResourceName string,
   206    lambdaResource gocf.LambdaFunction,
   207    resourceMetadata map[string]interface{},
   208    S3Bucket string,
   209    S3Key string,
   210    buildID string,
   211    template *gocf.Template,
   212    context map[string]interface{},
   213    logger *logrus.Logger) error {
   214    cfResource := template.AddResource(s3BucketResourceName, &gocf.S3Bucket{
   215      AccessControl: gocf.String("PublicRead"),
   216      Tags: &gocf.TagList{gocf.Tag{
   217        Key:   gocf.String("SpecialKey"),
   218        Value: gocf.String("SpecialValue"),
   219      },
   220      },
   221    })
   222    cfResource.DeletionPolicy = "Delete"
   223    return nil
   224  }
   225  lambdaFn.Decorators = []sparta.TemplateDecoratorHandler{
   226    sparta.TemplateDecoratorHookFunc(s3Decorator),
   227  }
   228  ```
   229  
   230  ### Dependencies
   231  
   232  In reality, we shouldn't even attempt to create the AWS Lambda function if the S3 bucket creation fails.  As application developers, we can help CloudFormation sequence infrastructure operations by stating this hard dependency on the S3 bucket via the [DependsOn](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html) attribute:
   233  
   234  ```go
   235  lambdaFn.DependsOn = append(lambdaFn.DependsOn, s3BucketResourceName)
   236  ```
   237  
   238  ## Code Listing
   239  
   240  Putting everything together, our Sparta lambda function with dynamic infrastructure is listed below.
   241  
   242  ```go
   243  
   244  s3BucketResourceName := sparta.CloudFormationResourceName("S3DynamicBucket")
   245  lambdaFn, _ := sparta.NewAWSLambda(sparta.LambdaName(echoS3DynamicBucketEvent),
   246    echoS3DynamicBucketEvent,
   247    sparta.IAMRoleDefinition{})
   248  
   249  // Our lambda function requires the S3 bucket
   250  lambdaFn.DependsOn = append(lambdaFn.DependsOn, s3BucketResourceName)
   251  
   252  // Add a permission s.t. the lambda function could read from the S3 bucket
   253  lambdaFn.RoleDefinition.Privileges = append(lambdaFn.RoleDefinition.Privileges,
   254    sparta.IAMRolePrivilege{
   255      Actions:  []string{"s3:GetObject",
   256                         "s3:HeadObject"},
   257      Resource: spartaCF.S3AllKeysArnForBucket(gocf.Ref(s3BucketResourceName)),
   258    })
   259  
   260  // Configure the S3 event source
   261  lambdaFn.Permissions = append(lambdaFn.Permissions, sparta.S3Permission{
   262    BasePermission: sparta.BasePermission{
   263      SourceArn: gocf.Ref(s3BucketResourceName),
   264    },
   265    Events: []string{"s3:ObjectCreated:*",
   266                     "s3:ObjectRemoved:*"},
   267  })
   268  
   269  // Actually add the resource
   270  lambdaFn.Decorator = func(lambdaResourceName string,
   271                            lambdaResource gocf.LambdaFunction,
   272                            template *gocf.Template,
   273                            logger *logrus.Logger) error {
   274    cfResource := template.AddResource(s3BucketResourceName, &gocf.S3Bucket{
   275      AccessControl: gocf.String("PublicRead"),
   276    })
   277    cfResource.DeletionPolicy = "Delete"
   278    return nil
   279  }
   280  ```
   281  
   282  ## Wrapping Up
   283  
   284  Sparta provides an opportunity to bring infrastructure management into the
   285  application programming model.  It's still possible to use literal Arn strings,
   286  but the ability to include other infrastructure requirements brings a service
   287  closer to being self-contained and more operationally sustainable.
   288  
   289  # Notes
   290    * The `echoS3DynamicBucketEvent` function can also access the bucket Arn via [sparta.Discover](/reference/discovery).
   291    * See the [DeletionPolicy](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html) documentation regarding S3 management.
   292    * CloudFormation resources also publish [other outputs](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html) that can be retrieved via [gocf.GetAtt](https://godoc.org/github.com/crewjam/go-cloudformation#GetAtt).
   293    * `go-cloudformation` exposes [gocf.Join](https://godoc.org/github.com/mweagle/go-cloudformation#Join) to create compound, dynamic expressions.
   294      - See the CloudWatch docs on [Fn::Join](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html) for more information.