Module 4: Policy As Code With Conftest
2. What Open Policy Agent and Rego are
Description
Open Policy Agent (OPA) is an open-source policy engine, independent of any specific cloud or tool: it receives structured data (JSON, almost always), evaluates it against rules you wrote, and returns a verdict. Rego is the language those rules are written in — declarative, not imperative: instead of describing how to reach a conclusion step by step, you describe what conditions make a conclusion true. conftest — the tool you install in lesson 3 — is a thin layer over OPA, built specifically to evaluate configuration files: YAML, JSON, HCL, and the terraform plan's JSON form you'll use from lesson 5 onward.
Connection to the module
This lesson is purely conceptual — the only one in the whole module with no command executed — and exists so that when lesson 4 has you write your first real rule, you're not learning the syntax and the concept at the same time. Here you separate the two questions: what a declarative policy is (this lesson), from how you write one in the exact syntax the current engine requires (lesson 4).
Analogy: the inspector who reviews the blueprint before the first brick
A building inspector has two possible ways to work. The first: visit the finished building, a year later, and flag which safety codes were violated — the fire escape is too narrow, the wiring isn't at the required distance from water. That work has value, but it arrives late: fixing it now means breaking down a wall that's already built. The second way: the inspector reviews the blueprint, before a single brick exists, and rejects the building permit if the blueprint itself violates a code. That's strictly cheaper work — fixing a blueprint costs a pencil; fixing a building costs a partial demolition.
Policy-as-code with conftest/OPA is the second inspector, applied to infrastructure. The terraform plan is the blueprint — the complete, precise description of what's going to be built, before a single resource actually exists — and a Rego policy is the code that blueprint has to satisfy before anyone approves the apply. This is, literally, what the word "preventive" means in this guide's vocabulary: it isn't that the guardrail acts quickly after the fact, it's that it acts before the fact — the resource created, modified, or destroyed — exists at all. You'll see this distinction once more, named explicitly, in this guide's Module 7, when you compare preventive controls against detective ones like CloudTrail — the inspector who reviews the already-built building.
What OPA does, in exact terms
OPA knows nothing about Terraform, or Kubernetes, or any specific system — and that ignorance is, on purpose, its central feature. OPA does exactly one thing: it receives a JSON document as input, evaluates it against a set of Rego rules, and produces a result. Who generates that JSON, and what format it had before becoming JSON, is completely indifferent to it. This is what makes the same tool you're going to use in this module over a terraform plan be, in other contexts — outside this guide's scope — the same tool teams use to validate Kubernetes manifests, an API's responses, or a firewall's configuration: the engine doesn't change, only the input and the rules you write for it change.
conftest is the layer that makes this generic engine usable from the command line, with no Go code to write and no server to stand up: it takes a file (.yaml, .json, or the .json terraform show -json produces), loads it as input, evaluates the Rego rules from a directory you point it to, and returns PASS or FAIL with the exact message from each rule that was violated. It's, in spirit, the same as a unit-testing framework — pytest, jest — but instead of testing that your code does what it says, it tests that your configuration meets the rules you decided are mandatory.
A Rego rule's minimal anatomy
Every policy in this module, without exception, follows the same shape: a rule called deny, that adds a message to a set when a condition holds. In the syntax current engine versions require — the one you'll confirm yourself, running the real engine, in lesson 4:
package main
deny contains msg if {
input.environment == "production"
input.debug == true
msg := "production environment must not run with debug mode enabled"
}
Five pieces, each with a concrete job:
package main— every.regofile belongs to a package;conftestlooks, by convention, for themainpackage's rules (orconftest, depending on how you invoke it) to decide what to evaluate. Every policy in this module usesmain.deny contains msg if { ... }— declares thatdenyis a set (contains, not a single value), and that each element of that set is added onlyifthe body that follows is true. If the body never holds, thedenyset stays empty — and an emptydenyis, forconftest, aPASS.input.environment,input.debug—inputis the special variable holding the complete document you're evaluating (whatever YAML or JSON is at hand), navigated as if it were a nested dictionary. You never declare it anywhere; the engine automatically populates it with the file you passed it.- The conditions inside
{ }— each line is a condition that has to be true for the rule to fire. Rego evaluates this as an implicit conjunction: every line has to hold at once, as if each line break were anAND. There's no explicitif/elsebecause none is needed — if no condition holds, the rule simply contributes nothing to thedenyset. msg := "..."— the messageconftestwill show you when this rule fires. This is, in practice, the most important part of any real policy: adenywith no clear message is a policy that fails without telling you why.
This shape — deny contains msg if { conditions; msg := "..." } — is the only one you'll need throughout this module. Lessons 6 and 7's three policies are, structurally, this same shape repeated three times, with different conditions over a different input (the terraform plan in JSON, instead of a test YAML).
Rego is declarative, not imperative — and that changes how you read it
If you come from Python or JavaScript, Rego's first surprise is that there's no return, no explicit for loop accumulating a result, no if/elif/else with branches. A Rego rule doesn't compute a result step by step — it declares under which conditions a fact is true, and the engine decides, evaluating every possible combination, whether those conditions hold for the input you gave it. When later on (lesson 7) you need to check every Statement inside an IAM policy, you won't write a for statement in policy.Statements: like in Python — you'll write some statement in policy.Statement, which tells the engine "there exists at least one element of this set for which what follows is true," and the engine takes care of testing every element for you. This paradigm difference is, by far, the hardest part of starting with Rego — and it's exactly what lesson 4 lets you practice with a minimal example, before lesson 7 asks you to iterate over a real, nested structure.
Sentinel, by contrast: the commercial engine you won't use here
HashiCorp has its own policy engine, called Sentinel, native to HCP Terraform (HashiCorp's SaaS platform) and Terraform Enterprise. It conceptually solves the same problem as OPA/conftest — evaluating a plan against rules before applying it — and a free-to-download Sentinel binary does exist. The real difference, the one that matters for this guide, isn't the binary's price: it's that Sentinel's real integration with Terraform is designed around HCP Terraform — the tfplan/v2 import a Sentinel policy uses to read a plan depends on data ("mocks") HCP Terraform automatically generates when it runs your plan on its own remote infrastructure. There's no simple, documented, $0 path to point the Sentinel binary, standalone, at any terraform show -json generated on your own machine — which is precisely what conftest does out of the box, with no platform in between, from this module's lesson 5 onward.
This guide names Sentinel here, once, for the same reason it names other commercial tools in other modules: so you know it exists, what problem it solves, and why this specific guide doesn't build it — not out of ignorance, but because the $0, executable path, verifiable in your own terminal, is conftest. If you ever work on a team with paid HCP Terraform, you'll recognize the same problem — a policy evaluating a plan before the apply — solved with a different syntax and a different platform behind it.
Common mistakes
Expecting Rego to have a return or imperative control flow (paradigm mistake). What happens: someone, coming from an imperative language, looks for where to "return" a rule's result, or writes an explicit if/else inside a deny's body. How to spot it: if your first draft of a Rego rule looks like a function that "computes and returns" instead of a condition that "declares when it's true." How to fix it: a Rego rule computes nothing — it declares under what conditions something belongs to a set (deny, in this module). Every line inside { } is a conjunction (implicit AND); if you need a disjunction (OR), the correct Rego way is writing two separate rules with the same name — each is an alternate way for the condition to hold, not a branch of an if.
Confusing input with data that needs to be declared or imported (syntax mistake). What happens: someone looks, inside the .rego file itself, for where the input variable is defined, or tries to assign it a value. How to spot it: if your Rego file has a line like input := {...}. How to fix it: input is a special variable, automatically populated by conftest with the content of the file you passed it on the command line — it's never declared or assigned inside the policy. Lesson 4 shows you this live: the same .rego file, without changing a line, evaluates input differently depending on which YAML you pass it.
Thinking Rego and Sentinel are interchangeable because they solve "the same problem" (scope mistake). What happens: someone, in a technical interview or a real project, assumes learning Rego means they already know how to write Sentinel, or vice versa. How to spot it: if your answer to "do you know Sentinel?" is "yes, it's the same as Rego." How to fix it: they solve the same problem (evaluating a plan against rules before applying it), but they're two completely different languages, with different syntax and different tool ecosystems — Rego belongs to OPA (Cloud Native Computing Foundation, open source, no single commercial owner), Sentinel is HashiCorp's proprietary language. Knowing one gives you the concept, not the other's syntax.
Exercises
Exercise 1 — Rewrite, in prose, what this Rego rule would do without running it. Before lesson 4, without running any command, read this rule and explain in one sentence what exact condition would make it fire:
deny contains msg if {
input.action == "delete"
input.resource_name == "Shipments"
msg := "cannot delete Shipments"
}
See solution
This rule fires — adds a message to the deny set — only when the input document has, at the same time, an action field with the exact value "delete" and a resource_name field with the exact value "Shipments". If either condition doesn't hold — say, action is "create", or resource_name is "AppServerRole" — the rule contributes nothing to the set, and conftest reports that policy as PASS for that input. This is, in simplified spirit, exactly the mechanism this module's lesson 6 is going to build over the real terraform plan.
Exercise 2 — Explain the difference between "declarative" and "imperative" in your own words, using the inspector example. Without repeating this lesson's text, explain in two or three sentences why Rego is described as declarative, using this same lesson's building inspector analogy.
See solution
A reasonable explanation: an imperative approach would be an inspector following a list of steps ("first measure the staircase, then check the wiring, then compute whether it complies") — it describes how to reach the conclusion. A declarative approach, like Rego, is an inspector who only has a list of conditions the blueprint must meet ("the staircase must measure at least X," "the wiring must be Y meters from water") — they don't care in what order it gets checked, nor how that conclusion is reached, only whether the conditions are true or false for the blueprint in front of them. Rego works the same way: a rule doesn't compute a result step by step, it declares which combination of facts about input makes that rule true.
Exercise 3 — Decide whether Sentinel would be a viable option for a team with no tooling budget. A teammate, with no budget for paid HCP Terraform, asks if they could use Sentinel instead of conftest for this same project. What would you tell them, and why?
See solution
The honest answer acknowledges the nuance without exaggerating: "A free-to-download Sentinel binary exists, so it's technically not a commercial secret, but its real integration with Terraform depends on data ('mocks') HCP Terraform automatically generates when it runs a plan on its own platform — there's no simple, documented way to point Sentinel, standalone, at any terraform show -json, without going through that platform. conftest, on the other hand, is designed from the start to consume any local JSON file, with no platform in between — it's the option with the most direct $0 path for this exact case." If your answer distinguishes "the binary is free" from "the Terraform integration requires the full platform," you have the correct distinction.
Summary and next step
In this lesson you saw what Open Policy Agent is — a generic policy engine, indifferent to its input's source format — and what Rego is — the declarative language its rules are written in — with the exact shape (deny contains msg if { ... }) you'll use in every policy in this module. You saw, with the blueprint-reviewing inspector analogy, why this approach is called preventive. And you named Sentinel, HashiCorp's commercial engine, with the precise distinction for why this guide doesn't build it: not for lack of a free binary, but because its real Terraform integration lives inside a paid platform, while conftest solves the same problem with no platform in between.
Before moving on you should be able to: write a minimal deny rule's shape from memory; explain why Rego is declarative, not imperative, in your own words; and distinguish what Sentinel does from what OPA/conftest does, without confusing "has a free binary" with "has a $0 path for this use case."
Lesson 3 leaves the theory behind: you're going to install conftest for real, on your own machine, and confirm the exact engine version you'll use for the rest of this module.
Resources
- Open Policy Agent — Official documentation — the engine
conftestis built on, this entire module's technical foundation. - Open Policy Agent — Policy Language (Rego) — Rego's complete syntax reference, including the
ifandcontainskeywords you'll confirm live in lesson 4. - Conftest — Official documentation — the layer that turns OPA into a command-line tool, installed in lesson 3.
- HashiCorp — Sentinel — the commercial engine named by contrast in this lesson.
terraform-and-iac-guide, Module 8, lesson 4 — the exact point where that guide namedconftestwithout building it, the promise this module fulfills.