Terraform で KVM 上に Single Node OpenShift/OKD (SNO) を構築する

Terraform  を利用して、Linux KVM 上にシングルノード OpenShift/OKD クラスタ (SNO) を構築する。
本記事では OSS 版の OKD4 cluster を構築する。

## 概要
OKD 4.12 以降は、Agent-based installation というより手軽なインストール手段が提供されている。これは、事前設定済みの ISO イメージをマウントし直接 OpenShift cluster をインストールする方法である。

本記事では、Agent ISO image を生成したあと、Terraform を用いてそのイメージをディスクマウントし、VM をプロビジョニングする。 

おおまかな手順は次のようになる：

1. `openshift-install` コマンドで Agent ISO イメージを生成する
1. 生成された Agent ISO イメージをホストの libvirt pool にデプロイする
1. ISO イメージをベースに、Terraform で KVM 仮想マシンを作成する

インストールのフローを図に表すと次の通りになる：

![agent-based-installation.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/36105f45-2c04-9017-75f9-42ef17f1ecd5.png)

また、本記事で作成するマシンおよびネットワークの構成は次の通り：
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3236313732372f37656535626438612d326165362d323963352d623838612d3635616465623764623331372e706e67.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/a9d9b94c-3c0f-fe23-b430-4fcd86043064.png)

基本的な手順は次の公式ドキュメンテーションに従う：

https://docs.openshift.com/container-platform/4.15/installing/installing_sno/install-sno-preparing-to-install-sno.html

サンプルコードはこちら：

https://github.com/sawa2d2/k8s-on-kvm/tree/main/sno

## 前提条件

### Software
- Container runtime (`docker`, `podman`, `containerd`, etc.)
- `terraform`
- KVM Packages
  - `qemu-kvm`
  - `libvirt`

### Host OS
64bit Linux

### RedHat アカウント
[RedHat Hybrid Cloud Console](https://console.redhat.com/) にログインできること。

## ホスト側のネットワーク設定
本節では、VM ネットワークをホスト側で用意したブリッジに接続する。
次のコマンドを実行し、ブリッジ `br0` を作成する。

```bash:create_br.sh
HOST_IP=192.168.8.10
CIDR_PREFIX=24
GATEWAY=192.168.8.1
DNS=192.168.8.1
NWIF=enp1s0

# ブリッジを作成
nmcli con add type bridge con-name br0 ifname br0

# ブリッジに既存の NIC 設定を与える
nmcli con modify br0 \
ipv4.method manual \
ipv4.addresses "$HOST_IP/$CIDR_PREFIX" \
ipv4.gateway "$GATEWAY" \
ipv4.dns $DNS

# enp1s0 のマスターを br0 に
nmcli con add type bridge-slave ifname $NWIF master br0

# 既存の接続を無効化
nmcli con down $NWIF

# br0 を有効化
nmcli con up br0
```

## OKD インストーラおよび CLI のインストール
CLI [`oc`](https://github.com/okd-project/okd/releases) をインストールする：
```
$ OKD_VERSION=4.15.0-0.okd-2024-03-10-010116
$ cd /tmp
$ curl -LO https://github.com/okd-project/okd/releases/download/${OKD_VERSION}/openshift-client-linux-${OKD_VERSION}.tar.gz
$ tar -xvzf openshift-client-linux-${OKD_VERSION}.tar.gz
$ sudo cp oc /usr/local/bin/
$ oc version
```

インストーラ [`openshift-install`](https://github.com/okd-project/okd/releases) をインストールする：
```
$ OKD_VERSION=4.15.0-0.okd-2024-03-10-010116
$ cd /tmp
$ curl -LO https://github.com/okd-project/okd/releases/download/${OKD_VERSION}/openshift-install-linux-${OKD_VERSION}.tar.gz
$ tar -xvzf openshift-install-linux-${OKD_VERSION}.tar.gz
$ sudo cp openshift-install /usr/local/bin/
$ openshift-install version
```

## OKD4 クラスタ設定ファイルの用意

### install-config.yaml の用意

次のような `install-config.yaml` を用意する。

```yaml:install-config.yaml
---
apiVersion: v1
baseDomain: example.com
compute:
- hyperthreading: Enabled
  name: worker
  replicas: 0
controlPlane:
  hyperthreading: Enabled
  name: master
  replicas: 1
metadata:
  name: sno
networking:
  clusterNetwork:
  - cidr: 10.128.0.0/14
    hostPrefix: 23
  networkType: OVNKubernetes
  serviceNetwork:
  - 172.30.0.0/16
  machineNetwork:
  - cidr: 192.168.8.0/24
platform:
  none: {}
fips: false
pullSecret: '{"auths":{"fake":{"auth":"aWQ6cGFzcwo="}}}'
sshKey: |
  ssh-ed25519 AAAA... user1@example.com
  ssh-ed25519 AAAA... user2@example.com
```

特に各々の環境によって書き換える必要のある key は以下の通り：
- `networking.machineNetwork[].cidr`: 本記事のようにブリッジネットワーク上に VM を作成する場合、ブリッジネットワークのサブネットを指定する。
- `pullSecret`: [Install OpenShift 4 | Pull Secret](https://console.redhat.com/openshift/install/pull-secret) からコピーしてきた値を入れる。


### agent-config.yaml の用意
次のような agent-config.yaml を用意する。
コメントが挿入された行は各々のネットワーク設定に依存するので、適宜書き換える。

```yaml:agent-config.yaml
apiVersion: v1alpha1
kind: AgentConfig
metadata:
  name: sno
rendezvousIP: 192.168.8.80    # Master node に設定する IP
hosts:
  - hostname: master0
    interfaces:
      - name: ens3
        macAddress: 52:54:00:00:00:50
    rootDeviceHints:
      deviceName: "/dev/vda"
    networkConfig:
      interfaces:
        - name: ens3
          type: ethernet
          state: up
          mac-address: 52:54:00:00:00:50
          ipv4:
            enabled: true
            address:
              - ip: 192.168.8.80     # Master node に設定する IP
                prefix-length: 24    # Bridge network の prefix を指定
            dhcp: false
      dns-resolver:
        config:
          server:
            - 192.168.8.1    # DNS server を指定
      routes:
        config:
          - destination: 0.0.0.0/0
            next-hop-address: 192.168.8.1    # Default Gateway を指定 
            next-hop-interface: ens3
            table-id: 254
```

:::note
"Rendezvous IP" とは ... 従来のインストール手法では、"Bootstrap node" と呼ばれるインストーラ専用のノードを別途、用意する必要があった。一方、Agent-based Installation においては、bootstrap node の機能が通常の Controle Plane ノード上に展開されるようになっている。この役割を担うものが Rendezvous ホストと呼ばれる ([参考](https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.12/html/installing/installing-an-on-premise-cluster-with-the-agent-based-installer#understanding-agent-install_preparing-to-install-with-agent-based-installer))。 
:::

## Agent ISO イメージの作成
まずは、Agent ISO イメージを生成するためのコンテナを作成する。

:::note
Agent ISO を作成する際に依存ソフトウェア (`nmstate`) が必要になるのですが、Fedora 系以外にこれを導入するのは面倒、または困難だったりします。なので、この作業はコンテナで行うことをお勧めします。
:::

非常にありがたいことに、 RedHat Japan の tech blog 内記事： [Apple Silicon搭載MacにシングルノードOpenShiftをインストールする（エージェントベース編） - 赤帽エンジニアブログ](https://rheb.hatenablog.com/entry/sno-on-arm-mac-abi#Containerfile) で Agent ISO 作成用の Dockerfile が公開されているので、こちらを使用する。ただしオリジナルは OpenShift 仕様なので、下記のように OKD 仕様に修正する：

```Dockerfile
# Based on https://rheb.hatenablog.com/entry/sno-on-arm-mac-abi#Containerfile

# Preparation Build
FROM quay.io/centos/centos:stream9-minimal as prep

# ARG
ARG VERSION

# ENV 
ENV VERSION=${VERSION}

# Download binary
RUN microdnf install tar gzip -y 
RUN curl -L https://github.com/okd-project/okd/releases/download/${VERSION}/openshift-install-linux-${VERSION}.tar.gz -o openshift-install-linux.tar.gz
RUN curl -L https://github.com/okd-project/okd/releases/download/${VERSION}/openshift-client-linux-${VERSION}.tar.gz -o openshift-client-linux.tar.gz
RUN tar xzf openshift-install-linux.tar.gz
RUN tar xzf openshift-client-linux.tar.gz

# Build image
FROM quay.io/centos/centos:stream9-minimal as build

RUN microdnf update -y \
&& microdnf install nmstate -y \
&& microdnf clean all

COPY --from=prep openshift-install /usr/local/bin
COPY --from=prep oc /usr/local/bin

ENTRYPOINT ["/usr/local/bin/openshift-install"]
```

[Releases · okd-project/okd](https://github.com/okd-project/okd/releases) から所望のバージョンを確認の上、下記コマンドを実行しコンテナをビルドする：
```
$ export OKD_VERSION=4.15.0-0.okd-2024-03-10-010116
$ podman image build ./ -t openshift-install --build-arg VERSION=${OKD_VERSION}
$ podman container run -it --rm openshift-install version
```

Agent ISO イメージを作成する。

```
$ mkdir -p ocp image_cache files_cache
$ cp ./*.yaml ocp
$ podman run --privileged --rm \
    -v $PWD:/data \
    -v ./image_cache:/root/.cache/agent/image_cache \
    -v ./files_cache:/root/.cache/agent/files_cache \
    -w /data openshift-install:latest \
     --dir ocp agent create image
```

作成されたイメージを libvirt の pool にコピーする：
```
sudo cp ./ocp/agent.x86_64.iso /var/lib/libvirt/images/
```

## DNS の設定

DNS の要求は次の表の通り (参考:[Preparing to install OpenShift on a single node - Installing on a single node | Installing | OpenShift Container Platform 4.15](https://docs.openshift.com/container-platform/4.15/installing/installing_sno/install-sno-preparing-to-install-sno.html#install-sno-requirements-for-installing-on-a-single-node_install-sno-preparing))。

SNO の場合、すべて同じ IP アドレスを指定する。
| Record | Target | Example |
| -- | -- |-- |
| `api.<cluster_name>.<base_domain>.` | API IP | `192.168.8.80`
| `api-int.<cluster_name>.<base_domain>.` | API IP | `192.168.8.80`
| `*.apps.<cluster_name>.<base_domain>.` | Ingress IP | `192.168.8.80`

:::note info
`API` は Master nodes を代表する Virtual IP、`Ingress` は Worker nodes を代表する Virtual IP である。通常の OpenShift クラスタ はそれぞれの Virtual IP を `install-config.yaml` で指定するが、SNO のように Master node ＝ Worker node のケースでは両者に異なる Virtual IP を割り当てる必要がないので、設定項目からは省略されている。
:::


:::note info
横着してワイルドカードレコード (e.g. `*.sno.example.com`) 1個で設定しないようにする。Agetn はワイルドカードレコードを検知すると、次のようにインストールを中断してしまう：
```
$ openshift-install  agent wait-for install-complete --log-level=debug
...
WARNING Host master0 validation: DNS wildcard configuration was detected for domain *.sno.example.com - the installation will not be able to complete while this record exists. Please remove it to proceed. The domain resolves to addresses 192.168.8.80 
```
:::

## Terraform によるクラスタのプロビジョニング
次のような `main.tf` を作成する。

```terraform:main.tf
terraform {
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.7.1"
    }
  }
}

provider "libvirt" {
  uri = "qemu:///system"
}

resource "libvirt_domain" "vm" {
  name   = "master0"
  vcpu   = 16
  memory = 65536
  machine = "q35"

  boot_device {
    dev = ["hd", "cdrom"]
  }

  disk {
    file = "/var/lib/libvirt/images/agent.x86_64.iso"
  }

  disk {
    volume_id = libvirt_volume.system.id
  }

  disk {
    volume_id = libvirt_volume.volume.id
  }

  autostart = true

  network_interface {
    bridge    = "br0"
    addresses = ["192.168.8.80"]
    mac       = "52:54:00:00:00:50"
  }

  cpu {
    mode = "host-passthrough"
  }

  graphics {
    type        = "vnc"
    listen_type = "address"
  }

  xml {
    xslt = file("${path.module}/patch.xsl")
  }
}

resource "libvirt_volume" "system" {
  name   = "sno_system.qcow2"
  pool   = "default"
  format = "qcow2"
  size   = 512 * 1024 * 1024 * 1024
}

resource "libvirt_volume" "volume" {
  name   = "sno_volume.qcow2"
  pool   = "default"
  format = "qcow2"
  size   = 1024 * 1024 * 1024 * 1024
}
```

今回、KVM は Q35 type と呼ばれる machine type で作成する。そのための**おまじない**として、次の XSLT ファイル `patch.xml` を用意する（現在の Terraform libvert provider がもつ不具合への対処として、作成するものである。詳しくはこちらの記事：[Terraform Libvirt Provider で Q35 タイプの VM を作成する - Qiita](https://qiita.com/sawa2d2/items/a453d2d3ee20af11d208) を参照）：
```patch.xml
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <!-- Identity transform to copy everything as is -->
  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <!-- Use SATA CD-ROM for Linux setup -->
  <xsl:template match="target[@bus='ide']">
    <xsl:copy>
      <xsl:apply-templates select="@*[name()!='bus']"/>
      <xsl:attribute name="bus">sata</xsl:attribute>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>
```

:::note
参考：SNO におけるマシンリソースの最小要求は次の表の通り（[引用](https://docs.openshift.com/container-platform/4.15/installing/installing_sno/install-sno-preparing-to-install-sno.html#install-sno-requirements-for-installing-on-a-single-node_install-sno-preparing)）：

| vCPU | Virtual RAM | Storage |
|---|-------|--------|
| 8 | 16 GB | 120 GB |
:::

次のコマンドを実行し、VM のプロビジョニングを実行する。

```
$ terraform init
$ terraform apply -auto-approve
```

## インストールの完了を待つ
次のコマンドを実行し、インストールの進捗を確認する：
```
$ export KUBECONFIG=ocp/auth/kubeconfig
$ openshift-install --dir=ocp wait-for install-complete
```

インストールが完了すると次のようなメッセージが出力され、OKD ダッシュボードのログイン ID および パスワードが出力される。
```
INFO Waiting up to 40m0s (until 1:27PM JST) for the cluster at https://api.sno.example.com:6443 to initialize... 
INFO Waiting up to 30m0s (until 1:17PM JST) to ensure each cluster operator has finished progressing... 
INFO All cluster operators have completed progressing 
INFO Checking to see if there is a route at openshift-console/console... 
INFO Install complete!                            
INFO To access the cluster as the system:admin user when using 'oc', run 'export KUBECONFIG=/mnt/ext/repo/sawa2d2/okd/sno/ocp/auth/kubeconfig' 
INFO Access the OpenShift web-console here: https://console-openshift-console.apps.sno.example.com 
INFO Login to the console with user: "kubeadmin", and password: "xxxxx-xxxxx-xxxxx-xxxxx" 
INFO Time elapsed: 0s    
```

## kubeconfig file の保存
`ocp/auth/kubeconfig` を適当な名前に変更し (例：`sno.conf`)、`~/.kube` に保存する：
```
mkdir -p ~/.kube
mv ./ocp/auth/kuberconfig ~/.kube/sno.conf
```

環境変数 `KUBECONFIG` に kubeconfig file へのパスを追加する：
```
export KUBECONFIG=$KUBECONFIG:$HOME/.kube/sno.conf 
```

## ダッシュボードへのログイン
コンソールのURLを調べる：
```
$ oc whoami --show-console
https://console-openshift-console.apps.sno.example.com
```

ブラウザ上からアクセスできることを確認する。

![Screenshot from 2024-06-12 12-50-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/7a00c718-3047-4843-c700-ef655c58a5cf.png)

`kubeadmin` user でダッシュボードにログインできることを確認する。
![Screenshot from 2024-06-12 12-50-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/2d13d629-bbc7-e46e-02de-a14bfd02fa84.png)