remotion lambda cicd github-actions aws automation

Remotion Lambda × GitHub Actions — 動画自動生成のCI/CDパイプライン構築

Remotion Lambda × GitHub Actions — 動画自動生成のCI/CDパイプライン構築

Remotion LambdaをCI/CDなしで本番運用するのは、バックアップなしでデータベースを動かすようなものです。コード変更でレンダリングパイプラインが壊れたとき、手作業での復旧は想像以上に時間がかかります。大量の失敗ジョブ、配信遅延、深夜のデバッグ——これを防ぐのがCI/CDパイプラインです。

適切に設計されたパイプラインでは、コードがmainブランチにマージされると自動的にLambda関数とサイトバンドルがデプロイされ、StripeのWebhookやフォーム送信といる外部イベントに応じてレンダリングが開始され、失敗したジョブは自動的にリトライされます。

本記事ではその全体像を構築します。GitHub Actionsを使ったデプロイワークフロー・CI環境でのAWS認証情報の管理・renderMediaOnLambdagetRenderProgress の使い方・Webhook連動レンダリングパターン・framesPerLambda によるコスト最適化・本番ジョブの監視まで、実際のコードとともに解説します。


アーキテクチャの全体像

実装に入る前に、システム全体の構造を把握します。

┌────────────────────────────────────────────────────────────────┐
│  GitHubリポジトリ                                               │
│  ├── src/                  ← Remotionコンポジション            │
│  ├── .github/workflows/    ← CI/CDパイプライン                │
│  └── scripts/              ← デプロイ・レンダースクリプト      │
└────────────────────────────────────────────────────────────────┘
         │ mainへのpush

┌────────────────────────────────────────────────────────────────┐
│  GitHub Actions                                                 │
│  1. deploy.yml    → Lambda関数 + サイトバンドルをデプロイ      │
│  2. render.yml    → スケジュール・手動トリガーでレンダー        │
└────────────────────────────────────────────────────────────────┘
         │ deployFunction() + deploySite()

┌────────────────────────────────────────────────────────────────┐
│  AWS                                                            │
│  ├── Lambda関数           ← Remotionレンダーワーカー           │
│  ├── S3バケット           ← サイトバンドル + 出力ファイル      │
│  └── CloudWatch          ← ログと監視                          │
└────────────────────────────────────────────────────────────────┘
         │ renderMediaOnLambda()

┌────────────────────────────────────────────────────────────────┐
│  外部トリガー                                                   │
│  ├── Stripe Webhook      → 購入完了 → パーソナライズ動画       │
│  ├── Cronスケジュール    → 夜間バッチレンダー                  │
│  └── REST API            → オンデマンドレンダーエンドポイント  │
└────────────────────────────────────────────────────────────────┘

ステップ1:プロジェクト構造

CI/CDに対応しやすい構成を最初から用意します。

remotion-project/
├── src/
│   ├── index.ts                # Remotionルート(コンポジション登録)
│   ├── compositions/
│   │   ├── WelcomeVideo.tsx
│   │   └── ReportVideo.tsx
│   └── components/             # 共通Reactコンポーネント
├── scripts/
│   ├── deploy.ts               # Lambda関数 + サイトバンドルのデプロイ
│   └── render.ts               # プログラマティックレンダースクリプト
├── .github/
│   └── workflows/
│       ├── deploy.yml          # mainへのpush時に実行
│       └── render.yml          # スケジュール・手動ディスパッチ
├── package.json
└── tsconfig.json

ステップ2:GitHub ActionsへのAWS認証情報の設定

AWS認証情報はGitHub Actionsのシークレットとして管理します。リポジトリの Settings → Secrets and variables → Actions → New repository secret から以下を登録します。

シークレット名
AWS_ACCESS_KEY_IDIAMユーザーのアクセスキーID
AWS_SECRET_ACCESS_KEYIAMユーザーのシークレットアクセスキー
AWS_REGION例:ap-northeast-1
REMOTION_FUNCTION_NAMEデプロイ済みLambda関数名
REMOTION_SERVE_URLdeploySite() が返したS3サーブURL

IAMユーザーには npx remotion lambda policies user が生成するポリシーを付与します。認証情報をワークフローファイルに直接書くことは絶対に避けてください。シークレットはログ上でマスクされますが、ファイルにハードコードするとリポジトリごと流出するリスクがあります。

