Saltstack入门到精通教程(三):配置管理

在上一节的《Saltstack入门到精通教程(二):实验环境搭建和体验》中,我们通过实验环境的操作,熟悉了如何去写一个简单的state文件,如何在命令行去应用state文件,以及如何利用top文件去对不同的minion去应用不同的state文件。这一节我们基于这些基础进一步来探索配置管理中的更多的功能。

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

state函数

书写格式

上一节中,我们简单创建了一个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中查找,发现有purgedremoved两个函数。熟悉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操作

如果要批量部署少不了要从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函数都是调用了远程执行函数。

init.sls

无论是在命令行或者是在前面提到的top.sls中,如果state.apply的目标是一个目录的话,salt会去寻找目录中的init.sls文件来首先执行。

冒烟测试

有些state文件因为造成的结果影响会比较大,在真正下发之前希望可以先测试一下查看运行结果,如果结果没问题再考虑实际下发。

可以通过在state.apply的命令行加上test=True去完成这个冒烟测试的目的,例如

root@saltmaster:/srv/salt# salt 'minion2' state.apply google test=True

会对目标minion执行的操作会在返回结果中用黄色字体标出。

Pillar

前面创建的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/masterfile_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的,所以可以用来传递密码之类的敏感数据

Includes

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.slsgoogle.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

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才会保证两边的文件一致,并不会自动去检测

下一步

学完了这一节,我们已经对salt中的配置管理有了个初步的掌握,但是目前还是一些皮毛,真正的经验还是需要通过实战去积累。官方给我们提供了很多的实战集锦供我们去学习,找一个跟自己比较相关的好好研究下相信对大家的成长是有很大帮组的。

发布了25 篇原创文章 · 获赞 2 · 访问量 1683

猜你喜欢

转载自blog.csdn.net/Victor2code/article/details/103825020