Profile picture

[Ansible] 시스템 구축 자동화하기

JaehyoJJAng2023년 04월 04일

개요

서비스를 구축하고 사용하기 위해 가장 먼저 안정적인 시스템을 구축한다.

그리고 사용자 계정을 만들어 해당 사용자 계정으로 접속할 수 있도록 SSH 키를 생성한다.

이때 여러 시스템의 시간대를 설정하고 동기화하는 작업도 진행한다.

이런 상황에서 효율적으로 구축할 수 있는 앤서블 활용법을 이번 실습을 통해 진행해보도록 하겠다.


구성도

image


1. 사용자 계정 생성하기

시스템 구축시 가장 먼저 하는 일은 사용자 계정을 만드는 것이다.

사용자 계정은 목적에 따라 시스템에 접근하기 위해 생성할 수도 있고, 서비스를 설치하거나 구축하기 위해 생성할 수도 있다.

사용자 계정 생성 업무만 할 경우에는 굳이 앤서블을 사용하지 않고 셸 스크립트만으로도 충분히 가능하다.

그러나 앤서블 플레이북을 통해 사용자 계정을 생성할 줄 알면 시스템이나 서비스를 구축할 때 해당 플레이북의 태스크를 재활용할 수 있다.


How ?

모든 개발에는 분석과 설계 단계가 필요하다.

앤서블 역시 플레이북을 개발하는 것이므로 사전 분석과 플레이북 설계가 이루어져야 한다.

  • 사용자 계정과 패스워드는 Vault를 이용해 암호화 처리
  • 사용자 계정 생성은 ansible.builtin.user 모듈을 사용

플레이북 개발

실습 진행을 위해 01_사용자계정생성 이라는 디렉터리를 하나 생성해주자.

mkdir -p /ansible/01_사용자계정생성

그리고 inventory 파일을 생성하자.

호스트 그룹을 따로 나누지 않고 다음과 같이 호스트명으로만 구성된 inventory를 작성하도록 한다.

ubuntu1
ubuntu2

ansible.cfg 파일은 아래와 같이 작성한다.

[defaults]
inventory = ./inventory
remote_user = root
ask_pass = false
roles_path = ./roles
collection_paths = ./collection
interpreter_python=auto

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false%

이번에는 ansible-vault로 사용자 계정 정보가 정의된 변수 파일을 생성하자.

이때 Vault Password를 입력하라는 프롬프트가 나오면 사용하고자 하는 패스워드를 입력한다.

$ ansible-vault create vars/secret.yaml

패스워드 입력 시 에디터 창으로 전환된다.

여기에 user_info 변수에 useriduserpw가 같이 있는 사전형 변수를 정의하도록 하자.

---
user_info:
  - userid: "ansible_auto"
    userpw: "qwer123"
  - userid: "ansible_auto2"
    userpw: "qwer123"

Vault로 사용자 계정을 정의했다면, 이번에는 사용자 계정을 생성하는 플레이북을 작성해보자.

사용자 계정은 모든 호스트에 동일하게 생성하며 vault로 작성된 변수 파일을 읽어 사용한다.

사용자 계정은 ansible.builtin.user 모듈과 loop 문을 사용하여 생성한다.

---

- hosts: all
  vars_files:
    - vars/secret.yaml
  
  tasks:
    - name: "Create {{ item }}"
      ansible.builtin.user:
        name: "{{ item.userid }}"
        password: "{{ item.userpw }}"
        state: present
      loop: "{{ user_info }}"

플레이북 작성이 끝나면 플레이북 문법을 체크하고 실행하여 인벤토리에 등록된 전 노드에 secret.yaml 파일에 등록한 사용자 계정이 생성되는지 테스트해보자.


먼저 문법을 체크하자.

$ ansible-playbook --syntax-check --ask-vault-pass create-user.yaml

Vault password: 
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

playbook: create-user.yaml

문법 체크가 완료되었다면 create-user.yaml 플레이북을 실행해보자.

$ ansible-playbook -i ./inventory --ask-vault-pass create-user.yaml

ok: [ubuntu1]

TASK [Create {{ item }}] ***************************************************************************************************************************************************************************************************************************************************
ok: [ubuntu1] => (item={'userid': 'ansible_auto', 'userpw': 'qwer123'})
ok: [ubuntu1] => (item={'userid': 'ansible_auto2', 'userpw': 'qwer123'})
[WARNING]: The input password appears not to have been hashed. The 'password' argument must be encrypted for this module to work properly.

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

정상적으로 플레이북이 실행되었다.


이후 관리 노드(ubuntu1)에 들어가 사용자 계정을 조회해보면 해당 계정이 생성된 것을 볼 수 있다.

# ubuntu1
$ tail -n 2 /etc/passwd

ansible_auto:x:1004:1004::/home/ansible_auto:/bin/sh
ansible_auto2:x:1005:1005::/home/ansible_auto2:/bin/sh

2. SSH 키 생성 및 복사하기

시스템을 구축하거나 애플리케이션을 설치하는 경우,

해당 애플리케이션을 사용하는 서버들간에 SSH 접속을 할 때는 패스워드 대신 SSH 키를 주로 사용한다.

앤서블 역시 대상 노드에 접속하여 모듈을 실행할 경우 SSH 접근을 하는데 이때 SSH 키를 사용하여 접근하면 쉽게 작업할 수 있다.

이렇게 생성한 SSH 공개 키를 여러 서버에 복사할 때도 앤서블을 활용하면 매우 편리하다.


How ?

먼저 앤서블 콘텐츠 컬렉션의 어떤 모듈을 사용하면 SSH 키를 생성하고 복사할 수 있는지를 찾아야 한다.

분석을 통해 플레이북 설계와 개발에 필요한 모듈을 찾아보자.

  • 사용자 아이디는 외부 변수로 받는다.
  • ansible-server(제어 노드)에서 ansible 계정을 생성하고 SSH 키를 생성한다.
  • ansible-server(제어 노드)에 생성된 SSH 공개 키를 각 관리 node에 복사한다.
  • 계정을 생성할 때는 ansible.builtin.user 모듈을, SSH 공개 키를 복사할 때는 ansible.posix.authorized_key 모듈을 사용한다.

플레이북 개발

실습 진행을 위해 02_SSH키생성및복사 이라는 디렉터리를 하나 생성해주자.

mkdir -p /ansible/02_SSH키생성및복사

그리고 inventory 파일을 생성하자.

호스트 그룹을 따로 나누지 않고 다음과 같이 호스트명으로만 구성된 inventory를 작성하도록 한다.

ubuntu1
ubuntu2

ansible.cfg 파일은 아래와 같이 작성한다.

[defaults]
inventory = ./inventory
remote_user = root
ask_pass = false
roles_path = ./roles
collection_paths = ./collection
interpreter_python=auto

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false%

그리고 외부 변수를 주입하기 위한 secret.yaml 파일을 vars 디렉토리 하위에 생성해주자.

$ vi vars/secret.yaml

userid: ansible

그리고 ansible-vault encrypt 명령어로 암호화해주자.

ansible-vault encrypt vars/secret.yaml

이번에는 SSH 키를 생성하고 복사하는 플레이북을 작성해볼거다.

create_sshkey.yaml 이라는 파일을 만들고 다음과 같은 내용으로 플레이북을 생성한다.

여기서는 실행될 호스트별로 태스크가 작성되었다.

localhost인 ansible-server(제어 노드)에서 생성된 SSH 공개 키는 ansible.posix.authorized_keys 모듈을 이용하여 인벤토리의 관리 노드 각 서버로 복사된다.

이때 키를 등록하기 위해 lookup 함수가 등록되었다.

---

- hosts: localhost
  vars_files:
    - vars/secret.yaml
  tasks:
  - name : Create ssh key
    ansible.builtin.user:
      name: "{{ userid }}"
      generate_ssh_key: true
      ssh_key_bits: 2048
      ssh_key_file: /home/{{ userid }}/.ssh/id_rsa
    
- hosts: ubuntu1
  vars_files:
    - vars/secret.yaml
  tasks:
  - name: Copy SSH Pub key
    ansible.posix.authorized_key:
      user: "{{ userid }}"
      state: present
      key: "{{ lookup('file', '/home/{{ userid }}/.ssh/id_rsa.pub') }}"

첫 번쨰 태스크 설명

  • generate_ssh_key: true: Ansible의 generate_ssh_key 모듈을 사용하여 SSH 키 쌍을 생성함.
  • ssh_key_bits: 4096: 생성되는 SSH 키의 길이를 4096비트로 지정. 이는 강력한 암호화 보안을 제공함.
  • ssh_key_file: /home/{{ userid }}/.ssh/ansible_id_rsa: 생성된 SSH 키 파일이 저장될 경로를 지정
    • {{ userid }}는 Ansible 변수로, 실제로는 사용자 ID로 치환됨. (userid 라는 변수를 주입해줘야 함)
    • 여기서 SSH 키 파일은 사용자의 홈 디렉터리에 위치하게 된다.

