aws-samples / aws-samples/serverless-full-stack-webapp-starter-kit

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

Open
#112 1 comment 0 reactions 0 assignees View on GitHub
enhancement stale
Dominant language
TypeScript
Stars
229
Forks
45
Avg merge
1m
Merged PRs (30d)
4

Description

## 背景

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

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

## 提案

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

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

```typescript
// 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-syntax` で `process.env` 直接参照を禁止

```javascript
// 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`で済む)

```javascript
#!/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化(#98)後に実装する前提。モノレポ構成ではスクリプトの配置場所や`.env.local`のパスが変わる可能性がある

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.