Codementor Events

Awesome Rails development with Docker, Kubernetes

Published Mar 06, 2020Last updated Sep 01, 2020
Awesome Rails development with Docker, Kubernetes

Introduction

I have spent quite a bit of time lately trying to polish my setup and workflow for Rails web development; a lot has changed since I started working with containers and Kubernetes, so I had to adapt. I am very happy with the end result, and I’d like to share in this post what I am doing now as I am sure this can save time to others and answer some questions others may have See More Information On Ruby On Rails Online Training

Once upon a time, doing Rails development for me meant installing all my app’s dependencies on my Mac including Ruby, databases, things like Redis and more. This mostly worked, but it also meant developing on a platform which at times can be quite different from the platform I deploy to, and I’ve happened to run into issues a few times because of this. There’s a lot to like about containers, but I love them particularly because they enable me to develop in an environment which is almost identical to production, and also because of the simplicity with which I can set up throw away environments. Containers make it very easy to pack all the libraries and components an app needs to run, making sure the app always runs in exactly the same consistent way regardless of where it is running. And containers can be used to run external dependencies as well.rt writing here...

Dockerfile

The first step is to “containerize” the app. How you write your Dockerfile is quite important, because an “unoptimised” Dockerfile can lead to very big images that have to be rebuilt from scratch often, slowing things down. Instead, since Docker builds images as “layers” - where each layer is created with a particular instruction in the Dockerfile - a good Dockerfile can leverage caching of layers as well as reduce the final image size considerably. For Rails apps there’s a Ruby image, which comes in various flavours. The most convenient is the one based on the Alpine Linux distro, since it’s very very small compared to the default image which is based on Debian. There’s an important aspect of Alpine-based images to take into account: Alpine uses musl instead of glibc so some software may have problems running in Alpine. However, with most apps this isn’t an issue, so by using Alpine you can benefit from smaller images which are quicker to build, push and deploy.

This is my current Dockerfile:

ARG RUBY_VERSION=2.6.1

FROM ruby:$RUBY_VERSION-alpine as development

