Lambda + S3 で、CloudWatch Logsのログをログローテーションする

#🔷 はじめに
AWSの各種プロダクトのログは、CloudWatchに出力する事ができます。
そして、CloudWatchに出力したログは、手動でS3にエクスポートする事もできます。

しかし、**S3に自動でログローテーションする機能はありません。**（2018年12月時点）

そこで、今回は、ログローテーションをLambdaで実現したいと思います。


#🔷 作業手順
今回は、CloudWatchのログを日次処理でS3にエクスポートする処理を行う、Lambda関数を作成します。
作業手順は大きく3つになります。
1. S3のバケットを作成する。
2. CloudWatch LogsのログをS3にエクスポートする処理を行う、Lambda関数を作成する。
3. CloudWatch Eventsで、Lambda関数が日次処理で実行するように設定する。

#🔷 実装
##🔶 S3のバケット作成

S3は「バケット」と呼ばれる空間にデータを格納します。
まずは、ログの格納先となるバケットを作成します。

Amazon S3
https://s3.console.aws.amazon.com/s3/home

**1. S3の[バケットを作成する] ボタンをクリックします。**
<img width="1115" alt="S0.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/0d2c59f3-6933-e636-acd0-cffaf02a6a16.png">　　
必要項目を入力します。
<img width="1104" alt="s00.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/e11aecbe-00a7-baf0-8b66-eb46dc34f293.png">　　
⬛バケット名
・任意のバケット名を入力します。
・ただし、この名称がそのままURLなるので、他と重複しない名前にする必要があります。
・今回は、「test-logs-2-0-1-8」とします。
⬛リージョン
・CloudWatchのログと同じリージョンを選択する必要があります。
・今回は`米国東部（バージニア北部）`を選択します。

**2. バケットが作成されたら、バケットに対する権限を設定します。**
<img width="1112" alt="s01.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/d36183aa-b1a8-9db8-c195-f67c5f6f527f.png">　　
対象バケットを開き、[アクセス権限]→[バケットポリシー]をクリックします。
<img width="1114" alt="S02.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/66860639-b8ec-4b2d-3fac-46637551df16.png">　　
バケットポリシーはJSON形式で記述します。
以下の内容を貼り付けます。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "s3:GetBucketAcl",
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::test-logs-2-0-1-8",
            "Principal": { "Service": "logs.us-east-1.amazonaws.com" }
        },
        {
            "Action": "s3:PutObject" ,
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::test-logs-2-0-1-8/log-okiba/*",
            "Condition": { "StringEquals": { "s3:x-amz-acl": "bucket-owner-full-control" } },
            "Principal": { "Service": "logs.us-east-1.amazonaws.com" }
        }
    ]
}
```
簡単に説明すると、バケットに以下の権限を付与しています。

- 「test-logs-2-0-1-8」バケットにアクセスする権限（s3:GetBucketAcl）
- 「test-logs-2-0-1-8」バケットの「log-okiba」プレフィックス（フォルダ）に、ファイルを書き込む権限（s3:PutObject）
- バケット利用対象サービスは、CloudWatch Logs　　

**3. 作成したバケットの設定が上手くいっているか、実際にログをエクスポートして確認します。**
CloudWatch Logsでロググループを選択して、[アクション]→[データをS3にエクスポート]をクリックします。
<img width="1289" alt="S03.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/3b733dae-efb9-56e4-409b-b5f87c9ca6fa.png">


エクスポートする範囲と、エクスポート先を設定します。
<img width="456" alt="S04.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/b30d517c-f7ce-1b8c-6c96-3e92356a1e2b.png">

⬛開始日
・エクスポートしたいログの範囲の、開始日を設定します。
⬛終了日
・エクスポートしたいログの範囲の、終了日を設定します。
⬛バケット名
・エクスポート先のバケット名を設定します。
・今回は作成した「test-logs-2-0-1-8」バケットを設定します。
⬛バケットプレフィックス
・エクスポート先のバケットの、プレフィックス名を設定します。
・今回は権限設定で`arn:aws:s3:::test-logs-2-0-1-8/log-okiba/*`と設定したので、「log-okiba」とします。
・もし`arn:aws:s3:::test-logs-2-0-1-8/*`と設定していたら、記載しなくても大丈夫でした。　　
入力したら[エクスポートの開始]ボタンをクリックします。

**4. S3で「test-logs-2-0-1-8」バケットの中を確認します。**
「log-okiba」プレフィックスが作られています。さらにその中にはログがエクスポートされています。
<img width="1164" alt="S05.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/90e8f82f-c5ed-c384-5fe3-bf686787dbc4.png">
確認できたら、バケットの中身は削除しましょう。

**5. ログの保持期間の設定。**
ログローテーションで毎日ログをエクスポートするので、ログの保持期間は短くします。
CloudWatch Logsで、対象のロググループのイベント失効期間をクリックします。
<img width="1173" alt="S06.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/1e06a326-7f84-9fc6-1438-18a965833e05.png">
保持期間を変更します。
<img width="436" alt="S07.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/82e6dcf3-3d59-50c0-2161-00c7dfc248eb.png">
⬛保持期間
・任意の保持期間を選択します。
・今回は毎日エクスポートするので、`3日間`にします。

[OK]ボタンをクリックしたら、CloudWatch Logsの設定は完了です。

##🔶 Lambda関数の作成

先程、CloudWatchのログをS3にエクスポートする処理を手動で行いましたが、同じ事を自動で行うLambda関数を作成します。

AWS Lambda
https://console.aws.amazon.com/lambda/home

**１． [関数の作成]ボタンをクリックして、作成を開始します。**
<img width="1157" alt="L00.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/73831669-48a2-5b5a-20ff-817b56ce6540.png">

**2． 今回は、「一から作成」でLambda関数を作成します。**
<img width="1164" alt="L01.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/30443250-c03e-782a-0386-9cd193761bef.png">
⬛名前
・任意の名称を入力します。
・今回は「exportLogTest」とします。
⬛ランタイム
・今回は`Node.js　8.10`を選択します。
<img width="1164" alt="L02.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/be9bb80d-c73d-c2f8-7cb6-cd05f2f3d83b.png">
⬛ロール
・LambdaにS3やCloudWatchを操作する権限を設定します。
・そのような権限のみが設定されたロールは都合よく無いので、今回はロールを作成します。
・`カスタムロールの作成`を選択します。

`カスタムロールの作成`を選択すると、ロール作成画面が開きます。
<img width="1162" alt="L02_iam01.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/737db7e5-ac78-44f9-5727-f15633938ac2.png">
⬛IAMロール
・`新しいIAMロールの作成`を選択します。
⬛ロール名
・任意の名称を入力します。
・今回は「exportLogTestRole」とします。

権限の内容はポリシードキュメントにJSON形式で書きます。編集リンクをクリックします。

<img width="1161" alt="L02_iam02.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/494e1d7e-1feb-0951-4685-83d18e2a722a.png">

デフォルトは、以下のようにLambdaからCloudWatch Logsにログを出力するための、3つの権限のみが設定されています。

* logs:CreateLogGroup
* logs:CreateLogStream
* logs:PutLogEvents

ここに、以下のようなログをエクスポートする権限と、S3バケットにアクセスして、書き込む権限を追加します。

* logs:CreateExportTask
* s3:GetBucketAcl
* s3:PutObject

よってポリシードキュメントは以下のように編集します。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:CreateExportTask",
                "s3:GetBucketAcl",
                "s3:PutObject"
            ],
            "Resource": "*"
        }
    ]
}
```
編集したら、[許可]ボタンをクリックします。

Lambda関数の作成画面に戻るので、[関数の作成]ボタンをクリックします。

<img width="1163" alt="L02_iam03.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/97215978-ab98-4342-1755-01dca25b267d.png">

**3． Lambda関数のコードを書きます。**

作成したLambda関数に、CloudWatch LogsとS3への権限が設定されていれば下のように、表示されます。
<img width="1162" alt="L03.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/ea20646c-0571-af74-ea03-a67848a8f240.png">
index.jsに、ログをバケットにエクスポートする処理を書きます。

<img width="1161" alt="L04.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/3744e2ae-2f5c-a017-29f2-0e0e688deef2.png">
以下の内容を貼り付けるだけでOKです。

```js
'use strict';

const aws = require('aws-sdk');
aws.config.update({region: 'us-east-1'}); // バージニア北部にグローバルリージョンを指定

/**
 * メイン処理
 * @param event : 呼び出し元サービスから渡される値
 * @param context : AWS側の各種情報
 * @param callback : 呼び出し元サービスへ返す値
 * 
 */
exports.handler = (event, context, callback) => {
  const cloudwatchlogs = new aws.CloudWatchLogs();
  let params;
  let getToTime = [];
  getToTime = getTimeData();
  

  /* 
  "destination"で設定したS3バケットに、"destinationPrefix"で設定したフォルダを作り、
  "logGroupName"で設定したCludWatchのロググループのlogをexportする。
  exportするlogの範囲は、"from"から"to"で設定した範囲。
  */
  params = {
    'destination': process.env.BucketName,
    'from': getToTime[0],
    'to': getToTime[1],
    'logGroupName': process.env.LogGroupName,
    'destinationPrefix': `${process.env.DestinationPrefix}/${getToTime[2]}`,
    'taskName': `${process.env.TaskName}/${getToTime[2]}`
  };

  console.log(JSON.stringify(params));

  /* logのexport処理 */
  cloudwatchlogs.createExportTask(params, (err, data) => {
    let response;
    if (err) {
      console.log(err, err.stack);
      response = err.stack;
    } else {
      console.log(data);
      response = data;
    }
    callback(null, response);
  });
};


/**
 * ログ取得の開始日時、終了日時、日付フォーマットを返す。
 * ログデータは、エクスポートできるようになるまで最大 12時間かかる場合があるので、
 * 1日前のログを取得するように日時を調整。
 * @return arr : [from, to, format]
 * 
 */
function getTimeData() {
  let arr=[];
  let now = new Date();
  now.setTime(now.getTime() + 1000*60*60*9);// JSTに変換
  let yyyy,mm,dd;
  console.log(now);

  /* 開始日：fromをarrにセット */
  now.setDate(now.getDate()-2);
  now.setHours(0);
  now.setMinutes(0);
  now.setSeconds(0);
  now.setMilliseconds(0);
  arr.push(now.getTime());
  console.log(`ログ取得from：${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`);
  
  /* 終了日：toをarrにセット */
  now.setDate(now.getDate()+1);
  arr.push(now.getTime());
  console.log(`ログ取得to：${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`);

  yyyy = now.getFullYear();
  mm = ('0' + (now.getMonth() + 1)).slice(-2);
  dd = ('0' + now.getDate()).slice(-2);
  arr.push(`${yyyy}-${mm}-${dd}`);
  
  return arr;
}
```

キモとなる処理は、`createExportTask`に、`param`でまとめた`destination`や`logGroupName `等の必要パラメータを渡して、ログのエクスポートを実行しているところです。（参考：[createExportTask](https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateExportTask.html)）

`getTimeData`でログの取り込み範囲を2日前〜1日前にしていますが、これはドキュメントに、

> ログデータは、エクスポートできるようになるまで最大 12時間かかる場合があります。

と書かれているためです。（参考：[Amazon S3 へのログデータのエクスポート
](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/S3Export.html)）

`params`で渡している必要パラメータは、バケットの変更等に対応しやすいように、直接ソースに書かずに、`process.env.○○`で環境変数に設定しています。
（必要なければ、設定しないでソースの中に直接書き込んでOKです。）
<img width="1165" alt="L05.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/f72c8964-5997-735c-83b3-bffc2a136330.png">
⬛環境変数

- `BucketName` : test-logs-2-0-1-8
- `DestinationPrefix` : log-okiba
- `LogGroupName` : hello-api-logs
- `TaskName` : export_task

また、エクスポート処理は数秒では完了しない可能性が高いので、タイムアウトする時間も延長します。
<img width="1165" alt="L06.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/19de5906-274e-ef2f-ca83-2bd02b064592.png">
⬛タイムアウト
・Lambda関数を実行した時の、タイムアウトまでの時間。
・今回は1分とします。

##🔶 日次実行ルールの作成。
**1． Lambda関数を実行するトリガを設定します。**
LambdaのDesignerメニューの中から、`CloudWatch Events`をクリックします。
すると、下図のようにトリガとして`CloudWatch Events`がセットされるので、さらにセットされたアイコンをクリックします。
<img width="1165" alt="L07.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/1ea5a435-9b36-8c3a-ebc7-7eb6fe7c23d9.png">
「トリガーの設定」メニューが表示されます。
<img width="1166" alt="L08.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/47a3909d-257b-8308-e3fa-802927f420e7.png">
⬛ルール
・`新規ルールの作成`を選択します。
<img width="1168" alt="L09.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/236f7cf7-fa15-1280-79c9-abd338b18618.png">
ルールの詳細を設定します。
<img width="1168" alt="L09.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/a0b7d45e-b494-d522-073b-5997d28a101d.png">
⬛ルール名
・任意の名称をセットします。
・今回は`daily_exe_test_rule`とします。
⬛ルールの説明
・分かりやすい説明を入力します。
・今回は、毎日、日本時間の01:05にLambda関数を実行するルールを作るので、その旨を記載します。
⬛ルールタイプ
・`イベントパターン`は指定のAWSプロダクトで発生するイベントをトリガとするタイプです。
・今回は、毎日指定の時間をトリガにしたいので、`スケジュール式`を選択します。
⬛スケジュール式
・実行ルールの式をセットします。
・今回は、毎日、日本時間の01:05にLambda関数を実行するルールを作るので、`cron(5 16 * * ? *)`をセットします。（UTCの時差9時間を足した時間）
・式の書き方は[ルールのスケジュール式](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html)を参照。

最後に、[保存]ボタンをクリックして、Lambdaの設定は完了です。
<img width="1164" alt="L10.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/7195e343-63e1-3c28-402b-e172f179c8b2.png">

##🔶 動作確認
正常に処理が実行できていれば、以下のように日毎にログがエクスポートされます。
<img width="1173" alt="S08.png" src="https://qiita-image-store.s3.amazonaws.com/0/38099/0746d0dc-9647-f372-1dbe-985a9ed9cf8a.png">

##🔶 注意
手動やLambadaから、エクスポートを行った時に、「エクスポートタスク」が上がりますが、**エクスポートタスクを複数立ち上げる事はできません。**
ドキュメントにも以下のように書かれています。

>アカウントごとに、一度に 1 つのアクティブ (実行中または保留中) のエクスポートタスクがあります。この制限は変更できません。

そのため、エクスポートの自動実行を複数設定している場合、他と実行のタイミングが被らないように、注意が必要です。

#🔷 参考
AWS CLI を使用した Amazon S3 へのログデータのエクスポート
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/S3ExportTasks.html

CloudWatch Logs に蓄積したログをS3に定期的にエクスポートする
https://honeybe.hatenablog.jp/entry/2018/03/14/114635