依赖项的处理与层的创建与注册
- 依赖项的处理与层的创建与注册
- 新问题
- [什么是
layer
?](#什么是 layer?) - [
layer
的创建与注册](#layer 的创建与注册)- 与函数同时创建和绑定
- [单独上传
layer
再绑定函数(推荐)](#单独上传 layer 再绑定函数(推荐))
- 真正的运行时依赖
- 注册包的约定
- 与平台强关联的运行时
- [1. 云端安装依赖](#1. 云端安装依赖)
- [2. 本地构建
Amazon Linux 2
容器环境](#2. 本地构建 Amazon Linux 2 容器环境) - [3. 利用 CI 构建并进行上传和部署](#3. 利用 CI 构建并进行上传和部署)
- 镜像部署
- [Next Chapter](#Next Chapter)
- 完整示例及文章仓库地址
新问题
在一个 nodejs
函数里,我们往往会去安装和使用各种各样的依赖包,来辅助我们项目的开发。
这些依赖以及对应的版本都被注册在了 package.json
里 (由 npm init
创建)。在安装依赖之后,它们会被存放在当前项目的 node_modules
目录里。而 sls deploy
默认也会把 node_modules
里所有文件,打包压缩后,和代码一起上传到云上。这点很好理解,没有依赖项,函数压根跑不起来。
但是当我们 node_modules
依赖足够大,足够深之后,整个 node_modules
就会变成一个黑洞。随随便便就有 1GB/2GB
甚至更多,这时候去进行 sls deploy
就会变成一种折磨,因为要压缩 node_modules
,这个行为耗时太长了,即使压缩好了,上传到 S3
对象存储也要花费很久的时间。
如何解决? 这时候就需要让我们的 layer
(层)出场了。
什么是 layer
?
layer
你可以理解成,预先放置在我们 Lambda
函数容器中的文件包,你可以在里面放一些代码,库,数据文件,配置文件甚至是一些自定义语言运行时。比如我在使用的时候,往往会把 运行时
依赖的包,打成 zip
上传上去,又或者有些爬虫函数,需要使用 chromium
来模仿用户的行为,那么这种case
则需要在 layer
里面直接内置一个 headless chromium
。
这些文件最终都会被挂载到函数容器中的 /opt
目录。
layer
的创建与注册
这里我给出一个示例,假设运行时的依赖只有 uuid
(假设我们只依赖这一个包)
我们现在项目中安装 npm i uuid
并使用它:
ts
// index.ts
import { v4 } from 'uuid'
v4()
// ...code...
这时候 uuid
在函数package.json
这一层的 node_modules
文件夹里,测试运行后,运转良好。
让我们在当前目录下,创建一个 layers
文件夹,然后再在里面创建一个 uuid
(名称可自定义)文件夹,创建 package.json
并写入:
json
{
"dependencies": {
"uuid": "^9.0.0"
}
}
layers/uuid/package.json#dependencies
字段中的uuid
即为你主目录下安装的版本。
然后执行 npm i
/yarn
安装依赖 (不要使用pnpm
),安装完成后会出现 layers/uuid/node_modules
文件夹,接下来就可以 layer
的上传和绑定了。
与函数同时创建和绑定
我们可以复用原先部署函数的那个 serverless.yml
文件,让它不但部署函数,也同时创建 layer
并进行绑定。具体配置如下:
yml
layers:
# layer 配置
uuid:
# 路径
path: layers/uuid
# aws lambda layer 里的名称
name: ${sls:stage}-uuid
functions:
api:
# ...
package:
individually: true
# 既然我们把所有运行时都打成 layer 了自然不用上传 node_modules 了
patterns:
- "!node_modules/**"
# layer 绑定配置
layers:
# 绑定 UuidLambdaLayer
- !Ref UuidLambdaLayer
environment:
# 环境变量,告诉函数应该从哪里找依赖
NODE_PATH: "./:/opt/node_modules"
其中最重要的就是2
个layers
的配置了,其中 !Ref UuidLambdaLayer
这个其实就是引用了layers.uuid
,只不过它的命名是一种约定,即 uuid
的 u
大写加上 LambdaLayer
,于是就变成了 UuidLambdaLayer
了,命名规则为 layer
名称的 Title_Case
加 LambdaLayer
。
注意!
NODE_PATH: "./:/opt/node_modules"
这个环境变量是必不可少的,不然会导致无法从layer
中加载node_modules
此时使用 sls package
会在 .serverless
目录下生成 2
个 zip
:
api.zip
函数代码包uuid.zip
layer包
这2
个压缩包的名字,就是我们在 serverless.yml
注册的名字。
sls deploy
会把它们依次上传,先layer
后function
,并把layer
上传部署后的结果,注册进我们的function
,从而达成绑定的效果。
单独上传 layer
再绑定函数(推荐)
除了与函数同时创建和绑定的形式,我们还可以分步骤单独上传 layer
再绑定函数,这也是我推荐的方式。
这个就顾名思义,我们可以先上传layer
拿到结果,在把结果写到函数的 serverless.yml
中去。这样步骤方面分为了 2
步,但是好处却是显而易见的。
它适用于这样的场景,我们的 layer
包很大,且不经常更新,这种情况是没有必要每次都去打包上传 layer
的。
我们部署只需要部署我们自己的函数代码,然后告诉 AWS
应该去绑定哪个 layer
的哪个版本就行。
按照这样的思路,我们就可以把刚刚那个 serverless.yml
拆成 2
个 yml
文件:
- 单独上传
layer
配置文件 - 函数的
deploy
配置文件,加一行绑定layer
的配置即可
单独部署 layer
的配置,内容如下:
yml
# layers/serverless.yml
layers:
uuid:
path: uuid
name: ${sls:stage}-uuid
执行 sls deploy
后上传部署成功会显示:
txt
layers:
uuid: arn:aws-cn:lambda:cn-northwest-1:000000000000:layer:dev-uuid:2
这一个字符串就是接下来我们需要写入函数 yml
配置进行绑定的关键,当然当时没保存刷了terminal
也没关系,这个信息通过在当前目录执行 sls info
还会显示出来。
接下来我们去函数的 yml
配置去绑定 layers
:
yml
# serverless.yml
functions:
api:
# ....
layers:
# - !Ref UuidLambdaLayer 改为
- arn:aws-cn:lambda:cn-northwest-1:000000000000:layer:dev-uuid:2
这样再在函数这一层执行 sls deploy
这层绑定关系就完成了,同时上传速度也要比 与函数同时创建和绑定
快不少,因为这种方法只要上传函数的代码包,再通过调用 AWS API
告诉它们这层绑定关系即可。
真正的运行时依赖
之前有一个细节没有讲,为什么我们上传 layer
是单独建一个 layers
的目录,在里面单个单个上传,而不是把外面函数的 node_modules
整个打包上传上去呢?
其实那样也可以,但是不够完美,因为这样做会导致layer
代码包中的文件,过于冗余。
举个简单的例子,默认情况下 npm
安装包时,会把 devDependencies
和 dependencies
都安装在 node_modules
。
比如 dependencies
里就是一些 express
/lodash
啥的,devDependencies
里面就是 eslint
/sass
/webpack
这些开发时用的包,运行代码的时候用不着。
这时候,你直接打包上传 node_modules
显然是可以的,因为你所有的运行时依赖已经在里面了,不幸的是 eslint
等等许多无关紧要的包也进去了,这些加起来有可能会占用你 layer
包整体体积的一般以上甚至更多,显然这是没有必要的。
所以,你必须找到真正的运行时依赖!
注册包的约定
首先你必须在安装包时正确的注册 devDependencies
和 dependencies
。
我们把运行时用的到的放在 dependencies
里,到时候也从这里面抽出包名和版本,做成 layer
。至于devDependencies
里我们只会把那些开发时用得到的包放在里面,它们就没有必要上传了,本地运行即可。
与平台强关联的运行时
其次你必须很清楚你的运行时是 nodejs
而不是什么浏览器。
虽然说 nodejs
是跨平台的,但是我们使用的第三方库里,有时候会存在着很多和平台强关联的二进制文件。
比如有些包会在下载下来之后,调用 postinstall
脚本获取我们的系统信息(平台,cpu架构),然后根据这些信息去使用不同的二进制文件。
又或者有些包基于 C++ addons
的,安装好之后,才在我们机器上实时去编译,生成适配我们系统平台的二进制文件。
这就导致一个问题,假如你直接用 win/macos
开发,你安装的二进制包,大概率无法在 aws lambda
环境下运行,因为它的环境是 Amazon Linux 2
所以,我们应该在挑选与线上 Lambda 运行时,相似的容器中,进行开发,上传代码和层 (layer)
具体怎么做呢?这里给出三种解决方案:
1. 云端安装依赖
顾名思义,直接在云上的目标容器环境中进行安装依赖这个行为,这也是最简单的解决方案。
2. 本地构建 Amazon Linux 2
容器环境
这要求我们在 docker
容器中开发,拉下 Amazon Linux 2
的镜像,在里面开发并上传层函数和代码包。
3. 利用 CI 构建并进行上传和部署
这个思路便是,由环境相同或者相似的 CI 容器,进行上传和部署。
比如我们可以利用 Github Action
去构建和上传,大致的配置如下:
yml
name: Layer
on:
workflow_dispatch:
jobs:
upload:
name: upload
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
# 找一个相似的镜像 os
os: [ubuntu-latest]
node-version: [18]
runs-on: ${{ matrix.os }}
steps:
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Configure AWS credentials from Test account
uses: aws-actions/configure-aws-credentials@v3
with:
aws-region: cn-northwest-1
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Setup Node.js environment
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install Serverless
run: yarn global add serverless
- name: Install puppeteer Layer
run: yarn
working-directory: apps/aws-express-api-ts/layers/puppeteer
- name: sls deploy
run: yarn deploy
working-directory: apps/aws-express-api-ts/layers
镜像部署
当然,假如你使用镜像去部署 lambda
,那以上这些问题都可以避免,但是代价便是冷启动时间比较长。
不过很多技术上的问题,都是可以通过付出更多金钱的方式去解决的,这点就综合考虑利弊吧。
Next Chapter
现在你已经学会了 layer
的用法和一些稀奇古怪的场景。
下一篇,《lambda nodejs 函数降低冷启动时间的最佳实践》中,将会详细介绍如何优化我们自身的代码,欢迎阅读。
完整示例及文章仓库地址
https://github.com/sonofmagic/serverless-aws-cn-guide
如果你遇到什么问题,或者发现什么勘误,欢迎提 issue
给我