Skip to content

Conversation

@Youssef-Beltagy
Copy link

@Youssef-Beltagy Youssef-Beltagy commented Jan 9, 2026

Current lambdaWrapWithClient handles PhysicalResourceID incorrectly and is inconsistent with documentation.

The current behavior doesn't set a fallback PhysicalResourceID when an error is returned by the lambdaFunction. It also doesn't reuse the old PhysicalResourceID when an update succeeds. This causes multiple problems:

  1. When Create/Update (and maybe Delete) fail, the error gets obfuscated in CFn UI/CLI.
  2. When Update succeeds, the PhysicalResourceID is set to lambda log stream. Lambda log streams change for different invocations, so a new PhysicalResourceID can be returned. CFn perceives this as a resource replacement and triggers a deletion request for the "old" PhysicalResourceID. This behavior is wrong and obscure.
  3. Delete validation is inconsistent with Typescript CDK: Typescript code validates that Delete requests should return the same PhysicalResourceID as the input. This is the only breaking change in this PR. I tested returning a different PhysicalResourceID but, honestly, I didn't see any issues (maybe point 2 happened and I didn't notice). I still made the change to maintain alignment with documentation and the typescript code.

References:

Tests

Before:

Create Failure:
Before Create Fail

Update Failure:
Before Update Fail

When update succeeds, there often is an undesired followup delete.

After:

Create Failure:
After Create Fail

Update Failure:
After Update Fail

Tests

Edit: Please use this repository to replicate/test https://github.com/Youssef-Beltagy/test-go-cdk-PR-613

I used this setup along with go workspaces.

CDK:

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib/core';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
import * as path from "path";
import * as iam from 'aws-cdk-lib/aws-iam';

export class AppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const hanlderFunc = new lambda.Function(this, "goCFnResourceHanlder", {
      runtime: lambda.Runtime.PROVIDED_AL2023,
      handler: "bootstrap",
      code: lambda.Code.fromAsset(path.join(__dirname, "../lambda")),
      architecture: lambda.Architecture.X86_64,
      timeout: cdk.Duration.minutes(2),
      memorySize: 512,
      retryAttempts: 0,
    });
    hanlderFunc.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess'))

    new cdk.CustomResource(this, `CustomResourceConstruct`, {
      resourceType: 'Custom::TestCustomResourceConstruct',
      serviceToken: hanlderFunc.functionArn,
      properties: {
        myInput: "AFTER: ybeltagy test delete series succeed 10",
        shouldFail: false
      },
    });
  }
}

const app = new cdk.App();
new AppStack(app, 'CDKResourceStack-final', {
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});

Golang lambda:

package main

import (
	"context"
	"fmt"

	"github.com/aws/aws-lambda-go/cfn"
	"github.com/aws/aws-lambda-go/lambda"
)

func main() {
	fmt.Printf("Running \n")
	lambda.Start(cfn.LambdaWrap(handleCFNEvent))
}

func handleCFNEvent(ctx context.Context, event cfn.Event) (PhysicalResourceID string, data map[string]interface{}, err error) {
	fmt.Printf("received CFn event: %+v\n", event)

	data = event.ResourceProperties

	if event.RequestType == cfn.RequestDelete {
		return "", data, nil
	}

	if v, ok := data["shouldFail"]; ok && v == "true" {
		return "", data, fmt.Errorf("Received a fail request with message %s\n", data["myInput"])
	}

	fmt.Printf("Succeeding with: %s\n", data["myInput"])

	return "", data, nil
}

@codecov-commenter
Copy link

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.75%. Comparing base (d4fbc0b) to head (fb5a5fa).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #613      +/-   ##
==========================================
+ Coverage   73.59%   73.75%   +0.15%     
==========================================
  Files          35       35              
  Lines        1337     1345       +8     
==========================================
+ Hits          984      992       +8     
  Misses        277      277              
  Partials       76       76              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Youssef-Beltagy Youssef-Beltagy changed the title fix: always return PhysicalResourceId for CFn CustomResources fix: always return PhysicalResourceID for CFn CustomResources Jan 9, 2026
@bmoffatt
Copy link
Collaborator

