LLM WikiAccess-protected knowledge portal
← 스터디 홈
5편 · 약 16분

CI/CD 통합과 팀 협업 전략

Terraform CI/CD의 목표는 자동 적용이 아니라 통제된 변경이다

Terraform을 팀에서 쓰기 시작하면 가장 먼저 부딪히는 문제는 HCL 문법이 아니다. 누가 언제 terraform apply를 실행했는지, 그때 본 plan이 실제로 적용된 plan과 같은지, 같은 state를 두 사람이 동시에 건드리지 않았는지가 더 중요해진다.

그래서 Terraform CI/CD는 단순히 명령어를 GitHub Actions나 Jenkins로 옮기는 작업이 아니다. 핵심은 인프라 변경을 코드 리뷰, 재현 가능한 plan, 승인, 감사 로그, 잠금이 있는 실행 경로로 고정하는 것이다.

좋은 기본 흐름은 다음과 같다.

  1. Pull request에서는 fmt, validate, 정적 검사, speculative plan을 실행한다.
  2. 리뷰어는 HCL diff만 보지 않고 plan 결과까지 확인한다.
  3. main branch에 merge된 뒤에만 apply가 가능하다.
  4. production apply에는 별도의 승인이나 protected environment를 둔다.
  5. state는 remote backend에 두고 locking을 켠다.
  6. 실행 계정은 사람의 장기 credential이 아니라 CI용 단기 credential을 쓴다.

이 구조를 만들면 팀원이 로컬에서 직접 apply하는 빈도가 줄고, 장애가 났을 때도 "어떤 commit이 어떤 plan으로 어떤 리소스를 바꿨는지"를 추적할 수 있다.

PR 생성 HCL 변경 검증 fmt / validate lint / policy Plan 변경 예측 PR에 결과 노출 리뷰 코드 + plan 승인 조건 Apply main merge 이후 환경별 승인 Remote State locking + audit trail 검증 output drift 감시
Terraform CI/CD 흐름

plan과 apply를 분리하되, 같은 plan을 적용해야 한다

자동화에서 가장 위험한 패턴은 PR에서 plan을 보여준 뒤, merge 후에는 다시 plan 없이 바로 apply하거나, 리뷰한 plan과 다른 조건에서 apply하는 것이다. HashiCorp의 자동화 가이드는 비대화형 실행에서 다음 흐름을 기본으로 제시한다.

terraform init -input=false
terraform plan -out=tfplan -input=false
terraform apply -input=false tfplan

-input=false는 CI가 중간에 프롬프트에서 멈추지 않게 한다. 대신 필요한 변수는 *.tfvars, 환경 변수, CI secret, HCP Terraform workspace variable 같은 경로로 미리 제공해야 한다. -out=tfplan은 리뷰한 plan을 파일로 남겨 같은 실행 산출물을 apply할 수 있게 한다.

다만 plan 파일은 아무 곳에서나 안전하게 재사용할 수 있는 휴대용 문서가 아니다. plan과 apply가 다른 runner에서 실행된다면 Terraform 버전, provider plugin, OS와 CPU architecture, 모듈 경로, credential이 같아야 한다. 특히 저장된 plan은 민감한 값도 포함할 수 있으므로 artifact 보관 권한과 만료 정책을 신중하게 잡아야 한다.

실무적으로는 두 가지 모델 중 하나를 고른다.

모델장점주의점
CI runner에서 직접 plan/apply기존 CI와 단순히 연결 가능, 로그와 승인 흐름을 직접 설계 가능state locking, artifact 보안, runner credential, 동시 실행 제어를 직접 책임져야 함
HCP Terraform/원격 실행remote state, run lifecycle, 승인 UI, VCS 연동을 플랫폼이 제공조직·workspace 설계와 권한 모델을 먼저 정해야 함

어느 쪽이든 "PR에서 본 plan"과 "production에 적용되는 변경" 사이의 간격을 줄이는 것이 핵심이다.

PR 단계: 빠르게 실패시키고, 리뷰 가능한 plan을 남긴다

Pull request에서는 실제 인프라를 바꾸지 않는다. 대신 바꿔도 되는 변경인지 판단할 수 있도록 신호를 많이 만든다.

기본 게이트는 다음 순서가 읽기 좋다.

  1. terraform fmt -check로 형식 차이를 막는다.
  2. terraform init -input=false로 backend와 provider를 준비한다.
  3. terraform validate -no-color로 구성 오류를 잡는다.
  4. terraform plan -no-color -input=false로 변경 예측을 만든다.
  5. 필요하면 tflint, checkov, OPA, Sentinel, Infracost 같은 정책·보안·비용 검사를 붙인다.

GitHub Actions에서는 hashicorp/setup-terraform action으로 Terraform CLI를 설치하고, 필요한 경우 HCP Terraform/Terraform Enterprise token을 CLI config에 넣을 수 있다. 이 action의 wrapper를 켜면 Terraform 명령의 stdout, stderr, exitcode를 후속 step에서 사용할 수 있어 PR comment 작성에 편하다.

name: terraform-pr

on:
  pull_request:
    paths:
      - "infra/**"

jobs:
  plan:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: infra
    env:
      TF_IN_AUTOMATION: "1"
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v4
        with:
          terraform_version: "1.14.6"
      - run: terraform fmt -check
      - run: terraform init -input=false
      - run: terraform validate -no-color
      - run: terraform plan -no-color -input=false

여기서 TF_IN_AUTOMATION=1은 Terraform이 자동화 환경에 맞는 출력 방식을 쓰도록 알려준다. 사람에게 "다음 명령을 실행하라"는 식의 로컬 실행 힌트가 줄어들어, CI 로그를 읽는 사람이 우회 실행을 덜 유도받는다.

apply 단계: main branch와 환경 승인을 기준으로 좁힌다

apply는 PR branch에서 열어두면 안 된다. 보통은 main 또는 release branch merge 이후에만 실행하고, production은 별도 승인 게이트를 둔다.

GitHub Actions라면 environment: production과 required reviewers를 연결할 수 있고, GitLab/Jenkins라면 manual approval stage를 둔다. HCP Terraform의 VCS-driven workflow를 쓰면 workspace가 특정 branch에 연결되고, branch에 commit이 merge될 때 run이 자동으로 queue된다. PR에는 speculative plan이 붙고, 실제 run은 version control의 특정 commit과 연결된다.

HCP Terraform을 쓸 때 알아둘 운영 디테일도 있다.

  • workspace는 하나의 VCS branch에 연결된다.
  • 특정 파일·디렉터리 변경만 run을 trigger하도록 설정할 수 있다.
  • 새 VCS-driven workspace는 첫 run을 수동으로 queue해야 webhook run을 받는 상태가 될 수 있다.
  • manual run은 VCS의 최신 commit을 새로 가져오는 것이 아니라 workspace의 현재 configuration version을 사용한다.

이 차이를 모르면 "방금 push했는데 manual run에 반영되지 않았다" 같은 혼란이 생긴다. VCS-driven run과 manual run은 같은 버튼처럼 보여도 configuration을 가져오는 방식이 다르다.

monorepo에서는 변경 감지와 state 경계를 같이 설계한다

여러 환경과 모듈이 한 저장소에 있으면 CI/CD 설계가 갑자기 어려워진다. infra/dev, infra/stage, infra/prod, modules/network가 같이 있을 때 어떤 변경이 어느 workspace를 실행해야 할지 정해야 하기 때문이다.

나쁜 설계는 모든 변경마다 모든 환경의 plan을 돌리는 것이다. 비용도 크고, 리뷰어도 plan noise에 무뎌진다. 더 나은 기준은 state 단위와 CI trigger 단위를 맞추는 것이다.

예를 들어 다음처럼 나눌 수 있다.

infra/
  dev/      # dev state
  stage/    # stage state
  prod/     # prod state
modules/
  network/  # 공용 모듈
  service/  # 공용 모듈
  • infra/dev/**가 바뀌면 dev workspace만 plan한다.
  • infra/prod/**가 바뀌면 prod workspace plan과 production approval을 요구한다.
  • modules/network/**가 바뀌면 이 모듈을 참조하는 환경의 plan을 모두 돌린다.
  • 문서나 README만 바뀌면 Terraform run을 생략한다.

HCP Terraform VCS trigger도 working directory와 trigger pattern을 지원한다. 직접 CI를 구성할 때도 paths filter나 변경 파일 목록을 사용해 같은 원칙을 구현할 수 있다. 중요한 것은 "어느 파일이 어느 state에 영향을 주는가"를 사람이 암기하지 않게 만드는 것이다.

credential은 로컬 개인 키가 아니라 실행 주체로 관리한다

Terraform CI/CD에서 secret 관리는 특히 조심해야 한다. CI가 cloud API를 호출하므로 권한이 과하면 한 번의 잘못된 apply가 큰 장애나 비용으로 이어질 수 있다.

권장 방향은 다음과 같다.

  1. 장기 access key를 저장소 secret에 오래 보관하지 않는다.
  2. GitHub Actions OIDC, cloud workload identity, Vault 같은 방식으로 단기 credential을 발급한다.
  3. 환경별 role을 분리한다. dev apply role과 prod apply role은 같으면 안 된다.
  4. plan 전용 권한과 apply 권한을 가능하면 분리한다.
  5. CI 로그에 secret이 찍히지 않도록 -no-color, masking, sensitive variable을 확인한다.

HCP Terraform을 사용한다면 workspace variable에 provider credential을 넣거나 dynamic provider credentials를 구성하는 방식을 검토한다. 직접 CI runner에서 실행한다면 runner가 어떤 cloud role을 assume하는지, branch와 environment 조건이 trust policy에 반영되는지까지 봐야 한다.

팀 협업 규칙은 코드보다 먼저 합의한다

Terraform CI/CD가 안정적으로 굴러가려면 도구 설정만으로 부족하다. 팀 규칙이 같이 있어야 한다.

운영 저장소에는 최소한 아래 규칙을 문서화하는 편이 좋다.

규칙이유
production apply는 main branch와 승인된 workflow에서만 수행로컬 apply와 우회 변경을 줄임
.terraform.lock.hcl은 커밋CI, 로컬, 원격 실행의 provider 버전 차이를 줄임
state 단위별 owner 지정plan 리뷰 책임과 장애 대응 경로가 명확해짐
module 변경은 영향 받는 root module plan까지 확인공용 모듈 변경의 blast radius를 드러냄
긴급 변경도 사후 PR과 state 확인을 남김감사 로그와 drift 추적을 보존

특히 "누가 apply 버튼을 누를 수 있는가"보다 "누가 plan을 해석할 책임이 있는가"가 중요하다. Terraform plan은 단순 diff가 아니라 리소스 교체, 삭제, 권한 변경, 네트워크 노출 변경을 포함한다. 리뷰어가 -/+ replacement, forces replacement, IAM policy diff, security group ingress, database deletion 같은 신호를 읽을 수 있어야 한다.

drift와 out-of-band 변경을 처리하는 방식

CI/CD가 있어도 클라우드 콘솔에서 직접 바꾸는 일이 완전히 사라지지는 않는다. 장애 대응 중 수동으로 security group을 열거나, autoscaler와 외부 컨트롤러가 값을 바꿀 수도 있다. 이때 Terraform은 다음 apply에서 실제 인프라와 state를 다시 비교한다.

팀은 drift를 세 종류로 나눠 대응해야 한다.

  1. 정상 drift: autoscaling desired capacity처럼 Terraform이 일부러 무시해야 하는 값이다. 필요한 경우 ignore_changes를 좁게 쓴다.
  2. 긴급 수동 변경: 장애 대응을 위해 임시로 바꾼 값이다. 사후에 코드로 반영하거나 되돌리고 기록한다.
  3. 무단 변경: 코드 리뷰 없이 권한, 네트워크, 데이터 리소스가 바뀐 경우다. 알림과 권한 회수가 필요하다.

정기 drift detection job을 둘 수도 있지만, 무작정 자동 apply로 되돌리는 것은 위험하다. 먼저 plan을 만들고, 어떤 변경이 왜 생겼는지 사람이 확인한 뒤 코드 반영 또는 되돌림을 결정하는 쪽이 안전하다.

운영 체크리스트

Terraform CI/CD를 처음 붙일 때는 거창한 플랫폼보다 아래 체크리스트를 먼저 만족시키는 것이 낫다.

  • remote state와 locking이 켜져 있는가?
  • CI에서 terraform init/plan/apply-input=false를 쓰는가?
  • PR에서 fmt, validate, plan이 자동으로 실행되는가?
  • production apply는 merge 후 승인된 경로에서만 가능한가?
  • plan artifact와 로그에 민감 정보가 과도하게 남지 않는가?
  • Terraform CLI와 provider 버전이 고정되어 있는가?
  • 환경별 state, credential, approval boundary가 분리되어 있는가?
  • module 변경 시 영향 받는 root module plan을 볼 수 있는가?
  • 로컬 emergency apply를 했을 때 사후 기록과 state 확인 절차가 있는가?

결국 Terraform CI/CD의 목적은 "사람을 빼고 자동으로 인프라를 바꾸는 것"이 아니다. 팀이 더 자주, 더 작게, 더 안전하게 인프라를 바꿀 수 있도록 변경 경로를 하나로 모으고, plan을 리뷰 가능한 계약으로 만드는 것이다.

References