CloudWatch LogsのロググループをStep Functionsを使用してS3へ定期的にエクスポートしてみた
CloudWatch LogsのロググループをS3へエクスポートするのにStep Functionsを使用してみました。
CloudWatch LogsからS3へログを送るにはサブスクリプションフィルターを使用してAmazon Data Firehose経由で送ることができます。
この方法だとデータ量が多い場合Amazon Data Firehoseの料金がそこそこ発生する可能性があります。
そのため今回はStep Functionsを使用して定期的にエクスポートする設定を行ってみました。
ちなみに定期的にログをS3へエクスポートする場合はサブスクリプションフィルターの使用が推奨されています。
We recommend that you don't regularly export to Amazon S3 as a way to continuously archive your logs. For that use case, we instead recommend that you use subscriptions. For more information about subscriptions, see Real-time processing of log data with subscriptions.
構成
構成は以下のようになります。
Systems Managerパラメータストアにログのエクスポート対象となるロググループ名を保存します。
EventBridge SchedulerでStep Functionsを1日1回実行します。
Step FunctionsでCreateExportTaskを実行してログのエクスポートを実行していきます。
設定
Systems Managerパラメータストア作成
CloudWatch Logsロググループ名を保存するパラメータストアを作成します。
マネジメントコンソールへサインインして以下のURLからパラメータストアの画面を開いてください。
パラメータストアの画面を開いたら「パラメータの作成」をクリックします。
クリック後、「名前」、「タイプ」、「値」を設定します。
名前は任意のもので問題無いです。
タイプは文字列のリストを選択してください。
値はCloudWatch Logsロググループ名をカンマ区切りで入力してください。
設定後、画面下にある「パラメータを作成」をクリックしてください。
Step Functions作成
パラメータストアの作成が完了したらStep Functionsを作成します。
Step Functionsの作成は以下のCloudFormationテンプレートで行います。
AWSTemplateFormatVersion: 2010-09-09
Description: StepFunctions,EventBridge
Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Environment:
Type: String
AllowedValues:
- dev
- prd
SSMParameterStoreName:
Type: String
Resources:
# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------#
EventBridgeSchedulerRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub role-${Environment}-eventbridge-scheduler-${AWS::Region}-001
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: scheduler.amazonaws.com
Action: sts:AssumeRole
EventBridgeSchedulerPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub "policy-${Environment}-eventbridge-scheduler-${AWS::Region}-001"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Resource:
- "*"
Action: "states:StartExecution"
Effect: "Allow"
Roles:
- !Ref EventBridgeSchedulerRole
StepFunctionsExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: states.amazonaws.com
Action: sts:AssumeRole
RoleName: !Sub role-${Environment}-sfn-${AWS::Region}-001
StepFunctionsExecutionPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub "policy-${Environment}-sfn-${AWS::Region}-001"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Resource:
- "*"
Action:
- "ssm:GetParameter"
- "logs:CreateExportTask"
- "logs:CancelExportTask"
- "logs:DescribeExportTasks"
- "logs:DescribeLogStreams"
- "logs:DescribeLogGroups"
Effect: "Allow"
Roles:
- !Ref StepFunctionsExecutionRole
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
S3:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub s3-${Environment}-cloudwatch-log-export-${AWS::AccountId}-${AWS::Region}-001
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
BucketKeyEnabled: false
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:GetBucketAcl"
Resource:
- !GetAtt S3.Arn
Principal:
Service: !Sub "logs.${AWS::Region}.amazonaws.com"
Condition:
StringEquals:
aws:SourceAccount:
- !Sub ${AWS::AccountId}
ArnLike:
aws:SourceArn: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*
- Effect: Allow
Action:
- "s3:PutObject"
Resource:
- !Sub ${S3.Arn}/*
Principal:
Service: !Sub "logs.${AWS::Region}.amazonaws.com"
Condition:
StringEquals:
aws:SourceAccount:
- !Sub ${AWS::AccountId}
s3:x-amz-acl: bucket-owner-full-control
ArnLike:
aws:SourceArn: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*
# ------------------------------------------------------------#
# StepFunctions
# ------------------------------------------------------------#
StepFunctions:
Type: AWS::StepFunctions::StateMachine
Properties:
DefinitionString:
!Sub
- |
{
"Comment": "State machine for log export",
"StartAt": "Timestamp",
"States": {
"Timestamp": {
"Type": "Pass",
"Next": "GetParameter",
"Assign": {
"YesterdayStartTime": "{% $toMillis($substring($fromMillis($toMillis($now()) - 86400000), 0, 10) & 'T00:00:00.000Z') %}",
"YesterdayEndTime": "{% $toMillis($substring($fromMillis($toMillis($now()) - 86400000), 0, 10) & 'T23:59:59.999Z') %}"
}
},
"GetParameter": {
"Type": "Task",
"Arguments": {
"Name": "${SSMParameterStoreName}"
},
"Resource": "arn:aws:states:::aws-sdk:ssm:getParameter",
"Next": "SplitValue",
"Output": {
"LogGroups": "{% $states.result.Parameter.Value %}"
}
},
"SplitValue": {
"Type": "Pass",
"Output": {
"LogGroups": "{% $split($states.input.LogGroups, ',') %}"
},
"Next": "Map"
},
"Map": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "CheckEmpty",
"States": {
"CheckEmpty": {
"Type": "Choice",
"Choices": [
{
"Next": "Finish",
"Condition": "{% $states.input = \"\" %}"
}
],
"Default": "CreateExportTask"
},
"Finish": {
"Type": "Succeed"
},
"CreateExportTask": {
"Type": "Task",
"Arguments": {
"Destination": "${S3}",
"DestinationPrefix": "{% $states.input %}",
"From": "{% $YesterdayStartTime %}",
"LogGroupName": "{% $states.input %}",
"To": "{% $YesterdayEndTime %}"
},
"Resource": "arn:aws:states:::aws-sdk:cloudwatchlogs:createExportTask",
"Output": {
"TaskId": "{% $states.result.TaskId %}"
},
"Next": "DescribeExportTasks"
},
"DescribeExportTasks": {
"Type": "Task",
"Arguments": {
"TaskId": "{% $states.input.TaskId %}"
},
"Resource": "arn:aws:states:::aws-sdk:cloudwatchlogs:describeExportTasks",
"Output": {
"Status": "{% $states.result.ExportTasks[*].Status.Code %}",
"TaskId": "{% $states.result.ExportTasks[*].TaskId %}"
},
"Next": "Choice"
},
"Choice": {
"Type": "Choice",
"Choices": [
{
"Condition": "{% $states.input.Status != \"COMPLETED\" %}",
"Next": "Wait"
}
],
"Default": "Finish"
},
"Wait": {
"Type": "Wait",
"Seconds": 30,
"Next": "DescribeExportTasks"
}
}
},
"Items": "{% $states.input.LogGroups %}",
"End": true,
"MaxConcurrency": 1
}
},
"QueryLanguage": "JSONata"
}
- {
SSMParameterStoreName: !Ref SSMParameterStoreName,
S3: !Ref S3
}
RoleArn: !GetAtt StepFunctionsExecutionRole.Arn
StateMachineName: !Sub "state-machine-${Environment}-log-export-${AWS::Region}-001"
# ------------------------------------------------------------#
# EventBridge
# ------------------------------------------------------------#
EventBridgeScheduler:
Type: AWS::Scheduler::Schedule
Properties:
GroupName: "default"
ScheduleExpression: cron(0 0 * * ? *)
Target:
Arn: !Ref StepFunctions
RoleArn:
!GetAtt EventBridgeSchedulerRole.Arn
State: ENABLED
FlexibleTimeWindow:
Mode: "OFF"
ScheduleExpressionTimezone: Asia/Tokyo
Name: !Sub "eventbridge-${Environment}-log-export-scheduler-${AWS::Region}-001"
上記のCloudFormationテンプレートではIAMロール、ログ出力用S3、Step Functions、EventBridge Schedulerが作成されます。
以下のAWS CLIコマンドでデプロイを行います。
aws cloudformation create-stack --stack-name スタック名 --template-body file://CloudFormationテンプレートファイル名 --capabilities CAPABILITY_NAMED_IAM --parameters ParameterKey=Environment,ParameterValue=dev ParameterKey=SSMParameterStoreName,ParameterValue=
パラメータストア名
Step Functionsの説明
Step Functionsの定義は以下のようになっています。
{
"Comment": "State machine for log export",
"StartAt": "Timestamp",
"States": {
"Timestamp": {
"Type": "Pass",
"Next": "GetParameter",
"Assign": {
"YesterdayStartTime": "{% $toMillis($substring($fromMillis($toMillis($now()) - 86400000), 0, 10) & 'T00:00:00.000Z') %}",
"YesterdayEndTime": "{% $toMillis($substring($fromMillis($toMillis($now()) - 86400000), 0, 10) & 'T23:59:59.999Z') %}"
}
},
"GetParameter": {
"Type": "Task",
"Arguments": {
"Name": "パラメータストア名"
},
"Resource": "arn:aws:states:::aws-sdk:ssm:getParameter",
"Next": "SplitValue",
"Output": {
"LogGroups": "{% $states.result.Parameter.Value %}"
}
},
"SplitValue": {
"Type": "Pass",
"Output": {
"LogGroups": "{% $split($states.input.LogGroups, ',') %}"
},
"Next": "Map"
},
"Map": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "CheckEmpty",
"States": {
"CheckEmpty": {
"Type": "Choice",
"Choices": [
{
"Next": "Finish",
"Condition": "{% $states.input = \"\" %}"
}
],
"Default": "CreateExportTask"
},
"Finish": {
"Type": "Succeed"
},
"CreateExportTask": {
"Type": "Task",
"Arguments": {
"Destination": "s3-dev-cloudwatch-log-export-アカウントID-ap-northeast-1-001",
"DestinationPrefix": "{% $states.input %}",
"From": "{% $YesterdayStartTime %}",
"LogGroupName": "{% $states.input %}",
"To": "{% $YesterdayEndTime %}"
},
"Resource": "arn:aws:states:::aws-sdk:cloudwatchlogs:createExportTask",
"Output": {
"TaskId": "{% $states.result.TaskId %}"
},
"Next": "DescribeExportTasks"
},
"DescribeExportTasks": {
"Type": "Task",
"Arguments": {
"TaskId": "{% $states.input.TaskId %}"
},
"Resource": "arn:aws:states:::aws-sdk:cloudwatchlogs:describeExportTasks",
"Output": {
"Status": "{% $states.result.ExportTasks[*].Status.Code %}",
"TaskId": "{% $states.result.ExportTasks[*].TaskId %}"
},
"Next": "Choice"
},
"Choice": {
"Type": "Choice",
"Choices": [
{
"Condition": "{% $states.input.Status != \"COMPLETED\" %}",
"Next": "Wait"
}
],
"Default": "Finish"
},
"Wait": {
"Type": "Wait",
"Seconds": 30,
"Next": "DescribeExportTasks"
}
}
},
"Items": "{% $states.input.LogGroups %}",
"End": true,
"MaxConcurrency": 1
}
},
"QueryLanguage": "JSONata"
}
UIで確認すると以下のようになります。
一番最初のTimestampというステートでCreateExportTaskで使用するタイムスタンプを作成しています。
JSONataではDate/Timeという関数で日付を操作することができます。
そのため、以下のように現在時刻 ($toMillis($now())) をミリ秒で取得して86400000 (1日) マイナスした後にミリ秒からYYYY-MM-DD形式に変換後 ($fromMillis) 文字列へ変換して10文字目まで取得します ($substring)
最後にミリ秒に再度変換します ($toMillis)
"YesterdayStartTime": "{% $toMillis($substring($fromMillis($toMillis($now()) - 86400000), 0, 10) & 'T00:00:00.000Z') %}",
"YesterdayEndTime": "{% $toMillis($substring($fromMillis($toMillis($now()) - 86400000), 0, 10) & 'T23:59:59.999Z') %}"
次のGetParameterというステートでパラメータストアからCloudWatch Logsロググループ一覧を取得します。
次のSplitValueというステートでロググループをリストにしてMapへ渡します。
Map内ではロググループのリストが空でないことの確認とCreateExportTaskを実行します。
CreateExportTaskを実行したらDescribeExportTasksを実行してステータスがCOMPLETEDになっているか確認します。
COMPLETEDになっていない場合は30秒待機して再度DescribeExportTasksを実行してステータスを確認します。
CreateExportTaskはAWSアカウント内で1つのみ実行できるのでMapの同時実行は1にしています。
動作確認
リソースを作成したら以下のURLからステートマシン一覧を表示します。
ステートマシン一覧を表示したら「state-machine-dev-log-export-ap-northeast-1-001」を選択して「実行を開始」をクリックしてください。
「入力 - オプション」はとくに指定せずに実行するとログ出力用のS3バケットにログが出力されていることが確認できます。
私の環境だとロググループを作成したばかりなので以下のようにテスト実行のファイルのみ作成されていました。
次に定期実行の動作を確認します。
EvenBridge Schedulerによって毎日0時に実行されます。
以下のようにステートマシンの詳細を確認すると毎日0時に実行されていることが確認できます。
S3バケットも確認するとログストリームごとにフォルダが作成されてログが出力されていることが確認できます。
さいごに
今回はStep Functionsを使用してCloudWatch LogsのログをS3へエクスポートしてみました。
こちらの構成はプロジェクトの要件でコスト的な観点やAmazon Data Firehoseなどを使用できない場合の選択肢として使用していただくのがよいと思います。