2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

AWS CDKAdvent Calendar 2021

Day 15

AWS-CDKのCustomResourceでAPIGatewayのStageを更新する方法

Last updated at Posted at 2021-12-14

AWS-CDKのCustomResourceでAPIGatewayのStageを更新する

AWS-CDKのカスタムリソースを利用してAPIGatewayのStageを更新します。

cdkを使用するとAPIGateway+Lambdaを爆速で構築でき非常に便利ですが、CloudFormationの仕様上APIGatewayのStageを一度デプロイすると以降は更新を行ってくれません。おそらくcdkでAPIGateway+Lambdaを始めたばかりのほとんどの人が躓くポイントなのかなと思います。

そこで今回はカスタムリソースを使用してAPIGatewayの更新を行うスタックを作成してみました。

(AWS-CDK v2のGAおめでとう🎉)

node:v16.13.0

aws-cdk:2.1.0

今回のソースコードはこちらから確認できます。

環境構築

はじめにcdk用のフォルダを作って初期化します。

mkdir cdk-custom-resource
cd cdk-custom-resource
npx cdk init app --language typescript

各種パッケージ群をインストールします

yarn add esbuild@0 uuid
yarn add -D @types/uuid aws-lambda @types/aws-lambda

Lambdaを格納するディレクトリを作成します。

mkdir lambda

最終的には下記のような構造にします。

$ tree -I node_modules
.
├── README.md
├── bin
│   └── cdk-custom-resource.ts
├── cdk.json
├── jest.config.js
├── lambda
│   ├── deploy-apigateway.ts
│   └── sample.ts
├── lib
│   └── cdk-custom-resource-stack.ts
├── package.json
├── test
│   └── cdk-custom-resource.test.ts
├── tsconfig.json
└── yarn.lock

Stack

本体となるStackです。

new cr.ProviderでLambdaを使用したカスタムリソースの作成をよしなにしてくれます。onEventHandlerにカスタムリソース用のLambdaを指定してあげます。ほかにもisCompleteHandlertotalTimeoutなどの使えそうなオプションも用意されています。

少しハッキーな点として、カスタムリソースのプロパティにuuid()を渡しています。こうすることによってAPIのスタックがDeployされる度にStageの更新を行います。一緒にAPIGatewayのidとステージ名も渡します。

lib/cdk-custom-resource-stack.ts
import {
  Stack,
  StackProps,
  aws_iam as iam,
  aws_apigateway as apigateway,
  custom_resources as cr,
  CustomResource,
} from "aws-cdk-lib";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";
import { v4 as uuid } from "uuid";

const prefix = "cdk-custom-resource";

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

    const api = new apigateway.RestApi(this, "RestAPI", {
      restApiName: `${prefix}-api`,
    });

    const sampleLambda = new NodejsFunction(this, "SampleFunction", {
      entry: "lambda/sample.ts",
      functionName: `${prefix}-sample`,
    });

    api.root.addMethod("GET", new apigateway.LambdaIntegration(sampleLambda));

    const deployLambda = new NodejsFunction(this, "DeployFunction", {
      entry: "lambda/deploy-apigateway.ts",
      functionName: `${prefix}-api-deploy`,
    });

    const provider = new cr.Provider(this, "Provider", {
      onEventHandler: deployLambda,
    });

    provider.onEventHandler.addToRolePolicy(
      new iam.PolicyStatement({
        actions: ["apigateway:POST", "apigateway:PATCH"],
        effect: iam.Effect.ALLOW,
        resources: [
          `arn:aws:apigateway:${this.region}::/restapis/${api.restApiId}/*`,
        ],
      })
    );

    new CustomResource(this, "CustomResource", {
      serviceToken: provider.serviceToken,
      properties: {
        uuid: uuid(),
        API_ID: api.restApiId,
        API_STAGE: api.deploymentStage.stageName,
      },
    });
  }
}

Deploy用のLambda

APIGatewayのStageに新しいDeploymentを発行するLambdaです。cdkよりパラメーターとしてRestAPIのIDとStage名を受け取ります。

カスタムリソースのLambdaはCdkCustomResourceResponseを返す必要があります。

lambda/deploy-apigateway.ts
import { APIGateway } from "aws-sdk";
import {
  CdkCustomResourceEvent,
  CdkCustomResourceHandler,
  CdkCustomResourceResponse,
} from "aws-lambda";

const api = new APIGateway();

const deployApi = async (restApiId: string, stageName: string) => {
  console.log(`deploying ${restApiId}/${stageName}`);

  const deployment = await api
    .createDeployment({
      restApiId,
    })
    .promise();

  const updateResult = await api
    .updateStage({
      restApiId,
      stageName,
      patchOperations: [
        {
          op: "replace",
          path: "/deploymentId",
          value: deployment.id,
        },
      ],
    })
    .promise();
  console.log("complete deploy");

  return updateResult;
};

const putApi = async (
  event: CdkCustomResourceEvent
): Promise<CdkCustomResourceResponse> => {
  const apiId: string = event.ResourceProperties["API_ID"];
  const apiStage: string = event.ResourceProperties["API_STAGE"];

  if (typeof apiId !== "string" || typeof apiStage !== "string") {
    throw new Error('"API_ID" and "API_STAGE" is required');
  }

  const deployResult = await deployApi(apiId, apiStage);

  return {
    PhysicalResourceId: deployResult.deploymentId,
    Data: {
      API_ID: apiId,
      API_STAGE: apiStage,
    },
  };
};

export const handler: CdkCustomResourceHandler = async (event, context) => {
  console.log(event);
  console.log(context);

  switch (event.RequestType) {
    case "Create":
    case "Update":
      return putApi(event);

    case "Delete":
      const promise: CdkCustomResourceResponse = new Promise((resolve) => {
        resolve("ok");
      });
      return promise;
  }
};

export default handler;

API用のサンプルLambda

APIGatewayから呼ばれるサンプル用のLambdaです。

lambda/sample.ts
import { APIGatewayProxyHandler } from "aws-lambda";

export const handler: APIGatewayProxyHandler = async (event) => {
  console.log("hello world");
  console.log(event);

  return {
    statusCode: 200,
    body: JSON.stringify({ message: "hello world" }),
  };
};

cdk deploy

最後にデプロイをして完了です。

yarn cdk deploy

さいごに

今まではAPIGatewayのステージを更新するLambdをスタックに入れ、package.json内に"deploy:api": "cdk deploy *APIStack && aws lambda invoke --function-name api-deploy-api ..."のように書いてましたが、カスタムリソースを使うことで関数名をハードに指定する必要が無くなりCI/CDへ組み込めそうです。

お役に立てれば幸いです。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?