Deploying Fargate ECS and DynamoDB with CDK

4 minute read

In a previous blog post, I showed how you can use AWS Cloud Development Kit (CDK) to deploy an Elastic Beanstalk application with an RDS backend. In this post, I’m going to switch it up with a more serverless architecture with ECS powered by Fargate, DynamoDB tables, and an Application Load Balancer for good measure.

Getting Started

The following sections will briefly go over code and is meant to be read while viewing the code on the GitHub.

app.py

Just like the code from the previous, we start with the main point of entry, the app.py file.

The Props Dictionary holds some of the common values we’ll make use in the code that does the heavy lifting. Something new here is the introduction of runtime contexts to pass two account-specific values, noted by the -c VAR flag.. You could use dotenvs, but I wanted to show more CDK concepts this time.

DynamoDBStack

The DynamoDB is setup first, but the two stacks are not dependent upon each other and can swap places in order.

The prepare_import_data function highlights one of the main benefits of using CDK over CloudFormation, or even Terraform. This is a custom function I’ve created to handle importing data from CSV files (located in ./data) and bringing it into the DynamoDB tables that will be created later. You can obviously create functions or classes to handle anything you can imagine.

Digging deeper into this function, we’re formatting the data for PutRequest, a DynamoDB data type that will be used in an API call in this stack.

There are three tables being created: “Resource”, “Category”, and “Bookmark”. The Resource table has an added Global Secondary Index.

A cool feature of CDK is the ability to create custom resources which are AWS Lambda-backed functions. This function will load our BatchWriteItem API call to a short-lived Lambda function that will be responsible for actually making the API request to the DynamoDB table. We don’t have to worry about creating and managing a Lambda function.

The props.copy() is going to copy the props Dictionary we passed to it from app.py and copy it, combining any additions you might add to it so it can be passed to another stack. In this case, looking at app.py we pass this to the ECSStack.

ECSStack

While this stack is the bulk of our infrastructure, ironically it has fewer lines of code than the DynamoDB stack.

The VPC is created, and that’s all we really need to do in terms of our VPC configuration. The Fargate construct will create nearly everything else for us.

The construct for creating the ECS Cluster object is straightforward.

We have to create a Task Execution Role, so using the Role construct and giving it the correct service principal. This is so the AmazonEC2ContainerRegistryReadOnly managed role can get attached to it. Allows the task to pull from an ECR registry on launch if needed.

The ecs_patterns is a high-level construct library, and it will require less code to accomplish what we need. This particular code creates the service which will be underneath the cluster that will be created. This service will have an Application Load Balancer created with the target group being the service and its tasks. The image in this example is an image from Docker Hub, but it can be from any Docker registry, including ECR. There are also example environmental variables set, and Docker labels for the sake of example.

Since the Fargate construct created the security group, we use the object holding it to modify the ingress rule for the Security Group associated with the ECS Service. The SG for the ECS Service will allow port 8080 from the ELB and the CIDR 10.0.0.0/16. The ELB will use a default rule on port 80 from 0.0.0.0/0.

The db_stack object was passed from the DynamoDB stack through the main app.py to this ECSStack so that we can modify the Fargate task role to allow access to the DynamoDB Tables created in the previous stack.

Putting it all together

The last part simply prints the load balancer DNS to the terminal.

cdk deploy --all -c account_id="111111111111" -c preferred_region="us-east-2"

Why Contexts

The CDK Documentation recommends for production stacks to explicitly specify the environment (Account no. & Region) for each stack in your app using the env property.

Instead of hardcoding the values, I opted to use runtime contexts as mentioned earlier.

Found in app.py:

preferred_region = app.node.try_get_context("preferred_region")
account_id = app.node.try_get_context("account_id")
env = core.Environment(region=preferred_region, account=account_id)

Defined just below:

db_stack = DynamoDBStack(app, f"{props['namespace']}-Dynamo",props,env=env)
ecs_stack = ECSStack(app, f"{props['namespace']}-ECS",db_stack.output_props, db_stack=db_stack, env=env)

Conclusion

After this, you will have an ECS Fargate service setup behind an Application Load Balancer. The ALB will be setup as you expected, forwarding traffic on the ports you specified, and performing health checks on the traffic port (8080).

The ECR repository can easily be replaced by a DockerHub registry, or any supported image repository. There’s obviously a lot more you can do here, but I think this is a good start.