从MySQL范例看Salt的State设计
对于Salt的配置管理功能来说,一个清晰合理的sls格式及结构是其能够可持续发展的关键。Salt最佳实践里面提到了以下几条重要规则:
- 尽可能的清晰和模块化。
- pillar和salt之间要有清晰的映射关系。
- 在需要的地方使用变量,但不要滥用。
- 敏感数据放到pillar中存储。
- 不要在pillar的敏感数据部分使用grains匹配对应服务器。
为了更深的体会这些规则,我准备分析一下官方的Mysql配置范例,学习Salt State的设计、结构以及常用语法。
目录结构
经过简单整理,该范例的目录结构如下:
.
├── pillar
│ └── mysql.sls # pillar与state相对应
└── salt
├── mysql # 每类服务单独一个目录
│ ├── client.sls # 服务的每个部分单独一个sls,这里是安装mysql客户端
│ ├── database.sls # 这里是mysql的db结构配置
│ ├── defaults.yaml # 各类系统的默认服务端、客户端等配置
│ ├── files # 文件、模板单独一个目录
│ │ └── my.cnf # mysql server的配置模板
│ ├── init.sls # 当服务器状态为mysql时的配置,类似于默认配置
│ ├── python.sls # python的mysql模块,注意并没有与client混为一谈
│ ├── remove_test_database.sls # 删除test db的配置
│ ├── server.sls # mysql server配置
│ ├── supported_sections.yaml #
│ └── user.sls # mysql用户权限配置
└── scripts # 辅助的脚本目录,感觉放到mysql目录下更好
└── import_users.py # 将mysql用户权限以合适的格式导入pillar的脚本
动态适配不同的系统
在之前测试时,是在pillar中定义不同服务的配置文件路径的,如果按照这个思路,那么可能会在sls和pillar中使用太多if语句。从vim或者git等示例的map.jinja
文件中,可以看出salt已经给出了一种更清晰的解决方案。在本范例中,对应的文件是defaults.yaml
,从中抽取一个片段:
{% load_yaml as rawmap %}
Ubuntu:
server: mysql-server
client: mysql-client
...
...
CentOS:
server: mysql-server
client: mysql
service: mysqld
python: MySQL-python
config:
file: /etc/my.cnf
sections:
mysqld_safe:
log_error: /var/log/mysqld.log
pid_file: /var/run/mysqld/mysqld.pid
mysqld:
datadir: /var/lib/mysql
socket: /var/lib/mysql/mysql.sock
user: mysql
port: 3306
bind_address: 127.0.0.1
...
...
{% endload %}
在后续的sls中都可以很方便的调用,比如client.sls
:
{% from "mysql/defaults.yaml" import rawmap with context %}
{%- set mysql = salt['grains.filter_by'](rawmap, grain='os', merge=salt['pillar.get']('mysql:server:lookup')) %}
mysql:
pkg.installed:
- name: {{ mysql.client }}
这样再也不用将这些数据臃肿的定义在pillar里或者在每个文件里都重复写一遍if了。
其中load_yaml
和from ... import ...
是序列与反序列化yaml数据,使用格式如下:
# doc1.sls
{% load_yaml as var1 %}
foo: it works
{% endload %}
{% load_yaml as var2 %}
bar: for real
{% endload %}
# doc2.sls
{% from "doc1.sls" import var1, var2 as local2 %}
{{ var1.foo }} {{ local2.bar }}
最重要的是上面的salt['grains.filter_by']
:
salt.modules.grains.filter_by(lookup_dict, grain='os_family', merge=None, default='default', base=None)
在本例中,通过grains='os'
为不同的系统匹配了不同的配置,并且使用merge=salt['pillar.get']('mysql:server:lookup'))
为将敏感数据存储在pillar中提供了扩展(pillar的mysql:lookup字段目前应该废弃了,挺迷惑人的,一度以为mysql:server:lookup
写错了),这个merge应该也能够用于合并通用配置。
模块化并解决依赖问题
Salt提倡尽可能的模块化,本例也确实做到了这一点,服务端、客户端、Python连接模块、库表结构和用户权限等,都分别写在单独的文件里。include
命令能够非常方便的对这些sls进行引用,下面看看本例的init.sls
是如何进行引用并解决一些问题的。
{% from 'mysql/database.sls' import db_states with context %}
{% from 'mysql/user.sls' import user_states with context %}
{% macro requisites(type, states) %}
{%- for state in states %}
- {{ type }}: {{ state }}
{%- endfor -%}
{% endmacro %}
include:
- mysql.server
- mysql.database
- mysql.user
{% if (db_states|length() + user_states|length()) > 0 %}
extend:
mysqld:
service:
- require_in:
{{ requisites('mysql_database', db_states) }}
{{ requisites('mysql_user', user_states) }}
{% for state in user_states %}
{{ state }}:
mysql_user:
- require:
- sls: mysql.database
{% endfor %}
{% endif %}
由于这个文件不长且亮点较多,就都粘贴在这了。init.sls
是一个目录的默认入口,在本例里,三行include
语句平平无奇,会给对应服务器安装mysql服务端,并导入表结构和用户(如果有的话),这个文件的亮点在extend部分。
模块化的好处是显而易见的,但是模块化之后需要解决依赖问题。在本例中,database和user都依赖于service,而且user依赖于database,为了解决这些依赖,init.sls
里进行了以下操作:
- 导入database和user的state ID,当然database和user都提供了这个数据。
- 编写了一个宏,用以避免重复和改善输入格式。(需要关注空白控制)
- 使用extend扩展引用的sls,添加各模块依赖关系。
- 使用require_in,将’a依赖于X,b依赖于X,c依赖于…‘变为’依赖于X的有abc..’,大幅精简这种多对一的依赖关系写法。
- 使用require sls,将多对多的依赖关系精简为多对一个整体。
如果require_in
也能支持sls就好了。这个文件主要向我们展示了如何在引用sls的时候传递授依赖的state ID并精简一对多及多对多依赖关系的写法。
变与不变
不同的服务器需要安装不同的服务;不同的系统安装服务的方式不同;不同的数据库需要不同的数据库结构和用户。在这个示例中,通过以下方法处理了变与不变的关系。
-
通过pillar存储业务所需的变量。
pillar可以用于存储敏感数据,本例中也确实将用户密码、权限等信息存储在这里。pillar还可以用于将过程与数据分离,state不用关心具体的用户权限,只需要根据pillar数据动态生成,pillar也不需要关注各个系统的服务安装过程,只需要定义数据库表结构和用户权限。
-
通过
filter_by
处理系统差异。在上面已经提到,同一个服务在不同的系统上需要不同的安装方式,通过这种过滤手段,state文件自动适用不同的系统。
-
通过程序生成配置。
在本例中,state文件中充满了大量的jinja语句,大部分的state定义其实是通过程序解析数据动态生成的,这一方面简化了sls文件,另一方面也使程序(state)本身与数据分离,提高了扩展性。另外值得注意的就是pillar数据的层级关系,定义好的变量能够很方便的在配置文件模板中使用;以及
user.sls
等文件中的state ID收集,对外提供了解决依赖问题所需的数据。
总结
这个示例的具体语法上面仍然有很多值得学习的,不在此一一列出。通过本例可以学习如何合理的组织一个复杂服务的方方面面,其核心思路如下:
- 模块化服务的不同部分。
- 各模块分别对外提供state ID,通过
extend
处理模块之间的依赖。 - 通过
filter_by
动态适配不同的系统。 - 敏感及更高层次的变量定义在pillar中,与state分离,更方便进行差异化配置。
以上是作为初学者对Salt配置的一些初步想法,可能在后续的使用中会有更多更具体的感受。官方提供了很多范例,有需求的时候要多多参考学习(拷过来改改^_^),避免闭门造车。