ECS上で運用中のシステム向け、メンテンナンスモード導入

## 入る前に
こんにちは:)　アプリケーションエンジニアのキムです。
今日は現在チームで運用中のECS Fargateベースのシステムに新たにメンテモードを作った経験談を残してみたいと思います。
この機能については、すでに様々な方法が存在して、たくさんいろんな方法を導入して運用してると思います。
今回案内する方法は、その中のただ一つの方法に過ぎないと思います。
このような方法を選択して運用してるところもあるんだーレベルでの受け入れでお願いします。

## 前提条件
現在、私と私たちのチームが運用中のサービスの場合、基本的にはメンテナンスモードがあえて存在する必要がない構造です。
今回このような作業を行うことになった背景は、当サービスより上位にあるサービスが一定期間ベースでメンテナンス作業を行っていますが、その際、影響を受けるいくつかの状況が発生したため、当チームでもその対応が必要となりました。
そのため、巨大なサービス、アプリケーションに比べて比較的簡単に作業できる方法を採用したことを予めお知らせします。

## システム運用状況
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76353/b59bfa88-8717-2df8-ac7a-ef5ecfdc894f.png)
現在、このように1つのALBを通じて接続経路ごとのリスナーを作成し、3分割して運用しています。
コンテナはfargateで運用中であり、ターゲットグループを指定して転送させている状況です。

## やりたかったこと
上記の現在の状況におけるメンテモードの役割は、アプリケーションへの接続を阻んでおくことが目的です。

そのため、上記のシステムは指定された時間の間は、

- フロントエンド
    - メンテナンス中の案内ページにリダイレクト
- BFF
- API
    - 503システムエラーリターン
- メンテナンス作業中に発生したアクセス確認

程度の作業を希望し、この作業はAWSLambdaとEventBridgeの設定を通じて作成することができました。

## 実行内容
上記の内容をもとに以下の構想図を作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76353/374d05f6-e313-46af-5df9-6acf80d84738.png)

イベントブリッジで時間を指定しておくと、最初に `update listner priority` Lambda関数が実行され、4番にあるリスナー情報を最優先順位に調整させます。 このリスナーの場合、すべてのリクエストを`maintenance_function`というLambda関数に送っており、この関数は内部でパスを確認し、`/a`の場合はメンテンス案内ページに、`/b、/c`の場合は503エラーを返す作業を実行します。
さぁ、登場人物はこれですべて登場しました。 整理してみると。
1. ラムダ関数作成
    - update listner priority
    - maintenance_function
3. EventBridge設定
4. その他AWS設定

### Lambda関数作成1

```python
import json
import boto3
import logging
# logger 設定
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

def lambda_handler(event, context):
    # event bridgeから呼び出す時のパラメータ取得 defaultはstart
    type = event['type']
    str = '開始'
    if type == 'end':
        str = '終了'

    logger.info( 'メンテナンス作業' + str )
    logger.info( 'ALBの切り替え作業開始' )
    client = boto3.client('elbv2')

    #リスナールールの優先順位切替。（メンテナンスページを優先して開くようにする）
    if type == 'start':
        ## 終了時にPriorityを1で設定
        responce = client.set_rule_priorities(
        RulePriorities=[
                {
                    'RuleArn': {メンテナンス用リスナのARN},
                    'Priority': 1
                }
            ]
        )
    else:
        ## 終了時にPriorityを99で設定→結果的には現リスナの中一番うしろの番号で設定される。この場合は「４」
        responce = client.set_rule_priorities(
        RulePriorities=[
                {
                    'RuleArn': {メンテナンス用リスナのARN},
                    'Priority': 99
                },
            ]
        )


    # 変更後の設定を表示
    result = client.describe_rules(
        ListenerArn={リスナんもARN},
    )
    logger.debug( 'ALBの切り替え作業完了' )

    return result
```

ALBの場合、リスナの優先順位で転送対象を分活させることができます。
パスを通じて分活をする場合、最上位権限を持っている`/*`をメンテナンス用に別途準備した後、この項目でメンテナンスが行われる時間帯の間、最も最上位に上げることで対応できます。
Event Bridgeからメンテンス`開始`、`終了`状況に合わせてパラメータを渡す方式でLambda関数を構成しました。
スタート→優先順位を1位に
終了→優先順位を最下位に（現状では99と指定しても実際は4になります )

### Lambda関数作成2
```python
import json
import logging
import re
# logger 設定
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)


# coding: UTF-8
def lambda_handler(event, context):
    # アクセスパスを記録
    logger.info('アクセスパス：　' + event['path'])
    # /a へのアクセスなのかを確認
    p = re.compile('^\/a*')
    match1 = p.match(event['path'])

    if match1 is not None:
        # /a パスへのアクセスの場合メンテナンスようページへのリダイレクト
        response = {
            "isBase64Encoded": False,
            "statusCode": 303,
            "headers": {
                "Location": "https://example.com/a/maintenance"
            },
            "multiValueHeaders": {},
        }
    else:
        # ほかへのアクセスの場合
        # 503エラーを返す
        response = {
            "statusCode": 503,
            "statusDescription": "503 Service Unavailable",
            "isBase64Encoded": False,
            "headers": {
                "Content-Type": "application/json; charset=utf-8"
            },
            "body": json.dumps({"message": "Service Unavailable"})
        }

    return response

```

該当関数を作成した後、ターゲットグループに設定しておくことで、メンテモードが実行中の時にLambda関数が呼び出され、パスを確認して上記の要件事項を満たそうとします。

加えて、ここにアクセスすることで、どのパスで接続したのかをCloudWatchにログとして残すことができるため、この条件もクリアできるようになります。

`/a`の場合、フロントエンドにあらかじめ用意されたメンテナンス用画面へのリダイレクトを`/b、/c`の場合jsonを返す処理を担当します。

フロントエンド側が実際にメンテナンス中の場合があるのではないかという質問には、現行の構造上はメンテインメント時間をあえて確保する必要がない構造であることを改めて申し上げます。

### EventBridge設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76353/fe786e09-57ee-0bcb-710b-c062eab1a005.png)
ブリッジ設定は、クローン形式で指定時間帯実行でルールを生成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76353/c3a9b3b6-06b4-686e-bcb1-381ab7aec89f.png)

基本的にUTCへの設定が必要ですが、下記の現地時間帯に確認ボタンを利用して現地時間帯がどのくらいか（東京の場合+9）を確認することができます。

そして、パラメータは以下の設定で利用できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76353/0f02c80d-8feb-ec21-f59d-b2409f8f4043.png)
私はjsonを主に活用する方です。 以下のように設定すると、上で作成したLambda関数から該当キーにアクセスして習得することができます。

### その他AWS設定
前述のALBに`maintenance_function`で配信するTargetGroupを事前に登録しておく必要があります。 `update listner priority`関数において、該当リスナーのARNが必要からです。

この程度にしておけば、上記の全ての設定を満足するでしょう。

## テスト
```json-doc
// 開始テスト
{
  "type": "start"
}

// 開始終了
{
  "type": "end"
}

```
テストは、`update listner priority` Lambda関数のテスト機能を利用して確認することができます。
テストコードは上記の2パターンを作成します。
テストボタンを押して正しくALBのリスナが更新されているかどうかの確認と、各経路への接続を確認してみましょう。

## 仕上げ
私が提案している方法は数多くの方法の中の一つに過ぎず、私もこの作業を実行する前に数多くの方法を調査しました。
ただ、私と私たちのチームが望むことを満足する方法は、この方法が最善だったので、この方法を選択して運用中です。
もちろんこの方法以外にももう少し良い方法は何でも存在すると思います。
もう少しいい方法があればコメントを通じて意見を残していただけるとありがたいです。

この記事が誰かの悩みに少しでもヒントになったら嬉しいです。=)