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.