Slide 1

Slide 1 text

API Gatewayの自動デプロイができない! → 解消したらCDKコントリビュートできた話 クラスメソッドのCDK事情大公開スペシャル#1 塚本太朗 1

Slide 2

Slide 2 text

自己紹介 塚本太朗 リテールアプリ共創部 エンハンスチーム 2023年5月入社(1年半!) 最近ハマっていること HUNTER×HUNTERをちゃんと読む 継承戦編が難しすぎる X: @9Lgo1 2

Slide 3

Slide 3 text

💬 今日話すこと3行 チームで困っていた謎の挙動を解決したら… → CDK調査の勘所が身につき… → CDKコントリビュートもできた! 3

Slide 4

Slide 4 text

📇 目次 どんな問題があったか? 問題の原因を紹介 解消方法 CDKの挙動調査Tips CDKコントリビュートを紹介 まとめ 4

Slide 5

Slide 5 text

cdk deployしても、API Gatewayが更新されな い! 😞 5

Slide 6

Slide 6 text

😞 どんな問題があったか? CDKでデプロイ (GitHub Actions) API Gatewayが自動デプロイされないので、手動でデプロイを行う CDKでデプロイ 〜 手動デプロイの間に瞬断が発生する 定期的に 🌙深夜リリースが必要だった 6

Slide 7

Slide 7 text

😀 改善した結果 深夜リリースの対応が不要に! メンバーのリソースを削減 深夜リリースによる身体的ダメージを抑制 7

Slide 8

Slide 8 text

原因はスタック分割! ✂️ 8

Slide 9

Slide 9 text

🔍 原因 API GatewayのリソースとLambdaのリソースが別スタックで定義さ れていた 9

Slide 10

Slide 10 text

