func (resourceInfo *customResourceInfo) export(serviceName string, targetLambda *gocf.StringExpr, S3Bucket string, S3Key string, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) error { // Figure out the role name iamRoleArnName := resourceInfo.roleName // If there is no user supplied role, that means that the associated // IAMRoleDefinition name has been created and this resource needs to // depend on that being created. if iamRoleArnName == "" && resourceInfo.roleDefinition != nil { iamRoleArnName = resourceInfo.roleDefinition.logicalName(serviceName, resourceInfo.userFunctionName) } lambdaDescription := resourceInfo.options.Description if "" == lambdaDescription { lambdaDescription = fmt.Sprintf("%s CustomResource: %s", serviceName, resourceInfo.userFunctionName) } // Create the Lambda Function lambdaResource := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String(lambdaDescription), Handler: gocf.String(fmt.Sprintf("index.%s", resourceInfo.jsHandlerName())), MemorySize: gocf.Integer(resourceInfo.options.MemorySize), Role: roleNameMap[iamRoleArnName], Runtime: gocf.String(NodeJSVersion), Timeout: gocf.Integer(resourceInfo.options.Timeout), VpcConfig: resourceInfo.options.VpcConfig, } lambdaFunctionCFName := CloudFormationResourceName("CustomResourceLambda", resourceInfo.userFunctionName, resourceInfo.logicalName()) cfResource := template.AddResource(lambdaFunctionCFName, lambdaResource) safeMetadataInsert(cfResource, "golangFunc", resourceInfo.userFunctionName) // And create the CustomResource that actually invokes it... newResource, newResourceError := newCloudFormationResource(cloudFormationLambda, logger) if nil != newResourceError { return newResourceError } customResource := newResource.(*cloudFormationLambdaCustomResource) customResource.ServiceToken = gocf.GetAtt(lambdaFunctionCFName, "Arn") customResource.UserProperties = resourceInfo.properties template.AddResource(resourceInfo.logicalName(), customResource) return 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 (mapping *EventSourceMapping) export(serviceName string, targetLambda *gocf.StringExpr, S3Bucket string, S3Key string, template *gocf.Template, logger *logrus.Logger) error { eventSourceMappingResource := gocf.LambdaEventSourceMapping{ EventSourceArn: gocf.String(mapping.EventSourceArn), FunctionName: targetLambda, StartingPosition: gocf.String(mapping.StartingPosition), BatchSize: gocf.Integer(mapping.BatchSize), Enabled: gocf.Bool(!mapping.Disabled), } hash := sha1.New() hash.Write([]byte(mapping.EventSourceArn)) binary.Write(hash, binary.LittleEndian, mapping.BatchSize) hash.Write([]byte(mapping.StartingPosition)) resourceName := fmt.Sprintf("LambdaES%s", hex.EncodeToString(hash.Sum(nil))) template.AddResource(resourceName, eventSourceMappingResource) return nil }
// Marshal this object into 1 or more CloudFormation resource definitions that are accumulated // in the resources map func (info *LambdaAWSInfo) export(serviceName string, S3Bucket string, S3Key string, buildID string, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, context map[string]interface{}, logger *logrus.Logger) error { // If we have RoleName, then get the ARN, otherwise get the Ref var dependsOn []string if nil != info.DependsOn { dependsOn = append(dependsOn, info.DependsOn...) } iamRoleArnName := info.RoleName // If there is no user supplied role, that means that the associated // IAMRoleDefinition name has been created and this resource needs to // depend on that being created. if iamRoleArnName == "" && info.RoleDefinition != nil { iamRoleArnName = info.RoleDefinition.logicalName(serviceName, info.lambdaFunctionName()) dependsOn = append(dependsOn, info.RoleDefinition.logicalName(serviceName, info.lambdaFunctionName())) } lambdaDescription := info.Options.Description if "" == lambdaDescription { lambdaDescription = fmt.Sprintf("%s: %s", serviceName, info.lambdaFunctionName()) } // Create the primary resource lambdaResource := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String(lambdaDescription), Handler: gocf.String(fmt.Sprintf("index.%s", info.jsHandlerName())), MemorySize: gocf.Integer(info.Options.MemorySize), Role: roleNameMap[iamRoleArnName], Runtime: gocf.String(NodeJSVersion), Timeout: gocf.Integer(info.Options.Timeout), VpcConfig: info.Options.VpcConfig, } if "" != info.Options.KmsKeyArn { lambdaResource.KmsKeyArn = gocf.String(info.Options.KmsKeyArn) } if nil != info.Options.Environment { lambdaResource.Environment = &gocf.LambdaFunctionEnvironment{ Variables: info.Options.Environment, } } // Need to check if a functionName exists in the LambdaAwsInfo struct // If an empty string is passed, the template will error with invalid // function name. if "" != info.functionName { lambdaResource.FunctionName = gocf.String(info.functionName) } cfResource := template.AddResource(info.logicalName(), lambdaResource) cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) safeMetadataInsert(cfResource, "golangFunc", info.lambdaFunctionName()) // Create the lambda Ref in case we need a permission or event mapping functionAttr := gocf.GetAtt(info.logicalName(), "Arn") // Permissions for _, eachPermission := range info.Permissions { _, err := eachPermission.export(serviceName, info.lambdaFunctionName(), info.logicalName(), template, S3Bucket, S3Key, logger) if nil != err { return err } } // Event Source Mappings for _, eachEventSourceMapping := range info.EventSourceMappings { mappingErr := eachEventSourceMapping.export(serviceName, functionAttr, S3Bucket, S3Key, template, logger) if nil != mappingErr { return mappingErr } } // CustomResource for _, eachCustomResource := range info.customResources { resourceErr := eachCustomResource.export(serviceName, functionAttr, S3Bucket, S3Key, roleNameMap, template, logger) if nil != resourceErr { return resourceErr } } // Decorator if nil != info.Decorator { logger.Debug("Decorator found for Lambda: ", info.lambdaFunctionName()) // Create an empty template so that we can track whether things // are overwritten metadataMap := make(map[string]interface{}, 0) decoratorProxyTemplate := gocf.NewTemplate() err := info.Decorator(serviceName, info.logicalName(), lambdaResource, metadataMap, S3Bucket, S3Key, buildID, decoratorProxyTemplate, context, logger) if nil != err { return err } // This data is marshalled into a DiscoveryInfo struct s.t. it can be // unmarshalled via sparta.Discover. We're going to just stuff it into // it's own same named property if len(metadataMap) != 0 { safeMetadataInsert(cfResource, info.logicalName(), metadataMap) } // Append the custom resources err = safeMergeTemplates(decoratorProxyTemplate, template, logger) if nil != err { return fmt.Errorf("Lambda (%s) decorator created conflicting resources", info.lambdaFunctionName()) } } return nil }
func ensureCustomResourceHandler(serviceName string, customResourceTypeName string, sourceArn *gocf.StringExpr, dependsOn []string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // AWS service basename awsServiceName := awsPrincipalToService(customResourceTypeName) // Use a stable resource CloudFormation resource name to represent // the single CustomResource that can configure the different // PushSource's for the given principal. keyName, err := json.Marshal(ArbitraryJSONObject{ "Principal": customResourceTypeName, "ServiceName": awsServiceName, }) if err != nil { logger.Error("Failed to create configurator resource name: ", err.Error()) return "", err } subscriberHandlerName := CloudFormationResourceName(fmt.Sprintf("%sCustomResource", awsServiceName), string(keyName)) ////////////////////////////////////////////////////////////////////////////// // IAM Role definition iamResourceName, err := ensureIAMRoleForCustomResource(customResourceTypeName, sourceArn, template, logger) if nil != err { return "", err } iamRoleRef := gocf.GetAtt(iamResourceName, "Arn") _, exists := template.Resources[subscriberHandlerName] if !exists { logger.WithFields(logrus.Fields{ "Service": customResourceTypeName, }).Debug("Including Lambda CustomResource for AWS Service") configuratorDescription := customResourceDescription(serviceName, customResourceTypeName) ////////////////////////////////////////////////////////////////////////////// // Custom Resource Lambda Handler // The export name MUST correspond to the createForwarder entry that is dynamically // written into the index.js file during compile in createNewSpartaCustomResourceEntry handlerName := lambdaExportNameForCustomResourceType(customResourceTypeName) logger.WithFields(logrus.Fields{ "CustomResourceType": customResourceTypeName, "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(configuratorDescription), Handler: gocf.String(handlerName), Role: iamRoleRef, Runtime: gocf.String(NodeJSVersion), Timeout: gocf.Integer(30), } cfResource := template.AddResource(subscriberHandlerName, customResourceHandlerDef) if nil != dependsOn && (len(dependsOn) > 0) { cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) } } return subscriberHandlerName, 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 }
// TODO - Refactor ensure Lambdaconfigurator, then finish // implementing the CloudWatchEvents Principal type. func ensureConfiguratorLambdaResource(awsPrincipalName string, sourceArn *gocf.StringExpr, dependsOn []string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { // AWS service basename awsServiceName := awsPrincipalToService(awsPrincipalName) configuratorExportName := strings.ToLower(awsServiceName) logger.WithFields(logrus.Fields{ "ServiceName": awsServiceName, "NodeJSExportName": configuratorExportName, }).Debug("Ensuring AWS push service configurator CustomResource") // Use a stable resource CloudFormation resource name to represent // the single CustomResource that can configure the different // PushSource's for the given principal. keyName, err := json.Marshal(ArbitraryJSONObject{ "Principal": awsPrincipalName, "ServiceName": awsServiceName, }) if err != nil { logger.Error("Failed to create configurator resource name: ", err.Error()) return "", err } subscriberHandlerName := CloudFormationResourceName(fmt.Sprintf("%sCustomResource", awsServiceName), string(keyName)) ////////////////////////////////////////////////////////////////////////////// // IAM Role definition iamResourceName, err := ensureIAMRoleForCustomResource(awsPrincipalName, sourceArn, template, logger) if nil != err { return "", err } iamRoleRef := gocf.GetAtt(iamResourceName, "Arn") _, exists := template.Resources[subscriberHandlerName] if !exists { logger.WithFields(logrus.Fields{ "Service": awsServiceName, }).Debug("Including Lambda CustomResource for AWS Service") configuratorDescription := fmt.Sprintf("Sparta created Lambda CustomResource to configure %s service", awsServiceName) ////////////////////////////////////////////////////////////////////////////// // Custom Resource Lambda Handler // NOTE: This brittle function name has an analog in ./resources/index.js b/c the // AWS Lamba execution treats the entire ZIP file as a module. So all module exports // need to be forwarded through the module's index.js file. handlerName := nodeJSHandlerName(configuratorExportName) logger.Debug("Lambda Configuration handler: ", handlerName) customResourceHandlerDef := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String(configuratorDescription), Handler: gocf.String(handlerName), Role: iamRoleRef, Runtime: gocf.String("nodejs"), Timeout: gocf.Integer(30), } cfResource := template.AddResource(subscriberHandlerName, customResourceHandlerDef) if nil != dependsOn && (len(dependsOn) > 0) { cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) } } return subscriberHandlerName, nil }
// Marshal this object into 1 or more CloudFormation resource definitions that are accumulated // in the resources map func (info *LambdaAWSInfo) export(serviceName string, S3Bucket string, S3Key string, roleNameMap map[string]*gocf.StringExpr, template *gocf.Template, logger *logrus.Logger) error { // If we have RoleName, then get the ARN, otherwise get the Ref var dependsOn []string if nil != info.DependsOn { dependsOn = append(dependsOn, info.DependsOn...) } iamRoleArnName := info.RoleName // If there is no user supplied role, that means that the associated // IAMRoleDefinition name has been created and this resource needs to // depend on that being created. if iamRoleArnName == "" && info.RoleDefinition != nil { iamRoleArnName = info.RoleDefinition.logicalName() dependsOn = append(dependsOn, info.RoleDefinition.logicalName()) } lambdaDescription := info.Options.Description if "" == lambdaDescription { lambdaDescription = fmt.Sprintf("%s: %s", serviceName, info.lambdaFnName) } // Create the primary resource lambdaResource := gocf.LambdaFunction{ Code: &gocf.LambdaFunctionCode{ S3Bucket: gocf.String(S3Bucket), S3Key: gocf.String(S3Key), }, Description: gocf.String(lambdaDescription), Handler: gocf.String(fmt.Sprintf("index.%s", info.jsHandlerName())), MemorySize: gocf.Integer(info.Options.MemorySize), Role: roleNameMap[iamRoleArnName], Runtime: gocf.String("nodejs"), Timeout: gocf.Integer(info.Options.Timeout), } cfResource := template.AddResource(info.logicalName(), lambdaResource) cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...) safeMetadataInsert(cfResource, "golangFunc", info.lambdaFnName) // Create the lambda Ref in case we need a permission or event mapping functionAttr := gocf.GetAtt(info.logicalName(), "Arn") // Permissions for _, eachPermission := range info.Permissions { _, err := eachPermission.export(serviceName, info.logicalName(), template, S3Bucket, S3Key, logger) if nil != err { return err } } // Event Source Mappings hash := sha1.New() for _, eachEventSourceMapping := range info.EventSourceMappings { eventSourceMappingResource := gocf.LambdaEventSourceMapping{ EventSourceArn: gocf.String(eachEventSourceMapping.EventSourceArn), FunctionName: functionAttr, StartingPosition: gocf.String(eachEventSourceMapping.StartingPosition), BatchSize: gocf.Integer(eachEventSourceMapping.BatchSize), Enabled: gocf.Bool(!eachEventSourceMapping.Disabled), } hash.Write([]byte(eachEventSourceMapping.EventSourceArn)) binary.Write(hash, binary.LittleEndian, eachEventSourceMapping.BatchSize) hash.Write([]byte(eachEventSourceMapping.StartingPosition)) resourceName := fmt.Sprintf("LambdaES%s", hex.EncodeToString(hash.Sum(nil))) template.AddResource(resourceName, eventSourceMappingResource) } // Decorator if nil != info.Decorator { logger.Debug("Decorator found for Lambda: ", info.lambdaFnName) // Create an empty template so that we can track whether things // are overwritten decoratorProxyTemplate := gocf.NewTemplate() err := info.Decorator(info.logicalName(), lambdaResource, decoratorProxyTemplate, logger) if nil != err { return err } // Append the custom resources err = safeMergeTemplates(decoratorProxyTemplate, template, logger) if nil != err { return fmt.Errorf("Lambda (%s) decorator created conflicting resources", info.lambdaFnName) } } return nil }
// 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 }