CDKv2でLambda+API Gateway作成(テストまで)
この記事について
実務でCDKを利用してLambdaを作成する機会がありました。
その時試行錯誤して作成したものを主に自分のためにわかりやすく記事にして残しておきます。
今回の記事ではCDK(TypeScript)を使用して、API GatewayとLambdaを作成して動作させるところまで行いたいと思います。
tsconfigやpackage.jsonなどの説明は省略します。
この記事では下記の順番で説明していきます。
- CDKの準備
- Lambdaの処理内容を作成
- CDKを使用してLambdaを作成
- CDKを使用してAPIGatewayを作成
- 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のインストール
下記の記事を参考にインストールしてください。
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 init
のapp
とlanguage
は、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
というファイルを作成して下記を貼り付けます。
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
を以下のように書き換えます。
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
});
}
}
解説をしていきます。
CustomizedProps
は LambdaWithCdkStack
の引数にプロジェクト名(projectName
)を渡すために作成しています。今回の例だと必要性があまりないですが、作成するものやStackが増えてくると便利になるのでこういうことも出来るという一例として作ってみています。
iamRoleForLambda
はLambda用のIAMロールです。明示的に記入しなくても自動で作成されるようですが、S3等にアクセスする権限を加えていく想定で作成しています。
sampleLambda
についてはコメントに大まかな説明を記入しています。もっと詳しくみたい方は公式の説明を確認してください!
Stackを修正
CustomizedProps
に対応させるため、bin/lambda_with_cdk.ts
を下記のように修正します。CustomizedProps
を作成しない場合はそのままで大丈夫です。
#!/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
に下記のコードを追加します。
//~~~~~
// 省略
//~~~~~
// 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
で指定した名前をつけています。
loggingLevel
でCloudWatch
ログに吐き出すログレベルを選択します。デフォルトがOFFになっているので、INFOを選択しています。
dataTraceEnabled
はAPI Gateway
自体のログを有効にします。どんなことができるかは下記の記事が参考になります。このログが意外と便利なのでtrueにするのがお勧めです。
参考:[AWS CDK] API Gatewayのログ出力を有効にしてCloudWatch Logsでログを確認してみた
metricsEnabled
はCloudWatchメトリクスを有効にするかどうかを決定します。今回は省略しましたが、アラームを設定するとエラーをすぐに知ることができます。
参考: Amazon CloudWatch メトリクスを使用する
apiKey
はAPI Gateway
で使用するAPIキーを作成しています。
usagePlan
はapiKey
とapigateway
の紐付けをおこなっています。
上記の内容をさらに詳しく確認したい方は公式のドキュメントをご覧ください。
今回は、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コマンドで動作確認を行ってみましょう。
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
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
を実行してみてください。
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のリソースが想定通り作成されるか
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
ここまでお読みいただき、ありがとうございました。
参考として使用したリンク一覧
- https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html
- https://aws.amazon.com/jp/cdk/
- https://docs.aws.amazon.com/cdk/api/latest/
- AWS CDK/CloudFormation、リソースを変更せずにスタックドリフトを解消する
- https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html#getting_started_install
- https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv2.html
- AWS CLIでaws configureコマンドにprofileオプションを付けて使ってみた
- https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs-readme.html
- [AWS CDK] API Gatewayのログ出力を有効にしてCloudWatch Logsでログを確認してみた
- Amazon CloudWatch メトリクスを使用する
- https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway-readme.html
- AWS CDKのスナップショットテストでアセットを無視する方法
Discussion