AWS Cloud Resume Challenge 2
cloud

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.

January 6, 2026
1 min read
1 likes
AWSDevOpsCI/CD

My Cloud Resume Challenge Journey - Part 2: Building the Backend

← Back to Part 1

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 id as the partition key (simple string identifier)
  • Initial Data: Added an item with id: "1" and views: 1 as 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:

  1. Connects to the DynamoDB table
  2. Retrieves the current view count
  3. Increments it by 1
  4. Updates the database
  5. 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, UpdateItem for least privilege access
  • CORS Setup: Configured to allow requests from aungmoemt.site
  • Route Configuration: Set up /visitors endpoint 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 →

About this post

Category:cloud
Published:Jan 6, 2026
Reading time:1 minutes

Quick Actions