Validations, vérifications et politiques
Pourquoi
Un plan Terraform dit ce qui va changer, il ne dit pas si c'est acceptable. Rien dans terraform plan ne vous empêche de créer un bucket d'images de signalements en lecture publique, de déployer la production sur une seule zone, ou d'oublier l'étiquette qui rattache chaque ressource à son centre de coût. Ces erreurs ne cassent rien : le plan est valide, l'apply réussit, et la faille ou la dérive de coûts ne se découvre que des semaines plus tard.
Dans le premier cours, la leçon 12 a posé le flux de travail : plan dans la demande de fusion, apply après fusion. Ce flux est le bon endroit pour des règles automatiques, parce que la machine relit tout, à chaque changement, sans fatigue. Reste à décider quelle règle va où. Car Terraform offre plusieurs mécanismes qui se ressemblent et ne voient pas la même chose : une validation de variable ne voit que la variable, une postcondition voit une ressource créée, un bloc check regarde l'infrastructure en marche, une politique sur le plan voit l'ensemble des changements avant qu'ils n'arrivent.
Cette leçon les parcourt dans l'ordre de ce qu'ils savent observer, avec des sorties réelles produites par Terraform 1.16.1 sur des ressources sans coût, puis écrit une politique complète pour Signalements. Une règle bien placée échoue tôt, avec un message qui dit quoi corriger. Une règle mal placée est ignorée, contournée ou, pire, bloque la production un vendredi soir pour une raison que personne ne comprend.
Les concepts
Cinq contrôles, cinq points de vue
Le point de vue détermine ce que le contrôle peut affirmer.
| Mécanisme | Quand il s'exécute | Ce qu'il voit | Échec |
|---|---|---|---|
validation d'une variable | à la lecture de la valeur | cette variable (et, depuis Terraform 1.9, d'autres objets du module) | erreur, rien ne commence |
precondition | avant d'agir sur la ressource | tout ce qui est connu à ce moment | erreur, la ressource n'est pas touchée |
postcondition | après l'opération sur la ressource | la ressource telle que l'API l'a renvoyée (self) | erreur, les dépendants ne sont pas traités |
bloc check | en dernier, à la fin du plan et de l'apply | toute la configuration, plus ses propres sources de données | avertissement, l'opération continue |
| politique sur le plan | après le plan, avant l'apply | tous les changements prévus, sous forme de JSON | décidé par l'outil qui l'exécute (CI, plateforme) |
Deux idées structurent le tableau. D'abord, les trois premiers sont dans le code : ils voyagent avec le module, protègent tous ses utilisateurs, mais ne les protègent que contre ce que l'auteur du module avait prévu. Ensuite, la politique est à l'extérieur : elle appartient à la plateforme ou à la sécurité, s'applique à tous les dépôts et peut être plus stricte que ce que les auteurs de modules imaginent. Les deux sont nécessaires. La première famille documente les hypothèses d'un module, la seconde fait respecter les règles de l'organisation.
Preconditions et postconditions
Ce sont des blocs precondition et postcondition placés dans le lifecycle d'une ressource, d'une source de données ou d'une ressource éphémère (et, pour precondition, d'une sortie). Chacun porte une condition (une expression booléenne) et un error_message.
- Une precondition exprime ce que la ressource suppose : « la zone demandée existe », « le CIDR est assez grand pour tous les sous-réseaux ». Si elle est fausse, Terraform s'arrête avant de toucher la ressource.
- Une postcondition exprime ce que la ressource garantit une fois créée : « l'adresse IP obtenue est bien publique », « le moteur de base est celui qu'on a demandé ». Dans une postcondition,
selfdésigne la ressource avec ses attributs connus après l'opération. Si elle est fausse, l'opération en cours échoue et rien de ce qui dépend de la ressource n'est traité.
La documentation de Terraform conseille de les employer pour les hypothèses qui dépassent une seule variable, ou qui portent sur des valeurs que seul le fournisseur connaît. Elle déconseille d'en faire un test général de l'infrastructure : c'est le rôle des blocs check.
Les blocs check
Disponible depuis Terraform 1.5, un bloc check contient une ou plusieurs assertions (assert) et, éventuellement, une source de données scopée : une data déclarée dans le bloc, visible de lui seul. Il s'exécute en dernier, à la fin du plan et de l'apply, et son échec est un avertissement : l'opération continue. C'est voulu. Un check répond à la question « l'infrastructure est-elle en bonne santé ? », pas « puis-je continuer ? ». Il sert à constater qu'un site répond, qu'un certificat n'expire pas dans la semaine, qu'un bucket porte ses étiquettes. HCP Terraform sait les rejouer en dehors des déploiements (validation continue).
Une subtilité utile : si la source de données scopée échoue (le point de contrôle ne répond pas), cela devient aussi un avertissement et non une erreur. Un contrôle de santé ne doit pas empêcher d'appliquer le correctif qui rétablira la santé.
La politique as code
Une politique as code est une règle de conformité écrite dans un langage exécutable, versionnée, testée et appliquée automatiquement. Sur Terraform, la matière première est le plan en JSON : terraform show -json plan.binaire produit un document qui liste chaque changement prévu, avec les valeurs avant et après. Un moteur de politique lit ce JSON et rend un verdict. Comme le plan est calculé sans rien créer, la politique peut s'exécuter dans la demande de fusion, avec le même compte en lecture seule que le plan.
Les outils courants :
- OPA (Open Policy Agent), moteur généraliste du CNCF, et son langage Rego. conftest est l'outil en ligne de commande qui applique des politiques Rego à des fichiers de configuration, JSON compris.
- Sentinel, le moteur de politiques de HashiCorp, intégré à HCP Terraform et à Terraform Enterprise. HCP Terraform accepte aussi des politiques OPA.
- Terraform policy, un cadre natif en HCL (fichiers
.policy.hcl) annoncé avec Terraform 1.16, encore en bêta à la date de vérification de cette leçon : à surveiller, pas à adopter aujourd'hui pour une politique d'entreprise. - Trivy (
trivy config), et d'autres analyseurs statiques, qui lisent le code HCL ou le plan et détectent des mauvaises configurations connues sans que vous écriviez de règle.
Les politiques Rego et l'analyse statique ne se concurrencent pas : l'analyse statique apporte des centaines de règles génériques « gratuites », la politique Rego encode vos règles, celles de Lyneko et de ses clients.
En pratique
Les exemples qui suivent utilisent des ressources terraform_data, qui ne créent rien dans le cloud, pour qu'on puisse rejouer chaque sortie. Elles jouent le rôle du bucket de Signalements : une entrée avec un nom, une ACL et des étiquettes.
Rappel : la validation de variable
La leçon 5 du premier cours a présenté validation. Voici le cas qui nous servira de fil :
variable "environnement" {
type = string
validation {
condition = contains(["preprod", "prod"], var.environnement)
error_message = "L'environnement doit valoir preprod ou prod."
}
}$ terraform plan -no-color -var environnement=qa
Error: Invalid value for variable
on main.tf line 8:
8: variable "environnement" {
├────────────────
│ var.environnement is "qa"
L'environnement doit valoir preprod ou prod.
This was checked by the validation rule at main.tf:10,3-13.
Une validation est le contrôle le moins cher : elle échoue avant tout calcul, y compris pendant terraform validate quand la valeur est connue. Elle convient aux règles qui ne portent que sur la forme d'une entrée. Dès qu'il faut comparer à une autre ressource ou à une valeur calculée, elle ne suffit plus.
Precondition et postcondition
On ajoute à la ressource les deux blocs :
variable "zones" {
type = list(string)
default = ["fr-par-1", "fr-par-2"]
}
resource "terraform_data" "bucket" {
input = {
name = "sig-${var.environnement}-medias"
acl = var.environnement == "prod" ? "private" : "public-read"
tags = var.environnement == "prod" ? ["env:prod", "app:signalements"] : ["env:preprod"]
}
lifecycle {
precondition {
condition = length(var.zones) >= 2 || var.environnement != "prod"
error_message = "La production exige au moins deux zones."
}
postcondition {
condition = self.output.acl == "private"
error_message = "Le bucket ${self.output.name} ne doit pas être public."
}
}
}La precondition porte sur une hypothèse du déploiement (deux zones en production). La postcondition porte sur le résultat : self.output est l'attribut calculé par terraform_data, connu seulement après l'apply, ce qui est précisément le cas d'usage d'une postcondition. Avec un environnement preprod qui, par une faute de conception, génère une ACL publique :
$ terraform apply -no-color -auto-approve -var environnement=preprod
...
terraform_data.bucket: Creating...
terraform_data.bucket: Creation complete after 0s [id=2e06a9ee-a7dc-1e13-03de-a9e297972782]
Error: Resource postcondition failed
on main.tf line 34, in resource "terraform_data" "bucket":
34: condition = self.output.acl == "private"
├────────────────
│ self.output.acl is "public-read"
Le bucket sig-preprod-medias ne doit pas être public.
Remarquez l'ordre : la ressource a bien été créée, puis la postcondition a échoué. Une postcondition ne défait rien. Elle empêche la suite (les ressources qui dépendent du bucket ne sont pas traitées) et fait échouer l'apply, mais le bucket existe et il est dans l'état : le plan suivant (ici, un apply en prod) le voit et propose une modification. Si la règle protège contre une exposition, elle arrive donc trop tard : une postcondition garantit la cohérence du graphe, pas l'absence d'effet.
Pour qu'une règle empêche la création, il faut qu'elle soit évaluable avant. Ici, l'ACL se déduit de la configuration seule : une precondition ou une validation portant sur var.environnement l'aurait arrêtée au plan. Une bonne règle de pouce : tout ce qui se déduit de la configuration va dans une precondition ou une validation ; ce qui dépend d'une réponse de l'API va dans une postcondition.
Un bloc check
Le contrôle d'étiquettes ne doit pas empêcher d'agir : un bucket sans étiquette est un problème de gouvernance, pas une panne. C'est un check :
check "etiquettes" {
assert {
condition = contains(terraform_data.bucket.input.tags, "app:signalements")
error_message = "Le bucket ${terraform_data.bucket.input.name} n'a pas l'étiquette app:signalements."
}
}Sur le plan de production dont on a retiré l'étiquette :
$ terraform plan -no-color -var environnement=prod
Warning: Check block assertion failed
on main.tf line 42, in check "etiquettes":
42: condition = contains(terraform_data.bucket.input.tags, "app:signalements")
├────────────────
│ terraform_data.bucket.input.tags is tuple with 1 element
Le bucket sig-prod-medias n'a pas l'étiquette app:signalements.
Le plan se déroule et aboutit ; seul l'avertissement signale le problème. Avec la première version du bloc, qui lisait terraform_data.bucket.output.tags (un attribut calculé), le plan affichait à la place :
Warning: Check block assertion known after apply
on main.tf line 42, in check "etiquettes":
42: condition = contains(terraform_data.bucket.output.tags, "app:signalements")
├────────────────
│ terraform_data.bucket.output.tags is a list of string
The condition could not be evaluated at this time, a result will be known
when this plan is applied.
C'est un deuxième enseignement : une assertion qui dépend d'une valeur inconnue au plan ne peut pas être évaluée avant l'apply. Elle n'est pas fausse, elle est différée. Pour qu'un check serve de contrôle au moment de la demande de fusion, faites-le porter sur ce que la configuration connaît déjà.
Le bloc check peut aussi contenir une source de données scopée (par exemple une requête HTTP sur /sante après un déploiement) : elle n'est visible que de ce bloc, et son échec reste un avertissement.
Le plan en JSON
Une politique ne lit pas la sortie lisible du plan, mais son équivalent JSON. On enregistre le plan, puis on le convertit :
terraform plan -var environnement=prod -out=prod.plan
terraform show -json prod.plan > prod.jsonLe document a, à cette date, les clés de premier niveau suivantes (relevées sur le plan de la démonstration) :
$ jq 'keys' prod.json
[
"applyable",
"checks",
"complete",
"configuration",
"errored",
"format_version",
"planned_values",
"resource_changes",
"terraform_version",
"timestamp",
"variables"
]
La clé qui intéresse presque toutes les politiques est resource_changes : un élément par instance de ressource touchée. Voici l'élément du bucket, tel que Terraform 1.16.1 le produit :
$ jq '.resource_changes[0]' prod.json
{
"address": "terraform_data.bucket",
"mode": "managed",
"type": "terraform_data",
"name": "bucket",
"provider_name": "terraform.io/builtin/terraform",
"change": {
"actions": ["create"],
"before": null,
"after": {
"input": {
"acl": "private",
"name": "sig-prod-medias",
"tags": ["env:prod", "app:signalements"]
},
"store": null,
"triggers_replace": null
},
"after_unknown": {
"id": true,
"input": { "tags": [false, false] },
"output": true
},
"before_sensitive": false,
"after_sensitive": { "input": { "tags": [false, false] }, "output": {} }
}
}
(La sortie a été mise en forme sur moins de lignes pour la lecture ; les valeurs sont celles de la commande.) À retenir :
address,type,modeidentifient la ressource. Les politiques filtrent presque toujours surtype(par exemplescaleway_object_bucket_acl) etmode == "managed"(les sources de données, de modedata, sont aussi dans la liste).change.actionsest une liste :["create"],["update"],["delete"],["delete", "create"]pour un remplacement,["no-op"]ou["read"]. Une règle « pas de suppression en production » lit ce champ.change.afterest la valeur planifiée,change.beforela valeur actuelle (nullà la création).change.after_unknownmarque les attributs connus seulement après l'apply. Ils sont absents deafter. Une politique qui litafter.aclquand l'ACL est inconnue obtient « rien », et la règle ne se déclenche pas. C'est la faille classique : on croit avoir vérifié et on n'a rien vu. Il faut décider, règle par règle, ce que signifie « inconnu » (en général : refuser, ou signaler).after_sensitivemarque les valeurs sensibles, queterraform show -jsonmasque. Leçon 7 : une valeur éphémère ou en écriture seule n'apparaît pas du tout.checkscontient le résultat de chaque blocchecket de chaque validation : une politique peut donc aussi exiger qu'aucun contrôle ne soit en échec.
Pour s'exercer sans installer d'autre outil, jq suffit. Cette requête, appliquée au plan de préproduction, fait exactement ce que fera la politique Rego ci-dessous pour les ACL :
$ jq -r '.resource_changes[] | select(.type=="terraform_data")
| select(.change.after.input.acl == "public-read")
| "REFUSE \(.address) : acl publique"' pre.json
REFUSE terraform_data.bucket : acl publique
Une politique Rego pour Signalements
Les règles qu'on veut faire respecter :
- aucun bucket ne doit avoir une ACL autre que
private; - tout bucket doit porter les étiquettes
envetapp.
Avec le fournisseur Scaleway 2.84, l'ACL d'un bucket est portée par la ressource scaleway_object_bucket_acl (argument acl), et les étiquettes par l'argument tags (une table) de scaleway_object_bucket. Les deux noms ont été vérifiés dans le schéma du fournisseur par terraform validate. La politique, dans un fichier policy/buckets.rego :
package main
import rego.v1
# Les changements de ressources gérées qui ne sont pas de simples suppressions.
gerees contains rc if {
some rc in input.resource_changes
rc.mode == "managed"
rc.change.actions != ["delete"]
}
# Règle 1 : pas d'ACL autre que private.
deny contains msg if {
some rc in gerees
rc.type == "scaleway_object_bucket_acl"
rc.change.after.acl != "private"
msg := sprintf("%s : l'ACL doit être private (valeur planifiée : %s)", [rc.address, rc.change.after.acl])
}
# Une ACL inconnue au plan ne peut pas être vérifiée : on refuse aussi.
deny contains msg if {
some rc in gerees
rc.type == "scaleway_object_bucket_acl"
rc.change.after_unknown.acl == true
msg := sprintf("%s : l'ACL n'est pas connue au plan, impossible de la vérifier", [rc.address])
}
# Règle 2 : étiquettes obligatoires.
etiquettes_requises := {"env", "app"}
deny contains msg if {
some rc in gerees
rc.type == "scaleway_object_bucket"
presentes := {k | some k, _ in rc.change.after.tags}
manquantes := etiquettes_requises - presentes
count(manquantes) > 0
msg := sprintf("%s : étiquettes manquantes : %s", [rc.address, concat(", ", sort(manquantes))])
}On l'exécute sur le plan converti :
conftest test --policy policy/ plan.jsonconftest charge les règles du paquet package main et traite tout ensemble nommé deny comme un ensemble de violations (warn donne des avertissements, violation des violations structurées). Un code de sortie non nul fait échouer la tâche du pipeline. La syntaxe deny contains msg if { ... } est celle de Rego v1, la forme actuelle d'OPA ; l'import rego.v1 la rend aussi valable sur d'anciennes versions.
Note
Cette politique n'a pas été exécutée pour cette leçon : ni OPA ni conftest ne sont installés dans l'environnement de rédaction. Elle est écrite d'après la documentation de conftest et d'OPA, et sa logique est celle de la requête jq ci-dessus, qui, elle, a été exécutée. Avant de l'adopter, testez-la avec un plan qui doit passer et un plan qui doit échouer, comme le font les exercices.
Deux détails de Rego méritent d'être compris. D'abord, une règle dont une condition est indéfinie ne s'applique pas : si rc.change.after.acl n'existe pas (valeur inconnue), la règle 1 est silencieuse, d'où la deuxième règle deny qui traite explicitement after_unknown. Ensuite, le comprehension {k | some k, _ in rc.change.after.tags} construit l'ensemble des clés présentes ; si tags vaut null, l'ensemble est vide et toutes les étiquettes sont déclarées manquantes, ce qui est le comportement voulu.
Trivy en rappel
Le premier cours a rappelé que la CI exécute des analyses statiques sans identifiant. trivy config en est un exemple :
trivy config --severity HIGH,CRITICAL --exit-code 1 .Trivy analyse les fichiers HCL, résout les variables et les modules quand c'est possible, et signale des configurations à risque d'après une base de règles (groupes de sécurité ouverts à tout Internet, stockage non chiffré, etc.). Il peut aussi lire un plan JSON, ce qui améliore la précision sur les valeurs calculées. Son avantage : zéro règle à écrire. Sa limite : il ne connaît ni votre vocabulaire d'étiquettes ni vos conventions de nommage, et la couverture des ressources Scaleway est plus mince que celle des grands fournisseurs. Gardez-le comme filet général, et réservez Rego à vos règles propres.
Sentinel et HCP Terraform, en mention
Dans HCP Terraform, les politiques (Sentinel, ou OPA) s'attachent à des ensembles de politiques appliqués à des projets, avec des niveaux d'application (consultatif, avec dérogation, obligatoire), et la plateforme les exécute entre le plan et l'apply. Le principe est identique : un verdict sur le plan. Le coût est l'abonnement et la dépendance à la plateforme ; conftest dans la CI n'ajoute aucune dépendance.
Sous le capot
Où vivent les conditions dans le graphe. Terraform évalue une precondition quand il traite le nœud de la ressource, avant d'appeler le fournisseur, et une postcondition après la réponse du fournisseur, avant de marquer le nœud comme terminé. Les ressources qui dépendent de celle-ci attendent la fin du nœud : c'est pourquoi un échec de postcondition les empêche de se dérouler. Les conditions sont donc des arêtes de contrôle intégrées au graphe de dépendances.
Le plan JSON est une photographie. terraform show -json sérialise le plan binaire : il contient les variables d'entrée non éphémères, la configuration résolue et les changements. C'est donc un artefact sensible : il peut contenir des valeurs que la politique veut juger (noms, CIDR, ACL) et, si une valeur sensible a été marquée par un fournisseur, elle est masquée mais son existence est notée. Gardez-le comme le plan binaire, avec une durée de rétention courte, et ne le publiez pas en tant qu'artefact public.
Rego est un langage de requête. Une règle deny contains msg if { ... } cherche toutes les combinaisons de valeurs qui rendent le corps vrai ; chacune ajoute un message. Si une expression n'a pas de valeur (« indéfini »), aucune combinaison ne la satisfait et la règle se tait.
Pièges courants
Confondre une postcondition et une barrière. Elle arrive après l'opération (voir plus haut). Pour empêcher une création, utilisez une validation, une precondition ou une politique sur le plan.
Attendre d'un check qu'il bloque. Il avertit. Si votre pipeline doit échouer en cas d'avertissement check, lisez la clé checks du plan JSON et appliquez-y une règle, ou analysez la sortie -json du plan.
Les valeurs inconnues au plan. Aussi bien pour un check (« known after apply ») que pour une politique (l'attribut est dans after_unknown, pas dans after). Le message d'erreur à reconnaître : The condition could not be evaluated at this time. Décidez explicitement, pour chaque règle, du comportement en cas d'inconnu.
Des messages d'erreur inutiles. error_message = "Invalid" fait perdre dix minutes à chaque lecteur. Un bon message dit ce qui est faux, la valeur observée et la correction : « Le bucket sig-prod-medias ne doit pas être public : passez l'ACL à private ». Dans Rego, le message msg est tout ce que verra la personne qui ouvre la demande de fusion.
Une politique jamais testée. Une règle qui ne se déclenche jamais n'est pas distinguable d'une règle qui fonctionne. Chaque règle a besoin d'un plan qui doit échouer. conftest propose conftest verify et des fichiers _test.rego pour cela (règles nommées test_... qui évaluent la politique sur une entrée fabriquée, avec with input as ...).
Sécurité
- Une politique est un contrôle de sécurité, donc une surface d'attaque. Qui peut modifier le dépôt de politiques peut désactiver la règle. Placez-le dans un dépôt dont les mainteneurs sont ceux de la sécurité, protégez la branche principale, et faites tirer les politiques par la CI à une version épinglée plutôt que de les recopier dans chaque dépôt.
- La CI qui exécute la politique ne doit pas pouvoir la contourner. Si un développeur modifie la tâche qui lance conftest dans sa propre demande de fusion, il peut la désactiver. Les mêmes règles que pour les workflows s'appliquent : fichiers de pipeline soumis à des propriétaires de code (CODEOWNERS), ou tâche imposée par la plateforme.
- Le plan JSON peut contenir des données personnelles ou des secrets mal protégés. Un plan se traite comme un secret de classe faible : stockage restreint, rétention courte.
- Les dérogations se tracent. Une règle qu'il faut parfois contourner a besoin d'un mécanisme de dérogation explicite (une étiquette sur la ressource, une liste d'exceptions versionnée avec justification), pas d'un
|| truedans le pipeline.
En production
Où placer chaque contrôle. Un chemin qui marche pour une équipe comme celle de Signalements :
- Poste et pre-commit :
terraform fmt -check,terraform validate,trivy config. Rapide, sans identifiant. - Demande de fusion, avant le plan : les mêmes, plus
terraform test(leçon 5), qui exécute les validations et les conditions sur des plans fictifs. - Demande de fusion, après le plan :
terraform show -jsonpuisconftest test. Le plan est joint à la demande de fusion ; la politique en bloque la fusion si une règledenyse déclenche. - Apply : les preconditions et postconditions protègent le déroulement. Le plan appliqué est celui qui a été validé par la politique (on applique le fichier de plan enregistré, pas un nouveau plan).
- Après l'apply et en continu : les blocs
check, et un plan planifié qui détecte la dérive (leçon 11 du premier cours). Une règle de conformité qui doit rester vraie dans le temps se contrôle ici, parce que la politique sur le plan ne voit que les changements.
Commencer en mode avertissement. Une nouvelle règle de conformité qui bloque d'emblée toutes les demandes de fusion se fait détester en deux jours. Lancez-la en warn (conftest distingue warn de deny), mesurez ce qu'elle trouve, corrigez le stock, puis passez-la en deny.
Le stock existant. Une politique « pas de bucket public » s'applique à ce que le plan change. Un bucket public existant et inchangé n'apparaît pas dans resource_changes (ou y apparaît en no-op, avec la valeur actuelle). Une règle qui doit couvrir le stock se lit dans la clé prior_state du plan, ou se vérifie par un check, ou par l'analyse de l'état.
Politique commune, exceptions locales. Un dépôt de politiques versionné (même discipline que les modules de la leçon 3), avec des tests, des exemples de plans et un journal des changements. Les équipes l'épinglent à une version et mettent à jour à leur rythme.
Les politiques ne remplacent pas la conception. Les modules de la leçon 1 et de la leçon 2 rendent certains états impossibles en n'exposant pas l'option (un module de bucket qui n'a pas de variable public). C'est plus fort qu'une règle qui l'interdit.
Exercices
Exercice 1 : une précondition et une validation
Dans le module reseau de Signalements, la variable cidr doit être un bloc CIDR valide, inclus dans 172.16.0.0/12, et au moins aussi grand qu'un /24. Écrivez la validation, puis expliquez pourquoi la vérification « le CIDR ne chevauche pas celui d'un autre réseau privé de l'organisation » ne peut pas être une validation de variable.
Solution
variable "cidr" {
type = string
validation {
condition = can(cidrnetmask(var.cidr))
error_message = "cidr doit être un bloc CIDR valide, par exemple 172.16.20.0/22."
}
validation {
condition = can(regex("^172\\.(1[6-9]|2[0-9]|3[01])\\.", var.cidr)) && tonumber(split("/", var.cidr)[1]) <= 24
error_message = "cidr doit être inclus dans 172.16.0.0/12 et avoir un préfixe de 24 bits au plus."
}
}(Ces expressions n'ont pas été exécutées : vérifiez-les avec terraform console.) La règle sur le chevauchement a besoin de données extérieures à la variable : la liste des réseaux déjà alloués, qui vit dans un autre état ou dans une source de vérité externe. Une validation ne voit que des valeurs de la configuration. On la place dans une precondition de la ressource réseau, qui lit une variable reseaux_existants alimentée par l'inventaire, ou dans une politique sur le plan.
Exercice 2 : postcondition ou précondition ?
Pour chaque règle, dites si elle va dans une validation, une precondition, une postcondition, un check ou une politique, et pourquoi : (a) le nom d'un bucket respecte ^sig-(preprod|prod)-[a-z-]+$ ; (b) la base créée a bien le moteur PostgreSQL-16 demandé ; (c) le site répond 200 après le déploiement ; (d) aucune ressource de production n'est supprimée sans l'étiquette suppression:autorisee ; (e) le groupe de sécurité de la base n'ouvre pas le port 5432 à 0.0.0.0/0.
Solution
(a) Validation de la variable nom : forme d'une entrée, connue d'emblée. (b) Postcondition : seule la réponse du fournisseur dit quel moteur a été réellement créé. (c) Bloc check avec source de données scopée : c'est de la santé, pas une barrière, et le contrôle a un sens après l'apply. (d) Politique sur le plan : elle lit change.actions et les étiquettes avant ; c'est une règle d'organisation, qui doit s'appliquer à tous les dépôts. (e) Politique (ou analyse statique comme Trivy, qui connaît ce cas) : la règle d'organisation s'évalue sur change.after des règles du groupe de sécurité ; en complément, le module du groupe de sécurité peut ne pas exposer l'option.
Exercice 3 : trouver la faille
Un collègue écrit la règle Rego deny contains msg if { some rc in input.resource_changes; rc.type == "scaleway_object_bucket_acl"; rc.change.after.acl == "public-read"; msg := "bucket public" }. Donnez deux cas où un bucket exposé passe malgré tout. Corrigez.
Solution
(1) L'ACL vaut public-read-write, authenticated-read ou une autre valeur exposante : la règle teste l'égalité avec une seule valeur, au lieu de vérifier que l'ACL est private. (2) L'ACL est inconnue au plan (calculée depuis une autre ressource) : l'attribut est dans after_unknown, pas dans after, la condition est indéfinie et la règle ne se déclenche pas. Corrections : tester rc.change.after.acl != "private" et ajouter une règle qui refuse after_unknown.acl == true, comme dans la politique de la leçon. On pense aussi à exclure les suppressions (actions != ["delete"]) pour éviter de refuser le retrait d'un bucket public.
Récapitulatif
- Chaque contrôle voit une chose : la validation une variable, la precondition les hypothèses avant d'agir, la postcondition le résultat de l'API, le
checkla santé de l'ensemble (avertissement, jamais un blocage), la politique tous les changements prévus. - Une postcondition arrive après la création : elle protège le graphe, pas l'infrastructure. Pour empêcher, placez la règle avant (validation, precondition, politique).
- Une valeur inconnue au plan n'est pas fausse, elle est différée : un
checkl'indique, une politique la trouve dansafter_unknown, et doit décider quoi en faire. - Le plan JSON (
terraform show -json) contientresource_changes(avecactions,before,after,after_unknown),checks,variables: c'est l'entrée de toute politique. - Une politique Rego exécutée par conftest exprime les règles de l'organisation ; Trivy apporte des règles génériques ; Sentinel et la politique native de Terraform (bêta) sont les voies de la plateforme HCP.
- On place les contrôles du plus rapide au plus tardif, on lance une règle en avertissement avant de la durcir, on teste chaque règle avec un plan qui doit échouer.
Pour aller plus loin
- La page des conditions personnalisées de la documentation de Terraform, et celle des blocs
check, dont la section sur la validation continue dans HCP Terraform. - La page « JSON Output Format » de la documentation interne de Terraform : tous les champs de
resource_changes,prior_state,planned_values. - Le guide Terraform d'OPA, et la documentation de conftest (
verify,pullpour partager des politiques par un registre OCI). - La leçon 5, qui teste les modules avec
terraform test: les conditions de cette leçon y sont exercées. La leçon 7, sur les valeurs qu'un plan ne doit jamais contenir, et la leçon 10, où l'on rejoue les politiques à chaque mise à jour de fournisseur. - Un cours sur la gouvernance et la conformité dans le chapitre « Sécuriser » (à venir).
Sources
- HashiCorp, Terraform : preconditions et postconditions
- HashiCorp, Terraform : blocs check
- HashiCorp, Terraform : format de représentation JSON des plans
- Open Policy Agent, Terraform : évaluer un plan
- Conftest, documentation
- Aqua Security, Trivy : analyse des configurations Terraform
- HashiCorp, Terraform policy (bêta) et politiques HCP Terraform
- Kief Morris, Infrastructure as Code (3e éd., O'Reilly, 2025), chapitres sur le test et la gouvernance