Skip to content

feat(aws-cdk): surface custom resource Lambda logs on deployment failure#1645

Merged
rix0rrr merged 17 commits into
mainfrom
iankhou-custom-resource-logs
Jun 26, 2026
Merged

feat(aws-cdk): surface custom resource Lambda logs on deployment failure#1645
rix0rrr merged 17 commits into
mainfrom
iankhou-custom-resource-logs

Conversation

@iankhou

@iankhou iankhou commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

When a CloudFormation custom resource fails during deployment, the CLI today shows only the raw status reason — typically a cfn-response pointer like "See the details in CloudWatch Log Stream: …" — leaving the developer to go search for the lambda logs by hand. This PR fetches the backing Lambda's CloudWatch logs and surfaces them inline in the failure diagnosis, mirroring the existing ECS service investigation. The result is the actual handler error, where the failure is reported.

Applies to Custom::* and AWS::CloudFormation::CustomResource. The whole investigation is best-effort: every AWS call is wrapped so a failure is logged at debug level and never breaks the deployment error path.

Future work - explore Cloudtrail events following deployment failure. This will come in another PR! Cloudtrail events aren't available in console until 5-15 minutes after they occur, so will only be available on cdk diagnose runs after failure.

Before

❌  MyStack failed: ... Received response status [FAILED] from custom resource.
   Message returned: See the details in CloudWatch Log Stream: 2026/.../[$LATEST]abc123

After

MyStack/MyResource  (AWS::CloudFormation::CustomResource MyResource)
  Received response status [FAILED] from custom resource. ...

   Logs from /aws/lambda/MyStack-Handler-XXXX:
   ERROR Boom: the handler blew up
   INFO  Response body: {"Status":"FAILED", ...}
   Logs: https://<region>.console.aws.amazon.com/cloudwatch/...

How it works

1. Find the backing Lambda. The CloudFormation event doesn't name the function — only the resource's ServiceToken does, and that lives in the template. We fetch the template once and read ServiceToken, resolving:

  • a literal Lambda ARN,
  • an Fn::GetAtt (JSON array form and the YAML string short-form "Fn.Arn"), or
  • a Ref, via describeStackResources.

For an intrinsic reference we confirm the target is actually AWS::Lambda::Function before treating its physical ID as a function name (a Ref to, say, an SNS topic is ignored rather than producing a bogus /aws/lambda/<name> lookup).

2. Resolve the log group — rollback-proof on stack CREATE. A first-deploy failure rolls back and deletes the backing Lambda, so getFunctionConfiguration is unavailable exactly when we need it. We therefore derive the log group without the live function wherever possible:

  • the /aws/lambda/<function-name> convention (covers the vast majority);
  • for advanced logging controls (custom LoggingConfig.LogGroup), read it from the template — including the common CDK shape where it's a Ref to an AWS::Logs::LogGroup whose physical name CloudFormation generates (resolved via describeStackResources, which still returns RETAINed resources after rollback);
  • getFunctionConfiguration is used only as a last-resort fallback.

3. Target the right invocation. cfn-response writes the failing log stream name into the status reason, so we query that exact stream. Because that stream is pinned to the original create invocation, on update/rollback failures we bound the query to a time window around the failure event and fall back to a group-wide scan, so a stale stream can't hide the real logs. This required adding an optional timestamp to ResourceError (populated from stack events).

4. Readable output. Lambda log events are normalized before display (Lambda-only — ECS logs are arbitrary container output and left untouched):

  • text format → strip the per-line timestamp/requestId prefix, render aligned LEVEL message;
  • JSON format → render level + message (and error envelopes' errorMessage/stackTrace) instead of dumping raw JSON;
  • Lambda platform boilerplate (INIT_START/START/END/REPORT, JSON platform.*) is dropped;
  • application output is never dropped by level — failure detail often rides in INFO lines (e.g. the cfn-response "Response body") — and anything unrecognized passes through verbatim.

When logs genuinely can't be retrieved, the message distinguishes an empty group ("No log events found…") from a failed fetch ("Could not fetch logs… the credentials may lack logs:FilterLogEvents"), and the console deep-link is always emitted so the full logs are one click away.

Changes

  • lib/api/diagnosing/resource-investigation.ts — custom-resource investigation, log-group resolution, stream targeting, and log normalization (shares the log-trimming helper with the ECS path).
  • lib/api/stack-events/resource-errors.ts — optional ResourceError.timestamp, populated from stack events.
  • lib/api/aws-auth/sdk.tsgetFunctionConfiguration on the Lambda client (fallback log-group discovery).
  • test/api/diagnosing/resource-investigation.test.ts — unit tests for ServiceToken resolution (ARN / GetAtt array+string / Ref), log-group resolution (convention / template literal / template Ref / generated name), stream targeting + timestamp window, log normalization (text/JSON/platform/malformed), and the empty-vs-error messaging.

Testing

  • Unit tests as above; new behaviors are mutation-verified (each fix's test fails when the fix is reverted).
  • Verified end-to-end against live deployments: a cfn-response custom resource failing on create and on update (iankhou/cdk-app-with-problems@d733eb6), and an advanced-logging variant (iankhou/cdk-app-with-problems@571c33d) (custom log group, function deleted by rollback) — confirming the template-sourced log-group resolution surfaces the real logs without the live function.

Notes

  • Limitation: if a custom log group is itself a CFN-managed resource deleted by rollback without a retain policy, the log data is gone (the resolution still names the right group). The default /aws/lambda/<fn> group's data persists regardless.

Checklist

  • This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed
    • Release notes for the new version:

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

…failure

When a custom resource (Custom::* or AWS::CloudFormation::CustomResource) fails
to deploy, fetch the backing Lambda's CloudWatch logs and surface them as
additional diagnostic context, mirroring the ECS service investigation.

- Resolve the backing Lambda by reading the resource's ServiceToken from the
  stack's original template (literal ARN, Fn::GetAtt, or Ref via
  describeStackResources).
- Derive the log group from the /aws/lambda/<fn> convention; only call
  getFunctionConfiguration to read a custom LoggingConfig.LogGroup if the
  convention group has no events. (cfn-response usage does not imply the default
  group; advanced logging controls can override it.)
- Target the exact failing invocation by extracting the log stream name from the
  cfn-response status reason ("See the details in CloudWatch Log Stream: <name>").
- Bound the query to a window around the failure event timestamp so update and
  rollback failures resolve the right invocation, not the latest stream. Adds an
  optional ResourceError.timestamp for this.
- Add getFunctionConfiguration to the Lambda SDK client.

All exploratory calls are best-effort: failures are logged at debug level and
never break diagnosis. Verified end-to-end against a live cfn-response custom
resource deployment failure.
@github-actions github-actions Bot added the p2 label Jun 18, 2026
@iankhou iankhou changed the title feat(toolkit-lib): surface custom resource Lambda logs on deployment failure feat(aws-cdk): surface custom resource Lambda logs on deployment failure Jun 18, 2026
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@aws-cdk-automation aws-cdk-automation requested a review from a team June 18, 2026 21:28
@iankhou iankhou requested a review from Copilot June 18, 2026 21:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

…ive function

A first-deploy custom resource failure rolls back and deletes the backing
Lambda, so getFunctionConfiguration is unavailable exactly when we need the
configured (advanced-logging) log group. Read LoggingConfig.LogGroup from the
stack template instead — the template survives rollback — handling both a
literal value and the common CDK shape where it is a Ref to an
AWS::Logs::LogGroup resource. Fall back to the live function configuration only
when the template value is an unresolvable intrinsic or the function is defined
outside this stack.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/resource-investigation.ts Outdated
Comment thread packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/resource-investigation.ts Outdated
iankhou added 5 commits June 19, 2026 15:00
…g group names

The template-sourced log group only handled a literal LogGroupName. The common
CDK case is an AWS::Logs::LogGroup with no explicit name, where CloudFormation
generates the physical name — absent from the template. Resolve that name via
describeStackResources (which still returns RETAINed/orphaned resources after a
rollback), so advanced-logging functions whose backing Lambda was deleted by
rollback still surface their logs. Extract a shared resolvePhysicalId helper used
by both the ServiceToken and log-group resolution paths.

Verified end-to-end against a live advanced-logging custom resource deployment.
Parse Lambda CloudWatch log events into readable lines before rendering, on the
custom-resource path only (ECS logs are arbitrary container output and are left
as-is). Handles both Lambda log formats:

- Text: strip the per-line timestamp/requestId prefix, render aligned
  'LEVEL message'.
- JSON: render the level + message (and error envelopes' errorMessage +
  stackTrace) instead of dumping raw JSON objects.

Drops only Lambda platform boilerplate (INIT_START/START/END/REPORT and JSON
platform.* events). Application output is never dropped by level — failure
detail often rides in INFO lines (e.g. the cfn-response 'Response body'). Lines
we don't recognize pass through verbatim, and full logs remain available via the
console link.
- Confirm an intrinsic ServiceToken resolves to an AWS::Lambda::Function before
  treating its physical ID as a function name. A Ref/GetAtt to a non-Lambda
  resource whose physical ID is a bare name would otherwise produce a misleading
  /aws/lambda/<name> log lookup. (resolveStackResource now returns ResourceType.)
- Distinguish a failed log fetch (e.g. missing logs:FilterLogEvents, or the group
  doesn't exist) from a genuinely empty log group, so the user-facing message
  reflects the real cause instead of always saying 'No log events found'.

Both addressed from PR review feedback; each covered by a mutation-verified test.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/resource-investigation.ts Outdated
Comment thread packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/resource-investigation.ts Outdated
iankhou and others added 3 commits June 22, 2026 15:21
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…tion

resource-investigation.ts had grown to mix orchestration (the AWS-calling
investigate/fetch/resolve functions) with a pile of pure utilities. Move the
pure helpers to dedicated homes, leaving resource-investigation focused on
orchestration (908 -> 681 lines):

- CloudWatch log formatting (trimToRecentLines, parseLambdaLogEvents + helpers,
  cloudWatchLogsConsoleUrl, MAX_LOG_LINES) -> format-utils.ts
- Resource identifier parsers (parseEcsServiceIdentifier,
  serviceTokenReferencedLogicalId, functionNameFromArnOrName, extractLogStreamName,
  logStreamNameFromPhysicalId, ecsStoppedTasksConsoleUrl) -> new resource-identifiers.ts

Behavior-preserving; tests only change import paths.
@iankhou

iankhou commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

More thorough log examples:

cfn-response module - Output of cdk deploy with JSON lambda logs, logging to default lambda log group

Logs from testing against the following project: https://github.com/iankhou/cdk-app-with-problems/tree/iankhou-cr-main on commit iankhou/cdk-app-with-problems@d733eb6

TEXT lambda logs also yields basically the same result

FailingCustomResourceStack: deploying... [1/1]
4:02:19 PM | CREATE_FAILED           | AWS::CloudFor
mation::CustomResource | MyFailingResource
Received response status [FAILED] from custom resour
ce. Message returned: See the details in CloudWatch
Log Stream: 2026/06/19/[$LATEST]2e517e59d4c0461cb4f3
f52e4a9064f7 (RequestId: 9f3d93cb-f86b-41dc-b2e2-7b0
a00033b82)

❌  FailingCustomResourceStack failed: DeploymentError: Resource updates failed:
FailingCustomResourceStack/MyFailingResource/Default  (AWS::CloudFormation::CustomResource MyFailingResource)
  Received response status [FAILED] from custom resource. Message returned: See the details in CloudWatch Log Stream:
  2026/06/19/[$LATEST]2e517e59d4c0461cb4f3f52e4a9064f7 (RequestId: 9f3d93cb-f86b-41dc-b2e2-7b0a00033b82)

   Logs from /aws/lambda/FailingCustomResourceStack-CrHandler8F2F7DBF-pq4JFc9Ri647:
   INFO  request type: "Create"
   ERROR Boom: simulated custom resource failure on Create
   INFO  Response body:
 {"Status":"FAILED","Reason":"See the details in CloudWatch Log Stream: 2026/06/19/[$LATEST]2e517e59d4c0461cb4f3f52e4a9064f7","PhysicalResourceId":"2026/06/19/[$LATEST]2e517e59d4c0461cb4f3f52e4a9064f7","StackId":"arn:aws:cloudformation:us-east-1:053108159643:stack/FailingCustomResourceStack/acb17810-6c19-11f1-9f58-0e525b316913","RequestId":"9f3d93cb-f86b-41dc-b2e2-7b0a00033b82","LogicalResourceId":"MyFailingResource","NoEcho":false,"Data":{"error":"simulated"}}
   INFO  Status code: 200
   INFO  Status message: OK
   INFO  request type: "Delete"
   INFO  Response body:
 {"Status":"SUCCESS","Reason":"See the details in CloudWatch Log Stream: 2026/06/19/[$LATEST]2e517e59d4c0461cb4f3f52e4a9064f7","PhysicalResourceId":"2026/06/19/[$LATEST]2e517e59d4c0461cb4f3f52e4a9064f7","StackId":"arn:aws:cloudformation:us-east-1:053108159643:stack/FailingCustomResourceStack/acb17810-6c19-11f1-9f58-0e525b316913","RequestId":"25da2754-c695-48c4-ba84-41199cf5128e","LogicalResourceId":"MyFailingResource","NoEcho":false,"Data":{}}
   INFO  Status code: 200
   INFO  Status message: OK
   Logs: https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#logsV2:log-groups/log-group/$252Faws$252Flambda$252FFailingCustomResourceStack-CrHandler8F2F7DBF-pq4JFc9Ri647

@github-actions

Copy link
Copy Markdown
Contributor

Total lines changed 1204 is greater than 1000. Please consider breaking this PR down.

@iankhou

iankhou commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Sorry for the big PR! Includes some refactoring to move util functions out of resource-investigation.ts, which was getting bloated.

@rix0rrr rix0rrr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3 it!

Couple small nits.

*
* This is Lambda-specific; it is not applied to ECS logs, which are arbitrary container output.
*/
export function parseLambdaLogEvents(events: Array<{ message?: string }>): Array<{ message: string }> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says you are normalizing to LEVEL message, so that's 2 fields.

The return type here only has message, so either that's a string containing both, or the comment is wrong.

  • If the return type is a combined string, just return string[]
  • Otherwise add level: string and leave it to the caller to format these.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will correct the documentation and adjust the field names the prevent confusion.

}

/**
* If `line` is a JSON-format Lambda log object, render it as `LEVEL<tab>message`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not to <tab>s, I don't trust em.

Pad with spaces to the desired length.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was actually using spaces, the documentation here was wrong. Will make the correction, thanks for the catch!

* in the status reason ("See the details in CloudWatch Log Stream: <name>"), so we can
* target that exact invocation.
*/
async function investigateCustomResource(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these functions should probably go into their own files as well. investigate-custom-resource.ts, investigate-ecs-service.ts ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

Move investigateEcsService and investigateCustomResource into their own
files (investigate-ecs-service.ts, investigate-custom-resource.ts),
leaving resource-investigation.ts as the dispatcher. Simplify the log
line plumbing: parseLambdaLogEvents now returns string[] (the level is
already combined into each rendered line) and trimToRecentLines operates
on lines rather than raw CloudWatch events.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants