docker+jenkins+harbor自动部署方案

记录本地搭建docker + jenkins + harbor的持续集成方案过程

思路

​ 由于springcloud项目需要部署在测试和生产环境的不同服务器,但项目规模还达不到使用k8s,所以使用docker+jenkins+harbor来做自动部署方案。大致流程图如下:

docker+jenkins+harbor

  1. 项目根目录下编写Jenkinsfile
  2. Jenkins上触发构建,拉取gitLab代码,使用pipeline自动构建流水线
  3. 将构建好的docker镜像推送到harbor仓库(这里比较特殊是Jenkins是使用docker部署的容器,需调用宿主机的docker来build镜像,再推送到harbor,有点套娃~)
  4. Jenkins推送镜像成功后,ssh到目标服务器,执行目标服务器删除容器、镜像,从harbor拉取新的容器来构建服务。

前提

每台机已有docker、docker-compose

部署机上的docker装有容器:jenkinsci/blueocean 、harbor1.8.4

配置每台docker主机能连到harbor

1
2
3
4
5
6
7
8
9
10
11
#给每台机器都添加域名解析某ip
vim /etc/hosts
xxx.xxx.xxx.xxx docker.xxx.cc

#docker配置域名,让docker login时能识别此域名
vim /etc/docker/daemon.json
#由于我这里给部署的harbor端口是59010,所以每个docker配置的时候都配成如下形式。
"insecure-registries":[
"docker.xxx.cc:59010"
]

测试登录到harbor

1
2
3
4
5
docker login docker.rainbow.BSS:59010
#可能会出了错 Error saving credentials: error storing credentials - err: exit status 1, out: `Cannot autolaunch D-Bus without X11 $DISPLAY`
#问题出在Linux缺少一个密码管理包gnupg,它用于加密,我们在登录时需要这个包将密码加密后才能完成,因此直接安装
sudo apt update
sudo apt install gnupg2 pass

配置Jenkins容器能免密登录到目标服务器

由于部署中Jenkins需要远程到最终部署的服务器上执行脚本,所以需先配置免密登录。

1
2
3
4
5
6
#先进入Jenkins容器里面
docker exec -it jenkins bash
#当前登录用户的~/.ssh文件夹下生成一对公秘钥
ssh-keygen -b 4096 -t rsa
#将公钥id_rsa.pub传到目标服务器上,并把内容cat到~/.ssh/authorized_keys文件中
cat id_rsa.pub >> authorized_keys

项目下的Jenkinsfile

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
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#使用Jenkins控制台安装Active Choices Plug-in插件,配置项目构建前的联动参数选择。
properties([
parameters([
[$class: 'ChoiceParameter',
choiceType: 'PT_SINGLE_SELECT', //简单的下来选择框
description: '选择当前jenkins所要构建的服务环境',
filterLength: 1,
filterable: false,
name: 'Env',
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: true,
script: """
return["Could not get The environments"]
"""
],
script: [
classpath: [],
sandbox: true,
script: """
return["test","prod"]
"""
]
]
],
[$class: 'CascadeChoiceParameter',
choiceType: 'PT_CHECKBOX', //复选框
description: '选择具体要构建的服务',
name: 'JavaService',
referencedParameters: 'Env', //与上面的Env值联动
script:
[$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: true,
script: "return['Could not get Environment from Env Param']"
],
script: [
classpath: [],
sandbox: true,
script: '''
if (Env.equals("test")){
return["serverA","serverB", "serverC"]
}
else if(Env.equals("prod")){
return["serverA","serverB", "serverC"]
}
'''
]
]
],
[$class: 'CascadeChoiceParameter',
choiceType: 'PT_CHECKBOX',
description: '选择要部署的目标服务器',
name: 'TargetServer',
referencedParameters: 'Env',
script:
[$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: true,
script: "return['Could not get Environment from Env Param']"
],
script: [
classpath: [],
sandbox: true,
script: '''
if (Env.equals("test")){
return["192.168.94.1", "192.168.94.2", "192.168.94.3"]
}
else if(Env.equals("prod")){
return["192.168.111.1","192.168.111.2"]
}
'''
]
]
]
])
])
#流水线步骤
pipeline {
agent any
environment {
my_env = 'none'
}
stages {
stage('Service selection') {
input {
message "选择了环境:${params.Env} \n并将以下服务:${params.JavaService}\n部署到:${params.TargetServer}\n是否立即执行构建?"
}
steps{
echo "构建完成"
}
}

stage('Package') {
agent {
//此阶段的所有构建都是基于此docker环境
docker {
image 'maven:3-alpine'
//maven仓库挂载到服务器本地,方便后需部署,无需重复下载
args '-v /var/jenkins_home/.m2:/root/.m2'
}
}
steps {
script {
for(ser in params.JavaService.tokenize(',')){
switch("${ser}"){
case "serverA":
echo "package serverA"
//这些操作的初始路径都是项目根路径,所以假设serverA这个子项目位于项目根目录下的server文件夹下,即"项目根路径/server/serverA”,那么打包语句如下
sh 'cd server/serverA && mvn clean package -Dmaven.test.skip=true -Dproject.type=jar -pl serverA -am'
break
case "serverB":
echo "package serverB"
sh '...'
break
case "serverC":
echo "package serverA"
sh '...'
break
}
}
}
}
}

stage ('Deploy') {
agent none
steps {
script {
echo "WORKSPACE:${env.WORKSPACE}"
echo "Branch:${env.NODE_NAME}"
}
script {
for(ser in params.JavaService.tokenize(',')){
switch("${ser}"){
case "serverA":
echo "deploy febs-tx-manager"
//此脚本下面详解,该脚本也是位于项目根路径的相对路径下。
sh "bash ./scripts/${params.Env}/deploy-serverA-${params.Env}.sh ${params.TargetServer}"
break
case "xxx":
...
}
}
}
}
}

}
}

deploy-serverA-${params.Env}.sh脚本

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
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
90
91
92
93
94
95
96
97
98
99
100
#!/bin/bash

#接受以","分割的ip字符串
ips="$1"
echo $ips
OLD_IFS="$IFS"
IFS=","
ipArray=($ips)
IFS="$OLD_IFS"

#准备部署的镜像版本号
vendor=0

#harbor主机 /etc/docker/daemon.json下配置的域名
harborHost=docker.xxx.cc:59010
#仓库名
repository=Server
#子项目所在目录,起始于项目根路径
projectPath=/server/
#项目目录名
projectName=serverA
#包名
jarName=serverA-2.2-RELEASE
#docker镜像名
dockerImageName=serverA
#docker容器名
containerName=serverA
#docker-compose.yml
dockerComposeFileName=docker-compose-serverA

#进入target文件夹
#直接的构建是再容器里,这个是在 Jenkins 容器里,所以空间不一样
#容器的空间是原空间路径后面多了 @2
cd $WORKSPACE@2$projectPath$projectName/target
#创建Dockerfile文件
#VOLUME /tmp:挂载到/tmp目录下,调用docker去build的时候,会把上下文的文件(也就是 docker bulid xxx . 的这个.)放到/var/lib/docker/tmp/下。
cat << EOF > Dockerfile
FROM openjdk:8u212-jre
MAINTAINER $projectName
VOLUME /tmp
COPY $jarName.jar $jarName.jar
ENTRYPOINT ["java", "-Xmx256m", "-jar","-Duser.timezone=GMT+08","$jarName.jar"]
EOF

#删除jenkins所在的docker服务里serverA的旧镜像
docker images | grep $harborHost/$repository/$dockerImageName | awk '{print $3}' | xargs docker rmi
#创建新镜像
docker build -t $harborHost/$repository/$dockerImageName:$vendor .

cat << EOF > harbor_password
yourpassword
EOF
#登录 harbor
cat ./harbor_password | docker login $harborHost --username damon --password-stdin
#推送镜像
docker push $harborHost/$repository/$dockerImageName:$vendor





#ssh到对应服务器上pull镜像
#创建对应的compose文件并执行容器的创建
for ipVar in ${ipArray[@]}
do
echo "将${projectName}部署到目标服务器${ipVar}"
ssh root@$ipVar << remotessh
docker stop $containerName
docker rm $containerName
docker images | grep $harborHost/$repository/$dockerImageName | awk '{print $3}' | xargs docker rmi
cd /usr/app/standalone_febs
cat ./harbor_password | docker login $harborHost --username damon --password-stdin
docker pull $harborHost/$repository/$dockerImageName:$vendor

cat << EOF > $dockerComposeFileName.yaml
version: '3'
services:
$containerName:
image: $harborHost/$repository/$dockerImageName:$vendor
container_name: $containerName
restart: always
volumes:
- "/usr/app/standalone_febs/febs-cloud/log:/log"
ports:
- 8224:8224
command:
- "--nacos.url=192.168.xx.xx"
- "--mysql.url=192.168.xx.xx"
- "--redis.url=192.168.xx.xx"
- "--febs-admin=192.168.xx.xx"
- "--febs-gateway=192.168.xx.xx"
- "--febs-tx-manager=192.168.xx.xx"
environment:
- TZ=Asia/Shanghai
EOF
docker-compose -f $dockerComposeFileName.yaml up -d

exit
remotessh
done

小贴士:清除已在harbor删除的镜像

有时候harbor无法正常GC掉已删除镜像,需要进行如下操作。

  1. 进入harbor所在目录执行垃圾回收
1
2
3
4
5
6
7
8
9
docker-compose stop
//使用--dry-run参数运行容器,预览运行效果,但不删除任何数据
docker run -it --name gc --rm --volumes-from registry vmware/registry:2.6.2-photon garbage-collect --dry-run /etc/registry/config.yml
//不使用--dry-run参数,将删除相关的文件和镜像
docker run -it --name gc --rm --volumes-from registry vmware/registry:2.6.2-photon garbage-collect /etc/registry/config.yml
docker-compose start

//可验证统计目录大小
du -sh /data/registry/docker/registry/v2/blobs&repositories
  1. 重启后还需进入harbor系统后台,去手动执行垃圾清理

  2. 试试是否成功

1
2
//重新上传之前删除的镜像,如没成功删除会报镜像已存在
docker push docker.damon.cc:9010/rainbow/busybox:latest

参考

jenkinsfile的编写:https://zhuanlan.zhihu.com/p/89312003

评论加载中...