Gitlab + Jenkins + Harbor + k8s 实现前端持续集成与部署

CI/CD 流程图

前提环境

  • 已有 Gitlab
  • 已有 Jenkins
  • 已有 Harbor
  • 已有 K8S

如果还没有以上的环境,先自行搭建。

在 Harbor 中新建一个项目用来管理前端镜像

  1. 新建 fe 项目

  2. 进入 fe 项目,新建机器人账号

  3. 保存令牌信息,将其配置到 Jenkins 中(略)

准备一个 Web 项目

  1. 使用 create-react-app 创建一个 React App,这里就叫 my-app

    步骤参考 https://zh-hans.reactjs.org/docs/create-a-new-react-app.html#create-react-app

1
2
3
npx create-react-app my-app
cd my-app
yarn start
  1. 在项目中新建文件 Nginx.conf,做为 myapp 的 Nginx 配置文件

    配置文件中将 api 开头的请求都代理到了后端服务,具体地址用 k8s 管理

1
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
# Nginx.conf
upstream backend {
server my-app-backend;
}

server {
server_name _;
root /usr/share/nginx/html;

gzip on;
gzip_min_length 1k;
gzip_comp_level 3;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";

location /api/ {
rewrite ^.+api/?(.*)$ /$1 break;
proxy_pass http://backend;
}

location / {
add_header Cache-Control no-cache;
try_files $uri $uri /index.html;
}

location /static {
add_header Cache-Control max-age=2592000;
}
}
  1. 新建 Dockerfile,用于构建 my-app,生成用于部署的 docker 镜像

    使用分阶段构建,有效使用缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Dockerfile
FROM node:12.17 AS builder

WORKDIR /workspace

ADD yarn.lock .
ADD package.json .
RUN yarn

ADD public public
ADD src src
RUN yarn run build


FROM nginx:alpine
COPY Nginx.conf /etc/nginx/conf.d/app.conf
COPY --from=builder /workspace/build /usr/share/nginx/html/
  1. 新建 Jenkinsfile,用于执行 Jenkins 流水线

    10.104.6.214 是 Harbor 服务 IP 地址,10.104.6.215:32567 是 k8s 服务地址,请自行修改为自己的 IP 地址
    使用 package.json 中的 name 字段作为 k8s 命名空间名和容器名
    imageName 是镜像的 tag,这里会将镜像上传到 Harbor 的 fe 项目下
    robot_jenkins-test 是上面创建的 Harbor 机器人账号在 Jenkins 中的配置 ID

    1
    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
    pipeline{
    agent any
    environment {
    branchName = sh(returnStdout: true, script: "echo ${GIT_BRANCH} | sed 's/origin\\///g'").trim()
    pkgInfo = readJSON file: 'package.json'
    pkgName = "${pkgInfo.name}"
    commitId = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
    imageTag = "${branchName}-${commitId}"
    imageName = "10.104.6.214/fe/${pkgName}:${imageTag}"
    }
    stages{
    stage("Build Image"){
    steps {
    retry(3){
    script{
    appImage = docker.build("${imageName}")
    }
    }
    }
    }
    stage("Push Image"){
    steps {
    retry(3){
    script{
    docker.withRegistry('http://10.104.6.214/v2', 'robot_jenkins-test') {
    appImage.push()
    appImage.push('latest')
    }
    }
    }
    }
    }
    }
    }
    将 my-app 提交后推到 Gitlab 上

配置 Jenkins

  1. 登录 Jenkins 服务,新建任务,新建一个流水线

  2. 配置流水线

保存后点击立即构建,测试流水线。流水线将构建出一个镜像并推送到 Harbor。

配置 k8s

  1. 创建 my-app 命名空间用于部署 my-app

    1
    kubectl create namespace my-app
  2. 使用 deployment.yaml 部署 my-app

    查看 Harbor 的 fe 项目中 my-app 镜像的 tag 为 master-673d670

1
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
#  deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app # 部署名称
namespace: my-app # 部署到的命名空间
labels:
app: my-app
spec:
replicas: 1
template:
metadata:
name: my-app
labels:
app: my-app
spec:
containers:
- name: my-app
image: 10.104.6.214/fe/my-app:master-673d670
imagePullPolicy: IfNotPresent
ports:
- name: http-port
containerPort: 80
imagePullSecrets:
- name: harbor-test-fe
restartPolicy: Always
selector:
matchLabels:
app: my-app
1
kubectl apply -f deployment.yaml

harbor-test-fe 是拉取镜像所需要的令牌信息,具体配置可参考 k8s 从 Harbor 拉取镜像

查看 pod

1
kubectl get pods -l app=my-app -n my-app

此时 pod 已经部署成功,但是 Ready 一直是 Fasle,查看容器日志发现以下报错

1
host not found in upstream "my-app-backend" in /etc/nginx/conf.d/app.conf:2

是因为没有配置 my-app-backend 的 DNS。

  1. 配置后端服务地址 my-app-backend

    这里假设后端服务没有部署在 k8s 集群上,而是部署在 IP 地址为 192.16.100.56 的服务器上,端口为 26003

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-backend
namespace: my-app
spec:
ports:
- protocol: TCP
port: 80
targetPort: 26003

---

apiVersion: v1
kind: Endpoints
metadata:
name: my-app-backend
subsets:
- addresses:
- ip: 192.16.100.56
ports:
- port: 26003

部署 service

1
kubectl apply -f service.yaml

删除 pod

1
kubectl delete pods my-app-ddc88c8f9-xzcl7 -n my-app

此时 pod 已经成功运行

暴露 Web 服务

  1. 为 my-app 创建 service
    1
    kubectl expose deployment/my-app -n my-app
  2. 配置 ingress 规则

    my-app.com 是域名,如果没有正式域名,修改本机的 hosts 访问即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: my-app
namespace: my-app
spec:
rules:
- host: my-app.com
http:
paths:
- backend:
serviceName: my-app
servicePort: 80
path: /
pathType: ImplementationSpecific
1
kubectl apply -f ingress.yaml

Jenkins 自动化部署

  1. 在 kube-system 下创建一个 ServiceAccount
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# serviceAccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins-user
namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: jenkins-user
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: jenkins-user
namespace: kube-system
1
kubectl apply -f serviceAccount.yaml
  1. 查看刚刚创建的 ServiceAccont 的 token

    1
    kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep jenkins-user | awk '{print $1}')
  2. 在 Jenkins 增加一个 ID 为 k8s-test-token 的凭据

  3. 修改 Jeninsfile

1
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
pipeline{
agent any
environment {
branchName = sh(returnStdout: true, script: "echo ${GIT_BRANCH} | sed 's/origin\\///g'").trim()
pkgInfo = readJSON file: 'package.json'
pkgName = "${pkgInfo.name}"
commitId = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
imageTag = "${branchName}-${commitId}"
imageName = "10.104.6.214/fe/${pkgName}:${imageTag}"
}
stages{
stage("Build Image"){
steps {
retry(3){
script{
appImage = docker.build("${imageName}")
}
}
}
}
stage("Push Image"){
steps {
retry(3){
script{
docker.withRegistry('http://10.104.6.214/v2', 'robot_jenkins-test') {
appImage.push()
appImage.push('latest')
}
}
}
}
}
stage("Deploy Test"){
steps{
withCredentials([string(credentialsId: 'k8s-test-token', variable: 'k8s_test_token')]) {
retry(3){
sh "curl -X PATCH \
-H \"content-type: application/strategic-merge-patch+json\" \
-H \"Authorization:Bearer $k8s_test_token\" \
-d '{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"${pkgName}\",\"image\":\"${imageName}\"}]}}}}' \
\"http://10.104.6.215:32567/k8s-api/apis/apps/v1/namespaces/${pkgName}/deployments/${pkgName}\""
}
}

}
}
stage('Deploy Production') {
when {
environment name: 'branchName', value: 'master'
}
steps {
script {
timeout(time: 15, unit: 'MINUTES') {
input message: '立即部署到生产环境?', ok: '确认部署生产环境'
}
retry(3) {
echo 'Deploying Production'
}
}
}
}
}
}
  1. Gitlab 自动触发

提交代码到 Gitlab 后将自动触发构建部署