Tuesday, June 26, 2018

Containerizing Django and MySQL with Kubernetes Part 1

I've been fidding with Docker and Docker Swarm a bit for the past couple of years and wanted to see how easy it was to use Kubernetes. I thought I'd start with a simple Django project I could deploy locally using Minikube. The tutorial uses Django 1.10 and 18.03.1-ce. Assuming a blank slate, we can start by installing Docker.

sudo apt-get update

sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

sudo apt-key fingerprint 0EBFCD88

sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

sudo apt-get update

sudo apt-get install docker-ce

Next we can create a Django app to use for testing Docker. The app will allow us to add names to a local database.

sudo mkdir /kubernetestest
sudo chown dan:dan /kubernetestest
cd /kubernetestest

django-admin startproject simplesite

Next I created a models directory in the app directory with a people.py file with the following:

from django.db import models

class People(models.Model):
    class Meta:
        db_table = "people"
        managed = True
    people_id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=50)

The main page code in views.py page looks like this:

from models.people import People

def index(request):
    people = []
    try:
        #if posting, add a new person to the database
        if request.method == 'POST':
            People.objects.create(name=request.POST['name'])

        #fetch all names from the database to display on the page
        people = People.objects.all()
    except Exception as ex:
        pass

    return render(request, "index.html", { "people": people })

And the template looks like this:

<html>
<body>
    Current Values
    <ul>
    {% for person in people %}
        <li>{{person.name}}</li>
    {% endfor %}
    </ul>

    <form method="POST">
        {% csrf_token %}
        Add a value:<br/>
        <input type="text" name="name"/><br/>
        <input type="submit" value="Add"/>
    </form>

</body>
</html>

For the initial run, I created a sqlite database so the site can be run locally:


python manage.py makemigrations simplesite
python manage.py sqlmigrate simplesite 0001
python manage.py migrate

Now we can run our server and test it:

python manage.py runserver

Now that the basic app is runable as a site, we can think about how to containerize. I wanted the containerized solution to use NGINX and uWSGI instead of just the built in Django webserver so Docker would need to set them up complete with config files. In a production solution you might use separate containers for these services but I combined them with Django for simplicity. I created a directory parallel to the Django website called setupscripts with the basic uWSGI and NGINX config files. The uWSGI config sits in simplesite.ini:


[uwsgi]
touch-reload = /tmp/simplesite
socket = 127.0.0.1:3031
enable-threads = true
single-interpreter = true
chmod-socket = 770
chown-socket www-data:www-data
workers = 15
uid = www-data
gid = www-data
chdir = /simplesite
wsgi-file=/simplesite/simplesite/simplesite.wsgi
for-readline = /simplesite/uwsgi_vars.ini
  env = %(_)
endfor =
module = django.core.handlers.wsgi:WSGIHander()
buffer-size = 16384

The environment variables that uWSGI will pass to Django will come from a config file that will be created when the container starts. The NGINX settings are in a file called nginx-default:

server {
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;

        location / {
          include uwsgi_params;
          uwsgi_pass 127.0.0.1:3031;
        }

}

Finally we need a wsgi file in our Django app for uWSGI to use. The simplesite.wsgi file goes in the main app directory:

import os, sys
import json
import socket

apache_configuration = os.path.dirname(__file__)
project = os.path.dirname(apache_configuration)
workspace = os.path.dirname(project)
toplevel = os.path.dirname(workspace)
sys.path.append(workspace)
sys.path.append(project)
sys.path.append(toplevel)

#if not set by uwsgi, default to base settings
if not "DJANGO_SETTINGS_MODULE" in os.environ:
    os.environ["DJANGO_SETTINGS_MODULE"] = "simplesite.settings"

if "MYSQL_HOST_DNS" in os.environ:
    os.environ["MAIN_DB_HOST"] = socket.gethostbyname(os.environ["MYSQL_HOST_DNS"])
else:
    os.environ["MAIN_DB_HOST"] = "localhost"

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

The file makes sure the app defaults to the base settings file if no alternate settings file is supplied in the environment variables. The actual Dockerfile to house the Djano app sits in the root project directory along with manage.py. It looks like this:

FROM ubuntu:latest

RUN apt-get update
RUN apt-get install -y python python-dev python-pip build-essential
RUN apt-get install -y nginx uwsgi uwsgi-plugin-python curl nano net-tools
RUN apt-get install -y libmysqlclient-dev python-mysqldb

RUN /usr/bin/pip install Django==1.10.8

