How to deploy an ALB + ASG + EC2 using CDK and TypeScript

Have you heard about Cloud Development Kit or CDK? [Yes, No]

What is AWS CDK?

The AWS Cloud Development Kit (CDK), lets you define your cloud Infrastructure as Code (IaC) in one of five supported programming languages. It is intended for moderately to highly experienced AWS users.

In this blog post, you will see how to create your CDK Construct and why this should be done.

Infrastructure as Code

To use CDK, we should know first what is Infrastructure as Code (IaC). If you never heard about it before, you can view some documentation about the concepts behind it here: (https://containersonaws.com/introduction/infrastructure-as-code/#:~:text=) To summarise, IaC manages infrastructure (Machine, Load Balancers, Network, Services) using configuration files. So basically, instead of going to the console and creating all the resources that your application requires, we write a few lines of code, and it provides everything for us.

You’re probably thinking, ‘But this is nothing new. There are tools like Terraform, Cloud Formation, Ansible, or even bash script to do this simply and clearly.’ And yes, you are right, and they play their role very well. The only difference between these and the CDK is that the CDK allows you to use your expertise in programming languages to create code infrastructure by provisioning resources using AWS CloudFormation. AWS CDK supports TypeScript, JavaScript, Python, Java, C#/.Net, and Go. Additionally, developers can use one of the supported programming languages to define reusable cloud components known as Constructs, and today we are going to build a superpower EC2 Construct.

Let’s code!

How to Create CDK Constructs

First of all, we need to set up our environment. In this case, I will use a docker image using the same principles from 3Musketeers (if you don’t know what this is, I recommend you have a look, it is pretty nice 😉).

Dockerfile

ARG AWS_CDK_VERSION=1.111.0 
FROM node:12-alpine

RUN apk -v --no-cache --update add \
python3 \
ca-certificates \ 
groff \
less \
bash \
make \
curl \
wget \
zip \
git \
&& \
update-ca-certificates && \
pip3 install awscli && \
npm install -g aws-cdk@${AWS_CDK_VERSION} && \
rm -rf /var/cache/apk/*

WORKDIR /work

CMD ["cdk"]

Let’s build the image:

$ docker build -t my-cdk-image:1.11.0 .

Now, let’s get into the docker container. As the Docker is stateless, we are going to share our folder using volumes:

$ docker run --rm -it -v $(pwd):/work my-cdk-image:1.11.0 bash

Create the CDK project

 

1.First let’s create a project folder called cdk-ec2-construct:

$ mkdir cdk-ec2-construct
$ cd cdk-ec2-construct


2. Now create your CDK application:

$ cdk init app --language=typescript Applying project template app for typescript 
# Welcome to your CDK TypeScript project!

This is a blank project for TypeScript development with CDK.

The `cdk.json` file tells the CDK Toolkit how to execute your app.
## Useful commands
* `npm run build` compile typescript to js 
* `npm run watch` watch for changes and compile 
* `npm run test` perform the jest unit tests 
* `cdk deploy` deploy this stack to your default AWS account/region 
* `cdk diff` compare deployed stack with current state 
* `cdk synth` emits the synthesized CloudFormation template

Executing npm install... 
✅ All done!

Exploring the files, we can see that the CLI has done a massive step for us, creating all the structure of folders and initial base files.

We will find our stack file in:

/lib/cdk-ec2-construct-stack.ts

And the main entrypoint of the application is in:

/bin/cdk-ec2-construct.ts

Let’s start creating our Construct (aka module) which we will use within our Stack to make as much EC2 we want.

First, let’s create our Construct file /lib/cdk-ec2-construct.ts:

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

  }
}

Now, as we are using Typescript, we are going to write down our interface to our props.

interface ICdkEc2Props {
  VpcId: string;
  ImageName: string;
  CertificateArn: string;
  InstanceType: string;
  InstanceIAMRoleArn: string;
  InstancePort: number;
  HealthCheckPath: string;
  HealthCheckPort: string;
  HealthCheckHttpCodes: string;
}

Getting some real data from our Account

const vpc = ec2.Vpc.fromLookup(this, 'VPC', {
  vpcId: props.VpcId
})

const ami = ec2.MachineImage.lookup({
  name: props.ImageName
})

Creating the Load Balancer

this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, `ApplicationLoadBalancerPublic`, {
  vpc,
  internetFacing: true
})


const httpsListener = this.loadBalancer.addListener('ALBListenerHttps', {
  certificates: elbv2.ListenerCertificate.fromArn(props.CertificateArn)),
  protocol: elbv2.ApplicationProtocol.HTTPS,
  port: 443
})

Creating the Auto Scaling Group

const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', {
  vpc, 
  instanceType: new ec2.InstanceType(props.InstanceType),
  machineImage: ami, 
  allowAllOutbound: true,
  role: iam.Role.fromRoleArn(this, 'IamRoleEc2Instance', props.InstanceIAMRoleArn),
  healthCheck: autoscaling.HealthCheck.ec2()
})

Including scripts in the user data:

autoScalingGroup.addUserData('sudo yum install -y https://s3.region.amazonaws.com/amazon-ssm-region/latest/linux_amd64/amazon-ssm-agent.rpm')
autoScalingGroup.addUserData('sudo systemctl enable amazon-ssm-agent')
autoScalingGroup.addUserData('sudo systemctl start amazon-ssm-agent')

autoScalingGroup.addUserData('echo "Hello Wolrd" > /var/www/html/index.html')

Now that we have almost everything in place, we need to create the connection between our Load Balancer and our Auto Scaling group, and we can do that by adding a Target Group to our Load Balancer.

httpsListener.addTargets('TargetGroup', {
  port: props.InstancePort,
  protocol: elbv2.ApplicationProtocol.HTTP,
  targets: [autoScalingGroup], //Reference of our Austo Scaling group.
  healthCheck: {
    path: props.HealthCheckPath,
    port: props.HealthCheckPort,
    healthyHttpCodes: props.HealthCheckHttpCodes
  }
})

Also, we will expose our Load Balancer as read-only, so we will be able to access it from our Stack.

export class CdkEc2Construct extends cdk.Construct {
	readonly loadBalancer: elbv2.ApplicationLoadBalancer

  constructor(scope: cdk.Construct, id: string, props: ICdkEc2Props) {
	.
	.
	.
	}
}

Now, our construct should be look like this:

import * as cdk from '@aws-cdk/core'
import * as ec2 from '@aws-cdk/aws-ec2'
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'
import * as targets from '@aws-cdk/aws-elasticloadbalancingv2-targets'
import * as autoscaling from '@aws-cdk/aws-autoscaling'
import * as acm from '@aws-cdk/aws-certificatemanager'
import * as iam from '@aws-cdk/aws-iam'

interface ICdkEc2Props {
  VpcId: string;
  ImageName: string;
  CertificateArn: string;
  InstanceType: string;
  InstanceIAMRoleArn: string;
  InstancePort: number;
  HealthCheckPath: string;
  HealthCheckPort: string;
  HealthCheckHttpCodes: string;
}

export class CdkEc2Construct extends cdk.Construct {
	readonly loadBalancer: elbv2.ApplicationLoadBalancer

  constructor(scope: cdk.Construct, id: string, props: ICdkEc2Props) {
    super(scope, id)

    const vpc = ec2.Vpc.fromLookup(this, 'VPC', {
      vpcId: props.VpcId
    })

    const ami = ec2.MachineImage.lookup({
      name: props.ImageName
    })

    this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, `ApplicationLoadBalancerPublic`, {
      vpc,
      internetFacing: true
    })


    const httpsListener = this.loadBalancer.addListener('ALBListenerHttps', {
      certificates: elbv2.ListenerCertificate.fromArn(props.CertificateArn),
      protocol: elbv2.ApplicationProtocol.HTTPS,
      port: 443,
      sslPolicy: elbv2.SslPolicy.TLS12
    })


    const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', {
      vpc, 
      instanceType: new ec2.InstanceType(props.InstanceType),
      machineImage: ami, 
      allowAllOutbound: true,
      role: iam.Role.fromRoleArn(this, 'IamRoleEc2Instance', props.InstanceIAMRoleArn),
      healthCheck: autoscaling.HealthCheck.ec2(),
    })


    autoScalingGroup.addUserData('sudo yum install -y https://s3.region.amazonaws.com/amazon-ssm-region/latest/linux_amd64/amazon-ssm-agent.rpm')
    autoScalingGroup.addUserData('sudo systemctl enable amazon-ssm-agent')
    autoScalingGroup.addUserData('sudo systemctl start amazon-ssm-agent')
   
    autoScalingGroup.addUserData('echo "Hello Wolrd" > /var/www/html/index.html')

  
    httpsListener.addTargets('TargetGroup', {
      port: props.InstancePort,
      protocol: elbv2.ApplicationProtocol.HTTP,
      targets: [autoScalingGroup], 
      healthCheck: {
        path: props.HealthCheckPath,
        port: props.HealthCheckPort,
        healthyHttpCodes: props.HealthCheckHttpCodes
      }
    })

  }
}

We have built our construct, let’s create our Stack. For that, we will edit the file: /lib/cdk-ec2-construct-stack.ts

import * as cdk from '@aws-cdk/core'
import * as route53 from '@aws-cdk/aws-route53';
import * as route53Targets from '@aws-cdk/aws-route53-targets';

import { CdkEc2Construct } from '../lib/cdk-ec2-construct.ts';

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

    const app = new CdkEc2Construct(this, 'EC2Test', {
      VpcId: "vpc-123456890123";
      ImageName: "Amazon 2 Linux";
      CertificateArn: "rn:aws:acm:us-east-1:123456789:certificate/be12312-ecad-3123-1231s-123ias9123";
      InstanceType: "t3.micro";
      InstanceIAMRoleArn: "arn:aws:iam::123456789:role/ec2-role";
      InstancePort: 80;
      HealthCheckPath: "/";
      HealthCheckPort: "80";
      HealthCheckHttpCodes: "200";
    })

    const route53_hosted_zone = route53.HostedZone.fromLookup(this, 'MyZone', {
      domainName: 'labs2.dnx.host'
    })

    new route53.ARecord(this, 'AliasRecord', {
      zone: route53_hosted_zone,
      target: route53.RecordTarget.fromAlias(new alias.LoadBalancerTarget(app.loadBalancer)),
      recordName: 'cdk.labs2.dnx.host'
    })

  }
}

We should now be able to deploy our Stack. To do that, we just need to run a simple command. Then the framework will take care of everything for us, build the code, create a Cloud Formation file, deploy the Cloud Formation, and monitor it for us.

$ cdk deploy

Wrapping up

 

If you already know Terraform or Cloud Formation, you may be wondering, ‘But that’s it? Isn’t it missing resources? Where are the Security Groups? Where are all the extra settings needed to deploy a framework like this?’.

Well, this is the magic that the CDK provides us. As there is a library behind all the methods and functions, it sees all the dependencies and automatically creates the missing resources for us, connecting them so that everything has a connection with as little access as possible, leaving what is necessary for the correct function between the resources. For example, the instance’s Security Group. As we marked that the EC2 instance listens on port 80, only port 80 will be added to the Security Group as an ingress value.

At DNX Solutions, we work to bring a better cloud and application experience for digital-native companies in Australia.

Our current focus areas are AWS, Well-Architected Solutions, Containers, ECS, Kubernetes, Continuous Integration/Continuous Delivery and Service Mesh.

We are always hiring cloud engineers for our Sydney office, focusing on cloud-native concepts.

Check our open-source projects at https://github.com/DNXLabs and follow us on Twitter, Linkedin or Facebook.

Stay informed on the latest
insights and tech-updates

No spam - just releases, updates, and tech information.