OpenShift (OKD4) on KVM

## TL;DR
[RedHat OpenShift](https://www.redhat.com/ja/technologies/cloud-computing/openshift) のコミュニティ版である [OKD4](https://www.okd.io/) のクラスタを、1台の物理マシン上に構成する。

`install-config.yaml` を用意し、 `openshift-install` コマンドを実行すると Ignition 設定ファイルが生成される。それを Terraform に与え、 KVM 上に OKD4 クラスタをプロビジョニングする。導入フローを図に表すと次の通り：

![installation-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/41aaa6ca-8da6-c068-ab56-e2a0ece20c84.png)

サンプルコード：

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

## 概要

本記事で作成するネットワーク構成は下図の通り：

![network_architecture.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/338ec246-9334-366a-bb63-be4d1dd8f8e6.png)

手順は次の公式のガイドに従う：

https://docs.okd.io/latest/installing/installing_bare_metal/preparing-to-install-on-bare-metal.html

https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.3/html/cli_tools/openshift-cli-oc

## 前提条件
### Softwares
  - `terraform` 
  - KVM Packages
    - `qemu-kvm`
    - `libvirt`
  - `systemd-resolved`

### ホスト（物理マシン）

| | |
|-|-|
| Machine | Intel NUC13 Pro (NUC13ANHi7) |
| CPU | Core i7-1065G7  |
| Memory | 64GB (DDR4-3200MHz) |
| SSD | 1TB (NVMe) |
| Host OS | Rocky Linux 9.2 (Minimal) |

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

## Host の事前準備
簡単のために SE Linux は permisive にしておく。
```
$ setenforce 0
```

同様に、簡単のために Firewall を無効化する。
```
$ systemctl stop firewalld
$ systemctl disable firewalld
```

## Libvirt の事前準備 
libvirt の事前設定準備として、こちらの [Libvirt HOTOW · openshift/installer](https://github.com/openshift/installer/tree/53ce73daedd90659c7d690151bb0ecc7086a5fd2/docs/dev/libvirt) に記載の内容を実行する。

次を実行し、`net.ipv4.ip_forward` 設定値が `1` であるか確認する：
```
$ sysctl net.ipv4.ip_forward
sysctl net.ipv4.ip_forward=1
```

もし `net.ipv4.ip_forward = 0` であった場合、次を実行して設定値を変更する。
```
$ sysctl net.ipv4.ip_forward=1
```

設定を永続化する場合は：
```
$ echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-ipforward.conf
$ sudo sysctl -p /etc/sysctl.d/99-ipforward.conf
```

libvirtd-tcp.socket を有効化する：
```
$ sudo systemctl enable libvirtd-tcp.socket
$ sudo systemctl start libvirtd-tcp.socket
```

NetworkManager DNS overlayを使用する：

```
$ echo -e "[main]\ndns=dnsmasq" | sudo tee /etc/NetworkManager/conf.d/openshift.conf
$ echo server=/ocp4.example.com/192.168.126.1 | sudo tee /etc/NetworkManager/dnsmasq.d/openshift.conf
```

設定を適用する：
```
sudo systemctl restart NetworkManager
```

libvirtd args の値を設定する。ホストが Fedora 系OS (e.g. Fedora, RHEL, CentOS, etc.) か Debian 系 OS (Debian, Ubuntu, etc.) かで設定値が異なるので注意。

（Fedora系 の場合）：`/etc/sysconfig/libvirtd` に次の設定を加える。
```:/etc/sysconfig/libvirtd
# For Fedora-based distro
LIBVIRTD_ARGS="--listen"
```

（Debian 系の場合）：`/etc/default/libvirtd` に次の設定を加える。
```:/etc/default/libvirtd:
# For Debian-based distro
libvirtd_opts="--listen"
```

`/etc/libvirt/libvirtd.conf` で次の設定を有効にする：
```
listen_tls = 0
listen_tcp = 1
auth_tcp = "none"
tcp_port = "16509"
```

libvirt を再起動する。
```
sudo systemctl restart libvirtd
```

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

本節では、 OKD4 クラスタ設定ファイル `install-config.yaml` を用意し、VM の作成に必要な Ignition ファイルを生成するまでの手順を説明する。

### `openshift-install` の用意
まずはインストーラ `openshift-install` をインストールする：

```
$ curl -LO https://github.com/okd-project/okd/releases/download/4.13.0-0.okd-2023-09-30-084937/openshift-client-linux-4.13.0-0.okd-2023-09-30-084937.tar.gz
$ tar -zxvf openshift-client-linux-4.13.0-0.okd-2023-09-30-084937.tar.gz
$ mv openshift-install /usr/local/bin
$ openshift-install version
openshift-install 4.14.0-0.okd-2023-10-28-073550
built from commit 03546e550ae68f6b36d78d78b539450e66b5f6c2
release image quay.io/openshift/okd@sha256:7a6200e347a1b857e47f2ab0735eb1303af7d796a847d79ef9706f217cd12f5c
release architecture amd64
```

### install-config.yaml の設定

[公式ドュメンテーションのサンプル](https://docs.okd.io/latest/installing/installing_bare_metal/installing-bare-metal.html#installation-bare-metal-config-yaml_installing-bare-metal
) をベースに、次の `install-config.yaml` を作成する：

:::note info 
カスタマイズ可能な項目は [Cluster Customization · openshift/installer](https://github.com/openshift/installer/blob/master/docs/user/customization.md#platform-customization) から確認できる。
:::

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

特に書き換える必要がある項目について説明する：

#### pullSecret
[Install OpenShift 4 | Pull Secret](https://console.redhat.com/openshift/install/pull-secret) から pullSecret をコピーし、そのまま上記 yaml の `pullSecret` の値とする。

設定しなかった場合、コンテナレジストリ （quay.io や registry.redhat.io など） へのアクセスや、Operator Hub でインストール可能な Operator の種類が制限される場合がある。

https://docs.openshift.com/container-platform/4.12/installing/installing_bare_metal_ipi/ipi-install-installation-workflow.html#additional-resources_config

#### sshKey
各ノードの CoreOS に SSH ログインするために設定する。

### Ignition ファイルの生成
Ignition ファイルは、 自動で CoreOS の初期設定を行うための設定ファイルであり、先ほどの `openshift-install` コマンドから生成できる。

:::note info
`install-config.yaml` は次の `openshift-install` コマンド実行後に自動で削除されるため、バックアップをとっておく。
```
$ cp install-config.yaml install-config.yaml.backup
```
:::

次のコマンドで ignition ファイルを作成する：

```
$ openshift-install create ignition-configs
```

下記ファイルが生成されていればOK：
```
./
├── auth/
│   ├── kubeadmin-password
│   └── kubeconfig
├── bootstrap.ign
├── master.ign
├── metadata.json
└── worker.ign
```

:::note info
これらの ignition ファイルには有効期限が埋め込まれており、24時間で失効する。失効後はクラスタを作成できないため、 ignition ファイルを作り直す必要がある ([参考](https://docs.okd.io/latest/installing/installing_bare_metal/installing-bare-metal.html#installation-user-infra-generate-k8s-manifest-ignition_installing-bare-metal))。
:::


## Terraform によるクラスタのプロビジョニング
Terraform を用いて、要求されるリソースを作成する。
詳細は公式ドキュメンテーション [Installing a cluster on any platform - Installing on any platform | Installing | OKD 4](https://docs.okd.io/latest/installing/installing_platform_agnostic/installing-platform-agnostic.html#installation-machine-requirements_installing-platform-agnostic) を参照。

ノードの最小要求は次の表の通り：

| Machine | Operating System | vCPU | Virtual RAM | Storage | Input/Output Per Second | required number | 備考 |
|---------------|------|---|-------|--------|-----|---|-|
| Bootstrap     | FCOS | 4 | 16 GB | 100 GB | 300 | 1  |クラスタ構築後に削除可能 |
| Control plane | FCOS | 4 | 16 GB | 100 GB | 300 | 3  |
| Compute       | FCOS | 2 | 8 GB  | 100 GB | 300 | >=2 |

DNS の要求は次の表の通り：

| Record | Target |
| ------- | -- |
| `api.<cluster_name>.<base_domain>.` | Load balancer  |
| `api-int.<cluster_name>.<base_domain>.` | Load balancer |
| `*.apps.<cluster_name>.<base_domain>.` | Load balancer|
| `bootstrap.<cluster_name>.<base_domain>.` | Bootstrap node |
| `master<n>.<cluster_name>.<base_domain>.` | Master nodes |
| `worker<n>.<cluster_name>.<base_domain>.` | Worker nodes |


### CoreOS イメージの用意
CoreOS の qcow2 イメージを用意する。推奨される CoreOS のバージョンは `openshift-install` のバージョンに対応して定められており、次のコマンドで確認できる（[参考](https://github.com/openshift/installer/blob/master/docs/user/overview.md#coreos-bootimages)）：

```
$ openshift-install coreos print-stream-json | jq -r '.architectures.x86_64.artifacts.qemu.formats["qcow2.xz"].disk.location'
https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/38.20230609.3.0/x86_64/fedora-coreos-38.20230609.3.0-qemu.x86_64.qcow2.xz
```

次のコマンドでイメージをダウンロードする。
```
$ wget $(openshift-install coreos print-stream-json | jq -r '.architectures.x86_64.artifacts.qemu.formats["qcow2.xz"].disk.location')
```

ダウンロードしたイメージは libvirt のデフォルトプール `/var/lib/libvirt/images` に配置する：
```
$ xz -dv ./fedora-coreos-38.20230609.3.0-qemu.x86_64.qcow2.xz
$ sudo mv fedora-coreos-38.20230609.3.0-qemu.x86_64.qcow2  /var/lib/libvirt/images
```


### Terraform ファイルの作成
下記の `main.tf` を作成する。
（CoreOS の qcow2 イメージの path `vm_base_image_uri` は、適宜修正してください。）

:::note info
下記の `main.tf` は、OKDを作成するための module `github.com/sawa2d2/k8s-on-kvm//okd/` をインポートしています。このモジュールの説明は [本記事内の付録](#付録) でしています。
:::


```main.tf
locals {
  user_home_directory = pathexpand("~")
}

module "okd" {
  source = "github.com/sawa2d2/k8s-on-kvm//okd/"

  ## Localhost:
  # libvirt_uri = "qemu:///system"
  ## Remote:
  # libvirt_uri = "qemu+ssh://<remote-user>@<remote-host>/system?keyfile=${local.user_home_directory}/.ssh/id_rsa&known_hosts_verify=ignore"
  ## Remote via bastion:
  ##   Forward port in advance.
  ##   $ ssh -C -N -f -L 50000:<remote-user>@<remote-host>:22 <bastion-host> -p <bastion-port>
  # libvirt_uri = "qemu+ssh://<remote-user>@localhost:50000/system?keyfile=${local.user_home_directory}/.ssh/id_rsa&known_hosts_verify=ignore"
  libvirt_uri = "qemu:///system"

  domain       = "ocp4.example.com"
  network_name = "okd"
  bridge_name  = "tt0"
  cidr         = "192.168.126.0/24"
  gateway      = "192.168.126.1"
  nameservers  = ["192.168.126.1"]

  load_balancer_ip = "192.168.126.1" # or `null`
  # load_balancer_ip = null とした場合、DNS ラウンドロビンで分散する。
  # HAProxy が不要になる一方、ホスト外部からクラスタにアクセスする入口はなくなる。


  # Download a CoreOS image from:
  #   $ openshift-install version
  #   openshift-install 4.14.0-0.okd-2023-10-28-073550
  #   $ wget $(openshift-install coreos print-stream-json | jq -r '.architectures.x86_64.artifacts.qemu.formats["qcow2.xz"].disk.location')
  #   $ xz -dv *.qcow2.xz
  vm_base_image_uri = "/var/lib/libvirt/images/fedora-coreos-38.20231002.3.1-qemu.x86_64.qcow2"
  pool              = "default"

  bootstrap = {
    name          = "bootstrap"
    vcpu          = 4
    memory        = 12288                    # in MiB
    disk          = 100 * 1024 * 1024 * 1024 # 100 GB
    ip            = "192.168.126.100"
    ignition_file = "bootstrap.ign"
    volumes       = []
  }

  masters = [
    {
      name          = "master0"
      vcpu          = 4
      memory        = 12288                    # in MiB
      disk          = 100 * 1024 * 1024 * 1024 # 100 GB
      ip            = "192.168.126.101"
      ignition_file = "master.ign"
      volumes       = []
    },
    {
      name          = "master1"
      vcpu          = 4
      memory        = 12288                    # in MiB
      disk          = 100 * 1024 * 1024 * 1024 # 100 GB
      ip            = "192.168.126.102"
      ignition_file = "master.ign"
      volumes       = []
    },
    {
      name          = "master2"
      vcpu          = 4
      memory        = 12288                    # in MiB
      disk          = 100 * 1024 * 1024 * 1024 # 100 GB
      ip            = "192.168.126.103"
      ignition_file = "master.ign"
      volumes       = []
    },
  ]

  workers = [
    {
      name          = "worker0"
      vcpu          = 2
      memory        = 8192                     # in MiB
      disk          = 100 * 1024 * 1024 * 1024 # 100 GB
      ip            = "192.168.126.104"
      ignition_file = "worker.ign"
      volumes = [
        {
          name = "additional_disk"
          disk = 200 * 1024 * 1024 * 1024 # 200 GB
        }
      ]
    },
    {
      name          = "worker1"
      vcpu          = 2
      memory        = 8192                     # in MiB
      disk          = 100 * 1024 * 1024 * 1024 # 100 GB
      ip            = "192.168.126.105"
      ignition_file = "worker.ign"
      volumes = [
        {
          name = "additional_disk"
          disk = 200 * 1024 * 1024 * 1024 # 200 GB
        }
      ]
    },
  ]
}
```

次のコマンドを実行し、VM とネットワークを作成する。
```
$ terraform init
$ terraform apply -auto-approve
```

ネットワークの設定は `/etc/libvirt/qemu/networks/okd.xml` から確かめることができる。DNSレコードが下記のようになっていれば OK。
```
$ less /etc/libvirt/qemu/networks/okd.xml
...
  <dnsmasq:options>
    <dnsmasq:option value='address=/api.ocp4.example.com/192.168.126.1'/>
    <dnsmasq:option value='address=/api-int.ocp4.example.com/192.168.126.1'/>
    <dnsmasq:option value='address=/apps.ocp4.example.com/192.168.126.1'/>
    <dnsmasq:option value='address=/bootstrap.ocp4.example.com/192.168.126.100'/>
    <dnsmasq:option value='address=/master0.ocp4.example.com/192.168.126.101'/>
    <dnsmasq:option value='address=/master1.ocp4.example.com/192.168.126.102'/>
    <dnsmasq:option value='address=/master2.ocp4.example.com/192.168.126.103'/>
    <dnsmasq:option value='address=/worker0.ocp4.example.com/192.168.126.104'/>
    <dnsmasq:option value='address=/worker1.ocp4.example.com/192.168.126.105'/>
  </dnsmasq:options>
```


ホスト内において、ドメイン `ocp4.example.com` に対して libvirt の DNS `192.168.126.1` を利用させる：

```
$ sudo resolvectl dns tt0 192.168.126.1
$ sudo resolvectl domain tt0 ~ocp4.example.com
```

:::note info
上記の操作は永続化されないので、ネットワークを作成するたびに実行する。
永続化する場合は、`/etc/systemd/resolved.conf` を次のように編集する：

```:/etc/systemd/resolved.conf
[Resolve]
DNS=192.168.126.1
Domains=~ocp4.example.com
```

次を実行して設定を反映する：
```
sudo systemctl restart systemd-resolved
```
:::

ドメインが解決されることを確認する。
```
$ nslookup api.ocp4.example.com
Server:         127.0.0.53
Address:        127.0.0.53#53

Non-authoritative answer:
Name:   api.ocp4.example.com
Address: 192.168.126.1
```

## Load Balancer (HAProxy) の起動

まず HAProxy をインストールする。
```
$ dnf install haproxy
```

[公式ドキュメントのサンプル](https://docs.okd.io/latest/installing/installing_bare_metal/installing-bare-metal.html#installation-load-balancing-user-infra-example_installing-bare-metal) をもとに、下記 HAProxy の設定ファイルを `/etc/haproxy/haproxy.cfg` に置く。

```:/etc/haproxy/haproxy.cfg
global
  log         127.0.0.1 local2
  pidfile     /var/run/haproxy.pid
  maxconn     4000
  daemon
defaults
  mode                    http
  log                     global
  option                  dontlognull
  option http-server-close
  option                  redispatch
  retries                 3
  timeout http-request    10s
  timeout queue           1m
  timeout connect         10s
  timeout client          1m
  timeout server          1m
  timeout http-keep-alive 10s
  timeout check           10s
  maxconn                 3000
listen api-server-6443 
  bind *:6443
  mode tcp
  server bootstrap bootstrap.ocp4.example.com:6443 check inter 1s backup
  server master0 master0.ocp4.example.com:6443 check inter 1s
  server master1 master1.ocp4.example.com:6443 check inter 1s
  server master2 master2.ocp4.example.com:6443 check inter 1s
listen machine-config-server-22623 
  bind *:22623
  mode tcp
  server bootstrap bootstrap.ocp4.example.com:22623 check inter 1s backup
  server master0 master0.ocp4.example.com:22623 check inter 1s
  server master1 master1.ocp4.example.com:22623 check inter 1s
  server master2 master2.ocp4.example.com:22623 check inter 1s
listen ingress-router-443 
  bind *:443
  mode tcp
  balance source
  server worker0 worker0.ocp4.example.com:443 check inter 1s
  server worker1 worker1.ocp4.example.com:443 check inter 1s
listen ingress-router-80 
  bind *:80
  mode tcp
  balance source
  server worker0 worker0.ocp4.example.com:80 check inter 1s
  server worker1 worker1.ocp4.example.com:80 check inter 1s

```

:::note warn
最後の1行は、空行にしておかないとエラーになるので注意。
:::

HAProxy を起動する。

```
$ systemctl start haproxy.service
```

## ホームネットワーク上からクラスタにアクセスできるようにする

ホストから直接ではなく、外部のクライアントからクラスタにアクセスしたい場合、ルートドメインとホスト IP の対応をホームネットワーク内の DNS に追加する。例えば、ホームネットワーク192.168.8.0/24 上でホスト 192.168.8.10 を公開する場合、次のようなレコードを DNS に追加すればよい。

```
address=/ocp4.example.com/192.168.8.10
```

![publish.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/a37d153c-f402-407e-5b26-fa002a1426f0.png)

:::note info
ホームネットワークの DNS を設定するのが難しい場合、クライアントマシン側の `/etc/hosts` 次のようにを設定してもよい。

```:/etc/hosts
192.168.8.10 ocp4.example.com
192.168.8.10 api.ocp4.example.com
192.168.8.10 api-int.ocp4.example.com
192.168.8.10 console-openshift-console.apps.ocp4.example.com
192.168.8.10 oauth-openshift.apps.ocp4.example.com
```
:::

## Bootstrap の完了を待つ
次のコマンドで Bootstrap の進捗を確認できる：
```
$ openshift-install wait-for bootstrap-complete --log-level=info
```

さらに別ターミナルで次のコマンドを実行すると、リアルタイムのログを確認できる：
```
$ tail --follow .openshift_install.log
```

より詳細なログを確認する場合、bootstrap ノード内で次を実行する：
```
$ journalctl -f -u bootkube -u openshift
```

Bootstrap が完了すると、次のようなログが確認できる。
```
$ openshift-install wait-for bootstrap-complete --log-level=info
INFO Waiting up to 20m0s (until 11:08AM JST) for the Kubernetes API at https://api.ocp4.example.com:6443...
INFO API v1.27.1-3348+f67aeb31c9b95f-dirty up
INFO Waiting up to 30m0s (until 11:18AM JST) for bootstrapping to complete...
INFO It is now safe to remove the bootstrap resources
INFO Time elapsed: 0s
```

:::note warn
インストールがうまくいかない場合、 [Troubleshooting installations - Troubleshooting | Support | OKD 4](https://docs.okd.io/latest/support/troubleshooting/troubleshooting-installations.html) を参照しながらトラブルシューティングを行う。
:::

Bootstrap 完了後は、次のコマンドで Bootstrap ノードを削除することができる：
```
$ terraform destroy -auto-approve -target=module.okd.module.bootstrap
$ terraform apply -auto-approve -target=module.okd.libvirt_network.network -var="exclude_bootstrap=true"
```

Bootstrap 完了後は、次のコマンドでインストールプロセス完了までのログをモニタリングできる。
```
$ ./openshift-install agent wait-for install-complete --log-level=debug
```


## OKD4 クラスタへのアクセス

クライアントに OpenShift 用 の CLI `oc` コマンドをインストールする：
```
$ curl -LO https://github.com/okd-project/okd/releases/download/4.13.0-0.okd-2023-09-30-084937/openshift-client-linux-4.13.0-0.okd-2023-09-30-084937.tar.gz
$ openshift-client-linux-4.13.0-0.okd-2023-09-30-084937.tar.gz
$ mv oc /usr/local/bin
```

インストーラが生成した Kubeconfig ファイルを環境変数に通す。
```
$ export KUBECONFIG=`pwd`/auth/kubeconfig
```

`oc` コマンドからクラスタにアクセスできることを確認する。
```
$ oc whoami
system:admin
```

### CSR の承認
ブートストラップ完了直後は worker node の CSR (CertificateSigningRequest) が承認されていないため、使用できない状態にある。 
```
$ oc get node
NAME      STATUS   ROLES                  AGE   VERSION
master0   Ready    control-plane,master   47m   v1.27.6+f67aeb3
master1   Ready    control-plane,master   47m   v1.27.6+f67aeb3
master2   Ready    control-plane,master   48m   v1.27.6+f67aeb3
```


次のコマンドで CSR の一覧を確認する。
```
$ oc get csr
csr-2x4wd                                        26m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Pending
csr-82sgp                                        50m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Approved,Issued
csr-8hb7s                                        31m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Pending
csr-8vlll                                        10m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Pending
csr-9kcj8                                        9s    kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Pending
csr-9lbqw                                        15m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Pending
csr-bn4w4                                        51m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Approved,Issued
csr-ch4vc                                        31m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Pending
csr-j8jh6                                        29m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Pending
csr-mjk2h                                        29m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Pending
csr-rdv2b                                        30m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Pending
csr-wzcgp                                        50m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         <none>              Approved,Issued
```

Pending になっているものを `oc amd certificate approve <csr>` で承認する。全ての Pending の CSR を承認する場合は、次のコマンドを実行する：
```
$ oc get csr -o go-template='{{range .items}}{{if not .status}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}' | xargs --no-run-if-empty oc adm certificate approve
```

少し待つと Worker node がReadyになる
```
$ oc get node
NAME      STATUS   ROLES                  AGE     VERSION
master0   Ready    control-plane,master   61m     v1.27.6+f67aeb3
master1   Ready    control-plane,master   61m     v1.27.6+f67aeb3
master2   Ready    control-plane,master   61m     v1.27.6+f67aeb3
worker0   Ready    worker                 9m22s   v1.27.6+f67aeb3
worker1   Ready    worker                 9m25s   v1.27.6+f67aeb3
```

:::note
ROLES の `control-plane` と `master` は、どちらも同じものである。旧 `master` がより中立的な `control-plane` という表現に変えられたが、`master` も互換性のために残されている、という事情らしい。
:::

### Web コンソールへのアクセス
コンソールのURLを調べる
```
$ oc whoami --show-console
https://console-openshift-console.apps.ocp4.example.com
```

ブラウザからアクセスする。

![Screenshot from 2023-11-09 14-54-57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/e93bb7f4-161b-8cdf-c68a-bfd32883b29c.png)

ユーザ名は `kubeadmin`、パスワードは `./auth/kubeamind-password` に書かれたものを使用し、コンソールにログインする。

![Screenshot from 2023-11-09 14-55-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/bb0fd975-87e0-e1f0-65ad-602e432a8854.png)

## その他の初期設定

OKD は OpenShift と異なり、初期設定では Community 提供の Operator しか表示されないように制限されているので、解除する。
```

$ oc patch operatorhub cluster -p '{"spec": {"disableAllDefaultSources": false}}' --type=merge
```


## 付録

### Terraform module の詳細
モジュールは次の3ファイルからなる。

- `provider.tf`
- `variables.tf`
- `main.tf`

#### provider.tf

Terraform Libvirt Provider をインポートする。

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

provider "libvirt" {
  uri = var.libvirt_uri
}
```

#### variables.tf

変数を定義する。

```tf:variables.tf
variable "bootstrap" {
  type = object({
    name          = string
    vcpu          = number
    memory        = number
    disk          = number
    ip            = string
    ignition_file = string
    volumes = list(
      object({
        name = string
        disk = number
      })
    )
  })
}

variable "masters" {
  type = list(
    object({
      name          = string
      vcpu          = number
      memory        = number
      disk          = number
      ip            = string
      ignition_file = string
      volumes = list(
        object({
          name = string
          disk = number
        })
      )
    })
  )
}

variable "workers" {
  type = list(
    object({
      name          = string
      vcpu          = number
      memory        = number
      disk          = number
      ip            = string
      ignition_file = string
      volumes = list(
        object({
          name = string
          disk = number
        })
      )
    })
  )
}

variable "domain" {
  type = string
}

variable "exclude_bootstrap" {
  type    = bool
  default = false
}

variable "load_balancer_ip" {
  type    = string
  default = null
}

variable "libvirt_uri" {
  type    = string
  default = "qemu:///system"
}

variable "pool" {
  type    = string
  default = "default"
}

variable "vm_base_image_uri" {
  type = string
}

variable "network_name" {
  type    = string
  default = "okd"
}

variable "cidr" {
  type    = string
  default = "192.168.126.0/24"
}

variable "bridge_name" {
  type    = string
  default = "tt0"
}

variable "gateway" {
  type    = string
  default = "192.168.126.1"
}

variable "nameservers" {
  type    = list(string)
  default = ["192.168.126.1"]
}
```

#### main.tf

VM、およびネットワークを作成するロジックを書く。なお、変数 `load_balancer_ip` は HAProxy が稼働するホストの IP 指定する変数だが、これを `null` とした場合、DNS ラウンドロビンによる分散を行う。

```tf:main.tf
locals {
  dns_hosts = var.load_balancer_ip == null ? concat(
    [
      for vm in var.masters : {
        hostname = "api.${var.domain}"
        ip       = vm.ip
      }
    ],
    [
      for vm in var.masters : {
        "hostname" : "api-int.${var.domain}"
        "ip" : vm.ip
      }
    ],
    var.exclude_bootstrap ? [] : [
      {
        hostname = "api.${var.domain}"
        ip       = var.bootstrap.ip
      },
      {
        hostname = "api-int.${var.domain}"
        ip       = var.bootstrap.ip
      },
    ],
  ) : []

  dnsmasq_options = concat(
    var.load_balancer_ip != null ? [
      {
        option_name  = "address"
        option_value = "/api.${var.domain}/${var.load_balancer_ip}"
      },
      {
        option_name  = "address"
        option_value = "/api-int.${var.domain}/${var.load_balancer_ip}"
      },
      {
        option_name  = "address"
        option_value = "/apps.${var.domain}/${var.load_balancer_ip}"
      },
    ] : [],
    var.exclude_bootstrap ? [] : [
      {
        option_name  = "address"
        option_value = "/bootstrap.${var.domain}/${var.bootstrap.ip}"
      },
    ],
    [
      for vm in var.masters : {
        option_name  = "address",
        option_value = "/${vm.name}.${var.domain}/${vm.ip}"
      }
    ],
    [
      for vm in var.workers : {
        option_name  = "address"
        option_value = "/${vm.name}.${var.domain}/${vm.ip}"
      }
    ],
  )
}

resource "libvirt_network" "network" {
  name      = var.network_name
  mode      = "nat"
  domain    = var.domain
  bridge    = var.bridge_name
  addresses = [var.cidr]
  autostart = true

  dns {
    local_only = true
    dynamic "hosts" {
      for_each = local.dns_hosts
      content {
        hostname = hosts.value.hostname
        ip       = hosts.value.ip
      }
    }
  }

  dnsmasq_options {
    dynamic "options" {
      for_each = local.dnsmasq_options
      content {
        option_name  = options.value["option_name"]
        option_value = options.value["option_value"]
      }
    }
  }
}

module "bootstrap" {
  source = "github.com/sawa2d2/terraform-modules//libvirt-ignition-nat/"

  libvirt_uri       = var.libvirt_uri
  pool              = var.pool
  vm_base_image_uri = var.vm_base_image_uri
  network_name      = var.network_name
  vms               = [var.bootstrap]
}

module "cluster" {
  source = "github.com/sawa2d2/terraform-modules//libvirt-ignition-nat/"

  libvirt_uri       = var.libvirt_uri
  pool              = var.pool
  vm_base_image_uri = var.vm_base_image_uri
  network_name      = var.network_name
  vms               = concat(var.masters, var.workers)
}
```

中でさらに使用している Terraform モジュールの詳細は次のURLを参照（libvirt terraform provider のラッパーである）。

https://github.com/sawa2d2/terraform-libvirt-modules/tree/main/libvirt-ignition-nat

## 補足
### プロキシの設定
プロキシを設定する場合は、`install-config.yaml` を次のように書く
（本記事内 『[OKD4 クラスタ設定ファイルの用意](https://qiita.com/drafts/3cf9c9d5d9ce5f589124/edit#install-configyaml-%E3%81%AE%E8%A8%AD%E5%AE%9A) 』の `install-config.yaml` との違いは、 `proxy` および `networking.machineNetwork` を指定しているかどうか）。

```install-config.yaml
---
apiVersion: v1
baseDomain: example.com
proxy:
  httpProxy: http://<username>:<pswd>@<ip>:<port>
  httpsProxy: https://<username>:<pswd>@<ip>:<port>
  noProxy: example.com
compute:
- hyperthreading: Enabled
  name: worker
  replicas: 2
controlPlane:
  hyperthreading: Enabled
  name: master
  replicas: 3
metadata:
  name: ocp4
networking:
  clusterNetwork:
  - cidr: 10.128.0.0/14
    hostPrefix: 23
  serviceNetwork:
  - 172.30.0.0/16
  networkType: OVNKubernetes
  machineNetwork:
  - cidr: 192.168.126.0/24
platform:
  none: {}
pullSecret: '{"auths":{"fake":{"auth":"aWQ6cGFzcwo="}}}'
sshKey: |
  ssh-ed25519 AAAA... user1@example.com
  ssh-ed25519 AAAA... user2@example.com
```

Proxy を指定する場合、`machineNetwork`（OpenShift クラスタを構成するノード間のネットワーク、本記事における `192.168.126.0/24`）を明示すると、そのネットワークは自動的に noProxy 扱いになる。

:::note info
Proxy を指定する場合、`networking.machineNetwork[].cidr`、`networking.clusterNetwork[].cidr`、および `networking.serviceNetwork[]` は自動的に noProxy として扱われる 。
詳細は [1.3.7.3. インストール時のクラスター全体のプロキシーの設定 OpenShift Container Platform 4.6 | Red Hat Customer Portal](https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.6/html/installing_on_bare_metal/installation-configure-proxy_installing-restricted-networks-bare-metal) を参照。

:::

## 参考資料
### OKD4 公式ドキュメント
- [Installation overview | Installing | OKD 4](https://docs.okd.io/latest/installing/index.html)
### OKD4 on other platforms:
- [Red Hat OKD4 on AWS を使って見た - Qiita](https://qiita.com/MahoTakara/items/1c672cce82ba5a6f2ddb)
- [OKD on OpenStack - Qiita](https://qiita.com/nakkoh/items/a2e99a960330cc1d3350)

### libvirt for OKD 関連
- [okd/Guides/UPI/libvirt/libvirt.md at 968dbbeec8e089bddad887cff6d85a39d2c60ab1 · okd-project/okd](https://github.com/okd-project/okd/blob/968dbbeec8e089bddad887cff6d85a39d2c60ab1/Guides/UPI/libvirt/libvirt.md)
- [Libvirt HOWTO · openshift/installer](https://github.com/openshift/installer/blob/master/docs/dev/libvirt/README.md)
- https://github.com/openshift/installer/tree/master/data/data/libvirt ... 参考にした OpenShift リポジトリ内の Terraform リソース

## 関連記事

https://qiita.com/sawa2d2/items/f95b9da08c4aa3b8a429