Aller au contenu

CodeBuild inter-comptes — builder une fois en CI, déployer dans N comptes

Builder les artefacts dans un compte CI partagé et déployer dans des comptes par environnement. Assomption de rôle IAM, partage de clé KMS, réplication ECR, politiques S3.

L’organisation AWS de l’équipe a quatre comptes — shared-ci, dev, staging, prod. Le pipeline de build s’exécute actuellement trois fois : une par environnement, chacune dans son propre compte, chacune pullant la même image Docker de base, exécutant les mêmes tests, produisant des artefacts qui devraient être identiques mais ne le sont pas toujours. La bonne forme, c’est un seul build dans shared-ci qui produit un seul artefact, et trois jobs de déploiement qui promeuvent ce même artefact à travers les comptes. Cet article est la plomberie IAM, KMS, ECR et S3 qui le fait fonctionner.

Ce qui se passe

Le pattern « build une fois, déploie plusieurs fois » repose sur quatre pièces d’accès inter-comptes :

  1. Assomption de rôle IAM — CodeBuild dans shared-ci assume un rôle de déploiement dans dev/staging/prod pour y appliquer les changements.
  2. Partage de clé KMS — les artefacts CI chiffrés avec une CMK dans shared-ci doivent être déchiffrables par les comptes cibles.
  3. Accès ECR inter-comptes — l’image Docker poussée dans l’ECR de shared-ci doit être pullable par ECS/EKS dans les comptes cibles. Soit via la réplication ECR (une image, copiée dans les registres par compte), soit via les politiques de dépôt ECR (image unique, multiples permissions de pull).
  4. Politiques de bucket S3 — quand CodePipeline traverse les comptes, le bucket d’artefacts doit autoriser le rôle de pipeline du compte cible à le lire.

Le pattern qui passe à l’échelle : les artefacts vivent dans le compte CI, sont chiffrés avec la clé KMS du compte CI, et les comptes cibles assument un rôle qui a les permissions de déchiffrement sur cette clé et de lecture sur le bucket. La promotion, c’est « redéployer avec le rôle ciblant le compte prod », pas « rebuilder pour prod ».

La promotion ressemble à :

  • shared-ci build → produit imageDetail.json, image ECR à 123-ci.dkr.ecr.us-east-1.amazonaws.com/web:abc1234
  • déploiement dev → assume arn:aws:iam::111-dev:role/PipelineDeployRole, applique la mise à jour du service ECS pointant vers l’image CI
  • déploiement staging → assume arn:aws:iam::222-staging:role/PipelineDeployRole, même image
  • déploiement prod → assume arn:aws:iam::333-prod:role/PipelineDeployRole, même image

Le même SHA d’image circule du build à la prod. L’auditabilité est simple — aws ecr describe-images dans shared-ci montre quel SHA a été déployé où, avec le pushedAt de l’image et l’heure de déploiement comme les deux seules dates qui comptent.

Comment vérifier

La plomberie IAM minimum. Dans le compte cible (prod montré ; même pattern pour dev/staging), créez le rôle de déploiement :

aws iam create-role \
  --role-name PipelineDeployRole \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::CI-ACCOUNT-ID:role/CodeBuildServiceRole" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "prod-deploy-2026" }
      }
    }]
  }'

L’ExternalId est la mitigation canonique du confused deputy — même si le rôle du compte CI est compromis, l’attaquant ne peut pas assumer le rôle cible sans connaître l’ID externe.

Attachez les permissions du rôle de déploiement — pour un service basé sur ECS :

aws iam attach-role-policy \
  --role-name PipelineDeployRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

aws iam put-role-policy \
  --role-name PipelineDeployRole \
  --policy-name DeployServices \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "ecs:UpdateService",
          "ecs:DescribeServices",
          "ecs:RegisterTaskDefinition"
        ],
        "Resource": "*"
      },
      {
        "Effect": "Allow",
        "Action": "iam:PassRole",
        "Resource": "arn:aws:iam::PROD-ACCOUNT-ID:role/ecsTaskExecutionRole"
      }
    ]
  }'

