Learn OpenTofu - CI/CD Pipeline Automation (GitHub Actions & GitLab CI)
Episode 13 of 21

Learn OpenTofu - CI/CD Pipeline Automation (GitHub Actions & GitLab CI)

GitOps pipelines turn manual apply into a safe, documented flow. This episode builds a PR-to-production flow with the official setup-opentofu action on GitHub Actions and its counterpart on GitLab CI: fmt, validate, security scan, plan with PR comments, and apply via OIDC with an approval gate.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In the previous episode 12 we arranged a DRY architecture with Terragrunt: backends and providers centralized in the root file, while the Dev, Staging, and Prod folders only carry genuinely different values. That structure removes duplication, but it still leaves the most fatal weak point in IaC: who presses tofu apply. Manual apply from a laptop means the wrong environment, leaked credentials, and changes with zero trace in the repository. In the real world, this is the most common cause of drift and production incidents.

In episode 13 we'll build CI/CD pipeline automation: using the official opentofu/setup-opentofu action on GitHub Actions, then arranging a full GitOps flow — PR arrives, tofu fmt, tofu validate, security scan, tofu plan posting a comment to the PR, merge, approval gate, then tofu apply through OIDC. At the end we mirror it all in GitLab CI via .gitlab-ci.yml. Let's start.

Main Discussion

Why Automation Replaces Manual Apply

Imagine a team of four, each with their own laptop and different AWS account. One person applies from an old branch, another forgets to run tofu init, and a third changes something directly in the console. All of that leaves a big question mark: what actually happened, by whom, and when? A pipeline turns those questions into definite answers, because the entire flow is recorded as build logs and only two things can trigger execution: repository events and documented human approval.

Setting Up OpenTofu on GitHub Actions

GitHub Actions officially provides the opentofu/setup-opentofu action — maintained by the OpenTofu team itself, not a third party. This action downloads the tofu binary at the requested version, verifies its checksum, and stores the download as a cache so subsequent jobs don't re-download it. Installing it is as simple as one step:

setup-opentofu in a workflow
- name: Setup OpenTofu
  uses: opentofu/setup-opentofu@v1
  with:
    tofu_version: 1.8.2
    cache: true
    github_token: ${{ secrets.GITHUB_TOKEN }}

Tip

Pin the tofu_version explicitly in the workflow. OpenTofu minor versions evolve quickly, and determinism is non-negotiable for pipelines — a job whose behavior changes just because of an overnight release must never happen in an IaC repository.

The Full GitOps Flow: from PR to Production

Here's a workflow that summarizes the entire flow: a pull request triggers validate and security-scan, then plan posts a comment; a push to the main branch triggers apply running in an environment with an approval gate. Cloud credentials are never stored as long-lived secrets — every job receives a temporary AWS token via OIDC:

.github/workflows/gitops.yaml
name: OpenTofu GitOps
 
on:
  pull_request:
    types: [opened, synchronize, reopened]
  push:
    branches: [main]
 
permissions:
  id-token: write
  contents: read
  pull-requests: write
 
env:
  TF_IN_AUTOMATION: "1"
 
jobs:
  validate:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup OpenTofu
        uses: opentofu/setup-opentofu@v1
        with:
          tofu_version: 1.8.2
          cache: true
      - run: tofu fmt -check -recursive
      - run: tofu init
      - run: tofu validate
 
  security-scan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Checkov scan
        uses: bridgecrewio/checkov-action@v1
        with:
          directory: .
          framework: tofu
      - name: Trivy config scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: config
          severity: HIGH,CRITICAL
          exit-code: 1
 
  plan:
    needs: [validate, security-scan]
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup OpenTofu
        uses: opentofu/setup-opentofu@v1
        with:
          tofu_version: 1.8.2
          cache: true
      - name: Assume role via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/opentofu-github-oidc
          aws-region: ap-southeast-1
      - name: Plan
        run: |
          tofu init
          tofu plan -out=plan.tfplan
      - name: Post plan comment to PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          tofu show -no-color plan.tfplan > plan.txt
          gh pr comment "$PR_NUMBER" --body-file plan.txt
 
  apply:
    needs: [plan]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Setup OpenTofu
        uses: opentofu/setup-opentofu@v1
        with:
          tofu_version: 1.8.2
          cache: true
      - name: Assume role via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/opentofu-github-oidc
          aws-region: ap-southeast-1
      - run: tofu init
      - run: tofu apply -auto-approve

Notice the strict role separation in this workflow. The validate and security-scan jobs have no cloud access at all — they purely check the code. The plan job is only entitled to use the OIDC role, and the apply job is hung on environment: production, which in the repository settings is configured to require reviewers — a human approval gate that nobody with push access can bypass.

Warning

Never put long-lived AWS credentials in as workflow secrets. With OIDC, the AWS role only trusts short-lived tokens from GitHub, and the apply job only runs after approval. Long-lived secrets in CI are one of the most common leak sources in IaC repositories.

The GitLab CI Counterpart: .gitlab-ci.yml

GitLab CI offers the same concepts with different syntax: ordered stages, artifacts to pass plan files between jobs, rules to limit when jobs run, and id_tokens for OIDC. The apply job is manual and tied to the production environment, so it requires approval:

.gitlab-ci.yml
stages:
  - validate
  - plan
  - apply
 
image: ghcr.io/opentofu/opentofu:1.8.2
 
variables:
  TF_IN_AUTOMATION: "true"
 
workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
 
tofu:validate:
  stage: validate
  script:
    - tofu fmt -check -recursive
    - tofu init
    - tofu validate
 
tofu:plan:
  stage: plan
  id_tokens:
    AWS_ID_TOKEN:
      aud: https://gitlab.com
  script:
    - tofu init
    - tofu plan -out=plan.tfplan
  artifacts:
    paths:
      - plan.tfplan
    expire_in: 1 week
 
tofu:apply:
  stage: apply
  id_tokens:
    AWS_ID_TOKEN:
      aud: https://gitlab.com
  script:
    - tofu init
    - tofu apply plan.tfplan
  environment:
    name: production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  when: manual

The id_tokens block gives each job an OIDC token exchangeable for an AWS role, similar to the role of aws-actions/configure-aws-credentials on GitHub. The plan.tfplan artifact passes from the plan job to the apply job so what's executed is exactly what was reviewed — with no window between review and execution.

Conclusion

In episode 13 we built the automation foundation:

  • Replaced manual apply with a fully documented CI/CD flow.
  • Used the official opentofu/setup-opentofu action with pinned versions and caching.
  • Arranged a GitOps flow: PR triggers tofu fmt, tofu validate, security scan, and tofu plan with PR comments; merge to main triggers apply.
  • Secured credentials with OIDC instead of long-lived secrets.
  • Applied an approval gate through the production environment.
  • Built a full counterpart in GitLab CI with id_tokens and when: manual.

Building your own pipeline gives full control, but not without maintenance costs. In the next episode, episode 14, we'll review the managed platform ecosystem — Spacelift, env0, Scalr — and get to know Digger, an open-source GitOps tool that executes OpenTofu directly inside GitHub Actions or GitLab CI without external SaaS. See you there!

Learn OpenTofu - CI/CD Pipeline Automation (GitHub Actions & GitLab CI) | Learn OpenTofu