Using the Kubernetes Secrets API

This is the third part in my series on building and deploying a Rails app using Docker containers and Kubernetes. Here is the first part and the second part.

To follow along, you’ll need to complete the steps in the “Project Set Up” section of the previous blog post.

What are secrets?

The previous version of the ToDo app has some issues. One of the biggest is that the username and password for the database are included in plain text in several places. This isn’t a good idea for real applications. The Kubernetes Secret object was designed to handle securely sharing sensitive information between containers.

There are a couple ways to access secret objects. For this tutorial, I’m going to have the secret object mounted as a set of files on a volume the container can access. If you are used to accessing secrets in environment variables this may feel a little awkward at first but it quickly becomes familiar.

Creating the Secrets File

I have three secrets to store: the database username, the database password, and the Rails secret key. Secrets need to be base64 encoded and it is easy to do the encoding in Ruby.

1
2
3
4
5
6
7
8
9
10
require 'base64'

username = Base64.encode64('rails')
puts username

password = Base64.encode64('password')
puts password

secret_key = Base64.encode64('secret_key')
puts secret_key

Once I have the base64 encoded values I create a file for the secrets object. The yaml file to create a set of secrets looks very similar to the file that creates a pod or service.

1
2
3
4
5
6
7
8
9
10
#secrets.yml
apiVersion: v1
kind: Secret
metadata:
  name: secrets
type: Opaque
data:
  password: cGFzc3dvcmQ=
  username: cmFpbHM=
  secret-key: c2VjcmV0X2tleQ==

To create the secret I use kubectl create -f just like I do with other Kubernetes objects.

kubectl create -f secrets.yml

Modifying the Database Pod

The next step is to make the database container use the secrets. The official Postgres image doesn’t work with secrets as is so I need to make a Dockerfile to extend it. I put this Dockerfile in a separate pg directory.

FROM postgres:9.4
ENTRYPOINT []
CMD export POSTGRES_PASSWORD=$(cat /etc/secrets/password); export POSTGRES_USER=$(cat /etc/secrets/username); /docker-entrypoint.sh postgres

The last line of that Dockerfile contains three separate commands and is a bit hard to read. Here are the commands with some added whitespace:

export POSTGRES_PASSWORD=$(cat /etc/secrets/password)
export POSTGRES_USER=$(cat /etc/secrets/username)
/docker-entrypoint.sh postgres

The first line reads the password from /etc/secrets/password and puts it in the POSTGRES_PASSWORD environment variable. The second line does the same thing for the POSTGRES_USER environment variable. The last line runs the entrypoint script that is part of the Postgres image and starts up Postgres. Now I need to build this image and store it in the Google Container Registry.

docker build -t todo/pg pg/.
docker tag -f todo/pg gcr.io/my_project_id/pg:v1
gcloud docker push gcr.io/my_project_id/pg:v1

Now I need to modify the database pod to use the new image and access the secrets object. Here’s the old version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# db-pod.yml
apiVersion: v1
kind: Pod
metadata:
  labels:
    name: db
  name: db
spec:
  containers:
  - image: postgres
    name: db
    env:
    - name: POSTGRES_PASSWORD
      value: password
    - name: POSTGRES_USER
      value: rails
    ports:
    - name: pg
      containerPort: 5432
      hostPort: 5432

And here’s the new version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# db-pod.yml
apiVersion: v1
kind: Pod
metadata:
  labels:
    name: db
  name: db
spec:
  volumes:
  - name: secrets
    secret:
      secretName: secrets
  containers:
  - image: gcr.io/my_project_id/pg:v1
    name: db
    volumeMounts:
    - name: secrets
      mountPath: "/etc/secrets"
      readOnly: true
    ports:
    - name: cp
      containerPort: 5432
      hostPort: 5432

The first change is to remove the env: section. That information will come in via secrets now. After that I add a volume for the secrets (lines 9 - 12) and a volume mount (line 16 - 19). The last change is updating the image from the official Postgres image: postgres to the image I just built: gcr.io/my_project_id/pg:v1.

I create the database pod and database service using kubectl create -f.

kubectl create -f db-pod.yml
kubectl create -f db-service.yml
kubectl get pods

Now the database pods are using secrets to get the Postgres password and username.

Modifying the Rails Pods

Modifying the Rails pods to use secrets is also pretty straightforward. All the setup for the Rails pods is in init.sh so I just add lines to create environment variables from my three secrets. Here is the new version of init.sh:

1
2
3
4
5
6
7
export SECRET_KEY_BASE=$(cat /etc/secrets/secret_key)
export POSTGRES_PASSWORD=$(cat /etc/secrets/password)
export POSTGRES_USER=$(cat /etc/secrets/username)

bundle exec rake db:create db:migrate
bundle exec rake assets:precompile
bundle exec rails server -b 0.0.0.0

I also need to modify config/database.yml (the Rails database config file) to use the environment variables created in init.sh.

1
2
3
4
5
6
7
8
9
production:
  <<: *default
  adapter: postgresql
  encoding: unicode
  database: todo_production
  user: <%= ENV['POSTGRES_USER'] %>
  password: <%= ENV['POSTGRES_PASSWORD'] %>
  host:     <%= ENV['DB_SERVICE_HOST'] %>
  port:     <%= ENV['DB_SERVICE_PORT'] %>

Because I changed the Dockerfile, I need to rebuild the Docker image for the Rails container. I’m tagging this version of the image as v2. Once the image is built, I push it up to the Google Container Registry.

docker build -t todo .
docker tag -f todo gcr.io/my_project_id/todo:v2
gcloud docker push gcr.io/my_project_id/todo:v2

Just like with the database I need to modify the pods to use the new image and to access the secrets. The Rails pods are created by the web replication controller so I make my changes to web-controller.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# web-controller.yml
apiVersion: v1
kind: ReplicationController
metadata:
  labels:
    name: web
  name: web-controller
spec:
  replicas: 2
  selector:
    name: web
  template:
    metadata:
      labels:
        name: web
    spec:
      volumes:
      - name: secrets
        secret:
          secretName: secrets
      containers:
      - image: gcr.io/my_project_id/todo:v3
        name: web
        volumeMounts:
        - name: secrets
          mountPath: /etc/secrets
          readOnly: true
        ports:
        - containerPort: 3000
          name: http-server

To start up the Rails pods and web service I follow the same steps as I did in the previous Kubernetes blog post.

kubectl create -f web-controller.yml

kubectl get rc
kubectl get pods

kubectl create -f web-service.yml

kubectl get services

The final step is opening up the firewall. This step doesn’t change at all:

gcloud compute firewall-rules create --allow=tcp:3000 --target-tags=your-node-name-here
gcloud compute forwarding-rules list

In the next post in this series, I’ll show how to make your database pod(s) more fault tolerant with a persistent disk.