concord-consortium/lara

View on GitHub
.github/workflows/actions/deploy-rails-app-to-aws/action.yml

Summary

Maintainability
Test Coverage
name: Deploy Rails App to AWS
description: Deploy containerized Rails application to AWS ECS environment using CloudFormation
inputs:
  aws-access-key-id:
    required: true
  aws-secret-access-key:
    required: true
  aws-region:
    required: true
  cf-stack-name:
    required: true
  # String prefix for specifiying the app's Docker image CloudFormation template parameter (i.e. "Lara" for 
  # "LaraDockerImage" or "Portal" for "PortalDockerImage")
  docker-image-template-key-prefix:
    required: true
runs:
  using: "composite"
  steps:
    - name: 🧐 Ensure deployment environment has non-empty value
      run: |-
        if [[ "${{ github.event.inputs.environment }}" == "" ]]; then
          test # Fail if it is blank
        fi
        echo "Deploying to environment '${{ github.event.inputs.environment }}'"
      shell: bash
    - name: 👉 Select version to deploy or use latest version of project released
      shell: bash
      run: |-
        if [[ -n "${{ github.event.inputs.version }}" ]]; then
          echo "User selected to deploy release version '${{ github.event.inputs.version }}'"
          echo -n "selected-app-version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
        # Otherwise use latest pre-release/stable release version...
        else
          echo "User did not select a release version to deploy."
          echo -n "Querying API for latest "
          # If environment name/URL contains ".staging." subdomain
          if [[ "${{ github.event.inputs.environment }}" == *".staging."* ]]; then
            echo -n "pre-release version"
            latest_github_release_ver=$(gh release list | grep -oP 'Pre-release\s+v\K\d+\.\d+\.\d-pre\.\d+' | sort -uVr | head -n 1 | tr -d '\n')
          # Otherwise assume that it's a production deployment environment
          else
            echo -n "release version"
            latest_github_release_ver=$(gh release view --json 'tagName' --jq '.tagName[1:]' | tr -d '\n')
          fi
          echo "..."
          echo "Determined latest pre-release/release version is '$latest_github_release_ver'"
          echo -n "selected-app-version=$latest_github_release_ver" >> "$GITHUB_OUTPUT"
        fi
      id: selected-app-version
    - name: 🧐 Ensure app version is set to a non-empty value
      shell: bash
      run: |-
        if [[ "${{ steps.selected-app-version.outputs.selected-app-version }}" == "" ]]; then
          echo "Unable to determine an application version to deploy and application version may not be left blank"
          test # Fail if it is blank
        fi
    - name: 🔐 Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v3
      with:
        aws-access-key-id: ${{ inputs.aws-access-key-id }}
        aws-secret-access-key: ${{ inputs.aws-secret-access-key }}
        aws-region: ${{ inputs.aws-region }}
    - name: 🔍 Lookup ECS cluster name for selected CloudFormation stack environment
      id: ecs-cluster-name
      shell: bash
      run: |-
        aws cloudformation describe-stacks \
          --stack-name \
            $(aws cloudformation describe-stacks \
              --stack-name ${{ inputs.cf-stack-name }} \
              --query "Stacks[0].Parameters[?ParameterKey=='ClusterStackName'].ParameterValue" \
              --output text | tr -d '\n') \
          --output text \
          --query "Stacks[0].Parameters[?ParameterKey=='EcsClusterName'].ParameterValue" | \
            awk '{printf "ecs-cluster-name=" $1}' >> "$GITHUB_OUTPUT"
    - name: 🛠️ Create modified copy of 'App' ECS task definition
      shell: bash
      run: |-
        aws ecs describe-task-definition \
          --task-definition '${{ inputs.cf-stack-name }}-App' \
          --query 'taskDefinition' | \
          jq -r '.containerDefinitions |= map(.name = "apply-rails-migrations" | .image = "${{ env.CONTAINER_IMAGE }}:${{ steps.selected-app-version.outputs.selected-app-version }}" | .entryPoint = ["bundle", "exec"] | .command = ["rake", "db:migrate"]) | del(.taskDefinitionArn) | del(.registeredBy)' > /tmp/rails_migration_task_definition.json
    - name: 📝 Register new ECS task definition for running database schema migrations
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: /tmp/rails_migration_task_definition.json
        cluster: ${{ steps.ecs-cluster-name.outputs.ecs-cluster-name }}
      id: register-ecs-task-def
    - name: 🏃 Run ECS task to apply database schema migrations
      shell: bash
      run: |-
        # See https://stackoverflow.com/a/48314818
        migrations_task_id=$(aws ecs run-task \
          --cluster '${{ steps.ecs-cluster-name.outputs.ecs-cluster-name }}' \
          --task-definition '${{ steps.register-ecs-task-def.outputs.task-definition-arn }}' \
          --output text \
          --query 'tasks[0].taskArn' | cut -d '/' -f 3)
        echo "Waiting for migrations to finish applying (running as ECS task with ID '$migrations_task_id'..."
        aws ecs wait tasks-stopped \
          --tasks "$migrations_task_id" \
          --cluster '${{ steps.ecs-cluster-name.outputs.ecs-cluster-name }}'
        echo "Migration task completed:"
        aws ecs describe-tasks --query 'tasks[0].{ExitCode: containers[0].exitCode, StoppedReason: stoppedReason}' --tasks $migrations_task_id --cluster '${{ steps.ecs-cluster-name.outputs.ecs-cluster-name }}' | tee /tmp/task_exit_status.json
        exit_code=$(jq -r '.ExitCode' /tmp/task_exit_status.json)
        stopped_reason=$(jq -r '.StoppedReason' /tmp/task_exit_status.json)
        if [[ "$exit_code" != "0" ]] || [[ "$stopped_reason" != "Essential container in task exited" ]]; then
          echo "Rails migration task failed to complete running successfully. See ECS task logs for more details: https://${{ inputs.aws-region }}.console.aws.amazon.com/ecs/v2/clusters/staging/tasks/$migrations_task_id/logs?region=${{ inputs.aws-region }}"
          exit 1
        else
          echo "Migrations task completed successfully, deploying CloudFormation stack..."
        fi
    - name: 🚀 Deploy to AWS CloudFormation & watch stack events
      shell: bash
      # Ideally it would be nicer to use the 'aws-actions/aws-cloudformation-github-deploy' Action but it currently
      # has an issue of being unable to inherrit current CloudFormation stack parameters and only modify a select few
      # params.
      # See: https://github.com/aws-actions/aws-cloudformation-github-deploy/issues/99#issue-1694259285
      #uses: aws-actions/aws-cloudformation-github-deploy@v1
      #with:
      #  name: ${{ inputs.cf-stack-name }}
      #  template: ./configs/cloudformation/stack_template.yml
      #  parameter-overrides: "LaraDockerImage=${{ env.CONTAINER_IMAGE }}"
      run: |-
        npm install -g tail-stack-events
        aws cloudformation update-stack \
          --template-body file://$(pwd)/configs/cloudformation/stack_template.yml \
          --stack-name '${{ inputs.cf-stack-name }}' \
          --parameters $(aws cloudformation describe-stacks --stack-name '${{ inputs.cf-stack-name }}' --query 'Stacks[0].Parameters[].ParameterKey' --output json | jq -r 'map("ParameterKey=" + . + ",UsePreviousValue=true") | join(" ")') ParameterKey=${{ inputs.docker-image-template-key-prefix }}DockerImage,ParameterValue=${{ env.CONTAINER_IMAGE }}:${{ steps.selected-app-version.outputs.selected-app-version }}
        tail-stack-events \
          --stack-name ${{ inputs.cf-stack-name }} \
          --follow \
          --die

        # Get the last status of the CloudFormation stack
        status=$(aws cloudformation describe-stacks \
          --stack-name '${{ inputs.cf-stack-name }}' \
          --query 'Stacks[0].StackStatus' \
          --output text)

        if [ "$status" == "UPDATE_COMPLETE" ]; then
          echo "Stack update succeeded."
        elif [[ "$status" == *"ROLLBACK"* ]]; then
          echo "Stack update failed and was rolled back"
          exit 1
        else
          echo "Stack has unexpected state: $status"
          exit 1
        fi