Try T.M Engineer Blog

多摩市で生息するエンジニアが「アウトプットする事は大事だ」と思って始めたブログ

AWS CDKを使ってSwagger + API Gateway + Lambdaを作る話

最近、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を使用してみたのだが、 自動生成されてしまうnameprotocolTypeを生成後に削除する必要があった。

あとは、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すごいっ!