免费架构:Heroku 不免费了,何去何从之 eggjs 的容器化部署之路

哈德韦

共 8472字,需浏览 17分钟

 · 2023-03-08

前情提要

我好几年前,将自己的 eggjs 项目(https://github.com/Jeff-Tian/alpha),部署在了 Heroku 上,运行得非常好。部署过程也非常丝滑,只需要添加一个 Procfile,内部写上:web: egg-scripts start就可以了。但是,Heroku 不再免费了(Free Arch: Bye-bye to Heroku),做为免费架构的拥趸,我必须找一个替代方案。

在线演示

原来的 heroku 站点是:https://uniheart.pa-ca.me/,这个站点本来可以一直访问。后来知名度越来越高,导致访问额度用得比较快,导致月末会打不开,因为提前把额度用完了。最近我发现月中就打不开了……

替代方案是:https://alpha-jeff-tian.cloud.okteto.net/,为了这个实现这个替代方案,需要对原有项目做一些改造,这正是本文接下来要详细讨论的。

为什么这次不是 Serverless?
之前有过将 NestJs 服务部署到 AWS Lambda 的经历:《一顿操作猛如虎,部署一个万能 BFF》;又有将 koa 服务部署到 Vercel Function 中的经历:《Free Arch:将 Koa 服务部署到 Vercel》。所以这一次如果要将 eggjs 部署成 Serverless,那是很连贯的。不料,网上已经有教程了,并且正好是将 eggjs 部署到阿里函数计算,有兴趣可以参考:《https://www.alibabacloud.com/help/en/function-compute/latest/deploy-an-egg-js-application-to-function-compute》。将这三篇连起来看,不仅尝试了 nodejs 中不同的 web 框架,还体验了不同的云厂商提供的 Serverless 服务,可谓爽哉!

起步
这是一个典型的 eggjs 项目(https://github.com/Jeff-Tian/alpha),当你看到这篇文章时,它已经被容器化了。但是前不久,它还是一个很久没有更新了的代码库,可以说这一次又是一个复活老项目的例子。

在容器化前,只要使用 nodejs 15.4.0,有 docker desktop 软件,下载到本地后,可以直接yarn dev运行起来。当然现在仍然也是如此,总之,起步条件是该项目依赖 nodejs 15.4.0,依赖 mysql 数据库、以及 redis。
容器化
这一次没有采用 Serverless,而是准备将 eggjs 项目容器化,再部署到 k8s 集群中。

Dockerfile
容器化的第一步,就是写一个 Dockerfile 出来。参考了 eggjs 官方的 docker,做了一些调整。因为我的 eggjs 项目,使用了 TypeScript 语言,所以要多一个构建过程。
FROM node:15.4.0-alpine
ENV TIME_ZONE=Asia/Shanghai
RUN \ mkdir -p /usr/src/app \ && apk add --no-cache tzdata \ && echo "${TIME_ZONE}" > /etc/timezone \ && ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime
WORKDIR /usr/src/app
# RUN npm i --registry=https://registry.npm.taobao.org
COPY . /usr/src/app
RUN yarn && yarn build
EXPOSE 7001
CMD yarn eggstart

注意,最后一行还使用了yarn eggstart命令,这也是新加的,原本的项目中没有这个命令。因为 eggjs 默认是集群方式以守护进程模式运行,但是我们的目标是部署到 k8s 集群中,不需要集群模式,也不需要守护进程,并且希望在 pod 中保持一个实例就好,于是新加了yarn eggstart专用在 k8s 集群中,它本质上是以下命令的快捷方式,定义在 package.json 文件中:
"eggstart": "NODE_ENV=k8s EGG_SERVER_ENV=k8s eggctl start --workers=1 --no-daemon",
小提示
在参考官方 dockerfile 时,发现官方的 Dockerfile 也有一些不太合理的地方,顺便提了个 PR 以改进:https://github.com/eggjs/docker/pull/3。看到一些有改进空间的地方,顺手改一下,不仅方便他人;如果得到采纳,还能混个 Contributor 啥的。eggjs 算是 nodejs 生态中比较火的框架了,我就是一边使用一边在碰到问题时提出改进的 PR,就混了个 eggjs 的贡献者身份:



混到一些知名开源项目的贡献者,是有实际好处的,比如,可以得到 Copilot 的免费使用权:



如果没有免费特权,也极为推荐付费使用,实现编程自由(《Copilot 与 ChatGPT,让程序员如虎添翼 —— 让 AI 们为我们打工!》)。
构建脚本
有了 Dockerfile,就需要在每次的 CICD 过程中,构建它,测试它,并上传。以便最终在 k8s 集群中拉取上传的镜像,为此,可以写一个脚本文件:
docker build -t jefftian/alpha:"$1" .docker imagesdocker run --network host -e CI=true -d -p 127.0.0.1:7001:7001 --name alpha:"$1"jefftian/alphadocker ps | grep -q alphadocker ps -aqf "name=alpha$"docker push jefftian/alpha:"$1"docker logs $(docker ps -aqf name=alpha$)curl localhost:7001 || docker logs $(docker ps -aqf name=alpha$)docker kill alpha || echo "alpha killed"docker rm alpha || echo "alpha removed"
不妨给该脚本文件起个名字叫dockerize.sh。注意,它接受一个参数,是为了给镜像打 tag 用。它不仅可以在 CICD 过程中跑,如果需要在本地测试一下,也是可以的:
sh ./dockerize.sh test-tag
SOPS
安装 SOPS,通过 SOPS 加密它后再保存在代码库中。本地可以通过brew install sops之类的方式来安装它,但是我样在 CICD 过程中,也需要用 SOPS 来解密,所以还需要在 CICD 流水线中安装它,命令如下,后面会用到:
- run: wget https://github.com/mozilla/sops/releases/download/v3.7.3/sops-v3.7.3.linux.amd64- run: sudo cp sops-v3.7.3.linux.amd64 /usr/local/bin/sops- run: sudo chmod +x /usr/local/bin/sops
在安装了 SOPS 后,要在项目中启用,还需要在项目根目录创建一个 `.sops.yaml`  文件,来定义规则,比如对哪个文件进行保护等等:
creation_rules:  # If assuming roles for another account use "arn+role_arn".  # See Advanced usage  - path_regex: k8s\/secrets\.yaml$    kms: "arn:aws:kms:us-east-1:443862765029:key/b1739688-ec15-407d-895d-d05ca1217a2f"    aws_profile: lambda-doc-rotary

以上配置定义了对 k8s/secrets.yaml 文件进行加密保护,并指定了采用 AWS KMS 的名为 lambda-doc-rotary 的秘钥进行加解密。为了使用该秘钥,还需要记下对应的 aws access_key 和 secret_key,并保存在~/.aws/config文件中:
[lambda-doc-rotary]aws_access_key_id = xxxaws_secret_access_key = yyy
为了在 CICD 过程中成功连接 AWS KMS,可以使用命令行动态生成上述文件,比如:
- run: mkdir ${HOME}/.aws- run: echo -e "[lambda-doc-rotary]\naws_access_key_id = ${{secrets.AWS_ACCESS_KEY}}\naws_secret_access_key = ${{secrets.AWS_SECRET_KEY}}\n" > ~/.aws/config

配置好了 AWS KMS,加密文件只需要:
sops -e -i k8s/secrets.yaml --aws-profile lambda-doc-rotary

解密文件只需要:
sops -d -i k8s/secrets.yaml --aws-profile lambda-doc-rotary
配置 GitHub Actions Secrets
我们准备使用 GitHub Actions 来做 CICD。在 CICD 过程中需要连接一些使用密码的服务,这些密码,我们保存在 GitHub Actions 的 Secrets 里。对于我们要做的,将 eggjs 部署到 k8s 中,需要使用到如下的秘密值:



其中 AWS_ACCESS_KEY 和 AWS_SECRET_KEY 是给 sops 加解密用的,而 DOCKER_USERNAME 和 DOCKER_PASSWORD 是用来推镜像使用。GH_TOKEN 后面会再次介绍,这是为了拉取我的私人仓库用的,这个仓库里保存了 k8s 集群的信息。

配置 CICD 流水线
容器化完成后,就可以在 CICD 流水线中使用它了。先看一下最终效果:



可以看到这个流水线配置了 3 个步骤,第一步是验证项目的功能正常,其中包括了测试和构建项目;如果这一步通过,那么就进行容器镜像的构建。容器构建完毕,会上传到 Docker Hub:



最后,该镜像会在部署到 k8s 集群时被拉取。

准备 k8s 声明文件
创建一个 k8s 文件夹,用来存放所有 k8s 声明文件。
准备秘密文件
该项目依赖 MySQL 数据库和 REDIS,其连接信息要通过环境变量传入运行时,所以我们创建一个 secrets.yaml 文件,放在项目中新建的 k8s 目录下:
apiVersion: v1kind: Secretmetadata:    name: alpha-secrets    labels:        branch: maintype: OpaquestringData:    MYSQL_HOST: alpha.xxxx.rds.cn-northwest-1.amazonaws.com.cn    MYSQL_PORT: "3306"    MYSQL_USERNAME: admin    MYSQL_PASSWORD: yyyy    MYSQL_DATABASE: alpha    REDIS_URI: redis://username:password@host:port
这样的秘密文件,显然不能明文存储在代码库中,于是我们需要加密它。这就要用到前面提到的 sops,后面还会再次提到。

准备 kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomization
bases: []resources: - deployment.yaml - service.yaml
准备 service.yaml
apiVersion: v1kind: Servicemetadata:  name: alpha  annotations:    dev.okteto.com/auto-ingress: 'true'spec:  type: ClusterIP  ports:    - name: tcp      port: 7001      protocol: TCP      targetPort: 7001  selector:    app: alpha    tier: backend
准备 deployment.yaml
apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: alpha    tier: backend    deployedBy: deploy-node-app  name: alphaspec:  minReadySeconds: 5  progressDeadlineSeconds: 600  replicas: 2  revisionHistoryLimit: 10  selector:    matchLabels:      app: alpha      tier: backend  strategy:    rollingUpdate:      maxSurge: 1      maxUnavailable: 0    type: RollingUpdate  template:    metadata:      labels:        app: alpha        tier: backend        deployedBy: deploy-node-app    spec:      containers:        - image: jefftian/alpha          imagePullPolicy: Always          name: alpha          ports:            - containerPort: 7001              name: http              protocol: TCP          resources:            limits:              cpu: 500m              memory: 512Mi            requests:              cpu: 250m              memory: 256Mi          envFrom:            - secretRef:                name: alpha-secrets      restartPolicy: Always      terminationGracePeriodSeconds: 30
流水线第一步:验证项目
这一步是先安装依赖,再跑测试,最后验证构建。即 yarn install、yarn test、yarn build。

流水线第二步:构建容器镜像
本质上是调用前面的容器化脚本,只不过,这里使用了 github.sha 做为参数传递给这个脚本。
build-docker-image:  runs-on: ubuntu-latest  needs: build  steps:    - uses: actions/checkout@v3    - run: echo "${{secrets.DOCKER_PASSWORD}}" | docker login -u "${{secrets.DOCKER_USERNAME}}" --password-stdin    - run: git_hash=$(git rev-parse ${{ github.sha }})    - run: sh .github/dockerize.sh ${{ github.sha }}

流水线第三步:部署到 k8s 集群
按《Free Arch: 使用 OAM 摆脱厂商锁定》提到的,可以同时部署到多个 k8s 集群。步骤是一样的,只是连接信息不同。这里再次以 Okteto 为例。

本质上这里先使用 SOPS 解密秘密文件,并应用到 k8s secrets;然后,应用 k8s kustomization;最后,如果只是更新镜像,可以使用kubectl set image deployment alpha alpha=jefftian/alpha:新Tag
deploy-okteto:  runs-on: ubuntu-latest  needs: build-docker-image  steps:    - uses: actions/checkout@v3
- run: mkdir ${HOME}/.aws - run: echo -e "[lambda-doc-rotary]\naws_access_key_id = ${{secrets.AWS_ACCESS_KEY}}\naws_secret_access_key = ${{secrets.AWS_SECRET_KEY}}\n" > ~/.aws/config
- run: wget https://github.com/mozilla/sops/releases/download/v3.7.3/sops-v3.7.3.linux.amd64 - run: sudo cp sops-v3.7.3.linux.amd64 /usr/local/bin/sops - run: sudo chmod +x /usr/local/bin/sops
- run: curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl - run: chmod +x ./kubectl - run: sudo mv ./kubectl /usr/local/bin/kubectl - run: mkdir ${HOME}/.kube - run: npm i -g k8ss - run: echo -e "machine github.com\n login ${{secrets.GH_TOKEN}}" > ~/.netrc - run: git clone https://github.com/Jeff-Tian/k8s-config.git ${HOME}/k8s-config - run: k8ss switch --cluster=okteto --namespace=jeff-tian - run: sops -d k8s/secrets.yaml --aws-profile lambda-doc-rotary | kubectl apply -f - - run: kubectl apply -k k8s - run: kubectl set image deployment alpha alpha=jefftian/alpha:${{ github.sha }}
完整的 CICD 流水线代码详见:https://github.com/Jeff-Tian/alpha/blob/master/.github/workflows/nodejs.yml















浏览 39
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报