OpenTofu in GitHub Actions vs a managed platform
GitHub Actions can run OpenTofu with a few dozen lines of YAML. The first workflow is easy. Approvals, locking, policy, and per-environment credentials are where the work is.
A minimal OpenTofu workflow
This workflow plans on every pull request and applies on pushes to main. It uses GitHub's OIDC token to assume an AWS role, so no access keys are stored as secrets:
name: opentofu
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
id-token: write
concurrency:
group: opentofu-production
jobs:
tofu:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/opentofu-production
aws-region: us-east-1
- run: tofu init -input=false
- run: tofu plan -input=false -out=tfplan
- if: github.event_name == 'push'
run: tofu apply -input=false tfplanPin action versions to commit SHAs in a real repository, and point the configuration at a remote backend such as S3 with locking turned on.
What you have to build yourself
Approval that binds to the plan
GitHub environments can require a reviewer before a job runs, but the reviewer approves a job, not a plan. To make sure the apply runs what was reviewed, you need to save the plan as an artifact, show it to the reviewer, and apply that exact file in a later job.
Locking and queueing
The backend lock stops two applies from writing at once, but a second job fails rather than waiting. The concurrency group above queues jobs per environment; you have to keep it consistent across every workflow that touches that state.
Least-privilege credentials for pull requests
Pull request plans run unreviewed code. Give them a separate, read-only role with an OIDC trust condition on the pull request context, and keep the apply role restricted to the protected branch.
Policy and audit
Policy checks mean adding a tofu show -json step and an OPA or Conftest step. Audit means stitching together workflow logs, CloudTrail, and backend access logs.
State access
The backend bucket now holds secrets in plain JSON. You manage its encryption, versioning, access policy, and who can download it.
When GitHub Actions is enough
- You have a handful of stacks and one or two environments.
- One team owns all of them and is happy to own the pipeline.
- Your configuration has to reach private networks through self-hosted runners.
When a managed platform fits better
- Many workspaces, many teams, or regulated environments.
- Approvals must bind to the exact plan, with a record of who approved.
- You want policy, state history, and audit without assembling them.
Or use both
You don't have to choose. Keep GitHub Actions for tests and linting, and let the plan and apply run in StackShip. With a workspace token stored as a secret, a step running tofu plan starts a remote run through the cloud block, streams its output into the job log, and applies policy like any other run:
- run: tofu init -input=false && tofu plan -input=false
env:
TF_TOKEN_stackship_run: ${{ secrets.STACKSHIP_TOKEN }}If you connect the repository directly, StackShip plans every pull request and reports a GitHub check without any workflow at all. See runs and approvals for the full flow.