【AWS CDK】ApiGateway × Lambda サーバーレスAPIのテンプレートを作成しました

2022/07/10に公開1

ApiGateway×Lambdaを使ったサーバーレスAPIのインフラをコード化するタスクを任せていただいたので、そのときに得たCDKまわりの知見を共有します。

CDKコードはテンプレートとして使っていただければと思います!

ApiGateway

ApiGatewayに関する設定を記述したクラスです。
今回は必要最小限のためモジュール化するメリットは感じられませんが、実際はログやキャッシュ等の設定を追加していきます。

/lib/ApiGateaway.ts
import * as apigw from "aws-cdk-lib/aws-apigateway"
import * as env from "../config/env"
import { Construct } from "constructs"

export class ApiGateway {
  public api: apigw.RestApi

  constructor(scope: Construct) {
    this.api = new apigw.RestApi(scope, `${env.APP_NAME}-apigw`, {
      restApiName: `${env.APP_NAME}Api`
    })
  }
}

Lambda

Lambdaに関する設定を記述したクラスです。
今回はあくまでテンプレなのでLambdaのロジック部分の記載は省略します。

/Lambda/index.js
exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
/lib/Lambda.ts
import * as lambda from "aws-cdk-lib/aws-lambda"
import { Construct } from "constructs"
import { SubnetType } from "aws-cdk-lib/aws-ec2"
import * as iam from "aws-cdk-lib/aws-iam"
import { RetentionDays } from "aws-cdk-lib/aws-logs"

export class Lambda {
  private func: lambda.Function
  private scope: Construct

  constructor(scope: Construct) {
    this.scope = scope
  }

  build(id: string, handler: string) {
    // Lambdaに付与するIAMポリシー・ロールの設定
    const vpcPolicy = iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole")
    const secretsManagerPolicy = iam.ManagedPolicy.fromAwsManagedPolicyName("SecretsManagerReadWrite")
    const rdsPolicy = iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonRDSDataFullAccess")

    const role = new iam.Role(this.scope, `iamRoleForLambda${id}`, {
      roleName: `lambda-role-${id}`,
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      managedPolicies: [vpcPolicy, rdsPolicy, kmsPolicy, secretsManagerPolicy]
    })

    this.func = new lambda.Function(this.scope, id, {
      functionName: id,
      role: role,
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: 'index.handler',// ファイル名.メソッド名
      code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/')), // Lambdaコードを配置してあるフォルダを指定
      vpcSubnets: {
        // VPC内に配置する必要あり
        subnetType: SubnetType.PRIVATE_WITH_NAT 
      }
    })
    return this.func
  }
}

IAMポリシー名の確認方法

fromAwsManagedPolicyNameメソッドの引数は文字列型で、ドキュメントを読んでも指定できるポリシー名ががわからないので、IAMポリシーの一覧をAWSのマネジメントコンソールから確認します。

LambdaからRDSにアクセスするには、LambdaをVPC内に配置する必要があり、VPC内に設置したLambdaからRDSにアクセスするには、Lambda用のIAMロールにAWSLambdaVPCAccessExecutionRoleを付与する必要があります。

  1. IAMポリシー選択画面から検索して

  2. ポリシー詳細ページでポリシーARN名を確認する

↓↓ ポリシーARN名部分 拡大表示 ↓↓

arn:aws:iam::aws:policy/ 以降の部分が今回指定する箇所になるので、こちらの文字列をfromAwsManagedPolicyNameメソッドの引数に指定します。

   const vpcPolicy = iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole")

ちなみに今回はservice-role/AWSLambdaVPCAccessExecutionRole
のように、ポリシー名直前が /service-roleですが、すべてのAWSのサービスがservice-role/であるというわけではないので、都度ポリシー詳細画面で確認するようにしてください。

ApiFactory

コンポーネント化したApiGatewayクラスとLambdaクラスを使って、エンドポイントを複数作成します。

/utils/ApiFactory.ts


export class ApiGatewayFactory {
  private scope: Construct
  private lambda: Lambda
  public apigw: apigw.RestApi

  constructor(scope: Construct, vpc: VPC, env: { rdsClusterArn: string; secretsManagerArn: string }) {
    this.scope = scope
    this.lambda = new Lambda(this.scope, vpc.vpc, env, [vpc.privateSubnet1a, vpc.privateSubnet1c])
    this.apigw = new ApiGateway(this.scope).api

    this.buildApi()
  }
  
  private buildApi() {
    this.buildUsersApi()
    this.buildPostsApi()
    this.buildCommentsApi()
  }
  
    private buildUsersApi() {
    // [GET]/users
    const users = this.apigw.root.addResource("users")
    const getUsersFnc = this.lambda.build(`${env.APP_NAME}GetUsers`, "getUsers")
    users.addMethod("GET", new apigw.LambdaIntegration(getUsersFnc))
    // [POST]/users
    const postUsersFnc = this.lambda.build(`${env.APP_NAME}PostUsers`, "postUsers")
    users.addMethod("POST", new apigw.LambdaIntegration(postUsersFnc))
    // [PUT]/users/:id
    const userId = this.apigw.root.addResource("{id}")
    const putUsersFnc = this.lambda.build(`${env.APP_NAME}PutUser`, "putUser")
    userId.addMethod("PUT", new apigw.LambdaIntegration(putUsersFnc))
    // [DELETE]/users/:id
    const deleteUsersFnc = this.lambda.build(`${env.APP_NAME}DeleteUser`, "deleteUser")
    userId.addMethod("DELETE", new apigw.LambdaIntegration(deleteUsersFnc))
  }
  // ...
}

project-cdk-stack.ts(エントリポイント)

project-cdk-stack
import { ApiGatewayFactory } from "./../utils/ApiGatewayFactory"
import { Stack, StackProps } from "aws-cdk-lib"
import { Construct } from "constructs"

class ProjectCdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)
    
   // Rdsクラス, SecretsManagersクラス インスタンス生成(省略)
   // const rds = ...
   // const secretsManagers = ...
    
    new ApiGatewayFactory(this, new VPC(this), { rdsClusterArn: secretsManagers.getArn(), secretsManagerArn: rds.cluster.clusterArn })
  }
}
  
new ProjectCdkStack(app, "ProjectCdkStack", {
  env: {
    region: "ap-northeast-1" //CDKで定義したリソースをデプロイするリージョンを指定
  }
})

最後に

今回はApiGateway×Lambdaを用いたサーバーレスAPIを作成してみました。
コードを必要に応じて変更していただくことで、手元でサクッと実行できるAPIが作れるかと思います。

なお、今回はVPC・ RDS・SecretsManager部分を省略しましたが、また別の記事でご紹介できればと思っています。

最後までお読み頂き有難うございました!!

Discussion

牧

丁度取り組んでいたの課題でして非常に参考になりました!
今後も更新楽しみにしております👍🏼