Dans le compte CI, le rôle CodeBuild doit être autorisé à assumer chaque rôle cible :

aws iam put-role-policy \
  --role-name CodeBuildServiceRole \
  --policy-name AssumeDeployRoles \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": [
        "arn:aws:iam::DEV-ACCOUNT-ID:role/PipelineDeployRole",
        "arn:aws:iam::STAGING-ACCOUNT-ID:role/PipelineDeployRole",
        "arn:aws:iam::PROD-ACCOUNT-ID:role/PipelineDeployRole"
      ]
    }]
  }'

Partage KMS — la CMK qui chiffre le bucket d’artefacts dans shared-ci doit laisser les rôles des comptes cibles déchiffrer :

aws kms put-key-policy \
  --key-id arn:aws:kms:us-east-1:CI-ACCOUNT-ID:key/abc12345-... \
  --policy-name default \
  --policy '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "EnableIAMUserPermissions",
        "Effect": "Allow",
        "Principal": { "AWS": "arn:aws:iam::CI-ACCOUNT-ID:root" },
        "Action": "kms:*",
        "Resource": "*"
      },
      {
        "Sid": "AllowCrossAccountDecrypt",
        "Effect": "Allow",
        "Principal": {
          "AWS": [
            "arn:aws:iam::DEV-ACCOUNT-ID:role/PipelineDeployRole",
            "arn:aws:iam::STAGING-ACCOUNT-ID:role/PipelineDeployRole",
            "arn:aws:iam::PROD-ACCOUNT-ID:role/PipelineDeployRole"
          ]
        },
        "Action": ["kms:Decrypt", "kms:DescribeKey"],
        "Resource": "*"
      }
    ]
  }'

Le bucket S3 d’artefacts dans shared-ci a besoin d’une politique accordant la lecture aux principaux des comptes cibles :

aws s3api put-bucket-policy \
  --bucket shared-ci-pipeline-artifacts \
  --policy '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::DEV-ACCOUNT-ID:role/PipelineDeployRole",
          "arn:aws:iam::STAGING-ACCOUNT-ID:role/PipelineDeployRole",
          "arn:aws:iam::PROD-ACCOUNT-ID:role/PipelineDeployRole"
        ]
      },
      "Action": ["s3:GetObject", "s3:GetObjectVersion", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::shared-ci-pipeline-artifacts",
        "arn:aws:s3:::shared-ci-pipeline-artifacts/*"
      ]
    }]
  }'

ECR — l’image vit dans le compte CI. Les comptes cibles la pullent. Soit on la réplique (une image, des copies dans chaque ECR cible), soit on partage le dépôt source :

aws ecr set-repository-policy \
  --repository-name web \
  --policy-text '{
    "Version": "2012-10-17",
    "Statement": [{
      "Sid": "CrossAccountPull",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::DEV-ACCOUNT-ID:root",
          "arn:aws:iam::STAGING-ACCOUNT-ID:root",
          "arn:aws:iam::PROD-ACCOUNT-ID:root"
        ]
      },
      "Action": [
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability"
      ]
    }]
  }'

Dans la définition de tâche ECS du compte cible, référencez directement l’URL ECR du compte CI : 123-ci.dkr.ecr.us-east-1.amazonaws.com/web:abc1234. Le rôle d’exécution de tâche ECS dans le compte cible a besoin de ecr:GetAuthorizationToken (au niveau du compte, pas d’ARN de ressource possible) et ecr:BatchGetImage sur le dépôt source. La politique du dépôt source ci-dessus accorde la seconde.

Dans le buildspec, l’étape de déploiement assume le rôle cible et agit comme lui :

post_build:
  commands:
    - |
      ROLE_OUTPUT=$(aws sts assume-role \
        --role-arn arn:aws:iam::PROD-ACCOUNT-ID:role/PipelineDeployRole \
        --role-session-name codebuild-deploy-${CODEBUILD_BUILD_NUMBER} \
        --external-id prod-deploy-2026)
      export AWS_ACCESS_KEY_ID=$(echo $ROLE_OUTPUT | jq -r .Credentials.AccessKeyId)
      export AWS_SECRET_ACCESS_KEY=$(echo $ROLE_OUTPUT | jq -r .Credentials.SecretAccessKey)
      export AWS_SESSION_TOKEN=$(echo $ROLE_OUTPUT | jq -r .Credentials.SessionToken)
    - aws ecs update-service
        --cluster prod-web
        --service api
        --task-definition api:$NEW_REVISION
        --force-new-deployment

Le unset des credentials de session entre comptes compte — plusieurs déploiements dans un seul buildspec doivent ré-assumer par cible pour éviter la contamination croisée :

unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN

La correction

Le pattern qui passe à l’échelle d’une organisation à 10 comptes :

  1. Un compte de build, N comptes de déploiement. N’exécutez pas CodeBuild dans chaque compte cible. Le code source est buildé une fois. La promotion, c’est un redéploiement du même artefact, pas une reconstruction.

  2. Rôle par cible avec ID externe. Chaque compte cible a un PipelineDeployRole qui fait confiance au rôle de service CodeBuild du compte CI, gardé par un ExternalId unique par cible. Les IDs vont dans SSM Parameter Store dans le compte CI, pas dans le buildspec.

  3. KMS sur le compte CI. La CMK du bucket d’artefacts vit dans shared-ci. Les rôles des comptes cibles obtiennent kms:Decrypt et kms:DescribeKey sur cette unique clé, pas la leur. Centraliser la clé simplifie la rotation.

  4. Pull ECR inter-comptes, pas la réplication, par défaut. La réplication double le coût de stockage et ajoute de la complexité de rotation. La politique de dépôt accordant le pull aux comptes cibles est plus simple et le SHA d’image est vérifiablement le même. Utilisez la réplication seulement quand la latence ou le coût du pull inter-régions fait mal (la réplication inter-régions est un cas valide séparé).

  5. unset des credentials entre les assumes de cibles. Un buildspec qui déploie aux trois environnements en séquence déploiera silencieusement staging en prod si vous oubliez le unset entre les rôles. Faites du unset une discipline.

  6. Limites de permissions sur le rôle de déploiement. Le PipelineDeployRole en prod est puissant (il peut mettre à jour les services de production). Attachez une limite de permissions qui plafonne ce qu’il peut faire — pas d’écritures IAM, pas de règles de groupes de sécurité, pas de Route 53. Le buildspec a rarement besoin d’autre chose que des mises à jour ECS/EKS/Lambda.

  7. CloudTrail dans tous les comptes, agrégé. Les appels assume-role inter-comptes atterrissent dans le CloudTrail du compte source. Le CloudTrail du compte cible montre l’identité AssumedRole agissant localement. Une piste agrégée dans un compte de sécurité, c’est l’enregistrement auditable de chaque action inter-comptes.

Mode d’échec courant : le déploiement réussit contre une image intra-compte mais échoue quand l’image est dans shared-ci. La cause, c’est presque toujours le rôle d’exécution de tâche ECS à qui il manque ecr:BatchGetImage pour le dépôt inter-comptes, même si la politique du dépôt l’accorde. Les deux côtés de la chaîne de politiques doivent autoriser le pull — la politique de ressource sur le dépôt ECR et la politique d’identité sur le rôle d’exécution.


Stack Harbor opère CodeBuild + CodePipeline à l’échelle production dans le cadre de l’onboarding Gestion AWS et des Environnements infogérés en continu. Si votre build s’exécute trois fois quand il devrait s’exécuter une seule fois et que vos auditeurs n’arrêtent pas de demander quel SHA est arrivé en prod, commencez par une consultation d’infrastructure.