雑なAPIはtRPCで作り、ApiGateayでデプロイでいいかもしれない [デプロイ編]

2022.10.06

はじめに

前回は実装まで、今回はApiGateayで動かしていきます。

Lambdaに上げるようにビルドする

今回はcdkを使いますが、Lambdaにあげるときに、node_moduleを含めたzipでアップロードします。

node_moduleはproductionだけ、開発環境はそのままにしときたいので、ビルド用のDockerImageを作っていきたいと思います。

また、tRPCをLambdaのHandlerで使うためのエンドポイントも作ります。

Lambdaのエンドポイントを作る

./server/src/index.ts

import { awsLambdaRequestHandler } from "@trpc/server/adapters/aws-lambda";
import { apiRouter } from "./trpc/router";
import { createApiContext } from "./trpc/context";

export const handler = awsLambdaRequestHandler({
  router: apiRouter,
  createContext: createApiContext,
});

tRPCがすでにAWS Lambda用のHandlerを用意してくれてるので、被せて終わりです。

Lambda用にアップロード用のzipを作る

./server/Dockerfile

FROM node:16

RUN apt-get update
RUN apt-get install zip -y

VOLUME /output
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
RUN npm run build:src
RUN npm install --omit=dev
RUN cp -rf node_modules dist/node_modules
RUN zip -9yr lambda.zip ./dist/

ENTRYPOINT ["cp", "lambda.zip", "/output/lambda.zip"]。

Lambda用のビルドコマンドも登録しておくと便利です

./server/package.json

{
   ...
    "scripts": {
      "build:lambda": "rm lambda.zip; docker build -t trpc-server .; docker run --rm --volume $PWD:/output trpc-server"
   },
   ...
}

ビルドしてみよう

npm run build:lambda

プロジェクトルートにlambda.zipができていれば成功です。

CDK構築

コンソールで構築しても良いんですが、雑APIはお片付けも簡単な方がいいです。作るのも消すのも簡単がベスト。

なのでCDKをつかって構築していきます。

CDKのプロジェクトを作成

プロジェクトルートにCDK用のプロジェクトを追加していきます。

$ mkdir formation
$ cd formation
$ cdk init --language typescript

API Gatewayを構築

今回はデフォルトであるFormationStackを書き換えていきます。API Gateway構築まで。

./formation/lib/formation-stack.ts

import {
  aws_iam as iam,
  aws_lambda as lambda,
  aws_apigateway as apigateway,
  Stack,
  StackProps,
} from "aws-cdk-lib";
import { Construct } from "constructs";
import * as path from "path";

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

    const lambdaRole = new iam.Role(this, "TRpcSampleLambdaRole", {
      roleName: "TRpcSampleLambdaRole",
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "service-role/AWSLambdaBasicExecutionRole"
        ),
      ],
    });

    const tRpcLambda = new lambda.Function(this, "TRpcLambda", {
      functionName: "trpc",
      runtime: lambda.Runtime.NODEJS_16_X,
      code: lambda.Code.fromAsset(
        path.join(__dirname, "../../server/lambda.zip")
      ),
      memorySize: 128,
      handler: "dist/index.handler",
      role: lambdaRole,
      environment: {
        NODE_ENV: "production",
      },
    });

    const api = new apigateway.RestApi(this, "TRpcApi", {
      restApiName: "TRpcApi",
      description: "TRpcApi",
      endpointTypes: [apigateway.EndpointType.EDGE],
      deployOptions: { stageName: "v1" },
    });

    const apiResource = api.root.addResource("api");
    const anyResource = apiResource.addResource("{path+}");
    anyResource.addMethod("any", new apigateway.LambdaIntegration(tRpcLambda))

  }
}

const apiResource = api.root.addResource("api"); const anyResource = apiResource.addResource("{path+}");`

Cloud Frontで /api のときにApiGatewayを呼べるように設定するのapiのresourceを追加してます。

すべてのメソッドを受けるので {path+}を追加します。

Cloud Frontを構築

import {
  aws_apigateway as apigateway,
  aws_cloudfront as cloudfront,
  aws_cloudfront_origins as origins,
  aws_iam as iam,
  aws_lambda as lambda,
  aws_s3 as s3,
  aws_s3_deployment as s3Deploy,
  Duration,
  RemovalPolicy,
  Stack,
  StackProps,
} from "aws-cdk-lib";
import { Construct } from "constructs";
import * as path from "path";

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

    ...
    // 追記します。


    const trpSampleBucket = new s3.Bucket(this, "TRpcSampleKamedonS3", {
      bucketName: "trpc-sample-kamedon",
      removalPolicy: RemovalPolicy.DESTROY,
    });

    const originAccessIdentity = new cloudfront.OriginAccessIdentity(
      this,
      "OriginAccessIdentity",
      {
        comment: "website-distribution-originAccessIdentity",
      }
    );

    const bucketPolicyStatement = new iam.PolicyStatement({
      actions: ["s3:GetObject"],
      effect: iam.Effect.ALLOW,
      principals: [
        new iam.CanonicalUserPrincipal(
          originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId
        ),
      ],
      resources: [trpSampleBucket.bucketArn + "/*"],
    });
    trpSampleBucket.addToResourcePolicy(bucketPolicyStatement);

    const cachePolicy = new cloudfront.CachePolicy(
      this,
      "TRpcSampleCFCachePolicy",
      {
        queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
      }
    );

    const distribution = new cloudfront.Distribution(this, "TRpcSampleCF", {
      defaultRootObject: "index.html",
      defaultBehavior: {
        allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
        cachePolicy: cachePolicy,
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        origin: new origins.S3Origin(trpSampleBucket, {
          originAccessIdentity: originAccessIdentity,
        }),
      },
      additionalBehaviors: {
        "/api/*": {
          origin: new origins.RestApiOrigin(api),
          allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
          cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
          viewerProtocolPolicy:
            cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
          cachePolicy: cachePolicy,
        },
      },
      priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
    });

    new s3Deploy.BucketDeployment(this, "WebsiteDeploy", {
      sources: [
        s3Deploy.Source.asset(path.join(__dirname, "../../client/dist")),
      ],
      destinationBucket: trpSampleBucket,
      distribution: distribution,
      distributionPaths: ["/*"],
    });

  }
}

CORS対応で /api にApiGatewayのパスをあてます。全てのメソッド、全てのQueryをApiに渡せられるようにします。

ついでにクライアントもデプロイするようにしてます。

CDKでデプロイ

npx cdk deploy

Cloud FrontのURLにアクセスして動作確認します。

GETもPostも問題なく叩けています。

まとめ

ApiGatewayにデプロイしてみました。

Operating Lambda: イベント駆動型アーキテクチャにおけるアンチパターン ではあるので、プロダクションでつかうというより、雑API用途として使います。

tRPCはルーティングに特化してる感じがあり、今回のLambdaのhandlerのように、Next.jsで使う、Express、Fastifyなどで使うことができます。

雑に作って、そのまま他のフレームワークに移植することも簡単そうです。