Write your first Kubernetes charm for a Django app¶
Imagine you have a Django application backed up by a database such as PostgreSQL and need to deploy it. In a traditional setup, this can be quite a challenge, but with Charmcraft you’ll find yourself packaging and deploying your Django application in no time.
Introduction¶
In this tutorial we will build a Kubernetes charm for a Django application using Charmcraft, so we can have a Django application up and running with Juju. Let’s get started!
This tutorial should take 90 minutes for you to complete.
Note
If you’re new to the charming world: Django applications are specifically supported with a template to quickly generate a rock (i.e., a special kind of OCI-compliant container image) and a matching template to quickly generate a charm (i.e., a software operator for cloud operations done with the Juju orchestration engine). The result is Django applications that can be easily deployed, configured, scaled, integrated, etc., on any Kubernetes cluster.
What you’ll need¶
A local system, e.g., a laptop, with amd64 or arm64 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¶
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.
Set things up¶
Warning
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.
First, install Multipass.
See also
See more: Multipass | How to install Multipass
Use Multipass to launch an Ubuntu VM with the name charm-dev
from the 24.04 blueprint:
multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 24.04
Once the VM is up, open a shell into it:
multipass shell charm-dev
In order to create the rock, you need to install Rockcraft with classic confinement, which grants it access to the whole file system:
sudo snap install rockcraft --classic
LXD will be required for building the rock. Make sure it is installed and initialized:
lxd --version
lxd init --auto
If LXD
is not installed, install it with sudo snap install lxd
.
In order to create the charm, you’ll need to install Charmcraft:
sudo snap install charmcraft --channel latest/stable --classic
MicroK8s is required to deploy the Django application on Kubernetes.
Let’s install MicroK8s using the 1.31-strict/stable
track:
sudo snap install microk8s --channel 1.31-strict/stable
sudo adduser $USER snap_microk8s
newgrp snap_microk8s
Several MicroK8s add-ons are required for deployment:
# Required for Juju to provide storage volumes
sudo microk8s enable hostpath-storage
# Required to host the OCI image of the application
sudo microk8s enable registry
# Required to expose the application
sudo microk8s enable ingress
Check the status of MicroK8s:
sudo microk8s status --wait-ready
If successful, the terminal will output microk8s is running
along with a list of enabled and disabled add-ons.
Juju is required to deploy the Django application.
Install Juju using the 3.6/stable
track, and bootstrap a
development controller:
sudo snap install juju --channel 3.6/stable
mkdir -p ~/.local/share
juju bootstrap microk8s dev-controller
It could take a few minutes to download the images.
Let’s create a new directory for this tutorial and enter into it:
mkdir django-hello-world
cd django-hello-world
Finally, install python3-venv
and create a virtual environment:
sudo apt update && sudo apt install python3-venv -y
python3 -m venv .venv
source .venv/bin/activate
Create the Django application¶
Let’s start by creating the “Hello, world” Django application that will be used for this tutorial.
Create a requirements.txt
file using touch requirements.txt
.
Then, open the file in a text editor using nano requirements.txt
,
copy the following text into it and then save the file:
Django
psycopg2-binary
Note
The psycopg2-binary
package is needed so the Django application can
connect to PostgreSQL.
Install the packages:
pip install -r requirements.txt
Create a new project using django-admin
:
django-admin startproject django_hello_world
Run the Django application locally¶
We will test the Django application by visiting the app in a web browser.
Change into the /django_hello_world
directory:
cd django_hello_world
Open the settings file of the application located at
django_hello_world/settings.py
. Update the ALLOWED_HOSTS
setting
to allow all traffic:
ALLOWED_HOSTS = ['*']
Save and close the settings.py
file.
Now, run the Django application to verify that it works:
python3 manage.py runserver 0.0.0.0:8000
Note
Specifying 0.0.0.0:8000
allows for traffic outside of the Multipass VM.
Now we need the private IP address of the Multipass VM. Outside of the Multipass VM, run:
multipass info charm-dev | grep IP
With the Multipass IP address, we can visit the Django app in a web
browser. Open a new tab and visit
http://<MULTIPASS_PRIVATE_IP>:8000
, replacing
<MULTIPASS_PRIVATE_IP>
with your VM’s private IP address.
The Django application should respond in the browser with
The install worked successfully! Congratulations!
.
The Django application looks good, so we can stop it for now from the original terminal of the Multipass VM using Ctrl + C.
Pack the Django application into a rock¶
First, we’ll need a rockcraft.yaml
file. Using the
django-framework
profile, Rockcraft will automate the creation of
rockcraft.yaml
and tailor the file for a Django application. Change
back into the /django-hello-world
directory and initialize the rock:
cd ..
rockcraft init --profile django-framework
The rockcraft.yaml
file will automatically be created and set the
name based on your working directory, /django-hello-world
.
Check out the contents of rockcraft.yaml
:
cat rockcraft.yaml
The top of the file should look similar to the following snippet:
name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this Django application
version: '0.1' # just for humans. Semantic versioning is recommended
summary: A summary of your Django application # 79 char long summary
description: |
This is django-hello-world's description. You have a paragraph or two to tell the
most important story about it. Keep it under 100 words though,
we live in tweetspace and your description wants to look good in the
container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:
...
Verify that the name
is django-hello-world
.
Ensure that platforms
includes the architecture of your host. Check
the architecture of your system:
dpkg --print-architecture
If your host uses the ARM architecture, open rockcraft.yaml
in a
text editor and include arm64
under platforms
.
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. From the /django-hello-world
directory, open
django_hello_world/django_hello_world/settings.py
and update the
imports to include json
, os
and secrets
. The top of the
settings.py
file should look similar to the following snippet:
"""
Django settings for django_hello_world project.
Generated by 'django-admin startproject' using Django 5.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
import json
import os
import secrets
We need to change some settings to be production ready.
Near the top of the settings.py
file, change the SECRET_KEY
,
DEBUG
and ALLOWED_HOSTS
variables to:
# 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', '[]'))
We will also use PostgreSQL as the database for our Django app. In
settings.py
, 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'),
}
}
Save and close the settings.py
file.
Now let’s pack the rock:
rockcraft pack
Note
In older versions of Rockcraft, you might need to set
ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true
before the pack command.
Depending on your system and network, this step can take several minutes to finish.
Once Rockcraft has finished packing the Django rock, the
terminal will respond with something similar to
Packed django-hello-world_0.1_amd64.rock
.
Note
If you are not on the amd64
platform, the name of the .rock
file
will be different for you.
The rock needs to be copied to the MicroK8s registry, which stores OCI archives so they can be downloaded and deployed in a Kubernetes cluster. Copy the rock:
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
oci-archive:django-hello-world_0.1_$(dpkg --print-architecture).rock \
docker://localhost:32000/django-hello-world:0.1
See also
Create the charm¶
From the /django-hello-world
directory, create a new directory for
the charm and change inside it:
mkdir charm
cd charm
Using the django-framework
profile, Charmcraft will automate the
creation of the files needed for our charm, including a
charmcraft.yaml
, requirements.txt
and source code for the charm.
The source code contains the logic required to operate the Django
application.
Initialize a charm named django-hello-world
:
charmcraft init --profile django-framework --name django-hello-world
The files will automatically be created in your working directory.
We will need to connect the Django application 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
Now let’s pack the charm:
charmcraft pack
Note
CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true
may be required
in the pack command for older versions of Charmcraft.
Depending on your system and network, this step can take several minutes to finish.
Once Charmcraft has finished packing the charm, the terminal will
respond with something similar to
Packed django-hello-world_ubuntu-22.04-amd64.charm
.
Note
If you 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 handle Kubernetes resources while deploying the Django application. Let’s 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.
Set the Juju model constraints with:
juju set-model-constraints -m django-hello-world \
arch=$(dpkg --print-architecture)
Now let’s use the OCI image we previously uploaded to deploy the Django
application. Deploy using Juju by specifying the OCI image name with the
--resource
option:
juju deploy \
./django-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \
django-hello-world --resource \
django-app-image=localhost:32000/django-hello-world:0.1
Now let’s deploy PostgreSQL:
juju deploy postgresql-k8s --trust
Integrate PostgreSQL with the Django application:
juju integrate django-hello-world postgresql-k8s
It will take a few minutes to deploy the Django application. You can monitor its progress with:
juju status --relations --watch 2s
The --relations
flag will list the currently enabled integrations.
It can take a couple of minutes for the apps to finish the deployment.
During this time, the Django app may enter a blocked
state as it
waits to become integrated with the PostgreSQL database.
Once the status of the App has gone to active
, you can stop watching
using Ctrl + C.
See also
See more: Command ‘juju status’
The Django application should now be running. We can see the status of
the deployment using juju status
which should be similar to the
following output:
user@host:~$
juju status
Model Controller Cloud/Region Version SLA Timestamp
django-hello-world dev-controller microk8s/localhost 3.6.2 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 enable debug mode for now. Set the configuration:
juju config django-hello-world django-debug=true
Note
Turning on debug mode should not be done in production. We will do this in the tutorial for now and later disable debug mode.
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 --channel=latest/stable --trust
juju integrate nginx-ingress-integrator django-hello-world
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
.
Now we will visit the Django app in a web browser. Outside of the
Multipass VM, open your machine’s /etc/hosts
file in a text editor
and add a line like the following:
<MULTIPASS_PRIVATE_IP> django-hello-world
Here, replace <MULTIPASS_PRIVATE_IP>
with the same Multipass VM
private IP address you previously used.
Now you can open a new tab and visit http://django-hello-world. The
Django app should respond in the browser with
The install worked successfully! Congratulations!
.
The development cycle¶
So far, we have worked though the entire cycle, from creating an application to deploying it. But now – as in every real-world case – we will go through the experience of iterating to develop the application, and deploy each iteration.
Add a “Hello, world” app¶
In this iteration, we’ll add a greeting app that returns a Hello, world!
greeting.
The generated Django application does not come with an app, which is why
we had to initially enable debug mode for testing. We will need to go back
out to the /django-hello-world
directory where the rock is and enter
into the /django_hello_world
directory where the Django application
is. Let’s add a new Django app:
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 imports for
django.urls
and the value of urlpatterns
like in the following example:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("", include("greeting.urls")),
path("admin/", admin.site.urls),
]
Update the rock¶
Since we’re changing the application we should update the version of the
rock. Go back to the /django-hello-world
directory where the rock is
and change the version
in rockcraft.yaml
to 0.2
. The top of
the rockcraft.yaml
file should look similar to the following:
name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this Django application
version: '0.2' # just for humans. Semantic versioning is recommended
summary: A summary of your Django application # 79 char long summary
description: |
This is django-hello-world's description. You have a paragraph or two to tell the
most important story about it. Keep it under 100 words though,
we live in tweetspace and your description wants to look good in the
container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:
...
Now let’s pack and upload the rock using similar commands as before:
rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
oci-archive:django-hello-world_0.2_$(dpkg --print-architecture).rock \
docker://localhost:32000/django-hello-world:0.2
Redeploy the charm¶
We’ll redeploy the new version with juju refresh
.
In the /charm
directory, run:
cd charm
juju refresh django-hello-world \
--path=./django-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \
--resource django-app-image=localhost:32000/django-hello-world:0.2
Now that we have the greeting app, we can disable debug mode:
juju config django-hello-world django-debug=false
Use juju status --watch 2s
again to wait until the App is active
again. You may visit http://django-hello-world from a web browser, or
you can use
curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1
inside the Multipass VM. Either way, the Django application should respond
with Hello, world!
.
Provide a configuration¶
To demonstrate how to provide a configuration to the Django application,
we will make the greeting configurable. We will expect this
configuration option to be available in the Django app configuration under the
keyword DJANGO_GREETING
. Go back out to the rock
directory /django-hello-world
using cd ..
. From there, 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")
Update the rock (again)¶
Increment the version
in rockcraft.yaml
to 0.3
such that the
top of the rockcraft.yaml
file looks similar to the following:
name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this Django application
version: '0.3' # just for humans. Semantic versioning is recommended
summary: A summary of your Django application # 79 char long summary
description: |
This is django-hello-world's description. You have a paragraph or two to tell the
most important story about it. Keep it under 100 words though,
we live in tweetspace and your description wants to look good in the
container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:
...
Let’s pack and upload the rock:
rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
oci-archive:django-hello-world_0.3_$(dpkg --print-architecture).rock \
docker://localhost:32000/django-hello-world:0.3
Update the charm¶
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 capitalized and -
are
replaced by _
. A DJANGO_
prefix will also be added as a
namespace for app configurations.
We can now pack a new version of the charm, and then deploy it once more with juju
refresh
:
charmcraft pack
juju refresh django-hello-world \
--path=./django-hello-world_ubuntu-22.04-$(dpkg --print-architecture).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 http://django-hello-world --resolve django-hello-world:80:127.0.0.1
or visiting http://django-hello-world in a web browser should result in the
Django application responding with Hello, world!
again.
Now let’s change the greeting:
juju config django-hello-world greeting='Hi!'
After we wait for a moment for the app to be restarted, using
curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1
or visiting http://django-hello-world should now respond with Hi!
.
Tear things down¶
We’ve reached the end of this tutorial. We went through the entire development process, including:
Creating a Django application
Deploying the application locally
Packaging the application using Rockcraft
Building the application with Ops code using Charmcraft
Deplyoing the application using Juju
Integrating the application with PostgreSQL to be production ready
Exposing the application using an ingress
Adding an initial app and configuring the application
If you’d like to reset your working environment, you can run the
following in the rock directory /django-hello-world
for the tutorial:
charmcraft clean
# Back out to main directory for cleanup
cd ..
rockcraft clean
# exit and delete the virtual environment
deactivate
rm -rf .venv
# delete all the files created during the tutorial
rm -rf charm __pycache__ django_hello_world
rm django-hello-world_0.1_$(dpkg --print-architecture).rock \
django-hello-world_0.2_$(dpkg --print-architecture).rock \
django-hello-world_0.3_$(dpkg --print-architecture).rock \
rockcraft.yaml requirements.txt
# Remove the juju model
juju destroy-model django-hello-world --destroy-storage --no-prompt --force
You can also clean up your Multipass instance. 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…?” |
|
“How do I debug?” |
|
“How do I get in touch?” |
|
“What is…?” |
|
“Why…?”, “So what?” |