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, so there is no console page to visit first. Nova Lite is an Amazon model and carries no subscription at all. Third-party models are sold through AWS Marketplace, and the first call to one in an account starts a subscription in the background; that needs aws-marketplace:Subscribe on the calling identity and can take a few minutes to settle. 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. 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 from the logs. After, it prints a JSON body with an answer field containing a real sentence from the model.
Two 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 need no subscription; read the ARN the message names and check it against the execution role. On a third-party model the same error can also mean the Marketplace subscription has not finished, in which case the call succeeds on a retry a few minutes later. A ValidationException about on-demand throughput means the model is only served through 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.; redeploy with the profile id for the geography you are calling from (MODEL_ID=us.amazon.nova-lite-v1:0 ./scripts/deploy.sh in a US region).
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. Third-party models add a Marketplace subscription that Bedrock starts on first use, and Anthropic adds a use-case form. All three produce the sameAccessDeniedExceptionand have different fixes. - 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
- On-demand Bedrock inference is an SDK call with IAM permission; there is no endpoint to stand up.
- Model access is on by default in commercial regions, so
bedrock:InvokeModelper model ARN is the gate that matters; third-party models add a Marketplace subscription on first use. - The Converse API gives one message shape across model providers, so changing models is a config change.
- The reply text lives at
output.message.content[0].text; everything else in the response is metadata you will care about later (token counts, stop reason). inferenceConfigis where the sampling controls live:maxTokenscaps the answer,temperaturesets how much the answer varies.- Some models are only served through a cross-region inference profile; the
ValidationExceptionabout on-demand throughput is the tell. - Tear the stack down when you finish a session; the function is free at rest but the habit is what keeps labs from becoming bills.