แก้ไข

แชร์ผ่าน


Tutorial: Build and deploy a Python web app with Azure Container Apps and PostgreSQL

This article is part of a tutorial series about how to containerize and deploy a Python web app to Azure Container Apps. Container Apps enables you to deploy containerized apps without managing complex infrastructure.

In this tutorial, you:

  • Containerize a Python sample web app (Django or Flask) by building a container image in the cloud.
  • Deploy the container image to Azure Container Apps.
  • Define environment variables that enable the container app to connect to an Azure Database for PostgreSQL - Flexible Server instance, where the sample app stores data.

The following diagram highlights the tasks in this tutorial: building and deploying a container image.

Diagram of services involved in deploying a Python app on Azure Container Apps, with the parts about building an image manually highlighted.

Prerequisites

If you don't have an Azure subscription, create a free account before you begin.

You can run Azure CLI commands in Azure Cloud Shell or on a workstation with the Azure CLI installed.

If you're running locally, follow these steps to sign in and install the necessary modules for this tutorial:

  1. Sign in to Azure and authenticate, if necessary:

    az login
    
  2. Make sure you're running the latest version of the Azure CLI:

    az upgrade
    
  3. Install or upgrade the containerapp and rdbms-connect Azure CLI extensions by using the az extension add command:

    az extension add --name containerapp --upgrade
    az extension add --name rdbms-connect --upgrade
    

    Note

    To list the extensions installed on your system, you can use the az extension list command. For example:

    az extension list --query [].name --output tsv
    

Get the sample app

Fork and clone the sample code to your developer environment:

  1. Go to the GitHub repository of the sample app (Django or Flask) and select Fork.

    Follow the steps to fork the repo to your GitHub account. You can also download the code repo directly to your local machine without forking or a GitHub account. But if you use the download method, you won't be able to set up continuous integration and continuous delivery (CI/CD) in the next tutorial in this series.

  2. Use the git clone command to clone the forked repo into the python-container folder:

    # Django
    git clone https://github.com/$USERNAME/msdocs-python-django-azure-container-apps.git python-container
    
    # Flask
    # git clone https://github.com/$USERNAME/msdocs-python-flask-azure-container-apps.git python-container
    
  3. Change the directory:

    cd python-container
    

Build a container image from web app code

After you follow these steps, you'll have an Azure Container Registry instance that contains a Docker container image built from the sample code.

  1. Create a resource group by using the az group create command:

    az group create \
        --name pythoncontainer-rg \
        --location <location>
    

    Replace <location> with one of the Azure location Name values from the output of the command az account list-locations -o table.

  2. Create a container registry by using the az acr create command:

    az acr create \
        --resource-group pythoncontainer-rg \
        --name <registry-name> \
        --sku Basic \
        --admin-enabled
    

    The name that you use for <registry-name> must be unique within Azure, and it must contain 5 to 50 alphanumeric characters.

  3. Sign in to the registry by using the az acr login command:

    az acr login --name <registry-name>
    

    The command adds "azurecr.io" to the name to create the fully qualified registry name. If the sign-in is successful, the message "Login Succeeded" appears. If you're accessing the registry from a subscription that's different from the one in which you created the registry, use the --suffix switch.

    If sign-in fails, make sure the Docker daemon is running on your system.

  4. Build the image by using the az acr build command:

    az acr build \
        --registry <registry-name> \
        --resource-group pythoncontainer-rg \
        --image pythoncontainer:latest .
    

    These considerations apply:

    • The dot (.) at the end of the command indicates the location of the source code to build. If you aren't running this command in the sample app's root directory, specify the path to the code.

    • If you're running the command in Azure Cloud Shell, use git clone to first pull the repo into the Cloud Shell environment. Then change directory into the root of the project so that the dot (.) is interpreted correctly.

    • If you leave out the -t (same as --image) option, the command queues a local context build without pushing it to the registry. Building without pushing can be useful to check that the image builds.

  5. Confirm that the container image was created by using the az acr repository list command:

    az acr repository list --name <registry-name>
    

