Despliegue en Container Apps (Parte 1)
En este post desplegamos Promptfoo sobre Azure Container Apps desde cero junto con la la gestión segura de credenciales con Key Vault y managed identity, y la configuración de persistencia con Azure File Share para que los resultados de las campañas no se pierdan entre sesiones.
Arquitectura
Antes de empezar con los comandos, vamos a ver la arquitectura de lo que vamos a construir:

Por qué Azure Container Apps: Nos permite desplegar contenedores sin gestionar infraestructura. Con el plan Consumption y min-replicas 1 el container siempre está disponible. La integración nativa con Key Vault a través de managed identity simplifica la gestión de credenciales sin exponer secrets en variables de entorno.
Por qué Azure File Share: Promptfoo almacena su base de datos SQLite en /root/.promptfoo. Sin persistencia externa, todo el historial de campañas desaparece cuando el container se reinicia. El File Share montado en esa ruta garantiza que los datos sobreviven a reinicios y redespliegues.
Por qué Key Vault + Managed Identity: las API keys del modelo target nunca aparecen en código, en la CLI ni en ficheros de configuración. La Container App tiene una system-assigned managed identity con rol Key Vault Secrets User y los secrets se inyectan en tiempo de ejecución.
Por que se llama pyrit: Por limitaciones en mi suscripción de estudiantee no puedo tener más de un ACA en la misma región , y actualmente es la que me permite realizar más pruebas por lo que para este caso que es algo temporal vale , las buenas practicas es cada uno con su resource group correspondiente.
Requisitos
- Azure CLI instalado y autenticado (
az login) - Suscripción Azure activa
- Deployment de Azure OpenAI con GPT-4o disponible
Variables que usaremos a lo largo del proceso defínelas al inicio:
bash
RG_ENV="rg-lab-pyrit"
LOCATION="swedencentral"
ENV_NAME="cae-copyrit-lab"
APP_NAME="promptfoo"
KV_NAME="kv-promptfoo-lab"
STORAGE_NAME="stprompfoolab"
SHARE_NAME="promptfoo-data"
Como digo , el entorno de cae-copyrit-lab es el que usamos en la serie de post anteriores del despliegue de PyRIT versión UI , aquí lo aprovecharemos , si no lo tenéis creado podéis ir al post y ver como lo hicimos, este paso del entorno ya nos lo ahorramos.
Paso 1: Registrar el namespace de Key Vault
Azure for Students no registra todos los namespaces por defecto. Key Vault hay que registrarlo antes de poder crearlo:
bash
az provider register --namespace Microsoft.KeyVault
# Verificar — esperar hasta "Registered"
az provider show --namespace Microsoft.KeyVault --query registrationState -o tsv
Paso 2: Desplegar la Container App
La imagen oficial de Promptfoo está en GitHub Container Registry y no requiere build:
bash
az containerapp create \
--name $APP_NAME \
--resource-group $RG_ENV \
--environment $ENV_NAME \
--image ghcr.io/promptfoo/promptfoo:latest \
--target-port 3000 \
--ingress external \
--min-replicas 1 \
--max-replicas 1 \
--cpu 0.5 --memory 1.0Gi \
--env-vars "PROMPTFOO_DISABLE_TELEMETRY=true"
Si tiene éxito obtendrás la URL pública:
Container app created. Access your app at https://promptfoo.XXXXX.swedencentral.azurecontainerapps.io/
Errores que me he encontrado con la suscripción de Azure for Students
Límite de 1 Container App Environment por suscripción. Si ya tienes un lab desplegado (PyRIT u otro), no puedes crear un environment nuevo. El error es:
(MaxNumberOfGlobalEnvironmentsInSubExceeded) The subscription cannot have
more than 1 Container App Environments.
Solución: reutilizar el environment existente apuntando ENV_NAME al que ya tienes. Las Container Apps de distintos proyectos conviven sin problema en el mismo environment.
Región bloqueada. La mayoría de regiones europeas están restringidas en Students. El error al crear el Log Analytics workspace asociado al environment es el síntoma:
(RequestDisallowedByAzure) Resource was disallowed by Azure.
swedencentral es la región que funciona para este lab es la que os recomiendo usar para todos los recursos.
Paso 3: Crear el Key Vault y asignar permisos
bash
az keyvault create \
--name $KV_NAME \
--resource-group $RG_ENV \
--location $LOCATION
El Key Vault usa RBAC y el usuario de CLI no tiene permisos de escritura por defecto. Hay que asignárselos:
bash
MY_OID=$(az ad user show --id [email protected] --query id -o tsv)
az role assignment create \
--role "Key Vault Secrets Officer" \
--assignee-object-id $MY_OID \
--assignee-principal-type User \
--scope "/subscriptions/TU_SUBSCRIPTION_ID/resourceGroups/$RG_ENV/providers/Microsoft.KeyVault/vaults/$KV_NAME"
sleep 30
Paso 4: Guardar los secrets
bash
az keyvault secret set \
--vault-name $KV_NAME \
--name "azure-openai-key" \
--value "TU_AZURE_OPENAI_KEY"
az keyvault secret set \
--vault-name $KV_NAME \
--name "azure-openai-endpoint" \
--value "URL del endpoint"
Paso 5: Managed Identity y permisos de lectura
bash
az containerapp identity assign \
--name $APP_NAME \
--resource-group $RG_ENV \
--system-assigned
IDENTITY=$(az containerapp show \
--name $APP_NAME \
--resource-group $RG_ENV \
--query identity.principalId -o tsv)
az role assignment create \
--role "Key Vault Secrets User" \
--assignee-object-id $IDENTITY \
--assignee-principal-type ServicePrincipal \
--scope "/subscriptions/SUBSCRIPTION_ID/resourceGroups/$RG_ENV/providers/Microsoft.KeyVault/vaults/$KV_NAME"
Paso 6: Vincular secrets a la Container App
bash
az containerapp secret set \
--name $APP_NAME \
--resource-group $RG_ENV \
--secrets \
"azure-openai-key=keyvaultref:https://$KV_NAME.vault.azure.net/secrets/azure-openai-key,identityref:system" \
"azure-openai-endpoint=keyvaultref:https://$KV_NAME.vault.azure.net/secrets/azure-openai-endpoint,identityref:system"
az containerapp update \
--name $APP_NAME \
--resource-group $RG_ENV \
--set-env-vars \
"AZURE_OPENAI_API_KEY=secretref:azure-openai-key" \
"AZURE_OPENAI_ENDPOINT=secretref:azure-openai-endpoint"
Paso 7: Persistencia con Azure File Share
bash
# Crear el Storage Account y el File Share
az storage account create \
--name $STORAGE_NAME \
--resource-group $RG_ENV \
--location $LOCATION \
--sku Standard_LRS
az storage share create \
--name $SHARE_NAME \
--account-name $STORAGE_NAME
# Vincular el storage al environment
STORAGE_KEY=$(az storage account keys list \
--account-name $STORAGE_NAME \
--resource-group $RG_ENV \
--query "[0].value" -o tsv)
az containerapp env storage set \
--name $ENV_NAME \
--resource-group $RG_ENV \
--storage-name promptfoo-storage \
--azure-file-account-name $STORAGE_NAME \
--azure-file-account-key $STORAGE_KEY \
--azure-file-share-name $SHARE_NAME \
--access-mode ReadWrite
El mount del volumen en la Container App requiere editar el YAML directamente la CLI no expone esta opción con flags:
bash
az containerapp show \
--name $APP_NAME \
--resource-group $RG_ENV \
--output yaml > /tmp/promptfoo-app.yaml
python3 << 'PYEOF'
import yaml
with open('/tmp/promptfoo-app.yaml', 'r') as f:
app = yaml.safe_load(f)
template = app['properties']['template']
template['volumes'] = [{
'name': 'promptfoo-data',
'storageName': 'promptfoo-storage',
'storageType': 'AzureFile'
}]
for container in template['containers']:
if container['name'] == 'promptfoo':
container['volumeMounts'] = [{
'volumeName': 'promptfoo-data',
'mountPath': '/root/.promptfoo'
}]
with open('/tmp/promptfoo-mount.yaml', 'w') as f:
yaml.dump(app, f, default_flow_style=False, allow_unicode=True)
print("OK")
PYEOF
az containerapp update \
--name $APP_NAME \
--resource-group $RG_ENV \
--yaml /tmp/promptfoo-mount.yaml

Verificación
bash
az containerapp revision list \
--name $APP_NAME \
--resource-group $RG_ENV \
--query "[].{Name:name, State:properties.runningState, Health:properties.healthState}" \
-o table
Debe mostrar RunningAtMaxScale y Healthy. Abrimos la URL en el navegador y ya vemos la pantalla de Promptfoo.

Nota sobre autenticación
El lab tal como está desplegado es accesible públicamente sin autenticación. La URL larga y no indexable ofrece protección mínima por oscuridad, suficiente para un lab personal.
La solución correcta para un entorno corporativo es Entra ID SSO, que Azure Container Apps soporta de forma nativa. Con una suscripción M365 E5 estándar se configura con un solo comando. Azure for Students no permite crear app registrations en Entra ID, que es el requisito previo, así que queda como mejora.
En el siguiente post configuramos la primera campaña de red teaming desde la Web UI y analizamos los resultados reales obtenidos contra GPT-4o.
