In this tutorial, we'll look at how to securely deploy a Django app to Azure App Service.
Contents
Objectives
By the end of this tutorial, you should be able to:
- Explain what Azure App Service is and how it works.
- Deploy a Django App to Azure App Service.
- Spin up a Postgres instance on Azure.
- Set up persistent storage with Azure Storage.
- Link a custom domain to your web app and serve it on HTTPS.
What is Azure App Service?
Azure App Service allows you to quickly and easily create enterprise-ready web and mobile apps for any platform or device and deploy them on scalable and reliable cloud infrastructure. It natively supports Python, .NET, .NET Core, Node.js, Java, PHP, and containers. They have built-in CI/CD, security features, and zero downtime deployments.
Azure App Service offers great scaling capabilities, allowing applications to scale up or down automatically based on traffic and usage patterns. Azure also guarantees a 99.95% SLA.
If you're a new customer you can get $200 free credit to test Azure.
Why App Service?
- Great scaling capabilities
- Integrates well with Visual Studio
- Authentication via Azure Active Directory
- Built-in SSL/TLS certificate
- Monitoring and alerts
Project Setup
In this tutorial, we'll be deploying a simple image hosting application called django-images.
Check your understanding by deploying your own Django application as you follow along with the tutorial.
First, grab the code from the repository on GitHub.
$ git clone [email protected]:duplxey/django-images.git
$ cd django-images
Create a new virtual environment and activate it:
$ python3 -m venv venv && source venv/bin/activate
Install the requirements and migrate the database:
(venv)$ pip install -r requirements.txt
(venv)$ python manage.py migrate
Run the server:
(venv)$ python manage.py runserver
Open your favorite web browser and navigate to http://localhost:8000. Make sure everything works correctly by using the form on the right to upload an image. After you upload an image, you should see it displayed in the table:
Configure Django Project
In this section of the tutorial, we'll configure the Django project to work with App Service.
Environment variables
We shouldn't store secrets in the source code, so let's utilize environment variables. The easiest way to do this is to use a third-party Python package called python-dotenv. Start by adding it to requirements.txt:
python-dotenv==1.0.0
Feel free to use a different package for handling environment variables like django-environ or python-decouple.
For Django to initialize the environment change, update the top of settings.py like so:
# core/settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env')
Next, load SECRET_KEY
, DEBUG
, ALLOWED_HOSTS
, and other settings from the environment:
# core/settings.py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', '0').lower() in ['true', 't', '1']
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(' ')
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(' ')
SECURE_SSL_REDIRECT = \
os.getenv('SECURE_SSL_REDIRECT', '0').lower() in ['true', 't', '1']
if SECURE_SSL_REDIRECT:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Database
To use Postgres instead of SQLite, we first need to install the database adapter.
Add the following line to requirements.txt:
psycopg2-binary==2.9.5
Later, when we spin up a Postgres instance, Azure will provide us with a database connection string. It will have the following format:
dbname=<db_name> host=<db_host> port=5432 user=<db_user> password=<db_password>
Since this string is pretty awkward to utilize with Django, we'll split it into the following env variables: DBNAME
, DBHOST
, DBUSER
, DBPASS
.
Navigate to core/settings.py and change DATABASES
like so:
# core/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DBNAME'),
'HOST': os.environ.get('DBHOST'),
'USER': os.environ.get('DBUSER'),
'PASSWORD': os.environ.get('DBPASS'),
'OPTIONS': {'sslmode': 'require'},
}
}
Azure App Service also requires sslmode
so make sure to enable it.
Gunicorn
Moving along, let's install Gunicorn, a production-grade WSGI server that it's going to be used in production instead of Django's development server.
Add it to requirements.txt:
gunicorn==20.1.0
That's it for the Django configuration!
Deploy App
In this section of the tutorial, we'll deploy the web app to Azure App Service. Go ahead and sign up for an Azure account if you don't already have one.
Project Initialization
From the dashboard, use the search bar to search for "Web App + Database". You should see it within the "Marketplace" section:
Click it and you should get redirected to the app creation form.
Use the form to create the project, app, and database.
Project details
- Subscription: Leave as default
- Resource group: django-images-group
- Region: The region the closest to you
Web App Details
- Name: django-images
- Runtime stack: Python 3.11
Database
- Database engine: PostgreSQL - Flexible Server
- Server name: django-images-db
- Database name: django-images
- Hosting: Up to you
After you've filled out all the details, click on the "Review" button and then "Create".
It should take about five minutes to set up. Once done, navigate to the newly created resource by clicking on the "Go to resource" button.
Take note of the app URL since we'll need it to configure ALLOWED_HOSTS
, CSRF_TRUSTED_ORIGINS
, and a few other Django settings.
App Configuration
For the app to work, we need to add all the environment variables we used in our Django project.
Navigate to your App Service app and select "Settings > Configuration" on the sidebar.
You'll notice that an application setting named AZURE_POSTGRESQL_CONNECTIONSTRING
has already been added. This is because we used "Web App + Database" to initialize the project.
The connection string contains all the information required to connect to the database. As mentioned before, let's split it into multiple variables and add them as separate application settings:
DBNAME=<db_name>
DBHOST=<db_host>
DBUSER=<db_user>
DBPASS=<db_pass>
Make sure to replace the placeholders with your actual credentials.
If your password ends with
$
make sure to include it. That$
is part of the password, not a regex anchor.
Next, add the following additional variables:
DEBUG=1
SECRET_KEY=w7a8a@lj8nax7tem0caa2f2rjm2ahsascyf83sa5alyv68vea
ALLOWED_HOSTS=localhost 127.0.0.1 [::1] <your_app_url>
CSRF_TRUSTED_ORIGINS=https://<your_app_url>
SECURE_SSL_REDIRECT=0
Make sure to replace <your_app_url>
with the app URL from the previous step.
Don't worry about
DEBUG
and other insecure settings. We'll change them later!
Once you've added all the application settings, click "Save" to update and restart your App Service app. Wait a minute or two for the restart to complete and then move to the next step.
Deploy Code
To deploy your code to Azure App Service, you'll first need to push it to GitHub, Bitbucket, Azure Repos or another git-based version control system. Go ahead and do that if you haven't already.
We'll be using GitHub in this tutorial.
Navigate to your App Service app and select "Deployment > Deployment Center" on the sidebar. Select the source you'd like to use and authenticate with your third-party account if you haven't already.
Next, fill out the form:
- Source: GitHub
- Organization: Your personal account or organization
- Repository: The repository you'd like to deploy
- Branch: The branch you'd like to deploy
Lastly, click "Save".
Azure will now set up a GitHub Action deployment pipeline, for deploying your application. From now on, every time you push your code to the selected branch, your app will automatically redeploy.
If you navigate to your GitHub repository, you'll notice a new folder named ".github/workflows". There will also be a GitHub action workflow running. After the workflow run is done, try visiting your app URL in your favorite web browser.
When you visit your newly deployed app for the first time, App Service can take a bit of time to "wake up". Give it a few minutes and try again.
If your app depends on the database migrations, you'll probably get an error since we haven't migrated the database yet. We'll do it in the next step.
SSH
App Service makes it easy for us to SSH into our server right from the browser.
To SSH, navigate to your App Service app and select "Development Tools > SSH" on the sidebar. Next, click on the "Go" button. Azure will open a new browser window with an active SSH connection to the server:
_____
/ _ \ __________ _________ ____
/ /_\ \\___ / | \_ __ \_/ __ \
/ | \/ /| | /| | \/\ ___/
\____|__ /_____ \____/ |__| \___ >
\/ \/ \/
A P P S E R V I C E O N L I N U X
Documentation: http://aka.ms/webapp-linux
Python 3.9.16
Note: Any data outside '/home' is not persisted
(antenv) root@6066faafff94:/tmp/8db11d9b11cc42a#
Let's migrate the database and create a superuser:
(antenv)$ python manage.py migrate
(antenv)$ python manage.py createsuperuser
Nice! At this point your web application should be fully working.
Persistent Storage
Azure App Service offers an ephemeral filesystem. This means that your data isn't persistent and might vanish when your application shuts down or is redeployed. This is extremely bad if your app requires files to stick around.
To set up persistent storage for static and media files, we can use Azure Storage.
Navigate to your Azure dashboard and search for "Azure Storage". Then select "Storage accounts".
Create Storage Account
To use Azure Storage, you first need to create a storage account. Click on "Create storage account", and create a new storage account with the following details:
- Subscription: Leave as default
- Resource group: django-images-group
- Storage account name: Pick a custom name
- Region: The same region as your app
- Performance: Up to you
- Redundancy: Leave as default
Leave everything else as default, review, and save. Take note of the storage account name, since we'll need it later in the tutorial.
Once the storage account is successfully created, navigate to it. Then select "Security + Networking > Access keys" in the sidebar and grab one of the keys.
Create Containers
To better organize our storage we'll create two separate containers.
First, navigate back to the "Overview" and then click "Blob service".
Go ahead and create two containers, one named "static" and another one named "media". They should both have "Public access level" set to "Blob (anonymous read access for blobs only)".
Configure App
Next, navigate to your App Service app configuration and add the following two application settings:
AZURE_ACCOUNT_NAME=<your_storage_account_name>
AZURE_ACCOUNT_KEY=<your_storage_account_key>
Click "Save" and wait for your application to restart.
Configure Django
To utilize Azure Storage, we'll use a third-party package called django-storages.
Add the following lines to requirements.txt
django-storages==1.13.2
azure-core==1.26.3
azure-storage-blob==12.14.1
Next, go to core/settings.py and change static and media files settings like so:
# core/settings.py
DEFAULT_FILE_STORAGE = 'core.azure_storage.AzureMediaStorage'
STATICFILES_STORAGE = 'core.azure_storage.AzureStaticStorage'
AZURE_ACCOUNT_NAME = os.getenv('AZURE_ACCOUNT_NAME')
AZURE_ACCOUNT_KEY = os.getenv('AZURE_ACCOUNT_KEY')
AZURE_CUSTOM_DOMAIN = f'{AZURE_ACCOUNT_NAME}.blob.core.windows.net'
STATIC_URL = f'https://{AZURE_CUSTOM_DOMAIN}/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = f'https://{AZURE_CUSTOM_DOMAIN}/media/'
MEDIA_ROOT = BASE_DIR / 'mediafiles'
Since we're using two separate containers, we'll need to define our own AzureMediaStorage
and AzureStaticStorage
. Create a new file called azure_storage.py in the "core" directory (next to settings.py) with the following contents:
# core/azure_storage.py
import os
from storages.backends.azure_storage import AzureStorage
class AzureMediaStorage(AzureStorage):
account_name = os.getenv('AZURE_ACCOUNT_NAME')
account_key = os.getenv('AZURE_ACCOUNT_KEY')
azure_container = 'media'
expiration_secs = None
class AzureStaticStorage(AzureStorage):
account_name = os.getenv('AZURE_ACCOUNT_NAME')
account_key = os.getenv('AZURE_ACCOUNT_KEY')
azure_container = 'static'
expiration_secs = None
Commit your code and push it to the VCS.
After your app redeploys, SSH into the server and try to collect the static files:
(antenv)$ python manage.py collectstatic
You have requested to collect static files at the destination
location as specified in your settings.
This will overwrite existing files!
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: yes
141 static files copied.
To make sure the static and media files work, navigate to your app's /admin
and check if CSS has been loaded. Next, try to upload an image.
Custom Domain
To link a custom domain to your app, first navigate to your app dashboard and then select "Settings > Custom Domains" on the sidebar. After that, click "Add custom domain".
Then, add a custom domain with the following details:
- Domain provider: All other domain services
- TLS/SSL Certificate: App Service Managed Certificate
- TLS/SSL type: SNI SSL
- Domain: Your domain (e.g., azure.testdriven.io)
- Hostname record type: CNAME
After you enter all the details, Azure will ask you to validate your domain ownership. To do that, you'll need to navigate to your domain registrar's DNS settings and add a new "CNAME Record", pointing to your app URL and a TXT record like so:
+----------+--------------+------------------------------------+-----------+
| Type | Host | Value | TTL |
+----------+--------------+------------------------------------+-----------+
| CNAME | <some host> | <your_app_url> | Automatic |
+----------+--------------+------------------------------------+-----------+
| TXT | asuid.azure | <your_txt_value> | Automatic |
+----------+--------------+------------------------------------+-----------+
Example:
+----------+--------------+------------------------------------+-----------+
| Type | Host | Value | TTL |
+----------+--------------+------------------------------------+-----------+
| CNAME | azure | django-images.azurewebsites.net | Automatic |
+----------+--------------+------------------------------------+-----------+
| TXT | asuid.azure | BXVJAHNLY3JLDG11Y2H3B3C6KQASDFF | Automatic |
+----------+--------------+------------------------------------+-----------+
Wait a few minutes for the DNS changes to propagate, and then click on "Validate". Once the validation has been completed, click "Add".
Azure will link a custom domain to the app and issue an SSL certificate. Your custom domain should be displayed in the table with the status "Secured".
If your domain's status is "No binding" or you get an error saying "Failed to create App Service Managed Certificate for ...", click "Add binding", leave everything as default, and then click "Validate". In case it fails, try again in a few minutes.
The last thing we need to do is to change ALLOWED_HOSTS
and CSRF_TRUSTED_ORIGINS
and enable SECURE_SSL_REDIRECT
. Navigate to your App Service app configuration and change them like so:
ALLOWED_HOSTS=localhost 127.0.0.1 [::1] <your_custom_domain>
CSRF_TRUSTED_ORIGINS=https://<your_custom_domain>
SECURE_SSL_REDIRECT=1
Your web app should now be accessible at your custom domain on HTTPS.
Conclusion
In this tutorial, we've successfully deployed a Django app to Azure App Service. We've taken care of the Postgres database, configured static and media file serving, linked a custom domain name, and enabled HTTPS. You should now have a basic idea of how Azure works and be able to deploy your own Django apps.
Grab the final source code from the GitHub repo.
Future steps
- Look into Azure App Service Monitoring and Logging.
- Learn how to use Azure Command-Line Interface.