Deploying Ruby apps to Google Cloud Kubernetes Engine continuously with CircleCI
I hadn’t had a chance to try Kubernetes for a long time, but finally a few weeks ago I got one: I’ve been working on a simple Ruby application (a Slack bot), and decided to deploy it to Google Cloud Kubernetes Engine.
There was an additional requirement to set up a continuous deployment process. For that, I chose CircleCI service.
NOTE: This post is just a playbook containing all the actions I made to get the application up and running.
Software versions:
- Kubernetes 1.9.4-gke.1
- Google Cloud SDK 195.0.0
Preparing GCloud
Let’s assume that you already have a Google Cloud account.
Go to the Console and create a new project (my-project
) with Kubernetes API enabled (more thorough description of this step could be found here).
Next step—install gcloud
CLI:
# for macOS/Homebrew users it's pretty simple
brew install caskroom/cask/google-cloud-sdk
Then you must authenticate yourself:
gcloud auth login
(Optionally) Configure the default project (to avoid specifying it with every command):
gcloud config set project my-project
Create K8S cluster:
# "g1-small" is a minimal instance type (as of the time of writing) that allows having only one node
gcloud container clusters create my-cluster --machine-type g1-small --num-nodes 1
Obtain the cluster’s credentials:
gcloud container clusters get-credentials my-cluster
Get the list of clusters to verify that everything is okay:
gcloud container clusters list
The output looks like this:
NAME LOCATION MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS
my-app-web us-east1-b 1.9.4-gke.1 35.227.92.102 g1-small 1.9.4-gke.1 1 RUNNING
Preparing application
As I’ve already told, the application I want to deploy is a simple Ruby-only app (i.e., no databases, caches, persistent stores). See the links at the end of the post on how to deploy Rails applications.
We need to build a Docker image and push to Google Container Registry.
Here is my Dockerfile
:
FROM ruby:2.5.0
RUN apt-get update && apt-get install -y build-essential git
RUN mkdir -p /app
WORKDIR /app
ENV LANG C.UTF-8
COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install --jobs 20 --retry 5
COPY . .
# We want to include the current git revision information
# to a build to track releases
ARG BUILD_SHA=unknown
ENV BUILD_SHA ${BUILD_SHA}
EXPOSE 3000
ENTRYPOINT ["bundle", "exec"]
CMD ["puma", "-p", "3000"]
Let’s build it:
docker build -t my-app -f Dockerfile.prod .
NOTE: I use Dockerfile.prod
file for production and Dockerfile
for development.
Then tag it using a specific GCloud format (containing region and project name):
docker tag my-app us.gcr.io/my-project/my-app:v1
And push:
gcloud docker -- push us.gcr.io/my-project/my-app:v1
OK. We’re almost there. Now we have to tell somehow to our K8S cluster to get this image and run.
For that we’re going to use K8S deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
name: web
spec:
replicas: 2
selector:
matchLabels:
name: web
template:
metadata:
labels:
name: web
spec:
containers:
- name: web
image: gcr.io/my-project/my-app:latest
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /_health
port: 3000
initialDelaySeconds: 10
timeoutSeconds: 1
readinessProbe:
httpGet:
path: /_health
port: 3000
initialDelaySeconds: 10
timeoutSeconds: 1
K8S documentation is a good place to read about deployments; no need to recall it here.
I’d only like to pay attention to the livenessProbe
and readinessProbe
sections: they tell K8S how to monitor the application state to provide rolling update functionality and make sure that the desired number of replicas are up and running. Note that your application should provide endpoints for these checks (/_health
in my case).
Now we need to install one more tool—kubectl
—to operate our cluster:
gcloud components install kubectl
It’s time to finally deploy our application! Let’s create our deployment:
kubectl create -f kube/web-deployment.yml
To get the information about our deployment, you can see the list of pods:
kubectl get pods
More information about a pod:
kubectl describe pod web-<pod-id>
Updating application manually
Your application is up and running. How to push a new release?
If you want to update your deployment configuration, just run the command below:
kubectl apply -f kube/web-deployment.yml
For updating the codebase, you should tell your cluster to use a new image:
docker tag my-app us.gcr.io/my-project/my-app:v1.1
gcloud docker -- push us.gcr.io/my-project/my-app:v1.1
kubectl set image deployment web web=gcr.io/my-project/my-app:v1.1
K8S will take care of creating new pods and replacing the old ones.
Running three commands and manually providing new versions is not the most convenient way to deploy, isn’t it?
Let’s make someone else do all the dirty work.
CircleCI integration
CircleCI 2.0 is a very flexible automation tool.
We want to make our deployment as easy as just making git push
(or merging PR into master
branch).
Below you can find an example .circle/config.yml
with the comments explaining every step:
version: 2
workflows:
version: 2
build_and_deploy:
jobs:
- build
- deploy:
# Run the "deploy" job only if the "build" job was successful
requires:
- build
# Run deploy job only on master branch
filters:
branches:
only:
- master
jobs:
# This job is responsible for running tests and linters
build:
docker:
- image: circleci/ruby:2.5.0
environment:
RACK_ENV: test
steps:
- checkout
- run:
name: Install gems
command: bundle install
- run:
name: Run Rubocop and RSpec
# Our default rake task contains :spec and :rubocop tasks
command: bundle exec rake
# Cache all the project files to re-use in the deploy job
# (instead of pulling the repo again)
- persist_to_workspace:
root: .
paths: .
deploy:
docker:
# official image which includes `gcloud` and `kubectl` tools
- image: google/cloud-sdk
# project information
environment:
GOOGLE_PROJECT_ID: my-project
GOOGLE_COMPUTE_ZONE: us-east1-b
GOOGLE_CLUSTER_NAME: my-app
steps:
# Attach previously cached workspace
- attach_workspace:
at: .
# SERVICE_KEY provides access to your GCloud project.
# Read more here https://circleci.com/docs/2.0/google-auth/
- run: echo $GCLOUD_SERVICE_KEY > ${HOME}/gcloud-service-key.json
# Authenticate gcloud
- run: gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
# Configure gcloud (the same steps as you do on your local machine)
- run: gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
- run: gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE}
- run: gcloud --quiet container clusters get-credentials ${GOOGLE_CLUSTER_NAME}
# Enable remote Docker (https://circleci.com/docs/2.0/building-docker-images/)
- setup_remote_docker
# Build Docker image with the current git revision SHA
- run: docker build -t brooder -f Dockerfile.prod --build-arg BUILD_SHA=${CIRCLE_SHA1} .
# Use the same SHA as our image version
- run: docker tag brooder gcr.io/my-project/my-app:${CIRCLE_SHA1}
# Using remote Docker is a little bit tricky but this "spell" works
- run: gcloud docker --docker-host=$DOCKER_HOST -- --tlsverify --tlscacert $DOCKER_CERT_PATH/ca.pem --tlscert $DOCKER_CERT_PATH/cert.pem --tlskey $DOCKER_CERT_PATH/key.pem push gcr.io/my-project/my-app:${CIRCLE_SHA1}
# and finally, "deploy" the new image
- run: kubectl set image deployment web web=gcr.io/my-project/my-app:${CIRCLE_SHA1} --record
Keeping secrets
Let’s cover one more topic here—managing app secrets.
We use Kubernetes Secrets to store sensitive information (such as third-party services API tokens).
First, you have to create secrets definition file:
# kube/app-secrets.yml
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
# The value should be base64 encoded
github_token: b2t0b2NhdA==
slack_token: Y3V0bWVzb21lc2xhY2s=
Don’t forget to encode the value into base64. For example:
echo -n "myvalue" | base64
Push secrets to the cluster:
kubectl create -f kube/app-secrets.yml
#=> secret "mysecret" created
NOTE: Remove app-secrets.yml
right after pushing to K8S (or, at least, add to .gitignore
).
Now you can pass the secrets to your app through env variables:
# web-deployment.yml
apiVersion: apps/v1
kind: Deployment
# ...
spec:
# ...
templaee:
# ...
spec:
containers:
- name: web
image: gcr.io/my-project/my-app:latest
# ....
env:
- name: MY_GITHUB_TOKEN
valueFrom:
secretKeyRef:
name: mysecret
key: github_token
- name: MY_SLACK_TOKEN
valueFrom:
secretKeyRef:
name: mysecret
key: slack_token