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.