AWS Lambdaで不要なCloudWatch Logsのログストリームを削除する

記事タイトルとURLをコピーする

はじめに

はじめまして、技術4課の水垣です。サーバーワークスに転職して、早くも半年が過ぎました。 AWS&アプリケーション開発をガリガリと進めていけるように、もっと力をつけないとです。

今日は、カラになったCloudWatch Logsのログストリームを定期的に自動削除する仕組みを作っていきたいと思います。 ロググループに「次の期間経過後にイベントを失効」(保持設定)をしているけど、ログストリームが日にち単位で作成されていて、空のログストリームが残り続けているような環境に有効かなと思います。

動作確認はしていますが、適用する際には自身でも事前に検証をお願いします。

Amazon CloudWatch Logsの概念

AWSの公式サイトで、CloudWath Logsの用語と概念がまとめられていますので、サクッと目を通しておくと後が楽です。基本大事。

Amazon CloudWatch Logs の概念

何をしたいか

  • CloudWatch Logsにはログの保持設定機能があり、保持期間を過ぎたログは自動的に削除される
  • ただし、この削除対象となるのはログストリーム内のログイベントのみ(ログストリームは削除されない)
  • ログストリームが日にち単位等で作成されるような場合、中のログイベントは保持設定により削除されるが、ログストリームは空でも残り続ける
  • 邪魔なので、この保持期間が過ぎたログストリームを定期的に自動で削除したい

どんな仕掛けでやるか

  • 専用のサーバーは用意したくない → 起動した分だけの課金がいい → サーバーレス → AWS Lambda
  • 削除は定期的に自動でしたい → スケジュール実行で上のLambdaを発火させたい → Amazon CloudWatch Events 構成図

削除対象のログストリームを何で判断するか

  • ひとまず、取得できるログストリームの情報を実際に確認してみる
  • サクッと確認したいので、describe系のAWS CLIコマンドを探す(Python環境があるならPythonで確認もあり)
  • describe-log-streams を使って、ログストリームの情報を見てみる
$ aws logs describe-log-streams --log-group-name /aws/connect/one-lambda
{
    "logStreams": [
        {
            "logStreamName": "2020/02/25/09/stream-NpuEk_WgdNW5aqsYYF7DqA==",
            "creationTime": 1582622638973,
            "firstEventTimestamp": 1582622633017,
            "lastEventTimestamp": 1582622656471,
            "lastIngestionTime": 1582622663900,
            "uploadSequenceToken": "49602288682431489398299098678286773935908565265655327074",
            "arn": "arn:aws:logs:ap-northeast-1:<アカウントID>:log-group:/aws/connect/one-lambda:log-stream:2020/02/25/09/stream-NpuEk_WgdNW5aqsYYF7DqA==",
            "storedBytes": 2107
        },
... 上のオブジェクトを1つのログストリーム情報として、/aws/connect/one-lambdaロググループ内のログストリームがリストで出力される
    ]
}
  • 名前から推測すると、lastEventTimestamp lastIngestionTime が使えそう
    • 最後の出力 < 保持期限の日時(例えば今日より3日前とか)
  • describe-log-streams の公式ページを参照してみる
    • https://docs.aws.amazon.com/cli/latest/reference/logs/describe-log-streams.html
    • Outputの項目で、lastEventTimestamp lastIngestionTime を確認
    • lastEventTimestamp
      • 最新のログイベントの時刻。 1970年1月1日00:00:00 UTCからのミリ秒数。 取り込みから1時間以内に更新されるが、まれな状況では時間がかかる場合がある。
    • lastIngestionTime
      • 1970年1月1日00:00:00 UTCからのミリ秒数で表される取り込み時間。
    • 今回は最新のログイベント時刻 lastEventTimestampでやってみる

ログストリームの削除

Lambda

Lambda関数を作成していきます。基本設定は以下のようにしました。

  • Python 3.8
  • Lambdaロール
    • デフォルトで作成されるロールを使用
    • IAMロールの画面から、CloudWatchLogsFullAccessを追加でアタッチする
  • 環境変数
    • LOG_GROUP_NAME:削除対象のロググループ名(例:/aws/connect/input-wav)
    • RETENTION_PERIOD:保持期間(例:10)
  • タイムアウト・CPU
    • 削除する件数が多い場合は、CPUとタイムアウトを上げて調整する
import datetime
import os
import time
import logging

import boto3


LOG_GROUP_NAME = os.environ.get('LOG_GROUP_NAME')
RETENTION_PERIOD = os.environ.get('RETENTION_PERIOD')   # 保持期間

logger = logging.getLogger()
logger.setLevel(logging.INFO)

logs = boto3.client('logs')


def lambda_handler(event, context):
    # 削除基準日 = Lambdaが起動した日 - 保持期間
    # 起動日の前日を1世代として保持期間(世代)より前を削除
    # 保持期間:3 ・ 起動日:2020/3/6 ・ 削除基準日:2020/3/3 ・ 削除:2020/3/2から前

    logger.info('【Start】Logstream-Cleaner Cleaning')

    # 起動日の00:00:00のdatetimeインスタンスを作成
    startup_date = datetime.datetime.today().replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    deletion_date = startup_date - \
        datetime.timedelta(days=int(RETENTION_PERIOD))
    logger.info('削除基準日:%s', deletion_date)

    # Unixタイムスタンプに変換(lastEventTimestamp形式に合わせてミリ秒3桁までの整数化)
    global deletion_timestamp
    deletion_timestamp = int(deletion_date.timestamp() * 1000)

    # 削除対象のログストリームを取得
    target_streams = find_target_streams(next_token=None)
    logger.info('対象件数:%d', len(target_streams))

    # 削除する
    for stream in target_streams:
        # logger.info('対象:%s', stream['logStreamName'])
        logs.delete_log_stream(
            logGroupName=LOG_GROUP_NAME,
            logStreamName=stream['logStreamName']
        )

    logger.info('【Conmplete】Logstream-Cleaner Cleaning')
    return {
        'statusCode': 200
    }


def find_target_streams(next_token: str) -> list:
    """
    describe_log_streamsを利用して、ログストリームを取得する(Max50/回)。
    取得したログストリームを、lastEventTimestamp < 削除基準日タイムスタンプで対象を抽出。
    自身を再帰呼出しし、nextToken(続き)がある限り処理を継続。

    Parameters
    ----------
    next_token : str
        describe_log_streamsの結果に含まれるnextToken

    Returns
    -------
    target_streams : list
        対象のログストリーム名のリスト

    See Also
    --------
    https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/logs.html#CloudWatchLogs.Client.describe_log_streams
    """
    # 対象を抽出する
    def is_target_stream(stream):
        return stream.get('lastEventTimestamp') < deletion_timestamp

    # describe_log_streamsの制限で1回にMax50。もっとある場合はnextTokenで続きをdescribe
    if next_token is None:
        describe_response = logs.describe_log_streams(
            logGroupName=LOG_GROUP_NAME,
            orderBy='LastEventTime'
        )
    else:
        describe_response = logs.describe_log_streams(
            logGroupName=LOG_GROUP_NAME,
            orderBy='LastEventTime',
            nextToken=next_token
        )

    # describeの結果から対象のログストリームのみ抽出する(filterの戻りはiterator object)
    itr_streams = filter(is_target_stream, describe_response['logStreams'])
    target_streams = list(itr_streams)

    # 続きがないならリターン
    if 'nextToken' not in describe_response:
        return target_streams

    # API制限(1秒間に5トランザクションの回避)
    time.sleep(0.3)

    # nextTokenがあるので、自分を再帰呼出しして、続きから対象を抽出、結果をリストに追加
    target_streams.extend(find_target_streams(describe_response['nextToken']))

    return target_streams


if __name__ == "__main__":
    lambda_handler(object, object)

CloudWatch Events

  • ルール → スケジュールを作成して、定期的にLambdaを発火させたい
  • Cron式で指定できます(記載方法はAWSサイトを確認してください)
  • 今回は、「毎週 月曜日 00:00(JST)」に起動
    • スケジュールの指定は UTC なので、設定は「毎週 日曜日 15:00(UTC)」となります
    • Cron式: 0 15 ? * SUN *
  • ターゲットに作成したLambda関数を指定します CloudWatchEvents

おわりに

サクッと作れるかなと思ってましたが、describeで1回で取得できるMax50件が落とし穴でした。 ちょっと頭の中を吐き出しながら書いたので冗長な記述もありますが、やってみると色々と学ぶことが多いですね。 それでは、また。Have a nice Serverless!