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.