Serverless

使用现有的 Fastify 应用运行无服务器 (serverless) 应用与 REST API。

Fastify 无法直接运行在无服务器平台上,需要做一点修改。本文为如何在知名的无服务器平台上运行 Fastify 应用提供指导。

我应该在无服务器平台上使用 Fastify 吗?

取决于你自己!FaaS 通常使用精简专注的函数,但你依然可以运行完整的 web 应用。但请牢记,应用越繁重,初始化越漫长。在无服务器环境中运行 Fastify,最好的方式便是使用例如 Google Cloud Run、AWS Fargate 以及 Azure Container Instances 之类的平台,它们能同时处理多个请求,因此能充分利用 Fastify 的特性。

开发便利是通过 Fastify 构建无服务器应用的优势之一。在本地环境,Fastify 应用无需任何额外工具即可运作,而相同的代码加上一些额外内容便能在无服务器平台上运行。

目录

AWS Lambda

以下是使用 Fastify 在 AWS Lambda 和 Amazon API Gateway 架构上构建无服务器 web 应用/服务的示例。

注:使用 aws-lambda-fastify 仅是一种可行方案。

app.js

  1. const fastify = require('fastify');
  2. function init() {
  3. const app = fastify();
  4. app.get('/', (request, reply) => reply.send({ hello: 'world' }));
  5. return app;
  6. }
  7. if (require.main === module) {
  8. // 直接调用,即执行 "node app"
  9. init().listen(3000, (err) => {
  10. if (err) console.error(err);
  11. console.log('server listening on 3000');
  12. });
  13. } else {
  14. // 作为模块引入 => 用于 aws lambda
  15. module.exports = init;
  16. }

你可以简单地把初始化代码包裹于可选的 serverFactory 选项里。

当执行 lambda 函数时,我们不需要监听特定的端口,因此,在这个例子里我们只要导出 init 函数即可。 在 lambda.js 里,我们会用到它。

当像往常一样运行 Fastify 应用, 比如执行 node app.js(可以用 require.main === module 来判断), 你可以监听某个端口,如此便能本地运行应用了。

lambda.js

  1. const awsLambdaFastify = require('aws-lambda-fastify')
  2. const init = require('./app');
  3. const proxy = awsLambdaFastify(init())
  4. // 或
  5. // const proxy = awsLambdaFastify(init(), { binaryMimeTypes: ['application/octet-stream'] })
  6. exports.handler = proxy;
  7. // 或
  8. // exports.handler = (event, context, callback) => proxy(event, context, callback);
  9. // 或
  10. // exports.handler = (event, context) => proxy(event, context);
  11. // 或
  12. // exports.handler = async (event, context) => proxy(event, context);

我们只需要引入 aws-lambda-fastify (请确保安装了该依赖 npm i --save aws-lambda-fastify) 以及我们写的 app.js,并使用 app 作为唯一参数调用导出的 awsLambdaFastify 函数。 以上步骤返回的 proxy 函数拥有正确的签名,可作为 lambda 的处理函数。 如此,所有的请求事件 (API Gateway 的请求) 都会被代理到 aws-lambda-fastifyproxy 函数。

示例

你可以在这里找到使用 claudia.js 的可部署的例子。

注意事项

  • 你没法操作 stream,因为 API Gateway 还不支持它。
  • API Gateway 的超时时间为 29 秒,请务必在此时限内回复。

Google Cloud Run

与 AWS Lambda 和 Google Cloud Functions 不同,Google Cloud Run 是一个无服务器容器环境。它的首要目的是提供一个能运行任意容器的底层抽象 (infrastucture-abstracted) 的环境。因此,你能将 Fastify 部署在 Google Cloud Run 上,而且相比正常的写法,只需要改动极少的代码。

参照以下步骤部署 Google Cloud Run。如果你对 gcloud 还不熟悉,请看其入门文档

调整 Fastify 服务器

为了让 Fastify 能正确地在容器里监听请求,请确保设置了正确的端口与地址:

  1. function build() {
  2. const fastify = Fastify({ trustProxy: true })
  3. return fastify
  4. }
  5. async function start() {
  6. // Google Cloud Run 会设置这一环境变量,
  7. // 因此,你可以使用它判断程序是否运行在 Cloud Run 之中
  8. const IS_GOOGLE_CLOUD_RUN = process.env.K_SERVICE !== undefined
  9. // 监听 Cloud Run 提供的端口
  10. const port = process.env.PORT || 3000
  11. // 监听 Cloud Run 中所有的 IPV4 地址
  12. const address = IS_GOOGLE_CLOUD_RUN ? "0.0.0.0" : undefined
  13. try {
  14. const server = build()
  15. const address = await server.listen(port, address)
  16. console.log(`Listening on ${address}`)
  17. } catch (err) {
  18. console.error(err)
  19. process.exit(1)
  20. }
  21. }
  22. module.exports = build
  23. if (require.main === module) {
  24. start()
  25. }

添加 Dockerfile

你可以添加任意合法的 Dockerfile,用于打包运行 Node 程序。在 gcloud 官方文档中,你能找到一份基本的 Dockerfile

  1. # 使用官方 Node.js 10 镜像。
  2. # https://hub.docker.com/_/node
  3. FROM node:10
  4. # 创建并切换到应用目录。
  5. WORKDIR /usr/src/app
  6. # 拷贝应用依赖清单至容器镜像。
  7. # 使用通配符来确保 package.json 和 package-lock.json 均被复制。
  8. # 独立地拷贝这些文件,能防止代码改变时重复执行 npm install。
  9. COPY package*.json ./
  10. # 安装生产环境依赖。
  11. RUN npm install --only=production
  12. # 复制本地代码到容器镜像。
  13. COPY . .
  14. # 启动容器时运行服务。
  15. CMD [ "npm", "start" ]

添加 .dockerignore

添加一份如下的 .dockerignore,可以将仅用于构建的文件排除在容器之外 (能减小容器大小,加快构建速度):

  1. Dockerfile
  2. README.md
  3. node_modules
  4. npm-debug.log

提交构建

接下来,使用以下命令将你的应用构建成一个 Docker 镜像 (将 PROJECT-IDAPP-NAME 替换为 Google 云平台的项目 id 和 app 名称):

  1. gcloud builds submit --tag gcr.io/PROJECT-ID/APP-NAME

部署镜像

镜像构建之后,使用如下命令部署它:

  1. gcloud beta run deploy --image gcr.io/PROJECT-ID/APP-NAME --platform managed

如此,便能从 Google 云平台提供的链接访问你的应用了。

netlify-lambda

首先,完成与 AWS Lambda 有关的准备工作。

新建 functions 文件夹,在其中创建 server.js (应用的入口文件)。

functions/server.js

  1. export { handler } from '../lambda.js'; // 记得将路径修改为你的应用中对应的 `lambda.js` 的路径

netlify.toml

  1. [build]
  2. # 构建站点时执行的命令
  3. command = "npm run build:functions"
  4. # 发布到 netlify CDN 的文件夹
  5. # 同时也是应用的前端
  6. # publish = "build"
  7. # 构建好的 Lambda 函数的目录
  8. functions = "functions-build" # 总是为构建后的 `functions` 文件夹名称加上 `-build` 后缀

webpack.config.netlify.js

别忘记添加这个文件,否则会有不少问题

  1. const nodeExternals = require('webpack-node-externals');
  2. const dotenv = require('dotenv-safe');
  3. const webpack = require('webpack');
  4. const env = process.env.NODE_ENV || 'production';
  5. const dev = env === 'development';
  6. if (dev) {
  7. dotenv.config({ allowEmptyValues: true });
  8. }
  9. module.exports = {
  10. mode: env,
  11. devtool: dev ? 'eval-source-map' : 'none',
  12. externals: [nodeExternals()],
  13. devServer: {
  14. proxy: {
  15. '/.netlify': {
  16. target: 'http://localhost:9000',
  17. pathRewrite: { '^/.netlify/functions': '' }
  18. }
  19. }
  20. },
  21. module: {
  22. rules: []
  23. },
  24. plugins: [
  25. new webpack.DefinePlugin({
  26. 'process.env.APP_ROOT_PATH': JSON.stringify('/'),
  27. 'process.env.NETLIFY_ENV': true,
  28. 'process.env.CONTEXT': env
  29. })
  30. ]
  31. };

Scripts

package.jsonscripts 里加上这一命令

  1. "scripts": {
  2. ...
  3. "build:functions": "netlify-lambda build functions --config ./webpack.config.netlify.js"
  4. ...
  5. }

这样就完成了。

Vercel

Vercel 针对 Node.js 应用提供了零配置部署方案。要使用 now,只需要如下配置你的 vercel.json 文件:

  1. {
  2. "rewrites": [
  3. {
  4. "source": "/(.*)",
  5. "destination": "/api/serverless.js"
  6. }
  7. ]
  8. }

之后,写一个 api/serverless.js 文件:

  1. "use strict";
  2. // 读取 .env 文件
  3. import * as dotenv from "dotenv";
  4. dotenv.config();
  5. // 引入 Fastify 框架
  6. import Fastify from "fastify";
  7. // 实例化 Fastify
  8. const app = Fastify({
  9. logger: true,
  10. });
  11. // 将应用注册为一个常规插件
  12. app.register(import("../src/app"));
  13. export default async (req, res) => {
  14. await app.ready();
  15. app.server.emit('request', req, res);
  16. }