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?
Deployment
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
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.
Complexity
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.
Simplicity
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).
- Initialize a project with
azd init
- Create (*or modify) the infrastructure files
- Update the
azure.yml
file - Deploy with
azd 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?
Templates
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:
musicbinder/
┣ .devcontainer/
┃ ┣ devcontainer.json
┃ ┗ Dockerfile
┣ .venv/
┣ app/
┃ ┣ static/
┃ ┣ templates/
┃ ┣ config.py
┃ ┣ crud.py
┃ ┣ helpers.py
┃ ┣ main.py
┃ ┣ routes.py
┃ ┗ __init__.py
┣ data/
┃ ┗ data.json
┣ tests/
┣ .gitignore
┣ CODE_OF_CONDUCT.md
┣ LICENSE.md
┣ pyproject.toml
┣ README.md
┣ 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:
your_project/
┣ .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 ]
Well!
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:
your_project/
┣ .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 ]
Finetuning
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
services:
app:
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' })
appServicePlanId: appServicePlan.outputs.id
appCommandLine: 'python -m gunicorn -w 2 -k uvicorn.workers.UvicornWorker --timeout 60 --access-logfile '-' --error-logfile '-' --bind=0.0.0.0:8000 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.
Up
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.
Down
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.
Conclusion
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.