RUN mkdir /simplesite && chown www-data:www-data /simplesite

ADD setupscripts/init.sh /tmp/init.sh
ADD setupscripts/nginx-default /etc/nginx/sites-available/default
ADD setupscripts/simplesite.ini /etc/uwsgi/apps-available/simplesite.ini
ADD manage.py /simplesite
COPY simplesite /simplesite/simplesite

RUN ln -s /etc/uwsgi/apps-available/simplesite.ini /etc/uwsgi/apps-enabled/simplesite.ini
RUN touch /tmp/simplesite

RUN ["chmod", "+x", "/tmp/init.sh"]
ENTRYPOINT ["/tmp/init.sh"]

CMD ["nginx", "-g", "daemon off;"]

Now we need to create the init.sh file that the container will call when it starts. The file goes in the setupscripts directory and creates the file with the needed uWSGI variables from environment variables and then runs NGINX:

#!/bin/bash

chown -R www-data:www-data /simplesite

touch /simplesite/uwsgi_vars.ini

service uwsgi restart

exec "$@"

Finally we can build our docker container:

sudo docker build -t simplesite .

To test it:


sudo docker run -d -p 8095:80 simplesite

To run it, go to http://localhost:8095/. You should be able to add names to the database using the text entry box on the form.

The next step is to set up our MySQL database so we can use it in place of sqlite. I created a new Dockerfile in a parallel directory to the Django project root:

FROM mysql:5.7.15

ADD schema.sql /docker-entrypoint-initdb.d

EXPOSE 3306

Next I created a schema.sql file in the same directory to handle the base setup for our new db:

create database kubernetes_test;
create user 'testuser'@'%' identified by 'password';
grant all privileges on kubernetes_test.* to 'testuser'@'%' with grant option;
use kubernetes_test;
create table people (people_id integer not null auto_increment primary key, name varchar(50));

Now we can create the docker image from the directory where the Dockerfile is:


sudo docker build -t mysqlmain .

Now we need to update our Django container to be able to optionally use a MySQL database. I created a new settings file in the Django app and called it settings_docker.py:

from settings import *
import os

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": os.getenv("MAIN_DB_NAME"),
        "USER": os.getenv("MAIN_DB_USER"),
        "PASSWORD": os.getenv("MAIN_DB_PASSWORD"),
        "HOST": os.getenv("MAIN_DB_HOST"),
    }
}

Environment variables allow us to not hard code any assumptions about the database. The init.sh file can now be updated to set the variables our Django container will need.

#!/bin/bash

chown -R www-data:www-data /simplesite

touch /simplesite/uwsgi_vars.ini

echo "MYSQL_HOST_DNS=$MYSQL_HOST_DNS" >>/simplesite/uwsgi_vars.ini
echo "MAIN_DB_NAME=$MAIN_DB_NAME" >>/simplesite/uwsgi_vars.ini
echo "MAIN_DB_USER=$MAIN_DB_USER" >>/simplesite/uwsgi_vars.ini
echo "MAIN_DB_PASSWORD=$MAIN_DB_PASSWORD" >>/simplesite/uwsgi_vars.ini

if [ -z "$DJANGO_SETTINGS_FILE" ]; then
    echo "DJANGO_SETTINGS_MODULE=simplesite.settings" >>/simplesite/uwsgi_vars.ini
    python /simplesite/manage.py migrate
    chown www-data:www-data /simplesite/db.sqlite3
else
    echo "DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_FILE" >>/simplesite/uwsgi_vars.ini
fi

service uwsgi restart

exec "$@"

We can now re-create our docker image:


sudo docker build -t simplesite .

Next we will use docker compose to test them out together. You can install docker compose with the following if you don't already have it:


sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Now we can create a docker-compose.yml file in a separate directory:

version: '3'
services:
  db:
    image: "mysqlmain"
    environment:
     - MYSQL_ROOT_PASSWORD=password
  web:
    image: "simplesite"
    ports:
     - "8095:80"
    links:
     - db:mysqlmain
    environment:
     - DJANGO_SETTINGS_FILE=settings_docker
     - MYSQL_HOST_DNS=mysqlmain
     - MAIN_DB_NAME=kubernetes_test
     - MAIN_DB_USER=testuser
     - MAIN_DB_PASSWORD=password

This file will set some environment variables for us such as the settings file Django should use and the credentials to connect to our database. To create our containers, run:

sudo docker-compose up

And now the site should again be reachable at http://localhost:8095/. In the part 2 I'll show how to use kubernetes locally to manage the containers.