LABS/MINI-PROJECTS Links
LAB: Serverless Pet Cuddle-O-Tron
Pet Cuddle-O-Tron – Overview
Final Architecture Diagram (Simplified) – After Stage 5

Final Architecture Diagram (Extended) – After Stage 7

STAGE 1: Set Up Amazon Simple Email Service (SES)

Note: SES handles outgoing emails. The serverless application must approve the sender and recipient addresses before sending.
- Amazon Simple Email Service (SES) = managed service for sending emails automatically
- By default, SES operates in sandbox mode, restricting emails to verified addresses (prevents accidental spamming)
- For this lab, create two SES email identities (one sender and one recipient) and verify them to allow email delivery
STAGE 2: Create Lambda Function for Sending Emails

A Lambda function will use SES to send emails from the serverless app.
- Create a Lambda execution role (via IAM manually or with CloudFormation stack):
- Grant permissions for SES, SNS, Step Functions (
states) and CloudWatch logs.
- Grant permissions for SES, SNS, Step Functions (
- Create and configure the Lambda function:
- Name:
email_reminder_lambda - Runtime: Python 3.9
- Execution role: the role created above
- Update
FROM_EMAIL_ADDRESSwith your SES sender email.
print(“Received event: ” + json.dumps(event))
ses.send_email(
Source=FROM_EMAIL_ADDRESS,
Destination={‘ToAddresses’: [event[‘Input’][’email’]]},
Message={
‘Subject’: {‘Data’: ‘Whiskers Commands You to attend!’},
‘Body’: {‘Text’: {‘Data’: event[‘Input’][‘message’]}}
}
)
return ‘Success!’ - Name:
STAGE 3: Set Up Step Functions State Machine

The state machine manages the workflow and coordinates AWS services.
- Create a State Machine role (IAM or CloudFormation):
- Allow logging to CloudWatch
- Invoke the email Lambda
- Use SNS for optional SMS
- Create the State Machine:
- Name:
Pet-Cuddle-O-Tron, Type:Standard - Assign the created role
- Logging:
ALL - Replace
FunctionNamewith your Lambda ARN:
“Comment”: “Pet-Cuddle-o-Tron using Lambda for email.”,
“StartAt”: “Timer”,
“States”: {
“Timer”: {“Type”: “Wait”, “SecondsPath”: “$.waitSeconds”, “Next”: “Email”},
“Email”: {
“Type”: “Task”,
“Resource”: “arn:aws:states:::lambda:invoke”,
“Parameters”: {“FunctionName”: “EMAIL_LAMBDA_ARN”,”Payload”: {“Input.$”: “$”}},
“Next”: “NextState”
},
“NextState”: {“Type”: “Pass”, “End”: true}
}
} - Name:
Validate State Machine workflow:

STAGE 4: Set Up Backend API Using API Gateway and Lambda

Note: The backend of the application (state machine and supporting services) will expose an API for the frontend. This API is managed via API Gateway (APIGW), with a resource and method linked to a Lambda function. The API is deployed to the Prod stage.
- Create and configure a Lambda function for the API
- Function name:
api_lambda, Runtime:Python 3.9 - Assign the same Lambda execution role used for the email function
- Update the code to replace
SM_ARNwith the ARN of the application’s state machine
print(“Received event: ” + json.dumps(event))
data = json.loads(event[‘body’])
data[‘waitSeconds’] = int(data[‘waitSeconds’]) checks = []
checks.append(‘waitSeconds’ in data)
checks.append(type(data[‘waitSeconds’]) == int)
checks.append(‘message’ in data) if False in checks:
response = {
“statusCode”: 400,
“headers”: {“Access-Control-Allow-Origin”: “*”},
“body”: json.dumps({“Status”: “Success”, “Reason”: “Input validation failed”}, cls=DecimalEncoder)
}
else:
sm.start_execution(stateMachineArn=SM_ARN, input=json.dumps(data, cls=DecimalEncoder))
response = {
“statusCode”: 200,
“headers”: {“Access-Control-Allow-Origin”: “*”},
“body”: json.dumps({“Status”: “Success”}, cls=DecimalEncoder)
}
return responseclass DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, decimal.Decimal):
return int(obj)
return super(DecimalEncoder, self).default(obj) - Function name:
- Create a new API
- API Gateway → APIs → REST API → Build → New API
- API name:
petcuddleotronapi, Endpoint type:Regional - Click “Create API”
- Add a Resource to the API
- Resource name:
petcuddleotron - Do not enable “Proxy Resource” to maintain control over requests
- Enable CORS to allow the frontend (hosted on S3) to call this API
- Resource name:

- Add a Method to the
petcuddleotronresource- Method type:
POST - Integration type:
Lambda function- Lambda region:
us-east-1, Lambda function:api_lambda - Enable Lambda Proxy Integration to forward request details to Lambda
- Lambda region:
- Integration timeout:
29000ms - Click “Create Method”
- Method type:
- Deploy the API
- API name:
petcuddleotronapi→ Click “Deploy API” - Stage name:
Prod - Click “Deploy”
- API name:
- Record the Invoke URL of the deployed method for use in the next stage
STAGE 5: Set Up Frontend Using S3 Static Website

The frontend of the application will be hosted on an S3 bucket configured as a static website. This site will load an HTML page that collects input from the user and forwards it to the API.
- Create and configure an S3 bucket
- Choose a globally unique name for the bucket.
- Set the region to
us-east-1. - Disable “Block all public access” and confirm the bucket may be public.
- Adjust bucket permissions for public access:
- Update the bucket policy to allow public read access (replace the
Resourcewith your bucket’s ARN):
“Version”:”2012-10-17″,
“Statement”:[
{
“Sid”:”PublicRead”,
“Effect”:”Allow”,
“Principal”: “*”,
“Action”:[“s3:GetObject”],
“Resource”:[“REPLACEME_PET_CUDDLE_O_TRON_BUCKET_ARN/*”]
}
]
}- Make sure the
/*is included after the bucket ARN.
- Update the bucket policy to allow public read access (replace the
- Enable static website hosting for the bucket
- Navigate to Properties > Static website hosting > Edit > Enable > “Host a static website.”
- Use
index.htmlfor both the index and error pages. - Record the
Bucket Website EndpointURL for later use.
- Upload frontend files to the S3 bucket
- Download the website files from Adrian’s GitHub and update
serverless.js:whiskers.png→ image of Whiskers the cat.index.html→ main webpage, containing the form and inputs for user data.main.css→ styles for the HTML page.serverless.js→ JavaScript to handle button clicks and send data to API Gateway.- Replace the
API_ENDPOINTvariable with the API Gateway URL from Stage 4.
- Replace the
serverless.js: var API_ENDPOINT = ‘REPLACEME_API_GATEWAY_INVOKE_URL’;var errorDiv = document.getElementById(‘error-message’)
var successDiv = document.getElementById(‘success-message’)
var resultsDiv = document.getElementById(‘results-message’)function waitSecondsValue() { return document.getElementById(‘waitSeconds’).value }
function messageValue() { return document.getElementById(‘message’).value }
function emailValue() { return document.getElementById(’email’).value }function clearNotifications() {
errorDiv.textContent = ”;
resultsDiv.textContent = ”;
successDiv.textContent = ”;
}document.getElementById(’emailButton’).addEventListener(‘click’, function(e) { sendData(e, ’email’); });function sendData (e, pref) {
e.preventDefault()
clearNotifications()
fetch(API_ENDPOINT, {
headers:{
“Content-type”: “application/json”
},
method: ‘POST’,
body: JSON.stringify({
waitSeconds: waitSecondsValue(),
message: messageValue(),
email: emailValue()
}),
mode: ‘cors’
})
.then((resp) => resp.json())
.then(function(data) {
successDiv.textContent = ‘Submission complete. Check results below.’;
resultsDiv.textContent = JSON.stringify(data);
})
.catch(function(err) {
errorDiv.textContent = ‘Error occurred:\n’ + err.toString();
});
}; - After updating
serverless.js, upload all frontend files to the S3 bucket.
- Download the website files from Adrian’s GitHub and update
STAGE 6: Test the Serverless Application
- Open the S3 bucket’s public endpoint
- The index page should load in your browser.

- Provide input values:
- Wait time (in seconds), message, and recipient email.
- Click the “Email Minion” button to submit.
- Monitor the State Machine execution under the Logging tab.

- Verify delivery:
- Check the recipient email for the message. Delivery may take some time depending on email propagation.
- At this stage, the demo application is fully functional:
- Loads HTML and JavaScript from S3 (static hosting)
- Sends data to API Gateway via JavaScript
- Uses
api_lambdaas the backend - Executes a Step Functions State Machine that processes parameters
- Sends email notifications through the State Machine
- No servers were provisioned; the application runs entirely serverless.
STAGE 7 (Optional): Add SMS Notifications

- The original Pet-Cuddle-O-Tron could notify users by email and SMS, but the initial demo only used email. Here we extend it to send SMS as well.
- Important: Sending SMS via SNS is not included in the AWS Free Tier but is low-cost if used sparingly.
- Configure Simple Notification Service (SNS)
- Go to SNS > Mobile > Text Messaging (SMS) > Sandbox numbers.
- Sandbox mode requires whitelisting recipient phone numbers (verify via code sent by AWS).
- For this demo, we will not subscribe numbers to SNS topics or configure a dedicated origination number.
- Create a Lambda function for SMS notifications
- Function name:
sms_reminder_lambda, Runtime: Python 3.9 - Assign the same execution role used for
email_reminder_lambdaandapi_lambda. - Example code:
print(“Received event: ” + json.dumps(event))
sns.publish(
PhoneNumber=event[‘Input’][‘phone’],
Message=event[‘Input’][‘message’]
)
return ‘Success’ - Function name:
- Update the frontend to include phone input
- Add a phone field to
index.html. - Update
serverless.jsto includephonein the payload. Use placeholders"NO_EMAIL"or"NO_PHONE"if fields are left blank.
function phoneValue() { return document.getElementById(‘phone’).value || ‘NO_PHONE’; }document.getElementById(‘notifyButton’).addEventListener(‘click’, function(e) { sendData(e); });function sendData(e) {
e.preventDefault();
clearNotifications();
fetch(API_ENDPOINT, {
headers: { “Content-type”: “application/json” },
method: ‘POST’,
body: JSON.stringify({
waitSeconds: waitSecondsValue(),
message: messageValue(),
email: emailValue(),
phone: phoneValue()
}),
mode: ‘cors’
})
.then(resp => resp.json())
.then(data => {
successDiv.textContent = ‘Submission complete. Check results below.’;
resultsDiv.textContent = JSON.stringify(data);
})
.catch(err => {
errorDiv.textContent = ‘Error:\n’ + err.toString();
});
} - Add a phone field to
- Update
api_lambdato validate input- Ensure at least one contact method is provided:
- Update Step Functions State Machine
- Add a Choice state to handle:
EmailOnly,SMSOnly,EmailAndSMS. - Use two parallel branches for the
EmailAndSMSstate to invoke both Lambda functions concurrently.
- Add a Choice state to handle:

- Test the extended application

- Test all three cases:
- Email blank → only SMS is sent
- Phone blank → only email is sent
- Both provided → email and SMS are sent
The application now fully supports notifications via both email and SMS.
STAGE 8: Clean Up AWS Resources
- Delete all resources created during the demo (S3 buckets, Lambda functions, API Gateway, Step Functions, SNS) to avoid incurring charges.
LAB: Adding CDN to an S3 Static Website
PART 1: Create and Deploy a CloudFront Distribution with S3 Origin
- Deploy the provided CloudFormation stack
- This creates an S3 bucket with static website hosting.
- Why CloudFront? A barebones S3 static website has limitations:
- Slow performance for users far from the bucket region
- High load on the origin server (no caching)
- Only supports HTTP, not HTTPS
- Create a new CloudFront distribution and set the origin domain name to the S3 bucket.
- Selecting the S3 bucket directly makes it an S3 origin.
- If you enter the S3 static website URL instead, CloudFront treats it as a custom origin.

- Leave most other settings as default:
- Viewer protocol policy, cache policy, and access restrictions can remain default for demo purposes.
- CachingOptimized policy works for demonstration.

- Set the root object to
index.html- This object is returned when a client requests the root URL
/of the distribution.
- This object is returned when a client requests the root URL

- Deploy the distribution (deployment may take several minutes).
PART 2: Test the CloudFront Distribution
- Find the default CloudFront domain (ends with
cloudfront.net). - Open the distribution in a browser and refresh several times:
- Pages load quickly because content is cached in both the browser and the nearest edge location.
- Replace
merlin.jpgin the S3 bucket with a new image (e.g.,Whiskers.jpgrenamed tomerlin.jpg).- Verify changes by opening
index.htmldirectly from the S3 static website endpoint.
- Verify changes by opening
- Refresh the CloudFront URL in your browser:
- The original image may still appear due to caching at the edge location.
- You won’t see updates until the object’s cache TTL expires or you manually perform a cache invalidation.
- Options for frequently updated objects:
- Reduce cache TTL for the object
- Use versioned object names (
merlin_v1.jpg,merlin_v2.jpg) - Perform cache invalidation (incurs costs)

- CloudFront supports HTTPS, whereas the default S3 static website endpoint only supports HTTP.

PART 3: Add an Alternate CNAME and SSL
- You must have a custom domain purchased and hosted in Route 53.
- Optional: follow the demo video if you do not want to spend money.
- Edit the CloudFront distribution and add an alternate domain name (CNAME) for your custom domain.

3. Request an SSL certificate from AWS Certificate Manager (ACM):
- The certificate name must match your alternate domain name.

- Verify domain ownership via DNS validation (recommended) or email.

- If using Route 53, records can be created automatically.

4. After ACM issues the certificate, select it in the CloudFront distribution along with the alternate name, and save changes. Deployment may take several minutes.
5. Create a Route 53 record pointing your custom domain to the CloudFront distribution:
- Use an Alias A (or AAAA) record.
- Record name must match the alternate name used in CloudFront and ACM.

6. Test the custom domain in a browser; the site should load via CloudFront with HTTPS enabled.

PART 4: Secure the S3 Origin with OAC
- By default, the S3 bucket is public, allowing access via the static website endpoint.
- This means content can be bypassed and accessed without CloudFront.
- Configure Origin Access Control (OAC) in CloudFront:
- Change the origin access from Public to Origin Access Control settings.
- Use the default configuration so CloudFront signs all requests sent to the S3 origin.
- Update the S3 bucket policy to allow access only for the CloudFront distribution using the specified OAC.

- Test: opening the S3 static website directly should return 403 Forbidden.
- Content is now only accessible via CloudFront.
- Save and deploy the CloudFront distribution with the updated settings.
- Clean up AWS resources after the demo to avoid unnecessary charges.