Serverless App - Pet-Cuddle-O-Tron - Terraform Version

Search for a command to run...

No comments yet. Be the first to comment.
This project from Ifeanyi Otuonye focuses on creating an alerting system to deliver scores from sports games. I decided to go with NBA games since I enjoy watching Jokic in Denver. The services used include AWS SNS, AWS Lambda, AWS Event Bridge, IAM ...
Amazon Lex is a fully managed artificial intelligence (AI) service with advanced natural language models to design, build, test, and deploy AI chatbots and voice bots in applications. In this post, I’ll walk you through my experience implementing the...
The growth of customer bases has led to businesses building robust and highly available applications by deploying across multiple regions. By pursuing global infrastructure, organisations can reduce latency, enhance availability, and maintain uptime ...

Grafana is a multi-platform open-source analytics and interactive visualization web application. It can be used to create graphs, charts, and alerts and allows connections to multiple data sources. In this post, I’ll discuss how to connect to AWS Clo...

This is the IaC implementation of the AWS project by Adrian Cantrill that involves implementing a simple serverless application using S3, API Gateway, Lambda, Step Functions, SNS, and SES. The application loads from an S3 bucket, communicates with lambda and step functions, and enables the configuration of reminders for pet cuddles to be sent via email.
There are six basic stages involved:
Configure Simple Email Service (SES)
Add an email lambda function to use SES to send emails for the serverless application
Implement and configure the state machine, the core of the application
Implement the API Gateway, API, and supporting lambda function
Implement the static frontend application and test its functionality
The first step of the project was setting up two verified identities on SES. I created these with my email addresses and verified both.
resource "aws_ses_email_identity" "sender" {
email = "morolaanney@gmail.com"
}
resource "aws_ses_email_identity" "receiver" {
email = "rolakeanney@outlook.com"
}
An IAM role and lambda function were created in this stage.
resource "aws_iam_role" "lambda_role" {
name = "lambda_role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Service" : "lambda.amazonaws.com"
},
"Action" : "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy" "lambda_policy" {
name = "lambda_policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:*:*"
Effect = "Allow"
},
{
Action = [
"ses:*",
"sns:*",
"states:*"
]
Resource = "*"
Effect = "Allow"
}
]
})
}
data "archive_file" "api_lambda" {
type = "zip"
source_file = "lambda-python-code/api_lambda.py"
output_path = "api_lambda.zip"
}
resource "aws_lambda_function" "api_lambda" {
function_name = "api_lambda"
role = aws_iam_role.lambda_role.arn
filename = "api_lambda.zip"
runtime = "python3.9"
handler = "api_lambda.lambda_handler"
depends_on = [
aws_iam_role.lambda_role
]
}
Python code for the Email Reminder Lambda function
import boto3, os, json
FROM_EMAIL_ADDRESS = 'morolaanney@gmail.com'
ses = boto3.client('ses')
def lambda_handler(event, context):
# Print event data to logs ..
print("Received event: " + json.dumps(event))
# Publish message directly to email, provided by EmailOnly or EmailPar TASK
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!'
Another IAM role was created for the state machine to enable interaction with other AWS services. Following that was the creation of the state machine, which was configured using the email_reminder_lambda_arn and the previously created IAM role.
resource "aws_iam_role" "state_machine_role" {
name = "state_machine_role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Service" : "states.amazonaws.com"
},
"Action" : "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy" "invoke_lambda_and_send_SNS" {
name = "invoke_lambda_and_send_SNS"
role = aws_iam_role.state_machine_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
"Action" : [
"lambda:InvokeFunction",
"sns:*"
],
"Resource" : "*",
"Effect" : "Allow"
}
]
})
}
resource "aws_iam_role_policy" "cloudwatch" {
name = "cloudwatch"
role = aws_iam_role.state_machine_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
"Action" : [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogDelivery",
"logs:GetLogDelivery",
"logs:UpdateLogDelivery",
"logs:DeleteLogDelivery",
"logs:ListLogDeliveries",
"logs:PutResourcePolicy",
"logs:DescribeResourcePolicies",
"logs:DescribeLogGroups"
],
"Resource" : "*",
"Effect" : "Allow"
}
]
})
}
resource "aws_sfn_state_machine" "sfn_state_machine" {
name = "PetCuddleOTron"
role_arn = aws_iam_role.state_machine_role.arn
definition = <<EOF
{
"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": "${aws_lambda_function.email_reminder.arn}",
"Payload": {
"Input.$": "$"
}
},
"Next": "NextState"
},
"NextState": {
"Type": "Pass",
"End": true
}
}
}
EOF
}
data "archive_file" "api_lambda" {
type = "zip"
source_file = "lambda-python-code/api_lambda.py"
output_path = "api_lambda.zip"
}
resource "aws_lambda_function" "api_lambda" {
function_name = "api_lambda"
role = aws_iam_role.lambda_role.arn
filename = "api_lambda.zip"
runtime = "python3.9"
handler = "api_lambda.lambda_handler"
depends_on = [
aws_iam_role.lambda_role
]
}
resource "aws_api_gateway_rest_api" "petcuddleotron" {
name = "petcuddleotron"
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_api_gateway_deployment" "petcuddleotron" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
# triggers = {
# redeployment = sha1(jsonencode(aws_api_gateway_rest_api.petcuddleotron.body))
# }
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_stage" "prod" {
deployment_id = aws_api_gateway_deployment.petcuddleotron.id
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
stage_name = "prod"
}
resource "aws_api_gateway_resource" "petcuddleotron" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
parent_id = aws_api_gateway_rest_api.petcuddleotron.root_resource_id
path_part = "petcuddleotron"
}
resource "aws_api_gateway_method" "petcuddleotron_method" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
resource_id = aws_api_gateway_resource.petcuddleotron.id
http_method = "POST"
authorization = "NONE"
}
resource "aws_api_gateway_method_response" "method_response" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
resource_id = aws_api_gateway_resource.petcuddleotron.id
http_method = aws_api_gateway_method.petcuddleotron_method.http_method
status_code = "200"
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = true
"method.response.header.Access-Control-Allow-Methods" = true
"method.response.header.Access-Control-Allow-Origin" = true
}
}
resource "aws_api_gateway_integration" "integration" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
resource_id = aws_api_gateway_resource.petcuddleotron.id
http_method = aws_api_gateway_method.petcuddleotron_method.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api_lambda.invoke_arn
}
resource "aws_api_gateway_integration_response" "integration_response" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
resource_id = aws_api_gateway_resource.petcuddleotron.id
http_method = aws_api_gateway_method.petcuddleotron_method.http_method
status_code = "200"
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
"method.response.header.Access-Control-Allow-Methods" = "'POST'"
"method.response.header.Access-Control-Allow-Origin" = "'*'"
}
}
# OPTIONS HTTP method.
resource "aws_api_gateway_method" "options" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
resource_id = aws_api_gateway_resource.petcuddleotron.id
http_method = "OPTIONS"
authorization = "NONE"
api_key_required = false
}
# OPTIONS method response.
resource "aws_api_gateway_method_response" "options" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
resource_id = aws_api_gateway_resource.petcuddleotron.id
http_method = aws_api_gateway_method.options.http_method
status_code = "200"
response_models = {
"application/json" = "Empty"
}
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = true
"method.response.header.Access-Control-Allow-Methods" = true
"method.response.header.Access-Control-Allow-Origin" = true
}
}
# OPTIONS integration.
resource "aws_api_gateway_integration" "options" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
resource_id = aws_api_gateway_resource.petcuddleotron.id
http_method = "OPTIONS"
type = "MOCK"
passthrough_behavior = "WHEN_NO_MATCH"
request_templates = {
"application/json" : "{\"statusCode\": 200}"
}
}
# OPTIONS integration response.
resource "aws_api_gateway_integration_response" "options" {
rest_api_id = aws_api_gateway_rest_api.petcuddleotron.id
resource_id = aws_api_gateway_resource.petcuddleotron.id
http_method = aws_api_gateway_integration.options.http_method
status_code = "200"
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
"method.response.header.Access-Control-Allow-Methods" = "'POST,OPTIONS'"
"method.response.header.Access-Control-Allow-Origin" = "'*'"
}
}
At this stage of the project, an S3 bucket was created with static website hosting to host the application frontend.
resource "aws_s3_bucket" "pet-cuddle-tron-airat" {
bucket = "pet-cuddle-tron-airat"
}
resource "aws_s3_bucket_public_access_block" "enable_public_access" {
bucket = aws_s3_bucket.pet-cuddle-tron-airat.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "bucket_policy" {
bucket = aws_s3_bucket.pet-cuddle-tron-airat.id
policy = data.aws_iam_policy_document.bucket_policy.json
}
data "aws_iam_policy_document" "bucket_policy" {
statement {
sid = "PublicRead"
effect = "Allow"
principals {
type = "*"
identifiers = ["*"]
}
actions = [
"s3:GetObject",
]
resources = [
"${aws_s3_bucket.pet-cuddle-tron-airat.arn}/*",
]
}
}
resource "aws_s3_bucket_website_configuration" "example" {
bucket = aws_s3_bucket.pet-cuddle-tron-airat.id
index_document {
suffix = "index.html"
}
error_document {
key = "index.html"
}
}
resource "aws_s3_object" "object1" {
bucket = aws_s3_bucket.pet-cuddle-tron-airat.id
key = "index.html"
source = "files/index.html"
content_type = "text/html"
}
resource "aws_s3_object" "object2" {
bucket = aws_s3_bucket.pet-cuddle-tron-airat.id
key = "main.css"
source = "files/main.css"
}
resource "aws_s3_object" "object3" {
bucket = aws_s3_bucket.pet-cuddle-tron-airat.id
key = "serverless.js"
source = "files/serverless.js"
}
resource "aws_s3_object" "object4" {
bucket = aws_s3_bucket.pet-cuddle-tron-airat.id
key = "cat.jpeg"
source = "files/whiskers.png"
}
The final web page is shown below alongside the email:

