RHEL 에서 Ansible 플레이북 실행 제어하기

Red Hat Enterprise LinuxBeginner
지금 연습하기

소개

이 랩에서는 Red Hat Enterprise Linux (RHEL) 시스템에서 Ansible 플레이북의 실행 흐름을 제어하는 방법을 배우게 됩니다. 효율적인 작업 반복을 위한 루프 (loops) 와 특정 조건이 충족될 때만 작업을 실행하기 위한 조건문 (conditionals) 을 포함한 기본 제어 구조를 활용하는 플레이북을 작성하는 것으로 시작합니다. 또한, 변경이 발생했을 때만 서비스 재시작과 같은 작업을 트리거하는 핸들러 (handlers) 를 구현하여 자동화를 더욱 지능적이고 효율적으로 만들 것입니다.

이러한 기본 기술을 바탕으로 플레이북 실행을 관리하는 고급 기법을 탐색합니다. 여기에는 작업 실패를 우아하게 처리하기 위한 블록 (block) 및 복구 (rescue) 문 사용과 작업 상태에 대한 세밀한 제어를 얻기 위한 changed_whenfailed_when 사용이 포함됩니다. 랩을 마무리하기 위해 이러한 모든 개념을 실제 웹 서버 배포 연습에 적용하여 강력하고 안정적인 Ansible 자동화를 생성하는 능력을 공고히 할 것입니다.

루프 및 조건문을 사용한 플레이북 작성

이 단계에서는 Ansible 에서 작업 실행을 제어하는 두 가지 기본 개념인 루프 (loops) 와 조건문 (conditionals) 을 배우게 됩니다. 루프를 사용하면 여러 값으로 작업을 여러 번 반복할 수 있으며, 이는 여러 패키지를 설치하거나 여러 사용자를 생성하는 것과 같은 작업에 매우 효율적입니다. when 키워드를 사용하는 조건문을 사용하면 운영 체제가 특정 버전이거나 파일이 이미 존재하는 경우와 같이 특정 기준이 충족될 때만 작업을 실행할 수 있습니다.

먼저 LabEx VM 에 Ansible 이 설치되어 있는지 확인합니다. 이를 위해 DNF 패키지 관리자를 사용합니다.

sudo dnf install -y ansible-core

ansible-core 및 해당 종속성이 설치되고 있음을 나타내는 출력이 표시되어야 합니다.

...
Installed:
  ansible-core-2.x.x-1.el9.x86_64
  ...
Complete!

이제 프로젝트 디렉토리를 설정해 보겠습니다. 이 랩의 모든 작업은 체계적으로 관리하기 위해 전용 디렉토리 내에서 수행됩니다.

cd ~/project
mkdir control-flow-lab
cd control-flow-lab

Ansible 프로젝트에는 관리하려는 호스트를 정의하는 인벤토리 파일이 필요합니다. 이 랩에서는 로컬 머신인 localhost를 관리합니다.

nano 편집기를 사용하여 inventory라는 인벤토리 파일을 만듭니다.

nano inventory

파일에 다음 줄을 추가합니다. 이 줄은 Ansible 에 localhost에서 플레이북을 실행하고 SSH 를 사용하지 않고 직접 연결하도록 지시합니다.

localhost ansible_connection=local

파일을 저장하고 Ctrl+X, Y, Enter를 눌러 nano를 종료합니다.

다음으로 루프를 시연하기 위해 첫 번째 플레이북인 playbook.yml을 만들 것입니다. 이 플레이북은 유용한 명령줄 도구 목록을 설치합니다.

nano playbook.yml

편집기에 다음 YAML 내용을 입력합니다. 이 플레이북은 패키지를 설치하기 위해 ansible.builtin.dnf 모듈을 사용하는 하나의 작업을 정의합니다. become: yes 지시어는 Ansible 에 sudo 권한으로 작업을 실행하도록 지시하며, 이는 패키지를 설치하는 데 필요합니다. loop 키워드는 패키지 이름 목록을 제공합니다. Ansible 은 목록의 각 항목에 대해 이 작업을 한 번 실행하고 {{ item }} 플레이스홀더를 현재 패키지 이름으로 대체합니다.

