ADR-0015: Scheduled Jobs hand off in adopt-in-place¶
- Status: accepted
- Date: 2026-10-02
- Deciders: maintainer
Context¶
PLAN §3 blocked Scheduled Jobs (issue #5). A Copilot Scheduled Job has no ECS service. Its stack holds the task definition and roles, plus:
-
an
AWS::Events::Ruleon the job's schedule, whose target assumes a role (RuleRole) to start a state machine; -
an
AWS::StepFunctions::StateMachinethat runs the task (ecs:runTask.sync) with retries and a timeout. Its definition is a string with${...}placeholders, filled in fromDefinitionSubstitutions(cluster, task definition, subnets, security groups); -
StateMachineRole, and anAWS::EFS::AccessPointwhen the job mounts the environment's managed file system (EnvControllerAction.ManagedFileSystemID).
The only custom resource is the env-controller, as with services.
Decision¶
-
Scheduled Job is a supported workload type. The atomic per-stack hand-off, retain patches and checks apply unchanged.
-
Events targets may carry
RoleArn(role_arnonaws_cloudwatch_event_target). The live targets must still match the template exactly, role included. -
The state machine is imported as deployed as
aws_sfn_state_machine:-
the definition is the template's
DefinitionString(orDefinition) with every substitution applied, written asjsonencode(...). Any placeholder left without a value blocks the resource; -
when the inventory read the state machine, the live definition must be JSON-equal to that, and the role and type must match. A definition edited outside CloudFormation blocks the stack rather than being overwritten;
-
logging and tracing configuration are copied;
publish, a Terraform-only argument, is written at its default, ignored for the import and hardened afterwards (RUNBOOK step 4b).
-
-
Live read:
states:DescribeStateMachineandListTagsForResource. The definition holds only orchestration and resource identifiers, no secret values.
Evidence¶
-
The synthetic full-handoff app includes Copilot's verbatim job render (with an nginx sidecar, secrets, and values from other stacks' exports) and turns on the environment's managed EFS for it. All five stacks hand off, with a clean closure, and the generated Terraform passes
terraform validate. -
terraform planagainst moto: the rule and its role-assuming target plan as2 to import, 0 to add, 0 to change, 0 to destroy. Moto does not implementListStateMachineVersions, which the provider calls when it reads a state machine, so that import was unverified offline. -
Real AWS, 2026-10-07 (report): 63/63 pure imports, every Copilot stack deleted, and the Terraform-owned state machine ran the job successfully afterwards. The run found that the provider compares the definition as text, so the deployed text is now written verbatim, and that
ManagedFileSystemIDmust be read from the environment stack's outputs.
Consequences¶
After the hand-off, Terraform owns the schedule and the state machine. A new task definition revision needs the state machine definition updated too, because it names the revision literally. The runbook's guidance on task-definition rollouts applies to jobs as well.