オブジェクトの広場はオージス総研グループのエンジニアによる技術発表サイトです

クラウド/Webサービス

もっとじっくり AWS CDK のコンセプト

第6回 アセット
オージス総研
樋口 匡俊
2024年3月27日

コンストラクトは L1, L2, L3 のように AWS のリソースを作成するものばかりではありません。リソースが利用するファイルや Docker イメージなどのアセットもまた、コンストラクトとして表現されます。今回は前半でコンストラクトとしてのアセットについて説明し、後半でその背後にあるブートストラップやクラウドアセンブリなど具体的な仕組みについて説明します。

アプリとアセット

なにかアプリケーションを開発しようという場合、ふつうはプログラミング言語でソースコードを書きます。 ソースコードやそれをコンパイルしたバイナリは、いわばアプリの本体です。

その本体とは別に、アプリの多くはさまざまなファイルを必要とします。 たとえば Web アプリケーションであれば、たいていは画面に表示する PNG や JPEG の画像ファイルが必要でしょう。 それらのファイルはソースコードが置かれる src ディレクトリではなく、よく assets という名の別のディレクトリに分けて置かれます。

AWS CDK アプリにも、本体のソースコードとは別にアプリに必要なものがあり、それらはアセット (asset) と呼ばれます。 アセットは AWS CDK アプリとともに AWS のリソースを作成するために利用されます。

AWS CDK のアセットは次の二種類に分けられます。

  1. ファイルアセット (file asset)

    JSON ファイルや、AWS Lambda 関数のコード一式を含むディレクトリなど、ファイルやディレクトリ全般がこれに分類されます。

  2. イメージアセット (image asset)

    Dockerfile など Docker イメージのビルドに必要なファイル一式を含むディレクトリは、ファイルアセットではなくこちらに分類されます。 正確には「Docker イメージアセット」と呼ぶべきですが、長いので省略します。

アセットのパブリッシュ

アセットはたいていアプリとともに作成し、その後アセットストア (asset store) に格納して利用します。 アセットストアは、ファイルアセットの場合 Amazon S3 バケットで、イメージアセットの場合 Amazon Elastic Container Registry (Amazon ECR) のリポジトリです。 アセットストアの作り方はあとで説明します。

アセットがディレクトリであれば、zip のようなファイルにまとめてから格納します。 イメージアセットであれば Docker イメージをビルドしてから格納します。 これら一連の格納処理のことをパブリッシュ (publish) といいます。

パブリッシュ概要

パブリッシュはアプリのデプロイと合わせて自動で行われるので、開発者は特に気にする必要はありません。 デプロイ用のコマンドである cdk deploy を実行すると、先にパブリッシュを行い、その後で CloudFormation テンプレートをデプロイしてくれます。

パブリッシュしてからデプロイする

アセットのコンストラクト

それぞれのアセットにはコンストラクトが提供されています。 ファイルアセットのコンストラクトは aws-s3-assets モジュールの Asset、イメージアセットのコンストラクトは aws-ecr-assets モジュールの DockerImageAssetです。

コンストラクトである以上、それらは L1, L2, L3 などと同じようにコンストラクトツリーの一部としてあつかえます。 例を見てみましょう。

import { Asset } from 'aws-cdk-lib/aws-s3-assets';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';

// ファイルアセット
new Asset(this, 'FileAsset', {
  path: path.join(__dirname, 'file-asset.json'),
});

// イメージアセット
new DockerImageAsset(this, 'ImageAsset', {
  directory: path.join(__dirname, 'image-asset-directory'),
});

'file-asset.json''image-asset-directory' には、あらかじめアセットとして利用するファイルを置いておきます。 あとはいつものようにアプリをシンセサイズしてデプロイするだけです。 開発者はアセットの細かいことは気にせず、コンストラクトの使い方さえ知っていればよいのです。

アセットのさらなる抽象化

