Back to Blog

Deploying FastAPI to Azure Container Apps with Key Vault, Container Registry, and GitHub Actions

·15 min read·By Ibrahim Shittu
azure
fastapi
key-vault
container-apps
container-registry
github-actions
devops

In this tutorial, we'll deploy a FastAPI application to Azure Container Apps using a complete DevOps pipeline. We'll leverage Azure Key Vault for secure secrets management, Azure Container Registry for container images, and GitHub Actions for automated CI/CD.

By the end of this tutorial, you'll have a production-ready deployment that demonstrates modern cloud-native practices with Azure's serverless container platform.

Objectives

In this tutorial, you will:

1. Create a minimal FastAPI application

2. Set up Azure Container Registry for image storage

3. Configure Azure Key Vault for secrets management

4. Deploy to Azure Container Apps with auto-scaling

5. Implement a complete CI/CD pipeline with GitHub Actions

6. Apply production security and monitoring practices

Prerequisites

Before starting, ensure you have:

  • Python 3.11 or later
  • Docker Desktop
  • Azure CLI (az)
  • Git
  • A GitHub account
  • An Azure subscription with appropriate permissions

Verify your installations:

bash
1python --version
2docker --version
3az --version
4git --version

Project Setup

Let's start by creating our project structure and FastAPI application.

Create Project Structure

bash
1mkdir fastapi-azure-demo
2cd fastapi-azure-demo
3git init

Create the following structure:

text
1fastapi-azure-demo/
2├── app/
3│   └── main.py
4├── tests/
5│   └── test_main.py
6├── .github/
7│   └── workflows/
8│       └── deploy.yml
9├── requirements.txt
10├── Dockerfile
11├── .dockerignore
12├── .gitignore
13└── README.md

FastAPI Application

Create app/main.py:

python
1from fastapi import FastAPI
2import os
3from datetime import datetime
4
5app = FastAPI(title="Azure Demo API", version="1.0.0")
6
7@app.get("/")
8def read_root():
9    return {
10        "message": "Hello from Azure Container Apps!",
11        "timestamp": datetime.now().isoformat(),
12        "version": "1.0.0"
13    }
14
15@app.get("/health")
16def health_check():
17    return {
18        "status": "healthy",
19        "timestamp": datetime.now().isoformat(),
20        "environment": os.getenv("ENVIRONMENT", "development")
21    }
22
23@app.get("/config")
24def get_config():
25    # Demonstrate reading secrets from Key Vault (via environment variables)
26    return {
27        "database_configured": bool(os.getenv("DATABASE_URL")),
28        "api_key_configured": bool(os.getenv("API_KEY")),
29        "environment": os.getenv("ENVIRONMENT", "development")
30    }

Test Script

Create tests/test_main.py:

python
1import pytest
2from fastapi.testclient import TestClient
3from app.main import app
4
5client = TestClient(app)
6
7def test_root_endpoint():
8    """Test the root endpoint returns expected response."""
9    response = client.get("/")
10    assert response.status_code == 200
11    data = response.json()
12    assert "message" in data
13    assert "timestamp" in data
14    assert "version" in data
15    assert data["message"] == "Hello from Azure Container Apps!"
16    assert data["version"] == "1.0.0"
17
18def test_health_endpoint():
19    """Test the health check endpoint."""
20    response = client.get("/health")
21    assert response.status_code == 200
22    data = response.json()
23    assert "status" in data
24    assert "timestamp" in data
25    assert "environment" in data
26    assert data["status"] == "healthy"
27    assert data["environment"] == "development"
28
29def test_config_endpoint():
30    """Test the configuration endpoint."""
31    response = client.get("/config")
32    assert response.status_code == 200
33    data = response.json()
34    assert "database_configured" in data
35    assert "api_key_configured" in data
36    assert "environment" in data
37    assert isinstance(data["database_configured"], bool)
38    assert isinstance(data["api_key_configured"], bool)
39
40def test_invalid_endpoint():
41    """Test that invalid endpoints return 404."""
42    response = client.get("/invalid")
43    assert response.status_code == 404
44
45if __name__ == "__main__":
46    # Run tests with simple output when executed directly
47    print("Running FastAPI tests...\n")
48
49    test_functions = [
50        test_root_endpoint,
51        test_health_endpoint,
52        test_config_endpoint,
53        test_invalid_endpoint
54    ]
55
56    passed = 0
57    failed = 0
58
59    for test_func in test_functions:
60        try:
61            test_func()
62            print(f"✅ {test_func.__name__}: PASSED")
63            passed += 1
64        except AssertionError as e:
65            print(f"❌ {test_func.__name__}: FAILED - {e}")
66            failed += 1
67        except Exception as e:
68            print(f"❌ {test_func.__name__}: ERROR - {e}")
69            failed += 1
70
71    print(f"\n{'='*50}")
72    print(f"Results: {passed} passed, {failed} failed")
73    print(f"{'='*50}")
74
75    if failed > 0:
76        exit(1)
77    print("\n🎉 All tests passed!")

Create requirements.txt:

text
1fastapi==0.109.0
2uvicorn[standard]==0.27.0

Basic Files

Create .gitignore:

text
1__pycache__/
2*.py[cod]
3*.pyc
4.env
5.venv
6venv/
7.DS_Store

Create .dockerignore:

text
1__pycache__
2*.pyc
3.git
4.gitignore
5.env
6README.md
7.venv
8venv/

Containerizing the Application

Create a simple, production-ready Dockerfile:

dockerfile
1FROM python:3.11-slim
2
3WORKDIR /app
4
5# Copy requirements first for better caching
6COPY requirements.txt .
7RUN pip install --no-cache-dir -r requirements.txt
8
9# Copy application code
10COPY ./app ./app
11
12# Create non-root user
13RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
14USER appuser
15
16# Health check
17HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
18    CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
19
20EXPOSE 8000
21
22CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Test the container locally:

bash
1# Build and test locally
2docker build -t fastapi-demo .
3docker run -d -p 8000:8000 --name test-app fastapi-demo
4
5# Test endpoints
6curl http://localhost:8000/
7curl http://localhost:8000/health
8curl http://localhost:8000/config
9
10# Clean up
11docker stop test-app && docker rm test-app

Setting Up Azure Infrastructure

Now we'll create the Azure resources needed for our deployment.

Login and Setup Variables

bash
1# Login to Azure
2az login
3
4# Set subscription (replace with your subscription ID)
5az account set --subscription "your-subscription-id"
6
7# Define variables
8RESOURCE_GROUP="rg-fastapi-demo"
9LOCATION="eastus"
10ACR_NAME="acrfastapidemo$RANDOM"
11KEYVAULT_NAME="kv-fastapi-demo-$RANDOM"
12CONTAINER_APP_NAME="ca-fastapi-demo"
13CONTAINER_APP_ENV="cae-fastapi-demo"
14LOG_WORKSPACE="law-fastapi-demo"
15
16echo "Resource Group: $RESOURCE_GROUP"
17echo "ACR Name: $ACR_NAME"
18echo "Key Vault: $KEYVAULT_NAME"

Create Resource Group

bash
1az group create \
2  --name $RESOURCE_GROUP \
3  --location $LOCATION

Create Azure Container Registry

bash
1# Create Container Registry
2az acr create \
3  --resource-group $RESOURCE_GROUP \
4  --name $ACR_NAME \
5  --sku Basic \
6  --admin-enabled true
7
8# Get registry credentials
9ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --query loginServer --output tsv)
10ACR_USERNAME=$(az acr credential show --name $ACR_NAME --query username --output tsv)
11ACR_PASSWORD=$(az acr credential show --name $ACR_NAME --query passwords[0].value --output tsv)
12
13echo "Registry Server: $ACR_LOGIN_SERVER"
14echo "Username: $ACR_USERNAME"
15echo "Password: $ACR_PASSWORD"

Create Azure Key Vault

bash
1# Create Key Vault
2az keyvault create \
3  --name $KEYVAULT_NAME \
4  --resource-group $RESOURCE_GROUP \
5  --location $LOCATION \
6  --sku standard
7
8# Add example secrets
9az keyvault secret set \
10  --vault-name $KEYVAULT_NAME \
11  --name "DATABASE-URL" \
12  --value "postgresql://user:pass@host:5432/mydb"
13
14az keyvault secret set \
15  --vault-name $KEYVAULT_NAME \
16  --name "API-KEY" \
17  --value "super-secret-api-key-12345"
18
19# Verify secrets
20az keyvault secret list --vault-name $KEYVAULT_NAME --output table

