AWS Batch EC2: one On-Demand compute environment + queue + job definitions under Terraform¶
Decision¶
Complete the "one Fargate + one EC2" Batch consolidation (#322) by standing up a single
On-Demand, MANAGED EC2 compute environment, one EC2 job queue, and two EC2 job definitions under
Terraform (terraform/batch/), mirroring the Fargate side. The legacy/duplicate EC2 environments
(BigLever_*, BigLeverOnDemandEC2, ToshiHazardPost_*) are retired by hand in the console.
Concretely:
- Two Terraform-managed EC2 job definitions, tracking the same floating image tags as their
Fargate counterparts (no EC2-specific image;
docker-buildmoves:experimental,promotemoves:prod): runzi-ec2-JD(prod) →<ecr>/nzshm22/runzi:prodrunzi-ec2-experimental-JD→<ecr>/nzshm22/runzi:experimental
Both carry platform_capabilities = ["EC2"]. Their container_properties are the Fargate base
shape minus the two Fargate-only fields (fargatePlatformConfiguration and
networkConfiguration/assignPublicIp); execution role, job role, resting vCPU/memory, env, and
command mirror the Fargate definitions.
-
One MANAGED EC2 compute environment (
runzi-ec2-CE) and one queue (runzi-ec2-Q). On-Demand purchasing;allocation_strategy = BEST_FIT_PROGRESSIVE;min_vcpus = 0(scales to zero when idle, no standing cost);instance_type = ["optimal"](Batch picks from the C/M/R families). Subnets/security groups are shared with the Fargate settings. The Batch service-linked role is used unless a custom role is supplied. -
Canonical EC2 name constants are added to
runzi/arguments.py(EC2_JOB_DEFINITION,EC2_EXPERIMENTAL_JOB_DEFINITION,EC2_JOB_QUEUE). Fargate stays the default for every task; EC2 remains an explicit per-job opt-in via a config file'ssys_arg_overrides(ecs_compute_environment: ec2, plusecs_job_queue/ecs_job_definitionset to the EC2 names). No submit-path logic changes —get_ecs_job_config()already branches onComputeEnvironment.EC2for validation, andresolve_job_definition_digest()already resolves any job definition's tag → digest for provenance.
Two deciding inputs¶
-
An EC2 compute environment is inert without a job definition that targets it. ADR-0007 brought only the Fargate job definitions under Terraform and explicitly deferred the EC2 definition to this consolidation. Standing up an EC2 compute environment + queue without a matching
platformCapabilities: EC2definition would leave a half-built target nothing can submit to, so the EC2 JDs are part of the same unit of work — not a follow-up. -
On-Demand is the honest minimum for the first consolidated EC2 environment. Spot is cheaper but needs the Spot fleet/service-linked role, a bid allocation strategy, and jobs that tolerate interruption/retry — none of which the current workload is set up for. On-Demand matches the legacy
BigLeverOnDemandEC2posture, stands up with no extra IAM, and leaves Spot as a clean upgrade path if cost ever demands it. Likewise, instance-type/cost tuning is deliberately left at["optimal"]here because it is its own investigation (#323);"optimal"gives a working, self-tuning environment without pre-committing to hardware choices that issue is meant to make.
Background¶
ADR-0003 consolidated the workload onto Fargate and retired the old EC2 BigLever_32GB_8VCPU_v2_*
path, re-supporting EC2 only as a per-job opt-in — at the time fully config-driven, with no
canonical EC2 names (there was no single canonical EC2 target to name). ADR-0004 brought the
Fargate compute environment + queue under Terraform; ADR-0007 brought the two Fargate job
definitions under Terraform via floating tags and flagged the EC2 definition as a followup for
322. This ADR closes that loop: because #322 creates exactly one canonical EC2 compute¶
environment + queue + definition pair, it now makes sense to name them in code — refining ADR-0003's addendum ("no canonical EC2 names") now that a canonical EC2 target exists. Fargate remains the default; this does not reopen the retired BigLever path.
Consequences / deferred obligations¶
Terraform code for the EC2 resources ships in the runzi repo, but the cutover is ordered and partly deployer-credentialed (same posture as ADR-0004/0007):
- Discovery before apply. The EC2 instance-profile ARN and a sensible
max_vcpusmust be read from a live EC2 compute environment (aws batch describe-compute-environments) and filled intoterraform.tfvars— the live values can't be read from CI, soterraform.tfvars.examplecarriesTODOs (matching the ADR-0007 convention). - Apply, then retire. A deployer applies the EC2 compute environment → queue → two definitions
(created fresh; the legacy EC2 environments are not imported, since they're slated for
deletion), smoke-tests a job against
runzi-ec2-Q/runzi-ec2-JD, then retiresBigLever_*,BigLeverOnDemandEC2, andToshiHazardPost_*compute envs/queues by hand in the console (disable → drain → delete).terraform planmust be clean afterwards. - EC2 sizing has no static matrix. Unlike Fargate, EC2 vCPU/memory pairs aren't validated
against a table (
validate_ec2_resources()is a light positive-value check only). An oversized EC2 job sits inRUNNABLEindefinitely rather than erroring — the operator-facing failure mode documented indocs/usage/aws_batch.md. - Permissions. Per ADR-0006/0007 the task-execution/job role lives on the job definition
(Terraform substrate); the EC2 JDs reuse the same
execution_role_arn/job_role_arnas the Fargate JDs, so no new access-tier IAM is required for this change. - EC2 needs its own egress-capable subnets (found during smoke test). EC2 container instances
must reach the ECS/ECR endpoints to register, so they need a NAT gateway or auto-assigned public
IPs — unlike Fargate, whose ENIs get a public IP via
assign_public_ip = ENABLEDin the shared public/no-NAT subnet. Reusing the Fargate subnet leaves EC2 instances with no egress; they never register and jobs stick inRUNNABLE. The EC2 CE therefore has its ownec2_subnets/ec2_security_group_ids(falling back to the Fargate values if unset), set to the subnets/SG a working EC2 compute environment already uses.
Followups not blocking this decision¶
- Instance-type / cost optimization — evaluate specific families/sizes and Spot vs On-Demand
economics. Tracked in #323;
["optimal"]+ On-Demand is the deliberate starting point. - Spot compute environment — a future upgrade path (Spot fleet role + interruption-tolerant retry) if cost demands it; nothing here blocks adding a second Spot environment behind the queue.
- Ergonomic EC2/
--experimentalsubmission flag — still deferred (ADR-0007); EC2 opt-in usessys_arg_overridesuntil the JD/compute-env set is confirmed stable, which this ADR advances.
Addendum: queue + compute-environment derive from the job definition¶
Smoke-testing the EC2 target showed that requiring a user to set ecs_job_definition,
ecs_job_queue, and ecs_compute_environment consistently is friction and a foot-gun (an EC2
job definition submitted to the Fargate queue sits in RUNNABLE forever). Each canonical job
definition has exactly one correct queue + compute-environment type, so the user now picks only
the job definition; the queue and compute-environment type derive from it. A JOB_DEFINITION_TARGETS
registry in runzi/arguments.py maps each canonical job definition to its BatchTarget(job_queue,
compute_environment); ecs_job_queue / ecs_compute_environment become None-default override
inputs, and SystemArgs.resolved_job_queue / resolved_compute_environment return the explicit
override if set, else the job definition's target. An unknown/custom job definition falls back to
the Fargate target, so behaviour is unchanged unless a config explicitly sets those fields.
The derivation lives on the model as compute-on-read properties, not a model_validator,
because sys_arg_overrides are applied by setattr after construction (runzi/job_runner.py) and
pydantic doesn't re-run validators on assignment — a stored/validated value would go stale when the
job definition is overridden. A pure read-time resolver is never stale and needs no cooperation from
callers. This refines ADR-0003's addendum further: EC2 is still opt-in, but no longer
"fully config-driven" — the canonical names carry their own routing.
Files¶
terraform/batch/main.tf—aws_batch_compute_environment.ec2,aws_batch_job_queue.ec2,local.ec2_container_properties, and theaws_batch_job_definition.ec2_prod/.ec2_experimentalresources.terraform/batch/variables.tf,outputs.tf,terraform.tfvars.example— the EC2 variables (instance role, max vCPUs, instance types, allocation strategy, names, resting sizing), the four EC2 ARN outputs, and theTODO-marked discovery section.runzi/arguments.py—EC2_JOB_DEFINITION,EC2_EXPERIMENTAL_JOB_DEFINITION,EC2_JOB_QUEUE; theBatchTarget/JOB_DEFINITION_TARGETSregistry andSystemArgs.resolved_job_queue/resolved_compute_environmentproperties (addendum).runzi/build_tasks.pyreads the resolved properties.docs/usage/aws_batch.md— EC2 target, canonical names, when to use EC2 vs Fargate.terraform/batch/README.md— EC2 discovery + apply-order + legacy-retirement runbook.- 0003 — its addendum's "no canonical EC2 names" point is refined here now that a single canonical EC2 target exists. 0007 — its EC2-job-definition followup is actioned here.