RHEL 실전 #5 Ansible로 RHEL 자동화: RHCE 트랙으로 연결

8 분 소요

RHEL 실전 트랙의 #1〜#4에서 nginx 웹 서버, PostgreSQL, Podman 컨테이너, 모니터링을 손으로 올렸습니다. 한 대를 그렇게 다루는 일은 충분히 익혔습니다. 그런데 같은 구성을 두 번째, 세 번째 서버에 올릴 때부터 상황이 달라집니다. 똑같은 dnf 명령, 똑같은 systemctl, 똑같은 firewalld 규칙, 똑같은 SELinux boolean을 손으로 다시 치다 보면 한두 단계는 반드시 빠집니다. 이번 글에서는 그 손작업을 Ansible로 옮겨 같은 결과를 코드 한 벌로 재현하는 큰 그림을 정리하겠습니다.

이 글의 목적은 Ansible 문법을 깊이 파는 것이 아닙니다. “왜 자동화로 넘어가는가”, 그리고 RHEL 실무에서 익힌 손작업이 어떻게 playbook 한 줄로 바뀌는지를 보여 주는 것입니다. 본격적인 Ansible 문법과 RHCE 시험 범위는 RHCE 시리즈에서 따로 다루겠습니다.

손작업의 한계 #

#1에서 nginx를 올린 절차를 떠올려 보겠습니다. 패키지를 설치하고, 서비스를 enable하고, firewalld에서 http,https를 열고, 비표준 포트를 쓰면 SELinux port 레이블을 등록했습니다. 한 대라면 5분이면 끝납니다. 그런데 이 작업에는 세 가지 약점이 있습니다.

첫째, 재현이 어렵습니다. 두 번째 서버에서 firewalld --reload를 빠뜨리거나 setsebool-P를 안 붙이면, 그 서버만 미묘하게 다르게 동작합니다. 둘째, 기록이 남지 않습니다. 6개월 뒤에 “이 서버는 어떤 boolean을 켰더라"를 떠올리려면 명령 히스토리를 뒤져야 합니다. 셋째, 검증할 방법이 없습니다. 지금 상태가 의도한 상태와 같은지 확인하려면 결국 손으로 하나씩 확인해야 합니다.

Ansible은 이 세 가지를 한 번에 해결합니다. 절차가 아니라 원하는 상태를 코드로 적어 두면, Ansible이 현재 상태와 비교해 부족한 부분만 맞춰 줍니다. 코드 자체가 곧 기록이고, 같은 코드를 몇 번 돌려도 결과가 같습니다.

Ansible 설치와 최소 구성 #

RHEL에서 Ansible은 제어 노드(명령을 내리는 쪽)에만 설치합니다. 대상 서버에는 Python과 SSH만 있으면 됩니다. 별도 에이전트를 깔지 않는 점이 Ansible의 큰 장점입니다.

# 제어 노드에 ansible-core 설치
sudo dnf install -y ansible-core

# 버전 확인
ansible --version

ansible-core는 엔진과 기본 모듈만 담은 경량 패키지입니다. 더 많은 컬렉션이 묶인 ansible 패키지도 있지만, RHEL 작업에는 ansible-core에 필요한 컬렉션을 따로 더하는 방식이 깔끔합니다. 다음은 작업 디렉터리에 inventory와 ansible.cfg를 둡니다.

# inventory: 관리할 서버 목록
[web]
web1.example.com
web2.example.com

[db]
db1.example.com
# ansible.cfg: 프로젝트 기본값
[defaults]
inventory = ./inventory
remote_user = ansible
host_key_checking = False

[privilege_escalation]
become = True
become_method = sudo

inventory는 어떤 서버를 다룰지를 그룹으로 묶은 파일이고, ansible.cfg는 매번 옵션을 붙이지 않도록 기본값을 모아 둔 파일입니다. become = True는 대상에서 sudo로 권한을 올린다는 뜻이라, RHEL에서 패키지 설치나 서비스 제어 같은 작업에 필수입니다. 연결이 되는지는 한 줄로 확인합니다.

# 모든 호스트에 ping (SSH,Python 연결 점검)
ansible all -m ping

