🏃‍♂️

AWS CDKにLambda関数を数秒でデプロイするhotswap deployments機能が追加されました

2021/09/10に公開

はじめに

おはようございます、加藤です。AWS CDKのv1.122.0からhotswap deploymentsという機能が追加されました。
通常cdk deployを実行するとCloudFormationをデプロイしますが、このオプションがONの場合はそうせずにSDKでLambda関数をデプロイします。これによってCloudFormationを実行する時間が発生しないため素早くデプロイを行うことができます。

当然この方法によるデプロイを行うとCloudFormationが管理する状態とドリフトが発生してしまいます。この機能は開発環境で素早くデプロイして動作を確認する為のものであり、本番環境では使用してはいけません。(ドキュメントにも明記されています)
hotswap deployments実験的な実装であり今後破壊的な変更が入る可能性があります。

今回検証のために書いたコードは下記で公開しています。

intercept6/cdk-hotswap-demo

hotswap deploymentsを試してみる

デプロイ速度

まず最初に下記のようにでAPI Gateway(HTTP API)と紐づくLambda関数を作成します。ランタイムはNode.js 14を使用しました。

import { CfnOutput, Construct, Stack, StackProps } from "@aws-cdk/core";
import { Code, Function, Runtime } from "@aws-cdk/aws-lambda";
import { join } from "path";
import { HttpApi, HttpMethod } from "@aws-cdk/aws-apigatewayv2";
import { LambdaProxyIntegration } from "@aws-cdk/aws-apigatewayv2-integrations";

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

    const httpApi = new HttpApi(this, "Api");

    httpApi.addRoutes({
      path: "/lambda",
      methods: [HttpMethod.GET],
      integration: new LambdaProxyIntegration({
        handler: new Function(this, "Function", {
          code: Code.fromAsset(join(__dirname, "../src/function")),
          handler: "index.handler",
          runtime: Runtime.NODEJS_14_X,
        }),
      }),
    });
    new CfnOutput(this, "LambdaOutput", { value: httpApi.url! + "lambda" });
  }
}

関数の中身はこのようにメッセージを返却するだけです。

exports.handler = async () => {
  return {
    statusCode: 200,
    body: JSON.stringify({ message: "Hello World from AWS Lambda Node.js" }),
  };
};

これをデプロイし、生成されたAPIにリクエストを投げて動作を確認します。

curl https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda
{"message":"Hello World from AWS Lambda Node.js"}% 

メッセージを少し変更して、--hotswapオプションを使いデプロイしてみます。時間を計測するためにtimeコマンドを被せて実行します。

time npm run cdk -- deploy --hotswap

> cdk-hotswap-demo@0.1.0 cdk
> cdk "deploy" "--hotswap"

⚠️ The --hotswap flag deliberately introduces CloudFormation drift to speed up deployments
⚠️ It should only be used for development - never use it for your production Stacks!
CdkHotswapDemoStack: deploying...
[0%] start: Publishing dcc84381791fed0f9d2d0e26507b2ad6248880b37f360b3546d6225b23fb27f3:current
[100%] success: Published dcc84381791fed0f9d2d0e26507b2ad6248880b37f360b3546d6225b23fb27f3:current

 ✅  CdkHotswapDemoStack

Outputs:
CdkHotswapDemoStack.LambdaOutput = https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:${AWS_ACCOUNT_ID}:stack/CdkHotswapDemoStack/5fdf0c20-11c7-11ec-a5ad-065be1cbeaa1
npm run cdk -- deploy --hotswap  4.79s user 1.07s system 88% cpu 6.659 total

curl https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda
{"message":"🎉Hello World from AWS Lambda Node.js🎉"}% 

約7秒でデプロイが完了しました、普通にCloudFormationをデプロイするのと比較するとありえないぐらいに早いです、ビビります。

変更できる内容

hotswap deploymentsが機能するのはLambda関数のみが変更されている(CDKのコードは一切変更されていない)場合のみです。例えばLambda関数のランタイムバージョンを変更してデプロイすると--hotswapオプションを付けていても通常のデプロイが実行されます。
ポジティブに捉えれば開発中は常に--hotswapオプションを付けてデプロイしていれば、必要に応じてhotswap deploymentsが機能してくれるということです。

time npm run cdk -- deploy --hotswap

> cdk-hotswap-demo@0.1.0 cdk
> cdk "deploy" "--hotswap"

⚠️ The --hotswap flag deliberately introduces CloudFormation drift to speed up deployments
⚠️ It should only be used for development - never use it for your production Stacks!
CdkHotswapDemoStack: deploying...
[0%] start: Publishing dcc84381791fed0f9d2d0e26507b2ad6248880b37f360b3546d6225b23fb27f3:current
[100%] success: Published dcc84381791fed0f9d2d0e26507b2ad6248880b37f360b3546d6225b23fb27f3:current
Could not perform a hotswap deployment, as the stack CdkHotswapDemoStack contains non-Asset changes
Falling back to doing a full deployment
CdkHotswapDemoStack: creating CloudFormation changeset...





 ✅  CdkHotswapDemoStack

Outputs:
CdkHotswapDemoStack.LambdaOutput = https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:${AWS_ACCOUNT_ID}:stack/CdkHotswapDemoStack/5fdf0c20-11c7-11ec-a5ad-065be1cbeaa1
npm run cdk -- deploy --hotswap  5.44s user 1.05s system 11% cpu 56.750 total

他のLambda関数パッケージでの動作

AWS CDKのLambda関数パッケージは先程使用した**@aws-cdk/aws-lambda**以外に各ランタイムに特化した下記の3つが存在します。

  • @aws-cdk/aws-lambda-python
  • @aws-cdk/aws-lambda-go
  • @aws-cdk/aws-lambda-nodejs

これらでもhotswap deploymentsが機能するのか検証してみます。

下記のコードを追加して3つパッケージそれぞれでLambda関数を作成しAPI Gatewayに接続します。

    httpApi.addRoutes({
      path: "/lambda-python",
      methods: [HttpMethod.GET],
      integration: new LambdaProxyIntegration({
        handler: new PythonFunction(this, "FunctionPython", {
          entry: "src/function-python",
        }),
      }),
    });
    new CfnOutput(this, "LambdaPythonOutput", {
      value: httpApi.url! + "lambda-python",
    });

    httpApi.addRoutes({
      path: "/lambda-go",
      methods: [HttpMethod.GET],
      integration: new LambdaProxyIntegration({
        handler: new GoFunction(this, "FunctionGo", {
          entry: "src/function-go",
        }),
      }),
    });
    new CfnOutput(this, "LambdaGoOutput", {
      value: httpApi.url! + "lambda-go",
    });

    httpApi.addRoutes({
      path: "/lambda-nodejs",
      methods: [HttpMethod.GET],
      integration: new LambdaProxyIntegration({
        handler: new NodejsFunction(this, "FunctionNodejs", {
          entry: "src/function-nodejs/index.ts",
        }),
      }),
    });
    new CfnOutput(this, "LambdaNodejsOutput", {
      value: httpApi.url! + "lambda-nodejs",
    });

それぞれのランタイムの関数は下記のとおりです。

def handler(event, context):
    return {
        'isBase64Encoded': False,
        'statusCode': 200,
        'headers': {},
        'body': '{"message": "Hello World from AWS Lambda Python"}'
    }
package main

import (
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)


func handler() (events.APIGatewayV2HTTPResponse, error) {

    return events.APIGatewayV2HTTPResponse{
        Body:       "Hello World from AWS Lambda Go",
        StatusCode: 200,
    }, nil
}

func main() {
    lambda.Start(handler)
}
export const handler = async () => {
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: "Hello World from AWS Lambda Node.js(TypeScript)",
    }),
  };
};

デプロイ後にcurlして動作を確認します。

curl https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda-python
{"message": "Hello World from AWS Lambda Python"}%

curl https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda-go
Hello World from AWS Lambda Go%

curl https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda-nodejs
{"message":"Hello World from AWS Lambda Node.js(TypeScript)"}%

それぞれの関数のレスポンスするメッセージを変更してhotswap deploymentsを有効にした状態でデプロイを行います。

time npm run cdk -- deploy --hotswap

> cdk-hotswap-demo@0.1.0 cdk
> cdk "deploy" "--hotswap"

[+] Building 1.2s (6/6) FINISHED                                                                                                                                                            
 => [internal] load build definition from Dockerfile                                                                                                                                   0.0s
 => => transferring dockerfile: 374B                                                                                                                                                   0.0s
 => [internal] load .dockerignore                                                                                                                                                      0.0s
 => => transferring context: 2B                                                                                                                                                        0.0s
 => [internal] load metadata for public.ecr.aws/sam/build-python3.7:latest                                                                                                             1.1s
 => [1/2] FROM public.ecr.aws/sam/build-python3.7@sha256:555478e07a2384ae3c7dc1a8ec7d4f91b2930069ea556b0ac14de8491218b535                                                              0.0s
 => CACHED [2/2] RUN yum -q list installed rsync &>/dev/null || yum install -y rsync                                                                                                   0.0s
 => exporting to image                                                                                                                                                                 0.0s
 => => exporting layers                                                                                                                                                                0.0s
 => => writing image sha256:5bf0c4e8d007387733eed35c926862a2c7833bb346895200bab3d5413e83a42c                                                                                           0.0s
 => => naming to docker.io/library/cdk-aea63b3d5c84bcc57874f6d2fda127a01ba42fa30b91a317779526ac590b72fd                                                                                0.0s
Bundling asset CdkHotswapDemoStack/FunctionPython/Code/Stage...
Bundling asset CdkHotswapDemoStack/FunctionGo/Code/Stage...
Bundling asset CdkHotswapDemoStack/FunctionNodejs/Code/Stage...

  cdk.out/bundling-temp-43ef8a1610718734b80727f7ec2dda5f64181fc8773dcf536846cb4518346da9/index.js  785b 

⚡ Done in 7ms
⚠️ The --hotswap flag deliberately introduces CloudFormation drift to speed up deployments
⚠️ It should only be used for development - never use it for your production Stacks!
CdkHotswapDemoStack: deploying...
[0%] start: Publishing dcc84381791fed0f9d2d0e26507b2ad6248880b37f360b3546d6225b23fb27f3:current
[25%] success: Published dcc84381791fed0f9d2d0e26507b2ad6248880b37f360b3546d6225b23fb27f3:current
[25%] start: Publishing 98016784d874b1ea5d04b0da69c828db9e66829e95f1155d43e64f19135aab73:current
[50%] success: Published 98016784d874b1ea5d04b0da69c828db9e66829e95f1155d43e64f19135aab73:current
[50%] start: Publishing ea5d728f2b56166955da0a63510a2fb3716193fd2888e53cf88f637ac001af67:current
[75%] success: Published ea5d728f2b56166955da0a63510a2fb3716193fd2888e53cf88f637ac001af67:current
[75%] start: Publishing 1e3d48944356bcdf68991e13110146243a008ff199c1cf2f79c74818fddf2c2b:current
[100%] success: Published 1e3d48944356bcdf68991e13110146243a008ff199c1cf2f79c74818fddf2c2b:current

 ✅  CdkHotswapDemoStack

Outputs:
CdkHotswapDemoStack.LambdaGoOutput = https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda-go
CdkHotswapDemoStack.LambdaNodejsOutput = https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda-nodejs
CdkHotswapDemoStack.LambdaOutput = https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda
CdkHotswapDemoStack.LambdaPythonOutput = https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda-python

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:${AWS_ACCOUNT_ID}:stack/CdkHotswapDemoStack/5fdf0c20-11c7-11ec-a5ad-065be1cbeaa1
npm run cdk -- deploy --hotswap  7.18s user 2.95s system 78% cpu 12.891 total

約13秒で完了しました。バンドリ処理を含めての時間なので十分に早いです。

Lambda関数が更新されているか、レスポンスを確認します。

curl https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda-python
{"message": "🎉Hello World from AWS Lambda Python🎉"}%

curl https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda-go
🎉Hello World from AWS Lambda Go🎉%

curl https://${API_ID}.execute-api.ap-northeast-1.amazonaws.com/lambda-nodejs
{"message":"🎉Hello World from AWS Lambda Node.js(TypeScript)🎉"}%    

正常に更新されていました。

あとがき

AWS CDKを使って開発する場合のネックとしてCloudFormationのデプロイが遅いというものがあります、これに対する私のアプローチとしてはユニットテストやインテグレーションテストを充実させてデプロイして試す前に完成度を高めるというものでした。
とはいえ、Lambdaを使ったサーバーレス開発では実際にAWS環境上で動作させて確認したくなる場合があり、マネジメントコンソール上でコード変更してから動作を検証し、OKだったらリポジトリのコードを変更することがありました。
hotswap deployments機能があれば数秒〜10数秒でデプロイが完了するので、マネジメントコンソールからコードを変更できないGoやTypeScriptなどトランスコンパイルが発生する言語でもより快適にデバッグしながら開発が行えそうです。また、類似する機能としてServerless Stackやcdk-watchがあるのでサーバーレス開発でのデバッグに課題があるかたは合わせて調べてみると良いと思います。

以上でした。

Discussion