はじめに
Juniper & Oat は、2 つのカフェで同じ承認済みドリンク価格を使いながら、各店舗の名前、営業時間、限定メニューを保持する必要があります。1 つの Ansible テンプレートと playbook で実際の 2 つの SSH 管理対象を更新し、その後、意図的に起こしたメニューのドリフトを修復します。
基本的なターミナル操作と、小さな Ansible インベントリや playbook を読んだ経験が必要です。ホスト変数、JSON レンダリング、安全なプレビュー、管理対象とローカルコンテンツの違いは使用時に説明します。準備済みのツールとサイトにより、インストールではなく自動化に集中できます。
共通価格と店舗固有の情報を分ける
このステップでは、両カフェで共通するものと異なるまま保持するものを見分け、再利用可能なテンプレートの承認済みデータを宣言します。
Juniper & Oat には地域のカフェが 2 店舗あります。通常のドリンクと価格は共通ですが、営業時間、限定メニュー、お知らせは店舗ごとに異なります。これらのローカル情報を置き換えずに共通メニューを更新するのが仕事です。準備された web1 と web2 は、実習 VM のカーネルを共有する SSH 管理のコンテナーであり、独立した VM ではありません。Controller は Ansible を実行し、各対象は独自の実際のメニューサービスを実行します。
Menu Preview を開いて First branch を選び、メニュー、営業時間、限定メニューを読んでください。ブラウザーの戻るボタンで店舗一覧に戻り、Second branch を選択します。両店舗の Flat white は古い価格で、Park Café の営業時間も誤っています。各ページの限定メニューとお知らせは既に正しい状態です。Visit は同じ店舗の来店案内を開き、Download menu は現在の実際の CSV をダウンロードします。これらはページ内リンクであり、LabEx のタブではありません。
Controller タブに切り替えます。ターミナルはプロジェクトディレクトリから始まります。ディレクトリを間違えた場合も、この明示的なコマンドで戻れます。
cd /home/labex/project
インベントリの web1 と web2 は、それぞれ異なる準備済み SSH 対象を指します。ファイルを変更せずに読む cat で、インベントリと承認済みの店舗入力を確認します。
cat inventory.ini
cat host_vars/web1.yml host_vars/web2.yml
Harbor Café の営業時間は 07:00–16:00、Park Café は 08:00–18:00 です。host_vars ディレクトリは、名前が一致するインベントリホストに変数を渡します。これらの入力と準備された SSH 設定は変更しないでください。違いは意図的であり、取り除くべき誤りではありません。
承認済みの共通メニューはシナリオデータとして用意されています。変数を書く前に読んでください。
cat approved-menu.json
Espresso は 3.20、Flat white は 4.20、Tea は 3.00 と記載されています。価格は引用符付きの文字列として保存し、書式設定後も小数点以下 2 桁を保ちます。menu_items は、名前と価格を持つオブジェクトの YAML リストとなり、両ホストで共有されます。
準備された group_vars と templates ディレクトリは空です。group_vars/all.yml はすべてのインベントリホストに変数を渡します。次のヒアドキュメントはファイル全体を書き込みます。> は指定したそのファイルを置き換え、引用符で囲んだ区切り文字 YAML はシェルによる内容の展開を防ぎます。終端の区切り文字は行頭から単独で記述します。入力を間違えたらブロック全体を再実行してください。置き換わるのは自分の変数ファイルだけです。
cat > group_vars/all.yml <<'YAML'
menu_items:
- name: Espresso
price: "3.20"
- name: Flat white
price: "4.20"
- name: Tea
price: "3.00"
YAML
Jinja テンプレートは解決した値を管理対象ファイルに挿入します。二重の波括弧は式を評価し、縦棒はフィルターを適用します。ここでは to_nice_json がオブジェクト全体を、非 ASCII 文字も含めて有効で読みやすい JSON にシリアライズします。各値を手作業で引用符で囲む必要がなくなります。両店舗で使うテンプレートは同じです。branch_name と opening_hours は各ホストの入力から取得し、menu_items は共通です。
cat > templates/menu.json.j2 <<'JINJA'
{{ {'branch': branch_name, 'hours': opening_hours, 'items': menu_items} | to_nice_json }}
JINJA
Ansible を実行する前に、作成した両方のソースを読み、引用符の不足、インデントや区切り文字の誤りを見つけます。
cat group_vars/all.yml templates/menu.json.j2
サイトはまだ変わっていません。意図するデータを宣言しただけで、適用はしていません。次のステップで Ansible が各ホストの変数コンテキストを使ってレンダリングします。
両カフェに同じ Playbook を適用する
このステップでは、実際の 2 つの SSH 対象に同じ宣言をプレビューして配備し、実際のメニューとダウンロードを利用します。
Controller の /home/labex/project に留まります。play はホストを選び、tasks は管理対象の状態を宣言します。become: true は、ファイルを root 所有にできるよう、準備済み対象で許可された権限昇格を要求します。LabEx のログインアカウントは変わりません。file モジュールはディレクトリを宣言し、template モジュールは共通ソースをレンダリングして menu.json だけを置き換えます。所有者を明示し権限を引用符で囲むことで、ディレクトリの通過と公開メニューの読み取りを予測可能にします。ディレクトリは 0755、メニューは 0644 です。
ローカルファイル local-special.txt、local-notice.txt、非公開の staff-notes.txt はメニュー宣言の管理範囲外です。ディレクトリの内容を置き換えたり、これらのファイルを変更したりしないでください。バイト列、root 所有、元の権限を保持します。限定メニューとお知らせは 0644、非公開メモは 0600 です。サービスはリクエストごとに menu.json を読むため、データのみの更新に不要な再起動や handler を加える必要はありません。
完全な playbook を書きます。YAML のリスト要素は - を使い、スペースでモジュール引数を入れ子にします。src はコントローラー上のソース、dest は各管理対象上の場所を指します。
cat > menus.yml <<'YAML'
- name: Keep both branch menus current
hosts: all
gather_facts: false
become: true
tasks:
- name: Keep the menu directory available
ansible.builtin.file:
path: /srv/branch-menu
state: directory
owner: root
group: root
mode: "0755"
- name: Render the shared menu with each branch's inputs
ansible.builtin.template:
src: templates/menu.json.j2
dest: /srv/branch-menu/menu.json
owner: root
group: root
mode: "0644"
YAML
最初に --syntax-check で、適用せずに playbook の構文を検証します。
ansible-playbook menus.yml --syntax-check
YAML エラーが表示されたら、指定のファイルと行を読みます。上の完全なブロックとインデントを比べ、そのブロックを書き直して構文検証を繰り返してください。Ansible の構文エラーで VM をリセットする必要はありません。
--check は対応モジュールの変更を予測し、--diff は提案される変更前後の内容を示します。これらの秘密を含まないメニューファイルには便利ですが、プレビューは実際の業務テストではありません。両オプションを一緒に使います。
ansible-playbook menus.yml --check --diff
両ホストでメニュー変更が予測されるはずです。diff は店舗名の違いを保ちながら、価格と Park Café の営業時間を修正します。対象のファイルはまだ変わっていません。Menu Preview に切り替え、店舗を開くか更新して、古い値が残っていることを確認します。Controller に戻り、プレビュー用オプションなしで同じ宣言を適用します。
ansible-playbook menus.yml
各ホストの集計に、到達不能や失敗したタスクがないことを確認します。必要だった修正が異なるため、changed の正確な数はホスト間で異なる場合があります。ad hoc コマンドで実際の管理対象データを読みます。all はインベントリの両ホストを選び、-m はモジュール名、-a は引数を指定します。command モジュールはコントローラーではなく、各対象で cat を実行します。
ansible all -m ansible.builtin.command -a 'cat /srv/branch-menu/menu.json'
どちらにも承認済みの通常ドリンクと価格が含まれているはずです。Harbor Café と Park Café は、それぞれの名前と営業時間を保持する必要があります。
Menu Preview に切り替え、各店舗を更新します。3 つの価格、店舗名、営業時間、限定メニュー、お知らせを読んでください。Visit を開き、Menu で戻ります。Download menu を選んで CSV を開きます。店舗の承認済み 3 商品が含まれ、各行にその店舗名があることを確認します。トップページが正常に開くだけでは不十分です。

左側の共通価格と右側の限定メニューを比べます。Park Café も同じ承認済みドリンク一覧を使いつつ、独自の営業時間と限定メニューを保持します。
ブラウザーにメニューの欠落や無効なデータのエラーが出たら、Controller に戻り、コマンド出力を読んで JSON テンプレートをステップ 1 と比べてください。コントローラーのソースを修正し、playbook を再実行します。対象に到達できなければ、inventory.ini を読み元の出力と比較します。SSH の信頼確認を無効にしたり、プラットフォームのアカウントを変えたりしないでください。
Controller に戻ります。入力を変えずに繰り返した場合、保留中の変更はないはずです。
ansible-playbook menus.yml
集計の 2 行を探します。
web1 ... changed=0 ... unreachable=0 ... failed=0 ...
web2 ... changed=0 ... unreachable=0 ... failed=0 ...
これが冪等な設定です。望ましいファイルは既に一致しているため、Ansible は書き直しません。通常のリクエストやサービスログが発生しなくなるという意味ではありません。
ローカルコンテンツを消さずにドリフトを修復する
このステップでは、管理対象メニューの値を 1 つ意図的に壊し、変更していないコントローラーのソースで復元します。ローカルコンテンツは保持します。
Controller に留まります。設定ドリフトとは、実際の管理対象の状態が宣言した意図から外れることです。承認済みの営業時間が店舗ごとに異なることはドリフトではありません。共通価格を手動で変えることはドリフトです。
次の操作は web1 だけを対象にした意図的な障害です。-b はこの ad hoc タスクに become を要求します。copy の content は指定したメニュー JSON のみを置き換え、ディレクトリは置き換えません。構造は正しいまま、Espresso に未承認の価格 0.01 を設定します。この一時ファイルは故意に誤っています。価格を group_vars にコピーしないでください。
ansible web1 -b -m ansible.builtin.copy -a 'dest=/srv/branch-menu/menu.json content={"branch":"Harbor Café","hours":"07:00–16:00","items":[{"name":"Espresso","price":"0.01"},{"name":"Flat white","price":"4.20"},{"name":"Tea","price":"3.00"}]} owner=root group=root mode=0644'
Menu Preview に切り替え、First branch を更新して、誤った Espresso の価格を見ます。もう一方の店舗と両店舗の限定メニューには影響しないはずです。これは模擬の赤い表示ではなく、実際の対象ファイルの変更です。
Controller に戻ります。元のソースは編集しません。既存の望ましい状態をプレビューします。
ansible-playbook menus.yml --check --diff
変更を予測するのは web1 だけのはずです。Web2 は既に承認済みの状態です。同じ playbook を適用してメニューを復元します。
ansible-playbook menus.yml
Menu Preview に切り替えて両店舗を更新します。Espresso は再び 3.20 になるはずです。両店舗の限定メニュー、お知らせ、営業時間、ダウンロードも引き続き正しい必要があります。自動化が管理するのは 1 つの設定ファイルであり、ディレクトリ内のすべてのローカルファイルではありません。
Controller に戻り、最後にもう一度実行します。
ansible-playbook menus.yml
両ホストの集計は再び changed=0、unreachable=0、failed=0 を示すはずです。修復が成功するのは、対象を一度手で直したからではなく、コントローラーの再利用可能な意図と共通テンプレートがあるためです。将来、承認済みの共通商品が変わっても、同じテンプレートを使える必要があります。
まとめ
共通価格、店舗入力、ローカルコンテンツを区別し、2 つの SSH 対象に同じテンプレートをプレビューして適用しました。実際のページと CSV ダウンロードを利用し、正当な店舗差異を消さずに本当の設定ドリフトを修復しました。変更のない再実行によって、宣言したメニューが収束したことを確認しました。



