autoinstallでUbuntu Server 24.04を入れるための備忘録

# モチベーション
kubeadmでKubernetesクラスタ建ててぇなァ～って思っても、自前Proxmox環境に毎回Ubuntuを建てるのがだるい。
ということで、autoinstallを使ってUbuntu VM作成をKubeadmでコンテナ環境をデプロイする手前まで一気に進めてみる。
~~VagrantとかTerraformとかは知らん。~~

# TL;DR

1. [`user-data`を作る](#user-dataを作る)
    <details><summary>kubeadmを実行できる直前までのサンプル</summary>
    
    ```yaml
    #cloud-config
    autoinstall:
      version: 1
      identity:
        hostname: autoinstall-test
        password: (snip)
        username: thrust2799
        realname: 'thrust2799'
      network:
        version: 2
        ethernets:
          ens18:
            dhcp4: false
            addresses:
              - 192.168.100.1/24
            nameservers:
              addresses:
                - 192.168.100.254
              search:
                - mylab.home
          ens19:
            dhcp4: true
          ens20:
            addresses:
              - 172.16.1.1/24
      storage:
        config:
          - type: disk
            id: disk-0
            name: ''
            match:
              size: smallest
            ptable: gpt
            wipe: superblock
            preserve: false
          - type: partition
            id: partition-0
            device: disk-0
            number: 1
            size: 512M
            flag: boot
            preserve: false
            grub_device: true
          - type: partition
            id: partition-1
            device: disk-0
            number: 2
            size: 1G
            flag: ''
            preserve: false
            grub_device: false
          - type: partition
            id: partition-2
            device: disk-0
            number: 3
            size: -1
            flag: ''
            preserve: false
            grub_device: false
          - type: lvm_volgroup
            id: lvm_volgroup-0
            name: ubuntu-vg
            devices:
              - partition-2
            preserve: false
          - type: lvm_partition
            id: lvm_partition-0
            name: ubuntu-lv
            volgroup: lvm_volgroup-0
            size: -1
            sipe: superblock
            preserve: false
          - type: format
            id: format-0
            volume: partition-0
            fstype: fat32
            label: ESP
          - type: format
            id: format-1
            volume: partition-1
            fstype: ext4
            label: BOOT
          - type: format
            id: format-2
            volume: lvm_partition-0
            fstype: ext4
            label: ROOT
          - type: mount
            id: mount-0
            device: format-0
            path: /boot/efi
          - type: mount
            id: mount-1
            device: format-1
            path: /boot
          - type: mount
            id: mount-2
            device: format-2
            path: /
        swap:
          size: 0
      locale: "en_US.UTF-8"
      keyboard:
        layout: "jp"
      timezone: "Asia/Tokyo"
      ssh:
        install-server: true
        authorized-keys: 
          - ssh-ed25519 (snip)
        allow-pw: false
      debconf-selections: |
        openssh-server openssh-server/permit-root-login boolean false
      packages:
        - curl
        - ca-certificates
        - gpg
        - apt-transport-https
      apt:
        preserve_source_list: false
        mirror-selection:
          primary:
            - country-mirror
        geoip: true
      updates: all
      shutdown: reboot
      late-commands:
        - echo overlay | tee -a /target/etc/modules-load.d/kubernetes.conf > /dev/null
        - echo br_netfilter | tee -a /target/etc/modules-load.d/kubernetes.conf > /dev/null
        - echo net.bridge.bridge-nf-call-iptables = 1 | tee -a /target/etc/sysctl.d/kubernetes.conf > /dev/null
        - echo net.bridge.bridge-nf-call-ip6tables = 1 | tee -a /target/etc/sysctl.d/kubernetes.conf > /dev/null
        - echo net.ipv4.ip_forward = 1 | tee -a /target/etc/sysctl.d/kubernetes.conf > /dev/null
        - curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /target/etc/apt/keyrings/docker.asc
        - curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | gpg --dearmor -o /target/etc/apt/keyrings/kubernetes.gpg
        - chmod a+r /target/etc/apt/keyrings/docker.asc
        - echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable"
          | tee /target/etc/apt/sources.list.d/docker.list > /dev/null
        - echo "deb [signed-by=/etc/apt/keyrings/kubernetes.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb /"
          | tee /target/etc/apt/sources.list.d/kubernetes.list > /dev/null
        - curtin in-target --target=/target -- apt update
        - curtin in-target --target=/target -- apt upgrade -y
        - curtin in-target --target=/target -- apt install -y containerd.io kubelet kubeadm kubectl
        - curtin in-target --target=/target -- apt-mark hold kubelet kubeadm kubectl
        - curtin in-target --target=/target -- containerd config default | tee /target/etc/containerd/config.toml > /dev/null
        - echo "KUBELET_EXTRA_ARGS=--cgroup-driver=systemd" | tee -a /target/etc/default/kubelet > /dev/null
        - sed -ie "s/SystemdCgroup = false/SystemdCgroup = true/" /target/etc/containerd/config.toml
    ```
    </details>

1. [httpで`user-data`を配信できるようにする](#httpでuser-dataを配信できるようにする)
1. [Ubuntu ServerのインストーラのGrubで起動パラメータをいじる](#ubuntu-serverのインストーラのgrubで起動パラメータをいじる)
   ```
   linux /casper/vmlinuz autoinstall ip=192.168.100.1:::255.255.255.0::ens18:off ds=nocloud-net\;s=http://192.168.100.253/autoinstall-test
   ```

# autoinstallとは
Ubuntuをインストールするときに使える自動化の仕組みとして「autoinstall」が用意されている。
RedHat系でいうところの「Kickstart」に相当する。

Ubuntu Server 20.04以降、およびUbuntu Desktop 23.04以降でサポートされている。
Cloud-Initの仕組みを利用してインストールデータを与えられるほか、インストールメディアに同梱して読み込むこともできる。

https://canonical-subiquity.readthedocs-hosted.com/en/latest/intro-to-autoinstall.html

今回インストールメディアは特に加工せず、Cloud-Initの仕組みを利用して進める。

::: note warn
なお、Cloud-InitやYamlの仕様を概ね理解している前提とする。
:::

# `user-data`を作る

## 前提

- インストール対象のホスト
    - NIC1 / ens18: Cloud-Initが使えるNW、静的にアドレス割当て(`192.168.100.1/24`とする)
    - NIC2 / ens19: インターネット向けのNW、DHCPでアドレス自動割当て
    - NIC3 / ens20: 内部のテキトーNW
- Cloud-Init用のホスト
    - Cloud-Initが使えるNWに接続(`192.168.100.253/24`とする)
- Gateway
    - `192.168.100.254/24`とする
- インターネット向けのNW
    - OSのアップデートやパッケージ取得などのために用意しておく

## 大まかな構造

今回作った`user-data`大体は次の通り。
`#cloud-config`から`version: 1`までは固定で冒頭に含める。
`autoconfig`は唯一のトップレベルなキー要素となる。

```yaml
#cloud-config
autoinstall:
  version: 1
  identity:
    # ホスト系の設定
  network:
    # Netplanの設定
  storage:
    # インストール先ディスクの設定
  locale: # ロケール
  keyboard:
    # キーボード設定
  timezone: # タイムゾーン設定
  ssh:
    # `identity`で規定するユーザ用のSSH設定
  debconf-selections:
    # debconf関連
  packages:
    # 追加パッケージ設定
  apt:
    # aptリポジトリ設定
  updates: # インストール後のアップデート設定
  shutdown: # インストール完了後の動作
  late-commands:
    # その他、インストール完了後に併せて実施するコマンド群
```

## `identity`要素
この要素へはホスト系の設定を記載する。

```yaml
    hostname: autoinstall-test
    password: (snip)
    username: thrust2799
    realname: 'thrust2799'
```

`hostname`はインストールするUbuntuのホスト名。
`password`と`username`は初期ユーザとして設定する項目で、特に`password`は平文でなくshadowで投入する必要がある。（セキュア）
`realname`はオプションで、初期ユーザの本名を入れたりする。

shadow生成方法はいろいろあるが、今回はOpenSSLを利用する方法で生成する。

```shell-session
$ openssl password -6 -salt=<random string> <password>
```

## `network`要素
この要素へはNetplanの設定を記載する。
NetplanやCloud-Init(`network-config`)と同じ内容になる。

```yaml
  network:
    version: 2
    ethernets:
      ens18:
        dhcp4: false
        addresses:
          - 192.168.100.1/24
        nameservers:
          addresses:
            - 192.168.100.254
          search:
            - mylab.home
      ens19:
        dhcp4: true
      ens20:
        addresses:
          - 172.16.1.1/24
```

このあたり、本当はあまりよろしくないが自前環境では確実に`ens18`から始まることが分かっているので、それを前提にProxmox VE側と設定を合わせている。

::: note info
IF名は基本的にudevで制御されるので、たとえ物理IFだったとしてもPCIスロット番号やポート番号からある程度の予想ができる。また、その他の仮想化環境上のVMでもIF名の法則性が見られることもある。
:::

::: note warn
仮想化環境のバージョンアップやLinuxカーネルのバージョンアップでNICの命名規則が変わることもあるため、本当に設定を固定したいときはMACアドレスを利用してIF名を固定すると良い。
:::

## `storage`要素
この要素はインストール先ディスクの設定を記載する。
省略した場合はデフォルト動作が実行されるが、複数のディスクがある場合や大容量のディスクをすべて使い切りたい場合などは個別に設定する。

今回は2本のディスクをアタッチしている状態なので、詳細なパーティション構成も含めて設定していく。
なお、おおらかに設定したいときは`config:`の代わりに`layout:`が使える。
※詳細は[リファレンス参照](https://canonical-subiquity.readthedocs-hosted.com/en/latest/reference/autoinstall-reference.html#storage)。

<details><summary>長すぎるので折り畳み</summary>
    
```yaml
  storage:
    config:
      - type: disk
        id: disk-0
        name: ''
        match:
          size: smallest
        ptable: gpt
        wipe: superblock
        preserve: false
      - type: partition
        id: partition-0
        device: disk-0
        number: 1
        size: 512M
        flag: boot
        preserve: false
        grub_device: true
      - type: partition
        id: partition-1
        device: disk-0
        number: 2
        size: 1G
        flag: ''
        preserve: false
        grub_device: false
      - type: partition
        id: partition-2
        device: disk-0
        number: 3
        size: -1
        flag: ''
        preserve: false
        grub_device: false
      - type: lvm_volgroup
        id: lvm_volgroup-0
        name: ubuntu-vg
        devices:
          - partition-2
        preserve: false
      - type: lvm_partition
        id: lvm_partition-0
        name: ubuntu-lv
        volgroup: lvm_volgroup-0
        size: -1
        sipe: superblock
        preserve: false
      - type: format
        id: format-0
        volume: partition-0
        fstype: fat32
        label: ESP
      - type: format
        id: format-1
        volume: partition-1
        fstype: ext4
        label: BOOT
      - type: format
        id: format-2
        volume: lvm_partition-0
        fstype: ext4
        label: ROOT
      - type: mount
        id: mount-0
        device: format-0
        path: /boot/efi
      - type: mount
        id: mount-1
        device: format-1
        path: /boot
      - type: mount
        id: mount-2
        device: format-2
        path: /
    swap:
      size: 0
```
</details>

### `type: disk`：物理ディスク設定
`match:`でどのディスクを対象にするか制御できるが、今回は2台のみなので小さいほう(`smallest`)を対象にする。
GPTディスクとして使用する。
```yaml
      - type: disk
        id: disk-0
        name: ''
        match:
          size: smallest
        ptable: gpt
        wipe: superblock
        preserve: false
```

### `type: partition`：パーティション設定
`device:`で対象のディスクIDを設定し、パーティション番号とサイズ、その他設定を記載する。
`size:`には`fdisk`で使うような補助記号(`M`や`G`など)が使えるほか、末尾のパーティションのみ`-1`で最大容量の割当てができる。
`flag:`にはパーティションに与える起動ディスクフラグなどを設定する。空の時は`''`を設定する。
```yaml
      - type: partition
        id: partition-0
        device: disk-0
        number: 1
        size: 512M
        flag: boot
        preserve: false
        grub_device: true
```

### `type: lvm_volgroup`：LVM VG設定
`devices:`でVGに含めるパーティションをリスト形式で追加する。
```yaml
      - type: lvm_volgroup
        id: lvm_volgroup-0
        name: ubuntu-vg
        devices:
          - partition-2
        preserve: false
```

### `type: lvm_partition：LVM LV設定
`volgroup:`でLVとして切り出すVGを記載する。
`size:`の考え方はパーティション設定と同じ。
```yaml
      - type: lvm_partition
        id: lvm_partition-0
        name: ubuntu-lv
        volgroup: lvm_volgroup-0
        size: -1
        wipe: superblock
        preserve: false
```

### `type: format`：ファイルシステム設定
`volume:`に設定対象のパーティションを記載する。`lvm_partition`も設定可能。
`fstype:`にはフォーマット形式のファイスシステム名を記載する。(EFIなら`fat32`固定)
`label:`でラベルを付けることもできる。
```yaml
      - type: format
        id: format-0
        volume: partition-0
        fstype: fat32
        label: ESP
```

### `type: mount``：マウントパス設定
`device:`でマウント対象のファイルシステムを記載する。
`path:`にはマウント先のディレクトリを記載する。
```yaml
      - type: mount
        id: mount-0
        device: format-0
        path: /boot/efi
```

::: note warn
マウントオプションの指定はないっぽい。特殊なことがしたい場合は`late-command`で`/etc/fstab`をいじる方が良さそう？
:::

## その他の主要設定
ロケール設定、キーボード設定、タイムゾーン設定、SSH設定を記載している。
SSH設定については、サーバをインストールした上で初期ユーザへのログインに使用する公開鍵を登録しパスワードログインを無効化している。
また、`debconf-selections:`でRootユーザのログインを無効している。

```yaml
  locale: "en_US.UTF-8"
  keyboard:
    layout: "jp"
  timezone: "Asia/Tokyo"
  ssh:
    install-server: true
    authorized-keys: 
      - (snip)
    allow-pw: false
  debconf-selections: |
    openssh-server openssh-server/permit-root-login boolean false
```

## パッケージ関連設定
aptの関連設定と、追加で入れる設定だったり更新の設定だったりを記載する。
追加パッケージについては`packages:`へリスト形式で指定する。(今回はKubernetesを入れるために必要とされるものを記載している。)

`apt:`にはリポジトリ設定を入れているが、正直ここはデフォルトでも良さそう。
プライベートミラーや追加リポジトリがある場合はここに記載するか`late-command`で設定する。

`updates:`はインストール後の更新有無を記載する。
`security`だとセキュリティパッチのみ、`all`だとすべて更新となる。

```yaml
  packages:
    - curl
    - ca-certificates
    - gpg
    - apt-transport-https
  apt:
    preserve_source_list: false
    mirror-selection:
      primary:
        - country-mirror
    geoip: true
  updates: all
```

## インストール後の挙動について

`shutdown:`ですべての処理が完了した後の動作を記載する。
`poweroff`にするとシャットダウンして電源を切る動作となる。
デフォルトは`reboot`(再起動)。

`late-command`はインストール完了後にユーザ指定で実行するコマンドリストを与える。

```yaml
  shutdown: reboot
  late-commands:
    - echo overlay | tee -a /target/etc/modules-load.d/kubernetes.conf > /dev/null
    - echo br_netfilter | tee -a /target/etc/modules-load.d/kubernetes.conf > /dev/null
    - echo net.bridge.bridge-nf-call-iptables = 1 | tee -a /target/etc/sysctl.d/kubernetes.conf > /dev/null
    - echo net.bridge.bridge-nf-call-ip6tables = 1 | tee -a /target/etc/sysctl.d/kubernetes.conf > /dev/null
    - echo net.ipv4.ip_forward = 1 | tee -a /target/etc/sysctl.d/kubernetes.conf > /dev/null
    - curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /target/etc/apt/keyrings/docker.asc
    - curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | gpg --dearmor -o /target/etc/apt/keyrings/kubernetes.gpg
    - chmod a+r /target/etc/apt/keyrings/docker.asc
    - echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable"
      | tee /target/etc/apt/sources.list.d/docker.list > /dev/null
    - echo "deb [signed-by=/etc/apt/keyrings/kubernetes.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb /"
      | tee /target/etc/apt/sources.list.d/kubernetes.list > /dev/null
    - curtin in-target --target=/target -- apt update
    - curtin in-target --target=/target -- apt upgrade -y
    - curtin in-target --target=/target -- apt install -y containerd.io kubelet kubeadm kubectl
    - curtin in-target --target=/target -- apt-mark hold kubelet kubeadm kubectl
    - curtin in-target --target=/target -- containerd config default | tee /target/etc/containerd/config.toml > /dev/null
    - echo "KUBELET_EXTRA_ARGS=--cgroup-driver=systemd" | tee -a /target/etc/default/kubelet > /dev/null
    - sed -ie "s/SystemdCgroup = false/SystemdCgroup = true/" /target/etc/containerd/config.toml
```

### `late-command`について
インストーラ環境からコマンド実行する内容を記載する。
基本的にインストーラのシェルで実行可能な内容のみが指定できるので、追加パッケージが必要なものはインストールした環境に入って(chrootして)から実行すると良い。

::: note info
`user-data`要素を与えることで、再起動後のCloud-Init動作を制御することができる。
こちらで追加パッケージに関連する動作を入れても良いかもしれない。
:::

インストールした環境でコマンドを動かす際は、動かしたいコマンドの先頭に下記を追加する。

```yaml
  - curtin in-target --target=/target -- <command>
```

## `user-data`に問題がないか（最低限）確認する

これを参考にVlidationスクリプトを用意する。

https://github.com/canonical/subiquity

依存パッケージの導入まで完了したら、`./scripts/validate-autoinstall-user-data.py`にファイルを渡して問題がないか確認する。

```shell-session
$ ./scripts/validate-autoinstall-user-data.py ~/manifests/autoinstall-test/user-data 
Success: The provided autoinstall config validated successfully
```

:::note warn
ここでの確認は構文チェックや必須条件を満たすかの確認にとどまる。
`late-command`に不備があったり`network`要素に誤りがあっても検知してくれないので注意する。（2敗）
:::

# httpで`user-data`を配信できるようにする

作成した`user-data`ファイルをUbuntuのインストーラからアクセスできるようにする。
いろいろ選択肢はあるけど、今回はDocker ComposeでHTTPサーバを建てて配信する方式を採用した。

## 用意したファイル

### compose.yml
```yml
name: nginx-autoinstall
services:
  nginx:
    build:
      context: .
      dockerfile: dockerfile
    volumes:
      - ./manifests:/webroot # `./manifests/<hostname>/user-data`の構造で格納する
      - ./nginx.conf:/etc/nginx/conf.d/nginx.conf # Nginxの設定
    ports:
      - "80:8080"
```

### dockerfile
```dockerfile
FROM nginxinc/nginx-unprivileged:stable-alpine # rootless nginx

EXPOSE 8080
USER nginx
```

### nginx.conf
```conf
server {
    listen  8080;
    server_name 192.168.100.253;

    location / {
        root    /webroot;
        index   index.html;
    }
}

```

# Ubuntu ServerのインストーラのGrubで起動パラメータをいじる
Ubuntu ServerのISOからOSをインストールする。
この時、`cmdline`としてCloud-Initを動作させるためのパラメータを渡す必要がある。

## `cmdline`変更方法

まずは通常通り起動し、Grubのメニュー表示を待つ。
↓この画面になったら、`Try or Install Ubuntu Server`を選択した状態で「E」キーを押下する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3535545/d8e85dc9-5b92-47df-b6d2-8853d4fd7ad7.png)

するとテキストエディタが表示されるので、`linux`から始まる行の末尾を編集する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3535545/d12794f5-29f4-481d-afcf-79751f3bc6f9.png)

## `cmdline`に与える情報

最終的にはこれを与える。

```
linux /casper/vmlinuz autoinstall ip=192.168.100.1:::255.255.255.0::ens18:off ds=nocloud-net\;s=http://192.168.100.253/autoinstall-test
```
- `linux /casper/vmlinuz`
    - 起動するvmlinuzの指定
- `autoinstall`
    - autoinstallを有効にする
- `ip=192.168.100.1:::255.255.255.0::ens18:off`
    - ホストのIPアドレス設定する
    - ens18に対して静的に`192.168.100.1/24`を割当て
- `ds=nocloud-net\;s=http://192.168.100.253/autoinstall-test/`
    - Cloud-Initの動作設定
        - `nocloud-net`環境での動作
        - `http://192.168.100.253/autoinstall-test/`に`user-data`などを取りに行く
            - 先に設定したnginxコンテナの接続先を指定する
        - セミコロン(`;`)はエスケープする

# 最後に
これでautoinstallによりUbuntu Server 24.04が自動インストールされる。
再起動が完了すればSSH接続もできるようになるので、煩わしいインストール作業から解放され快適なUbuntu Server構築が実現した。
あとはkubeadmを叩くだけでKubernetesクラスタができる。