🔍 原因: 実装ソース(一部抜粋) /* ApiGatewayのStack */ export class ApiStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { const api = new RestApi(this, "base-api", {}); } } /* LambdaのStack */ export class LambdaStack extends cdk.Stack { constructor(scope: Construct, id: string, props: LambdaStackProps) { // ⭐ API GatewayをfromXXXXAttributeで取得する const apiGateway = RestApi.fromRestApiAttributes(this, "api", { restApiId: props.apiId, rootResourceId: props.rootId, }); // API GatewayとLambdaの紐付けを行う apiGateway.root.addResource("hoge").addMethod("GET", hogeIntegration); } } 10

Slide 11

Slide 11 text

🔍 原因: 実装ソース(一部抜粋) const api = new RestApi(this, "base-api", {}); /* ApiGatewayのStack */ export class ApiStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { } } /* LambdaのStack */ export class LambdaStack extends cdk.Stack { constructor(scope: Construct, id: string, props: LambdaStackProps) { // ⭐ API GatewayをfromXXXXAttributeで取得する const apiGateway = RestApi.fromRestApiAttributes(this, "api", { restApiId: props.apiId, rootResourceId: props.rootId, }); // API GatewayとLambdaの紐付けを行う apiGateway.root.addResource("hoge").addMethod("GET", hogeIntegration); } } 10

Slide 12

Slide 12 text

🔍 原因: 実装ソース(一部抜粋) // ⭐ API GatewayをfromXXXXAttributeで取得する const apiGateway = RestApi.fromRestApiAttributes(this, "api", { restApiId: props.apiId, rootResourceId: props.rootId, }); /* ApiGatewayのStack */ export class ApiStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { const api = new RestApi(this, "base-api", {}); } } /* LambdaのStack */ export class LambdaStack extends cdk.Stack { constructor(scope: Construct, id: string, props: LambdaStackProps) { // API GatewayとLambdaの紐付けを行う apiGateway.root.addResource("hoge").addMethod("GET", hogeIntegration); } } 10

Slide 13

Slide 13 text

🔍 原因: 実装ソース(一部抜粋) // API GatewayとLambdaの紐付けを行う apiGateway.root.addResource("hoge").addMethod("GET", hogeIntegration); /* ApiGatewayのStack */ export class ApiStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { const api = new RestApi(this, "base-api", {}); } } /* LambdaのStack */ export class LambdaStack extends cdk.Stack { constructor(scope: Construct, id: string, props: LambdaStackProps) { // ⭐ API GatewayをfromXXXXAttributeで取得する const apiGateway = RestApi.fromRestApiAttributes(this, "api", { restApiId: props.apiId, rootResourceId: props.rootId, }); } } 10

Slide 14

Slide 14 text

🔍 原因: 実装ソース(一部抜粋) /* ApiGatewayのStack */ export class ApiStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { const api = new RestApi(this, "base-api", {}); } } /* LambdaのStack */ export class LambdaStack extends cdk.Stack { constructor(scope: Construct, id: string, props: LambdaStackProps) { // ⭐ API GatewayをfromXXXXAttributeで取得する const apiGateway = RestApi.fromRestApiAttributes(this, "api", { restApiId: props.apiId, rootResourceId: props.rootId, }); // API GatewayとLambdaの紐付けを行う apiGateway.root.addResource("hoge").addMethod("GET", hogeIntegration); } } 10

Slide 15

Slide 15 text

⛏️さらに深掘り: CDKのソースを追ってみる Method, Resourceの作成時、デプロイメントのIDを更新するように なっている。 aws-cdk-lib/aws-apigateway/lib/method.tsの抜粋 export class Method extends Resource { constructor(scope: Construct, id: string, props: MethodProps) { // RestApiのlatestDeploymentがある時... const deployment = props.resource.api.latestDeployment; if (deployment) { deployment.node.addDependency(resource); // ロジカルIDを変更する deployment.addToLogicalId({ method: { ...methodProps, integrationToken: bindResult?.deploymentToken, }, }); } 11

Slide 16

Slide 16 text

⛏️さらに深掘り: CDKのソースを追ってみる Method, Resourceの作成時、デプロイメントのIDを更新するように なっている。 aws-cdk-lib/aws-apigateway/lib/method.tsの抜粋 // RestApiのlatestDeploymentがある時... const deployment = props.resource.api.latestDeployment; if (deployment) { export class Method extends Resource { constructor(scope: Construct, id: string, props: MethodProps) { deployment.node.addDependency(resource); // ロジカルIDを変更する deployment.addToLogicalId({ method: { ...methodProps, integrationToken: bindResult?.deploymentToken, }, }); } 11

Slide 17

Slide 17 text

⛏️さらに深掘り: CDKのソースを追ってみる Method, Resourceの作成時、デプロイメントのIDを更新するように なっている。 aws-cdk-lib/aws-apigateway/lib/method.tsの抜粋 // ロジカルIDを変更する deployment.addToLogicalId({ method: { ...methodProps, integrationToken: bindResult?.deploymentToken, }, }); export class Method extends Resource { constructor(scope: Construct, id: string, props: MethodProps) { // RestApiのlatestDeploymentがある時... const deployment = props.resource.api.latestDeployment; if (deployment) { deployment.node.addDependency(resource); } 11

Slide 18

Slide 18 text

⛏️さらに深掘り: CDKのソースを追ってみる Method, Resourceの作成時、デプロイメントのIDを更新するように なっている。 aws-cdk-lib/aws-apigateway/lib/method.tsの抜粋 export class Method extends Resource { constructor(scope: Construct, id: string, props: MethodProps) { // RestApiのlatestDeploymentがある時... const deployment = props.resource.api.latestDeployment; if (deployment) { deployment.node.addDependency(resource); // ロジカルIDを変更する deployment.addToLogicalId({ method: { ...methodProps, integrationToken: bindResult?.deploymentToken, }, }); } 11

Slide 19

Slide 19 text

⛏️さらに深掘り: CDKのソースを追ってみる fromRestApiAttributeで取得した IRestApi には latestDeployment が設定されていない aws-cdk-lib/aws-apigateway/lib/restapi.tsの抜粋 export class RestApi extends RestApiBase { public static fromRestApiAttributes(scope: Construct, id: string, attrs: RestApiAttributes): IRestApi { class Import extends RestApiBase { public readonly restApiId = attrs.restApiId; public readonly restApiName = attrs.restApiName ?? id; public readonly restApiRootResourceId = attrs.rootResourceId; public readonly root: IResource = new RootResource(this, {}, this.restApiRootResourceId); } return new Import(scope, id); } } 12

Slide 20

Slide 20 text

⛏️さらに深掘り: CDKのソースを追ってみる 再び Methodクラスの実装 export class Method extends Resource { constructor(scope: Construct, id: string, props: MethodProps) { // RestApiのlatestDeploymentがある時... const deployment = props.resource.api.latestDeployment; if (deployment) { // ⭐️ここに入ってこない! deployment.node.addDependency(resource); // ロジカルIDを変更する deployment.addToLogicalId({ method: { ...methodProps, integrationToken: bindResult?.deploymentToken, }, }); } } } 13

Slide 21

Slide 21 text

⛏️さらに深掘り: CDKのソースを追ってみる 再び Methodクラスの実装 // RestApiのlatestDeploymentがある時... const deployment = props.resource.api.latestDeployment; if (deployment) { // ⭐️ここに入ってこない! export class Method extends Resource { constructor(scope: Construct, id: string, props: MethodProps) { deployment.node.addDependency(resource); // ロジカルIDを変更する deployment.addToLogicalId({ method: { ...methodProps, integrationToken: bindResult?.deploymentToken, }, }); } } } 13

Slide 22

Slide 22 text

⛏️さらに深掘り: CDKのソースを追ってみる 再び Methodクラスの実装 export class Method extends Resource { constructor(scope: Construct, id: string, props: MethodProps) { // RestApiのlatestDeploymentがある時... const deployment = props.resource.api.latestDeployment; if (deployment) { // ⭐️ここに入ってこない! deployment.node.addDependency(resource); // ロジカルIDを変更する deployment.addToLogicalId({ method: { ...methodProps, integrationToken: bindResult?.deploymentToken, }, }); } } } 13

Slide 23

Slide 23 text

💡 解決方法 毎回新しいDeploymentを作成するため、専用のスタックを用意した ※ 0から作るなら、この実装はあまりオススメできません 14

Slide 24

Slide 24 text

💡 解決方法: コード紹介 // ApiDeploymentStack export class ApiDeploymentStack extends cdk.Stack { constructor(scope: Construct, id: string, props: ApiDeploymentStackProps) { // api-stackで定義したAPI Gateway const apiGateway = RestApi.fromRestApiAttributes(this, "api", { restApiId: props.apiId, rootResourceId: props.rootId, }); // Deploymentを定義 const deployment = new Deployment(this, "base-api-deployment", { api: apiGateway, retainDeployments: true, }); deployment.addToLogicalId(new Date().toISOString()); // ⭐️ Deploymentを更新 // Stageを定義し、deploymentを紐づける const stage = new Stage(this, "base-api-stage", { deployment, stageName: "prod", }); } } 15

Slide 25

Slide 25 text

💡 解決方法: コード紹介 // api-stackで定義したAPI Gateway const apiGateway = RestApi.fromRestApiAttributes(this, "api", { restApiId: props.apiId, rootResourceId: props.rootId, }); // ApiDeploymentStack export class ApiDeploymentStack extends cdk.Stack { constructor(scope: Construct, id: string, props: ApiDeploymentStackProps) { // Deploymentを定義 const deployment = new Deployment(this, "base-api-deployment", { api: apiGateway, retainDeployments: true, }); deployment.addToLogicalId(new Date().toISOString()); // ⭐️ Deploymentを更新 // Stageを定義し、deploymentを紐づける const stage = new Stage(this, "base-api-stage", { deployment, stageName: "prod", }); } } 15

Slide 26

Slide 26 text

💡 解決方法: コード紹介 // Deploymentを定義 const deployment = new Deployment(this, "base-api-deployment", { api: apiGateway, retainDeployments: true, }); deployment.addToLogicalId(new Date().toISOString()); // ⭐️ Deploymentを更新 // ApiDeploymentStack export class ApiDeploymentStack extends cdk.Stack { constructor(scope: Construct, id: string, props: ApiDeploymentStackProps) { // api-stackで定義したAPI Gateway const apiGateway = RestApi.fromRestApiAttributes(this, "api", { restApiId: props.apiId, rootResourceId: props.rootId, }); // Stageを定義し、deploymentを紐づける const stage = new Stage(this, "base-api-stage", { deployment, stageName: "prod", }); } } 15

Slide 27

Slide 27 text

💡 解決方法: コード紹介 // ApiDeploymentStack export class ApiDeploymentStack extends cdk.Stack { constructor(scope: Construct, id: string, props: ApiDeploymentStackProps) { // api-stackで定義したAPI Gateway const apiGateway = RestApi.fromRestApiAttributes(this, "api", { restApiId: props.apiId, rootResourceId: props.rootId, }); // Deploymentを定義 const deployment = new Deployment(this, "base-api-deployment", { api: apiGateway, retainDeployments: true, }); deployment.addToLogicalId(new Date().toISOString()); // ⭐️ Deploymentを更新 // Stageを定義し、deploymentを紐づける const stage = new Stage(this, "base-api-stage", { deployment, stageName: "prod", }); } } 15

Slide 28

Slide 28 text

🕵️‍♀️ Tips: 役に立った調査方法 CDKのソースコードを解析する(今までの流れ) CloudTrailでデプロイ時の流れを確認する 16

Slide 29

Slide 29 text

🕵️‍♀️ CloudTrailでデプロイ時の流れを確認する デプロイ時のリソース作成・更新などを時系列で確認 正常動作と異常動作の差を確認できる 17

Slide 30

Slide 30 text

🌱 0から作れるなら? RestApiを作成したスタックで、LambdaのIDを参照する 参考: AWS CDKでAPI Gateway+Lambdaを作成する際のベストな スタック構成について Lambdalithの構成を検討する 参考: LambdalithとSingle purpose Lambdaは1つのAPI Gateway で共存できる 18

Slide 31

Slide 31 text

🎉 CDKコントリビュートもできました! https://github.com/aws/aws-cdk/pull/29691 19

Slide 32

Slide 32 text

🎉CDKコントリビュートもできました!: 対応内容 今回紹介した問題が起こるサンプルコードがCDKの公式ドキュメン トで紹介されていた そのサンプルコードの下に注意文言を記載するPRを作成 対応内容はシンプルですが、レビュアーの方とのやり取りで色々勉 強になりました!! 20

Slide 33

Slide 33 text

得られた知見 他スタックから fromXXX などでインポートしたリソースが、予想外 の挙動を引き起こすかもしれない。 CloudTrailを活用して、CDKデプロイ時の挙動を分析できる。 CDKで謎の挙動をする時はソースを見てみる。読みやすいので、意 外と早く原因が特定できる。 21

Slide 34

Slide 34 text

まとめ 慎重にスタック分割をしよう!メリット・デメリットを把握した上 で慎重に行う。 CDKの構成があまり良くない時は、OSSコントリビュートのチャン ス転がってくるかも! とはいえ…サーバーレス構成をCDKで実装する時は、さまざまな制 限に注意して構成を考えよう。 22

Slide 35

Slide 35 text

ご清聴ありがとうございました!質問あればお願 いします! 23

Slide 36

Slide 36 text

参考 aws-cdk AWS CDKでAPI Gateway+Lambdaを作成する際のベストなスタック 構成について LambdalithとSingle purpose Lambdaは1つのAPI Gatewayで共存で きる 今回対応したPR 登壇の元ブログ 24

Slide 37

Slide 37 text

この登壇の元ブログ 25

Slide 38

Slide 38 text

26