Deploying Remotion Using the AWS CDK

Remotion is an awesome tool that enables you to creat videos using React. It basically renders a bunch of frames using React components that you write and stitches those frames together into a video using FFmpeg. The rendering can be done locally or on a server, and they even provide integration with AWS Lambda to render videos very efficiency in the cloud.

Unfortunately, Remotion assumes that you set up the rendering in AWS using your local CLI. I deploy all the cloud infrastructure in my project declaratively using the AWS Cloud Development Kit (CDK), so I was looking for a way to incorporate Remotion into my CDK setup. It's a bit hacky, as this way is not officially supported/recommended by Remotion (yet?), but I made it work and thought I'd share the solution here.

There are a few different parts that need to play together here:

  • S3 bucket, where the rendered videos will be stored
  • Render function set up in AWS Lambda, which is responsible for rendering the videos
  • Remotion site, which is basically a hosted bundle of a Remotion project
  • API endpoint that kicks off the rendering of a video

S3 Bucket

const uid: string = cdk.Names.uniqueId(this); const bucketName = `remotionlambda-${region.replace(/-/g, "")}-${uid.toLowerCase()}`; new cdk.aws_s3.Bucket(this, "Bucket", { bucketName, accessControl: cdk.aws_s3.BucketAccessControl.PUBLIC_READ, });

Render Function

import { AwsRegion, getUserPolicy } from "@remotion/lambda"; import { hostedLayers } from "@remotion/lambda/dist/shared/hosted-layers.js"; // The role used by Remotion const role = new cdk.aws_iam.Role(this, "RemotionExecutionRole", { roleName: "RemotionExecutionRole", assumedBy: new cdk.aws_iam.ServicePrincipal("lambda.amazonaws.com"), managedPolicies: [ cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName( "service-role/AWSLambdaBasicExecutionRole" ), ], }); // The policy for the role (Policy content provided by @remotion/lambda) const userPolicy = getUserPolicy(); const document = cdk.aws_iam.PolicyDocument.fromJson(JSON.parse(userPolicy)); const policy = new cdk.aws_iam.Policy(this, "RemotionExecutionPolicy", { policyName: "remotion-executionrole-policy", document, }); policy.attachToRole(role); // Remotion Render Function const assetPath = __dirname.concat( "../node_modules/@remotion/lambda/remotionlambda-arm64.zip" ); const layers = hostedLayers[region].map(({ layerArn, version }) => cdk.aws_lambda.LayerVersion.fromLayerVersionArn( this, `${layerArn}-${version}`, `${layerArn}:${version}` ) ); const remotionRenderFunction = new cdk.aws_lambda.Function( this, "RemotionRenderFunction", { functionName: "remotion-render-feld", code: cdk.aws_lambda.Code.fromAsset(assetPath), runtime: cdk.aws_lambda.Runtime.NODEJS_14_X, handler: "index.handler", timeout: cdk.Duration.seconds(120), memorySize: 2048, architecture: cdk.aws_lambda.Architecture.ARM_64, ephemeralStorageSize: cdk.Size.mebibytes(2048), role, layers, runtimeManagementMode: cdk.aws_lambda.RuntimeManagementMode.manual( `arn:aws:lambda:${region}::runtime:69000d3430a08938bcab71617dffcb8ea551a2cbc36c59f38c52a1ea087e461b` ), } );

The Remotion site is basically an AWS S3 Bucket Deployment of the folder containing your Remotion project:

const destinationBucket = new cdk.aws_s3.Bucket(this, "Bucket", { publicReadAccess: false, blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, removalPolicy: cdk.RemovalPolicy.DESTROY, autoDeleteObjects: true, accessControl: cdk.aws_s3.BucketAccessControl.PRIVATE, objectOwnership: cdk.aws_s3.ObjectOwnership.BUCKET_OWNER_ENFORCED, encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED, }); const origin = new cdk.aws_cloudfront_origins.S3Origin(destinationBucket); const cachePolicy = new cdk.aws_cloudfront.CachePolicy(this, "CachePolicy", { queryStringBehavior: cdk.aws_cloudfront.CacheQueryStringBehavior.none(), headerBehavior: cdk.aws_cloudfront.CacheHeaderBehavior.none(), cookieBehavior: cdk.aws_cloudfront.CacheCookieBehavior.none(), defaultTtl: cdk.Duration.hours(1), maxTtl: cdk.Duration.hours(1), minTtl: cdk.Duration.hours(1), enableAcceptEncodingBrotli: true, enableAcceptEncodingGzip: true, }); const defaultBehavior: cdk.aws_cloudfront.BehaviorOptions = { viewerProtocolPolicy: cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, origin, allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS, cachedMethods: cdk.aws_cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS, compress: true, cachePolicy, }; const distribution = new cdk.aws_cloudfront.Distribution(this, "Distribution", { defaultBehavior, certificate, defaultRootObject: "index.html", }); new cdk.aws_s3_deployment.BucketDeployment(this, "Deployment", { sources: [cdk.aws_s3_deployment.Source.asset(remotionPath)], destinationBucket, distribution, memoryLimit: 2048, ephemeralStorageSize: cdk.Size.gibibytes(2), });