AWS NLBを活用した自動メンテナンスモードの実装: EC2インスタンスの停止時にSorryページを表示する仕組み

## はじめに
本記事では、AWSのNetwork Load Balancer (NLB)を使用して、2台のEC2インスタンスが稼働するサービスにおいて、両方のインスタンスが停止した際に自動的にメンテナンス用の「Sorryページ」を表示する仕組みを解説します。

また、1台でもインスタンスが復旧した際に通常サービスに戻る自動化構成についても紹介します。

このシステムは可用性とメンテナンス性を向上させるために有効ですが、現在は検証を行っていないため、今後自分の環境で実施していく予定です。

## フロー全体の流れ
### 通常時のトラフィックフロー

```css
クライアント 
  ↓
NLB 
  ↓ 
通常のターゲットグループ 
  ↓ 
稼働中のEC2インスタンス (通常サービス)
```

:::note
**クライアント → NLB**
ユーザーがサービスにアクセスすると、リクエストはNLB（Network Load Balancer）に送信されます。

**NLB → 通常時のターゲットグループ**
NLBは正常に稼働中のEC2インスタンス（通常のターゲットグループ）にトラフィックをルーティングします。

**EC2インスタンス → レスポンス**
EC2インスタンスがリクエストを処理し、ユーザーにレスポンスを返します。
:::

### EC2インスタンス停止時のトラフィックフロー

```css
クライアント 
  ↓
NLB 
  ↓ 
Lambda関数がターゲットグループをメンテナンスモードに変更 
  ↓ 
Sorryページ (メンテナンス中)
```

:::note
**EventBridgeがインスタンス停止を検知**
EventBridgeルールが2台のEC2インスタンスの停止を検知します。

**EventBridge → Lambda関数**
2台のインスタンスが停止した場合、EventBridgeがLambda関数をトリガーします。

**Lambda関数 → NLB**
Lambda関数がNLBリスナーを更新し、トラフィックをメンテナンス用のターゲットグループに切り替えます。

**NLB → Sorryページ**
NLBがメンテナンス用ターゲットグループにトラフィックをルーティングし、Sorryページをユーザーに表示します。
:::

### 復旧時のトラフィックフロー

```css
クライアント 
  ↓
NLB 
  ↓ 
Lambda関数がターゲットグループを通常モードに戻す 
  ↓ 
稼働中のEC2インスタンス (通常サービス)
```

:::note
**EventBridgeがインスタンスの復旧（running 状態）を検知**
どちらかのEC2インスタンスがrunning になると、EventBridgeルールが再度Lambda関数をトリガーします。

**Lambda関数 → NLB**
Lambda関数がNLBリスナーを通常のターゲットグループに戻します。

**NLB → 通常のターゲットグループ**
NLBが通常のターゲットグループにトラフィックをルーティングし、サービスが復旧します。
:::

## 実装手順案
### 1. EventBridgeルールの設定
ルール名を設定し、イベントパターンを次のように指定します。

**EC2インスタンス停止時のルール**
```json
{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 Instance State-change Notification"],
  "detail": {
    "state": ["stopped"],
    "instance-id": ["i-0123456789abcdef0", "i-0abcdef1234567890"]
  }
}
```

**EC2インスタンス復旧時のルール**
同様に、次のように "running" の状態を指定した新しいルールを作成します。

```json
{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 Instance State-change Notification"],
  "detail": {
    "state": ["running"],
    "instance-id": ["i-0123456789abcdef0", "i-0abcdef1234567890"]
  }
}
```

## 2. IAMロールの作成と権限付与

Lambda関数がNLBを操作するためには、必要なIAMロールを設定する必要があります。以下のような権限を付与します。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:ModifyListener",
        "elasticloadbalancing:DescribeTargetGroups"
      ],
      "Resource": "*"
    }
  ]
}
```

## 3. Lambda関数の作成
以下は、2台のインスタンスの状態を確認して、両方が停止した場合にのみNLBのターゲットグループを変更するLambda関数の例です。

**EC2インスタンス停止時**
```python
import boto3

def lambda_handler(event, context):
    ec2_client = boto3.client('ec2')
    elb_client = boto3.client('elbv2')
    
    # 監視対象のEC2インスタンスのID
    instance_ids = ['i-0123456789abcdef0', 'i-0abcdef1234567890']
    
    # インスタンスの状態を確認
    response = ec2_client.describe_instances(InstanceIds=instance_ids)
    
    stopped_instances = 0
    
    for reservation in response['Reservations']:
        for instance in reservation['Instances']:
            state = instance['State']['Name']
            if state == 'stopped':
                stopped_instances += 1
    
    # 2台とも停止している場合のみNLBのターゲットグループをメンテナンスモードに変更
    if stopped_instances == len(instance_ids):
        # NLBのターゲットグループをメンテナンス用に変更
        target_group_arn_maintenance = 'arn:aws:elasticloadbalancing:region:account-id:targetgroup/my-maintenance-tg/123456789012'
        
        # NLBのリスナーを更新
        response = elb_client.modify_listener(
            ListenerArn='arn:aws:elasticloadbalancing:region:account-id:listener/net/my-nlb-listener/123456789012',
            DefaultActions=[
                {
                    'Type': 'forward',
                    'TargetGroupArn': target_group_arn_maintenance
                }
            ]
        )
        
        return {
            'statusCode': 200,
            'body': 'Both instances are stopped. NLB listener updated to maintenance mode.'
        }
    
    return {
        'statusCode': 200,
        'body': 'Not all instances are stopped yet.'
    }
```

**EC2インスタンス復旧時**
```python
import boto3

def lambda_handler(event, context):
    elb_client = boto3.client('elbv2')
    
    # 通常のターゲットグループARN
    target_group_arn_normal = 'arn:aws:elasticloadbalancing:region:account-id:targetgroup/my-normal-tg/123456789012'
    
    # NLBのリスナーを更新して通常モードに戻す
    response = elb_client.modify_listener(
        ListenerArn='arn:aws:elasticloadbalancing:region:account-id:listener/net/my-nlb-listener/123456789012',
        DefaultActions=[
            {
                'Type': 'forward',
                'TargetGroupArn': target_group_arn_normal
            }
        ]
    )
    
    return {
        'statusCode': 200,
        'body': 'NLB listener updated to normal mode.'
    }
```

## 4. NLBターゲットグループの設定
NLBターゲットグループの設定として、通常用のターゲットグループとメンテナンス用ターゲットグループを作成します。

## 5. トリガー設定
EventBridgeルールでEC2インスタンスの状態変化をトリガーとして、Lambda関数が自動実行されるように設定を実施してください。

## まとめ
本記事では、AWSのNetwork Load Balancer (NLB)を利用し、2台のEC2インスタンスが同時に停止した際に自動的にメンテナンス用の「Sorryページ」を表示する仕組みを解説しました。

このシステムは可用性とメンテナンス性を向上させるための有効な手段ですが、現在は検証を行っておらず、今後自分の環境で実施していく予定です。

## おまけ
```python
{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 Instance State-change Notification"],
  "detail": {
    "state": ["stopped"],
    "instance-id": ["i-0123456789abcdef0", "i-0abcdef1234567890"]
  }
}
```

EventBridgeのルール自体は、個々のEC2インスタンスの状態変化に対してイベントを検知する仕組みなので、両方のインスタンスが停止した場合だけトリガーされるようにすることは直接はできません。

:::note
・どちらかのインスタンスが停止 → EventBridgeがLambda関数をトリガー

・Lambda関数が両方のインスタンスの状態を確認

・両方が停止していたら、次の処理（例：通知、ロードバランサーの変更など）を実行
:::

これにより、両方のインスタンスが停止したときにのみアクションが実行されるようになります。


## 参考記事
https://repost.aws/ja/questions/QUC_U4WD0OT9y3BzBd_E8fRg/lambda%E3%81%A7alb%E3%81%AE%E3%82%BF%E3%83%BC%E3%82%B2%E3%83%83%E3%83%88%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E5%A4%89%E6%9B%B4%E3%81%AE%E4%BB%95%E6%96%B9

https://qiita.com/naraken/items/35751991f652c077ca19

https://qiita.com/NEK0SHIMA/items/69d7c09f98f445f342af