카테고리 없음

k8s Deploy Study 2주차 Ansible 기초

juyeon22 2026. 1. 17. 14:33

k8s Deploy Study 2주차 Ansible 기초

가시다님이 운영하시는 k8s Deploy 2주차 내용을 정리한 게시글 입니다.

Ansible 기초에 대한 설명은 다른 글에도 많은 것 같아, 이번 게시물에서는 스터디에서 다루는 추가과제 내용을 위주로 구성하려고합니다.

1. 실습 환경

저는 로컬 서버에 Proxmox를 이용하여 실습환경을 구축해서 가시다님이 설정한 환경 설정과 다르게 설정을 진행 할 예정입니다.

proxmox를 이용한 Terraform 코드를 이용하여 서버를 구성 했습니다.

이번 실습에서는 기존에 제가 Home-lab k8s cluster 구성 시 사용했던 코드를 사용해서 구성하려고합니다.

실습 환경 구성

가상환경은 다음과 같이 구성됩니다.

Node OS Kernel vCPU Memory Disk NIC2 IP 관리자 계정  
k8s-master Ubuntu 24.04 6.8.0 4 8GB 80GB 192.168.0.31 key인증  
k8s-worker-1 Ubuntu 24.04 상동 8 18 GB 260GB 192.168.0.32 key인증  
k8s-worker-2 Ubuntu 24.04 상동 8 18 GB 260GB 192.168.0.33 key인증  
k8s-worker-3 Ubuntu 24.04 6.8.0 8 18 GB 260GB 192.168.0.34 key인증  
git clone https://github.com/yeonjukim164/proxmox.git
tofu init
tofu plan
tofu apply

vagrant init script ansible 적용

가시다님이 작성한 코드는 Vagrant에 적합한 코드라 저는 Terraform으로 배포하고 ansible로 자동으로 설정해보려고 합니다.

# host.ini 파일
[k8s_cluster]
k8s-master   ansible_host=192.168.0.31
k8s-worker-1 ansible_host=192.168.0.32
k8s-worker-2 ansible_host=192.168.0.33
k8s-worker-3 ansible_host=192.168.0.34

[k8s_cluster:vars]
ansible_user=kubeadm
ansible_password=kubeadm
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
# playbook.yaml
- name: Initial Configuration for K8s Cluster
  hosts: k8s_cluster
  become: yes
  vars:
    target_password: "qwe123"  # 보안 취약: 실제 운영 환경에서는 절대 평문 사용 금지

  tasks:
    # [TASK 1] Setting Profile & Change Timezone
    - name: Set alias for vim
      lineinfile:
        path: /etc/profile
        line: 'alias vi=vim'
        state: present

    - name: Force sudo su - in .bashrc (Not Recommended)
      lineinfile:
        path: /home/kubeadm/.bashrc
        line: 'sudo su -'
        state: present
      # 주의: 이 설정은 kubeadm 사용자의 일반 쉘 접근을 차단할 수 있음

    - name: Set Timezone to Asia/Seoul
      timezone:
        name: Asia/Seoul

    # [TASK 2] Disable AppArmor & UFW
    - name: Stop and Disable UFW
      service:
        name: ufw
        state: stopped
        enabled: no

    - name: Stop and Disable AppArmor
      service:
        name: apparmor
        state: stopped
        enabled: no
      ignore_errors: yes  # 일부 환경에서 AppArmor가 없을 경우 무시

    # [TASK 3] Install Packages
    - name: Update apt cache and install packages
      apt:
        update_cache: yes
        name:
          - tree
          - sshpass
          - unzip
        state: present

    # [TASK 4] Config account & ssh config
    - name: Set password for kubeadm user
      user:
        name: kubeadm
        password: "{{ target_password | password_hash('sha512') }}"
        state: present
        shell: /bin/bash
        create_home: yes

    - name: Set password for root user
      user:
        name: root
        password: "{{ target_password | password_hash('sha512') }}"

    - name: Enable PasswordAuthentication in SSH
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PasswordAuthentication'
        line: 'PasswordAuthentication yes'
      notify: Restart SSH

    - name: Enable PermitRootLogin in SSH
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PermitRootLogin'
        line: 'PermitRootLogin yes'
      notify: Restart SSH

    # [TASK 5] Setting Local DNS Using Hosts file
    - name: Remove 127.0.x.1 entries from /etc/hosts
      lineinfile:
        path: /etc/hosts
        regexp: '^127\.0\.(1|2)\.1'
        state: absent

    - name: Add K8s nodes to /etc/hosts
      blockinfile:
        path: /etc/hosts
        block: |
          192.168.0.31 k8s-master
          192.168.0.32 k8s-worker-1
          192.168.0.33 k8s-worker-2
          192.168.0.34 k8s-worker-3
        marker: "# {mark} ANSIBLE MANAGED BLOCK K8S NODES"

  handlers:
    - name: Restart SSH
      service:
        name: sshd
        state: restarted

2. 도전과제 실습

User 생성 후 삭제 실습

Ansible을 이용해서 user을 생성 후 삭제하는 실습을 진행해보겠습니다.

# task1.yaml
- name: Manage System Users
  hosts: all
  become: yes  # root 권한 필요
  vars:
    # 생성할 유저 목록
    users_to_create:
      - username: "devops_engineer"
        password: "changeme123!"
        groups: "sudo"
        shell: "/bin/bash"
      - username: "junior_dev"
        password: "changeme123!"
        groups: ""
        shell: "/bin/bash"

    # 삭제할 유저 목록
    users_to_delete:
      - username: "junior_dev"

  tasks:
    - name: Create user accounts
      ansible.builtin.user:
        name: "{{ item.username }}"
        # 비밀번호를 평문으로 넣으면 안됨 -> sha512로 해싱 처리
        password: "{{ item.password | password_hash('sha512') }}"
        groups: "{{ item.groups | default(omit) }}"
        shell: "{{ item.shell }}"
        append: yes
        create_home: yes
        state: present
      loop: "{{ users_to_create }}"
      tags: create

    - name: Delete user accounts
      ansible.builtin.user:
        name: "{{ item.username }}"
        state: absent
        remove: yes
        force: yes
      loop: "{{ users_to_delete }}"
      tags: delete

devops_engineer라는 계정을 생성 후 삭제하는 ansible-playbook yaml파일의 내용입니다.

ansible_user.yaml파일로 해당 파일을 저장하고 ansible을 실행하면 다음과 같이 결과가 출력됩니다.


ansible-playbook -i inventory.ini ansible_user.yaml

# PLAY [Manage System Users] **************************************************************************************************************************************************************************************************************************

# TASK [Gathering Facts] ******************************************************************************************************************************************************************************************************************************
# ok: [k8s-worker-1]
# ok: [k8s-worker-3]
# ok: [k8s-master]
# ok: [k8s-worker-2]

# TASK [Create user accounts] *************************************************************************************************************************************************************************************************************************
# changed: [k8s-worker-3] => (item={'username': 'devops_engineer', 'password': 'changeme123!', 'groups': 'sudo', 'shell': '/bin/bash'})
# changed: [k8s-master] => (item={'username': 'devops_engineer', 'password': 'changeme123!', 'groups': 'sudo', 'shell': '/bin/bash'})
# changed: [k8s-worker-1] => (item={'username': 'devops_engineer', 'password': 'changeme123!', 'groups': 'sudo', 'shell': '/bin/bash'})
# changed: [k8s-worker-2] => (item={'username': 'devops_engineer', 'password': 'changeme123!', 'groups': 'sudo', 'shell': '/bin/bash'})
# changed: [k8s-worker-3] => (item={'username': 'junior_dev', 'password': 'changeme123!', 'groups': '', 'shell': '/bin/bash'})
# changed: [k8s-worker-1] => (item={'username': 'junior_dev', 'password': 'changeme123!', 'groups': '', 'shell': '/bin/bash'})
# changed: [k8s-master] => (item={'username': 'junior_dev', 'password': 'changeme123!', 'groups': '', 'shell': '/bin/bash'})
# changed: [k8s-worker-2] => (item={'username': 'junior_dev', 'password': 'changeme123!', 'groups': '', 'shell': '/bin/bash'})

# TASK [Delete user accounts] *************************************************************************************************************************************************************************************************************************
# changed: [k8s-master] => (item={'username': 'junior_dev'})
# changed: [k8s-worker-1] => (item={'username': 'junior_dev'})
# changed: [k8s-worker-2] => (item={'username': 'junior_dev'})
# changed: [k8s-worker-3] => (item={'username': 'junior_dev'})

# PLAY RECAP ******************************************************************************************************************************************************************************************************************************************
# k8s-master                 : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-1               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-2               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-3               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

실제로 Master 서버에서 유저 리스트를 출력해보면 junior_dev계정은 없는 것을 확인할 수 있습니다.

grep /bin/bash /etc/passwd | cut -f1 -d:
# root
# kubeadm
# devops_engineer

관리 대상에 uptime을 ansible.builtin.debug 모듈을 통해서 확인

명령어 수행 결과를 uptime_result에 담하 debug 모듈을 통해 확인하는 playbook 입니다.

# task2.yaml
- name: Check System Uptime
  hosts: all
  tasks:
    - name: Execute uptime command
      ansible.builtin.command: uptime
      register: uptime_result

    - name: Print uptime info
      ansible.builtin.debug:
        msg: "System Uptime: {{ uptime_result.stdout }}"

ansibe 수행 결과는 다음과 같습니다.

ansible-playbook -i inventory.ini task2.yaml

# PLAY [Check System Uptime] **************************************************************************************************************************************************************************************************************************

# TASK [Gathering Facts] ******************************************************************************************************************************************************************************************************************************
# ok: [k8s-worker-3]
# ok: [k8s-worker-1]
# ok: [k8s-master]
# ok: [k8s-worker-2]

# TASK [Execute uptime command] ***********************************************************************************************************************************************************************************************************************
# changed: [k8s-worker-1]
# changed: [k8s-worker-2]
# changed: [k8s-master]
# changed: [k8s-worker-3]

# TASK [Print uptime info] ****************************************************************************************************************************************************************************************************************************
# ok: [k8s-master] => {
#     "msg": "System Uptime:  13:02:34 up  1:52,  1 user,  load average: 0.00, 0.00, 0.00"
# }
# ok: [k8s-worker-1] => {
#     "msg": "System Uptime:  13:02:34 up  1:52,  1 user,  load average: 0.00, 0.00, 0.00"
# }
# ok: [k8s-worker-2] => {
#     "msg": "System Uptime:  13:02:34 up  1:52,  1 user,  load average: 0.00, 0.00, 0.00"
# }
# ok: [k8s-worker-3] => {
#     "msg": "System Uptime:  13:02:34 up  1:52,  1 user,  load average: 0.00, 0.00, 0.00"
# }

# PLAY RECAP ******************************************************************************************************************************************************************************************************************************************
# k8s-master                 : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-1               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-2               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-3               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

커널 버전 및 OS 정보 추출

ansible 사용 시 Facts를 이용하여 Target Server들의 Metadata(커널버전, OS 정보.. )를 추출할 수 있는데 이때 사용하는 playbook은 다음과 같습니다.

# task3.yaml
- name: Check OS and Kernel Version
  hosts: all
  gather_facts: yes  # Facts 수집

  tasks:
    - name: Print OS and Kernel Info
      ansible.builtin.debug:
        msg:
          - "Operating System: {{ ansible_distribution }} {{ ansible_distribution_version }}"
          - "Kernel Version: {{ ansible_kernel }}"

저는 proxmox에서 Image(Ubuntu24.04)로 생성 했어서 다음과 같이 결과가 출력이 됩니다.

ansible-playbook -i inventory.ini task3.yaml

# PLAY [Check OS and Kernel Version] ******************************************************************************************************************************************************************************************************************

# TASK [Gathering Facts] ******************************************************************************************************************************************************************************************************************************
# ok: [k8s-master]
# ok: [k8s-worker-2]
# ok: [k8s-worker-1]
# ok: [k8s-worker-3]

# TASK [Print OS and Kernel Info] *********************************************************************************************************************************************************************************************************************
# ok: [k8s-master] => {
#     "msg": [
#         "Operating System: Ubuntu 24.04",
#         "Kernel Version: 6.8.0-60-generic"
#     ]
# }
# ok: [k8s-worker-1] => {
#     "msg": [
#         "Operating System: Ubuntu 24.04",
#         "Kernel Version: 6.8.0-60-generic"
#     ]
# }
# ok: [k8s-worker-2] => {
#     "msg": [
#         "Operating System: Ubuntu 24.04",
#         "Kernel Version: 6.8.0-60-generic"
#     ]
# }
# ok: [k8s-worker-3] => {
#     "msg": [
#         "Operating System: Ubuntu 24.04",
#         "Kernel Version: 6.8.0-60-generic"
#     ]
# }

# PLAY RECAP ******************************************************************************************************************************************************************************************************************************************
# k8s-master                 : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-1               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-2               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-3               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

리눅스 유저 10명을 반복문을 통해 생성 확인 삭제

Ansible을 이용하여 Linux 유저 10명을 반복문을 통해 생성 후 playbook에서 확인 후 삭제하는 스크립트를 작성 해보겠습니다.

# task4.yaml
- name: User Lifecycle Test (Create -> Verify -> Delete)
  hosts: all
  become: yes
  vars:
    user_prefix: "user"
    user_count: 10

  tasks:
    # 사용자 10명 생성
    - name: Create 10 users ({{ user_prefix }}1 ~ {{ user_prefix }}10)
      ansible.builtin.user:
        name: "{{ user_prefix }}{{ item }}"
        state: present
        shell: /bin/bash
        create_home: yes
      loop: "{{ range(1, user_count + 1) | list }}"

    # 생성 확인
    - name: Verify users exist (Check /etc/passwd)
      ansible.builtin.command: "id {{ user_prefix }}{{ item }}"
      register: user_check_result
      changed_when: false
      ignore_errors: yes
      loop: "{{ range(1, user_count + 1) | list }}"

    - name: Print verification result
      ansible.builtin.debug:
        msg: "User '{{ item.item }}' Status: {{ 'EXISTS (uid=' + item.stdout.split()[0].split('=')[1] + ')' if item.rc == 0 else 'MISSING' }}"
      loop: "{{ user_check_result.results }}"
      loop_control:
        label: "{{ item.item }}"

    # 사용자 10명 삭제
    - name: Delete the users (remove home directory)
      ansible.builtin.user:
        name: "{{ user_prefix }}{{ item }}"
        state: absent
        remove: yes  # 홈 디렉터리까지 삭제
      loop: "{{ range(1, user_count + 1) | list }}"
ansible-playbook -i inventory.ini task4.yaml
# PLAY [User Lifecycle Test (Create -> Verify -> Delete)] *********************************************************************************************************************************************************************************************

# TASK [Gathering Facts] ******************************************************************************************************************************************************************************************************************************
# ok: [k8s-master]
# ok: [k8s-worker-3]
# ok: [k8s-worker-1]
# ok: [k8s-worker-2]

# TASK [Create 10 users (user1 ~ user10)] *************************************************************************************************************************************************************************************************************
# changed: [k8s-worker-1] => (item=1)
# changed: [k8s-worker-3] => (item=1)
# changed: [k8s-master] => (item=1)
# changed: [k8s-worker-2] => (item=1)
# ....
# changed: [k8s-master] => (item=10)
# changed: [k8s-worker-2] => (item=10)

# TASK [Verify users exist (Check /etc/passwd)] *******************************************************************************************************************************************************************************************************
# ok: [k8s-worker-2] => (item=1)
# ...
# ok: [k8s-worker-3] => (item=9)
# ok: [k8s-master] => (item=9)
# ok: [k8s-worker-1] => (item=7)
# ok: [k8s-worker-2] => (item=10)
# ok: [k8s-worker-3] => (item=10)
# ok: [k8s-master] => (item=10)
# ok: [k8s-worker-1] => (item=8)
# ok: [k8s-worker-1] => (item=9)
# ok: [k8s-worker-1] => (item=10)

# TASK [Print verification result] ********************************************************************************************************************************************************************************************************************
# ok: [k8s-master] => (item=1) => {
#     "msg": "User '1' Status: EXISTS (uid=1002(user1))"
# }
# ok: [k8s-master] => (item=2) => {
#     "msg": "User '2' Status: EXISTS (uid=1003(user2))"
# }
# ok: [k8s-worker-1] => (item=1) => {
#     "msg": "User '1' Status: EXISTS (uid=1002(user1))"
# }
# ok: [k8s-master] => (item=3) => {
#     "msg": "User '3' Status: EXISTS (uid=1004(user3))"
# }
# ...
# }
# ok: [k8s-worker-1] => (item=10) => {
#     "msg": "User '10' Status: EXISTS (uid=1011(user10))"
# }
# ok: [k8s-worker-2] => (item=10) => {
#     "msg": "User '10' Status: EXISTS (uid=1011(user10))"
# }
# ok: [k8s-worker-3] => (item=8) => {
#     "msg": "User '8' Status: EXISTS (uid=1009(user8))"
# }
# ok: [k8s-worker-3] => (item=9) => {
#     "msg": "User '9' Status: EXISTS (uid=1010(user9))"
# }
# ok: [k8s-worker-3] => (item=10) => {
#     "msg": "User '10' Status: EXISTS (uid=1011(user10))"
# }

# TASK [Delete the users (remove home directory)] *****************************************************************************************************************************************************************************************************
# changed: [k8s-master] => (item=1)
# changed: [k8s-worker-2] => (item=1)
# changed: [k8s-worker-1] => (item=1)
# changed: [k8s-worker-3] => (item=1)
# ...
# changed: [k8s-worker-3] => (item=9)
# changed: [k8s-worker-1] => (item=9)
# changed: [k8s-master] => (item=10)
# changed: [k8s-worker-2] => (item=10)
# changed: [k8s-worker-3] => (item=10)
# changed: [k8s-worker-1] => (item=10)

# PLAY RECAP ******************************************************************************************************************************************************************************************************************************************
# k8s-master                 : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-1               : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-2               : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-3               : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

실제로 Master 서버에 접속해서 유저를 확인해보겠습니다.

grep /bin/bash /etc/passwd | cut -f1 -d:
# root
# kubeadm
# devops_engineer

loop 반복문 중 sequence 를 이용하여 /var/log/test1 ~ /var/log/test100 100개 파일(file 모듈)을 생성 확인 후 삭제

반복문과 state를 이용하여 파일 생성 -> 확인 -> 삭제 작업을 진행해보겠습니다.

---
# task5.yaml
- name: Create 100 files
  hosts: all
  become: yes  # /var/log 권한 문제 해결을 위해 필수
  vars:
    file_path_prefix: "/var/log/test"
    start_num: 1
    end_num: 100

  tasks:
    # 파일 100개 생성 (state: touch)
    - name: Create 100 files ({{ file_path_prefix }}1 ~ {{ end_num }})
      ansible.builtin.file:
        path: "{{ file_path_prefix }}{{ item }}"
        state: touch
        mode: '0644'
      # Python의 range는 끝 번호를 포함하지 않으므로 +1을 해야 100까지 돕니다.
      loop: "{{ range(start_num, end_num + 1) | list }}"

    # 파일 생성 확인
    - name: Check if files exist
      ansible.builtin.stat:
        path: "{{ file_path_prefix }}{{ item }}"
      register: file_check_result
      loop: "{{ range(start_num, end_num + 1) | list }}"

    # 확인 결과 요약 출력 (존재하는 파일 개수만 계산)
    - name: Report verification result
      ansible.builtin.debug:
        msg: "Verified: {{ file_check_result.results | selectattr('stat.exists', 'equalto', true) | list | length }} / {{ end_num }} files exist."

    #  파일 100개 삭제
    - name: Delete the files
      ansible.builtin.file:
        path: "{{ file_path_prefix }}{{ item }}"
        state: absent
      loop: "{{ range(start_num, end_num + 1) | list }}"

수행 결과는 다음과 같이 출력됩니다.

ansible-playbook -i inventory.ini task5.yaml
# PLAY [Create 100 files] *****************************************************************************************************************************************************************************************************************************

# TASK [Gathering Facts] ******************************************************************************************************************************************************************************************************************************
# ok: [k8s-master]
# ok: [k8s-worker-1]
# ok: [k8s-worker-2]
# ok: [k8s-worker-3]

# TASK [Create 100 files (/var/log/test1 ~ 100)] ******************************************************************************************************************************************************************************************************
# changed: [k8s-worker-1] => (item=1)
# changed: [k8s-master] => (item=1)
# changed: [k8s-worker-3] => (item=1)
# changed: [k8s-worker-2] => (item=1)
# changed: [k8s-worker-1] => (item=2)
# changed: [k8s-master] => (item=2)
# changed: [k8s-worker-3] => (item=2)
# changed: [k8s-worker-2] => (item=2)
# changed: [k8s-worker-1] => (item=3)
# changed: [k8s-master] => (item=3)
# changed: [k8s-worker-3] => (item=3)
# changed: [k8s-worker-2] => (item=3)
# changed: [k8s-worker-1] => (item=4)
# changed: [k8s-master] => (item=4)
# ...
# changed: [k8s-master] => (item=98)
# changed: [k8s-worker-1] => (item=99)
# changed: [k8s-worker-3] => (item=99)
# changed: [k8s-worker-2] => (item=99)
# changed: [k8s-master] => (item=99)
# changed: [k8s-worker-1] => (item=100)
# changed: [k8s-worker-3] => (item=100)
# changed: [k8s-worker-2] => (item=100)
# changed: [k8s-master] => (item=100)

# TASK [Report verification result] *******************************************************************************************************************************************************************************************************************
# ok: [k8s-worker-1] => {
#     "msg": "Verified: 100 / 100 files exist."
# }
# ok: [k8s-master] => {
#     "msg": "Verified: 100 / 100 files exist."
# }
# ok: [k8s-worker-2] => {
#     "msg": "Verified: 100 / 100 files exist."
# }
# ok: [k8s-worker-3] => {
#     "msg": "Verified: 100 / 100 files exist."
# }


# TASK [Delete the files] *****************************************************************************************************************************************************************************************************************************
# changed: [k8s-master] => (item=1)
# changed: [k8s-worker-1] => (item=1)
# changed: [k8s-worker-2] => (item=1)
# changed: [k8s-worker-3] => (item=1)
# changed: [k8s-master] => (item=2)
# changed: [k8s-worker-1] => (item=2)
# changed: [k8s-worker-2] => (item=2)
# changed: [k8s-worker-3] => (item=2)
# changed: [k8s-master] => (item=3)
# changed: [k8s-worker-1] => (item=3)
# ...
# changed: [k8s-master] => (item=99)
# changed: [k8s-worker-1] => (item=99)
# changed: [k8s-worker-3] => (item=99)
# changed: [k8s-worker-2] => (item=99)
# changed: [k8s-master] => (item=100)
# changed: [k8s-worker-1] => (item=100)
# changed: [k8s-worker-3] => (item=100)
# changed: [k8s-worker-2] => (item=100)

# PLAY RECAP ******************************************************************************************************************************************************************************************************************************************
# k8s-master                 : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-1               : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-2               : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-3               : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Ubuntu OS이면서 fqdn으로 k8s-worker-1 인 경우, debug 모듈을 사용하여 OS 정보와 fqdn 정보를 출력

Ansible에서 지원하는 조건문(when)을 사용해서 실습을 진행해보겠습니다.

---
# task6.yaml
- name: Debug OS and FQDN for Specific Worker
  hosts: all
  gather_facts: yes  # 필수: OS 및 FQDN 정보를 수집합니다.

  tasks:
    - name: Print Info only if Ubuntu AND fqdn matches 'k8s-worker-1'
      ansible.builtin.debug:
        msg:
          - "System Matched!"
          - "Operating System: {{ ansible_distribution }} {{ ansible_distribution_version }}"
          - "FQDN: {{ ansible_fqdn }}"
      when:
        - ansible_distribution == "Ubuntu"
        - ansible_fqdn == "k8s-worker-1"

조건에 맞지 않는 경우 skipping으로 지나가고, 조건이 맞는 경우 로직을 수행하는 것을 확인 할 수 있습니다.

ansible-playbook -i inventory.ini task6.yaml

# PLAY [Debug OS and FQDN for Specific Worker] ********************************************************************************************************************************************************************************************************

# TASK [Gathering Facts] ******************************************************************************************************************************************************************************************************************************
# ok: [k8s-master]
# ok: [k8s-worker-2]
# ok: [k8s-worker-3]
# ok: [k8s-worker-1]

# TASK [Print Info only if Ubuntu AND fqdn matches 'k8s-worker-1'] ************************************************************************************************************************************************************************************
# skipping: [k8s-master]
# ok: [k8s-worker-1] => {
#     "msg": [
#         "System Matched!",
#         "Operating System: Ubuntu 24.04",
#         "FQDN: k8s-worker-1"
#     ]
# }
# skipping: [k8s-worker-2]
# skipping: [k8s-worker-3]

# PLAY RECAP ******************************************************************************************************************************************************************************************************************************************
# k8s-master                 : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
# k8s-worker-1               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-2               : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
# k8s-worker-3               : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

반복문+조건문을 함께 사용

반복문과 조건문을 함께 사용하는 스크립트를 패키지 설치 유무의 조건을 사용해서 수행해보겠습니다

---
# task7.yaml

- name: Conditional Loop Example (Install Only Missing Packages)
  hosts: all
  become: yes
  vars:
    required_packages:
      - name: "cmatrix" # Matrix screen saver
        version: "present"
      - name: "figlet" # ASCII Logo Maker
        version: "present"

  tasks:
    # 패키지 설치 여부 사전 확인
    # ignore_errors: yes를 통해 설치 안 된 패키지에서 에러가 나도 멈추지 않게 함
    - name: Check if packages are installed
      ansible.builtin.command: "dpkg-query -W {{ item.name }}"
      loop: "{{ required_packages }}"
      register: package_check_results
      ignore_errors: yes
      changed_when: false  # 단순 확인이므로 변경 상태로 표시하지 않음

    # 설치되지 않은 패키지만 골라서 설치
    - name: Install missing packages
      ansible.builtin.apt:
        name: "{{ item.item.name }}"
        state: present
      # loop_control을 사용하여 이전 Task의 결과(package_check_results)를 순회
      loop: "{{ package_check_results.results }}"
      # 이전 명령어가 실패(failed)했다면, 즉 패키지가 없다는 뜻으로 설치 진행
      when: item.failed == true
      loop_control:
        label: "{{ item.item.name }}"

패키지가 설치가 안되어있는 경우 다음과 같이 에러가 출력되고 설치가 됩니다

ansible-playbook -i inventory.ini task7.yaml
# TASK [Check if packages are installed] **************************************************************************************************************************************************************************************************************
# failed: [k8s-worker-3] (item={'name': 'cmatrix', 'version': 'present'}) => {"ansible_loop_var": "item", "changed": false, "cmd": ["dpkg-query", "-W", "cmatrix"], "delta": "0:00:00.010607", "end": "2026-01-17 13:58:26.683417", "item": {"name": "cmatrix", "version": "present"}, "msg": "non-zero return code", "rc": 1, "start": "2026-01-17 13:58:26.672810", "stderr": "dpkg-query: no packages found matching cmatrix", "stderr_lines": ["dpkg-query: no packages found matching cmatrix"], "stdout": "", "stdout_lines": []}
# failed: [k8s-worker-1] (item={'name': 'cmatrix', 'version': 'present'}) => {"ansible_loop_var": "item", "changed": false, "cmd": ["dpkg-query", "-W", "cmatrix"], "delta": "0:00:00.011463", "end": "2026-01-17 13:58:26.697810", "item": {"name": "cmatrix", "version": "present"}, "msg": "non-zero return code", "rc": 1, "start": "2026-01-17 13:58:26.686347", "stderr": "dpkg-query: no packages found matching cmatrix", "stderr_lines": ["dpkg-query: no packages found matching cmatrix"], "stdout": "", "stdout_lines": []}
# failed: [k8s-master] (item={'name': 'cmatrix', 'version': 'present'}) => {"ansible_loop_var": "item", "changed": false, "cmd": ["dpkg-query", "-W", "cmatrix"], "delta": "0:00:00.012263", "end": "2026-01-17 13:58:26.709341", "item": {"name": "cmatrix", "version": "present"}, "msg": "non-zero return code", "rc": 1, "start": "2026-01-17 13:58:26.697078", "stderr": "dpkg-query: no packages found matching cmatrix", "stderr_lines": ["dpkg-query: no packages found matching cmatrix"], "stdout": "", "stdout_lines": []}
# failed: [k8s-worker-2] (item={'name': 'cmatrix', 'version': 'present'}) => {"ansible_loop_var": "item", "changed": false, "cmd": ["dpkg-query", "-W", "cmatrix"], "delta": "0:00:00.010972", "end": "2026-01-17 13:58:26.699857", "item": {"name": "cmatrix", "version": "present"}, "msg": "non-zero return code", "rc": 1, "start": "2026-01-17 13:58:26.688885", "stderr": "dpkg-query: no packages found matching cmatrix", "stderr_lines": ["dpkg-query: no packages found matching cmatrix"], "stdout": "", "stdout_lines": []}
# failed: [k8s-worker-3] (item={'name': 'figlet', 'version': 'present'}) => {"ansible_loop_var": "item", "changed": false, "cmd": ["dpkg-query", "-W", "figlet"], "delta": "0:00:00.012058", "end": "2026-01-17 13:58:27.047979", "item": {"name": "figlet", "version": "present"}, "msg": "non-zero return code", "rc": 1, "start": "2026-01-17 13:58:27.035921", "stderr": "dpkg-query: no packages found matching figlet", "stderr_lines": ["dpkg-query: no packages found matching figlet"], "stdout": "", "stdout_lines": []}
# failed: [k8s-worker-1] (item={'name': 'figlet', 'version': 'present'}) => {"ansible_loop_var": "item", "changed": false, "cmd": ["dpkg-query", "-W", "figlet"], "delta": "0:00:00.011165", "end": "2026-01-17 13:58:27.062956", "item": {"name": "figlet", "version": "present"}, "msg": "non-zero return code", "rc": 1, "start": "2026-01-17 13:58:27.051791", "stderr": "dpkg-query: no packages found matching figlet", "stderr_lines": ["dpkg-query: no packages found matching figlet"], "stdout": "", "stdout_lines": []}
# ...ignoring
# TASK [Install missing packages] *********************************************************************************************************************************************************************************************************************
# changed: [k8s-master] => (item=cmatrix)
# changed: [k8s-worker-3] => (item=cmatrix)
# changed: [k8s-worker-2] => (item=cmatrix)
# changed: [k8s-worker-1] => (item=cmatrix)
# changed: [k8s-worker-1] => (item=figlet)
# changed: [k8s-worker-2] => (item=figlet)
# changed: [k8s-master] => (item=figlet)
# changed: [k8s-worker-3] => (item=figlet)

# PLAY RECAP ******************************************************************************************************************************************************************************************************************************************
# k8s-master                 : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1
# k8s-worker-1               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1
# k8s-worker-2               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1
# k8s-worker-3               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1

Master 서버에서 직접 설치된 패키지를 사용해보면 다음과 같습니다.

cmatrix

Figure 2.1 cmatrix 사용 예시

apache2 패키지를 apt 모듈을 통해서 설치 시, 핸들러를 호출하여 service 모듈로 apache2를 재시작

# task8.yaml
---
- name: Install Apache and Restart on Change
  hosts: all
  become: yes

  tasks:
    - name: Install Apache2 package
      ansible.builtin.apt:
        name: apache2
        state: present
        update_cache: yes
      notify: Restart Apache  # 핸들러 호출

    - name: Ensure Apache is enabled and running
      ansible.builtin.service:
        name: apache2
        state: started
        enabled: yes

  handlers:
    - name: Restart Apache
      ansible.builtin.service:
        name: apache2
        state: restarted
ansible-playbook -i inventory.ini task8.yaml
# ....
# TASK [Install Apache2 package] **********************************************************************************************************************************************************************************************************************
# changed: [k8s-worker-3]
# changed: [k8s-master]
# changed: [k8s-worker-2]
# changed: [k8s-worker-1]

# TASK [Ensure Apache is enabled and running] *********************************************************************************************************************************************************************************************************
# ok: [k8s-worker-3]
# ok: [k8s-master]
# ok: [k8s-worker-1]
# ok: [k8s-worker-2]

# RUNNING HANDLER [Restart Apache] ********************************************************************************************************************************************************************************************************************
# changed: [k8s-master]
# changed: [k8s-worker-1]
# changed: [k8s-worker-2]
# changed: [k8s-worker-3]

# PLAY RECAP ******************************************************************************************************************************************************************************************************************************************
# k8s-master                 : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-1               : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-2               : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
# k8s-worker-3               : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Master 서버에서 확인해보겠습니다.

dpkg-query -W  apache2
# apache2 2.4.58-1ubuntu8.8

block rescure always 키워드를 사용한 플레이북을 작성하여 테스트

Ansible에서 제공하는 block, rescure, always는 프로그래밍에서 Try, Catch finally와 같이 에러 발생에 대해서 조치하는 키워드입니다.

실습시 hosts를 로컬로 지정하여 로컬에서만 수행되도록 설정했으며, robots 파일 예제를 통해 실습을 해보겠습니다.

# task9.yaml
- name: Robot File Download Test (Block/Rescue/Always)
  hosts: localhost
  connection: local
  gather_facts: no
  vars:
    # 테스트 1: 정상 URL (https://www.google.com/robots.txt)

    # 테스트 2: 없는 URL (에러 유발용)
    target_url: "https://www.google.com/aaaaaaaa.txt"

    dest_file: "/tmp/downloaded_file.txt"
    log_file: "/tmp/download_activity.log"

  tasks:
    - name: Download Process Wrapper
      block:
        # 메인 작업 (파일 다운로드 시도)
        - name: Attempt to download file from URL
          ansible.builtin.get_url:
            url: "{{ target_url }}"
            dest: "{{ dest_file }}"
            timeout: 5
          register: download_result

        - name: Success Message (Only runs if download succeeds)
          ansible.builtin.debug:
            msg: "Download successful! File saved to {{ dest_file }}"

      rescue:
        #  실패 시 실행 (대체 파일 생성)
        - name: Rescue - Download failed! Creating a dummy file instead
          ansible.builtin.copy:
            content: "This is a fallback file because the download failed.\n"
            dest: "{{ dest_file }}"

        - name: Warn the user
          ansible.builtin.debug:
            msg: "WARNING: Could not download from {{ target_url }}. Created fallback file."

      always:
        # 무조건 실행 (로그 기록)
        - name: Always - Record activity timestamp
          ansible.builtin.lineinfile:
            path: "{{ log_file }}"
            create: yes
            line: "Activity recorded at {{ lookup('pipe', 'date') }} | Target: {{ target_url }}"
            state: present

        - name: Final Cleanup Check
          ansible.builtin.debug:
            msg: "Process finished. Check {{ dest_file }} and {{ log_file }}."

cat 명령어를 수행해서 로그파일이 저장된 경로를 확인해보면 ansible을 통해 수행된 기록이 적혀져있습니다.

ansible-playbook -i inventory.ini task9.yaml

