DEV Community

Cover image for Part 3 - Simple EC2 instance - Awesome AWS CDK
Emmanuel K
Emmanuel K

Posted on • Updated on

Part 3 - Simple EC2 instance - Awesome AWS CDK

Introduction

Most tutorials on AWS try to get you to deploy the world famous t2.micro instance (a small virtual server under the free usage tier on AWS) using the AWS console. You then go on to install something like Wordpress through SSH or through a user script (a script that runs when an instance is created) that you define in the AWS console.

So that's exactly what we're going to deploy. But we're going to use the AWS CDK instead.

This is what you'll learn in this tutorial:

  • Bootstrapping a new CDK application
  • The structure and setup of a CDK application
  • How to provision and setup an ec2 instance
  • How to setup terminal access to the instance via SSH key.
  • How to update the ec2 instance with a user script and update the deployed cdk stack
  • How to write tests for the cdk application
  • How to destroy the deployed stack

Let's go!

Bootstrapping a new CDK Application

Using your terminal, create a new directory simple-ec2 and cd into it:

mkdir simple-ec2 && cd simple-ec2
Enter fullscreen mode Exit fullscreen mode

We previously setup the AWS CDK cli globally. If you haven't done that see Part 2 in this series.

Bootstrap a new cdk project template that uses Typescript:

cdk init --language=typescript
Enter fullscreen mode Exit fullscreen mode

This will initialize a new cdk project for you in Typescript.

  • Run npm update to ensure you're using the latest version of the CDK.
  • If you have version conflicts between @aws-cdk/core and other @aws-cdk sub-packages, then you'll come across some weird errors. @aws-cdk/core and every other imported @aws-cdk/PACKAGE should have the same version.

Structure and Setup of a CDK application

Structure

# tree -I 'node_modules'
.
├── bin
│   └── simple-ec2.ts       # entry point
├── cdk.json
├── jest.config.js          # for tests
├── lib                     # where the infrastructure code you write will go
│   └── simple-ec2-stack.ts 
├── package.json
├── package-lock.json
├── README.md
├── test                    # test folder
│   └── simple-ec2.test.ts
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode
  • ./bin/simple-ec2.ts is the entry point file used by the cdk. This is where you define your stack(s).
  • The IaC that provisions the resources will be inside the lib folder and is required by ./bin/simple-ec2.ts during synth and deploy actions. I'll explain both commands later.
  • ./test/simple-ec2.test.ts contains the template code to test your CDK application

Setup

In ./bin/simple-ec2.ts a new App() is defined and this represents a single stack.

  • We can add a description for our stack in this file.
  • This description will be visible in the Cloudformation console:
// ./bin/simple-ec2.ts

import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { SimpleEc2Stack } from '../lib/simple-ec2-stack';

const app = new cdk.App();
new SimpleEc2Stack(app, 'SimpleEc2Stack', {
   description: 'This is a simple EC2 stack'
});
Enter fullscreen mode Exit fullscreen mode

Let's head over to ./lib/simple-ec2-stack.ts and see what the CDK boostrapped for us:

// ./lib/simple-ec2-stack.ts

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

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

    // The code that defines your stack goes here
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the cdk init command bootstrapped a nice template for us to get to writing our IaC fast. The base class cdk.Stack gives us the ability to create a new Cloudformation stack.

We also need to create a .env file to keep our AWS Account number and the region we will use. In the root of the project create a file called .env and add the following. Replace the xxXxXxxXXxXxx with your AWS account number and use the region you want.

AWS_ACCOUNT_NUMBER=xxXxXxxXXxXxx
AWS_ACCOUNT_REGION=us-west-2
Enter fullscreen mode Exit fullscreen mode

Writing the infrastructure code

We want to create an ec2 instance so we will need the @aws-cdk/ec2 library. (This is the same process for any other AWS service you need to provision resources). We will also need @aws-cdk/aws-iam library to give permissions to our instance to do stuff. We also want to be able to read our .env file so lets also install dotenv package

npm install @aws-cdk/aws-ec2 @aws-cdk/aws-iam dotenv
Enter fullscreen mode Exit fullscreen mode

Remember, since the CDK is written in Typescript and is typed excellently, while typing you can access intellisense and see the various properties of CDK resources e.g. in the image below I can see what properties an instance of ec2.Instance() class has.

Alt Text

Here goes our first iteration:

import * as cdk from '@aws-cdk/core'
import * as ec2 from '@aws-cdk/aws-ec2' // import ec2 library 
import * as iam from '@aws-cdk/aws-iam' // import iam library for permissions

require('dotenv').config()

