Ansible Practices: Loop, Conditions and Roles

A101 Week 2: 앤서블의 반복문과 조건문, 과 콘텐츠 컬렉션

Sigrid Jin
89 min readJan 20, 2024

실습 준비

ssh key 복사

# for i in {1..3}; do **ssh-copy-id ubuntu@tnode$i**; done
cat ~/.ssh/id_rsa.pub | ssh -i sigrid-gashida.pem ubuntu@tnode1 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"# 확인하기
ubuntu@server:~$ for i in {1..3}; do echo ">> tnode$i <<"; ssh tnode$i cat ~/.ssh/authorized_keys; echo; done
>> tnode1 <<
>> tnode2 <<>> tnode3 <<
**cd ~/my-ansible/**
cat ansible.cfg inventory
# **[ad-hoc] ansible 모듈 , facts 캐싱
ansible -m ping all**
ansible -m **shell** **-a** "tail -n 3 /etc/passwd" **db**

AWS SecretManager에 등록

aws **sts get-caller-identity** | jq
curl -s <http://169.254.169.254/latest/meta-data/**iam**/info> | jq
ubuntu@server:~/my-ansible$ echo "Password1!" > mysecret.txt
chmod go-rx mysecret.txt
ls -l mysecret.txt
-rw--w---- 1 ubuntu ubuntu 11 Jan 20 14:28 mysecret.txt
ubuntu@server:~/my-ansible$ cat mysecret.txt
Password1!
ubuntu@server:~/my-ansible$ aws secretsmanager create-secret --name MySecret1 --secret-string file://mysecret.txt --region ap-northeast-2
{
"ARN": "arn:aws:secretsmanager:ap-northeast-2:712218945685:secret:MySecret1-w4zBMQ",
"Name": "MySecret1",
"VersionId": "9a6f90b6-bc7a-4192-a032-bd1f1c199435"
}
ubuntu@server:~/my-ansible$ aws secretsmanager get-secret-value --secret-id MySecret1 --region ap-northeast-2 | jq
{
"ARN": "arn:aws:secretsmanager:ap-northeast-2:712218945685:secret:MySecret1-w4zBMQ",
"Name": "MySecret1",
"VersionId": "9a6f90b6-bc7a-4192-a032-bd1f1c199435",
"SecretString": "Password1!\\n",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": 1705728517.502
}
**shred -u** mysecret.txt # The file is destroyed so it can no longer be accessed
ls -l
# <https://malwareanalysis.tistory.com/692#4.-%EC%A4%91%EC%9A%94-%EC%89%98-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%97%90%EC%84%9C-ansible-vault-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95>

For-loop playbook with 단일 아이템

  • Ansible Loops : 반복문을 사용하면 동일한 모듈을 사용하는 작업을 여러 번 작성하지 않아도 된다.
  • 예를 들어 서비스에 필요한 포트를 방화벽에 추가한다고 하면, 포트를 추가하는 작업을 여러 개 작성하는 대신 loop 반복문을 이용해 작업 하나로 여러 개의 포트를 추가할 수 있다.
  • 단순 반복문 : 특정 항목에 대한 작업을 반복하고 item 변수, 변수 목록 지정할 수 있다.
  • loop 키워드를 작업에 추가하면 작업을 반복해야 하는 항목의 목록으로 사용한다.
  • 해당하는 값을 사용하려면 item 변수를 이용할 수 있다.
  • 플레이북 파일 생성 : 플레이북의 목적은 sshd 와 rsyslog 서비스가 시작되어 있지 않다면 시작한다.

yaml without loop

  • loop 문에 사용하는 아이템을 변수에 저장하면 loop 키워드에 해당 변수명을 사용할 수 있다.
---
- hosts: all
tasks:
- name: Check sshd state
ansible.builtin.service:
name: sshd
state: started
- name: Check rsyslog state
ansible.builtin.service:
name: rsyslog
state: started

yaml with loop hardcodes

  • loop 키워드를 작업에 추가하면 작업을 반복해야 하는 항목의 목록으로 사용한다.
  • 그리고 해당하는 값을 사용하려면 item 변수를 이용할 수 있다.
---
- hosts: all
tasks:
- name: Check sshd and rsyslog state
ansible.builtin.service:
name: "{{ item }}"
state: started
loop:
- sshd
- rsyslog

yaml with loop variables

  • 위에서 말했다시피 loop 문에 들어가는 item 변수를 지정하면 작업을 반복할 수 있는데, loop 키워드도 외부에서 역으로 주입할 수 있다.
  • loop 문에 사용하는 아이템을 변수에 저장하면 loop 키워드에 해당 변수명을 사용할 수 있다. 즉 반복 변수를 별도 파일로 분리할 수 있다.
---
- hosts: all
vars:
services:
- sshd
- rsyslog
tasks:
- name: Check sshd and rsyslog state
ansible.builtin.service:
name: "{{ item }}"
state: started
loop: "{{ services }}"

Logs

ubuntu@server:~/my-ansible$ ansible -m shell -a "pstree |grep sshd" all
tnode1 | CHANGED | rc=0 >>
|-sshd---sshd---sshd---sh---sudo---sudo---sh---python3---sh-+-grep
tnode3 | CHANGED | rc=0 >>
|-sshd---sshd---sshd---sh---sudo---sudo---sh---python3---sh-+-grep
tnode2 | CHANGED | rc=0 >>
|-sshd---sshd---sshd---sh---sudo---sudo---sh---python3---sh-+-grep
ubuntu@server:~/my-ansible$ ansible -m shell -a "pstree |grep rsyslog" all
tnode2 | CHANGED | rc=0 >>
|-rsyslogd---3*[{rsyslogd}]
tnode3 | CHANGED | rc=0 >>
|-rsyslogd---3*[{rsyslogd}]
tnode1 | CHANGED | rc=0 >>
|-rsyslogd---3*[{rsyslogd}]
ubuntu@server:~/my-ansible$ ansible-playbook check-services.ymlPLAY [all] **************************************************************************************************************TASK [Check sshd state] *************************************************************************************************
ok: [tnode2]
ok: [tnode3]
ok: [tnode1]
TASK [Check rsyslog state] **********************************************************************************************
ok: [tnode3]
ok: [tnode2]
ok: [tnode1]
PLAY RECAP **************************************************************************************************************
tnode1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu@server:~/my-ansible$ ansible-playbook check-services-1.ymlPLAY [all] **************************************************************************************************************TASK [Check sshd and rsyslog state] *************************************************************************************
ok: [tnode2] => (item=sshd)
ok: [tnode1] => (item=sshd)
ok: [tnode3] => (item=sshd)
ok: [tnode1] => (item=rsyslog)
ok: [tnode2] => (item=rsyslog)
ok: [tnode3] => (item=rsyslog)
PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu@server:~/my-ansible$ ansible-playbook check-services-2.ymlPLAY [all] **************************************************************************************************************TASK [Check sshd and rsyslog state] *************************************************************************************
ok: [tnode1] => (item=sshd)
ok: [tnode3] => (item=sshd)
ok: [tnode2] => (item=sshd)
ok: [tnode1] => (item=rsyslog)
ok: [tnode2] => (item=rsyslog)
ok: [tnode3] => (item=rsyslog)
PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

