서버가 한 대일 때는 SSH로 들어가서 명령을 치면 됩니다. 그런데 두 대, 열 대, 백 대가 되면요? 똑같은 설정을 매번 손으로 하다 보면 어느 서버는 설정이 빠지고, 어느 서버는 버전이 다른 일이 반드시 생깁니다. 이 문제를 해결해 주는 도구가 Ansible(앤서블)입니다.
이 글은 Ansible을 처음 보는 분이 설치 → 첫 명령 → 플레이북 작성 → 실전 예제까지 따라 할 수 있게 정리했습니다. 글에 나오는 명령과 출력은 제가 Rocky Linux 8.10 서버에서 직접 실행해 확인한 결과입니다(Ansible core 2.19 기준).
목차
- Ansible이란? 무엇이 좋은가
- 핵심 개념 5가지
- 리눅스에 설치하기
- 인벤토리 만들기와 SSH 준비
- 애드혹(ad-hoc) 명령 - 한 줄로 여러 서버 제어
- 플레이북 첫 작성 - 그리고 멱등성
- 실전 예제 ① 웹서버(nginx) 한 번에 구축
- 실전 예제 ② 사용자 계정·SSH 키 일괄 등록
- 실전 예제 ③ 패키지 업데이트와 서비스 재시작
- 변수·반복·조건·핸들러·템플릿
- 비밀번호는 Vault로 암호화
- 자주 막히는 문제와 해결
- 디렉터리 구조 추천과 마무리 체크리스트
1. Ansible이란? 무엇이 좋은가
Ansible은 Red Hat이 관리하는 오픈소스 IT 자동화 도구입니다. 서버 설정, 프로그램 설치, 배포, 점검 같은 반복 작업을 YAML 파일 하나에 적어 두고 여러 서버에 똑같이 적용합니다.
- 에이전트가 필요 없음(Agentless) - 관리 대상 서버에는 아무것도 설치하지 않습니다. SSH와 파이썬만 있으면 됩니다.
- 읽기 쉬운 YAML - 프로그래밍을 몰라도 "nginx를 설치해라"처럼 문장에 가깝게 씁니다.
- 멱등성(Idempotent) - 같은 파일을 몇 번 실행해도 결과가 같습니다. 이미 원하는 상태면 아무것도 건드리지 않습니다.
- 무료 - 개인 홈서버부터 회사 인프라까지 비용 없이 사용합니다.
- 코드로 남음 - 서버 설정이 파일로 남아 Git으로 관리하고, 서버가 망가져도 같은 파일로 복구할 수 있습니다.
비슷한 도구로 Puppet, Chef, Terraform이 있습니다. 대략 Terraform은 "서버·네트워크를 만드는" 도구, Ansible은 "만들어진 서버 안을 설정하는" 도구로 역할을 나눠 같이 쓰기도 합니다.
2. 핵심 개념 5가지
- 컨트롤 노드 - Ansible을 설치하고 명령을 내리는 내 PC나 관리용 서버.
- 관리 대상(Managed node) - 설정을 적용받는 서버들. 파이썬과 SSH만 있으면 됩니다.
- 인벤토리(Inventory) - 서버 목록과 그룹을 적은 파일.
- 모듈(Module) - 실제 일을 하는 작은 부품.
package(설치),service(서비스),copy(파일 복사),user(계정) 등 수천 개가 있습니다. - 플레이북(Playbook) - 어떤 서버에 어떤 모듈을 어떤 순서로 쓸지 적은 YAML 파일. Ansible의 핵심입니다.
3. 리눅스에 설치하기
Ansible은 컨트롤 노드 한 곳에만 설치합니다. 관리 대상 서버에는 설치하지 않습니다.
방법 A. 패키지 매니저 (가장 간단)
# Ubuntu / Debian
sudo apt update
sudo apt install -y ansible
# RHEL / Rocky / Alma (EPEL 필요)
sudo dnf install -y epel-release
sudo dnf install -y ansible
# Fedora
sudo dnf install -y ansible
방법 B. pip + 가상환경 (최신 버전, 추천)
배포판 저장소의 Ansible은 버전이 오래된 경우가 많습니다. 최신 버전을 쓰고 시스템 파이썬을 어지럽히지 않으려면 가상환경이 깔끔합니다. 아래는 제 서버에서 실제로 한 방법입니다.
python3 -m venv ~/ansible-venv
source ~/ansible-venv/bin/activate
pip install ansible # 전체 패키지(자주 쓰는 컬렉션 포함)
# 용량을 줄이려면: pip install ansible-core
ansible --version
ansible [core 2.19.14]
config file = None
configured module search path = ['/root/.ansible/plugins/modules', ...]
주의 - 파이썬 버전: 최신 Ansible은 컨트롤 노드에 파이썬 3.11 이상, 관리 대상 서버에도 비교적 최신 파이썬을 요구합니다. Rocky/CentOS 8의 기본 파이썬(3.6)을 대상 서버에서 그대로 쓰면 아래 오류가 납니다.
SyntaxError: future feature annotations is not defined
해결은 두 가지입니다. ① 대상 서버에 파이썬 3.9+를 설치하고 인벤토리에 ansible_python_interpreter로 지정하거나, ② 구형 서버가 많다면 낮은 버전의 ansible-core(예: 2.16 이하)를 씁니다. 저는 ①번으로 해결했습니다.
작업 폴더와 기본 설정 파일
작업 폴더를 만들고 그 안에 ansible.cfg를 두면 "이 프로젝트 전용 설정"이 됩니다.
mkdir -p ~/ansible-lab && cd ~/ansible-lab
cat > ansible.cfg <<'EOF'
[defaults]
inventory = inventory.ini
host_key_checking = False # 처음 접속 시 yes/no 질문 생략 (실서비스는 True 권장)
EOF
4. 인벤토리 만들기와 SSH 준비
인벤토리는 "누구에게 명령할지"를 적은 목록입니다.
# inventory.ini
[web]
web1 ansible_host=192.168.0.11
web2 ansible_host=192.168.0.12
[db]
db1 ansible_host=192.168.0.21
# web + db 를 묶어서 prod 그룹으로
[prod:children]
web
db
[all:vars]
ansible_user=ubuntu
ansible_python_interpreter=/usr/bin/python3
대상 서버로 비밀번호 없이 들어갈 수 있게 SSH 키를 먼저 배포합니다.
ssh-keygen -t ed25519 # 키가 없을 때만
ssh-copy-id ubuntu@192.168.0.11
ssh-copy-id ubuntu@192.168.0.12
ssh-copy-id ubuntu@192.168.0.21
연습만 해 보고 싶다면 서버가 없어도 됩니다. 내 컴퓨터 자신을 대상으로 삼으면 됩니다.
# inventory.ini (연습용 - 내 PC 자신)
[web]
web1 ansible_connection=local
이제 연결 테스트입니다. ping 모듈은 네트워크 ping이 아니라 "Ansible로 이 서버를 제어할 수 있는지" 확인합니다.
ansible all -m ping
web1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
pong이 나오면 준비 완료입니다.
5. 애드혹(ad-hoc) 명령 - 한 줄로 여러 서버 제어
플레이북을 쓰기 전에, 명령 한 줄로 즉석에서 쓰는 방법입니다. 형식은 ansible <대상> -m <모듈> -a "<인자>" 입니다.
# 모든 서버의 가동 시간 확인
ansible all -m command -a "uptime"
# web 그룹의 디스크 사용량
ansible web -m command -a "df -h /"
# 서버 정보(팩트) 수집 - OS 종류만 필터
ansible web -m setup -a "filter=ansible_distribution*"
# 관리자 권한(-b)으로 패키지 설치
ansible web -b -m package -a "name=htop state=present"
# 파일 배포
ansible web -b -m copy -a "src=./motd.txt dest=/etc/motd"
# 서비스 재시작
ansible web -b -m service -a "name=nginx state=restarted"
실제 실행 결과 예시입니다.
$ ansible web -m command -a "uptime"
web1 | CHANGED | rc=0 >>
06:15:20 up 59 days, 15:34, 0 users, load average: 1.61, 0.86, 0.81
$ ansible web -m setup -a "filter=ansible_distribution*"
web1 | SUCCESS => {
"ansible_facts": {
"ansible_distribution": "Rocky",
"ansible_distribution_major_version": "8",
"ansible_distribution_version": "8.10",
...
},
"changed": false
}
알아 둘 점: -b는 sudo로 권한 상승(become)한다는 뜻입니다. command 모듈은 파이프(|)나 리다이렉트(>)를 못 쓰므로 필요하면 shell 모듈을 쓰되, 가능하면 전용 모듈이 더 안전합니다. 또 uptime처럼 상태를 바꾸지 않는 명령도 CHANGED로 표시되는데, command 모듈은 변경 여부를 모르기 때문이니 정상입니다.
6. 플레이북 첫 작성 - 그리고 멱등성
애드혹은 일회성이고, 반복할 일은 플레이북에 적습니다. 첫 예제입니다. 디렉터리를 만들고, 설정 한 줄을 보장하고, 템플릿으로 파일을 만듭니다.
# templates/motd.j2
서버 이름: {{ ansible_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
환경: {{ env_name }}
# site.yml
---
- name: 데모 - 디렉터리, 파일, 템플릿
hosts: web
vars:
env_name: demo
demo_dir: /tmp/ans/out
tasks:
- name: 디렉터리 만들기
ansible.builtin.file:
path: "{{ demo_dir }}"
state: directory
mode: "0755"
- name: 설정 파일 한 줄 보장
ansible.builtin.lineinfile:
path: "{{ demo_dir }}/app.conf"
line: "max_connections=100"
create: true
- name: 템플릿으로 안내 파일 생성
ansible.builtin.template:
src: templates/motd.j2
dest: "{{ demo_dir }}/motd.txt"
notify: 알림 출력
handlers:
- name: 알림 출력
ansible.builtin.debug:
msg: "motd가 바뀌어서 핸들러 실행!"
실행합니다.
ansible-playbook site.yml
TASK [Gathering Facts] *****
ok: [web1]
TASK [디렉터리 만들기] *****
changed: [web1]
TASK [설정 파일 한 줄 보장] *****
changed: [web1]
TASK [템플릿으로 안내 파일 생성] *****
changed: [web1]
RUNNING HANDLER [알림 출력] *****
ok: [web1] => {
"msg": "motd가 바뀌어서 핸들러 실행!"
}
PLAY RECAP *****
web1 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
같은 걸 한 번 더 실행하면? (멱등성)
ansible-playbook site.yml
TASK [디렉터리 만들기] *****
ok: [web1]
TASK [설정 파일 한 줄 보장] *****
ok: [web1]
TASK [템플릿으로 안내 파일 생성] *****
ok: [web1]
PLAY RECAP *****
web1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
changed=0입니다. 이미 원하는 상태라서 아무것도 하지 않았고, 핸들러도 실행되지 않았습니다. 이게 Ansible의 가장 큰 장점입니다. "몇 번을 돌려도 안전하다"는 뜻이라서, 서버 설정이 바뀌었는지 확인하는 용도로 주기적으로 돌려도 됩니다.
실행 전 미리 보기: --check --diff
실서버에 적용하기 전에 "무엇이 바뀔지"만 보는 안전장치입니다. 누군가 설정을 손으로 바꿔 놓은 상황으로 시험해 봤습니다.
echo "max_connections=5" > out/app.conf # 누가 손으로 바꿔 둠
ansible-playbook site.yml --check --diff
TASK [설정 파일 한 줄 보장] *****
--- before: /tmp/ans/out/app.conf (content)
+++ after: /tmp/ans/out/app.conf (content)
@@ -1 +1,2 @@
max_connections=5
+max_connections=100
changed: [web1]
실제로 파일을 바꾸지 않고 "이 줄이 추가될 예정"이라고 보여 줍니다. 참고로 lineinfile은 기존 max_connections=5 줄을 교체하지 않고 새 줄을 추가하려 했습니다. 값을 교체하려면 regexp: '^max_connections='를 함께 쓰세요. 이렇게 --check로 먼저 보면 이런 의도 차이를 미리 잡을 수 있습니다.
7. 실전 예제 ① 웹서버(nginx) 한 번에 구축
가장 많이 쓰는 형태입니다. 패키지 설치 → 설정 배포 → 서비스 시작 → 방화벽 개방을 한 파일로 처리합니다.
# templates/nginx.conf.j2
server {
listen {{ site_port }};
server_name _;
root /usr/share/nginx/html;
index index.html;
}
# nginx.yml
---
- name: 웹서버 표준 구성
hosts: web
become: true
vars:
packages: [nginx, git, vim-enhanced]
site_port: 80
tasks:
- name: 패키지 설치
ansible.builtin.package:
name: "{{ packages }}"
state: present
- name: nginx 설정 배포
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/conf.d/mysite.conf
validate: "nginx -t -c /etc/nginx/nginx.conf"
notify: nginx 재시작
- name: 서비스 켜기 + 부팅 시 자동 시작
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: 방화벽 80 포트 열기
ansible.posix.firewalld:
service: http
permanent: true
immediate: true
state: enabled
when: ansible_os_family == "RedHat"
handlers:
- name: nginx 재시작
ansible.builtin.service:
name: nginx
state: restarted
ansible-playbook nginx.yml --syntax-check # 문법 검사
ansible-playbook nginx.yml --check # 미리보기
ansible-playbook nginx.yml # 적용
참고로 validate는 설정 파일 전체를 검사하는 방식이라, 환경에 따라 조각 파일 검증이 맞지 않을 수 있습니다. 그럴 땐 validate 줄을 빼고 nginx -t를 별도 작업으로 두세요. 위 플레이북은 문법 검사(--syntax-check)까지 통과를 확인했습니다.
package모듈은 apt/dnf를 자동으로 골라 줍니다. 배포판이 섞여 있어도 한 파일로 처리됩니다.notify+핸들러는 설정 파일이 실제로 바뀐 때만 nginx를 재시작합니다.enabled: true는 서버를 재부팅해도 nginx가 자동으로 켜지게 합니다.when으로 RHEL 계열에서만 방화벽 작업을 실행합니다. Ubuntu라면community.general.ufw를 쓰면 됩니다.
주의: ansible.posix.firewalld 같은 모듈은 ansible.posix 컬렉션에 들어 있습니다. ansible-core만 설치했다면 couldn't resolve module/action 'ansible.posix.firewalld' 오류가 나는데, 아래 한 줄로 해결됩니다. (전체 ansible 패키지는 기본 포함)
ansible-galaxy collection install ansible.posix
8. 실전 예제 ② 사용자 계정·SSH 키 일괄 등록
새 팀원이 들어오거나 서버가 늘 때마다 계정을 손으로 만드는 일, 정말 번거롭죠. 이름만 목록에 추가하고 다시 돌리면 끝입니다.
# users.yml
---
- hosts: all
become: true
vars:
admins: [alice, bob, carol]
tasks:
- name: 관리자 계정 생성
ansible.builtin.user:
name: "{{ item }}"
groups: wheel # Ubuntu 는 sudo
shell: /bin/bash
loop: "{{ admins }}"
- name: SSH 공개키 등록
ansible.posix.authorized_key:
user: "{{ item }}"
key: "{{ lookup('file', 'keys/' + item + '.pub') }}"
loop: "{{ admins }}"
keys/alice.pub처럼 공개키 파일을 작업 폴더에 모아 두면, 서버 전체에 계정과 키가 동일하게 배포됩니다. 퇴사자는 state: absent로 지우면 됩니다.
9. 실전 예제 ③ 패키지 업데이트와 서비스 재시작
보안 업데이트를 전 서버에 적용하고, 재부팅이 필요하면 재부팅까지 하는 운영 작업입니다. 한 번에 모든 서버를 내리지 않도록 serial로 나눠서 진행하는 것이 포인트입니다.
# update.yml
---
- hosts: web
become: true
serial: 1 # 서버를 한 대씩 순서대로 (롤링)
tasks:
- name: 전체 패키지 업데이트 (RHEL 계열)
ansible.builtin.dnf:
name: "*"
state: latest
when: ansible_os_family == "RedHat"
- name: 전체 패키지 업데이트 (Debian 계열)
ansible.builtin.apt:
upgrade: dist
update_cache: true
when: ansible_os_family == "Debian"
- name: 재부팅 필요 여부 확인 (RHEL)
ansible.builtin.command: needs-restarting -r
register: reboot_check
failed_when: false
changed_when: reboot_check.rc == 1
when: ansible_os_family == "RedHat"
- name: 필요하면 재부팅
ansible.builtin.reboot:
when: reboot_check.rc | default(0) == 1
register로 명령 결과를 변수에 담고, when으로 다음 작업의 실행 여부를 정하는 패턴입니다. 처음에는 꼭 --check와 서버 1~2대(--limit web1)로 시험하세요.
ansible-playbook update.yml --limit web1 # web1 한 대만
ansible-playbook update.yml --check # 미리보기
10. 변수·반복·조건·핸들러·템플릿 한눈에 정리
- 변수
vars-{{ 변수명 }}으로 사용. 서버 그룹별 값은group_vars/web.yml같은 파일에 분리합니다. - 반복
loop- 같은 작업을 목록 항목마다 실행. 아래처럼 파일 3개를 한 번에 만들 수 있습니다. - 조건
when- 조건이 참일 때만 실행. OS 종류, 변수 값, 이전 결과 등을 쓸 수 있습니다. - 핸들러
handlers- 변경이 있을 때만 실행하는 후속 작업(재시작 등). - 템플릿
template- Jinja2 문법으로 서버마다 다른 값이 들어가는 설정 파일을 만듭니다. - 팩트
ansible_*- 서버 OS, IP, 메모리 같은 정보를 Ansible이 자동 수집해 변수로 줍니다.
- hosts: web
tasks:
- name: 파일 3개 만들기
ansible.builtin.file:
path: "/tmp/ans/out/{{ item }}.txt"
state: touch
loop: [a, b, c]
- name: 조건부 실행
ansible.builtin.debug:
msg: "RHEL 계열입니다"
when: ansible_os_family == "RedHat"
TASK [파일 3개 만들기] *****
changed: [web1] => (item=a)
changed: [web1] => (item=b)
changed: [web1] => (item=c)
TASK [조건부 실행] *****
ok: [web1] => {
"msg": "RHEL 계열입니다"
}
11. 비밀번호는 Vault로 암호화
DB 비밀번호나 API 키를 플레이북에 평문으로 적어 Git에 올리면 큰일 납니다. Ansible에는 Vault라는 암호화 기능이 내장되어 있습니다.
# 비밀 변수 파일을 암호화해서 생성
ansible-vault create group_vars/all/vault.yml
# (편집기에서) db_password: "아주긴비밀번호"
# 내용 수정 / 확인
ansible-vault edit group_vars/all/vault.yml
ansible-vault view group_vars/all/vault.yml
# 실행할 때 암호를 물어보게 함
ansible-playbook site.yml --ask-vault-pass
암호화된 파일은 $ANSIBLE_VAULT;1.1;AES256... 형태로 저장되어 Git에 올려도 안전합니다. 단, Vault 암호 자체는 Git에 올리지 마세요.
12. 자주 막히는 문제와 해결
- UNREACHABLE! Permission denied (publickey) - SSH 키가 배포되지 않았거나
ansible_user가 틀림. 먼저ssh 사용자@서버가 되는지 확인하세요. - Missing sudo password - become에 비밀번호가 필요한 상태.
-K(--ask-become-pass)를 붙이거나, 대상 서버에서 NOPASSWD sudo를 설정하세요. - future feature annotations is not defined - 대상 서버 파이썬이 너무 낮음(3.6 등). 파이썬 설치 후
ansible_python_interpreter지정. - couldn't resolve module/action - 컬렉션 누락.
ansible-galaxy collection install 컬렉션명. - YAML 들여쓰기 오류 - 탭 대신 공백 2칸으로 통일.
ansible-playbook x.yml --syntax-check로 먼저 점검. - 항상 changed로 나오는 작업 -
command/shell은 변경 여부를 모릅니다.creates:,changed_when:을 쓰거나 전용 모듈로 바꾸세요. - 어디서 실패했는지 모르겠다 -
-v,-vvv옵션으로 자세한 로그를 볼 수 있습니다.
13. 디렉터리 구조 추천과 마무리
프로젝트가 커지면 파일을 이렇게 나눠 두면 관리가 편합니다. 그리고 이 폴더 전체를 Git에 넣으세요.
ansible-lab/
├── ansible.cfg
├── inventory.ini
├── group_vars/
│ ├── all/vault.yml # 암호화된 비밀 값
│ └── web.yml # web 그룹 공통 변수
├── templates/
│ └── nginx.conf.j2
├── keys/
│ └── alice.pub
├── site.yml # 전체를 호출하는 메인 플레이북
├── nginx.yml
├── users.yml
└── update.yml
체크리스트
- 컨트롤 노드에만 Ansible 설치 (파이썬 3.11+ 권장, 가상환경 사용)
- 대상 서버에 SSH 키 배포,
ansible all -m ping으로 확인 - 처음엔 애드혹 명령으로 감 잡기 → 반복되면 플레이북으로
- 적용 전
--syntax-check→--check --diff→ 소수 서버(--limit) → 전체 순서 - 비밀 값은 Vault로, 폴더 전체는 Git으로 관리
- 모듈은
command보다 전용 모듈(package, service, template 등) 우선
Ansible의 매력은 "서버 설정이 기억이 아닌 코드로 남는다"는 점입니다. 홈서버를 운영하다 보면 어느 서버에 무슨 설정을 했는지 헷갈리는 순간이 오는데, 그때부터 반복 작업을 플레이북으로 옮기면 훨씬 편해집니다. 처음에는 자기 PC를 대상으로 ansible_connection=local 연습부터 해 보세요. 한 번 익히면 서버가 몇 대든 두렵지 않습니다.
공식 문서: docs.ansible.com (모듈별 예제가 풍부합니다. 버전에 따라 내용이 바뀔 수 있습니다.)
댓글 0
글에 대한 질문이나 오류 제보는 문의하기로 보내 주세요.