Skip to content

Commit 0adc8b7

Browse files
authored
feat(cli): introduce the 'watch' command (#17240)
This PR introduces the "watch" command, in two variants: as a separate new `cdk watch` command, and as an argument to the existing `cdk deploy` command. The "watch" process will observe the project files, defined by the new `"include"` and `"exclude"` settings in `cdk.json` (see aws/aws-cdk-rfcs#383 for details), and will trigger a `cdk deploy` when it detects any changes. The deployment will by default use the new "hotswap" deployments for maximum speed. Since `cdk deploy` is a relatively slow process for a "watch" command, there is some logic to perform intelligent queuing of any file events that happen while `cdk deploy` is running. We will batch all of those events, and trigger a single `cdk deploy` after the current one finishes. This ensures only a single `cdk deploy` command ever executes at a time. The observing of the files, and reacting to their changes, is accomplished using the [`chokidar` library](https://www.npmjs.com/package/chokidar). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 7bbd10d commit 0adc8b7

File tree

9 files changed

+535
-54
lines changed

9 files changed

+535
-54
lines changed

packages/aws-cdk/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,69 @@ For this reason, only use it for development purposes.
372372
**⚠ Note #2**: This command is considered experimental,
373373
and might have breaking changes in the future.
374374

375+
### `cdk watch`
376+
377+
The `watch` command is similar to `deploy`,
378+
but instead of being a one-shot operation,
379+
the command continuously monitors the files of the project,
380+
and triggers a deployment whenever it detects any changes:
381+
382+
```console
383+
$ cdk watch DevelopmentStack
384+
Detected change to 'lambda-code/index.js' (type: change). Triggering 'cdk deploy'
385+
DevelopmentStack: deploying...
386+
387+
✅ DevelopmentStack
388+
389+
^C
390+
```
391+
392+
To end a `cdk watch` session, interrupt the process by pressing Ctrl+C.
393+
394+
What files are observed is determined by the `"watch"` setting in your `cdk.json` file.
395+
It has two sub-keys, `"include"` and `"exclude"`, each of which can be either a single string, or an array of strings.
396+
Each entry is interpreted as a path relative to the location of the `cdk.json` file.
397+
Globs, both `*` and `**`, are allowed to be used.
398+
Example:
399+
400+
```json
401+
{
402+
"app": "mvn -e -q compile exec:java",
403+
"watch": {
404+
"include": "src/main/**",
405+
"exclude": "target/*"
406+
}
407+
}
408+
```
409+
410+
The default for `"include"` is `"**/*"`
411+
(which means all files and directories in the root of the project),
412+
and `"exclude"` is optional
413+
(note that we always ignore files and directories starting with `.`,
414+
the CDK output directory, and the `node_modules` directory),
415+
so the minimal settings to enable `watch` are `"watch": {}`.
416+
417+
If either your CDK code, or application code, needs a build step before being deployed,
418+
`watch` works with the `"build"` key in the `cdk.json` file,
419+
for example:
420+
421+
```json
422+
{
423+
"app": "mvn -e -q exec:java",
424+
"build": "mvn package",
425+
"watch": {
426+
"include": "src/main/**",
427+
"exclude": "target/*"
428+
}
429+
}
430+
```
431+
432+
Note that `watch` by default uses hotswap deployments (see above for details) --
433+
to turn them off, pass the `--no-hotswap` option when invoking it.
434+
435+
**Note**: This command is considered experimental,
436+
and might have breaking changes in the future.
437+
375438
### `cdk destroy`
376439

377440
Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were

packages/aws-cdk/bin/cdk.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,45 @@ async function parseCommandLineArguments() {
118118
'which skips CloudFormation and updates the resources directly, ' +
119119
'and falls back to a full deployment if that is not possible. ' +
120120
'Do not use this in production environments',
121+
})
122+
.option('watch', {
123+
type: 'boolean',
124+
desc: 'Continuously observe the project files, ' +
125+
'and deploy the given stack(s) automatically when changes are detected. ' +
126+
'Implies --hotswap by default',
127+
}),
128+
)
129+
.command('watch [STACKS..]', "Shortcut for 'deploy --watch'", yargs => yargs
130+
// I'm fairly certain none of these options, present for 'deploy', make sense for 'watch':
131+
// .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' })
132+
// .option('ci', { type: 'boolean', desc: 'Force CI detection', default: process.env.CI !== undefined })
133+
// @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment
134+
// .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true })
135+
// .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
136+
// These options, however, are more subtle - I could be convinced some of these should also be available for 'watch':
137+
// .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' })
138+
// .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} })
139+
// .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' })
140+
// .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true })
141+
// .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true })
142+
.option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] })
143+
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' })
144+
.option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' })
145+
.option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false })
146+
.option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' })
147+
.option('rollback', {
148+
type: 'boolean',
149+
desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " +
150+
'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail',
151+
})
152+
// same hack for -R as above in 'deploy'
153+
.option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true)
154+
.option('hotswap', {
155+
type: 'boolean',
156+
desc: "Attempts to perform a 'hotswap' deployment, " +
157+
'which skips CloudFormation and updates the resources directly, ' +
158+
'and falls back to a full deployment if that is not possible. ' +
159+
"'true' by default, use --no-hotswap to turn off",
121160
}),
122161
)
123162
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
@@ -335,6 +374,26 @@ async function initCommandLine() {
335374
ci: args.ci,
336375
rollback: configuration.settings.get(['rollback']),
337376
hotswap: args.hotswap,
377+
watch: args.watch,
378+
});
379+
380+
case 'watch':
381+
return cli.watch({
382+
selector,
383+
// parameters: parameterMap,
384+
// usePreviousParameters: args['previous-parameters'],
385+
// outputsFile: configuration.settings.get(['outputsFile']),
386+
// requireApproval: configuration.settings.get(['requireApproval']),
387+
// notificationArns: args.notificationArns,
388+
exclusively: args.exclusively,
389+
toolkitStackName,
390+
roleArn: args.roleArn,
391+
reuseAssets: args['build-exclude'],
392+
changeSetName: args.changeSetName,
393+
force: args.force,
394+
progress: configuration.settings.get(['progress']),
395+
rollback: configuration.settings.get(['rollback']),
396+
hotswap: args.hotswap,
338397
});
339398

340399
case 'destroy':

packages/aws-cdk/lib/api/cloudformation-deployments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export interface DeployStackOptions {
142142
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
143143
* and update the affected resources like Lambda functions directly.
144144
*
145-
* @default - false (do not perform a 'hotswap' deployment)
145+
* @default - false for regular deployments, true for 'watch' deployments
146146
*/
147147
readonly hotswap?: boolean;
148148
}

packages/aws-cdk/lib/api/cxapp/cloud-executable.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@ export class CloudExecutable {
4646
}
4747

4848
/**
49-
* Synthesize a set of stacks
49+
* Synthesize a set of stacks.
50+
*
51+
* @param cacheCloudAssembly whether to cache the Cloud Assembly after it has been first synthesized.
52+
* This is 'true' by default, and only set to 'false' for 'cdk watch',
53+
* which needs to re-synthesize the Assembly each time it detects a change to the project files
5054
*/
51-
public async synthesize(): Promise<CloudAssembly> {
52-
if (!this._cloudAssembly) {
55+
public async synthesize(cacheCloudAssembly: boolean = true): Promise<CloudAssembly> {
56+
if (!this._cloudAssembly || !cacheCloudAssembly) {
5357
this._cloudAssembly = await this.doSynthesize();
5458
}
5559
return this._cloudAssembly;

packages/aws-cdk/lib/api/deploy-stack.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export interface DeployStackOptions {
179179
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
180180
* and update the affected resources like Lambda functions directly.
181181
*
182-
* @default - false (do not perform a 'hotswap' deployment)
182+
* @default - false for regular deployments, true for 'watch' deployments
183183
*/
184184
readonly hotswap?: boolean;
185185
}

0 commit comments

Comments
 (0)