Multi-Environment Policy Management
Learn to manage and apply different policy rules across your development, staging, and production environments using a single, parameterized policy set.
📋 Prerequisites
- Experience with managing distinct deployment environments (e.g., dev, staging, prod).
- Proficiency in at least one policy-as-code framework (OPA/Rego is used in examples).
- Strong understanding of CI/CD principles and pipeline configuration (GitHub Actions is used).
- Familiarity with passing variables and data files (JSON) to scripts.
What You'll Learn
🏷️ Topics Covered
💡 The Core Principle
The goal is to strike a balance between developer velocity and production stability. Policies should be lenient in development to encourage experimentation but increasingly strict as code moves closer to production.
Choosing a Strategy
There are several ways to manage policies across environments. For most use cases, **Parameterization** provides the best balance of maintainability and power, and it's the strategy we'll focus on in this guide.
🥇 Parameterization (Recommended)
A single policy's behavior is modified by an input parameter (e.g., environment = "prod"). This is highly reusable and avoids code duplication.
🥈 Separate Policy Sets
Maintain entirely separate sets of policy files for each environment (e.g., a prod/ and a dev/ directory). This is explicit but leads to high code duplication.
🥉 Policy Inheritance
A base set of policies applies everywhere, with environment-specific files overriding or adding rules. This is a good balance but can be harder to debug.
Implementation: A Parameterized OPA Policy
Let's create a parameterized policy that restricts AWS EC2 instance types. The rules will be defined in a separate JSON file, making them easy to manage without changing the policy logic.
Step 1: Define Environment Settings (config.json)
Create a data file that defines the allowed settings for each environment. This decouples your configuration from your logic.
{
"environments": {
"dev": {
"allowed_instance_types": ["t2.micro", "t3.small", "t3.medium"]
},
"staging": {
"allowed_instance_types": ["t3.medium", "t3.large"]
},
"prod": {
"allowed_instance_types": ["m5.large", "m5.xlarge"]
}
}
} Step 2: Write the Environment-Aware Policy (policy.rego)
This single Rego policy can handle all environments. It uses the input.environment variable to look up the correct rules from the data.config file.
package terraform.validation
import rego.v1
# Find all EC2 instances in the Terraform plan
ec2_instances := [res | res := input.resource_changes[_]; res.type == "aws_instance"]
# Get the allowed types for the CURRENT environment
allowed_types := data.config.environments[input.environment].allowed_instance_types
# Violation rule
deny[msg] if {
# For every instance...
some instance in ec2_instances
# ...get its planned instance type
instance_type := instance.change.after.instance_type
# ...check if it's NOT in the allowed list for this environment
not contains(allowed_types, instance_type)
# If not, generate a clear error message
msg := sprintf("Instance type '%s' is not allowed in the '%s' environment. Allowed types are: %v", [
instance_type,
input.environment,
allowed_types
])
} Integrating with CI/CD
The final step is to configure your CI/CD pipeline to pass the correct environment name and configuration to your policy check. This example uses a GitHub Actions matrix strategy to run the same job for different environments based on the target branch of a pull request.
Dynamic Inputs in GitHub Actions
name: Multi-Environment Policy Check
on:
pull_request:
branches:
- main # Represents 'prod'
- staging # Represents 'staging'
- develop # Represents 'dev'
jobs:
policy-check:
runs-on: ubuntu-latest
# Define a matrix to map git branches to environment names
strategy:
matrix:
environment:
- dev
- staging
- prod
include:
- environment: dev
branch: develop
- environment: staging
branch: staging
- environment: prod
branch: main
# Only run the job if the PR target branch matches the matrix branch
if: github.base_ref == matrix.branch
steps:
- uses: actions/checkout@v4
- uses: open-policy-agent/setup-opa@v2
- name: Create environment input file
run: echo '{"environment": "${{ matrix.environment }}"}' > env.json
- name: Run OPA Check
run: |
# opa eval combines the terraform plan (input), the config (data), and the environment (data)
opa eval \
--input plan.json \
--data policy.rego \
--data config.json \
--data env.json \
"data.terraform.validation.deny" Best Practices
💡 Key Takeaways
- Parameterize, Don't Duplicate: Prefer passing environment data to a single, smart policy over maintaining multiple, near-identical policy files. This reduces maintenance overhead.
- Decouple Policy from Configuration: Keep your environment-specific settings (like allowed instance types or regions) in a separate data file (e.g.,
config.json), not hardcoded in the policy logic. - Automate Environment Detection: Use CI/CD variables or Git branch names to automatically determine and pass the correct environment to your policy checks. Avoid manual selection.
- Test All Environments: When you unit test your policies, ensure you have test cases that validate the logic for each of your environments (dev, staging, prod) to prevent surprises.