This is the first of the hands-on labs that run alongside these posts. You get a working base and write the missing part. Here the scaffolding is at its highest, everything is built except one function body, and each lab after this hands you less.
The full lab, CloudFormation and scripts, is in lab-01-invoke-a-model.zip. Download it, unpack, and follow the README; this post is the walk-through and the why.
Before your first lab, do the one-time, once-per-account setup: run the zip’s preflight.sh to confirm your account is ready, then deploy the lab reaper, a standing backstop that auto-deletes any lab you forget to tear down after 24 hours.
The scenario
A team wants the simplest thing a GenAI feature can be: a function that takes a prompt, asks a Bedrock model, and returns the answer as JSON. No retrieval, no memory, no safety layer yet. Just prove that code you own can call a model you don’t have to host.
That last part is what the lab demonstrates. There is no endpoint to stand up, no instance to size, no container to build. On-demand inference on Bedrock is an ordinary AWS SDK call from anything with the right IAM permission, and a Lambda function is the smallest thing that can make one.
What you’re given
The CloudFormation template builds two resources:
- a Lambda function (Python 3.12) that receives the model id in a
MODEL_IDenvironment variable; - an execution role allowing
bedrock:InvokeModel(and the streaming variant) on foundation models, plus inference profiles in the account.
src/handler.py already parses the prompt out of an HTTP-shaped JSON body and has an _ok() helper that wraps the reply. The one thing missing is the middle: the model call.
Every lab in the track is built this way: one CloudFormation stack holding a Lambda function and an IAM role scoped to that lab, driven by the same three scripts. Later labs add resources inside the stack (a Guardrail, a DynamoDB table, a Knowledge Base) without changing the outline. Bedrock itself never appears in the stack, because on-demand inference is serverless and billed per token, and that is also why deleting the stack removes everything with a meter on it.
Your task
Replace the NotImplementedError in handler(). The whole change is a client, one call, and pulling the assistant’s text out of the reply: create a bedrock-runtime client with boto3, make a single converse() call passing MODEL_ID and the prompt as one user message, and set an inferenceConfig that caps the tokens and keeps the temperature low. The assistant’s text sits at output.message.content[0].text in the reply; hand it back through _ok() as the answer field. The docstring in src/handler.py spells out the exact request and response shapes if you get stuck.
Deploy and prove it
The zip ships a preflight.sh; run it once before your first lab. It checks the CLI, your credentials, the region, a live call to each of the three models the track uses, quota visibility, and whether an organisation-level policy is going to block you, so none of it surfaces halfway through a deploy. Its model probes send three one-token requests, which together cost a fraction of a US cent.
Model access is worth understanding before the probe reports on it. In commercial regions, access to every Bedrock foundation model is on by default given the right AWS Marketplace permissions, so there is no console page to visit first. Nova Lite is an Amazon model, and Amazon’s models are not sold through AWS Marketplace, so no subscription sits behind them; the same goes for DeepSeek, Meta, Mistral AI and Qwen, none of which carry a Marketplace product id either. For the providers that are sold that way, the first call to one of their models in an account starts the subscription in the background, which needs aws-marketplace:Subscribe, aws-marketplace:Unsubscribe and aws-marketplace:ViewSubscriptions on the calling identity and can take up to fifteen minutes to finish. Anthropic models add a one-time use-case form per account or organisation. GovCloud is the exception, where the console’s Model access page still gates each model, and a third-party model has to be enabled in the linked commercial account as well. Then:
cd lab-01-invoke-a-model
./scripts/deploy.sh
./scripts/test.sh
./scripts/test.sh "Explain retrieval-augmented generation in two sentences."
The defaults are stack genai-lab-01, region us-east-1, and amazon.nova-lite-v1:0; override any of them with environment variables. Before you fill the gap, the test prints a NotImplementedError in the function’s error payload. After, it prints a JSON body with an answer field containing a real sentence from the model.
A few failures are worth recognising on sight. An AccessDeniedException naming the model is an IAM problem when the model is Nova, since Amazon’s own models carry no subscription; read the ARN the message names and check it against the execution role. A Marketplace-sold model has its own code for a subscription still in flight, MPAgreementBeingCreated, and AWS’s advice there is to try again after fifteen minutes; a missing Anthropic use-case form returns FTUFormNotFilled and a 404 rather than an access denial. A ValidationException is a different problem again: the model is not served in-region where you are calling from, so the call has to name a cross-region Inference profileA Bedrock resource wrapping a model so calls to it can be tagged, routed across regions, or repointed without changing app code. instead. Each model’s detail page lists its regional availability alongside the geo inference ids it publishes, so redeploy with the one for your geography (MODEL_ID=us.amazon.nova-lite-v1:0 ./scripts/deploy.sh in a US region; Nova Lite publishes us. and eu. and nothing else).
When you are done:
./scripts/teardown.sh
When you want the reference answer, deploy it without editing anything (SRC=solution ./scripts/deploy.sh), or unfold it here:
Show the answer
import boto3
_bedrock = boto3.client("bedrock-runtime")
response = _bedrock.converse(
modelId=MODEL_ID,
messages=[{"role": "user", "content": [{"text": prompt}]}],
inferenceConfig={"maxTokens": 512, "temperature": 0.2},
)
answer = response["output"]["message"]["content"][0]["text"]
return _ok({"answer": answer})
What the call is actually doing
The lab is five lines of Python, but the shape of those lines carries most of what matters about Bedrock:
- Invocation is an SDK call, not infrastructure. For on-demand inference there is nothing to provision and nothing to keep warm; AWS runs the model and you pay per token. The service boundary is IAM, the same as S3 or DynamoDB.
- IAM is the gate that is left.
bedrock:InvokeModelis granted per model ARN, and for an Amazon model like Nova Lite that is the only check between the function and an answer. Models sold through AWS Marketplace add a subscription that Bedrock starts on first use, and Anthropic adds a use-case form. Each of the three fails with its own code,AccessDeniedException,MPAgreementBeingCreatedandFTUFormNotFilled, and each has a different fix. - The Converse API is one request shape across providers. The handler never inspects which model it is calling. Swap
MODEL_IDfor another model and the code is unchanged, which is what makes model choice a configuration decision rather than a rewrite. - The permission scope is worth reading once. The role allows
bedrock:InvokeModelon foundation models in any region, plus the inference profiles in your account, because some models are only reachable through a profile and a cross-region profile is authorised against the foundation model in every region it can route your call to. That distinction shows up again the moment you leaveus-east-1.
What’s worth remembering
- IAM is the gate. On-demand inference is an SDK call with no endpoint to stand up;
bedrock:InvokeModelis granted per model ARN. - Model access is on by default. In commercial regions; Marketplace-sold models start a subscription on first use, Amazon’s own do not.
- Converse is one shape across providers. Changing model means changing
MODEL_ID, not rewriting the handler. - The reply has one path. The answer is at
output.message.content[0].text; the rest is metadata such as token counts and stop reason. inferenceConfigholds sampling controls.maxTokenscaps the answer length;temperaturesets how much the answer varies.- Throughput errors point to a profile. A
ValidationExceptionabout on-demand throughput means the model is served only through a cross-region inference profile.