100個あっても大丈夫!Cloud Watch Logs ロググループの保持期間をLambdaで一括変更する

ロググループの保持期間を手動で変更するのは辛いので、それを一括で変更するLambdaを作りました。ついでにCloudFormationテンプレート化もしてあります。

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

コンバンハ、千葉(幸)です。

Cloud Watch Logsのロググループには、ログの保持期間を定義する「保持設定(Retention settings)」というパラメータがあります。

保持設定は、CloudWatch Logs にログイベントを保持する期間を指定するために使用できます。期限切れのログイベントは自動的に削除されます。メトリクスフィルターと同様に、保持設定はロググループに割り当てられ、ロググループに割り当てられた保持期間はそのログストリームに適用されます。 Amazon CloudWatch Logs の概念

デフォルトでは「期限なし」が設定されているため、明示的に削除しない限りロググループ内のログは保持され続けます。S3に置いておくよりもお値段が少し張るので、きちんと必要な保持期間のみに抑えておきたいものですが、マネジメントコンソールから手動で変更していくのはちょっと面倒です。

マネジメントコンソールからできないならスクリプトを作ればいいじゃない」ということで、ちょっと楽にしてくれるLambda関数を作りました。

目次

マネジメントコンソールではロググループ保持設定の一括変更ができなくて面倒くさい!

先に、マネジメントコンソールから設定変更する時の手順を確認しておきましょう。なお、2020年3月時点で最新のデザインのコンソール画面で試しています。

グループの一覧画面からスタートです。ここで保持設定の値は見れるものの、この画面からは変更することはできません。ロググループの詳細画面に遷移します。

アクションから「保持設定の編集」を押下します。

期間を選んで、「Confirm」という文字列を入力して「Save」を押下します。保持期間を変更すると過去のログは消去されるため、うっかり消さないようにこのようなプロセスを踏んでいるようです。

これでようやくひとつ設定変更ができました。

これを繰り返すのはなかなか骨が折れますね。手動ではやりたくない作業です。

Lambdaで一括変更しよう

ということで出来上がったのがこちらのLambda関数用のコードです。 ランタイムpython3.7で動作確認済みです。

Lambdaの環境変数で、保持設定の期間を指定するようにしています。キーRetentionDaysの値として、保持期間の日数を指定してください。

import boto3
import os

Days=os.environ['RetentionDays']
# Possible values are: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653.

def lambda_handler(event, context):

    logs_client = boto3.client('logs')
    response = logs_client.describe_log_groups()

    group_list = response['logGroups']

    while 'nextToken' in response:
        response = logs_client.describe_log_groups(nextToken=response['nextToken'])
        add_groups = response['logGroups']
        group_list.extend(add_groups)

    for log_group in group_list:
        print(log_group['logGroupName'])
        result = logs_client.put_retention_policy(
            logGroupName=log_group['logGroupName'],
            retentionInDays=int(Days)
        )
        code = result['ResponseMetadata']['HTTPStatusCode']
        print('HTTPStatusCode is ' + str(code))

すべてのロググループに対して一括で同じ保持設定を指定するようになっています。ロググループごとに保持設定を打ち分けしたい場合は、カスタマイズが必要です。(本稿の対象外です。)

nextTokenを意識する

上記のコードの中身は至ってシンプルなのですが、一点考慮すべき存在として、nextTokenの存在があります。

保持設定を変更する対象のロググループをリスト化するために先頭でdescribe_log_groups()を実行しているのですが、ここで取得できるグループ数の上限は50になっています。

Valid Range: Minimum value of 1. Maximum value of 50.

API_DescribeLogGroups_RequestSyntax

対象に51個以上のロググループがあった場合、レスポンスの中に含まれるnextTokenを指定して再度describe_log_groups()することで前回の続きから取得できるようになっています。

レスポンスにおいてnextTokenが含まれる場合は、以下のような形式で渡されます。値には、ロググループ名と同一の文字列がセットされています。

{
    'logGroups': [
        {
            'logGroupName': 'string',
            'creationTime': 123,
            'retentionInDays': 123,
            'metricFilterCount': 123,
            'arn': 'string',
            'storedBytes': 123,
            'kmsKeyId': 'string'
        },
    ],
    'nextToken': 'string'
}

CloudWatchLogs.Client.describe_log_groups

すべてのロググループをdescribeした場合には、レスポンスにはキーnextToken自体が存在しません。辞書型のレスポンスの中からキーnextTokenの有無を確認し、存在する限り繰り返し実行するという処理を以下の部分で実現しています。

    while 'nextToken' in response:
        response = logs_client.describe_log_groups(nextToken=response['nextToken'])
        add_groups = response['logGroups']
        group_list.extend(add_groups)

動作確認

きちんと意図通りに動くかを確認しておきましょう。

以下のようなコードをLambdaで実行し、Cloud Watch Logsグループを101個作成します。

import boto3

def lambda_handler(event, context):

    logs_client = boto3.client('logs')
    
    num = 0
    while num < 100:
        responce = logs_client.create_log_group(
            logGroupName='DummyLogGroup-' + str(num)
            )
        num += 1

1回のdescribe_log_groupsで取得できるロググループ数が50であり、nextTokenを取得する処理が2回入るので、これをうまく回せれば問題ないでしょう。

上記のコードを実行して作成されたロググループがこちら。保持設定はいずれもデフォルトの「失効しない」になっています。

一括変更Lambdaを実行すると以下の通り。すべてのロググループの保持設定が、Lambdaの環境変数で指定した日数になっています。

一括変更Lambdaの実行ログは以下のような形に。ロググループの名称とHTTPステータスコードを出力するようにしていますが、不要であればコードからprint()部を削ってください。

手元の環境では、ロググループ106個に対して実行して、使用メモリ78MB、実行時間およそ23秒でした。

CloudFormationもあるよ!

上記のコードを載せたLambda関数を作成するCloudFormationのコードを作成しました。1から作るよりはだいぶ楽かと思いますので、ぜひご活用ください。

CloudFormationスタックのパラメータとして渡すようにしている値は以下の通りです。

論理ID 概要
LambdaMemorySize Lambda関数の割り当てメモリ(MB)
LambdaTimeout Lambda関数のタイムアウト(秒)
LogGroupPutRetentionDays ロググループの保持設定(日間)

特記事項としては以下のあたりです。

  • VPC外Lambdaとして作成される
  • Lambdaに割り当てるロールには、AWS管理ポリシーCloudWatchLogsFullAccessをアタッチするようにしている
  • 同時実行数の予約なし
AWSTemplateFormatVersion: '2010-09-09'
Description: "Create Lambda"
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
        Parameters:
          - LambdaMemorySize
          - LambdaTimeout
          - LogGroupPutRetentionDays

Parameters:
  LambdaMemorySize:
    Description: 128 ~ 3008. The value must be a multiple of 64.
    Type: String
    Default: 128
  LambdaTimeout:
    Description: The maximum allowed value is 900 seconds.
    Type: String
    Default: 60
  LogGroupPutRetentionDays:
    Description: Possible values are 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653.
    Type: String
    Default: 14

Resources:
# ------------------------------------------------------------#
# IAM Role
# ------------------------------------------------------------#
  LamnbdaFunctionRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "Lambda-LogGroupPutRetentionPolicy-Role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "lambda.amazonaws.com"
            Action: "sts:AssumeRole"
      Path: "/"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess


# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
  LamnbdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      FunctionName: "LogGroupPutRetentionPolicy"
      Handler: "index.lambda_handler"
      Role: !GetAtt [ LamnbdaFunctionRole, Arn ]
      Environment:
        Variables:
         RetentionDays : !Ref LogGroupPutRetentionDays
      MemorySize: !Ref LambdaMemorySize
      Timeout: !Ref LambdaTimeout
      Code:
        ZipFile: |
          import boto3
          import os

          Days=os.environ['RetentionDays']
          # Possible values are: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653.

          def lambda_handler(event, context):

              logs_client = boto3.client('logs')
              response = logs_client.describe_log_groups()

              group_list = response['logGroups']

              while 'nextToken' in response:
                  response = logs_client.describe_log_groups(nextToken=response['nextToken'])
                  add_groups = response['logGroups']
                  group_list.extend(add_groups)

              for log_group in group_list:
                  print(log_group['logGroupName'])
                  result = logs_client.put_retention_policy(
                      logGroupName=log_group['logGroupName'],
                      retentionInDays=int(Days)
                  )
                  code = result['ResponseMetadata']['HTTPStatusCode']
                  print('HTTPStatusCode is ' + str(code))
      Runtime: "python3.7"
# ------------------------------------------------------------#
# Output
# ------------------------------------------------------------#

CloudFormationをカスタマイズしたい場合は、以下を参考にどうぞ。

AWS::Lambda::Function

終わりに

マネジメントコンソールから一括で変更できてもおかしくないと思うのですが、現状は対応していません。一手間必要ですが、なるべく楽な方法で対応しましょう。

RDSやLambdaのログなど、気づけばロググループが増えていることもあるので、定期的に一括変更Lambdaを実行して保持設定をコントロールするのもよいでしょう。

今回のコード作成時には以下の記事を参考にさせていただきました。本稿のコードではすべてのロググループに同じ保持設定を割り当てるような仕様になっていますが、ロググループのプレフィックスごとに保持設定を変えたい、という場合には以下の記事が参考になります。

以上、千葉(幸)がお送りしました。