(aws-bedrock-alpha): GuardrailVersion logical id is hashed from an unresolved token, so every deploy replaces the version
- Dominant language
- TypeScript
- Stars
- 12.9k
- Forks
- 4.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 83
Description
### Describe the bug
`Guardrail.createVersion()` derives the version's construct id — and therefore its CloudFormation **logical id** — by hashing `guardrail.lastUpdated`, which is the CloudFormation runtime attribute `AttrUpdatedAt`. At synth time that attribute is an unresolved CDK token, so the hash is taken over a placeholder string like `${Token[TOKEN.21]}` rather than over anything about the guardrail.
Token numbers are assigned sequentially as tokens are minted during app construction, so the number depends on how many tokens were created earlier in the app. Any unrelated change upstream in the stack shifts it, which changes the hash, which changes the logical id.
A changed logical id is a different resource to CloudFormation. So an otherwise no-op deploy destroys the existing `AWS::Bedrock::GuardrailVersion` and creates a replacement, and every resource referencing the version ARN shows as modified.
The two lines involved, in `2.260.0-alpha.0`:
```js
// bedrock/guardrails/guardrails.js
this.lastUpdated = this.__resource.attrUpdatedAt;
// bedrock/guardrails/guardrail-version.js
const hash = md5hash(props.guardrail.lastUpdated ?? 'Default');
// → child id: `GuardrailVersion-${hash.slice(0, 16)}`
```
### Regression?
- [ ] Select this option if this issue appears to be a regression.
### Last known working version
_No response_
### Expected Behavior
Synthesizing the same guardrail configuration twice produces the same `GuardrailVersion` logical id, so a deploy that changes nothing about the guardrail does not replace the version.
### Current Behavior
The logical id changes whenever the token counter shifts. The guardrail's own configuration is irrelevant to it.
Adding unrelated constructs *before* the guardrail — nothing about the guardrail itself changing — produces three different logical ids:
```
0 fillers -> …GuardrailVersion1a00c205979bd7920AE80EEC
1 filler -> …GuardrailVersion405e6c07861746afA75F5480
2 fillers -> …GuardrailVersion8d92dd26d63506f0F7ECFD73
Full ids share the prefix
`InputGuardrailGuardrailVersione402e22a022f0007`; only the trailing
hash differs.
```
And the value being hashed is visibly a token placeholder, whose number moves:
```
0 fillers: lastUpdated = ${Token[TOKEN.21]} (unresolved=true)
1 fillers: lastUpdated = ${Token[TOKEN.31]} (unresolved=true)
```
In a real stack this surfaces as a spurious replacement in every `cdk diff`:
```
[-] AWS::Bedrock::GuardrailVersion InputGuardrail/GuardrailVersion-… destroy
```
plus every consumer of the version ARN reported as changed — in our stack, ten Lambda functions. It also makes "this change is template-identical" impossible to verify by hand during review, which is how we found it.
### Reproduction Steps
```js
const { App, Stack } = require('aws-cdk-lib');
const bedrock = require('@aws-cdk/aws-bedrock-alpha');
function synth(extraTokensBefore) {
const app = new App();
const stack = new Stack(app, 'S');
// Any construct minting CFN tokens first shifts the token counter.
for (let i = 0; i < extraTokensBefore; i++) {
new bedrock.Guardrail(stack, `F${i}`, { guardrailName: `f-${i}` });
}
const g = new bedrock.Guardrail(stack, 'InputGuardrail', {
guardrailName: 'g',
});
g.createVersion('v');
const tpl = app.synth().getStackByName('S').template;
return Object.keys(tpl.Resources)
.filter((k) => k.includes('GuardrailVersion'));
}
console.log(synth(0)[0]);
console.log(synth(1)[0]);
console.log(synth(2)[0]);
```
All three print a different logical id. The `InputGuardrail` declaration is identical in each.
### Possible Solution
Hash the guardrail's **configuration** rather than a deploy-time attribute — name, description, content/topic/word/PII filters, thresholds — so the id is stable across synths and changes only when the guardrail actually changes. That also gives `createVersion` the semantics its name implies: a new version id when the configuration is new.
If the current behaviour is intended as "always cut a fresh version on deploy", the id should not be content-addressed at all, since it is not addressing content — a plain deterministic child id (`GuardrailVersion`, or one derived from the `description` argument) would express that without the replacement churn. Either way, hashing an unresolved token seems unlikely to be deliberate.
Happy to open a PR if the maintainers have a preference between the two.
### Additional Information/Context
Found while reviewing an unrelated PR in our repo: the reviewed change claimed to be template-identical by default, and confirming that required normalizing this away first. It is not caused by that change — reproduced on the unmodified base, twice, with identical code.
### CDK CLI Version
2.1129.0 (build 629ca49)
### Framework Version
aws-cdk-lib 2.261.0, @aws-cdk/aws-bedrock-alpha 2.260.0-alpha.0
### Node.js Version
v24.10.0
### OS
macOS 15 (darwin arm64)
### Language
TypeScript
### Language Version
TypeScript 6.x
### Other information
_No response_
Contributor guide
Research direction
Start by reading bedrock/guardrails/guardrails.js and bedrock/guardrails/guardrail-version.js, focusing on how lastUpdated and the GuardrailVersion child id are derived. Run the supplied synth reproduction with 0, 1, and 2 fillers. Done means the synthesized logical id remains stable when unrelated constructs are added and changes only with the guardrail configuration or chosen version semantics.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, typescript
- Domain
- cloud, infrastructure
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100