본문 바로가기

* Twodragon

Ansible 101: 조건문과 반복문 활용하여 인프라 관리하기

안녕하세요. Twodragon 입니다.

가시다 님의 온라인 A101(Ansible 101 study) 강의에 대한 2번째 주차이며 조건문과 반복문 활용하여 인프라 관리를 위한 도전기 입니다!

조건문과 반복문 활용

 

1. Ansible 반복문 이해하기

앤서블(Ansible)에서 반복문을 사용하면 코드를 중복 작성하지 않고도 반복적인 작업을 수행할 수 있습니다. 특히 여러 포트에 대한 방화벽 규칙을 추가하거나 다양한 서비스의 상태를 확인하고 관리할 때 유용합니다.

 

1.1 Ansible service 모듈

ansible.builtin.service 모듈은 원격 호스트에서 서비스를 관리하는 데 사용됩니다. 이 모듈을 반복문과 함께 어떻게 사용할 수 있는지 살펴보겠습니다.

 

1.2 간단한 반복문: 서비스 상태 확인

여러 호스트에서 sshd 및 rsyslog 서비스가 시작되었는지 확인하려면 간단한 반복문을 사용할 수 있습니다.

yamlCopy code
---
- hosts: all
  tasks:
    - name: sshd 및 rsyslog 상태 확인
      ansible.builtin.service:
        name: "{{ item }}"
        state: started
      loop:
        - sshd
        - rsyslog

이 예에서 loop 키워드는 서비스 목록(sshd 및 rsyslog)을 반복하며 이를 "started" 상태로 설정합니다.

 

1.3 변수와 함께 반복하기

플레이북을 더 동적으로 만들기 위해 서비스 목록을 변수에 저장하고 루프에서 참조할 수 있습니다.

yamlCopy code
---
- hosts: all
  vars:
    services:
      - sshd
      - rsyslog

  tasks:
    - name: sshd 및 rsyslog 상태 확인
      ansible.builtin.service:
        name: "{{ item }}"
        state: started
      loop: "{{ services }}"

이제 services 변수를 손쉽게 수정하여 메인 플레이북을 변경하지 않고도 작업을 수행할 수 있습니다.

 

1.4 Ansible file 모듈

ansible.builtin.file 모듈은 파일 및 디렉터리를 관리하는 데 사용됩니다. 이 모듈을 반복문과 함께 어떻게 사용할 수 있는지 알아보겠습니다.

 

1.5 사전 목록 반복문: 파일 생성

여러 호스트에 다양한 권한으로 여러 로그 파일을 생성하려면 사전 목록 반복문을 사용할 수 있습니다.

yamlCopy code
---
- hosts: all
  tasks:
    - name: 파일 생성
      ansible.builtin.file:
        path: "{{ item['log-path'] }}"
        mode: "{{ item['log-mode'] }}"
        state: touch
      loop:
        - log-path: /var/log/test1.log
          log-mode: '0644'
        - log-path: /var/log/test2.log
          log-mode: '0600'

이 플레이북은 각 파일의 속성을 나타내는 딕셔너리 목록을 사용하는 반복문과 함께 사용합니다.

 

1.6 Ansible shell 모듈 및 Register

ansible.builtin.shell 모듈은 셸 명령을 실행합니다. register 키워드와 결합되면 명령의 출력을 캡처합니다.

 

Register를 사용한 출력 캡처

이 예에서는 루프를 사용하여 셸 명령을 실행하고 register 키워드를 사용하여 출력을 저장합니다.

yamlCopy code
---
- hosts: localhost
  tasks:
    - name: 반복문을 통한 echo 테스트
      ansible.builtin.shell: "echo '나는 {{ item }} 할 수 있어요'"
      loop:
        - 한국어
        - 영어
      register: result

    - name: 결과 표시
      ansible.builtin.debug:
        msg: "표준 출력: {{ item.stdout }}"
      loop: "{{ result.results }}"

여기서 셸 명령의 출력은 result 변수에 저장되며, 각 반복의 **stdout**를 디버그 작업을 통해 표시합니다.

 

도전과제 1: Linux 사용자 생성 및 삭제

루프를 사용하여 열 개의 Linux 사용자를 생성한 다음 삭제해 보겠습니다.

---
- hosts: localhost
  tasks:
    - name: Linux user
      ansible.builtin.user:
        name: "{{ 'user' + item | string }}"
        state: present
        password: "{{ 'password' + item | string | password_hash('sha512') }}"
      loop: "{{ range(1, 11) | list }}"

 

 

도전과제 2: Loop의 Sequence를 사용한 파일 생성 및 삭제

with_sequence 루프를 활용하여 /var/log 디렉터리에 100개의 파일을 생성한 다음 삭제해 보겠습니다.

yamlCopy code
---
- hosts: localhost
  tasks:
    - name: 시퀀스를 사용한 파일 생성
      ansible.builtin.file:
        path: "/var/log/test{{ item }}.log"
        state: touch
      loop: "{{ range(1, 101) | list }}"

 

2. 조건문

Ansible은 특정 조건이 충족될 때 작업을 실행하는 데에 조건문을 사용합니다. 주로 특정 변수의 값이나 호스트의 상태에 따라 실행 여부를 결정합니다. 여러 가지 조건문을 사용하는 방법을 알아봅시다.

 

2.1 조건 작업 구문

when 문은 조건부로 작업을 실행할 때 테스트할 조건을 값으로 사용합니다. 조건이 충족되면 작업이 실행되고, 그렇지 않으면 작업을 건너뛰게 됩니다.

 

yamlCopy code
---
- hosts: localhost
  vars:
    run_my_task: true

  tasks:
    - name: echo message
      ansible.builtin.shell: "echo test"
      when: run_my_task
      register: result

    - name: Show result
      ansible.builtin.debug:
        var: result

위의 예제에서는 run_my_task 변수가 **true**일 때만 첫 번째 작업이 실행됩니다.

 

2.2 조건 연산자

==!=><>=<=andornotin 등의 연산자를 사용하여 다양한 조건을 표현할 수 있습니다.

 

yamlCopy code
---
- hosts: all
  vars:
    supported_distros:
      - Ubuntu
      - CentOS

  tasks:
    - name: Print supported os
      ansible.builtin.debug:
        msg: "This {{ ansible_facts['distribution'] }} need to use apt"
      when: ansible_facts['distribution'] in supported_distros

위의 예제는 운영체제가 Ubuntu 또는 CentOS일 때만 작업이 수행됩니다.

 

2.3 복수 조건문

두 가지 이상의 조건을 조합하여 사용할 수 있습니다.

 

yamlCopy code
---
- hosts: all

  tasks:
    - name: Print os type
      ansible.builtin.debug:
        msg: "OS Type {{ ansible_facts['distribution'] }}"
      when: ansible_facts['distribution'] == "CentOS" or ansible_facts['distribution'] == "Ubuntu"

위의 예제는 운영체제가 CentOS이거나 Ubuntu일 때 작업이 실행됩니다.

 

3. 반복문과 조건문 함께 사용

Ansible에서는 반복문과 조건문을 함께 사용하여 더 복잡한 작업을 수행할 수 있습니다.

 

3.1 반복문 사용

loop 키워드를 사용하여 반복문을 구현할 수 있습니다.

 

yamlCopy code
---
- hosts: db
  tasks:
    - name: Print Root Directory Size
      ansible.builtin.debug:
        msg: "Directory {{ item.mount }} size is {{ item.size_available }}"
      loop: "{{ ansible_facts['mounts'] }}"
      when: item['mount'] == "/" and item['size_available'] > 300000000

 

위의 예제는 루트 디렉토리의 사이즈가 300MB보다 큰 경우에만 작업이 수행됩니다.

 

3.2 작업 변수 활용

register 키워드를 사용하여 작업 변수를 생성하고, 그 변수를 활용하여 조건문을 작성할 수 있습니다.

 

yamlCopy code
---
- hosts: all

  tasks:
    - name: Get rsyslog service status
      ansible.builtin.command: systemctl is-active rsyslog
      register: result

    - name: Print rsyslog status
      ansible.builtin.debug:
        msg: "Rsyslog status is {{ result.stdout }}"
      when: result.stdout == "active"

위의 예제는 rsyslog 서비스가 활성화되어 있는 경우에만 상태를 출력합니다.

 

도전과제 3: Ubuntu OS이면서 fqdn이 tnode1인 경우, debug 모듈을 사용하여 OS 정보와 fqdn 정보를 출력해보세요.

yamlCopy code
---
- hosts: all
  tasks:
    - name: Print OS and FQDN info
      ansible.builtin.debug:
        msg: >-
          OS Type: {{ ansible_facts['distribution'] }}
          OS Version: {{ ansible_facts['distribution_version'] }}
          FQDN: {{ ansible_facts['fqdn'] }}
      when: ansible_facts['distribution'] == "Ubuntu" and ansible_facts['fqdn'] == "tnode1"

 

도전과제 4: 반복문과 조건문을 함께 사용하여 복잡한 작업 수행하기

Ansible을 사용하여 반복문과 조건문을 함께 활용하여 복잡한 작업을 수행하는 예제입니다.

yamlCopy code
---
- hosts: tnode4
  vars:
    users:
      - name: user1
        state: present
      - name: user2
        state: absent
      - name: user3
        state: present

  tasks:
    - name: Manage Users
      ansible.builtin.user:
        name: "{{ item.name }}"
        state: "{{ item.state }}"
      loop: "{{ users }}"
      when: item.state == 'present'

    - name: Print User Info
      ansible.builtin.debug:
        msg: "User {{ item.name }} is {{ item.state }}"
      loop: "{{ users }}"

이 플레이북은 여러 사용자를 관리하며, **state**가 'present'인 사용자에 대해서만 ansible.builtin.user 모듈을 사용하여 사용자를 관리합니다. 그 후, 각 사용자에 대한 정보를 출력하는 작업이 이어집니다.

플레이북을 실행하면 조건문과 반복문이 함께 사용되어 다양한 상황에서 유연하게 인프라를 관리할 수 있습니다.

 

4. 핸들러 및 작업 실패 처리

Ansible 모듈은 멱등성(idempotent)을 지원하도록 설계되어 있습니다. 이는 동일한 플레이북을 여러 번 실행해도 항상 동일한 결과를 얻을 수 있다는 것을 의미합니다. 플레이와 작업은 여러 번 실행될 수 있지만 해당 호스트는 원하는 상태로 변경이 필요한 경우에만 변경됩니다.

그러나 작업에서 시스템을 변경해야 하는 경우 추가 작업을 실행해야 할 수 있습니다. 핸들러는 다른 작업에서 트리거한 알림에 응답하는 작업이며, 해당 호스트에서 작업이 변경될 때만 핸들러에 통지합니다.

Ansible 핸들러를 사용하기 위해서는 notify 문을 사용하여 명시적으로 호출된 경우에만 활성화할 수 있습니다. 핸들러를 정의할 때는 같은 이름으로 여러 개의 핸들러를 정의하기보다는 각각의 고유한 이름을 사용하는 것이 좋습니다.

 

앤서블 핸들러 예제

아래는 rsyslog 서비스를 다시 시작하는 플레이북에서 핸들러를 사용하는 예제입니다. 이 플레이북은 특정 작업이 완료된 후 핸들러를 호출하여 메시지를 출력합니다.

yamlCopy code
---
- hosts: tnode2
  tasks:
    - name: Restart rsyslog
      ansible.builtin.service:
        name: "rsyslog"
        state: restarted
      notify:
        - print msg

  handlers:
    - name: print msg
      ansible.builtin.debug:
        msg: "rsyslog is restarted"

위의 플레이북을 실행하면 rsyslog 서비스를 재시작하고 핸들러에 의해 "rsyslog is restarted" 메시지가 출력됩니다. 핸들러는 변경된 작업이 있을 때만 호출됩니다.

 

도전과제 5

이제 Apache2 패키지를 apt 모듈을 통해 설치하고, 핸들러를 호출하여 service 모듈로 Apache2를 재시작하는 도전 과제를 수행해봅시다.

yamlCopy code
---
- hosts: tnode2
  tasks:
    - name: Install Apache2
      ansible.builtin.apt:
        name: "apache2"
        state: latest
      notify:
        - restart apache2

  handlers:
    - name: restart apache2
      ansible.builtin.service:
        name: "apache2"
        state: restarted

위의 플레이북을 실행하면 Apache2를 설치하고, 설치가 완료된 후 restart apache2 핸들러에 의해 Apache2 서비스가 재시작됩니다.

 

4.1 작업 실패 무시

Ansible은 플레이 실행 중 각 작업의 반환 코드를 평가하여 작업의 성공 여부를 결정합니다. 일반적으로 작업이 실패하면 Ansible은 이후 모든 작업을 건너뛰지만, 작업이 실패해도 플레이를 계속 실행할 수 있습니다. 이는 ignore_errors 키워드를 사용하여 구현할 수 있습니다.

 

예제 1: 작업 실패 무시하지 않음

아래의 플레이북은 Apache3 패키지를 설치하는 작업에서 실패하면 그 이후의 작업을 실행하지 않습니다.

yamlCopy code
---
- hosts: tnode1
  tasks:
    - name: Install Apache3
      ansible.builtin.apt:
        name: "apache3"
        state: latest

    - name: Print msg
      ansible.builtin.debug:
        msg: "Before task is ignored"

위의 플레이북을 실행하면 Apache3 패키지가 없어서 설치 작업이 실패하고, 따라서 "Before task is ignored" 메시지가 출력되지 않습니다.

 

예제 2: 작업 실패 무시

이제 **ignore_errors: yes**를 사용하여 작업이 실패하더라도 플레이를 계속 실행하는 예제를 살펴봅시다.

yamlCopy code
---
- hosts: tnode1
  tasks:
    - name: Install Apache3
      ansible.builtin.apt:
        name: "apache3"
        state: latest
      ignore_errors: yes

    - name: Print msg
      ansible.builtin.debug:
        msg: "Before task is ignored"

위의 플레이북을 실행하면 Apache3 패키지가 없어서 설치 작업이 실패해도 플레이는 계속 실행되고 "Before task is ignored" 메시지가 출력됩니다.

 

4.2 작업 실패 후 핸들러 실행

일반적으로 Ansible은 작업이 실패하고 해당 호스트에서 플레이가 중단되면 이전 작업에서 알림을 받은 모든 핸들러가 실행되지 않습니다. 그러나 플레이북에 force_handlers: yes 키워드를 설정하면 작업이 실패하더라도 알림을 받은 핸들러가 호출됩니다.

 

예제 1: 핸들러 호출 불가

아래의 플레이북은 **force_handlers: no**로 설정되어 있어 핸들러가 호출되지 않습니다.

yamlCopy code
---
- hosts: tnode2
  tasks:
    - name: Restart rsyslog
      ansible.builtin.service:
        name: "rsyslog"
        state: restarted
      notify:
        - print msg

  handlers:
    - name: print msg
      ansible.builtin.debug:
        msg: "rsyslog is restarted"

위의 플레이북을 실행하면 rsyslog 서비스를 재시작하고 핸들러에 의해 "rsyslog is restarted" 메시지가 출력됩니다. 그러나 이 플레이북은 작업이 실패하더라도 핸들러를 호출하지 않습니다.

 

예제 2: 핸들러 호출 가능

이제 **force_handlers: yes**로 설정하여 핸들러가 항상 호출되도록 해보겠습니다.

yamlCopy code
---
- hosts: tnode2
  force_handlers: yes

  tasks:
    - name: Restart rsyslog
      ansible.builtin.service:
        name: "rsyslog"
        state: restarted
      notify:
        - print msg

  handlers:
    - name: print msg
      ansible.builtin.debug:
        msg: "rsyslog is restarted"

위의 플레이북을 실행하면 rsyslog 서비스를 재시작하고 핸들러에 의해 "rsyslog is restarted" 메시지가 출력됩니다. 작업이 실패하더라도 **force_handlers: yes**로 설정되어 있기 때문에 핸들러가 항상 호출됩니다.

 

4.3 작업 실패 조건 지정: 셸 스크립트 보다는 모듈 사용

Ansible을 사용할 때 셸 스크립트 대신 모듈을 사용하는 것이 좋습니다. 셸 스크립트를 실행할 때는 반환 코드가 0이어도 작업이 실패한 것으로 간주됩니다. 이런 경우 failed_when 키워드를 사용하여 실패 조건을 지정할 수 있습니다.

 

예제 1: 셸 스크립트 실행

아래의 플레이북은 셸 스크립트를 실행하여 사용자를 추가하는 작업을 수행합니다.

yamlCopy code
---
- hosts: tnode1
  tasks:
    - name: Run user add script
      ansible.builtin.shell: /home/ubuntu/adduser-script.sh
      register: command_result

    - name: Print msg
      ansible.builtin.debug:
        msg: "{{ command_result.stdout }}"

위의 플레이북을 실행하면 셸 스크립트를 통해 사용자를 추가하고, 결과를 출력합니다.

 

예제 2: 실패 조건 지정

이번에는 실패 조건을 지정하여 특정 문자열이 출력되면 작업을 실패로 처리하는 예제입니다.

yamlCopy code
---
- hosts: tnode1
  tasks:
    - name: Run user add script
      ansible.builtin.shell: /home/ubuntu/adduser-script.sh
      register: command_result
      failed_when: "'Please input user id and password' in command_result.stdout"

    - name: Print msg
      ansible.builtin.debug:
        msg: "{{ command_result.stdout }}"

위의 플레이북을 실행하면 셸 스크립트를 통해 사용자를 추가하고, 만약 출력 결과에 "Please input user id and password"라는 문자열이 포함되어 있으면 작업을 실패로 처리합니다.

 

예제 3: 실패 시 사용자 정의 메시지 출력

작업이 실패할 때 사용자 정의 메시지를 출력할 수 있습니다. 이를 위해 fail 모듈을 사용합니다.

yamlCopy code
---
- hosts: tnode1
  tasks:
    - name: Run user add script
      ansible.builtin.shell: /home/ubuntu/adduser-script.sh
      register: command_result
      ignore_errors: yes

    - name: Report script failure
      ansible.builtin.fail:
        msg: "{{ command_result.stdout }}"
      when: "'Please input user id and password' in command_result.stdout"

위의 플레이북을 실행하면 셸 스크립트를 통해 사용자를 추가하고, 만약 출력 결과에 "Please input user id and password"라는 문자열이 포함되어 있으면 작업을 실패로 처리하고 사용자 정의 메시지가 출력됩니다.

 

4.4 Ansible 블록 및 오류 처리

Ansible은 블록(block)이라는 오류를 제어하는 문법을 제공합니다. 블록은 작업을 논리적으로 그룹화하는 데 사용되며, 작업 실행 방법을 제어하는 데 활용할 수 있습니다. 또한 블록을 통해 rescue 문과 always 문을 함께 사용하여 오류를 처리할 수 있습니다.

블록 예제

아래의 플레이북은 로그 디렉터리를 생성하고 로그 파일을 만드는 작업을 수행합니다. 이 작업을 블록으로 묶어 실패 시 오류를 처리하고, 항상 실행되는 작업을 추가했습니다.

yamlCopy code
---
- hosts: tnode2
  vars:
    logdir: /var/log/daily_log
    logfile: todays.log

  tasks:
    - name: Configure Log Env
      block:
        - name: Find Directory
          ansible.builtin.find:
            paths: "{{ logdir }}"
          register: result
          failed_when: "'Not all paths' in result.msg"

        rescue:
          - name: Make Directory when Not found Directory
            ansible.builtin.file:
              path: "{{ logdir }}"
              state: directory
              mode: '0755'

        always:
          - name: Create File
            ansible.builtin.file:
              path: "{{ logdir }}/{{ logfile }}"
              state: touch
              mode: '0644'

위의 플레이북을 실행하면 블록 내에서 디렉터리를 찾고 실패하면 rescue 절이 실행되어 디렉터리를 생성합니다. 항상 실행되는 always 절에서는 로그 파일을 만들어줍니다.

 

도전과제6: block, rescue, always 키워드를 사용한 플레이북

yamlCopy code
---
- hosts: tnode3
  vars:
    logdir: /var/log/my_logs
    logfile: todays_logs.log

  tasks:
    - name: Check if Log Directory Exists
      block:
        - name: Find Log Directory
          ansible.builtin.find:
            paths: "{{ logdir }}"
          register: result
          failed_when: "'Not all paths' in result.msg"

      rescue:
        - name: Create Log Directory
          ansible.builtin.file:
            path: "{{ logdir }}"
            state: directory
            mode: '0755'

      always:
        - name: Create Log File
          ansible.builtin.file:
            path: "{{ logdir }}/{{ logfile }}"
            state: touch
            mode: '0644'

이 플레이북은 주어진 디렉터리와 로그 파일을 만들기 위해 block, rescue, always 키워드를 사용합니다. 플레이 실행 중 블록 안의 작업이 실패하면 rescue 절이 실행되어 디렉터리를 만들고, always 절은 항상 실행되어 로그 파일을 생성합니다.