早在 2021 年,我提出免费架构(Free Arch),即尽量使用免费资源来构建应用和服务,并且要在多处部署,参考《Free Arch: 狡兔三窟,多处部署 - Jeff Tian的文章 - 知乎 》。

另外,见《欢迎来调戏我:在公众号里对接 AWS Bedrock 服务 - Jeff Tian的文章 - 知乎 》,我将一个微信智能助理部署到了 Okteto,好景不长,Okteto 于 2024 年 1 月 15 日开始停止了免费服务。于是在《调用 AWS Bedrock 服务,为微信公众号消息提供自动回复功能。 - Jeff Tian的文章 - 知乎 》中我提到,我又将该智能助理部署到了 Cyclic 平台。

然而,没想到该文引来巨大的流量,很快就将我在 Cyclic 平台上的免费额度花光了。

因为加了张红包封面,导致流量暴增

公众号收到一大波调戏,不过大多都是索要红包封面而已

很快收到了 Cyclic 平台的服务中止邮件

于是今天再来折腾,将它部署到 AWS Lambda 上。

AWS Lambda,我已经在自己的万能 BFF (详见《基于 AWS 构建 BFF 的架构说明 - Jeff Tian的文章 - 知乎 》)中使用了很多年了,至今仍然免费,看来还是 AWS 大方!不过,万能 BFF 使用的是 Serverless 框架,但我这次想玩点新鲜的,于是决定试用一下 AWS SAM 来进行部署。

什么是 SAM

SAM 是 <font style=color:rgb(22, 25, 31);>Serverless Application Model 的缩写,我感觉是 serverless 框架的竞争对手,它们的很多理念都相似,也都可以通过 yaml 文件来描述函数。

<font style=color:rgb(22, 25, 31);>怎么做

以下几个提交描述了接下来要概述的步骤,但是到了代码级别,非常详细,分别是:

代码重构

看过上一篇文章,已经知道我们写了一个 Koa 应用,命名为 app.js。现在,将它最后一行:app.listen(8080) 删除,并新建一个 server.js,引用 app.js,并将 app.listen(8080) 写在这个新的文件里。它是服务器化的启动方式,我们即将新增无服务器化的启动方式,但是同时我们保证不破坏已有的服务器启动方式。这时,再顺便将 yarn start命令从 node app.js改成 node server.js。

安装 serverless-http

yaml yarn add serverless-http

即可。

无服务器化

新增文件 serverless.js,并引用 app.js,以及增加 handler函数。这一步我们应该很熟悉,之前在万能 BFF 里也写同样的函数,即无服务器化的主入口是 handler 函数。

yaml const ServerlessHttp = require(serverless-http); const app = require(./app);

module.exports.handler = ServerlessHttp(app);

增加 SAM 描述文件

在新增文件夹 sam里添加 template.yaml文件,这有点类似万能 BFF 里的 serverless.yaml,但是细节有一些区别,这个文件如下:

yaml AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Resources: EasySchoolBackendFunction: Type: AWS::Serverless::Function Properties: CodeUri: ./dist.zip Handler: dist/serverless/serverless.handler Runtime: nodejs18.x Timeout: 900 PackageType: Zip Environment: Variables: AWSAccessKeyId: !Ref AWSAccessKeyId AWSSecretAccessKey: !Ref AWSSecretAccessKey CYCLIC_DB: outstanding-necklace-frogCyclicDB CYCLIC_APP_ID: outstanding-necklace-frog CYCLIC_URL: https://outstanding-necklace-frog.cyclic.app CYCLIC_BUCKET_NAME: cyclic-outstanding-necklace-frog-eu-north-1 Events: ApiEvent: Type: Api Properties: Path: /{any+} Method: ANY

注意,需要加密存储的值放在 GitHub Actions 里的秘密值里,在这里使用 !Ref引用。

从本地部署到 AWS Lambda

先在本地执行 yarn deploy,它其实是 yarn zip&&cd sam&&sam build&&AWS_PROFILE=lambda-doc-rotary sam deploy --guided 的组合,通过它,会生成一个元数据文件 samconfig.toml如下:

toml version = 0.1 [default.deploy.parameters] stack_name = sam-app resolve_s3 = true s3_prefix = sam-app region = us-east-1 confirm_changeset = false capabilities = CAPABILITY_IAM image_repositories = []

从 GitHub Actions 运行 CICD

新增一个 workflow,如下:

yaml

This workflow will run tests using node and then publish a package to GitHub Packages when a release is created

For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages

name: Node.js Package

on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - run: npm i -g yarn - run: yarn && yarn ci - run: yarn zip - uses: aws-actions/setup-sam@v2 - uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} aws-region: us-east-1 - run: | cd sam sam build --use-container # Prevent prompts and failure when the stack is unchanged - run: | cd sam sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --parameter-overrides AWSAccessKeyId=${{ secrets.AWS_ACCESS_KEY }} AWSSecretAccessKey=${{ secrets.AWS_SECRET_KEY }}

最终效果

提交代码,等待新的 GitHub Actions 执行,执行完成后,可以从 AWS 控制台看到新的 Cloud Formation:

1705750606705 7d7e5829 2f27 45dd a7d4 a9288cbf2920

并能从 AWS Lambda 控制台看到新创建出来的函数:

1705750666677 c48a2648 fd59 4b4e b53a 75573bea83ec

其触发器 API Gateway 详情如下:

1705750715728 4559210a a31b 45cf 8778 13943a015e81

简单做个测试,得到结果符合预期:

1705752263774 c5481dd9 01d8 4c51 852c f8732948462f

在 postman 中测试也通过:

1705761372413 a1d8a53e 241a 48db b9bd f2d8c11877d8

难点

因为 cyclic 对 AWS dynamodb sdk 进行了二次封装,使用了自己的模型,即所谓的单表缓存,极大地简化了对 Dynamodb 的调用。但是,如果要在自己的 AWS 账号里,也能正常使用 cyclic 的 dynamodb 封装,那就要反向推测出其 dynamodb 的模式,这有点困难,好在有一篇文档:https://docs.cyclic.sh/concepts/database 。简单介绍了其结构:

IndexName Partition Key Range Key Projected Fields
primary pk sk all
keys_gsi keys_gsi keys_gsi_sk pk,sk, gsi_prj
gsi_prj gsi_prj - prj

然后,通过 GPT-4 Turbo,终于推断出了一个 SAM template:

yaml ChatsTable: Type: AWS::DynamoDB::Table Properties: TableName: outstanding-necklace-frogCyclicDB AttributeDefinitions: - AttributeName: pk AttributeType: S - AttributeName: sk AttributeType: S - AttributeName: keys_gsi AttributeType: S - AttributeName: keys_gsi_sk AttributeType: S - AttributeName: gsi_prj AttributeType: S KeySchema: - AttributeName: pk KeyType: HASH - AttributeName: sk KeyType: RANGE GlobalSecondaryIndexes: - IndexName: keys_gsi KeySchema: - AttributeName: keys_gsi KeyType: HASH - AttributeName: keys_gsi_sk KeyType: RANGE Projection: ProjectionType: ALL # 或者指定具体的属性 - IndexName: gsi_prj KeySchema: - AttributeName: gsi_prj KeyType: HASH Projection: ProjectionType: ALL # 或者指定具体的属性 BillingMode: PAY_PER_REQUEST

这一步,花了我好多时间!

总结

Cyclic 是一个非常好的平台,的确为我们做了很多事情,从而简化了对 AWS 的使用。也就是说 Cyclic 是一个很好的服务者,但是使用它的服务,超过一定限度,就需要收费了。这非常合情合理,如果不想付费,而额度又超过了它的免费计划,就需要自己和 AWS 打交道了。

也就是说,一切技术或者 IT 本质上也是一种服务。要么付费买人家的服务,要么自己去做所有的脏活累活。

另外,如果有能力自己做这些脏活累活,别人又有需求,完全可以打包成产品卖出去。

事实上,我已经发现很多所谓的产品,都是这样的。同时也越来越感觉到 AWS 的了不起,不知道多少公司和产品,都是依托 AWS,来为别人提供服务的呢!