DevOps10 octobre 2026 9 min de lecture

Le coût de l'infrastructure dans la pull request : estimer un plan Terraform avant le merge

Le coût d'un changement d'infrastructure se découvre généralement sur la facture, un mois plus tard. Voici comment AAA Sentinel indique dans chaque pull request de combien un plan Terraform fait monter ou baisser la dépense mensuelle, ce qui quitte votre machine, comment le mettre en place dans GitHub Actions et ce qu'il ne sait pas encore chiffrer.

Cet article est aussi disponible en:esenpt

Une ligne dans un fichier .tf remplace m5.large par m5.xlarge. La revue se concentre sur la justesse du code, personne ne demande combien cela coûte et le changement est fusionné. Un mois plus tard, la facture EC2 a augmenté et il faut reconstituer ce qui s'est passé. Dans cet article, nous montrons comment afficher le prix de chaque changement dans la pull request elle-même avec AAA Sentinel, tant qu'il est encore peu coûteux de changer d'avis.

Le problème : le coût arrive un mois trop tard

Dans la plupart des équipes, on regarde le coût de l'infrastructure une fois qu'elle est déployée. Cela a trois conséquences :

  • Celui qui décide du changement ne voit pas son prix : le développeur choisit une taille d'instance sans savoir ce qu'elle coûte par mois.
  • Celui qui voit le prix ne peut pas décider : la finance découvre la hausse sur la facture, quand la ressource est déjà en production et qu'il faut convaincre quelqu'un de la modifier.
  • Corriger tard coûte plus cher : une taille se change en quelques secondes dans une pull request, alors qu'en production il faut une fenêtre de maintenance.

C'est ce qu'on appelle le FinOps « shift-left » : amener l'information de coût au moment où se prend la décision technique, comme on l'a fait auparavant pour les tests ou la sécurité.

Ce que vous y gagnez

Avec le coût dans la pull request :

  • Chaque changement d'infrastructure arrive en revue avec son delta mensuel estimé.
  • La discussion sur le coût a lieu entre l'auteur du changement et son relecteur, et non un mois plus tard entre la finance et l'ingénierie.
  • Vous voyez aussi ce qui n'a pas pu être chiffré et pourquoi, au lieu d'un montant qui paraît complet sans l'être.
  • Vous n'ajoutez pas de point de défaillance à votre déploiement : si l'estimation échoue, le pipeline continue.

Comment AAA Sentinel s'y prend

L'élément central est une CLI publiée sur npm, @aaa-sentinel/iac-cost. Elle lit la sortie de terraform show -json, la réduit sur votre machine au strict nécessaire, demande l'estimation à l'API d'AAA Sentinel et écrit sur la sortie standard un commentaire Markdown prêt à publier dans la pull request. Elle est accompagnée d'exemples pour GitHub Actions et Bitbucket Pipelines qui publient le commentaire et le mettent à jour à chaque push.

Forfaits : il faut Pro ou Business et une application OAuth2 avec la permission iac:estimate. Pro permet deux applications OAuth2 et Business, dix.

