Deploy to AWS Lambda #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # INFO: THIS WORKFLOW DEPLOYS MINIMAL API TO AWS LAMBDA AND CONFIGURES API GATEWAY WITH AWS CLI | |
| # INFO: IAM USER REQUIRES PERMISSIONS TO EXECUTE THIS WORKFLOW (SEE IN FILE '.github/aws/iam-user-policy.json' AND 'README.md' FOR DETAILS) | |
| # INFO: ROLES CREATED BY THIS WORKFLOW ARE USED BY LAMBDA AND API GATEWAY FOR THEIR OPERATIONS (OPTIMIZED FOR SECURITY) | |
| # INFO: THIS WORKFLOW PRESERVES API GATEWAY ID ACROSS RUNS TO MAINTAIN CONSISTENT API ENDPOINTS | |
| # INFO: USAGE PLANS AND API KEYS ARE REUSED WHEN THEY ALREADY EXIST TO PREVENT CONFLICTS | |
| # INFO: PROPER ERROR HANDLING IS IMPLEMENTED TO ENSURE SMOOTH DEPLOYMENT EVEN WHEN RESOURCES ALREADY EXIST | |
| # INFO: GITHUB ACTIONS ANNOTATIONS ARE USED TO DISPLAY IMPORTANT INFORMATION LIKE API GATEWAY URL | |
| # INFO: ENHANCED SECURITY MEASURES ARE IN PLACE INCLUDING LEAST PRIVILEGE ACCESS AND RESOURCE MASKING | |
| # INFO: API GATEWAY CONFIGURED WITH UNIVERSAL CORS FOR MICROSERVICE USAGE ACROSS MULTIPLE PROJECTS | |
| # WARN: THERE IS DUPLICATION BETWEEN IAM USER PERMISSIONS AND THOSE OF CREATED ROLES | |
| # WARN: THIS DUPLICATION MAKES SOME PERMISSIONS IN ROLES REDUNDANT BECAUSE IAM USER ALREADY HAS THESE PERMISSIONS | |
| # WARN: THIS APPROACH IS NOT OPTIMAL FROM A SECURITY PERSPECTIVE (PRINCIPLE OF LEAST PRIVILEGE) | |
| # WARN: API GATEWAY ID IS VISIBLE IN LOGS BECAUSE URL ARE PUBLIC BUT API KEY IS PROPERLY MASKED FOR SECURITY | |
| # WARN: USAGE PLAN ASSOCIATION MIGHT FAIL IF THE API STAGE IS ALREADY ASSOCIATED WITH ANOTHER USAGE PLAN | |
| # TODO FIX: TO OPTIMIZE SECURITY, CONSIDER: | |
| # FIX: 1. LIMITING IAM USER TO ONLY PERMISSIONS NECESSARY TO CREATE/MANAGE ROLES | |
| # FIX: 2. ENSURING THAT CREATED ROLES HAVE SPECIFIC PERMISSIONS FOR THEIR RESPECTIVE TASKS | |
| # FIX: 3. USING SPECIFIC ARNS IN POLICIES RATHER THAN "*" TO LIMIT SCOPE OF PERMISSIONS | |
| # FIX: 4. IMPLEMENTING PROPER CLEANUP OF UNUSED RESOURCES TO PREVENT ACCUMULATION | |
| name: Deploy to AWS Lambda | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| run_tests: | |
| description: "Run tests before deployment" | |
| type: boolean | |
| default: true | |
| required: true | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| # PROJECT-SPECIFIC SETTINGS | |
| DEFAULT_FUNCTION_NAME: "ContactFormCsharp" | |
| DEFAULT_RESOURCE_PREFIX: "ContactFormCsharp-" | |
| DEFAULT_PROJECT_PATH: "ContactForm.MinimalAPI" | |
| DEFAULT_TESTS_PATH: "ContactForm.Tests" | |
| DEFAULT_LAMBDA_HANDLER: "ContactForm.MinimalAPI::ContactForm.MinimalAPI.LambdaEntryPoint::FunctionHandlerAsync" | |
| DEFAULT_API_GATEWAY_TYPE: "REST" | |
| # PROJECT ENVIRONMENT VARIABLES | |
| DEFAULT_PROJECT_ENV: >- | |
| SMTP_1_PASSWORD=${{ secrets.SMTP_1_PASSWORD }}; | |
| SMTP_1_PASSWORD_TEST=${{ secrets.SMTP_1_PASSWORD_TEST }}; | |
| SMTP_2_PASSWORD=${{ secrets.SMTP_2_PASSWORD }}; | |
| SMTP_2_PASSWORD_TEST=${{ secrets.SMTP_2_PASSWORD_TEST }}; | |
| # APPLICATION SETTINGS | |
| DEFAULT_ASPNETCORE_ENVIRONMENT: "Production" | |
| DEFAULT_LAMBDA_DEBUG: "true" | |
| # AWS DEPLOYMENT CONFIGURATION - ADJUST THESE FOR PERFORMANCE/COST BALANCE | |
| DEFAULT_REGION: "us-east-1" | |
| DEFAULT_DOTNET_VERSION: "8.0.x" | |
| DEFAULT_MEMORY: "512" # MEMORY IN MB | |
| DEFAULT_TIMEOUT: "30" # TIMEOUT IN SECONDS | |
| DEFAULT_THROTTLE_RATE: "10" # REQUESTS PER SECOND | |
| DEFAULT_THROTTLE_BURST: "20" # MAX CONCURRENT REQUESTS | |
| DEFAULT_QUOTA_LIMIT: "1000" # MAX REQUESTS PER PERIOD | |
| DEFAULT_QUOTA_PERIOD: "DAY" # QUOTA PERIOD (DAY, WEEK, MONTH) | |
| # DOTNET BUILD SETTINGS - TYPICALLY NO NEED TO MODIFY | |
| DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true | |
| DOTNET_CLI_TELEMETRY_OPTOUT: true | |
| run-name: "Deploy to AWS Lambda" | |
| permissions: | |
| id-token: write | |
| contents: read | |
| packages: read | |
| jobs: | |
| create-github-role: | |
| name: Create GitHub OIDC role | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| role_arn: ${{ steps.create-oidc-role.outputs.role_arn }} | |
| role_exists: ${{ steps.check-role-exists.outputs.exists }} | |
| steps: | |
| - name: Set variables | |
| run: | | |
| # SET VARIABLES DIRECTLY FROM DEFAULTS | |
| echo "FUNCTION_NAME=$DEFAULT_FUNCTION_NAME" >> $GITHUB_ENV | |
| echo "RESOURCE_PREFIX=$DEFAULT_RESOURCE_PREFIX" >> $GITHUB_ENV | |
| echo "AWS_REGION=$DEFAULT_REGION" >> $GITHUB_ENV | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Configure AWS credentials with access key | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ env.AWS_REGION || 'us-east-1' }} | |
| - name: Check if GitHub OIDC role already exists | |
| id: check-role-exists | |
| run: | | |
| echo "TESTING AWS CREDENTIALS..." | |
| # GET ACCOUNT ID WITHOUT DISPLAYING IT | |
| ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text) | |
| echo "::add-mask::$ACCOUNT_ID" | |
| echo "ACCOUNT_ID=$ACCOUNT_ID" >> $GITHUB_ENV | |
| ROLE_NAME="${{ env.RESOURCE_PREFIX }}github-actions-role" | |
| ROLE_ARN=$(aws iam get-role --role-name "$ROLE_NAME" --query 'Role.Arn' --output text 2>/dev/null || echo "") | |
| echo "::add-mask::$ROLE_ARN" | |
| if [ -n "$ROLE_ARN" ]; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "role_arn=$ROLE_ARN" >> $GITHUB_OUTPUT | |
| echo "::notice::GITHUB OIDC ROLE ALREADY EXISTS. REUSING EXISTING ROLE." | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create GitHub Actions role | |
| id: create-oidc-role | |
| if: steps.check-role-exists.outputs.exists != 'true' | |
| run: | | |
| # CREATE TRUST POLICY | |
| echo '{ | |
| "Version": "2012-10-17", | |
| "Statement": [ | |
| { | |
| "Effect": "Allow", | |
| "Principal": { | |
| "Federated": "PROVIDER_ARN_PLACEHOLDER" | |
| }, | |
| "Action": "sts:AssumeRoleWithWebIdentity", | |
| "Condition": { | |
| "StringLike": { | |
| "token.actions.githubusercontent.com:sub": "repo:${{ github.repository }}:*" | |
| } | |
| } | |
| } | |
| ] | |
| }' > trust-policy-template.json | |
| # CREATE POLICY | |
| echo '{ | |
| "Version": "2012-10-17", | |
| "Statement": [ | |
| { | |
| "Effect": "Allow", | |
| "Action": [ | |
| "iam:CreateRole", | |
| "iam:GetRole", | |
| "iam:PutRolePolicy", | |
| "iam:AttachRolePolicy", | |
| "iam:ListRolePolicies", | |
| "iam:ListAttachedRolePolicies" | |
| ], | |
| "Resource": [ | |
| "arn:aws:iam::${{ env.ACCOUNT_ID }}:role/${{ env.RESOURCE_PREFIX }}*" | |
| ] | |
| }, | |
| { | |
| "Effect": "Allow", | |
| "Action": [ | |
| "lambda:CreateFunction", | |
| "lambda:GetFunction", | |
| "lambda:UpdateFunctionCode", | |
| "lambda:UpdateFunctionConfiguration", | |
| "lambda:AddPermission", | |
| "lambda:DeleteFunction" | |
| ], | |
| "Resource": "arn:aws:lambda:${{ env.AWS_REGION }}:${{ env.ACCOUNT_ID }}:function:${{ env.FUNCTION_NAME }}" | |
| }, | |
| { | |
| "Effect": "Allow", | |
| "Action": [ | |
| "apigateway:GET", | |
| "apigateway:POST", | |
| "apigateway:PUT", | |
| "apigateway:DELETE" | |
| ], | |
| "Resource": [ | |
| "arn:aws:apigateway:${{ env.AWS_REGION }}::/restapis", | |
| "arn:aws:apigateway:${{ env.AWS_REGION }}::/restapis/*" | |
| ] | |
| } | |
| ] | |
| }' > policy.json | |
| # GET PROVIDER ARN WITHOUT DISPLAYING IT | |
| PROVIDER_ARN=$(aws iam list-open-id-connect-providers --query 'OpenIDConnectProviderList[?contains(Arn, `token.actions.githubusercontent.com`)].Arn' --output text) | |
| echo "::add-mask::$PROVIDER_ARN" | |
| if [ -z "$PROVIDER_ARN" ]; then | |
| echo "CREATING GITHUB OIDC PROVIDER..." | |
| THUMBPRINT=$(curl -s https://token.actions.githubusercontent.com/.well-known/openid-configuration | jq -r '.jwks_uri' | xargs curl -s | jq -r '.keys[0].x5c[0]' | base64 -d | openssl x509 -noout -fingerprint -sha1 | cut -d'=' -f2 | tr -d ':') | |
| PROVIDER_ARN=$(aws iam create-open-id-connect-provider --url "https://token.actions.githubusercontent.com" --client-id-list "sts.amazonaws.com" --thumbprint-list "$THUMBPRINT" --query 'OpenIDConnectProviderArn' --output text) | |
| echo "::add-mask::$PROVIDER_ARN" | |
| fi | |
| ROLE_NAME="${{ env.RESOURCE_PREFIX }}github-actions-role" | |
| # DON'T DISPLAY PROVIDER ARN | |
| jq -n \ | |
| --arg provider "$PROVIDER_ARN" \ | |
| --arg repo "${{ github.repository }}" \ | |
| '{ | |
| Version: "2012-10-17", | |
| Statement: [{ | |
| Effect: "Allow", | |
| Principal: { | |
| Federated: $provider | |
| }, | |
| Action: "sts:AssumeRoleWithWebIdentity", | |
| Condition: { | |
| StringLike: { | |
| "token.actions.githubusercontent.com:sub": ("repo:" + $repo + ":*") | |
| } | |
| } | |
| }] | |
| }' > trust-policy.json | |
| ROLE_ARN=$(aws iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document file://trust-policy.json --query 'Role.Arn' --output text) | |
| echo "::add-mask::$ROLE_ARN" | |
| aws iam put-role-policy --role-name "$ROLE_NAME" --policy-name "${RESOURCE_PREFIX}github-actions-policy" --policy-document file://policy.json | |
| echo "WAITING FOR ROLE PROPAGATION..." | |
| sleep 10 | |
| echo "role_arn=$ROLE_ARN" >> $GITHUB_OUTPUT | |
| test: | |
| needs: create-github-role | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: ${{ github.event.inputs.run_tests == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set variables | |
| run: | | |
| echo "TESTS_PATH=$DEFAULT_TESTS_PATH" >> $GITHUB_ENV | |
| echo "DOTNET_VERSION=$DEFAULT_DOTNET_VERSION" >> $GITHUB_ENV | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v3 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v3 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/packages.config') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nuget- | |
| - name: Restore dependencies | |
| run: | | |
| cd ${{ env.TESTS_PATH }} | |
| dotnet restore | |
| - name: Run tests | |
| run: | | |
| cd ${{ env.TESTS_PATH }} | |
| dotnet test --no-restore --verbosity normal --collect:"XPlat Code Coverage" --logger "trx;LogFileName=test-results.trx" | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: test-results | |
| path: ${{ env.TESTS_PATH }}/TestResults | |
| retention-days: 30 | |
| build: | |
| needs: [create-github-role, test] | |
| if: | | |
| !failure() && !cancelled() && | |
| (needs.test.result == 'success' || needs.test.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| outputs: | |
| package_path: ${{ steps.package.outputs.package_path }} | |
| steps: | |
| - name: Check skipped jobs status | |
| run: | | |
| if [[ "${{ needs.test.result }}" == "skipped" ]]; then | |
| echo "::notice::TEST JOB WAS SKIPPED BECAUSE RUN_TESTS IS SET TO FALSE." | |
| fi | |
| - uses: actions/checkout@v4 | |
| - name: Set remaining variables | |
| run: | | |
| echo "PROJECT_PATH=$DEFAULT_PROJECT_PATH" >> $GITHUB_ENV | |
| echo "TESTS_PATH=$DEFAULT_TESTS_PATH" >> $GITHUB_ENV | |
| echo "LAMBDA_HANDLER=$DEFAULT_LAMBDA_HANDLER" >> $GITHUB_ENV | |
| # ENVIRONMENT VARIABLES JSON | |
| echo "ASPNETCORE_ENVIRONMENT=$DEFAULT_ASPNETCORE_ENVIRONMENT" >> $GITHUB_ENV | |
| echo "LAMBDA_DEBUG=$DEFAULT_LAMBDA_DEBUG" >> $GITHUB_ENV | |
| # BOOLEAN FLAGS | |
| echo "RUN_TESTS=${{ github.event.inputs.run_tests }}" >> $GITHUB_ENV | |
| echo "ENABLE_CORS=true" >> $GITHUB_ENV | |
| # SET Lambda config values | |
| echo "DOTNET_VERSION=$DEFAULT_DOTNET_VERSION" >> $GITHUB_ENV | |
| echo "LAMBDA_MEMORY=$DEFAULT_MEMORY" >> $GITHUB_ENV | |
| echo "LAMBDA_TIMEOUT=$DEFAULT_TIMEOUT" >> $GITHUB_ENV | |
| # SET API Gateway config values | |
| echo "API_THROTTLE_RATE=$DEFAULT_THROTTLE_RATE" >> $GITHUB_ENV | |
| echo "API_THROTTLE_BURST=$DEFAULT_THROTTLE_BURST" >> $GITHUB_ENV | |
| echo "API_QUOTA_LIMIT=$DEFAULT_QUOTA_LIMIT" >> $GITHUB_ENV | |
| echo "API_QUOTA_PERIOD=$DEFAULT_QUOTA_PERIOD" >> $GITHUB_ENV | |
| echo "API_GATEWAY_TYPE=$DEFAULT_API_GATEWAY_TYPE" >> $GITHUB_ENV | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v3 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Debug Environment Variables | |
| run: | | |
| echo "ENVIRONMENT VARIABLES:" | |
| echo "PROJECT_PATH=${{ env.PROJECT_PATH }}" | |
| echo "DOTNET_VERSION=${{ env.DOTNET_VERSION }}" | |
| ls -la | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v3 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/packages.config') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nuget- | |
| - name: Install Amazon.Lambda.Tools | |
| run: dotnet tool install -g Amazon.Lambda.Tools | |
| - name: Build and Package | |
| id: package | |
| run: | | |
| PROJECT_DIR="${PROJECT_PATH:-$DEFAULT_PROJECT_PATH}" | |
| echo "USING PROJECT DIRECTORY: $PROJECT_DIR" | |
| cd $PROJECT_DIR | |
| dotnet restore | |
| dotnet build -c Release --no-restore | |
| dotnet publish -c Release -o ./publish --no-restore | |
| dotnet lambda package --configuration Release --framework net8.0 --output-package bin/Release/net8.0/lambda-package.zip | |
| echo "package_path=bin/Release/net8.0/lambda-package.zip" >> $GITHUB_OUTPUT | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: lambda-package | |
| path: ${{ env.PROJECT_PATH }}/bin/Release/net8.0/lambda-package.zip | |
| create-iam-roles: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: ${{ !failure() && !cancelled() }} | |
| outputs: | |
| lambda_role_arn: ${{ steps.create-lambda-role.outputs.role_arn }} | |
| role_exists: ${{ steps.check-lambda-role-exists.outputs.exists }} | |
| steps: | |
| - name: Set variables | |
| run: | | |
| # SET variables directly from defaults | |
| echo "FUNCTION_NAME=$DEFAULT_FUNCTION_NAME" >> $GITHUB_ENV | |
| echo "RESOURCE_PREFIX=$DEFAULT_RESOURCE_PREFIX" >> $GITHUB_ENV | |
| echo "AWS_REGION=$DEFAULT_REGION" >> $GITHUB_ENV | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ env.AWS_REGION || 'us-east-1' }} | |
| - name: Check if Lambda role already exists | |
| id: check-lambda-role-exists | |
| run: | | |
| # GET AWS ACCOUNT ID | |
| ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text) | |
| echo "::add-mask::$ACCOUNT_ID" | |
| echo "ACCOUNT_ID=$ACCOUNT_ID" >> $GITHUB_ENV | |
| # CHECK IF LAMBDA ROLE EXISTS | |
| ROLE_NAME="${{ env.RESOURCE_PREFIX }}lambda-exec-role" | |
| ROLE_ARN=$(aws iam get-role --role-name "$ROLE_NAME" --query 'Role.Arn' --output text 2>/dev/null || echo "") | |
| echo "::add-mask::$ROLE_ARN" | |
| if [ -n "$ROLE_ARN" ]; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "role_arn=$ROLE_ARN" >> $GITHUB_OUTPUT | |
| echo "::notice::LAMBDA EXECUTION ROLE ALREADY EXISTS. REUSING EXISTING ROLE." | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create Lambda execution role | |
| id: create-lambda-role | |
| if: steps.check-lambda-role-exists.outputs.exists != 'true' | |
| run: | | |
| ROLE_NAME="${{ env.RESOURCE_PREFIX }}lambda-exec-role" | |
| echo "CREATING LAMBDA EXECUTION ROLE..." | |
| echo '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}' > trust-policy.json | |
| ROLE_ARN=$(aws iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document file://trust-policy.json --query 'Role.Arn' --output text) | |
| echo "::add-mask::$ROLE_ARN" | |
| aws iam attach-role-policy --role-name "$ROLE_NAME" --policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" > /dev/null | |
| # LEAST PRIVILEGED POLICY (SPECIFIC ARNS) | |
| echo '{ | |
| "Version": "2012-10-17", | |
| "Statement": [ | |
| { | |
| "Effect": "Allow", | |
| "Action": [ | |
| "logs:CreateLogGroup", | |
| "logs:CreateLogStream", | |
| "logs:PutLogEvents" | |
| ], | |
| "Resource": [ | |
| "arn:aws:logs:${{ env.AWS_REGION }}:${{ env.ACCOUNT_ID }}:log-group:/aws/lambda/${{ env.FUNCTION_NAME }}:*" | |
| ] | |
| } | |
| ] | |
| }' > lambda-policy.json | |
| aws iam put-role-policy --role-name "$ROLE_NAME" --policy-name "${{ env.RESOURCE_PREFIX }}lambda-policy" --policy-document file://lambda-policy.json > /dev/null | |
| echo "WAITING FOR ROLE PROPAGATION..." | |
| sleep 10 | |
| # STORE ROLE ARN IN OUTPUT WITHOUT DISPLAYING IT | |
| echo "role_arn=$ROLE_ARN" >> $GITHUB_OUTPUT | |
| deploy: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [build, create-iam-roles] | |
| if: ${{ !failure() && !cancelled() }} | |
| env: | |
| LAMBDA_PACKAGE_PATH: lambda-package.zip | |
| steps: | |
| - name: Set variables | |
| run: | | |
| # SET variables directly from defaults | |
| echo "FUNCTION_NAME=$DEFAULT_FUNCTION_NAME" >> $GITHUB_ENV | |
| echo "RESOURCE_PREFIX=$DEFAULT_RESOURCE_PREFIX" >> $GITHUB_ENV | |
| echo "PROJECT_PATH=$DEFAULT_PROJECT_PATH" >> $GITHUB_ENV | |
| echo "TESTS_PATH=$DEFAULT_TESTS_PATH" >> $GITHUB_ENV | |
| echo "LAMBDA_HANDLER=$DEFAULT_LAMBDA_HANDLER" >> $GITHUB_ENV | |
| # APPLICATION ENVIRONMENT VARIABLES | |
| echo "ASPNETCORE_ENVIRONMENT=$DEFAULT_ASPNETCORE_ENVIRONMENT" >> $GITHUB_ENV | |
| echo "LAMBDA_DEBUG=$DEFAULT_LAMBDA_DEBUG" >> $GITHUB_ENV | |
| # BOOLEAN flags | |
| echo "RUN_TESTS=${{ github.event.inputs.run_tests }}" >> $GITHUB_ENV | |
| echo "ENABLE_CORS=true" >> $GITHUB_ENV | |
| # SET CONFIG VALUES | |
| echo "AWS_REGION=$DEFAULT_REGION" >> $GITHUB_ENV | |
| echo "DOTNET_VERSION=$DEFAULT_DOTNET_VERSION" >> $GITHUB_ENV | |
| echo "LAMBDA_MEMORY=$DEFAULT_MEMORY" >> $GITHUB_ENV | |
| echo "LAMBDA_TIMEOUT=$DEFAULT_TIMEOUT" >> $GITHUB_ENV | |
| # SET API GATEWAY CONFIG VALUES | |
| echo "API_THROTTLE_RATE=$DEFAULT_THROTTLE_RATE" >> $GITHUB_ENV | |
| echo "API_THROTTLE_BURST=$DEFAULT_THROTTLE_BURST" >> $GITHUB_ENV | |
| echo "API_QUOTA_LIMIT=$DEFAULT_QUOTA_LIMIT" >> $GITHUB_ENV | |
| echo "API_QUOTA_PERIOD=$DEFAULT_QUOTA_PERIOD" >> $GITHUB_ENV | |
| echo "API_GATEWAY_TYPE=$DEFAULT_API_GATEWAY_TYPE" >> $GITHUB_ENV | |
| - name: Debug Info | |
| run: | | |
| echo "JOB DEPLOY IS RUNNING!" | |
| echo "AWS_REGION=${{ env.AWS_REGION }}" | |
| echo "AWS_ACCESS_KEY_ID EXISTS: ${{ secrets.AWS_ACCESS_KEY_ID != '' }}" | |
| echo "AWS_SECRET_ACCESS_KEY EXISTS: ${{ secrets.AWS_SECRET_ACCESS_KEY != '' }}" | |
| - uses: actions/checkout@v4 | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ env.AWS_REGION || 'us-east-1' }} | |
| - name: Get Lambda Execution Role | |
| run: | | |
| ROLE_ARN="${{ needs.create-iam-roles.outputs.lambda_role_arn }}" | |
| if [ -z "$ROLE_ARN" ]; then | |
| echo "ROLE ARN WAS NOT CORRECTLY PASSED FROM THE CREATE-IAM-ROLES JOB, GETTING IT DIRECTLY..." | |
| ROLE_NAME="${{ env.RESOURCE_PREFIX }}lambda-exec-role" | |
| ROLE_ARN=$(aws iam get-role --role-name "$ROLE_NAME" --query 'Role.Arn' --output text) | |
| if [[ "${{ needs.create-iam-roles.result }}" == "skipped" ]]; then | |
| echo "::notice::IAM ROLES JOB WAS SKIPPED BECAUSE LAMBDA EXECUTION ROLE ALREADY EXISTS AND IS BEING REUSED." | |
| fi | |
| fi | |
| echo "::add-mask::$ROLE_ARN" | |
| echo "LAMBDA_ROLE_ARN=$ROLE_ARN" >> $GITHUB_ENV | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v3 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Get AWS Account ID | |
| run: | | |
| ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text) | |
| echo "::add-mask::$ACCOUNT_ID" | |
| echo "ACCOUNT_ID=$ACCOUNT_ID" >> $GITHUB_ENV | |
| - name: Install Amazon.Lambda.Tools | |
| run: dotnet tool install -g Amazon.Lambda.Tools | |
| - name: Download artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: lambda-package | |
| path: ${{ env.PROJECT_PATH }}/ | |
| - name: Verify package exists | |
| run: | | |
| if [ ! -f "$PROJECT_PATH/${{ env.LAMBDA_PACKAGE_PATH }}" ]; then | |
| echo "PACKAGE NOT FOUND AT $PROJECT_PATH/${{ env.LAMBDA_PACKAGE_PATH }}" | |
| ls -la $PROJECT_PATH/ | |
| exit 1 | |
| fi | |
| - name: Deploy Lambda function | |
| run: | | |
| cd ${{ env.PROJECT_PATH }} | |
| # DEPLOYING WITH THE RETRIEVED ROLE ARN | |
| echo "DEPLOYING WITH AWS CREDENTIALS..." | |
| dotnet lambda deploy-function ${{ env.FUNCTION_NAME }} \ | |
| --region ${{ env.AWS_REGION }} \ | |
| --function-role "${{ env.LAMBDA_ROLE_ARN }}" \ | |
| --function-handler ${{ env.LAMBDA_HANDLER }} \ | |
| --package ${{ env.LAMBDA_PACKAGE_PATH }} \ | |
| --function-memory-size ${{ env.LAMBDA_MEMORY }} \ | |
| --function-timeout ${{ env.LAMBDA_TIMEOUT }} \ | |
| --function-runtime dotnet8 \ | |
| --function-url ${{ env.FUNCTION_URL }} \ | |
| --function-role ${{ env.LAMBDA_ROLE_ARN }} \ | |
| --function-environment-variables "ASPNETCORE_ENVIRONMENT=${{ env.ASPNETCORE_ENVIRONMENT }};LAMBDA_DEBUG=${{ env.LAMBDA_DEBUG }};${{ env.DEFAULT_PROJECT_ENV }}" \ | |
| ${{ env.PROJECT_PATH }} | |
| - name: Ensure environment variables are set with AWS CLI | |
| run: | | |
| echo "WAITING FOR LAMBDA FUNCTION TO BECOME ACTIVE..." | |
| # WAIT FOR FUNCTION TO BECOME ACTIVE | |
| MAX_WAIT=60 | |
| WAIT_TIME=0 | |
| DELAY=5 | |
| while [ $WAIT_TIME -lt $MAX_WAIT ]; do | |
| FUNCTION_STATE=$(aws lambda get-function --function-name ${{ env.FUNCTION_NAME }} --query 'Configuration.State' --output text 2>/dev/null || echo "") | |
| echo "Current function state: $FUNCTION_STATE" | |
| if [ "$FUNCTION_STATE" = "Active" ]; then | |
| echo "LAMBDA FUNCTION IS ACTIVE, PROCEEDING WITH ENVIRONMENT VARIABLE UPDATE..." | |
| break | |
| fi | |
| echo "WAITING FOR FUNCTION TO BECOME ACTIVE, WAITED ${WAIT_TIME}s SO FAR..." | |
| sleep $DELAY | |
| WAIT_TIME=$((WAIT_TIME + DELAY)) | |
| done | |
| if [ $WAIT_TIME -ge $MAX_WAIT ]; then | |
| echo "WARNING: TIMED OUT WAITING FOR FUNCTION TO BECOME ACTIVE, ATTEMPTING UPDATE ANYWAY..." | |
| fi | |
| echo "SETTING ENVIRONMENT VARIABLES DIRECTLY WITH AWS CLI..." | |
| # Parse DEFAULT_PROJECT_ENV into variables | |
| # Create base ENV_VARS with non-DEFAULT_PROJECT_ENV variables | |
| ENV_VARS_JSON='{ | |
| "Variables": { | |
| "ASPNETCORE_ENVIRONMENT": "'$ASPNETCORE_ENVIRONMENT'", | |
| "LAMBDA_DEBUG": "'$LAMBDA_DEBUG'" | |
| } | |
| }' | |
| # PARSE SMTP VARIABLES FROM DEFAULT_PROJECT_ENV | |
| IFS=';' read -ra ENV_VARS_ARRAY <<< "${{ env.DEFAULT_PROJECT_ENV }}" | |
| for VAR in "${ENV_VARS_ARRAY[@]}"; do | |
| if [ -n "$VAR" ]; then | |
| VAR=$(echo "$VAR" | xargs) # Trim whitespace | |
| if [ -n "$VAR" ]; then | |
| KEY=$(echo "$VAR" | cut -d'=' -f1) | |
| VALUE=$(echo "$VAR" | cut -d'=' -f2-) | |
| # ADD TO THE JSON USING JQ | |
| ENV_VARS_JSON=$(echo "$ENV_VARS_JSON" | jq --arg key "$KEY" --arg value "$VALUE" '.Variables[$key] = $value') | |
| fi | |
| fi | |
| done | |
| # CONVERT TO PROPER FORMAT FOR AWS CLI | |
| ENV_VARS=$(echo "$ENV_VARS_JSON") | |
| # UPDATE THE FUNCTION CONFIGURATION WITH THE ENVIRONMENT VARIABLES | |
| MAX_RETRIES=5 | |
| RETRY=0 | |
| RETRY_DELAY=10 | |
| while [ $RETRY -lt $MAX_RETRIES ]; do | |
| if aws lambda update-function-configuration \ | |
| --function-name ${{ env.FUNCTION_NAME }} \ | |
| --environment "$ENV_VARS" \ | |
| --region ${{ env.AWS_REGION }}; then | |
| echo "SUCCESSFULLY UPDATED ENVIRONMENT VARIABLES" | |
| break | |
| else | |
| RETRY=$((RETRY + 1)) | |
| if [ $RETRY -ge $MAX_RETRIES ]; then | |
| echo "FAILED TO UPDATE ENVIRONMENT VARIABLES AFTER MAXIMUM RETRIES" | |
| echo "PLEASE CHECK LAMBDA FUNCTION CONFIGURATION MANUALLY" | |
| exit 1 | |
| fi | |
| echo "UPDATE FAILED, RETRYING IN ${RETRY_DELAY}s (ATTEMPT $RETRY OF $MAX_RETRIES)..." | |
| sleep $RETRY_DELAY | |
| fi | |
| done | |
| echo "VERIFYING ENVIRONMENT VARIABLES..." | |
| aws lambda get-function-configuration \ | |
| --function-name ${{ env.FUNCTION_NAME }} \ | |
| --region ${{ env.AWS_REGION }} \ | |
| --query "Environment.Variables.ASPNETCORE_ENVIRONMENT" \ | |
| --output text | |
| echo "ENVIRONMENT VARIABLES CONFIGURATION COMPLETED." | |
| - name: Set API Gateway variables | |
| run: | | |
| # API GATEWAY ID/name variables | |
| echo "API_NAME=${{ env.RESOURCE_PREFIX }}api" >> $GITHUB_ENV | |
| echo "API_DESCRIPTION=API for ${{ env.FUNCTION_NAME }}" >> $GITHUB_ENV | |
| # USAGE PLAN VARIABLES | |
| echo "USAGE_PLAN_NAME=${{ env.RESOURCE_PREFIX }}usage-plan" >> $GITHUB_ENV | |
| echo "USAGE_PLAN_DESCRIPTION=Usage plan for ${{ env.FUNCTION_NAME }}" >> $GITHUB_ENV | |
| # API KEY VARIABLES | |
| echo "API_KEY_NAME=${{ env.RESOURCE_PREFIX }}api-key" >> $GITHUB_ENV | |
| echo "API_KEY_DESCRIPTION=API key for ${{ env.FUNCTION_NAME }}" >> $GITHUB_ENV | |
| - name: Check if API Gateway exists | |
| id: check-api-exists | |
| run: | | |
| # CHECK IF API ALREADY EXISTS BY LISTING ALL APIS AND FILTERING BY NAME | |
| API_ID=$(aws apigateway get-rest-apis --query "items[?name=='${{ env.API_NAME }}'].id" --output text) | |
| if [ -n "$API_ID" ]; then | |
| echo "API_ID=$API_ID" >> $GITHUB_ENV | |
| echo "::notice::REUSING EXISTING API GATEWAY WITH ID: $API_ID" | |
| echo "api_exists=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "api_exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create API Gateway | |
| if: steps.check-api-exists.outputs.api_exists != 'true' | |
| run: | | |
| echo "CREATING NEW API GATEWAY..." | |
| API_RESULT=$(aws apigateway create-rest-api \ | |
| --name "${{ env.API_NAME }}" \ | |
| --description "${{ env.API_DESCRIPTION }}" \ | |
| --endpoint-configuration "types=REGIONAL") | |
| API_ID=$(echo $API_RESULT | jq -r '.id') | |
| echo "API_ID=$API_ID" >> $GITHUB_ENV | |
| echo "::notice::CREATED NEW API GATEWAY WITH ID: $API_ID" | |
| - name: Configure API Gateway | |
| run: | | |
| # GET ROOT RESOURCE ID | |
| echo "Getting API Gateway root resource ID" | |
| ROOT_RESOURCE_ID=$(aws apigateway get-resources \ | |
| --rest-api-id $API_ID \ | |
| --query 'items[?path==`/`].id' \ | |
| --output text) | |
| echo "Root resource ID: $ROOT_RESOURCE_ID" | |
| # CHECK IF PROXY RESOURCE EXISTS | |
| echo "Checking if proxy resource exists" | |
| PROXY_RESOURCE_ID=$(aws apigateway get-resources \ | |
| --rest-api-id $API_ID \ | |
| --query 'items[?path==`/{proxy+}`].id' \ | |
| --output text) | |
| # CREATE PROXY RESOURCE IF IT DOESN'T EXIST | |
| if [ -z "$PROXY_RESOURCE_ID" ] || [ "$PROXY_RESOURCE_ID" = "None" ]; then | |
| echo "Creating proxy resource" | |
| PROXY_RESOURCE_RESULT=$(aws apigateway create-resource \ | |
| --rest-api-id $API_ID \ | |
| --parent-id $ROOT_RESOURCE_ID \ | |
| --path-part "{proxy+}") | |
| PROXY_RESOURCE_ID=$(echo $PROXY_RESOURCE_RESULT | jq -r '.id') | |
| echo "Created proxy resource with ID: $PROXY_RESOURCE_ID" | |
| else | |
| echo "Proxy resource already exists with ID: $PROXY_RESOURCE_ID" | |
| fi | |
| # CHECK IF ANY METHOD EXISTS FOR PROXY RESOURCE | |
| echo "Checking if ANY method exists for proxy resource" | |
| PROXY_ANY_METHOD=$(aws apigateway get-method \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $PROXY_RESOURCE_ID \ | |
| --http-method ANY 2>/dev/null || echo "") | |
| # CREATE ANY METHOD FOR PROXY RESOURCE IF IT DOESN'T EXIST | |
| if [ -z "$PROXY_ANY_METHOD" ]; then | |
| echo "Creating ANY method for proxy resource" | |
| aws apigateway put-method \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $PROXY_RESOURCE_ID \ | |
| --http-method ANY \ | |
| --authorization-type NONE | |
| else | |
| echo "ANY method for proxy resource already exists" | |
| fi | |
| # CHECK IF INTEGRATION EXISTS FOR PROXY RESOURCE | |
| echo "Checking if integration exists for proxy resource" | |
| PROXY_INTEGRATION=$(aws apigateway get-integration \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $PROXY_RESOURCE_ID \ | |
| --http-method ANY 2>/dev/null || echo "") | |
| # CREATE INTEGRATION FOR PROXY RESOURCE IF IT DOESN'T EXIST | |
| if [ -z "$PROXY_INTEGRATION" ]; then | |
| echo "Creating integration for proxy resource" | |
| aws apigateway put-integration \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $PROXY_RESOURCE_ID \ | |
| --http-method ANY \ | |
| --type AWS_PROXY \ | |
| --integration-http-method POST \ | |
| --uri arn:aws:apigateway:${{ env.AWS_REGION }}:lambda:path/2015-03-31/functions/arn:aws:lambda:${{ env.AWS_REGION }}:${{ env.ACCOUNT_ID }}:function:${{ env.FUNCTION_NAME }}/invocations | |
| else | |
| echo "Integration for proxy resource already exists" | |
| fi | |
| # CHECK IF ANY METHOD EXISTS FOR ROOT RESOURCE | |
| echo "Checking if ANY method exists for root resource" | |
| ROOT_ANY_METHOD=$(aws apigateway get-method \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $ROOT_RESOURCE_ID \ | |
| --http-method ANY 2>/dev/null || echo "") | |
| # CREATE ANY METHOD FOR ROOT RESOURCE IF IT DOESN'T EXIST | |
| if [ -z "$ROOT_ANY_METHOD" ]; then | |
| echo "Creating ANY method for root resource" | |
| aws apigateway put-method \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $ROOT_RESOURCE_ID \ | |
| --http-method ANY \ | |
| --authorization-type NONE | |
| else | |
| echo "ANY method for root resource already exists" | |
| fi | |
| # CHECK IF INTEGRATION EXISTS FOR ROOT RESOURCE | |
| echo "Checking if integration exists for root resource" | |
| ROOT_INTEGRATION=$(aws apigateway get-integration \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $ROOT_RESOURCE_ID \ | |
| --http-method ANY 2>/dev/null || echo "") | |
| # CREATE INTEGRATION FOR ROOT RESOURCE IF IT DOESN'T EXIST | |
| if [ -z "$ROOT_INTEGRATION" ]; then | |
| echo "Creating integration for root resource" | |
| aws apigateway put-integration \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $ROOT_RESOURCE_ID \ | |
| --http-method ANY \ | |
| --type AWS_PROXY \ | |
| --integration-http-method POST \ | |
| --uri arn:aws:apigateway:${{ env.AWS_REGION }}:lambda:path/2015-03-31/functions/arn:aws:lambda:${{ env.AWS_REGION }}:${{ env.ACCOUNT_ID }}:function:${{ env.FUNCTION_NAME }}/invocations | |
| else | |
| echo "Integration for root resource already exists" | |
| fi | |
| - name: Configure Lambda Permissions | |
| run: | | |
| # ADD LAMBDA PERMISSION FOR API GATEWAY | |
| echo "Checking for existing Lambda permission" | |
| STATEMENT_ID="${{ env.RESOURCE_PREFIX }}api-gateway-permission" | |
| # ALWAYS CHECK FOR EXISTING PERMISSION THROUGH THE ERROR MESSAGE | |
| # THIS IS MORE RELIABLE THAN CHECKING THE POLICY | |
| ADD_PERMISSION_RESULT=$(aws lambda add-permission \ | |
| --function-name ${{ env.FUNCTION_NAME }} \ | |
| --statement-id "$STATEMENT_ID" \ | |
| --action lambda:InvokeFunction \ | |
| --principal apigateway.amazonaws.com \ | |
| --source-arn "arn:aws:execute-api:${{ env.AWS_REGION }}:${{ env.ACCOUNT_ID }}:$API_ID/*/*" 2>&1 || true) | |
| # Check if the error indicates the permission already exists | |
| if echo "$ADD_PERMISSION_RESULT" | grep -q "ResourceConflictException"; then | |
| echo "Lambda permission already exists with statement ID $STATEMENT_ID" | |
| elif echo "$ADD_PERMISSION_RESULT" | grep -q "Statement"; then | |
| echo "Lambda permission successfully added with statement ID $STATEMENT_ID" | |
| else | |
| echo "Unexpected error when adding Lambda permission: $ADD_PERMISSION_RESULT" | |
| exit 1 | |
| fi | |
| - name: Deploy API | |
| run: | | |
| # CREATE DEPLOYMENT | |
| DEPLOYMENT_RESULT=$(aws apigateway create-deployment \ | |
| --rest-api-id $API_ID \ | |
| --stage-name prod \ | |
| --description "Deployed from GitHub Actions") | |
| # CONFIGURE CORS IF ENABLED | |
| if [[ "${{ env.ENABLE_CORS }}" == "true" ]]; then | |
| echo "CONFIGURING CORS FOR API GATEWAY..." | |
| # GET ALL RESOURCES | |
| RESOURCES=$(aws apigateway get-resources \ | |
| --rest-api-id $API_ID \ | |
| --query 'items[].id' \ | |
| --output text) | |
| for RESOURCE_ID in $RESOURCES; do | |
| # ADD OPTIONS METHOD | |
| aws apigateway put-method \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $RESOURCE_ID \ | |
| --http-method OPTIONS \ | |
| --authorization-type NONE 2>/dev/null || echo "Method likely exists" | |
| # CONFIGURE OPTIONS RESPONSE | |
| aws apigateway put-method-response \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $RESOURCE_ID \ | |
| --http-method OPTIONS \ | |
| --status-code 200 \ | |
| --response-models '{"application/json":"Empty"}' \ | |
| --response-parameters '{ | |
| "method.response.header.Access-Control-Allow-Headers":true, | |
| "method.response.header.Access-Control-Allow-Methods":true, | |
| "method.response.header.Access-Control-Allow-Origin":true | |
| }' 2>/dev/null || echo "Method response likely exists" | |
| # CONFIGURE OPTIONS MOCK INTEGRATION | |
| aws apigateway put-integration \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $RESOURCE_ID \ | |
| --http-method OPTIONS \ | |
| --type MOCK \ | |
| --integration-http-method OPTIONS \ | |
| --request-templates '{"application/json":"{\"statusCode\": 200}"}' 2>/dev/null || echo "Integration likely exists" | |
| # CONFIGURE INTEGRATION RESPONSE | |
| aws apigateway put-integration-response \ | |
| --rest-api-id $API_ID \ | |
| --resource-id $RESOURCE_ID \ | |
| --http-method OPTIONS \ | |
| --status-code 200 \ | |
| --response-parameters '{ | |
| "method.response.header.Access-Control-Allow-Headers":"'"'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"'", | |
| "method.response.header.Access-Control-Allow-Methods":"'"'GET,POST,PUT,DELETE,OPTIONS,PATCH'"'", | |
| "method.response.header.Access-Control-Allow-Origin":"'"'*'"'" | |
| }' \ | |
| --response-templates '{"application/json":""}' 2>/dev/null || echo "Integration response likely exists" | |
| done | |
| fi | |
| - name: Check if Usage Plan exists | |
| id: check-usage-plan | |
| run: | | |
| # CHECK IF USAGE PLAN EXISTS | |
| EXISTING_USAGE_PLANS=$(aws apigateway get-usage-plans --query "items[?name=='${{ env.USAGE_PLAN_NAME }}'].id" --output text) | |
| if [ -n "$EXISTING_USAGE_PLANS" ]; then | |
| echo "USAGE_PLAN_ID=$EXISTING_USAGE_PLANS" >> $GITHUB_ENV | |
| echo "usage_plan_exists=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "usage_plan_exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create Usage Plan | |
| if: steps.check-usage-plan.outputs.usage_plan_exists != 'true' | |
| run: | | |
| # CREATE USAGE PLAN | |
| USAGE_PLAN_RESULT=$(aws apigateway create-usage-plan \ | |
| --name "${{ env.USAGE_PLAN_NAME }}" \ | |
| --description "${{ env.USAGE_PLAN_DESCRIPTION }}" \ | |
| --throttle "rateLimit=${{ env.API_THROTTLE_RATE }},burstLimit=${{ env.API_THROTTLE_BURST }}" \ | |
| --quota "limit=${{ env.API_QUOTA_LIMIT }},period=${{ env.API_QUOTA_PERIOD }}" \ | |
| --api-stages "apiId=$API_ID,stage=prod") | |
| USAGE_PLAN_ID=$(echo $USAGE_PLAN_RESULT | jq -r '.id') | |
| echo "USAGE_PLAN_ID=$USAGE_PLAN_ID" >> $GITHUB_ENV | |
| - name: Check if API Key exists | |
| id: check-api-key | |
| run: | | |
| # CHECK IF API KEY EXISTS | |
| EXISTING_API_KEY_ID=$(aws apigateway get-api-keys --name-query "${{ env.API_KEY_NAME }}" --include-values --query "items[0].id" --output text) | |
| if [ "$EXISTING_API_KEY_ID" != "None" ] && [ -n "$EXISTING_API_KEY_ID" ]; then | |
| echo "API_KEY=$EXISTING_API_KEY_ID" >> $GITHUB_ENV | |
| echo "api_key_exists=true" >> $GITHUB_OUTPUT | |
| echo "EXISTING_API_KEY_ID=$EXISTING_API_KEY_ID" >> $GITHUB_ENV | |
| else | |
| echo "api_key_exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create API Key | |
| if: steps.check-api-key.outputs.api_key_exists != 'true' | |
| run: | | |
| # ENSURE ALL OUTPUT IS CORRECTLY MASKED | |
| # FIRST ADD MASKING FOR THE OUTPUT | |
| # THIS IS USED TO PRE-MASK OUTPUTS BEFORE THEY'RE CREATED | |
| echo "::add-mask::$(date +%s%N | sha256sum | head -c 64)" | |
| # CREATE API KEY WITHOUT DIRECT OUTPUT LOGGING | |
| # REDIRECT STDOUT TO A SECURE FILE INSTEAD OF DISPLAYING | |
| API_KEY_JSON=$(aws apigateway create-api-key \ | |
| --name "${{ env.API_KEY_NAME }}" \ | |
| --description "${{ env.API_KEY_DESCRIPTION }}" \ | |
| --enabled \ | |
| --output json > /tmp/api_key_result.json) | |
| # GET ID WITHOUT PRINTING THE FULL JSON TO LOGS | |
| API_KEY=$(cat /tmp/api_key_result.json | jq -r '.id') | |
| echo "::add-mask::$API_KEY" | |
| echo "API_KEY=$API_KEY" >> $GITHUB_ENV | |
| # IMMEDIATELY MASK THE VALUE IN THE OUTPUT FILE | |
| API_KEY_VALUE=$(cat /tmp/api_key_result.json | jq -r '.value') | |
| echo "::add-mask::$API_KEY_VALUE" | |
| # SECURELY REMOVE THE TEMP FILE WITH THE KEY DATA | |
| shred -u /tmp/api_key_result.json | |
| # ADD API KEY TO USAGE PLAN | |
| aws apigateway create-usage-plan-key \ | |
| --usage-plan-id ${{ env.USAGE_PLAN_ID }} \ | |
| --key-id $API_KEY \ | |
| --key-type API_KEY | |
| - name: Output API URL | |
| run: | | |
| # GET API URL | |
| API_URL="https://$API_ID.execute-api.${{ env.AWS_REGION }}.amazonaws.com/prod/" | |
| echo "::add-mask::$API_URL" | |
| # IMPORTANT: MASK THE API KEY VALUE BEFORE RETRIEVING IT | |
| # USE A TEMPORARY FILE TO STORE THE RESULT WITHOUT DISPLAYING IT | |
| aws apigateway get-api-key \ | |
| --api-key "$API_KEY" \ | |
| --include-value \ | |
| --output json > /tmp/api_key_value.json | |
| # EXTRACT AND MASK THE API KEY VALUE | |
| API_KEY_VALUE=$(cat /tmp/api_key_value.json | jq -r '.value') | |
| echo "::add-mask::$API_KEY_VALUE" | |
| # SECURELY REMOVE THE TEMP FILE | |
| shred -u /tmp/api_key_value.json | |
| # DISPLAY NOTICE WITH SECURE INSTRUCTIONS | |
| echo "::notice title=API Gateway Deployed::API GATEWAY HAS BEEN SUCCESSFULLY DEPLOYED WITH URL: $API_URL" | |
| if [ -z "$EXISTING_API_KEY_ID" ]; then | |
| echo "A NEW API KEY HAS BEEN GENERATED. USE THE 'X-API-KEY' HEADER WITH THIS KEY TO ACCESS THE API." | |
| else | |
| echo "EXISTING API KEY HAS BEEN REUSED. CONTINUE USING THE SAME API KEY WITH THE 'X-API-KEY' HEADER." | |
| fi |