微服务CI/CD实践系列:
微服务CI/CD实践(一)环境准备及虚拟机创建
微服务CI/CD实践(二)服务器先决准备
微服务CI/CD实践(三)gitlab部署及nexus3部署
微服务CI/CD实践(四)Jenkins部署及环境配置
微服务CI/CD实践(五)Jenkins + Dokcer 部署微服务后端项目
微服务CI/CD实践(六)Jenkins + Dokcer 部署微服务前端VUE项目
微服务CI/CD实践(七)Minio服务器部署及应用
文章目录
前端项目是基于NodeJS(Vue)框架开发,我们通过打包成Docker镜像的方式进行部署,原理是先将项目打包成静态页面,然后再将静态页面直接copy到Nginx镜像中运行。构建部署流程如下:
- 拉取代码
- jenkins服务器进行nodejs编译
- 使用dockerfile构建镜像并打包镜像
- 上传镜像包
- 执行sh
一、先决条件
1.1 服务器先决条件
Jenkins 和 server服务器先决条件参考微服务CI/CD实践(二)服务器先决准备 和 微服务CI/CD实践(四)Jenkins部署及环境配置
1.2 项目配置
Dockerfile
使用Jenkins本地编译项目在构建镜像
FROM nginx:latest
# 将生成的静态页面文件复制到nginx的/usr/share/nginx/html/目录
COPY dist/ /usr/share/nginx/html/
# 将mime文件复制到nginx的/etc/nginx/目录 后续配置ng会使用
COPY mime.types /etc/nginx/mime.types
# 容器启动时运行的命令
CMD ["nginx", "-g", "daemon off;"]
也可以直接使用docker 镜像编译-构建镜像,此模式jenkins服务器可以不需要node环境
# Install dependencies
FROM node:18.20.4 as builder
WORKDIR /app
# Install pnpm
RUN npm i -g pnpm
# copy file for next stage
COPY . /app
RUN pnpm install && pnpm run build
# copy dist from the first stage for Production
FROM nginx:latest AS runner
COPY --from=builder /app/dist/ /usr/share/nginx/html
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
不过此模式在docker-hub停止国内服务后可能无法正常拉取镜像。
Nginx配置文件
根据项目要求编写ng配置
cd /data/container/nginx/etc
vi nginx.conf
# 编写配置并保存
vi mime.types
# 编写配置并保存
以下为nginx.conf配置示例
events {
worker_connections 1024;
}
http {
# 需要引入mime.types配置或者显示配置静态文件mimetype类型,否则运行后,浏览器会因为文件类型导致无法正常加载静态文件
include mime.types;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 100m;
server {
listen 80;
listen [::]:80;
server_name localhost;
# 设置 CORS 相关的响应头
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' '*' always;
add_header 'Access-Control-Max-Age' 1728000 always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
gzip on;
gzip_buffers 32 4k;
gzip_comp_level 6;
gzip_min_length 100;
gzip_types application/javascript text/css text/xml text/plain application/x-javascript image/jpeg image/gif image/png;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;
charset utf8;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
if (!-e $request_filename) {
rewrite ^/(.*) /index.html last;
break;
}
}
location ~ .*\.(jpg|png|js|css|woff2|ttf|woff|eot)$ {
root /usr/share/nginx/html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# 配置全局代理并统一处理CORS
location /gateway-api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-Ip $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.1.103:10000/;
# 添加 CORS 相关的响应头
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' '*' always;
add_header 'Access-Control-Max-Age' 1728000 always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# 处理 OPTIONS 请求
if ($request_method = 'OPTIONS') {
return 204;
}
}
}
}
以下为mime.type示例
types {
text/html html htm shtml;
text/css css;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/xml xml;
application/json json;
application/pdf pdf;
application/rss+xml rss;
application/atom+xml atom;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
application/zip zip;
application/tar tar;
application/x-7z-compressed 7z;
application/x-java-archive jar;
application/x-rar-compressed rar;
application/x-web-app-manifest+json webapp;
application/xhtml+xml xhtml;
application/x-msdownload exe dll;
audio/midi mid midi kar;
audio/mpeg mp3;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/webm webm;
video/x-msvideo avi;
video/x-ms-wmv wmv;
video/x-ms-asf asx asf;
video/x-flv flv;
application/x-shockwave-flash swf;
application/vnd.ms-excel xls;
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
application/vnd.ms-fontobject eot;
application/vnd.apple.mpegurl m3u8;
application/x-font-ttf ttc ttf;
application/x-httpd-php-source phps;
}
部署脚本
step1 定义入参
可以通过Jenkins任务将参数传入脚本中,我们定义了下面7个参数:
container_name : 容器名称
image_name : 镜像名称
version : 镜像版本
portal_port: 宿主主机端口映射
server_port: 容器内服务端口
portal_ssl_port: 宿主主机端口映射
serve_sslr_port: 容器内服务端口
step2 定义入参对参数进行检查
将必传参数放在最前面,这里根据自己的实际情况判断,检查是否传递参数。比如设置container_name、image_name、version 、portal_port、server_port5个参数必须传入,就设置参数的个数不能小于5。
echo "param validate"
if [ $# -lt 5 ]; then
echo "you must use like this : /usr/docker-sh/your_script.sh <container_name> <image_name> <version> [portal port] [server port] [portal ssl port] [server ssl port]"
exit
fi
step3 入参赋值
如果有参数传入,则赋值参数
# 前五个参数是必传参数,无需判断直接赋值
container_name="$1"
image_name="$2"
version="$3"
portal_port="$4"
server_port="$5"
if [ "$6" != "" ]; then
portal_ssl_port="$6"
fi
echo "portal_ssl_port=" $portal_ssl_port
if [ "$7" != "" ]; then
serve_sslr_port="$7"
fi
step4 停止并删除容器
echo "执行docker ps"
docker ps
if [[ "$(docker inspect $container_name 2> /dev/null | grep $container_name)" != "" ]];
then
echo $container_name "容器存在,停止并删除"
echo "docker stop" $container_name
docker stop $container_name
echo "docker rm" $container_name
docker rm $container_name
else
echo $container_name "容器不存在"
fi
step5 停止并删除镜像
# 删除镜像
echo "执行docker images"
docker images
if [[ "$(docker images -q $image_name 2> /dev/null)" != "" ]];
then
echo $image_name '镜像存在,删除镜像'
docker rmi $(docker images -q $image_name 2> /dev/null) --force
else
echo $image_name '镜像不存在'
fi
step6 备份和加载安装包
#bak image
echo "bak image" $image_name
BAK_DIR=/opt/bak/docker/$image_name/`date +%Y%m%d`
mkdir -p "$BAK_DIR"
cp "/opt/tmp/$container_name.tar" "$BAK_DIR"/"$image_name"_`date +%H%M%S`.tar
echo "docker load" $image_name
docker load --input /opt/tmp/$container_name.tar
step7 执行运行镜像命令
echo "docker run" $image_name
docker run -d -p $portal_port:$server_port --name=$container_name --network=my-network -e TZ="Asia/Shanghai" --restart=always -v /data/container/nginx/www:/var/www -v /data/container/nginx/logs:/var/log/nginx -v /data/container/nginx/etc:/etc/nginx -v /data/container/nginx/etc/nginx.conf:/etc/nginx/nginx.conf -v /data/container/nginx/etc/mime.types:/etc/nginx/mime.types -v /etc/localtime:/etc/localtime -v /usr/share/zoneinfo/Asia/Shanghai:/etc/timezone $image_name
step8 执行删除安装包命令
echo "remove tmp " $image_name
rm -rf /opt/tmp/$container_name.tar
以下为完整的安装部署脚本
#!/usr/bin/env bash
echo "param validate"
if [ $# -lt 5 ]; then
echo "you must use like this : /usr/docker-sh/your_script.sh <container_name> <image_name> <version> [portal port] [server port] [portal ssl port] [server ssl port]"
exit
fi
container_name="$1"
image_name="$2"
version="$3"
portal_port="$4"
server_port="$5"
if [ "$6" != "" ]; then
portal_ssl_port="$6"
fi
echo "portal_ssl_port=" $portal_ssl_port
if [ "$7" != "" ]; then
serve_sslr_port="$7"
fi
echo "执行docker ps"
docker ps
if [[ "$(docker inspect $container_name 2> /dev/null | grep $container_name)" != "" ]];
then
echo $container_name "容器存在,停止并删除"
echo "docker stop" $container_name
docker stop $container_name
echo "docker rm" $container_name
docker rm $container_name
else
echo $container_name "容器不存在"
fi
# 删除镜像
echo "执行docker images"
docker images
if [[ "$(docker images -q $image_name 2> /dev/null)" != "" ]];
then
echo $image_name '镜像存在,删除镜像'
docker rmi $(docker images -q $image_name 2> /dev/null) --force
else
echo $image_name '镜像不存在'
fi
#bak image
echo "bak image" $image_name
BAK_DIR=/opt/bak/docker/$image_name/`date +%Y%m%d`
mkdir -p "$BAK_DIR"
cp "/opt/tmp/$container_name.tar" "$BAK_DIR"/"$image_name"_`date +%H%M%S`.tar
echo "docker load" $image_name
docker load --input /opt/tmp/$container_name.tar
echo "docker run" $image_name
docker run -d -p $portal_port:$server_port --name=$container_name --network=my-network -e TZ="Asia/Shanghai" --restart=always -v /data/container/nginx/www:/var/www -v /data/container/nginx/logs:/var/log/nginx -v /data/container/nginx/etc:/etc/nginx -v /data/container/nginx/etc/nginx.conf:/etc/nginx/nginx.conf -v /data/container/nginx/etc/mime.types:/etc/nginx/mime.types -v /etc/localtime:/etc/localtime -v /usr/share/zoneinfo/Asia/Shanghai:/etc/timezone $image_name
echo "remove tmp " $image_name
rm -rf /opt/tmp/$container_name.tar
echo "Docker Portal is starting,please try to access $container_name conslone url"
二、Jenkins构建部署
2.1 创建项目
新建一个流水线任务
2.2 配置项目基本信息
创建完成项目,点击项目进入项目页面,点击左侧菜单》配置,进行项目基本配置
step1 项目构建历史存储策略配置
根据项目实际情况配置存储策略
step2 配置参数化构建过程
Jenkins List Git Branches插件 构建选择指定git分支,点击添加参数选择List Git branchers选项进行Jenkins List Git Branches插件配置
Jenkins List Git Branches插件配置流程如下:
- 配置name
- 配置仓库并选择凭证
- 选择Parameter Type
- 配置Branch Filter
2.3 定义 Pipeline script
step1 配置全局变量
environment {
REPOSITORY="http://192.168.1.101:8929/hka/hka-admin-wocwin.git"
projectdir="hka-web-01"
projectname="hka-admin-wocwin"
}
step2 获取代码
检出选择指定git分支的代码
stages {
stage('获取代码') {
steps {
echo "start fetch code from git:${REPOSITORY} ${branch}"
deleteDir()
checkout([
$class: 'GitSCM',
branches: [[name: '${branch}']],
doGenerateSubmoduleConfigurations: false,
extensions: [],
userRemoteConfigs: [[
credentialsId: '2',
url: 'http://192.168.1.101:8929/hka/hka-admin-wocwin.git'
]]
])
}
}
step3 编译项目
这里需要显示指定node的环境变量,否则执行编译命令会抛异常
stage('Build NodeJS Vue') {
steps {
echo "build nodejs code"
nodejs('node') {
sh 'export PATH="/usr/local/nodejs/bin:$PATH"'
sh 'node -v'
sh 'npm -v'
sh 'pnpm -v'
sh 'pnpm install'
sh 'pnpm run prod'
}
echo "build nodejs success"
}
}
step4 删除历史容器和镜像
如何没有在jenkins服务器运行容器可以忽略Delete Old Docker Container步骤
stage('Delete Old Docker Container') {
steps {
echo "delete docker container"
sh '''if [[ "$(docker inspect ${projectname} 2> /dev/null | grep ${projectname})" != "" ]];
then
echo ${projectname} "容器存在,停止并删除"
echo "docker stop" ${projectname}
docker stop ${projectname}
echo "docker rm" ${projectname}
docker rm ${projectname}
else
echo ${projectname} "容器不存在"
fi'''
}
}
stage('Delete Old Docker Image') {
steps {
echo "delete docker image"
sh '''if [[ "$(docker images -q ${projectname} 2> /dev/null)" != "" ]];
then
echo ${projectname} \'镜像存在,删除镜像\'
docker rmi $(docker images -q ${projectname} 2> /dev/null) --force
else
echo ${projectname} \'镜像不存在,创建镜像\'
fi'''
}
}
step5 构建镜像
stage('Build Docker Image') {
steps {
echo "start docker build ${projectname} code"
sh 'docker build -t ${projectname} .'
echo "save docker images tar"
sh 'docker save -o ${projectname}.tar ${projectname}'
}
}
stage('Delete New Docker Image') {
steps {
echo "delete docker image"
sh '''if [[ "$(docker images -q ${projectname} 2> /dev/null)" != "" ]];
then
echo ${projectname} \'镜像存在,删除镜像\'
docker rmi $(docker images -q ${projectname} 2> /dev/null) --force
else
echo ${projectname} \'镜像不存在,创建镜像\'
fi'''
}
}
step6 上传镜像包’
这里的configName: ‘103’, 就是微服务CI/CD实践(四)Jenkins部署及环境配置### 2.2.4 全局系统配置 SSH Server配置
该流水线步骤会通过ssh将 镜像tar包上传到SSH Server配置的Remote Directory目录下
stage('Upload img tar') {
steps {
sshPublisher(
publishers: [
sshPublisherDesc(
configName: '103',
transfers: [
sshTransfer(
cleanRemote: false,
excludes: '',
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: '',
remoteDirectorySDF: false,
removePrefix: '',
sourceFiles: '${projectname}.tar'
)
],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false
)
]
)
}
}
step7 执行安装部署脚本
这里的configName: ‘103’, 就是微服务CI/CD实践(四)Jenkins部署及环境配置### 2.2.4 全局系统配置 SSH Server配置
该步骤通过ssh远程执行sh安装部署脚本
stage('Execute Command sh') {
steps {
sshPublisher(
publishers: [
sshPublisherDesc(
configName: '103',
transfers: [
sshTransfer(
execCommand: '/usr/docker-sh/publish_hka-admin-wocwin.sh hka-admin-wocwin hka-admin-wocwin latest 80 80',
execTimeout: 300000
)
],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false
)
]
)
}
}
以下为完整流水线定义
pipeline {
agent any
environment {
REPOSITORY="http://192.168.1.101:8929/hka/hka-admin-wocwin.git"
projectdir="hka-web-01"
projectname="hka-admin-wocwin"
}
stages {
stage('获取代码') {
steps {
echo "start fetch code from git:${REPOSITORY} ${branch}"
deleteDir()
checkout([
$class: 'GitSCM',
branches: [[name: '${branch}']],
doGenerateSubmoduleConfigurations: false,
extensions: [],
userRemoteConfigs: [[
credentialsId: '2',
url: 'http://192.168.1.101:8929/hka/hka-admin-wocwin.git'
]]
])
}
}
stage('Build NodeJS Vue') {
steps {
echo "build nodejs code"
nodejs('node') {
sh 'export PATH="/usr/local/nodejs/bin:$PATH"'
sh 'node -v'
sh 'npm -v'
sh 'pnpm -v'
sh 'pnpm install'
sh 'pnpm run prod'
}
echo "build nodejs success"
}
}
stage('Delete Old Docker Container') {
steps {
echo "delete docker container"
sh '''if [[ "$(docker inspect ${projectname} 2> /dev/null | grep ${projectname})" != "" ]];
then
echo ${projectname} "容器存在,停止并删除"
echo "docker stop" ${projectname}
docker stop ${projectname}
echo "docker rm" ${projectname}
docker rm ${projectname}
else
echo ${projectname} "容器不存在"
fi'''
}
}
stage('Delete Old Docker Image') {
steps {
echo "delete docker image"
sh '''if [[ "$(docker images -q ${projectname} 2> /dev/null)" != "" ]];
then
echo ${projectname} \'镜像存在,删除镜像\'
docker rmi $(docker images -q ${projectname} 2> /dev/null) --force
else
echo ${projectname} \'镜像不存在,创建镜像\'
fi'''
}
}
stage('Build Docker Image') {
steps {
echo "start docker build ${projectname} code"
sh 'docker build -t ${projectname} .'
echo "save docker images tar"
sh 'docker save -o ${projectname}.tar ${projectname}'
}
}
stage('Delete New Docker Image') {
steps {
echo "delete docker image"
sh '''if [[ "$(docker images -q ${projectname} 2> /dev/null)" != "" ]];
then
echo ${projectname} \'镜像存在,删除镜像\'
docker rmi $(docker images -q ${projectname} 2> /dev/null) --force
else
echo ${projectname} \'镜像不存在,创建镜像\'
fi'''
}
}
stage('Upload img tar') {
steps {
sshPublisher(
publishers: [
sshPublisherDesc(
configName: '103',
transfers: [
sshTransfer(
cleanRemote: false,
excludes: '',
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: '',
remoteDirectorySDF: false,
removePrefix: '',
sourceFiles: 'hka-admin-wocwin.tar'
)
],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false
)
]
)
}
}
stage('Execute Command sh') {
steps {
sshPublisher(
publishers: [
sshPublisherDesc(
configName: '103',
transfers: [
sshTransfer(
execCommand: '/usr/docker-sh/publish_hka-admin-wocwin.sh hka-admin-wocwin hka-admin-wocwin latest 80 80 4413 4413',
execTimeout: 300000
)
],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false
)
]
)
}
}
stage('Publish Results') {
steps {
echo "End Publish ${projectname}"
}
}
}
}
2.4 构建部署项目
回到项目页面,点击参数化构建,选择用于构建的分支点击Build执行构建任务。