func annotateDiscoveryInfo(template *gocf.Template, logger *logrus.Logger) *gocf.Template { for eachResourceID, eachResource := range template.Resources { // Only apply this to lambda functions if eachResource.Properties.CfnResourceType() == "AWS::Lambda::Function" { // Update the metdata with a reference to the output of each // depended on item... for _, eachDependsKey := range eachResource.DependsOn { dependencyOutputs, _ := outputsForResource(template, eachDependsKey, logger) if nil != dependencyOutputs && len(dependencyOutputs) != 0 { logger.WithFields(logrus.Fields{ "Resource": eachDependsKey, "DependsOn": eachResource.DependsOn, "Outputs": dependencyOutputs, }).Debug("Resource metadata") safeMetadataInsert(eachResource, eachDependsKey, dependencyOutputs) } } // Also include standard AWS outputs at a resource level if a lambda // needs to self-discover other resources safeMetadataInsert(eachResource, TagLogicalResourceID, gocf.String(eachResourceID)) safeMetadataInsert(eachResource, TagStackRegion, gocf.Ref("AWS::Region")) safeMetadataInsert(eachResource, TagStackID, gocf.Ref("AWS::StackId")) safeMetadataInsert(eachResource, TagStackName, gocf.Ref("AWS::StackName")) } } return template }
func parseFnJoinExpr(data map[string]interface{}) (*gocf.StringExpr, error) { if len(data) <= 0 { return nil, fmt.Errorf("FnJoinExpr data is empty") } for eachKey, eachValue := range data { switch eachKey { case "Ref": return gocf.Ref(eachValue.(string)).String(), nil case "Fn::GetAtt": attrValues, attrValuesErr := toExpressionSlice(eachValue) if nil != attrValuesErr { return nil, attrValuesErr } if len(attrValues) != 2 { return nil, fmt.Errorf("Invalid params for Fn::GetAtt: %s", eachValue) } return gocf.GetAtt(attrValues[0], attrValues[1]).String(), nil case "Fn::FindInMap": attrValues, attrValuesErr := toExpressionSlice(eachValue) if nil != attrValuesErr { return nil, attrValuesErr } if len(attrValues) != 3 { return nil, fmt.Errorf("Invalid params for Fn::FindInMap: %s", eachValue) } return gocf.FindInMap(attrValues[0], gocf.String(attrValues[1]), gocf.String(attrValues[2])), nil } } return nil, fmt.Errorf("Unsupported AWS Function detected: %#v", data) }
func appendDynamicSNSLambda(api *sparta.API, lambdaFunctions []*sparta.LambdaAWSInfo) []*sparta.LambdaAWSInfo { snsTopicName := sparta.CloudFormationResourceName("SNSDynamicTopic") lambdaFn := sparta.NewLambda(sparta.IAMRoleDefinition{}, echoDynamicSNSEvent, nil) lambdaFn.Permissions = append(lambdaFn.Permissions, sparta.SNSPermission{ BasePermission: sparta.BasePermission{ SourceArn: gocf.Ref(snsTopicName), }, }) lambdaFn.Decorator = func(serviceName string, lambdaResourceName string, lambdaResource gocf.LambdaFunction, resourceMetadata map[string]interface{}, S3Bucket string, S3Key string, buildID string, template *gocf.Template, context map[string]interface{}, logger *logrus.Logger) error { template.AddResource(snsTopicName, &gocf.SNSTopic{ DisplayName: gocf.String("Sparta Application SNS topic"), }) return nil } return append(lambdaFunctions, lambdaFn) }
func appendDynamicS3BucketLambda(api *sparta.API, lambdaFunctions []*sparta.LambdaAWSInfo) []*sparta.LambdaAWSInfo { s3BucketResourceName := sparta.CloudFormationResourceName("S3DynamicBucket") lambdaFn := sparta.NewLambda(sparta.IAMRoleDefinition{}, echoS3DynamicBucketEvent, nil) lambdaFn.Permissions = append(lambdaFn.Permissions, sparta.S3Permission{ BasePermission: sparta.BasePermission{ SourceArn: gocf.Ref(s3BucketResourceName), }, Events: []string{"s3:ObjectCreated:*", "s3:ObjectRemoved:*"}, }) lambdaFn.DependsOn = append(lambdaFn.DependsOn, s3BucketResourceName) // Add permission s.t. the lambda function could read from the S3 bucket lambdaFn.RoleDefinition.Privileges = append(lambdaFn.RoleDefinition.Privileges, sparta.IAMRolePrivilege{ Actions: []string{"s3:GetObject", "s3:HeadObject"}, Resource: spartaCF.S3AllKeysArnForBucket(gocf.Ref(s3BucketResourceName)), }) lambdaFn.Decorator = func(serviceName string, lambdaResourceName string, lambdaResource gocf.LambdaFunction, resourceMetadata map[string]interface{}, S3Bucket string, S3Key string, buildID string, template *gocf.Template, context map[string]interface{}, logger *logrus.Logger) error { cfResource := template.AddResource(s3BucketResourceName, &gocf.S3Bucket{ AccessControl: gocf.String("PublicRead"), Tags: []gocf.ResourceTag{ gocf.ResourceTag{ Key: gocf.String("SpecialKey"), Value: gocf.String("SpecialValue"), }, }, }) cfResource.DeletionPolicy = "Delete" return nil } return append(lambdaFunctions, lambdaFn) }
// NewMessageBodyStorageResource provisions a new S3 bucket to store message body // content. func (perm *SESPermission) NewMessageBodyStorageResource(bucketLogicalName string) (*MessageBodyStorage, error) { if len(bucketLogicalName) <= 0 { return nil, errors.New("NewMessageBodyStorageResource requires a unique, non-empty `bucketLogicalName` parameter ") } store := &MessageBodyStorage{ logicalBucketName: bucketLogicalName, } store.cloudFormationS3BucketResourceName = CloudFormationResourceName("SESMessageStoreBucket", bucketLogicalName) store.bucketNameExpr = gocf.Ref(store.cloudFormationS3BucketResourceName).String() return store, nil }
func makeTemplate() *cf.Template { t := cf.NewTemplate() t.Description = "example production infrastructure" t.Parameters["DnsName"] = &cf.Parameter{ Description: "The top level DNS name for the infrastructure", Type: "String", Default: "preview.example.io", } t.AddResource("ServerLoadBalancer", cf.ElasticLoadBalancingLoadBalancer{ ConnectionDrainingPolicy: &cf.ElasticLoadBalancingConnectionDrainingPolicy{ Enabled: cf.Bool(true), Timeout: cf.Integer(30), }, CrossZone: cf.Bool(true), HealthCheck: &cf.ElasticLoadBalancingHealthCheck{ HealthyThreshold: cf.String("2"), Interval: cf.String("60"), Target: cf.String("HTTP:80/"), Timeout: cf.String("5"), UnhealthyThreshold: cf.String("2"), }, Listeners: &cf.ElasticLoadBalancingListenerList{ cf.ElasticLoadBalancingListener{ InstancePort: cf.String("8000"), InstanceProtocol: cf.String("TCP"), LoadBalancerPort: cf.String("443"), Protocol: cf.String("SSL"), SSLCertificateId: cf.Join("", cf.String("arn:aws:iam::"), cf.Ref("AWS::AccountID"), cf.String(":server-certificate/"), cf.Ref("DnsName")), }, }, Policies: &cf.ElasticLoadBalancingPolicyList{ cf.ElasticLoadBalancingPolicy{ PolicyName: cf.String("EnableProxyProtocol"), PolicyType: cf.String("ProxyProtocolPolicyType"), Attributes: []map[string]interface{}{ map[string]interface{}{ "Name": "ProxyProtocol", "Value": "true", }, }, InstancePorts: []int{8000}, }, }, Subnets: cf.StringList( cf.Ref("VpcSubnetA"), cf.Ref("VpcSubnetB"), cf.Ref("VpcSubnetC"), ), SecurityGroups: cf.StringList(cf.Ref("LoadBalancerSecurityGroup")), }) return t }
func annotateCodePipelineEnvironments(lambdaAWSInfo *LambdaAWSInfo, logger *logrus.Logger) { if nil != codePipelineEnvironments { if nil == lambdaAWSInfo.Options { lambdaAWSInfo.Options = defaultLambdaFunctionOptions() } if nil == lambdaAWSInfo.Options.Environment { lambdaAWSInfo.Options.Environment = make(map[string]*gocf.StringExpr, 0) } for _, eachEnvironment := range codePipelineEnvironments { logger.WithFields(logrus.Fields{ "Environment": eachEnvironment, "LambdaFunction": lambdaAWSInfo.lambdaFunctionName(), }).Debug("Annotating Lambda environment for CodePipeline") for eachKey := range eachEnvironment { lambdaAWSInfo.Options.Environment[eachKey] = gocf.Ref(eachKey).String() } } } }
func outputsForResource(template *gocf.Template, logicalResourceName string, logger *logrus.Logger) (map[string]interface{}, error) { item, ok := template.Resources[logicalResourceName] if !ok { return nil, nil } outputs := make(map[string]interface{}, 0) attrs, exists := cloudformationTypeMapDiscoveryOutputs[item.Properties.ResourceType()] if exists { outputs["Ref"] = gocf.Ref(logicalResourceName).String() outputs[TagResourceType] = item.Properties.ResourceType() for _, eachAttr := range attrs { outputs[eachAttr] = gocf.GetAtt(logicalResourceName, eachAttr) } // Any tags? r := reflect.ValueOf(item.Properties) tagsField := reflect.Indirect(r).FieldByName("Tags") if tagsField.IsValid() && !tagsField.IsNil() { outputs["Tags"] = tagsField.Interface() } } if len(outputs) != 0 { logger.WithFields(logrus.Fields{ "ResourceName": logicalResourceName, "Outputs": outputs, }).Debug("Resource Outputs") } return outputs, nil }
} } func init() { gocf.RegisterCustomResourceProvider(customResourceProvider) rand.Seed(time.Now().Unix()) } //////////////////////////////////////////////////////////////////////////////// // Variables //////////////////////////////////////////////////////////////////////////////// // Represents the CloudFormation Arn of this stack, referenced // in CommonIAMStatements var cloudFormationThisStackArn = []gocf.Stringable{gocf.String("arn:aws:cloudformation:"), gocf.Ref("AWS::Region").String(), gocf.String(":"), gocf.Ref("AWS::AccountId").String(), gocf.String(":stack/"), gocf.Ref("AWS::StackName").String(), gocf.String("/*")} // CommonIAMStatements defines common IAM::Role Policy Statement values for different AWS // service types. See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namespaces // for names. // http://docs.aws.amazon.com/lambda/latest/dg/monitoring-functions.html // for more information. var CommonIAMStatements = struct { Core []spartaIAM.PolicyStatement VPC []spartaIAM.PolicyStatement DynamoDB []spartaIAM.PolicyStatement
// export marshals the API data to a CloudFormation compatible representation func (api *API) export(serviceName string, session *session.Session, S3Bucket string, S3Key string, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, noop bool, logger *logrus.Logger) error { apiGatewayResourceNameForPath := func(fullPath string) string { pathParts := strings.Split(fullPath, "/") return CloudFormationResourceName("%sResource", pathParts[0], fullPath) } apiGatewayResName := CloudFormationResourceName("APIGateway", api.name) // Create an API gateway entry apiGatewayRes := &gocf.ApiGatewayRestApi{ Description: gocf.String(api.Description), FailOnWarnings: gocf.Bool(false), Name: gocf.String(api.name), } if "" != api.CloneFrom { apiGatewayRes.CloneFrom = gocf.String(api.CloneFrom) } if "" == api.Description { apiGatewayRes.Description = gocf.String(fmt.Sprintf("%s RestApi", serviceName)) } else { apiGatewayRes.Description = gocf.String(api.Description) } template.AddResource(apiGatewayResName, apiGatewayRes) apiGatewayRestAPIID := gocf.Ref(apiGatewayResName) // List of all the method resources we're creating s.t. the // deployment can DependOn them optionsMethodPathMap := make(map[string]bool) var apiMethodCloudFormationResources []string for eachResourceMethodKey, eachResourceDef := range api.resources { // First walk all the user resources and create intermediate paths // to repreesent all the resources var parentResource *gocf.StringExpr pathParts := strings.Split(strings.TrimLeft(eachResourceDef.pathPart, "/"), "/") pathAccumulator := []string{"/"} for index, eachPathPart := range pathParts { pathAccumulator = append(pathAccumulator, eachPathPart) resourcePathName := apiGatewayResourceNameForPath(strings.Join(pathAccumulator, "/")) if _, exists := template.Resources[resourcePathName]; !exists { cfResource := &gocf.ApiGatewayResource{ RestApiId: apiGatewayRestAPIID.String(), PathPart: gocf.String(eachPathPart), } if index <= 0 { cfResource.ParentId = gocf.GetAtt(apiGatewayResName, "RootResourceId") } else { cfResource.ParentId = parentResource } template.AddResource(resourcePathName, cfResource) } parentResource = gocf.Ref(resourcePathName).String() } // Add the lambda permission apiGatewayPermissionResourceName := CloudFormationResourceName("APIGatewayLambdaPerm", eachResourceMethodKey) lambdaInvokePermission := &gocf.LambdaPermission{ Action: gocf.String("lambda:InvokeFunction"), FunctionName: gocf.GetAtt(eachResourceDef.parentLambda.logicalName(), "Arn"), Principal: gocf.String(APIGatewayPrincipal), } template.AddResource(apiGatewayPermissionResourceName, lambdaInvokePermission) // BEGIN CORS - OPTIONS verb // CORS is API global, but it's possible that there are multiple different lambda functions // that are handling the same HTTP resource. In this case, track whether we've already created an // OPTIONS entry for this path and only append iff this is the first time through if api.CORSEnabled { methodResourceName := CloudFormationResourceName(fmt.Sprintf("%s-OPTIONS", eachResourceDef.pathPart), eachResourceDef.pathPart) _, resourceExists := optionsMethodPathMap[methodResourceName] if !resourceExists { template.AddResource(methodResourceName, corsOptionsGatewayMethod(apiGatewayRestAPIID, parentResource)) apiMethodCloudFormationResources = append(apiMethodCloudFormationResources, methodResourceName) optionsMethodPathMap[methodResourceName] = true } } // END CORS - OPTIONS verb // BEGIN - user defined verbs for eachMethodName, eachMethodDef := range eachResourceDef.Methods { apiGatewayMethod := &gocf.ApiGatewayMethod{ HttpMethod: gocf.String(eachMethodName), AuthorizationType: gocf.String("NONE"), ResourceId: parentResource.String(), RestApiId: apiGatewayRestAPIID.String(), Integration: &gocf.APIGatewayMethodIntegration{ IntegrationHttpMethod: gocf.String("POST"), Type: gocf.String("AWS"), RequestTemplates: defaultRequestTemplates(), Uri: gocf.Join("", gocf.String("arn:aws:apigateway:"), gocf.Ref("AWS::Region"), gocf.String(":lambda:path/2015-03-31/functions/"), gocf.GetAtt(eachResourceDef.parentLambda.logicalName(), "Arn"), gocf.String("/invocations")), }, } if len(eachMethodDef.Parameters) != 0 { requestParams := make(map[string]string, 0) for eachKey, eachBool := range eachMethodDef.Parameters { requestParams[eachKey] = fmt.Sprintf("%t", eachBool) } apiGatewayMethod.RequestParameters = requestParams } // Add the integration response RegExps apiGatewayMethod.Integration.IntegrationResponses = integrationResponses(eachMethodDef.Integration.Responses, api.CORSEnabled) // Add outbound method responses apiGatewayMethod.MethodResponses = methodResponses(eachMethodDef.Responses, api.CORSEnabled) prefix := fmt.Sprintf("%s%s", eachMethodDef.httpMethod, eachResourceMethodKey) methodResourceName := CloudFormationResourceName(prefix, eachResourceMethodKey, serviceName) res := template.AddResource(methodResourceName, apiGatewayMethod) res.DependsOn = append(res.DependsOn, apiGatewayPermissionResourceName) apiMethodCloudFormationResources = append(apiMethodCloudFormationResources, methodResourceName) } } // END if nil != api.stage { // Is the stack already deployed? stageName := api.stage.name stageInfo, stageInfoErr := apiStageInfo(api.name, stageName, session, noop, logger) if nil != stageInfoErr { return stageInfoErr } if nil == stageInfo { // Use a stable identifier so that we can update the existing deployment apiDeploymentResName := CloudFormationResourceName("APIGatewayDeployment", serviceName) apiDeployment := &gocf.ApiGatewayDeployment{ Description: gocf.String(api.stage.Description), RestApiId: apiGatewayRestAPIID.String(), StageName: gocf.String(stageName), StageDescription: &gocf.APIGatewayDeploymentStageDescription{ StageName: gocf.String(api.stage.name), Description: gocf.String(api.stage.Description), Variables: api.stage.Variables, }, } if api.stage.CacheClusterEnabled { apiDeployment.StageDescription.CacheClusterEnabled = gocf.Bool(api.stage.CacheClusterEnabled) } if api.stage.CacheClusterSize != "" { apiDeployment.StageDescription.CacheClusterSize = gocf.String(api.stage.CacheClusterSize) } deployment := template.AddResource(apiDeploymentResName, apiDeployment) deployment.DependsOn = append(deployment.DependsOn, apiMethodCloudFormationResources...) deployment.DependsOn = append(deployment.DependsOn, apiGatewayResName) } else { newDeployment := &gocf.ApiGatewayDeployment{ Description: gocf.String("Sparta deploy"), RestApiId: apiGatewayRestAPIID.String(), StageName: gocf.String(stageName), } // Use an unstable ID s.t. we can actually create a new deployment event. Not sure how this // is going to work with deletes... deploymentResName := CloudFormationResourceName("APIGatewayDeployment") deployment := template.AddResource(deploymentResName, newDeployment) deployment.DependsOn = append(deployment.DependsOn, apiMethodCloudFormationResources...) deployment.DependsOn = append(deployment.DependsOn, apiGatewayResName) } template.Outputs[OutputAPIGatewayURL] = &gocf.Output{ Description: "API Gateway URL", Value: gocf.Join("", gocf.String("https://"), apiGatewayRestAPIID, gocf.String(".execute-api."), gocf.Ref("AWS::Region"), gocf.String(".amazonaws.com/"), gocf.String(stageName)), } } return nil }
func (perm CloudWatchEventsPermission) export(serviceName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // Tell the user we're ignoring any Arns provided, since it doesn't make sense for // this. if nil != perm.BasePermission.SourceArn && perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...).String() != wildcardArn.String() { logger.WithFields(logrus.Fields{ "Arn": perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...), }).Warn("CloudWatchEvents do not support literal ARN values") } arnPermissionForRuleName := func(ruleName string) *gocf.StringExpr { return gocf.Join("", gocf.String("arn:aws:events:"), gocf.Ref("AWS::Region"), gocf.String(":"), gocf.Ref("AWS::AccountId"), gocf.String(":rule/"), gocf.String(ruleName)) } // First thing we need to do is uniqueify the rule names s.t. we prevent // collisions with other stacks. globallyUniqueRules := make(map[string]CloudWatchEventsRule, len(perm.Rules)) for eachRuleName, eachDefinition := range perm.Rules { uniqueRuleName := CloudFormationResourceName(eachRuleName, lambdaLogicalCFResourceName, serviceName) // Trim it... if len(eachDefinition.Description) <= 0 { eachDefinition.Description = fmt.Sprintf("%s CloudWatch Events rule for service: %s", eachRuleName, serviceName) } globallyUniqueRules[uniqueRuleName] = eachDefinition } // Self test - there should only be 1 element since we're only ever configuring // the same AWS principal service. If we end up with multiple configuration resource names // it means that the stable resource name logic is broken configurationResourceNames := make(map[string]int, 0) var dependsOn []string for eachRuleName := range globallyUniqueRules { basePerm := BasePermission{ SourceArn: arnPermissionForRuleName(eachRuleName), } dependOn, err := basePerm.export(CloudWatchEventsPrincipal, cloudformationEventsSourceArnParts, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } dependsOn = append(dependsOn, dependOn) // Ensure the configurator for this ARNs sourceArnExpression := basePerm.sourceArnExpr(cloudformationEventsSourceArnParts...) // Make sure the custom lambda that manages CloudWatch Events is provisioned. configuratorResName, err := ensureConfiguratorLambdaResource(CloudWatchEventsPrincipal, sourceArnExpression, []string{}, template, S3Bucket, S3Key, logger) if nil != err { return "", err } configurationResourceNames[configuratorResName] = 1 } // Although we ensured multiple configuration resources, they were all for the // same AWS principal. We're only supposed to get a single name back. if len(configurationResourceNames) > 1 { return "", fmt.Errorf("Multiple configuration resources detected: %#v", configurationResourceNames) } else if len(configurationResourceNames) == 0 { return "", fmt.Errorf("CloudWatchEvent configuration provider failed") } // Insert the invocation for eachConfigResource := range configurationResourceNames { ////////////////////////////////////////////////////////////////////////////// // And finally the custom resource forwarder newResource, err := newCloudFormationResource("Custom::SpartaCloudWatchEventsPermission", logger) if nil != err { return "", err } customResource := newResource.(*cloudformationCloudWatchEventsPermissionResource) customResource.ServiceToken = gocf.GetAtt(eachConfigResource, "Arn") customResource.Rules = globallyUniqueRules customResource.LambdaTarget = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn") // Name? resourceInvokerName := CloudFormationResourceName("ConfigCloudWatchEvents", lambdaLogicalCFResourceName, perm.BasePermission.SourceAccount) // Add it cfResource := template.AddResource(resourceInvokerName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) } return "", nil }
func (storage *MessageBodyStorage) export(serviceName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { if "" != storage.cloudFormationS3BucketResourceName { s3Bucket := &gocf.S3Bucket{ Tags: []gocf.ResourceTag{ gocf.ResourceTag{ Key: gocf.String("sparta:logicalBucketName"), Value: gocf.String(storage.logicalBucketName), }, }, } cfResource := template.AddResource(storage.cloudFormationS3BucketResourceName, s3Bucket) cfResource.DeletionPolicy = "Retain" lambdaResource, _ := template.Resources[lambdaLogicalCFResourceName] if nil != lambdaResource { safeAppendDependency(lambdaResource, storage.cloudFormationS3BucketResourceName) } logger.WithFields(logrus.Fields{ "LogicalResourceName": storage.cloudFormationS3BucketResourceName, }).Info("Service will orphan S3 Bucket on deletion") // Save the output template.Outputs[storage.cloudFormationS3BucketResourceName] = &gocf.Output{ Description: "SES Message Body Bucket", Value: gocf.Ref(storage.cloudFormationS3BucketResourceName), } } // Add the S3 Access policy s3BodyStoragePolicy := &gocf.S3BucketPolicy{ Bucket: storage.bucketNameExpr, PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": []ArbitraryJSONObject{ { "Sid": "PermitSESServiceToSaveEmailBody", "Effect": "Allow", "Principal": ArbitraryJSONObject{ "Service": "ses.amazonaws.com", }, "Action": []string{"s3:PutObjectAcl", "s3:PutObject"}, "Resource": gocf.Join("", gocf.String("arn:aws:s3:::"), storage.bucketNameExpr, gocf.String("/*")), "Condition": ArbitraryJSONObject{ "StringEquals": ArbitraryJSONObject{ "aws:Referer": gocf.Ref("AWS::AccountId"), }, }, }, }, }, } s3BucketPolicyResourceName := CloudFormationResourceName("SESMessageBodyBucketPolicy", fmt.Sprintf("%#v", storage.bucketNameExpr)) template.AddResource(s3BucketPolicyResourceName, s3BodyStoragePolicy) // Return the name of the bucket policy s.t. the configurator resource // is properly sequenced. The configurator will fail iff the Bucket Policies aren't // applied b/c the SES Rule Actions check PutObject access to S3 buckets return s3BucketPolicyResourceName, nil }
func ensureCloudFormationStack() workflowStep { return func(ctx *workflowContext) (workflowStep, error) { for _, eachEntry := range ctx.lambdaAWSInfos { err := eachEntry.export(ctx.serviceName, ctx.s3Bucket, ctx.s3LambdaZipKey, ctx.lambdaIAMRoleNameMap, ctx.cfTemplate, ctx.logger) if nil != err { return nil, err } } // If there's an API gateway definition, include the resources that provision it. Since this export will likely // generate outputs that the s3 site needs, we'll use a temporary outputs accumulator, pass that to the S3Site // if it's defined, and then merge it with the normal output map. apiGatewayTemplate := gocf.NewTemplate() if nil != ctx.api { err := ctx.api.export(ctx.s3Bucket, ctx.s3LambdaZipKey, ctx.lambdaIAMRoleNameMap, apiGatewayTemplate, ctx.logger) if nil == err { err = safeMergeTemplates(apiGatewayTemplate, ctx.cfTemplate, ctx.logger) } if nil != err { return nil, fmt.Errorf("Failed to export APIGateway template resources") } } // If there's a Site defined, include the resources the provision it if nil != ctx.s3SiteContext.s3Site { ctx.s3SiteContext.s3Site.export(ctx.s3Bucket, ctx.s3LambdaZipKey, ctx.s3SiteContext.s3SiteLambdaZipKey, apiGatewayTemplate.Outputs, ctx.lambdaIAMRoleNameMap, ctx.cfTemplate, ctx.logger) } // Save the output ctx.cfTemplate.Outputs[OutputSpartaHomeKey] = &gocf.Output{ Description: "Sparta Home", Value: gocf.String("http://gosparta.io"), } ctx.cfTemplate.Outputs[OutputSpartaVersionKey] = &gocf.Output{ Description: "Sparta Version", Value: gocf.String(SpartaVersion), } // Next pass - exchange outputs between dependencies. Lambda functions for _, eachResource := range ctx.cfTemplate.Resources { // Only apply this to lambda functions if eachResource.Properties.ResourceType() == "AWS::Lambda::Function" { // Update the metdata with a reference to the output of each // depended on item... for _, eachDependsKey := range eachResource.DependsOn { dependencyOutputs, _ := outputsForResource(ctx.cfTemplate, eachDependsKey, ctx.logger) if nil != dependencyOutputs && len(dependencyOutputs) != 0 { ctx.logger.WithFields(logrus.Fields{ "Resource": eachDependsKey, "DependsOn": eachResource.DependsOn, "Outputs": dependencyOutputs, }).Debug("Resource metadata") safeMetadataInsert(eachResource, eachDependsKey, dependencyOutputs) } } // Also include standard AWS outputs at a resource level if a lambda // needs to self-discover other resources safeMetadataInsert(eachResource, TagStackRegion, gocf.Ref("AWS::Region")) safeMetadataInsert(eachResource, TagStackID, gocf.Ref("AWS::StackId")) safeMetadataInsert(eachResource, TagStackName, gocf.Ref("AWS::StackName")) } } // Generate a complete CloudFormation template cfTemplate, err := json.Marshal(ctx.cfTemplate) if err != nil { ctx.logger.Error("Failed to Marshal CloudFormation template: ", err.Error()) return nil, err } // Upload the actual CloudFormation template to S3 to increase the template // size limit contentBody := string(cfTemplate) sanitizedServiceName := sanitizedName(ctx.serviceName) hash := sha1.New() hash.Write([]byte(contentBody)) s3keyName := fmt.Sprintf("%s-%s-cf.json", sanitizedServiceName, hex.EncodeToString(hash.Sum(nil))) uploadInput := &s3manager.UploadInput{ Bucket: &ctx.s3Bucket, Key: &s3keyName, ContentType: aws.String("application/json"), Body: strings.NewReader(contentBody), } formatted, err := json.MarshalIndent(contentBody, "", " ") if nil != err { return nil, err } ctx.logger.WithFields(logrus.Fields{ "Body": string(formatted), }).Debug("CloudFormation template body") if nil != ctx.templateWriter { io.WriteString(ctx.templateWriter, string(formatted)) } if ctx.noop { ctx.logger.WithFields(logrus.Fields{ "Bucket": ctx.s3Bucket, "Key": s3keyName, }).Info("Bypassing template upload & creation due to -n/-noop command line argument") } else { ctx.logger.Info("Uploading CloudFormation template") uploader := s3manager.NewUploader(ctx.awsSession) templateUploadResult, err := uploader.Upload(uploadInput) if nil != err { return nil, err } // Cleanup if there's a problem ctx.registerRollback(createS3RollbackFunc(ctx.awsSession, ctx.s3Bucket, s3keyName, ctx.noop)) // Be transparent ctx.logger.WithFields(logrus.Fields{ "URL": templateUploadResult.Location, }).Info("Template uploaded") stack, err := convergeStackState(templateUploadResult.Location, ctx) if nil != err { return nil, err } ctx.logger.WithFields(logrus.Fields{ "StackName": *stack.StackName, "StackId": *stack.StackId, "CreationTime": *stack.CreationTime, }).Info("Stack provisioned") } return nil, nil } }
// export marshals the API data to a CloudFormation compatible representation func (s3Site *S3Site) export(S3Bucket string, S3Key string, S3ResourcesKey string, apiGatewayOutputs map[string]*gocf.Output, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) error { websiteConfig := s3Site.WebsiteConfiguration if nil == websiteConfig { websiteConfig = &s3.WebsiteConfiguration{} } ////////////////////////////////////////////////////////////////////////////// // 1 - Create the S3 bucket. The "BucketName" property is empty s.t. // AWS will assign a unique one. if nil == websiteConfig.ErrorDocument { websiteConfig.ErrorDocument = &s3.ErrorDocument{ Key: aws.String("error.html"), } } if nil == websiteConfig.IndexDocument { websiteConfig.IndexDocument = &s3.IndexDocument{ Suffix: aws.String("index.html"), } } s3WebsiteConfig := &gocf.S3WebsiteConfigurationProperty{ ErrorDocument: gocf.String(aws.StringValue(websiteConfig.ErrorDocument.Key)), IndexDocument: gocf.String(aws.StringValue(websiteConfig.IndexDocument.Suffix)), } s3Bucket := &gocf.S3Bucket{ AccessControl: gocf.String("PublicRead"), WebsiteConfiguration: s3WebsiteConfig, } s3BucketResourceName := stableCloudformationResourceName("Site") cfResource := template.AddResource(s3BucketResourceName, s3Bucket) cfResource.DeletionPolicy = "Delete" template.Outputs[OutputS3SiteURL] = &gocf.Output{ Description: "S3 Website URL", Value: gocf.GetAtt(s3BucketResourceName, "WebsiteURL"), } // Represents the S3 ARN that is provisioned s3SiteBucketResourceValue := gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.Ref(s3BucketResourceName)) s3SiteBucketAllKeysResourceValue := gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.Ref(s3BucketResourceName), gocf.String("/*")) ////////////////////////////////////////////////////////////////////////////// // 2 - Add a bucket policy to enable anonymous access, as the PublicRead // canned ACL doesn't seem to do what is implied. // TODO - determine if this is needed or if PublicRead is being misued s3SiteBucketPolicy := &gocf.S3BucketPolicy{ Bucket: gocf.Ref(s3BucketResourceName).String(), PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": []ArbitraryJSONObject{ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": ArbitraryJSONObject{ "AWS": "*", }, "Action": "s3:GetObject", "Resource": s3SiteBucketAllKeysResourceValue, }, }, }, } s3BucketPolicyResourceName := stableCloudformationResourceName("S3SiteBucketPolicy") template.AddResource(s3BucketPolicyResourceName, s3SiteBucketPolicy) ////////////////////////////////////////////////////////////////////////////// // 3 - Create the IAM role for the lambda function // The lambda function needs to download the posted resource content, as well // as manage the S3 bucket that hosts the site. statements := CommonIAMStatements["core"] statements = append(statements, iamPolicyStatement{ Action: []string{"s3:ListBucket"}, Effect: "Allow", Resource: s3SiteBucketResourceValue, }) statements = append(statements, iamPolicyStatement{ Action: []string{"s3:DeleteObject", "s3:PutObject"}, Effect: "Allow", Resource: s3SiteBucketAllKeysResourceValue, }) statements = append(statements, iamPolicyStatement{ Action: []string{"s3:GetObject"}, Effect: "Allow", Resource: gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.String(S3Bucket), gocf.String("/"), gocf.String(S3ResourcesKey)), }) iamS3Role := &gocf.IAMRole{ AssumeRolePolicyDocument: AssumePolicyDocument, Policies: &gocf.IAMPoliciesList{ gocf.IAMPolicies{ ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": statements, }, gocf.String("S3SiteMgmnt"), }, }, } iamRoleName := stableCloudformationResourceName("S3SiteIAMRole") cfResource = template.AddResource(iamRoleName, iamS3Role) cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName) iamRoleRef := gocf.GetAtt(iamRoleName, "Arn") ////////////////////////////////////////////////////////////////////////////// // 4 - Create the lambda function definition that executes with the // dynamically provisioned IAM policy customResourceHandlerDef := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String("Manage static S3 site resources"), Handler: gocf.String(nodeJSHandlerName("s3Site")), Role: iamRoleRef, Runtime: gocf.String("nodejs"), Timeout: gocf.Integer(30), // Default is 128, but we're buffering everything in memory, in NodeJS MemorySize: gocf.Integer(256), } lambdaResourceName := stableCloudformationResourceName("S3SiteCreator") cfResource = template.AddResource(lambdaResourceName, customResourceHandlerDef) cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName, iamRoleName) ////////////////////////////////////////////////////////////////////////////// // 5 - Create the custom resource that invokes the site bootstrapper lambda to // actually populate the S3 with content customResourceName := stableCloudformationResourceName("S3SiteInvoker") newResource, err := newCloudFormationResource("Custom::SpartaS3SiteManager", logger) if nil != err { return err } customResource := newResource.(*cloudformationS3SiteManager) customResource.ServiceToken = gocf.GetAtt(lambdaResourceName, "Arn") customResource.TargetBucket = s3SiteBucketResourceValue customResource.SourceKey = gocf.String(S3ResourcesKey) customResource.SourceBucket = gocf.String(S3Bucket) customResource.APIGateway = apiGatewayOutputs cfResource = template.AddResource(customResourceName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, lambdaResourceName) return nil }
func (perm CloudWatchLogsPermission) export(serviceName string, lambdaFunctionDisplayName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // If there aren't any expressions to register with? if len(perm.Filters) <= 0 { return "", fmt.Errorf("CloudWatchLogsPermission for function %s does not specify any filters", lambdaFunctionDisplayName) } // The principal is region specific, so build that up... regionalPrincipal := gocf.Join(".", gocf.String("logs"), gocf.Ref("AWS::Region"), gocf.String("amazonaws.com")) // Tell the user we're ignoring any Arns provided, since it doesn't make sense for // this. if nil != perm.BasePermission.SourceArn && perm.BasePermission.sourceArnExpr(cloudformationLogsSourceArnParts...).String() != wildcardArn.String() { logger.WithFields(logrus.Fields{ "Arn": perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...), }).Warn("CloudWatchLogs do not support literal ARN values") } // Make sure we grant InvokeFunction privileges to CloudWatchLogs lambdaInvokePermission, err := perm.BasePermission.export(regionalPrincipal, cloudformationLogsSourceArnParts, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Then we need to uniqueify the rule names s.t. we prevent // collisions with other stacks. configurationResourceNames := make(map[string]int, 0) // Store the last name. We'll do a uniqueness check when exiting the loop, // and if that passes, the last name will also be the unique one. var configurationResourceName string // Create the CustomResource entries globallyUniqueFilters := make(map[string]CloudWatchLogsSubscriptionFilter, len(perm.Filters)) for eachFilterName, eachFilter := range perm.Filters { filterPrefix := fmt.Sprintf("%s_%s", serviceName, eachFilterName) uniqueFilterName := CloudFormationResourceName(filterPrefix, lambdaLogicalCFResourceName) globallyUniqueFilters[uniqueFilterName] = eachFilter // The ARN we supply to IAM is built up using the user supplied groupname cloudWatchLogsArn := gocf.Join("", gocf.String("arn:aws:logs:"), gocf.Ref("AWS::Region"), gocf.String(":"), gocf.Ref("AWS::AccountId"), gocf.String(":log-group:"), gocf.String(eachFilter.LogGroupName), gocf.String(":log-stream:*")) lastConfigurationResourceName, ensureCustomHandlerError := ensureCustomResourceHandler(serviceName, cloudformationresources.CloudWatchLogsLambdaEventSource, cloudWatchLogsArn, []string{}, template, S3Bucket, S3Key, logger) if nil != ensureCustomHandlerError { return "", err } configurationResourceNames[configurationResourceName] = 1 configurationResourceName = lastConfigurationResourceName } if len(configurationResourceNames) > 1 { return "", fmt.Errorf("Internal integrity check failed. Multiple configurators (%d) provisioned for CloudWatchLogs", len(configurationResourceNames)) } // Get the single configurator name from the // Add the custom resource that uses this... ////////////////////////////////////////////////////////////////////////////// newResource, newResourceError := newCloudFormationResource(cloudformationresources.CloudWatchLogsLambdaEventSource, logger) if nil != newResourceError { return "", newResourceError } customResource := newResource.(*cloudformationresources.CloudWatchLogsLambdaEventSourceResource) customResource.ServiceToken = gocf.GetAtt(configurationResourceName, "Arn") customResource.LambdaTargetArn = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn") // Build up the filters... customResource.Filters = make([]*cloudformationresources.CloudWatchLogsLambdaEventSourceFilter, 0) for eachName, eachFilter := range globallyUniqueFilters { customResource.Filters = append(customResource.Filters, &cloudformationresources.CloudWatchLogsLambdaEventSourceFilter{ Name: gocf.String(eachName), Pattern: gocf.String(eachFilter.FilterPattern), LogGroupName: gocf.String(eachFilter.LogGroupName), }) } resourceInvokerName := CloudFormationResourceName("ConfigCloudWatchLogs", lambdaLogicalCFResourceName, perm.BasePermission.SourceAccount) // Add it cfResource := template.AddResource(resourceInvokerName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, lambdaInvokePermission, lambdaLogicalCFResourceName, configurationResourceName) return "", nil }
func (perm CloudWatchEventsPermission) export(serviceName string, lambdaFunctionDisplayName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // There needs to be at least one rule to apply if len(perm.Rules) <= 0 { return "", fmt.Errorf("CloudWatchEventsPermission for function %s does not specify any expressions", lambdaFunctionDisplayName) } // Tell the user we're ignoring any Arns provided, since it doesn't make sense for this. if nil != perm.BasePermission.SourceArn && perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...).String() != wildcardArn.String() { logger.WithFields(logrus.Fields{ "Arn": perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...), }).Warn("CloudWatchEvents do not support literal ARN values") } arnPermissionForRuleName := func(ruleName string) *gocf.StringExpr { return gocf.Join("", gocf.String("arn:aws:events:"), gocf.Ref("AWS::Region"), gocf.String(":"), gocf.Ref("AWS::AccountId"), gocf.String(":rule/"), gocf.String(ruleName)) } // Add the permission to invoke the lambda function uniqueRuleNameMap := make(map[string]int, 0) for eachRuleName, eachRuleDefinition := range perm.Rules { // We need a stable unique name s.t. the permission is properly configured... uniqueRuleName := CloudFormationResourceName(eachRuleName, lambdaFunctionDisplayName, serviceName) uniqueRuleNameMap[uniqueRuleName]++ // Add the permission basePerm := BasePermission{ SourceArn: arnPermissionForRuleName(uniqueRuleName), } _, exportErr := basePerm.export(gocf.String(CloudWatchEventsPrincipal), cloudformationEventsSourceArnParts, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != exportErr { return "", exportErr } cwEventsRuleTargetList := gocf.CloudWatchEventsRuleTargetList{} cwEventsRuleTargetList = append(cwEventsRuleTargetList, gocf.CloudWatchEventsRuleTarget{ Arn: gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"), Id: gocf.String(uniqueRuleName), }, ) // Add the rule eventsRule := &gocf.EventsRule{ Name: gocf.String(uniqueRuleName), Description: gocf.String(eachRuleDefinition.Description), Targets: &cwEventsRuleTargetList, } if nil != eachRuleDefinition.EventPattern && "" != eachRuleDefinition.ScheduleExpression { return "", fmt.Errorf("CloudWatchEvents rule %s specifies both EventPattern and ScheduleExpression", eachRuleName) } if nil != eachRuleDefinition.EventPattern { eventsRule.EventPattern = eachRuleDefinition.EventPattern } else if "" != eachRuleDefinition.ScheduleExpression { eventsRule.ScheduleExpression = gocf.String(eachRuleDefinition.ScheduleExpression) } cloudWatchLogsEventResName := CloudFormationResourceName(fmt.Sprintf("%s-CloudWatchEventsRule", eachRuleName), lambdaLogicalCFResourceName, lambdaFunctionDisplayName) template.AddResource(cloudWatchLogsEventResName, eventsRule) } // Validate it for _, eachCount := range uniqueRuleNameMap { if eachCount != 1 { return "", fmt.Errorf("Integrity violation for CloudWatchEvent Rulenames: %#v", uniqueRuleNameMap) } } return "", nil }
} func (perm BasePermission) descriptionInfo(b *bytes.Buffer, logger *logrus.Logger) error { return errors.New("Describe not implemented") } // // END - BasePermission //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // START - LambdaPermission // var lambdaSourceArnParts = []gocf.Stringable{ gocf.String("arn:aws:lambda:"), gocf.Ref("AWS::Region"), gocf.String(":function:"), } // LambdaPermission type that creates a Lambda::Permission entry // in the generated template, but does NOT automatically register the lambda // with the BasePermission.SourceArn. Typically used to register lambdas with // externally managed event producers type LambdaPermission struct { BasePermission // The entity for which you are granting permission to invoke the Lambda function Principal string } func (perm LambdaPermission) export(serviceName string, lambdaLogicalCFResourceName string,
// export marshals the API data to a CloudFormation compatible representation func (s3Site *S3Site) export(serviceName string, S3Bucket string, S3Key string, S3ResourcesKey string, apiGatewayOutputs map[string]*gocf.Output, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) error { websiteConfig := s3Site.WebsiteConfiguration if nil == websiteConfig { websiteConfig = &s3.WebsiteConfiguration{} } ////////////////////////////////////////////////////////////////////////////// // 1 - Create the S3 bucket. The "BucketName" property is empty s.t. // AWS will assign a unique one. if nil == websiteConfig.ErrorDocument { websiteConfig.ErrorDocument = &s3.ErrorDocument{ Key: aws.String("error.html"), } } if nil == websiteConfig.IndexDocument { websiteConfig.IndexDocument = &s3.IndexDocument{ Suffix: aws.String("index.html"), } } s3WebsiteConfig := &gocf.S3WebsiteConfigurationProperty{ ErrorDocument: gocf.String(aws.StringValue(websiteConfig.ErrorDocument.Key)), IndexDocument: gocf.String(aws.StringValue(websiteConfig.IndexDocument.Suffix)), } s3Bucket := &gocf.S3Bucket{ AccessControl: gocf.String("PublicRead"), WebsiteConfiguration: s3WebsiteConfig, } s3BucketResourceName := stableCloudformationResourceName("Site") cfResource := template.AddResource(s3BucketResourceName, s3Bucket) cfResource.DeletionPolicy = "Delete" template.Outputs[OutputS3SiteURL] = &gocf.Output{ Description: "S3 Website URL", Value: gocf.GetAtt(s3BucketResourceName, "WebsiteURL"), } // Represents the S3 ARN that is provisioned s3SiteBucketResourceValue := gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.Ref(s3BucketResourceName)) s3SiteBucketAllKeysResourceValue := gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.Ref(s3BucketResourceName), gocf.String("/*")) ////////////////////////////////////////////////////////////////////////////// // 2 - Add a bucket policy to enable anonymous access, as the PublicRead // canned ACL doesn't seem to do what is implied. // TODO - determine if this is needed or if PublicRead is being misued s3SiteBucketPolicy := &gocf.S3BucketPolicy{ Bucket: gocf.Ref(s3BucketResourceName).String(), PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": []ArbitraryJSONObject{ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": ArbitraryJSONObject{ "AWS": "*", }, "Action": "s3:GetObject", "Resource": s3SiteBucketAllKeysResourceValue, }, }, }, } s3BucketPolicyResourceName := stableCloudformationResourceName("S3SiteBucketPolicy") template.AddResource(s3BucketPolicyResourceName, s3SiteBucketPolicy) ////////////////////////////////////////////////////////////////////////////// // 3 - Create the IAM role for the lambda function // The lambda function needs to download the posted resource content, as well // as manage the S3 bucket that hosts the site. statements := CommonIAMStatements.Core statements = append(statements, spartaIAM.PolicyStatement{ Action: []string{"s3:ListBucket", "s3:ListObjectsPages"}, Effect: "Allow", Resource: s3SiteBucketResourceValue, }) statements = append(statements, spartaIAM.PolicyStatement{ Action: []string{"s3:DeleteObject", "s3:PutObject", "s3:DeleteObjects", "s3:DeleteObjects"}, Effect: "Allow", Resource: s3SiteBucketAllKeysResourceValue, }) statements = append(statements, spartaIAM.PolicyStatement{ Action: []string{"s3:GetObject"}, Effect: "Allow", Resource: gocf.Join("", gocf.String("arn:aws:s3:::"), gocf.String(S3Bucket), gocf.String("/"), gocf.String(S3ResourcesKey)), }) iamPolicyList := gocf.IAMPoliciesList{} iamPolicyList = append(iamPolicyList, gocf.IAMPolicies{ PolicyDocument: ArbitraryJSONObject{ "Version": "2012-10-17", "Statement": statements, }, PolicyName: gocf.String("S3SiteMgmnt"), }, ) iamS3Role := &gocf.IAMRole{ AssumeRolePolicyDocument: AssumePolicyDocument, Policies: &iamPolicyList, } iamRoleName := stableCloudformationResourceName("S3SiteIAMRole") cfResource = template.AddResource(iamRoleName, iamS3Role) cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName) iamRoleRef := gocf.GetAtt(iamRoleName, "Arn") // Create the IAM role and CustomAction handler to do the work ////////////////////////////////////////////////////////////////////////////// // 4 - Create the lambda function definition that executes with the // dynamically provisioned IAM policy. This is similar to what happens in // ensureCustomResourceHandler, but due to the more complex IAM rules // there's a bit of duplication handlerName := lambdaExportNameForCustomResourceType(cloudformationresources.ZipToS3Bucket) logger.WithFields(logrus.Fields{ "CustomResourceType": cloudformationresources.ZipToS3Bucket, "NodeJSExport": handlerName, }).Debug("Sparta CloudFormation custom resource handler info") customResourceHandlerDef := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String(customResourceDescription(serviceName, "S3 static site")), Handler: gocf.String(handlerName), Role: iamRoleRef, Runtime: gocf.String(NodeJSVersion), MemorySize: gocf.Integer(256), Timeout: gocf.Integer(180), } lambdaResourceName := stableCloudformationResourceName("S3SiteCreator") cfResource = template.AddResource(lambdaResourceName, customResourceHandlerDef) cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName, iamRoleName) ////////////////////////////////////////////////////////////////////////////// // 5 - Create the custom resource that invokes the site bootstrapper lambda to // actually populate the S3 with content customResourceName := stableCloudformationResourceName("S3SiteBuilder") newResource, err := newCloudFormationResource(cloudformationresources.ZipToS3Bucket, logger) if nil != err { return err } zipResource := newResource.(*cloudformationresources.ZipToS3BucketResource) zipResource.ServiceToken = gocf.GetAtt(lambdaResourceName, "Arn") zipResource.SrcKeyName = gocf.String(S3ResourcesKey) zipResource.SrcBucket = gocf.String(S3Bucket) zipResource.DestBucket = gocf.Ref(s3BucketResourceName).String() // Build the manifest data with any output info... manifestData := make(map[string]interface{}, 0) for eachKey, eachOutput := range apiGatewayOutputs { manifestData[eachKey] = map[string]interface{}{ "Description": eachOutput.Description, "Value": eachOutput.Value, } } zipResource.Manifest = manifestData cfResource = template.AddResource(customResourceName, zipResource) cfResource.DependsOn = append(cfResource.DependsOn, lambdaResourceName, s3BucketResourceName) return nil }