Why this matters
Policy as Code (PaC) lets you express security, compliance, and platform standards in code and enforce them automatically on Infrastructure as Code (IaC) changes. As a Platform Engineer you will:
- Block risky resources (e.g., public storage buckets, open security groups) before they reach cloud.
- Enforce consistency (tags, regions, naming, encryption) across teams and environments.
- Shift-left compliance by running policies in local dev, pull requests, and CI/CD.
- Provide auditable, reviewable policy changes using version control.
Concept explained simply
Policy as Code means you write rules (in a policy language) that inspect IaC plans or manifests and return violations. Pipelines treat violations like test failures and stop the deployment.
Mental model
Think of PaC as a spellchecker for infrastructure. It reads your IaC file, compares it with company rules, and highlights what breaks the rules. No highlights = good to go.
- Inputs: IaC manifests or plans (Terraform plan JSON, Kubernetes manifests, cloud templates).
- Policy runtime: A policy engine (commonly OPA/Rego) evaluates rules against the input.
- Outputs: Messages describing violations. If none, the change passes.
Key terms in practice
- Policy: A rule, e.g., “All buckets must be private.”
- Decision: Allow/deny or a list of violation messages.
- Enforcement point: Where you run policies (pre-commit, PR checks, CI/CD, admission controller).
- Source of truth: Policies stored in version control with code review.
Worked examples
Example 1: Deny public storage buckets
Goal: Reject any S3-like bucket configured with a public ACL.
Simple Rego sketch:
package policies.buckets
deny[msg] {
input.kind == "aws_s3_bucket"
input.acl == "public-read" # or public-read-write
msg := sprintf("Bucket %s must not be public", [input.name])
}
Behavior: If a bucket is public, a message appears and the pipeline fails.
Example 2: Require mandatory tags
Goal: Every resource must have owner, cost_center tags.
package policies.tags
required := {"owner", "cost_center"}
missing[tag] {
tag := required[_]
not input.tags[tag]
}
deny[msg] {
count(missing) > 0
msg := sprintf("Missing required tags: %v", [missing])
}
Behavior: If any tag is missing, it prints which ones.
Example 3: Allowlist regions
Goal: Only specific regions can be used.
package policies.region
allowed := {"us-east-1", "eu-west-1"}
deny[msg] {
input.region
not allowed[input.region]
msg := sprintf("Region %s is not allowed", [input.region])
}
Behavior: Blocks resources using non-approved regions.
Who this is for
- Platform Engineers designing guardrails for multi-team IaC.
- DevOps/SREs adding automated checks to CI/CD.
- Security engineers who want policy automation without slowing delivery.
Prerequisites
- Basic understanding of Terraform or Kubernetes manifests.
- Comfort with JSON/YAML and reading structured data.
- Familiarity with CI pipelines (e.g., PR checks).
Learning path
- Understand what inputs your policies will read (Terraform plan JSON or manifest files).
- Learn a policy language pattern (deny rules that produce messages).
- Write 2–3 baseline guardrails (public exposure, tags, regions).
- Run policies locally, then add to PR checks.
- Iterate with teams: small, testable policies; fast feedback.
Time estimate
- Concepts & mental model: 30–45 minutes
- Examples & exercises: 45–60 minutes
- Integrating into a demo pipeline: 60–90 minutes
Exercises
Do these locally with any policy runner you prefer, or just reason through the inputs/outputs.
Exercise 1 — Write a deny policy for public buckets
Input (conceptual JSON):
{
"kind": "aws_s3_bucket",
"name": "app-assets",
"acl": "public-read",
"tags": {"owner": "web", "cost_center": "1001"}
}
Task: Write a policy that denies when acl is public-read or public-read-write. Include a helpful message with the bucket name.
Hint
- Create a deny rule that fires when acl matches a public value.
- Return a message string including the resource name.
Expected output
A violation message like: "Bucket app-assets must not be public" and a non-zero exit in CI.
Show solution
package learn.ex1
public_acls := {"public-read", "public-read-write"}
deny[msg] {
input.kind == "aws_s3_bucket"
public_acls[input.acl]
msg := sprintf("Bucket %s must not be public", [input.name])
}
Exercise 2 — Require standard tags
Input (conceptual JSON):
{
"kind": "any_resource",
"name": "api",
"tags": {"owner": "platform"}
}
Task: Deny if tags are missing either owner or cost_center. Output which tags are missing.
Hint
- Define a set of required keys.
- Collect missing ones and use them in the message.
Expected output
Message such as: "Missing required tags: {cost_center}" and a failure status.
Show solution
package learn.ex2
required := {"owner", "cost_center"}
missing[tag] {
tag := required[_]
not input.tags[tag]
}
deny[msg] {
count(missing) > 0
msg := sprintf("Missing required tags: %v", [missing])
}
Self-check checklist
- I can write a deny rule that returns clear messages.
- I can read input fields and compare against allowed values.
- I can collect missing fields and report them in one message.
- I can reason about policy behavior without running it.
Common mistakes and how to self-check
- Inverted conditions: You deny compliant resources by mistake. Self-check: Create a passing input and ensure no messages appear.
- Overly broad rules: Deny everything. Self-check: Add a known-good example and verify it passes.
- Silent policies: No message means confused teams. Self-check: Always include actionable messages and examples.
- Enforcement too late: Only in production. Self-check: Run policies in pre-commit and PR checks.
- No version control: Policies drift between repos. Self-check: Store policies centrally and reuse via modules or shared folders.
Practical projects
Project 1 — Local guardrail pack
- Create a policy folder with three rules: public exposure, required tags, allowed regions.
- Add sample inputs and run locally to see pass/fail.
- Document expected messages in a README snippet.
Project 2 — PR check integration
- Add a policy step to your CI that evaluates the IaC plan or manifest files.
- Fail the job if any deny messages exist; print them in logs.
- Share one screenshot or log excerpt with your team for feedback.
Project 3 — Policy starter kit for teams
- Package common rules in a reusable folder or template.
- Provide examples and a simple make task to run policies.
- Collect 2 team requests and convert them into small, testable policies.
Quick Test
Anyone can take the test for free. Only logged-in users get saved progress.
When you finish, review explanations and revisit exercises if needed.
Mini challenge
Design a single policy that prevents any resource from being created without encryption at rest. It should:
- Check an encryption flag or algorithm field.
- Return a message naming the resource and the missing field.
- Pass if the flag is true and algorithm is non-empty.
Hint
Use deny[msg] with two conditions: not input.encrypted and (not input.kms_key or input.algorithm == "").
Next steps
- Expand policies to cover network rules (no 0.0.0.0/0 on sensitive ports).
- Add policy tests with known-good and known-bad inputs.
- Roll out enforcement gradually: warn in first PRs, then block once stable.