Skip to content

加餐1:搭建基于K8和Docker的Jenkin可伸缩持续集成系统

根据前面用户的反馈,这里补充一个完整的动手实践的案例------搭建"基于 K8s 和 Docker 的 Jenkins 可伸缩持续集成系统",让模块 3 所介绍的内容落地。


这部分内容比较多且非常具体,包括 4 大部分:

  • Kubernetes (K8s)集群的部署,包括 kube-proxy、kubelet、docker 和 flanneld services 等安装;

  • 企业级容器注册管理平台 Harbor 的安装部署,包括 Docker、Docker Compose 等安装;

  • 采用 Jenkins pipeline 实现自动构建并部署至 K8s,包括建立 spring boot 示例工程、创建 Dockerfile 和 Jenkinsfile、配置 jenkins pipeline 任务和 K8s 的 kube.config 到最后测试 pipeline 任务等;

  • 遇到的问题(坑)及解决方法,比如启动 Jenkins,安装插件出现"无法连接服务器"错误,运行 pipeline,出现 command not found 错误等几个问题的解决。


这些具体的操作步骤经过了真实环境上的检验,最终整个基于 K8s 、Docker、Jenkins 的 CI 系统被成功部署起来。建议你按照下面介绍的详细步骤,自己亲自动手操作一回,功力会大增。

工作流程图

系统配置

Harbor 仓库 CentOS7、4 核 CPU、16G 内存、160G 硬盘

  • 192.168.10.160 harbor

集群 3 台机器,CentOS7、4 核 CPU、16G 内存、60G 硬盘

  • 192.168.10.161 k8s-master

  • 192.168.10.162 k8s-node1

  • 192.168.10.163 k8s-node2

Kubernetes 集群部署

安装前准备

(1)关闭 firewalld 改用 iptables。输入以下命令,关闭 firewalld:


[root@master ~]# systemctl stop firewalld.service #停止firewall 
[root@master ~]# systemctl disable firewalld.service #禁止firewall开机启动

(2)安装 ntp 服务:


[root@master ~]# yum install -y ntp wget net-tools 
[root@master ~]# systemctl start ntpd 
[root@master ~]# systemctl enable ntpd

安装配置

(1)安装 Kubernetes Master

使用以下命令安装 kubernetes 和 etcd:


# yum install -y kubernetes etcd

编辑 /etc/etcd/etcd.conf 使 etcd 监听所有的 IP 地址,确保下列行没有注释,并修改为下面的值:


[root@master ~]# cat /etc/etcd/etcd.conf 
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379" 
#[cluster] 
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.10.161:2379"

编辑 Kubernetes API server 的配置文件 /etc/kubernetes/apiserver,确保下列行没有被注释,并设置合适的值:


[root@master ~]# cat /etc/kubernetes/apiserver
###
# kubernetes system config
#
# The following values are used to configure the kube-apiserver
#

# The address on the local server to listen to.
KUBE_API_ADDRESS="--address=0.0.0.0"

# The port on the local server to listen on.
KUBE_API_PORT="--port=8080"

# Port minions listen on
KUBELET_PORT="--kubelet_port=10250"

# Comma separated list of nodes in the etcd cluster
KUBE_ETCD_SERVERS="--etcd_servers=http://192.168.10.161:2379"

# Address range to use for services
KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16"

# default admission control policies
KUBE_ADMISSION_CONTROL="--admission_control=NamespaceLifecycle,NamespaceExists,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota"

# Add your own!
KUBE_API_A

启动 etcd、kube-apiserver、kube-controller-manager and kube-scheduler 服务,并设置开机自启:


[root@master ~]# cat /script/kubenetes_service.sh

for SERVICES in etcd kube-apiserver kube-controller-manager kube-scheduler; do 
    systemctl restart $SERVICES
    systemctl enable $SERVICES
    systemctl status $SERVICES 
done
[root@master ~]# sh /script/kubenetes_service.sh

在 etcd 中定义 flannel network 的配置,这些配置会被 flannel service 下发到 nodes 中:


[root@master ~]# etcdctl mk /centos.com/network/config '{"Network":"172.17.0.0/16"}'

添加 iptables 规则,打开相应的端口:


[root@master ~]# iptables -I INPUT -p tcp --dport 2379 -j ACCEPT
[root@master ~]# iptables -I INPUT -p tcp --dport 10250 -j ACCEPT
[root@master ~]# iptables -I INPUT -p tcp --dport 8080 -j ACCEPT 
[root@master ~]# iptables-save

或者写入 iptables 配置文件 /etc/sysconfig/iptables。


查看节点信息(我们还没有配置节点信息,所以这里应该为空):


[root@master ~]# kubectl get nodes

NAME LABELS STATUS

(2)安装 Kubernetes Nodes

注:下面这些步骤应该在 node1 和 node2 上执行(也可以添加更多的 node)。


使用 yum 安装 kubernetes 和 flannel:


[root@slave1 ~]# yum install -y flannel kubernetes

为 flannel service 配置 etcd 服务器,编辑 /etc/sysconfig/flanneld 文件中的下列行以连接到 master:


[root@slave1 ~]# cat /etc/sysconfig/flanneld

FLANNEL_ETCD="http://192.168.10.161:2379" #改为etcd服务器的ip
FLANNEL_ETCD_PREFIX="/centos.com/network"

编辑 /etc/kubernetes/config 中 kubernetes 的默认配置,以确保 KUBE_MASTER 的值连接到 Kubernetes master API server:


[root@slave1 ~]# cat /etc/kubernetes/config

KUBE_MASTER="--master=http://192.168.10.161:8080"

编辑 /etc/kubernetes/kubelet 中五个参数的值:


node1:


[root@slave1 ~]# cat /etc/kubernetes/kubelet

KUBELET_ADDRESS="--address=0.0.0.0"
KUBELET_PORT="--port=10250"
KUBELET_HOSTNAME="--hostname_override=192.168.10.162"
KUBELET_API_SERVER="--api_servers=http://192.168.10.161:8080"
KUBELET_ARGS=""

node2:


[root@slave2 ~]# cat /etc/kubernetes/kubelet

KUBELET_ADDRESS="--address=0.0.0.0"
KUBELET_PORT="--port=10250"
KUBELET_HOSTNAME="--hostname_override=192.168.10.163"
KUBELET_API_SERVER="--api_servers=http://192.168.10.161:8080"
KUBELET_ARGS=""

启动 kube-proxy、kubelet、docker 和 flanneld services 服务,并设置开机自启:


[root@slave1 ~]# cat /script/kubernetes_node_service.sh

for SERVICES in kube-proxy kubelet docker flanneld; do 
systemctl restart $SERVICES
systemctl enable $SERVICES
systemctl status $SERVICES 
done

在每个 node 节点上,你应当注意到有两块新的网卡 docker0 和 flannel0,应该得到不同的 IP 地址范围在 flannel0 上,就像下面这样:


node1:


[root@slave1 ~]# ip a | grep docker | grep inet
inet 172.17.0.1/16 scope global docker0

node2:


[root@slave2 ~]# ip a | grep docker | grep inet
inet 172.17.60.0/16 scope global docker0

添加 iptables 规则:


[root@slave1 ~]# iptables -I INPUT -p tcp --dport 2379 -j ACCEPT
[root@slave1 ~]# iptables -I INPUT -p tcp --dport 10250 -j ACCEPT
[root@slave1 ~]# iptables -I INPUT -p tcp --dport 8080 -j ACCEPT

现在登录 kubernetes master 节点验证 minions 的节点状态:


[root@master ~]# kubectl get nodes
NAME           STATUS    AGE
192.168.10.162   Ready     2h
192.168.10.163   Ready     2h

