最近、AWS CDKに挑戦している。
正直、最初はCDKを使うメリットをあまり感じられなかったのだが、慣れてくるとCDKを使うほうがテンプレートの管理がしやすく、書くスピードも上がって効率が良くなっていることが実感できる。
後から気づいた事なのだが、CDKにはCfnテンプレートの内容をそのままコードで書けるCfnXXX関数
が用意されている。
そのため「うわっ、これ書くの難しそう・・・」と思ったら、CfnXXX関数
を使うのが良さそうだ。
CDKを使うと試してみたくなるのが、Swagger + API Gateway + Lambdaのパターン。
今までのCfnテンプレートでSwaggerを使うとなると、どうしても手作業のコピー&ペーストが必要だった。しかし、CDKを使うとimport
で読み取ることができるので、コピー&ペーストが不要。
というわけで、さっそくやってみる。
Swaggerファイル )を用意する。
{ "openapi": "3.0.1", "info": { "title": "Swagger Sample", "description": "Swagger Sample", "version": "1.0.0" }, "paths": { "/request": { "get": { "operationId": "testGetOperation", "responses": { "200": { "description": "successful.", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" } } } } } } }, "x-amazon-apigateway-integration": { "type": "AWS_PROXY", "httpMethod": "POST", "uri": "あとで上書きする", "connectionType": "INTERNET", "payloadFormatVersion": "2.0", "timeoutInMillis": 30000 }, "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body" } } } }, "x-codegen-request-body-name": "body" } } }, "components": { "schemas": { "Body": { "type": "object", "properties": { "key": { "type": "string" } } } } } }
というわけで、さっそくStackの中身を書いていこう。
import * as cdk from '@aws-cdk/core' import * as httpApi from '@aws-cdk/aws-apigatewayv2' import * as iam from '@aws-cdk/aws-iam' import * as logs from '@aws-cdk/aws-logs' import * as lambda from '@aws-cdk/aws-lambda' import openapiJson from '../swagger/openapi.json' export class ApigwSample extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props) const stack = cdk.Stack.of(this) // Lambdaの定義 const rolelambda = new iam.Role(this, 'lambdaSampleRole', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( 'service-role/AWSLambdaBasicExecutionRole' ), ], path: '/lambda/', }) const lambdaSampleFunction = new lambda.Function(this, 'lambdaSampleFunction', { runtime: lambda.Runtime.NODEJS_14_X, code: lambda.Code.fromAsset('lambda/functions'), functionName: 'lambdaSampleFunction', handler: 'index.lambdaHandler', environment: { TZ: 'Asia/Tokyo', }, role: rolelambda }) const lambdaFunctionCurrentVersionAlias = new lambda.Alias( this, 'lambdaSampleAlias', { aliasName: 'dev', version: lambdaSampleFunction.currentVersion, } ) lambdaFunctionCurrentVersionAlias.addPermission( 'lambdaSampleCurrentAlias', { principal: new iam.ServicePrincipal('apigateway.amazonaws.com'), action: 'lambda:InvokeFunction', sourceArn: `arn:aws:execute-api:${stack.region}:${stack.account}:*/*/*/request` } ) new logs.LogGroup(this, 'lambdaSampleLogGroup', { logGroupName: '/aws/lambda/' + lambdaSampleFunction.functionName, retention: logs.RetentionDays.ONE_DAY, removalPolicy: cdk.RemovalPolicy.DESTROY, }) // APIGateway(HttpAPI)の定義 const apigwSampleHttp = new httpApi.HttpApi(this, 'apigwSampleHttp', { apiName: 'ApigwSample', createDefaultStage: false, }) const apigwSampleHttpNode = apigwSampleHttp.node.findChild('Resource') as httpApi.CfnApi apigwSampleHttpNode.body = openapiJson delete apigwSampleHttpNode.name delete apigwSampleHttpNode.protocolType new httpApi.HttpStage(this, 'apigwSampleHttpStage', { httpApi: apigwSampleHttp, autoDeploy: true, stageName: 'dev', }) // 上記で作成したlambdaのArnを設定する必要があるため、openapi.jsonを上書きする openapiJson['paths']['/request']['get']['x-amazon-apigateway-integration'][ 'uri' ] = `arn:aws:apigateway:${stack.region}:lambda:path/2015-03-31/functions/${lambdaFunctionCurrentVersionAlias.functionArn}/invocations` new cdk.CfnOutput(this, 'apigwSampleHttpUrl', { value: apigwSampleHttp.apiEndpoint }) } }
TypeScriptで書いているけど、型推論してくれるので、自分で型を定義することはほとんどありません。
APIGatewayを作成する時、Swaggerを使用すると、Swagger内にAPIGatewayの設定内容がほとんど書かれているため、Cfnのテンプレートを書く時にはbody
にSwaggerの内容を転記する必要があった。
なので、CDKではhttpApi.CfnApi
を使用してbody
に直接をSwaggerの内容を設定する方がすっきり書けそう・・・
今回はhttpApi.HttpApi
を使用してみたのだが、 自動生成されてしまうname
やprotocolType
を生成後に削除する必要があった。
あとは、x-amazon-apigateway-integration
。さすがにArnの情報まではSwagger内に書けないので、Lambda定義後に上書きが必要。
あとは、簡単にStackを書けば、CDKからCfnのテンプレートを作るところまでが完成。
import 'source-map-support/register'; import * as cdk from '@aws-cdk/core'; import { ApigwSample } from '../lib/apigw-sample'; const app = new cdk.App(); new ApigwSample(app, 'CdkSampleStack');
次は、Lambdaを書いていきます。
CDKに合わせてLambdaもTypeScriptで書いていくことにします。
import { APIGatewayProxyEventV2 } from 'aws-lambda' export interface LambdaHandlerResult { StatusCode: number, Message: string } export async function lambdaHandler(event: APIGatewayProxyEventV2): Promise<LambdaHandlerResult> { return { StatusCode: 200, Message: 'Successfully' } }
「ビルドどうしよう・・・」と悩んでいたら、クラメソさんのココの記事を参考にさせていただきました。
const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { mode: 'development', target: 'node', entry: { functions: path.resolve( __dirname, './lambda/functions/index.ts', ), }, // 依存ライブラリをデプロイ対象とするか設定(対象はpackage.json参照) // devDependencies:開発時に必要なライブラリを入れる // dependencies:実行時に必要なライブラリを入れる externals: [ nodeExternals({ modulesFromFile: { exclude: ['dependencies'], include: ['devDependencies'], }, }), ], output: { filename: '[name]/index.js', path: path.resolve(__dirname, './lambda'), libraryTarget: 'commonjs2', }, // 変換後ソースと変換前ソースの関連付け devtool: 'inline-source-map', module: { rules: [ { // ローダーが処理対象とするファイルを設定 test: /\.ts$/, exclude: /node_modules/, // 先ほど追加したts-loaderを設定 use: [ { loader: 'ts-loader', }, ], }, ], }, // import時のファイル指定で拡張子を外す // https://webpack.js.org/configuration/module/#ruleresolve resolve: { extensions: ['.ts', '.js'], }, };
また、クラメソさんの別記事で、aws-lambda-nodejsというモジュールが追加されたというのがあり、最初はこれを使おうかな・・・と思ったのですが、まだベータ版なので、今回は見送りました。
AWSのリソースをTypeScriptを使って全部書けると意外と楽だったので、Now!!でJavaScript/TypeScriptを勉強している身としては、CDKやLambdaをどんどん書いていこう!という気持ちになれました。
いやぁ・・・CDKすごいっ!