【实战篇】踩坑无数,助你无痛基于docker部署jenkins+rancher

0x01:前提概要

本篇做了两种方案,一种将AWS-ECR作为镜像仓库来使用,另外一种是自己搭建的Harbor仓库。(测试环境跑在内网虚拟机,线上环境跑在公网AWS云主机,所以采用了两套仓库部署)

0x02:关键名词解释

github: 代码托管工具
docker: 容器操作工具
kubectl: k8s集群客户端
jenkins: CI/CD工具
rancher: k8s集群管理工具
harbor: docker开源镜像仓库
aws-cli: AWS命令行工具
aws-ecr: 亚马逊云镜像仓库

0x03:服务器资源介绍

本次实验使用了2台4c8g的虚拟服务器,操作系统为centos7.3,服务器资源描述如下:
172.100.10.13 【关闭selinux、安装docker、docker-compose、kubectl】
172.100.10.23 【关闭selinux、安装docker】
我们选用172.100.10.13这台服务器作为master节点,它上面会部署jenkins、rancher以及harbor,整个服务部署情况如下图所示:


部署结构图.png

我们预先在172.100.10.13上创建好如下的目录结构:

opt/
├── docker
│   ├── jenkins
│   │   └── home
│   └── rancher_home
│   │   ├── auditlog
│   │   └── rancher
│   └── harbor

