Deploying a Node app to Beanstalk using aws-cdk (TypeScript)
Tags: cdk, aws, node, typescript
Published: 2020-04-18 18:31:21

#### In my previous post of this series, we saw how we can deploy a Single Page Application on S3 and Cloudfront with CI/CD via Codebuild using `aws-cdk`.

Before reading this, I would recommend that you check out my previous post in this series where I have explained the advantages of using the `aws-cdk`.

In this post, we shall see how we can deploy a Node application to Elastic Beanstalk and the same Continuous Deployment setup with Codebuild that we had done last time using the CDK.

TLDR; all the code is in this [repo](https://github.com/ryands17/node-beanstalk-cdk) if you want to start hacking right away!

**_Note_**: This post assumes that you have aws-cli installed and configured an AWS profile with an access and secret key via `aws configure`.

We will be creating 3 services for our project.

1. An Elastic Beanstalk (EB) application that will hold our application and its environments (`develop`, `prod` etc.).

2. An EB environment that we will create to deploy our Node app.

3. A Codebuild project that will trigger whenever your code is pushed or a PR is merged.

Let's start with the EB Application. The code will be as follows:

```ts
import * as EB from '@aws-cdk/aws-elasticbeanstalk';

const ebApp = new EB.CfnApplication(this, `${cfg.APP_NAME}-app`, {
  applicationName: cfg.APP_NAME,
});
```

First we import the `aws-elasticbeanstalk` package and tell the cdk to create an EB application with the name specified in our configuration. This name is passed via an `.env` file and we can add any name we want the application to be.

**_Note_**: I have provided an example env file in the repo so you can copy that and change it to the values you prefer.

That was all to creating an EB application. Our next step is setting up an EB environment in which our Node app will be deployed.

These environments are just like the different environments we have during the app development lifecycle. For e.g. `develop` for development, and `production` for our main application that the end user will interact with.

So let's create the EB environment as follows:

```ts
const platform = this.node.tryGetContext('platform');

const options: EB.CfnEnvironment.OptionSettingProperty[] = [
  {
    namespace: 'aws:autoscaling:launchconfiguration',
    optionName: 'IamInstanceProfile',
    value: 'aws-elasticbeanstalk-ec2-role',
  },
];

const ebEnv = new EB.CfnEnvironment(this, `${cfg.APP_NAME}-env`, {
  // default environmentName is `develop` as stated in `config.ts`
  environmentName: cfg.APP_STAGE_NAME,
  applicationName: ebApp.applicationName,
  platformArn: platform,
  optionSettings: options,
});

ebEnv.addDependsOn(ebApp);
```

Let's start with creating the environment using the `CfnEnvironment` class. We pass the context and application name as usual and in the last parameter, we're passing a set of props that are required to create our environment. The props that standout currently are `platformArn`, `optionSettings` and `solutionStackName`. Let's go through these.

- The `platformArn` prop is used to specify which system and what application plaform that we will be using. Beanstalk supports many platforms like Node, Python, Ruby etc. and from this we will be adding a platform property in our `cdk.json` file. This will tell Beanstalk to use the given platform.

Currently as a platform we have the following value: `arn:aws:elasticbeanstalk:us-east-1::platform/Node.js 12 running on 64bit Amazon Linux 2/5.0.2`. It means that we will be running Amazon Linux 2 with Node 10 support that has been recently released.

The way we get this value from the `cdk.json` is in the following manner.

```ts
const platform = this.node.tryGetContext('platform');
```

The method `tryGetContext` returns the value of the property that we pass to it. So `platform` will return the value of the platform key inside the `context` key from `cdk.json`.

- The `optionSettings` prop is used to provide Beanstalk the EC2 role to create an instance. Without this, we won't be able to create an EC2 instance. We create this using the `OptionSettingProperty` object.

- The last prop `solutionStackName` is the stack we will be using. This will create for us a sample Node app with all the defaults set.

**_Note_**: The value in `solutionStackName` is not random, but one that AWS provides by default. This is true for all platforms (Node, Python etc.) and you can choose the one you want to for the specific platform you're building.

The last part is the following line:

```ts
ebEnv.addDependsOn(ebApp);
```

This is added to ensure that the EB environment is only created after the EB application. This is necessary as there being no implicit dependency between the two, we have to specify it explicitly as the environment cannot be created without the application.

Now we are move on to the third and final service, i.e. creating a Codebuild project.

First, we create a GitHub repository source that Codebuild can use.

**_Note_**: You can create a Bitbucket repo in the same manner as well.

```ts
const repo = Codebuild.Source.gitHub({
  owner: cfg.REPO_OWNER,
  repo: cfg.REPO_NAME,
  webhook: true,
  webhookFilters: webhooks,
  reportBuildStatus: true,
});
```

The above code will create our repository that will act as a source to our Codebuid Project. We have passed the owner of the repo and the repository name as well.

You must have noticed that we have passed something called `webhooks` set to `true` and also `webhookFilters`. What are those?

Webhook filters allow you to run the build on any branch based on the conditions and action on the branch.

We have added a webhook in the following manner

```ts
import * as Codebuild from '@aws-cdk/aws-codebuild';

const webhooks: Codebuild.FilterGroup[] = [
  Codebuild.FilterGroup.inEventOf(
    Codebuild.EventAction.PUSH,
    Codebuild.EventAction.PULL_REQUEST_MERGED
  ).andHeadRefIs(cfg.BUILD_BRANCH),
];
```

This webhook states that on PUSH and PULL REQUEST MERGED on the branch specified in our config, initiate the build runner in Codebuild. As an example, we will be using the `master` branch. So any push or any PR merge to the master will trigger the build.

Lastly, we shall combine all this in creating our Codebuild project as shown below.

```ts
const project = new Codebuild.Project(this, `${cfg.APP_NAME}`, {
  buildSpec: Codebuild.BuildSpec.fromSourceFilename('buildspec.yml'),
  projectName: `${cfg.APP_NAME}-build`,
  environment: {
    buildImage: Codebuild.LinuxBuildImage.AMAZON_LINUX_2_3,
    computeType: Codebuild.ComputeType.SMALL,
  },
  source: repo,
  timeout: cdk.Duration.minutes(20),
});
```

Here we are telling Codebuild to create a project for the source that we have added above (via GitHub) and we specify parameters related to the build image.

One last thing left right now. Our Codebuild setup needs access to Beanstalk and its related services to deploy the application and AWS has just the policy for that.

So let's add an AWS Managed policy our codebuild project.

```ts
project.role.addManagedPolicy(
  IAM.ManagedPolicy.fromAwsManagedPolicyName(
    'AWSElasticBeanstalkFullAccess'
  )
);
```

This adds the already created policy `AWSElasticBeanstalkFullAccess` by AWS and allows Codebuild to deploy to Beanstalk on our behalf.

So we're done and the only thing required now for us to test is to create a repository with a simple Node application with something like [express](https://expressjs.com/).

Then replace all the config variables with the one's related to the repository and then run `npm run deploy -- --profile <profileName>` where `profileName` is the one you configured with the `aws-cli`.

I have added a sample `buildspec.yml` below that you can tweak and add in your repository.

```yml
version: 0.2
phases:
  install:
    runtime-versions:
      python: 3.7
  pre_build:
    commands:
      - echo Installing eb-cli...
      - pip3 install awsebcli --upgrade
  build:
    commands:
      - echo Build started on `date`
      - eb deploy $EB_STAGE --staged
    finally:
      - echo Build completed on `date`
```

Here, I have used Codebuild's environment variables to refer to the EB environment that we will be deploying to. You can add those in the Codebuild build project from the console or directly add it in the file above as configuration (I have done that in the repo!).

Thanks for reading and do spread this post to all the cloud enthusiasts out there! Also do let me know which AWS service to cover next :)