ping 모듈은 ICMP가 아니라 SSH로 붙어 Python이 동작하는지를 확인합니다. 여기서 pong이 돌아오면 playbook을 돌릴 준비가 된 것입니다.

멱등성: 같은 결과를 보장하는 핵심 #

Ansible을 이해하는 데 가장 중요한 개념이 멱등성입니다. 같은 playbook을 몇 번 돌려도 결과가 한 번 돌린 것과 같다는 성질입니다. 손으로 dnf install을 두 번 치면 두 번째는 “이미 설치됨"이 뜨지만, 스크립트로 무턱대고 명령을 나열하면 두 번째 실행에서 엉뚱한 부작용이 생기기 쉽습니다.

Ansible 모듈은 명령이 아니라 상태를 다룹니다. state: present라고 적으면 “이 패키지가 있어야 한다"는 뜻이지 “지금 설치하라"가 아닙니다. 이미 있으면 아무것도 하지 않고 ok로 넘어가고, 없을 때만 설치하며 changed로 보고합니다. 그래서 처음 실행에서는 여러 항목이 changed로 뜨지만, 곧바로 다시 돌리면 모두 ok가 됩니다. 이 “두 번째 실행에서 전부 ok"가 멱등성이 지켜졌다는 신호입니다.

이 성질 덕분에 playbook은 “한 번 만드는 설치 스크립트"가 아니라 “언제 돌려도 서버를 의도한 상태로 맞추는 정의서"가 됩니다. 서버가 어쩌다 틀어졌어도 같은 playbook을 다시 돌리면 제자리로 돌아옵니다.

손작업을 playbook으로 #

이제 #1의 nginx 작업을 playbook 하나로 옮겨 보겠습니다. 패키지 설치, 서비스 enable, firewalld 개방, SELinux boolean까지 손으로 했던 네 가지를 한 파일에 담습니다.

# web.yml: nginx 한 사이클
- name: 웹 서버 구성
  hosts: web
  become: true
  tasks:
    - name: nginx 설치
      ansible.builtin.dnf:
        name: nginx
        state: present

    - name: nginx 서비스 enable + start
      ansible.builtin.systemd:
        name: nginx
        enabled: true
        state: started

    - name: firewalld에서 http,https 허용
      ansible.posix.firewalld:
        service: "{{ item }}"
        permanent: true
        immediate: true
        state: enabled
      loop:
        - http
        - https

    - name: reverse proxy용 SELinux boolean
      ansible.posix.seboolean:
        name: httpd_can_network_connect
        state: true
        persistent: true

손으로 친 명령과 한 줄씩 맞춰 보면 대응이 또렷합니다. dnf install -y nginxdnf 모듈의 state: present로, systemctl enable --now nginxsystemd 모듈의 enabled: true + state: started로, firewall-cmd --add-service ... --permanent + --reloadfirewalld 모듈의 permanent: true + immediate: true로, setsebool -Pseboolean 모듈의 persistent: true로 옮겨졌습니다. #1에서 손으로 했던 작업이 그대로 코드가 된 셈입니다.

실행은 한 줄입니다.

# 실제 적용 전에 변경 예정만 미리 보기
ansible-playbook web.yml --check

# 실제 적용
ansible-playbook web.yml

--check는 실제로 바꾸지 않고 무엇이 바뀔지만 보여 주는 모드입니다. 운영 서버에 적용하기 전에 이 모드로 한 번 돌려 영향 범위를 확인하는 습관이 사고를 줄입니다. inventory의 [web] 그룹에 web1, web2가 들어 있으니, 이 playbook 한 번으로 두 서버가 똑같이 구성됩니다. 서버가 열 대로 늘어도 같은 명령 한 줄입니다. 손작업의 세 가지 약점이 여기서 모두 사라집니다.

rhel-system-roles로 추상화 #

위 playbook은 firewalld와 SELinux를 모듈로 직접 다뤘습니다. Red Hat은 여기서 한 단계 더 추상화한 rhel-system-roles를 공식 제공합니다. 방화벽, SELinux, 시간 동기화, 스토리지 같은 RHEL 운영 영역을 미리 검증된 role로 묶어, 모듈 호출 대신 “원하는 결과"만 변수로 적으면 되도록 만든 것입니다.

# 시스템 role 컬렉션 설치
sudo dnf install -y rhel-system-roles

예를 들어 시간 동기화는 timesync role에 NTP 서버만 넘기면 chrony 설정과 서비스까지 알아서 맞춥니다.

# timesync.yml: 시간 동기화 role
- name: 시간 동기화 구성
  hosts: all
  become: true
  roles:
    - role: rhel-system-roles.timesync
      vars:
        timesync_ntp_servers:
          - hostname: 0.kr.pool.ntp.org
          - hostname: 1.kr.pool.ntp.org

같은 방식으로 selinux role은 boolean,port,fcontext를 변수로 받아 #1에서 손으로 한 semanage,setsebool 작업을 대신하고, firewall role은 서비스,포트 개방을 변수로 받습니다. 모듈을 직접 쓰는 것보다 추상화 수준이 높아, RHEL 표준 구성에 가까운 일은 system-roles로, 세밀한 제어가 필요한 일은 모듈로 나눠 쓰는 것이 현실적인 운영입니다. system-roles는 Red Hat이 RHEL 버전에 맞춰 유지보수하므로, 직접 모듈을 엮을 때보다 OS 업그레이드에 따른 깨짐도 적습니다.

운영 포인트 #

자동화로 넘어갈 때 실무에서 챙길 점을 정리하겠습니다.

  • 손작업을 먼저 이해한 뒤 자동화합니다. #1〜#4에서 손으로 막혀 본 경험이 있어야 playbook이 무엇을 하는지 읽힙니다. 손작업을 건너뛰고 playbook부터 베끼면 막혔을 때 손을 못 댑니다.
  • --check로 먼저 돌립니다. 운영 서버에 바로 적용하지 않고 변경 예정을 확인하는 단계를 거치면 큰 사고를 막습니다.
  • 멱등성을 확인합니다. playbook을 두 번 돌려 두 번째가 전부 ok로 끝나는지 봅니다. 두 번째에도 changed가 계속 뜨면 그 task는 멱등하지 않게 짠 것이라 손봐야 합니다.
  • 표준은 system-roles, 세밀한 것은 모듈로 나눕니다. RHEL 표준 구성에 가까운 작업은 검증된 role을 쓰고, 특수한 요구만 모듈로 직접 제어합니다.
  • playbook을 버전 관리에 둡니다. inventory와 playbook을 Git에 넣으면 코드가 곧 서버 구성 기록이 됩니다.

정리 #

이번 글에서 잡은 것:

  • 왜 자동화인가. 손작업의 약점은 재현 어려움, 기록 부재, 검증 불가입니다. Ansible은 상태를 코드로 적어 이 셋을 한 번에 해결합니다.
  • 최소 구성. 제어 노드에 ansible-core만 설치하고, inventory로 대상을 묶고, ansible.cfg에 기본값을 둡니다. 대상에는 에이전트가 없습니다.
  • 멱등성. 모듈은 명령이 아니라 상태를 다뤄, 몇 번 돌려도 결과가 같습니다. 두 번째 실행이 전부 ok인지로 확인합니다.
  • 손작업 이전. #1의 nginx 네 단계가 dnf,systemd,firewalld,seboolean 모듈 한 playbook으로 그대로 옮겨집니다.
  • 추상화. rhel-system-roles로 selinux,firewall,timesync를 변수만으로 다룹니다.

다음: 트랙 마무리 #

손으로 한 사이클을 돌리고, 그것을 코드로 묶는 데까지 왔습니다. 트랙의 마지막은 지금까지 다룬 조각을 하나의 그림으로 합치는 일입니다.

#6 트랙 마무리: 레퍼런스 아키텍처에서는 웹,DB,컨테이너,모니터링,자동화를 한 장의 레퍼런스 아키텍처로 모아, 실전 트랙 전체를 어떻게 한 시스템으로 운영하는지 정리하겠습니다. 그리고 더 깊은 자동화가 필요하다면 RHCE 시리즈에서 Ansible 문법과 시험 범위를 본격적으로 다루겠습니다.

X