Ce qui quitte votre machine (et ce qui n'en sort pas)

La sortie de terraform show -json contient des plages CIDR, des identifiants de compte et, souvent, des secrets en clair. C'est pourquoi le plan ne quitte pas votre machine. Avant tout envoi, la CLI le réduit localement à une liste blanche :

  • Le type de ressource et l'action : création, modification, suppression ou remplacement.
  • Le nombre d'instances produites par le bloc : count et for_each sont regroupés en une quantité.
  • Les quelques attributs qui font varier le prix : la taille de l'instance et, quand la ressource la porte, la région.

Même les adresses de ressource, comme aws_instance.web[0], ne sont pas envoyées : dans un dépôt public, le commentaire serait une carte de votre infrastructure. La projection n'est jamais écrite dans le log du CI non plus. Et la permission iac:estimate ne donne accès à rien d'autre : elle ne peut lire ni vos comptes, ni vos résultats, ni vos rapports, on peut donc la stocker sans risque dans un CI que des tiers peuvent déclencher.

Cas pratique : la pull request d'ACME

ACME, une entreprise fictive, a trois serveurs d'application sur AWS, en us-east-1, en taille m5.large. Une pull request les passe en m5.xlarge et ajoute au passage une base de données de staging et une règle de security group. Le bloc provider "aws" fixe la région comme valeur constante. Avec les prix catalogue approximatifs d'EC2 à la demande sous Linux :

ACME · changement de taille dans la pull request

m5.large (avant)
environ 0,096 USD/heure × 730 h = 70,08 USD/mois par instance
m5.xlarge (après)
environ 0,192 USD/heure × 730 h = 140,16 USD/mois par instance
Delta par instance
+70,08 USD/mois
Delta des 3 instances
+210,24 USD/mois
aws_db_instance (nouvelle)
non chiffrée : RDS n'a pas encore de catalogue
aws_security_group_rule (nouvelle)
sans coût par nature

Étape 1 : créer l'application OAuth2

La CLI s'authentifie avec OAuth2 Client Credentials. Un propriétaire ou un administrateur de l'organisation crée l'application dans Développeur → « Applications OAuth2 » :

  • Dans « Mes applications », cliquez sur « Nouvelle application ».
  • Dans « Nouvelle application OAuth2 », renseignez « Nom de l'application » (par exemple, ci-iac-cost).
  • Dans « Permissions (scopes) », au sein du groupe « Lecture », cochez uniquement iac:estimate, « Estimer le coût d'un plan Terraform ».
  • Cliquez sur « Créer l'application » et copiez le client_id et le client_secret.

Le client_secret ne s'affiche qu'une fois. Enregistrez-le directement comme secret chiffré du CI : sur GitHub, sous les noms IAC_COST_CLIENT_ID et IAC_COST_CLIENT_SECRET dans les secrets du dépôt.

Étape 2 : l'essayer en local

Avant de toucher au pipeline, mieux vaut l'essayer sur votre machine avec Node.js 20 ou plus récent :

Terminalbash
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json

export IAC_COST_CLIENT_ID=<client_id>
export IAC_COST_CLIENT_SECRET=<client_secret>
npx @aaa-sentinel/iac-cost plan.json

Variables d'environnement de la CLI

IAC_COST_CLIENT_ID
Obligatoire · client_id de l'application OAuth2
IAC_COST_CLIENT_SECRET
Obligatoire · client_secret, stocké comme secret chiffré
IAC_COST_BASE_URL
Facultative · par défaut https://api.aaa-sentinel.com ; doit être en https
  • Le commentaire part sur la sortie standard et les avertissements sur la sortie d'erreur. S'il n'y a rien à dire, la sortie standard reste vide.
  • Elle accepte aussi le plan sur l'entrée standard, redirigé vers npx @aaa-sentinel/iac-cost.

Étape 3 : commenter chaque pull request avec GitHub Actions

Voici le workflow du README du paquet. Il publie un commentaire par pull request et le met à jour sur place lors des push suivants, au lieu d'en empiler un nouveau à chaque fois. Il utilise le jeton que le run injecte déjà : ni jeton personnel, ni installation d'application.

.github/workflows/infrastructure-cost.ymlyaml
name: Infrastructure cost
on: pull_request

permissions:
  contents: read
  pull-requests: write

jobs:
  cost:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: terraform init
      - run: terraform plan -out=plan.tfplan
      - run: terraform show -json plan.tfplan > plan.json

      - name: Estimate cost
        env:
          IAC_COST_CLIENT_ID: ${{ secrets.IAC_COST_CLIENT_ID }}
          IAC_COST_CLIENT_SECRET: ${{ secrets.IAC_COST_CLIENT_SECRET }}
        run: npx --yes @aaa-sentinel/iac-cost plan.json > comment.md

      - name: Comment on the pull request
        continue-on-error: true
        env:
          GH_TOKEN: ${{ github.token }}
          PR: ${{ github.event.number }}
        run: |
          [ -s comment.md ] || exit 0
          id=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \
                 --jq '.[] | select((.body // "") | contains("<!-- aaa-sentinel:iac-cost -->")) | .id' \
               | head -n1)
          if [ -n "$id" ]; then
            gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -F body=@comment.md
          else
            gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -F body=@comment.md
          fi
  • Ajoutez les étapes d'identifiants de votre cloud avant terraform plan, comme dans n'importe quel pipeline Terraform.
  • continue-on-error n'est pas décoratif : sans lui, un incident de l'API GitHub lors de la publication du commentaire ferait échouer le job.
  • Sur une pull request issue d'un fork, GitHub donne au run un jeton en lecture seule et l'étape du commentaire est ignorée. C'est une restriction de GitHub qui s'applique à tout outil qui commente des pull requests.

Et si vous utilisez Bitbucket Pipelines

Bitbucket n'injecte pas de jeton autorisé à écrire dans le dépôt. Créez un Repository Access Token avec le scope pullrequest:write et stockez-le comme variable sécurisée du dépôt nommée BB_TOKEN. Le set +e et le exit 0 final jouent le même rôle que continue-on-error sur GitHub.

bitbucket-pipelines.ymlyaml
pipelines:
  pull-requests:
    '**':
      - step:
          name: Infrastructure cost
          image: hashicorp/terraform:latest
          script:
            - apk add --no-cache nodejs npm curl jq
            - terraform init
            - terraform plan -out=plan.tfplan
            - terraform show -json plan.tfplan > plan.json
            - npx --yes @aaa-sentinel/iac-cost plan.json > comment.md
            - |
              set +e
              [ -s comment.md ] || exit 0
              API="https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pullrequests/$BITBUCKET_PR_ID/comments"
              BODY=$(jq -Rs . < comment.md)
              ID=$(curl -sf -H "Authorization: Bearer $BB_TOKEN" "$API?pagelen=100" \
                    | jq -r '[.values[] | select((.content.raw // "") | contains("<!-- aaa-sentinel:iac-cost -->"))][0].id // empty')
              if [ -n "$ID" ]; then
                curl -sf -X PUT -H "Authorization: Bearer $BB_TOKEN" -H 'Content-Type: application/json' \
                  -d "{\"content\":{\"raw\":$BODY}}" "$API/$ID"
              else
                curl -sf -X POST -H "Authorization: Bearer $BB_TOKEN" -H 'Content-Type: application/json' \
                  -d "{\"content\":{\"raw\":$BODY}}" "$API"
              fi
              exit 0

Étape 4 : lire le commentaire

Pour la pull request d'ACME, le commentaire se présente ainsi (extrait ; le commentaire est toujours rédigé en anglais) :

Commentaire de la pull request (extrait)markdown
**Coverage: 3 of 4 billable resources priced** (1 more have no cost by design).

**Estimated change: +$210.24/mo** (list price, USD)

| Resource | Change | Qty | Delta |
|---|---|---|---|
| `aws_instance` | update | 3 | +$210.24/mo |

<details><summary>Not priced (1 resources)</summary>

| Resource | Change | Qty | Why |
|---|---|---|---|
| `aws_db_instance` | new | 1 | no matching price found in the catalog |

</details>
  • La couverture passe en premier, pas le total : 3 des 4 ressources facturables ont un prix. La règle de security group ne compte pas, car elle ne génère pas de facture.
  • Le changement de taille est chiffré des deux côtés : le delta est le coût de l'après moins celui de l'avant, multiplié par les 3 instances. Un remplacement est traité de la même façon, comme une suppression plus une création.
  • La base de données apparaît comme non chiffrée, avec son motif. Elle n'est jamais comptée comme gratuite.

Pourquoi il ne bloque jamais le merge

Le code de sortie de la CLI est toujours 0. Un identifiant manquant, une erreur réseau, un plan illisible ou une panne d'AAA Sentinel affichent un avertissement d'une ligne et le pipeline continue. C'est un choix délibéré : un commentaire de coût n'est pas une barrière de qualité, et la disponibilité d'un tiers ne doit pas devenir une dépendance de vos déploiements. La CLI ne propose pas de mode qui fasse échouer le build au-delà d'un seuil de coût ; la décision reste à la revue humaine de la pull request.

Comment lire le chiffre

Le montant du commentaire a un sens précis :

  • C'est un prix catalogue. Ni AWS ni Azure ne renvoient ce que vous payez : les remises contractuelles et les réservations ou Savings Plans sont invisibles au moment de la pull request. Si une réservation couvre déjà la nouvelle ressource, le coût marginal réel est nul.
  • Il est en USD, la devise dans laquelle publient les fournisseurs. Le convertir empilerait une estimation sur une autre, et la même pull request afficherait un montant différent chaque matin.
  • Si moins de la moitié des ressources facturables ont un prix, la répartition est publiée mais pas le total : un total bâti sur 2 ressources sur 30 n'est pas une estimation partielle, c'est du bruit.
  • Une région inconnue est traitée comme non prise en charge. Chez AWS, la région vit dans le bloc provider ; si elle vient d'une variable ou d'un provider avec alias, le plan ne la résout pas et le commentaire le dit au lieu d'utiliser le prix d'une autre région.

Ce qu'il ne fait pas encore

Les limites comptent, car un commentaire qui paraît complet sans l'être est pire que pas de commentaire :

  • Aujourd'hui, il ne chiffre que deux choses : les instances EC2 à la demande sur AWS (aws_instance) et les machines virtuelles Azure (azurerm_linux_virtual_machine, azurerm_windows_virtual_machine et leurs scale sets).
  • Tout le reste apparaît comme non chiffré, avec son motif : RDS, volumes EBS, load balancers, NAT, stockage et tout autre type de ressource.
  • Google Cloud est reconnu, mais son catalogue de prix n'est pas encore connecté. Les autres fournisseurs, comme Kubernetes, Datadog ou Cloudflare, ne sont pas couverts.
  • aws_instance est chiffrée comme Linux : le plan n'indique pas le système d'exploitation de l'AMI.
  • Le prix EC2 est celui de l'instance seule, sans le volume racine ni le transfert de données.
  • Un même plan accepte au maximum 5 régions chiffrées, les deux clouds confondus. Au-delà, l'estimation échoue et le pipeline continue sans commentaire.

Checklist pour démarrer

Avec tout cela en place, chaque changement d'infrastructure arrive en revue avec son prix :

  • Forfait Pro ou Business.
  • Une application OAuth2 avec la seule permission iac:estimate, créée dans Développeur → « Applications OAuth2 ».
  • Le client_id et le client_secret stockés comme secrets chiffrés du CI.
  • Un essai en local avec terraform show -json et npx @aaa-sentinel/iac-cost.
  • Le workflow GitHub Actions, ou le pipeline Bitbucket, dans le dépôt Terraform.
  • La région AWS fixée comme constante dans le bloc provider, pour que le plan la résolve.
  • Une équipe qui sait lire le commentaire : la couverture d'abord, prix catalogue, en USD.