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.