Skip to content

Commit

Permalink
feat(cli): introduce the 'watch' command (#17240)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
skinny85 authored and iliapolo committed Nov 7, 2021
1 parent 9cb31fc commit a899870
Show file tree
Hide file tree
Showing 9 changed files with 1,915 additions and 100 deletions.
63 changes: 63 additions & 0 deletions packages/aws-cdk/README.md
Expand Up @@ -372,6 +372,69 @@ For this reason, only use it for development purposes.
**⚠ Note #2**: This command is considered experimental,
and might have breaking changes in the future.

### `cdk watch`

The `watch` command is similar to `deploy`,
but instead of being a one-shot operation,
the command continuously monitors the files of the project,
and triggers a deployment whenever it detects any changes:

```console
$ cdk watch DevelopmentStack
Detected change to 'lambda-code/index.js' (type: change). Triggering 'cdk deploy'
DevelopmentStack: deploying...

✅ DevelopmentStack

^C
```

To end a `cdk watch` session, interrupt the process by pressing Ctrl+C.

What files are observed is determined by the `"watch"` setting in your `cdk.json` file.
It has two sub-keys, `"include"` and `"exclude"`, each of which can be either a single string, or an array of strings.
Each entry is interpreted as a path relative to the location of the `cdk.json` file.
Globs, both `*` and `**`, are allowed to be used.
Example:

```json
{
"app": "mvn -e -q compile exec:java",
"watch": {
"include": "src/main/**",
"exclude": "target/*"
}
}
```

The default for `"include"` is `"**/*"`
(which means all files and directories in the root of the project),
and `"exclude"` is optional
(note that we always ignore files and directories starting with `.`,
the CDK output directory, and the `node_modules` directory),
so the minimal settings to enable `watch` are `"watch": {}`.

If either your CDK code, or application code, needs a build step before being deployed,
`watch` works with the `"build"` key in the `cdk.json` file,
for example:

```json
{
"app": "mvn -e -q exec:java",
"build": "mvn package",
"watch": {
"include": "src/main/**",
"exclude": "target/*"
}
}
```

Note that `watch` by default uses hotswap deployments (see above for details) --
to turn them off, pass the `--no-hotswap` option when invoking it.

**Note**: This command is considered experimental,
and might have breaking changes in the future.

### `cdk destroy`

Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were
Expand Down
59 changes: 59 additions & 0 deletions packages/aws-cdk/bin/cdk.ts
Expand Up @@ -118,6 +118,45 @@ async function parseCommandLineArguments() {
'which skips CloudFormation and updates the resources directly, ' +
'and falls back to a full deployment if that is not possible. ' +
'Do not use this in production environments',
})
.option('watch', {
type: 'boolean',
desc: 'Continuously observe the project files, ' +
'and deploy the given stack(s) automatically when changes are detected. ' +
'Implies --hotswap by default',
}),
)
.command('watch [STACKS..]', "Shortcut for 'deploy --watch'", yargs => yargs
// I'm fairly certain none of these options, present for 'deploy', make sense for 'watch':
// .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' })
// .option('ci', { type: 'boolean', desc: 'Force CI detection', default: process.env.CI !== undefined })
// @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment
// .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 })
// .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
// These options, however, are more subtle - I could be convinced some of these should also be available for 'watch':
// .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' })
// .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} })
// .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)' })
// .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true })
// .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true })
.option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] })
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' })
.option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' })
.option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false })
.option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' })
.option('rollback', {
type: 'boolean',
desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " +
'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail',
})
// same hack for -R as above in 'deploy'
.option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true)
.option('hotswap', {
type: 'boolean',
desc: "Attempts to perform a 'hotswap' deployment, " +
'which skips CloudFormation and updates the resources directly, ' +
'and falls back to a full deployment if that is not possible. ' +
"'true' by default, use --no-hotswap to turn off",
}),
)
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
Expand Down Expand Up @@ -335,6 +374,26 @@ async function initCommandLine() {
ci: args.ci,
rollback: configuration.settings.get(['rollback']),
hotswap: args.hotswap,
watch: args.watch,
});

case 'watch':
return cli.watch({
selector,
// parameters: parameterMap,
// usePreviousParameters: args['previous-parameters'],
// outputsFile: configuration.settings.get(['outputsFile']),
// requireApproval: configuration.settings.get(['requireApproval']),
// notificationArns: args.notificationArns,
exclusively: args.exclusively,
toolkitStackName,
roleArn: args.roleArn,
reuseAssets: args['build-exclude'],
changeSetName: args.changeSetName,
force: args.force,
progress: configuration.settings.get(['progress']),
rollback: configuration.settings.get(['rollback']),
hotswap: args.hotswap,
});

case 'destroy':
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/cloudformation-deployments.ts
Expand Up @@ -142,7 +142,7 @@ export interface DeployStackOptions {
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
* and update the affected resources like Lambda functions directly.
*
* @default - false (do not perform a 'hotswap' deployment)
* @default - false for regular deployments, true for 'watch' deployments
*/
readonly hotswap?: boolean;
}
Expand Down
10 changes: 7 additions & 3 deletions packages/aws-cdk/lib/api/cxapp/cloud-executable.ts
Expand Up @@ -46,10 +46,14 @@ export class CloudExecutable {
}

/**
* Synthesize a set of stacks
* Synthesize a set of stacks.
*
* @param cacheCloudAssembly whether to cache the Cloud Assembly after it has been first synthesized.
* This is 'true' by default, and only set to 'false' for 'cdk watch',
* which needs to re-synthesize the Assembly each time it detects a change to the project files
*/
public async synthesize(): Promise<CloudAssembly> {
if (!this._cloudAssembly) {
public async synthesize(cacheCloudAssembly: boolean = true): Promise<CloudAssembly> {
if (!this._cloudAssembly || !cacheCloudAssembly) {
this._cloudAssembly = await this.doSynthesize();
}
return this._cloudAssembly;
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/deploy-stack.ts
Expand Up @@ -179,7 +179,7 @@ export interface DeployStackOptions {
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
* and update the affected resources like Lambda functions directly.
*
* @default - false (do not perform a 'hotswap' deployment)
* @default - false for regular deployments, true for 'watch' deployments
*/
readonly hotswap?: boolean;
}
Expand Down

0 comments on commit a899870

Please sign in to comment.