NAMEibrahim shittu
ROLEsenior software engineer
FOCUSai agents · web · mobile · infrastructure
SINCE2018 · 8y shipping
← writingAIFebruary 3, 202515 min read

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

A comprehensive guide to deploying FastAPI applications using Azure Container Apps, Key Vault for secrets management, Container Registry for images, and GitHub Actions for CI/CD automation.

Ibrahim ShittuIbrahim Shittu·Senior Software Engineer

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
python --version docker --version az --version git --version

Project Setup

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

Create Project Structure

bash
mkdir fastapi-azure-demo cd fastapi-azure-demo git init

Create the following structure:

text
fastapi-azure-demo/ ├── app/ │ └── main.py ├── tests/ │ └── test_main.py ├── .github/ │ └── workflows/ │ └── deploy.yml ├── requirements.txt ├── Dockerfile ├── .dockerignore ├── .gitignore └── README.md

FastAPI Application

Create app/main.py:

python
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:

python
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:

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

Basic Files

Create .gitignore:

text
__pycache__/ *.py[cod] *.pyc .env .venv venv/ .DS_Store

Create .dockerignore:

text
__pycache__ *.pyc .git .gitignore .env README.md .venv venv/

Containerizing the Application

Create a simple, production-ready Dockerfile:

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 --interval=30s --timeout=3s --start-period=5s --retries=3 \ 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:

bash
# 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

Setting Up Azure Infrastructure

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

Login and Setup Variables

bash
# 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

bash
az group create \ --name $RESOURCE_GROUP \ --location $LOCATION

Create Azure Container Registry

bash
# 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

bash
# 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 Log Analytics Workspace

bash
# 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

bash
# 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 Service Principal for GitHub Actions

bash
# 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

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
# 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

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
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:

bash
# 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

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
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 10

Add Health Probes

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

Monitoring and Debugging

View Application Logs

bash
# 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

Check Container App Status

bash
# 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

Security Best Practices

Use Managed Identity

Enable system-assigned managed identity for better security:

bash
# 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

Network Security

For production deployments, consider using internal ingress:

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

Troubleshooting

Common Issues

1. Container App won't start:

bash
# 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

2. Image pull failures:

bash
# 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

3. Key Vault access denied:

bash
# Check access policies az keyvault show \ --name $KEYVAULT_NAME \ --query properties.accessPolicies # Verify secrets exist az 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! 🚀

filed under AI · February 3, 2025reply by email ↗
← previous
My Journey Building Fabrio: From First Engineering Hire to Serving World-Class Universities
next →
Using Predictive Analytics for Energy Optimization
// get in touch

Have a problem worth solving? I'd like to hear about it.

© 2026 ibrahim shittushipping since 2018last updated Apr 2026