advanced 25 min read advanced-topics Updated: 2025-06-27

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.

🏷️ Topics Covered

multi environment policy management tutorialdev staging production policy differencesenvironment specific policy configurationpolicy inheritance across environmentsdynamic policy parameters by environmentci cd environment policy automation

💡 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.