OpenShift VirtualizationでRHELのHigh Availabilityを実現！

# はじめに
OpenShiftの基本機能である「OpenShift Virtualization」では、Kubernetesの上で仮想マシンを起動することができます。勿論、RHEL（Red Hat Enterprise Linux）の起動も対応しています。

さて、RHELは複数台をクラスタとして高可用性（High Availability）を実現するアドオン「High Availability Add-On（略して、HA Add-on）を追加オプションとして提供しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/29c81b9e-7a00-4f0a-9f18-0994cf60d76c.png)

複数台のRHEL仮想マシンでHAクラスタを組むことで、主系の仮想マシンにおいて何らかの原因でアプリが動かなくなった場合、即時に副系に切り替え（フェイルオーバ）することで、アプリのダウンタイムを極力下げることが可能です。

OpenShift Virtualizationは、その上で起動するRHEL仮想マシンに関して、[RHEL HA Add-onをサポート](https://www.redhat.com/en/blog/red-hat-openshift-virtualization-faqs)しています。
> **Does OpenShift Virtualization support application clustering?**
Yes, the Red Hat Enterprise Linux (RHEL) High-Availability Add-on (Pacemaker) and Windows Server Failover Clustering (WSFC) are supported.

ミッションクリティカルなシステムを提供する場合、仮想基盤のみならず「OSレイヤでのHA確保」も重要な観点です。

:::note
RHEL HA Add-onについての詳細は、[Red Hatのブログ](https://rheb.hatenablog.com/entry/201812-rhel-ha-addon)を御覧ください。
:::

## OSレイヤのクラスタリングのメリット / デメリット

OSレイヤのクラスタリングのメリットとしては、大きく以下が考えられます。
- プロセスレベルで死活監視可能
- もし仮想マシン（主系）が落ちてしまっても、副系に切り替えるだけで、すぐにアプリケーションの稼働が継続できる

一方でデメリットとしては以下が考えられます。
- 仮想マシンを複数起動するため仮想基盤のリソースを多く要する
- 仮想マシンの数が増え、RHELのライセンスコストが増大する
- 共有ストレージの管理運用が必要になる

## OpenShift VirtualizationでRHEL HA Add-onを利用するメリット
RHEL HA Add-onは複数の仮想基盤やAWSを始めとしたパブリッククラウドでも利用できますが、いずれのインフラであってもHAクラスタを組むからには、クラスタに参加する仮想マシンの数だけRHELのサブスクリプションが必要です。

しかし、[対応するOpenShiftのEdition](https://www.redhat.com/en/blog/subscribing-rhel-vms-in-openshift-virtualization)においては、OpenShift Virtualizationで起動するRHEL仮想マシンのサブスクリプションがエンタイトルされています。
>If you are an OpenShift bare-metal customer, your OpenShift entitlement includes RHEL entitlements for any hosted RHEL virtual machines. 

つまり、**RHEL仮想マシンをいくら起動しても追加料金は掛かりません**。

さらに、ストレージ共有の仕組みはKubernetesのネイティブな機能を利用し、比較的簡単に実装可能です。これがOpenShift VirtualizationでRHEL HA Add-onを組むメリットです。

:::note
RHELサブスクリプションのエンタイトルメントについては、[こちらの記事](https://qiita.com/masaki-oomura/items/37dd4008e1b6d230954a)も参照ください。
:::

::: note warn
HA Add-onのサブスクリプションは別途購入が必要です。
:::

# いまからやることのイメージ
をこんな感じで絵に示します。まずは物理構成イメージです。

## クラスタの物理構成イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/0895cd67-bef1-4699-a285-8931eecd0633.png)

今回はサーバ調達の容易性に鑑み、AWSを用いてOpenShiftクラスタを構築しました。また、既にOpenShift Virtualization及びODFを利用可能としています。

### クラスタの情報
- [3ノードクラスタ](https://rheb.hatenablog.com/entry/3-node-ocp)で構築。
- OpenShift Virtualization Operatorを介してOpenShift Virtualizationが利用可能な状態。
- ストレージシステムとしてOpenShift Data Foundation（ODF）を利用。
- 追加でベアメタルインスタンス（c5.metal）を2台アタッチ。

::: note warn
OpenShift Virtualizationにおいて仮想マシンをデプロイするためにはベアメタルインスタンスが必要です。今回はc5.metalを2台アタッチしました。仮想マシンはこの2台のいずれかにスケジュールされ、仮想マシンノードにはスケジュールされません。なお、AWSのベアメタルインスタンスは[相応のコスト](https://aws.amazon.com/jp/ec2/pricing/on-demand/)を要しますので、ご注意ください。
:::


次は論理構成イメージを示します。

## 仮想マシンの論理構成イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/ee9b469e-8607-4b55-ad7d-44a9c07fb6b0.png)

なんだかみょうちくりんな絵になってしまいました...
ちょっと分かりづらいので、以下補足説明です。

### HAクラスタ構成
- HAクラスタは2台構成（rhel9-ha1 / rhel9-ha2）とします。
- 各VMが必ず異なるノードにスケジュールされるようにします。

### ネットワーク構成
- 各VMに追加した2つめのvNIC（eth1）にはそれぞれ静的IPアドレスを付与します。
- またeth1は追加のオーバレイネットワークに参加し、このネットワークをPacemakerのHeartbeatネットワーク（互いの死活を確認する経路）とします。
- 元々Podについているeth0はデフォルトのオーバレイネットワーク（Pod Network）に接続します。


### サンプルアプリ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/3cef0425-401c-468a-b4ef-a82566e4310b.png)

- アクセスするたびにカウンター値がインクリメントされていくだけの簡単なステートフルアプリです。
- Node.jsで作成し、カウンター値（ステート）はsqliteで保存します。
- アプリは`/var/lib/nodeapp`で動作させます。

### ストレージ
- 各VMの`/var/lib/nodeapp`に「nodeapp-storage」という共有ディスクをマウントします。
- ディスクの永続化を行うためにPersistentVolumeClaim（PVC）を利用します。
- PVCは、異なるノードにスケジュールされた2つのVMから読み書き可能となるReadWriteMany(RWX)で永続ボリュームを要求するようにします。


この構成にて、以下を試します。
1. rhel9-ha1のNode.jsプロセスをKill（強制終了）した場合、rhel9-ha1上で速やかにプロセスが再起動するか？
1. rhel9-ha1を停止（Stop）した場合、速やかにrhel9-ha2にてアプリの動作が引き継がれるか？
1. rhel9-ha1がスケジュールされているノードをシャットダウンした際に、速やかにrhel9-ha2にてアプリの動作が引き継がれるか？

「1.」についてはプロセスレベルの障害を再現、「2.」についてはOSレベルの障害を再現します。また「3.」については仮想基盤を構成するホストマシン（Kubernetesのノード）レベルでの障害を再現します。

それでは早速環境を作りましょう〜！

:::note warn
今回はOpenShift Virtualization上でRHEL HAを簡単かつ分かりやすく試すことを目的にしており、商用利用ではサポートされない構成です。具体的なサポート対象外の設定や構成に関しては、本記事の該当箇所で適宜補足しています。[公式ドキュメント](https://docs.redhat.com/ja/documentation/red_hat_enterprise_linux/9/html-single/configuring_and_managing_high_availability_clusters/index#proc_providing-feedback-on-red-hat-documentation_configuring-and-managing-high-availability-clusters)と合わせて適宜ご参照ください。
:::

# 事前準備
OpenShift Virtualizationを利用可能なOpenShiftクラスタにおいて、事前に以下を実行します。

## プロジェクト（＝NameSpace）作成
OpenShiftクラスタ内に「rhel-ha」という名前のプロジェクトを作成しておきます。

![スクリーンショット 2025-02-21 23.33.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/518faa0d-4a32-4e2d-93c6-8232e94fcb13.png)


## NAD作成
2nd Overlay Netowork（追加のオーバレイネットワーク）を作成します。「NetworkAttachmentDefinition（NAD）」というカスタムリソースを作成すると、追加のオーバレイネットワークを作成することができます。NADはNameSpaceスコープのリソースのため、NADによって作成される追加のオーバレイネットワークは、同じNameSpace内のPodだけが参加できます。

OpenShiftコンソールの管理者パースペクティブにて、「ネットワーク」メニューから「NetworkAttachmentDefinitions」を選んで作成することが可能です。

![スクリーンショット 2025-02-21 23.31.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/58d86111-4cbd-4439-a25b-53a51758bf13.png)

なお、以下のマニフェストを適用（`oc apply -f`）するだけでも同NADを作成可能です。

```nad-rhel-ha.yaml
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: nad-rhel-ha
  namespace: rhel-ha
spec:
  config: |-
    {
        "cniVersion": "0.3.1",
        "name": "nad-rhel-ha",
        "type": "ovn-k8s-cni-overlay",
        "netAttachDefName": "rhel-ha/nad-rhel-ha",
        "topology": "layer2"
    }
```
:::note 
OVN Kubernetes L2 overlay networkは、OpenShiftに標準で採用されているSoftware Defined Network（SDN）であるOVN Kubernetesを活用したネットワークです。このネットワークに参加するPodは、ノードを跨いでレイヤ2で通信できます。

詳細はOpenShiftの[公式ドキュメント](https://docs.redhat.com/ja/documentation/openshift_container_platform/4.17/html/virtualization/virt-connecting-vm-to-ovn-secondary-network)を確認いただくことをおすすめします。
:::

## Secret作成
同プロジェクト内に、RHEL仮想マシンに登録するSSH公開鍵をSecretとして登録しておきます。YAMLは以下の通りです。

```public-key-rhel-ha.yaml
kind: Secret
apiVersion: v1
metadata:
  name: public-key-rhel-ha
  namespace: rhel-ha
data:
  key: <SSH公開鍵をBase64エンコードしたもの>
type: Opaque
```

## PVC作成
共有ディスク（nodeapp-storage）のためのPVCを作成しておきます。今回、要求するPVのスペックは以下の通りです。
- 容量：5GiB
- ボリュームモード：ブロック
- アクセスモード（読み書きモード）：ReadWriteMany(RWX)
- StorageClass：ocs-storagecluster-ceph-rbd-virtualization

YAMLは以下の通りです。

```nodeapp-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: nodeapp-pvc
  namespace: rhel-ha
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
  storageClassName: ocs-storagecluster-ceph-rbd-virtualization
  volumeMode: Block
```

:::note 
「ocs-storagecluster-ceph-rbd-virtualization」は、ODFによって提供されるCeph RBD（RADOS Block Device）を利用した仮想マシン向けのストレージクラスです。仮想マシンのディスクイメージを永続化し、高可用性やスケーラビリティを備えたブロックストレージを提供します。OpenShift Virtualizationが利用可能なクラスタにODFをインストールすると、当該StorageClassが自動的に作成されます。OpenShift Virtualizationにおける仮想マシン用のディスクの永続化に最適化されているので、特段理由がない限り、こちらを利用することがおすすめです。
:::

## ゲストシステムログへのアクセスの有効化
RHEL仮想マシンを起動する前に、仮想マシンのログへのアクセスを有効化しておきます。これにより、OpenShiftのコンソール画面上で仮想マシンの`var/log/`配下のログ内容が出力順に確認できるようになります。

![スクリーンショット 2025-02-28 15.20.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/ae5f99f2-32cd-4125-abfa-a1dec7b5c74d.png)

Virtualizationパースペクティブの「Overviewメニュー」にて、「Settings」から「Guest managemnet」と進み、「Enable guest system log access」のトグルをONにしてください。


# 1台目のRHEL仮想マシンの起動
それでは早速RHEL仮想マシンを立てましょう〜！まずは1台目のrhel9-ha1から起動します。

## Templateを選択
OpenShiftのコンソール画面にて、管理者パースペクティブの「Virtualization」メニューから「VirtualMachines」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/40b72854-43ea-4182-bf19-549e8a9af6d2.png)

「Create VirtualMachine」から「From Template」を選択すると、テンプレートを使って仮想マシンを簡単に作成できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/62788a75-2509-4ad5-ba01-121071f7f12b.png)

「Red Hat Enterprise Linux 9 VM」を選択します。

![スクリーンショット 2025-02-22 0.22.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/816ca1e6-db79-4992-85d3-ce214aa17bb3.png)

「Customize Virtualmachine」を選択します。

![スクリーンショット 2025-02-22 0.27.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/e30f5f6d-9e78-412f-b28c-251fa6ed420a.png)


## マニフェストファイルを適用
「YAML」タブに切り替えたら、内容を以下のものに置き換え、「保存」をクリックしてください。

```rhel9-ha1.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  labels:
    app: rhel9-ha1
  name: rhel9-ha1
  namespace: rhel-ha
spec:
  dataVolumeTemplates:
    - apiVersion: cdi.kubevirt.io/v1beta1
      kind: DataVolume
      metadata:
        creationTimestamp: null
        name: rhel9-ha1
      spec:
        sourceRef:
          kind: DataSource
          name: rhel9
          namespace: openshift-virtualization-os-images
        storage:
          resources:
            requests:
              storage: 30Gi
          storageClassName: ocs-storagecluster-ceph-rbd-virtualization
  running: true
  template:
    metadata:
      labels:
        cluster: my_cluster
        role: primary
    spec:
      accessCredentials:
        - sshPublicKey:
            propagationMethod:
              noCloud: {}
            source:
              secret:
                secretName: public-key-rhel-ha
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: role
                    operator: In
                    values:
                      - secondary
              topologyKey: kubernetes.io/hostname
      architecture: amd64
      domain:
        cpu:
          cores: 1
          sockets: 1
          threads: 1
        devices:
          disks:
            - disk:
                bus: virtio
              name: rootdisk
            - disk:
                bus: virtio
              name: cloudinitdisk
            - disk:
                bus: virtio
              name: nodeapp-storage
              shareable: true
          interfaces:
            - macAddress: ''
              masquerade: {}
              model: virtio
              name: default
            - bridge: {}
              macAddress: ''
              model: virtio
              name: eth1
          rng: {}
        features:
          acpi: {}
          smm:
            enabled: true
        firmware:
          bootloader:
            efi: {}
        machine:
          type: pc-q35-rhel9.4.0
        memory:
          guest: 2Gi
        resources: {}
      networks:
        - name: default
          pod: {}
        - multus:
            networkName: nad-rhel-ha
          name: eth1
      terminationGracePeriodSeconds: 180
      volumes:
        - dataVolume:
            name: rhel9-ha1
          name: rootdisk
        - name: nodeapp-storage
          persistentVolumeClaim:
            claimName: nodeapp-pvc
        - cloudInitNoCloud:
            networkData: |
              ethernets:
                eth1:
                  addresses:
                    - 192.168.122.11/24
              version: 2
            userData: |
              #cloud-config
              user: cloud-user
              password: cloud-password
              chpasswd:
                expire: false
              package_update: true
              package_upgrade: true
              packages:
                - httpd
              runcmd:
                - subscription-manager register --org=<ORG_ID> --activationkey=<ACTIVATION_KEY>
                - subscription-manager repos --enable=rhel-9-for-x86_64-highavailability-rpms
                - dnf install pcs pacemaker fence-agents-all nodejs sqlite -y
                - systemctl start pcsd.service
                - systemctl enable pcsd.service
                - echo "hacluster:rhel9ha" | chpasswd
                - mkdir -p /var/lib/nodeapp
                - echo "192.168.122.11 rhel9-ha1" >> /etc/hosts
                - echo "192.168.122.12 rhel9-ha2" >> /etc/hosts
          name: cloudinitdisk
```

::: note warn
YAMLファイルの中にはユーザの環境毎に異なるパラメータがあります。

|パラメータ|内容|
|---|---|
|`<ORG_ID>`|Red Hat Accountが所属する組織のID|
|`<ACTIVATION_KEY>`|RHELのサブスクリプションを有効化するために作成するキー|

どちらの内容についても、[ハイブリッドクラウドコンソールのアクティベーションキー作成画面](https://console.redhat.com/insights/connector/activation-keys) から確認できます。

![スクリーンショット 2025-02-22 1.26.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/5e909a4a-9022-4c09-a74b-4bf1cc5b7f96.png)

アクティベーションキーを作成したら、その名称を`<ACTIVATION_KEY>`に入れてください。

:::


## マニフェストの内容を確認
今貼り付けたマニフェストファイルを使って仮想マシンを即時起動しても良いのですが、中身についてしっかり見ておくことにします。

:::note
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/691bfce1-dfaf-4737-ada7-cef675e0a435.png)
画像：[児童相談所の保護訓練で虐待親になりきる警察が「役に入りすぎ」と話題に](http://blog.livedoor.jp/qmanews/archives/52313725.html)
:::

### Labels
この仮想マシンには以下の通りラベルを定義しました。
```yaml
...
spec:
  template:
    metadata:
      labels:
        cluster: my_cluster
        role: primary
...
```

`cluster: my_cluster`は、HAクラスタに参加するVM全てに共通して定義するラベルとして用います。後ほどNode.jsアプリをService / Routeを介してインターネットに共有する際に、当該ラベルをセレクタに使います。

`role: primary`はrhel9-ha1にのみ定義するラベルです。このラベルはrhel9-ha1 / rhel9-ha2それぞれのPod Anti-Affinityルールを適用する際に利用します。

YAMLの内容はすべてコンソール画面（GUI）の表示にも反映されますので、主要な内容をGUIでも確認しておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/abbc3bfc-6b8e-4fe3-9dcd-cebf320f994a.png)


### Overviewタブ
この画面では仮想マシンの名称の変更結果を確認しておきます。「Name」が「rhel9-ha1」となっているか確認してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/6a3a46e8-6a12-4460-95b0-f526eeceac1a.png)

なお、`metadata.name`の値は、そのまま仮想マシンのホスト名にもなります。

```rhel9-ha1.yaml
metadata:
  name: rhel9-ha1
...
```

### Schedulingタブ
Kubernetesでは、Podのスケジュールに関する戦略を選択することができます。`rhel9-ha1.yaml`を見ると、今回は1つのアフィニティルールが適用されています。

```rhel9-ha1.yaml
...
spec:
  ...
  template:
    ...
    spec:
      ...
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
              topologyKey: kubernetes.io/hostname
            - labelSelector:
                matchExpressions:
                  - key: role
                    operator: In
                    values:
                      - secondary
...
```

Pod Anti-Affinityによって特定のラベルを有するPodとは異なるノードにスケジュールされるようにしています。これがOpenShift VirtualizationのGUIの「Scheduling」タブにも反映されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/238865d4-33e4-488d-8d8d-9428d5154312.png)

この内容は、以下のラベルが設定されているPodとは同じノードにスケジュールされないようにしています。
```yaml
...
spec:
  template:
    metadata:
      labels:
        role: secondary
...
```
なお、同ラベルはこれからrhel-ha2に設定します。これにより、2つのRHEL VMは異なるノードにスケジュールされ、単一の物理マシン（ホスト）に障害が起きても、両仮想マシンが同時に被害に合わないように（停止しないよう）にします。

:::note
Affinityとは日本語で「親和性」という意味です。今回は「特定のPod同士を異なるノードに配置する（Pod Anti-Affinity）」という戦略を採用しましたが、逆に「同じノードに配置する（Pod Affinity）」などもを選択可能です。また、KubernetesではAffinityルールに加えて、Podを明示的に特定のノードにスケジュールするためのNodeSelectorというリソースも存在します。逆に「許可されたPodしか特定のノードにスケジュールされないようにする」ルールとして、Taint/Tolerationという仕組みも存在します。OpenShift Virtualizationでは、これらのリソースを組み合わせて、仮想マシンの柔軟なスケジュール戦略を実現可能です。

詳細はKubernetesの公式ドキュメントを確認することをおすすめします。
* [ノード上へのPodのスケジューリング](https://kubernetes.io/ja/docs/concepts/scheduling-eviction/assign-pod-node/)
* [TaintとToleration](https://kubernetes.io/ja/docs/concepts/scheduling-eviction/taint-and-toleration/)
:::

### Network interfacesタブ
rhel9-ha1にはvNIC（仮想NIC）を2つ付与しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/f5450a3f-6e55-4841-abc5-8b1ba54bce66.png)

Kubernetes上の全てのPodに最初からアタッチされているvNIC「default」に加え、追加のvNIC「eth1」をアタッチしました。defaultはKubernetesのデフォルトのオーバレイネットワーク「Pod Network」に参加していますが、追加したeth1はNADによって作成した追加のオーバレイネットワークである2nd Oberlay NW（multus Network）に参加させています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/e74c1db8-3865-472d-bfc3-a45fd5f19891.png)


マニフェストにおける以下の内容が、上述した内容を定義した箇所です。

```rhel9-ha1.yaml
spec:
  template:
    spec:
      domain:
        ...
        devices:
          ...
          interfaces: ##ネットワークインターフェース(vNIC）の定義
            - macAddress: ''
              masquerade: {}
              model: virtio
              name: default
            - bridge: {}
              macAddress: ''
              model: virtio
              name: eth1
        ...
      networks: ##どのvNICをどのオーバレイネットワークに追加するかの定義
        - name: default
          pod: {}
        - multus:
            networkName: nad-rhel-ha
          name: eth1
      ...
```

:::note
それぞれのvNICに付与されるMACアドレスは、マニフェストファイルでは指定していません。MACアドレスアドレスを`''`として空欄にすると、KubeMacPoolコンポーネントによって、共有MACアドレスプールから仮想マシンネットワークインターフェイスのMACアドレスを割り当てます。これにより、各ネットワークインターフェイスに一意のMACアドレスが確実に割り当てられます。

* [ネットワークインターフェイスの MAC アドレスプールの管理](https://docs.redhat.com/ja/documentation/openshift_container_platform/4.17/html/virtualization/virt-using-mac-address-pool-for-vms)
:::

### Disksタブ
次に、今回の一番の肝になるディスクについて見ておきます。まず、rhel9-ha1には3つのディスクが存在します。

|ディスク|ドライバ|補足説明|
|---|---|---|
|rootdisk|VirtIO|OpenShift Virtualizationで仮想マシンを作成すると必ず作られるディスク。その名の通り、仮想マシンのルートディスクとして、OSを起動（Boot）し、OSイメージや仮想マシン内のデータ一式を保持している。|
|cloudinitdisk|VirtIO|OpenShift Virtualizationでは、仮想マシンの初期設定に[cloud-init](https://cloudinit.readthedocs.io/en/latest/)を利用可能。この設定・実行を行うためのディスク。データは永続化されず、スナップショット対象にもならない。仮想マシンを削除した場合は当該ディスクも削除される。|
|nodeapp-storage|VirtIO|今回使うサンプルアプリを動作させるために個別に用意した追加のデータディスク。サンプルアプリの資材一式やステートは当該ディスクに保存する。|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/d5eefe55-f1a1-462b-a4f8-20c9c96fcfce.png)

これらのディスク一覧は「Disiks」タブでも確認できます。このなかで「nodeapp-storage」の情報を確認しておきます。右端の「 ⋮ 」をクリックしてして「Edit」を選択すると、設定情報を確認・編集できます。

![スクリーンショット 2025-03-03 22.23.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/a040230b-d9f9-4b44-9dd7-d358810947fc.png)

データディスクであるnodeapp-storageのデータを永続化するためにPVCを利用します。このPVCは事前に作成したnodeapp-pvcです。nodeapp-pvcのスペックは以下の通りでした。

>- 容量：5GiB
>- ボリュームモード：ブロック
>- アクセスモード（読み書きモード）：ReadWriteMany(RWX)
>- StorageClass：ocs-storagecluster-ceph-rbd-virtualization

当該PVCからプロビジョニングされるPVはRWXのボリュームであり、複数ノードからの参照・更新に対応しています。さらに、「Advanced settings」欄の「share this disk between multiple VirtualMachine」をにチェックを入れました。これにより、文字通り「複数の仮想マシン間の共有ディスクとする」を実現できます。

このように、Kubernetesの永続ボリュームの仕組みと、OpenShift Virtualizationのデフォルトの機能だけで、仮想マシン間の共有ディスクを簡単に用意することができます。

:::note
VirtIO は、仮想マシン内のデバイス（ネットワーク、ストレージなど）を効率的に処理するために用いられる、オープンソースの準仮想化デバイスドライバです。KVMやQEMUなどの仮想環境で広く採用されていますが、特定のハイパーバイザやゲストOSに依存しないフレームワークです。OpenShift Virtualizationにおいて仮想マシンにディスクやvNICをアタッチする際には、特段の理由がなければVirtIOをドライバとして利用すると良いでしょう。
:::

:::note warn
「share this disk between multiple VirtualMachine」を有効化したい場合は、共有ディスクのボリュームは、必ずブロックモード（ブロックデバイス）」でなければなりません。そのため、永続ボリューム（PV）を払い出す際には、ブロックデバイスでプロビジョニングできるStorageClassを指定する必要があります。
- [仮想マシンの共有ボリュームの設定](https://docs.redhat.com/ja/documentation/openshift_container_platform/4.17/html/virtualization/vm-disks#virt-configuring-shared-volumes-for-vms)
>共有ディスクを設定して、複数の仮想マシン (VM) が同じ基礎となるストレージを共有できるようにすることができます。共有ディスクのボリュームは、ブロックモードである必要があります。
:::

### Scriptsタブ
Scriptsでは、仮想マシンの初期設定に関するいくつかの設定を実施できます。今回はcloud-initによる初期設定と、仮想マシンにSSH公開鍵の登録を行います。特に肝となるのは[cloud-init](https://cloudinit.readthedocs.io/en/latest/#)です。

:::note
cloud-initは、仮想マシンの初期設定やパッケージインストールを自動化するためのオープンソースのツールです。これにより、仮想マシンの起動時に自動で必要な初期設定が実行できます。OpenShift Virtualizationにおいて仮想マシンのセットアップの自動化を推進する上では、cloud-initを積極的に使いましょう。
:::

「Scriptes」タブの「cloud-init」欄の「Edit」をクリックすると、Form ViewとScript直書きの2種類で設定内容を編集できます。

![スクリーンショット 2025-02-24 22.18.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/207e5092-18cc-47dc-89bd-cd8fcf2e1ff6.png)

この黄色の枠内にcloud-initの書式に基づいた設定内容を記載することができ、その内容はVirtualMachineのマニフェストファイルのspec.template.spec.volumesのcloudInitNoCloudにも反映されます。

```rhel9-ha1.yaml
spec:
  ...
  template:
    ...
    spec:
    ...
      volumes:
          ...
        - cloudInitNoCloud:
            networkData: |
              ethernets:
                eth1:
                  addresses:
                    - 192.168.122.11/24
              version: 2
            userData: |
              #cloud-config
              user: cloud-user
              password: cloud-password
              chpasswd:
                expire: false
              runcmd:
                - subscription-manager register --org=<ORG_ID> --activationkey=<ACTIVATION_KEY>
                - subscription-manager repos --enable=rhel-9-for-x86_64-highavailability-rpms
                - dnf install pcs pacemaker fence-agents-all nodejs sqlite -y
                - systemctl start pcsd.service
                - systemctl enable pcsd.service
                - echo "hacluster:rhel9ha" | chpasswd
                - mkdir -p /var/lib/nodeapp
                - echo "192.168.122.11 rhel9-ha1" >> /etc/hosts
                - echo "192.168.122.12 rhel9-ha2" >> /etc/hosts
```

この内容について少し補足します。

**■ networkData**
追加したvNICに付与する静的IPアドレスを指定できます。今回はeth1に192.168.122.11/24を設定しました。

**■ userData**
ユーザ情報を設定する欄です。今回は`cloud-user`というユーザを作成し、ログインパスワードを`cloud-password`としました。

また、`runcmd`以下には仮想マシンを作成する際にroot権限で実行可能なコマンドを記載できます。今回は以下のようなコマンドを記載しています。

```sh
# RHELをRed Hat Sbscription Manger（RHSM)に登録し、dnf/yumを利用可能とします
# 自身のRed Hatアカウントの<ORG_ID>と、作成した<ACTIVATION_KEY>を設定してください。
subscription-manager register --org=<ORG_ID> --activationkey=<ACTIVATION_KEY>

# HA Add-onを利用する際に必要なパッケージを取得するためのリポジトリを登録します
subscription-manager repos --enable=rhel-9-for-x86_64-highavailability-rpms

# HA Add-on関連のパッケージ及び、この後node.jsアプリを作成・実行するのに必要パッケージをインストールします
dnf install pcs pacemaker fence-agents-all nodejs sqlite -y

# HA Add-on関連の設定を行うためのpcsデーモンを起動し、仮想マシン起動時に自動開始する設定を行います
systemctl start pcsd.service
systemctl enable pcsd.service

# HAクラスタに参加する仮想マシンに自動作成されるユーザ「hacluster」のパスワードを「rhel9ha」に設定します
echo "hacluster:rhel9ha" | chpasswd

# node.jsアプリを実行するためのディレクトリを作成しておきます
mkdir -p /var/lib/nodeapp

# rhel9-ha1/rhel9-ha2の追加vNICに付与する静的IPアドレスがホスト名で名前解決できるように/etc/hosts内に同内容を追記します
echo "192.168.122.11 rhel9-ha1" >> /etc/hosts
echo "192.168.122.12 rhel9-ha2" >> /etc/hosts
```

:::note
OpenShift Virtualizationの管理者設定メニューから、RHEL起動時に自動的にRHSM登録を行う設定が可能です。

![スクリーンショット 2025-02-28 15.22.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/2b3e45a0-cfbb-4484-b108-338952290584.png)

当該箇所に自身の`<ORG_ID>`と`<ACTIVATION_KEY>`を入力しておくと、TemplateからRHEL VMを作成する際、自動的にRHSM登録が行われます。大変便利な機能なのでぜひ活用しましょう。
:::

これでrhel9-ha1の設定内容を確認できました。

![スクリーンショット 2025-02-28 15.51.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/1f393905-21b9-4940-bcb9-0dcdf17fa00d.png)

「Create VirtualMachine」をクリックして、仮想マシン「rhel9-ha1」を起動します。

# 2台目のRHEL 仮想マシンの起動
続いて2台目です。1台目と全く同様の手順で、以下のマニフェストファイルを用いて仮想マシン「rhel9-ha2」を起動します。

```rhel9-ha2.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  labels:
    app: rhel9-ha2
  name: rhel9-ha2
  namespace: rhel-ha
spec:
  dataVolumeTemplates:
    - apiVersion: cdi.kubevirt.io/v1beta1
      kind: DataVolume
      metadata:
        creationTimestamp: null
        name: rhel9-ha2
      spec:
        sourceRef:
          kind: DataSource
          name: rhel9
          namespace: openshift-virtualization-os-images
        storage:
          resources:
            requests:
              storage: 30Gi
          storageClassName: ocs-storagecluster-ceph-rbd-virtualization
  running: true
  template:
    metadata:
      labels:
        cluster: my_cluster
        role: secondary
    spec:
      accessCredentials:
        - sshPublicKey:
            propagationMethod:
              noCloud: {}
            source:
              secret:
                secretName: public-key-rhel-ha
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: role
                    operator: In
                    values:
                      - primary
              topologyKey: kubernetes.io/hostname
      architecture: amd64
      domain:
        cpu:
          cores: 1
          sockets: 1
          threads: 1
        devices:
          disks:
            - disk:
                bus: virtio
              name: rootdisk
            - disk:
                bus: virtio
              name: cloudinitdisk
            - disk:
                bus: virtio
              name: nodeapp-storage
              shareable: true
          interfaces:
            - macAddress: ''
              masquerade: {}
              model: virtio
              name: default
            - bridge: {}
              macAddress: ''
              model: virtio
              name: eth1
          rng: {}
        features:
          acpi: {}
          smm:
            enabled: true
        firmware:
          bootloader:
            efi: {}
        machine:
          type: pc-q35-rhel9.4.0
        memory:
          guest: 2Gi
        resources: {}
      networks:
        - name: default
          pod: {}
        - multus:
            networkName: nad-rhel-ha
          name: eth1
      terminationGracePeriodSeconds: 180
      volumes:
        - dataVolume:
            name: rhel9-ha2
          name: rootdisk
        - name: nodeapp-storage
          persistentVolumeClaim:
            claimName: nodeapp-pvc
        - cloudInitNoCloud:
            networkData: |
              ethernets:
                eth1:
                  addresses:
                    - 192.168.122.12/24
              version: 2
            userData: |
              #cloud-config
              user: cloud-user
              password: cloud-password
              chpasswd:
                expire: false
              runcmd:
                - subscription-manager register --org=<ORG_ID> --activationkey=<ACTIVATION_KEY>
                - subscription-manager repos --enable=rhel-9-for-x86_64-highavailability-rpms
                - dnf install pcs pacemaker fence-agents-all nodejs sqlite -y
                - systemctl start pcsd.service
                - systemctl enable pcsd.service
                - echo "hacluster:rhel9ha" | chpasswd
                - mkdir -p /var/lib/nodeapp
                - echo "192.168.122.11 rhel9-ha1" >> /etc/hosts
                - echo "192.168.122.12 rhel9-ha2" >> /etc/hosts
          name: cloudinitdisk
```

## 1台目との差分
rhel9-ha1との差分は以下の通りです。
|マニフェストの箇所|rhel9-ha1|rhel9-ha2|備考|
|---|---|---|---|
|metadata.name|rhel-ha1|rhel-ha2|各VMのホスト名。これに伴い、DataVolumeの名称も合わせて変更しています。|
|spec.template.metadata.labels.role|primary|secondary|HAクラスタ内における役割（主系/副系）を示しています。
|spec.template.spec.volumes.cloudInitNoCloud.ethernets.eth1.addresses|192.168.122.11/24|192.168.122.12/24|各VMのeth1に異なる静的IPアドレスを付与

内容を確認できたら、rhel9-ha2も起動してください。

:::note
「Enable guest system log access」をONにすることで、仮想マシンが起動し、cloud-initに記載したコマンドが実行される様を、OpenShiftのコンソール画面から確認できます。

![スクリーンショット 2025-02-28 15.55.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/010872a5-b928-488b-9e60-bc8c2a607d0d.png)

:::

両仮想マシンに必要なパッケージのインストールや設定などが完了するまで、しばらく待ちましょう。


# HAクラスタを作成
2台のRHEL仮想マシンが起動し、必要なパッケージのインストールや設定などが完了したら、いよいよHAクラスタを作成します。なお、必要なコマンドの詳細はRHEL HA Add-onの[公式ドキュメント](https://docs.redhat.com/ja/documentation/red_hat_enterprise_linux/9/html-single/configuring_and_managing_high_availability_clusters/index)でも確認できますので、気になる方はそちらを読んでください。

なお、HAクラスタの設定はどちらの仮想マシンで実行してもOKです。今回はrhel9-ha1で実施します。

## SSHでリモート接続
以後、仮想マシンに対するコマンド操作は、作業端末のターミナルで実行します。そのため、仮想マシンにSSHでリモート接続します。SSHにはvirtctl CLIを利用します。

::: note 
virtctl CLIのダウンロードは、OpenShiftのコンソール画面から可能です。右上にある「?」アイコンをクリックし、表示されるメニューから「コマンドラインツール」を選択します。「

![スクリーンショット 2025-03-03 15.49.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/5b930029-7d5f-42c2-8a3e-a37be8598f05.png)

virtctl - KubeVirt command line interface」の欄から、利用している OS およびアーキテクチャに対応するバージョンをダウンロードしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/1a3f8094-23f3-4f0d-927a-ec1a7affa140.png)

ダウンロードした圧縮ファイルを展開し、実行可能な状態へ変更します。以下は Apple Silicon Mac
(ARM64)での操作例です。

```sh
## ダウンロードした ZIP ファイルを解凍
$ tar xvf virtctl.zip
x virtctl

## virtctl CLI を PATH に含まれるディレクトリへ移動
$ sudo mv virtctl /usr/local/bin/virtctl

## virtctl CLI コマンドが利用できるか確認
$ virtctl
Available Commands:
addvolume add a volume to a running VM
completion Generate the autocompletion script for the specified shell
console Connect to a console of a virtual machine instance.
...
```

これでvirtctlコマンドが利用可能になりました。

::: 

rhel9-ha1及びrhel9-ha2に対して、それぞれ以下のコマンドでSSHしてください。

```sh
# rhel9-ha1にSSH
Username@Loaclmachine ~ % virtctl -n rhel-ha ssh cloud-user@rhel9-ha1 --identity-file=~/.ssh/id_rsa

# rhel9-ha2にSSH
Username@Loaclmachine ~ % virtctl -n rhel-ha ssh cloud-user@rhel9-ha2 --identity-file=~/.ssh/id_rsa
```

:::note
なお、今回はSSH秘密鍵`id_rsa`が`~/.ssh/`内に存在するとします。ご自身の秘密鍵の格納場所に応じて`--identity-file=`の内容は適宜変更してください。また、仮想マシンのSSH接続コマンドは、仮想マシンの詳細画面からもコピー可能です。

![スクリーンショット 2025-03-03 15.54.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/a2fcf5fc-3e30-4560-8983-b8cac7ccde7f.png)

:::

## rhel9-ha1で以下のコマンドを実行

まずは、rhel9-ha1 / rhel9-ha2をHAクラスタに参加させるための認証を行います。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs host auth rhel9-ha1 rhel9-ha2 -u hacluster -p rhel9ha
rhel9-ha1: Authorized
rhel9-ha2: Authorize
```
認証が成功すると上記のようなログが出力されます。なお、`hacluster`はHA Add-on関連のパッケージをインストールしたRHELに自動的に作成されるユーザです。パスワードはcloud-init内のコマンドで作成した`rhel9ha`としました。

::: note
もし各仮想マシンの`etc/hosts`に名前解決情報が無い場合は、IPアドレスを直指定することで認証可能です。

```sh
sudo pcs host auth 192.168.122.11 192.168.122.12 -u hacluster -p rhel9ha
```
:::

次にクラスタを作成し稼働させます。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs cluster setup my_cluster rhel9-ha1 rhel9-ha2 --start
...
Cluster has been successfully set up.
Starting cluster on hosts: 'rhel9-ha1', 'rhel9-ha2'...
```

また、各仮想マシンが再起動した際に自動的にHAクラスタも動作するための設定を行います。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs cluster enable --all
rhel9-ha1: Cluster Enabled
rhel9-ha2: Cluster Enabled
```

以下のコマンドで一度HAクラスタの状態を確認しておきます。HA Add-onを構成する各コンポーネント（デーモン）がきちんと動作しているか確認しておきましょう。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs status
Cluster name: my_cluster

WARNINGS:
No stonith devices and stonith-enabled is not false

Cluster Summary:
  * Stack: corosync (Pacemaker is running)
  * Current DC: rhel9-ha1 (version 2.1.8-3.el9-3980678f0) - partition with quorum
  * Last updated: Fri Feb 28 02:32:16 2025 on rhel9-ha1
  * Last change:  Fri Feb 28 02:30:45 2025 by hacluster via hacluster on rhel9-ha1
  * 2 nodes configured
  * 0 resource instances configured

Node List:
  * Online: [ rhel9-ha1 rhel9-ha2 ]

Full List of Resources:
  * No resources

Daemon Status:
  corosync: active/enabled
  pacemaker: active/enabled
  pcsd: active/enabled
```

いくつか追加でコマンドを実行します。まずは、「Shoot the Other Node in the Head (STONITH)」の無効化です。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs property set stonith-enabled=false
```

HAクラスタに参加する仮想マシンを監視するコンポーネントがPacemakerです。このPacemakerはクラスタ内のノードが応答しなくなった場合に STONITH（フェンシング） を使用して、強制的に電源を切ることでデータの整合性を保ちますが、フェンシングデバイスがない環境では STONITH を無効化する必要があります。今回はAWS環境でサーバを調達していますので、ホストマシンの電源をユーザが何等か管理したり操作することができません。よって、今回はSTONITHを無効化します。この設定は商用利用では推奨されません。

:::note warn 
フェンシングに関する詳細に記載の通り、STONITHの無効化はRed Hatのサポート構成では無いのでご注意ください。

[Fencing in a Red Hat High Availability Cluster](https://access.redhat.com/solutions/15575)
>Important: Red Hat does not provide support for clusters without a STONITH device configured for any member, and the expected behavior of such a cluster is undefined. Clusters without STONITH for every node may behave in problematic ways, and should not be relied on for High Availability of critical services or applications.
:::

次は過半数（クォーラム）ポリシーの無効化です。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs property set no-quorum-policy=ignore
```

Pacemaker は、クラスタのノードが過半数（クォーラム）を満たさない場合、リソースの起動やフェイルオーバーを停止します。しかし、2ノードクラスタ（偶数ノード）では、1台が落ちると過半数を満たせないので、クラスタが停止する問題があります。この設定をすることで、クォーラムが失われてもクラスタを継続動作させることができます。なお、2ノードクラスタでは通常この設定が必要ですが、3ノード以上のクラスタでは、スプリットブレインを防ぐ観点から推奨されません。

:::note warn
クォーラムに関する詳細に記載の通り、2ノードクラスタ（2つのRHELインスタンスで構成するクラスタ）では1つのノードが故障するとクラスタとしての機能が意味を成しません。

[Exploring Concepts of RHEL High Availability Clusters - Quorum](https://access.redhat.com/articles/2824071)
>Two-node cluster: The majority-wins policy doesn't make much sense in a cluster with two nodes. A single member would not have a majority, meaning a failure of a single node would leave the cluster unable to function - not a very useful design for a system that is meant to function through member-failures.

このことから、商用環境における高い可用性の実現には3ノード以上が強く推奨されます。
:::

これで、HAクラスタの作成と設定が完了しました。

# 共有ディスクを`var/lib/nodeapp`にマウントする
共有ディスク「nodeapp-storage」をrhel9-ha1 / rhel9-ha2の`var/lib/nodeapp`にマウントします。

## ファイルシステムを作成する
nodeapp-storageの永続ボリュームはブロックデバイスとなっており、まだまっさらな状態です。まずはファイルシステムを作成します。

その前に、nodeapp-storageがどのデバイスとして認識されているかを確認します。引き続きrhel9-ha1で以下のコマンドを実行します。

```sh
[cloud-user@rhel9-ha1 ~]$ lsblk -f
NAME   FSTYPE  FSVER            LABEL  UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
vda                                                                                        
├─vda1                                                                                     
├─vda2 vfat    FAT16                   7B77-95E7                             192.8M     4% /boot/efi
├─vda3 xfs                      boot   a53019c4-0c14-499f-b4aa-10f62413b407  786.7M    18% /boot
└─vda4 xfs                      root   4b34029a-2c52-4051-bb74-af43d491bc74   26.7G     7% /
vdb    iso9660 Joliet Extension cidata 2025-02-28-06-54-22-00                              
vdc
```

ちょっとわかりにくいのですが、vdcの項目が空っぽのデバイスとして認識されています。以下のコマンドを使い、ext4形式のファイルシステムを作成します。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo mkfs.ext4 -F /dev/vdc
mke2fs 1.46.5 (30-Dec-2021)
Discarding device blocks: done                            
Creating filesystem with 1310720 4k blocks and 327680 inodes
Filesystem UUID: b63cc29b-6792-4452-afea-6f19c455716f
Superblock backups stored on blocks: 
	32768, 98304, 163840, 229376, 294912, 819200, 884736

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done
```

再度`lsblk -f`で今作成したファイルシステムを確認しましょう。

```sh
[cloud-user@rhel9-ha1 ~]$ lsblk -f
NAME   FSTYPE  FSVER            LABEL  UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
vda                                                                                        
├─vda1                                                                                     
├─vda2 vfat    FAT16                   7B77-95E7                             192.8M     4% /boot/efi
├─vda3 xfs                      boot   a53019c4-0c14-499f-b4aa-10f62413b407  786.7M    18% /boot
└─vda4 xfs                      root   4b34029a-2c52-4051-bb74-af43d491bc74   26.7G     7% /
vdb    iso9660 Joliet Extension cidata 2025-02-28-06-54-22-00                              
vdc    ext4    1.0                     <FILESYSTEM_UUID>
```

`<FILESYSTEM_UUID>`は環境毎に異なりますので、ご自身のものを確認しておいてください。


## 共有ディスクをマウントする
以下のコマンドにて、`/var/lib/nodeapp`にファイルシステム（を作成した共有ディスク）をマウントしましょう。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo mount /dev/vdc /var/lib/nodeapp
```

以下のコマンドにて正しくマウントされたか一応確認しておきます。

```sh
[cloud-user@rhel9-ha1 ~]$ mount | grep /var/lib/nodeapp
/dev/vdc on /var/lib/nodeapp type ext4 (rw,relatime,seclabel)
```

# サンプルアプリケーションをデプロイする
共有ディスクをマウントした`var/lib/nodeapp/`内にNode.jsアプリの資材を配置します。

## `/var/lib/nodeapp`の権限設定を変更する
`/var/lib/nodeapp`はrootユーザによって作成されたディレクトリです。そのため、`/var/lib/nodeapp`ディレクトリの 所有者 (owner) とグループ (group) を cloud-user に変更し、cloud-userがこのディレクトリ内でファイルを作成・変更できるようにしておきます。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo chown cloud-user:cloud-user /var/lib/nodeapp
```

また、`/var/lib/nodeapp`の アクセス権限を「755」 に設定し、所有者（cloud-user）が 読み・書き・実行できるが、グループとその他のユーザーは 読み・実行のみを許可します。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo chmod 755 /var/lib/nodeapp
```

## サンプルアプリを作成する
以下のコマンドで、`/var/lib/nodeapp`内に`index.js`を作成します。

```sh
cat <<EOF | sudo tee /var/lib/nodeapp/index.js
const express = require("express");
const sqlite3 = require("sqlite3").verbose();
const os = require("os");

const app = express();
const PORT = 3000;

// SQLite データベースの設定
const DB_PATH = "/var/lib/nodeapp/data.db";
const db = new sqlite3.Database(DB_PATH, (err) => {
    if (err) {
        console.error("❌ データベース接続エラー:", err);
    } else {
        console.log("✅ SQLite データベースに接続しました");
        db.run("CREATE TABLE IF NOT EXISTS counter (id INTEGER PRIMARY KEY, value INTEGER)", () => {
            db.run("INSERT OR IGNORE INTO counter (id, value) VALUES (1, 0)");
        });
    }
});

// アクセス時にカウンターを増やして表示
app.get("/", (req, res) => {
    db.run("UPDATE counter SET value = value + 1 WHERE id = 1", function (err) {
        if (err) {
            return res.status(500).send("❌ データ更新エラー");
        }
        db.get("SELECT value FROM counter WHERE id = 1", (err, row) => {
            if (err) {
                return res.status(500).send("❌ データ取得エラー");
            }
            res.send(\`
                <h1>🌐 RHEL HA Node.js Demo</h1>
                <h2>🖥️ ホスト名: \${os.hostname()}</h2>
                <h3>🔢 カウンター値: \${row.value}</h3>
            \`);
        });
    });
});

// サーバー起動
app.listen(PORT, () => {
    console.log(\`🚀 サーバー起動: http://localhost:\${PORT}\`);
});
EOF
```

次に、`/var/lib/nodeapp`内でNode.jsアプリの実行に必要なライブラリをインストールします。

```sh
## ディレクトリを移動
[cloud-user@rhel9-ha1 ~]$ cd /var/lib/nodeapp

## Node.jsサーバを初期化
[cloud-user@rhel9-ha1 nodeapp]$ npm init -y
Wrote to /var/lib/nodeapp/package.json:

{
  "name": "nodeapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

## 必要なライブラリをインストール
[cloud-user@rhel9-ha1 nodeapp]$ npm install express sqlite3
...
npm notice Run npm install -g npm@11.1.0 to update!
npm notice

## ホームディレクトリに戻っておきます
[cloud-user@rhel9-ha1 nodeapp]$ cd ~
```

::: note
expressは、Node.js向けのHTTPサーバフレームワークです。また、sqlite3はSQLiteデータベースをNode.jsで操作するためのライブラリです。
:::

## Systemdサービス登録
今作成したNode.jsアプリケーションをSystemdのサービスとして登録し、仮想マシンが起動した際にアプリケーションも自動的に起動するようにします。

まずは以下のコマンドで、Systemd サービスファイルを作成しておきます。

```sh
[cloud-user@rhel9-ha1 ~]$ cat <<EOF | sudo tee /etc/systemd/system/nodeapp.service
[Unit]
Description=Node.js High Availability App
After=network.target nodeapp-storage.mount

[Service]
ExecStart=/usr/bin/node /var/lib/nodeapp/index.js
Restart=always
User=cloud-user
Group=cloud-user
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/var/lib/nodeapp

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

作成が完了したら、以下のコマンドで自動起動の有効化と、Node.jsアプリを起動します。

```sh
## Systemdの設定を再読み込みする
[cloud-user@rhel9-ha1 ~]$ sudo systemctl daemon-reload

## 自動起動の有効化
[cloud-user@rhel9-ha1 ~]$ sudo systemctl enable nodeapp
Created symlink /etc/systemd/system/multi-user.target.wants/nodeapp.service → /etc/systemd/system/nodeapp.service.

## アプリを起動する
[cloud-user@rhel9-ha1 ~]$ sudo systemctl start nodeapp
```

## サンプルアプリの動作確認
`curl`コマンドでサンプルアプリケーションにアクセスし、動作確認を行います。
```sh
[cloud-user@rhel9-ha1 ~]$ curl http://localhost:3000

                <h1>🌐 RHEL HA Node.js Demo</h1>
                <h2>🖥️ ホスト名: rhel9-ha1</h2>
                <h3>🔢 カウンター値: 1</h3>
```

アプリが起動していることを確認できました。`curl`コマンドを実行するたびに、カウンター値が増えます。

# rhel9-ha2に共有ディスクをマウントする
rhel9-ha1の`/var/lib/nodeapp`に共有ディスクをマウントし、サンプルアプリケーションを起動できるようにしました。この共有ディスクをrhel9-ha2側の`/var/lib/nodeapp`にもマウントします。

以下の操作は、rhel9-ha2側で実施します。

rhel9-ha1で実行した通り、`lsblk -f`で共有ディスク「nodeapp-storage」の認識状況を確認してみましょう。

```sh
[cloud-user@rhel9-ha2 ~]$ lsblk -f
NAME   FSTYPE  FSVER            LABEL  UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
vda                                                                                        
├─vda1                                                                                     
├─vda2 vfat    FAT16                   7B77-95E7                             192.8M     4% /boot/efi
├─vda3 xfs                      boot   a53019c4-0c14-499f-b4aa-10f62413b407  786.7M    18% /boot
└─vda4 xfs                      root   4b34029a-2c52-4051-bb74-af43d491bc74   26.7G     7% /
vdb    iso9660 Joliet Extension cidata 2025-02-28-06-56-45-00                              
vdc
```

おや？vdcの欄が空です。root権限で`partprobe`コマンドを実行すると、デバイスの再読み込みが可能です。再読み込み後、もう一度`lsblk -f`コマンドでデバイスを確認しましょう。

```sh
[cloud-user@rhel9-ha2 ~]$ sudo partprobe
[cloud-user@rhel9-ha2 ~]$ lsblk -f
NAME   FSTYPE  FSVER            LABEL  UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
vda                                                                                        
├─vda1                                                                                     
├─vda2 vfat    FAT16                   7B77-95E7                             192.8M     4% /boot/efi
├─vda3 xfs                      boot   a53019c4-0c14-499f-b4aa-10f62413b407  786.7M    18% /boot
└─vda4 xfs                      root   4b34029a-2c52-4051-bb74-af43d491bc74   26.7G     7% /
vdb    iso9660 Joliet Extension cidata 2025-02-28-06-56-45-00                              
vdc    ext4    1.0                     <FILESYSTEM_UUID>
```

先ほどrhel9-ha1で作成したファイルシステムがrhel9-ha2でも認識されたので、rhel9-ha2側でも`var/lib/nodeapp`にマウントします。


```sh
[cloud-user@rhel9-ha2 ~]$ sudo mount /dev/vdc /var/lib/nodeapp
[cloud-user@rhel9-ha2 ~]$ mount | grep /var/lib/nodeapp
/dev/vdc on /var/lib/nodeapp type ext4 (rw,relatime,seclabel)
```

rhel9-ha1側で作成したNode.jsアプリケーションのソースを確認します。

```sh
[cloud-user@rhel9-ha2 ~]$ ls -l /var/lib/nodeapp
total 176
-rw-r--r--.   1 cloud-user cloud-user   8192 Feb 28 03:52 data.db
-rw-r--r--.   1 root       root         1482 Feb 28 03:50 index.js
drwx------.   2 root       root        16384 Feb 28 02:56 lost+found
drwxr-xr-x. 183 cloud-user cloud-user   4096 Feb 28 03:23 node_modules
-rw-r--r--.   1 cloud-user cloud-user 142160 Feb 28 03:23 package-lock.json
-rw-r--r--.   1 cloud-user cloud-user    296 Feb 28 03:23 package.json
```

無事、rhel9-ha1 / rhel9-ha2間でディスクが共有され、アプリケーションのソースを両仮想マシンから確認できました。Kubernetesの永続ボリューム要求（PersistentVolumeClaim, PVC）を活用すれば、こんな簡単に仮想マシン間のディスク共有が設定できてしまいます。

### rhel9-ha2側でもSystemdサービス登録
rhel9-ha1と同様に以下コマンドでSystemdサービスとして登録し、仮想マシン起動時にNode.jsアプリが自動起動するようにしましょう。

```sh
[cloud-user@rhel9-ha2 ~]$ cat <<EOF | sudo tee /etc/systemd/system/nodeapp.service
[Unit]
Description=Node.js High Availability App
After=network.target nodeapp-storage.mount

[Service]
ExecStart=/usr/bin/node /var/lib/nodeapp/index.js
Restart=always
User=cloud-user
Group=cloud-user
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/var/lib/nodeapp

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

```sh
[cloud-user@rhel9-ha2 ~]$ sudo systemctl daemon-reload
[cloud-user@rhel9-ha2 ~]$ sudo systemctl enable nodeapp
Created symlink /etc/systemd/system/multi-user.target.wants/nodeapp.service → /etc/systemd/system/nodeapp.service.
```

さて、共有ディスクのマウントが確認できたら、一度両仮想マシンからアンマウントしておきます。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo systemctl stop nodeapp
[cloud-user@rhel9-ha1 ~]$ sudo umount /var/lib/nodeapp

[cloud-user@rhel9-ha2 ~]$ sudo umount /var/lib/nodeapp
```

# Peacemakerリソースを作成する
つづいて、Node.jsアプリケーション及び、共有ディスクのマウントに関して、それぞれをPacemakerのリソースとして登録します。以下の作業もどちらの仮想マシンで実行指定も構いませんが、今回はrhel9-ha1上で実行します。

## デバイスのマウント管理

まずは、特定のデバイス（`dev/vdc`）を指定したディレクトリ（`/var/lib/nodeapp`）にマウントする設定をPacemakerリソースとして登録し、Pacemakerの管理配下とします。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs resource create nodeapp-storage ocf:heartbeat:Filesystem \
  device="/dev/vdc" directory="/var/lib/nodeapp" fstype="ext4" \
  --group nodeapp-group
```

:::note
将来的に`--group`というオプションが廃止される旨の警告が表示されますが、今回は気にせず進めて大丈夫です。
> Deprecation Warning: Using '--group' is deprecated and will be replaced with 'group' in a future release. Specify --future to switch to the future behavior.
:::

上記コマンドでは、「nodeapp-storage」という名称でPacemakerリソースを作成します。今回は、クラスタ管理用のリソースエージェントの標準仕様であるOCF（Open Cluster Framework）のFilesystemリソースエージェントを使用し、マウント設定をPacemakerに管理させます。これにより、フェイルオーバ時に切り替え先の仮想マシンにおいて、自動的に共有ディスクが`dev/vdc`にマウントされます。

以下のコマンドで自動起動の有効化しておきます。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs resource enable nodeapp-storage
```

## Systemdサービスの管理

次に、Systemdサービスとして登録したnodeappを、Pacemakerリソースとして登録します。
```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs resource create nodeapp systemd:nodeapp --group nodeapp-group
```

このコマンドでは「nodeapp」という名称でPacemakerリソースを作成し、systemctl を使って nodeapp.service を制御させる設定を行いました。また、nodeapp-group というグループに当該リソースを追加しました。これにより、同じグループに属するPacemakerリソースは、必ず同じ仮想マシン上で動作します。

nodeapp-storageと同様、自動起動の有効化しておきます。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs resource enable nodeapp
```

:::note warn
Pacemakerリソース「nodeapp」を起動するには、`dev/vdc`にアタッチされている共有ディスク「nodeapp-storage」が`/var/lib/nodeapp`にマウントされている必要があります。つまり、Pacemakerリソース「nodeapp-storage」が先に起動していないと、nodeappが起動できません。そのため、起動順を明示的に設定する必要があります。同じグループ内のPacemakerリソースは上から順に起動されますので、必ず

```sh
Full List of Resources:
  * Resource Group: nodeapp-group:
    * nodeapp-storage	(ocf:heartbeat:Filesystem):	 Started rhel9-ha1
    * nodeapp	(systemd:nodeapp):	 Started rhel9-ha1
```

の順番（nodeapp-storage → nodeapp）で登録されているか確認しましょう。なお、同じグループ内のPacemakerリソースの並び順は作成順で決定されます。
:::

## Pacemakerリソースの状態確認

さて、これでNode.jsアプリケーションの自動起動及び、共有ディスクのマウントがPacemakerリソースとして管理されるようになりました。再度以下のコマンドでHAクラスタの設定状況を確認しておきましょう。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs status
Cluster name: my_cluster
Cluster Summary:
  * Stack: corosync (Pacemaker is running)
  * Current DC: rhel9-ha1 (version 2.1.8-3.el9-3980678f0) - partition with quorum
  * Last updated: Sun Mar  2 20:10:46 2025 on rhel9-ha1
  * Last change:  Sun Mar  2 20:10:36 2025 by root via root on rhel9-ha1
  * 2 nodes configured
  * 2 resource instances configured

Node List:
  * Online: [ rhel9-ha1 rhel9-ha2 ]

Full List of Resources:
  * Resource Group: nodeapp-group:
    * nodeapp-storage	(ocf:heartbeat:Filesystem):	 Started rhel9-ha1
    * nodeapp	(systemd:nodeapp):	 Started rhel9-ha1

Daemon Status:
  corosync: active/enabled
  pacemaker: active/enabled
  pcsd: active/enabled
```

「nodeapp-group」に属するPacemakerリソース（nodeapp-storage, nodeapp）がrhel9-ha1上で起動していることを確認できました。

# サンプルアプリケーションをインターネットに公開
それでは、仮想マシン上で起動しているNode.jsアプリケーションをインターネットに公開します。そのためにService及びRouteを利用します。

以下のマニフェストをそれぞれOpenShiftクラスタに適用し、リソースを作成しましょう。

```yaml
kind: Service
apiVersion: v1
metadata:
  name: service-ha
  namespace: rhel-ha
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: ClusterIP
  selector:
    cluster: my_cluster
```

これにより、Podのラベルに`cluster: my_cluster`がついた仮想マシンに対するサービスディスカバリ（通信窓口）が作成され、OpenShiftクラスタ内からアクセスできるようになります。

```yaml
kind: Route
apiVersion: route.openshift.io/v1
metadata:
  name: route-ha
  namespace: rhel-ha
spec:
  to:
    kind: Service
    name: service-ha
  port:
    targetPort: 3000
```

Routeによって、クラスタ内の通信窓口をインターネットに公開します。それでは、PCのブラウザにRouteのURLを入力して、アプリケーションにアクセスしてみましょう。

![スクリーンショット 2025-02-28 23.50.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/cff8602c-828a-4df4-9682-bc9a768643ee.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/bae316d9-792a-4dec-b83c-221663a9f56b.png)

サンプルアプリにアクセスできました。このアプリがrhel9-ha1上で動作していることも分かります。

# フェイルオーバしてみる
今から、冒頭に示した以下3つのフェイルオーバを実験してみます。

>1. rhel9-ha1のNode.jsプロセスをKill（強制終了）した場合、rhel9-ha1上で速やかにプロセスが再起動するか？
>1. rhel9-ha1を停止（Stop）した場合、速やかにrhel9-ha2にてアプリの動作が引き継がれるか？
>1. rhel9-ha1がスケジュールされているノードをシャットダウンした際に、速やかにrhel9-ha2にてアプリの動作が引き継がれるか？

## プロセスをKill
Node.jsプロセスをKill（強制終了）させます。以下のコマンドでまずはindex.jsを起動しているプロセスIDを確認します。

```sh
[cloud-user@rhel9-ha1 ~]$ ps aux | grep node
cloud-u+   20635  0.0  3.6 848148 62424 ?        Ssl  20:10   0:00 /usr/bin/node /var/lib/nodeapp/index.js
cloud-u+   23849  0.0  0.1   3876  2176 pts/0    S+   20:21   0:00 grep --color=auto node
```

この結果から、プロセスID=20635でアプリが起動していることが確認できます。なお、プロセスIDは環境毎に異なりますので、ご自身の環境で確認したプロセスIDを使い、以下のコマンドでプロセスを強制終了させましょう。

```sh
sudo kill 20635
```

サンプルアプリケーションを更新してみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/fb91b606-fa88-4adf-85b2-d7e78599bc96.png)

何事もなかったように、アプリが起動し続けていますね。ジャーナルログを確認して、再起動した証拠を確認しておきます。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo journalctl -u nodeapp --no-pager --lines=4
Mar 02 20:21:34 rhel9-ha1 systemd[1]: nodeapp.service: Deactivated successfully.
Mar 02 20:21:41 rhel9-ha1 systemd[1]: Started Cluster Controlled nodeapp.
Mar 02 20:21:41 rhel9-ha1 node[24009]: 🚀 サーバー起動: http://localhost:3000
Mar 02 20:21:41 rhel9-ha1 node[24009]: ✅ SQLite データベースに接続しまし
```

Node.jsアプリが停止（Stopped）した後、すぐに再起動（Started）していることが確認できます。再度`ps`コマンドを使うことで、再起動はプロセスIDが変わっていることも確認できます。

```sh
[cloud-user@rhel9-ha1 ~]$ ps aux | grep node
cloud-u+   24009  1.0  3.6 848148 62396 ?        Ssl  20:21   0:00 /usr/bin/node /var/lib/nodeapp/index.js
cloud-u+   24222  0.0  0.1   3876  2176 pts/0    S+   20:22   0:00 grep --color=auto node
```

ご自身の環境でもプロセスIDが変化したことを確認してください。また、`sudo pcs status`コマンドにて、HAクラスタの状況を確認します。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs status
Cluster name: my_cluster
Cluster Summary:
  * Stack: corosync (Pacemaker is running)
  * Current DC: rhel9-ha1 (version 2.1.8-3.el9-3980678f0) - partition with quorum
  * Last updated: Sun Mar  2 20:22:17 2025 on rhel9-ha1
  * Last change:  Sun Mar  2 20:10:36 2025 by root via root on rhel9-ha1
  * 2 nodes configured
  * 2 resource instances configured

Node List:
  * Online: [ rhel9-ha1 rhel9-ha2 ]

Full List of Resources:
  * Resource Group: nodeapp-group:
    * nodeapp-storage	(ocf:heartbeat:Filesystem):	 Started rhel9-ha1
    * nodeapp	(systemd:nodeapp):	 Started rhel9-ha1

Failed Resource Actions:
  * nodeapp 1m-interval monitor on rhel9-ha1 returned 'not running' (inactive) at Sun Mar  2 20:21:39 2025

Daemon Status:
  corosync: active/enabled
  pacemaker: active/enabled
  pcsd: active/enabled
```
nodeapp-groupに属するPacemakerリソース（nodeapp-storage、nodeapp）が、変わらずrhel9-ha1上で動作していることが確認できます。なお、`Failed Resource Actions:`に以下のようなログが出力されていることが確認できます。

```sh
...
Failed Resource Actions:
  * nodeapp 1m-interval monitor on rhel9-ha1 returned 'not running' (inactive) at Sun Mar  2 20:21:39 2025
...
```

これは、Pacemakerがは1m-interval monitor（60秒間隔）でリソースの状態を監視しており、そのタイミングで`not running`を確認したという趣旨のログです。このように、Systemdサービス「nodeapp」がPacemakerリソースとして管理され、何らかの異常なプロセス終了に対して、即時にプロセス再起動を行っていることが確認できました。

## OSを停止（シャットダウン）する
それでは、rhel9-ha1を停止し、Node.jsアプリがrhel9-ha2にフェイルオーバするかを確認してみます。OpenShiftのコンソール画面にて、rhel9-ha1を停止（Stop）してみましょう。

![スクリーンショット 2025-03-01 0.15.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/48ef9593-c0c3-4712-ae05-23e1d92b5509.png)

PCのターミナルでrhel9-ha1にSSH接続していたセッションが切れます。サンプルアプリケーションを更新すると、ホスト名が「rhel9-ha2」に変わっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/27e3977d-8f80-455f-9ca8-f1481237ed2a.png)

しかし、直前のカウンター値+1がカウンター値に表示され、ステートはそのまま引き継がれていることが確認できます。まだSSH接続が生きているrhel9-ha2の方から、`sudo pcs statsu`コマンドで状況を確認してみてください。

```sh
[cloud-user@rhel9-ha2 ~]$ sudo pcs status
Cluster name: my_cluster
Cluster Summary:
  * Stack: corosync (Pacemaker is running)
  * Current DC: rhel9-ha2 (version 2.1.8-3.el9-3980678f0) - partition with quorum
  * Last updated: Sun Mar  2 20:40:33 2025 on rhel9-ha2
  * Last change:  Sun Mar  2 20:10:36 2025 by root via root on rhel9-ha1
  * 2 nodes configured
  * 2 resource instances configured

Node List:
  * Online: [ rhel9-ha2 ]
  * OFFLINE: [ rhel9-ha1 ]

Full List of Resources:
  * Resource Group: nodeapp-group:
    * nodeapp-storage	(ocf:heartbeat:Filesystem):	 Started rhel9-ha2
    * nodeapp	(systemd:nodeapp):	 Started rhel9-ha2

Daemon Status:
  corosync: active/enabled
  pacemaker: active/enabled
  pcsd: active/enabled
```

rhel9-ha1がオフラインになり、Pacemakerリソースがrhel9-ha2上で実行されていることが確認できました。rhel9-ha1が何らかの原因で停止（シャットダウン）してしまっても、アプリケーションはrhel-ha2上にフェイルオーバされて継続動作している旨を確認できました。

rhel9-ha1を再起動（Start）させます。

![スクリーンショット 2025-03-01 0.21.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/be8ceb18-381f-45ad-834c-9d79d1d2ea98.png)

rhel9-ha1が完全に再起動したらSSHで再接続しておきます。rhel9-ha1側からもう一度`sudo pcs status`を実行すると、rhel9-ha1がオンラインに戻り、Pacemakerリソースは引き続きrhel9-ha2側で実行されていることが確認できます。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs status
Cluster name: my_cluster
...
Node List:
  * Online: [ rhel9-ha1 rhel9-ha2 ]

Full List of Resources:
  * Resource Group: nodeapp-group:
    * nodeapp-storage	(ocf:heartbeat:Filesystem):	 Started rhel9-ha2
    * nodeapp	(systemd:nodeapp):	 Started rhel9-ha2
...
```

## ノード障害を再現する
最後に、アプリケーションが動作している仮想マシンがスケジュールされているノード（ホストマシン）が停止した場合の挙動を確認しておきます。

まずは、現在アプリケーションが動作している仮想マシン（rhel9-ha2）がスケジュールされているノードを確認しておきます。以下のコマンドで確認できます。

```sh
User@localmachine ~ % oc get vmi -n rhel-ha -o wide        
NAME        AGE     PHASE     IP             NODENAME                                        READY   LIVE-MIGRATABLE   PAUSED
rhel9-ha1   9m40s   Running   10.130.2.118   ip-10-0-19-3.ap-northeast-1.compute.internal    True    True              
rhel9-ha2   129m    Running   10.131.2.15    ip-10-0-3-184.ap-northeast-1.compute.internal   True    True              
oomuramaki@Masakis-MacBook-Air15 ~ % 
```

また、OpenShiftのコンソールからも簡単に確認できます。

![スクリーンショット 2025-03-03 11.19.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/33b4ebb9-b329-4a45-bb4c-180c0731986a.png)

では、AWSのマネジメントコンソールのEC2メニューから、rhel9-ha2がスケジュールされているインスタンスを停止してみましょう。

![スクリーンショット 2025-03-03 11.23.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/6eb52927-5b24-40da-833d-4a0e00a97270.png)


インスタンスが終了するまではアプリケーションが動作し続けてますが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/b4b22fab-78f7-48c4-b0ba-f8b6a4921334.png)

インスタンスの停止が完了すると、アプリにアクセスできなくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/a7d3677c-0e77-433b-adbe-393453a1768d.png)

この時、OpenShiftのコンソール上ではrhel9-ha2がスケジュールされていたノードが`NotReady`となります。

![スクリーンショット 2025-03-03 11.27.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/e9f2b0bb-ad3f-41a3-a54b-fdccdc4237ea.png)

アプリケーションの画面を更新すると、再びrhel9-ha1側にアプリケーションが戻ってきており、正しくフェイルオーバされたことが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/a4db5706-c91b-48c6-8814-30b8b183196a.png)

カウンター値も引き継がれています。最後に`sudo pcs status`で状況を確認しておきます。

```sh
[cloud-user@rhel9-ha1 ~]$ sudo pcs status
Cluster name: my_cluster
...
Node List:
  * Online: [ rhel9-ha1 ]
  * OFFLINE: [ rhel9-ha2 ]

Full List of Resources:
  * Resource Group: nodeapp-group:
    * nodeapp-storage	(ocf:heartbeat:Filesystem):	 Started rhel9-ha1
    * nodeapp	(systemd:nodeapp):	 Started rhel9-ha1
...
```

rhel9-ha2はスケジュールされていたノードが落ちたことに伴いオフラインになりました。Pacemakerリソースがrhel9-ha1側で起動していることが確認でき、物理ホスト障害を起因としたフェイルオーバが動作していることが見て取れました。

# おわりに
OpenShift Virtualization上で起動しているRHEL仮想マシンのHAクラスタリングを試しました。OpenShift Virtualizationの特徴である「RHEL仮想マシン建て放題」を活用すれば、これまでは「本当はOSレベルのHAクラスタリングをしたいけど、RHELのサブスクリプションを追加で購入するのはちょっとなあ...」と感じていた方々のハードルを下げてくれると思います。

また、共有ディスクや共有ストレージの管理がセルフサービス化され、開発者側でHAクラスタを組む仮想マシンといっしょに共有ストレージもプロビジョニングできるようになります。これにより、よりHA Add-on利用のハードルも下がるのではないでしょうか！？