本着步子迈得太大容易扯蛋的原则,平台设计初衷就是能调用开源产品肯定不自己做,这样平台只作为一个综合调度中心使用,无需考虑后面具体的功能实现逻辑。

使用Jenkins还是要追溯到很久很久之前认知的一家公司,当时的技术总监张晓峰让我学到了持续集成引擎Hudson,也就是后来的Jenkins。以前公司是Jenkins结合Maven,Ant做敏捷式开发,而我只是取巧,用了其中的一些最基本的功能来实现系统发布更新。

传统的运维发布

  • SVN迁出代码-本地打成tar包(rar包)- sftp上传到服务器

  • Jenkins建立发布项目-SVN迁出代码-通过SFTP插件直接分发到服务器

优点:粗暴简单


缺点

  • 效率: 发布方式一单一服务器上线没问题,多服务器分发效率下降。如果网内是统一入口登录(即堡垒机为单一入口时),发布工作将变得极为困难。

  • 安全风险:发布方式二的SSH账号密码必须存在Jenkins上,虽然不是明文,但.....同样也面临这服务器的22口要对Jenkins开放,安全是问题。

采用系统方案:YUM

Paste_Image.png我当时的思路:公司是等保三级的单位,在当初我制定内网规则的时候,强烈建议SSH登录范围必须限定,统一的入口可以极大的减少被***跳板式***的可能,所以我想到了是用YUM更新的方式:

  1. 发布:把代码从SVN上迁出后,打成RPM包(强烈推介FPM)

  2. 更新:通过YUM的特性,更新的程序包每次保持版本号+1,例如test-519-1.x86_64(519就是Jenkins的发布版本号),服务器每次只需要执行以下2条命令即可。

    bash yum clean all yum install test

  3. 批量操作:通过Saltstack去通知每台服务器去进行Yum的动作啦。。。

  4. 回退: 就更简单了,粗暴点在YUM服务器直接 mv test-518-1.x86_64 test-520-1.x86_64即可,斯文点当然还是回调Jenkins的接口,使用TAG回滚。

具体逻辑及实施

那么下面先来解决打RPM包,更新YUM源的问题(我的Jenkins就是我们内网的YUM源):

配置Jenkins

首先我们需要打开Jenkins中的batch tasks(批处理,其实就是脚本),不会用Jenkins自己百度吧。

  • 点击Add post-build action-选择Invoke batch tasks

  • Batch tasks里填入脚本

bash
mkdir -p /home/release/$JOB_NAME && \
fpm -s dir -t rpm -n $JOB_NAME -v $BUILD_NUMBER --prefix /home/www/bbs -C /var/lib/jenkins/workspace/$JOB_NAME -p /home/release/$JOB_NAME ./ && \
createrepo --update /home/release/$JOB_NAME/ && \
curl -d "job_id=$JOB_NAME" http://salt master IP/cmdb/salt_jenkins_post/


这段Jenkins脚本的大体意思:

  • 创建/home/release/$JOB_NAME目录

  • 然后把/var/lib/jenkins/workspace/$JOB_NAME(Jenkins项目工作区)的代码打成一个以$JOB_NAME命名,版本号为$JOB_NAME的RPM包,其存放在/home/release/$JOB_NAME这个目录里,其解压后会解压到/home/www/bbs目录。

  • 然后createrepo --update /home/release/$JOB_NAME/ 通知更新更新YUM源.

  • 最后就是回调我的Salt接口(此接口的作用其实就是根据这个项目反查对应的哪几台发布主机,然后在这些主机上执行yum install命令,是不是很无脑~)

Saltstack接口(saltjenkinspost)

由于我的平台和Salt master是同一台(主要是省事),省去了调用API,直接调用了本地Saltstack已经封装的一些yum install 之类的命令。

  • upgradeavailable 验证yum源是否更新

  • install 安装

  • modrepo 创建yum源

  • getrepo 验证yum源是否存在

  • intro 执行更新后的一些命令

我在接口处理的每一步后都会验证返回的主机是否跟数据库预设的项目主机一样,只有一样了才会进行下一步(比如接口只返回了一台服务器通过salt执行的结果,而数据库里该项目是两台服务器,我会认为这个发布有问题,而进行中断)这样也是为了避免有的主机更新成功了,有的主机没更新成功,导致线上用户体验不好(目前已经成功从深信服公司要到了负载均衡的API)后面要做的就是采用灰度发布,从负载上摘除一个然后就更新一个,更新完毕再加回负载。

import json
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

try:
    import salt.client
except:
    pass
from cmdb.models import *

class Salt_jenkins:
    def __init__(self, host_list, job):
        self.client = salt.client.LocalClient()
        self.host_list = host_list
        self.type = type
        self.job = job


    def upgradeavailable(self):
        """检测目标主机组项目在yum上是否有新版本更新,返回可以更新的主机"""
        ret = self.client.cmd('%s'% self.host_list, 'pkg.upgrade_available', ['%s'% self.job],expr_form='list',ret='return_redis')
        true_hostlist = []
        for host in ret.keys():
            if ret['%s' % host]:
                true_hostlist.append(host)
            else:
                pass
        return true_hostlist

    def install(self):
        """YUM安装项目RPM包"""
        ret = self.client.cmd('%s'% self.host_list, 'pkg.install', ['%s'% self.job],expr_form='list',ret='return_redis')
        true_hostlist = []
        for host in ret.keys():
            if ret['%s' % host] != {}:
                install_ret = ret['%s' % host]['%s' % self.job]
                if install_ret != '':
                    true_hostlist.append(host)
                else:
                    pass
            else:
                pass
        return true_hostlist

    def modrepo(self):
        """创建项目YUM源"""
        ret = self.client.cmd('%s'% self.host_list, 'pkg.mod_repo',['repo=%s'% self.job,'baseurl=http://172.18.11.98/release/%s'% self.job,'enabled=1','gpgcheck=0','name=%s'% self.job,'priority=10'],expr_form='list',ret='return_redis')
        true_hostlist = []
        for host in ret.keys():
            if type(ret['%s' % host]) == dict:
               true_hostlist.append(host)
            else:
                pass
        return  true_hostlist

    def getrepo(self):
        """验证项目YUM源是否存在"""
        ret = self.client.cmd('%s'% self.host_list, 'pkg.get_repo', ['repo=%s'% self.job],expr_form='list',ret='return_redis')
        true_hostlist = []
        for host in ret.keys():
            if ret['%s' % host] != {}:
               true_hostlist.append(host)
            else:
                pass
        return  true_hostlist

    def intro(self):
        """执行svn_intro内命令"""
        command = Svn.objects.get(svn_name = self.job).svn_intro
        ret = self.client.cmd('%s'% self.host_list, 'cmd.run', ['%s'% command],expr_form='list',ret='return_redis')
        return ret


@csrf_exempt    
def salt_jenkins_post(request):
    if request.method == 'POST':     
        ip = request.META.get("REMOTE_ADDR", None)
        if ip == '172.18.11.98':   
            job =  request.POST.get('job_id')
            job_hosts =  Svn.objects.get(svn_name=job).svn_hosts
            if job =='cms_template.youth.cn' or job =='cms_assets.youth.cn':
                pass
            else:
                """初始化Salt_jenkins"""
                salt_jenkins = Salt_jenkins(job_hosts, job)

                """目标主机检查YUM源是否存在"""
                if sorted(salt_jenkins.getrepo()) == sorted(str(job_hosts).split(',')):
                    pass
                else:
                    """不存在就创建YUM源"""
                    salt_jenkins.modrepo()
                """目标主机检查YUM源是否更新"""
                if sorted(salt_jenkins.upgradeavailable()) == sorted(str(job_hosts).split(',')):
                    if sorted(salt_jenkins.install()) == sorted(str(job_hosts).split(',')):
                        """目标主机执行命令"""
                        salt_jenkins.install()
                        """目标主机执行命令"""
                        salt_jenkins.intro()
                        return HttpResponse('install success')
                    else:
                        return HttpResponse('install fail')
                else:
                    return HttpResponse('upgradeavailable fail')
        else:
            return HttpResponse('ip deny')
    else:
        return HttpResponse('get deny')

后续会介绍发布的状态返回,也就是Saltstack的MasterEvent及通过Jenkins结合Saltstack创新发布项目。