PacemakerのResource Agentを自作し、Red Hat High Availability ClusterからIBM Cloudのリソースを管理する

# 前置き

:::note info
この記事は[IBM CloudのVirtual Server for VPCでRed Hat High Availability Clusterを構成する](https://qiita.com/ytada/items/911d512d669d27089886)の補足記事として作成したものです。背景の理解のためにあらかじめ上記の記事をお読みいただくことをおすすめします。
:::

# はじめに

Pacemakerには、リソースエージェントと呼ばれるスクリプトが同梱されています。これは、Pacemakerが起動や停止といった対象のリソースへの操作を行うために実行されるスクリプトです。管理対象として様々なサービスに対応したリソースエージェントがあらかじめ用意されていますが、このリソースエージェントは必要に応じて自分で作成することも可能であり、クラスターの管理対象のサービスとして追加することが可能です。 

そこで、この記事ではIBM CloudのRIP(Reserved IP)と呼ばれるプライベートIPアドレスを管理するためのリソースエージェントを作成し、Red Hat High Availability Clusterの管理下に置くことが目標です。また、上記を達成することで、フェイルオーバー時に自動的にRIPをパッシブノード側の仮想サーバーに付与する構成を実現したいと思います。


# 参考文献

作成にあたって参考にさせていただいたのが、公式ドキュメントである[OCFリソースエージェント開発者ガイド](https://github.com/linux-ha-japan/doc-ja/blob/master/linux-ha-doc/dev-guides/ra-dev-guide.asc)の日本語版です。基本的にはこちらのドキュメントに記載されている通りに記述すれば問題ありません。

# スクリプトの作成

```shell: ibmcloud-secondary-ip
#!/bin/sh

#
#   Resource Agent for IBM Cloud resources.
#
#   Copyright (c) 2024 Yuki Tada
#


#Initialize
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/resource.d/heartbeat}
. ${OCF_FUNCTIONS_DIR}/.ocf-shellfuncs

#Environment variables
host_name=`hostname`
case ${host_name} in
	ytada-rhel-ha-test-1)
		vni_id="xxxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
		;;
	ytada-rhel-ha-test-2)
		vni_id="xxxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
		;;
esac 
vip_id="xxxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
api_key="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

meta_data() {
	cat << EOF
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="secondary-rip-resource" version="1.0">
	<version>1.0</version>
	<longdesc lang="en">This is a Resource Agent to manage IBM Cloud secondary ip address</longdesc>
	<shortdesc lang="en">Resource Agent for IBM Cloud secondary IP</shortdesc>
	<parameters>
	</parameters>
	<actions>
		<action name="meta-data" timeout="60" />
		<action name="start" timeout="60" />
		<action name="stop" timeout="60" />
		<action name="monitor" timeout="60" />
		<action name="validate-all" timeout="60" />
	</actions>
</resource-agent>
EOF
	return $OCF_SUCCESS
}

validate_status(){
	return $OCF_SUCCESS
}

rip_start(){
	rip_monitor
	if [ $? =  $OCF_SUCCESS ]; then
		ocf_log info "Resource is already running"
		return $OCF_SUCCESS
        fi
	
	echo N | ibmcloud login -a cloud.ibm.com --apikey ${api_key} -r jp-tok
	echo y | ibmcloud plugin install vpc-infrastructure
	ibmcloud is virtual-network-interface-reserved-ip-bind ${vni_id} ${vip_id}
	sleep 2
	return $OCF_SUCCESS
}

rip_stop(){
	rip_monitor
	case $? in
		"$OCF_SUCCESS")
			ocf_log debug "Resource is currently running"
			;;
		"$OCF_NOT_RUNNING")
			ocf_log info "Resource is already stopped"
			return $OCF_SUCCESS
			;;
	esac
	
	echo N | ibmcloud login -a cloud.ibm.com --apikey ${api_key} -r jp-tok
	echo y | ibmcloud plugin install vpc-infrastructure
	echo y | ibmcloud is virtual-network-interface-reserved-ip-unbind ${vni_id} ${vip_id}
	sleep 2
	return $OCF_SUCCESS
}

rip_monitor(){
	echo N | ibmcloud login -a cloud.ibm.com --apikey ${api_key} -r jp-tok
	echo y | ibmcloud plugin install vpc-infrastructure
	number_rip=$(ibmcloud is virtual-network-interface-reserved-ips ${vni_id} --output JSON  | jq "length")
	
	if [ ${number_rip} -eq 2 ];
		then
			return $OCF_SUCCESS
		fi
		return $OCF_NOT_RUNNING
}

sample_usage(){
	echo "Resource Agent for IBM Cloud secondary IP"
	return $OCF_SUCCESS
}

# Translate each action into the appropriate function call
case $__OCF_ACTION in
meta-data)	meta_data
		exit $OCF_SUCCESS
		;;
start)		rip_start;;
stop)		rip_stop;;
monitor)	rip_monitor;;
validate-all)	validate_status;;
usage|help)	sample_usage
		exit $OCF_SUCCESS
		;;
esac

```

# 解説

それぞれの項目について解説します。

## 使用言語について

リソースエージェントはどのような言語でも実装可能ですが、基本的にはシェルスクリプトで記述されることが多いようです。今回はシェルスクリプトを利用しています。
```shell
#!/bin/sh
```

## 初期化

### Initialize

必要なライブラリ関数を読み込んでいるとのことです。全てのリソースエージェントで利用するため、必ず必要になります。
```shell
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/resource.d/heartbeat}
. ${OCF_FUNCTIONS_DIR}/.ocf-shellfuncs
```

### Environment variables

環境変数を設定しています。API Key等のデータはベタ打ちせず、他のファイルから読み込むなどのセキュリティ対策を講じた方が良いと思います。今回は動作検証のためここで指定しました。
このスクリプトは、クラスター内の全てのノード上に配置されることになります。そのため、ホスト名に合わせて変数の値を変え、後続の処理を実施するように設定しています。

```shell
host_name=`hostname`
case ${host_name} in
	ytada-rhel-ha-test-1)
		vni_id="xxxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
		;;
	ytada-rhel-ha-test-2)
		vni_id="xxxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
		;;
esac 
vip_id="xxxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
api_key="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```


## 実行ブロックの定義

リソースエージェントが起動すると、この内容が実行されます。リソースエージェントは実行されるときに引数としてこの実行ブロック中のアクション(start/stop/monitorなど)が指定されます。ここではそれぞれのアクションを作成した関数の呼び出しに変換しています。

```shell
# Translate each action into the appropriate function call
case $__OCF_ACTION in
meta-data)	meta_data
		exit $OCF_SUCCESS
		;;
start)		rip_start;;
stop)		rip_stop;;
monitor)	rip_monitor;;
validate-all)	validate_status;;
usage|help)	sample_usage
		exit $OCF_SUCCESS
		;;
esac
```
下記の4つのアクションは必ずサポートする必要があります。
- start
    - リソースを起動します。
- stop
    - リソースを停止します。
- monitor
    - リソースの状態を問合せします。
- meta-data
    - リソースエージェントメタデータをダンプします。

また、meta-dataとusage|helpにおいては必ず`$OCF_SUCCESS`を戻り値として返す必要があります。戻り値については[こちら](https://github.com/linux-ha-japan/doc-ja/blob/master/linux-ha-doc/dev-guides/ra-dev-guide.asc#%E6%88%BB%E3%82%8A%E5%80%A4)に記載されています。


## アクション

### meta-dataアクション
リソースエージェントのメタデータを標準出力へ書き出します。その目的やサポートしているパラメータを一連のXMLメタデータとして記述することができます。なお、<actions>タグではそれぞれのアクションのタイムアウト値を指定することができますが、ここでは推奨値が記載できるのみで、実際のタイムアウトの管理はクラスターマネージャー側で実施します。

```shell
meta_data() {
	cat << EOF
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="secondary-rip-resource" version="1.0">
	<version>1.0</version>
	<longdesc lang="en">This is a Resource Agent to manage IBM Cloud secondary ip address</longdesc>
	<shortdesc lang="en">Resource Agent for IBM Cloud secondary IP</shortdesc>
	<parameters>
	</parameters>
	<actions>
		<action name="meta-data" timeout="60" />
		<action name="start" timeout="60" />
		<action name="stop" timeout="60" />
		<action name="monitor" timeout="60" />
		<action name="validate-all" timeout="60" />
	</actions>
</resource-agent>
EOF
	return $OCF_SUCCESS
}
```

### validate-allアクション
動作環境がそもそも正しいことを確認します。この関数は明示的に実行されたときだけでなく、start/stop/monitorなどの他のアクションから呼び出されることもあるので、その点を念頭に置いて設計する必要があります。
正しい動作環境にある場合は`$OCF_SUCCESS`を戻り値として返します。今回は動作検証として一旦$OCF_SUCCESSをそのまま返すようにしましたが、実際はIBM Cloud CLIが導入されているか、IBM Cloudにログインできるか、**必要な`ibmcloud`コマンドが実行できるか**、APIが正しく実行できるか、といった点をこの関数内で確認するようにするのが良いと思います。

```shell
validate_status(){
	return $OCF_SUCCESS
}
```

### startアクション
monitorアクションを実行し、リソースが稼働しているか(2次IPがすでに割り当てられているかどうか)を確認します。割り当てられている場合はそのまま$OCF_SUCCESSを返して関数を終了し、割り当てられていない場合は`ibmcloud`コマンドを利用してあらかじめ予約したVIP用のRIPを自身のサーバーのVNI(仮想ネットワークインターフェース)にバインドします。

```shell
rip_start(){
	rip_monitor
	if [ $? =  $OCF_SUCCESS ]; then
		ocf_log info "Resource is already running"
		return $OCF_SUCCESS
        fi
	
	echo N | ibmcloud login -a cloud.ibm.com --apikey ${api_key} -r jp-tok
	echo y | ibmcloud plugin install vpc-infrastructure
	ibmcloud is virtual-network-interface-reserved-ip-bind ${vni_id} ${vip_id}
	sleep 2
	return $OCF_SUCCESS
}
```


### stopアクション
monitorアクションを実行し、リソースの稼働状況を確認します。startアクションと違いcase文にしているのは、戻り値によっては他のアクションを実行する必要があるからです(今回はstatelessリソースですが、statefulリソースの場合にはMaster/Slaveの稼働状態が存在するため、`$OCF_RUNNING_MASTER`が返ってくる場合があります)。すでにリソースが止まっている場合(2次IPが割り当てられていない場合)にはそのまま$OCF_SUCCESSを返して関数を終了し、リソースが稼働している場合(2次IPが割り当てられている場合)は`ibmcloud`コマンドを利用して割り当てられているVIP用のRIPをアンバインドします。

```shell
rip_stop(){
	rip_monitor
	case $? in
		"$OCF_SUCCESS")
			ocf_log debug "Resource is currently running"
			;;
		"$OCF_NOT_RUNNING")
			ocf_log info "Resource is already stopped"
			return $OCF_SUCCESS
			;;
	esac
	
	echo N | ibmcloud login -a cloud.ibm.com --apikey ${api_key} -r jp-tok
	echo y | ibmcloud plugin install vpc-infrastructure
	echo y | ibmcloud is virtual-network-interface-reserved-ip-unbind ${vni_id} ${vip_id}
	sleep 2
	return $OCF_SUCCESS
}
```

### monitorアクション
リソースの稼働状況を確認します。ここでは、自身の仮想サーバーのVNIに対してRIPが付与されている個数を`ibmcloud`コマンドで確認しています。1つならPrimary IPのみ、2つならSecondary IPも付与されているだろう、という単純な確認方法です。
```shell
rip_monitor(){
	echo N | ibmcloud login -a cloud.ibm.com --apikey ${api_key} -r jp-tok
	echo y | ibmcloud plugin install vpc-infrastructure
	number_rip=$(ibmcloud is virtual-network-interface-reserved-ips ${vni_id} --output JSON  | jq "length")
	
	if [ ${number_rip} -eq 2 ];
		then
			return $OCF_SUCCESS
		fi
		return $OCF_NOT_RUNNING
}
```

### usageアクション
リソースエージェントの使用方法を記載します。それぞれの実行コマンドの用途などを記載すると良いと思います。
```shell
sample_usage(){
	echo "Resource Agent for IBM Cloud secondary IP"
	return $OCF_SUCCESS
}
```

# スクリプトのテスト

スクリプトの作成が完了した後はテストを実行します。作成したスクリプトは`/usr/lib/ocf/resource.d/`配下に作成者名でフォルダを作成し、格納する必要があります。テストには`ocf-tester`コマンドを利用します。

```shell: テストコマンド
ocf-tester -n ibmcloud-secondary-ip /usr/lib/ocf/resource.d/ytada/ibmcloud-secondary-ip
```

テストに失敗する場合は`-d`や`-v`、`-X`オプションを付与して実行し、エラーの原因を修正します。`/var/log/messages`ファイルにもpacemakerのログが出力されますので、確認すると参考になります。
```shell
$ ocf-tester -n ibmcloud-secondary-ip /usr/lib/ocf/resource.d/ytada/ibmcloud-secondary-ip
Beginning tests for /usr/lib/ocf/resource.d/ytada/ibmcloud-secondary-ip...
* rc=7: Monitoring an active resource should return 0
* rc=7: Probing an active resource should return 0
* rc=7: Monitoring an active resource should return 0
* rc=7: Monitoring an active resource should return 0
Tests failed: /usr/lib/ocf/resource.d/ytada/ibmcloud-secondary-ip failed 4 tests
```


全てのテストにパスすれば完了です。
```shell
$ ocf-tester -n ibmcloud-secondary-ip /usr/lib/ocf/resource.d/ytada/ibmcloud-secondary-ip
Beginning tests for /usr/lib/ocf/resource.d/ytada/ibmcloud-secondary-ip...
/usr/lib/ocf/resource.d/ytada/ibmcloud-secondary-ip passed all tests
```


# リソースの作成

リソースエージェントを指定し、Pacemakerのリソースを作成します。スクリプトの中ではIBM Cloudにログインしたり、CLIで情報を取得したりといった時間がかかるアクションも存在するため、タイムアウト値を長めに設定しています。

```shell
pcs resource create controll_rip ocf:ytada:ibmcloud-secondary-ip op monitor interval=10s timeout=60s op start timeout=60s op stop timeout=60s --before website --group ha_test_group 
```

正しく作成できているか確認します。

```shell
$ pcs status
Cluster name: my_cluster
Cluster Summary:
  * Stack: corosync (Pacemaker is running)
  * Current DC: 10.50.4.15 (version 2.1.7-5.2.el9_4-0f7f88312) - partition with quorum
  * Last updated: Mon Dec  9 01:51:57 2024 on 10.50.4.15
  * Last change:  Mon Dec  9 01:49:31 2024 by root via root on 10.50.4.15
  * 2 nodes configured
  * 5 resource instances configured

Node List:
  * Online: [ 10.50.4.15 10.50.4.16 ]

Full List of Resources:
  * ibmvpcfence	(stonith:fence_ibm_vpc):	 Started 10.50.4.15
  * Resource Group: ha_test_group:
    * fs_nfs	(ocf:heartbeat:Filesystem):	 Started 10.50.4.16
    * vip	(ocf:heartbeat:IPaddr2):	 Started 10.50.4.16
    * controll_rip	(ocf:ytada:ibmcloud-secondary-ip):	 Started 10.50.4.16
    * website	(ocf:heartbeat:apache):	 Started 10.50.4.16

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

設定したVIPで正しく接続できていることを確認します。

```shell
$ curl 10.50.4.100
<html>
<body>Hello</body>
</html>
```