基于docker打造实现自动化集成和无状态持续交付流水线

项目背景

此项目是我在我第一家公司,一家做p2p的互金公司做的项目。当时我主要负责公司所有项目在预发布环境和生产环境部署。公司早期的技术骨干多来自BAT,所以有着很鲜明互联网公司的基因,采用的也是敏捷开发模式。所以是靠着持续迭代的方式,来不断优化改进产品的。并且是用dubbo这样的SOA架构,对后台应用做了比较细致地拆分,因此有大量独立部署的应用服务。这样一来,作为负责发布部署的运维人员,就需要承担高负荷的发布部署工作。

三大痛点

我先说说在当时的技术条件下,发布部署工作的几个痛点:

1.环境层次较多

我们当时的流程里包括多套环境:开发环境、测试环境、预发布生产环境,一个版本的代码从开发开始,到生产为止,每个环境都需要做部署,以进行开发、测试验收和投入最终的生产。

2.应用配置和应用代码耦合

应用代码是采用 Tomcat运行的,配置以配置文件的方式存放在本地读取,在不同的环境中,诸如有关数据库访问地址、中间件地址等配置项就完全不一样,需要部署人员对其一一手工修改以适应相应的环境,如果有遗漏和错误,哪怕是多一个空格这种肉眼难以察觉的错误,都可能引发致命的问题。

3.Docker镜像创建时间长,且需要重复创建

虽然在我接手此块工作之前,已经在除开发环境以外的其他三套环境,引入了Docker容器。但是由于前面说的第二个痛点,所以要将Tomcat应用以Docker容器的方式运行,必须在创建镜像时将代码和与环境相适应的配置文件同时ADD。这样一来,镜像就做不到通用,即使是统一版本的代码,也必须在多套环境创建多个镜像,传说中的Docker的核心价值“一次编排,随处可用”在当时完全没有体现。每次创建镜像都要对配置文件进行修改,等待将镜像上传个集团科技支持公司维护的镜像仓库(通过公网走VPN),十分费事又费时。

综上,要提升效率,提升运维人员的价值感,就必须追求做到发布流程的自动化,将各个环节都打通。整个流程的痛点和难点,都集中在配置文件和代码耦合这个关键问题上。我也做过很多尝试,包括用python脚本拉取数据库存储的方式自动化修改配置文件。经过大量踩坑和技术调研,我最终采用下面这套方案,找到了Docker正确的打开方式,实现了自动化集成和无状态持续交付流水线。

解决方案:

方案主要流程:

自动化集成和无状态交付流水线流程图

1.在不同的环境搭建etcd作为应用配置中心,存储常用的应用配置项的键值对。

2.由测试工程师通过web控制台,编写应用配置文件的模版文件,调用docker-py将模版文件随maven打包的应用代码和confd进行编排,在测试环境创建新的docker镜像;

3.通过docker-py用新建的镜像创建容器运行,容器内部启动脚本自动调用confd生成配置文件,上报war包MD5值,测试对应用进行测试; 4.测试环境测试通过,通过web控制台测试将镜像从内网测试环境push至阿里云(预发布和生产环境)的私有镜像仓库Harbor,用tag更换标签,测试工程师向运维申请预发布测试。

5.运维工程师同意预发布测试,通过web控制台,在预发布环境从Harbor拉取镜像,创建容器并启动,通过web控制台查看日志和MD5值无异常后,交由测试工程师继续在预发布环境测试验证。

6.预发布环境测试通过,测试工程师提出生产发布申请,运维工程师参照第5步流程进行生产发布和验收。

7.自动录入数据库一次完整的发布操作过程,以便异常时回退和管理分析;

项目亮点:

1.大幅简化了发布工作流程,发布时间大幅缩短,一次配置修改,一次编排,一次镜像上传,随处可用;

2.全自动化流程,减少了人工干预,节约人力资源也减少人为错误;

3.实现了代码和配置的解耦,用docker容器化部署和配置中心做到了与环境无关,从内网到云环境无缝衔接;

4.人性化操作,包括docker镜像创建,容器部署,war包校验,日志查看等操作,全流程都可以用web控制台操作,非常简单人性化,负责业务测试的同事不需掌握docker等底层技术,也可以非常容易地上手操作。

技术栈

代码管理:git/gitlab

打包编译工具:maven

容器:docker

镜像仓库:harbor

应用配置中心:redis

Docker管理API:docker-py

配置文件模版渲染程序:confd

管理控制台开发框架:django

管理控制台数据库:mysql

管理控制台前端功能实现:

html+ajax+javascript+jquery

编码设计与实现

这里只展示部分关键代码,完整代码待整理脱敏后上传github开源

