Ansible 플레이북과 역할(role) 차이점 및 사용 시기
Ansible 플레이북과 역할의 구조적 차이를 설명합니다. 단순한 자동화에는 단일 플레이북이 유리하지만, 코드 재사용이 필요하거나 파일이 100줄을 넘어갈 때 왜 역할을 도입해야 하는지, 변수 우선순위와 디렉터리 구성 기준을 중심으로 상세히 정리했습니다.
Ansible 플레이북과 역할(role)의 차이점
Ansible 플레이북은 ansible-playbook 명령으로 실행하는 파일입니다. 플레이북은 호스트 그룹과 그 호스트들이 수행해야 할 작업을 매핑합니다. Ansible 역할은 작업, 템플릿, 핸들러, 기본 변수를 포함하는 고정된 구조의 디렉터리이며, 플레이북에서 이름을 호출하여 사용합니다. 두 방식 내부의 작업 문법은 동일하므로, 무엇을 구현할 수 있는지의 문제가 아닙니다. 이는 재사용성에 관한 문제입니다.
단순한 플레이북으로 시작하십시오. tasks: 목록을 포함하는 하나의 site.yml은 첫 자동화를 구성하기에 적합한 형태이며, 대부분의 예상보다 더 오랫동안 유효합니다. 동일한 작업 블록을 다른 호스트 그룹에 적용해야 하거나, 파일이 약 100줄을 넘어가 스크롤만으로 작업을 찾기 어려워질 때 역할로 전환하십시오.
아직 작성해 본 적이 없다면, 단일 VPS를 대상으로 첫 플레이북 작성하기를 먼저 수행하고, 파일이 커지기 시작할 때 다시 돌아오십시오.
단일 플레이북이 적절한 선택인 경우
작업이 일회성이거나 단일 호스트에서 수행되는 경우, 또는 다른 사람이 해당 코드를 읽을 일이 없다면 단일 플레이북을 사용하는 것이 적절합니다. 단일 애플리케이션 서버를 프로비저닝하거나 유지보수 기간 전에 서버에 패치를 적용하는 작업은 디렉터리 구조를 복잡하게 만들 필요가 없습니다. 역할(role)을 도입하면 7개의 디렉터리가 추가되고 간접 참조 계층이 하나 더 생깁니다. 호출하는 곳이 바로 옆에 있는 플레이북뿐이라면, 이러한 간접 참조는 아무런 이득을 주지 못하며 실제 실행 내용을 확인하려 할 때마다 파일을 이동해야 하는 번거로움만 발생시킵니다.
단일 플레이북이 부적절해지는 시점은 명확하며, 이를 알아차리기는 쉽습니다. 작업 블록을 복사하여 두 번째 플레이북에 붙여넣는 순간이 바로 그 신호입니다. 그 시점부터 모든 수정 사항은 두 번씩 적용되어야 하며, 언젠가는 반드시 한쪽만 수정되는 실수가 발생하게 됩니다.
역할(role) 디렉터리의 실제 구성 요소
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에 의해 트리거되는 작업을 담고 있습니다. 핸들러는 알림을 보낸 작업의 횟수와 관계없이 플레이(play)가 끝날 때 한 번만 실행됩니다.files/는copy모듈에 의해 그대로 복사되는 파일을 담고 있으며,templates/은template모듈에 의해 렌더링되는 Jinja2 템플릿을 담고 있습니다. 역할 내부에서는 경로 없이 파일 이름만으로 참조하는데, 이는 Ansible이 역할의 자체 디렉터리를 먼저 검색하기 때문입니다.meta/main.yml은 역할 의존성과 Ansible Galaxy가 읽는 메타데이터를 선언합니다.
이 구조는 단순한 스타일 선호의 문제가 아닙니다. Ansible은 정확히 지정된 경로를 찾으므로, roles/common/template/(단수형)에 넣은 템플릿은 절대 찾을 수 없습니다.
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 실행 결과는 두 번째 실행 시 동일한 결과를 생성하며 변경된 사항이 없음을 보고해야 합니다. 플레이북을 두 번 실행하고 요약(recap)을 확인하십시오.
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은 한 줄을 포함합니다. 두 번째 실행에서 보호된(guarded) 작업은 전혀 실행되지 않았으며, 그 결과에는 skipped, since /tmp/guarded.txt exists 메시지가 포함됩니다. creates는 모듈이 먼저 찾아볼 수 있는 가시적인 결과물을 제공하기 때문입니다. 명령이 그러한 결과물을 남기지 않는다면, 출력을 등록(register)하고 changed_when을 사용하여 직접 판단하십시오.
ansible-playbook --check --diff site.yml는 변경 사항을 실제로 적용하지 않고 예측하며, --diff는 템플릿이 다시 작성할 정확한 줄을 출력합니다. 출력을 읽을 때 한 가지 주의할 점이 있습니다. shell 및 command 작업은 체크 모드에서 건너뛰므로, 깔끔해 보이는 계획이라도 실제 작업이 숨겨져 있을 수 있습니다.
요약의 또 다른 열도 주의 깊게 살펴봐야 합니다. Ansible이 연결할 수 없는 호스트는 failed가 아닌 unreachable로 집계되며, 해당 호스트의 작업은 전혀 실행되지 않습니다. 따라서 이 역할을 두 대 이상의 장비에 적용하기 전에 연결할 수 없는 호스트 하나가 전체 실행을 중단시킬지 미리 결정하십시오.
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는 역할 내에서 실제로 어떤 파일이 동작하는지 파악하기 어렵게 만들기 때문입니다.