Ansible을 이용하여 Linux 업데이트 자동화 (8편) - 운영 환경을 위한 로그 저장 위치와 보존 정책에서 이어지는 글이다.
8편에서는 Ansible 업데이트 자동화를 실제 Production 환경에서 사용할 수 있도록 로그 저장 위치를 /var/log/ansible로 옮기고, controller 실행 로그와 package change report의 역할을 구분했다.
이번 9편에서는 업데이트 후 재부팅 처리 과정을 조금 더 안정적으로 다듬는다.
기존에도 Debian/Ubuntu 계열은 /run/reboot-required와 needrestart, RHEL 계열은 dnf needs-restarting --reboothint를 이용해 재부팅 필요 여부를 판단했다.
다만 실제 Production 환경에서는 단순히 재부팅 명령을 실행하는 것만으로는 부족하다. 업데이트 후 정말 재부팅이 필요했는지, 재부팅 후 실제로 새 부팅 세션으로 올라왔는지, Ansible이 재접속을 안정적으로 처리했는지까지 확인할 수 있도록 보완하는 것이 좋다.
목차
1) 왜 재부팅 검증을 강화해야 하는가?
패키지 업데이트 자동화에서 재부팅은 민감한 작업이다. 패키지 업데이트 자체는 성공했더라도 커널, systemd, OpenSSH, OpenSSL, glibc 같은 핵심 패키지가 바뀌면 재부팅이 필요할 수 있다.
특히 커널 업데이트의 경우 새 커널 패키지가 설치되었다고 해서 현재 실행 중인 커널이 바로 바뀌는 것은 아니다. 새 커널은 다음 부팅부터 적용된다.
따라서 업데이트 자동화에서는 업데이트 후 재부팅이 필요한지, 재부팅이 필요하다면 실제로 재부팅이 진행되었는지, 재부팅 후 SSH로 다시 접속 가능한지, 정말 이전 부팅 세션이 아니라 새 부팅 세션인지 확인할 수 있어야 한다.
기존 playbook도 ansible.builtin.reboot 모듈을 사용했기 때문에 기본적인 재부팅 대기와 접속 확인은 수행했다. 하지만 운영 환경에서는 로그만 봐도 재부팅 사유, 재부팅이 실제로 이뤄졌는지를 알 수 있는 구조가 더 좋다.
그래서 이번 9편에서는 재부팅 판단 결과를 변수로 분리하고, 재부팅 전후의 boot ID를 비교하는 방식으로 검증을 강화한다.
2) Debian/Ubuntu 계열 재부팅 판단 기준
Debian/Ubuntu 계열에서는 재부팅 필요 여부를 크게 두 가지 기준으로 판단한다.
1. /run/reboot-required 파일 존재 여부
2. needrestart -b 결과의 NEEDRESTART-KSTA 값
/run/reboot-required 파일은 Debian/Ubuntu 계열에서 재부팅이 필요할 때 생성되는 대표적인 플래그다.
하지만 실제 테스트 중 Debian 13에서는 /run/reboot-required가 없는데도 needrestart 기준으로 커널 재부팅이 필요한 경우가 있었다. 그래서 /run/reboot-required만 보는 것보다 needrestart -b의 커널 상태도 함께 보는 편이 더 안전하다.
최종 playbook에서는 다음처럼 재부팅 필요 여부를 deb_reboot_required 변수로 저장한다.
- name: Set Debian reboot decision
ansible.builtin.set_fact:
deb_reboot_required: >-
{{
deb_reboot_required_file.stat.exists
or ((deb_needrestart.stdout | default('')) is search('(?m)^NEEDRESTART-KSTA:[ \t]*[23][ \t]*$'))
}}
changed_when: false
여기서 중요한 부분은 다음 정규식이다.
(?m)^NEEDRESTART-KSTA:[ \t]*[23][ \t]*$
이 조건은 needrestart -b 출력 중에서 아래와 같은 줄을 찾는다.
NEEDRESTART-KSTA: 2
NEEDRESTART-KSTA: 3
KSTA는 kernel status를 의미한다. 여기서 2 또는 3이면 새 커널 적용을 위해 재부팅이 필요한 상태로 판단한다.
즉, Debian/Ubuntu 계열에서는 다음 둘 중 하나라도 참이면 재부팅 대상이 된다.
/run/reboot-required 파일이 존재함
또는
needrestart 결과에서 NEEDRESTART-KSTA가 2 또는 3
이렇게 하면 /run/reboot-required가 없는 경우에도 커널 업데이트 후 재부팅이 필요한 상황을 놓치지 않을 수 있다.
3) Debian/Ubuntu 계열 재부팅 판단 결과 출력
재부팅 판단 결과는 로그에도 남기는 것이 좋다.
그래야 나중에 Ansible 실행 로그만 보고도 "왜 서버가 재부팅됐는지" 확인할 수 있다.
최종 playbook에서는 다음 task를 추가했다.
- name: Show Debian reboot decision summary
ansible.builtin.debug:
msg:
- "deb_reboot_required_file={{ deb_reboot_required_file.stat.exists }}"
- "deb_reboot_required={{ deb_reboot_required }}"
- "{{ deb_needrestart.stdout_lines | default([]) | select('search', '^NEEDRESTART-KSTA:') | list }}"
예를 들어 Debian 13에서는 다음과 비슷하게 출력된다.

"deb_reboot_required_file=False",
"deb_reboot_required=True",
[
"NEEDRESTART-KSTA: 3"
]
이 결과는 중요한 의미가 있다./run/reboot-required 파일은 없었지만, needrestart가 NEEDRESTART-KSTA: 3을 보고했기 때문에 재부팅 대상으로 판단했다는 뜻이다.
즉, 단순히 재부팅이 실행된 것이 아니라, 왜 재부팅했는지 로그에 근거가 남는다.
4) RHEL 계열 재부팅 판단 기준
RHEL 계열에서는 dnf needs-restarting --reboothint를 사용한다.
최종 playbook에서는 다음 task로 재부팅 필요 여부를 확인한다.
- name: Check RHEL reboot hint
ansible.builtin.command:
argv:
- dnf
- needs-restarting
- --reboothint
register: rhel_reboot_hint
changed_when: false
failed_when: rhel_reboot_hint.rc not in [0, 1]
이 명령은 재부팅이 필요 없으면 보통 rc=0, 재부팅이 필요하면 rc=1을 반환한다.
그래서 다음처럼 rhel_reboot_required 변수로 정리한다.
- name: Set RHEL reboot decision
ansible.builtin.set_fact:
rhel_reboot_required: "{{ rhel_reboot_hint.rc == 1 }}"
changed_when: false
그리고 Debian 계열과 마찬가지로 RHEL 계열도 판단 결과를 로그에 남긴다.
- name: Show RHEL reboot decision summary
ansible.builtin.debug:
msg:
- "rhel_reboot_hint_rc={{ rhel_reboot_hint.rc }}"
- "rhel_reboot_required={{ rhel_reboot_required }}"
실제 출력은 다음과 비슷하다.

"rhel_reboot_hint_rc=1",
"rhel_reboot_required=True"
이렇게 하면 RHEL 계열 서버도 재부팅이 필요한 이유를 로그에서 바로 확인할 수 있다.
5) 재부팅 전후 boot ID 비교
이번 9편에서 가장 중요한 개선 중 하나는 boot ID 검증이다.
Linux에는 현재 부팅 세션을 식별할 수 있는 값이 있다.
/proc/sys/kernel/random/boot_id
이 값은 부팅할 때마다 바뀐다.
따라서 재부팅 전의 boot ID와 재부팅 후의 boot ID가 다르면, 실제로 시스템이 재부팅되었다고 판단할 수 있다.
Debian/Ubuntu 계열에서는 재부팅 전 boot ID를 다음처럼 저장한다.
- name: Capture Debian boot ID before reboot
ansible.builtin.command:
argv:
- cat
- /proc/sys/kernel/random/boot_id
register: deb_boot_id_before
changed_when: false
when: deb_reboot_required | bool
재부팅 후에는 다시 boot ID를 읽는다.
- name: Capture Debian boot ID after reboot
ansible.builtin.command:
argv:
- cat
- /proc/sys/kernel/random/boot_id
register: deb_boot_id_after
changed_when: false
when: deb_reboot_required | bool
그리고 두 값을 비교한다.
- name: Verify that Debian-family host actually rebooted
ansible.builtin.assert:
that:
- deb_boot_id_before.stdout != deb_boot_id_after.stdout
fail_msg: "Debian-family host did not actually reboot."
success_msg: "Debian-family host reboot verification completed successfully."
when: deb_reboot_required | bool
RHEL 계열도 같은 방식이다.
- name: Capture RHEL boot ID before reboot
ansible.builtin.command:
argv:
- cat
- /proc/sys/kernel/random/boot_id
register: rhel_boot_id_before
changed_when: false
when: rhel_reboot_required | bool
- name: Capture RHEL boot ID after reboot
ansible.builtin.command:
argv:
- cat
- /proc/sys/kernel/random/boot_id
register: rhel_boot_id_after
changed_when: false
when: rhel_reboot_required | bool
- name: Verify that RHEL-family host actually rebooted
ansible.builtin.assert:
that:
- rhel_boot_id_before.stdout != rhel_boot_id_after.stdout
fail_msg: "RHEL-family host did not actually reboot."
success_msg: "RHEL-family host reboot verification completed successfully."
when: rhel_reboot_required | bool
이렇게 하면 재부팅 명령이 실행되었다는 사실뿐 아니라, 실제로 부팅 세션이 바뀌었는지까지 확인할 수 있다.
운영 로그에서는 다음과 같은 메시지를 볼 수 있다.


"Debian-family host reboot verification completed successfully."
"RHEL-family host reboot verification completed successfully."
이 메시지가 나오면 Ansible이 재부팅 후 다시 접속했고, boot ID도 실제로 바뀌었다는 뜻이다.
6) ansible.builtin.reboot 옵션 정리
Debian/Ubuntu 계열 재부팅 task는 다음과 같이 작성했다.
- name: Reboot Debian-family host if reboot is required
ansible.builtin.reboot:
msg: "Reboot initiated by Ansible after Debian/Ubuntu updates"
connect_timeout: "{{ connect_timeout_seconds }}"
reboot_timeout: "{{ reboot_timeout_seconds }}"
post_reboot_delay: "{{ post_reboot_delay_seconds }}"
test_command: whoami
when: deb_reboot_required | bool
RHEL 계열도 거의 같다.
- name: Reboot RHEL-family host if required
ansible.builtin.reboot:
msg: "Reboot initiated by Ansible after package updates"
connect_timeout: "{{ connect_timeout_seconds }}"
reboot_timeout: "{{ reboot_timeout_seconds }}"
post_reboot_delay: "{{ post_reboot_delay_seconds }}"
test_command: whoami
when: rhel_reboot_required | bool
여기서 사용하는 변수는 다음과 같다.
reboot_timeout_seconds: 1200
connect_timeout_seconds: 10
post_reboot_delay_seconds: 30
각 값의 의미는 다음과 같다.
connect_timeout_seconds
재부팅 후 SSH 접속을 시도할 때 한 번의 연결 시도에 기다릴 시간이다.
reboot_timeout_seconds
재부팅이 완료되고 다시 접속 가능해질 때까지 기다릴 최대 시간이다.
post_reboot_delay_seconds
재부팅 후 접속이 가능해진 뒤, 추가로 조금 더 기다릴 시간이다.
post_reboot_delay는 특히 웹서버, DB 서버, TURN 서버처럼 부팅 후 여러 서비스가 함께 올라오는 환경에서 여유를 주는 용도로 사용할 수 있다.
물론 post_reboot_delay가 없다고 해서 Apache, Nginx, MariaDB 같은 서비스가 손상되는 것은 아니다. 다만 Ansible이 너무 빨리 다음 검증 task로 넘어가면, 일부 서비스가 아직 올라오는 중일 수 있다.
그래서 운영 환경에서는 10초보다 30초 정도로 여유 있게 두는 편이 더 안정적이다.
7) SSH ControlMaster 비활성화
기존 ansible.cfg에는 SSH 연결 재사용을 위해 다음 설정을 사용했다.
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
일반적인 Ansible 작업에서는 SSH 연결 재사용이 성능에 도움이 된다. 하지만 재부팅 작업에서는 SSH 연결이 끊겼다가 다시 붙는 과정이 핵심이다.
이때 기존 ControlMaster 연결이 남아 있으면, 일부 환경에서는 재접속 검증 과정에 혼선이 생길 수 있다. 특히 Mesh VPN(Tailscale 등), VM, ARM 보드, 클라우드 서버처럼 네트워크 경로와 부팅 시간이 다양하게 섞인 환경에서는 더 보수적으로 접근하는 편이 좋다.
그래서 update_all.yml에서는 Debian/RHEL play에 다음 설정을 추가했다.
ansible_ssh_args: >-
-C
-o ControlMaster=no
-o ControlPath=none
-o ControlPersist=no
이 설정은 해당 play에서 SSH ControlMaster 재사용을 끈다.
즉, 전체 Ansible 설정에서는 연결 재사용을 유지하되, 패키지 업데이트와 재부팅을 수행하는 play에서는 더 보수적인 SSH 연결 방식을 사용하게 된다.
정리하면 다음과 같다.
ansible.cfg
일반 작업에서는 SSH ControlMaster를 사용해 성능 확보
update_all.yml
업데이트 후 재부팅 검증이 필요하므로 ControlMaster를 끄고 안정성 우선
8) 재부팅이 필요 없지만 서비스 또는 세션 재시작이 필요한 경우
Debian/Ubuntu 계열에서는 전체 재부팅까지는 필요 없지만, 일부 서비스 또는 사용자 세션만 재시작이 필요한 경우가 있다.
이때 needrestart -b 출력에는 다음과 같은 줄이 나올 수 있다.

"NEEDRESTART-SVC: systemd-timesyncd.service",
"NEEDRESTART-SVC: systemd-udevd.service",
"NEEDRESTART-SVC: udisks2.service",
"NEEDRESTART-SVC: wpa_supplicant.service",
"NEEDRESTART-SESS: ansible @ user manager"
최종 playbook에서는 전체 재부팅이 필요하지 않지만, 서비스 또는 세션 재시작 대상이 있을 때만 경고를 출력한다.
- name: Warn if services or sessions still need restart without full reboot
ansible.builtin.debug:
msg:
- "needrestart reported restartable services or sessions."
- "{{ deb_needrestart.stdout_lines | default([]) | select('search', '^NEEDRESTART-(SVC|SESS):') | list }}"
- "Consider restarting affected services if no full reboot was performed."
when:
- not deb_reboot_required | bool
- (deb_needrestart.stdout | default('')) is search('(?m)^NEEDRESTART-(SVC|SESS):')
이렇게 하면 단순히 needrestart 출력이 있다는 이유만으로 경고하지 않고, 실제 서비스나 세션 재시작 대상이 있을 때만 경고한다.
즉, 최종 구조는 다음과 같다.
• 커널 재부팅 필요: 서버 전체 재부팅
• 커널 재부팅은 필요 없음, 하지만 서비스/세션 재시작 필요: 경고 출력
• 둘 다 해당 없음: 아무 경고 없이 진행
이 방식이 운영 로그를 읽기에도 더 명확하다.
9) 실제 실행 결과 확인
최종 playbook을 실행하기 전에 먼저 check mode로 미리 확인한다.
ansible-playbook playbooks/update_all.yml --check --diff
--check --diff에서는 실제 업데이트와 로그 생성, 재부팅은 수행하지 않는다.
대신 Debian/Ubuntu 계열에서는 업그레이드 예정 패키지를 보여주고, RHEL 계열에서는 dnf upgrade --assumeno와 dnf 모듈의 check mode 결과를 통해 변경 예정 내용을 확인할 수 있다.
실제 적용은 다음 명령으로 실행한다.
ansible-playbook playbooks/update_all.yml
실행 후에는 다음과 같은 흐름을 확인할 수 있다.
• 패키지 업데이트
• 패키지 변경 리포트 생성
• 재부팅 필요 여부 판단
• 필요하면 재부팅
• 재부팅 후 boot ID 검증
• 최종 package-changes 로그 경로 출력
예를 들어 Debian 계열에서 다음과 같은 출력이 나오면 정상이다.

"deb_reboot_required_file=False",
"deb_reboot_required=True",
[
"NEEDRESTART-KSTA: 3"
]
이 경우 /run/reboot-required 파일은 없지만, needrestart가 커널 기준 재부팅 필요 상태를 감지한 것이다.
재부팅 후에는 다음 메시지가 나와야 한다.

"Debian-family host reboot verification completed successfully."
RHEL 계열에서는 다음과 같은 출력이 나오면 재부팅이 필요한 상태다.

"rhel_reboot_hint_rc=1",
"rhel_reboot_required=True"
재부팅 후에는 다음 메시지를 확인한다.

"RHEL-family host reboot verification completed successfully."
마지막에는 실행 단위 로그 디렉터리가 출력된다.

"Package change logs are stored in /var/log/ansible/package-changes/20260514-152053-s6761c_x"
10) 운영 환경에서 주의할 점
이번 playbook은 운영 환경에서 사용할 수 있도록 많이 다듬었지만, 그래도 패키지 업데이트와 재부팅은 영향이 큰 작업이다.
따라서 실제 Production 서버에 적용할 때는 다음 절차를 권장한다.
- 먼저
--check --diff로 변경 예정 내용을 확인한다. - 중요 서버는 스냅샷 또는 백업 상태를 확인한다.
- 처음에는 --limit으로 일부 서버에만 적용한다.
- 문제가 없으면 전체 서버에 적용한다.
- 재부팅 후 주요 서비스 상태를 확인한다.
예를 들어 처음에는 특정 호스트 하나만 대상으로 실행할 수 있다.
ansible-playbook playbooks/update_all.yml --limit debian13
또는 Debian 계열만 먼저 실행할 수도 있다.
ansible-playbook playbooks/update_all.yml --limit debian_family
업데이트 후 웹서버나 DB 서버가 있다면 다음과 같이 서비스 상태를 확인한다.
systemctl status apache2 --no-pager
systemctl status mariadb --no-pager
systemctl status nginx --no-pager
중요한 것은 Ansible playbook이 끝났다고 해서 모든 애플리케이션 검증이 끝난 것은 아니라는 점이다.
Ansible은 시스템 업데이트와 재부팅 검증까지 자동화해 주지만, 실제 서비스가 정상적으로 동작하는지는 관리자가 별도로 확인하는 편이 안전하다.
11) 9편 최종 Ansible 코드 파일 링크
https://drive.google.com/file/d/1sxlXnn9gJb_Xlw2zPI5j-jmwqzg_FDg6
9편의 최종 Ansible 코드이다. 위 링크의 압축 파일에는 ansible.cfg, update_all.yml 등 모든 Ansible 코드가 포함되어 있다.
12) 마무리하며
이번 9편에서는 패키지 업데이트 후 재부팅 처리 과정을 조금 더 Production 환경에 맞추고 안정성을 높였다.
Debian/Ubuntu 계열에서는 기존에 사용하던 /run/reboot-required와 needrestart의 NEEDRESTART-KSTA 기반 재부팅 판단을 deb_reboot_required 변수로 정리하고, NEEDRESTART-KSTA 값을 더 정확하게 감지하도록 정규식을 보강했다. RHEL 계열도 dnf needs-restarting --reboothint 결과를 rhel_reboot_required 변수로 정리해 재부팅 판단 근거를 로그에서 확인할 수 있게 했다.
또한 재부팅 전후 boot ID를 비교하여 실제로 새로운 부팅 세션으로 올라왔는지도 확인하도록 했다.
실제 Production 환경에서는 작업 진행 전 유지보수 시간대, 백업, 서비스 상태 확인 절차를 함께 체크하는 것이 좋다.