DEVOPS笔记(一):基于Python(Django)+jenkins+saltstack实现一套成熟的CI/CD方案

文章目录

      • 前文
      • 关于CI/CD
      • jenkins
          • jenkins说明
          • jenkins+Python
      • saltstack
          • saltstack介绍
          • salt-api安装
          • salt-api与Python交互
      • 总结

前文

  公司马上要上k8s+docker的形式来实现CI/CD(持续集成和持续部署),在这里汇整总结一套基于Python+jenkins+saltstack来实现传统虚拟机上的CI/CD。集成是依赖于jenkins的搭建,关于jenkins的搭建和使用这篇文章就不叙述了,主要阐述如何通过Python将jenkins集成到自建的集成发布平台;发布是依赖于saltstack来管理多台服务器,saltstack本身是Python撰写的,也提供了一套成熟的API来交互,所以采用saltstack来管控发布平台也是非常轻松的。当然你也可以用ansible等其他多服务器管理方案。

关于CI/CD

  随着互联网的蓬勃发展,如何快速的迭代APP功能显得尤其重要,于是出了敏捷开发的概念,它的核心理念是,既然我们无法充分了解用户的真实需求是怎样的,那么不如将一个大的目标不断拆解,把它变成一个个可交付的小目标,然后通过不断迭代,以小步快跑的方式持续开发。随后就是DEVOPS的时代,即DEV(development,开发人员)+OPS(operations,运维人员)相结合的模式,来加速CI(continuous integration,持续集成)+CD(continuous deploy,持续部署),因为敏捷开发仅仅是提高了开发和测试的速度,但是在专注于系统和安全的运维身上,却又没办法更快地加快部署,所以产生了DEVOPS这个概念,来让开发、测试、运维打造成一体,由运维负责审核,开发、测试负责上线新功能这样的迭代方式。而这就依赖于需要建设一个可以提供Ci\CD和供运维人员审核的平台。
  当然DEVOPS更多的是阐述一种文化和理念,CI/CD是其中重要的一环,但并不是全部。本文即讲述了基于jenkins的持续集成,基于saltstack的持续部署,而后将这二者集成于一个平台上实现devops平台。

jenkins

jenkins说明

  作为当下最流行的构建平台之一,jenkins的地位是毋庸置疑的。Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,利用jenkins可以很轻松地从github、gitlab上拉代码,在进行打包构建成对应的格式,比如vue这类的可以在代码拉取后直接执行npm install进行编译,最后再将生成的dist包压缩发往服务器;比如Python这类的拉取代码后直接压缩打包成tar.gz发往服务器;比如java这类的拉取代码后通过maven编译构建后生成jar包后发往服务器。由此可见, 各类的编程语言都能通过jenkins来编译构建成需要的包类型,然后发往服务器。
  除此之外,可以通过jenkins轮询gitlab这类代码仓库,当用户打tag、或者push后就自动按照程序构建,不过本篇的实现,则并没有通过轮询来实现,是需要用户手动打了tag后通过输入tag标签,然后jenkins去gitlab获取对应的代码来打包。

jenkins+Python

  Python集成了jenkins的库:jenkins,因为jenkins本身提供了大量基于RESTFUL的API接口,所以Python这个库基本就是基于API接口通过requests实现的一套封装,官网位于:https://python-jenkins.readthedocs.io/en/latest/。通过官网查看,可以发现jenkins提供了以下功能:
DEVOPS笔记(一):基于Python(Django)+jenkins+saltstack实现一套成熟的CI/CD方案_第1张图片
  这里重点讲述如何调用jenkins构建、获取构建状态、获取构建输出三个功能,这三个功能也是我们在调用jenkins-api时最关注的点,其余功能都可以依葫芦画瓢调用。首先让我们在jenkins上创建一个可供API调用的账号,当然账号的权限是要管理员权限,所以我们要基于管理员创建一个token,将token赋予API调用即可,点击jenkins右上角进入设置页面:
DEVOPS笔记(一):基于Python(Django)+jenkins+saltstack实现一套成熟的CI/CD方案_第2张图片
  上图可以看到用户id是:admin,然后能看到API Token的选项,通过添加新token来定义一个名称生成token,这个名称可以随意,因为我们只要账号+token就能实现登录了:在这里插入图片描述
  好的,假设token是:123456,账号是:admin,让我们通过Python来登录jenkins,并执行jenkins的构建任务。

pip install python-jenkins  #安装对应的包
import jenkins
from time import sleep

class JenkinsController:
    def __init__(self, job_name):
        self.job_name = job_name
        self.server = jenkins.Jenkins(jenkins_url,username,token)

    def build(self, tag, giturl):
        """构建"""

        build_number = self.server.get_job_info(self.job_name)["nextBuildNumber"]  # 当前构建编号
        parameters = {"GITURL":giturl,"TAG": tag}   #根据tag来拉取代码并打包
        try:
            output = self.server.build_job("Build"+self.job_name,parameters)
        except Exception as err:
            errmsg = str(err) #记录错误信息返回
        else:
            errmsg = None

        return build_number, errmsg 

    def get_build_status(self, build_number):
        sleep(1)  #jenkins在构建任务前有段缓冲,所以需要睡眠1s再访问
        try:
            build_info = self.server.get_build_info(self.job_name, build_number)
            is_building = build_info["building"]

            if is_building:
                status = "building"
            else:
                status  = build_info["result"] #status状态FAILURE、SUCCESS
        except Exception as e:
            status = "building"

        return status

    def get_build_console_output(self, build_number):
        """获取某次构建的控制台输出"""

        console_output = self.server.get_build_console_output(self.job_name, build_number)
        return console_output

  从上到下,依次是执行构建、获取构建状态、获取构建输出。唯一要注意的是在获取状态的时候,如果再构建的请求发送后马上获取可能会获取失败,此时可以通过睡眠1s再获取来避免,也可以捕获失败来避免。另外关于构建后如何及时获取到其构建成功,可以通过前端轮询来实现。

saltstack

saltstack介绍

  在我们构建成功后可以通过saltstack来实现对应的包升级,此时我们可以把jenkins和salt-master放在同一台机器上,这样构建后的包,可以直接通过salt-master通过salt-cp下发到对应的机器上。
  那什么是saltstack呢?官网介绍:Salt,一种全新的基础设施管理方式,部署轻松,在几分钟内可运行起来,扩展性好,很容易管理上万台服务器,速度够快,服务器之间秒级通讯。
  所以通过salt可以很轻易地管理多台机器,本质上也是C/S架构,即是服务端和客户端的交互,这里的服务端在salt里就是salt-master,客户端就是salt-minion,通过master可以下达shell命令给客户端执行、同步脚本、拷贝文件等等,功能非常丰富,详细的可以参考官网介绍。官网地址位于:

  • 中文:http://docs.saltstack.cn/
  • 英文:https://docs.saltstack.com/en/latest/

  这里简单讲述如何在centos7上安装salt:

yum install -y epel-release
yum install -y salt-master salt-minion

  在本机上测试也是可以的,因为saltstack是支持master和minion安装在同一台机器上,所以单机完全可以构造saltstack来测试(测试环境就是基于单机实现的)。在装完salt-master、salt-minion后,需要修改配置文件,将minion的连向指向master,然后各自启动即可:

minion:vi /etc/salt/minion  修改master:192.168.0.101 #这个地址是我本机地址,当然是随意编造的
salt-master start &
salt-minion restart &

  如果报错如下,这是因为minion在第一次启动时,会在/etc/salt/pki/minion/(该路径在/etc/salt/minion里面设置)下自动生成minion.pem(private key)和 minion.pub(public key),然后将公钥发给master,master通过接受公钥和minion通信

[ERROR   ] The Salt Master has cached the public key for this node, this salt minion will wait for 10 seconds before attempting to re-authenticate

  所以解决办法是:salt-key 查看当前key的状态,然后salt-key -a key接收即可。

salt-api安装

  安装salt并连接通信后,可以通过salt ‘*’ test.ping,简单测试下是否master和minion已经成功连接,此时也可以测试下其他命令。这里继续讲述salt-api的调用,同jenkins一样,Python如果需要和salt连接需要借助于salt-api,而api默认是不安装的,所以我们需要安装和开启salt-api,步骤如下:

安装pyOpenSSL:  /root/.pyenv/versions/2.7.12/bin/pip install pyOpenSSL==0.15.1 
安装salt-api: yum -y install salt-api
配置用户及权限:
useradd -M -s /sbin/nologin devops
echo "123456" | passwd devops --stdin

  安装后我在linux上创建了用户devops,密码为123456服务于api的调用,此时我们需要编辑salt-api的配置,让其开启后监听8000端口来可供调用:

vim /etc/salt/master.d/api.conf
#输入:
rest_cherrypy:
  host: 0.0.0.0
  port: 8000
  debug: True
  disable_ssl: True
  #ssl_crt: /etc/pki/tls/certs/localhost.crt
  #ssl_key: /etc/pki/tls/certs/localhost.key
  log.access_file: /var/log/salt/api_access.log
  log.error_file: /var/log/salt/api_error.log

vim /etc/salt/master.d/eauth.conf
#输入:
external_auth:
  pam:
    devops:
      - .*
      - '@wheel'
      - '@runner'

  然后启动服务,同时需要重启salt-master才行:

systemctl restart salt-master
systemctl start salt-api     /sudo service salt-api restart
netstat -lnpt | grep 8000

  再然后通过curl可以简单地测试是否能ping通master:

curl -k http://127.0.0.1:8000/login \
-H 'Accept: application/x-yaml' \
-d username='devops' \
-d password='123456' \
-d eauth='pam'
salt-api与Python交互

  在下面代码里实现了执行shell命令、shell脚本、分发文件等操作,详细代码如下:

from settings import SALT_API_SERVER,SALT_API_USERNAME,SALT_API_PASSWORD,SALT_API_EAUTH
import requests

class SaltAPI(object):
    """
    调用 salt-api 返回结果格式,为列表嵌套字典形式:
        {'return': [{
            'xxx.xxx.xxx.xxx': 'Thu May 24 14:26:11 ICT 2018',
            'xxx.xxx.xxx.xxx': 'Thu May 24 14:26:11 ICT 2018'
        }]}
    """

    def __init__(self):
        self.session = self.connect()

    def connect(self):
        """
        请求salt-api, 返回session对象,用于保存cookie
        :return: requests.Session instance
        """

        session = requests.Session()
        login_url = SALT_API_SERVER + "/login"
        try:
            session.post(login_url, json={
                "username": SALT_API_USERNAME,
                "password": SALT_API_PASSWORD,
                "eauth": SALT_API_EAUTH
            })
        except Exception as err:
            raise Exception("salt-api connect faield. ", err)

        return session

    def post(self, param, tgt, is_async=False):
        """session post

        :param is_async: async or sync request
        :return: {'http_status_code': http_status_code, 'return_': {"ip": "xxx"}, 'not_matched_agent_list': not_matched_agent_list}
                "return_" type: dict
                "not_matched_agent_list" type: list
        """

        if isinstance(tgt, str):
            tgt = [tgt]

        try:
            response = self.session.post(SALT_API_SERVER, json=[param])
        except requests.exception.ConnectionError as err:
            http_status_code = 500
            return_ = str(err)
            not_matched_agent_list = tgt
        else:
            if response.status_code == 200:
                http_status_code = 200
                return_ = response.json().get('return', [])[0]      # 没有结果则初始化为[], 兼容有的机器不在salt key中的情况

                if is_async:
                    not_matched_agent_list = list(set(tgt) - set(return_.get('minions', [])))
                else:
                    not_matched_agent_list = list(set(tgt) - set(return_.keys()))
            else:
                http_status_code = response.status_code
                return_ = response.reason
                not_matched_agent_list = tgt

        return {'http_status_code': http_status_code, 'return_': return_, 'not_matched_agent_list': not_matched_agent_list}

    def tgt_type(self, tgt):
        """
        根据不同的Python类型返回不同的salt minion匹配类型;
        saltstack minion支持list或者string类型;

        1. 默认string类型,expr_form值为'glob'
        2. list类型时,expr_form值为'list'

        :param tgt: str type or list type ==> eg. '192.168.1.1' or ['192.168.1.1', '192.168.1.2']
        :return: 'list' or 'glob'
        """

        if isinstance(tgt, str):
            return 'glob'
        if isinstance(tgt, list):
            return 'list'

    def execute_cmd(self, tgt='*', cmd=""):
        """在远程主机上执行shell命令

        :param tgt: str or list
        :param cmd: str
        :return:
        """

        expr_form = self.tgt_type(tgt)
        param = {
            'client': 'local',
            'tgt': tgt,
            'fun': 'cmd.run',
            'arg': cmd,
            'expr_form': expr_form
        }
        return self.post(param, tgt)

    def execute_scripts(self, script_name, tgt='*'):
        """在远程主机上执行服务器上的本地shell脚本

        :param script_name: str type ==> eg.  "xxx.sh"
        """

        expr_form = self.tgt_type(tgt)
        kwargs = {"pillar": {'script_name': script_name}}
        param = {
            'client': 'local',
            'tgt': tgt,
            'fun': 'state.sls',
            'arg': ['script'],
            'kwarg': kwargs,
            'expr_form': expr_form
        }
        return self.post(param, tgt)


    def send_file(self, tgt, src, dest):
        """
        文件分发。该函数等同于:salt '*' cp.get_file salt://1.txt /root/

        """

        src = "salt://{0}".format(src)
        expr_form = self.tgt_type(tgt)
        param = {
            'client': 'local',
            'tgt': tgt,
            'fun': 'cp.get_file',
            'arg': [src, dest],
            'expr_form': expr_form
        }

        return self.post(param, tgt)

  每个功能上的注释写的很清楚,这里就不一一介绍了。基于此就能简单地操作各机器,不过要实现CD还差如何发布,这里简单介绍下是如何实现的,可以通过在master的salt的源目录下,正常是:/srv/salt/创建一个_modules文件夹,在这里可以创建各式的Python文件,通过这个文件来实现一套CD,我们主要实现了PHP、Nodejs、Java等三门语言的部署,详细步骤如下:

1.校验打包后的MD5  #因为是从jenkins打包过来的,要确认传输过程中没有损坏
2.备份打包原来的程序目录  #对于java应用就是备份jar包,对于php、nodejs这类的则是先用tar打包,随后再备份
3.更新salt推送过来的包  #java应用即替换jar包,其他服务就是通过tar解压目录到对应的目录
4.通过chown更改用户权限  #因为salt-api是root起的,所以替换后权限会变更为root,此时需要更改回用户的权限才行
5.执行升级后动作  #升级后动作为supervisor来执行启动命令

  根据以上流程就能写出一套部署的Python脚本,从而实现CI/CD。

总结

  介绍到此为止,后续会介绍基于docker+k8s来实现CI/CD方案,感谢观看~

你可能感兴趣的:(devops)