.github/workflows/actions/deploy-rails-app-to-aws/action.yml
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