Vercel LabsのemulateをAWSとVercel APIの観点で試してみた

Vercel LabsのemulateをAWSとVercel APIの観点で試してみた

2026.05.05

こんにちは、豊島です。

Vercel Labsから emulate という、ローカルAPIエミュレーション用のOSSが公開されています。
AWS、GitHub、Google、Slack、Stripe、Resendなど、Webサービス連携で扱うことが多いSaaSやクラウドサービスを、状態を保ったままローカルで再現できるツールです。

本記事ではサポート対象のAWSとVercel APIを手元で動かして気づいた点を整理します。

そもそもemulateとは

複数のSaaSやクラウドAPIを、状態を保ったままローカルで再現するOSSです。公式サイトでは次のように説明されています。

Stateful, production-fidelity replacements for Stripe, GitHub, Google, AWS, and 7 more services. No API keys. No network. Not mocks.
参考: emulate.dev

各サービスがインメモリのストアを持っているため、「Not mocks」と強調されている通り、S3にオブジェクトをPutObjectすると後続のGetObjectで同じデータが返ってきます。GitHubでリポジトリを作ってcommitをpushしてPRを開くといった一連の流れも、状態を保ったまま動きます。

検証環境

今回検証した構成です。

項目 バージョン
emulate 0.5.0
Node.js 24 (LTS)
@aws-sdk/client-s3 3.1042.0
@aws-sdk/client-sqs 3.1042.0
@aws-sdk/client-iam 3.1042.0
@aws-sdk/client-sts 3.1042.0

2026年5月時点ではemulate v0.5.0でClerkが追加されており、サポートサービスは12種類になっています。READMEの記述(11サービス)と差分があるので、pnpm exec emulate listで確認するのが安全です。

クイックスタートとデフォルトポート

起動はシンプルです。

# 全サービスを起動(zero-config)
npx emulate

# 特定サービスだけ起動
npx emulate start --service vercel,aws

# シードコンフィグを使う
npx emulate start --seed config.yaml

pnpm exec emulate startを実行すると、以下のような出力でポートが表示されます。

emulate v0.5.0

  vercel      http://localhost:4000
  github      http://localhost:4001
  google      http://localhost:4002
  slack       http://localhost:4003
  apple       http://localhost:4004
  microsoft   http://localhost:4005
  okta        http://localhost:4006
  aws         http://localhost:4007
  resend      http://localhost:4008
  stripe      http://localhost:4009
  mongoatlas  http://localhost:4010
  clerk       http://localhost:4011

  Tokens
  test_token_admin -> admin

ここで気を付けたいのが、v0.5.0でサービス数が増えた結果、AWSのポートが4006 → 4007、Stripeが4010 → 4009のように、READMEのトップに書かれているポート表とズレている点です。
新しい順序はOkta、AWS、Resend、Stripe、MongoDB Atlas、ClerkのようにApple/Microsoftの後ろに並びます。READMEのデフォルトポート表は古い情報(7サービス時点)で残っているようなので、実機の起動ログを正としたほうが安全です。
この差分についてはIssue #94を立てて報告済みです。

デフォルトのトークン(test_token_admin)はadminユーザーにマップされており、各サービスのAPIはAuthorization: Bearer test_token_adminで叩けます。

どういう観点で使えるのか

ユースケース別に整理します。

観点1: CIでネットワークを切ってテストを通したい

emulateの本来の主用途です。GitHub ActionsなどのCIで、本番APIキーをCIに持たせたくない、外部APIのレート制限や障害にCIを依存させたくない、というケースに刺さります。

  • E2EテストでGitHubにPRを作ってSlackに通知する処理 → --service github,slackで起動して、本物のトークン無しで一連のフローを検証
  • Stripeの決済フローを通すテスト → ローカルemulateのStripeに向けることで、テストキーをCIに置く必要がなくなる
  • AWSのS3にアップロードしてSQSに通知を流すジョブ → --service awsでローカル完結

観点2: Vercel Preview DeploymentのOAuth問題を解決したい

Vercelのpreview deploymentはブランチごとにURLが変わるので、OAuthコールバックURLやWebhook受信先を事前登録するタイプのSaaSと相性が悪い、という構造的な問題があります。
@emulators/adapter-nextを使うとNext.jsアプリに直接エミュレータを埋め込めるので、preview deploymentと同一オリジンで動きます。previewごとに本物のGoogle OAuthクライアントやGitHub OAuth Appを作り直す手間もなくなります。

観点3: オフライン/サンドボックスで開発したい

ネットワーク分離環境で開発を継続したいケースです。AWSのS3/SQS、Gmail/Calendar/Drive、StripeのWebhookハンドラなど、いずれもローカル完結で書けます。

観点4: テスト時の状態リセットを綺麗にやりたい

createEmulator APIをコードから呼び出して起動・停止・リセットできるので、VitestやJestのテストフックに組み込めます。後述の「動作検証3: reset()の挙動」で、実際にデータが消えるところまで確認しています。

動作検証1: AWS S3をAWS SDKから叩いてみる

emulateのS3エンドポイントは、実際のS3と同じHTTPメソッドとURLパスの組み合わせ(PUT /:bucketでバケット作成、PUT /:bucket/:keyでオブジェクト配置、レスポンスはXML、など)で応答するので、AWS SDKはforcePathStyle: trueを指定すれば動きます。
IAMキーペアも初期値として用意されているので、別途キーを発行する必要もありません。

手順1: 検証用プロジェクトを用意する

適当な作業ディレクトリでpnpmを初期化し、emulate本体とAWS SDKのS3クライアントを入れます。

mkdir emulate-test && cd emulate-test
pnpm init
pnpm add emulate @aws-sdk/client-s3

手順2: emulateを起動する

別ターミナルで全サービスを起動します。

pnpm exec emulate start

起動ログでAWSが何番ポートに割り当てられたかを確認します。前述の通りv0.5.0ではAWSはhttp://localhost:4007です。このターミナルは検証中つけっぱなしにします。

手順3: 検証スクリプトを書く

プロジェクト直下にtest-s3.mjsを作成して以下を貼り付けます。バケット作成 → オブジェクト配置 → 一覧取得 → 取得・削除までを通しで叩く構成です。

test-s3.mjs
import {
  S3Client,
  CreateBucketCommand,
  PutObjectCommand,
  GetObjectCommand,
  ListBucketsCommand,
  ListObjectsV2Command,
  DeleteObjectCommand,
} from "@aws-sdk/client-s3";

const s3 = new S3Client({
  endpoint: "http://localhost:4007",
  region: "us-east-1",
  credentials: {
    // emulateのデフォルトキー(AWSの公開サンプル値そのままで、念のため伏せています)
    accessKeyId: "AKIA...EXAMPLE",
    secretAccessKey: "...EXAMPLEKEY",
  },
  forcePathStyle: true,
});

await s3.send(new CreateBucketCommand({ Bucket: "my-bucket" }));
await s3.send(new PutObjectCommand({
  Bucket: "my-bucket",
  Key: "hello.txt",
  Body: "Hello from emulate",
  ContentType: "text/plain",
}));

const list = await s3.send(new ListBucketsCommand({}));
console.log("buckets:", list.Buckets?.map((b) => b.Name));

const got = await s3.send(new GetObjectCommand({
  Bucket: "my-bucket",
  Key: "hello.txt",
}));
console.log("body:", await got.Body?.transformToString());

ポイントはS3Clientのコンストラクタでendpointをローカルのemulateに向け、forcePathStyle: trueを付ける2点です。本番のAWS向けコードからの差分はここだけで、環境変数で切り替える運用にすれば本番コードを汚さずに済みます。

クレデンシャル文字列はマスクして書いていますが、emulateがデフォルトで用意しているのはAWS CLIユーザーガイドで例示用に使われている公開サンプル値と同じものです。
実値はemulateのREADME、またはリンク先のAWSドキュメントから拾えます。実在のクレデンシャルではないので、そのまま読み替えて使って問題ありません。

手順4: スクリプトを実行する

node test-s3.mjs

実行すると、各コマンドのレスポンスが順に出力されます。

=== 1. CreateBucket ===
status: 200
location: /my-bucket

=== 2. PutObject ===
status: 200
etag: "3f*******"

=== 3. ListBuckets ===
buckets: [ 'emulate-default', 'my-bucket' ]

=== 4. ListObjectsV2 ===
count: 1
keys: [ { Key: 'hello.txt', Size: 18 } ]

=== 5. GetObject ===
status: 200
content-type: text/plain
body: Hello from emulate

=== 6. DeleteObject ===
status: 204

=== 7. ListObjectsV2 (after delete) ===
count: 0

ETag、Content-Type、HTTPステータス(DeleteObjectは204)まで本物のS3と同じ挙動を返しています。

なお、最初にListBucketsを叩くとemulate-defaultという見覚えのないバケットが出てきます。これはemulateが初期値として用意しているサンプルバケットです。
SQSでも同様にemulate-default-queueが用意されています。本物のAWSにはない挙動なので、テストを書く場合は注意したほうがよさそうです。

また、http://localhost:4007/_inspectorを開くと、現在の状態をブラウザで確認できるダッシュボードが付いています。?tab=s3|sqs|iamでタブを切り替えられます。(デザインがイケてます!)
S3タブはバケット一覧と、各バケットの中のオブジェクトをまとめて表示してくれます。emulateのデフォルトで入っているemulate-defaultバケットも一緒に並ぶ点に注意です。

S3タブ。バケット一覧と各バケットのオブジェクト一覧が並ぶ

SQSタブはキューと、各キューに溜まっているメッセージ数。空のキューも一覧に出ます。

SQSタブ。キュー名・メッセージ数・FIFO/Visibility Timeoutが表で並ぶ

IAMタブはユーザー一覧(アクセスキー数とARN)とロール一覧。
※スクリーンショットのARNに見えるアカウントID 123456789012は、AWSの公式ドキュメントでも例示用に使われている架空の値で、実在のAWSアカウントを指すものではありません。

IAMタブ。ユーザー一覧とロール一覧

動作検証2: AWS SQSでSDK互換性問題を踏んだ話

S3はすんなり通ったのでSQSも同じノリで@aws-sdk/client-sqsから叩こうとしたらハマりました。

SDKでListQueuesを呼ぶとパースエラーが発生する

SQSClientを作ってListQueuesCommandを実行するとレスポンスのデシリアライズで落ちます。

ERROR: SyntaxError Unexpected token '<', "<?xml vers"... is not valid JSON
  Deserialization error: ...

原因の切り分け: curlで生のレスポンスを見る

SDKを介さずにcurlで同じエンドポイントを叩くと、XMLが正常に返ってきます。

curl -s "http://localhost:4007/sqs/" -X POST \
  -d "Action=ListQueues" \
  -H "Content-Type: application/x-www-form-urlencoded"
<?xml version="1.0" encoding="UTF-8"?>
<ListQueuesResponse>
  <ListQueuesResult>
    <QueueUrl>http://localhost:4007/sqs/123456789012/emulate-default-queue</QueueUrl>
  </ListQueuesResult>
  <ResponseMetadata><RequestId>...</RequestId></ResponseMetadata>
</ListQueuesResponse>

