In addition to provisioning AWS Lambda functions, Sparta supports the creation of other CloudFormation Resources. This enables a service to move towards immutable infrastructure, where the service and its infrastructure requirements are treated as a logical unit.
For instance, consider the case where two developers are working in the same AWS account.
Using a shared, externally provisioned S3 bucket has several impacts:
Alternatively, each developer could provision and manage disjoint topologies:
Enabling each developer to create other AWS resources also means more complex topologies can be expressed. These topologies can benefit from CloudWatch monitoring (eg, per-Lambda Metrics ) without the need to add custom metrics.
Sparta supports Dynamic Resources via TemplateDecoratorHandler satisfying types.
A template decorator is a go interface:
type TemplateDecoratorHandler interface {
DecorateTemplate(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 *zerolog.Logger) error
}
Clients use go-cloudformation
types for CloudFormation resources and template.AddResource
to add them to
the *template
parameter. After a decorator is invoked, Sparta also verifies that
the user-supplied function did not add entities that that collide with the
internally-generated ones.
CloudFormation uses Logical IDs as resource key names.
To minimize collision likelihood, Sparta publishes CloudFormationResourceName(prefix, …parts) to generate compliant identifiers. To produce content-based hash values, callers can provide a non-empty set of values as the ...parts
variadic argument. This produces stable identifiers across Sparta execution (which may affect availability during updates).
When called with only a single value (eg: CloudFormationResourceName("myResource")
), Sparta will return a random resource name that is NOT stable across executions.
Let’s work through an example to make things a bit more concrete. We have the following requirements:
To start with, we’ll need a Sparta lambda function to expose:
import (
awsLambdaEvents "github.com/aws/aws-lambda-go/events"
)
func echoS3DynamicBucketEvent(ctx context.Context,
s3Event awsLambdaEvents.S3Event) (*awsLambdaEvents.S3Event, error) {
logger, _ := ctx.Value(sparta.ContextKeyRequestLogger).(*zerolog.Logger)
discoveryInfo, discoveryInfoErr := sparta.Discover()
logger.Info().
Interface("Event", s3Event).
Interface("Discovery", discoveryInfo).
Err(discoveryInfoErr).
Msg("Event received")
return &s3Event, nil
}
For brevity our demo function doesn’t access the S3 bucket objects. To support that please see the sparta.Discover functionality.
The next thing we need is a Logical ID for our bucket:
s3BucketResourceName := sparta.CloudFormationResourceName("S3DynamicBucket", "myServiceBucket")
With these two values we’re ready to get started building up the lambda function:
lambdaFn, _ := sparta.NewAWSLambda(sparta.LambdaName(echoS3DynamicBucketEvent),
echoS3DynamicBucketEvent,
sparta.IAMRoleDefinition{})
The open issue is how to publish the CloudFormation-defined S3 Arn to the compile
-time application. Our lambda function needs to provide both:
The missing piece is gocf.Ref(), whose single argument is the Logical ID of the S3 resource we’ll be inserting in the decorator call.
The IAMRolePrivilege
struct references the dynamically assigned S3 Arn as follows:
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)
The S3Permission
struct also requires the dynamic Arn, to which it will append "/*"
to enable object read access.
lambdaFn.RoleDefinition.Privileges = append(lambdaFn.RoleDefinition.Privileges,
sparta.IAMRolePrivilege{
Actions: []string{"s3:GetObject", "s3:HeadObject"},
Resource: spartaCF.S3AllKeysArnForBucket(gocf.Ref(s3BucketResourceName)),
})
The spartaCF.S3AllKeysArnForBucket
call is a convenience wrapper around gocf.Join to generate the concatenated, dynamic Arn expression.
All that’s left to do is actually insert the S3 resource in our decorator:
s3Decorator := 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 *zerolog.Logger) error {
cfResource := template.AddResource(s3BucketResourceName, &gocf.S3Bucket{
AccessControl: gocf.String("PublicRead"),
Tags: &gocf.TagList{gocf.Tag{
Key: gocf.String("SpecialKey"),
Value: gocf.String("SpecialValue"),
},
},
})
cfResource.DeletionPolicy = "Delete"
return nil
}
lambdaFn.Decorators = []sparta.TemplateDecoratorHandler{
sparta.TemplateDecoratorHookFunc(s3Decorator),
}
In reality, we shouldn’t even attempt to create the AWS Lambda function if the S3 bucket creation fails. As application developers, we can help CloudFormation sequence infrastructure operations by stating this hard dependency on the S3 bucket via the DependsOn attribute:
lambdaFn.DependsOn = append(lambdaFn.DependsOn, s3BucketResourceName)
Putting everything together, our Sparta lambda function with dynamic infrastructure is listed below.
s3BucketResourceName := sparta.CloudFormationResourceName("S3DynamicBucket")
lambdaFn, _ := sparta.NewAWSLambda(sparta.LambdaName(echoS3DynamicBucketEvent),
echoS3DynamicBucketEvent,
sparta.IAMRoleDefinition{})
// Our lambda function requires the S3 bucket
lambdaFn.DependsOn = append(lambdaFn.DependsOn, s3BucketResourceName)
// Add a 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)),
})
// Configure the S3 event source
lambdaFn.Permissions = append(lambdaFn.Permissions, sparta.S3Permission{
BasePermission: sparta.BasePermission{
SourceArn: gocf.Ref(s3BucketResourceName),
},
Events: []string{"s3:ObjectCreated:*",
"s3:ObjectRemoved:*"},
})
// Actually add the resource
lambdaFn.Decorator = func(lambdaResourceName string,
lambdaResource gocf.LambdaFunction,
template *gocf.Template,
logger *zerolog.Logger) error {
cfResource := template.AddResource(s3BucketResourceName, &gocf.S3Bucket{
AccessControl: gocf.String("PublicRead"),
})
cfResource.DeletionPolicy = "Delete"
return nil
}
Sparta provides an opportunity to bring infrastructure management into the application programming model. It’s still possible to use literal Arn strings, but the ability to include other infrastructure requirements brings a service closer to being self-contained and more operationally sustainable.
echoS3DynamicBucketEvent
function can also access the bucket Arn via sparta.Discover.go-cloudformation
exposes gocf.Join to create compound, dynamic expressions.