はじめに
Amazon CloudWatch Logs って、知らない間に溜まって料金を圧迫したりしますよね。ロググループを自動で作成するサービスもありますし、保持期間の見直しは定期的に行いたいものです。
こういったケースの打開策のひとつに、AWS CDK の利用が挙げられます。CDK の様々な L2 コンストラクトではプロパティひとつでロググループの保持期間を設定できます。これを実現するために、裏で自動でカスタムリソースを作成してくれているのです。この抽象度の高さが CDK の売りでもあります。
個別の対策としては、このように AWS CDK の導入が有効だと思います。では、すでに大量に存在する雑多なロググループはどうすればいいでしょうか?
前置きが長くなりましたが、この問題を解決するために OSS ツールを作って公開しました!!
どんなツールか
わざわざツールを使って管理するので、使いやすくなければなりません。最初に以下の方針を決め、これらを軸に開発しました。
- オプトインが不要なリージョンすべてをデフォルトで対象にする
- リージョン横断で、保持期間の変更やロググループの削除を一括で適用する
- 高速な処理のためにリージョンごとに並行で動作させる
- 式評価を用いた柔軟なフィルタリング機能を実装する
- 多用なユースケースに対応するために、アウトプット形式も多様にする
- 削減量のシミュレーションを実装する
- パッケージ部分と CLI 部分を分離し、直接パッケージを利用できるようにする
- ついでにインストールが楽
リージョン横断で高速に一括処理できるのがベースラインで、そこに付加価値としてフィルタリング、アウトプット形式の多様さ、そして削減量シミュレーションが乗っかるイメージです。言語は以下の理由から Go を選定しました。
- 並行処理がしやすい
- ランタイムを内包したバイナリを簡単に生成できる
- 単に慣れている
インストール
Mac であれば homebrew でインストールできます。
brew install nekrassov01/tap/llcm
Windows, Linux の場合はリリースページからバイナリをダウンロードし、パスの通ったディレクトリに放り込んでください。
コマンド
llcm には 3 つのコマンドがあります。基本的には list
や preview
で対象を絞り込み、その後 apply
でロググループの削除や保持期間の変更を適用するような設計になっています。
コマンド | 機能 |
---|---|
list |
リージョン横断でロググループを一覧化します。デフォルトは ASCII テーブル形式です。DescribeLogGroups API の返り値を見やすく加工して出力します。 |
preview |
リージョン横断でロググループを一覧化しますが、--desired オプションで希望する状態を渡すことで、その状態を適用した場合の削減量をシミュレーションします。これは日割りのログ量を基準とした単純な計算結果です。 |
apply |
--desired で設定した状態を実際に適用します。保持期間の変更やロググループの削除が一括で行われます。 |
オプション
オプションは以下の通りです。各オプションがどのような値を期待しているかを示しています。
オプション | 紐づくコマンド | 値 | デフォルト値 | 環境変数 |
---|---|---|---|---|
–profile value -p value |
list preview apply |
任意 | – | AWS_PROFILE |
–log-level value -l value |
list preview apply |
debug info warn error |
info |
LLCM_LOG_LEVEL |
–region v1,v2 -r v1,v2 |
list preview apply |
af-south-1 ap-east-1 ap-northeast-1 ap-northeast-2 ap-northeast-3 ap-south-1 ap-south-2 ap-southeast-1 ap-southeast-2 ap-southeast-3 ap-southeast-4 ap-southeast-5 ap-southeast-7 ca-central-1 ca-west-1 eu-central-1 eu-central-2 eu-north-1 eu-south-1 eu-south-2 eu-west-1 eu-west-2 eu-west-3 il-central-1 me-central-1 me-south-1 mx-central-1 sa-east-1 us-east-1 us-east-2 us-west-1 us-west-2 |
オプトインなしで使えるすべてのリージョン | – |
–filter v1,v2 -f v1,v2 |
list preview apply |
key:name source class elapsed retention bytes operator: > >= < <= == ==* != !=* =~ =~* !~ !~* |
– | – |
–desired value -d value |
preview apply |
delete 1day 3days 5days 1week 2weeks 1month 2months 3months 4months 5months 6months 1year 13months 18months 2years 3years 5years 6years 7years 8years 9years 10years infinite |
– | – |
–output value -o value |
list preview |
json prettyjson text compressedtext markdown backlog tsv chart |
compressedtext |
LLCM_OUTPUT_TYPE |
–help -h |
list preview apply |
– | – | – |
–version -v |
list preview apply |
– | – | – |
使用例
ユースケースをいくつか紹介します。
ケース 1
Lambda によって生成されたロググループのうち、すべてのリージョンの、中身が空ではなく保存期間が 1 年を超える設定のロググループをリストアップし、保存期間を一律 1 年に変更した場合の削減効果をリストアップする。 出力はマークダウンの表形式とする。
この要件であれば、以下のようなコマンドになります。
llcm preview --desired 1year --filter 'name =~ ^/aws/lambda/.*','bytes != 0','retention > 1year' --output markdown
とりわけ重要なのは --filter
です。複数の式を渡すことができ、これらは AND で接続されます。以下のような意味になります。
式 | 意味 |
---|---|
name =~ ^/aws/lambda/.* |
Lambda が生成するロググループ名にマッチし、 |
bytes != 0 |
中身が空ではなく、 |
retention > 1year |
1 年を超える保持期間が設定されたロググループすべて (無制限含む) |
preview
における --desired 1year
は、上記でフィルターしたロググループの保持期間をすべて 1 年に変更した場合にどの程度ログ容量が削減されるかを示します。マークダウン形式を指定しているため、出力は以下のようになります。BytesPerDay
以降のカラムがシミュレーション結果です。
| Name | Region | Source | Class | CreatedAt | ElapsedDays | RetentionInDays | StoredBytes | BytesPerDay | DesiredState | ReductionInDays | ReducibleBytes | RemainingBytes | | -------------------- | -------------- | --------------------------- | -------- | ------------------------- | ----------- | --------------- | ------------ | ----------- | ------------ | --------------- | -------------- | -------------- | | /aws/lambda/tokyo-1 | ap-northeast-1 | 000000000000/ap-northeast-1 | STANDARD | 2019-04-15T21:50:12+09:00 | 2107 | 731 | 161094000389 | 220374829 | 365 | 366 | 80657187414 | 80436812975 | | /aws/lambda/tokyo-2 | ap-northeast-1 | 000000000000/ap-northeast-1 | STANDARD | 2020-08-26T23:45:50+09:00 | 1608 | 731 | 30273686566 | 41414071 | 365 | 366 | 15157549986 | 15116136580 | | /aws/lambda/oregon-1 | us-west-2 | 000000000000/us-west-2 | STANDARD | 2020-08-27T14:34:54+09:00 | 1607 | 731 | 28578246408 | 39094728 | 365 | 366 | 14308670448 | 14269575960 | | /aws/lambda/oregon-2 | us-west-2 | 000000000000/us-west-2 | STANDARD | 2020-08-26T23:48:51+09:00 | 1608 | 731 | 22822519036 | 31220956 | 365 | 366 | 11426869896 | 11395649140 |
この出力で問題ないことが確認できたら、--desired
と --filter
に同じ値を設定した上で apply
します。これで、対象のロググループの保持期間が一括で 1 年に設定されます。
llcm apply --desired 1year --filter 'name =~ ^/aws/lambda/.*','bytes != 0','retention > 1year'
ケース 2
東京とオレゴンのロググループのうち、中身が空で 1 年以上前に作成されたものを Backlog のテーブル形式でリストアップする。
この場合リージョンを絞りたいので、以下のように明示的に設定します。
llcm list --filter 'bytes == 0','elapsed > 365' --region ap-northeast-1,us-west-2 --output backlog
--filter
は以下のような意味になります。
式 | 意味 |
---|---|
bytes == 0 |
中身が空で、 |
elapsed > 365 |
作成から 1 年経過しているロググループすべて |
なおキーが日数を表す elapsed
または retention
である場合は、以下のように --desired
と同形式の文字列表現を渡すことができます。評価が失敗するとエラーになります。ちなみに delete
が渡された場合も失敗します。
elapsed > 1year
このツールは、Backlog 記法のテーブル形式もサポートします。弊社が Backlog のヘビーユーザーなので、実務利用のためにはこの機能が必要でした。
| Name | Region | Source | Class | CreatedAt | ElapsedDays | RetentionInDays | StoredBytes |h | test-1 | ap-northeast-1 | 000000000000/ap-northeast-1 | STANDARD | 2017-12-07T13:16:02+09:00 | 2601 | 731 | 0 | | test-2 | us-west-2 | 000000000000/us-west-2 | STANDARD | 2017-12-07T12:44:45+09:00 | 2601 | 731 | 0 | | test-3 | ap-northeast-1 | 000000000000/ap-northeast-1 | STANDARD | 2017-12-07T13:21:09+09:00 | 2601 | 731 | 0 | | test-4 | us-west-2 | 000000000000/us-west-2 | STANDARD | 2017-12-07T12:50:11+09:00 | 2601 | 731 | 0 |
上記コマンドで削除可能であると判断した場合は、以下のように --desired delete
を指定して apply
し、削除します。--filter
--region
は list
時の設定と同じにします。
llcm apply --desired delete --filter 'bytes == 0','elapsed > 365' --region ap-northeast-1,us-west-2
ケース 3
より本格的な調査のために、そのアカウントのすべてのロググループを TSV 形式でクリップボードにコピーし、スプレッドシートに貼り付ける。
TSV 形式をサポートしているので、クリップボードにコピーすればスプレッドシートや Excel に貼り付けることができます。
# macOS llcm list --output tsv | pbcopy # Windows llcm list --output tsv | Set-Clipboard
ケース 4
削減量をドキュメントにまとめるため、グラフにプロットして視覚化する。保持期間が設定されていないロググループを一律で 1 年に変更した場合の削減量をグラフに出す。
ここまではテキスト形式での出力でしたが、実は簡単な視覚化にも対応しています。Apache ECharts を Go の世界から呼び出せる go-echarts を使い、HTML 化したものをブラウザに表示します。この要件であれば以下コマンドになります。
llcm preview --desired 1year --filter 'retention == inifinite' --output chart
以下のように、削減量シミュレーションが積み上げ棒グラフにプロットされます。
一方 list
の場合、現在の容量が円グラフにプロットされます。
ざっくり把握したい場合や簡単な状況だけ伝えたい場合には、やはり視覚化が有用ですね。
スロットリングエラーの回避について
apply
で使っている API にはそれぞれ以下のクォータがあります。
API | クォータ | 引き上げ |
---|---|---|
PutRetentionPolicy | アカウントまたはリージョンごとに秒間 5 回まで | 不可 |
DeleteRetentionPolicy | アカウントまたはリージョンごとに秒間 5 回まで | 不可 |
DeleteLogGroup | アカウントまたはリージョンごとに秒間 10 回まで | 可 |
llcm は複数のロググループを並行で取り扱うので、なにも対策をしないと当然 ThrottlingException が発生して処理が中断してしまいます。このため、以下のような Retryer を実装しました。今後チューニングするかもしれませんが、現状は問題なく動作しています。
エラーメッセージに “api error ThrottlingException” が含まれる場合のみ、最大 10 回までリトライする。各リトライ前に 1-3 秒のランダムな待機時間を挟む。
AWS SDK for Go v2 におけるリトライパターンについてはこちらの記事が詳しいです。llcm でも大いに参考にさせていただきました。
Lambda で定期実行
llcm はパッケージとしても使用できます。つまり Lambda で動かせます。例えば特定のフィルターで絞り込んだロググループに対して Apply したい場合は以下のようなコードになります。
package main import ( "context" "fmt" "log" "os" "strings" "github.com/aws/aws-lambda-go/lambda" "github.com/nekrassov01/llcm" ) var client *llcm.Client func init() { cfg, err := llcm.LoadConfig(context.TODO(), "") if err != nil { log.Fatal(err) } client = llcm.NewClient(cfg) log.Println("client created in init") } func handleRequest(ctx context.Context) error { log.Println("handleRequest started") w := os.Stdout d := os.Getenv("DESIRED_STATE") desired, err := llcm.ParseDesiredState(d) if err != nil { return err } f := os.Getenv("FILTERS") filter, err := llcm.EvaluateFilter(strings.Split(f, ",")) if err != nil { return err } man := llcm.NewManager(ctx, client) if err := man.SetFilter(filter); err != nil { return err } if err := man.SetDesiredState(desired); err != nil { return err } n, err := man.Apply(w) if err != nil { return err } fmt.Fprintf(w, "done: %d\n", n) log.Println("handleRequest finished") return nil } func main() { lambda.Start(handleRequest) }
この Lambda を Amazon EventBridge Scheduler で毎月初に動作させるサンプルを AWS CDK で定義し、サンプルとしてリポジトリに配置しました。ぜひ使ってみてください。
ちなみに TypeScript での Lambda の定義は以下のように Docker イメージから作成し、引数は環境変数で渡すようにしています。冒頭で触れた通り Lambda 関数自体が生成するロググループの保持日数は logRetention
プロパティで設定できます。これが裏でカスタムリソース化されるわけです。
... const fn = new cdk.aws_lambda.DockerImageFunction(this, "Function", { description: "Set retention period for log groups with no expiration date.", code: cdk.aws_lambda.DockerImageCode.fromImageAsset("src/lambda"), architecture: cdk.aws_lambda.Architecture.ARM_64, role: role, logRetention: cdk.aws_logs.RetentionDays.THREE_MONTHS, // コレ currentVersionOptions: { removalPolicy: cdk.RemovalPolicy.RETAIN, }, timeout: cdk.Duration.minutes(5), environment: { FILTERS: "retention == infinite", DESIRED_STATE: "3months", }, }); this.alias = new cdk.aws_lambda.Alias(this, "Alias", { aliasName: "live", version: fn.currentVersion, }); ...
注意点
いくつか注意点があります。
- フィルターに渡す文字列は、シングルクォートで囲んでください。 bash による履歴展開など、意図しない展開が起こる可能性があります(bash で
echo "name !~ test"
と入力してみてください) preview
コマンドは削減量のシミュレーションに適していますが、ログ量の日割りを基準にした単純な計算に過ぎないことに注意してください。- 便宜上、
preview
コマンドにおけるBytesPerDay
フィールドの最小値は 1byte です。サイズが小さいロググループが大量にある場合、この仕様に注意してください。
今後の展望
このツールをメンテナンスしていく上で、現状では以下のような機能を考えています。
- フィルター式に論理オペレーター
&&
||
を導入する list
preview
で大量のロググループがある場合の対策として、ストリーミング出力を導入する (ソートはできなくなる)
おわりに
AWS の SDK を使って痒いところを掻くためのツールを作るのは、私の場合ほとんど趣味なのですが、今回は業務における課題をクリアするために開発しました。仕事で見つけた課題感を趣味に転用でき、自分のスキルアップにも繋げることができたため、これはいい循環だと実感しています。
ストレージ量削減の補助ツールとしては、もうひとつ S3 関連の簡単なツールを作っており、次回はそちらも紹介したいと思います。