【AWS備忘】Auto Scaling スケールイン時と定時にEC2インスタンスの任意のログをS3に保存【ライフサイクルフック】

# 目的
・Auto Scalingでスケールインするサーバーについて、直前のログを保存しておきたい
・決まった時刻にサーバーのログを保存しておきたい
・StepFunctionsを使うならなるべく1つにまとめておきたい

# 前提
メインのサーバーが１台常時起動している
メインのサーバーのAMIから起動テンプレートを作成し、0~3台で推移するAuto Scalingグループを作成している
S3作成済み
それぞれのリソースで使うIAMは良いように設定

# 概要
## 利用するサービス
・Auto Scalingのライフサイクルフックルール
・EventBridge
・StepFunctions
・Lambda
・SSMドキュメント
・S3

## 手順
### 1.Auto Scalingグループの設定
ライフサイクルフックを設定する

【設定】
ライフサイクルフック名：任意
ライフサイクル移行：インスタンス終了
ハートビートタイムアウト：任意(短すぎるとStepFunctions実行が終わらないのである程度確保)
デフォルトの結果：CONTINUEだとタイムアウト時に残りのアクションを続行する、ABANDONだと直ちにインスタンスを終了する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574230/6318e791-e59f-4926-b7fd-156b26bed3d4.png)


※追加設定として、CLIから、ライフサイクルフック発動時に通知を飛ばす設定ができるよう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574230/f7299fcb-0516-42b9-87a8-85edea7310f5.png)

### 2.SSMドキュメントの設定
S3ログ転送を行うSSMドキュメントを作成する

・ログ転送をサーバー内のシェルスクリプトで行う場合

【設定】
ターゲットタイプ：/AWS::EC2::Instance

```
schemaVersion: '2.2'
description: 'Execute XXXXX.sh on the specified instance'
mainSteps:
  - name: ExecuteScript
    action: 'aws:runShellScript'
    inputs:
      runCommand:
        - '【シェルスクリプトのディレクトリ】'

```
シェルスクリプトの中身
```
# S3バケット名を設定
S3_BUCKET="s3://【S3バケット名】"

# 現在の日時を取得
CURRENT_DATE=$(date +"%Y/%m/%d")
CURRENT_DATETIME=$(date +"%Y%m%d-%H%M%S")

# ランダム文字列を生成 (8文字)
RANDOM_STRING=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1)

# アップロードするファイルのパス
SOURCE_FILE="【アップロードするファイルのパス】"

# S3の目的地パス
DESTINATION_PATH="${S3_BUCKET}/${CURRENT_DATE}/${CURRENT_DATETIME}-${RANDOM_STRING}/"

# 変数の内容を確認
echo "SOURCE_FILE: ${SOURCE_FILE}"
echo "DESTINATION_PATH: ${DESTINATION_PATH}"

# ファイルの存在確認
if [ ! -f "${SOURCE_FILE}" ]; then
    echo "エラー: ソースファイル ${SOURCE_FILE} が見つかりません。"
    exit 1
fi

# ファイルをS3にアップロード
aws s3 cp "${SOURCE_FILE}" "${DESTINATION_PATH}$(basename ${SOURCE_FILE})"

# アップロードの成功を確認
if [ $? -eq 0 ]; then
    echo "ファイルが正常にアップロードされました: ${DESTINATION_PATH}$(basename ${SOURCE_FILE})"
else
    echo "ファイルのアップロードに失敗しました"
    exit 1
fi

```

・ログ転送をSSMドキュメント内で行う場合(参考なので↑と格納時のフォルダ命名規則が異なります)

【設定】
ターゲットタイプ：/AWS::EC2::Instance

```

{
  "schemaVersion": "2.2",
  "description": "log upload",
  "parameters": {},
  "mainSteps": [
    {
      "action": "aws:runShellScript",
      "name": "configureServer",
      "inputs": {
        "runCommand": [
          "instanceid=(`ec2-metadata -i | cut -d ' ' -f 2`)",
          "echo 'Starting log upload process'",
          "aws s3 cp 【転送したいログのディレクトリ】 s3://【S3バケット名】"
        ]
      }
    }
  ]
}
```

