QUICKGUARD ホームページ >

Step Functions と Lambda で Cloudwatch Logs を S3 へ転送

2019.01.25

前回のエンジニアブログ AWS Step Functions では、Step Functions と Lambda を組み合わせて Cloudwatch Logs を 定期的に S3 へ保存する方法について、主に AWS Step Functions の部分をご紹介しました。今日はその続編として、AWS Step Functions から呼び出される Lambda 関数を見てみましょう。


処理の全体像については、前回の記事をご参照ください。

事前準備

事前準備として、Lambda 関数 に Cloudwatch Logs を実行する権限を確認しておきましょう。
ログを作成する権限の他に、エクスポートタスクを参照する権限(DescribeExportTasks)が必要な事に注意してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "logs:CreateExportTask",
                "logs:CreateLogStream",
                "logs:CreateLogGroup",
                "logs:PutLogEvents",
                "logs:DescribeExportTasks"
            ],
            "Resource": "*"
        }
    ]
}

Lambda関数

AWS Step Functions から呼び出される Lambda関数は2種類です。

  • export_log_to_s3:Cloudwatch Logs を S3 へ送る エクスポートタスクを作成する
  • check_export_task:エクスポートタスクが完了したかチェックする

エクスポートタスクを作成する

まずは、ログのエクスポートタスクを作成する関数を見てみましょう。

Step Functionsからのパラメータは event. の部分で受け取ることができます。
エクスポートタスク作成後は、次のチェック関数へ渡すための タスクID を返します。

下記の関数は「前月分のログを抽出する」という条件になっていますが、注意が必要なのはログ抽出条件の日時を算出する部分です。
Cloudwatch Logs はUTCで管理されているので、格納されているログの時刻との差異があります。JSTで「前月の1日0:00」を指定したい場合には、9時間前の「前々月の末日15:00」時刻を設定します。

const aws = require('aws-sdk');
const cwl = new aws.CloudWatchLogs();
aws.config.update({region: 'ap-northeast-1'});

exports.handler = async function handler(event, context) {
    const s3BucketName = event.s3BucketName;
    const logGroupName = event.logGroupName;
    const s3Prefix = event.s3Prefix;
    const today = new Date();
    const from = new Date(today.getFullYear(), today.getMonth() - 1, 0, 15, 0, 0, 0);   // 前々月の末日15:00(UTC)=前月の1日0:00(JST)
    const to = new Date(today.getFullYear(), today.getMonth(), 0, 14, 59, 59, 999);     // 前月の末日14:59.59(UTC)=前月の末日23:59.59(JST)
    
    try {
        const ret = await exportTask(s3BucketName, s3Prefix, logGroupName, from, to);
        context.succeed({
            s3BucketName: s3BucketName,
            s3Prefix: s3Prefix,
            logGroupName: logGroupName,
            taskId: ret.taskId
    });
    } catch (e) {
        if (e.code == 'ResourceNotFoundException') {
            context.succeed({
            s3BucketName: s3BucketName,
            s3Prefix: s3Prefix,
            logGroupName: logGroupName,
            taskId: event.taskId
        });
        } else {
            context.fail(e);
        }
    }
};
async function exportTask(s3BucketName, s3prefix, logGroupName, from, to) {
    const date = to.toLocaleDateString('ja-JP', {year: 'numeric', month: '2-digit'});
    const destinationPrefix = date + '/' +s3prefix ;
    const params = {
        destination: s3BucketName,
        from: from.valueOf(),
        logGroupName: logGroupName,
        to: to.valueOf(),
        destinationPrefix: destinationPrefix
    };
    return await cwl.createExportTask(params).promise();
}

エクスポートタスクの完了を確認する

次はエクスポートタスクの完了をチェックする関数です。
前段の関数で返された タスクID を使って、describeExportTasks で確認できます。
リトライや Wait の処理は StepFunctions 側がやってくれるので、関数の中身はエクスポートタスクの完了・未完了を返すのみです。

const aws = require('aws-sdk');
const cwl = new aws.CloudWatchLogs();

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 側に持っているので、S3へ移動したいログが増えた場合にも 関数は変更する必要はありませんし、余計なリトライや Wait の処理もないので、Lambdaだけで作成するよりはだいぶすっきりしたと思います。