aws #lambda #serverless #tutorial
Working with AWS Lambda
INTRO
This lab builds a small but complete serverless pipeline: a Lambda function that runs on a schedule, pulls sales data out of a MySQL database running on a Café web app, formats it into a report, and emails it to you — all without a single server you manage yourself. Along the way you'll create two Lambda functions, a Lambda layer for a shared library, an SNS topic for email delivery, and a CloudWatch schedule to trigger the whole thing automatically.
Here's the flow, start to finish:
- A CloudWatch schedule triggers
salesAnalysisReportevery evening - That function calls a second function,
salesAnalysisReportDataExtractor - The extractor queries the café database directly
- The result comes back to
salesAnalysisReport -
salesAnalysisReportformats it into a message and publishes it to an SNS topic - SNS emails the finished report to you
Everything below builds one piece of that chain at a time.
TASK 1: LOOK AT THE IAM ROLES BEFORE TOUCHING ANYTHING
Before creating either function, you look at the two IAM roles they'll use, so you know exactly what each function is — and isn't — allowed to do.
salesAnalysisReportRole (used by the report function):
- AmazonSNSFullAccess — lets it publish the report to SNS
- AmazonSSMReadOnlyAccess — lets it read the database credentials from Parameter Store
- AWSLambdaBasicRunRole — lets it write logs to CloudWatch
- AWSLambdaRole — lets it invoke another Lambda function (the data extractor)
salesAnalysisReportDERole (used by the data extractor function):
- AWSLambdaBasicRunRole — same logging permission
- AWSLambdaVPCAccessRunRole — lets it create network interfaces so it can actually reach into the VPC where the database lives
Why we did this: Every Lambda function is only as capable as the role attached to it — no role, no permission, no matter how correct the code is. Looking at these upfront means that later, if a function fails with an access error, you already know exactly which permission to check first instead of guessing blind.
TASK 2: CREATE A LAMBDA LAYER, THEN THE DATA EXTRACTOR FUNCTION
This task has five parts — a layer, a function, attaching the layer, importing code, and wiring up networking.
Grab the two files you'll need for this task:
2.1 — Create the Lambda layer:
In Lambda → Layers → Create layer:
- Name:
pymysqlLibrary - Description:
PyMySQL library modules - Upload the
pymysql-v3.zipfile - Compatible runtime: Python 3.9
2.2 — Create the data extractor function:
In Functions → Create function:
- Author from scratch
- Name:
salesAnalysisReportDataExtractor - Runtime: Python 3.9
- Execution role: use the existing
salesAnalysisReportDERole
2.3 — Attach the layer to the function:
In the function's Layers section → Add a layer → Custom layers → pymysqlLibrary, version 1.
2.4 — Import the actual code:
In Runtime settings, set the handler to salesAnalysisReportDataExtractor.lambda_handler, then upload salesAnalysisReportDataExtractor-v3.zip as the code source. Take a minute to actually read through the code and its comments — notice it expects dbURL, dbName, dbUser, and dbPassword to be passed in as part of its input event.
2.5 — Configure networking:
Under Configuration → VPC, set:
- VPC: Cafe VPC
- Subnet: Cafe Public Subnet 1
- Security group: CafeSecurityGroup
Why we did this: A Lambda layer exists so you're not bundling the same library into every function that needs it — one shared package, reused anywhere. And the VPC step matters because Lambda functions run outside your VPC by default; without explicitly attaching it to the café's VPC, subnet, and security group, this function would have no network path to reach the database at all.
TASK 3: TEST THE DATA EXTRACTOR — AND FIX WHAT BREAKS
3.1 — Run a first test:
Grab the four database connection values from Parameter Store (/cafe/dbUrl, /cafe/dbName, /cafe/dbUser, /cafe/dbPassword), then create a test event on the function with those values as the JSON input:
{
"dbUrl": "<value of /cafe/dbUrl>",
"dbName": "<value of /cafe/dbName>",
"dbUser": "<value of /cafe/dbUser>",
"dbPassword": "<value of /cafe/dbPassword>"
}
Run the test — it fails.
3.2 — Read the failure:
The error says the task timed out after 3 seconds. That 3-second default timeout is barely enough time to even attempt a database connection, let alone get a response back — so this points squarely at something blocking the connection itself, not a code bug.
3.3 — Find and fix the real cause:
MySQL listens on port 3306. Check the inbound rules on the security group attached to the database's EC2 instance — if 3306 isn't open to the Lambda function's security group, the connection attempt just hangs until it times out. Add that inbound rule, then run the test again. This time it succeeds, but the response body comes back empty — there's simply no order data in the database yet.
3.4 — Put real data in, then test again:
Open the café website (http://<publicIP>/cafe), place a couple of orders through the menu, then re-run the test. This time the response body actually contains real order data — quantities, product names, everything the report needs.
Why we did this: This is a textbook case of "the code is fine, the network isn't" — a timeout error tells you almost nothing about why by itself, but knowing MySQL's default port narrows the search immediately. And testing with an empty database first, then a populated one, proves two separate things: that the connection works, and that the query logic itself is correct — not just that the function didn't crash.
TASK 4: SET UP THE EMAIL NOTIFICATION
4.1 — Create the SNS topic:
In SNS → Topics → Create topic:
- Type: Standard
- Name:
salesAnalysisReportTopic - Display name:
SARTopic
Save the topic's ARN — you'll need it in the next task.
4.2 — Subscribe your email:
Create a subscription on that topic with Protocol: Email, and your own email as the endpoint. Confirm it from the email AWS sends you.
Why we did this: SNS is the delivery mechanism, not the report generator — the Lambda function doesn't know how to send email itself, it just knows how to publish a message to a topic. Confirming the subscription is a required step too: an unconfirmed subscription silently receives nothing, so skipping it means the whole pipeline "works" but nobody ever sees the result.
TASK 5: CREATE THE MAIN REPORT FUNCTION — THIS TIME VIA THE CLI
5.1 — Connect to the CLI host:
Use EC2 Instance Connect to log into the CLI host instance, which already has the AWS CLI and the Python code for this function pre-loaded.
5.2 — Configure the CLI:
aws configure
Supply the access key, secret key, region (us-west-2 for this lab), and json as output format.
5.3 — Create the function via the CLI:
First confirm the code is where you expect it:
cd activity-files
ls
Grab the ARN of salesAnalysisReportRole from IAM, then create the function:
aws lambda create-function \
--function-name salesAnalysisReport \
--runtime python3.9 \
--zip-file fileb://salesAnalysisReport-v2.zip \
--handler salesAnalysisReport.lambda_handler \
--region <region> \
--role <salesAnalysisReportRoleARN>
5.4 — Configure the environment variable:
The code reads the SNS topic ARN from an environment variable called topicARN — it's not hardcoded in the function, so you have to set it. In Configuration → Environment variables, add:
- Key:
topicARN - Value: the ARN of
salesAnalysisReportTopicfrom Task 4
5.5 — Test it:
Create a test event (no input parameters needed — this function doesn't take any), run it, and check for a statusCode: 200 response. Then check your inbox — you should receive an actual "Daily Sales Analysis Report" email with the items you ordered earlier.
5.6 — Add the daily trigger:
Add an EventBridge (CloudWatch Events) trigger with a schedule expression written as a Cron expression:
cron(Minutes Hours Day-of-month Month Day-of-week Year)
For testing, set it 5 minutes ahead of the current UTC time so you can see it fire quickly. For production, you'd want something like cron(0 20 ? * MON-SAT *) — 8 PM UTC, Monday through Saturday, matching the schedule described at the start of this lab.
Why we did this: Using the CLI here instead of the console is deliberate — it's the same underlying operation, just proving that Lambda functions can be created and deployed as part of a script or pipeline, not only by clicking through a UI. The environment variable step matters for a different reason: hardcoding an ARN into the code would mean editing and redeploying code every time the SNS topic changed, while an environment variable means you can update it in seconds without touching a single line of Python.
WHY THIS MATTERS OVERALL
- Two functions calling each other, each with its own scoped IAM role, is a far safer pattern than one function with every permission bundled together
- A timeout error almost always means "something on the network side is blocking me," not "my code is broken" — check ports and security groups before you start rewriting logic
- Layers, environment variables, and VPC config all exist for the same underlying reason: keeping configuration and shared code outside the function itself, so changes don't require a full redeploy
- Testing with empty data and then real data isn't redundant — it's how you separate "the connection works" from "the logic is correct"

Top comments (0)