長期間有効なアクセスキーの代わりに、GitHub ActionsのOIDCフェデレーションを使ってIAMロールを一時引き受けする方法もあります。静的な認証情報を管理せずに済むため、セキュリティ上より優れた選択肢です。どちらの方式もRemotion Lambdaのセットアップとは互換性があります。


ステップ3:デプロイワークフロー(コード変更時に自動実行)

src/ 以下のファイルが main ブランチに変更されるたびに、Lambda関数とサイトバンドルを自動デプロイします。

# .github/workflows/deploy.yml
name: Deploy Remotion Lambda

on:
  push:
    branches:
      - main
    paths:
      - "src/**"
      - "package.json"
      - "package-lock.json"

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Node.jsのセットアップ
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: 依存パッケージのインストール
        run: npm ci

      - name: AWS認証情報の設定
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Lambda関数のデプロイ
        run: npx remotion lambda functions deploy --yes

      - name: サイトバンドルのデプロイ
        run: |
          npx remotion lambda sites create src/index.ts \
            --site-name=my-video \
            --yes \
            | tee /tmp/deploy-output.txt

      - name: サーブURLの抽出と保存
        run: |
          SERVE_URL=$(grep -oP 'https://[^\s]+index\.html' /tmp/deploy-output.txt | head -1)
          echo "Serve URL: $SERVE_URL"
          # 本番ではSSM Parameter Storeに書き込む
          # aws ssm put-parameter --name /remotion/serve-url --value "$SERVE_URL" --overwrite

なぜコード変更のたびにLambda関数をデプロイするのか

RemotionのLambda関数名にはRemotionのバージョンが含まれます(例:remotion-render-4-0-272-mem2048mb-disk2048mb-120sec)。Remotionをアップグレードすると関数名が変わり、古い関数名でのレンダーは失敗します。functions deploy は冪等です——同一パラメータの関数がすでに存在する場合は素早く完了するため、毎回実行しても無駄なコストは発生しません。

--yes フラグはインタラクティブな確認プロンプトをスキップします。CI環境では確認待ちでジョブがハングするのを防ぐために必須です。


ステップ4:プログラマティックデプロイスクリプト

CLIではなくNode.js APIを使うと、サーブURLをSSMに書き込んだりSlack通知を送ったりといった処理を柔軟に追加できます。

// scripts/deploy.ts
import { deployFunction, deploySite, getOrCreateBucket } from "@remotion/lambda";
import path from "path";

const REGION = process.env.AWS_REGION ?? "ap-northeast-1";
const SITE_NAME = "my-video";

async function deploy(): Promise<void> {
  console.log("Lambda関数をデプロイ中...");
  const { functionName } = await deployFunction({
    region: REGION,
    timeoutInSeconds: 120,
    memorySizeInMb: 2048,
    createCloudWatchLogGroup: true,
  });
  console.log("デプロイ完了:", functionName);

  console.log("S3バケットを確認・作成中...");
  const { bucketName } = await getOrCreateBucket({ region: REGION });

  console.log("サイトバンドルをデプロイ中...");
  const { serveUrl } = await deploySite({
    bucketName,
    entryPoint: path.resolve("./src/index.ts"),
    region: REGION,
    siteName: SITE_NAME,
  });
  console.log("サイトデプロイ完了:", serveUrl);

  // サーブURLを永続化(例:SSM Parameter Store)
  // await ssmClient.send(new PutParameterCommand({
  //   Name: "/remotion/serve-url",
  //   Value: serveUrl,
  //   Overwrite: true,
  //   Type: "String",
  // }));
}

deploy().catch((err) => {
  console.error(err);
  process.exit(1);
});

ステップ5:renderMediaOnLambda APIの使い方

インフラが整ったら、renderMediaOnLambda を呼んでレンダリングを開始します。この関数は非同期かつノンブロッキングで、即座に renderId を返します。

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

interface RenderParams {
  compositionId: string;
  inputProps: Record<string, unknown>;
  outName?: string;
}

async function triggerRender(params: RenderParams): Promise<string> {
  const { renderId, bucketName } = await renderMediaOnLambda({
    region: process.env.AWS_REGION!,
    functionName: process.env.REMOTION_FUNCTION_NAME!,
    serveUrl: process.env.REMOTION_SERVE_URL!,
    compositionId: params.compositionId,
    inputProps: params.inputProps,
    codec: "h264",
    imageFormat: "jpeg",
    maxRetries: 1,
    privacy: "public",
    outName: params.outName ?? `render-${Date.now()}.mp4`,
    framesPerLambda: 20, // 後述のコスト最適化セクション参照
  });

  console.log(`レンダー開始 — renderId: ${renderId}, bucket: ${bucketName}`);
  return renderId;
}

主要なパラメータを整理します。

パラメータ説明
regionstringLambda関数と同じAWSリージョン
functionNamestringdeployFunction() が返した関数名(完全一致)
serveUrlstringdeploySite() が返したS3サーブURL
compositionIdstringルートファイルに登録したコンポジションの id
inputPropsobjectコンポジションに渡すシリアライズ可能なデータ
codecstring"h264" / "h265" / "vp8" / "vp9" / "gif" / "prores"
imageFormatstring"jpeg"(高速)または "png"(透過サポート)
maxRetriesnumberチャンクが失敗した際のリトライ回数
privacystring"public"(公開URL)または "private"(S3キーのみ)
framesPerLambdanumber並列度の制御(後述)

ステップ6:getRenderProgressで進捗を確認する

renderMediaOnLambda はレンダーを開始するだけです。完了を確認するには getRenderProgress をポーリングします。

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

async function waitForRender(
  renderId: string,
  bucketName: string,
  functionName: string,
  region: string,
  pollIntervalMs = 2000
): Promise<string> {
  while (true) {
    const progress = await getRenderProgress({
      renderId,
      bucketName,
      functionName,
      region,
    });

    if (progress.done) {
      console.log("レンダー完了:", progress.outputFile);
      console.log("ファイルサイズ:", progress.outputSizeInBytes, "bytes");
      return progress.outputFile!;
    }

    if (progress.fatalErrorEncountered) {
      const errors = progress.errors.map((e) => e.message).join("; ");
      throw new Error(`レンダー失敗: ${errors}`);
    }

    const pct = Math.round(progress.overallProgress * 100);
    console.log(`進捗: ${pct}%`);

    await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
  }
}

getRenderProgress のレスポンスに含まれる主なフィールドです。

フィールド説明
donebooleantrue でレンダー完了
overallProgressnumber0〜1の全体進捗
fatalErrorEncounteredbooleantrue でレンダー失敗
errorsarray失敗時のエラーオブジェクト
outputFilestring | null完了時の出力ファイルURL
outputSizeInBytesnumber | null完了時のファイルサイズ
renderedFramesnumberレンダー済みフレーム数
encodedFramesnumberエンコード済みフレーム数
costsobjectこのレンダーの推定AWSコスト内訳

ステップ7:Webhookトリガーのレンダリング — Stripe購入の例

本番でもっとも一般的なパターンは、ビジネスイベントに応じてパーソナライズ動画を生成することです。Stripe購入完了をトリガーに、顧客向けのウェルカム動画を自動生成してメールで届ける例を示します。

import express from "express";
import Stripe from "stripe";
import { renderMediaOnLambda, getRenderProgress } from "@remotion/lambda";

const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// Stripeの署名検証にはrawボディが必要
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
  const sig = req.headers["stripe-signature"] as string;
  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error("Webhook署名検証失敗:", err);
    return res.status(400).send("Webhook Error");
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session;

    const customerName = session.customer_details?.name ?? "お客様";
    const customerEmail = session.customer_details?.email ?? "";
    const productName = session.metadata?.product_name ?? "ご購入商品";

    // レンダーは非同期で実行 — Webhookレスポンスをブロックしない
    renderPersonalizedVideo({
      customerName,
      customerEmail,
      productName,
      sessionId: session.id,
    }).catch(console.error);
  }

  // Stripeは数秒以内に200レスポンスを期待する
  res.json({ received: true });
});

