Alastair
04/28/2026, 9:15 AMMarvin
04/28/2026, 9:15 AMMarvin
04/28/2026, 9:18 AMinfrastructure_pid alone.
A few important points:
* For ECS push work pools, infrastructure_pid is the worker’s handle to the infrastructure, and for ECS it is typically formatted like cluster::task_arn.
* In practice, users often observe that it’s mainly useful while the run is active.
* If you need a durable mapping between flow_run_id and ECS taskArn, the best long-term source is Prefect Events, not the transient execution metadata view.
Recommended approach: use ECS infrastructure events
Prefect emits ECS task lifecycle events that include both:
* the ECS task identifier / ARN
* the related Prefect flow run resource
That gives you a historical record you can query later.
Relevant docs:
* AWS ECS infrastructure events
* Prefect Events concepts
Those ECS events include resource metadata like:
* ecs.taskArn
* ecs.clusterArn
* a related resource for the flow run, like prefect.flow-run.<id>
So the durable correlation pattern is:
1. Flow run starts on ECS
2. Prefect emits prefect.ecs.task.* events
3. Later, query events for the flow run and extract ecs.taskArn
If you want, you can also persist that mapping yourself into your own DB/S3/log sink as soon as the run starts.
Example pattern:
from prefect import get_client
from prefect.events.filters import EventFilter, EventNameFilter, EventRelatedFilter
async def get_ecs_tasks_for_flow_run(flow_run_id: str):
async with get_client() as client:
event_filter = EventFilter(
event=EventNameFilter(prefix=["prefect.ecs.task"]),
related=EventRelatedFilter(
resources=[f"prefect.flow-run.{flow_run_id}"]
),
)
events = await client.read_events(filter=event_filter, limit=100)
tasks = []
for event in events.events:
task_arn = event.resource.get("ecs.taskArn")
if task_arn:
tasks.append(
{
"event": event.event,
"occurred": event.occurred,
"task_arn": task_arn,
"resource_id": event.resource.get("prefect.resource.id"),
}
)
return tasks
A couple of caveats:
* If you need exactly one ECS task ID per flow run, decide how to handle retries / re-submissions, since a flow run may have more than one infrastructure attempt over time.
* If you need guaranteed long-term retention beyond your Prefect event retention window, copy the mapping into your own persistence layer.
If you want the most robust setup, I’d recommend one of these:
* Best for audit/history: query Prefect Events
* Best for permanent internal tracking: write flow_run_id -> ecs_task_arn to your own store when the run starts
* Best for AWS-native observability: also tag or log the flow_run_id into ECS task logs / CloudWatch context
If you'd like, I can help you with either:
* a concrete script to query historical ECS task ARNs for flow runs, or
* a pattern to persist the mapping automatically at flow start.Alastair
04/28/2026, 9:52 AMMarvin
04/28/2026, 9:52 AMMarvin
04/28/2026, 9:54 AMresource
* attach the flow run as a related resource
* optionally also put the IDs in payload for convenience
Here’s a simple example from inside a flow.
from prefect import flow
from prefect.events import emit_event
from prefect.runtime import flow_run
@flow
def my_flow():
current_flow_run_id = flow_run.id
# however you obtain this in your environment
ecs_task_arn = "arn:aws:ecs:us-east-1:123456789012:task/my-cluster/abc123def456"
ecs_task_id = ecs_task_arn.split("/")[-1]
event = emit_event(
event="acme.ecs.task.tracked",
resource={
"prefect.resource.id": f"ecs.task.{ecs_task_id}",
"prefect.resource.name": ecs_task_id,
"ecs.task_id": ecs_task_id,
"ecs.task_arn": ecs_task_arn,
},
related=[
{
"prefect.resource.id": f"prefect.flow-run.{current_flow_run_id}",
"prefect.resource.role": "flow-run",
}
],
payload={
"flow_run_id": str(current_flow_run_id),
"ecs_task_id": ecs_task_id,
"ecs_task_arn": ecs_task_arn,
},
)
return event
If you want the full flow-run context attached with a name too:
from prefect import flow
from prefect.events import emit_event
from prefect.context import FlowRunContext
@flow
def my_flow():
ctx = FlowRunContext.get()
flow_run_id = str(ctx.flow_run.id)
flow_name = ctx.flow.name
ecs_task_arn = "arn:aws:ecs:us-east-1:123456789012:task/my-cluster/abc123def456"
ecs_task_id = ecs_task_arn.split("/")[-1]
emit_event(
event="acme.ecs.task.started",
resource={
"prefect.resource.id": f"ecs.task.{ecs_task_id}",
"prefect.resource.name": ecs_task_id,
"ecs.task_id": ecs_task_id,
"ecs.task_arn": ecs_task_arn,
},
related=[
{
"prefect.resource.id": f"prefect.flow-run.{flow_run_id}",
"prefect.resource.role": "flow-run",
"prefect.resource.name": flow_name,
}
],
payload={
"flow_run_id": flow_run_id,
"ecs_task_id": ecs_task_id,
"ecs_task_arn": ecs_task_arn,
},
)
If you’re doing this in a task instead of directly in the flow:
from prefect import flow, task
from prefect.events import emit_event
from prefect.runtime import flow_run
@task
def emit_ecs_mapping(ecs_task_arn: str):
current_flow_run_id = flow_run.id
ecs_task_id = ecs_task_arn.split("/")[-1]
emit_event(
event="acme.ecs.task.mapping.created",
resource={
"prefect.resource.id": f"ecs.task.{ecs_task_id}",
"prefect.resource.name": ecs_task_id,
"ecs.task_id": ecs_task_id,
"ecs.task_arn": ecs_task_arn,
},
related=[
{
"prefect.resource.id": f"prefect.flow-run.{current_flow_run_id}",
"prefect.resource.role": "flow-run",
}
],
payload={
"flow_run_id": str(current_flow_run_id),
"ecs_task_id": ecs_task_id,
"ecs_task_arn": ecs_task_arn,
},
)
@flow
def my_flow():
ecs_task_arn = "arn:aws:ecs:us-east-1:123456789012:task/my-cluster/abc123def456"
emit_ecs_mapping(ecs_task_arn)
A few tips:
* prefect.resource.id is required on the primary resource
* prefect.resource.role is required on each related resource
* Put fields you want easy filtering on in resource / related
* Put extra details in payload
If your main goal is correlation later, I’d use a shape like this:
* Event name: acme.ecs.task.mapping.created
* Primary resource: ecs.task.<task_id>
* Related resource: prefect.flow-run.<flow_run_id>Marvin
04/28/2026, 9:54 AMAlastair
04/28/2026, 9:55 AMMarvin
04/28/2026, 9:55 AMMarvin
04/28/2026, 9:55 AMECS_CONTAINER_METADATA_URI_V4
* ECS_CONTAINER_METADATA_URI
Then you can call <metadata_uri>/task to get task-level metadata, including the task ARN.
Here’s a Prefect example that emits a custom event with both the flow run id and ECS task metadata.
import os
import requests
from prefect import flow, task
from prefect.events import emit_event
from prefect.runtime import flow_run
def get_ecs_task_metadata():
metadata_base = (
os.environ.get("ECS_CONTAINER_METADATA_URI_V4")
or os.environ.get("ECS_CONTAINER_METADATA_URI")
)
if not metadata_base:
raise RuntimeError(
"ECS container metadata endpoint not available. "
"This code must run inside an ECS task with metadata enabled."
)
response = requests.get(f"{metadata_base}/task", timeout=5)
response.raise_for_status()
return response.json()
@task
def emit_ecs_task_mapping_event():
task_metadata = get_ecs_task_metadata()
flow_run_id = str(flow_run.id)
task_arn = task_metadata["TaskARN"]
cluster = task_metadata.get("Cluster")
family = task_metadata.get("Family")
revision = task_metadata.get("Revision")
ecs_task_id = task_arn.split("/")[-1]
event = emit_event(
event="acme.ecs.task.mapping.created",
resource={
"prefect.resource.id": f"ecs.task.{ecs_task_id}",
"prefect.resource.name": ecs_task_id,
"ecs.task_id": ecs_task_id,
"ecs.task_arn": task_arn,
"ecs.cluster": cluster or "",
},
related=[
{
"prefect.resource.id": f"prefect.flow-run.{flow_run_id}",
"prefect.resource.role": "flow-run",
}
],
payload={
"flow_run_id": flow_run_id,
"ecs_task_id": ecs_task_id,
"ecs_task_arn": task_arn,
"cluster": cluster,
"family": family,
"revision": revision,
"raw_metadata": task_metadata,
},
)
return {
"flow_run_id": flow_run_id,
"ecs_task_id": ecs_task_id,
"ecs_task_arn": task_arn,
"event_id": str(event.id) if event else None,
}
@flow
def my_flow():
return emit_ecs_task_mapping_event()
if __name__ == "__main__":
print(my_flow())
A lighter-weight version if you just want the ARN:
import os
import requests
from prefect import flow
from prefect.events import emit_event
from prefect.runtime import flow_run
def get_ecs_task_arn():
metadata_base = (
os.environ.get("ECS_CONTAINER_METADATA_URI_V4")
or os.environ.get("ECS_CONTAINER_METADATA_URI")
)
if not metadata_base:
return None
response = requests.get(f"{metadata_base}/task", timeout=5)
response.raise_for_status()
return response.json()["TaskARN"]
@flow
def my_flow():
task_arn = get_ecs_task_arn()
if not task_arn:
return
task_id = task_arn.split("/")[-1]
emit_event(
event="acme.ecs.task.mapping.created",
resource={
"prefect.resource.id": f"ecs.task.{task_id}",
"prefect.resource.name": task_id,
"ecs.task_arn": task_arn,
},
related=[
{
"prefect.resource.id": f"prefect.flow-run.{flow_run.id}",
"prefect.resource.role": "flow-run",
}
],
payload={
"flow_run_id": str(flow_run.id),
"ecs_task_arn": task_arn,
"ecs_task_id": task_id,
},
)
A few practical notes:
* This only works inside the ECS task/container.
* If you run locally, those env vars won’t exist, so you should handle that case gracefully.Marvin
04/28/2026, 9:55 AM/task returns task-level metadata; TaskARN is the field you want.
* You may also want to include:
* AvailabilityZone
* LaunchType
* container image/name
* task definition family/revision
If you want a safer production version, use graceful fallback instead of raising:
def try_get_ecs_task_metadata():
metadata_base = (
os.environ.get("ECS_CONTAINER_METADATA_URI_V4")
or os.environ.get("ECS_CONTAINER_METADATA_URI")
)
if not metadata_base:
return None
try:
response = requests.get(f"{metadata_base}/task", timeout=5)
response.raise_for_status()
return response.json()
except Exception:
return None
Then emit only when metadata is present.
AWS docs for the endpoint:
* ECS task metadata endpoint v4
If you want, I can also show a version that:
* stores this as a Prefect artifact/log,
* emits it once at flow start automatically, or
* queries these custom events later by flow_run_id.Alastair
04/28/2026, 9:57 AMMarvin
04/28/2026, 9:57 AMMarvin
04/28/2026, 10:01 AMinfrastructure_pid as:
a convenient Prefect-side correlation field, but not the most direct source from inside the container.
Why I’d lean that way:
ECS metadata endpoint
* Pros
* direct from AWS / the running container
* no Prefect API round-trip
* no need to parse Prefect-specific formatting
* tells you the actual container’s current task ARN
* less coupling to Prefect internals
* Cons
* ECS-specific
* only available from inside the running ECS environment
* if you want to look it up later from outside the task, you need to persist it yourself
infrastructure_pid from Prefect API
* Pros
* available centrally from Prefect
* useful from outside the container too
* infrastructure-agnostic concept
* already tied to the flow run object
* Cons
* requires a Prefect API lookup
* format is worker-specific; for ECS it’s cluster::task_arn
* more coupled to Prefect implementation details
* if you’re asking from inside the task, it’s a little indirect
On your specific assumption:
I assume at this time it will already have been populatedUsually yes by the time user flow code is running, but I would still call it less robust than the ECS metadata endpoint. The worker launches the ECS task, then updates the flow run’s
infrastructure_pid via the Prefect API. In normal conditions, by the time your flow code starts, that field is typically populated. But conceptually that still depends on:
* the worker successfully writing it back
* the Prefect API being reachable
* timing/order of operations
So the tradeoff is:
* If you want the current task’s identity from inside the task, metadata endpoint is the strongest source.
* If you want the Prefect record of which infrastructure was attached to a flow run, infrastructure_pid is the better source.
* If you want durable correlation later, best practice is to read the metadata endpoint, then emit/persist that mapping yourself.
My practical recommendation:
Inside the flow running on ECS
1. Read task ARN from ECS metadata endpoint
2. Get flow_run.id from Prefect runtime
3. Emit a custom event or persist the mapping externally
Outside the flow / for retrospective lookup
1. Check Prefect events or your persisted mapping
2. Optionally fall back to infrastructure_pid if still present
So I’d summarize it as:
Best source of truth for the running task: ECS metadata endpoint
Best Prefect-side correlation handle: infrastructure_pid
Best long-term strategy: emit/persist your own mapping event
A robust hybrid helper would look like this:
import os
import requests
from prefect.context import FlowRunContext
def get_ecs_task_arn_from_metadata():
metadata_base = (
os.environ.get("ECS_CONTAINER_METADATA_URI_V4")
or os.environ.get("ECS_CONTAINER_METADATA_URI")
)
if not metadata_base:
return None
response = requests.get(f"{metadata_base}/task", timeout=5)
response.raise_for_status()
return response.json().get("TaskARN")
def get_ecs_task_arn_from_infrastructure_pid():
ctx = FlowRunContext.get()
if not ctx or not ctx.flow_run or not ctx.flow_run.infrastructure_pid:
return None
infrastructure_pid = ctx.flow_run.infrastructure_pid
if "::" not in infrastructure_pid:
return None
_, task_arn = infrastructure_pid.split("::", 1)
return task_arn
def get_ecs_task_arn():
return (
get_ecs_task_arn_from_metadata()
or get_ecs_task_arn_from_infrastructure_pid()
)
That gives you:
* first choice: direct AWS metadata
* fallback: Prefect’s recorded infrastructure id
Relevant docs:
* ECS task metadata endpoint v4Marvin
04/28/2026, 10:01 AM