AlpineLinuxをRaspberryPiに簡単インストール

f:id:irieda:20210412144834p:plain 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/

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が意図する単一のパスにならなくなっていてうまく動かなくなるということもありました。

イメージ作成ユーティリティは以下のファイルセットを指定フォルダに出力するだけです

  • Alpineイメージ解凍ファイル群(元のアーカイブは該当の外部URLから自動ダウンロードします)
  • headless.apkovl.tar.gz(インストールシナリオが入ったオーバーレイファイル)
  • wifi.txt(ssidパスフレーズ
  • authorized_keys(ssh用公開鍵一覧)