bmoffatt commented Jan 9, 2026

When Update succeeds, the PhysicalResourceID is set to lambda log stream. Lambda log streams change for different invocations, so a new PhysicalResourceID can be returned. CFn perceives this as a resource replacement and triggers a deletion request for the "old" PhysicalResourceID. This behavior is wrong and obscure.

Agree. For updates, I think defaulting to the input physical resource id (as CDK does) is correct, and fixes a bug.

I do wonder if changing the default for Create events is the right thing to do though. I think following the CDK behavior is the least surprising thing, but wonder if the change is nessecary. It might also be a behavior change that could break someone.

As I review this though, I realize that defaulting to the LogStreamName for Create currently means it's possible to accidentally create many different logical resources with the same physical id, if there where many custom resource instances in a stack that used the same function, called non-concurrently such that the log stream was reused. IDK if CloudFormation dislikes this, but it seems wrong.


Delete validation is inconsistent with Typescript CDK: Typescript code validates that Delete requests should return the same PhysicalResourceID as the input. This is the only breaking change in this PR. I tested returning a different PhysicalResourceID but, honestly, I didn't see any issues (maybe point 2 happened and I didn't notice). I still made the change to maintain alignment with documentation and the typescript code.

Porting of the CDK logic makes sense, and looks correct to me.

What's the breaking behavior you think could happen here?

The scenario I could think of is a delete that always returns physical resource id "blah", would flip from succeeding to failing, which wouldn't be great. But that'd require CloudFormation also also not require matching physical resource ids for delete responses. In which case, I think we'd want to maintain that behavior. If CloudFormation does reject such responses, then following CDK's approach is fine, as the delete would have failed anyways.

@bmoffatt
Copy link
Collaborator

bmoffatt commented Jan 9, 2026

The scenario I could think of is a delete that always returns physical resource id "blah", would flip from succeeding to failing, which wouldn't be great. But that'd require CloudFormation also also not require matching physical resource ids for delete responses. In which case, I think we'd want to maintain that behavior. If CloudFormation does reject such responses, then following CDK's approach is fine, as the delete would have failed anyways.

I'll try to make some time to test CloudFormation's behavior here, and form a firmer opinion :)