두 번째 태스크 설명

  • ansible.posix.authorized_keys: authorized_keys 모듈을 사용하여 사용자의 authorized_keys 파일에 SSH 공개 키를 추가. 이 모듈은 SSH 키 인증을 통해 사용자의 액세스를 제어하는 데 사용된다.
  • user: "{{ userid }}": SSH 키가 추가될 대상 사용자 계정을 지정.
    • {{ userid }}는 변수로 사용자의 ID로 치환됨 (userid 라는 변수를 주입해줘야 함).
  • state: present: 지정된 키가 존재하도록 보장.
    • 즉, 키가 존재하지 않으면 추가하고, 이미 존재하면 변경하지 않음.
  • key: "{{ lookup('file', '/home/{{ userid }}/.ssh/ansible_id_rsa.pub') }}": lookup('file', ...) 함수를 사용하여 로컬 파일 시스템에서 SSH 공개 키 파일을 읽는다.
    • 이 파일의 경로는 /home/{{ userid }}/.ssh/ansible_id_rsa.pub로 지정되어 있으며,
    • 이는 첫 번째 Play에서 생성된 SSH 공개 키 파일이다.

플레이북을 실행해보면 다음과 같다.

$ ansible-playbook -i ./inventory --ask-vault-pass create_sshkey.yaml

TASK [Gathering Facts] *****************************************************************************************************************************************************************************************************************************************************
[WARNING]: Platform linux on host ubuntu1 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-
core/2.17/reference_appendices/interpreter_discovery.html for more information.
ok: [ubuntu1]

TASK [Copy SSH Pub key] ****************************************************************************************************************************************************************************************************************************************************
ok: [ubuntu1]

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

자 그러면, 제대로 실행되었는지 검증해보자.

우선 ansible-server(제어 노드)에서 ansible 계정으로 전환 후 ansible 홈 디렉토리의 .ssh 디렉토리에 SSH 공개 키와 개인 키가 생성되었는지 확인하자.

$ su - ansible
$ ls -lh ~/.ssh

-rw------- 1 ansible ansible 1.8K  9518:16 id_rsa
-rw-r--r-- 1 ansible ansible  407  9518:16 id_rsa.pub

공개 키와 개인 키가 정상적으로 생성되었다면 각 제어 노드(ubuntu1)에 접속하여 /home/ansible/.ssh 디렉토리에 authorized_keys 파일이 생성되었는지 확인하자.

# ubuntu1
root@ubuntu1:~# ls -lh ~/.ssh
total 4.0K
-rw------- 1 root root 1.2K Sep  5 09:19 authorized_keys

정상적으로 파일이 생성되어있다.


3. 호스트명 설정하기

초기에 시스템을 구성하다 보면 여러 서버의 호스트명을 설정하고 /etc/hosts 파일에 호스트명을 추가하는 작업들을 종종 하게 된다.

호스트명을 추가하는 것은 각 서버에서 수동으로 해줘도 될 만큼 쉬운 작업이지만

서버 대수가 늘어난다면 정신/육체적인 피로가 폭발하게 된다.

이때 앤서블을 이용해 호스트명을 설정하고 /etc/hosts 파일에 해당 호스트 정보까지 추가하는 작업을 한다면 매우 효율적이다.


How ?

호스트명을 설정할 때는 단순히 호스트명만 설정할 때가 있고, FQDN(Fully Qualified Domain Name) 형식의 호스트명을 설정할 때가 있다.

시스템을 구성할 때는 단순 이름보다는 FQDN 형식의 이름을 호스트명으로 자주 사용한다.

앤서벌의 어떤 모듈을 사용하여 호스트명을 확인하고 설정할 수 있는지 알아보자.


  • 앤서블로 접근하기 위한 대상 서버들은 이미 제어 노드의 인벤토리에 등록되어 있다.
  • 호스트명을 설정하기 위해 ansible.bultin.hostname 모듈을 사용한다.
  • /etc/hosts에 노드 정보들을 등록하기 위해 필요한 정보들을 변수로 정의한다.
  • 호스트명을 hosts 파일에 추가할 때는 ansible.bultin.lineinfile 모듈을 사용한다.

플레이북 개발

실습 진행을 위해 03_hostname 디렉토리를 생성해주자.

mkdir ./03_hostname

그리고 inventory 파일을 생성하자.

호스트 그룹을 따로 나누지 않고 다음과 같이 호스트명으로만 구성된 inventory를 작성하도록 한다.