Note

The steps in this section create a container registry in the Basic service tier. This tier is cost-optimized, with a feature set and throughput targeted for developer scenarios, and is suitable for the requirements of this tutorial. In production scenarios, you would most likely use either the Standard or Premium service tier. These tiers provide enhanced levels of storage and throughput.

To learn more, see Azure Container Registry service tiers. For information about pricing, see Azure Container Registry pricing.

Create a PostgreSQL Flexible Server instance

The sample app (Django or Flask) stores restaurant review data in a PostgreSQL database. In these steps, you create the server that will contain the database.

  1. Use the az postgres flexible-server create command to create the PostgreSQL server in Azure. It isn't uncommon for this command to run for a few minutes before it finishes.

    az postgres flexible-server create \
       --resource-group pythoncontainer-rg \
       --name <postgres-server-name>  \
       --location <location> \
       --admin-user demoadmin \
       --admin-password <admin-password> \
       --active-directory-auth Enabled \
       --tier burstable \
       --sku-name standard_b1ms \
       --public-access 0.0.0.0 
    

    Use these values:

    • pythoncontainer-rg: The resource group name that this tutorial uses. If you used a different name, change this value.

    • <postgres-server-name>: The PostgreSQL database server name. This name must be unique across all of Azure. The server endpoint is https://<postgres-server-name>.postgres.database.azure.com. Allowed characters are A to Z, 0 to 9, and hyphen (-).

    • <location>: Use the same location that you used for the web app. <location> is one of the Azure location Name values from the output of the command az account list-locations -o table.

    • <admin-username>: The username for the administrator account. It can't be azure_superuser, admin, administrator, root, guest, or public. Use demoadmin for this tutorial.

    • <admin-password>: The password of the administrator user. It must contain 8 to 128 characters from three of the following categories: English uppercase letters, English lowercase letters, numbers, and non-alphanumeric characters.

      Important

      When you're creating usernames or passwords, do not use the dollar sign ($) character. Later, when you create environment variables with these values, that character has a special meaning within the Linux container that you use to run Python apps.

    • --active-directory-auth: This value specifies whether Microsoft Entra authentication is enabled on the PostgreSQL server. Set it to Enabled.

    • --sku-name: The name of the pricing tier and compute configuration; for example, Standard_B1ms. For more information, see Azure Database for PostgreSQL pricing. To list available tiers, use az postgres flexible-server list-skus --location <location>.

    • --public-access: Use 0.0.0.0. It allows public access to the server from any Azure service, such as Container Apps.

    Note

    If you plan to work with the PostgreSQL server from your local workstation by using tools, you need to add a firewall rule for your workstation's IP address by using the az postgres flexible-server firewall-rule create command.

  2. Use the az ad signed-in-user show command to get the object ID of your user account. You use this ID in the next command.

    az ad signed-in-user show --query id --output tsv
    
  3. Use the az postgres flexible-server ad-admin create command to add your user account as a Microsoft Entra administrator on the PostgreSQL server:

    az postgres flexible-server ad-admin create \
       --resource-group pythoncontainer-rg \
       --server-name <postgres-server-name>  \
       --display-name <your-email-address> \
       --object-id <your-account-object-id>
    

    For your account object ID, use the value that you got in the previous step.

Note

The steps in this section create a PostgreSQL server with a single vCore and limited memory in the Burstable pricing tier. The Burstable tier is a lower-cost option for workloads that don't need the full CPU continuously, and is suitable for the requirements of this tutorial. For production workloads, you might upgrade to either the General Purpose or Memory Optimized pricing tier. These tiers provide higher performance but increase costs.

To learn more, see Compute options in Azure Database for PostgreSQL - Flexible Server. For information about pricing, see Azure Database for PostgreSQL pricing.

Create a database on the server

At this point, you have a PostgreSQL server. In this section, you create a database on the server.

Use the az postgres flexible-server db create command to create a database named restaurants_reviews:

az postgres flexible-server db create \
   --resource-group pythoncontainer-rg \
   --server-name <postgres-server-name> \
   --database-name restaurants_reviews

Use these values:

  • pythoncontainer-rg: The resource group name that this tutorial uses. If you used a different name, change this value.
  • <postgres-server-name>: The name of the PostgreSQL server.

You could also use the az postgres flexible-server connect command to connect to the database and then work with psql commands. When you're working with psql, it's often easier to use Azure Cloud Shell because the shell includes all the dependencies for you.

You can also connect to the Azure Database for PostgreSQL flexible server and create a database by using psql or an IDE that supports PostgreSQL, like Azure Data Studio. For steps using psql, see Configure the managed identity on the PostgreSQL database later in this article.

Create a user-assigned managed identity

Create a user-assigned managed identity to use as the identity for the container app when it's running in Azure.

Note

To create a user-assigned managed identity, your account needs the Managed Identity Contributor role assignment.

Use the az identity create command to create a user-assigned managed identity:

az identity create --name my-ua-managed-id --resource-group pythoncontainer-rg

Configure the managed identity on the PostgreSQL database

Configure the managed identity as a role on the PostgreSQL server and then grant it necessary permissions for the restaurants_reviews database. Whether you're using the Azure CLI or psql, you must connect to the Azure PostgreSQL server with a user who's configured as a Microsoft Entra admin on your server instance. Only Microsoft Entra accounts configured as a PostgreSQL admin can configure managed identities and other Microsoft admin roles on your server.

  1. Get an access token for your Azure account by using the az account get-access-token command. You use the access token in the next steps.

    az account get-access-token --resource-type oss-rdbms --output tsv --query accessToken
    

    The returned token is long. Set its value in an environment variable to use in the commands in the next step:

    MY_ACCESS_TOKEN=<your-access-token>
    
  2. Add the user-assigned managed identity as database role on your PostgreSQL server by using the az postgres flexible-server execute command:

    az postgres flexible-server execute \
        --name <postgres-server-name> \
        --database-name postgres \
        --querytext "select * from pgaadauth_create_principal('"my-ua-managed-id"', false, false);select * from pgaadauth_list_principals(false);" \
        --admin-user <your-Azure-account-email> \
        --admin-password $MY_ACCESS_TOKEN
    

    Use these values:

    • If you used a different name for your managed identity, replace my-ua-managed-id in the pgaadauth_create_principal command with the name of your managed identity.

    • For the --admin-user value, use the email address for your Azure account.

    • For the --admin-password value, use the access token from the output of the previous command, without quotation marks.

    • Make sure the database name is postgres.

    Note

    If you're running the az postgres flexible-server execute command on your local workstation, make sure that you added a firewall rule for your workstation's IP address. You can add a rule by using the az postgres flexible-server firewall-rule create command. The same requirement also exists for the command in the next step.

  3. Grant the user-assigned managed identity the necessary permissions on the restaurants_reviews database by using the following az postgres flexible-server execute command:

    az postgres flexible-server execute \
        --name <postgres-server-name> \
        --database-name restaurants_reviews \
        --querytext "GRANT CONNECT ON DATABASE restaurants_reviews TO \"my-ua-managed-id\";GRANT USAGE ON SCHEMA public TO \"my-ua-managed-id\";GRANT CREATE ON SCHEMA public TO \"my-ua-managed-id\";GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"my-ua-managed-id\";ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO \"my-ua-managed-id\";" \
        --admin-user <your-Azure-account-email> \
        --admin-password $MY_ACCESS_TOKEN
    

    Use these values:

    • If you used a different name for your managed identity, replace all instances of my-ua-managed-id in the command with the name of your managed identity. There are five instances in the query string.

    • For the --admin-user value, use the email address of your Azure account.

    • For the --admin-password value, use the access token from the previous output, without quotation marks.

    • Make sure the database name is restaurants_reviews.

    This Azure CLI command connects to the restaurants_reviews database on the server and issues the following SQL commands:

    GRANT CONNECT ON DATABASE restaurants_reviews TO "my-ua-managed-id";
    GRANT USAGE ON SCHEMA public TO "my-ua-managed-id";
    GRANT CREATE ON SCHEMA public TO "my-ua-managed-id";
    GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "my-ua-managed-id";
    ALTER DEFAULT PRIVILEGES IN SCHEMA public
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "my-ua-managed-id";
    