Create Log Analytics Workspace

bash
1# Create workspace for Container Apps logging
2az monitor log-analytics workspace create \
3  --resource-group $RESOURCE_GROUP \
4  --workspace-name $LOG_WORKSPACE \
5  --location $LOCATION
6
7# Get workspace details
8WORKSPACE_ID=$(az monitor log-analytics workspace show \
9  --resource-group $RESOURCE_GROUP \
10  --workspace-name $LOG_WORKSPACE \
11  --query customerId --output tsv)
12
13WORKSPACE_KEY=$(az monitor log-analytics workspace get-shared-keys \
14  --resource-group $RESOURCE_GROUP \
15  --workspace-name $LOG_WORKSPACE \
16  --query primarySharedKey --output tsv)

Create Container Apps Environment

bash
1# Create Container Apps environment
2az containerapp env create \
3  --name $CONTAINER_APP_ENV \
4  --resource-group $RESOURCE_GROUP \
5  --location $LOCATION \
6  --logs-workspace-id $WORKSPACE_ID \
7  --logs-workspace-key $WORKSPACE_KEY

Create Service Principal for GitHub Actions

bash
1# Create service principal for GitHub Actions
2SUBSCRIPTION_ID=$(az account show --query id --output tsv)
3
4az ad sp create-for-rbac \
5  --name "sp-fastapi-demo-github" \
6  --role "Contributor" \
7  --scopes "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP" \
8  --sdk-auth

Important: Save the JSON output from the service principal creation. You'll need it for GitHub Actions.

GitHub Actions CI/CD Pipeline

Now let's automate this entire process with GitHub Actions.

GitHub Repository Setup

bash
1# Initialize git and create GitHub repo
2git add .
3git commit -m "Initial commit"
4
5# Create GitHub repository (using GitHub CLI)
6gh repo create fastapi-azure-demo --public --push

Configure GitHub Secrets

Add these secrets to your GitHub repository (Settings > Secrets and variables > Actions):

  • AZURE_CREDENTIALS: The service principal JSON from earlier
  • ACR_NAME: Your container registry name
  • ACR_USERNAME: Container registry username
  • ACR_PASSWORD: Container registry password
  • AZURE_SUBSCRIPTION_ID: Your Azure subscription ID
  • AZURE_RESOURCE_GROUP: Your resource group name
  • KEYVAULT_NAME: Your Key Vault name

GitHub Actions Workflow

Create .github/workflows/deploy.yml:

yaml
1name: Deploy to Azure Container Apps
2
3on:
4  push:
5    branches: [main]
6  pull_request:
7    branches: [main]
8  workflow_dispatch:
9
10env:
11  ACR_NAME: ${{ secrets.ACR_NAME }}
12  IMAGE_NAME: fastapi-demo
13  CONTAINER_APP_NAME: ca-fastapi-demo
14  CONTAINER_APP_ENV: cae-fastapi-demo
15  RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP }}
16
17jobs:
18  test:
19    name: Test Application
20    runs-on: ubuntu-latest
21
22    steps:
23      - name: Checkout code
24        uses: actions/checkout@v4
25
26      - name: Set up Python
27        uses: actions/setup-python@v5
28        with:
29          python-version: "3.11"
30
31      - name: Install dependencies
32        run: |
33          python -m pip install --upgrade pip
34          pip install -r requirements.txt
35          pip install pytest httpx
36
37      - name: Run tests with pytest
38        run: |
39          pytest tests/ -v --tb=short
40
41  build:
42    name: Build and Push Docker Image
43    runs-on: ubuntu-latest
44    needs: test
45    if: github.ref == 'refs/heads/main'
46
47    steps:
48      - name: Checkout code
49        uses: actions/checkout@v4
50
51      - name: Login to Azure
52        uses: azure/login@v1
53        with:
54          creds: ${{ secrets.AZURE_CREDENTIALS }}
55
56      - name: Login to Azure Container Registry
57        run: az acr login --name ${{ secrets.ACR_NAME }}
58
59      - name: Build and push Docker image
60        run: |
61          IMAGE_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
62          LATEST_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:latest
63
64          docker build -t $IMAGE_TAG -t $LATEST_TAG .
65          docker push $IMAGE_TAG
66          docker push $LATEST_TAG
67
68          echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
69
70  deploy:
71    name: Deploy to Azure Container Apps
72    runs-on: ubuntu-latest
73    needs: build
74    if: github.ref == 'refs/heads/main'
75
76    steps:
77      - name: Checkout code
78        uses: actions/checkout@v4
79
80      - name: Login to Azure
81        uses: azure/login@v1
82        with:
83          creds: ${{ secrets.AZURE_CREDENTIALS }}
84
85      - name: Get secrets from Key Vault
86        id: keyvault
87        run: |
88          DATABASE_URL=$(az keyvault secret show \
89            --vault-name ${{ secrets.KEYVAULT_NAME }} \
90            --name DATABASE-URL \
91            --query value --output tsv)
92
93          API_KEY=$(az keyvault secret show \
94            --vault-name ${{ secrets.KEYVAULT_NAME }} \
95            --name API-KEY \
96            --query value --output tsv)
97
98          # Mask secrets in logs
99          echo "::add-mask::$DATABASE_URL"
100          echo "::add-mask::$API_KEY"
101
102          # Set outputs
103          echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_OUTPUT
104          echo "API_KEY=$API_KEY" >> $GITHUB_OUTPUT
105
106      - name: Deploy to Container Apps
107        run: |
108          IMAGE_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
109
110          echo "🚀 Deploying to Azure Container Apps..."
111
112          az containerapp up \
113            --name ${{ env.CONTAINER_APP_NAME }} \
114            --resource-group ${{ env.RESOURCE_GROUP }} \
115            --environment ${{ env.CONTAINER_APP_ENV }} \
116            --image $IMAGE_TAG \
117            --target-port 8000 \
118            --ingress external \
119            --registry-server ${{ secrets.ACR_NAME }}.azurecr.io \
120            --registry-username ${{ secrets.ACR_USERNAME }} \
121            --registry-password ${{ secrets.ACR_PASSWORD }} \
122            --env-vars \
123              DATABASE_URL="${{ steps.keyvault.outputs.DATABASE_URL }}" \
124              API_KEY="${{ steps.keyvault.outputs.API_KEY }}" \
125              ENVIRONMENT=production \
126              VERSION=${{ github.sha }}
127
128      - name: Get deployment URL and test
129        run: |
130          APP_URL=$(az containerapp show \
131            --name ${{ env.CONTAINER_APP_NAME }} \
132            --resource-group ${{ env.RESOURCE_GROUP }} \
133            --query properties.configuration.ingress.fqdn \
134            --output tsv)
135
136          echo "🚀 Application deployed to: https://$APP_URL"
137
138          # Wait for deployment to be ready
139          echo "⏳ Waiting for deployment to be ready..."
140          sleep 30
141
142          # Test the deployment
143          echo "🧪 Testing deployment..."
144          curl -f https://$APP_URL/health || exit 1
145
146          echo "✅ Deployment successful!"
147          echo "🌐 App URL: https://$APP_URL"
148          echo "📊 Health: https://$APP_URL/health"
149          echo "⚙️ Config: https://$APP_URL/config"

Testing the CI/CD Pipeline

Now let's test our complete pipeline:

bash
1# Make a small change to trigger deployment
2echo "# FastAPI Azure Demo" > README.md
3echo "This is a demo FastAPI app deployed to Azure Container Apps." >> README.md
4
5# Commit and push
6git add .
7git commit -m "Add README and trigger deployment"
8git push origin main

Go to your GitHub repository's Actions tab to watch the deployment process. The pipeline will:

1. Test: Run basic tests on the FastAPI application

2. Build: Build and push the Docker image to Azure Container Registry

3. Deploy: Deploy to Azure Container Apps with secrets from Key Vault

Production Enhancements

Enable Autoscaling

Configure HTTP-based autoscaling:

bash
1az containerapp update \
2  --name $CONTAINER_APP_NAME \
3  --resource-group $RESOURCE_GROUP \
4  --scale-rule-name http-rule \
5  --scale-rule-type http \
6  --scale-rule-http-concurrency 50 \
7  --min-replicas 1 \
8  --max-replicas 10

Add Health Probes

bash
1az containerapp update \
2  --name $CONTAINER_APP_NAME \
3  --resource-group $RESOURCE_GROUP \
4  --health-probe-path /health \
5  --health-probe-interval 30 \
6  --health-probe-timeout 5

Monitoring and Debugging

View Application Logs