[nodes]
ubuntu1
ubuntu2

ansible.cfg 파일은 아래와 같이 작성한다.

[defaults]
inventory = ./inventory
remote_user = root
ask_pass = false
roles_path = ./roles
collection_paths = ./collection
interpreter_python=auto

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false%

hosts 파일에 추가할 호스트 정보들을 vars_hosts_info.yaml 파일로 생성하고 다음 내용을 변수로 설정한다.

이때 변수는 사전형 변수로 정의하여 반복문을 사용할 수 있도록 한다.

nodes:
  - hostname: ubuntu1
    fqdn: ubuntu1-ubuntu.exp.com
    net_ip: 192.168.219.192
  - hostname: ubuntu2
    fqdn: ubuntu2-ubuntu.exp.com
    net_ip: 192.168.219.193

마지막으로 메인 플레이북을 개발한다.

이번엔 변수를 외부 파일로부터 읽어와 사용한다.

vars_files 라는 키워드를 사용하여 변수가 정의된 파일명을 입력하면 해당 파일로부터 변수를 가져와 사용할 수 있다.

---
- hosts: nodes
  vars_files: vars_hosts_info.yaml

  tasks:
    - name: Set hostname from inventory
      ansible.builtin.hostname:
        name: "{{ inventory_hostname }}"
    
    - name: Add host ip to hosts
      ansible.builtin.lineinfile:
        path: /etc/hosts
        line: "{{ item.net_ip }} {{ item.hostname }} {{ item.fqdn }}"
        regexp: "^{{ item.net_ip }}"
      loop: "{{ nodes }}"

플레이북 작성이 끝났다면 문법 체크를 하고 플레이북을 실행해보자.

$ ansible-playbook --systax-check set_hostname.yaml
$ ansible-playbook -i ./inventory set_hostname.yaml

TASK [Set hostname from inventory] *****************************************************************************************************************************************************************************************************************************************
ok: [ubuntu2]
ok: [ubuntu1]

TASK [Add host ip to hosts] ************************************************************************************************************************************************************************************************************************************************
changed: [ubuntu1] => (item={'hostname': 'ubuntu1', 'fqdn': 'ubuntu1-ubuntu.exp.com', 'net_ip': '192.168.219.192'})
changed: [ubuntu2] => (item={'hostname': 'ubuntu1', 'fqdn': 'ubuntu1-ubuntu.exp.com', 'net_ip': '192.168.219.192'})
changed: [ubuntu2] => (item={'hostname': 'ubuntu2', 'fqdn': 'ubuntu2-ubuntu.exp.com', 'net_ip': '192.168.219.193'})
changed: [ubuntu1] => (item={'hostname': 'ubuntu2', 'fqdn': 'ubuntu2-ubuntu.exp.com', 'net_ip': '192.168.219.193'})

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

4. Facts를 이용한 시스템 모니터링

앤서블은 팩트라는 유용한 기능을 제공한다.

팩트는 관리 노드에서 시스템과 관련된 정보들을 자동으로 찾아 변수로 제공하고 있다.

이를 이용하면 실행 중인 관리 노드의 인프라 정보를 파악하거나 이를 로그로 저장할 수 있다.

이렇게 저장된 로그는 연동된 모니터링 도구에서 활용 가능하다.


How ?

팩트를 이용하여 시스템 정보를 모니터링하려면 팩트에서 어떤 정보를 제공하고 어떤 정보를 수집할 것인지 먼저 확인해야 한다.


  • 팩트를 이용하여 다음과 같은 정보를 추출한다.
    • 호스트 이름
    • 커널 버전
    • 네트워크 인터페이스 이름
    • 네트워크 인터페이스 IP 주소
    • 운영체제 버전
    • CPU 개수
    • 사용 가능 메모리
    • 스토리지 장치의 크기 및 여유 공간
  • 추출한 내용은 ansible.builtin.shell 모듈을 이용하여 /var/log/daily_check 디렉토리에 저장한다.

플레이북 개발

실습 진행을 위해 04_system_info 디렉토리를 생성해주자.

mkdir ./04_system_info

그리고 inventory 파일을 생성하자.

호스트 그룹을 따로 나누지 않고 다음과 같이 호스트명으로만 구성된 inventory를 작성하도록 한다.

[nodes]
ubuntu1
ubuntu2

ansible.cfg 파일은 아래와 같이 작성한다.

[defaults]
inventory = ./inventory
remote_user = root
ask_pass = false
roles_path = ./roles
collection_paths = ./collection
interpreter_python=auto

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false%

그리고 system_info.yaml 파일을 다음과 같이 작성해주자.

ansible.builtin.debug 모듈을 이용하여 ansible_facts에서 수집한 시스템 변수 중 추출하고자 하는 변수를 먼저 msg를 통해 출력하고,

출력한 결과를 register 키워드를 사용하여 result 변수에 저장한다.

그리고 이렇게 저장된 내용을 ansible.builtin.shell 모듈에서 loop 키워드를 사용하여 하나씩 로그 파일에 저장할 것이다.

---
- hosts: nodes
  vars:
    log_directory: /var/log/daily_check
  
  tasks:
    - name: Print system info
      ansible.builtin.debug:
        msg:
          - "############ Start ############"
          - "Date: {{ ansible_facts.date_time.date }} {{ ansible_facts.date_time.time }} "
          - "HostName: {{ ansible_facts.hostname }}"
          - "OS: {{ ansible_facts.distribution }}"
          - "OS Version: {{ ansible_facts.distribution_version }}"
          - "OS Kernel: {{ ansible_facts.kernel }}"
          - "CPU Cores: {{ ansible_facts.processor_vcpus}}"
          - "Memory: {{ ansible_facts.memory_mb.real }}"
          - "Interfaces: {{ ansible_facts.interfaces }}"
          - "IPv4: {{ ansible_facts.all_ipv4_addresses }}"
          - "Devices: {{ ansible_facts.mounts }}"
          - "############ Start ############"
      register: result

    - name: Create log directory
      ansible.builtin.file:
        path: "{{ log_directory }}"
        state: directory
    
    - name: Print logs to log file
      ansible.builtin.shell: |
        echo "{{ item }}" >> "{{ log_directory }}/system_info.logs"
      loop: "{{ result.msg }}"

문법을 체크하고 플레이북을 실행해보자.

$ ansible-playbook --systax-check system_info.yaml
$ ansible-playbook -i ./inventory system_info.yaml

TASK [Create log directory] ************************************************************************************************************************************************************************************************************************************************
changed: [ubuntu2]
changed: [ubuntu1]

TASK [Print logs to log file] **********************************************************************************************************************************************************************************************************************************************
changed: [ubuntu2] => (item=############ Start ############)
changed: [ubuntu1] => (item=############ Start ############)
changed: [ubuntu1] => (item=Date: 2024-09-10 10:09:47 )
changed: [ubuntu2] => (item=Date: 2024-09-10 10:09:48 )
changed: [ubuntu1] => (item=HostName: ubuntu1)
changed: [ubuntu2] => (item=HostName: ubuntu2)
changed: [ubuntu1] => (item=OS: Ubuntu)
changed: [ubuntu2] => (item=OS: Ubuntu)
changed: [ubuntu1] => (item=OS Version: 22.04)
changed: [ubuntu2] => (item=OS Version: 22.04)
changed: [ubuntu1] => (item=OS Kernel: 5.15.0-119-generic)
changed: [ubuntu2] => (item=OS Kernel: 5.15.0-119-generic)
changed: [ubuntu1] => (item=CPU Cores: 1)
changed: [ubuntu2] => (item=CPU Cores: 1)
changed: [ubuntu1] => (item=Memory: {'total': 3912, 'used': 511, 'free': 3401})
changed: [ubuntu2] => (item=Memory: {'total': 3912, 'used': 541, 'free': 3371})
changed: [ubuntu1] => (item=Interfaces: ['ens18', 'lo'])
changed: [ubuntu2] => (item=Interfaces: ['lo', 'ens18'])
changed: [ubuntu1] => (item=IPv4: ['192.168.219.192'])
changed: [ubuntu2] => (item=IPv4: ['192.168.219.193'])
changed: [ubuntu1] => (item=Devices: [{'mount': '/', 'device': '/dev/mapper/ubuntu--vg-ubuntu--lv', 'fstype': 'ext4', 'options': 'rw,relatime', 'dump': 0, 'passno': 0, 'size_total': 25180848128, 'size_available': 16110280704, 'block_size': 4096, 'block_total': 6147668, 'block_available': 3933174, 'block_used': 2214494, 'inode_total': 1572864, 'inode_available': 1487555, 'inode_used': 85309, 'uuid': 'efab5bcf-8a63-4463-a220-1ed261707ab1'}, {'mount': '/snap/core20/2318', 'device': '/dev/loop1', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 67108864, 'size_available': 0, 'block_size': 131072, 'block_total': 512, 'block_available': 0, 'block_used': 512, 'inode_total': 12057, 'inode_available': 0, 'inode_used': 12057, 'uuid': 'N/A'}, {'mount': '/snap/snapd/21759', 'device': '/dev/loop3', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 40763392, 'size_available': 0, 'block_size': 131072, 'block_total': 311, 'block_available': 0, 'block_used': 311, 'inode_total': 651, 'inode_available': 0, 'inode_used': 651, 'uuid': 'N/A'}, {'mount': '/snap/core20/1974', 'device': '/dev/loop4', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 66584576, 'size_available': 0, 'block_size': 131072, 'block_total': 508, 'block_available': 0, 'block_used': 508, 'inode_total': 11995, 'inode_available': 0, 'inode_used': 11995, 'uuid': 'N/A'}, {'mount': '/snap/snapd/19457', 'device': '/dev/loop2', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 55967744, 'size_available': 0, 'block_size': 131072, 'block_total': 427, 'block_available': 0, 'block_used': 427, 'inode_total': 658, 'inode_available': 0, 'inode_used': 658, 'uuid': 'N/A'}, {'mount': '/snap/lxd/29351', 'device': '/dev/loop0', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 91357184, 'size_available': 0, 'block_size': 131072, 'block_total': 697, 'block_available': 0, 'block_used': 697, 'inode_total': 959, 'inode_available': 0, 'inode_used': 959, 'uuid': 'N/A'}, {'mount': '/snap/lxd/24322', 'device': '/dev/loop5', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 117440512, 'size_available': 0, 'block_size': 131072, 'block_total': 896, 'block_available': 0, 'block_used': 896, 'inode_total': 873, 'inode_available': 0, 'inode_used': 873, 'uuid': 'N/A'}, {'mount': '/boot', 'device': '/dev/sda2', 'fstype': 'ext4', 'options': 'rw,relatime', 'dump': 0, 'passno': 0, 'size_total': 2040373248, 'size_available': 1779724288, 'block_size': 4096, 'block_total': 498138, 'block_available': 434503, 'block_used': 63635, 'inode_total': 131072, 'inode_available': 130756, 'inode_used': 316, 'uuid': '7bd52725-dc76-44b1-ab51-4308590bf771'}]) 
changed: [ubuntu2] => (item=Devices: [{'mount': '/', 'device': '/dev/mapper/ubuntu--vg-ubuntu--lv', 'fstype': 'ext4', 'options': 'rw,relatime', 'dump': 0, 'passno': 0, 'size_total': 25180848128, 'size_available': 16343920640, 'block_size': 4096, 'block_total': 6147668, 'block_available': 3990215, 'block_used': 2157453, 'inode_total': 1572864, 'inode_available': 1489492, 'inode_used': 83372, 'uuid': 'ad2973b8-4adf-412e-8459-12c35403ae5d'}, {'mount': '/snap/core20/1974', 'device': '/dev/loop2', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 66584576, 'size_available': 0, 'block_size': 131072, 'block_total': 508, 'block_available': 0, 'block_used': 508, 'inode_total': 11995, 'inode_available': 0, 'inode_used': 11995, 'uuid': 'N/A'}, {'mount': '/snap/snapd/21759', 'device': '/dev/loop0', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 40763392, 'size_available': 0, 'block_size': 131072, 'block_total': 311, 'block_available': 0, 'block_used': 311, 'inode_total': 651, 'inode_available': 0, 'inode_used': 651, 'uuid': 'N/A'}, {'mount': '/snap/snapd/19457', 'device': '/dev/loop1', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 55967744, 'size_available': 0, 'block_size': 131072, 'block_total': 427, 'block_available': 0, 'block_used': 427, 'inode_total': 658, 'inode_available': 0, 'inode_used': 658, 'uuid': 'N/A'}, {'mount': '/snap/lxd/29351', 'device': '/dev/loop3', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 91357184, 'size_available': 0, 'block_size': 131072, 'block_total': 697, 'block_available': 0, 'block_used': 697, 'inode_total': 959, 'inode_available': 0, 'inode_used': 959, 'uuid': 'N/A'}, {'mount': '/snap/core20/2318', 'device': '/dev/loop4', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 67108864, 'size_available': 0, 'block_size': 131072, 'block_total': 512, 'block_available': 0, 'block_used': 512, 'inode_total': 12057, 'inode_available': 0, 'inode_used': 12057, 'uuid': 'N/A'}, {'mount': '/snap/lxd/24322', 'device': '/dev/loop5', 'fstype': 'squashfs', 'options': 'ro,nodev,relatime,errors=continue', 'dump': 0, 'passno': 0, 'size_total': 117440512, 'size_available': 0, 'block_size': 131072, 'block_total': 896, 'block_available': 0, 'block_used': 896, 'inode_total': 873, 'inode_available': 0, 'inode_used': 873, 'uuid': 'N/A'}, {'mount': '/boot', 'device': '/dev/sda2', 'fstype': 'ext4', 'options': 'rw,relatime', 'dump': 0, 'passno': 0, 'size_total': 2040373248, 'size_available': 1779724288, 'block_size': 4096, 'block_total': 498138, 'block_available': 434503, 'block_used': 63635, 'inode_total': 131072, 'inode_available': 130756, 'inode_used': 316, 'uuid': '336df425-25c5-40b8-a429-f329dd2eca7f'}]) 
changed: [ubuntu1] => (item=############ Start ############)
changed: [ubuntu2] => (item=############ Start ############)

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

로그 파일이 정상적으로 생성되었는지 테스트하기 위해 노드 중 하나인 ubuntu1 로 이동하여 확인해보자.

root@ubuntu1:~# ls -lh /var/log/daily_check/
total 4.0K
-rw-r--r-- 1 root root 3.2K Sep 10 10:09 system_info.logs

5. CPU, 메모리, 디스크 사용률 모니터링

앤서블 팩트에서 제공하지 않는 모니터링 도구로 조금 더 상세한 자원 사용률을 모니터링하려면 해당 명령어를 사용하기 위한 패키지를 설치하면 된다.

그러나 여러 서버의 사용률을 모니터링해야 한다면 상황은 달라진다.

그런 경우에는 셸 스크립트나 앤서블을 활용하면 조금 더 쉽게 모니터링이 가능해진다.


How ?

이번에는 CPU, 메모리, 디스크 사용률을 조금 더 자세하게 살펴볼 수 있는 dstat, iostat, vmstat와 같은 명령어를 사용하여

관리 노드의 CPU, 메모리, 디스크 사용률을 모니터링해보자.

dstatiostat를 실행하려면 우선 dstatsysstat 이라는 패키지를 설치해야 한다.

그리고 각각의 명령어에 해당하는 옵션도 다양하다.

  • dstat, sysstat 패키지를 설치한다. 운영체제가 레드헷 계열이면 ansible.builtin.dnf를, 데비안 계열이면 ansible.builtin.apt 모듈을 이용하여 설치한다.
  • 각각의 명령어 실행은 ansible.builtin.shell을 이용해 실행하고, loop 키워드를 사용하여 모니터링 명령어 별로 여러 옵션을 추가하여 명령을 실행한다.
  • 실행된 명령어 결과를 로그 디렉토리에 저장한다.

플레이북 개발

실습 진행을 위해 05_monitoring_system 디렉토리를 생성해주자.

mkdir ./05_monitoring_system

그리고 inventory 파일을 생성하자.

호스트 그룹을 따로 나누지 않고 다음과 같이 호스트명으로만 구성된 inventory를 작성하도록 한다.

[nodes]
ubuntu1
ubuntu2

ansible.cfg 파일은 아래와 같이 작성한다.

[defaults]
inventory = ./inventory
remote_user = root
ask_pass = false
roles_path = ./roles
collection_paths = ./collection
interpreter_python=auto

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false%

변수 정의를 위해 vars_packages.yaml 파일을 생성하고 변수와 변수 값을 정의한다.

---
log_directory: /home/ansible/logs
packages:
  - dstat
  - sysstat

변수 정의가 끝나면 monitoring_system.yaml 파일을 생성하고,

모니터링 명령어를 실행하는 ansible.builtin.shell 모듈을 작성한다.

loop 키워드를 이용하면 실행하고자 하는 명령어들을 사전 형식으로 반복하여 실행하도록 구현할 수 있다.

---
- hosts: nodes
  vars_files: ./vars_packages.yaml

  tasks:
    - name: Install packages on RedHat
      ansible.builtin.dnf:
        name: "{{ item }}"
        state: present
      loop: "{{ packages }}"
      when: ansible_facts.os_family == "RedHat"
    
    - name: Install packages on Debian
      ansible.builtin.apt:
        name: "{{ item }}"
        state: present
      loop: "{{ packages }}"
      when: ansible_facts.os_family == "Debian"

    - name: Monitoring dstat
      ansible.builtin.shell: |
        {{ item }} >> {{ log_directory/dstat.log }}
      loop:
        - dstat 2 10
        - dstat -cmdlt -D vda 2 10
    
    - name: Monitoring iostat
      ansible.builtin.shell: |
        {{ item }} >> {{ log_directory }}/iostat.log
      loop:
        - iostat
        - echo "=========="
        - iostat -t -c -dp vda
        - echo "=========="
  
    - name: Monitoring vmstat
      ansible.builtin.shell: |
        {{ item }} >> {{ log_directory/vmstat.log }}
      loop:
        - vmstat
        - echo "=========="
        - vmstat -dt
        - echo "=========="
        - vmstat -D
        - echo "=========="

    - name: Monitoring df
      ansible.builtin.shell: |
        df -h >> {{ log_directory }}/df.log

문법을 체크하고 플레이북을 실행해보자.

$ ansible-playbook --syntax-check monitoring_system.yaml
$ ansible-playbook -i ./inventory monitoring_system.yaml

TASK [Install packages on RedHat] ******************************************************************************************************************************************************************************************************************************************
skipping: [ubuntu1] => (item=dstat) 
skipping: [ubuntu1] => (item=sysstat) 
skipping: [ubuntu1]
skipping: [ubuntu2] => (item=dstat) 
skipping: [ubuntu2] => (item=sysstat)
skipping: [ubuntu2]

TASK [Install packages on Debian] ******************************************************************************************************************************************************************************************************************************************
ok: [ubuntu1] => (item=dstat)
ok: [ubuntu2] => (item=dstat)
ok: [ubuntu1] => (item=sysstat)
ok: [ubuntu2] => (item=sysstat)

TASK [Monitoring dstat] ****************************************************************************************************************************************************************************************************************************************************
changed: [ubuntu1] => (item=dstat 2 10)
changed: [ubuntu2] => (item=dstat 2 10)
changed: [ubuntu2] => (item=dstat -cmdlt -D vda 2 10)
changed: [ubuntu1] => (item=dstat -cmdlt -D vda 2 10)

TASK [Monitoring iostat] ***************************************************************************************************************************************************************************************************************************************************
changed: [ubuntu1] => (item=iostat)
changed: [ubuntu2] => (item=iostat)
changed: [ubuntu1] => (item=echo "==========")
changed: [ubuntu2] => (item=echo "==========")
changed: [ubuntu1] => (item=iostat -t -c -dp vda)
changed: [ubuntu2] => (item=iostat -t -c -dp vda)
changed: [ubuntu1] => (item=echo "==========")
changed: [ubuntu2] => (item=echo "==========")

TASK [Monitoring vmstat] ***************************************************************************************************************************************************************************************************************************************************
changed: [ubuntu1] => (item=vmstat)
changed: [ubuntu2] => (item=vmstat)
changed: [ubuntu1] => (item=echo "==========")
changed: [ubuntu2] => (item=echo "==========")
changed: [ubuntu1] => (item=vmstat -dt)
changed: [ubuntu2] => (item=vmstat -dt)
changed: [ubuntu2] => (item=echo "==========")
changed: [ubuntu1] => (item=echo "==========")
changed: [ubuntu2] => (item=vmstat -D)
changed: [ubuntu1] => (item=vmstat -D)
changed: [ubuntu1] => (item=echo "==========")
changed: [ubuntu2] => (item=echo "==========")

TASK [Monitoring df] *******************************************************************************************************************************************************************************************************************************************************
changed: [ubuntu1]
changed: [ubuntu2]

PLAY RECAP *****************************************************************************************************************************************************************************************************************************************************************
ubuntu1                    : ok=6    changed=4    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
ubuntu2                    : ok=6    changed=4    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

로그 파일이 정상적으로 생성되었는지 확인하기 위해 노드 중 하나인 ubuntu1로 이동하여 /home/ansible/logs 디렉토리를 확인해보자.

root@ubuntu1:~# ls -lh /home/ansible/logs
total 16K
-rw-r--r-- 1 root root  454 Sep 10 10:38 df.log
-rw-r--r-- 1 root root 1.8K Sep 10 10:38 dstat.log
-rw-r--r-- 1 root root 2.9K Sep 10 10:38 iostat.log
-rw-r--r-- 1 root root 1.9K Sep 10 10:38 vmstat.log

트러블슈팅

1. 권한 오류

  • 제어 노드에서 root 계정으로 진행하지 않아 권한 관련 오류가 발생하였음
    • root로 전환 후 해결

2. 변수 문법 오류

  • 플레이북에서 변수를 사용할 때는 큰따옴표와 함께 겹 중괄호({{ }}) 사이에 변수명을 정의한다.

Loading script...