// A previous physical resource id exists unless this is a create request.
fallbackPhysicalResourceID := event.PhysicalResourceID
if event.RequestType == RequestCreate {
// If this is a create request, the fallback should be the request ID
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm thinking falling back to the LogStreamName for RequestCreate is more backwards compatible.

However, switching to RequestId may be better for correctness with multiple custom resources in the same stack. There may well be another bug lurking here that needs fixing. But that could also land as a separate PR.

r.Status = StatusFailed
r.Reason = err.Error()
log.Printf("sending status failed: %s", r.Reason)
} else if event.RequestType == RequestDelete && event.PhysicalResourceID != r.PhysicalResourceID {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm going to test a scenario I'm worried about, will come back to this. Even though the CDK throws a validation error, this might be legal/ignored by CloudFormation, in which case this validation could break existing users.

Copy link
Author

Choose a reason for hiding this comment

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

It is legal/ignored by CloudFromation. That's why I called out this change as breaking.

Copy link
Author

@Youssef-Beltagy Youssef-Beltagy Jan 9, 2026

Choose a reason for hiding this comment

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

In fact, it definitely is legal/ignored because for all the CustomResources that were created and then deleted with this code, the deletion request would've had old PhysicalResourceId as "lambda-log-stream" and the new PhysicalResourceId would be "".

Copy link
Author

Choose a reason for hiding this comment

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

Though personally, I think it very unlikely for a customer workload to break because of this. They'd have to intentially try to break things.

@Youssef-Beltagy
Copy link
Author

Youssef-Beltagy commented Jan 9, 2026

About Create/Update

I'm making a clarification to my earlier statements to ease this conversation.

Lambda log streams change for different invocations

Specifically, when the lambda instance changes according to Lambda's docs

Lambda has a standard naming convention for log streams that uses a YYYY/MM/DD/[] format. The InstanceId is generated by AWS to identify the Lambda function instance.


I think following the CDK behavior is the least surprising thing, but wonder if the change is nessecary. It might also be a behavior change that could break someone.

We can do logstream name in create. But it'd mostly be perpetuating tech debt.

I don't think using RequestID will break someone's setup because it would be setting up new/clean resources. If the customers had a dependency on the format of the PhysicalResourceId, they would directly set it instead of using the defaults.

As I review this though, I realize that defaulting to the LogStreamName for Create currently means it's possible to accidentally create many different logical resources with the same physical id, if there where many custom resource instances in a stack that used the same function, called non-concurrently such that the log stream was reused. IDK if CloudFormation dislikes this, but it seems wrong.

I agree it seems wrong.

I'm not sure if it causes issues, though, because it felt like the PhysicalResourceId was tied to the CustomResource/LogicalId. Nothing in the docs implied this is a potential issue and it felt like the PhysicalResourceId is mostly used to know if it is an in-place or a replacement update.

It doesn't affect this PR, but I can POC if something breaks if I hardcode the PhysicalResourceId and create two resources.


About Delete

What's the breaking behavior you think could happen here?

It's an obscure case: if the customer returns a different PhysicalResourceID every time. I don't think we should worry about it because this would be an intentional breaking of the system... just in golang too.

Before:

  1. Object created with PhysicalResourceId as "randomString1"
  2. Deletion returns nil for err and returns "randomString2" for PhysicalResourceId
  3. Deletion succeeds

After:

  1. Object created with PhysicalResourceId as "randomString1"
  2. Deletion returns nil for err and returns "randomString2" for PhysicalResourceId
  3. Deletion fails

But that'd require CloudFormation also also not require matching physical resource ids for delete responses.

I POC'd that. CFn didn't fail. It succeeded the delete even when a different PhysicalResourceID is returned. It's also worth noting that I didn't see anything in the docs about deletion failing when returning a different PhysicalResourceId. I read all the docs I found. I'll but them below.

There was one time where the stack got stuck but I believe that was my error because I failed to replicate it multiple times afterwards while paying more attention to what I was doing.

But I may have done something wrong because testing this in general is very confusing.

References:


The scenario I could think of is a delete that always returns physical resource id "blah", would flip from succeeding to failing

No, this shouldn't fail because the PhysicalResourceID is always "blah". This is actually the ideal case.


I'll try to make some time to test CloudFormation's behavior here, and form a firmer opinion :)

I'll share more testing details in the description.

@Youssef-Beltagy
Copy link
Author

Youssef-Beltagy commented Jan 9, 2026

I recreated a repo with the test setup. You should be able to just clone and test with it: https://github.com/Youssef-Beltagy/test-go-cdk-PR-613

As a reminder (because I always forget), to trigger a CFn custom resource update, you need to update the input in the CDK/CFn. Updating the lambda handler doesn't trigger an CFn update.

Best wishes.


if r.PhysicalResourceID == "" {
r.PhysicalResourceID = fallbackPhysicalResourceID
log.Printf("PhysicalResourceID not set. Using fallback PhysicalResourceID: %s\n", r.PhysicalResourceID)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I remembered #237, which resulted in a revert of a similar change.

As I understand from re-reading that, if a create failure status response contains a physical resource id, CloudFormation will issue a followup delete request. Which makes sense, however, some existing setups might not handle a delete with an unrecognized physical resource id. And such a request today would not be issued, as CloudFormation instead swallows the creation failure with "invalid physical resource id".

Which sucks, because it means resolving the related #107 means re-introducing these (unexpected for some) deletes on create errors.

Copy link
Author

@Youssef-Beltagy Youssef-Beltagy Jan 9, 2026

Choose a reason for hiding this comment

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

As I understand from re-reading that, if a create failure status response contains a physical resource id, CloudFormation will issue a followup delete request.

As far as I could see, CFn always issues a delete request if create failed regardless of whether the PhysicalResourceID is returned or not. I can double check if you wish.


The problem with #237 was that, for DELETE request, a new PhysicalResourceID was returned. This is definitely wrong because #108 was implemented incorrectly. Do you see how it always defaults to the lambda logstream and doesn't try to use the old PhysicalResourceID. That was the problem.

if r.PhysicalResourceID == "" {
	log.Println("PhysicalResourceID must exist, copying Log Stream name")
	r.PhysicalResourceID = lambdacontext.LogStreamName
}

Actually, this may tie back to the discussion on whether Delete request should validate the returned PhysicalResourceId.

Copy link
Author

@Youssef-Beltagy Youssef-Beltagy Jan 9, 2026

Choose a reason for hiding this comment

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

I'm surprised #237 saw delete fail because of different Physical Resource ID. I wonder how/why they saw it. I didn't. Maybe CFn behavior changed in all this time, maybe it had to do with their logic (which I agree we should still support).

Actually, what they described is similar to the one time I saw my stack get stuck and then failed to replicate it. It may be that I just need to think more about the methodology for testing delete.

Copy link
Collaborator

Choose a reason for hiding this comment

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

My assumption about about a delete not being issued is wrong. Or at least, it's wrong today. Perhaps there was a behavior change since 2019 🤷‍♂️

When I try to validate this

package main

import (
        "context"
        "errors"

        "github.com/aws/aws-lambda-go/cfn"
        "github.com/aws/aws-lambda-go/lambda"
)

func main() {
        lambda.Start(cfn.LambdaWrap(func(ctx context.Context, e cfn.Event) (string, map[string]any, error) {
                if e.RequestType == cfn.RequestCreate {
                        return "", nil, errors.New("fail") // in v1.51.1, "" won't fallback to a sane default, a delete will not be issued
                }
                return e.PhysicalResourceID, nil, errors.New("in v1.51.1, this should never occur")
        }))
}

That error infact, does occur, a delete is issued, and we can see that in the stack events, CloudFormation came up with it's own PhysicalResourceId to pass in a delete when we failed to provide a valid one. Notice below, after the create fails for our invalid physical resource id, CloudFormation starts plumbing something else. "PhysicalResourceId": "Test3-Resouce-1FUJL4XBGXTTC",

...
        {
            ...
            "LogicalResourceId": "Resouce",
            "PhysicalResourceId": "Test3-Resouce-1FUJL4XBGXTTC",
            "ResourceType": "AWS::CloudFormation::CustomResource",
            "Timestamp": "2026-01-10T00:21:36.084000+00:00",
            "ResourceStatus": "DELETE_FAILED",
            "ResourceStatusReason": "Received response status [FAILED] from custom resource. Message returned: in v1.51.1, this should never occur (RequestId: b6be9d6b-6ed3-47c1-b115-74d16dcab99c)",
        },
        ...
        {
            ...
            "LogicalResourceId": "Resouce",
            "PhysicalResourceId": "Test3-Resouce-1FUJL4XBGXTTC",
            "ResourceType": "AWS::CloudFormation::CustomResource",
            "Timestamp": "2026-01-10T00:21:34.300000+00:00",
            "ResourceStatus": "DELETE_IN_PROGRESS",
            ...
        },
        ...
        {
            ...
            "LogicalResourceId": "Resouce",
            "PhysicalResourceId": "Test3-Resouce-1FUJL4XBGXTTC",
            "ResourceType": "AWS::CloudFormation::CustomResource",
            "Timestamp": "2026-01-10T00:21:31.490000+00:00",
            "ResourceStatus": "CREATE_FAILED",
            "ResourceStatusReason": "Invalid PhysicalResourceId",
            ...
        },
        {
            ...
            "LogicalResourceId": "Resouce",
            "PhysicalResourceId": "",
            "ResourceType": "AWS::CloudFormation::CustomResource",
            "Timestamp": "2026-01-10T00:21:28.684000+00:00",
            "ResourceStatus": "CREATE_IN_PROGRESS",
            ...
        },
...

I'm surprised #237 saw delete fail because of different Physical Resource ID. I wonder how/why they saw it. I didn't. Maybe CFn behavior changed in all this time, maybe it had to do with their logic (which I agree we should still support).

I suspect whatever issue occurred for this customer, (assuming the revert resolved this issue), could have began re-occuring at some point, but with that funny logical id looking thing rather than the funny log stream id that got reverted.

Actually, what they described is similar to the one time I saw my stack get stuck and then failed to replicate it. It may be that I just need to think more about the methodology for testing delete.

This feels familiar to me too, for an app I had where the API call was timing out.


I'll comment on #107 with this context too. #108 could probably be re-applied (either directly, or as a part of this PR's changes) given this observation.

Copy link
Author

Choose a reason for hiding this comment

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

It's not obvious if that was the behavior at the time of #237,

After reading more, I suspect #237 was a user-error. They were facing the issue all the way back in 2016 (they referenced this: remind101/empire/918). This was before #108 was introduced. CFn behavior was the same back then.

As you noted, according to remind101/empire/918, it seems that even if we don't return a PhysicalResourceID upon creation failure, CFn generates one anyway when it sends the delete. At least if we are the ones who create that dummy PhysicalResourceID, the actual error gets surfaced instead of "Invalid PhysicalResourceID".

@bmoffatt
Copy link
Collaborator

So on the delete validation part of this PR, here's a trivial app with custom resource function that would break.

app.go

package main

import (
        "time"

        "github.com/aws/aws-cdk-go/awscdk/v2"
        lambda "github.com/aws/aws-cdk-go/awscdklambdagoalpha/v2"
        "github.com/aws/jsii-runtime-go"
)

func main() {
        defer jsii.Close()

        app := awscdk.NewApp(nil)
        stack := awscdk.NewStack(app, jsii.String("Test"), &awscdk.StackProps{})

        handler := lambda.NewGoFunction(stack, jsii.String("Handler"), &lambda.GoFunctionProps{
                Entry: jsii.String("lambda"),
        })

        awscdk.NewCustomResource(stack, jsii.String("Resouce"), &awscdk.CustomResourceProps{
                ServiceToken: handler.FunctionArn(),
                Properties: &map[string]any {
                        "time": time.Now().Format(time.RFC3339),
                },
        })

        app.Synth(nil)
}

lambda/main.go

package main

import (
        "context"
        "time"

        "github.com/aws/aws-lambda-go/cfn"
        "github.com/aws/aws-lambda-go/lambda"
)

func main() {
        lambda.Start(cfn.LambdaWrap(func(ctx context.Context, e cfn.Event) (string, map[string]any, error) {
                return time.Now().Format(time.RFC3339), nil, nil
        }))
}

This app does an update-with-delete on every deploy.

Editing the go.mod with replace github.com/aws/aws-lambda-go => github.com/Youssef-Beltagy/aws-lambda-go v0.0.0-20260109120744-fb5a5fada270 to take this PR, then results in an updates to this stack... well not failing, but doing this weirdness

[███████████████████████████████████████████▌··············] (3/4)

3:50:32 PM | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack          | Test
3:50:34 PM | DELETE_FAILED        | AWS::CloudFormation::CustomResource | Resouce/Default
Received response status [FAILED] from custom resource. Message returned: DELETE: cannot change the physical resource ID from 2026-01-09T23:39:43Z to 2026-01-09T23:50:33Z during deletion (RequestId: 5986b481-f91e-44a7-9888-2e14ab599fc0)
3:53:37 PM | DELETE_FAILED        | AWS::CloudFormation::CustomResource | Resouce/Default
Received response status [FAILED] from custom resource. Message returned: DELETE: cannot change the physical resource ID from 2026-01-09T23:39:43Z to 2026-01-09T23:53:36Z during deletion (RequestId: 57084098-1646-4eb7-b90c-b960b5cb898e)

but not failing or rolling back, which is good I guess. Final stack state is

...
            "StackStatus": "UPDATE_COMPLETE",
            "StackStatusReason": "Update successful. One or more resources could not be deleted.",
...

destroying the stack however results in the classic stuck state

Are you sure you want to delete: Test (y/n) y
Test: destroying... [1/1]
4:01:41 PM | DELETE_FAILED        | AWS::CloudFormation::CustomResource | Resouce
Received response status [FAILED] from custom resource. Message returned: DELETE: cannot change the physical resource ID from 2026-01-09T23:50:29Z to 2026-01-10T00:01:39Z during deletion (RequestId: 44819a73-980b-421d-9a08-dc0a7e204912)


 ❌  Test: destroy failed ToolkitError: The stack named Test is in a failed state. You may need to delete it from the AWS console : DELETE_FAILED (The following resource(s) failed to delete: [Resouce]. )
...
The stack named Test is in a failed state. You may need to delete it from the AWS console : DELETE_FAILED (The following resource(s) failed to delete: [Resouce]. )
❌  Test failed: ValidationError: Stack:arn:aws:cloudformation:us-west-2:281058736431:stack/Test/42f56500-edb4-11f0-985a-0a36c440077d is in DELETE_FAILED state and can not be updated.

@Youssef-Beltagy
Copy link
Author

Youssef-Beltagy commented Jan 10, 2026

So on the delete validation part of this PR, here's a trivial app with custom resource function that would break.

This is the "breaking change" I tried to call out (also quoted below).

It's an obscure case: if the customer returns a different PhysicalResourceID every time. I don't think we should worry about it because this would be an intentional breaking of the system... just in golang too.

I did the same thing with timestamps in the lambda response before opening the PR. I'm aware of this behavior after my change.

The Delete behavior change is controversial. I only did it to maintain alignment with typescript CDK. So I was trying to understand why typescript CDK does what it does. I was wondering if CFn goes into a recursive delete cycle, fails in a problematic way, or something similar before my changes. That's what I failed to replicate/prove: your time.now() stack but before my changes. I apologize if my wording so far was confusing.

Always changing PhysicalResourceID before this PR

That said, I tried something similar to your example but before my changes at commit d4fbc0b070f2d682054a6f932528162e431a9de5.

The resources get deleted without issue. CFn ignores the Physical resource id returned from delete because it didn't change in the request result. This is in contrast to how the physical id changes at the end of update.

I don't know if this is new behavior from CFn because things used to break or if this was always the behavior and the typescript CDK ensures the same physical id is returned to make sure everything is aligned.

        {
            "StackId": "arn:aws:cloudformation:us-west-2:<my-account>:stack/CDKResourceStack-beyondFinal2/a6b7efe0-ee1d-11f0-9e13-0296883e7a1b",
            "EventId": "CustomResourceConstruct-DELETE_COMPLETE-2026-01-10T18:40:28.711Z",
            "StackName": "CDKResourceStack-beyondFinal2",
            "OperationId": "33265d25-9fe9-4a51-9b9c-2007ab2191d8",
            "LogicalResourceId": "CustomResourceConstruct",
            "PhysicalResourceId": "Returning-Time-2026-01-10T18:38:12Z",
            "ResourceType": "Custom::TestCustomResourceConstruct",
            "Timestamp": "2026-01-10T18:40:28.711Z",
            "ResourceStatus": "DELETE_COMPLETE",
            "ResourceProperties": "{\"ServiceToken\":\"arn:aws:lambda:us-west-2:<my-account>:function:CDKResourceStack-beyondFi-goCFnResourceHanlder7F9B-PoRTxF5rnSRU\",\"myInput\":\"testing always changing id Sat Jan 10 2026 18:37:17 GMT+0000 (Coordinated Universal Time)\",\"shouldFail\":\"false\"}",
            "ClientRequestToken": "Console-DeleteStack-9eacd5e8-5e87-33b4-be40-59addec7f366"
        },
        {
            "StackId": "arn:aws:cloudformation:us-west-2:<my-account>:stack/CDKResourceStack-beyondFinal2/a6b7efe0-ee1d-11f0-9e13-0296883e7a1b",
            "EventId": "CustomResourceConstruct-DELETE_IN_PROGRESS-2026-01-10T18:40:26.708Z",
            "StackName": "CDKResourceStack-beyondFinal2",
            "OperationId": "33265d25-9fe9-4a51-9b9c-2007ab2191d8",
            "LogicalResourceId": "CustomResourceConstruct",
            "PhysicalResourceId": "Returning-Time-2026-01-10T18:38:12Z",
            "ResourceType": "Custom::TestCustomResourceConstruct",
            "Timestamp": "2026-01-10T18:40:26.708Z",
            "ResourceStatus": "DELETE_IN_PROGRESS",
            "ResourceProperties": "{\"ServiceToken\":\"arn:aws:lambda:us-west-2:<my-account>:function:CDKResourceStack-beyondFi-goCFnResourceHanlder7F9B-PoRTxF5rnSRU\",\"myInput\":\"testing always changing id Sat Jan 10 2026 18:37:17 GMT+0000 (Coordinated Universal Time)\",\"shouldFail\":\"false\"}",
            "ClientRequestToken": "Console-DeleteStack-9eacd5e8-5e87-33b4-be40-59addec7f366"
        },
        {
            "StackId": "arn:aws:cloudformation:us-west-2:<my-account>:stack/CDKResourceStack-beyondFinal2/a6b7efe0-ee1d-11f0-9e13-0296883e7a1b",
            "EventId": "CustomResourceConstruct-UPDATE_COMPLETE-2026-01-10T18:38:14.387Z",
            "StackName": "CDKResourceStack-beyondFinal2",
            "OperationId": "5d46cc3a-6d8b-4e07-8980-8597d2a091a4",
            "LogicalResourceId": "CustomResourceConstruct",
            "PhysicalResourceId": "Returning-Time-2026-01-10T18:38:12Z",
            "ResourceType": "Custom::TestCustomResourceConstruct",
            "Timestamp": "2026-01-10T18:38:14.387Z",
            "ResourceStatus": "UPDATE_COMPLETE",
            "ResourceProperties": "{\"ServiceToken\":\"arn:aws:lambda:us-west-2:<my-account>:function:CDKResourceStack-beyondFi-goCFnResourceHanlder7F9B-PoRTxF5rnSRU\",\"myInput\":\"testing always changing id Sat Jan 10 2026 18:37:17 GMT+0000 (Coordinated Universal Time)\",\"shouldFail\":\"false\"}",
            "ClientRequestToken": "execa4851c87-ccd1-4c7b-b5fe-2f208d032308"
        },
        {
            "StackId": "arn:aws:cloudformation:us-west-2:<my-account>:stack/CDKResourceStack-beyondFinal2/a6b7efe0-ee1d-11f0-9e13-0296883e7a1b",
            "EventId": "CustomResourceConstruct-UPDATE_IN_PROGRESS-2026-01-10T18:38:14.157Z",
            "StackName": "CDKResourceStack-beyondFinal2",
            "OperationId": "5d46cc3a-6d8b-4e07-8980-8597d2a091a4",
            "LogicalResourceId": "CustomResourceConstruct",
            "PhysicalResourceId": "Returning-Time-2026-01-10T18:37:12Z",
            "ResourceType": "Custom::TestCustomResourceConstruct",
            "Timestamp": "2026-01-10T18:38:14.157Z",
            "ResourceStatus": "UPDATE_IN_PROGRESS",
            "ResourceStatusReason": "Requested update required the provider to create a new physical resource",
            "ResourceProperties": "{\"ServiceToken\":\"arn:aws:lambda:us-west-2:<my-account>:function:CDKResourceStack-beyondFi-goCFnResourceHanlder7F9B-PoRTxF5rnSRU\",\"myInput\":\"testing always changing id Sat Jan 10 2026 18:37:17 GMT+0000 (Coordinated Universal Time)\",\"shouldFail\":\"false\"}",
            "ClientRequestToken": "execa4851c87-ccd1-4c7b-b5fe-2f208d032308"
        },

I'm ok with removing the Delete PhysicalResourceID validation

I see value in maintaining alignment with typescript CDK and find it unreasonable to maintain the current behavior for obscure/bad lambda implementations. Especially, since the "fix" is trivial on the customers' side (returning the same PhysicalResourceID as the request in deletion).

But I know it is a breaking change with no clear benefit. So I don't mind removing the deletion validation part.

if event.RequestType == RequestDelete && event.PhysicalResourceID != r.PhysicalResourceID {
			r.Status = StatusFailed
			r.Reason = fmt.Sprintf("DELETE: cannot change the physical resource ID from %s to %s during deletion", event.PhysicalResourceID, r.PhysicalResourceID)
			log.Printf("sending status failed: %s", r.Reason)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants