fix(deployment): retry SNS/SQS resource-policy create on fresh-role propagation (#839) - #843
Merged
Conversation
…immediate-assume A new integ that maximizes the chance of catching an UNRETRIED IAM-propagation race on cdkd's fast SDK path. cdkd creates an IAM role and has a service assume it within ~1s, before IAM finishes propagating the role / its trust policy; CloudFormation tolerates this via deployment latency, cdkd does not. The race is handled narrowly today (RDS Enhanced Monitoring unprotected, and no integ deliberately stresses the breadth of fresh-role edges. CdkdIamPropagationStressExample creates four brand-new IAM roles, each consumed immediately by a different service in one deploy: - Lambda exec role -> AWS::Lambda::Function (CreateFunction) - SFN role -> AWS::StepFunctions::StateMachine (CreateStateMachine) - EventBridge target role -> AWS::Events::Rule SFN target (PutTargets) - fresh principal -> AWS::SQS::QueuePolicy + AWS::SNS::TopicPolicy No VPC / NAT / long-lived compute. The EventBridge rule is disabled on a 365-day schedule so it never fires, but PutTargets still validates the fresh target role at create time. The pass condition is: deploy SUCCEEDS. A deploy failure is a real cdkd finding (an unprotected consumer racing IAM propagation), so verify.sh prints which resource failed plus the error (deploy-log tail + cdkd events RESOURCE_FAILED lines + partial-state logicalId -> type map) for trivial triage, then still attempts destroy / cleanup. On success it asserts each role consumer works (invoke the Lambda + marker; start-execution -> poll to SUCCEEDED, proving the fresh SFN role and the SFN -> Lambda grant; list-targets-by-rule shows the SFN target bound to a role; queue/topic carry non-empty resource policies), then destroys and asserts the Lambda / state machine / rule / queue / topic / state file are each gone from AWS. verify.sh is BSD/macOS-portable (no grep -P / date -d), recovers the real deploy exit code from PIPESTATUS so a tee'd non-zero is not masked, traps aggressively (state + lock + deployment-events sidecar), and prints [verify] PASS only on full success. New scenario tag iam-fresh-role-immediate-assume in KNOWN_SCENARIOS (scripts/build-scenario-coverage-matrix.ts); coverage matrices regenerated; changelog + docs/testing.md entries added. Test-only change, no src edit. Not yet run against real AWS (a flaky failure there is the whole point).
…ropagation (#839) The iam-propagation-stress integ surfaced two deploy failures whose root cause is the same just-created-role IAM-propagation race already handled narrowly for RDS #794 / ECS #805 / Custom Resource #756: StressTopicPolicy: Invalid parameter: Policy Error: PrincipalNotFound StressQueuePolicy: Invalid value for the parameter Policy. Both the SNS TopicPolicy and the SQS QueuePolicy in the fixture name the same freshly-created PublisherRole ARN as Principal.AWS (verified in the synthesized template: the two policy documents are byte-for-byte the same shape). cdkd's fast SDK path issues the resource-policy PUT before IAM propagates the new role principal, so SNS rejects with the explicit PrincipalNotFound wording and SQS rejects with the less specific "Invalid value for the parameter Policy." -- both are the SAME race, not a malformed policy document. CloudFormation tolerates it via deployment latency; cdkd did not retry it. Add two narrowly-anchored patterns to RETRYABLE_ERROR_MESSAGE_PATTERNS so the existing withRetry absorbs the propagation window: - 'Policy Error: PrincipalNotFound' (SNS) - 'Invalid value for the parameter Policy' (SQS) Both anchors are specific enough that a genuinely malformed / non-existent principal still fails after the bounded retries. Unit tests assert the exact wire messages classify retryable, plus a negative guard that a permanently malformed policy without the race anchor stays non-retryable. No fixture change: both failures are confirmed races, so the fixture already exercises the real race path. Closes #839
…tions
The Phase 1b edge-3 assertion fed the EventBridge rule's PHYSICAL ID directly
to `aws events list-targets-by-rule --rule` and (post-destroy)
`describe-rule --name`. cdkd's EventBridgeRuleProvider stores the rule ARN as
the physical id (arn:<p>:events:<r>:<acct>:rule/<RuleName>), but those AWS CLI
flags expect the bare rule NAME, not the ARN — so the call returned a
ValidationException, the edge-3 target/role checks could not be satisfied, and
the run aborted after edge 1 + edge 2 had already printed OK.
Derive the bare rule name from the physical id by stripping everything up to
and including the last `/` (`${RULE_PHYSICAL_ID##*/}`). A bare name (no `/`)
passes through unchanged, and a custom-bus ARN (rule/<bus>/<name>) still yields
the trailing rule name. The destroy-phase `describe-rule --name "${RULE_NAME}"`
assertion now uses the correct name too.
Edge 4 (SQS QueuePolicy + SNS TopicPolicy presence) was already correct and is
unchanged.
…ress post-destroy assertion DeleteStateMachine is asynchronous: the state machine enters Status=DELETING and remains describable for a short window before AWS fully removes it (StateMachineDoesNotExist). The previous single-shot describe-state-machine assertion treated any successful describe as "still exists", racing AWS's async teardown and producing a false-positive "state machine still exists" failure. Replace it with a poll loop that tolerates Status=DELETING, treats a describe-failure / NotFound as gone, and fails fast on any non-DELETING status (delete never issued). cdkd's destroy was clean (13 deleted, 0 errors); this is a verify.sh assertion fix only, not a cdkd bug.
…the IAM-propagation window Wrap the edge-2 StepFunctions execution (start + poll) in an outer retry loop (up to 6 attempts, linear backoff) that starts a FRESH execution on the IAM-propagation authz signal (cause matching lambda:InvokeFunction / no identity-based policy allows / AccessDeniedException / not authorized to perform). Any non-authz terminal status fails immediately. Fixes an intermittent real-AWS false-positive: cdkd's fast SDK deploy returns before IAM propagates the SFN role's lambda:InvokeFunction grant to the SFN-assumed session, so the first execution started right after deploy can 403. CloudFormation never hits this because its slower finish lets IAM settle. cdkd's deploy is correct; this is a post-deploy runtime assertion that must tolerate the IAM propagation window.
…tion in iam-propagation-stress entry
…-at-timeout in iam-propagation-stress edge-2 Two review fixes for the iam-propagation-stress branch: - Add SQS QueuePolicy retry-classification boundary unit tests in retryable-errors.test.ts. The #839 SQS pattern `Invalid value for the parameter Policy` is intentionally broad (AWS emits it for any malformed SQS QueuePolicy). One test pins the accepted bounded-retry-then-surface tradeoff (a structurally-malformed QueuePolicy carrying that phrase classifies retryable); the other proves the substring match does not over-broaden to a different SQS error (InvalidAttributeValue) that lacks the phrase. Locks the contract so a future widening of the pattern is caught. - Handle a still-RUNNING execution at the inner-poll timeout in iam-propagation-stress edge-2. Previously a slow-but-not-failed execution left SM_STATUS=RUNNING, the cause query returned None, and the `*)` arm failed it immediately. Now a RUNNING status continues the outer retry loop (fresh execution after backoff) so a slow run gets more wall-clock across attempts; the outer loop stays capped at 6 and the post-loop guard fails only if it never SUCCEEDED.
go-to-k
force-pushed
the
test/iam-propagation-stress
branch
from
June 13, 2026 14:24
e23a822 to
49d483f
Compare
Owner
Author
|
Review complete: pr-code-reviewer + pr-test-reviewer ran (3-axis; spec-reviewer N/A for a bug fix without a design doc). Both findings fixed in 49d483f: (1) added SQS 'Invalid value for the parameter Policy' retry-classification boundary unit tests; (2) edge-2 SFN poll now handles RUNNING-at-timeout by continuing the bounded outer retry. Rebased on main. |
|
🎉 This PR is included in version 0.221.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a real cdkd deploy bug (issue #839) and adds a failure-seeking integration test that stresses four fresh-role IAM-propagation race edges in a single deploy.
Root cause (#839)
cdkd's fast SDK path creates an IAM role and then has a downstream resource consume it within roughly one second, before IAM has finished propagating the just-created role. CloudFormation never hits this because its own deployment latency lets IAM settle; cdkd does. For SNS
TopicPolicy/ SQSQueuePolicywhose policy document names a same-stack, just-created IAM role asPrincipal.AWS, the resource-policy PUT was issued before propagation completed and AWS rejected it:SetTopicAttributesrejects withInvalid parameter: Policy Error: PrincipalNotFound.SetQueueAttributesrejects the byte-for-byte-equivalent fresh-principal document with the less specificInvalid value for the parameter Policy.Neither phrase was in cdkd's retry classifier, so the create failed permanently instead of retrying through the propagation window.
Fix
Adds two narrowly-anchored patterns to
RETRYABLE_ERROR_MESSAGE_PATTERNSinsrc/deployment/retryable-errors.ts:Policy Error: PrincipalNotFound(SNS wording)Invalid value for the parameter Policy(SQS wording)Both are anchored on the full vendor phrase so a genuinely malformed or non-existent principal (typo'd ARN, deleted role) still burns only the bounded retries before surfacing - they do not false-positive unrelated SNS/SQS parameter-validation errors. Unit tests in
tests/unit/deployment/retryable-errors.test.tsassert both wire-format messages classify as retryable.New integ: iam-propagation-stress
tests/integration/iam-propagation-stress/**- a race detector that creates FOUR brand-new IAM roles in ONE deploy, each consumed IMMEDIATELY by a DIFFERENT service so the DAG carries many independent race edges at once:AWS::Lambda::Function(CreateFunctionvalidateslambda.amazonaws.comcan assume it)AWS::StepFunctions::StateMachine(CreateStateMachinevalidates the role; the SFN provider has no propagation retry of its own)AWS::Events::Rulewith an SFN target (PutTargetsvalidates the rule can assume the role toStartExecution)AWS::SQS::QueuePolicy+AWS::SNS::TopicPolicy(the resource-policy PUT validates the principal - this is the fix(deployment): SNS/SQS resource-policy create not retried on PrincipalNotFound (fresh-role propagation race) #839 edge)Everything is cheap (no VPC, no NAT, no long-lived compute); the EventBridge rule is
enabled: falseon a 365-day schedule so it never fires butPutTargetsstill validates the fresh role at create time.verify.sh assertion fixes
DeleteStateMachine's async deletion - the API returns before the state machine is actually gone, so a single immediatedescribe-state-machinecould falsely pass/fail.start-execution->describe-executionacross the IAM-propagation window, because the fresh SFN role can take a moment to become assumable for the SFN-to-Lambda invoke grant.Live validation against real AWS
This was validated GREEN end-to-end via
/run-integ iam-propagation-stressbefore opening this PR (the "live-test the changed behavior" requirement is satisfied):The first real-AWS run is what surfaced the #839 edge-4 failure that this PR fixes.
Lessons (retrospective)
DeleteStateMachinereturns before the resource is gone) and IAM-propagation windows (an SFN execution invoking a Lambda on a fresh role) - assert with bounded polling/retry, not a single immediate probe.Closes #839