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.
- Using environment variables.
- 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.
Pre-requisite
-
Understanding of Dockerfile and image building.
-
Access to working kubernetes cluster.
-
Understanding of Kubernetes terms like pods, deployments, services, configmap, and annotations.
Building a generic container image.
Dockerfile (Link is not available) in wheel uses bash script setup_while_container_init.sh 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.
1--- 2apiVersion: extensions/v1beta1 3kind: Deployment 4metadata: 5 labels: 6 app: db 7 name: db 8spec: 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 23 24--- 25apiVersion: v1 26kind: Service 27metadata: 28 labels: 29 app: db 30 name: db 31spec: 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.
1--- 2apiVersion: v1 3kind: ConfigMap 4metadata: 5 name: database-config 6data: 7 database.yml: | 8 development: 9 adapter: postgresql 10 database: wheel_development 11 host: db 12 username: postgres 13 password: welcome 14 pool: 5 15 16 test: 17 adapter: postgresql 18 database: wheel_test 19 host: db 20 username: postgres 21 password: welcome 22 pool: 5 23 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.
1--- 2apiVersion: extensions/v1beta1 3kind: Deployment 4metadata: 5 name: wheel-web 6 labels: 7 app: wheel-web 8spec: 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 35 36--- 37 38apiVersion: v1 39kind: Service 40metadata: 41 labels: 42 app: wheel-web 43 name: web 44spec: 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
1--- 2apiVersion: extensions/v1beta1 3kind: Deployment 4metadata: 5 name: wheel-background 6 labels: 7 app: wheel-background 8spec: 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 34 35--- 36apiVersion: v1 37kind: Service 38metadata: 39 labels: 40 app: wheel-background 41 name: background 42spec: 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}' 2a55714dd1a22d11e88d4b0a87a399dcf-2144329260.us-east-1.elb.amazonaws.com
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.