Skip to content

Commit a9038ae

Browse files
authored
feat(cli): watch streams resources' CloudWatch logs to the terminal (#18159)
This adds a new `--logs` flag on `cdk watch` which is set to `true` by default. Watch will monitor all CloudWatch Log groups in the application and stream the log events back to the users terminal. re #18122 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 7779c14 commit a9038ae

19 files changed

+924
-151
lines changed

Diff for: packages/aws-cdk/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,13 @@ for example:
436436
Note that `watch` by default uses hotswap deployments (see above for details) --
437437
to turn them off, pass the `--no-hotswap` option when invoking it.
438438

439+
By default `watch` will also monitor all CloudWatch Log Groups in your application and stream the log events
440+
locally to your terminal. To disable this feature you can pass the `--no-logs` option when invoking it:
441+
442+
```console
443+
$ cdk watch --no-logs
444+
```
445+
439446
**Note**: This command is considered experimental,
440447
and might have breaking changes in the future.
441448

Diff for: packages/aws-cdk/bin/cdk.ts

+15
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ async function parseCommandLineArguments() {
124124
desc: 'Continuously observe the project files, ' +
125125
'and deploy the given stack(s) automatically when changes are detected. ' +
126126
'Implies --hotswap by default',
127+
})
128+
.options('logs', {
129+
type: 'boolean',
130+
default: true,
131+
desc: 'Show CloudWatch log events from all resources in the selected Stacks in the terminal. ' +
132+
"'true' by default, use --no-logs to turn off. " +
133+
"Only in effect if specified alongside the '--watch' option",
127134
}),
128135
)
129136
.command('watch [STACKS..]', "Shortcut for 'deploy --watch'", yargs => yargs
@@ -157,6 +164,12 @@ async function parseCommandLineArguments() {
157164
'which skips CloudFormation and updates the resources directly, ' +
158165
'and falls back to a full deployment if that is not possible. ' +
159166
"'true' by default, use --no-hotswap to turn off",
167+
})
168+
.options('logs', {
169+
type: 'boolean',
170+
default: true,
171+
desc: 'Show CloudWatch log events from all resources in the selected Stacks in the terminal. ' +
172+
"'true' by default, use --no-logs to turn off",
160173
}),
161174
)
162175
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
@@ -376,6 +389,7 @@ async function initCommandLine() {
376389
rollback: configuration.settings.get(['rollback']),
377390
hotswap: args.hotswap,
378391
watch: args.watch,
392+
traceLogs: args.logs,
379393
});
380394

381395
case 'watch':
@@ -395,6 +409,7 @@ async function initCommandLine() {
395409
progress: configuration.settings.get(['progress']),
396410
rollback: configuration.settings.get(['rollback']),
397411
hotswap: args.hotswap,
412+
traceLogs: args.logs,
398413
});
399414

400415
case 'destroy':

Diff for: packages/aws-cdk/lib/api/aws-auth/sdk.ts

+5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface ISDK {
6262
kms(): AWS.KMS;
6363
stepFunctions(): AWS.StepFunctions;
6464
codeBuild(): AWS.CodeBuild
65+
cloudWatchLogs(): AWS.CloudWatchLogs;
6566
}
6667

6768
/**
@@ -185,6 +186,10 @@ export class SDK implements ISDK {
185186
return this.wrapServiceErrorHandling(new AWS.CodeBuild(this.config));
186187
}
187188

189+
public cloudWatchLogs(): AWS.CloudWatchLogs {
190+
return this.wrapServiceErrorHandling(new AWS.CloudWatchLogs(this.config));
191+
}
192+
188193
public async currentAccount(): Promise<Account> {
189194
// Get/refresh if necessary before we can access `accessKeyId`
190195
await this.forceCredentialRetrieval();

Diff for: packages/aws-cdk/lib/api/cloudformation-deployments.ts

+81-79
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,86 @@ export async function replaceEnvPlaceholders<A extends { }>(object: A, env: cxap
2727
});
2828
}
2929

30+
/**
31+
* SDK obtained by assuming the lookup role
32+
* for a given environment
33+
*/
34+
export interface PreparedSdkWithLookupRoleForEnvironment {
35+
/**
36+
* The SDK for the given environment
37+
*/
38+
readonly sdk: ISDK;
39+
40+
/**
41+
* The resolved environment for the stack
42+
* (no more 'unknown-account/unknown-region')
43+
*/
44+
readonly resolvedEnvironment: cxapi.Environment;
45+
46+
/**
47+
* Whether or not the assume role was successful.
48+
* If the assume role was not successful (false)
49+
* then that means that the 'sdk' returned contains
50+
* the default credentials (not the assume role credentials)
51+
*/
52+
readonly didAssumeRole: boolean;
53+
}
54+
55+
/**
56+
* Try to use the bootstrap lookupRole. There are two scenarios that are handled here
57+
* 1. The lookup role may not exist (it was added in bootstrap stack version 7)
58+
* 2. The lookup role may not have the correct permissions (ReadOnlyAccess was added in
59+
* bootstrap stack version 8)
60+
*
61+
* In the case of 1 (lookup role doesn't exist) `forEnvironment` will either:
62+
* 1. Return the default credentials if the default credentials are for the stack account
63+
* 2. Throw an error if the default credentials are not for the stack account.
64+
*
65+
* If we successfully assume the lookup role we then proceed to 2 and check whether the bootstrap
66+
* stack version is valid. If it is not we throw an error which should be handled in the calling
67+
* function (and fallback to use a different role, etc)
68+
*
69+
* If we do not successfully assume the lookup role, but do get back the default credentials
70+
* then return those and note that we are returning the default credentials. The calling
71+
* function can then decide to use them or fallback to another role.
72+
*/
73+
export async function prepareSdkWithLookupRoleFor(
74+
sdkProvider: SdkProvider,
75+
stack: cxapi.CloudFormationStackArtifact,
76+
): Promise<PreparedSdkWithLookupRoleForEnvironment> {
77+
const resolvedEnvironment = await sdkProvider.resolveEnvironment(stack.environment);
78+
79+
// Substitute any placeholders with information about the current environment
80+
const arns = await replaceEnvPlaceholders({
81+
lookupRoleArn: stack.lookupRole?.arn,
82+
}, resolvedEnvironment, sdkProvider);
83+
84+
// try to assume the lookup role
85+
const warningMessage = `Could not assume ${arns.lookupRoleArn}, proceeding anyway.`;
86+
const upgradeMessage = `(To get rid of this warning, please upgrade to bootstrap version >= ${stack.lookupRole?.requiresBootstrapStackVersion})`;
87+
try {
88+
const stackSdk = await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForReading, {
89+
assumeRoleArn: arns.lookupRoleArn,
90+
assumeRoleExternalId: stack.lookupRole?.assumeRoleExternalId,
91+
});
92+
93+
// if we succeed in assuming the lookup role, make sure we have the correct bootstrap stack version
94+
if (stackSdk.didAssumeRole && stack.lookupRole?.bootstrapStackVersionSsmParameter && stack.lookupRole.requiresBootstrapStackVersion) {
95+
const version = await ToolkitInfo.versionFromSsmParameter(stackSdk.sdk, stack.lookupRole.bootstrapStackVersionSsmParameter);
96+
if (version < stack.lookupRole.requiresBootstrapStackVersion) {
97+
throw new Error(`Bootstrap stack version '${stack.lookupRole.requiresBootstrapStackVersion}' is required, found version '${version}'.`);
98+
}
99+
} else if (!stackSdk.didAssumeRole) {
100+
warning(upgradeMessage);
101+
}
102+
return { ...stackSdk, resolvedEnvironment };
103+
} catch (e) {
104+
debug(e);
105+
warning(warningMessage);
106+
warning(upgradeMessage);
107+
throw (e);
108+
}
109+
}
30110

31111
export interface DeployStackOptions {
32112
/**
@@ -171,31 +251,6 @@ export interface ProvisionerProps {
171251
sdkProvider: SdkProvider;
172252
}
173253

174-
/**
175-
* SDK obtained by assuming the lookup role
176-
* for a given environment
177-
*/
178-
export interface PreparedSdkWithLookupRoleForEnvironment {
179-
/**
180-
* The SDK for the given environment
181-
*/
182-
readonly sdk: ISDK;
183-
184-
/**
185-
* The resolved environment for the stack
186-
* (no more 'unknown-account/unknown-region')
187-
*/
188-
readonly resolvedEnvironment: cxapi.Environment;
189-
190-
/**
191-
* Whether or not the assume role was successful.
192-
* If the assume role was not successful (false)
193-
* then that means that the 'sdk' returned contains
194-
* the default credentials (not the assume role credentials)
195-
*/
196-
readonly didAssumeRole: boolean;
197-
}
198-
199254
/**
200255
* SDK obtained by assuming the deploy role
201256
* for a given environment
@@ -237,7 +292,7 @@ export class CloudFormationDeployments {
237292
let stackSdk: ISDK | undefined = undefined;
238293
// try to assume the lookup role and fallback to the deploy role
239294
try {
240-
const result = await this.prepareSdkWithLookupRoleFor(stackArtifact);
295+
const result = await prepareSdkWithLookupRoleFor(this.sdkProvider, stackArtifact);
241296
if (result.didAssumeRole) {
242297
stackSdk = result.sdk;
243298
}
@@ -311,59 +366,6 @@ export class CloudFormationDeployments {
311366
return stack.exists;
312367
}
313368

314-
/**
315-
* Try to use the bootstrap lookupRole. There are two scenarios that are handled here
316-
* 1. The lookup role may not exist (it was added in bootstrap stack version 7)
317-
* 2. The lookup role may not have the correct permissions (ReadOnlyAccess was added in
318-
* bootstrap stack version 8)
319-
*
320-
* In the case of 1 (lookup role doesn't exist) `forEnvironment` will either:
321-
* 1. Return the default credentials if the default credentials are for the stack account
322-
* 2. Throw an error if the default credentials are not for the stack account.
323-
*
324-
* If we successfully assume the lookup role we then proceed to 2 and check whether the bootstrap
325-
* stack version is valid. If it is not we throw an error which should be handled in the calling
326-
* function (and fallback to use a different role, etc)
327-
*
328-
* If we do not successfully assume the lookup role, but do get back the default credentials
329-
* then return those and note that we are returning the default credentials. The calling
330-
* function can then decide to use them or fallback to another role.
331-
*/
332-
private async prepareSdkWithLookupRoleFor(stack: cxapi.CloudFormationStackArtifact): Promise<PreparedSdkWithLookupRoleForEnvironment> {
333-
const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(stack.environment);
334-
335-
// Substitute any placeholders with information about the current environment
336-
const arns = await replaceEnvPlaceholders({
337-
lookupRoleArn: stack.lookupRole?.arn,
338-
}, resolvedEnvironment, this.sdkProvider);
339-
340-
// try to assume the lookup role
341-
const warningMessage = `Could not assume ${arns.lookupRoleArn}, proceeding anyway.`;
342-
const upgradeMessage = `(To get rid of this warning, please upgrade to bootstrap version >= ${stack.lookupRole?.requiresBootstrapStackVersion})`;
343-
try {
344-
const stackSdk = await this.sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForReading, {
345-
assumeRoleArn: arns.lookupRoleArn,
346-
assumeRoleExternalId: stack.lookupRole?.assumeRoleExternalId,
347-
});
348-
349-
// if we succeed in assuming the lookup role, make sure we have the correct bootstrap stack version
350-
if (stackSdk.didAssumeRole && stack.lookupRole?.bootstrapStackVersionSsmParameter && stack.lookupRole.requiresBootstrapStackVersion) {
351-
const version = await ToolkitInfo.versionFromSsmParameter(stackSdk.sdk, stack.lookupRole.bootstrapStackVersionSsmParameter);
352-
if (version < stack.lookupRole.requiresBootstrapStackVersion) {
353-
throw new Error(`Bootstrap stack version '${stack.lookupRole.requiresBootstrapStackVersion}' is required, found version '${version}'.`);
354-
}
355-
} else if (!stackSdk.didAssumeRole) {
356-
warning(upgradeMessage);
357-
}
358-
return { ...stackSdk, resolvedEnvironment };
359-
} catch (e) {
360-
debug(e);
361-
warning(warningMessage);
362-
warning(upgradeMessage);
363-
throw (e);
364-
}
365-
}
366-
367369
/**
368370
* Get the environment necessary for touching the given stack
369371
*

Diff for: packages/aws-cdk/lib/api/deploy-stack.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { AssetManifestBuilder } from '../util/asset-manifest-builder';
99
import { publishAssets } from '../util/asset-publishing';
1010
import { contentHash } from '../util/content-hash';
1111
import { ISDK, SdkProvider } from './aws-auth';
12+
import { CfnEvaluationException } from './evaluate-cloudformation-template';
1213
import { tryHotswapDeployment } from './hotswap-deployments';
1314
import { ICON } from './hotswap/common';
14-
import { CfnEvaluationException } from './hotswap/evaluate-cloudformation-template';
1515
import { ToolkitInfo } from './toolkit-info';
1616
import {
1717
changeSetHasNoChanges, CloudFormationStack, TemplateParameters, waitForChangeSet,

Diff for: packages/aws-cdk/lib/api/hotswap/evaluate-cloudformation-template.ts renamed to packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
11
import * as cxapi from '@aws-cdk/cx-api';
22
import * as AWS from 'aws-sdk';
3-
import { ListStackResources } from './common';
3+
import { ISDK } from './aws-auth';
4+
5+
export interface ListStackResources {
6+
listStackResources(): Promise<AWS.CloudFormation.StackResourceSummary[]>;
7+
}
8+
9+
export class LazyListStackResources implements ListStackResources {
10+
private stackResources: AWS.CloudFormation.StackResourceSummary[] | undefined;
11+
12+
constructor(private readonly sdk: ISDK, private readonly stackName: string) {
13+
}
14+
15+
public async listStackResources(): Promise<AWS.CloudFormation.StackResourceSummary[]> {
16+
if (this.stackResources === undefined) {
17+
this.stackResources = await this.getStackResources();
18+
}
19+
return this.stackResources;
20+
}
21+
22+
private async getStackResources(): Promise<AWS.CloudFormation.StackResourceSummary[]> {
23+
const ret = new Array<AWS.CloudFormation.StackResourceSummary>();
24+
let nextToken: string | undefined;
25+
do {
26+
const stackResourcesResponse = await this.sdk.cloudFormation().listStackResources({
27+
StackName: this.stackName,
28+
NextToken: nextToken,
29+
}).promise();
30+
ret.push(...(stackResourcesResponse.StackResourceSummaries ?? []));
31+
nextToken = stackResourcesResponse.NextToken;
32+
} while (nextToken);
33+
return ret;
34+
}
35+
}
436

537
export class CfnEvaluationException extends Error {}
638

@@ -45,6 +77,21 @@ export class EvaluateCloudFormationTemplate {
4577
this.urlSuffix = props.urlSuffix;
4678
}
4779

80+
public async establishResourcePhysicalName(logicalId: string, physicalNameInCfnTemplate: any): Promise<string | undefined> {
81+
if (physicalNameInCfnTemplate != null) {
82+
try {
83+
return await this.evaluateCfnExpression(physicalNameInCfnTemplate);
84+
} catch (e) {
85+
// If we can't evaluate the resource's name CloudFormation expression,
86+
// just look it up in the currently deployed Stack
87+
if (!(e instanceof CfnEvaluationException)) {
88+
throw e;
89+
}
90+
}
91+
}
92+
return this.findPhysicalNameFor(logicalId);
93+
}
94+
4895
public async findPhysicalNameFor(logicalId: string): Promise<string | undefined> {
4996
const stackResources = await this.stackResources.listStackResources();
5097
return stackResources.find(sr => sr.LogicalResourceId === logicalId)?.PhysicalResourceId;

0 commit comments

Comments
 (0)