Terraform plan on PRs, apply on merge

Terraform plan on PRs, apply on merge

The most common Terraform pipeline runs `terraform fmt -check` and `terraform validate` always, `terraform plan` on every pull request, and a manually-gated `terraform apply` on the main branch.

Conversion notes

  • On GitHub, the apply job uses an `environment:` with required reviewers as the manual gate.
  • On GitLab, `when: manual` on the apply job is the gate; combine with `rules:` to ensure it only appears for main-branch pushes.
  • On CircleCI, the apply job is a `type: approval` workflow node followed by the actual apply.
  • On Bitbucket, the apply runs in `pipelines.custom.deploy` (manual trigger).

Side-by-side implementation

GitHub Actions
name: terraform
on:
  pull_request:
  push:
    branches: [main]

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform fmt -check
      - run: terraform validate
      - run: terraform plan -out=tfplan
      - uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: tfplan

  apply:
    needs: [plan]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - uses: actions/download-artifact@v4
        with:
          name: tfplan
      - run: terraform init
      - run: terraform apply tfplan
GitLab CI
stages: [plan, apply]

plan:
  stage: plan
  image: hashicorp/terraform:1.7
  script:
    - terraform init
    - terraform fmt -check
    - terraform plan -out=tfplan
  artifacts:
    paths: [tfplan]

apply:
  stage: apply
  image: hashicorp/terraform:1.7
  needs: [plan]
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
  script:
    - terraform init
    - terraform apply tfplan
CircleCI
version: 2.1

jobs:
  plan:
    docker:
      - image: hashicorp/terraform:1.7
    steps:
      - checkout
      - run: terraform init
      - run: terraform fmt -check
      - run: terraform plan -out=tfplan
      - persist_to_workspace:
          root: .
          paths: [tfplan]

  apply:
    docker:
      - image: hashicorp/terraform:1.7
    steps:
      - checkout
      - attach_workspace:
          at: .
      - run: terraform apply tfplan

workflows:
  main:
    jobs:
      - plan
      - hold:
          type: approval
          requires: [plan]
          filters:
            branches: { only: [main] }
      - apply:
          requires: [hold]
          filters:
            branches: { only: [main] }
Bitbucket Pipelines
image: hashicorp/terraform:1.7

pipelines:
  default:
    - step:
        name: plan
        script:
          - terraform init
          - terraform fmt -check
          - terraform plan -out=tfplan
        artifacts: [tfplan]
  custom:
    deploy:
      - step:
          name: plan
          script:
            - terraform init
            - terraform plan -out=tfplan
          artifacts: [tfplan]
      - step:
          name: apply
          trigger: manual
          script:
            - terraform init
            - terraform apply tfplan

Related Tools