RSS

What is policy as code? A complete guide for infrastructure teams

Policy As Code Infrastructure As Code CI/CD Policy

What you'll learn: What policy as code is, how it differs from infrastructure as code, and where it fits in the software development lifecycle when you want automated policy enforcement instead of manual processes. You’ll also get practical policy as code examples in Rego, plus a realistic path to implement policy as code (PaC) in CI/CD and GitOps workflows.

Infrastructure teams usually learn about governance the hard way, which is to say after an incident, after an audit, or after someone notices that the “temporary” public bucket has been public for eight months. The root problem is not that people are careless, it’s that the system is a patchwork of GUIs, tribal knowledge, and one-off exceptions, and in complex systems, human error is not an edge case, it’s the default failure mode.

Policy as code is what happens when you stop treating rules as a PDF and start treating them as software code, which means they live in version control, they get reviewed like application code, they can be tested, and they can be enforced consistently on every infrastructure change. It’s how you go from thinking about enforcing security policies to policy enforcement that actually runs in the same place your work already happens, which is the pull request.

What is policy as code?

Policy as code is the practice of writing organizational rules around things like security, compliance, and cost as machine-readable code that can be version-controlled, tested, and automatically enforced. Instead of relying on manual checklists or click-through approvals in a console, policies as code express defined policies in a high-level declarative language, then evaluate them during deployment, in CI/CD, or at runtime in places like Kubernetes admission control.

The important parts are boring in the best way, because boring is how you get reliability. Policies live in Git alongside infrastructure provisioning code, which makes policy as code management a normal part of software development.

Those policies are testable, so you can run compliance checks against sample plans and prove the rules behave before you turn on enforcement. They are automated, meaning the checks run without a human remembering to do them, and they are consistent, meaning the same access control rules and security standards apply regardless of who opens the pull request or which team happens to be on call.

Policy as code vs. infrastructure as code

Infrastructure as code (IaC) defines what to build, while policy as code (PaC) defines what’s allowed. IaC provisions cloud resources, networks, and data management primitives, while code as policy evaluates those proposed resources against regulatory and security standards, compliance policies, and cost control requirements, then produces a pass or fail decision that can block non-compliant code before it reaches production environments.

Infrastructure as code Policy as code
Purpose Define and provision resources Validate and enforce rules
Output Created infrastructure Pass or fail decisions
Common languages HCL, YAML, JSON Rego, Sentinel, Cedar
When it runs During apply Before or during apply

Once you internalize this split, a lot of arguments get easier. IaC is about what you’re changing; policy-as-code is about knowing if you’re allowed to change it that way. In a mature workflow, both answers show up in the same pull request, because that is where the work is reviewed and where it should be governed.

When infrastructure teams adopt policy as code

Built-in guardrails make deployments faster, not slower

Manual enforcement feels safe because it looks like control, but it’s really just a queue. When automated policy enforcement runs in CI/CD, developers get immediate feedback on policy violations while they still remember what they changed, and security teams stop being the human API that every team has to call before they can ship. That feedback loop matters because infrastructure management is a compounding system, and short loops beat long loops every time.

Audit trails show up naturally when policies live in version control

When policies as code are version-controlled, you get an audit-ready history almost as a side effect. Every change is a commit, every change is reviewed, and the enforcement point is tied to a pull request, which gives compliance audits something they can actually reason about, not a spreadsheet of screenshots. This is compliance as code in practice: not because it is trendy, but because it produces evidence.

Catching misconfigurations before apply reduces security risk at scale

Security policy as code is the difference between meaning to enforce encryption and actually making sure encryption is enforced.

Policies can deny public S3 buckets, block open ingress, require encryption at rest, and enforce access control rules that protect sensitive data, and they do it before the change is applied, which is the only time it is cheap to fix. Security testing that happens after deployment is still useful, but it’s always more expensive, and it tends to get negotiated away when the incident clock is ticking.

Cost controls belong in the same loop as changes

