Rails on Kubernetes - Part 3

Overview

This is Part 3 of the Rails on Kubernetes series.

Code

If you don't want to read the previous posts and just follow along you can clone my repo and get going right away.

Deployments

So far we created our Pods using ReplicationControllers. While this is fine, the recommended way is to use Deployments instead. This is a new feature in Kubernetes that allows you to control Rolling Upgrades, Rollbacks and more.

This is how the basic template looks:

apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  minReadySeconds: 1 # number of seconds after pod is up that it's considered ready (default 0)
  strategy:
    type: RollingUpdate # the other option is Recreate where all pods killed before update
    maxUnavailable: 1 # max number of pods that can become unavailable during update
    maxSurge: 1 # max number of extra pods created during deploy (3 + 1 in our case)
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

I went ahead and re-organized the /kube folder in our application. You can see the changes in this commit log.

This is our folder structure now:

├── deployments
│   ├── postgres_deploy.yaml
│   ├── rails_deploy.yaml
│   ├── redis_deploy.yaml
│   └── sidekiq_deploy.yaml
├── ingresses
│   └── ingress.yaml
├── jobs
│   └── setup.yaml
├── services
│   ├── postgres_svc.yaml
│   ├── rails_svc.yaml
│   └── redis_svc.yaml
└── volumes
    └── postgres_volumes.yaml

Initial Deploy

Since we changed things around we should re-deploy. You can tear down all the resources you created on your own. I will start on a fresh namespace:

~ $ kubectl create namespace rails-staging

Let's create the secrets:

~ $ kubectl -n rails-staging create secret generic db-user-pass --from-literal=password=mysecretpass
~ $ kubectl -n rails-staging create secret generic db-user --from-literal=username=postgres
~ $ kubectl -n rails-staging create secret generic secret-key-base --from-literal=secret-key-base=50dae16d7d1403e175ceb2461605b527cf87a5b18479740508395cb3f1947b12b63bad049d7d1545af4dcafa17a329be4d29c18bd63b421515e37b43ea43df64

Now we can run Redis and Postgres:

~ $ kubectl -n rails-staging create -f kube/volumes/postgres_volumes.yaml
~ $ kubectl -n rails-staging create -f kube/services/postgres_svc.yaml
~ $ kubectl -n rails-staging create -f kube/services/redis_svc.yaml
~ $ kubectl -n rails-staging create -f kube/deployments/postgres_deploy.yaml --record
~ $ kubectl -n rails-staging create -f kube/deployments/redis_deploy.yaml --record

Finally, let's run the Setup job for migrations and the Rails app:

~ $ kubectl -n rails-staging create -f kube/jobs/setup.yaml
~ $ kubectl -n rails-staging create -f kube/services/rails_svc.yaml
~ $ kubectl -n rails-staging create -f kube/deployments/rails_deploy.yaml --record
~ $ kubectl -n rails-staging create -f kube/deployments/sidekiq_deploy.yaml --record
~ $ kubectl -n rails-staging create -f kube/ingresses/ingress.yaml

Rollout History

Because we ran our deployment resources with the --record flag we can now access a Revision History for each deploy.

~ $ kubectl -n rails-staging rollout history deploy/rails-deployment
deployments "rails-deployment"
REVISION  CHANGE-CAUSE
1         kubectl create --namespace=rails-staging --filename=kube/deployments/rails_deploy.yaml --record=true

Applying an update

Let's see what happens if we push an update. First let's add some changes to our app. In my case I added Bootstrap. You can see the changes here.

To test this locally I can either run the app locally or use docker-compose to build and serve it.

~ $ docker-compose build && docker-compose up

Now I can navigate to http://localhost:3000 and check that everything looks good.

Let's push this new image to DockerHub:

~ $ bundle exec rake docker:push_image
...
Done pushing image a7f0b5d

Now we can set a new image for our rails and sidekiq deployments:

~ $ kubectl -n rails-staging set image deploy/rails-deployment rails=tzumby/rails-app-alpine:57b3e12

