Categorias
Programação Technology

GitHub Actions workflow for deploying a scheduled task using AWS ECS and EventBridge Scheduler

Cron jobs are repetitive tasks scheduled to run periodically at fixed times, dates, or intervals. It typically automates system maintenance or administration. Some workloads that are still running on non-containerized platforms (VMs, bare metal, etc.) are suitable to be moved to Serveless with multiple alternatives, depending on the context of each task.

Considering AWS services, for most of the options EventBridge Scheduler will be used to manage tasks as it is capable of invoking lots of AWS services. One of them is invoking a containerized application, or ECS task.

Amazon EventBridge Scheduler is a serverless scheduler that allows you to create, run, and manage tasks from one central, managed service. Highly scalable, EventBridge Scheduler allows you to schedule millions of tasks that can invoke more than 270 AWS services and over 6,000 API operations. Without the need to provision and manage infrastructure, or integrate with multiple services, EventBridge Scheduler provides you with the ability to deliver schedules at scale and reduce maintenance costs.
-- https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html
(EventBridge Scheduler is recommended to be used instead of CloudWatch Scheduler with EventBridge rules)

I worked on an application running on EC2 to ECS that is still using its cron jobs. Cron jobs were migrated to EventBridge Scheduler. Our CI/CD uses GitHub Actions and Terraform. AWS provides actions that can create and deploy a ECS task definition (the container blueprint) to an ECS service, but there is no action to deploy an ECS task to the EventBridge Scheduler, as the cron task is not executed under a service.

To deploy the new code we have to write some code to the GitHub Action and I think it might benefit others in a similar context. We use Terraform as Infrastructure as a Code, so keep this in mind if you need to adapt to your IaaS solution.

There will be the full Yaml file here but I will comment parts of it separately afterwards.

name: Deploy Scheduled task XYZ

on:
  workflow_dispatch:
    inputs:
      imageHash:
        description: 'Image hash to deploy'
        required: true
        type: string
      environment:
        description: 'Environment to run tests against'
        type: environment
        required: true
        default: test

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  APP_NAME: application-name-here
  AWS_REGION: "ap-southeast-2"
  ECR_NAME: ecr-name-here
  ECS_CLUSTER: cluster-name-here
  IMAGE_NAME: application-image-name-here
  TASK_NAME: cron-task-name-here

permissions:
  id-token: write
  contents: read    # This is required for actions/checkout

