personal-lifecycle-tracker / cloudformation / aws-serverless-lambda-prod.yaml
aws-serverless-lambda-prod.yaml
Raw
AWSTemplateFormatVersion: '2010-09-09'
Description: >
  Personal Lifecycle Tracker - Production (Prod) 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: production
    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: "prod-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: 30

  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":"production"}'
              }          
      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