DevOps journey for a Rails programmer

How I went about making my first ever production Rails deployment

After having written Ruby and Rails code for over 2 years, one day I finally got down to making a production-grade deployment from scratch for my Rails web service. The experiment took me through a more arduous journey than I had ever imagined. Finally, after few weeks of trial & error, mixing and matching, and the usual shenanigans experienced while learning a new concept/technology I had come to this conclusion:

Doing devops is incredibly hard for an application developer.

I only had a superficial understanding of software packaging, networking, scaling, etc. After this experiment, I feel a little more confident but I would still leave core devops to a devops “guy”. Turns out I am not alone feeling this way in the software community and this incredible community has built tooling around simplifying this very problem. I am going to describe my experience with few of these tools and give my conclusions at the end.

Capistrano

I started by googling “deploy rails production” and found Capistrano [1]. At first glance, I found the learning curve to be little steep since the DSL seemed heavy and awkward to write in ruby. Although I did manage to understand the various parameters of the deploy config file, I found it uneasy to supply few parameters like the IP address of my running instance and the ssh key location in the deployment code. I felt these should be parameters to my `cap production deploy` command.

role :web, [ “[email protected]” ]

set :ssh_options, {
  keys: %w(~/rails-prod.pem),
  forward_agent: false,
  auth_methods: %w(publickey)
}

I also wondered that if the need of specifying IP address would complicate issues with restarts (on dynamic IP), auto-scale setup, etc coupled with the fact that my configuration code was in the same repo as my app code. My inner conscious: This doesn’t look like a hardcore production setup. I had to toil for a few hours with some excruciating passenger and nginx bug which got solved by installing a different version of nginx. I managed to get the server running but I wanted to find something better.

Chef

Apparently, a lot of the previous complications could have been solved by being more strict with the philosophical razor: “separate code from config” ( as described in 12-factor methodology [2]). Ideally, I should be able to provision and deploy from a config without even having to log into my remote server. Upon some reading, I decided to check out Chef and googled : “deploy rails production chef” [3]. Chef was very easy to understand: there are cookbooks which have recipes, files, etc and you can choose to run a list of recipes on nodes. Recipes are nothing but Capistrano-like config file to do some action on your node. For example, a simple dependency-setup recipe could like this:

execute “apt-get update” do
  command “apt-get update”
end
%w(git ruby-dev build-essential libsqlite3-dev libssl-dev).each do |pkg|
  package pkg
end

Apparently, not many people like to use Chef to do deployments for making updates [3][4]. Kind of a deal breaker. Chef solves my problem of provisioning an environment but doesn’t handle deployment so well. I still had to use Capistrano with few of its hard-coded parameters to make an update. I also wasn’t sure if Chef would also help me in monitoring/maintaining my application in production. Turns out I was asking more from Chef than it had to offer.

Docker

I had heard a lot about Docker. I decided to see what it is. I am glad I did. Docker containers come with the app packaged with all its dependencies as a single binary. One might think, this is not very different compared to Chef provisioning but it is quite different: you are packaging the dependencies with the app on your localhost and only distributing the binary image. We can then specify an environment for each container and use env variables as config parameters (another 12-factor methodology) for my app. This was separating code from config at its best. I had to write a Docker config file called Dockerfile which was quite simple (almost like writing bash commands). This was definitely the quickest method to run my app till now. All I needed was to manage provisioning and deployment of these containers. With a faint relief, I moved to exploring docker management tools.

Dockerfile:

FROM ruby:2.3.3-slim

ENV APP_HOME /myapp/

ADD ./Gemfile* $APP_HOME

RUN apt-get update && apt-get install -qq -y — no-install-recommends build-essential nodejs libpq-dev

RUN gem install bundler

RUN cd $APP_HOME ; bundle install — without development test

ADD ./ $APP_HOME

RUN chown -R nobody:nogroup $APP_HOME
USER nobody

ENV RAILS_ENV production

WORKDIR $APP_HOME
CMD [“puma”, “-c”, “config/puma.rb”]

Kubernetes

Everything converges to Kubernetes: an open-source container (Docker) orchestration tool developed by Google. Kubernetes helps in configuring, updating as well as monitoring docker containers over a cluster. Kubernetes presents a very clean interface of describing a deployed application. The abstractions provided with this interface allows your entire cluster to be seen as a simple logical units ( think single-node app). We describe the deployment config in a YAML file and run a create command to bootstrap an app. Deploying updates or scaling is as simple as editing the deployment config with a new Docker image or number of replicas and applying it. Everything is managed automatically with zero-downtime. This is a declarative style of configuration management where the tool hides the process of bringing the targets to the desired state and just brings it! So we avoid the complex DSL and the imperative work-flow in a Chef-like management utility.

deployment.yaml:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: frontend
spec:
 replicas: 3
 template:
   metadata:
    labels:
      app: todo
   spec:
     containers:
       — name: nginx
       image: “nginx:1.9.14”
         volumeMounts:
           — name: “nginx-frontend-conf”
             mountPath: “/etc/nginx/conf.d”
     volumes:
       — name: “nginx-frontend-conf”
         configMap:
           name: “nginx-frontend-conf”
           items:
             — key: “frontend.conf”
               path: “frontend.conf”

Conclusion

Kubernetes achieved what I intended with minimal intellectual overhead. I need not be an expert in devops to bootstrap, update and maintain my apps. I may not understand how Kubernetes does so many things behind the scenes (for which there is extensive documentation) but I no longer need to worry about that. It was intended that way, let developers do development and Kubernetes do operations. I would recommend to any developer/solution architect to explore Kubernetes if they are facing a similar devops problem.

You can get a quickstart template for deploying your Rails app using Docker/Kubernetes here:

hasura/quickstart-docker-git
quickstart-docker-git - Boostrap projects with a Dockerfile and a folder structure that works.github.com

Godspeed.

[1] https://gorails.com/deploy/ubuntu/16.04
[2] https://12factor.net
[3] https://launchschool.com/blog/chef-basics-for-rails-developers
[4] http://codefol.io/posts/why-do-we-need-both-capistrano-and-chef


Hasura is an open-source engine that gives you realtime GraphQL APIs on new or existing Postgres databases, with built-in support for stitching custom GraphQL APIs and triggering webhooks on database changes.