Deploy the web app to Container Apps

Container apps are deployed to Azure Container Apps environments, which act as a secure boundary. In the following steps, you create the environment and a container inside the environment. You then configure the container so that the website is visible externally.

These steps require the Azure Container Apps extension, containerapp.

  1. Create a Container Apps environment by using the az containerapp env create command:

    az containerapp env create \
    --name python-container-env \
    --resource-group pythoncontainer-rg \
    --location <location>
    

    <location> is one of the Azure location Name values from the output of the command az account list-locations -o table.

  2. Get the sign-in credentials for the Azure Container Registry instance by using the az acr credential show command:

    az acr credential show -n <registry-name>
    

    You use the username and one of the passwords returned from the command's output when you create the container app in step 5.

  3. Use the az identity show command to get the client ID and resource ID of the user-assigned managed identity:

    az identity show --name my-ua-managed-id --resource-group pythoncontainer-rg --query "[clientId, id]" --output tsv
    

    You use the value of the client ID (GUID) and the resource ID from the command's output when you create the container app in step 5. The resource ID has the following form: /subscriptions/<subscription-id>/resourcegroups/pythoncontainer-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-ua-managed-id.

  4. Run the following command to generate a secret key value:

    python -c 'import secrets; print(secrets.token_hex())'
    

    You use the secret key value to set an environment variable when you create the container app in step 5.

    Note

    The command that this step shows is for a Bash shell. Depending on your environment, you might need to invoke Python by using python3. On Windows, you need to enclose the command in the -c parameter in double quotation marks rather than single quotation marks. You also might need to invoke Python by using py or py -3, depending on your environment.

  5. Create a container app in the environment by using the az containerapp create command:

    az containerapp create \
    --name python-container-app \
    --resource-group pythoncontainer-rg \
    --image <registry-name>.azurecr.io/pythoncontainer:latest \
    --environment python-container-env \
    --ingress external \
    --target-port <5000 for Flask or 8000 for Django> \
    --registry-server <registry-name>.azurecr.io \
    --registry-username <registry-username> \
    --registry-password <registry-password> \
    --user-assigned <managed-identity-resource-id> \
    --query properties.configuration.ingress.fqdn \
    --env-vars DBHOST="<postgres-server-name>" \
    DBNAME="restaurants_reviews" \
    DBUSER="my-ua-managed-id" \
    RUNNING_IN_PRODUCTION="1" \
    AZURE_CLIENT_ID="<managed-identity-client-id>" \
    AZURE_SECRET_KEY="<your-secret-key>"
    

    Be sure to replace all of the values in angle brackets with values that you're using in this tutorial. Be aware that the name of your container app must be unique across Azure.

    The value of the --env-vars parameter is a string composed of space-separated values in the key="value" format with the following values:

    • DBHOST="\<postgres-server-name>"
    • DBNAME="restaurants_reviews"
    • DBUSER="my-ua-managed-id"
    • RUNNING_IN_PRODUCTION="1"
    • AZURE_CLIENT_ID="\<managed-identity-client-id>"
    • AZURE_SECRET_KEY="\<your-secret-key>"

    The value for DBUSER is the name of your user-assigned managed identity.

    The value for AZURE_CLIENT_ID is the client ID of your user-assigned managed identity. You got this value in a previous step.

    The value for AZURE_SECRET_KEY is the secret key value that you generated in a previous step.

  6. For Django only, migrate and create a database schema. (In the Flask sample app, it's done automatically, and you can skip this step.)

    Connect by using the az containerapp exec command:

        az containerapp exec \
            --name python-container-app \
            --resource-group pythoncontainer-rg
    

    Then, at the shell command prompt, enter python manage.py migrate.

    You don't need to migrate for revisions of the container.

  7. Test the website.

    The az containerapp create command that you entered previously outputs an application URL that you can use to browse to the app. The URL ends in azurecontainerapps.io. Go to the URL in a browser. Alternatively, you can use the az containerapp browse command.

Here's an example of the sample website after the addition of a restaurant and two reviews.

Screenshot of the sample website built in this tutorial.

Troubleshoot deployment

You forgot the application URL to access the website

In the Azure portal:

  • Go to the Overview page of the container app and look for Application Url.

In VS Code:

  1. Go to the Azure view (Ctrl+Shift+A) and expand the subscription that you're working in.
  2. Expand the Container Apps node, expand the managed environment, right-click python-container-app, and then select Browse. VS Code opens the browser with the application URL.

In the Azure CLI:

  • Use the command az containerapp show -g pythoncontainer-rg -n python-container-app --query properties.configuration.ingress.fqdn.

In VS Code, the Build Image in Azure task returns an error

If you see the message "Error: failed to download context. Please check if the URL is incorrect" in the VS Code Output window, refresh the registry in the Docker extension. To refresh, select the Docker extension, go to the Registries section, find the registry, and select it.

If you run the Build Image in Azure task again, check whether your registry from a previous run exists. If so, use it.

In the Azure portal, an access error appears during the creation of a container app

An access error that contains "Cannot access ACR '<name>.azurecr.io'" occurs when admin credentials on an Azure Container Registry instance are disabled.

To check admin status in the portal, go to your Azure Container Registry instance, select the Access keys resource, and ensure that Admin user is enabled.

Your container image doesn't appear in the Azure Container Registry instance

  • Check the output of the Azure CLI command or VS Code output and look for messages to confirm success.
  • Check that the name of the registry was specified correctly in your build command with the Azure CLI or in the VS Code task prompts.
  • Make sure your credentials aren't expired. For example, in VS Code, find the target registry in the Docker extension and refresh. In the Azure CLI, run az login.

Website returns "Bad Request (400)"

If you get a "Bad Request (400)" error, check the PostgreSQL environment variables passed in to the container. The 400 error often indicates that the Python code can't connect to the PostgreSQL instance.

The sample code used in this tutorial checks for the existence of the container environment variable RUNNING_IN_PRODUCTION, which can be set to any value (like 1).

Website returns "Not Found (404)"

  • Check the Application Url value on the Overview page for the container. If the application URL contains the word "internal," ingress isn't set correctly.
  • Check the ingress of the container. For example, in the Azure portal, go to the Ingress resource of the container. Make sure that HTTP Ingress is enabled and Accepting traffic from anywhere is selected.

Website doesn't start, you get "stream timeout," or nothing is returned

  • Check the logs:
    • In the Azure portal, go to the container app's revision management resource and check Provision Status for the container:
      • If the status is Provisioning, wait until provisioning finishes.
      • If the status is Failed, select the revision and view the console logs. Choose the order of the columns to show Time Generated, Stream_s, and Log_s. Sort the logs by most recent and look for Python stderr and stdout messages in the Stream_s column. Python print output is stdout messages.
    • In the Azure CLI, use the az containerapp logs show command.
  • If you're using the Django framework, check to see if the restaurants_reviews tables exist in the database. If not, use a console to access the container and run python manage.py migrate.

Next step