Write your first Kubernetes charm for a FastAPI app¶
Imagine you have a FastAPI app 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 FastAPI app in no time.
In this tutorial we will build a Kubernetes charm for a FastAPI app using Charmcraft, so we can have a FastAPI app 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, FastAPI apps are specifically supported with a template to quickly generate a rock and a matching template to generate a charm. A rock is a special kind of OCI-compliant container image, while a charm is a software operator for cloud operations that use the Juju orchestration engine. The result is a FastAPI app 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 FastAPI app.
Use that to create a rock with Rockcraft.
Use that to create a charm with Charmcraft.
Use that to test, deploy, configure, etc., your FastAPI app on a local Kubernetes cloud with Juju.
Repeat the process, 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
.
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/edge --classic
MicroK8s is required to deploy the FastAPI 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 FastAPI 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 directory for this tutorial and enter into it:
mkdir fastapi-hello-world
cd fastapi-hello-world
Finally, install python-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 FastAPI app¶
Start by creating the “Hello, world” FastAPI app 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:
fastapi[standard]
psycopg2-binary
Note
The psycopg2-binary
package is needed so the FastAPI app can
connect to PostgreSQL.
Install the packages:
pip install -r requirements.txt
In the same directory, create a file called app.py
.
Then copy and save the following code into the file:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
Run the FastAPI app locally¶
Now that we have a virtual environment with all the dependencies, let’s run the FastAPI app to verify that it works:
fastapi dev app.py --port 8080
Test the FastAPI app by using curl
to send a request to the root
endpoint. You will need a new terminal for this; use
multipass shell charm-dev
to open a new terminal in Multipass:
curl localhost:8080
The FastAPI app should respond with {"message":"Hello World"}
.
The FastAPI app looks good, so we can stop for now from the original terminal using Ctrl + C.
Pack the FastAPI app into a rock¶
First, we’ll need a rockcraft.yaml
file. Using the
fastapi-framework
profile, Rockcraft will automate the creation of
rockcraft.yaml
and tailor the file for a FastAPI app.
From the ~/fastapi-hello-world
directory, initialize the rock:
rockcraft init --profile fastapi-framework
The rockcraft.yaml
file will be automatically created, with the name being
set based on your working directory.
Check out the contents of rockcraft.yaml
:
cat rockcraft.yaml
The top of the file should look similar to the following snippet:
name: fastapi-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this FastAPI app
version: '0.1' # just for humans. Semantic versioning is recommended
summary: A summary of your FastAPI app # 79 char long summary
description: |
This is fastapi project'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 fastapi-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
in platforms
.
Now let’s pack the rock:
ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack
Note
ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS
is required while the FastAPI
extension is experimental.
Depending on your system and network, this step can take several minutes to finish.
Once Rockcraft has finished packing the FastAPI rock,
the terminal will respond with something similar to
Packed fastapi-hello-world_0.1_amd64.rock
.
Note
If you aren’t on AMD64 architecture, 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 the Kubernetes cluster. Copy the rock:
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
oci-archive:fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \
docker://localhost:32000/fastapi-hello-world:0.1
See also
Create the charm¶
From the ~/fastapi-hello-world
directory, let’s create a new directory
for the charm and change inside it:
mkdir charm
cd charm
Using the fastapi-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 FastAPI
app.
Initialize a charm named fastapi-hello-world
:
charmcraft init --profile fastapi-framework --name fastapi-hello-world
The files will automatically be created in your working directory.
Check out the contents of charmcraft.yaml
:
cat charmcraft.yaml
The top of the file should look similar to the following snippet:
# This file configures Charmcraft.
# See https://juju.is/docs/sdk/charmcraft-config for guidance.
name: fastapi-hello-world
type: charm
base: [email protected]
# the platforms this charm should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:
# (Required)
summary: A very short one-line summary of the FastAPI app.
...
Verify that the name
is fastapi-hello-world
. Ensure that platforms
includes the architecture of your host. If your host uses the ARM architecture,
open charmcraft.yaml
in a text editor and include arm64
in platforms
.
Let’s pack the charm:
CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack
Note
CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS
is required while the FastAPI
extension is experimental.
Depending on your system and network, this step may take several minutes to finish.
Once Charmcraft has finished packing the charm, the terminal will
respond with something similar to
Packed fastapi-hello-world_ubuntu-24.04-amd64.charm
.
Note
If you aren’t on AMD64 architecture, the name of the .charm
file will be different for you.
Deploy the FastAPI app¶
A Juju model is needed to handle Kubernetes resources while deploying the FastAPI app. Let’s create a new model:
juju add-model fastapi-hello-world
If you aren’t 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 fastapi-hello-world arch=$(dpkg --print-architecture)
Now let’s use the OCI image we previously uploaded to deploy the FastAPI
app. Deploy using Juju by specifying the OCI image name with the
--resource
option:
juju deploy \
./fastapi-hello-world_$(dpkg --print-architecture).charm \
fastapi-hello-world --resource \
app-image=localhost:32000/fastapi-hello-world:0.1
It will take a few minutes to deploy the FastAPI app. You can monitor its progress with:
juju status --watch 2s
It can take a couple of minutes for the app to finish the deployment.
Once the status of the App has gone to active
, you can stop watching
using Ctrl + C.
See also
See more: Juju | juju status
The FastAPI app should now be running. We can monitor 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
fastapi-hello-world dev-controller microk8s/localhost 3.6.2 unsupported 13:45:18+10:00
App Version Status Scale Charm Channel Rev Address Exposed Message
fastapi-hello-world active 1 fastapi-hello-world 0 10.152.183.53 no
Unit Workload Agent Address Ports Message
fastapi-hello-world/0* active idle 10.1.157.75
Let’s expose the app using ingress. Deploy the
nginx-ingress-integrator
charm and integrate it with the FastAPI app:
juju deploy nginx-ingress-integrator --channel=latest/stable --trust
juju integrate nginx-ingress-integrator fastapi-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=fastapi-hello-world path-routes=/
Monitor juju status
until everything has a status of active
.
Test the deployment using
curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1
to send a request via the ingress. It should return the
{"message":"Hello World"}
greeting.
Note
The --resolve fastapi-hello-world:80:127.0.0.1
option to the curl
command is a way of resolving the hostname of the request without
setting a DNS record.
Configure the FastAPI app¶
To demonstrate how to provide a configuration to the FastAPI app,
we will make the greeting configurable. We will expect this
configuration option to be available in the FastAPI app configuration under the
keyword APP_GREETING
. Change back to the ~/fastapi-hello-world
directory
using cd ..
and copy the following code into app.py
:
import os
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": os.getenv("APP_GREETING", "Hello World")}
Increment the version
in rockcraft.yaml
to 0.2
such that the
top of the rockcraft.yaml
file looks similar to the following:
name: fastapi-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this FastAPI app
version: '0.2' # just for humans. Semantic versioning is recommended
summary: A summary of your FastAPI app # 79 char long summary
description: |
This is fastapi project'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_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
oci-archive:fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \
docker://localhost:32000/fastapi-hello-world:0.2
Change back into the charm directory using cd charm
.
The fastapi-framework
Charmcraft extension supports adding
configurations to charmcraft.yaml
which will be passed as
environment variables to the FastAPI app. Add the
following to the end of the charmcraft.yaml
file:
# configuration snippet for FastAPI application
config:
options:
greeting:
description: |
The greeting to be returned by the FastAPI application.
default: "Hello, world!"
type: string
Note
Configuration options are automatically capitalized and -
are replaced
by _
. An APP_
prefix will also be added as a namespace
for app configurations.
We can now pack and deploy the new version of the FastAPI app:
CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack
juju refresh fastapi-hello-world \
--path=./fastapi-hello-world_$(dpkg --print-architecture).charm \
--resource app-image=localhost:32000/fastapi-hello-world:0.2
After we wait for a bit monitoring juju status
the app
should go back to active
again. Verify that the
new configuration has been added using
juju config fastapi-hello-world | grep -A 6 greeting:
which should show
the configuration option.
Using curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1
shows that the response is still {"message":"Hello, world!"}
as expected.
Now let’s change the greeting:
juju config fastapi-hello-world greeting='Hi!'
After we wait for a moment for the app to be restarted, using
curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1
should now return the updated {"message":"Hi!"}
greeting.
Integrate with a database¶
Now let’s keep track of how many visitors your app has received. This will require integration with a database to keep the visitor count. This will require a few changes:
We will need to create a database migration that creates the
visitors
table.We will need to keep track of how many times the root endpoint has been called in the database.
We will need to add a new endpoint to retrieve the number of visitors from the database.
Let’s start with the database migration to create the required tables.
The charm created by the fastapi-framework
extension will execute the
migrate.py
script if it exists. This script should ensure that the
database is initialized and ready to be used by the app. We will
create a migrate.py
file containing this logic.
Go back out to the ~/fastapi-hello-world
directory using cd ..
,
create the migrate.py
file, open the file using a text editor
and paste the following code into it:
# Adds database to FastAPI application
import os
import psycopg2
DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"]
def migrate():
with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS visitors (
timestamp TIMESTAMP NOT NULL,
user_agent TEXT NOT NULL
);
""")
conn.commit()
if __name__ == "__main__":
migrate()
Note
The charm will pass the Database connection string in the
POSTGRESQL_DB_CONNECT_STRING
environment variable once postgres has
been integrated with the charm.
Increment the version
in rockcraft.yaml
to 0.3
such that the
top of the rockcraft.yaml
file looks similar to the following:
name: fastapi-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this FastAPI app
version: '0.3' # just for humans. Semantic versioning is recommended
summary: A summary of your FastAPI app # 79 char long summary
description: |
This is fastapi project'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:
The app code also needs to be updated to keep track of the number of visitors
and to include a new endpoint to retrieve the number of visitors to the
app. Open app.py
in a text editor and replace its contents with the
following code:
app.py
# FastAPI application that keeps track of visitors using a database
import datetime
import os
from typing import Annotated
from fastapi import FastAPI, Header
import psycopg2
app = FastAPI()
DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"]
@app.get("/")
async def root(user_agent: Annotated[str | None, Header()] = None):
with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur:
timestamp = datetime.datetime.now()
cur.execute(
"INSERT INTO visitors (timestamp, user_agent) VALUES (%s, %s)",
(timestamp, user_agent)
)
conn.commit()
return {"message": os.getenv("APP_GREETING", "Hello World")}
@app.get("/visitors")
async def visitors():
with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM visitors")
total_visitors = cur.fetchone()[0]
return {"count": total_visitors}
Let’s pack and upload the rock:
ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
oci-archive:fastapi-hello-world_0.3_$(dpkg --print-architecture).rock \
docker://localhost:32000/fastapi-hello-world:0.3
Change back into the charm directory using cd charm
.
The FastAPI app now requires a database which needs to be declared in the
charmcraft.yaml
file. Open charmcraft.yaml
in a text editor and
add the following section to the end:
# requires snippet for FastAPI application with a database
requires:
postgresql:
interface: postgresql_client
optional: false
We can now pack and deploy the new version of the FastAPI app:
CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack
juju refresh fastapi-hello-world \
--path=./fastapi-hello-world_$(dpkg --print-architecture).charm \
--resource app-image=localhost:32000/fastapi-hello-world:0.3
Now let’s deploy PostgreSQL and integrate it with the FastAPI app:
juju deploy postgresql-k8s --trust
juju integrate fastapi-hello-world postgresql-k8s
Wait for juju status
to show that the App is active
again. Running
curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1
should still return the {"message":"Hi!"}
greeting.
To check the local visitors, use
curl http://fastapi-hello-world/visitors
--resolve fastapi-hello-world:80:127.0.0.1
, which should return
{"count":1}
after the previous request to the root endpoint. This should
be incremented each time the root endpoint is requested. If we repeat
this process, the output should be as follows:
user@host:~$
curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1
{"message":"Hi!"}
user@host:~$
curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1
{"count":2}
Tear things down¶
We’ve reached the end of this tutorial. We went through the entire development process, including:
Creating a FastAPI app
Deploying the app locally
Packaging the app using Rockcraft
Building the app with Ops code using Charmcraft
Deplyoing the app using Juju
Exposing the app using an ingress
Configuring the app
Integrating the app with a database
If you’d like to reset your working environment, you can run the following
in the rock directory ~/fastapi-hello-world
for the tutorial:
CHARMCRAFTCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft clean
# Back out to main directory for cleanup
cd ..
ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft clean
# exit and delete the virtual environment
deactivate
rm -rf charm .venv __pycache__
# delete all the files created during the tutorial
rm fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \
fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \
fastapi-hello-world_0.3_$(dpkg --print-architecture).rock \
rockcraft.yaml app.py requirements.txt migrate.py
# Remove the juju model
juju destroy-model fastapi-hello-world --destroy-storage --no-prompt --force
You can also clean up your Multipass instance. Start by exiting it:
exit
You can then 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?” |