Friday, March 3, 2023

Deploying a containerized FastAPI app to Azure Container Apps

Based on my older post about deploying a containerized Flask app to Azure Container Apps, here's a similar process for deploying a FastAPI app instead.

First, some Docker jargon:

  • A Docker image is a multi-layered environment that is exactly the environment your app thrives in, such as a Linux OS with Python 3.11 and FastAPI installed. You can also think of an image as a snapshot or a template.
  • A Docker container is an instance of an image, which could run locally on your machine or in the cloud.
  • A registry is a place to host images. There are cloud hosted registries like DockerHub and Azure Container Registry. You can pull images down from those registries, or push images up to them.

These are the high-level steps:

  1. Build an image of the FastAPI application locally and confirm it works when containerized.
  2. Push the image to the Azure Container Registry.
  3. Create an Azure Container App for that image.

Build image of FastAPI app

I start from a very simple FastAPI app inside a main.py file:

import random

import fastapi

app = fastapi.FastAPI()

@app.get("/generate_name")
async def generate_name(starts_with: str = None):
    names = ["Minnie", "Margaret", "Myrtle", "Noa", "Nadia"]
    if starts_with:
        names = [n for n in names if n.lower().startswith(starts_with)]
    random_name = random.choice(names)
    return {"name": random_name}

I define the dependencies in a requirements.txt file. The FastAPI documentation generally recommends uvicorn as the server, so I stuck with that.

fastapi==0.92.0
uvicorn[standard]==0.20.0

Based on FastAPI's tutorial on Docker deployment, I add this Dockerfile file:

FROM python:3.11

WORKDIR /code

COPY requirements.txt .

RUN pip install --no-cache-dir --upgrade -r requirements.txt

COPY . .

EXPOSE 80

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"]

That file tells Docker to start from a base image which has python 3.11 installed, create a /code directory, install the package requirements, copy the code into the directory, expose port 80, and run the uvicorn server at port 80.

I also add a .dockerignore file to make sure Docker doesn't copy over unneeded files:

.git*
**/*.pyc
.venv/

I build the Docker image using the "Build image" option from the VS Code Docker extension. However, it can also be built from the command line:

docker build --tag fastapi-demo .

Now that the image is built, I can run a container using it:

docker run -d --name fastapi-container -p 80:80 fastapi-demo

The Dockerfile tells FastAPI to use a port of 80, so the run command publishes the container's port 80 as port 80 on the local computer. I visit localhost:80/generate_name to confirm that my API is up and running. 🏃🏽‍♀️

Deploying Option #1: az containerapp up

The Azure CLI has a single command that can take care of all the common steps of container app deployment: az container up.

From the app folder, I run the up command:

az containerapp up \
  -g fastapi-aca-rg \
  -n fastapi-aca-app \
  --registry-server pamelascontainerregistry.azurecr.io \
  --ingress external \
  --target-port 80 \
  --source .

That command does the following:

  1. Creates an Azure resource group named "fastapi-aca-rg". A resource group is basically a folder for all the resources it creates after.
  2. Creates a Container App Environment and Log Analytics workspace inside that group.
  3. Builds the container image using the local Dockerfile.
  4. Pushes the image to my existing registry (pamelascontainerregistry.azurecr.io). I'm reusing my old registry to save costs, but if I wanted the command to create a new one, I would just remove the registry-server argument.
  5. Creates a Container App "fastapi-aca-app" that uses the pushed image and allows external ingress on port 80 (public HTTP access).

When the steps are successful, the public URL is displayed in the output:

Browse to your container app at:
http://fastapi-aca-app.salmontree-4f877506.northcentralusstage.azurecontainerapps.io 

Whenever I update the app code, I run that command again and it repeats the last three steps. Easy peasy! Check the az containerapp up reference to see what additional options are available.

Deploying Option #2: Step-by-step az commands

If you need more customization of the deploying process than is possible with up, it's also possible to do each of those steps yourself using specific Azure CLI commands.

Push image to registry

I follow this tutorial to push an image to the registry, with some customizations.

I create a resource group:

az group create --location eastus --name fastapi-aca-rg

I already had a container registry from my containerized FastAPI app, so I reuse that registry to save costs. If I didn't already have it, I'd run this command to create it:

az acr create --resource-group fastapi-aca-rg \
  --name pamelascontainerregistry --sku Basic

Then I log into the registry so that later commands can push images to it:

az acr login --name pamelascontainerregistry

Now comes the tricky part: pushing an image to that repository. I am working on a Mac with an M1 (ARM 64) chip, but Azure Container Apps (and other cloud-hosted container runners) expect images to be built for an Intel (AMD 64) chip. That means I can't just push the image that I built in the earlier step, I actually have to build specifically for AMD 64 and push that image.

One way to do that is with the docker buildx command, specifying the target architecture and target registry location:

docker buildx build --push --platform linux/amd64 \
    -t pamelascontainerregistry.azurecr.io/fastapi-aca:latest .

However, a much faster way to do it is with the az acr build command, which uploads the code to cloud and builds it there:

az acr build --platform linux/amd64 \
    -t pamelascontainerregistry.azurecr.io/fastapi-aca:latest \
    -r pamelascontainerregistry .

⏱ The `docker buildx` command takes ~ 10 minutes, whereas the `az acr build` command takes only a minute. Nice!

Deploy to Azure Container App

Now that I have an image uploaded to a registry, I can create a container app for that image. I followed this tutorial.

I upgrade the extension and register the necessary providers:

az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights

Then I create an environment for the container app:

az containerapp env create --name fastapi-aca-env \
    --resource-group fastapi-aca-rg --location eastus

Next, I generate credentials to use for the next step:

az acr credential show --name pamelascontainerregistry

Finally, I create the container app, passing in the username and password from the credentials:

az containerapp create --name fastapi-aca-app \
    --resource-group fastapi-aca-rg \
    --image pamelascontainerregistry.azurecr.io/fastapi-aca:latest \
    --environment fastapi-aca-env \
    --registry-server pamelascontainerregistry.azurecr.io \
    --registry-username pamelascontainerregistry \
    --registry-password <PASSWORD HERE> \
    --ingress external \
    --target-port 80

The command returns JSON describing the created resource, which includes the URL of the created app in the "fqdn" property. That URL is also displayed in the Azure portal, in the overview page for the container app. I followed the URL, appended the API route "/generate_name", and verified it responded successfully. 🎉 Woot!

When I make any code updates, I re-build the image and tell the container app to update:

az acr build --platform linux/amd64 \
    -t pamelascontainerregistry.azurecr.io/fastapi-aca:latest \
    -r pamelascontainerregistry .

az containerapp update --name fastapi-aca-app \
  --resource-group fastapi-aca-rg \
  --image pamelascontainerregistry.azurecr.io/fastapi-aca:latest 

🐳 Now I'm off to containerize more apps!

No comments: