Azure DevOps a AWS con AWS Toolkit + Service Connection
- victoriagimenez5
- hace 3 días
- 5 Min. de lectura

Basado en la implementación real del proyecto + referencias oficialesAzure DevOps NO soporta OIDC nativo hacia AWS, aunque a nivel teórico se puede emitir un token,
NO es utilizable para escritura,
NO funciona con SigV4,
NO es un método soportado oficialmente por AWS.
Por eso:
Tuvimos que instalar el AWS Toolkit
Tuvimos que crear un AWS Service Connection
Recién ahí Azure Pipelines pudo escribir en AWS (ECR/Mira, S3, Terraform, etc.)
Esto es exactamente lo que hicimos en un proyecto y coincide 100% con ambos vendors (AWS y Microsoft Azure).
1. Verificación oficial: Azure DevOps NO soporta federación OIDC hacia AWS
Microsoft lo dice en forma explícita:
Azure DevOps AWS Service Connection → Requiere Access Key o STS con AWS Toolkit
Texto clave:
“To connect to AWS, you must provide an AWS Access Key ID and Secret Access Key.”
No menciona OIDC en ningún lugar.
AWS también confirma que Azure DevOps se conecta vía AWS Toolkit, no vía OIDC:
“Authentication is performed using AWS service connections… configured with Access Keys or used to assume roles.”
2. Instalar el AWS Toolkit en Azure DevOps
(Exactamente como hicimos con el cliente)
Ir a Azure DevOps.
Navegar a Organization Settings → Extensions → Browse Marketplace.
Buscar “AWS Toolkit for Azure DevOps”.
Instalarlo en la organización o en el proyecto.

Documentación oficial:
3. Crear un AWS Service Connection (paso crítico)
Una vez instalado el AWS Toolkit, Azure DevOps habilita un nuevo tipo de conexión llamado AWS, que es necesario para que los pipelines puedan autenticarse correctamente contra AWS usando AssumeRole y generar firmas SigV4 para operaciones de escritura (ECR, S3, Terraform, ECS, etc.).
3.1. Crear una nueva Service Connection
Ir a:
Project Settings → Service connections → New service connection
En la ventana emergente seleccionar:
AWS (provisto por AWS Toolkit)

3.2. Completar los datos requeridos por el AWS Toolkit
El formulario pedirá:
Role ARN que Azure DevOps debe asumir
External ID (si se utiliza en la Trust Policy, recomendado)
Default AWS Region
Connection name
Scope: Project-scoped o Organization-scoped
Luego hacer clic en Verify.
Si la trust policy está correcta en AWS, la conexión mostrará:
Verified successfully
Esto valida que Azure DevOps puede invocar sts:AssumeRole.


Nota: La opción “Use OIDC” que aparece en el formulario no establece un flujo de federación OIDC completo hacia AWS. Azure DevOps no soporta OIDC para firmar solicitudes AWS SigV4. Por eso, aunque el campo aparece disponible, NO debe usarse para este escenario y la forma soportada es mediante AssumeRole a través del AWS Toolkit.
3.3. Dónde ver la Service Connection una vez creada
Una vez creada, aparece listada en:
Project Settings → Service connections
Aquí se pueden ver:
Tipo de conexión
Nombre asignado
Descripción
Historial de uso
Configuración de aprobaciones

4. Crear un IAM Role para Azure DevOps (AssumeRole)
Esto sí lo hicimos en AWS.
La trust policy fue de este estilo:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<ACCOUNT_ID>:user/<TOOLKIT-USER>"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "<EXTERNAL_ID_GENERADO>"
}
}
}
]
}Nota: <TOOLKIT-USER> es un IAM User creado por el cliente únicamente para ser utilizado por el AWS Toolkit dentro de Azure DevOps. No es generado automáticamente.
4.1. Código de terraform para crear la policy
# Policy para ECR Push desde Azure DevOps
resource "aws_iam_role_policy" "azure_devops_ecr_policy" {
name = "ECR-Push-Policy"
role = aws_iam_role.azure_devops_ecr.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# ECR push
{
Effect = "Allow"
Action = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
]
Resource = "*"
},
# STS caller identity (para validación en pipeline)
{
Effect = "Allow"
Action = [
"sts:GetCallerIdentity"
]
Resource = "*"
},
# SSM Read-only parameters
{
Effect = "Allow"
Action = [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
]
Resource = [
"arn:aws:ssm:${local.region}:${data.aws_caller_identity.current.id}:parameter/${local.project}/${local.environment}/*",
"arn:aws:ssm:${local.region}:${data.aws_caller_identity.current.id}:parameter/${local.project}-star/${local.environment}/*"
]
},
# S3 as needed
{
Effect = "Allow"
Action = [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket",
"s3:GetBucketLocation"
]
Resource = [
"arn:aws:s3:::los-archivos-data-temp",
"arn:aws:s3:::los-archivos-data-temp/*"
]
}
]
})
}5. Paso 4 – Asignar permisos al Role para que Azure pueda escribir en AWS
Ejemplos usados por nosotros:
Ejemplo de Permisos para ECR:
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
],
"Resource": "*"
}Permisos para Terraform:
IAM
S3 backend
DynamoDB Lock
ECS/ECR updates
6. Paso 5 – Consumir la Service Connection desde YAML
Ejemplo típico:
- task: AWSCLI@1
inputs:
awsCredentials: 'aws-connection-name'
regionName: 'us-east-1'
awsCommand: 'sts'
awsSubCommand: 'get-caller-identity'
displayName: "Validar identidad en AWS"Esto fue lo que usamos para validar la conexión.

7. Ejemplo Sintetizado: Build & Push a AWS ECR usando la Service Connection
El siguiente pipeline muestra la forma recomendada de construir una imagen Docker y publicarla en AWS ECR utilizando la AWS Service Connection, sin Access Keys ni IAM Users. Todo se basa en AssumeRole a través del AWS Toolkit.:
# Ejemplo simplificado de Build & Push a AWS ECR con Azure DevOps
trigger:
branches:
include:
- master
pool:
vmImage: 'ubuntu-latest'
variables:
- name: AWS_REGION
value: 'eu-central-1'
- name: ECR_ACCOUNT_ID
value: '123456789'
- name: IMAGE_NAME
value: 'prod/webapp'
steps:
- checkout: self
# Obtener token de SSM (opcional)
- task: AWSShellScript@1
displayName: 'Fetch NuGet Token from SSM'
inputs:
awsCredentials: 'AWS-serviceConnection'
regionName: '$(AWS_REGION)'
scriptType: 'inline'
inlineScript: |
TOKEN=$(aws ssm get-parameter --name "/serenity/production/VSS_NUGET_ACCESSTOKEN" --with-decryption --query 'Parameter.Value' --output text)
echo "##vso[task.setvariable variable=NUGET_TOKEN;issecret=true]${TOKEN}"
# Setear tag corto basado en el commit
- script: |
SHORT_SHA=$(echo "$(Build.SourceVersion)" | cut -c1-7)
echo "##vso[task.setvariable variable=IMAGE_TAG]${SHORT_SHA}"
displayName: "Set image tag"
# Build & Push a ECR
- task: AWSShellScript@1
displayName: 'Build & Push Docker Image to ECR'
inputs:
awsCredentials: 'AWS-serviceConnection'
regionName: '$(AWS_REGION)'
scriptType: 'inline'
inlineScript: |
set -euxo pipefail
REGISTRY="${ECR_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
IMAGE_URI="${REGISTRY}/${IMAGE_NAME}"
# Login a ECR usando STS + Role federado (sin credenciales)
aws ecr get-login-password --region "${AWS_REGION}" | docker login --username AWS --password-stdin "${REGISTRY}"
# Build
docker build -f Dockerfile.webapp -t "${IMAGE_URI}:${IMAGE_TAG}" .
# Tags
docker tag "${IMAGE_URI}:${IMAGE_TAG}" "${IMAGE_URI}:latest"
# Push
docker push "${IMAGE_URI}:${IMAGE_TAG}"
docker push "${IMAGE_URI}:latest"
env:
IMAGE_TAG: $(IMAGE_TAG)
NUGET_TOKEN: $(NUGET_TOKEN)7.1 Versión súper reducida (ideal si querés dejarlo en un solo bloque al final)
# Build & Push a AWS ECR usando AWS Toolkit + Service Connection
- task: AWSShellScript@1
inputs:
awsCredentials: 'AWS-serviceConnection'
regionName: 'eu-central-1'
scriptType: 'inline'
inlineScript: |
set -euxo pipefail
REGISTRY="123456789.dkr.ecr.eu-central-1.amazonaws.com"
IMAGE="${REGISTRY}/prod/webapp"
TAG=$(echo "$(Build.SourceVersion)" | cut -c1-7)
aws ecr get-login-password --region eu-central-1 \
| docker login --username AWS --password-stdin "${REGISTRY}"
docker build -t "${IMAGE}:${TAG}" .
docker push "${IMAGE}:${TAG}"
docker tag "${IMAGE}:${TAG}" "${IMAGE}:latest"
docker push "${IMAGE}:latest"La Seguridad primero
En resumen, este enfoque reproduce fielmente las mejores prácticas de seguridad recomendadas por AWS y la industria: evitar credenciales estáticas, eliminar IAM Users y reemplazar toda autenticación manual por federación basada en identidades. Al utilizar OIDC junto con el AWS Toolkit y un rol dedicado para Azure DevOps, los pipelines operan con credenciales temporales generadas por STS, firmadas automáticamente y con un alcance estrictamente controlado. Esto reduce la superficie de ataque, mejora la trazabilidad en CloudTrail y elimina por completo la necesidad de almacenar secretos sensibles en Azure DevOps.
En definitiva, este modelo no solo incrementa la seguridad del ciclo de despliegue, sino que también simplifica su operación diaria y se alinea directamente con el pilar Security del AWS Well-Architected Framework.
Nota sobre tokens de NuGet (para .NET en Azure DevOps)
En escenarios donde la aplicación se basa en .NET algo común en los entornos microsoft, Azure DevOps requiere un token privado de NuGet para restaurar paquetes desde su feed privado (nuget.org, Azure Artifacts, etc.). Este token no forma parte del proceso de autenticación contra AWS y debe obtenerse desde otro origen seguro. En nuestro pipeline, adoptamos una práctica coherente con el modelo “no-secrets-in-pipelines”:
El token se almacena en AWS Systems Manager Parameter Store, y Azure DevOps lo recupera dinámicamente a través del rol federado usando SSM GetParameter.
De esta manera, incluso los artefactos propios del ecosistema .NET pueden consumirse sin exponer secretos, manteniendo plena coherencia con la arquitectura de autenticación sin credenciales estáticas.

Silvio Depetri
Cloud Engineer



