Deploying Remotion Lambda with Terraformv4.0.544
You can deploy Remotion Lambda using Terraform by using the .zip files on S3 and Lambda layers that Remotion publishes.
Prerequisites
- Have a Remotion project.
- Have Terraform and set up AWS credentials for it.
- Disable account-level S3 Block Public Access.
AWS China regions are not supported because Remotion cannot publish artifacts there.
1. Build your site
From your Remotion project, build a Remotion Bundle.
This guide assumes the site will live in the sites/remotion-project subfolder of your S3 bucket.
- CLI
- Node.js
Build the sitebunx remotion bundle src/index.ts --out-dir=./sites/remotion-project --public-path=/sites/remotion-project/
Install @remotion/bundler at the same version as your other Remotion packages, then use bundle():
bundle.mjsimportpath from 'node:path'; import {bundle } from '@remotion/bundler'; awaitbundle ({entryPoint :path .resolve ('src/index.ts'),outDir :path .resolve ('sites/remotion-project'),publicPath : '/sites/remotion-project/', });
Run the build scriptnode bundle.mjs
Pass any bundler overrides from your project configuration to bundle() as well.
2. Configure Terraform
Put the following Terraform files in an infra/ directory next to sites/.
Project structureyour-project/ ├─ sites/ │ ├─ remotion-project/ # Generated by the build step ├─ infra/ │ ├─ main.tf │ ├─ variables.tf │ ├─ terraform.tfvars │ ├─ bucket.tf │ ├─ iam.tf │ ├─ function.tf │ ├─ site.tf │ ├─ outputs.tf
Configuration
infra/main.tfterraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } http = { source = "hashicorp/http" version = "~> 3.0" } } } provider "aws" { region = var.region } data "aws_caller_identity" "current" {} data "http" "manifest" { url = "https://remotionlambda-binaries-${var.region}.s3.${var.region}.amazonaws.com/functions/v${var.remotion_version}/manifest.json" } locals { manifest = jsondecode(data.http.manifest.response_body) function_name = "remotion-render-${replace(var.remotion_version, ".", "-")}-mem${var.memory_size_mb}mb-disk${var.disk_size_mb}mb-${var.timeout_seconds}sec" function_arn = "arn:aws:lambda:${var.region}:${data.aws_caller_identity.current.account_id}:function:${local.function_name}" }
Configure your Terraform variables in infra/variables.tf for region, Remotion version, memory size, disk size, function timeout.
infra/variables.tfvariable "region" { type = string default = "eu-central-1" } variable "remotion_version" { type = string default = "4.0.544" } variable "memory_size_mb" { type = number default = 2048 } variable "disk_size_mb" { type = number default = 2048 } variable "timeout_seconds" { type = number default = 120 } variable "site_name" { type = string default = "remotion-project" validation { condition = can(regex("^[A-Za-z0-9_-]+$", var.site_name)) error_message = "Use letters, numbers, hyphens, or underscores for the site name." } }
You can override these defaults in a file called infra/terraform.tfvars.
infra/terraform.tfvarsmemory_size_mb = 3008 disk_size_mb = 2048 timeout_seconds = 240
Bucket
Terraform creates a uniquely named bucket with the Remotion prefix and region.
Your site is publicly readable so the Lambda browser can load your composition - use input props and environment variables to pass private data.
Object ACLs are enabled because Remotion writes private intermediate files and uses public-read ACLs for public render outputs.
Use the privacy setting to control output access.
infra/bucket.tfresource "aws_s3_bucket" "remotion" { bucket_prefix = "remotionlambda-${replace(var.region, "-", "")}-" } resource "aws_s3_bucket_ownership_controls" "remotion" { bucket = aws_s3_bucket.remotion.id rule { object_ownership = "BucketOwnerPreferred" } } resource "aws_s3_bucket_public_access_block" "remotion" { bucket = aws_s3_bucket.remotion.id block_public_acls = false block_public_policy = false ignore_public_acls = false restrict_public_buckets = false } resource "aws_s3_bucket_acl" "remotion" { bucket = aws_s3_bucket.remotion.id acl = "private" depends_on = [ aws_s3_bucket_ownership_controls.remotion, aws_s3_bucket_public_access_block.remotion, ] } resource "aws_s3_bucket_policy" "site" { bucket = aws_s3_bucket.remotion.id policy = jsonencode({ Version = "2012-10-17" Statement = [{ Sid = "PublicSiteFiles" Effect = "Allow" Principal = "*" Action = "s3:GetObject" Resource = "${aws_s3_bucket.remotion.arn}/sites/*" }] }) depends_on = [aws_s3_bucket_public_access_block.remotion] }
IAM permissions
The execution role lets the function access this bucket, invoke itself to distribute rendering, and write logs.
infra/iam.tfresource "aws_iam_role" "remotion" { name_prefix = "remotion-lambda-" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Action = "sts:AssumeRole" Principal = { Service = "lambda.amazonaws.com" } }] }) } resource "aws_iam_role_policy" "execution" { name = "remotion-execution" role = aws_iam_role.remotion.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "ListBuckets" Effect = "Allow" Action = ["s3:ListAllMyBuckets"] Resource = ["*"] }, { Sid = "ReadBucket" Effect = "Allow" Action = ["s3:ListBucket", "s3:GetBucketLocation"] Resource = [aws_s3_bucket.remotion.arn] }, { Sid = "ReadWriteObjects" Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:PutObjectAcl", "s3:AbortMultipartUpload", ] Resource = ["${aws_s3_bucket.remotion.arn}/*"] }, { Sid = "InvokeRenderer" Effect = "Allow" Action = ["lambda:InvokeFunction"] Resource = [local.function_arn] }, { Sid = "WriteLogs" Effect = "Allow" Action = ["logs:CreateLogStream", "logs:PutLogEvents"] Resource = ["${aws_cloudwatch_log_group.remotion.arn}:*"] }, ] }) }
Lambda function
Functions must follow the function naming convention and include tags to match how Remotion deploys functions.
infra/function.tfresource "aws_cloudwatch_log_group" "remotion" { name = "/aws/lambda/${local.function_name}" retention_in_days = 14 } resource "aws_lambda_function" "remotion" { function_name = local.function_name role = aws_iam_role.remotion.arn s3_bucket = local.manifest.code.s3Bucket s3_key = local.manifest.code.s3Key source_code_hash = local.manifest.code.sha256Base64 handler = local.manifest.handler runtime = local.manifest.runtime architectures = [local.manifest.architecture] layers = local.manifest.layers["default"] memory_size = var.memory_size_mb timeout = var.timeout_seconds ephemeral_storage { size = var.disk_size_mb } tags = { "remotion-lambda" = "true" "remotion-version" = var.remotion_version "remotion-memory-in-mb" = tostring(var.memory_size_mb) "remotion-timeout-in-seconds" = tostring(var.timeout_seconds) "remotion-ephemeral-storage-in-mb" = tostring(var.disk_size_mb) } depends_on = [aws_iam_role_policy.execution] } resource "aws_lambda_function_event_invoke_config" "remotion" { function_name = aws_lambda_function.remotion.function_name maximum_retry_attempts = local.manifest.maximumRetryAttempts } resource "aws_lambda_runtime_management_config" "remotion" { function_name = aws_lambda_function.remotion.function_name update_runtime_on = "Manual" runtime_version_arn = local.manifest.runtimeVersionArn }
Site files
Add any extra file extensions your project needs to site_content_types.
infra/site.tflocals { site_bundle_dir = abspath("${path.module}/../sites/${var.site_name}") site_prefix = "sites/${var.site_name}" site_files = fileset(local.site_bundle_dir, "**") site_content_types = { css = "text/css" gif = "image/gif" html = "text/html" jpeg = "image/jpeg" jpg = "image/jpeg" js = "text/javascript" json = "application/json" mjs = "text/javascript" mp3 = "audio/mpeg" mp4 = "video/mp4" otf = "font/otf" png = "image/png" svg = "image/svg+xml" ttf = "font/ttf" wasm = "application/wasm" webm = "video/webm" webp = "image/webp" woff = "font/woff" woff2 = "font/woff2" } } resource "aws_s3_object" "site_asset" { for_each = setsubtract(local.site_files, toset(["index.html"])) bucket = aws_s3_bucket.remotion.id key = "${local.site_prefix}/${each.value}" source = "${local.site_bundle_dir}/${each.value}" source_hash = filemd5("${local.site_bundle_dir}/${each.value}") content_type = try( local.site_content_types[lower(regex("[^.]+$", each.value))], "application/octet-stream" ) depends_on = [aws_s3_bucket_policy.site, aws_s3_bucket_acl.remotion] } resource "aws_s3_object" "site_index" { bucket = aws_s3_bucket.remotion.id key = "${local.site_prefix}/index.html" source = "${local.site_bundle_dir}/index.html" source_hash = filemd5("${local.site_bundle_dir}/index.html") content_type = "text/html" depends_on = [ aws_s3_object.site_asset, aws_s3_bucket_policy.site, aws_s3_bucket_acl.remotion, ] }
Deployment outputs
If your Terraform files are the source of truth for region, function name, bucket name and site name, export them as JSON. We will use it in step 4 again.
infra/outputs.tfoutput "remotion" { value = { region = var.region functionName = aws_lambda_function.remotion.function_name bucketName = aws_s3_bucket.remotion.id serveUrl = "https://${aws_s3_bucket.remotion.bucket_regional_domain_name}/${aws_s3_object.site_index.key}" } }
3. Deploy
Deploy the infrastructure and sitecd infra terraform init terraform plan -out=tfplan terraform apply tfplan terraform output -json remotion > ../remotion-deployment.json
If a function or log group with the same name already exists, import it into Terraform before applying.
Terraform creates a new bucket and execution role for this configuration.
To update your composition, rebuild ./sites/remotion-project and run the plan and apply steps again. Let Terraform exclusively manage this site's S3 objects.
4. Render
From your Remotion project, read the exported settings and call renderMediaOnLambdaAndStream() to stream progress and wait for the render to finish:
render.tsimport {readFileSync } from 'node:fs'; import {renderMediaOnLambdaAndStream , typeAwsRegion , } from '@remotion/lambda/client'; constcontents =readFileSync ('./remotion-deployment.json', 'utf-8'); constdeployment : {region :AwsRegion ;functionName : string;bucketName : string;serveUrl : string; } =JSON .parse (contents ); const {region ,functionName ,bucketName ,serveUrl } =deployment ; constresult = awaitrenderMediaOnLambdaAndStream ({region ,functionName ,serveUrl ,composition : 'HelloWorld',codec : 'h264',forceBucketName :bucketName ,onProgress : ({overallProgress }) => {console .log (`${Math .round (overallProgress * 100)}%`); }, });console .log (result .outputFile );
Replace the composition ID with your own and configure AWS authentication with the required user permissions for the script.
Upgrading
Keep the function, site, and Remotion packages in your application on the same version.
Changing remotion_version, memory, disk space, or timeout changes the function name and makes Terraform replace the function.
Don't use create_before_destroy for zero-downtime upgrades, instead see: Zero-downtime upgrades and apply the same principles.