@hyperframes/aws-lambda 0.8.177 → 0.8.179

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,782 @@
1
+ AWSTemplateFormatVersion: "2010-09-09"
2
+ Transform: AWS::Serverless-2016-10-31
3
+ Description: >-
4
+ HyperFrames distributed rendering — Step Functions standard workflow with
5
+ one Lambda function handling Plan, RenderChunk (fan-out via Map state),
6
+ and Assemble. One S3 bucket, alarms for runaway concurrency, Lambda
7
+ errors, and Step Functions execution failures.
8
+
9
+ Built from the handler ZIP at packages/aws-lambda/dist/handler.zip.
10
+
11
+ See:
12
+ - packages/aws-lambda/README.md (handler architecture)
13
+ - examples/aws-lambda/README.md (this directory's deploy guide)
14
+
15
+ Parameters:
16
+ ProjectName:
17
+ Type: String
18
+ Default: hyperframes
19
+ Description: Name prefix applied to all created resources.
20
+
21
+ LambdaMemoryMb:
22
+ Type: Number
23
+ Default: 10240
24
+ AllowedValues: [2048, 3072, 4096, 5120, 6144, 7168, 8192, 9216, 10240]
25
+ Description: >-
26
+ Lambda memory in MB. Render workloads are CPU-bound; bumping memory
27
+ proportionally bumps the CPU share Lambda gives the function. 10 GB
28
+ (the max) is recommended for 1080p renders.
29
+
30
+ LambdaTimeoutSec:
31
+ Type: Number
32
+ Default: 900
33
+ MinValue: 60
34
+ MaxValue: 900
35
+ Description: >-
36
+ Per-invocation Lambda timeout. Render chunks at the default
37
+ chunkSize=240 frames complete in seconds; 15 minutes is the Lambda
38
+ hard ceiling and the default here to absorb cold-start variance.
39
+
40
+ ReservedConcurrency:
41
+ Type: Number
42
+ Default: -1
43
+ Description: >-
44
+ Lambda reserved concurrency cap. Set to a positive integer to bound
45
+ simultaneous chunk renders (e.g. 50 to limit cost). -1 means
46
+ unreserved: each render runs up to 90% of the account's unreserved
47
+ concurrency.
48
+
49
+ ChromeSource:
50
+ Type: String
51
+ Default: sparticuz
52
+ AllowedValues: [sparticuz, chrome-headless-shell]
53
+ Description: >-
54
+ Which Chrome runtime the bundled ZIP was built with. Must match the
55
+ `--source=` flag passed to `build-zip.ts`. The handler reads this
56
+ via the HYPERFRAMES_LAMBDA_CHROME_SOURCE env var at boot.
57
+
58
+ ChunkInvocationAlarmThreshold:
59
+ Type: Number
60
+ Default: 1000
61
+ Description: >-
62
+ CloudWatch alarm threshold for total RenderChunk invocations per
63
+ hour. The runaway-Map state pathology would fan out far more
64
+ chunks than expected; an alarm at 10× the typical workload
65
+ protects against billing surprises.
66
+
67
+ Conditions:
68
+ HasReservedConcurrency: !Not [!Equals [!Ref ReservedConcurrency, -1]]
69
+
70
+ Globals:
71
+ Function:
72
+ Runtime: nodejs22.x
73
+ MemorySize: !Ref LambdaMemoryMb
74
+ Timeout: !Ref LambdaTimeoutSec
75
+ # x86_64 is required for @sparticuz/chromium — its prebuilt
76
+ # Chromium ships x86_64-only. Adopters who switch to a custom
77
+ # ARM-built chrome-headless-shell can change this to `arm64`, but
78
+ # the default ZIP build will fail to launch on Graviton.
79
+ Architectures: [x86_64]
80
+ # Lambda function-level X-Ray tracing. The state machine already
81
+ # has Tracing.Enabled: true; without this, X-Ray traces would
82
+ # terminate at the Step Functions → Lambda boundary instead of
83
+ # following into per-function spans.
84
+ Tracing: Active
85
+ # Cost-allocation tags. Setting these at the Globals level applies
86
+ # to every AWS::Serverless::Function in the template — there's
87
+ # only one today, but the contract is portable to multi-function
88
+ # variants. Bucket + state-machine carry the same tags resource-
89
+ # locally because Globals only covers functions.
90
+ Tags:
91
+ Project: !Ref ProjectName
92
+ HyperFramesComponent: lambda-renderer
93
+ Environment:
94
+ Variables:
95
+ NODE_OPTIONS: "--enable-source-maps"
96
+ HYPERFRAMES_LAMBDA_CHROME_SOURCE: !Ref ChromeSource
97
+
98
+ Resources:
99
+ # ── S3 bucket for plan tarballs, chunk outputs, and final renders ───────
100
+ RenderBucket:
101
+ Type: AWS::S3::Bucket
102
+ DeletionPolicy: Retain
103
+ UpdateReplacePolicy: Retain
104
+ Properties:
105
+ # BucketName omitted — CloudFormation generates a unique name like
106
+ # "<stack-name>-renderbucket-<random>". S3 bucket names are capped at
107
+ # 63 chars; a static !Sub expression including ProjectName +
108
+ # AWS::AccountId + AWS::Region trips that limit when ProjectName
109
+ # carries a timestamp (e.g. the smoke script's per-run stack name).
110
+ PublicAccessBlockConfiguration:
111
+ BlockPublicAcls: true
112
+ BlockPublicPolicy: true
113
+ IgnorePublicAcls: true
114
+ RestrictPublicBuckets: true
115
+ VersioningConfiguration:
116
+ # `Suspended` keeps storage costs flat — versions are not
117
+ # retained on overwrites. Tradeoff: if an adopter writes their
118
+ # final rendered mp4 to this bucket and a re-render overwrites
119
+ # the same key, the prior version is gone. Adopters who treat
120
+ # the final mp4 as user-keepable should set this to `Enabled`
121
+ # (intermediates under `renders/` still expire via the
122
+ # lifecycle rule below regardless).
123
+ Status: Suspended
124
+ LifecycleConfiguration:
125
+ Rules:
126
+ - Id: ExpireIntermediates
127
+ Status: Enabled
128
+ Prefix: renders/
129
+ # Plan tarballs and chunk outputs are intermediate artifacts.
130
+ # Users keep the final mp4 (different key prefix); the rest
131
+ # can age out after a week to keep storage costs flat.
132
+ ExpirationInDays: 7
133
+ - Id: ExpireSharedContent
134
+ Status: Enabled
135
+ Prefix: cas/sha256/
136
+ # Project files and plan artifacts by SHA-256, shared across renders.
137
+ ExpirationInDays: 30
138
+ Tags:
139
+ - Key: Project
140
+ Value: !Ref ProjectName
141
+ - Key: HyperFramesComponent
142
+ Value: lambda-renderer
143
+
144
+ # ── Single Lambda function handling all three roles ──────────────────────
145
+ RenderFunction:
146
+ Type: AWS::Serverless::Function
147
+ Properties:
148
+ FunctionName: !Sub "${ProjectName}-render"
149
+ Description: >-
150
+ HyperFrames distributed render handler. Dispatches on event.Action.
151
+ Handler: handler.handler
152
+ # Local path is resolved by `sam build` + `sam deploy --resolve-s3`
153
+ # (or `--s3-bucket`); the resulting CodeUri rewrites to s3://.
154
+ CodeUri: handler.zip
155
+ PackageType: Zip
156
+ ReservedConcurrentExecutions: !If
157
+ - HasReservedConcurrency
158
+ - !Ref ReservedConcurrency
159
+ - !Ref AWS::NoValue
160
+ EphemeralStorage:
161
+ Size: 10240
162
+ Environment:
163
+ Variables:
164
+ # Lambda's Node 22 runtime sets these by default; explicit for
165
+ # clarity + so users can override during local SAM invoke.
166
+ TMPDIR: /tmp
167
+ HYPERFRAMES_RENDER_BUCKET: !Ref RenderBucket
168
+ HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY: !Ref ReservedConcurrency
169
+ Policies:
170
+ - S3CrudPolicy:
171
+ BucketName: !Ref RenderBucket
172
+ - Statement:
173
+ - Effect: Allow
174
+ Action: s3:AbortMultipartUpload
175
+ Resource: !Sub "arn:${AWS::Partition}:s3:::${RenderBucket}/*"
176
+ # Plan sizes an unreserved render to the account's concurrency.
177
+ - Statement:
178
+ - Effect: Allow
179
+ Action: lambda:GetAccountSettings
180
+ Resource: "*"
181
+ # CloudWatch Logs perms are covered by SAM's default
182
+ # AWSLambdaBasicExecutionRole — explicit `CloudWatchLogsFullAccess`
183
+ # would be overscope (`logs:*` on `*`, including DeleteLogGroup +
184
+ # CreateExportTask). Reference templates shouldn't leak overbroad
185
+ # IAM into adopters' accounts.
186
+
187
+ # ── CloudWatch log group for the state machine ──────────────────────────
188
+ # SAM doesn't auto-create one when `LoggingConfiguration` is set, so we
189
+ # define it explicitly — that way the IAM grant on the state-machine
190
+ # role has a destination to write to.
191
+ RenderStateMachineLogGroup:
192
+ Type: AWS::Logs::LogGroup
193
+ Properties:
194
+ LogGroupName: !Sub "/aws/states/${ProjectName}-render"
195
+ RetentionInDays: 30
196
+
197
+ # ── Step Functions state machine: Plan → Map(N) RenderChunk → Assemble ──
198
+ RenderStateMachine:
199
+ Type: AWS::Serverless::StateMachine
200
+ Properties:
201
+ Name: !Sub "${ProjectName}-render"
202
+ Type: STANDARD
203
+ Tracing:
204
+ Enabled: true
205
+ Logging:
206
+ # Without this, the `WriteCloudwatchLogs` grant on the state
207
+ # machine role would be unused — operators would see zero
208
+ # execution history outside the Step Functions console.
209
+ # `Level: ERROR` keeps log volume low; bump to `ALL` for
210
+ # heavy debugging.
211
+ Level: ERROR
212
+ IncludeExecutionData: false
213
+ Destinations:
214
+ - CloudWatchLogsLogGroup:
215
+ LogGroupArn: !GetAtt RenderStateMachineLogGroup.Arn
216
+ Definition:
217
+ Comment: >-
218
+ HyperFrames distributed render orchestration: Plan → Map(N)
219
+ RenderChunk → Assemble.
220
+ # Defensive 1-hour ceiling on the whole choreography. The
221
+ # individual states already have retries + per-task timeouts;
222
+ # this catches pathological runaways (Plan-retry storm,
223
+ # stuck-state-machine bugs) at the top before per-task budgets
224
+ # compound into a multi-hour execution. The longest legitimate
225
+ # render observed in PR 880's eval was ~3 minutes.
226
+ TimeoutSeconds: 3600
227
+ StartAt: SelectPlanProtocol
228
+ States:
229
+ SelectPlanProtocol:
230
+ Type: Choice
231
+ Choices:
232
+ - Variable: $.PlanProtocol
233
+ StringEquals: v2
234
+ Next: PlanV2
235
+ - Variable: $.PlanProtocol
236
+ StringEquals: v1
237
+ Next: Plan
238
+ - Variable: $.PlanProtocol
239
+ IsPresent: true
240
+ Next: UnsupportedPlanProtocol
241
+ Default: PlanV2
242
+
243
+ UnsupportedPlanProtocol:
244
+ Type: Fail
245
+ Error: PLAN_PROTOCOL_UNSUPPORTED
246
+ Cause: PlanProtocol must be "v1", "v2", or absent (defaults to v2).
247
+
248
+ Plan:
249
+ Type: Task
250
+ Resource: arn:aws:states:::lambda:invoke
251
+ Parameters:
252
+ FunctionName: !GetAtt RenderFunction.Arn
253
+ Payload:
254
+ Action: plan
255
+ PlanProtocol: v1
256
+ ProjectS3Uri.$: "$.ProjectS3Uri"
257
+ PlanOutputS3Prefix.$: "$.PlanOutputS3Prefix"
258
+ Config.$: "$.Config"
259
+ ResultSelector:
260
+ PlanProtocol: v1
261
+ PlanS3Uri.$: "$.Payload.PlanS3Uri"
262
+ PlanHash.$: "$.Payload.PlanHash"
263
+ ChunkCount.$: "$.Payload.ChunkCount"
264
+ MaxChunkConcurrency.$: "$.Payload.MaxChunkConcurrency"
265
+ Format.$: "$.Payload.Format"
266
+ HasAudio.$: "$.Payload.HasAudio"
267
+ AudioS3Uri.$: "$.Payload.AudioS3Uri"
268
+ ResultPath: $.Plan
269
+ Retry:
270
+ - ErrorEquals:
271
+ # These error names are thrown by the producer's plan
272
+ # stage when retrying can never help — version skew,
273
+ # determinism violations, GPU misconfiguration, font
274
+ # fetch failures, plan-size cap, unsupported format.
275
+ # Fail fast rather than burning ~120s of retry budget.
276
+ - FFMPEG_VERSION_MISMATCH
277
+ - PLAN_HASH_MISMATCH
278
+ - BROWSER_GPU_NOT_SOFTWARE
279
+ - FONT_FETCH_FAILED
280
+ - PLAN_TOO_LARGE
281
+ - PlanTooLargeError
282
+ - PLAN_PROTOCOL_UNSUPPORTED
283
+ - PlanProtocolUnsupportedError
284
+ - VIDEO_SOURCE_UNRENDERABLE
285
+ - INVALID_VIDEO_METADATA
286
+ - NOT_MEDIA_PAYLOAD
287
+ - NotMediaPayloadError
288
+ - PLAN_ARTIFACT_DIGEST_MISMATCH
289
+ - FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED
290
+ - CHUNK_EXCEEDS_WORKER_TIMEOUT
291
+ - SITE_MANIFEST_INVALID
292
+ MaxAttempts: 0
293
+ # Another render on the same function can still take the reserved slots; wait minutes.
294
+ - ErrorEquals: [Lambda.TooManyRequestsException]
295
+ IntervalSeconds: 5
296
+ MaxAttempts: 10
297
+ BackoffRate: 2
298
+ MaxDelaySeconds: 60
299
+ JitterStrategy: FULL
300
+ - ErrorEquals: [States.ALL]
301
+ IntervalSeconds: 2
302
+ MaxAttempts: 4
303
+ BackoffRate: 2
304
+ MaxDelaySeconds: 60
305
+ Next: BuildChunkList
306
+
307
+ PlanV2:
308
+ Type: Task
309
+ Resource: arn:aws:states:::lambda:invoke
310
+ Parameters:
311
+ FunctionName: !GetAtt RenderFunction.Arn
312
+ Payload:
313
+ Action: plan
314
+ PlanProtocol: v2
315
+ ProjectS3Uri.$: "$.ProjectS3Uri"
316
+ PlanOutputS3Prefix.$: "$.PlanOutputS3Prefix"
317
+ Config.$: "$.Config"
318
+ ResultSelector:
319
+ PlanProtocol: v2
320
+ PlanV2ManifestS3Uri.$: "$.Payload.PlanV2ManifestS3Uri"
321
+ PlanV2ArtifactS3Prefix.$: "$.Payload.PlanV2ArtifactS3Prefix"
322
+ PlanHash.$: "$.Payload.PlanHash"
323
+ ChunkCount.$: "$.Payload.ChunkCount"
324
+ MaxChunkConcurrency.$: "$.Payload.MaxChunkConcurrency"
325
+ Format.$: "$.Payload.Format"
326
+ HasAudio.$: "$.Payload.HasAudio"
327
+ ResultPath: $.Plan
328
+ Retry:
329
+ - ErrorEquals:
330
+ - FFMPEG_VERSION_MISMATCH
331
+ - PLAN_HASH_MISMATCH
332
+ - S3_URI_NOT_ALLOWED
333
+ - BROWSER_GPU_NOT_SOFTWARE
334
+ - FONT_FETCH_FAILED
335
+ - PLAN_TOO_LARGE
336
+ - PlanTooLargeError
337
+ - PLAN_PROTOCOL_UNSUPPORTED
338
+ - PlanProtocolUnsupportedError
339
+ - PLAN_V2_INTEGRITY_UNRECOVERABLE
340
+ - VIDEO_SOURCE_UNRENDERABLE
341
+ - INVALID_VIDEO_METADATA
342
+ - NOT_MEDIA_PAYLOAD
343
+ - NotMediaPayloadError
344
+ - PlanV2IntegrityError
345
+ - PLAN_ARTIFACT_DIGEST_MISMATCH
346
+ - FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED
347
+ - CHUNK_EXCEEDS_WORKER_TIMEOUT
348
+ - ChromeBinaryUnavailableError
349
+ - SITE_MANIFEST_INVALID
350
+ MaxAttempts: 0
351
+ - ErrorEquals: [Lambda.TooManyRequestsException]
352
+ IntervalSeconds: 5
353
+ MaxAttempts: 10
354
+ BackoffRate: 2
355
+ MaxDelaySeconds: 60
356
+ JitterStrategy: FULL
357
+ - ErrorEquals: [States.ALL]
358
+ IntervalSeconds: 2
359
+ MaxAttempts: 4
360
+ BackoffRate: 2
361
+ MaxDelaySeconds: 60
362
+ Next: BuildChunkList
363
+
364
+ BuildChunkList:
365
+ # Translate ChunkCount into an array `[0, 1, ..., N-1]` so the
366
+ # Map state below has something to iterate. Range is the
367
+ # idiomatic Step Functions intrinsic for this; no Lambda call
368
+ # required.
369
+ Type: Pass
370
+ Parameters:
371
+ ChunkIndexes.$: "States.ArrayRange(0, States.MathAdd($.Plan.ChunkCount, -1), 1)"
372
+ ResultPath: $.Iterator
373
+ Next: AssertChunkCount
374
+
375
+ AssertChunkCount:
376
+ # Defensive gate: `resolveChunkPlan` guarantees ChunkCount ≥ 1,
377
+ # but if some future regression let a zero-chunk plan through,
378
+ # `RenderChunks` (Map state) would iterate zero times and
379
+ # `Assemble` would receive an empty `ChunkS3Uris` array — silently
380
+ # producing an empty output. Fail fast instead.
381
+ Type: Choice
382
+ Choices:
383
+ - Variable: $.Plan.ChunkCount
384
+ NumericGreaterThan: 0
385
+ Next: SelectWorkerProtocol
386
+ Default: PlanProducedZeroChunks
387
+
388
+ PlanProducedZeroChunks:
389
+ Type: Fail
390
+ Error: PLAN_TOO_LARGE
391
+ Cause: Plan returned ChunkCount=0 — non-retryable producer-side invariant violation.
392
+
393
+ SelectWorkerProtocol:
394
+ Type: Choice
395
+ Choices:
396
+ - Variable: $.Plan.PlanProtocol
397
+ StringEquals: v2
398
+ Next: RenderChunksV2
399
+ Default: RenderChunks
400
+
401
+ RenderChunks:
402
+ Type: Map
403
+ Label: RenderChunks
404
+ ItemsPath: $.Iterator.ChunkIndexes
405
+ ItemSelector:
406
+ ChunkIndex.$: "$$.Map.Item.Value"
407
+ PlanS3Uri.$: "$.Plan.PlanS3Uri"
408
+ PlanHash.$: "$.Plan.PlanHash"
409
+ ChunkOutputS3Prefix.$: "$.PlanOutputS3Prefix"
410
+ Format.$: "$.Plan.Format"
411
+ # Chunks past the plan's concurrency queue here instead of being throttled by Lambda.
412
+ MaxConcurrencyPath: $.Plan.MaxChunkConcurrency
413
+ ResultPath: $.Chunks
414
+ ItemProcessor:
415
+ ProcessorConfig:
416
+ # Inline Map runs at most 40 iterations at once. STANDARD children
417
+ # because EXPRESS ones stop at 5 minutes and a chunk may run 15.
418
+ Mode: DISTRIBUTED
419
+ ExecutionType: STANDARD
420
+ StartAt: RenderChunk
421
+ States:
422
+ RenderChunk:
423
+ Type: Task
424
+ Resource: arn:aws:states:::lambda:invoke
425
+ Parameters:
426
+ FunctionName: !GetAtt RenderFunction.Arn
427
+ Payload:
428
+ Action: renderChunk
429
+ PlanProtocol: v1
430
+ ChunkIndex.$: "$.ChunkIndex"
431
+ PlanS3Uri.$: "$.PlanS3Uri"
432
+ PlanHash.$: "$.PlanHash"
433
+ ChunkOutputS3Prefix.$: "$.ChunkOutputS3Prefix"
434
+ Format.$: "$.Format"
435
+ ResultSelector:
436
+ ChunkS3Uri.$: "$.Payload.ChunkS3Uri"
437
+ ChunkIndex.$: "$.Payload.ChunkIndex"
438
+ Sha256.$: "$.Payload.Sha256"
439
+ FramesEncoded.$: "$.Payload.FramesEncoded"
440
+ DurationMs.$: "$.Payload.DurationMs"
441
+ Retry:
442
+ - ErrorEquals:
443
+ - FFMPEG_VERSION_MISMATCH
444
+ - PLAN_HASH_MISMATCH
445
+ - BROWSER_GPU_NOT_SOFTWARE
446
+ - PLAN_TOO_LARGE
447
+ - PlanTooLargeError
448
+ - PLAN_PROTOCOL_UNSUPPORTED
449
+ - PlanProtocolUnsupportedError
450
+ - INVALID_VIDEO_METADATA
451
+ - PLAN_ARTIFACT_DIGEST_MISMATCH
452
+ MaxAttempts: 0
453
+ - ErrorEquals: [Lambda.TooManyRequestsException]
454
+ IntervalSeconds: 5
455
+ MaxAttempts: 10
456
+ BackoffRate: 2
457
+ MaxDelaySeconds: 60
458
+ JitterStrategy: FULL
459
+ - ErrorEquals: [States.ALL]
460
+ IntervalSeconds: 2
461
+ MaxAttempts: 4
462
+ BackoffRate: 2
463
+ MaxDelaySeconds: 60
464
+ End: true
465
+ Next: Assemble
466
+
467
+ Assemble:
468
+ Type: Task
469
+ Resource: arn:aws:states:::lambda:invoke
470
+ Parameters:
471
+ FunctionName: !GetAtt RenderFunction.Arn
472
+ Payload:
473
+ Action: assemble
474
+ PlanProtocol: v1
475
+ PlanS3Uri.$: "$.Plan.PlanS3Uri"
476
+ ChunkS3Uris.$: "$.Chunks[*].ChunkS3Uri"
477
+ AudioS3Uri.$: "$.Plan.AudioS3Uri"
478
+ OutputS3Uri.$: "$.OutputS3Uri"
479
+ Format.$: "$.Plan.Format"
480
+ ResultSelector:
481
+ OutputS3Uri.$: "$.Payload.OutputS3Uri"
482
+ FramesEncoded.$: "$.Payload.FramesEncoded"
483
+ FileSize.$: "$.Payload.FileSize"
484
+ ResultPath: $.Output
485
+ Retry:
486
+ - ErrorEquals:
487
+ # Same non-retryable error names as the Plan state's
488
+ # gate — these surface at assemble time too because
489
+ # ffmpeg-driven concat picks up version drift and we
490
+ # re-verify plan hash + format at assemble. Skip the
491
+ # retry storm; fail fast.
492
+ - FFMPEG_VERSION_MISMATCH
493
+ - PLAN_HASH_MISMATCH
494
+ - FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED
495
+ - PLAN_TOO_LARGE
496
+ - PlanTooLargeError
497
+ - PLAN_PROTOCOL_UNSUPPORTED
498
+ - PlanProtocolUnsupportedError
499
+ - PLAN_ARTIFACT_DIGEST_MISMATCH
500
+ MaxAttempts: 0
501
+ - ErrorEquals: [Lambda.TooManyRequestsException]
502
+ IntervalSeconds: 5
503
+ MaxAttempts: 10
504
+ BackoffRate: 2
505
+ MaxDelaySeconds: 60
506
+ JitterStrategy: FULL
507
+ - ErrorEquals: [States.ALL]
508
+ IntervalSeconds: 2
509
+ MaxAttempts: 4
510
+ BackoffRate: 2
511
+ MaxDelaySeconds: 60
512
+ End: true
513
+
514
+ RenderChunksV2:
515
+ Type: Map
516
+ Label: RenderChunksV2
517
+ ItemsPath: $.Iterator.ChunkIndexes
518
+ ItemSelector:
519
+ ChunkIndex.$: "$$.Map.Item.Value"
520
+ PlanV2ManifestS3Uri.$: "$.Plan.PlanV2ManifestS3Uri"
521
+ PlanV2ArtifactS3Prefix.$: "$.Plan.PlanV2ArtifactS3Prefix"
522
+ PlanHash.$: "$.Plan.PlanHash"
523
+ ChunkOutputS3Prefix.$: "$.PlanOutputS3Prefix"
524
+ Format.$: "$.Plan.Format"
525
+ # Chunks past the plan's concurrency queue here instead of being throttled by Lambda.
526
+ MaxConcurrencyPath: $.Plan.MaxChunkConcurrency
527
+ ResultPath: $.Chunks
528
+ ItemProcessor:
529
+ ProcessorConfig:
530
+ Mode: DISTRIBUTED
531
+ ExecutionType: STANDARD
532
+ StartAt: RenderChunkV2
533
+ States:
534
+ RenderChunkV2:
535
+ Type: Task
536
+ Resource: arn:aws:states:::lambda:invoke
537
+ Parameters:
538
+ FunctionName: !GetAtt RenderFunction.Arn
539
+ Payload:
540
+ Action: renderChunk
541
+ PlanProtocol: v2
542
+ ChunkIndex.$: "$.ChunkIndex"
543
+ PlanV2ManifestS3Uri.$: "$.PlanV2ManifestS3Uri"
544
+ PlanV2ArtifactS3Prefix.$: "$.PlanV2ArtifactS3Prefix"
545
+ PlanHash.$: "$.PlanHash"
546
+ ChunkOutputS3Prefix.$: "$.ChunkOutputS3Prefix"
547
+ Format.$: "$.Format"
548
+ ResultSelector:
549
+ ChunkS3Uri.$: "$.Payload.ChunkS3Uri"
550
+ ChunkIndex.$: "$.Payload.ChunkIndex"
551
+ Sha256.$: "$.Payload.Sha256"
552
+ FramesEncoded.$: "$.Payload.FramesEncoded"
553
+ DurationMs.$: "$.Payload.DurationMs"
554
+ Retry:
555
+ - ErrorEquals:
556
+ - FFMPEG_VERSION_MISMATCH
557
+ - PLAN_HASH_MISMATCH
558
+ - S3_URI_NOT_ALLOWED
559
+ - BROWSER_GPU_NOT_SOFTWARE
560
+ - PLAN_TOO_LARGE
561
+ - PlanTooLargeError
562
+ - PLAN_PROTOCOL_UNSUPPORTED
563
+ - PlanProtocolUnsupportedError
564
+ - PLAN_V2_INTEGRITY_UNRECOVERABLE
565
+ - INVALID_VIDEO_METADATA
566
+ - PlanV2IntegrityError
567
+ - PLAN_ARTIFACT_DIGEST_MISMATCH
568
+ - ChromeBinaryUnavailableError
569
+ MaxAttempts: 0
570
+ - ErrorEquals: [Lambda.TooManyRequestsException]
571
+ IntervalSeconds: 5
572
+ MaxAttempts: 10
573
+ BackoffRate: 2
574
+ MaxDelaySeconds: 60
575
+ JitterStrategy: FULL
576
+ - ErrorEquals: [States.ALL]
577
+ IntervalSeconds: 2
578
+ MaxAttempts: 4
579
+ BackoffRate: 2
580
+ MaxDelaySeconds: 60
581
+ End: true
582
+ Next: AssembleV2
583
+
584
+ AssembleV2:
585
+ Type: Task
586
+ Resource: arn:aws:states:::lambda:invoke
587
+ Parameters:
588
+ FunctionName: !GetAtt RenderFunction.Arn
589
+ Payload:
590
+ Action: assemble
591
+ PlanProtocol: v2
592
+ PlanV2ManifestS3Uri.$: "$.Plan.PlanV2ManifestS3Uri"
593
+ PlanV2ArtifactS3Prefix.$: "$.Plan.PlanV2ArtifactS3Prefix"
594
+ PlanHash.$: "$.Plan.PlanHash"
595
+ ChunkS3Uris.$: "$.Chunks[*].ChunkS3Uri"
596
+ AudioS3Uri: null
597
+ OutputS3Uri.$: "$.OutputS3Uri"
598
+ Format.$: "$.Plan.Format"
599
+ ResultSelector:
600
+ OutputS3Uri.$: "$.Payload.OutputS3Uri"
601
+ FramesEncoded.$: "$.Payload.FramesEncoded"
602
+ FileSize.$: "$.Payload.FileSize"
603
+ ResultPath: $.Output
604
+ Retry:
605
+ - ErrorEquals:
606
+ - FFMPEG_VERSION_MISMATCH
607
+ - PLAN_HASH_MISMATCH
608
+ - S3_URI_NOT_ALLOWED
609
+ - FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED
610
+ - PLAN_TOO_LARGE
611
+ - PlanTooLargeError
612
+ - PLAN_PROTOCOL_UNSUPPORTED
613
+ - PlanProtocolUnsupportedError
614
+ - PLAN_V2_INTEGRITY_UNRECOVERABLE
615
+ - PlanV2IntegrityError
616
+ - PLAN_ARTIFACT_DIGEST_MISMATCH
617
+ - ChromeBinaryUnavailableError
618
+ MaxAttempts: 0
619
+ - ErrorEquals: [Lambda.TooManyRequestsException]
620
+ IntervalSeconds: 5
621
+ MaxAttempts: 10
622
+ BackoffRate: 2
623
+ MaxDelaySeconds: 60
624
+ JitterStrategy: FULL
625
+ - ErrorEquals: [States.ALL]
626
+ IntervalSeconds: 2
627
+ MaxAttempts: 4
628
+ BackoffRate: 2
629
+ MaxDelaySeconds: 60
630
+ End: true
631
+ Role: !GetAtt RenderStateMachineRole.Arn
632
+
633
+ RenderStateMachineRole:
634
+ Type: AWS::IAM::Role
635
+ Properties:
636
+ AssumeRolePolicyDocument:
637
+ Version: "2012-10-17"
638
+ Statement:
639
+ - Effect: Allow
640
+ Principal:
641
+ Service: states.amazonaws.com
642
+ Action: sts:AssumeRole
643
+ Policies:
644
+ - PolicyName: InvokeRenderFunction
645
+ PolicyDocument:
646
+ Version: "2012-10-17"
647
+ Statement:
648
+ - Effect: Allow
649
+ Action: lambda:InvokeFunction
650
+ Resource: !GetAtt RenderFunction.Arn
651
+ # A Distributed Map runs each chunk as a child execution of this state machine.
652
+ # ARNs built by name: !Ref RenderStateMachine here would be a dependency cycle.
653
+ - PolicyName: RunDistributedMapChildren
654
+ PolicyDocument:
655
+ Version: "2012-10-17"
656
+ Statement:
657
+ - Effect: Allow
658
+ Action: states:StartExecution
659
+ Resource: !Sub "arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${ProjectName}-render"
660
+ - Effect: Allow
661
+ Action:
662
+ - states:DescribeExecution
663
+ - states:StopExecution
664
+ Resource: !Sub "arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:execution:${ProjectName}-render:*"
665
+ - PolicyName: WriteCloudwatchLogs
666
+ PolicyDocument:
667
+ Version: "2012-10-17"
668
+ Statement:
669
+ - Effect: Allow
670
+ Action:
671
+ - logs:CreateLogDelivery
672
+ - logs:GetLogDelivery
673
+ - logs:UpdateLogDelivery
674
+ - logs:DeleteLogDelivery
675
+ - logs:ListLogDeliveries
676
+ - logs:PutResourcePolicy
677
+ - logs:DescribeResourcePolicies
678
+ - logs:DescribeLogGroups
679
+ Resource: "*"
680
+ - PolicyName: XRayTracing
681
+ PolicyDocument:
682
+ Version: "2012-10-17"
683
+ Statement:
684
+ - Effect: Allow
685
+ Action:
686
+ - xray:PutTraceSegments
687
+ - xray:PutTelemetryRecords
688
+ Resource: "*"
689
+
690
+ # ── CloudWatch alarm: runaway chunk invocations ─────────────────────────
691
+ RenderChunkInvocationAlarm:
692
+ Type: AWS::CloudWatch::Alarm
693
+ Properties:
694
+ AlarmName: !Sub "${ProjectName}-runaway-chunk-invocations"
695
+ AlarmDescription: >-
696
+ Fires if RenderChunk Lambda invocations exceed the configured
697
+ threshold in a 1-hour window. The Map state's MaxConcurrency cap
698
+ protects against simultaneous fan-out, but a runaway state
699
+ machine that triggers many sequential renders would still rack
700
+ up cost; this alarm catches that pattern.
701
+ Namespace: AWS/Lambda
702
+ MetricName: Invocations
703
+ Dimensions:
704
+ - Name: FunctionName
705
+ Value: !Ref RenderFunction
706
+ Statistic: Sum
707
+ Period: 3600
708
+ EvaluationPeriods: 1
709
+ Threshold: !Ref ChunkInvocationAlarmThreshold
710
+ ComparisonOperator: GreaterThanThreshold
711
+ TreatMissingData: notBreaching
712
+
713
+ # ── CloudWatch alarm: Lambda function errors ────────────────────────────
714
+ # Fires on any non-zero error rate. The invocation alarm above catches
715
+ # *too many calls*; this catches *calls that failed*. Without it,
716
+ # silent per-chunk failures (a non-retryable error inside the
717
+ # producer) would only surface by reading Step Functions execution
718
+ # history.
719
+ RenderFunctionErrorsAlarm:
720
+ Type: AWS::CloudWatch::Alarm
721
+ Properties:
722
+ AlarmName: !Sub "${ProjectName}-render-function-errors"
723
+ AlarmDescription: >-
724
+ Fires if the render Lambda reports any errors in a 5-minute
725
+ window. Set EvaluationPeriods=1 so a single failure pages.
726
+ Namespace: AWS/Lambda
727
+ MetricName: Errors
728
+ Dimensions:
729
+ - Name: FunctionName
730
+ Value: !Ref RenderFunction
731
+ Statistic: Sum
732
+ Period: 300
733
+ EvaluationPeriods: 1
734
+ Threshold: 1
735
+ ComparisonOperator: GreaterThanOrEqualToThreshold
736
+ TreatMissingData: notBreaching
737
+
738
+ # ── CloudWatch alarm: Step Functions execution failures ─────────────────
739
+ # Fires when a state-machine execution reaches a terminal failure
740
+ # state (typed non-retryable, retry-exhausted, or top-level timeout).
741
+ # Complementary to the Lambda Errors alarm: SFN failures include
742
+ # Choice-state Fail branches (PlanProducedZeroChunks) that bypass
743
+ # Lambda entirely, plus retry-exhaustion of transient errors that
744
+ # individual Lambda invocations counted as successful "retries".
745
+ RenderStateMachineFailedAlarm:
746
+ Type: AWS::CloudWatch::Alarm
747
+ Properties:
748
+ AlarmName: !Sub "${ProjectName}-render-state-machine-failed"
749
+ AlarmDescription: >-
750
+ Fires when the render state machine reports a failed
751
+ execution. Catches retry-exhaustion + typed non-retryable +
752
+ TimeoutSeconds cases that the Lambda Errors metric misses.
753
+ Namespace: AWS/States
754
+ MetricName: ExecutionsFailed
755
+ Dimensions:
756
+ - Name: StateMachineArn
757
+ Value: !Ref RenderStateMachine
758
+ Statistic: Sum
759
+ Period: 300
760
+ EvaluationPeriods: 1
761
+ Threshold: 1
762
+ ComparisonOperator: GreaterThanOrEqualToThreshold
763
+ TreatMissingData: notBreaching
764
+
765
+ Outputs:
766
+ RenderBucketName:
767
+ Description: S3 bucket for plan tarballs, chunk outputs, and final renders.
768
+ Value: !Ref RenderBucket
769
+ Export:
770
+ Name: !Sub "${AWS::StackName}-RenderBucket"
771
+
772
+ RenderFunctionArn:
773
+ Description: ARN of the Lambda function. Pass to `aws lambda invoke` for local testing.
774
+ Value: !GetAtt RenderFunction.Arn
775
+ Export:
776
+ Name: !Sub "${AWS::StackName}-RenderFunctionArn"
777
+
778
+ RenderStateMachineArn:
779
+ Description: ARN of the Step Functions state machine. Pass to `aws stepfunctions start-execution`.
780
+ Value: !Ref RenderStateMachine
781
+ Export:
782
+ Name: !Sub "${AWS::StackName}-RenderStateMachineArn"