Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
IamAbbey committed Feb 9, 2024
1 parent 43d61f8 commit 7aee214
Show file tree
Hide file tree
Showing 10 changed files with 1,302 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/.idea/*
!/.idea/watcherTasks.xml
__pycache__/
.coverage
.tmp/
113 changes: 113 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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: Get Image URI
shell: bash
id: get-image-uri
run: |
# For testing purposes ( actual value: =master-${{ github.sha }} )
imageTag=$(echo ${{ github.ref_name }} | awk '{print tolower($0)}' | sed -e 's|/|-|g')
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
- name: Render and Deploy local-exec task definition to Amazon ECS
id: deploy-local-task-definition
uses: ./custom-deploy-steps/ecs-register-task-definition.yml
working-directory: ${{ github.action_path }}
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: ./custom-deploy-steps/ecs-register-task-definition.yml
working-directory: ${{ github.action_path }}
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 }}

- 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 \
--propagate-tags TASK_DEFINITION \
--region ${{ inputs.aws_region }}
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()
shell: bash
run: |
aws ecs deregister-task-definition \
--task-definition "${{ steps.deploy-local-task-definition.outputs.previous-task-definition-arn }}" \
--region ${{ inputs.aws_region }}
- name: Deregister previous task definition
if: always()
shell: bash
run: |
aws ecs deregister-task-definition \
--task-definition "${{ steps.deploy-task-definition.outputs.previous-task-definition-arn }}" \
--region ${{ inputs.aws_region }}
Empty file added actions_helper/__init__.py
Empty file.
109 changes: 109 additions & 0 deletions actions_helper/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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"]

active_task_definitions = [
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)
]

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,) = 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: {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,
):
ecs_client: BaseClient = boto3.Session(region_name=aws_region).client("ecs")
click.echo(
get_active_task_definition_arn_by_tag(
ecs_client=ecs_client,
task_definition_family_prefix=application_id,
task_definition_tags=tags,
allow_initial_deployment=allow_initial_deployment,
),
)


if __name__ == "__main__":
cli()
109 changes: 109 additions & 0 deletions custom-deploy-steps/ecs-register-task-definition.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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.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:
- 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 }}

- 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'

- 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 "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 }}
Loading

0 comments on commit 7aee214

Please sign in to comment.