MatchBoxを使ってCoreOS Container Linuxをベアメタルにプロビジョンする

# はじめに

この記事は[MicroAd Advent Calendar 2017](https://qiita.com/advent-calendar/2017/microad)の8日目の記事です。

内容は[MatchBoxを使ってCoreOS Container Linuxをベアメタルにプロビジョンする全体像](https://qiita.com/kanga/items/de223e4ba3c389777886)の続きになります。
実際にCoreOSをベアメタル環境にデプロイするにあたり、必要な環境やら設定やらを整えます。

## Matchboxサーバの用意

Machboxサーバを実際に構築します。SystemdがinitのLinux前提です。
大体、[ここに書いてある](https://coreos.com/matchbox/docs/latest/deployment.html)ことまんまです。
GithubからもろもろDLして解凍してディレクトリ内に移動します。

```shell-session
$ wget https://github.com/coreos/matchbox/releases/download/v0.6.1/matchbox-v0.6.1-linux-amd64.tar.gz
$ tar xvfz matchbox-v0.6.1-linux-amd64.tar.gz
$ cd matchbox-v0.6.1-linux-amd64
```
中身はこんな感じです。

```shell-session
$ ls   
CHANGES.md  LICENSE  README.md  contrib  docs  examples  matchbox  scripts
```

バイナリをパスが通る場所に配置します。

```shell-session
$ sudo cp matchbox /usr/local/bin
```

Systemdのユニットファイルを配置します。

```shell-session
$ sudo cp contrib/systemd/matchbox-local.service /etc/systemd/system/matchbox.service
```

中身はこんな感じです。

```bash:/etc/systemd/system/matchbox.service
[Unit]
Description=CoreOS matchbox Server
Documentation=https://github.com/coreos/matchbox

[Service]
User=matchbox
Group=matchbox
Environment="MATCHBOX_ADDRESS=0.0.0.0:8080"
ExecStart=/usr/local/bin/matchbox

# systemd.exec
ProtectHome=yes
ProtectSystem=full

[Install]
WantedBy=multi-user.target
```

matchbox実行ユーザやデータディレクトリを作成します。

```
$ sudo useradd -U matchbox
$ sudo mkdir -p /var/lib/matchbox/assets
$ sudo chown -R matchbox:matchbox /var/lib/matchbox
```


次にTerraformからの接続で使うTLS証明書を用意します。
`scripts/tls/cert-gen`に証明書作成用のスクリプトが置いてあります。
環境変数SANにより、証明書のSANを設定できます。
使用するサーバのFQDN、あるいはIPを設定して、証明書を作成します。

```:
$ export SAN=DNS.1:matchbox.microad.jp,IP.1:172.18.0.2
$ cd scripts/tls/
$ ./cert-gen
```

サーバ側の資格情報を配置します。

```
$ sudo mkdir -p /etc/matchbox
$ sudo cp ca.crt server.crt server.key /etc/matchbox
```

また、クライアント側の資格情報は後で使用するので保存します。
手元に持ってきておきましょう。`client.crt`,`client.key`,`ca.crt`

ここまで来れば、systemdのUnitを起動します。

```
$ sudo systemctl daemon-reload
$ sudo systemctl start matchbox
$ sudo systemctl enable matchbox
```

また、オプションですがCoreOSのイメージをネットワークブートサーバが直でインターネットからダウンロードする代わりにMatchBoxから配布することも可能です。

DLしたMatchboxのディレクトリ内に便利なDLスクリプトが付随しています。

```shell-session
$ cd ~/matchbox-v0.6.1-linux-amd64
$ sudo ./scripts/get-coreos beta 1590.2.0 /var/lib/matchbox/assets
Creating directory /var/lib/matchbox/assets/coreos/1590.2.0
Downloading CoreOS beta 1590.2.0 images and sigs to /var/lib/matchbox/assets/coreos/1590.2.0
CoreOS Image Signing Key
略
```

こんな感じにアセットファイルが配置されます。

```shell-session
$ ls /var/lib/matchbox/assets/coreos/1590.2.0/     
CoreOS_Image_Signing_Key.asc     coreos_production_image.bin.bz2.sig  coreos_production_pxe.vmlinuz.sig    coreos_production_pxe_image.cpio.gz.sig
coreos_production_image.bin.bz2  coreos_production_pxe.vmlinuz        coreos_production_pxe_image.cpio.gz
```


## PXE環境の用意

PXE環境のセットアップについてはDHCP/TFTP/DNS全部乗せのコンテナイメージをCoreOS社が提供しており、これを使えば一発で環境準備できます。

```
sudo docker run --rm --cap-add=NET_ADMIN --net=host quay.io/coreos/dnsmasq \
  -d -q \
  --dhcp-range=172.18.1.2,172.18.1.254 \
  --enable-tftp --tftp-root=/var/lib/tftpboot \
  --dhcp-userclass=set:ipxe,iPXE \
  --dhcp-boot=tag:#ipxe,undionly.kpxe \
  --dhcp-boot=tag:ipxe,http://matchbox.microad.jp:8080/boot.ipxe \
  --address=/matchbox.microad.jp/172.18.0.2 \
  --log-queries \
  --log-dhcp
```

とりあえず検証で動かす分には上記のDockerImageで十分です。
注意点として
`--dhcp-range`は付加したいDHCPのIPレンジを渡しましょう
`--dhcp-boot`は使用するmatchboxサーバのアドレスを渡しましょう
`--address`はmatchboxサーバのFQDNとIPを渡しましょう

以上でMatchboxサーバ、PXEサーバ側の設定は完了です。

## Terraform pluginを準備

以後はTerraformを使って諸々の設定をデプロイする準備になります。

Matchbox用のTerraformプラグインはGithubからDLして、Terraformのpluginディレクトリに配置しましょう。
https://github.com/coreos/terraform-provider-matchbox/releases

また、Matchboxで作成したTLSのクライアン側の資格情報（`client.crt`,`client.key`,`ca.crt`）がTerraformデプロイに必要になるので、~/.matchboxに配置しましょう。

```shell-session
$  ls ~/.matchbox/
ca.crt  client.crt  client.key

```


## tfファイルの作成

では実際にtfファイルを作成していきます。

とりあえずこんな感じのtree構造で作成してみます。

```shell-session
$ tree matchbox.microad.jp
matchbox.microad.jp
├── cl
│   ├── container-linux-install.yaml.tmpl
│   └── static.yaml.tmpl
├── container-linux-install.tf
├── provider.tf
├── static.tf
├── terraform.tfvars
└── variables.tf
```

clディレクトリにはContaine linux configのyamlをgoのtemplate形式でレンダリングしたものを配置します。

まずは`provider.tf`ですがこんな感じでmatchboxのエンドポイントとtlsの資格情報を記入します。

```
provider "matchbox" {
  endpoint    = "${var.matchbox_rpc_endpoint}"
  client_cert = "${file("~/.matchbox/client.crt")}"
  client_key  = "${file("~/.matchbox/client.key")}"
  ca          = "${file("~/.matchbox/ca.crt")}"
}

```
次に実際のCoreOSのインストールなんですが、最初PXEブートしてきた時、CoreOSのOSイメージはメモリ上に展開されます。
その後、メモリ上に展開されたCoreOSにバンドルされている`coreos-install`を使ってディスクにインストールがされるという流れになります。

そのRAMにCoreOSを展開するための設定が以下になります。

```:container-linux-install.tf
resource "matchbox_profile" "container-linux-install" {
  name   = "container-linux-install"
  kernel = "/assets/coreos/${var.container_linux_version}/coreos_production_pxe.vmlinuz"

  initrd = [
    "/assets/coreos/${var.container_linux_version}/coreos_production_pxe_image.cpio.gz",
  ]

  args = [
    "initrd=coreos_production_pxe_image.cpio.gz",
    "coreos.config.url=${var.matchbox_http_endpoint}/ignition?uuid=$${uuid}&mac=$${mac:hexhyp}",
    "coreos.first_boot=yes",
    "console=tty0",
    "console=ttyS0",
  ]

  container_linux_config = "${data.template_file.container-linux-install-config.rendered}"
}

data "template_file" "container-linux-install-config" {
  template = "${file("./cl/container-linux-install.yaml.tmpl")}"

  vars {
    container_linux_channel = "${var.container_linux_channel}"
    container_linux_version = "${var.container_linux_version}"
    ignition_endpoint       = "${format("%s/ignition", var.matchbox_http_endpoint)}"
    install_disk            = "${var.install_disk}"

    baseurl_flag = "-b ${var.matchbox_http_endpoint}/assets/coreos"
  }
}

resource "matchbox_group" "default" {
  name    = "default"
  profile = "${matchbox_profile.container-linux-install.name}"

  metadata {
    ssh_authorized_key = "${var.ssh_authorized_key}"
  }
}
```

2つのリソースと1つのdataが定義されています。
`matchbox_profile`リソースはどんなclファイルをやイメージを使ってBootするか、といった設定のまとまりのリソースになります。
`matchbox_group`はどのノードがどの`matchbox_profile`を使うか？といったルーティングを行うリソースになります。
`template_file`は単なるterrafomの機能のtemplateです。
ややこしいですがgoでtemplate化されたyamlファイルをterraformのtemplateにより生成しています。
中身は以下のような感じです。

```yaml:cl/container-linux-install.yaml.tmpl
---
systemd:
  units:
    - name: installer.service
      enable: true
      contents: |
        [Unit]
        Requires=network-online.target
        After=network-online.target
        [Service]
        Type=simple
        ExecStart=/opt/installer
        [Install]
        WantedBy=multi-user.target
storage:
  files:
    - path: /opt/installer
      filesystem: root
      mode: 0500
      contents:
        inline: |
          #!/bin/bash -ex
          curl --retry 10 "${ignition_endpoint}?{{.request.raw_query}}&os=installed" -o ignition.json
          coreos-install \
            -d ${install_disk} \
            -C ${container_linux_channel} \
            -V ${container_linux_version} \
            ${baseurl_flag} \
            -i ignition.json
          udevadm settle
          systemctl reboot
passwd:
  users:
    - name: core
      ssh_authorized_keys:
        - {{.ssh_authorized_key}}

```

OSイメージが起動してくると`installer.service`というSystemdのユニットが起動し、CoreOSをディスクにインストールします。
その際、Matchboxにignition設定を要求するhttpリクエストに`os=installed`というクエリパラメータを付けることで実際にディスクインストールされる際に、別の`matchbox_profile`が適用されるようにします。

以下が実際にディスクにインストールされるCoreOSの`matchbox_profile`になります。

```:static.tf
resource "matchbox_profile" "static" {
  name                   = "static"
  container_linux_config = "${file("./cl/static.yaml.tmpl")}"
}

resource "matchbox_group" "static" {
  count   = "${length(var.static_names)}"
  name    = "${element(var.static_names, count.index)}"
  profile = "${matchbox_profile.static.name}"

  selector {
    os  = "installed"
    mac = "${element(var.static_macs, count.index)}"
  }

  metadata {
    hostname = "${element(var.static_names, count.index)}"
    ssh_authorized_key = "${var.ssh_authorized_key}"
    ip_addr = "${element(var.static_ips, count.index)}"
  }
}
```

`matchbox_group`のリソースの`selector`セクションが、実際にそのGroupを適用させるための条件になるんですがここにMACアドレスを指定することで、特定のMACアドレスのサーバに対して任意の設定を設定する事が可能となります。
また、`metadata`セクションはclファイルをレンダリングする際に渡す事ができるパラメータになっています。

今回clファイルはstatic ipとhostnameをmetadataから埋め込めるシンプルなものを作成しました。

```yaml:cl/static.yaml.tmpl
---
storage:
  files:
    - path: /etc/systemd/resolved.conf
      filesystem: root
      mode: 0644
      contents:
        inline: |
          [Resolve]
          DNS=172.18.0.1
          Domains=microad.jp
    - path: /etc/hostname
      filesystem: root
      mode: 0644
      contents:
        inline: |
          {{.hostname}}.microad.jp
    - path: /etc/systemd/network/en*f0.network
      filesystem: root
      mode: 0644
      contents:
        inline: |
          [Match]
          Name=enp*f0
          
          [Network]
          Address={{.ip_addr}}/24
          Gateway=172.18.0.254
passwd:
  users:
    - name: core
      ssh_authorized_keys:
        - {{.ssh_authorized_key}}
locksmith:
    reboot_strategy: "off"

```

変数はこんな感じになってます。

```variables.tf

variable "matchbox_http_endpoint" {
  type        = "string"
  default     = "http://matchbox.microad.jp:8080"
  description = "Matchbox HTTP read-only endpoint (e.g. http://matchbox.example.com:8080)"
}

variable "matchbox_rpc_endpoint" {
  type        = "string"
  default     = "matchbox.microad.jp:8081"
  description = "Matchbox gRPC API endpoint, without the protocol (e.g. matchbox.example.com:8081)"
}

variable "ssh_authorized_key" {
  type        = "string"
  default     = "公開鍵"
}

variable "container_linux_version" {
  type        = "string"
  default     = "1590.2.0"
  description = "Container Linux version of the kernel/initrd to PXE or the image to install"
}

variable "container_linux_channel" {
  type        = "string"
  default     = "beta"
  description = "Container Linux channel corresponding to the container_linux_version"
}

variable "install_disk" {
  type        = "string"
  default     = "/dev/sda"
  description = "Disk device to which the install profiles should install Container Linux (e.g. /dev/sda)"
}

variable "static_names" {
  type = "list"
}

variable "static_macs" {
  type = "list"
}

variable "static_ips" {
  type = "list"
}

```

```terraform.tfvars
static_names = ["coreos01", "coreos02", "coreos03"]
static_macs = ["52:2f:69:54:d0", "52:25:54:c5:5c:0e", "52:54:7a:80:8f:60"]
static_ips = ["172.18.0.3", "172.18.0.4", "172.18.5."]
```

この辺はHashにしたほうが良かったかも。。。
ちなみにTerraformですが本番で使う場合はremote storageを使いましょう。

## デプロイ

ではplanとapplyを実際にしてみます。

```
$ terraform init
$ terraform plan
$ terraform apply
```

Matchboxサーバに展開された設定を確認します。

最終的にMatchboxサーバには以下のような設定が展開されます。

```shell-session
$ ls /var/lib/matchbox          
assets  groups  ignition  profiles
```

templateが配置されます。

```shell-session
$ ls /var/lib/matchbox/ignition/
container-linux-install.yaml.tmpl  static.yaml.tmpl
```

groupsにはルーティング設定が展開されます。

```
$ ls /var/lib/matchbox/groups/
coreos01.json  coreos02.json  coreos03.json  default.json
```

```json:coreos01.json
{
	"id": "coreos01",
	"profile": "static",
	"selector": {
		"mac": "52:2f:69:54:d0",
		"os": "installed"
	},
	"metadata": {
		"hostname": "coreos01",
		"ip_addr": "172.18.0.3",
		"ssh_authorized_key": "公開鍵"
	}
}
```
```json:default.json
{
	"id": "default",
	"profile": "container-linux-install",
	"metadata": {
		"ssh_authorized_key": "公開鍵"
	}
}
```

profilesにはどんなclファイルをやイメージを使ってBootするか、といった設定のまとまりが展開されてます。

```shell-session
$ ls /var/lib/matchbox/profiles/
container-linux-install.json  static.json
```

```json:static.json
{
	"id": "static",
	"ignition_id": "static.yaml.tmpl",
	"boot": {}
}
```

```json:ontainer-linux-install.json
{
	"id": "container-linux-install",
	"ignition_id": "container-linux-install.yaml.tmpl",
	"boot": {
		"kernel": "/assets/coreos/1590.2.0/coreos_production_pxe.vmlinuz",
		"initrd": [
			"/assets/coreos/1590.2.0/coreos_production_pxe_image.cpio.gz"
		],
		"args": [
			"initrd=coreos_production_pxe_image.cpio.gz",
			"coreos.config.url=http://matchbox.microad.jp:8080/ignition?uuid=${uuid}\u0026mac=${mac:hexhyp}",
			"coreos.first_boot=yes",
			"console=tty0",
			"console=ttyS0"
		]
	}
}
```



ぶっちゃけTerraform云々は必ずしも必要な訳ではなく、上記に置いてあるようなファイルがあれば良いだけです。
Terraformに慣れていなければ、Anisibleなどの既存の使い慣れたツールで配置する事も可能です。
とはいえTerraformによるワークフローの抽象化ってのは魅力なので、なるべくTerraformを使った方が良いとは思いますが。

## 起動

あとは対象MACアドレスの機器をIPMIツールなりなんなりで起動すれば、諸々設定されたCoreOSが立ち上がってきます！

## おわりに

ちょっと試すには若干ハードルが高いのですが、一旦環境を整えてしまえば、Terraformの設定を書く→レビュー→Plan→Apply→Boot！のフローに載せる事ができるので、便利です。

またTerraformの公式サンプルは以下に色々のってます。
https://github.com/coreos/matchbox/tree/master/examples/terraform