至此,Kubernetes 集群已经配置并运行了,然后我们继续下面的步骤。

Harbor 安装部署

Harbor 是 VMWare 公司开源的企业级 Docker Registry 项目,项目地址是https://github.com/goharbor/harbor

下载离线安装包

下载地址 https://github.com/goharbor/harbor/releases



机器配置要求:



将下载的安装包上传到服务器,运行以下命令解压:


tar zxvf harbor-offline-installer-v1.10.1.tgz

安装 Docker

# 安装依赖包
yum install -y yum-utils device-mapper-persistent-data lvm2
# 添加Docker软件包源
yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo
# 安装Docker CE
yum install -y docker-ce
# 启动Docker服务并设置开机启动
systemctl start docker
systemctl enable docker

安装 docker-compose

Docker Compose 是 Docker 提供的一个命令行工具,用来定义和运行由多个容器组成的应用。使用 compose,我们可以通过 YAML 文件声明式的定义应用程序的各个服务,并由单个命令完成应用的创建和启动。


执行以下命令进行安装:


yum install epel-release 
yum install -y python-pip 
pip install docker-compose 
yum install git

Harbor 安装与配置

修改 harbor.yml:

  • hostname 这里设置本机的 IP

  • harbor_admin_password web 页面的密码

运行:


sh ./install.sh

访问页面 http://192.168.10.160/


Docker 主机访问 Harbor

在另外一个服务器(client)登录 Harbor:


# docker login 192.168.10.160
Username: admin
Password: 
Error response from daemon: Get https:// 192.168.10.160/v2/: dial tcp 192.168.10.160:443: connect: connection refused

这是因为 docker1.3.2 版本开始默认 docker registry 使用的是 https,我们设置 Harbor 默认为 http 方式,所以当执行用 docker login、pull、push 等命令操作而非 https 的 docker regsitry 的时就会报错。

解决 https

在 harbor 那台服务器上,其安装目录:


vi docker-compose.yml


修改 ports 信息


然后我们执行:


docker-compose stop
./install.sh

然后同时编辑 harbor 和 client 的 docker 配置文件:


# 1.
vim /etc/docker/daemon.json
 
{
 "insecure-registries": ["192.168.10.160"]
}
 
# 2.添加ExecStart=/usr/bin/dockerd |--insecure-registry=192.168.10.160
vim /usr/lib/systemd/system/docker.service
 
# 把这行注释掉,添加下面的配置 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecStart=/usr/bin/dockerd
  |--insecure-registry=192.168.10.160

  1. 重启 docker

systemctl daemon-reload
systemctl restart docker

2.重启 harbor 的 docker-compose,命令如下:


docker-compose restart

client 登录仓库

# docker login 192.168.10.160
Username: admin
Password: 
Login Succeeded

采用 jenkins pipeline 实现自动构建并部署至 K8s

部署 jenkins

这里采用 yum install 的方式部署 jenkins。


(1)安装JDK:


yum install -y java

(2)安装 jenkins


添加 Jenkins 库到 yum 库,Jenkins 将从这里下载安装。


wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key
yum install -y Jenkins

(3)配置 jenkis 的端口:


vi /etc/sysconfig/jenkins

找到修改端口号:


JENKINS_PORT="8085"  此端口不冲突可以不修改

(4)启动 jenkins:


service jenkins start/stop/restart

(5)访问 http://localhost:8085 地址,等待出现下面解锁 jenkins 界面:



(6)在上图提示的密码文件中复制自动生成的密码。

(7)在解锁 jenkins 页面,粘贴密码并继续。

(8)解锁 jenkins 后,在界面中选择"安装建议的插件" 选项。

(9)最后,jenkins 要求创建管理员用户,创建新用户或使用 admin 用户,按照步骤完成后即可登录并使用 jenkis 了。

准备 java 示例工程

下面新建 spring boot 示例工程,示例工程的代码地址为:https://github.com/gemedia/docker-demo

创建 spring boot 示例工程

(1)生成 spring boot 基础工程,添加一个示例 Controller 类。


package com.docker.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @GetMapping("")
    public String hello() {
        return "Hello!";
    }

(2)修改 application 配置文件,设置端口。


spring.application.name=docker-demo
server.port=40080

(3)编译运行,访问 http://localhost:40080 地址可以看到示例运行结果。


添加 Dockerfile

在工程根目录创建 Dockerfile,用来构建 docker 镜像,其中 ${JAR_FILE} 参数在 pipeline 执行 docker build 时,通过 build-arg 参数传入。


FROM openjdk:8-jdk-alpine

#构建参数
ARG JAR_FILE
ARG WORK_PATH="/opt/demo"
# 环境变量
ENV JAVA_OPTS="" \
    JAR_FILE=${JAR_FILE}

#设置时区
RUN apk update && apk add ca-certificates && \
    apk add tzdata && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

COPY target/$JAR_FILE $WORK_PATH/

WORKDIR $WORK_PATH

ENTRYPOINT exec java $JAVA_OPTS -jar $JAR_

添加 K8s 的 Deployment 配置

在工程根目录创建 k8s-deployment.tpl 文件,此文件用来作为 K8s 的 yaml 文件模板。在 jenkens pipeline 执行时,会先将 tpl 文件中 {} 括起来的自定义参数用 sed 命令替换为实际的内容。


apiVersion: apps/v1
kind: Deployment
metadata:
  name: {APP_NAME}-deployment
  labels:
    app: {APP_NAME}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {APP_NAME}
  template:
    metadata:
      labels:
        app: {APP_NAME}
    spec:
      containers:
      - name: {APP_NAME}
        image: {IMAGE_URL}:{IMAGE_TAG}
        ports:
        - containerPort: 40080
        env:
          - name: SPRING_PROFILES_ACTIVE
            value: {SPRING_PROFILE}

添加 Jenkinsfile

在工程根目录创建 Jenkinsfile,用来执行 jenkins pipeline 任务。Jenkinsfile 文件的大概内容描述如下。


environment 中变量说明:

  • HARBOR_CREDS 为 harbor 镜像仓库的用户密码,数据保存为 jenkins 的"username and password"类型的凭据,用 credentials 方法从凭据中获取,使用时通过 HARBOR_CREDS_USR 获取用户名,HARBOR_CREDS_PSW 获取密码;

  • K8S_CONFIG 为 K8s 中 kubectl 命令的 yaml 配置文件内容,数据保存为 jenkins 的"Secret Text"类型的凭据,用 credentials 方法从凭据中获取,这里保存的 yaml 配置文件内容以 base64 编码格式保存,在设置凭据时先要进行 base64 编码(此 base64 编码是非必须的,如果直接保存原文,下面 Jenkinsfile 中需要去掉 base64 -d 解码) ;

  • GIT_TAG 变量通过执行 sh 命令获取当前 git 的 tag 值,由于后面构建 docker 镜像时使用 git 的 tag 作为镜像的标签,所以这个变量也不能为空。


parameters 中变量说明:

  • HARBOR_HOST,harbor 镜像仓库地址;

  • DOCKER_IMAGE,docker 镜像名,包含 harbor 项目名称;

  • APP_NAME,K8s 中的标签名称,对应 K8s 的 yaml 模板中的 {APP_NAME};

  • K8S_NAMESPACE,K8s 中的 namespace 名称,执行 kubectl 命令会部署至此命名空间。


stages 说明:

  • Maven Build,使用 docker 的方式执行 maven 命令,args 参数中将 .m2 目录映射出来,避免执行时重复从远端获取依赖;stash 步骤中将 jar 文件保存下来,供后面的 stage 使用;

  • Docker Build,unstash 获取 jar 文件,通过 sh 依次执行 docker 命令登录 harbor、构建镜像、上传镜像、移除本地镜像,构建镜像时,会获取 jar 文件名传入 JAR_FILE 参数;

  • Deploy,使用 docker 的方式执行 kubectl 命令,在执行前先将 K8S_CONFIG 中的内容进行 base64 解密并存为 ~/.kube/config 配置文件,然后执行 sed 命令将 k8s-deployment.tpl 文件中"{参数名}"形式参数替换为实际的参数值,最后执行 kubectl 命令部署至 K8s。


// 需要在jenkins的Credentials设置中配置jenkins-harbor-creds、jenkins-k8s-config参数
pipeline {
    agent any
    environment {
        HARBOR_CREDS = credentials('jenkins-harbor-creds')
        K8S_CONFIG = credentials('jenkins-k8s-config')
        GIT_TAG = sh(returnStdout: true,script: 'git describe --tags --always').trim()
    }
    parameters {
        string(name: 'HARBOR_HOST', defaultValue: '192.168.10.160', description: 'harbor仓库地址')
        string(name: 'DOCKER_IMAGE', defaultValue: 'tssp/pipeline-demo', description: 'docker镜像名')
        string(name: 'APP_NAME', defaultValue: 'pipeline-demo', description: 'k8s中标签名')
        string(name: 'K8S_NAMESPACE', defaultValue: 'demo', description: 'k8s的namespace名称')
    }
    stages {
        stage('Maven Build') {
            when { expression { env.GIT_TAG != null } }
            agent {
                docker {
                    image 'maven:3-jdk-8-alpine'
                    args '-v $HOME/.m2:/root/.m2'
                }
            }
            steps {
                sh 'mvn clean package -Dfile.encoding=UTF-8 -DskipTests=true'
                stash includes: 'target/*.jar', name: 'app'
            }

        }
        stage('Docker Build') {
            when { 
                allOf {
                    expression { env.GIT_TAG != null }
                }
            }
            agent any
            steps {
                unstash 'app'
                sh "docker login -u ${HARBOR_CREDS_USR} -p ${HARBOR_CREDS_PSW} ${params.HARBOR_HOST}"
                sh "docker build --build-arg JAR_FILE=`ls target/*.jar |cut -d '/' -f2` -t ${params.HARBOR_HOST}/${params.DOCKER_IMAGE}:${GIT_TAG} ."
                sh "docker push ${params.HARBOR_HOST}/${params.DOCKER_IMAGE}:${GIT_TAG}"
                sh "docker rmi ${params.HARBOR_HOST}/${params.DOCKER_IMAGE}:${GIT_TAG}"
            }
            
        }
        stage('Deploy') {
            when { 
                allOf {
                    expression { env.GIT_TAG != null }
                }
            }
            agent {
                docker {
                    image 'lwolf/helm-kubectl-docker'
                }
            }
            steps {
                sh "mkdir -p ~/.kube"
                sh "echo ${K8S_CONFIG} | base64 -d > ~/.kube/config"
                sh "sed -e 's#{IMAGE_URL}#${params.HARBOR_HOST}/${params.DOCKER_IMAGE}#g;s#{IMAGE_TAG}#${GIT_TAG}#g;s#{APP_NAME}#${params.APP_NAME}#g;s#{SPRING_PROFILE}#k8s-test#g' k8s-deployment.tpl > k8s-deployment.yml"
                sh "kubectl apply -f k8s-deployment.yml --namespace=${params.K8S_NAMESPACE}"
            }
            
        }
        
    }
}

配置 jenkins pipeline 任务

创建 jenkins pipeline 任务,并设置需要的参数。

新建 pipeline 任务

单击"新建任务"按钮,输入名称并选择"流水线"(pipeline),然后单击"确定"按钮。


配置 pipeline 任务

进入任务的配置界面,在流水线(pipeline)设置部分,选择"Pipeline script from SCM"选项。SCM 选项选为"Git",配置好工程的 git 地址以及获取代码的凭证信息;然后在"Additional Behaviours"中添加"Clean before checkout"。


配置 harbor 账号与密码

选择"凭据",然后在下图所示位置单击"添加凭据"按钮。在新凭据设置界面,类型选择为"Username with password",ID 设置为"jenkins-harbor-creds"(此处的 ID 必须与 Jenkinsfile 中的保持一致)。Username 与 Password 分别设置为 harbor 镜像私库的用户名和密码。


配置 K8s 的 kube.config 信息

在 K8s 中使用 kubectl 命令时需要 yaml 格式的服务器以及授权信息配置文件,这里将 kubectl 的 yaml 配置文件的内容以 base64 编码后保存在 jenkins 的凭据中。pipeline 任务执行时,先从 jenkins 凭据中获取内容,进行 base64 解码后将配置保存为 ~/.kube/config 文件。kubectl 的配置文件的内容如下:


apiVersion: v1
kind: Config
clusters:
- name: "test"
  cluster:
    server: "https://xxxxx"
    api-version: v1
    certificate-authority-data: "xxxxxx"

users:
- name: "user1"
  user:
    token: "xxxx"

contexts:
- name: "test"
  context:
    user: "user1"
    cluster: "test"

current-context: "tes

可以在 Linux 中采用下面命令将 kubectl 的 yaml 配置文件进行 base64 编码。


base64 kube-config.yml > kube-config.txt

然后类似上一步,在 jenkins 凭据中增加配置文件内容。在凭据设置界面,类型选择为"Secret text",ID 设置为"jenkins-k8s-config"(此处的 ID 必须与 Jenkinsfile 中的保持一致),Secret 设置为上面经过 base64 编码后的配置文件内容。


测试 pipeline 任务

在创建的 pipeline 任务中,单击"Build With Parameters"按钮,即可立即执行 pipeline 任务。



在当前界面中查看任务的执行结果,可以看到每个阶段的运行状态。



执行成功后,查看 harbor 镜像仓库,docker 镜像成功上传至 harbor。



在 Linux 服务器查看 deployment,运行以下命令:


kubectl get deployment


查看 pod:


kubectl get pod

遇到的问题及解决方法

(1)启动 Jenkins,安装插件出现"无法连接服务器"错误


安装插件那个页面,就是提示你 offline 的那个页面,不要动,然后打开一个新的 tab,输入网址 http://localhost:port/pluginManager/advanced。


这里面最底下有个"Update Site",把其中的链接改成http://mirror.esuni.jp/jenkins/updates/update-center.json。


单击 submit 按钮:



然后重新启动 jenkins,这样就能正常安装插件。


(2)运行 pipeline,出现 command not found 错误


yum 安装的 Jenkins 配置文件默认位置 /etc/sysconfig/jenkins,默认 jenkins 服务以 jenkins 用户运行,这时在 jenkins 执行 ant 脚本时可能会发生没有权限删除目录、覆盖文件等情况。可以让 jenkins 以 root 用户运行来解决这个问题。


a.将 jenkins 账号分别加入到 root 组中:


gpasswd -a jenkins root

b.修改 /etc/sysconfig/jenkins 文件:


#user id to be invoked as (otherwise will run as root; not wise!)
JENKINS_USER=root
JENKINS_GROUP=root

可以修改为 root 权限运行,重启 jenkins 服务。


(3)在 docker build 阶段出现 exec: "docker-proxy": executable file not found in $PATH 错误,解决方法是,需要启动 docker-proxy:


cd /usr/libexec/docker/
 ln -s docker-proxy-current docker-proxy

(4)出现 Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? 错误,运行如下命令:


systemctl daemon-reload
service docker restart

(5)出现 shim error: docker-runc not installed on system. 错误,经过一番排查,如下解决方案有用:


cd /usr/libexec/docker/
 ln -s docker-runc-current docker-runc