In this hands-on tutorial, I’ll walk you through how to design and build a simple serverless email marketing application from scratch. When we're done, our system will send emails on a schedule, grabbing HTML email templates and a list of contacts from an S3 bucket.
- Amazon Simple Email Service (SES)
- AWS Lambda
- Amazon Simple Storage Service (S3)
- Amazon EventBridge
- AWS Identity and Access Management (IAM)
👉 A place to store email templates and list of contacts
👉 A way to send emails
👉 A way to “merge” email templates with contacts and send them to the email service
👉 A way to trigger sending of emails on a schedule
- Sign in to the AWS Management Console and open the Amazon S3
- In the left navigation pane, choose Buckets
- Choose Create bucket
- For Bucket name, enter a name for your bucket, i'm going to name mine
jm-email-marketing
- Block Public Access as best practice, and leave all the defaults, scroll down then click on "Create bucket"
- Inside of our S3 bucket, we want to store an email template, an HTML template that we'll actually send to people and then we need a list of the contacts as well, the email addresses to send to.
- For the email template, copy and paste the code below and save it as
email_template.html
email_template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Decode the Programmer's Riddle - Tiny Tales Mail</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
color: #333;
}
.container {
max-width: 600px;
margin: auto;
background: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h1, h2 {
color: #007bff;
}
p {
font-size: 16px;
line-height: 1.5;
}
.challenge-details {
background-color: #eee;
border-left: 5px solid #007bff;
padding: 10px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>This Week's Tech Challenge: Decode the Programmer's Riddle</h1>
<p>Hello {{FirstName}}!</p>
<p>It's that time again—Tiny Tales Mail is back with a new challenge to stretch your tech muscles and ignite your problem-solving spark. This week, we're dialing up the difficulty with a puzzle that blends coding logic, pattern recognition, and a bit of creative thinking. Ready to dive in?</p>
<div class="challenge-details">
<h2>Challenge: The Programmer's Riddle</h2>
<p>Imagine you're exploring an ancient digital labyrinth. To escape, you need to input a code sequence into the central computer. You find a clue carved on the wall:</p>
<p><strong>8, 5, 4, 9, 1, 7, 6, 3, 2, 0</strong></p>
<p>But there's a catch: The sequence is not what it seems. To find the correct next number, you must uncover the hidden pattern. Here's a hint to guide you on your quest:</p>
<p><strong>Hint:</strong> The order is determined not by mathematics, but by a property inherent to each number.</p>
</div>
<h2>How to Participate:</h2>
<p>Convinced you've unraveled the labyrinth's secret? Email your solution and the reasoning behind it.</p>
<h2>Prizes and Recognition:</h2>
<p><strong>Grand Solver:</strong> The first to submit the correct answer will receive a $20 Amazon gift card and be featured in our next newsletter.</p>
<p><strong>Creative Thinkers:</strong> The next four insightful submissions will earn a spot in our "Hall of Fame" section, celebrating your cleverness and creativity.</p>
<p>This puzzle is designed to challenge and inspire. We encourage you to think outside the box and look forward to seeing the innovative ways you approach this problem.</p>
<p>Best of luck, and let the best minds prevail!</p>
<p>Eagerly awaiting your solutions,</p>
<p><strong>The Tiny Tales Mail Team</strong></p>
</div>
</body>
</html>
- The other thing that that we're going to place in the S3 bucket is a CSV file of our contacts, make sure the emails that you put are ones that you can actually validate, meaning you can receive emails and click on a link that AWS sends you if you want them to actually go through, you can easily create one from scratch as well but make sure to call it
contacts.csv
-
Go back to the S3 bucket, naviagate to the object tab, upload the
email_template.html
andcontacts.csv
make sure that the files are called that because later on we'll use them in the Lambda function not always the best idea but for this demo you will need these names to get it to work. -
Then click on "Upload"
✔️ we've got the S3 bucket
✔️ we've got the list of contacts to send to
✔️ we've also got an email template that we can send
Before you start using Amazon SES, you must complete the following tasks:
- Verify an identity for sending email, Verify an email address or domain to use when sending email.
- Send your first email, Send an email by using the Amazon SES console, the Amazon SES API, an AWS SDK, or the AWS Command-Line Interface.
- Back over to the console, navigate to SES, then click "Get started"
- You need to enter the email address that emails will come from, you do have to validate this, AWS will send you an email there's a link in that email that you need to click on, and make sure it's a valid email address
- Add your sending domain, a domain identity that matches your website or business name. Amazon SES needs to be linked to your domain and verified in order to send emails to your recipients through SES.
I'm using julienmuke.com
(if you need to purchase a domain you can use Amazon's Route 53 or other providers
like GoDaddy etc.)
- Amazon is going to send an email to your email address, you'll need to open it up, click on the link to verify that you actually own the email address and once you do you'll be able to send emails from that address.
- If you successfully did all of those steps then you also will have a Sandbox dashboard
Let's send an email to make sure everything is working
- On your Sandbox dashboard click on "Send test email"
- Select "Raw" which will let us test with HTML format
- For Scenario, we want to test the scenario where everything is successful, select "Successful delivery" (But you can also test different scenarios, like what happens if it bounces or what happens if there's a complaint and so on)
- Then paste in the HTML that we have from the email template on the Message box
- Click on "Send test email"
- Then you will get a green checkmark, everything is working
We need to validate the individual email addresses that we want to send to, that are stored in our contacts.csv file
- Back over the Sandbox dashboard, under verify email address click on "View all identities"
- Let's create a new identity, click on "Create identity"
- Identity type choose "Email address" and leave everything as default, then click "Create identity"
-
You will recieve an email from your inbox, open it and click the link to verify, you will get a success message, we can now start sending emails to that address
-
After you verify all the emails, you will see a list of verified emails
We need a way to merge the email template using Lambda to create and send personalized emails to SES with the contacts and then send them to SES to the email service.
- Back to the console, navigate to Lambda, then click on "Create function"
- We are going to "Author from scratch", for the function name i'm going to use
SendSESEmailToContacts
, for Runtime we are going to usepython3.12
leave everything as default then click "Create function"
- Scroll down to the code section, i've got some code already so you don't have to write this from scratch:
Lambda Function Python Code
import boto3
import csv
# Initialize the boto3 client
s3_client = boto3.client('s3')
ses_client = boto3.client('ses')
def lambda_handler(event, context):
# Specify the S3 bucket name
bucket_name = 'jm-email-marketing' # Replace with your bucket name
try:
# Retrieve the CSV file from S3
csv_file = s3_client.get_object(Bucket=bucket_name, Key='contacts.csv')
lines = csv_file['Body'].read().decode('utf-8').splitlines()
# Retrieve the HTML email template from S3
email_template = s3_client.get_object(Bucket=bucket_name, Key='email_template.html')
email_html = email_template['Body'].read().decode('utf-8')
# Parse the CSV file
contacts = csv.DictReader(lines)
for contact in contacts:
# Replace placeholders in the email template with contact information
personalized_email = email_html.replace('{{FirstName}}', contact['FirstName'])
# Send the email using SES
response = ses_client.send_email(
Source='you@yourdomainname.com', # Replace with your verified "From" address
Destination={'ToAddresses': [contact['Email']]},
Message={
'Subject': {'Data': 'Your Weekly Tiny Tales Mail!', 'Charset': 'UTF-8'},
'Body': {'Html': {'Data': personalized_email, 'Charset': 'UTF-8'}}
}
)
print(f"Email sent to {contact['Email']}: Response {response}")
except Exception as e:
print(f"An error occurred: {e}")
- Grab this code then come back to the Lambda function and just replace everything
- Make sure to replace your
bucket_name
email_template
contact_csv_file
andyou@yourdomainname.com
with your information if you named them differently with mine
- When copy and paste the Python and update it with your information, click on "Deploy"
- Set up a new test event, click on the Arrow to the right of test, then configure test event, create a new event
- We are going to name it
TestSendEmail
- we can use a really simple generic JSON test event, copy and replace it in the event, then click "Save"
{
"comment": "Generic test event for scheduled Lambda execution. The function does not use this event data.",
"test": true
}
N.B: If you click on Test back over the Lambda source code, you will get an "ACCESS DENIED" message, it's a pretty common thing to come across and this is where the permissions and the execution role come into play, let's do that on the next step.
- Back to the Lambda, go to configutation tab, under permissions, click "Role name"
- Scrolling down to permissions policies if we expand the execution Url, we just have some default permissions in to basically write to class watch logs there's nothing specific to S3 or SES, we need to add that manually to do that I'm going to go create a new Creating a new policy with permissions for S3 and SES policy with those permissions in it and then we'll attach it to this role when we're done
- Click "Policies" on left menu, click "Create policy"
- Toggle over to the JSON editor, copy IAM policy code below and replace it to the policy editor, make sure to update your bucket name, if you named yours differently, mine is
jm-email-marketing
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::jm-email-marketing/*"
},
{
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
}
]
}
N.B: mAKE sure you leave the /*
at the end your bucket name but just update your bucket name. This basically says we're allowing get object so getting things out of the S3 bucket and then also we want to allow sending emails and sending raw emails which is the HTML code through SCS.
- When you click on "Next", name your policy
LambdaS3SESPolicy
, then click "Create policy"
- Now the policy exists we just need to go back to the execution Role and attach it
- Select the one we created
LambdaS3SESPolicy
✔️ The Lambda function should have the permissions it needs to talk to S3 as well as SCS
- Now let's run the test event for the Lambda function again, back to the Lambda function, source code section, click on "test" button
✔️ this is looking better, I'm not getting an access denied, as you can see the email is sent to the two different email addresses.
Let's check my inbox:
✔️ It's filled in my first name that was the one placeholder that we had everything else is the same from the template, it worked perfectly
We need a way to trigger sending those emails, in other words we need to trigger the Lambda function. There's lots of ways that you can do that with Lambda you could trigger it when you upload a new email template to the S3 bucket, you could trigger from the console from the CLI, we're actually going to use EventBridge which used to be called cloudwatch events but with this you can set up a schedule to trigger it say weekly or daily or whenever you want to send your emails.
- Create a new schedule using EventBridge:
A. To create an EventBridge, navigate to the console search for EventBridge, then select "EventBridge schedule"
B. For schedule name, i will name it SendWeeklyEmail
C. And then scrolling down you choose your schedule pattern so you can do this just one time or you can
set up recurring schedule like every week or every month:
- Based on the name of mine I intend to do this weekly but just for testing purposes let's do a one-time schedule,
- I'll select today's date and then the time,
- Select the time zone it's default to the local one which is great
- And then the flexible time window this basically gives it some leeway basically a window in which it can run I'm going to turn that off and say I want it to run at exactly this time and then click "Next"
D. The target there's lots of options to chose from but basically we want to invoke our Lambda function
Tip: If you don’t see your function, make sure you’re in the same region where you created the function
E. Select the Lambda function, mine was SendSESEmailToContacts
, then click "Next"
F. The schedule should be enabled by default, if you want to take any action after the schedule completes like deleting that will delete your schedule I'm going to select "none" G. You can just leave everything the default, then click "Next",
H. Review all, the click "Create schedule" G. In few minutes it should trigger the Lambda function, the Lambda function should send us an email to your inbox right on schedule that you created.
Let's just go take a quick look at what happened and how you can debug if you didn't get the email like you thought you should:
- Back in the EventBridge, clikc the target tab then click the target link
- That'll open up a new tab and then in Lambda if you come into the monitor tab this is where you can see what's going on with this particular function, then click "cloudwatch logs"
✔️ If you check our log streams, if we look at the latest one, I've got success messages, I sent two different emails
- Incorporate DynamoDB to hold names and email addresses (instead of a CSV file)
- Build a UI you can use to trigger sending the emails (use API Gateway)
All services used are eligible for the AWS Free Tier. However, charges will incur at some point so it's recommended that you shut down resources after completing this tutorial.