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

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

然而,没想到该文引来巨大的流量,很快就将我在 Cyclic 平台上的免费额度花光了。
![因为加了张红包封面,导致流量暴增](https://cdn.nlark.com/yuque/0/2024/png/221736/1705742773139-a6a135d1-7386-4e73-aba5-b28af10c936c.png#averageHue=%23fdfdfc&clientId=uf17942a5-e6c2-4&from=paste&height=319&id=u062a8a5c&originHeight=638&originWidth=2242&originalType=binary∶=2&rotation=0&showTitle=true&size=241742&status=done&style=none&taskId=u58d40974-fde6-4fa7-a0c4-4cfee11b5c1&title=%E5%9B%A0%E4%B8%BA%E5%8A%A0%E4%BA%86%E5%BC%A0%E7%BA%A2%E5%8C%85%E5%B0%81%E9%9D%A2%EF%BC%8C%E5%AF%BC%E8%87%B4%E6%B5%81%E9%87%8F%E6%9A%B4%E5%A2%9E&width=1121 因为加了张红包封面,导致流量暴增)

![公众号收到一大波调戏,不过大多都是索要红包封面而已](https://cdn.nlark.com/yuque/0/2024/png/221736/1705742887505-a1f0f5ba-53e4-43b4-a01c-7a3850d68d0a.png#averageHue=%23dfc56b&clientId=uf17942a5-e6c2-4&from=paste&height=696&id=u29a6264e&originHeight=1392&originWidth=2900&originalType=binary∶=2&rotation=0&showTitle=true&size=475094&status=done&style=none&taskId=u6849f8c3-b34b-4095-9a79-76a914da632&title=%E5%85%AC%E4%BC%97%E5%8F%B7%E6%94%B6%E5%88%B0%E4%B8%80%E5%A4%A7%E6%B3%A2%E8%B0%83%E6%88%8F%EF%BC%8C%E4%B8%8D%E8%BF%87%E5%A4%A7%E5%A4%9A%E9%83%BD%E6%98%AF%E7%B4%A2%E8%A6%81%E7%BA%A2%E5%8C%85%E5%B0%81%E9%9D%A2%E8%80%8C%E5%B7%B2&width=1450 公众号收到一大波调戏,不过大多都是索要红包封面而已)

![很快收到了 Cyclic 平台的服务中止邮件](https://cdn.nlark.com/yuque/0/2024/png/221736/1705742954841-c361d074-7592-4cb9-a414-6c210944cad8.png#averageHue=%23f4ecea&clientId=uf17942a5-e6c2-4&from=paste&height=933&id=u1b105900&originHeight=1866&originWidth=1530&originalType=binary∶=2&rotation=0&showTitle=true&size=316969&status=done&style=none&taskId=ud87a805f-fd80-4cb9-8b12-1920fcdb00c&title=%E5%BE%88%E5%BF%AB%E6%94%B6%E5%88%B0%E4%BA%86%20Cyclic%20%E5%B9%B3%E5%8F%B0%E7%9A%84%E6%9C%8D%E5%8A%A1%E4%B8%AD%E6%AD%A2%E9%82%AE%E4%BB%B6&width=765 很快收到了 Cyclic 平台的服务中止邮件)

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

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

什么是 SAM

SAM 是 Serverless Application Model 的缩写,我感觉是 serverless 框架的竞争对手,它们的很多理念都相似,也都可以通过 yaml 文件来描述函数。

怎么做

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

代码重构

看过上一篇文章,已经知道我们写了一个 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:
image.png
并能从 AWS Lambda 控制台看到新创建出来的函数:
image.png
其触发器 API Gateway 详情如下:
image.png
简单做个测试,得到结果符合预期:
image.png
在 postman 中测试也通过:
image.png

难点

因为 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,来为别人提供服务的呢!