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 NONE rather than AWS_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-notes table 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 that curl is there by running curl --version; if that prints no version, install curl before you go on.
  • The DynamoDB table hoc-notes (partition key noteId, type String) and the IAM role hoc-notes-api-role from 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.

  1. 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).
  2. 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).
  3. 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 for aws 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. Run aws login, enter ap-south-1 when 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 policy SignInLocalDevelopmentAccess. Check with aws sts get-caller-identity: the Account field is your 12-digit account ID, which the project’s appendix asks for (Login for AWS local development using console credentials).
  4. 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 and body are all fields in it.
  • The body is a string, and may be base64-encoded. The event’s body is text; if the request was binary, Lambda sets isBase64Encoded to 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. noteId is a random UUID, createdAt an ISO timestamp, and text the note. The notes you added in Unit 6 also carry priority, a number, and Boto3 returns DynamoDB numbers as Python Decimal values, which json.dumps refuses on its own. That is what default=str in respond() is for: anything json.dumps cannot serialise is written as text, so a priority of 2 reaches the caller as "2" instead of crashing the function.
  • The response is a dictionary with statusCode, headers and body. The function URL documentation says Lambda converts that into the HTTP response. If you return plain JSON without a statusCode, Lambda assumes status 200, content type application/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.
  • OPTIONS returns 204. Browsers send an OPTIONS request 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 a LastEvaluatedKey when 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:

  1. The resource-based policy still decides. NONE turns off authentication, not authorisation. The function’s resource-based policy must allow lambda:InvokeFunctionUrl and lambda: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 with aws lambda add-permission.
  2. 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.
  3. Deleting the URL leaves the policy behind. If you delete a NONE function 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 --follow shows 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:

  1. You add an “edit note” feature that calls UpdateItem and test it with curl. It returns 502. The log shows not authorized to perform: dynamodb:UpdateItem. The role was never given that action.
  2. A friend opens your web page and nothing loads. curl works for you. Their browser’s console shows a CORS error naming a different origin — they opened the page through a different address.
  3. 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.

  1. Caller authentication. Name the auth type and why: NONE because the page has no sign-in, so its visitors’ browsers hold no AWS credentials to sign with, or AWS_IAM because only your own signed-in services call it.
  2. Caller authorisation. Say that the function’s resource-based policy must allow lambda:InvokeFunctionUrl and lambda:InvokeFunction, and that the console adds this for NONE while the CLI does not.
  3. 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.
  4. 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 to AWS_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, and curl ignores it.
  5. 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; AccessDenied inside 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 allow lambda:InvokeFunctionUrl and lambda:InvokeFunction for 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 by lambda.amazonaws.com and 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, and curl ignores 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 to AWS_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; an AccessDenied inside 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 NONE to 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 to AWS_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 curl and 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.

  1. Lambda runtimes — https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html (fetched 2026-10-02)
  2. Create your first Lambda function — https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html (fetched 2026-10-02)
  3. Define Lambda function handler in Python — https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html (fetched 2026-10-02)
  4. 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)
  5. Configure Lambda function timeout — https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html (fetched 2026-10-02)
  6. Creating and managing Lambda function URLs — https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html (fetched 2026-10-02)
  7. Control access to Lambda function URLs — https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html (fetched 2026-10-02)
  8. Invoking Lambda function URLs — https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html (fetched 2026-10-02)
  9. 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)
  10. Sending Lambda function logs to CloudWatch Logs — https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html (fetched 2026-10-02)
  11. Viewing CloudWatch logs for Lambda functions — https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-view.html (fetched 2026-10-02)
  12. AWS CLI create-function-url-config — https://docs.aws.amazon.com/cli/latest/reference/lambda/create-function-url-config.html (fetched 2026-10-02)
  13. AWS CLI create-function — https://docs.aws.amazon.com/cli/latest/reference/lambda/create-function.html (fetched 2026-10-02)
  14. AWS CLI logs tail — https://docs.aws.amazon.com/cli/latest/reference/logs/tail.html (fetched 2026-10-02)
  15. Boto3 DynamoDB Table.scan — https://docs.aws.amazon.com/boto3/latest/reference/services/dynamodb.html#DynamoDB.Table.scan (fetched 2026-10-02)
  16. Explore AWS services with AWS Free Tier — https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/free-tier.html (fetched 2026-10-02)
  17. Tracking your AWS Free Tier usage — https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/tracking-free-tier-usage.html (fetched 2026-10-02)
  18. AWS Lambda pricing — https://aws.amazon.com/lambda/pricing/ (fetched 2026-10-02)
  19. 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)
  20. AWS CLI version 2 changelog (InvokedViaFunctionUrl added in 2.31.13) — https://raw.githubusercontent.com/aws/aws-cli/v2/CHANGELOG.rst (fetched 2026-10-02)
  21. 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)
  22. Create an IAM user in your AWS account — https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html (fetched 2026-10-02)
  23. Using a budget template (simplified) — https://docs.aws.amazon.com/cost-management/latest/userguide/budget-templates.html (fetched 2026-10-02)
  24. 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)
  25. 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)
  26. curl man page (-w variable http_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.

  1. 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).
  2. 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)).
  3. 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 for aws login. Run aws login; when it asks for an AWS Region, enter ap-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 policy SignInLocalDevelopmentAccess attached. Check with aws sts get-caller-identity: the Account field it prints is your 12-digit account ID, which the appendix below asks for (Login for AWS local development using console credentials).
  4. 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 GET returns 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.

  1. Open the Lambda console → Create function → Author from scratch.

  2. 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: leave x86_64.

  3. Expand Permissions, choose Use another role, and pick hoc-notes-api-role. Choose Create function.

  4. Write the handler yourself. In the Code tab, replace everything in lambda_function.py with the scaffold below, fill in the parts marked TODO so 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
    GET Read every item in the table (a scan) and return status 200 with body {"notes": [...]}, newest createdAt first; items with no createdAt go last
    POST whose body is a JSON object with a text value that is non-empty after trimming and at most 500 characters Store a new item with noteId (a new random UUID, as a string), text (trimmed) and createdAt (the current UTC time in ISO 8601), and return status 201 with that item as the body
    POST whose body is not valid JSON Return status 400 with body {"error": "<a message>"}, and store nothing
    POST with text missing, empty or only spaces Return status 400 with body {"error": "text is required"}, and store nothing
    POST with text over 500 characters Return status 400 with body {"error": "<a message>"}, and store nothing
    OPTIONS Return status 204 with no body
    Any other method Return status 405 with body {"error": "<a message>"}

    Your function is done when the three test events in step 6 each return what this table says.

  5. In Configuration → Environment variables, choose Edit → Add environment variable, Key TABLE_NAME, Value hoc-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 shows KeyError: 'TABLE_NAME', come back to this step.

  6. In the Code tab’s TEST EVENTS section, create a test event named post-note with 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 create empty-note, identical to post-note except "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.

  1. Configuration → Function URL → Create function URL. Auth type: NONE. Leave CORS unticked. Save. Copy the URL from the Function overview.

  2. 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:InvokeFunctionUrl or lambda:InvokeFunction.

  3. 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":""}'
  4. 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-cloud

    The 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 GET and prints only the three-digit HTTP status code (-o /dev/null discards 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 URL again 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 15m

    Logs 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 own START, END and REPORT lines.

For each break, write down:

  • Status — the three digits the curl command 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, write no record. If it has entries but none contains Error, write no error line.
  • Restored status — the three digits the same curl command 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, type confirm, 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 the project tag will not find it.

  • IAM console → Roles → hoc-notes-api-role → Delete, type the role name, Delete. The delete-role reference says the console removes attached and inline policies with the role, while the CLI requires you to remove them first.

  • DynamoDB → delete the hoc-notes table, 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:

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.

TechEazy Consulting training material — not affiliated with or endorsed by Amazon Web Services (AWS).