很久没做笔记了。新年期间时间充裕,可以重新开工了。
因为工作的需要,接触到saltstack。也围绕着saltstack做了很多的工作。
stackstack不仅是一个自动化工具,也是一个自动化任务的开发框架。
相较于ansible, chef等自动化工具,salt也有其独到的特点:
- 纯python开发,插件式的架构让pythoner很容易的进行扩展
- 分布式架构,节点minion自动连接master,不依赖于ssh,非常适合主机自动注册的场景。并且方便在客户端执行独立的逻辑
- 对windows的支持很好
而标题中所谓的设计模式,指的式在实现具体的功能任务之外,还需要考虑以下的因素:
- 最大化代码的复用
- 减少不必要的调用,增加程序性能
- 增加代码的可读性,方便团队协作
- 降低可能出现人为失误的情况,缩小人为失误因素所带来的影响
在这个系列中,我会用一些实际的案例分享saltstack中的任务设计经验。
废话不多说,下面就是我们第一个案例
aws标签管理
在这个案例中,我们的任务是对aws主机的标签信息进行检测,并关联于salt中的其他自动化任务。
我们的需要面临的问题主要有:
- 如何获取aws的信息,并将这部分信息应用于saltstack的其他任务
- 如何编写标签检测的任务,并嵌入到其他业务逻辑中
获取aws主机信息
salt主要有两种存储变量的方案:
- grains,minion根据本地主机信息进行生成
- pillar,通过master端生成,并按照规则分配给minion,每个minion独享自己的pillar,无法得知其他minion的pillar信息
这个场景下,适配的方案显然是使用pillar。但是默认的pillar是服务端的yaml,如何将外部的动态数据变成pillar,我们有两种选择。
- 使用pillar extension的方式将外部数据库接入salt系统,并通过一个外部脚本周期性的通过aws api将aws的数据塞进这个外部数据库
- 使用py renderer的方式将对应的pillar换成python程序
对于第一种方案,优点在于,提供了一个一致性的数据接口,一定程度上简化了设计。但最大的问题在于pillar的获取和刷新之间没有任何联系,实时性不好。在刷新周期新注册的主机无法完成基于这些数据的自动化任务。如果需要拥有一定的实时性,通过事件驱动的方式触发数据更新。比如采用aws lambda或者是salt reactor拦截主机进入的event。实现起来都比较复杂。同时,因为事件系统本身是异步的,很难做到和依赖的任务进行同步,想要达到理想的效果实现起来十分复杂。但是如果这些问题都由外部的cmdb来解决,那无疑是一个更好的办法。
对于第二种方案,能够保证实时性,但是最大的问题在于调用次数过度频繁。这个问题可以通过在代码中引入一层缓存来解决。
平衡利弊,第二种方案更合适一些。
在pillar中存储aws信息
安装依赖服务与相关第三方库
在saltmaster上安装redis,以及对应的缓存库。我找到了一个第三方的redis缓存库来做这个工作,减少代码量。
如果希望采用别的方式,可以自己来实现缓存逻辑,也是比较简单的。
|
|
实现代码
/srv/pillar/aws/init.sls
|
|
我利用了salt.config 将配置同代码进行了分离。
配置文件放置在/etc/salt/master.d/aws.conf
下面。
|
|
而run函数,则是pillar的主体,返回的dict,就是需要的结果。
而renderer的关键之处,在与我们使用__salt__["grains.get"]("ipv4")
获得了minion的ip地址。虽然整个代码是在master端执行的,但是整个执行环境中导入的双下划线变量,却是在minion端执行。因此,我们也可以将客户端的ip地址作为变量,传给渲染器
剩下的代码逻辑就比较简单了,如果在缓存中已经存在信息了,就读取缓存,否则,通过aws的api直接获取信息。这样就可以解决实时性的问题
通过定期脚本更新信息
虽然新加入的主机pillar的实时性的问题得到了解决,但是更新还是需要等缓存过期。如果缓存时间过短,aws api的调用还是会很频繁,缓存时间太长。主机信息的更新变不及时。一个最简单的办法就是周期性进行一个全量的查询,进行缓存更新。同时利用这次全量查询,可以执行一些额外的任务,比如清除已经不在aws中存在的主机key, 代码和pillar的代码差别不大
/opt/scripts/saltcron.py
|
|
如何利用pillar中aws的信息
对于大部分任务,我们可以使用salt推荐的jinja模版渲染的方式利用pillar里面的值并进行逻辑控制。
或者是通过state的onlyif,unless等参数进行约束,这种方式可以应用于绝大部分场景。
但是对于这个案例,我们需要对tag进行一些正则匹配,然后用正则匹配的结果反复应用于各种任务。
因此,我们最好一次性将所有的检测条件写好,并在其他的sls中引用这个写好的检测条件。
于是,我又再换一种设计模式
添加正则匹配的state扩展
正则匹配校验是一个同业务无关非常底层的需求。因此我将这部分功能做成state扩展,最大化代码的可重用性。
/srv/salt/_states/valuecheck.py
|
|
将校验逻辑写成一个独立的sls
校验逻辑已经属于业务层的代码了,因此放在sls中更为简单且易于阅读。
/srv/salt/system/checks/checktag.sls
|
|
上面的valuecheck.check就是我们刚刚写好的state扩展。
但是我们如果每次做这组校验,都要写一堆条件,比较麻烦。因此最后的wrap.wrap,就是为了解决这个问题的。
warp的代码如下:
/srv/salt/_states/wrap.py
|
|
如代码所见,这个函数的目的是在满足state模块接口的情况下,实现一个极为简单空逻辑,通过传入参数的不同,直接决定state的结果。我们利用这个不执行任何有意义逻辑的任务,附加上require,就可以实现简化的目的了
在业务逻辑中使用校验逻辑
终于到最后一步了。我祛除了具体的业务逻辑,代之以一个简单的例子。
somejob.sls
|
|
在这个任务的sls中,我们通过include引用刚刚写好标签检测任务。并将这个检测点附加在对应的执行节点上。如果我们有很多任务都依赖于这个检测,那么通过highstate调用的检测只会执行一次,不会重复执行。
总结
最后针对这个案例,我们进行一下技术总结:
- 可以通过pillar + renderer 的方式从外部系统导入数据到salt中,但需要使用缓存来降调用次数
- 通过state扩展的方式,实现一些控制逻辑,这样代码会比使用jinja渲染更为清晰,同时避免执行顺序不通导致的逻辑错误
- 将真实的业务逻辑以及基础代码分离解耦,让业务逻辑更为清晰且易于维护