Write your first Kubernetes charm for a Django app¶
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
Note that a workstation with arm64 architecture can complete the majority of this tutorial.
Familiarity with Linux.
About an hour of free time.
What you’ll do¶
Create a Django application. Use that to create a rock with rockcraft
. Use that to
create a charm with charmcraft
. Use that to test-deploy, configure, etc., your Django
application on a local Kubernetes cloud, microk8s
, with juju
. All of that multiple
times, mimicking a real development process.
Note
rock
An Ubuntu LTS-based OCI compatible container image designed to meet security, stability, and reliability requirements for cloud-native software.
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.
Juju
An orchestration engine for charmed applications.
Set things up¶
Install Multipass.
See more: Multipass | How to install Multipass
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
Note
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:
sudo microk8s enable hostpath-storage
# Required to host the OCI image of the Django application
sudo microk8s enable registry
# Required to expose the Django application
sudo microk8s enable ingress
Juju is required to deploy the Django application. Install Juju and bootstrap a development controller:
sudo snap install juju --channel 3.5/stable
mkdir -p ~/.local/share
juju bootstrap microk8s dev-controller
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!
Note
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 Django 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
.
Note
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', '{ref}`]'))
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
Note
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 Django 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
Note
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
Note
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
Note
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
Note
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 using Juju:
juju deploy ./django-hello-world_ubuntu-22.04-amd64.charm \
django-hello-world \
--resource django-app-image=localhost:32000/django-hello-world:0.1
Deploy PostgreSQL, and integrate and PostgreSQL with the Django application:
juju deploy postgresql-k8s --trust
juju integrate django-hello-world postgresql-k8s
Note
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
.
Note
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
Note
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¶
Note
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
Note
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.
Note
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!
.
Tear things down¶
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
Then, 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
And 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¶
By the end of this tutorial you will have built a charm and evolved it in a number of typical ways. But there is a lot more to explore:
If you are wondering… |
Visit… |
---|---|
“How do I…?” |
|
“What is…?” |