目的

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

前提

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

概要

利用するサービス

・Auto Scalingのライフサイクルフックルール ・EventBridge ・StepFunctions ・Lambda ・SSMドキュメント ・S3

手順

1.Auto Scalingグループの設定

ライフサイクルフックを設定する

【設定】 ライフサイクルフック名:任意 ライフサイクル移行:インスタンス終了 ハートビートタイムアウト:任意(短すぎるとStepFunctions実行が終わらないのである程度確保) デフォルトの結果:CONTINUEだとタイムアウト時に残りのアクションを続行する、ABANDONだと直ちにインスタンスを終了する image.png

※追加設定として、CLIから、ライフサイクルフック発動時に通知を飛ばす設定ができるよう。 image.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

{
  "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 image.png ※log中身省略

EventBridgeのcronを直近の時間に変更してみる image.png image.png ※log中身省略

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

いずれ記載