Write your first Kubernetes charm for a Django app

In this tutorial you will learn how to build a rock and Kubernetes charm for a Django application using the charm SDK, so you can have your Django application up and running with Juju in about an hour!

:open_book: rock
An Ubuntu LTS-based OCI compatible container image designed to meet security, stability, and reliability requirements for cloud-native software.

:open_book: charm

A package consisting of YAML files + Python code that will automate every aspect of an application’s lifecycle so it can be easily orchestrated with Juju.

:open_book: Juju
An orchestration engine for software operators that enables the deployment, integration and lifecycle management of applications using charms.

What you’ll need:

  • A working station, e.g., a laptop, with amd64 architecture which has sufficient resources to launch a virtual machine with 4 CPUs, 4 GB RAM, and a 50 GB disk
  • Familiarity with Linux

What you’ll do:

At any point, to give feedback or ask for help:

Don’t hesitate to get in touch on Matrix or Discourse (or follow the “Help improve this document in the forum” on the bottom of this doc to comment directly on the doc).

Set things up

Install Multipass.

See more: Multipass | How to install Multipass

Use Multipass to launch an Ubuntu VM.

Fast track approach

To fast-track the setup, launch a VM with the name charm-dev using the charm-dev blueprint:

multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev charm-dev

Once the VM is up, open a shell into it:

multipass shell charm-dev

MicroK8s ingress is required to expose the Django application. Enable using:

sudo microk8s enable ingress

Hands-on approach

For a more hands-on setup, use Multipass to launch an Ubuntu VM with the name charm-dev from the 22.04 blueprint:

multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 22.04

Once the VM is up, open a shell into it:

multipass shell charm-dev

In order to create the rock, you’ll need to install Rockcraft:

sudo snap install rockcraft --classic

LXD will be required for building the rock. Make sure it is installed and initialised:

sudo snap install lxd
lxd init --auto

In order to create the charm, you’ll need to install Charmcraft:

sudo snap install charmcraft --channel latest/edge --classic

This tutorial requires version 3.2.0 or later of Charmcraft. Check the version of Charmcraft using charmcraft --version If you have an older version of Charmcraft installed, use sudo snap refresh charmcraft --channel latest/edge to get the latest edge version of Charmcraft.

MicroK8s is required to deploy the Django application on Kubernetes. Install MicroK8s:

sudo snap install microk8s --channel 1.31-strict/stable
sudo adduser $USER snap_microk8s
newgrp snap_microk8s

Wait for MicroK8s to be ready using sudo microk8s status --wait-ready. Several MicroK8s add-ons are required for deployment. The MicroK8s registry is required to host the OCI image of the Django application and ingress is required to expose the Django application. Enable all using:

sudo microk8s enable hostpath-storage
sudo microk8s enable registry
sudo microk8s enable ingress

Juju is required to deploy the Django application. Install Juju and boostrap a develoment controller:

sudo snap install juju --channel 3.5/stable
mkdir -p ~/.local/share
juju bootstrap microk8s dev-controller

Create the tutorial directory

Finally, create a new directory for this tutorial and go inside it:

mkdir django-hello-world
cd django-hello-world

Create the Django application

Create a requirements.txt file, copy the following text into it and then save it:

Django

Install python3-venv and create a virtual environment:

sudo apt-get update && sudo apt-get install python3-venv -y
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Create a new project using django-admin:

django-admin startproject django_hello_world

Run the Django application locally

Change into the django_hello_world directory and run the Django application to verify that it works:

cd django_hello_world
python3 manage.py runserver

Test the Django application by using curl to send a request to the root endpoint. You may need a new terminal for this; if you are using Multipass use multipass shell charm-dev to get another terminal:

curl localhost:8000

The Django application should respond with The install worked successfully! Congratulations!.

The response from the Django application includes HTML and CSS which makes it difficult to read on a terminal.

The Django application looks good, so you can stop it for now using ctrl+C.

Pack the Django application into a rock

First, we’ll need a rockcraft.yaml file. Rockcraft will automate its creation and tailoring for a Flask application by using the django-framework profile:

cd ..
rockcraft init --profile django-framework

The rockcraft.yaml file will automatically be created and set the name based on your working directory. Open it in a text editor and check that the name is django-hello-world. Ensure that platforms includes the architecture of your host. For example, if your host uses the ARM architecture, include arm64 in platforms.

For this tutorial, we’ll use the name django-hello-world and assume you are on the amd64 platform. Check the architecture of your system using dpkg --print-architecture. Choosing a different name or running on a different platform will influence the names of the files generated by Rockcraft.

Django applications require a database. Django will use a sqlite database by default. This won’t work on Kubernetes because the database would disappear every time the pod is restarted (e.g., to perform an upgrade) and this database would not be shared by all containers as the application is scaled. We’ll use Juju later to easily deploy a database.

We’ll need to update the settings.py file to prepare for integrating the app with a database. Open django_hello_world/django_hello_world/settings.py and include import json, import os and import secrets along with the other imports at the top of the file.

Near the top of the settings.py file change the following settings to be production ready:

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', secrets.token_hex(32))

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DJANGO_DEBUG', 'false') == 'true'

ALLOWED_HOSTS = json.loads(os.environ.get('DJANGO_ALLOWED_HOSTS', '[]'))

Go further down to the Database section and change the DATABASES variable to:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('POSTGRESQL_DB_NAME'),
        'USER': os.environ.get('POSTGRESQL_DB_USERNAME'),
        'PASSWORD': os.environ.get('POSTGRESQL_DB_PASSWORD'),
        'HOST': os.environ.get('POSTGRESQL_DB_HOSTNAME'),
        'PORT': os.environ.get('POSTGRESQL_DB_PORT'),
    }
}

We’ll need to update the requirements.txt file to include psycopg2-binary so that the Django app can connect to PostgreSQL.

Pack the rock:

ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack

Depending on your network, this step can take a couple of minutes to finish.

ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS is required whilst the Django extension is experimental.

Once Rockcraft has finished packing the Flask rock, you’ll find a new file in your working directory with the .rock extension:

ls *.rock -l

The rock needs to be copied to the MicroK8s registry so that it can be deployed in the Kubernetes cluster:

rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:django-hello-world_0.1_amd64.rock docker://localhost:32000/django-hello-world:0.1

If you changed the name or version in rockcraft.yaml or are not on an amd64 platform, the name of the .rock file will be different for you.

Create the charm

Create a new directory for the charm and go inside it:

mkdir charm
cd charm

We’ll need a charmcraft.yaml, requirements.txt and source code for the charm. The source code contains the logic required to operate the Django application. Charmcraft will automate the creation of these files by using the django-framework profile:

charmcraft init --profile django-framework --name django-hello-world

The files will automatically be created in your working directory. We will need to connect to the PostgreSQL database. Open the charmcraft.yaml file and add the following section to the end of the file:

requires:
  postgresql:
    interface: postgresql_client
    optional: false
    limit: 1

The charm depends on several libraries. Download the libraries and pack the charm:

CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft fetch-libs
CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack

Depending on your network, this step can take a couple of minutes to finish.

Once Charmcraft has finished packing the charm, you’ll find a new file in your working directory with the .charm extension:

ls *.charm -l

If you changed the name in charmcraft.yaml or are not on the amd64 platform, the name of the .charm file will be different for you.

Deploy the Django application

A Juju model is needed to deploy the application. Create a new model:

juju add-model django-hello-world

If you are not on a host with the amd64 architecture, you will need to include a constraint to the Juju model to specify your architecture.For example, for the arm64 architecture, use juju set-model-constraints -m django-hello-world arch=arm64. Check the architecture of your system using dpkg --print-architecture.

Now deploy the Django application and PostgreSQL using Juju, and integrate and PostgreSQL with the Django application:

juju deploy ./django-hello-world_ubuntu-22.04-amd64.charm django-hello-world --resource django-app-image=localhost:32000/django-hello-world:0.1
juju deploy postgresql-k8s --trust
juju integrate django-hello-world postgresql-k8s

It will take a few minutes to deploy the Django application. You can monitor the progress using juju status --watch 5s. Once the status of the App has gone to active, you can stop watching using Ctrl+C.

The Django application should now be running. You can see the status of the deployment using juju status which should be similar to the following output:

django-hello-world  dev-controller  microk8s/localhost  3.5.3    unsupported  16:47:01+10:00

App                 Version  Status  Scale  Charm               Channel    Rev  Address         Exposed  Message
django-hello-world           active      1  django-hello-world               3  10.152.183.126  no       
postgresql-k8s      14.11    active      1  postgresql-k8s      14/stable  281  10.152.183.197  no       

Unit                   Workload  Agent  Address      Ports  Message
django-hello-world/0*  active    idle   10.1.157.80         
postgresql-k8s/0*      active    idle   10.1.157.78         Primary

To be able to test the deployment, we need to include the IP address in the allowed hosts configuration. We’ll also enable debug mode for now whilst we are testing. Both can be done using juju config django-hello-world django-allowed-hosts=* django-debug=true.

Setting the Django allowed hosts to * and turning on debug mode should not be done in production where you should set the actual hostname of the application and disable debug mode. We will do this in the tutorial for now and later demonstrate how we can set these to production ready values.

Test the deployment using curl to send a request to the root endpoint. The IP address is the Address listed in the Unit section of the juju status output (e.g., 10.1.157.80 in the sample output above):

curl 10.1.157.80:8000

The Django app should again respond with The install worked successfully! Congratulations!.

Add a root endpoint

The generated Django application does not come with a root endpoint, which is why we had to initially enable debug mode for testing. Let’s add a root endpoint that returns a Hello, world! greeting. We will need to go back out to the root directory for the tutorial and go into the django_hello_world directory using cd ../django_hello_world. Add a new Django app using:

django-admin startapp greeting

Open the greeting/views.py file and replace the content with:

from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world!\n")

Create the greeting/urls.py file with the following contents:

from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
]

Open the django_hello_world/urls.py file and edit the value of urlpatterns to include path('', include("greeting.urls"), for example:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("", include("greeting.urls")),
    path("admin/", admin.site.urls),
]

Since we’re changing the application we should update the version of it. Go back to the root directory of the tutorial using cd .. and change the version in rockcraft.yaml to 0.2. Pack and upload the rock using similar commands as before:

ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:django-hello-world_0.2_amd64.rock docker://localhost:32000/django-hello-world:0.2

Now we can deploy the new version of the Django application using:

cd charm
juju refresh django-hello-world --path=./django-hello-world_ubuntu-22.04-amd64.charm --resource django-app-image=localhost:32000/django-hello-world:0.2

Now that we have a valid root endpoint we can disable debug mode:

juju config django-hello-world django-debug=false

Use juju status --watch 5s again to wait until the app is active again. The IP address will have changed so we need to retrieve it again using juju status. Now we can call the root endpoint using curl 10.1.157.80:8000 and the Django application should respond with Hello, world!.

Enable a configuration

To demonstrate how to provide configuration to the Django application, we will make the greeting configurable. Go back out to the tutorial root directory using cd ... Open the django_hello_world/greeting/views.py file and replace the content with:

import os

from django.http import HttpResponse


def index(request):
    return HttpResponse(f"{os.environ.get('DJANGO_GREETING', 'Hello, world!')}\n")

Increment the version in rockcraft.yaml to 0.3 and run the pack and upload commands for the rock:

ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:django-hello-world_0.3_amd64.rock docker://localhost:32000/django-hello-world:0.3

Change back into the charm directory using cd charm. The django-framework Charmcraft extension supports adding configurations in charmcraft.yaml which will be passed as environment variables to the Django application. Add the following to the end of the charmcraft.yaml file:

config:
  options:
    greeting:
      description: |
        The greeting to be returned by the Django application.
      default: "Hello, world!"
      type: string

Configuration options are automatically capitalised and - are replaced by _. A DJANGO_ prefix will also be added as a namespace for app configurations.

We can now pack and deploy the new version of the Django app:

CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack
juju refresh django-hello-world --path=./django-hello-world_ubuntu-22.04-amd64.charm --resource django-app-image=localhost:32000/django-hello-world:0.3

After we wait for a bit monitoring juju status the application should go back to active again. Sending a request to the root endpoint using curl 10.1.157.81:8000 (after getting the IP address from juju status) should result in the Django application responding with Hello, world! again. We can change the greeting using juju config django-hello-world greeting='Hi!'. After we wait for a moment for the app to be restarted, curl 10.1.157.81:8000 should now respond with Hi!.

Expose the app using ingress

This step of the tutorial only works for hosts with the amd64 architecture. For other architectures, skip this step.

As a final step, let’s expose the application using ingress. Deploy the nginx-ingress-integrator charm and integrate it with the Django app:

juju deploy nginx-ingress-integrator
juju integrate nginx-ingress-integrator django-hello-world

RBAC is enabled in the charm-dev Multipass blueprint. Run juju trust nginx-ingress-integrator --scope cluster if you’re using the charm-dev blueprint.

The hostname of the app needs to be defined so that it is accessible via the ingress. We will also set the default route to be the root endpoint:

juju config nginx-ingress-integrator service-hostname=django-hello-world path-routes=/

Monitor juju status until everything has a status of active. Use curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 to send a request via the ingress. It should still be returning the Hi! greeting.

The -H "Host: django-hello-world" option to the curl command is a way of setting the hostname of the request without setting a DNS record.

We can now also change the Django allowed hosts to django-hello-world which is a production ready value (for production, you will need to setup a DNS record):

juju config django-hello-world django-allowed-hosts=django-hello-world

Running curl 127.0.0.1 -H "Host: django-hello-world" should still get the Django app to respond with Hi!.

Clean up environment

You’ve reached the end of this tutorial. You have created a Django application, deployed it locally, build an OCI image for it and deployed it using Juju. Then we integrated it with PostgreSQL to be production ready, demonstrated how to add a root endpoint and how to configure the application and finally we exposed our application using an ingress.

If you’d like to reset your working environment, you can run the following in the root directory for the tutorial:

cd ..
deactivate
rm -rf charm .venv django_hello_world
# delete all the files created during the tutorial
rm django-hello-world_0.1_amd64.rock \
   django-hello-world_0.2_amd64.rock \
   django-hello-world_0.3_amd64.rock \
   rockcraft.yaml requirements.txt

# Remove the juju model
juju destroy-model django-hello-world --destroy-storage

If you created an instance using Multipass, you can also clean it up. Start by exiting it:

exit

And then you can proceed with its deletion:

multipass delete charm-dev
multipass purge

Next steps

If you are wondering… visit…
“How do I…?” SDK How-to docs
“What is…?” SDK Reference docs
“Why…?”, “So what?” SDK Explanation docs