CI/CD Pipeline with Azure DevOps

Build a multi-stage YAML pipeline that deploys a Flask app to Azure.

Introduction

30 Second Summary

Every time a developer changes code, someone has to manually run tests, package the app, and deploy it to a server. This process is slow, error-prone, and one of the first things teams automate.

In this project, you will build a fully automated CI/CD pipeline using Azure DevOps that builds, tests, and deploys a Python Flask web app to two live environments every time you push code.

What You'll Build

Push a code change and watch your pipeline carry it through automated tests, deploy it to a live dev URL, pause for your approval, then ship it to production.

By the end of this project, you'll have:

  • A multi-stage YAML pipeline that kicks off automatically on every push to main, running lint checks and unit tests.
  • A Python Flask app deployed to two live Azure App Service URLs you can open in your browser. One for dev, one for production.
  • A manual approval gate that pauses the pipeline before production. You control exactly when code goes live.
  • Secret Mission: Add a smoke test stage that validates your dev deployment before the pipeline promotes to production.

Are there any prerequisites?

You need an Azure account (a free account works) and either a Microsoft account or GitHub account to sign up for Azure DevOps. Step 1 walks you through setting up everything else.

Before We Start

Before you write a single line of YAML, take a moment to commit to what you're about to build and why it matters.

Set Up Your Toolchain

A pipeline needs three things to work: a place to run (Azure DevOps), a way to talk to your cloud account (Azure CLI), and code to deploy.

In this step, you will install the Azure CLI, create your Azure DevOps organization and project, build a small Flask web app, and push everything to Azure Repos so the pipeline has something to work with.

In this step, get ready to:
  • Install Azure CLI and sign in to Azure.
  • Create an Azure DevOps organization and project.
  • Build a local Flask project and push it to Azure Repos.
Install Azure CLI and create your Azure DevOps organization

The Azure CLI is the command-line tool you will use to create and manage Azure resources like App Service instances.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check whether Azure CLI is already installed by running:
az --version

✔️ I see a version number

Azure CLI is already installed. You are ready to move on.

ⓧ I see an older version

  • Upgrade Azure CLI to the latest version by running:
az upgrade

Follow any prompts that appear to complete the upgrade.

ⓧ Command not found

Azure CLI is not installed yet. Install it using the instructions for your operating system below.

macOS

  • Install Azure CLI with Homebrew by running:
brew install azure-cli
What if I don't have Homebrew?

Homebrew is a package manager for macOS. Install it first by visiting brew.sh and following the one-line install command. Then re-run the Azure CLI install command above.

Windows

  • Install Azure CLI with winget by running this in PowerShell:
winget install --exact --id Microsoft.AzureCLI
  • After the install finishes, close and reopen your PowerShell window so the az command is available.
  • After installation, verify Azure CLI is working by running:
az --version

You should see a version number printed in the output.

  • Sign in to Azure by running:
az login

A browser window opens. Sign in with your Azure account credentials. After signing in, the terminal shows your subscription details.

Browser not opening?

If the browser does not open automatically, look for a URL in the terminal output, copy it, and paste it into your browser manually.

Help me troubleshoot az login

Now you need a place for your pipeline to live. Azure DevOps provides the repository, pipeline engine, and deployment environments all in one place.

  • Open dev.azure.com in your browser.
  • Sign in with the same Microsoft account you used for az login.
  • Click New organization.
  • Enter a name for your organization and select a hosting region.
  • Click Continue.

What is an Azure DevOps organization?

An organization is the top-level container in Azure DevOps. It groups your projects, pipelines, and repos under one account. Think of it like a GitHub account that holds multiple repositories.

  • Azure DevOps prompts you to create your first project. Enter a project name such as cicd-pipeline.
  • Set the visibility to Private.
  • Under Advanced, confirm Version control is set to Git.
  • Click Create project.

You should see the empty project dashboard for your new project.

Create a local Flask project with a test file

Your pipeline needs an app to build and deploy. You will create a small Flask web app with a health-check route, a unit test, and a requirements file.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir cicd-pipeline
cd cicd-pipeline
  • Create a Python virtual environment and activate it by running:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

What is a virtual environment?

A virtual environment is an isolated Python installation that keeps this project's packages separate from your system Python. Everything you install with pip stays inside the venv folder.

  • Confirm your virtual environment is active. You should see (venv) at the start of your terminal prompt.
  • Install Flask by running:
pip install flask==3.1.3

You should see Flask and its dependencies downloading and installing.

  • Open the project folder in Cursor. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • In Cursor, click File in the top menu bar.
  • Click Open Folder.
  • Select the cicd-pipeline folder on your Desktop, then click Open.
  • Create a new file called app.py in the root of the cicd-pipeline folder.
  • Paste the following code into app.py:
from flask import Flask

app = Flask(__name__)


@app.route("/")
def health():
    return "OK - Pipeline deployed!", 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

What does this code do?

This is a minimal Flask web app with a single route at /. When someone visits the root URL, it responds with OK - Pipeline deployed! and an HTTP 200 status code.

