CoursesOPA & RegoInstall, eval & the REPL

Install, eval & the REPL

Run your first query.

Advanced10 min · lesson 2 of 12

Before you wire a policy engine into an API gateway (the front door every request to your service passes through) or a Kubernetes admission webhook (the checkpoint that inspects every object before the cluster will store it), you want a bench to bang on. Something like a pocket calculator, where you can test a formula before you trust it inside a spreadsheet that other people depend on. OPA (Open Policy Agent) ships as one self-contained binary, and that binary is the bench.

OPA is a single static Go binary. There is no runtime to install underneath it and no package tree to resolve. It gives you two ways to ask a question. opa eval takes one query, answers it, and exits, the way typing a sum and hitting equals does. opa run opens a REPL (read-eval-print loop, an interactive shell) where you type Rego line by line and watch each line evaluate. Pin a version before you do either one. OPA 1.0 made if and contains mandatory in rule syntax, so the same file can compile happily on one machine and be rejected on another.

Get the binary

Installing OPA means copying one executable somewhere on your PATH (the list of directories your shell searches when you type a command). That is the whole procedure. Nothing to configure, nothing to link. You have three routes: download a pinned release straight from the download endpoint, let a package manager fetch it, or pull the official container image if you are running it in CI (continuous integration, the automated build that fires on every commit) or as a sidecar (a second container riding along in the same pod as your app). Pin the version whichever route you take. OPA has been past 1.0 since December 2024 and follows semantic versioning (the major.minor.patch scheme where only a major bump is allowed to break you), but a major release can still change defaults, and 1.0 is the one that made if and contains mandatory. opa version confirms the install and prints the build, the Go version it was compiled with, and the Rego version it speaks.

terminal
curl -L -o opa https://openpolicyagent.org/downloads/v1.18.2/opa_linux_amd64_static
chmod 755 ./opa
./opa version
output
Version: 1.18.2
Build Commit: ...
Go Version: go1.23.x
Rego Version: v1
terminal
brew install opa
opa version
output
==> Pouring opa...
Version: 1.18.2

One-shot answers with opa eval

opa eval takes a query as a string, works it out once, and exits. Hand it no files at all and it behaves like a calculator for expressions, which makes it the quickest way to check what a built-in function returns or whether a comparison means what you assumed. Add --format pretty and results print as plain values instead of arriving inside the verbose JSON (JavaScript Object Notation, the text format OPA reads and writes) envelope. Two flags supply the facts. --data loads policy .rego files or JSON data. --input loads the request document you are deciding about. Then you query the tree of documents OPA builds in memory. Everything a policy defines hangs off data, so data.authz.allow is you saying: evaluate the allow rule in package authz against this input.

terminal
opa eval --format pretty '"hello" == "hello"'
output
true
terminal
opa eval --format pretty 'numbers.range(1, 5)'
output
[1, 2, 3, 4, 5]
terminal
opa eval --data policy.rego --input request.json --format pretty 'data.authz.allow'
output
true
How a query becomes an answer
1Query string
opa eval / REPL prompt
2Load & compile
--data policy, --input facts
3Evaluate
search rule bodies
4Result
JSON or pretty value
eval and the REPL run the same pipeline. The REPL keeps it loaded between queries.

Iterate live in the REPL

opa run with no server flags drops you at an interactive prompt instead. Load policy on the command line, then type queries at the > prompt and read the answers as they come. Spend your first hour with Rego here. Try a built-in, test a comparison, poke at the data you loaded, all without retyping a command each time. A handful of meta-commands come with it: show prints the current module, unset clears a definition you regret, json switches the output format, and exit (or Ctrl-D) gets you out.

terminal
opa run example.rego
# at the > prompt:
> 1 + 1
> numbers.range(1, 3)
> help
output
2
[1, 2, 3]
Meta commands:
show — show current module
unset — remove a definition
json — toggle JSON output
exit — leave the REPL

The online Rego Playground

The Rego Playground at play.openpolicyagent.org is the same engine running in a browser tab. You load policy, set input, run queries, and share the whole thing as a link. That last part is what earns its keep. When a rule misbehaves on your laptop, pasting it into the Playground tells you within a minute whether the bug lives in the policy or in the way you loaded files locally. Treat it as a paste bin that actually evaluates what you paste.

Query paths and undefined results

Most "my policy is not working" moments come down to a query path that does not match the package. If your file says package example.authz, you have to query data.example.authz.allow. Query data.authz.allow and you get nothing back. Nothing back is the trap. Ask OPA for a path that does not exist and it hands you undefined with exit code 0, which a shell script happily reads as success.

terminal
opa eval -d policy.rego --format pretty 'data.nonexistent.allow'
output
# empty output — undefined, not an error
# exit code: 0
eval's exit code answers a different question than you asked
Nearly everyone gets caught by this once. You wire opa eval 'data.authz.allow' into a CI gate, expect a non-zero exit when the policy denies, and then watch the build stay green no matter what you feed it. opa eval exits 0 whenever evaluation succeeded, whether the answer came back true, false, or undefined. Exit 0 means "OPA ran fine". It does not mean "the answer is true". Use --fail to exit non-zero when the result is undefined or empty, and --fail-defined when a matching deny rule should break the build. Pair either one with --format raw or a jq check (jq is the small command-line tool for pulling values out of JSON) so the pipeline asserts on the decision itself, not on whether the binary crashed.

Pin OPA to one version across laptops, CI, and sidecars, the same discipline you already apply to kubectl or Terraform. Drift between 0.x and 1.x Rego syntax is the expensive kind of drift: a policy compiles on your machine and fails in the pipeline, and you go looking for a bug that was never in the rule. The container image openpolicyagent/opa:<tag> holds that same binary, so using it in Actions and in sidecars keeps eval, test, and runtime agreeing with each other.

Server mode preview

opa run --server loads policy once and answers over HTTP (hypertext transfer protocol, the request-and-response protocol your browser speaks). Bundles and the decision API get a lesson of their own in op-bundles, but it is worth seeing the shape now, because install lessons often stop at eval and leave you thinking OPA is only a command you shell out to. The server exposes /v1/data for decisions and /health for probes. A sidecar calls localhost:8181 and gets an answer back from a process that is already warm. It does not fork opa eval per request.

terminal
opa run --server --log-level info policy.rego &
curl -s localhost:8181/health
output
{"healthy":true}

The same binary runs on Windows and on macOS. Only the way you install it changes. If you work in WSL (Windows Subsystem for Linux), install the Linux amd64 build inside WSL so your laptop matches production CI. And if you would rather install nothing at all, the container image does the entire job: docker run --rm -v $PWD:/work -w /work openpolicyagent/opa:1.18.2 test /work/policy mounts the repo and runs eval, test, or the server as the container entrypoint.

terminal
docker run --rm -v "${PWD}:/work" -w /work openpolicyagent/opa:1.18.2 test /work/policy -v
output
PASS: 8/8

Flags you will use daily

Past --data and --input, four flags carry most of the load. --format json|pretty|raw picks between machine output and human output, and raw is the one you want in a shell script that compares the result against the string true. --bundle (-b) loads a compiled bundle instead of a directory of loose files. --fail and --fail-defined are what turn a decision into a pipeline gate. Learn these four before you memorize REPL tricks. Production glue reaches for them far more often than anyone opens the REPL.

Environment variables named OPA_EXPERIMENTAL_* gate preview features now and then, so read the release notes when you upgrade rather than after something starts behaving oddly. Air-gapped installs are unusually painless for once: copy the static binary across, verify its SHA256 (a short fingerprint of the file's contents) against the release page, and it runs with no outbound network at all.

terminal
sha256sum opa
./opa eval --format raw 'true'
output
abc123...
true

Package managers run behind pinned releases, sometimes by weeks. Check what brew or apt actually installed against the version your CI pins, especially before a workshop. Otherwise you get the maddening class of bug where the instructor's laptop accepts a policy that every student's pipeline rejects, and the cause is version skew rather than anything written in the Rego. Publish the version everyone should have in a .tool-versions file or a Makefile OPAVERSION variable, and have people source it instead of guessing.

Shell completion scripts exist for bash, zsh, and fish. Optional, but a kindness to yourself during a long eval session. One thing the REPL will not do is remember. History does not survive across sessions, so anything you worked out and want to keep belongs in a scratch.rego file rather than in your scrollback.

Verify download integrity

Teams that take supply chain seriously check the checksum, and the cosign signature (a cryptographic signature on the release file) where one is published, before they trust an OPA release artifact. The reason is blunt. A tampered binary sitting in the policy path owns every decision your platform makes, and it can say yes to anything while the logs look entirely normal. Give OPA binary updates the same urgency you give a kubectl or OpenSSL patch.

terminal
curl -sL https://openpolicyagent.org/downloads/v1.18.2/opa_linux_amd64_static.sha256
output
abc... opa_linux_amd64_static

Bake the OPA version into your CI image tag, something like ci-opa:1.18.2, so application repos inherit a known engine without downloading a binary on every run. Watch the architecture too. A cluster with a mix of linux/arm64 and amd64 nodes needs the matching artifact, and kubectl debug node tells you which one you are actually standing on before you copy the wrong release file onto it.

When you bring a team onto OPA, standardize on one interface per job: opa eval for scripts, the REPL for learning, opa test for regression. Server mode arrives later, in op-bundles. Jumping to HTTP too early hides the document model, and the document model is exactly the thing you need in your head to write query paths that resolve.

A native Windows binary exists and works fine. Most policy repos still assume Linux in CI, though, so WSL2 Ubuntu matches the ubuntu-latest runner on GitHub Actions and spares you the line-ending surprises that heredoc policy fixtures (test files written inline inside a shell script) produce on Windows.

Write your working eval invocations down as Makefile targets, make policy-test and make policy-eval INPUT=req.json, so onboarding is copy-paste from the repo root instead of archaeology through someone's shell history. Note the required OPA version in CONTRIBUTING.md next to the Go or Node pins. Policy contributors are not always platform engineers with OPA already sitting on their machine.

Treat an OPA version bump as a change that needs testing, not as a dependency chore. Open a dedicated PR (pull request) for the pin, run the full opa test suite and the Conftest suite against it, and read the release notes before you merge. Evaluator bugfixes show up in those notes, and they can quietly change the outcome of edge-case comparisons against undefined.

The sidecar pattern puts an OPA container in the same pod as the app, sharing a network namespace (one private network stack between them), so the app asks 127.0.0.1:8181 and the request never leaves the machine. Point the liveness probe at OPA's /health. Point the readiness probe at /health?bundles=true when a bundle has to load before any answer means anything. Skip that readiness gate and Kubernetes will cheerfully route traffic to a pod whose OPA never loaded policy, which is fail-open at scale. When cold-start latency matters, an init container (a container that must finish before the app container starts) can wait for the bundle download first.

Put opa in your shell prompt, or use direnv (a small tool that swaps environment settings per directory), when you are working inside a policy repo. Running eval from the wrong directory loads stale policy files without complaining, and you can burn an afternoon debugging a rule you already fixed. pwd && ls policy before an eval becomes a reflex quickly enough.

Write the exit codes into your runbook: opa test exits 1 when a test fails and 2 when compilation fails. That one line lets whoever is on call tell "bad policy" from "bad test" without reading the full log first.

A single VERSION file in the policy repo, read by both CI and your Docker tags, is the cheapest way to stop the split where the Conftest job runs OPA 1.18.2 while the bundle sidecar runs 1.17.x. Two versions of the engine can read the same input JSON and disagree about it, and nothing in either log will announce that this is what happened.

Try this

Four checks, in order. The binary answers. A literal evaluates. Input JSON reaches the query. Server mode comes up healthy. If any of them fails, fix your PATH and your download before you write a line of Rego, because broken tooling does a very convincing impression of broken policy.

terminal
opa version
opa eval --format pretty '1 + 2'
opa eval --format pretty 'input.user' -i <(echo '{"user":"alice"}')
opa run --server &
sleep 1
curl -s localhost:8181/health
kill %1 2>/dev/null || true
output
Version: 1.x.x
3
"alice"
{}

The loop you will actually use

Your daily loop is small. opa version when something looks off. opa eval for one-shot answers. opa run when you want to poke at Rego by hand. opa run --server when another process needs to ask. Verify the checksum whenever the release page publishes one. And hold the query path in your head as carefully as the rule itself, because data.example.authz.allow and data.authz.allow fail in very different ways. One gives you an answer. The other gives you silence and exit code 0.

Rego syntax comes next: how rules are written and how a query actually resolves against them. Keep these commands within reach while you learn it. When a decision looks mysterious later on, the first move is almost always to pull the policy and the input back into opa eval and ask the same question by hand.

Quick check
01True or false: opa eval exits non-zero when the policy evaluates to false.
Incorrect — No. An evaluation that completes exits 0 whether the answer is true, false, or undefined.
Correct — Exit 0 only tells you OPA ran; you still have to assert on the decision.
Incorrect — A syntax error does fail the run, but that is not what the statement says. A false result still exits 0 unless you pass --fail.
Incorrect — It does not. A successful evaluation exits 0 unless you add a --fail flag.
02A policy's rules are queried under which document root?
Correct — package example.authz with an allow rule becomes data.example.authz.allow.
Incorrect — input holds the facts you hand to a single query. Policy output does not live there.
Incorrect — There is no policy. prefix. Compiled rules hang off data.
Incorrect — Rego is the name of the language, not a document path.
03Your CI job runs opa eval -d policy.rego --format pretty 'data.authz.allow'. It prints nothing and exits 0, and the build goes green. What do you do next?
Correct — Empty output plus exit 0 is the signature of undefined, which usually means the query path does not match the package. Fixing the path gets you an answer, and --fail stops silence from passing as success.
Incorrect — Formatting is not the problem. Nothing printed because the path resolved to undefined, and no output format will conjure a value that was never produced.
Incorrect — Exit 0 means evaluation ran fine. Undefined is an ordinary result, not a crash, so the binary is doing exactly what it should.
Incorrect — input is the request document you pass in. Rules a policy defines always hang off data, so this would return undefined too.

Takeaway

The trap worth remembering here: eval's exit code answers a different question than you asked. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related