« Back
in AWS teamcity Docker Node.js read.

Deploying a Dockerized Node.js App onto Elastic Beanstalk with TeamCity.

Setting up a TeamCity deployment pipeline to AWS was a bit tricky so I'm putting it together here in case anyone runs into any issues as well or just need a guideline on how to do it. I won't go into what Docker as there are plenty of tutorials out there for it. One of the reasons I chose to put the Node.js app into a docker container though is that Elastic Beanstalk doesn't support version 5.7.0+ at the time and I wanted to use Promises, ES6/ES2015 syntax and other features not available in lower versions. The beauty of packaging it into a Docker container onto AWS is that we have complete control of all the modules and versioning.

Build Agent Requirements

  1. The build agent is running linux, I run one in Ubuntu on an EC2 instance.
  2. AWS CLI Tools. Here are some installation instructions. Though you might want to Puppetize/Cheferize it.
  3. Create an AWS IAM Role for Elastic Beanstalk. You will notice that its default security group is pretty open. That's kind of expected of an agent because it needs write access in so many places that it could potentially deploy code from. Make sure the access keys are in the right locations on the agent. See here for some details on access keys and default regions.
  4. Git

General Flow of Deployment

The following is an overview of all the steps for deployment:
teamcity_build_agent_overview_aws_docker 1. Generate the build number based on the Git Hash. This version number will be our application version number for Beanstalk as well. So we know exactly what version our production environment is running. I should note now even though you may know already that Beanstalk has both Application Version and Environment. An application version is the revision of your code. The code runs in an environment. In this case it will be a Beanstalk Docker environment. When we update our code we specify the new application version which will run on the same environment. So the environment needs to be updated with the new version when we deploy.
2. Package the code into a zip file.
3. Upload the package onto S3. This step is necessary if we want to automate it. If you used the AWS Console, you could just upload the zip file directly and relaunch.
4. Create the Beanstalk application if it doesn't exist. If it does exist, update the new application version.
5. Create the Docker environment in that application if it doesn't exist. If it does exist, update it with the new application version.
6. Poll AWS to see if the deployment was successful and complete the build.

Step 1. Generate the Build Number

Step 1 Generate Git Build Number We take the first 7 characters of the commit as the git hash. This will be part of our application version.

Step 2. Package the Code

Step 2 Package code

Step 3. Upload package onto S3 Bucket

Upload package to S3

Step 4. Create/Update Application Version

Step 4 Create Beanstalk Application Version The ebconfig file defined as a TeamCity parameter is a json file describing our environment. e.g. ebconfig.json:

[
  {
    "Namespace" : "aws:autoscaling:launchconfiguration",
    "OptionName" : "InstanceType",
    "Value" : "t2.small"
  },
  {
    "Namespace" : "aws:autoscaling:launchconfiguration",
    "OptionName" : "EC2KeyName",
    "Value" : "my-key"
  },
  {
    "Namespace" : "aws:autoscaling:launchconfiguration",
    "OptionName" : "SecurityGroups",
    "Value" : "sg-xxxxxxx"
  },
  {
    "Namespace" : "aws:autoscaling:launchconfiguration",
    "OptionName" : "IamInstanceProfile",
    "Value" : "arn:aws:iam::xxxxxxx:instance-profile/my_profile"
  },
  {
    "Namespace" : "aws:elasticbeanstalk:environment",
    "OptionName" : "EnvironmentType",
    "Value" : "LoadBalanced"
  },
  {
    "Namespace" : "aws:ec2:vpc",
    "OptionName" : "Subnets",
    "Value" : "subnet-xxxxx, subnet-xxxxx"
  },
  {
    "Namespace" : "aws:ec2:vpc",
    "OptionName" : "ELBSubnets",
    "Value" : "subnet-xxxxx, subnet-xxxx"
  },
  {
    "Namespace" : "aws:ec2:vpc",
    "OptionName" : "VPCId",
    "Value" : "vpc-xxxxxxxx"
  },
  {
    "Namespace" : "aws:autoscaling:asg",
    "OptionName" : "MinSize",
    "Value" : "1"
  },
  {
    "Namespace" : "aws:autoscaling:asg",
    "OptionName" : "MaxSize",
    "Value" : "10"
  }
]

The above indicates an auto-scaling web application that starts off as one node and scales up to 10 instances. It specifies security groups and which VPC to sit in. In this case it would be a public facing subnet. It will have a load balancer and use t2.small EC2 instances.

Step 5. Create/Update Application Environment

Step 5 update application environment on beanstalk

Step 6. Loop until Environment status is known

Step 6 query Beanstalk environment status

TeamCity Parameters

You'll notice that the above used some TeamCity paramters enclosed in %my parameter%. The important ones are:

bucket:bucketname e.g. elasticbeanstalk-ap-southeast-x-xxxxxxx  
region: ap-southeast-2  
solutionstack: 64bit Amazon Linux 2015.09 v2.0.8 running Docker 1.9.1  
application: the application base name  

Dockerrun.aws.json

Beanstalk accepts a Dockerrun.aws.json file to do things like expose the EC2 port and mount volumes to the container:

{

 "AWSEBDockerrunVersion": "1",

 "Ports": [
   {
     "ContainerPort": "3000"
   }
 ],

 "Volumes": [
    {
      "HostDirectory": "/var/app/current",
      "ContainerDirectory": "/usr/src/app"
    }
  ]

}

You'll see why we mount the source code into the container in a minute. Although the app in the container runs on port 3000, Beanstalk single container instance will only expose one port which maps to the host 80. It's also important to note that when deploying the application version, the latest source code on the Beanstalk EC2 instance will always sit under /var/app/current. I did not arbitrarily choose that path.

If you bake the source code into a Docker image, all you need is the Dockerrun file and point to that image. Then to deploy this application all you would need use this Dockerrun file. No need to upload code to S3.

Dockerfile

I didn't want to use a private Docker image and bake the code into the image. So what we'll do is actually mount the host drive containing the source files into the container, run npm install to get all the dependencies, and then execute the web app. This is a Node.js Express web app.

When deploying a Dockerfile to Beanstalk, it will actually build the image for us.

FROM node:5.7.0

RUN mkdir -p /usr/src/app

WORKDIR /usr/src/app

RUN npm install

EXPOSE 3000

CMD ["/bin/sh","runfile"]

Okay so here I use Node version 5.7.0 which Amazon does not support in their Node.js stack. We make the working directory the same one we mounted the source code in.

The last line CMD["/bin/sh","runfile"] is the default command the container will run once it's built and run.

A nice thing about Beanstalk is that if your app crashes, Beanstalk will automatically restart your container using this command. I tested this by modifying the code to constantly crash then ssh-ed into the Beanstalk EC2 instance and ran Docker ps. The CREATED value was constantly under 2 seconds ago because it kept restarting. I fixed the code and restarted the container and the CREATED time was longer.

The reason why I point it to a runfile (which should be part of your source code) is that I might use new modules and need them installed into the application in node_modules. So the runfile consists of two commands:

#!/bin/bash
npm install  
npm start  

If you log into the console you should see this:
AWS Console EBS Docker App version update from TeamCity

Notice the Running Version is the 7 digit Git Hash.

That should do it.

comments powered by Disqus