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:
python --version
docker --version
az --version
git --versionpython --version
docker --version
az --version
git --versionProject Setup
Let's start by creating our project structure and FastAPI application.
Create Project Structure
mkdir fastapi-azure-demo
cd fastapi-azure-demo
git initmkdir fastapi-azure-demo
cd fastapi-azure-demo
git initCreate the following structure:
fastapi-azure-demo/
├── app/
│ └── main.py
├── tests/
│ └── test_main.py
├── .github/
│ └── workflows/
│ └── deploy.yml
├── requirements.txt
├── Dockerfile
├── .dockerignore
├── .gitignore
└── README.mdfastapi-azure-demo/
├── app/
│ └── main.py
├── tests/
│ └── test_main.py
├── .github/
│ └── workflows/
│ └── deploy.yml
├── requirements.txt
├── Dockerfile
├── .dockerignore
├── .gitignore
└── README.mdFastAPI Application
Create app/main.py:
from fastapi import FastAPI
import os
from datetime import datetime
app = FastAPI(title="Azure Demo API", version="1.0.0")
@app.get("/")
def read_root():
return {
"message": "Hello from Azure Container Apps!",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0"
}
@app.get("/health")
def health_check():
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"environment": os.getenv("ENVIRONMENT", "development")
}
@app.get("/config")
def get_config():
# Demonstrate reading secrets from Key Vault (via environment variables)
return {
"database_configured": bool(os.getenv("DATABASE_URL")),
"api_key_configured": bool(os.getenv("API_KEY")),
"environment": os.getenv("ENVIRONMENT", "development")
}from fastapi import FastAPI
import os
from datetime import datetime
app = FastAPI(title="Azure Demo API", version="1.0.0")
@app.get("/")
def read_root():
return {
"message": "Hello from Azure Container Apps!",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0"
}
@app.get("/health")
def health_check():
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"environment": os.getenv("ENVIRONMENT", "development")
}
@app.get("/config")
def get_config():
# Demonstrate reading secrets from Key Vault (via environment variables)
return {
"database_configured": bool(os.getenv("DATABASE_URL")),
"api_key_configured": bool(os.getenv("API_KEY")),
"environment": os.getenv("ENVIRONMENT", "development")
}Test Script
Create tests/test_main.py:
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_root_endpoint():
"""Test the root endpoint returns expected response."""
response = client.get("/")
assert response.status_code == 200
data = response.json()
assert "message" in data
assert "timestamp" in data
assert "version" in data
assert data["message"] == "Hello from Azure Container Apps!"
assert data["version"] == "1.0.0"
def test_health_endpoint():
"""Test the health check endpoint."""
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert "status" in data
assert "timestamp" in data
assert "environment" in data
assert data["status"] == "healthy"
assert data["environment"] == "development"
def test_config_endpoint():
"""Test the configuration endpoint."""
response = client.get("/config")
assert response.status_code == 200
data = response.json()
assert "database_configured" in data
assert "api_key_configured" in data
assert "environment" in data
assert isinstance(data["database_configured"], bool)
assert isinstance(data["api_key_configured"], bool)
def test_invalid_endpoint():
"""Test that invalid endpoints return 404."""
response = client.get("/invalid")
assert response.status_code == 404
if __name__ == "__main__":
# Run tests with simple output when executed directly
print("Running FastAPI tests...\n")
test_functions = [
test_root_endpoint,
test_health_endpoint,
test_config_endpoint,
test_invalid_endpoint
]
passed = 0
failed = 0
for test_func in test_functions:
try:
test_func()
print(f"✅ {test_func.__name__}: PASSED")
passed += 1
except AssertionError as e:
print(f"❌ {test_func.__name__}: FAILED - {e}")
failed += 1
except Exception as e:
print(f"❌ {test_func.__name__}: ERROR - {e}")
failed += 1
print(f"\n{'='*50}")
print(f"Results: {passed} passed, {failed} failed")
print(f"{'='*50}")
if failed > 0:
exit(1)
print("\n🎉 All tests passed!")import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_root_endpoint():
"""Test the root endpoint returns expected response."""
response = client.get("/")
assert response.status_code == 200
data = response.json()
assert "message" in data
assert "timestamp" in data
assert "version" in data
assert data["message"] == "Hello from Azure Container Apps!"
assert data["version"] == "1.0.0"
def test_health_endpoint():
"""Test the health check endpoint."""
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert "status" in data
assert "timestamp" in data
assert "environment" in data
assert data["status"] == "healthy"
assert data["environment"] == "development"
def test_config_endpoint():
"""Test the configuration endpoint."""
response = client.get("/config")
assert response.status_code == 200
data = response.json()
assert "database_configured" in data
assert "api_key_configured" in data
assert "environment" in data
assert isinstance(data["database_configured"], bool)
assert isinstance(data["api_key_configured"], bool)
def test_invalid_endpoint():
"""Test that invalid endpoints return 404."""
response = client.get("/invalid")
assert response.status_code == 404
if __name__ == "__main__":
# Run tests with simple output when executed directly
print("Running FastAPI tests...\n")
test_functions = [
test_root_endpoint,
test_health_endpoint,
test_config_endpoint,
test_invalid_endpoint
]
passed = 0
failed = 0
for test_func in test_functions:
try:
test_func()
print(f"✅ {test_func.__name__}: PASSED")
passed += 1
except AssertionError as e:
print(f"❌ {test_func.__name__}: FAILED - {e}")
failed += 1
except Exception as e:
print(f"❌ {test_func.__name__}: ERROR - {e}")
failed += 1
print(f"\n{'='*50}")
print(f"Results: {passed} passed, {failed} failed")
print(f"{'='*50}")
if failed > 0:
exit(1)
print("\n🎉 All tests passed!")Create requirements.txt:
fastapi==0.109.0
uvicorn[standard]==0.27.0fastapi==0.109.0
uvicorn[standard]==0.27.0Basic Files
Create .gitignore:
__pycache__/
*.py[cod]
*.pyc
.env
.venv
venv/
.DS_Store__pycache__/
*.py[cod]
*.pyc
.env
.venv
venv/
.DS_StoreCreate .dockerignore:
__pycache__
*.pyc
.git
.gitignore
.env
README.md
.venv
venv/__pycache__
*.pyc
.git
.gitignore
.env
README.md
.venv
venv/Containerizing the Application
Create a simple, production-ready Dockerfile:
FROM python:3.11-slim
WORKDIR /app
# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY ./app ./app
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Health check
HEALTHCHECK \
CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]FROM python:3.11-slim
WORKDIR /app
# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY ./app ./app
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Health check
HEALTHCHECK \
CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Test the container locally:
# Build and test locally
docker build -t fastapi-demo .
docker run -d -p 8000:8000 --name test-app fastapi-demo
# Test endpoints
curl http://localhost:8000/
curl http://localhost:8000/health
curl http://localhost:8000/config
# Clean up
docker stop test-app && docker rm test-app# Build and test locally
docker build -t fastapi-demo .
docker run -d -p 8000:8000 --name test-app fastapi-demo
# Test endpoints
curl http://localhost:8000/
curl http://localhost:8000/health
curl http://localhost:8000/config
# Clean up
docker stop test-app && docker rm test-appSetting Up Azure Infrastructure
Now we'll create the Azure resources needed for our deployment.
Login and Setup Variables
# Login to Azure
az login
# Set subscription (replace with your subscription ID)
az account set --subscription "your-subscription-id"
# Define variables
RESOURCE_GROUP="rg-fastapi-demo"
LOCATION="eastus"
ACR_NAME="acrfastapidemo$RANDOM"
KEYVAULT_NAME="kv-fastapi-demo-$RANDOM"
CONTAINER_APP_NAME="ca-fastapi-demo"
CONTAINER_APP_ENV="cae-fastapi-demo"
LOG_WORKSPACE="law-fastapi-demo"
echo "Resource Group: $RESOURCE_GROUP"
echo "ACR Name: $ACR_NAME"
echo "Key Vault: $KEYVAULT_NAME"# Login to Azure
az login
# Set subscription (replace with your subscription ID)
az account set --subscription "your-subscription-id"
# Define variables
RESOURCE_GROUP="rg-fastapi-demo"
LOCATION="eastus"
ACR_NAME="acrfastapidemo$RANDOM"
KEYVAULT_NAME="kv-fastapi-demo-$RANDOM"
CONTAINER_APP_NAME="ca-fastapi-demo"
CONTAINER_APP_ENV="cae-fastapi-demo"
LOG_WORKSPACE="law-fastapi-demo"
echo "Resource Group: $RESOURCE_GROUP"
echo "ACR Name: $ACR_NAME"
echo "Key Vault: $KEYVAULT_NAME"Create Resource Group
az group create \
--name $RESOURCE_GROUP \
--location $LOCATIONaz group create \
--name $RESOURCE_GROUP \
--location $LOCATIONCreate Azure Container Registry
# Create Container Registry
az acr create \
--resource-group $RESOURCE_GROUP \
--name $ACR_NAME \
--sku Basic \
--admin-enabled true
# Get registry credentials
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --query loginServer --output tsv)
ACR_USERNAME=$(az acr credential show --name $ACR_NAME --query username --output tsv)
ACR_PASSWORD=$(az acr credential show --name $ACR_NAME --query passwords[0].value --output tsv)
echo "Registry Server: $ACR_LOGIN_SERVER"
echo "Username: $ACR_USERNAME"
echo "Password: $ACR_PASSWORD"# Create Container Registry
az acr create \
--resource-group $RESOURCE_GROUP \
--name $ACR_NAME \
--sku Basic \
--admin-enabled true
# Get registry credentials
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --query loginServer --output tsv)
ACR_USERNAME=$(az acr credential show --name $ACR_NAME --query username --output tsv)
ACR_PASSWORD=$(az acr credential show --name $ACR_NAME --query passwords[0].value --output tsv)
echo "Registry Server: $ACR_LOGIN_SERVER"
echo "Username: $ACR_USERNAME"
echo "Password: $ACR_PASSWORD"Create Azure Key Vault
# Create Key Vault
az keyvault create \
--name $KEYVAULT_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku standard
# Add example secrets
az keyvault secret set \
--vault-name $KEYVAULT_NAME \
--name "DATABASE-URL" \
--value "postgresql://user:pass@host:5432/mydb"
az keyvault secret set \
--vault-name $KEYVAULT_NAME \
--name "API-KEY" \
--value "super-secret-api-key-12345"
# Verify secrets
az keyvault secret list --vault-name $KEYVAULT_NAME --output table# Create Key Vault
az keyvault create \
--name $KEYVAULT_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku standard
# Add example secrets
az keyvault secret set \
--vault-name $KEYVAULT_NAME \
--name "DATABASE-URL" \
--value "postgresql://user:pass@host:5432/mydb"
az keyvault secret set \
--vault-name $KEYVAULT_NAME \
--name "API-KEY" \
--value "super-secret-api-key-12345"
# Verify secrets
az keyvault secret list --vault-name $KEYVAULT_NAME --output tableCreate Log Analytics Workspace
# Create workspace for Container Apps logging
az monitor log-analytics workspace create \
--resource-group $RESOURCE_GROUP \
--workspace-name $LOG_WORKSPACE \
--location $LOCATION
# Get workspace details
WORKSPACE_ID=$(az monitor log-analytics workspace show \
--resource-group $RESOURCE_GROUP \
--workspace-name $LOG_WORKSPACE \
--query customerId --output tsv)
WORKSPACE_KEY=$(az monitor log-analytics workspace get-shared-keys \
--resource-group $RESOURCE_GROUP \
--workspace-name $LOG_WORKSPACE \
--query primarySharedKey --output tsv)# Create workspace for Container Apps logging
az monitor log-analytics workspace create \
--resource-group $RESOURCE_GROUP \
--workspace-name $LOG_WORKSPACE \
--location $LOCATION
# Get workspace details
WORKSPACE_ID=$(az monitor log-analytics workspace show \
--resource-group $RESOURCE_GROUP \
--workspace-name $LOG_WORKSPACE \
--query customerId --output tsv)
WORKSPACE_KEY=$(az monitor log-analytics workspace get-shared-keys \
--resource-group $RESOURCE_GROUP \
--workspace-name $LOG_WORKSPACE \
--query primarySharedKey --output tsv)Create Container Apps Environment
# Create Container Apps environment
az containerapp env create \
--name $CONTAINER_APP_ENV \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--logs-workspace-id $WORKSPACE_ID \
--logs-workspace-key $WORKSPACE_KEY# Create Container Apps environment
az containerapp env create \
--name $CONTAINER_APP_ENV \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--logs-workspace-id $WORKSPACE_ID \
--logs-workspace-key $WORKSPACE_KEYCreate Service Principal for GitHub Actions
# Create service principal for GitHub Actions
SUBSCRIPTION_ID=$(az account show --query id --output tsv)
az ad sp create-for-rbac \
--name "sp-fastapi-demo-github" \
--role "Contributor" \
--scopes "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP" \
--sdk-auth# Create service principal for GitHub Actions
SUBSCRIPTION_ID=$(az account show --query id --output tsv)
az ad sp create-for-rbac \
--name "sp-fastapi-demo-github" \
--role "Contributor" \
--scopes "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP" \
--sdk-authImportant: 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
# Initialize git and create GitHub repo
git add .
git commit -m "Initial commit"
# Create GitHub repository (using GitHub CLI)
gh repo create fastapi-azure-demo --public --push# Initialize git and create GitHub repo
git add .
git commit -m "Initial commit"
# Create GitHub repository (using GitHub CLI)
gh repo create fastapi-azure-demo --public --pushConfigure 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:
name: Deploy to Azure Container Apps
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
env:
ACR_NAME: ${{ secrets.ACR_NAME }}
IMAGE_NAME: fastapi-demo
CONTAINER_APP_NAME: ca-fastapi-demo
CONTAINER_APP_ENV: cae-fastapi-demo
RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP }}
jobs:
test:
name: Test Application
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest httpx
- name: Run tests with pytest
run: |
pytest tests/ -v --tb=short
build:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Login to Azure Container Registry
run: az acr login --name ${{ secrets.ACR_NAME }}
- name: Build and push Docker image
run: |
IMAGE_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
LATEST_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:latest
docker build -t $IMAGE_TAG -t $LATEST_TAG .
docker push $IMAGE_TAG
docker push $LATEST_TAG
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
deploy:
name: Deploy to Azure Container Apps
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Get secrets from Key Vault
id: keyvault
run: |
DATABASE_URL=$(az keyvault secret show \
--vault-name ${{ secrets.KEYVAULT_NAME }} \
--name DATABASE-URL \
--query value --output tsv)
API_KEY=$(az keyvault secret show \
--vault-name ${{ secrets.KEYVAULT_NAME }} \
--name API-KEY \
--query value --output tsv)
# Mask secrets in logs
echo "::add-mask::$DATABASE_URL"
echo "::add-mask::$API_KEY"
# Set outputs
echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_OUTPUT
echo "API_KEY=$API_KEY" >> $GITHUB_OUTPUT
- name: Deploy to Container Apps
run: |
IMAGE_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
echo "🚀 Deploying to Azure Container Apps..."
az containerapp up \
--name ${{ env.CONTAINER_APP_NAME }} \
--resource-group ${{ env.RESOURCE_GROUP }} \
--environment ${{ env.CONTAINER_APP_ENV }} \
--image $IMAGE_TAG \
--target-port 8000 \
--ingress external \
--registry-server ${{ secrets.ACR_NAME }}.azurecr.io \
--registry-username ${{ secrets.ACR_USERNAME }} \
--registry-password ${{ secrets.ACR_PASSWORD }} \
--env-vars \
DATABASE_URL="${{ steps.keyvault.outputs.DATABASE_URL }}" \
API_KEY="${{ steps.keyvault.outputs.API_KEY }}" \
ENVIRONMENT=production \
VERSION=${{ github.sha }}
- name: Get deployment URL and test
run: |
APP_URL=$(az containerapp show \
--name ${{ env.CONTAINER_APP_NAME }} \
--resource-group ${{ env.RESOURCE_GROUP }} \
--query properties.configuration.ingress.fqdn \
--output tsv)
echo "🚀 Application deployed to: https://$APP_URL"
# Wait for deployment to be ready
echo "⏳ Waiting for deployment to be ready..."
sleep 30
# Test the deployment
echo "🧪 Testing deployment..."
curl -f https://$APP_URL/health || exit 1
echo "✅ Deployment successful!"
echo "🌐 App URL: https://$APP_URL"
echo "📊 Health: https://$APP_URL/health"
echo "⚙️ Config: https://$APP_URL/config"name: Deploy to Azure Container Apps
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
env:
ACR_NAME: ${{ secrets.ACR_NAME }}
IMAGE_NAME: fastapi-demo
CONTAINER_APP_NAME: ca-fastapi-demo
CONTAINER_APP_ENV: cae-fastapi-demo
RESOURCE_GROUP: ${{ secrets.AZURE_RESOURCE_GROUP }}
jobs:
test:
name: Test Application
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest httpx
- name: Run tests with pytest
run: |
pytest tests/ -v --tb=short
build:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Login to Azure Container Registry
run: az acr login --name ${{ secrets.ACR_NAME }}
- name: Build and push Docker image
run: |
IMAGE_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
LATEST_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:latest
docker build -t $IMAGE_TAG -t $LATEST_TAG .
docker push $IMAGE_TAG
docker push $LATEST_TAG
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
deploy:
name: Deploy to Azure Container Apps
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Get secrets from Key Vault
id: keyvault
run: |
DATABASE_URL=$(az keyvault secret show \
--vault-name ${{ secrets.KEYVAULT_NAME }} \
--name DATABASE-URL \
--query value --output tsv)
API_KEY=$(az keyvault secret show \
--vault-name ${{ secrets.KEYVAULT_NAME }} \
--name API-KEY \
--query value --output tsv)
# Mask secrets in logs
echo "::add-mask::$DATABASE_URL"
echo "::add-mask::$API_KEY"
# Set outputs
echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_OUTPUT
echo "API_KEY=$API_KEY" >> $GITHUB_OUTPUT
- name: Deploy to Container Apps
run: |
IMAGE_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
echo "🚀 Deploying to Azure Container Apps..."
az containerapp up \
--name ${{ env.CONTAINER_APP_NAME }} \
--resource-group ${{ env.RESOURCE_GROUP }} \
--environment ${{ env.CONTAINER_APP_ENV }} \
--image $IMAGE_TAG \
--target-port 8000 \
--ingress external \
--registry-server ${{ secrets.ACR_NAME }}.azurecr.io \
--registry-username ${{ secrets.ACR_USERNAME }} \
--registry-password ${{ secrets.ACR_PASSWORD }} \
--env-vars \
DATABASE_URL="${{ steps.keyvault.outputs.DATABASE_URL }}" \
API_KEY="${{ steps.keyvault.outputs.API_KEY }}" \
ENVIRONMENT=production \
VERSION=${{ github.sha }}
- name: Get deployment URL and test
run: |
APP_URL=$(az containerapp show \
--name ${{ env.CONTAINER_APP_NAME }} \
--resource-group ${{ env.RESOURCE_GROUP }} \
--query properties.configuration.ingress.fqdn \
--output tsv)
echo "🚀 Application deployed to: https://$APP_URL"
# Wait for deployment to be ready
echo "⏳ Waiting for deployment to be ready..."
sleep 30
# Test the deployment
echo "🧪 Testing deployment..."
curl -f https://$APP_URL/health || exit 1
echo "✅ Deployment successful!"
echo "🌐 App URL: https://$APP_URL"
echo "📊 Health: https://$APP_URL/health"
echo "⚙️ Config: https://$APP_URL/config"Testing the CI/CD Pipeline
Now let's test our complete pipeline:
# Make a small change to trigger deployment
echo "# FastAPI Azure Demo" > README.md
echo "This is a demo FastAPI app deployed to Azure Container Apps." >> README.md
# Commit and push
git add .
git commit -m "Add README and trigger deployment"
git push origin main# Make a small change to trigger deployment
echo "# FastAPI Azure Demo" > README.md
echo "This is a demo FastAPI app deployed to Azure Container Apps." >> README.md
# Commit and push
git add .
git commit -m "Add README and trigger deployment"
git push origin mainGo 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:
az containerapp update \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--scale-rule-name http-rule \
--scale-rule-type http \
--scale-rule-http-concurrency 50 \
--min-replicas 1 \
--max-replicas 10az containerapp update \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--scale-rule-name http-rule \
--scale-rule-type http \
--scale-rule-http-concurrency 50 \
--min-replicas 1 \
--max-replicas 10Add Health Probes
az containerapp update \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--health-probe-path /health \
--health-probe-interval 30 \
--health-probe-timeout 5az containerapp update \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--health-probe-path /health \
--health-probe-interval 30 \
--health-probe-timeout 5Monitoring and Debugging
View Application Logs
# Stream live logs
az containerapp logs show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--follow
# View recent logs
az containerapp logs show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--tail 100# Stream live logs
az containerapp logs show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--follow
# View recent logs
az containerapp logs show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--tail 100Check Container App Status
# View app details
az containerapp show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--output table
# View revisions
az containerapp revision list \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--output table
# View replicas
az containerapp replica list \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--output table# View app details
az containerapp show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--output table
# View revisions
az containerapp revision list \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--output table
# View replicas
az containerapp replica list \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--output tableSecurity Best Practices
Use Managed Identity
Enable system-assigned managed identity for better security:
# Enable managed identity
az containerapp identity assign \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--system-assigned
# Get the identity principal ID
IDENTITY_ID=$(az containerapp identity show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--query principalId --output tsv)
# Grant Key Vault access to the managed identity
az keyvault set-policy \
--name $KEYVAULT_NAME \
--object-id $IDENTITY_ID \
--secret-permissions get list# Enable managed identity
az containerapp identity assign \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--system-assigned
# Get the identity principal ID
IDENTITY_ID=$(az containerapp identity show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--query principalId --output tsv)
# Grant Key Vault access to the managed identity
az keyvault set-policy \
--name $KEYVAULT_NAME \
--object-id $IDENTITY_ID \
--secret-permissions get listNetwork Security
For production deployments, consider using internal ingress:
# Update to internal ingress (requires VNet integration)
az containerapp update \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--ingress internal# Update to internal ingress (requires VNet integration)
az containerapp update \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--ingress internalTroubleshooting
Common Issues
1. Container App won't start:
# Check revision status
az containerapp revision list \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP
# View detailed logs
az containerapp logs show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--tail 50# Check revision status
az containerapp revision list \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP
# View detailed logs
az containerapp logs show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--tail 502. Image pull failures:
# Verify ACR credentials
az containerapp show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--query properties.configuration.registries
# Test ACR access
az acr repository list --name $ACR_NAME# Verify ACR credentials
az containerapp show \
--name $CONTAINER_APP_NAME \
--resource-group $RESOURCE_GROUP \
--query properties.configuration.registries
# Test ACR access
az acr repository list --name $ACR_NAME3. Key Vault access denied:
# Check access policies
az keyvault show \
--name $KEYVAULT_NAME \
--query properties.accessPolicies
# Verify secrets exist
az keyvault secret list --vault-name $KEYVAULT_NAME# Check access policies
az keyvault show \
--name $KEYVAULT_NAME \
--query properties.accessPolicies
# Verify secrets exist
az keyvault secret list --vault-name $KEYVAULT_NAMEConclusion
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! 🚀