-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from moneymeets/feature/MD-7037-implement-ecs-d…
…eployment-workflow feat(action): add ecs deployment workflow
- Loading branch information
Showing
11 changed files
with
1,337 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
name: CI | ||
|
||
on: [ push ] | ||
|
||
jobs: | ||
lint-and-test: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
|
||
- uses: moneymeets/action-setup-python-poetry@master | ||
|
||
- uses: moneymeets/moneymeets-composite-actions/lint-python@master | ||
|
||
- run: poetry run python -m pytest --cov --cov-fail-under=95 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/.idea/* | ||
!/.idea/watcherTasks.xml | ||
__pycache__/ | ||
.coverage | ||
.tmp/ |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
name: "AWS ECS Task Deploy" | ||
description: "Deploy production image to AWS ECS" | ||
inputs: | ||
environment: | ||
description: Deployment environment | ||
required: true | ||
ecr_repository: | ||
description: ECR repository to pull image from | ||
required: true | ||
|
||
aws_access_key_id: | ||
description: AWS access key | ||
required: true | ||
aws_secret_access_key: | ||
description: AWS secret access key | ||
required: true | ||
aws_region: | ||
description: AWS region | ||
required: true | ||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- id: configure-aws-credentials | ||
uses: aws-actions/configure-aws-credentials@v4 | ||
with: | ||
aws-access-key-id: ${{ inputs.aws_access_key_id }} | ||
aws-secret-access-key: ${{ inputs.aws_secret_access_key }} | ||
aws-region: ${{ inputs.aws_region }} | ||
|
||
- id: login-ecr | ||
uses: aws-actions/amazon-ecr-login@v2 | ||
|
||
- name: Setup Python + Poetry | ||
uses: moneymeets/action-setup-python-poetry@master | ||
with: | ||
working_directory: ${{ github.action_path }} | ||
# ToDo: Re-enable cache when https://github.com/actions/setup-python/issues/361 is fixed | ||
poetry_cache_enabled: 'false' | ||
|
||
- id: get-image-uri | ||
uses: moneymeets/action-ecs-deploy/custom-deploy-steps/get-image-uri@master | ||
with: | ||
ecr_repository: ${{ inputs.ecr_repository }} | ||
aws_region: ${{ inputs.aws_region }} | ||
|
||
- name: Render and deploy local-exec task definition to Amazon ECS | ||
id: deploy-local-task-definition | ||
uses: moneymeets/action-ecs-deploy/custom-deploy-steps/create-task-definition@master | ||
with: | ||
application_id: ${{ inputs.ecr_repository }}-local-exec-${{ inputs.environment }} | ||
image_uri: ${{ steps.get-image-uri.outputs.image-uri }} | ||
aws_access_key_id: ${{ inputs.aws_access_key_id }} | ||
aws_secret_access_key: ${{ inputs.aws_secret_access_key }} | ||
aws_region: ${{ inputs.aws_region }} | ||
|
||
- name: Render and deploy task definition to Amazon ECS | ||
id: deploy-task-definition | ||
uses: moneymeets/action-ecs-deploy/custom-deploy-steps/create-task-definition@master | ||
with: | ||
application_id: ${{ inputs.ecr_repository }}-${{ inputs.environment }} | ||
image_uri: ${{ steps.get-image-uri.outputs.image-uri }} | ||
aws_access_key_id: ${{ inputs.aws_access_key_id }} | ||
aws_secret_access_key: ${{ inputs.aws_secret_access_key }} | ||
aws_region: ${{ inputs.aws_region }} | ||
|
||
# Service is managed by Pulumi, only desired count and task definition should be updated here | ||
- name: Update service | ||
shell: bash | ||
run: | | ||
aws ecs update-service \ | ||
--health-check-grace-period-seconds 900 \ | ||
--task-definition "${{ steps.deploy-task-definition.outputs.latest-task-definition-arn }}" \ | ||
--cluster ${{ inputs.environment }} \ | ||
--service ${{ inputs.ecr_repository }}-${{ inputs.environment }} \ | ||
--desired-count 1 \ | ||
--region ${{ inputs.aws_region }} | ||
- name: Await service stability | ||
shell: bash | ||
# ToDo: MD-7199 Re-evaluate logic after deployment failures are correctly handled by ECS | ||
id: check-service-stability | ||
run: | | ||
aws ecs wait services-stable \ | ||
--cluster ${{ inputs.environment }} \ | ||
--service ${{ inputs.ecr_repository }}-${{ inputs.environment }} \ | ||
--region ${{ inputs.aws_region }} | ||
- name: Deregister previous local-exec task definition | ||
if: ${{ always() && steps.deploy-local-task-definition.outputs.previous-task-definition-arn != '' }} | ||
shell: bash | ||
run: | | ||
if [ "${{ steps.check-service-stability.outcome }}" == 'success' ]; then | ||
TASK_DEFINITION_TO_DEREGISTER="${{ steps.deploy-local-task-definition.outputs.previous-task-definition-arn }}" | ||
else | ||
TASK_DEFINITION_TO_DEREGISTER="${{ steps.deploy-local-task-definition.outputs.latest-task-definition-arn }}" | ||
fi | ||
aws ecs deregister-task-definition \ | ||
--task-definition "$TASK_DEFINITION_TO_DEREGISTER" \ | ||
--region ${{ inputs.aws_region }} | ||
- name: Deregister previous task definition | ||
if: ${{ always() && steps.deploy-task-definition.outputs.previous-task-definition-arn != '' }} | ||
shell: bash | ||
run: | | ||
if [ "${{ steps.check-service-stability.outcome }}" == 'success' ]; then | ||
TASK_DEFINITION_TO_DEREGISTER="${{ steps.deploy-task-definition.outputs.previous-task-definition-arn }}" | ||
else | ||
TASK_DEFINITION_TO_DEREGISTER="${{ steps.deploy-task-definition.outputs.latest-task-definition-arn }}" | ||
fi | ||
aws ecs deregister-task-definition \ | ||
--task-definition "$TASK_DEFINITION_TO_DEREGISTER" \ | ||
--region ${{ inputs.aws_region }} |
Empty file.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import os | ||
from dataclasses import dataclass | ||
|
||
import boto3 | ||
import click | ||
from botocore.client import BaseClient | ||
|
||
|
||
class NonSingleValueError(Exception): | ||
pass | ||
|
||
|
||
@dataclass | ||
class Tag: | ||
key: str | ||
value: str | ||
|
||
|
||
def format_tags(task_definition_tags: str) -> tuple[Tag, ...]: | ||
try: | ||
return tuple( | ||
Tag(tag.strip().split(":")[0], tag.strip().split(":")[1]) for tag in task_definition_tags.split(",") | ||
) | ||
except IndexError: | ||
raise ValueError( | ||
"Invalid task definition tag format, expects 'key:value,key:value'", | ||
) | ||
|
||
|
||
def get_active_task_definition_arn_by_tag( | ||
*, | ||
ecs_client: BaseClient, | ||
task_definition_family_prefix: str, | ||
task_definition_tags: str, | ||
allow_initial_deployment: bool, | ||
) -> str: | ||
tags = format_tags(task_definition_tags) | ||
|
||
active_task_definition_arns = ecs_client.list_task_definitions( | ||
familyPrefix=task_definition_family_prefix, | ||
status="ACTIVE", | ||
sort="DESC", | ||
)["taskDefinitionArns"] | ||
|
||
tagged_active_task_definitions = tuple( | ||
task_definition["taskDefinition"]["taskDefinitionArn"] | ||
for task_definition_arn in active_task_definition_arns | ||
if ( | ||
task_definition := ecs_client.describe_task_definition( | ||
taskDefinition=task_definition_arn, | ||
include=["TAGS"], | ||
) | ||
) | ||
and all({"key": tag.key, "value": tag.value} in task_definition["tags"] for tag in tags) | ||
) | ||
|
||
# Allows for initial deployment of task definition. | ||
# Requires that the only other active task definition is to be created by Pulumi, | ||
# in order to prevent multiple deployed task definitions with different tags. | ||
if ( | ||
allow_initial_deployment | ||
and len(active_task_definition_arns) == 1 | ||
and {"key": "created_by", "value": "Pulumi"} | ||
in ecs_client.describe_task_definition( | ||
taskDefinition=active_task_definition_arns[0], | ||
include=["TAGS"], | ||
)["tags"] | ||
): | ||
return "" | ||
|
||
try: | ||
(task_definition,) = tagged_active_task_definitions | ||
return task_definition | ||
except ValueError as e: | ||
raise NonSingleValueError( | ||
f"Expected exactly one active task definition with tags {task_definition_tags}. " | ||
f"Found: {tagged_active_task_definitions}", | ||
) from e | ||
|
||
|
||
@click.group() | ||
def cli(): | ||
pass | ||
|
||
|
||
@cli.command( | ||
name="get-active-task-definition-arn-by-tag", | ||
short_help="Get active task definition ARN by specified tags", | ||
) | ||
@click.option("--application-id", default=os.environ.get("APPLICATION_ID"), type=str) | ||
@click.option("--tags", default=os.environ.get("TAGS"), type=str) | ||
@click.option("--aws-region", default=os.environ.get("AWS_DEFAULT_REGION"), type=str) | ||
@click.option("--allow-initial-deployment", is_flag=True, default=False) | ||
def cmd_get_active_task_definition_arn_by_tag( | ||
application_id: str, | ||
tags: str, | ||
aws_region: str, | ||
allow_initial_deployment: bool, | ||
): | ||
click.echo( | ||
get_active_task_definition_arn_by_tag( | ||
ecs_client=boto3.Session(region_name=aws_region).client("ecs"), | ||
task_definition_family_prefix=application_id, | ||
task_definition_tags=tags, | ||
allow_initial_deployment=allow_initial_deployment, | ||
), | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
cli() |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
name: Create task definition revision | ||
description: Use image_uri to create new task definition revision, and deregister previous active task definition | ||
|
||
inputs: | ||
application_id: | ||
description: Application ID also the task definition name in this case | ||
required: true | ||
image_uri: | ||
description: The URI of the container image to insert into the ECS task definition | ||
required: true | ||
|
||
aws_access_key_id: | ||
description: AWS access key | ||
required: true | ||
aws_secret_access_key: | ||
description: AWS secret access key | ||
required: true | ||
aws_region: | ||
description: AWS region | ||
required: true | ||
|
||
outputs: | ||
previous-task-definition-arn: | ||
description: "ARN of previous task definition revision" | ||
value: ${{ steps.get-github-action-task-definition-arn.outputs.previous-task-definition-arn }} | ||
latest-task-definition-arn: | ||
description: "ARN of new task definition revision" | ||
value: ${{ steps.deploy-task-definition.outputs.task-definition-arn }} | ||
|
||
runs: | ||
using: composite | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Get latest active task definition revision created by Pulumi | ||
id: get-pulumi-task-definition-arn | ||
shell: bash | ||
env: | ||
APPLICATION_ID: ${{ inputs.application_id }} | ||
AWS_REGION: ${{ inputs.aws_region }} | ||
run: | | ||
TASK_DEFINITION_ARN=$( | ||
poetry run --directory ${{ github.action_path }} actions_helper get-active-task-definition-arn-by-tag \ | ||
--application-id "$APPLICATION_ID" \ | ||
--tags "created_by:Pulumi,Name:$APPLICATION_ID" \ | ||
--aws-region "$AWS_REGION" | ||
) | ||
echo "task-definition-arn=${TASK_DEFINITION_ARN}" >> $GITHUB_OUTPUT | ||
- name: Download and check task definition | ||
shell: bash | ||
run: | | ||
aws ecs describe-task-definition \ | ||
--task-definition ${{ steps.get-pulumi-task-definition-arn.outputs.task-definition-arn }} \ | ||
--query taskDefinition \ | ||
--region ${{ inputs.aws_region }} \ | ||
> task-definition.json | ||
result=$(jq '.' task-definition.json | jq '.containerDefinitions[].image | select(. != "PLACEHOLDER")') | ||
if [ "$result" ]; then | ||
echo "Error: Not all values for containerDefinitions 'image' equal to 'PLACEHOLDER'" | ||
exit 1 | ||
fi | ||
- name: Replace PLACEHOLDER in task definition with image URI | ||
shell: bash | ||
run: | | ||
jq --arg image_uri "${{ inputs.image_uri }}" '.containerDefinitions[].image |= $image_uri' \ | ||
task-definition.json > tmpfile && mv tmpfile task-definition.json | ||
- name: Get latest active task definition created by GitHub Actions deployment | ||
id: get-github-action-task-definition-arn | ||
shell: bash | ||
env: | ||
APPLICATION_ID: ${{ inputs.application_id }} | ||
run: | | ||
TASK_DEFINITION_ARN=$( | ||
poetry run --directory ${{ github.action_path }} actions_helper get-active-task-definition-arn-by-tag \ | ||
--application-id "$APPLICATION_ID" \ | ||
--tags "created_by:GitHub Actions Deployment,Name:$APPLICATION_ID" \ | ||
--aws-region "$AWS_REGION" \ | ||
--allow-initial-deployment | ||
) | ||
echo "previous-task-definition-arn=${TASK_DEFINITION_ARN}" >> $GITHUB_OUTPUT | ||
- uses: aws-actions/amazon-ecs-deploy-task-definition@v1 | ||
id: deploy-task-definition | ||
with: | ||
task-definition: task-definition.json | ||
|
||
- name: Add tags to the new task definition revision | ||
shell: bash | ||
run: | | ||
aws ecs tag-resource \ | ||
--resource-arn ${{ steps.deploy-task-definition.outputs.task-definition-arn }} \ | ||
--tags key=created_by,value='GitHub Actions Deployment' key=Name,value=${{ inputs.application_id }} \ | ||
--region ${{ inputs.aws_region }} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
name: Get image URI | ||
description: Get image uri from AWS ECR | ||
|
||
inputs: | ||
ecr_repository: | ||
description: ECR repository to pull image from | ||
required: true | ||
aws_region: | ||
description: AWS region | ||
required: true | ||
|
||
outputs: | ||
image-uri: | ||
description: "Image URI" | ||
value: ${{ steps.get-image-uri.outputs.image-uri }} | ||
|
||
runs: | ||
using: composite | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Get Image URI | ||
shell: bash | ||
id: get-image-uri | ||
run: | | ||
imageTag=master-${{ github.sha }} | ||
IMAGE_TAGS=$( | ||
aws ecr describe-images \ | ||
--repository-name ${{ inputs.ecr_repository }} \ | ||
--image-ids imageTag="${imageTag}" \ | ||
--query 'imageDetails[*].imageTags[0]' \ | ||
--output json \ | ||
--region ${{ inputs.aws_region }} | ||
) | ||
REPOSITORY_URI=$( | ||
aws ecr describe-repositories \ | ||
--repository-name ${{ inputs.ecr_repository }} \ | ||
--query 'repositories[0].repositoryUri' \ | ||
--output text \ | ||
--region ${{ inputs.aws_region }} | ||
) | ||
IMAGE_URI=$(jq --arg uri "${REPOSITORY_URI}" '.[] | ($uri + ":" + .)' <<< "${IMAGE_TAGS}") | ||
echo "image-uri=${IMAGE_URI}" >> $GITHUB_OUTPUT |
Oops, something went wrong.