Jenkins进阶

Jenkins进阶


使用步骤

1.环境准备

需要一个springcloud项目
需要会一些基本的Dockerfile构建镜像
需要四台centos
搞一台,安装好docker,然后克隆三台就可以了(完整克隆)
太冗长,不是重点,就不上代码了

2.下载harbor

代码如下(示例):

#解压
tar -zxf harbor-offline-installer-v1.9.2.tgz
#移动
mv harbor /opt/
#进入harbor
cd /opt/harbor/
#修改yml文件
vim harbor.yml

Jenkins进阶

#运行prepare
./prepare 
#就会开始下载一些初始化镜像
#2.2.2版本prepare报错了,百度没找到答案,换成和视频相同版本1.9.2
#安装(安装的同时服务就已经开启了,可以用docker ps查看)
./install.sh
#访问harbor
192.168.59.132:85
#默认账号密码
admin--Harbor12345

harbor新建项目(示例):
Jenkins进阶
harbor创建用户(示例):Jenkins进阶
harbor新建成员(即给项目分配权限):
Jenkins进阶

3.将docker镜像push到harbor

代码如下(示例):

#打tag,tag名为 harbor地址+harbor项目名+镜像push后的名字
docker tag eureka-8001:latest 192.168.59.132:85/springcloud/eureka-8001
#修改docker配置文件,将harbor地址添加到docker的信任列表
vim /etc/docker/daemon.json
#修改内容
"insecure-registries": ["192.168.59.132:85"]
#重启docker
systemctl restart docker

Jenkins进阶

#push发现访问被拒绝,因为harbor中的项目为私有,所以需要登入
#登入之前注册的账号,或者harbor默认的账号(Login Succeeded代表登入成功)
docker login -u chen -p Bmw123456 192.168.59.132:85
#push镜像
docker push 192.168.59.132:85/springcloud/eureka-8001

Jenkins进阶

4.把镜像从harbor pull下来

代码如下(示例):

  1. 添加信任列表:不论上传或者下载都需要添加信任列表
  2. 登入,因为私有,需要权限
  3. 回到harbor界面
    Jenkins进阶

5.环境搭建小结

代码如下(示例):

  1. 不管push 还是 pull 都需要登入,登入就需要加入信任列表,加了信任列表需要restart docker
  2. Jenkinsfile要引用变量 "${}" 需要用双引号,单引号不能解析
  3. maven打包插件父项目和common是不需要的
  4. common是依赖父项目的,如果没有父项目的pom,其他依赖common的项目是打不了包的(就算是install了common )
  5. 多看日志,大多数问题静下心看日志都能解决

6.Jenkins+sonarqube+docker+harbor

Jenkins(编译,打包)–>sonarqube(代码审查)–>docker(构建镜像)–>发布到harbor(示例):

编写DockerfileJenkins进阶
编写sonar-project.properties(如果报错说找不到class什么,末尾追加sonar.java.binaries=.)
Jenkins进阶
pom文件添加maven插件(dockerfile 构建 image 插件)

<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>dockerfile-maven-plugin</artifactId>
    <version>1.3.6</version>
    <configuration>
        <repository>${project.artifactId}</repository>
        <!--这个就是dockerfile里面需要传进去的arg参数,动态获取-->
        <buildArgs>
            <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
        </buildArgs>
    </configuration>
</plugin>

创建Jenkins凭证
Jenkins进阶

在Jenkins中使用凭证(流水线语法生成代码)
片段生成器–>withCredentials: Bind credentials to variables–>Username and password (separated)

编写Jenkinsfile

//git凭证
def git_auth="a5851cde-faab-47fb-92f0-7e0066dc110a"
//git url
def git_url="git@gitee.com:jx-chen/jenkins_springcloud.git"
//tag
def tag="latest"
//harbor-url
def harbor_url="192.168.59.132:85"
//harbor项目名
def harbor_project="springcloud"
//harbor凭证id
def harbor_auth="7a206b05-1a6b-46bb-a1ba-b632fd739db1"

node {
    stage('拉取代码') {
        checkout([$class: 'GitSCM', branches: [[name: '*/${branch}']], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]])
    }
    stage('代码审查') {
        script {
            //引用了之前全局工具配置里面配置的sonar-scanner
            scannerHome = tool 'sonar-scanner'
        }
        //引用之前系统配置里面配置的sonarqube-server,sonarqube是当时起的名字
        withSonarQubeEnv('sonarqube') {
            sh """
                cd ${project_name}
                ${scannerHome}/bin/sonar-scanner
               """
        }
    }
    stage('编译,安装公共子工程') {
        sh "mvn clean install"
    }
    stage('编译,打包微服务工程,上传镜像') {
        sh "mvn -f ${project_name} clean package dockerfile:build"
        //定义镜像名字
        def imageName="${project_name}:${tag}"
        //打tag
        sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"
        //登入harbor账号 and push镜像
        withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
            // 登入
            sh "docker login -u ${username} -p ${password} ${harbor_url}"
            // push镜像到harbor
            sh "docker push ${harbor_url}/${harbor_project}/${imageName}"
            sh "echo 镜像push成功"
        }
    }
}

其他服务基本上都差不多
Dockerfile改下端口
sonar-project.properties改下名字
Dockerfile-maven插件复制粘贴
当在harbor中看到上传的镜像,说明成功了

7.从harbor拉取镜像并部署

由Jenkins发送SSH远程调用,并执行被调用者的shell脚本,实现pull and deploy(示例):

搜索插件: Publish Over SSH --> install
Manage Jenkins --> Configure System

#在Jenkins服务器上使用ssh-copy-id将秘钥copy给pull and deploy服务器(需要输入密码)
ssh-copy-id 192.168.59.133

Jenkins进阶
Jenkins进阶

如果测试失败,报错(Failed to add SSH key. Message [invalid privatekey: [B@2a9cc18e])
可能是密匙版本太高,这种开头的(-----BEGIN OPENSSH PRIVATE KEY-----)
使用这个命令重新生成密匙并copy(ssh-keygen -m PEM -t rsa -b 4096)
生成后是这种开头的(-----BEGIN RSA PRIVATE KEY-----)
然后重新copy应该就可以了
生成的密匙都在 /root/.ssh 目录下

编写Jenkinsfile
片段生成器 --> sshPublisher: Send build artifacts over SSH(直接生成,不需要填写,其中最主要的参数为Exec command)Jenkins进阶
可以看到用到了前面创建的SSH Server
execCommand中的意思是去执行这个文件/opt/jenkins_shell/deploy.sh
并且携带了4个参数(最后一个参数port , 添加一个参数化构建)

//部署应用
sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deploy.sh $harbor_url $harbor_project $project_name $tag $port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])

编写shell脚本

#! /bin/sh
#接收外部参数
harbor_url=$1
harbor_project_name=$2
project_name=$3
tag=$4
port=$5
imageName=$harbor_url/$harbor_project_name/$project_name:$tag
echo "$imageName"
#查询容器是否存在,存在则删除
containerId=`docker ps -a | grep -w ${project_name}:${tag} | awk '{print $1}'`
if [ "$containerId" != "" ] ; then
#停掉容器
docker stop $containerId
#删除容器
docker rm $containerId
echo "成功删除容器"
fi
#查询镜像是否存在,存在则删除
imageId=`docker images | grep -w $project_name | awk '{print $3}'`
if [ "$imageId" != "" ] ; then
#删除镜像
docker rmi -f $imageId
echo "成功删除镜像"
fi
# 登录Harbor私服
docker login -u chen -p Bmw123456 $harbor_url
# 下载镜像
docker pull $imageName
# 启动容器
docker run -di -p $port:$port $imageName
echo "容器启动成功"

当容器在master_server服务器启动成功,代表部署成功(不知道为什么shell脚本的echo没有在Jenkins日志中输出)

最后修改application.yaml文件里的地址(改成master_server的地址,全部服务都要改)
Jenkins进阶
费劲千辛万苦,终于都启动了
Jenkins进阶

Jenkins进阶

当执行feign调用时发现异常(java.net.UnknownHostException: cf90055b05e9)
cf90055b05e9为容器id
幸好docker有些基础,默认的network容器之间是无法ping通的
所以要使用自己创建的网络

#创建mynet网络
docker network create mynet
#将所有容器都连接到自己创建mynet上
docker network connect mynet 4bf778c8575d
docker network connect mynet f39997bd7ab2
docker network connect mynet cf90055b05e9
docker network connect mynet 3d64bb63762a
#查看mynet
docker network inspect mynet 

现在feign和gateway都能正常访问了
不知道视频上为什么直接就成功了,有知道的大佬告知下
可以直接修改deploy.sh,启动容器的时候加上参数 --net mynet

8.部署前端网站

代码如下(示例):

前端我没写,我也没有视频源码(记录一下步骤)
搜索插件:NodeJS --> install
Manage Jenkins --> Global Tool Configuration(全局工具配置)
Jenkins进阶
创建一个流水线项目拉取前端代码
前端Jenkinsfile脚本

//gitlab的凭证
def git_auth = "a5851cde-faab-47fb-92f0-7e0066dc110a"
node {
stage('拉取代码') {
checkout([$class: 'GitSCM', branches: [[name: '*/${branch}']],
doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [],
userRemoteConfigs: [[credentialsId: "${git_auth}", url:
'git@192.168.66.100:itheima_group/tensquare_front.git']]])
}
stage('打包,部署网站') {
//使用NodeJS的npm进行打包
nodejs('nodejs12'){
sh '''
npm install
npm run build
'''
}
//=====以下为远程调用进行项目部署========
sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server',
transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '',
execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes:
false, patternSeparator: '[, ]+', remoteDirectory: '/usr/share/nginx/html',
remoteDirectorySDF: false, removePrefix: 'dist', sourceFiles: 'dist/**')],
usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
}

核心代码:
sourceFiles: 'dist/**' (此处为npm build后的代码)
remoteDirectory: '/usr/share/nginx/html'(此处为远程目录)
和之前execCommand(执行远程脚本)不同,此处执行的是一个copy操作,减npm build后的代码copy到远程的/usr/share/nginx/html中(Nginx)

9.持续部署方案优化

代码如下(示例):

上述部署方案存在的问题:

  1. 一次只能选择一个微服务部署
  2. 只有一台生产者部署服务器
  3. 每一个微服务只有一个实例,容错率低

优化方案:

  1. 在一个Jenkins工程中可以选择多个微服务同时发布
  2. 在一个Jenkins工程中可以选择多台生产服务器同时部署
  3. 每个微服务都是以集群高可用形式部署

10.cluster部署

1台Jenkins,一台harbor,两台deploy(一台master,一台slave):

修改eureka的application.yml

spring:
  application:
    name: eureka
---
server:
  port: 8001
spring:
  profiles: eureka-server1
eureka:
  instance:
    hostname: 192.168.59.133
  client:
    service-url:
      defaultZone: http://192.168.59.133:8001/eureka/,http://192.168.59.129:8001/eureka/
---
server:
  port: 8001
spring:
  profiles: eureka-server2
eureka:
  instance:
    hostname: 192.168.59.129
  client:
    service-url:
      defaultZone: http://192.168.59.133:8001/eureka/,http://192.168.59.129:8001/eureka/

创建一个Jenkins流水线项目(从Git上面拿Jenkinsfile)
参数化构建默认是没有多选框的,所以需要去下载一个查件
搜索插件: Extended Choice Parameter --> install
Jenkins进阶
Jenkins进阶
点击构建,就可以看到下面的效果
Jenkins进阶
添加第二台SSH Server
添加docker信任列表
在Jenkins服务器运行: ssh-copy-id 192.168.59.129 (将公匙copy给第二台ssh server)
Jenkins进阶
添加参数化构建(选择服务器 master or slave)
Jenkins进阶Jenkins进阶

修改Jenkinsfile

//git凭证
def git_auth="a5851cde-faab-47fb-92f0-7e0066dc110a"
//git url
def git_url="git@gitee.com:jx-chen/jenkins_springcloud.git"
//tag
def tag="latest"
//harbor-url
def harbor_url="192.168.59.132:85"
//harbor项目名
def harbor_project="springcloud"
//harbor凭证id
def harbor_auth="7a206b05-1a6b-46bb-a1ba-b632fd739db1"

node {
    //获取当前选择项目的名称
    def selectedProjectNames="${project_name}".split(",")
    //获取当前选择服务器的名称
    def selectedServers="${publish_server}".split(",")
    stage('拉取代码') {
        checkout([$class: 'GitSCM', branches: [[name: '*/${branch}']], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]])
    }
    stage('代码审查') {
        //循环遍历出所有的项目名
        for(int i=0;i<selectedProjectNames.length;i++){
            //eureka-8001@8001
            def projectInfo=selectedProjectNames[i];
            //当前遍历的项目名
            def currentProjectName=projectInfo.split("@")[0]
            //当前遍历的端口
            def currentProjectPort=projectInfo.split("@")[1]

            script {
                //引用了之前全局工具配置里面配置的sonar-scanner
                scannerHome = tool 'sonar-scanner'
            }
            //引用之前系统配置里面配置的sonarqube-server,sonarqube是当时起的名字
            withSonarQubeEnv('sonarqube') {
                sh """
                    cd ${currentProjectName}
                    ${scannerHome}/bin/sonar-scanner
                   """
            }
        }

    }
    stage('编译,安装公共子工程') {
        sh "mvn clean install"
    }
    stage('编译,打包微服务工程,上传镜像') {
        //循环遍历出所有的项目名
        for(int i=0;i<selectedProjectNames.length;i++){
            //eureka-8001@8001
            def projectInfo=selectedProjectNames[i];
            //当前遍历的项目名
            def currentProjectName=projectInfo.split("@")[0]
            //当前遍历的端口
            def currentProjectPort=projectInfo.split("@")[1]

            sh "mvn -f ${currentProjectName} clean package dockerfile:build"
            //定义镜像名字
            def imageName="${currentProjectName}:${tag}"
            //打tag
            sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"
            //登入harbor账号 and push镜像
            withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
                // 登入
                sh "docker login -u ${username} -p ${password} ${harbor_url}"
                // push镜像到harbor
                sh "docker push ${harbor_url}/${harbor_project}/${imageName}"
                sh "echo 镜像push成功"
            }
            //遍历所有的服务器分别部署
            for(int y=0;y<selectedServers.length;y++){
                //获取当前服务器的名称
                def currentServer=selectedServers[y]

                //加上的参数格式,通过不同的参数,启动多文档模块下不同的eureka --spring.profiles.active=
                def activeProfile="--spring.profiles.active="
                if(currentServer=="master_server"){
                    activeProfile=activeProfile+"eureka-server1"
                }else if(currentServer=="slave_server"){
                    activeProfile=activeProfile+"eureka-server2"
                }
                //部署应用
                sshPublisher(publishers: [sshPublisherDesc(configName: "${currentServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deployCluster.sh $harbor_url $harbor_project $currentProjectName $tag $currentProjectPort $activeProfile", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
            }
        }
    }
}

Jenkins进阶
Jenkins进阶

记得改名字,给执行权限
可以看到已经成功了
Jenkins进阶

11.Jenkins主从架构

代码如下(示例):

Manage Jenkins --> Configure Global Security --> 代理
Jenkins进阶
Manage Jenkins --> Manage Nodes and Clouds
Jenkins进阶
Jenkins进阶

将jar包放在root目录下,然后执行命令(当然需要java环境)

java -jar agent.jar -jnlpUrl http://192.168.59.131:8888/computer/slave1/jenkins-agent.jnlp -secret 65bd25c2158b08eeba6867864f924096496cea5169e71a00222a34ac3a4586ee -workDir "/root/jenkins"

在会到Jenkins刷新页面,可以看到已经连接了
Jenkins进阶
创建一个*风格项目,并让其在salve1节点上运行
Jenkins进阶
当然要确保salve1服务器上面有git环境,不然会报错,无法init
在slave1节点的工作目录(前面配置的),里面可以看到workspace,workspace内存放的就是刚刚拉取下来的项目了

创建流水线项目,并指定节点
Jenkins进阶
一旦退出这个界面,连接就会断开
Jenkins进阶

12.引入kubernates(即 k8s),集群配置失败,无奈转战尚硅谷k8s最新视频

传统Jenkins的master-slave方案的缺陷:

  1. master节点发生单点故障时,整个流程都不可用了
  2. 每个slave节点的配置环境不一样,来完成不同语言的编译打包等操作,但是这些差异化的配置导致管理起来非常不方便,维护起来也是比较费劲
  3. 资源分配不均衡,有的slave节点要运行的job出现排队等待,而有的slave节点处于空闲状态
  4. 资源浪费,每台slave节点可能是实体机或者VM,当slave节点处于空闲状态时,也不会完全释放掉资源

kubernates简介:

kubernates(简称,k8s)是Google开源的容器集群管理系统,在docker技术的基础上,为容器化的应用提供部署运行,资源调度,服务发现和动态伸缩等一系列完整功能,提高了大规模容器集群管理的便捷性
其主要功能如下:

  1. 使用docker对应用程序包装(package),实例化(instantiate),运行(run)
  2. 以集群的方式运行,管理跨机器的容器
  3. 解决docker跨机器容器之间的通讯问题
  4. kubernates的自我修复机制使得容器集群总是运行在用户期望状态

kubernates+docker+Jenkins持续集成架构图:
Jenkins进阶
kubernates+docker+Jenkins持续集成方案好处

  1. 服务高可用: 当Jenkins master出现故障时,kubernates会自动创建一个新的Jenkins master容器,并且将volume分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用
  2. 动态伸缩,合理使用资源: 每次运行job时,会自动创建一个Jenkins slave,job完成后,salve自动注销并删除容器,资源自动释放,而且kubernates会根据每个资源的使用情况,动态分配slave到空闲的节点上创建,降低出现因某节点资源利用率高,还排队等待在该节点的情况
  3. 扩展性好: 当kubernates集群的资源严重不足而导致job排队等待时,可以很容易的添加kubernates node到集群中,从而实现扩展

13.kubernates环境搭建

4台centos(1台harbor,1台k8s-master,两台k8s-slave):Jenkins进阶
设置主机名及修改hosts文件

#修改k8s-master主机名
hostnamectl set-hostname k8s-master
#修改k8s-node1主机名
hostnamectl set-hostname k8s-node1
#修改k8s-node2主机名
hostnamectl set-hostname k8s-node2
#查看主机名
hostname

前面主机名是点对点,这个是全局的

cat>>/etc/hosts<<EOF
192.168.59.135 k8s-master
192.168.59.136 k8s-node1
192.168.59.137 k8s-node2
EOF
#查看hosts文件
cat /etc/hosts

关闭防火墙(全局)

#关闭
systemctl stop firewalld
#关闭开启启动
systemctl disable firewalld
#临时关闭
setenforce 0
#永久关闭
#修改selinux
vim /etc/sysconfig/selinux
#改为
SELINUX=disabled

设置允许路由转发,不对bridge的数据进行处理(全部)

#创建文件
vi /etc/sysctl.d/k8s.conf
#文件内容
net.bridge.bridge-nf-call-ip6tables = 1 
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1 
vm.swappiness = 0
#执行文件
sysctl -p /etc/sysctl.d/k8s.conf

kube-proxy开启ipvs的前置条件(全部)

#写入脚本
cat > /etc/sysconfig/modules/ipvs.modules <<EOF
#!/bin/bash
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack
EOF
#给权限,并执行
chmod 755 /etc/sysconfig/modules/ipvs.modules && bash /etc/sysconfig/modules/ipvs.modules && lsmod | grep -E 'ip_vs|nf_conntrack'

报错: nf_conntrack_ipv4 not found

解决办法(百度来的,不知道行不行) :跳转链接
将 nf_conntrack_ipv4 改为 nf_conntrack

关闭swap(全部)

#又回来了,之前注释swap分区导致虚拟机打不开的问题解决了
#临时关闭
swapoff -a

第一步: vim /etc/fstab
Jenkins进阶
第二部: vim /etc/default/grub
Jenkins进阶
第三步: grub2-mkconfig -o /boot/grub2/grub.cfg

安装kubelet,kubeadm,kubectl(全部)

#清空yum缓存
yum clean all
#设置yum镜像源
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes Repository
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=0
EOF
#安装
yum install -y kubelet kubeadm kubectl
#设置开机启动
systemctl enable kubelet
#查看版本
kubelet --version

master节点单独操作(全部)

#注意: version就是当前版本,addr就是当前master的ip
kubeadm init --kubernetes-version=1.21.1 \
--apiserver-advertise-address=192.168.59.135 \
--image-repository registry.aliyuncs.com/google_containers \
--service-cidr=10.1.0.0/16 \
--pod-network-cidr=10.244.0.0/16

总结几点

  1. CPU要大于2
  2. 内存最小2G(2g卡死了,3g吧)
  3. 关闭swap分区
    照着前面的方法init还是报错swap的话,在执行一遍 swapoff -a
  4. 警告:[WARNING IsDockerSystemdCheck]: detected “cgroupfs” as the Docker cgroup driver
    vim /etc/docker/daemon.json
    加入这一段 "exec-opts":["native.cgroupdriver=systemd"]
    然后重启docker
  5. 查看日志
    journalctl -xeu kubelet
  6. 杂项
    #查看版本(sort是排序)
    yum --showduplicates list PACKAGE | sort -r
    #安装指定版本
    yum install PACKAGE-VERSION
    #初始化(对于init失败情况)
    kubeadm reset

Jenkins进阶

#执行提示命令
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
#安装Calico(用于master和slave之间的网络通讯)
cd /root/ && mkdir k8s && cd /root/k8s
#下载yaml文件(下载不下来的话打开网页复制粘贴)
wget https://docs.projectcalico.org/v3.10/gettingstarted/kubernetes/installation/hosted/kubernetes-datastore/caliconetworking/1.7/calico.yaml
#修改通讯地址
sed -i 's/192.168.0.0/10.244.0.0/g' calico.yaml
#安装calico
kubectl apply -f calico.yaml
#查看所有命名空间的pod
kubectl get pod --all-namespaces
#等待所有pod处于READY状态(大概几分钟)
NAMESPACE	NAME	READY	STATUS

总结

文章主要内容来自B站黑马
对于一个学java的我来说,k8s搭建还是太难了,比之前seata环境都难
几乎是每一步都有新的问题,百度也找不到的那种

上一篇:【Harbor】Harbor修改暴露端口


下一篇:harbor更新SSL证书