diff --git a/.github/workflows/rainbow-deployment.yml b/.github/workflows/rainbow-deployment.yml new file mode 100644 index 0000000000..0a74c9229c --- /dev/null +++ b/.github/workflows/rainbow-deployment.yml @@ -0,0 +1,114 @@ +name: Rainbow deployment (WIP) + +on: + pull_request: + branches: + - main + - feature/* + types: + - labeled + - opened + - reopened + - synchronize + +env: + AWS_ACCOUNT_ID: ${{ vars.STAGING_AWS_ACCOUNT_ID }} + AWS_REGION: ca-central-1 + GITHUB_SHA: ${{ github.sha }} + REGISTRY: ${{ vars.STAGING_AWS_ACCOUNT_ID }}.dkr.ecr.ca-central-1.amazonaws.com + COGNITO_APP_CLIENT_ID: ${{secrets.STAGING_COGNITO_APP_CLIENT_ID}} + COGNITO_USER_POOL_ID: ${{ secrets.STAGING_COGNITO_USER_POOL_ID}} + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.STAGING_NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} + ROLE_ARN: arn:aws:iam::${{ vars.STAGING_AWS_ACCOUNT_ID }}:role/forms-lambda-client + LISTENER_ARN: arn:aws:elasticloadbalancing:ca-central-1:${{ vars.STAGING_AWS_ACCOUNT_ID }}:listener/app/form-viewer/5e6bc2d9ab810b68/028e8eeeed9c3a34 + +permissions: + id-token: write + contents: write + pull-requests: write + +jobs: + rainbow-deployment: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + # - name: Get second to last release commit identifier + # id: get-second-to-last-release + # shell: bash + # run: echo "commit-id=$(git rev-parse @~)" >> $GITHUB_OUTPUT + + # - name: Checkout + # uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # with: + # ref: ${{ steps.get-second-to-last-release.outputs.commit-id }} + + - name: Configure AWS credentials using OIDC + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + with: + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/platform-forms-client-pr-review-env + role-session-name: RainbowDeployment + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Staging Amazon ECR + id: login-ecr-staging + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 + + # BASE_IMAGE should use the ECS image built for the previous release + # NEXT_DEPLOYMENT_ID should use right deployment identifier based on whether we are in Staging (git sha) or Production (git tag) + - name: Build Rainbow Lambda image + run: | + docker build -t rainbow \ + -f Dockerfile.rainbow \ + --build-arg BASE_IMAGE=$REGISTRY/forms_app_legacy:ecs \ + --build-arg COGNITO_APP_CLIENT_ID=$COGNITO_APP_CLIENT_ID \ + --build-arg COGNITO_USER_POOL_ID=$COGNITO_USER_POOL_ID \ + --build-arg NEXT_DEPLOYMENT_ID=$GITHUB_SHA . + + - name: Tag and Push Rainbow Lambda image + run: | + docker tag rainbow $REGISTRY/forms_app_legacy:$GITHUB_SHA + docker push $REGISTRY/forms_app_legacy:$GITHUB_SHA + + # Could be divided in multiple steps + # For "aws elbv2 create-target-group" there is a limitation name size (32 characters max) + # For "aws elbv2 create-rule" we need to find a solution to deal with priority since rules can't have the same one + # For "aws elbv2 create-rule", if possible, we need to find a way to dynamically pass the right host header value + - name: Rainbow magic + run: | + lambdaArn=$(aws lambda create-function \ + --function-name rainbow-$GITHUB_SHA \ + --package-type Image \ + --role $ROLE_ARN \ + --timeout 15 \ + --memory-size 2048 \ + --code ImageUri=$REGISTRY/forms_app_legacy:$GITHUB_SHA \ + --vpc-config SubnetIds=${{ secrets.PR_REVIEW_ENV_SUBNET_IDS }},SecurityGroupIds=${{ secrets.PR_REVIEW_ENV_SECURITY_GROUP_IDS }} | jq -r ".FunctionArn") + + aws lambda wait function-active --function-name rainbow-$GITHUB_SHA + + ./bin/set-rainbow-lambda-env-vars.sh rainbow-$GITHUB_SHA > /dev/null 2>&1 + + aws lambda add-permission \ + --function-name rainbow-$GITHUB_SHA \ + --statement-id rainbow-elb-invoke-permission \ + --principal elasticloadbalancing.amazonaws.com \ + --action lambda:InvokeFunction > /dev/null 2>&1 + + targetGroupArn=$(aws elbv2 create-target-group \ + --name rainbow-${GITHUB_SHA:0:24} \ + --target-type lambda | jq -r ".TargetGroups[0].TargetGroupArn") + + aws elbv2 register-targets \ + --target-group-arn $targetGroupArn \ + --targets Id=$lambdaArn + + aws elbv2 create-rule \ + --listener-arn $LISTENER_ARN \ + --conditions "[{\"Field\":\"host-header\",\"Values\":[\"forms-staging.cdssandbox.xyz\"]},{\"Field\":\"http-header\",\"HttpHeaderConfig\":{\"HttpHeaderName\":\"x-deployment-id\",\"Values\":[\"$GITHUB_SHA\"]}}]" \ + --priority 1 \ + --actions Type=forward,TargetGroupArn=$targetGroupArn \ + --tags Key=Name,Value=rainbow-$GITHUB_SHA > /dev/null 2>&1 \ No newline at end of file diff --git a/Dockerfile.rainbow b/Dockerfile.rainbow new file mode 100755 index 0000000000..cc9a508e88 --- /dev/null +++ b/Dockerfile.rainbow @@ -0,0 +1,41 @@ +ARG BASE_IMAGE +FROM $BASE_IMAGE as base + +FROM node:22-alpine as final +LABEL maintainer="-" + +ARG COGNITO_APP_CLIENT_ID +ENV COGNITO_APP_CLIENT_ID=$COGNITO_APP_CLIENT_ID + +ARG COGNITO_USER_POOL_ID +ENV COGNITO_USER_POOL_ID=$COGNITO_USER_POOL_ID + +ARG INDEX_SITE="false" +ENV INDEX_SITE=$INDEX_SITE + +ARG NEXT_DEPLOYMENT_ID +ENV NEXT_DEPLOYMENT_ID=$NEXT_DEPLOYMENT_ID + +ENV AWS_LWA_ENABLE_COMPRESSION=true +ENV HOSTNAME=localhost +ENV PORT=3000 + +# Use the LAMBDA_ENV environment variable to determine if the app is running in a Lambda environment +# Enables memory caching for the prerendering of pages +ENV LAMBDA_ENV=1 + +WORKDIR /src + +COPY --from=base /src/.next/standalone ./ +COPY --from=base /src/.next/static ./.next/static + +# Lambda web adapter: https://github.com/awslabs/aws-lambda-web-adapter +# The public.ecr.aws/awsguru/aws-lambda-adapter:0.7.0 image reference in the docs has +# been pushed to the public CDS ECR to avoid rate limiting when pulling the image. +COPY --from=public.ecr.aws/cds-snc/aws-lambda-adapter:0.7.0@sha256:00b1441858fb3f4ce3d67882ef6153bacf8ff4bb8bf271750c133667202926af /lambda-adapter /opt/extensions/lambda-adapter + +RUN ln -s /tmp ./.next/cache + +EXPOSE 3000 + +ENTRYPOINT [ "node", "server.js"] \ No newline at end of file diff --git a/bin/set-rainbow-lambda-env-vars.sh b/bin/set-rainbow-lambda-env-vars.sh new file mode 100755 index 0000000000..e8379c22c0 --- /dev/null +++ b/bin/set-rainbow-lambda-env-vars.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -euo pipefail + +LAMBDA_FUNCTION_NAME=$1 + +TASK_ARN="$(aws ecs list-tasks --cluster Forms --service-name form-viewer --output text --query 'taskArns[0]')" +TASK_DEF_ARN="$(aws ecs describe-tasks --cluster Forms --task "$TASK_ARN" --output text --query 'tasks[0].taskDefinitionArn')" +TASK_DEF="$(aws ecs describe-task-definition --task-definition "$TASK_DEF_ARN")" + +# Get environment variables from current task definition +ENV_VARS=$(echo "$TASK_DEF" | jq -r '.taskDefinition.containerDefinitions[0].environment | map("\(.name)=\(.value)") | join(",")') + +# Get secrets from current task definition +SECRET_VARS="$(echo "$TASK_DEF" | jq -r '.taskDefinition.containerDefinitions[0].secrets | flatten[] | [.name,.valueFrom] | join("=")')" +while IFS= read -r SECRET; do + SECRET_NAME="${SECRET%%=*}" + SECRET_ARN="${SECRET#*=}" + SECRET_VALUE="$(aws secretsmanager get-secret-value --secret-id "$SECRET_ARN" --query 'SecretString' --output text)" + ENV_VARS="$ENV_VARS"$','"$SECRET_NAME=$SECRET_VALUE" +done <<< "$SECRET_VARS" + +# Set Lambda environment variables +LAMBDA_ENV_VARS="$(echo "$ENV_VARS" | sort | sed -e "s/{/'{/" -e "s/}/}'/")" +aws lambda update-function-configuration --function-name $LAMBDA_FUNCTION_NAME --environment "Variables={$LAMBDA_ENV_VARS}" > /dev/null 2>&1 \ No newline at end of file