Fedora IoTとCoreOS上のGreenboot:アップデート失敗時の自動ヘルスチェックとロールバック

Fedora tutorial - IT technology blog
Fedora tutorial - IT technology blog

先月、Fedora IoTを動かしているRaspberry PiのカーネルをアップデートしたらOSが起動しなくなった。モニターもキーボードもなく、部屋の隅に置いた小さな黒い箱だけ。別のマシンからSSHでrescueモードに入ってようやく直せた。あのとき初めて、Greenbootが存在する理由を身をもって理解した。

背景 & Greenbootが必要な理由

Fedora IoTとCoreOSはどちらもrpm-ostreeベースのイミュータブルファイルシステムを採用している。アップデートのたびに新しい「デプロイメント」が作成され、旧システムはそのまま保持される。理論上、アップデートが失敗しても古いデプロイメントから起動し直せばいい。しかしモニターもキーボードもないヘッドレスデバイスで、一体誰がその失敗を検知して自動的に以前のバージョンに戻してくれるのか?

それを担うのがGreenbootだ。

Greenbootは本質的にsystemdと統合された小さなフレームワークだ。起動のたびにヘルスチェックスクリプトを実行し、required.dディレクトリ内のいずれかのスクリプトが失敗すると、現在のブートを失敗とマークする。3回連続で失敗すると、rpm-ostreeが自動的に前のデプロイメントにロールバックする。人間の介入は一切不要だ。

Fedoraをメインの開発機として2年使ってきた。ラップトップでアップデートが失敗しても面倒だが、まだ直せる。部屋の隅に置いたヘッドレスデバイスや、数百キロ離れたデータセンターのサーバーとなると話は全く変わる。Greenbootが特に必要になるのは:

  • 物理アクセスができない遠隔地のデバイス(VPS、エッジデバイス、Raspberry Pi)
  • 高いアップタイムが求められるシステム――手動修復を待つ30分のダウンタイムは許容できない
  • rpm-ostreed-automaticまたはzincatiで無人自動アップデートを有効にしている

Greenbootのインストール

Fedora IoTとCoreOSでは、Greenbootは通常プリインストールされている。確認するには:

rpm -q greenboot
# greenboot-0.15.0-4.fc40.noarch

インストールされていない場合、ファイルシステムがイミュータブルなのでdnfの代わりにrpm-ostreeを使う必要がある:

# Fedora IoT / CoreOS
sudo rpm-ostree install greenboot greenboot-default-health-checks

# 変更を適用するために再起動
sudo systemctl reboot

通常のFedora Workstation/Serverの場合:

sudo dnf install greenboot greenboot-default-health-checks

インストール後、2つのメインサービスを有効化して起動する:

sudo systemctl enable --now greenboot-healthcheck.service
sudo systemctl enable --now greenboot-status.service

# ステータスを確認
sudo systemctl status greenboot-healthcheck.service

詳細設定

Greenbootは2つのメインディレクトリからヘルスチェックスクリプトを読み込む:

  • /etc/greenboot/check/required.d/ — 必ず通過しなければならないスクリプト。失敗するとシステムがunhealthyとマークされる
  • /etc/greenboot/check/wanted.d/ — 任意のスクリプト。失敗してもwarningをログに記録するだけで、ロールバックは発動しない

ヘルスチェックの結果に応じて、Greenbootはさらに2つのhookディレクトリを実行する:

  • /etc/greenboot/green.d/ — すべてのチェックが通過したときに実行
  • /etc/greenboot/red.d/ — チェックが失敗したとき、ロールバックのための再起動前に実行

ネットワーク接続チェックスクリプト

終了コード0がhealthy、0以外がfailだ。シンプルなルールだが重要――Greenbootはテキスト出力を読まず、スクリプトの終了コードだけを見る。カーネルアップデート後に最も影響を受けやすいのがネットワークなので、私は常にネットワークチェックから始める:

sudo nano /etc/greenboot/check/required.d/01-check-network.sh
#!/bin/bash
# 起動後のネットワーク接続を確認

TIMEOUT=30
TARGET="8.8.8.8"

echo "Checking network connectivity..."

for i in $(seq 1 $TIMEOUT); do
    if ping -c 1 -W 1 "$TARGET" &>/dev/null; then
        echo "Network OK after ${i}s"
        exit 0
    fi
    sleep 1
done

echo "ERROR: Network unreachable after ${TIMEOUT}s"
exit 1
sudo chmod +x /etc/greenboot/check/required.d/01-check-network.sh

ファイル名の数字プレフィックス(01-10-99-)が実行順序を制御する。ネットワークチェックを01-に置くのは、他の多くのチェックがネットワークに依存しているからだ――ネットワークが立ち上がっていない状態でAPIエンドポイントをテストしても意味がない。

重要なサービスのチェックスクリプト

sudo nano /etc/greenboot/check/required.d/10-check-services.sh
#!/bin/bash
# 必須サービスが稼働しているか確認

REQUIRED_SERVICES=("sshd" "NetworkManager")

for svc in "${REQUIRED_SERVICES[@]}"; do
    if ! systemctl is-active --quiet "$svc"; then
        echo "ERROR: Service $svc is not running"
        systemctl status "$svc" --no-pager
        exit 1
    fi
    echo "OK: $svc is active"
done

exit 0
sudo chmod +x /etc/greenboot/check/required.d/10-check-services.sh

起動成功時にTelegram通知を送信

リモートデバイスの場合、手動でターミナルを開いてpingする代わりに、マシンが正常に起動したタイミングを正確に把握できると便利だ。green.dのhookはすべてのヘルスチェック通過後に実行されるので、「生きてます」シグナルを送るのに理想的なタイミングだ:

sudo nano /etc/greenboot/green.d/99-notify-success.sh
#!/bin/bash
HOSTNAME=$(hostname)
TOKEN="your-bot-token"
CHAT_ID="your-chat-id"

MESSAGE="✅ ${HOSTNAME} が $(date '+%Y-%m-%d %H:%M') に正常起動しました"

curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
    -d "chat_id=${CHAT_ID}&text=${MESSAGE}" &>/dev/null

exit 0

ロールバック前に警告を送信

sudo nano /etc/greenboot/red.d/99-notify-failure.sh
#!/bin/bash
HOSTNAME=$(hostname)
TOKEN="your-bot-token"
CHAT_ID="your-chat-id"
BOOT_COUNTER=$(cat /run/greenboot/boot_counter 2>/dev/null || echo "unknown")

MESSAGE="⚠️ ${HOSTNAME}: Boot health check FAILED (attempt ${BOOT_COUNTER}/3). Preparing rollback..."

curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
    -d "chat_id=${CHAT_ID}&text=${MESSAGE}" &>/dev/null

exit 0

チェック & モニタリング

再起動なしで手動ヘルスチェックを実行

# すべてのヘルスチェックを即座に実行
sudo systemctl start greenboot-healthcheck.service

# 結果を確認
sudo systemctl status greenboot-healthcheck.service

# 各スクリプトの詳細ログを確認
sudo journalctl -u greenboot-healthcheck.service -n 50 --no-pager

デプロイメント状態とブートカウンターを確認

# 現在のブートカウンター(失敗のたびに増加し、成功するとリセット)
sudo grub2-editenv - list | grep boot_counter

# すべてのデプロイメントを確認
rpm-ostree status

# 出力例:
# ● fedora:fedora/40/x86_64/iot
#                    Version: 40.20240601.0 (current)
#   fedora:fedora/40/x86_64/iot
#                    Version: 40.20240501.0 (rollback target)

意図的な失敗スクリプトでロールバックをテスト

実際のデバイスでGreenbootを信頼する前に、必ず手動でテストしている。最も手早い方法は、意図的に失敗するスクリプトを作成し、3回再起動して、システムが正しくロールバックするか確認することだ:

# 意図的に失敗するスクリプトを作成
sudo bash -c 'cat > /etc/greenboot/check/required.d/99-test-failure.sh << EOF
#!/bin/bash
echo "Simulated failure for testing"
exit 1
EOF'
sudo chmod +x /etc/greenboot/check/required.d/99-test-failure.sh

# 再起動して動作を観察
sudo systemctl reboot

3回連続でブートが失敗すると、システムは自動的に以前のデプロイメントにロールバックする。確認が終わったら、sudo rm /etc/greenboot/check/required.d/99-test-failure.shでテストスクリプトを削除することを忘れずに。

CoreOSでZincatiと統合する

CoreOSでは、Zincatiがアップデートの自動ダウンロードと適用を担う。Greenbootと組み合わせることで、監視不要の完全な自動ループが完成する。深夜2時にアップデートされ、朝目覚めると安定した新バージョンが動いているか、すでに旧バージョンにロールバック済みかのどちらかだ。

sudo mkdir -p /etc/zincati/config.d
sudo nano /etc/zincati/config.d/55-updates-strategy.toml
[updates]
strategy = "periodic"

[[updates.periodic.window]]
days = [ "Mon", "Wed", "Fri" ]
start_time = "02:00"
length_minutes = 60

月・水・金の深夜2時に更新――週末は万一のときに対応できるよう外している。アップデートで問題が起きても、Greenbootが次回起動時に即座に検知し、夜明け前にロールバックを完了させる。

リアルタイムログの監視

# すべてのGreenbootサービスをリアルタイムで監視
sudo journalctl -fu "greenboot*"

# 直近1時間のログのみ表示
sudo journalctl -u "greenboot*" --since "1 hour ago" --no-pager

セットアップ後、Raspberry Pi上のFedora IoTを数ヶ月間放置して自動更新させた。一度カーネルアップデートでWi-Fiドライバーに問題が起き、Greenbootが即座に検知(ネットワークチェックスクリプトが失敗)して自動ロールバック。朝早くTelegram通知が届いた。ダウンタイムゼロ、以前のようにrescueモードで格闘する必要もなくなった。

Share: