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:
1python --version
2docker --version
3az --version
4git --version
Project Setup
Let's start by creating our project structure and FastAPI application.
Create Project Structure
1mkdir fastapi-azure-demo
2cd fastapi-azure-demo
3git init
Create the following structure:
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
:
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
:
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
:
1fastapi==0.109.0
2uvicorn[standard]==0.27.0
Basic Files
Create .gitignore
:
1__pycache__/
2*.py[cod]
3*.pyc
4.env
5.venv
6venv/
7.DS_Store
Create .dockerignore
:
1__pycache__
2*.pyc
3.git
4.gitignore
5.env
6README.md
7.venv
8venv/
Containerizing the Application
Create a simple, production-ready 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 \
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:
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
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
1az group create \
2 --name $RESOURCE_GROUP \
3 --location $LOCATION
Create Azure Container Registry
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
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
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
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
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
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
:
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:
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:
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
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
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
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:
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:
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:
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:
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:
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! 🚀