Conftest dans le CI : policies YAML et Terraform

À la fin de ce tutoriel, vous saurez installer Conftest, écrire des policies Rego sur des manifests Kubernetes YAML et des plans Terraform JSON, brancher un job fail-closed dans GitHub Actions (variante GitLab CI), et bloquer un merge qui viole vos garde-fous — sans attendre Gatekeeper en cluster.

Niveau : Intermédiaire · Temps estimé : 45–60 min · Versions testées : Conftest (OPA), Rego, Terraform 1.5+, kubectl manifests, GitHub Actions, GitLab CI · Dernière vérification : 2026-09-11 · Region : ca-central-1

Slug : wow-conftest-ci · Série : WOW (10/50) · Mot-clé SEO : conftest ci · Publish : READY (feu vert Maître)

← Précédent : Policy-as-Code OPA/Gatekeeper · → Suivant : HashiCorp Vault pour secrets DevOps · Aussi : Tests Terraform 2026 · Trivy en CI

Prérequis

  • Bases Git + CI (GitHub Actions ou GitLab CI)
  • Notions Kubernetes (Deployment, SecurityContext) — Kubernetes
  • Terraform installé localement pour le lab plan JSON — Installer Terraform
  • Optionnel : profil AWS lab en ca-central-1 si vous générez un vrai plan S3
  • Aucun cluster Gatekeeper requis pour ce lab : Conftest tourne hors cluster, dans le pipeline

Coût estimé : 0 € en local / runners CI. Pas de ressources AWS obligatoires ; si vous créez un bucket de lab, détruisez-le (terraform destroy) et restez Free Tier.

export AWS_PROFILE=lab
export AWS_DEFAULT_REGION=ca-central-1

Ce que nous allons construire

Lab Conftest CI (WOW 10/50) — ca-central-1
  ├── policy/k8s : deny privileged + require resources
  ├── policy/tf  : deny S3 public + require tags + region
  ├── fixtures YAML + terraform show -json
  ├── GitHub Actions : conftest test (fail-closed)
  ├── Variante GitLab CI
  └── Troubleshooting + FAQ + quiz

(Schéma — alt : « Pull request → Conftest (Rego) sur YAML K8s et plan Terraform → merge seulement si policies OK ».)

Étape 1 — Pourquoi Conftest dans le CI ?

OPA (Open Policy Agent) évalue des policies Rego. Gatekeeper applique OPA dans Kubernetes (admission). Conftest applique le même moteur sur des fichiers : YAML, JSON, HCL via JSON, Dockerfile, etc. — idéal avant le merge.

Outil Quand Force Limite
Conftest CI / pre-commit / local Rapide, multi-formats, fail-closed Ne voit pas le runtime cluster
Gatekeeper Admission K8s Bloque kubectl apply live Trop tard si le YAML est déjà mergé
terraform test / OPA TF Plan / modules Couverture IaC profonde Moins « multi-YAML » générique
Checkov / tfsec Scan IaC Règles prêtes Moins flexible qu’un Rego maison

Chez DevOps Elastic Hayway, le pattern gagnant : Conftest en CI (shift-left) + OPA/Gatekeeper en prod (défense en profondeur). Voir aussi Policy-as-Code OPA/Gatekeeper et Tests Terraform 2026.

Étape 2 — Installer Conftest

# Linux amd64 — adaptez la version sur https://github.com/open-policy-agent/conftest/releases
CONFTEST_VERSION=0.56.0
curl -sL "https://github.com/open-policy-agent/conftest/releases/download/v${CONFTEST_VERSION}/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz" 
  | tar -xz conftest
sudo mv conftest /usr/local/bin/
conftest --version

Sous macOS : brew install conftest. Sur les runners GitHub, préférez l’image officielle openpolicyagent/conftest plutôt qu’un binaire ad hoc.

Initialisez le lab :

mkdir -p ~/lab-conftest-ci/{policy/k8s,policy/tf,fixtures/k8s,tf-lab}
cd ~/lab-conftest-ci

Étape 3 — Première policy Rego sur YAML Kubernetes

Créez un Deployment volontairement mauvais (privileged, sans requests/limits) :

cat > fixtures/k8s/bad-deploy.yaml << 'YAML'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-bad
spec:
  replicas: 1
  selector:
    matchLabels: { app: api }
  template:
    metadata:
      labels: { app: api }
    spec:
      containers:
        - name: api
          image: ghcr.io/example/api:1.0.0
          securityContext:
            privileged: true
YAML

Policy Conftest (package = dossier relatif sous policy/) :

cat > policy/k8s/deny.rego << 'REGO'
package main

deny[msg] {
  input.kind == "Deployment"
  c := input.spec.template.spec.containers[_]
  c.securityContext.privileged == true
  msg := sprintf("container %q ne doit pas être privileged", [c.name])
}

deny[msg] {
  input.kind == "Deployment"
  c := input.spec.template.spec.containers[_]
  not c.resources.requests.cpu
  msg := sprintf("container %q : resources.requests.cpu obligatoire", [c.name])
}

deny[msg] {
  input.kind == "Deployment"
  c := input.spec.template.spec.containers[_]
  not c.resources.limits.memory
  msg := sprintf("container %q : resources.limits.memory obligatoire", [c.name])
}
REGO

Testez :

conftest test fixtures/k8s/bad-deploy.yaml --policy policy/k8s
echo exit:$?

Conftest doit échouer (exit ≠ 0) avec les messages deny. Corrigez le fixture (privileged false + requests/limits) et relancez jusqu’à PASS. C’est le cœur du HowTo : la CI doit échouer comme votre terminal.

Étape 4 — Policies Terraform via JSON de plan

Conftest lit du JSON. Générez un plan Terraform, puis terraform show -json :

cd ~/lab-conftest-ci/tf-lab
cat > main.tf << 'TF'
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}
provider "aws" { region = "ca-central-1" }

resource "aws_s3_bucket" "lab" {
  bucket = "deh-conftest-lab-CHANGE-ME"
  tags   = { Project = "deh-lab", Owner = "platform" }
}

# Exemple volontairement risqué à bloquer en policy :
# resource "aws_s3_bucket_public_access_block" "lab" { ... public ACL ... }
TF

Sans credentials AWS, vous pouvez committer un plan JSON de fixture (généré une fois en lab). Structure utile pour Rego : resource_changes[_].type, .change.after, .change.actions.

cat > ../policy/tf/s3.rego << 'REGO'
package main

deny[msg] {
  rc := input.resource_changes[_]
  rc.type == "aws_s3_bucket"
  after := rc.change.after
  after.acl == "public-read"
  msg := sprintf("S3 %v : acl public-read interdit", [rc.address])
}

deny[msg] {
  rc := input.resource_changes[_]
  rc.type == "aws_s3_bucket"
  after := rc.change.after
  not after.tags.Project
  msg := sprintf("S3 %v : tag Project obligatoire", [rc.address])
}

deny[msg] {
  # provider_config / planned values selon version terraform show -json
  cfg := input.configuration.provider_config.aws
  region := cfg.expressions.region.constant_value
  region != "ca-central-1"
  msg := sprintf("région provider AWS %v : seules ca-central-1 autorisée en lab DEH", [region])
}
REGO

Exécution typique :

terraform init -backend=false
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
conftest test plan.json --policy ../policy/tf

Astuce DEH : gardez plan.json en artefact CI éphémère (noms/ARNs possibles, pas de secrets).

Étape 5 — Brancher Conftest dans GitHub Actions

Workflow fail-closed sur PR (YAML + plan TF si présent) :

# .github/workflows/conftest.yml
name: conftest-policies
on:
  pull_request:
    paths:
      - "deploy/**/*.yaml"
      - "infra/**"
      - "policy/**"
      - ".github/workflows/conftest.yml"
jobs:
  policy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Conftest K8s YAML
        uses: docker://openpolicyagent/conftest:latest
        with:
          args: test deploy/ --policy policy/k8s
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_wrapper: false
      - name: Terraform plan JSON
        working-directory: infra
        run: |
          terraform init -backend=false
          terraform plan -out=tfplan
          terraform show -json tfplan > plan.json
      - name: Conftest Terraform plan
        uses: docker://openpolicyagent/conftest:latest
        with:
          args: test infra/plan.json --policy policy/tf

Lecture repo suffit sans secrets si init -backend=false. En prod DEH, OIDC (GitHub Actions OIDC AWS) pour un plan réel ca-central-1. Check required en branch protection.

Étape 6 — Variante GitLab CI

# .gitlab-ci.yml (extrait)
stages: [policy]
conftest-k8s:
  stage: policy
  image:
    name: openpolicyagent/conftest:latest
    entrypoint: [""]
  script:
    - conftest test deploy/ --policy policy/k8s
conftest-tf:
  stage: policy
  image: hashicorp/terraform:1.9
  script:
    - cd infra && terraform init -backend=false
    - terraform plan -out=tfplan && terraform show -json tfplan > plan.json
    - apk add --no-cache curl && curl -sL ... # ou image multi-stage avec conftest
    - conftest test plan.json --policy ../policy/tf

