Teaching people to do serverless is hard. It's far less about teaching someone about FaaS and far more about getting people into the right mindset. It is not about technology, or at least, it's not about doing technology
— Paul Johnston - containing his snark (@PaulDJohnston) December 13, 2018
While Serverless and FaaS are often used interchangeably, there are types of workloads that are more challenging to move to FaaS. Perhaps due to third party libraries, latency, or storage requirements, the FaaS model isn’t an ideal fit. An example that is commonly provided is the need to run ffmpeg.
To benefit from the serverless model in these cases, Sparta provides the ability to leverage the Fargate service to run Containers without needing to manage servers.
There are several steps to Fargate-ifying your application and Sparta exposes
functions and hooks to make that operation scoped to a provision
operation.
Those steps include:
This overview is based on the SpartaStepServicefull project. The implementation uses a combination of ServiceDecoratorHookHandlers to achieve the end result.
Please see servicefull_build.go for the most up-to-date version of code samples.
The first step is to provide an opportunity for our application to behave
differently when run as a Fargate task. To do this we add a new
application subcommand option that augments the standard Main
behavior:
// Add a hook to do something
fargateTask := &cobra.Command{
Use: "fargateTask",
Short: "Sample Fargate task",
Long: `Sample Fargate task that simply logs a message"`,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("Insert your Fargate code here! 🎉")
return nil
},
}
// Register the command with the Sparta root dispatcher. This
// command `fargateTask` matches the command line option in the
// Dockerfile that is used to build the image.
sparta.CommandLineOptions.Root.AddCommand(fargateTask)
This subcommand is defined in the servicefull_task
file. Note that the file uses go
build tags
so that the new fargateTask subcommand is only available when the
build target includes the lambdaBinary flag:
// +build lambdabinary
package bootstrap
We can now package our Task-aware executable and deploy it to the cloud.
The first step is to create a version of your application that
can support a Fargate task. This is done in the ecrImageBuilderDecorator
function which delegates the compiling and image creation to Sparta:
// Always build the image
buildErr := spartaDocker.BuildDockerImage(serviceName,
"",
dockerTags,
logger)
The second empty argument above is an optional Dockerfile path. The sample project uses the default Dockerfile filename and defines that at the root of the repository. The full Dockerfile is:
FROM alpine:3.8
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
# Sparta provides the SPARTA_DOCKER_BINARY argument to the builder
# in order to embed the binary.
# Ref: https://docs.docker.com/engine/reference/builder/
ARG SPARTA_DOCKER_BINARY
ADD $SPARTA_DOCKER_BINARY /SpartaServicefull
CMD ["/SpartaServicefull", "fargateTask"]
The BuildDockerImage
function supplies the transient binary executable
path to docker via the SPARTA_DOCKER_BINARY ARG
value.
The CMD
instruction includes our previously registered fargateTask
subcommand name to invoke the Task-appropriate codepath at runtime.
The log output includes the docker build info:
INFO[0002] Calling WorkflowHook
ServiceDecoratorHook=github.com/mweagle/SpartaStepServicefull/bootstrap.ecrImageBuilderDecorator.func1
WorkflowHookContext="map[]"
INFO[0002] Docker version 18.09.0, build 4d60db4
INFO[0002] Running `go generate`
INFO[0002] Compiling binary
Name=ServicefulStepFunction-1544976454011339000-docker.lambda.amd64
INFO[0003] Creating Docker image
Tags="map[servicefulstepfunction:adc67a77aef22b6dab9c6156d13853e2cfe06488.1544976453]"
NFO[0004] Sending build context to Docker daemon 35.43MB
INFO[0004] Step 1/5 : FROM alpine:3.8
INFO[0004] ---> 196d12cf6ab1
INFO[0004] Step 2/5 : RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
INFO[0004] ---> Using cache
INFO[0004] ---> 99402375b7f2
INFO[0004] Step 3/5 : ARG SPARTA_DOCKER_BINARY
INFO[0004] ---> Using cache
INFO[0004] ---> a44d27522c40
INFO[0004] Step 4/5 : ADD $SPARTA_DOCKER_BINARY /SpartaServicefull
INFO[0005] ---> 87ffd10e9901
INFO[0005] Step 5/5 : CMD ["/SpartaServicefull", "fargateTask"]
INFO[0005] ---> Running in 0a3b503201c7
INFO[0005] Removing intermediate container 0a3b503201c7
INFO[0005] ---> 7cb1b2261a92
INFO[0005] Successfully built 7cb1b2261a92
INFO[0005] Successfully tagged
servicefulstepfunction:adc67a77aef22b6dab9c6156d13853e2cfe06488.1544976453
The next step is to push the locally built image to the Elastic Container Registry. The push will return either the ECR URL which will be used as Fargate Task image property or an error:
// Push the image to ECR & store the URL s.t. we can properly annotate
// the CloudFormation template
ecrURLPush, pushImageErr := spartaDocker.PushDockerImageToECR(buildTag,
ecrRepositoryName,
awsSession,
logger)
The ECR push URL is stored in the context
variable so that a downstream
Fargate cluster builder knows the image to use:
context[contextKeyImageURL] = ecrURLPush
The Step Function definition indirectly references the Fargate Task via task specific parameters:
fargateParams := spartaStep.FargateTaskParameters{
LaunchType: "FARGATE",
Cluster: gocf.Ref(resourceNames.ECSCluster).String(),
TaskDefinition: gocf.Ref(resourceNames.ECSTaskDefinition).String(),
NetworkConfiguration: &spartaStep.FargateNetworkConfiguration{
AWSVPCConfiguration: &gocf.ECSServiceAwsVPCConfiguration{
Subnets: gocf.StringList(
gocf.Ref(resourceNames.PublicSubnetAzs[0]).String(),
gocf.Ref(resourceNames.PublicSubnetAzs[1]).String(),
),
AssignPublicIP: gocf.String("ENABLED"),
},
},
}
fargateState := spartaStep.NewFargateTaskState("Run Fargate Task", fargateParams)
The ECSCluster and ECSTaskDefinition are resources that are provisioned
by the fargateClusterDecorator
decorator function.
The final step is to provision the ECS cluster that supports the Fargate
task. This is encapsulated in the fargateClusterDecorator
which creates
the required set of CloudFormation resources. The set of CloudFormation
resource names is represented in the stackResourceNames
struct:
type stackResourceNames struct {
StepFunction string
SNSTopic string
ECSCluster string
ECSRunTaskRole string
ECSTaskDefinition string
ECSTaskDefinitionLogGroup string
ECSTaskDefinitionRole string
VPC string
InternetGateway string
AttachGateway string
RouteViaIgw string
PublicRouteViaIgw string
ECSSecurityGroup string
PublicSubnetAzs []string
}
The ECS Task Definition is of particular interest and is where the inline created ECR_URL is used to define a FARGATE task.
imageURL, _ := context[contextKeyImageURL].(string)
if imageURL == "" {
return errors.Errorf("Failed to get image URL from context with key %s",
contextKeyImageURL)
}
...
// Create the ECS task definition
ecsTaskDefinition := &gocf.ECSTaskDefinition{
ExecutionRoleArn: gocf.GetAtt(resourceNames.ECSTaskDefinitionRole, "Arn"),
RequiresCompatibilities: gocf.StringList(gocf.String("FARGATE")),
CPU: gocf.String("256"),
Memory: gocf.String("512"),
NetworkMode: gocf.String("awsvpc"),
ContainerDefinitions: &gocf.ECSTaskDefinitionContainerDefinitionList{
gocf.ECSTaskDefinitionContainerDefinition{
Image: gocf.String(imageURL),
Name: gocf.String("sparta-servicefull"),
Essential: gocf.Bool(true),
LogConfiguration: &gocf.ECSTaskDefinitionLogConfiguration{
LogDriver: gocf.String("awslogs"),
// Options Ref: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html
Options: map[string]interface{}{
"awslogs-region": gocf.Ref("AWS::Region"),
"awslogs-group": strings.Join([]string{"",
sparta.ProperName,
serviceName}, "/"),
"awslogs-stream-prefix": serviceName,
"awslogs-create-group": "true",
},
},
},
},
}
The final step is to provide the three decorators to the WorkflowHooks structure:
workflowHooks := &sparta.WorkflowHooks{
ServiceDecorators: []sparta.ServiceDecoratorHookHandler{
ecrImageBuilderDecorator("spartadocker"),
// Then build the state machine
stateMachine.StateMachineDecorator(),
// Then the ECS cluster that supports the Fargate task
fargateClusterDecorator(resourceNames),
},
}
The provisioning workflow for this service is the same as a Lambda-based one:
$ go run main.provision --s3Bucket $MY_S3_BUCKET
Output:
INFO[0000] ════════════════════════════════════════════════
INFO[0000] ╔═╗╔═╗╔═╗╦═╗╔╦╗╔═╗ Version : 1.8.0
INFO[0000] ╚═╗╠═╝╠═╣╠╦╝ ║ ╠═╣ SHA : 597d3ba
INFO[0000] ╚═╝╩ ╩ ╩╩╚═ ╩ ╩ ╩ Go : go1.11.1
INFO[0000] ════════════════════════════════════════════════
INFO[0000] Service: ServicefulStepFunction
LinkFlags= Option=provision UTC="2018-12-16T16:07:31Z"
INFO[0000] ════════════════════════════════════════════════
INFO[0000] Using `git` SHA for StampedBuildID
Command="git rev-parse HEAD" SHA=adc67a77aef22b6dab9c6156d13853e2cfe06488
INFO[0000] Provisioning service
BuildID=adc67a77aef22b6dab9c6156d13853e2cfe06488
CodePipelineTrigger=
InPlaceUpdates=false
NOOP=false Tags=
WARN[0000] No lambda functions provided to Sparta.Provision()
INFO[0000] Verifying IAM Lambda execution roles
INFO[0000] IAM roles verified Count=0
The end result is a Step function that uses our go
binary, Step functions,
and SNS rather than Lambda functions: