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" + } + } + } +} +