Use parametrized containers to deploy Rails microservices

Rahul Mahale

By Rahul Mahale

on September 26, 2018

When using micro services with containers, one has to consider modularity and reusability while designing a system.

While using Kubernetes as a distributed system for container deployments, modularity and reusability can be achieved using parameterizing containers to deploy micro services.

Parameterized containers

Assuming container as a function in a program, how many parameters does it have? Each parameter represents an input that can customize a generic container to a specific situation.

Let's assume we have a Rails application isolated in services like puma, sidekiq/delayed-job and websocket. Each service runs as a separate deployment on a separate container for the same application. When deploying the change we should be building the same image for all the three containers but they should be different function/processes. In our case, we will be running 3 pods with the same image. This can be achieved by building a generic image for containers. The Generic container must be accepting parameters to run different services.

We need to expose parameters and consume them inside the container. There are two ways to pass parameters to our container.

  1. Using environment variables.
  2. Using command line arguments.

In this article, we will use environment variables to run parameterized containers like puma, sidekiq/delayed-job and websocket for Rails applications on kubernetes.

We will deploy wheel on kubernetes using parametrized container approach.


Building a generic container image.

Dockerfile (Link is not available) in wheel uses bash script as a command to start a container. The script is self-explanatory and, as we can see, it consists of two functions web and background. Function web starts the puma service and background starts the delayed_job service.

We create two different deployments on kubernetes for web and background services. Deployment templates are identical for both web and background. The value of environment variable POD_TYPE to init-script runs the particular service in a pod.

Once we have docker image built, let's deploy the application.

Creating kubernetes deployment manifests for wheel application

Wheel uses PostgreSQL database and we need postgres service to run the application. We will use the postgres image from docker hub and will deploy it as deployment.

Note: For production deployments, database should be deployed as a statefulset or use managed database services.

K8s manifest for deploying PostgreSQL.

2apiVersion: extensions/v1beta1
3kind: Deployment
5  labels:
6    app: db
7  name: db
9  replicas: 1
10  template:
11    metadata:
12      labels:
13        app: db
14    spec:
15      containers:
16        - image: postgres:9.4
17          name: db
18          env:
19            - name: POSTGRES_USER
20              value: postgres
21            - name: POSTGRES_PASSWORD
22              value: welcome
25apiVersion: v1
26kind: Service
28  labels:
29    app: db
30  name: db
32  ports:
33    - name: headless
34      port: 5432
35      targetPort: 5432
36  selector:
37    app: db

Create Postgres DB and the service.

1$ kubectl create -f db-deployment.yml -f db-service.yml
2deployment db created
3service db created

Now that DB is available, we need to access it from the application using database.yml.

We will create configmap to store database credentials and mount it on the config/database.yml in our application deployments.

2apiVersion: v1
3kind: ConfigMap
5  name: database-config
7  database.yml: |
8    development:
9      adapter: postgresql
10      database: wheel_development
11      host: db
12      username: postgres
13      password: welcome
14      pool: 5
16    test:
17      adapter: postgresql
18      database: wheel_test
19      host: db
20      username: postgres
21      password: welcome
22      pool: 5
24    staging:
25      adapter: postgresql
26      database: postgres
27      host: db
28      username: postgres
29      password: welcome
30      pool: 5

Create configmap for database.yml.

1$ kubectl create -f database-configmap.yml
2configmap database-config created

We have the database ready for our application, now let's proceed to deploy our Rails services.

Deploying Rails micro-services using the same docker image

In this blog, we will limit our services to web and background with kubernetes deployment.

Let's create a deployment and service for our web application.

2apiVersion: extensions/v1beta1
3kind: Deployment
5  name: wheel-web
6  labels:
7    app: wheel-web
9  replicas: 1
10  template:
11    metadata:
12      labels:
13        app: wheel-web
14    spec:
15      containers:
16      - image: bigbinary/wheel:generic
17        name: web
18        imagePullPolicy: Always
19        env:
20        - name: DEPLOY_TIME
21          value: $date
22          value: staging
23        - name: POD_TYPE
24          value: WEB
25        ports:
26        - containerPort: 80
27        volumeMounts:
28          - name: database-config
29            mountPath: /wheel/config/database.yml
30            subPath: database.yml
31      volumes:
32        - name: database-config
33          configMap:
34            name: database-config
38apiVersion: v1
39kind: Service
41  labels:
42    app: wheel-web
43  name: web
45  ports:
46  - name: puma
47    port: 80
48    targetPort: 80
49  selector:
50    app: wheel-web
51  type: LoadBalancer

Note that we used POD_TYPE as WEB, which will start the puma process from the container startup script.

Let's create a web/puma deployment and service.

1kubectl create -f web-deployment.yml -f web-service.yml
2deployment wheel-web created
3service web created
2apiVersion: extensions/v1beta1
3kind: Deployment
5  name: wheel-background
6  labels:
7    app: wheel-background
9  replicas: 1
10  template:
11    metadata:
12      labels:
13        app: wheel-background
14    spec:
15      containers:
16        - image: bigbinary/wheel:generic
17          name: background
18          imagePullPolicy: Always
19          env:
20            - name: DEPLOY_TIME
21              value: $date
22            - name: POD_TYPE
23              value: background
24          ports:
25            - containerPort: 80
26          volumeMounts:
27            - name: database-config
28              mountPath: /wheel/config/database.yml
29              subPath: database.yml
30      volumes:
31        - name: database-config
32          configMap:
33            name: database-config
36apiVersion: v1
37kind: Service
39  labels:
40    app: wheel-background
41  name: background
43  ports:
44    - name: background
45      port: 80
46      targetPort: 80
47  selector:
48    app: wheel-background

For background/delayed-job we set POD_TYPE as background. It will start delayed-job process.

Let's create background deployment and the service.

1kubectl create -f background-deployment.yml -f background-service.yml
2deployment wheel-background created
3service background created

Get application endpoint.

1$ kubectl get svc web -o wide | awk '{print $4}'

We can access the application using the endpoint.

Now let's see pods.

1$ kubectl get pods
2NAME                                READY     STATUS    RESTARTS   AGE
3db-5f7d5c96f7-x9fll                 1/1       Running   0          1h
4wheel-background-6c7cbb4c75-sd9sd   1/1       Running   0          30m
5wheel-web-f5cbf47bd-7hzp8           1/1       Running   0          10m

We see that db pod is running postgres, wheel-web pod is running puma and wheel-background pod is running delayed job.

If we check logs, everything coming to puma is handled by web pod. All the background jobs are handled by background pod.

Similarly, if we are using websocket, separate API pods, traffic will be routed to respective services.

This is how we can deploy Rails micro services using parametrized containers and a generic image.

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, ReactJS, React Native, remote work,open source, engineering & design.