以下の内容はhttps://tech.enechange.co.jp/entry/2026/03/13/115502より取得しました。


Corosync+PacemakerによるNginx HA構成を自立型AIエージェントで構築したかった話

Corosync + Pacemaker によるオンプレミス向けHA設計

PlatformEngineeringのLDRです。 Qごとに1日だけ、プロダクト開発や事業課題の解決に活用できそうな技術についてインプット/アウトプットする「I/O Day」があります。今回は、過去に構築したことのあるCorosync + PacemakerによるHA構成を、自立型AIエージェントで構築させてみようとした話です。

裏設定

  • SaaSばかりやってるとオンプレ案件の時に環境での構築ってこれどうやってたっけ…って浦島状態になるのでたまに棚卸しが必要。DCの機器の持ち込み制限のある区域でBIOS/UEFI設定してイメージ焼いて物理へクリーンインストールしてRAID組めますか?
  • 10数年前に2Sprintほど費やして構築したHA、AIを利用した今ならどこまで工数短縮できるのか?
  • AIエージェントを活用した設定作業の効率化を検証する

検証の位置づけ

  • 最終目標: フェイルオーバーを要件(30秒以内)通りに動作させること
  • 今回はHA挙動の動作確認を目的として、AWS EC2を仮想的なサーバー環境として使用
  • AWSの高可用性サービス(ALB等)は使用せず、オンプレミスと同等の構成で検証

Corosync + Pacemaker の適用領域

代表的な手法 特徴
フロント〜アプリ層 F5 / HAProxy / Keepalived 無停止・高速切替(秒以下)が求められる
DB・基幹系 Corosync + Pacemaker ある程度のダウンタイム(〜30秒)が許容される

💡Corosync + Pacemaker は「完全無停止」ではなく「自動復旧」を目的とした仕組み。 フロント層の無停止にはLB(F5等)を組み合わせるのが一般的。

今回 Nginx を検証対象にした理由

  • PostgreSQL(DB)の方がPacemakerの典型的なユースケースに近い
  • ただし今回はHA挙動(VIP切替・自動復旧)の確認が目的のため、設定がシンプルなNginxで代替
  • 本番適用時はDB(PostgreSQL等)のリソースエージェントに置き換えて同様の構成が可能

AIエージェント(Aider)の制約

⚠️今回はAnthropicのAPIキーが利用できなかったため、Bedrock対応のAider(対話型)で代替した。

  • Aiderは対話型のAIコーディングアシスタントであり、自律的にコマンドを実行する自立型エージェントではない
  • ユーザーが指示を出し、Aiderがファイル編集と「実行すべきコマンド」を提示する形で動作する
  • SSH経由での他ノードへの直接操作は不可。node1で編集した設定ファイルはnode2へ手動でscpする必要がある
  • そのため nginx-ha-requirements.md に「編集後はscp + restartを実行すること」などの手順を明記しておくことで、Aiderがユーザーに正しい操作手順を提示できる

障害パターン別RTO要件

障害パターン 内容 目標RTO
ノード障害 サーバーダウン、Corosync停止 30秒以内
プロセス障害 Nginxクラッシュ、プロセス停止 60秒以内(restart含む)
ネットワーク障害 スプリットブレイン 別途設計が必要

💡今回の検証はノード障害パターン(Corosync停止)を対象とした


構成概要

graph TD
    Client["クライアント"]
    VIP["Virtual IP (10.0.0.100)"]

    Client -->|HTTP| VIP

    subgraph VPC["AWS VPC"]
        subgraph node1["node1 (10.0.0.182)"]
            N1["Nginx"]
            PM1["Pacemaker"]
            CS1["Corosync"]
        end
        subgraph node2["node2 (10.0.5.29)"]
            N2["Nginx"]
            PM2["Pacemaker"]
            CS2["Corosync"]
        end
    end

    VIP --> node1
    VIP -.->|待機| node2
    CS1 <-->|ハートビート UDP 5404/5405| CS2
  • 2台のEC2インスタンス(Amazon Linux 2023)
  • 同一サブネット内にVIP(10.0.0.100)を配置
  • Corosync: ノード間ハートビート通信
  • Pacemaker: リソース(VIP・Nginx)の管理・制御

当初検証したかった構成: 完全自立型AIエージェントによるHA設定管理

Anthropic APIキーが利用可能であれば、Claude Codeによる完全自立型構成が実現できる。

graph TD
    API["Anthropic API\nclaude-3-5-sonnet"]

    subgraph node1["node1 (10.0.0.182)"]
        CC["Claude Code\n(ターミナル)"]
        Files1["設定ファイル\n/etc/corosync/corosync.conf\n/etc/corosync/authkey\n/etc/nginx/nginx.conf"]
        CS1["Corosync\nUDP 5404/5405"]
        PM1["Pacemaker\nVIP・Nginx リソース管理"]
    end

    subgraph node2["node2 (10.0.5.29)"]
        Files2["HA設定ファイル受信"]
        CS2["Corosync\nUDP 5404/5405"]
        PM2["Pacemaker\nVIP・Nginx リソース管理"]
        N2["Nginx (待機中)"]
    end

    API -->|HTTPS APIキー認証| CC
    CC -->|"① 編集\n② pcs maintenance-mode=true\n⑤ pcs resource restart Nginx\n⑥ pcs maintenance-mode=false"| Files1
    CC -->|"③ scp\n④ systemctl restart corosync"| node2
    Files1 --> CS1
    CS1 -->|ノード障害通知| PM1
    CS1 <-->|"ハートビート UDP 5404/5405"| CS2
    CS2 -->|ノード障害通知| PM2

コンポーネント相関図(今回の実装: Aider + Bedrock)

graph TD
    Bedrock["Amazon Bedrock\nclaude-3-5-sonnet (APAC)"]

    subgraph node1["node1 (10.0.0.182)"]
        Aider["Aider 対話型AIコーディングアシスタント\nコンテキスト: nginx-ha-requirements.md\n← ユーザーが自然言語で指示\n→ ファイル編集 + コマンドをユーザーに提示"]
        Files1["設定ファイル\n/etc/corosync/corosync.conf\n/etc/nginx/nginx.conf"]
        User["ユーザーがコマンドを実行\nscp → node2\nsystemctl restart corosync"]
        CS1["Corosync\nUDP 5404/5405"]
        PM1["Pacemaker\nVIP・Nginx リソース管理\nawsvip (OCF RA)"]
    end

    subgraph node2["node2 (10.0.5.29)"]
        Files2["HA設定ファイル(scp)"]
        CS2["Corosync\nUDP 5404/5405"]
        PM2["Pacemaker\nVIP・Nginx リソース管理"]
        N2["Nginx (待機中)"]
    end

    Bedrock -->|HTTPS IAMロール認証| Aider
    Aider -->|編集| Files1
    Aider -->|scp手順を提示| User
    User -->|scp + systemctl| node2
    CS1 -->|ノード障害通知| PM1
    CS1 <-->|"ハートビート UDP 5404/5405"| CS2
    CS2 -->|ノード障害通知| PM2

フェイルオーバートリガー 1. Corosync: ノード障害(ハートビート途絶 → 約3秒で検知) 2. Pacemaker: Nginxプロセス障害(monitor 10秒毎 → 3回失敗でF/O)


フェイルオーバーの仕組み

sequenceDiagram
    participant C as クライアント
    participant VIP as VIP (10.0.0.100)
    participant N2 as node2 (Nginx稼働中)
    participant N1 as node1 (待機中)

    Note over C,N2: 通常時
    C->>VIP: HTTP リクエスト
    VIP->>N2: 転送
    N2-->>C: 200 OK

    Note over N2: node2 Corosync停止(障害)
    N2--xN1: ハートビート途絶(約3秒で検知)
    N1->>VIP: PacemakerがVIPをnode1のENIに移動
    N1->>N1: Nginx起動

    Note over C,N1: フェイルオーバー完了(12秒以内)
    C->>VIP: HTTP リクエスト
    VIP->>N1: 転送
    N1-->>C: 200 OK

検証結果

フェイルオーバー前

  • リソース稼働ノード: node2
  • クラスター状態: node1・node2ともに Online、VIP・Nginx が node2 で稼働中

障害シミュレート

  • node2のCorosync停止 → node2が OFFLINE

フェイルオーバー後(12秒以内)

  • リソース移行先: node1(自動)
  • VIPへのHTTPアクセス: 200 OK(継続)

エビデンスファイル: ha-evidence-20260312_034606.log

=== HA動作確認エビデンス ===
Thu Mar 12 03:46:06 UTC 2026

--- [1] フェイルオーバー前: クラスター状態 ---
Cluster name: nginx-ha
Cluster Summary:
  * Stack: corosync (Pacemaker is running)
  * Current DC: node1 (version 3.0.1-0.4.rc2.amzn2023.0.1-042ad46) - partition with quorum
  * Last updated: Thu Mar 12 03:46:06 2026 on node1
  * Last change:  Thu Mar 12 03:45:47 2026 by root via root on node1
  * 2 nodes configured
  * 2 resource instances configured

Node List:
  * Online: [ node1 node2 ]

Full List of Resources:
  * Nginx   (ocf:heartbeat:nginx):   Started node2
  * VirtualIP   (ocf:heartbeat:awsvip):  Started node2

Daemon Status:
  corosync: active/enabled
  pacemaker: active/enabled
  pcsd: inactive/disabled

--- [2] 障害シミュレート ---
アクティブノード(node2)で以下を実行してください:
  sudo systemctl stop corosync
フェイルオーバー開始時刻: 2026-03-12 03:46:16

--- [3] フェイルオーバー監視: VIPへのポーリング(最大30秒) ---
[03:46:16] +0秒: HTTP 000 (待機中...)
[03:46:17] +1秒: HTTP 000 (待機中...)
[03:46:18] +2秒: HTTP 000 (待機中...)
[03:46:19] +3秒: HTTP 000 (待機中...)
[03:46:20] +4秒: HTTP 000 (待機中...)
[03:46:21] +5秒: HTTP 000 (待機中...)
[03:46:22] +6秒: HTTP 000 (待機中...)
[03:46:23] +7秒: HTTP 000 (待機中...)
[03:46:24] +8秒: HTTP 000 (待機中...)
[03:46:25] +9秒: HTTP 000 (待機中...)
[03:46:26] +10秒: HTTP 000 (待機中...)
[03:46:27] +11秒: HTTP 000 (待機中...)
[03:46:28] +12秒: HTTP 200 ← フェイルオーバー完了

--- [4] フェイルオーバー後: クラスター状態 ---
Node List:
  * Online: [ node1 ]
  * OFFLINE: [ node2 ]

Full List of Resources:
  * Nginx   (ocf:heartbeat:nginx):   Started node1
  * VirtualIP   (ocf:heartbeat:awsvip):  Started node1 (Monitoring)

--- [5] フェイルオーバー後: VIPへのHTTPアクセス ---
< HTTP/1.1 200 OK
< Server: nginx/1.28.2
OK

--- [6] 結果サマリー ---
✓ フェイルオーバー完了: 12秒(要件: 30秒以内)
✓ 要件達成

=== 確認完了: ha-evidence-20260312_034606.log に保存されました ===


corosync.conf チューニングポイント

totem:
  transport: udpu      # ユニキャスト(EC2はマルチキャスト不可)
  token: 3000          # ハートビートタイムアウト 3000ms
  join: 60
  consensus: 3600
  max_messages: 20

quorum:
  two_node: 1          # 2ノード構成用クォーラム設定
パラメータ 設定値 理由
transport: udpu ユニキャスト EC2環境ではマルチキャスト(udp)が使用不可
token: 3000 3000ms ノード障害の検知タイムアウト。EC2のネットワーク遅延を考慮しデフォルト(1000ms)より余裕を持たせた
two_node: 1 有効 2ノード構成では1台停止時にクォーラム喪失を防ぐために必須
join: 60 60ms ノードがクラスターに参加するまでの待機時間(単位: ms)
consensus: 3600 3600ms クォーラム合意のタイムアウト。EC2環境での安定性を考慮

F/O時間12秒の内訳:

token: 3000ms → ノード障害検知(約3秒)
      ↓
Pacemaker: VIP付け替え(AWS ENI操作)
      ↓
Nginx起動 + ヘルスチェック通過
      ↓
合計: 12秒以内

💡token の値を下げるほど検知は速くなるが、ネットワーク瞬断での誤検知リスクが上がる。EC2環境では3000ms程度が安定動作の目安

💡クラウド環境で異なるリージョンをまたいで構成した場合、リージョン間レイテンシ(数十〜100ms超)によりCorosyncのハートビートが誤タイムアウトし、正常ノードを障害と誤検知して誤フェイルオーバーが頻発する。Corosync+Pacemakerはそもそも同一データセンター(低レイテンシ前提)の設計なので、リージョン跨ぎはアーキテクチャ的に向いていない。


スティッキー動作とは: フェイルオーバー後、元のノードが復旧しても自動的に戻らない制御

# enable,disableじゃなくてスコアで指定する。
# 0にすると復旧した元のノードに自動フェイルバックし、INFINITYにすると障害が起きても絶対に移動しなくなるので実用上は使わない。
設定: default-resource-stickiness=100
タイミング リソース稼働ノード
フェイルオーバー前 node2
node2障害発生 node1(自動移行)
node2復旧後 node1のまま(自動フェイルバックなし)

→ 不要な切り替えによるサービス断を防止


セキュリティ対策

項目 対策
SSH認証 公開鍵認証のみ(パスワード認証無効)
Corosync通信 UDP 5404/5405をノード間のみ許可
authkey パーミッション400(オーナーのみ読み取り)
AWS認証 IAMロール(最小権限)、APIキー不要
IAMロール Bedrock + ENI操作に必要なEC2 APIのみ許可

AIエージェント(Aider)の活用

各ノードにAider + Amazon Bedrockをインストール

# EC2環境: IAMロールで自動認証(APIキー不要)
export AWS_REGION=ap-northeast-1
bash /home/ec2-user/start-aider.sh
  • nginx-ha-requirements.md をコンテキストとして読み込み
  • 要件を理解した上で設定ファイルを編集支援
  • 編集対象: /etc/nginx/nginx.conf, /etc/corosync/corosync.conf
  • モデル: bedrock/apac.anthropic.claude-3-5-sonnet-20241022-v2:0
項目 Anthropic API Amazon Bedrock(今回)
認証 APIキー(手動管理) IAMロール(自動)
EC2での利用 インターネット経由 AWS内で完結
オンプレでの利用 可能 AWS認証情報で可能
コスト管理 Anthropic請求 AWS請求に統合

⚠️Bedrockのモデル呼び出し方式が2種類あり、anthropic.claude-3-5-sonnet-20241022-v2:0 を直接指定するとオンデマンドスループットで呼び出そうとしてエラーになる。apac. を付けることでクロスリージョン推論プロファイル経由になり、正常に動作した。


AI以前 / AI以降: Corosync+Pacemaker設定の変化

AI以前: ハマりやすかったポイント

# 問題 症状
1 transport のデフォルトがマルチキャスト(udp) EC2/VM環境でハートビートが届かずクラスター未形成。ログは「ring0 is not connected」程度
2 authkeyの同期忘れ・パーミッション不正 authentication failed で起動拒否。ログを見ないと原因不明
3 pcs / crm コマンドの混在 ネット上の古い手順(crm)と新しい手順(pcs)が混在しコピペが動かない
4 EC2で IPaddr2 を使用 Gratuitous ARPが効かずVIPが引き継がれない。AWSのネットワーク仕様に気づくまでが長い
5 two_node: 1 の設定漏れ 1台停止でクォーラム喪失 → リソース全停止。「F/Oしたはずなのにサービスが上がらない」
6 コロケーション・順序制約の未設定 VIPとNginxが別ノードで起動、またはNginxがVIPより先に起動して失敗
7 ログが分散 corosync.log / pacemaker.log / journalctl の3箇所。どこを見るか分からず切り分けに時間

💡上記の問題は「なぜこの設定が必要か」の背景知識がないと、エラーログだけでは原因にたどり着けない。HA界隈のコミュニティでの会話を重ねて得た知見で試行錯誤して数日かかるケースも珍しくなかった。

AI以降(今回): Claude + Aider で変わったこと

観点 変化
設計の抜け漏れ 要件定義・設計書の段階でチューニングポイントを網羅。udputwo_node・制約設定が最初から含まれる
EC2固有の制約 awsvip の必要性、NIC名が ens5 になる点など
コマンド体系 pcs / crm の混乱なく、正しいコマンド体系で一貫して進められる
トラブルシュート エラーが出た際に自然言語で質問 → 原因と対処を即座に提示
ドキュメント 設定の意図・理由が設計書に残るため、後から見ても理解できる

まとめ

  • たった半日でF/O動作まで確認できた
  • オンプレミス向けNginx HA設計をAWS EC2で動作検証完了
  • ノード障害パターンで12秒以内のフェイルオーバーを確認(要件: 30秒以内)
  • スティッキー動作により不要なフェイルバックを防止
  • AIエージェント(Aider)による設定作業支援の基盤を整備
  • エビデンスログ(タイムスタンプ付き)による第三者への説明が可能

💡当時の工数と比較して概ね作業時間93%削減、14倍速で完了。 AIが「答えを知っている」というより、「ハマりポイントを事前に潰した設計書を最初から出してくれる」のが本質的な価値。


[Bonus1] 技術選定の背景

オンプレミス環境でのHA構成として Corosync + Pacemaker を選定。

方式 オンプレ適用 備考
ハードウェアLB コスト高(F5とか1台で2000万円するし独自の管理方法にも座学コストが発生する)
Corosync + Pacemaker OSS、実績豊富(大きなコミュニティがある)
Keepalived + VRRP 機能限定(使ったことない)

今回のAWS検証環境での制約と対応:

オンプレ設定 AWS検証環境での代替
ocf:heartbeat:IPaddr2(Gratuitous ARP) ocf:heartbeat:awsvip(ENIセカンダリIP)
VRRPによるVIP引き継ぎ AWS ENI操作によるVIP引き継ぎ

💡オンプレ本番環境では ocf:heartbeat:IPaddr2 を使用する


[Bonus2] 作成した設定ファイル

phase1-install-packages.sh

#!/bin/bash
# =============================================================================
# フェーズ1: 両ノードへのパッケージインストールスクリプト
# 対象: Primary Node / Secondary Node(共通)
# 実行方法: 各ノードにSSH接続後、このスクリプトを実行する
#
# 使用方法:
#   # ローカルからノードへスクリプトを転送
#   scp -i ~/.ssh/<key>.pem phase1-install-packages.sh ec2-user@<NODE_IP>:/home/ec2-user/
#
#   # ノードにSSH接続してスクリプトを実行
#   ssh -i ~/.ssh/<key>.pem ec2-user@<NODE_IP>
#   chmod +x /home/ec2-user/phase1-install-packages.sh
#   sudo /home/ec2-user/phase1-install-packages.sh
#
# 要件: 1.2, 1.3, 1.4, 4.1, 6.1, 6.2, 6.6
# =============================================================================

set -e

# カラー出力
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

log_info()    { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn()    { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error()   { echo -e "${RED}[ERROR]${NC} $1"; }
log_section() { echo -e "\n${YELLOW}========================================${NC}"; echo -e "${YELLOW}$1${NC}"; echo -e "${YELLOW}========================================${NC}"; }

# rootで実行されているか確認
if [ "$(id -u)" -ne 0 ]; then
  log_error "このスクリプトはrootまたはsudoで実行してください"
  exit 1
fi

# =============================================================================
# タスク 3.1: Python 3.8以上の確認(要件: 6.6)
# =============================================================================
log_section "タスク 3.1: Python 3.8以上の確認"

PYTHON_VERSION=$(python3 --version 2>&1)
log_info "Python バージョン: ${PYTHON_VERSION}"

# バージョンチェック(3.8以上であることを確認)
python3 -c "
import sys
version = sys.version_info
if version < (3, 8):
    print(f'ERROR: Python 3.8以上が必要です。現在のバージョン: {version.major}.{version.minor}.{version.micro}')
    sys.exit(1)
else:
    print(f'OK: Python {version.major}.{version.minor}.{version.micro} は要件を満たしています(3.8以上)')
"

log_info "タスク 3.1 完了: Python バージョン確認OK"

# =============================================================================
# タスク 3.3: CorosyncおよびPacemakerのインストール(要件: 1.3, 1.4)
# =============================================================================
log_section "タスク 3.3: CorosyncおよびPacemakerのインストール"

log_info "パッケージをインストールしています: corosync pacemaker pcs"
dnf install -y corosync pacemaker pcs

log_info "インストール確認中..."
rpm -q corosync pacemaker pcs

log_info "タスク 3.3 完了: Corosync/Pacemaker/pcs インストールOK"

# =============================================================================
# タスク 3.5: Nginxのインストール(要件: 4.1)
# =============================================================================
log_section "タスク 3.5: Nginxのインストール"

log_info "Nginxをインストールしています..."
dnf install -y nginx

log_info "インストール確認中..."
nginx -v

log_info "タスク 3.5 完了: Nginx インストールOK"

# =============================================================================
# タスク 3.6: Aiderのインストール(要件: 6.1, 6.2)
# =============================================================================
log_section "タスク 3.6: Aiderのインストール"

log_info "python3-pipをインストールしています..."
dnf install -y python3-pip

log_info "aider-chatをインストールしています..."
# --ignore-installed: rpmでインストール済みのパッケージとの競合を回避
python3 -m pip install --ignore-installed aider-chat

log_info "インストール確認中..."
python3 -m aider --version 2>/dev/null || aider --version

log_info "タスク 3.6 完了: Aider インストールOK"

# =============================================================================
# タスク 3.8: pcsdサービスの有効化と起動(要件: 1.2)
# =============================================================================
log_section "タスク 3.8: pcsdサービスの有効化と起動"

log_info "pcsdサービスを有効化して起動しています..."
systemctl enable --now pcsd

log_info "pcsdサービスの状態確認..."
systemctl status pcsd --no-pager

log_warn "haclusterユーザーのパスワードを設定してください(クラスター認証に必要)"
log_warn "以下のコマンドを手動で実行してください:"
echo ""
echo "  sudo passwd hacluster"
echo ""
log_warn "Primary NodeとSecondary Nodeで同じパスワードを設定することを推奨します"

log_info "タスク 3.8 完了: pcsd サービス有効化・起動OK"

# =============================================================================
# 完了サマリー
# =============================================================================
log_section "フェーズ1 完了サマリー"

echo ""
log_info "以下のタスクが完了しました:"
echo "  ✓ 3.1 Python 3.8以上の確認"
echo "  ✓ 3.3 Corosync/Pacemaker/pcs インストール"
echo "  ✓ 3.5 Nginx インストール"
echo "  ✓ 3.6 Aider インストール"
echo "  ✓ 3.8 pcsd サービス有効化・起動"
echo ""
log_warn "残りの手動作業:"
echo "  ! sudo passwd hacluster  ← 両ノードで実行してください"
echo ""
log_info "次のステップ: フェーズ4(Corosync設定)へ進んでください"

phase2-corosync-setup.sh(Primary Nodeで実行)

#!/bin/bash
# =============================================================================
# フェーズ4: Corosync設定スクリプト
# nginx-ha-corosync-pacemaker
#
# 【設計方針】
#   Primary NodeからSecondary Nodeへ直接scpでファイルを配布する。
#   両ノード間のSSH接続が事前に設定済みであることを前提とする。
#
# 【実行手順】
#
#   Step 1: スクリプトを両ノードへ転送(ローカルマシンで実行)
#     scp -i ~/.ssh/ha2026.pem phase4-corosync-setup.sh \
#       ec2-user@ec2-54-65-133-39.ap-northeast-1.compute.amazonaws.com:/home/ec2-user/
#     scp -i ~/.ssh/ha2026.pem phase4-corosync-setup.sh \
#       ec2-user@ec2-13-158-13-202.ap-northeast-1.compute.amazonaws.com:/home/ec2-user/
#
#   Step 2: Primary Nodeで実行
#     ssh -i ~/.ssh/ha2026.pem ec2-user@ec2-54-65-133-39.ap-northeast-1.compute.amazonaws.com
#     chmod +x ~/phase4-corosync-setup.sh
#     ~/phase4-corosync-setup.sh primary \
#       --primary-ip 10.0.0.182 \
#       --secondary-ip 10.0.18.234 \
#       --secondary-host ec2-13-158-13-202.ap-northeast-1.compute.amazonaws.com \
#       --key ~/.ssh/ha2026.pem
#
#   Step 3: Secondary Nodeで実行(Primary完了後)
#     ssh -i ~/.ssh/ha2026.pem ec2-user@ec2-13-158-13-202.ap-northeast-1.compute.amazonaws.com
#     ~/phase4-corosync-setup.sh secondary \
#       --primary-ip 10.0.0.182 \
#       --secondary-ip 10.0.18.234
#
# 要件: 2.1, 2.2, 2.3, 2.5, 7.2, 1.2, 8.2
# =============================================================================

set -euo pipefail

# -----------------------------------------------------------------------------
# 定数
# -----------------------------------------------------------------------------
COROSYNC_CONF="/etc/corosync/corosync.conf"
AUTHKEY_PATH="/etc/corosync/authkey"
LOG_DIR="/var/log/cluster"
CLUSTER_NAME="nginx-ha"
TOKEN_TIMEOUT=3000   # 要件 2.3: 5000ms以内

# -----------------------------------------------------------------------------
# カラー出力
# -----------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log_info()  { echo -e "${GREEN}[INFO]${NC}  $*"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }

# -----------------------------------------------------------------------------
# 引数パース
# -----------------------------------------------------------------------------
NODE_ROLE="${1:-}"
shift || true

PRIMARY_IP="${PRIMARY_IP:-}"
SECONDARY_IP="${SECONDARY_IP:-}"
SECONDARY_HOST="${SECONDARY_HOST:-}"
SSH_KEY="${SSH_KEY:-}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --primary-ip)     PRIMARY_IP="$2";     shift 2 ;;
    --secondary-ip)   SECONDARY_IP="$2";   shift 2 ;;
    --secondary-host) SECONDARY_HOST="$2"; shift 2 ;;
    --key)            SSH_KEY="$2";        shift 2 ;;
    *) log_error "不明なオプション: $1"; exit 1 ;;
  esac
done

# -----------------------------------------------------------------------------
# 入力検証
# -----------------------------------------------------------------------------
validate_inputs() {
  if [[ -z "$NODE_ROLE" ]]; then
    log_error "ノードロールを指定してください: primary または secondary"
    echo "使用方法: $0 {primary|secondary} --primary-ip <IP> --secondary-ip <IP> [--secondary-host <HOST>] [--key <pem>]"
    exit 1
  fi

  if [[ "$NODE_ROLE" != "primary" && "$NODE_ROLE" != "secondary" ]]; then
    log_error "ノードロールは 'primary' または 'secondary' を指定してください"
    exit 1
  fi

  if [[ -z "$PRIMARY_IP" || -z "$SECONDARY_IP" ]]; then
    log_error "--primary-ip と --secondary-ip は必須です"
    exit 1
  fi

  if [[ "$NODE_ROLE" == "primary" ]]; then
    if [[ -z "$SECONDARY_HOST" ]]; then
      log_error "Primary Nodeでは --secondary-host が必要です"
      exit 1
    fi
    if [[ -z "$SSH_KEY" ]]; then
      log_error "Primary Nodeでは --key (pemファイルパス) が必要です"
      exit 1
    fi
    if [[ ! -f "$SSH_KEY" ]]; then
      log_error "SSHキーファイルが見つかりません: $SSH_KEY"
      exit 1
    fi
  fi
}

# -----------------------------------------------------------------------------
# ログディレクトリの作成(両ノード共通)
# 要件: 8.2
# -----------------------------------------------------------------------------
setup_log_dir() {
  log_info "ログディレクトリを作成します: $LOG_DIR"
  sudo mkdir -p "$LOG_DIR"
  sudo chown root:root "$LOG_DIR"
  sudo chmod 755 "$LOG_DIR"
}

# -----------------------------------------------------------------------------
# タスク 5.1: authkey生成(Primary Nodeのみ)
# 要件: 2.5, 7.2
# -----------------------------------------------------------------------------
generate_authkey() {
  log_info "=== タスク 5.1: Corosync認証キーを生成 ==="

  if [[ -f "$AUTHKEY_PATH" ]]; then
    log_warn "authkeyが既に存在します。既存のキーを使用します。"
  else
    sudo corosync-keygen -l -k "$AUTHKEY_PATH"
    sudo chmod 400 "$AUTHKEY_PATH"
    sudo chown root:root "$AUTHKEY_PATH"
    log_info "authkey生成完了"
  fi

  # パーミッション確認(要件 7.2)
  local perm
  perm=$(stat -c "%a" "$AUTHKEY_PATH")
  if [[ "$perm" != "400" ]]; then
    log_error "authkeyのパーミッションが正しくありません: $perm (期待値: 400)"
    exit 1
  fi
  log_info "authkeyパーミッション確認OK: $perm"
}

# -----------------------------------------------------------------------------
# タスク 5.3: authkeyをSecondary Nodeへ配布(Primary Nodeのみ)
# 要件: 2.5
# -----------------------------------------------------------------------------
distribute_authkey() {
  log_info "=== タスク 5.3: authkeyをSecondary Nodeへ配布 ==="

  # scpはrootファイルを直接転送できないため、一時的に読み取り可能な状態でコピー
  sudo cp "$AUTHKEY_PATH" /tmp/authkey.transfer
  sudo chmod 644 /tmp/authkey.transfer

  scp -i "$SSH_KEY" \
    -o StrictHostKeyChecking=no \
    -o ConnectTimeout=10 \
    /tmp/authkey.transfer \
    "ec2-user@${SECONDARY_HOST}:/tmp/authkey"

  sudo rm -f /tmp/authkey.transfer

  # Secondary Node側でパーミッションを設定
  ssh -i "$SSH_KEY" \
    -o StrictHostKeyChecking=no \
    -o ConnectTimeout=10 \
    "ec2-user@${SECONDARY_HOST}" \
    "sudo mv /tmp/authkey /etc/corosync/authkey && sudo chmod 400 /etc/corosync/authkey && sudo chown root:root /etc/corosync/authkey"

  # Secondary NodeのパーミッションをSSH経由で確認
  local remote_perm
  remote_perm=$(ssh -i "$SSH_KEY" \
    -o StrictHostKeyChecking=no \
    -o ConnectTimeout=10 \
    "ec2-user@${SECONDARY_HOST}" \
    "stat -c '%a' /etc/corosync/authkey")

  if [[ "$remote_perm" != "400" ]]; then
    log_error "Secondary NodeのauthkeyパーミッションNG: $remote_perm (期待値: 400)"
    exit 1
  fi
  log_info "Secondary Node authkeyパーミッション確認OK: $remote_perm"
}

# -----------------------------------------------------------------------------
# タスク 5.4: corosync.conf作成
# 要件: 2.1, 2.2, 2.3
# -----------------------------------------------------------------------------
create_corosync_conf() {
  log_info "=== タスク 5.4: corosync.confを作成 ==="

  sudo mkdir -p "$LOG_DIR"

  sudo tee "$COROSYNC_CONF" > /dev/null <<EOF
# Corosync設定ファイル
# クラスター名: ${CLUSTER_NAME}
# 生成日時: $(date '+%Y-%m-%d %H:%M:%S')
#
# 要件:
#   2.1 - 有効な設定ファイル
#   2.2 - UDPユニキャスト(EC2環境ではマルチキャスト不可)
#   2.3 - トークンタイムアウト ${TOKEN_TIMEOUT}ms(上限5000ms)

totem {
  version: 2
  cluster_name: ${CLUSTER_NAME}
  transport: udpu
  token: ${TOKEN_TIMEOUT}
  join: 60
  consensus: 3600
  max_messages: 20
}

nodelist {
  node {
    ring0_addr: ${PRIMARY_IP}
    name: node1
    nodeid: 1
  }
  node {
    ring0_addr: ${SECONDARY_IP}
    name: node2
    nodeid: 2
  }
}

quorum {
  provider: corosync_votequorum
  two_node: 1
}

logging {
  to_logfile: yes
  logfile: ${LOG_DIR}/corosync.log
  to_syslog: yes
}
EOF

  sudo chmod 644 "$COROSYNC_CONF"
  sudo chown root:root "$COROSYNC_CONF"

  # token値の検証(要件 2.3)
  local token_val
  token_val=$(grep -E "^\s*token:" "$COROSYNC_CONF" | awk '{print $2}')
  if [[ -z "$token_val" ]] || [[ "$token_val" -gt 5000 ]]; then
    log_error "token値が要件を満たしていません: $token_val (上限: 5000ms)"
    exit 1
  fi
  log_info "corosync.conf作成完了 (token: ${token_val}ms)"
}

# -----------------------------------------------------------------------------
# corosync.confをSecondary Nodeへ配布(Primary Nodeのみ)
# -----------------------------------------------------------------------------
distribute_corosync_conf() {
  log_info "=== corosync.confをSecondary Nodeへ配布 ==="

  scp -i "$SSH_KEY" \
    -o StrictHostKeyChecking=no \
    -o ConnectTimeout=10 \
    "$COROSYNC_CONF" \
    "ec2-user@${SECONDARY_HOST}:/tmp/corosync.conf"

  ssh -i "$SSH_KEY" \
    -o StrictHostKeyChecking=no \
    -o ConnectTimeout=10 \
    "ec2-user@${SECONDARY_HOST}" \
    "sudo mv /tmp/corosync.conf /etc/corosync/corosync.conf && sudo chmod 644 /etc/corosync/corosync.conf && sudo chown root:root /etc/corosync/corosync.conf"

  log_info "Secondary Nodeへのcorosync.conf配布完了"
}

# -----------------------------------------------------------------------------
# タスク 5.6: Corosync起動
# 要件: 2.2
# -----------------------------------------------------------------------------
start_corosync() {
  log_info "=== タスク 5.6: Corosyncサービスを起動 ==="
  sudo systemctl enable --now corosync

  local retries=6
  for ((i=1; i<=retries; i++)); do
    if sudo systemctl is-active --quiet corosync; then
      log_info "Corosync起動確認OK"
      break
    fi
    if [[ $i -eq $retries ]]; then
      log_error "Corosyncの起動に失敗しました"
      sudo systemctl status corosync --no-pager || true
      exit 1
    fi
    log_warn "起動待機中... (${i}/${retries})"
    sleep 5
  done

  log_warn "ring0確認(両ノード起動後に正常になります):"
  sudo corosync-cfgtool -s || log_warn "ring0確認失敗(両ノード起動後に再確認してください)"
}

# -----------------------------------------------------------------------------
# タスク 5.7: Pacemaker起動
# 要件: 1.2
# -----------------------------------------------------------------------------
start_pacemaker() {
  log_info "=== タスク 5.7: Pacemakerサービスを起動 ==="
  sudo systemctl enable --now pacemaker

  local retries=6
  for ((i=1; i<=retries; i++)); do
    if sudo systemctl is-active --quiet pacemaker; then
      log_info "Pacemaker起動確認OK"
      break
    fi
    if [[ $i -eq $retries ]]; then
      log_error "Pacemakerの起動に失敗しました"
      sudo systemctl status pacemaker --no-pager || true
      exit 1
    fi
    log_warn "起動待機中... (${i}/${retries})"
    sleep 5
  done

  sudo crm_mon -1 || log_warn "クラスター状態確認失敗(両ノード起動後に再確認してください)"
}

# -----------------------------------------------------------------------------
# メイン処理
# -----------------------------------------------------------------------------
main() {
  validate_inputs

  echo ""
  log_info "=============================================="
  log_info "フェーズ4: Corosync設定"
  log_info "ノードロール : $NODE_ROLE"
  log_info "Primary IP  : $PRIMARY_IP"
  log_info "Secondary IP: $SECONDARY_IP"
  log_info "=============================================="
  echo ""

  setup_log_dir

  if [[ "$NODE_ROLE" == "primary" ]]; then
    generate_authkey
    create_corosync_conf
    distribute_authkey
    distribute_corosync_conf
    start_corosync
    start_pacemaker

    echo ""
    log_info "=============================================="
    log_info "Primary Node フェーズ4完了"
    log_info ""
    log_info "次のステップ: Secondary Nodeで以下を実行"
    log_info "  ~/phase4-corosync-setup.sh secondary \\"
    log_info "    --primary-ip $PRIMARY_IP \\"
    log_info "    --secondary-ip $SECONDARY_IP"
    log_info ""
    log_info "両ノード起動後の状態確認:"
    log_info "  sudo corosync-cfgtool -s"
    log_info "  sudo crm_mon -1"
    log_info "=============================================="

  else
    # Secondary Node: authkeyとcorosync.confはPrimary Nodeから配布済みであることを確認
    if [[ ! -f "$AUTHKEY_PATH" ]]; then
      log_error "authkeyが見つかりません。Primary Nodeでスクリプトを先に実行してください。"
      exit 1
    fi
    if [[ ! -f "$COROSYNC_CONF" ]]; then
      log_error "corosync.confが見つかりません。Primary Nodeでスクリプトを先に実行してください。"
      exit 1
    fi

    # パーミッション確認
    local perm
    perm=$(stat -c "%a" "$AUTHKEY_PATH")
    if [[ "$perm" != "400" ]]; then
      log_warn "authkeyのパーミッションを修正します: $perm -> 400"
      sudo chmod 400 "$AUTHKEY_PATH"
    fi
    log_info "authkeyパーミッション確認OK: $(stat -c '%a' $AUTHKEY_PATH)"

    start_corosync
    start_pacemaker

    echo ""
    log_info "=============================================="
    log_info "Secondary Node フェーズ4完了"
    log_info ""
    log_info "両ノードの状態確認:"
    log_info "  sudo corosync-cfgtool -s"
    log_info "  sudo crm_mon -1"
    log_info "=============================================="
  fi
}

main "$@"

phase3-pacemaker-setup.sh(Primary Nodeで実行)

#!/bin/bash
# =============================================================================
# フェーズ3: Pacemaker設定スクリプト
# nginx-ha-corosync-pacemaker
#
# 【設計方針】
#   Primary NodeでPacemakerのクラスタープロパティ、リソース、制約を設定する。
#   フェーズ4(Corosync設定)が両ノードで完了していることを前提とする。
#
# 【実行手順】
#
#   Step 1: スクリプトをPrimary Nodeへ転送(ローカルマシンで実行)
#     scp -i ~/.ssh/ha2026.pem phase5-pacemaker-setup.sh \
#       ec2-user@ec2-54-65-133-39.ap-northeast-1.compute.amazonaws.com:/home/ec2-user/
#
#   Step 2: Primary Nodeで実行(Primary Nodeのみで実行)
#     ssh -i ~/.ssh/ha2026.pem ec2-user@ec2-54-65-133-39.ap-northeast-1.compute.amazonaws.com
#     chmod +x ~/phase5-pacemaker-setup.sh
#     ~/phase5-pacemaker-setup.sh --vip 10.0.0.100
#
#   ※ VIPアドレスは同一サブネット内の未使用IPアドレスを指定すること
#
# 要件: 3.1, 3.2, 3.4, 3.5, 3.6, 3.7, 5.2
# =============================================================================

set -euo pipefail

# -----------------------------------------------------------------------------
# VIPアドレス(スクリプト先頭で定義 - 実際のIPに置き換えること)
# 同一サブネット内の未使用IPアドレスを設定する
# -----------------------------------------------------------------------------
VIP_ADDRESS="${VIP_ADDRESS:-REPLACE_WITH_ACTUAL_VIP}"

# -----------------------------------------------------------------------------
# カラー出力
# -----------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log_info()  { echo -e "${GREEN}[INFO]${NC}  $*"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }

# -----------------------------------------------------------------------------
# 引数パース
# -----------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
  case "$1" in
    --vip) VIP_ADDRESS="$2"; shift 2 ;;
    *) log_error "不明なオプション: $1"; exit 1 ;;
  esac
done

# -----------------------------------------------------------------------------
# 入力検証
# -----------------------------------------------------------------------------
validate_inputs() {
  # --vip未指定の場合は対話的に入力を促す
  if [[ "$VIP_ADDRESS" == "REPLACE_WITH_ACTUAL_VIP" || -z "$VIP_ADDRESS" ]]; then
    echo ""
    echo "VIPアドレスを入力してください。"
    echo "  - 同一サブネット内の未使用IPアドレスを指定すること"
    echo "  - 例: 10.0.0.100"
    echo ""
    read -rp "VIPアドレス: " VIP_ADDRESS
    if [[ -z "$VIP_ADDRESS" ]]; then
      log_error "VIPアドレスが入力されませんでした"
      exit 1
    fi
  fi

  # IPアドレス形式の簡易チェック
  if ! echo "$VIP_ADDRESS" | grep -qE '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'; then
    log_error "VIPアドレスの形式が正しくありません: $VIP_ADDRESS"
    exit 1
  fi

  # Pacemakerが起動しているか確認
  if ! sudo systemctl is-active --quiet pacemaker; then
    log_error "Pacemakerが起動していません。フェーズ4を先に完了してください。"
    exit 1
  fi

  # Corosyncが起動しているか確認
  if ! sudo systemctl is-active --quiet corosync; then
    log_error "Corosyncが起動していません。フェーズ4を先に完了してください。"
    exit 1
  fi
}

# -----------------------------------------------------------------------------
# タスク 6.1: クラスタープロパティの設定
# 要件: 5.2
# -----------------------------------------------------------------------------
configure_cluster_properties() {
  log_info "=== タスク 6.1: クラスタープロパティを設定 ==="

  # STONITHを無効化(AWS環境ではSTONITH代替設定)
  log_info "STONITHを無効化します..."
  pcs property set stonith-enabled=false

  # 2ノード構成用クォーラムポリシー設定
  log_info "no-quorum-policyをignoreに設定します..."
  pcs property set no-quorum-policy=ignore

  # スティッキー動作の設定(要件 5.2: 自動フェイルバック禁止)
  log_info "default-resource-stickiness=100 を設定します(スティッキー動作)..."
  pcs resource defaults update resource-stickiness=100

  log_info "クラスタープロパティ設定完了"

  # 設定確認
  log_info "--- クラスタープロパティ確認 ---"
  pcs property config | grep -E "stonith-enabled|no-quorum-policy" || true
  pcs resource defaults || true
}

# -----------------------------------------------------------------------------
# タスク 6.2: VirtualIPリソースの作成
# 要件: 3.2
# -----------------------------------------------------------------------------
create_virtualip_resource() {
  log_info "=== タスク 6.2: VirtualIPリソースを作成 ==="
  log_info "VIPアドレス: $VIP_ADDRESS"

  # 既存のVirtualIPリソースが存在する場合は削除して再作成
  if pcs resource config VirtualIP &>/dev/null 2>&1; then
    log_warn "既存のVirtualIPリソースを削除して再作成します..."
    pcs resource delete VirtualIP
  fi

  # VirtualIPリソースの作成
  pcs resource create VirtualIP ocf:heartbeat:awsvip \
    secondary_private_ip="$VIP_ADDRESS" \
    auth_type="role" \
    region="ap-northeast-1" \
    op monitor interval="10s" timeout="30s" \
    op start timeout="30s" \
    op stop timeout="30s"

  log_info "VirtualIPリソース作成完了"

  # 設定確認
  log_info "--- VirtualIPリソース確認 ---"
  pcs resource show VirtualIP || pcs resource config VirtualIP
}

# -----------------------------------------------------------------------------
# タスク 6.3: Nginxリソースの作成
# 要件: 3.1, 3.6, 3.7
# -----------------------------------------------------------------------------
create_nginx_resource() {
  log_info "=== タスク 6.3: Nginxリソースを作成 ==="

  # 既存のNginxリソースが存在する場合は削除して再作成
  if pcs resource config Nginx &>/dev/null 2>&1; then
    log_warn "既存のNginxリソースを削除して再作成します..."
    pcs resource delete Nginx
  fi

  # Nginxリソースの作成(要件 3.1, 3.6, 3.7)
  pcs resource create Nginx ocf:heartbeat:nginx \
    configfile="/etc/nginx/nginx.conf" \
    status10url="http://localhost/health" \
    op start timeout="60s" \
    op stop timeout="60s" \
    op monitor interval="10s" timeout="30s" \
    meta migration-threshold="3" failure-timeout="120s"

  log_info "Nginxリソース作成完了"

  # 設定確認
  log_info "--- Nginxリソース確認 ---"
  pcs resource show Nginx || pcs resource config Nginx
}

# -----------------------------------------------------------------------------
# タスク 6.4: コロケーション制約と順序制約の設定
# 要件: 3.4, 3.5
# -----------------------------------------------------------------------------
configure_constraints() {
  log_info "=== タスク 6.4: コロケーション制約と順序制約を設定 ==="

  # 既存の制約を削除して再作成
  pcs constraint colocation remove Nginx VirtualIP 2>/dev/null || true
  pcs constraint order remove VirtualIP Nginx 2>/dev/null || true

  # コロケーション制約(要件 3.4): NginxとVirtualIPを同一ノードで稼働
  log_info "コロケーション制約を設定します..."
  pcs constraint colocation add Nginx with VirtualIP INFINITY

  # 順序制約(要件 3.5): VirtualIP → Nginx の順で起動
  log_info "順序制約を設定します..."
  pcs constraint order VirtualIP then Nginx

  log_info "制約設定完了"

  # 設定確認
  log_info "--- 制約確認 ---"
  pcs constraint config
}

# -----------------------------------------------------------------------------
# 全体設定の確認
# -----------------------------------------------------------------------------
verify_configuration() {
  log_info "=== 全体設定の確認 ==="

  log_info "--- pcs status ---"
  pcs status || true

  echo ""
  log_info "--- 設定検証チェック ---"

  if pcs property config | grep -q "stonith-enabled=false"; then
    log_info "✓ stonith-enabled=false 設定済み"
  else
    log_warn "✗ stonith-enabled=false が設定されていません"
  fi

  if pcs property config | grep -q "no-quorum-policy=ignore"; then
    log_info "✓ no-quorum-policy=ignore 設定済み"
  else
    log_warn "✗ no-quorum-policy=ignore が設定されていません"
  fi

  if pcs resource show VirtualIP &>/dev/null 2>&1 || pcs resource config VirtualIP &>/dev/null 2>&1; then
    log_info "✓ VirtualIPリソース設定済み (VIP: $VIP_ADDRESS)"
  else
    log_warn "✗ VirtualIPリソースが設定されていません"
  fi

  if pcs resource show Nginx &>/dev/null 2>&1 || pcs resource config Nginx &>/dev/null 2>&1; then
    log_info "✓ Nginxリソース設定済み"
  else
    log_warn "✗ Nginxリソースが設定されていません"
  fi

  log_info "--- 制約一覧 ---"
  pcs constraint config
}

# -----------------------------------------------------------------------------
# メイン処理
# -----------------------------------------------------------------------------
main() {
  validate_inputs

  echo ""
  log_info "=============================================="
  log_info "フェーズ5: Pacemaker設定"
  log_info "VIPアドレス: $VIP_ADDRESS"
  log_info "=============================================="
  echo ""

  # タスク 6.1: クラスタープロパティの設定
  configure_cluster_properties
  echo ""

  # タスク 6.2: VirtualIPリソースの作成
  create_virtualip_resource
  echo ""

  # タスク 6.3: Nginxリソースの作成
  create_nginx_resource
  echo ""

  # タスク 6.4: コロケーション制約と順序制約の設定
  configure_constraints
  echo ""

  # 全体設定の確認
  verify_configuration

  echo ""
  log_info "=============================================="
  log_info "フェーズ5: Pacemaker設定完了"
  log_info ""
  log_info "次のステップ: フェーズ6(Nginx設定)へ進む"
  log_info "  ~/phase6-nginx-setup.sh"
  log_info ""
  log_info "クラスター状態の継続監視:"
  log_info "  sudo pcs status"
  log_info ""
  log_info "プロパティテスト(タスク 6.5, 6.6)の確認コマンド:"
  log_info "  sudo pcs constraint config | grep colocation"
  log_info "  sudo pcs constraint config | grep order"
  log_info "  sudo pcs resource config Nginx | grep migration-threshold"
  log_info "  sudo pcs resource defaults"
  log_info "=============================================="
}

main "$@"

start-aider.sh

#!/bin/bash
# Aider起動スクリプト for nginx-ha-corosync-pacemaker
# Amazon Bedrock経由でClaudeを利用
# EC2環境: IAMロールで自動認証(APIキー不要)
# オンプレ環境: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY を事前に設定すること
#
# [モデル選択の注意]
# - anthropic.claude-sonnet-4-6 はオンデマンドスループット非対応(エラーになる)
# - anthropic.claude-3-5-sonnet-20241022-v2:0 も同様にオンデマンドスループット非対応
# - クロスリージョン推論プロファイル apac.anthropic.claude-3-5-sonnet-20241022-v2:0 を使用すること
#
# [boto3について]
# aider-chat v0.82.3 には boto3 が内包されていないため別途インストールが必要:
#   pip3 install boto3 --no-deps
#   pip3 install botocore --no-deps
#   pip3 install s3transfer --no-deps
# (urllib3競合を避けるため --no-deps オプションを使用すること)
#
# [AWS認証について]
# aws sts get-caller-identity は環境によって動作しないため IMDSv2 で認証確認を行う

set -e

REQUIREMENTS_FILE="/home/ec2-user/nginx-ha-requirements.md"
MODEL="bedrock/apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
AWS_REGION="${AWS_REGION:-ap-northeast-1}"

if [ ! -f "${REQUIREMENTS_FILE}" ]; then
  echo "ERROR: 要件定義書が見つかりません: ${REQUIREMENTS_FILE}"
  exit 1
fi