This health-check response is what you will see in the browser once the pipeline deploys the app to Azure App Service.

  • Save app.py.
  • Test the app locally by switching back to your terminal and running:
python3 app.py

On Windows?

Use python app.py instead of python3 app.py.

Help me run my Flask app on Windows

You should see output indicating Flask is running on http://0.0.0.0:8000. Open that URL in your browser to see the text OK - Pipeline deployed!.

  • Press Ctrl+C in your terminal to stop the Flask server.

Don't see the message in your browser?

Make sure your virtual environment is activated (you should see (venv) at the start of your terminal prompt). Also confirm you saved app.py before running it.

Help me debug my Flask app

  • Create a new file called requirements.txt in the root of cicd-pipeline and paste the following:
flask==3.1.3
pytest==8.3.5
flake8==7.1.2

What is requirements.txt?

This file lists the exact package versions your project needs. The pipeline will use it to install dependencies in the build stage. Pinning versions ensures every build uses the same packages.

  • Save requirements.txt.
  • Create a new file called test_app.py in the root of cicd-pipeline and paste the following:
from app import app


def test_health():
    client = app.test_client()
    response = client.get("/")
    assert response.status_code == 200
    assert b"OK - Pipeline deployed!" in response.data

What does this test do?

This unit test creates a test client for your Flask app and sends a GET request to /. It checks that the response has a 200 status code and contains the expected message.

The pipeline will run this test automatically in the Build stage to catch bugs before deployment.

  • Save test_app.py.
  • Install the remaining testing dependencies and run the test by running:
pip install pytest==8.3.5 flake8==7.1.2
pytest test_app.py --verbose

You should see output showing 1 test passed. This confirms your app and test are working correctly.

Test failing?

Double-check that the string in test_app.py matches the return value in app.py exactly, including capitalization and punctuation.

Help me fix my failing test

✔️ Awesome, I've got everything!

Great. Make sure you have saved all three files.

ⓧ I'd like to double check the full code

Here is each file. Confirm your code matches exactly.

app.py:

from flask import Flask

app = Flask(__name__)


@app.route("/")
def health():
    return "OK - Pipeline deployed!", 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

requirements.txt:

flask==3.1.3
pytest==8.3.5
flake8==7.1.2

test_app.py:

from app import app


def test_health():
    client = app.test_client()
    response = client.get("/")
    assert response.status_code == 200
    assert b"OK - Pipeline deployed!" in response.data
Initialize a Git repo and push to Azure Repos

Your code needs to live in Azure Repos so the pipeline can access it. You will initialize a local Git repository, commit your files, and push them to the remote.

  • In your terminal, make sure you are inside the cicd-pipeline folder on your Desktop.
  • Initialize a Git repository and make your first commit by running:
git init
git add app.py requirements.txt test_app.py
git commit -m "Initial commit: Flask app with health check and tests"

Why not git add . ?

You are adding only the three project files and skipping the venv folder. Virtual environments contain thousands of files that should never be committed to source control. The pipeline will install its own dependencies from requirements.txt.

