Gitlab + Jenkins + Harbor + k8s 实现前端持续集成与部署
CI/CD 流程图

前提环境
- 已有 Gitlab
- 已有 Jenkins
- 已有 Harbor
- 已有 K8S
如果还没有以上的环境,先自行搭建。
在 Harbor 中新建一个项目用来管理前端镜像
新建 fe 项目

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

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

准备一个 Web 项目
- 使用 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
|
- 在项目中新建文件 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; } }
|
- 新建 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/
|
- 新建 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
登录 Jenkins 服务,新建任务,新建一个流水线

配置流水线

保存后点击立即构建,测试流水线。流水线将构建出一个镜像并推送到 Harbor。
配置 k8s
创建 my-app 命名空间用于部署 my-app
1
| kubectl create namespace my-app
|
使用 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。
- 配置后端服务地址 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 服务
- 为 my-app 创建 service
1
| kubectl expose deployment/my-app -n my-app
|
- 配置 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 自动化部署
- 在 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
|
查看刚刚创建的 ServiceAccont 的 token
1
| kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep jenkins-user | awk '{print $1}')
|
在 Jenkins 增加一个 ID 为 k8s-test-token 的凭据

修改 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' } } } } } }
|
- Gitlab 自动触发


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