注意把/opt/docker/jenkins/home目录用户设置给jenkins用户,否则容器内无法写入该目录,~/.docker 目录也是如此,因为jenkins容器内部需要登录镜像仓库,登录操作是需要修改~/.docker/config.json文件的(参考资料

chown -R 1000 home
chown -R 1000 /root/.docker
0x04:打通AWS服务,安装配置aws-cli (如果走的是Harbor仓库,这步跳过)

由于我们需要依赖aws的镜像仓库服务,所以需要有能操作aws的客户端工具,AWS官方提供了两种使用方式,一种是在宿主机安装,另外一种直接通过docker来完成,因为宿主机要配置aws,所以我先采用的第一种方式,参考连接:
aws-cli安装
aws-cli配置
通过以上,我们已经可以对aws进行基本的操作,通过以下命令来检测,如果能正常回显说明配置完成。

aws configure get region

配置完成以后我们回到用户目录下,看一下生成了.aws目录,里面包含了两个文件,这里先放一遍,后面我们会用到。

.aws/
├── config
└── credentials

另外一种方式可以通过docker来启动aws-cli,这里我们也可以尝试一下,后面在容器内部使用aws-cli就是用的这种方式:

docker run --rm -it -v /root/.aws:/root/.aws amazon/aws-cli configure get region

可以看到跟上面执行的结果是一样的。

0x05:安装Harbor (如果走的是AWS-ECR仓库,这步跳过)

1.进入 /opt/docker/harbor 目录,下载harbor程序包并解压,本次实验用的版本是v1.10.3;
2.进入harbor目录,找到harbor.yml,修改 hostname、port并且注释掉https访问(没有证书);
3.执行 ./install.sh ,等待下载镜像,安装完毕以后当前目录下会生成 docker-compose.yml 文件,后面可以通过docker-compose来启停harbor服务;
4.安装启动完成以后,访问 http://172.100.10.13:9090/ 即可看到harbor登录界面。
整体的安装步骤可以参考 https://www.jianshu.com/p/6fcacc2020d5
【注意】这里有个需要注意的坑,因为我们的harbor是用的http协议,在执行docker pull的时候会提示:

[root@cpe-172-100-10-13 docker]# docker pull 172.100.10.13:9090/test/demo1:20200623102639
Error response from daemon: Get https://172.100.10.13:9090/v2/: http: server gave HTTP response to HTTPS client

怎么解决该问题,网上目前搜到的方式是加docker配置文件: /etc/docker/daemon.json,文件内容具体如下:

{
  "insecure-registries": ["172.100.10.13:9090"]
}

然后重启docker即可。(生产环境还是推荐配置https)

0x06: 部署Rancher,创建K8S集群

我们采用docker的方式部署,我这里选择的rancher版本是2.4.3

sudo docker run -d --restart=unless-stopped -v /opt/docker/rancher_home/rancher:/var/lib/rancher/ -v /opt/docker/rancher_home/auditlog:/var/log/auditlog/ --name rancher -p 80:80 -p 443:443 rancher/rancher:v2.4.3

启动成功以后就可以通过浏览器访问了,如果访问不成功的可以耐心等待一会,启动过程需要一点时间,如果长时间没有成功,注意看一下 docker log,有防火墙限制的记得把80和443端口开放出来。
我这里通过访问 https://172.100.10.13:443/ 进入rancher ,选择语言,设置密码,然后就OK了。
这时我们还没有K8S集群,需要来创建一个,看到rancher主菜单有一个“集群”,点进去,找到添加集群按钮


image.png

由于我们是自己的服务器搭建集群,所以这里选择自定义,进去以后输入集群名称,进入下一步,


image.png

这一步把Etcd和Control都选上,然后复制下面的命令,到172.100.10.13服务器上面执行,没有报错的话就静静等待集群启动起来。
启动完成以后我们查看新创建的集群,看到状态已经是绿色的Active了,这个时候可以给集群添加主机,点击集群升级按钮,打开页面后滚动到最下方,找到添加主机命令,复制。
image.png

登录到172.100.10.23服务器,执行上面的命令,完成以后节点会自动加入集群。
image.png

集群创建好了,我们还需要一个命令行客户端来管理它,这里用到了kubectl,我们来配置一下,首先拿到kubeconfig文件,在这个位置:


image.png

复制好内容,保存到172.100.10.13服务器的 /root/.kube/config文件中,然后测试一下看看是不是生效
kubectl get pods --all-namespaces 

或者利用docker操作kubectl

docker run --rm --name kubectl -v /root/.kube/config:/.kube/config bitnami/kubectl:latest get pods --all-namespaces
0x07: 部署Jenkins

docker启动jenkins脚本:

docker run -d -p 8081:8080 -p 50000:50000 \
-v /opt/docker/jenkins/home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
-v /usr/lib64/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7 \
-v /root/.aws:/root/.aws \
-v /root/.docker:/root/.docker \
-v /root/.kube/config:/root/.kube/config \
-u root --name jenkins --restart=always --privileged=true jenkins/jenkins

看到我们挂载了好多宿主机上的文件,这里简单介绍一下:

/opt/docker/jenkins/home # jenkins 工作的主要目录,用于保存jenkins状态数据;
/var/run/docker.sock && /usr/bin/docker && /usr/lib64/libltdl.so.7  # 用于在容器内部调用宿主机docker命令;
/root/.aws # aws-cli配置文件
/root/.docker # docker仓库授权配置文件存放于此目录下
/root/.kube/config # k8s集群配置文件

启动完成以后,访问 http://172.100.10.13:8081/ ,按照引导进入jenkins。
接下来开始安装jenkins插件,这里我们主要用到了这些插件:
Localization: Chinese 、Git、GitHub、ssh、Blue Ocean、docker、docker-build-step、Pipeline

0x08: 通过Jenkins部署golang应用

我们的代码都托管在github上,首先在github上面创建仓库,然后把编写好的代码提交上去,注意在项目根目录下写好你的Dockerfile和Jenkinsfile,用来制作docker镜像以及jenkins发布的pipeline步骤。我这里是用branchName做环境区分,test分支发布测试环境,master分支发布线上环境。
Jenkinsfile文件内容:

// 支持发布 test pro
def createVersion() {
    // 定义一个版本号作为当次构建的版本,输出结果 20200615175842
    return new Date().format('yyyyMMddHHmmss')
}
def currentVersion = createVersion()
pipeline {
  agent any
  parameters {
    string(
        name: 'appVersion',
        defaultValue: currentVersion,
        description: '应用版本号'
    )
  }
  stages {
    stage('prepare') {
      steps {
        echo "workspace: ${WORKSPACE}"
        echo "GIT_COMMIT: ${GIT_COMMIT}"
        echo "APP_VERSION: ${params.appVersion} ."
        echo "branch: ${BRANCH_NAME}"
      }
    }

    stage('repository authorization') {
      steps {
        script {
          echo "repository authorization stage ..."
          if(env.BRANCH_NAME == 'test'){
            sh "docker login -u admin -p Harbor12345 172.100.10.13:9090"
          }
          else if(env.BRANCH_NAME == 'master'){
            sh "docker run --rm -v /root/.aws:/root/.aws amazon/aws-cli ecr get-login-password >> ecr-login.txt"
            sh "cat ecr-login.txt | docker login --username ***** --password-stdin *****.dkr.ecr.*****.amazonaws.com"
          }
        }
      }
    }

    stage('build') {
      steps {
        echo "build stage ..."
        sh "docker build -t *****:${params.appVersion} ."
      }
    }

    stage('tag') {
      steps {
        script {
          echo "tag stage ..."
          if(env.BRANCH_NAME == 'test'){
            sh "docker tag *****:${params.appVersion} 172.100.10.13:9090/*****/*****:${params.appVersion}"
          }else if(env.BRANCH_NAME == 'master'){
            sh "docker tag *****:${params.appVersion} *****.dkr.ecr.*****.amazonaws.com/*****:${params.appVersion}"
          }
        }
      }
    }

    stage('push') {
      steps {
        script {
          echo "push stage ..."
          if(env.BRANCH_NAME == 'test'){
            sh "docker push 172.100.10.13:9090/*****/*****:${params.appVersion}"
          }else if(env.BRANCH_NAME == 'master'){
            sh "docker push *****.dkr.ecr.*****.amazonaws.com/*****:${params.appVersion}"
          }
        }
      }
    }

    stage('clean') {
      steps {
        script {
          echo "clean stage ..."
          if(env.BRANCH_NAME == 'test'){
            sh "docker rmi 172.100.10.13:9090/*****/*****:${params.appVersion}"
          }else if(env.BRANCH_NAME == 'master'){
            sh "docker rmi *****.dkr.ecr.*****.amazonaws.com/*****:${params.appVersion}"
          }
          sh "docker rmi *****:${params.appVersion}"
        }
      }
    }

    stage('k8s secret') {
      steps {
        script {
          echo "k8s secret stage ..."
          if(env.BRANCH_NAME == 'test' ){
            sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config bitnami/kubectl:latest delete --ignore-not-found=true secrets harbor-secret"
            sh "docker run --rm -u root --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v /root/.docker/config.json:/root/.docker/config.json bitnami/kubectl:latest create secret generic harbor-secret --from-file=.dockerconfigjson=/root/.docker/config.json --type=kubernetes.io/dockerconfigjson"
          }else if(env.BRANCH_NAME == 'master'){
            // 不完美,不能做到无缝升级,查阅了官方文档,没有 exist for update 机制
            sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config bitnami/kubectl:latest delete --ignore-not-found=true secrets aws-ecr-secret"
            sh "docker run --rm -u root --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v /root/.docker/config.json:/root/.docker/config.json bitnami/kubectl:latest create secret generic aws-ecr-secret --from-file=.dockerconfigjson=/root/.docker/config.json --type=kubernetes.io/dockerconfigjson"
          }
        }
        //sh "kubectl delete secrets aws-ecr-secret"
        //sh "kubectl create secret generic aws-ecr-secret --from-file=.dockerconfigjson=/root/.docker/config.json --type=kubernetes.io/dockerconfigjson"
      }
    }

    stage('deploy') {
      steps {
        script {
          echo "deploy stage ..."
          if(env.BRANCH_NAME == 'test'){
            sh '''
            cat > *****.yaml << EOF
# 声明一个Deployment资源对象
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-*****
spec:
  # 通过replicas声明pod个数是2
  replicas: 2
  # 通过标签选择被控制的pod
  selector:
    matchLabels:
      app: *****
  # 在template中定义pod
  template:
    metadata:
      labels:
        # 给pod打上标签app=*****
        app: *****
    spec:
      imagePullSecrets:
        - name: harbor-secret
      containers:
        # 声明容器名称,注意不是pod名称,pod名称应该定义在metadata中
        - name: *****
          image: 172.100.10.13:9090/*****/*****:${appVersion}
          args: ["test"]
          ports:
            - name: http
              containerPort: 8898
          resources:
            limits:
              cpu: 1000m
              memory: 2Gi
            requests:
              cpu: 500m
              memory: 1Gi
# 在一个yaml文件中通过---分割多个资源对象
---
apiVersion: v1
# 声明一个Service资源对象
kind: Service
metadata:
  name: service-*****
spec:
  ports:
    - name: http
      # Service监听端口
      port: 8898
      # 转发到后端Pod的端口号
      targetPort: 8898
      # 外部访问端口
      #nodePort: 30076
  # service-*****将选择标签包含app=*****的pod
  selector:
    app: *****
  type: NodePort
EOF
            '''
            sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v /opt/docker/jenkins/home/workspace/*****_test/*****.yaml:/.kube/*****.yaml bitnami/kubectl:latest apply -f ./*****.yaml"
          }else if(env.BRANCH_NAME == 'master'){
            sh '''
            cat > *****.yaml << EOF
# 声明一个Deployment资源对象
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-*****
spec:
  # 通过replicas声明pod个数是2
  replicas: 2
  # 通过标签选择被控制的pod
  selector:
    matchLabels:
      app: *****
  # 在template中定义pod
  template:
    metadata:
      labels:
        # 给pod打上标签app=*****
        app: *****
    spec:
      imagePullSecrets:
        - name: aws-ecr-secret
      containers:
        # 声明容器名称,注意不是pod名称,pod名称应该定义在metadata中
        - name: *****
          image: *****.dkr.ecr.*****.amazonaws.com/*****:${appVersion}
          args: ["pro"]
          ports:
            - name: http
              containerPort: 8898
          resources:
            limits:
              cpu: 1000m
              memory: 2Gi
            requests:
              cpu: 500m
              memory: 1Gi
# 在一个yaml文件中通过---分割多个资源对象
---
apiVersion: v1
# 声明一个Service资源对象
kind: Service
metadata:
  name: service-*****
spec:
  ports:
    - name: http
      # Service监听端口
      port: 8898
      # 转发到后端Pod的端口号
      targetPort: 8898
      # 外部访问端口
      #nodePort: 30076
  # service-*****将选择标签包含app=*****的pod
  selector:
    app: *****
  type: NodePort
EOF
            '''
            sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v /opt/docker/jenkins/home/workspace/*****_master/*****.yaml:/.kube/*****.yaml bitnami/kubectl:latest apply -f ./*****.yaml"
          }
          //sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v ${WORKSPACE}/*****.yaml:/.kube/*****.yaml bitnami/kubectl:latest apply -f ./*****.yaml"
          //sh "kubectl apply -f *****.yaml"
        }
      }
    }
  }
}

下面我们来部署应用,打开Blue Ocean,进去之后点击“创建流水线”,代码仓库选择GitHub,首次会提示你输入access_token,输入进去即可,下一步选择组织,接着选择项目仓库,最后点击创建流水线,完成以后就会按照项目中配置的Jenkinsfile开始执行发布流程,我们上面分拆了这几个步骤:预备、编译打包镜像、生成tag、推送镜像、部署镜像、清理镜像,当然这些不是固定的,可以按照你的需求自由增减。


image.png
0x09: 总结归纳

目前整体流程下来基本上没什么大问题,就是还有几个做法不是太完美:
1.部署jenkins依赖宿主机的docker,最好能在jenkins容器内直接操作docker【我看目前没有合适的镜像,可能需要自己单独打镜像】;
2.【已解决】kubectl也是同样的问题,我试了kubtctl镜像,在宿主环境下可以执行kubectl apply,但是到container环境中会报错,已经反馈issue给到作者,看看后面能不能解决;https://github.com/bitnami/bitnami-docker-kubectl/issues/16
3.Jenkinsfile 因为用到了参数化构建,第一次会构建错误,镜像拿不到版本号,但是第二次以后就都可以了,不知道是何原因

image.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341