EC2 Instance Example in AWS CDK - Complete Guide

avatar

Borislav Hadzhiev

Fri May 07 20215 min read

A complete example of creating an EC2 instance with User Data in AWS CDK.

Table of Contents #

  1. Creating an EC2 Instance in AWS CDK
  2. Adding User Data to an EC2 Instance in AWS CDK
  3. Deploying the EC2 Instance Provisioned with AWS CDK

Creating an EC2 Instance in AWS CDK #

In order to create an EC2 instance in AWS CDK, we have to instantiate and configure the Instance class.

Let's start by creating:

  • a VPC, in which we will launch our EC2 instance
  • a security group for the instance
  • a role for the instance
The code for this article is available on GitHub
lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

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

    // ๐Ÿ‘‡ create VPC in which we'll launch the Instance
    const vpc = new ec2.Vpc(this, 'my-cdk-vpc', {
      cidr: '10.0.0.0/16',
      natGateways: 0,
      subnetConfiguration: [
        {name: 'public', cidrMask: 24, subnetType: ec2.SubnetType.PUBLIC},
      ],
    });

    // ๐Ÿ‘‡ create Security Group for the Instance
    const webserverSG = new ec2.SecurityGroup(this, 'webserver-sg', {
      vpc,
      allowAllOutbound: true,
    });

    webserverSG.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(22),
      'allow SSH access from anywhere',
    );

    webserverSG.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(80),
      'allow HTTP traffic from anywhere',
    );

    webserverSG.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443),
      'allow HTTPS traffic from anywhere',
    );

    // ๐Ÿ‘‡ create a Role for the EC2 Instance
    const webserverRole = new iam.Role(this, 'webserver-role', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess'),
      ],
    });
  }
}

Let's go over what we did in the code snippet.

  1. we created a VPC with a PUBLIC subnet group. Our EC2 instance will be a web server, so we want it to be accessible from the world

  2. we created a security group for our EC2 instance. The security group allows all outbound access.

    For inbound access we have allowed:

    • SSH access from anywhere
    • HTTP traffic on port 80 from anywhere
    • HTTPS traffic on port 443 from anywhere
  3. we created an IAM role our EC2 instance will assume. As an example, we have attached a managed policy that grants S3 read access to the role.

The code for this article is available on GitHub

Now, let's add the code that creates the EC2 Instance:

lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

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

    // ... rest

    // ๐Ÿ‘‡ create the EC2 Instance
    const ec2Instance = new ec2.Instance(this, 'ec2-instance', {
      vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      role: webserverRole,
      securityGroup: webserverSG,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO,
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),
      keyName: 'ec2-key-pair',
    });
  }
}

In the code snippet, we created an EC2 instance, passing it the following props:

NameDescription
vpcthe VPC, the instance will be launched in
vpcSubnetsin which subnet of the VPC the instance will be launched
rolethe IAM role, the EC2 instance will assume
securityGroupa security group to assign to the EC2 instance
instanceTypethe class and size configuration for the EC2 instance, in our case t2.micro
machineImagethe Amazon Machine Image of the EC2 instance, in our case Amazon Linux 2
keyNamethe name of the SSH key pair we are going to use, to SSH into the instance
In the code example we've used the name ec2-key-pair for the name of the key. If a key with that name does not exist in your default AWS region, you will get an error when trying to deploy the EC2 instance.

Let's create a key pair in your default AWS region with the name of ec2-key-pair. Alternatively you could replace the value of the keyName prop to one that already exists in your account.

To create a key pair, open the EC2 Management console and click on Key Pairs > Create key pair.

Depending on your operating system you can choose between pem (Mac, Linux) and ppk (Windows). I'm on Linux, so I've selected pem:

ec2 instance key pair creation

After the key pair has been created, navigate to the directory it was downloaded in and change its permissions:

shell
chmod 400 ec2-key-pair.pem

Next, we are going to add a User Data script to the ec2 instance.

Adding User Data to an EC2 Instance in AWS CDK #

EC2 User data allows us to add commands to the startup script of an instance.

To add user data, we are going to use the addUserData method on our EC2 instance.

The code for this article is available on GitHub
lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';
import {readFileSync} from 'fs';

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

    // ... rest

    // ๐Ÿ‘‡ load contents of script
    const userDataScript = readFileSync('./lib/user-data.sh', 'utf8');
    // ๐Ÿ‘‡ add the User Data script to the Instance
    ec2Instance.addUserData(userDataScript);
  }
}

In the code snippet we used the readFileSync method from the fs module to load the contents of a script. We then passed the result to the addUserData method of the EC2 instance.

Let's add the code for the user data script at lib/user-data.sh:

lib/user-data.sh
#!/bin/bash
yum update -y
sudo su

amazon-linux-extras install -y nginx1
systemctl start nginx
systemctl enable nginx

chmod 2775 /usr/share/nginx/html
find /usr/share/nginx/html -type d -exec chmod 2775 {} \;
find /usr/share/nginx/html -type f -exec chmod 0664 {} \;

echo "<h1>It worked</h1>" > /usr/share/nginx/html/index.html

The startup script installs and starts nginx and writes a simple "It worked" heading tag to the root of our web server.

Deploying the EC2 Instance Provisioned with AWS CDK #

Let's execute a deployment and test if everything works as expected:

shell
npx cdk deploy

It takes about 5 minutes for the EC2 instance to be launched in my default AWS region.

The inbound rules for the security group of the EC2 instance allow access on ports 22, 80, 443:

ec2 security inbound rules

Let's try go access the nginx server we installed and started. Copy the Public IPv4 address of your EC2 instance:

ec2 instance public ipv4

Now paste the public IPv4 address in your browser and you should see the response from our nginx web server:

ec2 instance response

Now let's SSH into the instance and change the message our nginx web server responds with.

Open your terminal in the directory where you placed your ec2-key-pair.pem file and execute the following command:

shell
ssh -i "ec2-key-pair.pem" ec2-user@YOUR_PUBLIC_IPv4

Once you're in the instance change the response of the web server with the following commands:

shell
sudo su

echo "<h1>Hello World</h1>" > /usr/share/nginx/html/index.html

If you now paste the public IPv4 address of the EC2 instance in your browser, you will se the updated response:

ec2 instance response updated

Clean up #

To delete the resources we have provisioned, execute the destroy command:

shell
npx cdk destroy

Further Reading #

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee