公司马上要上k8s+docker的形式来实现CI/CD(持续集成和持续部署),在这里汇整总结一套基于Python+jenkins+saltstack来实现传统虚拟机上的CI/CD。集成是依赖于jenkins的搭建,关于jenkins的搭建和使用这篇文章就不叙述了,主要阐述如何通过Python将jenkins集成到自建的集成发布平台;发布是依赖于saltstack来管理多台服务器,saltstack本身是Python撰写的,也提供了一套成熟的API来交互,所以采用saltstack来管控发布平台也是非常轻松的。当然你也可以用ansible等其他多服务器管理方案。
随着互联网的蓬勃发展,如何快速的迭代APP功能显得尤其重要,于是出了敏捷开发的概念,它的核心理念是,既然我们无法充分了解用户的真实需求是怎样的,那么不如将一个大的目标不断拆解,把它变成一个个可交付的小目标,然后通过不断迭代,以小步快跑的方式持续开发。随后就是DEVOPS的时代,即DEV(development,开发人员)+OPS(operations,运维人员)相结合的模式,来加速CI(continuous integration,持续集成)+CD(continuous deploy,持续部署),因为敏捷开发仅仅是提高了开发和测试的速度,但是在专注于系统和安全的运维身上,却又没办法更快地加快部署,所以产生了DEVOPS这个概念,来让开发、测试、运维打造成一体,由运维负责审核,开发、测试负责上线新功能这样的迭代方式。而这就依赖于需要建设一个可以提供Ci\CD和供运维人员审核的平台。
当然DEVOPS更多的是阐述一种文化和理念,CI/CD是其中重要的一环,但并不是全部。本文即讲述了基于jenkins的持续集成,基于saltstack的持续部署,而后将这二者集成于一个平台上实现devops平台。
作为当下最流行的构建平台之一,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获取对应的代码来打包。
Python集成了jenkins的库:jenkins,因为jenkins本身提供了大量基于RESTFUL的API接口,所以Python这个库基本就是基于API接口通过requests实现的一套封装,官网位于:https://python-jenkins.readthedocs.io/en/latest/。通过官网查看,可以发现jenkins提供了以下功能:
这里重点讲述如何调用jenkins构建、获取构建状态、获取构建输出三个功能,这三个功能也是我们在调用jenkins-api时最关注的点,其余功能都可以依葫芦画瓢调用。首先让我们在jenkins上创建一个可供API调用的账号,当然账号的权限是要管理员权限,所以我们要基于管理员创建一个token,将token赋予API调用即可,点击jenkins右上角进入设置页面:
上图可以看到用户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来实现对应的包升级,此时我们可以把jenkins和salt-master放在同一台机器上,这样构建后的包,可以直接通过salt-master通过salt-cp下发到对应的机器上。
那什么是saltstack呢?官网介绍:Salt,一种全新的基础设施管理方式,部署轻松,在几分钟内可运行起来,扩展性好,很容易管理上万台服务器,速度够快,服务器之间秒级通讯。
所以通过salt可以很轻易地管理多台机器,本质上也是C/S架构,即是服务端和客户端的交互,这里的服务端在salt里就是salt-master,客户端就是salt-minion,通过master可以下达shell命令给客户端执行、同步脚本、拷贝文件等等,功能非常丰富,详细的可以参考官网介绍。官网地址位于:
这里简单讲述如何在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并连接通信后,可以通过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'
在下面代码里实现了执行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方案,感谢观看~