📚

【AWS】AWS CDKで書いたLambdaをLambda LayerとあわせてAPI Gatewayにデプロイ【CDK】

2022/07/28に公開

AWS CDK in Typescript + Lambda in Python

最近個人的にも世間的にもかなり来てるAWS CDKをTypescriptで実装します。CDKは型セーフで簡単にかけるスクリプト言語としてTypescriptを、Lambdaは柔軟にかけるPythonで採用します。

Lambdaはライブラリを複数のコードで使い回せるLambda Layerとあわせて実装します。
これによって

  1. Lambda Layerにnumpyをインストール
  2. Lambda Function AからLambda Layerのnumpyを呼び出し
  3. Lambda Function BからLambda Layerのnumpyを呼び出し
    みたいなことをできてライブラリを共通化したり容量を節約できたりするのです。自前で作ったライブラリなんかもLambdaのHandlerを除いてLayerにパッケージングしちゃいましょう。

前提

こんな感じのフォルダストラクチャです。cdkの初期化は素敵なサイトなどを参考に進めます。
backend/
├─ bin/
│ ├─ backend.ts
├─ lambda/
│ ├─ index.py
├─ lambda_layer/
├─ lib/
│ ├─ backend_stack.ts

現時点でのaws-cdkの最新バージョンは2.33.0です。バージョンによって大きく仕様が異なるので気をつけてください。

Lambda Layerをセットアップしておく

AWSにアップロードするライブラリをインストールします。pipでインストールするのですがシステムのpythonのsite-packagesにインストールされないように次のコマンドでインストールします。ここが一番難しいかも。

pip install -t lambda_layer/python/lib/python3.9/site-packages numpy

Lambda関数を実装しておく

今回はインストールしたnumpyを呼んでランダムな値を返すだけのやる気のないバックエンドサーバーを作ることにします。

import json
import numpy as np


def handler(event: dict, context: dict) -> dict:
    body = {"hoge": f"randomly chosen number is {str(np.random.randint(10))}"}

    return {
        "statusCode": 200,
        "headers": {"Access-Control-Allow-Origin": "*"},
        "body": json.dumps(body),
    }

bin

これから作るstackをimportしてinstance化するだけです。CDKのこのインスタンス化して終わりな感じは個人的にはなかなか馴染みません。

#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { BackendStack } from "../lib/backend-stack";

const app = new cdk.App();
new BackendStack(app, "BackendStack", {});

Stackを作る

StackはAWS CDKの最も基本的な単位です。特別な理由がなければたった1つのStackを用意しConstructと呼ばれる最小単位のリソースをStackの中で宣言していきます。

import { StackProps, Stack } from "aws-cdk-lib";
import { Construct } from "constructs";

export class BackendStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
  }
}

Roleを作る

cdkでLambdaやを動かすRoleを作りましょう。Constructorの中で適当にdynamodbとLambdaを実行するための基本的な機能でも持たせてやります。(ちなみにこの記事ではdynamodbは使いません)

    const lambdaRole = new Role(this, "lambdaRole", {
      assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
      managedPolicies: [
        ManagedPolicy.fromManagedPolicyArn(
          this,
          "dynamodb",
          "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
        ),
        ManagedPolicy.fromManagedPolicyArn(
          this,
          "lambda",
          "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        ),
      ],
      description: "Basic Lambda Role",
    });

Lambda Layerを作る

今回の主役Lambda Layer。コードがインストールされてるPathに気をつけながら次のように書きます。

    const lambdaLayer = new LayerVersion(this, "PomodoroLayer", {
      code: AssetCode.fromAsset("lambda_layer"),
      compatibleRuntimes: [Runtime.PYTHON_3_9],
    });

Lambdaを作る

handler引数がちょっと特徴的な表現なので注意します。codeに渡すのはフォルダまでのpath, handlerに渡すのは"{ファイル名}.{関数名}"になります。

const lambda = new Function(this, "python-function", {
      functionName: "python-function",
      runtime: Runtime.PYTHON_3_9,
      code: Code.fromAsset("lambda"),
      handler: "index.handler",
      role: lambdaRole,
      layers: [lambdaLayer],
    });

API Gatewayを作ってLambdaを登録する

みたまっまですが、LambdaはLambdaIntegrationでラップしなければいけません。なんで?

const lambdaIntegration = new LambdaIntegration(lambda);
    const apigw = new RestApi(this, "apigw", {
      restApiName: "apigw",
      deployOptions: {
        loggingLevel: MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        metricsEnabled: true,
      },
      defaultCorsPreflightOptions: {
        allowOrigins: Cors.ALL_ORIGINS,
        allowMethods: Cors.ALL_METHODS,
        allowHeaders: Cors.DEFAULT_HEADERS,
        statusCode: 200,
      },
      endpointTypes: [EndpointType.REGIONAL],
    });

    const root = apigw.root.addResource("hello");
    root.addMethod("GET", lambdaIntegration);

Full Code

import { StackProps, Stack } from "aws-cdk-lib";
import { AssetCode, Runtime, LayerVersion, Code } from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
import { Function } from "aws-cdk-lib/aws-lambda";
import { ManagedPolicy, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import {
  Cors,
  EndpointType,
  LambdaIntegration,
  MethodLoggingLevel,
  RestApi,
} from "aws-cdk-lib/aws-apigateway";

export class BackendStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const lambdaRole = new Role(this, "lambdaRole", {
      assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
      managedPolicies: [
        ManagedPolicy.fromManagedPolicyArn(
          this,
          "dynamodb",
          "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
        ),
        ManagedPolicy.fromManagedPolicyArn(
          this,
          "lambda",
          "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        ),
      ],
      description: "Basic Lambda Role",
    });

    const lambdaLayer = new LayerVersion(this, "PomodoroLayer", {
      code: AssetCode.fromAsset("lambda_layer"),
      compatibleRuntimes: [Runtime.PYTHON_3_9],
    });

    const lambda = new Function(this, "python-function", {
      functionName: "python-function",
      runtime: Runtime.PYTHON_3_9,
      code: Code.fromAsset("lambda/endpoint"),
      handler: "index.handler",
      role: lambdaRole,
      layers: [lambdaLayer],
    });
    const lambdaIntegration = new LambdaIntegration(lambda);
    const apigw = new RestApi(this, "apigw", {
      restApiName: "apigw",
      deployOptions: {
        loggingLevel: MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        metricsEnabled: true,
      },
      defaultCorsPreflightOptions: {
        allowOrigins: Cors.ALL_ORIGINS,
        allowMethods: Cors.ALL_METHODS,
        allowHeaders: Cors.DEFAULT_HEADERS,
        statusCode: 200,
      },
      endpointTypes: [EndpointType.REGIONAL],
    });

    const root = apigw.root.addResource("hello");
    root.addMethod("GET", lambdaIntegration);
  }
}

実行する

cdk deployまたはローカルインストールの人はnpm run cdk deployとすると関数がデプロイされます。実行するとご丁寧に"https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/]でAPIを公開しといたよ!"と教えてくれるのでクリックするとエラーがでますのでURLの末尾にhelloと加えてあげてください。

{"hoge": "randomly chosen number is 4"}

と表示されたら成功です。

まとめ

いかがだったでしょうか。AWS CDKはインストールしたパッケージのバージョンをあわせるところでよく壊れます。package.jsonをキレイに書きかえたとおもってもグローバルインストールされたAWS CDKとコンフリクトしたりするのでバージョンとそれにともなうAPIの変更に気をつけてください。最後にこの記事が参考になった人はぜひいいねつけていってくださーい!それではまた。

Discussion