📖

CDK を使ってデプロイ前に意図しない "Replacement" を検出したい - part1

2024/08/01に公開

最近出した CDK の誰得記事、「cdk.context.json が CDK 的にどう扱われているか調べてみた」に引き続き、また CDK のソースコード読解シリーズを出してみました。

このシリーズの主テーマは次の2点です。

  • cdk diff はどうして "Replacement" を検出できるのか?
  • デプロイ前に組み込める、Replacement を効果的に検出する方法があるか?

なお、今回は前フリ的な位置付けで、ソースコードはまだ読みません。

(追記) 2024/08/04

part2 書きました。

https://zenn.dev/hassaku63/articles/2570b9f5e6bd05

追記) 2024/08/06

この記事を書く過程で得た知識をまとめたものを所属会社のブログで発表しました。

https://blog.serverworks.co.jp/2024/08/06/123000

発端

私もそれなりに CDK に詳しくなってきただろうということで、AWS さんの活動を真似て社内で "AWS CDK Office Hour" なる活動を始めてみました。

私個人が CDK 推しであるため社内的にもうちょい大々的に CDK が流行って欲しい事情と、自社内の大半のエンジニア組織にとっても広める意義のあるプロダクトであると私は信じているので、だったらその領域を牽引できるアイコン的な奴が一人いた方が色々好都合だろうと考えました。趣味と実益、啓蒙と腕試しを兼ねたような感じです。

本記事の執筆時点で合計3回ほど開催しています。そこで受けた質問から派生したテーマとしてこの記事を書いています。

前提事項

この記事で参照する CDK のバージョンは v2.150.0 を前提とします。

https://github.com/aws/aws-cdk/tree/v2.150.0

https://github.com/aws/aws-cdk/tree/v2.150.0

$ git checkout -b v2.150.0 tags/v2.150.0

元々の問いと、それに対する現時点の立場の表明

開発者が意図せず発生した Replacement を伴う更新をデプロイしてしまうことによりサービスの一部が瞬断ないし停止してしまうケースを懸念されているようでした。そのために「意図しない Replacement」を検出する手段があれば良い、ということでご質問いただきました。

質問者の方は cdk diff の存在をすでに知っており、他の方法があるかを気にしていました。

現時点の見解

「"replacement" な変更を検知する」という話に関する直接的な回答としては、執筆時点の私の見解は質問者同様 cdk diff を用いるくらいしか案はありませんでした。

cdk diff は "replacement" の可能性がある変更を検出してくれます。実際に cdk diff の出力を見てみることにしましょう。

以下に示す例は IAM Role の "RoleName" を使ったものです。一度 IAM Role リソースを持つ CDK App をデプロイし、手元のエディタで RoleName を変更した状態で cdk diff を実行します。なお、検証には単一 App の中に単一の Stack が存在する、最も単純な構成を用いました。

Stack の実装は以下です。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';

export class CdkDetectReplacementStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const role = new iam.Role(this, 'Role', {
      roleName: 'my-role-2', // change 'my-role' -> 'my-role-2'
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')],
    });

    role.addToPolicy(new iam.PolicyStatement({
      actions: ['s3:GetObject'],
      resources: ['arn:aws:s3:::my-bucket/*'],
    }));
  }
}

これを予めデプロイしておき、手元で RoleName 属性を更新して cdk diff を実行します。

$ npx cdk diff  
Stack CdkDetectReplacementStack
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)
Resources
[~] AWS::IAM::Role Role Role1ABCC5F0 replace
 └─ [~] RoleName (requires replacement)
     ├─ [-] my-role-1
     └─ [+] my-role-2


✨  Number of stacks with differences: 1

RoleName 属性の変更が "requires replacement" であると出ています。このように、cdk diff コマンドは更新要件に replacement を持つプロパティの変更を検出します。

よって、質問への直接的な回答としては cdk diff があればOK、ということになります。

ちなみに、cdk diff コマンドは ChangeSet を作って手元の Stack 定義と比較することもできます。

https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-diff.html#ref-cli-cmd-diff-options

Specify whether to create a change set to analyze resource replacements.

When true, the CDK CLI will create an AWS CloudFormation change set to display the exact changes that will be made to your stack. This output includes whether resources will be updated or replaced. The CDK CLI uses the deploy role instead of the lookup role to perform this action.

When false, a quicker, but less-accurate diff is performed by comparing CloudFormation templates. Any change detected to properties that require resource replacement will be displayed as a resource replacement, even if the change is purely cosmetic, like replacing a resource reference with a hard-coded ARN.

