1. Background: two task definitions that differed only in CPU/memory
The system is a business application on Rails 7 / ECS Fargate that MOOBON operates and maintains. The web runs as an ECS service with two tasks always on; batch jobs are started from ECS “Scheduled tasks,” 8 rules in total, hourly and daily, each running a rake task.
The system had two task definition families: myapp-prod for the web and myapp-prod-batch for batch. Pulling the latest revision of each with describe-task-definition and comparing them (hashing the environment variable values) gave this:
| Item | myapp-prod (web) | myapp-prod-batch (batch) |
|---|---|---|
| Image | Same (immutable prod-<commit sha> tag) | |
| Environment variables | All 22 keys identical, all values identical | |
| Task role / execution role / network mode | Same | |
| CPU / memory | 1 vCPU / 4 GiB | 2 vCPU / 8 GiB |
| Log group (awslogs) | /ecs/myapp-prod | /ecs/myapp-prod-batch |
The only substantive difference was CPU/memory (the log group comes up in §3). Yet because there were two task definitions, every deploy meant re-registering the batch one by hand and fixing the revision referenced by the 8 rules. “If only the batch could launch with a bigger task size, one task definition would do” is where this article starts.
CPU/memory, command, environment variables, and task roles can all be overridden at RunTask time (§3). If those are the only reasons, there is no need for a second task definition.
2. How it works: Input is passed straight through as RunTask overrides
2-1. A scheduled task is really an EventBridge rule
What you create in the ECS console's “Scheduled tasks” tab is, underneath, an EventBridge rule (cron expression) with the ECS cluster as its target. Looking at it with aws events list-targets-by-rule, a target holds:
Arn: the ECS cluster to run onRoleArn: the role EventBridge uses to call RunTask (e.g.ecsEventsRole)EcsParameters: task definition ARN, launch type, network configuration, task countInput: the overrides passed to RunTask (a JSON string)
At the cron time, EventBridge calls ecs:RunTask with this content. Updating a rule does not run the batch; it simply starts at the next cron time with whatever the rule contains at that moment.
2-2. Put cpu / memory in the Input
The key is Input. The EventBridge documentation explains that if the input to an ECS target matches RunTask's TaskOverride structure, it is mapped one-to-one and passed through to RunTask. Most people only use it for containerOverrides to swap the command, but TaskOverride also includes task-level cpu / memory. In other words, writing this in Input changes the CPU/memory for that launch only, without touching the task definition:
{
"cpu": "2048",
"memory": "8192",
"containerOverrides": [
{
"name": "Rails",
"command": ["bin/rails", "sync:hourly"]
}
]
}In our setup the task definition myapp-prod stays at 1 vCPU / 4 GiB, and all 8 rules carry cpu: 2048 / memory: 8192 in their Input. The details page of a launched batch task shows “Task definition: myapp-prod:607” and “CPU | memory: 2 vCPU | 8 GiB”, confirming that the task was provisioned with the Input values, not the task definition values (§4-3).
3. What can and cannot be overridden
3-1. The scope of TaskOverride / ContainerOverride
What is overridable is defined by the TaskOverride / ContainerOverride API. Here is the summary we used to decide whether merging was viable.
| Item | RunTask override | Notes |
|---|---|---|
| Task cpu / memory | Yes | Task level. Must be a valid Fargate combination |
| ephemeralStorage | Yes | Task level (Fargate) |
| taskRoleArn / executionRoleArn | Yes | The caller needs iam:PassRole on that role |
| Container command / environment | Yes | containerOverrides. For environment, list only the keys to override |
| Container cpu / memory / memoryReservation | Yes | containerOverrides. Raise these too if the container definition has a hard limit |
| logConfiguration (log group etc.) | No | After merging, batch logs go to the same log group as the web |
| healthCheck / portMappings / volumes | No | The web settings come along to the batch as they are |
In our case, losing the separate log group was the only trade-off. Logs still go to a separate stream per task and can be filtered in Logs Insights, so we accepted it. If separate log groups are themselves a requirement, that alone remains a valid reason to keep two task definitions.
3-2. Fargate size combinations and container-level memory
The override must be a CPU/memory pair that Fargate supports, or RunTask itself fails (for example, 2 vCPU allows 4 to 16 GiB in 1 GiB steps). You can go above or below the task definition, but stay within the table.
The other thing to check is a container-level memory (hard limit). Our task definition had none on the container, so the task-level override was enough. If your container has something like memory: 3072, raising the task to 8 GiB still leaves the container OOM-killed at 3 GiB. In that case, override containerOverrides[].memory as well.
4. Setup steps (AWS CLI)
The same change goes into 8 rules, so we did it with the CLI in one pass rather than in the console. AWS CloudShell is convenient: it reuses the console sign-in and already has aws and jq.
4-1. Back up the current targets
The rules we care about are “rules whose target is the ECS cluster,” so list-rule-names-by-target with the cluster ARN lists them. First, dump every rule's targets to a file. These files double as the rollback input.
CLUSTER=arn:aws:ecs:ap-northeast-1:123456789012:cluster/my-cluster
mkdir -p backup
for rule in $(aws events list-rule-names-by-target --target-arn $CLUSTER --query RuleNames --output text); do
aws events list-targets-by-rule --rule "$rule" --query Targets > "backup/$rule.json"
done4-2. Rewrite with jq and put-targets
From the backed-up JSON, select only the targets that reference myapp-prod-batch, then change two things: (1) the task definition ARN to the web one, (2) add cpu/memory to Input. Write it back with put-targets. The command override, network configuration, and roles are carried over as they are.
NEW=arn:aws:ecs:ap-northeast-1:123456789012:task-definition/myapp-prod:607
for f in backup/*.json; do
rule=$(basename "$f" .json)
jq --arg arn "$NEW" '[.[]
| select((.EcsParameters.TaskDefinitionArn // "") | test("/myapp-prod-batch:"))
| .EcsParameters.TaskDefinitionArn = $arn
| .Input = (.Input | fromjson | .cpu = "2048" | .memory = "8192" | tojson)]' "$f" > new.json
[ "$(jq length new.json)" -gt 0 ] || continue
echo "== $rule"; jq -c '.[] | {TaskDefinitionArn: .EcsParameters.TaskDefinitionArn, Input}' new.json
aws events put-targets --rule "$rule" --targets file://new.json
done- Always check that the
put-targetsresponse saysFailedEntryCount: 0. Anything else means that rule was not updated - Comment out the
put-targetsline on the first pass and just read theechooutput (dry run) - To roll back:
aws events put-targets --rule <rule> --targets file://backup/<rule>.json
4-3. Verification
Updating a rule does not run the batch, so verification waits for the next cron time. We used the hourly sync at 10 minutes past the hour. What to check:
- In the ECS task details, Task definition is
myapp-prod:607, CPU | memory is 2 vCPU | 8 GiB, and Started by is the rule name - The container exit code is 0 (
containers[].exitCodeindescribe-tasks) - The
describe-tasksoutput contains bothoverrides.cpu/overrides.memoryand the effectivecpu/memory
5. Caveats
5-1. Don't save from the console form
The ECS console's Update screen for a scheduled task has the task definition revision, networking, a task role override, and container overrides (command and environment variables), but no field for task-level cpu / memory. Since there is no guarantee how cpu / memory written in Input are handled when saving from this form, we only touch the merged rules from the CLI. To just look at the stored values, open the rule in the EventBridge console → Targets → “Input to target: Constant” → View, which shows the JSON as is.
5-2. put-targets replaces the whole target
put-targets replaces the target with the same Id entirely with what you pass. If you pass only TaskDefinitionArn, you end up with a target that has lost its Input, NetworkConfiguration, and RoleArn. Always start from the list-targets-by-rule output and change only the fields you mean to (the script in §4-2 does exactly that).
5-3. Cost comparison with “just make the web bigger”
The simplest merge would be to skip overrides and raise the task definition itself to 2 vCPU / 8 GiB, but then the two web tasks pay that price around the clock. A rough estimate with Fargate's published Tokyo (Linux/x86) prices:
| Option | Web (2 tasks, always on) | Monthly increase (approx.) |
|---|---|---|
| Raise the task definition to 2 vCPU / 8 GiB | 1 vCPU / 4 GiB → 2 vCPU / 8 GiB | + about $106 |
| Override in Input (this article) | Stays at 1 vCPU / 4 GiB | $0 |
* Computed as 1 vCPU $0.05056/h + 4 GiB × $0.00553/h ≈ $0.0727/h, × 730 h × 2 tasks ≈ $106. The batch side is billed only while running; an hourly job that runs about 2 minutes costs around $4/month even at 2 vCPU / 8 GiB. Prices are approximate, based on the public price list as of September 2026; check the official AWS pricing page for current values.
Afterword
That EventBridge's Input is passed straight into RunTask's TaskOverride is obvious once you know it, but it is easy to miss because the console form never shows a cpu / memory field. If the only reason you keep a second task definition is “the batch needs to be bigger,” a few lines of Input will do.
For the networking side of ECS, see our companion article Replacing NAT Gateway with fck-nat While Keeping a Static Egress IP for ECS. For AWS operations design or a cost review, feel free to contact us at info@moobon.jp.
Frequently Asked Questions
QCan I override task-level CPU/memory from the ECS console's Scheduled tasks screen?
As of September 2026, the ECS console's Update screen for a scheduled task offers the task definition family / revision, number of tasks, networking, a task role override, the EventBridge IAM role, and container overrides (command and environment variables). There is no field for task-level cpu / memory. Task-level overrides have to be written directly into the Input (JSON) that the EventBridge rule passes to its target; we set them with the AWS CLI's put-targets. To check what is stored, open the rule in the EventBridge console → Targets → "Input to target: Constant" → View, or run aws events list-targets-by-rule.
QCan I override to a smaller CPU/memory than the task definition?
Yes. The cpu / memory in RunTask overrides are independent of the task definition values, and you can go up or down as long as the pair is a valid Fargate combination. In this article we raise the web task definition (1 vCPU / 4 GiB) to 2 vCPU / 8 GiB for batch, but the same mechanism lets you shrink a light batch to 0.5 vCPU / 1 GiB to save money. One caveat: if the container definition has a hard memory limit, raising the task level alone leaves the container capped at that limit, so override memory in containerOverrides as well.
QCan the log destination (logConfiguration) also be changed per rule?
No. RunTask's TaskOverride only covers cpu / memory / ephemeralStorage / taskRoleArn / executionRoleArn plus per-container command / environment / cpu / memory; the awslogs log group and stream prefix are not part of it. So once you merge into one task definition, batch logs land in the same log group as the web. If separate log groups are a hard requirement, that alone is a valid reason to keep separate task definitions.
QCan environment variables be overridden per rule too? Anything to watch out for?
Yes, via environment in containerOverrides (list only the keys you want to override; the rest come from the task definition). The catch is that values written on the rule take precedence over the task definition. If you later change a value on the task definition, the rule keeps using its old value for as long as it overrides that key. Keep rule-level overrides to keys that are truly batch-specific, and leave values you want to manage in one place, such as secrets, out of the rules.
