aws / aws/aws-cdk

(aws-bedrock-alpha): GuardrailVersion logical id is hashed from an unresolved token, so every deploy replaces the version

Open
#38,674 0 comments 1 reaction 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.