Skip to content

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 tfplan

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

Get started

Run your first OpenTofu plan today.

Free for every organization. Sign up with email or Google.