
AWS Cloud Resume Challenge 2
Welcome to my Cloud Resume Challenge experience! This is the second part of a three-part series where I'll share everything I learned while building a serverless resume website on AWS. If you're thinking about taking on this challenge, I hope my journey helps you navigate through the process.
My Cloud Resume Challenge Journey - Part 2: Building the Backend
7. Database
First, I set up a DynamoDB table to store the visitor count:
- Table Creation: Created a DynamoDB table named
resume-visitors - Partition Key: Used
idas the partition key (simple string identifier) - Initial Data: Added an item with
id: "1"andviews: 1as the starting point
8. Lambda Function
This is where the magic happens! I wrote a Python Lambda function to handle visitor counting:
import boto3 def get_table(): dynamodb = boto3.resource("dynamodb", region_name="ap-southeast-1") return dynamodb.Table("resume-visitors") def handler(event, context): table = get_table() response = table.get_item(Key={"id": "1"}) views = response["Item"]["views"] + 1 table.put_item( Item={ "id": "1", "views": views } ) return views
The function is straightforward:
- Connects to the DynamoDB table
- Retrieves the current view count
- Increments it by 1
- Updates the database
- Returns the new count
9. Tests
Testing is crucial for reliable code. I wrote tests using the moto library to mock AWS services:
import boto3 import pytest from moto import mock_aws from func import handler @mock_aws def test_handler_increments_views(): # Arrange: create mock DynamoDB table dynamodb = boto3.resource("dynamodb", region_name="ap-southeast-1") table = dynamodb.create_table( TableName="resume-visitors", KeySchema=[ {"AttributeName": "id", "KeyType": "HASH"} ], AttributeDefinitions=[ {"AttributeName": "id", "AttributeType": "S"} ], BillingMode="PAY_PER_REQUEST", ) # Seed initial item table.put_item( Item={ "id": "1", "views": 5 } ) # Act result = handler({}, {}) # Assert assert result == 6 response = table.get_item(Key={"id": "1"}) assert response["Item"]["views"] == 6
10. API
To make the Lambda function accessible from the web, I integrated it with API Gateway:
- Function Setup: Deployed the Python Lambda function as
cloudresume-view-counter - API Gateway Integration: Connected the function to API Gateway with proper CORS configuration
- IAM Configuration: Created specific IAM roles and policies for security
- Resource-Based Policy: Added permission for API Gateway to invoke the Lambda function
The main components included:
- DynamoDB Permissions:
PutItem,GetItem,UpdateItemfor least privilege access - CORS Setup: Configured to allow requests from
aungmoemt.site - Route Configuration: Set up
/visitorsendpoint for GET requests
11. Infrastructure as Code
Instead of clicking through the AWS console, I used Terraform to define all infrastructure. Here are the key components:
Lambda Function Configuration:
resource "aws_lambda_function" "myfunc" { filename = data.archive_file.zip_the_python_code.output_path source_code_hash = data.archive_file.zip_the_python_code.output_base64sha256 function_name = "cloudresume-visitor-counter" role = aws_iam_role.iam_role_for_lambda.arn handler = "func.handler" runtime = "python3.8" } data "archive_file" "zip_the_python_code" { type = "zip" source_file = "${path.module}/lambda/func.py" output_path = "${path.module}/lambda/func.zip" }
IAM Policies for Least Privilege:
data "aws_iam_policy_document" "access_visitors_table_policy_doc" { statement { effect = "Allow" actions = [ "dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:UpdateItem" ] resources = [aws_dynamodb_table.resume-visitors.arn] } } resource "aws_iam_policy" "access_visitors_table_policy" { name = "AccessVisitorsTablePolicy" description = "Allows access to the resume-visitors DynamoDB table" policy = data.aws_iam_policy_document.access_visitors_table_policy_doc.json }
Lambda Permission for API Gateway:
resource "aws_lambda_permission" "apigw" { statement_id = "AllowHttpApiInvoke" action = "lambda:InvokeFunction" function_name = aws_lambda_function.myfunc.function_name principal = "apigateway.amazonaws.com" source_arn = "${aws_apigatewayv2_api.http_api.execution_arn}/*/*/visitors" }
CloudWatch Logging:
resource "aws_cloudwatch_log_group" "lambda_log_group" { name = "/aws/lambda/${aws_lambda_function.myfunc.function_name}" retention_in_days = 7 # optional }
DynamoDB Table:
resource "aws_dynamodb_table" "resume-visitors" { name = "resume-visitors" billing_mode = "PAY_PER_REQUEST" hash_key = "id" attribute { name = "id" type = "S" } } resource "aws_dynamodb_table_item" "visitor_count_item" { table_name = aws_dynamodb_table.resume-visitors.name hash_key = "id" item = jsonencode({ id = { S = "1" } views = { N = "0" } }) }
API Gateway with CORS:
resource "aws_apigatewayv2_api" "http_api" { name = "cloudresume-http-api" protocol_type = "HTTP" cors_configuration { allow_origins = ["https://aungmoemt.site"] allow_methods = ["GET"] allow_headers = [] max_age = 300 # Cache preflight response for 5 minutes } } resource "aws_apigatewayv2_integration" "lambda" { api_id = aws_apigatewayv2_api.http_api.id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.myfunc.invoke_arn integration_method = "POST" payload_format_version = "2.0" } resource "aws_apigatewayv2_route" "counter" { api_id = aws_apigatewayv2_api.http_api.id route_key = "GET /visitors" target = "integrations/${aws_apigatewayv2_integration.lambda.id}" } output "api_url" { value = "${aws_apigatewayv2_api.http_api.api_endpoint}/visitors" }
12. Frontend Integration
Finally, I updated the JavaScript to fetch the visitor count from the API:
The API Gateway outputs the endpoint URL, which I used in my JavaScript to make the API calls and display the real-time visitor count on the website.
This is Part 2 of 3 in my Cloud Resume Challenge series. ← Back to Part 1 | Continue to Part 3 →