さらに上のようなコンストラクトでさえも、直接利用することはあまりありません。 かわりに、各モジュールが提供する抽象化された仕組みを利用します。

例としてファイルアセットを用いて Lambda 関数を作成するコードを見てみましょう。

import * as lambda from 'aws-cdk-lib/aws-lambda';

new lambda.Function(this, 'Function', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  // file-asset-directory ディレクトリには Lambda 関数のコード index.js などが含まれている
  code: lambda.Code.fromAsset(path.join(__dirname, 'file-asset-directory')),
});

code プロパティの型は aws-lambda モジュールが提供する Code という抽象クラスです。 fromAsset はいわゆるスタティックファクトリーメソッド (static factory method) で、引数の 'file-asset-directory' ディレクトリをファイルアセットとして利用する Code 型のオブジェクトを生成してくれます。 もしも Docker イメージを用いる Lambda 関数を作成したい場合は、かわりに fromAssetImage メソッドを呼び出し、イメージアセットのための Code 型のオブジェクトを生成します。

Code クラスはアセットを利用しない from メソッドも提供しています。 たとえば fromInline は Lambda 関数のコードを直接 CloudFormation テンプレートに埋め込むための Code 型のオブジェクトを生成してくれます。

それぞれの from メソッドが生成する Code 型のオブジェクトの実体は異なり、実行される処理も異なります。 けれども開発者はそうした細かいことは気にせず、Lambda 関数で利用したいものに応じた適切な from メソッドを呼び出すだけでよいのです。

アセットの課題

ここでひとつ、アセットで注意したいことを紹介しておきます。

実はアプリで作成した CloudFormation のスタックを削除しても、そこで利用していたアセットは削除されません。 cdk コマンドには cdk destroy というスタックを削除するコマンドがありますが、やはりアセットは削除しません。

これはアセットが長年かかえている課題です。 コミュニティ有志が開発したツールや対策法はあるものの、公式では GitHub に issue が立てられたまま 5 年以上未解決のままです1。 本番環境等、簡単にはアセットを削除しづらい環境では、この点をふまえてあらかじめ対応を検討しておくとよいでしょう。

アセットを支える仕組み

さて、以上のようにアセットはコンストラクトとして抽象化され、AWS CDK アプリの中で簡単にあつかえるようになっています。 それでは、その抽象化の背後ではいったいどのような仕組みが働いているのでしょうか。

以下ではアセットを支える主な仕組みについて、次の四つに分けて見ていきます。

① ブートストラップ
② スタックシンセサイザー
③ クラウドアセンブリ
④ パブリッシュとデプロイ

① ブートストラップ

ブートストラップ (bootstrap) とは、AWS CDK アプリのための環境を AWS 上に構築する処理のことです。 IAM ロールをはじめ、シンセサイズやデプロイで利用するさまざまなリソースを作成します。 今までこの連載ではふれずに来ましたが、アセットを使うか使わないかにかかわらず、実質的に AWS CDK にとって必須の処理です。

ブートストラップを行うもっとも簡単な方法は cdk bootstrap コマンドを実行することです。 このコマンドは、デフォルトの CloudFormation テンプレートを用いてブートストラップのスタックを作成してくれます。

アセットストアである S3 バケットと ECR リポジトリは、ブートストラップで作成されます。 また、パブリッシュできる権限を持った IAM ロールも作成されます。 それらが無いとアセットが格納できずデプロイの時に使えないので、遅くともデプロイ前にはブートストラップを済ませておかなくてはなりません。

アセットのためのブートストラップ

cdk bootstrap コマンドで作成されるリソースは、コマンドの各種オプションでカスタマイズできます。 また、デフォルトのテンプレートが cdk bootstrap --show-template で出力できるので、それを書き換えることでもカスタマイズできます。

② スタックシンセサイザー

スタックシンセサイザー (stack synthesizer) とは、スタックのシンセサイズを担うクラスの総称です。 シンセサイザーをカスタマイズすることで、出力されるクラウドアセンブリを変え、結果としてデプロイの挙動も変えることができます。

