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.git
git -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が意図する単一のパスにならなくなっていてうまく動かなくなるということもありました。
イメージ作成ユーティリティは以下のファイルセットを指定フォルダに出力するだけです