AWS S3 アクセスログの集約化


前提

AWSのS3のアクセスログは1回のアクセスにつき、指定したバケットに1個のアクセスログファイルを作成するとします。

感覚的には1回につき1個のファイルではなく、同時間に発行されたログを1つのファイルにまとめて吐いているようです。細かいことはAmazon S3 サーバーアクセスのログ記録を見て下さい。

つまり「1時間毎に1回、1日で計24回のアクセスがあったとすると作成されるログファイルの数は24個」としてこの記事を読んで下さい。

何がしたいか

前提で述べたように1回のアクセスで1個のログファイルが作成されるので、1000回アクセスされると1000個のログファイルが作成されてしまいます。「1日に発行されたログを全てまとめて見たい」という時に全てのログファイルをローカルにダウンロードするなんて事はしたくありません。S3の仕様上、それなりにコストがかかります。よってこれらのログファイルを1日毎にまとめてしまいます。1日毎ではなく1週間毎や1ヶ月毎にまとめてしまってもいいかもしれません。ただこの後に使用するAWSのLambdaの仕様上ファイルが大きくなりすぎると無理かもしれないので1日毎にしました。

Direction

上の図のような流れで処理を実行します。

1. 左から「User」が「Bucket A」にアクセスする(2020-09-07-14-30)。

2. 「Bucket A」はアクセスされるとログを「Bucket B」に書き込む。

3. 「Bucket B」はログを書き込まれたら「Bucket C」においてある、「2020_09_07.txt」にそのログを追記する。

これらの処理の内、1と2の処理はAmazon S3 サーバーアクセスのログ記録の設定で自動で行われるので割愛します。

前準備

3の処理をするためにLambdaより関数を作成します。今回私はNode.jsのデプロイパッケージを作成することで関数を作成しました。

Node.js の Lambda デプロイパッケージを作成するには、どうすればよいですか?

作成する際には、アクセス権限に「S3FullAccess」をもつロールを与えておいて下さい。

作成した関数の名前を「sum_logs」とします。sum_logsの設定、「トリガーを追加」より、「Bucket B」に新しいオブジェクトが作成されたらsum_logsが実行されるようにしておきます。

また「Bucket C」に「2020_09_07.txt」というファイルを準備しておいて下さい。

「Bucket B」や「Bucket C」と書いていますが実際のBucketの名前は"B"や"C"として以下のコードを扱って下さい。

index.js

var AWS = require("aws-sdk");
const moment = require("moment");
const s3 = new AWS.S3({
  region: "ap-northeast-1",
});

exports.handler = async (event, context, callback) => {
  try {
    //this means Bucket "C"
    const dest_bucket = "C"; 

    var uploaded_params = {
      // event.Records[0].s3.bucket.name means Bucket "B"
      Bucket: event.Records[0].s3.bucket.name,
      Key: event.Records[0].s3.object.key,
    };
    var uploaded_obj = await s3.getObject(uploaded_params).promise();
    var uploaded_body = uploaded_obj.Body.toString();

    var dest_params = {
      Bucket: dest_bucket,
      Key: moment().format("YYYY-MM-DD") + ".txt",
    };
    var dest_obj = await s3.getObject(dest_params).promise();
    var dest_body = dest_obj.Body.toString();
    dest_body += uploaded_body;

    var new_params = {
      Bucket: dest_bucket,
      Key: moment().format("YYYY-MM-DD") + ".txt",
      Body: dest_body,
    };
    var put_obj = await s3.putObject(new_params).promise();

    // delete uploaded logs in Bucket "B"
    var deleted = await s3.deleteObject(uploaded_params).promise();

    return;
  } catch (e) {
    console.log(e);
    return;
  }
};

解説

内容は大体読めば分かるでしょう。
今回デプロイパッケージにしたのはMoment.jsを使用したかったからです。個人的には扱いやすいのでおすすめします。

注意点とまとめ

今回、日付毎にファイルをまとめていますが、そのベースとなるファイル(今回は2020_09_07.txt)は前もって特定のバケット(今回はBucket "C")に作成しておく必要があります。

このベースになるファイルも手動で作成するなんて馬鹿らしいことはしないので、無論自動で行わせるわけですが疲れたので気が向けば書きます。処理は同様にLambdaを使用しますが、トリガーはEventBridgeを使用します。とっても簡単です。

コストについて

「Logが吐かれる度にS3にファイルをPutしてたら、データ転送料は相当かかるんじゃないの?」とか思われるかもしれませんが、僕はAWS Lambda 料金に書かれている
同じ AWS リージョン内における Amazon S3、Amazon Glacier、Amazon DynamoDB、Amazon SES、Amazon SQS、Amazon Kinesis、Amazon ECR、Amazon SNS、Amazon EFS、または Amazon SimpleDB と AWS Lambda 関数の間でのデータ転送は無料です。

という文章を信じています。

参考

S3
Amazon S3 サーバーアクセスのログ記録
Lambda
EventBridge
AWS Lambda 料金
mermaid.js