0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS CDK ゾンビアセットのガベージコレクション

Posted at

はじめに

AWS CDKは、AWS公式のIaC (Infrastructure as Code) フレームワークです。CloudFormationをベースとしており、CloudFormationをそのまま使用する場合と比べて、以下のような特長があります。

  1. より抽象度が高く、簡潔、的なリソース定義ができる
  2. コンテナイメージなどアセットをデプロイ時に作成し、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リソースを関連付けるために便宜的に用いているだけであり、本稿における意味はありません)。

lib/gc-demo-stack.ts
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が作成するファイルアセットとイメージアセットの元になるソースファイルです (それぞれ内容は重要ではないのでデプロイ可能であれば何でも大丈夫です)。

file/index.js
exports.handler = async function (event) {
  return {
    statusCode: 200,
    body: JSON.stringify({ message: "hello cdk-gc" }),
  };
};
image/Dockerfile
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バケットに以下のアセットが作成されました。

ファイルアセット1

"6866..."で始まるzipファイルはLambda関数用パッケージで、jsonファイルはCDKが中間ファイルとして自動作成したスタックテンプレートファイルです。テンプレートファイルもデプロイが済めば不要になるので広義のアセットとして扱い、GC対象となります。

ECRには、イメージアセットとして以下が作成されました。

イメージアセット1

合成 (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

再デプロイしてアセットを更新

次に元のソースの内容を更新して再デプロイし、新バージョンのアセットを作成します。

file/index.js
exports.handler = async function (event) {
  return {
    statusCode: 200,
    body: JSON.stringify({ message: "hello cdk-gc AGAIN" }), // 更新
  };
};
image/Dockerfile
FROM alpine
CMD ["echo", "hello cdk-gc AGAIN"]

再デプロイした結果は以下の通りです。新しいアセットがアップロードされています。

ファイルアセット。"2da3..."で始まるファイルが新しいzipファイルアセットです。
ファイルアセット2

イメージアセット。"0318..."で始まるイメージが新アセットです。
イメージアセット2

合成結果のテンプレートから、参照先のアセットが新しいものに置き換わり、古いアセットは参照されなくなったことが確認できます。

新スタックテンプレート抜粋
  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ファイル
  • 旧 中間テンプレートファイル
  • 新 中間テンプレートファイル

file-asset-gc.png

新中間テンプレートファイルが削除されていることが気になるかもしれませんが、テンプレートファイル自体は、スタックテンプレート内で参照されていないので、孤立アセットとみなされ削除されます。デプロイが完了後は中間テンプレートファイルは不要になるので削除しても問題ありません。

なお残念ながら、本稿執筆時点で 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つ削除され、現在有効な新イメージのみが残ります。
image-asset-gc.png

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 孤立タグ付けと削除を行う デフォルト
print GC検査情報を表示する (本稿執筆時点で有意義な情報は表示されません)

では、タグ付けとは具体的に何が行われるのでしょうか。ファイルアセットとイメージアセットでは、タグ付け方法が異なります。実際に孤立アセットを作成し、タグ付けしてみた結果が以下の通りです。

ファイルアセット: S3オブジェクトタグが付与される

キー: "aws-cdk:isolated"のタグがS3オブジェクトに付与されます。その値は、タグ付け日時のunixエポックからの経過ミリ秒となります。

file-asset-tagged.png

イメージアセット: コンテナイメージタグが付与される

孤立状態の既存イメージに、別名のイメージタグとして "0-aws-cdk-{unixエポック経過ミリ秒}" というタグが付与されます。

image-asset-tagged.png

ライフサイクル日数の指定

--rollback-buffer-days オプション

日数を数字で指定します。デフォルトは0。1以上が指定された場合、アセットが孤立状態になっても即時に削除されず、孤立タグのついた日時から指定日数が経過するまで保持されます。この間にスタックがロールバックするなどアセットの参照が復活した場合、孤立タグが除去され削除候補から外れます。

--created-buffer-days オプション

日数を数字で指定します。デフォルトは1。作成日時から指定日数が経過していないアセットはGC対象から外れます。デフォルトが1になっている理由は、アセットの誤削除を防止するためです。デプロイとGCが同時に走った場合、アセット作成後かつスタック完成前の状態でアセットが検査されるとアセット未使用と誤判定されGCされてしまうリスクがゼロではありません。こうしたレースコンディション問題はIssueも挙げられており、今後改善されるものと思われます。

さいごに

CDKの不要アセットは、cdk gc コマンドで一掃するのが便利です。ただし、本稿執筆時点でunstable扱いなので、まだまだ改善の余地がありますし、本番環境での使用には十分注意してください。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?