Network/ACI

[ACI-IaC] Terraform vs Ansible 실전 비교

넷곰 2026. 4. 7. 15:11
[ACI-IaC] Terraform vs Ansible 실전 비교
📌 [ACI-IaC] 시리즈 구성

[ACI-IaC] Terraform vs Ansible 개요
[ACI-IaC] Terraform으로 Basic Tenant 프로비저닝하기
[ACI-IaC] Terraform으로 3계층 앱 구성하기
[ACI-IaC] Ansible로 Basic Tenant 프로비저닝하기
[ACI-IaC] Terraform vs Ansible 실전 비교 ← 현재 글

GitHub: github.com/MinYongUm/aci-as-code

개요

시리즈 내내 동일한 ACI 구성을 Terraform과 Ansible로 각각 구현했습니다. 이번 글에서는 실제 구현 경험을 바탕으로 두 도구를 항목별로 비교하고, 마지막으로 GitHub Actions CI로 두 도구의 검증 자동화를 어떻게 구성했는지 소개합니다.

WHY — "Terraform이 좋아요 vs Ansible이 좋아요" 가 아닌 이유

이 비교를 시작하기 전에 먼저 짚고 넘어가야 할 것이 있습니다. Terraform과 Ansible은 경쟁 관계가 아닙니다.

실무에서는 두 도구를 함께 씁니다. Terraform으로 초기 인프라를 구성하고(Day 0), Ansible로 운영 중 변경을 자동화합니다(Day 1-2). 어느 도구가 더 낫냐는 질문보다, 어떤 상황에 어느 도구가 더 적합한가를 판단하는 것이 실용적입니다.

이 시리즈에서 같은 구성을 두 도구로 모두 구현한 이유도 여기 있습니다. 코드를 직접 짜보지 않으면 차이가 느껴지지 않습니다.

HOW — 항목별 실전 비교

1. 상태 관리(State)

두 도구의 가장 근본적인 차이입니다.

항목TerraformAnsible
상태 추적 terraform.tfstate 파일로 현재 인프라 상태를 로컬에 저장 State 없음. 실행 시마다 APIC에 직접 현재 상태 조회
드리프트 감지 terraform plan으로 코드와 실제 상태 차이를 자동 감지 직접 APIC GUI 또는 별도 확인 필요
State 관리 부담 팀 환경에서 State 파일 공유 필요 (S3 Remote Backend 권장) State 없으므로 공유 부담 없음
드리프트(Drift)란? 코드로 정의한 상태와 실제 인프라 상태가 달라지는 현상입니다. 누군가 APIC GUI에서 직접 설정을 바꾸면 Terraform 코드와 실제 상태가 어긋납니다. terraform plan을 실행하면 이 차이를 즉시 확인할 수 있습니다.

2. 실행 순서 제어

# Terraform — 의존성 그래프로 자동 결정
resource "aci_vrf" "this" {
  tenant_dn = aci_tenant.this.id  # 이 참조를 보고 Tenant 먼저 생성
}
# → 작성자가 순서를 신경 쓰지 않아도 됨

# Ansible — site.yml에 직접 명시
roles:
  - role: aci_tenant      # 1번
  - role: aci_networking  # 2번 (반드시 tenant 다음)
  - role: aci_policy      # 3번
  - role: aci_epg         # 4번 (반드시 나머지 다음)
# → 작성자가 올바른 순서를 직접 알고 있어야 함

3. 반복 처리 (다중 오브젝트 생성)

BD 3개, Filter Entry 여러 개처럼 같은 타입의 오브젝트를 여러 개 만들 때 두 도구의 접근 방식이 다릅니다.

# Terraform — for_each + map 키
resource "aci_bridge_domain" "this" {
  for_each  = { for bd in var.bridge_domains : bd.name => bd }
  parent_dn = var.tenant_dn
  name      = each.key
}

# 중첩 구조는 flatten()으로 펼친 뒤 for_each
locals {
  filter_entries = flatten([
    for filter_name, filter in var.filters : [
      for entry in filter.entries : {
        filter_name = filter_name
        entry_name  = entry.name
        ...
      }
    ]
  ])
}
# Ansible — loop + subelements 필터
- name: Bridge Domain 생성
  cisco.aci.aci_bd:
    bd: "{{ item.name }}"
  loop: "{{ bridge_domains }}"

# 중첩 구조는 subelements 필터
- name: Filter Entry 생성
  cisco.aci.aci_filter_entry:
    filter:       "{{ item.0.name }}"
    filter_entry: "{{ item.1.name }}"
  loop: "{{ filters | subelements('entries') }}"
Terraform의 flatten() + for_each와 Ansible의 subelements는 같은 문제를 해결합니다. "중첩된 리스트를 펼쳐서 각 조합을 하나의 리소스로 만든다"는 것입니다. 표현 방식은 다르지만 결과는 동일합니다.

4. 삭제(Destroy) 동작

항목TerraformAnsible
삭제 명령 terraform destroy -e "aci_state=absent"
삭제 순서 State 기반 역순 자동 처리
(EPG → BD → VRF → Tenant)
Tenant 삭제 → ACI cascade로 하위 전체 삭제
삭제 가시성 리소스별 삭제 로그 출력
(13개, 24개 각각 확인 가능)
Tenant 1건 삭제 로그만 출력
(하위 삭제는 ACI 내부 처리)
부분 삭제 terraform destroy -target으로 특정 리소스만 삭제 가능 특정 Role만 absent 처리하면 부분 삭제 가능

5. 크리덴셜 보호

항목TerraformAnsible
방식 sensitive = true + .gitignore ansible-vault 암호화
Git 커밋 tfvars 파일 자체를 커밋하지 않음 암호화된 vault.yml을 커밋 가능
파일 유출 시 평문 노출 (파일 자체가 암호화되지 않음) AES256 암호화 → 복호화 비밀번호 없으면 안전
CI/CD 연동 GitHub Secrets → TF_VAR_* 환경변수 주입 GitHub Secrets → vault 비밀번호 환경변수 주입

6. DN(Distinguished Name) 처리 방식

ACI의 모든 오브젝트는 DN으로 식별됩니다. 두 도구가 DN을 다루는 방식이 완전히 다릅니다.

# Terraform — DN 전체를 직접 사용
resource "aci_vrf" "this" {
  tenant_dn = "uni/tn-demo-tenant"  # DN 전체를 직접 입력
  name      = "prod-vrf"
}

# 또는 다른 리소스의 .id(DN)를 참조
resource "aci_vrf" "this" {
  tenant_dn = aci_tenant.this.id    # aci_tenant 리소스의 DN을 참조
}
# Ansible — 오브젝트 이름만 입력, DN은 모듈이 자동 처리
- name: VRF 생성
  cisco.aci.aci_vrf:
    tenant: "demo-tenant"  # Tenant 이름만 입력
    vrf:    "prod-vrf"
    # 모듈 내부에서 uni/tn-demo-tenant/ctx-prod-vrf DN을 자동 구성
Ansible의 cisco.aci 모듈이 DN을 자동으로 구성해주기 때문에 코드가 더 직관적입니다. 반면 Terraform은 DN을 직접 다루므로 ACI 오브젝트 구조를 더 명확하게 이해해야 합니다. Terraform 코드를 읽으면 DN 경로만 봐도 오브젝트 계층 구조가 보입니다.

7. Subject-Filter 연결 방식 차이

같은 동작인데 구현 방식이 가장 크게 다른 부분 중 하나입니다.

# Terraform — 속성(attribute)으로 한 번에 연결
resource "aci_contract_subject" "this" {
  contract_dn = aci_contract.this.id
  name        = var.subject_name
  relation_vz_rs_subj_filt_att = [for f in aci_filter.this : f.id]
  # Subject 생성과 Filter 연결을 하나의 리소스로 처리
}

# Ansible — 별도 모듈로 분리
- name: Contract Subject 생성
  cisco.aci.aci_contract_subject:
    contract: "allow-http"
    subject:  "http-subj"

- name: Subject에 Filter 연결         ← 별도 Task 필요
  cisco.aci.aci_contract_subject_to_filter:
    contract: "allow-http"
    subject:  "http-subj"
    filter:   "filter-http"

최종 비교 요약

항목TerraformAnsible
적합한 단계Day 0 (초기 프로비저닝)Day 1-2 (운영/변경 자동화)
언어HCL (선언형)YAML (절차형)
상태 관리tfstate로 추적없음 (멱등성으로 보완)
실행 순서의존성 그래프 자동 결정작성자가 직접 명시
반복 처리for_each + flatten()loop + subelements
드리프트 감지terraform plan으로 자동직접 확인 필요
삭제 가시성리소스별 로그 (높음)cascade 삭제 (낮음)
크리덴셜gitignore + sensitiveansible-vault 암호화
DN 처리DN 전체 직접 사용이름만 입력, 모듈이 처리
학습 곡선HCL + State 개념 필요YAML 친숙 시 진입 쉬움

실습 — GitHub Actions CI 구성

코드를 작성하고 GitHub에 올렸다면, PR(Pull Request)마다 자동으로 검증이 실행되도록 CI를 구성했습니다. 두 도구에 대해 각각 워크플로우 파일을 만들었습니다.

terraform-ci.yml — Terraform 검증 자동화

# .github/workflows/terraform-ci.yml
name: Terraform CI

on:
  pull_request:
    paths:
      - 'terraform/**'
      - 'scenarios/**'

permissions:
  pull-requests: write    # PR 코멘트 게시 권한

jobs:
  fmt-check:
    name: Format Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - name: terraform fmt
        run: terraform fmt -recursive -check -diff

  validate-and-plan:
    name: Validate & Plan
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false          # 하나 실패해도 나머지 Scenario 계속 실행
      matrix:
        scenario:
          - 01_basic_tenant
          - 02_three_tier_app

    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3

      - name: terraform init
        working-directory: scenarios/${{ matrix.scenario }}
        run: terraform init

      - name: terraform validate
        working-directory: scenarios/${{ matrix.scenario }}
        run: terraform validate

      - name: terraform plan
        working-directory: scenarios/${{ matrix.scenario }}
        env:
          # GitHub Secrets → TF_VAR_* 환경변수로 주입 (terraform.tfvars 없이 동작)
          TF_VAR_aci_url:      ${{ secrets.ACI_URL }}
          TF_VAR_aci_username: ${{ secrets.ACI_USERNAME }}
          TF_VAR_aci_password: ${{ secrets.ACI_PASSWORD }}
          TF_VAR_aci_insecure: "true"
        run: terraform plan -no-color

      - name: PR 코멘트에 plan 결과 게시
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '### Terraform Plan — ${{ matrix.scenario }}\n\`\`\`\n' + plan + '\n\`\`\`'
            })

ansible-ci.yml — Ansible 검증 자동화

# .github/workflows/ansible-ci.yml
name: Ansible CI

on:
  pull_request:
    paths:
      - 'ansible/**'

jobs:
  yaml-lint:
    name: YAML Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: yamllint
        run: |
          pip install yamllint
          yamllint ansible/

  ansible-lint:
    name: Ansible Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: cisco.aci 컬렉션 설치
        run: ansible-galaxy collection install cisco.aci==2.8.0
      - name: ansible-lint
        run: |
          pip install ansible-lint
          ansible-lint ansible/
      - name: syntax-check
        run: |
          ansible-playbook \
            ansible/scenarios/01_basic_tenant/site.yml \
            --syntax-check \
            -i ansible/scenarios/01_basic_tenant/inventory.yml
Terraform CI의 핵심 설계 포인트 두 가지:
1. permissions: pull-requests: write — PR 코멘트 게시를 위해 반드시 필요합니다. 없으면 403 오류가 발생합니다.
2. TF_VAR_* 환경변수 방식 — terraform.tfvars는 .gitignore로 커밋되지 않으므로, CI 환경에서는 GitHub Secrets를 환경변수로 주입해서 plan을 실행합니다.

PR 코멘트 자동 게시 결과

# PR을 열면 자동으로 아래 내용이 코멘트로 달림

### Terraform Plan — 01_basic_tenant

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

### Terraform Plan — 02_three_tier_app

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

삽질 기록

삽질 1 — PR 코멘트 403 오류

상황 terraform plan 결과를 PR 코멘트로 게시하는 step을 추가했다.
실제 워크플로우 실행 시 "Resource not accessible by integration" 403 오류 발생. plan 자체는 성공했는데 코멘트 게시만 실패했다.
원인 GitHub Actions의 기본 GITHUB_TOKEN은 PR에 코멘트를 쓰는 권한이 없다. permissions: pull-requests: write를 워크플로우에 명시해야 한다.
교훈 GitHub Actions에서 PR 코멘트, 이슈 수정, 릴리즈 생성 등 쓰기 작업은 반드시 permissions 블록에 해당 권한을 명시해야 한다. 읽기는 기본 허용이지만 쓰기는 기본 차단이다.

삽질 2 — TF_VAR_* 변수 주입 누락

상황 CI 환경에서 terraform plan을 실행했더니 변수 입력 프롬프트가 뜨면서 파이프라인이 멈췄다.
실제 var.aci_password 값을 입력하라는 프롬프트가 나오고 타임아웃으로 실패했다.
원인 terraform.tfvars는 .gitignore로 커밋되지 않으므로 CI 환경에는 없다. GitHub Secrets를 TF_VAR_aci_password처럼 TF_VAR_ 접두사가 붙은 환경변수로 주입해야 Terraform이 자동으로 인식한다.
교훈 CI에서 Terraform을 실행할 때는 tfvars 대신 환경변수 방식이 표준이다. Terraform은 TF_VAR_{변수명} 형태의 환경변수를 자동으로 변수로 인식한다.

삽질 3 — vault.yml이 ansible-lint 대상에 포함됨

상황 ansible-lint를 ansible/ 전체 경로로 실행했다.
실제 암호화된 vault.yml을 YAML 파일로 인식하고 파싱하려다 오류가 발생했다.
원인 ansible-vault 암호화 파일은 $ANSIBLE_VAULT;1.1;AES256으로 시작하는 텍스트 파일이다. lint 도구가 이를 유효하지 않은 YAML로 인식한다.
교훈 .ansible-lint.yamllint 설정 파일에 vault.yml을 exclude 목록에 추가해야 한다. 처음 CI 구성 시 반드시 확인하는 항목이다.

면접 포인트

Q. ACI 자동화 도구를 선택할 때 어떤 기준으로 결정하나요?
세 가지 기준으로 판단합니다.

첫째, 작업의 성격입니다. 처음부터 새로 구성을 만드는 초기 프로비저닝이라면 Terraform, 기존 구성에 부분적인 변경을 반복적으로 가하는 운영 작업이라면 Ansible이 적합합니다.

둘째, 드리프트 감지 필요성입니다. "코드와 실제 상태가 다를 수 있다"는 환경이라면 terraform plan으로 자동 감지가 가능한 Terraform이 유리합니다. GUI 클릭이 빈번한 환경일수록 드리프트 감지의 가치가 높아집니다.

셋째, 팀의 기존 스택입니다. 이미 Ansible을 서버 자동화에 쓰고 있는 팀이라면 cisco.aci 컬렉션을 추가하는 것이 진입 장벽이 낮습니다. 반면 클라우드 인프라를 Terraform으로 관리하는 팀이라면 ACI도 Terraform으로 통일하는 것이 일관성 면에서 유리합니다.
Q. Terraform State를 팀 환경에서 안전하게 관리하는 방법은?
로컬 State 파일은 팀 환경에서 두 가지 문제를 일으킵니다. 팀원 간 State 공유가 안 되고, 동시 실행 시 충돌이 발생합니다.

표준적인 해결책은 Remote Backend를 사용하는 것입니다. AWS S3 + DynamoDB 조합이 가장 많이 쓰입니다. S3에 State 파일을 저장하고, DynamoDB로 동시 실행을 방지하는 Lock을 관리합니다.

이 프로젝트는 개인 포트폴리오용이라 로컬 State를 .gitignore로 처리했지만, 실제 팀 환경에서는 반드시 Remote Backend를 구성해야 합니다. Terraform Cloud(HCP Terraform)를 쓰면 별도 인프라 없이 Remote Backend를 바로 사용할 수 있습니다.
Q. 네트워크 자동화에서 IaC를 도입할 때 가장 큰 장벽은 무엇인가요?
기술적인 장벽보다 문화적인 장벽이 더 큽니다.

기술적으로는 Terraform이나 Ansible 자체를 배우는 것보다, 네트워크 오브젝트를 코드로 표현하는 사고 전환이 어렵습니다. ACI를 예로 들면 "APIC GUI에서 클릭하던 것을 HCL로 쓰면 된다"는 것을 이해하는 게 첫 번째 허들입니다.

문화적으로는 "검증된 방식(GUI 클릭)을 왜 바꿔야 하나"는 저항이 있습니다. 이를 극복하는 가장 효과적인 방법은 작은 성공 경험을 만드는 것입니다. 반복적으로 하던 작업 하나를 Playbook으로 만들어서 시간을 줄여보는 것, 그리고 Git으로 변경 이력이 자동으로 쌓이는 것을 팀에 보여주는 것이 시작점이 될 수 있습니다.