演習進捗 0 / 5 未完了
⏱ 約 90〜120 分 · 署名検証・カナリア・rollback
Part 3Chapter 06/サーバ運用

パッチを当てる・配る:閉じた2台へ安全に展開する

外へ直接出られない2台へ、承認した更新だけを署名検証付きで届けます。カナリア配布、障害の切り分け、rollback、安全な再展開までを一つの変更作業として通します。

教材は登録なしで読めます。実機でコマンドを試す場合のみ、Google ログインで2時間枠の予約が必要です。

所要時間約 90〜120 分
章タイプstandard
前提知識Part 3 Chapter 1〜5・2台へ ops で SSH 接続できること
使用機材workstation01 + learner01 + learner02(Proxmox 管理操作あり)
関連章前: Chapter 5「ディスクを拡張する」/ 次: 監視・異常検知
この章のゴール: 署名付き内部 APT リポジトリから承認済み更新をカナリア順に配布し、障害の証拠を採取して rollback したあと、安全な更新だけを再展開します。

「1台目は直った。2台目だけ止まった」

サーバが1台だけなら、更新後に動作を見て終わりにできるかもしれません。2台以上になると、それでは足りません。

この章では、2台を同時に更新しません。learner01 をカナリアとして先に更新し、証拠を確認したあとで learner02 へ進みます。さらに learner02 にだけ意図的な障害を入れ、rollback と安全な再展開まで行います。

この章を終えるとできること

概念説明

1. なぜサーバを直接インターネットへ出さないのか

更新のためだけに外向き経路を追加すると、その経路は受講者が root 権限を得たあとにも使えます。必要なのは「自由に外へ出ること」ではなく、「承認済みの更新を受け取ること」です。

本章では役割を分離します。

期待される出力
外部配布元
    │  管理ノードだけが収集
    ▼
管理ノード
  - 内容を検証
  - 配布版を承認
  - reprepro 5.3.1 で索引を生成
  - InRelease を署名
    │  専用データディスクで受け渡し
    ▼
workstation01 / 172.16.10.22
  - 内部リポジトリを静的配信
  - 外部 egress なし
    │  VLAN10
    ├──────── learner01 / 172.16.10.20
    └──────── learner02 / 172.16.10.21

learner の境界は最後まで変えません。成功のために default route や自由な DNS を足すのではなく、VLAN10 内の配信サーバだけを APT source として追加します。

2. 「キャッシュ」ではなく「承認済みリポジトリ」

内部リポジトリは、外部から要求された物をその場で取りに行く中継ではありません。管理者が事前に選び、検証し、署名した内容だけを配ります。

役割することしないこと
管理ノード収集、版の固定、索引生成、署名受講者へ公開しない
配信サーバ生成済みリポジトリの静的配信外部へ取りに行かない、秘密鍵を持たない
learner公開鍵導入、fingerprint 照合、署名検証、更新外部へ直接取りに行かない

「取得できた」だけでは、配ってよい更新か分かりません。承認と署名を取得処理から分けることで、毎回同じ版を再現できます。

3. APT が検証するのは .deb の個別署名ではない

この章で使う信頼連鎖は次の形です。

期待される出力
公開鍵で InRelease の署名を検証
        ↓
InRelease に書かれた SHA-256 と Packages を照合
        ↓
Packages に書かれた SHA-256 と取得した .deb を照合

署名の起点は InRelease です。そこからハッシュで索引とパッケージ本体を結びます。「すべての .deb が個別に署名されている」とは説明しません。

試作では、次の構造を実データで確認しました。ハッシュは読みやすさのため先頭8桁だけを示します。

期待される出力
InRelease(署名を検証)
└─ SHA256: ba320ab9…  dists/noble/main/binary-amd64/Packages
   └─ hello 2.10-3build1
      └─ SHA256: e68cf436…  pool/main/h/hello/hello_2.10-3build1_amd64.deb

4. 公開鍵を同じ HTTP 配信だけで信じてはいけない

リポジトリと公開鍵を同じ経路だけから取得し、そのまま信じると、その経路を置換した者が両方を差し替えられます。

そこで公開鍵ファイルは配信サーバから取得しても、fingerprint は別経路の教材 HTTPS ページに掲載された値と照合します。この照合が「何を信頼の起点にしたか」を明示します。

5. Signed-By は鍵の権限範囲を狭める

公開鍵は /etc/apt/keyrings/infra-study-archive-keyring.asc に置き、deb822 形式の .sources から Signed-By で参照します。

期待される出力
Types: deb
URIs: <実機で確定した内部リポジトリURI>
Suites: noble
Components: main broken
Architectures: amd64
Signed-By: /etc/apt/keyrings/infra-study-archive-keyring.asc

内部リポジトリでは、reprepro が同じパッケージの複数版を保持できず(Limit は未対応)、管理対象サービスの正常な v1 を main、意図的に壊した v2 を broken に置いています。承認済みの棚と演習でわざと壊すための棚を分ける設計であり、実務でもテスト用の版を本番の棚に混ぜないのは自然です。

この URI と Suite は試作時の値です。撮影時に実配信と一致することを確認してから確定します。鍵をシステム全体へ広く信頼させず、この source にだけ結び付けることが重要です。

6. updateupgrade は別の変更

索引を更新しただけでは、パッケージは変わりません。計画を見ずに更新を始めないことが、複数台運用の基本です。

7. カナリアと needrestart

最初の1台をカナリアにすると、全台へ同時に障害を広げずに済みます。needrestart は勝手に再起動するためではなく、更新後に古いライブラリを使い続けているサービスや再起動要否を判断する材料として使います。

8. スナップショットは判断を省略する道具ではない

rollback できることと、原因を説明できることは別です。戻す前に status、journal、変更した版を記録し、戻した後に「何が消え、何を再設定したか」を確かめます。

試してみよう:署名を確認し、閉じた2台へ段階配布する

演習環境

役割対象
カナリアlearner01 / 172.16.10.20
展開先learner02 / 172.16.10.21
内部リポジトリ配信workstation01 / 172.16.10.22
公開鍵 fingerprint の正本教材の HTTPS ページに掲載する実測値
sourcedeb822 /etc/apt/sources.list.d/infra-study.sources
keyring/etc/apt/keyrings/infra-study-archive-keyring.asc
所要時間実機で確定予定
期待される出力
      VMID NAME        STATUS    MEM(MB)   BOOTDISK(GB)
       103 learner01   running      2048          32.00
       105 learner02   running      2048          32.00
(他の VM は省略)

VMID_L1=103 VMID_L2=105 SNAP=ch6-before-update

表示例はマスクしています。 実機の VM 名には管理用のアドレスが含まれるため伏せました。 ここで確定させたいのは 3 つだけです ── 2 台の VMID(103 / 105)と、 これから取るスナップショットの名前ch6-before-update)。 章の最後にこの名前へ戻します。名前を決めずに作業を始めると、戻すときに「どれに戻すのか」で迷います。

ターミナルの使い分け

手順(9 ステップ)

STEP 01 · 2台の初期状態と「鍵・source 未 seed」を確認する

まず2台へ SSH 接続し、演習前の状態を確認します。

bash · コマンド
for host in learner01 learner02; do
  echo "=== $host ==="
  ssh ops@"$host" '
    hostname
    systemctl --failed --no-legend
    test ! -e /etc/apt/keyrings/infra-study-archive-keyring.asc
    echo "keyring absent=$?"
    test ! -e /etc/apt/sources.list.d/infra-study.sources
    echo "source absent=$?"
  '
done

終了コード 0 は「ファイルが無い」という確認に成功したことを表します。

期待される出力
● systemd-networkd-wait-online.service loaded failed failed Wait for Network to be Configured
key=0
source=0

● NetworkManager-wait-online.service   loaded failed failed Network Manager Wait Online
● systemd-networkd-wait-online.service loaded failed failed Wait for Network to be Configured
key=0
source=0

key=0source=0 が 2 台とも出れば、公開鍵も .sources もまだ無い状態です。 これがこの章の出発点です。 受講者が自分で作るところに意味があるので、環境には置いてありません。

一緒に failed のサービスが見えていますが、これは壊れているわけではありませんwait-online は「ネットワークが完全に整うまで待つ」サービスで、 この演習環境の learner はインターネットへ出る経路を持たないため、待ち続けて時間切れになります。 章の作業とは無関係なので、そのまま進めて構いません。

既存 source と、対象ファイルがまだ無いことも記録します。

bash · コマンド
grep -RhsE '^[[:space:]]*(deb |URIs:)' \
  /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null || true
ls -l /etc/apt/keyrings/
test ! -e /etc/apt/sources.list.d/infra-study.sources
test ! -e /etc/apt/keyrings/infra-study-archive-keyring.asc
期待される出力
deb http://archive.ubuntu.com/ubuntu noble main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu noble-updates main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu noble-security main restricted universe multiverse
URIs: http://jp.archive.ubuntu.com/ubuntu/
URIs: http://security.ubuntu.com/ubuntu/
URIs: http://archive.ubuntu.com/ubuntu/
URIs: http://security.ubuntu.com/ubuntu/
source-grep-exit=0

合計 0

最後の 合計 0/etc/apt/keyrings/ が空だという意味です。まだ何の鍵も置かれていません。

上の 3 行が上流の指定です。この章では、このあとこれを一時的に無効にして、 自分たちのリポジトリだけを見る状態にします。消すのではなく、退避します。 章の最後に戻すためです。

baseline のパッケージ一覧も保存します。

bash · コマンド
dpkg-query -W | sort > ~/ch6-baseline-packages.tsv
sha256sum ~/ch6-baseline-packages.tsv
期待される出力
learner01:
a28bb8666e0073f2d1cab8cef485ad17103016b2f8e5ca56b519d9c554bc45e1  /home/ops/ch6-baseline-packages.tsv

learner02:
a28bb8666e0073f2d1cab8cef485ad17103016b2f8e5ca56b519d9c554bc45e1  /home/ops/ch6-baseline-packages.tsv

2 台のハッシュが 1 文字も違いません。 つまり両者は完全に同じパッケージ構成から始まります。 これは偶然ではなく、同じイメージから作られているからです。

ここを押さえておく理由は、あとで 1 台目だけを更新して 2 台を比べるからです。 出発点が同じだと確認できていなければ、あとで差が出たときに 「更新のせいなのか、もともと違ったのか」が言えません。

STEP 02 · 変更前スナップショットを2台とも取得する

APT や dpkg が動作中でないことを確認します。

bash · コマンド
for host in learner01 learner02; do
  ssh ops@"$host" 'pgrep -a apt || true; pgrep -a dpkg || true'
done
期待される出力
learner01:
pgrep-apt-exit=1
pgrep-dpkg-exit=1

learner02:
pgrep-apt-exit=1
pgrep-dpkg-exit=1

pgrep見つからなかったときに 1 を返します。 つまり 1 が出てほしい状態です。 0 が返るなら apt か dpkg が動いている最中なので、終わるまで待ちます。 更新の途中でスナップショットを取ると、戻したときに中途半端な状態が復元されます。

Proxmox ホストで VMID を確認し、同じ名前の snapshot を作ります。

bash · コマンド
VMID_L1='<実機で確認した learner01 の VMID>'
VMID_L2='<実機で確認した learner02 の VMID>'
SNAP='ch6-before-update'

qm snapshot "$VMID_L1" "$SNAP"
qm snapshot "$VMID_L2" "$SNAP"
qm listsnapshot "$VMID_L1"
qm listsnapshot "$VMID_L2"
期待される出力
snapshotting 'drive-scsi0' (local-lvm:vm-103-disk-0)
  Logical volume "snap_vm-103-disk-0_ch6-before-update" created.
snapshotting 'drive-scsi0' (local-lvm:vm-105-disk-0)
  Logical volume "snap_vm-105-disk-0_ch6-before-update" created.

(スナップショット一覧・末尾のみ)
  `-> ch6-before-update   2026-08-07 18:54:03   no-description
   `-> current                                  You are here!

You are here!いまの状態、その 1 つ上が たったいま取った戻り先です。 2 台とも同じ名前で取れていることを確認します。片方だけ取れていると、章の最後で片方だけ戻せません。

表示例はマスクしています。 実機の一覧には演習と無関係な VM や過去のスナップショットも並びます。

STEP 03 · 直取り失敗の原因を層ごとに切り分ける

「APT が失敗した」だけで、原因を egress 遮断と断定してはいけません。名前解決、経路、外部接続の順に確認します。

bash · コマンド
getent ahostsv4 archive.ubuntu.com || true
ip route
timeout 5 bash -c 'exec 3<>/dev/tcp/1.1.1.1/80' || true
sudo timeout 45 apt-get -o Acquire::Retries=0 update
echo "apt-get exit=$?"

実測では、APT の画面に最初に現れた直接原因は名前解決失敗でした。一方、IP 直打ちの外部接続も成功しませんでした。画面に出たエラー、経路、境界の検査を分けて記録します。

期待される出力
dns-exit=2

default via 172.16.10.1 dev ens20.10 proto static
172.16.10.0/24 dev ens20.10 proto kernel scope link src 172.16.10.20
172.16.20.0/24 via 172.16.10.1 dev ens20.10 proto static
192.0.2.0/24 dev ens18 proto kernel scope link src 192.0.2.203 metric 100

direct-ip-exit=124

エラー:1 http://jp.archive.ubuntu.com/ubuntu noble InRelease
  'jp.archive.ubuntu.com' が一時的に解決できません
エラー:3 http://archive.ubuntu.com/ubuntu noble InRelease
  'archive.ubuntu.com' が一時的に解決できません
(同種のエラーが計 7 件)
W: いくつかのインデックスファイルのダウンロードに失敗しました。これらは無視されるか、古いものが代わりに使われます。

direct-apt-get-exit=0

管理用のアドレスはマスクしています192.0.2.x は説明用の帯です)。

3 つの数字を順に見ます。

  • dns-exit=2名前が引けていませんarchive.ubuntu.com がどこにあるか分からない状態です。
  • direct-ip-exit=124124時間切れを表します。名前を使わず IP を直接叩いても届きません。

名前の問題ではなく、経路として出られないことがここで分かります。

  • direct-apt-get-exit=0 … 🔴 ここが引っかかりどころです。

すべての取得に失敗しているのに、apt-get update の終了コードは 0(成功) です。 apt-get update は索引が取れなくても、それを致命的な失敗とは扱わず 「古いものを使う」と言って正常終了します。最後の W: がその宣言です。

つまり終了コードだけを見ていると「更新できた」と誤解します。 外に出られたかどうかを判断したいときは、終了コードではなく取得結果そのものを見ます。

STEP 04 · 公開鍵を検証し、署名付き内部リポジトリを設定する

まず配信サーバから公開鍵を取得します。取得元と、信頼する根拠は別です。

bash · コマンド
REPO_URI='<実機で確定した内部リポジトリURI>'
curl -fsS "$REPO_URI/infra-study-archive-keyring.asc" \
  -o /tmp/infra-study-archive-keyring.asc

gpg --show-keys --with-fingerprint --keyid-format long \
  /tmp/infra-study-archive-keyring.asc

表示された fingerprint を、教材 HTTPS ページに掲載された値と目視で照合します。値は省略せず、空白を除いた全桁が一致することを確認します。

bash · コマンド
EXPECTED_FINGERPRINT='<教材HTTPSページに掲載した実測fingerprint>'
ACTUAL_FINGERPRINT="$(gpg --show-keys --with-colons \
  /tmp/infra-study-archive-keyring.asc | awk -F: '$1=="fpr" {print $10; exit}')"
test "$ACTUAL_FINGERPRINT" = "$EXPECTED_FINGERPRINT"
echo "fingerprint match=$?"

一致した鍵だけを keyring へ置きます。

bash · コマンド
sudo install -d -m 0755 /etc/apt/keyrings
sudo install -o root -g root -m 0644 \
  /tmp/infra-study-archive-keyring.asc \
  /etc/apt/keyrings/infra-study-archive-keyring.asc

次に deb822 形式の source を作ります。

bash · コマンド
sudo tee /etc/apt/sources.list.d/infra-study.sources > /dev/null << EOF
Types: deb
URIs: $REPO_URI
Suites: noble
Components: main broken
Architectures: amd64
Signed-By: /etc/apt/keyrings/infra-study-archive-keyring.asc
EOF

sudo chmod 0644 /etc/apt/sources.list.d/infra-study.sources
cat /etc/apt/sources.list.d/infra-study.sources
期待される出力
pub   ed25519/297FAD1317CA1DC4 2026-08-05 [SC] [有効期限: 2027-08-05]
      フィンガープリント = 089E 07BD FFA2 6DB7 C720  3016 297F AD13 17CA 1DC4
uid                            infra-study lab archive <archive@lab.invalid>

(教材ページに載っている値)
089E 07BD FFA2 6DB7 C720 3016 297F AD13 17CA 1DC4

gpg: ディレクトリ'/home/ops/.gnupg'が作成されました
gpg: keybox'/home/ops/.gnupg/pubring.kbx'が作成されました

URIs: http://172.16.10.22/ubuntu
Suites: noble
Components: main broken
Architectures: amd64
Signed-By: /etc/apt/keyrings/infra-study-archive-keyring.asc

644 /etc/apt/keyrings/infra-study-archive-keyring.asc
644 /etc/apt/sources.list.d/infra-study.sources

鍵と一緒に配られた指紋を信じてはいけません。 鍵をすり替えた相手なら、指紋も一緒にすり替えられます。 だから照合の相手は、鍵を配ってきたのとは別の経路でなければ意味がありません。 ここでは HTTPS の教材ページに載っている値と、40 桁すべてを突き合わせます。 先頭や末尾だけを見るのでは足りません。

Signed-By: は「この鍵で署名されたものだけを、このリポジトリとして受け入れる」という指定です。 これを書かないと、その鍵はシステム全体のどのリポジトリに対しても有効になってしまいます。 鍵の効き先を 1 つに絞るための行だと考えてください。

Components: main brokenbroken は、このあとわざと壊れた版を配るために用意した区画です。 通常のリポジトリには出てきません。

小 STEP:もう届かない上流の source を無効化する

内部リポジトリを追加する前から、受講者機には焼込み時点で取得した上流の索引が残っています。届かない上流の source を残したままにすると、その古い索引に載った取得できない版まで更新候補に出ます。apt の候補は「索引に載っているもの」であって、「今も取得できるもの」とは限りません。使えない経路を消すことは、更新経路を管理するうえでの基本です。

まず、無効化する前の候補数を記録します。この時点の候補には、届かない上流の索引に載ったままの版が含まれます。内部リポジトリから実際に取得できるのはその一部だけです。

bash · コマンド
apt-get -s upgrade | awk '/^[0-9]+ upgraded/ {print}'

ここでは ubuntu.sourcesubuntu.sources.disabled へ退避します。ファイル内に複数の設定があっても上流 source をまとめて無効化でき、元の設定も復元可能な形で残せるからです。

bash · コマンド
sudo mv /etc/apt/sources.list.d/ubuntu.sources \
  /etc/apt/sources.list.d/ubuntu.sources.disabled
ls -l /etc/apt/sources.list.d/ubuntu.sources*

索引を取得します。

bash · コマンド
time sudo apt-get -o Acquire::Retries=0 update
echo "apt-get exit=$?"

出力では、内部 URI から InRelease を取得し、署名警告なしで終了コード 0 になることを確認します。

索引を内部リポジトリだけの状態へ更新したら、同じシミュレーションをもう一度実行します。無効化の前後で候補数が変わり、取得可能な範囲と更新計画が揃ったことを確認してください。この数の変化そのものが、残っていた索引と現在使える経路を区別する手掛かりです。

bash · コマンド
apt-get -s upgrade | awk '/^[0-9]+ upgraded/ {print}'
期待される出力
-rw-r--r-- 1 root root 386  2月  3  2026 /etc/apt/sources.list.d/ubuntu.sources.disabled

(誤った鍵に差し替えてから update した場合)
取得:1 http://172.16.10.22/ubuntu noble InRelease [1,961 B]
エラー:1 http://172.16.10.22/ubuntu noble InRelease
  公開鍵を利用できないため、以下の署名は検証できませんでした: NO_PUBKEY 297FAD1317CA1DC4
E: リポジトリ http://172.16.10.22/ubuntu noble InRelease は署名されていません。
wrong-key-exit=100

(正しい鍵に戻してから update した場合)
取得:1 http://172.16.10.22/ubuntu noble InRelease [1,961 B]
internal-apt-get-exit=0

ここがこの章でいちばん大事な対照です。

誤った鍵では E: で止まり、終了コードは 100 です。 索引そのものは取得できています取得:1 … [1,961 B])。 落ちているのは取得ではなく 検証です。「届いたが、信用できないので使わない」という判断が働いています。

正しい鍵に戻すと同じ操作が 0 で終わります。

🔴 片方だけを見ても意味がありません。 「正しい鍵で成功した」だけなら、 検証を一切していない場合でも同じ結果になります。 誤った鍵で失敗することまで確かめて、初めて「署名を検証している」と言えます。

なお上流のエラーは無視して構いません。ubuntu.sources.disabled へ退避したので、 上流はもう見に行きません(残っているエラーは別ファイルに書かれた古い指定によるものです)。

needrestart が無ければ内部リポジトリから導入します。

bash · コマンド
command -v needrestart || sudo apt-get install needrestart
needrestart --version
期待される出力
learner01:
/usr/sbin/needrestart
needrestart-preinstalled=yes
needrestart 3.6 - Restart daemons after library updates.

learner02:
/usr/sbin/needrestart
needrestart-preinstalled=yes
needrestart 3.6 - Restart daemons after library updates.

2 台とも最初から入っていましたpreinstalled=yes)。導入は不要です。

needrestart は「更新したライブラリを、まだ古いまま抱えたまま動いているサービス」を教えてくれます。 パッケージを新しくしても、動き続けているプロセスはメモリ上の古いコードを使ったままです。 更新したのに直っていない、という食い違いはここで起きます。

配信サーバではアクセスログを確認します。実装方式と実ログ位置は撮影前に確定します。

bash · コマンド
sudo tail -n 100 '<実機で確定した配信アクセスログ>'
期待される出力
172.16.10.20 - - [07/Aug/2026:18:54:38] "GET /ubuntu/dists/noble/InRelease HTTP/1.1" 200 1961 "-" "Debian APT-HTTP/1.3"
172.16.10.20 - - [07/Aug/2026:18:54:38] "GET /ubuntu/dists/noble/main/binary-amd64/Packages.gz HTTP/1.1" 200 4302
172.16.10.20 - - [07/Aug/2026:18:54:38] "GET /ubuntu/dists/noble/broken/binary-amd64/Packages.gz HTTP/1.1" 200 469
172.16.10.21 - - [07/Aug/2026:18:54:45] "GET /ubuntu/dists/noble/InRelease HTTP/1.1" 200 1961 "-" "Debian APT-HTTP/1.3"
172.16.10.21 - - [07/Aug/2026:18:54:47] "GET /ubuntu/dists/noble/main/binary-amd64/Packages.gz HTTP/1.1" 200 4302
172.16.10.21 - - [07/Aug/2026:18:54:47] "GET /ubuntu/dists/noble/broken/binary-amd64/Packages.gz HTTP/1.1" 200 469

