diff --git a/.config/github-actions-schedule.json b/.config/github-actions-schedule.json new file mode 100644 index 0000000..14b290d --- /dev/null +++ b/.config/github-actions-schedule.json @@ -0,0 +1,6 @@ +{ + "containerDefinitions": [ + { + } + ] +} \ No newline at end of file diff --git a/.github/workflows/dev-build-upload.yml b/.github/workflows/dev-build-upload.yml new file mode 100644 index 0000000..2f0176f --- /dev/null +++ b/.github/workflows/dev-build-upload.yml @@ -0,0 +1,23 @@ +name: Demo Deployment Workflow + +on: + pull_request: + branches: + - main + +jobs: + build-push: + uses: ./.github/workflows/docker-build-upload.yml + with: + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + AWS_ROLE_TO_ASSUME: ${{ vars.AWS_ROLE_TO_ASSUME }} + ECR_REPOSITORY: 'gs-sandbox-github-actions' + NAME_PREFIX: 'gs-sandbox' + + DEPLOY_SCHEDULED_TASK: true # Only when you want to deploy scheduled tasks for the app + EXECUTION_NAME: 'github-actions-schedule' # It can be a list separated by ';', only when you want to deploy scheduled tasks + + DEPLOY_ECS_SERVICE: true # Only when you want to deploy a ecs service for the app + SERVICE_NAME: 'demo-svc' # Only when you want to deploy a ecs service + ECS_CLUSTER_NAME: 'gs-sandbox-ecs-cluster' # Only when you want to deploy a ecs service + ECS_DEV_DEPLOY: true # Only when you want to deploy a dev ecs service for the app \ No newline at end of file diff --git a/.github/workflows/docker-build-upload.yml b/.github/workflows/docker-build-upload.yml index 231c6e5..9258a52 100644 --- a/.github/workflows/docker-build-upload.yml +++ b/.github/workflows/docker-build-upload.yml @@ -8,13 +8,13 @@ name: Docker Build & Push # ##### GITHUB REQUIRED VARS ##### # AWS_DEFAULT_REGION: Already available in all /gsoftcolombia projects. # AWS_ROLE_TO_ASSUME: ARN of the role that aws-actions can assume. -# REPO_NAME: ECR Repository Name -# APP_NAME_PREFIX: Prefix of the task definition in AWS. -# APP_NAME_SUFFIX: Suffix/Postfix of the task definition in AWS. +# ECR_REPOSITORY: ECR Repository Name +# NAME_PREFIX: Prefix of the task definition in AWS. +# EXECUTION_NAME: Suffix/Postfix of the task definition in AWS. # - If there are multiple task-definitions you can add them separated by ';' e.g. customers;products # - If the Action is executed from Main, it expects to update a task definition -# with the name {APP_NAME_PREFIX}-prod-{APP_NAME_SUFFIX} -#  otherwise, {APP_NAME_PREFIX}-dev-{APP_NAME_SUFFIX} +# with the name {NAME_PREFIX}-prod-{EXECUTION_NAME} +#  otherwise, {NAME_PREFIX}-dev-{EXECUTION_NAME} # ##### USAGE TEMPLATE ##### # on: @@ -31,10 +31,14 @@ name: Docker Build & Push # with: # AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} # AWS_ROLE_TO_ASSUME: ${{ vars.AWS_ROLE_TO_ASSUME }} -# REPO_NAME: ${{ vars.REPO_NAME }} -# APP_NAME_PREFIX: ${{ vars.APP_NAME_PREFIX }} # Required for both ECS Services and Scheduled Tasks -# APP_NAME_SUFFIX: ${{ vars.APP_NAME_SUFFIX }} # Only when you want to deploy scheduled tasks -# ECS_SERVICE_NAME: ${{ vars.ECS_SERVICE_NAME }} # Only when you want to deploy ECS Services +# ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} +# NAME_PREFIX: ${{ vars.NAME_PREFIX }} # Required for both ECS Services and Scheduled Tasks +# +# DEPLOY_SCHEDULED_TASK: true # Only when you want to deploy scheduled tasks for the app +# EXECUTION_NAME: ${{ vars.EXECUTION_NAME }} # Only when you want to deploy scheduled tasks +# +# DEPLOY_ECS_SERVICE: true # Only when you want to deploy a ecs service for the app +# SERVICE_NAME: ${{ vars.SERVICE_NAME }} # Only when you want to deploy ECS Services # ECS_CLUSTER_NAME: ${{ vars.ECS_CLUSTER_NAME }} # Only when you want to deploy ECS Services # ECS_DEV_DEPLOY: true # Only when you want to deploy a dev ECS Service for the app @@ -49,24 +53,32 @@ on: AWS_ROLE_TO_ASSUME: required: true type: string - REPO_NAME: + ECR_REPOSITORY: required: true type: string - APP_NAME_PREFIX: - required: false + NAME_PREFIX: + required: true type: string - APP_NAME_SUFFIX: + + DEPLOY_SCHEDULED_TASK: + required: true + type: boolean + EXECUTION_NAME: required: false type: string - default: '' - ECS_SERVICE_NAME: + default: 'missing_execution_name' # Only when you want to deploy scheduled tasks, it can be a list separated by ';' + + DEPLOY_ECS_SERVICE: + required: true + type: boolean + SERVICE_NAME: required: false type: string - default: '' + default: 'missing_service_name' # Only when you want to deploy a ecs service for the app ECS_CLUSTER_NAME: - required: false # Required if there is ECS_SERVICE_NAME + required: false # Required if there is SERVICE_NAME type: string - default: '' + default: 'missing_cluster_name' # Only when you want to deploy a ecs service for the app ECS_DEV_DEPLOY: required: false type: boolean @@ -110,49 +122,73 @@ jobs: echo "TASK_ENV=$TASK_ENV" >> $GITHUB_OUTPUT - name: Build and Push to ECR - continue-on-error: true # for cases when the image is already pushed id: build env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: ${{ inputs.REPO_NAME }} + ECR_REPOSITORY: ${{ inputs.ECR_REPOSITORY }} run: | IMAGE_ENV=$([ "$GITHUB_REF" = "refs/heads/main" ] && echo "prod" || echo "dev") IMAGE_TAG="${IMAGE_ENV}-${{ steps.commit-sha.outputs.COMMIT_SHORT_SHA }}" docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + aws ecr batch-delete-image --repository-name $ECR_REPOSITORY --image-ids imageTag=$IMAGE_TAG 2>/dev/null || true docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "FULL_IMAGE_URI=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT - - name: Update ECS Schedule Tasks # It only runs when there are APPs specified (SUFFIX) - if: ${{ inputs.APP_NAME_SUFFIX != '' }} - id: update-schedules + - name: Deploy ECS Schedule Tasks + if: ${{ inputs.DEPLOY_SCHEDULED_TASK == true }} + id: deploy-schedules run: | echo "Updating Task Definitions" export IFS=";" - SUFFIXES="${{ inputs.APP_NAME_SUFFIX }}" - for app in $SUFFIXES; do + list_execution_name="${{ inputs.EXECUTION_NAME }}" + for execution_name in $list_execution_name; do + config_file=".config/${execution_name}.json" + if [ ! -f "$config_file" ]; then + echo "ERROR: Missing scheduled task config file: $config_file" + exit 1 + fi + if ! aws ecs describe-task-definition \ - --task-definition ${{ inputs.APP_NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${app} \ + --task-definition ${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${execution_name} \ --region=${{ inputs.AWS_DEFAULT_REGION }} &> /dev/null; then - echo "WARNING: Task definition $TASK_DEFINITION does not exist." + echo "ERROR: Task definition ${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${execution_name} does not exist." + exit 1 else - echo "Updating Task-Definition for: ${app} ..." - TASK_DEFINITION=$(aws ecs describe-task-definition --task-definition ${{ inputs.APP_NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${app} --region=${{ inputs.AWS_DEFAULT_REGION }}) - NEW_TASK_DEFINITION=$(echo $TASK_DEFINITION | jq --arg IMAGE "${{ steps.build.outputs.FULL_IMAGE_URI }}" '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') + echo "Updating Task-Definition for: ${execution_name} ..." + TASK_DEFINITION=$(aws ecs describe-task-definition --task-definition ${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${execution_name} --region=${{ inputs.AWS_DEFAULT_REGION }}) + updated_task_definition=$(echo "$TASK_DEFINITION" | jq --slurpfile config ".config/${execution_name}.json" ' + .taskDefinition as $base + | ($config[0] // {}) as $updates + | ($updates.containerDefinitions // []) as $container_updates + | ($base * ($updates | del(.containerDefinitions))) as $merged + | if ($container_updates | length) > 0 then + $merged + | .containerDefinitions = [ + range(0; ($merged.containerDefinitions | length)) as $index + | (($merged.containerDefinitions[$index] // {}) * ($container_updates[$index] // {})) + ] + else + $merged + end + ') + NEW_TASK_DEFINITION=$(echo "$updated_task_definition" | jq --arg IMAGE "${{ steps.build.outputs.FULL_IMAGE_URI }}" '.containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') aws ecs register-task-definition --region ${{ inputs.AWS_DEFAULT_REGION }} --cli-input-json "$NEW_TASK_DEFINITION" fi done - - name: Render Amazon ECS Service Task Definition # It only runs when there is an ECS_SERVICE_NAME specified. - if: ${{ inputs.ECS_SERVICE_NAME != '' }} + - name: Render Amazon ECS Service Task Definition # It only runs when there is an SERVICE_NAME specified. + if: ${{ inputs.DEPLOY_ECS_SERVICE == true }} id: render-web-container uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition-family: ${{ inputs.APP_NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.ECS_SERVICE_NAME }} - container-name: ${{ inputs.APP_NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.ECS_SERVICE_NAME }} + with: + task-definition-family: ${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.SERVICE_NAME }} + container-name: ${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.SERVICE_NAME }} image: ${{ steps.build.outputs.FULL_IMAGE_URI }} + environment-variables: | + ENVIRONMENT=${{ steps.commit-sha.outputs.TASK_ENV }} - name: Generate appspec.yaml dynamically - if: ${{ inputs.ECS_SERVICE_NAME != '' }} + if: ${{ inputs.DEPLOY_ECS_SERVICE == true }} id: generate-appspec run: | cat < appspec.yaml @@ -161,19 +197,19 @@ jobs: - TargetService: Type: AWS::ECS::Service Properties: - TaskDefinition: "${{ inputs.APP_NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.ECS_SERVICE_NAME }}" + TaskDefinition: "${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.SERVICE_NAME }}" LoadBalancerInfo: - ContainerName: "${{ inputs.APP_NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.ECS_SERVICE_NAME }}" + ContainerName: "${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.SERVICE_NAME }}" ContainerPort: 80 EOF - name: Deploy to Amazon ECS service - if: ${{ inputs.ECS_SERVICE_NAME != '' && (steps.commit-sha.outputs.TASK_ENV == 'prod' || ( steps.commit-sha.outputs.TASK_ENV == 'dev' && inputs.ECS_DEV_DEPLOY == true )) }} + if: ${{ inputs.DEPLOY_ECS_SERVICE == true && (steps.commit-sha.outputs.TASK_ENV == 'prod' || ( steps.commit-sha.outputs.TASK_ENV == 'dev' && inputs.ECS_DEV_DEPLOY == true )) }} uses: aws-actions/amazon-ecs-deploy-task-definition@v2 with: task-definition: ${{ steps.render-web-container.outputs.task-definition }} - service: ${{ inputs.APP_NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.ECS_SERVICE_NAME }} + service: ${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.SERVICE_NAME }} cluster: ${{ inputs.ECS_CLUSTER_NAME }} - wait-for-service-stability: true - codedeploy-application: ${{ inputs.APP_NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.ECS_SERVICE_NAME }} - codedeploy-deployment-group: ${{ inputs.APP_NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.ECS_SERVICE_NAME }} \ No newline at end of file + wait-for-service-stability: false + codedeploy-application: ${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.SERVICE_NAME }} + codedeploy-deployment-group: ${{ inputs.NAME_PREFIX }}-${{ steps.commit-sha.outputs.TASK_ENV }}-${{ inputs.SERVICE_NAME }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cfbdd3f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM httpd:latest + +COPY ./entrypoint.sh /app/ +RUN chmod +x /app/entrypoint.sh + +ENTRYPOINT ["/app/entrypoint.sh"] +CMD ["httpd-foreground"] \ No newline at end of file diff --git a/README.md b/README.md index ddc0a08..aaa30a9 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,56 @@ This is the main repo where our github actions and also general technical documentation are located. +## Reusable Workflows + +### Docker Build & Push + +This reusable workflow builds a Dockerfile located in the root of the project, pushes the new Docker image to an ECR Registry, and optionally updates ECS task definitions for services or scheduled tasks. + +For scheduled tasks, the caller repository can define runtime overrides under `.config/`: + +- Scheduled tasks must provide `.config/.json`. + +The file is merged with the current active task definition before the image tag is updated, so the application repository owns the scheduled-task runtime fields without Terraform managing them. + +#### Usage + +To use this workflow, call it from another workflow file as shown in `.github/workflows/dev-build-upload.yml`: + +```yaml +jobs: + build-push: + uses: gsoftcolombia/github-actions/.github/workflows/docker-build-upload.yml@main + with: + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + AWS_ROLE_TO_ASSUME: ${{ vars.AWS_ROLE_TO_ASSUME }} + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} + NAME_PREFIX: ${{ vars.NAME_PREFIX }} + + DEPLOY_SCHEDULED_TASK: true + EXECUTION_NAME: ${{ vars.EXECUTION_NAME }} + + DEPLOY_ECS_SERVICE: false + SERVICE_NAME: ${{ vars.SERVICE_NAME }} + ECS_CLUSTER_NAME: ${{ vars.ECS_CLUSTER_NAME }} + ECS_DEV_DEPLOY: false +``` + +#### Required Variables + +- `AWS_DEFAULT_REGION`: AWS region (available in all /gsoftcolombia projects). +- `AWS_ROLE_TO_ASSUME`: ARN of the role for aws-actions to assume. +- `ECR_REPOSITORY`: ECR Repository Name. +- `NAME_PREFIX`: Prefix of the task definition in AWS. + +#### Optional Inputs + +- `DEPLOY_SCHEDULED_TASK`: Set to true to deploy scheduled tasks. +- `EXECUTION_NAME`: Suffix for scheduled task definitions (can be a list separated by ';'). +- `DEPLOY_ECS_SERVICE`: Set to true to deploy an ECS service. +- `SERVICE_NAME`: Name of the ECS service. +- `ECS_CLUSTER_NAME`: Name of the ECS cluster. +- `ECS_DEV_DEPLOY`: Set to true to deploy to dev environment. + +For more details, see the workflow file at `.github/workflows/docker-build-upload.yml`. + diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..f64c689 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e # Exit on error + +name_prefix="gs-sandbox" +environment="${environment}" +execution_name="demo" + +echo "This is ${name_prefix}-${environment}-${execution_name} and it is ready to start..." + +# Run the main application command +exec "$@" \ No newline at end of file