It's what happens when there's not enough time during the day...

Deploying a Python App to Azure

Using the Azure Developer CLI

9 min read Jun 10, 2023 Azure Deployment FastAPI

Deploying a Python App to Azure

Using the Azure Developer CLI

At PyCon US 2023, I had the opportunity to present a tutorial on how to create a PyHAT (Python, htmx, TailwindCSS) web application. The result was a "Music Binder" app that contains info on different Musical Artists. (You can watch the tutorial on the PyCon US YouTube channel.)

So that got me thinking. If this were my first time creating a web app, what's next?


Ordinarily, the next step after actually building a web application is sharing it. But the process of deployment can be pretty daunting if it's your first time.

I've deployed apps on different services before. Currently, This blog is hosted on Digital Ocean.

However, I'm always curious to see how the deployment experience differs from one service to another. So I decided to give it a shot on a service I've never used before.


Azure is Microsoft's cloud computing platform, which allows you to manage and develop applications and services on the proverbial cloud (aka, global data centers).

While I have some familiarity with AWS and Google Cloud, I haven't had much (any) experience with Azure in the past. Because of that, I can't really comment on what it's like hosting a long-term application on their service. However, I did give their deployment journey a spin and lived to tell the tale.

First Things First

Before being able to host on any provider, you are likely going to have to sign up to access the provider's dashboard/features. What I often find missing (or glossed over) in deployment documentation is the amount of "buy in" to access the services.

Some platforms allow you to create an account and sign up for a "free tier" of service, while others offer a trial period, with a subscription model set up at different pricing tiers which kicks in after the trial is over. (There are just too many pricing models to count.)

This usually means that you have to make decisions about what sort of service you're signing up for, which can include decisions about CPU usage, RAM allotment, database provisions, and a wealth of other infrastructure-related services.

That can become rather dizzying very quickly, especially when just starting out, or working on a hobby-type project.

On the Azure side, signing up is not terribly convoluted. You are asked for basic information, and are given a $200 credit that will last for one month (first time users). You'll most likely want to sign up for a "pay as you go" service, which will not incur any immediate cost.

You are required to provide a credit card number upon sign up, even though your card is not charged. I believe the requirement is there to prevent spam accounts from being created easily.

Back to the Terminal

Once you've taken care of creating an account, the next step will be to download the Azure Developer CLI (azd for short).

On Windows, you can download it from the Windows Package Manager (winget) using Powershell:

winget install microsoft.azd

With MacOS, you're probably best off using Homebrew:

brew tap azure/azd && brew install azd

On Apple Silicon Macs (M1 and M2) azd requires Rosetta 2. If Rosetta 2 is not already installed run softwareupdate --install-rosetta from the terminal.

Additional installation instructions can be found on Azure's docs.


One of the ways that Azure facilitates the process of deployment is by utilizing an Infrastructure as Code (IaC) approach.

In short, this means that you include config/definition files within your app structure. These files contain the instructions that will (theoretically) make it easy to get your app on the Azure cloud with an easy and repeatable process.

You can use Terraform (general, cross-platform IaC language) or Bicep (IaC language developed specifically for an Azure environment) to define your infrastructure.

I'll be frank. These files are scary looking, and it can feel daunting to look through them and figure out exactly what it is that they do.

Thankfully, you don't necessarily have to understand them all that much to get your project on the cloud.


In spite of the "complicated" nature of Bicep files, the overall process of deploying through azd can be summarized like so (presuming you have an Azure account set up).

That's it. Pretty simple, right? That is, if you can get around the infrastructure files.

If you're coming in cold, that second task can feel off-putting. What if you don't want to spend time learning a new DevOps language?


In order to make the process more approachable, the Azure team provides a list of templates designed to get you started much quicker.

The Awesome AZD Templates repo contains a WIDE array of templates. These templates include sample projects built with a variety of different tech stacks, including the necessary infrastructure files to make Azure deployment easy as pie.

If you have not yet started building your application, you can use azd to start you off with one of these templates by typing azd template list in your terminal.

This will give you a project structure with the necessary infrastructure files ready to go. You can tweak these if you want, but they already contain reasonable defaults to get your project to the cloud.

But what if you have already built your project and want to use azd to get it on the Azure cloud?

Music Binder AZD

As mentioned at the top, I built a Music Binder app for the tutorial I taught at PyCon. You can see that project over on the Music Binder github repo.

The file structure looks a little something like this:

┣ .devcontainer/
┃ ┣ devcontainer.json
┃ ┗ Dockerfile
┣ .venv/
┣ app/
┃ ┣ static/
┃ ┣ templates/
┃ ┣
┃ ┣
┃ ┣
┃ ┣
┃ ┣
┃ ┗
┣ data/
┃ ┗ data.json
┣ tests/
┣ .gitignore
┣ pyproject.toml
┣ requirements.txt
┗ tailwind.config.js

I went ahead and forked my own repo to make an azd compatible version.

Once I had it set up locally, I navigated to the project root and ran the following CLI command to generate the necessary files.

azd init

This command created a series of files and directories within my project. According to the Azure documentation, this is what is generated:

┣ .azdo/                   [ For Azure DevOps ]
┃ ┗ azure-dev.yaml         [ Azure Pipelines workflow to deploy to Azure using azd ]
┣ .azure/                  [ For storing Azure configurations]
┃ ┗ <your environment>/    [ For storing all environment-related configurations]
┃   ┣ .env                 [ Contains environment variables ]
┃   ┗ config.json          [ Contains environment configuration ]
┣ .devcontainer/           [ For DevContainer ]
┃ ┣ devcontainer.json      [ For setting up the containerized development environment ]
┃ ┗ Dockerfile             [ For building the container image ]
┣ .github/                 [ For GitHub workflow ]
┃ ┗ azure-dev.yaml         [ GitHub Actions workflow to deploy to Azure using azd ]
┣ infra/                   [ For creating and configuring Azure resources ]
┃ ┣ abbreviations.json     [ Recommended abbreviations for Azure resources ]
┃ ┣ main.bicep             [ Main infrastructure file ]
┃ ┗ main.parameters.json   [ Parameters file ]
┃ azure.yaml               [ Describes the app and type of Azure resources]
┗ your_app/                [ Your own app code ]


That's quite a lot!

I'm not gonna sugarcoat it. That felt intimidating.

But reading through the docs, I discovered the following.

The .azdo and .github directories are optional (Azure Pipelines and Github Actions respectively).

The .azure directory stores specific Azure configurations, which I had no need for, hence I removed it.

I already had a .devcontainer directory created for the tutorial (for Codespaces compatibility), but this is also optional.

So at the end of the day, the only files you need to worry about are the azure.yaml file and those in the infra directory.

After all that, this certainly feels more approachable:

┣ .devcontainer/           [ DevContainers! (optional) ]
┃ ┣ devcontainer.json
┃ ┗ Dockerfile
┣ infra/                   [ Bicep files ]
┃ ┣ abbreviations.json     [ Optional, I believe ]
┃ ┣ main.bicep             [ Main infrastructure file ]
┃ ┗ main.parameters.json   [ Parameters file ]
┃  azure.yaml              [ App description]
┗  your_app/               [ Your own app code ]


What did I know about Bicep before starting on this? Well, I knew just a bit from a very, very high level, but next to nothing about how to write it or how it works internally.

Because of this, I ended up searching for a template utilizing some of the resources I wanted for my app.

I needed to define a "service plan" (pay as you go), what kind of service I need (a Python web app), and a basic logging resource (analytics).

These are all defined in the main.bicep file. The module sections define the provisioning for each of those elements, but I copied those from other projects.

That is about all I had to change, with a few tweaks as I browsed through different templates.

I didn't have to touch the main.parameters.json file or the abbreviations.json files.

Lastly, I modified the azure.yaml file with some minimal information. Here you can specify your app's source code location (project) and the Azure service you want to use (host).

This is the entirety of the file:

# azure.yaml

name: PyHAT-Musicbinder
       project: /
       language: py
       host: appservice

And there you have it...

Well, almost.

Note that if you are defining a start command for your application (such as the uvicorn command to start up the server for FastAPI), this also has to be defined in that main.bicep file.

Here's what it looks like to provision my FastAPI app:

// main.bicep

module web 'core/host/appservice.bicep' = {
  name: 'appservice'
  scope: resourceGroup
  params: {
    name: '${prefix}-appservice'
    location: location
    tags: union(tags, { 'azd-service-name': 'app' })
    appCommandLine: 'python -m gunicorn -w 2 -k uvicorn.workers.UvicornWorker  --timeout 60 --access-logfile '-' --error-logfile '-' --bind= app.main:app'
    runtimeName: 'python'
    runtimeVersion: '3.11'
    scmDoBuildDuringDeployment: true
    ftpsState: 'Disabled'


The only line I changed was the appCommandLine which contains the app startup command.

But again, the examples are all there in the array of templates in the Awesome AZD Templates site.


So all that's left, then, is to get this thing up to the cloud.

azd up

That's it. That's all you need to do.

Wait, wait, wait. You should always be wary of "that's it." There are still just a few tiny hurdles remaining.

If you aren't already signed in to Azure, you will be prompted to sign in.

Next, you will be prompted to provide an environment name (such as "my-cool-app"), select (or create) a subscription from your Azure account, and select a location (I recommend "useast", as choosing a different location may have availability constraints).

This will then provision the resources (defined in your Bicep files) in your account and deploy your latest code.

The first time you do this, the provisioning of resources can take a while, but progress is tracked via the terminal.

If the deploy is successful, you'll see an endpoint URI in the terminal, which will send you to your app on the cloud.

After making edits to your code, if you wish to redeploy, you can type azd deploy and this will send your newest revisions to Azure. This process is much quicker than the initial provisioning.


Lastly, If you were following along and have no intention of keeping your app up and running on Azure, you want to make sure to clean up your resources.

To power down your app:

azd down

I must admit, the overall CLI experience is quite pleasant! I'm still iffy on those Bicep files though.


Thanks for going on this journey with me. Whether this has convinced you to try out Azure or stay away completely, I'm glad I was able to inform your decision.

There are aspects of it that I really like (the CLI experience), and others that I'm still uncertain about (leaning into an IaC approach by learning a new language).

It's kind of tricky with Bicep, since it is an Azure-specific language, it requires buy in to the Microsoft ecosystem.

However, the Terraform support means you can migrate to other vendors if that works better for your situation.

I will say that I was initially fairly intimidated by the prospect of using Bicep, but the process ended up being far less complex than I imagined, mostly due to the ease of copying existing template infrastructure code.

It took me an afternoon, and I got it working with few hiccups, so there's that.

Oh, and of course, here is that Music Binder AZD compatible repo where all of this went down.

So now, back to my regularly (non)scheduled Pythoning by night.