Exam Room · Advanced GenAI

Lab: Invoke a Foundation Model From Lambda

July 27, 2026 · 10 min read

Generative AI Development · part of The Exam Room

This is the first of the hands-on labs that run alongside these posts. The idea is simple: you get a working base and build the part that matters. 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.

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 the point of the lab. 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_ID environment 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.

This shape is the whole track’s shape. Every lab is 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.

Lab 01 solution architecture A CloudFormation stack contains a Lambda function and an IAM execution role scoped to bedrock:InvokeModel. A prompt goes into the Lambda, which calls Amazon Nova Lite through the bedrock-runtime Converse API. The model sits outside the stack in Amazon Bedrock, serverless and billed per token. CloudFormation stack: genai-lab-01 Amazon Bedrock serverless, billed per token A prompt, HTTP-shaped JSON in and back out Lambda function handler.py Execution role bedrock:InvokeModel, foundation models only runs as Converse (bedrock-runtime) Nova Lite or any model id you pass

Your task

Replace the NotImplementedError in handler() with a Converse call:

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})

That is the whole change: a client, one call, and pulling the assistant’s text out of the reply.

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, model access for 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.

The check most worth internalising is model access, because it is the single most common reason a lab fails: in the Bedrock console, open Model access and enable the model you plan to use, in the region you are working in. 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 nearly always the console gate: model access is not enabled for that model in that region. IAM refusals arrive as the same error, so read the ARN the message names and check it against the role before you decide which gate is shut. A ValidationException complaining 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

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.
  • Two separate gates must both be open. Model access is enabled per model, per region, in the console; bedrock:InvokeModel is granted per model ARN in IAM. Passing one and not the other produces errors that look similar and have different fixes.
  • The Converse API is one request shape across providers. The handler never inspects which model it is talking to. Swap MODEL_ID for 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:InvokeModel on 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 leave us-east-1.

What’s worth remembering

  1. On-demand Bedrock inference is an SDK call with IAM permission; there is no endpoint to stand up.
  2. Model access (console, per region) and bedrock:InvokeModel (IAM, per model ARN) are two separate gates, and you need both.
  3. The Converse API gives one message shape across model providers, so changing models is a config change.
  4. 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).
  5. inferenceConfig is where the sampling controls live: maxTokens caps the answer, temperature trades determinism for variety.
  6. Some models are only served through a cross-region inference profile; the ValidationException about on-demand throughput is the tell.
  7. 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.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.