微服务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服务器部署及应用
文章目录
- 一、先决条件
- 1.1 服务器先决条件
- 1.2 项目配置
- Dockerfile
- Nginx配置文件
- 部署脚本
- 二、Jenkins构建部署
- 2.1 创建项目
- 2.2 配置项目基本信息
- 2.3 定义 Pipeline script
- 2.4 构建部署项目
前端项目是基于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" != "" ]; thenportal_ssl_port="$6"
fi
echo "portal_ssl_port=" $portal_ssl_port
if [ "$7" != "" ]; thenserve_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_namedocker stop $container_nameecho "docker rm" $container_namedocker 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`.tarecho "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 bashecho "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
ficontainer_name="$1"
image_name="$2"
version="$3"
portal_port="$4"
server_port="$5"
if [ "$6" != "" ]; thenportal_ssl_port="$6"
fi
echo "portal_ssl_port=" $portal_ssl_port
if [ "$7" != "" ]; thenserve_sslr_port="$7"
fiecho "执行docker ps"
docker ps
if [[ "$(docker inspect $container_name 2> /dev/null | grep $container_name)" != "" ]];
then echo $container_name "容器存在,停止并删除"echo "docker stop" $container_namedocker stop $container_nameecho "docker rm" $container_namedocker 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`.tarecho "docker load" $image_name
docker load --input /opt/tmp/$container_name.tarecho "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_nameecho "remove tmp " $image_name
rm -rf /opt/tmp/$container_name.tarecho "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) --forceelse 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) --forceelse 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 anyenvironment { 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) --forceelse 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) --forceelse 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执行构建任务。