はじめに
こんにちは、ピクトリンク事業部開発部開発2課のkitajimaです。マイブームは冷凍ブルーベリーです。
先日、AWS Summit Japan 2024に参加してきました。
CDKブースのAWSアーキテクトの方にCDKのリファクタリングに関する助言をいただいたので、共有させていただきます。 また、助言をもとに、社内のプロダクトをリファクタリングする場合を想定して流れを紹介します。
現状
社内のCDKプロジェクトがあり、現在以下のような状況です。
- 「CFnを機械的に参考にしたL1 Constructの部分」と、「一から記述したL2 Constructの部分」が混在している
- L1 Constructが残って嫌なこと
- Lambdaのコードがインラインで書かれている部分があるため、メンテナンス性が低いと思っている
- 設定を全て明示的に記述するため、"いい感じに"設定してくれるというCDKの恩恵を生かしきれていないと思っている
そのため、L1 Constructを安全にL2 Constructに移行(リファクタリング)したいと考えていました。 しかしIaCの変更は影響範囲も大きく、とっつきにくいと感じていました。
そんな中、AWS Summit 2024でAWSのアーキテクトの方々がなんでも答えてくださるブースがあり、相談をしてみました。
安全にCDKをリファクタリングする3ステップ
AWSアーキテクトの方から、L1 ConstructをL2 Constructに移行する3ステップを教えていただきました。
- 置換されると困るリソースを特定
- 書き換え
- 差分を確認
以下、僕たちのプロダクトでも使用しているCognitoのユーザプールのリソースを例に説明します。 現在のL1 Constructが以下のようになっているとします。(実際のプロダクトコードとは一部異なります)
new cognito.CfnUserPool(this, 'FooUserPool', { userPoolName: `${props.stackEnv}-${props.projectName}-foo-user-pool`, // optionは以下略 });
1. 置換されると困るリソースを特定
まず重要なのが、置換されると困るリソースを特定することだと教えていただきました。
そのままL2 Constructに書き直した場合、リソースのreplace(今あるリソースが削除され、全く別のリソースが新たに作成される)が発生してしまいます。
new UserPool(this, 'FooUserPool', { userPoolName: `${props.stackEnv}-${props.projectName}-foo-user-pool`, // optionは以下省略 });
cdk diff
コマンドで差分を確認すると、リソースのreplaceが発生することがわかります。
$ cdk diff Stack dev-foo-cognito-stack Resources [-] AWS::Cognito::UserPool FooUserPool FooUserPool destroy [+] AWS::Cognito::UserPool FooUserPool FooUserPool25D4C015 [~] AWS::Cognito::UserPoolClient userPoolClient userPoolClient replace └─ [~] UserPoolId (requires replacement) └─ [~] .Ref: ├─ [-] FooUserPool └─ [+] FooUserPool25D4C015
L1 Constructの論理IDはFooUserPool
ですが、L2 Constructの論理IDはFooUserPool25D4C015
という、suffixにハッシュが付与されている形式になり、同一リソースとして扱われませんでした。
replaceが発生するとそのリソースが保持しているデータが失われてしまうため、特に以下のようなリソースは注意が必要です。
- ストレージサービス
- データベース
- その他状態を持つリソース(Cognitoユーザプールも含まれます)
2. 書き換え
置換されると困るリソースの場合は、L2 Constructで書きつつ、論理IDのoverrideを行うことで、replaceを回避することができます。
const userPool = new UserPool(this, 'FooUserPool', { userPoolName: `${props.stackEnv}-${props.projectName}-foo-user-pool`, // optionは以下省略 }); (userPool.node.defaultChild as cognito.CfnUserPool).overrideLogicalId('FooUserPool');
class CfnResource (construct) · AWS CDK
これで差分を確認すると、リソースのreplaceが発生していません。 同一のリソースのオプションのみを変更するという扱いになっていることがわかります。
$ cdk diff Stack dev-foo-cognito-stack Resources [~] AWS::Cognito::UserPool FooUserPool FooUserPool ├─ [+] EmailVerificationMessage │ └─ The verification code to your new account is {####} ├─ [+] EmailVerificationSubject │ └─ Verify your new account ├─ [-] UsernameConfiguration │ └─ {"CaseSensitive":true} ...(一部抜粋)
3. 差分を確認
上のステップで既に登場しましたが、差分が期待通りのものであるかを確認することが重要です。 差分を確認することで、品質上のリスクを抑えることができます。
差分を確認する方法は以下を参照ください。
cdk diff
コマンドスナップショットテスト
基本的に「リファクタリング」なので、"no differences"(実際にデプロイされているリソースとの差異が無いこと)が理想です。
$ cdk diff
Stack dev-foo-cognito-stack
There were no differences
✨ Number of stacks with differences: 0
移行の方針を考える
アーキテクトの方の助言及び社内の対象プロダクトを実際に眺めた結果を踏まえ、以下のような方針で進めてみたいと思いました。
基本的に全て論理IDのoverrideを実施する
置換されてもいいリソースであっても、論理IDをoverrideすることで、cdk diff
コマンドやスナップショットテストにより差分を確認しやすくなります。
また、余計なreplaceによるダウンタイムも回避することができます。
よって、基本的に全てのリソースに対して論理IDのoverrideを実施しながらリファクタリングを進めていきたいと思います。
差分は確認するが、必ずしも"no differences"を目指さない
L2 Constructはデフォルトでいい感じの設定値を使ってくれるので、どうしても差分が出てしまう場合もあります。
その場合、その差分がアプリケーションの要件上問題ない差分であるかを判断する必要があります。
$ cdk diff Stack dev-foo-cognito-stack Resources [~] AWS::Cognito::UserPool FooUserPool FooUserPool ├─ [+] EmailVerificationMessage │ └─ The verification code to your new account is {####} ├─ [+] EmailVerificationSubject │ └─ Verify your new account ├─ [-] UsernameConfiguration │ └─ {"CaseSensitive":true} ...(一部抜粋)
例えば、EmailVerificationMessage
というオプションはアプリケーションで使用しないと分かっています。差分は無視しても問題ないと判断できそうです。
逆に、UsernameConfiguration
というオプションは僕たちのアプリケーションの要件上必要な設定であったため、L2 Construct側のoptionで設定を明示して差分を出してはいけないと判断できそうです。
このように、厳密にno difference
を目指すかどうかはそのオプションの内容とアプリケーションの要件をもとに判断する必要があると感じました。
さいごに
さっそくこの記事の執筆と並行して、リファクタリングのPRを上げてみました。
AWS Summit、現地参戦して生の知見を得ることができてよかったです。