const config = {
  env: {
    account: process.env.AWS_ACCOUNT_NUMBER,
    region: process.env.AWS_REGION
  }
}

export class SimpleEc2Stack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    // its important to add our env config here otherwise CDK won't know our AWS account number
    super(scope, id, { ...props, env: config.env })

    // Get the default VPC. This is the network where your instance will be provisioned
    // All activated regions in AWS have a default vpc. 
    // You can create your own of course as well. https://aws.amazon.com/vpc/
    const defaultVpc = ec2.Vpc.fromLookup(this, 'VPC', { isDefault: true })

    // Lets create a role for the instance
    // You can attach permissions to a role and determine what your
    // instance can or can not do
      const role = new iam.Role(
        this,
        'simple-instance-1-role', // this is a unique id that will represent this resource in a Cloudformation template
        { assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') }
      )

    // lets create a security group for our instance
    // A security group acts as a virtual firewall for your instance to control inbound and outbound traffic.
    const securityGroup = new ec2.SecurityGroup(
      this,
      'simple-instance-1-sg',
      {
        vpc: defaultVpc,
        allowAllOutbound: true, // will let your instance send outboud traffic
        securityGroupName: 'simple-instance-1-sg',
      }
    )

    // lets use the security group to allow inbound traffic on specific ports
    securityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(22),
      'Allows SSH access from Internet'
    )

    securityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(80),
      'Allows HTTP access from Internet'
    )

    securityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443),
      'Allows HTTPS access from Internet'
    )

    // Finally lets provision our ec2 instance
    const instance = new ec2.Instance(this, 'simple-instance-1', {
      vpc: defaultVpc,
      role: role,
      securityGroup: securityGroup,
      instanceName: 'simple-instance-1',
      instanceType: ec2.InstanceType.of( // t2.micro has free tier usage in aws
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: ec2.MachineImage.latestAmazonLinux({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),

      keyName: 'simple-instance-1-key', // we will create this in the console before we deploy
    })

    // cdk lets us output prperties of the resources we create after they are created
    // we want the ip address of this new instance so we can ssh into it later
    new cdk.CfnOutput(this, 'simple-instance-1-output', {
      value: instance.instancePublicIp
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating a new Key Pair in the AWS Console

Before we try to deploy our newly created instance, we need to go to the AWS console and create a key pair that we will use to access the instance called simple-instance-1-key

  • Log into the AWS console.
  • Go to EC2 dashboard.
  • Go to Key Pairs and click create Key Pair
  • Enter key name as simple-instance-1-key and click create
  • Your new key pair will be created and your browser will automatically download a new .pem file called simple-instance-1-key.pem
  • this is the key file you'll use to gain access to your instance via SSH

Alt Text

  • Create a new directory under .aws/ called pems
mkdir ~./aws/pems/
Enter fullscreen mode Exit fullscreen mode
  • move the newly downloaded file to this directory and give it the necessary permissions
mv ~/Downloads/simple-instance-1-key.pem ~/.aws/pems

# important step or your key file won't work
chmod 400 ~/.aws/pems/simple-instance-1-key.pem
Enter fullscreen mode Exit fullscreen mode
  • Now that we have our key file properly setup, lets deploy our instance!

Deploy

Remember we set up our aws profiles and crednetials in ~/.aws/config and ~/.aws/credentials back in part 2

I will be deploying to my default profile which is linked to my personal AWS account with the region us-west-2.

If you have another profile you want to use then in the commands below use that profile name instead of default.

In your terminal:

cdk synth --profile default
Enter fullscreen mode Exit fullscreen mode

This command will synthesize your stack.

When CDK apps are executed, they produce (or “synthesize”, in CDK parlance) an AWS CloudFormation template for each stack defined in your application.

  • Essentially it will print the cloudformation template for your stack to your console.
  • It's a good way to check that there's nothing wrong with your stack before trying to deploy since cdk synth will verify the resources you are trying to provision can actually be provisioned.
  • You should something like this:
❯ cdk synth --profile default
Resources:
  simpleinstance1role9EEDA67C:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
   ...
   ...
   ...
Enter fullscreen mode Exit fullscreen mode

If the entire stack prints without error then you're okay to go.

Now we can deploy:

cdk deploy --profile default
Enter fullscreen mode Exit fullscreen mode

You should get a prompt accessing for Cloudformation to allow the creation of resources that need approval. Type y and press ENTER to continue with the deployment:
Alt Text

You'll start seeing the output from Cloudformation in your console as the stack is being created.

When the stack has been successfully deployed, you should see:
Alt Text

Notice the output which we defined at the end of the stack. The CDK printed the public ip address of the newly created instance for us because we told it to, awesome! You can use this method to print out any value when a stack has successfully deployed.

You can alternatively get this information from the ec2 console by checking on the instances dashboard.

Accessing the instance

Let's ssh into our newly created instance with our key file and the public ip address.

Note: that the default user for AMAZON LINUX images is ec2-user

ssh -i ~/.aws/pems/simple-instance-1-key.pem ec2-user@34.220.79.175
Enter fullscreen mode Exit fullscreen mode

You should now be able to log into your instance!
Alt Text

Unfortunately, we have an instance that isn't running anything on it.

Let's fix that!

Adding User script

  • Let's create a new file under ./lib/ directory called user_script.sh. Paste this code into that file.
  • This code will deploy Apache, Wordpress, Mysql server on this intance. In this script, the database password will be pl55w0rd.
  • It's very insecure to add passwords to scripts but in this case I'm doing this just for demonstration purposes.
  • In production you should first of all never use such a weak password and secondly, not inside such a script.
  • Rather, you should deploy the Mysql database on AWS RDS and setup credentials for that database using AWS Secrets Manager.

Here's the setup file:

#! /bin/bash
# become root user
sudo su

# update dependencies
yum -y update

# we'll install 'expect' to input keystrokes/y/n/passwords
yum -y install expect 

# Install Apache
yum -y install httpd

# Start Apache
service httpd start

# Install PHP
yum -y install php php-mysql
# php 7 needed for latest wordpress
amazon-linux-extras -y install php7.2 

# Restart Apache
service httpd restart

# Install MySQL
wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm
rpm -ivh mysql-community-release-el7-5.noarch.rpm

yum -y update 
yum -y install mysql-server

# Start MySQL
service mysqld start

# Create a database named blog
mysqladmin -uroot create blog

# Secure database
# non interactive mysql_secure_installation with a little help from expect.

SECURE_MYSQL=$(expect -c "

set timeout 10
spawn mysql_secure_installation

expect \"Enter current password for root (enter for none):\"
send \"\r\"

expect \"Change the root password?\"
send \"y\r\"
expect \"New password:\"
send \"pl55w0rd\r\"
expect \"Re-enter new password:\"
send \"pl55w0rd\r\"
expect \"Remove anonymous users?\"
send \"y\r\"

expect \"Disallow root login remotely?\"
send \"y\r\"

expect \"Remove test database and access to it?\"
send \"y\r\"

expect \"Reload privilege tables now?\"
send \"y\r\"

expect eof
")

echo "$SECURE_MYSQL"

# Change directory to web root
cd /var/www/html

# Download Wordpress
wget http://wordpress.org/latest.tar.gz

# Extract Wordpress
tar -xzvf latest.tar.gz

# Rename wordpress directory to blog
mv wordpress blog

# Change directory to blog
cd /var/www/html/blog/

# Create a WordPress config file 
mv wp-config-sample.php wp-config.php

#set database details with perl find and replace
sed -i "s/database_name_here/blog/g" /var/www/html/blog/wp-config.php
sed -i "s/username_here/root/g" /var/www/html/blog/wp-config.php
sed -i "s/password_here/pl55w0rd/g" /var/www/html/blog/wp-config.php

# create uploads folder and set permissions
mkdir wp-content/uploads
chmod 777 wp-content/uploads

#remove wp file
rm -rf /var/www/html/latest.tar.gz
Enter fullscreen mode Exit fullscreen mode

We will need to add fs module at the top of our ./lib/simple-ec2-stack.ts file since instance.addUserData() needs to access the file system during deployment.

import * as cdk from '@aws-cdk/core'
import * as ec2 from '@aws-cdk/aws-ec2' // import ec2 library 
import * as iam from '@aws-cdk/aws-iam' // import iam library for permissions

// lets include fs module
import * as fs from 'fs'
Enter fullscreen mode Exit fullscreen mode

Then we can the function instance.addUserData() right before our output function:

...

    // add user script to instance
    // this script runs when the instance is started 
    instance.addUserData(
      fs.readFileSync('lib/user_script.sh', 'utf8')
    )

    // cdk lets us output prperties of the resources we create after they are created
    // we want the ip address of this new instance so we can ssh into it later
    new cdk.CfnOutput(this, 'simple-instance-1-output', {
      value: instance.instancePublicIp
    })
...
Enter fullscreen mode Exit fullscreen mode

Update the deployed stack

Let's re-synthesize to check everything is okay:

cdk --synth --profile default
Enter fullscreen mode Exit fullscreen mode
  • You should now see the user script commands in the synth output
  • Let's deploy our new changes.
  • Cloudformation will only update resources that are being updated.
  • In this case, only ec2 instance is being updated.
  • Other things like roles and security groups will remain as they are since there are no changes to them in the updated stack.
cdk --deploy --profile default
Enter fullscreen mode Exit fullscreen mode

Alt Text

Take note: Since we are not using an elastic IP, its highly likely that the public ip address of the instance has changed.

Let's use the outputted IP address and check to see that Wordpress, Mysql and PHP were installed correctly:

  • In your browser, navigate to http:///blog

  • You should then see:

Alt Text

  • and then you can complete the installation of Wordpress!
  • Remember your database credentials root and pl55w0rd as defined in the script

Testing the stack

Well we know our CDK code works and can provision an ec2 instance to run our Wordpress server and database. Good.

But how can we make sure that changes to the CDK code do not do what we don't want it to do?

This is where tests come in.

Test requirements

  • I do not want any instance other t2.micro to be used as my server instance type because I always want to remain under AWS free tier usage for EC2. Let's ensure that.
  • I want to ensure that my instance uses the SSH key with the name simple-instance-1-key

To accomplish this, we change the code inside the file ./test/simple-ec2.test.ts to:

import { expect as expectCDK, haveResourceLike } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import * as SimpleEc2 from '../lib/simple-ec2-stack';

test('Check InstanceType and SSH KeyName', () => {
    const app = new cdk.App();
    const stack = new SimpleEc2.SimpleEc2Stack(app, 'MyTestStack');

    expectCDK(stack).to(
      haveResourceLike('AWS::EC2::Instance', {
        InstanceType: 't2.micro',
        KeyName: 'simple-instance-1-key'
      })
    )
});
Enter fullscreen mode Exit fullscreen mode

As you can see from the test code, the test will check the generated Cloudformation template generated by the CDK.

In our case we want to check that the instance is a t2.micro and that it uses the SSH key simple-instance-1-key. These are two crucial properties to us.

You can read more about testing infrastructure with the CDK here here

Run the test:

npm test
Enter fullscreen mode Exit fullscreen mode

Alt Text

All good!

And now your code should be able to run a test before deploying your infrastructure! Fantastic!

npm test && cdk deploy --profile default
Enter fullscreen mode Exit fullscreen mode

Destroying the stack

If you would like to destroy the infrastructure you just provisioned, it's as simple as:

cdk destroy --profile default
Enter fullscreen mode Exit fullscreen mode

And Cloudformation will remove your entire stack!

Notes:

  • It's not advisable to run mysql on the same instance as your Wordpress server. You can instead use AWS managed Database service RDS to deploy the database that Wordpress will use.
  • Don't put sensitive information like passwords in user scripts since in many cases they are committed to source control or their output is visible in a CI/CD console or instance terminal history

Conclusion

The AWS CDK makes writing IaC, provisioning, deploying, updating and destroying infrastructure very painless. You can write tests to make sure you do not deploy the wrong things.

This was a simple example and may seem quite a lot just to deploy an ec2 instance. However, as we progress through the series, you will realize how its very beneficial to complex infrastructure.

Up next

In part 4, using the CDK, we will make our Wordpress server more more production ready. We will:

  • create AWS RDS Mysql database instead of running the database on the ec2 instance
  • provision this database in an isolated subnet to keep it secure from the public Internet
  • use AWS SSM to access our instance instead of an SSH key and gain all the benefits of IAM permissions/roles
  • deploy an Application Load Balancer
  • Create our EC2 instance with better/more advanced script
  • Place the Wordpress instance in an AutoScaling Group

Hi I'm Emmanuel! I write about Software and DevOps.

If you liked this article and want to see more, add me on LinkedIn or follow me on Twitter

Top comments (4)

Collapse
 
kranshah profile image
Karan Shah

Very useful article. All works fine except .env file variables aren't getting loaded, had to hardcode account and region at last. Is there any step missing?

Collapse
 
emmanuelnk profile image
Emmanuel K

Did you configure your aws profile in ./aws/config?

Collapse
 
kranshah profile image
Karan Shah

Yes, cdk only works when when I'm hardcoding the account/region

require('dotenv').config()

const config = {
env: {
// account: '7339854xxxxx',
// region: 'us-east-1'
account: process.env.AWS_ACCOUNT_NUMBER,
region: process.env.AWS_REGION
}
}

Collapse
 
_j_162737067b41ffdd8966c0 profile image
J

Let’s say after deploy, I create a new EC2 instance in new VPC with in a same stack. How could I destroy the old EC2 in default VPC during deploy ? Thanks