For-loop playbook with 복수의 아이템

  • 하나의 아이템을 사용할 때도 있지만, 여러 개의 아이템이 필요한 경우가 발생한다.
  • 예를 들어 여러 개의 사용자 계정을 생성하는 플레이북을 작성한다면 사용자 계정을 생성하기 위해 필요한 이름과 패스워드 등의 여러 항목을 loop 문에서 사전 목록으로 사용할 수 있다.
  • ansible.builtin.file 모듈
  • https://docs.ansible.com/ansible/latest/collections/ansible/builtin/file_module.html
# ansible-playbook make-file.yml
---
- hosts: all
tasks:
- name: Create files
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'
ubuntu@server:~/my-ansible$ ansible-playbook make-file.ymlPLAY [all] **************************************************************************************************************TASK [Create files] *****************************************************************************************************
changed: [tnode2] => (item={'log-path': '/var/log/test1.log', 'log-mode': '0644'})
changed: [tnode1] => (item={'log-path': '/var/log/test1.log', 'log-mode': '0644'})
changed: [tnode3] => (item={'log-path': '/var/log/test1.log', 'log-mode': '0644'})
changed: [tnode2] => (item={'log-path': '/var/log/test2.log', 'log-mode': '0600'})
changed: [tnode3] => (item={'log-path': '/var/log/test2.log', 'log-mode': '0600'})
changed: [tnode1] => (item={'log-path': '/var/log/test2.log', 'log-mode': '0600'})
PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu@server:~/my-ansible$ ansible -m shell -a "ls -l /var/log/test*.log" all
tnode1 | CHANGED | rc=0 >>
-rw-r--r-- 1 root root 0 Jan 20 14:59 /var/log/test1.log
-rw------- 1 root root 0 Jan 20 14:59 /var/log/test2.log
tnode3 | CHANGED | rc=0 >>
-rw-r--r-- 1 root root 0 Jan 20 14:59 /var/log/test1.log
-rw------- 1 root root 0 Jan 20 14:59 /var/log/test2.log
tnode2 | CHANGED | rc=0 >>
-rw-r--r-- 1 root root 0 Jan 20 14:59 /var/log/test1.log
-rw------- 1 root root 0 Jan 20 14:59 /var/log/test2.log
ubuntu@server:~/my-ansible$ ansible all -m shell -a "cat /var/log/test1.log"
tnode1 | CHANGED | rc=0 >>
tnode2 | CHANGED | rc=0 >>tnode3 | CHANGED | rc=0 >>ubuntu@server:~/my-ansible$ ansible all -m shell -a "cat /var/log/test2.log"
tnode2 | CHANGED | rc=0 >>
tnode1 | CHANGED | rc=0 >>tnode3 | CHANGED | rc=0 >>**
  • 다음과 같이 플레이북을 작성하면 파일의 output도 볼 수 있다.
---
- hosts: all
tasks:
- name: Display contents of /var/log/test1.log
ansible.builtin.command:
cmd: cat /var/log/test1.log
register: cat_output
ignore_errors: true
- name: Show output
ansible.builtin.debug:
var: cat_output.stdout
- name: Display contents of /var/log/test2.log
ansible.builtin.command:
cmd: cat /var/log/test2.log
register: cat_output
ignore_errors: true
- name: Show output
ansible.builtin.debug:
var: cat_output.stdout
  • 반복문과 Register 변수 사용 : Register 변수는 반복 실행되는 작업의 출력을 캡처할 수 있다.
---
- hosts: localhost
tasks:
- name: Loop echo test
ansible.builtin.shell: "echo 'I can speak {{ item }}'"
loop:
- Rust
- Go
register: result
    - name: Show result
ansible.builtin.debug:
var: result
  • 결과
ubuntu@server:~/my-ansible$ ansible-playbook loop_register.yml
PLAY [localhost] ********************************************************************************************************TASK [Gathering Facts] **************************************************************************************************
ok: [localhost]
TASK [Loop echo test] ***************************************************************************************************
changed: [localhost] => (item=Rust)
changed: [localhost] => (item=Go)
TASK [Show result] ******************************************************************************************************
ok: [localhost] => {
"result": {
"changed": true,
"msg": "All items completed",
"results": [
{
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo 'I can speak Rust'",
"delta": "0:00:00.012589",
"end": "2024-01-20 15:03:19.674231",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo 'I can speak Rust'",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true
}
},
"item": "Rust",
"msg": "",
"rc": 0,
"start": "2024-01-20 15:03:19.661642",
"stderr": "",
"stderr_lines": [],
"stdout": "I can speak Rust",
"stdout_lines": [
"I can speak Rust"
]
},
{
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo 'I can speak Go'",
"delta": "0:00:00.003447",
"end": "2024-01-20 15:03:19.922256",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo 'I can speak Go'",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true
}
},
"item": "Go",
"msg": "",
"rc": 0,
"start": "2024-01-20 15:03:19.918809",
"stderr": "",
"stderr_lines": [],
"stdout": "I can speak Go",
"stdout_lines": [
"I can speak Go"
]
}
],
"skipped": false
}
}
PLAY RECAP **************************************************************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
  • 위에서 보듯 results의 실행 결과는 배열 형식으로 저장된 것을 볼 수 있다. 이때 results의 특정 값을 플레이북에서 사용할 경우 loop문을 이용할 수 있다.
  • debug 모듈에 loop 키워드를 사용하여 result.results를 아이템 변수로 사용하자.
  • 그리고 해당 아이템의 stdout의 값을 출력할 때는 item.stdout이라는 변수로 결과값을 출력한다.
  • Return Values : Common — Link
  • stderr : 명령의 표준 에러
  • stderr_lines : 표준 에러 출력을 행 단위로 구분한 목록
  • stdout : 명령의 표준 출력
  • stdout_lines : 표준 출력을 행 단위로 구분한 목록
  • rc : ‘return code’ 반환 코드
  • msg : 사용자가 전달한 일반 문자열 메시지