デフォルトは true なので、特になにも指定しないならばこのコマンドは手元の CDK App 定義と ChangeSet の比較になります。false にすることでテンプレートベースの比較もでき、精度が落ちる代わりに高速になるようです。

以上のことから、diff コマンドで直接的な課題は解決できそうです。ただ、元々の問題意識からするといくつか周辺事情も踏まえてアプローチがあるように思いました。

以降のサブセクションで解説します。

更新要件が replacement であるプロパティの利用を最小限にする

このセクションでは、そもそも replacement が必要となる変更が発生しにくいように作りましょう、ということを述べます。

具体的に、かつ典型的な例を述べると、上述した IAM Role "RoleName" プロパティのような「物理名」に相当するプロパティの指定がこれに該当します。

CloudFormation リソースタイプのプロパティとして存在する物理名のプロパティは更新要件が replacement になっている場合が多いです。

一例を挙げると、上述の IAM Role "RoleName" プロパティや、ELB v2 LoadBalancer の Name プロパティDynamoDB Table の TableName などがあります。

この中だと、おそらく DynamoDB Table の TableName が少々曲者です。ドキュメントの TableName プロパティの箇所に記載がある通り、TableName を明示的に指定することで一部のリソース更新に追加の制約が生じます。これによって、スキーマ変更などに伴いテーブル移行が必要な場合の移行手順に考慮事項が増えます。

If you specify a name, you cannot perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you must replace the resource, specify a new name.

私が知る身近な例はこれだけですが、おそらく他の AWS リソースタイプにも「物理名」を指定することで制約を受ける仕様は存在すると思われます。

おそらく、多くのユーザーは IaC を使うと決めた段階でできるだけ多くのものを IaC で管理しておきたいと考えるはずです。そして、IaC による更新のみで構成を維持管理し続けること自体そもそも簡単な話ではありません。機能改修やリアーキテクチャなどによってリソースを移行せざるを得ない場合もあるでしょう。そうしたときに、Replacement 要件を持つリソースが多ければ多いほど、IaC の管理状態と整合と取りつつ上手く移行するのは大変です。

物理名を明示的につけたい理由として「人間が見やすいから」という話がよく挙げられます。これはこれで大変良く同意できる話です。なんのかんの言ってトラブルシューティングする際にマネコンを見に行くことは良くあります。ただし、そのメリットを手にすることは、中長期のサービス運用で発生しうるリソース変更あるいはリソース移行の困難さと引き換えになる可能性があります。このトレードオフを考慮すると良いでしょう。

私見を述べさせていただくなら、IaC でやると決めた時点である程度そのあたり(命名)は割り切った方がよく、できるだけ物理名には関与しない作りにした方が無難であると考えます。また、CDK であればリソースタグの付与は簡単ですので、そのリソースのオーナーシップや役割、所属するサービス・プロジェクトの名称など、意味的な部分の管理・把握という意識は物理名でなくタグにも担ってもらう方向で検討する、というのもあるかと思います。

ちなみに、物理名に関係する議論は CDK レポジトリにも存在します。

https://github.com/aws/aws-cdk/discussions/20275

上記リンク先で peterwoodworth が次のように回答しています。

※回答者の peterwoodworth は幾度となく CDK へのコントリビューションを行った実績のある人物です。本来発言者の権威性を議論の根拠とすることはできませんが、分野に対する知見を持つ人物の見解であるということを鑑みると一定以上の信頼性はあると考えます。

Answer: We recommend against specifying the names of resources because it causes a conflict that is hard/annoying to get out of if you end up making another change that requires replacing your resource.
You should only go through resource replacement if you need to change something about your resource that requires resource replacement to do so. There may be unwanted side effects to doing this depending on what kind of resource you are dealing with

和訳すると、次のようなことを述べています。

  • 名前を指定することで他の(Replacementが必要な)リソース更新を行う際に競合が発生し、解決困難なトラブルを招く可能性がある
  • リソースの "置き換え" は、それが必要である場合にだけ実行すべき

私もこの意見に同意します。[1]

意図しない Replacement が発生してしまうことで特に困るリソースがある場合、その周辺の構成を見直す

意図せず replacement されてしまっても影響が出にくい構成にすればリスクを緩和できるのでは、という話です。

質問者の言をお借りすると、具体的に困る可能性があるものとして言及していたひとつが Vercel で用いる IAM User リソースとのことでした。

質問者が実装をした時点では、Vercel から AWS API へのリクエストを行う場合は IAM user ベースでクレデンシャルを持っておく方法しか採用できなかったようです[2]

IAM user の情報を Vercel のシークレット格納の仕組みに放り込んでおくようにしたが、意図しないタイミングで IAM user のリソースが置き換わってしまった場合に AWS アカウントへの疎通が取れなくなる可能性がある、ということを心配されているようでした。

私から提案したのは、その IAM user を極力直接触らずに済む構成にすることと、追加で実行中に動的に値を変えられる Feature flag の導入を検討してはどうか?ということです。

その IAM user が直接リクエストを行うのではなく、アプリの動作に必要な実質的な権限を持つ IAM Role に AssumeRole するようにします。このような構成にすることで、Vercel から直接見に行く IAM user への参照や、その実体となるモノ(アタッチされた Policy 等も含む)を変更せねばならないケースはおそらく減ります。追加の権限を与えたい場合は IAM Role の方を変更すれば良く、大事を取るのであれば既存の隣に IAM Role + Policy を作っておき、Vercel の再デプロイで AssumeRole のターゲット指定を切り替えれば切り替えによる障害の発生は比較的抑えられます。ただし、このケースだと直近の AssumeRole のセッション持続時間を考慮する必要があるので、アプリケーション側も相応のケアが求められます。

もう少しリッチにするのであれば、ここに動的に変更できる Feature flag を導入します。例えば LaunchDarkly のような製品です[3]

LaunchDarkly の例で言えば、参照したい IAM user のクレデンシャルと、そこから AssumeRole する先の ARN を LaunchDarkly のストアに預けておくイメージです。

値の取得は LaunchDarkly の SDK を介して行います。そして、SDK にはフラグ値の変更をアプリケーション側でサブスクライブする機能があります。

https://docs.launchdarkly.com/sdk/features/flag-changes

以下は Node.js (server-side) のサンプルコードの引用です。

client.on('update', (param) => {
  console.log('a flag was changed: ' + param.key);
});

client.on('update:flag-key-123abc', () => {
  console.log('the flag-key-123abc flag was changed');
});

このような機能を組み合わせることで、Vercel が利用する IAM Role を切り替えた際に、アプリケーション側で現在取得している Session token のリフレッシュが必要であるとわかるようになります。ここでのポイントは「再デプロイしなければ値の変更を反映できないストアを使わない」という点になります。

本旨からは少し外れますが、このあたりはどういう性質の Feature flag で分類して考えるとよいと思います。サイバーエージェントの岩見 (@Iwamin) さんという方が出している資料で言及されている、次のスライドの図が参考になるでしょう。

https://speakerdeck.com/biwashi/feature-flag-deep-dive?slide=36

まとめと part2 の予告

質問いただいた内容に関してはだいたいここまで述べたような内容で回答をしたのですが、cdk diff については疑問が残りました。

ヘルプメッセージやドキュメントは、--change-set=false にした場合は ChangeSet を作らないと言っています。

ChangeSet を作らないで replacement を検知する方法などあるのでしょうか...?? 少なくともこの質問をいただくまで、私はそのような方法を知りませんでした。答えは CDK の実装の中にあるはずです。

part2 から本格的にそのへんの実装を読解しつつ、どうやって ChangeSet なしに replacement を検知しているのか、その手段を探っていこうと思います。

脚注
  1. さらに私見を加えるとすれば、IaC 周りで将来どのような具体的困難が生じるかを予測することは困難です。IaC を変更するようなアーキテクチャへの変更は、ビジネス上の要請(つまり外部要因)によって必要に迫られる場合も多いと考えるからです。書き捨てアプリや小規模 Bot などではない、未来の展望があるサービスであれば、できるだけ IaC 固有の問題を生じにくいように作ることが無難であると考えます。問題が発生しうる、あるいは変更が予想されるリソースタイプが見えているのであれば意図的に物理名を付ける選択をしても問題は起きないかもしれませんが、そもそもそんな予測を確度高く行うこと自体が難題なのでは...?と考えています。 ↩︎

  2. 2024/08 現在では OIDC Provider との連携がベータでサポートされており、IAM Role ベースの手段もあるようです https://vercel.com/docs/security/secure-backend-access/oidc/aws ↩︎

  3. ベータではあるものの、Vercel にも Feature flag の名を持つ機能があるようですね https://vercel.com/docs/workflow-collaboration/feature-flags ↩︎

Discussion