目的
・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だと直ちにインスタンスを終了する
※追加設定として、CLIから、ライフサイクルフック発動時に通知を飛ばす設定ができるよう。
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
{
"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にしてみる
※log中身省略
EventBridgeのcronを直近の時間に変更してみる
※log中身省略
おまけ 定期時刻にオートスケーリンググループ内のインスタンスIDをすべて取得し並列でログをS3に保存するLambdaとStepfunctions
いずれ記載