Cost control works best when it is not a monthly surprise. Policy-based governance can require tags for chargeback, block oversized instances, restrict regions, and enforce budgeting rules directly in the development lifecycle, so finance visibility is part of every change. If cost governance is a separate system, it becomes a separate argument, and arguments are where cost overruns go to hide.

Policy as code common use cases

Security policy enforcement is the obvious starting point

This is security policy as code in practice, and it usually begins with the greatest hits of cloud security vulnerabilities. You write policies that deny public buckets, require encryption, and enforce network rules that prevent “open to the world” from ever being merged, which is less dramatic than responding to an incident, but dramatically better.

Compliance as code turns frameworks into continuous checks

SOC 2, HIPAA, and PCI-DSS are often treated like episodic events, but the controls they represent are ongoing. With compliance as code, policies map to specific regulatory requirements, run on every change, and produce a record of continuous compliance that is easier to maintain than a scramble every audit season.

Cost and resource governance prevents slow-motion failures

Policies can enforce tags, block expensive instance classes, and limit deployments to approved regions, which is cost control that happens at decision time. The trick is not to make the rules punitive, it is to make them predictable, so teams can plan for them instead of fighting them.

Environment-specific routing keeps dev fast and prod strict

Teams rarely want identical policy enforcement in every environment, because development needs experimentation and production needs stability. A GitOps workflow makes this natural because the target environment is already a property of the change, and tools like Terrateam can route the right code as policies based on directories, workspaces, or other environment signals, while also layering in directory-level RBAC and approval requirements so access control is not an afterthought.

Policy as code tools and languages

Open Policy Agent and Rego are the portable defaults

Open Policy Agent (OPA) is a general-purpose policy engine, and Rego is the policy language that makes it useful. The adoption curve is driven by the fact that OPA is engine-agnostic, so it shows up in infrastructure tooling, Kubernetes admission control, API gateways, and anywhere else you want a consistent policy engine without coupling everything to one vendor.

HashiCorp Sentinel fits Terraform Cloud because it is designed to

Sentinel is HashiCorp’s proprietary policy as a code framework, and it is tightly integrated with Terraform Cloud and Enterprise. The syntax is approachable, the integration story is strong if you already live inside the HashiCorp platform, and the trade-off is that portability is not the point, which is fine when the platform is the strategy.

AWS Cedar is about authorization, not just infrastructure

Cedar is an AWS-focused policy language for fine-grained authorization decisions, which puts it closer to application access control than classic infrastructure checks. If your problem is knowing who can do what, Cedar is in its element, and if your problem is understanding which resources should exist, you might still reach for a plan-evaluating policy engine.

Static analysis tools like Checkov catch issues before you even have a plan

Tools like Checkov scan Terraform, CloudFormation, and Kubernetes manifests against built-in benchmarks, which makes them a good fit for early compliance checks and security posture improvements. They are not a full replacement for a policy engine, but they are useful as an early gate, especially when you want fast feedback on obvious misconfigurations.

How to implement policy as code

Identify the policies worth codifying

Start with what you already do manually, because those manual processes are telling you where the risk is. The highest impact rules tend to be the ones that are frequently violated, hard to review consistently, or tied directly to regulatory requirements, security standards, or recurring cost overruns.

Choose a policy engine that matches your toolchain

OPA is a strong general choice, Sentinel is ideal when Terraform Cloud is the center of gravity, and cloud-native approaches work when you are intentionally single-provider. The practical question is not which tool is best, it’s which tool can enforce policies where we already work, because policy as code that is not in the workflow is just another document.

Write policies like software, and test them like you mean it

Policies should start small, then grow, and they should have tests that prove behavior on representative inputs. Many teams begin in warn mode, which surfaces policy violations without blocking deployments, then shift to enforce once false positives are under control, because credibility matters and a policy engine that cries wolf becomes a policy engine that gets bypassed.

Integrate checks into the pull request, not into a separate portal

If policy enforcement shows up as a comment next to the plan output, it becomes part of normal review, and if it shows up in a separate system, it becomes someone else’s problem. Terrateam, for example, can run policy validation in the pull request thread alongside plan and apply, which keeps governance attached to the change itself instead of scattered across multiple systems.

Monitor policy violations and evolve the rules

Policy code is living code. Track failures, refine rules, tune exceptions, and add new policies as your infrastructure and regulatory landscape evolves, because continuous compliance is not a one-time migration, it is an operational habit.

GitOps makes policy-as-code feel inevitable

GitOps assumes Git is the control plane, so policy as code fits naturally because policies live next to the infrastructure code, get evaluated on pull requests, and get enforced before changes are applied.

The flow is straightforward once you stop trying to make governance a separate ceremony: a pull request opens, a plan is generated, policies evaluate the proposed change, results are posted back into the pull request, and the change is either allowed to proceed or blocked with a clear reason.

In practice, you end up with layers of enforcement that match the lifecycle of a change.

Apply-time enforcement becomes the final gate when you want to prevent a last-minute override from pushing non-compliant code into production environments.

Policy as code examples that map to real pain

Terraform resource tagging policy in Rego

The simplest useful policy-as-code rule is tagging, because tags are the glue for cost allocation, incident response, ownership, and sometimes access control. The example below inspects a Terraform plan in JSON form, then denies resources that are missing required tags like environment and owner.

package terraform.policies.tagging
required_tags := {"environment", "owner"}
is_mutating(actions) {
  actions[_] == "create"
} else {
  actions[_] == "update"
}
# Only enforce tagging on resources that actually have a tags field in the plan output.
has_tags_field(rc) {
  rc.change.after.tags
}
after_tags(rc) = tags {
  tags := rc.change.after.tags
} else = {} {
  true
}
missing_required_tags(rc) = missing {
  tags := after_tags(rc)
  present := {k | tags[k]}
  missing := required_tags - present
}
deny[msg] {
  rc := input.resource_changes[_]
  rc.mode == "managed"
  is_mutating(rc.change.actions)
  has_tags_field(rc)
  missing := missing_required_tags(rc)
  count(missing) > 0
  missing_sorted := sort([t | t := missing[_]])
  msg := sprintf(
    "Resource %s is missing required tags %v",
    [rc.address, missing_sorted]
  )
}

That is not fancy, but it is effective, and because it is code as policy, you can unit test it, review it, and evolve it as your tagging model grows, which is the whole point.

Cost threshold policy for cloud resources

Cost policies usually start with instance sizing because it’s a direct line to spend. The rule below denies oversized EC2 instance types unless an explicit override is present, which is how you keep a hard guardrail while still allowing exceptions with intent and review.

package terraform.policies.cost
disallowed_instance_types := {
  "t2.large",
  "t3.large",
  "m5.large",
  "m5.xlarge",
  "c5.xlarge"
}
is_mutating(actions) {
  actions[_] == "create"
} else {
  actions[_] == "update"
}
after_tags(rc) = tags {
  tags := rc.change.after.tags
} else = {} {
  true
}
has_cost_override(rc) {
  tags := after_tags(rc)
  lower(tags["cost_override"]) == "true"
}
deny[msg] {
  rc := input.resource_changes[_]
  rc.mode == "managed"
  rc.type == "aws_instance"
  is_mutating(rc.change.actions)
  itype := rc.change.after.instance_type
  disallowed_instance_types[itype]
  not has_cost_override(rc)
  msg := sprintf(
    "Instance %s uses disallowed type %s without cost_override=true",
    [rc.address, itype]
  )
}

This pattern scales beyond instance types. You can enforce required cost allocation tags, restrict resource classes that blow up bills, and even drive approval workflows by requiring special markers that get audited, which is cost control that is visible in every change.

Security group validation policy

If you want a single policy as code example that prevents real incidents, it’s the one that blocks open ingress on sensitive ports, because someone always thinks they will remember to lock it down later. The rule below denies any security group that allows 0.0.0.0/0 inbound on SSH or RDP.

package terraform.policies.security_groups
sensitive_ports := {22, 3389}
is_mutating(actions) {
  actions[_] == "create"
} else {
  actions[_] == "update"
}
is_world_ipv4(cidr) { cidr == "0.0.0.0/0" }
is_world_ipv6(cidr) { cidr == "::/0" }
# True if any sensitive port is inside the port range, or if the rule is "all ports".
exposes_sensitive_port(from, to) {
  from == -1
  to == -1
} else {
  p := sensitive_ports[_]
  from <= p
  to >= p
}
# Inline ingress rules on aws_security_group
deny[msg] {
  rc := input.resource_changes[_]
  rc.mode == "managed"
  rc.type == "aws_security_group"
  is_mutating(rc.change.actions)
  ing := rc.change.after.ingress[_]
  # Match IPv4 anywhere
  is_world_ipv4(ing.cidr_blocks[_])
  exposes_sensitive_port(ing.from_port, ing.to_port)
  msg := sprintf(
    "Security group %s allows world ingress on sensitive ports",
    [rc.address]
  )
}
deny[msg] {
  rc := input.resource_changes[_]
  rc.mode == "managed"
  rc.type == "aws_security_group"
  is_mutating(rc.change.actions)
  ing := rc.change.after.ingress[_]
  # Match IPv6 anywhere
  is_world_ipv6(ing.ipv6_cidr_blocks[_])
  exposes_sensitive_port(ing.from_port, ing.to_port)
  msg := sprintf(
    "Security group %s allows world ingress on sensitive ports",
    [rc.address]
  )
}
# Best-practice rule resources aws_vpc_security_group_ingress_rule
deny[msg] {
  rc := input.resource_changes[_]
  rc.mode == "managed"
  rc.type == "aws_vpc_security_group_ingress_rule"
  is_mutating(rc.change.actions)
  after := rc.change.after
  is_world_ipv4(after.cidr_ipv4)
  exposes_sensitive_port(after.from_port, after.to_port)
  msg := sprintf(
    "Security group ingress rule %s allows world IPv4 ingress on sensitive ports",
    [rc.address]
  )
}
deny[msg] {
  rc := input.resource_changes[_]
  rc.mode == "managed"
  rc.type == "aws_vpc_security_group_ingress_rule"
  is_mutating(rc.change.actions)
  after := rc.change.after
  is_world_ipv6(after.cidr_ipv6)
  exposes_sensitive_port(after.from_port, after.to_port)
  msg := sprintf(
    "Security group ingress rule %s allows world IPv6 ingress on sensitive ports",
    [rc.address]
  )
}

That is security policy as code doing what humans are bad at, which is being consistent under time pressure.

Bring policy as code into your infrastructure workflow

The best way to adopt policy as code is to integrate it into your existing PR-based workflow so governance is part of development rather than a bolt-on approval gate. When policy enforcement runs next to plan output, violations become review comments instead of meeting invites, and your software development lifecycle gets faster because the rules are consistently enforced at the point of change.

Terrateam is built for this shape of work, which means policies as code with Rego support, cost estimation that shows up where decisions get made, approval workflows that map to how teams actually ship, and enforcement that runs alongside plan and apply.

Policy as code FAQs

What is policy as code in AWS?

Policy as code in AWS uses services like AWS Config Rules, Lambda, and CloudFormation Guard to define and enforce governance rules as code across AWS resources.

How does policy as code handle exceptions and overrides?

Most policy engines support soft-fail modes and override mechanisms where designated approvers can bypass specific policy violations when business justification exists.

Can policy as code work with Terraform, OpenTofu, and CDKTF?

Yes—policy engines like OPA evaluate the JSON plan output from any Terraform-compatible tool, making them agnostic to whether you use Terraform, OpenTofu, or CDKTF.

What is the difference between policy as code and compliance as code?

Policy as code is the broader practice of codifying any organizational rule, while compliance as code specifically refers to policies that map to regulatory frameworks like SOC 2, HIPAA, or PCI-DSS.

How do you version control policies as code?

Store policy files in Git repositories alongside your infrastructure code, using branches, pull requests, and code review to manage changes just like application code.