Simple AWS ECS Deployment With AWS CDK

AWS CDK provides a library of constructs in many programming languages to easily automate AWS infrastructure. In addition, these constructs can be customized and be used to provision your application as required.

If you have a production application running on any cloud platform, you must think of automating the cloud infrastructure.

Let's deploy the service created in a previous post. Note that I have a health check endpoint, such that the Load balancer can call this endpoint to ensure the container that runs this API is healthy.

const express = require("express");
const app = express();

app.get("/", (req, res) => res.send("Thabo Lebelo 🚀"));

app.get("/health", (req, res) => {
  res.status(200);
  res.send("healthy");
});

app.listen(1000, () => {
  console.log("App listening on port 1000!");
});
Basic NodeJS app with two endpoints

I will still Dockerize the API and this is the Dockerfile that will create a docker image of the API.

FROM alpine:latest
RUN apk add --no-cache nodejs npm

WORKDIR /app
COPY package.json /app
RUN npm install

COPY . /app

EXPOSE 1000
CMD ["npm", "start"]

Overview of the Infrastructure

I need to provision several resources in AWS in order to get the API running.

  • A VPC (virtual private cloud) to isolate our application within its own network.
  • A load balancer to route traffic to the API (the running container)
  • An AWS ECS cluster to run the API. Within the cluster I will create a task definition that define the API and a service to run the task definition.

Installing CDK toolkit

I am going to use TypeScript to provision cloud resources using CDK. Therefore make sure you have NodeJS (>= 10.3.0) installed on your computer as well, if you wish to follow along. NodeJS installation is going to install the Node Package Manager(npm) which is needed to install CDK toolkit:

npm install -g aws-cdk
Install CDK globally on your computer
cdk --version
// the version used for this example: 2.8.0 (build 8a5eb49)
Verify the CDK installation

A few commands to be aware of as we continue building the infrastructure:

  • If this is your first time using the CDK you'll need to run cdk bootstrap
  • cdk init --language typescript initialize a new typescript CDK application
  • cdk synth generates the CloudFormation template for your infrastructure. This template eventually ends up in AWS CloudFormation as a stack that creates all the resources specified in the template.
  • cdk diff inspect the differences between a currently deployed stack and any changes made locally. I do this before deploying because there are some useful information printed to the terminal.
  • cdk deploy deploy the current CDK application to AWS CloudFormation. CloudFormation will then use the template to create the necessary AWS resources.
  • cdk destroy destroy all the resource created by CloudFormation.

Initializing a CDK project

Now let’s create a new CDK project using the CDK cli. I’ll create a new directory and issue the cdk init command.

// Creating a new directory and change into that directory
mkdir cdk-ecs-demo
cd cdk-ecs-demo
// Initializing a cdk typescript project
cdk init --language typescript

At the root level of the CDK project we created earlier, you should find the lib folder. Let’s create a file called, fargate.ts inside this folder. The code is in this repo.

import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";
import * as ecs from "@aws-cdk/aws-ecs";
import * as ecr from "@aws-cdk/aws-ecr";
import * as elbv2 from "@aws-cdk/aws-elasticloadbalancingv2";

export class FargateDemoStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // base infrastucture
        const vpc = new ec2.Vpc(this, "thabolebeloVPC", {
            maxAzs: 2,
            natGateways: 1,
        });

        const cluster = new ecs.Cluster(this, "thabolebeloCluster", {
            vpc: vpc,
            clusterName: "API"
        });

        const alb = new elbv2.ApplicationLoadBalancer(this, "thabolebeloALB", {
            vpc: vpc,
            internetFacing: true,
            loadBalancerName: 'API'
        });

        // get our image
        const repo = ecr.Repository.fromRepositoryArn(this, "thabolebeloRepo",
            "arn:aws:ecr:<region>:<account-number>:repository/<repo-name>"
        );
        const image = ecs.ContainerImage.fromEcrRepository(repo, 'latest')

        // task definition
        const task = new ecs.TaskDefinition(this, "thabolebeloTaskDefinition", {
            compatibility: ecs.Compatibility.EC2_AND_FARGATE,
            cpu: '256',
            memoryMiB: '512',
            networkMode: ecs.NetworkMode.AWS_VPC
        });

        const container = task.addContainer("thabolebeloContainer", {
            image: image,
            memoryLimitMiB: 512
        });

        container.addPortMappings({
            containerPort: 1000,
            protocol: ecs.Protocol.TCP
        });

        // create service
        const service = new ecs.FargateService(this, "thabolebeloService", {
            cluster: cluster,
            taskDefinition: task,
            serviceName: 'service'
        });

        // network the service with the load balancer
        const listener = alb.addListener('listener', {
            open: true,
            port: 80
        });

        // add target group to container
        listener.addTargets('service', {
            targetGroupName: 'ServiceTarget',
            port: 80,
            targets: [service],
            healthCheck: {
                enabled: true,
                path: '/health'
            }
        })

    }
}
Create the infrastructure

Let's dig in and see what this code is going to create in AWS

  1. A VPC with 2 availability zones and all associated networking by default. This includes, public and private subnets, internet gateway, NAT gateway, routing tables, etc.
  2. An ECS cluster
  3. An internet facing (public) load balancer
  4. Fetching the image pushed to AWS ECR
  5. Create a task definition
  6. Attach a container to the task definition
  7. Configure port mappings for the container
  8. Create the ECS service to run the container
  9. Create a listener for the load balancer
  10. Route traffic from the listener to the service

Deploying the Infrastructure to AWS

Finally, I can deploy the infrastructure to AWS. I can update the bin/cdk.ts file to execute the CDK stack when running cdk deploy

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { FargateDemoStack } from '../lib/fargate'

const app = new cdk.App();

new FargateDemoStack(app, "FargateDemoStack", {
    env: { account: "<account-id>", region: "<region>"}
});

Before deploying the stack we can inspect the resources that will be created by the CDK. cdk diff will output a bunch of text explaining roles and security group changes, and a list of all the resources that will be created. cdk deploy will generate a CloudFormation template, push it to AWS then begin creating the infrastructure on AWS. Remember, the CDK is all CloudFormation under the hood.

Once the stack is deployed, go to the AWS load balancer console on AWS, find the load balancer DNS and copy, then paste that into your URL bar to see the service response.

Run cdk destroy to destroy all the resources created in AWS. Checkout the code in this repo.