カテゴリー
Serverless Frameworkを使ってApollo(v4)サーバーをAWS Lambdaと統合する
※ 当ページには【広告/PR】を含む場合があります。
2024/01/16

AWSを使ったサーバレスベースのREST/HTTP APIをAPIGatewayをゴリゴリと開発を進めていく過程で、APIルーティングのパス構造が非常に深くなってしまったり、クエリパラメータが複雑化してくる問題は悩ましいものです。
そこでApolloを導入すれば、すべてのデータが一つのエンドポイントからGraphQLサーバー経由でアクセスすることができ、APIGateway/Lambdaのルーティング構造が単純化できるなど、大きなアドバンテージになります。
バックエンドの複雑性のほとんどをApolloが受け持ってくれることで、フロントエンドとバックエンドの開発者は双方がどのような技術でアプリケーションを構築しようと、お互いに意識することなく、とにかくApolloとの接続に注力すれば良くなります。
こういった側面もあり、Apolloに代表されるGraphQLを受け持つアプリケーションは、
BFFのテーマは、バックエンドの構造複雑化を吸収・隠蔽し、他方、フロントエンド側には理解しやすく利便性の高い機能を提供する、という地味に難しいところを狙っています。
とりあえず今回はApollo公式のガイドに従いながら、AWS APIGateway/LambdaにApolloサーバーを統合・運用する基礎を解説していきます。
ApolloサーバーをLambdaに統合
まず、ApolloをAWS Lambdaで動かすにあたり、前提条件として、
1. (個人や組織の所有する)AWSアカウント
2. 適切な権限ロールのあるIAMユーザー
3. AWS-CLIコマンドが使える・ある程度知識がある
みたいなことがあげられます。
これら3つのセットアップ方法自体はこの記事では取り上げませんのでご了承ください。
Apollo/Typescriptのインストール
ここから簡単な関数でApolloをLambdaへデプロイをテストするまでの工程を検証していきます。
とりあえず、必須なライブラリモジュールを導入します。
$ yarn add @apollo/server graphql @as-integrations/aws-lambda
$ yarn add typescript -D
Apolloサーバー開発に必ずしもtypescriptは必要ないかもしれませんが、後々、GraphQLスキーマとの型付けのやり取りを考えれば初手の時点でtypescriptを選択するほうが賢明です。
Apolloサーバーの実装
次にApolloサーバーのコアコードを追加・実装します。
import { ApolloServer } from '@apollo/server';
//👇簡単なお試し用GraphQLスキーマ(クエリ)
const typeDefs = `#graphql
type Query {
hello: String
}
`;
//👇先程のクエリでApolloが応答するためのリゾルバ
const resolvers = {
Query: {
hello: () => 'ようこそLambdaな世界!',
},
};
//👇Apolloサーバー本体
const server = new ApolloServer({typeDefs, resolvers});
ここでのApolloサーバーの初期化はスキーマの型定義にあたる
typeDefs
resolvers
開発が進んでくるとApolloサーバーの多様な初期化オプションが必要になってきますが、かなり細かい内容になるのでここでは触れません。
ApolloServerインスタンスのコンストラクタの詳細は、
で確認できます。
ApolloサーバーをLambdaハンドラでラップする
先程のApolloサーバーを実装をローカルで試したい気持ちを抑えて、AWS Lambdaへデプロイさせることを先に考えます。
先程のコードをさらに以下のように編集しましょう。
import { ApolloServer } from '@apollo/server';
//👇Lambda統合のためのランタイム&ハンドラを追加
import { startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';
const typeDefs = `#graphql
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'ようこそLambdaな世界!',
},
};
const server = new ApolloServer({typeDefs, resolvers});
//👇LambdaとAPIGatewayV2(HTTP API)の設定を上手くブリッジしてくれるお助けミドルウェアでApolloサーバーをラップ
export const graphqlHandler = startServerAndCreateLambdaHandler(
server,
handlers.createAPIGatewayProxyEventV2RequestHandler(),
);
これで、Lambdaハンドラとしてそのまま
graphqlHandler
かつてはApolloサーバーとAPIGateway/Lambdaとのリクエスト・レスポンスの設計構造の差を自前で補完するのには骨の折れるミドルウェアを書いた記憶もありますが...version4にもなると、ここらへんの機能も大変便利になりました。
@as-integrations/aws-lambda
Apolloサーバー化したLambdaをServerless Frameworkでデプロイ
上記の都合上、APIGatewayV2のHTTP APIの利用が前提となるので、手動でLambdaデプロイするのには結構骨が折れます。
そこで適当なIoCツールを使うのですが、とりわけAPIGateway前提な
ということで以降では、SLSを使って先程のLambdaをデプロイして動作確認までをやってみましょう。
なお、以下のコマンドでSLSをローカルに導入して利用します。
$ yarn add serverless -D
$ npx serverless --version
Framework Core: 3.38.0 (local)
Plugin: 7.2.0
SDK: 4.5.1
オプションでserverless-plugin-typescriptを入れる
Lambdaのランタイムがnodejs18以降では、これまでのCommonjsのハンドラは非推奨になり、ESModuleベースのリソースコードの利用が促されています。
上の例のようにTypesriptのハンドラのままでは、Lambda側からは使えないので、デプロイ前にはESModuleコードへのトランスパイルが可能な適当なツールで変換する必要があります。
この一手間が面倒な人向けに、
serverless-plugin-typescript
yarn add serverless-plugin-typescript -D
個人的には色々とデプロイ前に手動でesbuildで選択的にパッケージを固めたいので、
serverless-plugin-typescript
serverless.ymlを準備する
ではデプロイするための設定を記述した
serverless.yml
service: apollo-lambda
provider:
name: aws
runtime: nodejs20.x
httpApi:
cors: true
functions:
graphql:
#👇ハンドラを指定(書式:<ファイルパス(.ts抜き)>.<ハンドラ名>)
handler: server.graphqlHandler
events:
- httpApi:
path: /
method: POST
- httpApi:
path: /
method: GET
#👇tsファイルを直接ハンドラ指定したい場合
plugins:
- serverless-plugin-typescript
余談〜invoke localを試すのは諦めよう
さて、Serverless FrameworkにはLambda/APIGWv2のデプロイ前にローカルの開発環境で動作テストができる
オフラインでLambdaの動作試験ができるなんて、聞こえはとても便利なのですが、正常に動作するかどうかはローカルの実行環境に依存します。
ローカルでLambdaを起動させるための適切なエミュレータ・ランタイム等の設定がないと、おそらく以下のような感じで動作試験がコケるはずです。
$ npx sls invoke local
× Error: Cannot find module '/usr/src/app/apigw-apollo/handler'
Require stack:
- /usr/src/app/apigw-apollo/node_modules/serverless/lib/plugins/aws/invoke-local/index.js
- /usr/src/app/apigw-apollo/node_modules/serverless/lib/plugins/index.js
- /usr/src/app/apigw-apollo/node_modules/serverless/lib/classes/plugin-manager.js
- /usr/src/app/apigw-apollo/node_modules/serverless/lib/serverless.js
- /usr/src/app/apigw-apollo/node_modules/serverless/scripts/serverless.js
- /usr/src/app/apigw-apollo/node_modules/serverless/bin/serverless.js
at Module._resolveFilename (node:internal/modules/cjs/loader:1075:15)
at Module._load (node:internal/modules/cjs/loader:920:27)
at Module.require (node:internal/modules/cjs/loader:1141:19)
at require (node:internal/modules/cjs/helpers:110:18)
at loadModule (/usr/src/app/apigw-apollo/node_modules/serverless/lib/plugins/aws/invoke-local/index.js:836:16)
at AwsInvokeLocal.invokeLocalNodeJs (/usr/src/app/apigw-apollo/node_modules/serverless/lib/plugins/aws/invoke-local/index.js:817:39)
at AwsInvokeLocal.invokeLocal (/usr/src/app/apigw-apollo/node_modules/serverless/lib/plugins/aws/invoke-local/index.js:248:19)
at invoke:local:invoke (/usr/src/app/apigw-apollo/node_modules/serverless/lib/plugins/aws/invoke-local/index.js:54:47)
at PluginManager.runHooks (/usr/src/app/apigw-apollo/node_modules/serverless/lib/classes/plugin-manager.js:530:15)
at PluginManager.invoke (/usr/src/app/apigw-apollo/node_modules/serverless/lib/classes/plugin-manager.js:564:20)
at async PluginManager.run (/usr/src/app/apigw-apollo/node_modules/serverless/lib/classes/plugin-manager.js:604:7)
at async Serverless.run (/usr/src/app/apigw-apollo/node_modules/serverless/lib/serverless.js:179:5)
at async /usr/src/app/apigw-apollo/node_modules/serverless/scripts/serverless.js:819:9 {
code: 'MODULE_NOT_FOUND',
requireStack: [
'/usr/src/app/apigw-apollo/node_modules/serverless/lib/plugins/aws/invoke-local/index.js',
'/usr/src/app/apigw-apollo/node_modules/serverless/lib/plugins/index.js',
'/usr/src/app/apigw-apollo/node_modules/serverless/lib/classes/plugin-manager.js',
'/usr/src/app/apigw-apollo/node_modules/serverless/lib/serverless.js',
'/usr/src/app/apigw-apollo/node_modules/serverless/scripts/serverless.js',
'/usr/src/app/apigw-apollo/node_modules/serverless/bin/serverless.js'
]
}
Environment: linux, node 18.15.0, framework 3.30.1 (local), plugin 6.2.3, SDK 4.3.2
Credentials: Local, environment variables
Docs: docs.serverless.com
Support: forum.serverless.com
Bugs: github.com/serverless/serverless/issues
Error:
Exception encountered when loading /usr/src/app/apigw-apollo/handler.cjs
公式の説明の冒頭には、以下の文言があります。
This runs your code locally by emulating the AWS Lambda environment.
Please keep in mind, it's not a 100% perfect emulation,
there may be some differences, but it works for the vast majority of users.
We mock the context with simple mock data.
触れられているように、
「100%動くエミュレータにはなっていない」
どうしても信頼性・機密性の高いAWSマイクロサービスのモック環境がほしい場合には、「LocalStack」等のサービスを利用しましょう。
そもそも、個人の趣味や学習程度の小規模なプロジェクトで、エミュレータによるローカル実行にこだわる必要も薄いです。
1回ごとのビルド出力物のデプロイは結構時間はかかりますが、待ち時間だと思って気長にAWSへデプロイして、問題なく動作するか否か泥臭くトライアンドエラーするほうがさほどお金も掛けずに確実な方法とも言えます。
Lambdaへデプロイ&実行する
さて、役者はそろったので、早速このLambdaハンドラをデプロイしてみます。
$ npx sls deploy --verbose
で問題がなければ、スタックが順次生成されて、そのままアプリケーションがLambda側にデプロイされるはずです。
デプロイが完了したら、
serverless/sls invoke
Apolloサーバーに送信するAWS Lambdaベースのモック・リクエストデータをまとめた
query.json
{
"version": "2",
"headers": {
"content-type": "application/json"
},
"isBase64Encoded": false,
"rawQueryString": "",
"requestContext": {
"http": {
"method": "POST"
}
},
"rawPath": "/",
"routeKey": "/",
"body": "{\"operationName\": null, \"variables\": null, \"query\": \"{ hello }\"}"
}
これで動作テストの準備が整いました。
あとは以下のように実行するだけです。
$ npx sls invoke -f graphql -p query.json --verbose
{
"statusCode": 200,
"headers": {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
"content-length": "65"
},
"body": "{\"data\":{\"hello\":\"ようこそLambdaな世界!\"}}\n"
}
正常にレスポンスが得られたら、Apolloサーバー兼Lambdaの完成です。
LambdaからApolloへContextを受け流す
開発が進んでくると、
@as-integrations/aws-lambda
Apollo v4以降では、LambdaからContextデータを処理しやすい構文になりました。
import { ApolloServer } from '@apollo/server';
import {
startServerAndCreateLambdaHandler,
handlers,
} from '@as-integrations/aws-lambda';
//Apolloサーバー内部処理で共通して利用するコンテクストデータの型を準備
//※ コンテクストの型はコードでインライン化するほか、GraphQL Codegenの
// ような外部ツールから出力した定義セットから呼び出して利用することもできる
type ContextValue = {
isAuthenticated: boolean;
};
const typeDefs = `#graphql
type Query {
hello: String!
isAuthenticated: Boolean!
}
`;
const server = new ApolloServer<ContextValue>({
typeDefs,
resolvers: {
Query: {
hello: () => 'world',
//👇解説ポイント①
isAuthenticated: (root, args, context) => {
return context.isAuthenticated;
},
},
},
});
export default startServerAndCreateLambdaHandler(
server,
handlers.createAPIGatewayProxyEventV2RequestHandler(),
{
//👇解説ポイント②
context: async ({ event }) => {
///...event値を介して、JWT・クッキー・ヘッダーのAuth値などの抽出を行う処理
return {
isAuthenticated: true,
};
},
},
);
まず上記のコードの
解説ポイント①
isAuthenticated
そもそもApolloのリゾルバー関数は、省略可能な4つの引数で実装することが可能です。
root:
第一引数。
親関係にあたるルートリゾルバーが返したオブジェクト
args:
第二引数。
外部のクライアントから渡されたオブジェクト
context:
第三引数。
単に"コンテクスト"というとこの引数を指す。
アプリケーション全体で共有されるオブジェクト(=ContextValue)
info:
第四引数。
レゾルバーでの処理に関する詳細情報の保持したオブジェクト
複数のリゾルバーからコンテクストを利用したい場合には、関数の実装の第三引数を使うことで、アプリケーションで共有した値を使いまわすことができます。
解説ポイント②
startServerAndCreateLambdaHandler
context
この
context
event
context
Apollo/Lambdaのミドルウェア
ここからはより突っ込んだ発展的なネタである
なお、Express.jsにもミドルウェアが存在しますが、ここではそれとは別物のApollo側のミドルウェアです。
ミドルウェアはクラアントからのリクエストから、Apolloサーバーからのレスポンスが返させる途中に処理を記述することができる機能を指します。
ミドルウェアのユースケースに関しては以下にいくつか紹介があります。
ミドルウェアの基本
ミドルウェアの実装には、
middleware.MiddlewareFn<T>
以下に、実装の基本として、簡単なミドルウェアを作成して、Apolloサーバーに登録してみましょう。
import {
middleware, startServerAndCreateLambdaHandler, handlers
} from '@as-integrations/aws-lambda';
import type {
APIGatewayProxyEventV2,
APIGatewayProxyStructuredResultV2,
} from 'aws-lambda';
const server = new ApolloServer({
typeDefs: `#graphql
type Query { hello: String }
`,
resolvers: {
Query: {
hello: () => 'ようこそLambdaな世界〜ミドルウェアを添えて。'
}
}
});
const requestHandler = handlers.createAPIGatewayProxyEventV2RequestHandler();
//👇何もしないミドルウェア・その1
const middlewareFn1: middleware.MiddlewareFn<typeof requestHandler> = async (event) => {
///ここでリクエストイベントを取得したり、パラメータを変更する
//👇リクエストヘッダに追加
event.headers["my-custom-header"] = 'xxxxxxx';
//👇場合によっては処理結果(result)を別の値に入れ替るコールバック
return async (result) => {
/// ここでApolloサーバーの処理結果の取得や結果に修正を加える
if (result.body != null) {
const body = JSON.parse(result.body);
body.data.hello = body.data.hello != null ? body.data.hello + '\nミドルウェア1「ヨシッ」' : body.data.hello;
result.body = JSON.stringify(body);
}
};
};
//👇何もしないミドルウェア・その2(ミドルウェアの関数の引数を明示に書く場合)
const middlewareFn2: middleware.MiddlewareFn<
handlers.RequestHandler<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>
> = async (event) => {
///ここでリクエストイベントを取得したり、パラメータを変更する
//👇先行のミドルウェアで書き換えられたリクエストヘッダはCloudWatchのログで確認できる
console.dir(event);
// --> headers: { 'content-type': 'application/json', 'my-custom-header': 'xxxxxxx' },
//👇場合によっては処理結果(result)を別の値に入れ替るコールバック
return async (result) => {
if (result.body != null) {
const body = JSON.parse(result.body);
body.data.hello = body.data.hello != null ? body.data.hello + '\nミドルウェア2「ヨシッ」' : body.data.hello;
result.body = JSON.stringify(body);
}
};
};
startServerAndCreateLambdaHandler(server, requestHandler, {
//👇ミドルウェアの登録(順序クリティカル)
middleware: [middlewareFn1, middlewareFn2],
});
これをデプロイ後に実行すると、
$ npx sls invoke --function graphql --path query.json --verbose
{
"statusCode": 200,
"headers": {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
"content-length": "98"
},
"body": "{\"data\":{\"hello\":\"ようこそLambdaな世界〜ミドルウェアを添えて。\\nミドルウェア1「ヨシッ」\\nミドルウェア2「ヨシッ」\"}}"
}
となって、ミドルウェアがレスポンスの結果も書き変えていることが分かります。
ミドルウェアに渡る引数として、第一引数の
event
APIGatewayProxyEventV2
result
APIGatewayProxyStructuredResultV2
ですので、2番目のミドルウェアで見て取れるように、ミドルウェアの基本の型は
handlers.RequestHandler<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>
となります。
他にも、後述する「イベント拡張」でも説明しているようなミドルウェアの型もあるため、明示でのミドルウェアの型指定はコーディングに苦痛を伴うかもしれません。
ということでこの場合、1番目のミドルウェアで利用しているような、予め定義してあるハンドラから型を
typeof
ミドルウェアの注意として、ミドルウェアは複数登録できますが、登録順でリクエストとレスポンスに何らかの操作を加えます。
ミドルウェアでcookieを操作する
ミドルウェアの代表的な使い方の例として、クライアント側から送信されたリクエストをAPIGWv2のイベントを経由して、ミドルウェアを挟んでcookieの値を操作する操作は以下のようなものになります。
import {
startServerAndCreateLambdaHandler, middleware, handlers
} from '@as-integrations/aws-lambda';
const server = new ApolloServer({
typeDefs: `#graphql
type Query { hello: String }
`,
resolvers: {
Query: {
hello: () => 'ようこそLambdaな世界〜ミドルウェアを添えて。'
}
}
});
//👇具体的な実装は省略するが、クッキーの期限切れを判別し、
// 期限が切れた場合には新しいクッキーを発行するメソッド
import { refreshCookie } from './cookies';
const requestHandler = handlers.createAPIGatewayProxyEventV2RequestHandler();
const cookieMiddleware: middleware.MiddlewareFn<typeof requestHandler> = async (event) => {
//eventから現在のクッキーを取得し、期限が切れた場合に新しいクッキーを発行する
const cookie = refreshCookie(event.cookies);
return async (result) => {
//👇resultのクッキーを更新(クッキーが未定義の場合には空の配列で初期化)
result.cookies = result.cookies ?? [];
//👇発行したクッキーをresultで返す
result.cookies.push(cookie);
};
};
export default startServerAndCreateLambdaHandler(
server,
requestHandler,
{ middleware: [cookieMiddleware],}
);
ミドルウェアでルーティングを遮断する
他には、認証トークンやセッションの有効期限が無効になった場合に、それ以降の処理をミドルウェアで中断させたいに有効な実装です。
import {
startServerAndCreateLambdaHandler,
middleware,
handlers,
} from '@as-integrations/aws-lambda';
const server = new ApolloServer({
typeDefs: `#graphql
type Query { hello: String }
`,
resolvers: {
Query: {
hello: () => 'ようこそLambdaな世界〜ミドルウェアを添えて。'
}
}
});
const requestHandler = handlers.createAPIGatewayProxyEventV2RequestHandler();
const sessionMiddleware: middleware.MiddlewareFn<typeof requestHandler> = (event) => {
//セッションの有効期限をチェックする処理...
if (!event.headers['X-Session-Key']) {
//セッションが無効だった際には、認証できなかった旨を結果に上書きする
return {
statusCode: 401
body: 'Unauthorized'
}
}
};
export default startServerAndCreateLambdaHandler(server, requestHandler, {
middleware: [sessionMiddleware],
});
この例では、
X-Session-Key
ResultType
拡張イベントへの応用
場合によっては、APIGW側から独自に追加したカスタムイベントのパラメータをGraphQL側のレゾルバに渡す必要があります。
代表的な例として、Lambdaオーソライザーによるユーザー認証ある場合にやりとりされるAuthorizationヘッダーのような情報です。
デフォルトでは、
handlers.createAPIGatewayProxyEventV2RequestHandler
APIGatewayProxyEventV2
APIGatewayProxyEventV2WithLambdaAuthorizer
Lambdaオーソライザーを使った認証情報付きのリクエストイベントが漏れ落ちないように、リクエストハンドラが
APIGatewayProxyEventV2WithLambdaAuthorizer
contextValue
以下のサンプルでは、Lambdaオーソライザー付きのイベントで、ユーザーの独自定義したパラメーター・
myAuthorizerContext
import {
startServerAndCreateLambdaHandler, middleware, handlers
} from '@as-integrations/aws-lambda';
import type {
APIGatewayProxyEventV2WithLambdaAuthorizer
} from 'aws-lambda';
import { server } from './server';
export default startServerAndCreateLambdaHandler(
server,
handlers.createAPIGatewayProxyEventV2RequestHandler<
APIGatewayProxyEventV2WithLambdaAuthorizer<{
myAuthorizerContext: string;
}>
>(),
);
カスタムイベントをハンドリングする
Lambdaからのイベント型を漏らすことなく取得させるためには、
handlers.createHandler
eventParser
resultGenerator
まず、
eventParser
parseHttpMethod:
型: (event: EventType) => string
HTTPメソッド(GET/POST/...)を返す
parseQueryParams:
型: (event: EventType) => string
リクエストのクエリパラメータ(例:foo=1&bar=2)を返す。
リクエストで既にマップオブジェクト化していたら、
ここではURLSearchParams関数等で再度文字列化する必要がある
parseHeaders:
型: (event: EventType) => HeaderMap
イベントからApollo形式のヘッダーマップを生成して返す。
parseBody:
型: (event: EventType, headers?: HeaderMap) => string
リクエストBodyを返す。
ここではbase64や文字列エンコードを処理させる。
また、headersからcontent-typeを読み出すことで、適切な文字列へ変換させる
resultGenerator
startServerAndCreateLambdaHandler
success:
型: (response: HTTPGraphQLResponse) => ResultType
処理が正常に終了した後のレスポンスを生成して返す
error:
型: (e: unknown) => ResultType
異常終了した際のレスポンスを生成して返す
以下は、カスタムイベントのハンドリングの一例を示したコードになります。
import { startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';
import type { APIGatewayProxyEventV2 } from 'aws-lambda';
import { HeaderMap } from '@apollo/server';
import { server } from './server';
type CustomInvokeEvent = {
httpMethod: string;
queryParams: string;
headers: Record<string, string>;
body: string;
};
type CustomInvokeResult = {
success: true;
body: string;
} | {
success: false;
error: string;
};
const requestHandler = handlers.createRequestHandler<CustomInvokeEvent, CustomInvokeResult>(
{
parseHttpMethod(event) {
return event.httpMethod;
},
parseHeaders(event) {
const headerMap = new HeaderMap();
for (const [key, value] of Object.entries(event.headers)) {
headerMap.set(key, value);
}
return headerMap;
},
parseQueryParams(event) {
return event.queryParams;
},
parseBody(event) {
return event.body;
},
},
{
success({ body }) {
return {
success: true,
body: body.string,
};
},
error(e) {
if (e instanceof Error) {
return {
success: false,
error: e.toString(),
};
}
console.error('Unknown error type encountered!', e);
throw e;
},
},
);
export default startServerAndCreateLambdaHandler(server, requestHandler);
Apolloサーバーでイベント情報を取得する
Lambdaを経由したデータ構造からApolloサーバーの使うコンテクスト情報を正しく引き抜きたい場合、
context
context
const server = new ApolloServer<MyContext>({
typeDefs,
resolvers,
});
export const graphqlHandler = startServerAndCreateLambdaHandler(
server,
handlers.createAPIGatewayProxyEventV2RequestHandler(),
{
context: async ({ event, context }) => {
return {
lambdaEvent: event,
lambdaContext: context,
};
},
}
);
Apolloに設定できる
context
event
もう一つの引数内の
context
なお、Apolloでいう
context
serverless-express(旧@vendia/serverless-express)と統合する
既存のAWS Lambda上で運用しているExpressアプリにApolloサーバーの機能を新たに追加したい場合もあります。
多くの開発者がLambdaとExpress.jsを統合してSLSからデプロイまでを一括しておこなえる
serverless-express(旧@vendia/serverless-express)
ちなみに、かつて
aws-serverless-express
@vendia/serverless-express
serverless-express
これにより、
@vendia/serverless-express
特定のHTTPルーティングをApolloサーバーの処理に割り当てたい場合は以下のようにExpressサーバーを修正すると良いでしょう。
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const serverlessExpress = require('@codegenie/serverless-express');
const express = require('express');
const cors = require('cors');
const server = new ApolloServer({
typeDefs: 'type Query { x: ID }',
resolvers: { Query: { x: () => 'hi!' } },
});
server.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests();
const app = express();
app.use(cors(), express.json(), expressMiddleware(server));
exports.graphqlHandler = serverlessExpress({ app });
ここでの
serverless-express
これにより、Apolloサーバーへのリクエストとレスポンスが通常通りに処理させることができます。
ExpressとLambdaへのリクエスト・コンテクスト構造を分離する
既存のルートが使っていた
req(express.Request)/res(express.Response)
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const serverlessExpress = require('@codegenie/serverless-express');
const express = require('express');
const cors = require('cors');
const server = new ApolloServer({
typeDefs: 'type Query { x: ID }',
resolvers: { Query: { x: () => 'hi!' } },
});
server.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests();
const app = express();
app.use(
cors(), express.json(),
expressMiddleware(server, {
//カスタマイズしたcontex関数の初期化:
// -->デフォルトのExpress側のrequest/responseと
// Lambda側のevent/contextを統合・拡張
context: async ({ req, res }) => {
//👇APIGWイベントとLambdaコンテクストにアクセス
const { event, context } = serverlessExpress.getCurrentInvoke();
return {
expressRequest: req,
expressResponse: res,
lambdaEvent: event,
lambdaContext: context,
};
},
}),
);
exports.handler = serverlessExpress({ app });
上のコード例では、Lambda/Apollo側に処理させたいリクエストイベントやコンテクストを
lambdaEvent/lambdaContext
expressRequest/expressResponse
まとめ
以上、公式で説明されているApollo v4とAWS Lambdaの統合するテクニックを一通りさらってみました。
ご自身の開発にあったサーバレスパターン内で、Apolloサーバーを実装されるのにお役に立てれば幸いです。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー