0
0

More than 1 year has passed since last update.

Step Functionsを使ってCloudWatch Logsのログストリーム整理を行なってみた

Last updated at Posted at 2021-12-20

先にまとめ

  • Step Functionsを使ってCloudWatch Logsのログストリーム整理する処理を作ってみた。
  • Step FunctionsからAWSのAPIを叩くことができるので、あまりLambdaに頼らずに処理をかける。
  • 安直に処理を書くとけっこう簡単にStep Functionsの制限に引っかかる。
  • マネージメントコンソールから見ると、どういった処理なのか見通すことはできるが、CDKで記載したコード側から見ると、正直見通しがよいとは言えない。
  • ロググループの機能として保持期間の設定とかあるので、それで空になったログストリームは削除してくれる機能も提供されるといいのに
  • 無理にStep Functionsだけでワークフローを組むとコストが見合わないこともある。(2022-01-11追記)

はじめに

CloudWatch Logsって保持期間の設定があって、保持期間を経過したログ自体は削除してくれるものの、結果からになったログストリームは削除してくれません。

結果、空になったログストリームが大量に残ることになり、マネージメントコンソール等からログ確認しようとした時に、面倒になったりします。

そこで、空になったログストリームを削除することをやろうと考えました。

アプローチの検討

今回のような要件の場合、ストレートに考えると、Lambdaを定期実行して、LambdaからCloudWatchのAPIを叩く、というアプローチになると思います。

実際にこのアプローチを試みている人はいて、以下の記事でも試みられています。

私も同様の処理を書いて試してみたのですが、私の環境ではログストリームの数が多すぎたせいか、Lambdaの制限時間内で処理できず、何らかの別の方法をとる必要がありました。

単純に「Step Functionsで処理をバラしてもっと小さい単位でLambda処理かなぁ…」と思っていたところ、Step Functionsにこんなアップデートが入ったじゃないですか。

「これはLambdaなしにStep Functions Onlyでやるしかないでしょ!」

要件とか設計方針とか

要件

  • 保持期間が過ぎて空になっているログストリームを削除する。
  • この処理は1日1回自動で起動するものとする。
  • 保持期間設定されていないロググループに対しては保持期間として30日を設定する。
    • 追加要件。Lambda関数を普通に作ると、保持期間未設定のロググループが作られてしまう関係で、大量にログが残ってしまっていたので。
  • 空になっているロググループは削除する。
    • 追加要件。正直この要件を入れてもいいのか悩みましたが、Lambdaで色々作っては捨ててを繰り返している関係で、不要になったロググループが結構残っていたので、今回はこの要件を入れました。
  • 作成後、1週間を経過していないロググループは処理の対象としない。
    • 作りたてのものを対象にしてしまうと、開発中のものに干渉しちゃう可能性もあるので。

設計方針

  • 処理本体はStep Functionsで行う。
  • Lambdaは極力使わない。
  • 1日1回の実行はEventBridgeトリガーで行う。
  • 今回のスタックはCDK+TypeScriptで記述を行う。

StepFunctions.drawio.png

設計・実装

とりあえず作成したソースはこちらに置いてあります。

このコードからできたStep Functionsのワークフローはこんな感じです。

stepfunctions_graph.png

stepfunctions_graph (1).png

このワークフローの中で語っておきたいところをピックアップして書いていきます。
なお、2枚画像を貼ったことでわかるように、今回のスタック内にはステートマシンを2つ作っています。
その辺の事情も含めて書いていきます。

GetEventTime

EventBridgeからの定時実行される際にStep Functionsへ渡される入力は以下。

{
  "version": "0",
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "detail-type": "Scheduled Event",
  "source": "aws.events",
  "account": "123456789012",
  "time": "2021-12-18T00:35:51Z",
  "region": "ap-northeast-1",
  "resources": [
    "arn:aws:events:ap-northeast-1:123456789012:rule/CleanCloudwatchLogsStack-ap-northeast-Rule01234567-XXXXXXXXXXXXX"
  ],
  "detail": {}
}

ここのtimeを現時刻として使いたいと思います。
ただ、この先で使う予定のDescribeLogGroupsDescribeLogStreamsで取得できる時刻はエポックミリ秒なので、この現時刻もエポックミリ秒に変換する必要があります。

「Lambdaは極力使わない」と言っていたのに早速Lambda頼り…ではありますが、CDKには便利なものがあります。

裏ではLambdaとしてデプロイされるのですが、CDKでスタックを書く側としてはLambdaを使っていると意識する必要はありません。

使い方はこんな感じ。

const getEventTime = new tasks.EvaluateExpression(this, 'GetEventTime', {
  expression: 'Date.parse($.time)',
  resultPath: '$.eventTime',
});

変換や算術演算とか必要になる状況では同じようにEvaluateExpressionを使います。

DescribeLogGroups

ロググループの取得はDiscribeLogGroupsAPIを使用します。

これをCDK上でStep Functionsから呼び出すにはこんな感じ。

const describeLogGroups = new tasks.CallAwsService(this, 'DescribeLogGroups', {
  service: 'cloudwatchlogs',
  action: 'describeLogGroups',
  iamAction: 'logs:describeLogGroups',
  iamResources: ['*'],
  resultPath: '$.describeLogGroupOutput',
}).addRetry(retryProps);

service, actionなどは、APIのドキュメントを見て書く流れですが、「Workflow Studio」を使ってサンプルのワークフローを実際に配置しつつ調べるほうが早いかもです。
iamAction, iamResourceのところに関しても、これらの文字列からそのままIAMポリシーが作られますので、ドキュメントを見ながら書く形になると思います。
(存在しないアクションをiamActionの文字列に書いてもそのままIAMポリシーに記載されます)

addRetryのところは、APIの実行時に例外が飛んだ時のリトライ方法を記載。
スロットリング時のリトライを想定して1秒後、2秒後、4秒後…と5回リトライするように設定しました。
この辺りのリトライ処理の記述はシンプルにできていいですね。

const retryProps: sfn.RetryProps = {
  errors: ['CloudWatchLogs.CloudWatchLogsException', 'CloudWatchLogs.SdkClientException'],
  interval: cdk.Duration.seconds(1),
  maxAttempts: 5,
  backoffRate: 2,
};

なお、今回の環境では、ロググループの数が50以下のため、DescribeLogGroupsを繰り返し呼ぶ作りにはする必要ありませんでしたが、汎用的な本来的な作りであれば、DescribeLogGroupsの戻り値に含まれるNextTokenがなくなるまで繰り返し呼ぶ、というのが正解になると思います。

また、これ以降もCloudWatch APIを使っている箇所はありますが、同様にCallAwsServiceを使用しています。

LogStreamFlowMap

const logStreamsMap = new tasks.StepFunctionsStartExecution(this, 'LogStreamFlowMap', {
  stateMachine: stateMachineLogStreamFlow,
  integrationPattern: sfn.IntegrationPattern.RUN_JOB,
  resultPath: sfn.JsonPath.DISCARD,
});

2つ目のステートマシンを使って処理しています。
ステートマシンを分けた理由ですが、Step Functionsには、実行履歴のイベント上限が25,000、という制限があります。

1ステップ1イベント、とかならまだいいんですが、タスクの内容によっては、1ステップ5イベントとか履歴に記録されているので、Mapなどを使って繰り返し処理を実施してしまうと、けっこう簡単に25,000という上限に引っかかってしまいます。

これを回避するために、繰り返し呼び出されるループ箇所を別ステートマシンに分けています。

LogGroupMapLast, LogStreamMapFlowLast

命名がグダグダでしたが…
Mapの最後に状態を空オブジェクトにするステップを入れています。

const logStreamMapLast = new sfn.Pass(this, 'LogStreamsMapLast', {
  result: sfn.Result.fromObject({}),
});

Mapとしての結果はMap内で処理したすべての結果を配列にまとめたものになりますが、その際に下のリンクに記載のある「タスク、状態、実行の最大入力または出力サイズ」の上限に引っかかってしまったため、ループの最後に明確に潰すようにしました。

後から考えれば、途中で結果を捨てるとかしていけば、もっとシンプルに解決できていたかもしれないです。

コード全体を通して

Step Functionsのマネージメントコンソールからワークフローの確認はできるのでそちらからの全体把握は行いやすいのですが、CDKのコード側から見ると、正直見通しがよいコードとは言えず、どういう書き方がベストなのかわかっていません。

終わりに

今回のスタックを使って、実際にデイリーでのログストリーム整理処理を走らせています。
今はデイリーで流しているので、1時間程度で処理が終わっていますが、空のログストリームが大量に残っていた初回は数時間かかっていました。(本当に大量に残っていたので…)

せっかく保持期間設定が提供されているので、それに伴って空になってしまったログストリームを削除してくれる機能もAWS側で提供してくれればいいのになぁ…とちょっと思ったりしました。

2022-01-11追記

今月のコスト状況を見たところ、約10日でこんな感じ。
ちょっとやっている内容と費用がバランスしないですね。ちょっと構成を見直そう…

スクリーンショット 2022-01-11 21.37.18.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0