sdcasas.dev

Cómo usar Managed Identity con FastAPI en Azure

Guía práctica para usar Azure Managed Identity en un backend FastAPI, eliminando secretos de la aplicación por completo.

3 min read

El problema

Si alguna vez pusiste un connection string de Azure Storage directo en tu código o en un .env que terminó en el repo, este post es para vos.

Managed Identity es el mecanismo de Azure para que una aplicación se autentique con otros servicios sin credenciales hardcodeadas. Tu App Service, Container App o Function obtiene una identidad en Azure AD y con esa identidad accede a Key Vault, Storage, Service Bus, etc.

Cómo funciona

App Service (con Managed Identity)

    │  "Soy el app service X, quiero un token para Storage"

Azure AD (verifica identidad del recurso)

    │  Token JWT válido

Azure Blob Storage (valida el token, autoriza la operación)

No hay contraseñas. No hay tokens que rotar. No hay secretos en variables de entorno (en teoría).

Setup en Azure

Primero, habilitar la Managed Identity en el App Service:

az webapp identity assign \
  --name my-fastapi-app \
  --resource-group my-rg

Eso devuelve un principalId. Ahora darle acceso al Storage:

az role assignment create \
  --assignee <principalId> \
  --role "Storage Blob Data Contributor" \
  --scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<storage>

FastAPI: código real

Instalar las dependencias:

pip install azure-identity azure-storage-blob

Ahora el cliente que funciona tanto local (con tu usuario de Azure CLI) como en producción (con Managed Identity):

from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient

# DefaultAzureCredential prueba en orden:
# 1. Environment variables (desarrollo local con service principal)
# 2. Workload Identity (Kubernetes)
# 3. Managed Identity (Azure App Service, Container Apps, etc.)
# 4. Azure CLI (tu máquina local)
# 5. Visual Studio Code, etc.

def get_blob_client(account_url: str) -> BlobServiceClient:
    credential = DefaultAzureCredential()
    return BlobServiceClient(account_url=account_url, credential=credential)

En tu app FastAPI podés usar dependency injection:

from functools import lru_cache
from fastapi import Depends
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
import os

STORAGE_ACCOUNT_URL = os.environ["AZURE_STORAGE_ACCOUNT_URL"]

@lru_cache
def get_credential() -> DefaultAzureCredential:
    return DefaultAzureCredential()

def get_blob_service(
    credential: DefaultAzureCredential = Depends(get_credential),
) -> BlobServiceClient:
    return BlobServiceClient(
        account_url=STORAGE_ACCOUNT_URL,
        credential=credential
    )

# En tu router
from fastapi import APIRouter

router = APIRouter()

@router.get("/files/{blob_name}")
async def download_file(
    blob_name: str,
    blob_service: BlobServiceClient = Depends(get_blob_service),
):
    container = blob_service.get_container_client("my-container")
    blob = container.get_blob_client(blob_name)
    data = blob.download_blob().readall()
    return {"size": len(data)}

Desarrollo local

Para que funcione en tu máquina sin Managed Identity:

# Login con Azure CLI
az login

# Setear el tenant si tenés varios
az account set --subscription <sub-id>

DefaultAzureCredential va a usar tu sesión de CLI automáticamente. No necesitás ninguna variable de entorno extra.

Qué evitar

  • ❌ No uses ManagedIdentityCredential directamente — hace difícil el desarrollo local
  • ❌ No guardes el client_secret de un Service Principal en el código
  • ❌ No pongas connection strings en variables de entorno cuando podés usar Managed Identity
  • ✅ Usá DefaultAzureCredential siempre — funciona en todos los contextos

Key Vault también

Si necesitás secretos que no tienen RBAC nativo de Azure (ej: una API key de terceros), ponelos en Key Vault y accedé con Managed Identity:

from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential

KEY_VAULT_URL = os.environ["AZURE_KEY_VAULT_URL"]

def get_secret(secret_name: str) -> str:
    credential = DefaultAzureCredential()
    client = SecretClient(vault_url=KEY_VAULT_URL, credential=credential)
    return client.get_secret(secret_name).value

Con esto eliminás la necesidad de tener ningún secreto en el código o en las variables de entorno de la aplicación.