Policy as Code in GCP: OPA, Organisation Policies, and CI/CD Enforcement
Policy as Code means writing your infrastructure governance rules as code: version-controlled files that automated tools evaluate on every change, before anything reaches GCP. Instead of relying on code reviewers to catch a misconfigured bucket or an over-privileged service account, a policy engine does it automatically on every pull request.
This page explains how Policy as Code works in a GCP and Terraform workflow, covers the main enforcement approaches, and helps you decide which tools to use and when.
Simple explanation
Think of Policy as Code as a quality gate for infrastructure changes. When a developer opens a pull request that modifies Terraform, a policy engine reads the planned changes and checks them against a list of rules stored in the repository. If a rule is violated (for example, a Cloud Storage bucket is configured to allow public access, or a VM is being created in a disallowed region), the check fails and the PR cannot merge.
Nothing depends on a human reviewer remembering a rule. The rule is written in a file, the file is version-controlled, and the evaluation happens automatically in CI. That is the core idea: governance encoded as code, enforced like a test.
Imagine a nightclub with a bouncer who has a printed list of rules: no trainers, smart dress only, over-18s only. Every person at the door is checked against that same list, every night, regardless of who is working. Policy as Code is the same idea applied to infrastructure: a consistent, written ruleset that runs on every change without anyone needing to remember the rules.
Why Policy as Code matters
Without policy automation, compliance depends on people catching problems manually. Someone must remember to check whether a new Cloud Storage bucket is public, whether a new VM is in an approved region, or whether a new service account has been granted more permissions than it needs. Under deadline pressure, things get missed.
Policy as Code moves those checks into the delivery pipeline. The benefits compound quickly:
- Faster feedback. Developers see violations on their pull request, not after a failed
terraform applyor a post-deployment security audit. - Fewer production surprises. Rules that apply in PR also apply in every environment. Nothing slips into prod that was not already caught in dev and staging.
- Consistent reviews. The same rules apply to every change, regardless of which reviewer is on duty or how busy the team is.
- Reduced manual overhead. Reviewers can focus on architectural decisions rather than checking whether every resource label is present.
- Better compliance evidence. Policy checks leave a clear audit trail in CI logs. You can show exactly which rules were evaluated, when they ran, and when they passed.
Small teams benefit just as much as large enterprises. A three-person startup that enforces a “no public buckets” rule in CI will avoid a data exposure incident that a team relying on manual checks might not.
How it works
The typical flow in a Terraform-based GCP delivery pipeline looks like this:
- A developer changes a Terraform file and opens a pull request.
- CI runs
terraform planand saves the output as a plan file. - The plan is converted to JSON using
terraform show -json. - A policy engine (usually OPA) reads the plan JSON and evaluates it against policy files stored in the repository.
- If any rule is violated, the CI step fails with a message that describes exactly what is wrong. The pull request cannot merge.
- If all rules pass, the CI step succeeds. The pull request can be reviewed and merged normally.
- After merge,
terraform applyruns against the exact plan that was already checked. No new surprises at apply time.
Checking a plan rather than the raw Terraform code gives you something concrete: an exact description of what GCP resources will be created, updated, or destroyed. You are not guessing what the code might do; you are reading what it will do. That is what makes plan-time enforcement reliable.
GCP Organisation Policies operate separately and in parallel. They enforce at the GCP API level, rejecting non-compliant API calls at apply time regardless of where the request came from. They are a hard backstop, not a replacement for CI-based checks.
This workflow depends on a consistent Terraform foundation. If you are not yet using Infrastructure as Code and a remote Terraform state backend, start there before adding policy checks.
When to use Policy as Code
Policy as Code is useful any time you want to enforce a rule consistently across infrastructure changes. Common real-world examples:
- Restricting resources to approved regions. Useful for data residency requirements or cost control. Write one rule and it applies to every new resource in every environment, without anyone needing to remember.
- Blocking public Cloud Storage buckets. One of the most common causes of cloud data exposure. A short OPA policy catches this at plan time, before any bucket is created.
- Preventing service account key creation. Service account keys are a significant security risk. You can block them in OPA and reinforce the constraint with a GCP Organisation Policy. See why service account keys are dangerous for the full context.
- Requiring specific labels or tags. Useful for cost attribution and environment management. A policy can require that every resource has a
teamandenvironmentlabel before it is created. - Enforcing approved machine types or network settings. Useful for cost control and security baseline compliance across a fleet of VMs.
- Flagging unexpected IAM changes. Combined with managing IAM with Terraform, policy checks can catch unexpected changes to IAM role bindings at plan time, before they are applied.
You do not need a comprehensive governance programme before Policy as Code is worth doing. Pick one high-value rule, get it running on every pull request, and build from there. Even a single “no public buckets” check running automatically is more reliable than a checklist that depends on someone remembering to look.
Open Policy Agent (OPA)
OPA is the most widely used Policy as Code tool in the open-source Terraform ecosystem. You write policies in Rego, a declarative language designed specifically for policy evaluation. OPA reads the JSON output of terraform plan and returns a set of violations, if any. It runs as a step in your Cloud Build pipeline after the plan step and before apply.
Rego takes some getting used to, but the basic pattern is consistent: you define a deny set, iterate over resources in the plan input, and add a message to the set when a resource violates a rule. The more you write, the more readable it becomes.
OPA is like a legal reviewer who reads every contract before it is signed. The reviewer works from a written checklist of rules. If any clause violates a rule, the contract is returned for revision. Discovering a problem after signing is far more expensive to fix than before.
Example: deny resources outside approved regions
This Rego policy denies any Terraform plan that creates a Cloud Storage bucket outside the EU region:
package terraform.gcp
import future.keywords
deny contains msg if {
resource := input.resource_changes[_]
resource.type == "google_storage_bucket"
resource.change.actions[_] == "create"
not startswith(resource.change.after.location, "EU")
msg := sprintf(
"Bucket '%s' uses location '%s'. Only EU locations are permitted.",
[resource.address, resource.change.after.location]
)
}The deny message tells the developer exactly what is wrong and what the correct configuration looks like. A vague error wastes time; a specific message gets the fix done immediately.
Run this policy in Cloud Build after the plan step:
steps:
- name: 'hashicorp/terraform:1.6'
args: ['plan', '-out=tfplan']
id: terraform-plan
- name: 'hashicorp/terraform:1.6'
args: ['show', '-json', 'tfplan']
id: terraform-show
- name: 'openpolicyagent/opa'
entrypoint: bash
args:
- -c
- |
terraform show -json tfplan > plan.json
opa eval --data policy/ --input plan.json \
--format pretty 'data.terraform.gcp.deny' \
| tee /dev/stderr | grep -q '"set()"'
waitFor: ['terraform-plan']
id: policy-checkThe step passes when the deny set is empty (“set()”) and fails otherwise. Any violations are printed to the build log for the developer to read.
GCP Organisation Policies
GCP Organisation Policies are a separate enforcement mechanism that operates at the GCP API level rather than inside CI. When an Organisation Policy is set, GCP enforces it by rejecting any API call that violates the constraint, regardless of whether the request came from Terraform, gcloud, the Console, or any other tool.
Organisation Policies apply at organisation, folder, or project level. Child resources inherit policies from their parents, which makes them an effective way to enforce guardrails across an entire organisation with a single configuration. For a detailed walkthrough of how to set these up, see GCP Organisation Policies.
Common constraints include:
constraints/gcp.resourceLocations: restrict which regions resources can be created in. See restricting resource locations for a step-by-step guide.constraints/iam.disableServiceAccountKeyCreation: prevent creation of service account keys organisation-wide. One of the most impactful security constraints available.constraints/compute.requireShieldedVm: require Shielded VM features on new instancesconstraints/storage.uniformBucketLevelAccess: enforce uniform bucket-level access on all buckets
# Apply a location restriction to a project
gcloud resource-manager org-policies set-policy \
--project=my-app-prod \
policy.yamlOrganisation Policies are powerful, but they only fire at apply time. A developer writing Terraform will not know they have violated a constraint until terraform apply fails, after all their planning, review, and approval work is complete. OPA in CI surfaces the same violation much earlier, at plan time.
Organisation Policies and OPA are complementary. Organisation Policies are the hard backstop, rejecting non-compliant API calls even if CI is bypassed. OPA is the fast feedback mechanism, catching violations in the pull request pipeline before any apply attempt. Use both.
Terraform Sentinel
Sentinel is HashiCorp’s policy framework, built directly into Terraform Cloud and Terraform Enterprise. It evaluates policies after terraform plan and before terraform apply, using a language called Sentinel rather than Rego. Policies can be set to advisory (warn only), soft mandatory (can be overridden with a justification), or hard mandatory (cannot be bypassed under any circumstances).
If your team uses Terraform Cloud or Terraform Enterprise, Sentinel is the natural choice. For everyone else running open-source Terraform, OPA with Rego is the right tool.
Sentinel is not available with open-source Terraform. If you manage Terraform yourself (including with Cloud Build or GitHub Actions), Sentinel is not an option. Use OPA instead. The two tools solve the same problem, but Sentinel is tightly integrated into the Terraform Cloud workflow while OPA works with any CI system.
Terraform Validator and the Google Policy Library
Google provides Terraform Validator as an open-source tool that validates Terraform plan output against a library of pre-built GCP-specific policies. Rather than writing custom Rego from scratch for common security checks, you can use constraints that Google’s security team already maintains.
# Validate a Terraform plan against GCP policy bundles
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
terraform-validator validate plan.json \
--policy-path=./policy-library \
--project=my-app-prodThe policy library includes pre-built constraints for common GCP security best practices: blocking public bucket access, requiring VPC flow logs, enforcing IAM conditions, and more. It is a reasonable starting point for teams that want coverage quickly without writing Rego from scratch.
Start with Google’s pre-built policy library for common security checks. Write custom Rego policies only for rules that are specific to your organisation and not already covered. There is no value in reimplementing well-established security constraints from scratch.
OPA vs GCP Organisation Policies
These two tools are often confused because they both enforce infrastructure rules. They are quite different in practice.
| Aspect | OPA (in CI) | GCP Organisation Policies |
|---|---|---|
| Where it runs | Inside CI (Cloud Build, GitHub Actions, etc.) | At the GCP API level |
| When feedback arrives | At plan time, on the pull request | At apply time, after review work is complete |
| What it evaluates | Terraform plan JSON | GCP API calls |
| Can be bypassed | Yes, if CI is skipped | No, enforced by GCP itself |
| Developer experience | Immediate, descriptive message in the PR | Error in apply logs, less context |
| Best use case | Fast developer feedback, custom organisation rules | Hard enforcement, compliance backstop |
Relying only on Organisation Policies means developers discover violations at apply time, after all their planning and review work is already done. Relying only on OPA means a developer who bypasses CI entirely faces no enforcement. You need both: OPA for fast feedback in the PR workflow, Organisation Policies as an unconditional safety net that cannot be circumvented.
OPA vs Terraform Sentinel
Both OPA and Sentinel evaluate a Terraform plan and block non-compliant changes before they are applied. The choice between them comes down to how you run Terraform.
- Open-source Terraform (including Cloud Build and GitHub Actions): use OPA. It is portable, well-supported, and works with any CI system. Policies are written in Rego.
- Terraform Cloud or Terraform Enterprise: Sentinel is built in and integrates natively. Policies are written in the Sentinel language, and you get enforcement levels (advisory, soft mandatory, hard mandatory) that OPA does not provide out of the box.
If your team pays for Terraform Cloud or Terraform Enterprise, use Sentinel. If you run open-source Terraform and manage your own CI (including with Cloud Build), use OPA. Rego has a steeper initial learning curve than Sentinel’s syntax, but it is more widely used outside HashiCorp’s platform and integrates with non-Terraform tools as well.
Where policy checks fit in the pipeline
Policy checks must run after terraform plan and before terraform apply. This placement is deliberate: the plan gives you a concrete description of what will change, and no resources have been created yet, so a failure is cheap to fix.
In a pull request workflow:
terraform planruns when the pull request is opened or updated- OPA evaluates the plan output
- The PR cannot merge if the policy check fails
- After merge,
terraform applyruns using the saved-out=tfplanplan file, which is the exact same plan that was checked and approved
If the apply step generates a fresh plan at apply time rather than using the saved -out=tfplan file, it may differ from what was reviewed and checked. A new plan can include changes that were not in the original. Using a saved plan file guarantees that what was reviewed is exactly what gets applied. See Terraform state management for how remote state and locking support this workflow reliably.
For a broader look at hardening the pipeline around these checks, see secure CI/CD pipelines and managing environments in CI/CD.
Common mistakes
Relying only on GCP Organisation Policies for compliance. Organisation Policies reject non-compliant API calls, but only at apply time. The developer finds out the plan is non-compliant after all the review and approval work is already done. OPA in CI catches the same problem at plan time, on the pull request. Use both layers.
Writing Rego policies that are too broad or complex. A policy that enforces five unrelated rules in a single file is hard to read, hard to test, and produces confusing messages. Keep each policy file focused on one rule. One file, one concern.
Producing vague deny messages. A message like “Policy violation detected” tells the developer nothing useful. A message like “Bucket ‘my-app-logs’ uses location ‘US’. Only EU locations are permitted.” tells them exactly what is wrong and how to fix it. Write deny messages that a junior developer can act on immediately.
Only running policy checks in the production pipeline. Run checks on every pull request in every environment. A violation that slips into dev and staging will eventually reach prod. Catch it in the PR, before any merge.
Not version-controlling policy files. Policy files are code. They belong in the same repository as the infrastructure they validate, go through the same review process, and are tracked in git history. Policy changes applied directly without a pull request are invisible and unreviewed.
Treating policy checks as a last-minute compliance task. Policy as Code works best when rules are defined alongside the infrastructure requirements they enforce. Adding policies after the fact means retrofitting rules against infrastructure that was never designed with them in mind.
Best practices
- Start with a small number of high-value rules. Three well-written, well-tested policies that run on every PR are more valuable than twenty policies that are poorly maintained or frequently bypassed.
- Write clear, actionable deny messages. Every deny message should tell the developer what the correct configuration looks like, not just that something is wrong.
- Keep policy files small and testable. OPA supports unit tests for Rego policies. Use them. A tested policy is a reliable policy.
- Combine CI checks with hard enforcement. Use OPA for fast developer feedback and Organisation Policies as the unconditional backstop. The two layers reinforce each other.
- Review policy changes like application code. A policy pull request deserves the same attention as an infrastructure change. An overly permissive policy change is a security regression, even if CI passes.
- Apply least-privilege principles at both the IAM and policy layer. Policy checks that flag unexpected IAM changes complement careful IAM design with Terraform-managed IAM.
Think of policy files like the fire safety regulations for a building. They are written down, reviewed, updated over time, and inspected regularly. Nobody would accept a verbal fire safety check that changes depending on which inspector shows up. Your infrastructure policies deserve the same rigour: written, version-controlled, tested, and reviewed like any other safety-critical system.
Summary
- Policy as Code encodes infrastructure governance rules as version-controlled files that CI evaluates automatically on every change
- OPA with Rego is the standard choice for open-source Terraform workflows: it validates the plan in CI, before any apply
- Terraform Sentinel is HashiCorp’s equivalent for Terraform Cloud and Terraform Enterprise only
- GCP Organisation Policies enforce at the GCP API level: a hard backstop that applies regardless of how changes are made
- OPA and Organisation Policies complement each other: OPA gives fast developer feedback, Organisation Policies provide unconditional enforcement
- Policy checks belong after
terraform planand beforeterraform apply, on every pull request, in every environment - Start with Google’s pre-built policy library for common checks; add custom Rego rules only for organisation-specific requirements
Frequently asked questions
What is Policy as Code in GCP?
Policy as Code means encoding your infrastructure governance rules as version-controlled files that automated tools evaluate on every change. Instead of relying on a reviewer to catch a public Cloud Storage bucket or an over-privileged service account, a policy engine checks every Terraform change automatically before anything is deployed.
What is the difference between OPA and GCP Organisation Policies?
OPA runs inside your CI pipeline and evaluates the Terraform plan before terraform apply, giving developers fast feedback at plan time. GCP Organisation Policies enforce constraints at the GCP API level, rejecting non-compliant API calls regardless of origin. OPA catches things early; Organisation Policies are the hard backstop. Use both.
Do I need OPA if I already use GCP Organisation Policies?
Yes. Organisation Policies only reject violations at apply time, after all your review and approval work is done. OPA validates at plan time inside CI, so developers see the problem immediately on their pull request. Catching violations earlier saves time and avoids wasted effort on reviews that will ultimately fail at apply.
Where should Policy as Code run in a CI/CD pipeline?
Policy checks should run after terraform plan and before terraform apply. You need a concrete plan to evaluate, but no resources should have been created yet. In a pull request workflow, the policy check runs on PR open and update, and the PR cannot merge if the check fails.
Is Terraform Sentinel only for Terraform Cloud?
Yes. Sentinel is built into Terraform Cloud and Terraform Enterprise. It is not available with open-source Terraform. If you manage Terraform yourself, including with Cloud Build, use OPA with Rego instead. The concepts are similar but the integration and language differ.