livaの雑記帳

alpine linuxのminirootfsからqemuで起動する話

小さくて便利で皆大好きalpine linux

docker触ってたら一度くらい使った事ありますよね?(個人の意見です)

このalpine linuxではminirootfsというものが提供されています。これはalpineのファイルシステムをtar.gzに圧縮して固めたもので、公式サイトいわく、コンテナやchrootする時に使う事を想定されているようです。

このminirootfsは2MBしかなく、数十MBもあるインストールISOと違って、ものすごく軽量です。これなら、この軽量なminirootfsを使ってファイルシステムを構築すれば軽量なんじゃないかと思い、(ちょうど軽量なLinux環境をqemuで立ち上げたかったので)やってみました、というのが今回の話です。

別にminirootfsとか使わなくても、これとか使ってVMイメージを作れば十分軽量なんじゃないかと言われればそれまでなんですけどね・・・。
 

 

kernelを準備する

minirootfsにはカーネルが無いので、適当なバージョンのソースコードを引っ張ってきて、ビルドしましょう。僕はモジュールとかを適切にrootfsに展開するのが面倒になり、全てkernelにstatic buildしてしまいました。

ここで得られたbzImageをqemuのオプションで渡します。

 

qemuでどうやって起動するか

qemuからはこんな感じで起動しようと思います。

 

$ qemu-system-x86_64 -kernel bzImage -initrd rootfs -append "root=/dev/ram rdinit=/bin/sh console=ttyS0,115200" -net nic -net user,hostfwd=tcp::2222-:22 -serial stdio -display none

 

-kernelオプションで先程のbzImageを指定、initramfsとしてrootfs(minirootfs)を指定、出力はシリアルポート、initramfsで起動したまま、シェルを立ち上げる(カーネルパラメータオプションの rdinit=/bin/sh)、みたいなのを想定しています。

 

initramfsをrootとしてしまうのは完全に手抜き(別にinitramfsでの起動でも僕が本当にやりたい事は達成できるので)です。この上でファイルシステムに何か書き込んでも、当然qemuを終了したら吹き飛びます。まあ気になる人はちゃんとハードディスクイメージにするか、適当にNFSマウントでもすれば良いのではないでしょうか。

 

とりあえずこれをやるために必要な事として、rootfsを準備しなきゃいけません。次からこれを準備していきます。

 

rootfsの準備

initramfsのファイルフォーマットはcpioです。minirootfsはtar.gzで落ちてくるので、展開した後、cpioにしなければいけません。

そして展開しようとすると、デバイスファイルが展開できなくてこけます。

 

なので、fakerootを使いましょう。

 

$ fakeroot && tar xf ../alpine-minirootfs-3.8.0-x86_64.tar.gz && 

find | cpio --quiet -o -H newc | gzip -9 > ../rootfs

tarでカレントディレクトリにminirootfsを展開した後、cpioコマンドで固め、gzip圧縮しています。

 

確かこれだけで/bin/shは起動するはず

 

initを起動したい

シェルが立っただけだと、何もサービスが使えないので、使い物にならないです。わかりやすい例で言えばネットワークとか。

というわけで、initスクリプトを起動しましょう。

 

$ qemu-system-x86_64 -kernel bzImage -initrd rootfs -append "root=/dev/ram rdinit=/sbin/init console=ttyS0,115200" -net nic -net user,hostfwd=tcp::2222-:22 -serial stdio -display none

rdinit=/sbin/initにすると、initスクリプトが起動します。

 

これで起動すると、以下のようなエラーが出てしまいます。シェルすら出ません。辛い。

can't open /dev/tty1: No such file or directory

 

/dev/tty1というデバイスファイルがない、というエラーですね。

minirootfs内の/etc/inittabには以下のような行があり、ここで/dev/tty1を参照していますが、その時までに/dev/tty1が作成されていないため、このようなエラーが出るわけです。

tty1::respawn:/sbin/getty 38400 tty1

 

バイスファイルを準備したい


/dev/tty1を生やす簡単な方法を探すと、こんなのが出てきます。

 

kernhack.hatenablog.com

 

initramfsのinitと同様に、以下のようなコマンドを実行すればデバイスファイルやその他諸々が整備されるはずです。

mount -t proc proc /proc -o nosuid,noexec,nodev
mount -t sysfs sys /sys -o nosuid,noexec,nodev
mount -t devtmpfs dev /dev -o mode=0755,nosuid
mount -t tmpfs run /run -o nosuid,nodev,mode=0755


なので例えばこんな感じの事を/etc/inittabの最初でやれば、解決します。

 

::sysinit:mount -t proc proc /proc -o nosuid,noexec,nodev

 

最初はこれで解決していたのですが、途中でもっと綺麗なやり方がある事に気づく事になります。ひとまずこれで解決した、という体で話を進めさせてください。

 

そもそも/etc/inittabの編集ってどうやれば良いんだ!という方もいらっしゃるかもしませんが、それはtarで展開した後、/etc/inittabで編集し、cpioに食わせれば良いだけですよ。

 

シリアルでログイン画面が出てこない。

これだけだと、エラーは出なくなりますが、まだログイン画面には到達できません。/etc/inittabでは以下の行がコメントアウトされているためです。

 

ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100

 

まあそりゃ今はシリアルで画面出力しているから、シリアルコンソールに対応するデバイスファイルを設定しなきゃ駄目ですね。

 

ログインしたい

ログイン画面出ただけではどうしようもないので、ユーザー作りましょう。

 

cpioに固める前に、アカウント情報をファイルシステム上に乗っけます。(cpioに固めた後はファイルシステムに手出しできないので。ログインもできてないですし)

minirootfsの展開先にchrootし、minirootfs上のadduserコマンドでユーザーを作れば、minirootfs上にアカウント情報が作成されます。

$ chroot . addgroup alpine

$ chroot . adduser -S -s /bin/sh -G alpine alpine

$ chroot . addgroup alpine wheel

$ chroot . sh -c "echo '%wheel ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers"

 

alpineユーザーを作り、ついでにwheelにも追加します。wheelはパスワード無しでsudoできるようにします。

 

sudoするためにはminirootfs上にsudoコマンドが無いといけないので、sudoをapkでインストールします。僕はsshサーバーを立てたかったので、ついでにdropbearと、dropbearを自動起動するためにopenrcも入れました。

 

$ chroot . apk add --no-cache --initdb sudo dropbear openrc

 

これだけで終わりと言いたい所ですが、手元だとpermissionエラーでログインできませんでした。minirootfsを展開後に、/(minirootfsのroot)を755に設定する必要があるようです。

 

/etc/network/interfacesの設定

これで無事ログインできたのですが、"rc-service dropbear start"ってしてdropbearを起動しようとすると、「/etc/network/interfacesがナイヨ」と怒られます。

 

というわけで、/etc/network/interfacesを整備しましょう。

 

auto lo

iface lo inet loopback

 

auto eth0

     iface eth0 inet dhcp

こうすると、ifconfigでloとeth0が見えるようになります。でも相変わらず通信できません。ていうか、それ以前にdropbearを起動する事でnetworkingが起動するってどういう事よ、最初からnetworkingは起動しててよ、みたいな気になりますよね。

 

openrcの設定

なので、openrcがinitを自動起動してくれるようにします。

$ chroot . rc-update add dropbear default

$ chroot . rc-update add networking boot

これが上手く動けば良いんですが、後者はなぜかSEGVして動きませんでした。コードを読むと、bootの時だけ何か特殊な処理が走るっぽいんですよね。その中でコケてるらしい。もしかしたらdocker環境上で実行したのが悪かったのかもしれません。

 

で、コードいわく、こいつはシンボリックリンクを貼る以上の事をやってなさそうなので、自分でやります。こんな感じ。

 

$ chroot . ln -s /etc/init.d/networking /etc/runlevels/boot

 

/etc/runlevels/bootは存在しないので、事前に作っておく必要があります。

 

なんかnetworkingとdropbearの設定をするんだったら、ついでに他のサービスも全部立てとこうぜ、みたいな気持ちになったので、alpineのVMイメージの設定と比較して、全部設定する事にしました。

 

$ chroot . install -d /etc/runlevels/boot /etc/runlevels/default /etc/runlevels/sysinit /etc/runlevels/shutdown /etc/runlevels/nonetwork

$ chroot . ln -s /etc/init.d/acpid /etc/runlevels/default

$ chroot . ln -s /etc/init.d/bootmisc /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/crond /etc/runlevels/default

$ chroot . ln -s /etc/init.d/devfs /etc/runlevels/sysinit

$ chroot . ln -s /etc/init.d/dmesg /etc/runlevels/sysinit

$ chroot . ln -s /etc/init.d/dropbear /etc/runlevels/default

$ chroot . ln -s /etc/init.d/hostname /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/hwclock /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/hwdrivers /etc/runlevels/sysinit

$ chroot . ln -s /etc/init.d/killprocs /etc/runlevels/shutdown

$ chroot . ln -s /etc/init.d/loadkmap /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/mdev /etc/runlevels/sysinit

$ chroot . ln -s /etc/init.d/modules /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/mount-ro /etc/runlevels/shutdown

$ chroot . ln -s /etc/init.d/networking /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/savecache /etc/runlevels/shutdown

$ chroot . ln -s /etc/init.d/swap /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/sysctl /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/syslog /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/urandom /etc/runlevels/boot

 

※これは最終系のコードで、後の話でapkによってインストールされるサービスも登場しています。

 

これによって、devfsサービスが自動起動するようになり(/etc/inittabの最初の、"::sysinit:/sbin/openrc sysinit"の中で起動される)、先程のmount文は不要になりました。

 

アドレスが割当らない

さて、これでdropbearを手動で起動しなくても良くなりましたし、networkingも最初から起動するようになりました。でも相変わらずssh接続ができません。ネットワークが死んでます。


しかもこれ、何か変なんですよね。ifconfig走らせるとIPv6アドレスは何か割当たってるっぽいし、udhcpc走らせるとちゃんとIPアドレスは振ってくる。でもifconfigにinet addr(IPv4アドレス)は割当らないみたいな。

なんでじゃなんでじゃって言いながら調べてたら、これが見つかり、「ふんふん、busyboxがudhcpcのサンプルスクリプトを提供してるのか、でもminirootfsには無いな」となり、じゃあサンプルスクリプトコピペするか、なんて思いながらググってたらこんなのを見つけました。

 

Alpine Linux packages

 

どうやらbusybox-initscriptsをインストールすると、/usr/share/udhcpc/default.scriptが手に入るらしい!!

 

$ chroot . apk add --no-cache --initdb busybox-initscripts

 

ちゃんとifconfigするとIPv4アドレスも割り当たるようになったし、apkでパッケージもダウンロードできるようになりました。めでたしめでたし。