デフォルトのシンセサイザーである DefaultStackSynthesizer には、デフォルトのブートストラップに合わせた設定がほどこされています。 逆に言うと、DefaultStackSynthesizer に合わせてブーストラップのデフォルトスタックは作成されます。 ブートストラップとシンセサイザーのどちらかをカスタマイズするならば、もう一方もカスタマイズすることになるわけです。

以下の例では、ブートストラップでカスタマイズしたアセットストアの名前を設定しています。

new SampleAppStack(app, 'SampleAppStack', {
  synthesizer: new DefaultStackSynthesizer({
    // ファイルアセットのアセットストアの名前
    fileAssetsBucketName: 'custom-bucket',
    // イメージアセットのアセットストアの名前
    imageAssetsRepositoryName: 'custom-repo',
  }),
});

synthesizer プロパティの型は IStackSynthesizerインターフェースです。 このインターフェースを実装していれば、自作したシンセサイザーでも設定できます。

③ クラウドアセンブリ

AWS CDK アプリをシンセサイズすると、デフォルトでは cdk.out ディレクトリにクラウドアセンブリが出力されます。 クラウドアセンブリについては、第1回で CloudFormation テンプレートとその他何種類かのファイルとして概要を説明しました。 今回はもっと具体的に、以下の三つのファイルに分けて見てみましょう。

  • manifest.json
  • assets.json
  • template.json

manifest.json

いわゆるマニフェストファイルです。 JSON Schema の定義にしたがい、クラウドアセンブリを構成する各種データやファイルの情報が記述されています。

それらの情報はアーティファクト (artifact) と呼ばれ、以下の四種類があります。 (“()” の中は JSON Schema で定義されている ArtifactType 文字列)

  1. CloudFormation スタック (aws:cloudformation:stack)

    スタックを作成するための CloudFormation テンプレートや、IAM ロールの ARN などの情報です。

  2. クラウドアセンブリ (cdk:cloud-assembly)

    クラウドアセンブリは、別のクラウドアセンブリを中に持てます。 つまりネストができます。 ネストしているクラウドアセンブリのディレクトリの相対パスが記述されています。

  3. ツリー (cdk:tree)

    コンストラクトツリーの構造を出力したファイルの情報です。 ファイル名はデフォルトで tree.json です。 tree.json は、主に AWS の VS Code 拡張のような周辺ツールで利用されます。

  4. アセットマニフェスト (cdk:asset-manifest)

    次に説明するアセットマニフェストの情報です。 アセットマニフェストの相対パスが記述されています。

assets.json

アセットの情報が記述されているマニフェストファイルです。 ふつう先頭にスタック名が付いて XXXX.assets.json のようなファイル名になります。

ファイルアセットの情報は、以下のように files に記述されます。

"files": {
  "<アセットのハッシュ値>": {
    "source": {
      "path": "...",
      ...
    },
    "destinations": {
      "123456789012-ap-northeast-1": {
        "bucketName": "custom-bucket",
        "objectKey": "...",
        "region": "ap-northeast-1",
        "assumeRoleArn": "..."
      }
    }
  }
},

シンセサイズすると、アセットはコピーされ、クラウドアセンブリの一部として cdk.out ディレクトリの下に置かれます。 sourcepath は、そのコピーしたアセットの相対パスです。

destinations には、コピーしたアセットを格納するアセットストアの情報が記述されます。 具体的な値は、先ほど見たスタックシンセサイザーによって変わってきます。 この例では、バケット名を custom-bucket にカスタマイズしています。

イメージアセットの情報は、以下のように dockerImages に記述されます。

"dockerImages": {
  "<アセットのハッシュ値>": {
    "source": {
      "directory": "asset.<アセットのハッシュ値>"
    },
    "destinations": {
      "123456789012-ap-northeast-1": {
        "repositoryName": "custom-repo",
        "imageTag": "...",
        "region": "ap-northeast-1",
        "assumeRoleArn": "..."
      }
    }
  }
}

ファイルアセットもイメージアセットも、それぞれアセットの元になる source と、格納する destinations が明示されています。 デフォルトのブートストラップやスタックシンセサイザーを使う場合でも、それらの情報は省略されません。 そしてそれらの情報は、AWS CDK アプリとその開発者によって決められたものです。

このことは、一見当たり前のようですが実は重要です。 AWS CDK アプリは、シンセサイズが終わったらデプロイなどの後続処理には直接は関与しません。 そのため、cdk deploy コマンドなどアプリ以外のものに、開発者の意図したとおりに後続処理を実行してもらうには、このように明確に省略せずに必要な情報を伝えなければならないのです。

template.json

おなじみの CloudFormation テンプレートです。 これもふつう先頭にスタック名が付いて XXXX.template.json のようなファイル名になります。

テンプレートは、利用したいアセットがすでに assets.json の各 destinations に格納されている前提で生成されます。 以下の例では、アセットストア custom-bucket にファイルが格納されている前提で Lambda 関数のコードとして利用しています。

"LambdaFunction": {
  "Type": "AWS::Lambda::Function",
  "Properties": {
  "Code": {
    "S3Bucket": "custom-bucket",
    "S3Key": "..."
  },
  ...

cdk deploy コマンドが、テンプレートをデプロイするより前にパブリッシュを行うのはこうした背景もあるわけです。

④ パブリッシュとデプロイ

クラウドアセンブリがあれば、いつもの cdk deploy コマンドを使わなくてもパブリッシュやデプロイを実行できます。

パブリッシュは、cdk-assets という CLI で実行できます。 実行する際は cdk-assets publish --path <アセットマニフェストのパス> のようにアセットマニフェストを指定します。 アプリの開発者の意図はアセットマニフェストにきちんと記述されているので、それらを正しく解釈できればパブリッシュの手段は問わないのです。

cdk-assets は AWS CDK の一部として開発されていますが、コンストラクトライブラリ等とは独立したパッケージとして配布されています。 アプリケーションで利用できるライブラリでもあり、cdk deploy コマンドがパブリッシュする処理でも利用されています。 やろうと思えば、パブリッシュ用の独自のツールやサービスも開発できるでしょう。

デプロイは、ふつうの CloudFormation の仕組みで実行できます。 シンセサイズして出力したクラウドアセンブリと、パブリッシュしたアセットがあれば、あとはそれらを用いて CloudFormation の API や AWS SDK を操作し、テンプレートをデプロイすればよいのです。

おわりに

今回はアセットとその背後にある仕組みについて説明しました。

前半で説明したように、AWS CDK アプリの開発者はアセットをコンストラクトとしてアプリの中で簡単に利用できます。 普段はその背後にあるブートストラップやクラウドアセンブリなどについて意識する必要はありません。

しかし、それらは後半で見てきたとおり要件に合わせてカスタマイズできるようになっています。 定番の cdk コマンドを使わずに、パブリッシュやデプロイができるようにもなっています。 これは時間をかけてよく練られた設計であり、さらにくわしく知りたい方は、以下の参考資料のうち特に AWS CDK RFC を参照してください。


参考資料

公式ドキュメント『Concepts』の中の『Assets』には、アセットのサンプルコードがひととおり紹介されています。 同じく『Concepts』の『Bootstrapping』には、ブートストラップとスタックシンセサイザーの詳細が説明されています。

アセットやその背後の仕組みのもとになった考え方を知りたい場合は、以下の AWS CDK RFC が参考になります。

AWS CDK アプリの CI/CD を行う pipelines モジュールは、これら RFC の考え方にもとづいて開発されています。


  1. ちなみにこの issue、昨年末に突如クローズされ一時騒然とした(と筆者は感じた)のですが、すぐに復活しました。