この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
前回は実装まで、今回は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などで使うことができます。
雑に作って、そのまま他のフレームワークに移植することも簡単そうです。