Préférez une image ci-conftest (Terraform + Conftest) pour éviter d’installer à chaque job.

Étape 7 — Bundles et fail-closed

  • Séparez packages Rego (k8s.security, tf.aws) si vous mutualisez plusieurs dossiers.
  • Versionnez policy/ (repo app ou platform-policies) ; taggez les releases comme du code.
  • Fail-closed : exit ≠ 0 = job rouge. Jamais continue-on-error: true « pour voir ».
  • Exceptions : data files Rego documentés (owner, ticket, expiry) plutôt que commenter la règle.
  • Complément : Conftest ≠ CVE — ajoutez Trivy.

Troubleshooting

Symptôme Cause / correction
no policies found Mauvais --policy path ; package Rego attendu sous ce dossier
Always PASS alors que deny attendu Mauvais input : testez avec conftest parse / inspectez le JSON
Rego compile error conftest verify --policy policy/ ; regardez [_] vs règles if selon version
Plan JSON énorme / lent Filtrez resource_changes ou découpez policies par type
OK en local, KO en CI Versions Conftest/Terraform différentes ; pinez le tag image
Gatekeeper OK, CI KO (ou inverse) Policies non synchronisées — même source Rego pour Conftest et contraintes

FAQ

Conftest remplace-t-il Gatekeeper ?
Non. Conftest shift-left en CI ; Gatekeeper (ou Kyverno) reste la barrière admission cluster. Les deux se complètent.

HCL brut ou JSON de plan ?
Le plan JSON (terraform show -json) est le contrat le plus fiable pour l’IaC appliqué. Le HCL source peut diverger du plan réel (modules, locals).

Où mettre les policies ?
Dans le repo applicatif pour commencer (policy/), puis extrayez un repo platform-policies quand plusieurs équipes partagent les mêmes règles.

Peut-on tester sans AWS ?
Oui : fixtures JSON de plan + terraform init -backend=false. Gardez ca-central-1 dans les exemples DEH même en mock.

Rego est-il obligatoire ?
Conftest est centré Rego/OPA. Pour des règles « batteries included » sans Rego, regardez Checkov/tfsec — moins flexibles pour du métier custom.

Quiz (5 questions)

1. Conftest évalue surtout :
– A. Uniquement l’admission K8s en live
– B. Des fichiers (YAML/JSON/…) via policies Rego, souvent en CI
– C. Les CVE d’images Docker

2. Pour Terraform, le flux robuste est :
– A. Lire le README du module
– B. terraform plan puis terraform show -json + conftest test
– C. Committer l’état terraform.tfstate dans Git

3. Un job Conftest avec continue-on-error: true :
– A. Renforce le fail-closed
– B. Affaiblit la barrière (deny ignoré)
– C. Remplace Gatekeeper

4. La région lab DEH dans ce tuto est :
– A. us-east-1 uniquement
– B. ca-central-1
– C. eu-west-3 obligatoire

5. Conftest + Trivy ensemble :
– A. Redondants à 100 %
– B. Complémentaires : policies métier (Conftest) + CVE/misconfig (Trivy)
– C. Interdits sur GitHub Actions

Réponses : 1‑B · 2‑B · 3‑B · 4‑B · 5‑B

Pour aller plus loin

Maillage série WOW

← Précédent Policy-as-Code OPA/Gatekeeper
→ Suivant HashiCorp Vault pour secrets DevOps
Aussi Tests Terraform · Trivy · DevSecOps

Meta publication (SEO)

  • Title SEO : Conftest dans le CI : policies YAML et Terraform (guide FR)
  • Meta description : Conftest + Rego en CI : policies Kubernetes YAML et plan Terraform JSON, lab GitHub Actions / GitLab fail-closed, FAQ. Région ca-central-1 — DevOps Elastic Hayway.
  • Focus keyword : conftest ci
  • Secondary : conftest terraform, conftest kubernetes, policy as code ci, rego ci
  • Image : assets/web/devopelastichayway/cover-wow-conftest-ci-1200x630.webp (à générer)
  • Catégorie : WOW / Policy-as-Code · Niveau : Intermédiaire
  • URL cible : https://devopelastichayway.com/tutoriels/wow-conftest-ci/
  • Post live : N/A (nouveau) · slug wow-conftest-ci · Publish : READY (auto-push WP-CLI autorisé)

← Catalogue Tutoriels

Pour aller plus loin — hubs live

Retour parcours Catalogue Tutoriels — hub de la série et leçons sœurs.