bash
1# Stream live logs
2az containerapp logs show \
3  --name $CONTAINER_APP_NAME \
4  --resource-group $RESOURCE_GROUP \
5  --follow
6
7# View recent logs
8az containerapp logs show \
9  --name $CONTAINER_APP_NAME \
10  --resource-group $RESOURCE_GROUP \
11  --tail 100

Check Container App Status

bash
1# View app details
2az containerapp show \
3  --name $CONTAINER_APP_NAME \
4  --resource-group $RESOURCE_GROUP \
5  --output table
6
7# View revisions
8az containerapp revision list \
9  --name $CONTAINER_APP_NAME \
10  --resource-group $RESOURCE_GROUP \
11  --output table
12
13# View replicas
14az containerapp replica list \
15  --name $CONTAINER_APP_NAME \
16  --resource-group $RESOURCE_GROUP \
17  --output table

Security Best Practices

Use Managed Identity

Enable system-assigned managed identity for better security:

bash
1# Enable managed identity
2az containerapp identity assign \
3  --name $CONTAINER_APP_NAME \
4  --resource-group $RESOURCE_GROUP \
5  --system-assigned
6
7# Get the identity principal ID
8IDENTITY_ID=$(az containerapp identity show \
9  --name $CONTAINER_APP_NAME \
10  --resource-group $RESOURCE_GROUP \
11  --query principalId --output tsv)
12
13# Grant Key Vault access to the managed identity
14az keyvault set-policy \
15  --name $KEYVAULT_NAME \
16  --object-id $IDENTITY_ID \
17  --secret-permissions get list

Network Security

For production deployments, consider using internal ingress:

bash
1# Update to internal ingress (requires VNet integration)
2az containerapp update \
3  --name $CONTAINER_APP_NAME \
4  --resource-group $RESOURCE_GROUP \
5  --ingress internal

Troubleshooting

Common Issues

1. Container App won't start:

bash
1# Check revision status
2az containerapp revision list \
3  --name $CONTAINER_APP_NAME \
4  --resource-group $RESOURCE_GROUP
5
6# View detailed logs
7az containerapp logs show \
8  --name $CONTAINER_APP_NAME \
9  --resource-group $RESOURCE_GROUP \
10  --tail 50

2. Image pull failures:

bash
1# Verify ACR credentials
2az containerapp show \
3  --name $CONTAINER_APP_NAME \
4  --resource-group $RESOURCE_GROUP \
5  --query properties.configuration.registries
6
7# Test ACR access
8az acr repository list --name $ACR_NAME

3. Key Vault access denied:

bash
1# Check access policies
2az keyvault show \
3  --name $KEYVAULT_NAME \
4  --query properties.accessPolicies
5
6# Verify secrets exist
7az keyvault secret list --vault-name $KEYVAULT_NAME

Conclusion

We've successfully built a complete CI/CD pipeline for deploying FastAPI applications to Azure Container Apps. Our solution includes:

Infrastructure Components:

  • Azure Container Apps: Serverless container hosting with auto-scaling
  • Azure Container Registry: Secure, private container image storage
  • Azure Key Vault: Centralized secrets management
  • Log Analytics: Comprehensive logging and monitoring

CI/CD Pipeline:

  • Automated Testing: Basic application testing before deployment
  • Container Building: Automated Docker image building and pushing
  • Secure Deployment: Automated deployment with secrets from Key Vault
  • Zero-Downtime Updates: Rolling deployments with health checks

Production Features:

  • Auto-scaling based on HTTP traffic
  • Health probes for reliability
  • Secure secrets management
  • Comprehensive logging and monitoring

This architecture provides a solid foundation for deploying production FastAPI applications on Azure with modern DevOps practices. You can extend this setup by adding:

  • Database integration (Azure Database for PostgreSQL/MySQL)
  • Redis caching (Azure Cache for Redis)
  • API Gateway (Azure API Management)
  • Advanced monitoring (Application Insights)
  • Multi-environment deployments (dev/staging/prod)

The complete source code is available in your GitHub repository, and the deployment pipeline will automatically handle future updates when you push changes to the main branch.

Happy deploying! 🚀

Ibrahim Shittu's profile picture

Ibrahim Shittu

Senior Software Engineer passionate about AI, web/mobile development, and building products that make a difference.

Made with ❤️ by Ibrahim Shittu