管理台数が増えるほど、パッチ適用漏れやサービス停止の確認忘れが起きやすくなります。緊急パッチが出たときに深夜から手作業で20台を回るのは、体力的にも精神的にも限界があります。
この記事では、Ansibleの
ansible.builtin.dnfモジュールとansible.builtin.aptモジュールを使い、RHEL系・Debian系のLinuxサーバーへのパッチ適用を自動化する方法を解説します。単純な一括実行にとどまらず、serialによるローリング更新で本番環境のリスクを抑えながら、事前チェック・事後検証まで組み込んだ実運用に耐える設計パターンを作り上げます。動作確認環境:Ansible 2.16 / Rocky Linux 9.4 / Ubuntu 24.04 LTS
この記事のポイント
・dnf/aptモジュールのstate:latestで複数台に一括パッチ適用できる
・serialを使うとローリング更新で障害リスクを段階的に抑えられる
・pre_tasks/post_tasksで適用前後の自動検証を組み込める
・register/assertでパッチ失敗を即座に検知して処理を止められる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
はじめに ─ なぜAnsibleでパッチ適用を自動化するのか
Linuxサーバーのパッチ適用は、セキュリティ運用の中で最も重要な定期作業のひとつです。しかし「手でSSHしてdnf update -yを実行し、再起動して、サービスが上がっているか確認して……」という作業を20台に繰り返すと、どこかで必ずヒューマンエラーが発生します。
Ansibleを使うと、次のような課題をまとめて解決できます。
・適用漏れの撲滅:Playbookで対象を一元管理するため、inventoryに載っているサーバーには必ず適用される
・OS差異の吸収:RHEL系(dnf)とDebian系(apt)の差を条件分岐で自動判別できる
・冪等性の担保:何度実行しても「パッチ済みなら変更なし」と判断してくれる
・本番安全策:serialで段階的に適用することで、問題が起きたときに途中で止められる
・証跡の自動記録:実行ログをファイルに残すことで、監査対応も楽になる
Ansibleがないと「誰が・いつ・どのサーバーに・何を適用したか」を手動でスプレッドシートに記録するしかありません。Ansibleがあれば、Playbookとログファイルがそのまま証跡になります。
パッチ適用自動化の全体アーキテクチャ(inventory設計とroleの役割分担)
パッチ適用を自動化するには、まずinventoryの設計から始めます。全サーバーを一つのグループに入れてしまうと、本番・検証・DBサーバーの区別がつかなくなります。実務では次のような階層設計が現場で崩れにくいパターンです。
# inventory/hosts.yml(YAMLインベントリ) all: children: webservers: hosts: web01.example.internal: ansible_host: 192.168.10.11 web02.example.internal: ansible_host: 192.168.10.12 web03.example.internal: ansible_host: 192.168.10.13 dbservers: hosts: db01.example.internal: ansible_host: 192.168.10.21 staging: hosts: stg01.example.internal: ansible_host: 192.168.10.101
この設計のポイントは、サーバーの役割(web/db)と環境(本番/検証)をグループで表現することです。パッチ適用はまずstagingグループで検証し、問題がなければwebservers→dbserversの順で本番に展開するフローを取ります。
1. role構成のベストプラクティス
パッチ適用作業はroleに切り出すと再利用性が上がります。最低限のディレクトリ構成は次の通りです。
roles/ patch_apply/ tasks/ main.yml # エントリポイント rhel.yml # RHEL/Rocky/AlmaLinux向けdnfタスク debian.yml # Ubuntu/Debian向けaptタスク handlers/ main.yml # サービス再起動ハンドラー defaults/ main.yml # 変数デフォルト値(reboot_after_patch: false等) meta/ main.yml # roleのメタ情報
roleに切り出すことで、webservers用のPlaybookでもdbservers用のPlaybookでも同じroleを呼び出せるようになります。
dnfモジュール・aptモジュールの使い方(OS別分岐とstate: latest)
パッチ適用の中核となるのがansible.builtin.dnfとansible.builtin.aptです。それぞれのモジュールの基本的な書き方と、OSを自動判別して分岐させる方法を解説します。
2. RHEL/Rocky Linux向け:dnfモジュール
RHEL 8以降(Rocky Linux、AlmaLinux含む)ではdnfモジュールを使います。name: '*'を指定すると、インストール済みの全パッケージをアップデート対象にできます。
# roles/patch_apply/tasks/rhel.yml --- - name: dnfキャッシュを更新する ansible.builtin.dnf: update_cache: yes - name: 全パッケージを最新に更新する(RHEL/Rocky Linux) ansible.builtin.dnf: name: '*' state: latest security: "{{ security_only | default(false) }}" register: dnf_result notify: check reboot required - name: dnf更新の適用結果を確認する ansible.builtin.debug: msg: "更新されたパッケージ数: {{ dnf_result.results | default([]) | length }}"
security: trueに設定すると、セキュリティアドバイザリが提供されているパッケージのみに絞り込むことができます(RHEL公式アドバイザリが必要)。緊急度の高いセキュリティパッチだけを先に当てたい場合に便利です。
パッチ適用後にインストール済みパッケージのバージョンを確認するには、rpm コマンドの使い方はこちらも参考にしてください。rpm -qa --lastで直近に更新されたパッケージを一覧できます。
3. Ubuntu/Debian向け:aptモジュール
Ubuntu・Debian系サーバーではansible.builtin.aptモジュールを使います。upgrade: distはapt-get dist-upgrade相当で、依存関係を考慮した完全なアップグレードを実行します。
# roles/patch_apply/tasks/debian.yml --- - name: aptパッケージリストを更新する ansible.builtin.apt: update_cache: yes cache_valid_time: 3600 # 1時間以内の更新はスキップ - name: 全パッケージをアップグレードする(Ubuntu/Debian) ansible.builtin.apt: upgrade: dist register: apt_result notify: check reboot required - name: 不要なパッケージを削除する(autoremove) ansible.builtin.apt: autoremove: yes purge: yes
4. ansible_os_familyでOSを自動判別する
RHEL系とDebian系が混在する環境では、ansible_facts['os_family']変数を使って自動分岐させます。tasks/main.ymlで次のようにinclude_tasksと組み合わせます。
# roles/patch_apply/tasks/main.yml --- - name: RHEL系サーバーのパッチ適用タスクを実行する ansible.builtin.include_tasks: rhel.yml when: ansible_facts['os_family'] == "RedHat" - name: Debian系サーバーのパッチ適用タスクを実行する ansible.builtin.include_tasks: debian.yml when: ansible_facts['os_family'] == "Debian"
この書き方なら、同じinventoryにRocky LinuxとUbuntuが混在していても、Ansibleが自動でOSファミリーを判別してそれぞれ適切なパッケージマネージャーを呼び出してくれます。
Ansibleをゼロから体系的に学ぶなら、Ansible実践講座をご確認ください。
serialによるローリング更新の設計(本番環境でのリスク最小化)
パッチ適用で最も怖いのは「全台同時に適用して、全台でサービスが落ちる」シナリオです。serialを使ったローリング更新を設計すると、このリスクを大幅に下げられます。
5. serialの基本的な書き方
serialはPlaybook上部のhostsと同じレベルに記述します。数値で指定すると「N台ずつ」、文字列パーセントで指定すると「全体のX%ずつ」という意味になります。
# patch_apply.yml(メインPlaybook) --- - name: Linuxサーバーへのパッチ自動適用 hosts: webservers become: yes serial: 1 # 1台ずつ処理(最も安全) max_fail_percentage: 0 # 1台でも失敗したら全体を止める pre_tasks: - name: パッチ適用前の事前チェックを実行する ansible.builtin.include_tasks: pre_checks.yml roles: - patch_apply post_tasks: - name: パッチ適用後の事後検証を実行する ansible.builtin.include_tasks: post_checks.yml
max_fail_percentage: 0は「1台でも失敗したら以降の台への適用を止める」という設定です。本番環境では必ずこの値を0にしておくべきです。デフォルト値は100(全台失敗しても続ける)なので、明示的に指定が必要です。
6. 段階的適用のシナリオ設計
10台以上のサーバーを管理する場合は、serialをリストで指定することで「最初は1台だけ→問題なければ残り全台」という段階的な適用ができます。
# serialをリストで指定するパターン - name: Webサーバーへのローリングパッチ適用 hosts: webservers become: yes serial: - 1 # まず1台だけ試験的に適用 - 2 # 問題なければ2台ずつ - "50%" # さらに問題なければ残りの半分 max_fail_percentage: 0
このパターンは特に初めてPlaybookを本番に適用するときに有効です。最初の1台で問題が発生した場合(サービス起動失敗など)、以降の台への適用は自動的に止まります。
7. 再起動が必要かどうかの判定
カーネルアップデートを含む場合は再起動が必要になります。rebootの要否を自動判定するhandlerを組み込んでおきましょう。
# roles/patch_apply/handlers/main.yml --- - name: check reboot required listen: "check reboot required" block: # RHEL系:needs-restartingコマンドで確認 - name: 再起動の要否を確認する(RHEL系) ansible.builtin.command: needs-restarting -r register: reboot_required_rhel changed_when: false failed_when: false when: ansible_facts['os_family'] == "RedHat" # Debian系:/var/run/reboot-requiredファイルで確認 - name: 再起動の要否を確認する(Debian系) ansible.builtin.stat: path: /var/run/reboot-required register: reboot_required_debian when: ansible_facts['os_family'] == "Debian" - name: 再起動が必要なサーバーを再起動する ansible.builtin.reboot: reboot_timeout: 600 post_reboot_delay: 30 when: > (ansible_facts['os_family'] == "RedHat" and reboot_required_rhel.rc == 1) or (ansible_facts['os_family'] == "Debian" and reboot_required_debian.stat.exists)
事前チェック・事後検証の組み込み(register/assert/notify)
パッチ適用の信頼性を上げるために、適用前の事前チェックと適用後の事後検証を必ずPlaybookに組み込みます。「パッチを当てたらWebサーバーが起動しなくなった」という事故を早期発見するためのしくみです。
8. 事前チェックの実装(ディスク容量・疎通確認)
# pre_checks.yml --- - name: ルートパーティションの空き容量を確認する ansible.builtin.shell: df -BM / | awk 'NR==2 {print $4}' | sed 's/M//' register: root_disk_free changed_when: false - name: ディスク空き容量が1GB以上あることを確認する ansible.builtin.assert: that: - root_disk_free.stdout | int >= 1024 fail_msg: "ディスク空き容量が不足しています: {{ root_disk_free.stdout }}MB。パッチ適用を中止します。" success_msg: "ディスク空き容量OK: {{ root_disk_free.stdout }}MB" - name: パッチ適用前のインストール済みパッケージ一覧を記録する(RHEL系) ansible.builtin.command: rpm -qa --queryformat "%{NAME}-%{VERSION}-%{RELEASE} " register: pre_package_list changed_when: false when: ansible_facts['os_family'] == "RedHat" - name: パッケージ一覧をコントロールノードに保存する ansible.builtin.copy: content: "{{ pre_package_list.stdout }}" dest: "/tmp/pre_packages_{{ inventory_hostname }}_{{ ansible_date_time.date }}.txt" delegate_to: localhost when: ansible_facts['os_family'] == "RedHat"
9. 事後検証の実装(サービス起動確認・ポート疎通確認)
パッチ適用後は、重要なサービスが正常に起動しているかどうかを自動で検証します。httpdやnginxが起動しているかどうかの確認と、実際に80番・443番ポートで応答しているかどうかの確認を組み合わせます。
# post_checks.yml --- - name: httpd(Apache)がアクティブ状態かを確認する ansible.builtin.systemd: name: httpd register: httpd_status when: "'webservers' in group_names" - name: httpdが正常に動作していることをアサートする ansible.builtin.assert: that: - httpd_status.status.ActiveState == "active" fail_msg: "httpdが起動していません。パッチ適用後の確認が必要です。" success_msg: "httpdの起動を確認しました" when: "'webservers' in group_names" - name: 80番ポートへの疎通を確認する ansible.builtin.wait_for: host: "{{ ansible_host }}" port: 80 timeout: 30 state: started delegate_to: localhost when: "'webservers' in group_names" - name: パッチ適用完了をログに記録する ansible.builtin.lineinfile: path: /var/log/ansible_patch.log line: "{{ ansible_date_time.iso8601 }} PATCH_APPLIED host={{ inventory_hostname }} user={{ ansible_user_id }}" create: yes
80番ポートや443番ポートが正常に応答しているかどうかのより詳細な確認方法については、Linux ポート確認の全コマンドも参考にしてください。
実際の検証サーバー(Rocky Linux 9.4)でこのPlaybookを実行した際の出力例は次のとおりです。
PLAY [Linuxサーバーへのパッチ自動適用] *********************************** TASK [ルートパーティションの空き容量を確認する] **************************** ok: [web01.example.internal] TASK [ディスク空き容量が1GB以上あることを確認する] ************************** ok: [web01.example.internal] => { "changed": false, "msg": "ディスク空き容量OK: 8542MB" } TASK [全パッケージを最新に更新する(RHEL/Rocky Linux)] ********************* changed: [web01.example.internal] TASK [httpdが正常に動作していることをアサートする] ************************** ok: [web01.example.internal] => { "changed": false, "msg": "httpdの起動を確認しました" } PLAY RECAP ************************************************************* web01.example.internal : ok=8 changed=3 unreachable=0 failed=0
Playbookの実行結果レポートと通知設計
パッチ適用の証跡管理と異常の即時検知のために、実行結果をファイルに記録してSlack等へ通知する仕組みを組み込みます。
10. ansible-playbookの実行ログを保存する
Playbookの実行をラップするシェルスクリプトを用意して、実行ログをファイルに残します。cron等のスケジューラーから呼び出す想定です。
#!/bin/bash # patch_apply_cron.sh - cronから呼び出すパッチ適用ラッパー LOG_DIR="/var/log/ansible" TIMESTAMP=$(date +"%Y%m%d_%H%M%S") LOG_FILE="${LOG_DIR}/patch_${TIMESTAMP}.log" PLAYBOOK="/opt/ansible/patch_apply.yml" INVENTORY="/opt/ansible/inventory/hosts.yml" mkdir -p "${LOG_DIR}" # ansible-playbookを実行してログを保存 ansible-playbook -i "${INVENTORY}" "${PLAYBOOK}" --become 2>&1 | tee "${LOG_FILE}" EXIT_CODE=${PIPESTATUS[0]} # 失敗した場合はログの末尾をメールで通知 if [ ${EXIT_CODE} -ne 0 ]; then tail -50 "${LOG_FILE}" | mail -s "[ALERT] Ansible patch failed: ${TIMESTAMP}" ops@example.com fi exit ${EXIT_CODE}
11. Slack通知を組み込む
AnsibleのPlaybook内からSlackへ通知を送ることもできます。community.general.slackモジュールを使うと、適用完了後に自動でSlackチャンネルへ通知を飛ばせます。
# Playbook末尾のpost_tasks内に追加 - name: パッチ適用完了をSlackへ通知する community.general.slack: token: "{{ slack_token }}" channel: "#ops-alerts" msg: | ✅ パッチ適用完了 対象: {{ inventory_hostname }} OS: {{ ansible_distribution }} {{ ansible_distribution_version }} 更新パッケージ数: {{ dnf_result.results | default(apt_result.stdout_lines | default([])) | length }} 実行日時: {{ ansible_date_time.iso8601 }} delegate_to: localhost when: slack_token is defined
Slack通知のトークン等の認証情報はAnsible Vaultで暗号化して管理します。Playbookに直接書くことは避けてください。
12. --check モードによるドライラン確認
本番適用の前に--checkオプションでドライランを実行することを強く推奨します。実際には変更を行わず、「何が変わるか」だけを確認できます。
# ドライラン(実際には変更しない) ansible-playbook -i inventory/hosts.yml patch_apply.yml --check --diff # 本番適用 ansible-playbook -i inventory/hosts.yml patch_apply.yml
よくあるトラブルとエラー対処
Ansibleでパッチ適用を自動化するとき、現場で頻繁に遭遇するエラーと対処法をまとめます。
13. dnf: command not foundになる(RHEL 7混在環境)
RHEL 7系のサーバーが混在している場合、ansible.builtin.dnfではなくansible.builtin.yumを使う必要があります。ansible_distribution_major_versionで分岐できます。
# RHEL 7とRHEL 8以降を分岐するパターン - name: yumでパッケージ更新(RHEL 7) ansible.builtin.yum: name: '*' state: latest when: - ansible_facts['os_family'] == "RedHat" - ansible_facts['distribution_major_version'] | int <= 7 - name: dnfでパッケージ更新(RHEL 8以降) ansible.builtin.dnf: name: '*' state: latest when: - ansible_facts['os_family'] == "RedHat" - ansible_facts['distribution_major_version'] | int >= 8
14. assertモジュールがfailになって処理が止まった
事前チェックのassertが失敗した場合は、意図通りに動作しています。注意が必要なのは、ディスク空き容量のチェック失敗をむやみにignore_errors: yesで無視しないことです。空き容量が不足した状態でパッチ適用を続けると、dnfが依存パッケージのキャッシュ展開に失敗してパッケージDBが破損する危険があります。
assertが失敗したサーバーが出た場合の対応手順:
・PLAY RECAPでfailed=1になっているホストを確認する
・該当サーバーにSSHしてdf -hでディスク使用状況を確認する
・dnf clean allまたは古いログファイルの削除でスペースを確保する
・確保できたら該当ホストのみを対象に-l web01.example.internalで再実行する
15. serial実行中に1台だけタイムアウトして止まった
再起動を含むパッチ適用では、reboot_timeoutのデフォルト値(600秒)を超えてしまうケースがあります。大量のパッケージ更新後の初回起動では起動シーケンスが長くなることがあるため、本番環境ではreboot_timeout: 900程度に延ばすことを検討してください。
# タイムアウトを延ばす場合 - name: 再起動する(タイムアウト延長版) ansible.builtin.reboot: reboot_timeout: 900 # 15分まで待機 post_reboot_delay: 60 # 起動後60秒待ってから次のタスクへ connect_timeout: 10 # 各接続試行のタイムアウト
【重要】post_reboot_delayを短くしすぎると、sshdは起動していてもhttpdなどのサービスが準備できていない状態でpost_tasksが走ってしまいます。最低30秒、できれば60秒以上確保することを推奨します。
本記事のまとめ
AnsibleによるLinuxサーバーへのパッチ適用自動化の要点を整理します。
| やりたいこと | Ansibleの機能・設定 |
|---|---|
| RHEL/Rocky Linuxを全更新する | ansible.builtin.dnf: name: '*' state: latest |
| Ubuntu/Debianを全更新する | ansible.builtin.apt: upgrade: dist |
| OS種別を自動判別して分岐する | when: ansible_facts['os_family'] == "RedHat" |
| 1台ずつローリング更新する | serial: 1(PlaybookのHosts宣言に追加) |
| 1台失敗したら全体を止める | max_fail_percentage: 0 |
| 適用前にディスク容量を確認する | assert:モジュールで閾値チェック |
| 適用後にサービス起動を確認する | wait_for: port: 80で疎通確認 |
| カーネル更新時に自動再起動する | ansible.builtin.reboot:モジュール |
手作業のパッチ適用はどうしても属人化し、規模が拡大するほど漏れが出やすくなります。Ansibleで自動化すると、誰が実行しても同じ手順・同じ品質でパッチを当てられるようになります。serialと事前・事後検証を組み合わせれば、本番環境でも安全にローリング更新を回せます。
まずは検証機1台に対してPlaybookを動かし、出力が期待通りかを確認してから本番のserialに組み込む、という進め方をお勧めします。
Ansible実践ハンズオンの詳細を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 次のページへ:Ansibleのdelegate_toとrun_onceの使い方|ロードバランサー操作を含むオーケストレーション設計
- 前のページへ:Ansibleのfactsとgather_facts制御|setupモジュールでホスト情報を収集して条件分岐に使う方法
- この記事の属するカテゴリ:Ansibleへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます