Devops
블로그 불러오는 중...
Devops
앤서블(Ansible)은 IT 환경의 다양한 작업을 자동화하는 오픈소스 도구로, 여러 서버의 프로비저닝, 구성 관리, 애플리케이션 배포 등을 코드로 정의하여 효율적으로 관리하게 해주며, 에이전트 설치 없이 SSH 접속을 통해 관리하는 특징이 있습니다. 즉, 반복적인 IT 업무를 자동화하여 생산성을 높이는 데 사용됩니다.
| Node | OS | Kernel | vCPU | Memory | Disk | NIC2 IP | 관리자 계정 | (기본) 일반 계정 |
|---|---|---|---|---|---|---|---|---|
| server | Ubuntu 24.04 | 6.8.0 | 2 | 1.5GB | 30GB | 10.10.1.10 | root / qwe123 | vagrant / qwe123 |
| tnode1 | 상동 | 상동 | 2 | 1.5GB | 30GB | 10.10.1.11 | root / qwe123 | vagrant / qwe123 |
| tnode2 | 상동 | 상동 | 2 | 1.5GB | 30GB | 10.10.1.12 | root / qwe123 | vagrant / qwe123 |
| tnode3 | Rocky Linux 9 | 5.14.0 | 2 | 1.5GB | 60GB | 10.10.1.13 | root / qwe123 | vagrant / qwe123 |
가상 머신 구동을 위한 vagrant file 을 추가한다. vagrant file 은 가시다님의 github 에서 받을 수 있으며 아래에 다운받는 스크립트도 있다.
mkdir ansible
cd ansible
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/ansible/Vagrantfile
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/ansible/init_cfg.sh
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/ansible/init_cfg2.sh
vagrant up
VagrantFile
# Variables
# max number of T nodes
N = 2
# Base Image https://portal.cloud.hashicorp.com/vagrant/discover/bento/ubuntu-24.04
BOX_IMAGE = "bento/ubuntu-24.04"
BOX_VERSION = "202510.26.0"
Vagrant.configure("2") do |config|
#-Server Node : Ubuntu
config.vm.define "server" do |subconfig|
subconfig.vm.box = BOX_IMAGE
subconfig.vm.box_version = BOX_VERSION
subconfig.vm.provider "virtualbox" do |vb|
vb.customize ["modifyvm", :id, "--groups", "/Ansible-Lab"]
vb.name = "server"
vb.cpus = 2
vb.memory = 1536 # 2048
vb.linked_clone = true
end
subconfig.vm.host_name = "server"
subconfig.vm.network "private_network", ip: "10.10.1.10"
subconfig.vm.network "forwarded_port", guest: 22, host: 60000, auto_correct: true, id: "ssh"
subconfig.vm.synced_folder "./", "/vagrant", disabled: true
subconfig.vm.provision "shell", path: "init_cfg.sh"
end
# Test Node : Ubuntu
(1..N).each do |i|
config.vm.define "tnode#{i}" do |subconfig|
subconfig.vm.box = BOX_IMAGE
subconfig.vm.box_version = BOX_VERSION
subconfig.vm.provider "virtualbox" do |vb|
vb.customize ["modifyvm", :id, "--groups", "/Ansible-Lab"]
vb.name = "tnode#{i}"
vb.cpus = 2
vb.memory = 1536 # 2048
vb.linked_clone = true
end
subconfig.vm.host_name = "tnode#{i}"
subconfig.vm.network "private_network", ip: "10.10.1.1#{i}"
subconfig.vm.network "forwarded_port", guest: 22, host: "6000#{i}", auto_correct: true, id: "ssh"
subconfig.vm.synced_folder "./", "/vagrant", disabled: true
subconfig.vm.provision "shell", path: "init_cfg.sh", args: [ N ]
end
end
# Test Node : Rocky Linux
config.vm.define "tnode3" do |subconfig|
subconfig.vm.box = "bento/rockylinux-9"
subconfig.vm.box_version = "202510.26.0"
subconfig.vm.provider "virtualbox" do |vb|
vb.customize ["modifyvm", :id, "--groups", "/Ansible-Lab"]
vb.name = "tnode3"
vb.cpus = 2
vb.memory = 1536 # 2048
vb.linked_clone = true
end
subconfig.vm.host_name = "tnode3"
subconfig.vm.network "private_network", ip: "10.10.1.13"
subconfig.vm.network "forwarded_port", guest: 22, host: 60003, auto_correct: true, id: "ssh"
subconfig.vm.synced_folder "./", "/vagrant", disabled: true
subconfig.vm.provision "shell", path: "init_cfg2.sh"
end
endinit_cfg.sh
#!/usr/bin/env bash
echo ">>>> Initial Config Start <<<<"
echo "[TASK 1] Setting Profile & Change Timezone"
echo 'alias vi=vim' >> /etc/profile
echo "sudo su -" >> /home/vagrant/.bashrc
ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
echo "[TASK 2] Disable AppArmor"
systemctl stop ufw && systemctl disable ufw >/dev/null 2>&1
systemctl stop apparmor && systemctl disable apparmor >/dev/null 2>&1
echo "[TASK 3] Install Packages"
apt update -qq >/dev/null 2>&1
apt-get install tree sshpass unzip -y -qq >/dev/null 2>&1
echo "[TASK 4] Config account & ssh config"
echo 'vagrant:qwe123' | chpasswd
echo 'root:qwe123' | chpasswd
sed -i "s/^#PasswordAuthentication yes/PasswordAuthentication yes/g" /etc/ssh/sshd_config
sed -i "s/^#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
systemctl restart ssh
echo "[TASK 5] Setting Local DNS Using Hosts file"
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/hosts
cat << EOF >> /etc/hosts
10.10.1.10 server
10.10.1.11 tnode1
10.10.1.12 tnode2
10.10.1.13 tnode3
EOF
echo ">>>> Initial Config End <<<<"
init_cfg2.sh
#!/usr/bin/env bash
echo ">>>> Initial Config Start <<<<"
echo "[TASK 1] Setting Profile & Change Timezone"
echo 'alias vi=vim' >> /etc/profile
echo "sudo su -" >> /home/vagrant/.bashrc
ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
echo "[TASK 2] Disable firewalld and selinux"
systemctl stop firewalld && systemctl disable firewalld >/dev/null 2>&1
setenforce 0
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
echo "[TASK 5] Install Packages"
dnf install -y yum sshpass jq git >/dev/null 2>&1
echo "[TASK 4] Config account & ssh config"
echo 'vagrant:qwe123' | chpasswd
echo 'root:qwe123' | chpasswd
sed -i "s/^#PasswordAuthentication yes/PasswordAuthentication yes/g" /etc/ssh/sshd_config
sed -i "s/^#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
systemctl restart sshd
echo "[TASK 5] Setting Local DNS Using Hosts file"
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/hosts
cat << EOF >> /etc/hosts
10.10.1.10 server
10.10.1.11 tnode1
10.10.1.12 tnode2
10.10.1.13 tnode3
EOF
echo ">>>> Initial Config End <<<<"
해당 내용을 본문에도 추가 해 둔다.
# 작업 기본 디렉터리 확인
whoami
pwd
# 파이썬 버전 확인
python3 --version
# 설치
apt install software-properties-common -y
add-apt-repository --yes --update ppa:ansible/ansible
apt install ansible -y
# 확인
ansible --version
ansible [core 2.19.5]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3/dist-packages/ansible
ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.12.3 (main, Aug 14 2025, 17:47:21) [GCC 13.3.0] (/usr/bin/python3)
jinja version = 3.1.2
pyyaml version = 6.0.1 (with libyaml v0.2.5)
cat /etc/ansible/ansible.cfg
ansible-config list
which ansible
# 작업 디렉터리 생성
mkdir my-ansible
cd my-ansibleansible 이 접근할 수 있게 끔 ssh 키를 생성한다.
# 모니터링
tree ~/.ssh
watch -d 'tree ~/.ssh'
# Create SSH Keypair
ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa공개키를 노드에 복사한다.
# 공개 키를 관리 노드에 복사
for i in {1..3}; do sshpass -p 'qwe123' ssh-copy-id -o StrictHostKeyChecking=no root@tnode$i; done
# 복사 확인
for i in {1..3}; do echo ">> tnode$i <<"; ssh tnode$i cat ~/.ssh/authorized_keys; echo; done
# ssh 접속 테스트
for i in {1..3}; do echo ">> tnode$i <<"; ssh tnode$i hostname; echo; done
# python 정보 확인
for i in {1..3}; do echo ">> tnode$i <<"; ssh tnode$i python3 -V; echo; done
앤서블 인벤토리의 경우, 다를 노드들의 목록을 정의하는 파일이다. 아래는 작성하는 예시이다.
master01 ansible_host=192.168.10.10 ip=192.168.10.10 ansible_user=rootansible_host: 실제 접속할 IP/도메인ip: 쿠버네티스 내부 통신 등에 사용되는 변수ansible_user: 접속 시 사용할 계정그룹별로 호스트를 묶을 수 있습니다. 그룹화를 하게 되면 그룹별로 시행 작업들을 관리할 수 있어 관리에 효율이 좋아 집니다.
[web]
web1.example.com
web2.example.com
[db]
db1.example.com위와 같이, 웹서버, DB 서버와 같이 Role 을 분리해서 실행 시킬 수 있습니다.
[webservers]
web1.example.com
web2.example.com
[db-servers]
db01.example.com
db02.example.com
[datacenter:children]
webservers
dbservers유사한 이름의 호스트가 많을 경우 대괄호를 사용하여 간소화할 수 있습니다. 그리고 알파벳, 그리고 IP 대역으로도 범위 문법을 통한 간소화가 가능합니다.
플레이북 작성을 학습 전에, ansible config 에 대해서 잠시 정리한다. ansible 명령어를 실행할 때 -i 옵션으로 인벤토리 파일을 명시해야 하는데, 설정에서 어떤 config 를 바라볼지 등을 설정할 수 있어, 설정을 먼저 해보고 진행하려고 한다.
설정파일은 여러군데 설정할 수 있고, 우선순위는 다음과 같다.
ANSIBLE_CONFIG (environment variable if set)ansible.cfg (in the current directory)~/.ansible.cfg (in the home directory)/etc/ansible/ansible.cfg실습에서는 2번으로 설정하고 진행할 예정이다.
[defaults]
inventory = ./inventory
remote_user = root
ask_pass = false
[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false설정 파일 섹션에 대한 설명이다. [defaults] 섹션 : 앤서블 작업을 위한 기본값 설정
| 매개 변수 | 설명 |
|---|---|
| inventory | 인벤토리 파일의 경로를 지정함. |
| remote_user | 앤서블이 관리 호스트에 연결할 때 사용하는 사용자 이름을 지정함. 이때, 사용자 이름을 지정하지 않으면 현재 사용자 이름으로 지정됨. |
| ask_pass | SSH 암호를 묻는 메시지 표시 여부를 지정함. SSH 공개 키 인증을 사용하는 경우 기본값은 false임. |
[privilege_escalation] 섹션 : 보안/감사로 인해 원격 호스트에 권한 없는 사용자 연결 후 관리 액세스 권한을 에스컬레이션하여 루트 사용자로 진행 할 때
| 매개 변수 | 설명 |
|---|---|
| become | 기본적으로 권한 에스컬레이션을 활성화할 때 사용하며, 연결 후 관리 호스트에서 자동으로 사용자를 전환할지 여부를 지정함.일반적으로 root로 전환되며, 플레이북에서도 지정할 수 있음. |
| become_method | 권한을 에스컬레이션하는 사용자 전환 방식을 의미함. 일반적으로 기본값은 sudo를 사용하며, su는 옵션으로 설정할 수 있음. |
| become_user | 관리 호스트에서 전환할 사용자를 지정함. 일반적으로 기본값은 root임. |
| become_ask_pass | become_method 매개 변수에 대한 암호를 묻는 메시지 표시 여부를 지정함. 기본값은 false임.권한을 에스컬레이션하기 위해 사용자가 암호를 입력해야 하는 경우, 구성 파일에 become_ask_pass = true 매개 변수를 설정하면 됨. |
실습용 config 파일 my-ansible/ansible.cfg
cat <<EOT > ansible.cfg
[defaults]
inventory = ./inventory
remote_user = root
ask_pass = false
[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false
EOTmy-ansible/first-playbook.yml
---
- hosts: all
tasks:
- name: Print message
debug:
msg: Hello CloudNet@ Ansible Studymy-ansible/first-playbook-with-error.yml
---
- hosts: all
tasks:
- name: Print message
debug:
msg: Hello CloudNet@ Ansible Studyansible 실행 시 ansible-playbook 명령어로 실행한다. 우리는 위에서 설정했기 때문에 인벤토리의 위치를 명시하지 않아도 된다.
ansible-playbook first-playbook.ymlAnsible은 변수를 사용해서, 작업 시 필요한 값을 저장할 수 있다. 변수의 종류에는 그룹변수, 호스트변수, 플레이변수, 추가 변수, 그리고 결과를 저장하는 작업 변수가 있다.
인벤토리에 정의된 호스트 그룹에 적용하는 변수
[all:vars] 섹션을 주목하자.
[web]
tnode1 ansible_python_interpreter=/usr/bin/python3
tnode2 ansible_python_interpreter=/usr/bin/python3
[db]
tnode3 ansible_python_interpreter=/usr/bin/python3
[all:children]
web
db
[all:vars]
user=ansible---
- hosts: all
tasks:
- name: Create User {{ user }}
ansible.builtin.user:
name: "{{ user }}"
state: present해당 변수를 적용하고 플레이북을 실행한다
# (터미널2) 모니터링
watch -d "ssh tnode1 tail -n 3 /etc/passwd"
# 실행
ansible-playbook create-user.yml
...
TASK [Create User ansible] *********
...
# 한번 더 실행 : 멱등성 확인
ansible-playbook create-user.ymluser 가 바인딩 된 것을 확인 할 수 있다.
호스트에서만 사용할 수 있는 변수
[db] 영역의 user=ansible1 을 확인한다.
[web]
tnode1 ansible_python_interpreter=/usr/bin/python3
tnode2 ansible_python_interpreter=/usr/bin/python3
[db]
tnode3 ansible_python_interpreter=/usr/bin/python3 user=ansible1
[all:children]
web
db
[all:vars]
user=ansible다음과 같은 플레이북을 만들고 실행한다.
---
- hosts: db
tasks:
- name: Create User {{ user }}
ansible.builtin.user:
name: "{{ user }}"
state: present# (터미널2) 모니터링
watch -d "ssh tnode3 tail -n 3 /etc/passwd"
# 실행
ansible-playbook create-user1.yml
...
TASK [Create User ansible1] *********
...
# 확인
for i in {1..3}; do echo ">> tnode$i <<"; ssh tnode$i tail -n 3 /etc/passwd; echo; donedb에 호스트 변수가 적용된 것을 볼 수 있으며, 그룹 변수보다 높은 우선순위를 가지는 것을 알 수 있다.
플레이북 내에서 사용되는 변수
vars: 영역에 변수가 정의되어 있다.
---
- hosts: all
vars:
user: ansible2
tasks:
- name: Create User {{ user }}
ansible.builtin.user:
name: "{{ user }}"
state: present플레이북 실행 시 파라미터로 넘겨주는 변수.
# (터미널2) 모니터링
watch -d "ssh tnode3 tail -n 3 /etc/passwd"
# 실행
ansible-playbook -e user=ansible4 create-user3.yml
...
TASK [Create User ansible4] *********
...
# 확인
for i in {1..3}; do echo ">> tnode$i <<"; ssh tnode$i tail -n 5 /etc/passwd; echo; done플레이북의 수행 결과를 가지고, 그 후속 작업에서 사용하기 위해 저장하는 변수
register 를 선언하면, 그 선언한 변수에 데이터가 들어가게 된다.
---
- hosts: db
tasks:
- name: Create User {{ user }}
ansible.builtin.user:
name: "{{ user }}"
state: present
register: result
- ansible.builtin.debug:
var: resultbulletin.debug 모듈로 변수를 출력 가능하다.
아래 출력 결과를 보면 알 수 있듯이, result 결과값을 볼 수 있다.
# (터미널2) 모니터링
watch -d "ssh tnode3 tail -n 3 /etc/passwd"
# 실행 : ok=3의 의미는?
ansible-playbook -e user=ansible5 create-user4.yml
PLAY [db] ***************************************************************************
TASK [Gathering Facts] **************************************************************
ok: [tnode3-ubuntu.local]
TASK [Create User ansible5] *********
changed: [tnode3-ubuntu.local]
TASK [ansible.builtin.debug] ********************************************************
ok: [tnode3-ubuntu.local] => {
"result": {
"changed": true,
"comment": "",
"create_home": true,
"failed": false,
"group": 1006,
"home": "/home/ansible5",
"name": "ansible5",
"shell": "/bin/sh",
"state": "present",
"system": false,
"uid": 1006
}
}
PLAY RECAP **************************************************************************
tnode3-ubuntu.local : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0변수 우선 순위 : 추가변수(실행 시 파라미터) > 플레이 변수 > 호스트 변수 > 그룹 변수
팩트는 호스트에서 수집한 내용이 들어 있다.
팩트 관련 공식 문서 를 참고하면 좋을 것 같다.
---
- hosts: db
tasks:
- name: Print all facts
ansible.builtin.debug:
var: ansible_facts
#
ansible-playbook facts.yml
...
TASK [Gathering Facts] ***********************************************************************************************************************************************************************
ok: [tnode3]
TASK [Print all facts] ***********************************************************************************************************************************************************************
ok: [tnode3] => {
...
"hostname": "tnode3",
...
"default_ipv4": {
"address": "10.10.1.13",
...
"os_family": "Debian", # 참고로 Ubuntu도 os_family는 Debian 으로 출력 - Link
...전체 결과
PLAY [db] **********************************************************************
TASK [Gathering Facts] *********************************************************
ok: [tnode3]
TASK [Print all facts] *********************************************************
ok: [tnode3] => {
"ansible_facts": {
"all_ipv4_addresses": [
"10.10.1.13",
"10.0.2.15"
],
"all_ipv6_addresses": [
"fe80::a00:27ff:fe97:afa0",
"fd17:625c:f037:2:a00:27ff:fe1f:ae8b",
"fe80::a00:27ff:fe1f:ae8b"
],
"ansible_local": {},
"apparmor": {
"status": "disabled"
},
"architecture": "aarch64",
"bios_date": "NA",
"bios_vendor": "NA",
"bios_version": "NA",
"board_asset_tag": "NA",
"board_name": "NA",
"board_serial": "NA",
"board_vendor": "NA",
"board_version": "NA",
"chassis_asset_tag": "NA",
"chassis_serial": "NA",
"chassis_vendor": "NA",
"chassis_version": "NA",
"cmdline": {
"BOOT_IMAGE": "(hd0,gpt3)/boot/vmlinuz-5.14.0-570.52.1.el9_6.aarch64",
"console": "ttyS0,115200n8",
"no_timer_check": true,
"ro": true,
"root": "UUID=858fc44c-7093-420e-8ecd-aad817736634"
},
"date_time": {
"date": "2026-01-10",
"day": "10",
"epoch": "1768032997",
"epoch_int": "1768032997",
"hour": "17",
"iso8601": "2026-01-10T08:16:37Z",
"iso8601_basic": "20260110T171637976625",
"iso8601_basic_short": "20260110T171637",
"iso8601_micro": "2026-01-10T08:16:37.976625Z",
"minute": "16",
"month": "01",
"second": "37",
"time": "17:16:37",
"tz": "KST",
"tz_dst": "KST",
"tz_offset": "+0900",
"weekday": "Saturday",
"weekday_number": "6",
"weeknumber": "01",
"year": "2026"
},
"default_ipv4": {
"address": "10.0.2.15",
"alias": "enp0s8",
"broadcast": "10.0.2.255",
"gateway": "10.0.2.2",
"interface": "enp0s8",
"macaddress": "08:00:27:1f:ae:8b",
"mtu": 1500,
"netmask": "255.255.255.0",
"network": "10.0.2.0",
"prefix": "24",
"type": "ether"
},
"default_ipv6": {
"address": "fd17:625c:f037:2:a00:27ff:fe1f:ae8b",
"gateway": "fe80::2",
"interface": "enp0s8",
"macaddress": "08:00:27:1f:ae:8b",
"mtu": 1500,
"prefix": "64",
"scope": "global",
"type": "ether"
},
"device_links": {
"ids": {},
"labels": {},
"masters": {},
"uuids": {
"sda1": [
"19AA-5BCD"
],
"sda2": [
"170ab8d6-f3c1-4df4-a6c8-f5ca12bf7724"
],
"sda3": [
"858fc44c-7093-420e-8ecd-aad817736634"
]
}
},
"devices": {
"sda": {
"holders": [],
"host": "SCSI storage controller: Red Hat, Inc. Virtio 1.0 SCSI (rev 01)",
"links": {
"ids": [],
"labels": [],
"masters": [],
"uuids": []
},
"model": "HARDDISK",
"partitions": {
"sda1": {
"holders": [],
"links": {
"ids": [],
"labels": [],
"masters": [],
"uuids": [
"19AA-5BCD"
]
},
"sectors": 1228800,
"sectorsize": 512,
"size": "600.00 MB",
"start": "2048",
"uuid": "19AA-5BCD"
},
"sda2": {
"holders": [],
"links": {
"ids": [],
"labels": [],
"masters": [],
"uuids": [
"170ab8d6-f3c1-4df4-a6c8-f5ca12bf7724"
]
},
"sectors": 7993344,
"sectorsize": 512,
"size": "3.81 GB",
"start": "1230848",
"uuid": "170ab8d6-f3c1-4df4-a6c8-f5ca12bf7724"
},
"sda3": {
"holders": [],
"links": {
"ids": [],
"labels": [],
"masters": [],
"uuids": [
"858fc44c-7093-420e-8ecd-aad817736634"
]
},
"sectors": 124991488,
"sectorsize": 512,
"size": "59.60 GB",
"start": "9224192",
"uuid": "858fc44c-7093-420e-8ecd-aad817736634"
}
},
"removable": "0",
"rotational": "1",
"sas_address": null,
"sas_device_handle": null,
"scheduler_mode": "none",
"sectors": 134217728,
"sectorsize": "512",
"size": "64.00 GB",
"support_discard": "0",
"vendor": "VBOX",
"virtual": 1
}
},
"distribution": "Rocky",
"distribution_file_parsed": true,
"distribution_file_path": "/etc/redhat-release",
"distribution_file_variety": "RedHat",
"distribution_major_version": "9",
"distribution_release": "Blue Onyx",
"distribution_version": "9.6",
"dns": {
"nameservers": [
"168.126.63.1",
"8.8.8.8"
]
},
"domain": "",
"effective_group_id": 0,
"effective_user_id": 0,
"enp0s8": {
"active": true,
"device": "enp0s8",
"features": {
"esp_hw_offload": "off [fixed]",
"esp_tx_csum_hw_offload": "off [fixed]",
"generic_receive_offload": "on",
"generic_segmentation_offload": "on",
"highdma": "off [fixed]",
"hsr_dup_offload": "off [fixed]",
"hsr_fwd_offload": "off [fixed]",
"hsr_tag_ins_offload": "off [fixed]",
"hsr_tag_rm_offload": "off [fixed]",
"hw_tc_offload": "off [fixed]",
"l2_fwd_offload": "off [fixed]",
"large_receive_offload": "off [fixed]",
"loopback": "off [fixed]",
"macsec_hw_offload": "off [fixed]",
"ntuple_filters": "off [fixed]",
"receive_hashing": "off [fixed]",
"rx_all": "off",
"rx_checksumming": "off",
"rx_fcs": "off",
"rx_gro_hw": "off [fixed]",
"rx_gro_list": "off",
"rx_udp_gro_forwarding": "off",
"rx_udp_tunnel_port_offload": "off [fixed]",
"rx_vlan_filter": "on [fixed]",
"rx_vlan_offload": "on",
"rx_vlan_stag_filter": "off [fixed]",
"rx_vlan_stag_hw_parse": "off [fixed]",
"scatter_gather": "on",
"tcp_segmentation_offload": "on",
"tls_hw_record": "off [fixed]",
"tls_hw_rx_offload": "off [fixed]",
"tls_hw_tx_offload": "off [fixed]",
"tx_checksum_fcoe_crc": "off [fixed]",
"tx_checksum_ip_generic": "on",
"tx_checksum_ipv4": "off [fixed]",
"tx_checksum_ipv6": "off [fixed]",
"tx_checksum_sctp": "off [fixed]",
"tx_checksumming": "on",
"tx_esp_segmentation": "off [fixed]",
"tx_fcoe_segmentation": "off [fixed]",
"tx_gre_csum_segmentation": "off [fixed]",
"tx_gre_segmentation": "off [fixed]",
"tx_gso_list": "off [fixed]",
"tx_gso_partial": "off [fixed]",
"tx_gso_robust": "off [fixed]",
"tx_ipxip4_segmentation": "off [fixed]",
"tx_ipxip6_segmentation": "off [fixed]",
"tx_nocache_copy": "off",
"tx_scatter_gather": "on",
"tx_scatter_gather_fraglist": "off [fixed]",
"tx_sctp_segmentation": "off [fixed]",
"tx_tcp6_segmentation": "off [fixed]",
"tx_tcp_ecn_segmentation": "off [fixed]",
"tx_tcp_mangleid_segmentation": "off",
"tx_tcp_segmentation": "on",
"tx_tunnel_remcsum_segmentation": "off [fixed]",
"tx_udp_segmentation": "off [fixed]",
"tx_udp_tnl_csum_segmentation": "off [fixed]",
"tx_udp_tnl_segmentation": "off [fixed]",
"tx_vlan_offload": "on [fixed]",
"tx_vlan_stag_hw_insert": "off [fixed]",
"vlan_challenged": "off [fixed]"
},
"hw_timestamp_filters": [],
"ipv4": {
"address": "10.0.2.15",
"broadcast": "10.0.2.255",
"netmask": "255.255.255.0",
"network": "10.0.2.0",
"prefix": "24"
},
"ipv6": [
{
"address": "fd17:625c:f037:2:a00:27ff:fe1f:ae8b",
"prefix": "64",
"scope": "global"
},
{
"address": "fe80::a00:27ff:fe1f:ae8b",
"prefix": "64",
"scope": "link"
}
],
"macaddress": "08:00:27:1f:ae:8b",
"module": "e1000",
"mtu": 1500,
"pciid": "0000:00:08.0",
"promisc": false,
"speed": 1000,
"timestamping": [],
"type": "ether"
},
"enp0s9": {
"active": true,
"device": "enp0s9",
"features": {
"esp_hw_offload": "off [fixed]",
"esp_tx_csum_hw_offload": "off [fixed]",
"generic_receive_offload": "on",
"generic_segmentation_offload": "on",
"highdma": "off [fixed]",
"hsr_dup_offload": "off [fixed]",
"hsr_fwd_offload": "off [fixed]",
"hsr_tag_ins_offload": "off [fixed]",
"hsr_tag_rm_offload": "off [fixed]",
"hw_tc_offload": "off [fixed]",
"l2_fwd_offload": "off [fixed]",
"large_receive_offload": "off [fixed]",
"loopback": "off [fixed]",
"macsec_hw_offload": "off [fixed]",
"ntuple_filters": "off [fixed]",
"receive_hashing": "off [fixed]",
"rx_all": "off",
"rx_checksumming": "off",
"rx_fcs": "off",
"rx_gro_hw": "off [fixed]",
"rx_gro_list": "off",
"rx_udp_gro_forwarding": "off",
"rx_udp_tunnel_port_offload": "off [fixed]",
"rx_vlan_filter": "on [fixed]",
"rx_vlan_offload": "on",
"rx_vlan_stag_filter": "off [fixed]",
"rx_vlan_stag_hw_parse": "off [fixed]",
"scatter_gather": "on",
"tcp_segmentation_offload": "on",
"tls_hw_record": "off [fixed]",
"tls_hw_rx_offload": "off [fixed]",
"tls_hw_tx_offload": "off [fixed]",
"tx_checksum_fcoe_crc": "off [fixed]",
"tx_checksum_ip_generic": "on",
"tx_checksum_ipv4": "off [fixed]",
"tx_checksum_ipv6": "off [fixed]",
"tx_checksum_sctp": "off [fixed]",
"tx_checksumming": "on",
"tx_esp_segmentation": "off [fixed]",
"tx_fcoe_segmentation": "off [fixed]",
"tx_gre_csum_segmentation": "off [fixed]",
"tx_gre_segmentation": "off [fixed]",
"tx_gso_list": "off [fixed]",
"tx_gso_partial": "off [fixed]",
"tx_gso_robust": "off [fixed]",
"tx_ipxip4_segmentation": "off [fixed]",
"tx_ipxip6_segmentation": "off [fixed]",
"tx_nocache_copy": "off",
"tx_scatter_gather": "on",
"tx_scatter_gather_fraglist": "off [fixed]",
"tx_sctp_segmentation": "off [fixed]",
"tx_tcp6_segmentation": "off [fixed]",
"tx_tcp_ecn_segmentation": "off [fixed]",
"tx_tcp_mangleid_segmentation": "off",
"tx_tcp_segmentation": "on",
"tx_tunnel_remcsum_segmentation": "off [fixed]",
"tx_udp_segmentation": "off [fixed]",
"tx_udp_tnl_csum_segmentation": "off [fixed]",
"tx_udp_tnl_segmentation": "off [fixed]",
"tx_vlan_offload": "on [fixed]",
"tx_vlan_stag_hw_insert": "off [fixed]",
"vlan_challenged": "off [fixed]"
},
"hw_timestamp_filters": [],
"ipv4": {
"address": "10.10.1.13",
"broadcast": "10.10.1.255",
"netmask": "255.255.255.0",
"network": "10.10.1.0",
"prefix": "24"
},
"ipv6": [
{
"address": "fe80::a00:27ff:fe97:afa0",
"prefix": "64",
"scope": "link"
}
],
"macaddress": "08:00:27:97:af:a0",
"module": "e1000",
"mtu": 1500,
"pciid": "0000:00:09.0",
"promisc": false,
"speed": 1000,
"timestamping": [],
"type": "ether"
},
"env": {
"BASH_FUNC_which%%": "() { ( alias;\n eval ${which_declare} ) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot $@\n}",
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/0/bus",
"DEBUGINFOD_IMA_CERT_PATH": "/etc/keys/ima:",
"DEBUGINFOD_URLS": "https://debuginfod.rockylinux.org/ ",
"HOME": "/root",
"LANG": "en_US.UTF-8",
"LESSOPEN": "||/usr/bin/lesspipe.sh %s",
"LOGNAME": "root",
"LS_COLORS": "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.m4a=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.oga=01;36:*.opus=01;36:*.spx=01;36:*.xspf=01;36:",
"MOTD_SHOWN": "pam",
"PATH": "/root/.local/bin:/root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin",
"PWD": "/root",
"SELINUX_LEVEL_REQUESTED": "",
"SELINUX_ROLE_REQUESTED": "",
"SELINUX_USE_CURRENT_RANGE": "",
"SHELL": "/bin/bash",
"SHLVL": "1",
"SSH_CLIENT": "10.10.1.10 49620 22",
"SSH_CONNECTION": "10.10.1.10 49620 10.10.1.13 22",
"SSH_TTY": "/dev/pts/0",
"TERM": "xterm-256color",
"USER": "root",
"XDG_RUNTIME_DIR": "/run/user/0",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_ID": "431",
"XDG_SESSION_TYPE": "tty",
"_": "/usr/bin/python3",
"which_declare": "declare -f"
},
"fibre_channel_wwn": [],
"fips": false,
"form_factor": "NA",
"fqdn": "tnode3",
"gather_subset": [
"all"
],
"hostname": "tnode3",
"hostnqn": "nqn.2014-08.org.nvmexpress:uuid:b4201204-1135-498b-8536-3a8fe83e7131",
"interfaces": [
"enp0s8",
"lo",
"enp0s9"
],
"is_chroot": false,
"iscsi_iqn": "",
"kernel": "5.14.0-570.52.1.el9_6.aarch64",
"kernel_version": "#1 SMP PREEMPT_DYNAMIC Wed Oct 15 14:48:33 UTC 2025",
"lo": {
"active": true,
"device": "lo",
"features": {
"esp_hw_offload": "off [fixed]",
"esp_tx_csum_hw_offload": "off [fixed]",
"generic_receive_offload": "on",
"generic_segmentation_offload": "on",
"highdma": "on [fixed]",
"hsr_dup_offload": "off [fixed]",
"hsr_fwd_offload": "off [fixed]",
"hsr_tag_ins_offload": "off [fixed]",
"hsr_tag_rm_offload": "off [fixed]",
"hw_tc_offload": "off [fixed]",
"l2_fwd_offload": "off [fixed]",
"large_receive_offload": "off [fixed]",
"loopback": "on [fixed]",
"macsec_hw_offload": "off [fixed]",
"ntuple_filters": "off [fixed]",
"receive_hashing": "off [fixed]",
"rx_all": "off [fixed]",
"rx_checksumming": "on [fixed]",
"rx_fcs": "off [fixed]",
"rx_gro_hw": "off [fixed]",
"rx_gro_list": "off",
"rx_udp_gro_forwarding": "off",
"rx_udp_tunnel_port_offload": "off [fixed]",
"rx_vlan_filter": "off [fixed]",
"rx_vlan_offload": "off [fixed]",
"rx_vlan_stag_filter": "off [fixed]",
"rx_vlan_stag_hw_parse": "off [fixed]",
"scatter_gather": "on",
"tcp_segmentation_offload": "on",
"tls_hw_record": "off [fixed]",
"tls_hw_rx_offload": "off [fixed]",
"tls_hw_tx_offload": "off [fixed]",
"tx_checksum_fcoe_crc": "off [fixed]",
"tx_checksum_ip_generic": "on [fixed]",
"tx_checksum_ipv4": "off [fixed]",
"tx_checksum_ipv6": "off [fixed]",
"tx_checksum_sctp": "on [fixed]",
"tx_checksumming": "on",
"tx_esp_segmentation": "off [fixed]",
"tx_fcoe_segmentation": "off [fixed]",
"tx_gre_csum_segmentation": "off [fixed]",
"tx_gre_segmentation": "off [fixed]",
"tx_gso_list": "on",
"tx_gso_partial": "off [fixed]",
"tx_gso_robust": "off [fixed]",
"tx_ipxip4_segmentation": "off [fixed]",
"tx_ipxip6_segmentation": "off [fixed]",
"tx_nocache_copy": "off [fixed]",
"tx_scatter_gather": "on [fixed]",
"tx_scatter_gather_fraglist": "on [fixed]",
"tx_sctp_segmentation": "on",
"tx_tcp6_segmentation": "on",
"tx_tcp_ecn_segmentation": "on",
"tx_tcp_mangleid_segmentation": "on",
"tx_tcp_segmentation": "on",
"tx_tunnel_remcsum_segmentation": "off [fixed]",
"tx_udp_segmentation": "on",
"tx_udp_tnl_csum_segmentation": "off [fixed]",
"tx_udp_tnl_segmentation": "off [fixed]",
"tx_vlan_offload": "off [fixed]",
"tx_vlan_stag_hw_insert": "off [fixed]",
"vlan_challenged": "on [fixed]"
},
"hw_timestamp_filters": [],
"ipv4": {
"address": "127.0.0.1",
"broadcast": "",
"netmask": "255.0.0.0",
"network": "127.0.0.0",
"prefix": "8"
},
"ipv6": [
{
"address": "::1",
"prefix": "128",
"scope": "host"
}
],
"mtu": 65536,
"promisc": false,
"timestamping": [],
"type": "loopback"
},
"loadavg": {
"15m": 0.08,
"1m": 0.13,
"5m": 0.12
},
"locally_reachable_ips": {
"ipv4": [
"10.0.2.15",
"10.10.1.13",
"127.0.0.0/8",
"127.0.0.1"
],
"ipv6": [
"::1",
"fd17:625c:f037:2:a00:27ff:fe1f:ae8b",
"fe80::a00:27ff:fe1f:ae8b",
"fe80::a00:27ff:fe97:afa0"
]
},
"lsb": {},
"lvm": {
"lvs": {},
"pvs": {},
"vgs": {}
},
"machine": "aarch64",
"machine_id": "8230d6facca94508ad85549845ad2b3c",
"memfree_mb": 776,
"memory_mb": {
"nocache": {
"free": 1100,
"used": 224
},
"real": {
"free": 776,
"total": 1324,
"used": 548
},
"swap": {
"cached": 0,
"free": 3902,
"total": 3902,
"used": 0
}
},
"memtotal_mb": 1324,
"module_setup": true,
"mounts": [
{
"block_available": 15091154,
"block_size": 4096,
"block_total": 15607552,
"block_used": 516398,
"device": "/dev/sda3",
"dump": 0,
"fstype": "xfs",
"inode_available": 31209705,
"inode_total": 31247872,
"inode_used": 38167,
"mount": "/",
"options": "rw,seclabel,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota",
"passno": 0,
"size_available": 61813366784,
"size_total": 63928532992,
"uuid": "858fc44c-7093-420e-8ecd-aad817736634"
},
{
"block_available": 151423,
"block_size": 4096,
"block_total": 153290,
"block_used": 1867,
"device": "/dev/sda1",
"dump": 0,
"fstype": "vfat",
"inode_available": 0,
"inode_total": 0,
"inode_used": 0,
"mount": "/boot/efi",
"options": "rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortname=winnt,errors=remount-ro",
"passno": 0,
"size_available": 620228608,
"size_total": 627875840,
"uuid": "19AA-5BCD"
}
],
"nodename": "tnode3",
"os_family": "RedHat",
"pkg_mgr": "dnf",
"proc_cmdline": {
"BOOT_IMAGE": "(hd0,gpt3)/boot/vmlinuz-5.14.0-570.52.1.el9_6.aarch64",
"console": [
"tty0",
"ttyS0,115200n8"
],
"no_timer_check": true,
"ro": true,
"root": "UUID=858fc44c-7093-420e-8ecd-aad817736634"
},
"processor": [
"0",
"1"
],
"processor_cores": 1,
"processor_count": 2,
"processor_nproc": 2,
"processor_threads_per_core": 1,
"processor_vcpus": 2,
"product_name": "NA",
"product_serial": "NA",
"product_uuid": "NA",
"product_version": "NA",
"python": {
"executable": "/usr/bin/python3",
"has_sslcontext": true,
"type": "cpython",
"version": {
"major": 3,
"micro": 21,
"minor": 9,
"releaselevel": "final",
"serial": 0
},
"version_info": [
3,
9,
21,
"final",
0
]
},
"python_version": "3.9.21",
"real_group_id": 0,
"real_user_id": 0,
"selinux": {
"config_mode": "permissive",
"mode": "permissive",
"policyvers": 33,
"status": "enabled",
"type": "targeted"
},
"selinux_python_present": true,
"service_mgr": "systemd",
"ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA3NDADxITYCQ+Yzl24Rv5+HTMqHFo1A1i6bGwRXfIvZ5ZPFQYMrR2CAi0vU69QVLwdWCJoLoQP8He+NBfrlrV8=",
"ssh_host_key_ecdsa_public_keytype": "ecdsa-sha2-nistp256",
"ssh_host_key_ed25519_public": "AAAAC3NzaC1lZDI1NTE5AAAAIKcCJWNOyGZ6SW350FH+dihyL+qJBcvRb7NIvkyes3Lt",
"ssh_host_key_ed25519_public_keytype": "ssh-ed25519",
"ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABgQCQOhfEeFMicbWK5uXUAbgkFqTQRQGptB4FLBmiMIrpVVC0J9PFfBTJL2UIvDg1TCdwQ+DkJhtjffLDjWM/OU75gsYs9Ih0aGgE1zHf+93Wt0tM5+I8z0fTact/+4GaBAuSO4o7rjlebOC5XpgGT7aglCbuUn7UIefvf4m1OIdrWL8szWb6jZLGNH6AOn7itpri2cXWp9pnffr8FHYWIsKyHJWnRGtPXSOlU7/2wc7j0b8M1+H2FpBS8d4Y9+0Jdf0T9wq3tK7UkcW5hOGv09X1h42xMLzaCqivdvP2dxMz5xcvUWrC6g2wQgzg7IkA+hVszF6Nfazu2GYrNOTNyy3Pf+Krb8SthldVg4/Skw7sMkmGQ6TGCO5yTY0ul13sPiEx4SHBbtebJXk075W2+dB1yx6/6lxziUkUeW9jBJf2IVUpSMTf6PGLOefce1pLu/bSdD0pxR6CpukDfUhHaNGJCZpaiOEbk5FK2f9B+9TJ0y+w6ZaXzcfNmPj2hzzhN+8=",
"ssh_host_key_rsa_public_keytype": "ssh-rsa",
"swapfree_mb": 3902,
"swaptotal_mb": 3902,
"system": "Linux",
"system_capabilities": [],
"system_capabilities_enforced": "False",
"system_vendor": "NA",
"systemd": {
"features": "+PAM +AUDIT +SELINUX -APPARMOR +IMA +SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN -IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT -QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK +XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified",
"version": 252
},
"uptime_seconds": 12173,
"user_dir": "/root",
"user_gecos": "root",
"user_gid": 0,
"user_id": "root",
"user_shell": "/bin/bash",
"user_uid": 0,
"userspace_bits": "64",
"virtualization_role": "NA",
"virtualization_tech_guest": [],
"virtualization_tech_host": [],
"virtualization_type": "NA"
}
}
PLAY RECAP *********************************************************************
tnode3 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 팩트로 수집된 값을 사용하는 예제
---
- hosts: db
tasks:
- name: Print all facts
ansible.builtin.debug:
msg: >
The default IPv4 address of {{ ansible_facts.hostname }}
is {{ ansible_facts.default_ipv4.address }}실행
#
ansible-playbook facts1.yml
...
TASK [Gathering Facts] **************************************************************************
ok: [tnode3]
TASK [Print all facts] **************************************************************************
ok: [tnode3] => {
"msg": "The default IPv4 address of tnode3 is 10.10.1.13"
}반복문을 사용할 수 있다. loop 항목의 부분이 실행되는 것을 알 수 있다.
---
- hosts: all
tasks:
- name: Check sshd and rsyslog state
ansible.builtin.service:
name: "{{ item }}"
state: started
loop:
- vboxadd-service # ssh
- rsyslog실행 스크린샷을 첨부한다.

---
- 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'
적용이 되었는지 확인해 본다.

---
- hosts: localhost
tasks:
- name: Loop echo test
ansible.builtin.shell: "echo 'I can speak {{ item }}'"
loop:
- Korean
- English
register: result
- name: Show result
ansible.builtin.debug:
var: result실행해 본다
#
ansible-playbook loop_register.yml
...
TASK [Loop echo test] *************************************************************************************************************************************************************************
changed: [localhost] => (item=Korean)
changed: [localhost] => (item=English)
TASK [Show result] ****************************************************************************************************************************************************************************
ok: [localhost] => {
"result": {
"changed": true,
"msg": "All items completed",
"results": [
{
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo 'I can speak Korean'",
"delta": "0:00:00.002555",
"end": "2023-12-29 05:21:52.898098",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo 'I can speak Korean'",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true
}
},
"item": "Korean",
"msg": "",
"rc": 0,
"start": "2023-12-29 05:21:52.895543",
"stderr": "",
"stderr_lines": [],
"stdout": "I can speak Korean",
"stdout_lines": [
"I can speak Korean"
]
},
{
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo 'I can speak English'",
"delta": "0:00:00.003048",
"end": "2023-12-29 05:21:53.076240",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo 'I can speak English'",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true
}
},
"item": "English",
"msg": "",
"rc": 0,
"start": "2023-12-29 05:21:53.073192",
"stderr": "",
"stderr_lines": [],
"stdout": "I can speak English",
"stdout_lines": [
"I can speak English"
]
}
],
"skipped": false
}
}너무 데이터가 많아, 실제로 접근 가능한 지 테스트 해 본다.
---
- hosts: localhost
tasks:
- name: Loop echo test
ansible.builtin.shell: "echo 'I can speak {{ item }}'"
loop:
- Korean
- English
register: result
- name: Show result
ansible.builtin.debug:
msg: "Stdout: {{ item.stdout }}"
loop: "{{ result.results }}"다음과 같이 변수를 정확히 사용할 수 있는 것을 알 수 있다.

when 문은 조건부로 작업을 실행할 때 테스트할 조건을 값으로 사용합니다.
---
- 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: resultrun_my_task 가 true 로 되어 있다.
#
ansible-playbook when_task.yml
...
TASK [echo message] ***************************************************************************************************************************************************************************
changed: [localhost]
...run_my_task 를 false 로 변경 후 실행한다.
#
ansible-playbook when_task.yml
...
TASK [echo message] ***************************************************************************************************************************************************************************
skipping: [localhost]
...skipping 이 되는 것을 볼 수 있다.
| 연산 예시 | 설명 |
|---|---|
ansible_facts['machine'] == "x86_64" | ansible_facts['machine'] 값이 x86_64와 같으면 true |
max_memory == 512 | max_memory 값이 512와 같다면 true |
min_memory < 128 | min_memory 값이 128보다 작으면 true |
min_memory > 256 | min_memory 값이 256보다 크면 true |
min_memory <= 256 | min_memory 값이 256보다 작거나 같으면 true |
min_memory >= 512 | min_memory 값이 512보다 크거나 같으면 true |
min_memory != 512 | min_memory 값이 512와 같지 않으면 true |
min_memory is defined | min_memory 라는 변수가 있으면 true |
min_memory is not defined | min_memory 라는 변수가 없으면 true |
memory_available | memory 값이 true이며 true, 이때 해당 값이 1이거나 True 또는 yes면 true |
not memory_available | memory 값이 false이며 true, 이때 해당 값이 0이거나 False 또는 no면 true |
ansible_facts['distribution'] in supported_distros | ansible_facts['distribution']의 값이 supported_distros 라는 변수에 있으면 true |
아래와 같이 조건
os 로 분기를 타는 예시이다.
---
- 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아래 결과를 보면, tnode3 의 경우엔 rocky 계열이라 무시가 된 것을 확인할 수 있다.

단일 조건문이 아닌 여러 조건을 사용할 수 있다. and, or 등으로 조건을 걸어 실행할 수 있다.
---
- 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"다음과 같이, OS Type 이 우분투인 태스크가 나오게 된다.

---
- 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'] == "24.04"다음과 같이 두 조건을 만족해야 보이게 된다.

and 연산자의 경우 when 절 밑에, - 로 구분해서 올리게 될 경우 모두 and 로 동작한다. 예시)
---
- 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'] == "24.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'] == "Rocky" and
ansible_facts['distribution_version'] == "9.6" )
or
( ansible_facts['distribution'] == "Ubuntu" and
ansible_facts['distribution_version'] == "24.04" )
특정 작업을 끝내고, 후속 작업을 진행하기 위한 기능이다.
rsyslog 를 재시작하고, 메시지를 출력하는 예시이다.
---
- 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"재시작 이후에, 메시지가 나오는 것을 볼 수 있다.

태스크가 실패할 시 무시하는데, 옵션을 ignore_errors 옵션을 주면 계속 실행할 수 있다.
태스크 실패 시 종료 예제
---
- hosts : tnode1
tasks:
- name: Install apache3
ansible.builtin.apt:
name: apache3
state: latest
- name: Print msg
ansible.builtin.debug:
msg: "Before task is ignored"****실패 시 failed 로 뒤에 핸들러가 실행되지 않는다.

