Build and Deploy using Jenkins, Packer and Terraform
By: Shashwat Tripathi
Tags: DevOps, Terraform, Continuous Delivery, Continuous Integration, Cloud Computing

Dear users, it is my first story on Medium. I wanted to share my experience for how I managed to completely automate deployments for one my project to a Load Balanced, Auto Scalable infrastructure with Immutable Servers.

What is an Immutable Server?

An immutable server doesn’t accept any change once it is launched and provisioned. Which means you can’t update your code or deploy new files to rollout an update for your application. To rollout an update, new servers group with the updated code will be created while destroying the old ones. This way, it becomes easier to auto scale your application with one true source (an AWS AMI) to launch new instances to meet your workload.

Requirements

To perform this exercise or setup, you need a Jenkins server, GitHub account and AWS account. Most of the resources will fit into AWS free tier, so you should not be charged if you destroy everything after we complete the exercise.

So, how we do it?

We do it with the help of modern DevOps tools. I will show you the step by step process for a complete setup, right from the sample ExpressJs (Nodejs web app) setup to the deployment.

Your Web Server Code (ExpressJs)

We will use default template generated by express generator for our Nodejs web application.

Install NodeJs on your system (please note version which you are installing, as we will use the same version in further configuration. As of now we’ve installed 8.9.4 LTS) Generate template using express node-app . This will create a project skeleton. Check express generator docs for help. Push your code to a GitHub repository. You can use other central repository if you wish so. (Note: make sure you’ve ignored node_modules through your .gitignore file.

Docker Container

We will create a custom image with Packer, Terraform and latest Git installed already setup. Please find Dockerfile below.

You can save yourself from creating your custom image as I’ve already built it and published it for public use on Docker Hub. I will also use my public image from Docker Hub in this setup.

Generate AWS Key and Secret Key

Generate AWS Key and Secret Key for your AWS account for your root account or an IAM user with permission to manage resources like EC2, Elastic Load Balancer (ELB), Auto Scaling Group (ASG), Security Group (SG) and Launch Configuration (LC). You will need this key and secret to manage resource using Packer and Terraform.

Packer Configurations

We are using Packer to create native AWS AMI which will be used in the Launch Configuration for our Auto Scaling Group which is attached to the Load Balancer.

We will configure our machine by installing applications like NodeJs, copy our project code and configuring auto startup of web server. We will use Ansbile Local provisioner to configure everything so our application will start when the machine boots up.

Packer is used to bake everything into the native AWS AMI. Below is the complete packer configuration.

We are using three provisioners here,

Shell provisioner to install Ansible and create project directory home/ubuntu/node-app . File provisioner to copy project code. Ansible provisioner to configure our machine.

Our Ansible playbook file looks like below:

We’ve used AWS amazon-ebs builder to create a builder. You should be able to understand configuration by reading out. Or you can put a comment if you do not understand something.

Create a new folder in your project folder with name packer and store these files with name packer.json and ansible-playbook.yml and push to the repository.

Terraform Configurations

When you apply these configurations, Terraform will use latest AMI (notice most_recent = true ) and will modify AWS resources to use new AMI. Executing terraform apply will modify and recreate some of the resources in a way so create happens before destroy (notice create_before_destroy ). You can also extract AMI id from Packer

Some important points to note here are:

Both LC and ASG have creaet_before_destroy set to true

set to The LC omits the name attribute to allow Terraform to auto-generate a random one, which prevent collisions

attribute to allow Terraform to auto-generate a random one, which prevent collisions The ASG interpolates the launch configuration name into its name, so LC changes always force replacement of the ASG (and not just an ASG update).

The ASG sets min_elb_capicity which means Terraform will wait for instances in the new ASG to show up as InService in the ELB before considering the ASG successfully created.

The behavior when AMI changes is:

New LC is created with the fresh AMI New ASG is created with the fresh LC Terraform waits for the new ASG’s instances to spin up and attach to the ELB Once all new instances are InService, Terraform begins destroy of old ASG Once old ASG is destroyed, Terraform destroys old LC

Note: Terraform uses a file terraform.tfstate to store all the mappings and states of AWS resources it manages. We need to get this file before we run terraform apply again. Using Jenkins, we will be updating this file to our repository (a separate GitHub repository) to preserve the states created by Terraform.

Jenkins Configuration

Aah ha! finally we go for automating everything using Jenkins. Create a new pipeline for your GitHub project in Jenkins which you created for NodeJs express. Try doing it using new Blue Ocean interface, it is much simpler that way.

Our `Jenkinsfile` looks like below:

We’ve divided whole process into 3 simple stages

Build

Create Packer AMI ( packer part)

part) AWS Deployment ( terraform part)

We are using our custom docker image goforgold/build-container from Docker Hub.

Also note that, we are pushing back our terraform.tfstate each time after successful terraform apply run. It is important because Terraform stores mappings it has created in this file and need it while applying changes.

Result

If you’ve connected all the wires correctly. You should be able to access your web server using your ELB DNS on 3000 port.