AWS Lambda専用!環境変数を型安全にする軽量ライブラリ「lambda-env-schema」を作りました
概要
タイトルの通り、Lambda関数をTypeScriptで実装する方々に向けて、環境変数をType-Safeに扱うためのOSSツールを作成しました。
lambda-env-schemaです。
この記事では、このOSSツールの特徴や基本的な使用方法を解説していますので、現在抱えている課題にマッチした際には是非lambda-env-schemaを使用していただけると嬉しいです。
また、このツールをより良くするためのPRはもちろん、feature requestやissue提案、応援の意味でのスターなど一つ一つが開発の励みになりますので、是非使用して思ったことや感じたことがあればGithub上でアクションいただけますと幸いです!
従来の課題
TypeScriptによるLambda開発において、環境変数の扱いには従来、以下のような課題がありました。
型安全性の欠如
// process.env は常に string | undefined
const port = process.env.PORT; // string | undefined
const timeout = parseInt(process.env.TIMEOUT_MS!); // 非Nullアサーションが必要
process.envの値は常にstring | undefinedになってしまうため、実際には値が存在することが分かっていても、TypeScriptの型システム上は!やas stringで明示的にアサーションしなければいけませんでした。
手動の型変換のボイラープレート化
const port = parseInt(process.env.PORT || '3000');
const debug = process.env.DEBUG === 'true';
const origins = process.env.ALLOWED_ORIGINS?.split(',') || [];
環境変数は常に文字列として取得されるため、数値・真偽値・配列などへの変換ロジックを毎回手書きする必要がありました。同じパターンの繰り返しはコードの冗長化を招き、変換ミスによるバグの原因となります。
重いライブラリの導入によるコールドスタート
Zodなどのバリデーションライブラリはとても人気で便利ですが、Lambdaの環境変数のバリデーションをしたいだけのユースケースにおいては重厚です。また他にもライブラリを導入している場合、Lambda環境ではバンドルサイズの増加がコールドスタート時間に直結するため、開発体験とパフォーマンス面でのトレードオフが生じてしまいます。
これらの課題をlambda-env-schemaは解決します。
特徴
lambda-env-schemaの主要な特徴は以下の通りです。
-
Zero Dependencies
依存ライブラリが無く、軽量さを維持しているためライブラリ導入によるコールドスタートへの影響を最小化しています。 -
Zero-config Coercion
環境変数を自動で指定した型に変換します。
Typeを設定するだけであり、特別な処理の実装は不要です。 -
Lambda-First Design
env.aws.region など Lambda 固有の環境変数に型安全にアクセスできます。
また、30種類以上のAWSリソースバリデーターを実装しており、ARNやURLに応じて自動的に適切なバリデーションを実行します。また、一部のリソースはパースされるため、構造化オブジェクトとして取得して各プロパティへアクセス可能です。 -
Fail-fast Validation
ハンドラ外で定義することで、初期化時に一括バリデーションを行います。
そのため、環境変数におけるエラーをリクエスト処理中ではなく、起動時に検出できます。 -
Developer-friendly
厳密な型補完やSNAKE_CASEからCamelCaseへの変換など、開発体験がよくなる機能を豊富に実装しています。
利用方法
lambda-env-schemaは一般的なnpmライブラリとして公開されています。
npm install @kawaaaas/lambda-env-schema
pnpm add @kawaaaas/lambda-env-schema
yarn add @kawaaaas/lambda-env-schema
また、Nodeランタイム上で動作するため、ローカルでの動作検証が可能です。
内部では、process.envにて環境変数を取得するため、dotenvなどを利用することでローカルの.envファイルから環境変数を取得できます。
主な機能
それでは、lambda-env-schemaの主要機能について解説していきます。
APIの詳細はnpmのREADMEをご確認ください。
基本機能
createEnvによるスキーマ定義とバリデーション
Lambdaハンドラ外でcreateEnvを呼び出すことによって環境変数のスキーマを定義できます。
createEnvによってスキーマを定義することにより、バリデーションがかかり、指定した型に自動で変換されます。
const env = createEnv({
DEBUG: { type: 'boolean' },
});
export const handler = async () => {
console.log(env.DEBUG); // boolean型
return {
statusCode: 200,
body: JSON.stringify({ message: 'Hello from Lambda!' }),
};
};
対応する型
基本の型として、以下の型をサポートしています。
stringnumberbooleanarrayjson
それぞれ、以下の様に指定できます。
const env = createEnv({
LOG_LEVEL: { type: 'string'},
PORT: { type: 'number'},
DEBUG: { type: 'boolean' },
ALLOWED_ORIGINS: { type: 'array', itemType: 'string'},
DATABASE_CONFIG: { type: 'json' },
});
console.log(env.LOG_LEVEL)
console.log(env.PORT)
console.log(env.DEBUG)
console.log(env.ALLOWED_ORIGINS)
console.log(env.DATABASE_CONFIG)
requiredとdefault
環境変数の必須/任意をrequiredとdefaultで制御できます。
| 設定 | 環境変数が未設定の場合 | 型推論 |
|---|---|---|
required: true |
バリデーションエラー | T |
default: 値 |
デフォルト値を使用 | T |
| どちらも未指定 | undefined |
T | undefined |
const env = createEnv({
LOG_LEVEL: { type: 'string', required: true},
PORT: { type: 'number', default: 80},
DEBUG: { type: 'boolean'},
});
console.log(env.LOG_LEVEL) // string
console.log(env.PORT) // number
console.log(env.DEBUG) // boolean | undefined
高度なバリデーション
デフォルトのTypeベースのバリデーションに加えて、ユーザーが独自のバリデーションロジックを指定することもできます。
文字列のバリデーション
文字列型では以下のバリデーションオプションが利用できます。
| オプション | 説明 |
|---|---|
enum |
許可する値のリスト |
pattern |
正規表現パターン |
minLength |
最小文字数 |
maxLength |
最大文字数 |
const env = createEnv({
// 許可リスト - 'dev', 'staging', 'prod' のみ許可
NODE_ENV: {
type: 'string',
enum: ['dev', 'staging', 'prod'] as const,
required: true,
},
// 正規表現 - UUIDフォーマットを検証
REQUEST_ID: {
type: 'string',
pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
},
// 文字数制限
SESSION_TOKEN: {
type: 'string',
minLength: 32,
maxLength: 128,
required: true,
},
});
数値のバリデーション
数値型では範囲を指定できます。
| オプション | 説明 |
|---|---|
min |
最小値 |
max |
最大値 |
const env = createEnv({
// ポート番号 - 1〜65535 の範囲
PORT: {
type: 'number',
min: 1,
max: 65535,
default: 3000,
},
});
配列のバリデーション
配列型では要素数とセパレータを指定できます。
| オプション | 説明 |
|---|---|
itemType |
要素の型('string' または 'number') |
separator |
区切り文字(デフォルト: ",") |
minLength |
最小要素数 |
maxLength |
最大要素数 |
const env = createEnv({
// 文字列配列 - "a,b,c" → ["a", "b", "c"]
ALLOWED_ORIGINS: {
type: 'array',
itemType: 'string',
minLength: 1, // 最低1つは必要
required: true,
},
// 数値配列 - "1,2,3" → [1, 2, 3]
RETRY_DELAYS_MS: {
type: 'array',
itemType: 'number',
default: [100, 500, 1000],
},
// カスタムセパレータ - "a|b|c" → ["a", "b", "c"]
TAGS: {
type: 'array',
itemType: 'string',
separator: '|',
},
});
Lambda特化
env.aws による Lambda 環境変数への型安全なアクセス
createEnvの戻り値にはawsプロパティが含まれており、Lambda実行環境が自動的に設定する環境変数に型安全にアクセスできます。
const env = createEnv({
MY_VAR: { type: 'string', required: true },
});
export const handler = async () => {
// Lambda環境変数への型安全なアクセス
console.log(env.aws.region); // 'ap-northeast-1'
console.log(env.aws.functionName); // 'my-function'
console.log(env.aws.functionVersion); // '$LATEST'
console.log(env.aws.memoryLimitInMB); // 128 (number型)
console.log(env.aws.logGroupName); // '/aws/lambda/my-function'
console.log(env.aws.logStreamName); // '2024/01/01/[$LATEST]abc123...'
return { statusCode: 200, body: 'OK' };
};
現在は以下の環境変数に対応しています。
| プロパティ | 対応する環境変数 | 型 | 説明(補足) |
|---|---|---|---|
region |
AWS_REGION |
string | undefined |
実行リージョン |
functionName |
AWS_LAMBDA_FUNCTION_NAME |
string | undefined |
関数名 |
functionVersion |
AWS_LAMBDA_FUNCTION_VERSION |
string | undefined |
バージョン |
memoryLimitInMB |
AWS_LAMBDA_FUNCTION_MEMORY_SIZE |
number | undefined |
メモリ量 (MB) |
logGroupName |
AWS_LAMBDA_LOG_GROUP_NAME |
string | undefined |
ロググループ名 |
logStreamName |
AWS_LAMBDA_LOG_STREAM_NAME |
string | undefined |
ログストリーム名 |
executionEnv |
AWS_EXECUTION_ENV |
string | undefined |
ランタイム識別子 |
accessKeyId |
AWS_ACCESS_KEY_ID |
string | undefined |
アクセスキーID |
secretAccessKey |
AWS_SECRET_ACCESS_KEY |
string | undefined |
シークレットキー |
sessionToken |
AWS_SESSION_TOKEN |
string | undefined |
セッショントークン |
runtimeApi |
AWS_LAMBDA_RUNTIME_API |
string | undefined |
ランタイムAPIのアドレス |
taskRoot |
LAMBDA_TASK_ROOT |
string | undefined |
コードへのパス |
全てを網羅できているわけではありませんが、今後もリクエストに応じて追加で実装していきます。
30種類以上のAWSリソースバリデーター
AWSリソースの識別子(ARN、URL、名前など)を検証する専用の型が用意されています。フォーマットが不正な場合、起動時にエラーとなります。
また、一部のARNやURLなど、構造を持つ識別子はパースされたオブジェクトとして取得できます。
バリデーションのみ(string型を返す)
const env = createEnv({
// リージョン・アカウント
REGION: { type: 'aws-region', required: true },
ACCOUNT_ID: { type: 'aws-account-id', required: true },
// S3
BUCKET_NAME: { type: 's3-bucket-name', required: true },
// DynamoDB
TABLE_NAME: { type: 'dynamodb-table-name', required: true },
// Lambda
FUNCTION_NAME: { type: 'lambda-function-name', required: true },
// VPC
VPC_ID: { type: 'vpc-id', required: true },
SUBNET_ID: { type: 'subnet-id', required: true },
SECURITY_GROUP_ID: { type: 'security-group-id', required: true },
});
// すべて string 型
console.log(env.BUCKET_NAME) // string
パース付き(構造化されたオブジェクトを返す)
const env = createEnv({
QUEUE_URL: { type: 'sqs-queue-url', required: true },
BUCKET_ARN: { type: 's3-arn', required: true },
TABLE_ARN: { type: 'dynamodb-table-arn', required: true },
DB_ENDPOINT: { type: 'rds-endpoint', required: true },
});
// SQS Queue URL からキュー名やリージョンを取得
console.log(env.QUEUE_URL.queueName); // 'my-queue'
console.log(env.QUEUE_URL.region); // 'ap-northeast-1'
console.log(env.QUEUE_URL.accountId); // '123456789012'
// S3 ARN からバケット名やキーを取得
console.log(env.BUCKET_ARN.bucketName); // 'my-bucket'
console.log(env.BUCKET_ARN.key); // 'path/to/object'
// DynamoDB Table ARN からテーブル名を取得
console.log(env.TABLE_ARN.tableName); // 'my-table'
console.log(env.TABLE_ARN.region); // 'ap-northeast-1'
// RDS Endpoint からホストやポートを取得
console.log(env.DB_ENDPOINT.host); // 'mydb.xxx.ap-northeast-1.rds.amazonaws.com'
console.log(env.DB_ENDPOINT.port); // 5432
console.log(env.DB_ENDPOINT.region); // 'ap-northeast-1'
開発者体験
namingStrategy: 'camelCase' オプション
namingStrategy: 'camelCase'を指定すると、SNAKE_CASEの環境変数名をcamelCaseに変換して取得できます。
const env = createEnv({
API_KEY: { type: 'string', required: true },
MAX_CONNECTIONS: { type: 'number', default: 10 },
LOG_LEVEL: { type: 'string', default: 'info' },
}, { namingStrategy: 'camelCase' });
// camelCase でアクセス
console.log(env.apiKey); // string
console.log(env.maxConnections); // number
console.log(env.logLevel); // string
解析可能なエラーメッセージ
バリデーションエラーが発生した場合、どの環境変数に問題があるか、何が期待されているかが一目で分かるエラーメッセージを出力します。
以下は例です。
EnvironmentValidationError: 3 environment variable(s) failed validation:
✗ PORT: Expected number, received "abc"
✗ NODE_ENV: Value "invalid" is not in allowed values: dev, staging, prod
Set these in your Lambda configuration or .env file.
エラーハンドリング
ログのマスク
ログに環境変数が出力されない様、secretオプションを用意しています。
secret: trueを指定した環境変数は、エラーメッセージ内で値が表示されません。
try {
createEnv({
PORT: { type: 'number', required: true, secret: true },
});
} catch (e) {
if (e instanceof EnvironmentValidationError) {
for (const error of e.errors) {
console.log(error.key); // 'PORT'
console.log(error.message); // 'Expected number, got "***"'
console.log(error.received); // '***'
console.log(error.expected); // 'number'
}
console.error(JSON.stringify({
errorType: e.name,
errorCount: e.errors.length,
errors: e.errors,
}));
}
}
PORT
Expected number, got "***"
***
number
{"errorType":"EnvironmentValidationError","errorCount":1,"errors":[{"key":"PORT","message":"Expected number, got \"***\"","received":"***","expected":"number"}]}
上記の様に、エラーメッセージが***でマスクされます。
まとめ
まだまだ改善の余地がありますが、是非lambda-env-schemaを使用してくださると嬉しいです!
最後に、ここまで読んでくださった皆様本当にありがとうございました!
Discussion