🛡️

AWS Lambda専用!環境変数を型安全にする軽量ライブラリ「lambda-env-schema」を作りました

に公開

概要

タイトルの通り、Lambda関数をTypeScriptで実装する方々に向けて、環境変数をType-Safeに扱うためのOSSツールを作成しました。
lambda-env-schemaです。

https://github.com/kawaaaas/lambda-env-schema

https://www.npmjs.com/package/@kawaaaas/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の主要な特徴は以下の通りです。

  1. Zero Dependencies
    依存ライブラリが無く、軽量さを維持しているためライブラリ導入によるコールドスタートへの影響を最小化しています。
  2. Zero-config Coercion
    環境変数を自動で指定した型に変換します。
    Typeを設定するだけであり、特別な処理の実装は不要です。
  3. Lambda-First Design
    env.aws.region など Lambda 固有の環境変数に型安全にアクセスできます。
    また、30種類以上のAWSリソースバリデーターを実装しており、ARNやURLに応じて自動的に適切なバリデーションを実行します。また、一部のリソースはパースされるため、構造化オブジェクトとして取得して各プロパティへアクセス可能です。
  4. Fail-fast Validation
    ハンドラ外で定義することで、初期化時に一括バリデーションを行います。
    そのため、環境変数におけるエラーをリクエスト処理中ではなく、起動時に検出できます。
  5. 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をご確認ください。

https://www.npmjs.com/package/@kawaaaas/lambda-env-schema

基本機能

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!' }),
  };
};

対応する型

基本の型として、以下の型をサポートしています。

  • string
  • number
  • boolean
  • array
  • json

それぞれ、以下の様に指定できます。

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実行環境が自動的に設定する環境変数に型安全にアクセスできます。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime

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