Docker容器及镜像管理模块
# -*- coding: utf-8 -*-
import docker
import config
import DBquery as dbq
import DBwrite as dbw
import os
import logging
logger = logging.getLogger("crosscloud") # 为loggers中定义的名称

#初始化DockerAPI客户端
def initClient(hostname):
    try:
        hostip = dbq.getHostip(hostname)
        cli = docker.DockerClient(base_url='tcp://%s:2375'%hostip)
        return  cli
    except Exception,e:
        logger.error(e)
        raise

#获取容器运行状态
def getContainerStatus(hostname,servername):
    try:
        cli = initClient(hostname)
        status = cli.containers.get(servername).status
        return status
    except Exception,e:
        logger.error(e)
        raise

#生成指定应用最新的镜像版本号
def getNewImageversion(servername):
    lastversion = dbq.getLastImageVersion(servername)
    fst = int(lastversion.split('.')[0])
    secd = int(lastversion.split('.')[1])
    thrd = int(lastversion.split('.')[2])
    thrd = thrd + 1
    if thrd >= 10:
        secd = secd + 1
        thrd = thrd % 10
    if secd >= 10:
        fst = fst + 1
        secd = secd % 10
    newversion = "%d.%d.%d" % (fst, secd, thrd)
    return newversion

#获取指定主机的镜像列表
def getImageList(hostname):
    try:
        cli = initClient(hostname)
        imagelist = []
        for image in cli.images.list():
            if image.tags != []:
                imagelist.append(image.tags[0])
        logger.info(hostname+"查询镜像结果:"+str(imagelist))
        return imagelist
    except Exception:
        raise

#获取指定主机上指定应用容器的镜像版本号
def getContainerVersion(hostname,servername):
    try:
        cli = initClient(hostname)
        c = cli.containers.get(servername)
        containerVersion = c.image.attrs['RepoTags'][-1].split(':')[-1]
        return containerVersion
    except Exception,e:
        logger.error(e)
        exit(1)

#登陆harbor镜像仓库
def registryLogin(cli):
    try:
        cli.login(
            username=config.REGISTRY_USERNAME,
            password=config.REGISTRY_PASSWD,
            registry=config.REGISTRY,
        )
    except Exception,e:
        print e
        logger.error("登录registry失败!"+str(e))
        exit(1)
    logger.info("登录registry成功!")


#拉取指定镜像在指定机器上实例化一个容器,并将其启动
def start_container(hostname,servername,version):
    cli = initClient(hostname)
    #登录私有仓库
    registryLogin(cli)
    image ='%s/qguanzi/%s:%s'%(config.REGISTRY,servername,version)
    #拉取指定镜像
    try:
        cli.images.pull(image)
    except Exception,e:
        logger.error(e)
        return False,str(e)
    try:
        c = cli.containers.get(servername)
        c.stop()
        c.remove()
    except Exception,e:
        logger.error("%s容器在%s不存在:\n%s"%(servername,hostname,e))
        return False,str(e)
    finally:
        logger.info("%s容器在%s开始启动..."%(servername,hostname))
        try:
            cli.containers.run(
                image=image,
                name=servername,
                volumes={
                    '/data/docker/logs/%s' % servername:
                        {
                            'bind': '/data/logs',
                            'mode': 'rw'
                        }
                },
                mem_limit='1g',
                network_mode='host',
                detach=True  # True表示运行容器后,就结束run方法
            )
        except Exception,e:
            logger.info("%s容器在%s启动失败:\n%s" % (servername, hostname,e))
            return False,str(e)
        logger.info("基于镜像%s创建的%s容器在%s启动完成!"%(image,servername,hostname))
        return True,"基于镜像%s创建的%s容器在%s启动完成!"%(image,servername,hostname)

#容器运行状态切换开关
def StatusSwitch(hostname,servername):
    try:
        container_status = str(getContainerStatus(hostname,servername))
        c  = initClient(hostname).containers.get(servername)
    except Exception,e:
        logger.error(e)
        return str(e)
    if container_status == 'running':
        c.stop()
        logger.info(hostname+"上的"+ servername +"容器已经停止!")
        return (hostname+"上的"+ servername +"容器已经停止!")
    else:
        c.start()
        logger.info(hostname+"上的"+ servername +"容器已经启动!")
        return (hostname+"上的"+ servername +"容器已经启动!")

#删除容器
def deleteContainer(hostname,servername):
    try:
        container_status = str(getContainerStatus(hostname, servername))
        c = initClient(hostname).containers.get(servername)
    except Exception,e:
        logger.error(e)
        return str(e)
    if container_status == 'running':
        c.stop()
        c.remove()
        logger.info(hostname + "上的" + servername + "容器已经删除!")
        return (hostname + "上的" + servername + "容器已经删除!")
    else:
        c.remove()
        logger.info(hostname + "上的" + servername + "容器已经删除!")
        return (hostname + "上的" + servername + "容器已经删除!")

#创建镜像
def createImage(hostname,servername,instruction,branch):
    try:
        cli = initClient(hostname)
        path = '/data/configcenter/%s/%s'%(hostname,servername)
        version = getNewImageversion(servername)
        image = '%s/qguanzi/%s:%s' % (config.REGISTRY, servername, version)
        repo_path = '%s/package/'%path
        repo_url = '%s/%s.git'%(config.REPO_URL,servername)
        buildWar(servername, repo_path, branch, repo_url)
        cli.images.build(path=path, tag=image)
    except Exception,e:
        logger.error("因为"+instruction+","+hostname + "上的" + image +"镜像创建失败:"+str(e))
        return str("因为"+instruction+","+hostname + "上的" + image +"镜像创建失败:"+str(e))
    dbw.SetNewImageversion(servername,version,instruction)
    logger.info("因为"+instruction+","+hostname + "上的" + image +"镜像已经创建!")
    return ("因为"+instruction+","+hostname + "上的" + image +"镜像已经创建!")

#拉取代码,编译生成war包
def buildWar(servername,repo_path, branch, repo_url):
    try:
        logger.info('开始从' + repo_url + "拉取" + branch + "代码分支")
        if not os.path.isdir(repo_path):
            os.makedirs(repo_path)
            print repo_path
        re = os.system('cd %s;git clone -b %s %s' % (repo_path, branch, repo_url))
        if re != 0:
            os.system('cd %s;git checkout %s;git pull %s' % (repo_path, branch, repo_url))
        logger.info("拉取新代码完成,开始maven打包.....")
        os.system(
            'cd /data/configcenter/%s/package/%s && /usr/local/maven/bin/mvn clean package -U -Dmaven.test.skip=true' % (
            servername, servername)
        )
    except Exception:
        raise

#上传镜像到harbor镜像仓库
def pushImage(hostname,image):
    imagefullname = image
    try:
        cli = initClient(hostname)
        version = image.split(':')[-1]
        image = image.split(':')[0]
        print image
        registryLogin(cli)
        cli.images.push(imagefullname, tag=version)
    except Exception,e:
        logger.error(imagefullname+"上传失败:"+str(e))
        return str(e)
    return imagefullname+"上传成功!"

#删除指定机器上的指定镜像
def delImage(hostname,image):
    try:
        cli = initClient(hostname)
        cli.images.remove(image)
    except Exception,e:
        logger.error(str(e))
        raise

#新增部署节点,在数据库插入相应记录
def addNode(env,hostname,servername):
    try:
        dbw.addNode(env, hostname, servername)
    except Exception,e:
        logger.error("增加节点失败:"+str(e))
        return str(e)
    return "增加节点成功!"

#删除部署节点,在数据库删除相应记录
def delNode(env,hostname,servername):
    try:
        dbw.delNode(env, hostname, servername)
    except Exception,e:
        logger.error("删除节点失败:"+str(e))
        return str(e)
    return "删除节点成功!"

if __name__ == '__main__':
    # cli = initClient('132')
    # registryLogin(cli)
    hostname = '132'
    image = 'docker.example.com/example/webapi:0.1.1'
    print pushImage(hostname,image)
    # version = '0.1.1'
    # for info in cli.images.push('docker.example.com/example/webapi', tag='0.1.1',stream=True):
    #     print info
redis配置中心管理模块
# -*- coding: utf-8 -*-
import redis
import DBquery as dbq
import config
import logging
logger = logging.getLogger("crosscloud")

#初始化链接
def initConnection(env):
    host,port = dbq.getConfigCenterUrl(env)
    redisConn = redis.StrictRedis(host=host, port=port, db=0)
    return redisConn

#获取配置列表的键值对
def getConfiglist(env):
    redisConn = initConnection(env)
    configlist = redisConn.keys('%s*'%config.APPCONFIGKEY)
    confKV = {}
    for conf in configlist:
        confKV[conf] = redisConn.get(conf)
    return confKV

#新增/修改指定环境的指定配置项
def setConfigKV(env,config,value):
    try:
        redisConn = initConnection(env)
        redisConn.set(config,value)
    except Exception,e:
        logger.error(str(e))
        return str(e)
    logger.info("配置项:"+config+"的值已经成功设置为:"+value)
    return "配置项:"+config+"的值已经成功设置为:"+value