配る側に誰が何を取りに来たかが残ります。ここでは 2 台(.20.21)が それぞれ索引を取りに来たことが読めます。

更新を配る仕組みを自分で持つ意味の一つがこれです。 上流から各自が勝手に取ってくる形だと、誰がどこまで更新したのかを配る側から確認できません。

Debian APT-HTTP/1.3apt が名乗っている名前です。 手で curl を叩いたときとは違う名前が出るので、どの操作による要求かを区別できます。

期待される出力
172.16.10.20 [07/Aug/2026:18:54:38 /ubuntu/dists/noble/InRelease 200 1961
172.16.10.20 [07/Aug/2026:18:54:38 /ubuntu/dists/noble/main/binary-amd64/Packages.gz 200 4302
172.16.10.20 [07/Aug/2026:18:54:38 /ubuntu/dists/noble/broken/binary-amd64/Packages.gz 200 469
172.16.10.21 [07/Aug/2026:18:54:45 /ubuntu/dists/noble/InRelease 200 1961
172.16.10.21 [07/Aug/2026:18:54:47 /ubuntu/dists/noble/InRelease 416 206
172.16.10.21 [07/Aug/2026:18:54:47 /ubuntu/dists/noble/main/binary-amd64/Packages.gz 200 4302

取りに来る順番に意味があります。 どの行も InRelease が先で、Packages.gz が後です。 索引に書かれたハッシュを先に手に入れてから中身を取りに行く、という順序が守られています。 逆にはなりません。先に中身を受け取ってしまうと、それが正しいかを判断する材料が無いからです。

416 は「その範囲は持っていない」という応答で、apt続きから取ろうとして断られただけです。 失敗ではありません。 直後に改めて全体を取り直しています。

対照実験:誤った鍵なら本当に拒否されるか

成功画面だけでは、署名検証が働いた証拠になりません。正しい keyring を退避し、別の鍵ファイルへ一時的に置き換えます。

bash · コマンド
sudo cp /etc/apt/keyrings/infra-study-archive-keyring.asc \
  /etc/apt/keyrings/infra-study-archive-keyring.asc.good

# 撮影用に用意した「異なる公開鍵」を同じパスへ置く
sudo install -o root -g root -m 0644 \
  /tmp/other-public-key.asc \
  /etc/apt/keyrings/infra-study-archive-keyring.asc

sudo rm -rf /var/lib/apt/lists/*
sudo apt-get update
echo "wrong-key exit=$?"

NO_PUBKEY を含む署名エラーで更新が拒否されることを確認します。確認後は正しい鍵を戻し、再び成功することを確かめます。

bash · コマンド
sudo mv /etc/apt/keyrings/infra-study-archive-keyring.asc.good \
  /etc/apt/keyrings/infra-study-archive-keyring.asc
sudo apt-get update
echo "restored-key exit=$?"

STEP 05 · 更新計画を2台で比較し、作業票を確定する

承認と、その版を実際に入手できることは別です。実機でも、承認した版のひとつが上流から消えていて、管理ノードでの収集時に取得できませんでした。収集の時点で失敗が見つかれば、配布前に承認一覧とリポジトリの内容を突き合わせられます。この不一致を配布前に止められるのは、変更管理が正しく働いている状態です。

bash · コマンド
apt-get -s upgrade | tee ~/ch6-upgrade-simulation.txt
awk '/^Inst / {print $2}' ~/ch6-upgrade-simulation.txt \
  | sort -u > ~/ch6-packages.txt
cat ~/ch6-packages.txt
期待される出力
注意: これはシミュレーションにすぎません!
      apt-get は実際の実行に root 権限を必要とします。

以下のパッケージは保留されます:
  nftables
以下のパッケージはアップグレードされます:
  base-files coreutils iproute2 motd-news-config sosreport thermald
アップグレード: 6 個、新規インストール: 0 個、削除: 0 個、保留: 1 個。

Inst motd-news-config [13ubuntu10.3] (13ubuntu10.4 infra-study lab archive:172.16.10.22 [all])
(以下同様に 6 件)

CH6_CANDIDATE_COUNT=6

-s実際には何もしないという指定です。まず「何が起きるか」だけを見ます。

見るところは 2 つあります。

  • どこから来るのかInst の行の括弧の中が入手元です。infra-study lab archive と出ていれば、

自分たちのリポジトリから取る計画になっています。上流の名前が混ざっていたら、 まだ上流を見に行く設定が残っています。

  • 保留された nftables。「アップグレードされます」ではなく「保留されます」に入っています。

これは依存関係の都合で、単独では上げられないという意味です。 無理に上げようとせず、まず上げられるものだけを上げるのがこの章の進め方です。

2台の計画を workstation01 へ集めて比較します。

bash · コマンド
mkdir -p ~/ch6-evidence
scp ops@learner01:ch6-upgrade-simulation.txt ~/ch6-evidence/learner01-plan.txt
scp ops@learner02:ch6-upgrade-simulation.txt ~/ch6-evidence/learner02-plan.txt
scp ops@learner01:ch6-packages.txt ~/ch6-evidence/learner01-packages.txt
scp ops@learner02:ch6-packages.txt ~/ch6-evidence/learner02-packages.txt
diff -u ~/ch6-evidence/learner01-plan.txt ~/ch6-evidence/learner02-plan.txt
期待される出力
(差分なし。diff は何も出力せず、終了コード 0 で終わった)

何も出ないのが正解です。 diff は違いがあったときだけ中身を出します。 2 台の更新計画が 1 文字も違わない、という意味です。

ここで差が出たら、その場で止めます。同じイメージから作った 2 台の計画が食い違うなら、 前提のどこかがすでに崩れています。 そのまま 1 台目を更新しても、 2 台目で同じ結果になる保証がありません。

差分がある場合は、理由を説明できるまで進みません。承認した一覧を CH6_PACKAGES として使います。

STEP 06 · learner01 をカナリアとして先行更新する

更新前に、演習用の正常な service v1 を導入し、基準となる状態を作ります。

bash · コマンド
sudo apt-get install ch6-demo-service=1.0-1
systemctl is-active ch6-demo.service
systemctl show ch6-demo.service -p MainPID -p FragmentPath
期待される出力
以下のパッケージが新たにインストールされます:
  ch6-demo-service
アップグレード: 0 個、新規インストール: 1 個、削除: 0 個、保留: 7 個。
取得:1 http://172.16.10.22/ubuntu noble/main amd64 ch6-demo-service all 1.0-1 [1,152 B]

   Loaded: loaded (/usr/lib/systemd/system/ch6-demo.service; enabled; preset: enabled)
   Active: active (running) since Fri 2026-08-07 18:55:02 JST; 872ms ago
 Main PID: 3806 (ch6-demo)
   CGroup: /system.slice/ch6-demo.service
           ├─3806 /bin/sh /usr/bin/ch6-demo
           └─3808 sleep 30

MainPID=3806
FragmentPath=/usr/lib/systemd/system/ch6-demo.service

1.0-1 が入り、active (running) になりました。これがこのあと壊す対象です。

FragmentPath は、そのサービスの定義ファイルが実際にどこから読まれているかを示します。 あとで壊れた版に入れ替えたとき、どのファイルが差し替わったのかをここで確かめます。

Main PID も控えておきます。更新でサービスが本当に入れ替わったかは、 「動いている」ことではなく PID が変わったかで判断できます。

【T2: learner01】

カナリアには、⑤で確定したパッケージ名だけを適用します。一覧が空でないことを確認してから、パッケージ名を引数に渡します。途中の確認を読み、計画外の追加・削除が出たら中止します。

bash · コマンド
CH6_PACKAGES="$(tr '\n' ' ' < ~/ch6-packages.txt)"
test -n "$CH6_PACKAGES"
time sudo env NEEDRESTART_MODE=l \
  apt-get install --only-upgrade $CH6_PACKAGES
echo "apt-get exit=$?"
期待される出力
以下のパッケージはアップグレードされます:
  base-files coreutils iproute2 motd-news-config sosreport thermald
アップグレード: 6 個、新規インストール: 0 個、削除: 0 個、保留: 2 個。
3,204 kB のアーカイブを取得する必要があります。

取得:1 http://172.16.10.22/ubuntu noble/main amd64 motd-news-config all 13ubuntu10.4 [4,012 B]
取得:2 http://172.16.10.22/ubuntu noble/main amd64 base-files amd64 13ubuntu10.4 [73.3 kB]
(以下 6 件)

No containers need to be restarted.
No user sessions are running outdated binaries.
apt-get-exit=0

real	0m18.153s
user	0m6.502s
sys	0m3.862s

1 台目だけを更新しました。 2 台目はまだ触っていません。

取得元がすべて http://172.16.10.22/ubuntu になっています。 外へ一度も出ずに 3,204 kB を受け取れたということです。

real 0m18.153sこの環境での実測です。同じ内容を上流から取れば、 回線と相手のサーバー次第でこれより長くなります。 同じ更新を 2 台目にも当てるとき、この数字が見積もりの根拠になります。

--only-upgrade を付けているので、すでに入っているものを新しくするだけです。 新しいパッケージは入りません。保留の 2 個には手を付けていません。

更新後の再起動要否を list only で確認します。

bash · コマンド
sudo needrestart -r l
test -e /var/run/reboot-required && cat /var/run/reboot-required || true
test -e /var/run/reboot-required.pkgs && cat /var/run/reboot-required.pkgs || true
期待される出力
Running kernel seems to be up-to-date.

Services to be restarted:
 systemctl restart ch6-demo.service

No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host.

needrestart-exit=0
reboot-required=absent
reboot-required.pkgs=absent

🔴 ここが更新作業でいちばん見落とされるところです。

apt は成功しました。ファイルはすべて新しくなっています。 それでも Services to be restarted:ch6-demo.service が挙がっています。 動き続けているプロセスは、メモリ上の古いコードをそのまま使っているからです。

「更新したのに直っていない」という食い違いの多くはこれです。 パッケージを新しくすることと、動いているものが新しくなることは別の出来事です。

Running kernel seems to be up-to-date.reboot-required=absent は、 再起動は要らないという判断です。カーネルが更新された場合はここが変わります。

なお debconf: フロントエンドの初期化に失敗しました という行が出ますが、 これは失敗ではありません。 画面から対話できない環境なので、 対話しない方式へ自動的に切り替えた、という報告です。

健全性を確認します。

bash · コマンド
systemctl is-active ch6-demo.service
systemctl --failed --no-legend
sudo journalctl -p err --since "-15 min" --no-pager
exit
ssh -o BatchMode=yes -o ConnectTimeout=10 ops@learner01 true
期待される出力
active

 8月 07 18:53:05 learner01 systemd-networkd-wait-online[655]: Timeout occurred while waiting for network connection
 8月 07 18:53:05 learner01 systemd[1]: Failed to start systemd-networkd-wait-online.service - Wait for Network

active が出れば、1 台目は更新後も動いています。ここで初めて 2 台目へ進む判断ができます。

エラーログに出ているのは、章の最初でも見た wait-online の時間切れです。 時刻を見てください。18:53:05 は更新を始める前で、起動したときのものです。 更新が原因ではありません。

ログにエラーがある=更新が失敗した、ではありません。 「いつからあるものか」を確かめずに慌てると、問題のない更新を巻き戻すことになります。

needrestart が再起動候補を示しても、この場で一括自動再起動はしません。サービス影響と回復経路を作業票へ追記し、カナリア合格条件を満たしたものだけ次へ進めます。

STEP 07 · 承認済みの同じ更新を learner02 へ逐次配布する

カナリア合格後にだけ2台目へ進みます。大規模な自動化ではなく、対象を表示しながら逐次実行します。

カナリアの健全性が確認できた時刻を記録してから、2台目へ進みます。

bash · コマンド
date -u
CH6_PACKAGES="$(tr '\n' ' ' < ~/ch6-packages.txt)"
sudo env NEEDRESTART_MODE=l apt-get install --only-upgrade $CH6_PACKAGES
echo "apt-get exit=$?"
期待される出力
2026年  8月  7日 金曜日 09:55:28 UTC

以下のパッケージはアップグレードされます:
  base-files coreutils iproute2 motd-news-config sosreport thermald
アップグレード: 6 個、新規インストール: 0 個、削除: 0 個、保留: 2 個。
3,204 kB のアーカイブを取得する必要があります。

1 台目と同じ 6 個、同じ容量です。 事前に取った計画どおりに進んでいます。

1 台目で 18 秒だったので、2 台目もおおよそ同じだけかかると見積もれます。 カナリアを 1 台置く価値はここにもあります。 「うまくいくか」だけでなく、 「どれくらいかかるか」も先に分かります。

bash · コマンド
sudo needrestart -r l
systemctl is-active ch6-demo.service
systemctl --failed --no-legend
sudo journalctl -p err --since "-15 min" --no-pager
期待される出力
Running kernel seems to be up-to-date.

No services need to be restarted.

2 台目は No services need to be restarted. です。1 台目とは違います。

違うのはこのとき 2 台目にはまだデモ用サービスが入っていないからです。 再起動が必要なサービスが無いので、何も挙がりません。

同じ更新でも、動いているものが違えば、後始末は変わります。 「1 台目でこう出たから 2 台目も同じはず」とは限りません。

2台を同じ観点で並べて確認します。

bash · コマンド
for host in learner01 learner02; do
  ssh ops@"$host" '
    dpkg-query -W
    systemctl is-active ch6-demo.service
    apt-get -s upgrade
  '
done
期待される出力
learner01:
  base-files       13ubuntu10.4
  coreutils        9.4-3ubuntu6.2
  ch6-demo-service 1.0-1
  ch6-demo.service … active

learner02:
  base-files       13ubuntu10.4
  coreutils        9.4-3ubuntu6.2
  ch6-demo-service (未導入)

(残更新候補の確認)
以下のパッケージはアップグレードされます:
  ch6-demo-service
アップグレード: 1 個、新規インストール: 0 個、削除: 0 個、保留: 1 個。
Inst ch6-demo-service [1.0-1] (2.0-1 infra-study lab archive:172.16.10.22 [all])

更新したパッケージのバージョンが 2 台で一致しました。 ここが揃っていれば、 「片方だけ古い」という状態は残っていません。

一方で ch6-demo-service にはまだ新しい版 2.0-1 が残っています[1.0-1] (2.0-1 …) は「いま 1.0-1 で、2.0-1 に上げられる」という意味です。

この 2.0-1 が、次にわざと壊す版です。 入手元が broken 区画であることも確認できます。

配信ログから、1台目と2台目が同じ承認済みファイルを取得したことを確認します。

期待される出力
[07/Aug/2026:18:54:38 172.16.10.20 /ubuntu/dists/noble/InRelease 1961
[07/Aug/2026:18:54:38 172.16.10.20 /ubuntu/dists/noble/main/binary-amd64/Packages.gz 4302
[07/Aug/2026:18:55:07 172.16.10.20 /ubuntu/pool/main/b/base-files/base-files_13ubuntu10.4_amd64.deb 73300
[07/Aug/2026:18:54:45 172.16.10.21 /ubuntu/dists/noble/InRelease 1961
[07/Aug/2026:18:55:30 172.16.10.21 /ubuntu/pool/main/b/base-files/base-files_13ubuntu10.4_amd64.deb 73300

1 台目(.20)が先に取り、2 台目(.21)が後から取っています。 カナリアから順次展開へ進んだことが、配る側のログだけで読み取れます。

受講者の手元を見なくても、どこまで配られたかを配る側から確認できるというのが、 自分でリポジトリを持つことの実務上の利点です。 上流から各自が勝手に取る形では、この確認ができません。

STEP 08 · 壊れた service 更新を learner02 にだけ配る

OS 更新が2台で完了したあと、管理対象サービスの「次版」を模した ch6-demo-service v2 を learner02 にだけ配ります。壊れる理由は、ExecStart が存在しないファイルを指すことです。実在パッケージの不具合を装いません。

bash · コマンド
sudo apt-get install ch6-demo-service=2.0-1
sudo systemctl daemon-reload
sudo systemctl restart ch6-demo.service
echo "restart exit=$?"
systemctl --failed --no-legend
sudo systemctl status ch6-demo.service --no-pager
sudo journalctl -u ch6-demo.service -n 30 --no-pager
期待される出力
以下のパッケージはアップグレードされます:
  ch6-demo-service
取得:1 http://172.16.10.22/ubuntu noble/broken amd64 ch6-demo-service all 2.0-1 [1,156 B]

restart-exit=0

● ch6-demo.service                     loaded failed failed Ch6 demo service

   Active: failed (Result: exit-code) since Fri 2026-08-07 18:55:54 JST; 19ms ago
  Process: 10394 ExecStart=/usr/bin/ch6-demo-missing (code=exited, status=203/EXEC)
 Main PID: 10394 (code=exited, status=203/EXEC)

 8月 07 18:55:54 learner02 systemd[1]: ch6-demo.service: Main process exited, code=exited, status=203/EXEC
 8月 07 18:55:54 learner02 systemd[1]: ch6-demo.service: Failed with result 'exit-code'.

🔴 restart-exit=0 に注目してください。再起動コマンドは成功しています。 それなのにサービスは failed です。

このサービスは Type=simple で、systemd はプロセスを起動できた時点で「起動成功」と見なします。 その直後にプロセスが死んでも、systemctl restart の終了コードには現れません

status=203/EXEC は「実行しようとしたファイルが無い」という意味です。 壊れた版の定義は /usr/bin/ch6-demo-missing を指していて、そんなファイルはありません。

ここから引き出す教訓は 1 つです。 「コマンドが成功したか」と「起きてほしいことが起きたか」は別物です。 更新の成否を終了コードだけで判断すると、この障害は素通りします。 確かめるべきは結果の状態systemctl is-active)です。

この STEP の完了条件は「失敗画面を見る」ことではありません。次の事実を言葉で説明できることです。

  • learner01 は同じ OS 更新後も正常
  • learner02 も OS 更新直後までは正常だった
  • learner02 が壊れたのは service 更新を追加した直後
  • 2台へ配った OS 更新は同じで、差分は ch6-demo-service の v2 だけである
  • journal が unit の ExecStart を原因として示している
  • したがって、現時点の第一仮説は OS パッチ全体ではなく service 更新

ここまで説明できたら rollback へ進みます。

STEP 09 · スナップショットへ戻し、安全な更新だけを再展開する

【T3: Proxmox ホスト】

障害が起きた learner02 だけを変更前へ戻します。まず正常停止を試し、応答しない場合だけ強制停止へ切り替えます。

bash · コマンド
date -u
qm shutdown "$VMID_L2"
qm status "$VMID_L2"
# 正常停止しない場合だけ: qm stop "$VMID_L2"
qm rollback "$VMID_L2" "$SNAP"
qm start "$VMID_L2"
qm status "$VMID_L2"
date -u

正常停止しない場合だけ qm stop を使い、その理由を記録します。

期待される出力
Fri Aug  7 09:56:00 AM UTC 2026
status: stopped
  Logical volume "vm-105-disk-0" successfully removed.
  Logical volume "vm-105-disk-0" created.
  Logical volume pve/vm-105-disk-0 changed.
status: running
Fri Aug  7 09:56:11 AM UTC 2026

rollback 自体は 11 秒で終わりました。 スナップショットに戻すという操作は、ディスクを作り直すだけなので速いです。

ただし status: running は「電源が入った」という意味でしかありません。 中の OS が使える状態になったかどうかは、まだ何も言っていません。 ここを取り違えると、次の手順で必ず失敗します。

【T1: workstation01】

SSH が戻るまで待ち、スナップショット時点へ戻ったことを確認します。

rollback 後は、演習で入れた鍵・source・service が消えたことを確認します。

bash · コマンド
timeout 120 bash -c 'until (echo > /dev/tcp/learner02/22) 2>/dev/null; do sleep 3; done'
ssh ops@learner02 '
  test ! -e /etc/apt/keyrings/infra-study-archive-keyring.asc
  test ! -e /etc/apt/sources.list.d/infra-study.sources
  test ! -e /etc/systemd/system/ch6-demo.service
  dpkg-query -W | sort > /tmp/ch6-after-rollback.tsv
  diff ~/ch6-baseline-packages.tsv /tmp/ch6-after-rollback.tsv
'
期待される出力
(SSH が応答するまで待機)
待機開始  09:56:11 UTC
応答      09:58:23 UTC   = 2 分 12 秒

公開鍵           … 無し
infra-study.sources … 無し
ch6-demo.service … 無し

🔴 戻したあと、SSH で入れるようになるまで 2 分 12 秒かかりました。 status: running になってからさらにこれだけ待つ、ということです。 章の中でいちばん長い待ち時間がここです。

3 つの「無し」は、戻した状態が章の開始時点と同じだという確認です。 自分で作った鍵も .sources も、壊れたサービスも消えています。

スナップショットへ戻すというのは、やったことを選んで取り消す操作ではありません。 その時点の状態へ丸ごと戻るので、途中で作った正しいものも一緒に消えます。 だからこのあと、必要なものを作り直すところからやり直します。

ここで選択肢を整理します。

選択採用条件本章での判断
同じ一式をそのまま再試行一時的な通信障害など、成果物自体が原因でない採用しない
OS 更新と service 更新を全部保留原因を分離できない、影響が広い原因不明なら採用
安全な OS 更新だけ再展開service 更新が原因と証明でき、OS 更新後のカナリアが正常本章ではこれを採用
別版を待つ壊れた service 更新の修正版が必要service 更新に対して採用

learner01 のカナリアが正常で、learner02 も service 更新前までは正常でした。したがって、壊れた service 更新を隔離し、承認済み OS 更新だけを learner02 へ再展開します。

【T1: workstation01】

スナップショットで消えた公開鍵と .sources の設定、および承認済みパッケージ一覧を再配置します。壊れた service v2 は送りません。

STEP④と同じ順序で、公開鍵の取得、HTTPS 掲載 fingerprint との照合、keyring 配置、deb822 source 作成、届かない上流 source の無効化をやり直します。snapshot で戻したのだから、信頼設定も戻っていることが重要です。

bash · コマンド
REPO_URI='<実機で確定した内部リポジトリURI>'
curl -fsS "$REPO_URI/infra-study-archive-keyring.asc" \
  -o /tmp/infra-study-archive-keyring.asc

EXPECTED_FINGERPRINT='<教材HTTPSページに掲載した実測fingerprint>'
ACTUAL_FINGERPRINT="$(gpg --show-keys --with-colons \
  /tmp/infra-study-archive-keyring.asc | awk -F: '$1=="fpr" {print $10; exit}')"
test "$ACTUAL_FINGERPRINT" = "$EXPECTED_FINGERPRINT"

sudo install -d -m 0755 /etc/apt/keyrings
sudo install -o root -g root -m 0644 \
  /tmp/infra-study-archive-keyring.asc \
  /etc/apt/keyrings/infra-study-archive-keyring.asc

sudo tee /etc/apt/sources.list.d/infra-study.sources > /dev/null << EOF
Types: deb
URIs: $REPO_URI
Suites: noble
Components: main broken
Architectures: amd64
Signed-By: /etc/apt/keyrings/infra-study-archive-keyring.asc
EOF

sudo mv /etc/apt/sources.list.d/ubuntu.sources \
  /etc/apt/sources.list.d/ubuntu.sources.disabled

承認済み一覧を戻して OS 更新だけを再展開します。壊れた service v2 は指定しません。

bash · コマンド
scp ~/ch6-evidence/learner02-packages.txt ops@learner02:ch6-packages.txt
ssh ops@learner02
sudo apt-get -o Acquire::Retries=0 update
CH6_PACKAGES="$(tr '\n' ' ' < ~/ch6-packages.txt)"
sudo env NEEDRESTART_MODE=l apt-get install --only-upgrade $CH6_PACKAGES
echo "apt-get exit=$?"
期待される出力
gpg: ディレクトリ'/home/ops/.gnupg'が作成されました
gpg: keybox'/home/ops/.gnupg/pubring.kbx'が作成されました
gpg: /home/ops/.gnupg/trustdb.gpg: 信用データベースができました

取得:1 http://172.16.10.22/ubuntu noble InRelease [1,961 B]

gpg のディレクトリがまた作られています。章の前半でも同じ行を見ました。 戻したので、あのときの作業ごと消えているためです。

面倒に見えますが、ここは飛ばせません。 鍵を作り直すたびに、別経路の指紋と照合し直します。 「前に確認したから今回もいいだろう」で省くと、 戻したあとに違う鍵を掴んでも気づけません。

【T2: learner02】

bash · コマンド
sudo needrestart -r l
systemctl --failed --no-legend
test ! -e /etc/systemd/system/ch6-demo.service
exit
ssh -o BatchMode=yes -o ConnectTimeout=10 ops@learner02 true
期待される出力
Running kernel seems to be up-to-date.

No services need to be restarted.

壊れた 2.0-1 は入れ直していません。 入れ直したのは、 1 台目で問題が出なかった 6 個だけです。

ここが復旧の勘所です。戻したあとに「元どおり全部入れ直す」と、 壊れたものまで一緒に戻ってきます。 何が原因だったかを切り分けたうえで、安全だと分かっているぶんだけを再度当てます。

これで「戻せた」だけでなく、戻したあとに変更を分割して再判断し、安全な部分だけ再展開するところまで完了しました。

詰まったときの対処

よくある症状と切り分け

症状最初に見る物判断
公開鍵を取得できないip route get 172.16.10.22curl -v、配信ログVLAN10 経路、配信 bind、URI を分けて確認する
fingerprint が一致しないgpg --show-keys --with-colons、教材 HTTPS の掲載値一致するまで鍵を導入しない。空白や短縮 key ID で比較しない
NO_PUBKEYkeyring の有無・権限、Signed-By の絶対パス鍵が無い、違う、または source が別パスを参照している
InRelease が 404URIsSuites、配信サーバの dists/ネットワーク成功とリポジトリ構造の不一致を分ける
ハッシュ不一致InReleasePackages、対象ファイル、配信内容更新途中や改竄を疑い、検証を無効化しない
候補版が見えないapt-cache policyapt-get update の終了コードindex が更新されているか、Suite/Component/Architecture を確認する
service が failedsystemctl statusjournalctl -u、導入版OS 更新と演習用 service 更新を分けて比較する

段階ヒント

ヒント 1 段(方向だけ)
  1. 配信サーバへ届くか
  2. 取得した鍵が正しいか
  3. source がその鍵を参照しているか
  4. InRelease の署名検証を通ったか
  5. 候補版と実際の導入版が一致するか
ヒント 2 段(確認コマンド)
bash · コマンド
ip route get 172.16.10.22
REPO_URI='<実機で確定した内部リポジトリURI>'
curl -v "$REPO_URI/dists/noble/InRelease" -o /tmp/InRelease
gpg --show-keys --with-fingerprint /etc/apt/keyrings/infra-study-archive-keyring.asc
grep -E '^(URIs|Suites|Components|Architectures|Signed-By):' \
  /etc/apt/sources.list.d/infra-study.sources
apt-cache policy
sudo apt-get update

apt-get update の再試行だけを繰り返さず、どの層まで通っているかを記録してください。

答えを見る(章全体の判断フロー)
期待される出力
配信サーバへの VLAN10 到達を確認
        ↓
公開鍵を取得
        ↓
別経路の fingerprint と全桁照合
        ↓
keyring へ配置
        ↓
deb822 source を Signed-By 付きで作成
        ↓
apt-get update で InRelease を検証
        ↓
誤鍵の対照実験で拒否を確認し、正しい鍵へ復元
        ↓
simulation で更新計画を固定
        ↓
カナリア → 健全性確認 → 2台目
        ↓
意図した障害 → 証拠採取 → rollback
        ↓
鍵とsourceを再設定 → 承認済みOS更新だけを再展開

覚えるべき中心はコマンド列ではなく、境界を開けない・変更を記録する・1台ずつ進める・戻した後に再判断するの4点です。

AI に相談

AIAPT・署名検証・段階配布の詰まりを質問できます
この章の内部 APT リポジトリ・署名検証・カナリア配布を前提に答えます

AI には「直して」だけでなく、次を渡します。

「検証を無効化する設定」は解決ではありません。原因を鍵、source、リポジトリ構造、配信経路のどこかへ絞り込みます。

修了確認

問 1(出口を開けずに更新する)

learner に default route や自由な DNS を追加せず、どうやって更新を届けますか。

答え

管理ノードが外部から収集し、承認した版だけで署名付き内部リポジトリを生成します。生成物はネットワークを使わず配信サーバへ渡し、learner は VLAN10 の配信サーバだけを source にします。配信サーバも外部 egress を持ちません。

つまり、外へ出る扉を learner にも配信サーバにも作らず、扉を持つ機械(管理ノード)を受講者から遠ざけることで成立させます。

問 2(needrestart を見たあとの判断)

needrestart に再起動候補が出たとき、すぐ全台を再起動せず何を確認しますか。

答え

一括再起動はしません。少なくとも次を確認します。

  • そのサービスが停止すると、どの利用者・機能へ影響するか
  • 冗長構成があるか、片系ずつ再起動できるか
  • SSH・ネットワークなど回復経路に関わるサービスではないか
  • カーネル再起動が必要か、保守時間と承認があるか
  • 再起動後の健全性確認コマンドと rollback 条件が定義されているか

needrestart -r l は判断材料を列挙するために使い、自動再起動の許可そのものにはしません。

問 3(rollback 後の再展開)

rollback 後に鍵と source をもう一度設定し、壊れた service v2 を再配布しないのはなぜですか。

答え

時系列とカナリア結果から、第一原因は service 更新だと判断できます。公開鍵と source は clean 状態へ seed しておらず、演習中の変更なので、信頼設定から再現します。

  1. 壊れた service 更新を隔離し、同じ成果物は再配布しない
  2. learner01 の OS 更新後の健全性と、learner02 が service 更新前まで正常だった記録を確認する
  3. 公開鍵と .sources を再設定し、承認済みの OS 更新だけを learner02 へ再展開する
  4. needrestart・SSH・failed unit を再確認する
  5. service 更新は修正版ができるまで保留する

原因を分離できない場合は、安全な部分だけと決めつけず、変更一式を保留して追加調査します。

問 4(同じ更新を2台へ配った証拠)

2台へ同じ承認済み更新を順番に配ったことを、何で証明しますか。

答え

2台の source・hold・baseline・一覧更新時刻が違えば、同じコマンドでも候補パッケージや版が変わります。したがって「同じ apt-get upgrade を実行した」ことは証拠になりません。次を残します。

  • 適用前に2台の apt-get -s upgrade を保存し、パッケージ名・旧版・候補版を diff する
  • 承認済みの ch6-packages.txt に列挙した名前だけを --only-upgrade で指定する
  • 適用後の版と残更新候補を横並びで確認する
  • 開始時刻、終了コード、配信アクセスログを横並びで残す
  • InRelease の署名検証、公開鍵 fingerprint の一致、誤鍵が拒否された対照実験を残す

これらを組み合わせることで、同じ取得物を信頼連鎖付きで配ったことを説明できます。

章末クリーンアップ ── 2台を演習前の clean 状態へ戻す

本章では OS パッケージ、APT の一覧、公開鍵と .sources の設定、管理対象サービスを変更しました。個別に逆操作して「だいたい元通り」にするのではなく、最初に取った snapshot へ2台とも戻してディスク状態を揃えます。その後、snapshot より前に作った baseline ファイルと、workstation01 上の演習物を削除します。

1. 2台を変更前スナップショットへ rollback する

【T3: Proxmox ホスト】

VMID_L1VMID_L2SNAP が正しいことを表示してから実行します。

bash · コマンド
date -u
printf 'SNAP=%s learner01=%s learner02=%s\n' "$SNAP" "$VMID_L1" "$VMID_L2"
qm listsnapshot "$VMID_L1"
qm listsnapshot "$VMID_L2"

for id in "$VMID_L1" "$VMID_L2"; do
  qm shutdown "$id"
  qm status "$id"
  # 正常停止しない場合だけ: qm stop "$id"
  qm rollback "$id" "$SNAP"
  qm start "$id"
  qm status "$id"
done
date -u
期待される出力
Fri Aug  7 09:59:01 AM UTC 2026

  Logical volume "vm-103-disk-0" successfully removed.
  Logical volume "vm-103-disk-0" created.
status: running

  Logical volume "vm-105-disk-0" successfully removed.
  Logical volume "vm-105-disk-0" created.
status: running

Fri Aug  7 09:59:21 AM UTC 2026

2 台まとめて戻して 20 秒です。1 台ずつでも同じで、ディスクを作り直すだけなので速く終わります。

ここで戻す先は、章の最初に取った ch6-before-update です。 章の中で作ったものは、正しいものも含めてすべて消えます。

2. clean 状態を確認し、baseline ファイルを消す

【T1: workstation01】

bash · コマンド
for host in learner01 learner02; do
  timeout 120 bash -c "until (echo > /dev/tcp/$host/22) 2>/dev/null; do sleep 3; done"
  echo "===== cleanup verify: $host ====="
  ssh ops@"$host" '
    test ! -e /etc/apt/keyrings/infra-study-archive-keyring.asc
    test ! -e /etc/apt/sources.list.d/infra-study.sources
    test ! -e /etc/systemd/system/ch6-demo.service
    systemctl --failed --no-legend
    rm -f ~/ch6-baseline-packages.tsv ~/ch6-packages.txt ~/ch6-upgrade-simulation.txt
  '
done
期待される出力
(2 台の SSH が応答するまで待機)
待機開始  09:59:22 UTC
応答      10:01:33 UTC   = 2 分 11 秒

learner01: 公開鍵 無し / infra-study.sources 無し / ch6-demo.service 無し
learner02: 公開鍵 無し / infra-study.sources 無し / ch6-demo.service 無し

また 2 分 11 秒待ちました。 先ほどの 1 台のときとほぼ同じです。 戻す台数を増やしても、待ち時間はあまり変わりません(同時に起動するため)。

章の作業で作ったものが 2 台とも消えていることを確認します。 ここまで確認して、環境を次の人へ引き渡せる状態になりました。

3. 演習スナップショットを削除する

【T3: Proxmox ホスト】

clean 復帰を確認してから、本章で作ったスナップショットだけを削除します。

bash · コマンド
qm delsnapshot "$VMID_L1" "$SNAP"
qm delsnapshot "$VMID_L2" "$SNAP"
qm listsnapshot "$VMID_L1"
qm listsnapshot "$VMID_L2"
期待される出力
  Logical volume "snap_vm-103-disk-0_ch6-before-update" successfully removed.
  Logical volume "snap_vm-105-disk-0_ch6-before-update" successfully removed.

(スナップショット一覧・末尾のみ)
  `-> pre-ch6-capture-20260806   2026-08-06 18:27:44   Ch6 撮影前 baseline
   `-> current                                         You are here!

章のために取ったスナップショットを消します。 戻し終わったので、もう出番はありません。

消さずに残すと、ディスクを使い続けます。スナップショットは 「取った時点との差分」を保持し続けるため、放置するほど容量を食います。

一覧から ch6-before-update が消えたことを確認します。 他のスナップショットには手を付けません。 章と無関係なものを消すと、戻せなくなります。

4. workstation01 の一時ファイルを削除する

【T1: workstation01】

削除対象を表示し、本章専用ディレクトリであることを確認してから削除します。

bash · コマンド
find ~/ch6-evidence -maxdepth 2 -type f -print
rm -rf ~/ch6-evidence
test ! -e ~/ch6-evidence
期待される出力
~/ch6-evidence/learner01-packages.txt
~/ch6-evidence/learner01-plan.txt
~/ch6-evidence/learner02-packages.txt
~/ch6-evidence/learner02-plan.txt

(削除後、ディレクトリは残らない)

消す前に、何があるかを一度出してから消します。 rm -rf を先に打つと、消えたものを後から確認できません。

ここで消しているのは、章の途中で作った比較用の控えです。 更新計画とパッケージ一覧を 2 台ぶん取っておいたもので、 章が終われば役目は終わりです。次の受講者の環境に残しません。

ゴール確認

対象clean 状態
learner01 / learner02 の公開鍵keyring なし
learner01 / learner02 の内部 sourceinfra-study.sources なし
learner01 / learner02 の演習用 serviceなし
learner01 / learner02 の OS 更新baseline と一致
workstation01 の内部リポジトリ維持(章の恒久前提)
workstation01 の外部 egressなし
管理ノードの秘密鍵管理ノードだけに存在

配信側の seed は章末でも消しません。 workstation01 の内部リポジトリ本体と配信設定は、CTO 承認6条件を通して clean へ焼き込む共通基盤です。一方、learner 側の公開鍵と .sources は受講者が STEP で設定する演習物なので、seed せず snapshot rollback で撤去します。この非対称が設計どおりの clean 状態です。

次の章へ

この章では、更新を「ダウンロードする作業」ではなく、信頼の起点、承認、段階配布、観測、復旧を含む変更管理として扱いました。次の章では、この一連の変更を継続的に監視し、異常を早く見つける方法へ進みます。

比較コラム:なぜキャッシュ方式ではなく署名付き内部リポジトリなのか

当初は、配信サーバが外部から取得しながら受講者へ渡す構成を検討していました。しかし実機で確認すると、配信サーバへ安全な取得経路を追加すること自体が受講者の外向き経路になり得ました。外部配布元のアドレス変動へ追随する運用負債も残ります。

そこで「防御を積み増す」のではなく、「その防御が必要になる外向き経路を作らない」構成へ変更しました。最初から正解を引いたのではありません。実機で前提を確かめ、成立しない前提と新しい穴が見つかったため、方式そのものを撤回しました。

もっと知りたい方へ

なぜ InRelease だけの署名から全体を守れるのか

InReleasePackages のハッシュを持ち、Packages は各 .deb のファイル名、サイズ、ハッシュを持ちます。署名済みの起点からハッシュが連鎖するため、途中の索引やパッケージが変われば照合に失敗します。

実務で残すべき証拠