SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-21

Ansible 템플릿과 핸들러 사용법 및 예제

Jinja2 템플릿으로 nginx 설정을 렌더링하고 핸들러를 통해 변경 시에만 서비스를 재시작하는 방법을 설명합니다. 플레이북을 두 번 실행하여 멱등성을 검증하고, Ansible 설정 파일 구성부터 실제 적용까지 단계별로 안내합니다.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 20, 2026.

Ansible 템플릿과 핸들러가 첫 플레이북에 더해주는 것

Ansible 템플릿과 핸들러는 정적인 플레이북을 유용한 도구로 바꿔주는 두 가지 요소입니다. 템플릿은 변수를 사용하여 설정 파일을 렌더링하므로, 하나의 파일로 모든 호스트를 처리할 수 있습니다. 핸들러는 작업이 실제로 변경 사항을 발생시켰을 때만 실행됩니다. 따라서 설정이 실제로 변경되었을 때만 서비스를 다시 불러오고, 그 외의 경우에는 서비스를 그대로 둡니다.

이 가이드는 VPS에서 첫 Ansible 플레이북 만들기에서 이어집니다. 이미 패키지를 설치하고 서비스를 시작하는 플레이를 가지고 있을 것입니다. 아래의 모든 내용은 로컬 연결을 통해 localhost를 대상으로 실행되므로 한 대의 머신에서 진행됩니다. 이 과정을 따라 하기 위해 두 번째 서버는 필요하지 않습니다. 동일한 플레이를 실제 인벤토리 호스트에 실행해도 작업 내용은 변경되지 않으며, 마지막 섹션에서 변경되는 부분을 다룹니다.

작업 디렉터리 설정

sudo apt update
sudo apt install -y ansible nginx
ansible --version
mkdir -p ~/ansible-templates/templates
cd ~/ansible-templates

nginx를 사용하는 이유는 설정 파일과 리로드 명령을 갖춘 실제 서비스이기 때문이며, 이는 예제에 필요한 모든 요건을 충족합니다. ansible --version는 ansible-core 버전과 사용할 Python 인터프리터를 출력합니다. 두 정보를 모두 확인하십시오. 아래 플레이북은 ansible.builtin.template과 같이 완전히 규정된 모듈 이름을 사용하는데, 이는 Ansible 2.10 이상이 필요하며 현재 배포판 패키지는 모두 이 버전을 상회합니다.

inventory.ini을 생성하십시오:

[local]
localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"

ansible_connection=local은 Ansible이 SSH 세션을 여는 대신 각 작업을 로컬 프로세스로 실행하도록 지시합니다. 두 번째 설정은 장식이 아닙니다. 인벤토리 파일에 localhost를 작성하면 일반 호스트가 되며, Ansible이 암시적 localhost에 무료로 제공하는 인터프리터를 잃게 됩니다. 따라서 인터프리터 탐색으로 넘어가며 플레이를 실행 중인 Python과 다른 Python을 선택할 수 있습니다. ansible_playbook_python은 현재 ansible-playbook을 실행 중인 인터프리터이며, 이를 통해 둘의 일관성을 유지합니다.

ansible.cfg를 생성하십시오:

[defaults]
inventory = inventory.ini

이 파일이 없으면 모든 명령마다 -i inventory.ini을 전달해야 합니다. 인벤토리가 전혀 없으면 Ansible은 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'를 출력하며, hosts: all가 포함된 플레이는 아무것도 매칭하지 못합니다. ansible.cfg에 관해 한 가지 더 알아둘 점은, 전 세계 쓰기 권한이 있는 디렉터리에 위치할 경우 Ansible이 이를 무시한다는 것입니다. 따라서 프로젝트를 홈 디렉터리 아래에 유지하십시오. 인벤토리 파일은 호스트 목록 그 이상을 담습니다, 이것이 그 역할을 수행하는 가장 작은 단위의 파일입니다.

template와 copy의 차이점 및 올바른 사용 시점

ansible.builtin.copy은 파일을 있는 그대로 전송합니다. ansible.builtin.template은 파일을 먼저 Jinja2로 처리한 뒤 그 결과를 전송합니다. 모듈 소스에서는 template를 "액션 플러그인으로 완전히 구현되어 컨트롤러에서 실행되는 가상 모듈"이라고 설명하며, 이는 기억해야 할 중요한 결과를 낳습니다. 즉, 렌더링은 ansible-playbook을 입력한 머신에서 수행됩니다. 대상 호스트는 사용자의 변수를 전혀 볼 수 없으며, Jinja2가 설치되어 있을 필요도 없습니다.

모든 호스트에서 파일이 동일하다면 copy을 사용하십시오. 호스트마다 값이 하나라도 다르거나 {% for %} 루프 또는 {% if %} 블록이 필요하다면 즉시 template를 사용하십시오. copy에도 content: 매개변수가 있으며, 그 내부의 변수는 다른 작업 인수와 마찬가지로 치환되지만 루프나 조건문은 사용할 수 없습니다. 따라서 구조가 필요한 모든 작업은 템플릿으로 처리해야 합니다. 두 모듈은 동일한 문서 조각을 불러오므로 파일 옵션도 동일하게 취급하며, 따라서 owner, group, mode, backupvalidate은 각 모듈에서 동일하게 동작합니다.

템플릿 작성: 변수 하나, 루프 하나

이 내용을 templates/app.conf.j2로 저장하십시오:

# {{ ansible_managed }}
upstream {{ app_name }}_backend {
{% for backend in app_backends %}
    server {{ backend.host }}:{{ backend.port }} weight={{ backend.weight }};
{% endfor %}
}

server {
    listen {{ app_listen_port }};
    server_name {{ app_server_name }};

    location / {
        proxy_pass http://{{ app_name }}_backend;
        proxy_set_header Host $host;
    }
}

여기서는 두 가지 종류의 Jinja2 태그가 작동합니다. {{ ... }}은 표현식이며 해당 값을 출력합니다. {% ... %}는 구문이며 자체적으로는 아무것도 출력하지 않습니다. app_backends는 딕셔너리 목록이므로 backend.host은 각 항목에서 하나의 키를 읽어오며, 루프는 정의한 개수만큼 server 줄을 작성합니다.

공백과 관련하여 다른 환경에서 Jinja2를 사용해 본 사람들이 놀라는 세부 사항이 하나 있습니다. Ansible은 기본적으로 trim_blocksyes로 설정하는데, Jinja2 자체는 그렇지 않으므로 {% ... %} 태그 바로 뒤의 줄바꿈은 제거되며 루프 뒤에 빈 줄이 남지 않습니다. Ansible은 lstrip_blocksno로 유지하므로 {% 태그 앞에 넣은 공백은 그대로 유지되어 렌더링된 파일에 나타납니다. 출력물에 원치 않는 들여쓰기가 포함된다면 템플릿 작업에서 lstrip_blocks: true를 설정하십시오.

{{ ansible_managed }}는 기본적으로 리터럴 텍스트 Ansible managed으로 렌더링됩니다. 그대로 두십시오. 사람들은 종종 날짜를 포함하기 위해 ansible.cfg에서 ansible_managed을 재정의하곤 하는데, 그렇게 하는 순간 렌더링된 파일은 실행할 때마다 달라지고, 작업은 매번 변경 사항이 있다고 보고하며, 서비스는 매번 다시 로드됩니다. 그 설정 하나가 이 가이드의 나머지 부분에서 다루는 속성을 파괴합니다. .j2 확장자는 관례일 뿐이며, Ansible은 이를 확인하지 않습니다.

플레이북

이 내용을 site.yml으로 저장하십시오:

- name: Render an nginx site from a template
  hosts: local
  become: true

  vars:
    app_name: learn
    app_listen_port: 8080
    app_server_name: learn.example.com
    app_backends:
      - host: 127.0.0.1
        port: 9001
        weight: 3
      - host: 127.0.0.1
        port: 9002
        weight: 1

  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Render the site configuration
      ansible.builtin.template:
        src: templates/app.conf.j2
        dest: "/etc/nginx/conf.d/{{ app_name }}.conf"
        owner: root
        group: root
        mode: '0644'
        backup: true
      notify: nginx config changed

    - name: Make sure nginx is enabled and running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Test the nginx configuration
      ansible.builtin.command:
        cmd: /usr/sbin/nginx -t
      changed_when: false
      listen: nginx config changed

    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded
      listen: nginx config changed

mode: '0644'은 의도적으로 따옴표로 묶었습니다. 파일 옵션 문서에 따르면 8진수를 따옴표로 묶어야 "Ansible이 문자열로 전달받아 직접 숫자 형식으로 변환"할 수 있습니다. 따옴표를 생략하면 YAML 파서가 0644를 일반 숫자로 읽어 들여 의도하지 않은 권한이 설정될 수 있습니다.

notify: nginx config changed은 핸들러가 아닌 토픽을 지정합니다. 두 핸들러 모두 listen: nginx config changed를 포함하므로, 하나의 notify 호출로 두 핸들러가 모두 실행됩니다. 나중에 동일한 listen 줄을 가진 세 번째 핸들러를 추가해도 템플릿 태스크를 수정할 필요가 없습니다. cache_valid_time: 3600은 한 시간 내에 두 번째 실행이 발생할 경우 패키지 미러로 다시 연결되는 것을 방지합니다.

한 번 실행한 뒤 출력 내용을 확인하십시오

ansible-playbook site.yml

sudo 사용 시 암호를 묻는다면 -K을 추가하십시오. 그러면 Ansible이 암호를 입력하라는 메시지를 표시합니다.

먼저 작업별 출력 줄을 읽고, 하단의 PLAY RECAP을 확인하십시오. 각 작업은 Ansible이 변경을 수행했을 때 changed:를, 호스트가 이미 원하는 상태일 때 ok:을 출력하며, 요약 부분은 호스트별로 해당 카운트를 합산합니다. 플레이의 모든 작업이 완료된 직후에만 RUNNING HANDLER [Test the nginx configuration]과 그 뒤를 잇는 RUNNING HANDLER [Reload nginx]가 표시됩니다.

이제 출력 결과만 신뢰하지 말고 시스템 상태를 직접 확인하십시오:

sudo cat /etc/nginx/conf.d/learn.conf
sudo /usr/sbin/nginx -t
curl -sI http://127.0.0.1:8080/

nginx -t은 구성 파일이 정상적으로 구문 분석되면 nginx: configuration file /etc/nginx/nginx.conf test is successful를 출력합니다. curl는 nginx의 상태 줄을 반환하며, 서버 블록이 활성화되어 있고 9001번이나 9002번 포트에서 대기 중인 서비스가 없으므로 502 Bad Gateway이 올바른 응답입니다. sudo tail /var/log/nginx/error.log은 그 이유를 명확하게 설명합니다: connect() failed (111: Connection refused) while connecting to upstream.

멱등성을 확인하기 위해 두 번째 실행

ansible-playbook site.yml

이번 실행이 핵심이므로 첫 번째 실행 결과와 한 줄씩 비교합니다. 템플릿 작업은 이제 changed: 대신 ok:를 출력해야 하며, 핸들러는 출력 어디에도 나타나지 않아야 합니다.

이 메커니즘은 단순하지만 디버깅을 위해 반드시 알아두어야 합니다. template은 컨트롤러에서 파일을 렌더링한 뒤, 그 결과의 체크섬과 이미 dest에 존재하는 파일의 체크섬을 비교합니다. 내용, 소유권, 모드가 일치하면 수행할 작업이 없으므로 작업은 ok을 보고합니다. 따라서 notify는 실행되지 않으며 핸들러도 작동하지 않습니다. 핸들러는 changed 상태일 때만 작동합니다.

반대 방향도 증명해 봅니다. vars에서 weight: 3weight: 1로 변경한 뒤 플레이를 다시 실행하면, 템플릿 작업은 changed를 보고하고 두 핸들러가 모두 실행되며 sudo cat /etc/nginx/conf.d/learn.conf에 새로운 값이 표시됩니다.

두 번째 동일한 실행에서도 여전히 변경 사항이 보고된다면 렌더링이 안정적이지 않은 것입니다. 출력 내용 중 시간 기반 요소가 있는지 먼저 확인하십시오. 이는 흔한 원인이며, 사용자 정의 ansible_managed이 주로 문제를 일으킵니다. 그 후, 작업에 설정된 modeowner이 실제 디스크의 상태와 일치하는지 확인하십시오. 바이트가 동일하더라도 설정이 일치하지 않으면 변경으로 간주되기 때문입니다.

변경 사항을 적용하기 전에 확인하기

ansible-playbook site.yml --check --diff

--check는 호스트를 변경하지 않고 플레이를 실행합니다. --diff는 각 작업이 변경할 내용을 출력하며, template의 경우 렌더링된 결과와 디스크에 있는 파일 간의 줄 단위 차이점을 보여줍니다. 이 두 옵션을 함께 사용하면 실제 작업을 수행하지 않고도 "이 작업을 실행하면 어떤 결과가 나올지" 확인할 수 있습니다. 체크 모드에는 고유한 위험 요소가 있습니다. 주로 체크 모드에서 실제로 수행되지 않은 이전 작업의 결과에 의존하는 작업에서 문제가 발생합니다.

핸들러가 플레이 종료 시점까지 대기하는 이유

핸들러 문서에는 다음과 같이 명시되어 있습니다: "기본적으로 핸들러는 특정 플레이의 모든 작업이 완료된 후에 실행됩니다. 알림을 받은 핸들러는 다음 각 섹션이 끝난 후, 정해진 순서에 따라 자동으로 실행됩니다: pre_tasks, roles/taskspost_tasks."

그 이유는 일괄 처리(batching) 때문입니다. 하나의 서비스에 대해 4개의 설정 파일을 생성하는 플레이가 있다면, 4개의 파일이 모두 준비된 상태에서 마지막에 한 번만 서비스를 재시작하는 것이 효율적입니다. 파일 하나를 생성할 때마다 재시작하면 총 4번 재시작하게 되며, 그중 3번은 설정이 완료되지 않은 상태로 서비스를 로드하게 됩니다. 같은 문서에서는 다음과 같이 보장합니다: "동일한 핸들러에 여러 번 알림을 보내더라도, 몇 개의 작업이 알림을 보냈는지와 관계없이 핸들러는 단 한 번만 실행됩니다."

실행 순서 또한 고정되어 있습니다: "핸들러는 notify 문에 나열된 순서가 아니라, handlers 섹션에 정의된 순서대로 실행됩니다." 이것이 플레이북에서 Test the nginx configurationReload nginx보다 위에 위치하는 이유입니다. 테스트는 먼저 작성되었기 때문에 먼저 실행되며, notify 라인의 내용은 실행 순서에 영향을 주지 않습니다.

핸들러를 조기에 실행하는 방법 및 실패 후 실행하는 방법

때로는 동일한 플레이 내의 후속 작업에서 새로운 설정이 적용된 서비스를 미리 실행해야 할 경우가 있습니다. 이 시점에 meta 모듈을 사용하여 알림을 받은 핸들러를 즉시 실행하십시오. 문서에 따르면 이 모듈은 "지금까지 알림을 받은 모든 핸들러 작업을 Ansible이 실행하도록" 만듭니다.

    - name: Run the notified handlers now instead of at the end of the play
      ansible.builtin.meta: flush_handlers

    - name: Wait for the new listener to accept connections
      ansible.builtin.wait_for:
        host: 127.0.0.1
        port: 8080
        timeout: 10

meta 라인을 제거하면 nginx가 여전히 이전 설정을 서비스하는 동안 wait_for 작업이 실행됩니다. 첫 실행 시에는 8080 포트에서 대기 중인 프로세스가 없으므로, 해당 작업은 10초를 모두 대기한 후 실패하게 됩니다.

두 번째 경우는 실패 상황입니다. "작업이 핸들러에 알림을 보냈더라도 플레이 내의 다른 작업이 나중에 실패하면, 기본적으로 해당 호스트에서는 핸들러가 실행되지 않으며, 이로 인해 호스트가 예상치 못한 상태로 남을 수 있습니다." 따라서 설정을 렌더링한 후 관련 없는 작업에서 오류가 발생하면, 새로운 파일은 디스크에 기록되지만 실행 중인 서비스에는 이전 설정이 그대로 로드된 상태가 됩니다. 이를 해결하려면 명령줄에서 --force-handlers를 사용하거나 플레이 내에서 force_handlers: true을 설정하여 기본 동작을 재정의하십시오. 동일한 옵션이 ansible.cfg[defaults] 아래 force_handlers = True로 존재하며, 환경 변수 ANSIBLE_FORCE_HANDLERS로도 설정할 수 있습니다. 기본값은 False입니다.

핸들러 이름이 충돌하면 나중에 정의된 쪽이 무시됩니다

문서에는 다음과 같은 규칙이 명시되어 있습니다. "각 핸들러는 전역적으로 고유한 이름을 가져야 합니다. 동일한 이름으로 여러 핸들러가 정의된 경우, 플레이에 마지막으로 로드된 핸들러만 알림을 받고 실행될 수 있습니다." 역할(role) 내부에 정의된 핸들러도 해당 역할에만 국한되지 않습니다. 핸들러는 전체 플레이를 위한 하나의 전역 핸들러 목록에 삽입됩니다. 따라서 각각 Restart nginx을 정의하는 두 역할을 사용하면, 이름은 둘 중 하나로만 결정됩니다. 이때 알림을 보낸 역할이 아니라 로드 순서에 따라 결정됩니다.

이 규칙을 의존하기 전에 직접 테스트해 보십시오. 다음 내용을 handlers-dup.yml로 저장합니다.

- name: Two handlers, one name
  hosts: local
  gather_facts: false

  tasks:
    - name: Notify the duplicated name
      ansible.builtin.command:
        cmd: /bin/true
      changed_when: true
      notify: Duplicated handler

  handlers:
    - name: Duplicated handler
      ansible.builtin.file:
        path: /tmp/dup-first
        state: touch
        mode: '0644'

    - name: Duplicated handler
      ansible.builtin.file:
        path: /tmp/dup-second
        state: touch
        mode: '0644'
rm -f /tmp/dup-first /tmp/dup-second
ansible-playbook handlers-dup.yml
ls -l /tmp/dup-first /tmp/dup-second

플레이는 성공하며 RUNNING HANDLER [Duplicated handler]은 한 번만 나타나고, ls/tmp/dup-first에 대한 줄을 출력하고 다른 하나는 ls: cannot access '/tmp/dup-second': No such file or directory을 출력합니다. 실행된 핸들러는 마지막에 로드된 것이 아니라 처음에 작성된 것입니다. 이는 해당 문장이 예측하는 결과와 정반대입니다.

이 차이를 이해하는 것은 중요합니다. 문서의 규칙은 파일의 줄 단위가 아니라 핸들러 블록에 관한 것이기 때문입니다. 서로 다른 곳(예: 한 역할에서 다른 역할로)에서 전달된 핸들러는 별도의 블록으로 취급되며, 나중에 로드된 블록이 이전 블록을 가립니다(shadowing). 플레이 내의 일반적인 handlers: 목록은 하나의 단일 블록이며, 블록 내부의 검색은 위에서 아래로 진행되다가 일치하는 첫 번째 이름에서 멈춥니다. 따라서 하나의 파일 안에서는 첫 번째 정의가 응답하고 두 번째는 도달할 수 없게 되며, 역할 간에는 문서에 설명된 대로 가리기(shadowing)가 발생합니다. 어느 경우든 두 핸들러 모두에 도달할 수는 없으며, 이러한 방식은 설계의 기반으로 삼기에 적절하지 않습니다.

이를 해결하는 깔끔한 방법은 두 가지입니다. 모든 핸들러 이름에 역할별 접두사를 붙이거나, role_name : handler_name과 같이 정규화된 형식을 사용하여 알림을 보내는 것입니다. 문서는 이를 "역할 외부의 동일한 이름을 가진 핸들러가 아닌, 역할 내의 핸들러가 알림을 받도록 보장하는 방법"으로 제시합니다. 콜론 주변의 공백은 해당 문법의 일부입니다. 이는 직접 작성하지 않은 역할을 가져오기 시작하는 순간 실질적인 문제가 됩니다.

같은 페이지에 있는 또 다른 규칙은 다음과 같습니다. "핸들러 이름에 변수를 사용하지 마십시오. 핸들러 이름은 초기에 템플릿화되므로, Ansible은 이와 같은 핸들러 이름에 대한 값을 사용할 수 없을 수 있습니다." Restart {{ service_name }}라는 이름의 핸들러는 이름이 템플릿화되는 시점에 해당 변수가 정의되어 있지 않으면 전체 플레이를 실패하게 만듭니다. 핸들러 이름을 고정된 문자열로 유지하고 listen를 사용하여 그룹화하면 이러한 문제를 피할 수 있습니다.

validate: 손상된 렌더링 파일 설치 거부

validate은 Ansible이 렌더링된 파일을 최종 위치로 이동하기 전에 해당 파일에 대해 명령을 실행합니다. 공식 문서의 설명은 다음과 같습니다. "업데이트된 파일을 최종 목적지로 복사하기 전에 실행할 검증 명령입니다. 검증에는 임시 파일 경로가 사용되며, 아래 예시와 같이 %s을 반드시 포함해야 합니다. 또한, 명령은 안전하게 전달되므로 셸 확장이나 파이프와 같은 셸 기능은 작동하지 않습니다."

이 텍스트에서 두 가지 규칙이 도출됩니다. %s은 필수이며, 이를 포함하지 않는 validate 문자열은 validate must contain %s 오류와 함께 작업을 실패시킵니다. 또한 셸이 없으므로 파이프, 리다이렉션, globbing, &&은 작동하지 않습니다. 하나의 명령에 하나의 파일 인자만 사용할 수 있습니다.

공식 모듈 예제는 이 기능이 완벽하게 작동하는 두 가지 사례를 보여줍니다.

- name: Copy a new sudoers file into place, after passing validation with visudo
  ansible.builtin.template:
    src: /mine/sudoers
    dest: /etc/sudoers
    validate: /usr/sbin/visudo -cf %s

- name: Update sshd configuration safely, avoid locking yourself out
  ansible.builtin.template:
    src: etc/ssh/sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: '0600'
    validate: /usr/sbin/sshd -t -f %s
    backup: yes

두 예제 모두 각 검사기가 하나의 파일을 읽어 독자적인 기준으로 판단하기 때문에 정상 작동합니다. visudo -cf은 sudoers 파일을 읽고, sshd -t -f는 완전한 sshd_config을 읽습니다.

이 가이드에서 nginx 파일을 검증할 수 없는 이유

위의 템플릿 작업에 validate: /usr/sbin/nginx -t -c %s를 추가하면 작업이 실패합니다. 메시지에서 그 원인을 확인할 수 있습니다.

nginx: [emerg] "upstream" directive is not allowed here in <ansible temporary path>:2

nginx -t -ceventshttp 블록으로 시작하는 전체 설정을 기대합니다. 이 플레이가 렌더링하는 파일은 /etc/nginx/nginx.conf 내부의 include /etc/nginx/conf.d/*.conf;에 의해 http 블록으로 불러와지는 조각일 뿐입니다. 해당 컨텍스트를 제외하고 단독으로 보면 upstream은 잘못된 위치에 있는 지시어이므로, 실제 위치에서는 완전히 올바른 파일임에도 nginx는 이를 거부합니다. 검사 도구는 파일 조각을 전달받고 이를 전체 설정으로 처리하라는 요청을 받은 셈입니다.

실질적인 해결책은 이미 플레이북에 포함되어 있습니다. 조각 파일을 설치한 다음, 리로드 핸들러보다 앞서 정의된 핸들러에서 조립된 설정을 검사하십시오. 핸들러는 정의된 순서대로 실행되므로, nginx -t는 사용자의 조각이 포함된 실제 /etc/nginx/nginx.conf을 확인하게 됩니다. 여기서 실패가 발생하면 systemctl reload가 호출되기 전에 플레이가 중단됩니다. 비용에 대해 명확히 이해해야 합니다. 검사가 실패하면 디스크에는 이미 깨진 파일이 존재하게 되며, nginx는 누군가 다시 시작하기 전까지 마지막으로 로드된 설정을 계속 서비스합니다.

이것이 바로 backup: true가 필요한 이유입니다. 이 옵션은 덮어쓰기 전에 원본 파일의 복사본을 basename.PID.YYYY-MM-DD@HH:MM:SS~이라는 이름으로 생성하므로, 디렉터리에는 learn.conf.4127.2026-08-20@11:42:09~과 같은 항목들이 남게 됩니다. 변경 후 sudo ls -l /etc/nginx/conf.d/을 실행하면 해당 파일을 찾을 수 있습니다.

이러한 이름 지정 방식은 보이는 것보다 중요합니다. 메인 설정 파일은 conf.d/*.conf만 포함하고 백업 파일 이름은 물결표(tilde)로 끝나기 때문에 /etc/nginx/conf.d/에서는 백업 파일이 아무런 문제를 일으키지 않습니다. 하지만 *로 포함되는 디렉터리에서는 문제가 될 수 있으며, Debian 및 Ubuntu의 /etc/nginx/nginx.conf/etc/nginx/sites-enabled/*을 정확히 그런 방식으로 포함합니다. backup: true를 사용하여 sites-enabled에 템플릿을 생성하면 nginx는 백업 파일을 두 번째 라이브 서버 블록으로 로드하게 됩니다. 이것이 바로 이 플레이가 conf.d에 파일을 쓰는 이유입니다.

실제 인벤토리 호스트를 대상으로 플레이 실행하기

hosts: local을 사용하는 그룹 이름으로 변경하십시오. 플레이의 다른 부분은 수정할 필요가 없습니다. 템플릿은 호스트당 한 번씩 렌더링되므로, app_listen_portapp_backendsgroup_varshost_vars에서 가져올 수 있으며 템플릿 파일은 하나만 유지하면 됩니다. 이것이 바로 값을 파일에 직접 적지 않고 변수로 관리할 때 얻는 이점입니다.

두 가지 사항은 변경됩니다. become: true는 이제 각 대상 호스트에서 sudo 비밀번호를 요구하므로, 비밀번호 없는 sudo 설정이 되어 있지 않다면 -K을 추가해야 합니다. 또한 데이터베이스 비밀번호나 API 토큰과 같이 템플릿에 포함된 모든 비밀 정보는 커밋하는 파일 내에 일반 vars: 텍스트로 존재해서는 안 됩니다. Ansible Vault로 해당 값들을 암호화하고 현재와 동일하게 이름으로 참조하십시오. 템플릿은 변수가 어디에서 왔는지 신경 쓰지 않기 때문입니다.

플레이가 하나의 서비스를 넘어 확장되면 vars:, templates/, handlers:은 이미 이를 위한 표준 위치를 갖추고 있습니다. 이들을 해당 위치로 옮기는 것이 바로 플레이북과 역할을 분리하는 이유입니다.

FAQ

Ansible 핸들러가 실행되지 않는 이유는 무엇입니까?

거의 대부분 알림을 보내는 작업이 changed가 아닌 ok을 보고했기 때문입니다. 핸들러는 변경 사항이 발생할 때만 실행되므로, 템플릿 작업의 렌더링 결과가 이미 디스크에 있는 파일과 일치하면 아무것도 알리지 않습니다. 그 다음으로 네 가지를 확인하십시오. notify의 문자열은 핸들러 name 또는 listen 토픽과 대소문자 및 공백을 포함하여 정확히 일치해야 합니다. 해당 호스트에서 나중에 실패한 작업은 --force-handlers을 전달하지 않는 한 알림을 받은 핸들러를 억제합니다. 다른 플레이에 정의된 핸들러는 현재 플레이에서 볼 수 없습니다. 또한 when 조건에 의해 건너뛴 작업은 알림을 전혀 보내지 않습니다.

플레이북이 실행될 때마다 변경(changed)되었다고 보고하는 이유는 무엇입니까?

렌더링된 텍스트가 실행 간에 안정적이지 않기 때문입니다. 가장 흔한 원인은 출력에 포함된 타임스탬프이며, 날짜를 포함하는 사용자 지정 ansible_managed 문자열이 정확히 그런 결과를 낳습니다. 다음으로 확인할 사항은 작업의 modeowner입니다. 이 설정값이 이미 디스크에 있는 파일과 일치하지 않으면, Ansible은 내용이 동일하더라도 이를 수정하고 변경된 것으로 보고합니다. ansible-playbook site.yml --check --diff을 실행하여 두 가지 중 무엇인지 확인하십시오. --diff는 작업이 수행하려는 차이점을 보여줍니다.

Ansible에서 template과 copy의 차이점은 무엇입니까?

ansible.builtin.copy은 파일을 변경하지 않고 전송합니다. ansible.builtin.template은 컨트롤러에서 먼저 Jinja2를 통해 렌더링한 후 결과를 전송하므로, 파일이 대상 호스트에 도달하기 전에 변수와 루프가 해결됩니다. 어디서나 바이트 단위로 동일해야 하는 파일에는 copy를 사용하십시오. 호스트마다 달라지는 파일에는 template을 사용하십시오. 두 모듈은 동일한 파일 옵션을 공유하므로 mode, owner, backupvalidate은 두 경우 모두 동일하게 작동합니다.

플레이 도중에 핸들러를 실행하려면 어떻게 해야 합니까?

핸들러를 실행하려는 지점에 작업으로 ansible.builtin.meta: flush_handlers을 추가하십시오. 이 작업은 지금까지 알림을 받은 모든 핸들러를 트리거한 다음 플레이를 정상적으로 계속합니다. 같은 플레이의 후속 작업이 새로운 설정으로 실행 중인 서비스에 의존할 때 사용하십시오. 예를 들어, 리로드 후에만 존재하는 포트에서 wait_for를 수행해야 하는 경우가 이에 해당합니다. 이것이 플레이가 끝나기 전에 핸들러를 실행하는 지원되는 방식입니다.

nginx 설정 조각에 validate를 사용할 수 있습니까?

nginx -t -c %s으로는 불가능합니다. 해당 명령은 최상위 eventshttp 블록으로 시작하는 완전한 설정을 기대하므로, "upstream" directive is not allowed here와 같은 메시지를 출력하며 conf.d 조각을 거부합니다. 해당 조각은 http 블록 내부에서는 유효하지만 단독으로는 유효하지 않습니다. 파일을 설치한 다음, 리로드 핸들러보다 위에 정의된 핸들러에서 조립된 설정을 대상으로 nginx -t을 실행하십시오. 핸들러는 정의된 순서대로 실행되므로, 잘못된 설정은 리로드가 시도되기 전에 플레이를 실패하게 만듭니다. 템플릿 작업에 backup: true을 설정하여 이전 파일이 그대로 유지되도록 하십시오.

#ansible#jinja2#handlers#idempotence#automation