R RenderComp
remotion aws-lambda ec2 rendering cost-optimization

RemotionのLambda vs EC2:フレーム分散とコスト設計

執筆: RenderComp チーム 編集方針

Remotion は「動画をコードで書く」フレームワークですが、その本質は「フレーム番号を入力として受け取り、React ツリーを決定論的に評価する純粋関数」です。同じフレーム番号を渡せば、どのマシン・どのプロセスで実行しても同一の画像が生成されます。フレーム 0〜299 があるなら、0〜149 と 150〜299 を別々のプロセスに投げても最終結果は変わりません。これが Remotion の並列化戦略の根拠です。

この決定論的並列性が、Lambda と EC2 のどちらを選ぶかという問いの出発点になります。Lambda は「1 リクエスト = 1 コンテナ」のモデルで数百のワーカーを秒単位で起動でき、EC2 は「1 インスタンス = N コア」のモデルで長時間稼働のスループット単価を下げます。どちらが有利かは「バースト性」「コンポジションあたりのフレーム計算コスト」「1 動画あたりのフレーム数」の三変数で決まります。

この記事では @remotion/lambda@remotion/renderer の実 API を通じて両アーキテクチャの内部動作を解説し、どのパラメータがコストと速度のバランスを左右するかを具体的に示します。


Remotion のフレーム並列化モデル

Lambda:チャンク分散アーキテクチャ

renderMediaOnLambda() を呼び出すと、Remotion は最初に「ランチャー」Lambda が起動し、フレームを framesPerLambda ずつのチャンクに分割して複数の「レンダラー」Lambda を並行起動します。各レンダラーは担当フレームを PNG として S3 に書き出し、最後に「スティッチャー」Lambda が ffmpeg で動画に結合します。

この三層構造を踏まえて、renderMediaOnLambda() の主要パラメータを確認します。

import { renderMediaOnLambda, getRenderProgress } from "@remotion/lambda/client";

const { renderId, bucketName } = await renderMediaOnLambda({
  region: "ap-northeast-1",
  functionName: "remotion-render-4-0-272-mem3008mb-disk2048mb-240sec",
  composition: "KV-Slide",
  serveUrl: process.env.REMOTION_SERVE_URL!,
  codec: "h264",
  inputProps: { title: "Q3 Results" },

  // 1 Lambda あたり何フレームを担当するか。
  // 300 フレームで 20 を指定すると 15 個の Lambda が並行起動する。
  framesPerLambda: 20,

  // 関数名の末尾 "240sec" に対応させる。
  timeoutInMilliseconds: 240_000,

  // Lambda 環境では GPU が使えないため swangle(ソフトウェア OpenGL)が必須。
  chromiumOptions: {
    gl: "swangle",
    disableWebSecurity: false,
  },

  // チャンク単位のリトライ回数。ネットワーク起因のフレーム失敗に有効。
  maxRetries: 1,
});

300 フレームのコンポジションで framesPerLambda: 20 を指定した場合、レンダラー Lambda は 300 / 20 = 15 個が並行起動します。フレームの計算が重い場合でも、全体の所要時間は「最も遅いチャンクの実行時間 + スティッチ時間」に収束します。

EC2 / ローカル:コア並列モデル

EC2 や手元のマシンで renderMedia() を使う場合は、フレームを同一プロセス内の複数スレッドで並列処理します。

import { renderMedia, selectComposition } from "@remotion/renderer";
import path from "path";

const composition = await selectComposition({
  serveUrl: "http://localhost:3000",
  id: "KV-Slide",
  inputProps: { title: "Q3 Results" },
});

await renderMedia({
  composition,
  serveUrl: "http://localhost:3000",
  codec: "h264",
  outputLocation: path.join(process.cwd(), "out", "kv-slide.mp4"),

  // 同時にレンダリングするフレーム数(≒ 使用コア数)。
  // 省略するとマシンのコア数を自動検出して設定される。
  concurrency: 8,

  // フレーム 1 枚あたりのタイムアウト(ミリ秒)。
  // 複雑な WebGL シーンで無制限に待ち続けないための安全弁。
  timeoutInMilliseconds: 30_000,

  onProgress: ({ progress, renderedFrames, encodedFrames }) => {
    process.stdout.write(
      `\r${(progress * 100).toFixed(1)}%  render:${renderedFrames}  encode:${encodedFrames}`
    );
  },
});

concurrency: 8 は「同時に 8 フレームを Chromium ページで並列評価する」という意味です。Remotion のレンダリングは CPU バウンドなので、ハイパースレッディングによる論理コアの増加よりも物理コア数のほうが効果的な指標になります。


Lambda のメモリと vCPU の関係

Lambda の料金は GB-second(メモリ × 実行時間)で課金されます。見落としやすい重要な仕様として、Lambda はメモリを増やすと vCPU 数も線形に増加します。AWS の公式ドキュメントでは 1,769 MB が 1 vCPU の基準点とされており、メモリを 2 倍にするとおおよそ vCPU も 2 倍になります。

メモリ割り当て割り当て vCPU(近似)
128 MB0.07 vCPU
1,769 MB1 vCPU(基準点)
3,008 MB約 1.7 vCPU
3,538 MB2 vCPU
5,307 MB3 vCPU
10,240 MB約 5.8 vCPU

Remotion のデフォルト推奨値 3,008 MB は約 1.7 vCPU に相当します。Chromium は内部でマルチスレッドを活用するため、1,769 MB から 3,008 MB に増やすだけでも同一チャンクのレンダリング時間が短縮され、結果として GB-second の実質消費が下がるケースがあります。特に 3D WebGL アニメーションや多数のテキストレイヤーを含む CPU 負荷の高いコンポジションで効果が顕著です。

deployFunction() でメモリサイズを明示的に指定するには次のように記述します。

import { deployFunction } from "@remotion/lambda";

const { functionName } = await deployFunction({
  region: "ap-northeast-1",
  timeoutInSeconds: 240,
  // 2 vCPU を得るための最小ライン(1769 × 2 = 3538 MB)
  memorySizeInMb: 3538,
  diskSizeInMb: 2048,
  createCloudWatchLogGroup: true,
});

console.log("Deployed:", functionName);
// → "remotion-render-4-0-272-mem3538mb-disk2048mb-240sec"

3,008 MB と 3,538 MB のどちらが総コストで安いかは、コンポジションの CPU 飽和度に依存します。まず 3,008 MB でレンダリングして CloudWatch の Lambda Duration メトリクスを確認し、実行時間が長い場合に 3,538 MB へ変更して GB-second の増減を比較するのが実践的なアプローチです。


framesPerLambda とタイムアウトの設計

framesPerLambda の値はレンダリングコストと安定性に直接影響します。値を小さくするほど並列度が上がりますが、Lambda の起動オーバーヘッドと S3 PUT リクエスト数が増えます。値を大きくするほど起動コストは減りますが、1 Lambda の実行時間が延びてタイムアウトリスクが上がります。

タイムアウトリスクを事前に見積もる近似式を示します。

必要実行時間(秒)≈ framesPerLambda × (1フレームの平均処理時間秒 / 割り当てvCPU数)

1 フレームあたり 3 秒かかる重いコンポジション、framesPerLambda: 60、メモリ 3,538 MB(約 2 vCPU)の場合を例にとると次のようになります。

60 × (3 / 2) = 90 秒

タイムアウト 240 秒には余裕がありますが、アニメーションの後半フレームが重くなる設計、たとえばモーションブラーの蓄積や大きなデータセットのレンダリングが含まれる場合は、最悪ケースで見積もる必要があります。フレームごとの処理時間にばらつきがある場合は framesPerLambda を小さめに設定して 1 チャンクの最大実行時間を短く抑えるほうが安全です。

レンダリング進捗を polling して実行状況とエラーを確認する実装例です。

import { getRenderProgress } from "@remotion/lambda/client";

const pollProgress = async (renderId: string, bucketName: string) => {
  while (true) {
    const progress = await getRenderProgress({
      renderId,
      bucketName,
      functionName: "remotion-render-4-0-272-mem3538mb-disk2048mb-240sec",
      region: "ap-northeast-1",
    });

    if (progress.done) {
      console.log("完了:", progress.outputFile);
      break;
    }

    if (progress.fatalErrorEncountered) {
      // errors 配列にはチャンクごとのスタックトレースが含まれる
      console.error("致命的エラー:", progress.errors);
      break;
    }

    // lambdasInvoked はリトライ含む総起動数。chunks は完了チャンク数。
    // 差が大きい場合はタイムアウトやメモリ不足によるリトライが多発している。
    const retryCount = progress.lambdasInvoked - progress.chunks;
    console.log(
      `${(progress.overallProgress * 100).toFixed(1)}%`,
      `| 完了チャンク: ${progress.chunks}`,
      `| リトライ発生: ${retryCount}`
    );

    await new Promise((r) => setTimeout(r, 1_000));
  }
};

EC2 での長時間・大量レンダリング

Lambda のハードタイムアウト(最大 900 秒)は、フレーム数の多い長尺コンポジションや 4K 以上の高解像度レンダリングで制約になります。2 時間の長編動画を 30fps でレンダリングすると 216,000 フレームになります。framesPerLambda: 20 なら 10,800 個の Lambda 関数が起動し、AWS アカウントのデフォルト同時実行数上限(1,000)の 10 倍以上に達します。上限引き上げ申請は可能ですが、S3 への中間フレーム書き込みコストも比例して増大します。

こうした用途では EC2 を選ぶほうが理にかなっています。c6i.8xlarge(32 vCPU)を使う場合、OS 用にコアを残して concurrency: 28〜30 を設定することで、単一インスタンスから高いスループットを引き出せます。

import { renderMedia, selectComposition } from "@remotion/renderer";
import path from "path";

// 複数コンポジションをバッチ処理する例
const jobs = [
  { id: "EP01", inputProps: { episode: 1 } },
  { id: "EP02", inputProps: { episode: 2 } },
  { id: "EP03", inputProps: { episode: 3 } },
];

for (const job of jobs) {
  const composition = await selectComposition({
    serveUrl: "http://localhost:3000",
    id: job.id,
    inputProps: job.inputProps,
  });

  const outputPath = path.join("/mnt/render-output", `${job.id}.mp4`);

  await renderMedia({
    composition,
    serveUrl: "http://localhost:3000",
    codec: "h264",
    outputLocation: outputPath,
    // 32 vCPU インスタンスで 30 スレッド(OS プロセス用に 2 コアを残す)
    concurrency: 30,
    // 中間フレームの JPEG 品質(最終 H.264 エンコードの品質には別途 crf で設定する)
    jpegQuality: 80,
    // フレーム 1 枚あたりのタイムアウト
    timeoutInMilliseconds: 60_000,
    onProgress: ({ progress }) => {
      process.stdout.write(`\r${job.id}: ${(progress * 100).toFixed(0)}%`);
    },
  });

  console.log(`\n${job.id} 完了 →`, outputPath);
}

EC2 Spot との相性

Remotion のレンダリングは本質的に決定論的なため、Spot Instance の中断に比較的強い設計が取りやすいです。ただし renderMedia() 自体にはチェックポイント・再開機能がないため、中断時は最初から再実行する前提でジョブ設計をするか、完了済みフレームを別途 S3 に書き出す独自ラッパーを実装する必要があります。キューに積んだジョブが冪等(同じ入力 → 同じ出力)になるよう設計しておくことが Spot 活用の前提条件です。


サイトデプロイと serveUrl の管理

Lambda でも EC2 でも、serveUrl に渡す Remotion バンドルは deploySite() で S3 にアップロードしておきます。

import { deploySite, getOrCreateBucket } from "@remotion/lambda";

const { bucketName } = await getOrCreateBucket({ region: "ap-northeast-1" });

const { serveUrl, siteName } = await deploySite({
  bucketName,
  entryPoint: "./src/index.ts",
  region: "ap-northeast-1",
  // 同名で再デプロイすると変更ファイルのみ差分更新される
  siteName: "kv-slide-v2",
  options: {
    onBundleProgress: (p) => console.log(`Bundle: ${p}%`),
    onUploadProgress: ({ totalFiles, filesUploaded }) =>
      console.log(`Upload: ${filesUploaded}/${totalFiles}`),
  },
});

console.log("serveUrl:", serveUrl);
// → https://remotionlambda-xxx.s3.ap-northeast-1.amazonaws.com/sites/kv-slide-v2/index.html
console.log("siteName:", siteName);
// → kv-slide-v2

この serveUrl は Lambda 版の renderMediaOnLambda() にも EC2 版の renderMedia() にも同じ値を渡せます。EC2 の場合はインスタンス上で npx remotion preview を起動したローカル URL でも動作します。コンポジションのコードとインフラを分離し、serveUrl だけを切り替えることでステージング環境と本番環境を簡単に分けられるのは、このアーキテクチャの大きな利点です。

コンポジション一覧は同じ URL から取得します。

import { getCompositions } from "@remotion/renderer";

const compositions = await getCompositions(serveUrl, {
  inputProps: {},
  timeoutInMilliseconds: 15_000,
});

compositions.forEach((c) => {
  console.log(`${c.id}: ${c.durationInFrames} frames @ ${c.fps}fps`);
  // → KV-Slide: 300 frames @ 30fps
});

Lambda と EC2 の選択フレームワーク

両者の選択基準を整理します。

Lambda が適するケース:

  • バースト性の高いオンデマンド処理に向いています。ユーザーが「動画生成」ボタンを押した瞬間に秒単位でスケールアウトし、完了後はゼロにスケールインする用途で、アイドルコストがゼロになるのは Lambda 最大の優位性です。
  • 30 秒・30fps = 900 フレーム程度で、フレームあたりの計算が比較的軽い短尺・軽量コンポジションにも適しています。framesPerLambda: 30 を指定すれば 30 個の Lambda が並行起動し、実質的な待ち時間を短縮できます。
  • インスタンスの起動・監視・パッチ適用といったインフラ管理の手間を省きたい場合にも Lambda が有効です。これらの作業をすべて AWS に委任できます。

EC2 が適するケース:

  • 4K・60fps で 10 分を超える動画は Lambda の 15 分制限に引っかかるリスクが高く、長尺・高解像度レンダリングには EC2 が適しています。1 フレームの平均処理時間が 4 秒を超える場合は framesPerLambda を極端に小さく設定しない限り Lambda の選択肢は狭まります。
  • 1 日に数百本の動画を常時レンダリングするような高頻度・継続的なバッチ処理では、EC2 の時間課金モデルのほうが合理的になります。c6i ファミリーの CPU 最適化インスタンスを 24 時間稼働させ、キューからジョブを取り続けるモデルが一般的です。
  • Lambda は起動時に React バンドルのロードと Chromium の初期化が発生し、バンドルサイズが大きい場合はコールドスタートが数秒になることがあります。Provisioned Concurrency で緩和できますが追加コストが発生します。コールドスタートを避けたい場合は、サーバーが常時稼働している EC2 を選ぶほうが確実です。

まとめ

Lambda と EC2 の選択は「フレーム数・バースト性・フレームあたりの計算コスト」の三変数で決まります。

  • 1 動画が 2,000 フレーム未満かつリクエスト数が予測しにくい → Lambda
  • 1 動画が 10,000 フレームを超える、または 4K・60fps → EC2
  • 月間レンダリング時間が数千時間を超える規模 → EC2 Spot
  • インフラ管理なしで素早くプロダクションへ持ち込みたい → Lambda

どちらのアーキテクチャでも、Remotion のコンポジションコードは変更不要です。RenderComp カタログのテンプレートはすべてこの互換性を前提に設計されており、serveUrl の切り替えだけで Lambda と EC2 を行き来できます。最初は Lambda でプロトタイプを動かして CloudWatch Logs からチャンクごとの実行時間を計測し、そのデータを元に EC2 への移行コストが正当化できるかを判断するアプローチが最も確実です。決定論的なフレーム計算というアーキテクチャ的な性質を正しく理解していれば、インフラの選択はパラメータの調整に帰着します。

販売中

1,000以上のRemotionテンプレートを一括入手

買い切り(一括払い)・サブスクなし・生涯アップデート無料。TypeScript製。

料金プランを見る →