简单Docker集群搭建

记录一个简单的docker搭建,适用于小项目

Posted by Gjx on 2018-04-12

引言

背景介绍

这篇文章最早大约是在2019年中旬写的,当时是为了给某项目组搭建一套规范化的发布流程,同时方便运维工作。如今已经2024年,又参与了一个项目,场景与当时相似,所以完善这篇文章,供参考。

作者的公司最早打包发布全靠Svn同步和拷贝依赖,在Eclipse中手动配置依赖等信息,这样其实很容易造成Jar包不一致和依赖配置难登问题,时间久了,因为没有做依赖的版本管理,当时的构建环境不容易还原,新同事入职配置开发环境全靠口口相传。

所以在16年以后Jar包依赖全部使用maven管理,代码使用git管理,代码发布通过jenkins流水线构建来迭代Jar包版本,作者也是在这个时候接触了相关的东西,刚开始是自己搭建Jenkins,构建项目工程,然后发布到docker,后来的项目中因为服务比较多,采用了mesos+marathon来做容器编排,再就到了这篇文章初做的2019年,因为当时项目比较小,所以考虑到不适用于mesos+marathon这种复杂的架构,就搭建了这套比较简单的集群环境。

目的阐述

本文的场景是搭建一个适用于小团队(3-4人左右)的Devops环境。

安装Docker

安装docker的方法有很多,我习惯到官网查看官方文档安装,介绍的比较全一些

官网安装

官网:https://www.docker.com/
image.png
image.png

image.png

在该选项中有几种不同的安装方法,可根据自己需要进行安装,我这里常用的安装方式是下面这个:

$ curl -fsSL https://get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh

Ubuntu

  1. 更新软件包索引:
    sudo apt update
  2. 安装Docker依赖项:
    sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
  3. 添加Docker GPG密钥并设置Docker的APT仓库:
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
  4. 安装Docker CE:
    sudo apt install docker-ce docker-ce-cli containerd.io

CentOS

  1. 安装必要的软件包管理工具(EPEL):
    sudo yum install -y epel-release
  2. 安装Docker CE:
    sudo yum install -y docker-ce docker-ce-cli containerd.io

docker配置

docker私有仓库配置

docker默认使用https下载镜像,我们搭建的私库需要添加到下面配置中才能正常推送和拉取镜像

添加/etc/docker/daemon.json文件

{
        "insecure-registries": ["s01.ycrh.jsyt:5000","10.83.3.120:5000"]
}

配置docker代理

因为部分网络环境下是无法访问外网下载镜像的,所以需要配置代理进行镜像的下载

vi /lib/systemd/system/docker.service

在 Service 部分下 增加 Environment 变量,配置成你自己的代理地址,如下

[Service]
Environment="HTTP_PROXY=http://[proxy-addr]:[proxy-port]/" "HTTPS_PROXY=https://[proxy-addr]:[proxy-port]/"

启动并启用Docker服务

sudo systemctl start docker
sudo systemctl enable docker

重启docker服务

service docker restart

启动docker registry

这里采用最简单的方式搭建docker仓库,不过推荐采用nexus或harbor,他们有可视化管理界面,我觉得比较方便的是可以自定义镜像的清理策略,比如可以设置保留最近10个版本的镜像

如果你想最简单的搭建这套环境,nexus应该是个不错的选择,因为nexus可以同时作为maven、npm、docker仓库,搭配gitlab的流水线,甚至不要jenkins

新建目录:mkdir /home/registry

docker run -d -p 5000:5000 -v /home/registry:/var/lib/registry -u 0 --restart=always --name registry registry

安装Docker Swarm

docker swarm是docker自带的集群管理组件,不过要确保你的docker版本至少为17.06,创建Docker Swarm集群涉及初始化一个管理节点(Manager node),随后将工作节点(Worker nodes)加入到集群中。

初始化Swarm

sudo docker swarm init --advertise-addr <MANAGER_IP>

例如:

[root@manager-node ~]# docker swarm init --advertise-addr 10.83.3.120
Swarm initialized: current node (39efft8yudtdpwmnenxewlt4r) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-5mci820jcs7dard6jrp19gjduj562kimptqmbfgd6omurkxa36-92jgt38tfowrsmo1wdzojtnde \
    10.83.3.120:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

[root@manager-node ~]

加入Worker节点

使用Manager节点提供的命令进行加入。

docker swarm join \
    --token SWMTKN-1-5mci820jcs7dard6jrp19gjduj562kimptqmbfgd6omurkxa36-92jgt38tfowrsmo1wdzojtnde \
    10.83.3.120:2377

即可加入到该集群,此处需要注意防火墙的配置。

查看当前集群节点信息

[root@swarm-master ~]# docker node ls
ID                            HOSTNAME      STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
kpcxiz0pnby0if3co2jx9kr30                     Down      Active                          
vojwoufr5bdfl5edat8m1c3qc *   swarm-master    Ready     Active         Leader           24.0.7
kjoitys5lmf3ka3lthjthjcrf     swarm-node1     Ready     Active                          24.0.7
kj4og3ldcdeyvhymk8ptrg72r     swarm-node2     Ready     Active                          24.0.7
o1i1x3xlra5stpgh9n39gqmxs     swarm-node3     Ready     Active         Reachable        24.0.7
c9s4bkzykq390prblj4708wyl     swarm-node4     Ready     Active                          24.0.7
u2gjy4xfiex0r7e9ngqr2a8oa     swarm-node5     Ready     Active                          24.0.7

到了这里,其实就可以使用docker swarm发布docker服务了,前提是你习惯使用命令去管理docker集群。

下面列举用命令行创建docker服务

创建网络

docker network create --driver overlay test

创建服务

docker service create --replicas 3 --network test  -p xx:xx --name 服务名 镜像名

新增副本,根据你服务器的压力进行伸缩

docker service scale 服务名称=3

其余命令根据下面提示使用:

docker swarm默认采用的是 vip 负载均衡模式,vip模式,就是docker swarm为每一个启动的service分配一个vip,并在DNS中将service name解析为该vip,发往该vip的请求将被自动分发到service下面的诸多active task上(down掉的task将被自动从vip均衡列表中删除)。

下面推荐一个docker图形管理工具:portainer,效果还是很好的。

安装Portainer

Portainer是Docker的图形化管理工具,提供状态显示面板、应用模板快速部署、容器镜像网络数据卷的基本操作(包括上传下载镜像,创建容器等操作)、事件日志显示、容器控制台操作、Swarm集群和服务等集中管理和操作、登录用户管理和控制等功能。功能十分全面,基本能满足中小型单位对容器管理的全部需求。

创建Portainer数据卷

sudo docker volume create portainer_data

启动Portainer容器

sudo docker run -d -p 9000:9000 --name=portainer --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce

访问Portainer

在Web浏览器中访问 http://<YOUR_SERVER_IP>:9000 来开始使用Portainer。

添加docker集群

直接将docker swarm的管理节点添加上即可。

image.png

连接失败的时候,可以检查下是否远程docker 没有开启 2375端口。
以下是 docker 端口配置方法:

# 1. 编辑docker.service
vim /usr/lib/systemd/system/docker.service

# 找到 ExecStart字段修改如下
ExecStart=/usr/bin/dockerd-current -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock 

# 2. 重启docker重新读取配置文件,重新启动docker服务
systemctl daemon-reload
systemctl restart docker

# 3. 开放防火墙端口
firewall-cmd --zone=public --add-port=6379/tcp --permanent

# 4.刷新防火墙
firewall-cmd --reload

# 5.再次配置连接远程docker就可以了

添加成功后,就可以看到docker swarm集群的情况了,如下图

可以看到集群中一共有7个节点,3个stacks,21个服务
image.png

发布应用

应用发布可以使用下面三种方式:

  • 直接根据docker镜像启动一个container

  • 发布一个service,好处是支持横向扩展和收缩,比如发布多个服务实例

  • 发布一个stack,当你的服务比较多的时候,定义一个docker-compose配置文件,可以很方便的发布

启动一个container

image.png
image.png

发布一个service

这里的配置和上面的container类似,只是多了服scale等参数可以实现发布多个实例来负载

image.png
image.png

发布一个stack

发布stack有4种方式,我使用比较多的是web editor和repository

其实刚开始使用的是web editor方式发布,但是后来发现还需要单独去维护docker-compose文件,而且从jenkins的流水线也不好直接去修改web editor中的配置,如果直接修改docker swarm的配置,portainer中的配置其实是不会更新的,所以最后选择了repository方式,这种方式完美的解决了我的诉求:

一、我的配置通过gitlab来维护,也不怕丢失了,并且有版本管理;

二、在jenkins中可以通过发起portainer的webhook请求来触发portainer自动拉取最新的gitlab上的配置,然后更新服务;

image.png

web editor方式
image.png

repository方式,通过配置一个git仓库地址来发布
配置完后,界面上会提供一个webhook地址来触发更新
image.png

配合jenkins来实现自动发布

下面的jenkins流水线配置是一个标准的springboot打包和发布流程,你可以通过少许改动来直接复用,主要过程就是

  1. maven打包

  2. docker镜像打包

  3. docker镜像推送

  4. 调用portainer webhook请求,触发stack更新

pipeline {
    agent any
    tools {
        maven 'Maven3' // 替换为你在全局配置中配置的Maven名称
    }
    environment {
        // 全局变量,用于存储时间戳
        TIMESTAMP = getCurrentTimestamp()
        MAVEN_HUB_URL = '192.168.100.10:6000'
        APP_NAME = 'demo-mf-main'
        ARCHITECTURE = 'amd64'
        DOCKER_COMPOSE_FILE = 'app-dev.yml'
        STACK_NAME = 'app-dev'
    }
    stages {
        stage('拉取主体功能代码') {
            steps {
                script {
                    git credentialsId: 'git-lab',
                        url: 'http://192.168.100.9:9980/rdcenter/demo-mf-main.git',
                        branch: 'develop'
                }
            }
        }
        stage('Maven构建') {
            steps {
                // 使用Maven构建项目
                script {
                    sh ' mvn clean package'
                }
            }
        }
        
        stage('构建Docker镜像') {
            steps {
                script {
                    // 使用Docker Pipeline插件构建镜像,并使用年月日时分秒作为tag
                    def customImage = docker.build("${env.MAVEN_HUB_URL}/${env.APP_NAME}:${env.TIMESTAMP}-${env.ARCHITECTURE}", '-f Dockerfile .')
                    // 推送镜像到私有仓库
                    docker.withRegistry("http://${env.MAVEN_HUB_URL}", 'Maven_Hub') {
                        customImage.push()
                        // 添加 latest 标签并推送
                        customImage.push()
                    }
                   
                    // 删除本地镜像
                    //customImage.remove()
                    sh "docker rmi ${env.MAVEN_HUB_URL}/${env.APP_NAME}:${env.TIMESTAMP}-${env.ARCHITECTURE}"
                }
            }
        }
                stage('发布应用') {
            steps {
                  script {
                      dir('devops') {
                        git credentialsId: 'gaojunxin-git',
                            url: 'http://192.168.100.9:9980/rdcenter/devops.git',
                            branch: 'main'
                        
                         // 使用双引号包围sed命令,并使用单引号包裹正则表达式内的特殊字符,同时正确插入环境变量
                        def imageName = "${env.MAVEN_HUB_URL}\\/${env.APP_NAME}:${env.TIMESTAMP}-${env.ARCHITECTURE}"
                        sh """
                          sed -i " \"s/image.*${env.APP_NAME}:[^\\":]*\\\$/'image: ${imageName}'/\" " ${env.DOCKER_COMPOSE_FILE}
                        """
                        sh "cat ${env.DOCKER_COMPOSE_FILE}"
                        
                        
                        // 先不做docker swarm发布,直接通过portainer触发
                        // sh "docker stack deploy --compose-file '${env.DOCKER_COMPOSE_FILE}' '${env.STACK_NAME}'"
                    
                        // 添加、提交并推送更改到Git仓库
                        sh """
                          git config --global user.email "gjx.xin@qq.com"
                          git config --global user.name "gaojunxin"
                          git add .
                          git commit -m "更新${APP_NAME}镜像版本到${env.TIMESTAMP}-${ARCHITECTURE}"
                        """
                        
                        withCredentials([usernamePassword(credentialsId: 'gaojunxin-git', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) {
                           sh 'git push http://${GIT_USERNAME}:${GIT_PASSWORD}@192.168.100.9:9980/rdcenter/devops.git HEAD:refs/heads/main'
                        }
                        
                        sh '''
                        curl -X POST \
                             -H "Content-Type: application/json" \
                             -d '{}' \
                             'http://192.168.100.11:9000/api/stacks/webhooks/adf41602-71cb-4b36-8fd7-c71f669ad1ab'
                        '''
                      }
                }
                   
            }
        }
    }
}
//获取时间
def getCurrentTimestamp() {
    def calendar = Calendar.getInstance()
    def timestamp = calendar.format("yyyyMMdd-HHmmss")
    return timestamp
}