Containers

Migrate cron jobs to event-driven architectures using Amazon Elastic Container Service and Amazon EventBridge

Introduction

Many customers use traditional cron job schedulers in on-premise systems. They need a simple approach to move these scheduled tasks to AWS without refactoring while unlocking the scalability of the cloud. A lift-and-shift migration to Amazon Elastic Compute Cloud (Amazon EC2) is always a possibility, but that doesn’t take advantage of cloud-native services or scalability – they still have an always-on Amazon EC2 instance whose job is to wait for a specific time to start a task. Other services, such as AWS Batch or AWS Lambda may be able to fill the role, but may require refactoring the code and have other constraints that must be considered, such as 15 minute function timeout in AWS Lambda. Many customers choose to solve this challenge by moving their cron jobs to short-lived containers and scheduling them via Amazon EventBridge.

As an example, one customer had hundreds of cron jobs configured on a single on-premise server. They chose to run these various cron jobs as separate scheduled tasks on Amazon Elastic Container Service (Amazon ECS) using AWS Fargate. They used Amazon EventBridge to schedule the tasks because it triggers an Amazon ECS task at scheduled times rather than having to run highly-available virtual machines or containers waiting for the event to happen. They chose Amazon ECS because of its simple container orchestration. And they chose AWS Fargate because it allows them to pay for only what they use and frees them from provisioning, configuring, and scaling clusters of Amazon EC2 instances.

They had three architecture options they could have used to migrate the jobs to Amazon ECS:

  • Option 1: A different container image for each job and a different task definition for each image.
  • Option 2: A single container image that includes all the jobs with a different task definition for each job.
  • Option 3: A single container image that includes all the jobs and a single task definition. Each invocation of the task definition will override the default command.

Option 1 assumes that customers might attempt to create a different container image for each scheduled task in order to change the CMD instruction of the Dockerfile (see this blog post on Demystifying ENTRYPOINT and CMD in Docker for more details on CMD instruction in Dockerfile). They would also need to create different Amazon ECS task definitions and scheduled tasks, each running a different command at a different schedule. These containers and task definitions would still have the same underlying code and most will have the same resource requirements since these tasks were originally performed on the exact same system. All this results in increased operational overhead and unnecessary duplication of Dockerfiles, container images, and task definitions.

A single container image with separate task definitions (option 2) would still suffer from similar unnecessary duplication of task definitions and an operational challenge to connect each cron schedule to its corresponding task definition.

Lastly, a single container image and task definition that includes all the tasks (as implied in the option 3) is the simplest way to architect this solution because all scheduled tasks use the same container image and task definition. But how will the scheduled task know which command to run in the container? It is possible to provide a command-override for scheduled tasks in Amazon ECS. Command-override will replace the run-time command (i.e., CMD instructions in Dockerfile) to be executed upon the start, as it was configured in the traditional crontab file (see Figure 1).

0/2 * * * * bash processA.sh 0/5 * * * * bash process.sh

Figure 1: Traditional Crontab file

This post is focused on implementation details of the option 3 above and describes in detail how to solve this very common use-case of running multiple jobs (i.e., tasks) from Amazon EventBridge using the same task definition and the same underlying container image.

Solution overview

The solution we’re creating in this post uses a single container image for all tasks. This replicates the way most cron job servers are architected and simplifies migration from a server-based architecture. We’ll show three methods to create Amazon EventBridge schedules and targets, using different tools:

  1. Option 1 – AWS EventBridge Scheduler
  2. Option 2 – AWS CloudFormation Templates
  3. Option 3 – AWS Cloud Development Kit (AWS CDK).

Each target includes an input argument that will be used to override the container image defaults at run-time.

The current-state architecture is schematically shown on Figure 2. It demonstrates a “Cronjob server” in the data center that has a crontab service configured with two different cron jobs, each running its own different process (“Process A” and “Process B”).

A single server runs crontab, which schedules two other processes (processA and processB) running on the server.

Figure 2: Current-state architecture

The future-state architecture (as shown on Figure 3) demonstrates the same “Process A” and “Process B” jobs running as separate tasks (previously “cron jobs”) under Amazon ECS cluster, using the same common task definition (Amazon ECS task definitions), triggered by Amazon EventBridge Scheduler.

EventBridge Scheduler creates 2 processes (processA and process) using the pre-defined Task definitions through the ECS Run Task API.

Figure 3: Future-state architecture

Prerequisites

To implement this solution, you need an Amazon ECS cluster (see Cluster management in the Amazon ECS console) and a task definition (see Task definition management in the Amazon ECS console) with a single container image (see  Creating a container image for use on Amazon ECS) that includes all the processes that will be scheduled (i.e., “Process A” and “Process B” in the previous example).

Limitations

This solution assumes each process is independent of the others and stateless. Each scheduled event creates a new container without any access or awareness to the other containers’ memory or disk, including previous invocations of the same scheduled task.

Walkthrough

Option 1 – Amazon EventBridge Scheduler

Amazon EventBridge Scheduler is a new capability from Amazon EventBridge that allows AWS users to create, run, and manage scheduled tasks at specific times or intervals without having to run an always-on cron server. Users can trigger an Amazon ECS task from the Amazon EventBridge Scheduler console using the templated Amazon ECS RunTask target. With that template, users can provide overrides in the task input to replace the command that runs when the container starts (as shown on Figure 4).

{"containerOverrides":[{"name":"my-container","command":"["bash processA.sh"]"}]}

Figure 4: Override container commands when running Amazon ECS tasks with Amazon EventBridge Scheduler

Option 2 – CloudFormation template

Creating these tasks in the console is a great way to understand the process, but it doesn’t scale well beyond a few scheduled tasks. To scale and operationalize these scheduled tasks, we’ll need to create them with DevOps pipelines or infrastructure-as-code tools. We can create the schedules in Amazon EventBridge with AWS Command Line Interface (AWS CLI), AWS Cloud Formation, or AWS CDK.

This is how the Amazon EventBridge Scheduler resource would look in AWS CloudFormation:

AWSTemplateFormatVersion: "2010-09-09"
Description: "Creates an EventBridge Scheculer that runs an ECS task every 2 minutes. The Input will override the default run command with `bash processA.sh`"
Resources:
  myProcessASchedule:
    Type: AWS::Scheduler::Schedule
    Properties: 
      Name: "my-processA-schedule"
      State: "ENABLED"
      FlexibleTimeWindow: 
        Mode: "OFF"
      ScheduleExpression: "cron(0/2 * * * ? *)"
      Target: 
        Arn: !Sub "arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:cluster/my-cluster"
        EcsParameters: 
          TaskDefinitionArn: !Sub "arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:task-definition/my-task-definition:2"
        Input:  |
          {
            "containerOverrides": [
              {
                "name": "my-container",
                "command": ["bash processA.sh"]
              }
            ]
          }
        RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/ecsEventsRole"

The ScheduleExpression element tells Amazon EventBridge when to run the task. This can be a cron expression, a specific time, or a set interval (referred to as rate). The Targets element tells Amazon EventBridge which task to run (as TaskDefinitionArn in EcsParameters) and how to run it. Notice that Input in the Cloud Formation Template follows the same JSON format as Input from the scheduled task created in the console.

Option 3 – AWS CDK

Many crontab servers have dozens or more cron jobs. At a certain scale, it becomes cumbersome to create and maintain a AWS CloudFormation template containing all of the scheduled tasks. This can be resolved with AWS CDK, which allows us to loop through a list of cron jobs to create an Amazon EventBridge schedule for each. Here’s how that would look in AWS CDK for TypeScript:

export class MyStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Define myCluster and myTaskDefinition here 
    const myCluster = . . .
    const myTaskDefinition = . . .
    
    // current cron jobs as arrays:
    const crons = [
      ['0/2',  '*', '*', '*', 'bash processA.sh'],
      ['0/5',  '*', '*', '*', 'bash processB.sh'],
      ['0', '0/12', '*', '*', 'bash processC.sh'],
      ['*',    '*', '*', '*', 'bash processD.sh']
    ]

    // for each cronJob ...
    crons.forEach( (cron, i) => {
      const minute  = cron[0]
      const hour    = cron[1]
      const day     = cron[2]
      const month   = cron[3]
      const command = cron[4]
      const scheduleName = `my-scheduler-${i}`

      // create the schedule expression
      const schedule = events.Schedule.cron({ 
          minute: minute, 
          hour: hour, 
          day: day, 
          month: month, 
      })
      
      // create the scheduler resource
      const cfnSchedule = new scheduler.CfnSchedule(this, scheduleName, {
        name: scheduleName,
        state: 'ENABLED',
        flexibleTimeWindow: {
          mode: 'OFF'
        },
        scheduleExpression: schedule.expressionString,
        target: {
          arn: `arn:aws:ecs:${region}:${account}:cluster/my-cluster`,
          ecsParameters: {
            taskDefinitionArn: `arn:aws:ecs:${region}:${account}:task-definition/my-task-definition:2`
          },
          // this will override the default command when the container starts
          input: `{"containerOverrides":[{"name":"my-container","command": ["${command}"]}]}`,
          roleArn: `arn:aws:iam::${account}:role/ecsEventsRole`
        }
      })

    })
  }
}

The crons variable in this example is a list of cron jobs where each element represents a line in a crontab file. AWS CDK loops through the cron jobs, creating an Amazon EventBridge schedule and target for each. Just like the Amazon ECS console and AWS CloudFormation implementations, each schedule has a customized schedule and each target has a customized command; however, they share the cluster, container image, and task definition. See Figure 5 for the list of EventBridge schedules created as a result.

At the time of publishing, AWS CDK doesn’t have an official L2 construct, so we’re using the automatically generated L1 construct, which is the same as generating AWS CloudFormation directly. Check the AWS CDK aws_scheduler module documentation for updates.

Schedules (5) Schedule name, Schedule Group, Status, Target, Target Type, Last Modified my-schedule-2, default, Enabled, my-cluster, ECS_RunTask, Mar 28, 2023, 19:22:22 (UTC+00:00) my-schedule-2, default, Enabled, my-cluster, ECS_RunTask, Mar 28, 2023, 19:22:18 (UTC+00:00) my-schedule-1, default, Enabled, my-cluster, ECS_RunTask, Mar 28, 2023, 19:22: 18 (UTC+00:00) my-schedule-0, default, Enabled, my-cluster, ECS_RunTask, Mar 28, 2023, 19:22: 18 (UTC+00:00)

Figure 5: View of Amazon EventBridge schedules created by AWS CDK

Cleaning up

When you’re done experimenting, remove any Amazon EventBridge schedules or Amazon ECR images you created to avoid unnecessary charges. Other Amazon ECS resources won’t incur a charge if no task is running, but it is best practice to remove any resources not in use.

Conclusion

In this post, we showed you how to convert a traditional server-based worker node with minimal refactoring into an event-driven architecture. With this architecture, no server is running 24/7 waiting for a scheduled event to start. In fact, we have serverless schedulers with Amazon EventBridge Scheduler, serverless container orchestration with Amazon ECS, and serverless compute with AWS Fargate, all of which reduce operational overhead and eliminate costs for idle resources. Continue your serverless container modernization journey by reading this post about event-driven serverless containers and let us know in the comments how you were able to use this solution.