Home Linux AWS Cloud Docker Python AI
클릭하여 터미널 활성화

Ansible로 Docker 인프라 전체 자동화하기 IaC 구축 완전 가이드

// Ansible · IaC · Docker · 자동화 · Rocky Linux · DevOps
지금까지 수동으로 구축한 Docker 인프라 전체를 Ansible로 자동화하여, 새 서버에 단일 명령 하나로 전체 인프라를 배포할 수 있도록 구성했다. 수동 2시간 → 자동 3분.
📋 이 글에서 다루는 것
Ansible 프로젝트 구조 설계 — inventory, roles, templates, handlers 구성
5개 Role(docker, nginx-app, monitoring, security, backup)의 역할과 코드 상세
Jinja2 템플릿으로 환경별 설정 분리 (Grafana 포트, 비밀번호, 서버 IP 등)
전체 플레이북 실행 및 검증 — 41개 태스크, 실패 0, 소요 시간 ~3분
수동 구축 vs Ansible 자동화 비교 — IaC(Infrastructure as Code)의 실무 가치
1. Overview — 왜 Ansible이 필요한가

이전 글들에서 Docker 설치, Nginx 리버스 프록시, Prometheus + Grafana 모니터링, Loki 로그 수집, HTTPS SSL 설정, 백업 자동화까지 전부 구축했다. 문제는 이 과정이 수동이라는 점이다. 서버를 새로 세팅하거나, 두 번째 서버를 추가하려면 같은 작업을 처음부터 반복해야 한다. 명령어를 하나라도 빠뜨리거나 순서를 틀리면 장애가 난다.

Ansible은 이 전체 과정을 코드(YAML)로 정의해서, ansible-playbook site.yml 한 줄로 전부 자동 실행한다. 이걸 IaC(Infrastructure as Code)라고 한다 — 인프라를 코드로 관리하면, 코드를 읽는 것만으로 인프라 상태를 알 수 있고, Git으로 버전 관리도 가능하다.

#항목Before (수동)After (Ansible)
1Docker 설치수동 dnf 명령NEW Role: docker
2Nginx + 백엔드 배포수동 파일 생성NEW Role: nginx-app
3모니터링 스택수동 docker composeNEW Role: monitoring
4보안 설정수동 설정 파일 편집NEW Role: security
5백업 자동화수동 스크립트 배포NEW Role: backup
6전체 배포 시간~2시간 (수동)~3분 (자동)

2. Architecture

Ansible은 에이전트리스(Agentless) 구조다. 관리 대상 서버에 별도 프로그램을 설치할 필요 없이, SSH 접속만 가능하면 된다. Mac(Control Node)에서 SSH로 Rocky Linux(Managed Node)에 접속해서 5개 Role을 순서대로 실행한다.

[Mac Mini — Ansible Control Node] │ │ SSH (Key Auth) │ ▼ [Rocky Linux 9.7 — Managed Node] │ ├── Role: docker → Docker CE 설치, 서비스 활성화 ├── Role: nginx-app → Nginx + Python 백엔드 + SSL ├── Role: monitoring → Prometheus + Grafana + Loki + Promtail ├── Role: security → SSH, Firewalld, Fail2ban, SELinux └── Role: backup → 자동 백업 스크립트 + cron
💡 에이전트리스의 장점 — Chef나 Puppet은 관리 대상 서버마다 에이전트를 설치해야 한다. Ansible은 SSH만 있으면 되기 때문에 설치 부담이 없고, 기존 서버에도 바로 적용할 수 있다. Python만 설치되어 있으면 된다.

3. Ansible 설치

Ansible은 Control Node(Mac)에만 설치한다. 관리 대상 서버(Rocky Linux)에는 아무것도 설치하지 않는다 — SSH 접속과 Python 3만 있으면 된다.

Mac (Control Node)
$ brew install ansible
설치 버전
ComponentVersion
Ansible13.4.0
Ansible Core2.20.3
Python3.14
서버 요구사항
요구사항왜 필요한가
SSH 키 인증 접속 가능Ansible이 SSH로 접속하기 때문 — 비밀번호 인증은 자동화에 부적합
Python 3 설치됨Ansible 모듈이 Python으로 실행되기 때문
sudo NOPASSWD 설정Docker, 방화벽 등 root 권한 작업을 비밀번호 입력 없이 실행하기 위해

4. Project Structure — 프로젝트 구조

Ansible 프로젝트는 정해진 디렉토리 구조를 따른다. 이 구조를 지키면 Ansible이 파일을 자동으로 찾는다. 각 디렉토리가 뭔지, 왜 이렇게 나누는지 정리한다.

~/Projects/ansible-infra/
~/Projects/ansible-infra/
├── ansible.cfg                    # Ansible 전역 설정
├── site.yml                       # 메인 플레이북 (전체 인프라 실행)
├── inventory/
│   └── hosts.yml                  # 관리 대상 서버 목록
├── playbooks/
│   ├── docker-only.yml            # Docker만 설치
│   ├── monitoring-only.yml        # 모니터링만 배포
│   └── security-only.yml          # 보안만 적용
└── roles/
    ├── docker/
    │   └── tasks/main.yml         # Docker 설치 태스크
    ├── nginx-app/
    │   ├── tasks/main.yml         # 앱 배포 태스크
    │   ├── handlers/main.yml      # 재시작 핸들러
    │   ├── templates/             # Jinja2 템플릿 (.j2)
    │   │   ├── docker-compose.yml.j2
    │   │   ├── nginx.conf.j2
    │   │   └── index.html.j2
    │   └── files/                 # 정적 파일 (그대로 복사)
    │       ├── Dockerfile
    │       └── app.py
    ├── monitoring/
    │   ├── tasks/main.yml
    │   ├── handlers/main.yml
    │   └── templates/
    │       ├── docker-compose.yml.j2
    │       ├── prometheus.yml.j2
    │       ├── loki-config.yml.j2
    │       └── promtail-config.yml.j2
    ├── security/
    │   ├── tasks/main.yml
    │   ├── handlers/main.yml
    │   └── files/
    │       ├── 99-security.conf
    │       └── jail.local
    └── backup/
        ├── tasks/main.yml
        └── files/
            └── backup.sh
디렉토리별 역할
디렉토리/파일역할왜 이렇게 나누는가
ansible.cfgAnsible 전역 설정인벤토리 경로, 사용자, sudo 설정을 한 곳에서 관리
inventory/관리 대상 서버 목록어떤 서버에 배포할지 정의 — 서버 추가/제거 용이
site.yml메인 플레이북어떤 Role을 어떤 순서로 실행할지 정의
roles/*/tasks/실행할 작업 목록기능별로 분리 — 재사용 가능
roles/*/templates/Jinja2 템플릿변수를 넣어서 환경별 설정 파일 자동 생성
roles/*/files/정적 파일변수 치환 없이 그대로 복사할 파일
roles/*/handlers/이벤트 핸들러설정 변경 시에만 서비스 재시작 — 불필요한 재시작 방지
💡 templates vs files 차이templates/에 넣으면 Jinja2 변수({{ server_ip }})가 치환되고, files/에 넣으면 그대로 복사된다. Grafana 비밀번호나 서버 IP처럼 환경마다 달라지는 값은 template, Dockerfile처럼 고정된 파일은 files에 넣는다.

5. Configuration Files — 핵심 설정
ansible.cfg — Ansible 전역 설정

이 파일이 프로젝트 루트에 있으면 Ansible이 자동으로 읽는다. 인벤토리 경로, SSH 사용자, sudo 설정 등을 한 곳에서 관리한다. host_key_checking = false가 중요한데, 새 서버에 처음 접속할 때 "Are you sure you want to continue connecting?" 프롬프트가 자동화를 멈추게 하는 걸 방지한다.

ansible.cfg
[defaults]
inventory = inventory/hosts.yml        # 인벤토리 파일 경로
remote_user = admin                    # SSH 접속 사용자
become = true                          # sudo 사용
become_method = sudo
host_key_checking = false              # SSH 호스트키 확인 비활성화
roles_path = roles                     # Role 디렉토리 경로
timeout = 30                           # SSH 타임아웃

[privilege_escalation]
become = true
become_method = sudo
become_ask_pass = false                # sudo 비밀번호 묻지 않음
inventory/hosts.yml — 서버 목록

Ansible이 어떤 서버에 작업할지 정의하는 파일이다. 서버를 추가하려면 여기에 호스트를 한 줄 추가하면 된다. ansible_python_interpreter를 명시하는 이유는 Rocky Linux에 Python 2와 3이 둘 다 있을 수 있어서, Ansible이 Python 3를 확실히 사용하도록 하기 위함이다.

inventory/hosts.yml
all:
  hosts:
    rocky:
      ansible_host: 10.211.55.10
      ansible_user: admin
      ansible_become: true
  vars:
    ansible_python_interpreter: /usr/bin/python3
site.yml — 메인 플레이북

이 파일이 전체 인프라의 진입점이다. ansible-playbook site.yml을 실행하면 여기 정의된 순서대로 5개 Role이 실행된다. vars에 정의한 변수들은 Jinja2 템플릿에서 {{ server_ip }} 같은 형태로 참조된다. 서버 IP나 Grafana 비밀번호가 바뀌면 이 파일만 수정하면 모든 설정에 자동 반영된다.

site.yml
---
- name: Rocky Linux Infrastructure Setup
  hosts: rocky
  become: true
  vars:
    server_ip: 10.211.55.10
    grafana_port: "9300"
    grafana_password: "EofGrafana2026!"

  roles:
    - docker           # 1. Docker 설치
    - monitoring       # 2. Prometheus + Grafana + Loki
    - nginx-app        # 3. Nginx + Backend + SSL
    - security         # 4. SSH, 방화벽, Fail2ban
    - backup           # 5. 백업 스크립트 + cron
⚠️ Role 순서가 중요하다 — docker가 먼저 설치되어야 monitoring과 nginx-app이 컨테이너를 띄울 수 있고, security는 서비스가 다 떠있는 상태에서 방화벽 포트를 열어야 한다. 순서를 바꾸면 실패한다.

6. Role 상세 — 5개 역할 코드
Role: docker — Docker CE 설치

이 Role이 하는 일: Docker CE 레포지토리 추가 → 패키지 설치 → 서비스 시작/자동실행 → admin 사용자 docker 그룹 추가. 이전 글에서 수동으로 했던 것과 완전히 동일한 작업이다.

roles/docker/tasks/main.yml
---
- name: Docker CE 레포지토리 추가
  yum_repository:
    name: docker-ce
    description: Docker CE Stable
    baseurl: https://download.docker.com/linux/centos/$releasever/$basearch/stable
    gpgcheck: true
    gpgkey: https://download.docker.com/linux/centos/gpg
    enabled: true

- name: Docker 패키지 설치
  dnf:
    name:
      - docker-ce
      - docker-ce-cli
      - containerd.io
      - docker-compose-plugin
      - docker-buildx-plugin
    state: present

- name: Docker 서비스 시작 및 자동 실행
  systemd:
    name: docker
    state: started
    enabled: true

- name: admin 사용자를 docker 그룹에 추가
  user:
    name: admin
    groups: docker
    append: true
💡 state: present의 의미 — "이 패키지가 설치되어 있는 상태를 보장하라"는 뜻이다. 이미 설치되어 있으면 아무것도 하지 않는다. 이것이 Ansible의 멱등성(Idempotency)이다 — 몇 번을 실행해도 결과가 같다.
Role: monitoring — 모니터링 스택

Prometheus, Grafana 11.4.0, Node Exporter, cAdvisor, Loki 3.4.2, Promtail 3.4.2를 배포한다. 핵심은 Jinja2 템플릿이다 — docker-compose.yml.j2{{ grafana_password }} 같은 변수를 넣어두면, site.yml의 vars에 정의한 값이 자동으로 들어간다. 서버 IP나 포트를 바꾸면 vars만 수정하면 모든 설정 파일이 같이 바뀐다.

설정 파일이 변경되면 handlerdocker compose up -d를 자동으로 실행해서 서비스를 재시작한다. 변경이 없으면 재시작하지 않는다 — 불필요한 다운타임을 방지하는 것이다.

Role: nginx-app — 웹 앱 + HTTPS

Nginx 리버스 프록시 + Python 백엔드 + 자체 서명 SSL 인증서를 배포한다. 이전 글들에서 만든 설정이 전부 포함된다: HTTPS(TLS 1.2/1.3), 보안 헤더(HSTS, X-Frame-Options), 서브 경로 라우팅(/api/, /grafana/, /prometheus/), monitoring 네트워크 연결(external network).

Role: security — 보안 설정

SSH 보안, Firewalld, Fail2ban, SELinux, chrony(시간 동기화), dnf-automatic(자동 업데이트)을 설정한다.

태스크Ansible 모듈왜 이 모듈을 쓰나
SSH 설정 배포copy파일을 그대로 복사 (변수 치환 불필요)
Fail2ban 설치/설정dnf + copy패키지 설치 후 설정 파일 배포
포트 개방/차단firewalldAnsible 전용 방화벽 모듈 — 선언적 포트 관리
시간 동기화dnf + systemdchrony 설치 후 서비스 활성화
SELinux 확인command + debug현재 상태 확인 (변경은 안 함)
자동 업데이트lineinfile + systemd설정 파일 특정 라인만 수정
Role: backup — 백업 자동화

이전 글에서 만든 백업 스크립트(backup.sh)를 서버에 배포하고 cron에 등록한다. files/backup.sh를 서버의 /opt/backups/에 복사하고, /etc/cron.d/infrastructure-backup에 매일 03:00 실행을 등록한다.


7. Execution — 실행 및 검증
연결 테스트

플레이북을 실행하기 전에 Ansible이 서버에 접속할 수 있는지 확인한다. ping 모듈은 ICMP ping이 아니라 SSH 접속 + Python 실행을 테스트하는 것이다.

연결 테스트
$ cd ~/Projects/ansible-infra
$ ansible rocky -m ping

rocky | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
전체 플레이북 실행
실행
$ ansible-playbook site.yml --diff

--diff 옵션은 파일이 변경될 때 diff를 보여준다. 어떤 설정이 바뀌었는지 한눈에 확인할 수 있어서, 프로덕션에서 변경 추적에 유용하다.

실행 결과
PLAY RECAP
PLAY RECAP *********************************************************************
rocky    : ok=41   changed=15   unreachable=0   failed=0   skipped=0   rescued=0   ignored=0
항목설명
ok41전체 태스크 수 — 41개 작업 모두 성공
changed15실제로 변경이 발생한 태스크 — 나머지 26개는 이미 원하는 상태
failed0실패 없음
unreachable0연결 문제 없음
✓ 전체 배포 완료 — 41개 태스크, 실패 0, 8개 컨테이너 전부 정상 가동.
서비스 확인
docker ps
NAMES           STATUS
grafana         Up (healthy)
cadvisor        Up (healthy)
loki            Up
node-exporter   Up
prometheus      Up
promtail        Up
backend-app     Up
nginx-proxy     Up
API 테스트
API 응답
$ curl -sk https://10.211.55.10/api/
{
  "status": "ok",
  "message": "Hello from Backend!",
  "server": "Rocky Linux 9.7",
  "deployed_by": "Ansible",
  "time": "2026-03-08T12:01:30.871908"
}

8. Usage Guide — 사용법
전체 인프라 배포
전체 배포
$ cd ~/Projects/ansible-infra
$ ansible-playbook site.yml
개별 역할만 실행
개별 실행
# Docker만 설치
$ ansible-playbook playbooks/docker-only.yml

# 모니터링만 배포
$ ansible-playbook playbooks/monitoring-only.yml

# 보안만 적용
$ ansible-playbook playbooks/security-only.yml

# 변경 사항 미리보기 (Dry Run — 실제 변경 없음)
$ ansible-playbook site.yml --check --diff
새 서버 추가 시

서버를 추가하려면 inventory/hosts.yml에 한 블록만 추가하고 플레이북을 다시 실행하면 된다. 같은 코드로 몇 대든 배포할 수 있다.

inventory/hosts.yml
all:
  hosts:
    rocky:
      ansible_host: 10.211.55.10
    rocky2:                          # 새 서버 추가
      ansible_host: 10.211.55.11
      ansible_user: admin
      ansible_become: true
전체 서버에 배포
$ ansible-playbook site.yml

9. Ansible 핵심 개념 정리
개념적용 내용실무 의미
Inventoryhosts.yml로 서버 관리다수 서버 중앙 관리
Roles기능별 역할 분리 (5개)재사용 가능한 모듈화
Templates (Jinja2)변수 기반 설정 파일 생성환경별 설정 분리
Handlers설정 변경 시 자동 재시작불필요한 재시작 방지
Idempotency반복 실행해도 동일 결과안전한 반복 배포
VariablesGrafana 포트/비밀번호 변수화환경별 커스터마이징
Dry Run--check --diff변경 사항 사전 확인

10. Before vs After — 수동 vs Ansible
항목Before (수동)After (Ansible)
배포 시간~2시간~3분
재현성매번 명령어 입력코드로 정의 (IaC)
실수 가능성높음 (오타, 누락)낮음 (자동화)
문서화별도 보고서 필요코드 자체가 문서
다수 서버서버마다 반복한 번에 전체 배포
롤백수동 복원이전 버전 재배포
변경 추적없음Git으로 버전 관리

11. 프로덕션 권장 사항
항목권장 사항왜 필요한가
Vaultansible-vault로 비밀번호 암호화Grafana PW 등 민감 정보가 평문으로 코드에 노출됨
Tags역할별 태그 추가전체가 아닌 특정 역할만 선택적 실행 가능
Git프로젝트를 Git 저장소로 관리변경 이력 추적, 팀 협업, 롤백 용이
CI/CDGitHub Actions + AnsibleGit push 시 자동 배포 파이프라인
AWX/Tower웹 UI 기반 Ansible 관리팀 환경에서 실행 이력, 권한 관리
Dynamic Inventory클라우드 동적 인벤토리AWS, GCP 등에서 서버 자동 검색
MoleculeRole 테스트 자동화Role이 제대로 동작하는지 자동 검증

12. 자주 묻는 질문 (FAQ)
Q. Ansible은 서버에 뭘 설치해야 하나?
서버에는 아무것도 설치하지 않는다(에이전트리스). SSH 접속과 Python 3만 있으면 된다. Ansible은 Control Node(Mac, Linux 등)에만 설치한다. 이것이 Chef나 Puppet과의 가장 큰 차이점이다.
Q. 멱등성(Idempotency)이란?
같은 플레이북을 100번 실행해도 결과가 동일한 성질이다. 예를 들어 state: present는 "패키지가 설치되어 있는 상태를 보장"하므로, 이미 설치되어 있으면 아무것도 하지 않는다. 덕분에 "이미 실행했는데 다시 실행해도 괜찮나?"라는 걱정 없이 언제든 재실행할 수 있다.
Q. Jinja2 템플릿과 files의 차이는?
templates/에 넣으면 {{ variable }} 변수가 치환되고, files/에 넣으면 그대로 복사된다. Grafana 비밀번호나 서버 IP처럼 환경마다 달라지는 값이 있는 파일은 template, Dockerfile처럼 고정된 파일은 files에 넣는다.
Q. Handler는 왜 쓰나?
설정 파일이 변경되었을 때만 서비스를 재시작하기 위해서다. 예를 들어 nginx.conf가 바뀌면 handler가 docker compose up -d를 실행하지만, 변경이 없으면 재시작하지 않는다. 불필요한 다운타임을 방지하는 것이 핵심이다.
Q. Ansible vs Terraform, 어떤 걸 써야 하나?
Terraform은 인프라 자체를 만드는 도구(서버 생성, 네트워크 구성)이고, Ansible은 만들어진 인프라 위에 소프트웨어를 설정하는 도구다. AWS에서 EC2를 만드는 건 Terraform, 그 EC2에 Docker를 설치하고 서비스를 배포하는 건 Ansible. 둘은 경쟁이 아니라 조합이다.

📌 이 글 핵심 요약
Ansible로 Docker 인프라 전체(Docker 설치, 웹 앱, 모니터링, 보안, 백업)를 코드(YAML)로 정의했다
5개 Role로 기능별 분리 — 재사용 가능하고, 개별 실행도 가능하다
Jinja2 템플릿으로 환경 변수(IP, 비밀번호)를 분리하여 다수 서버에 동일 코드로 배포한다
수동 2시간 → 자동 3분. 멱등성 보장으로 언제든 안전하게 재실행 가능하다
프로덕션에서는 ansible-vault(비밀번호 암호화), Git, CI/CD 파이프라인을 추가해야 한다