Aller au contenu
Validations, vérifications et politiques

Validations, vérifications et politiques

300 Concevoir ⏱ 1 h 30 terraformopentofuscalewayiachcl

À la fin, vous saurez

  • Choisir entre validation de variable, precondition, postcondition, bloc check et politique sur le plan, selon ce que chacun peut observer
  • Écrire des preconditions et des postconditions qui arrêtent une opération avec un message actionnable
  • Écrire un bloc check avec une source de données scopée et expliquer pourquoi il ne bloque jamais
  • Lire la structure du plan JSON (resource_changes, after, after_unknown) et en tirer une règle
  • Rédiger une politique Rego exécutable par conftest qui interdit un bucket public et exige des étiquettes
  • Placer chaque contrôle au bon endroit de la chaîne : poste, demande de fusion, apply, exploitation

Prérequis

Testé avec conftest d'après la documentation opa politiques en Rego v1 scaleway 2.84.0 terraform 1.16.1 , vérifié le 5 octobre 2026

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écanismeQuand il s'exécuteCe qu'il voitÉchec
validation d'une variableà la lecture de la valeurcette variable (et, depuis Terraform 1.9, d'autres objets du module)erreur, rien ne commence
preconditionavant d'agir sur la ressourcetout ce qui est connu à ce momenterreur, la ressource n'est pas touchée
postconditionaprès l'opération sur la ressourcela ressource telle que l'API l'a renvoyée (self)erreur, les dépendants ne sont pas traités
bloc checken dernier, à la fin du plan et de l'applytoute la configuration, plus ses propres sources de donnéesavertissement, l'opération continue
politique sur le planaprès le plan, avant l'applytous les changements prévus, sous forme de JSONdé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, self dé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.json

Le 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, mode identifient la ressource. Les politiques filtrent presque toujours sur type (par exemple scaleway_object_bucket_acl) et mode == "managed" (les sources de données, de mode data, sont aussi dans la liste).
  • change.actions est 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.after est la valeur planifiée, change.before la valeur actuelle (null à la création).
  • change.after_unknown marque les attributs connus seulement après l'apply. Ils sont absents de after. Une politique qui lit after.acl quand 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_sensitive marque les valeurs sensibles, que terraform show -json masque. Leçon 7 : une valeur éphémère ou en écriture seule n'apparaît pas du tout.
  • checks contient le résultat de chaque bloc check et 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 :

  1. aucun bucket ne doit avoir une ACL autre que private ;
  2. tout bucket doit porter les étiquettes env et app.

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.json

conftest 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 || true dans le pipeline.

En production

Où placer chaque contrôle. Un chemin qui marche pour une équipe comme celle de Signalements :

  1. Poste et pre-commit : terraform fmt -check, terraform validate, trivy config. Rapide, sans identifiant.
  2. 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.
  3. Demande de fusion, après le plan : terraform show -json puis conftest test. Le plan est joint à la demande de fusion ; la politique en bloque la fusion si une règle deny se déclenche.
  4. 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).
  5. 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 check la 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 check l'indique, une politique la trouve dans after_unknown, et doit décider quoi en faire.
  • Le plan JSON (terraform show -json) contient resource_changes (avec actions, 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, pull pour 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).
Voir ma constellation →

Sources