Skip to content
ioob.dev
Go back

Terraform 13편 — CI/CD 통합

· 9분 읽기
Terraform 시리즈 (13/15)
  1. Terraform 1편 — Terraform이란
  2. Terraform 2편 — 설치와 첫 배포
  3. Terraform 3편 — HCL 문법
  4. Terraform 4편 — 변수와 출력
  5. Terraform 5편 — 프로바이더
  6. Terraform 6편 — 리소스와 의존성
  7. Terraform 7편 — 데이터 소스와 Import
  8. Terraform 8편 — State 관리
  9. Terraform 9편 — 모듈
  10. Terraform 10편 — 반복과 조건
  11. Terraform 11편 — 워크스페이스와 환경 분리
  12. Terraform 12편 — Kubernetes와 Helm 프로바이더
  13. Terraform 13편 — CI/CD 통합
  14. Terraform 14편 — 테스트와 정책
  15. Terraform 15편 — 실전 패턴과 함정
Table of contents

Table of contents

왜 CI/CD가 필요한가

Terraform을 혼자 노트북에서 돌리면 빠르고 편하다. 하지만 팀이 되는 순간부터 문제가 쌓인다.

CI/CD 파이프라인은 이 모든 문제를 한꺼번에 정리한다. 모든 변경은 PR을 통과하고, plan 결과가 리뷰어에게 보이고, 승인된 뒤에만 apply된다. credential은 CI 서버에만 있고 개인 노트북에는 없다.

flowchart LR
    Dev["개발자"] -->|"PR 생성"| PR["Pull Request"]
    PR -->|"자동 실행"| Plan["terraform plan"]
    Plan -->|"결과 코멘트"| Review["코드 리뷰"]
    Review -->|"승인 후 머지"| Merge["main 브랜치"]
    Merge -->|"자동 또는 수동 트리거"| Apply["terraform apply"]
    Apply --> Infra["클라우드 변경"]

이 편에서는 대표적인 두 가지 접근을 살펴본다. GitHub ActionsAtlantis다.

GitHub Actions 기본 파이프라인

가장 흔하게 쓰이는 방식이다. GitHub 레포에 워크플로우 YAML을 두면 PR과 push에 반응해서 Terraform이 자동으로 돈다.

기본 디렉토리 구조부터 보자.

infra/
├── .github/
│   └── workflows/
│       └── terraform.yml
└── envs/
    ├── dev/
    └── prod/

간단한 workflow 예시다.

# .github/workflows/terraform.yml
name: Terraform

on:
  pull_request:
    paths:
      - 'envs/**'
      - 'modules/**'
  push:
    branches: [main]
    paths:
      - 'envs/**'
      - 'modules/**'

permissions:
  contents: read
  pull-requests: write
  id-token: write   # OIDC용

jobs:
  plan:
    name: Plan (${{ matrix.env }})
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'

    strategy:
      fail-fast: false
      matrix:
        env: [dev, prod]

    defaults:
      run:
        working-directory: envs/${{ matrix.env }}

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/github-actions-tf-${{ matrix.env }}
          aws-region: ap-northeast-2

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.8.0

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

      - name: Init
        run: terraform init

      - name: Validate
        run: terraform validate

      - name: Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        continue-on-error: true

      - name: Comment Plan on PR
        uses: actions/github-script@v7
        env:
          PLAN: ${{ steps.plan.outputs.stdout }}
        with:
          script: |
            const output = `#### Terraform Plan: \`${{ matrix.env }}\` 📖
            \`\`\`
            ${process.env.PLAN}
            \`\`\`
            `;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            });

      - name: Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1

  apply:
    name: Apply (${{ matrix.env }})
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: [plan]

    strategy:
      matrix:
        env: [dev, prod]

    environment:
      name: ${{ matrix.env }}   # prod는 수동 승인 게이트 적용

    defaults:
      run:
        working-directory: envs/${{ matrix.env }}

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/github-actions-tf-${{ matrix.env }}
          aws-region: ap-northeast-2

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.8.0

      - name: Init
        run: terraform init

      - name: Apply
        run: terraform apply -auto-approve

흐름을 정리하면 이렇다. PR을 열면 모든 환경에서 plan이 돌고 결과가 PR에 코멘트로 달린다. 머지되면 main에서 apply가 실행된다. prod는 GitHub Environment의 승인 게이트를 거쳐야 한다.

sequenceDiagram
    participant Dev as 개발자
    participant GH as GitHub
    participant CI as GitHub Actions
    participant AWS as AWS

    Dev->>GH: PR 생성
    GH->>CI: pull_request 이벤트
    CI->>AWS: OIDC 인증
    CI->>AWS: terraform plan
    AWS-->>CI: plan 결과
    CI->>GH: PR에 plan 코멘트
    Dev->>GH: 리뷰 & 승인, 머지
    GH->>CI: push 이벤트
    CI->>CI: dev apply 자동 실행
    CI->>GH: prod environment 승인 대기
    Dev->>GH: 수동 승인
    GH->>CI: apply 재개
    CI->>AWS: terraform apply

OIDC로 credential 제거하기

위 예시에서 aws-actions/configure-aws-credentials가 access key 없이 OIDC로 AWS에 인증한다. 이게 요즘의 표준 방식이다.

GitHub Actions가 IAM Role을 assume하는 구조다. AWS에 GitHub OIDC provider를 한 번 등록하고, 그 provider를 신뢰하는 IAM Role을 만든다. 해당 Role의 trust policy에서 특정 레포의 특정 브랜치만 허용한다.

# GitHub OIDC provider
resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = ["sts.amazonaws.com"]
  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1",
    "1c58a3a8518e8759bf075b76b750d4f2df264fcd",
  ]
}

# IAM Role trust policy
data "aws_iam_policy_document" "github_actions_assume" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:my-org/infra-repo:ref:refs/heads/main"]
    }
  }
}

resource "aws_iam_role" "github_actions_tf" {
  name               = "github-actions-tf-prod"
  assume_role_policy = data.aws_iam_policy_document.github_actions_assume.json
}

이렇게 해두면 GitHub Actions에 AWS access key를 저장할 필요가 없다. 키 회전 걱정도 없고, 유출 리스크도 낮다.

PR 코멘트로 plan 보여주기

위 워크플로우의 Comment Plan on PR 스텝이 이 역할을 한다. 리뷰어는 PR을 열면 바로 “이 변경이 실제로 뭘 바꾸는지” 확인할 수 있다.

#### Terraform Plan: `prod` 📖

Terraform will perform the following actions:

aws_security_group_rule.allow_https will be created

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

이게 있으면 코드 리뷰의 품질이 확 올라간다. “이 PR이 실제로 뭘 만드는 거지?”라는 질문이 사라진다. 특히 destroy가 생기는 경우, 리뷰어가 즉시 경계할 수 있다.

plan 출력이 너무 길면 GitHub 코멘트 크기 제한(65536자)에 걸린다. 긴 경우 요약만 보여주거나 <details> 태그로 접는 방식이 흔하다.

const planSummary = `${process.env.PLAN}`.slice(0, 50000);
const output = `#### Plan: \`${{ matrix.env }}\`
<details><summary>상세 보기</summary>

\`\`\`diff
${planSummary}
\`\`\`
</details>`;

시크릿 관리

Terraform 코드나 state에 민감 정보가 들어가면 안 된다. CI에서 다뤄야 할 시크릿은 주로 두 가지다.

1) 클라우드 credential

OIDC를 쓰면 제거할 수 있다(위 참조). 어쩔 수 없이 access key를 써야 한다면 GitHub Secrets에 저장하고, 환경별 분리 접근을 건다.

2) Terraform 변수 중 민감한 것

DB 비밀번호, 외부 API 키 등이다. 이걸 CI에서 어떻게 전달할까.

- name: Apply
  env:
    TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
    TF_VAR_slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
  run: terraform apply -auto-approve

TF_VAR_<변수명> 환경변수는 Terraform의 입력 변수로 자동 주입된다. .tfvars 파일에 평문으로 쓰지 않아도 된다.

다만 이렇게 넣으면 state 파일에는 평문으로 저장된다. state를 보호하는 건 백엔드 쪽 암호화(S3 server-side encryption)로 처리한다.

더 나아가려면 시크릿을 Terraform 바깥에 두는 방식이 있다. AWS Secrets Manager나 HashiCorp Vault에 저장하고, Terraform은 참조만 한다.

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/rds/password"
}

resource "aws_db_instance" "db" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
}

여전히 state에는 기록되지만, 회전이 필요할 때 Secrets Manager에서 바꾸고 apply만 다시 치면 된다.

Atlantis — PR 기반 Terraform 자동화

GitHub Actions로도 충분하지만, 더 정교한 Terraform 전용 자동화 도구가 있다. Atlantis다.

Atlantis는 PR 코멘트로 Terraform을 조작하는 도구다. PR에 atlantis plan이라고 코멘트를 남기면 Atlantis가 plan을 실행하고 결과를 PR에 자동으로 올려준다. atlantis apply로 apply를 실행할 수도 있다.

flowchart LR
    PR["PR 생성"] --> Auto["Atlantis 자동 plan"]
    Auto --> Comment["PR에 plan 코멘트"]
    Comment --> Human["리뷰어 확인"]
    Human -->|"atlantis apply\n코멘트"| Apply["Atlantis가 apply 실행"]
    Apply --> Lock["PR 잠금\n(머지 자동화)"]

Atlantis의 장점은 이렇다.

기본 설정 파일은 레포 루트의 atlantis.yaml이다.

version: 3
automerge: false
projects:
  - name: dev
    dir: envs/dev
    autoplan:
      when_modified:
        - "*.tf"
        - "../../modules/**/*.tf"
      enabled: true
    apply_requirements:
      - approved
      - mergeable

  - name: prod
    dir: envs/prod
    autoplan:
      when_modified:
        - "*.tf"
        - "../../modules/**/*.tf"
      enabled: true
    apply_requirements:
      - approved
      - mergeable
    workflow: prod-workflow

workflows:
  prod-workflow:
    plan:
      steps:
        - init
        - plan
    apply:
      steps:
        - run: echo "프로덕션 apply — 각별한 주의"
        - apply

apply_requirementsapproved를 걸면 PR이 승인돼야만 apply가 가능하다. mergeable은 머지 가능 상태(충돌 없고 CI 통과)를 요구한다.

Atlantis는 Kubernetes에 배포하거나, ECS, 혹은 단일 EC2에 Docker로도 올릴 수 있다. 웹훅을 받을 수 있는 공인 엔드포인트가 필요하다.

GitHub Actions vs Atlantis

둘을 언제 써야 할까.

항목GitHub ActionsAtlantis
설치/운영 부담없음 (GitHub가 제공)서버 직접 운영
Terraform 전용 기능직접 구현내장
state 잠금 관리직접 처리자동
멀티 디렉토리 조율수동 설정자동 감지
러닝커브낮음중간
비용GitHub Actions 사용료서버 운영비

GitHub Actions가 맞는 경우

Atlantis가 맞는 경우

작게 시작한다면 GitHub Actions로 충분하고, 팀이 커지고 동시 작업이 잦아지면 Atlantis 도입을 검토하는 순서가 자연스럽다.

몇 가지 실전 팁

1) plan과 apply를 같은 workflow run에 묶는 아이디어는 접는다

“머지되면 자동으로 apply”는 쉽다. 하지만 “plan과 apply 사이에 사람이 개입해서 승인하는 흐름”은 GitHub Actions에서 바로 되지 않는다. Environment의 승인 게이트로 우회하거나, Atlantis를 쓴다.

2) plan을 파일로 저장하고 apply 때 그 파일을 쓴다

terraform plan -out=tfplan
terraform apply tfplan

-out으로 plan을 파일로 저장하면, apply 시점에 state가 plan 당시와 달라졌다면 실패한다. “내가 리뷰한 plan과 실제 apply가 다를 가능성”을 원천 차단하는 장치다. 단, tfplan 파일은 민감한 정보를 포함할 수 있어 아티팩트로 관리할 때 주의한다.

3) CI 실행 시간에 timeout을 건다

jobs:
  apply:
    timeout-minutes: 30

뭔가 잘못돼서 apply가 무한 대기에 빠지는 걸 방지한다.

4) drift 감지 잡을 따로 둔다

정기적으로(하루 한 번) 모든 환경에서 terraform plan -refresh-only를 돌려서 drift를 Slack에 알리는 워크플로우를 두면 좋다.

on:
  schedule:
    - cron: '0 9 * * *'   # 매일 오전 9시

jobs:
  drift-check:
    # ... plan -refresh-only -detailed-exitcode

콘솔에서 누군가 몰래 설정을 바꿔놓은 걸 빠르게 잡아낼 수 있다.

5) 모듈이 있는 레포는 버전 태깅을 자동화한다

PR이 머지되면 자동으로 SemVer 태그를 붙이는 워크플로우를 두면, 모듈 사용자가 특정 버전으로 안정적으로 고정할 수 있다. release-pleasesemantic-release 같은 도구가 도움된다.


CI/CD는 Terraform 운영의 신뢰성을 지탱하는 기둥이다. PR → plan → 리뷰 → 머지 → apply의 사이클이 자리잡으면, 누가 언제 뭘 바꿨는지 투명해지고 사고 가능성이 크게 줄어든다. 크든 작든 팀이라면 이 파이프라인을 반드시 구축하고 넘어가야 한다.

다음 편에서는 테스트와 정책 검증을 다룬다. Terratest, Checkov, tfsec, OPA로 Terraform 코드의 품질과 보안을 어떻게 보증하는지 살펴본다.

14편: 테스트와 정책


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Terraform 12편 — Kubernetes와 Helm 프로바이더
Next Post
Terraform 14편 — 테스트와 정책