はじめまして。去年 2021 年の 5 月に DeNA のソリューション事業本部にサーバーサイドエンジニアとしてジョインさせていただいた佐藤康仁といいます。佐藤はたくさんいるので、愛称としてやっさんと呼ばれています。よろしくお願いします。
この記事では、AWS CDK の導入に悩んでいる人や実際 CDK を V2 に上げようか悩んでいる読者に向けて、改めて AWS CDK のよさであったり、CDK を継続的に運用していくための仕組みであったり、V1 と V2 の差分について実際の体験や検証を通して伝えることを目指します。
はじまり
「マイグレーション」と聞くと変な汗が出て走馬灯のように辛い思い出が浮かぶエンジニアの方も多いかと思います。僕も経験としてプログラミング言語のマイグレーションやデータマイグレーション、EKS クラスタのマイグレーションなどなどを通ってきてそれなりに変な汗が出ます。
僕自身 AWS CDK に触れたのは、ジョインさせていただいた去年が初めてでした。AWS CDK のマイグレーションについては公式ドキュメントがありますが、まだ stable となったばかりで情報も枯れていません。「 2.0.0 でキリがいいしあげてしまおう!」… なんて軽い気持ちで始めましたが、例によって様々な苦労がありました。
workshop に触れた程度の経験で今回、継続的にアップデートをウォッチし、マイグレーション作業をやり遂げることができました。それはひとえにマイグレーションを行う文化がチームにあり、仕組みが出来ていたからだと思います。
その辺りの実体験を踏まえ、AWS CDK とは何かというところから v2 のことや検証したことを残せればと思います。
AWS CDK について
AWS CDK とは
AWS Cloud Development Kit ( 公式リンク ) 略して CDK です。AWS のサービスの一つであり、プログラミング言語によってクラウドアプリケーションリソースが定義できることが特徴です。僕の所属しているチーム内では TypeScript で CDK を利用しています。プロビジョニングには、同じく AWS のサービスである CloudFormation ( 公式リンク )を使用することができます。
AWS CDK は OSS として公開されていて現在進行系でコントリビュートが活発です。
https://github.com/aws/aws-cdk
CloudFormation とは
プロビジョニングに利用できる ClouFormation は独自のテンプレート( JSON もしくは YAML )によってインフラコードを管理しています。例として S3 bucket を作るテンプレートは、以下のような感じです。
...
...
"SampleBucket7F6F8160": Object {
"DeletionPolicy": "Retain",
"Properties": Object {
"BucketEncryption": Object {
"ServerSideEncryptionConfiguration": Array [
Object {
"ServerSideEncryptionByDefault": Object {
"SSEAlgorithm": "AES256",
},
},
],
},
"BucketName": "sample-bucket",
"VersioningConfiguration": Object {
"Status": "Enabled",
},
},
"Type": "AWS::S3::Bucket",
"UpdateReplacePolicy": "Retain",
},
...
...
CloudFormation の利点として Stack 単位のロールバックが上げられます。これが terraform などにはない利点で、安全にクラウドアプリケーションをプロビジョニングできる点が嬉しいところです。しかし、上述のテンプレートは決して人が読み書きしやすい構造であるとはいい切れません。
AWS CDK の利点① - Stack を TypeScript で表現できる
上記 CloudFormation を AWS CDK で書くと以下のようになります。
new s3.Bucket(this, 'SampleBucket', {
bucketName: 'sample-bucket',
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
publicReadAccess: false,
});
両者を比べた場合のわかりやすさは一目瞭然です。これはクラウドアプリケーションで様々な AWS リソースを利用し、アーキテクチャが複雑化してくるほどこのわかりやすさは大きなメリットとなります。
また、プログラミング言語として表現されている利点として、型としてオプションも表現されている点が挙げられます。「 S3 には暗号化の機能があり、暗号化には KMS の key が必要である。」といったことをコードから読み取ることができます。
export interface BucketProps {
...
...
readonly encryption?: BucketEncryption;
...
...
readonly encryptionKey?: kms.IKey;
...
...
こちらについても型を読みに行けばサービスの仕様が掴めるという点はクラウドアプリケーション構築、運用の面でメリットとなります。
AWS CDK の利点② - Stack に対するユニットテストを書ける
プログラミング言語としてクラウドアプリケーションを定義できるということで、そちらに対するテストコードを用意することができる点も大きなメリットです。AWS 公式のテストドキュメントは以下となります。
https://docs.aws.amazon.com/cdk/v2/guide/testing.html
ドキュメントにある通り、TypeScript の場合 jest に絡めてテストが書けます。先程の S3 Bucket の用意のテストは以下の様に書けます。
const app = new cdk.App();
const stack = new SampleStack(app, 'MyTestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties("AWS::S3::Bucket", {
VersioningConfiguration: {
Status: "Enabled",
},
});
また、CloudFormation に変換された定義をスナップショットを残すことができてこれがマイグレーションにおいても大役立ちしてくれます。
+ expect(template.toJSON()).toMatchSnapshot();
このようにスナップショットを比較する記述を書くとテスト時に test/__snapshots__/**.test.ts.snap
に吐かれたスナップショットと cdk code を比較してくれます。スナップショットは初回および jest の -u オプションによって作成および更新されます。
new s3.Bucket(this, 'SampleBucket', {
bucketName: 'sample-bucket',
encryption: s3.BucketEncryption.S3_MANAGED,
- versioned: true,
+ versioned: false,
publicReadAccess: false,
});
このような差分があったときテスト結果としては以下のようになります。
- Snapshot - 3
+ Received + 0
@@ -37,13 +37,10 @@
},
},
],
},
"BucketName": "sample-bucket",
- "VersioningConfiguration": Object {
- "Status": "Enabled",
- },
このスナップショットが同一であれば、更新による影響はなしとなりますし、変更に意図的でないものが含まれている場合にプロビジョニング前に異変に気付くこともできます。どこまでテストを書くかといった辺りはケースバイケースとなりますが、スナップショット一つ利用するだけでマイグレーションだけでなく、日常的な運用も安全に進めることができます。
改めて AWS CDK のメリットは「人間に優しい(分かりやすい。)定義」「型からサービス仕様が把握できるところ」「テストコードを書けるところ。」となっていて、何より TypeScript で書けるのが楽しいです。エンジニアの日々の作業において、効率化や保守性といった原動力として “楽しい” は大きなメリットであり、そこも含め AWS CDK はいいなと思っています。
AWS CDK アップデートのウォッチについて
今回の v2.x 化を含め CDK の更新は継続的にウォッチされています。ウォッチには npm info {package} version
のコマンドが利用されています。
# 最新 ver を取得する
npm info aws-cdk version
2.8.0
# 1.x 系の一覧を取得する
npm info aws-cdk@v1 version
...
...
aws-cdk@1.138.2 '1.138.2'
aws-cdk@1.139.0 '1.139.0'
aws-cdk@1.140.0 '1.140.0'
# (応用) 1.x の最新 ver を取得する
npm info aws-cdk@1 version | sort --version-sort -r | head -n 1 | cut -d "'" -f 2
1.140.0
また対象バージョンの release ページにリクエストすることで、リリース内容を取ることができます。
❯ curl -fsS https://api.github.com/repos/aws/aws-cdk/releases/tags/v1.140.0 | jq -r .body
### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES
* **apigatewayv2:** `HttpIntegrationType.LAMBDA_PROXY` has been renamed to `HttpIntegrationType.AWS_PROXY`
* **iot:** the class `FirehoseStreamAction` has been renamed to `FirehosePutRecordAction`
...
...
私が所属するチームでは、これらを組み合わせ「最新の ver があったら PR が投げられる。」といったことが自動化されています。これによって継続的なアップデートウォッチが可能となっています。実現方法としては他にあるかもしれませんし細かい部分ではありますが、こういった辺りの仕組みや更新に当たり前に追従する文化といったところがアサインされて最初に感銘を受けた部分でもありました。
この仕組自体は汎用的に様々なライブラリのウォッチに利用できそうです。
実際にアップデート時差分ありとして、引っかかったアップデート例をいくつか紹介
v1.130.0 より、 fix(elasticloadbalancingv2): always set stickiness 動作内容としては、ELB のスティッキーセッションの有効化フラグを常に明示する。という内容です。コメントにある通り CloufFormation の挙動として明示されていない場合に意図的ではない挙動となることがあるようです。
v1.136.0 より、 feat(aws-ecs): expose environment from containerDefinition 対応内容自体は、ECS の containerDefinition の environment の扱いを変更するものですが、差分として空配列が明示的に undefined 扱いとなるようになりました。
‼️ NOTICE: aws:asset:original-path is non deterministic
内容としては 「cdk の asset が含んでいるメタデータである original-path を廃止する。」というものです。cdk の asset としては、ビルドされた Lambda などを含みます。original-path は、ビルドしたパスのことです。これが絶対パスとなっていて、ローカルであれば /Users/yasuhito.sato/project/cdk/assets...
, CircleCI であれば /workspace/project...
のように保存されます。デプロイ環境に依存したメタデータとなるため、開発者の都合でローカルでデプロイした後、実質的な差分がないのに CI 環境では差分ありとして表示されてしまいます。これが不評で廃止しようといったものになります。original-path を「足そう。」といった流れから「やっぱり廃止しよう。」といったところまでかなり短いスパンで Issue がたち、ここのドタバタ感を見ているのも楽しかったです。余談ですが、僕が所属しているチームでも original-path は不評でした(笑)
上記は PR を見てわかる通り、実装としてはかなり簡単なものです。他にも様々ありまして、コントリビュートチャンスは結構転がっていそうだなと感じれて面白いです。
AWS CDK v2.x で変わること
そんな AWS CDK ですが、v2 はかなり歴史が長めで v2.0.0-alpha.0 として tag が切られ日の目を浴びたのは 2020.12.11 のこと になります。それから stable までおよそ一年と長いです。こういったプロジェクト変遷を含め外から追えるのは OSS の面白いところですね。そんな v2.x ですが、大きく v1.x との変更点は以下の 3 点があります。
library が aws-cdk-lib に集約
元々 aws-cdk 関連モジュールは、リソース単位で分かれていました。例えば以下のような感じです。
- EC2 を扱うモジュール
@aws-cdk/aws-ec2 module
- IAM を扱うモジュール
@aws-cdk/aws-iam module
- …… etc
これらは、別々で追加する必要があり、package.json が膨れ上がるのが常でした。v2 となってこれが aws-cdk-lib にまとまりました。
- EC2 を扱うモジュール
aws-cdk-lib/aws-ec2
- IAM を扱うモジュール
aws-cdk-lib/aws-iam
- …… etc
これは明確にメリットであり、v2 にマイグレーションするポジティブな面になります。
deprecated API が使用不可に
時代の流れとともに deprecated となるものが多く生まれるのはエンジニアの世界においてはあるあるなのかなと思います。AWS CDK においての deprecated は一覧化できる形で管理されていて、それは以下になります。
https://github.com/aws/aws-cdk/blob/master/DEPRECATED_APIs.md
CommonMetricOptions.dimensions の実態が hash map なので dimensionsMap の property の方を使おうみたいな細かいものから、後述の feature flag の有効化によって冗長な表現となるので削ろうといったものまで様々な定義があり、v2 となりこれが完全に廃止となるため対応が必要となります。
feature 機能がデフォルトで有効に
AWS CDK の RFC は OSS 上で議論されていて、
feature flag に関してもその RFC の内の一つ
です。
具体的な feature flag の一覧は、
aws-cdk 上で管理
されています。
v1 から v2 の世界観の違い
feature flag の @aws-cdk/core:newStyleStackSynthesis
により CDK の実行に大きく差分が出ます。 synthesize
とは CDK の用語で「プログラミング言語 to CloudFormation テンプレート」変換を行う処理のことであり cdk サブコマンドとしても提供されています。
❯ yarn cdk synthesize SampleStack
yarn run v1.22.10
warning package.json: No license field
$ cdk synthesize SampleStack
Resources:
SampleBucket7F6F8160:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
BucketName: sample-bucket
...
...
CDK だけ開発者が書いて synthesize で CloudFormation テンプレートを吐いてインフラ管理者に渡せばいいみたいに責務分担できたりするのもいいところです。この feature flag は newStyle とある通り Synthesize 処理全般に影響があり、これが通常全スタックに影響を与えます。具体的には各種スタックが Synthesizer というオプションを持つようになります。
export interface StackProps {
...
...
/**
* Synthesis method to use while deploying this stack.
*
* @default - `DefaultStackSynthesizer` if the `@aws-cdk/core:newStyleStackSynthesis` feature flag
* is set, `LegacyStackSynthesizer` otherwise.
* @stability stable
*/
readonly synthesizer?: IStackSynthesizer;
これにより CloudFormation に対するプロビジョニング挙動が大きく整理されます。具体的な概要図としては以下のように CDK が管理する各種 Role を経由しプロビジョニングされます。これは図解でも一見複雑になった印象を受けてしまいますが、権限の管理を CDK 管理の Role に一任することができ、こちらで管理するものが少なくなる分権限管理の面で優位です。各種 Role 詳細については、 AWS 公式ドキュメント が詳しいです。
CDK v2 にする時点で再度 bootstrap を求められますが、これら CDK 管理のリソースはその再 bootstrap のときに生成されます。 bootstrap は CloudFormation Template として aws-cdk 上で管理 されていて、各種 Role の名前や権限などこの bootstrap 時のプロパティから細かく調整可能になっています。また、Role を作ったりする都合 bootstrap については 強い権限 を持った方による操作が必要となります。もし都合上開発者が CreateRole 権限がないといった形となる場合に、上長など強い権限を持つ人との協力が必須となります。
DefaultStackSynthesizer は以下のように定義されていて、上記で生成されたリソースを経由してプロビジョニングがデフォルトで行われるようになるという流れです。
export declare class DefaultStackSynthesizer extends StackSynthesizer {
...
...
static readonly DEFAULT_CLOUDFORMATION_ROLE_ARN = "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region}";
DefaultStackSynthesizer 自体は明示できるオプションとなるため、意図的に利用したい Role を変更したりする戦略を取ることもできますが、多くのインフラを Stack 単位で分割して管理する手法を取っているため、synthesizer を都度設定する運用だと事故のリスクが増えたり保守面に懸念点が残ったため、DefaultStackSynthesizer をそのまま利用する戦略を今回は取りました。
is not authorized to perform: iam:PassRole !?
変更後の世界観の図に刺した通り、v2 の世界線においては、手元から何らかの方法で cdk exec role を扱いその上で cdk managed roles に AssumeRole する世界観となります。分かってしまえば簡単なのですが、AssumeRole 権限を cdk exec role にもたせていない時、エラー文言として違う文言が出てしまいハマってしまうことがありました。
エラー内容例としては以下となります。
...is not authorized to perform: iam:PassRole on resource: arn:aws:...
AssumeRole と PassRole は IAM の難しい概念のうちの一つでこちらを説明している有名な記事としては Classmethod さんの コチラ の記事があります。AssumeRole は特定の Role に成り代わって権限行使することであり、PassRole は文字通り特定のリソースへ Role を Pass することです。
v2 の世界線においては、最初の cdk managed roles への AssumeRole が失敗した場合でも warning message が出るのみでそのまま cdk exec role による旧デプロイを試みます。この場合に cdk exec role の権限を絞っていた場合に権限不足が起こります。つまり iam:PassRole でコケたのは今回「たまたま最終的にコケたのが iam:PassRole 」となるのですが、何の権限が必要で何が不要なのかといった整理においてこの挙動にはかなり悩まされました。
もしかしたら既にあるのかもしれませんが、v2 の世界観において AssumeRole の権限がなく Assume に失敗した場合、明示的にプロビジョニングに失敗するオプションなりが欲しいところですね。。
v2 後の世界観に対する検証
DefaultStackSynthesizer をそのまま利用するにあたり、いくつかの検証を行いました。検証を進めるにあたって、検証用の AWS Account という本番、開発環境から隔離された環境で検証を行いました。このアカウントは組織として事業部毎に配布されているもので、エンジニアは(少なくとも僕の所属するチームでは)」自由に検証アカウントを利用した検証を行うことができます。好きに作って壊してができ、本番や開発アカウントに影響なく進められるので「検証の文脈では検証アカウントで進める。」というのは他のケースにおいても有効だなと思いました。
検証前に DefaultStackSynthesizer をそのまま利用する場合の世界観の概要
- cloudformation exec role はデフォルトで AdministratorAccess を持っている
- cdk bootstrap 時に –cloudformation-execution-policies オプションから任意のポリシーをアタッチできる
- 任意のポリシーはデフォルトでどの IAM からも扱うことができる
1. cloudformation exec role はデフォルトで AdministratorAccess を持っている
bootstrap をプロパティを指定せずそのまま使うと cloudformation exec role が AdministratorAccess を持ってしまいます。 https://github.com/aws/aws-cdk/blob/v2.0.0/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml#L482
ManagedPolicyArns:
Fn::If:
- HasCloudFormationExecutionPolicies
- Ref: CloudFormationExecutionPolicies
- Fn::If:
- HasTrustedAccounts
# The CLI will prevent this case from occurring
- Ref: AWS::NoValue
# The CLI will advertise that we picked this implicitly
- - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess"
つまり、デフォルトの状態だと cloudformation exec role に AssumeRole できる権限さえあれば CDK を通して何でもできてしまいます。
2. cdk bootstrap 時に –cloudformation-execution-policies オプションから任意のポリシーをアタッチできる
あくまでもデフォルトでそうというだけで、オプションから任意のポリシーをアタッチできます。
bootstrap のオプションの詳細については、公式ドキュメントが詳しいです。
https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html
specifies the ARNs of managed policies that should be attached to the deployment role assumed by AWS CloudFormation during deployment of your stacks. At least one policy is required; otherwise, AWS CloudFormation will attempt to deploy without permissions and deployments will fail.
とだけ書かれていますが、実際にはアセットを参照するための権限であったり、bootstrap の version を取得するための権限であったりが必要になります。色々と試したんですが最終的には Readonly を持たせるに落ち着きました。任意のポリシーを設定する場合には CDK として生成したい対象の権限とは別に参照権限も必要であるという点は注意が必要です。
3. 任意のポリシーはデフォルトでどの IAM からも扱うことができる
これは cdk の role にアタッチできるポリシーに限ったことではありませんが、任意に用意したポリシーは何からでも利用できてしまいます。 (ほとんどないエッジケースにはなりますが)CDK の文脈で少し強めの権限を付けていたものを便利だからと別の Role に紐付けて利用を続けた結果意図的ではない権限行使を許可してしまって不具合に繋がるといった点は考えられます。簡単に防ぐことができたので明示しておく方が無難そうです。
検証① AdministratorAccess を持った cdk role で実際なんでもできてしまうのか?
CDK を通すことで、なんでもできてしまうというのがどういうことがというと例えば以下のような強い権限を持つユーザーを作ってしまうといった deploy が通ってしまいました。
const user = new iam.User(this, 'AdminUser');
user.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"));
内部的なレポジトリにおいて意図的に強い権限を行使してイタズラするようなケースは早々ないので運用でカバーでもなんとかなる気はしますが防いでおいて任意のポリシーをアタッチする方がより安全そうです。
検証② 任意のポリシーをアタッチしたとして、任意のポリシー以上のことが本当にできないのか?
例えば、iam:CreateUser の権限を持たせないようにすれば上記のような deploy は通りませんでした。 以下のようなエラーが出力され、デプロイはロールバックされます。
is not authorized to perform: iam:CreateUse
検証③ 任意のポリシーを利用できる Role を制限するには?
元々 CDK v1 の世界観においては、cdk を実行する Role が直接 Cloudformation を見に行く世界観でした。そのため、特定の権限が cloudformation 上でのみ行使されることを condition から明示することが出来ました。具体的には以下のようなポリシーになります。
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:CalledViaFirst": "cloudformation.amazonaws.com"
}
}
これを行うことで、管理しているポリシーが cloudformation でのみ扱われることを明示していたのですが、v2 となってこれが正しく動かなくなりました。AWS サポートさんによると、場合によって AWS Internal なサービスを通る場合があるようになり cloudformation 指定だけではいけなくなったようです。
...-us-east-1/AWSCloudFormation is not authorized to perform: iam:CreateUser on resource: arn:...
管理しているポリシーに対する明示的なルールは出来なく暗黙的にドキュメントと照らし合わせるしかないのかと落ち込んでいたところ AWS サポートさんに policy 利用者を制限する方法を教えていただきました。例えば以下のように明示してあげることで、「cdk 管理の cloudformation exec role からしか利用されない。」といったことが明示できます。
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::${aws-account-id}:role/cdk-hnb659fds-cfn-exec-role-*"
}
}
condition がある状態であっても attach 自体は出来てしまいますが、実行時に正しくエラーとなる点を確認できました。
v2 マイグレーション戦略
諸々検証が済み v2 にあげて問題なしと判断したところで改めて戦略です。
今回は、プロジェクト内で初めての v2 へのマイグレーションとなったため段階的に慎重にマイグレーションを行う戦略を取りました。また、feature flag については AWS CDK が公式に「これがデフォルトの動作である」といったことを表明している点や、今後の開発において feature flag が有効である前提の機能が追加される可能性があることも加味して有効とするよう対応しました。
- deprecated な API を使わないようにする
- 差分が発生しうる feature flag を段階的に有効にする
- v2.x に上げる
deprecated な API を使わないようにする
v1 を利用している場合でも、 v1.133.0 の対応から deprecated が 明示的に warning として出る対応 が加わりました。v1.x 系のまま 1.133 まで上げることで例えば test で warning が出るようになるため、deprecated な API を意図的に倒すことができます。今回のマイグレーション対応においては、細かい対応が多くほとんどがスナップショットに影響を与えないものでした。
差分が発生しうる feature flag を段階的に有効にする
主に以下の feature flag に引っかかりました。いずれも一つずつ差分を確認しデプロイという流れで対応しました。
@aws-cdk/core:newStyleStackSynthesis
@aws-cdk/aws-s3:grantWriteWithoutAcl
@aws-cdk/aws-kms:defaultKeyPolicies
v2.x に上げる
公式のリンクが コチラ になっていて、主に package.json の更新および Construct の分離が大きな差分として挙げられます。上記より deprecated と feature flag の対応を済ませることで snapshot に変更がないようにアップデートすることが可能でした。
まとめ
お疲れさまでした。以上が実際に v2 化を行った体験となります。要所、要所で上長やチームメンバー、AWSサポートの方にお世話になりました。この場を借りてありがとうございました。
この記事には書ききれなかった詰まりポイントや難しいところは多々ありました。
マイグレーションを終えた後、さらに継続的にウォッチし続けるか一旦止めるのかといった観点で話は続きますが、改めて大事なのは「バージョンアップに追従しやすくするための仕組みがチームにある」ことと「マイグレーションを恐れない文化がある。」ことなのかなと思います。
幸いなことに今僕が所属させていただいている部署はそういった文化や仕組みが根付いていて、今回のマイグレーションも辛さはあったものの周りのサポートもありなんとかやり遂げることができました。
また、アップデートのウォッチの中で様々なエンジニアの思惑があっていろいろな考えに触れられたことも面白かったです。
AWS CDK を触ったことがないけど興味が湧いたという方は公式にワークショップがあり、こちらがおすすめです。
AWS CDK でインフラ構築していてちょうど v2 化に悩んでいたといった方にも、少しでも参考になれば幸いです。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。