手摸手实战前端项目CI CD
CI/CD 是 持续集成(Continuous Integration) 和 持续交付/部署(Continuous Delivery/Continuous Deployment) 的缩写,是现代软件开发中的一种自动化方法论,用于加速代码交付和部署的流程,同时保证代码质量和稳定性
大家工作中应该也都接触到了它的方便,如:提交MR、自动打包、自动部署等等,让开发者大大省去了大量的部署时间,从而专注于需求的开发(纯牛马)
那么时间久了你是否对这种方法有过思考,如:这一系列是如何运作的、自己如何搭建尝试呢?对于每位开发者来说都有必要尝试去搭建完整的体系,这样自己对软件落地的整体体系会有更明确的认知。本篇文章将手把手带你完成前端项目从开发到部署的整个过程
🎁 本篇文章所用示例项目可以点击这里查看
阅前须知
本篇不适合0️⃣基础小白,需要掌握docker、linux、gitlab、k8s、node等相关知识,如果你对这方面的内容还是空白,可以参考我的往期文章
项目准备
首先在代码管理仓库创建一个项目用来存放项目代码(这里以Gitlab为例),创建fe-cicd
:
接着创建工程项目,并简单写2个页面;这里本人用CLI助手直接用已有的项目模板来作为演示项目,读者也可以直接使用:执行以下命令很快就会创建好空壳项目模板
# 初始化项目
➜ hengshuai create fe-cicd
HengShuai CLI v0.0.13
? 请选择要创建的模板👇 create a vue project
? 请选择模板类型👀 vue3 admin skeleton with nestjs
? 是否需要自动安装依赖❓ No
🎉 Successfully created project fe-cicd.
👉 Get started with the following commands:
➜ cd fe-cicd
➜ pnpm install
2
3
4
5
6
7
8
9
10
11
将本地项目推送到代码仓库
git add .
git cm -m '初始化'
git remote add gitlab http://192.168.10.10/learn/fe-cicd.git
git push origin dev
2
3
4
以上步骤比较简单,我们搞完后就可以在仓库里看到项目代码了:
打包测试
对于一个新项目在真正部署前一般我们都会在本地执行打包脚本先验证下是否可以跑的通,执行以下命令:
pnpm run build
pnpm run start
2
查看运行结果:项目正常运行了
CI准备
跑流水线最重要的还是ci的配置脚本,通过ci脚本我们可以精确控制流水线如何执行,因此配置好ci是关键
注册Runner
在写ci配置前要确保已经准备好了gitlab runner
,它主要来执行ci的,如果你对runner还是很陌生话请先参考我的往期文章;这里在项目中已经启用了准备好的runner,使用标签是testing
;runner的标签名在注册时就可以确定,建议使用有意义的命名规则
一个项目可以使用的多个runner,在ci中体现就是:每个阶段都可以使用不同的runner,或者同阶段的并行任务使用不同runner也可以加速流程,各位视机器配置自行决定
编写gitlab ci
本次项目使用Gitlab CI 进行来执行流水线工作,因此我们在项目根目录下创建新建.gitlab-ci.yml
温馨提示
如果你对gitlab ci的配置文件不是很熟悉,可以参考我的往期文章并结合官方文档,本篇以主要流程为主不做很详细的配置介绍
这里我们将流水线分4个阶段:
- install:安装依赖
- build:项目构建
- deploy:项目部署
- notice:消息通知,消息通知又分为成功和失败
根据以上划分可以简单写出下面这些内容:
image: node:18-alpine
variables:
IMAGE_TAG: $CI_PROJECT_NAME:$CI_COMMIT_REF_NAME
stages:
- install_deps
- build
- deploy
- notice
install_dependencies:
stage: install_deps
only:
- dev
tags:
- testing
cache:
key:
files:
- pnpm-lock.yaml
paths:
- node_modules
artifacts:
expire_in: "30" #30s
paths:
- node_modules
script:
- echo "Installing dependencies..."
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
- apk update && apk add git && apk add openssh-client
- npm config set registry https://registry.npmmirror.com/
- npm install -g pnpm
- pnpm install
- echo "Dependencies installed successfully!"
build_project:
stage: build
only:
- dev
tags:
- testing
artifacts:
expire_in: "30"
paths:
- dist
needs:
- install_dependencies
script:
- echo "Building project..."
- pnpm build
deploy_project:
stage: deploy
only:
- dev
tags:
- testing
needs:
- build_project
script:
- echo "Deploying project..."
- echo "success"
# 流水线成功消息
notice_success:
stage: notice
only:
- dev
tags:
- testing
needs:
- deploy_project
when: on_success
script:
- echo "success"
# 流水线失败消息
notice_failure:
stage: notice
only:
- dev
tags:
- testing
needs:
- deploy_project
when: on_failure
script:
- echo "failure"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
以上流水线管道在GitLab上的可视化效果如下:
按分支执行
通常项目开发会分为好几个环节如:beta、testing、prod等等,不同环境的可能会有不同的权限,不同环境的runner可能也不一样,因此在ci脚本中就可以真不同环境来区分哪些阶段的流程
假设以下的消息通知,让它只在dev、testing环境下执行,那么就可以通过only
关键字来明确指出:
notice_success:
stage: notice
only:
- dev
- testing
tags:
- testing
script:
- echo "success"
2
3
4
5
6
7
8
9
自动化测试
团队中一般对于基建项目会很严格,出一点错误可能就会影响到成百上千的项目。所以通常都会在发布前做相关的自动化测试保证最基本的测试用例不会受到影响,只有这样才能把握住代码的质量;而在ci中我们也可以把它集成进来,在流水线时通过跑自动化测试根据结果来决定是否可以继续执行下去了
本次通过Vitest来演示单元测试,你可以通过其他的工具(如:Jest、Cypress)附加测试功能;先创建一个简单的测试用例:
// __test__/unit/demo.spec.ts
import { describe, expect, it } from "vitest";
describe("Test Demo", () => {
it("should pass", () => {
expect(true).toBe(true);
});
});
2
3
4
5
6
7
8
新增测试npm脚本:
{
"scripts": {
"test:unit": "vitest run"
}
}
2
3
4
5
读者先在本地运行看能否跑通,避免在ci中增加未知问题
增加测试脚本后,我们需要把它集成到ci中;在ci中增加一个testing
阶段用来跑测试用例
stages:
- testing # 新增测试阶段
testing_project:
stage: testing
tags:
- testing
needs:
- install_dependencies
script:
- echo "Testing project..."
- pnpm run test:unit
2
3
4
5
6
7
8
9
10
11
12
以上只是简单的将测试放入了ci管道,如果你对测试数据比较感兴趣,如:代码覆盖率、代码质量等等,可以参考Test coverage文档、Code quality文档
打包镜像
以上我们就对流水线的上半部分的工作就基本做完了,接下来就是打包应用并发布了,这也是很关键的一个步骤;本篇只做云原生应用的部署,如果你还没有涉及到容器相关的技术,这里不做相关的介绍
容器最关键的就是镜像,对于应用来说可以通过Dockerfile
来创建镜像文件,下面是简单的一个例子:
FROM node:18.19.0-alpine3.19
WORKDIR /app
# 直接从ci打包阶段拿到产物
COPY dist dist
COPY node_modules node_modules
COPY ecosystem.config.js ecosystem.config.js
RUN npm install pm2 -g --registry=https://registry.npmmirror.com/
ENTRYPOINT ["pm2-runtime", "start", "ecosystem.config.js", "--env", "production"]
2
3
4
5
6
7
8
9
10
11
12
13
你可以能对上面的docker镜像文件有疑问❓可能觉得他太少了,最基本的项目打包脚本都没有哪来的构建产物;这里我们可以理解为在流水线的流程中在打包阶段将产物保存,然后在构建镜像时直接用就行而不需要在docker中再次构建,这样就省去了大量的时间(更过可以参考gitlab官方文档)
当然docker最终跑的脚本是通过pm2来维护的,我们还需要编写个pm2的配置文件:
// https://pm2.keymetrics.io/docs/usage/quick-start/
module.exports = {
apps: [
{
name: "fe-cicd",
script: "dist/server/main.js",
watch: false,
instance: 2,
autorestart: true,
max_memory_restart: "1G",
env: {
NODE_ENV: "development",
},
env_production: {
NODE_ENV: "production",
},
},
],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这里使用kaniko来构建镜像:
build_image:
stage: build
needs:
- build_project
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
only:
- dev
tags:
- testing
script:
- echo "Deploying project..."
- for var in ${PROJECT_ENV_VARIABLE// / }; do echo $var >> .env;done
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"${DOCKER_REPO}\":{\"username\":\"${DOCKER_REGISTRY_USER}\",\"password\":\"${DOCKER_REGISTRY_PASSWORD}\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "$DOCKER_REPO/$IMAGE_TAG"
rules:
- if: $CI_COMMIT_TAG
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
以上构建镜像后会把它推送到容器的仓库中心,细心的读者已经发现我们在,我们在build
阶段新增了镜像的构建,因此以下为整体的流程呈现效果
部署应用
在流水线中docker镜像构建好后,就可以把镜像推到镜像仓库了(通常公司都会有自己的私有仓库),然后通过k8s拉取最新镜像自动部署了,这就是部署的基本流程
deploy_to_k8s:
stage: deploy
image: bitnami/kubectl:latest
only:
- dev
tags:
- testing
needs:
- build_image
before_script:
- echo "Setting up kubectl"
- mkdir -p ~/.kube
- echo "$KUBE_CONFIG" > ~/.kube/config
script:
- echo "Updating Kubernetes deployment"
- kubectl set image deployment/$K8S_DEPLOYMENT_NAME $K8S_CONTAINER_NAME=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA --namespace=$K8S_NAMESPACE
- kubectl rollout status deployment/$K8S_DEPLOYMENT_NAME --namespace=$K8S_NAMESPACE
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
消息通知
等k8s镜像部署阶段完成后代表整个流程就结束了,因此我们可以在流水线的尾部在增加一个通知阶段,通过发送具体的消息来来告知我们的开发人员,当然流水线的执行状态有成功
和失败
2中状态,根据不同的状态来告知
你可以通过HTTP请求将信息发送到消息服务中心,消息中心可以集成到公司用到的应用,这样就会一触即达
消息通知阶段的流水线以上已经展示过了,这里就不再写了,稍微补充下用到的基础镜像:
notice_success:
# 通过curl来发送消息
image: curlimages/curl:7.80.0
# 以下内容自行发挥
2
3
4
执行机制
通常流水线的执行时机都是通过固定的事件触发的,如:分支合并、分支push等等,而对于重要的环境往往需要手动来降低风险,或者说可以更好的决定执行时机;对于一个复杂应用的发布,可能会涉及到很多基础服务的发布,那么对于自动化发布来说会很麻烦,这里简单提一下如何修改执行时机
deploy-job:
stage: deploy
script:
- echo "Deploying the application"
when: manual # 配置为手动触发
2
3
4
5
在往期的文章中都有关于执行时机的介绍,这里只是提醒下如何更好的实践
心跳检测
大多数的应用都会面对很多问题,如:负载、故障等等;而稳定的应用都会有很多监控措施,对于镜像如何检测它是否正常运行呢,最简单的就是通过写一个检测接口,通过一定的频率来调用,根据响应来判断是否正常,这样在镜像拉胯时,可以重新部署
下面我们用NodeJS简单写了一个接口/common/health-check
:
@Controller("/common")
export class CommonController {
// 心跳检测
@Get("/health-check")
healthCheck() {
return "ok";
}
}
2
3
4
5
6
7
8
接着在k8s pod配置文件中添加心跳检测
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-http
spec:
containers:
- name: liveness
image: registry.k8s.io/e2e-test-images/agnhost:2.40
args:
- liveness
livenessProbe:
httpGet:
path: /common/health-check
port: 8080
initialDelaySeconds: 3
periodSeconds: 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
更多玩法请参考探针检测
其他
纯静态前端项目,大部分会配合nginx使用,这里不做演示
线上的应用运行效果如何(内存、负载、响应等等),通常都会在相关的监控后台查看,可以参考k8s可视化资源监控,也可以根据api自行设计监控后台
🎁 本篇文章所用示例项目可以点击这里查看