You should see a message confirming 3 files were committed.

  • Go back to your Azure DevOps project in the browser.
  • Click Repos in the left sidebar.
  • On the empty repo page, find the Push an existing repository from command line section.
  • Copy the two commands shown (they look like git remote add origin https://... and git push -u origin --all).
  • Paste and run both commands in your terminal.

What is the remote URL?

The remote URL follows the pattern https://dev.azure.com/{org}/{project}/_git/{repo}. This tells Git where to push your code in Azure DevOps.

You may be prompted to sign in. Use the same Microsoft account you used for Azure DevOps.

Authentication error when pushing?

If you see an authentication error, Azure DevOps may require a Personal Access Token (PAT) instead of your password. Go to User settings (the icon next to your avatar in Azure DevOps), select Personal access tokens, and create one with Code (Read & Write) scope. Use this token as your password when Git prompts you.

Help me push to Azure Repos

  • Before you check, what do you expect to see when you visit the Azure Repos page? Take a moment to predict.
  • In your browser, click Repos in the left sidebar of your Azure DevOps project.

You should see your three files listed: app.py, requirements.txt, and test_app.py. Your code is now in Azure Repos and ready for the pipeline.

Your toolchain is ready. You have Azure CLI connected to your account, a project in Azure DevOps, and your Flask app pushed to Azure Repos. Next up, you will write the first pipeline stage and watch it build your code automatically.

Write the Build Stage and Watch It Run

In the previous step, you set up your toolchain and pushed a Flask app to Azure Repos. Your code is sitting safely in a remote repo, but there is no automation yet.

Right now, deploying your app would mean manually installing dependencies, running tests, and copying files. A CI/CD pipeline removes all of that manual work. In this step, you will create a YAML pipeline file that automatically builds and tests your code every time you push to main.

In this step, get ready to:
  • Create azure-pipelines.yml with a Build stage that installs dependencies, lints, and runs tests.
  • Push your YAML to Azure Repos and create the pipeline in Azure DevOps.
  • Watch the Build stage run green and confirm the published artifact.
Create azure-pipelines.yml with a Build stage

Azure Pipelines reads a YAML file in your repo to know what to do when code is pushed. You will create this file from scratch and define a Build stage inside it.

  • In Cursor, create a new file called azure-pipelines.yml in the root of your project folder (the same level as app.py).
  • Paste the following code into the file to set up the pipeline trigger, variables, and the first half of the Build stage:
trigger:
  - main

variables:
  pythonVersion: "3.12"
  vmImageName: "ubuntu-latest"
  devAppName: "your-dev-app-name"
  prodAppName: "your-prod-app-name"
  azureSubscription: "your-service-connection-name"

stages:
  - stage: Build
    displayName: "Build and Test"
    jobs:
      - job: BuildJob
        pool:
          vmImage: $(vmImageName)
        steps:
          - task: UsePythonVersion@0
            inputs:
              versionSpec: "$(pythonVersion)"
            displayName: "Use Python $(pythonVersion)"

          - script: |
              python -m pip install --upgrade pip
              pip install -r requirements.txt
            displayName: "Install dependencies"

What does this code do?

The trigger block tells Azure Pipelines to run this pipeline automatically whenever code is pushed to the main branch.

The variables section defines reusable values. devAppName, prodAppName, and azureSubscription are placeholders you will fill in with real values in later steps when you create your Azure resources.

The Build stage uses an ubuntu-latest virtual machine, sets up Python 3.12, and installs your project's dependencies from requirements.txt.

  • Still in azure-pipelines.yml, add the remaining Build steps directly below the "Install dependencies" step (keeping the same indentation level):
          - script: |
              flake8 app.py --max-line-length 120
            displayName: "Run linter"

          - script: |
              pytest test_app.py --verbose
            displayName: "Run unit tests"

          - task: ArchiveFiles@2
            displayName: "Archive files"
            inputs:
              rootFolderOrFile: "$(System.DefaultWorkingDirectory)"
              includeRootFolder: false
              archiveType: zip
              archiveFile: "$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip"
              replaceExistingArchive: true

          - publish: "$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip"
            artifact: drop
            displayName: "Publish artifact"

What do these steps do?

The flake8 step runs a linter on app.py to catch style issues and syntax errors. The pytest step runs your unit tests to make sure the health-check route works.

The ArchiveFiles@2 task zips your entire project into a single file. The publish step uploads that ZIP as a build artifact named drop. Later stages will download this artifact to deploy your app.

  • Save azure-pipelines.yml.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm both code chunks are present in azure-pipelines.yml, in this order:

  • The trigger, variables, and the Build stage opening with UsePythonVersion@0 and the "Install dependencies" script.
  • The "Run linter", "Run unit tests", ArchiveFiles@2, and "Publish artifact" steps.

Make sure the indentation is consistent throughout. YAML is whitespace-sensitive.

Seeing a YAML syntax error?

YAML is very sensitive to indentation. Make sure every line uses spaces (not tabs) and that nested items line up consistently. Check that there are no trailing spaces after colons.

help me fix indentation issues in my azure-pipelines.yml file

Push your YAML and create the pipeline in Azure DevOps

Before Azure DevOps can use your YAML file, it needs to exist in your remote repo. You will push it now, then tell Azure DevOps to create a pipeline that points to it.

  • Commit and push your new YAML file by running these commands in your terminal:
git add azure-pipelines.yml
git commit -m "Add Build stage pipeline"
git push
  • Open your Azure DevOps project in your browser at dev.azure.com.
  • Go to Repos in the left sidebar and confirm you can see azure-pipelines.yml listed alongside your other files.

Why check Repos first?

The next step requires Azure DevOps to browse your repo for the YAML file. If the push failed silently, the file will not appear and the pipeline creation will not work. Confirming it is in Repos now saves you a confusing dead end.

  • Click Pipelines in the left sidebar.
  • Click New pipeline (or Create Pipeline if this is your first pipeline).
  • On the "Where is your code?" screen, select Azure Repos Git.
  • Select your repository from the list.
  • On the Configure your pipeline page, choose Existing Azure Pipelines YAML file.
  • In the panel that appears, set Branch to main and Path to /azure-pipelines.yml.
  • Click Continue.
  • Review the YAML preview, then click Run to save and start the pipeline.

Don't see your YAML file in the path selector?

Make sure the Branch dropdown is set to main. If the file still does not appear, return to your terminal and run git log --oneline -1 to confirm your commit was pushed. You can also check Repos in the Azure DevOps sidebar to verify the file is there.

help me troubleshoot why Azure DevOps cannot find my azure-pipelines.yml file

Watch the Build stage run

After clicking Run, Azure DevOps queues your pipeline. The Build stage will install dependencies, lint your code, run your tests, and package the result into a ZIP artifact.

Before you check the results, what do you expect to see? Will all four build steps pass, or will something fail?

  • Click on the pipeline run to open its detail view.
  • Click on the Build and Test stage to expand it.
  • Watch each step complete with a green check mark.

All four steps should show green checks. Your linter found no issues, and your unit test passed.

  • Navigate back to the pipeline run Summary tab.
  • In the related section, look for the published artifact. You should see an artifact named drop containing a ZIP file.
  • Click on drop to expand it and confirm the ZIP file is listed inside.

What is this artifact for?

The drop artifact is a packaged snapshot of your project at the time of the build. In the next step, you will create a Deploy stage that downloads this artifact and deploys it to a live Azure App Service instance.

Pipeline failed with a red X?

Click on the failed step to read the error log. Common causes:

  • Linter failure: flake8 found a style issue in app.py. Check for trailing whitespace or lines longer than 120 characters.
  • Test failure: the assertion in test_app.py did not match the response from app.py. Make sure both files use the exact string OK - Pipeline deployed!.
  • YAML syntax error: check the indentation in azure-pipelines.yml. Every level must use exactly two spaces.

help me debug a failed Azure Pipelines Build stage

Your first pipeline stage is live. Every push to main now triggers an automatic build, lint, and test. Next up, you will create an Azure App Service instance and add a Deploy stage so your code goes live at a public URL.

Deploy to Dev on Every Push

Your Build stage proves the code compiles, passes lint checks, and passes unit tests. That is a great start. But the pipeline does not actually deploy anything yet.

In this step, you will create an Azure App Service instance for your dev environment, connect Azure DevOps to your Azure subscription, and add a Deploy-Dev stage so your Flask app goes live automatically on every push.

In this step, get ready to:
  • Create a dev App Service instance using Azure CLI.
  • Connect Azure DevOps to your Azure subscription with a service connection.
  • Add a Deploy-Dev stage to your pipeline that ships the build artifact to the live dev URL.
Create the dev App Service instance

Your Flask app needs a place to live on the internet. Azure App Service is a managed hosting platform that runs your web app without requiring you to manage servers.

  • Make sure your terminal is inside your project folder (the one containing app.py).
  • Create a dev App Service instance by running this command:
az webapp up --name [[DEV_APP_NAME="your-unique-name-dev"]] --sku F1 --runtime PYTHON:3.12

What does this command do?

The az webapp up command creates a resource group, an App Service plan, and the App Service app itself in a single step.

  • --name sets a globally unique name for your app. This name becomes part of the URL. Suffix it with -dev so you can tell it apart from the prod instance later.
  • --sku F1 selects the Free tier, which costs nothing.
  • --runtime PYTHON:3.12 tells App Service to use Python 3.12 on Linux.

The command takes a minute or two to complete. When it finishes, look for the URL in the output.

  • Copy the URL from the output (it looks like https://your-unique-name-dev.azurewebsites.net).
  • Open that URL in your browser.

You should see your Flask health-check response: OK - Pipeline deployed!.

Seeing a quota or SKU error?

The F1 Free tier is not available in every region. If you see an error mentioning quota or SKU availability, try running the command with --sku B1 instead. B1 Basic costs approximately $13/month, but you will delete the resource at the end of this project.

You can also try a different Azure region by adding --location followed by a region name (for example, eastus2 or centralus).

Help me troubleshoot my az webapp up command

What is the resource group name?

Note the resource group name from the command output. You will need it when creating the service connection in the next substep.

  • Now update your pipeline so it knows the app name. Switch back to azure-pipelines.yml in Cursor.
  • In the variables section near the top, find this line:
  devAppName: "your-dev-app-name"
  • Replace your-dev-app-name with the actual app name you used in the --name flag (for example, janedoe-flask-dev). Keep the surrounding quotes.
Create a service connection in Azure DevOps

Azure DevOps needs permission to deploy to your Azure subscription. A service connection provides that link.

  • Return to your Azure DevOps project in the browser.
  • Click the gear icon in the bottom-left corner to open Project settings.
  • In the left sidebar under Pipelines, click Service connections.
  • Click New service connection.
  • Select Azure Resource Manager, then click Next.
  • Select Service principal (automatic), then click Next.
  • For Scope level, select Subscription.
  • Select your Azure subscription from the dropdown.
  • Select the resource group that az webapp up created (the one you noted earlier).
  • In the Service connection name field, type a name you will remember (for example, azure-service-connection). This is the name your pipeline YAML references.
  • Check the Grant access permission to all pipelines checkbox.
  • Click Save.

Your new service connection appears in the list. This confirms Azure DevOps can now deploy to your Azure subscription.

Save button not responding?

This can happen due to permission or browser issues. Try opening your Azure DevOps project in an Incognito or InPrivate browser window and repeating the steps. Make sure you are signed into the same Microsoft account that owns the Azure subscription.

You also need at least Contributor access on the Azure subscription to create a service connection with the automatic method.

Help me fix my service connection issue

  • Now update your pipeline with the service connection name. Switch back to azure-pipelines.yml in Cursor.
  • In the variables section, find this line:
  azureSubscription: "your-service-connection-name"
  • Replace your-service-connection-name with the exact service connection name you typed in Azure DevOps (for example, azure-service-connection). Keep the surrounding quotes.
Add the Deploy-Dev stage and push

The Build stage produces a ZIP artifact. Now you need a second stage that picks up that artifact and deploys it to your dev App Service.

  • In azure-pipelines.yml, scroll to the very end of the file (after the last line of the Build stage).
  • Add the following DeployDev stage below the Build stage:
  - stage: DeployDev
    displayName: "Deploy to Dev"
    dependsOn: Build
    condition: succeeded()
    jobs:
      - deployment: DeployDevJob
        pool:
          vmImage: $(vmImageName)
        environment: dev
        strategy:
          runOnce:
            deploy:
              steps:
                - task: UsePythonVersion@0
                  inputs:
                    versionSpec: "$(pythonVersion)"
                  displayName: "Use Python $(pythonVersion)"

                - task: AzureWebApp@1
                  displayName: "Deploy to Dev App Service"
                  inputs:
                    azureSubscription: $(azureSubscription)
                    appType: webAppLinux
                    appName: $(devAppName)
                    package: "$(Pipeline.Workspace)/drop/$(Build.BuildId).zip"

What does this stage do?

  • dependsOn: Build ensures this stage only runs after Build succeeds.
  • deployment is a special job type designed for deploying to an environment. Azure DevOps auto-creates the dev environment the first time this stage runs.
  • AzureWebApp@1 is the task that deploys your ZIP artifact to the App Service. It uses the service connection for authentication and targets your dev app by name.
  • package points to the ZIP that the Build stage published as the drop artifact.
  • Save azure-pipelines.yml.

✔️ Awesome, I've got everything!

Great. Double check you have saved azure-pipelines.yml with the updated variable values and the new DeployDev stage.

ⓧ I'd like to double check the full code

Your azure-pipelines.yml should have these sections in order. Scroll through your file and confirm each one is present:

  • The trigger block at the top, targeting main.
  • The variables block with pythonVersion, vmImageName, devAppName (your actual app name), prodAppName, and azureSubscription (your actual service connection name).
  • The Build stage with UsePythonVersion, install dependencies, flake8, pytest, ArchiveFiles, and publish steps.
  • The DeployDev stage with a deployment job targeting environment dev, using AzureWebApp@1 to deploy the ZIP artifact.
  • Commit and push the updated YAML by running these commands:
git add -A
git commit -m "Add Deploy-Dev stage"
git push

Before you check the pipeline, what do you think will happen? Will you see one stage or two?

  • Return to your Azure DevOps project in the browser.
  • Click Pipelines in the left sidebar.
  • Click on the latest pipeline run.

You should see two stages: Build and Test and Deploy to Dev. Watch them both go green.

What about the dev environment?

Azure DevOps automatically creates the dev environment the first time a pipeline references it. You do not need to create it manually. You can see it later under Pipelines then Environments.

  • Once both stages are green, open your dev URL in the browser:
https://[[DEV_APP_NAME="your-unique-name-dev"]].azurewebsites.net

You should see OK - Pipeline deployed! in your browser. This response came from your Flask app, deployed automatically by the pipeline.

Pipeline failed at Deploy to Dev?

  • Check that the devAppName value in your YAML exactly matches the app name you created with az webapp up. Even a small typo will cause the deployment to fail.
  • Check that the azureSubscription value exactly matches the service connection name you typed in Azure DevOps.
  • Click on the failed stage in the pipeline run to see the detailed error log. The log usually names the exact field that could not be resolved.

Help me debug my Deploy-Dev stage failure

Your pipeline now builds, tests, and deploys your app to a live dev URL on every push to main. Next up, you will add a production environment with a manual approval gate so code does not reach production without a human sign-off.

Add the Production Gate

Your two-stage pipeline is working. Every push to main now builds, tests, and deploys your Flask app to the dev App Service instance automatically.

The next logical step is adding a production deployment. But here is the catch: if you simply add a prod stage to the pipeline, it will deploy to production automatically on every push, with no human review. In a real team, that is a recipe for shipping broken code to users. You need a manual approval gate that pauses the pipeline before production, giving someone the chance to verify the dev deployment first.

In this step, get ready to:
  • Create a prod App Service instance for your production deployment.
  • Add a Deploy-Prod stage to the pipeline and see what happens without a gate.
  • Configure a manual approval gate to protect production from unreviewed deployments.
Create the prod App Service instance

Your dev instance is already running. Now you need a second, separate Azure App Service instance dedicated to production.

  • Create the prod App Service by running this command from your project folder:
az webapp up --name [[PROD_APP_NAME="your-app-name-prod"]] --sku F1 --runtime PYTHON:3.12

What does this command do?

This is the same az webapp up command you used for dev. Using a different --name creates a brand new App Service instance. The name must be globally unique across all of Azure, so pick something distinctive with a -prod suffix.

Used B1 for dev?

If the F1 Free tier was unavailable in your region when you created the dev instance, replace --sku F1 with --sku B1 here too. B1 Basic costs approximately $13/month, so remember to delete this resource after the project.

  • Wait for the command to finish. Look for a line showing your new app's URL in the output.
  • Copy the URL (it looks like https://your-prod-app-name.azurewebsites.net) and open it in your browser. You should see your Flask app's health-check message.

Command failed or app not loading?

  • If you see a naming error, your chosen name may already be taken. App names must be globally unique. Try adding extra characters or numbers.
  • If the F1 SKU fails with a quota error, your region may not support it. Use --sku B1 instead.
  • If the page shows a default Azure placeholder instead of your Flask app, wait a minute and refresh. First deployments can take a moment to start.

Still stuck? Ask for help.

Add the Deploy-Prod stage to the pipeline

The prod App Service exists, but the pipeline does not know about it yet. You need a third stage in azure-pipelines.yml that deploys the build artifact to production after the dev deployment succeeds.

  • First, update the prodAppName variable in your azure-pipelines.yml. In the variables section at the top, find this line:
  prodAppName: "your-prod-app-name"
  • Replace your-prod-app-name with the actual name you chose when running az webapp up above, so the line looks like this:
  prodAppName: "[[PROD_APP_NAME="your-app-name-prod"]]"
  • Now scroll to the bottom of azure-pipelines.yml, after the entire DeployDev stage. Add the following Deploy-Prod stage:
  - stage: DeployProd
    displayName: "Deploy to Prod"
    dependsOn: DeployDev
    condition: succeeded()
    jobs:
      - deployment: DeployProdJob
        pool:
          vmImage: $(vmImageName)
        environment: prod
        strategy:
          runOnce:
            deploy:
              steps:
                - task: UsePythonVersion@0
                  inputs:
                    versionSpec: "$(pythonVersion)"
                  displayName: "Use Python $(pythonVersion)"

                - task: AzureWebApp@1
                  displayName: "Deploy to Prod App Service"
                  inputs:
                    azureSubscription: $(azureSubscription)
                    appType: webAppLinux
                    appName: $(prodAppName)
                    package: "$(Pipeline.Workspace)/drop/$(Build.BuildId).zip"

What does this stage do?

  • dependsOn: DeployDev tells the pipeline to only run this stage after the dev deployment succeeds.
  • environment: prod targets a named environment in Azure DevOps. If this environment does not exist yet, Azure DevOps creates it automatically on the first run.
  • The AzureWebApp@1 task deploys the same ZIP artifact to the prod App Service using your existing service connection.
  • Save azure-pipelines.yml.
  • Commit and push the change by running these commands:
git add azure-pipelines.yml
git commit -m "Add Deploy-Prod stage"
git push

Before you check the pipeline run, think about this: you just added a production deployment stage. Do you think the pipeline will wait for your approval before deploying to prod, or will it deploy automatically?

  • Switch back to your Azure DevOps project in the browser.
  • Click Pipelines in the left sidebar.
  • Click the running pipeline to open it.

Watch all three stages go green: Build, Deploy to Dev, and Deploy to Prod. The pipeline deployed your code straight to production with zero human review.

That just shipped to production with no review

This is the gap. Right now, every push to main auto-deploys all the way to production. In a real team, a broken commit would go live before anyone could review it.

That is exactly what the next substep fixes. You will add a manual approval gate to the prod environment so the pipeline pauses and waits for explicit human sign-off before deploying to production.

Deploy-Prod stage failed?

  • Check that the prodAppName variable in your YAML matches the exact name you gave your prod App Service. A single typo will cause the deployment to fail.
  • Make sure the new stage is indented at the same level as the existing Build and DeployDev stages under stages:. YAML is sensitive to indentation.
  • Verify your service connection still has access to all pipelines. Go to Project Settings, then Service connections and check the connection you created earlier.

Need help debugging the failure?

✔️ Awesome, I've got everything!

Great. Double check you have saved azure-pipelines.yml and that all three stages ran in Azure DevOps.

ⓧ I'd like to double check the full code

Your azure-pipelines.yml is too long to show in one block. Scroll up and confirm these sections are present in this order:

  • trigger: block targeting main.
  • variables: block with pythonVersion, vmImageName, devAppName, prodAppName, and azureSubscription.
  • Stage 1: Build with UsePythonVersion, install dependencies, flake8 lint, pytest, ArchiveFiles, and publish artifact.
  • Stage 2: DeployDev with a deployment job targeting environment dev and AzureWebApp@1 deploying to devAppName.
  • Stage 3: DeployProd with a deployment job targeting environment prod and AzureWebApp@1 deploying to prodAppName.
Add a manual approval check to the prod environment

You just saw the problem: production deployed with no review. The fix is a manual approval check on the prod environment. Approval checks in Azure DevOps are configured on the environment itself, not in the YAML file. This means pipeline authors cannot bypass the gate by editing the YAML.

Why are approvals on the environment, not in the YAML?

If approvals lived in the YAML, any developer could remove the gate by editing the pipeline file. By placing checks on the environment, only administrators control who can deploy to production. The YAML references the environment name, and the environment enforces the rules.

  • In Azure DevOps, click Pipelines in the left sidebar.
  • Click Environments.
  • Click the prod environment. This was auto-created when the pipeline ran the Deploy-Prod stage for the first time.
  • Click the Approvals and checks tab.
  • Click the + button to add a new check.
  • Select Approvals, then click Next.
  • Add yourself as an approver by searching for your name or email.
  • Leave the timeout at its default of 30 days.
  • Click Create.

The approval gate is in place. Now you need to trigger a new pipeline run to see it work.

  • Trigger a new pipeline run by pushing an empty commit:
git commit --allow-empty -m "Test approval gate"
git push

Before you check Azure DevOps, predict: will the pipeline deploy to prod automatically this time, or will something be different?

  • Switch back to Azure DevOps and open the new pipeline run from Pipelines.
  • Watch the Build and Deploy to Dev stages go green.

This time, the Deploy to Prod stage does not run automatically. It pauses and shows a Review button. The approval gate is working.

  • Click the Review button on the Deploy to Prod stage.
  • Click Approve.
  • Watch the Deploy to Prod stage run and complete green.
  • Open your prod URL (https://your-prod-app-name.azurewebsites.net) in your browser. You should see your Flask app's health-check message.

Pipeline did not pause at Deploy-Prod?

  • Make sure you added the approval check to the prod environment, not the dev environment. Go to Pipelines, then Environments, click prod, and check the Approvals and checks tab for your approval entry.
  • If you see no prod environment listed, the first pipeline run may have failed before reaching the Deploy-Prod stage. Re-run the pipeline and confirm all three stages appear.

Need help with the approval gate?

Your pipeline now has a proper safety net. Production deployments are blocked until a human approves them. Next up, you will make a visible code change and trace the entire journey from commit to live deployment across both environments.

Trigger the Full Pipeline and Trace the Deployment

Your three-stage pipeline is fully wired. Build runs automatically, Deploy-Dev ships your code to a live URL, and Deploy-Prod waits for your approval before going live.

You have not yet watched the complete journey from a single code change all the way through to a live production deployment. In this step, you'll make a visible change, push it, and trace every stage through Azure DevOps to prove the pipeline works as a zero-touch deployment system.

In this step, get ready to:
  • Make a visible code change and push it to main.
  • Watch the full pipeline flow from Build through the approved prod deployment.
  • Trace deployment history in the Environments blade.
Make a visible code change

The best way to prove a deployment pipeline works end-to-end is to change something visible in the app and watch it propagate to both live URLs. You'll update the health-check message in your Flask app, fix the test to match, and push.

  • In Cursor, open app.py.
  • Find this line:
    return "OK - Pipeline deployed!", 200
  • Replace the message string with something new and distinctive. For example, change it to "v2 - Zero-touch CI/CD!" or any short phrase you like.
  • Save app.py.

Now update the unit test so it matches the new message.

  • Open test_app.py.
  • Find this line:
    assert b"OK - Pipeline deployed!" in response.data
  • Replace OK - Pipeline deployed! with the exact text of your new message. The b"..." wrapper stays. Only the text inside the quotes changes.
  • Make sure the string in test_app.py matches app.py exactly, character for character.
  • Save test_app.py.
  • Commit and push both changes by running these commands:
git add app.py test_app.py
git commit -m "Update health-check message"
git push

You should see the commit pushed successfully to your Azure Repos remote.

Push rejected?

  • Make sure you are on the main branch. Run git branch to check.
  • If you see a credentials error, run az login to refresh your Azure CLI session.
  • Why can't I push to Azure Repos?
Watch the full pipeline flow

Your push to main triggers the pipeline automatically. Now you get to watch the complete three-stage flow in real time.

  • Switch back to your Azure DevOps project in the browser.
  • Click Pipelines in the left sidebar.
  • Click on the latest pipeline run. It should have started automatically from your push.

Before you watch the stages run, predict: will the pipeline run straight through to production, or will it stop somewhere along the way?

  • Watch the Build and Test stage complete with green checks.
  • Watch the Deploy to Dev stage complete.

The pipeline pauses at Deploy to Prod. A Review button appears. This is your approval gate in action.

Why does the pipeline pause here?

The manual approval check you configured on the prod environment in Step 4 is doing its job. Every deployment to production now requires a human to review and approve it first.

In a real team, this is the moment a senior engineer or release manager checks the dev deployment, confirms it looks good, and clicks Approve. No one can accidentally ship to production without that sign-off.

  • Click Review.
  • Click Approve.
  • Watch the Deploy to Prod stage complete.

All three stages now show green checks. Your code went from commit to production in one continuous flow.

Pipeline stuck or a stage failed?

  • If the Build stage fails, click on the failed job to read the logs. A common cause is a mismatch between the message in app.py and test_app.py. The test assertion must match the return string exactly.
  • If the Deploy stage fails, check that your service connection still has access. Go to Project Settings, then Service connections, and verify the connection shows a green status.
  • Why is my Azure DevOps pipeline stage failing?
Verify deployment history in the Environments blade

The Environments blade in Azure DevOps keeps a full record of every deployment. Each entry links back to the exact commit that triggered it, giving you a complete audit trail from code change to live deployment.

  • Click Pipelines in the left sidebar.
  • Click Environments.
  • Click dev to see its deployment history.
  • Click on a deployment entry to see which commit triggered it.

Now check the prod environment.

  • Go back to Environments.
  • Click prod to see its deployment history.
  • Click on a deployment entry to trace it back to the commit.

What am I looking at?

Each deployment entry shows the pipeline run number, the commit hash, and a timestamp. Clicking through to the commit shows you exactly which lines of code changed.

This is the audit trail that teams rely on when debugging production issues. If something breaks after a deployment, you can trace the exact commit responsible in seconds.

Before you check the live URLs, what message do you expect each one to display?

  • Open your dev Azure App Service URL in a browser.
  • Open your prod App Service URL in a new tab.

Both URLs display the new message you chose. You just traced a single code change from your terminal through Build, Deploy-Dev, manual approval, and Deploy-Prod, all the way to two live production URLs.

Still seeing the old message?

  • App Service can take up to two to three minutes to restart after a deployment. Wait a moment and refresh the page.
  • Check the pipeline run in Azure DevOps to confirm all three stages completed with green checks. If a stage is still running, the deployment is not finished yet.
  • Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to bypass your browser cache.
  • Why is my App Service still showing the old response after deployment?

Secret mission

Add Smoke Tests Before Production

Your pipeline deploys to dev and then waits for your approval before prod. But what if the dev deployment is broken? Nothing checks whether the dev app actually works before asking you to promote it. Add a smoke test stage that catches broken deployments before they reach the production gate.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. If you used the F1 Free tier for your App Service instances, there are no ongoing Azure costs.

Cost warning

If you used the B1 Basic tier because F1 was unavailable in your region, each App Service instance costs approximately $13 per month. With two instances (dev and prod), that adds up to approximately $26 per month.

Delete your Azure resource group now if you are done with the project to stop these charges.

Resources you used:

  • Azure resource group containing your dev and prod App Service instances and App Service plan.
  • Azure DevOps project containing your pipeline, environments, and service connection.
  • Local project files on your Desktop, including the Python virtual environment.

Keep everything running

No action needed. Choose this option if you are still actively building or want to experiment further.

  • If you used the F1 Free tier, your App Service instances have no ongoing cost.
  • If you used the B1 Basic tier, each instance costs approximately $13 per month.
  • Azure DevOps free tier covers 1,800 build minutes per month at no cost.
  • Your local project files and virtual environment take minimal disk space.

Pause - I'll come back to this later

Azure App Service does not support pausing. If you are on the F1 Free tier, your instances are already free, so there is nothing to pause.

If you are on the B1 Basic tier, the App Service plan still incurs charges even when the app is stopped. Consider deleting your resource group and recreating the instances when you are ready to continue.

  • Your Azure DevOps project, pipeline, and environments are free and will be waiting for you when you return.
  • Your local project files stay on your machine with no ongoing cost.

Delete - I don't want to use this again

Remove all project resources to free up cloud resources and local disk space.

Delete your Azure resource group

Deleting the resource group removes both App Service instances (dev and prod), the App Service plan, and the resource group itself. The resource group name was displayed in the output when you ran az webapp up earlier.

  • Delete your resource group by running this command:
az group delete --name [[RESOURCE_GROUP="your resource group name"]] --yes --no-wait

What do these flags do?

  • The --yes flag skips the confirmation prompt.
  • The --no-wait flag returns control to your terminal immediately while Azure deletes the resources in the background.
Delete your Azure DevOps project

Deleting the project removes your pipeline, environments, deployment history, and service connection. Your Azure DevOps organization stays intact and costs nothing.

  • In Azure DevOps, click Project settings in the lower-left corner.
  • Scroll down to the bottom of the General section and click Delete.
  • Type your project name to confirm, then click Delete.
Delete your local project files

Remove the project folder from your Desktop to free up disk space.

  • If your virtual environment is still active, deactivate it first by running:
deactivate
  • Delete your project folder by running this command:

macOS

cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

Windows

cd ~\Desktop
Remove-Item -Recurse -Force [[PROJECT_FOLDER="your project folder name"]]

Linux

cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

Nice Work!

Nice Work!

Nice work! You just built a zero-touch CI/CD pipeline that takes your code from a git push all the way to a live production deployment.

You've learned how to:

  • Built a multi-stage YAML pipeline from scratch with Build, Deploy-Dev, and Deploy-Prod stages, wired to Azure App Service using a service connection and the AzureWebApp@1 deployment task.
  • Configured a manual approval gate on the prod environment so production deployments require human sign-off before going live.
  • Deployed a Python Flask web app to two separate live URLs and traced the complete deployment history in the Azure DevOps Environments blade.
  • Secret Mission: Added a smoke test stage that validates the dev deployment before allowing promotion to prod.

Ready to quiz yourself?