RUN apk add --no-cache \
  git build-base yarn nodejs mariadb-dev imagemagick \
  chromium-chromedriver chromium tzdata \
  && rm -rf /var/cache/apk/* 

ENV RAILS_ENV=development
ENV RACK_ENV=development
ENV RAILS_LOG_TO_STDOUT=true
ENV RAILS_ROOT=/app
ENV LANG=C.UTF-8
ENV GEM_HOME=/bundle
ENV BUNDLE_PATH=$GEM_HOME
ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH
ENV BUNDLE_BIN=$BUNDLE_PATH/bin
ENV PATH=/app/bin:$BUNDLE_BIN:$PATH

WORKDIR /app

Let’s go through this file in detail. First we specify that we want to base our image on the Alpine version of the Ruby image:

ARG RUBY_VERSION=2.6.1

FROM ruby:$RUBY_VERSION-alpine as development

Also note that we are calling this stage ‘development’. In fact we are going to build a “multi-stage” image with two stages, one for an image that contains everything needed during development, and a final version, smaller than the original one, which will be used in production.

The next RUN instruction installs some packages required for things to work.

RUN apk add --no-cache \
  git build-base yarn nodejs mariadb-dev imagemagick \
  chromium-chromedriver chromium tzdata \
  && rm -rf /var/cache/apk/* 

So we install:

• git, so that Bundler can fetch and install Ruby gems
• build-base, required to compile some stuff also for Ruby gems
• nodejs to compile assets and “packs” with Webpacker
• mariadb-dev, so that we can install and use the MySQL Ruby gem; of course this may be changed to the equivalent for PostgreSQL or other database in use
• imagemagick - optional - to manage images with ActiveStorage
• chromium-chromedriver and chromium to run system tests with Capybara
• tzdata for time zone stuff, also required by Rails or some gem (can’t remember).
Note that after installing these packages we clear the apk cache, which will help reduce the final image size.

Next, we default some environment variables for development and bundler:

ENV RAILS_ENV=development
ENV RACK_ENV=development
ENV RAILS_LOG_TO_STDOUT=true
ENV RAILS_ROOT=/app
ENV LANG=C.UTF-8
ENV GEM_HOME=/bundle
ENV BUNDLE_PATH=$GEM_HOME
ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH
ENV BUNDLE_BIN=$BUNDLE_PATH/bin
ENV PATH=/app/bin:$BUNDLE_BIN:$PATH

The next thing we do is copy the Gemfile and install the gems with Bundler:

WORKDIR /app

COPY Gemfile Gemfile.lock ./

RUN gem install bundler \
  && bundle install -j "$(getconf _NPROCESSORS_ONLN)"  \
  && rm -rf $BUNDLE_PATH/cache/*.gem \
  && find $BUNDLE_PATH/gems/ -name "*.c" -delete \
  && find $BUNDLE_PATH/gems/ -name "*.o" -delete  

If you want to Gain In-depth Knowledge on Kubernetes, please go through this link Kubernetes Online Training

Setting up a development Kubernetes cluster with K3s

There are various ways to set up a Kubernetes cluster that we can use for development, but my favourite is using Rancher’s K3s Kubernetes distribution because it’s certified (just a few differences from the upstream Kubernetes which need to be taken into account) and incredibly lightweight! It’s a complete Kubernetes distro in a single small binary. Thanks to K3s’ low CPU/RAM usage, for development I can use a single node cluster with just 2 cores and 4 GB of ram, which is really cheap (I use Hetzner Cloud and this costs me 6 euros per month).

Installing K3s is super easy using the official script:

curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--no-deploy=traefik" sh -

Note that in the command above I make sure that Traefik ingress controller is not installed automatically (because otherwise it’s installed by default) because I prefer Nginx as ingress controller. You may remove that parameter if you are happy with Traefik. Once K3s is up and running, you need to copy the kubeconfig file to your dev machine so that you can manage the cluster with kubectl:

ssh <server name or IP> "sudo cat /etc/rancher/k3s/k3s.yaml" > ~/.kube/config-<cluster name>

sed -i -e "s/localhost/<server IP>/g" ~/.kube/config-<cluster name>
sed -i -e "s/default/<cluster name>/g" ~/.kube/config-<cluster name>

Deployment to Kubernetes with Helm and Helmfile

To deploy our app to the cluster of course we’ll use Helm, but together with another tool called Helmfile which makes it possible to use the same chart with multiple environments, more easily. To install Helmfile on Mac, for example, run:

brew install helmfile

We also need to install the secrets plugin for Helm so that we can manage secrets for each environment:

helm plugin install https://github.com/futuresimple/helm-secrets

We can now create the Helm chart. I like to keep the chart in the same repository as the app’s code, so I create the necessary files in the helm subdirectory with this structure:

helmfile.yaml
helm
├── chart
│   ├── Chart.yaml
│   ├── templates
│   │   ├── deployment-web.yaml
│   │   ├── deployment-worker.yaml
│   │   ├── ingress.yaml
│   │   ├── secret.yml
│   │   └── service-web.yaml
│   └── values.yaml
├── helmfiles
│   └── 00-my-app.yaml
└── values
    └── my-app
        └── dev
            ├── secrets.yaml
            └── values.yamlhelmfile.yaml
helm
├── chart
│   ├── Chart.yaml
│   ├── templates
│   │   ├── deployment-web.yaml
│   │   ├── deployment-worker.yaml
│   │   ├── ingress.yaml
│   │   ├── secret.yml
│   │   └── service-web.yaml
│   └── values.yaml
├── helmfiles
│   └── 00-my-app.yaml
└── values
    └── my-app
        └── dev
            ├── secrets.yaml
            └── values.yaml

Conclusion

I hope I remembered to include everything relevant for this tutorial. Like I said I am very happy with this setup and am happy to share. It’s a little bit of work upfront, but it’s something that needs to be done only once anyway. Then, I just start the web and worker containers locally with Telepresence and get on with development as usual! Thanks to this setup and workflow, I don’t miss anything from the old way of doing Rails development, even though there’s of course some additional complexity in comparison. Please feel free to let me know in the comments if you run into issues while replicating this setup and I will be happy to help if I can.

Discover and read more posts from Sravan Cynixit
get started