AWSTemplateFormatVersion: '2010-09-09'
Description: >
Personal Lifecycle Tracker - Serverless AWS Deployment (AWS Lambda + S3 + CloudFront + SNS On-Call Responder).
Zero EC2 instances, zero server maintenance, 100% Free-Tier eligible ($0.00/month idle cost).
Parameters:
Environment:
Type: String
Default: dev
Description: Deployment environment identifier (dev, staging, production).
AllowedValues:
- dev
- staging
- production
ProjectName:
Type: String
Default: personal-lifecycle-tracker
Description: Name identifier for the project.
DatabaseUrl:
Type: String
NoEcho: true
Default: "postgresql://postgres:postgres@localhost:5432/lifecycle"
Description: Neon or PostgreSQL serverless database connection URL.
SecretKey:
Type: String
NoEcho: true
Default: "dev-secret-key-super-secure-at-least-32-chars-long"
Description: Cryptographic secret key for signing JWT tokens.
Resources:
# ===========================================================================
# 1. LOGS & IAM FOR BACKEND LAMBDA
# ===========================================================================
BackendLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${ProjectName}-api-${Environment}"
RetentionInDays: 14
BackendLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${ProjectName}-lambda-backend-role-${Environment}"
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess
# ===========================================================================
# 2. FASTAPI BACKEND SERVERLESS LAMBDA
# ===========================================================================
BackendLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub "${ProjectName}-api-${Environment}"
Role: !GetAtt BackendLambdaRole.Arn
PackageType: Zip
Runtime: python3.11
Handler: index.handler
MemorySize: 512
Timeout: 60
Code:
ZipFile: |
def handler(event, context):
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": '{"status":"healthy","service":"personal-lifecycle-tracker-api","environment":"serverless"}'
}
Environment:
Variables:
APP_ENV: !Ref Environment
DATABASE_URL: !Ref DatabaseUrl
SECRET_KEY: !Ref SecretKey
PYTHONUNBUFFERED: "1"
BackendFunctionUrl:
Type: AWS::Lambda::Url
Properties:
TargetFunctionArn: !GetAtt BackendLambdaFunction.Arn
AuthType: NONE
Cors:
AllowOrigins:
- "*"
AllowMethods:
- "*"
AllowHeaders:
- "*"
MaxAge: 86400
BackendUrlPublicPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt BackendLambdaFunction.Arn
Action: lambda:InvokeFunctionUrl
Principal: "*"
FunctionUrlAuthType: NONE
# ===========================================================================
# 3. STATIC SPA HOSTING: S3 BUCKET & ORIGIN ACCESS CONTROL
# ===========================================================================
FrontendS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${ProjectName}-frontend-${Environment}-${AWS::AccountId}"
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
# ===========================================================================
# 4. CLOUDFRONT CDN DISTRIBUTION (SPA + /api/* LAMBDA ROUTING)
# ===========================================================================
CloudFrontOriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: !Sub "${ProjectName}-oac-${Environment}"
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Comment: !Sub "CDN for ${ProjectName} (${Environment})"
DefaultRootObject: index.html
Origins:
- Id: S3FrontendOrigin
DomainName: !GetAtt FrontendS3Bucket.RegionalDomainName
OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
S3OriginConfig: {}
- Id: LambdaBackendOrigin
DomainName: !Select [2, !Split ["/", !GetAtt BackendFunctionUrl.FunctionUrl]]
CustomOriginConfig:
HTTPSPort: 443
OriginProtocolPolicy: https-only
DefaultCacheBehavior:
TargetOriginId: S3FrontendOrigin
ViewerProtocolPolicy: redirect-to-https
AllowedMethods: [GET, HEAD, OPTIONS]
CachedMethods: [GET, HEAD, OPTIONS]
ForwardedValues:
QueryString: false
Cookies:
Forward: none
CacheBehaviors:
- PathPattern: "/api/*"
TargetOriginId: LambdaBackendOrigin
ViewerProtocolPolicy: https-only
AllowedMethods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
ForwardedValues:
QueryString: true
Cookies:
Forward: all
Headers: [Authorization, Accept, Content-Type]
MinTTL: 0
DefaultTTL: 0
MaxTTL: 0
- PathPattern: "/health"
TargetOriginId: LambdaBackendOrigin
ViewerProtocolPolicy: https-only
AllowedMethods: [GET, HEAD, OPTIONS]
ForwardedValues:
QueryString: false
Cookies:
Forward: none
MinTTL: 0
DefaultTTL: 0
MaxTTL: 0
# ===========================================================================
# 5. LIVE ON-CALL RESPONDER & SNS ALERT TOPIC
# ===========================================================================
OnCallAlertsTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: !Sub "${ProjectName}-alerts-${Environment}"
OnCall5xxAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${ProjectName}-api-5xx-alarm-${Environment}"
AlarmDescription: "Elevated 5xx error rate detected on serverless backend Lambda"
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 5
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref BackendLambdaFunction
AlarmActions:
- !Ref OnCallAlertsTopic
OnCallResponderRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${ProjectName}-on-call-responder-role-${Environment}"
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: OnCallRemediationPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:GetFunction
- lambda:GetFunctionConfiguration
- lambda:UpdateFunctionConfiguration
- lambda:UpdateFunctionCode
- lambda:PublishVersion
- lambda:ListVersionsByFunction
Resource: !GetAtt BackendLambdaFunction.Arn
OnCallResponderFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub "${ProjectName}-on-call-responder-${Environment}"
Role: !GetAtt OnCallResponderRole.Arn
PackageType: Zip
Runtime: python3.11
Handler: index.handler
MemorySize: 256
Timeout: 60
Code:
ZipFile: |
def handler(event, context):
import json
print("Received on-call event trigger:", json.dumps(event))
return {"statusCode": 200, "resolutionStatus": "RESOLVED"}
Environment:
Variables:
APP_ENV: !Ref Environment
TARGET_FUNCTION: !Ref BackendLambdaFunction
HEALTH_URL: !GetAtt BackendFunctionUrl.FunctionUrl
OnCallSnsPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt OnCallResponderFunction.Arn
Action: lambda:InvokeFunction
Principal: sns.amazonaws.com
SourceArn: !Ref OnCallAlertsTopic
OnCallSnsSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref OnCallAlertsTopic
Protocol: lambda
Endpoint: !GetAtt OnCallResponderFunction.Arn
Outputs:
CloudFrontDomainName:
Description: CloudFront Global CDN Domain URL for the frontend and API
Value: !GetAtt CloudFrontDistribution.DomainName
LambdaFunctionUrl:
Description: Direct AWS Lambda Function URL for backend API
Value: !GetAtt BackendFunctionUrl.FunctionUrl
FrontendS3BucketName:
Description: S3 Bucket Name hosting React SPA static assets
Value: !Ref FrontendS3Bucket
OnCallSnsTopicArn:
Description: Amazon SNS Topic ARN for incident alerts
Value: !Ref OnCallAlertsTopic
OnCallResponderFunctionArn:
Description: AWS Lambda On-Call Responder Function ARN
Value: !GetAtt OnCallResponderFunction.Arn