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

2023-03-08 15:48:23 来源:程序员客栈

前情提要

我好几年前,将自己的 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-alpineENV TIME_ZONE=Asia/ShanghaiRUN \mkdir -p /usr/src/app \&& apk add --no-cache tzdata \&& echo "${TIME_ZONE}" > /etc/timezone \&& ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtimeWORKDIR /usr/src/app# RUN npm i --registry=https://registry.npm.taobao.orgCOPY . /usr/src/appRUN yarn && yarn buildEXPOSE 7001CMD 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-secretslabels:branch: maintype: OpaquestringData:MYSQL_HOST: alpha.xxxx.rds.cn-northwest-1.amazonaws.com.cnMYSQL_PORT: "3306"MYSQL_USERNAME: adminMYSQL_PASSWORD: yyyyMYSQL_DATABASE: alphaREDIS_URI: redis://username:password@host:port

这样的秘密文件,显然不能明文存储在代码库中,于是我们需要加密它。这就要用到前面提到的 sops,后面还会再次提到。准备 kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationbases: []resources:- deployment.yaml- service.yaml

准备 service.yaml

apiVersion: v1kind: Servicemetadata:name: alphaannotations:dev.okteto.com/auto-ingress: "true"spec:type: ClusterIPports:- name: tcpport: 7001protocol: TCPtargetPort: 7001selector:app: alphatier: backend

准备 deployment.yaml

apiVersion: apps/v1kind: Deploymentmetadata:labels:app: alphatier: backenddeployedBy: deploy-node-appname: alphaspec:minReadySeconds: 5progressDeadlineSeconds: 600replicas: 2revisionHistoryLimit: 10selector:matchLabels:app: alphatier: backendstrategy:rollingUpdate:maxSurge: 1maxUnavailable: 0type: RollingUpdatetemplate:metadata:labels:app: alphatier: backenddeployedBy: deploy-node-appspec:containers:- image: jefftian/alphaimagePullPolicy: Alwaysname: alphaports:- containerPort: 7001name: httpprotocol: TCPresources:limits:cpu: 500mmemory: 512Mirequests:cpu: 250mmemory: 256MienvFrom:- secretRef:name: alpha-secretsrestartPolicy: AlwaysterminationGracePeriodSeconds: 30

流水线第一步:验证项目这一步是先安装依赖,再跑测试,最后验证构建。即 yarn install、yarn test、yarn build。流水线第二步:构建容器镜像本质上是调用前面的容器化脚本,只不过,这里使用了 github.sha 做为参数传递给这个脚本。

build-docker-image:runs-on: ubuntu-latestneeds: buildsteps:- 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-latestneeds: build-docker-imagesteps:- 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

标签:

为您推荐

新闻快讯