Archive · This post is from a previous era of this site.
Pack:er and Terra:deploy a JRuby Application [Part 1]
TL;DR — This is the first part of two of this blog post series. In this part, the goal is to bundle a simple JRuby Application into a war file using Warbler and then build a base image in AWS (AMI) using Packer. For the second part, we will create the necessary infrastructure to spin up an EC2 instance based on our base image (AMI) using Terraform.
All the code samples are in this GitHub repository.
Disclaimer: This blog does not represent the thoughts, intentions, plans or strategies of my employer. It is solely my opinion.
Context
Nowadays we all hear the buzz word microservices. A lot of companies started to break down their monolithic applications into several microservices. We all hear that microservices are the best thing in the world and only good things come with them. But let me tell you, they’re not.
When talking about microservices, we want them to be resilient, scalable, immutable and fully automated processes for building and deploying — and that’s not easy to accomplish!
In this blog post series, I am going to demonstrate how Packer and Terraform can help in the build and deployment processes.
Motivation
Since I joined the platform team at Talkdesk I have been working with several technologies, including Elixir, Kotlin, Packer, Terraform but mainly with JRuby.
I was quite surprised with Packer and Terraform and since DevOps is a topic I’m very interested in I wanted to explore them more in depth on my free time. In these blog posts, I will expose my learnings and experiences.
Let’s get started!
The Application
To better demonstrate how it works it is better to have a real application working. For that reason, I decided to create a simple JRuby Sinatra application that has a root endpoint (GET) which returns a 200 with a “Hello World” in the body.
# app/app.rb
require 'sinatra'
get '/' do
"#{['Hello', 'Hi', 'Hey', 'Yo'][rand(4)]} World!"
end
Building the Base Image (AMI)
Before we start building the base image, we need to understand the requirements to run the application in a production/production-like environment.
Since it’s a JRuby application, we will need to have Java JDK installed and depending on the strategy adopted to deploy it we may also need the JRuby binaries and a Java Web Server.
There are a few strategies for deployment of the application, but for this blog post, I decided to bundle the application using Warbler into a war file. This way we avoid installing JRuby binaries and also a Java Webserver, which makes the building process simpler.
Regarding building base images for production you must have in consideration security concerns: you should do your own OS Hardening, ensure that root user doesn’t run your application, logging stuff, permissions, etc.
Disclaimer: The code samples provided here are not intended to be used in production and are not production ready. In case you use them it’s your responsibility.
Creating the Packer configuration source
If you don’t know what Packer is, I advise you to start by reading the starting guide. But in short, Packer is a tool to create images for multiple providers in parallel from a single source configuration. It can also provision those machines using shell scripts, Chef, Puppet, Ansible and other similar tools.
{
"variables": {
"aws_access_key": "{{env `AWS_ID`}}",
"aws_secret_key": "{{env `AWS_SECRET`}}",
"aws_region": "eu-west-1",
"aws_base_ami": "ami-402f1a33",
"aws_instance_type": "t2.micro"
},
"builders": [
{
"type": "amazon-ebs",
"access_key": "{{user `aws_access_key`}}",
"secret_key": "{{user `aws_secret_key`}}",
"region": "{{user `aws_region`}}",
"source_ami": "{{user `aws_base_ami`}}",
"instance_type": "{{user `aws_instance_type`}}",
"ssh_username": "admin",
"ami_name": "base-image-openjdk-8-{{timestamp}}"
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"echo 'Waiting 180 seconds for cloud-init'",
"timeout 180 /bin/bash -c 'until stat /var/lib/cloud/instance/boot-finished &>/dev/null; do echo waiting...; sleep 10; done'"
]
},
{
"type": "shell",
"inline": [
"echo 'deb http://ftp.debian.org/debian jessie-backports main' | sudo tee /etc/apt/sources.list.d/backports.list > /dev/null",
"sudo apt-get update",
"sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -t jessie-backports -q openjdk-8-jre-headless ca-certificates-java"
]
}
]
}
The Variables block is where default values are defined — you can also define environment variables instead of values. The Builders block defines in which platform you want to build the image (AWS, Azure, DigitalOcean, etc.). The Provisioners block defines the provisioning steps — here we wait for cloud-init to finish and then install Java OpenJDK 8.
Validate and build the base image
cd /my_project/build
echo "validating the configuration file"
AWS_ID=my_id AWS_SECRET=my_secret packer validate base-image.json
> Template validated successfully.
echo "build the image"
AWS_ID=my_id AWS_SECRET=my_secret packer build base-image.json
The building process will spin up an EC2 instance, provision it with Java OpenJDK 8, stop the instance, create the AMI from that state, and then terminate the instance.
Bundle the Application into a war file
Warbler is a gem that enables you to turn your JRuby application into a Java jar or war file. References:
- Warbler Documentation
- down the JRuby rabbit hole
- The Frankenjar: The Easiest Ruby App Deployment Ever
For this example, we will use Warbler to create an executable war file. This means that Warbler will embed Jetty Web Server and it will run on its own.
cd /my_project
gem install warbler
bundle install
warble war
At this point we have our war file ready. The second part of this series will cover how to use Terraform to provision the infrastructure and deploy the application.