🔧

Amplify環境でAWSアカウント毎にLambdaレイヤーを切り替える方法

に公開

はじめに

AWS Amplify を利用して開発環境と本番環境で AWS アカウントを分けて運用している場合、Lambda レイヤーの管理で問題が発生することがあります。特に、Amplify の管理外で手動でアップロードしている Lambda レイヤーがある場合に顕著です。

この記事では、そのような状況で発生する問題と、CDK のカスタムリソースと AWS SDK を利用して解決する方法について解説します。

発生する問題

画像変換などで Sharp ライブラリを使用するために、Lambda レイヤーを利用しているケースを考えます。この Lambda レイヤーが Amplify で管理されておらず、ZIP ファイルを手動でアップロードして各 AWS アカウントに設定している場合、以下のような問題が発生する可能性があります。

  • AWS アカウントごとに Lambda レイヤーのバージョンが異なる: 開発環境と本番環境で、同じ Lambda レイヤーでも異なるバージョンが登録されている可能性があります。
  • Amplify デプロイ時のエラー: Amplify で Lambda 関数に Lambda レイヤーを設定する際、通常はレイヤー名とバージョンを指定します。しかし、デプロイ対象の AWS アカウントに指定したバージョンの Lambda レイヤーが存在しない場合、デプロイ時にエラーが発生してしまいます。

この問題を回避するためには、デプロイ先の AWS アカウントに応じて、適用する Lambda レイヤーの ARN (Amazon Resource Name) を動的に切り替える必要があります。

解決策: CDKカスタムリソースとAWS SDKの活用

この問題は、Amplify が内部で使用している AWS CDK (Cloud Development Kit) の機能と AWS SDK を組み合わせることで解決できます。具体的には、amplify/backend.ts ファイル内で現在の AWS アカウント ID を取得し、その ID に基づいて Lambda レイヤーの ARN を条件分岐で設定します。

1. AWS アカウント ID の取得

まず、amplify/backend.ts で AWS SDK (v2) を使用して、現在の AWS アカウント ID を取得します。

amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
// 他のインポート...
import AWS from 'aws-sdk'; // AWS SDK v2 をインポート

// Backend 定義など...
const backend = defineBackend({
  // function や auth などの定義
});

// AWS SDK を使ってアカウント ID を取得
const sts = new AWS.STS();
const accountId = (await sts.getCallerIdentity({}).promise()).Account;

console.log(`Current AWS Account ID: ${accountId}`); // デプロイ時にアカウントIDを確認

注意: このコードは amplify/backend.ts のトップレベルで実行されるため、await を使用できます。また、aws-sdk (v2) がプロジェクトにインストールされている必要があります。

2. Lambda レイヤーの動的設定

次に、取得した accountId を使用して、特定の Lambda 関数に適用するレイヤーを条件分岐で設定します。ここでは例として amplify/functions/query/test/resource.ts で定義された test 関数にレイヤーを設定します。

amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { test } from './functions/query/test/resource'; // 対象のLambda関数をインポート
import AWS from 'aws-sdk';

const backend = defineBackend({
  test, // バックエンド定義に関数を追加
  // 他のリソース定義...
});

const sts = new AWS.STS();
const accountId = (await sts.getCallerIdentity({}).promise()).Account;

// Lambda関数の L1 Construct (CfnFunction) を取得
const cfnFunction = backend.test.resources.cfnResources.cfnFunction;

// アカウントIDに基づいてLambdaレイヤーのARNを決定
let sharpLayerArn: string;
if (accountId === 'ACCOUNT_ID_FOR_PRODUCTION') { // 本番環境のアカウントIDに置き換えてください
  sharpLayerArn = 'arn:aws:lambda:ap-northeast-1:ACCOUNT_ID_FOR_PRODUCTION:layer:sharp:PROD_VERSION'; // 本番用レイヤーARN
} else if (accountId === 'ACCOUNT_ID_FOR_DEVELOPMENT') { // 開発環境のアカウントIDに置き換えてください
  sharpLayerArn = 'arn:aws:lambda:ap-northeast-1:ACCOUNT_ID_FOR_DEVELOPMENT:layer:sharp:DEV_VERSION'; // 開発用レイヤーARN
} else {
  // 想定外のアカウントIDの場合のエラーハンドリングやデフォルト設定
  console.warn(`Unknown Account ID: ${accountId}. No specific layer applied.`);
  // 必要であればデフォルトのレイヤーARNを設定するか、エラーを発生させる
  // sharpLayerArn = 'DEFAULT_LAYER_ARN';
}

// 決定したレイヤーARNをLambda関数に設定
if (sharpLayerArn) {
  cfnFunction.layers = [sharpLayerArn];
  console.log(`Applied Lambda layer ${sharpLayerArn} to function ${backend.test.resources.lambda.functionName}`);
}

上記の例では、ACCOUNT_ID_FOR_PRODUCTIONACCOUNT_ID_FOR_DEVELOPMENT、および対応する Lambda レイヤーの ARN (PROD_VERSION, DEV_VERSION を含む) を実際の値に置き換える必要があります。

backend.<function_name>.resources.cfnResources.cfnFunction を使用することで、CDK の L1 Construct である CfnFunction にアクセスでき、その layers プロパティを直接操作できます。

別のアプローチ: defineFunction コールバックと CloudFormation 条件の活用

amplify/backend.ts で AWS SDK を使用する方法以外にも、Amplify Gen2 の defineFunction ユーティリティが提供するコールバック関数と AWS CDK の CloudFormation 機能 (CfnCondition, CfnMapping, Fn.conditionIf) を組み合わせることで、Lambda レイヤーを動的に設定する、より堅牢なアプローチがあります。

この方法では、CloudFormation のデプロイ時に解決される条件ロジックを使用して、現在の AWS アカウント ID に基づいて適用するレイヤーを決定します。

amplify/functions/test-callback/resource.ts
import { defineFunction } from '@aws-amplify/backend'
import { aws_lambda, Aws, Fn, CfnCondition, CfnMapping } from 'aws-cdk-lib'

export const testCallback = defineFunction((scope) => {
  // Lambda 関数を定義
  const fn = new aws_lambda.Function(scope, 'testCallback', {
    runtime: aws_lambda.Runtime.NODEJS_20_X,
    handler: 'index.handler',
    code: aws_lambda.Code.fromAsset('amplify/functions/test-callback'),
    // 注意: ここでは layers プロパティを直接設定しない
  })

  // レイヤーを適用する対象のアカウントIDかを判定する CloudFormation 条件を作成
  const isKnownAccountCondition = new CfnCondition(scope, 'IsKnownAccountCondition', {
    expression: Fn.conditionOr(
      Fn.conditionEquals(Aws.ACCOUNT_ID, 'ACCOUNT_ID_FOR_PRODUCTION'), // 本番環境のアカウントID
      Fn.conditionEquals(Aws.ACCOUNT_ID, 'ACCOUNT_ID_FOR_DEVELOPMENT') // 開発環境のアカウントID
      // ... 必要に応じて他のアカウントIDの条件を追加
    )
  })

  // アカウントIDと適用するレイヤーARNのリストをマッピングする CloudFormation Mapping を作成
  const accountLayerMapping = new CfnMapping(scope, 'AccountLayerMapping', {
    mapping: {
      'ACCOUNT_ID_FOR_PRODUCTION': { layers: ['arn:aws:lambda:ap-northeast-1:ACCOUNT_ID_FOR_PRODUCTION:layer:sharp:PROD_VERSION'] }, // 本番用レイヤーARN
      'ACCOUNT_ID_FOR_DEVELOPMENT': { layers: ['arn:aws:lambda:ap-northeast-1:ACCOUNT_ID_FOR_DEVELOPMENT:layer:sharp:DEV_VERSION'] } // 開発用レイヤーARN
      // ... 他のアカウントIDとレイヤーARNのマッピングを追加
    }
  })

  // Lambda 関数の L1 Construct (CfnFunction) を取得
  const cfnFunction = fn.node.defaultChild as aws_lambda.CfnFunction

  // CloudFormation 条件に基づいて Layers プロパティを上書き
  cfnFunction.addPropertyOverride('Layers', Fn.conditionIf(
    isKnownAccountCondition.logicalId, // もし既知のアカウントIDなら
    accountLayerMapping.findInMap(Aws.ACCOUNT_ID, 'layers'), // Mapping から対応するレイヤーARNリストを取得
    Aws.NO_VALUE // それ以外のアカウントIDならレイヤーを設定しない (空のリストではないことに注意)
  ))

  return fn
})

このコードでは、以下のステップで処理を行っています:

  1. CfnCondition: Fn.conditionOrFn.conditionEquals を使い、現在の AWS アカウント ID (Aws.ACCOUNT_ID) が、レイヤーを適用したい既知のアカウント ID のいずれかと一致するかどうかを判定する CloudFormation 条件 (IsKnownAccountCondition) を定義します。
  2. CfnMapping: 各アカウント ID と、そのアカウントに適用すべき Lambda レイヤーの ARN のリストを対応付ける CloudFormation マッピング (AccountLayerMapping) を定義します。
  3. addPropertyOverride: Lambda 関数の L1 Construct (CfnFunction) を取得し、addPropertyOverride メソッドを使用します。ここで Fn.conditionIf を使い、IsKnownAccountCondition が真 (true) であれば、AccountLayerMapping から現在のアカウント ID に対応するレイヤー ARN のリストを Layers プロパティに設定します。偽 (false) であれば、Aws.NO_VALUE を指定して Layers プロパティ自体を CloudFormation テンプレートから除外します(これにより、レイヤーが適用されません)。

この方法は、TypeScript の実行時ではなく CloudFormation のデプロイ時に条件が評価されるため、より信頼性が高く、CDK のベストプラクティスに沿っています。amplify/backend.ts での非同期処理も不要です。

注意: コード例中の ACCOUNT_ID_FOR_PRODUCTION, ACCOUNT_ID_FOR_DEVELOPMENT および対応するレイヤー ARN (PROD_VERSION, DEV_VERSION などを含む) は、実際の環境に合わせて置き換えてください。

まとめ

Amplify 環境で開発アカウントと本番アカウントを分けて運用している際に発生する Lambda レイヤーのバージョン問題を解決する二つの方法を紹介しました。

  1. amplify/backend.ts と AWS SDK を利用する方法: backend.ts でアカウント ID を取得し、L1 Construct を直接操作してレイヤー ARN を設定します。
  2. defineFunction コールバックと CloudFormation 条件を利用する方法: 各 Lambda 関数の定義内で CfnConditionCfnMapping を使用し、CloudFormation レベルで条件付きでレイヤーを設定します。

どちらの方法でも、Amplify の管理外である Lambda レイヤーを環境ごとに適切なバージョンで安全に適用できるようになります。後者の CloudFormation 条件を利用するアプローチは、より堅牢で CDK ネイティブな記述が可能であり、特に複数の環境を管理する場合に推奨されます。

リバナレテックブログ

Discussion