Courses / Hands-on Cloud for Freshers
Session 7 of 8
Code without servers: Lambda
Overview
Unit 7 — Code Without Servers: Lambda
Course: Hands-on Cloud for Freshers
Prerequisites: You can sign in to your AWS account as your everyday administrator identity (not
the root user) and you know where your budget alert lives. You can run the AWS CLI with short-lived
credentials. You have a DynamoDB table called hoc-notes with a string partition key noteId, and
an IAM role called hoc-notes-api-role that Lambda can assume, carrying basic logging permissions
and a policy that allows reading and writing only that table. If you do not have the table or the
role, the project for this unit starts with a short appendix that creates both. If you are starting
here without the earlier units, the notes’ If you are starting here steps set up the sign-in, the
budget alert and the AWS CLI (version 2.32.0 or later) first. You can read a short Python function
and a JSON document.
What the reader can do after this unit:
- Deploy a Python Lambda function from the console that lists and adds notes in a DynamoDB table, using an execution role instead of any stored password or key.
- Give the function a public HTTPS address with a function URL, and explain what changes when the
auth type is
NONErather thanAWS_IAM, and why a browser on another origin also needs CORS. - Find the record of any single request in CloudWatch Logs, read its START, END and REPORT lines, and use them to tell a code bug from a permissions problem.
Core question this unit answers: When you put a piece of code on the internet without running a server for it, who is allowed to call it, what is it allowed to touch, and how do you find out what happened on a request that went wrong?
Connections:
- Builds on: the identity and permission ideas from earlier units — a role is an identity that a
service takes on, and a policy decides what that identity may do — and the
hoc-notestable built in the DynamoDB unit. - Leads into: Unit 8, which puts a static web page in front of this function, adds an alarm that emails you when it fails, and then removes every resource the course created.
Your answers are scored
This unit is assessed. The quiz is marked against a key held separately from your materials. The first seven questions are multiple choice and are marked by comparing your letter. The last three ask you to write something — a command, a configuration document, a response object — and each is marked by a script that checks named properties of what you submit against a rubric kept in that key.
Two consequences worth knowing before you start:
- You can test your constructed answers before submitting them. Each question states exactly what your answer must do, and you may run a command or parse a JSON document as often as you like. The rubric checks only those stated requirements; there is no opinion applied afterwards.
- Style is not marked. Extra spaces, a different order of options on a command line, or a different but equivalent layout of a JSON document score exactly what the tidiest version does.
Notes
Hands-on Cloud for Freshers — Unit 7: Code Without Servers: Lambda
Before you start
Have these ready:
- Your AWS account, signed in as your everyday administrator identity, with the Region selector in
the console set to the Region you have used so far. The examples use Asia Pacific (Mumbai),
ap-south-1. Function URLs are not offered in every Region; the Lambda documentation says to check the AWS Regional Services list for the feature Function URLs before relying on it. - The AWS CLI working from a terminal with short-lived credentials, and
curl. Check thatcurlis there by runningcurl --version; if that prints no version, installcurlbefore you go on. - The DynamoDB table
hoc-notes(partition keynoteId, type String) and the IAM rolehoc-notes-api-rolefrom the earlier units. If either is missing, the project’s appendix creates them; do that first.
If you are starting here, without the earlier units, set up these four things first. Each step comes from the AWS page named beside it in Sources.
- An everyday sign-in that is not the root user. The IAM guide says not to use your root user credentials for daily tasks. Signed in as root, open IAM → Users → Create user, give the user console access, and give it administrator permissions with a permissions policy; then sign in as that user (Setting up your AWS account; Create an IAM user in your AWS account).
- A budget alert. Billing and Cost Management → Budgets → Create budget → Use a template (simplified) → Zero spend budget, which notifies you once spending exceeds the Free Tier limits (Using a budget template).
- The AWS CLI, signed in with short-lived credentials. Install AWS CLI version 2 and check
aws --version: it must report 2.32.0 or later, the minimum the CLI guide gives foraws login. That version also covers the newest option this unit uses,add-permission --invoked-via-function-url, which the AWS CLI changelog shows arriving in 2.31.13. Runaws login, enterap-south-1when it asks for a Region, and finish the sign-in in the browser. If the sign-in is refused, the guide says the IAM identity needs the AWS managed policySignInLocalDevelopmentAccess. Check withaws sts get-caller-identity: theAccountfield is your 12-digit account ID, which the project’s appendix asks for (Login for AWS local development using console credentials). - The table and the role. Build both from the project’s appendix. Your table then starts empty, with none of the notes the earlier units wrote, which changes one expected output below.
Cost. Under the current Free Tier, Lambda has an Always Free monthly allowance — the Lambda pricing page puts it at one million requests and 400,000 GB-seconds of compute per month — and the billing guide says a Free account plan has only Always Free offerings active. An allowance is not a guarantee: the billing guide also says that usage beyond a monthly allowance is covered by your Free Tier credits, and on a Paid plan is charged once the credits are gone. This unit’s few dozen requests stay far inside the allowance, and your budget alert from Unit 1 is the backstop; leave it on.
Every claim here about how Lambda, CloudWatch Logs or the CLI behaves is taken from the AWS pages listed under Sources at the end of these notes, fetched on 2026-10-02.
Summary
Every earlier unit asked you to rent something and keep it running: a bucket that is always there, a
server that is on until you stop it. Lambda inverts that. You hand AWS a function, and AWS runs it
only when a request arrives, in an environment it creates, scales and throws away for you; you never
see or patch a machine. That convenience moves the hard questions somewhere else. Instead of “is the
server up?”, you ask who may call this code, which the function URL’s auth type and the function’s
resource-based policy decide; what may this code touch, which the execution role decides; and what
happened on that one request, which only the logs can tell you. This unit builds a small notes API —
a Python function that lists and adds notes in the hoc-notes table — gives it a public HTTPS
address, and teaches you to read its logs well enough to tell a bug in your code from a permission
you forgot to grant.
Key concepts
| Term | Definition |
|---|---|
| Lambda function | Code plus configuration (runtime, handler, memory, timeout, role) that AWS runs on demand when it is invoked |
| Lambda runtime | The language environment Lambda provides, named by an identifier such as python3.14. Each identifier has a published deprecation date |
| Handler | The function in your code that Lambda calls for each invocation. For a Python file lambda_function.py with def lambda_handler(event, context), the handler is lambda_function.lambda_handler |
| Event | The JSON document Lambda passes to your handler describing what triggered it. For a function URL it describes an HTTP request: method, path, headers, body |
| Execution role | The IAM role Lambda assumes when it runs your function. Its policies decide what AWS resources the code may call. Its trust policy must name lambda.amazonaws.com |
| Function URL | A dedicated HTTPS endpoint for one function, of the form https://<url-id>.lambda-url.<region>.on.aws. It never changes once created |
| Auth type | A function URL setting. AWS_IAM requires each request to be signed by an AWS identity that has permission; NONE performs no authentication, so anyone with the URL can call it once the function’s resource-based policy allows public access |
| Resource-based policy | A policy attached to the function itself, saying which callers may invoke it. Distinct from the execution role, which governs what the function may call |
| CORS | Cross-origin resource sharing: response headers that tell a browser whether a page loaded from one origin may read responses from another |
| Log group / log stream | CloudWatch Logs containers. Lambda writes to the log group /aws/lambda/<function-name> by default; each execution environment writes its own log stream inside it |
| REPORT line | The line Lambda writes at the end of every invocation with Duration, Billed Duration, Memory Size, Max Memory Used and, on a cold start, Init Duration |
| Timeout | The longest Lambda lets one invocation run. Default 3 seconds, adjustable up to 900 seconds |
Explanations
The failure, before the explanation
Here is what a first attempt usually looks like. You write a handler that saves a note to DynamoDB, create the function in the console, accept every default, and send it a request:
$ curl -s -o /dev/null -w '%{http_code}\n' -X POST "$URL" -H 'content-type: application/json' -d '{"text":"first note"}'
502
(-o /dev/null throws the body away and -w '%{http_code}\n' prints only the status code.) A
status in the 5XX range is what a function URL returns when the function itself fails — its
monitoring page counts “function errors and timeouts” as 5XX — and 502 is the code callers report
seeing. Nothing in that response tells you what went wrong, and nothing on your side changed between “it
worked in my head” and this. The answer is in the function’s log, one request’s worth of lines:
START RequestId: 7c1e... Version: $LATEST
[ERROR] ClientError: An error occurred (AccessDeniedException) when calling the PutItem operation:
User: arn:aws:sts::111122223333:assumed-role/hoc-notes-api-role-x1y2/hoc-notes-api is not
authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:ap-south-1:111122223333:table/hoc-notes
END RequestId: 7c1e...
REPORT RequestId: 7c1e... Duration: 412.30 ms Billed Duration: 413 ms Memory Size: 128 MB Max Memory Used: 79 MB Init Duration: 301.55 ms
The code is fine. The console’s default created a fresh role with logging permission only, so the function was allowed to write its own logs and nothing else. The rest of this unit is about the three questions that failure raises — what the function may touch, who may call it, and how you find out — in that order.
What a Lambda function actually is
Think of a food stall that only lights its stove when an order arrives. You write the recipe (the code) and pin it to the wall with a few instructions — which cuisine (the runtime), which recipe card to start from (the handler), how long a dish may take before it is abandoned (the timeout). The stall owner (Lambda) does the rest: lights the stove when an order comes in, opens a second stove if two arrive together, and lets them go cold when it is quiet. You pay per order cooked, not for the stall standing there.
A second way to see it: a Lambda function is a single Python function with a contract. Lambda calls
lambda_handler(event, context) once per request. event is a dictionary describing the request;
context describes the invocation (its request ID, its log group name). Whatever you return becomes
the response. Everything outside the handler — imports, creating an SDK client — runs once when an
execution environment starts and is reused by later invocations in that environment. That is why the
Lambda documentation tells you to create SDK clients outside the handler: you pay their start-up
cost once per environment, not once per request.
Two settings to know from day one:
| Setting | Default | Why it matters here |
|---|---|---|
| Timeout | 3 seconds (maximum 900) | A first call that has to start an environment and open a connection to DynamoDB can be slow; if it routinely nears the limit, raise it rather than letting requests be cut off |
| Memory | 128 MB | Plenty for this function. The REPORT line’s Max Memory Used tells you how close you are |
Choosing a runtime. The Lambda runtimes page lists every supported runtime with its deprecation
date. On 2026-10-02 the newest generally available Python runtime is python3.14 (Amazon Linux
2023, deprecation forecast June 2029). python3.15 is listed but marked public preview, which the
page says is not covered by the Lambda SLA and should not be used for production; python3.10 and
python3.11 run on Amazon Linux 2, which the page flags for end of life. Pick python3.14.
What the function may touch: the execution role
Picture the stall cook again, this time wearing a staff badge. The badge opens the store room (the table) and the noticeboard (the logs), and nothing else in the building. The cook did not choose the badge; whoever hired them did, and the badge is checked at every door. That badge is the execution role.
When Lambda runs your code, the code acts as the execution role. The role is an identity with two halves:
- a trust policy saying who may take it on — for an execution role, the Lambda service principal
lambda.amazonaws.com; - permission policies saying what it may do once taken on.
The console’s default, Create a new role with basic Lambda permissions, attaches the AWS managed
policy AWSLambdaBasicExecutionRole, which grants exactly three actions: logs:CreateLogGroup,
logs:CreateLogStream and logs:PutLogEvents. That is why the function in the failure above could
log its own error but not save the note. The fix is not to give the function more power in general;
it is to pick, under Permissions, Use another role and choose hoc-notes-api-role, which you
built in Unit 6 for read and write on the one table; the project adds basic logging to it.
A second way to see it is the failure itself, read as a sentence: “assumed-role/hoc-notes-api-role … is not authorized to perform: dynamodb:PutItem”. The subject is the role, the verb is the action, the object is the table. Every permission problem inside a function reads that way, which is why the fix is always “add exactly that action on exactly that resource to the role”. A badge that opens everything would also “fix” the failure — and would turn a bug in your code into a bug that can delete other people’s data.
Notice what is not in this picture: no access key, no password, no secret in your code. The SDK for Python (Boto3) finds temporary credentials for the role in the function’s environment on its own. The Lambda handler page says this directly: you don’t need to provide credentials to initialise a client.
The notes function
Here is the whole function. Read it once top to bottom before the explanation.
import base64
import json
import os
import uuid
from datetime import datetime, timezone
import boto3
TABLE_NAME = os.environ.get("TABLE_NAME", "hoc-notes")
table = boto3.resource("dynamodb").Table(TABLE_NAME) # created once per environment
MAX_TEXT = 500
def respond(status, payload):
return {
"statusCode": status,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(payload, default=str),
}
def read_body(event):
body = event.get("body") or ""
if event.get("isBase64Encoded"):
body = base64.b64decode(body).decode("utf-8")
return body
def lambda_handler(event, context):
method = event.get("requestContext", {}).get("http", {}).get("method", "")
print(f"{method} {event.get('rawPath', '/')}")
if method == "GET":
items = table.scan().get("Items", [])
items.sort(key=lambda note: note.get("createdAt", ""), reverse=True)
return respond(200, {"notes": items})
if method == "POST":
try:
data = json.loads(read_body(event))
except json.JSONDecodeError:
return respond(400, {"error": "body must be JSON"})
text = str(data.get("text", "")).strip() if isinstance(data, dict) else ""
if not text:
return respond(400, {"error": "text is required"})
if len(text) > MAX_TEXT:
return respond(400, {"error": f"text must be at most {MAX_TEXT} characters"})
note = {
"noteId": str(uuid.uuid4()),
"text": text,
"createdAt": datetime.now(timezone.utc).isoformat(),
}
table.put_item(Item=note)
return respond(201, note)
if method == "OPTIONS":
return {"statusCode": 204}
return respond(405, {"error": f"method {method} not allowed"})
What each part is doing, and why:
- The method comes from
event["requestContext"]["http"]["method"]. A function URL turns the HTTP request into an event in the payload format version 2.0 shape, the same shape Amazon API Gateway uses. The method, path (rawPath), headers andbodyare all fields in it. - The body is a string, and may be base64-encoded. The event’s
bodyis text; if the request was binary, Lambda setsisBase64Encodedto true and encodes it. Decoding when that flag is set costs one line and removes a class of surprise. - Every value written is a string.
noteIdis a random UUID,createdAtan ISO timestamp, andtextthe note. The notes you added in Unit 6 also carrypriority, a number, and Boto3 returns DynamoDB numbers as PythonDecimalvalues, whichjson.dumpsrefuses on its own. That is whatdefault=strinrespond()is for: anythingjson.dumpscannot serialise is written as text, so apriorityof 2 reaches the caller as"2"instead of crashing the function. - The response is a dictionary with
statusCode,headersandbody. The function URL documentation says Lambda converts that into the HTTP response. If you return plain JSON without astatusCode, Lambda assumes status 200, content typeapplication/json, and uses what you returned as the body. Returning the full shape is what lets you send 201 and 400. - Bad input gets a 400, not an exception. Empty text, missing JSON, text over 500 characters: each returns a clear error to the caller. An exception would reach the caller only as an opaque failure — the failure at the top of these notes.
OPTIONSreturns 204. Browsers send anOPTIONSrequest before some cross-origin calls (more on this below). Answering it harmlessly costs nothing.table.scan()reads the whole table. A scan examines every item, up to 1 MB of data per call, and returns aLastEvaluatedKeywhen there is more to read. For a table of class notes this is fine. For a large table it is the wrong tool, and you would design a key you can query by instead.
The table name comes from the environment variable TABLE_NAME, set under Configuration →
Environment variables. The code falls back to hoc-notes if it is unset. Unit 8 uses this variable
to break the function on purpose and watch an alarm fire, so set it explicitly now.
Deploying from the console. Create the function with Create function → Author from scratch,
name hoc-notes-api, runtime Python 3.14, and under Permissions choose Use another role
→ hoc-notes-api-role. In the Code tab, replace the contents of lambda_function.py with the
code above and choose Deploy. Nothing runs until you deploy; an edited but undeployed file is the
commonest reason a “fixed” function still fails.
A note on the SDK: the Python runtimes include a version of Boto3, so this code runs without packaging anything. The Lambda documentation recommends bundling your own copy for production, so that an automatic runtime update cannot change the SDK under you, and says the included version is appropriate when you create a function with the console code editor — which is what you are doing.
Who may call it: function URLs and auth types
Think of the stall’s serving hatch. Without one, only staff with a key to the kitchen can place an order. Cut a hatch into the wall, and anyone walking past can order — unless you put a ticket reader on it. A function URL is that hatch; its auth type is whether the ticket reader is fitted.
A function with no trigger can only be invoked by someone with AWS credentials and permission. A function URL gives it an ordinary HTTPS address. In the console: Configuration → Function URL → Create function URL, choose an Auth type, optionally Configure cross-origin resource sharing (CORS), then Save. The address appears in the Function overview.
The auth type is the decision that matters:
| Auth type | Who can call it | What the caller must do |
|---|---|---|
AWS_IAM |
Only AWS identities whose policies allow lambda:InvokeFunctionUrl and lambda:InvokeFunction |
Sign every request with AWS Signature Version 4 |
NONE |
Anyone on the internet who has the URL, once the function’s resource-based policy grants public access | Nothing — a browser or curl works as is |
An analogy: AWS_IAM is a door with a badge reader; NONE is a shop front with the door propped
open. A shop front is right for a shop. It is wrong for the store room. The notes page in Unit 8 has
no sign-in, so its visitors’ browsers hold no AWS credentials to sign a request with, and this API
uses NONE. (Larger applications do sign browser requests — by handing signed-in users temporary AWS
credentials, or by putting their own sign-in in front of the API — but that is beyond this course.)
The moment you choose NONE, your function has to defend itself, because nobody else will.
Three facts about NONE that the documentation is explicit about:
- The resource-based policy still decides.
NONEturns off authentication, not authorisation. The function’s resource-based policy must allowlambda:InvokeFunctionUrlandlambda:InvokeFunction(the second required for new function URLs since October 2025) for the principal*. When you create the URL in the console, Lambda adds those two statements for you. When you create it with the CLI, it does not, and every call returns 403 Forbidden until you add them withaws lambda add-permission. - Anyone with the URL can make you pay for invocations. A public URL with no checks is an invitation. Your free allowance is large, but it is not a defence. The documentation’s emergency switch is to set the function’s reserved concurrency to 0, which throttles every request with HTTP 429 until you remove it.
- Deleting the URL leaves the policy behind. If you delete a
NONEfunction URL, Lambda does not delete the resource-based policy statements. Remove them yourself, or delete the function.
The CLI version, for reference — note the option is --cors in the AWS CLI command reference:
aws lambda create-function-url-config \
--function-name hoc-notes-api \
--auth-type NONE
aws lambda add-permission --function-name hoc-notes-api \
--statement-id UrlPolicyInvokeURL \
--action lambda:InvokeFunctionUrl \
--principal '*' \
--function-url-auth-type NONE
aws lambda add-permission --function-name hoc-notes-api \
--statement-id UrlPolicyInvokeFunction \
--action lambda:InvokeFunction \
--principal '*' \
--invoked-via-function-url
The --invoked-via-function-url option adds the condition lambda:InvokedViaFunctionUrl, so the
public permission applies only to calls through the URL and not to any other way of invoking the
function. The documentation recommends it. It is a recent option: the AWS CLI changelog shows it arriving in
version 2.31.13, so an older CLI rejects it as unknown. Check aws --version first; the 2.32.0 this
course asks for is new enough.
Testing with curl
Store the URL in a shell variable and call both methods:
URL='https://abc123example.lambda-url.ap-south-1.on.aws/'
curl -s "$URL"
curl -s -X POST "$URL" -H 'content-type: application/json' -d '{"text":"first note"}'
curl -s "$URL"
curl -s -i -X POST "$URL" -H 'content-type: application/json' -d '{"text":""}'
The first returns {"notes": [...]} holding whatever the table already contains. If you kept the
table from Unit 6, that is the notes you wrote there — items with a title and a priority (shown
as text, such as "2", because of default=str) and no text or createdAt. If you built a fresh
table from the project’s appendix, it is {"notes": []}. The second returns the new note with its
noteId, text and createdAt. The third lists it first: the function sorts by createdAt,
newest first, and the Unit 6 notes have no createdAt, so they sort to the end. The fourth, with
-i to show the status line, returns 400 and {"error": "text is required"}. That last request is
the important one: it proves your validation runs on the real endpoint, not only in your head.
CORS: why the browser refuses what curl accepts
Here is the second failure worth seeing before it is explained. In Unit 8 a page served from
http://hoc-site-ab-4821.s3-website.ap-south-1.amazonaws.com calls your function URL with
fetch(). curl worked a moment ago. The browser’s console says:
Access to fetch at 'https://abc123example.lambda-url.ap-south-1.on.aws/' from origin
'http://hoc-site-ab-4821.s3-website.ap-south-1.amazonaws.com' has been blocked by CORS policy
curl does not enforce CORS; browsers do. An origin is scheme plus host plus port. Your page
and your function URL are different origins, and a browser will not let a script on one read a
response from another unless the response carries an Access-Control-Allow-Origin header naming the
first. For a POST with a JSON content type, the browser first sends an OPTIONS preflight
asking whether the method and the content-type header are allowed.
Two ways to picture it: CORS is the receiving building’s guest list, checked by the visitor’s own security guard (the browser) rather than by the building. Or: the server does not block anything — it states which origins may read its answers, and the browser enforces that statement.
Configure CORS on the function URL rather than in your code:
| Setting | Value for this course |
|---|---|
| Allow origin | Your bucket’s website endpoint, exactly — http:// scheme, no trailing slash |
| Allow methods | GET, POST |
| Allow headers | content-type |
The documentation recommends configuring all CORS settings on the function URL and not also sending
CORS headers from the function: for non-preflight requests Lambda returns both, which can produce a
duplicate Access-Control-Allow-Origin that the browser rejects. That is why the function above
sends no CORS headers of its own.
Allowing * as the origin also “works”. It means any web page anywhere can call your API from a
visitor’s browser. Name the one origin you mean.
How to find out what happened: CloudWatch Logs
Go back to the failure at the top of these notes. The caller saw only a bare 502; the
log showed the exact action, role and table that collided. That one record is the whole reason logs
matter here: a function has no screen you can look at, so its log is the only witness to what a
single request did. Think of it as the stall’s order book — one line per order, written whether the
dish came out right or not.
Lambda sends every invocation’s output to CloudWatch Logs, provided the execution role allows it —
the three logs: actions above. By default the log group is /aws/lambda/hoc-notes-api. Inside it
each execution environment writes its own log stream. Every invocation produces at least three
lines whatever your code does:
| Line | What it tells you |
|---|---|
START RequestId: … |
One invocation began. Everything until its END belongs to it |
END RequestId: … |
It finished |
REPORT RequestId: … Duration … Billed Duration … Memory Size … Max Memory Used … Init Duration … |
How long the handler ran, what you were billed for, how much memory it used, and — only when a new environment started — how long start-up took |
Anything your code prints (print(...)) or logs appears between START and END. The log line the
function writes first, GET / or POST /, is there so you can tell requests apart at a glance.
Three ways to read the logs:
- Lambda console: the function’s Monitor tab → View CloudWatch logs, then choose a log stream.
- CloudWatch console: Log groups →
/aws/lambda/hoc-notes-api→ a log stream. - CLI:
aws logs tail /aws/lambda/hoc-notes-api --since 10m --followshows recent events and keeps polling until you press Control-C.
Logs are not instant: the documentation’s own example script pauses before it reads them, to allow time for the logs to become available. A log stream that looks empty immediately after a request is not yet proof the request never ran — refresh before you conclude anything.
Reading the failure. The failure at the top of these notes is diagnosable from one log record:
| What you see | What it usually means |
|---|---|
AccessDeniedException … is not authorized to perform: dynamodb:PutItem |
The execution role lacks a permission. Fix the role, not the code |
ResourceNotFoundException naming the table |
The table name or Region is wrong — check the TABLE_NAME variable and the Region |
| A Python traceback in your own file | A bug in your code |
Task timed out after 3.00 seconds |
The timeout is too short for what the code does, or the code is stuck |
| No START line at all for your request, and the caller got 403 | The request never reached the function: the resource-based policy refused it |
The last row is the one that saves the most time. A 403 from a function URL and an
AccessDeniedException in the log are both “permission” problems, on opposite sides of the function.
Three situations, then the rule
Before the summary table, three situations you will meet:
- You add an “edit note” feature that calls
UpdateItemand test it withcurl. It returns 502. The log showsnot authorized to perform: dynamodb:UpdateItem. The role was never given that action. - A friend opens your web page and nothing loads.
curlworks for you. Their browser’s console shows a CORS error naming a different origin — they opened the page through a different address. - You create the URL from the CLI in a script, run
curl, and get{"Message":"Forbidden"}. The log group has no new stream at all.
| Symptom | Where the decision was made | What to change |
|---|---|---|
| 403 from the URL, nothing in the logs | The function’s resource-based policy (who may call it) | Add the two invoke permissions, or create the URL in the console |
500 to the caller, AccessDenied in the log |
The execution role (what it may touch) | Add exactly the missing action on exactly that resource |
Works in curl, blocked in the browser |
CORS on the function URL | Allow the page’s exact origin, methods and headers |
Flashcards
Say the answer aloud before revealing it. Speaking it is what exposes the gaps that reading past a written answer hides.
What are the two halves of an execution role?
A trust policy saying who may assume it (for Lambda, the service principal lambda.amazonaws.com), and permission policies saying what it may do once assumed.
What does AWSLambdaBasicExecutionRole allow?
Writing logs: logs:CreateLogGroup, logs:CreateLogStream and logs:PutLogEvents. Nothing else, which is why a default role cannot touch your table.
Why create the DynamoDB client outside the handler?
Code outside the handler runs once when an execution environment starts and is reused by later invocations, so you pay the set-up once per environment rather than once per request.
Your handler returns {"notes": []} with no statusCode. What does the caller receive?
Status 200 with content type application/json and that JSON as the body; Lambda infers the response format when no status code is returned.
What does auth type NONE turn off, and what does it not?
It turns off authentication: no signature is checked. It does not turn off the function’s resource-based policy, which must still allow public invocation or every call gets 403.
You created a NONE function URL with the CLI and every call returns 403. Why?
The CLI does not add the resource-based policy statements the console adds. You must add permissions for both lambda:InvokeFunctionUrl and lambda:InvokeFunction yourself.
Why does curl succeed where a browser on your S3 site fails?
Browsers enforce CORS and curl does not. The browser needs the response to allow the page’s origin, and sends a preflight for a JSON POST.
Why configure CORS on the function URL and not also in your code?
For non-preflight requests Lambda returns both sets of headers, which can produce a duplicated Access-Control-Allow-Origin that the browser rejects.
What three lines does every invocation write, whatever your code does?
START, END and REPORT, each carrying the request ID, so you can group every line of one request together.
What does Init Duration on a REPORT line tell you?
That this invocation had to start a new execution environment first, and how long that start-up took. It appears only on those invocations.
A request got 403 from the URL and there is no new log stream. Code bug or permission?
Permission, on the calling side: the request was refused before your code ran, so the resource-based policy is the place to look.
How do you stop all traffic to a public function URL in an emergency?
Set the function’s reserved concurrency to 0. Every request is throttled with HTTP 429 until you remove the setting or raise it.
Model answer
The question: “You’ve put a small API on Lambda behind a function URL. Walk me through how you decided who can call it and what it can do — and what you’d check first if a request failed.”
There are five beats, in this order. Missing any one of them is a failure state, not a stylistic gap: each is a separate place where access is decided, and an answer that skips one has left a door unexamined.
- Caller authentication. Name the auth type and why:
NONEbecause the page has no sign-in, so its visitors’ browsers hold no AWS credentials to sign with, orAWS_IAMbecause only your own signed-in services call it. - Caller authorisation. Say that the function’s resource-based policy must allow
lambda:InvokeFunctionUrlandlambda:InvokeFunction, and that the console adds this forNONEwhile the CLI does not. - What the code may touch. The execution role: trusted by
lambda.amazonaws.com, with logging plus only the table actions the code uses, on only that table. No keys in the code. - What a public caller can and cannot do. Under
NONE, anyone with the URL can invoke the function, and nothing in the code or the role changes that — only switching toAWS_IAM, or removing the public statements from the resource-based policy, stops unauthenticated calls. So the code limits what each call can do: input validation returning 400 and a size limit on the text. Reserved concurrency of 0 is the off switch. CORS limited to one named origin is not access control: it only stops other web pages reading the responses in a visitor’s browser, andcurlignores it. - How you diagnose a failure. Request ID, then the log stream: START/END/REPORT present or not,
then the error text. No START means the call was refused before the code;
AccessDeniedinside means the role; a traceback means the code.
Spoken, all five beats sound like this:
“I made two separate decisions about callers. For authentication, I chose auth type
NONE, because the people calling this API are visitors to a page with no sign-in, so their browsers hold no AWS credentials to sign a request with. For authorisation, the function’s resource-based policy still has to allowlambda:InvokeFunctionUrlandlambda:InvokeFunctionfor the public principal — the console added those two statements for me, and I know that from the CLI I would have to add them myself. Then there is what the code may touch: the execution role is trusted bylambda.amazonaws.comand carries logging plus only the table actions the code uses, on that one table, with no keys anywhere in the code. Because the URL is public, anyone who has it can call the function — CORS only limits which web pages can read the answers in a browser, andcurlignores it — so the code limits what each call can do: it rejects empty or oversized text with a 400. Setting reserved concurrency to zero is my off switch, and if this API ever stopped needing to be public, I would switch the auth type toAWS_IAM. And if a request fails, I start from its request ID in the logs. No START line means it was refused before my code ran, so I look at the resource-based policy; anAccessDeniedinside the request means the role; a traceback means my code.”
Beat 5 is the one most answers drop, and it is the one an interviewer probes, because it shows whether you have actually run the thing rather than described it.
Why this matters
- The internal tool that became public. Someone creates a quick function URL with auth type
NONEto test from a laptop, forgets it, and the function writes to a production table. Anyone who finds the URL can now write there. The fix is to require authentication: switch the auth type toAWS_IAM, or remove the public statements from the function’s resource-based policy so every unauthenticated call gets 403. A narrower execution role only limits the damage each call can do, and CORS only binds browsers — neither stops an unauthenticated caller. The fix is one setting; noticing usually takes an incident. - The role that was “fixed” with full access. A permission error at a deadline gets solved by attaching an administrator policy to the execution role. It works. From then on, a bug in that one function can modify anything in the account.
- The bug report that says only “500”. A user reports an error with no detail. With request IDs and START/END/REPORT lines, you can find their request, see the exception, and tell whether it was your code, your permissions or your timeout — without reproducing it.
- The front end that works on the developer’s machine. The API is tested with
curland passes; the browser blocks it on the first real page load because nobody configured CORS for the page’s origin. Every web developer meets this once.
After this unit
The project for this unit is Ship a Notes API on Lambda, Then Break It and Read the Logs
(project.md). You will deploy hoc-notes-api, give it a function URL, prove it with curl, then
break it twice on purpose — a wrong table name and a blocked caller — and diagnose each failure from
what the logs show, and what they do not. The function and the
table stay up at the end, because Unit 8 builds on them; the project ends by removing everything
Unit 8 will not need and recording what is left.
Unit 8 puts a static web page in front of the function, adds a CloudWatch alarm that emails you when it fails, and then tears down every resource the course created, proving it with resource listings and then confirming on the billing pages.
Sources
Every page below was fetched on 2026-10-02.
- Lambda runtimes — https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html (fetched 2026-10-02)
- Create your first Lambda function — https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html (fetched 2026-10-02)
- Define Lambda function handler in Python — https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html (fetched 2026-10-02)
- Defining Lambda function permissions with an execution role — https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html (fetched 2026-10-02)
- Configure Lambda function timeout — https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html (fetched 2026-10-02)
- Creating and managing Lambda function URLs — https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html (fetched 2026-10-02)
- Control access to Lambda function URLs — https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html (fetched 2026-10-02)
- Invoking Lambda function URLs — https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html (fetched 2026-10-02)
- Tutorial: Creating a webhook endpoint using a Lambda function URL — https://docs.aws.amazon.com/lambda/latest/dg/urls-webhook-tutorial.html (fetched 2026-10-02)
- Sending Lambda function logs to CloudWatch Logs — https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html (fetched 2026-10-02)
- Viewing CloudWatch logs for Lambda functions — https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-view.html (fetched 2026-10-02)
- AWS CLI
create-function-url-config— https://docs.aws.amazon.com/cli/latest/reference/lambda/create-function-url-config.html (fetched 2026-10-02) - AWS CLI
create-function— https://docs.aws.amazon.com/cli/latest/reference/lambda/create-function.html (fetched 2026-10-02) - AWS CLI
logs tail— https://docs.aws.amazon.com/cli/latest/reference/logs/tail.html (fetched 2026-10-02) - Boto3 DynamoDB
Table.scan— https://docs.aws.amazon.com/boto3/latest/reference/services/dynamodb.html#DynamoDB.Table.scan (fetched 2026-10-02) - Explore AWS services with AWS Free Tier — https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/free-tier.html (fetched 2026-10-02)
- Tracking your AWS Free Tier usage — https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/tracking-free-tier-usage.html (fetched 2026-10-02)
- AWS Lambda pricing — https://aws.amazon.com/lambda/pricing/ (fetched 2026-10-02)
- Login for AWS local development using console credentials (
aws login, minimum CLI 2.32.0) — https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sign-in.html (fetched 2026-10-02) - AWS CLI version 2 changelog (
InvokedViaFunctionUrladded in 2.31.13) — https://raw.githubusercontent.com/aws/aws-cli/v2/CHANGELOG.rst (fetched 2026-10-02) - Setting up your AWS account (do not use root for daily tasks) — https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-started-account-iam.html (fetched 2026-10-02)
- Create an IAM user in your AWS account — https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html (fetched 2026-10-02)
- Using a budget template (simplified) — https://docs.aws.amazon.com/cost-management/latest/userguide/budget-templates.html (fetched 2026-10-02)
- Monitoring Lambda function URLs (
Url5xxCount: 5XX codes indicate server-side errors such as function errors and timeouts) — https://docs.aws.amazon.com/lambda/latest/dg/urls-monitoring.html (fetched 2026-10-02) - AWS re:Post, Internal Server Error/502 Bad Gateway testing a Lambda Function via the Function URL (a failing function behind a function URL answered 502 Bad Gateway) — https://repost.aws/questions/QUsqRr4LFkQveggtBKvSMjpw/internal-server-error-502-bad-gateway-testing-a-lambda-function-via-the-function-url (fetched 2026-10-02)
- curl man page (
-wvariablehttp_code) — https://curl.se/docs/manpage.html (fetched 2026-10-02)
Project
Hands-on Project — Ship a Notes API on Lambda, Then Break It and Read the Logs
Objective
Deploy hoc-notes-api, a Python Lambda function that lists and adds notes in the hoc-notes
table, give it a public HTTPS address, prove it works with curl, and then break it four times on
purpose — once in the code, once in the execution role’s permissions, once in the function’s
configuration, and once in front of the function — so that you diagnose each failure from the
logs. You finish holding a working API that Unit 8 puts a web page in front of, and a written
record showing you can tell a code problem from a permission problem from a configuration problem,
and all three from a request that never reached your code.
What you need: your AWS account signed in as your everyday administrator identity, the AWS CLI
version 2.32.0 or later (aws --version) signed in with short-lived credentials, curl, and a text
editor. No other tool, key or paid account is needed. If you are starting here without the
earlier units, first do the set-up steps in If you are starting here below; everything they need
is in this file.
Region: the one you have used throughout; the examples say ap-south-1.
Cost. Lambda, DynamoDB and CloudWatch each have an Always Free monthly allowance under the current Free Tier, and this project’s few dozen requests stay well inside them. That is an allowance, not a guarantee: AWS’s billing guide says usage beyond an allowance is covered by your Free Tier credits, and charged on a Paid plan once credits are gone. Keep your budget alert on.
Keep one plain-text file open as you go, unit7-evidence.md. Every task below says exactly what to
paste into it.
If you are starting here — set-up
Skip this section if you already have an everyday sign-in, a budget alert and a signed-in CLI from the earlier units. Otherwise do these steps in order. Each comes from the AWS page named beside it in Sources.
- An everyday sign-in that is not the root user. The IAM guide’s best practice is not to use your root user credentials for daily tasks. Signed in as the root user, open IAM → Users → Create user, enable console access for the user and set its password, and give it administrator permissions by attaching a permissions policy directly to the user. Then sign out and sign in as that user (Setting up your AWS account; Create an IAM user in your AWS account).
- A budget alert. Open the Billing and Cost Management console, choose Budgets → Create budget, under Budget setup choose Use a template (simplified), choose the Zero spend budget template — a budget that notifies you after your spending exceeds the Free Tier limits — update the template’s details and settings, and choose Create budget (Using a budget template (simplified)).
- The AWS CLI, signed in with short-lived credentials. Install AWS CLI version 2 and run
aws --version: it must report 2.32.0 or later, the minimum the CLI guide gives foraws login. Runaws login; when it asks for an AWS Region, enterap-south-1; it then opens your browser, where you select the credentials to use and then return to the terminal. If the sign-in is refused, the guide says the IAM identity needs the AWS managed policySignInLocalDevelopmentAccessattached. Check withaws sts get-caller-identity: theAccountfield it prints is your 12-digit account ID, which the appendix below asks for (Login for AWS local development using console credentials). - The table and the role. Build both with the appendix below. Your table then starts empty,
with none of the notes the earlier units wrote, so your first
GETreturns an empty list.
Output: aws sts get-caller-identity prints your account ID, and the budget appears under
Budgets.
Before Task 1 — give the role permission to write logs
The role you built in Unit 6 carries only hoc-notes-table-access, so a function using it could
read and write the table but could not write a single log line. Attach the AWS managed logging
policy to it now:
aws iam attach-role-policy --role-name hoc-notes-api-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Output: nothing on success. If the role does not exist, build it with the appendix below, which already includes this step.
Appendix first — only if you do not have the table or the role
Skip this if hoc-notes and hoc-notes-api-role already exist from the earlier units. If you kept
them at the end of Unit 6, its hand-off record teardown.txt already says so in its table and
role lines; run the check anyway, because it reads what exists now:
aws dynamodb describe-table --table-name hoc-notes --query 'Table.TableStatus'
aws iam get-role --role-name hoc-notes-api-role --query 'Role.Arn'
If either command reports that the resource is not found, create what is missing.
The table (on-demand capacity, string partition key, tagged for the final teardown):
aws dynamodb create-table \
--table-name hoc-notes \
--attribute-definitions AttributeName=noteId,AttributeType=S \
--key-schema AttributeName=noteId,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--tags Key=project,Value=hands-on-cloud
The role. Save this as trust-policy.json — it lets the Lambda service assume the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
Save this as table-policy.json, replacing 111122223333 with your 12-digit account ID (shown in
the console’s account menu) and ap-south-1 with your Region if different:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["dynamodb:Scan", "dynamodb:PutItem"],
"Resource": "arn:aws:dynamodb:ap-south-1:111122223333:table/hoc-notes"
}
]
}
Then:
aws iam create-role --role-name hoc-notes-api-role \
--assume-role-policy-document file://trust-policy.json \
--tags Key=project,Value=hands-on-cloud
aws iam attach-role-policy --role-name hoc-notes-api-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam put-role-policy --role-name hoc-notes-api-role \
--policy-name hoc-notes-table-access \
--policy-document file://table-policy.json
Output: both check commands succeed — the first prints "ACTIVE", the second a role ARN.
Task 1 — Deploy the function and test it in the console
Scope: one function, one file, three console tests. Do not add a function URL yet.
-
Open the Lambda console → Create function → Author from scratch.
-
Function name:
hoc-notes-api. For the language, choose Python 3.14, which the Lambda runtimes page lists as the newest generally available Python runtime on 2026-10-02. The same page also lists Python 3.15, but marks it as a public preview that is not for production use, so do not choose it. Architecture: leavex86_64. -
Expand Permissions, choose Use another role, and pick
hoc-notes-api-role. Choose Create function. -
Write the handler yourself. In the Code tab, replace everything in
lambda_function.pywith the scaffold below, fill in the parts markedTODOso the function meets the behaviour table that follows, and choose Deploy. The notes walk through one way to build each behaviour; close them while you write, and go back to them only once your test events in step 6 have run. Scope: one file, no extra libraries, no CORS headers in the code (CORS is configured on the URL in Unit 8).import base64 import json import os import uuid from datetime import datetime, timezone import boto3 TABLE_NAME = os.environ["TABLE_NAME"] # no default: a missing variable must fail loudly table = boto3.resource("dynamodb").Table(TABLE_NAME) # keep this outside the handler MAX_TEXT = 500 def respond(status, payload): # TODO: return a dictionary with "statusCode", a "headers" entry whose # Content-Type is application/json, and "body" holding payload as a JSON # *string*. Items can hold numbers (Unit 6's notes carry a priority): the # Boto3 DynamoDB guide shows them coming back as Decimal('25'), and Decimal # is not in the json module's conversion table, so json.dumps raises # TypeError for it. Pass json.dumps a default= function (default=str # writes such a value as text) -- see Sources. ... def read_body(event): # TODO: return the request body as text, decoding it from base64 when # event["isBase64Encoded"] is true. A missing body is "". ... def lambda_handler(event, context): method = event.get("requestContext", {}).get("http", {}).get("method", "") print(f"{method} {event.get('rawPath', '/')}") # keep this line exactly: Task 3 edits it # TODO: one branch per row of the behaviour table below. ...Request The function must GETRead every item in the table (a scan) and return status 200with body{"notes": [...]}, newestcreatedAtfirst; items with nocreatedAtgo lastPOSTwhose body is a JSON object with atextvalue that is non-empty after trimming and at most 500 charactersStore a new item with noteId(a new random UUID, as a string),text(trimmed) andcreatedAt(the current UTC time in ISO 8601), and return status201with that item as the bodyPOSTwhose body is not valid JSONReturn status 400with body{"error": "<a message>"}, and store nothingPOSTwithtextmissing, empty or only spacesReturn status 400with body{"error": "text is required"}, and store nothingPOSTwithtextover 500 charactersReturn status 400with body{"error": "<a message>"}, and store nothingOPTIONSReturn status 204with no bodyAny other method Return status 405with body{"error": "<a message>"}Your function is done when the three test events in step 6 each return what this table says.
-
In Configuration → Environment variables, choose Edit → Add environment variable, Key
TABLE_NAME, Valuehoc-notes, and Save. The scaffold reads this variable with no default, so every test in step 6 fails until it is set; if a test’s log showsKeyError: 'TABLE_NAME', come back to this step. -
In the Code tab’s TEST EVENTS section, create a test event named
post-notewith this JSON — a cut-down version of what a function URL sends:{ "version": "2.0", "rawPath": "/", "requestContext": { "http": { "method": "POST", "path": "/" } }, "headers": { "content-type": "application/json" }, "body": "{\"text\": \"note from the console\"}", "isBase64Encoded": false }Save it and run it. Then create a second event,
list-notes, identical except"method": "GET"and"body": "", and run that. Finally createempty-note, identical topost-noteexcept"body": "{\"text\": \"\"}", and run it. If any response differs from the behaviour table, fix the code, choose Deploy again, and rerun all three.
Paste into unit7-evidence.md, under a heading Task 1: the Response shown for each of the
three tests, and the REPORT line from each. Note whether any REPORT line carries Init Duration, and say in one
sentence what that field records.
Time budget: 40 minutes
Output: your deployed lambda_function.py (keep a copy beside unit7-evidence.md), and
unit7-evidence.md with the three test responses, each labelled with its test event name, the three
REPORT lines, and your one-sentence explanation.
Task 2 — Give it a public address and prove it with curl
Scope: one function URL, four curl requests, one tag. CORS waits for Unit 8, which is when a
browser page first calls the function.
-
Configuration → Function URL → Create function URL. Auth type:
NONE. Leave CORS unticked. Save. Copy the URL from the Function overview. -
Open Configuration → Permissions and scroll to Resource-based policy. Copy the statements the console added into
unit7-evidence.md, and next to each write which of the two actions it allows:lambda:InvokeFunctionUrlorlambda:InvokeFunction. -
In a terminal:
URL='paste-your-function-url-here' curl -s "$URL" curl -s -X POST "$URL" -H 'content-type: application/json' -d '{"text":"first note from curl"}' curl -s "$URL" curl -s -i -X POST "$URL" -H 'content-type: application/json' -d '{"text":""}' -
Tag the function for the final teardown. Copy the Function ARN from the Function overview (it looks like
arn:aws:lambda:ap-south-1:111122223333:function:hoc-notes-api) and run:aws lambda tag-resource \ --resource arn:aws:lambda:ap-south-1:111122223333:function:hoc-notes-api \ --tags project=hands-on-cloudThe command prints nothing on success.
Paste the four curl outputs into unit7-evidence.md under Task 2.
Time budget: 30 minutes
Output: in unit7-evidence.md: the resource-based policy statements, each labelled with its
action, and the four curl outputs, each labelled with the command that produced it.
Task 3 — Break it four times and diagnose each failure from the logs
Scope: four deliberate failures, each restored and confirmed before the next. Each one fails in a different place: your code, the execution role, the function’s configuration, and in front of the function. Your job is to record what the caller saw and what the log shows, so that you learn the signature of each.
The two tools you use for every break.
-
What the caller sees. This command sends one
GETand prints only the three-digit HTTP status code (-o /dev/nulldiscards the body;-w '%{http_code}\n'prints the code):curl -s -o /dev/null -w '%{http_code}\n' "$URL"If your terminal has been closed since Task 2, set
URLagain first. -
What the log shows. The function’s Monitor tab → View CloudWatch logs, then the newest log stream; or from the CLI:
aws logs tail /aws/lambda/hoc-notes-api --since 15mLogs are not instant. If the newest entries are from before your request, wait, refresh (or run the command again), and only then conclude anything. Each request that reaches your code prints a
GET /line, and each invocation has its ownSTART,ENDandREPORTlines.
For each break, write down:
- Status — the three digits the
curlcommand printed. - Log line — copy, word for word, the first line logged for that request that contains the word
Error(it usually starts with[ERROR]). If the log has no new entry at all for your request, writeno record. If it has entries but none containsError, writeno error line. - Restored status — the three digits the same
curlcommand printed after you undid the break.
Break 1 — a bug in the code. In the Code tab, change the print line in lambda_handler
from {method} to {methd} — a one-letter typo — and choose Deploy. Send the status curl,
then find the request in the log. To restore, change {methd} back to {method}, choose
Deploy, and send the status curl again; repeat it until it prints 200.
Break 2 — a missing permission. The function reads the table with the execution role’s inline
policy hoc-notes-table-access. First save that policy exactly as it is now, so you can put it
back:
aws iam get-role-policy --role-name hoc-notes-api-role --policy-name hoc-notes-table-access \
--query PolicyDocument --output json > table-policy-saved.json
Open table-policy-saved.json: it must contain a "Statement" entry. If it does not, stop and do not
continue this break — rerun the command and check the role and policy names. Then remove the policy
from the role:
aws iam delete-role-policy --role-name hoc-notes-api-role --policy-name hoc-notes-table-access
It prints nothing on success. Send the status curl. IAM changes take time to become visible
everywhere (the IAM guide calls this eventual consistency), so if it still prints 200, send it
again until it does not; if it keeps printing 200, change the function’s Description under
Configuration → General configuration → Edit, Save, and try again — Lambda’s troubleshooting
guide gives this trivial update as the way to replace running instances that hold older credentials
after a role’s permissions change. Record the first non-200 status and that request’s log line.
To restore:
aws iam put-role-policy --role-name hoc-notes-api-role --policy-name hoc-notes-table-access \
--policy-document file://table-policy-saved.json
Send the status curl again, repeating it until it prints 200.
Break 3 — a configuration mistake. In Configuration → Environment variables → Edit, change
the Key TABLE_NAME to TABLENAME (leave the value hoc-notes), and Save. Your code reads
os.environ["TABLE_NAME"] with no default, so the variable it expects is now missing. Send the
status curl and find the request in the log. To restore, change the key back to TABLE_NAME,
Save, and repeat the status curl until it prints 200.
Break 4 — refused in front of the function. Write down the current time. Open Configuration →
Concurrency, choose Edit, choose Reserve concurrency, enter 0, and Save — or from the
CLI:
aws lambda put-function-concurrency --function-name hoc-notes-api \
--reserved-concurrent-executions 0
Send the status curl. Then look for any log entry — a START line, a GET / line, anything —
written after the time you noted. Refresh until you are sure; if there is none, the log line is
no record. To restore:
aws lambda delete-function-concurrency --function-name hoc-notes-api
and repeat the status curl until it prints 200.
If Lambda refuses the concurrency change (the reserved-concurrency page says you can reserve only up
to your account’s unreserved concurrency minus 100, and a new account’s limit can be lower than
that), do Break 4 another way: change the function URL’s Auth type to AWS_IAM under
Configuration → Function URL → Edit and Save, send the same unsigned status curl, record
the status and the log line exactly as above, then change the auth type back to NONE and repeat the
status curl until it prints 200. Write (AWS_IAM) after the break number in the table.
Copy this table into unit7-evidence.md and fill in every cell:
| Break | Status | Log line | Where the decision was made | Restored status |
|---|---|---|---|---|
| 1 — typo in the code | ||||
| 2 — inline policy removed | ||||
| 3 — environment variable key renamed | ||||
| 4 — reserved concurrency 0 |
The Where the decision was made column takes exactly one of these four values, written as shown:
my code, the execution role, the function's configuration, before the function ran. Use
each value once. Choose it from the evidence in the Status and Log line cells.
Time budget: 40 minutes
Output: the completed four-row table in unit7-evidence.md, plus the output of a final
curl -s "$URL" showing the API returning your notes after all four restores.
Task 4 — Decide what stays, and tear down the rest (optional extension)
Unit 8 builds directly on hoc-notes-api, its function URL, hoc-notes-api-role and hoc-notes.
If you will start Unit 8 soon, keep all four, and park the public URL so nobody can use it while
you are away: set reserved concurrency to 0 again, or switch the auth type to AWS_IAM if your
account would not reserve concurrency (Unit 8 tells you when to undo it). Record in
unit7-evidence.md the four resources you kept and how the function is parked.
If you are stopping here, remove everything this unit created, in this order, ticking each line:
-
Lambda console → select
hoc-notes-api→ Actions → Delete, typeconfirm, Delete. The function URLs page says Lambda deletes a deleted function’s URL asynchronously. -
CloudWatch console → Log groups → select
/aws/lambda/hoc-notes-api→ Actions → Delete log group(s) → Delete. Lambda created this log group, not you, so you never tagged it and a search for theprojecttag will not find it. -
IAM console → Roles →
hoc-notes-api-role→ Delete, type the role name, Delete. Thedelete-rolereference says the console removes attached and inline policies with the role, while the CLI requires you to remove them first. -
DynamoDB → delete the
hoc-notestable, or from the CLI:aws dynamodb delete-table --table-name hoc-notes. -
Run these four checks; each must now report that the resource is not found, or list nothing:
``` aws lambda get-function --function-name hoc-notes-api aws logs describe-log-groups --log-group-name-prefix /aws/lambda/hoc-notes-api --query 'logGroups[].logGroupName' aws iam get-role --role-name hoc-notes-api-role aws dynamodb describe-table --table-name hoc-notes ```
Time budget: 15 minutes
Output: either a record of the four resources kept and the parked function, or the ticked checklist with the four check results pasted underneath.
What this feeds
Unit 8 opens with the URL you created in Task 2. Its first step adds CORS for one origin — the static
website you are about to host — and its first test is the same POST you sent here, this time from a
browser. The diagnosis table from Task 3 is the method Unit 8 uses when its alarm fires.
Sources
The AWS behaviour this project relies on is documented on these pages, each fetched on 2026-10-02:
- Lambda runtimes (Python 3.14 generally available; preview runtimes) — https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html
- Defining Lambda function permissions with an execution role — https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html
- Define Lambda function handler in Python (code outside the handler is reused across invocations in the same execution environment) — https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html
- AWS CLI
lambda tag-resource— https://docs.aws.amazon.com/cli/latest/reference/lambda/tag-resource.html - Creating and managing Lambda function URLs (URL deleted with its function; deactivating a URL) — https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html
- Control access to Lambda function URLs (console adds the
NONEpolicy; 403 without it) — https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html - Invoking Lambda function URLs (event and response format) — https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html
- Configuring reserved concurrency (reserve up to unreserved concurrency minus 100) — https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html
- Viewing CloudWatch logs for Lambda functions (Monitor tab; logs need time to become available) — https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-view.html
- AWS CLI
logs tail— https://docs.aws.amazon.com/cli/latest/reference/logs/tail.html - Login for AWS local development using console credentials (
aws login; minimum CLI 2.32.0) — https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sign-in.html - AWS CLI
iam delete-role(console removes attached policies; CLI does not) — https://docs.aws.amazon.com/cli/latest/reference/iam/delete-role.html - AWS CLI
dynamodb create-table— https://docs.aws.amazon.com/cli/latest/reference/dynamodb/create-table.html - Boto3 guide, Amazon DynamoDB (
get_itemandscanexamples return numbers asDecimal('25')) — https://docs.aws.amazon.com/boto3/latest/guide/dynamodb.html - Python standard library,
json(conversion table lists noDecimal; unserialisable objects raiseTypeError; thedefaultparameter) — https://docs.python.org/3/library/json.html - Setting up your AWS account (do not use root user credentials for daily tasks) — https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-started-account-iam.html
- Create an IAM user in your AWS account (console access; attaching permissions policies directly to the user) — https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html
- Using a budget template (simplified) (Zero spend budget) — https://docs.aws.amazon.com/cost-management/latest/userguide/budget-templates.html
- Explore AWS services with AWS Free Tier (Always Free allowances; credits beyond them) — https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/free-tier.html
- Monitoring Lambda function URLs (5XX codes indicate server-side errors such as function errors and timeouts) — https://docs.aws.amazon.com/lambda/latest/dg/urls-monitoring.html
- Troubleshoot execution issues in Lambda (errors can come from code, configuration or permissions; a trivial update replaces running instances that hold outdated credentials) — https://docs.aws.amazon.com/lambda/latest/dg/troubleshooting-execution.html
- Troubleshoot IAM (changes are not always immediately visible: eventual consistency) — https://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_general.html
- AWS CLI
iam get-role-policy(output includes thePolicyDocument) — https://docs.aws.amazon.com/cli/latest/reference/iam/get-role-policy.html - AWS CLI
iam delete-role-policy(no output on success) — https://docs.aws.amazon.com/cli/latest/reference/iam/delete-role-policy.html - AWS CLI
iam put-role-policy— https://docs.aws.amazon.com/cli/latest/reference/iam/put-role-policy.html - Error handling with DynamoDB (
AccessDeniedException) — https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html - Working with Lambda environment variables (reading a variable with
os.environ) — https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html - Python standard library,
os.environ(a mapping) and built-in exceptions (KeyErrorwhen a mapping key is not found;NameErrorwhen a name is not found) — https://docs.python.org/3/library/os.html and https://docs.python.org/3/library/exceptions.html - curl man page (
-o,-wand thehttp_codevariable) — https://curl.se/docs/manpage.html
Quiz
Online marking is not open yet. Work through the questions and check the written ones against the criteria printed with each question. The answer key is kept back for now.
Quiz — Unit 7: Code Without Servers: Lambda
Instructions: Ten questions in two parts, and both parts are scored.
- Questions 1–7 are multiple choice. Pick one option per question. They are marked by comparing your letter against the key.
- Questions 8–10 ask you to write a command or a JSON document. Each is marked by a script that applies the rubric held in the separate answer key. Layout, option order and quoting style are not marked; only whether your answer does what the question asks.
Work closed-book on questions 1–7. On questions 8–10 you may test your answer as often as you like before submitting it — run the command against your own account, or parse the JSON with any tool.
1. You create hoc-notes-api in the console, accept the default permissions, and send a POST
that should save a note. The caller gets an error, and the log shows AccessDeniedException … is not authorized to perform: dynamodb:PutItem. What is the smallest correct fix?
a) Attach an administrator policy to the execution role so the function can reach any table it needs
b) Change the function URL’s auth type from NONE to AWS_IAM so that every request is authenticated
c) Switch the function to an execution role allowing only PutItem, on only the hoc-notes table
d) Add lambda:InvokeFunction for the principal * to the function’s resource-based policy statements
2. Which statement about a function URL with auth type NONE is correct?
a) It turns off authentication, but the function’s resource-based policy must still allow public calls b) It turns off authentication and the resource-based policy, so every request reaches the handler code c) It keeps signature checks but accepts requests signed by any AWS account, not only your own account d) It makes the URL reachable only from inside the account’s own VPC, so outside callers are refused
3. A script creates a function URL with aws lambda create-function-url-config --function-name hoc-notes-api --auth-type NONE. Every curl to the new URL returns 403, and no new log stream
appears in /aws/lambda/hoc-notes-api. Why?
a) The function’s timeout is shorter than the request, so Lambda abandons the call before logging it
b) The execution role lacks logs:CreateLogStream, so the function runs but cannot record anything
c) CORS is not configured, so the function URL refuses any request that does not come from a browser
d) The CLI does not add the public invoke permissions that the console adds for an auth type of NONE
4. A handler returns the dictionary {"notes": []} and nothing else — no statusCode, no
headers, no body key. What does a caller of the function URL receive?
a) An error, because a function URL requires every response to include a statusCode field
b) Status 200, content type application/json, and that object as the body of the response
c) Status 204 with an empty body, because the returned dictionary does not set a body field
d) Status 200, content type text/plain, and the Python representation of the dictionary
5. A page served from http://hoc-site-ab-4821.s3-website.ap-south-1.amazonaws.com calls the
function URL with fetch() and a JSON POST. curl works; the browser reports the request was
blocked by CORS policy. Which change fixes it with the least exposure?
a) Configure CORS on the function URL to allow that one origin, GET and POST, and content-type
b) Return Access-Control-Allow-Origin from the function’s code as well as configuring it on the URL
c) Change the auth type to AWS_IAM so that the browser’s request is signed and trusted by Lambda
d) Configure CORS on the function URL with the allowed origin set to * so any page can call it
6. A REPORT line ends with Init Duration: 301.55 ms. What does that part of the line tell you?
a) The handler spent that long waiting for DynamoDB before it could return a response to the caller b) This invocation started a new execution environment first, and the start-up took that long c) The function was near its timeout, and Lambda granted that much extra time to let it finish d) Lambda billed that long for memory initialisation on top of the duration of every request
7. The notes function creates its DynamoDB table object at the top of the file, outside
lambda_handler. Why is that the recommended place?
a) Lambda supplies the execution role’s credentials only to code that runs outside the handler
b) Objects created inside the handler are discarded before the response is sent to the caller
c) Code outside the handler runs once per execution environment, so the set-up is reused
d) The console code editor refuses to deploy a file that imports boto3 inside a function body
Part 2 — Constructed response
Submit plain text exactly as you would type it — a command on one line (or with \ line
continuations), or a JSON document. Do not wrap your answer in quotes or code fences.
8. Write the AWS CLI command that creates a function URL for the function hoc-notes-api, with
no authentication, on the unpublished version of the function (not on an alias).
9. Write the JSON document you would pass as the CORS configuration of the function URL so that
only a page served from http://hoc-site-ab-4821.s3-website.ap-south-1.amazonaws.com may call it,
using GET and POST, and may send the content-type header.
10. A POST arrives with the body {"text": ""}. Write, as a JSON document, the exact response
object the notes handler should return so that the caller receives status 400, a JSON content
type, and a JSON body carrying an error message.
Sources
Each page below was fetched on 2026-10-02.
- Defining Lambda function permissions with an execution role — https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html (fetched 2026-10-02)
- Control access to Lambda function URLs — https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html (fetched 2026-10-02)
- Creating and managing Lambda function URLs — https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html (fetched 2026-10-02)
- Invoking Lambda function URLs — https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html (fetched 2026-10-02)
- Define Lambda function handler in Python — https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html (fetched 2026-10-02)
- Viewing CloudWatch logs for Lambda functions — https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-view.html (fetched 2026-10-02)
- AWS CLI
create-function-url-config— https://docs.aws.amazon.com/cli/latest/reference/lambda/create-function-url-config.html (fetched 2026-10-02)
TechEazy Consulting training material — not affiliated with or endorsed by Amazon Web Services (AWS).