Amazon Web Services ブログ

CDK アプリケーションの複雑さを軽減する L2 Construct の活用

AWS Cloud Development Kit (AWS CDK) は、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースを定義するためのオープンソースのソフトウェア開発フレームワークです。 AWS CDK は、プログラミング言語の使い慣れた表現力を利用してアプリケーションをモデル化します。 Construct は AWS CDK アプリケーションの基本的な構成要素です。 Construct は「クラウドコンポーネント」を表し、AWS CloudFormation がコンポーネントを作成するのに必要なすべてのものをカプセル化します。さらに、AWS Construct Library では、事前定義されたテンプレートとロジックを使用してアプリケーションを簡単に構築できます。 Construct には次の 3 つのレベルがあります。

  • L1 — これらは Cfn (CloudFormation の略) リソースと呼ばれる低レベルの Construct です。これらは AWS CloudFormation リソース仕様から定期的に生成されます。名前パターンは CfnXyz で、 Xyz はリソースの名前です。これらの Construct を使用するときは、すべてのリソースプロパティを設定する必要があります。そのためには、基礎となる CloudFormation リソースモデルとそれに対応する属性を十分に理解している必要があります。
  • L2 — これらは、より高いレベルの使用用途に応じた便利な API を備えた AWS リソースを表します。デフォルトの設定、定型コード、コンポーネント間を簡単に結合するロジックなど、 L1 Construct を使って自分で開発するような追加機能を提供します。 AWS Construct には便利なデフォルト設定が用意されており、それが表す AWS リソースの詳細をすべて知る必要がなくなります。リソースの操作をよりシンプルにするための便利なメソッドが提供され、結果的にアプリケーションを作成することが簡単になります
  • L3 — これらの Construct はパターンと呼ばれます。 AWS の一般的なタスクを完了するように設計されており、多くの場合、複数の種類のリソースが関係します。

このブログでは、サンプルアーキテクチャと、 L2 Construct を使用して AWS CDK アプリケーションの複雑さを軽減する方法を示します。

サンプルアーキテクチャの概要

このソリューションでは、Amazon API GatewayAWS Lambda、および Amazon DynamoDB を使用して、シンプルなサーバーレスウェブアプリケーションを実装しています。アプリケーションは API Gateway 経由でユーザーから POST リクエストを受け取り、プロキシ統合を使用して Lambda 関数に転送します。 Lambda 関数はリクエスト本文を DynamoDB テーブルに書き込みます。

The sample code can be found on GitHub.

サンプルコードは GitHub にあります。

ウォークスルー

GitHub リポジトリの README ファイルの指示に従ってスタックをデプロイできます。次のチュートリアルでは、それぞれの論理構成と、 L1 と L2 の Construct を使用して実装する場合の違いについて説明します。各サンプルコードの前に、ソースがある GitHub リポジトリ内のパスを示します。

DynamoDB テーブルの作成

まず、リクエストの内容を保存する DynamoDB テーブルを作成します。

L1 Construct

L1 Construct では、テーブルの各属性を個別に定義する必要があります。 DynamoDB テーブルの場合、これらはkeySchemaattributeDefinitions、およびprovisionedThroughputです。これらはすべて、たとえばkeyTypeの定義方法など、CloudFormation に関する詳細な知識を必要とします。

lib/level1/database/infrastructure.ts

dynamodb.CfnTable(
   this, 
   "CfnDynamoDbTable", 
   {
      keySchema: [
         {
            attributeName: props.attributeName,
            keyType: "HASH",
         },
      ],
      attributeDefinitions: [
         {
            attributeName: props.attributeName,
            attributeType: "S",
         },
      ],
      provisionedThroughput: {
         readCapacityUnits: 5,
         writeCapacityUnits: 5,
      },
   },

L2 Construct

対応する L2 Construct では、 readCapacity (5) と WriteCapacity (5) のデフォルト値を使用できます。さらに複雑さを軽減するために、属性とパーティションキーを同時に定義します。さらに、 dynamodb.AttributeType.STRING 列挙型を利用しています。

lib/level2/database/infrastructure.ts

this.dynamoDbTable = new dynamodb.Table(
   this, 
   "DynamoDbTable", 
   {
      partitionKey: {
         name: props.attributeName,
         type: dynamodb.AttributeType.STRING,
      },
   },
);

Lambda 関数の作成

次に、リクエストを受け取って DynamoDB テーブルにコンテンツを保存する Lambda 関数を作成します。ランタイムは Node.js を使用します。

L1 Construct

L1 Construct を使用して Lambda 関数を作成する場合、作成時にすべてのプロパティ (ビジネスロジックのコードの場所、ランタイム、関数ハンドラー) を指定する必要があります。これには Lambda 関数が引き受ける IAM ロールも含まれます。そのため、 IAM ロールのリソースネーム (ARN) を指定する必要があります。この記事の後半の「権限の付与」セクションでは、この IAM ロールの作成方法を示します。

lib/level1/api/infrastructure.ts

const cfnLambdaFunction = new lambda.CfnFunction(
   this, 
   "CfnLambdaFunction", 
   {
      code: {
         zipFile: fs.readFileSync(
            path.resolve(__dirname, "runtime/index.js"),
            "utf8"
         ),
      },
      role: this.cfnIamLambdaRole.attrArn,
      runtime: "nodejs16.x",
      handler: "index.handler",
      environment: {
         variables: {
            TABLE_NAME: props.dynamoDbTableArn,
         },
      },
   },
);

L2Construct

Lambda 関数の NodeJSFunction L2 Construct を利用することで、同じ結果をより簡単に得ることができます。別のバージョンを明示的に指定しない限り、 Node.js ランタイムのデフォルトバージョンが設定されます。この Construct は、 TypeScript または JavaScript コードを自動的にトランスパイルしてバンドルする Lambda 関数を作成します。その結果、関数の実行に必要なコードと依存関係のみを含む Lambda パッケージが小さくなり、内部では esbuild が使用されます。 Lambda 関数ハンドラーコードはリポジトリの runtimeディレクトリにあります。 Lambda ハンドラーファイルへのパスは entry プロパティに指定しています。NodeJSFunction Construct はデフォルトでハンドラー名を使用するため、ハンドラー関数名を指定する必要はありません。さらに、 L2 Lambda Construct の作成時に Lambda 実行ロールを指定する必要はありません。ロールが指定されていない場合は、 Lambda の実行権限を持つデフォルトの IAM ロールが生成されます。「権限の付与」セクションでは、 Construct の作成後に IAM ロールをカスタマイズする方法について説明します。

lib/level2/api/infrastructure.ts

this.lambdaFunction = new lambda_nodejs.NodejsFunction(
   this, 
   "LambdaFunction", 
   {
      entry: path.resolve(__dirname, "runtime/index.ts"),
      runtime: lambda.Runtime.NODEJS_16_X,
      environment: {
         TABLE_NAME: props.dynamoDbTableName,
      },
   },
);

API Gateway REST API の作成

次に、クロスオリジンリソース共有 (CORS) を有効にして POST リクエストを受信するように API Gateway REST API を定義します。

L1 Construct

新しい API Gateway REST API の作成からデプロイプロセスまで、すべてのステップを個別に設定する必要があります。 L1 Construct では、 CORS とヘッダーとメソッドの正確な設定を十分に理解している必要があります。

さらに、 Lambda プロキシ統合タイプでは URI の構築方法を知っておく必要があるなど、すべての詳細を知っておく必要があります。

lib/level1/api/infrastructure.ts

const cfnApiGatewayRestApi = new apigateway.CfnRestApi(
   this, 
   "CfnApiGatewayRestApi", 
   {
      name: props.apiName,
   },
);

const cfnApiGatewayPostMethod = new apigateway.CfnMethod(
   this, 
   "CfnApiGatewayPostMethod", 
   {
      httpMethod: "POST",
      resourceId: cfnApiGatewayRestApi.attrRootResourceId,
      restApiId: cfnApiGatewayRestApi.ref,
      authorizationType: "NONE",
      integration: {
         credentials: cfnIamApiGatewayRole.attrArn,
         type: "AWS_PROXY",
         integrationHttpMethod: "ANY",
         uri:
            "arn:aws:apigateway:" +
            Stack.of(this).region +
            ":lambda:path/2015-03-31/functions/" +
            cfnLambdaFunction.attrArn +
            "/invocations",
            passthroughBehavior: "WHEN_NO_MATCH",
      },
   },
);

const CfnApiGatewayOptionsMethod = new apigateway.CfnMethod(
    this,
    "CfnApiGatewayOptionsMethod",
   {    
      // fields omitted
   },
);

const cfnApiGatewayDeployment = new apigateway.CfnDeployment(
    this,
    "cfnApiGatewayDeployment",
    {
      restApiId: cfnApiGatewayRestApi.ref,
      stageName: "prod",
    },
);

L2 Construct

CORS を有効にして API Gateway REST API を作成するのは、 L2 Construct を使用するとより簡単になります。defaultCorspreflightOptions プロパティを利用すると、この Construct によって必要なOPTIONSメソッドが準備されます。オリジンとメソッドを設定するには、apigateway.Cors 列挙型を使用できます。 Lambda プロキシオプションを設定するには、メソッドのプロキシ変数を true に設定するだけです。デフォルトのデプロイは自動的に作成されます。

lib/level2/api/infrastructure.ts

this.api = new apigateway.RestApi(
   this, 
   "ApiGatewayRestApi", 
   {
      defaultCorsPreflightOptions: {
         allowOrigins: apigateway.Cors.ALL_ORIGINS,
         allowMethods: apigateway.Cors.ALL_METHODS,
      },
   },
);

this.api.root.addMethod(
    "POST",
    new apigateway.LambdaIntegration(this.lambdaFunction, {
      proxy: true,
    })
);

権限の付与

サンプルアプリケーションでは、次の 2 つの異なるリソースに権限を付与する必要があります。

  1. Lambda 関数を呼び出す API Gateway REST API
  2. DynamoDB テーブルにデータを書き込む Lambda 関数

L1 Construct

どちらのリソースでも、AWS Identity and Access Management (IAM) ロールを定義する必要があります。これには IAM 、ポリシーの構造、必要なアクションに関する深い知識が必要です。次のコードスニペットでは、まずポリシードキュメントを作成します。その後、リソースごとに IAM ロールを作成します。これらは作成時に、前述のように対応する Construct に渡されます。

lib/level1/api/infrastructure.ts

const cfnLambdaAssumeIamPolicyDocument = {
    // fields omitted
};

this.cfnLambdaIamRole = new iam.CfnRole(
   this, 
   "cfnLambdaIamRole", 
   {
      assumeRolePolicyDocument: cfnLambdaAssumeIamPolicyDocument,
      managedPolicyArns: [
        "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
      ],
   },
);
    
const cfnApiGatewayAssumeIamPolicyDocument = {
   // fields omitted
};

const cfnApiGatewayInvokeLambdaIamPolicyDocument = {
   Version: "2012-10-17",
   Statement: [
      {
         Action: ["lambda:InvokeFunction"],
         Resource: [cfnLambdaFunction.attrArn],
         Effect: "Allow",
      },
   ],
};

const cfnApiGatewayIamRole = new iam.CfnRole(
   this, 
   "cfnApiGatewayIamRole", 
   {
      assumeRolePolicyDocument: cfnApiGatewayAssumeIamPolicyDocument,
      policies: [{
         policyDocument: cfnApiGatewayInvokeLambdaIamPolicyDocument,
         policyName: "ApiGatewayInvokeLambdaIamPolicy",
      }],
   },
);

独自に定義されたDatabaseConstruct は、任意の IAM ロールに書き込みアクセスを許可する関数を公開します。この関数はポリシーを作成し、データベースのテーブルで dynamodb:PutItemを許可し、それを IAM ロールに追加ポリシーとして追加します。

lib/level1/database/infrastructure.ts

grantWriteData(cfnIamRole: iam.CfnRole) {
   const cfnPutDynamoDbIamPolicyDocument = {
      Version: "2012-10-17",
      Statement: [
         {
            Action: ["dynamodb:PutItem"],
            Resource: [this.cfnDynamoDbTable.attrArn],
            Effect: "Allow",
         },
      ],
   };

    cfnIamRole.policies = [{
        policyDocument: cfnPutDynamoDbIamPolicyDocument,
        policyName: "PutDynamoDbIamPolicy",
    }];
}

この時点で、 Lambda 関数には DynamoDB テーブルにデータを書き込む権限がまだないことを除いて、すべての権限が設定されています。書き込みアクセスを許可するには、 Lambda 関数の IAM ロールを使用してDatabaseConstruct の grantWriteData 関数を呼び出します。

lib/deployment.ts

database.grantWriteData(api.cfnLambdaIamRole)

L2 Construct

LambdaIntegration Construct を使用して API Gateway REST API を作成すると、 IAM ロールが生成され、そのロールが API Gateway REST API メソッドにアタッチされます。 Lambda 関数に DynamoDB テーブルへの書き込み権限を与えるには、次の 1 行を入力します。

lib/deployment.ts

database.dynamoDbTable.grantWriteData(api.lambdaFunction);

L3 Construct を使用する

さらに複雑さを軽減するには、 L3 Construct を活用できます。このサンプルアーキテクチャの場合、LambdaRestAPI Construct を使用できます。この Construct はデフォルトの Lambda プロキシ統合を使用します。メソッドとデプロイを自動的に生成し、権限を付与します。その結果、さらに少ないコードで同じことを実現できます。

const restApi = new apigateway.LambdaRestApi(
   this, 
   "restApiLevel3", 
   {
      handler: this.lambdaFunction,
      defaultCorsPreflightOptions: {
         allowOrigins: apigateway.Cors.ALL_ORIGINS,
         allowMethods: apigateway.Cors.ALL_METHODS
      },
   },
);

クリーンアップ

この記事で紹介するサービスの多くは AWS 無料利用枠で利用できます。ただし、このソリューションを使用するとコストが発生する可能性があるため、不要になった場合はスタックを削除する必要があります。クリーンアップ手順は GitHub リポジトリの README ファイルに含まれています。

結論

この記事では、 L1 と L2 の AWS CDK Construct を使用する場合の違いを、アーキテクチャ例とともに紹介しました。 L2 Construct を活用すると、定義済みのパターン、定型コード、コンポーネント間を簡単に結合するロジックを使用できるため、アプリケーションの複雑さが軽減されます。便利なデフォルト設定が用意されており、これらが表す AWS リソースの詳細をすべて知る必要がなくなると同時に、リソースをより簡単に操作できる便利なメソッドも用意されています。さらに、 L3 Construct を使用して一般的なタスクの複雑さをさらに軽減する方法も示しました。

AWS CDK のドキュメントを参照して、プログラミング言語の表現力を駆使して、耐障害性、スケーラビリティ、コスト効率の高いアーキテクチャを構築する方法の詳細をご覧ください。

本記事は、 David Boldt による “Leverage L2 constructs to reduce the complexity of your AWS CDK application” を翻訳したものです。翻訳はソリューションアーキテクトの平川 大樹が担当しました。