Deploying a Python bot to Azure Web App Service (Free Tier)

in Cloud & Serverless


Run a Python bot for free on Azure Web App Service

Pre-requisite: have a bot. You can use mehov/bot-clock as an example. It works with Telegram, so you will need TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID - see the bot's readme for more info.

Create a new Web App

Go to Azure Portal → App Services and create a new Web App:

  • Publish: Code
  • Runtime stack: Python
  • Operating System: Linux
  • Database, Deployment, Monitor + secure: disable or reject everything
  • Networking → Enable public access: On

Disable Deployment for now - you will configure it later after the app has been created.

Configure environment

Go to Settings → Environment Variables

  • set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID; see the bot's readme for more info

Configure Startup: gunicorn

Under Settings → Configuration → General settings → Startup Command:

chmod +x $APP_PATH/setup.sh && $APP_PATH/setup.sh "$WEBSITE_HOSTNAME" && gunicorn --bind=$HOST:$PORT app:app

The above runs setup.sh on startup. The script ensures the CRON command is set up. It then starts the Azure app using gunicorn - more info on that below under Gotchas. $WEBSITE_HOSTNAME, $HOST and $PORT are retrieved from the environment.

Configure Startup: manual

Sometimes you may want to handle gunicorn yourself. For example, if you want to create and run an instance of Application.builder() from telegram.ext and have it share the event loop with Flask. For that you will need to define the uvicorn.Server yourself and it would look like this:

import asyncio
import telegram
import uvicorn
from asgiref.wsgi import WsgiToAsgi
from flask import Flask
from telegram.ext import (
    Application,
)

flask_app = Flask(__name__)
telegram_app = Application.builder().token('TELEGRAM_BOT_TOKEN').updater(None).build()

async def main():
    webserver = uvicorn.Server(
        config=uvicorn.Config(
            app=WsgiToAsgi(flask_app),
            port=8000,
            use_colors=False,
            host='0.0.0.0',
        )
    )
    async with telegram_app:
        await telegram_app.start()
        await webserver.serve()
        await telegram_app.stop()

if __name__ == '__main__':
    asyncio.run(main())

Under Settings → Configuration → General settings → Startup Command:

chmod +x $APP_PATH/setup.sh && $APP_PATH/setup.sh "$WEBSITE_HOSTNAME" && HOME="/home/site/wwwroot" python3 app.py

Similarly to the gunicorn startup command, this one begins with setting up cron, and then it just starts the script directly (making sure it has Azure's persistent location as $HOME)

Configure deployment

Still under Settings → Configuration → General settings:

  • set SCM Basic Auth Publishing Credentials to On - when disabled, Github actions that rely on HTTP basic auth won't work
  • (optional) enable or disable FTP and SSH to your preference

Then go to App Services → Your New App → Deployment → Deployment Center. Open Settings.

  • Source: External Git (instead of choosing Github - this way you don't have to use Github Actions/Workflows, Azure picks up the commits by itself)
  • Build Provider: App Service Build Service
  • Repository: https://github.com/mehov/bot-clock.git
  • Branch: master

If you want Azure to have access to your private repositories, authorise Azure to your Github account.

Automatic deployment

If you want your app to be deployed to Azure automatically on each commit, you will have to set up a webhook in your repo settings.

  1. Get the secrets from Azure. Go to DeploymentDeployment CenterManage Publish Profile. In the pop-up click Download publish profile. In the XML file that downloads, in the entry where it says profileName="Web Deploy" and publishMethod="MSDeploy", you're looking for the following values:
    • publishUrl: {your-app}.scm.azurewebsites.net:443
    • userName: ${your-app}, $ is important
    • userPWD: {BigRandomPassword}
  2. Go to https://github.com/{you}/{repo}/settings/hooks and create a new webhook that looks like this:

     https://{userName}:{userPWD}@{publishUrl}/deploy

    More info:

Gotchas

Because Startup Command is not empty Azure won't automatically start the app

When you create an app, Startup Command is normally empty, and so Azure automatically detects the web app environment and then autostarts the app. If a WSGI-compatible app, e.g. based on Flask, is detected, Azure uses WSGI servers like Gunicorn to handle concurrent requests and manage threading, as Flask’s built-in server (app.run()) is not suitable for production environments.

But if you override Startup Command, you must manually start your web server after any setup scripts.

If you set Startup Command that only runs some housekeeping script and exits, Azure won’t autostart the app because it assumes you’re handling it yourself. In other words, if your custom Startup Command exits, Azure stops because there’s no running process.

Autostart? Azure looks for app.py with app variable in it

If Startup Command is empty and Azure Web App Service detects your app as Python and Flask powered, the default launch command is gunicorn -b 0.0.0.0:8000 app:app, where app:app is MODULE_NAME:VARIABLE_NAME:

  • MODULE_NAME: The Python module (file) that contains the application, app.py by default.
  • VARIABLE_NAME: The WSGI application instance (a Python variable inside the module), app by default.

For the above launch command to work, your main script needs to be app.py and the WSGI-compatible instance in it needs to be in the variable named app. If you import it and it has a different name where you import it from, you can alias it:

from src.Http import http as app

Here's detailed info on the Azure logic: azureossd.github.io/2023/01/30/Troubleshooting-'failed-to-find-attribute'-errors-on-Python-Linux-App-Services

You don't have to call app.run() in your python script

Azure will handle it automatically.

More info

#microsoft-azure #python #bot