Ansible 플레이북과 역할(role) 차이점 및 사용 시점
Ansible 플레이북과 역할의 구조적 차이를 설명합니다. 단일 파일로 충분한 경우와 재사용성을 위해 디렉터리 구조가 필요한 시점을 구분하고, 변수 우선순위와 관리 효율성을 높이는 기준을 정리했습니다.
Ansible 플레이북과 역할(role)의 차이점
Ansible 플레이북은 ansible-playbook으로 실행하는 파일입니다. 플레이북은 호스트 그룹과 수행해야 할 작업을 매핑합니다. Ansible 역할은 작업, 템플릿, 핸들러, 기본 변수를 포함하는 고정된 구조의 디렉터리이며, 플레이북에서 이름을 호출하여 사용합니다. 두 방식 내부의 작업 문법은 동일하므로, 무엇을 표현할 수 있는지의 문제가 아니라 재사용성의 문제입니다.
처음에는 단순한 플레이북으로 시작하십시오. 하나의 site.yml 안에 tasks: 목록을 담는 방식이 첫 자동화에 적합하며, 생각보다 훨씬 오랫동안 이 구조로 충분합니다. 동일한 작업 블록을 다른 호스트 그룹에도 실행해야 하거나, 파일이 약 100줄을 넘어가 스크롤만으로 작업을 찾기 어려워질 때 역할로 전환하십시오.
아직 작성해 본 적이 없다면, 단일 VPS를 대상으로 첫 플레이북 작성하기부터 시작하고 규모가 커지면 다시 돌아오십시오.
플랫 플레이북이 적절한 경우
작업이 일회성이거나 단일 호스트에서 수행되는 경우, 혹은 다른 사람이 읽을 일이 없는 경우에는 플랫 플레이북이 적절합니다. 단일 애플리케이션 서버를 프로비저닝하거나 유지보수 기간 전에 서버에 패치를 적용하는 작업은 디렉터리 트리를 구성할 필요가 없습니다. 역할(role)을 사용하면 7개의 디렉터리와 한 단계의 간접 참조가 추가됩니다. 호출하는 곳이 바로 옆에 있는 플레이북뿐이라면, 이러한 간접 참조는 아무런 이득 없이 실제 실행 내용을 확인할 때마다 이동해야 하는 번거로움만 더할 뿐입니다.
플랫 플레이북이 부적절해지는 시점은 명확하며, 쉽게 알아챌 수 있습니다. 작업 블록을 두 번째 플레이북으로 복사하는 순간이 바로 그 신호입니다. 그때부터 모든 수정 사항은 두 번 적용되어야 하며, 언젠가는 반드시 한쪽만 수정되는 실수가 발생하게 됩니다.
역할 디렉터리의 실제 구성 요소
roles/common/
defaults/main.yml
vars/main.yml
tasks/main.yml
handlers/main.yml
templates/99-hardening.conf.j2
files/
meta/main.ymltasks/main.yml는 진입점입니다. Ansible은 역할이 호출될 때 이 파일을 실행하며, 다른 모든 디렉터리는 선택 사항입니다.defaults/main.yml은 호출자가 재정의할 것으로 예상되는 변수를 담고 있습니다. Ansible에서 가장 낮은 우선순위를 가지므로, 거의 모든 다른 설정이 이보다 우선합니다.vars/main.yml은 호출자가 재정의하지 않을 변수를 담고 있습니다. 인벤토리보다 높은 우선순위를 가지며, 이는 매우 강력한 설정이므로 주의해서 사용해야 합니다.handlers/main.yml는notify에 의해 트리거되는 작업을 담고 있습니다. 핸들러는 이를 호출한 작업의 수와 관계없이 플레이가 끝날 때 한 번만 실행됩니다.files/는copy모듈에 의해 그대로 복사되는 파일을 담고 있으며,templates/은template모듈에 의해 렌더링되는 Jinja2 템플릿을 담고 있습니다. 역할 내부에서는 경로 없이 파일 이름만으로 참조할 수 있는데, 이는 Ansible이 역할의 디렉터리를 우선적으로 탐색하기 때문입니다.meta/main.yml은 역할 의존성과 Ansible Galaxy가 읽어 들이는 메타데이터를 선언합니다.
이 구조는 단순한 스타일 선호의 문제가 아닙니다. Ansible은 정확히 지정된 경로를 탐색하므로, roles/common/template/(단수형)에 템플릿을 넣으면 Ansible은 이를 찾지 못합니다.
ansible-galaxy init으로 공통 역할 빌드하기
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles common이 명령은 roles/common 아래에 전체 스켈레톤을 생성하며, 여기에는 사용하지 않을 디렉터리와 ---만 포함된 main.yml 스텁이 포함됩니다. 비어 있는 파일은 삭제하십시오. 빈 vars/main.yml 파일이 Ansible에 해를 끼치지는 않지만, 역할에서 실제로 중요한 파일이 무엇인지 파악하기 어렵게 만듭니다.
이제 작업을 수행할 파일을 채워 넣습니다. 역할의 공개 인터페이스인 defaults부터 작성하십시오.
# roles/common/defaults/main.yml
---
common_packages:
- ufw
- fail2ban
- unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no""no"와 "yes"는 따옴표로 감싸야 합니다. Ansible은 PyYAML을 사용하여 YAML을 파싱하는데, 따옴표 없는 no은 불리언 false로 읽히기 때문에 렌더링된 설정 줄이 PermitRootLogin False이 되어 sshd가 이를 거부하게 됩니다. 따옴표를 사용하면 해당 값을 문자열로 유지할 수 있습니다.
# roles/common/tasks/main.yml
---
- name: Install the base packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Create the admin group
ansible.builtin.group:
name: "{{ common_admin_group }}"
state: present
- name: Install the sshd hardening drop-in
ansible.builtin.template:
src: 99-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/99-hardening.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart sshd# roles/common/handlers/main.yml
---
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}Debian 및 Ubuntu에서 systemd 유닛 이름은 ssh이며, RHEL 계열 시스템에서는 sshd입니다. 잘못된 이름을 지정한 핸들러는 템플릿이 실제로 변경될 때만 실패하므로, 보통 몇 주가 지난 뒤에야 문제가 드러납니다.
validate 줄은 해당 작업에서 가장 유용한 부분입니다. Ansible은 템플릿을 임시 파일로 렌더링하고, %s 자리에 해당 파일 경로를 대입한 뒤 명령을 실행합니다. 대상 파일은 명령이 0을 반환할 때만 교체됩니다. 템플릿에 잘못된 지시어를 넣고 다시 실행해 보십시오. 작업은 failed to validate와 함께 실패하며, 실제 /etc/ssh/sshd_config.d/99-hardening.conf은 변경되지 않은 상태로 유지되므로 여전히 서버에 로그인할 수 있습니다. 이 검사는 단순히 구문 이상의 것을 테스트한다는 점을 유의하십시오. 만약 sshd -t가 호스트 키를 읽을 수 없으면 sshd: no hostkeys available -- exiting.를 반환하며, Ansible은 동일한 failed to validate을 보고합니다. 따라서 템플릿을 탓하기 전에 모듈의 msg을 먼저 읽어 보십시오.
플레이북에서 역할을 호출하는 방법
# site.yml
---
- name: Base configuration for every server
hosts: all
become: true
roles:
- common# inventory.ini
[local]
localhost ansible_connection=localansible-playbook -i inventory.ini site.yml플레이는 요약에서 failed=0로 끝나야 합니다. 호출 지점에서 확장된 형식을 사용하여 매개변수를 전달하십시오. 이것이 하나의 역할을 두 호스트 그룹에 사용하는 방법입니다.
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-password거의 모든 사람이 놀라는 순서 규칙이 하나 있습니다. 플레이에는 pre_tasks, roles, tasks, post_tasks가 포함될 수 있으며, Ansible은 파일에 작성한 순서와 상관없이 이 순서대로 실행합니다. tasks:을 roles: 위에 두어도 역할이 먼저 실행됩니다. 따라서 역할보다 먼저 수행되어야 하는 작업이 있다면, tasks:의 상단이 아니라 pre_tasks:에 배치해야 합니다.
- name: Ordering demonstration
hosts: local
gather_facts: false
pre_tasks:
- name: Runs first
ansible.builtin.debug:
msg: pre
roles:
- common
tasks:
- name: Runs after the role
ansible.builtin.debug:
msg: task
post_tasks:
- name: Runs last
ansible.builtin.debug:
msg: postroles: 키 대신 작업 목록 내부에서 역할을 호출하려면 import_role 또는 include_role를 사용하십시오.
tasks:
- name: Static, read when the playbook is parsed
ansible.builtin.import_role:
name: common
- name: Dynamic, resolved when the task runs
ansible.builtin.include_role:
name: postgres
when: "'db' in group_names"import_role은 정적입니다. Ansible은 구문 분석 시점에 역할을 읽고 해당 작업이 플레이의 일부가 됩니다. 따라서 ansible-playbook --list-tasks site.yml은 해당 작업을 나열하며, import에 적용된 태그는 내부의 모든 작업에 적용됩니다. include_role는 동적입니다. 작업이 실행될 때까지 아무것도 읽지 않으며, 이를 통해 변수나 루프를 사용하여 역할 이름을 지정할 수 있습니다. 그 대가로 해당 작업은 --list-tasks 및 --start-at-task에서 보이지 않게 됩니다.
여기에는 함정이 하나 있습니다. include_role 작업의 when:는 포함된 역할의 defaults/main.yml이 범위에 들어오기 전에 평가됩니다. include에 when: common_packages | length > 0을 작성하면 포함하려는 바로 그 역할에 해당 변수가 정의되어 있더라도 'common_packages' is undefined와 함께 실행이 중단됩니다. 해결 방법은 토글을 역할 밖으로 옮기는 것입니다. 모든 곳에서 범위가 지정되는 group_vars/all.yml에 배치하고, 역할이 직접 사용하는 값에 대해서는 역할의 기본값을 그대로 두십시오.
어떤 변수가 우선하는가: defaults, group_vars, vars, extra vars
Ansible은 20개가 넘는 변수 우선순위 단계를 정의합니다. 그중 4가지가 거의 모든 실무적 논쟁을 해결하며, 우선순위가 낮은 순서부터 높은 순서대로 나열하면 다음과 같습니다.
roles/<name>/defaults/main.yml은 우선순위가 가장 낮습니다. 다른 곳에서 설정한 거의 모든 값이 이를 덮어쓰므로, 역할(role)의 조정 가능한 설정값을 두기에 가장 적합한 위치입니다.group_vars/와host_vars/은 중간 단계에 위치합니다. 이곳은 사이트 고유의 설정값이 위치할 자리이며, 역할의 기본값을 깔끔하게 덮어씁니다.roles/<name>/vars/main.yml는host_vars보다 우선합니다. 이곳에 설정한 값은 인벤토리에서 덮어쓸 수 없습니다. 서비스 이름과 일치해야 하는 패키지 이름처럼, 역할 내부의 일관성을 유지하기 위해 필요한 항목을 위해 남겨두십시오.- 호출 시점에 전달된 역할 매개변수는
vars/main.yml보다 우선하며, 명령줄의-e은 역할 매개변수를 포함한 모든 것을 덮어씁니다.
이 우선순위가 결정되는 과정은 1분 내로 확인할 수 있습니다. 작은 역할에 기본값 하나와 역할 변수 하나를 설정한 뒤, host_vars에 동일한 이름의 변수를 설정해 보십시오.
# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults# roles/prec/vars/main.yml
---
prec_internal: from-rolevars# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars# roles/prec/tasks/main.yml
---
- name: Show which value survived
ansible.builtin.debug:
msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cli첫 번째 실행 결과는 tunable=from-hostvars internal=from-rolevars가 출력됩니다. 인벤토리가 역할의 기본값을 이겼지만, 역할 변수에는 패배했기 때문입니다. 두 번째 실행 결과는 internal=from-cli이 출력됩니다. extra vars는 최상위 우선순위를 가지며 하위 설정이 이를 밀어낼 수 없기 때문입니다. 이것이 바로 -e이 일회성 실행에는 적합하지만, 계속 유지해야 하는 스크립트에는 부적절한 이유입니다. 이는 저장소에 기록된 모든 신중한 결정을 아무런 경고 없이 무시해 버립니다.
운영 규칙은 다음과 같습니다. 값을 변경 가능하게 만들고 싶다면 defaults/에 두십시오. vars/에 값을 넣는 것은 해당 역할의 향후 사용자들에게 인벤토리로도 이 값을 변경할 수 없음을 알리는 행위입니다. 의도한 경우도 있겠지만, 대개는 실수일 가능성이 높습니다.
역할의 멱등성 증명: 두 번 실행하기
신뢰할 수 있는 Ansible 실행은 두 번째 실행 시 동일한 결과를 생성하며 변경된 사항이 없음을 보고해야 합니다. 플레이북을 두 번 실행하고 요약 결과를 확인하십시오.
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml두 번째 요약 결과는 다음과 같아야 합니다.
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0는 모든 모듈이 현재 상태를 검사했으며 작업이 이미 완료되었음을 확인했음을 의미합니다. 두 번째 실행에서 changed=2가 나타난다면 두 작업이 차이를 구분하지 못한다는 뜻이며, 이 경우 파일 재작성과 서비스 재시작이 무한히 반복됩니다. 흔한 원인은 command 또는 shell입니다. Ansible은 임의의 명령이 수행한 작업을 알 방법이 없기 때문입니다.
# traps.yml
---
- name: Command modules do not know what they changed
hosts: local
gather_facts: false
tasks:
- name: This appends a line on every run
ansible.builtin.shell: "echo run >> /tmp/grow.txt"
- name: This appends a line only once
ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
args:
creates: /tmp/guarded.txt해당 플레이북을 두 번 실행한 다음 wc -l /tmp/grow.txt /tmp/guarded.txt이 포함된 줄을 세어 보십시오. /tmp/grow.txt는 두 줄을 포함하고 /tmp/guarded.txt은 한 줄을 포함합니다. 두 번째 실행에서 보호된 작업은 전혀 실행되지 않았으며, 그 결과에는 skipped, since /tmp/guarded.txt exists 메시지가 포함됩니다. creates는 모듈이 먼저 찾아볼 수 있는 가시적인 결과물을 제공하기 때문입니다. 명령이 그러한 결과물을 남기지 않는다면, 출력을 등록하고 changed_when을 사용하여 직접 판단하십시오.
ansible-playbook --check --diff site.yml는 변경을 수행하지 않고 변경 사항을 예측하며, --diff는 템플릿이 재작성할 정확한 줄을 출력합니다. 한 가지 주의 사항을 염두에 두고 출력을 읽으십시오. shell 및 command 작업은 체크 모드에서 건너뛰므로, 깔끔해 보이는 계획이라도 실제 작업이 숨겨져 있을 수 있습니다.
Ansible에서 역할을 찾을 수 없다고 나오는 이유
Ansible은 플레이북 파일 옆에 있는 roles/ 디렉터리를 먼저 찾고, 그 다음 roles_path를 확인합니다. 탐색 경로는 사용자의 셸 위치가 아니라 플레이북의 위치를 기준으로 합니다.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely이 메시지는 site.yml와 roles/의 위치가 서로 어긋났음을 의미하며, Ansible은 시도했던 경로를 출력하여 도움을 줍니다. 두 파일을 같은 디렉터리에 두십시오. 플레이북 경로가 기준이 되므로 상위 디렉터리에서 실행하는 것은 문제가 없습니다.
ansible-playbook -i infra/inventory.ini infra/site.yml이와 유사하지만 더 조용하게 발생하는 문제가 있습니다. 디렉터리에 누구나 쓰기 권한(world writable)이 있는 경우, Ansible은 현재 디렉터리의 ansible.cfg를 무시합니다. 시스템의 다른 사용자가 해당 위치에 설정을 배치하여 실행 동작을 변경할 수 있기 때문입니다.
[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.이 경우 roles_path 및 inventory 설정이 조용히 무시되며, 역할과는 전혀 무관한 이유로 역할 탐색이 실패하게 됩니다. ansible --version는 실제로 로드된 config file을 출력하며, ansible-config dump --only-changed은 기본값과 다른 모든 설정을 출력합니다. 실행 결과가 설정 파일을 인식하지 못하는 것처럼 보일 때는 항상 이 두 명령어를 확인하십시오.
역할 공유: requirements.yml 및 버전 고정
다른 사람이 작성한 역할은 복사하지 않고 설치하여 사용합니다. 한 번만 선언하십시오.
# requirements.yml
---
roles:
- name: postgres
src: https://github.com/example/ansible-role-postgres
scm: git
version: v1.4.0ansible-galaxy install -r requirements.yml -p galaxy_roles항상 version을 설정하십시오. 이를 설정하지 않으면 명령을 실행하는 날의 기본 브랜치 버전을 가져오게 되며, 이로 인해 저장소의 변경 사항이 없더라도 지난달에는 작동하던 배포가 갑자기 실패할 수 있습니다. roles_path를 다운로드 디렉터리로 지정하고, 해당 디렉터리는 git에서 제외하십시오.
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_roles플레이북 옆의 roles/에 있는 역할은 여전히 정상적으로 인식됩니다. 해당 경로는 roles_path 외에도 항상 추가로 탐색되기 때문입니다. 따라서 직접 작성한 역할은 커밋하고 검토를 거치며, 타사 역할은 태그로 고정된 재현 가능한 다운로드 파일로 관리할 수 있습니다.
역할(role)이 해결책이 되지 못하는 경우
역할은 하나의 Ansible 실행 내에서 재사용을 위한 단위입니다. 역할은 클라우드 제공업체에서 서버나 DNS 레코드를 생성하지 않으며, 이를 강제로 수행하려 하면 플레이북은 유지보수가 불가능한 상태가 됩니다. 시작하기 전에 Ansible과 Terraform 간의 업무 분담에 관한 내용을 읽어보는 것이 좋습니다. 또한 역할은 인벤토리 설계를 대체하지 않습니다. 관리하는 서버가 수십 대를 넘어가면 서버를 그룹화하고 접근하는 방식이 태스크를 파일로 정리하는 방식보다 훨씬 중요해집니다.
이 common 역할이 설치하는 보안 강화 설정 역시 별도의 의사결정이 필요합니다. 위에서 제시한 설정은 두 가지 지시어만 포함하고 있으므로, 어떤 설정을 포함할지 결정하기 전에 실제로 변경할 가치가 있는 SSH 설정과 Ubuntu가 보안 업데이트를 자동으로 적용하게 만드는 방법을 먼저 확인하십시오.
FAQ
Ansible 플레이북을 언제 역할(role)로 변환해야 합니까?
동일한 작업 블록을 두 번째 플레이나 다른 호스트 그룹에 실행해야 할 때입니다. 플레이북 간에 작업을 복사하기 시작했다는 것은, 이후 수정 사항을 두 번 적용해야 하며 언젠가는 한쪽만 수정하게 될 위험이 있다는 신호입니다. 약 100줄 미만의 단일 플레이북이 특정 그룹 하나만을 대상으로 한다면 역할로 분리해도 얻을 이득이 없으며, 오히려 디렉터리 구조만 복잡해져 가독성이 떨어집니다.
역할은 같은 플레이 내의 작업보다 먼저 실행됩니까?
네. Ansible은 pre_tasks을 먼저 실행한 뒤 roles:에 나열된 모든 항목, tasks:, post_tasks: 순서로 실행하며, 파일 내 키의 작성 순서는 무시합니다. tasks:을 roles: 위에 작성한다고 해서 해당 작업이 먼저 실행되지는 않습니다. 역할보다 먼저 수행되어야 하는 작업이 있다면 pre_tasks:에 배치하십시오.
왜 group_vars 값이 역할을 덮어쓰지 못합니까?
변수가 defaults/main.yml이 아닌 역할의 vars/main.yml에 설정되어 있는지 확인하십시오. vars/는 Ansible 우선순위 체계에서 group_vars 및 host_vars보다 상위에 있으므로 인벤토리에서 이를 덮어쓸 수 없습니다. 변수를 우선순위 체계의 하단에 위치하며 호출자가 변경할 수 있는 올바른 위치인 defaults/main.yml로 옮기십시오. 우선순위 문제인지 오타인지 확인하려면 모든 소스보다 우선순위가 높은 -e name=value을 사용하여 한 번 실행해 보십시오.
왜 Ansible에서 역할을 찾을 수 없다고 나옵니까?
검색은 플레이북 파일 위치를 기준으로 시작되므로 site.yml과 roles/은 같은 디렉터리에 있어야 합니다. 오류 메시지에는 the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely와 같이 시도한 경로가 출력됩니다. 상위 디렉터리에서 플레이북을 실행해도 검색은 셸의 작업 디렉터리가 아닌 플레이북 경로를 따르므로 정상적으로 작동합니다. ansible.cfg에서 roles_path에 의존하는 경우, 해당 파일이 ansible --version로 로드되었는지 확인하십시오. 작업 디렉터리에 쓰기 권한이 모두에게 열려 있으면 Ansible이 해당 파일을 무시하기 때문입니다.
역할을 생성하려면 반드시 ansible-galaxy init을 사용해야 합니까?
아닙니다. 역할은 정해진 이름의 디렉터리 구조일 뿐이므로, mkdir -p roles/common/tasks과 tasks/main.yml만 있어도 작동하는 역할이 됩니다. ansible-galaxy init --init-path roles common를 사용하면 타이핑을 줄일 수 있고 meta/main.yml 및 README 스텁을 포함한 전체 골격을 제공받을 수 있습니다. 비어 있는 디렉터리는 삭제하십시오. 비어 있는 vars/main.yml은 역할 내에서 실제로 어떤 파일이 동작하는지 파악하기 어렵게 만듭니다.