태스크 실패시에도 무시하고 실행 예제
---
- 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"작업이 실패해도 핸들러가 실행이 된다.

위에서 보았듯이 실패하면 핸들러가 실행 되지 않았으나, 핸들러만 실행되게 실행하는 옵션이 있다.
플레이북에 force_handlers: yes 키워드를 설정하면 된다
쉘스크립트의 경우 ansible 에서 성공으로 처리하기 때문에, 멱등성을 위해 모듈을 쓰는 것을 추천한다.
추가로 failed_when 키워드로, shell script 실행 자체가 아닌 결과 리턴코드나 리턴 메세지를 기반으로 작업 실패로 인식하게 끔 처리가 가능하다.
앤서블에서 블록으로 오류를 제어할 수 있다.
개발에서 try catch finally 와 같은 문법이라고 생각하면 된다.
---
- 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'결과
#
ansible-playbook block-example.yml
...
TASK [Find Directory] ***********************************************************************************************************************************************************************
[WARNING]: Skipped '/var/log/daily_log' path due to this access issue: '/var/log/daily_log' is not a directory
fatal: [tnode2-ubuntu.local]: FAILED! => {"changed": false, "examined": 0, "failed_when_result": true, "files": [], "matched": 0, "msg": "Not all paths examined, check warnings for details", "skipped_paths": {"/var/log/daily_log": "'/var/log/daily_log' is not a directory"}}
TASK [Make Directory when Not found Directory] **********************************************************************************************************************************************
changed: [tnode2-ubuntu.local]
TASK [Create File] **************************************************************************************************************************************************************************
changed: [tnode2-ubuntu.local]
PLAY RECAP **********************************************************************************************************************************************************************************
tnode2-ubuntu.local : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
# tnode2에서 확인 : 디렉터리와 로그 파일 생성 확인
ansible -m shell -a "ls -l /var/log/daily_log/" tnode2
-rw-r--r-- 1 root root 0 Jan 3 15:00 todays.log한번 더 실행한다
#
ansible-playbook block-example.yml
...
TASK [Find Directory] ***********************************************************************************************************************************************************************
ok: [tnode2-ubuntu.local]
TASK [Create File] **************************************************************************************************************************************************************************
changed: [tnode2-ubuntu.local]
PLAY RECAP **********************************************************************************************************************************************************************************
tnode2-ubuntu.local : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
# tnode2에서 확인 : 디렉터리와 로그 파일 생성 확인
ansible -m shell -a "ls -l /var/log/daily_log/" tnode2
-rw-r--r-- 1 root root 0 Jan 3 15:05 todays.log플레이북 기능 단위로 나누어서 재사용 하기 위한 구조를 만드는 것 구조화된 role 로 재사용성을 높일 수 있다.
간단한 롤 플레이북 개발 실습을 진행한다 아래는 프로세스의 간단한 개요이다.
아래와 같이 ansible-galaxy 명령어를 이용해 role 을 생성한다.
# 롤 생성
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
my-role/tasks/main.yml 파일을 수정한다.
---
# tasks file for my-role
- name: install service {{ service_title }}
ansible.builtin.apt:
name: "{{ item }}"
state: latest
loop: "{{ httpd_packages }}"
when: ansible_facts.distribution in supported_distros
- name: copy conf file
ansible.builtin.copy:
src: "{{ src_file_path }}"
dest: "{{ dest_file_path }}"
notify:
- restart servicemy-role/files/index.html 에 index.html 파일을 생성한다.
echo "Hello! Ansible" > files/index.html핸들러를 작성한다 my-role/handlers/main.yml 파일을 수정한다.
---
# handlers file for my-role
- name: restart service
ansible.builtin.service:
name: "{{ service_name }}"
state: restarted가변 변수를 수정한다. my-role/defaults/main.yml
echo 'service_title: "Apache Web Server"' >> defaults/main.yml불변 변수를 작성한다. 불변 변수는 수정이 불가능하다. my-role/vars/main.yml
---
# 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해당 내용으로 role 생성을 완료 했으면, role 을 실행한다 role 폴더 상위로 나와서 role-example 파일을 만들어 실행한다.
---
- hosts: tnode1
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-roleansible-playbook role-example.yml 로 실행해 본다.


아까 만든 html 파일이 정상 serving 이 되는 것을 확인해 볼 수 있다.
사람들이 자신이 만든 롤들을 공유하는 사이트 https://docs.ansible.com/ansible/latest/cli/ansible-galaxy.html 해당 사이트를 확인해 보면 Role 을 찾아 볼 수 있다.
앤서블에 대해서 간단하게 알게 되었고, 회사에서 한두 개 서버만 관리하다 보니 처음 들어보는 툴들이 많다. 실습을 하면서 빠르게 배운 느낌이다. 좋은 실습을 할 수 있게 정보를 제공해주신 가시다님에게 감사한다는 말씀을 바친다.