Un pipeline qui teste du Terraform a besoin d'un cloud, et un cloud a besoin
d'un compte, d'un secret et d'une facture. feint
supprime les trois : l'émulateur entre dans un bloc services: comme n'importe
quelle base de données de test, et le vrai provider Terraform applique contre
lui. Aucun secret à créer, aucune ressource à nettoyer si le job échoue
au milieu.
Ce que vous allez obtenir
Section intitulée « Ce que vous allez obtenir »À la fin de cette page, une pull request déclenchera un apply, un second plan
et un destroy complets, sur un runner ordinaire, sans qu'un seul secret
cloud n'existe dans le dépôt et sans qu'une ressource facturée soit créée.
- Choisir entre le service conteneur, l'action dédiée et le binaire.
- Écrire le job pour GitHub Actions et pour GitLab CI.
- Comprendre pourquoi le second plan est la seule vraie assertion.
- Vérifier la signature de l'image avant de l'exécuter.
Trois formes, et laquelle prendre
Section intitulée « Trois formes, et laquelle prendre »Le choix se fait sur une seule question : votre job tourne-t-il déjà dans des conteneurs ?
| Forme | Quand la choisir | Ce qu'elle apporte |
|---|---|---|
| Service conteneur | le job utilise déjà des conteneurs | rien à installer, une image épinglée par digest |
Action setup-feint | le runner exécute des commandes directement | installe le binaire, vérifie sa somme de contrôle, attend la réponse |
| Binaire sur un hôte | vous voulez de vraies machines derrière l'API | le seul mode qui donne accès au runtime Incus |
Le conteneur est un plan de contrôle et rien d'autre. Il tourne avec
--vm off et n'émule que les trois API. Une image qui promettrait de démarrer
des conteneurs depuis l'intérieur d'un conteneur serait une demi-vérité, et
le projet préfère l'énoncer : les vraies machines demandent le binaire sur un
hôte, comme le détaille Faire tourner de vraies
machines.
Le pipeline complet, dans les deux forges
Section intitulée « Le pipeline complet, dans les deux forges »Le job fait la même chose des deux côtés : appliquer, replanifier, détruire. Seule la façon d'attendre l'émulateur change.
Le runner tient les étapes jusqu'à ce que le contrôle de santé de l'image réponde, donc la première étape peut lui parler immédiatement.
name: Terraform contre un cloud local
on: pull_request: workflow_dispatch:
permissions: {}
jobs: terraform: name: apply, replanifier, detruire runs-on: ubuntu-24.04 timeout-minutes: 10
services: feint: image: ghcr.io/stephrobert/feint:v0.13.0@sha256:d46639ac7c7a0fa2b6b69d0c5d74bdca0757e3124675563209e8ada58e6a6040 ports: - 4599:4599
env: SCW_ACCESS_KEY: SCWXXXXXXXXXXXXXXXXX SCW_SECRET_KEY: 11111111-1111-1111-1111-111111111111 SCW_DEFAULT_PROJECT_ID: 11111111-1111-1111-1111-111111111111 SCW_DEFAULT_ORGANIZATION_ID: 11111111-1111-1111-1111-111111111111 SCW_DEFAULT_ZONE: fr-par-1 SCW_DEFAULT_REGION: fr-par SCW_API_URL: http://127.0.0.1:4599 SCW_INSECURE: "true"
steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: terraform_version: 1.15.4
- run: terraform init - run: terraform apply -auto-approve - run: terraform plan -detailed-exitcode - run: terraform destroy -auto-approveGitLab n'attend pas le contrôle de santé du service, donc le job attend lui-même. Une temporisation fixe passerait sur un runner rapide et échouerait sur un runner lent : la boucle teste une condition, pas une durée.
variables: FEINT_IMAGE: ghcr.io/stephrobert/feint:v0.13.0@sha256:d46639ac7c7a0fa2b6b69d0c5d74bdca0757e3124675563209e8ada58e6a6040 FEINT_ENDPOINT: http://feint:4599 SCW_ACCESS_KEY: SCWXXXXXXXXXXXXXXXXX SCW_SECRET_KEY: 11111111-1111-1111-1111-111111111111 SCW_DEFAULT_PROJECT_ID: 11111111-1111-1111-1111-111111111111 SCW_DEFAULT_ORGANIZATION_ID: 11111111-1111-1111-1111-111111111111 SCW_DEFAULT_ZONE: fr-par-1 SCW_DEFAULT_REGION: fr-par SCW_API_URL: http://feint:4599 SCW_INSECURE: "true"
terraform: image: hashicorp/terraform:1.15@sha256:fd5debae63188975d6febc6aa5bd1a982a588f55e4a4ddb7de28be923f250456 services: - name: $FEINT_IMAGE alias: feint before_script: - | for _ in $(seq 1 60); do wget -q -O /dev/null "$FEINT_ENDPOINT/_feint/health" && break sleep 1 done wget -q -O - "$FEINT_ENDPOINT/_feint/health" || { echo "l'émulateur n'a jamais répondu"; exit 1; } script: - terraform init - terraform apply -auto-approve - terraform plan -detailed-exitcode - terraform destroy -auto-approveQuand le runner n'exécute pas de conteneurs, l'action dédiée installe le binaire publié, vérifie sa somme de contrôle avant de l'exécuter, puis attend que l'émulateur réponde. Elle exporte aussi l'environnement client du fournisseur demandé.
- uses: stephrobert/setup-feint@b7eba1d4fcaccf65cf9124bf97a0d995996709b9 # v1.0.0 with: version: 0.13.0 provider: scalewayC'est la forme à préférer sur un hôte qui possède Incus, puisqu'elle est la seule des trois à ouvrir la voie aux vraies machines derrière l'API, avec un runtime installé sur la machine du runner.
L'assertion qui fait de ce pipeline un test
Section intitulée « L'assertion qui fait de ce pipeline un test »La ligne qui compte est terraform plan -detailed-exitcode, pas l'apply.
Un apply réussi prouve que l'API a répondu 200 ou 201 ; il ne prouve pas
qu'elle a stocké ce qu'elle a dit avoir stocké.
Le drapeau -detailed-exitcode transforme le plan en assertion :
| Code | Signification |
|---|---|
0 | aucun changement, l'infrastructure a convergé |
1 | erreur |
2 | des changements sont proposés, donc dérive |
Un émulateur qui répondrait 200 en enregistrant autre chose serait invisible
à l'apply et flagrant ici : le second plan proposerait de recréer ce qui
existe déjà. C'est l'assertion que la suite de conformité du projet exécute
contre chaque fournisseur, et mesurée sur le conteneur v0.13.0, ce second plan
sort bien à 0.
Les identifiants qui ne sont pas des secrets
Section intitulée « Les identifiants qui ne sont pas des secrets »Les valeurs du bloc env méritent une explication, sans quoi la première revue
de code les prendra pour une fuite.
Elles sont bien formées et sans signification. Le SDK Scaleway valide le
format de ces variables avant d'envoyer quoi que ce soit : la clé d'accès
ressemble à SCWXXXXXXXXXXXXXXXXX et le secret est un UUID. Ce sont donc des
valeurs fausses de la seule manière qui laisse encore démarrer un vrai
client.
L'émulateur ne vérifie aucune authentification. Il analyse la forme d'un identifiant et ne valide rien, ce qui est exactement la propriété qui permet de se passer de compte. La contrepartie est nette : un test de permissions IAM n'a aucun sens ici, comme le détaille Ce que feint prouve.
Vérifier l'image avant de l'exécuter
Section intitulée « Vérifier l'image avant de l'exécuter »Le site enseigne la sécurité de la chaîne d'approvisionnement, et un émulateur qui tourne dans le pipeline de tout le monde est précisément une surface d'attaque. L'image est signée et attestée par le workflow de release, sous la même identité que les binaires.
cosign verify ghcr.io/stephrobert/feint:v0.13.0 \ --certificate-identity-regexp '^https://github\.com/stephrobert/feint/\.github/workflows/release\.yml@refs/tags/v' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comLa sortie confirme trois attestations distinctes sur le même digest : une nomenclature logicielle au format CycloneDX, une provenance SLSA et la signature elle-même. L'identité vérifiée est la partie décisive de la commande : elle exige que la signature vienne du workflow de release de ce dépôt, sur un tag, et non d'un compte quelconque ayant poussé une image.
Un seul tag par release, et pas de latest. Une référence mutable tire ce
qui est le plus récent, c'est-à-dire une image que personne ne peut nommer après
coup lorsqu'un test échoue. Épingler le digest dans le pipeline, comme dans
les exemples ci-dessus, ferme complètement la question.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause | Solution |
|---|---|---|
| Le job échoue immédiatement sur une connexion refusée | GitLab n'attend pas le service | Garder la boucle d'attente du before_script |
terraform plan sort à 2 | Une divergence réelle entre l'apply et l'état stocké | Lire le plan : c'est le défaut que ce test cherche |
| Le SDK refuse de démarrer | Une variable d'identifiant mal formée | Respecter les formats : SCWXXXXXXXXXXXXXXXXX et un UUID |
Une opération répond 404 sans raison | Le pack décline cette opération | Chercher l'en-tête X-Feint-Not-Emulated dans la réponse |
| Le pipeline est vert mais ne teste rien | Aucune assertion après l'apply | Ajouter terraform plan -detailed-exitcode |
| Des appels partent vers le vrai cloud | Une ressource de stockage objet dans la configuration | Lancer feint doctor dans le répertoire avant l'apply |
À retenir
Section intitulée « À retenir »- L'émulateur entre dans un bloc
services:comme n'importe quelle dépendance de test. - Le conteneur est un plan de contrôle : pas de machines, et c'est annoncé plutôt que promis.
- Le second plan est l'assertion, l'
applyn'est qu'un préalable. - Les identifiants sont bien formés et vides de sens, ce qui laisse démarrer un vrai client.
- L'image se vérifie par signature et s'épingle par digest, jamais par
latest. - GitLab n'attend pas le service : la boucle teste une condition, pas une durée.