微服务CI/CD实践(五)Jenkins & Docker 自动化构建部署Node服务

微服务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本地编译项目在构建镜像

bash 复制代码
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环境

bash 复制代码
# 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配置

bash 复制代码
cd /data/container/nginx/etc
vi nginx.conf
# 编写配置并保存

vi mime.types
# 编写配置并保存

以下为nginx.conf配置示例

bash 复制代码
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示例

bash 复制代码
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。

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

step3 入参赋值

如果有参数传入,则赋值参数

bash 复制代码
# 前五个参数是必传参数,无需判断直接赋值
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 停止并删除容器

bash 复制代码
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 停止并删除镜像

bash 复制代码
# 删除镜像
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 备份和加载安装包

bash 复制代码
#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 执行运行镜像命令

bash 复制代码
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 执行删除安装包命令

bash 复制代码
echo "remove tmp " $image_name
rm -rf /opt/tmp/$container_name.tar

以下为完整的安装部署脚本

bash 复制代码
#!/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 配置全局变量

bash 复制代码
environment {   
    REPOSITORY="http://192.168.1.101:8929/hka/hka-admin-wocwin.git"
	projectdir="hka-web-01"
	projectname="hka-admin-wocwin"
}

step2 获取代码

检出选择指定git分支的代码

typescript 复制代码
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的环境变量,否则执行编译命令会抛异常

typescript 复制代码
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步骤

typescript 复制代码
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 构建镜像

typescript 复制代码
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目录下

typescript 复制代码
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安装部署脚本

typescript 复制代码
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
                        )
                    ]
                )
            }
        }

以下为完整流水线定义

typescript 复制代码
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执行构建任务。

相关推荐
zeruns80238 分钟前
如何搭建自己的域名邮箱服务器?Poste.io邮箱服务器搭建教程,Linux+Docker搭建邮件服务器的教程
linux·运维·服务器·docker·网站
爱跑步的程序员~39 分钟前
Docker
docker·容器
疯狂的大狗1 小时前
docker进入正在运行的容器,exit后的比较
运维·docker·容器
长天一色2 小时前
【Docker从入门到进阶】01.介绍 & 02.基础使用
运维·docker·容器
伊玛目的门徒2 小时前
docker 搭建minimalist-web-notepad
运维·docker·notepad
theo.wu4 小时前
使用Buildpacks构建Docker镜像
运维·docker·容器
wusam13 小时前
螺蛳壳里做道场:老破机搭建的私人数据中心---Centos下Docker学习04(环境准备)
学习·docker·centos
wusam16 小时前
螺蛳壳里做道场:老破机搭建的私人数据中心---Centos下Docker学习03(网络及IP规划)
运维·服务器·网络·docker·容器
一直在进步的派大星16 小时前
Docker 从安装到实战
java·运维·docker·微服务·容器
henan程序媛20 小时前
Jenkins Pipline流水线
运维·pipeline·jenkins