해당 스터디는 90DaysOfDevOps
https://github.com/MichaelCade/90DaysOfDevOps
를 기반으로 진행한 내용입니다.
Day 78 - Scaling Terraform Deployments with GitHub Actions: Essential Configurations
성공적인 인프라 관리(IaC)의 시작은 코드를 담는 그릇인 Repository의 정책을 올바르게 수립하는 것에서부터 출발한다.
이는 단순한 권장 사항이 아니라, 사고를 예방하고 협업의 품질을 높이는 강제적 제어 장치이다.
운영 환경의 안정성을 위해 main 브랜치에 대한 직접적인 커밋을 차단하는 브랜치 보호 규칙 설정은 필수적이다.
사고 방지: 개발자가 실수로 운영 코드를 변경하거나 삭제하는 것을 시스템 레벨에서 차단한다.
제어된 Workflows: 모든 변경 사항은 검증된 절차를 거쳐야만 병합될 수 있도록 강제하여, 인프라의 무결성을 유지한다.
코드 병합 전에 반드시 Pull Request를 생성하도록 요구하는 것은 DevOps 문화의 핵심이다.
코드 리뷰의 제도화: PR은 팀원들이 변경 사항을 검토하고 피드백을 주고받는 공식적인 공간이 된다.
자동화된 검증:
PR이 생성되는 즉시 CI 파이프라인을 트리거하여, 코드가 병합되기 전에 문법 오류나 잠재적 문제를 사전에 탐지할 수 있다.
이는 배포 후 발생할 수 있는 회귀 오류를 최소화하는 가장 확실한 방법이다.
보안과 유연성을 동시에 확보하기 위해 GitHub의 Environment Secrets 기능을 적극 활용해야 한다.
민감 정보 격리:
Client ID, Secret, Subscription ID와 같은 민감한 정보는 절대 평문으로 코드에 포함되어서는 안 된다.
이를 GitHub Secrets에 암호화하여 저장하고 파이프라인 실행 시에만 주입받아 사용한다.
환경별 분리 전략:
개발과 운영 환경은 서로 다른 자격 증명(Credential)을 사용해야 한다.
GitHub Environments 기능을 통해 동일한 변수명을 사용하더라도, 배포되는 환경 컨텍스트에 따라 자동으로 다른 비밀 키 값을 매핑함으로써 보안 사고를 예방한다.
테라폼 코드베이스의 유지보수성과 확장성은 폴더 구조를 어떻게 설계하느냐에 달려 있다.
단일 파일 관리가 아닌, 기능과 역할에 따른 명확한 디렉터리 분리 전략이 필요하다.
프레젠테이션에서는 다음과 같은 계층적 구조를 제안한다.
.github/workflows: CI/CD 파이프라인 정의 파일(.yml)과 재사용 가능한 Template workflows가 위치
environments: 환경별 설정 값을 관리한다.
modules: 재사용 가능한 테라폼 모듈을 관리한다.
platform: 인프라를 구성하는 실제 리소스들을 기능 단위로 쪼개어 관리하는 디렉토리
.
├── .github
│ └── workflows
│ ├── deploy.yml # 메인 워크플로 (트리거 정의)
│ └── templates # 재사용 가능한 템플릿 (CI, Apply 로직 등)
├── environments
│ ├── dev
│ │ └── terraform.tfvars # 개발 환경 전용 변수 (IP 대역, 이름 등)
│ └── prod
│ └── terraform.tfvars # 운영 환경 전용 변수
├── modules # 재사용 가능한 테라폼 모듈 (예: DNS, VM)
└── platform # 실제 인프라 구성 요소 (기능 단위 분리)
├── core # 리소스 그룹 등 기초 자원
├── network # VNet, Subnet 등
└── logging # 모니터링 및 로깅 자원
거대한 하나의 main.tf 파일은 관리가 불가능하다. 인프라를 플랫폼 구성 요소 단위로 나누어야 한다.
구성 요소 예시:
Core: 리소스 그룹 등 가장 기초가 되는 자원
Network: VNet, Subnet 등 네트워킹 자원
Logging: Log Analytics Workspace 등 모니터링 자원
의존성 관리:
이러한 분리는 인프라 배포의 순서를 명확히 한다.
예를 들어, VM을 배포하기 전에 Network가 존재해야 하고, 네트워크 전에 리소스 그룹인 Core가 있어야 한다. 폴더를 분리함으로써 '왼쪽에서 오른쪽으로' 흐르는 배포 파이프라인을 설계할 수 있다.
단순한 스크립트 실행을 넘어, GitHub Actions의 고급 기능을 활용하여 중복을 줄이고 배포 속도를 높이는 전략이다.
DRY(Don't Repeat Yourself) 원칙을 CI/CD 파이프라인에도 적용해야 한다.
재사용 가능한 워크플로: terraform init, plan, apply와 같은 공통적인 작업 절차를 별도의 YAML 템플릿 파일로 정의한다.
효과:
메인 워크플로에서는 이 템플릿을 호출하고 필요한 Input만 전달하면 된다.
이를 통해 모든 프로젝트와 환경에서 일관된 배포 프로세스를 보장하고, 로직 수정 시 템플릿 파일 하나만 수정하면 되므로 유지보수가 용이해진다.
#.github/workflows/templates/terraform-template.yml
name: "Reusable Terraform Workflow"
on:
workflow_call: # 다른 워크플로에서 호출 가능하도록 설정
inputs:
environment: { required: true, type: string }
path: { required: true, type: string }
secrets:
AZURE_CREDENTIALS: { required: true }
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: hashicorp/setup-terraform@v2
# 1. 초기화 (공통 로직)
- run: terraform init
working-directory: ${{ inputs.path }}
# 2. 적용 (환경 변수 동적 할당)
- run: terraform apply -auto-approve -var-file=../../environments/${{ inputs.environment }}.tfvars
working-directory: ${{ inputs.path }}
대규모 배포 시 Matrix Strategy를 도입한다.
문제 상황: 개발/운영 2개의 환경에 Core, Network, Logging 3개의 구성 요소를 배포하려면 총 6번의 배포 스크립트를 작성해야 하는 비효율이 발생한다.
해결책: GitHub Actions의 Matrix 기능을 사용하여 환경 목록([dev, prod])과 구성 요소 목록([core, network, logging])을 정의한다.
동작 방식:
파이프라인이 실행되면 정의된 조합(2 x 3 = 6개)에 따라 6개의 Job이 자동으로 생성되어 병렬로 실행된다.
코드는 단 한 번만 작성하지만, 실행은 N번 이루어지므로 작성량이 획기적으로 줄어들고 배포 속도는 빨라진다.
구현 코드 예시:
jobs:
terraform-deploy:
strategy:
matrix:
environment: [dev, prod] # 배포할 환경 목록
component: [core, network, logging] # 배포할 플랫폼 구성 요소 목록
steps:
- name: Deploy ${{ matrix.component }} to ${{ matrix.environment }}
# working-directory를 동적으로 변경하여 해당 컴포넌트 폴더에서 실행
working-directory: platform/${{ matrix.component }}
# 해당 환경에 맞는 tfvars 파일을 동적으로 주입
run: |
terraform init
terraform apply -var-file="../../environments/${{ matrix.environment }}/terraform.tfvars"
실제 데모 시연을 통해 확인된 파이프라인의 구체적인 동작 방식과, 지속적인 개선을 위한 운영 도구 활용법이다.
PR이 생성되면 Terraform CI 작업이 실행된다.
자동 포맷팅:
개발자가 terraform fmt를 수행하지 않고 코드를 올렸을 경우(들여쓰기 오류 등), CI 파이프라인이 이를 감지하여 자동으로 포맷팅을 수정하고 해당 브랜치에 커밋한다.
사소한 스타일 문제로 리뷰 시간을 낭비하는 것을 방지한다.
피드백 루프 자동화:
terraform plan의 실행 결과가 GitHub Actions 로그에만 남는 것이 아니라, PR의 코멘트로 자동 등록된다.
리뷰어는 파이프라인 로그를 찾아 헤맬 필요 없이, PR 페이지에서 즉시 변경 예정 사항인Plan을 확인하고 승인 여부를 결정할 수 있다.
name: "Terraform CI"
on:
pull_request:
branches: [ main ]
permissions:
contents: write # 포맷팅 수정 후 커밋을 위해 쓰기 권한 필요
pull-requests: write # PR 코멘트 작성을 위해 쓰기 권한 필요
jobs:
terraform-ci:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
# 1. 자동 포맷팅 및 커밋 (Auto-Formatting)
- name: Terraform Format
id: fmt
run: terraform fmt -recursive
- name: Commit Formatting Changes
if: steps.fmt.outputs.exitcode != 0 # 변경사항이 있을 경우에만 실행
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "style: Apply terraform fmt changes"
# 2. Terraform Init & Validate
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
# 3. Plan 실행 및 결과 저장
- name: Terraform Plan
id: plan
run: terraform plan -no-color
continue-on-error: true # Plan 실패 시에도 코멘트는 남기도록 설정
# 4. PR에 코멘트로 Plan 결과 등록 (Feedback Loop)
- name: Update PR with Plan
uses: actions/github-script@v6
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`\n
${process.env.PLAN}
\`\`\`
</details>`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
PR이 승인되어 main 브랜치에 병합되면 실제 배포 파이프라인이 트리거 된다.
조건부 실행: if 문을 사용하여 main 브랜치일 경우에만 terraform apply가 실행되도록 제어한다.
매트릭스 기반 배포: 앞서 설계한 매트릭스 전략에 따라 개발 및 운영 환경의 각 구성 요소들이 의존성에 맞춰 배포된다. 데모에서는 Azure Tag가 업데이트되는 과정을 통해 실시간 반영을 확인했다.
name: "Terraform CD (Deploy)"
on:
push:
branches: [ main ]
jobs:
deploy:
name: Deploy ${{ matrix.component }} to ${{ matrix.environment }}
runs-on: ubuntu-latest
# Matrix 전략 정의: 환경(2개) * 컴포넌트(3개) = 총 6개의 병렬 Job 실행
strategy:
matrix:
environment: [dev, prod]
component: [core, network, logging]
fail-fast: false # 하나의 Job이 실패해도 나머지는 계속 진행
# 환경별 Secret 분리를 위해 GitHub Environment 기능 사용
environment: ${{ matrix.environment }}
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
# Azure 로그인 (OIDC 사용 시)
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# Matrix 변수를 활용한 동적 경로 설정 및 배포
- name: Terraform Init & Apply
# 실제 테라폼 코드가 있는 폴더로 이동 (예: platform/core)
working-directory: platform/${{ matrix.component }}
run: |
terraform init
# 환경별 tfvars 파일을 참조하여 Apply 실행
# (예: environments/dev/terraform.tfvars)
terraform apply -auto-approve \
-var-file="../../environments/${{ matrix.environment }}/terraform.tfvars"
인프라 코드는 한 번 작성하고 끝나는 것이 아니다. 지속적인 최신 상태 유지가 필요하다.
Renovate/Dependabot 도입: Terraform 모듈 버전, Provider 버전, GitHub Actions 템플릿 버전 등을 모니터링하는 도구를 설정한다.
종속성 대시보드: 업데이트가 필요한 항목을 대시보드 형태로 시각화해 주며, 새 버전이 나올 경우 자동으로 PR을 생성해 준다. 이를 통해 엔지니어는 수동으로 버전을 확인하는 수고를 덜고, 보안 취약점이 패치된 최신 버전을 손쉽게 유지할 수 있다.
대규모 테라폼 운영을 위해서는 환경과 플랫폼 구성 요소를 분리한 모듈식 저장소 구조와 Matrix Strategy를 결합하여 코드 중복 없이 다중 환경 병렬 배포를 구현해야 한다.
이 과정에서 브랜치 보호 규칙과 PR 기반의 자동화된 CI 검증을 통해 인프라의 무결성을 확보하고, GitHub Environment Secrets를 활용하여 보안성을 강화한다.
최종적으로 Renovate와 같은 종속성 관리 도구를 통합함으로써, 확장 가능하고 유지보수가 용이한 IaC 아키텍처를 완성할 수 있다.