Aller au contenu

Contrôler les coûts CodeBuild — maîtriser les minutes de build d'une équipe rapide

Dimensionnement compute, parallélisme, batch builds, filtres de chemins, envs éphémères. Requêtes CloudWatch et Cost Explorer pour trouver les jobs qui dérapent.

Une équipe de quinze ingénieurs pousse dans un monorepo. La CI s’exécute à chaque push, à chaque PR, à chaque merge — un pipeline complet qui build, teste et déploie des environnements de prévisualisation éphémères. La ligne CodeBuild de la facture AWS a grossi régulièrement pendant deux trimestres et est maintenant inconfortable. La responsable des finances demande une réduction de 40 % sans ralentir les ingénieurs. Six changements tactiques y parviennent, et la plupart sont de la configuration, pas du travail d’ingénierie.

Ce qui se passe

CodeBuild est facturé à la minute de build, par type de compute. La différence de prix entre les paliers de compute est à peu près linéaire en vCPU et mémoire, mais la durée du build n’est pas linéaire en taille de compute — un build qui dure 10 minutes sur BUILD_GENERAL1_SMALL ne dure pas 5 minutes sur BUILD_GENERAL1_MEDIUM. Il peut durer 7 minutes. Ou 4. Ou 10. La forme de votre build dicte le bon palier.

Les leviers de coût, en ordre d’impact pour l’équipe typique :

  1. Dimensionnement du type de compute. Un compute mal dimensionné, c’est le plus gros gaspillage. Les builds limités sur une étape mono-CPU (un bundle Webpack, un go build d’un gros module) finissent plus vite et moins cher sur un palier plus grand parce que l’augmentation du taux par minute est inférieure à la diminution de durée. Les builds limités I/O (npm install, pull de couches Docker) finissent à la même vitesse peu importe le palier — payez le plus petit.

  2. Mise en cache. Un build qui dure 6 minutes à froid et 90 secondes à chaud coûte 6,7× plus par exécution à froid. Sans cache activé, chaque build est à froid. Voir l’article KB séparé sur le cache des couches Docker pour le pattern complet.

  3. Filtres de chemins. Un monorepo avec apps/web, apps/api, apps/worker ne devrait pas rebuilder le worker à chaque changement de l’app web. Les filtres de chemins dans l’action source de CodePipeline sautent les builds quand les fichiers changés ne matchent pas le scope du projet.

  4. Batch builds. Plusieurs builds parallèles dans un seul « batch build » CodeBuild partagent le téléchargement de la source. Utile quand vous buildez N variantes (architectures, versions Node, environnements) de la même source.

  5. CI conditionnelle sur les étiquettes de PR. Toutes les PRs n’ont pas besoin du pipeline complet. Exécutez les tests d’intégration seulement quand la PR a l’étiquette needs-integration ou que le filtre de chemins matche des fichiers d’infra. Coupe les exécutions de pipeline les plus profondes à ~20 % des PRs.

  6. Démontez les envs éphémères agressivement. Des environnements de prévisualisation par PR, c’est génial jusqu’à ce que 50 d’entre eux tournent. Chacun a son propre service ECS, sa RDS, son Redis. Auto-démontage après fermeture de la PR, après 48 h d’inactivité, ou sur un cron nocturne.

Comment vérifier

Trouvez d’abord les générateurs de coûts. La requête Cost Explorer qui classe les projets par dépense de minutes de build sur les 30 derniers jours :

aws ce get-cost-and-usage \
  --time-period Start=$(date -u -v-30d +%Y-%m-%d),End=$(date -u +%Y-%m-%d) \
  --granularity DAILY \
  --metrics UnblendedCost \
  --filter '{"Dimensions":{"Key":"SERVICE","Values":["AWS CodeBuild"]}}' \
  --group-by Type=DIMENSION,Key=USAGE_TYPE \
  --query 'ResultsByTime[].Groups[].{Type:Keys[0],Cost:Metrics.UnblendedCost.Amount}' \
  --output table

