AWS Developer Tools Blog

Increasing development speed with CDK Watch

The AWS Cloud Development Kit (CDK) CLI introduces a new mode of operation, cdk watch, and two new flags for cdk deploy, --hotswap and --no-rollback. cdk watch makes development faster by monitoring your code and assets for changes and automatically performing the optimal form of deployment every time a file change is detected, meaning you no longer have to run cdk deploy each time you make a change to your CDK application. Where possible, cdk watch will use the --hotswap flag, which inspects the changes in your project and determines if those changes can be updated in-place without a full deployment through AWS CloudFormation. For CDK assets like your AWS Lambda handler code, Amazon ECS container images, or AWS Step Functions state machines, the CDK CLI will use AWS service APIs to directly make the changes; otherwise it will fall back to performing a full CloudFormation deployment. The --no-rollback flag will prevent CloudFormation from rolling back failed changes, saving more iteration time on failed deployments.

To see cdk watch and the --hotswap and --no-rollback flags in action, follow the instructions below. You will be using CDK in TypeScript in this blog post, but watch works with all CDK-supported languages. First, you will create a blank CDK application, and then you will add a simple containerized application using TypeScript and Express to your CDK application. Next, you will write the CDK stack that will create the infrastructure needed to deploy your application. Finally, you will use cdk watch to iterate on the application code.

Prerequisites

  • An AWS account
  • A local CDK installation

Setup

Ensure you have the CDK CLI V2 installed (cdk watch will also work with V1, but these examples are all written with V2 style imports). If you don’t have it installed, see the instructions in the AWS CDK Developer Guide. To verify your installation works correctly, run the cdk --version command in a terminal; you should see output similar to:

cdk --version
2.0.0 (build 4b6ce31)

First, create a new CDK application in TypeScript by running the following commands in your terminal.

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

Application Code

From the cdk-watch directory, create a directory and the files you will need to build your Docker image.

mkdir docker-app

Next, you must create the package.json that will declare our application’s dependencies. Note that the only dependency you need is Express; TypeScript does not need to be declared as a dependency, because it will be compiled to JavaScript before the application is deployed. Create docker-app/package.json and add the following contents.

{
     "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"
     }
}

Next, you must create the HTML file that will serve as your webpage. Create docker-app/index.html and add the following code.

<!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>

Now, you will create your Express code that will serve the HTML file you just created to any visitors to the site. Create docker-app/webpage.ts and add the following code.

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");
});

Lastly, you will create the Dockerfile that will start your application. Create docker-app/Dockerfile and add the following code.

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

Infrastructure Code

You will now create the CDK stack that defines the infrastructure that will host your webpage. You will use the ApplicationLoadBalancedFargateService construct from the aws_ecs_patterns module to greatly simplify your stack. Modify lib/cdk-watch-stack.ts so that it looks like the following example.

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,
     },
   });
  }
}

Any command specified in the build key (found in cdk.json) will be invoked prior to synthesis by any deployment, including those from cdk watch. Your application has TypeScript that needs to be compiled to JavaScript, so add this code to cdk.json at the same level as the “app” key:

"build": "cd docker-app && tsc",

This creates an entire serverless Docker application. With these changes, run the following commands.

yarn install # or, if you prefer, npm install
cdk deploy

You should see output similar to example below when the deployment finishes.

✅ CdkWatchStack

Outputs:
CdkWatchStack.EcsServiceLoadBalancerDNS6D595ACE = CdkWa-EcsSe-18QPSCKV5G8XP-1157603428.us-east-2.elb.amazonaws.com
CdkWatchStack.EcsServiceServiceURLE56F060F = http://CdkWa-EcsSe-18QPSCKV5G8XP-1157603428.us-east-2.elb.amazonaws.com

Stack ARN:
arn:aws:cloudformation:us-east-2:131099214097:stack/CdkWatchStack/1b15db20-428a-11ec-b96f-0a2907d0130e

Open the link included in the second line of the Outputs section. You should see a page that says Hello World.

Making an Application Code Change

Now that you’ve deployed your application, you can use cdk watch to make changes to it. Run cdk watch in a terminal, which should show the following output.

'watch' is observing directory '' for changes
'watch' is observing the file 'cdk.context.json' 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 'lib/cdk-watch-stack.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

When making application code changes, cdk watch can speedup the deployment. To see it, make the following change to 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%">
     <p>A paragraph</p>
</div>
</body>
</html>

In the terminal you will see cdk watch deploy this change.

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!

The warning message lets you know that this change is being hotswapped. This means that this change is made by going directly to the service API that provides the resource(s) being updated, bypassing CloudFormation entirely. This introduces drift between your CloudFormation template and your deployed application code. Because of this drift, hotswapping should never be used in a production environment. Hotswap deployments are faster, but lack the robust safety features of CloudFormation deployments, making hotswap deployments ideal for performing rapid code-compile-test loops in your development environment. If you need to disable hotswapping while running watch, pass the --no-hotswap flag to watch. If you need to remove the drift between CloudFormation and your application entirely, simply perform a full CloudFormation deployment by executing cdk deploy. If you want to perform a hotswap deployment without running cdk watch, run cdk deploy --hotswap.

Once this change has been deployed, refresh the page. You should now see the following update to the Hello World page.

Making an Infrastructure Change

Not all resource changes can be hotswapped. Currently, only Lambda Function code changes, ECS Service container definition changes, and Step Functions state machine definition changes can be hotswapped. If any other changes are made, hotswap deployments will fall back to full CloudFormation deployments. To see this, make the following code change to 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);

   // Fargate does not work with default VPCs
   const vpc = new ec2.Vpc(this, 'Vpc', {
     maxAzs: 2, // ALB requires 2 AZs
     natGateways: 2,    //changing this property does not trigger a hotswap, and a full deployment occurs instead
   });

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

Observe the terminal window. After the assets have finished publishing, you will see the following output.

Could not perform a hotswap deployment, as the stack CdkWatchStack contains non-Asset changes
Falling back to doing a full deployment
This means that the changes included a non-hotswappable change. Generally, changes to your application’s infrastructure are not hotswappable, while changes to assets used by your application are. This change increases the number of natGateways used by the vpc, so this is an infrastructure change and is therefore not hotswappable; thus, watch will fall back to performing a full CloudFormation deployment.

Disabling Rollback

By default, cdk watch does not use --no-rollback. Before disabling rollback, enter the ^C character (control+c) in the terminal window running cdk watch, and then run the cdk deploy command from your terminal.

cdk deploy

The full deployment is performed first to make CloudFormation aware of the changes you made earlier. These changes are considered replacement type changes by CloudFormation, which do not support the --no-rollback flag, because they require the deletion and creation of one of the resources that make up the ApplicationLoadBalancedFargateService. Once the deployment finishes, run the following command.

cdk watch --no-rollback

You should see the same output you did when you first ran cdk watch. Now make the following change to your stack:

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);

        // Fargate does not work with default VPCs
        const vpc = new ec2.Vpc(this, 'Vpc', {
            maxAzs: 2, // ALB requires 2 AZs
            natGateways: 2, 
        });

        new ec2.CfnVPC(this, 'mycfnvpc', { 
            cidrBlock: '10.0.0/16'       //intentionally incorrect code
         });

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

Note that this change specifies an invalid cidrBlock. The full deployment result is expected: this is an infrastructure change, so it is not hotswappable. As cdk watch is attempting the deployment, you will see the following error message.

Could not perform a hotswap deployment, as the stack CdkWatchStack contains non-Asset changes
Falling back to doing a full deployment
CdkWatchStack: creating CloudFormation changeset...
3:17:02 PM | CREATE_FAILED | AWS::EC2::VPC | mycfnvpc
Value (10.0.0/16) for parameter cidrBlock is invalid. This is not a valid CIDR block. (Service: AmazonEC2; Status Code: 400; Error Code: InvalidParameterValue; Request ID: 4b670ce5-32bd-46dd-88de-33765f18d479; Proxy: null)

❌ CdkWatchStack failed: Error: The stack named CdkWatchStack failed to deploy: UPDATE_FAILED (The following resource(s) failed to create: [mycfnvpc]. )
at Object.waitForStackDeploy (/usr/local/lib/node_modules/aws-cdk/lib/api/util/cloudformation.ts:309:11)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at prepareAndExecuteChangeSet (/usr/local/lib/node_modules/aws-cdk/lib/api/deploy-stack.ts:337:26)
at CdkToolkit.deploy (/usr/local/lib/node_modules/aws-cdk/lib/cdk-toolkit.ts:194:24)
at CdkToolkit.invokeDeployFromWatch (/usr/local/lib/node_modules/aws-cdk/lib/cdk-toolkit.ts:594:7)
at FSWatcher.<anonymous>(/usr/local/lib/node_modules/aws-cdk/lib/cdk-toolkit.ts:310:9)

Without --no-rollback, this change would be rolled back by CloudFormation. Now make this cidrBlock valid by making this change:

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);

    // Fargate does not work with default VPCs
    const vpc = new ec2.Vpc(this, 'Vpc', {
      maxAzs: 2, // ALB requires 2 AZs
      natGateways: 2,                                                                
    });
    
    new ec2.CfnVPC(this, 'mycfnvpc', {
      cidrBlock: '10.0.0.0/16'       //corrected code                                                       
    });

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

cdk watch will detect this change and automatically successfully deploy it with the following output.

Could not perform a hotswap deployment, as the stack CdkWatchStack contains non-Asset changes
Falling back to doing a full deployment
CdkWatchStack: creating CloudFormation changeset...


 ✅  CdkWatchStack

Outputs:
CdkWatchStack.EcsServiceLoadBalancerDNS6D595ACE = CdkWa-EcsSe-T2ZOAGRO8LGP-297129573.us-east-2.elb.amazonaws.com
CdkWatchStack.EcsServiceServiceURLE56F060F = http://CdkWa-EcsSe-T2ZOAGRO8LGP-297129573.us-east-2.elb.amazonaws.com

Stack ARN:
arn:aws:cloudformation:us-east-2:131099214097:stack/CdkWatchStack/95d784f0-4d73-11ec-a8b8-062cd5cc0070

Cleaning up

To delete the stack and application that you just deployed, run the cdk destroy command in your CDK project’s root directory.

cdk destroy

Summary

cdk watch allows you to make more rapid updates to your development stacks by leveraging hotswapping, where possible, to bypass CloudFormation. Not all resource changes can be hotswapped; if a hotswap deployment cannot be performed, watch will fall back to a full CloudFormation deployment. Due to the intentional drift introduced by hotswapping, it should never be used in a production environment. If desired, hotswapping can be turned off by passing the --no-hotswap flag. cdk watch can be invoked with the --no-rollback flag to disable rollback of failed updates, but any updates that CloudFormation considers as replacement type updates are not affected by this flag.