jobs:
  deploy:
    name: Deploy to ${{ inputs.environment }}
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    env:
      JOB_ENV: ${{ inputs.environment }}
    steps:
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets[format('iam_role_to_assume_{0}', inputs.environment)] }}
          role-session-name: github-ecr-push-workflow-${{ inputs.environment }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Verify image
        run: aws ecr describe-images --repository-name ${{ inputs.environment }}-${{ env.ECR_NAME }}-${{ env.APP_NAME }} --image-ids imageTag=${{ inputs.imageHash }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Download the task definition ${{ env.TASK_NAME }}
        run: aws ecs describe-task-definition --task-definition ${{ env.TASK_NAME }} --query taskDefinition > task-definition.json

      - name: Fill in the new image ID in the Amazon ECS task definition ${{ env.TASK_NAME }}
        id: task-def-cron
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        with:
          task-definition: task-definition.json
          container-name: ${{ env.TASK_NAME }}
          image: ${{ env.ECR_REGISTRY }}/${{ env.JOB_ENV }}-${{ env.ECR_NAME }}-${{ env.APP_NAME }}:${{ inputs.imageHash }}

      - name: Deploy Amazon ECS task definition ${{ env.TASK_NAME }}
        id: deploy-cron
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def-cron.outputs.task-definition }}
          cluster: ${{ inputs.environment }}-${{ env.ECS_CLUSTER }}

      - name: Checkout infrastructure
        uses: actions/checkout@v4
        with:
          repository: orgnamehere/iaas-repo-here
          ref: main
          path: './working-path'
          token: ${{ secrets.PAT_TOKEN }}

      - name: Update schedule ${{ env.TASK_NAME }}
        working-directory: './working-path'
        env:
          GH_TOKEN: ${{ secrets.PAT_TOKEN }}
          INFRASTRUCTURE_FILE: 'path/to/your/module/terraform-file-here.tf'
          UNESCAPED_ARN: ${{ steps.deploy-cron.outputs.task-definition-arn }}
        run: |
          # Escape regexp non-safe characters from the ARN to prevent sed to fail
          export ESCAPED_ARN=${UNESCAPED_ARN//:/\\:}
          export ESCAPED_ARN=${ESCAPED_ARN//\//\\/}
          echo "Escaped ARN: $ARN"
          # Retrieve <appl-name>:<version> part of the ARN to use in PR 
          export ARRAY_ARN_PARTS=(${UNESCAPED_ARN//\// })
          export VERSION_PART=${ARRAY_ARN_PARTS[1]}
          export COMMIT_MESSAGE="DEPLOY: Deployment on ${{ inputs.environment }} - $VERSION_PART"
          # Use task definition version for branch name
          export BRANCH_NAME="deploy-${VERSION_PART//:/-}"
          git config user.email "[email protected]"
          git config user.name "Github Actions Pipeline"
          git checkout -b ${BRANCH_NAME}
          sed -i '/task_definition_arn /s/".*/'"\"${ESCAPED_ARN}"\"'/' $INFRASTRUCTURE_FILE
          git add ${{ env.INFRASTRUCTURE_FILE }}
          git commit -m "$COMMIT_MESSAGE"
          git push --set-upstream origin ${BRANCH_NAME}
          gh pr create --fill --body "- [x] $COMMIT_MESSAGE"

This pipeline will create the scheduled task definition, check out the IaaS repository, change the Scheduler task definition ARN and create a PR in the IaaS repository. We are using that also as a way to have approval to deploy to Production, but it can be automated if needed.

Let's comment on some parts:

on:
  workflow_dispatch:
    inputs:
      imageHash:
        description: 'Image hash to deploy'

It is good to separate the image creation from the deployment. This input is required assuming the image was created and published to the registry. This promotes reusability and flexibility.

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets[format('iam_role_to_assume_{0}', inputs.environment)] }}
          role-session-name: github-ecr-push-workflow-${{ inputs.environment }}
          aws-region: ${{ env.AWS_REGION }}

Environment is a required input and this pipeline can be executed against any environment you defined in your repository.

      - name: Deploy Amazon ECS task definition ${{ env.TASK_NAME }}
        id: deploy-cron
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def-cron.outputs.task-definition }}
          cluster: ${{ inputs.environment }}-${{ env.ECS_CLUSTER }}

Although the action name is "deploy task definition" it will create the task, but not deploy, as this is only possible when you provide a service input (by the time this article is being written). But we are not deploying to a service though, so the action will only create the task definition, but the EventBridge Scheduler remains calling the same task definition it was invoking before the creation of this task definition.

      - name: Checkout infrastructure
        uses: actions/checkout@v4
        with:
          repository: orgnamehere/iaas-repo-here
          ref: main
          path: './working-path'
          token: ${{ secrets.PAT_TOKEN }}

Using AWS CLI was an alternative we considered, but changing the target of the EventBridge scheduler becomes a little bit confusing and brings some cognitive complexity in case we need to change something. We decided then to fetch the IaaS repository and control the task definition version to be the target of the scheduler in Terraform code, so we could also be sure any dependency that the target change could have would be managed by Terraform, instead of another CLI change in this pipeline. We checkout the IaaS repo and save the path as ./working-path to keep the workspace clean. The name is your choice.

      - name: Update schedule ${{ env.TASK_NAME }}
        working-directory: './working-path'
        env:
          GH_TOKEN: ${{ secrets.PAT_TOKEN }}
          INFRASTRUCTURE_FILE: 'path/to/your/module/terraform-file-here.tf'
          UNESCAPED_ARN: ${{ steps.deploy-cron.outputs.task-definition-arn }}
        run: |
          # Escape regexp non-safe characters from the ARN to prevent sed to fail
          export ESCAPED_ARN=${UNESCAPED_ARN//:/\\:}
          export ESCAPED_ARN=${ESCAPED_ARN//\//\\/}
          echo "Escaped ARN: $ARN"
          # Retrieve <appl-name>:<version> part of the ARN to use in PR 
          export ARRAY_ARN_PARTS=(${UNESCAPED_ARN//\// })
          export VERSION_PART=${ARRAY_ARN_PARTS[1]}
          export COMMIT_MESSAGE="DEPLOY: Deployment on ${{ inputs.environment }} - $VERSION_PART"
          # Use task definition version for branch name
          export BRANCH_NAME="deploy-${VERSION_PART//:/-}"
          git config user.email "[email protected]"
          git config user.name "Github Actions Pipeline"
          git checkout -b ${BRANCH_NAME}
          sed -i '/task_definition_arn /s/".*/'"\"${ESCAPED_ARN}"\"'/' $INFRASTRUCTURE_FILE
          git add ${{ env.INFRASTRUCTURE_FILE }}
          git commit -m "$COMMIT_MESSAGE"
          git push --set-upstream origin ${BRANCH_NAME}
          gh pr create --fill --body "- [x] $COMMIT_MESSAGE"

This is where we use sed to search and replace the ARN in the terraform code. We scape the ARN before applying sed to not mess with the search regexp.

The terraform code expected to be changed will be something like this:

# main.tf
-        task_definition_arn     = "arn:aws:ecs:ap-southeast-2:123456789012:task-definition/task-definition-cron-name:57"
+       task_definition_arn     = "arn:aws:ecs:ap-southeast-2:123456789012:task-definition/task-definition-cron-name:58"

Links:

Categorias
Tropeçando

Tropeçando 112

Treezor: a serverless banking platform

This case study dives into how Treezor went serverless for their banking platform. From legacy code running on servers to a serverless monolith, and then event-driven microservices on AWS with Bref.

Treezor is a high available banking application running mostly in PHP.

Wait, is cloud bad?

Forrest Brazeal review 37signals (Basecamp) movement from the Cloud back to DataCenter, their use-case and some reasoning about the mentioned arguments for Data Center.

ECS Blue/Green deployment with CodeDeploy and Terraform

How to make Rector Contribute Your Pull Requests Every Day

Do you enjoy making code-reviews with hundreds of rules in your head and adding extra work to the pull-request author?

We don't, so we let Rector for us in active code review.

Docker for the late majority

This is a guide for people who would like a brief introduction to Docker and are too afraid to ask for one. I get it. Everyone around you already seems to know what they’re talking about. Looking ignorant is no fun.

10 Essential PHP.ini Tweaks for Improved Web Performance

If you're running a website or web application with PHP, you may have encountered issues with slow loading times, high memory usage, or other performance problems. Fortunately, there are several tweaks you can make to your PHP configuration file (php.ini) to optimize your scripts and improve your website's performance. In this article, I'll cover the top 10 most common changes you might need to make to your php.ini file for best performance.

Categorias
Tropeçando

Tropeçando 106

5 Constant Lists That Give Context to your Integers and Strings

Enum i a great feature of modern software development. Here you will find five lists that lives in Enums (or Enum-like) that add lots of context for your code. It will look better and cleaner.

Validate all the things: improve your security with input validation!

If there's one habit that can make software more secure, it's probably input validation. Here's how to apply OWASP Proactive Control C5 (Validate All Inputs) to your code.

TypeScript Utility Types: The 6 Most Useful

Write better typescript code by using typescript utility types. Record, Partial, Required, Omit, Pick, Exclude utilities.

Query parameter data types and performance

Recently, I could help solve a “mysterious” performance problem for a customer. The problem turned out to be a badly chosen query parameter type, which led to poor query performance. After working on this customer’s case, I realized that the meaning of the data type of a query parameter is not universally understood, which is why I decided to write this article.

Terraform Best Practices

Building Serverless Applications That Scale The Perfect Amount

Great reasoning about how to think about the architecture that scale on Serverless, thinking about the load and costs.

Run a PHP application on AWS Fargate

An example for a pipeline to configure and deploy a PHP application on AWS Fargate.

6 Signs Your Daily Standups Aren’t Working

Architecture Decision Records help you, your team, and future teams

Decisions are made everyday and often the number of daily decisions make us forget older ones. Keeping a record of the decisions is a great tool to revisit what was done and also be more confident that a change can be made, when a previous statement become obsolete somehow. ADRs are a great tool for a mature software.

Waiting for PostgreSQL 15 – Add support for MERGE SQL command

MERGE SQL commands is able to perform INSERT/UPDATE/DELETE based on a base query and some conditions. This is very useful for some business logic that can be applied directly on the database data.