Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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
    • 状態
      • 有効

参考文献

tomiyan
最近はAPI Gateway、Lambda、k8sを主にやっています
http://tomiyan.net/
kurashicom
「北欧、暮らしの道具店」を運営するクラシコムのエンジニアチーム。
https://hokuohkurashi.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away