### 3.Lambdaの設定
EventBridgeからのライフサイクルフックイベントの場合はスケールインしたインスタンスに対して、
スケジュールされたイベントの場合、環境変数から取得したインスタンスに対してSSMドキュメントを実行するLambda

【設定】
ランタイム:Python 3.13
タイムアウト:15分
環境変数
DOCUMENT_NAME:【SSMドキュメント名】
EC2_INSTANCE_ID:【定期ログを取るメインインスタンスのID】
```
import boto3
import os

ssm = boto3.client('ssm')

def lambda_handler(event, context):
    # インスタンスIDを取得
    if 'original' in event and event['original'].get('source') == 'aws.autoscaling':
        # Auto Scalingからのイベントの場合
        if 'detail' in event['original'] and 'EC2InstanceId' in event['original']['detail']:
            EC2InstanceId = event['original']['detail']['EC2InstanceId']
        else:
            raise ValueError("Auto Scaling eventにEC2InstanceIdが見つかりません")
    elif 'original' in event and event['original'].get('source') == 'aws.events':
        # スケジュールされたイベントの場合
        EC2InstanceId = os.environ.get('EC2_INSTANCE_ID')
        if not EC2InstanceId:
            raise ValueError("環境変数にEC2InstanceIdが見つかりません")
    elif 'Payload' in event:
        # Step Functionsから直接渡された場合
        if 'EC2InstanceId' in event['Payload']:
            EC2InstanceId = event['Payload']['EC2InstanceId']
        else:
            raise ValueError("PayloadにEC2InstanceIdが見つかりません")
    else:
        raise ValueError("EC2InstanceIdが見つかりません")

    # SSMコマンド実行フラグをチェック
    try:
        if event.get('executeSSMCommand', True):  # デフォルトでTrue
            # 環境変数からDocument nameを取得
            document_name = os.environ.get('DOCUMENT_NAME')
            if not document_name:
                raise ValueError("環境変数にDOCUMENT_NAMEが見つかりません")
            
            print(f"Executing SSM command on instance: {EC2InstanceId}")
            
            response = ssm.send_command(
                InstanceIds=[EC2InstanceId],
                DocumentName=document_name
            )
            
            return {
                'CommandId': response['Command']['CommandId'],
                'EC2InstanceId': EC2InstanceId
            }
        else:
            # SSMコマンドを実行せず、インスタンスIDのみを返す
            return {
                'EC2InstanceId': EC2InstanceId
            }
    except Exception as e:
        print(f"Error occurred: {str(e)}")
        return {
            'error': 'Error',
            'message': str(e)
        }
```


### 4.StepFunctionsの設定
EventBridgeからのライフサイクルフックイベントの場合はスケールインしたインスタンスに対して、
スケジュールされたイベントの場合、Lambdaが環境変数から取得したインスタンスに対してSSMドキュメントを実行するStepFunctions

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574230/ae4f89a3-b03c-43da-be2c-82006f7da6c1.png)

```
{
  "Comment": "SSMコマンドを実行し、S3ログ転送を行うステートマシン",
  "StartAt": "DetermineInstanceId",
  "States": {
    "DetermineInstanceId": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.detail.EC2InstanceId",
          "IsPresent": true,
          "Next": "SetInstanceIdFromInput"
        }
      ],
      "Default": "SetOriginalFromScheduledEvent"
    },
    "SetInstanceIdFromInput": {
      "Type": "Pass",
      "Parameters": {
        "EC2InstanceId.$": "$.detail.EC2InstanceId",
        "original.$": "$"
      },
      "Next": "ExecuteSSMCommand"
    },
    "SetOriginalFromScheduledEvent": {
      "Type": "Pass",
      "Parameters": {
        "original.$": "$"
      },
      "Next": "GetInstanceIdFromEnvironment"
    },
    "GetInstanceIdFromEnvironment": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "【LambdaARN】",
        "Payload": {
          "executeSSMCommand": false,
          "original.$": "$.original"
        }
      },
      "ResultPath": "$.InstanceIdResult",
      "Next": "SetInstanceId"
    },
    "SetInstanceId": {
      "Type": "Pass",
      "Parameters": {
        "EC2InstanceId.$": "$.InstanceIdResult.Payload.EC2InstanceId",
        "original.$": "$.original"
      },
      "Next": "ExecuteSSMCommand"
    },
    "ExecuteSSMCommand": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "【LambdaARN】",
        "Payload": {
          "EC2InstanceId.$": "$.EC2InstanceId",
          "original.$": "$.original"
        }
      },
      "ResultPath": "$.ExecutionResult",
      "Next": "WaitForSSMExecution"
    },
    "WaitForSSMExecution": {
      "Type": "Wait",
      "Seconds": 60,
      "Next": "CheckSSMStatus"
    },
    "CheckSSMStatus": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:ssm:getCommandInvocation",
      "Parameters": {
        "CommandId.$": "$.ExecutionResult.Payload.CommandId",
        "InstanceId.$": "$.EC2InstanceId"
      },
      "ResultPath": "$.SSMStatus",
      "Next": "EvaluateStatus"
    },
    "EvaluateStatus": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.SSMStatus.Status",
          "StringEquals": "Success",
          "Next": "SuccessState"
        },
        {
          "Variable": "$.SSMStatus.Status",
          "StringEquals": "Failed",
          "Next": "RetryExecution"
        },
        {
          "Variable": "$.SSMStatus.Status",
          "StringEquals": "Cancelled",
          "Next": "RetryExecution"
        },
        {
          "Variable": "$.SSMStatus.Status",
          "StringEquals": "TimedOut",
          "Next": "RetryExecution"
        }
      ],
      "Default": "WaitForSSMExecution"
    },
    "RetryExecution": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "【LambdaARN】",
        "Payload": {
          "EC2InstanceId.$": "$.EC2InstanceId",
          "original.$": "$.original"
        }
      },
      "ResultPath": "$.ExecutionResult",
      "Next": "WaitForSSMExecution"
    },
    "SuccessState": {
      "Type": "Succeed"
    }
  },
  "TimeoutSeconds": 1800
}

```


### 5.EventBridgeの設定
目的より、以下2つのEventBridgeルールを作成する
A.スケールイン発生時に該当のインスタンスのログをS3に転送するStepFunctionsを実行する
B.定期時刻にメインのインスタンスのログをS3に転送するStepFunctionsを実行する

【A.設定】
タイプ:標準
イベントパターン
```
{
  "source": ["aws.autoscaling"],
  "detail-type": ["EC2 Instance-terminate Lifecycle Action"]
}

```

ターゲット:StepFunctionsステートマシン
ターゲット名:【作成したStepFunctions】

【B.設定】
タイプ:スケジュール済みスタンダード
イベントスケジュールCron:0 21 * * ? *(毎朝6時に実行したいので)
ターゲット:StepFunctionsステートマシン
ターゲット名:【作成したStepFunctions】

# 実行
オートスケーリンググループのキャパシティを1→0にしてみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574230/a4411d0e-ed56-4fe9-aa81-370a790327cd.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574230/7f3a109e-efe8-498d-8fda-76ba2dd7feb9.png)
※log中身省略



EventBridgeのcronを直近の時間に変更してみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574230/625b830f-8e75-466d-b55c-54f3d43ef52d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574230/d26e0246-35b8-47cb-bcb4-4e00774e840a.png)
※log中身省略




# おまけ　定期時刻にオートスケーリンググループ内のインスタンスIDをすべて取得し並列でログをS3に保存するLambdaとStepfunctions

いずれ記載