Alpine local backup は Apkovl ファイルをどのように読みだしているか

TL;DR

Alpine local backup とは何か

Alpine local backup(以下 ALB) は、 Alpine Linux の Persistence 機構の一つ。 これを利用すれば、 diskless モード などの再起動後に保存したファイルが失われるモードで、変更内容を再起動後に持ち越すことができる。

立ち位置としては、 Ubuntu の casper-rw などに似ている。 casper-rw とは違い、変更点を Apkovl と呼ばれる単純な tar ファイルに保存する。 そのため、別マシンで既存の Apkovl ファイルを編集することや、別マシンに変更点をデプロイすることが気軽に行える。

詳しい利用方法は Alpine Linux 公式 Wiki の Alpine local backup の項にまとまっている。

ALB は起動時に、どのように Apkovl ファイルを読みだしているか

Apkovl ファイルを使ってシステムに変更点を反映するには、 Apkovl ファイルをフロッピーなど適当なメディアのルートディレクトリに置いておくだけでよい。 利用側としては手軽だが、 Alpine Linux がどのように Apkovl ファイルを探し出し、変更を反映しているのだろうか。

Apkovl ファイルを見つける

Apkovl を 見つけ出し rootfs に展開する機能は、 initramfs init に実装されている。 ここからは、具体的な処理についてコードを交えながら見ていく。

Apkovl ファイルの場所は、カーネルコマンドラインの apkovl オプションで明示的に指定することができる。 この指定は必ずしも必要ではなく、未指定の場合は自動的に Apkovl ファイルを探索する。

実際に利用する Apkovl ファイルを決定する処理は init の 593 行付近に書かれている。実装を引用すると、

if [ -z "$KOPT_apkovl" ]; then
	# Not manually set, use the apkovl found by nlplug
	if [ -e /tmp/apkovls ]; then
		ovl=$(head -n 1 /tmp/apkovls)
	fi
elif is_url "$KOPT_apkovl"; then
	# Fetch apkovl via network
	MACHINE_UUID=$(cat /sys/class/dmi/id/product_uuid 2>/dev/null)
	url="${KOPT_apkovl/{MAC\}/$MAC_ADDRESS}"
	url="${url/{UUID\}/$MACHINE_UUID}"
	ovl=/tmp/${url##*/}
	wget -O "$ovl" "$url" || ovl=
else
	ovl="$KOPT_apkovl"
fi

となっており、 カーネルコマンドラインに何らかの値が設定されているときは、その値を信用して $ovl に設定していることがわかる。

apkovl オプションが未指定の場合は、 use the apkovl found by nlplug となっている。 ovl=$(head -n 1 /tmp/apkovls) となっていることから、 /tmp/apkovls は Apkovl ファイルのリストだと予想できる。 /tmp/apkovls は、 init の 565 行付近にある、以下のコードによって作られる。

nlplug-findfs $cryptopts -p /sbin/mdev ${KOPT_debug_init:+-d} \
	${KOPT_usbdelay:+-t $(( $KOPT_usbdelay * 1000 ))} \
	$repoopts -a /tmp/apkovls

nlplug-findfs というプログラムに対して /tmp/apkovls という引数を与えており、 use the apkovl found by nlplug が正しそうに見える。

nlplug-findfs は Alpine Linux の initramfs init 独自のユーティリティであり、 kobject_uevent に流れているメッセージを利用してデバイスのマウントなどを行う。

nlplug-findfs はファイルシステムのマウントに関するさまざまな処理を行っている。 Apkovl ファイルの検索で重要な関数としては scandev が挙げられる。 この関数は /media/* をマウントするためのものだが、 Apkovl を検索する機能も持っている。

実際に Apkovl を検索するコードは scandev_cbであり、 Apkovl の検索を行う実装は、

} else if (fnmatch("*.apkovl.tar.gz*", opts->filename, 0) == 0) {
	dbg("found apkovl %s", opts->path);
	append_line(conf->apkovls, opts->path);
	ctx->found |= FOUND_APKOVL;
}

になる。ここで、 append_line は第二引数のデータを第一引数のファイルに追加する関数であり、 conf->apkovls-a オプションで指定した内容(今回の場合は /tmp/apkovls )であることを踏まえると、 *.apkovl.tar.gz* を満たすファイルを Apkovl としてリストしていることがうかがえる。

なお、 scandev_cb を呼び出す scandev各デバイスのルートディレクトリしか探索しない設定がされているため、 /media/usb/foo/example.apkovl.tar.gz のようなファイルは Apkovl ファイルとして認識されない。

Apkovl ファイルを読みだす

読み出しは init の 614 行付近で行われる。実際の読み出し処理は unpack_apkovl に書かれている。

unpack_apkovl を読むと、

local ovl="$1"
local dest="$2"
local suffix=${ovl##*.}
local i
ovlfiles=/tmp/ovlfiles
if [ "$suffix" = "gz" ]; then
	tar -C "$dest" -zxvf "$ovl" > $ovlfiles
	return $?
fi

# we need openssl. let apk handle deps
apk add --quiet --initdb --repositories-file $repofile openssl || return 1

if ! openssl list -1 -cipher-commands | grep "^$suffix$" > /dev/null; then
	errstr="Cipher $suffix is not supported"
	return 1
fi
local count=0
# beep
echo -e "\007"
while [ $count -lt 3 ]; do
	openssl enc -d -$suffix -in "$ovl" | tar --numeric-owner \
		-C "$dest" -zxv >$ovlfiles 2>/dev/null && return 0
	count=$(( $count + 1 ))
done

のように、拡張子によって動作を分けていることがわかる。 これにより、 lbu commit -e が生み出す暗号化された Apkovl ファイルを読みだすことを可能にしている。 また、コードから、「暗号化されたファイルの Apkovl ファイルのキーファイルを指定する」機能はなく、起動時にユーザーが明示的にパスワードを入力する必要があるとわかる。

まとめ

Alpine Linux の Alpine local backup は、 initramfs init で魔法を起こしている。 Alpine local backup は仕組みを知らなくても手軽に利用できるが、裏側の仕組みを知っておけばより愛着も沸くことだろう。