Easily deploying infrastructure as code via AWS CDK

AWS-CDK

When you have to deploy / create / manage infrastructure do you usually do it manually in a more traditional approach or do you use an IaC approach like Terraform, Cloudformation or Ansible ? If you do, you might have faced quite a few challenges and even have been overwhelmed by the many complex configurations, vast amounts of boilerplate code and the steep learning curve of these. I went through all these transitions from manually creating / managing infrastructure to Cloudformation/Serverless Framework, then onto Terraform and finally ended up on CDK which I think is great !

These are great frameworks and each has its own strengths. However one thing that has always bothered me was how complex some of these could get and if all that complexity from the get go was really necessary. Some of the frameworks attempts to simplify this, for example Serverless Framework allows you to easily define a serverless architecture(API Gateway --> Lambdas) quite easily. However when defining the same architecture in Terraform it can get quite complex with 6 times more configurations/code. This is understandable as Serverless Framework is opinionated and optimized for Serverless architecture while Terraform tries to be unopinionated and expose and allow quite a bit lot of configuration/customization from the get go.

CDK aims to help with these challenges and makes IaC very much more enjoyable and simpler. However you might be left with a few questions about CDK, I have managed to summarize them below covering as much ground as I can.

What is AWS CDK ?

The AWS Cloud Development Kit (AWS CDK) is an open source software development framework to model and provision your cloud application resources using programming languages that you are familiar with. At time of writing this the supported languages for CDK are TypeScript/Javascript, Python , Java and C#.

How does it work ?

How it works

How it works is very simple, you are provided with a set of available modules/packages to use with your favorite language. Which you can use and configure them to whatever shape or form you need your infrastructure to be. The AWS CDK can then be used to synthesize a CloudFormation(CLF) template. This can then be deployed with all the benefits a CLF template has.

What are the benefits of using this ?

  • You get to program / use them with a language that you are familiar with. This I consider as one of the main points for using CDK which is kind of bridging the gap between devops and development by quite a bit.
  • It also is way simpler and efficient with less boilerplate code to write, most services are pre-configured with proven defaults recommended by experts. This means you will make less blunders or cause security vulnerabilities when defining your infrastructure.
  • You are able to build Customizable and shareable modules. Since it has all the power/flexibility offered in a programming language you are able to expose your architecture in a very customizable/reusable way which will make it easier for your target audience to use it.
  • Another point to mention is that the infrastructure is unit testable which is a whole new level from any other IaC framework, for more info click here.

What got me to try this out ?

So as I was working on my latest project - hacking together a blogging template for Ravensoft which essentially a Headless CMS + Frontend. As an initial step for setting up the project I wanted to create a CI/CD pipeline along with IaC to generate the needed Infrastructure. This would allow me to easily deploy as many instances of this template as we want for multiple clients/users on demand with little to no manual intervention.

I was looking into some options to do this, the CMS was sort of a monolith which meant deploying it on a lambda is not the most ideal option, so my usual go to serverless architecture wasn't going to work. This needed a different architecture,which is scalable yet at a relatively low cost to start off with. So I went with the following architecture which is reletively simple and common.

Architecture-ELB

What this essentially does is to allow us to push a dockerized application into a container registry(AWS ECR), which is added as a "Tasks" on the ECS layer which can scale on demand within the EC2 instances we specify.

Defining the infrastructure for this could be a nightmare in most other IaC frameworks. However AWS CDK simplifies this quite a bit as you would see below, so I have chosen my favorite language(JavaScript/TypeScript) to define the resources. However there are few prerequisites for this.

  • Make sure you have Node.js (>= 10.3.0)
  • Make sure you create an aws profile with a new user using (IAM) read more about how to do that here.

Initial Setup (for TS/JS)

  1. Run npm install -g aws-cdk - Installs AWS-CDK globally.
  2. Run mkdir my-cdk-app && cd my-cdk-app - Creates a new directory for your app and changes directory into it.
  3. Run cdk init --language typescript - Creates an empty app with minimal boilerplate code needed to create a basic stack.

checkout Getting started guide for more info.

Defining the stack.

Now that you have a boilerplate generated for you. You can open lib/my-cdk-app.ts and start defining the infrastructure. The code would come to be something like this once completed...

import * as cdk from '@aws-cdk/core';
import * as path from 'path';
import * as ecs from '@aws-cdk/aws-ecs';
import {
  TaskDefinition,
  Compatibility,
  ContainerImage,
} from '@aws-cdk/aws-ecs';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecsPatterns from '@aws-cdk/aws-ecs-patterns';

import { SubnetType } from '@aws-cdk/aws-ec2';

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

    const vpc = new ec2.Vpc(this, 'VPC', {
      subnetConfiguration: [{
        cidrMask: 24,
        name: 'Public',
        subnetType: SubnetType.PUBLIC,
      }]
    });

    // ECS cluster/resources
    const cluster = new ecs.Cluster(this, 'app-cluster', {
      clusterName: 'app-cluster',
      vpc,
    });

    const taskDefinition = new TaskDefinition(this, 'Task', {
      compatibility: Compatibility.EC2,
      memoryMiB: '512',
      cpu: '256',
    });

    taskDefinition
      .addContainer('cms-img', {
        image: ContainerImage.fromAsset(path.join(__dirname, '../../')),
        memoryLimitMiB:256,
        cpu: 256,
      })
      .addPortMappings({ containerPort: 1337 });

    cluster.addCapacity('app-scaling-group', {
      instanceType: new ec2.InstanceType('t2.micro'),
      desiredCapacity: 1,
      maxCapacity: 4
      minCapacity: 1
    });

    new ecsPatterns.ApplicationLoadBalancedEc2Service(
      this,
      'app-service',
      {
        cluster,
        cpu: 256,
        desiredCount: 1,
        minHealthyPercent: 50,
        maxHealthyPercent: 300,
        serviceName: 'cmsservice',
        taskDefinition: taskDefinition,
        publicLoadBalancer: true,
      },
    );
  }
}

If you are wondering what the hell all this means...

  1. Creates a virtual private cloud (VPC) with a single public subnet
    const vpc = new ec2.Vpc(this, 'VPC', {
      subnetConfiguration: [{
        cidrMask: 24,
        name: 'Public',
        subnetType: SubnetType.PUBLIC,
      }]
    });
  1. Creates an ECS cluster within our VPC.
    const cluster = new ecs.Cluster(this, 'app-cluster', {
      clusterName: 'app-cluster',
      vpc,
    });
  1. Creates a task definition by specifying a dockerfile as the task to run. In our case it points to the directory with the dockerfile for our app. We also map the port where our application runs (in my case 1337) to the default http port.
const taskDefinition = new TaskDefinition(this, 'Task', {
      compatibility: Compatibility.EC2,
      memoryMiB: '512',
      cpu: '256',
    });

taskDefinition
  .addContainer('cms-img', {
    image: ContainerImage.fromAsset(path.join(__dirname, '../../')),
    memoryLimitMiB:256,
    cpu: 256,

  })
  .addPortMappings({ containerPort: 1337 });
  1. The following would define the cluster capacity with an upperbound and lowerbound for the number of EC2 instances possible within the cluster.
cluster.addCapacity('app-scaling-group', {
    instanceType: new ec2.InstanceType('t2.micro'),
    desiredCapacity: 1,
    maxCapacity: 4
    minCapacity: 1
});
  1. Finally we define what type of pattern we would be using by using the module @aws-cdk/aws-ecs-patterns and then pasing our cluster and task definitions into it.
new ecsPatterns.ApplicationLoadBalancedEc2Service(
  this,
  'app-service',
  {
    cluster,
    cpu: 256,
    desiredCount: 1,
    minHealthyPercent: 50,
    maxHealthyPercent: 300,
    serviceName: 'cmsservice',
    taskDefinition: taskDefinition,
    publicLoadBalancer: true,
  },
);

Once we are done we can run npm run build and cdk bootstrap && cdk deploy and then the relevant infrastructure would be created along with which your dockerized app would be deployed. An output like the following would be shown where it was deployed / accessible at.

AWS-CDK-output

In my next followup article I would be describing how we can set up a CI/CD pipeline for this using Github Actions. I hope that this would get you to explore and learn more about AWS CDK. Please do let me know your comments/ thoughts in the comments below.