[AWS] ECSタスクとRDSを使わない時に停止させる

## 概要
- コスト削減のため、ECSやRDSを休日に停止させるコードを記述する
    - 平日に起動させる処理はまた別途作成が必要（後述の処理を流用して書き換える） 

## システム構成

* Lambda
* ECS
* RDS（RDS Proxy）
* EventBridge


## Lambdaのコード

```python:python
import boto3
import os
import logging
import time

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def retry(func, retries=3, delay=5):
    for attempt in range(retries):
        try:
            return func()
        except Exception as e:
            if attempt < retries - 1:
                logger.warning(f'Retry {attempt + 1} failed: {str(e)}. Retrying...')
                time.sleep(delay)
            else:
                logger.error(f'Failed after {retries} attempts: {str(e)}')
                raise

def stop_ecs_service(ecs_client, ecs_cluster_name, ecs_service_name):
    ecs_client.update_service(
        cluster=ecs_cluster_name,
        service=ecs_service_name,
        desiredCount=0
    )

def stop_rds_cluster(rds_client, rds_cluster_identifier):
    rds_client.stop_db_cluster(
        DBClusterIdentifier=rds_cluster_identifier
    )

def wait_for_rds_cluster_stop(rds_client, rds_cluster_identifier):
    while True:
        response = rds_client.describe_db_clusters(DBClusterIdentifier=rds_cluster_identifier)
        status = response['DBClusters'][0]['Status']
        logger.info(status)
        if status == 'stopped':
            break
        time.sleep(30)

def remove_rds_proxy_targets(rdsproxy_client, rds_proxy_name, rds_instance_identifier):
    response = rdsproxy_client.describe_db_proxies(DBProxyName=rds_proxy_name)
    logger.info(f'Describe DB Proxies response: {response}')
    if 'DBProxies' in response and len(response['DBProxies']) > 0:
        db_proxy = response['DBProxies'][0]
        logger.info(db_proxy)
        target_groups = db_proxy.get('TargetGroups', [])
        if target_groups:
            for target_group in target_groups:
                rdsproxy_client.deregister_db_proxy_targets(
                    DBProxyName=rds_proxy_name,
                    TargetGroupName=target_group['TargetGroupName'],
                    DBInstanceIdentifiers=[rds_instance_identifier]
                )
            logger.info(f'Target groups removed for DB Proxy {rds_proxy_name}')
        else:
            logger.warning(f"No target groups found for DB Proxy {rds_proxy_name}")
    else:
        logger.warning(f"No DB Proxies found with name {rds_proxy_name}")

def lambda_handler(event, context):
    ecs_client = boto3.client('ecs')
    rds_client = boto3.client('rds')
    rdsproxy_client = boto3.client('rds')

    ecs_cluster_name = os.environ['ECS_CLUSTER_NAME']
    ecs_service_name = os.environ['ECS_SERVICE_NAME']
    rds_cluster_identifier = os.environ['RDS_CLUSTER_IDENTIFIER']
    rds_instance_identifier = os.environ['RDS_INSTANCE_IDENTIFIER']
    rds_proxy_name = os.environ['RDS_PROXY_NAME']

    try:
        retry(lambda: stop_ecs_service(ecs_client, ecs_cluster_name, ecs_service_name))
        logger.info('ECSサービスを停止しました')

        retry(lambda: stop_rds_cluster(rds_client, rds_cluster_identifier))
        logger.info('RDSクラスターの停止処理が開始しました')

        wait_for_rds_cluster_stop(rds_client, rds_cluster_identifier)
        logger.info('RDSクラスターが停止しました')

        retry(lambda: remove_rds_proxy_targets(rdsproxy_client, rds_proxy_name, rds_instance_identifier))
        logger.info('RDS Proxyターゲットグループの削除処理が完了しました')

        message = 'Successfully stopped ECS, RDS, and updated RDS Proxy'
        logger.info(message)
    except Exception as e:
        message = f'Failed to stop ECS, RDS, or update RDS Proxy: {str(e)}'
        logger.error(message)
        raise e

    return {
        'statusCode': 200,
        'body': message
    }

```

:::note warn
上記のコードは同期処理のため、動かしてみて処理が15分以上かかる場合は非同期処理にする
:::

:::note success
コード上でリトライ処理を実装しているが、EventBridgeのルール作成でもリトライ処理は設定可能
:::

## 必要なこと
### IAM
- ロール作成
- ポリシー作成
  - LambdaExcecution
  - CloudWatchLog
  - RDS
  - ECS
  - RDSProxy

→作成したロールをLambdaにアタッチ

### セキュリティグループ
インバウンドルールまたはアウトバウンドルールを設定する

:::note success
RDS及びECSのインバウンドルールに、Lambdaのアクセス許可設定は不要。停止処理は管理者操作のため。
:::


## EventBridge
- cron式でスケジュール設定する（設定方法はAWS内のツールチップを参照）
- 実装したLambda関数を指定する


## おわりに
- 過去に個人で実装した経験を思い出しながら執筆しました
- このコードで問題なく動作した記憶があるので、問題ないはずです
    - 仮に動作しない場合は構成が違っている可能性があるので、コードを修正して実装してみてください

以上