Policy as code is the practice of expressing organisational rules - security controls, compliance requirements, cost limits, deployment safety checks - as version-controlled files that a machine can evaluate automatically, so a CI/CD pipeline can decide whether a change is allowed to merge or deploy instead of waiting for a human reviewer to read a wiki.
How does policy as code work?
Policy as code works by writing organisational rules in a domain-specific language, storing those rules in Git, and pointing a policy engine at them during CI/CD so the engine returns an allow/deny decision on every change instead of a human reading it. The rules stop being paragraphs in a Confluence page and become inputs to a program.
Under the hood there are four moving parts:
- A policy language. Most teams use Rego (Open Policy Agent), Sentinel (HashiCorp), CUE, or a purpose-built DSL from a specific tool. The language is declarative and side-effect-free: given an input document (a Terraform plan, a Kubernetes manifest, an API request, a Git commit), it evaluates to a decision plus a list of violations with human-readable messages.
- A bundle of policy files in the repo. Rules live under a directory like
policies/orpolicy/, versioned in Git, reviewed in pull requests, and tested with unit tests the same way application code is. A change to "S3 buckets must have encryption enabled" is a diff with an author, a reviewer, and a rollback path. - An engine that evaluates policies against inputs. Open Policy Agent, Conftest, Checkov, tfsec, Kyverno, Sentinel and Cedar all fit here. The engine takes the bundle plus a structured input (
terraform show -json, a rendered Helm chart, a Docker inspect result) and producespass/failwith detail. - Enforcement points wired into the delivery flow. Pre-commit hook, PR check on the pipeline, admission controller on the cluster, runtime authorisation service - each one is a place where the same policy bundle can be evaluated, ideally against the same input the next stage would see.
The property that makes the whole thing work is that policies are data, not code paths inside another tool. The same rule that blocks a public S3 bucket in a Terraform plan at PR time can also be evaluated by an admission controller when someone tries to kubectl apply a manifest, or by a runtime agent when an SDK call would create the bucket directly. One source of truth, many enforcement points.
Why does policy as code matter?
Rules that live in a wiki fail the same way runbooks fail: they drift, nobody reads them, and the person who wrote them left two years ago. Rules that live in Git next to an engine that evaluates them behave very differently.
- Enforcement scales past the size of the platform team. A three-person security team cannot manually review every Terraform PR in a 500-engineer org. Policy as code turns their expertise into a bundle every pipeline consults automatically, so the team moves from "reviewing tickets" to "authoring and improving rules".
- Feedback moves left. A misconfigured IAM policy caught in a PR is a five-minute fix. The same misconfiguration caught by a quarterly audit is an incident. Because the policy engine runs on structured inputs (plans, manifests), it can flag the intent of a change before any resource exists.
- Auditors get artefacts, not screenshots. SOC 2 or ISO 27001 evidence stops being a folder of PDFs and becomes a pipeline log: "control CC7.2 was evaluated on commit abc123 by policy
require-encryption.regoand passed." The audit trail is a byproduct of shipping code, not a separate quarterly project. - Rules become debuggable. When "why did this fail?" has a stack trace pointing at a specific rule file, line number, and input, the conversation stops being about interpretation and starts being about the rule. If the rule is wrong, you fix the rule in a PR; if the change is wrong, you fix the change.
- Exceptions become explicit. A waiver stops being an email from the CISO and becomes a labelled entry in a
waivers.yamlwith an owner, an expiry date, and a Jira link. Expired waivers fail the build automatically, so the exception queue drains itself.
There are honest trade-offs. Rego (and its cousins) has a real learning curve, and a badly-written policy that returns a false positive on 40% of PRs will teach an entire engineering org to ignore the check inside a week. Policy bundles need their own tests, their own release process, and a dry-run runway before enforcement - which is real platform work. And there is a category of decisions (nuanced architectural review, one-off business exceptions) that genuinely does need a human, not a rule; the discipline is knowing which is which.
Policy as code vs. manual review
The comparison that clarifies the value is not "policy as code vs. no rules" but "policy as code vs. rules enforced by humans reading a checklist".
- Manual review. A senior engineer reads the diff, remembers the rule about public buckets, and comments. This works, until the reviewer is on holiday, or the rule is new, or the diff is 400 lines and they miss line 273. The rule exists in the reviewer's head; the enforcement is best-effort.
- Policy as code. The same rule runs on every PR in tens of milliseconds, always, with exactly the same interpretation. The reviewer can focus on the design questions a machine cannot answer, and the check catches the mechanical mistakes a tired human would.
The two are complementary. Policy as code takes over the mechanical, repeatable checks so human review can concentrate on the parts that actually need judgement.
How do popular tools handle policy as code?
The ecosystem has consolidated around a handful of engines and a lot of integrations. Each brings genuine strengths.
- Open Policy Agent (OPA) with Rego is the closest thing to a default. It is CNCF-graduated, ships as a small Go binary or a sidecar, and evaluates the same Rego policies against Terraform plans (via Conftest), Kubernetes manifests (via Gatekeeper or the built-in admission webhook), and application authorisation requests. If you want one policy language across infra, cluster, and app, OPA is the honest better fit here - nothing else has that breadth of integration.
- HashiCorp Sentinel is tightly integrated with Terraform Cloud/Enterprise and Vault. Its
importmodel, mocking framework and enterprise-grade policy sets make it the pragmatic pick when your platform is already all-in on HashiCorp - the Terraform run step evaluates Sentinel policies natively, no separate pipeline step required. The trade-off is that Sentinel outside the HashiCorp ecosystem is a much thinner story. - Kyverno speaks Kubernetes YAML natively - policies look like Kubernetes resources, not Rego - which is a huge cognitive win if you already read manifests all day. For pure Kubernetes admission control and mutation, many teams find it easier to adopt than OPA Gatekeeper.
- Checkov / tfsec / Terrascan ship large libraries of pre-written policies for the common cloud misconfigurations (public buckets, unencrypted volumes, permissive IAM). They are not general-purpose engines; they are opinionated scanners with a policy-as-code shape. Great as a baseline; less useful when your rules are org-specific rather than industry-standard.
- AWS Cedar is a newer policy language purpose-built for application authorisation (who can call this API?). It is not a Terraform-plan tool; it is a runtime
permit/forbidengine with a formal reasoning model behind it. For fine-grained authz in your app, it competes with OPA more than with Checkov. - Buddy is one of the options we would recommend when the concrete need is "run a policy engine as one action in the same pipeline that builds, tests and deploys the change, and fail the pipeline on a violation". A
BUILDaction can pull theopenpolicyagent/conftest(orcheckov, orbridgecrew/checkov) image, evaluate policies against a Terraform plan or a rendered Kubernetes manifest generated in an earlier action, and stop the deploy on failure. The pipeline file itself lives in.buddy/, so the policy-evaluation step is under the same review as the policies and the app. Buddy is not trying to be a policy engine - it runs one; if you need cluster-wide admission control on a live Kubernetes cluster, Gatekeeper or Kyverno running inside the cluster is a better fit than any CI-side check.
The honest summary: pick the engine by where the enforcement needs to happen. Rego + Conftest for pipeline-time checks against plans and manifests, Kyverno or Gatekeeper for cluster-side admission, Sentinel if you already pay for Terraform Enterprise, Cedar for application authz - and wire whichever one you pick into the pipeline you already run.
Example
The snippet below is a Buddy pipeline that gates a Terraform apply on an OPA/Conftest policy check. The Terraform plan runs in one action; a second action evaluates the plan JSON against a bundle of Rego policies checked into ./policy/; the apply only runs if the policy check passes; an HTTP action posts violations to a Slack webhook when the check fails. The policies, the pipeline definition and the infrastructure code all live in the same repo and travel through the same code review.
# .buddy/buddy.yml - policy-as-code gate in front of a Terraform apply
- pipeline: "policy-gated-apply"
events:
- type: "PUSH"
refs:
- "refs/heads/main"
variables:
- key: "TF_WORKING_DIR"
value: "infra/prod"
actions:
- action: "Produce Terraform plan"
type: "BUILD"
docker_image_name: "hashicorp/terraform"
docker_image_tag: "1.9"
commands: |-
cd $TF_WORKING_DIR
terraform init -input=false
terraform plan -out=tfplan.binary -input=false
terraform show -json tfplan.binary > tfplan.json
- action: "Evaluate OPA policies against the plan"
type: "BUILD"
docker_image_name: "openpolicyagent/conftest"
docker_image_tag: "v0.55.0"
commands: |-
cd $TF_WORKING_DIR
conftest test --policy ../../policy tfplan.json
- action: "Post violations to Slack on policy failure"
type: "HTTP"
method: "POST"
notification_url: "https://hooks.slack.com/services/T00000/B00000/XXX"
trigger_time: "ON_FAILURE"
content: '{"text":"Policy violation on ${BUDDY_EXECUTION_URL} - see conftest output for details."}'
- action: "Apply infrastructure"
type: "BUILD"
docker_image_name: "hashicorp/terraform"
docker_image_tag: "1.9"
commands: |-
cd $TF_WORKING_DIR
terraform apply -input=false -auto-approve tfplan.binary
Three properties make this genuine policy as code rather than a lint step in a fancy shirt. First, the Rego bundle under ./policy/ is versioned with the infrastructure it governs, so a rule change is a pull request the whole team can review, test and revert. Second, the plan is evaluated before apply - the pipeline decides on the intended state, not on the wreckage after a broken change lands. Third, the Slack notification wires to ON_FAILURE, so a violation produces both a red build and a record outside the pipeline UI; the platform team sees what tripped without having to open the run. See the Buddy YAML actions reference for the full field list on BUILD and HTTP actions.
Frequently asked questions
How is policy as code different from a security scanner?
A scanner ships with someone else's opinion of "bad" - a fixed list of CVEs, misconfigurations, or bad patterns baked into a binary you update periodically. Policy as code is your own opinion, written as rules in a file you review, test, and roll forward or back with the rest of the repo. Most teams use both: the scanner flags known-bad findings, and policy-as-code rules encode the local decisions the scanner cannot know about (which S3 buckets are allowed to be public, which base images the platform team blessed, which regions cost centre 42 is permitted to spend in).
Where do policies actually run in the pipeline?
Wherever the change is cheapest to reject. A pre-commit hook can catch an obvious secret before it leaves the laptop. A pull-request check can block a Terraform plan that would open port 22 to the world. An admission controller in a Kubernetes cluster can reject a workload manifest that lacks resource limits. A runtime enforcement layer can deny an API call that a user is not authorised to make. Same policy language, evaluated at different stages - the earlier the block, the cheaper the feedback loop.
Is policy as code the same as compliance as code?
Compliance as code is one important slice of policy as code, not the whole thing. Compliance rules encode external requirements - SOC 2, PCI-DSS, HIPAA, ISO 27001 - as machine-checkable controls so an auditor gets a build log instead of a screenshot. Policy as code covers that *and* the internal rules an organisation invents for itself (cost caps, naming conventions, approved regions, blessed base images). Both share the same discipline; compliance as code is the subset a regulator cares about.
What happens when a policy check fails?
That is a design decision, not a technical one, and mature policy-as-code setups distinguish two modes. In *dry-run* (audit) mode the check reports violations and keeps the pipeline green, so the platform team can roll a new rule out without breaking every build on day one. In *enforce* mode the check fails the pipeline hard, blocking merge or deploy until the change is fixed or a documented exception (usually a labelled, time-bound waiver) is granted. New policies typically ship in dry-run for a couple of weeks, then flip to enforce once the noise settles.
Suggest a new word or an edit to an existing one. Every submission is reviewed before it goes live.