Módulo 4: Policy As Code With Conftest
4. Manos a la obra: tu primera política Rego
Descripción
Esta lección escribe, corre, y ve fallar y pasar tu primera política Rego real — sobre un YAML de prueba, deliberadamente simple, antes de acercarte a Terraform. Vas a ver un error real de sintaxis (el que la lección 3 ya anticipó), la sintaxis correcta que sí compila contra conftest 0.69.0/OPA 1.19.0, un FAIL real con el mensaje de tu propia regla, y un PASS real después de corregir el dato de entrada — sin cambiar una sola línea de la política.
Conexión con el módulo
Todo lo que la lección 2 explicó en abstracto —package main, deny contains msg if, input— lo escribes aquí con tus propias manos, contra el motor real. El input de esta lección es un YAML trivial, elegido a propósito: la lección 5 reemplaza ese input por el terraform plan completo de Andes Cargo, pero la mecánica de conftest test —cargar un archivo, evaluarlo contra policy/, reportar PASS o FAIL— es exactamente la misma en ambos casos.
El escenario: un flag de depuración que no debería llegar a producción
Un archivo de configuración de la app de tracking de Andes Cargo, simplificado a lo esencial para esta lección:
# app-config.yaml
service: tracking-app
environment: production
debug: true
La regla que quieres exigir es simple de decir en prosa: ningún archivo de configuración marcado como environment: production puede tener debug: true — un flag de depuración activo en producción expone información interna (trazas, variables de entorno, consultas SQL completas en el error) a cualquiera que pueda ver esa salida. Vas a escribir esa regla en Rego, exactamente como la dirías en prosa.
Paso 1 — El error real, primero: la sintaxis que ya no compila
Antes de la versión que sí funciona, vale la pena ver el error que la lección 3 anticipó — es el error más común que vas a encontrar buscando ejemplos de Rego en internet, porque la mayoría de los tutoriales existentes se escribieron antes de que esta sintaxis fuera la exigida por defecto:
# policy/debug_flag.rego — versión antigua, NO compila contra OPA 1.19.0
package main
deny[msg] {
input.environment == "production"
input.debug == true
msg := "production environment must not run with debug mode enabled"
}
conftest test app-config.yaml -p policy/
Qué esperar (literal, ejecutado para escribir esta lección):
Error: running test: load: loading policies: load: 2 errors occurred during loading:
policy/debug_flag.rego:3: rego_parse_error: `if` keyword is required before rule body
policy/debug_flag.rego:3: rego_parse_error: `contains` keyword is required for partial set rules
Dos errores, en la misma línea, y los dos son la misma causa: deny[msg] { ... } es la sintaxis de Rego anterior a la versión 1 del lenguaje (lo que la documentación oficial llama "Rego v0"). OPA 1.19.0 —el motor que conftest 0.69.0 trae empaquetado, confirmado en la lección anterior— exige, por defecto, la sintaxis de Rego v1: la palabra clave if antes del cuerpo de la regla, y la palabra clave contains para declarar que deny es un conjunto parcial (no un único valor). El motor no adivina qué quisiste decir — falla, con un mensaje de error preciso, señalando la línea exacta y la palabra clave exacta que falta.
Paso 2 — La sintaxis correcta, la única que vas a usar en este módulo
# policy/debug_flag.rego
package main
deny contains msg if {
input.environment == "production"
input.debug == true
msg := "production environment must not run with debug mode enabled"
}
El único cambio: deny[msg] se convirtió en deny contains msg if. El resto de la regla —las tres líneas dentro de { }— no cambió en absoluto, porque la lógica que describen nunca dependió de la sintaxis del encabezado.
Paso 3 — conftest test, contra el debug: true original: FAIL
conftest test app-config.yaml -p policy/
Qué esperar (literal, ejecutado para escribir esta lección):
FAIL - app-config.yaml - main - production environment must not run with debug mode enabled
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
Lee esta línea de izquierda a derecha, porque este es el formato exacto que vas a ver en cada política del resto de este módulo: FAIL (el veredicto), app-config.yaml (el archivo evaluado — el input), main (el paquete de la política que disparó), y el mensaje literal que tu propio msg := "..." definió. La línea de resumen —1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions— cuenta cuántas reglas se evaluaron en total (1 test, porque esta política tiene una sola regla deny), no cuántos archivos.
El código de salida del proceso también importa, y vas a depender de él más adelante para encadenar conftest dentro de un pipeline (Módulo 8 de esta guía):
echo $?
Qué esperar (literal):
1
Un FAIL siempre termina con código de salida distinto de cero — la señal exacta que un job de CI necesita para saber que debe detener el pipeline, sin tener que interpretar la salida de texto.
Paso 4 — Corrigiendo el dato, no la política: PASS
La política no cambia — el problema no está en la regla, está en el archivo que se supone que cumpla esa regla:
# app-config.yaml
service: tracking-app
environment: production
debug: false
conftest test app-config.yaml -p policy/
Qué esperar (literal, ejecutado para escribir esta lección):
1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions
echo $?
Qué esperar (literal):
0
Fíjate en un detalle real, no cosmético: cuando todo pasa, conftest no imprime ninguna línea por archivo — solo el resumen final. La ausencia de líneas FAIL es el resultado; no hay ninguna línea PASS - app-config.yaml... esperando que la busques. Este es exactamente el comportamiento —silencioso cuando todo está bien, ruidoso solo cuando algo falla— que hace que conftest sea legible dentro de un log de CI con cientos de líneas: lo único que necesitas buscar es la palabra FAIL.
Profundización: por qué la regla no cambió, y el dato sí
Este patrón —política fija, dato que varía— es la esencia completa de policy-as-code, y vale la pena nombrarlo explícitamente antes de avanzar: escribiste una regla, una sola vez, y la corriste contra dos versiones del mismo archivo sin tocar una línea de Rego. Esto es exactamente lo que hace que una política sea reutilizable de una forma en que una revisión manual nunca puede serlo — la misma regla que acabas de correr contra un YAML de once líneas es, estructuralmente, la misma forma que vas a correr, desde la lección 6, contra un terraform plan con cientos de líneas de JSON. conftest test <archivo> -p policy/ no cambia; lo único que cambia es qué archivo le pasas, y qué reglas viven dentro de policy/.
Errores comunes
Copiar un ejemplo de Rego de un tutorial o de Stack Overflow escrito antes de Rego v1 (el error de esta lección, en la práctica). Qué pasa: alguien busca "conftest deny rule example" y encuentra un resultado con deny[msg] { ... }, lo copia sin cambios, y se encuentra con el mismo rego_parse_error del Paso 1. Cómo detectarlo: el mensaje exacto — if keyword is required before rule body — es inconfundible; cualquier vez que lo veas, la causa es sintaxis de Rego v0 contra un motor que exige v1 por defecto. Cómo corregirlo: agrega if antes de { y cambia deny[msg] por deny contains msg — el resto de la regla no necesita ningún otro cambio, como viste en el Paso 2.
Buscar una línea PASS en la salida cuando todo funciona, y concluir que conftest no corrió nada (de expectativa). Qué pasa: alguien corre conftest test sobre un archivo que cumple todas las reglas, no ve ninguna línea con la palabra PASS, y asume que el comando falló silenciosamente o que no evaluó nada. Cómo detectarlo: si buscas la palabra literal PASS en la salida y no la encuentras, aunque el resumen final diga 1 test, 1 passed. Cómo corregirlo: conftest solo imprime una línea por cada regla que falla; cuando todo pasa, la única evidencia es el resumen final (N passed) y el código de salida 0. Confirma siempre con echo $? si tienes dudas, en vez de buscar una palabra que el formato de esta herramienta, a propósito, no imprime.
Modificar la política para "hacer que pase" en vez de corregir el dato que la viola (de intención, el error más peligroso de los tres). Qué pasa: alguien, frente a un FAIL que no esperaba, edita la regla Rego para que deje de disparar —por ejemplo, cambiando input.debug == true por una condición que nunca se cumple— en vez de corregir el archivo de configuración real que tiene el problema. Cómo detectarlo: si tu primer instinto frente a un FAIL es abrir el archivo .rego, no el archivo que estás evaluando. Cómo corregirlo: una política que empieza a fallar casi siempre significa que encontró un problema real —esa es, literalmente, su función—; la corrección correcta, en la enorme mayoría de los casos, es arreglar el dato que la violó (como hiciste en el Paso 4), no silenciar la regla. Vas a ver la versión seria de este mismo error, con consecuencias reales, en el Módulo 8 de esta guía, cuando compares un "arreglo real" con un intento de debilitar una política para que un cambio malo pase de todas formas.
Ejercicios
Ejercicio 1 — Escribe una segunda condición para la misma regla, y confírmala en vivo. Extiende debug_flag.rego para que también dispare si environment es "staging" (no solo "production"), usando una segunda regla deny (no modifiques la primera). Corre conftest test contra un app-config.yaml con environment: staging y debug: true, y confirma el resultado real.
Ver solución
package main
deny contains msg if {
input.environment == "production"
input.debug == true
msg := "production environment must not run with debug mode enabled"
}
deny contains msg if {
input.environment == "staging"
input.debug == true
msg := "staging environment must not run with debug mode enabled"
}
Contra un app-config.yaml con environment: staging y debug: true, conftest test app-config.yaml -p policy/ debería mostrar FAIL - app-config.yaml - main - staging environment must not run with debug mode enabled, y el resumen 2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions — dos reglas deny evaluadas en total (una por cada bloque), una de las cuales (la de production) no aplica a este input y por lo tanto "pasa" (no dispara), y la otra (la de staging) sí dispara. Este ejercicio es exactamente el patrón que vas a usar en la lección 7, donde dos condiciones distintas de mínimo privilegio conviven como dos reglas deny separadas dentro del mismo archivo.
Ejercicio 2 — Predice el código de salida antes de correrlo. Sin ejecutar nada todavía, para cada uno de estos tres casos, predice si conftest test terminaría con código de salida 0 o distinto de cero: (a) un archivo que cumple todas las reglas; (b) un archivo que viola una regla; (c) un directorio policy/ vacío, sin ningún archivo .rego.
Ver solución
(a) código 0 — todo pasó, como confirmaste en el Paso 4. (b) código distinto de cero (1) — al menos una regla disparó, como confirmaste en el Paso 3. (c) esto es el caso interesante: un directorio policy/ vacío no tiene ninguna regla que evaluar, así que conftest no puede reportar ningún FAIL — pero tampoco es un PASS significativo, porque no verificó nada. En la práctica, conftest trata esto como una condición de error explícita (no encontró ninguna política para cargar), no como un PASS silencioso — la herramienta está diseñada para que "no hay reglas" nunca se confunda con "las reglas pasaron", exactamente el error que el Ejercicio 3 del Módulo 4, lección 1, ya te hizo predecir en abstracto.
Ejercicio 3 — Explica, a alguien que nunca vio conftest, qué significa "1 test" en el resumen final. El resumen 1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions usa la palabra "test" — ¿test de qué, exactamente? Explica en una frase qué unidad cuenta ese número, usando lo que aprendiste en esta lección.
Ver solución
Cada "test" es una evaluación de una regla deny (o warn) contra un documento input específico — no un archivo, ni una línea de configuración. Si tu policy/ tiene tres reglas deny distintas y evalúas un solo archivo YAML, conftest reporta "3 tests" (una por regla), incluso si las tres viven en el mismo archivo .rego. El Ejercicio 1 de esta misma lección lo demuestra en vivo: dos reglas deny, un solo archivo app-config.yaml, resultado "2 tests" — el conteo sigue las reglas, no los archivos de ningún lado.
Resumen y siguiente paso
En esta lección escribiste tu primera política Rego real, viste el error exacto de sintaxis que la versión antigua del lenguaje produce contra el motor actual (rego_parse_error, literal), la corregiste con deny contains msg if, y confirmaste FAIL y PASS reales sobre el mismo archivo de política, cambiando solo el dato de entrada. Confirmaste también el código de salida (1 en FAIL, 0 en PASS) — la señal que un pipeline de CI usa para decidir si continúa.
Antes de avanzar deberías poder: escribir una regla deny contains msg if { ... } desde cero, sin copiar ningún ejemplo; explicar por qué conftest no imprime ninguna línea PASS cuando todo funciona; y reconocer el error rego_parse_error: if keyword is required a primera vista, sin tener que buscarlo.
La lección 5 reemplaza el YAML trivial de esta lección por el input real de todo el resto de este módulo: el terraform plan de andes-cargo-infra/, convertido a JSON con terraform show -json — la misma mecánica de conftest test, sobre una estructura mucho más rica.
Recursos
- Open Policy Agent — Policy Language, Rules — la referencia oficial de
ifycontains, las dos palabras clave que esta lección confirmó en vivo. - Conftest — Testing your first policy — el tutorial oficial que sigue el mismo patrón —YAML de prueba antes de un caso real— que esta lección.
- Este módulo, lección 2 — la anatomía completa de una regla
deny, la base conceptual de todo lo que escribiste aquí. - Este módulo, lección 3 — la instalación y verificación de versión que explica por qué el Paso 1 de esta lección produce el error que produce.