与 AWS Lambda 部署
如何使用 AWS Lambda 部署 Apollo Server
先决条件
在继续本指南之前,请确保您已完成以下步骤
⚠️ AWS最佳实践警告称,不建议在无需此类密钥的情况下使用您的AWS账户根用户密钥(例如,不要使用这些密钥来配置AWS CLI)。相反,创建一个IAM用户账户并且分配满足部署应用程序需求的最低权限分配权限,以及使用该账户。
设置您的项目
在此示例中,我们将从头开始,以展示所有组件是如何组合在一起的。
首先,安装用于使用Apollo Server及其AWS Lambda集成所需的包:
npm install @apollo/server graphql @as-integrations/aws-lambda
npm install -D typescript
接下来,我们将创建一个包含基本Apollo Server设置的文件。注意文件的名称和位置;我们将在后续步骤中用到这些信息:
import { ApolloServer } from '@apollo/server';// The GraphQL schemaconst typeDefs = `#graphqltype Query {hello: String}`;// A map of functions which return data for the schema.const resolvers = {Query: {hello: () => 'world',},};// Set up Apollo Serverconst server = new ApolloServer({typeDefs,resolvers,});
import { ApolloServer } from '@apollo/server';// The GraphQL schemaconst typeDefs = `#graphqltype Query {hello: String}`;// A map of functions which return data for the schema.const resolvers = {Query: {hello: () => 'world',},};// Set up Apollo Serverconst server = new ApolloServer({typeDefs,resolvers,});
现在我们可以导入startServerAndCreateLambdaHandler
函数和从@as-integrations/aws-lambda
中获取的handlers
对象,传入我们的ApolloServer
实例:
import { ApolloServer } from '@apollo/server';import { startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';const typeDefs = `#graphqltype Query {hello: String}`;const resolvers = {Query: {hello: () => 'world',},};const server = new ApolloServer({typeDefs,resolvers,});// This final export is important!export const graphqlHandler = startServerAndCreateLambdaHandler(server,// We will be using the Proxy V2 handlerhandlers.createAPIGatewayProxyEventV2RequestHandler(),);
import { ApolloServer } from '@apollo/server';import { startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';const typeDefs = `#graphqltype Query {hello: String}`;const resolvers = {Query: {hello: () => 'world',},};const server = new ApolloServer({typeDefs,resolvers,});// This final export is important!export const graphqlHandler = startServerAndCreateLambdaHandler(server,// We will be using the Proxy V2 handlerhandlers.createAPIGatewayProxyEventV2RequestHandler(),);
以上代码片段的最后一行创建了一个名为graphqlHandler
的导出,它是Lambda函数处理器。我们稍后将回到这个函数!
使用Serverless框架部署
Serverless是一个框架,可以帮助将无服务器应用程序部署到AWS Lambda等平台。
安装CLI
我们将使用Serverless CLI来部署我们的应用程序。您可以直接将Serverless软件包安装到项目中,或者全局安装Serverless CLI:
npm install -g serverless
Serverless CLI可以访问您之前配置的AWS CLI的凭据。所以现在我们只需要告诉Serverless我们想要部署哪个服务。
AWS最佳实践建议定期更换访问密钥,适用于需要长期凭证的场景(例如,托管应用程序)。
配置服务
您可以使用serverless.yml
文件配置Serverless,让它知道要部署哪些服务以及处理程序的存储位置。
如果您使用TypeScript,请下载serverless-plugin-typescript
包以启用Serverless使用您的TS文件:
npm install -D serverless-plugin-typescript
您可以使用下面的示例serverless.yml
配置;请注意,确保您使用的文件路径指向导出处理程序的文件:
service: apollo-lambdaprovider:name: awsruntime: nodejs16.xhttpApi:cors: truefunctions:graphql:# Make sure your file path is correct!# (e.g., if your file is in the root folder use server.graphqlHandler )# The format is: <FILENAME>.<HANDLER>handler: src/server.graphqlHandlerevents:- httpApi:path: /method: POST- httpApi:path: /method: GET# Omit the following lines if you aren't using TS!plugins:- serverless-plugin-typescript
本地运行
在部署之前,我们可以使用Serverless CLI在本地调用我们的处理程序,以确保一切正常。我们将通过使用GraphQL 操作来模拟HTTP请求。
您可以通过创建一个query.json
文件来在本地存储模拟的HTTP请求,如下所示:
{"version": "2","headers": {"content-type": "application/json",},"isBase64Encoded": false,"rawQueryString": "","requestContext": {"http": {"method": "POST",},// Other requestContext properties omitted for brevity},"rawPath": "/","routeKey": "/","body": "{\"operationName\": null, \"variables\": null, \"query\": \"{ hello }\"}"}
现在我们可以使用 serverless
调用上面的 查询 来触发我们的处理函数:
serverless invoke local -f graphql -p query.json
您的响应应该类似下面的样子
{"statusCode": 200,"headers": {"cache-control": "no-store","content-type": "application/json; charset=utf-8","content-length": "27"},"body": "{\"data\":{\"hello\":\"world\"}}\n"}
如果本地一切正常,我们可以继续进行部署!
部署
如我们前面所提到的, Serverless 已经获取了您的 AWS CLI 凭据,因此要部署,您只需运行以下命令:
serverless deploy
如果成功, serverless
将会输出类似以下的输出:
> serverless deploy> Deploying apollo-lambda to stage dev (us-east-1)> ✔ Service deployed to stack apollo-lambda-dev (187s)> ..............> endpoints:> POST - https://ujt89xxyn3.execute-api.us-east-1.amazonaws.com/dev/> GET - https://ujt89xxyn3.execute-api.us-east-1.amazonaws.com/dev/> functions:> graphql: apollo-lambda-dev-graphql> Monitor all your API routes with Serverless Console: run "serverless --console"
现在您可以通过 Apollo 沙箱 进入您的端点,并使用 query 查询您新部署的服务器。
serverless
做了什么?
首先,它构建了函数,将文件归档并上传到新的 S3 桶中。然后,它创建了一个 Lambda 函数并使用这些归档文件,如果在一切正常的情况下,它还将在控制台输出 HTTP 端点 URL。
管理生成的服务
您可以通过 AWS 控制台 访问生成的 S3 桶和 Lambda 函数。AWS 控制台也可以让查看您之前创建的 IAM 用户。
要找到Serverless创建的S3桶,在亚马逊列出的服务中搜索S3,然后查找您的桶名(例如,apollo-lambda-dev-serverlessdeploymentbucket-1s10e00wvoe5f
是我们桶的名字)。
要找到Serverless创建的Lambda函数,在亚马逊列出的服务中搜索Lambda
。如果您的Lambda函数列表为空或缺少您的新函数,请检查屏幕右上角的区域。Serverless部署的默认区域是us-east-1
(弗吉尼亚州北)。
如果您想删除Serverless创建的S3桶或Lambda函数,可以运行以下命令:
serverless remove
中间件
为了实现事件和结果变更,可以将类型安全的中间件传递给startServerAndCreateLambdaHandler
调用。此API如下所示:
import { middleware, startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';import { server } from './server';const requestHandler = handlers.createAPIGatewayProxyEventV2RequestHandler();// Middleware is an async function whose type is based on your request handler. Middleware// can read and mutate the incoming event. Additionally, returning an async function from your// middleware allows you to read and mutate the result before it's sent.const middlewareFn: middleware.MiddlewareFn<typeof requestHandler> = async (event) => {// read or update the event here// optionally return a callback to access the resultreturn async (result) => {// read or update the result here};};startServerAndCreateLambdaHandler(server, requestHandler, {middleware: [middlewareFn],});
import { startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';import { server } from './server';const requestHandler = handlers.createAPIGatewayProxyEventV2RequestHandler();// Middleware is an async function whose type is based on your request handler. Middleware// can read and mutate the incoming event. Additionally, returning an async function from your// middleware allows you to read and mutate the result before it's sent.const middlewareFn = async (event) => {// read or update the event here// optionally return a callback to access the resultreturn async (result) => {// read or update the result here};};startServerAndCreateLambdaHandler(server, requestHandler, {middleware: [middlewareFn],});
中间件的一个用例是Cookie修改。包含一个名为cookies
的可推送属性的APIGatewayProxyStructuredResultV2
类型可以将多个set-cookie
头部设置到响应中。
import {startServerAndCreateLambdaHandler,middleware,handlers,} from '@as-integrations/aws-lambda';import { server } from './server';import { refreshCookie } from './cookies';const requestHandler = handlers.createAPIGatewayProxyEventV2RequestHandler();// Utilizing typeofconst cookieMiddleware: middleware.MiddlewareFn<typeof requestHandler> = async (event,) => {// Access existing cookies and produce a refreshed oneconst cookie = refreshCookie(event.cookies);return async (result) => {// Ensure proper initialization of the cookies property on the resultresult.cookies = result.cookies ?? [];// Result is mutable so it can be updated hereresult.cookies.push(cookie);};};export default startServerAndCreateLambdaHandler(server, requestHandler, {middleware: [cookieMiddleware,],});
有关更多用例和API信息,请参阅库的README。
事件扩展
在许多情况下,API网关事件将有一个授权器在前,它包含将在GraphQL解析期间用于授权的定制状态。该库所包含的所有处理程序都包含一个通用类型,它允许您显式扩展基础事件类型。通过传递包含授权信息的事件,该事件类型将在创建contextValue
和middleware
时使用。以下是一个示例,更多信息请参阅库的README。
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;}>>(),);
import { startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';import { server } from './server';export default startServerAndCreateLambdaHandler(server, handlers.createAPIGatewayProxyEventV2RequestHandler());
自定义请求处理
为了支持从AWS Lambda(包括自定义事件)传递的所有事件类型,公开了一个请求处理程序创建实用程序handlers.createHandler(eventParser, resultGenerator)
。此函数返回一个完全类型化的请求处理程序,该处理程序可以作为第二个参数传递到startServerAndCreateLambdaHandler
的调用中。下面是一个示例,具体API在该库的README中记录。
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);
import { startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';import { HeaderMap } from '@apollo/server';import { server } from './server';const requestHandler = handlers.createRequestHandler({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);
使用事件信息
您可以使用context
函数从原始Lambda数据结构中获取有关当前操作的信息。
您的context
函数可以通过包含event和context
对象的参数访问此信息:
const server = new ApolloServer<MyContext>({typeDefs,resolvers,});// This final export is important!export const graphqlHandler = startServerAndCreateLambdaHandler(server, handlers.createAPIGatewayProxyEventV2RequestHandler(), {context: async ({ event, context }) => {return {lambdaEvent: event,lambdaContext: context,};},});
const server = new ApolloServer({typeDefs,resolvers,});// This final export is important!export const graphqlHandler = startServerAndCreateLambdaHandler(server, handlers.createAPIGatewayProxyEventV2RequestHandler(), {context: async ({ event, context }) => {return {lambdaEvent: event,lambdaContext: context,};},});
具有API网关事件的event
对象(HTTP头、HTTP方法、主体、路径等)。context
对象(不要与context
函数混淆)包含当前Lambda上下文(函数名称、函数版本、awsRequestId
、剩余时间等)。
如果您已经更改了您的配置以使用@vendia/serverless-express
您的context
函数将接收req
和res
选项,它们分别是express.Request
和express.Response
对象:
const { ApolloServer } = require('@apollo/server');const { expressMiddleware } = require('@apollo/server/express4');const serverlessExpress = require('@vendia/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, {// The Express request and response objects are passed into// your context initialization functioncontext: async ({ req, res }) => {// Here is where you'll have access to the// API Gateway event and Lambda Contextconst { event, context } = serverlessExpress.getCurrentInvoke();return {expressRequest: req,expressResponse: res,lambdaEvent: event,lambdaContext: context,};},}),);exports.handler = serverlessExpress({ app });
const { ApolloServer } = require('@apollo/server');const { expressMiddleware } = require('@apollo/server/express4');const serverlessExpress = require('@vendia/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, {// The Express request and response objects are passed into// your context initialization functioncontext: async ({ req, res }) => {// Here is where you'll have access to the// API Gateway event and Lambda Contextconst { event, context } = serverlessExpress.getCurrentInvoke();return {expressRequest: req,expressResponse: res,lambdaEvent: event,lambdaContext: context,};},}),);exports.handler = serverlessExpress({ app });
自定义HTTP路由行为
如果您想自定义HTTP路由行为,可以将Apollo Server's Express集成(即expressMiddleware
)与@vendia/serverless-express
包结合起来。该@vendia/serverless-express
库在Lambda事件和Express请求之间进行转换。尽管它们的名称相似,但Serverless CLI与@vendia/serverless-express
包并无关联。
您可以将您的Apollo Server设置更新如下,以获得一个完全运行的Lambda服务器,该服务器在AWS的各种功能中工作:
const { ApolloServer } = require('@apollo/server');const { expressMiddleware } = require('@apollo/server/express4');const serverlessExpress = require('@vendia/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 });
const { ApolloServer } = require('@apollo/server');const { expressMiddleware } = require('@apollo/server/express4');const serverlessExpress = require('@vendia/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 });
该设置允许您根据需要自定义HTTP行为。