#删除指定环境的指定配置项
def delConfigKV(env,config):
    try:
        redisConn = initConnection(env)
        redisConn.delete(config)
    except Exception,e:
        logger.error(str(e))
        return str(e)
    logger.info("配置项:"+config+"已经成功删除")
    return "配置项:"+config+"已经成功删除"

#测试用例
if __name__ == '__main__':
    # print setConfigKV('132','king','cao')
    print delConfigKV('132','king')
配置模版渲染脚本

这里提供了redis和etcd做配置中心的两种渲染方式,因python的etcd库不太好用,和生产业务系统中已经部署了redis的原因,我最终采用redis做配置中心,来存储应用的键值对。只需将下列命令放在docker容器内的tomcat启动脚本里,就能使容器在启动时拉取最新的配置,和对配置模版的渲染生成最终的配置文件。关于里面的配置中心的configCenter地址,这里只需在docker容器宿主机上的host文件,或者容器里的host文件,或者容器使用的DNS里进行解析,根据具体网络环境设定即可。

#从配置中心拉取配置命令
#./confd  -onetime -backend etcd -node "http://configCenter1:2379" "http://configCenter:2379" "http://configCenter3:2379"
#./confd  -onetime -backend redis -node configCenter:6379
部分页面(不会写样式,求别吐槽)

整体页面
整体页面

配置中心管理页
配置中心

应用配置编辑页
应用配置编辑

应用配置模版编辑页
应用配置模版编辑

端口号设定对话框
端口号设定

后续优化设想

这个项目是在忙里偷闲,在白天做好日常工作的同时,利用周末和晚上加班时间完成的。因而项目细节难免粗陋,后续经过思考主要想在以下几块做改进:

1.前端交互上,由于本人前端知识不扎实,所以前端只是勉强能用而已。后期打算采用bootstrap和vue.js来改进前端页面和交互体验;

2.完善日志和异常捕获机制;

3.将流程前期的测试人员通过控制台触发git代码拉取和Maven编译打包,以及镜像编排的过程由脚本驱动。改为jenkeins的hook+脚本的方式进行;

4.后期的集群管理,考虑调研swarm和k8s,与当前docker-py的方式进行对比,做一定程度融合,实现最完善的集群管理;

5.新增对nginx反向代理和负载均衡配置的管理,以适应docker容器实例动态变化的需要;

6.完善对容器状态的监控,目前只监控里启动/停止状态,后期还可以监控容器内存/cpu/磁盘/网络等硬件资源使用情况,以及业务日志的异常情况捕获,引入时间序列的数据库存储监控数据,结合前端的highchart库做实时的监控看板;

7.结合第6点的监控状态情况,调用AliYun的API,和第5点实现的容器集群管理机制,达到自动弹性扩容/缩容的目标;

8.完善权限管理,做到一套平台可以给不同角色的工程师管理不同环境。

参考文档

etcd

https://github.com/coreos/etcd/blob/master/Documentation/op-guide/clustering.md

confd

https://github.com/kelseyhightower/confd/blob/master/docs/quick-start-guide.md

docker-py

https://docker-py.readthedocs.io/en/stable/client.html
https://www.ipcpu.com/2015/03/docker-py-usage/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
云效是阿里巴巴开发的一款持续集成持续交付平台,可以帮助开发团队实现自动化的软件交付流程。而Docker是一种容器化技术,可以将应用程序及其依赖打包成一个独立的容器,实现跨平台、快速部署的特性。下面是使用云效流水线进行Docker部署Spring Boot应用的步骤: 1. 创建云效项目:在云效平台上创建一个项目,并将代码仓库与项目关联。 2. 配置构建环境:在云效平台上配置构建环境,选择合适的构建镜像,例如选择一个包含Java和Maven的镜像。 3. 编写Dockerfile:在项目根目录下创建一个名为Dockerfile的文件,用于定义Docker镜像的构建规则。在Dockerfile中指定基础镜像、复制应用程序代码、安装依赖、设置环境变量等。 4. 编写云效流水线配置文件:在项目根目录下创建一个名为pipeline.yml的文件,用于定义云效流水线的配置。在配置文件中指定构建步骤、构建触发条件、构建参数等。 5. 配置云效流水线:在云效平台上配置流水线,将构建步骤与代码仓库、构建环境、Dockerfile等关联起来。可以设置自动触发构建、定时触发构建或手动触发构建。 6. 执行流水线:提交代码到代码仓库后,云效会自动触发流水线的执行。流水线会按照配置文件中定义的步骤进行构建、测试、打包和部署。 7. 部署Docker镜像:在流水线的最后一步,可以使用云效提供的部署功能,将构建好的Docker镜像部署到目标环境中,例如云服务器、容器服务等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值