Node.js
S3
CloudWatch
lambda
stepfunctions

AWS Step FunctionsとLambdaでCloudWatch LogsのログをS3に定期的にエクスポートする

CloudWatch LogsのログをS3にエクスポートする方法としてはKinesis Firehoseなどがありますが、頻繁にエクスポートしなくても良い場合もあります。

その場合の選択肢の1つとしてStep Functionsもあるのかなと思って実装してみました。


大まかなながれ


  • CloudWatch Eventsで定期的にStep Functionsを実行

  • Step Functionsで複数のログに対してLambdaを実行

  • CloudWatch LogsのS3エクスポートタスクを実行


    • ロググループ名やバケット名をStep Functionsから渡すことによってLambdaを汎化できる



  • CloudWatch LogsのS3エクスポートタスクをポーリングする


    • 複数同時にS3へのエクスポートタスクを実行できないため



stepfunctions.png


Step Functions


おさらい


  • 視覚的なワークフローを使用して、分散アプリケーションとマイクロサービスのコンポーネントを調整できるウェブサービス

  • API Gateway、CloudWatch Eventsからキックすることができる

  • タスクとしてLambdaや(EC2、ECS)を実行する

  • Step Functions自体はサーバレス

  • リトライ制御が充実していて、失敗時に徐々に待ち時間を伸ばすことができる

  • 課金体系は状態遷移する毎に課金

  • JSONでStepを書く必要がある

  • ifとかforとかは無いので戸惑う



  • ステート間で多少のデータの受け渡しは可能


ステート

States
説明

Pass
入力や出力を上書きして次のStateを実行することができる

Task
Lambdaやアクティビティ(EC2、ECS)などを実行できる
タイムアウトやリトライを設定することができる

Choice
switch case文的なもの
条件にマッチした場合とデフォルトの次のStateを指定できる

Wait
秒数、指定したTimestampまで待つ
Inputから受け取ったJSON内の秒数、Timestampを利用することも可能

Succeed
成功して終わる

Fail
失敗して終わる

Parallel
並列にStateを実行できる
今回使っていないのでOutputがどうなるのかは未検証


前提条件


  • 2018/07/19時点での内容です

  • AWSのリージョン


    • ap-northeast-1



  • CloudWatch Logsの複数のロググループを1日1回S3へエクスポートする

  • CloudWatch Logs


    • ログがすでに出力されている


      • /test/log-group-a

      • /test/log-group-b

      • /test/log-group-c





  • 出力先のS3


    • バケットがすでに用意されている

    • stepfunction-sample



  • Lambda


    • ランタイム:Node.js8.10




手順


IAM Policy、信頼関係、Roleを作成する


LambdaがCloudWatch Logsを操作するためのRole

Policy



{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:DescribeExportTasks"
],
"Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/cwl-to-s3-check-task:*",
"arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/cwl-to-s3-create-task:*"
]
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": [
"logs:CreateExportTask"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:123456789012:log-group:/test/log-group-a*",
"arn:aws:logs:ap-northeast-1:123456789012:log-group:/test/log-group-b*",
"arn:aws:logs:ap-northeast-1:123456789012:log-group:/test/log-group-c*"
]
}
]
}





信頼関係

{

"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}





ロールARN:arn:aws:iam::123456789012:role/role-cwl-lambda


Step FunctionsがLambdaを実行するためのRole

Policy

{

"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"*"
]
}
]
}





信頼関係

{

"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "states.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}





ロールARN: arn:aws:iam::123456789012:role/role-cwl-step-function


S3のバケットポリシーを設定

バケットポリシー

{

"Version": "2012-10-17",
"Id": "PolicyID",
"Statement": [
{
"Sid": "StmtID2",
"Effect": "Allow",
"Principal": {
"Service": "logs.ap-northeast-1.amazonaws.com"
},
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::stepfunction-sample"
},
{
"Sid": "StmtID3",
"Effect": "Allow",
"Principal": {
"Service": "logs.ap-northeast-1.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::stepfunction-sample/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control"
}
}
}
]
}






Lambda関数を作成する


CloudWatch LogsからS3へのエクスポートタスクを作成する関数


  • 名前


    • cwl-to-s3-create-task



  • ロールARN


    • arn:aws:iam::123456789012:role/role-cwl-lambda



ソースコード

const AWS = require('aws-sdk');

const cwl = new AWS.CloudWatchLogs({apiVersion: '2014-03-28'});

exports.handler = async function handler(event, context) {
const today = new Date();
const from = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1, 0, 0, 0, 0);
const to = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1, 23, 59, 59, 999);
const s3BucketName = event.s3BucketName;
const logGroupName = event.logGroupName;
const destPrefix = event.destPrefix;

try {
const ret = await exportTask(s3BucketName, destPrefix, logGroupName, from, to);
context.succeed({
s3BucketName: s3BucketName,
destPrefix: destPrefix,
logGroupName: logGroupName,
taskId: ret.taskId
});
} catch (e) {
if (e.code == 'ResourceNotFoundException') {
context.succeed({
s3BucketName: s3BucketName,
destPrefix: destPrefix,
logGroupName: logGroupName,
taskId: event.taskId
});
} else {
context.fail(e);
}
}
}

async function exportTask(s3BucketName, destPrefix, logGroupName, from, to) {
const date = from.toLocaleDateString('ja-JP', {year: 'numeric', month: '2-digit', day: '2-digit'});
const destinationPrefix = destPrefix + logGroupName + '/' + date;
const params = {
destination: s3BucketName,
from: from.valueOf(),
logGroupName: logGroupName,
to: to.valueOf(),
destinationPrefix: destinationPrefix
};
return await cwl.createExportTask(params).promise();
}






CloudWatch LogsからS3へのエクスポートタスクのステータスを確認してハンドリングする


  • 名前


    • cwl-to-s3-check-task



  • ロールARN


    • arn:aws:iam::123456789012:role/role-cwl-lambda



ソースコード

const AWS = require('aws-sdk');

const cwl = new AWS.CloudWatchLogs({apiVersion: '2014-03-28'});

exports.handler = handler;

class CheckError extends Error {
constructor(name) {
super(name);
this.name = name;
}
}

async function handler(event, context) {
const s3BucketName = event.s3BucketName;
const logGroupName = event.logGroupName;
const destPrefix = event.destPrefix;
const taskId = event.taskId;
try {
const ret = await describeTask(taskId);
const retTask = ret.exportTasks[0];
const statusCode = retTask.status.code;

if (statusCode == 'COMPLETED') {
context.succeed({
s3BucketName: s3BucketName,
logGroupName: logGroupName,
destPrefix: destPrefix,
taskId: taskId,
status: retTask.status.code
});
} else {
context.fail(new CheckError(statusCode));
return;
}
} catch (e) {
context.fail(e);
}
}

async function describeTask(taskId) {
const params = {
taskId: taskId
};
return await cwl.describeExportTasks(params).promise();
}






Step Functionsを作成する


  • ステートマシンの名前


    • cwl-to-s3



  • IAMロールのARN


    • arn:aws:iam::123456789012:role/role-cwl-step-function



ステートマシンの定義

{

"StartAt": "FirstPass",
"States": {
"Succeed": {
"Type": "Succeed"
},
"Fail": {
"Type": "Fail"
},
"FirstPass": {
"Type": "Pass",
"Result": {
"logGroupName": "",
"s3BucketName": "stepfunction-sample",
"destPrefix": "path-prefix"
},
"Next": "Dispatch"
},
"Dispatch": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.logGroupName",
"StringEquals": "",
"Next": "log-group-a"
},
{
"Variable": "$.logGroupName",
"StringEquals": "/test/log-group-a",
"Next": "log-group-b"
},
{
"Variable": "$.logGroupName",
"StringEquals": "/test/log-group-b",
"Next": "log-group-c"
},
{
"Variable": "$.logGroupName",
"StringEquals": "/test/log-group-c",
"Next": "Succeed"
}
],
"Default": "Fail"
},
"log-group-a": {
"Type": "Pass",
"Result": "/test/log-group-a",
"ResultPath": "$.logGroupName",
"Next": "CwlCreateTask"
},
"log-group-b": {
"Type": "Pass",
"Result": "/test/log-group-b",
"ResultPath": "$.logGroupName",
"Next": "CwlCreateTask"
},
"log-group-c": {
"Type": "Pass",
"Result": "/test/log-group-c",
"ResultPath": "$.logGroupName",
"Next": "CwlCreateTask"
},
"CwlCreateTask": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:cwl-to-s3-create-task",
"Catch": [
{
"ErrorEquals": ["ResourceNotFoundException"],
"Next": "Dispatch"
},
{
"ErrorEquals": ["States.ALL"],
"Next": "Fail"
}
],
"Next": "CwlCheckTask"
},
"CwlCheckTask": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:cwl-to-s3-check-task",
"Retry": [
{
"ErrorEquals": [ "RUNNING", "PENDING" ],
"IntervalSeconds": 60,
"BackoffRate": 2.0,
"MaxAttempts": 3
}
],
"Catch": [
{
"ErrorEquals": [ "States.ALL" ],
"Next": "Fail"
}
],
"Next": "Dispatch"
}
}
}






CloudWatch EventsでStep Functionsを定期的にキックする


  • イベントソース:スケジュール


    • Cron式:0 1 * * ? *


      • GTM時なので注意が必要





  • ターゲット


    • Step Functions ステートマシン



  • ステートマシン


    • cwl-to-s3



  • role


    • この特定のリソースに対して新しいロールを作成する

    • arn:aws:iam::123456789012:role/service-role/role-event-cwl-to-s3



  • ルールの定義


    • 名前


      • event-rule-cwl-to-s3



    • 状態


      • 有効






参考文献