Kubernetes Frontend Service with Nginx

Wed Apr 12, 2017 - 1200 Words

With both Rails and Postgres running smoothly in your Kubernetes cluster it’s time for you to create a frontend service to handle the web requests. In this tutorial, we’ll walk through what it looks like for us to set up Nginx as the frontend service for our Rails application.

Goals:

  • Create Nginx Deployment/Service in front of Rails
  • Understand why a separate frontend vs backend is good.

If you went through the Ruby on Rails series then you’ve already worked a little with having Nginx in front of your Rails application, but today we’re going to translate that over to working with Kubernetes.

Before we get started, if you’ve stoped and restarted minikube and you attempt to connect to the mealplan service in your browser you’ll be met with this error:

An unhandled low-level error occurred. The application logs may have details.

Not the way we wanted to start this tutorial.

Fixing the Postgres Persistence Layer for Minikube

This is caused by an oversite when we created the PersistentVolumeClaim in the previous tutorial. Our database was removed sadly. The reason that we lost our database after spinning down the minikube was because we mapped our PersistentVolumeClaim to the wrong directory in our postgres container. We pointed to /var/lib/postgresql/db-data, but the PGDATA directory is actually at /var/lib/postgresql/data. Let’s delete what currently exists so that we can patch it up:

$ kubectl delete -f deployments/postgres.yml
service "postgres" deleted
deployment "postgres" deleted
persistentvolumeclaim "postgres-pv-claim" deleted

We removed them entirely since we working locally, obviously, if we made this sort of mistake in a non-development set up we would probably do something more graceful. In addition to mapping our directory incorrectly there is also an issue with where we’re storing the data on the minikube VM. By default, new PersistentVolumes with minikube store their data in /tmp/hostpath-provider and it isn’t supposed to be removed, but at the time of writing this it was being removed when we run minikube stop. Because of this issue we’re going to manually create a PersistentVolume that stores the data under /data/ which does indeed persist between minikube starts.

deployments/postgres.yml

# Skipping the Service portion of the file
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: postgres
spec:
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - image: "postgres:9.6.2"
          name: postgres
          env:
            - name: POSTGRES_USER
              valueFrom:
                configMapKeyRef:
                  name: mealplan-config
                  key: postgres_user
            - name: POSTGRES_PASSWORD
              valueFrom:
                configMapKeyRef:
                  name: mealplan-config
                  key: postgres_password
          ports:
            - containerPort: 5432
              name: postgres
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: postgres-pv-claim
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pv-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  volumeName: postgres-pv
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgres-pv
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 5Gi
  hostPath:
    path: /data/mealplan-postgres

Now we can bring up all of these objects and recreate our database (your pod ID will be different):

$ kubectl create -f deployments/postgres.yml
service "postgres" created
deployment "postgres" created
persistentvolumeclaim "postgres-pv-claim" created
persistentvolume "postgres-pv" created
$ kubectl exec mealplan-4029866644-1srx8 --stdin --tty -- bundle exec rake db:setup db:migrate

Now the database is back and we don’t have to worry about it getting deleted again.

Building the Nginx Image

If we think about the services that we have already, Rails & Postgres, they are both what we would call “backend” services. They aren’t really meant to be directly exposed to the web because that’s not where they shine. The Rails application’s main purpose is to handle the creation of recipes and meal plans, not serving static assets (stylesheets, javascript, etc.). For this reason, we want to put a more specialized tool in front, our “frontend” server, which in this case is going to be Nginx.

Thankfully, our project already has a Nginx configuration in the form of the site.conf file. Let’s take a look at that and see what we will need to change to make it work with Kubernetes.

site.conf

server {
  listen 80;

  # Properly server assets
  location ~ ^/(assets)/ {
    root /usr/share/nginx/html;
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    add_header ETag "";
  }

  # Proxy request to rails app
  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass_header Set-Cookie;
    proxy_pass http://prod_app:3000;
  }
}

This looks pretty good, though we’re going to need to make will be changing prod_app to be the name of the service for our Rails application. We’ll also add an upstream block that we can use later. Here’s the modified file:

upstream mealplan {
  server mealplan;
}

server {
  listen 80;

  # Properly server assets
  location ~ ^/(assets)/ {
    root /usr/share/nginx/html;
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    add_header ETag "";
  }

  # Proxy request to rails app
  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass_header Set-Cookie;
    proxy_pass http://mealplan:3000;
  }
}

Unlike how you may have deployed this if you followed along in the Ruby on Rails series, we’ll need to build the image before we can deploy it. If you’re following allowing using minikube then make sure you are connected to the minikube host as your Docker host (eval $(minikube docker-env)):

$ mkdir nginx
$ mv site.conf nginx/
$ touch nginx/Dockerfile

Let’s open up the nginx/Dockerfile and set it up.

nginx/Dockerfile

FROM nginx:1.11.13-alpine

COPY site.conf /etc/nginx/conf.d/default.conf

With the Dockerfile in place, we’re able to create the image we need before we’ll be able to create our Kubernetes Deployment & Service.

$ docker build -t coderjourney/mealplan-frontend:1.0.0 nginx/

Creating the Front-end Deployment & Service

Now that we have an image in place we need to specify what Kubernetes needs to run and maintain through a Deployment and Service. Similar to the postgres tutorial we’ll define our Service and Deployment in the same file.

deployments/frontend.yml

apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  selector:
    app: mealplan
    tier: frontend
  type: LoadBalancer
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: frontend
spec:
  template:
    metadata:
      labels:
        app: mealplan
        tier: frontend
    spec:
      containers:
        - image: "coderjourney/mealplan-frontend:1.0.0"
          name: nginx
          lifecycle:
            preStop:
              exec:
                command: ["/usr/sbin/nginx","-s","quit"]

We’ve seen most everything in this file except the extra lifecycle bit in the container declaration. We’re specifying what command should be running before the container is stopped so that we don’t drop connections when stopping this particular container. With our Service and Deployment defined it’s time to create them.

$ kubectl create -f deployments/frontend.yml
service "frontend" created
deployment "frontend" created

With our service up and running we should be able to connect to our Nginx service through the web browser after we get the URL for it from minikube:

$ minikube service frontend --url
http://192.168.99.100:31844

Visiting that URL you should now show our rails application through the eyes of Nginx.

Recap

In this tutorial, we continue to familiarize ourselves with Kubernetes by creating another service that will act as the frontend for our Rails backend service. We also fixed an issue that we created when originally setting up the persistent storage for our PostgreSQL database. There are still a few more things to tackle before we’ll have a production grade deployment setup for our Rails application, but we’re getting close.