La sortie montre l’usage par palier — USE1-Build-Min:Linux:gp1.small, USE1-Build-Min:Linux:gp1.medium, etc. Si la ligne medium domine, le palier SMALL est probablement sous-provisionné et pousse les builds vers MEDIUM inutilement. Si gp1.large est dans la liste et que vous ne vous souvenez pas l’avoir activé, l’appel update-project de quelqu’un a besoin d’une enquête.

Décomposition par projet sur la même fenêtre — CodeBuild émet une métrique CloudWatch pour la durée du build. La requête qui trouve les projets qui brûlent le plus de minutes de build :

aws cloudwatch get-metric-statistics \
  --namespace AWS/CodeBuild \
  --metric-name Duration \
  --dimensions Name=ProjectName,Value=web-api \
  --start-time $(date -u -v-30d +%Y-%m-%dT%H:%M:%S) \
  --end-time   $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 86400 \
  --statistics Sum,SampleCount \
  --query 'Datapoints[].{Date:Timestamp,TotalSec:Sum,Runs:SampleCount}' \
  --output table

Exécutez-la pour chaque projet (boucle pilotée par aws codebuild list-projects) et vous avez un tableau du total des secondes de compute par projet par jour. Le haut du tableau, c’est là où sont les économies.

La métrique à grapher à côté du coût, c’est les percentiles de Duration. Un projet dont le p95 de Duration est 2× son p50 a un problème de queue longue — habituellement un test flaky qui réessaie ou un cache miss. Régler la queue longue économise plus que régler la médiane :

aws cloudwatch get-metric-statistics \
  --namespace AWS/CodeBuild \
  --metric-name Duration \
  --dimensions Name=ProjectName,Value=web-api \
  --start-time $(date -u -v-7d +%Y-%m-%dT%H:%M:%S) \
  --end-time   $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 3600 \
  --extended-statistics p50,p95,p99 \
  --query 'Datapoints[].{Time:Timestamp,p50:ExtendedStatistics.p50,p95:ExtendedStatistics.p95,p99:ExtendedStatistics.p99}' \
  --output table

Si p99 est 3× p50, vous avez des retries flaky. Si p95 est 1,3× p50, vous avez un build en santé.

Le dimensionnement du type de compute, c’est empirique. Prenez un build représentatif, exécutez-le trois fois sur chaque palier que vous considérez, faites la moyenne de la durée, multipliez par le taux à la minute. La courbe « coût par build » a habituellement un minimum clair. Pour une app Node avec une grosse étape d’install, MEDIUM est souvent gagnant ; pour un binaire Go, SMALL est souvent suffisant.

Filtres de chemins dans l’action source de CodePipeline — pour une source GitHub :

aws codepipeline update-pipeline \
  --cli-input-json '{
    "pipeline": {
      "name": "web-monorepo",
      "stages": [{
        "name": "Source",
        "actions": [{
          "name": "GitHubSource",
          "configuration": {
            "ConnectionArn": "arn:aws:codestar-connections:...",
            "FullRepositoryId": "acme/monorepo",
            "BranchName": "main",
            "DetectChanges": "true"
          },
          "filterStateChange": {
            "includePaths": ["apps/web/**", "packages/shared/**"]
          }
        }]
      }]
    }
  }'

Le filterStateChange.includePaths, c’est le filtre — les pushs qui touchent seulement apps/api/** ne déclencheront pas le pipeline web. Chaque pipeline d’app a son propre chemin inclus. Le monorepo obtient N petits builds au lieu d’un gros, et N-1 d’entre eux sont des no-ops quand une PR typique touche une app.

La correction

Le manuel qui coupe la dépense CodeBuild sans ralentir l’équipe :

  1. Auditez la dépense actuelle par USAGE_TYPE et projet. La requête Cost Explorer ci-dessus plus la métrique Duration par projet donnent le classement. Optimisez les trois projets en tête ; ignorez la queue longue.

  2. Dimensionnez le compute par mesure, pas par intuition. Prenez le projet en haut du classement. Chronométrez un build représentatif sur chaque palier de compute. Coût = durée × taux à la minute. Le minimum, c’est le bon palier.

  3. Activez le cache LOCAL partout. aws codebuild update-project --cache type=LOCAL,modes=LOCAL_DOCKER_LAYER_CACHE,LOCAL_SOURCE_CACHE, c’est une ligne par projet. Gratuit. Couvre le cas de l’hôte chaud.

  4. Mettez des filtres de chemins sur les pipelines monorepo. Chaque pipeline reçoit un filterStateChange.includePaths scopé aux dossiers qu’il build vraiment. Un monorepo naïf exécute 3 à 10× plus de builds qu’il n’en a besoin.

  5. Rendez les tests d’intégration opt-in pour les PRs. Un filtre basé sur étiquette (needs-integration) sur le pipeline déclenché par PR coupe le nombre d’exécutions des tests d’intégration de 70 à 80 % sans perdre de couverture sur les changements d’infra (ces changements sont auto-étiquetés).

  6. TTL sur les environnements éphémères. Chaque env de prévisualisation reçoit une étiquette Expires mise à PR_creation_time + 7j. Une Lambda quotidienne liste les services ECS avec Expires < now() et les supprime. Les économies sont spectaculaires — la queue longue des envs de prévisualisation restés après fermeture des PRs est habituellement la plus grosse ligne silencieuse.

  7. Batch builds pour les charges de travail en éventail. Un build qui produit des images pour amd64 + arm64 devrait être un batch CodeBuild avec deux groupes de build parallèles, pas deux exécutions de CodePipeline séparées. Le batch partage le téléchargement de la source, qui est du temps réel pour les gros dépôts.

  8. Réglez une alarme de coût. Une alarme de facturation CloudWatch à 120 % de la dépense CodeBuild du mois dernier attrape le cas du build qui dérape. Le dérapage le plus courant, c’est un développeur qui pousse accidentellement un commit qui déclenche une boucle de builds (le build pousse un commit, qui déclenche un build, etc.). L’alarme l’attrape en moins d’une heure.

Le pattern qui exécute la CI pour une équipe de 15 ingénieurs sans factures surprises, en un paragraphe : compute SMALL pour typecheck/lint/tests-unitaires ; MEDIUM pour le build d’image Docker ; cache LOCAL sur chaque projet ; filtres de chemins sur le monorepo ; tests d’intégration sous contrôle d’étiquette ; envs de prévisualisation auto-supprimés après 7 jours. L’équipe ne remarque pas les changements. La facture chute.

Un pattern qui mérite d’être souligné parce qu’il mord beaucoup d’équipes : pollution de cache pilotée par nom de branche. Un pipeline qui pousse des tags ECR :cache-${BRANCH_NAME} accumule des tags de cache morts à mesure que les branches sont supprimées. Après un an, le dépôt ECR a 500 tags :cache-* morts et la facture de stockage grossit silencieusement. Une politique de cycle de vie sur ECR — garder les tags de cache seulement pour les branches vivantes dans les 30 derniers jours — ferme la boucle :

aws ecr put-lifecycle-policy \
  --repository-name web \
  --lifecycle-policy-text '{
    "rules": [{
      "rulePriority": 1,
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["cache-"],
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 30
      },
      "action": { "type": "expire" }
    }]
  }'

Les coûts CodeBuild sont mécaniques à contrôler. Le levier, c’est la mesure d’abord, puis le levier tactique le moins cher que la mesure pointe. L’ingénierie sur le buildspec lui-même, c’est habituellement le dernier levier à tirer, pas le premier.


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 ligne CodeBuild monte plus vite que votre équipe livre, commencez par une consultation d’infrastructure.