📀

CDKv2でLambda+API Gateway作成(テストまで)

2022/01/12に公開

この記事について

実務でCDKを利用してLambdaを作成する機会がありました。
その時試行錯誤して作成したものを主に自分のためにわかりやすく記事にして残しておきます。
今回の記事ではCDK(TypeScript)を使用して、API GatewayとLambdaを作成して動作させるところまで行いたいと思います。
tsconfigやpackage.jsonなどの説明は省略します。

この記事では下記の順番で説明していきます。

  1. CDKの準備
  2. Lambdaの処理内容を作成
  3. CDKを使用してLambdaを作成
  4. CDKを使用してAPIGatewayを作成
  5. Lambdaが作成されていることを確認するテストを2種類作成

CDKは頻繁に更新があるのでエラーや警告がでたら公式ドキュメントを読むことをお勧めします。

1. CDKの準備

CDKとは

AWS Cloud Development Kitのことです。
CloudFormationの上位存在的なやつで、慣れ親しんだ言語で記述が可能です。インフラのテストもTypescriptなどで記述することができるので便利です。
CDKの挙動のイメージは、CloudFormationのテンプレートを生成して、それを使用して実際に作成するような感じです。
JavaScript、TypeScript、Python、Java、C# が一般公開されています。version2ではGolangも使用できるみたいです。
参考: https://aws.amazon.com/jp/cdk/

このCDKはとても便利なのですが、個人的に欠点が2つあると思っています。
1つ目はバージョンアップがとんでもなく早いという点です。
1、2週間に1回はマイナーバージョンが1つ上がります。業務で半年ほど開発していたのですが、25回のマイナーバージョンアップを経験しました。とんでもなく早いスピードで更新されるので、結局CDKの公式ドキュメントを参考にするのが一番安全です。
テストを記入しておくとバージョンアップ時に少しだけ安心できます。

2つ目は(現時点では)外部変更後の修正がとんでもなく難しいという点です。
CDKで作成したものにAWSコンソールから修正を加えると、CDKコマンドがエラーになってしまいます。Terraformなどであればimportコマンド等で差分を簡単にキャッチ出来るのですが、CDKはそうはいきません。これを解決するにはCDKが作成するテンプレートを書き換えるか、一致するように戻すか、一度切り離す必要があるようです。
参考: AWS CDK/CloudFormation、リソースを変更せずにスタックドリフトを解消する

上記2つの理由から、CDKを大きいサービスで使用するのは正直お勧めできません。逆に小さなサービスなら、少ないコードでインフラがあっという間に出来上がるのでめちゃくちゃお勧めです。

この記事では、CDKのバージョンは2.5.0で作成していきます。

CDKのインストール

下記の記事を参考にインストールしてください。
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html#getting_started_install

npm install -g aws-cdk
cdk --version

AWS CLIのインストール

CDKはAWS CLI(AWS Command Line Interface)の設定ファイルと認証情報ファイルを参照して、AWSにアクセスします。そのためAWS CLIのインストールも必要です。

公式のインストール方法を参考にインストールしてください。

インストールできたらAWS CLIの設定を行います。
profileオプションでプロファイルに名前をつけることをお勧めします。
profileを設定すると、CDKデプロイ時にもprofileを指定する必要があるので事故が減らせます。profileがない場合にエラーが発生して失敗するようになるので、うっかり本番環境書き換えちゃったみたいなミスが減らせます。

aws configure --profile xxxxx

CDKで利用する、AWSユーザーとリージョンを登録して準備完了です。
(IAMユーザーは事前に作成しておいてください)

profileがどんなものかわかりやすく知りたい方は、こちらの記事が参考になります。

CDKアプリ作成

空のディレクトリを作成し、CDKのinitで新しく作成します。

mkdir lambda_with_cdk && cd lambda_with_cdk
cdk init app --language=typescript

cdk initapplanguageは、cdk initだけで実行すると説明が出てきます。

cdk init

Available templates:
* app: Template for a CDK Application
   └─ cdk init app --language=[csharp|fsharp|go|java|javascript|python|typescript]
* lib: Template for a CDK Construct Library
   └─ cdk init lib --language=typescript
* sample-app: Example CDK Application with some constructs
   └─ cdk init sample-app --language=[csharp|fsharp|go|java|javascript|python|typescript]

2. Lambdaの処理内容を作成

早速CDKでLambdaを作成したいところですが、処理内容を自分で作った方が作成した気になるのでLambdaのコードを作成していきます。lambda/get/index.tsというファイルを作成して下記を貼り付けます。

lambda/get/index.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {

  const params = event.queryStringParameters ? event.queryStringParameters : {};

  const RESPONSE_HEADERS = {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,access-token",
  };

  return {
    statusCode: 200,
    headers: RESPONSE_HEADERS,
    body: params.message ? params.message : "空です",
  };
};

リクエストにmessageというクエリがあればその内容を返し、なければ「空です」と返すだけの処理を作成しました。この関数をLambdaとして作成していきます。

3. CDKを使用してLambdaを作成

Lambda 作成用のコード追加

いよいよCDKを使用してLambdaを作成していきます。
まずは、lib/lambda_with_cdk-stack.ts を以下のように書き換えます。

lib/lambda_with_cdk-stack.ts
import { Duration, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { aws_iam as iam } from "aws-cdk-lib";
import { Runtime } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";

export interface CustomizedProps extends StackProps {
  projectName: string;
}

export class LambdaWithCdkStack extends Stack {
  constructor(scope: Construct, id: string, props: CustomizedProps) {
    super(scope, id, props);

    // iam role
    const iamRoleForLambda = new iam.Role(this, "iamRoleForLambda", {
      roleName: `${props.projectName}-lambda-role`,
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      // VPCに設置する場合下記が必要
      // managedPolicies: [
      //   iam.ManagedPolicy.fromAwsManagedPolicyName(
      //     "service-role/AWSLambdaVPCAccessExecutionRole"
      //   ),
      // ],
    });

    // lambda
    const sampleLambda = new NodejsFunction(this, "GETsample", {
      entry: "lambda/sample/index.ts", // どのコードを使用するか
      runtime: Runtime.NODEJS_14_X, // どのバージョンか
      timeout: Duration.seconds(30), // 何秒でタイムアウトするか
      role: iamRoleForLambda, // どのIAMロールを使用するか
      // vpc: vpc, // VPCに設置する場合に必要
      environment: {
        AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", // keepaliveを有効にする
      },
      memorySize: 128, // default=128
    });
  }
}

解説をしていきます。
CustomizedPropsLambdaWithCdkStack の引数にプロジェクト名(projectName)を渡すために作成しています。今回の例だと必要性があまりないですが、作成するものやStackが増えてくると便利になるのでこういうことも出来るという一例として作ってみています。

iamRoleForLambdaはLambda用のIAMロールです。明示的に記入しなくても自動で作成されるようですが、S3等にアクセスする権限を加えていく想定で作成しています。

sampleLambdaについてはコメントに大まかな説明を記入しています。もっと詳しくみたい方は公式の説明を確認してください!

Stackを修正

CustomizedPropsに対応させるため、bin/lambda_with_cdk.tsを下記のように修正します。CustomizedPropsを作成しない場合はそのままで大丈夫です。

bin/lambda_with_cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { LambdaWithCdkStack } from '../lib/lambda_with_cdk-stack';

const app = new cdk.App();
const projectName: string = "sample-lambda";

new LambdaWithCdkStack(app, "LambdaWithCdkStack", {
  projectName: projectName,
});

ここは基本的にStackを書く場所で、先ほど記述したLambdaWithCdkStackのインスタンスを作成しています。ここのStackを増やすと複数Stackを作成することが可能です。複数作成した場合は、

cdk diff hoge-stack-name --profile fuga-profile

のようにStackを指定することでコマンドの実行を行います。

CDKを使用してデプロイ

まずはCDKを使用するための準備を行います。
bootstrapコマンドでデプロイに必要なS3を作成します。

cdk bootstrap --profile fuga-profile

AWSのコンソールから「cdktoolkit-stagingbucket-xxx」という名称のS3が作成されていることが確認できると思います。
次にdiffコマンドで差分を確認してみましょう。

cdk diff --profile fuga-profile

最後にdeployコマンドでデプロイします。

cdk deploy --profile fuga-profile

デプロイが完了したらコンソールからLambdaを確認してみましょう。うまく作成できていれば、LambdaWithCdkStack-GETsamplexxxというLambdaが作成できているはずです。

テストを動かして予想通りの挙動か確認してみます。
LambdaWithCdkStack-GETsamplexxxを選択して「テスト」のタブを選択します。そこで下記をペーストしてテストを実行してみてください。

{
  "queryStringParameters": { "message": "hogehoge" }
}

実行結果を確認すると以下のようになっていると思います。

想定通りに動作することを確認できました。
次はこのLambdaを外部から叩けるように、API Gatewayと連携します。

4. CDKを使用してAPIGatewayを作成

次にAPI Gatewayを作成していきます。
3で作成したbin/lambda_with_cdk.tsに下記のコードを追加します。

bin/lambda_with_cdk.ts
//~~~~~
// 省略
//~~~~~
// aws_apigatewayを追加
import { Duration, Stack, StackProps, aws_apigateway } from "aws-cdk-lib";
//~~~~~
// 省略
//~~~~~

    // api gateway
    const sampleApi = new aws_apigateway.RestApi(this, "sampleApigateway", {
      restApiName: `${props.projectName}-apigateway`,
      deployOptions: {
        loggingLevel: aws_apigateway.MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        metricsEnabled: true,
      },
    });

    // api key
    const apiKey = sampleApi.addApiKey("sampleApiKey", {
      apiKeyName: `${props.projectName}-api-key`,
    }); // APIキーの値は未指定で自動作成

    // 使用量プランの作成
    const usagePlan = sampleApi.addUsagePlan("sampleApiUsagePlan");
    usagePlan.addApiKey(apiKey);
    usagePlan.addApiStage({ stage: sampleApi.deploymentStage });

    // GET/sample を作成
    const sample = sampleApi.root.addResource("sample");
    const courseSearchIntegration = new aws_apigateway.LambdaIntegration(
      sampleLambda
    );
    sample.addMethod("GET", courseSearchIntegration);

    // GET/messages/2 などを作成したい場合
    const messages = sampleApi.root.addResource("messages");
    const messageId = messages.addResource("{message_id}");
    messageId.addMethod("GET", courseSearchIntegration, {
      apiKeyRequired: true,
    });

//~~~~~
// 省略
//~~~~~

API Gatewayについて解説します。
restApiNameで指定した名前をつけています。
loggingLevelCloudWatchログに吐き出すログレベルを選択します。デフォルトがOFFになっているので、INFOを選択しています。
dataTraceEnabledAPI Gateway自体のログを有効にします。どんなことができるかは下記の記事が参考になります。このログが意外と便利なのでtrueにするのがお勧めです。
参考:[AWS CDK] API Gatewayのログ出力を有効にしてCloudWatch Logsでログを確認してみた
metricsEnabledはCloudWatchメトリクスを有効にするかどうかを決定します。今回は省略しましたが、アラームを設定するとエラーをすぐに知ることができます。
参考: Amazon CloudWatch メトリクスを使用する

apiKeyAPI Gatewayで使用するAPIキーを作成しています。

usagePlanapiKeyapigatewayの紐付けをおこなっています。

上記の内容をさらに詳しく確認したい方は公式のドキュメントをご覧ください。

今回は、GET/sample(APIキーの制限なし)と GET/messages/{message_id}(APIキーの制限あり)をAPI Gatewayに作成してみました。どちらも同じLambdaで処理を行うので、実際の挙動は変わりません。

これでAPI Gatewayの作成準備ができました。早速差分を確認してみましょう。

cdk diff --profile fuga-profile

想像通りのものが作成されることを確認したら、デプロイします。(IAMなど必要なものを一部勝手に作成してくれるので注意)

cdk deploy --profile fuga-profile

デプロイが完了したら、実際に動作するか確認してみましょう。
まずは作成されたAPI Gatewayをコンソールから見にいきましょう。添付画像のように作成できていることが確認できると思います。

作成したAPI Gatewayをクリックして、ステージを選択します。
prodというステージが出てくるので、それをクリックすると

 URL の呼び出し: https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod

のような記述を発見できると思います。これが現時点での外部から接続できるURLです。

次はAPIキーを確認してみましょう。
APIキーというタブをクリックすると作成したAPIキーが出てきます。
そこの「API キー 表示」をクリックすると自動で作成されたAPIキーの値が確認できます。

実際にcurlコマンドで動作確認を行ってみましょう。

GET/sampleを試す
curl --location -X GET https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/sample 

空です


curl --location -X GET "https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/sample?message=HelloWorld"

HelloWorld
GET/messages/{message_id}を試す
curl --location -X GET https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/messages/1

{"message":"Forbidden"}


curl -H "x-api-key: xxx" --location -X GET https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/messages/1

空です

これで動作できていることを確認できました。

5. Lambdaが作成されていることを確認するテストを2種類作成

作成したいだけであればここは読み飛ばしていただいて問題ありません。
ここでは、CDKを作成した際に書いておいたら少し安心できるテスト達を紹介ししていきます。

Snapshot テスト

まずはSnapshotテストです。
作成されるテンプレートをSnapshotとして保管しておき、差分が発生するとエラーになるというテストです。一回目は確定成功です。変更点が見つけやすのでバージョンアップ時に少し安心できます。
以下のテストを作成して、yarn testを実行してみてください。

test/snapshot.test.ts
import * as cdk from "aws-cdk-lib";
import { LambdaWithCdkStack } from "../lib/lambda_with_cdk-stack";
import { Template } from "aws-cdk-lib/assertions";

test("snapshot test", () => {
  const app = new cdk.App();

  // cdk.jsonをテスト時に使用したい場合
  // const app = new cdk.App({ context: { hoge: "fuga" } });

  const stack = new LambdaWithCdkStack(app, "snapshotTestStack", {
    projectName: "snapshot-test",
  });

  // テンプレートをJSONに変換
  const template = Template.fromStack(stack).toJSON();

  // Lambdaコードの変更によるsnapshotテスト失敗を防ぐ
  template.Parameters = {};
  Object.values(template.Resources).forEach((resource: any) => {
    // Codeを持つもののCodeを{}に上書き
    if (resource?.Properties?.Code) {
      resource.Properties.Code = {};
    }
  });

  expect(template).toMatchSnapshot();
});

やっていることはシンプルです。テンプレートをJSONに変換してSnapshotと比較しているだけです。

途中の「Lambdaコードの変更によるsnapshotテスト失敗を防ぐ」という箇所は、AWS CDKのスナップショットテストでアセットを無視する方法を参考にしています。雑に解説すると、Lambdaの内容が1文字でも変更されていると差分が出てしまうので、Lambdaの中身が変更されても差分が出ないように上書きしています。上書きしている箇所をコメントアウトしてyarn testしてみると意味がわかりやすいかもしれません。

Snapshotテストは更新したくなったらyarn test -uで更新することが可能です。

Fine-grained assertions テスト

期待したインフラが実際に作成されるかどうかを確認するためのテストです。
深く記述すると無限に書けてしまうので、Lambdaの個数などのどうしても確認しておきたい部分をテストするといいと思います。

今回は下記の2つを確認してみます。

  • LambdaとAPI Gatewayがきちんと想定数作成されるか
  • API Gatewayのリソースが想定通り作成されるか
test/fine_grained_assertions.test.ts
import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import { LambdaWithCdkStack } from "../lib/lambda_with_cdk-stack";

test("fine grained assertions test", () => {
  const app = new cdk.App();
  const stack = new LambdaWithCdkStack(app, "fineGrainedAssertionsTestStack", {
    projectName: "fine-grained-assertions-test",
  });

  const template = Template.fromStack(stack);

  // Lambdaが1つ作成されていること
  template.resourceCountIs("AWS::Lambda::Function", 1);

  // API Gatewayが1つ作成されていること
  template.resourceCountIs("AWS::ApiGateway::RestApi", 1);

  // API GatewayのMethodが2つ作成されていること
  template.resourceCountIs("AWS::ApiGateway::Method", 2);

  // sampleというResourceが作成されていること
  template.hasResourceProperties("AWS::ApiGateway::Resource", {
    PathPart: "sample",
  });
});

yarn testを実行すると成功することが確認できると思います。
数値を変えて失敗することも確認してみてください。

以上でテストは完成です。

まとめ

今回使用したコードはGitHubにあげておきます。
作成したコードをつかってデプロイコマンドを実行するだけで簡単にAWSに動くものが出来上がるので、やはりCDKは便利です。ただ少し使いづらいところはありますので、実際に使用するかはよく考えるべきですね。
バージョン2になっても恐ろしい勢いで開発が進んでいるので、この記事が新鮮なうちに試すのをお勧めします!

最後に、今回作成したものはdestroyコマンドで削除できます。(bootstrapで作成されたS3以外)
不要だと思うので削除しておくことをお勧めします。

cdk destroy --profile fuga-profile

ここまでお読みいただき、ありがとうございました。

参考として使用したリンク一覧

Discussion