---
- name: Install common tools
  hosts: localhost
  become: yes
  tasks:
    - name: Install specified packages
      ansible.builtin.dnf:
        name: "{{ item }}"
        state: present
      loop:
        - git
        - tree
        - wget

편집기를 저장하고 종료합니다. 이제 -i 플래그를 사용하여 인벤토리 파일을 지정하고 ansible-playbook 명령을 사용하여 플레이북을 실행합니다.

ansible-playbook -i inventory playbook.yml

출력에는 플레이북 실행이 표시됩니다. Ansible 은 각 패키지를 확인하고 아직 설치되지 않은 경우 설치합니다. 마지막의 PLAY RECAP은 결과를 요약합니다.

PLAY [Install tools and run conditional tasks] *********************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Install specified packages] **********************************************
changed: [localhost] => (item=git)
changed: [localhost] => (item=tree)
changed: [localhost] => (item=wget)

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

이제 조건부 작업을 포함하도록 플레이북을 수정해 보겠습니다. 운영 체제가 Red Hat Enterprise Linux 인 경우에만 메시지를 출력하는 작업을 추가합니다. 이는 특정 환경에 맞게 자동화를 조정하는 일반적인 사용 사례입니다.

playbook.yml 파일을 다시 엽니다.

nano playbook.yml

파일 끝에 다음 작업을 추가합니다. when 키워드는 주어진 표현식을 평가합니다. ansible_facts['distribution']은 Ansible 이 관리되는 호스트에 대해 자동으로 검색하는 변수입니다. 첫 번째 작업은 환경이 RHEL 이므로 실행되고 두 번째 작업은 건너뜁니다.

---
- name: Install tools and run conditional tasks
  hosts: localhost
  become: yes
  tasks:
    - name: Install specified packages
      ansible.builtin.dnf:
        name: "{{ item }}"
        state: present
      loop:
        - git
        - tree
        - wget

    - name: Show message on Red Hat systems
      ansible.builtin.debug:
        msg: "This system is a Red Hat family distribution."
      when: ansible_facts['distribution'] == "RedHat"

    - name: Show message on other systems
      ansible.builtin.debug:
        msg: "This system is NOT a Red Hat family distribution."
      when: ansible_facts['distribution'] != "RedHat"

편집기를 저장하고 종료합니다. 업데이트된 플레이북을 실행합니다.

ansible-playbook -i inventory playbook.yml

출력을 주의 깊게 관찰합니다. 패키지 설치 작업은 항목이 이미 설치되어 있으므로 모든 항목에 대해 ok로 보고될 가능성이 높습니다. 더 중요한 것은 첫 번째 디버그 메시지가 출력되는 것을 볼 수 있으며, 두 번째 메시지는 skipping으로 표시됩니다.

PLAY [Install tools and run conditional tasks] *********************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Install specified packages] **********************************************
ok: [localhost] => (item=git)
ok: [localhost] => (item=tree)
ok: [localhost] => (item=wget)

TASK [Show message on Red Hat systems] *****************************************
ok: [localhost] => {
    "msg": "This system is a Red Hat family distribution."
}

TASK [Show message on other systems] *******************************************
skipping: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

반복적인 작업을 수행하기 위한 루프와 시스템 사실에 따라 작업 실행을 제어하기 위한 조건문을 모두 사용하는 Ansible 플레이북을 성공적으로 작성하고 실행했습니다.

핸들러를 사용하여 서비스 재시작 트리거하기

이 단계에서는 Ansible 핸들러에 대해 배우게 됩니다. 핸들러는 다른 작업에 의해 "알림"을 받을 때만 실행되는 특수 작업입니다. 일반적으로 구성 파일이 업데이트된 후 서비스 재시작과 같이 변경이 발생했을 때만 수행해야 하는 작업에 사용됩니다. 이 접근 방식은 플레이북 실행 시마다 서비스를 재시작하는 것보다 효율적입니다. 필요한 경우에만 작업을 수행하도록 보장하기 때문입니다.

Nginx 웹 서버를 설치하고, 사용자 지정 홈페이지를 배포하며, 홈페이지 콘텐츠가 변경될 때만 Nginx 를 다시 로드하는 핸들러를 사용하는 플레이북을 만들 것입니다.

먼저 이 연습을 위해 새 디렉토리를 만들어 프로젝트를 체계적으로 관리합니다.

cd ~/project
mkdir control-handlers-lab
cd control-handlers-lab

이전과 마찬가지로 플레이북을 실행할 위치를 Ansible 에 알려주는 인벤토리 파일이 필요합니다.

nano inventory

로컬 머신을 지정하려면 다음 줄을 추가합니다.

localhost ansible_connection=local

편집기를 저장하고 종료합니다 (Ctrl+X, Y, Enter).

다음으로 웹 서버 홈페이지 역할을 할 파일이 필요합니다. 파일을 저장할 files 디렉토리를 만듭니다.

mkdir files

이제 files 디렉토리 안에 간단한 index.html 파일을 만듭니다.

nano files/index.html

다음 HTML 콘텐츠를 추가합니다.

<h1>Welcome to the Ansible Handler Lab!</h1>

편집기를 저장하고 종료합니다.

이제 deploy_nginx.yml 플레이북을 만듭니다. 이 플레이북은 Nginx 설치, index.html 파일 복사, Nginx 다시 로드 핸들러 정의의 세 가지 주요 작업을 수행합니다.

nano deploy_nginx.yml

다음 내용을 입력합니다. "Copy homepage" 작업의 notify 키워드와 끝에 있는 해당 handlers 섹션에 주의를 기울이십시오. become: yes 지시어는 Ansible 에 sudo 권한으로 작업을 실행하도록 지시하며, 이는 패키지를 설치하고 서비스를 관리하는 데 필요합니다.

---
- name: Deploy Nginx with a handler
  hosts: localhost
  become: yes
  tasks:
    - name: Ensure Nginx is installed
      ansible.builtin.dnf:
        name: nginx
        state: present

    - name: Start and enable Nginx service
      ansible.builtin.systemd:
        name: nginx
        state: started
        enabled: yes

    - name: Copy homepage
      ansible.builtin.copy:
        src: files/index.html
        dest: /usr/share/nginx/html/index.html
      notify: reload nginx

  handlers:
    - name: reload nginx
      ansible.builtin.systemd:
        name: nginx
        state: reloaded

편집기를 저장하고 종료합니다.

이제 플레이북을 처음 실행합니다.

ansible-playbook -i inventory deploy_nginx.yml

Nginx 가 설치되었거나 (또는 이미 존재했거나), Nginx 서비스가 시작 및 활성화되었고, index.html 파일이 복사되었으며 (상태 changed), 가장 중요한 것은 핸들러가 알림을 받고 플레이 끝에 실행되었음을 보여주는 출력이 표시됩니다.

...
TASK [Copy homepage] ***********************************************************
changed: [localhost]

RUNNING HANDLER [reload nginx] *************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

curl을 사용하여 웹 서버가 실행 중이고 사용자 지정 페이지를 제공하고 있는지 확인할 수 있습니다.

curl http://localhost

출력은 index.html 파일의 내용이어야 합니다.

<h1>Welcome to the Ansible Handler Lab!</h1>

이제 아무 변경 없이 동일한 플레이북을 다시 실행합니다.

ansible-playbook -i inventory deploy_nginx.yml

이번에는 출력을 관찰합니다. 대상의 파일이 이미 소스와 일치하므로 "Copy homepage" 작업은 changed 대신 ok로 보고됩니다. "Start and enable Nginx service" 작업도 서비스가 이미 실행 중이고 활성화되어 있으므로 ok로 보고됩니다. 변경 사항이 핸들러에 알림을 보내지 않았기 때문에 핸들러가 실행되지 않았습니다.

...
TASK [Copy homepage] ***********************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

핸들러를 다시 작동시키려면 원본 index.html 파일을 수정합니다.

nano files/index.html

내용을 다음으로 변경합니다.

<h1>The Handler Ran Again!</h1>

저장하고 종료합니다. 이제 플레이북을 한 번 더 실행합니다.

ansible-playbook -i inventory deploy_nginx.yml

원본 파일이 변경되었기 때문에 "Copy homepage" 작업은 다시 changed로 보고되며, 이는 reload nginx 핸들러를 알리고 실행합니다.

...
TASK [Copy homepage] ***********************************************************
changed: [localhost]

RUNNING HANDLER [reload nginx] *************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

마지막으로 curl로 변경 사항을 확인합니다.

curl http://localhost

업데이트된 메시지가 표시되어야 합니다.

<h1>The Handler Ran Again!</h1>

이 연습은 구성 변경에 대한 응답으로 서비스 상태를 관리하기 위한 핸들러의 강력함과 효율성을 보여줍니다.

블록 및 복구로 작업 실패 관리하기

이 단계에서는 Ansible 플레이북에서 오류를 우아하게 처리하는 방법을 배우게 됩니다. 기본적으로 작업이 실패하면 Ansible 은 해당 호스트에서 전체 플레이북 실행을 중지합니다. 이는 안전한 기본값이지만 때로는 더 많은 제어가 필요합니다. 오류 처리를 위한 두 가지 방법, 즉 간단한 ignore_errors 지시어와 더 강력한 block, rescue, always 구조를 탐색합니다. 이 구조는 작업을 시도하고 실패 시 복구 작업을 정의하는 방법을 제공합니다.

먼저 이 연습을 위해 새 디렉토리를 만듭니다.

cd ~/project
mkdir control-errors-lab
cd control-errors-lab

localhost에 대한 표준 inventory 파일을 만듭니다.

nano inventory

다음 내용을 추가합니다.

localhost ansible_connection=local

편집기를 저장하고 종료합니다 (Ctrl+X, Y, Enter).

이제 실패하도록 설계된 playbook.yml이라는 플레이북을 만듭니다. 첫 번째 작업은 존재하지 않는 패키지를 설치하려고 시도합니다.

nano playbook.yml

다음 내용을 입력합니다. 이 플레이북은 가짜 패키지 httpd-fake를 설치한 다음 실제 패키지인 mariadb-server를 설치하려고 시도합니다.

---
- name: Demonstrate Task Failure
  hosts: localhost
  become: yes
  tasks:
    - name: Attempt to install a non-existent package
      ansible.builtin.dnf:
        name: httpd-fake
        state: present

    - name: Install MariaDB server
      ansible.builtin.dnf:
        name: mariadb-server
        state: present

편집기를 저장하고 종료합니다. 이제 플레이북을 실행합니다.

ansible-playbook -i inventory playbook.yml

httpd-fake 패키지를 찾을 수 없기 때문에 첫 번째 작업이 오류 메시지와 함께 실패하는 것을 볼 수 있습니다. 중요한 것은 Ansible 이 중지되고 두 번째 작업인 "Install MariaDB server"는 실행되지 않는다는 것입니다.

...
TASK [Attempt to install a non-existent package] *******************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "No match for argument: httpd-fake", "rc": 1, "results": []}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

이제 blockrescue를 사용하여 이 실패를 더 우아하게 처리해 보겠습니다. block 키워드는 작업 세트를 그룹화합니다. block 내의 작업이 실패하면 Ansible 은 block의 나머지 작업을 건너뛰고 rescue 섹션의 작업을 실행합니다. always 섹션은 block 또는 rescue 섹션이 성공했는지 실패했는지에 관계없이 실행됩니다.

이 구조를 사용하도록 playbook.yml을 수정합니다.

nano playbook.yml

전체 내용을 다음으로 바꿉니다. 여기서 block에서 가짜 패키지를 설치하려고 시도합니다. 실패하면 rescue 섹션이 실행되어 복구 단계로 mariadb-server를 설치합니다. always 섹션은 끝에 메시지를 출력합니다.

---
- name: Handle Task Failure with Block and Rescue
  hosts: localhost
  become: yes
  tasks:
    - name: Attempt primary task, with recovery
      block:
        - name: Attempt to install a non-existent package
          ansible.builtin.dnf:
            name: httpd-fake
            state: present
        - name: This task will be skipped
          ansible.builtin.debug:
            msg: "This message will not appear because the previous task fails."
      rescue:
        - name: Install MariaDB server on failure
          ansible.builtin.dnf:
            name: mariadb-server
            state: present
      always:
        - name: This always runs
          ansible.builtin.debug:
            msg: "The block has completed, either by success or rescue."

