Part 3 - Simple EC2 instance - Awesome AWS CDK
Tags: aws, awscdk, ec2, devops
Published: 2021-03-18 11:06:56

- [Introduction](#introduction)
- [Bootstrapping a new CDK Application](#bootstrap)
- [Structure and Setup of a CDK application](#structure-and-setup)
   - [Structure](#structure)
   - [Setup](#setup)
- [Writing the infrastructure code](#iac)
- [Creating a new Key Pair in the AWS Console](#key-pair)
- [Deploy](#deploy)
- [Accessing the instance](#access-instance)
- [Updating the deployed stack
](#update-script)
- [Testing the stack](#test-stack)
    - [Test requirements](#test-requirements)
- [Destroying the stack](#destroy-script)
- [Notes](#notes)
- [Conclusion](#conclusion)
- [Up next](#up-next)


## <a name="introduction"/> 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!

## <a name="bootstrap"/> Bootstrapping a new CDK Application
Using your terminal, create a new directory `simple-ec2` and cd into it:
```
mkdir simple-ec2 && cd simple-ec2
```
We previously setup the AWS CDK cli globally. If you haven't done that see [Part 2](https://dev.to/emmanuelnk/awesome-aws-cdk-part-2-setting-up-aws-cdk-3ggj) in this series. 

Bootstrap a new cdk project template that uses Typescript:
```
cdk init --language=typescript
```
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.

## <a name="structure-and-setup"/>Structure and Setup of a CDK application
### <a name="structure"/> Structure
``` bash
# 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
```

- `./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

### <a name="setup"/> 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:
```ts
// ./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'
});
```

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

```ts
// ./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
  }
}
```

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
```

## <a name="iac"/> 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
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rwvn83wjbix9cgv72j5r.gif)


Here goes our first iteration:
```ts
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
    })
  }
}
```

## <a name="key-pair"/> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vo9h9zejdykpkapld38a.gif)

- Create a new directory under `.aws/` called `pems`
```bash
mkdir ~./aws/pems/
```
- move the newly downloaded file to this directory and give it the necessary permissions
```bash
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
```

- Now that we have our key file properly setup, lets deploy our instance!

## <a name="deploy"/> Deploy

Remember we set up our aws profiles and crednetials in `~/.aws/config` and `~/.aws/credentials` back in [part 2](https://dev.to/emmanuelnk/awesome-aws-cdk-part-2-setting-up-aws-cdk-3ggj) 

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
```
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:

```bash
❯ 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"
   ...
   ...
   ...
```
If the entire stack prints without error then you're okay to go.

Now we can deploy:
```bash
cdk deploy --profile default
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7c6qw50t0bidqoyhxp25.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fso15euru0fnvaksym5j.png)

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.

## <a name="access-instance"/> 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`

```bash
ssh -i ~/.aws/pems/simple-instance-1-key.pem ec2-user@34.220.79.175
```

You should now be able to log into your instance!
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dbau99kvx3em1ks61oxe.png)

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

Let's fix that!

## <a name="user-script"/> 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:

```bash
#! /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
```

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.

```ts
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'
```

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

```ts
...

    // 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
    })
...
```

## <a name="update-stack"/> Update the deployed stack
Let's re-synthesize to check everything is okay:
```bash
cdk --synth --profile default
```

- 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.

```bash
cdk --deploy --profile default
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ion34zyeoyj2zuv2xwv8.png)

*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://<PUBLIC_IP_ADDRESS>/blog

- You should then see:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/if3mfuajrtxfqb06x185.png)

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

## <a name="test-stack"/> 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.

### <a name="test-requirements"/> 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:
```ts
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'
      })
    )
});
```

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](https://aws.amazon.com/blogs/developer/testing-infrastructure-with-the-aws-cloud-development-kit-cdk/)

Run the test:
```
npm test
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/byipv0hu8l8f3owkdxkh.png)

All good!

And now your code should be able to run a test before deploying your infrastructure! Fantastic!
```
npm test && cdk deploy --profile default
```


## <a name="destroy-stack"/> Destroying the stack
If you would like to destroy the infrastructure you just provisioned, it's as simple as:
```
cdk destroy --profile default
```

And Cloudformation will remove your entire stack!

## <a name="notes"/> 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](https://aws.amazon.com/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

## <a name="conclusion"/> 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.

## <a name="up-next"/>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](https://emmanuelnk.com)! I write about Software and DevOps.

If you liked this article and want to see more, add me on [LinkedIn](https://www.linkedin.com/in/emmanuel-nsubuga-kyeyune/) or follow me on [Twitter](https://twitter.com/emmanuel_n_k)