Below is a structured “deep note” on executing an ECS job using Lambda, tuned to the architecture you’re building (video transcoding on Fargate).
Imagine this requirement:
- “Every time a large video lands in S3, start an isolated FFmpeg container that processes just that video, with its own CPU/memory, network, and IAM permissions, then disappears.”
You don’t want:
- a forever-running EC2 worker,
- a monolithic app polling S3,
- or a Lambda trying to do heavy FFmpeg work itself.
You want a tiny brain (Lambda) that reacts to events and spins up short-lived workers (ECS tasks) on demand.
This is exactly what “executing an ECS job using Lambda” gives you.
Concept:
Use a Lambda function as an event-driven controller that calls RunTask on ECS to start a one-shot ECS task (e.g., Fargate) for each unit of work (one video). The ECS task runs with its own IAM roles, resources, and container image, and exits when it’s done.
Why it exists:
- Lambda is great at reacting (S3 events, API calls, queues), but bad at long-running heavy CPU work.
- ECS tasks are great at running heavy, long, containerized jobs, but not event-aware by themselves.
- Glue them together and you get: “event → dedicated container job → done.”
Use Lambda as the lightweight scheduler that reacts to events and starts properly-permissioned, one-shot ECS tasks (Fargate) to do the real heavy work.
Think of the system as:
Doorbell (Lambda) Movers (ECS tasks)
─────────────── ────────────────
hears a knock carry the furniture
checks what it is do heavy work
calls movers leave when finished
- Lambda = brains: quick, event-driven, low resource.
- ECS task = muscle: slower to start, but powerful, isolated, and configurable.
Lambda does not “become” the worker. Instead, it orders a worker from ECS, specifying:
- which task definition to run (container image + CPU/memory),
- what environment variables to give (job inputs),
- which IAM roles the worker may use (what it can touch).
Problem before this pattern:
- Only Lambda:
- Limited runtime, ephemeral filesystem, poor fit for long FFmpeg jobs on large videos.
- Hard to control CPU/memory and container-level tooling compared to ECS.
- Only ECS:
- You still need something to decide when to start tasks.
- If you keep workers running, you pay even when there’s no work.
- Polling S3 from ECS is wasteful and adds latency.
This pattern is better because:
- Lambda gives you event-driven orchestration and scale-to-zero brain.
- ECS tasks give you containerized, resource-tuned workers for the heavy job.
- IAM separation lets the dispatcher (Lambda) and the worker (ECS task) have different, minimal permissions.
This pattern appears a lot in real systems:
- Media platforms: per-video transcoding, thumbnail generation, audio waveform extraction.
- Data engineering: per-file ETL, CSV → Parquet conversion, batch ingest.
- ML pipelines: feature extraction, batch inference, data preprocessing jobs.
- Security & compliance: per-object scanning, log normalization, report generation.
- CI-style workflows: “job runners” that spin up containers for tests or builds.
Anywhere you want “one job = one container”, a short-lived, isolated ECS task triggered by an event is a good fit.
Let’s outline the flow you are building:
1. S3 receives a new input video
↓ (S3 event)
2. Lambda is triggered
↓
3. Lambda:
- parses event (bucket/key)
- constructs RunTask request
- calls ECS RunTask with overrides (JOB_ID, INPUT_BUCKET, INPUT_KEY, OUTPUT_BUCKET)
↓
4. ECS:
- uses task definition (image, CPU, memory, roles)
- runs Fargate task with those env vars
↓
5. Worker container:
- reads env vars
- downloads input from S3
- runs FFmpeg
- uploads outputs to S3
- updates DB (optional)
- exits
Lambda never opens the video file itself; it only passes coordinates and asks ECS to run the actual work.
The most important “internal” logic is IAM. There are three distinct identities:
Lambda execution role
↓ calls
ECS service (RunTask)
↓ assumes
Task execution role
↓ sets up container, pulls image, logs, etc.
Task role
↓ used by your app code for S3, DB, etc.
-
Lambda execution role
- What your Lambda runs as.
- Needs permission to start tasks and pass roles.
-
Task execution role
- Used by ECS infrastructure (before your app starts).
- Pulls image from ECR, writes logs, fetches secrets.
-
Task role
- Used by your FFmpeg container.
- Grants access to S3 buckets, DB, queues.
Understanding this separation is key to understanding why iam:PassRole exists.
Responsibility:
- React to an event (S3).
- Translate it into a job request.
- Ask ECS to run a task for that job.
Inputs:
S3Event(bucket, key).- Config: task definition ARN, cluster ARN, subnets, security groups, output bucket.
Outputs:
- An ECS task started (or an error).
Core logic (simplified):
const command = new RunTaskCommand({
taskDefinition: "arn:...:task-definition/video-transcoder:1",
cluster: "arn:...:cluster/video-transcoding-cluster",
launchType: "FARGATE",
networkConfiguration: { ... },
overrides: {
containerOverrides: [
{
name: "video-transcoder",
environment: [
{ name: "JOB_ID", value: crypto.randomUUID() },
{ name: "INPUT_BUCKET", value: tempBucketName },
{ name: "INPUT_KEY", value: objectKey },
{ name: "OUTPUT_BUCKET", value: process.env.OUTPUT_BUCKET },
],
},
],
},
});
const response = await ecsClient.send(command);
if (response.failures?.length) {
throw new Error(JSON.stringify(response.failures));
}IAM logic you must satisfy:
- Lambda execution role:
ecs:RunTaskon the task definition.iam:PassRoleon:- the ECS task execution role,
- the task role (if present).
This is the “role delegation” point.
Responsibility:
- Describe how to run the worker container.
Contains:
- Container image (from ECR).
- CPU/memory.
- Environment variables (base).
- Task execution role ARN.
- Task role ARN.
- Network mode (awsvpc).
- Logging configuration.
The task definition is a blueprint, not a running thing. Lambda’s RunTask is: “Instantiate this blueprint once, with some overrides.”
Responsibility:
- Execute one job (one video).
Inputs:
- Job context via env vars (
JOB_ID,INPUT_BUCKET,INPUT_KEY,OUTPUT_BUCKET). - Credentials from task role (via ECS + STS).
- Network connectivity (via subnets, security groups).
Outputs:
- Transcoded files in output bucket.
- Optional status updates in DB or queues.
Internal logic (conceptually):
// inside container
const jobId = process.env.JOB_ID;
const inputBucket = process.env.INPUT_BUCKET;
const inputKey = process.env.INPUT_KEY;
const outputBucket = process.env.OUTPUT_BUCKET;
// 1. download from S3
// 2. run ffmpeg
// 3. upload outputs
// 4. update DB / status
// 5. exitWhen it exits, ECS marks task as STOPPED; CloudWatch logs record its lifecycle.
+---------------------+
| S3 Bucket |
| (input videos) |
+----------+----------+
|
| S3 Event
v
+--------+--------+
| Lambda |
| (dispatcher) |
+--------+--------+
|
| ecs:RunTask + iam:PassRole
v
+---------+---------+
| ECS |
| Task on Fargate |
+---------+---------+
|
| uses Task Role
v
+----------+----------+
| S3 Bucket |
| (output videos) |
+---------------------+
Lambda execution role
├─ ecs:RunTask on video-transcoder:*
└─ iam:PassRole on:
- ecsTaskExecutionRole
- FfmpegTaskRole
Task execution role
└─ permissions: ECR pull, CloudWatch logs, secrets
Task role
└─ permissions: S3 read/write, DB access, etc.
-
RunTask
API call that tells ECS: “Start N tasks using this task definition and configuration.” It’s scheduling authority. -
Task definition
A JSON blueprint describing how containers should run: image, CPU/mem, env vars, IAM roles, etc. -
Task execution role
Role assumed by the ECS agent / infrastructure to pull container images and send logs. -
Task role (task IAM role)
Role assumed by your container code so it can call S3, DynamoDB, etc. No credentials in code needed. -
Lambda execution role
Role under which the Lambda handler runs. Governs what AWS APIs the Lambda itself can call (likeRunTask). -
iam:PassRole
Permission that lets a caller say: “Service X should use role Y.” This is delegation, not self-assumption.
Let’s walk one S3 upload all the way through:
- User uploads
videos/source/film.mp4to S3. - S3 fires an event to your Lambda’s trigger.
- Lambda handler runs:
- Extracts bucket =
temp-bucket, key =videos/source/film.mp4. - Generates
JOB_ID = 550e8400-e29b-41d4-a716-446655440000. - Prepares
RunTaskrequest with:- taskDefinition:
video-transcoder:1. - cluster:
video-transcoding-cluster. - network configuration (subnets, SGs).
- containerOverrides:
JOB_ID,INPUT_BUCKET,INPUT_KEY,OUTPUT_BUCKET.
- taskDefinition:
- Extracts bucket =
- Lambda calls
RunTask. - IAM evaluates:
- Lambda role has
ecs:RunTask? - Lambda role has
iam:PassRolefor:ecsTaskExecutionRole?FfmpegTaskRole?
- Lambda role has
- If allowed:
- ECS pulls the image from ECR using task execution role.
- ECS starts a Fargate task in your subnets.
- The task container starts:
- It receives env vars you passed.
- It receives temporary credentials for task role.
- Your worker code:
- Downloads
film.mp4fromtemp-bucket. - Runs FFmpeg to produce e.g.
film_720p.mp4,film_1080p.mp4. - Uploads outputs to
final-videos-bucket. - Updates DB (if you implemented it).
- Exits.
- Downloads
- ECS marks the task as
STOPPED. - Logs are visible in CloudWatch for debugging.
Lambda never handled the file data directly – it just orchestrated.
-
Lambda’s
RunTasksucceeds but ECS fails to start the task- The API call returns HTTP 200 with a
failuresarray. - You must check
response.failuresin Lambda.
- The API call returns HTTP 200 with a
-
Network misconfiguration
- Fargate tasks in private subnets without NAT or VPC endpoints can’t reach:
- S3,
- ECR,
- CloudWatch.
- You see image pull errors, timeouts, or no logs.
- Fargate tasks in private subnets without NAT or VPC endpoints can’t reach:
-
Missing
iam:PassRole- Lambda gets
AccessDeniedExceptionwhen callingRunTask. - The task definition itself might be correct, but the caller is not allowed to delegate the roles.
- Lambda gets
-
Task role too permissive
- Worker can read/write more buckets or resources than necessary.
- In a multi-tenant environment, this is a security risk.
-
Poison job
- Bad input file causes FFmpeg to crash repeatedly.
- Without a DLQ or retry strategy, you might keep retrying the same failing job.
-
Confusing task execution role and task role
- Execution role = infra side; task role = app side.
- Giving S3 permissions to the execution role won’t help your app code.
-
Forgetting
iam:PassRole- Having only
ecs:RunTaskis not enough. - You must allow Lambda to pass the exact roles used by the task.
- Having only
-
Hardcoding infra details in code
- Task definition ARN, cluster ARN, subnet IDs, security group IDs embedded in code.
- Better: configure via env vars so you can change environments without code changes.
-
Not checking for
response.failures- The call can succeed but still not start the task.
- Always inspect the response.
-
Running Lambda logic in the task
- Trying to do worker-like work in Lambda (e.g., running FFmpeg in Lambda directly) instead of ECS defeats the purpose.
Benefits:
- Event-driven, scale-to-zero control plane.
- Clear separation between orchestration (Lambda) and execution (ECS task).
- Containerized, resource-tuned workers with per-job isolation.
- Strong IAM isolation between dispatcher and worker.
Costs:
- More moving parts (Lambda, ECS, ECR, VPC, IAM roles).
- Need to understand
iam:PassRoleand trust policies. - Need to design for failures across multiple services (S3, Lambda, ECS, S3 again).
- Slight latency overhead for task startup (cold start of Fargate tasks).
As a system designer, you’re trading simplicity (single service) for flexibility and robustness (two services with clear responsibilities).
| Approach | Pros | Cons | When to use |
|---|---|---|---|
| Lambda only (FFmpeg in Lambda) | Simple architecture, no ECS | Runtime limits, bad for large videos | Tiny workload, short jobs |
| ECS service (always-on workers) | Less orchestration logic | Pay for idling workers, custom scheduling | Constant, steady load |
| ECS job via Lambda (your pattern) | Event-driven, scale-to-zero, per-job isolation | More IAM & infra complexity | Spiky, heavy, per-file jobs |
For large, spiky video processing, Lambda → ECS task is usually the sweet spot.
- What new failure modes do you introduce by splitting orchestration and execution into different services?
- How would you add retries and a DLQ to this pipeline so bad inputs don’t cause infinite loops?
- How would you track job status across S3, Lambda, ECS, and your DB in a reliable way?
- What happens if two S3 events for the same file arrive? How would you design idempotency?
- How could you extend this to run multiple different kinds of jobs (e.g., thumbnails vs full transcode) off the same S3 events?
- When would you prefer Step Functions over plain Lambda for orchestrating ECS jobs?
- Lambda is your event-driven scheduler, not your video processor.
- ECS tasks (Fargate) are your one-job workers, tuned for heavy, long-running tasks.
- ECS task definitions encode:
- container image,
- resources,
- IAM roles (execution & task).
ecs:RunTaskgives Lambda permission to start tasks of a particular type.iam:PassRolegives Lambda permission to delegate specific IAM roles to ECS tasks.- The mental model is delegation: Lambda asks ECS to run a worker with certain powers, and PassRole decides if that delegation is allowed.
Try to answer these from memory:
- Explain in one sentence what Lambda is doing when it executes an ECS job.
- Draw an ASCII diagram showing S3 → Lambda → ECS task → S3, including the three IAM roles involved.
- What is the difference between the ECS task execution role and the task role?
- In your own words, why does Lambda need
iam:PassRolewhen callingRunTask? - What happens if Lambda has
ecs:RunTaskbut notiam:PassRolefor the roles referenced in the task definition? - Imagine Fargate tasks run in a private subnet with no NAT and no VPC endpoints. What breaks and why?
- How would you add job IDs so you can match ECS tasks back to S3 events or DB records?
- Compare: running FFmpeg inside Lambda vs running FFmpeg inside an ECS Fargate task. What is the main trade-off?
- What checks should you add in your Lambda to be sure the ECS task really started?
- How would you prevent a compromised Lambda from abusing
iam:PassRoleto escalate privileges?
If you want to go deeper, natural next topics:
- ECS task role vs task execution role and how credentials are vended to containers.
- Designing robust retry and DLQ patterns for S3 → Lambda → ECS.
- ECS networking for Fargate (NAT, VPC endpoints, security groups).
- Observability: mapping logs and metrics across S3, Lambda, ECS, and your application.
- Step Functions vs Lambda as orchestrators for multi-step workflows.
Once you see this pattern clearly, you’ll notice it everywhere: small brain, big muscles — a lightweight control plane launching heavyweight workers with carefully scoped permissions.