パッチを当てる・配る:閉じた2台へ安全に展開する
外へ直接出られない2台へ、承認した更新だけを署名検証付きで届けます。カナリア配布、障害の切り分け、rollback、安全な再展開までを一つの変更作業として通します。
教材は登録なしで読めます。実機でコマンドを試す場合のみ、Google ログインで2時間枠の予約が必要です。
| 所要時間 | 約 90〜120 分 |
|---|---|
| 章タイプ | standard |
| 前提知識 | Part 3 Chapter 1〜5・2台へ ops で SSH 接続できること |
| 使用機材 | workstation01 + learner01 + learner02(Proxmox 管理操作あり) |
| 関連章 | 前: Chapter 5「ディスクを拡張する」/ 次: 監視・異常検知 |
「1台目は直った。2台目だけ止まった」
サーバが1台だけなら、更新後に動作を見て終わりにできるかもしれません。2台以上になると、それでは足りません。
- 何を配るかを先に固定したか
- 署名を検証したか
- 1台目で問題がないことを確認してから2台目へ進んだか
- 2台目だけ壊れたとき、更新と設定変更を分けて考えられるか
- 戻したあと、承認済みの変更だけを再現できるか
この章では、2台を同時に更新しません。learner01 をカナリアとして先に更新し、証拠を確認したあとで learner02 へ進みます。さらに learner02 にだけ意図的な障害を入れ、rollback と安全な再展開まで行います。
この章を終えるとできること
- 外部へ直接出られないサーバへ、承認済み更新を内部から配布できる
- 公開鍵の入手経路と fingerprint 照合が、なぜ必要か説明できる
InReleaseから.debまでのハッシュ連鎖を説明できるSigned-Byを使って、特定のリポジトリだけに鍵の信頼範囲を限定できる- 署名検証が失敗する対照実験から、成功時に何が検証されていたか判断できる
- カナリア更新、健全性確認、逐次展開を実行できる
- 障害後に 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. update と upgrade は別の変更
apt-get update: リポジトリの索引を取得し、署名とハッシュ連鎖を検証するapt-get -s upgrade: 実際には変更せず、更新計画を作るapt-get install --only-upgrade ...: 承認済みの対象だけを更新する
索引を更新しただけでは、パッケージは変わりません。計画を見ずに更新を始めないことが、複数台運用の基本です。
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 ページに掲載する実測値 |
| source | deb822 /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)。 章の最後にこの名前へ戻します。名前を決めずに作業を始めると、戻すときに「どれに戻すのか」で迷います。
ターミナルの使い分け
- learner の操作は
workstation01からops@learner01/ops@learner02へ接続して行う。 - VM の snapshot / rollback は Proxmox ホストで行う。
- 配信サーバのログ確認は
workstation01で行う。 - 管理ノードの秘密鍵や生成作業は受講者演習に含めない。
手順(9 ステップ)
STEP 01 · 2台の初期状態と「鍵・source 未 seed」を確認する
まず2台へ SSH 接続し、演習前の状態を確認します。
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=0とsource=0が 2 台とも出れば、公開鍵も.sourcesもまだ無い状態です。 これがこの章の出発点です。 受講者が自分で作るところに意味があるので、環境には置いてありません。一緒に
failedのサービスが見えていますが、これは壊れているわけではありません。wait-onlineは「ネットワークが完全に整うまで待つ」サービスで、 この演習環境の learner はインターネットへ出る経路を持たないため、待ち続けて時間切れになります。 章の作業とは無関係なので、そのまま進めて構いません。
既存 source と、対象ファイルがまだ無いことも記録します。
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 のパッケージ一覧も保存します。
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 が動作中でないことを確認します。
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 を作ります。
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 遮断と断定してはいけません。名前解決、経路、外部接続の順に確認します。
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=124…124は 時間切れを表します。名前を使わず IP を直接叩いても届きません。名前の問題ではなく、経路として出られないことがここで分かります。
direct-apt-get-exit=0… 🔴 ここが引っかかりどころです。すべての取得に失敗しているのに、
apt-get updateの終了コードは0(成功) です。apt-get updateは索引が取れなくても、それを致命的な失敗とは扱わず 「古いものを使う」と言って正常終了します。最後のW:がその宣言です。つまり終了コードだけを見ていると「更新できた」と誤解します。 外に出られたかどうかを判断したいときは、終了コードではなく取得結果そのものを見ます。
STEP 04 · 公開鍵を検証し、署名付き内部リポジトリを設定する
まず配信サーバから公開鍵を取得します。取得元と、信頼する根拠は別です。
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 ページに掲載された値と目視で照合します。値は省略せず、空白を除いた全桁が一致することを確認します。
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 へ置きます。
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 を作ります。
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 brokenのbrokenは、このあとわざと壊れた版を配るために用意した区画です。 通常のリポジトリには出てきません。
小 STEP:もう届かない上流の source を無効化する
内部リポジトリを追加する前から、受講者機には焼込み時点で取得した上流の索引が残っています。届かない上流の source を残したままにすると、その古い索引に載った取得できない版まで更新候補に出ます。apt の候補は「索引に載っているもの」であって、「今も取得できるもの」とは限りません。使えない経路を消すことは、更新経路を管理するうえでの基本です。
まず、無効化する前の候補数を記録します。この時点の候補には、届かない上流の索引に載ったままの版が含まれます。内部リポジトリから実際に取得できるのはその一部だけです。
apt-get -s upgrade | awk '/^[0-9]+ upgraded/ {print}'
ここでは ubuntu.sources を ubuntu.sources.disabled へ退避します。ファイル内に複数の設定があっても上流 source をまとめて無効化でき、元の設定も復元可能な形で残せるからです。
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*
索引を取得します。
time sudo apt-get -o Acquire::Retries=0 update echo "apt-get exit=$?"
出力では、内部 URI から InRelease を取得し、署名警告なしで終了コード 0 になることを確認します。
索引を内部リポジトリだけの状態へ更新したら、同じシミュレーションをもう一度実行します。無効化の前後で候補数が変わり、取得可能な範囲と更新計画が揃ったことを確認してください。この数の変化そのものが、残っていた索引と現在使える経路を区別する手掛かりです。
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 が無ければ内部リポジトリから導入します。
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は「更新したライブラリを、まだ古いまま抱えたまま動いているサービス」を教えてくれます。 パッケージを新しくしても、動き続けているプロセスはメモリ上の古いコードを使ったままです。 更新したのに直っていない、という食い違いはここで起きます。
配信サーバではアクセスログを確認します。実装方式と実ログ位置は撮影前に確定します。
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.3はaptが名乗っている名前です。 手で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 を退避し、別の鍵ファイルへ一時的に置き換えます。
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 を含む署名エラーで更新が拒否されることを確認します。確認後は正しい鍵を戻し、再び成功することを確かめます。
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台で比較し、作業票を確定する
承認と、その版を実際に入手できることは別です。実機でも、承認した版のひとつが上流から消えていて、管理ノードでの収集時に取得できませんでした。収集の時点で失敗が見つかれば、配布前に承認一覧とリポジトリの内容を突き合わせられます。この不一致を配布前に止められるのは、変更管理が正しく働いている状態です。
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 へ集めて比較します。
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 を導入し、基準となる状態を作ります。
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】
カナリアには、⑤で確定したパッケージ名だけを適用します。一覧が空でないことを確認してから、パッケージ名を引数に渡します。途中の確認を読み、計画外の追加・削除が出たら中止します。
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 で確認します。
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: フロントエンドの初期化に失敗しましたという行が出ますが、 これは失敗ではありません。 画面から対話できない環境なので、 対話しない方式へ自動的に切り替えた、という報告です。
健全性を確認します。
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台目へ進みます。
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 台置く価値はここにもあります。 「うまくいくか」だけでなく、 「どれくらいかかるか」も先に分かります。
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台を同じ観点で並べて確認します。
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 が存在しないファイルを指すことです。実在パッケージの不具合を装いません。
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 だけを変更前へ戻します。まず正常停止を試し、応答しない場合だけ強制停止へ切り替えます。
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 が消えたことを確認します。
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 で戻したのだから、信頼設定も戻っていることが重要です。
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 は指定しません。
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】
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.22、curl -v、配信ログ | VLAN10 経路、配信 bind、URI を分けて確認する |
| fingerprint が一致しない | gpg --show-keys --with-colons、教材 HTTPS の掲載値 | 一致するまで鍵を導入しない。空白や短縮 key ID で比較しない |
NO_PUBKEY | keyring の有無・権限、Signed-By の絶対パス | 鍵が無い、違う、または source が別パスを参照している |
InRelease が 404 | URIs、Suites、配信サーバの dists/ | ネットワーク成功とリポジトリ構造の不一致を分ける |
| ハッシュ不一致 | InRelease、Packages、対象ファイル、配信内容 | 更新途中や改竄を疑い、検証を無効化しない |
| 候補版が見えない | apt-cache policy、apt-get update の終了コード | index が更新されているか、Suite/Component/Architecture を確認する |
| service が failed | systemctl status、journalctl -u、導入版 | OS 更新と演習用 service 更新を分けて比較する |
段階ヒント
ヒント 1 段(方向だけ)
- 配信サーバへ届くか
- 取得した鍵が正しいか
- source がその鍵を参照しているか
InReleaseの署名検証を通ったか- 候補版と実際の導入版が一致するか
ヒント 2 段(確認コマンド)
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 に相談
AI には「直して」だけでなく、次を渡します。
- 実行したコマンド
- 終了コード
apt-get updateの署名エラー全文- keyring のパスと
Signed-Byの値 - fingerprint は秘密情報ではないが、教材掲載値との一致・不一致だけでもよい
systemctl statusと該当 unit の journal
「検証を無効化する設定」は解決ではありません。原因を鍵、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 しておらず、演習中の変更なので、信頼設定から再現します。
- 壊れた service 更新を隔離し、同じ成果物は再配布しない
learner01の OS 更新後の健全性と、learner02が service 更新前まで正常だった記録を確認する- 公開鍵と
.sourcesを再設定し、承認済みの OS 更新だけをlearner02へ再展開する needrestart・SSH・failed unit を再確認する- 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_L1・VMID_L2・SNAP が正しいことを表示してから実行します。
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】
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 復帰を確認してから、本章で作ったスナップショットだけを削除します。
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】
削除対象を表示し、本章専用ディレクトリであることを確認してから削除します。
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 の内部 source | infra-study.sources なし |
| learner01 / learner02 の演習用 service | なし |
| learner01 / learner02 の OS 更新 | baseline と一致 |
workstation01 の内部リポジトリ | 維持(章の恒久前提) |
workstation01 の外部 egress | なし |
| 管理ノードの秘密鍵 | 管理ノードだけに存在 |
配信側の seed は章末でも消しません。
workstation01の内部リポジトリ本体と配信設定は、CTO 承認6条件を通して clean へ焼き込む共通基盤です。一方、learner 側の公開鍵と.sourcesは受講者が STEP で設定する演習物なので、seed せず snapshot rollback で撤去します。この非対称が設計どおりの clean 状態です。
次の章へ
この章では、更新を「ダウンロードする作業」ではなく、信頼の起点、承認、段階配布、観測、復旧を含む変更管理として扱いました。次の章では、この一連の変更を継続的に監視し、異常を早く見つける方法へ進みます。
比較コラム:なぜキャッシュ方式ではなく署名付き内部リポジトリなのか
当初は、配信サーバが外部から取得しながら受講者へ渡す構成を検討していました。しかし実機で確認すると、配信サーバへ安全な取得経路を追加すること自体が受講者の外向き経路になり得ました。外部配布元のアドレス変動へ追随する運用負債も残ります。
そこで「防御を積み増す」のではなく、「その防御が必要になる外向き経路を作らない」構成へ変更しました。最初から正解を引いたのではありません。実機で前提を確かめ、成立しない前提と新しい穴が見つかったため、方式そのものを撤回しました。
もっと知りたい方へ
apt-mark hold: 特定パッケージを一時的に更新対象から外す。依存関係と解除条件を作業票に残さず常用すると、セキュリティ更新の取りこぼしになるunattended-upgrades: 自動更新を行う仕組み。本章のようにカナリア・承認・健全性確認が必要な環境では、対象範囲、再起動方針、失敗通知を先に設計する- 段階展開の拡張: 台数が増えたら、1台ずつの手作業ではなく構成管理ツールでバッチ幅・中止条件・再試行をコード化する。本章はその前に、手で判断の型を通す章である
- スナップショットとバックアップの違い: スナップショットは同じ仮想化基盤上で短時間に戻す用途に強い。基盤障害や長期保管に備える別系統のバックアップは次章で扱う
なぜ InRelease だけの署名から全体を守れるのか
InRelease は Packages のハッシュを持ち、Packages は各 .deb のファイル名、サイズ、ハッシュを持ちます。署名済みの起点からハッシュが連鎖するため、途中の索引やパッケージが変われば照合に失敗します。
実務で残すべき証拠
- 公開鍵 fingerprint の正本と照合結果
InReleaseの署名検証結果- 承認したパッケージ名・版・SHA-256 の lock manifest
- カナリアと展開先の開始時刻・終了時刻
- 更新前後の版、service、journal、再起動要否
- rollback 後に消えた物と、再展開した物