https://tech.144lab.com/entry/2020/08/21/alpine-headless-install
この記事の内容だと失敗するケースが見つかったので改訂版としてこの記事を書きます。
AlpineLinuxはRaspberryPi用に公式イメージを配布しています。 執筆時点(2021−04−10)の最新バージョンはv3.13.4です
RaspberryPiのBIOSが持っている機能で 「FAT32のSDカードのルートに特定のファイルがあればブートできる」 というのを利用してtar.gz形式のブータブルイメージを配布しているのが特徴的です。
この配布形式の良いところはややこしい仕掛けを使わずに任意の容量のSDカードにインストールできること。数十個の合計100MB前後ファイルを書き込めばブート可能になるというお手軽さ。
さらに、そのブータブルなSDカードをバックアップしたりリストアするのも通常のファイル操作だけで行えます。
つまり、インストールもバックアップ・リストアも2〜3分でできちゃうというお手軽Linuxディストリビューションなのです。また、「ディスクレスモード」を持っており、突然の電源切りでディスクが壊れないという特徴もあります。
有線LANを使う最短のインストール方法
手元で最低限のセットアップを行ったAlpineLinux(aarch64)-v3.13.4のイメージをGitHubにおきました。以下の手順でインストールすることができます。
- マイクロSDカードをFAT32でフォーマット。
git clone https://github.com/144lab/rpi64-alpine-image.gitgit -C rpi64-alpine-image archive main | tar xv -C <マウントSDカードパス>curl https://github.com/<GitHubアカウントID>.keys > <マウントSDカードパス>/authorized_keys- マイクロSDカードを取り出してラズパイ(3or4)に挿す
- ラズパイにはDHCPでつながる有線LANケーブルを繋ぐ
- ラズパイの電源を供給開始
ラズパイのHDMI出力に以下のような表示が1〜2分後に表示されます。(確認しなくても良い)
* Starting firstboot ... [ ok ] * Starting local ... [ ok ] Welcome to Alpine Linux 3.13
以下のコマンドで同じLANに参加しているPCから接続
ssh root@raspberrypi.local
イメージを作成するユーティリティ
あらかじめ必要なツール
- go 1.16.x以降
go install github.com/144lab/rpi-alpine-installer@latest
マイクロSDカードをFAT32でフォーマットしておき、以下のコマンドを実行
curl https://github.com/<GitHubアカウントID>.keys > authorized_keys
rpi-alpine-installer -version=v3.13.4 -arch=aarch64 \
-authorized_keys=authorized_keys \
-ssid=<Wi-Fi SSID> -passphrase=<Wi-Fiパスフレーズ> \
-dist=<マウントSDカードパス>
-authorized_keysは指定があればそのファイルをrootユーザーのSSH設定に投げ込みます。-ssid指定をしない場合、有線LANを使って初期化します。指定した場合は無線LANを使って初期化します。-versionに指定可能なものはこちらで確認してください(ただし、あまり古いとWi-Fiドライバがバンドルされていないなどの問題があったりします。)-archに指定可能なのはarmv7/armhf/aarch64の3種類-dist指定を省略した場合はカレントフォルダ/distにイメージファイル群を出力します。
これで直接SDカードに書き込むやり方で「RaspberryPi 4」、「RaspberryPi Zero W」のブータブルイメージが作れることを確認しました。Zero Wの場合は-arch=armhf指定です。(Wi-Fiを設定する場合、パスフレーズが関わるのでGitHubの公開側に入れたりしてはいけません)
ヘッドレスインストールの仕組みとハマりどころ
初回起動時にだけ起動する「local」という名前のサービスをAlpine配布イメージに追加します。ディスクレスモードのためのオーバーレイファイルを初期投入することで実現しています。
headless.apkovl.tar.gzの中身
- etc/
- .default_boot_services
- local.d/
- headless.start // インストールシナリオ
- runlevels/
- default/
- local // -> headless.startへのシンボリックリンク
- default/
headless.startの中身
#!/bin/sh
__create_eni()
{
cat <<-EOF > /etc/network/interfaces
auto lo
iface lo inet loopback
auto ${iface}
iface ${iface} inet dhcp
hostname ${hostname}
EOF
}
__create_eww()
{
cat <<-EOF > /etc/wpa_supplicant/wpa_supplicant.conf
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=JP
network={
ssid="${ssid}"
psk="${psk}"
}
EOF
}
__edit_ess()
{
cat <<-EOF >> /etc/ssh/sshd_config
PermitRootLogin yes
IPQoS 0x00
EOF
}
__find_wint()
{
for dev in /sys/class/net/*
do
if [ -e "${dev}"/wireless -o -e "${dev}"/phy80211 ]
then
echo "${dev##*/}"
fi
done
}
ovlpath=$(find /media -name *.apkovl.tar.gz -exec dirname {} \; | head -n1)
read ssid psk < "${ovlpath}/wifi.txt"
if [ ${ssid} ]
then
iface=$(__find_wint)
apk add wpa_supplicant
__create_eww
rc-service wpa_supplicant start
else
iface="eth0"
fi
hostname=raspberrypi
setup-hostname ${hostname}
hostname ${hostname}
__create_eni
rc-service networking start
/sbin/setup-sshd -c openssh
__edit_ess
install -m 700 -d /root/.ssh
install -m 600 /dev/null /root/.ssh/authorized_keys
if [ -e "${ovlpath}/authorized_keys" ]
then
cat "${ovlpath}/authorized_keys" >> /root/.ssh/authorized_keys
fi
rc-service sshd restart
setup-ntp -c chrony
chronyc -a makestep
setup-apkrepos -1
setup-lbu mmcblk0p1
setup-apkcache /media/mmcblk0p1/cache
apk add rng-tools dbus avahi
rc-update add rngd boot
rc-update add wpa_supplicant boot
rc-update add urandom boot
rc-update add dbus
rc-update add avahi-daemon
rc-service rngd start
rc-service wpa_supplicant start
rc-service urandom start
rc-service dbus start
rc-service avahi-daemon start
mount -o remount,rw ${ovlpath}
rm ${ovlpath}/*.apkovl.tar.gz
rm ${ovlpath}/wifi.txt
rm ${ovlpath}/authorized_keys
rc-update del local default
lbu add /root/.ssh/authorized_keys
lbu commit -d
このシナリオの大まかな手順は以下の通り
- hostnameに「raspberrypi」をセット
- ネットワーキングをスタート
- sshdをセットアップ
- sshd_configにいくつか設定を追加
- SDカードにauthorized_keysファイルがあればその内容を/root/.ssh/authorized_keysに追記
- sshdをリスタート
- ntp同期デーモン(chirony)のセットアップ
- 強制時刻同期(chronyc -a makestep)
- apkリポジトリURLの設定とインデックス更新
- lbu(ディスクレスモード)のセットアップ
- apk-cacheのセットアップ
- Wi-Fi、avahi関連パッケージのインストール
- 各種デーモンの自動起動設定
- 各種サービスの開始
- SDカードを読み書き可能にして再マウント
- 初回起動用ファイルの削除
- 「local」サービス(このシナリオ)の自動起動無効化
- /root/.ssh/authorized_keysをlbuに追加
- lbu commitにてこれまでの構成変更を永続化
この中で原案のシナリオにも対策がなかったハマりどころは、ラズパイの時計があってなくてルート証明書の有効期間の範囲外のためにHTTPS通信に失敗するという現象。 それまでうまく動いたり動かなかったりがあったんですが強制時刻同期を追加することでやっと安定しました。
もう一つのはまりどころは以下の記述でheadコマンドへのパイプを追加しました。
ovlpath=$(find /media -name *.apkovl.tar.gz -exec dirname {} \; | head -n1)
これなしの時、SDカードの「headless.apkovl.tar.gz」を削除とかするとSDカードルートに「.Trush」フォルダが作られそこに一時保管されます。消し忘れにしろovlpathが意図する単一のパスにならなくなっていてうまく動かなくなるということもありました。
イメージ作成ユーティリティは以下のファイルセットを指定フォルダに出力するだけです