Bedrock model access is three gates, not one permission
Before an AWS account can call a Bedrock foundation model, three independent things have to be
true at once. The account needs an active AWS Marketplace subscription to that model. The account
needs to have accepted the model's end-user license agreement, which AWS calls a model agreement.
And the calling role's IAM policy needs to actually grant the invoke action for that specific
model. Miss any one of the three and the API gives you back the exact same error: an
AccessDeniedException with a message that does not say which of the three you're missing. I
maintain a shared AWS platform for AI workloads at KnowBe4, and the design problem worth writing
about isn't the error. It's that we treated "can this account use this model" as three separate,
machine-checkable preconditions from the start, instead of leaving an engineer to guess from one
opaque string.
Why one error string isn't enough information
A subscription gate, a license-acceptance gate, and an IAM gate fail identically on purpose, from
AWS's side: AccessDeniedException is a generic access-control response, not a diagnostic. That's
a reasonable design for a vendor API surface. It's a bad one to inherit as your own team's first
line of defense, because the three fixes are completely different in who can apply them and how
fast. IAM is something we control entirely in Terraform. A model agreement is something we can
automate through the Bedrock API directly. A Marketplace subscription requires the AWS Marketplace
console and admin-level account access. The Lambda's IAM role even carries
aws-marketplace:Subscribe, added deliberately for a later automation attempt, but nothing in the
Lambda's own code calls it, so subscribing today still means a human in the Marketplace console.
If your only diagnostic signal is the error message, every one of these looks like the same ticket,
and the person triaging it has no way to know whether they're about to fix it in five minutes or
wait on an admin.
A nightly job that treats entitlement as its own concern
The core piece is a Lambda that runs nightly at 02:00 UTC on a flexible thirty-minute schedule. It
enumerates Bedrock foundation models for the account and region, and for any model from a
configured provider list (Anthropic, Amazon, Meta, Mistral, Cohere by default) that isn't yet
entitled, it calls CreateFoundationModelAgreement to accept the model agreement automatically.
Then it sends a one-token Converse call to warm the model and confirm it actually responds, not
just that the agreement call returned success. This closes gate two, licensing, without a human in
the loop, every night, across every account this Lambda covers. It does nothing for gate one, the
Marketplace subscription: the Lambda's role has the IAM permission to touch that gate, but nothing
in its code exercises it, so gate one still waits on a human in the console. And it does nothing
for gate three, IAM, because that's a property of the calling role, not
the model.
The job also runs today against a single region, and the runbook says so explicitly rather than
leaving it implied: AccessDenied is per-account and per-region, and this Lambda only covers that
one region so far. Stating the boundary in the runbook instead of letting someone discover it by
testing a second region is the same instinct as splitting the three gates apart: make the limits of
the system something you can read, not something you find by hitting them.
The gap between nightly and "a new model just shipped"
A nightly cadence is too slow the day a new model ships and a downstream team wants to use it
immediately. For that we added an ad-hoc trigger, three manual CI jobs, one per environment, each
allowed to fail without blocking a pipeline since they're not part of the deploy path. The
production job only appears on tagged release pipelines, which has a consequence worth naming: a
commit classified as chore, ci, or docs cuts no version tag, so it never produces a pipeline
where that manual job exists to click. If someone needs entitlement fixed in production before the
next real release, the CI trigger simply isn't reachable yet, and the runbook gives a direct aws lambda invoke command as the fallback for exactly that gap instead of leaving it as a dead end.
IAM scoped to match each gate, not one blanket policy
The Lambda's own IAM reflects the same three-gate thinking. Read-only discovery calls like
ListFoundationModels are scoped broadly, because listing models carries no risk. But
CreateFoundationModelAgreement is scoped tightly to this account's own foundation models, and
invocation actions are scoped further still, to both the account's foundation models and the
inference-profile/us.* namespace that cross-region inference profiles live under. Each policy
statement maps to one gate. That mapping is what makes the runbook's diagnosis step meaningful
instead of decorative: if the nightly job reports a model as already authorized and a caller still
gets denied, the runbook says plainly that the caller's IAM policy is the problem, and that
per-service IAM is outside what this particular runbook fixes. That's a useful boundary to draw on
paper. It tells the next engineer exactly which of the three systems to go check first, instead of
re-running the same failed API call and hoping the error message says something new.
More in Infrastructure
All in Infrastructure →- The credential nobody watchedA shared service-account token expired silently and took a production integration down for 40 hours. The fix that mattered wasn't rotation. It was making four causes of the same error distinguishable.Infrastructure
- Your infrastructure has a new client, and it pushes changesThe architecture diagram for our shared AWS platform names two human actors and lists an AI coding agent's own cloud egress as a third. That's a trust-model decision, not a diagram detail.Infrastructure
- One gateway, eight backends, and no MCP session in sightWhy an internal MCP aggregation gateway hand-rolls raw HTTP instead of the standard SDK client, and how it disambiguates tool names across eight backends.Infrastructure