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 (rule *ReceiptRule) toResourceRule(serviceName string, functionArnRef interface{}, messageBodyStorage *MessageBodyStorage) *cloudformationresources.SESLambdaEventSourceResourceRule { resourceRule := &cloudformationresources.SESLambdaEventSourceResourceRule{ Name: gocf.String(rule.Name), ScanEnabled: gocf.Bool(!rule.ScanDisabled), Enabled: gocf.Bool(!rule.Disabled), Actions: make([]*cloudformationresources.SESLambdaEventSourceResourceAction, 0), Recipients: make([]*gocf.StringExpr, 0), } for _, eachRecipient := range rule.Recipients { resourceRule.Recipients = append(resourceRule.Recipients, gocf.String(eachRecipient)) } if "" != rule.TLSPolicy { resourceRule.TLSPolicy = gocf.String(rule.TLSPolicy) } // If there is a MessageBodyStorage reference, push that S3Action // to the head of the Actions list if nil != messageBodyStorage && !rule.BodyStorageOptions.DisableStorage { s3Action := &cloudformationresources.SESLambdaEventSourceResourceAction{ ActionType: gocf.String("S3Action"), ActionProperties: map[string]interface{}{ "BucketName": messageBodyStorage.bucketNameExpr, }, } if "" != rule.BodyStorageOptions.ObjectKeyPrefix { s3Action.ActionProperties["ObjectKeyPrefix"] = rule.BodyStorageOptions.ObjectKeyPrefix } if "" != rule.BodyStorageOptions.KmsKeyArn { s3Action.ActionProperties["KmsKeyArn"] = rule.BodyStorageOptions.KmsKeyArn } if "" != rule.BodyStorageOptions.TopicArn { s3Action.ActionProperties["TopicArn"] = rule.BodyStorageOptions.TopicArn } resourceRule.Actions = append(resourceRule.Actions, s3Action) } // There's always a lambda action lambdaAction := &cloudformationresources.SESLambdaEventSourceResourceAction{ ActionType: gocf.String("LambdaAction"), ActionProperties: map[string]interface{}{ "FunctionArn": functionArnRef, }, } lambdaAction.ActionProperties["InvocationType"] = rule.InvocationType if "" == rule.InvocationType { lambdaAction.ActionProperties["InvocationType"] = "Event" } if "" != rule.TopicArn { lambdaAction.ActionProperties["TopicArn"] = rule.TopicArn } resourceRule.Actions = append(resourceRule.Actions, lambdaAction) return resourceRule }
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 }
// 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 SESPermission) export(serviceName string, lambdaFunctionDisplayName string, lambdaLogicalCFResourceName string, template *gocf.Template, S3Bucket string, S3Key string, logger *logrus.Logger) (string, error) { sourceArnExpression := perm.BasePermission.sourceArnExpr(snsSourceArnParts...) targetLambdaResourceName, err := perm.BasePermission.export(gocf.String(SESPrincipal), sesSourcePartArn, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // MessageBody storage? var dependsOn []string if nil != perm.MessageBodyStorage { s3Policy, s3PolicyErr := perm.MessageBodyStorage.export(serviceName, lambdaFunctionDisplayName, lambdaLogicalCFResourceName, template, S3Bucket, S3Key, logger) if nil != s3PolicyErr { return "", s3PolicyErr } if "" != s3Policy { dependsOn = append(dependsOn, s3Policy) } } // Make sure the custom lambda that manages SNS notifications is provisioned. configuratorResName, err := ensureCustomResourceHandler(serviceName, cloudformationresources.SESLambdaEventSource, sourceArnExpression, dependsOn, template, S3Bucket, S3Key, logger) if nil != err { return "", err } // Add a custom resource invocation for this configuration ////////////////////////////////////////////////////////////////////////////// newResource, newResourceError := newCloudFormationResource(cloudformationresources.SESLambdaEventSource, logger) if nil != newResourceError { return "", newResourceError } customResource := newResource.(*cloudformationresources.SESLambdaEventSourceResource) customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn") // The shared ruleset name used by all Sparta applications customResource.RuleSetName = gocf.String("SpartaRuleSet") /////////////////// // Build up the Rules // If there aren't any rules, make one that forwards everything... var sesRules []*cloudformationresources.SESLambdaEventSourceResourceRule if nil == perm.ReceiptRules { sesRules = append(sesRules, &cloudformationresources.SESLambdaEventSourceResourceRule{ Name: gocf.String("Default"), Actions: make([]*cloudformationresources.SESLambdaEventSourceResourceAction, 0), ScanEnabled: gocf.Bool(false), Enabled: gocf.Bool(true), Recipients: []*gocf.StringExpr{}, TLSPolicy: gocf.String("Optional"), }) } // Append all the user defined ones for _, eachReceiptRule := range perm.ReceiptRules { sesRules = append(sesRules, eachReceiptRule.toResourceRule( serviceName, gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"), perm.MessageBodyStorage)) } customResource.Rules = sesRules // Name? resourceInvokerName := CloudFormationResourceName("ConfigSNS", lambdaLogicalCFResourceName, perm.BasePermission.SourceAccount) // Add it cfResource := template.AddResource(resourceInvokerName, customResource) cfResource.DependsOn = append(cfResource.DependsOn, targetLambdaResourceName, configuratorResName) 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, 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 }