Application Load BalancerのTarget GroupにHealthy Hostが1台もなければ、独自設定のSorry Pageを出す

# 概要
<strong>Application Load Balancerのリスナールールに設定されたTarget Gropuにて、Healthy Hostが1台もなかった場合に、独自設定のSorry Pageを表示させる。</strong>
例えば、AutoScaling GroupをApplication Load BalancerのTarget Groupに設定した場合、Health Checkで失敗したInstanceはAuto Scalingグループから外れ、代わりに「起動設定」に従って新規にインスタンスが起動される。この時、仮にすべてのインスタンスが同時に外れた場合などを想定している。
また、Auto Scaling Groupを利用せず直接Target Groupにインスタンスを登録した時、全ホストがUnhealthyになった場合は、Application Load BalancerはUnhealthy Hostへルーティングを行うため、不適切なページが表示される可能性がある[^1]。

<strong>ここで、Healthy Hostが1台もなかった場合に、独自設定のSorry Pageを表示させる。</strong>問題発生時に、デフォルトで表示されるエラーページ「503 Service Temporarily Unavailable」以外のページを表示させたり、UnHealthy Hostからの返答の代わりに独自設定のSorry Pageを表示させる。

![Qiita用の図表 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/929461/1e34e797-6fd3-1531-8618-3b2aff63da1b.png)

[^1]: [参考ページ](https://dev.classmethod.jp/articles/tsnote-alb-targetgroup-unhealthy-002/)

# 検証手順

## 1. 検証用のEC2インスタンスの構築
EC2インスタンスを起動し、ルートURL(/)に対してHello Worldページを表示するEC2インスタンスを構築する。

```bash:コマンド
# httpdのインストール
sudo yum update -y
sudo yum install -y httpd

# httpdの起動
sudo systemctl start httpd

# hello worldページの設定
cd /var/www/html/
sudo vim index.html
# -------- vim start --------
<!DOCTYPE html>
<html>
<head>
    <title>Hello World!</title>
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>
# -------- vim end----------
```

## 2. Target Groupおよび、Application Load Balancerのルールを作成する
### 2.1 Target Groupの作成
「1. 検証用のEC2インスタンスの構築」で作成したEC2をTargetsに登録したTarget Groupを作成する。Target GroupのPortはHTTP(80)に設定する。

### 2.2 Application Load Balancerのリスナールールの作成
Application Load Balancerにて、通常表示用のルールとSorry Page表示用のルールを作成する。通常表示用のルールにはTarget Group(alb-verification-ec2)を紐づける。Sorry Page表示用のルールでは固定レスポンスを設定する。

```html:固定レスポンス設定例
<!DOCTYPE html>
<html>
<head>
    <title>Sorry Page</title>
</head>
<body>
    <h1>Sorry.... This service is down</h1>
</body>
</html>
```

## 2. Application Load Balancerのルールを切り替えるLambda関数を実装する
### 2.1 Lambda関数の実装
以下の方針でLambda関数を実装する。

1. CloudWatchの対応するアラーム[^2]を確認。すでにSorry Pageが表示されている場合などでは、処理を中断する。
[^2]: 「3.Lamnbda関数とCloud Watchイベントを紐づける」にて設定
2. Application Load Balancerのルールのプライオリティを入れ替える。


```python:Lambda関数
import json
import boto3


def lambda_handler(event, context):
    # [Variable] alb arn
    alb_arn = {Application Load BalancerのARN}

    # [Variable]listner arn
    alb_listner_arn = {Application Load BalancerのリスナーのARN}

    # this is the rule arns
    ## [Variable]target group
    target_group_rule_arn = {Application Load Balancerのリスナーの通常表示用のルールのARN}
    ## [Variable]ALB error page
    sorry_rule_arn = {Application Load BalancerのリスナーのARN}

    # 1. CloudWatchの対応するアラームを確認。すでにSorry Pageが表示されている場合などでは、処理を中断する。
    cloudwatch = boto3.resource('cloudwatch')
    # [Variable]Cloud Watch Alarm
    alarm = cloudwatch.Alarm({Target GroupのHealthy Host Countを監視している、Cloud Watch Alarmの名称})
    print(alarm)

    client = boto3.client('elbv2')
    current_rule_arn = client.describe_rules(ListenerArn=alb_listner_arn)['Rules'][0]['RuleArn']
    print(current_rule_arn)

    # Check if it has already been set
    if alarm.state_value == "ALARM" and current_rule_arn == sorry_rule_arn:
        return {
            'statusCode': 200,
            'body': 'Sorry page is already displayed'
        }
    elif alarm.state_value == "OK" and current_rule_arn == target_group_rule_arn:
        return {
            'statusCode': 200,
            'body': 'Hello World page is already displayed'
        }

    # 2. Application Load Balancerのルールのプライオリティを入れ替える。
    target_group_rule_priority = 1
    sorry_rule_priority_rule_priority = 2
    if current_rule_arn == target_group_rule_arn:
        target_group_rule_priority = 2
        sorry_rule_priority_rule_priority = 1

    response = client.set_rule_priorities(
        RulePriorities=[
            {
                'RuleArn': target_group_rule_arn,
                'Priority': target_group_rule_priority
            },
            {
                'RuleArn': sorry_rule_arn,
                'Priority': sorry_rule_priority_rule_priority
            },
        ])

    print(response)

    return {
        'statusCode': 200,
        'body': json.dumps(response)
    }
```

### 2.2 Lambda関数に必要な権限を設定する
以下の権限をLambdaが使用するIAM Roleに設定する
+ cloudwatch:DescribeAlarms : Cloud Watch Alarmの状態(OK/Alarm)を取得する
+ elasticloadbalancing:DescribeRules : Application Load Balancerのリスナールールのプライオリティを取得する
+ elasticloadbalancing:SetRulePriorities : Application Load Balancerのリスナールールのプライオリティを更新する

```json:IAM_POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "cloudwatch:DescribeAlarms",
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:SetRulePriorities"
            ],
            "Resource": "*"
        }
    ]
}
```

## 3.Lamnbda関数とCloud Watchイベントを紐づける
### 3.1 Application Load BalancerのHealthy Host Countが0の時にアラートを出す、Cloud Watch Alarmを作成する。

以下の設定でCloud Wach Alarmを作成する。Target GroupにTargetsが1台も登録されていない場合、「HealthyHostCount」はデータ不足となるため、「欠落データを不正 (しきい値を超えている)として処理」する。
+ 対象メトリクス : ApplicationELB > AppELB別 > TG別メトリクス > {対象のTarget Group の「HealthyHostCount」メトリクス
+ 統計 : 最小
+ 期間 : 1分
+ 欠落データの扱い : 欠落データを不正 (しきい値を超えている)として処理

### 3.2 Cloud Watch Event Ruleを作成する
作成したCloud Watch Alarmの状態が変更した際に、Lambd関数を実行するEvent Ruleを作成する。Cloud Watch Alarmの状態変更に対して2つのRule(Alarm→OK, OK→Alarm)を作成する。2つのルールどちらに対しても「2. Application Load Balancerのルールを切り替えるLambda関数を実装する」で作成したLambda関数を関連付ける。

```json:Alarm→OK
{
  "source": [
    "aws.cloudwatch"
  ],
  "detail-type": [
    "CloudWatch Alarm State Change"
  ],
  "detail": {
    "alarmName": [
      {3.1で作成したCloud Watch Alarm}
    ],
    "previousState": {
      "value": [
        "ALARM"
      ]
    },
    "state": {
      "value": [
        "OK"
      ]
    }
  }
}
```

```json:OK→Alarm
{
  "source": [
    "aws.cloudwatch"
  ],
  "detail-type": [
    "CloudWatch Alarm State Change"
  ],
  "detail": {
    "alarmName": [
      {3.1で作成したCloud Watch Alarm}
    ],
    "previousState": {
      "value": [
        "OK"
      ]
    },
    "state": {
      "value": [
        "ALARM"
      ]
    }
  }
}
```

## 4.動作検証
### 4.1 すべてのHostがUnhealthyになった場合

1. Target GroupのHealth Check ruleをルート「/」から、対応していないパス「/check_unhealthy」に変更する。 → 独自Sorry Pageが表示される。
2. Target GroupのHealth Check ruleを対応していないパス「/check_unhealthy」から、ルート「/」に変更する。 → Hello Worldページが表示される。

### 4.2 Target GroupのTargetsにHostがなくなった場合

1. Target GroupのTargetsから今回作成したEC2を解除する。 → 独自Sorry Pageが表示される。
2. Target GroupのTargetsから今回作成したEC2を再登録する。 → Hello Worldページが表示される。