Skip to content

環境変数のZodバリデーション + sync-env.sh による自動同期 #112

@konokenj

Description

@konokenj

背景

現在の環境変数管理に以下の問題がある:

  1. typo・未設定の検出が遅い: process.env.XXX! の直接参照が6ファイルに散在しており、typoや未設定を実行時まで検出できない
  2. CDK Outputsの手動コピーが必要: cdk deploy 後にCLI出力から値を手動で .env.local にコピーする必要がある(README参照)
  3. .env.local.example が不完全: 現在のexampleファイルにはプレースホルダー値(dummy, "")が入っているが、どのCDK Outputに対応するかのコメントがない

提案

1. src/lib/env.ts — Zodバリデーション

環境変数をZodスキーマで一元管理し、トップレベルでバリデーションする。

// src/lib/env.ts
import { z } from "zod";

const envSchema = z.object({
  // Cognito認証
  COGNITO_DOMAIN: z.string().min(1),
  USER_POOL_ID: z.string().min(1),
  USER_POOL_CLIENT_ID: z.string().min(1),
  // カスタムドメインありの場合は直接設定、なしの場合はSSMパラメータ経由で動的取得
  AMPLIFY_APP_ORIGIN: z.string().min(1).optional(),
  // AppSync Events
  NEXT_PUBLIC_EVENT_HTTP_ENDPOINT: z.string().url(),
  NEXT_PUBLIC_AWS_REGION: z.string().min(1),
  // 非同期ジョブ
  ASYNC_JOB_HANDLER_ARN: z.string().min(1),
});

export const env = envSchema.parse(process.env);

各ファイルでは process.env の代わりに env.USER_POOL_ID のように参照する。

設計上の注意点:

  • トップレベルparse: モジュール読み込み時にバリデーションが走る。Next.jsが.env.localを読み込んだ後に実行されるため問題ない
  • AMPLIFY_APP_ORIGIN.optional(): カスタムドメインなしの場合、Lambda実行環境ではSSMパラメータから動的取得するため、デプロイ時点では未設定
  • Lambda注入変数は含めない: DATABASE_URL, EVENT_HTTP_ENDPOINT, AWS_REGION等はCDKがLambda環境変数として注入するため、ローカル開発用のenv.tsのスコープ外。DATABASE_URLはPrismaがprisma/.envから読み込む

2. ESLint no-restricted-syntaxprocess.env 直接参照を禁止

// eslint.config.mjs に追加
{
  rules: {
    "no-restricted-syntax": [
      "error",
      {
        selector: "MemberExpression[object.object.name='process'][object.property.name='env']",
        message: "process.env の直接参照は禁止です。src/lib/env.ts の env を使用してください。",
      },
    ],
  },
},
// 除外ファイル
{
  files: ["src/lib/env.ts", "src/lib/amplifyServerUtils.ts", "next.config.ts"],
  rules: { "no-restricted-syntax": "off" },
},
{
  files: ["tests/**/*.ts", "**/*.test.ts"],
  rules: { "no-restricted-syntax": "off" },
},

amplifyServerUtils.tsはAmplify SDKの初期化でAMPLIFY_APP_ORIGIN_SOURCE_PARAMETERからSSM経由の動的取得を行う特殊なファイルのため除外。テストファイルも除外する。

3. scripts/sync-env.mjs — CDK Outputsから .env.local を自動生成

bashスクリプトではなくNode.jsスクリプトとする。理由:

  • Node.js >= v20 はデプロイの前提条件に既にある(追加依存なし)
  • Windows環境でも動作する(WSL/Git Bash不要)
  • jqへの依存が不要(JSON.parseで済む)
#!/usr/bin/env node
// scripts/sync-env.mjs
import { execSync } from "child_process";
import { writeFileSync } from "fs";

const stackName = "ServerlessWebappStarterKitStack";
const outputs = JSON.parse(
  execSync(
    `aws cloudformation describe-stacks --stack-name ${stackName} --query "Stacks[0].Outputs" --output json`,
  ).toString(),
);

const get = (prefix) =>
  outputs.find((o) => o.OutputKey.startsWith(prefix))?.OutputValue ?? "";

const region = execSync("aws configure get region").toString().trim();

writeFileSync(
  "webapp/.env.local",
  `# DO NOT EDIT — generated by scripts/sync-env.mjs
COGNITO_DOMAIN=${get("AuthUserPoolDomainName")}
USER_POOL_ID=${get("AuthUserPoolId")}
USER_POOL_CLIENT_ID=${get("AuthUserPoolClientId")}
AMPLIFY_APP_ORIGIN=http://localhost:3010
NEXT_PUBLIC_EVENT_HTTP_ENDPOINT=${get("EventBusHttpEndpoint")}
NEXT_PUBLIC_AWS_REGION=${region}
ASYNC_JOB_HANDLER_ARN=${get("AsyncJobHandlerArn")}
`,
);

CfnOutputのキーにはCDKが付与するハッシュサフィックス(例: AuthUserPoolIdC0605E59)があるため、startsWithで前方一致検索する。

4. .env.local.example の拡充

# CDK Outputsから取得(scripts/sync-env.mjs で自動生成可能)
COGNITO_DOMAIN=       # CfnOutput: AuthUserPoolDomainName
USER_POOL_ID=         # CfnOutput: AuthUserPoolId
USER_POOL_CLIENT_ID=  # CfnOutput: AuthUserPoolClientId
NEXT_PUBLIC_EVENT_HTTP_ENDPOINT=  # CfnOutput: EventBusHttpEndpoint
NEXT_PUBLIC_AWS_REGION=us-west-2
ASYNC_JOB_HANDLER_ARN=            # CfnOutput: AsyncJobHandlerArn

# ローカル開発用(固定値)
AMPLIFY_APP_ORIGIN=http://localhost:3010

現在の process.env 直接参照箇所

ファイル 環境変数
src/lib/amplifyServerUtils.ts AMPLIFY_APP_ORIGIN, USER_POOL_ID, USER_POOL_CLIENT_ID, COGNITO_DOMAIN, AMPLIFY_APP_ORIGIN_SOURCE_PARAMETER
src/hooks/use-event-bus.ts NEXT_PUBLIC_EVENT_HTTP_ENDPOINT, NEXT_PUBLIC_AWS_REGION
src/lib/events.ts EVENT_HTTP_ENDPOINT, AWS_REGION
src/lib/jobs.ts ASYNC_JOB_HANDLER_ARN
src/lib/prisma.ts DATABASE_URL, NODE_ENV
src/jobs/async-job/translate.ts AWS_REGION

注: amplifyServerUtils.tsはESLint除外対象のため、process.env参照を維持する。それ以外のファイルはenv.ts経由に移行する。EVENT_HTTP_ENDPOINT, AWS_REGION, DATABASE_URL, NODE_ENVはLambda実行環境でCDKが注入する変数であり、ローカル開発時には使用しないためenv.tsのスキーマには含めない。

検証方法

  • scripts/sync-env.mjs でCDK Outputsから webapp/.env.local が自動生成される
  • 環境変数の不足・型不正がアプリ起動時にZodエラーとして検出される
  • process.env の直接参照がESLintエラーになる(除外ファイル以外)

備考

  • pnpm workspaces化(pnpm workspaces でモノレポ化 #98)後に実装する前提。モノレポ構成ではスクリプトの配置場所や.env.localのパスが変わる可能性がある

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions