はじめに
概要
最近のLinuxにはoverlayfsという仕組みがあり、対象のファイル・ディレクトリ構造を、ベースとなるread-onlyな領域と、変更差分を保存する領域とに分離して管理することができます。
しかし、ルートディレクトリはシステム起動に直に関わる部分であるため、CentOSの標準ではoverlayfsをルートとする手段が提供されていません。
そこで、CentOS標準のdracutを拡張し、ルートディレクトリをoverlay化する方法を試してみました。
話としては、ArchLinuxですがパッケージインストールからの変更をoverlayで管理すると似たようなものになるかと思います。
なお念のためですが、自身の環境で試す場合には自己責任でお願いします。
環境
実行環境としてはCentOS7.4を使用しました。
予備知識
overlayfs
overlayfsとは、はじめにで触れた通り、「ベースとなるread-onlyな領域と、変更差分を保存する領域とに分離して管理する」ファイルシステムです。
※色々眺めていると、Dockerを使用する文脈で登場することが多いでしょうか。
何が嬉しいかと言うと、例えば次のようなコトを実現できるからです
- 変更差分を別に管理できるため、一気に以前の状態にロールバックするのが容易
- 複数サーバで似たようなファイルシステムを用意する時に、共通部分をNFS共有なんかで括りだして管理をまとめることができる
しかも変更部分をノード毎に分離できるため、共通部分への影響を考えなくていい
前者は、よくネットカフェのPCにあるような「再起動したら元の状態に復元」を容易に実現できることが期待できますし、後者は多数の類似サーバをディスクレス管理するのに生かせそうです。
ただし、ext系やxfsのように何らかのデバイス上に直接ファイルシステムを構築する方式とは違い、何らかベースになるようなファイルシステムを用意して、それをマージして使う方式であることに注意が必要です。が、逆に言えば標準的なファイル操作でベース部分を操作すれば色々できるということで、ファイルシステム固有の特殊な手順というのを意識する必要がありません。これも良いところです。具体的な話は実践! OverlayFSが参考になるかと思います。
なお、overlayfs の機能は Linux kernel 3.18 でマージされた、となっていますが、CentOS7 の kernel 3.10系 にもバックポートされているようで、CentOS7.4 ならば最小インストール状態でも使うことができます。
initramfsとdracut
initramfsとは
Linux kernel は、各種デバイスドライバ、ファイルシステム等様々な機能をモジュールとして分離しています。このモジュールの実体は個々の .ko ファイルですから、
- ファイルシステムを認識するにはモジュールファイルが必要
- モジュールファイルをロードするには先にファイルシステムを認識する必要がある
という鶏と卵状態になってしまいます。
そのため、Linux が起動する際、ブートローダである grub等は、kernel と共に初期処理用のファイル一式 ( カーネルモジュール含む ) を詰め込んだイメージをロードし、主に本命のルートファイルシステムをマウントするまでの処理を実現します。このイメージ(或いは初期処理用に展開されたファイルシステム)のことをinitramfsと言っています。
この初期処理でルートファイルシステムが / でない仮の場所にマウントされた後は、switch_rootコマンド或いはsystemd(systemctl)の持つswitch-rootの機能により、ルートファイルシステムの切り替わりが、initramfsが展開された初期ルートファイルシステムから本命のルートファイルシステムへと行われ、更なる初期化処理や各種サービスの起動へと移っていきます。
dracutとは
dracutとは、上述のinitramfsによる初期化処理用のフレームワークであり、また、initramfs作成ツールであるdracutコマンドを含んでいます。
dracutはモジュール構成をとっており、初期化において必要な機能 ( 例えば lvm だとか、iscsiだとか… ) を個々のモジュールとして管理し、initramfsに組み込みます。
また、dracutはinitramfs上での処理を各ステージに分けており、個々のステージからモジュール毎に組み込まれたフックを呼び出すことでLinux起動時の処理を実現します。ステージの概要についてはdracut.modules(7)のBOOT PROCESS STAGESをご参照ください。フックの実装は当該サーバの構成をハードコーディングするような形で行うことも可能ですが、grub等ブートローダから渡される起動時オプションをパラメータとして活用するように柔軟に行うことも可能です。一般には後者の方が好ましいと思います。
ちなみに、フックを含めモジュールの実体はシェルスクリプトです。initramfs作成時のモジュール組み込みを制御するmodule-setup.sh
と、initramfsの中に組み込まれ、フックの処理を実現する各種スクリプトからなります。なお、フックや、必要なカーネルモジュール・ツール等はmodule-setup.sh
の中で、initramfsに組み込むよう指示します。
ルートのoverlay化
実現方針
ルートをoverlay化するに際し、initramfsを使う処理のどのステージでoverlayfsマウントするかを検討します。
ext系やxfs、nfsといった、直接マウントすることで即ファイルシステムとして使える場合であれば、mountフックでマウントする処理を書けば済む話です。しかし、overlayfsの場合はベースとなるファイルシステムを何かしら先にマウントしなければなりません。それを一つ一つパラメータとして与えるのも面倒なので、overlayで差分を管理する領域の情報のみを追加するようにしました。パラメータ名は ov.XX としています。
どうなるかというと、
- 標準 … カーネルパラメータ root=XX で、XX を新ルートディレクトリにマウント、スイッチ
- overlay … カーネルパラメータ root=XX で仮の新ルートとしてマウント、ov.dev=YY で、YYをupper,work を含む領域としてマウント、仮の新ルートをlowerとして overlay を新ルートディレクトリにマウント、スイッチ
というように起動時の挙動を変えます。なので、ステージとしてはpre-pivot ( ルートディレクトリをマウントした後、スイッチする前のステージ ) でけりをつけることになります。
モジュールの実装
ファイル構成
今回モジュール名はovroot
とし、/usr/lib/dracut/modules.d/99ovroot
に以下のファイルを生成しました。
-
module-setup.sh
initramfsへ(dracutの)モジュールを組み込む際の設定を記述する -
ovroot.sh
フックの実体。module-setup.sh
の中で、pre-pivotステージのフックとして組み込むよう指定する。
なお、ノリで付けたサブディレクトリ名の99は優先順位めいたものかと思うのですが、詳細は把握しておりません。少なくとも同一ステージでの各モジュールでのフックの処理順序は別途指定しますので、違うのではないかと思います。
モジュール組み込み
以下のようなmodule-setup.sh
を用意しました。
#!/bin/bash
# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
# ex: ts=8 sw=4 sts=4 et filetype=sh
check() {
modprobe -n overlay >/dev/null 2>&1 && return 255
return 1
}
depends() {
echo bash
return 0
}
installkernel() {
hostonly='' instmods overlay
return 0
}
install() {
inst_multiple find rsync
inst_hook pre-pivot 99 "$moddir/ovroot.sh"
}
このファイルは、シェルスクリプトではあるのですが単独で動作するわけではなく、ライブラリとしてdracutからsourceされ使われます。ここではモジュールの設定に従い幾つかの関数を定義するのですが、今回はcheck
,depends
,installkernel
,install
の4関数を定義しました。
- check
返り値により、モジュールの組み込み可否を決定します。0ならば無条件組み込み、1ならば無条件除外、255ならば明示的に指定された場合のみ組み込み、となります。
一応今回は、kernelがoverlayに対応しているかどうかだけをチェックしています。他にも、必要なツールが揃っているかなどをチェックして組み込むかどうかを切り替えることができます。 - depends
依存関係にあるdracutの他モジュールを、標準出力への出力により指定します。今回フックとして組み込むスクリプトはbash依存の書き方をしているので、標準のbashモジュールも同時に組み込まれるようにしています。 - instkernel
initramfsに組み込むkernelモジュールを指定する関数です。dracutの用意しているinstmods
関数によりoverlayカーネルモジュールを組み込みます。 - install
モジュールのインストール処理本体です。必要になるツールをinst
やinst_multiple
関数で組み込みます。今回はfind,rsyncを指定しています。また、フックをinst_hook
関数で組み込みます。今回は、pre-pivotステージの最後(優先度99)ということで、もう一つのovroot.sh
を指定しています。
フックの実体
以下のようなovroot.sh
を用意しました。
#!/bin/bash
# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
# ex: ts=8 sw=4 sts=4 et filetype=sh
# setup or manipulate overlay'ed root
OV_BASE=/run/ov
OV_ROOT=$OV_BASE/root
my_try() {
[ -n "$MY_DIVED_INTO_SUBSHELL" ] && return 0
local fd
exec {fd}>&1
MY_EXCEPTION=$(
set -e
exec {fd2}>&$fd
eval "exec $fd>&1 >&$fd2 $fd2>&-"
MY_DIVED_INTO_SUBSHELL=1 MY_EFD=$fd ${FUNCNAME[1]} "$@"
)
MY_TRY_RET=$?
exec {fd}>&-
return 1
}
# mount the additional device for overlay
ov_mount_dev() {
my_try
if [ $? != 0 ]; then
# some recovery operation; see $MY_EXCEPTION for excepctions happened
return $MY_TRY_RET
fi
mkdir "$OV_BASE"
mount -t "${OVTYPE:-xfs}" -o "$OVOPT" "$OVDEV" "$OV_BASE"
[ -d "$OV_ROOT" ] || mkdir -m 700 "$OV_ROOT"
for d in "$OV_ROOT"/{lower,upper{,/etc},work}; do
[ -d $d ] || mkdir $d
done
mount --bind "$NEWROOT" "$OV_ROOT/lower"
}
remount_rw_lower() {
[ "$ROOT_RO" != 1 || "$OVRW" = 1 ] && return 0
mount -o remount,rw "$OV_ROOT/lower"
OVRW=1
}
remount_ro_lower() {
[[ "$ROOT_RO" != 1 || "$OVRW" = 1 ]] || return 0
mount -o remount,ro "$OV_ROOT/lower"
}
ov_clear() {
rm -rf * .{[^.],??*}
}
ov_mount() {
if [ ! -e etc/fstab ]; then
sed -E 's/^([[:space:]]*[^#[:space:]]+[[:space:]]+\/[[:space:]])/#\1/' ../lower/etc/fstab > etc/fstab
fi
modprobe overlay
umount "$NEWROOT"
mount -t overlay ov -o "lowerdir=$OV_ROOT/lower,upperdir=$OV_ROOT/upper,workdir=$OV_ROOT/work" "$NEWROOT"
}
ov_sync() {
remount_rw_lower
rsync -av ./ ../lower/ \
&& find ../lower -type c \
| xargs -d \\n -L 100 stat -c '%t,%T %n' \
| sed -ne 's/^0,0 //p' \
| xargs -d \\n -L rm -f
}
ov_main() {
my_try
if [ $? != 0 ]; then
# some recovery operation; see $MY_EXCEPTION for exceptions happened
return $MY_TRY_RET
fi
cd "$OV_ROOT/upper"
OV_MOUNT=1
if [ -f .ov-clear ]; then
ov_clear
elif [[ -f .ov-sync || -f .ov-synconly ]]; then
[ -f .ov-synconly ] && OV_MOUNT=0
[ -f .ov-updatefstab ] || rm -f etc/fstab
rm -f .ov-*
ov_sync && ov_clear
fi
remount_ro_lower
[ "$OV_MOUNT" = 1 ] && ov_mount
}
# get parameters
OVDEV=$(getarg ov.dev)
[ -n "$OVDEV" ] || return
OVTYPE=$(getarg ov.type)
OVOPT=$(getarg ov.opt)
getarg ro && ROOT_RO=1
ov_mount_dev
[ $? = 0 ] || return
ov_main
こちらも、dracutからはsourceされることで実行されます。なので、getarg
等のdracut側で用意している関数を使うことができます。
※ただ関数仕様がちゃんと書いてないのが辛い…
基本的には、ov_mount
関数で、overlayのupper,work用領域のマウント、ov_main
関数でoverlayのマウントを行っています。
なお、overlay化処理を調整する起動時パラメータは以下の3つです。
- ov.dev
overlayのupper,work部分を保持するマウント対象(デバイス名等)を指定します。指定の仕方は、通常のroot=XX
と同じで、デバイスファイル名やUUID=xxxxx
といった値を使えます。もし指定がない場合、overlay化の処理は一切行いません。 - ov.type
ov.devで指定された対象のファイルシステムタイプを指定します。無指定時はデフォルトでxfsと見做します。 - ov.opt
追加のマウントオプションを、もし必要であれば指定します。
ところで、今回の実装では、特定のファイルの存在をスイッチにすることで、幾つかの処理を行うように調整しています。
-
/.ov-clear
overlayマウント前に、差分(upper領域)をクリアします。つまりoverlay化した時点、或いは最後に同期を取った時点まで、ファイルシステムの状態がロールバックされることになります。 -
/.ov-sync
,/.ov-synconly
差分(upper領域)の内容を、lower領域である、元のルートに反映(同期)します。/.ov-synconly
の場合、反映だけしてoverlayマウントはしません ( 素のルートをそのまま使います )。モジュールと一緒にrsyncを組み込んだのはこのためです。なお、削除されたファイルは、メジャー番号・マイナー番号が共に0のキャラクタデバイスとして見えますので、find,statを組み合わせて削除しています。 -
/.ov-updatefstab
上述の同期の際、fstabファイルも同期対象とします。詳細は後述しますが、fstabはoverlay化のために書き換えを行いますので、デフォルトでは同期対象から外しているのです。
なお、fstabファイルでのルートディレクトリのエントリはoverlay化していない状態のものですので、そのままだと矛盾が生じます。だからといって手動でコメントアウトするようだと、overlayを一時的にでも取り止めたい場合にちょっと面倒になります。( read-onlyからread-writeに自動再マウントしてくれなくなる )
そのため、overlay化とともにfstabを一時的に書き替えて、ルートディレクトリエントリを一時コメントアウトしています。
試験動作
initramfsの作成
前項の通り、/usr/lib/dracut/modules.d
以下にファイルを用意したら、次のコマンドにより新しいinitramfsを作成します。
# mv /boot/initramfs-$(uname -r).img{,.org}
# dracut --add ovroot /boot/initramfs-$(uname -r).img
そのままだと既存のファイルを上書きできないので、一旦現行のinitramfsは退避しておきます。( 何かあった時に戻せるように、という意味もあります )
ちなみに、作成されたinitramfsはgzip圧縮されたcpioアーカイブですので、展開することで内容を確認することができます。以下から、組み込んだovroot.sh
が、dracutのpre-pivotフックとして、initramfs内の/lib/dracut/hooks/pre-pivot/
に保存されていることが分かります。
# mkdir /tmp/initramfs
# cd /tmp/initramfs
# gzip -cd /boot/initramfs-$(uname -r).img | cpio -id
# ls -l lib/dracut/hooks/pre-pivot/
total 16
-rwxr-xr-x 1 root root 8679 Apr 2 18:08 85-write-ifcfg.sh
-rwxr-xr-x 1 root root 2498 Apr 2 18:08 99-ovroot.sh
起動オプションの調整
続いては起動オプションの調整です。CentOS7のgrub2の場合、/etc/default/grub
に設定を書いたのち、grub2-mkconfig
で反映させます。( 昔のgrub1の時のように、直接grub.cfgを書き換えることはしません )
#GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=vg1/root rd.lvm.lv=vg2/swap rhgb quiet"
GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=vg1/root rd.lvm.lv=vg2/swap rd.lvm.lv=vg2/ov ov.dev=/dev/mapper/vg2-ov rhgb quiet"
今回の環境は、もともとLVM構成で、vg1ボリュームグループのrootロジカルボリュームをルートにしていました。そこに vg2ボリュームグループのovロジカルボリュームをoverlay用の領域として指定します。( 予めロジカルボリュームを作って、xfsでフォーマットしておきます )
これは、ov.dev=/dev/mapper/vg2-ov
というパラメータ指定の部分が該当します。
なお、LVMの場合、別途ロジカルボリュームを認識するためのパラメータが必要です。( これはoverlayに関わらず、です )
インストール時に生成されているrd.lvm.lv=XX/XX
を真似て、rd.lvm.lv=vg2/ov
というパラメータも追加しておきます。
余談ですが、起動時のdracutの動きを追いたい場合は、rd.shell rd.debug log_buf_len=1M
あたりを追加しておくと、起動後にjournalctl
でログを拾うことができます。
※rd.debug
だけでも良いかも知れませんが試していません。
grub2-mkconfig
で書き換える設定ファイルですが、今回はUEFI起動するようにセットアップされている環境だったため/boot/efi/EFI/centos/grub.cfg
でした。Legacy BIOS起動の場合は/boot/grub2/grub.cfg
になると思います。
# grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
overlay化確認
設定を済ませたら再起動します。
上がってきたところでマウント状況を見てみると次のようになっています。( 一部抜粋 )
$ df -h
Filesystem Size Used Avail Use% Mounted on
ov 18G 38M 18G 1% /
devtmpfs 1.9G 0 1.9G 0% /dev
tmpfs 1.9G 0 1.9G 0% /dev/shm
tmpfs 1.9G 8.5M 1.9G 1% /run
tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup
/dev/mapper/vg2-ov 18G 38M 18G 1% /run/ov
/dev/vda2 1014M 155M 860M 16% /boot
/dev/vda1 200M 9.8M 191M 5% /boot/efi
# grep overlay /proc/mounts
ov / overlay rw,relatime,lowerdir=/run/ov/root/lower,upperdir=/run/ov/root/upper,workdir=/run/ov/root/work 0 0
overlayfsという、直接にはデバイス等と紐づかないファイルシステムであるため、今回のdracut/ovrootモジュール内で指定したov
という名前で現れます。
なお、overlayのベースとなるファイルシステムは/run/ov
にマウントした状態になっているため、そこから差分を確認することができます。
※ただし、lower,upper,workの各領域に直接書き込み削除をするのは動作保証外です。操作する場合はoverlayマウントしてない時にしてください。
次のように、色々見てみると、既にupper領域に幾つか差分が保存されていることが分かります。
# ls -ld /run/ov/root/upper/*/*
-rw-r--r-- 1 root root 629 Apr 2 17:31 /run/ov/root/upper/etc/fstab
-r--r--r--. 1 root root 33 Apr 1 14:34 /run/ov/root/upper/etc/machine-id
drwxr-xr-x. 2 root root 28 Apr 1 14:34 /run/ov/root/upper/etc/tuned
drwxr-xr-x. 7 root root 90 Apr 1 15:17 /run/ov/root/upper/var/lib
drwxr-xr-x. 4 root root 134 Apr 2 17:31 /run/ov/root/upper/var/log
drwxr-xr-x. 3 root root 21 Apr 1 14:34 /run/ov/root/upper/var/spool
drwxrwxrwt. 2 root root 6 Apr 2 17:31 /run/ov/root/upper/var/tmp
# sha1sum /var/log/boot.log /run/ov/root/{lower,upper}/var/log/boot.log
1b190f9dd15dd07a048e6562c6fe6fecfd5516b6 /var/log/boot.log
680c420a2cc8bed3ba6d660485be921f1e20db38 /run/ov/root/lower/var/log/boot.log
1b190f9dd15dd07a048e6562c6fe6fecfd5516b6 /run/ov/root/upper/var/log/boot.log
# diff /run/ov/root/lower/etc/fstab /etc/fstab
9c9
< /dev/mapper/vg1-root / xfs defaults 0 0
---
> #/dev/mapper/vg1-root / xfs defaults 0 0
その中でも、/var/log/boot.log
で試しに内容を比較してみると、lower にあるファイルを上書きする形でupperにある差分が実際のファイルとして見えていることが分かります。
fstabeについても、実装したdracut/ovrootモジュールの作用により、ルート部分がコメントアウトされていることが分かります。
なお、/.ov-XX
ファイルによる制御も試したのですが、ここでは詳細は割愛します。実際の挙動としては、次のようになっています。
- overlay化されている状態で
/.ov-XX
(/.ov-clear
等 ) をtouchする - 差分としてupper領域にファイルが生成される
- その後再起動すると、次回起動時のdracut/ovrootモジュールにより、upper領域にできた
/.ov-XX
ファイルに従って処理が行われる ( 処理が済んだら/.ov-XX
ファイルは削除される )
終わりに
というわけでまとまりがありませんが、試してみた時の一連の話を載せました。今回は試していませんが、NFS上のoverlayも、パラメータを調整すればそのままいけるんじゃないかと思います。
dracutについては、なかなかチュートリアルに使えるドキュメントが見つからず手探りだったのですが、分かってしまうと比較的容易に扱えるんではないかと思います。この記事が参考になれば幸いです。