Amplify Gen2 のCDKで環境識別子から固定のハッシュを生成して、リソース名の衝突と再作成を防ぐ方法
はじめに
前回の記事「Amplify Gen2 で OpenSearch を設定したらホットリロードの度にサンドボックス反映が15分かかるようになった話」では、OpenSearch Ingestion(OSIS)パイプラインのリソース名(物理名)がデプロイごとに変わってしまい、毎回作り直しが発生していた問題について解説しました。
その記事の解決策として、
実際のプロジェクトでは、環境識別子から短いハッシュ値を生成するヘルパーを使って suffix を決めていましたが、この記事では詳細なコードは割愛します。
と述べましたが、本記事ではその「環境識別子から短いハッシュ値を生成するヘルパー」の実装詳細と、Amplify Gen2(CDK)におけるスタック情報の取得テクニックについて解説します。
なぜ環境識別子からハッシュを生成するのか
Amplify Gen2では、開発者ごとの sandbox 環境や Feature ブランチごとの環境を簡単に作成できます。しかし、これを実現する裏側では以下のような課題があります。
- グローバルユニーク制約: S3バケットなどは全AWSアカウントで一意な名前である必要があります。
- リソース名の長さ制限: Lambda関数名(64文字)やIAMロール名など、物理名には厳しい長さ制限があります。
- 更新の安定性: 環境が変わらない限りは同じ名前(物理名)を維持しないと、リソースの再作成(削除→作成)が発生してしまいます。
これらを解決するために、Amplifyが環境ごとに生成する親スタックのIDから固定のハッシュ値を生成し、リソース名のサフィックスとして利用します。
実装: createIdentifier ヘルパー
以下は、実際にプロジェクトで使用しているヘルパー関数の実装です。Node.jsの標準モジュールである crypto を使用しています。
import * as crypto from 'crypto'
import { NestedStack } from 'aws-cdk-lib'
/**
* 親スタックのIDから決定論的な識別子を生成
*
* @param scope - 現在のCDKスコープ(NestedStack)
* @returns 10文字のハッシュ値
*/
const createIdentifier = (scope: NestedStack): string => {
// 親スタックのIDを取得(Amplify Gen2ではNestedStackとして展開されるため、親のIDに環境情報などが含まれる)
const id = scope.nestedStackParent!.node.id
// SHA-256でハッシュ化
const hash = crypto.createHash('sha256').update(id).digest('hex')
// 扱いやすい長さ(10文字)に切り出す
return hash.substring(0, 10)
}
ポイント
- 親スタックIDを利用: Amplify Gen2では、環境(sandbox の識別子やブランチ名)によって親スタックの名前が変化します。これを利用して、環境ごとにユニークかつ決定的なハッシュを生成します。
-
ランダムではなく決定的:
Math.random()ではなくハッシュ関数を使うことで、同じ環境(同じスタック名)からは常に同じサフィックスが生成されます。これにより、不要なリソース再作成を防ぎます。 - 短い: 10文字程度に抑えることで、プレフィックスと合わせてもAWSの長さ制限に収まりやすくなります。
CDKスタック情報の活用
上記のヘルパー関数では、Amplify Gen2が生成するスタック構造の特性を利用しています。
親スタックIDの利用
Amplify Gen2では、defineBackend で定義されたリソースは、環境ごとに生成されるルートスタック(親スタック)の下にNested Stackとして配置されます。この親スタックのID(scope.nestedStackParent.node.id) には、sandbox 環境の識別子やブランチ名などが含まれています。
そのため、自身のスタックIDではなく親スタックのIDをハッシュのシード(種)として利用することで、「環境ごとにユニーク」かつ「同じ環境なら常に同じ」 値を生成できます。
// Amplify Gen2では親スタックのIDに環境情報が含まれる
const parentId = scope.nestedStackParent!.node.id
ここでの scope は CDK の Construct を表す基本単位であり、ここから Stack.of(scope) や nestedStackParent を通じて、デプロイ先の環境情報を動的に取得できるのがCDKの強みです。
活用例: OpenSearch Pipelineの定義
実際にこのヘルパーを使って、OpenSearch Ingestion Pipelineのリソース名を定義する例です。
import { createIdentifier } from './utils'
import * as osis from 'aws-cdk-lib/aws-osis'
// ...
const uniqueSuffix = createIdentifier(scope)
// 論理ID(Logical ID)は固定にする
const pipelineId = 'BusinessCardsPipeline'
// 物理名(Physical Name)にハッシュを含める
// 例: bc-os-a1b2c3d4
const pipelineName = `bc-os-${uniqueSuffix}`
const pipeline = new osis.CfnPipeline(scope, pipelineId, {
pipelineName: pipelineName,
// ...
})
こうすることで、異なる開発者間でのリソース分離はもちろん、一人の開発者が複数の sandbox 環境(例: feature-login, fix-bug)を使い分ける場合でも、それぞれの親スタックIDが異なるため、リソースが競合することなく並行して開発を進められます。
また、同じ環境であれば常に同じハッシュになるため、デプロイのたびにリソースが作り直されることもありません。
おわりに
本記事では、Amplify Gen2 の CDK 環境においてリソース名の衝突を防ぎ、かつ安定したデプロイを実現するための「環境識別子を活用したハッシュ生成」について解説しました。
Amplify Gen2 は手軽に sandbox 環境を立ち上げられる反面、裏側の CDK/CloudFormation の挙動を理解していないと、意図しないリソースの再作成や命名衝突に悩まされることがあります。
今回の createIdentifier ヘルパーのように、CDK が持つコンテキスト情報(親スタックIDなど)をうまく活用して「環境ごとに決定的(Deterministic)な値」を作る というアプローチは、OpenSearch に限らず様々なステートフルリソースの管理に応用できるはずです。
この記事が、Amplify Gen2 で少し複雑な構成に挑戦する方の参考になれば幸いです。
使い倒せ、テクノロジー。(MAX OUT TECHNOLOGY)をミッションに掲げる、株式会社リバネスナレッジのチャレンジを共有するブログです。Buld in Publichの精神でオープンに綴ります。 Qiita:qiita.com/organizations/leaveanest
Discussion