From aa42e3e2ce37e11e1add50821233e0e420c86770 Mon Sep 17 00:00:00 2001 From: Wyatt Baggett Date: Mon, 16 Feb 2026 21:51:13 -0500 Subject: [PATCH] Enable AWS Lambda deployment for API Adds a SAM-based serverless setup and CI/CD pipeline to deploy the API to AWS Lambda, improving scalability and reducing operational overhead. Integrates an automated health check to validate deployments and introduces basic monitoring and alerting for reliability. Introduces a Lambda entrypoint for the ASP.NET Core application, defines the serverless stack with HTTP API integration, and sources configuration from a secrets manager. Configures concurrency, memory, and timeouts, and updates dependencies to target .NET 8 for Lambda runtime. - Adds GitHub Actions workflow to build and deploy via SAM - Introduces Lambda entrypoint for ASP.NET Core hosting - Defines serverless resources and environment variables from secrets - Configures alarms and notifications for errors and throttles - Updates project with AWS Lambda packages and tool defaults - Verifies deployment with an API health check --- .github/workflows/deploy-lambda.yml | 91 +++++++++ .../LambdaEntryPoint.cs | 45 +++++ .../ThriveChurchOfficialAPI.csproj | 3 + .../aws-lambda-tools-defaults.json | 21 +++ .../serverless.template | 173 ++++++++++++++++++ 5 files changed, 333 insertions(+) create mode 100644 .github/workflows/deploy-lambda.yml create mode 100644 API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/LambdaEntryPoint.cs create mode 100644 API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/aws-lambda-tools-defaults.json create mode 100644 API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/serverless.template diff --git a/.github/workflows/deploy-lambda.yml b/.github/workflows/deploy-lambda.yml new file mode 100644 index 0000000..8a29a28 --- /dev/null +++ b/.github/workflows/deploy-lambda.yml @@ -0,0 +1,91 @@ +name: Deploy Thrive API to AWS Lambda + +on: + push: + branches: + - master + paths: + - 'API/ThriveChurchOfficialAPI/**' + - '.github/workflows/deploy-lambda.yml' + workflow_dispatch: + +env: + AWS_REGION: us-east-2 + STACK_NAME: thrive-api-lambda + +permissions: + id-token: write + contents: read + actions: read + +jobs: + deploy: + name: Build and Deploy Lambda + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET 8.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Setup SAM CLI + uses: aws-actions/setup-sam@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::349570132161:role/GitHub_OIDC + aws-region: ${{ env.AWS_REGION }} + + - name: Build SAM application + working-directory: ./API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI + run: sam build --template-file serverless.template + + - name: Deploy SAM application + working-directory: ./API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI + run: | + sam deploy \ + --stack-name ${{ env.STACK_NAME }} \ + --region ${{ env.AWS_REGION }} \ + --s3-bucket thrive-sam-deployments \ + --no-confirm-changeset \ + --no-fail-on-empty-changeset \ + --capabilities CAPABILITY_IAM + + - name: Get API Gateway URL + id: get-url + run: | + API_URL=$(aws cloudformation describe-stacks \ + --stack-name ${{ env.STACK_NAME }} \ + --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" \ + --output text \ + --region ${{ env.AWS_REGION }}) + echo "api-url=$API_URL" >> $GITHUB_OUTPUT + echo "API Gateway URL: $API_URL" + + - name: Test Lambda deployment + run: | + API_URL="${{ steps.get-url.outputs.api-url }}" + + echo "Testing Lambda deployment at: ${API_URL}api/health/live" + sleep 10 + + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${API_URL}api/health/live") + + if [ "$HTTP_STATUS" -eq 200 ]; then + echo "Lambda deployment successful! Health check returned 200" + else + echo "Lambda deployment may have issues. Health check returned: $HTTP_STATUS" + exit 1 + fi + + - name: Deployment summary + run: | + echo "Deployment Complete!" + echo "Stack Name: ${{ env.STACK_NAME }}" + echo "Region: ${{ env.AWS_REGION }}" + echo "API Gateway URL: ${{ steps.get-url.outputs.api-url }}" diff --git a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/LambdaEntryPoint.cs b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/LambdaEntryPoint.cs new file mode 100644 index 0000000..a9f59dc --- /dev/null +++ b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/LambdaEntryPoint.cs @@ -0,0 +1,45 @@ +/* + MIT License + + Copyright (c) 2026 Thrive Community Church + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +using Amazon.Lambda.AspNetCoreServer; +using Microsoft.AspNetCore.Hosting; + +namespace ThriveChurchOfficialAPI +{ + /// + /// Lambda entry point that wraps the ASP.NET Core application. + /// This allows the same controllers, services, and repositories to run in AWS Lambda. + /// + public class LambdaEntryPoint : APIGatewayProxyFunction + { + /// + /// Initialize the Lambda function with the ASP.NET Core Startup class. + /// + protected override void Init(IWebHostBuilder builder) + { + builder.UseStartup(); + } + } +} + diff --git a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.csproj b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.csproj index d1d6e65..5c25f16 100644 --- a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.csproj +++ b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.csproj @@ -21,6 +21,9 @@ + + + diff --git a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/aws-lambda-tools-defaults.json b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/aws-lambda-tools-defaults.json new file mode 100644 index 0000000..12ed7a9 --- /dev/null +++ b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/aws-lambda-tools-defaults.json @@ -0,0 +1,21 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "default", + "region": "us-east-2", + "configuration": "Release", + "function-runtime": "dotnet8", + "function-memory-size": 512, + "function-timeout": 30, + "function-handler": "ThriveChurchOfficialAPI::ThriveChurchOfficialAPI.LambdaEntryPoint::FunctionHandlerAsync", + "framework": "net8.0", + "s3-bucket": "", + "s3-prefix": "ThriveChurchOfficialAPI/", + "template": "serverless.template", + "template-parameters": "" +} + diff --git a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/serverless.template b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/serverless.template new file mode 100644 index 0000000..1358d9b --- /dev/null +++ b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/serverless.template @@ -0,0 +1,173 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "Thrive Church Official API - Lambda Deployment", + "Resources": { + "ThriveApiFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "ThriveChurchOfficialAPI::ThriveChurchOfficialAPI.LambdaEntryPoint::FunctionHandlerAsync", + "Runtime": "dotnet8", + "CodeUri": "", + "MemorySize": 1024, + "Timeout": 30, + "Role": "arn:aws:iam::349570132161:role/ThriveApiAppRunnerRole", + "ReservedConcurrentExecutions": 10, + "Environment": { + "Variables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "PORT": "8080", + "MongoConnectionString": "{{resolve:secretsmanager:thrive-api/database-hzetzL:SecretString:MongoConnectionString}}", + "TokenConnectionStringPath": "Hashables", + "EsvApiKey": "{{resolve:secretsmanager:thrive/3rd-party-eohiNX:SecretString:EsvApiKey}}", + "OverrideEsvApiKey": "false", + "EmailPW": "{{resolve:secretsmanager:thrive-api/auth-zrM22S:SecretString:EmailPassword}}", + "S3__BucketName": "thrive-audio", + "S3__AccessKey": "{{resolve:secretsmanager:thrive-api/s3-U1eiYO:SecretString:AccessKey}}", + "S3__SecretKey": "{{resolve:secretsmanager:thrive-api/s3-U1eiYO:SecretString:SecretKey}}", + "S3__Region": "us-east-2", + "S3__BaseUrl": "https://podcast.thrive-fl.org", + "S3__MaxFileSizeMB": "50", + "JWT__SecretKey": "{{resolve:secretsmanager:thrive-api/auth-zrM22S:SecretString:JwtSecretKey}}", + "JWT__Issuer": "ThriveChurchOfficialAPI", + "JWT__Audience": "ThriveChurchClients", + "JWT__ExpirationMinutes": "60", + "JWT__RefreshTokenExpirationDays": "7", + "RedisConnectionString": "{{resolve:secretsmanager:thrive-api/database-hzetzL:SecretString:RedisConnectionString}}", + "IpRateLimiting__EnableEndpointRateLimiting": "false", + "IpRateLimiting__StackBlockedRequests": "true", + "IpRateLimiting__HttpStatusCode": "429" + } + }, + "Events": { + "ProxyResource": { + "Type": "HttpApi", + "Properties": { + "Path": "/{proxy+}", + "Method": "ANY" + } + }, + "RootResource": { + "Type": "HttpApi", + "Properties": { + "Path": "/", + "Method": "ANY" + } + } + } + } + }, + "AlarmNotificationTopic": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": "ThriveAPI-Alarms", + "DisplayName": "Thrive API Health Alerts", + "Subscription": [ + { + "Endpoint": "wyatt@thrive-fl.org", + "Protocol": "email" + } + ] + } + }, + "LambdaErrorAlarm": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmName": "ThriveAPI-Lambda-Errors", + "AlarmDescription": "Alert when Lambda function has errors", + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + "Statistic": "Sum", + "Period": 300, + "EvaluationPeriods": 1, + "Threshold": 5, + "ComparisonOperator": "GreaterThanThreshold", + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { + "Ref": "ThriveApiFunction" + } + } + ], + "TreatMissingData": "notBreaching", + "AlarmActions": [ + { + "Ref": "AlarmNotificationTopic" + } + ] + } + }, + "LambdaThrottleAlarm": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmName": "ThriveAPI-Lambda-Throttles", + "AlarmDescription": "Alert when Lambda function is throttled", + "MetricName": "Throttles", + "Namespace": "AWS/Lambda", + "Statistic": "Sum", + "Period": 300, + "EvaluationPeriods": 1, + "Threshold": 1, + "ComparisonOperator": "GreaterThanThreshold", + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { + "Ref": "ThriveApiFunction" + } + } + ], + "TreatMissingData": "notBreaching", + "AlarmActions": [ + { + "Ref": "AlarmNotificationTopic" + } + ] + } + }, + "ApiGateway5xxAlarm": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmName": "ThriveAPI-Gateway-5xx-Errors", + "AlarmDescription": "Alert when API Gateway returns 5xx errors", + "MetricName": "5XXError", + "Namespace": "AWS/ApiGateway", + "Statistic": "Sum", + "Period": 300, + "EvaluationPeriods": 1, + "Threshold": 5, + "ComparisonOperator": "GreaterThanThreshold", + "Dimensions": [ + { + "Name": "ApiId", + "Value": { + "Ref": "ServerlessHttpApi" + } + } + ], + "TreatMissingData": "notBreaching", + "AlarmActions": [ + { + "Ref": "AlarmNotificationTopic" + } + ] + } + } + }, + "Outputs": { + "ApiUrl": { + "Description": "API Gateway endpoint URL", + "Value": { + "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/" + } + }, + "LambdaFunctionName": { + "Description": "Lambda function name", + "Value": { + "Ref": "ThriveApiFunction" + } + } + } +} +