async function renderPersonalizedVideo(data: {
  customerName: string;
  customerEmail: string;
  productName: string;
  sessionId: string;
}): Promise<void> {
  const { renderId, bucketName } = await renderMediaOnLambda({
    region: process.env.AWS_REGION!,
    functionName: process.env.REMOTION_FUNCTION_NAME!,
    serveUrl: process.env.REMOTION_SERVE_URL!,
    compositionId: "WelcomeVideo",
    inputProps: {
      customerName: data.customerName,
      productName: data.productName,
      accentColor: "#10b981",
    },
    codec: "h264",
    imageFormat: "jpeg",
    maxRetries: 2,
    privacy: "public",
    outName: `welcome-${data.sessionId}.mp4`,
    framesPerLambda: 20,
  });

  // レンダー完了まで待機
  const outputUrl = await waitForRender(
    renderId,
    bucketName,
    process.env.REMOTION_FUNCTION_NAME!,
    process.env.AWS_REGION!
  );

  // 動画URLをメールで送信
  await sendWelcomeEmail({
    to: data.customerEmail,
    videoUrl: outputUrl,
    customerName: data.customerName,
  });

  console.log(`ウェルカム動画を送信: ${data.customerEmail} → ${outputUrl}`);
}

Webhookレスポンスは必ずレンダー開始前に返してください。Stripeはレスポンスを数秒以内に期待します。レンダーはその後非同期に進行します。


ステップ8:GitHub Actionsのレンダーワークフロー

スケジュールバッチや手動ディスパッチでレンダーをトリガーするワークフロー例です。

# .github/workflows/render.yml
name: 動画レンダー(スケジュール・手動)

on:
  workflow_dispatch:
    inputs:
      composition_id:
        description: "コンポジションID"
        required: true
        default: "WelcomeVideo"
      customer_name:
        description: "顧客名"
        required: false

  schedule:
    # 毎朝3時(JST)= UTC 18:00前日
    - cron: "0 18 * * *"

jobs:
  render:
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Node.jsのセットアップ
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: 依存パッケージのインストール
        run: npm ci

      - name: AWS認証情報の設定
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: 動画レンダー実行
        env:
          REMOTION_FUNCTION_NAME: ${{ secrets.REMOTION_FUNCTION_NAME }}
          REMOTION_SERVE_URL: ${{ secrets.REMOTION_SERVE_URL }}
          AWS_REGION: ${{ secrets.AWS_REGION }}
          COMPOSITION_ID: ${{ github.event.inputs.composition_id || 'NightlyReport' }}
        run: npx ts-node scripts/render.ts

ステップ9:framesPerLambdaによるコスト最適化

framesPerLambda はレンダー速度とコストのバランスを決める最重要パラメータです。1つのLambda関数が担当するフレーム数を指定します。

framesPerLambdaを下げる → より多くのLambda関数が並列起動 → ウォールクロック時間が短くなる
framesPerLambdaを上げる → Lambda起動回数が減る → Lambdaのオーバーヘッドが下がる

300フレーム(30fpsで10秒)の動画での比較例:

framesPerLambdaLambda起動数並列度
1030最大
2015標準
506最小

Remotionはデフォルトでコンポジション長に基づいて自動計算します。最小値は4フレームです。

実運用での使い分け:

ユーザーが結果を待っている(購入後の動画など)ならLambdaが増えても速度優先で低い値(10〜20)を選びます。夜間バッチで大量生成する場合はLambda起動回数を減らして管理しやすくするために高め(40〜80)にします。

レンダー前にコストを見積もる estimatePrice() も活用できます。

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

const estimate = estimatePrice({
  region: "ap-northeast-1",
  memorySizeInMb: 2048,
  durationInMilliseconds: 3000,
  concurrency: 15,
  renderingTimeInMilliseconds: 2000,
});

console.log("推定コスト:", estimate.priceInDollars.toFixed(6), "USD");

ステップ10:本番レンダージョブの監視

CloudWatchログ

deployFunction()createCloudWatchLogGroup: true を指定すると、各Lambda呼び出しがCloudWatchにログを書き込みます。失敗したレンダーのトレース・チャンクごとの処理時間プロファイリング・エラーレートへのアラートに活用します。

データベースでのジョブ管理

大量のレンダーが並行して走る本番環境では、レンダージョブのテーブルを持つことを推奨します。

// レンダー開始時
await db.renderJobs.create({
  id: renderId,
  compositionId,
  inputProps: JSON.stringify(inputProps),
  status: "pending",
  startedAt: new Date(),
  bucketName,
});

// ポーリング中
await db.renderJobs.update(renderId, {
  status: "rendering",
  progress: Math.round(progress.overallProgress * 100),
});

// 完了時
await db.renderJobs.update(renderId, {
  status: "complete",
  outputUrl,
  completedAt: new Date(),
  costEstimate: progress.costs?.estimatedCost,
});

失敗時のアラート

progress.fatalErrorEncounteredtrue になったときにSlackやPagerDutyへ通知を送ります。主な原因は次の3つです。

  • アセットURLのタイムアウト:CDNまたは事前キャッシュされたURLを使う
  • Lambdaのメモリ不足:複雑なコンポジションでは memorySizeInMb を増やす
  • バージョン不整合:デプロイ済みLambda関数とCI環境の @remotion/lambda のバージョンが違う
if (progress.fatalErrorEncountered) {
  await sendSlackAlert({
    channel: "#render-alerts",
    message: `レンダー失敗: ${renderId}\nエラー: ${progress.errors.map(e => e.message).join(", ")}`,
  });
  throw new Error("レンダー失敗");
}

FAQ

Q: サーブURLはどこに保存するのが最善ですか? AWS SSM Parameter Storeへの保存を推奨します。デプロイワークフローで aws ssm put-parameter を使って書き込み、レンダーワークフローで aws ssm get-parameter で読み込みます。こうすると2つのワークフローを疎結合に保ちながら、常に最新のURLを参照できます。

Q: Lambdaチャンクが失敗した場合はどうなりますか? maxRetries の設定に従ってリトライされます。すべてのリトライが失敗すると progress.fatalErrorEncounteredtrue になり、progress.errors にエラー内容が格納されます。チャンクによってはノンファタルなエラーが起きても残りのチャンクが処理を続ける場合もあります。

Q: デプロイとレンダーを1つのジョブにまとめても問題ありませんか? 動作はしますが分けることを推奨します。デプロイはコード変更時に実行され、レンダーはビジネスイベント時に実行されます。1つにまとめると「コードをpushするたびにレンダーが走る」という意図しない挙動につながります。

Q: AWS Lambdaのコンカレンシー制限に引っかかることはありますか? AWSアカウントのリージョンごとのデフォルトLambdaコンカレンシー上限は1000実行です。framesPerLambda=10で1000フレームの動画をレンダーすると100並列が発生します。同時に複数のレンダーを走らせる場合はコンカレンシー上限をAWSサポートに引き上げてもらうことを検討してください。

Q: このパイプラインをRemotionのCloud Runに移植できますか? できます。@remotion/cloudrun パッケージが renderMediaOnCloudRun()getRenderProgress() を提供しており、APIの形は類似しています。Lambda固有のセットアップ(IAMポリシー・関数デプロイ)をCloud Run対応のものに置き換えるだけで、GitHub Actionsのワークフロー構造はそのまま使えます。

Q: Lambdaで生成できる動画の最大の長さは? Lambda関数の最大実行タイムアウトは15分です。Remotionはチャンク分割でこれを回避するため、動画の長さ自体に理論上の上限はありません。60分の動画はチャンク数が増えるだけです。ただし動画が長くなるほどコストとS3ストレージが増加します。個々のLambda呼び出しを120秒以内に収めるよう framesPerLambda を調整することをRemotionチームは推奨しています。

Q: inputPropsに顧客の個人情報を入れても安全ですか? inputProps はレンダー実行中にLambdaインフラによって一時的にS3に保存されます。個人情報や秘匿情報を直接入れることは推奨しません。代わりに顧客IDや短期有効の署名付きURLを渡し、コンポジション内で必要な場合のみ解決するパターンを採用してください。名前や商品名程度のパーソナライズデータであれば通常は問題ありません。


RenderCompのテンプレートでパイプラインを高速化

本記事で解説したインフラとパイプラインのパターンは、どのRemotionコンポジションにも適用できます。ただし、コンポジションの品質——Reactコンポーネントの描画効率・propインターフェースの明確さ・アニメーションのフレーム効率——はLambdaの実行時間とコストに直結します。

RenderComp はLambdaパイプラインに最適化されたRemotionテンプレートライブラリです。軽量なバンドルサイズ・クリーンなpropsインターフェース・JPEG描画フレンドリーなデザインで構成されており、本記事のワークフローに直接組み込んで使えます。各テンプレートはTypeScriptの型定義付きで提供されています。

テンプレート一覧は rendercomp.com でご確認ください。

すぐに使える

700以上のRemotionテンプレートを一括入手

買い切り永続ライセンス。TypeScript製。今日から動画制作スピードが変わります。

RenderCompを試す →