Xidorn Blog

Ubuntuに「リカバリー」システムを作ってみる

どのOSでも、時々システム内部からでは解決できない問題が発生するのは避けられません。何らかの変更が原因でシステムが起動しなくなった時の修復や、システムパーティションの調整をしたい時などです。Linuxデスクトップを使い、自分でPCをいじるのが好きな人にとっては、なおさらでしょう。そんな時、メインOSから独立したOSがあると助かります。

デュアルブートやLiveUSBなど、既存の方法はたくさんありますが、それぞれに問題や限界があります。この問題に関しては、私はmacOSの「リカバリー」システムの仕組みがとても気に入っています。同じハードドライブにインストールされていながらメインシステムから独立した読み取り専用のシステムで、一連のツールが付属しており、起動時に切り替えることができます。そこで、私は自分のUbuntuデスクトップにも、このような「リカバリー」システムを、よりカスタマイズしやすく、より使いやすい形で自作してみることにしました。

課題

Linuxデスクトップをメインシステムとして使っている人にとって、一般的な「リカバリー」システムの選択肢は主に2つあります。

しかし、これらの方法にはそれぞれ問題があります。私の場合、最初の方法の問題点は、デュアルブート環境ではないということです。かなり早い段階からLinuxデスクトップ単独の環境で、Windowsが必要な時は仮想マシンを起動していました。この構成の方がシンプルで便利ですし、デュアルブートで2つのシステムが互いに干渉して問題を引き起こす可能性も避けられます。

LiveUSBも悪くありませんが、いくつかの主な欠点があります。

私もCubicを使ってカスタムISOを作成し、それをUSBメモリに書き込んで「リカバリー」システムとして使ってみたことがあります。これで利便性と互換性の問題は一部解決しましたが、何かを調整するたびにそのプロセスを繰り返してUSBメモリに書き込むのは非常に面倒でした。しかし、Cubicがワークスペースを作成し、ファイルシステムをその中に残すというアプローチは、私にとって大きなヒントになりました。

多くの資料を参考にした結果、私は自分のPC用に、Ubuntuベースの「リカバリー」システムを一から構築することに決めました。

目標と方針

この「リカバリー」システムに設定した目標は以下の通りです。

これらの要件と既存のツールに基づき、私の大まかな計画は次のようになりました。

ワークスペースとスクリプト

まず、新しいBtrfsサブボリューム/recoveryをワークスペースとして作成します。

sudo btrfs subvolume create /recovery

この場所は任意ですが、ほとんどの操作にはroot権限が必要なので、ユーザーディレクトリの外に置くと手間が省けます。独立したサブボリュームを作成する利点は、調整を行うたびに新しいスナップショットを作成して現在の状態を保存できることです。もし後の調整で壊してしまっても、いつでもスナップショットから以前の正常なバージョンに戻すことができ、ファイルシステムレベルのバージョン管理のようなものです。

このワークスペースには、3つのフォルダを置きました。

ベースファイルシステム

次に、ファイルシステムの中身を初期化する必要があります。公式サイトのミラーからUbuntu Baseのデイリービルド版をダウンロードできます。例えば、私の「リカバリー」システムをUbuntu 24.04 LTSベースにしたい場合、Ubuntu Base 24.04 (Noble Numbat) Daily Buildから対応するアーキテクチャの圧縮ファイル、例えばnoble-base-amd64.tar.gzをダウンロードし、ベースファイルシステムとして使います。fsディレクトリに移動し、その中身を展開します。

cd /recovery/fs
sudo tar xvf path/to/noble-base-amd64.tar.gz

デイリービルド版を使うメリットは、含まれているパッケージがそのメジャーバージョンの中で最新であるため、ダウンロード後にこれらのパッケージを再度アップグレードする必要がないことです。

chrootスクリプト

Ubuntu Baseには最も基本的なシステムしか含まれていないため、当然ながらより多くのパッケージをインストールし、その後も多くの調整を行う必要があります。そのため、この「リカバリー」システムのファイルシステムに「入って」変更を加えるためのスクリプトが必要です。ここでは、そのためのchrootスクリプトstart-chroot.shを作成しました。

#!/bin/sh

set -fv

# rootで実行されていることを確認
if [ "$(id -u)" != "0" ]; then
    echo "Require root!"
    exit 1
fi

cd /recovery

# メインシステムのDNS設定を使用してネットワークを確保
mv fs/etc/resolv.conf fs/etc/resolv.conf.1
cp /etc/resolv.conf fs/etc/resolv.conf

# 必要なシステムマウントポイントをマウント
mount --bind /dev fs/dev
mount none -t proc fs/proc
mount none -t sysfs fs/sys
mount none -t devpts fs/dev/pts

# chrootに入る
chroot fs

# 一時ファイルをクリーンアップ
rm -f fs/var/lib/dbus/machine-id
rm -f fs/root/.bash_history
rm -rf fs/tmp/*

# システムマウントポイントをアンマウント
umount fs/dev/pts
umount fs/dev
umount fs/proc
umount fs/sys

# DNS設定を元に戻す
mv fs/etc/resolv.conf.1 fs/etc/resolv.conf

このスクリプトがあれば、いつでも「リカバリー」システムが使用するファイルシステムに入り、その内容を変更できます。

スクリプト内でresolv.confを変更しているのは、このファイル自体がsystemdによって管理されるシンボリックリンクであり、「リカバリー」システム内でこの構造を変更したくないためです。しかし、chrootでこのファイルシステムに入ったとき、このシステム内のsystemdは実行されていないため、resolv.confは空になります。これにより、ネットワークを必要とするプログラムがDNS設定なしで正常に動作しなくなるため、一時的にメインシステムのファイルで置き換える必要があります。

ビルドスクリプト

次に必要なのは、ファイルシステムを読み取り専用にパッケージ化し、「リカバリー」システムを起動するために必要なEFI実行ファイルを構築するスクリプトです。以下が私のbuild-image.shスクリプトです。

#!/bin/sh

set -efv

# rootで実行されていることを確認
if [ "$(id -u)" != "0" ]; then
    echo "Require root!"
    exit 1
fi

cd /recovery

# 古いイメージを削除
if [ -L image ]; then
  old_image=$(readlink image)
  if [ -d "$old_image" ]; then
    rm -rf "$old_image"
  fi
  rm image
fi

# ビルド結果を格納するための一時ディレクトリを作成
tmp_dir=$(mktemp -d)
ln -s "$tmp_dir" image
mkdir image/casper

# ファイルシステムをパッケージ化
mksquashfs fs image/casper/filesystem.squashfs -comp zstd

# カーネルイメージをビルド
ukify build \
  --linux=fs/boot/vmlinuz \
  --initrd=fs/boot/initrd.img \
  --cmdline="boot=casper noprompt libata.allow_tpm=1" \
  --output=image/recovery.efi

ここでは、ビルド結果を一時フォルダに入れ、その一時フォルダをimageサブフォルダにシンボリックリンクしています。これは必須ではありませんが、私がこの方法を選んだ理由は以下の通りです。

イメージファイルはすでに圧縮された大きなバイナリブロックであり、ファイルシステムに追跡させるメリットは特にありません。それに、このスクリプトを使えばいつでもfsから新しいイメージを素早くビルドできるので1、古い結果を保持する必要はありません。

ここではukifyを使ってユニファイドカーネルイメージをビルドし、いくつかの簡単なカーネルパラメータを使用しています。

スナップショット

前述の通り、ワークスペースを独立したBtrfsサブボリュームに置くことで、スナップショットの作成が容易になります。私はsnapperを使ってスナップショットを管理しており、そのためにまず設定ファイル/etc/snapper/configs/recoveryを作成します。

SUBVOLUME="/recovery"
FSTYPE="btrfs"
QGROUP=""
NUMBER_CLEANUP="yes"
NUMBER_MIN_AGE="1800"
NUMBER_LIMIT="5"
NUMBER_LIMIT_IMPORTANT="1"

その後、最初のスナップショットを作成します。

sudo snapper -c recovery create

将来的にも同じコマンドでさらにスナップショットを作成できます。

システムの設定

ワークスペースを作成し、対応するスクリプトを配置したら、「リカバリー」システムを実際に使えるように設定する必要があります。

このセクションのコマンドは、特に断りのない限り、start-chroot.shによって作成されたchroot環境内で実行します。

基本パッケージのインストール

まず、ベースシステムをインストールします。

apt update
apt install linux-generic
apt install --no-install-recommends ubuntu-minimal
apt install casper discover laptop-detect os-prober
apt install ubuntu-desktop

Casperはデフォルトで起動時にMD5チェックを行いますが、これは私たちにとってあまり意味がないので無効にします。

systemctl disable casper-md5check.service

次に、「リカバリー」システムではあまり必要ないと思われるパッケージをいくつかクリーンアップします。

apt autoremove --purge \
    snapd \
    rhythmbox \
    libreoffice-common \
    totem \
    gnome-calendar \
    gnome-clocks \
    gnome-characters \
    gnome-startup-applications \
    gnome-online-accounts \
    transmission-gtk \
    cloud-init \
    unattended-upgrades \
    firefox \
    thunderbird \
    ubuntu-docs \
    ubuntu-report

ここに挙げたのは私が削除したパッケージで、主にメディア再生、オフィス文書編集、システムアップグレードなど、「リカバリー」システムではあまり使わないと思われるものです。おそらくもっと多くのパッケージをアンインストールしても「リカバリー」システムとしての機能に影響はないでしょうが、まだ詳しく調べていません。snapもアンインストールしました。snapベースのソフトウェアで必要なのはFirefoxだけですが、私はMozilla公式リポジトリのバージョンを好むからです。これについては後のセクションで詳しく説明します。

その他、エディタ、よく使うコマンドラインツール、メンテナンス用ツールなど、必要なツールをインストールします。

apt install vim-gtk3 ripgrep curl bash-completion
apt install gparted mtools dmraid \
    efibootmgr btrfs-progs \
    nvme-cli smartmontools \
    cryptsetup lvm2 \
    snapper-gui \
    systemd-ukify systemd-boot-efi

具体的なツールはもちろん、個人の使用習慣や実際のニーズに合わせて調整してください。例えば、ソフトウェア暗号化パーティションがなければcryptsetupは不要かもしれませんし、Emacsを常用しているならVIMは不要かもしれません。

APTリポジトリにないツールがあれば、自分でfsディレクトリにコピーしてもよいです。例えば、私はsedutilが必要なので、そのリリースぺージからダウンロードし、バイナリファイルsedutil-cli/usr/sbinにコピーして実行権限を付与しました。

Firefoxのインストール

上記のステップでFirefoxを削除しましたが、「リカバリー」システムにはブラウザが必要です。今の時代、PCが使えなくてもスマートフォンで情報を調べることはできますが、やはりPCで直接調べられる方が便利です。

ここでは、Mozillaの公式ドキュメントInstall Firefox on Linuxに従ってインストールします。

# 署名キーをインストール
install -d -m 0755 /etc/apt/keyrings
wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | \
	tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null
# APTリポジトリを追加
cat <<EOF | tee /etc/apt/sources.list.d/mozilla.sources
Types: deb
URIs: https://packages.mozilla.org/apt
Suites: mozilla
Components: main
Signed-By: /etc/apt/keyrings/packages.mozilla.org.asc
EOF
# 優先度を設定
cat <<EOF | tee /etc/apt/preferences.d/mozilla
Package: *
Pin: origin packages.mozilla.org
Pin-Priority: 1000
EOF
# パッケージをインストール
apt update
apt install firefox-esr

「リカバリー」システムは頻繁に更新しないので、サポート期間が長く安定しているESR版を選びましたが、これはそれほど重要なことではありません。

Firefoxには、自動更新チェック、Firefoxアカウント、プロファイルインポートなど、「リカバリー」システムには不要な機能が多くあります。また、Casperが作成する新規ユーザーに対して、「リカバリー」システムでは意味のない多くの通知が表示されます。これらはFirefoxのポリシーファイルを使ってすべて無効にできます。ここでは、ワークスペースにfiles/firefox-policies.jsonを追加しました。

{
  "policies": {
    "DisableAppUpdate": true,
    "DisableFirefoxAccounts": true,
    "DisableFirefoxStudies": true,
    "DisablePocket": true,
    "DisableProfileImport": true,
    "DisableProfileRefresh": true,
    "DisableSystemAddonUpdate": true,
    "DontCheckDefaultBrowser": true,
    "DisplayBookmarksToolbar": "never",
    "NoDefaultBookmarks": true,
    "OfferToSaveLogins": false,
    "OverrideFirstRunPage": "",
    "UserMessaging": {
      "ExtensionRecommendations": false,
      "FeatureRecommendations": false,
      "SkipOnboarding": true,
      "MoreFromMozilla": false,
      "FirefoxLabs": false
    }
  }
}

具体的なオプションはポリシーファイルのドキュメントを参照してください。多くのポリシーが選択可能です。

このポリシーファイルを「リカバリー」システム内のFirefoxに適用するには、chrootので以下を実行する必要があります。

mkdir -p fs/etc/firefox/policies
ln files/firefox-policies.json \
	fs/etc/firefox/policies/policies.json

これにより、ワークスペースのfilesとファイルシステム内の対応するファイルがリンクされます。これにより、外部から直接変更できるだけでなく、「リカバリー」システムを最初から作り直す必要がある場合でも、これらの設定ファイルを直接再利用できます。

WiFiの設定

「リカバリー」システムでネットワークが使えることは非常に重要です。情報を調べられるだけでなく、必要であればインストールしていないツールをダウンロードすることもできます。WiFiでインターネットに接続する場合、WiFiのパスワードも「リカバリー」システムに設定しておく必要があります。

nmcli --offline connection add type wifi \               
  autoconnect yes \
  ssid "<SSID>" \
  wifi-sec.key-mgmt wpa-psk \
  wifi-sec.psk "<password>" \
  > /etc/NetworkManager/system-connections/wifi.nmconnection
chmod 0600 /etc/NetworkManager/system-connections/wifi.nmconnection

ここの<SSID><password>を実際のWiFi設定に置き換えれば、「リカバリー」システムを起動すると自動的に指定のWiFiに接続され、追加の操作は不要になります。

ディスプレイの設定

システムがディスプレイ設定を正しく検出できないことがあります。特に、ディスプレイを回転させていたり、異なるスケーリング比率を使用したい場合です。

その場合、~/.config/monitors.xmlfs/root/monitors.xmlにコピーし、Casperのinitramfsスクリプトをfs/usr/share/initramfs-tools/scripts/casper-bottom/99gnome_monitorsに作成します。

#!/bin/sh

PREREQ=""

prereqs()
{
    echo "$PREREQ"
}

case $1 in
# get pre-requisites
prereqs)
    prereqs
    exit 0
    ;;
esac

chroot /root install \
    -o $USERNAME -g $USERNAME \
    /root/monitors.xml \
    /home/$USERNAME/.config/monitors.xml

そして、以下を実行します。

chmod +x /usr/share/initramfs-tools/scripts/casper-bottom/99gnome_monitors
update-initramfs -u

これにより、このディスプレイ設定ファイルは起動時に自動的に一時ユーザーの.configにインストールされ、ディスプレイ設定が正しく適用されるようになります。

GNOMEデスクトップ設定の上書き

Ubuntuのドックにデフォルトで表示されるアプリケーションは、私たちが望むものではないかもしれませんが、これも変更可能です。

先ほどのFirefoxのポリシーファイルと同様に、まず設定ファイルfiles/gnome-settings.schema.overrideを作成します。

[org.gnome.shell]
favorite-apps = [ 'org.gnome.Nautilus.desktop', 'firefox-esr.desktop', 'org.gnome.Terminal.desktop', 'gparted.desktop' ]

[org.gnome.shell:ubuntu]
favorite-apps = [ 'org.gnome.Nautilus.desktop', 'firefox-esr.desktop', 'org.gnome.Terminal.desktop', 'gparted.desktop' ]

次に、chrootので以下を実行します。

ln files/gnome-settings.schema.override \
	fs/usr/share/glib-2.0/schemas/99_settings.gschema.override

これで設定ファイルがファイルシステムにリンクされます。最後に、chroot内で以下を実行します。

cd /usr/share/glib-2.0/schemas
rm gschemas.compiled
glib-compile-schemas .

これにより、GNOMEのデータベースの情報が正しく更新されます。

ここではファイルブラウザ、ブラウザ、ターミナル、GPartedを配置しましたが、具体的にどのように配置するかは、個人のニーズに合わせて調整しても構いません。なぜここで:ubuntuを含む設定を2回書く必要があるのかは、私もはっきりとはわかりません。しかし、これらのデータが正しいかどうかは、次のコマンドで確認できます。

gsettings --schemadir . get org.gnome.shell favorite-apps

ビルドとデプロイ

システムの構成が完了したら、先ほど述べたビルドスクリプトを使って「リカバリー」システムをビルドします。

sudo ./scripts/build-image.sh

パッケージ化が完了するのを待つと、ワークスペースのimageフォルダ内にrecovery.eficasper/filesystem.squashfsという2つのファイルが見つかります。あとは、EFIがこのファイルを使えるようにするだけです。

私はこれらを直接EFIシステムパーティションにコピーしました2が、一般的にはこのパーティションは200MB程度で十分だとされているようです。しかし、Ubuntuベースの「リカバリー」システムは簡単に1GB以上のサイズになります。もしEFIシステムパーティションに十分な空き容量がない場合は、FAT32パーティションを新たに作成して保存する方法もあります。注意点として、filesystem.squashfsはパーティションのルートにあるcasperサブディレクトリに置く必要がありますが、recovery.efiはどこに置いても構いません。

最後に、EFIが「リカバリー」システムを見つけられるようにすれば完了です。recovery.efi/dev/nvmeXnYpZパーティションのルートディレクトリにあると仮定して、以下を実行します。

sudo efibootmgr --create \
	--disk /dev/nvmeXnY --part Z \
	--label "Ubuntu Recovery" \
	--loader recovery.efi

完了後、efibootmgrでこの「リカバリー」システムの起動番号を確認し、

sudo efibootmgr --bootnext XXXX

を使って次回の起動時に「リカバリー」システムに入ることができます。また、BIOSによってはインターフェースからEFIの起動エントリを選択して起動することもできます。

再起動してテストし、問題がなければ、snapperでワークスペースの現在の状態の新しいスナップショットを作成し、作業内容を保存します。

参考資料

脚注

  1. 私のマシンでは、1回のビルドにかかる時間はわずか30秒程度です。この時間で容量を節約できるなら、十分価値があります。

  2. 私は前回ハードドライブを交換した際に、このパーティションにわざわざ10GBを割り当てました。