二十一、DevOps:從零建設基于K8s的DevOps平臺(二)
二十一、DevOps:從零建設基于K8s的DevOps平臺(二)
- 二十一、DevOps:從零建設基于K8s的DevOps平臺(二)
1、DevOps 平臺建設
1.1 DevOps 流程
微服務發(fā)版的自動化流水線,一般會有如下步驟:
1、在 GitLab 中創(chuàng)建對應的項目;
2、配置 Jenkins 集成 Kubernetes 集群,后期 Jenkins 的 Slave 將為在 Kubernetes 中動態(tài)創(chuàng)建的 Slave;
3、Jenkins 創(chuàng)建對應的任務(Job),集成該項目的 Git 地址和 Kubernetes 集群;
4、開發(fā)者將代碼提交到 GitLab;
5、如有配置鉤子,推送(Push)代碼會自動觸發(fā) Jenkins 構建,如沒有配置鉤子,需要手動構建;
6、Jenkins 控制 Kubernetes(使用的是 Kubernetes 插件)創(chuàng)建 Jenkins Slave(Pod形式);
7、Jenkins Slave 根據(jù)流水線(Pipeline)定義的步驟執(zhí)行構建,生成交付物;
8、通過 Dockerfile 生成鏡像;
9、將鏡像提送(Push)到私有 Harbor(或者其它的鏡像倉庫);
10、Jenkins 再次控制 Kubernetes 進行最新的鏡像部署;
11、流水線結束刪除 Jenkins Slave。
1.2 DevOps 平臺整體設計

1.3 集群規(guī)劃(學習測試環(huán)境)
| 主機名稱 | 物理IP | 系統(tǒng) | 資源配置 | 說明 |
|---|---|---|---|---|
| k8s-master01 | 192.168.200.50 | Rocky9.4 | 4核4g | Master節(jié)點 |
| k8s-node01 | 192.168.200.51 | Rocky9.4 | 4核4g | Node01節(jié)點 |
| k8s-node02 | 192.168.200.52 | Rocky9.4 | 4核4g | Node02節(jié)點 |
| Harbor | 192.168.200.53 | Rocky9.4 | 2核4g | 鏡像倉庫 |
| Jenkins | 192.168.200.54 | Rocky9.4 | 2核4g | CI/CD流水線 |
| GitLab | 192.168.200.55 | Rocky9.4 | 2核4g | 代碼倉庫 |
1.4 Harbor 私有倉庫
1.4.1 安裝docker
# 添加docker的yum源配置文件
[root@harbor ~]# yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 安裝docker
[root@harbor ~]# yum install docker-ce docker-ce-cli -y
# 啟動docker
[root@harbor ~]# systemctl daemon-reload && systemctl enable --now docker
1.4.2 配置docker-compose
# 設置軟連接
[root@harbor ~]# ln -s /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/
[root@harbor ~]# docker-compose -v
Dcurl -L https://github.com/docker C/compose version v2.36.2
# 批量停止harbor實例
docker-compose down
# 批量啟動harbor實例
docker-compose up -d
1.4.3 安裝 Harbor
# 下載到本地
[root@harbor ~]# wget https://github.com/goharbor/harbor/releases/download/v2.13.1/harbor-offline-installer-v2.13.1.tgz
# 解壓tar包
[root@harbor ~]# tar xf harbor-offline-installer-v2.13.1.tgz
[root@harbor ~]# cd harbor
# 加載配置
[root@harbor harbor]# docker load -i harbor.v2.13.1.tar.gz
# 修改 Harbor 默認配置文件
[root@harbor harbor]# cp harbor.yml.tmpl harbor.yml
[root@harbor harbor]# vim harbor.yml
[root@harbor harbor]# sed -n "5p;10p;13,18p;47p;66p" harbor.yml
hostname: 192.168.200.53
port: 80
# https:
# https port for harbor, default is 443
# port: 443
# The path of cert and key files for nginx
# certificate: /your/certificate/path
# private_key: /your/private/key/path
harbor_admin_password: Harbor12345
data_volume: /data/harbor
- hostname:Harbor 的訪問地址,可以是域名或者 IP,生產推薦使用域名,并且?guī)в凶C書
- https:域名證書的配置,生產環(huán)境需要配置權威證書供 Harbor 使用,否則需要添加
insecure-registry配置,由于是學習環(huán)境,所以本示例未配置證書- harbor_admin_password:賬號密碼按需修改即可,默認為 admin:Harbor12345
- data_volume:Harbor 的數(shù)據(jù)目錄
# 創(chuàng)建 Harbor 數(shù)據(jù)目錄并進行預配置:
[root@harbor harbor]# mkdir /data/harbor -p
# 加載配置啟動
[root@harbor harbor]# ./prepare
# 執(zhí)行安裝:
[root@harbor harbor]# ./install.sh
登錄后,創(chuàng)建一個項目

1.4.4 創(chuàng)建開機自啟動腳本
[root@harbor harbor]# vim /etc/systemd/system/harbor.service
[root@harbor harbor]# cat /etc/systemd/system/harbor.service
[Unit]
Description=Harbor Service
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/usr/local/harbor
ExecStart=/usr/local/bin/docker-compose up -d
ExecStop=/usr/local/bin/docker-compose down
User=root
[Install]
WantedBy=multi-user.target
# 加上執(zhí)行權限
[root@harbor harbor]# chmod +x /etc/systemd/system/harbor.service
# 設置開機自啟動
[root@harbor harbor]# systemctl daemon-reload
[root@harbor harbor]# systemctl enable harbor
1.4.5 docker 配置 insecure registry
如果配置不是 https 協(xié)議,所有的 Kubernetes 節(jié)點的 Docker 都需要添加 insecure-registries 配置
[root@harbor harbor]# vim /etc/docker/daemon.json
[root@harbor harbor]# cat /etc/docker/daemon.json
{
"registry-mirrors": ["https://9upbt3ho.mirror.aliyuncs.com"],
"insecure-registries": ["192.168.200.53"]
}
[root@harbor harbor]# systemctl daemon-reload
[root@harbor harbor]# systemctl restart docker
1.4.6 Containerd 配置 insecure registry (k8s節(jié)點)
如果 Kubernetes 集群采用的是 Containerd 作為的 Runtime,那么 Containerd 也需要配置 insecure registry。
首先生成 Containerd 的配置(如果已經(jīng)生成過,請勿執(zhí)行):
# containerd config default > /etc/containerd/config.toml
# 修改配置(所有k8s節(jié)點都要執(zhí)行)
[root@k8s-master01 ~]# vim /etc/containerd/config.toml
[root@k8s-master01 ~]# sed -n "170,172p" /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."192.168.200.53"]
endpoint = ["http://192.168.200.53"]
# 重啟服務
[root@k8s-master01 ~]# systemctl restart containerd
# 需要把K8s所有的節(jié)點都需要進行更改
# config.toml并不是直接給ctr命令去使用的
# docker拉取的鏡像和containerd沒有任何關系
1.5 Jenkins 安裝
1.5.1 安裝 Docker
# 添加docker的yum源配置文件
[root@jenkins ~]# yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 安裝docker
[root@jenkins ~]# yum install docker-ce docker-ce-cli -y
# 啟動docker
[root@jenkins ~]# systemctl daemon-reload && systemctl enable --now docker
1.5.2 安裝Jenkins
創(chuàng)建 Jenkins 的數(shù)據(jù)目錄,防止容器重啟后數(shù)據(jù)丟失:
[root@jenkins ~]# mkdir /data/jenkins_data -p
[root@jenkins ~]# chmod -R 777 /data/jenkins_data
啟動 Jenkins,并配置管理員賬號密碼為 admin/admin123:
# 拉取鏡像
[root@jenkins ~]# docker pull crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/jenkins:2.504.3-debian-12-r1
# 啟動容器
[root@jenkins ~]# docker run -d --name=jenkins --restart=always -e JENKINS_PASSWORD=admin123 -e JENKINS_USERNAME=admin -e JENKINS_HTTP_PORT_NUMBER=8080 -p 8080:8080 -p 50000:50000 -v /usr/bin/docker:/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock -v /data/jenkins_data:/bitnami/jenkins crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/jenkins:2.504.3-debian-12-r1
# 查看容器
[root@jenkins ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8e44a0ba9584 crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/jenkins:2.504.3-debian-12-r1 "/opt/bitnami/script…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp, 0.0.0.0:50000->50000/tcp, [::]:50000->50000/tcp, 8443/tcp jenkins
其中 8080 端口為 Jenkins Web 界面的端口,50000 是 jnlp 使用的端口,后期 Jenkins Slave 需要使用 50000 端口和 Jenkins 主節(jié)點通信。
查看 Jenkins 日志:
# 查看到這條日志說明 Jenkins 已完成啟動
[root@jenkins ~]# docker logs -f jenkins
....
INFO hudson.lifecycle.Lifecycle#onReady: Jenkins is fully up and running
之后通過 Jenkins 宿主機的 IP+8080 即可訪問 Jenkins

1.5.3 插件安裝
登錄后點擊 Manage Jenkins → Manage Plugins 安裝需要使用的插件:


在安裝之前首先配置國內的插件源,點擊 Advanced,將插件源更改為國內插件源(https://mirrors.huaweicloud.com/jenkins/updates/update-center.json)

點擊 Submit 后在 Available 可以看到所有的可用插件:

Git
Git Parameter
Git Pipeline for Blue Ocean
GitLab
Credentials
Credentials Binding
Blue Ocean
Blue Ocean Pipeline Editor
Blue Ocean Core JS
Web for Blue Ocean
Pipeline SCM API for Blue Ocean
Dashboard for Blue Ocean
Build With Parameters
List Git Branches Parameter
Pipeline
Pipeline: Declarative
Kubernetes
Kubernetes CLI
Kubernetes Credentials
Image Tag Parameter
Docker
Docker Slaves
Docker Pipeline
Role-based Authorization Strategy
最后安裝完記得重啟

等待安裝后自動重啟后,就可以在 Installed 看到已經(jīng)安裝的插件:

至此 Jenkins 和 Jenkins 插件的安裝就完成了。
1.5.4 docker配置insecure registry
[root@jenkins ~]# vim /etc/docker/daemon.json
[root@jenkins ~]# cat /etc/docker/daemon.json
{
"registry-mirrors": ["https://9upbt3ho.mirror.aliyuncs.com"],
"insecure-registries": ["192.168.200.53"]
}
[root@jenkins ~]# systemctl daemon-reload
[root@jenkins ~]# systemctl restart docker
1.6 GitLab 安裝
GitLab 在企業(yè)內經(jīng)常用于代碼的版本控制,也是 DevOps 平臺中尤為重要的一個工具。
1.6.1 關閉機器防火墻和 selinux
# 關閉防火墻
[root@gitlab ~]# systemctl disable --now firewalld
[root@gitlab ~]# systemctl disable --now dnsmasq
# 關閉selinux
[root@gitlab ~]# setenforce 0
[root@gitlab ~]# sed -i 's#SELINUX=enforcing#SELINUX=disabled#g' /etc/sysconfig/selinux
[root@gitlab ~]# sed -i 's#SELINUX=enforcing#SELINUX=disabled#g' /etc/selinux/config
1.6.2 安裝gitlab
[root@gitlab ~]# wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el9/gitlab-ce-17.9.8-ce.0.el9.x86_64.rpm
[root@gitlab ~]# yum install gitlab-ce-17.9.8-ce.0.el9.x86_64.rpm -y
# 修改配置
[root@gitlab ~]# vim /etc/gitlab/gitlab.rb
[root@gitlab ~]# sed -n "32p;2346p" /etc/gitlab/gitlab.rb
external_url 'http://192.168.200.55' # 將 external_url 更改為自己的發(fā)布地址,可以是服務器的 IP,也可以是一個可被解析的域名
prometheus['enable'] = false # 關閉 Prometheus 插件(可選)
# 更改完成后需要重新加載配置文件
[root@gitlab ~]# gitlab-ctl reconfigure
....
Notes:
Default admin account has been configured with following details:
Username: root
Password: You didn't opt-in to print initial root password to STDOUT.
Password stored to /etc/gitlab/initial_root_password. This file will be cleaned up in first reconfigure run after 24 hours.
NOTE: Because these credentials might be present in your log files in plain text, it is highly recommended to reset the password following https://docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password.
gitlab Reconfigured!
之后可以通過 瀏覽器訪 問 GitLab,賬號 root,默認密碼在/etc/gitlab/initial_root_password:

登錄后,開啟 import 功能,可以從外部倉庫導入代碼到 Gitlab:


最后記得點擊保存

1.6.3 創(chuàng)建項目
1、創(chuàng)建一個測試項目,進行一些簡單的測試。首先創(chuàng)建一個組:


組名為 kubernetes,類型為 Private,之后點擊 Create group 即可:

之后在該組下創(chuàng)建一個 Project:

選擇創(chuàng)建一個空的項目:

輸入項目名稱,然后點擊 Create project 即可:

1.6.4 配置密鑰
之后可以將 k8s-master 服務器(任意機器均可)的 key 導入到 GitLab
# 首先生成密鑰(如有可以無需生成):
[root@k8s-master01 ~]# ssh-keygen -t rsa -C "1773464408@qq.com"
# 將公鑰的內容放在 GitLab 中即可:
[root@k8s-master01 ~]# cat .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDJTvF8NDvX/SKxS1z7j/SdqInCB2A3kfuRsOn3c/j962H+yN/0SfRXShaSpHzvrVPNQMtiEr4gFQ4UKgpcWUyyDeaCyYRDrUxZ96IDoY2KoTser5o9dtfmnYaKeP7koewINFesPj59Ur2pATks7SYoT59o7UGKrShcedgo23dI0pvJmNaYKOlLeYocbaCY31g76SCZ44jVmv92smU9L+rVS15FT6hTjMO194vflbhLQ5p5nyaWRORYHuHxTbe5YHpMmEQquLnvMvhAr9ngzCdeMNyVtpQx2ZcbR9YmB6WiL6VsGqyPywkb2rU9G4I0qW8bg+aI1tTnjlXCEGKCNV7rxqNx3h0vRADTlYpKeeh1Ysrt+5+CibYJj3KNfY1dXLFOCCDc6YIFRX/0jFk/7RHe2r7knsqajc1ytt0BdDYb6GBeckDhIL4WGiKI3LtHI4LdXammTAf9wwIv7ZEEyuzAHd71AKx/JTKcA+jmGGy64j2i8Xzpk9UyWuhhxOwwJKc= 1773464408@qq.com
在 GitLab 找到 Profile:

之后在 SSH Keys 添加公鑰:



添加后就可以在服務器上拉取代碼:
[root@k8s-master01 ~]# yum install git -y
[root@k8s-master01 ~]# git config --global user.email 1773464408@qq.com
[root@k8s-master01 ~]# git config --global user.name "yunwei"
[root@k8s-master01 ~]# git clone git@192.168.200.55:kubernetes/test.git
1.7 Jenkins 憑證 Credentials
Harbor 的賬號密碼、Kubernetes 的證書、GitLab 的私鑰均使用 Jenkins 的 Credentials 管理。
1.7.1 配置 Harbor 賬號密碼
首先點擊 Manage Jenkins,之后點擊 Credentials:



- Username:Harbor 或者其它平臺的用戶名
- Password:Harbor 或者其它平臺的密碼
- ID:該憑證的 ID
- Description:證書的描述
1.7.2 配置 Kubernetes 證書
首先需要找到集群中的 KUBECONFIG,一般是 kubectl 節(jié)點的~/.kube/config 文件,或者是 KUBECONFIG 環(huán)境變量所指向的文件。
接下來只需要把證書文件放置于 Jenkins 的 Credentials 中即可,點擊 Add Credentials,類型選擇為 Secret file:


1.7.3 配置 GitLab Key
前面將k8s的公鑰放到了gitlab上面,可以拉取上面的代碼;現(xiàn)在把k8s的私鑰放到jenkin上面,現(xiàn)在jenkins也就可以拉取gitlab上面的代碼
# 私鑰查詢
[root@k8s-master01 ~]# cat .ssh/id_rsa
點擊 Add Credentials,類型選擇為 SSH Username with private key:


至此我們所有的憑證都添加好了

1.8 配置 Agent
通常情況下,Jenkins Slave 會通過 Jenkins Master 節(jié)點的 50000 端口與之通信,所以需要開啟 Agent 的 50000 端口。
點擊 Manage Jenkins,然后點擊 Security:

在安全配置下方找到 Agents,點擊 Fixed,輸入 50000 即可:

實際使用時,如果不需要把整個 Kubernetes 集群的節(jié)點都充當創(chuàng)建 Jenkins Slave Pod 的節(jié)點,可以選擇任意的一個或多個節(jié)點作為創(chuàng)建 Slave Pod 的節(jié)點。
假設 k8s-node01 作為 Slave 節(jié)點(可選):
[root@k8s-master01 ~]# kubectl label node k8s-node01 build=true
1.9 Jenkins 配置 Kubernetes 多集群
首先點擊 Manage Jenkins,之后點擊 Clouds:

點擊 New cloud:

之后輸入集群的名稱,一般按照可識別的名稱即可,比如現(xiàn)在是學習環(huán)境的 Kubernetes 可以叫做kubernetes-study,選擇 Kubernetes,之后點擊 Create:

在 Credentials(憑據(jù))處選擇之前添加 Kubernetes 證書,選擇后點擊 Test Connection,最后在憑證下方即可看到能否正常連接的結果:

最后點擊 Save 即可,添加完 Kubernetes 后,在 Jenkinsfile 的 Agent 中,就可以選擇該集群作為創(chuàng)建 Slave 的集群。
如果想要添加多個集群,重復上述的步驟即可。

2、自動化流水線設計
2.1 自動化流水線統(tǒng)一模板
針對如上的邏輯,可以設計一個流水線模板,只需要變更一些參數(shù)即可。
首先是頂層的 Agent,定義的是 Kubernetes 的 Pod 作為 Jenkins 的 Slave:
agent {
kubernetes { # 定義使用 Kubernetes 作為 agent
cloud 'kubernetes-study'
slaveConnectTimeout 1200
# 將 workspace 改成 PVC,用于持久化工作目錄,claimName 為創(chuàng)建的 PVC 名稱
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "pipeline-cache", readOnly: false)
yaml '''
apiVersion: v1
kind: Pod
spec:
restartPolicy: "Never"
nodeSelector: # 固定節(jié)點部署,可選
build: "true"
securityContext: {}
volumes:
- name: docker-registry-config # docker 認證信息 volume
configMap:
name: docker-registry-config
- name: "localtime"
hostPath:
path: "/usr/share/zoneinfo/Asia/Shanghai"
- name: cache # 構建緩存 PVC
persistentVolumeClaim:
claimName: cache
readonly: false
containers:
# jnlp 容器,和 Jenkins 主節(jié)點通信
- name: jnlp
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/jnlp-agent-docker:latest"
imagePullPolicy: IfNotPresent
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
# build 容器,包含執(zhí)行構建的命令,比如 Java 的需要 mvn 構建,就可以用一個 maven 的鏡像
# 容器的名字,流水線的 stage 可以直接使用該名字
- name: "build"
# 使用 Maven 鏡像,包含 mvn 工具,NodeJS 可以用 node 的鏡像,根據(jù)實際情況修改
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/maven:3.5.3"
imagePullPolicy: "IfNotPresent"
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
# 由于 Java 編譯時,會把依賴的插件緩存到~/.m2目錄,所以把該目錄也進行緩存,如果其他語言的編譯也需要緩存,按照該方式配置即可
- name: "cache"
mountPath: "/root/.m2/"
readOnly: false
# 用于生成鏡像的容器,可以是 Docker 或者是其他的可以用于構建鏡像的工具
- name: "kaniko"
# 本示例采用 Kaniko 制作鏡像
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/kaniko-executor:debug"
imagePullPolicy: "IfNotPresent"
command:
- "sleep"
args:
- "99d"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
# 掛載 docker 配置文件,用于 Kaniko 鏈接鏡像倉庫
- name: "docker-registry-config"
mountPath: "/kaniko/.docker"
# 發(fā)版容器,因為最終是發(fā)版至 Kubernetes 的,所以需要有一個 kubectl 命令或者是其他的發(fā)版工具也可以,比如 helm
- name: "kubectl"
# 發(fā)版的鏡像根據(jù)實際需求進行變更
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/kubectl:devops"
imagePullPolicy: "IfNotPresent"
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
'''
}
之后看一下 Jenkinsfile 最后的環(huán)境變量和 parameters 的配置:
# 定義一些全局的環(huán)境變量
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.200.53" # Harbor 地址
REGISTRY_DIR = "kubernetes" # Harbor 的項目目錄
IMAGE_NAME = "spring-boot-project" # 鏡像的名稱
NAMESPACE = "kubernetes" # 該應用在 Kubernetes 中的命名空間
TAG = "" # 鏡像的 Tag,在此用 BUILD_TAG+COMMIT_ID 組成
GIT_URL = "git@192.168.200.55:kubernetes/spring-boot-project.git" # 代碼地址
}
parameters {
# 之前講過一些 choice、input 類型的參數(shù),本次使用的是 GitParameter 插件
# 該字段會在 Jenkins 頁面生成一個選擇分支的選項
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
}
接下來是拉代碼的 stage,這個 stage 是一個并行的 stage,因為考慮了該流水線是手動觸發(fā)還是觸發(fā):
stage('Pulling Code') {
parallel {
stage('Pulling Code by Jenkins') {
when {
expression {
# 假如 env.gitlabBranch 為空,則該流水線為手動觸發(fā),那么就會執(zhí)行該 stage,如果不為空則會執(zhí)行同級的另外一個 stage
env.gitlabBranch == null
}
}
steps {
# 這里使用的是 git 插件拉取代碼,BRANCH 變量取自于前面介紹的 parameters 配置
# git@xxxxxx:root/spring-boot-project.git 代碼地址
# credentialsId: 'gitlab-key',之前創(chuàng)建的拉取代碼的 key
git(changelog: true, poll: true, url: "${GIT_URL}", branch: "${BRANCH}", credentialsId: 'gitlab-key')
script {
# 定義一些變量用于生成鏡像的 Tag
# 獲取最近一次提交的 Commit ID
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
# 賦值給 TAG 變量,后面的 docker build 可以取到該 TAG 的值
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${BRANCH}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
stage('Pulling Code by trigger') {
when {
expression {
# 如果 env.gitlabBranch 不為空,說明該流水線是通過 webhook 觸發(fā),則此時執(zhí)行該 stage,上述的 stage 不再執(zhí)行。此時 BRANCH 變量為空
env.gitlabBranch != null
}
}
steps {
# 以下配置和上述一致,只是此時 branch: env.gitlabBranch 取的值為 env.gitlabBranch
git(url: "${GIT_URL}", branch: env.gitlabBranch, changelog: true, poll: true, credentialsId: 'gitlab-key')
script {
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${env.gitlabBranch}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
}
}
代碼拉下來后,就可以執(zhí)行構建命令,由于本次實驗是 Java 示例,所以需要使用 mvn 命令進行構建:
stage('Building') {
steps {
container(name: 'build') { # 使用 Pod 模板里面的 build 容器進行構建
sh """ # 編譯命令,需要根據(jù)自己項目的實際情況進行修改,可能會不一致
curl repo.maven.apache.org
mvn clean install -DskipTests
ls target/*
"""
}
}
}
生成編譯產物后,需要根據(jù)該產物生成對應的鏡像,此時可以使用 Pod 模板的 kaniko 容器:
stage('Build for creating image') {
steps {
container(name: 'kaniko') { # 指定使用 kaniko 容器
sh """
# 執(zhí)行 build 命令,Dockerfile 會在下一小節(jié)創(chuàng)建,也是放在代碼倉庫,和 Jenkinsfile 同級
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}
最后一步就是將該鏡像發(fā)版至 Kubernetes 集群中,此時使用的是包含 kubectl 命令的容器:
stage('Deploying to K8s') {
environment { # 獲取連接 Kubernetes 集群證書
MY_KUBECONFIG = credentials('STUDY_CLUSTER_CONFIG')
}
steps {
container(name: 'kubectl'){ # 指定使用 kubectl 容器
sh """ # 直接 set 更改 Deployment 的鏡像即可
kubectl --kubeconfig $MY_KUBECONFIG set image deploy -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}
}
2.2 Workspace 持久化
Jenkins 在構建時,會產生一些依賴文件,這些文件最好進行持久化存儲,防止重復下載。接下來創(chuàng)建一個 PVC 用于流水線工作目錄及依賴文件的數(shù)據(jù)持久化:
[root@k8s-master01 ~]# vim pipeline-cache-pvc.yaml
[root@k8s-master01 ~]# cat pipeline-cache-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pipeline-cache
namespace: default
spec:
resources:
requests:
storage: 200Gi
volumeMode: Filesystem
storageClassName: nfs-csi
accessModes:
- ReadWriteMany
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cache
namespace: default
spec:
resources:
requests:
storage: 200Gi
volumeMode: Filesystem
storageClassName: nfs-csi
accessModes:
- ReadWriteMany
創(chuàng)建后查看 PVC:
[root@k8s-master01 ~]# kubectl create -f pipeline-cache-pvc.yaml
[root@k8s-master01 ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
cache Bound pvc-5a714f8e-3d1e-432b-8152-4f734a3c95c8 200Gi RWX nfs-csi <unset> 74s
pipeline-cache Bound pvc-375e62f3-463c-449d-87e3-f215b37aedae 200Gi RWX nfs-csi <unset> 74s
注意此時需要在 NFS 把該 PVC 設置為 777 權限:
[root@habor ~]# chmod -R 777 /data/nfs/pvc-*
2.3 Kaniko 配置文件處理
首先登錄需要推送鏡像的目標倉庫,生成 docker 的配置文件:
[root@k8s-master01 ~]# docker login 192.168.200.53
查看生成的配置文件:
[root@k8s-master01 ~]# cat ~/.docker/config.json
{
"auths": {
"192.168.200.53": {
"auth": "YWRtaW46SGFyYm9yMTIzNDU="
}
}
}
創(chuàng)建 ConfigMap,用于掛載至 Jenkins Slave 的 Kaniko 容器:
[root@k8s-master01 ~]# kubectl create cm docker-registry-config --from-file=config.json=/root/.docker/config.json
[root@k8s-master01 ~]# kubectl get cm docker-registry-config
NAME DATA AGE
docker-registry-config 1 15s
3、自動化構建 Java 應用
3.1 創(chuàng)建 Java 測試用例
示例項目可以從 https://gitee.com/dukuan/spring-boot-project.git 找到該項目(也可以使用公司的 Java 項目也是一樣的)。
接下來將該項目導入到自己的 GitLab 中。首先找到之前創(chuàng)建的 Kubernetes 組

然后點擊 New Project:

選擇 Import Project:

點擊 Repo by URL,在 Git repository URL 輸入示例地址,然后點擊 Create Project 即可:


導入后,如下所示:

3.2 定義 Jenkinsfile
Jenkinsfile 是用來保存 Pipeline 代碼的文件,通常用代碼倉庫管理,或者直接放置于服務的代碼倉庫中。
Jenkinsfile 放置于代碼倉庫中,有以下好處:
- 方便對流水線上的代碼進行復查/迭代;
- 對管道進行審計跟蹤;
- 流水線真正的源代碼能夠被項目的多個成員查看和編輯。
接下來再 GitLab 的源代碼中添加 Jenkinsfile。首先點擊代碼首頁的“+”號,然后點擊 New file:

在窗口中,添加通過模板更改后的 Pipeline,并且命名為 Jenkinsfile:


完整配置
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "pipeline-cache", readOnly: false)
yaml '''
apiVersion: v1
kind: Pod
spec:
restartPolicy: "Never"
nodeSelector:
build: "true"
securityContext: {}
volumes:
- name: docker-registry-config
configMap:
name: docker-registry-config
- name: "localtime"
hostPath:
path: "/usr/share/zoneinfo/Asia/Shanghai"
- name: cache
persistentVolumeClaim:
claimName: cache
readonly: false
containers:
- name: jnlp
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/jnlp-agent-docker:latest"
imagePullPolicy: IfNotPresent
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
- name: "build"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/maven:3.5.3"
imagePullPolicy: "IfNotPresent"
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
- name: "cache"
mountPath: "/root/.m2/"
readOnly: false
- name: "kaniko"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/kaniko-executor:debug"
imagePullPolicy: "IfNotPresent"
command:
- "sleep"
args:
- "99d"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
- name: "docker-registry-config"
mountPath: "/kaniko/.docker"
- name: "kubectl"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/kubectl:devops"
imagePullPolicy: "IfNotPresent"
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
'''
}
}
stages {
stage('Pulling Code') {
parallel {
stage('Pulling Code by Jenkins') {
when {
expression {
env.gitlabBranch == null
}
}
steps {
git(changelog: true, poll: true, url: "${GIT_URL}", branch: "${BRANCH}", credentialsId: 'gitlab-key')
script {
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${BRANCH}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
stage('Pulling Code by trigger') {
when {
expression {
env.gitlabBranch != null
}
}
steps {
git(url: "${GIT_URL}", branch: env.gitlabBranch, changelog: true, poll: true, credentialsId: 'gitlab-key')
script {
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${env.gitlabBranch}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
}
}
stage('Building') {
steps {
container(name: 'build') {
sh """
curl repo.maven.apache.org
mvn clean install -DskipTests
ls target/*
"""
}
}
}
stage('Build for creating image') {
steps {
container(name: 'kaniko') {
sh """
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}
stage('Deploying to K8s') {
environment {
MY_KUBECONFIG = credentials('STUDY_CLUSTER_CONFIG')
}
steps {
container(name: 'kubectl'){
sh """
kubectl --kubeconfig $MY_KUBECONFIG set image deploy -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}
}
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.200.53"
REGISTRY_DIR = "kubernetes"
IMAGE_NAME = "spring-boot-project"
NAMESPACE = "kubernetes"
TAG = ""
GIT_URL = "git@192.168.200.55:kubernetes/spring-boot-project.git"
}
parameters {
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
}
}
3.3 定義 Dockerfile
在執(zhí)行流水線過程時,需要將代碼的編譯產物做成鏡像。而本次示例是 Java 項目,只需要把 Jar 包放在有 Jre 環(huán)境的鏡像中,然后啟動該 Jar 包即可:
# 基礎鏡像可以按需修改,可以更改為公司自有鏡像
FROM crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/jre:8u211-data
# jar 包名稱改成實際的名稱,本示例為 spring-cloud-eureka-0.0.1-SNAPSHOT.jar
COPY target/spring-cloud-eureka-0.0.1-SNAPSHOT.jar ./
# 啟動 Jar 包
CMD java -jar spring-cloud-eureka-0.0.1-SNAPSHOT.jar

3.4 定義 Kubernetes 資源
本示例在 GitLab 創(chuàng)建的 Group 為 kubernetes,可以將其認為是一個項目,同一個項目可以部署至 Kubernetes 集群中同一個 Namespace 中,本示例為 kubernetes 命名空間。
由于使用的是私有倉庫,因此也需要先配置拉取私有倉庫鏡像的密鑰:
[root@k8s-master01 ~]# kubectl create ns kubernetes
[root@k8s-master01 ~]# kubectl create secret docker-registry harborkey --docker-server=192.168.200.53 --docker-username=admin --docker-password=Harbor12345 --docker-email=1773464408@qq.com -n kubernetes
[root@k8s-master01 ~]# vim spring-boot-project.yaml
[root@k8s-master01 ~]# cat spring-boot-project.yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: spring-boot-project
name: spring-boot-project
namespace: kubernetes
spec:
ports: # 端口按照實際情況進行修改
- name: web
port: 8761
protocol: TCP
targetPort: 8761
selector:
app: spring-boot-project
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
creationTimestamp: null
name: spring-boot-project
namespace: kubernetes
spec:
ingressClassName: nginx
rules:
- host: spring-boot-project.test.com
http:
paths:
- backend:
service:
name: spring-boot-project
port:
number: 8761
path: /
pathType: ImplementationSpecific
status:
loadBalancer: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: spring-boot-project # Deployment 標簽,和流水線的 set -l 一致
name: spring-boot-project # Deployment 名稱
namespace: kubernetes
spec:
replicas: 1
selector:
matchLabels:
app: spring-boot-project # Pod 的標簽
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: spring-boot-project # Pod 的標簽
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- spring-boot-project
topologyKey: kubernetes.io/hostname
weight: 100
containers:
- env:
- name: TZ
value: Asia/Shanghai
- name: LANG
value: C.UTF-8
image: nginx # 此處使用的 nginx 作為原始的鏡像,通過 Jenkins 構建并發(fā)版后,變成 Java 應用的鏡像
imagePullPolicy: IfNotPresent
lifecycle: {}
livenessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 8761 # 端口號和健康檢查按照實際情況進行修改
timeoutSeconds: 2
name: spring-boot-project # 容器的名稱,需要和流水線 set 命令的容器名稱一致
ports:
- containerPort: 8761 # 端口號按照實際情況進行修改
name: web
protocol: TCP
readinessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 8761 # 端口號和健康檢查按照實際情況進行修改
timeoutSeconds: 2
resources: # 資源請求按照實際情況修改
limits:
cpu: 994m
memory: 1170Mi
requests:
cpu: 10m
memory: 55Mi
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: harborkey # Harbor 倉庫密鑰,需要和上述創(chuàng)建的 Secret 一致
restartPolicy: Always
securityContext: {}
serviceAccountName: default
創(chuàng)建該資源 (Pod 無法啟動請忽略):
[root@k8s-master01 ~]# kubectl create -f spring-boot-project.yaml
[root@k8s-master01 ~]# kubectl get -f spring-boot-project.yaml
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/spring-boot-project ClusterIP 10.107.107.79 <none> 8761/TCP 11s
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/spring-boot-project <none> spring-boot-project.test.com 80 11s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/spring-boot-project 0/1 1 0 10s
3.5 創(chuàng)建 Jenkins 任務(Job)
1、單擊首頁的創(chuàng)建任務(Job)選項(或者是 New Item)并配置任務信息,如圖所示:

2、輸入的 Job 的名稱(一般和 GitLab 的倉庫名字一致,便于區(qū)分),類型為 Pipeline,最后點擊 OK 即可:


3、如果有如下報錯,關閉 Host Key 校驗:

去安全里禁用主機密鑰驗證,直接信任所有連接(盡在內網(wǎng)可信環(huán)境中使用)

4、創(chuàng)建完成后,點擊 Build Now(由于 Jenkins 參數(shù)由 Jenkinsfile 生成,所以第一次執(zhí)行流水線會失敗):

5、第一次構建結束后,可以看到 Build Now 變成了 Build with Parameters。點擊 Build with Parameters 后,可以讀取到 Git 倉庫的分支,之后可以選擇分支進行手動構建(后面的章節(jié)會介紹自動觸發(fā)):

6、選擇分支,之后點擊 Build,然后點擊進度條即可看到構建日志:

構建日志上部分為創(chuàng)建 Pod 的日志,可以看到 Pod 為 Agent 指定的 Pod

7、此時在 Kubernetes 中,會創(chuàng)建一個 Jenkins Slave 的 Pod:
[root@k8s-master01 ~]# kubectl get po
NAME READY STATUS RESTARTS AGE
spring-boot-project-2-h155p-d68tz-jfbf2 0/4 ContainerCreating 0 84s
8、待 Pod 啟動后,Pipeline 任務就會執(zhí)行,同時也可以點擊 Blue Ocean 更加直觀的流程:

9、如果是 Java 應用,同時可以看到 mvn 編譯的過程:

編譯結束后,可以看到制作鏡像的日志:

最后為發(fā)版至 Kubernetes:

10、Finished:SUCCESS 說明該流水線正常結束。接下來可以查看 Deployment 的鏡像:
[root@k8s-master01 ~]# kubectl get deploy -n kubernetes spring-boot-project -oyaml | grep "image:"
image: 192.168.200.53/kubernetes/spring-boot-project:jenkins-spring-boot-project-2-3ed977e
[root@k8s-master01 ~]# kubectl get po -n kubernetes
NAME READY STATUS RESTARTS AGE
spring-boot-project-65b5fb956d-qrlc4 1/1 Running 0 13m

12、如果配置了域名,可以通過 域名訪問(或者通過 Service 訪問):

4、 自動化構建 Vue/H5 前端應用
本節(jié)介紹自動化構建 Vue/H5 應用,其構建方式和自動化構建 Java 基本相同,重點是更改 Deployment、Jenkinsfile 和 Dockerfile 即可。
前端應用測試項目地址:https://gitee.com/dukuan/vue-project.git,可以參考 Java 小節(jié)的方式,
導入前端項目到 GitLab 中,當然也可以使用公司自己的項目。
4.1 定義 Jenkinsfile
Jenkinsfile 和 Java 項目并無太大區(qū)別,需要更改的位置如下:
# 編譯鏡像改為 NodeJS
name: "build"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/node:lts"
imagePullPolicy: "IfNotPresent"
# 構建命令改為 npm
npm install --registry=https://registry.npmmirror.com/
npm run build
# 代碼地址和服務名稱
IMAGE_NAME = "vue-project"
NAMESPACE = "kubernetes"
TAG = ""
GIT_URL = "git@192.168.200.55:kubernetes/vue-project.git"
添加 Jenkinsfile 至項目根目錄

完整配置
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "pipeline-cache", readOnly: false)
yaml '''
apiVersion: v1
kind: Pod
spec:
restartPolicy: "Never"
nodeSelector:
build: "true"
securityContext: {}
volumes:
- name: docker-registry-config
configMap:
name: docker-registry-config
- name: "localtime"
hostPath:
path: "/usr/share/zoneinfo/Asia/Shanghai"
- name: cache
persistentVolumeClaim:
claimName: cache
readonly: false
containers:
- name: jnlp
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/jnlp-agent-docker:latest"
imagePullPolicy: IfNotPresent
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
- name: "build"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/node:lts"
imagePullPolicy: "IfNotPresent"
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
- name: "cache"
mountPath: "/root/.m2/"
readOnly: false
- name: "kaniko"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/kaniko-executor:debug"
imagePullPolicy: "IfNotPresent"
command:
- "sleep"
args:
- "99d"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
- name: "docker-registry-config"
mountPath: "/kaniko/.docker"
- name: "kubectl"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/kubectl:devops"
imagePullPolicy: "IfNotPresent"
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
'''
}
}
stages {
stage('Pulling Code') {
parallel {
stage('Pulling Code by Jenkins') {
when {
expression {
env.gitlabBranch == null
}
}
steps {
git(changelog: true, poll: true, url: "${GIT_URL}", branch: "${BRANCH}", credentialsId: 'gitlab-key')
script {
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${BRANCH}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
stage('Pulling Code by trigger') {
when {
expression {
env.gitlabBranch != null
}
}
steps {
git(url: "${GIT_URL}", branch: env.gitlabBranch, changelog: true, poll: true, credentialsId: 'gitlab-key')
script {
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${env.gitlabBranch}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
}
}
stage('Building') {
steps {
container(name: 'build') {
sh """
npm install --registry=https://registry.npmmirror.com/
npm run build
"""
}
}
}
stage('Build for creating image') {
steps {
container(name: 'kaniko') {
sh """
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}
stage('Deploying to K8s') {
environment {
MY_KUBECONFIG = credentials('STUDY_CLUSTER_CONFIG')
}
steps {
container(name: 'kubectl'){
sh """
kubectl --kubeconfig $MY_KUBECONFIG set image deploy -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}
}
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.200.53"
REGISTRY_DIR = "kubernetes"
IMAGE_NAME = "vue-project"
NAMESPACE = "kubernetes"
TAG = ""
GIT_URL = "git@192.168.200.55:kubernetes/vue-project.git"
}
parameters {
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
}
}
4.2 定義 Dockerfile
前端應用構建后一般會在 dist 文件下產生 html 文件,只需要拷貝到 nginx 的根目錄下即可:
FROM crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/nginx:1.15
COPY dist/* /usr/share/nginx/html/

4.3 定義 Kubernetes 資源
對于 Kubernetes 的資源也是類似的,只需要更改資源名稱和端口號即可(加粗部分):
[root@k8s-master01 ~]# vim vue-project.yaml
[root@k8s-master01 ~]# cat vue-project.yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: vue-project
name: vue-project
namespace: kubernetes
spec:
ports:
- name: web
port: 80
protocol: TCP
targetPort: 80
selector:
app: vue-project
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
creationTimestamp: null
name: vue-project
namespace: kubernetes
spec:
ingressClassName: nginx
rules:
- host: vue-project.test.com
http:
paths:
- backend:
service:
name: vue-project
port:
number: 80
path: /
pathType: ImplementationSpecific
status:
loadBalancer: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: vue-project
name: vue-project
namespace: kubernetes
spec:
replicas: 1
selector:
matchLabels:
app: vue-project
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: vue-project
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- vue-project
topologyKey: kubernetes.io/hostname
weight: 100
containers:
- env:
- name: TZ
value: Asia/Shanghai
- name: LANG
value: C.UTF-8
image: nginx
imagePullPolicy: IfNotPresent
lifecycle: {}
livenessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 80
timeoutSeconds: 2
name: vue-project
ports:
- containerPort: 80
name: web
protocol: TCP
readinessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 80
timeoutSeconds: 2
resources:
limits:
cpu: 994m
memory: 1170Mi
requests:
cpu: 10m
memory: 55Mi
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: harborkey
restartPolicy: Always
securityContext: {}
serviceAccountName: default
創(chuàng)建該資源 (Pod 無法啟動請忽略):
[root@k8s-master01 ~]# kubectl create -f vue-project.yaml
[root@k8s-master01 ~]# kubectl get -f vue-project.yaml
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vue-project ClusterIP 10.96.136.68 <none> 80/TCP 6s
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/vue-project <none> vue-project.test.com 80 5s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/vue-project 0/1 1 0 5s
4.4 創(chuàng)建 Jenkins Job
創(chuàng)建和構建 Job 和 Java 步驟類似,只需要變更名稱和倉庫地址即可,并且可以通過之前的 Job 進行復制:


執(zhí)行構建:

構建成功如下所示:

發(fā)布成功后可以在瀏覽器 訪問該域名(或者使用 Service 訪問):

5、自動化構建 Golang 項目
上述演示了 Java 和前端應用的自動化,接下來演示一下對于 Golang 的自動化構建,本次示例的代碼地址:https://gitee.com/dukuan/go-project.git。
5.1 定義 Jenkinsfile
本次示例的 Jenkinsfile 和之前的也無太大區(qū)別,需要的更改的位置是構建容器的鏡像、緩存目錄、Git 地址和項目名稱:
# 構建鏡像改為 golang,需要根據(jù)實際情況更改版本
name: "build"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/golang:1.15"
imagePullPolicy: "IfNotPresent"
...
volumeMounts:
...
- name: "cache"
mountPath: "/go/pkg/" # 緩存目錄為/go/pkg/,執(zhí)行 go build 時下載的依賴包會緩存在該目錄
readOnly: false
# 構建命令
export GO111MODULE=on
export CGO_ENABLED=0
go env -w GOPROXY=https://goproxy.cn,direct
go build
# 代碼地址和服務名稱
IMAGE_NAME = "go-project"
NAMESPACE = "kubernetes"
TAG = ""
GIT_URL = "git@192.168.200.55:kubernetes/go-project.git"

完整文件:
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "pipeline-cache", readOnly: false)
yaml '''
apiVersion: v1
kind: Pod
spec:
restartPolicy: "Never"
nodeSelector:
build: "true"
securityContext: {}
volumes:
- name: docker-registry-config
configMap:
name: docker-registry-config
- name: "localtime"
hostPath:
path: "/usr/share/zoneinfo/Asia/Shanghai"
- name: cache
persistentVolumeClaim:
claimName: cache
readonly: false
containers:
- name: jnlp
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/jnlp-agent-docker:latest"
imagePullPolicy: IfNotPresent
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
- name: "build"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/golang:1.15"
imagePullPolicy: "IfNotPresent"
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
- name: "cache"
mountPath: "/go/pkg/"
readOnly: false
- name: "kaniko"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/kaniko-executor:debug"
imagePullPolicy: "IfNotPresent"
command:
- "sleep"
args:
- "99d"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
- name: "docker-registry-config"
mountPath: "/kaniko/.docker"
- name: "kubectl"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/kubectl:devops"
imagePullPolicy: "IfNotPresent"
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
volumeMounts:
- name: "localtime"
mountPath: "/etc/localtime"
readOnly: false
'''
}
}
stages {
stage('Pulling Code') {
parallel {
stage('Pulling Code by Jenkins') {
when {
expression {
env.gitlabBranch == null
}
}
steps {
git(changelog: true, poll: true, url: "${GIT_URL}", branch: "${BRANCH}", credentialsId: 'gitlab-key')
script {
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${BRANCH}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
stage('Pulling Code by trigger') {
when {
expression {
env.gitlabBranch != null
}
}
steps {
git(url: "${GIT_URL}", branch: env.gitlabBranch, changelog: true, poll: true, credentialsId: 'gitlab-key')
script {
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = BUILD_TAG + '-' + COMMIT_ID
println "Current branch is ${env.gitlabBranch}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
}
}
}
}
}
stage('Building') {
steps {
container(name: 'build') {
sh """
export GO111MODULE=on
export CGO_ENABLED=0
go env -w GOPROXY=https://goproxy.cn,direct
go build
"""
}
}
}
stage('Build for creating image') {
steps {
container(name: 'kaniko') {
sh """
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}
stage('Deploying to K8s') {
environment {
MY_KUBECONFIG = credentials('STUDY_CLUSTER_CONFIG')
}
steps {
container(name: 'kubectl'){
sh """
kubectl --kubeconfig $MY_KUBECONFIG set image deploy -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}
}
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.200.53"
REGISTRY_DIR = "kubernetes"
IMAGE_NAME = "go-project"
NAMESPACE = "kubernetes"
TAG = ""
GIT_URL = "git@192.168.200.55:kubernetes/go-project.git"
}
parameters {
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
}
}
5.2 定義 Dockerfile
和之前不一樣的地方是,Golang 編譯后生成的是一個二進制文件,可以直接執(zhí)行,所以底層鏡像設置為 alpine 或者其它的小鏡像即可:
FROM crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/alpine-glibc:alpine-3.9
# 如果定義了單獨的配置文件,可能需要拷貝到鏡像中
# COPY conf/ ./conf
# 包名按照實際情況進行修改
COPY ./go-project ./
# 啟動該應用
ENTRYPOINT [ "./go-project"]

5.3 定義 Kubernetes 資源
[root@k8s-master01 ~]# vim go-project.yaml
[root@k8s-master01 ~]# cat go-project.yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: go-project
name: go-project
namespace: kubernetes
spec:
ports:
- name: web
port: 8080
protocol: TCP
targetPort: 8080
selector:
app: go-project
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
creationTimestamp: null
name: go-project
namespace: kubernetes
spec:
ingressClassName: nginx
rules:
- host: go-project.test.com
http:
paths:
- backend:
service:
name: go-project
port:
number: 8080
path: /
pathType: ImplementationSpecific
status:
loadBalancer: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: go-project
name: go-project
namespace: kubernetes
spec:
replicas: 1
selector:
matchLabels:
app: go-project
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: go-project
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- go-project
topologyKey: kubernetes.io/hostname
weight: 100
containers:
- env:
- name: TZ
value: Asia/Shanghai
- name: LANG
value: C.UTF-8
image: nginx
imagePullPolicy: IfNotPresent
lifecycle: {}
livenessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 8080
timeoutSeconds: 2
name: go-project
ports:
- containerPort: 8080
name: web
protocol: TCP
readinessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 8080
timeoutSeconds: 2
resources:
limits:
cpu: 994m
memory: 1170Mi
requests:
cpu: 10m
memory: 55Mi
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: harborkey
restartPolicy: Always
securityContext: {}
serviceAccountName: default
創(chuàng)建該資源 (Pod 無法啟動請忽略):
[root@k8s-master01 ~]# kubectl create -f go-project.yaml
[root@k8s-master01 ~]# kubectl get -f go-project.yaml
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/go-project ClusterIP 10.97.238.110 <none> 8080/TCP 8s
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/go-project <none> go-project.test.com 80 8s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/go-project 0/1 1 0 8s
5.4 創(chuàng)建 Jenkins Job
創(chuàng)建和構建 Job 和 Java 步驟類似,只需要變更名稱和倉庫地址即可,并且可以通過之前的 Job 進行復制:


執(zhí)行構建

構建成功如下所示:

之后即可 訪問該應用(通過 Ingress 和 Service 都可):

6、自動觸發(fā)構建
之前的構建都是采用手動選擇分支進行構建的,實際使用時,項目可能有很多,如果都是手動觸發(fā)可能比較消耗人力。所以推薦可以按需配置自動觸發(fā),即提交代碼后自動觸發(fā) Jenkins 進行構建任務。
本次用 Go 項目進行演示。首先找到 Go 項目的 Job,點擊 Configure

之后勾選 Build when

選擇需要觸發(fā)的分支,并生成觸發(fā) token:

最后點擊 Save 即可。
接下來配置 GitLab,允許觸發(fā)外部接口。點擊 Gitlab 的 Admin→Settings→Network:


保存后,找到 Go 項目,點擊 Settings→WebHooks:

新頁面輸入點擊 Add new webhook

并輸入 Jenkins 接口地址和 Token:


確認無誤后,點擊 Add webhook:

之后下方會添加一個新的 Project Hooks,可以點擊 Test 進行 Push 測試:

點擊后,即可在 Jenkins 頁面看到任務被觸發(fā):

也可以通過 Blue Ocean 看到是自動觸發(fā)的 stage 被執(zhí)行:

以上就是通過 GitLab 的事件觸發(fā) Jenkins 任務,在實際使用時,此功能非常常用,一般會用于開發(fā)、測試等環(huán)境,省去了手動構建的過程。而在 UAT 和生產環(huán)境,一般不需要再次構建,而是選擇其它環(huán)境產生的鏡像進行發(fā)版,接下來看一下如何進行不構建進行發(fā)版。
7、一次構建多次部署
在企業(yè)內部署服務時,往往每個項目都有多個環(huán)境,比如 dev、sit、prod 等。但是并非每個環(huán)境部署時,都需要進行重新編譯、構建鏡像等,此時可以把 dev 的鏡像直接部署至 sit 和 prod。

創(chuàng)建一個新的 Job,名字為 go-project-uat,類型 Pipeline:

點擊頁面的 This Project is parameterized(參數(shù)化構建):

選擇參數(shù)類型為 Image Tag Parameter(需要安裝 Image Tag 插件),之后定義 Name 為變量的名稱,Iamge Name 為 Harbor 的目錄和鏡像名稱:

點擊 Advance,輸入倉庫的地址,注意如果配置了證書,需要配置 https:

點擊 Configure,添加 Pipeline 腳本:

pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: jnlp
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/jnlp-agent-docker:latest"
imagePullPolicy: IfNotPresent
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
- name: "kubectl"
image: "crpi-q1nb2n896zwtcdts.cn-beijing.personal.cr.aliyuncs.com/ywb01/kubectl:devops"
imagePullPolicy: "IfNotPresent"
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
tty: true
restartPolicy: "Never"
'''
}
}
stages {
stage('Deploy') {
environment {
MY_KUBECONFIG = credentials('STUDY_CLUSTER_CONFIG')
}
steps {
container(name: 'kubectl'){
sh """
echo ${IMAGE_TAG}
kubectl --kubeconfig=${MY_KUBECONFIG} set image deployment -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${IMAGE_TAG} -n ${NAMESPACE}
kubectl --kubeconfig=${MY_KUBECONFIG} get po -l app=${IMAGE_NAME} -n ${NAMESPACE} -w
"""
}
}
}
}
environment {
HARBOR_ADDRESS = "192.168.200.53"
IMAGE_NAME = "go-project"
NAMESPACE = "kubernetes"
TAG = ""
}
}
點擊 Build with Parameters:

選擇一個鏡像,點擊 Build:

即可看到是直接將鏡像版本更新至 Kubernetes,并無構建過程,可以省下很多時間。該流水線也可以選擇之前的版本進行回滾操作。

8、集成 Helm 發(fā)布
8.1 生成 Helm 模板
雖然上述已經(jīng)借助流水線實現(xiàn)了服務的自動發(fā)版,但是針對 Kubernetes 的資源依舊是提前手動創(chuàng)建,接下來我們借助 Helm Charts 實現(xiàn)微服務資源的自動創(chuàng)建。
首先創(chuàng)建一個 Helm Chart 的模板:
[root@k8s-master01 ~]# helm create chart-template
接下來對模板稍加修改,使其變成一個通用的模板。首先更改默認拉取鏡像的 Secret:
[root@k8s-master01 ~]# cd chart-template
[root@k8s-master01 chart-template]# vim values.yaml
[root@k8s-master01 chart-template]# sed -n "19,20p;26p;28p;79,85p;98,103p" values.yaml
imagePullSecrets:
- name: harborkey
serviceAccount:
create: false
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
8.2 流水線集成 Helm
接下來使用 go-project 為例,生成一個 go-project 的 Chart:
[root@k8s-master01 ~]# git clone git@192.168.200.55:kubernetes/go-project.git
[root@k8s-master01 ~]# cp -rp chart-template/ go-project/chart
把模板上傳至該項目的 Git 倉庫,首先把代碼下載到本地,然后把模板放到代碼 chart 目錄下:
[root@k8s-master01 ~]# cd go-project
[root@k8s-master01 go-project]# git add .
[root@k8s-master01 go-project]# git commit -am "添加chart模板"
[root@k8s-master01 go-project]# git push origin master
創(chuàng)建一個新的uat環(huán)境
[root@k8s-master01 ~]# kubectl create ns kubernetes-uat
[root@k8s-master01 ~]# kubectl create secret docker-registry harborkey --docker-server=192.168.200.53 --docker-username=admin --docker-password=Harbor12345 --docker-email=1773464408@qq.com -n kubernetes-uat
接下來按需修改values模板文件


修改Jenkinsfile模板文件


cd chart
helm upgrade \
--kubeconfig $MY_KUBECONFIG \
--install $IMAGE_NAME . \
--namespace ${NAMESPACE} \
--create-namespace \
--set fullnameOverride=$IMAGE_NAME \
--set nameOverride=$IMAGE_NAME \
--set replicaCount=1 \
--set image.repository=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME} \
--set image.tag=${TAG}
提交代碼后,重新發(fā)版:

自動觸發(fā)構建完成發(fā)布

查看資源:
[root@k8s-master01 ~]# kubectl get po -n kubernetes-uat
NAME READY STATUS RESTARTS AGE
go-project-6c7bd8d985-qx7mv 1/1 Running 0 45s
[root@k8s-master01 ~]# helm list -n kubernetes-uat
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
go-project kubernetes-uat 1 2025-09-24 00:06:45.503610996 +0800 CST deployed go-project-0.1.0 1.16.0


浙公網(wǎng)安備 33010602011771號