jk's blog

23 Sep 2022

Django on ECS with CDK

Overview

In the previous post on this topic, we deployed a Django app and deployed it to AWS ECS using Terraform.

This time, we’re going to make the development process a lot easier by using the AWS Cloud Development Kit (CDK) to automate the creation of our infrastructure-as-code.

Django is a Python web framework that makes it easy to develop web apps quickly. We’ll be building a Django container image and deploying it with AWS ECS, a fully managed container orchestration service.

(It’s important to note that some of these steps will incur charges in your AWS account!)

Create a Django Docker image

Follow the previous instructions

If you didn’t follow the steps in the previous post to create a Django Docker image, go follow them now.

You should have a Docker image published to ECR looking something like this:

123456789012.dkr.ecr.ap-southeast-2.amazonaws.com/django-app:latest

Add a startup command to your container image

Firstly, you need to add a CMD instruction to your Dockerfile. Previously, we relied on a task definition template file to issue our startup command.

You won’t be able to do that with CDK, so you need to make sure your container image starts our Django server on boot.

Add this line to the end of your Dockerfile:

CMD ["gunicorn", "-w", "3", "-b", "0.0.0.0:8000", "django_aws.wsgi:application"]

Change your container image’s healthcheck URL

Secondly, you need to change your healthcheck URL to /. There’s currently no way to pass a Amazon.CDK.AWS.ECS.HealthCheck interface to the ApplicationLoadBalancedFargateService construct, so we need Django to issue healthchecks on the root of the URL.

Edit app/django_aws/middleware.py and change this line:

if request.META['PATH_INFO'] == '/health':

To this:

if request.META['PATH_INFO'] == '/':

This will respond with a 200 OK on the root URL if the Django application is available.

Rebuild your container image and publish it to ECR again.

Create your CDK project

Initialise your project

Once you’ve installed the CDK, create a new directory. Make sure it’s called ecs-cdk, because CDK uses this directory name to name things in the generated code!

mkdir ecs-cdk
cd ecs-dck

Then initialise a new app:

cdk init app --language typescript

Define your VPC

Start out by creating just VPC. We’ll add more resources to it later on.

Delete all the lines from ecs-cdk/lib/ecs-cdk-stack.ts. Replace it with this:

import ec2 = require('aws-cdk-lib/aws-ec2');
import cdk = require('aws-cdk-lib');
import { Construct } from 'constructs';

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

    // Create VPC
    // NOTE: Limit AZs to avoid reaching resource quotas
    const vpc = new ec2.Vpc(this, 'MyVpc', { maxAzs: 2 });

  }
}

All we’re doing here is creating a new VPC across two Availability Zones (AZs) in your default region.

Note that we needed to import the aws-cdk-lib/aws-ec2 module to define a new VPC.

Create your VPC

From your ecs-cdk/ directory, deploy the stack:

cdk deploy

After a few minutes, you should get a success message.

You can go into the AWS Console and confirm that your new VPC has been created.

Note that you didn’t need to define a CIDR range, subnets, route tables, an Internet Gateway, or NAT Gateways - CDK created a standard vanilla VPC with smart defaults.

Doing something similar in Cloudformation is not difficult, but it’s definitely far more verbose!

Add an ECS cluster

Replace your version of ecs-cdk/lib/ecs-cdk-stack.ts with the following, being sure to replace your AWS region and AWS account ID on line 20:

import ec2 = require('aws-cdk-lib/aws-ec2');
import ecs = require('aws-cdk-lib/aws-ecs');
import ecs_patterns = require('aws-cdk-lib/aws-ecs-patterns');
import ecr = require('aws-cdk-lib/aws-ecr')
import cdk = require('aws-cdk-lib');
import { Construct } from 'constructs';

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

    // Create VPC and Fargate Cluster
    // NOTE: Limit AZs to avoid reaching resource quotas
    const vpc = new ec2.Vpc(this, 'MyVpc', { maxAzs: 2 });
    const cluster = new ecs.Cluster(this, 'Cluster', { vpc });
    
    const djangoRepo = ecr.Repository.fromRepositoryArn(
      this,
      'django-app',
      'arn:aws:ecr:<<AWS REGION>>:<< AWS ACCOUNT ID>>:repository/django-app'
    )
    
    // Instantiate Fargate Service with just cluster and image
    new ecs_patterns.ApplicationLoadBalancedFargateService(this, "FargateService", {
      cluster,
      taskImageOptions: {
        image: ecs.ContainerImage.fromEcrRepository(djangoRepo, 'latest'),
        containerPort: 8000
      },
    });
  }
}

In this iteration, you’re:

  • Defining an ECR repository
  • Creating a new ECS Fargate cluster
  • Creating an ECS Task Definition referring to your ECR repo
  • Creating an ECS Service to load balance across your running containers

Note that you’re using the ApplicationLoadBalancedFargateService construct to abstract away most of these ECS internals. This is a high-level construct that simplifies the creation of an ECS-based service.

Again, run cdk deploy and watch your resources being created.

Add an RDS instance

Again, replace your version of ecs-cdk/lib/ecs-cdk-stack.ts with the following. And again, be sure to replace your AWS region and AWS account ID in the ECR definition:

import ec2 = require('aws-cdk-lib/aws-ec2');
import ecs = require('aws-cdk-lib/aws-ecs');
import ecs_patterns = require('aws-cdk-lib/aws-ecs-patterns');
import ecr = require('aws-cdk-lib/aws-ecr')
import rds = require('aws-cdk-lib/aws-rds')
import secretsmanager = require('aws-cdk-lib/aws-secretsmanager')
import cdk = require('aws-cdk-lib');
import { Construct } from 'constructs';

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

    // Create VPC and Fargate Cluster
    // NOTE: Limit AZs to avoid reaching resource quotas
    const vpc = new ec2.Vpc(this, 'MyVpc', { maxAzs: 2 });
    const cluster = new ecs.Cluster(this, 'Cluster', { vpc });
    
    const djangoRepo = ecr.Repository.fromRepositoryArn(
      this,
      'django-app',
      'arn:aws:ecr:<<AWS REGION>>:<< AWS ACCOUNT ID>>:repository/django-app'
    )
    
    const dbUser = "dbUser";
    const dbName = "mydb";
    const engine = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_11 });
    const dbCreds = new rds.DatabaseSecret(this, 'DBSecret', {
      secretName: "/rds/creds/ecs-rds",
      username: dbUser,
    });
    const db = new rds.DatabaseInstance(this, 'RDSInstance', {
      engine,
      vpc,
      credentials: rds.Credentials.fromSecret(dbCreds),
      databaseName: dbName,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.LARGE),
    })

    // Instantiate Fargate Service with just cluster and image
    new ecs_patterns.ApplicationLoadBalancedFargateService(this, "FargateService", {
      cluster,
      taskImageOptions: {
        image: ecs.ContainerImage.fromEcrRepository(djangoRepo, 'latest'),
        containerPort: 8000,
        environment: {
          RDS_DB_NAME: dbName,
          RDS_USERNAME: dbUser,
          RDS_PASSWORD: dbCreds.secretValue.unsafeUnwrap(),
          RDS_HOSTNAME: db.dbInstanceEndpointAddress,
          RDS_PORT: "5432",
        }
      },
    });
  }
}

Here, you’re creating a new Secrets Manager secret to use as your database password. Then you’re adding an RDS instance in a single AZ. Finally, you’re passing your credentials, database name, and RDS endpoint as environment variables to your ECS task.

Conclusion

Viewing the output

CDK will give you the URL of your load balancer. It will look something like: EcsCdkStack.FargateService3ServiceURL123456 = http://EcsCd-Farga-123ABCD-ZYXW0987.ap-southeast-2.elb.amazonaws.com.

Add a /polls/ to the end of the URL so it looks like this: http://EcsCd-Farga-123ABCD-ZYXW0987.ap-southeast-2.elb.amazonaws.com/polls/.

When you visit this URL, you should see a Hello world! displayed.

Congratulations! You’ve successfully deployed a Django container to an AWS ECS cluster using CDK!

Comparing CDK to Terraform

I hope you can see how much easier it can be to write infrastructure-as-code using CDK than with traditional infracode tools like Terraform or CloudFormation.