This exercise builds Milestone 1 of the capstone: the AWS serverless backend serving IPL scorecard data from two active-active regions. You will deploy Lambda, API Gateway HTTP API, DynamoDB Global Tables, CloudFront with WAF, and Route 53 latency routing as a complete integrated stack using AWS SAM. Each step builds on the previous, and the milestone validation at the end confirms all components work together before you proceed to Milestone 2.
The architecture you build in this exercise is the same architecture deployed to serve real-time cricket statistics at scale. Lambda handles API requests, DynamoDB Global Tables provides sub-second cross-region data replication, CloudFront caches scorecards at the edge reducing Lambda invocations by 80%, and Route 53 latency routing automatically directs Asia-Pacific fans to the closest healthy region. The GitHub Actions WIF deployment pipeline ensures no GCP or AWS credentials are stored in the repository.
Work through each step in sequence. Each step includes a reference code block and a validation command. Run the validation command after completing each step before proceeding to the next. If a validation fails, debug the current step before advancing: errors introduced in an early step compound into confusing multi-layer failures in later steps. The milestone validation at the end of the exercise confirms all components work together end-to-end.
Scenario
You are the lead architect for the IPL Scorecard Platform’s AWS infrastructure. The platform must be live before the IPL season starts in 6 weeks. The engineering team has agreed on the architecture from the project brief. Your role is to implement Milestone 1: the AWS serverless backend with active-active multi-region deployment, CloudFront edge caching with WAF, and Route 53 failover routing.
The team has already provisioned the DynamoDB table schema and seeded test data for the IPL 2024 Final. Your implementation starts with the Lambda function and works outward through API Gateway, CloudFront, and Route 53. The GitHub Actions pipeline will deploy changes to both Mumbai and Singapore simultaneously on every merge to main. You must configure the WIF authentication for the deployment pipeline as the final step.
Step 1 — SAM Template and Lambda Deployment
Write the SAM template defining the Lambda function with the IPL scorecard handler, the API Gateway HTTP API with JWT authorisation, and the DynamoDB table name as an environment variable. Deploy to ap-south-1 first and validate that the API responds with match data before deploying the Singapore replica. Use the sam deploy command with an explicit stack name that encodes the region to distinguish the two regional stacks.
# sam-template.yaml — Milestone 1 SAM template
sam_template = '''
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Parameters:
TableName:
Type: String
Default: ipl-scores-global
Environment:
Type: String
AllowedValues: [prod, staging]
Default: prod
Globals:
Function:
Runtime: python3.11
Tracing: Active
Environment:
Variables:
TABLE_NAME: !Ref TableName
ENVIRONMENT: !Ref Environment
Tags:
team: scorecard-api
environment: !Ref Environment
cost-centre: ipl-platform
Resources:
IplScorecardFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub ipl-scorecard-${Environment}
CodeUri: src/
Handler: scorecard.handler
MemorySize: 512
Timeout: 10
Policies:
- DynamoDBReadPolicy:
TableName: !Ref TableName
Events:
GetScoreById:
Type: HttpApi
Properties:
Path: /api/scores/{matchId}
Method: GET
ApiId: !Ref IplHttpApi
IplHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
StageName: prod
CorsConfiguration:
AllowOrigins: ["*"]
AllowMethods: [GET, OPTIONS]
Outputs:
ApiEndpoint:
Value: !Sub https://${IplHttpApi}.execute-api.${AWS::Region}.amazonaws.com/prod
Export:
Name: !Sub IplApiEndpoint-${AWS::Region}
LambdaArn:
Value: !GetAtt IplScorecardFunction.Arn
Export:
Name: !Sub IplLambdaArn-${AWS::Region}
'''
print('Deploy to Mumbai: sam deploy --stack-name ipl-scorecard-ap-south-1 --region ap-south-1')
print('Deploy to Singapore: sam deploy --stack-name ipl-scorecard-ap-southeast-1 --region ap-southeast-1')
print('Validate: curl https://<api-id>.execute-api.ap-south-1.amazonaws.com/prod/api/scores/IPL2024FINAL')
print('Expected: [{"matchId": "IPL2024FINAL", "battingTeam": "KolkataKnightRiders", ...}]')Step 2 — DynamoDB Global Tables
Enable DynamoDB Global Tables to replicate the ipl-scores-global table between ap-south-1 and ap-southeast-1. The table must have DynamoDB Streams enabled with NEW_AND_OLD_IMAGES before adding the Singapore replica. Verify replication is active by writing a test item in Mumbai and confirming it appears in Singapore within 5 seconds. Record the replication latency for the project brief’s RPO calculation.
import boto3, json, time
ddb_mum = boto3.client('dynamodb', region_name='ap-south-1')
ddb_sin = boto3.client('dynamodb', region_name='ap-southeast-1')
table_r = boto3.resource('dynamodb', region_name='ap-south-1').Table('ipl-scores-global')
# Enable streams if not already enabled (required for Global Tables).
ddb_mum.update_table(
TableName='ipl-scores-global',
StreamSpecification={'StreamEnabled': True, 'StreamViewType': 'NEW_AND_OLD_IMAGES'},
)
print('Streams enabled. Adding Singapore replica...')
# Add Singapore as a Global Tables replica.
ddb_mum.update_table(
TableName='ipl-scores-global',
ReplicaUpdates=[{'Create': {'RegionName': 'ap-southeast-1'}}],
)
# Wait for replication to be active (typically 2-5 minutes).
print('Waiting for replica to become ACTIVE (this takes 2-5 minutes)...')
while True:
desc = ddb_mum.describe_table(TableName='ipl-scores-global')['Table']
replicas = desc.get('Replicas', [])
sg_replica = next((r for r in replicas if r['RegionName']=='ap-southeast-1'), None)
if sg_replica and sg_replica['ReplicaStatus'] == 'ACTIVE':
print('Singapore replica is ACTIVE.')
break
time.sleep(30)
# Measure replication latency.
test_item = {'matchId': 'REPLICATION-TEST', 'inningsNum': 0, 'ts': int(time.time())}
table_r.put_item(Item=test_item)
write_time = time.time()
table_sg = boto3.resource('dynamodb', region_name='ap-southeast-1').Table('ipl-scores-global')
for _ in range(20):
result = table_sg.get_item(Key={'matchId': 'REPLICATION-TEST', 'inningsNum': 0})
if 'Item' in result:
print(f'Replication latency: {time.time()-write_time:.2f}s')
table_r.delete_item(Key={'matchId': 'REPLICATION-TEST', 'inningsNum': 0})
break
time.sleep(0.5)Step 3 — CloudFront Distribution and WAF
Deploy a CloudFront distribution with two cache behaviours: /api/scores/* with a 3-second TTL (live scores change every delivery), and /static/* with an 86,400-second TTL. Attach a WAF WebACL with the AWS Managed Rules Common Rule Set and a rate limit rule blocking IPs that exceed 1,000 requests per 5 minutes. Configure the CloudFront distribution to use both API Gateway endpoints as origin group members, with Mumbai as primary and Singapore as failover.
import boto3
cf = boto3.client('cloudfront')
wafv2 = boto3.client('wafv2', region_name='us-east-1') # WAF for CF must be us-east-1
# Create WAF WebACL with AWS Managed Rules + rate limit.
web_acl = wafv2.create_web_acl(
Name='ipl-scorecard-waf',
Scope='CLOUDFRONT',
DefaultAction={'Allow': {}},
Rules=[
{
'Name': 'AWSManagedRulesCommonRuleSet', 'Priority': 1,
'OverrideAction': {'None': {}},
'Statement': {'ManagedRuleGroupStatement': {
'VendorName': 'AWS', 'Name': 'AWSManagedRulesCommonRuleSet'}},
'VisibilityConfig': {'SampledRequestsEnabled': True,
'CloudWatchMetricsEnabled': True,
'MetricName': 'CommonRuleSet'},
},
{
'Name': 'IPLRateLimitRule', 'Priority': 2,
'Action': {'Block': {}},
'Statement': {'RateBasedStatement': {
'Limit': 1000, 'AggregateKeyType': 'IP',
'EvaluationWindowSec': 300}},
'VisibilityConfig': {'SampledRequestsEnabled': True,
'CloudWatchMetricsEnabled': True,
'MetricName': 'RateLimit'},
},
],
VisibilityConfig={'SampledRequestsEnabled': True,
'CloudWatchMetricsEnabled': True,
'MetricName': 'IPLScorecardWAF'},
)
waf_arn = web_acl['Summary']['ARN']
print(f'WAF WebACL: {waf_arn}')
# CloudFront distribution with origin group (Mumbai primary, Singapore failover).
# Full distribution config omitted for brevity; key settings shown:
print('CloudFront distribution config key settings:')
print(' Origins: ap-south-1 API GW (primary) + ap-southeast-1 API GW (failover)')
print(' Default cache behaviour: /api/scores/* TTL=3s, WAF attached')
print(' Cache behaviour: /static/* TTL=86400s')
print(' ViewerCertificate: ACM certificate for api.ipl.srihayavadhana.in')
print(f' WebACLId: {waf_arn}')Step 4 — Route 53 Failover Configuration
Configure Route 53 latency routing records for api.ipl.srihayavadhana.in pointing to the CloudFront distribution (which itself handles origin failover between Mumbai and Singapore). Create calculated health checks for each AWS region using the composite alarm pattern from Module 5: a region is considered unhealthy only when the API error rate AND the DynamoDB replication lag BOTH exceed thresholds simultaneously, preventing false positive failovers.
Step 5 — GitHub Actions WIF Deployment Pipeline
Configure the GitHub Actions deployment pipeline using Workload Identity Federation to deploy to both AWS regions without storing credentials. Create a WIF OIDC provider in AWS IAM for GitHub Actions, configure the trust policy on the deployment role, and write the GitHub Actions workflow file. The deployment should run sam deploy for both regions in parallel to minimise deployment time, with the Singapore deployment depending on the Mumbai deployment’s success.
# GitHub Actions workflow: deploy to both AWS regions using WIF.
gh_workflow = '''
name: Deploy IPL Scorecard API
on:
push:
branches: [main]
jobs:
deploy-mumbai:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111111:role/ipl-github-deploy-role
aws-region: ap-south-1
role-session-name: GithubActionsMumbai
- uses: aws-actions/setup-sam@v2
- run: sam deploy
--stack-name ipl-scorecard-ap-south-1
--region ap-south-1
--no-confirm-changeset
--no-fail-on-empty-changeset
deploy-singapore:
needs: deploy-mumbai # deploy Singapore only after Mumbai succeeds
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111111:role/ipl-github-deploy-role
aws-region: ap-southeast-1
role-session-name: GithubActionsSingapore
- uses: aws-actions/setup-sam@v2
- run: sam deploy
--stack-name ipl-scorecard-ap-southeast-1
--region ap-southeast-1
--no-confirm-changeset
--no-fail-on-empty-changeset
'''
print('GitHub Actions workflow configured for WIF deployment to both regions.')Milestone 1 Validation
- GET https://api.ipl.srihayavadhana.in/api/scores/IPL2024FINAL returns HTTP 200 with innings data and X-Served-By: aws-ap-south-1 header.
- DynamoDB console shows ipl-scores-global table with an ap-southeast-1 ACTIVE replica and replication latency under 2 seconds for test writes.
- CloudFront console shows the distribution with WAF attached, cache hit rate above 70% after 10 minutes of synthetic traffic, and origin failover configured.
- Route 53 shows two latency routing records with health checks both in Healthy state.
- GitHub Actions workflow run shows both deploy-mumbai and deploy-singapore jobs completing successfully with aws-actions/configure-aws-credentials using OIDC (no stored credentials).
- Failover drill: stopping the Mumbai API Gateway returns Singapore responses within 90 seconds as confirmed by repeated dig queries.
Warning: The SAM deploy for Singapore must use the same table name (ipl-scores-global) that was used for the Mumbai stack. If the Singapore stack creates a new table with a different name, it will not be part of the Global Table and data will not replicate. The DynamoDB Global Table is a single resource spanning multiple regions; do not create a separate table per region — add the Singapore region as a replica of the existing Mumbai table.