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

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
    

### Stage 1: Simple Email Service (SES)

The first step of the project was setting up two verified identities on SES. I created these with my email addresses and verified both.

```plaintext
resource "aws_ses_email_identity" "sender" {
  email = "morolaanney@gmail.com"
}

resource "aws_ses_email_identity" "receiver" {
  email = "rolakeanney@outlook.com"
}
```

### **Stage 2: Email Reminder Lambda Function**

An IAM role and lambda function were created in this stage.

```plaintext
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"
      }
    ]
  })
}
```

```plaintext
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

```plaintext
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!'
  
```

### **Stage 3: State Machine**

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.

```plaintext
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"
      }
    ]
  })
}
```

```plaintext
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
}
```

### **Stage 4: API Gateway and Supporting Lambda Function**

```plaintext
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
  ]
}
```

```plaintext
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"  = "'*'"
  }
}
```

### **Stage 5: Frontend Application**

At this stage of the project, an S3 bucket was created with static website hosting to host the application frontend.

```plaintext
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:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1716319649783/351b0688-0703-4532-b38c-9fd844b4bb81.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1716319663392/d1bc14fd-afd9-4456-ac90-de4674769352.png align="center")
