Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/@aws-cdk/toolkit-lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ Deployment is the process of provisioning or updating your infrastructure in AWS
For an introduction to deploying, see [Deploy AWS CDK applications](https://docs.aws.amazon.com/cdk/v2/guide/deploy.html).
With the CDK Toolkit Library, you can programmatically control deployments, including stack selection, rollback behavior, and deployment monitoring.

Deployment failure diagnosis includes CloudTrail context by default. Set `cloudTrail: false`
in the deploy options to skip CloudTrail lookups while retaining other failure diagnostics.

```ts
declare const cdk: Toolkit;
declare const cx: ICloudAssemblySource;
Expand Down
8 changes: 8 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ export interface BaseDeployOptions {
*/
readonly rollback?: boolean;

/**
* Include CloudTrail context when diagnosing deployment failures.
* Set to false to skip CloudTrail lookups while retaining other diagnostics.
*
* @default true
*/
readonly cloudTrail?: boolean;

/**
* Automatically orphan resources that failed during rollback
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ import { DEFAULT_TOOLKIT_STACK_NAME } from '../toolkit-info';
const BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK = 23;

export interface DeployStackOptions {
/**
* Include CloudTrail failure context.
* @default true
*/
readonly cloudTrail?: boolean;
/**
* Stack to deploy
*/
Expand Down Expand Up @@ -446,6 +451,7 @@ export class Deployments {
overrideTemplate: options.overrideTemplate,
assetParallelism: options.assetParallelism,
diagnoser: new CloudFormationStackDiagnoser({
cloudTrail: options.cloudTrail,
sdk: env.sdk,
envResources: env.resources,
sourceTracer: new StackArtifactSourceTracer(options.stack),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import type { ResourceError, ResourceErrors } from '../stack-events/resource-err
import { StackStatus } from '../stack-events/stack-status';

export interface CloudFormationStackDiagnoserProps {
/**
* Include CloudTrail failure context.
* @default true
*/
readonly cloudTrail?: boolean;
readonly sdk: SDK;
readonly envResources?: EnvironmentResources;
readonly sourceTracer: ISourceTracer;
Expand Down Expand Up @@ -188,6 +193,9 @@ export class CloudFormationStackDiagnoser {
* case the output notifies the user.
*/
private async addCloudTrailContext(problems: TracedResourceError[], errs: readonly ResourceError[]): Promise<TracedResourceError[]> {
if (this.props.cloudTrail === false) {
return problems;
}
const sdk = await this.additionalExplorationSdk();
if (!sdk || problems.length === 0) {
return problems;
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,7 @@ export class Toolkit extends CloudAssemblySourceBuilder {
usePreviousParameters: options.parameters?.keepExistingParameters,
rollback: options.rollback,
notificationArns,
cloudTrail: options.cloudTrail,
rollbackConfiguration,
extraUserAgent: options.extraUserAgent,
assetParallelism: options.assetParallelism,
Expand Down
8 changes: 8 additions & 0 deletions packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,14 @@ IAM Statement Changes
});

describe('deployment options', () => {
test.each([true, false])('CloudTrail preference %s is passed to deployment preparation and execution', async (cloudTrail) => {
const prepare = jest.spyOn(deployments.Deployments.prototype, 'prepareStack').mockResolvedValueOnce(undefined);
const cx = await cdkOutFixture(toolkit, 'stack-with-role');
await toolkit.deploy(cx, { cloudTrail, deploymentMethod: { method: 'change-set' } });
expect(prepare).toHaveBeenCalledWith(expect.objectContaining({ cloudTrail }));
expect(mockDeployStack).toHaveBeenCalledWith(expect.objectContaining({ cloudTrail }));
successfulDeployment();
});
test('parameters are passed in', async () => {
// WHEN
const cx = await cdkOutFixture(toolkit, 'stack-with-role');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,29 @@ import { AssetManifest } from '@aws-cdk/cdk-assets-lib';
import {
ContinueUpdateRollbackCommand,
DescribeStackEventsCommand,
DescribeStackResourcesCommand,
DescribeStacksCommand,
ListStackResourcesCommand,
RollbackStackCommand,
type StackResourceSummary,
StackStatus,
} from '@aws-sdk/client-cloudformation';
import { LookupEventsCommand } from '@aws-sdk/client-cloudtrail';
import { GetParameterCommand } from '@aws-sdk/client-ssm';
import { CloudFormationStack } from '../../../lib/api/cloudformation';
import { Deployments } from '../../../lib/api/deployments';
import * as cfnApi from '../../../lib/api/deployments/cfn-api';
import { determineAllowCrossAccountAssetPublishing } from '../../../lib/api/deployments/checks';
import { deployStack, destroyStack } from '../../../lib/api/deployments/deploy-stack';
import { ResourceErrors } from '../../../lib/api/stack-events/resource-errors';
import { ToolkitInfo } from '../../../lib/api/toolkit-info';
import type { BootstrapError } from '../../../lib/toolkit/toolkit-error';
import { ToolkitError } from '../../../lib/toolkit/toolkit-error';
import { testStack } from '../../_helpers/assembly';
import {
mockBootstrapStack,
mockCloudFormationClient,
mockCloudTrailClient,
MockSdk,
MockSdkProvider,
mockSSMClient,
Expand Down Expand Up @@ -76,6 +80,41 @@ function mockSuccessfulBootstrapStackLookup(props?: Record<string, any>) {
mockToolkitInfoLookup.mockResolvedValue(ToolkitInfo.fromStack(fakeStack));
}

test.each([undefined, true, false])('deployment diagnoser respects CloudTrail preference %s', async (cloudTrail) => {
const stackArn = 'arn:aws:cloudformation:us-east-1:123456789012:stack/boop/abc';
const timestamp = new Date('2026-06-19T12:00:00Z');
mockCloudFormationClient.on(DescribeStackResourcesCommand).resolves({
StackResources: [{
LogicalResourceId: 'Function', ResourceType: 'AWS::Lambda::Function', PhysicalResourceId: 'boop-function',
}],
});
mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [] });
(deployStack as jest.Mock).mockImplementation(async ({ diagnoser }) => {
const errors = new ResourceErrors();
errors.update({
event: {
StackId: stackArn,
StackName: 'boop',
EventId: 'failed',
LogicalResourceId: 'Function',
PhysicalResourceId: 'boop-function',
ResourceType: 'AWS::Lambda::Function',
ResourceStatus: 'CREATE_FAILED',
ResourceStatusReason: 'Handler error',
Timestamp: timestamp,
},
parentStackLogicalIds: [],
isRootStackEvent: false,
});
const diagnosis = await diagnoser.diagnoseFromErrorCollection(errors, {
StackName: 'boop', StackStatus: 'UPDATE_FAILED', CreationTime: timestamp,
});
expect(diagnosis.result).toMatchObject({ type: 'problem', problems: [expect.objectContaining({ message: 'Handler error' })] });
});
await deployments.deployStack({ stack: testStack({ stackName: 'boop' }), cloudTrail });
expect(mockCloudTrailClient.commandCalls(LookupEventsCommand).length).toBe(cloudTrail === false ? 0 : 1);
});

test('passes through deploymentMethod with hotswap to deployStack()', async () => {
// WHEN
await deployments.deployStack({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -932,8 +932,9 @@ describe('CloudFormationStackDiagnoser', () => {
return errors;
}

function makeExploringDiagnoser() {
function makeExploringDiagnoser(cloudTrail?: boolean) {
return new CloudFormationStackDiagnoser({
cloudTrail,
sdk,
sourceTracer: fakeTracer,
ioHelper: ioHost.asHelper('diagnose'),
Expand All @@ -942,9 +943,7 @@ describe('CloudFormationStackDiagnoser', () => {
});
}

test('attaches correlated CloudTrail errors to the failed resource on any path', async () => {
// No cloudTrailEnabled gating anymore: the investigation runs for deploy-originating
// error collections too (a recent failure just yields fewer delivered events).
test.each([undefined, true])('attaches CloudTrail errors when enabled: %s', async (cloudTrail) => {
fakeCfn.createStackSync({ StackName: 'MyStack', StackStatus: 'UPDATE_FAILED', StackId: STACK_ARN });
mockCloudFormationClient.on(DescribeStackResourcesCommand).resolves({
StackResources: [{
Expand All @@ -968,7 +967,7 @@ describe('CloudFormationStackDiagnoser', () => {
}],
});

const result = await makeExploringDiagnoser().diagnoseFromErrorCollection(makeErrors(), {
const result = await makeExploringDiagnoser(cloudTrail).diagnoseFromErrorCollection(makeErrors(), {
StackName: 'MyStack',
StackStatus: 'UPDATE_FAILED',
CreationTime: new Date(),
Expand All @@ -980,6 +979,16 @@ describe('CloudFormationStackDiagnoser', () => {
expect(context!.messages[0]).toMatch(/AccessDenied on s3\.amazonaws\.com:CreateBucket/);
});

test('disabling CloudTrail retains the original failure without querying CloudTrail', async () => {
const result = await makeExploringDiagnoser(false).diagnoseFromErrorCollection(makeErrors(), {
StackName: 'MyStack', StackStatus: 'UPDATE_FAILED', CreationTime: new Date(),
});
const problem = assertProblem(result);
expect(problem.problems[0].message).toContain('Handler error');
expect(mockCloudTrailClient).not.toHaveReceivedCommand(LookupEventsCommand);
expect(problem.problems[0].additionalContext?.some((context) => context.source.startsWith('CloudTrail')) ?? false).toBe(false);
});

test('does not run the investigation without an exploration SDK', async () => {
const result = await makeDiagnoser().diagnoseFromErrorCollection(makeErrors(), {
StackName: 'MyStack',
Expand Down
6 changes: 6 additions & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ In the output above:

### `cdk deploy`

Failed deployments include CloudTrail context by default. Use `cdk deploy --no-cloudtrail`
to skip CloudTrail lookups while retaining CloudFormation failure messages and other resource
diagnostics. You can also set `"cloudtrail": false` in `cdk.json`; `--cloudtrail` enables it
for an individual deployment. The programmatic `Toolkit.deploy()` API accepts the equivalent
`cloudTrail: false` option.

Deploys a stack of your CDK app to its environment. During the deployment, the toolkit will output progress
indications, similar to what can be observed in the AWS CloudFormation Console. If the environment was never
bootstrapped (using `cdk bootstrap`), only stacks that are not using assets and synthesize to a template that is under
Expand Down
13 changes: 13 additions & 0 deletions packages/aws-cdk/lib/cli/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ export class CdkToolkit {
forceDeployment: options.force,
rollback: options.rollback,
reuseAssets: options.reuseAssets,
cloudTrail: options.cloudTrail,
concurrency: options.concurrency,
traceLogs: options.traceLogs,
notificationArns: options.notificationArns,
Expand Down Expand Up @@ -622,6 +623,7 @@ export class CdkToolkit {
usePreviousParameters: options.usePreviousParameters,
rollback: options.rollback,
concurrency,
cloudTrail: options.cloudTrail,
requireApproval,
assetParallelism: options.assetParallelism,
extraUserAgent: options.extraUserAgent,
Expand Down Expand Up @@ -1724,6 +1726,11 @@ export interface DiffOptions {
}

interface CfnDeployOptions {
/**
* Include CloudTrail failure context.
* @default true
*/
readonly cloudTrail?: boolean;
/**
* Criteria for selecting stacks to deploy
*/
Expand Down Expand Up @@ -2319,6 +2326,11 @@ export async function displayFlagsMessage(ioHost: IoHelper, toolkit: InternalToo
}

interface WorkGraphDeploymentActionsOptions {
/**
* Include CloudTrail failure context.
* @default true
*/
readonly cloudTrail?: boolean;
readonly roleArn?: string;
readonly force?: boolean;
readonly stackCount?: number;
Expand Down Expand Up @@ -2493,6 +2505,7 @@ class WorkGraphDeploymentActions implements WorkGraphActions {
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
usePreviousParameters: this.options.usePreviousParameters,
rollback: this.options.rollback,
cloudTrail: this.options.cloudTrail,
notificationArns,
rollbackConfiguration,
extraUserAgent: this.options.extraUserAgent,
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ export async function makeConfig(): Promise<CliConfig> {
"'true' by default, use --no-logs to turn off. " +
"Only in effect if specified alongside the '--watch' option",
},
'cloudtrail': {
type: 'boolean',
desc: 'Include CloudTrail context when diagnosing deployment failures. Use --no-cloudtrail to skip the lookup',
},
'concurrency': { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true },
'asset-parallelism': { type: 'boolean', desc: 'Whether to build/publish assets in parallel' },
'asset-build-concurrency': { type: 'number', desc: 'Maximum number of asset builds to run in parallel', default: 1, requiresArg: true },
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,10 @@
"default": true,
"desc": "Show CloudWatch log events from all resources in the selected Stacks in the terminal. 'true' by default, use --no-logs to turn off. Only in effect if specified alongside the '--watch' option"
},
"cloudtrail": {
"type": "boolean",
"desc": "Include CloudTrail context when diagnosing deployment failures. Use --no-cloudtrail to skip the lookup"
},
"concurrency": {
"type": "number",
"desc": "Maximum number of simultaneous deployments (dependency permitting) to execute.",
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
progress: configuration.settings.get(['progress']),
ci: args.ci,
rollback: configuration.settings.get(['rollback']),
cloudTrail: configuration.settings.get(['cloudtrail']),
watch: args.watch,
traceLogs: args.logs,
concurrency: args.concurrency,
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export function convertYargsToUserInput(args: any): UserInput {
hotswapEcsStabilizationTimeoutSeconds: args.hotswapEcsStabilizationTimeoutSeconds,
watch: args.watch,
logs: args.logs,
cloudtrail: args.cloudtrail,
concurrency: args.concurrency,
assetParallelism: args.assetParallelism,
assetBuildConcurrency: args.assetBuildConcurrency,
Expand Down Expand Up @@ -494,6 +495,7 @@ export function convertConfigToUserInput(config: any): UserInput {
hotswapEcsStabilizationTimeoutSeconds: config.deploy?.hotswapEcsStabilizationTimeoutSeconds,
watch: config.deploy?.watch,
logs: config.deploy?.logs,
cloudtrail: config.deploy?.cloudtrail,
concurrency: config.deploy?.concurrency,
assetParallelism: config.deploy?.assetParallelism,
assetBuildConcurrency: config.deploy?.assetBuildConcurrency,
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk/lib/cli/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,11 @@ export function parseCommandLineArguments(args: Array<string>): any {
type: 'boolean',
desc: "Show CloudWatch log events from all resources in the selected Stacks in the terminal. 'true' by default, use --no-logs to turn off. Only in effect if specified alongside the '--watch' option",
})
.option('cloudtrail', {
default: undefined,
type: 'boolean',
desc: 'Include CloudTrail context when diagnosing deployment failures. Use --no-cloudtrail to skip the lookup',
})
.option('concurrency', {
default: 1,
type: 'number',
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/user-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export class Configuration {
versionReporting: true,
assetMetadata: true,
pathMetadata: true,
cloudtrail: true,
output: 'cdk.out',
});

Expand Down Expand Up @@ -354,6 +355,7 @@ export async function commandLineArgumentsToSettings(ioHelper: IoHelper, argv: A
bundlingStacks,
lookups: argv.lookups,
rollback: argv.rollback,
cloudtrail: argv.cloudtrail,
notices: argv.notices,
assetParallelism: argv['asset-parallelism'],
assetBuildConcurrency: argv['asset-build-concurrency'],
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cli/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,13 @@ export interface DeployOptions {
*/
readonly logs?: boolean;

/**
* Include CloudTrail context when diagnosing deployment failures. Use --no-cloudtrail to skip the lookup
*
* @default - undefined
*/
readonly cloudtrail?: boolean;

/**
* Maximum number of simultaneous deployments (dependency permitting) to execute.
*
Expand Down
18 changes: 18 additions & 0 deletions packages/aws-cdk/test/cli/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ jest.mock('../../lib/cli/parse-command-line-arguments', () => ({
...result,
_: ['deploy'],
parameters: [],
cloudtrail: args.includes('--no-cloudtrail') ? false : args.includes('--cloudtrail') ? true : undefined,
};
} else if (args.includes('publish-assets')) {
result = { ...result, _: ['publish-assets'] };
Expand Down Expand Up @@ -694,6 +695,23 @@ describe('AI agent progress auto-default', () => {
});
});

describe('deployment CloudTrail preference', () => {
test.each([true, false])('passes the configured CloudTrail preference %s to deployment', async (cloudTrail) => {
const original = Configuration.fromArgsAndFiles;
const deploy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'deploy').mockResolvedValue();
// Use the real configuration conversion to preserve explicit false values.
Configuration.fromArgsAndFiles = jest.fn().mockImplementation((helper: any, props: any) =>
actualUserConfig.Configuration.fromArgsAndFiles(helper, { ...props, readUserContext: false }));
try {
await exec(['deploy', ...(cloudTrail ? [] : ['--no-cloudtrail'])]);
expect(deploy).toHaveBeenCalledWith(expect.objectContaining({ cloudTrail }));
} finally {
Configuration.fromArgsAndFiles = original;
deploy.mockRestore();
}
});
});

describe('--yes', () => {
test('when --yes option is provided, CliIoHost is using autoRespond', async () => {
// GIVEN
Expand Down
Loading
Loading