Problema

En entornos donde los equipos gestionan infraestructura como código con Terraform dentro de Azure DevOps, es frecuente que los pipelines fallen de forma intermitente o constante. El patrón típico incluye errores de autorización (403, AccessDenied), fallos al ejecutar terraform plan o apply, y mensajes que hacen referencia a recursos tanto de Azure como de AWS. El problema no se limita a un proyecto concreto; ocurre siempre que se combinan:

  • Variables de entorno que transportan credenciales.
  • Service Connections configurados con permisos insuficientes.
  • Políticas de IAM que cambian sin que el pipeline se actualice.

El síntoma más reconocible es un job que se detiene en la fase de Terraform con mensajes como “The client ‘xxxx’ with object id ‘yyyy’ does not have authorization to perform action ‘Microsoft.Resources/subscriptions/resourceGroups/read’” o “AccessDenied: User: arn:aws:iam::123456789012:user/ci-cd does not have permission to perform: ec2:DescribeInstances”. El pipeline parece estar bien estructurado, pero la ejecución nunca llega a completar.

Causa

1. Service Connection mal alineada con el alcance requerido

Azure DevOps usa Service Connections para abstraer credenciales. Si la conexión está limitada a un resource group pero el Terraform intenta crear recursos en otro, Azure rechaza la solicitud. Lo mismo ocurre en AWS: un Service Connection basado en un role de IAM que no incluye las políticas necesarias provocará AccessDenied.

2. Variables de entorno sobrescritas o caducadas

Muchas organizaciones almacenan secretos en Azure Key Vault o AWS Secrets Manager y los inyectan al pipeline. Si la variable que contiene el client secret o el access key expira, Terraform falla al autenticarse. En pipelines largos, la expiración puede ocurrir entre la fase de terraform init y apply.

3. Cambios de política fuera del control del pipeline

Equipos de seguridad suelen actualizar políticas de IAM sin notificar a los propietarios de los pipelines. Un nuevo deny o la eliminación de un role assignment rompe la cadena de confianza.

4. Estado de Terraform bloqueado o inconsistente

Cuando varios pipelines comparten el mismo backend (por ejemplo, Azure Storage o S3), un lock pendiente o un estado corrupto puede generar errores que se manifiestan como problemas de permiso, aunque la raíz sea la concurrencia.

5. Versiones de proveedores desincronizadas

Si el required_providers del proyecto especifica una versión que no coincide con la versión instalada en el agente, el provider puede intentar usar una API que ya no está disponible para la cuenta actual, resultando en errores de autorización.

Solución

Una estrategia modular que cubra los puntos anteriores suele resolver la mayoría de los fallos. El proceso se divide en tres fases: validación de credenciales, revisión de permisos y sanidad del backend.

1. Validar credenciales antes de ejecutar Terraform

Añade un paso temprano en el pipeline que intente obtener un token de Azure y una llamada simple a AWS STS. Si cualquiera falla, aborta el job y muestra el mensaje de error real.

# Azure token test
az account get-access-token --resource https://management.azure.com/ >/dev/null 2>&1 || { echo "Azure token inválido"; exit 1; }

# AWS STS test
aws sts get-caller-identity >/dev/null 2>&1 || { echo "Credenciales AWS inválidas"; exit 1; }

2. Auditar los Service Connections

  • En Azure DevOps, abre la Service Connection y verifica que el Scope incluya todos los subscription y resource groups que el Terraform necesita.
  • En AWS, revisa el role asociado y asegúrate de que la política incluya acciones como ec2:*, s3:*, iam:PassRole, según lo requiera el módulo.

Una práctica útil es exportar la lista de permisos actuales y compararla con un archivo de referencia (permissions.json). Si hay diferencias, actualiza la Service Connection o la política IAM.

3. Centralizar la gestión de secretos

Utiliza Azure Key Vault o AWS Secrets Manager con rotación automática y referencia los secretos mediante la sintaxis de Azure DevOps ($(mySecret)). Evita hardcodear valores en variables de pipeline; en su lugar, usa variable groups vinculados a los vaults.

4. Desbloquear y validar el backend de estado

Antes de terraform apply, ejecuta:

terraform init -backend-config="lock_timeout=300s"
terraform state list >/dev/null 2>&1 || { echo "Estado corrupto o lock pendiente"; exit 1; }

Si el comando anterior falla, libera el lock manualmente (az storage blob delete o aws s3 rm) y verifica la integridad del archivo terraform.tfstate.

5. Sincronizar versiones de providers

Mantén un archivo versions.tf con versiones exactas y usa terraform init -upgrade solo cuando sea necesario. En el pipeline, instala la versión de Terraform que coincide con el proyecto:

tfenv install $(cat .terraform-version)
tfenv use $(cat .terraform-version)

6. Implementar pruebas de permisos como parte del CI

Crea un módulo de Terraform que solo declare los recursos de IAM necesarios y ejecuta terraform plan -target=module.permissions. Si el plan falla, el pipeline ya ha detectado la falta de permisos antes de tocar la infraestructura real.

Cuándo aplicar esta solución

  • Síntomas: errores de autorización en cualquier fase de Terraform, bloqueos de estado, o fallos inesperados después de una actualización de política.
  • Entorno: pipelines de Azure DevOps que despliegan tanto en Azure como en AWS, usando Service Connections y secretos externos.
  • No aplica: cuando el error proviene de un bug del provider o de una incompatibilidad de versión de Terraform que ya está documentada y requiere un downgrade. En esos casos, la solución pasa por alinear versiones, no por revisar IAM.

Código

# Paso 1: validar credenciales
az account get-access-token --resource https://management.azure.com/ >/dev/null 2>&1 || { echo "Azure token inválido"; exit 1; }
aws sts get-caller-identity >/dev/null 2>&1 || { echo "Credenciales AWS inválidas"; exit 1; }

# Paso 2: inicializar Terraform con backend seguro
terraform init -backend-config="lock_timeout=300s"

# Paso 3: comprobar estado y lock
terraform state list >/dev/null 2>&1 || { echo "Estado corrupto o lock pendiente"; exit 1; }

# Paso 4: plan de permisos
terraform plan -target=module.permissions

# Paso 5: aplicar solo si el plan de permisos pasa
if terraform plan -target=module.permissions | grep -q "No changes"; then
  terraform apply -auto-approve
else
  echo "Fallo en permisos, abortando"
  exit 1
fi

Verificación

  1. Ejecución limpia: el pipeline debe llegar al job de terraform apply sin abortar en los pasos de validación.
  2. Revisión de logs: busca en los logs de Azure DevOps la ausencia de mensajes AccessDenied o AuthorizationFailed.
  3. Estado del backend: confirma que el archivo terraform.tfstate se actualiza y que no existen locks residuales (terraform state list muestra recursos esperados).
  4. Prueba de idempotencia: ejecuta nuevamente el pipeline; el segundo run debe terminar con “No changes” en el plan, indicando que los permisos son correctos y el estado está consistente.

Notas adicionales

  • Rotación de secretos: si tu organización rota secrets cada 30 días, programa una tarea de pipeline que refresque los variable groups antes de la validación.
  • Políticas de denegación explícita: Azure y AWS pueden tener deny a nivel de management group o SCP que anulan permisos concedidos en roles. Usa az role assignment list y aws iam simulate-principal-policy para detectar esas capas ocultas.
  • Auditoría continua: habilita Azure Monitor y CloudTrail para capturar eventos de autorización fallidos; correlaciona esos logs con los IDs de build para identificar rápidamente la causa raíz.
  • Múltiples agentes: si usas agentes auto‑hosted, verifica que la versión del CLI (az, aws) coincida con la esperada; diferencias de versión pueden cambiar el formato de los tokens y generar errores aparentemente de IAM.