最終更新:
Vercel Edge Config実践: グローバル設定のリアルタイム管理
Edge Configとは
Vercel Edge Configは、エッジランタイムで高速にアクセスできるグローバルなKey-Valueストアです。従来のデータベースやAPIコールと異なり、超低レイテンシ(1ms以下)でデータを取得できるため、Feature Flag、A/Bテスト、動的設定に最適です。
主な特徴
- 超低レイテンシ: エッジネットワーク全体にレプリケーション
- リアルタイム更新: 数秒でグローバルに反映
- バージョン管理: 設定変更履歴の追跡
- SDKサポート: Next.js、SvelteKit等で簡単に利用可能
従来の設定管理との比較
// 従来: 環境変数 (デプロイが必要)
const FEATURE_ENABLED = process.env.NEXT_PUBLIC_FEATURE_ENABLED === 'true';
// 従来: データベース (レイテンシが高い)
const config = await db.config.findUnique({ where: { key: 'feature' } });
// Edge Config: リアルタイム + 低レイテンシ
import { get } from '@vercel/edge-config';
const featureEnabled = await get('feature_enabled');
セットアップ
1. Edge Config作成
Vercelダッシュボードまたはコマンドラインで作成します。
# Vercel CLIでプロジェクトにEdge Configを接続
vercel env add EDGE_CONFIG
# コンソールで生成されたEdge Config URLを貼り付け
# または直接作成
vercel edge-config create my-config
2. Next.jsプロジェクトへの統合
npm install @vercel/edge-config
// lib/edge-config.ts
import { createClient } from '@vercel/edge-config';
export const edgeConfig = createClient(process.env.EDGE_CONFIG);
// 型安全なラッパー
export interface AppConfig {
featureFlags: {
newDashboard: boolean;
betaFeatures: boolean;
maintenanceMode: boolean;
};
abTests: {
pricingPageVariant: 'A' | 'B' | 'C';
};
limits: {
maxUploadSize: number;
rateLimit: number;
};
}
export async function getConfig<K extends keyof AppConfig>(
key: K
): Promise<AppConfig[K] | undefined> {
return edgeConfig.get<AppConfig[K]>(key);
}
// すべての設定を一度に取得
export async function getAllConfig(): Promise<Partial<AppConfig>> {
return edgeConfig.getAll<AppConfig>();
}
3. Edge Runtime対応ミドルウェア
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { get } from '@vercel/edge-config';
export async function middleware(request: NextRequest) {
// メンテナンスモードチェック
const maintenanceMode = await get<boolean>('maintenanceMode');
if (maintenanceMode && !request.nextUrl.pathname.startsWith('/maintenance')) {
return NextResponse.redirect(new URL('/maintenance', request.url));
}
// Feature Flagによるルーティング
const newDashboard = await get<boolean>('featureFlags.newDashboard');
if (newDashboard && request.nextUrl.pathname === '/dashboard') {
return NextResponse.rewrite(new URL('/dashboard-v2', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Feature Flag実装パターン
1. コンポーネントレベルのFeature Flag
// components/feature-flag.tsx
import { getConfig } from '@/lib/edge-config';
interface FeatureFlagProps {
flag: string;
children: React.ReactNode;
fallback?: React.ReactNode;
}
export async function FeatureFlag({ flag, children, fallback }: FeatureFlagProps) {
const flags = await getConfig('featureFlags');
const isEnabled = flags?.[flag as keyof typeof flags] ?? false;
if (!isEnabled) {
return fallback ? <>{fallback}</> : null;
}
return <>{children}</>;
}
// 使用例
export default async function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<FeatureFlag
flag="newDashboard"
fallback={<LegacyDashboard />}
>
<NewDashboard />
</FeatureFlag>
</div>
);
}
2. ユーザーセグメント別Feature Flag
// lib/feature-flag.ts
import { getConfig } from '@/lib/edge-config';
import { getServerSession } from 'next-auth';
export async function isFeatureEnabled(
featureName: string,
userId?: string
): Promise<boolean> {
const config = await getConfig('featureFlags');
const feature = config?.[featureName];
if (!feature) return false;
// シンプルなBoolean
if (typeof feature === 'boolean') {
return feature;
}
// ロールアウト率指定
if (typeof feature === 'object' && 'rollout' in feature) {
const rollout = feature.rollout as number; // 0-100
if (!userId) return false;
// ユーザーIDをハッシュして一貫性のあるロールアウト
const hash = Array.from(userId).reduce(
(acc, char) => acc + char.charCodeAt(0),
0
);
return (hash % 100) < rollout;
}
// 許可リスト
if (typeof feature === 'object' && 'allowList' in feature) {
const allowList = feature.allowList as string[];
return userId ? allowList.includes(userId) : false;
}
return false;
}
// Edge Config設定例
/*
{
"featureFlags": {
"newDashboard": true,
"betaFeatures": {
"rollout": 25
},
"premiumFeature": {
"allowList": ["user-123", "user-456"]
}
}
}
*/
3. A/Bテスト実装
// lib/ab-test.ts
import { getConfig } from '@/lib/edge-config';
import { cookies } from 'next/headers';
export type Variant = 'A' | 'B' | 'C';
export async function getVariant(
testName: string,
userId?: string
): Promise<Variant> {
const cookieStore = cookies();
const variantCookie = cookieStore.get(`ab_${testName}`);
// 既存のバリアントがあればそれを使用
if (variantCookie) {
return variantCookie.value as Variant;
}
// Edge Configから配分設定を取得
const abTests = await getConfig('abTests');
const distribution = abTests?.[testName] || { A: 50, B: 50 };
// ランダムに割り当て
const variant = assignVariant(distribution, userId);
// Cookieに保存(クライアント側で設定)
return variant;
}
function assignVariant(
distribution: Record<Variant, number>,
userId?: string
): Variant {
const random = userId
? hashString(userId) % 100
: Math.floor(Math.random() * 100);
let cumulative = 0;
for (const [variant, percentage] of Object.entries(distribution)) {
cumulative += percentage;
if (random < cumulative) {
return variant as Variant;
}
}
return 'A';
}
function hashString(str: string): number {
return Array.from(str).reduce((acc, char) => acc + char.charCodeAt(0), 0);
}
// 使用例
export default async function PricingPage() {
const variant = await getVariant('pricingPageTest');
return (
<div>
{variant === 'A' && <PricingTableA />}
{variant === 'B' && <PricingTableB />}
{variant === 'C' && <PricingTableC />}
</div>
);
}
動的設定管理
レート制限の動的調整
// lib/rate-limit.ts
import { get } from '@vercel/edge-config';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
export async function createRateLimiter() {
// Edge Configから動的に取得
const limits = await get<{ rateLimit: number }>('limits');
const requestsPerMinute = limits?.rateLimit ?? 10;
return new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(requestsPerMinute, '1 m'),
});
}
// API Routeでの使用
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const ratelimit = await createRateLimiter();
const identifier = request.ip ?? 'anonymous';
const { success, limit, remaining } = await ratelimit.limit(identifier);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
},
}
);
}
// 処理を続行
return NextResponse.json({ success: true });
}
緊急メンテナンスモード
// app/layout.tsx
import { get } from '@vercel/edge-config';
import MaintenanceBanner from '@/components/maintenance-banner';
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const maintenance = await get<{
enabled: boolean;
message: string;
scheduledAt?: string;
}>('maintenance');
return (
<html lang="ja">
<body>
{maintenance?.enabled && (
<MaintenanceBanner message={maintenance.message} />
)}
{children}
</body>
</html>
);
}
パフォーマンス最適化
1. バッチ取得で通信回数を削減
// ❌ 非効率: 複数回の取得
const feature1 = await get('feature1');
const feature2 = await get('feature2');
const feature3 = await get('feature3');
// ✅ 効率的: 一度に取得
const config = await getAll(['feature1', 'feature2', 'feature3']);
2. Edge Functionでのキャッシング
// lib/cached-config.ts
import { unstable_cache } from 'next/cache';
import { getConfig } from './edge-config';
export const getCachedConfig = unstable_cache(
async (key: string) => getConfig(key),
['edge-config'],
{
revalidate: 60, // 60秒キャッシュ
tags: ['config'],
}
);
// 必要に応じて手動で再検証
import { revalidateTag } from 'next/cache';
export async function refreshConfig() {
revalidateTag('config');
}
3. クライアント側での利用
// app/api/config/route.ts
import { NextResponse } from 'next/server';
import { getConfig } from '@/lib/edge-config';
export async function GET() {
const publicConfig = await getConfig('publicSettings');
return NextResponse.json(publicConfig, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120',
},
});
}
// クライアント側
import useSWR from 'swr';
export function usePublicConfig() {
return useSWR('/api/config', fetch);
}
設定更新のベストプラクティス
Vercel APIを使った自動更新
// scripts/update-config.ts
import { fetch } from 'undici';
async function updateEdgeConfig(updates: Record<string, any>) {
const edgeConfigId = process.env.EDGE_CONFIG_ID!;
const token = process.env.VERCEL_TOKEN!;
const response = await fetch(
`https://api.vercel.com/v1/edge-config/${edgeConfigId}/items`,
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: Object.entries(updates).map(([key, value]) => ({
operation: 'update',
key,
value,
})),
}),
}
);
if (!response.ok) {
throw new Error(`Failed to update Edge Config: ${await response.text()}`);
}
return response.json();
}
// GitHub Actionsでの使用例
/*
name: Update Feature Flags
on:
workflow_dispatch:
inputs:
feature:
description: 'Feature name'
required: true
enabled:
description: 'Enable feature'
type: boolean
required: true
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Update Edge Config
run: |
curl -X PATCH \
"https://api.vercel.com/v1/edge-config/${{ secrets.EDGE_CONFIG_ID }}/items" \
-H "Authorization: Bearer ${{ secrets.VERCEL_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"items": [{
"operation": "update",
"key": "featureFlags.${{ inputs.feature }}",
"value": ${{ inputs.enabled }}
}]
}'
*/
設定スキーマ検証
// lib/config-schema.ts
import { z } from 'zod';
export const ConfigSchema = z.object({
featureFlags: z.object({
newDashboard: z.boolean(),
betaFeatures: z.boolean(),
maintenanceMode: z.boolean(),
}),
abTests: z.record(z.string(), z.number()),
limits: z.object({
maxUploadSize: z.number().positive(),
rateLimit: z.number().positive(),
}),
});
export type ValidatedConfig = z.infer<typeof ConfigSchema>;
// 安全な取得関数
import { getAll } from '@vercel/edge-config';
export async function getValidatedConfig(): Promise<ValidatedConfig> {
const config = await getAll();
return ConfigSchema.parse(config);
}
実践例: カナリアリリース
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { get } from '@vercel/edge-config';
export async function middleware(request: NextRequest) {
const canaryConfig = await get<{
enabled: boolean;
percentage: number;
}>('canary');
if (!canaryConfig?.enabled) {
return NextResponse.next();
}
// ユーザーをカナリアグループに振り分け
const userId = request.cookies.get('user_id')?.value;
const isCanary = userId
? hashString(userId) % 100 < canaryConfig.percentage
: Math.random() * 100 < canaryConfig.percentage;
if (isCanary) {
// 新バージョンにルーティング
const url = request.nextUrl.clone();
url.pathname = `/canary${url.pathname}`;
return NextResponse.rewrite(url);
}
return NextResponse.next();
}
function hashString(str: string): number {
return Array.from(str).reduce((acc, char) => acc + char.charCodeAt(0), 0);
}
まとめ
Vercel Edge Configを活用することで、デプロイ不要でアプリケーション設定をリアルタイムに変更できます。Feature Flag、A/Bテスト、動的レート制限など、現代的なWebアプリケーションに必要な機能を低レイテンシで実現できる強力なツールです。
次のステップ
- Edge Configと環境変数の使い分けを検討
- Feature Flagの段階的ロールアウト戦略を設計
- A/Bテスト結果の分析基盤構築
- 設定変更の監査ログ整備