# AWS認証確認(IMDSv2でIAMロール確認、awscliに依存しない)
_check_auth() {
  if [ -n "${AWS_ACCESS_KEY_ID}" ]; then
    return 0
  fi
  TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
    -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" 2>/dev/null)
  if [ -n "${TOKEN}" ]; then
    ROLE=$(curl -s -H "X-aws-ec2-metadata-token: ${TOKEN}" \
      "http://169.254.169.254/latest/meta-data/iam/security-credentials/" 2>/dev/null)
    if [ -n "${ROLE}" ]; then
      return 0
    fi
  fi
  return 1
}

if ! _check_auth; then
  echo "ERROR: AWS認証情報が設定されていません。"
  echo "EC2環境: IAMロールに bedrock:InvokeModel 権限を付与してください"
  echo "オンプレ環境: 以下を設定してください:"
  echo "  export AWS_ACCESS_KEY_ID='your-access-key'"
  echo "  export AWS_SECRET_ACCESS_KEY='your-secret-key'"
  echo "  export AWS_REGION=ap-northeast-1"
  exit 1
fi

echo "Aiderを起動します(Bedrock経由、要件定義書をコンテキストとして読み込み)..."
echo "モデル: ${MODEL}"
echo "リージョン: ${AWS_REGION}"
echo "編集対象: /etc/corosync/corosync.conf, /etc/nginx/nginx.conf"

AWS_REGION="${AWS_REGION}" aider \
  --model "${MODEL}" \
  --read "${REQUIREMENTS_FILE}" \
  /etc/corosync/corosync.conf \
  /etc/nginx/nginx.conf

ha-evidence-test.sh

#!/bin/bash
# HA動作確認エビデンス取得スクリプト
# 使用方法: bash ha-evidence-test.sh
# 前提: node2でリソースが稼働中の状態で実行

TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
LOG="ha-evidence-${TIMESTAMP}.log"
VIP="10.0.0.100"
TIMEOUT=30  # 要件: 30秒以内

echo "=== HA動作確認エビデンス ===" | tee $LOG
date | tee -a $LOG

echo -e "\n--- [1] フェイルオーバー前: クラスター状態 ---" | tee -a $LOG
sudo pcs status | tee -a $LOG

echo -e "\n--- [2] フェイルオーバー前: VIPへのHTTPアクセス ---" | tee -a $LOG
curl -v http://${VIP}/health 2>&1 | tee -a $LOG

echo -e "\n--- [3] 障害シミュレート ---" | tee -a $LOG
echo "アクティブノード(node2)で以下を実行してください:" | tee -a $LOG
echo "  sudo systemctl stop corosync" | tee -a $LOG
read -p "実行後にEnterを押してください..."

# ★ Enterを押した瞬間のタイムスタンプを記録
FAILOVER_START=$(date '+%Y-%m-%d %H:%M:%S')
FAILOVER_START_EPOCH=$(date +%s)
echo "フェイルオーバー開始時刻: ${FAILOVER_START}" | tee -a $LOG

echo -e "\n--- [4] フェイルオーバー監視: VIPへのポーリング(最大${TIMEOUT}秒) ---" | tee -a $LOG

FAILOVER_DONE=false
for i in $(seq 1 $TIMEOUT); do
  RESULT=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 1 --max-time 1 http://${VIP}/health)
  NOW=$(date '+%H:%M:%S')
  ELAPSED=$(($(date +%s) - FAILOVER_START_EPOCH))

  if [ "$RESULT" = "200" ]; then
    echo "[${NOW}] +${ELAPSED}秒: HTTP ${RESULT} ← フェイルオーバー完了" | tee -a $LOG
    FAILOVER_DONE=true
    FAILOVER_ELAPSED=$ELAPSED
    break
  else
    echo "[${NOW}] +${ELAPSED}秒: HTTP ${RESULT} (待機中...)" | tee -a $LOG
  fi
  sleep 1
done

echo -e "\n--- [5] フェイルオーバー後: クラスター状態 ---" | tee -a $LOG
sudo pcs status | tee -a $LOG

echo -e "\n--- [6] フェイルオーバー後: VIPへのHTTPアクセス ---" | tee -a $LOG
curl -v http://${VIP}/health 2>&1 | tee -a $LOG

echo -e "\n--- [7] 結果サマリー ---" | tee -a $LOG
if [ "$FAILOVER_DONE" = "true" ]; then
  echo "✓ フェイルオーバー完了: ${FAILOVER_ELAPSED}秒(要件: ${TIMEOUT}秒以内)" | tee -a $LOG
  if [ $FAILOVER_ELAPSED -le $TIMEOUT ]; then
    echo "✓ 要件達成" | tee -a $LOG
  else
    echo "✗ 要件未達(${FAILOVER_ELAPSED}秒 > ${TIMEOUT}秒)" | tee -a $LOG
  fi
else
  echo "✗ ${TIMEOUT}秒以内にフェイルオーバー完了せず(要件未達)" | tee -a $LOG
fi

echo -e "\n=== 確認完了: ${LOG} に保存されました ===" | tee -a $LOG

/etc/corosync/corosync.conf

# Corosync設定ファイル
# クラスター名: nginx-ha
# 生成日時: 2026-02-26 04:03:57
#
# 要件:
#   2.1 - 有効な設定ファイル
#   2.2 - UDPユニキャスト(EC2環境ではマルチキャスト不可)
#   2.3 - トークンタイムアウト 3000ms(上限5000ms)

totem {
  version: 2
  cluster_name: nginx-ha
  transport: udpu
  token: 3000
  join: 60
  consensus: 3600
  max_messages: 20
}

nodelist {
  node {
    ring0_addr: 10.0.0.182
    name: node1
    nodeid: 1
  }
  node {
    ring0_addr: 10.0.5.29
    name: node2
    nodeid: 2
  }
}

quorum {
  provider: corosync_votequorum
  two_node: 1
}

logging {
  to_logfile: yes
  logfile: /var/log/cluster/corosync.log
  to_syslog: yes
}

/etc/nginx/nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    server {
        listen 80;
        server_name _;

        location /health {
            return 200 "OK\n";
            add_header Content-Type text/plain;
        }

        location / {
            root /usr/share/nginx/html;
            index index.html;
        }
    }
}


[Bonus3] PostgreSQL HA特有の複雑さ

Nginxと違い、PostgreSQLはステートフルなのでHA構成が格段に複雑になる。

  • Primary/Standbyのレプリケーション設定(postgresql.conf、pg_hba.conf、recovery.conf)
  • PostgreSQL 12以降は recovery.conf が廃止され、postgresql.confprimary_conninfo 等を記述し、standby.signal ファイルを配置する方式に変更
  • F/O時にStandbyをPrimaryに昇格させる処理(pg_ctl promote
  • 旧Primaryが復旧した時にSplitBrain(両方がPrimaryになる)を防ぐフェンシング設定
  • データの巻き戻し(旧PrimaryをStandbyとして再参加させる pg_rewind
  • Pacemakerのリソースエージェントが pgsqlms(旧pgsql)で、パラメータがとても多い

💡SplitBrainが起きた時に両方が書き込みを続けるとデータが壊れる。DBに限らずNFSサーバーや分散ファイルシステムでも同じ問題がある。

STONITHが実質必須。 AWS環境だとSTONITHの代替実装も必要で、さらに複雑になる。




以上の内容はhttps://tech.enechange.co.jp/entry/2026/03/13/115502より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14