使用阿里云函数服务部署 nestjs

对于一个现有的 nestjs 项目,如何在阿里云上进行函数部署。
本文是一个一路踩坑的记录,其中遇到的问题,没有完全解决,直接绕过了。最终使用自定义镜像的方式部署完成。

Serverless Devs

按照官方推荐,使用 Serverless Devs
具体而言,先全局安装 npm install @serverless-devs/s -g,然后在项目中添加 s.yaml 配置文件。

配置文件详细说明:
Yaml规范 - Serverless Devs
fc/docs/zh/yaml at main · devsapp/fc

但是这个是完整版本的配置,初次使用完全是懵的,所以,有个偷懒的办法是,使用现成的组件,生成一个 yaml 文件,然后复制粘贴过来。

Serverless Regsitry - Serverless package management platform

找一个测试目录,运行 s init start-nest -d start-nest,就能得到一个新的 nestjs 模板。

edition: 1.0.0
name: web-framework-app
# access 是当前应用所需要的密钥信息配置:
# 密钥配置可以参考:https://www.serverless-devs.com/serverless-devs/command/config
# 密钥使用顺序可以参考:https://www.serverless-devs.com/serverless-devs/tool#密钥使用顺序与规范
access: "xxxxxx"

vars: # 全局变量
  region: "cn-hangzhou"
  functionName: "start-nest"
  service:
    name: "web-framework-1jl5"
    description: 'Serverless Devs Web Framework Service'

