feat(aws): CloudFormation-compatible custom resources, and bucket notifications on imported / shared buckets - #163
feat(aws): CloudFormation-compatible custom resources, and bucket notifications on imported / shared buckets#163so0k wants to merge 4 commits into
Conversation
…r-cfncompat Ports aws-cdk-lib/core custom-resource.ts (renderResourceType/uppercaseProperties verbatim) onto cfncompat_custom_resource; AwsStack gains a lazy cfncompat provider singleton and a per-stack force_destroy response bucket used unless the provider config names one. CustomResourceHandler is a getOrCreate stack singleton built from iam.Role + LambdaFunction.
…shared buckets Ports aws-s3 notifications-resource (v2.233.0) onto the cfncompat CustomResource core: NotificationsResourceHandler (stack singleton running the CDK Python handler verbatim) and BucketNotificationsResource (Custom::S3BucketNotifications, Managed=false). Imported buckets always use it; owned buckets keep the native aws_s3_bucket_notification unless the context key @terraconstructs/aws-s3:keepNotificationInImportedBucket is set. Adds the three-stack bucket-notifications-cross-stack integ app and TestBucketNotificationsCrossStack.
…rget and OpenTofu registry note
…t runs unmodified
|
In-repo integ now runs unmodified under OpenTofu: |
sakul-learning
left a comment
There was a problem hiding this comment.
Review: changes requested
I reviewed head 021f6779dcf86a5bc899e4b9843332a70e9aa485, ran the configured checks, and ran the new cross-stack integration target against AWS account 694710432912 in us-east-1.
Blocking correctness findings
-
src/aws/storage/notifications-resource-handler.ts:55-93— an unmanagedUpdaterewrites notification IDs owned by other stacks.get_id(n)clears and replacesn['Id']in place. TheUpdatepath at line 71 calls it for every existing queue, topic, and Lambda notification while deciding which entries are external. A notification owned by stack B can remain inexternal_notifications, but its dictionary has already been mutated using stack A'sstack_id; lines 89-93 then PUT that changed ID back to S3. A later Delete from B searches for the B prefix and can no longer recognize/remove its notification.Please derive comparison IDs without mutating the existing notification, and add a handler-level regression that changes A's notification properties, verifies B/C IDs remain byte-for-byte unchanged, then deletes B and verifies only B's entry disappears. The current
redeploy_aintegration stage reapplies unchanged Terraform and reportsNo changes, so it never sends the custom resource anUpdateevent and does not cover this path. -
src/aws/storage/index.ts:3exportsBucketNotificationsResourcedespite its internal-only contract.bucket-notifications-resource.tssays only one instance may exist for a bucket/stack pair and that the construct is not exported publicly, but the storage barrel exports it. The generated.jsiiassembly confirmsterraconstructs.aws.storage.BucketNotificationsResourceandBucketNotificationsResourcePropsas stable public cross-language types. Please remove the public barrel export or deliberately support and document direct construction; retracting an accidentally published JSII API later is a breaking change.
Live integration result and teardown findings
make bucket-notifications-cross-stack ultimately passed with full A/B/C deployment, notification/delivery validation, B removal, A/C revalidation, and teardown:
--- PASS: TestBucketNotificationsCrossStack (427.80s)
PASS
ok github.com/terraconstructs/base/integ/aws/storage 427.836s
The target is valuable standalone/on-demand coverage for real multi-stack notification ownership and delivery, but the run exposed three cleanup/isolation problems:
-
Deterministic fixture identities collide across runs.
integ/aws/storage/apps/bucket-notifications-cross-stack.ts:37-44hard-codesg${owner}12345678-1234, so every run reuses/aws/lambda/{ga,gb,gc}12345678-1234-buc5f094a3db834Handler. Two zero-byte groups left from an earlier run caused the first two attempts to fail withResourceAlreadyExistsException, even though the bucket and result resources use a random suffix. After explicit cleanup of those stale groups and manual destruction of partial state, the third run passed. The handler stack identity should be unique per test run while remaining stable across that run. -
An early failure can prevent cleanup of resources that did deploy.
storage_test.go:95-105unconditionally starts deferred teardown with C.util.UndeployUsingTerraformfatally loads C's savedTerraformOptions; if deployment failed before C was synthesized/deployed, the missingc/.test-data/TerraformOptions.jsonaborts cleanup before B and A. Each stack cleanup should be conditional on saved state/options, and one absent/failing stack should not suppress cleanup of the others. -
A successful destroy can still leave the custom-resource handler log group. The final run's generated Terraform has the correct dependency graph: the custom resource depends on the handler Lambda and its explicit log group. The actual stack-C destroy sequence was also correctly ordered: custom resource Delete completed at
03:55:37Z, Lambda deletion at03:55:43Z, and log-group deletion at03:55:45Z. Nevertheless, after OpenTofu reported empty A/B/C states, AWS contained a new zero-byte/aws/lambda/gc12345678-1234-buc5f094a3db834HandlerwithcreationTime=2026-08-29T03:55:45.350Z. I removed it manually and verified the test bucket, Lambdas, queues, response buckets, IAM roles, fixed handler log groups, and all three OpenTofu states were empty.I do not think the evidence shows a missing dependency or a straightforward
cfncompatimplementation error.cfncompatv0.2.0 invokes Lambda withInvocationTypeEvent, then waits for the handler's S3 response. AWS documents that CloudFormation also invokes custom-resource Lambdas asynchronously. The likely race is that the handler sends its success response before Lambda's delayed runtime logging is quiescent; Terraform then deletes the explicitly managed log group, whileAWSLambdaBasicExecutionRolestill permits Lambda to recreate/aws/lambda/<function-name>. AWS notes that Lambda log delivery can lag by minutes: https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html.This is still a product/test cleanup defect worth addressing here: prevent the explicitly managed group from being recreated (for example, avoid granting
logs:CreateLogGroupwhen the group is pre-created, with appropriately scoped stream/write permissions), or otherwise make teardown tolerate and verify this asynchronous logging lifecycle. The integration target should assert that the fixed handler groups are absent after teardown so this cannot silently poison the next run.
Checks
pnpm compile— PASS (0 errors; existing JSII warnings only)pnpm eslint— PASSmake bucket-notifications-cross-stack— PASS in 427.80s after removing stale prior-run groups- GitHub CI — green at this head
git diff --check origin/main...HEAD— reports trailing whitespace atsrc/aws/storage/notifications-resource-handler.ts:71
Ponytail artifact-value pass: the real-AWS integration scenario earns its maintenance cost, but it currently misses the handler Update regression and needs the isolation/cleanup hardening above to remain repeatable.
Review correction and refocusAfter comparing The
|
feat(aws): CloudFormation-compatible custom resources, and bucket notifications on imported / shared buckets
Summary
Adds a CloudFormation-shaped custom-resource primitive to TerraConstructs, backed by the
cdktn-io/cfncompatTerraform provider, and uses it to lift the long-standing restrictionthat S3 bucket notifications can only be managed by the stack that owns the bucket.
Two commits, on top of
origin/main:feat(aws): CustomResource and CustomResourceHandler on @cdktn/provider-cfncompat—aws.CustomResource(a port ofaws-cdk-lib/core/lib/custom-resource.ts, wrappingcfncompat_custom_resource) andaws.CustomResourceHandler(a stack-singleton Lambdabacking one or more custom resources).
AwsStackgrows a lazycfncompatProvidersingleton, an optional
cfncompatProviderConfigprop, and a lazy per-stackcustomResourceResponseBucket.feat(storage): custom-resource bucket notifications for imported and shared buckets—NotificationsResourceHandler(AWS CDK's Python handler, verbatim) andBucketNotificationsResource(Custom::S3BucketNotifications,Managed: "false").BucketBase.addEventNotification/enableEventBridgeNotificationnow pick between thenative
aws_s3_bucket_notificationresource and the custom resource.Imported buckets (
Bucket.fromBucketNameand friends) always use the custom resource — it isthe only way to attach a notification to a bucket the stack does not own. Owned buckets keep
the native resource unless the
@terraconstructs/aws-s3:keepNotificationInImportedBucketcontext key is set, matching AWS CDK's feature-flag name.
Docs: new
src/aws/storage/README.md("Notifications on imported / shared buckets"), and thenew integ target documented in
integ/aws/storage/README.md.Design decisions
(
Managed: "false"). On every apply the handler reads the bucket's existing notificationconfiguration, keeps everything it does not recognise, and merges in only this stack's own
entries, identified by an
Idprefixed with the stack id. That is what lets several stacks— the owning stack included — attach notifications to one bucket.
stackId=gridUUID. The handler distinguishes its own entries byStackIdprefix, sothe value must be stable across applies.
gridUUIDis; a construct path is not.FeatureFlagsport. Read withnode.tryGetContext; jsii has noexported constants, so the literal key string is the public API and
S3_KEEP_NOTIFICATION_IN_IMPORTED_BUCKETincx-api.tsstays internal (same treatment asTARGET_PARTITIONS).owned bucket destroys the native resource — which wipes the bucket's whole notification
configuration — unordered against the custom resource's Put. Called out in
addEventNotification's JSDoc, incx-api.ts, and in the storage README.cfncompat_custom_resourcedelivers handler responses through apre-signed S3 URL. Each stack lazily creates one
CustomResourceResponsesBucketwithforce_destroy(response objects are written at apply time and are not tracked in state, sodestroy would otherwise fail on a non-empty bucket). Setting
cfncompatProviderConfig.customResourceBucketopts out and defers to the provider's bucket.cfncompatprovider block or a response bucket.AwsStackrequiresstorage/bucketlazilyto break the
aws-stack↔bucketimport cycle.asset; that pipeline does not exist here, so the source is inlined verbatim (precedent:
compute/ecs/drain-hook). Upstream's comment-stripping is dropped — it only exists to fitCloudFormation's 4 KiB inline
ZipFilelimit, andCode.fromInlinerenders adata.archive_file.onEvent/isCompletestate machine: Terraform appliesare synchronous, so a plain Lambda invoked by
cfncompat_custom_resourcesuffices.Testing evidence
Unit (
pnpm exec jest test/aws/storage test/aws/custom-resource): 18 suites, 375 passed, 2skipped, 0 failed. New:
test/aws/custom-resource.test.ts(13),test/aws/custom-resource-handler.test.ts(6),test/aws/storage/notification-custom-resource.test.ts(15), plus imported-bucket cases intest/aws/storage/bucket.test.ts.Also green:
pnpm compile,pnpm exec eslinton every touched file,pnpm exec projen(nodrift). The repo sets
docgen: falseand ships noAPI.md, so there is no generatedreference to regenerate.
Live AWS run —
TestTconsPASS (1195s), account 694710432912 / us-east-1, usingterraconstructs@0.0.0.jsii.tgzbuilt from this branch. Three stacks share one bucket; A ownsit, B and C import it by name:
{a}{a,b}{a,b,c}{a,b,c}No changes. Your infrastructure matches the configuration.{a,c}Re-applying the owning stack leaving B and C's entries intact is the property the whole change
exists for.
OpenTofu registry limitation. The in-repo integ target
make bucket-notifications-cross-stack(three stacks, entry-set assertions after every stageplus an end-to-end delivery check per owner) could not be run. Terratest invokes
tofu(hardcoded in
integ/aws/util.go) andregistry.opentofu.orgdoes not servecdktn-io/cfncompat, so init fails withFailed to query available provider packages. Thescenario was therefore validated by the equivalent standalone harness run above. Running the
in-repo target today requires a
filesystem_mirrorforcdktn-io/cfncompatin the OpenTofuCLI configuration, or pointing terratest at a
terraformbinary; this is documented ininteg/aws/storage/README.md.Follow-ups
cdktn-io/cfncompatto the OpenTofu registry (or make the integ helpers' terraformbinary configurable) so
bucket-notifications-cross-stackruns in CI.(recorded as
not ported:lines innotification-custom-resource.test.ts).test/aws/storage/notification.test.ts,where they are commented out.
example) can now reuse
aws.CustomResource.Update — in-repo integ under OpenTofu
cdktn-io/cfncompatis now onregistry.opentofu.org(opentofu/registry #5338 + key #5340).make bucket-notifications-cross-stack(tofu, unmodified) →TestBucketNotificationsCrossStackPASS (472s, account 694710432912 / us-east-1, 2026-08-29). The OpenTofu limitation noted above no longer applies.