はじめに
AWS CDKは、AWS公式のIaC (Infrastructure as Code) フレームワークです。CloudFormationをベースとしており、CloudFormationをそのまま使用する場合と比べて、以下のような特長があります。
- より抽象度が高く、簡潔、的なリソース定義ができる
- コンテナイメージなどアセットをデプロイ時に作成し、IaCとして統合管理できる
上記のうち2番目のアセット作成機能は、CloudFormationには備わっていない便利な機能です。しかしながら、デプロイを繰り返す度に、古く不要になったアセットがゾンビのように残ってしまい、不要アセットの削除をどうするかという面倒な課題がありました。さらに、CDKが自動作成するアセットは名前が英数字の羅列となり、どのアセットが何かパッと見でわからず消してよいかどうか判断しづらいという難点もあり、削除問題をより複雑にしていました。
cdk gcコマンドで孤立アセットをガベージコレクション
上記の課題を解決するために、CDKにはどのスタックからも参照されていない、孤立した不要なアセットのガベージコレクションを行う、cdk gc
コマンドが実装されています。
本稿執筆時点でまだ実験的機能という位置付けなので --unstable=gc
オプションを付けて実行する必要があります。
CDKデプロイにより構築されるアセットの種類は、ファイル (ファイルアセット) またはコンテナイメージ (イメージアセット) があります。それぞれ、初回のブートストラップで作成済みのS3バケットまたはECRリポジトリに保存されます。
cdk gc
コマンドは、これらS3バケットおよびECRリポジトリに保存されたアセットと、現在有効なスタックテンプレートを照合し、スタックから参照されていないアセットを削除することができます。以下はごく簡単な概要図です。
GCしてみる
スタック定義
$ mkdir gc-demo && cd $_
$ cdk init -l typescript
以下はスタック定義です。ファイルアセットとしてLambda関数のzipファイル、イメージアセットとしてLambda関数コンテナパッケージ用のイメージを作成しています (Lambda関数自体は、作成されるアセットとAWSリソースを関連付けるために便宜的に用いているだけであり、本稿における意味はありません)。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export class GcDemoStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// ファイルアセット (zipファイル) を作成
new lambda.Function(this, 'GcDemoFileAsset', {
code: lambda.Code.fromAsset('file'),
runtime: lambda.Runtime.NODEJS_LATEST,
handler: 'index.handler',
});
// イメージアセットを作成
new lambda.Function(this, 'GcDemoImageAsset', {
code: lambda.Code.fromAssetImage('image'),
runtime: lambda.Runtime.FROM_IMAGE,
handler: lambda.Handler.FROM_IMAGE,
});
}
}
以下、CDKが作成するファイルアセットとイメージアセットの元になるソースファイルです (それぞれ内容は重要ではないのでデプロイ可能であれば何でも大丈夫です)。
exports.handler = async function (event) {
return {
statusCode: 200,
body: JSON.stringify({ message: "hello cdk-gc" }),
};
};
FROM alpine
CMD ["echo", "hello cdk-gc"]
ディレクトリツリーは以下のようになります (重要でないものは割愛)。
.
├── bin
│ └── gc-demo.ts
├── cdk.json
├── file
│ └── index.js
├── image
│ └── Dockerfile
├── lib
│ └── gc-demo-stack.ts
デプロイ
cdk deploy
でデプロイします。デプロイした結果、S3バケットに以下のアセットが作成されました。
"6866..."で始まるzipファイルはLambda関数用パッケージで、jsonファイルはCDKが中間ファイルとして自動作成したスタックテンプレートファイルです。テンプレートファイルもデプロイが済めば不要になるので広義のアセットとして扱い、GC対象となります。
ECRには、イメージアセットとして以下が作成されました。
合成 (synth) されるテンプレートは以下のようになっており、上記のアセットがスタックから参照されていることが確認できます。
GcDemoFileAsset4EC44DE6:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket:
Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}
# ファイルアセットを参照
S3Key: 68665c32237afdc390488e345cf25d27095b4952ade00507aa549d93f7857e16.zip
GcDemoImageAsset8B6AFE29:
Type: AWS::Lambda::Function
Properties:
Code:
ImageUri:
# イメージアセットを参照
Fn::Sub: ${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:a1bfa2be3548cc30d1ad402b27c3094154d48a864346eb2ddd4f34ee5db8abc3
PackageType: Image
再デプロイしてアセットを更新
次に元のソースの内容を更新して再デプロイし、新バージョンのアセットを作成します。
exports.handler = async function (event) {
return {
statusCode: 200,
body: JSON.stringify({ message: "hello cdk-gc AGAIN" }), // 更新
};
};
FROM alpine
CMD ["echo", "hello cdk-gc AGAIN"]
再デプロイした結果は以下の通りです。新しいアセットがアップロードされています。
ファイルアセット。"2da3..."で始まるファイルが新しいzipファイルアセットです。
イメージアセット。"0318..."で始まるイメージが新アセットです。
合成結果のテンプレートから、参照先のアセットが新しいものに置き換わり、古いアセットは参照されなくなったことが確認できます。
GcDemoFileAsset4EC44DE6:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket:
Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}
# 新しいアセットを参照
S3Key: 2da382397a720565cc74fba79aba2efadbfe89f2164dd72f8c378ff4aa701238.zip
GcDemoImageAsset8B6AFE29:
Type: AWS::Lambda::Function
Properties:
Code:
ImageUri:
# 新しいアセットを参照
Fn::Sub: ${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:031845b02949895fc71bbf4ffeb43af60dcb166b516969d1c188ebf5a6c6cd95
ガベージコレクション実施
どこからも参照されておらず孤立した古いアセットをGCしましょう。
$ cdk gc --unstable=gc --created-buffer-days=0
⏳ Garbage Collecting environment aws://xxxxxxxxxxxx/xxxxxx...
Found 3 objects to delete based off of the following criteria:
- objects have been isolated for > 0 days
- objects were created > 0 days ago
Delete this batch? ([y]es/[n]o/[a]ll) yes
[100.00%] 4 files scanned: 0 assets (0.00 MiB) tagged, 3 assets (0.02 MiB) deleted.
(続く...)
まずは、ファイルアセットからGCします。4ファイル中3ファイル削除されたことがわかります。削除対象アセットの内訳は以下の通りです。現在有効な新zipファイルのみが残ります。
- 旧 zipファイル
- 旧 中間テンプレートファイル
- 新 中間テンプレートファイル
新中間テンプレートファイルが削除されていることが気になるかもしれませんが、テンプレートファイル自体は、スタックテンプレート内で参照されていないので、孤立アセットとみなされ削除されます。デプロイが完了後は中間テンプレートファイルは不要になるので削除しても問題ありません。
なお残念ながら、本稿執筆時点で cdk gc
コマンドはどのアセットが削除されるかを教えてくれません。詳細情報を表示する --verbose
や --debug
オプションを付与しても表示されません。これは今後の改善点です。
続いてイメージアセットのGCです。
(...続き)
Found 1 image to delete based off of the following criteria:
- images have been isolated for > 0 days
- images were created > 0 days ago
Delete this batch? ([y]es/[n]o/[a]ll) yes
[100.00%] 2 files scanned: 0 assets (0.00 MiB) tagged, 1 assets (3.62 MiB) deleted.
2イメージ中、1イメージが削除されたことが分かります。イメージアセットは旧イメージが1つ削除され、現在有効な新イメージのみが残ります。
cdk gc
コマンドに指定した、--created-buffer-days
オプションについて補足します。このオプションに日数を指定すると、孤立アセットを検出した場合でも、作成から指定日数を経過していないアセットは削除せずに維持することができます。未指定の場合のデフォルト値は1日となり、アセット作成の翌日まで削除されません。今回は削除動作をすぐに見るために0日としています。詳細は後述のライフサイクル日数の指定を参照ください。
以上、cdk gc
コマンドを使って、何も考えずに孤立アセットをガベージコレクションできました。
GC対象の判定基準について補足しておきます。アセットが孤立しているかどうかの判定基準は、対象アセットの作成のきっかけとなったスタックが現在生きているか (=削除や変更されていないか) ではありません。判定基準は、現在有効なスタックのテンプレートボディ内で当該アセットの識別子が使用されているかどうかです。すなわちcdk deploy
でアセット及びスタックを作成しても、そのスタック内のAWSリソースが当該アセットを参照していなければ、スタックをそのまま維持していても当該アセットはGCされてしまいます。例えば DockerImageAsset コンストラクトを使えばスタック内リソースから独立したイメージアセットを作成できますが、そのままではGC対象になります。
GC動作のカスタマイズ
対象アセット種別の指定
アセット種別には、S3バケットに保存されるファイルアセット、ECRリポジトリに保存されるイメージアセットがあります。--type
オプションを指定することで、特定のアセット種別だけをGC対象として限定できます (例 --type=s3
)。
指定可能な値は以下の通りです。
値 | 意味 | 備考 |
---|---|---|
s3 | ファイルアセットをGC | |
ecr | イメージアセットをGC | |
all | 全アセットをGC | デフォルト |
アクションの指定
cdk gc
は、孤立したアセットを削除せずに、孤立状態であることのマーキング (タグ付け) だけ行うことも可能です。cdk gc
で実行されるアクション内容は --action
オプションで指定できます。
有効な値は以下のとおりです。
値 | 意味 | 備考 |
---|---|---|
tag | 孤立状態であることをアセットにタグ付けする | |
delete-tagged | 孤立タグのついたアセットを削除する | |
full | 孤立タグ付けと削除を行う | デフォルト |
GC検査情報を表示する | (本稿執筆時点で有意義な情報は表示されません) |
では、タグ付けとは具体的に何が行われるのでしょうか。ファイルアセットとイメージアセットでは、タグ付け方法が異なります。実際に孤立アセットを作成し、タグ付けしてみた結果が以下の通りです。
ファイルアセット: S3オブジェクトタグが付与される
キー: "aws-cdk:isolated"のタグがS3オブジェクトに付与されます。その値は、タグ付け日時のunixエポックからの経過ミリ秒となります。
イメージアセット: コンテナイメージタグが付与される
孤立状態の既存イメージに、別名のイメージタグとして "0-aws-cdk-{unixエポック経過ミリ秒}" というタグが付与されます。
ライフサイクル日数の指定
--rollback-buffer-days オプション
日数を数字で指定します。デフォルトは0。1以上が指定された場合、アセットが孤立状態になっても即時に削除されず、孤立タグのついた日時から指定日数が経過するまで保持されます。この間にスタックがロールバックするなどアセットの参照が復活した場合、孤立タグが除去され削除候補から外れます。
--created-buffer-days オプション
日数を数字で指定します。デフォルトは1。作成日時から指定日数が経過していないアセットはGC対象から外れます。デフォルトが1になっている理由は、アセットの誤削除を防止するためです。デプロイとGCが同時に走った場合、アセット作成後かつスタック完成前の状態でアセットが検査されるとアセット未使用と誤判定されGCされてしまうリスクがゼロではありません。こうしたレースコンディション問題はIssueも挙げられており、今後改善されるものと思われます。
さいごに
CDKの不要アセットは、cdk gc
コマンドで一掃するのが便利です。ただし、本稿執筆時点でunstable扱いなので、まだまだ改善の余地がありますし、本番環境での使用には十分注意してください。