(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisbon

CI/CD Terraform with GitHub Actions: validate and plan

João Barros 24 de July de 2026 3 min read

Automating Terraform validation and planning with GitHub Actions speeds up reviews and reduces human errors. This tutorial shows how to configure a pipeline that runs terraform fmt, init, validate and terraform plan and publishes the result as a comment on the Pull Request. By automating these steps, you can detect issues before merge and provide context for infrastructure decisions to reviewers — for example, knowing that a terraform plan predicts the creation of 3 resources and the modification of 1, without having to run commands locally.

Prerequisites

  • GitHub account with a repository (public or private). A repository with a protected branch and review policies is recommended.
  • Basic Terraform knowledge (.tf files). Know how to run terraform init, terraform plan and interpret the output.
  • Basic Git and GitHub CLI to create branches and Pull Requests. A typical flow: a branch with up to 10 .tf changes per PR is easier to review.
  • Secrets in GitHub (providers/credentials) when necessary. For a repository with 1-2 providers, add the required variables as secrets.

Step 1: Minimal Terraform repository structure

Create a simple structure with an example main.tf. We keep the backend local for simplicity — in production use a remote backend (S3, Azure Storage, etc.). A minimal structure for a module or environment can be:

# main.tf
resource "null_resource" "exemplo" {
  provisioner "local-exec" {
    command = "echo Hello"
  }
}

Practical suggestion: keep the repository organized by folders per environment (for example, environments/dev, environments/prod) and limit each PR to one environment when possible. For a repository with 20-50 .tf files, the pipeline still works — runtime increases according to the number of providers and modules, typically between 10s to 2min for init+validate in small projects.

Step 2: Create the GitHub Actions workflow

Create a workflow in .github/workflows/terraform.yml that executes the steps: checkout, setup-terraform, terraform fmt, terraform init, terraform validate, terraform plan and publish the plan to the PR. We use official actions to simplify and ensure stable versions.

name: Terraform CI

on:
  pull_request:
    paths:
      - '**/*.tf'

jobs:
  terraform:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v2
      with:
        terraform_version: 1.5.0

    - name: Terraform fmt
      run: terraform fmt -check -recursive

    - name: Terraform init
      run: terraform init -input=false

    - name: Terraform validate
      run: terraform validate

    - name: Terraform plan
      id: plan
      run: |
        terraform plan -no-color -out=plan.tfplan || true
        terraform show -no-color plan.tfplan > plan.txt || true
        echo "plan-body<> $GITHUB_OUTPUT
        cat plan.txt >> $GITHUB_OUTPUT
        echo "EOF" >> $GITHUB_OUTPUT

    - name: Comment plan on PR
      uses: peter-evans/create-or-update-comment@v3
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        issue-number: ${{ github.event.pull_request.number }}
        body: |
          **Terraform Plan**

          
Mostrar output do plan (abrir) ``` ${{ steps.plan.outputs.plan-body }} ```

Practical notes: using -no-color makes reading in comments easier. The terraform plan step uses || true to ensure that, even with exit code>0, we capture the output and comment on the PR — ideal for diagnosing errors without blocking the publication of the comment.

Step 3: Configure secrets and providers

If your Terraform uses providers that require credentials (AWS, Azure, GCP), add the secrets in GitHub (Settings > Secrets > Actions). Example for AWS:

- name: Configure AWS creds
  if: env.AWS_REQUIRED == 'true'
  run: |
    echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" >> $GITHUB_ENV
    echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> $GITHUB_ENV

Practical example: for a small project, define 3 secrets (ACCESS_KEY, SECRET_KEY and REGION). Verify that the GitHub Actions token has permissions to comment on PRs (GITHUB_TOKEN has permissions by default, but for repositories with restrictions a personal token with limited permissions may be necessary).

Step 4: Handling common errors

Typical errors and quick fixes:

  • terraform fmt -check fails: run terraform fmt locally or adjust CI to auto-fix using terraform fmt -recursive. In teams, set up a pre-commit hook to avoid these errors — it can reduce CI failures by ~30%.
  • Provider auth failure: confirm secrets in the repository and environment variables in the workflow. Also check key lifetime (a rotation routine every 90 days is common).
  • terraform plan failing with exit code >0: we capture the output and continue the job with || true so we can comment the plan; analyze the output in the comment. If the plan fails due to missing remote state, check backend and locks.

Verify the result

Open a branch, make changes to the .tf files and create a Pull Request. Go to the Actions tab of the PR and verify that the workflow ran successfully. The Pull Request itself should contain a comment with the terraform plan output — if the comment does not appear, check the logs of the Comment plan on PR step and the permission of the GITHUB_TOKEN. In projects with average CI, the total time from push to comment is typically 30s–3min.

Conclusion

With this basic CI pipeline for Terraform, you gain automation in reviews and reduce regression risks. Recommended next steps: move the state to a remote backend (S3/Azure Backend), add a terraform fmt job that automatically corrects files, and gate terraform apply to protected branches with manual approval. Small improvements, such as blocking merges without plan comments or adding drift analysis, can reduce production incidents by tens of percentage points. Tip: would you like me to include an example with a remote backend (S3/Consul/Azure Storage) and locks in the workflow?