CDKの開発を爆速に!手元の変更を自動デプロイするCDK Watchを試してみた

「cdk watch」コマンドを使用すると、ファイルの変更差分を検知してAWS環境へ自動でデプロイが走ります。 デフォルトで「--hotswap」が適用され、CloudFormationを介さずに爆速でデプロイが完了します。
2021.12.31

こんにちは。MAD事業部のきんじょーです。

TypeScriptやGoなどコンパイルが必要な言語でLambdaを開発をしていると、AWSマネジメントコンソールでコードの変更ができず、ちょっとした変更を試すにも、ローカルでコードを修正しデプロイする必要があります。
それを通常のcdk deployでデプロイすると、CloudFormationを挟むためスタックの大きさによっては数十分かかり、ついついお布団に入りたくなります。

その問題を解消するために、CDKにはCloudFormationを挟まずAWS SDKを使用して直接リソースを更新するhotswap deploymentsという機能があります。
つい先日、ファイルの変更を検知して継続的にhotswapを実行するcdk watchというコマンドがあることを知ったので、早速試してみました。

CDKのhotswapを使用したことがない方は以下を参照してみてください。

CDK Watchでできること

忙しい方へ向けて先にまとめです。

  • cdk watchを使用するとプロジェクト内のファイルの変更を検知して自動でデプロイが開始される
  • デフォルトでは--hotswapで実行されるが、--hotswapに対応していない変更は通常のCloudFormationでデプロイされる。(明示的に--no-hotswap--no-rollbackを指定することも可能)
  • 監視対象のファイルはcdk.jsonで指定する
  • MFAを有効化している環境では、変更が検知される度にトークンを求められるのであまり意味がない

ExpressをFargateで動かすアプリケーションで検証

記事冒頭のブログに習い、TypeScriptとExpressでコンテナ化されたWebアプリケーションをデプロイして検証します。

環境

macOS Big Sur 11.5.1 Apple M1
node v14.17.6
cdk 2.3.0

cdk watch自体はCDK v1でも使用可能ですが、以下のサンプルコードはv2の形式でCDKのモジュールをインポートしています。

Step1. プロジェクトの作成

TypeScriptでCDKプロジェクトを立ち上げます。

$ mkdir cdk-watch
$ cd cdk-watch
$ npx cdk init --language=typescript

以下のようにプロジェクトが作成されました。

ROOT
├── README.md
├── bin
│   └── cdk-watch.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── cdk-watch-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── test
│   └── cdk-watch.test.ts
└── tsconfig.json

Step2. アプリケーションコードの追加

アプリケーションコードを配置するディレクトリを作成します。

$ mkdir docker-app

docker-app/package.json

{
     "name": "simple-webpage",
     "version": "1.0.0",
     "description": "Demo web app running on Amazon ECS",
     "license": "MIT-0",
     "dependencies": {
          "express": "^4.17.1"
     },
     "devDependencies": {
          "@types/express": "^4.17.13"
     }
}

package.jsonを作成しアプリケーションの依存ライブラリにExpressを追加します

docker-app/index.html

<!DOCTYPE html>

<html lang="en" dir="ltr">
<head>
     <meta charset="utf-8">
     <title>Simple Webpage </title>
</head>

<body>
<div align="center"
     <h2>Hello World</h2>
     <hr width="25%">
</div>
</body>
</html>

サンプルのアプリケーションでホストするHTMLファイル追加します。

docker-app/webpage.ts

import * as express from 'express';

const app = express();

app.get("/", (req, res) => {
     res.sendFile(__dirname + "/index.html");
});

app.listen(80, function () {
     console.log("server started on port 80");
});

Webサーバーを立ち上げるExpressのコードを追加します。

docker-app/Dockerfile

FROM --platform=amd64 node:alpine
RUN mkdir -p /usr/src/www
WORKDIR /usr/src/www
COPY . .
RUN npm install --production-only
CMD ["node", "webpage.js"]

最後にアプリケーションを起動するDockerfileを追加します。
M1 Macを使用しているため、Fargateでイメージを動かすのに明示的にplatformの指定が必要で少しハマりました。

Step3. インフラコードの追加

lib/cdk-watch-stack.ts

import {
    Stack,
    StackProps,
    aws_ec2 as ec2,
    aws_ecs as ecs,
    aws_ecs_patterns as ecs_patterns,
} from 'aws-cdk-lib';

import { Construct } from 'constructs';

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

const vpc = new ec2.Vpc(this, 'Vpc', {
     maxAzs: 2,
     natGateways: 1,
});

new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'EcsService', {
     vpc,
     taskImageOptions: {
     image: ecs.ContainerImage.fromAsset('docker-app'),
     containerPort: 80,
     },
   });
  }
}

Expressを起動するFargateのコンテナを、lib配下のcdk-watch-stack.tsを更新して定義します。

cdk.json

{
  "app": "npx ts-node --prefer-ts-exts bin/cdk-watch.ts",
  "context": {
    "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
    "@aws-cdk/core:enableStackNameDuplicates": true,
    "aws-cdk:enableDiffNoFail": true,
    "@aws-cdk/core:stackRelativeExports": true,
    "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
    "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true,
    "@aws-cdk/aws-kms:defaultKeyPolicies": true,
    "@aws-cdk/aws-s3:grantWriteWithoutAcl": true,
    "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
    "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
    "@aws-cdk/aws-efs:defaultEncryptionAtRest": true,
    "@aws-cdk/aws-lambda:recognizeVersionProps": true,
    "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true
  },
  "build": "cd docker-app && tsc"
}

cdk.jsonに"build"をキーに指定したコマンドは、cdkをデプロイする前に自動的に実行されますが、cdk watchによるデプロイの場合も同様です。
今回はデプロイ前にTypeScriptをJavaScriptにトランスパイルする必要があるため、tscコマンドを追加します。

ここまでの変更で、アプリケーションは以下のような構成になりました。

ROOT
├── README.md
├── bin
│   ├── cdk-watch.d.ts
│   ├── cdk-watch.js
│   └── cdk-watch.ts
├── cdk.json
├── docker-app
│   ├── Dockerfile
│   ├── index.html
│   ├── node_modules
│   ├── package.json
│   ├── webpage.ts
├── jest.config.js
├── lib
│   └── cdk-watch-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── test
│   └── cdk-watch.test.ts
└── tsconfig.json

Step4. 手動でデプロイ

必要とするライブラリをインストールし、手動でデプロイしてみます。
今回、CDK v2をはじめてデプロイするのでcdk bootstrapが必要でした。

$ yarn install
$ npx cdk bootstrap
$ npx cdk deploy

 ✅  CdkWatchStack
Outputs:
CdkWatchStack.EcsServiceLoadBalancerDNS6D595ACE = CdkWa-EcsSe-1ER2RZFKM2OQ-1332853417.ap-northeast-1.elb.amazonaws.com
CdkWatchStack.EcsServiceServiceURLE56F060F = http://CdkWa-EcsSe-1ER2RZFKM2OQ-1332853417.ap-northeast-1.elb.amazonaws.com

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:999999999999:stack/CdkWatchStack/46fd2a60-6966-11ec-b8a9-0a8a9c9afbe7

無事デプロイに成功しました!

Outputsに出力されたサービスのURLを開くと、index.htmlが表示されました。

hello-world-from-fargate

Step4. CDK Watchを試してみる

cdk watchを実行すると、ファイルの変更検知が開始されました。

$ npx cdk watch

'watch' is observing directory '' for changes
'watch' is observing directory 'bin' for changes
'watch' is observing directory 'docker-app' for changes
'watch' is observing directory 'lib' for changes
'watch' is observing the file 'bin/cdk-watch.ts' for changes
'watch' is observing the file 'docker-app/Dockerfile' for changes
'watch' is observing the file 'docker-app/index.html' for changes
'watch' is observing the file 'docker-app/package.json' for changes
'watch' is observing the file 'docker-app/webpage.ts' for changes
'watch' is observing the file 'docker-app/yarn.lock' for changes
'watch' is observing the file 'lib/cdk-watch-stack.ts' for changes

docker-app/index.html

<!DOCTYPE html>

<html lang="en" dir="ltr">
<head>
     <meta charset="utf-8">
     <title>Simple Webpage </title>
</head>

<body>
<div align="center"
     <h2>Hello World v2</h2>
     <hr width="25%">
</div>
</body>
</html>

ここで、HTMLファイルを一部変更してみます。 cdk watchを走らせていたターミナルで変更が検知され、デプロイが開始されました!

Detected change to 'docker-app/index.html' (type: change). Triggering 'cdk deploy'
⚠️ 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!

デプロイ完了後、さきほどのURLを開くとコンテナイメージが更新されていることを確認できます。 hello-world-from-fargate-v2

デプロイの実行中にファイルを更新しても、進行中のデプロイが完了するまではキューに入らないようで、排他制御も効いていました。

Detected change to 'docker-app/index.html' (type: change) while 'cdk deploy' is still running. Will queue for another deployment after this one finishes

監視対象のファイル

cdk.json

{
  "app": "npx ts-node --prefer-ts-exts bin/cdk-watch.ts",
  "watch":{
     "include":"src/main/**"、
     "exclude":"target/*"
  }
}

cdk.jsonにincludeexcludeで指定をします。 デフォルトのinclude"**/*"で、プロジェクトに含まれるすべてのファイルを対象とし、exclude.始まりの隠しファイルやCDKの出力先、node_modulesを除外しています。

cdk watchで指定できるオプション

--hotswap

デフォルトでは--hotswapが有効になっており、CloudFormationを介さずに爆速でデプロイが走ります。

現時点で、--hotswapを使用してデプロイできる変更は以下の通りです。

  • Lambda Functionsのコードとタグの変更
  • Lambdaのバージョンとエイリアスの変更
  • StepFunctionsステートマシンの定義の変更
  • ECSサービスのコンテナアセットの変更
  • S3に静的ホスティングした資材の変更
  • CodeBuildプロジェクトのソースと環境の変更

上記以外の変更は通常のCloudFormationで、自動的にデプロイが走ります。
また、--no-hotswapを明示的に指定することもできます。

--no-rollback

--no-rollbackを指定することで、デプロイ失敗時にStackがロールバックされるのを防ぐことができます。

MFAとの相性

MFAを使用している場合、cdk deployを走らせる度にトークンの入力が必要です。
cdk watchの場合も、変更を検知する度にMFAトークンを求められるため、watchモードの恩恵はあまり感じられませんでした。

Detected change to 'docker-app/index.html' (type: change). Triggering 'cdk deploy'
MFA token for arn:aws:iam::999999999999:mfa/role-name:

まとめ

cdk deploy --hotswapを使うようになってからLambdaをデプロイする待ち時間が減り、開発効率が大幅に上がっていましたが、cdk watchはデプロイコマンドを打つ手間さえ不要なので、MFAさえ無ければどんどん使って行こうと思いました。

皆さんもデプロイの手間を減らしてCDKで爆速開発をしていきましょう!

以上、MAD事業部のきんじょーでした。