테라폼 운영 강좌 #1 협업 워크플로: PR에서 plan 리뷰, GitHub Actions로 apply

4 분 소요

실전 강좌 열 편으로 인프라는 완성됐지만, 운영 방식은 아직 “각자 노트북에서 apply"입니다. 이 방식은 팀에서 오래 못 갑니다. 강한 AWS 권한이 개인 노트북마다 깔려 있어야 하고, 리뷰 없이 적용이 일어나고, 누가 무엇을 언제 apply 했는지 남지 않습니다. 운영 강좌 일곱 편의 첫 주제는 이 구조를 바꾸는 것입니다. 목표는 한 문장으로 요약됩니다. 사람은 코드와 plan을 리뷰하고, apply는 CI만 한다.

워크플로의 모양: PR에 plan, 병합에 apply #

Git 협업의 관문이 PR이므로, 테라폼의 관문도 PR에 맞춥니다.

워크플로 흐름
브랜치에서 코드 수정 → PR 생성
  → CI: fmt 검사, validate, plan 실행
  → plan 결과가 PR 코멘트로 등록
  → 리뷰어가 코드와 plan을 함께 승인
main에 병합
  → CI: 병합된 코드로 apply

핵심 관점의 전환은 리뷰 대상이 코드만이 아니라 plan이라는 것입니다. 코드 diff는 의도를 보여 주고, plan은 결과를 보여 줍니다. 기초 #2에서 -/+(교체) 기호를 반드시 확인하라고 했는데, 그 확인이 개인의 습관이 아니라 팀의 리뷰 절차가 되는 순간입니다.

자격 증명: OIDC 역할 재사용 #

CI가 AWS를 만지려면 자격 증명이 필요한데, 실전 #7에서 만들어 둔 GitHub OIDC 역할이 바로 이 용도를 위한 것이었습니다. 그 역할에 배포 권한(관리 대상 리소스와 state 버킷 접근)을 붙이고, 워크플로에서는 액세스 키 없이 역할을 맡습니다. 저장된 시크릿이 없으니 유출될 시크릿도 없습니다.

GitHub Actions 워크플로 #

두 파일로 구성합니다. 먼저 PR 쪽입니다.

.github/workflows/terraform-plan.yml
# .github/workflows/terraform-plan.yml
name: terraform plan
on:
  pull_request:
    paths: ["envs/**", "modules/**"]

permissions:
  id-token: write      # OIDC 토큰 발급
  contents: read
  pull-requests: write # plan 코멘트 등록

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.TF_PLAN_ROLE_ARN }}
          aws-region: ap-northeast-2
      - uses: hashicorp/setup-terraform@v3
      - run: terraform fmt -check -recursive
      - run: terraform -chdir=envs/dev init
      - run: terraform -chdir=envs/dev validate
      - run: terraform -chdir=envs/dev plan -no-color -out=tfplan
      # 이어서 plan 출력을 PR 코멘트로 등록하는 스텝

plan 출력을 PR 코멘트로 올리는 부분은 공개 액션이나 gh pr comment를 쓰는 짧은 스크립트로 처리합니다. 다음은 병합 쪽입니다.

.github/workflows/terraform-apply.yml
# .github/workflows/terraform-apply.yml
name: terraform apply
on:
  push:
    branches: [main]
    paths: ["envs/**", "modules/**"]

permissions:
  id-token: write
  contents: read

concurrency:
  group: terraform-dev
  cancel-in-progress: false

jobs:
  apply:
    runs-on: ubuntu-latest
    environment: dev
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.TF_APPLY_ROLE_ARN }}
          aws-region: ap-northeast-2
      - uses: hashicorp/setup-terraform@v3
      - run: terraform -chdir=envs/dev init
      - run: terraform -chdir=envs/dev apply -auto-approve

설정 하나하나가 운영 장치입니다.

  • concurrency: 병합이 연달아 일어나도 apply가 한 번에 하나만 돕니다. 기초 #9의 state 잠금이 마지막 방어선이라면 이것은 첫 방어선이고, cancel-in-progress: false로 진행 중인 apply를 중단하지 않습니다(apply 중단은 리소스를 어중간한 상태로 남깁니다).
  • environment: dev: GitHub 환경 기능에 승인자를 설정하면 apply 직전에 사람의 승인 단계를 넣을 수 있습니다. prod 워크플로에서 특히 유용합니다.
  • paths 필터: 인프라 코드가 바뀔 때만 돌아 문서 수정 같은 커밋에 CI를 낭비하지 않습니다.
  • 역할 분리: plan용 역할은 읽기 위주, apply용 역할만 쓰기 권한을 갖게 나누면 PR 단계의 권한이 최소화됩니다.

신뢰 경계가 이렇게 바뀝니다 #

이 워크플로가 자리 잡으면 권한 구조가 달라집니다. 사람에게는 읽기와 plan 권한만 남기고, 쓰기 권한은 CI의 역할에만 있습니다. 긴급 상황에 로컬 apply가 필요하면 할 수는 있지만 그것은 예외 절차이고, 예외가 일어났다는 사실 자체가 감사 기록에 남습니다. “누가 언제 무엇을 적용했나"의 답이 Git 이력과 Actions 로그로 완결되는 구조입니다.

prod까지 확장하면 구조는 같고 값만 다릅니다. envs/prod용 워크플로에 prod 역할과 승인 필수의 environment를 걸면, 실전 #9에서 만든 “dev 먼저, 검증 후 prod” 흐름이 CI 위에서 굴러갑니다.

정리 #

이번 글에서 다룬 내용입니다.

  • 로컬 apply는 권한 집중, 리뷰 부재, 기록 부재의 문제를 갖습니다. 원칙은 “사람은 리뷰, apply는 CI"입니다
  • PR에는 fmt·validate·plan이 돌고 plan 출력이 코멘트로 남아, 코드와 결과를 함께 리뷰합니다
  • CI 자격 증명은 실전 #7의 OIDC 역할입니다. plan용과 apply용 역할을 나눠 PR 단계 권한을 최소화합니다
  • concurrency로 동시 apply를 막고, environment 승인으로 prod 앞에 사람의 관문을 둡니다
  • 예외적 로컬 apply는 가능하되 기록에 남는 예외가 됩니다

다음 글(#2 drift와 state 수술)에서는 이 워크플로 밖에서 일어나는 변경을 다룹니다. 콘솔 수동 변경의 감지, import와 removed 블록으로 하는 state 편입과 제외입니다.

X