在上一节的《Saltstack入门到精通教程(二):实验环境搭建和体验》中,我们通过实验环境的操作,熟悉了如何去写一个简单的state文件,如何在命令行去应用state文件,以及如何利用top文件去对不同的minion去应用不同的state文件。这一节我们基于这些基础进一步来探索配置管理中的更多的功能。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
在上一节中,我们简单创建了一个states的sls文件,并且用它改变了其中一个minion的配置。我们再来回顾一下我们创建的nettools.sls
文件
install_network_packages:
pkg.installed:
- pkgs:
- rsync
- lftp
- curl
这里使用的是YAML格式,是一种比JSON还要更友好的结构语句。我们将上面这6行文字大致对应到下面的这个格式里面来
ID:
module.function:
- name: name
- argument: value
- argument:
- value1
- value2
ID: 用来描述这个state的字符串,可以带空格和数字,但必须要唯一,后面接一个冒号。
module.function: 每一个函数调用都在ID
的基础上缩进两个空格(注意不能用tab),后面接一个冒号。需要调用的state模块可以在官方state模块列表中查找到,也就是那些名字为salt.states.*
的模块。例如上面这个例子用到的就是salt.states.pkg
,点进去模块就会看到很多个函数。上面提到的官方文档会有函数的功能描述,参数详解和使用范例,对于大多数用户来说是够用了。如果想要查看具体的函数源码,可以到salt的github仓库查看,和states有关的模块在salt/salt/states
中,这里使用的是salt/salt/states/pkg.py
模块中定义的installed
函数。
name:参数列表是一个list数据类型,在YAML中由短线表示每个元素,并且要缩进两个空格。name是一个比较特殊的函数参数,大多数的函数都用name来表示比较核心的参数,例如安装包函数的包名,启动进程的进程名,添加用户的用户名等。所以很少有name参数省略的情况,如果name参数省略,那么要么有别的参数代替了它的功能,或者ID被默认赋值给name。像上面这个例子,如果查阅参数详解就能看到,在pkgs
这个参数存在的情况下,name可以忽略。
argument:如果参数的值只有一个,直接写到同一行即可,中间由冒号和空格隔开。如果参数也是一个list,那么参数从下一行开始,并且有两个空格的缩进,正如上面的例子所展示的那样。
还是采用前一节搭建的测试环境。直接在本地的salt-vagrant-demo-master/saltstack/salt
目录里面添加example.sls
文件,vagrant会自动将其映射到saltmaster的/srv/salt
目录中。通过几个例子来演示下如何去使用state的函数,具体函数不重要,主要是如何去查找函数的思路。
如同上面讲解的那样,在调用pkg.installed
的时候,不仅可以用pkgs
这个参数,还可以用name
来表示待安装的包,只不过name
后面只能接单个参数。例如
install curl:
pkg.installed:
- name: curl
对minion2运行一下
root@saltmaster:/srv/salt# salt minion2 state.apply example
结果如下
minion2:
----------
ID: install curl
Function: pkg.installed
Name: curl
Result: True
Comment: All specified packages are already installed
Started: 09:03:57.208930
Duration: 98.343 ms
Changes:
Summary for minion2
------------
Succeeded: 1
Failed: 0
------------
Total states run: 1
Total run time: 98.343 ms
在salt.states.pkg
中查找,发现有purged
和removed
两个函数。熟悉ubuntu的朋友应该知道这两者的差别,前者完全删除,后者会保留一些配置文件便于误删恢复。我们选择removed
这个函数。
这里会发现函数名都是英文中的过去式,很有意思,而这也正是state的含义所在。state文件定义了一种状态,minion会针对这个状态进行自检,如果不符合这个状态就进行配置修改,如果符合就维持原状。
查看函数的说明,和installed
差不多,name
参数接一个包名,或者pkgs
参数接一个python的list。构建example.sls
文件如下
remove curl:
pkg.removed:
- name: curl
对minion2运行一下
root@saltmaster:/srv/salt# salt minion2 state.apply example
结果如下
minion2:
----------
ID: remove curl
Function: pkg.removed
Name: curl
Result: True
Comment: All targeted packages were removed.
Started: 10:20:15.377807
Duration: 5715.589 ms
Changes:
----------
curl:
----------
new:
old:
7.58.0-2ubuntu3.8
pollinate:
----------
new:
old:
4.33-0ubuntu1~18.04.1
ubuntu-server:
----------
new:
old:
1.417.3
Summary for minion2
------------
Succeeded: 1 (changed=1)
Failed: 0
------------
Total states run: 1
Total run time: 5.716 s
在上面的函数列表中并没有发现salt.states.directory
这个模块,但是找到了salt.states.file
。阅读模块说明发现这个模块不仅可以针对文件,也可以对目录进行操作。
SALT.STATES.FILE
OPERATIONS ON REGULAR FILES, SPECIAL FILES, DIRECTORIES, AND SYMLINKS
发现了其中的salt.states.file.directory
函数,刚好就是我们所需要的创建文件夹的功能。如果文件夹不存在就创建,如果存在就忽略。
Ensure that a named directory is present and has the right perms
构建example.sls
文件如下
make sure a directory exists:
file.directory:
- name: /home/vagrant/test_folder
- user: root
- group: root
- mode: 755
然后在salt master上跑
root@saltmaster:/srv/salt# salt 'minion1' state.apply example
结果如下
minion1:
----------
ID: make sure a directory exists
Function: file.directory
Name: /home/vagrant/test_folder
Result: True
Comment: Directory /home/vagrant/test_folder updated
Started: 07:10:27.070407
Duration: 13.465 ms
Changes:
----------
/home/vagrant/test_folder:
New Dir
Summary for minion1
------------
Succeeded: 1 (changed=1)
Failed: 0
------------
Total states run: 1
Total run time: 13.465 ms
如果ssh到minion1的话,会发现文件目录创建成功,并且属性也是对的
vagrant@minion1:~$ pwd
/home/vagrant
vagrant@minion1:~$ ll -d test_folder/
drwxr-xr-x 2 root root 4096 Jan 2 07:10 test_folder//
vagrant@minion1:~$
这里还有另外一种更简单的方式,也是更常见的方法,就是上面提到的,直接把name参数直接放在ID上面。构建example.sls
文件如下
/home/vagrant/test_folder2:
file.directory:
- user: root
- group: root
- mode: 755
然后在salt master上跑
root@saltmaster:/srv/salt# salt 'minion1' state.apply example
结果如下
minion1:
----------
ID: /home/vagrant/test_folder2
Function: file.directory
Result: True
Comment: Directory /home/vagrant/test_folder2 updated
Started: 07:22:26.027496
Duration: 8.564 ms
Changes:
----------
/home/vagrant/test_folder2:
New Dir
Summary for minion1
------------
Succeeded: 1 (changed=1)
Failed: 0
------------
Total states run: 1
Total run time: 8.564 ms
ssh到minion1的话发现结果也是没问题的
vagrant@minion1:~$ pwd
/home/vagrant
vagrant@minion1:~$ ll -d test_folder2
drwxr-xr-x 2 root root 4096 Jan 2 07:22 test_folder2/
同样的方式,查找到有一个salt.states.file.absent
函数。构建example.sls
文件如下
/home/vagrant/test_folder2:
file.absent
或者
/home/vagrant/test_folder2:
file.absent: []
如果除了name参数以外没有了别的参数,那么函数后面的冒号需要去掉。或者像第二种方式那样用一个空的list来表示
执行了下面的命令以后成功删除了minion1的/home/vagrant/test_folder2
目录,就不详细列出来了
root@saltmaster:/srv/salt# salt 'minion1' state.apply example
和进程相关的有一个salt.states.process
模块,但是里面没有确保进程在跑的函数。然后又找到一个salt.states.service
模块,里面有一个running
函数可以达到目的。这里我们同时确保目标机器安装了redis并且有在跑,创建example.sls
文件如下
install redis and keep running:
pkg.installed:
- name: redis
service.running:
- name: redis
或者利用上面的简洁方式
redis:
pkg.installed: []
service.running:
- require:
- pkg: redis
这里的require语句表示前提条件,会在下面“函数执行顺序”中详细讲到
然后在salt master上跑
root@saltmaster:/srv/salt# salt 'minion1' state.apply example
结果如下
minion1:
----------
ID: install mysql and keep running
Function: pkg.installed
Name: redis
Result: True
Comment: The following packages were installed/updated: redis
Started: 09:03:36.291272
Duration: 17932.682 ms
Changes:
----------
libjemalloc1:
----------
new:
3.6.0-11
old:
redis:
----------
new:
5:4.0.9-1ubuntu0.2
old:
redis-server:
----------
new:
5:4.0.9-1ubuntu0.2
old:
redis-tools:
----------
new:
5:4.0.9-1ubuntu0.2
old:
----------
ID: install mysql and keep running
Function: service.running
Name: redis
Result: True
Comment: The service redis is already running
Started: 09:03:54.252667
Duration: 71.444 ms
Changes:
Summary for minion1
------------
Succeeded: 2 (changed=1)
Failed: 0
------------
Total states run: 2
Total run time: 18.004 s
ssh到minion1的话发现redis确实在跑了
vagrant@minion1:~$ systemctl status redis
● redis-server.service - Advanced key-value store
Loaded: loaded (/lib/systemd/system/redis-server.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2020-01-02 09:03:49 UTC; 10min ago
Docs: http://redis.io/documentation,
man:redis-server(1)
Main PID: 17226 (redis-server)
Tasks: 4 (limit: 1108)
CGroup: /system.slice/redis-server.service
└─17226 /usr/bin/redis-server 127.0.0.1:6379
Jan 02 09:03:49 minion1 systemd[1]: Starting Advanced key-value store...
Jan 02 09:03:49 minion1 systemd[1]: redis-server.service: Can't open PID file /var/run/redis/redis-server.pid (yet?) after start: No such file or
Jan 02 09:03:49 minion1 systemd[1]: Started Advanced key-value store.
如果要批量部署少不了要从git仓库进行clone,查找到有一个函数salt.states.latest
可以确保仓库已经被clone到本地并且是最新状态。创建example.sls
文件如下
https://github.com/saltstack/salt-bootstrap:
git.latest:
- rev: develop
- target: /home/vagrant/temp
然后在salt master上跑
root@saltmaster:/srv/salt# salt 'minion1' state.apply example
可能大家也注意到了,state函数的功能和远程执行命令的函数有一些重复。例如我想确保一个服务在跑的话可以用远程执行命令的salt.modules.service.restart
,也可以通过state的salt.states.service.running
来实现。区别就在于state函数只会在有必要的时候对系统进行修改,而远程执行命令的函数每次跑都会进行修改。例如这个时候服务已经在跑了,远程执行命令操作还是会重启一下服务,而state函数就不会做任何操作。
远程执行函数是salt最先抽象出来的函数,而state函数是在其上又添加了一些代码进行了再次封装,所以很多时候state函数都是调用了远程执行函数。
无论是在命令行或者是在前面提到的top.sls
中,如果state.apply
的目标是一个目录的话,salt会去寻找目录中的init.sls
文件来首先执行。
有些state文件因为造成的结果影响会比较大,在真正下发之前希望可以先测试一下查看运行结果,如果结果没问题再考虑实际下发。
可以通过在state.apply
的命令行加上test=True
去完成这个冒烟测试的目的,例如
root@saltmaster:/srv/salt# salt 'minion2' state.apply google test=True
会对目标minion执行的操作会在返回结果中用黄色字体标出。
前面创建的state文件有一个比较明显的缺陷,就是不够灵活。想象有两批机器,在一个state文件里面有20个函数,有18个函数对两批机器都是完全一样的,但是余下的2个函数对两批机器的参数不一样,如果是前面的state文件就需要2个文件。如果情况更复杂一点的话就会有更多的state文件,但是其实里面大部分的函数都是重复的。
和写代码一样,我们都希望把相同的部分固定下来,然后把个性化的部分用变量来表示。这样不同的机器都可以用同样的state文件,只是传递进去的变量值不一样而已。这样我们只需要关系变量的定义,以及不同minion对应的变量定义文件即可。
pillar就是用来帮助我们解决上面这个问题的。pillar也是通过top.sls
文件去给minion指定不同的变量定义文件,然后每个变量定义文件也是sls后缀。
还是通过上面的实验环境来学习。实验环境已经将本地的salt-vagrant-demo-master/saltstack/pillar
目录映射到了master的/srv/pillar
,这里面是专门用来放pillar相关内容的。
state的默认路径是在
/srv/salt
,可以通过修改/etc/salt/master
中file_roots
来更改;而pillar的默认路径是在/srv/pillar
,可以通过修改/etc/salt/master
中的pillar_roots
来更改
创建/srv/pillar/top.sls
如下,格式和state文件类似,对所有minion采用default.sls
里面定义的变量
base:
'*':
- default
这里采用的还是默认的glob的匹配类型,和远程执行命令一样,也可以用grains,正则表达式等等不同的匹配类型,这时候就需要用match
关键字来进行声明,例如利用正则表达式来进行匹配
base:
'^(memcache|web).(qa|prod).loc$':
- match: pcre
- nagios.mon.web
- apache.serve
所有的匹配类型可以查看这里。
然后创建/srv/pillar/default.sls
如下
package: tree
pillar定义和分派好后要跑salt ‘*’ saltutil.refresh_pillar去下发生效。通过salt.modules.pillar.get去查看定义的pillar,和grains一样通过连续的冒号去获取多层字典的值
这样就把变量定义好了,然后在之前创建好的/srv/salt/example.sls
中去引入变量
install tree:
pkg.installed:
- name: {{ pillar['package'] }}
其中的双大括号是另一种叫做jinja的格式化语言,用于引入常量。这里将整个pillar看成是一个大的键值对的集合,利用中括号找出某个键对应的值。关于更详细的jinja使用语法,欢迎参考我的jinjia博客专栏
然后在salt master上跑
root@saltmaster:/srv/salt# salt 'minion1' state.apply example
就成功对minion1安装了tree这个服务。
假设要对minion1安装tree,但是要对minion2安装redis,那么就可以修改/srv/pillar/top.sls
如下
base:
'minion1':
- default
'minion2':
- redis
然后添加/srv/pillar/redis.sls
如下
package: redis
之后对所有的minion采用example.sls
即可
root@saltmaster:/srv/salt# salt '*' state.apply example
这样就达到了目的。
不过这里还是创建了两个pillar文件,依然不够简洁,后面我们会通过pillar中的条件判断在同一个pillar文件中为不同的minion配置不同的变量值。
pillar因为是加密传递给minion的,所以可以用来传递密码之类的敏感数据
pillar的变量引入已经让state文件变得非常的DRY(Don’t repeat yourself),但是我们还可以更进一步。可以将一些重复率较高的函数集合放入单独的state文件,如果有别的state文件要引入它们,可以直接利用include关键字去实现。
纯粹为了举例,假如有一个实现curl google网页的state文件。然后include另外一个安装curl的state文件以确保curl这个工具有被成功安装。
install curl.sls
内容如下
install curl:
pkg.installed:
- name: curl
curl google.sls
内容如下
include:
- install curl
curl google:
cmd.run:
- name: curl -L www.google.com -o /home/vagrant/google.html
然后在salt master上跑
root@saltmaster:/srv/salt# salt 'minion1' state.apply 'curl google'
这样的好处就是可以把install curl.sls
这个文件复用在别的地方
如果include的文件是在别的目录下面的,需要用dir.filename的格式去include
但是,我为什么不直接在top.sls
里面对目标minion分配这两个文件呢?
当然也是可以。通常来说复用的比较频繁的时候用include会比较合适,而偶尔的复用用top.sls去分配要更容易,这个完全看自己的喜好吧。
如果include的对象是一个目录名,那么其实是include目录下面的init.sls
文件。如果目录下面没有init.sls
会因为找不到state文件报错。
例如,创建example.sls
的内容如下,安装和启动ssh服务端
include:
- ssh
openssh-server:
pkg.installed
sshd:
service.running:
- require:
- pkg: openssh-client
- pkg: openssh-server
- file: /etc/ssh/banner
- file: /etc/ssh/sshd_config
/etc/ssh/sshd_config:
file.managed:
- user: root
- group: root
- mode: 644
- source: salt://ssh/sshd_config
- require:
- pkg: openssh-server
/etc/ssh/banner:
file:
- managed
- user: root
- group: root
- mode: 644
- source: salt://ssh/banner
- require:
- pkg: openssh-server
同时创建ssh/init.sls
,内容如下,安装和启动ssh客户端
openssh-client:
pkg.installed
/etc/ssh/ssh_config:
file.managed:
- user: root
- group: root
- mode: 644
- source: salt://ssh/ssh_config
- require:
- pkg: openssh-client
这里的require语句表示前提条件,会在下面“函数执行顺序”中详细讲到
如果想要include目录中的某个state文件,可以用folder.filename
的方式,需要注意的是要省略掉.sls
后缀名
默认情况下,state文件里面的ID都是按照从上到下的顺序去执行的,如果说在top.sls
文件里面分配了多个state文件给同一个minion,那么也是按照从上到下的顺序去执行这些文件的内容。
但是有的时候也是可以人为修改函数执行顺序,例如前面的include就可以让被include的函数先执行。
同样还可以用require
关键字去让被require的ID去先执行,但是这里要尤其注意格式,同时被require的ID所在的state文件要么通过include被引入,要么在top.sls中被分配到一起。参考下面这个github issue。
https://github.com/saltstack/salt/issues/42187
例如有两个state文件分别为curl.sls
和google.sls
,内容分别如下
install:
pkg.installed:
- name: curl
include:
- curl
curl google:
cmd.run:
- name: curl -L www.google.com -o /home/vagrant/google.html
- require:
- pkg: install
可以看到首先include了另一个state文件,这是前提条件一。然后在google.sls
中的curl google
这个ID跑之前首先要去找一个前提条件。这个前提条件的格式为module: ID
,这是前提条件二。
我知道这个格式非常的让人困惑,但是生活就是这样子,该遵守的规则还是得遵守。上面这个格式亲测有效,需要注意的是官网给出的格式是错的。
要查看一个state文件的函数执行顺序,可以用下面的语句。例如想查询上面google.sls
这个文件里面函数的执行顺序,就可以用
root@saltmaster:/srv/salt# salt 'minion1' state.show_sls google
执行结果如下
minion1:
----------
curl google:
----------
__env__:
base
__sls__:
google
cmd:
|_
----------
name:
curl -L www.google.com -o /home/vagrant/google.html
|_
----------
require:
|_
----------
pkg:
install
- run
|_
----------
order:
10001
install:
----------
__env__:
base
__sls__:
curl
pkg:
|_
----------
name:
curl
- installed
|_
----------
order:
10000
Jinja不光可以用在state文件里面,其实在salt里面是全局可用的,例如pillar。
这里简单用例子来介绍常用的语法,更细致的jinja语法可以参考我的jinja博客专栏。
在pillar中可以根据minion的os类型来定义变量,例如创建一个/srv/pillar/common.sls
文件如下
{% if grains['os_family'] == 'RedHat' %}
apache: httpd
git: git
{% elif grains['os_family'] == 'Debian' %}
apache: apache2
git: git-core
{% endif %}
这里可以看出grains和pillar一样在jinja中都是一个大的键值对
然后在/srv/pillar/top.sls
中添加
base:
'*':
- common
这一步在官方文档中省略了,亲测如果省略是不会传递下去给minion的
然后跑salt '*' saltutil.refresh_pillar
将pillar值下发到minion。这一步不跑的话下面这一步查看可能会有问题,建议跑一遍。
这之后跑salt '*' pillar.items
就可以查看所有minion的pillar值了,或者是跑salt '*' pillar.item apache
去查看apache这个变量的值。或者是salt '*' pillar.ls
去查看pillar项目。这里的操作和grains还是蛮像的。
这样就可以在state文件中去安装apache了,例如/srv/salt/example.sls
install apache:
pkg.installed:
- name: {{ pillar['apache'] }}
常用的是for循环,例如想要确保/home/vagrant/folder1
,/home/vagrant/folder2
,/home/vagrant/folder3
都是存在的,可以创建下面的state文件
{% for DIR in ['/home/vagrant/dir1','/home/vagrant/dir2','/home/vagrant/dir3'] %}
{{ DIR }}:
file.directory:
- user: root
- group: root
- mode: 774
{% endfor %}
就可以省去重复配置了。
更多的jinja在salt中的高级使用,例如管道符过滤,调用salt远程执行命令等等,可以参考官方文档。
很多时候,我们都在master上编辑更新一份配置文件,例如mysql的配置文件,然后希望这份配置文件可以自动同步到目标minion。这个时候就需要用到传递文件功能了。
查阅state模块,发现了salt.states.file.managed
函数用来解决这个问题。这个函数基本传入两个参数即可,一个是name
是目标的完整带路径的文件名,另一个是source
是master上完整带路径的文件名。
master上的salt://指向目录
/srv/salt
例如创建/srv/salt/example.sls
如下
copy file to minion:
file.managed:
- name: /home/vagrant/dir1/test.txt
- source: salt://files/test.txt
apply这个state文件之后就会同步master上面的文件到目标机器了。注意以后再次修改了master上的文件需要手动去apply这个state才会保证两边的文件一致,并不会自动去检测。
结合上面的jinja语法就可以更进一步,将配置文件中的某些配置项配置为jinja变量,然后在state文件中利用template
和defaults
关键字来传递变量值。当然也是可以选择用pillar来传递的。
例如有配置文件salt://files/jinja_defaults_test.txt
如下,其中有两个配置项为变量
Listen {{ port }}
People {{ people }}
然后创建state文件jinja_defaults_test.sls
如下
/home/vagrant/jinja_defaults_test.txt:
file.managed:
- source: salt://files/jinja_defaults_test.txt
- template: jinja
- defaults:
port: 8080
people: xiaofu
需要注意的是,defaults
内容必须要以字典的形式传递进去。然后就可以同步文件了
root@saltmaster:/srv/salt# salt 'minion2' state.apply jinja_defaults_test
成功以后去minion2上查看,发现变量值已经被成功传递
vagrant@minion2:~$ cat jinja_defaults_test.txt
Listen 8080
People xiaofu
还可以利用上面讲的条件选择来针对不同的机器设置不同的配置变量值,修改上面的jinja_defaults_test.sls
如下
/home/vagrant/jinja_defaults_test.txt:
file.managed:
- source: salt://files/jinja_defaults_test.txt
- template: jinja
- defaults:
port: 8080
{% if grains['id'] == 'minion1' %}
people: xiaofu
{% elif grains['id'] == 'minion2' %}
people: xiaozhu
{% endif %}
这里针对minion1和minion2传递了不同的变量值,对所有minion应用这个state文件
root@saltmaster:/srv/salt# salt '*' state.apply jinja_defaults_test
成功以后就可以去两个minion上分别查看
vagrant@minion1:~$ cat jinja_defaults_test.txt
Listen 8080
People xiaofu
People xiaofuvagrant@minion2:~$ cat jinja_defaults_test.txt
Listen 8080
People xiaozhu
通常来讲,state文件里面用来处理事务处理逻辑,不建议把数据处理放在这里面。可以通过pillar来进行上述数据的处理
创建pillar文件jinja_defaults_test.sls
如下,并分配给所有的minion
{% if grains['id'] == 'minion1' %}
people: xiaohong
{% elif grains['id'] == 'minion2' %}
people: xiaohua
{% endif %}
base:
'*':
- jinja_defaults_test
下发给所有的minion
root@saltmaster:/srv/salt# salt '*' saltutil.refresh_pillar
修改上面的state文件如下
/home/vagrant/jinja_defaults_test.txt:
file.managed:
- source: salt://files/jinja_defaults_test.txt
- template: jinja
- defaults:
port: 8080
people: {{ salt['pillar.get']('people',80) }}
推荐用执行命令的方式去获取pillar值,而不要直接用pillar[‘people’]的方式,因为可以返回默认值,处理key不存在的复杂情况
应用到所有的minion以后去查看,也达到了传递变量的目的
vagrant@minion1:~$ cat jinja_defaults_test.txt
Listen 8080
People xiaohong
vagrant@minion2:~$ cat jinja_defaults_test.txt
Listen 8080
People xiaohua
学完了这一节,我们已经对salt中的配置管理有了个初步的掌握,但是目前还是一些皮毛,真正的经验还是需要通过实战去积累。官方给我们提供了很多的实战集锦供我们去学习,找一个跟自己比较相关的好好研究下相信对大家的成长是有很大帮组的。