つまりサーバー側は正しくレスポンスを返していて、SDKがJSONを期待しているのに対してXMLが返るため落ちているという構図です。
AWS SDK v3のSQSクライアントは過去のXML queryプロトコルからJSONプロトコル(application/x-amz-json-1.0)に切り替わっており、emulateのSQSは今でもXML queryプロトコル前提で実装されている という世代差が原因と推察します。(この件もIssue #95を立てました。)

STSとIAMはエンドポイントにサフィックスを足せば動く

同じXML queryプロトコルでも、STSとIAMはSDKから動きました。endpoint/sts/iamのパスを足しておくと、SDKが組み立てるリクエストパスがemulate側のルーティング(POST /sts/POST /iam/)と噛み合います。

import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";

const sts = new STSClient({
  endpoint: "http://localhost:4007/sts",
  region: "us-east-1",
  credentials: {
    // emulateのデフォルトキー(AWSの公開サンプル値そのままで、念のため伏せています)
    accessKeyId: "AKIA...EXAMPLE",
    secretAccessKey: "...EXAMPLEKEY",
  },
});

const id = await sts.send(new GetCallerIdentityCommand({}));

実行すると、実際のSTSが返す形式でアカウント情報が取れます。

{
  Account: '123456789012',
  Arn:     'arn:aws:iam::123456789012:user/admin'
}

※出力されたアカウントID 123456789012は、AWS公式ドキュメントの識別子リファレンスでも例示用に広く使われている架空の値です。emulateもこの値をそのまま採用していて、実在のAWSアカウントを指すものではありません。

IAMも同じやり方(endpoint: "http://localhost:4007/iam")でユーザー作成・アクセスキー発行・一覧取得まで一通り動きます。

=== IAM: CreateUser ===
{ UserName: 'alice', Arn: 'arn:aws:iam::123456789012:user/alice' }

=== IAM: CreateAccessKey ===
{ Status: 'Active', HasSecret: true }

=== IAM: ListUsers ===
users: [ 'admin', 'alice' ]

S3/STS/IAMは実用範囲で動き、SQSもcurlで回避できる範囲なので致命的というほどではありません。組み込む前にサービスごとの叩き方だけ整理しておくと安全です。

動作検証3: reset()の挙動

emulateパッケージはCLIだけでなくプログラム内から呼べるcreateEmulator APIも持っていて、テストフックに組み込みやすい作りになっています。createEmulatorで起動してreset()でデータを巻き戻し、close()で停止する、という基本フローを検証します。

手順1: 検証スクリプトを書く

test-programmatic.mjsを作成します。バケットとオブジェクトを作ってからreset()を呼び、戻ったかを確認するシンプルな構成です。

test-programmatic.mjs
import { createEmulator } from "emulate";
import {
  S3Client,
  CreateBucketCommand,
  PutObjectCommand,
  ListObjectsV2Command,
} from "@aws-sdk/client-s3";

const aws = await createEmulator({ service: "aws", port: 5000 });
console.log("Started AWS emulator at:", aws.url);

const s3 = new S3Client({
  endpoint: aws.url,
  region: "us-east-1",
  credentials: {
    // emulateのデフォルトキー(AWSの公開サンプル値そのままで、念のため伏せています)
    accessKeyId: "AKIA...EXAMPLE",
    secretAccessKey: "...EXAMPLEKEY",
  },
  forcePathStyle: true,
});

await s3.send(new CreateBucketCommand({ Bucket: "prog-test" }));
await s3.send(new PutObjectCommand({ Bucket: "prog-test", Key: "key1", Body: "v1" }));
await s3.send(new PutObjectCommand({ Bucket: "prog-test", Key: "key2", Body: "v2" }));

const before = await s3.send(new ListObjectsV2Command({ Bucket: "prog-test" }));
console.log("Before reset:", before.KeyCount, "objects");

await aws.reset();

try {
  const after = await s3.send(new ListObjectsV2Command({ Bucket: "prog-test" }));
  console.log("After reset:", after.KeyCount, "objects");
} catch (e) {
  console.log("After reset: bucket gone (" + e.name + ")");
}

await aws.close();

このスクリプトはCLIでemulate startを立てる必要はなく、createEmulatorがプロセス内でサーバーを立ち上げます。テスト実行と同じプロセスで動くので、サーバーの起動待ちなどが不要になります。

手順2: 実行してデータが消えることを確認する

node test-programmatic.mjs
Started AWS emulator at: http://localhost:5000
Before reset: 2 objects in 'prog-test'
Called reset()
After reset: bucket gone (NoSuchBucket: NoSuchBucket)
Closed.

reset()を呼ぶとオブジェクトだけでなくバケットごと初期状態に戻り、再度ListObjectsV2を叩くとNoSuchBucketで落ちる挙動になりました。

手順3: Vitest/Jestのテストフックに組み込む

beforeAllで起動、afterEachreset()afterAllclose()という形に並べると、各テストで毎回まっさらなAWSが手に入る構成になります。

import { createEmulator, type Emulator } from "emulate";

let aws: Emulator;

beforeAll(async () => {
  aws = await createEmulator({ service: "aws", port: 5000 });
  process.env.AWS_ENDPOINT_URL = aws.url;
});

afterEach(() => aws.reset());
afterAll(() => aws.close());

外部APIモックとは違い、SDKの挙動そのものを通したままデータだけ初期化できるのは、地味にうれしい体験でした。

動作検証4: Vercel preview deploymentのOAuth問題が解けるか

@emulators/adapter-nextを使って、preview deployment環境でのOAuth検証問題をローカルで再現・解決できるか確かめます。

そもそも何が課題なのか

preview deployment自体の仕組みに馴染みがない方は、先に Vercel × GitLab連携でプレビュー環境(Preview Deployments)を試した別記事 を眺めておくとイメージしやすいです。

Vercelのpreview deploymentは、ブランチをpushしたりPRを作るたびにmy-app-git-feature-x-myteam.vercel.appのようなユニークなURLが自動採番されます。便利な仕組みですが、OAuth認証を絡めると次の壁にぶつかります。

GitHub・Google・SlackといったOAuthプロバイダー側では、ユーザーが認可した後にアプリへ戻ってくる「コールバックURL」をプロバイダー側のアプリ管理画面で事前登録する必要があります。たとえばGitHub OAuth Appだとhttps://my-app.com/api/auth/callback/githubのようなURLを登録しておき、それ以外のURLへリダイレクトしようとするとredirect_uri_mismatchで弾かれる、という仕様です。

ところがpreview deploymentのURLはブランチごと・pushごとに変わるので、

  • すべてのpreview URLを事前登録するのは現実的に不可能(数が無限)
  • ワイルドカード登録に対応していないプロバイダーが多い(セキュリティ上の理由)
  • 本番用とは別のテスト用OAuth Appを用意しても、結局URL分だけ登録が必要

となり、結果として「preview deploymentでサインインまわりの動作確認ができない」「マージ後の本番デプロイで初めて気づく」が発生しがち、というのが問題です。

@emulators/adapter-nextがどう解くか

@emulators/adapter-nextの役割は、エミュレータ(OAuthプロバイダーの代わり)をNext.jsアプリ自身のルートとして埋め込むことです。具体的にはNext.jsの動的ルーティング機能(app/emulate/[...path]/route.tsのように[...path]で書くと、/emulate/配下の任意のパスを1つのハンドラで受けられる仕組み)を使って、/emulate/github/...配下に来るリクエストをまとめてエミュレータに流します。

OAuthの認可サーバー役がアプリと同一オリジン(同じドメイン)で動くので、preview deploymentごとに変わるURLでも、コールバックは「自分自身の/emulate/github/...に戻ってくる」だけです。外部のGitHub OAuth Appを作る必要も、コールバックURLを事前登録する必要もなくなります。本番は普通に外部OAuthプロバイダーにつなぎ、preview期間中だけアプリ内のエミュレータで完結させる、という使い分けがしやすい設計です。

なおemulateのVercel API自体は素のREST + JSONで、プロジェクト作成・デプロイ作成・環境変数のCRUDあたりはイメージ通りに動きました。POST /v11/projectsPOST /v13/deployments/v10/projects/:idOrName/envといったパスもそのまま使えます。CRUDの動作確認は本物との差分が小さいので割愛し、ここではOAuthのループ検証に主眼を置きます。

手順1: Next.jsアプリを用意する

pnpm dlx create-next-app@latest oauth-demo --ts --tailwind --app --no-eslint --no-src-dir --use-pnpm
cd oauth-demo
pnpm add @emulators/adapter-next @emulators/github next-auth@beta

手順2: エミュレータをアプリのルートに埋め込む

app/emulate/[...path]/route.tsを作成し、@emulators/adapter-nextcreateEmulateHandlerでGitHubエミュレータを/emulate/github/*配下にマウントします。[...path]はNext.jsの動的ルーティング記法で、/emulate/以下に来る任意のパス(/emulate/github/login/oauth/authorizeでも/emulate/github/userでも)を1つのハンドラで一括して受けるためのものです。seedで擬似ユーザーを2人足しています。

app/emulate/[...path]/route.ts
import { createEmulateHandler } from "@emulators/adapter-next";
import * as github from "@emulators/github";

export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
  services: {
    github: {
      emulator: github,
      seed: {
        users: [
          { login: "octocat", name: "The Octocat", email: "octocat@github.com" },
          { login: "alice", name: "Alice", email: "alice@example.com" },
        ],
      },
    },
  },
});

あわせてnext.config.tswithEmulateでラップしておくと、エミュレータ画面が使うフォントなどがserverless traceに含まれます。Vercelにデプロイしたときに必要になる対応です。

next.config.ts
import type { NextConfig } from "next";
import { withEmulate } from "@emulators/adapter-next";

const nextConfig: NextConfig = {};

export default withEmulate(nextConfig);

手順3: Auth.jsのGitHub providerを埋め込みエミュレータに向ける

auth.tsnext-authのGitHubプロバイダーを設定し、authorization / token / userinfoのURLを/emulate/github/*に書き換えます。baseUrlprocess.env.VERCEL_URLを最優先にしているので、preview deploymentにデプロイしたときは自動でそのURLを掴みます。

auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";

const baseUrl = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : process.env.NEXTAUTH_URL ?? "http://localhost:3000";

export const { handlers, auth, signIn, signOut } = NextAuth({
  trustHost: true,
  providers: [
    GitHub({
      clientId: "any-value",
      clientSecret: "any-value",
      authorization: { url: `${baseUrl}/emulate/github/login/oauth/authorize` },
      token: { url: `${baseUrl}/emulate/github/login/oauth/access_token` },
      userinfo: { url: `${baseUrl}/emulate/github/user` },
    }),
  ],
});

clientId / clientSecretは実体がないので任意の値でよく、登録手順を踏まずに動かせる、というのがpreview deploymentとの相性の核です。

app/api/auth/[...nextauth]/route.tshandlersをre-exportし、app/page.tsxにサインインボタンとセッション表示を置けば最低限のデモが完成します。中身はsignIn("github")をServer Actionから呼ぶだけなので、ここでは省略します。

手順4: 実際にOAuthフローを通す

pnpm devで起動して、一連のフローを確認してみました。

サインアウト状態のホーム画面。Sign in with GitHub (emulated)ボタンが起点です。

Auth.jsデモのサインアウト状態。Sign in with GitHubボタンだけが置かれている

ボタンを押すと、Auth.jsが/emulate/github/login/oauth/authorize?...にリダイレクトされ、埋め込みGitHubエミュレータのユーザーピッカーが出てきます。seedで渡したユーザーも並んでいます。

GitHubエミュレータのユーザーピッカー。admin、alice、ghost、octocatの4人が並ぶ

octocatを選ぶとcodeが返ってきて、Auth.jsが裏で/emulate/github/login/oauth/access_tokenにコードを渡し、/emulate/github/userでユーザー情報を取得し、ホーム画面に戻ってきます。セッションが確立し、ユーザー名・メール・アバターURLが表示されています。

サインイン後のホーム画面。The Octocatとしてログイン済みでセッションペイロードが表示されている

セッションペイロードのimageの値がhttp://localhost:3001/emulate/github/avatars/u/octocatになっている点も気になります。アバターURLまで埋め込みエミュレータ側に向いていて、本物のgithub.comには一切触れていません。
preview deploymentにデプロイすれば、このベースURLがpreview URLに置き換わるだけで同じことが起きるはずです。

このあたりの疎通確認はAUTH_SECRETやVercel SSO Deployment Protectionまわりで踏み込みポイントが増えるので、本記事ではここまでの理論上の確認にとどめ、preview deployment上での通しの動作は別の記事でじっくり扱おうと思います。

サービスごとの守備範囲

ここまでで動かしたのはAWSとVercelですが、emulateがサポートしている12サービスをざっくり整理します。

サービス デフォルトポート(v0.5.0) 特に効く開発シーン
Vercel 4000 Vercel Marketplace連携機能、デプロイトリガーの自動化テスト
GitHub 4001 GitHub App/Actionの挙動検証、PR/Issue自動化テスト
Google 4002 OAuthログイン実装、Gmail/Calendar/Drive連携アプリ開発
Slack 4003 Slack AppのOAuth、Incoming Webhook、ボット応答テスト
Apple 4004 Sign in with AppleのPKCEフロー検証
Microsoft 4005 Entra ID SSOのフロー検証、Microsoft Graph連携
Okta 4006 エンタープライズSSOのテスト
AWS 4007 S3へのファイル配置、SQS、STSでAssumeRole(SQSはSDK互換性に注意)
Resend 4008 メール送信ロジック、テストでのインボックス検証
Stripe 4009 決済フロー、Webhookハンドラ、ホスト型Checkout動作確認
MongoDB Atlas 4010 Atlas APIを叩くプロビジョニング系コードのテスト
Clerk 4011 Clerk認証フロー、ユーザー・組織管理のテスト

OAuth/OIDC系(Google/Apple/Microsoft/Okta/Clerk)に厚いラインナップなのが特徴です。OIDCクライアントは通常、起動時に/.well-known/openid-configurationを叩いて認可サーバー側の各エンドポイントの場所を自動取得し、IDトークンの署名検証用の公開鍵もJWKSエンドポイントから取りに行く動きをします。emulateの各プロバイダーはこの2つを本物どおりに用意しているので、NextAuth/Auth.jsのようなOIDCクライアント側のコードをエミュレータ向けに書き換えなくてもそのまま動かせる、という流れです。

注意点

実運用前に押さえておきたい点を、検証で見つけたものを含めてまとめておきます。

ステートはインメモリです。プロセスを落とすと消えます。テストごとにリセットできるメリットでもありますが、永続化したいケースには向きません。Next.jsアダプタ(@emulators/adapter-next)を使う場合は、別途persistenceアダプタ(KVストアなど)を渡すことで永続化できる仕組みも用意されています。

AWSのサポート範囲はS3/SQS/IAM/STSのみです。DBやLambda、SNSなどはまだ対応していません。個人的にはDynamoDBをよく使うので、ここに乗ってきてくれると嬉しいです。

AWS SDKとの互換性はサービスによって差があります。今回検証した範囲では、

  • S3: forcePathStyle: trueを設定すればそのまま動く
  • STS/IAM: endpointにサービスサフィックス(/sts/iam)を付ければ動く
  • SQS: AWS SDK v3のデフォルトJSONプロトコルとは噛み合わない。fetch / curlでXML queryプロトコルを直接叩く必要あり

という結果でした。

@emulators/adapter-nextをVercelの実機preview deployment URLに乗せるところまでは試しましたが、Auth.jsのセッション完走(ユーザー情報の取得まで)はemulateの検証から外れる詰め事項が多く、本記事ではそこまで踏み込んでいません。
process.env.VERCEL_URLを拾うコード自体はそのまま動きますが、AUTH_SECRETなどの本番向け設定をしっかり整えてから取り組んだほうがよさそうです。

さいごに

emulateはAWSまるごとを狙うツールではなく、Vercel上で動くアプリが触るSaaS群を全部ローカルで完結させるためのライブラリという印象です。

  • AWS単体のローカル開発がしたい → LocalStackなど
  • SaaSとAWSが混在するフロントエンドアプリのCI・preview・オフライン開発 → emulate

という棲み分けで考えると、フロントエンド寄りのチームほど刺さりやすいと感じました。特にOAuthリダイレクトURLの事前登録が要るタイプのSaaSをVercel preview deploymentで動かしたい場合、他に良い選択肢が見当たらないので一度試す価値があります。
今回はAWSとVercelに絞って動かしましたが、SDKとの相性問題もS3/STS/IAMについては回避策で済む範囲でした。SQSだけは現在のAWS SDK v3と素直には繋がらなかったので、ここを使う場合は注意が必要です。
Vercel Labsはemulate以外にも portless(ローカルに安定した名前付きURLを割り当てるOSS)など、ローカル開発体験まわりに明確に投資しているので、このあたりの動きは追う価値がありそうです。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事