# PLAY [Robot File Download Test (Block/Rescue/Always)] ***********************************************************************************************************************************************************************************************

# TASK [Attempt to download file from URL] ************************************************************************************************************************************************************************************************************
# fatal: [localhost]: FAILED! => {"changed": false, "dest": "/tmp/downloaded_file.txt", "elapsed": 0, "msg": "Request failed", "response": "HTTP Error 404: Not Found", "status_code": 404, "url": "https://www.google.com/aaaaaaaa.txt"}

# TASK [Rescue - Download failed! Creating a dummy file instead] **************************************************************************************************************************************************************************************
# changed: [localhost]

# TASK [Warn the user] ********************************************************************************************************************************************************************************************************************************
# ok: [localhost] => {
#     "msg": "WARNING: Could not download from https://www.google.com/aaaaaaaa.txt. Created fallback file."
# }

# TASK [Always - Record activity timestamp] ***********************************************************************************************************************************************************************************************************
# changed: [localhost]

# TASK [Final Cleanup Check] **************************************************************************************************************************************************************************************************************************
# ok: [localhost] => {
#     "msg": "Process finished. Check /tmp/downloaded_file.txt and /tmp/download_activity.log."
# }

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

cat /tmp/download_activity.log
# Activity recorded at Sat Jan 17 14:21:46 KST 2026 | Target: https://www.google.com/aaaaaaaa.txt

앤서블 갤럭시에서 관심 있는 롤을 검색하여 해당 롤을 사용하는 플레이북을 만들어서 롤을 통한 애플리케이션을 배포

가시다님 실습에서는 PostgreSQL을 설명을 해주셨지만 저는 Redis 설치를 통해 playbook을 만들어 배포를 하려고합니다.

geerlingguy.redis라는 role이 redis 설치시 가장 많이 사용하는 role인것으로 알고 있어 해당 role로 작업을 진행해보겠습니다.

ansible-galaxy install geerlingguy.redis
# role.yaml
- name: Deploy Redis Server using Galaxy Role
  hosts: all
  become: yes

  vars:
    # geerlingguy.redis 롤의 기본 변수 오버라이딩
    redis_port: 6379
    redis_bind_interface: "0.0.0.0"  # 외부 접속 허용

    redis_requirepass: "ChangeMe123!"

    # 보안 설정: 위험한 명령어 비활성화
    redis_disabled_commands:
      - FLUSHDB
      - FLUSHALL
      - KEYS
      - SHUTDOWN
  roles:
    - role: geerlingguy.redis
ansible-playbook -i inventory.ini role.yaml
# TASK [geerlingguy.redis : Include OS-specific variables.] *******************************************************************************************************************************************************************************************
# ok: [k8s-master]
# ok: [k8s-worker-1]
# ok: [k8s-worker-2]
# ok: [k8s-worker-3]

# TASK [geerlingguy.redis : Define redis_package.] ****************************************************************************************************************************************************************************************************
# ok: [k8s-master]
# ok: [k8s-worker-1]
# ok: [k8s-worker-2]
# ok: [k8s-worker-3]

# TASK [geerlingguy.redis : Ensure Redis configuration dir exists.] ***********************************************************************************************************************************************************************************
# changed: [k8s-master]
# changed: [k8s-worker-3]
# changed: [k8s-worker-2]
# changed: [k8s-worker-1]
# ...
# TASK [geerlingguy.redis : Ensure Redis is running.] *************************************************************************************************************************************************************************************************
# ok: [k8s-master]
# ok: [k8s-worker-3]
# ok: [k8s-worker-2]
# ok: [k8s-worker-1]

# RUNNING HANDLER [geerlingguy.redis : restart redis] *************************************************************************************************************************************************************************************************
# changed: [k8s-master]
# changed: [k8s-worker-1]
# changed: [k8s-worker-2]
# changed: [k8s-worker-3]

# PLAY RECAP ******************************************************************************************************************************************************************************************************************************************
# k8s-master                 : ok=9    changed=4    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
# k8s-worker-1               : ok=9    changed=4    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
# k8s-worker-2               : ok=9    changed=4    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
# k8s-worker-3               : ok=9    changed=4    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0

설치가 되었는지 Master 서버에서 확인해보면 다음과 같습니다.

root@k8s-master:~# systemctl status redis
# ● redis-server.service - Advanced key-value store
#      Loaded: loaded (/usr/lib/systemd/system/redis-server.service; enabled; preset: enabled)
#      Active: active (running) since Sat 2026-01-17 14:30:23 KST; 1min 5s ago
#        Docs: http://redis.io/documentation,
#              man:redis-server(1)
#    Main PID: 32223 (redis-server)
#      Status: "Ready to accept connections"
#       Tasks: 5 (limit: 9486)
#      Memory: 3.2M (peak: 4.1M)
#         CPU: 138ms
#      CGroup: /system.slice/redis-server.service
#              └─32223 "/usr/bin/redis-server 0.0.0.0:6379"

# Jan 17 14:30:23 k8s-master systemd[1]: Starting redis-server.service - Advanced key-value store...
# Jan 17 14:30:23 k8s-master systemd[1]: Started redis-server.service - Advanced key-value store.

3. 실습환경 제거

tofu destroy