【新機能】AWS Amplify CLIで作成するAWS Lambda Functionで環境変数の設定とシークレット値へのアクセスが可能になりました

Amplify CLIから管理下のLambda関数に環境変数を設定できる機能が追加されました? これによって、Amplify CLIを使う場合でも、バックエンドにおいて環境(dev, stg, prdなど)ごとの固有の設定を切り離して管理することができるようになります。
2021.07.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

おはようございます、加藤です。本日、Amplify CLIから管理下のLambda関数に環境変数を設定できる機能が追加されました?
これによって、Amplify CLIを使う場合でも、バックエンドにおいて環境(dev, stg, prdなど)ごとの固有の設定を切り離して管理することができるようになりました。 また、Amplify CLIからAWS Systems Manager Paramater Storeにシークレットを保存し、環境変数にキーを保存することで、関数内からAWS SDK経由にシークレットを取得することも可能になりました。

AWS Amplify CLI adds support for storing environment variables and secrets accessed by AWS Lambda functions

環境の準備

AmplifyのReact向けチュートリアルをConnect API and database to the appまで完了させます。

Getting started - Amplify Docs

Amplify CLIのバージョンは現時点で最新の5.1.0を使用しました。

npm i -g @aws-amplify/cli@5.1.0

環境変数/シークレットを利用できるLambda関数を作成する

Amplify CLIでLambda関数を作成します。ウィザードに従い環境変数とシークレットを定義します。シークレットの値は適当に設定してください。私は THE_API_KEY としました。
途中で入力を求められるLambda関数の内容については後述しています。

$ amplify add function

? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: envs
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World

Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration

? Do you want to configure advanced settings? Yes
? Do you want to access other resources in this project from your Lambda function? No
? Do you want to invoke this function on a recurring schedule? No
? Do you want to enable Lambda layers for this function? No

# ? 環境変数API_URLの定義
? Do you want to configure environment variables for this function? Yes
? Enter the environment variable name: API_URL
? Enter the environment variable value: https://checkip.amazonaws.com
? Select what you want to do with environment variables: I'm done

# ? シークレットAPI_KEYの定義
? Do you want to configure secret values this function can access? Yes
? Enter a secret name (this is the key used to look up the secret value): API_KEY
? Enter the value for API_KEY: [hidden]
? What do you want to do? I'm done
Use the AWS SSM GetParameter API to retrieve secrets in your Lambda function.
More information can be found here: https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParameter.html

? Do you want to edit the local lambda function now? Yes
? Choose your default editor: IntelliJ IDEA
Edit the file in your editor: /Users/kato.ryo/tmp/amplify-cli-env-for-lambda/amplify/backend/function/envs/src/index.js
? Press enter to continue 
Successfully added resource envs locally.

Next steps:
Check out sample function code generated in <project-dir>/amplify/backend/function/envs/src
"amplify function build" builds all of your functions currently in the project
"amplify mock function <functionName>" runs your function locally
"amplify push" builds all of your local backend resources and provisions them in the cloud
"amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud

Lambda関数を書きます。

const aws = require('aws-sdk');

exports.handler = async (event) => {

  const {Parameters} = await (new aws.SSM())
    .getParameters({
      Names: ['API_KEY'].map(secretName => process.env[secretName]),
      WithDecryption: true,
    })
    .promise();

  console.dir(
  {
      apiUrl: process.env.API_URL,
      apiKey: Parameters[0].Value,
    },
  );

  return {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': '*',
    },
    body: JSON.stringify({
      apiUrl: process.env.API_URL,
      apiKey: Parameters[0].Value,
    }),
  };
};

環境変数API_URLには平文データが、環境変数API_KEYにはSSM Parameter Storeのキー名が保存されています。前者はprocess.env.API_KEYで直接データの取得ができ、後者はAWS SDKを使うことでシークレットを取得できます。

作成されたLambda関数を確認する

作成されたLambda関数にどのような環境変数が設定されているか確認します。

Paramater Storeへのアクセス権限も、対象のリソースが限定され不要な権限を与えないように考慮されています。

Lambda関数をテスト実行すると、環境変数とシークレットが取得できたことを確認できます。

環境変数を使うときに気をつけたいこと

ここまでだけだと文量が少なく少しさびしいので、環境変数に関連するソフトウエア設計の話を少しします。

今回のアップデートによってAmplify CLIを使った場合でもLambda関数に簡単に環境変数を設定できるようになりました。
環境変数を使うことによって外部からパラメータの注入を行いやすくなりますが、環境変数に関するありがちなアンチパターンとして環境名を使った条件分岐によるオープン・クローズドの原則への違反があります。(オープン・クローズドの原則に関してはインターフェイスを引数とすることによって準拠するサンプルが多いですが、筆者は下記のようなサンプルも該当すると原理的には当てはまるので対象だと思っています。)

環境変数NODE_ENVの値を元にリクエスト先に環境を分けるようなコードを書くと、こうなってしまう場合があります。

const getTodo = async (id: number) => {
  const env = process.env.NODE_ENV

  let apiUrl: string

  switch (env) {
    case "DEV":
      apiUrl = `http://dev.example.com/todos/${id}`
    case "STG":
      apiUrl = `https://stg.example.com/todos/${id}`
    case "PRD":
      apiUrl = `https://example.com/todos/${id}`
    default:
      throw new Error(`unknown env name ${env}`) 
  }

  return await fetch(apiUrl).then(r => r.json())
}

このコードの問題点は環境が追加された時、つまりenvsに入る値がDEV' | 'STG' | 'PRD以外に増えた際に例外を送出してしまいます。 増えたcaseを追加すれば解決しますが、条件分岐はリポジトリ内でコピー・アンド・ペーストで使い回され、同じ条件分岐が様々な箇所で発生しがちです。環境を追加する際にリポジトリ全体を文字列検索して、1つずつ直していくのは負担が大きく漏れも発生しがちです。

今回の場合は例えばこういった風に書くことで改善できます。

const getTodo = async (id: number) => {
  const apiUrl = `${process.env.API_URL}/todos/${id}`

  return await fetch(apiUrl).then(r => r.json())
}

process.env.API_URLにはdevの場合はhttp://dev.example.comといった感じで環境ごとに適切な値を格納しておきます。

ただし、これはあくまでサンプルとして解決しやすい例で、実際の開発では条件分岐から逃れられない場合も多々あると思います。その場合は可能な限り条件分岐を1つに集約することで変更のコストを最小限にします。

あとがき

最近のAmplify Frameworkの発展は眼を見張るものがありますね。私はAmplify CLIを使うことは今まであまり無かったですが機会があれば積極的に使って行きたいなと思います。