Running Terraform and OpenTofu on GitHub Actions
Most teams start with one workflow file that plans on every pull request and applies on merge. It works well for one stack. This page shows that workflow, where it stops once a repository holds many stacks that depend on each other, and the same repository with Stackorder. Terraform keeps running on your Actions runners either way.
The usual workflow: plan on pull_request, apply on merge
name: terraform
on:
pull_request:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: stacks/prod/vpc
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform
aws-region: us-east-1
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- run: terraform plan -input=false
if: github.event_name == 'pull_request'
- run: terraform apply -input=false -auto-approve
if: github.event_name == 'push'The job assumes an AWS role with its GitHub OIDC token, so there are no long-lived keys. For OpenTofu, use opentofu/setup-opentofu@v1 and run tofu instead of terraform. The plan output lands in the job log, and the apply runs after the merge.
What a single workflow cannot do
The workflow above names one stack directory. With several stacks, and modules and remote state shared between them, these jobs are left to you:
- Find the affected stacks. A change to a local module affects every stack that uses it, and a stack that reads another stack's state through
terraform_remote_stateis affected when that stack changes. A path filter sees the directories that changed, not the stacks that depend on them. - Order applies across stacks. A VPC has to apply before the cluster that uses it. Hard-coded
needs:between jobs fix one order in the file, whatever the change touches. - Lock stacks across pull requests. Terraform's state lock covers one command, not the time between a plan and its apply, so two open pull requests can both plan the same stack.
- Check fresh plans and approvals before apply. An apply that re-plans at merge time can apply something other than the plan the reviewer read.
- Schedule drift checks with issues. Each stack needs a scheduled plan, and something has to open an issue when it finds drift and close it once the drift is gone.
The same workflow with Stackorder
Stackorder keeps plans and applies in your Actions jobs and adds a server that holds the dependency graph and decides which stacks run and in what order. A repository gets a root stackorder.yaml, whose smallest form is version: 1, the Stackorder GitHub App, and two workflow files. The plan workflow runs on every pull request:
name: stackorder plan
on:
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: stackorder-plan-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
plan:
permissions:
id-token: write
contents: read
actions: read
checks: write
pull-requests: read
uses: stackorder/actions/.github/workflows/plan.yml@v1
with:
server-url: ${{ vars.STACKORDER_SERVER_URL }}
aws-role-arn: arn:aws:iam::123456789012:role/stackorder-plan
tool: terraform
secrets: inheritThe run workflow, stackorder-run.yml, runs only when the server dispatches it; Workflows has the file. Each gap above maps to one thing Stackorder does:
- Affected stacks. The
resolvejob scans the repository, uploads the graph and gets back the affected stacks as a job matrix: stacks changed directly, stacks that use a changed local module, stacks that read a changed stack's state, and their dependents. One plan job runs per stack, under the plan role, with onestackorder/plan: <key>check each and one sticky pull request comment. See The affected set. - Order. A
stackorder applycomment, or the merge inon_mergemode, applies the affected stacks in waves. The server dispatchesstackorder-run.ymlone wave at a time, and a failed stack blocks its dependents. - Locks. Before the first wave, the server locks every affected stack, all or nothing, and releases the locks on merge or when the run completes. See Locks.
- Fresh plans and approvals. The apply gate checks who asked, the pull request's approvals, fresh plans on the head commit, named policy checks and locks, and reports every failure in one comment. Each apply job runs under the stack's GitHub environment and assumes that environment's apply role through OIDC, so environment reviewers and the role's trust policy remain the hard gates.
- Drift. On a
drift.schedule, every stack runsplan -detailed-exitcode, andopen_issuekeeps one GitHub issue per drifted stack. It never applies to fix drift. See Drift detection.
The server never runs Terraform and holds no cloud credentials or state. State stays in your S3 bucket, and Stackorder adds under 10 s to what Terraform itself needs in each job. Getting started sets up one repository, and the local demo runs everything on one machine with no GitHub App and no AWS account.
FAQ
How do I run terraform plan on every pull request with GitHub Actions?
Trigger a workflow on pull_request, assume a read-only AWS role with aws-actions/configure-aws-credentials and the job's OIDC token, install Terraform, and run terraform init and terraform plan in each stack directory. With Stackorder, stackorder-plan.yml does this for every stack the pull request affects, and the server posts one check per stack and one sticky comment.
How do I apply Terraform in dependency order across multiple stacks?
Declare the dependencies and let a tool order them. In Stackorder, depends_on in a stack's .stackorder.yaml orders the run, and a terraform_remote_state read of another stack's state is inferred as an ordering edge too. Module sources decide which stacks are affected, not their order. Affected stacks are layered into waves by the longest path through the graph, and a cycle fails stackorder/resolve with the cycle spelled out. Edges to stacks in other repositories appear in the graph and can trigger plan-only runs there, but each run applies one repository.
Does it work with OpenTofu (setup-opentofu)?
Yes. Set tool: tofu in stackorder.yaml, or per stack. The reusable workflows install each stack's tool with hashicorp/setup-terraform@v3 or opentofu/setup-opentofu@v1, and Stackorder's end-to-end tests run OpenTofu 1.12.