services:
  framework: # 业务名称/模块名称
    # 如果只想针对 framework 下面的业务进行相关操作,可以在命令行中加上 framework,例如:
    # 只对framework进行构建:s framework build
    # 如果不带有 framework ,而是直接执行 s build,工具则会对当前Yaml下,所有和 framework 平级的业务模块(如有其他平级的模块,例如下面注释的next-function),按照一定顺序进行 build 操作
    component: fc # 组件名称,Serverless Devs 工具本身类似于一种游戏机,不具备具体的业务能力,组件类似于游戏卡,用户通过向游戏机中插入不同的游戏卡实现不同的功能,即通过使用不同的组件实现不同的具体业务能力
    actions: # 自定义执行逻辑,关于actions 的使用,可以参考:https://www.serverless-devs.com/serverless-devs/yaml#行为描述
      pre-deploy: # 在deploy之前运行
        - run: npm install --registry=https://registry.npmmirror.com # 要执行的系统命令,类似于一种钩子的形式
          path: . # 执行系统命令/钩子的路径
        - run: npm run build
          path: .
    props: # 组件的属性值
      region: ${vars.region} # 关于变量的使用方法,可以参考:https://www.serverless-devs.com/serverless-devs/yaml#变量赋值
      service: ${vars.service}
      function:
        name: ${vars.functionName}
        description: 'Serverless Devs Web Framework Function'
        codeUri: .
        runtime: custom
        memorySize: 512
        timeout: 6
        caPort: 9000
        customRuntimeConfig:
          command:
            - ./bootstrap
      triggers:
        - name: http-trigger
          type: http
          config:
            authType: anonymous
            methods:
              - GET
              - POST
      customDomains:
        - domainName: auto
          protocol: HTTP
          routeConfigs:
            - path: /*

有如下关键信息需要修改

账号相关

最终的目标,是通过 s deploy 这个命令,就可以一键部署到 aliyun 的函数服务,所以,先要配置阿里云的访问权限。

Config 命令 - Serverless Devs

第一步 创建账号,配置 AccessKey SecretKey
RAM 访问控制 中创建账号,但是,这个账号创建之后,并没有部署函数服务的权限

创建账号之后,使用 s config add 命令,在本地添加 AccessKey SecretKey,这里会让你配置一个别名,s.yaml 中会用到,如这里的别名为 ali-fc-account

第二步 给账号权限

在权限策略中,创建一个自定义的权限策略,选上 FC(函数计算)相关的权限,

回到用户界面,为创建的用户添加权限,选择刚刚创建的权限策略。

或者,也可以不自定义策略,参考 AliyunFCServerlessDevsRole 的权限,给账号授予相关的权限。

授权之后,使用 s deploy推送部署到阿里云,才不会报错。

未授权时的错误提示
POST /services failed with 403. requestid: 1-64d1fbb6-398045ba8576a96c74a485c9, message: the caller is not authorized to perform 'fc:CreateService' on resource 'acs:fc:cn-hangzhou:1111:services/serverless-devs-check' with condition '[fc:EnableServiceInternetAccess=unknown, fc:EnableServiceSLSLogging=unknown]'.

第三步 修改 s.yaml 中的配置

其实就是 access: "ali-fc-account" 这一行,其中 ali-fc-account 为使用 s config add 时,配置的别名

s.yaml 中的路径配置

使用 s init start-nest -d start-nest 生成的项目模板中,将源码放在了 code 子目录中,不是很喜欢,所以我将其移动到了根目录,那这里的几个路径,也需要跟着修改。但你如果不在意,建议别改,后面有坑。

其它文件

除了在项目中添加 s.yaml 文件,还需要添加 .fcignore 文件,在部署打包时,忽略不需要的文件。一般配置如下

./test
./src
./.s

运行环境

s.yaml 中,runtime 用于配置运行环境

function字段 - Serverless Devs

方式1

runtime: custom
customRuntimeConfig:
  command:
    - ./bootstrap

这里的 custom,在阿里云上,是 debian 环境,带了 nodejs
在使用 custom 这个配置时,需要带上 customRuntimeConfig 的配置。这里的配置就是说,使用 bootstrap 文件来启动程序,所以,此时你的项目根目录中,还需要有一个 bootstrap 文件(没有后缀名)

bootstrap 的具体内容是:

#!/usr/bin/env bash

node ./dist/main.js

如果一切顺利的话,此时运行 s deploy,就可以将服务部署到阿里云函数服务上了。

遇到的问题

1 疑似 nodejs 版本问题

不知道上面的 custom 运行环境中,node 的版本具体是多少,但我这里运行起来,会有如下错误。

Function instance exited unexpectedly(code 1, message:operation not permitted) with start command './bootstrap '.\nLogs:/code/node_modules/@nestjs/common/decorators/http/route-params.decorator.js:53\r\n if (options?.passthrough) {\r\n ^\r\n\r\nSyntaxError: Unexpected token .\r\n at Module._compile (internal/modules/cjs/loader.js:723:23)\r\n at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)\r\n at Module.load (internal/modules/cjs/loader.js:653:32)\r\n at tryModuleLoad (internal/modules/cjs/loader.js:593:12)\r\n at Function.Module._load (internal/modules/cjs/loader.js:585:3)\r\n at Module.require (internal/modules/cjs/loader.js:692:17)\r\n at require (internal/modules/cjs/helpers.js:25:18)\r\n at Object.<anonymous> (/code/node_modules/@nestjs/common/decorators/http/index.js:5:22)\r\n at Module._compile (internal/modules/cjs/loader.js:778:30)\r\n at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)"

chatgpt 是这么说的

这个错误 Function instance exited unexpectedly (code 1, message: operation not permitted) 表示函数实例异常退出,返回了一个退出码为 1 的错误,并且错误消息显示操作不允许。
根据错误日志中的内容,看起来是在加载 @nestjs/common 模块时发生了语法错误 SyntaxError: Unexpected token .。这可能是由于你的 Node.js 版本不支持可选链操作符 ?. 导致的。
可选链操作符是在 ECMAScript 2020 中引入的新特性,它用于简化访问可能为 null 或 undefined 的属性或方法。如果你的 Node.js 版本较旧,不支持可选链操作符,那么就会出现语法错误。
要解决这个问题,你可以尝试以下方法:
更新 Node.js 版本:确保你的 Node.js 版本符合支持可选链操作符的要求。建议使用较新的 LTS(长期支持)版本,以确保你可以使用最新的语言特性。
修改代码:如果你无法更新 Node.js 版本,你可以修改代码,将可选链操作符 ?. 替换为传统的属性访问方式,以避免语法错误。
例如,将 options?.passthrough 替换为 options && options.passthrough。
检查依赖项:如果你的代码依赖于其他模块或库,确保这些依赖项与你使用的 Node.js 版本兼容,并且能够正确加载和执行。
请注意,具体的解决方法可能因你的代码和环境而异。如果问题仍然存在,请提供更多详细信息,以便我可以更好地帮助你解决问题。

如果真是版本问题,还不好处理了。

2 修改 runtime 为 nodejs16 之后的问题

尽管文档中说只支持到 nodejs14,但好像设置成 16 也可以,不过,会有其它的问题。
函数计算运行时的支持情况和终止策略_函数计算-阿里云帮助中心

      function:
        codeUri: ./
        runtime: nodejs16
        handler: main.fcHandler

此时,需要配置 handler,文档见 什么是函数Node.js运行时的请求处理程序_函数计算-阿里云帮助中心

于是在 nestjs 项目的 main.ts 中,添加了如下代码

export async function fcHandler() {
  await bootstrap();
}

然后得到了一个报错,说 main.js 没有找到,因为去 code 目录下找了,但是我的 codeUri 已经配置成根目录了啊。感觉这里有只 BUG。我不想把代码放到 code 子目录中验证了,考虑其它方案。

{
    "errorMessage": "Module '/code/main.js' is missing.",
    "errorType": "FunctionUnhandledError: ImportModuleError",
    "stackTrace": [
        "ImportModuleError: Module '/code/main.js' is missing."
    ]
}

这里的问题意思是,函数服务没有在 code 目录下,找到 main.js,其实 code 目录就是函数服务的根目录,所以,这里的 codeUri 应该修改成 ./dist,不过,修改之后还是有其它问题,nestjs 应该不太能够是有 handler 这种唤起方式。
handler 只适合非常简单的 web 服务。

最终方案

折腾了一圈之后,放弃使用 serverless dev 进行部署了。直接使用 docker 镜像进行部署。当然,serverless dev 也支持配置使用镜像部署,当已经不想再折腾这个配置了。还不如手动操作。

第一步,在本地构建镜像

dockerfile 参考

# 一个本地的镜像,在 debian 基础上,安装了 node18 和 npm
FROM debian-nodejs18:v2

WORKDIR /app

COPY package*.json ./
COPY tsconfig*.json ./
COPY nest-cli.json ./

COPY src .

RUN npm i -g @nestjs/cli
RUN npm ci --production

# RUN npm run build  # 在容器中运行会出错,无法解析 @,安装了全部 npm 依赖(包括开发依赖)也无效,搁置,直接复制 dist 目录

COPY dist ./dist

EXPOSE 9000

CMD ["node", "./dist/main.js"]

第二步,推送镜像到阿里云

容器镜像服务

第三步,在函数服务部署中,使用镜像进行部署

完工!

https://www.cnblogs.com/jasongrass/p/17615357.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章