이전 글들에서 Docker 설치, Nginx 리버스 프록시, Prometheus + Grafana 모니터링, Loki 로그 수집, HTTPS SSL 설정, 백업 자동화까지 전부 구축했다. 문제는 이 과정이 수동이라는 점이다. 서버를 새로 세팅하거나, 두 번째 서버를 추가하려면 같은 작업을 처음부터 반복해야 한다. 명령어를 하나라도 빠뜨리거나 순서를 틀리면 장애가 난다.
Ansible은 이 전체 과정을 코드(YAML)로 정의해서, ansible-playbook site.yml 한 줄로 전부 자동 실행한다. 이걸 IaC(Infrastructure as Code)라고 한다 — 인프라를 코드로 관리하면, 코드를 읽는 것만으로 인프라 상태를 알 수 있고, Git으로 버전 관리도 가능하다.
| # | 항목 | Before (수동) | After (Ansible) |
|---|---|---|---|
| 1 | Docker 설치 | 수동 dnf 명령 | NEW Role: docker |
| 2 | Nginx + 백엔드 배포 | 수동 파일 생성 | NEW Role: nginx-app |
| 3 | 모니터링 스택 | 수동 docker compose | NEW Role: monitoring |
| 4 | 보안 설정 | 수동 설정 파일 편집 | NEW Role: security |
| 5 | 백업 자동화 | 수동 스크립트 배포 | NEW Role: backup |
| 6 | 전체 배포 시간 | ~2시간 (수동) | ~3분 (자동) |
Ansible은 에이전트리스(Agentless) 구조다. 관리 대상 서버에 별도 프로그램을 설치할 필요 없이, SSH 접속만 가능하면 된다. Mac(Control Node)에서 SSH로 Rocky Linux(Managed Node)에 접속해서 5개 Role을 순서대로 실행한다.
Ansible은 Control Node(Mac)에만 설치한다. 관리 대상 서버(Rocky Linux)에는 아무것도 설치하지 않는다 — SSH 접속과 Python 3만 있으면 된다.
$ brew install ansible
| Component | Version |
|---|---|
| Ansible | 13.4.0 |
| Ansible Core | 2.20.3 |
| Python | 3.14 |
| 요구사항 | 왜 필요한가 |
|---|---|
| SSH 키 인증 접속 가능 | Ansible이 SSH로 접속하기 때문 — 비밀번호 인증은 자동화에 부적합 |
| Python 3 설치됨 | Ansible 모듈이 Python으로 실행되기 때문 |
| sudo NOPASSWD 설정 | Docker, 방화벽 등 root 권한 작업을 비밀번호 입력 없이 실행하기 위해 |
Ansible 프로젝트는 정해진 디렉토리 구조를 따른다. 이 구조를 지키면 Ansible이 파일을 자동으로 찾는다. 각 디렉토리가 뭔지, 왜 이렇게 나누는지 정리한다.
~/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.cfg | Ansible 전역 설정 | 인벤토리 경로, 사용자, sudo 설정을 한 곳에서 관리 |
inventory/ | 관리 대상 서버 목록 | 어떤 서버에 배포할지 정의 — 서버 추가/제거 용이 |
site.yml | 메인 플레이북 | 어떤 Role을 어떤 순서로 실행할지 정의 |
roles/*/tasks/ | 실행할 작업 목록 | 기능별로 분리 — 재사용 가능 |
roles/*/templates/ | Jinja2 템플릿 | 변수를 넣어서 환경별 설정 파일 자동 생성 |
roles/*/files/ | 정적 파일 | 변수 치환 없이 그대로 복사할 파일 |
roles/*/handlers/ | 이벤트 핸들러 | 설정 변경 시에만 서비스 재시작 — 불필요한 재시작 방지 |
templates/에 넣으면 Jinja2 변수({{ server_ip }})가 치환되고, files/에 넣으면 그대로 복사된다. Grafana 비밀번호나 서버 IP처럼 환경마다 달라지는 값은 template, Dockerfile처럼 고정된 파일은 files에 넣는다.이 파일이 프로젝트 루트에 있으면 Ansible이 자동으로 읽는다. 인벤토리 경로, SSH 사용자, sudo 설정 등을 한 곳에서 관리한다. host_key_checking = false가 중요한데, 새 서버에 처음 접속할 때 "Are you sure you want to continue connecting?" 프롬프트가 자동화를 멈추게 하는 걸 방지한다.
[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 비밀번호 묻지 않음
Ansible이 어떤 서버에 작업할지 정의하는 파일이다. 서버를 추가하려면 여기에 호스트를 한 줄 추가하면 된다. ansible_python_interpreter를 명시하는 이유는 Rocky Linux에 Python 2와 3이 둘 다 있을 수 있어서, Ansible이 Python 3를 확실히 사용하도록 하기 위함이다.
all: hosts: rocky: ansible_host: 10.211.55.10 ansible_user: admin ansible_become: true vars: ansible_python_interpreter: /usr/bin/python3
이 파일이 전체 인프라의 진입점이다. ansible-playbook site.yml을 실행하면 여기 정의된 순서대로 5개 Role이 실행된다. vars에 정의한 변수들은 Jinja2 템플릿에서 {{ server_ip }} 같은 형태로 참조된다. 서버 IP나 Grafana 비밀번호가 바뀌면 이 파일만 수정하면 모든 설정에 자동 반영된다.
--- - 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 CE 레포지토리 추가 → 패키지 설치 → 서비스 시작/자동실행 → admin 사용자 docker 그룹 추가. 이전 글에서 수동으로 했던 것과 완전히 동일한 작업이다.
--- - 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
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만 수정하면 모든 설정 파일이 같이 바뀐다.
설정 파일이 변경되면 handler가 docker compose up -d를 자동으로 실행해서 서비스를 재시작한다. 변경이 없으면 재시작하지 않는다 — 불필요한 다운타임을 방지하는 것이다.
Nginx 리버스 프록시 + Python 백엔드 + 자체 서명 SSL 인증서를 배포한다. 이전 글들에서 만든 설정이 전부 포함된다: HTTPS(TLS 1.2/1.3), 보안 헤더(HSTS, X-Frame-Options), 서브 경로 라우팅(/api/, /grafana/, /prometheus/), monitoring 네트워크 연결(external network).
SSH 보안, Firewalld, Fail2ban, SELinux, chrony(시간 동기화), dnf-automatic(자동 업데이트)을 설정한다.
| 태스크 | Ansible 모듈 | 왜 이 모듈을 쓰나 |
|---|---|---|
| SSH 설정 배포 | copy | 파일을 그대로 복사 (변수 치환 불필요) |
| Fail2ban 설치/설정 | dnf + copy | 패키지 설치 후 설정 파일 배포 |
| 포트 개방/차단 | firewalld | Ansible 전용 방화벽 모듈 — 선언적 포트 관리 |
| 시간 동기화 | dnf + systemd | chrony 설치 후 서비스 활성화 |
| SELinux 확인 | command + debug | 현재 상태 확인 (변경은 안 함) |
| 자동 업데이트 | lineinfile + systemd | 설정 파일 특정 라인만 수정 |
이전 글에서 만든 백업 스크립트(backup.sh)를 서버에 배포하고 cron에 등록한다. files/backup.sh를 서버의 /opt/backups/에 복사하고, /etc/cron.d/infrastructure-backup에 매일 03:00 실행을 등록한다.
플레이북을 실행하기 전에 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 ********************************************************************* rocky : ok=41 changed=15 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
| 항목 | 값 | 설명 |
|---|---|---|
| ok | 41 | 전체 태스크 수 — 41개 작업 모두 성공 |
| changed | 15 | 실제로 변경이 발생한 태스크 — 나머지 26개는 이미 원하는 상태 |
| failed | 0 | 실패 없음 |
| unreachable | 0 | 연결 문제 없음 |
NAMES STATUS
grafana Up (healthy)
cadvisor Up (healthy)
loki Up
node-exporter Up
prometheus Up
promtail Up
backend-app Up
nginx-proxy Up
$ 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" }
$ 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에 한 블록만 추가하고 플레이북을 다시 실행하면 된다. 같은 코드로 몇 대든 배포할 수 있다.
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
| 개념 | 적용 내용 | 실무 의미 |
|---|---|---|
| Inventory | hosts.yml로 서버 관리 | 다수 서버 중앙 관리 |
| Roles | 기능별 역할 분리 (5개) | 재사용 가능한 모듈화 |
| Templates (Jinja2) | 변수 기반 설정 파일 생성 | 환경별 설정 분리 |
| Handlers | 설정 변경 시 자동 재시작 | 불필요한 재시작 방지 |
| Idempotency | 반복 실행해도 동일 결과 | 안전한 반복 배포 |
| Variables | Grafana 포트/비밀번호 변수화 | 환경별 커스터마이징 |
| Dry Run | --check --diff | 변경 사항 사전 확인 |
| 항목 | Before (수동) | After (Ansible) |
|---|---|---|
| 배포 시간 | ~2시간 | ~3분 |
| 재현성 | 매번 명령어 입력 | 코드로 정의 (IaC) |
| 실수 가능성 | 높음 (오타, 누락) | 낮음 (자동화) |
| 문서화 | 별도 보고서 필요 | 코드 자체가 문서 |
| 다수 서버 | 서버마다 반복 | 한 번에 전체 배포 |
| 롤백 | 수동 복원 | 이전 버전 재배포 |
| 변경 추적 | 없음 | Git으로 버전 관리 |
| 항목 | 권장 사항 | 왜 필요한가 |
|---|---|---|
| Vault | ansible-vault로 비밀번호 암호화 | Grafana PW 등 민감 정보가 평문으로 코드에 노출됨 |
| Tags | 역할별 태그 추가 | 전체가 아닌 특정 역할만 선택적 실행 가능 |
| Git | 프로젝트를 Git 저장소로 관리 | 변경 이력 추적, 팀 협업, 롤백 용이 |
| CI/CD | GitHub Actions + Ansible | Git push 시 자동 배포 파이프라인 |
| AWX/Tower | 웹 UI 기반 Ansible 관리 | 팀 환경에서 실행 이력, 권한 관리 |
| Dynamic Inventory | 클라우드 동적 인벤토리 | AWS, GCP 등에서 서버 자동 검색 |
| Molecule | Role 테스트 자동화 | Role이 제대로 동작하는지 자동 검증 |
state: present는 "패키지가 설치되어 있는 상태를 보장"하므로, 이미 설치되어 있으면 아무것도 하지 않는다. 덕분에 "이미 실행했는데 다시 실행해도 괜찮나?"라는 걱정 없이 언제든 재실행할 수 있다.templates/에 넣으면 {{ variable }} 변수가 치환되고, files/에 넣으면 그대로 복사된다. Grafana 비밀번호나 서버 IP처럼 환경마다 달라지는 값이 있는 파일은 template, Dockerfile처럼 고정된 파일은 files에 넣는다.nginx.conf가 바뀌면 handler가 docker compose up -d를 실행하지만, 변경이 없으면 재시작하지 않는다. 불필요한 다운타임을 방지하는 것이 핵심이다.