Skip to main content
Every stack runs through a Ravion-managed system pipeline that plans, waits for approval, and applies. System pipelines are global and read-only, so to enforce your own rules — required tags, forbidden instance types, blast-radius limits — you copy one into your own project, insert a policy check step, and make your copy the default for an organization, project, or environment. The process at a glance:
  1. Copy the system pipeline config you want to extend.
  2. Create your own pipeline in the project that should own it.
  3. Add the policy check step between plan and approval.
  4. Make it the default at the scope you want it enforced.

How a policy check stops a run

Steps run in order, and a step that exits nonzero fails the run. Put the check after terraform:plan and before approval, and a violation stops the run before anyone can approve it — so nothing reaches terraform:apply. The policy step is a custom step: it checks out your Terraform code and runs whatever commands you give it on an EC2 runner in your own cloud account. Your code, variables, and policy rules never leave your account.

Before you start

  • The ID of the project that should own the pipeline — run ravion project list.
  • A policy tool that exits nonzero on violations. Any CLI works, because the step is plain shell.
  • The Ravion CLI authenticated against your organization.

Step 1: Copy the system pipeline config

List your pipelines and find Terraform Change Pipeline, the system pipeline with the given ID tf-change-pipeline. It appears in every organization, and its pipeline ID starts with pipe_:
Pull its config into a file you can edit and commit:
Starting from the system config keeps the plan, approval, and apply wiring that stacks depend on. See Pipeline config file for the pull, edit, apply workflow.

Step 2: Create your own pipeline

Create an empty pipeline in the project that should own it, then note the returned pipeline ID:
Keep these parts of the copied config intact, or stack runs fail:
  • The inputs block. Stacks pass repo, branch, ref, base_path, stack_id, tool, tool_version, terraform_variables, and the AWS targeting inputs by name.
  • The standard variant. Stack configs request the variant named in stack.pipelines.defaults.variant, which is standard for the standard library modules.
  • plan_file_uri: << steps.plan.output.plan_file_uri >> on the apply step, so the apply uses exactly the plan your policy check inspected.
  • The autoapprove input. Ravion only passes autoapprove to a change run when the pipeline declares that input, so removing it makes every change run wait for approval.

Step 3: Add the policy check step

Insert the step between plan and approve in change-pipeline.yaml. This example runs Checkov against the checked-out Terraform code, but any command works — swap in tflint, conftest, an OPA bundle, or your own script:
Reusing the pipeline.input.* values means the step checks out the same commit and runs in the same account and region as the plan. See Templating for the available expressions and step types for every custom step field. Three things worth knowing:
  • Pin the tool version. Installing the latest release on every run lets a new release change your policy results, or a registry outage block stack changes, with no config change on your side.
  • Skip the check when nothing changed. if: << steps.plan.output.has_changes >> keeps no-op runs fast.
  • Gate on the plan’s blast radius. The plan step publishes change counts, so you can require a second approval only when resources would be destroyed:
To inspect the plan itself rather than the source, your commands must fetch and decode it: plan_file_uri is an S3 URI for a binary plan file, so the step needs the same IaC tool and version, an initialized working directory, and read access to that bucket — grant it with infrastructure.permissions.attach. Checking the checked-out configuration needs none of that, so start there unless your rules depend on the diff.
Apply the edited config to your pipeline:

Step 4: Make your pipeline the default

Stacks resolve their pipelines from default values whose definition given IDs are change_pipeline_id and destroy_pipeline_id. Find the definition ID:
Then set your pipeline at the scope where it should apply:
The most specific scope wins: an environment value overrides a project value, which overrides the organization value. So a safe rollout is to set the environment value for staging first, confirm a run, then move the value up to the organization. Existing stacks pick the new default up on their next run — you don’t have to touch module config. To change or remove a value later:
A module definition can pin an explicit pipeline through stack.pipelines.change.pipeline_id or stack.pipelines.destroy.pipeline_id, which wins over the resolved default. The standard library modules set these to << defaults.change_pipeline_id >> and << defaults.destroy_pipeline_id >>, so defaults apply — but a definition that sets a literal pipeline ID ignores them. See the module definition schema.

Cover destroys too

The change pipeline runs when a module is created or changed. Deleting a module runs the destroy pipeline instead, so policies that must also govern teardown — for example requiring an extra approver for production deletions — need the same treatment on a copy of Terraform Destroy Pipeline (given ID tf-destroy-pipeline), pointed at through destroy_pipeline_id. The destroy config is the same shape, with plan_type: destroy on its plan step. Destroy runs always stop for approval, because Ravion only passes autoapprove to change runs.

Verify the check

Trigger a change run against a stack and watch it:
Confirm both directions before you widen the scope: a compliant stack reaches the approval step, and a stack that violates a rule fails at the policy step with your tool’s output in the step logs.