Fedora CoreOSや色々なディストリビューションをnetboot.xyzでディスクレスブート

#### 構成
* dnsmasq Proxy DHCPで構成
* [matchbox](https://github.com/poseidon/matchbox)
* [NETBOOT.XYZ](https://netboot.xyz/)
* dnsmasq, matchbox, netboot.xyz はdocker-composeで起動
* CentOS 7
* KVM と Dockerが混在

#### こんな感じで起動

CoreOSの場合
![PXE_FedoraCoreOS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124921/49d7ae5c-83c1-9e51-95fd-89984979cc4d.png)

それ以外の場合は NETBOOT.XYZ でブートメニュー出す
![PXE_NETBOOT_XYZ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124921/159a022d-ce46-76f1-8d70-29c43cce591f.png)

#### [matchbox](https://github.com/poseidon/matchbox)
CoreOSの起動に必要なIgnitionファイルやカーネルパラメータをホストしてくれるやつです。
PXEブートする際に、MACアドレスやアーキテクチャによって起動するイメージを変えたくなると思うのですが、
matchboxの仕様に沿ってセットアップすると仕組みを自前で用意するより楽になります。

#### [netboot.xyz](https://netboot.xyz/)
LinuxディストリビューションのインストールにはDVD、USBメディアを用意する事が多いですが、メディアの用意が必要です。　
ネットワークインストールの仕組みで解決できるのですが、OSのバージョンアップごとにPXE用のメニューファイルの更新も必要です。

PXEブートのメニューファイルを各ディストリビューションのアップデートに追随してくれるのが netboot.xyz

※ netboot.xyz は、デフォルトで、カーネルイメージやinitramfsをインターネットの向こう側から取得するようです
　非常に遅いので、管理画面から事前にファイルを取得しておくことをオススメします。

#### docker-compose用のディレクトリ構成
docker-compose.ymlが置いてあるディレクトリに containers というディレクトリを作り、
配下に各コンテナ用サブディレクトリを作り管理。

matchboxの動作に必要なファイルはDockerfileでCOPYではなく、
docker-compose.yml の `volumes:` でマウントしています。


```
.
├── README.md
├── containers
│   ├── dnsmasq
│   │   ├── Dockerfile
│   │   ├── dnsmasq.conf
│   │   └── tftpboot
│   ├── matchbox
│   │   ├── etc
│   │   └── var
└── docker-compose.yml
```

#### docker-compose.yaml
dnsmasqはAlpineLinuxからDockerfileでビルド、
matchbox、netbootxyzはイメージをそのまま使わせてもらい、必要なディレクトリはマウント。

```
version: '3'

services:
  dnsmasq:
    container_name: dnsmasq
    build:
      context: containers/dnsmasq
    network_mode: "host"
    volumes:
    - ./containers/dnsmasq/tftpboot/linux-install:/var/ftpd
    - ./containers/dnsmasq/dnsmasq.conf:/etc/dnsmasq.conf
    restart: always
  matchbox:
    container_name: 'matchbox'
    image: quay.io/poseidon/matchbox:latest
    volumes:
      - ./containers/matchbox/var/lib/matchbox:/var/lib/matchbox:Z
      - ./containers/matchbox/etc/matchbox:/etc/matchbox:Z,ro
    ports:
      - '8080:8080'
    command: -address=0.0.0.0:8080 -rpc-address=0.0.0.0:8081
    restart: always
  netbootxyz:
    image: linuxserver/netbootxyz
    container_name: netbootxyz
    environment:
      - PUID=1000
      - PGID=1000
    ports:
      - 3000:3000
      - 8081:80
    restart: always
```

#### dnsmasq関連
DHCPが既にネットワーク内に立っているので、dhcp proxyで構成します

##### containers/dnsmasq/Dockerfile
```
FROM alpine:edge
RUN apk --no-cache add dnsmasq
EXPOSE 67/udp 69/udp 4011/udp
CMD ["dnsmasq", "-k", "-d"]
```

下記ポートDockerfileでEXPOSEしています。

|ポート|サービス|
|----|----|
|67/udp|dhcp|
|69/udp|TFTP|
|4011/udp|PXE Proxy|

##### dnsmasq.conf
コンテナで動かす際に
`network_mode: "host"` ホストのポートに直接紐付けて起動しています。
このモードの場合、ホストのネットワークインターフェースが見えるため、
dnsmasqが既存のvirbr0( libvirt のネットワークインターフェース )に悪さをしないように `except-interface=virbr0` の設定を入れています。
( すみません、自宅サーバで検証に時間が取れず... )

ただ、libvirtｄが立てるdnsmasqが立っていると、コンテナで立てるdnsmasqと競合するため、
サーバ起動後、手動でlibvirtdのdnsmasqのプロセスをkillしています( ここはもう少し調べたい... )

netboot.xyzを用いての初期セットアップを行いたいマシンは
`dhcp-mac=set:netboot,02:1a:4a:16:01:9c`
の様にして、netbootのタグを付けて振り分けています。

```
except-interface=virbr0
dhcp-range=10.10.254.0,proxy
pxe-service=tag:#ipxe,x86PC,"PXE chainload to iPXE",undionly.kpxe
# iPXE user classからのリクエストの場合はipxeタグをセットする
dhcp-userclass=set:ipxe,iPXE
# netboot.xyzでセットアップしたいMACアドレスを追記していく
dhcp-mac=set:netboot,02:1a:4a:16:01:9c
tag-if=set:netboot-xyz, tag:ipxe, tag:netboot
# netboot.xyz
pxe-service=tag:netboot-xyz,x86PC,"iPXE",http://10.10.254.200:8081/menus/menu.ipxe
# matchbox
pxe-service=tag:ipxe,x86PC,"iPXE",http://10.10.254.200:8080/boot.ipxe
enable-tftp
tftp-root=/var/ftpd
log-queries
log-dhcp
```

##### dnsmasqでの起動順序
1. パソコンの電源投入、PXEブートを開始、ネットワークカードが頑張ってIPアドレスとPXEサーバを探す
2. パソコンは dnsmasqのTFTPを経由してiPXEプロジェクトの undionly.kpxe をダウンロード、カーネルのチェインロードを始める
3. undionly.kpxeがHTTPプロトコルでmatchboxやnetboot.xyzとやり取りする
4. dnsmask.confでnetbootタグが設定されている場合は、 http://10.10.254.200:8080/assets/default.ipxe をPXEのメニューにしています
5. netbootタグがついてないマシンはmatchboxの http://10.10.254.200:8080/boot.ipxe で処理
6. matchboxはMACアドレスやアーキテクチャに応じてPXEのメニューファイルを作成し、パソコンに返す

という流れになります。

余談ですが、この辺、サイボウズのsabakan https://febc-yamamoto.hatenablog.jp/entry/2019/02/09/181927 で簡単にできるかな? とも思っているのですが、
CoreOSのIgnition周りなんかを生で触って理解したいというのもありmatchboxを使ってみています。

#### matchbox
docker-compose.ymlのmatchbox関連の設定ですが下記のようにしています。
オフィシャルのDockerイメージをそのまま使い、
設定ファイルやアセットはVolumesの設定でマウント。

ログは `docker logs matchbox` というようにコンテナの出力を見ています。

```
  matchbox:
    container_name: 'matchbox'
    image: quay.io/poseidon/matchbox:latest
    volumes:
      - ./containers/matchbox/var/lib/matchbox:/var/lib/matchbox:Z
      - ./containers/matchbox/etc/matchbox:/etc/matchbox:Z,ro
    ports:
      - '8080:8080'
    command: -address=0.0.0.0:8080 -rpc-address=0.0.0.0:8081
    restart: always
```

/var/lib/matchbox 配下のファイル構成は下記のようにしています。

```
.
├── assets
│   ├── default.ipxe
│   ├── fedora-coreos-31.20200113.3.1-live-initramfs.x86_64.img
│   └── fedora-coreos-31.20200113.3.1-live-kernel-x86_64
├── groups
│   └── fs01.json
├── ignition
│   ├── fedora-core-1.ign
│   └── netboot-xyz-1.ign
└── profiles
    ├── fedora-core-1.json
    └── netboot-xyz-1.json
```

それぞれのディレクトリですが

##### group MACアドレスにどのプロファイルを割り当てるか

```
{
  "id": "fedora-core-1",
  "name": "Fedora Core - 1",
  "profile": "fedora-core-1",
  "selector": {
    "mac": "00:24:1d:18:28:09"
  }
}
```

##### profile PXEブート時のカーネルイメージの指定とカーネルパラメータ
sshkey, ignition.config.url はそれぞれの環境に応じて変更する必要あり。

[基本的には Fedora CoreOS のPXEブートのパラメータ](https://docs.fedoraproject.org/en-US/fedora-coreos/bare-metal/#_installing_from_pxe) を参照

パラメータの意味については [CoreOSの説明](https://coreos.com/os/docs/latest/booting-with-pxe.html) が役に立つかも。

Fedora CoreOSのサイトから私が変更した箇所が2点あり

1. 2020/05時点ではdockerが cgroup v2 に対応していないので v1 に切り替え
2. selinuxのoff

cgroup v2 ではなく v1 に変更した手順
[Fedoraの新しめのカーネルは cgroup v2 だが、docker側が対応していないのでv1にしておく必要がある](https://qiita.com/TsutomuNakamura/items/52ec8ce47324bf692702) カーネルパラメータに `systemd.unified_cgroup_hierarchy=0` を追加した

[Fedora CoreOSのPXEでのセットアップ手順](https://docs.fedoraproject.org/en-US/fedora-coreos/bare-metal/#_live_pxe) には乗ってないので注意
( Fedora CoreOS はdockerではなく、podman推しなのかもしれませんが )

またカーネルオプションで `selinux=0` を追加して selinuxをオフにしました。

matchboxのインストールディレクトリ配下の
profiles/fedora-core-1.json に下記のように設置。

```
{
    "id": "fedora-core-1",
    "name": "Fedora Core - 1",
    "ignition_id": "fedora-core-1.ign",
    "boot": {
        "kernel": "/assets/fedora-coreos-31.20200407.3.0-live-kernel-x86_64",
        "initrd": [
            "/assets/fedora-coreos-31.20200407.3.0-live-initramfs.x86_64.img"
        ],
        "args": [
            "ip=dhcp",
            "rd.neednet=1",
            "console=tty0",
            "console=ttyS0",
            "selinux=0",
            "ignition.firstboot",
            "ignition.platform.id=metal",
            "systemd.unified_cgroup_hierarchy=0",
            "sshkey=ssh-rsa himitu naisho@kaoru-no-MacBook-Pro.local",
            "ignition.config.url=http://10.10.254.200:8080/ignition?mac=${mac:hexhyp}"
        ]
    }
}
```

##### assets CoreOS起動時に必要なカーネルイメージ、initramfsファイルなど

[fedora CoreOSのダウンロードページ](https://getfedora.org/coreos/download?tab=metal_virtualized&stream=stable) からダウンロードもできるのですが、
都度ダウンロードするのは面倒なので、そのページにあるjsonファイルを参考にして、下記のようにもダウンロードできます。

matchboxのインストールディレクトリのassetディレクトリ配下に移動し

```
coreos_stable=$(curl https://builds.coreos.fedoraproject.org/streams/stable.json)
kernel_url=$(echo $coreos_stable | jq  --raw-output '.architectures.x86_64.artifacts.metal.formats.pxe.kernel.location')
initramfs_url=$(echo $coreos_stable | jq  --raw-output '.architectures.x86_64.artifacts.metal.formats.pxe.initramfs.location')

curl -O $kernel_url
curl -O $initramfs_url
```

でstableなカーネル、initramfsイメージをダウンロードしています。

併せてmatchbox/profile配下のプロファイル用のファイルも更新する必要があります。

またmatchboxのこのディレクトリは、カーネルイメージやinitramfsを置く場所として使うのですが、
Webサーバのように公開されるので、インストール関連で使うものを置いておくのも便利かもしれません。
私はnetboot.xyzのファイルなどを置いています。

##### ignition OS内の各種設定( ユーザー追加、ストレージ、サービスの設定などなど )
はまったポイントなのですが、いろいろなサイトの事例のjsonをコピペして編集してみたのですが、
ユーザー追加ができてないようでハマってました( ログインできないので調査しずらい... )
[Container Linuxの後継となる新たなコンテナ向けOS「Fedora CoreOS」](https://knowledge.sakura.ad.jp/23287/)の「Ignitionの設定ファイルとfcctコマンド」を参考にしつつ、fcctコマンドでjsonを作成しました。

[fcctコマンドのインストールだけどpodman経由でインストールする手順もあるみたい](https://docs.fedoraproject.org/en-US/fedora-coreos/producing-ign/#_using_fcct) 手軽なのよい( プロダクション環境だとpodmanのレポジトリが落ちてるケースも考える必要がありそうだけど )

下記のようなyamlファイルを作成し、

```
variant: fcos
version: 1.0.0
passwd:
  users:
    - name: inoue
      groups:
        - adm
        - sudo
      ssh_authorized_keys:
        - ssh-rsa ssh公開鍵 hogehoge@hogehoge-no-MacBook-Pro.local
systemd:
  units:
    - name: shell.service
      enabled: true
      contents: |
        [Unit]
        Description=Alpine CLI Shell
        After=network.target
        [Service]
        Type=simple
        ExecStart=/usr/bin/podman run --rm --privileged -v /:/host -ti alpine:latest /bin/ash
        [Install]
        WantedBy=multi-user.target
```

`./fcct --input fcc_test.yaml --pretty --output fedora-core-1.ign` のように実行すると下記のものができました。

```
{
  "ignition": {
    "config": {
      "replace": {
        "source": null,
        "verification": {}
      }
    },
    "security": {
      "tls": {}
    },
    "timeouts": {},
    "version": "3.0.0"
  },
  "passwd": {
    "users": [
      {
        "groups": [
          "adm",
          "sudo"
        ],
        "name": "inoue",
        "sshAuthorizedKeys": [
          "ssh-rsa SSHの公開鍵 hogehoge@hogehoge-no-MacBook-Pro.local"
        ]
      }
    ]
  },
  "storage": {},
  "systemd": {
    "units": [
      {
        "contents": "[Unit]\nDescription=Alpine CLI Shell\nAfter=network.target\n[Service]\nType=simple\nExecStart=/usr/bin/podman run --rm --privileged -v /:/host -ti alpine:latest /bin/ash\n[Install]\nWantedBy=multi-user.target\n",
        "enabled": true,
        "name": "shell.service"
      }
    ]
  }
```

このjsonファイルをignitionディレクトリ配下に設置します。

#### firewalld設定
CentOS7だとファイアーウォールで開ける必要があります。

```
firewall-cmd --zone=public --add-service=tftp-client --permanent
firewall-cmd --zone=public --add-service=tftp --permanent
firewall-cmd --zone=public --add-service=proxy-dhcp --permanent
firewall-cmd --zone=public --add-service=dhcp --permanent
firewall-cmd --zone=public --add-port=69/udp --permanent         # TFTP
firewall-cmd --zone=public --add-service=dns --permanent
firewall-cmd --zone=public --add-port=4011/udp --permanent       # PXE proxy
firewall-cmd --zone=public --add-port=8080/tcp --permanent       # 私がmatchboxに割り当てたポート
firewall-cmd --reload
firewall-cmd --list-all
```