저장하고 종료합니다. 플레이북을 다시 실행합니다.

ansible-playbook -i inventory playbook.yml

출력을 관찰합니다. block의 첫 번째 작업은 예상대로 실패합니다. block의 두 번째 작업은 건너뜁니다. 그런 다음 Ansible 은 rescue 섹션으로 이동하여 mariadb-server를 성공적으로 설치합니다. 마지막으로 always 섹션이 실행됩니다.

...
TASK [Attempt to install a non-existent package] *******************************
fatal: [localhost]: FAILED! => ...

TASK [This task will be skipped] ***********************************************
skipping: [localhost]

RESCUE START *******************************************************************

TASK [Install MariaDB server on failure] ***************************************
changed: [localhost]

ALWAYS START *******************************************************************

TASK [This always runs] ********************************************************
ok: [localhost] => {
    "msg": "The block has completed, either by success or rescue."
}

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=1    rescued=1    ignored=0

이제 block이 성공하는 경우를 보겠습니다. 플레이북을 편집하고 패키지 이름을 수정합니다.

nano playbook.yml

httpd-fake를 실제 패키지인 httpd로 변경합니다.

## ... (rest of the playbook)
block:
  - name: Attempt to install a valid package
    ansible.builtin.dnf:
      name: httpd ## Corrected from httpd-fake
      state: present
  - name: This task will now run
    ansible.builtin.debug:
      msg: "This message will now appear because the previous task succeeds."
## ... (rest of the playbook)

저장하고 종료합니다. 마지막으로 플레이북을 실행합니다.

ansible-playbook -i inventory playbook.yml

이번에는 block의 두 작업 모두 성공합니다. block이 오류 없이 완료되었기 때문에 rescue 섹션은 완전히 건너뜁니다. 이름에서 알 수 있듯이 always 섹션은 여전히 실행됩니다.

...
TASK [Attempt to install a valid package] **************************************
changed: [localhost]

TASK [This task will now run] **************************************************
ok: [localhost] => {
    "msg": "This message will now appear because the previous task succeeds."
}

RESCUE START *******************************************************************
skipping rescue

ALWAYS START *******************************************************************

TASK [This always runs] ********************************************************
ok: [localhost] => {
    "msg": "The block has completed, either by success or rescue."
}

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

이제 block/rescue/always 구조를 사용하여 실패를 처리하고 복구 작업을 수행할 수 있는 강력한 플레이북을 성공적으로 사용했습니다.

changed_when 및 failed_when 으로 작업 상태 제어하기

이 단계에서는 Ansible 이 작업 결과를 해석하는 방식을 더 세밀하게 제어하는 방법을 배우게 됩니다. 두 가지 강력한 지시어인 changed_whenfailed_when에 대해 알아보겠습니다.

  • changed_when: 기본적으로 ansible.builtin.command 또는 ansible.builtin.shell과 같은 모듈은 실행된 명령이 시스템을 변경하지 않았더라도 거의 항상 "changed" 상태를 보고합니다. changed_when을 사용하면 작업이 "changed"로 보고되어야 하는지 여부를 결정하는 사용자 지정 조건을 정의할 수 있습니다. 이는 멱등성 (idempotent) 플레이북을 작성하고 핸들러를 정확하게 트리거하는 데 중요합니다.
  • failed_when: 때로는 명령이 허용 가능한 결과일 때에도 0 이 아닌 상태 코드 (Ansible 이 실패로 간주하는) 로 종료될 수 있습니다. failed_when을 사용하면 기본 실패 조건을 재정의하여 명령의 출력이나 특정 종료 코드와 같은 더 지능적인 기준에 따라 플레이북을 계속 실행할 수 있습니다.

새 프로젝트 디렉토리를 설정하는 것부터 시작하겠습니다.

cd ~/project
mkdir control-state-lab
cd control-state-lab

localhost에 대한 표준 inventory 파일을 만듭니다.

nano inventory

다음 내용을 추가합니다.

localhost ansible_connection=local

편집기를 저장하고 종료합니다 (Ctrl+X, Y, Enter).

changed_when 사용하기

먼저 명령 작업이 기본적으로 어떻게 작동하는지 살펴보겠습니다. date 명령을 실행하는 플레이북을 만들 것입니다. 이 명령은 단순히 날짜를 출력하고 시스템을 변경하지 않지만, command 모듈은 이를 변경으로 보고합니다.

playbook.yml이라는 새 플레이북을 만듭니다.

nano playbook.yml

다음 내용을 입력합니다.

---
- name: Control Task State
  hosts: localhost
  tasks:
    - name: Check local time (default behavior)
      ansible.builtin.command: date

저장하고 종료합니다. 이제 플레이북을 실행합니다.

ansible-playbook -i inventory playbook.yml

출력에서 시스템의 아무것도 수정되지 않았음에도 불구하고 작업이 changed=1로 보고되는 것을 확인합니다.

...
TASK [Check local time (default behavior)] *************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    ...

이제 changed_when을 사용하여 이 명령이 시스템을 변경하지 않는다고 Ansible 에 알립니다. playbook.yml을 수정합니다.

nano playbook.yml

작업에 changed_when: false를 추가합니다.

---
- name: Control Task State
  hosts: localhost
  tasks:
    - name: Check local time (with changed_when)
      ansible.builtin.command: date
      changed_when: false

저장하고 종료합니다. 플레이북을 다시 실행합니다.

ansible-playbook -i inventory playbook.yml

이번에는 작업이 ok로 보고되고 최종 요약에서 changed=0으로 표시됩니다. 기본 동작을 성공적으로 재정의했습니다.

...
TASK [Check local time (with changed_when)] ************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    ...

failed_when 사용하기

다음으로 failed_when을 탐색합니다. 존재하지 않는 파일의 존재 여부를 확인하는 작업을 만들 것입니다. 명령은 기본적으로 "실패"합니다.

먼저 더미 파일을 만들어 검색합니다.

echo "System is running" > status.txt

이제 playbook.yml을 수정하여 이 파일에서 "ERROR"라는 단어를 검색합니다. grep 명령은 단어가 발견되지 않았기 때문에 상태 코드 1 로 종료되며, 이는 Ansible 이 실패로 해석합니다.

nano playbook.yml

내용을 다음으로 바꿉니다.

---
- name: Control Task State
  hosts: localhost
  tasks:
    - name: Check for ERROR in status file (will fail)
      ansible.builtin.command: grep ERROR status.txt

저장하고 종료합니다. 플레이북을 실행합니다.

ansible-playbook -i inventory playbook.yml

예상대로 플레이북 실행이 FAILED! 메시지와 함께 중지됩니다.

...
TASK [Check for ERROR in status file (will fail)] ******************************
fatal: [localhost]: FAILED! => {"changed": true, "cmd": ["grep", "ERROR", "status.txt"], "delta": "...", "end": "...", "msg": "non-zero return code", "rc": 1, ...}
...

이것은 우리가 원하는 것이 아닙니다. "ERROR"의 부재는 우리에게 성공 조건입니다. failed_when을 사용하여 실패를 구성하는 것을 재정의할 수 있습니다. 명령의 반환 코드가 1 보다 클 때만 실패하도록 Ansible 에 지시할 것입니다. 반환 코드 1(패턴을 찾을 수 없음) 은 이제 성공으로 간주됩니다. 반환 코드 (rc) 를 검사하기 위해 작업 결과를 register해야 합니다.

playbook.yml을 마지막으로 수정합니다.

nano playbook.yml

registerfailed_when으로 플레이북을 업데이트합니다.

---
- name: Control Task State
  hosts: localhost
  tasks:
    - name: Check for ERROR in status file (with failed_when)
      ansible.builtin.command: grep ERROR status.txt
      register: grep_result
      failed_when: grep_result.rc > 1
      changed_when: false

grep은 읽기 전용 작업이며 시스템을 변경하지 않으므로 changed_when: false도 추가했습니다.

저장하고 종료합니다. 최종 플레이북을 실행합니다.

ansible-playbook -i inventory playbook.yml

성공! 작업의 반환 코드가 1 이었기 때문에 이제 ok로 보고됩니다. 이는 새 실패 조건 (rc > 1) 을 충족하지 않습니다. 플레이북이 성공적으로 완료됩니다.

...
TASK [Check for ERROR in status file (with failed_when)] ***********************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    ...

이제 changed_whenfailed_when을 사용하여 작업의 성공, 변경 및 실패 상태를 정확하게 정의하는 방법을 배웠으므로 더 강력하고 지능적인 자동화를 수행할 수 있습니다.

작업 제어를 사용하여 보안 웹 서버 배포하기

이 마지막 단계에서는 루프, 조건문, 핸들러 및 오류 처리 등 배운 모든 개념을 결합하여 단일의 강력한 플레이북을 구축하게 됩니다. 목표는 Apache 웹 서버 (httpd) 를 배포하고, mod_ssl로 보안을 강화하며, 자체 서명 SSL 인증서를 생성하고, 사용자 지정 홈페이지를 배포하는 것입니다. 이 실습은 실제 자동화 작업을 반영합니다.

먼저 이 최종 연습을 위한 프로젝트 디렉토리를 설정합니다.

cd ~/project
mkdir control-review-lab
cd control-review-lab

항상 그렇듯이 대상 호스트를 정의하는 inventory 파일을 만듭니다.

nano inventory

localhost 항목을 추가합니다.

localhost ansible_connection=local

편집기를 저장하고 종료합니다 (Ctrl+X, Y, Enter).

다음으로 플레이북이 배포할 파일을 저장할 디렉토리가 필요합니다.

mkdir files

이제 files 디렉토리 안에 사용자 지정 홈페이지인 index.html을 만듭니다.

nano files/index.html

다음 HTML 내용을 추가합니다. 이것이 보안 웹 서버에서 제공될 페이지입니다.

<h1>Secure Web Server Deployed by Ansible!</h1>
<p>This page is served over HTTPS.</p>

편집기를 저장하고 종료합니다.

이제 메인 플레이북인 deploy_secure_web.yml을 구축할 차례입니다. 이 플레이북은 이전 플레이북보다 더 복잡하며 여러 개념을 통합합니다.

nano deploy_secure_web.yml

다음의 완전한 플레이북을 입력합니다. 코드 내의 주석을 읽고 각 부분이 전체 목표에 어떻게 기여하는지 이해하십시오.

---
- name: Deploy a Secure Apache Web Server
  hosts: localhost
  become: yes
  vars:
    packages_to_install:
      - httpd
      - mod_ssl
    ssl_cert_path: /etc/pki/tls/certs/localhost.crt
    ssl_key_path: /etc/pki/tls/private/localhost.key

  tasks:
    - name: Stop nginx to free port 80
      ansible.builtin.systemd:
        name: nginx
        state: stopped
      ignore_errors: yes

    - name: Install httpd and mod_ssl packages
      ansible.builtin.dnf:
        name: "{{ packages_to_install }}"
        state: present

    - name: Generate self-signed SSL certificate if it does not exist
      ansible.builtin.command: >
        openssl req -new -nodes -x509
        -subj "/C=US/ST=None/L=None/O=LabEx/CN=localhost"
        -keyout {{ ssl_key_path }}
        -out {{ ssl_cert_path }}
      args:
        creates: "{{ ssl_cert_path }}"

    - name: Deploy custom index.html
      ansible.builtin.copy:
        src: files/index.html
        dest: /var/www/html/index.html
      notify: restart httpd

    - name: Start and enable httpd service
      ansible.builtin.systemd:
        name: httpd
        state: started
        enabled: yes

  handlers:
    - name: restart httpd
      ansible.builtin.systemd:
        name: httpd
        state: restarted

이 플레이북이 수행하는 작업을 분석해 보겠습니다.

  • vars: 설치할 패키지와 SSL 인증서 및 키의 경로에 대한 변수를 정의하여 플레이북을 더 쉽게 읽고 유지 관리할 수 있습니다.
  • Nginx 중지 작업: 이전 실험실 단계의 nginx 서비스를 중지하여 Apache 에 포트 80 을 확보합니다. nginx 가 실행 중이 아닌 경우를 대비하여 ignore_errors: yes를 사용합니다.
  • 설치 작업: packages_to_install 변수를 사용하여 httpdmod_ssl을 모두 설치합니다.
  • 인증서 생성 작업: 이것은 핵심 작업입니다. openssl 명령을 사용하여 자체 서명 인증서를 만듭니다. args: { creates: ... } 지시어는 이 작업을 멱등성으로 만듭니다. 명령은 인증서 파일 (/etc/pki/tls/certs/localhost.crt) 이 아직 존재하지 않는 경우에만 실행됩니다.
  • 홈페이지 배포 작업: 사용자 지정 index.html을 복사합니다. 중요한 것은 notify: restart httpd를 사용하여 파일이 변경된 경우 핸들러를 트리거한다는 것입니다.
  • 서비스 시작 작업: systemd 모듈을 사용하여 모든 구성이 완료된 후 httpd 서비스를 시작하고 활성화하여 부팅 시 시작되도록 합니다.
  • 핸들러: restart httpd 핸들러는 systemd 를 사용하여 Apache 를 다시 시작하며, 구성 또는 콘텐츠 파일이 변경될 때만 트리거됩니다.

편집기를 저장하고 종료합니다. 이제 포괄적인 플레이북을 실행합니다.

ansible-playbook -i inventory deploy_secure_web.yml

첫 번째 실행에서는 nginx 중지, 패키지 설치, 인증서 생성, 파일 복사 및 서비스 시작을 포함하여 여러 작업이 changed로 보고되는 것을 볼 수 있습니다.

...
TASK [Start and enable httpd service] ******************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=6    changed=5    unreachable=0    failed=0    ...

마지막으로 보안 웹 서버가 작동하는지 확인합니다. 먼저 HTTP 버전을 테스트한 다음 자체 서명 인증서에 대한 경고를 무시하기 위해 -k 플래그를 사용하여 HTTPS 버전을 테스트합니다.

curl http://localhost

사용자 지정 홈페이지의 내용을 볼 수 있습니다.

<h1>Secure Web Server Deployed by Ansible!</h1>
<p>This page is served over HTTPS.</p>

HTTPS 버전도 테스트할 수 있습니다.

curl -k https://localhost

플레이북을 다시 실행하면 changed로 보고되는 작업이 없으며 핸들러가 실행되지 않아 플레이북이 멱등성임을 증명합니다.

축하합니다! 루프, 변수, 멱등성 명령 실행 및 핸들러를 결합하여 보안 애플리케이션을 배포하는 실용적이고 강력한 Ansible 플레이북을 성공적으로 구축했습니다.

요약

이 실험실에서는 RHEL 시스템에서 Ansible 플레이북 실행을 제어하는 방법을 배웠습니다. Ansible 설치 및 인벤토리 파일 생성을 포함하여 기본 프로젝트 환경을 설정하는 것부터 시작했습니다. 그런 다음 루프를 사용하여 다른 입력으로 작업을 효율적으로 반복하고 특정 상황에서만 작업을 실행하기 위해 when 문을 사용한 조건문을 탐색했습니다. 이를 바탕으로 구성 파일이 수정된 경우에만 서비스 재시작을 트리거하는 것과 같이 반응성이 뛰어난 자동화를 만들기 위해 핸들러를 구현했습니다.

이 실험실에서는 플레이북 실행을 관리하는 고급 기술도 다루었습니다. blockrescue 절을 사용하여 작업 실패를 우아하게 처리함으로써 더 강력한 플레이북을 구축하는 방법을 배웠습니다. 또한 changed_whenfailed_when을 사용하여 사용자 지정 성공 및 실패 조건을 정의함으로써 작업 결과에 대한 세밀한 제어를 얻었습니다. 마지막으로, 이러한 모든 기술을 실용적인 시나리오에 적용하여 통합했습니다. 즉, 보안 웹 서버를 배포하여 루프, 조건문, 핸들러 및 오류 처리를 실제 자동화 워크플로에 효과적으로 결합하는 방법을 시연했습니다.