[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)
두 도구의 가장 근본적인 차이입니다.
| 항목 | Terraform | Ansible |
|---|---|---|
| 상태 추적 | terraform.tfstate 파일로 현재 인프라 상태를 로컬에 저장 |
State 없음. 실행 시마다 APIC에 직접 현재 상태 조회 |
| 드리프트 감지 | terraform plan으로 코드와 실제 상태 차이를 자동 감지 |
직접 APIC GUI 또는 별도 확인 필요 |
| State 관리 부담 | 팀 환경에서 State 파일 공유 필요 (S3 Remote Backend 권장) | State 없으므로 공유 부담 없음 |
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') }}"
flatten() + for_each와 Ansible의 subelements는 같은 문제를 해결합니다.
"중첩된 리스트를 펼쳐서 각 조합을 하나의 리소스로 만든다"는 것입니다.
표현 방식은 다르지만 결과는 동일합니다.
4. 삭제(Destroy) 동작
| 항목 | Terraform | Ansible |
|---|---|---|
| 삭제 명령 | 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. 크리덴셜 보호
| 항목 | Terraform | Ansible |
|---|---|---|
| 방식 | 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을 자동 구성
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"
최종 비교 요약
| 항목 | Terraform | Ansible |
|---|---|---|
| 적합한 단계 | Day 0 (초기 프로비저닝) | Day 1-2 (운영/변경 자동화) |
| 언어 | HCL (선언형) | YAML (절차형) |
| 상태 관리 | tfstate로 추적 | 없음 (멱등성으로 보완) |
| 실행 순서 | 의존성 그래프 자동 결정 | 작성자가 직접 명시 |
| 반복 처리 | for_each + flatten() | loop + subelements |
| 드리프트 감지 | terraform plan으로 자동 | 직접 확인 필요 |
| 삭제 가시성 | 리소스별 로그 (높음) | cascade 삭제 (낮음) |
| 크리덴셜 | gitignore + sensitive | ansible-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
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 오류
GITHUB_TOKEN은 PR에 코멘트를 쓰는 권한이 없다. permissions: pull-requests: write를 워크플로우에 명시해야 한다.
삽질 2 — TF_VAR_* 변수 주입 누락
terraform plan을 실행했더니 변수 입력 프롬프트가 뜨면서 파이프라인이 멈췄다.
var.aci_password 값을 입력하라는 프롬프트가 나오고 타임아웃으로 실패했다.
terraform.tfvars는 .gitignore로 커밋되지 않으므로 CI 환경에는 없다. GitHub Secrets를 TF_VAR_aci_password처럼 TF_VAR_ 접두사가 붙은 환경변수로 주입해야 Terraform이 자동으로 인식한다.
TF_VAR_{변수명} 형태의 환경변수를 자동으로 변수로 인식한다.
삽질 3 — vault.yml이 ansible-lint 대상에 포함됨
ansible/ 전체 경로로 실행했다.
vault.yml을 YAML 파일로 인식하고 파싱하려다 오류가 발생했다.
$ANSIBLE_VAULT;1.1;AES256으로 시작하는 텍스트 파일이다. lint 도구가 이를 유효하지 않은 YAML로 인식한다.
.ansible-lint와 .yamllint 설정 파일에 vault.yml을 exclude 목록에 추가해야 한다. 처음 CI 구성 시 반드시 확인하는 항목이다.
면접 포인트
첫째, 작업의 성격입니다. 처음부터 새로 구성을 만드는 초기 프로비저닝이라면 Terraform, 기존 구성에 부분적인 변경을 반복적으로 가하는 운영 작업이라면 Ansible이 적합합니다.
둘째, 드리프트 감지 필요성입니다. "코드와 실제 상태가 다를 수 있다"는 환경이라면 terraform plan으로 자동 감지가 가능한 Terraform이 유리합니다. GUI 클릭이 빈번한 환경일수록 드리프트 감지의 가치가 높아집니다.
셋째, 팀의 기존 스택입니다. 이미 Ansible을 서버 자동화에 쓰고 있는 팀이라면 cisco.aci 컬렉션을 추가하는 것이 진입 장벽이 낮습니다. 반면 클라우드 인프라를 Terraform으로 관리하는 팀이라면 ACI도 Terraform으로 통일하는 것이 일관성 면에서 유리합니다.
표준적인 해결책은 Remote Backend를 사용하는 것입니다. AWS S3 + DynamoDB 조합이 가장 많이 쓰입니다. S3에 State 파일을 저장하고, DynamoDB로 동시 실행을 방지하는 Lock을 관리합니다.
이 프로젝트는 개인 포트폴리오용이라 로컬 State를 .gitignore로 처리했지만, 실제 팀 환경에서는 반드시 Remote Backend를 구성해야 합니다. Terraform Cloud(HCP Terraform)를 쓰면 별도 인프라 없이 Remote Backend를 바로 사용할 수 있습니다.
기술적으로는 Terraform이나 Ansible 자체를 배우는 것보다, 네트워크 오브젝트를 코드로 표현하는 사고 전환이 어렵습니다. ACI를 예로 들면 "APIC GUI에서 클릭하던 것을 HCL로 쓰면 된다"는 것을 이해하는 게 첫 번째 허들입니다.
문화적으로는 "검증된 방식(GUI 클릭)을 왜 바꿔야 하나"는 저항이 있습니다. 이를 극복하는 가장 효과적인 방법은 작은 성공 경험을 만드는 것입니다. 반복적으로 하던 작업 하나를 Playbook으로 만들어서 시간을 줄여보는 것, 그리고 Git으로 변경 이력이 자동으로 쌓이는 것을 팀에 보여주는 것이 시작점이 될 수 있습니다.
'Network > ACI' 카테고리의 다른 글
| [ACI-IaC] Ansible로 Basic Tenant 프로비저닝하기 (0) | 2026.04.07 |
|---|---|
| [ACI-IaC] Terraform으로 3계층 앱 구성하기 (0) | 2026.04.07 |
| [ACI-IaC] Terraform으로 Basic Tenant 프로비저닝하기 (2) | 2026.04.07 |
| [ACI-IaC] Terraform vs Ansible 개요 (0) | 2026.04.07 |