Monday, July 16, 2018

Containerizing Django and MySQL with Kubernetes Part 2: Deploying with Minikube

In part 1 I created a simple site with a database and placed it in containers. In this part we'll use Minikube with Kubernetes to manage the containers locally. Kubernetes is a free and open source system for managing containers and Minikube is a utility to allow us to run a local Kubernetes setup. The first step is to install kubectl:
       
curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl

Next we install Minikube:
       
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

Now we need to install virtual box to work with our local Kubernetes:
       
sudo apt-get install virtualbox

Now we can start minikube. For simplicity, we want our docker commands to use the docker running in Minikube. The following will start Minikube and set environment variables to make this happen:
       
minikube start
eval $(minikube docker-env)

To deploy our custom Dockerfiles, we want to use the Docker running in Minikube so we want to rebuild the images so they reside in the Minikube's repository. From the django directory we can build it like before but without sudo:
       
docker build -t simplesite .

And then the same from the mysql directory:
       
docker build -t mysqlmain .

To deploy our containers to Minikube we will create yaml files that will be read by the kubectl utility. The yaml allow us to define Pods - groupings of one or more containers that we want to run together, Services - which allow us to define how to access our pods internally or externally, and Replication Sets - controllers that allow us to define how many instances of a Pod we want to run.

Before we define our Pods and Services, we want to utilize the secret functionality of Kubernetes to hold our database credentials so we don't have to store them in plain text in the yaml. We start by getting base64 encodings of our username and password:

       
echo -n 'testuser' | base64
dGVzdHVzZXI=
echo -n 'password' | base64
cGFzc3dvcmQ=
echo -n 'rootpass' | base64
cm9vdHBhc3M=

Then we create a new file called secret.yaml with the base64 values:
       
apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
data:
  username: dGVzdHVzZXI=
  password: cGFzc3dvcmQ=
  root_password: cm9vdHBhc3M=

Now we can pass this file to kubectl to store the credentials internally for use with the containers we'll create next.
       
kubectl create -f secret.yaml

Next we need a yaml file to create a MySQL pod and expose it as a service so that our main site can access it. We'll call it mysql-main.yaml make use of the root password secret:
       
apiVersion: v1
kind: Pod
metadata:
  labels:
    name: mysqlmain
    role: db
  name: mysqlmain
spec:
  containers:
    - name: mysqlmain
      image: mysqlmain
      imagePullPolicy: IfNotPresent
      env:
          - name: MYSQL_ROOT_PASSWORD
            valueFrom:
              secretKeyRef:
                name: mysql-secret
                key: root_password
      ports:
        - containerPort: 3036
      volumeMounts:
        - mountPath: /mysqldata
          name: data
  volumes:
    - name: data
      emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  labels:
    name: mysqlmain
    role: service
  name: mysqlmain
spec:
  ports:
    - port: 3306
      targetPort: 3306
      name: port1
  selector:
    role: db

Now we can create and start the pod and service:
       
kubectl create -f mysql-main.yaml

You can look up the service information here to ensure its running:
       
kubectl get services

Finally we can create the yaml for our website. We'll call it simplesite.yaml. This makes use of the username and password secrets we previously created:
       
apiVersion: v1
kind: Pod
metadata:
  labels:
    name: simplesite
    role: master
  name: simplesite
spec:
  containers:
    - name: simplesite
      image: simplesite
      imagePullPolicy: IfNotPresent
      env:
        - name: DJANGO_SETTINGS_FILE
          value: "simplesite.settings_kubernetes"
        - name: MYSQL_HOST_DNS
          value: "mysqlmain.default.svc.cluster.local"
        - name: MAIN_DB_NAME
          value: "kubernetes_test"
        - name: MAIN_DB_USER
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: username
        - name: MAIN_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
      ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  labels:
    name: simplesite
    role: service
  name: simplesite
spec:
  ports:
    - port: 80
      protocol: TCP
      targetPort: 80
      name: port1
  selector:
    role: master
  type: NodePort

Now we can create and start the pod and service:
       
kubectl create -f simplesite.yaml

And to see that it is running:
       
kubectl get services

Now we want to get the url of the running site. You can use the following minikube to retrieve the url it is listening on:
       
minikube service simplesite --url

In my case its http://192.168.99.100:30620

You might get an error about the host not being authorized. In this case we will need to update the site's hosts setting to allow the hosts root. I left this in to demonstrate how to connect to a running container. You can access the running container with the following:

       
kubectl exec -it simplesite -- /bin/bash

And then edit the /simplesite/simplesite/settings.py file:
       
ALLOWED_HOSTS = ['*']

Then restart our service:
       
service uwsgi reload

Now we can exit the container and refresh the page in the browser. The final thing we will do is set up replicas of our website. You'll want to update the ALLOWED_HOSTS in the source settings file and rebuild the docker image for simplesite before continuing. For simplicity, we will use the Kubernetes Deployment object which is the recommended way to control containers. We will create a new yaml called simplesite-deployment.yaml:

       
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: simplesite
  labels:
    name: simplesite
spec:
  replicas: 3
  template:
    metadata:
      labels:
        name: simplesite
    spec:
      containers:
      - name: simplesite
        image: simplesite
        imagePullPolicy: IfNotPresent
        env:
          - name: DJANGO_SETTINGS_FILE
            value: "simplesite.settings_kubernetes"
          - name: MYSQL_HOST_DNS
            value: "mysqlmain.default.svc.cluster.local"
          - name: MAIN_DB_NAME
            value: "kubernetes_test"
          - name: MAIN_DB_USER
            valueFrom:
              secretKeyRef:
                name: mysql-secret
                key: username
          - name: MAIN_DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: mysql-secret
                key: password
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: simplesite
  labels:
    name: simplesite
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 80
  selector:
    name: simplesite

This file will create 3 containers of our site and a service to load balance access to them. Before running it we should stop the existing service:
       
kubectl delete services simplesite
kubectl delete pods simplesite

And now we can create our new containers:
       
kubectl create -f simplesite-deployment.yaml

We can again retrieve the url of the service with:
       
minikube service simplesite --url

We now have a load balanced site with a MySQL backend. Further steps to improve this would include using a volume to persist the MySQL data in case the container is re-created later.