해당 스터디는 90DaysOfDevOps
https://github.com/MichaelCade/90DaysOfDevOps
를 기반으로 진행한 내용입니다.
Day 55 - Bringing Together IaC and CM with Terraform Provider for Ansible
IaC는 기계가 읽을 수 있는 파일을 통해 인프라를 관리하고 프로비저닝하는 방법론이다.
일관성(Consistency): 언제나 동일한 인프라 상태를 유지한다.
확장성(Scalability): 코드를 반복 실행하여 리소스를 빠르게 확장하거나 축소할 수 있다.
협업(Collaboration): 팀원들이 문서를 뒤지거나 구두로 묻는 대신, 코드를 공유하며 효율적으로 협업한다.
효율성(Efficiency): 수동 명령보다 코드 실행이 훨씬 빠르다.
멱등성(Idempotency): 가장 중요한 특성으로, 코드를 몇 번 실행하든 결과가 항상 동일함을 보장한다. 이는 중복 배포의 위험을 제거한다.
명령형(Imperative) 방식과 선언형(Declarative) 인프라를 제어하는 방식은 크게 두 가지로 나뉜다.
명령형(Imperative):
목표 도달을 위해 어떻게 (How) 해야 하는지 단계별로 세세하게 지시하는 방식
마이크로매니지먼트가 필요
선언형(Declarative) :
원하는 최종 상태를 정의하고, 구체적인 실행 순서는 도구에 맡기는 방식
현대적인 IaC 도구들의 방식
Terraform: 인프라 프로비저닝 Terraform은 인프라 리소스 (네트워크, 서버, 방화벽 등)를 생성하는 데 특화되어 있다.
아키텍처: 정적으로 컴파일된 바이너리인 Terraform Core와 특정 서비스(GCP, AWS 등) API와 통신하는 provider(플러그인)로 구성된다.
State 파일: Terraform은 실행 전후의 인프라 상태를 tfstate 파일에 기록하여 관리한다. 이를 통해 현재 인프라의 모습을 파악한다.
Ansible: 구성 관리 프로비저닝된 인프라 위에 애플리케이션을 배포하고 설정하는 데 사용된다.
Playbook: 수행할 작업을 정의한 YAML 파일이다.
Inventory: 작업을 수행할 대상 호스트의 목록을 정의한 파일이다.
Terraform과 Ansible을 함께 사용할 때 마주하는 문제는상태 정보의 불일치이다.
Terraform: 인프라를 동적으로 생성하므로 IP 주소 등의 정보가 실행 시점에 결정되며 State 파일에 저장된다.
Ansible: Playbook을 실행하려면 접속할 호스트의 IP 정보 즉, Inventory가 필요하다.

이 두 도구 사이의 정보를 동적으로 연결하기 위해 terraform-provider-ansible을 사용한다.
이 프로바이더는 Terraform이 생성한 리소스 정보를 Ansible이 이해할 수 있는 인벤토리 형태로 동적으로 전달하는 역할을 수행한다.
.
├── main.tf # 인프라 리소스 및 Ansible 연동 리소스 정의
├── variables.tf # 변수 정의 (GCP 프로젝트 ID, 리전 등)
├── outputs.tf # 배포 후 출력값 (웹 서버 URL 등)
└── web_vm.yaml # Ansible Playbook (Nginx 설치 및 설정)
해당 프레젠테이션은 Google Cloud Platform 위에 Nginx 웹 서버를 배포하는 과정을 코드로 구현한다.
# main.tf
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
ansible = {
source = "ansible/ansible"
version = "~> 1.0"
}
}
}
# 네트워크 리소스 (VPC, Subnet, Firewall)
resource "google_compute_network" "vpc_network" {
name = "terraform-network"
}
resource "google_compute_firewall" "allow_web" {
name = "allow-web-traffic"
network = google_compute_network.vpc_network.name
allow {
protocol = "tcp"
ports = ["80", "22"]
}
source_ranges = ["0.0.0.0/0"] # 데모용 설정 (실무에서는 제한 필요)
}
# 컴퓨트 인스턴스 (웹 VM)
resource "google_compute_instance" "web_vm" {
name = "web-server-instance"
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "ubuntu-os-cloud/ubuntu-2004-lts"
}
}
network_interface {
network = google_compute_network.vpc_network.name
access_config {
# Ephemeral public IP 할당
}
}
# Ansible이 접속할 수 있도록 SSH Key 주입
metadata = {
ssh-keys = "ubuntu:${file("~/.ssh/id_rsa.pub")}"
}
}
Provider 정의: Google Cloud 및 Ansible 프로바이더 버전을 명시
네트워크 리소스: VPC, Subnet을 생성하고, 방화벽 규칙(Port 22, 80 등)을 설정하여 외부 접속을 허용
컴퓨트 리소스: Ubuntu 이미지를 기반으로 VM을 생성한다. + metadata 필드를 통해 SSH 키를 주입하여 추후 Ansible이 접속할 수 있도록 한다.
# main.tf (이어짐)
# 1. VM 부팅 대기 (SSH 접속 가능할 때까지 대기)
resource "time_sleep" "wait_for_vm" {
create_duration = "20s"
depends_on = [google_compute_instance.web_vm]
}
# 2. 동적 인벤토리 구성 (Terraform -> Ansible 정보 전달)
resource "ansible_host" "web_host" {
name = google_compute_instance.web_vm.network_interface.0.access_config.0.nat_ip # 외부 IP
groups = ["web"] # Playbook에서 타겟팅할 그룹명
# Ansible 접속 변수 설정
variables = {
ansible_user = "ubuntu"
ansible_ssh_private_key_file = "~/.ssh/id_rsa"
ansible_python_interpreter = "/usr/bin/python3"
}
depends_on = [time_sleep.wait_for_vm]
}
# 3. Playbook 자동 실행
resource "ansible_playbook" "web_deploy" {
playbook = "web_vm.yaml"
name = google_compute_instance.web_vm.network_interface.0.access_config.0.nat_ip
groups = ["web"]
depends_on = [ansible_host.web_host]
}
Terraform 코드 내에서 Ansible 관련 리소스를 정의하여 연결성을 확보
time_sleep 리소스: VM이 완전히 부팅될 때까지 대기(예: 20초)하여 연결 실패를 방지한
ansible_host 리소스:
ansible_playbook 리소스: 실제로 실행할 Playbook 파일(web_vm.yaml)을 지정하고 실행을 트리거한다.
# web_vm.yaml
---
- name: Setup Web Server
hosts: web
become: yes # Root 권한 상승
tasks:
- name: Ensure Nginx is installed
apt:
name: nginx
state: present
update_cache: yes
- name: Ensure Nginx is started
service:
name: nginx
state: started
enabled: yes
- name: Remove default index.html
file:
path: /var/www/html/index.nginx-debian.html
state: absent
- name: Deploy App from GitHub
git:
repo: 'https://github.com/example/demo-app.git'
dest: /var/www/html
version: main
대상: web 그룹 (Terraform에서 동적으로 전달받음).
Task:
Nginx 최신 버전 설치 여부 확인 및 설치.
Nginx 서비스 시작 및 활성화.
기본 index.html 파일 삭제 .
Git 모듈을 사용하여 GitHub에서 데모용 웹 애플리케이션 소스 코드 다운로드.
Validate: terraform validate로 코드 구문 오류를 확인
Plan: terraform plan을 실행하여 생성될 리소스 (인프라 + Ansible 호스트/플레이북)를 미리 확인 (계획만 수립)
Apply: terraform apply로 실제 배포 진행
검증:
ansible-inventory --graph 명령을 통해 Terraform에 의해 동적으로 생성된 인벤토리 구조를 시각적으로 확인할 수 있다.
Terraform Output으로 출력된 URL (IP)에 접속하여 웹 페이지가 정상적으로 로딩되는지 확인한다.
이번 포스팅의 앞부분에서는 terraform-provider-ansible을 사용하여 Terraform 내에서 Ansible을 직접 실행하는 강한 결합(Tight Coupling) 방식을 살펴보았다.
강의에서는 단일 웹 서버를 배포하는 데 이 방식이 매우 효율적임을 보여주었다.
하지만 필자는 Oracle Cloud Infrastructure (OCI) 상에서 Kubernetes 클러스터를 구축하고, 그 위에 Cilium(CNI)과 Prometheus 같은 애드온을 자동화하는 프로젝트를 진행하면서, 강의와는 다른 접근 방식을 택했다.
Terraform은 인벤토리 파일만 생성하고, Ansible 실행은 분리하는 느슨한 결합 전략이다.
필자의 방식과 강의의 방식에 대해서 코드로서의 차이점, 장단점에 대해서 분석하고자 한다.
가장 큰 차이는 누가 Ansible을 실행하는가와 연결 고리가 무엇인가에 있다.
A. 강의 방식 (Terraform Provider Ansible) : Terraform이 주도권을 쥐고 Ansible Playbook까지 리소스로 관리한다.
# main.tf
resource "ansible_playbook" "web_deploy" {
playbook = "web_vm.yaml"
name = google_compute_instance.web_vm.network_interface.0.access_config.0.nat_ip
# Terraform이 Playbook 실행을 직접 트리거함
}
B. 나의 프로젝트 방식 (Local File + 분리 실행) : Terraform은 인프라를 프로비저닝하고 Ansible이 작업할 Inventory만 깔아주며, 실제 실행은 스크립트나 운영자가 수행한다.
# 파일구조
.
├── terraform/
│ ├── main.tf # OCI 인스턴스(Master/Worker) 생성
│ └── inventory.tf # Ansible용 hosts.ini 파일 생성 로직
├── ansible/
│ ├── playbooks/ # 단계별 Playbook (K8s 설치, Cilium, Monitoring...)
│ └── roles/ # 재사용 가능한 Role
└── scripts/
└── deploy.sh # Terraform Apply -> Ansible Playbook 순차 실행 스크립트
# terraform/inventory.tf
resource "local_file" "ansible_inventory" {
content = templatefile("${path.module}/inventory.tpl", {
master_ips = oci_core_instance.k8s_master[*].public_ip
worker_ips = oci_core_instance.k8s_worker[*].public_ip
})
filename = "${path.module}/../ansible/inventory/hosts.ini"
# Terraform은 파일만 생성하고 역할 종료
}
단순한 웹 서버 하나를 띄울 때는 강의의 방식이 훨씬 빠르고 간편하다.
하지만 Kubernetes 클러스터 + 애드온(Prometheus, Cilium)과 같이 복잡도가 높은 환경에서는 Local File + 분리 실행 방식이 실무적으로 더 적합하다.
| 비교 항목 | 강의 방식 (Provider 사용) | 내 프로젝트 방식 (파일 생성 + 분리) |
|---|---|---|
| 디버깅 용이성 | Ansible 오류 발생 시 Terraform 에러로 래핑되어 출력됨. 디버깅이 까다로움 | Ansible 로그를 직접 확인 가능. 특정 작업 실패 시 해당 Playbook만 재실행(--start-at-task) 가능. |
| 워크플로우 제어 | terraform apply 한 방에 실행되므로 단계별 제어가 어려움. | 단계별 실행 가능. (예: OS 설정 -> K8s 설치 -> CNI 배포 -> 모니터링 구축) |
| 변경 관리 (Day 2) | Prometheus 설정만 바꾸려 해도 terraform apply를 실행해야 함. | 인프라 변경 없이 Ansible만 독립적으로 실행하여 애플리케이션/애드온 설정 업데이트 가능. |
| 확장성 | 노드 추가 시 전체 로직이 재평가됨. | 노드 추가 시 Terraform으로 인프라/인벤토리만 갱신하고, 새 노드에 대해서만 Ansible 실행(--limit) 가능. |
강의에서 배운 terraform-provider-ansible은 완전한 자동화와 Single Source of Truth을 구현하는 데 훌륭한 도구다.
불변 인프라를 지향하고 배포 대상이 단순하다면 해당 방식이 정답이다.
하지만 내가 구축한 OCI 기반의 Kubernetes 클러스터처럼 인프라 프로비저닝 이후에도 지속적인 설정 변경과 복잡한 미들웨어 설치가 필요한 경우, Terraform은 인프라 생성에, Ansible은 구성 관리에 집중하도록 역할을 명확히 분리하는 것이 운영 안정성과 유지보수 측면에서 훨씬 유리하다.
결국, 아키텍처에 정답은 없다. 프로젝트의 복잡도와 운영 요구사항에 맞춰 강한 결합과 느슨한 결합 사이에서 올바른 균형을 찾는 것이 DevOps 엔지니어의 핵심 역량일 것이다.