Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/lambda-sam-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Lambda SAM template

on:
pull_request:
paths:
- "examples/aws-lambda/**"
- ".github/workflows/lambda-sam-template.yml"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
sam-deploy:
name: sam deploy validation
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- run: pipx install aws-sam-cli==1.169.0
- run: bash examples/aws-lambda/scripts/check-sam-deploy.sh
28 changes: 28 additions & 0 deletions examples/aws-lambda/scripts/check-sam-deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# `sam deploy` validates the template before it calls AWS. With fake credentials it must get past that
# validation and fail at the first upload, with and without a video image (an image function at the
# top level of template.yaml, for one, fails validation for every deploy).
set -uo pipefail
cd "$(dirname "$0")/.."
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/examples/aws-lambda" "$work/packages/aws-lambda/dist"
cp template.yaml video-function.yaml "$work/examples/aws-lambda/"
echo zip > "$work/packages/aws-lambda/dist/handler.zip"
cd "$work/examples/aws-lambda"
sam validate --lint --region us-east-1 -t template.yaml || exit 1
sam validate --lint --region us-east-1 -t video-function.yaml || exit 1
digest=sha256:$(printf '%064d' 0)
for uri in '""' "123456789012.dkr.ecr.us-east-1.amazonaws.com/hf-video@$digest"; do
# The CLI's argv (sam.ts), with --s3-bucket in place of --resolve-s3 so nothing calls AWS before the upload.
out=$(env -u AWS_PROFILE AWS_ACCESS_KEY_ID=AKIAFAKEFAKEFAKEFAKE AWS_SECRET_ACCESS_KEY=fake \
sam deploy --stack-name hf-sam-check --region us-east-1 --s3-bucket hf-sam-check-none \
--capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset \
--parameter-overrides ChromeSource=sparticuz "VideoImageUri=$uri" </dev/null 2>&1)
if ! grep -q "Unable to upload artifact" <<<"$out"; then
echo "sam deploy stopped before reaching AWS (VideoImageUri=$uri):"
echo "$out" | tail -5
exit 1
fi
echo "VideoImageUri=$uri: past validation, stopped at the upload"
done
121 changes: 101 additions & 20 deletions examples/aws-lambda/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Transform: AWS::Serverless-2016-10-31
Description: >-
HyperFrames distributed rendering — Step Functions standard workflow with
one Lambda function handling Plan, RenderChunk (fan-out via Map state),
and Assemble. One S3 bucket, alarms for runaway concurrency, Lambda
and Assemble, or Plan and RenderChunk on an optional arm64 image. One S3 bucket, alarms for runaway concurrency, Lambda
errors, and Step Functions execution failures.

Built from the handler ZIP at packages/aws-lambda/dist/handler.zip.
Expand Down Expand Up @@ -64,29 +64,29 @@ Parameters:
chunks than expected; an alarm at 10× the typical workload
protects against billing surprises.

VideoImageUri:
Type: String
Default: ""
Description: >-
Optional arm64 image (`<ECR repository URI>@sha256:<digest>`) that runs the
plan and renders the chunks. Audio and assemble stay on the x86 render
function. Empty: one x86 function does it all.

Conditions:
HasReservedConcurrency: !Not [!Equals [!Ref ReservedConcurrency, -1]]
HasVideoFunction: !Not [!Equals [!Ref VideoImageUri, ""]]

Globals:
Function:
Runtime: nodejs22.x
MemorySize: !Ref LambdaMemoryMb
Timeout: !Ref LambdaTimeoutSec
# x86_64 is required for @sparticuz/chromium — its prebuilt
# Chromium ships x86_64-only. Adopters who switch to a custom
# ARM-built chrome-headless-shell can change this to `arm64`, but
# the default ZIP build will fail to launch on Graviton.
Architectures: [x86_64]
# Lambda function-level X-Ray tracing. The state machine already
# has Tracing.Enabled: true; without this, X-Ray traces would
# terminate at the Step Functions → Lambda boundary instead of
# following into per-function spans.
Tracing: Active
# Cost-allocation tags. Setting these at the Globals level applies
# to every AWS::Serverless::Function in the template — there's
# only one today, but the contract is portable to multi-function
# variants. Bucket + state-machine carry the same tags resource-
# locally because Globals only covers functions.
# Cost-allocation tags for the render function. Bucket, state machine and
# video-function.yaml carry the same tags themselves; Globals covers only SAM functions.
Tags:
Project: !Ref ProjectName
HyperFramesComponent: lambda-renderer
Expand Down Expand Up @@ -149,14 +149,18 @@ Resources:
Description: >-
HyperFrames distributed render handler. Dispatches on event.Action.
Handler: handler.handler
Runtime: nodejs22.x
# The ZIP's @sparticuz/chromium ships x86_64 Chrome only; video-function.yaml runs chunks on arm64.
Architectures: [x86_64]
# Local path is resolved by `sam build` + `sam deploy --resolve-s3`
# (or `--s3-bucket`); the resulting CodeUri rewrites to s3://.
CodeUri: ../../packages/aws-lambda/dist/handler.zip
PackageType: Zip
# With a video function the chunks run there, so the reservation moves with them.
ReservedConcurrentExecutions: !If
- HasReservedConcurrency
- !Ref ReservedConcurrency
- HasVideoFunction
- !Ref AWS::NoValue
- !If [HasReservedConcurrency, !Ref ReservedConcurrency, !Ref AWS::NoValue]
EphemeralStorage:
Size: 10240
Environment:
Expand Down Expand Up @@ -184,6 +188,27 @@ Resources:
# CreateExportTask). Reference templates shouldn't leak overbroad
# IAM into adopters' accounts.

# ── Optional arm64 chunk renderer: the same handler as a container image ──
# Nested: `sam deploy` demands an image repository for any image function in this template.
VideoFunctionStack:
Type: AWS::CloudFormation::Stack
Condition: HasVideoFunction
Properties:
TemplateURL: video-function.yaml
Parameters:
ProjectName: !Ref ProjectName
ImageUri: !Ref VideoImageUri
RenderBucket: !Ref RenderBucket
MemorySize: !Ref LambdaMemoryMb
Timeout: !Ref LambdaTimeoutSec
ReservedConcurrency: !Ref ReservedConcurrency
ChromeSource: !Ref ChromeSource
Tags:
- Key: Project
Value: !Ref ProjectName
- Key: HyperFramesComponent
Value: lambda-renderer

# ── CloudWatch log group for the state machine ──────────────────────────
# SAM doesn't auto-create one when `LoggingConfiguration` is set, so we
# define it explicitly — that way the IAM grant on the state-machine
Expand Down Expand Up @@ -249,7 +274,13 @@ Resources:
Type: Task
Resource: arn:aws:states:::lambda:invoke
Parameters:
FunctionName: !GetAtt RenderFunction.Arn
# Chunks must run the ffmpeg build the plan records, so the plan runs where they do.
FunctionName:
!If [
HasVideoFunction,
!GetAtt VideoFunctionStack.Outputs.FunctionArn,
!GetAtt RenderFunction.Arn,
]
Payload:
Action: plan
PlanProtocol: v1
Expand Down Expand Up @@ -308,7 +339,12 @@ Resources:
Type: Task
Resource: arn:aws:states:::lambda:invoke
Parameters:
FunctionName: !GetAtt RenderFunction.Arn
FunctionName:
!If [
HasVideoFunction,
!GetAtt VideoFunctionStack.Outputs.FunctionArn,
!GetAtt RenderFunction.Arn,
]
Payload:
Action: plan
PlanProtocol: v2
Expand Down Expand Up @@ -424,7 +460,12 @@ Resources:
Type: Task
Resource: arn:aws:states:::lambda:invoke
Parameters:
FunctionName: !GetAtt RenderFunction.Arn
FunctionName:
!If [
HasVideoFunction,
!GetAtt VideoFunctionStack.Outputs.FunctionArn,
!GetAtt RenderFunction.Arn,
]
Payload:
Action: renderChunk
PlanProtocol: v1
Expand Down Expand Up @@ -542,7 +583,12 @@ Resources:
Type: Task
Resource: arn:aws:states:::lambda:invoke
Parameters:
FunctionName: !GetAtt RenderFunction.Arn
FunctionName:
!If [
HasVideoFunction,
!GetAtt VideoFunctionStack.Outputs.FunctionArn,
!GetAtt RenderFunction.Arn,
]
Payload:
Action: renderChunk
PlanProtocol: v2
Expand Down Expand Up @@ -725,7 +771,13 @@ Resources:
Statement:
- Effect: Allow
Action: lambda:InvokeFunction
Resource: !GetAtt RenderFunction.Arn
Resource:
- !GetAtt RenderFunction.Arn
- !If [
HasVideoFunction,
!GetAtt VideoFunctionStack.Outputs.FunctionArn,
!Ref AWS::NoValue,
]
# A Distributed Map runs each chunk as a child execution of this state machine.
# ARNs built by name: !Ref RenderStateMachine here would be a dependency cycle.
- PolicyName: RunDistributedMapChildren
Expand Down Expand Up @@ -780,7 +832,12 @@ Resources:
MetricName: Invocations
Dimensions:
- Name: FunctionName
Value: !Ref RenderFunction
Value:
!If [
HasVideoFunction,
!GetAtt VideoFunctionStack.Outputs.FunctionName,
!Ref RenderFunction,
]
Statistic: Sum
Period: 3600
EvaluationPeriods: 1
Expand Down Expand Up @@ -813,6 +870,25 @@ Resources:
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching

VideoFunctionErrorsAlarm:
Type: AWS::CloudWatch::Alarm
Condition: HasVideoFunction
Properties:
AlarmName: !Sub "${ProjectName}-video-function-errors"
AlarmDescription: >-
Fires if the arm64 chunk Lambda reports any errors in a 5-minute window.
Namespace: AWS/Lambda
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !GetAtt VideoFunctionStack.Outputs.FunctionName
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching

# ── CloudWatch alarm: Step Functions execution failures ─────────────────
# Fires when a state-machine execution reaches a terminal failure
# state (typed non-retryable, retry-exhausted, or top-level timeout).
Expand Down Expand Up @@ -853,6 +929,11 @@ Outputs:
Export:
Name: !Sub "${AWS::StackName}-RenderFunctionArn"

VideoFunctionArn:
Condition: HasVideoFunction
Description: ARN of the arm64 chunk-rendering function.
Value: !GetAtt VideoFunctionStack.Outputs.FunctionArn

RenderStateMachineArn:
Description: ARN of the Step Functions state machine. Pass to `aws stepfunctions start-execution`.
Value: !Ref RenderStateMachine
Expand Down
108 changes: 108 additions & 0 deletions examples/aws-lambda/video-function.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
HyperFrames arm64 video renderer: the render handler as a container image, invoked for plan and
renderChunk. Nested in template.yaml when VideoImageUri is set; plain CloudFormation, because `sam deploy`
demands an image repository for any top-level image function, even one whose Condition is false.

Parameters:
ProjectName:
Type: String
ImageUri:
Type: String
RenderBucket:
Type: String
MemorySize:
Type: Number
Timeout:
Type: Number
ReservedConcurrency:
Type: Number
ChromeSource:
Type: String

Conditions:
HasReservedConcurrency: !Not [!Equals [!Ref ReservedConcurrency, -1]]

Resources:
Role:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
- !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSXRayDaemonWriteAccess"
# The render function's S3CrudPolicy plus AbortMultipartUpload, spelled out.
Policies:
- PolicyName: render-bucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:ListBucket
- s3:GetBucketLocation
- s3:GetObjectVersion
- s3:PutObject
- s3:PutObjectAcl
- s3:GetLifecycleConfiguration
- s3:PutLifecycleConfiguration
- s3:DeleteObject
- s3:AbortMultipartUpload
Resource:
- !Sub "arn:${AWS::Partition}:s3:::${RenderBucket}"
- !Sub "arn:${AWS::Partition}:s3:::${RenderBucket}/*"
# The plan sizes an unreserved render to the account's concurrency.
- Effect: Allow
Action: lambda:GetAccountSettings
Resource: "*"
Tags:
- Key: Project
Value: !Ref ProjectName
- Key: HyperFramesComponent
Value: lambda-renderer

Function:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub "${ProjectName}-video"
Description: HyperFrames distributed render handler on arm64, invoked for plan and renderChunk.
PackageType: Image
Code:
ImageUri: !Ref ImageUri
Architectures: [arm64]
Role: !GetAtt Role.Arn
MemorySize: !Ref MemorySize
Timeout: !Ref Timeout
ReservedConcurrentExecutions: !If
- HasReservedConcurrency
- !Ref ReservedConcurrency
- !Ref AWS::NoValue
EphemeralStorage:
Size: 10240
TracingConfig:
Mode: Active
Environment:
Variables:
NODE_OPTIONS: "--enable-source-maps"
HYPERFRAMES_LAMBDA_CHROME_SOURCE: !Ref ChromeSource
TMPDIR: /tmp
HYPERFRAMES_RENDER_BUCKET: !Ref RenderBucket
HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY: !Ref ReservedConcurrency
Tags:
- Key: Project
Value: !Ref ProjectName
- Key: HyperFramesComponent
Value: lambda-renderer

Outputs:
FunctionArn:
Value: !GetAtt Function.Arn
FunctionName:
Value: !Ref Function
Loading
Loading