Wordpress and Varnish on Kubernetes Step by Step
Overview
My goal with this post is to explore Kubernetes' Ingress Controller and Ingress resources. The best way to do that is to deploy a real application: a Wordpress site with a Varnish HTTP cache and a MariaDB database.
If you're following along, I'm using minikube (v0.19.0).
Let's dive right in. This is the overall setup we want to end up with:
Ingress Controller
The Ingress Controller is a control loop that handles inbound traffic and applies rules to deliver that traffic to applications within our Kubernetes cluster.
It runs separately from the Kubernetes master and allows you to write your own controller code. This is great because we can decouple the routing rules from our application.
Minikube ships with an Nginx Ingress Controller that you can enable by running:
minikube addons enable ingress
If you are using GCE, the controller will be an L7 Cloud Load Balancer, while on AWS you will get an Application Load Balancer (ALS). In either case, the controller listens to ingress events in the Kubernetes API and creates all the resources needed to route the traffic.
Unless you plan to write your own controller, this will be done behind the scenes. All you need to do is create an Ingress resource that will be picked up by that controller. Take a look at this sample ingress:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: sample-ingress
spec:
tls:
- secretName: tls-certificate
hosts:
- example.com
rules:
- host: example.com
http:
paths:
- path: /
backend:
serviceName: example-service
servicePort: 80
The ingress will route the traffic based on domain and path. This example handles https traffic as well and receives the certificates via a secret resource.
Minikube Ingress Controller
With minikube this means the Ingress Controller will detect the Ingress we created and update the Virtual Hosts with the appropriate server
config blocks.
kubectl -n kube-system get all
You should see a few objects that we just created:
- rc/nginx-ingress-controller
- po/nginx-ingress-controller-***
- svc/default-http-backend
Let's connect to the nginx-ingress-controller pod to see what minikube created for us:
kubectl -n kube-system exec -it po/nginx-ingress-controller-*** bash
We know this is an Nginx server so let's check out the main nginx.conf:
cat /etc/nginx/nginx.conf
This is pretty standard, a few items worth mentioning is the default SSL certs and the default-backend:
upstream upstream-default-backend {
least_conn;
server 172.17.0.2:8080 max_fails=0 fail_timeout=0;
}
server {
ssl_certificate /ingress-controller/ssl/default-fake-certificate.pem;
ssl_certificate_key /ingress-controller/ssl/default-fake-certificate.pem;
location / {
proxy_pass http://upstream-default-backend;
}
}
Notice that Nginx will proxy all requests to upstream-default-backend
. This service will return a 404 error if the request is not host-matched by any of the running ingresses. We can check this by curl-ing the IP associated with minikube:
~ $ curl -I $(minikube ip)
HTTP/1.1 404 Not Found
Server: nginx/1.11.12
Date: Thu, 18 May 2017 20:35:55 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 21
Connection: keep-alive
Strict-Transport-Security: max-age=15724800; includeSubDomains;
Database
Let's deploy the MariaDB instance. I added everything in a single yaml file for this writeup. How you split those resources depends on how you plan to upgrade them in your cluster. If you think you will modify your services more often than the deployments then it makes sense to keep them separate.
We have a Persistent Volume and a Claim for it. The ReplicationController defined what container image we want our pods to run, how many replicas we want etc.
I will use the official MariaDB docker image. If the image has good documentation you will usually see a list of ENV variables it accepts. It looks like we can set the root password, an empty DB and a user.
apiVersion: v1
kind: Service
metadata:
name: wordpress-mariadb
spec:
ports:
- port: 3306
selector:
app: mariadb
---
kind: PersistentVolume
apiVersion: v1
metadata:
name: mariadb-pv-volume
labels:
type: local
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/tmp/data"
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mariadb-pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi
---
apiVersion: v1
kind: ReplicationController
metadata:
name: mariadb-rc
spec:
replicas: 1
selector:
app: mariadb
template:
metadata:
name: mariadb-rc
labels:
app: mariadb
spec:
volumes:
- name: mariadb-pv-storage
persistentVolumeClaim:
claimName: mariadb-pv-claim
containers:
- name: mariadb
image: mariadb:latest
env:
- name: MYSQL_DATABASE
value: wp-kube-db
- name: MYSQL_USER
value: wp-kube-usr
- name: MYSQL_PASSWORD
value: q1w2e3r4t5
- name: MYSQL_ROOT_PASSWORD
value: root-pass
ports:
- containerPort: 3306
volumeMounts:
- mountPath: "/var/lib/mysql"
name: mariadb-pv-storage
Now let's create the resources:
kubectl -f mariadb.yaml
$ kubectl get all
NAME READY STATUS RESTARTS AGE
po/mariadb-rc-hr35k 1/1 Running 0 47s
NAME DESIRED CURRENT READY AGE
rc/mariadb-rc 1 1 1 47s
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
svc/kubernetes 10.0.0.1 <none> 443/TCP 1d
svc/wordpress-mariadb None <none> 3306/TCP 47s
We can connect to the pod and try the database connection (remember to change the pod id):
kubectl exec -it mariadb-rc-hr35k bash
Wordpress
Just like in our previous step, we will use the official Wordpress Docker image. There are multiple PHP versions and a choice between Apache and Nginx. For simplicity we'll go for the latest tag that is pulling from php:5.6-apache
.
We can also setup a lot of configuration options through ENV variables. For our setup we need the database name, host, user and password. For the host we can use the name of the MariaDB service we just created (wordpress-mariadb) and have Kubernetes resolve that for us.
We are using a Persistent Volume and we're mounting that at the location where Wordpress installs the app. To find that out I had to check the Dockerfile of the latest tag.
apiVersion: v1
kind: Service
metadata:
name: wordpress-site
spec:
ports:
- port: 80
selector:
app: wordpress
---
kind: PersistentVolume
apiVersion: v1
metadata:
name: wordpress-pv-volume
labels:
type: local
spec:
capacity:
storage: 3Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/tmp/data"
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: wordpress-pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi
---
apiVersion: v1
kind: ReplicationController
metadata:
name: wordpress-rc
spec:
replicas: 1
selector:
app: wordpress
template:
metadata:
name: wordpress-rc
labels:
app: wordpress
spec:
volumes:
- name: wordpress-pv-storage
persistentVolumeClaim:
claimName: wordpress-pv-claim
containers:
- name: wordpress
image: wordpress:latest
env:
- name: WORDPRESS_DB_HOST
value: wordpress-mariadb
- name: WORDPRESS_DB_USER
value: wp-kube-usr
- name: WORDPRESS_DB_PASSWORD
value: q1w2e3r4t5
- name: WORDPRESS_DB_NAME
value: wp-kube-db
ports:
- containerPort: 80
volumeMounts:
- mountPath: "/var/www/html"
name: wordpress-pv-storage
Let's create the wordpress related objects as well:
kubectl create -f wordpress.yaml
When the container creation is done and we see everything in the Ready state we can connect directly to the Wordpress service and check that it's working:
minikube service wordpress-site
To get the port number and IP you can run minikube ip and kubectl get all:
svc/wordpress-site 10.0.0.24 <nodes> 80:31761/TCP 2m
Ingress
We explored the Ingress Controller earlier and now it's time to look at the Ingress resource that defines the host matching rules.
Let's create our initial ingress resource and direct the traffic to the wordpress service (we'll add Varnish in between in the next step)
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: wp-ingress
spec:
rules:
- host: wordpress.local
http:
paths:
- path: /
backend:
serviceName: wordpress-site
servicePort: 80
Note that I'm using wordpress.local to match requests for our Wordpress site. I had to add our cluster's IP to my /etc/hosts file
192.168.64.2 wordpress.local
We create the ingress by running the classic kubectl create -f ingress.yaml
command. We should be able to see the site if we browse to http://wordpress.local
.
Let's revisit the nginx-ingress-controller pod we checked last time and see the changes:
kubectl -n kube-system exec -it po/nginx-ingress-controller-*** bash
Let's cat the output of nginx.conf:
server {
server_name wordpress.local;
listen 80;
listen [::]:80;
location / {
proxy_pass http://default-wordpress-site-80;
}
}
It looks like the Ingress Controller added our newly created Ingress.
Varnish
Varnish is an in-memory HTTP Cache. You can have it listen on port 80 and direct the traffic to a backend running your application. In most cases it will sit behind an Nginx or HAProxy instance in order to handle SSL termination. As long as your application responds with proper cache headers Varnish will work with minimal configuration.
We will use this default.vcl config for Wordpress. I picked million12/varnish for the Varnish Docker image because it allows us to override the default.vcl
config very easily.
Let's create the configmap for the default.vcl file:
kubectl create configmap varnish-vcl --from-file=default.vcl
Now we can create the varnish service and RC:
kubectl create -f varnish.yaml
apiVersion: v1
kind: Service
metadata:
name: varnish-svc
spec:
ports:
- port: 80
selector:
app: varnish-proxy
---
apiVersion: v1
kind: ReplicationController
metadata:
name: varnish-proxy
spec:
replicas: 1
selector:
app: varnish-proxy
template:
metadata:
name: varnish-proxy
labels:
app: varnish-proxy
spec:
volumes:
- name: varnish-config
configMap:
name: varnish-vcl
items:
- key: default.vcl
path: default.vcl
containers:
- name: varnish
image: million12/varnish
env:
- name: VCL_CONFIG
value: /etc/varnish/default.vcl
volumeMounts:
- name: varnish-config
mountPath: /etc/varnish/
ports:
- containerPort: 80
Ingress Updates
Now that we have the Varnish service up and running we can direct the traffic to it. All we have to do is update our ingress resource and specify the Varnish service instead of the Wordpress one.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: wp-ingress
spec:
rules:
- host: wordpress.local
http:
paths:
- path: /
backend:
serviceName: varnish-svc
servicePort: 80
Now if we curl for http://wordpress.local we can check that Varnish handles that request:
$ curl -I http://wordpress.local
HTTP/1.1 302 Found
Access-Control-Allow-Origin: *
Age: 0
Cache-Control: no-cache, must-revalidate, max-age=0
Content-Length: 0
Content-Type: text/html; charset=UTF-8
Date: Fri, 19 May 2017 13:58:49 GMT
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Location: https://wordpress.local/wp-admin/install.php
Server: Apache/2.4.10 (Debian)
Via: 1.1 varnish-v4
X-Cache: MISS
X-Cacheable: NO:Not Cacheable
X-Powered-By: PHP/5.6.30
X-Unsetcookies: TRUE
X-Varnish: 2
Conclusion
Did you notice how easy it was for us to update the route mapping? After we added the Varnish Service all we had to do is specify that in our ingress. We didn't have to mess with the Wordpress deploy at all! It may seem trivial for this example, but if you are running a complex set of services with different domains and sub-domains, this separation will make your life a lot easier.
Future posts
Stay tuned for Part 2
where we will handle HTTPS traffic and improve the Wordpress deploy with an Nginx/PHP-FPM Docker image.