前言
之前我们主要讲的jar包部署。使用jar包部署可能导致不同服务互相争抢资源(隔离性),不同服务可能需要不同的jdk环境,有时也会造成困扰。故在微服务时代,我们通常使用docker部署
一、docker安装
docke相关的知识,其实之前文章也写过,本节主要讲harbor的安装以及与jenkins配合,做容器化部署。故docker相关的讨论会尽可能简略。
1.1 docker安装
安装官方文档的介绍安装即可
1.2 docker-compose安装
docker-compose相关内容,之前文章也写过,故知识点本次略过。
sudo apt-get update
sudo apt-get install docker-compose-plugin
curl -SL https://github.com/docker/compose/releases/download/v2.29.6/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
二、harbor安装
2.1 下载解压
首先,去官网下载离线包,然后拷到/WORK/DOWNLOADS文件夹内
使用以下命令解压:
tar -xzf harbor-offline-installer-v2.9.1.tgz
2.2 nginx配置证书
由于harbor更倾向于用https,所以需要配置nginx的证书
openssl genrsa -out /WORK/APP/nginx/cert/nginx-selfsigned.key 2048
openssl req -new -key /WORK/APP/nginx/cert/nginx-selfsigned.key -ou
t /WORK/APP/nginx/cert/nginx-selfsigned.csr
openssl x509 -req -days 3650 -in /WORK/APP/nginx/cert/nginx-selfsigned.csr -signkey /WORK/APP/nginx/cert/nginx-selfsigned.key -out /WORK/APP/nginx/cert/nginx-selfsigned.crt -subj "/CN=192.168.0.64"
然后把location部分的配置,单独拎一个文件出来,比如叫shared.conf
分别在http和https的server模块include进来
include /WORK/APP/nginx/conf/shared.conf;
即可完成https的配置。
2.3 harbor配置
编辑harbor.yml,重点编辑以下内容
http:
# 把端口设置成9090
port: 9090
# 你没看错,这些先注释掉
#https:
# https port for harbor, default is 443
#port: 443
# The path of cert and key files for nginx
#certificate: /WORK/APP/nginx/cert/nginx-selfsigned.crt
#private_key: /WORK/APP/nginx/cert/nginx-selfsigned.key
./prepare
./install
这样harbor就装好了。稍微解释一下,./prepare脚本,就是根据harbor.yml的配置,在当前目录生成相应的配置文件。而install命令,则是创建启动镜像。
2.3.1 安装番外
我尝试了把harbor放在nginx后面,最后还是失败了。
因为harbor的前端访问路径貌似无法配置,这就导致静态资源的加载全部出错。有哪位大佬知道怎么搞的可以私信或评论区回复。
三、docker化项目
3.1 创建一个dockerfile
我们接着之前的项目继续开发。之前的项目已经完成了lib,resource,config等与jar包的分离,并且以shell脚本启动项目,而非直接java -jar,我们的容器化也要支持这些功能(自讨苦吃式学习)。dockerfile创建如下所示
FROM openjdk:17-jdk-slim
ENV PORT="8081"
RUN mkdir -p /app
WORKDIR /app
EXPOSE 8081
ADD ./target/nbr.jar nbr.jar
ADD ./target/bin bin
RUN chmod +x bin/startup.sh
RUN chmod +x bin/shutdown.sh
VOLUME ["/app/config","/app/resources","/app/logs","/app/lib"]
WORKDIR "/app/bin"
CMD ./startup.sh -p $PORT
需要注意的是,VOLUME 字段表示容器卷,docker中先声明,docker run时再映射到宿主机上。如果这里不声明,docker run -v 的时候就不好用了。
3.2 新的jenkins脚本
很多人搞容器化的时候,喜欢使用docker的maven插件来实现。然而实际上,并没有这种必要。学习成本又高,还有很多限制,也不好调试。不如直接在jenkins里写命令实现。变量向dockerfile传递,可以用–env var_key=var_value来实现。
参数名 | 默认值 | 描述 |
---|---|---|
profile | test | 环境 |
appName | app001 | 部署文件夹 |
port | 8081 | 端口 |
version | 1.0.0 | 版本 |
isBuildImg | true | 是否编译镜像 |
isUpdateBin | false | 是否更新bin |
isUpdateConfig | false | 是否更新配置文件 |
isUpdateLib | false | 是否更新lib包 |
isUpdateStatic | false | 是否更新静态资源 |
isUpdateMapper | false | 是否更新mapper |
gitTag | master | git分支 |
import java.text.SimpleDateFormat
node{
def remote = [:]
remote.name = '地下室主机'
remote.host = '192.168.0.64'
remote.user = 'root'
remote.password = '???@1314'
remote.deploymentHome = "/WORK/APP/study2024-class006"
remote.allowAnyHosts = true
remote.harbor = "localhost:9090"
def app = [:]
app.codePath = "busy"
app.name = "study2024-class006"
app.module = "nbr"
app.version = "${version}"
app.cd = "${appName}"
app.port = "${port}"
def timestamp = currentBuild.getTimeInMillis()
def formattedTimestamp = new SimpleDateFormat("yyyy-MM-dd-HH_mm_ss").format(timestamp)
stage("拉取代码"){
git branch: "${gitTag}", credentialsId: 'gitSec', url: 'https://gitee.com/hataksumo/study2024-class006.git'
}
if(isCompileImage == "true"){
stage("编译代码"){
sh """
cd busy
mvn clean
mvn package -pl ${app.module} -am -P${profile} -Dmaven.test.skip=true
"""
}
stage("创建镜像"){
sh """
cd ${app.codePath}/${app.module}
docker build -f docker/Dockerfile \
--build-arg PORT=${app.port} \
-t ${app.name}-${app.module}:${app.version} .
docker tag ${app.name}-${app.module}:${app.version} ${remote.harbor}/library/${app.name}-${app.module}:${app.version}
docker login localhost:9090 -u admin -p Harbor12345
docker push ${remote.harbor}/library/${app.name}-${app.module}:${app.version}
docker logout ${remote.harbor}/library/${app.name}-${app.module}:${app.version}
docker image rm -f ${remote.harbor}/library/${app.name}-${app.module}:${app.version}
docker image rm -f ${app.name}-${app.module}:${app.version}
"""
}
}
stage("拷贝资源"){
echo "拷贝创建"
sshCommand remote: remote, failOnError:false, command: """
[ -d ${remote.deploymentHome}/${app.cd}] || mkdir -p ${remote.deploymentHome}/${app.cd}
cd ${remote.deploymentHome}/${app.cd}
[ -d lib] || mkdir -p lib
[ -d config] || mkdir -p config
[ -d resources/static] || mkdir -p resources/static
[ -d resources/templates] || mkdir -p resources/templates
[ -d resources/mybatis] || mkdir -p resources/mybatis
"""
if(isUpdateConfig == "true"){
echo "删除config"
sshRemove remote: remote, failOnError:false, path: "${remote.deploymentHome}/${app.cd}/config"
echo "拷贝lib包"
sshPut remote: remote, from: "${app.codePath}/${app.module}/target/config", into: "${remote.deploymentHome}/${app.cd}"
}
if(isUpdateLib == "true"){
echo "删除lib包"
sshRemove remote: remote, failOnError:false, path: "${remote.deploymentHome}/${app.cd}/lib"
echo "拷贝lib包"
sshPut remote: remote, from: "${app.codePath}/${app.module}/target/lib", into: "${remote.deploymentHome}/${app.cd}"
}
sshCommand remote: remote, failOnError:false, command: "mkdir ${remote.deploymentHome}/${app.cd}/resources"
if(isUpdateStatic == "true"){
echo "清除resources/static文件"
sshRemove remote: remote, failOnError:false, path: "${remote.deploymentHome}/${app.cd}/resources/static"
echo "拷贝static文件"
sshPut remote: remote, from: "${app.codePath}/${app.module}/target/resources/static", into: "${remote.deploymentHome}/${app.cd}/resources"
}
if(isUpdateMapper == "true"){
echo "清除resources/mybatis文件"
sshRemove remote: remote, failOnError:false, path: "${remote.deploymentHome}/${app.cd}/resources/mybatis"
echo "拷贝mybatis文件"
sshPut remote: remote, from: "${app.codePath}/${app.module}/target/resources/mybatis", into: "${remote.deploymentHome}/${app.cd}/resources"
}
}
stage("镜像拉取"){
//docker stop ${app.name}-${app.module}-${appName}
//此处是不优雅的关闭
sshCommand remote: remote, failOnError:false, command: """
docker image rm -f ${remote.harbor}/library/${app.name}-${app.module}:${app.version}
docker container rm -f ${app.name}-${app.module}-${appName}
docker login localhost:9090 -u admin -p Harbor12345
docker pull ${remote.harbor}/library/${app.name}-${app.module}:${app.version}
docker tag ${remote.harbor}/library/${app.name}-${app.module}:${app.version} ${app.name}-${app.module}:${app.version}
docker logout
echo "docker run -it -p ${app.port}:${app.port} --volume ${remote.deploymentHome}/${app.cd}/config:/app/config --volume ${remote.deploymentHome}/${app.cd}/resources:/app/resources --volume ${remote.deploymentHome}/${app.cd}/logs:/app/logs --volume ${remote.deploymentHome}/${app.cd}/lib:/app/lib --env PORT=${app.port} ${app.name}-${app.module}:${app.version} bin/bash"
docker run -d \
-p ${app.port}:${app.port} \
--volume ${remote.deploymentHome}/${app.cd}/config:/app/config \
--volume ${remote.deploymentHome}/${app.cd}/resources:/app/resources \
--volume ${remote.deploymentHome}/${app.cd}/logs:/app/logs \
--volume ${remote.deploymentHome}/${app.cd}/lib:/app/lib \
--env PORT=${app.port} \
--name ${app.name}-${app.module}-${appName} \
${app.name}-${app.module}:${app.version}
echo "容器启动完成"
lsof -i:${app.port}
"""
}
}
3.3 使用docker-compose
之前的方法,启动和关闭都不是很优雅,拓展性也不强。不如使用docker-compose技术。
3.3.1 compose.yml
compose.yml放入docker文件夹下
services:
nbr:
image: study2024-class006-nbr:${VERSION}
environment:
- PORT
- EXPORT_PORT
- VERSION
- APP_NAME
- CD_PATH
ports:
- ${EXPORT_PORT}:${PORT}
volumes:
- ${CD_PATH}/config:/app/config
- ${CD_PATH}/logs:/app/logs
- ${CD_PATH}/lib:/app/lib
- ${CD_PATH}/resources/static:/app/resources/static
- ${CD_PATH}/resources/mybatis:/app/resources/mybatis
- ${CD_PATH}/resources/template:/app/resources/template
container_name: study2024-nbr-${APP_NAME}
3.3.2 jenkins修改
- 更改 “镜像拉取”部分
stage("镜像拉取"){
//docker stop ${app.name}-${app.module}-${appName}
//此处是不优雅的关闭
sshCommand remote: remote, failOnError:false, command: """
docker image rm -f ${remote.harbor}/library/${app.name}-${app.module}:${app.version}
docker container rm -f ${app.name}-${app.module}-${appName}
docker login localhost:9090 -u admin -p Harbor12345
docker pull ${remote.harbor}/library/${app.name}-${app.module}:${app.version}
docker tag ${remote.harbor}/library/${app.name}-${app.module}:${app.version} ${app.name}-${app.module}:${app.version}
docker logout
cd ${remote.deploymentHome}/${app.cd}
docker-compose down
docker-compose up -d
echo "容器启动完成"
"""
}
- 更改 “拷贝资源” 部分
在尾部添加如下代码:
// 拷贝compose.yml
sshPut remote: remote, from: "${app.codePath}/${app.module}/target/compose.yml", into: "${remote.deploymentHome}/${app.cd}/compose.yml"
// 生成evn
def envContent = """
EXPORT_PORT=${app.port}
PORT=8080
VERSION=${app.version}
APP_NAME=${app.name}-${app.module}-${appName}
CD_PATH=${remote.deploymentHome}/${app.cd}/
"""
echo "$envContent"
writeFile(file: "${app.codePath}/${app.module}/target/.env", text: envContent)
sshPut remote: remote, from: "${app.codePath}/${app.module}/target/.env", into: "${remote.deploymentHome}/${app.cd}/.env"
注意,.env是compose.yml所用的环境变量配置。在docker-compose up的时候,会自动读取该文件
- pom添加拷贝配置
<resource>
<directory>docker</directory>
<includes>
<include>compose.yml</include>
</includes>
<targetPath>${project.build.directory}</targetPath>
</resource>
3.4 错误处理
3.4.1 调试容器
如果项目启动不成功,可以使用docker run -it <镜像名> /bin/bash 进入容器调试
3.4.2 脚本启动的坑
docker的机制,如果执行的脚本没有阻塞,就会退出容器。所以需要在脚本的最后添加这样一句
tail -f "${BASE_DIR}/logs/start.out"
3.4.3 注意运行在容器内
无论是mysql还是redis等中间件的链接,记得不要配localhost,配成内网ip
3.4.4 查看容器
docker inspect 容器Id
四、代码展示
还请众道友移步我的码云
五、一些额外的思考
当项目的打包容器化了,一个jdk-slim的容器都400M,那之前的把lib包分离还有什么意义。
实际上可以把主jar包也映射出去嘛。镜像只构建一回,以后的更新,更jar包就行了。这样的修改才是完整版的容器化打包方案。也就是说,实际的开发,还是原先的jar包打包开发,仅仅是第一次构建了镜像。是不是感觉世界一下就豁然开朗了。哈哈~~~