## loop_register1.yml
---
- hosts: localhost
tasks:
- name: Loop echo test
ansible.builtin.shell: "echo 'I can speak {{ item }}'"
loop:
- Rust
- Golang
register: result
- name: Show result
ansible.builtin.debug:
msg: "Stdout: {{ item.stdout }}"
loop: "{{ result.results }}"
ubuntu@server:~/my-ansible$ ansible-playbook loop_register1.yml

PLAY [localhost] ********************************************************************************************************
TASK [Loop echo test] ***************************************************************************************************
changed: [localhost] => (item=Rust)
changed: [localhost] => (item=Golang)
TASK [Show result] ******************************************************************************************************
ok: [localhost] => (item={'changed': True, 'stdout': 'I can speak Rust', 'stderr': '', 'rc': 0, 'cmd': "echo 'I can speak Rust'", 'start': '2024-01-20 15:04:57.932072', 'end': '2024-01-20 15:04:57.935678', 'delta': '0:00:00.003606', 'msg': '', 'invocation': {'module_args': {'_raw_params': "echo 'I can speak Rust'", '_uses_shell': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['I can speak Rust'], 'stderr_lines': [], 'failed': False, 'item': 'Rust', 'ansible_loop_var': 'item'}) => {
"msg": "Stdout: I can speak Rust"
}
ok: [localhost] => (item={'changed': True, 'stdout': 'I can speak Golang', 'stderr': '', 'rc': 0, 'cmd': "echo 'I can speak Golang'", 'start': '2024-01-20 15:04:58.156983', 'end': '2024-01-20 15:04:58.160636', 'delta': '0:00:00.003653', 'msg': '', 'invocation': {'module_args': {'_raw_params': "echo 'I can speak Golang'", '_uses_shell': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['I can speak Golang'], 'stderr_lines': [], 'failed': False, 'item': 'Golang', 'ansible_loop_var': 'item'}) => {
"msg": "Stdout: I can speak Golang"
}
PLAY RECAP **************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

도전과제 1

  • 리눅스 user1~10(10명) 를 반복문을 통해서 생성 후 확인 후 삭제를 해보자
  1. 생성의 과정
## assignment_1.yml
---- hosts: all
tasks:
- name: Assignment 1_Create Linux user # :은 문법 상 허용하지 않음
ansible.builtin.user:
name: user"{{ item }}"
state: present
loop: "{{ range(1, 11)|list }}"
ubuntu@server:~/my-ansible$ ansible-playbook assignment_1.yml

PLAY [all] **************************************************************************************************************
TASK [Assignment 1_Create Linux user] ***********************************************************************************
changed: [tnode3] => (item=1)
changed: [tnode1] => (item=1)
changed: [tnode2] => (item=1)
changed: [tnode1] => (item=2)
changed: [tnode3] => (item=2)
changed: [tnode2] => (item=2)
changed: [tnode1] => (item=3)
changed: [tnode3] => (item=3)
changed: [tnode2] => (item=3)
changed: [tnode1] => (item=4)
changed: [tnode3] => (item=4)
changed: [tnode2] => (item=4)
changed: [tnode1] => (item=5)
changed: [tnode3] => (item=5)
changed: [tnode2] => (item=5)
changed: [tnode1] => (item=6)
changed: [tnode3] => (item=6)
changed: [tnode1] => (item=7)
changed: [tnode2] => (item=6)
changed: [tnode3] => (item=7)
changed: [tnode1] => (item=8)
changed: [tnode2] => (item=7)
changed: [tnode3] => (item=8)
changed: [tnode1] => (item=9)
changed: [tnode2] => (item=8)
changed: [tnode3] => (item=9)
changed: [tnode1] => (item=10)
changed: [tnode2] => (item=9)
changed: [tnode3] => (item=10)
changed: [tnode2] => (item=10)
PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
  • 잘 생성되었는지 검증해본다
ubuntu@server:~/my-ansible$ for i in {1..3}; do echo ">> tnode$i <<"; ssh tnode$i tail -n 10 /etc/passwd; echo; done
>> tnode1 <<
user"1":x:1001:1001::/home/user"1":/bin/sh
user"2":x:1002:1002::/home/user"2":/bin/sh
user"3":x:1003:1003::/home/user"3":/bin/sh
user"4":x:1004:1004::/home/user"4":/bin/sh
user"5":x:1005:1005::/home/user"5":/bin/sh
user"6":x:1006:1006::/home/user"6":/bin/sh
user"7":x:1007:1007::/home/user"7":/bin/sh
user"8":x:1008:1008::/home/user"8":/bin/sh
user"9":x:1009:1009::/home/user"9":/bin/sh
user"10":x:1010:1010::/home/user"10":/bin/sh

>> tnode2 <<
user"1":x:1001:1001::/home/user"1":/bin/sh
user"2":x:1002:1002::/home/user"2":/bin/sh
user"3":x:1003:1003::/home/user"3":/bin/sh
user"4":x:1004:1004::/home/user"4":/bin/sh
user"5":x:1005:1005::/home/user"5":/bin/sh
user"6":x:1006:1006::/home/user"6":/bin/sh
user"7":x:1007:1007::/home/user"7":/bin/sh
user"8":x:1008:1008::/home/user"8":/bin/sh
user"9":x:1009:1009::/home/user"9":/bin/sh
user"10":x:1010:1010::/home/user"10":/bin/sh
>> tnode3 <<
user"1":x:1001:1001::/home/user"1":/bin/sh
user"2":x:1002:1002::/home/user"2":/bin/sh
user"3":x:1003:1003::/home/user"3":/bin/sh
user"4":x:1004:1004::/home/user"4":/bin/sh
user"5":x:1005:1005::/home/user"5":/bin/sh
user"6":x:1006:1006::/home/user"6":/bin/sh
user"7":x:1007:1007::/home/user"7":/bin/sh
user"8":x:1008:1008::/home/user"8":/bin/sh
user"9":x:1009:1009::/home/user"9":/bin/sh
user"10":x:1010:1010::/home/user"10":/bin/sh
  1. 삭제의 과정
  • state: absent: This parameter ensures that the specified user does not exist on the target system. When state is set to absent, Ansible will remove the user account if it exists. If the user account does not exist, Ansible will do nothing.
  • Additionally, in your playbook, you have the remove: yes parameter. This means that when the user account is removed, its home directory and mail spool will also be removed. This is an important aspect to note because deleting a user without this parameter will leave the user's home directory and mail spool on the system.
  • The loop: "{{ range(1, 11)|list }}" part of the playbook creates a loop that iterates over a range of numbers from 1 to 10. For each iteration, a user with the name user followed by the number (like user1, user2, up to user10) will be targeted for removal.
## assignment_1_2.yml

---
- hosts: all
tasks:
- name: Assignment 1_Remove Linux user
ansible.builtin.user:
name: user"{{ item }}"
state: absent
remove: yes
loop: "{{ range(1, 11)|list }}"

도전과제 2

  • loop 반복문 중 sequence 를 이용하여 /var/log/test1 ~ /var/log/test100 100개 파일(file 모듈)을 생성 확인 후 삭제를 해보자
  • 생성 플레이북
## assignment_2_1.yml
---
- hosts: all
tasks:
- name: Create files
ansible.builtin.file:
path: /var/log/test{{ item }}
state: touch
with_sequence: start=1 end=100
  • 비동기로 처리해보자
## assignment_2_1_4.yml
---
- hosts: all
tasks:
- name: Create files asynchronously
ansible.builtin.file:
path: "/var/log/test{{ item }}"
state: touch
with_sequence: start=1 end=100
async: 10 # Time in seconds to wait for a response
poll: 0 # Fire and forget, check back later
register: async_results
- name: Wait for the async tasks to complete
async_status:
jid: "{{ item.ansible_job_id }}"
loop: "{{ async_results.results }}"
loop_control:
extended: yes
register: job_results
until: job_results.finished
retries: 20 # Number of retries
delay: 5 # Delay in seconds between retries
when: item.ansible_job_id is defined
  • 두 내용을 비교하는 쉘 스크립트 코드
#!/bin/bash

# Define playbook paths
original_playbook="path_to_original_playbook.yml"
with_sequence_playbook="path_to_with_sequence_playbook.yml"
# Function to run playbook and calculate duration
run_playbook() {
local playbook=$1
local start_time=$(date +%s)
ansible-playbook $playbook
local end_time=$(date +%s)
local duration=$((end_time - start_time))
echo "Execution time for $playbook: $duration seconds"
}
# Run original playbook and record timing
echo "Running original playbook..."
run_playbook $original_playbook
# Run playbook with 'with_sequence' and record timing
echo "Running playbook with 'with_sequence'..."
run_playbook $with_sequence_playbook

비동기 처리

기본적으로 실행하도 있는 작업들은 동기 방식이며 태스크가 실행이 완료되고 다음 태스크가 실행된다. 이 때 물론 비동기 방식이 필요한 경우가 있다. 터미널이 하나밖에 없고, 여러개의 명령어를 내리고 싶은데 어떤 명령어 실행이 오래걸릴때 백그라운드에서 실행시킨다. 오래 걸리는 작업을(wget, 배치성 task 등) 백그라운드로 놓고 다른작업을 한 후 그 작업이 필요한 경우 동기화를 시킨다.

  • ssh 세션 타임아웃방지 (poll>0) 도 가능하다
  • 데비안 계열은 세션 제한이 없는데 래드햇 계열은 세션 제한이 300초가 있다. 300초동안 아무 작업이 없으면 끊어진다.
  • async 비동기 방식으로 실행할 타임아웃 시간은 충분히 길게 잡아준다.
  • poll값 polling 5초마다 한번씩 확인하는데 (”너 끝났니”) ssh세션 타임아웃 타이머가 초기화된다.
  • poll=0이라고 설정된 경우이다.
  • 비동기 방식은 poll 값을 0으로 설정하게 되면 이 작업이 완료될때까지 기다리지 않고 넘어가지만 실제로는 백그라운드에서 실행하고 있다. 이를 fire and forget이라고 부른다.
  • async_status : 비동기방식으로 실행한 작업의 상태를 알기위한 모듈
  • jid (job task identify/job id ) : 비동기 방식으로 실행한 모든 작업들은 jid가 존재한다. 등록변수로 설정하여 jid를 알아낸다
  • until : 반복문으로 테스트문이 참이 될떄까지 반복한다.
  • retries : 재시도 횟수
  • delay : 재시도 간격
  • 비동기 처리 건의 로그 보기
PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Execution time for assignment_2_1.yml: 42 seconds
Running playbook with 'with_sequence'...
PLAY RECAP **************************************************************************************************************
tnode1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Execution time for assignment_2_1_4.yml: 96 seconds
  • 왜 비동기 요청이 더 24초나 걸렸을까?
  1. Overhead of Asynchronous Management: Each asynchronous task in Ansible requires additional management. The playbook needs to fire off the task, register its job ID, and then later check back on each job ID’s status. This process can introduce significant overhead, especially when dealing with a large number of tasks (100 in your case).
  2. Nature of the Task: The task itself (creating an empty file using touch) is very lightweight and quick. The overhead of managing the asynchronous process might outweigh the time it takes to simply execute these tasks synchronously.
  3. Polling and Retry Overhead: The asynchronous version of your playbook checks back on the status of each task with a series of retries and delays, which can cumulatively add up to more time than the synchronous execution.
  • 그렇다면 배치 요청을 해볼 수 있겠다.
## assignment_2_1_6.yml
---
- hosts: all
tasks:
- name: Create multiple files using a shell script
ansible.builtin.shell: |
for i in {1..100}; do
touch /var/log/test${i}
done
async: 60 # Increased async duration
poll: 0
register: shell_async
- name: Wait for a short period before checking the task status
ansible.builtin.pause:
seconds: 10 # Wait for 10 seconds before the first check
when: shell_async.ansible_job_id is defined
- name: Wait for the async task to complete
async_status:
jid: "{{ shell_async.ansible_job_id }}"
register: job_result
until: job_result.finished | bool
retries: 3 # Reduced number of retries
delay: 5 # Increased delay
when: shell_async.ansible_job_id is defined
  • 삭제 요청도 마찬가지이다.
## remove-file100.yml
---
- hosts: all
tasks:
- name: Remove files
ansible.builtin.file:
path: /var/log/test{{ item }}
state: absent
with_sequence: start=1 end=100
## remove-file100-async.yml
---
- hosts: all
tasks:
- name: Remove files asynchronously
ansible.builtin.file:
path: "/var/log/test{{ item }}"
state: absent
with_sequence: start=1 end=100
async: 10 # Time in seconds to wait for a response
poll: 0 # Fire and forget, check back later
register: async_results
- name: Wait for a short period before checking the task status
ansible.builtin.pause:
seconds: 10 # Wait for 10 seconds before the first check
when: async_results.results | default([]) | length > 0
- name: Check on async task
async_status:
jid: "{{ item.ansible_job_id }}"
loop: "{{ async_results.results }}"
loop_control:
extended: yes
register: job_result
until: job_result.finished | default(true) | bool
retries: 20 # Number of retries
delay: 5 # Delay in seconds between retries
when: item.ansible_job_id is defined
  • 확인
ubuntu@server:~/my-ansible$ ansible -m shell -a "ls -l /var/log/test100" all
tnode1 | FAILED | rc=2 >>
ls: cannot access '/var/log/test100': No such file or directorynon-zero return code
tnode2 | FAILED | rc=2 >>
ls: cannot access '/var/log/test100': No such file or directorynon-zero return code
tnode3 | FAILED | rc=2 >>
ls: cannot access '/var/log/test100': No such file or directorynon-zero return code

조건문

https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_conditionals.html

앤서블은 조건문을 사용하여 특정 조건이 충족될 때 작업 또는 플레이를 실행할 수 있다.

예를 들면 조건문을 사용하여 호스트의 운영체제 버전에 해당하는 서비스를 설치하는 식이 있다.

앤서블에서 조건문을 사용할 때는 플레이 변수, 작업 변수, 앤서블 팩트 등을 사용할 수 있다.

  • 조건 작업 구문 : when 문은 조건부로 작업을 실행할 때 테스트할 조건을 값으로 사용합니다.
  • 조건이 충족되면 작업이 실행되고, 조건이 충족되지 않으면 작업을 건너뜁니다.
  • when 문을 테스트하는 가장 간단한 조건 중 하나는 boolean 변수가 true인지 false인지 여부입니다.
  • run_my_task 변수에 true 값, when 문에서 run_my_task를 사용하면 true인 경우에만 작업이 실행되는 플레이북이다.
---
- 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
# **ansible-playbook when_task.yml**
...
*TASK [**echo message**] ***************************************************************************************************************************************************************************
changed: [localhost]
...
# when_task_2.yml---
- hosts: localhost
vars:
run_my_task: false
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 값을 false 로 준다면 정상적으로 skip 된 것을 알 수 있다
ubuntu@server:~/my-ansible$ ansible-playbook when_task_2.yml
PLAY [localhost] ********************************************************************************************************TASK [echo message] *****************************************************************************************************
skipping: [localhost]
TASK [Show result] ******************************************************************************************************
ok: [localhost] => {
"result": {
"changed": false,
"false_condition": "run_my_task",
"skip_reason": "Conditional result was False",
"skipped": true
}
}
PLAY RECAP **************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0*
  • 조건 연산자 : when 문에 bool 변수(true, false) 외에도 조건 연산자를 사용할 수 있다.
  • 예를 들어 when 문에 ansible_facts[’machine’] == “x86_64” 라는 구문을 사용했다면 ansible_facts[’machine’] 값이 x86_64일 때만 해당 태스크를 수행합니다.
  • vars 키워드로 supported_distros 라는 변수를 사전 타입의 값으로 저장하고 태스크의 when 구문에서 ansible_facts[’distribution’] in supported_distros 라는 조건문을 추가하는 플레이북이다.
---
- 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@server:~/my-ansible$ ansible-playbook check-os.yml
PLAY [all] **************************************************************************************************************TASK [Print supported os] ***********************************************************************************************
ok: [tnode1] => {
"msg": "This Ubuntu need to use apt"
}
ok: [tnode2] => {
"msg": "This Ubuntu need to use apt"
}
ok: [tnode3] => {
"msg": "This Ubuntu need to use apt"
}
PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
  • 복수 연산자: when 문은 단일 조건문 뿐만 아니라 복수 조건문도 사용할 수 있습니다.
  • 운영체제가 CentOS이거나 우분투일 경우 작업이 수행되는 예제이다.
## check_os1.yml
---
- 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"


# ubuntu@server:~/my-ansible$ ansible-playbook check-os1.yml

PLAY [all] **************************************************************************************************************

TASK [Print os type] ****************************************************************************************************
ok: [tnode1] => {
"msg": "OS Type Ubuntu"
}
ok: [tnode2] => {
"msg": "OS Type Ubuntu"
}
ok: [tnode3] => {
"msg": "OS Type Ubuntu"
}

PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
  • 운영체제가 ubuntu 이고 버전이 22.04 인 경우에만 메시지를 출력하는 플레이북을 작성
## check_os2.yml
---
- hosts: all

tasks:
- name: Print os type
ansible.builtin.debug:
msg: >-
OS Type: {{ ansible_facts['distribution'] }}
OS Version: {{ ansible_facts['distribution_version'] }}
when: ansible_facts['distribution'] == "Ubuntu" and ansible_facts['distribution_version'] == "22.04"



## 사전 형태로 작성

---
- hosts: all

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


## and-or 혼용

---
- hosts: all

tasks:
- name: Print os type
ansible.builtin.debug:
msg: >-
OS Type: {{ ansible_facts['distribution'] }}
OS Version: {{ ansible_facts['distribution_version'] }}
when: >
( ansible_facts['distribution'] == "CentOS" and
ansible_facts['distribution_version'] == "8" )
or
( ansible_facts['distribution'] == "Ubuntu" and
ansible_facts['distribution_version'] == "22.04" )
ubuntu@server:~/my-ansible$ ansible-playbook check-os2.yml

PLAY [all] **************************************************************************************************************

TASK [Print os type] ****************************************************************************************************
ok: [tnode1] => {
"msg": "OS Type: Ubuntu OS Version: 22.04"
}
ok: [tnode2] => {
"msg": "OS Type: Ubuntu OS Version: 22.04"
}
ok: [tnode3] => {
"msg": "OS Type: Ubuntu OS Version: 22.04"
}

PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

도전 과제 3

  • Ubuntu OS이면서 fqdn으로 tnode1 인 경우, debug 모듈을 사용하여 OS 정보와 fqdn 정보를 출력해봅시다.
  • Private IP DNS name (IPv4 only): ip-10–10–1–11.ap-northeast-2.compute.internal
## fqdn.yml
---
- hosts: all

tasks:
- name: Print os type
ansible.builtin.debug:
msg: >-
fqdn: {{ ansible_facts['fqdn'] }}
OS: {{ ansible_facts['distribution'] }}
when:
- ansible_facts['fqdn'] == "ip-10-10-1-11.ap-northeast-2.compute.internal"
- ansible_facts['distribution'] == "Ubuntu"
ubuntu@server:~/my-ansible$ ansible-playbook fqdn.yml

PLAY [all] **************************************************************************************************************

TASK [Print os type] ****************************************************************************************************
ok: [tnode1] => {
"msg": "fqdn: ip-10-10-1-11.ap-northeast-2.compute.internal OS: Ubuntu"
}
skipping: [tnode2]
skipping: [tnode3]

PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=0 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
tnode3 : ok=0 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
  • 반복문과 조건문 사용
  • when 문의 item[’mount’]은 loop문에서 선언한 ansible_facts의 mounts 중 mount 값과 size_available 값을 사용해 구현할 수 있다.
---
- 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
  • 앤서블 팩트에서 mounts라는 사전 타입의 변수값을 반복하면서 mount가 ‘/’ 이고 size_available 값이 ‘300000000’(300메가)보다 큰 경우에만 메시지를 출력하고, 그렇지 않을 경우에는 작업을 건너뛴다.
TASK [Print Root Directory Size] ****************************************************************************************
ok: [tnode3] => (item={'block_available': 7108644, 'block_size': 4096, 'block_total': 7574288, 'block_used': 465644, 'device': '/dev/root', 'fstype': 'ext4', 'inode_available': 3796111, 'inode_total': 3870720, 'inode_used': 74609, 'mount': '/', 'options': 'rw,relatime,discard,errors=remount-ro', 'size_available': 29117005824, 'size_total': 31024283648, 'uuid': '7b61cee2-bd68-4c24-a525-e8d8e64c0d5b'}) => {
"msg": "Directory / size is 29117005824"
}
skipping: [tnode3] => (item={'block_available': 0, 'block_size': 131072, 'block_total': 200, 'block_used': 200, 'device': '/dev/loop0', 'fstype': 'squashfs', 'inode_available': 0, 'inode_total': 16, 'inode_used': 16, 'mount': '/snap/amazon-ssm-agent/7628', 'options': 'ro,nodev,relatime,errors=continue,threads=single', 'size_available': 0, 'size_total': 26214400, 'uuid': 'N/A'})
skipping: [tnode3] => (item={'block_available': 0, 'block_size': 131072, 'block_total': 446, 'block_used': 446, 'device': '/dev/loop1', 'fstype': 'squashfs', 'inode_available': 0, 'inode_total': 10944, 'inode_used': 10944, 'mount': '/snap/core18/2812', 'options': 'ro,nodev,relatime,errors=continue,threads=single', 'size_available': 0, 'size_total': 58458112, 'uuid': 'N/A'})
skipping: [tnode3] => (item={'block_available': 0, 'block_size': 131072, 'block_total': 512, 'block_used': 512, 'device': '/dev/loop2', 'fstype': 'squashfs', 'inode_available': 0, 'inode_total': 12041, 'inode_used': 12041, 'mount': '/snap/core20/2105', 'options': 'ro,nodev,relatime,errors=continue,threads=single', 'size_available': 0, 'size_total': 67108864, 'uuid': 'N/A'})
skipping: [tnode3] => (item={'block_available': 0, 'block_size': 131072, 'block_total': 324, 'block_used': 324, 'device': '/dev/loop4', 'fstype': 'squashfs', 'inode_available': 0, 'inode_total': 658, 'inode_used': 658, 'mount': '/snap/snapd/20671', 'options': 'ro,nodev,relatime,errors=continue,threads=single', 'size_available': 0, 'size_total': 42467328, 'uuid': 'N/A'})
skipping: [tnode3] => (item={'block_available': 0, 'block_size': 131072, 'block_total': 896, 'block_used': 896, 'device': '/dev/loop3', 'fstype': 'squashfs', 'inode_available': 0, 'inode_total': 873, 'inode_used': 873, 'mount': '/snap/lxd/24322', 'options': 'ro,nodev,relatime,errors=continue,threads=single', 'size_available': 0, 'size_total': 117440512, 'uuid': 'N/A'})
skipping: [tnode3] => (item={'block_available': 201292, 'block_size': 512, 'block_total': 213663, 'block_used': 12371, 'device': '/dev/nvme0n1p15', 'fstype': 'vfat', 'inode_available': 0, 'inode_total': 0, 'inode_used': 0, 'mount': '/boot/efi', 'options': 'rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro', 'size_available': 103061504, 'size_total': 109395456, 'uuid': '2917-8887'})

PLAY RECAP **************************************************************************************************************
tnode3 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
---
- 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"
ubuntu@server:~/my-ansible$ ansible-playbook register-when.yml

PLAY [all] **************************************************************************************************************

TASK [Get rsyslog service status] ***************************************************************************************
changed: [tnode1]
changed: [tnode2]
changed: [tnode3]

TASK [Print rsyslog status] *********************************************************************************************
ok: [tnode1] => {
"msg": "Rsyslog status is active"
}
ok: [tnode2] => {
"msg": "Rsyslog status is active"
}
ok: [tnode3] => {
"msg": "Rsyslog status is active"
}

PLAY RECAP **************************************************************************************************************
tnode1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode3 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

도전 과제 4

  • fqdn의 예제를 확장하여 반복문과 조건문을 같이 사용하도록 구현한다.
## complex_condition_loop.yml
---
- hosts: all
tasks:
- name: Check OS and Perform Action
ansible.builtin.debug:
msg: "Performing action on {{ ansible_facts['fqdn'] }} with OS {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"
loop: "{{ ansible_play_hosts }}"
when:
- (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_version'] == "8") or
(ansible_facts['distribution'] == "Ubuntu" and ansible_facts['distribution_version'] == "22.04")
- ansible_facts['fqdn'] == "ip-10-10-1-11.ap-northeast-2.compute.internal"
ubuntu@server:~/my-ansible$ ansible-playbook complex.yml

PLAY [all] **************************************************************************************************************

TASK [Check OS and Perform Action] **************************************************************************************
ok: [tnode1] => (item=tnode1) => {
"msg": "Performing action on ip-10-10-1-11.ap-northeast-2.compute.internal with OS Ubuntu 22.04"
}
skipping: [tnode3] => (item=tnode1)
skipping: [tnode3] => (item=tnode2)
skipping: [tnode3] => (item=tnode3)
skipping: [tnode3]
skipping: [tnode2] => (item=tnode1)
ok: [tnode1] => (item=tnode2) => {
"msg": "Performing action on ip-10-10-1-11.ap-northeast-2.compute.internal with OS Ubuntu 22.04"
}
skipping: [tnode2] => (item=tnode2)
skipping: [tnode2] => (item=tnode3)
skipping: [tnode2]
ok: [tnode1] => (item=tnode3) => {
"msg": "Performing action on ip-10-10-1-11.ap-northeast-2.compute.internal with OS Ubuntu 22.04"
}

PLAY RECAP **************************************************************************************************************
tnode1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
tnode2 : ok=0 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
tnode3 : ok=0 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0

핸들러

  • https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_handlers.html
  • 서비스의 구성 파일을 변경하려면 변경 내용이 적용되도록 서비스를 다시 로드해야 한다. 이때 핸들러는 다른 작업에서 트리거한 알림에 응답하는 작업이며, 해당 호스트에서 작업이 변경될 때만 핸들러통지한다.
  • 앤서블에서 핸들러를 사용하려면 notify 문을 사용하여 명시적으로 호출된 경우에만 사용할 수 있다.
  • 또한 핸들러를 정의할 때는 같은 이름으로 여러 개의 핸들러를 정의하기보다는 각각의 고유한 이름을 사용하여 정의하는 것이 좋다.
  • rsyslog 재시작 태스크가 실행되면 notify 키워드를 통해 print msg라는 핸들러를 호출하는 플레이북을 작성한다.

플레이북 예제

  • rsyslog 재시작 태스크가 실행되면 notify 키워드를 통해 print msg라는 핸들러를 호출한다. 핸들러는 handlers 키워드로 시작한다.
---
- 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"
ubuntu@server:~/my-ansible$ ansible-playbook handler-sample.yml
PLAY [tnode2] ***********************************************************************************************************TASK [restart rsyslog] **************************************************************************************************
changed: [tnode2]
RUNNING HANDLER [print msg] *********************************************************************************************
ok: [tnode2] => {
"msg": "rsyslog is restarted"
}
PLAY RECAP **************************************************************************************************************
tnode2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

도전 과제 5

---
- hosts : all

tasks:
- name: Install apache httpd (state=present is optional)
ansible.builtin.apt:
name: apache2
state: present
notify:
- restart Apache2
- print-msg

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

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

ansible 사용 시 셸 스크립트 보다는 모듈을 사용하자!

command 계열 모듈 사용 시

앤서블에서 셸 스크립트를 실행한 뒤 결과로 실패 또는 에러 메시지를 출력해도, 앤서블에서는 작업이 성공했다고 간주한다.

어떤 명령이라도 실행된 경우에는 태스크 실행 상태를 항상 changed 가 되는데 이런 경우 failed_when 키워드를 사용하여 작업이 실패했음을 나타내는 조건을 지정할 수 있다.

에러 무시하고 진행하기

  • 앤서블은 플레이 시 각 작업의 반환 코드를 평가하여 작업의 성공 여부를 판단한다.
  • 일반적으로 작업이 실패하면 앤서블은 이후의 모든 작업을 건너뛴다.
  • 하지만 작업이 실패해도 플레이를 계속 실행할 수 있다. 이는 **ignore_errors**라는 키워드로 구현할 수 있다.
---
- 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"
ubuntu@server:~/my-ansible$ ansible-playbook handler_apache_2.yaml

PLAY [tnode1] ***********************************************************************************************************

TASK [Install apache3] **************************************************************************************************
fatal: [tnode1]: FAILED! => {"changed": false, "msg": "No package matching 'apache3' is available"}
...ignoring

TASK [Print msg] ********************************************************************************************************
ok: [tnode1] => {
"msg": "Before task is ignored"
}

PLAY RECAP **************************************************************************************************************
tnode1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
  • 앤서블은 일반적으로 작업이 실패하고 해당 호스트에서 플레이가 중단되면 이전 작업에서 알림을 받은 모든 핸들러가 실행되지 않는다. 하지만 플레이북에 force_handlers: yes 키워드를 설정하면 이후 작업이 실패하여 플레이가 중단되어도 알림을 받은 핸들러가 호출된다.
  • hosts 아래 force_handlers: yes 를 추가하고, install apache2 태스크를 추가한 플레이북 예시이다.
## force-handler.yml

---
- hosts: tnode2
force_handlers: yes

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

- name: install apache3
ansible.builtin.apt:
name: "apache3"
state: latest

handlers:
- name: print msg
ansible.builtin.debug:
msg: "rsyslog is restarted"
ubuntu@server:~/my-ansible$ ansible-playbook force-handler.yml

PLAY [tnode2] ***********************************************************************************************************

TASK [restart rsyslog] **************************************************************************************************
changed: [tnode2]

TASK [install apache3] **************************************************************************************************
fatal: [tnode2]: FAILED! => {"changed": false, "msg": "No package matching 'apache3' is available"}

RUNNING HANDLER [print msg] *********************************************************************************************
ok: [tnode2] => {
"msg": "rsyslog is restarted"
}

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

Role

  • 롤의 특징은 바로 사용 가능하다는 점이다.
  • 가령 음식을 주문하려고 할 때 우리는 다음과 같은 프로세스를 거친다.
  1. 배달할 음식을 검색
  2. 구매
  3. 음식을 받음
  4. 음식의 포장을 뜯음
  • 위의 과정은 롤을 실행하는 과정과 동일하다.
  1. 어떠한 작업을 할지 검색 (예를 들어 nginx 설치, mysql 설치 등) -> 해당 작업에 필요한 Role들을 찾아줌
  2. 적절한 Role 선택
  3. 적용
  4. 결과
  • 롤은 모든 사람들이 사용하도록 규정을 정해놓았다.

Role의 구조

  • handlers : 핸들러가 담기는 디렉터리
  • defaults : 디폴트 인자가 들어가는 디렉터리
  • vars : 인자가 정의되는 디렉터리
  • files : 배포될 파일들이 위치하는 디렉터리
  • templates : 배포에 사용될 템플릿들이 들어가는 디렉터리
  • meta : 다른 Role과 의존성이 있는 경우 해당 롤을 명시
  • tasks : 지금까지 진행했던 기본 Task를 넣는 공간
ansible-galaxy role init my-role
- Role my-role was created successfully
#
**tree ./my-role/**
my-role/
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
8 directories, 8 files

간단한 플레이북을 개발해보자

각 구조에 맞게 테스크를 작성하여 프로세스를 완성해보자.

  • 롤이 호출되면 현재 호스트의 운영체제 버전이 지원 운영체제 목록에 포함되는지 확인한다.
  • 운영체제가 CentOS나 레드햇이면 httpd 관련 패키지를 dnf 모듈을 이용해 설치한다.
  • 설치가 끝나면 제어 노드의 files 디렉터리 안에 있는 index.html 파일을 관리 노드의 /var/www/html 디렉터리에 복사한다.
  • 파일 복사가 끝나면 httpd 서비스를 재시작한다.

이 때 롤 구조는 어떻게 되어야 할까?

  • 롤 이름 : my-role
  • tasks (메인 태스크)
  • install service : httpd 관련 패키지 설치
  • copy html file : index.html 파일 복사
  • files (정적 파일) / index.html
  • handlers (핸들러) / restart service : httpd 서비스 재시작
  • defaults (가변 변수) : 메인 태스크에서 사용된 변수 선언 / service_title
  • vars (불변 변수) : 메인 태스크와 핸들러에서 사용된 변수 선언
  • service_name : 서비스명
  • src_file_path : 복사할 파일 경로
  • dest_file_path : 파일이 복사될 디렉터리 경로
  • httpd_packages : httpd 관련 패키지 목록
  • supported_distros : 지원 OS 목록

코드 예시

  • 첫 번째 태스크인 install service에는 플레이북에서 변수로 정의한 서비스명을 함께 출력하고 서비스 설치가 끝나면 ansible.builtin.copy 모듈을 이용하여 파일을 복사한다.
  • 두 번째 태스크는 첫 번째 태스크가 복사가 끝나면 restart servie라는 핸들러를 호출할 때 실행된다.
---
# tasks file for my-role

- name: install service {{ service_title }}
ansible.builtin.apt: # 여기서 ansible.builtin.apt 모듈을 이용하여 httpd 관련 패키지를 설치한다.
name: "{{ item }}"
state: latest
loop: "{{ httpd_packages }}" # 이 때 관련 패키지는 여러 개이며 loop 문을 사용한다.
when: ansible_facts.distribution in supported_distros

- name: copy conf file
ansible.builtin.copy:
src: "{{ src_file_path }}"
dest: "{{ dest_file_path }}"
notify:
- restart service


---
# handlers file for my-role

- name: restart service
ansible.builtin.service:
name: "{{ service_name }}"
state: restarted

---
echo 'service_title: "Apache Web Server"' >> defaults/main.yml # defaults 가변변수

---
# vars file for my-role # 한번 정의되면 외부로부터 변수 값을 수정 할 수 없음. 롤 내의 플레이북에서만 사용되는 변수로 정의하는 것이 좋음

service_name: apache2
src_file_path: ../files/index.html
dest_file_path: /var/www/html
httpd_packages:
- apache2
- apache2-doc

supported_distros:
- Ubuntu
  • 플레이북에 롤을 추가하기
  • 플레이북에 롤을 추가하려면 ansible.builtin.import_roleansible.builtin.include_role모듈 2가지 방법이 있다.
  • ansible.builtin.import_role은 롤을 정적으로 추가하며, ansible.builtin.include_role는 롤을 동적으로 추가한다.
  • 정적으로 롤을 추가한다는 건 고정된 롤을 추가하겠다는 의미이며, 동적으로 추가한다는 건 반복문이나 조건문에 의해 롤이 변경될 수 있다는 의미이다.
---
- hosts: tnode1 # ~/my-ansible/role-example.yml

tasks:
- name: Print start play
ansible.builtin.debug:
msg: "Let's start role play"

- name: Install Service by role
ansible.builtin.import_role:
name: my-role
vars:
service_title: Httpd
#
ansible-playbook role-example.yml
...
TASK [Print start play] *****************************************************************************************************************
ok: [tnode1-ubuntu.local] => {
"msg": "Let's start role play"
}

TASK [my-role : install service Apache Web Server] **************************************************************************************
changed: [tnode1-ubuntu.local] => (item=apache2)
changed: [tnode1-ubuntu.local] => (item=apache2-doc)

TASK [my-role : copy conf file] *********************************************************************************************************
changed: [tnode1-ubuntu.local]

RUNNING HANDLER [my-role : restart service] *********************************************************************************************
changed: [tnode1-ubuntu.local]
...

# 확인
curl tnode1
Hello! Ansible

Reference

https://velog.io/@euijoo3233/Ansible-Study-2

--

--