CloudWatch Logsから、LmabdaでログをS3にエクスポートする。対象のロググループとバケット内の第一階層を引数で指定するようにした。今回の事例ではエクスポートの範囲は「前日0時〜実行当日の0時」となる。
参考
boto3 API Reference
LambdaよりCloudWatchログをS3に保存方法紹介
ちなみに過去マネコンから実行する手順書いた。
CloudWatchLogsからS3へログをエクスポートする
今回の検証に使用したアイテム(個人メモ)
アイテム | 名称 |
---|---|
Lambda用IAMロール | lambda_basic_execution |
Lambda関数 | log-export-function |
S3バケット | log-export-xxxxxxxx |
Lambda用IAMロールの権限はlogsフルアクセスのみ。S3もいると思ってたがなくてもできた。バケットポリシー側で許可しているからか。S3バケット名は環境変数で指定した。ちなみに対象バケットは、動作不可になるためオブジェクトロックを設定しないこと。
S3バケットポリシー(log-export-xxxxxxxx)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "logs.ap-northeast-1.amazonaws.com"
},
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::log-export-xxxxxxxx"
},
{
"Effect": "Allow",
"Principal": {
"Service": "logs.ap-northeast-1.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::log-export-xxxxxxxx/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control"
}
}
}
]
}
Lambdaコード(Python3.9)
import boto3
import collections
from datetime import datetime, date, time, timedelta
import os
def lambda_handler(event, context):
log_g = event.get('loggrp')
key1 = event.get('key_name')
# 日付情報取得
yesterday = datetime.combine(date.today()-timedelta(1),time())
today = datetime.combine(date.today(),time())
unix_start = datetime(1970,1,1)
# 日付範囲指定。範囲は実行時点を基準としたUNIXタイムスタンプの日本時間(+9h)
from_t = int((yesterday-unix_start).total_seconds() * 1000)
to_t = int((today-unix_start).total_seconds() * 1000)
# S3 bucket + prefix定義(bucket/key1/YYYY/mmDD)
bucket = os.environ['S3_BUCKET']
key2 = yesterday.strftime("%Y")
key3 = yesterday.strftime("%m-%d")
s3_key = key1 + "/" + key2 + "/" + key3
try:
logs = boto3.client('logs')
response = logs.create_export_task(
logGroupName = log_g,
fromTime = from_t,
to = to_t,
destination = bucket,
destinationPrefix = s3_key
)
except Exception as e:
print(e)
では、Lambda関数をCLIから実行してみる。ま、普通はEventBridge経由とかでやると思うけど。(でもEventBridgeって引数指定できるんかな?)
--payload
オプションのロググループ名、S3バケットの最初の階層となるサービス種別(ここではEC2)を指定している。これはJSONなので、file://parameter.jsonなどとしてファイル指定でもよい。
$ aws lambda invoke --function-name log-export-function \
--payload '{ "loggrp" : "/ec2/var/log/messages","key_name" : "EC2" }' \
--cli-binary-format raw-in-base64-out \
response.json
実行すると、Prefixはできていたが中身は書き込みテスト用ファイルしかない。このコード上の仕様は、前日の00:00:00 から当日の00:00:00までをエクスポートの対象としている。指定したロググループのログは、実行した時点で定義された日付範囲に含まれていないためである。
(つまり、12/18の日中にインスタンスを起動してログが送信されたとする。しかしエクスポート対象は前日の2021-12-17 00:00:00 〜 当日の2021-12-18 00:00:00までとなるため、この期間内にログがなければ対象は存在しないことになる)
ちなみに関数が実行されるEC2のタイムゾーンはUTC。Lamabaの環境変数でタイムゾーンをJSTにするのは非推奨らしいので、コード側で制御するのが望ましい、とのこと。タイムゾーンの問題を考え始めるといつも頭の中がジグソーパズル状態になる。
Lambda のタイムゾーンを環境変数TZで指定してはいけないっていう話
AWS Lambda でのタイムゾーン変換
「コード内での時間は常に UTC で扱い、表示する段階でローカル時間に変換する」を意識していればいいかなと思います。
これを当てはめると、「コード内の基準はUTCとし、JSTとして処理する必要になった時点でJSTに変換する」と考えればいいか。
ところでPythonでのタイムゾーン変換で検索すると大抵上記リンクと同様にpytzを使ったコードが紹介されている。pytzを使えば簡単なのだが、それができない事情があったから以下のように別の方法でやった。今回のコードではまた別の方法になっているが…、どこまでも執拗に追ってくるしつこい敵なので、一度タイムゾーン処理単体で記事を書こうかと思う。
CloudWatchアラーム + SNSからのメール本文をカスタマイズする(3)
話がそれたが、翌日再試行して、今度は無事エクスポートが実行された。今回のコード上の指定では以下スクショの階層でログがアップロードされる。(長いランダム文字列のプレフィックス配下に複数のログストリームのプレフィックス、最後に圧縮ログファイル)
しかし、引数にLambda自体のログを指定するとエクスポートされない。なぜなんだ〜!…と思ったら、数時間後にリトライしたら期待値になった。試しにひとつログをDLして中身を覗いてみる。
$ gunzip -c 000000.gz
2021-12-18T09:30:45.236Z START RequestId: 4880df55-4576-4ccd-8edc-daecb18b0686 Version: $LATEST
このログは、マネコンの画面上ではJST(実際にLambdaを実行した時刻の日本時間)となっている。
2021-12-18T18:30:45.236+09:00 #Timestamp列の値
START RequestId: 4880df55-4576-4ccd-8edc-daecb18b0686 Version: $LATEST
うーん、これでいいのか?コード内でJSTに変換しているけど、逆にしなくていいのか?脳内パズル。
…だが、Lambda関数が実行されるUTCのTZから、エクスポート対象のログ範囲をUTCのTZで指定すると、実際にログが吐き出されたJSTの時刻とは異なる日付範囲になる。だから関数でJSTに変換するのは正しい、と捉えてよい気がする。