Skip to content

Terraform in CI/CD Pipelines: GitHub Actions, GitLab CI & Terraform Cloud

DodaTech 6 min read

In this tutorial, you'll learn about Terraform in CI/CD Pipelines: GitHub Actions, GitLab CI & Terraform Cloud. We cover key concepts, practical examples, and best practices.

Running Terraform in CI/CD pipelines transforms infrastructure changes from manual CLI operations into automated, reviewed, and audited deployment workflows integrated with your software delivery process.

What You'll Learn

In this tutorial, you will learn how to build Terraform CI/CD pipelines with GitHub Actions, GitLab CI, and Terraform Cloud, implement plan-only checks on pull requests, design safe approval workflows for production, use OIDC for cloud authentication, and manage state across environments in CI.

Why It Matters

Manual <a href="/devops/terraform/">terraform</a> apply in production lacks audit trails and depends on individual judgment. CI/CD pipelines enforce consistent workflows, require code review, prevent unapproved changes, and provide full deployment history for compliance.

Real-World Use

DodaTech runs Terraform in GitHub Actions for dev and staging environments and Terraform Cloud for production. Durga Antivirus Pro's platform team reviews every production plan through Terraform Cloud's run UI before applying, maintaining SOC 2 compliance with full audit trails.

Pipeline Architecture

graph LR
    A[Git Push / PR] --> B{CI Trigger}
    B --> C[Feature Branch]
    B --> D[Main Branch]
    C --> E[Plan-Only Check]
    E --> F[PR Comment with Plan]
    F --> G[Code Review]
    G --> H[Merge to Main]
    D --> I[Init + Validate]
    I --> J[Plan]
    J --> K{Environment}
    K --> L[Dev: Auto-Apply]
    K --> M[Staging: Auto-Apply]
    K --> N[Production: Manual Approve]
    L --> O[Terraform Apply]
    M --> O
    N --> P[Approval Gate]
    P --> O
    style A fill:#4a90d9,color:#fff
    style H fill:#50c878,color:#fff
    style P fill:#ff9900,color:#fff

GitHub Actions Pipelines

Plan-Only on Pull Request

# .github/workflows/terraform-plan.yml
name: Terraform Plan
on:
  pull_request:
    branches: [main]
    paths:
      - 'terraform/**'
      - '.github/workflows/terraform-*'

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  plan:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: terraform/environments/dev

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-terraform
          aws-region: us-east-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.5

      - name: Terraform Init
        run: terraform init -backend-config=backend.hcl

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

      - name: Terraform Validate
        run: terraform validate -no-color

      - name: Terraform Plan
        run: terraform plan -no-color

Expected output in PR check:

Terraform Plan succeeded.

No changes. Your infrastructure matches the configuration.

Or:

Terraform will perform the following actions:
  # aws_instance.web will be updated in-place
  ~ resource "aws_instance" "web" {
      ~ instance_type = "t3.medium" -> "t3.large"
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Apply on Merge with Approval Gate

# .github/workflows/terraform-apply.yml
name: Terraform Apply
on:
  push:
    branches: [main]
    paths:
      - 'terraform/**'

permissions:
  id-token: write
  contents: read

jobs:
  apply-production:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: terraform/environments/production

    environment:
      name: production
      url: https://console.aws.amazon.com

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-terraform
          aws-region: us-east-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.5

      - name: Terraform Init
        run: terraform init -backend-config=backend.hcl

      - name: Terraform Plan
        run: terraform plan -no-color -out=plan.tfplan

      - name: Terraform Apply
        run: terraform apply -auto-approve plan.tfplan

GitLab CI Pipelines

Multi-Environment Pipeline

# .gitlab-ci.yml
image: hashicorp/terraform:1.9

cache:
  key: "${CI_COMMIT_REF_SLUG}"
  paths:
    - .terraform/

variables:
  TF_ROOT: terraform/environments/${ENVIRONMENT_NAME}

terraform:plan:
  stage: plan
  script:
    - cd ${TF_ROOT}
    - terraform init -backend-config=backend.hcl
    - terraform fmt -check -recursive
    - terraform validate
    - terraform plan -no-color
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - terraform/**/*

terraform:apply-dev:
  stage: apply
  script:
    - cd ${TF_ROOT}
    - terraform init -backend-config=backend.hcl
    - terraform apply -auto-approve
  variables:
    ENVIRONMENT_NAME: dev
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
      changes:
        - terraform/**/*

terraform:apply-production:
  stage: apply
  script:
    - cd ${TF_ROOT}
    - terraform init -backend-config=backend.hcl
    - terraform plan -no-color -out=plan.tfplan
    - terraform apply plan.tfplan
  variables:
    ENVIRONMENT_NAME: production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      changes:
        - terraform/**/*
  when: manual

Expected output: GitLab runs terraform plan automatically on merge requests. Dev applies on merge to develop. Production requires a manual button click in the GitLab UI.

OIDC Authentication

GitHub Actions OIDC to AWS

# aws-oidc.tf
resource "aws_iam_role" "github_actions" {
  name = "github-actions-terraform"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow]
        Principal = {
          Federated = "arn:aws:iam::${var.account_id}:oidc-provider/token.actions.githubusercontent.com"
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = "repo:${var.github_org}/${var.github_repo}:*"
          }
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "terraform" {
  name = "terraform-execution"
  role = aws_iam_role.github_actions.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow]
        Action = [
          "ec2:*",
          "s3:*",
          "dynamodb:*",
          "iam:*]
        ]
        Resource = ["*"]
      }
    ]
  })
}

Terraform Cloud Workflows

VCS-Driven Pipeline

terraform {
  cloud {
    organization = "dodatech"
    workspaces {
      name = "production"
    }
  }
}
terraform plan

Expected output: Terraform Cloud receives the plan request. The plan runs in Terraform Cloud's infrastructure. Output is visible in the TFC UI with cost estimation and policy check results.

Common Mistakes

1. Auto-Approving Production

Skipping manual approval in production can cause unreviewed destruction of critical resources.

2. Storing Credentials in CI Variables

AWS access keys in CI variables get leaked through logs. Always use OIDC for cloud authentication.

3. Sharing State Across CI Runners

Multiple CI runners applying to the same state file cause lock conflicts. Isolate state per environment.

4. Skipping Fmt Check in CI

Formatting inconsistencies accumulate. Always run <a href="/devops/terraform/">terraform</a> fmt -check early in the pipeline.

5. Not Using Plan Files

Applying without a saved plan file means the apply may differ from what was reviewed. Always save and reuse plan files.

Practice Questions

1. How does OIDC improve security in Terraform CI/CD pipelines? OIDC removes static cloud credentials from CI variables by exchanging short-lived tokens with the cloud provider's IAM service.

2. What is the difference between plan-only and apply pipelines? Plan-only runs on PRs to preview changes without modifying infrastructure. Apply runs on merge to execute approved changes.

3. How do you enforce manual approval for production Terraform applies? Use GitHub Environments with required reviewers or Terraform Cloud's manual apply setting in the workspace.

4. Challenge: Set up a GitHub Actions workflow with three stages: lint (fmt + validate), plan (with PR comment), and apply (on merge to main with environment approval gate). Use OIDC authentication.

Mini Project: Complete Terraform CI/CD Pipeline

Create a GitHub Actions workflow with plan-only on PR, auto-apply for dev and staging, and manual-approve for production. Include OIDC authentication, terraform fmt check, validate, tflint, and a PR comment with the plan output. Set up separate state backends per environment.

Testing Terraform Configurations
Remote State with S3 & DynamoDB

What's Next

Build Terraform CI/CD pipelines to automate your infrastructure deployments, then implement Testing for validation. Study Docker and DevOps practices for end-to-end automation.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro