CloudWatch LogsのロググループをStep Functionsを使用してS3へ定期的にエクスポートしてみた

CloudWatch LogsのロググループをStep Functionsを使用してS3へ定期的にエクスポートしてみた

2025.07.24

CloudWatch LogsのロググループをS3へエクスポートするのにStep Functionsを使用してみました。

CloudWatch LogsからS3へログを送るにはサブスクリプションフィルターを使用してAmazon Data Firehose経由で送ることができます。
この方法だとデータ量が多い場合Amazon Data Firehoseの料金がそこそこ発生する可能性があります。
https://dev.classmethod.jp/articles/cloudwatch-logs-to-s3-via-kinesis-data-firehose/
https://dev.classmethod.jp/articles/comparison-of-fees-for-cloudwatch-logs-and-s3/#toc-s3-

そのため今回はStep Functionsを使用して定期的にエクスポートする設定を行ってみました。
ちなみに定期的にログをS3へエクスポートする場合はサブスクリプションフィルターの使用が推奨されています。
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatchLogs/latest/APIReference/API_CreateExportTask.html

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.

構成

構成は以下のようになります。
ログエクスポート_202507221712

Systems Managerパラメータストアにログのエクスポート対象となるロググループ名を保存します。
EventBridge SchedulerでStep Functionsを1日1回実行します。
Step FunctionsでCreateExportTaskを実行してログのエクスポートを実行していきます。

設定

Systems Managerパラメータストア作成

CloudWatch Logsロググループ名を保存するパラメータストアを作成します。
マネジメントコンソールへサインインして以下のURLからパラメータストアの画面を開いてください。
https://ap-northeast-1.console.aws.amazon.com/systems-manager/parameters/?region=ap-northeast-1&tab=Table

パラメータストアの画面を開いたら「パラメータの作成」をクリックします。
クリック後、「名前」、「タイプ」、「値」を設定します。
名前は任意のもので問題無いです。
タイプは文字列のリストを選択してください。
値はCloudWatch Logsロググループ名をカンマ区切りで入力してください。

設定後、画面下にある「パラメータを作成」をクリックしてください。
スクリーンショット 2025-07-22 165952

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で確認すると以下のようになります。
スクリーンショット 2025-07-22 170947

一番最初の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からステートマシン一覧を表示します。
https://ap-northeast-1.console.aws.amazon.com/states/home?region=ap-northeast-1#/statemachines

ステートマシン一覧を表示したら「state-machine-dev-log-export-ap-northeast-1-001」を選択して「実行を開始」をクリックしてください。
「入力 - オプション」はとくに指定せずに実行するとログ出力用のS3バケットにログが出力されていることが確認できます。
私の環境だとロググループを作成したばかりなので以下のようにテスト実行のファイルのみ作成されていました。
スクリーンショット 2025-07-22 183546

次に定期実行の動作を確認します。
EvenBridge Schedulerによって毎日0時に実行されます。
以下のようにステートマシンの詳細を確認すると毎日0時に実行されていることが確認できます。
スクリーンショット 2025-07-24 084526

S3バケットも確認するとログストリームごとにフォルダが作成されてログが出力されていることが確認できます。
スクリーンショット 2025-07-24 084738

さいごに

今回はStep Functionsを使用してCloudWatch LogsのログをS3へエクスポートしてみました。
こちらの構成はプロジェクトの要件でコスト的な観点やAmazon Data Firehoseなどを使用できない場合の選択肢として使用していただくのがよいと思います。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.