Since we only have 1 Replica, our rollout settings won't mean anything - Kube will simply create a new Pod and kill the old one. We can check the rollout history:

~ $ kubectl -n rails-staging rollout history deploy/rails-deployment
deployments "rails-deployment"
REVISION  CHANGE-CAUSE
1         kubectl create --namespace=rails-staging --filename=kube/deployments/rails_deploy.yaml --record=true
2         kubectl set image deploy/rails-deployment rails=tzumby/rails-app-alpine:85d97f1 --namespace=rails-stagin

Undo a deploy

Let's say whatever change we pushed is crashing and we want to revert back. This is pretty simple:

~ $ kubectl -n rails-staging rollout undo deployment/rails-deployment --to-revision=1

Scaling

If you read Part 1 of this series you'll see I mentioned scaling as one of the benefits of running your Rails app in Kubernetes. It's time to deliver on that promise and perform some load testing & scaling.

Let's take a quick look at the resources we created so far.

~ $ kubectl get pods
NAME                                  READY     STATUS    RESTARTS   AGE
postgres-695fcd89f9-frz59             1/1       Running   0          5h
rails-deployment-59cd86c755-m66tq     1/1       Running   0          5h
redis-deployment-746c545869-tjbx6     1/1       Running   0          5h
sidekiq-deployment-7f5bcf6ccf-ncgxs   1/1       Running   0          5h

In my case I have one pod running for each service. These also come with their own Replica Sets:

~ $ kubectl get rs
AME                            DESIRED   CURRENT   READY     AGE
postgres-695fcd89f9             1         1         1         14m
rails-deployment-59cd86c755     1         1         1         6m
redis-deployment-746c545869     1         1         1         14m
sidekiq-deployment-7f5bcf6ccf   1         1         1         12m

Notice how the Desired, Current and Ready are all set to 1. This is because we used one replica when we defined those resources:

apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: rails
spec:
  replicas: 1 # this is the number of replicas

Apache Benchmark

We'll use ab to test our initial req/s throughput. This will be very basic - I just want to see an increase in the amount of req/s the app is handling (this is not by any means a performance test).

~ $ ab -n 500 -c 500 http://rails.local/
Requests per second:    172.24 [#/sec] (mean)

Now let's scale our Rails app to 4 replicas:

~ $ kubectl -n rails-staging scale deployment rails-deployment --replicas=4

We can check the Replica Sets and make sure everything worked:

~ $ kubectl -n rails-staging get rs
NAME                            DESIRED   CURRENT   READY     AGE
rails-deployment-59cd86c755     4         4         4         5h

We have 4 of the Pods ready it seems. Let's try to hammer the app again.

~ $ ab -n 500 -c 500 http://rails.local/
Requests per second:    256.35 [#/sec] (mean)

Looks like we're doing a lot better now at 256 req/s. Again, this doesn't really tell us anything about the performance of our app. I just wanted to use those numbers to verify that our deployment scaling worked.

We ran those scale commands without --record. If we did that we would even get those changes listed as Revisions:

~ $ kubectl -n rails-staging scale deployment rails-deployment --replicas=1 --record
~ $ kubectl -n rails-staging rollout history deployment/rails-deployment
REVISION  CHANGE-CAUSE
1         kubectl create --namespace=rails-staging --filename=kube/deployments/rails_deploy.yaml --record=true
2         kubectl set image deploy/rails-deployment rails=tzumby/rails-app-alpine:85d97f1 --namespace=rails-staging
3         kubectl scale deployment rails-deployment --namespace=rails-staging --replicas=1 --record=true

Conclusion

We looked at the new Deployments resource in Kubernetes and how we can use that to deploy new code or revert code changes. Deployments also allow us to scale our pods up and down. I didn't cover auto-scaling here but you can check that out on Kubernete's site.

What I described is a very manual process. This is great for learning how the ecosystem works but in a real production application you would most likely use a CI/CD tool that builds and pushes your containers on demand (via git-hooks or some manual trigger).