自动化运维之Puppet

自动化运维第二步, 比Ansible还强大的集中配置工具Puppet.

了解Puppet的类, 模板, 配置语言和资源.

Puppet简介

我们之前使用过Ansible使得集群实现了集中部署, 十分方便. 那么和Ansible相比较, Puppet有什么不同吗? 从规模上讲, Puppet管理的节点个数要远远大于Ansible, 使用Puppet管理几千个节点都是很轻松的. 而且我们在说Ansible的时候提到过Puppet是需要agent的, 而Ansible是agentless的.

在大部分的场景下, 我们使用Ansible都是需要手动进行的. 而Puppet可以做到在很多场景下进行自动的管理. 几乎是整个生命周期的管理, 如下:

  • provisioning: 安装
  • configuration: 配置
  • orchestration: 编排
  • reporting: 报告

另外, 说到Puppet的版本也很有趣, 他的版本演化大体是这样的:

0.2 –> 0.24.x –> 0.25.x –> 0.26.x(2.6) –> 2.7 –> 3.0 –> … –> 5.X

目前的版本已经到了5.3.2, 其中2.7是一个过渡版本, 和2.6差距甚远.

继续说说Puppet, 由于是master-agent模型, 所以我们需要在每一个节点上安装客户端, 接着选择一个节点充当服务器端. 但是, 即使是使用Ansible去集中分发安装也不是很理想, 一般的做法倾向于使用模板的方式, 基于虚拟机和容器来进行构建, 并且, 得益于云环境, 虚拟化技术, 我们在系统的安装上面也可以得到很多方便的地方.

我们来对比一下Ansible和Puppet(准确的说是对比一下有无agent), 首先, 如果没有agent的协助, 我们就需要使用ssh协议进行通信, 另外, 不仅如此, ssh的用户就是我们执行命令的用户, 而大部分情况下都是root权限执行, 这样直接放任登录是十分不安全的, 即使说可以使用sudo, 但还是一样, 攻击者依然可以利用sudo来间接使用管理权限. 而agent就不一样了, agent和本地的权限是仅仅存在在本机的, 而和服务器端的认证就是基于自己的私密协议, 或者证书, 或者预共享密钥等等. 这样的安全系数是提高了不少的.

我们真正的功能实现实在agent端的, master仅仅是为了发送指令, 并管理agent端. 接下来我们来说说Puppet的工作模型吧.

Puppet采用了基于模型, 声明性的配置编程语言, 由于Puppet是用Ruby写的, 所以这个编程语言其实可以说是Ruby的子集.那么具体是什么样的工作模式呢, 我们把他们分成四步:

  • 定义(define): 在这一步我们使用刚才说的编程语言来定义资源和他们的状态. 这里说的资源不是之前在说HA的时候的资源. 这里的资源可以类比的比作Ansible的模块
  • 模拟(stimulate): 这里, Puppet根据资源关系, puppet在本地无损运行测试代码, 相当于是一次模拟部署
  • 没有问题了之后, 就进入了强制(force): 比对客户机的状态和定义的资源状态是否一致, 自动强制执行
  • 最后就可以将执行的结果日志发送到自带的dashboard或者其他的第三方的可视化平台.

在说具体的Puppet的语言之前, 我们还需要介绍一下Puppet的组成层次, 一共分成三层:

  • 配置语言层
  • 事务层
  • 资源抽象层

这里我们稍微补充一些吧, 首先是资源抽象层,其实这个我们在Ansible中已经感受过了, 安装一个服务, 在Ubuntu和CentOS下的工具和过程肯定是不一样的. 再或者, Windows主机上和Linux主机上创建一个用户的系统调用也是不一样的, 而抽象就可以将不一样的调用封装成为统一的接口. 向上一层, 事务层其实就和他的名字一个作用, 如果我们要启动http的服务但是, Apache根本就没有安装, 这根本就不可能启动成功, 所以出现了问题就要进行回退. 基于这些, 最后提供一个配置语言的借口就可以了.

其中, 我们的资源可以分成这样的三个维度

  • 资源类型: 例如用户, 组, crontab, 文件, 服务, 等等..
  • 属性及状态以及实现方式分离
  • 期望状态, 决定对应的资源存在与否

因此, 说道这里, 我们就能知道了puppet的核心组件就是资源了. 我们在使用Ansible的时候, 因为在命令行中手敲命令实在是太麻烦了, 所以我们编制yaml格式的playbook来执行, 对于Puppet, 他把这个类似的东西叫做: 资源清单(manifest).

特定的资源在清单中组织起来,以及资源依赖的文件形成的这样的一个层次, 在Ansible中叫做角色(Role), 我当时提过但是没有深入的去实现. 所以简单的小结一下就是: Puppet的资源相当于是Ansible的模块, 而资源清单相当于Ansible中的角色.

现在我们就可以更加具体的过一遍Puppet的执行过程了:

先来考虑一台主机的过程, 这就很简单了. 根据需求我们可能会有多个manifest清单文件, 我们说过这是ruby的子集, 所以要经过编译成为伪代码, 这个伪代码其实已经是二进制的了, 所以会直接进行执行, 这个过程就是apply阶段了, 进行状态查询和执行目标状态.

但是更多的场景是master/agent的模型,agent向master端请求catalog(伪代码), 同时发送自己的主机名和facts信息. 而master节点收到了请求之后, 开始查询请求者的站点清单, 选择适当的manifest, 经过编译成为二进制的catalog, 发送给客户机 注意这发送的是二进制码, 客户机经过应用阶段之后, 将执行的报告发送给服务器端. — 编译过程发生在master节点上. 另外, 这里所说的站点清单, 其实就是include了许多manifest的一个清单文件.

我们知道Ansible是基于SSH协议做的认证和数据传递, 那么Puppet的主从节点之间是怎么沟通的呢?

答案是: HTTPS. 这也算是个挺麻烦的协议了, 但是实际上我们在配置的时候是挺容易的, 原因就在于这个master节点自己就是个CA. 所以他可以自己向client签发证书. 不仅如此, 每一个agent其实自己是有一个准备好的证书请求的, 那么问题就简单了, 并且这个也成为了核心: 要不要给agent签署证书. 一旦签署了就说明双方互相信任了. 那么到底怎么签署呢? 这个其实就只能看管理员自己了, 是否签署只能由人工来判定, 当然也是可以全部批量签署的, 但是那样显然是存在安全风险的.

说道这, 我们也可以看到Puppet和Ansible的另一个不一样的点: Ansible是服务器端主动进行推送, 而Puppet是客户端去服务器端拉取二进制代码, 服务器端根据主机名选择清单进行编译, 最后返回结果. 不过本质上二者的机制是一样的.

现在我们就来安装一下Puppet试试吧. 可以在他的官方站点找到rpm包. 由于Puppet是使用Ruby编写的, 所以需要Ruby的依赖.

安装完成之后, 我们就可以来试试了, 首先就是先来看帮助了. Puppet的帮助信息获取是通过help的子命令进行的, 他的命令格式就像这样:

1
Usage: puppet <subcommand> [options] <action> [options]

由于子命令也算不少, 所以就不一一说, 用到的时候我们再说好了.

前面说过Puppet有一个资源抽象, 那么支持什么资源类型呢, 我们可以通过下面的命令查看:

1
2
3
4
5
6
7
8
9
10
[root@master ~]# puppet describe -l
These are the types known to puppet:
augeas - Apply a change or an array of changes to the ...
computer - Computer object management using DirectorySer ...
cron - Installs and manages cron jobs
exec - Executes external commands
file - Manages files, including their content, owner ...
filebucket - A repository for storing and retrieving file ...
group - Manage groups
...(omitted)

接下来我们就从资源切入, 来说说如何定义资源和清单文件吧.

Puppet资源

定义一个资源很简单, 只要使用下面的语法格式就可以了:

1
2
3
type { 'name'
key => value1[,value2[,]]
}

例如这样的例子:

1
2
3
4
5
6
7
8
user { 'testuser'
ensure => present,
uid => '777',
gid => '777',
shell => '/bin/sh',
home => '/home/testuser',
managehome => true,
}

在定义资源的时候, 资源类型必须使用小写. 而且同一个类型下同一个名字的资源只能存在一个.(就是说不能同名啦)

另外, 我们在定义资源的时候, 会有一些特殊的属性, 主要分三类:name, ensure, metaparameter.

其中, name不同于我们在上面定义的'testuser', 这是一个用来在目标系统上识别特定属性的一个变量, 每一个资源类型只能有一个, 也就是说是唯一的. 有没有觉得这个又和'testuser'有点相像? 是了, 其实他们两个在某种意义上都指向同一变量, 如果不指明name, puppet就会使用'name'来充当namevar.

接着我们再来说说ensure这个特殊属性, 有点类似Ansible的state的, 主要有这些值:

  • file 存在并且是文件
  • directory 存在并且是目录
  • present 存在, 通用的描述上面三种
  • absent: 不存在

接下来, 就是实战一些常见的资源吧.

我们首先在家目录下创建一个manifest目录, 接着就在这里面做测试吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@master ~]# mkdir manifest
[root@master manifest]# vim test1.pp
<!--in vim-->
group { 'centos':
gid => 2000,
ensure => present
}

user { "centos":
gid => 2000,
uid => 2000,
shell => /bin/sh,
home => /home/centos,
ensure => present,
}

应该很好看懂的, 接着就是生成catalog了, 由于我们现在是在单机测试, 而不是主从模型, 所以我们使用puppet的apply子命令:

1
2
3
4
5
6
7
[root@master manifest]# puppet apply -v test1.pp
Notice: Compiled catalog for master in environment production in 3.86 seconds
Info: Applying configuration version '1510488497'
Notice: /Stage[main]/Main/Group[centos]/ensure: created
Notice: /Stage[main]/Main/User[centos]/ensure: created
Info: Creating state file /var/lib/puppet/state/state.yaml
Notice: Finished catalog run in 19.48 seconds

好慢啊…

此时 我们的用户和组就已经创建完成了. 可以来确认一下:

1
2
3
4
[root@master manifest]# tail -1 /etc/group
centos:x:2000:
[root@master manifest]# tail -1 /etc/passwd
centos:x:2000:2000::/home/centos:/bin/sh

而且, 这里我们都没有指明name, 所以就自动选择了title作为我们的namevar.

我们之前也说过了, 使用describe可以来查看特定的资源, 接下里我们就一个一的来说吧:

group

group的经藏使用的选项还是很少的, 所以比较简单, 就是以下的几个

  • name: 组名, NameVar
  • gid: GID
  • system: true, false
  • ensure: present, absent
  • members: 组内成员

很简单, 直接看下面类似的user吧

user

user就要比group麻烦一点了, 选项的个数也比较多

  • Comment: 注释信息
  • ensure: 不用再解释了吧
  • expiry: 过期期限
  • gid: GID
  • groups: 属于哪个组
  • home: 家目录位置
  • shell: 默认的shell
  • name: 用户名, nameVar
  • system: 是否是系统组
  • uid: UID
  • password: 用户的密码, 根据对方的操作系统不同, 使用不同的带有杂质的不同加密算法来进行

其实也很容易, 接着看一个和Ansible很相像的:

file

用来管理文件, 文件的内容, 文件的属主属组, 他们的权限 . 可以处理的文件包括一般文件, 目录文件, 符号链接等.

文件的内容, 可以直接通过content指出, 支持换行符等. 所以就有这样的属性了:

  • content: 文件的内容制定, 可以使用\n等特殊字符
  • source: 从指定位置下载文件
  • ensure: file, directory, link, present, absent

以上的三个属性, 用来指明文件的内容来源, 接下就是一些常规的属性:

  • force: 如果文件和目录同名, 是覆盖还是放弃
  • mode: 指明权限, 可以使用数字或者字符
  • group: 属组
  • owner: 属主
  • path: 目标路径
  • mtime, ctime, atime
  • target: 当ensure为link的时候, target指定的是符号链接的目标, path就是链接的位置了

接着就来试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
file { '/tmp/testdir':
ensure => directory,
}

file { '/tmp/puppet.test':
content => 'Puppet test\nLine two',
ensure => file,
owner => 'centos',
group => 'centos',
mode => '0400'
}

file { '/tmp/fstab':
source: "/etc/fstab",
ensure => file
}

file { '/tmp/testdir/puppet.link':
ensure => link,
target => "/tmp/puppet.test"
}

我们编写这样的示例, 接着编译一下试试:

1
2
3
4
5
6
7
[root@master manifest]# puppet apply test2.pp
Notice: Compiled catalog for master in environment production in 0.29 seconds
Notice: /Stage[main]/Main/File[/tmp/puppet.test]/ensure: defined content as '{md5}870d2a178ae85c5ea7732338c4863ef2'
Notice: /Stage[main]/Main/File[/tmp/fstab]/ensure: defined content as '{md5}82b1f625714ceefc4886929b1f95a00b'
Notice: /Stage[main]/Main/File[/tmp/testdir]/ensure: created
Notice: /Stage[main]/Main/File[/tmp/testdir/puppet.link]/ensure: created
Notice: Finished catalog run in 1.71 seconds

接着就可以去/tmp下去确认了.

接下来的一个也是一个很重要的类型了, 那就是执行命令的类型: exec

exec

用来执行外部命令, 而且支持条件执行, 以及触发执行, 这就很灵活了. 另外, 命令执行本身具有幂等性.

他有哪些属性呢? 我们来看一下:

  • command: 指定运行的命令, 这个就是nameVar
  • creates: 如果目标文件不存在, 就会执行nameVar指定的命令
  • cwd: 指定工作路径
  • user: 指定命令的执行者
  • onlyif: 只有这个属性的执行结果是0才会执行nameVar指定的命令
  • unless: 和onlyif相反, 只有执行失败才会执行nameVar
  • refresh: 指明如何更新这个资源, 如果是服务, 我们可以在更新了配置文件之后重新载入或者重新启动, 但是对于命令, 这个就需要我们手动指定了. 默认是在再次执行一遍 也可以手动指定成其他的.
  • refreshonly: 类似的, 这个意思就是只有在收到通知的时候才执行.
  • returns: 期望的返回值, 返回的其他值都被认定成是执行失败的.
  • tries: 尝试执行的次数
  • timeout: 超时时长的设定
  • path: 指明命令的搜索路径, 通常是列表的形式([‘’, ‘’, ‘’]). 在这里如果不指明, 每一个命令必须是绝对路径的.

我们说过有些命令是具有幂等性的, 例如:

1
2
3
4
5
6
7
exec {'/sbin/modprobe ext4':
user => root,
group => root,
refresh => "/sbin/modprobe -r ext4 && /sbin/modprobe ext4",
timeout => 5,
tries => 2
}

这个命令执行几遍结果都是一样的:

1
2
3
4
[root@master manifest]# lsmod | grep ext4
ext4 381065 2
jbd2 93284 1 ext4
mbcache 8193 1 ext4

但是, 像这样的命令就存在问题了:

1
2
3
4
exec {'/bin/echo Hello > /tmp/test':
user => root,
group => root
}

虽然第一次的执行是没有问题的, 但是第二次的执行将会破坏幂等性, 假设我们把Hello换成别的内容, 那么文件内的内容就会被被破坏.

这个时候, 为了保证文件不会被破坏, 我们就可以使用unless或者creates来控制命令执行, 修改之后的结果是这样的:

1
2
3
4
5
6
exec {'/bin/echo World > /tmp/test':
user => root,
group => root,
unless => '/usr/bin/test -e /tmp/test',
creates => '/tmp/test'
}

其实这里的unless和creates有一个就可以了, 他们都可以确保幂等性.

执行之后的结果就是这样的:

1
2
3
4
[root@master manifest]# puppet apply -v test4.pp
Notice: Compiled catalog for master in environment production in 0.07 seconds
Info: Applying configuration version '1510886222'
Notice: Finished catalog run in 0.08 seconds

根本就不会执行命令.

notify

这个资源可以说是最简单的一个资源了, 简单到只需要一行就可以定义结束, 他的参数一共其实就三个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@master manifest]# puppet describe notify

notify
======
Sends an arbitrary message to the agent run-time log.


Parameters
----------

- **message**
The message to be sent to the log.

- **name**
An arbitrary tag for your own reference; the name of the message.

- **withpath**
Whether to show the full object path. Defaults to false.
Valid values are `true`, `false`.

其中, message就是我们的nameVar, 这样的话我们定义一个资源就像这样:

1
notify {"Hello, there": }

执行的结果就像这样:

1
2
3
4
5
[root@master manifest]# puppet apply test5.pp
Notice: Compiled catalog for master in environment production in 0.06 seconds
Notice: Hello, there
Notice: /Stage[main]/Main/Notify[Hello, there]/message: defined 'message' as 'Hello, there'
Notice: Finished catalog run in 0.08 seconds

cron

见到名字就知道是定时任务的资源了, 来看一下常用的属性:

  • ensure: present, absent
  • command: 要执行的crontab任务
  • hour:
  • minute:
  • monthday:
  • weekday:
  • name:
  • user: 执行用户
  • environment: 执行的时候的环境变量

大体上就这些. 我们来实际写一个试试吧:

1
2
3
4
5
6
cron{'sync time':
command => "/usr/sbin/ntpdate edu.ntp.org.cn > /dev/null 2>&1",
user => "root",
ensure => present,
minute => "*/10"
}

效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@master manifest]# crontab -l
no crontab for root
[root@master manifest]# puppet apply test6.pp
Notice: Compiled catalog for master in environment production in 0.11 seconds
Notice: /Stage[main]/Main/Cron[sync time]/ensure: created
Notice: Finished catalog run in 0.73 seconds
[root@master manifest]# crontab -l
# HEADER: This file was autogenerated at Fri Nov 17 13:19:43 +0800 2017 by puppet.
# HEADER: While it can still be managed manually, it is definitely not recommended.
# HEADER: Note particularly that the comments starting with 'Puppet Name' should
# HEADER: not be deleted, as doing so could cause duplicate cron jobs.
# Puppet Name: sync time
*/10 * * * * /usr/sbin/ntpdate edu.ntp.org.cn > /dev/null 2>&1

如果要删除也是很简单的啦, 直接把ensure的值改成absent就行了.

package

这个是用来管理程序包的一个资源类型. 由于不同系统平台安装的方式不一样, 但是Puppet将目的和实现方式进行了分离, 这就方便了我们. 我们既可以直接指明安装的软件包是什么就行了.

他有这些参数:

  • configfiles: 有两个参数: keep和replace 它表明如果我们进行软件的覆盖安装的时候, 对于配置文件是怎么处理的.
  • ensure: 可以直接安装版本号, 或者跟上latest, present(installed), absent等.
  • name: 就是我们要安装的程序包的名字了
  • source: 包的来源, 可以是本地文件的路径, 也可以是URL

来看这样的例子:

1
2
3
4
5
6
7
8
9
package {"zsh":
ensure => installed
}

package {"kmod-e1000":
ensure => installed,
source => "/root/kmod-e1000-8.0.35-1.el6.elrepo.x86_64.rpm",
provider => rpm,
}

执行!

1
2
3
4
5
6
7
8
9
10
[root@master manifest]# puppet apply -v test7.pp
Notice: Compiled catalog for master in environment production in 0.43 seconds
Info: Applying configuration version '1510921085'
Notice: /Stage[main]/Main/Package[zsh]/ensure: created
Notice: Finished catalog run in 28.97 seconds
[root@master manifest]# modprobe e1000
[root@master manifest]# lsmod | grep e1000
e1000 165474 0
[root@master manifest]# rpm -q zsh
zsh-4.3.11-4.el6.centos.2.x86_64

这个例子表示, 我们也可以使用本地的RPM包, 但是要指明provider, 否则puppet会报错.

service

接下来再来说一些这个service吧, 主要负责服务的管理, 常用的属性有这些:

  • binary: 服务二进制程序的位置
  • enable: 是否开机自动启动, 对于Windows还有一个manual合法值(对于Linux不常见)
  • ensure: stopped(false), running(true)
  • hasrestart: 指明当前的启动脚本是否自携带restart选项
  • hasstatus: 于hasrestart类似, 不解释了
  • path: 和上面的exec一样, 指明搜索路径
  • restart, start, status, stop: 手动指明, 如果脚本自己不携带的话
  • pattern: 用来搜索服务相关的所有进程的模式字符串

接下来按照惯例, 写个小栗子吧:

1
2
3
4
5
6
7
8
9
10
11
package {'nginx':
ensure => installed
}

service {'nginx':
ensure => running,
enable => true,
hasstatus => true,
hasrestart => true,
restart => "service nginx reload"
}

执行的效果就像这样:

1
2
3
4
5
6
7
8
9
10
[root@master manifest]# puppet apply -v test8.pp
Notice: Compiled catalog for master in environment production in 8.88 seconds
Info: Applying configuration version '1510989337'
Notice: /Stage[main]/Main/Service[nginx]/ensure: ensure changed 'stopped' to 'running'
Info: /Stage[main]/Main/Service[nginx]: Unscheduling refresh on Service[nginx]
Notice: Finished catalog run in 30.72 seconds
[root@master manifest]# rpm -q nginx
nginx-1.10.2-1.el6.x86_64
[root@master manifest]# service nginx status
nginx (pid 8002) is running...

在前面我们说过一些特殊属性还记得不~ 我们来说一下metaparameters, 这个玩意可以来确保资源之间的执行次序, Puppet提供了四个元参数来定义资源之间的相关性, 为了说明资源就需要做资源的引用.

常见的资源引用就像这样写:

1
Type['title']

这里要注意一点: 资源引用的时候, 他的类型名必须要大写

而资源之间的相关性有这些:

  • before
  • require
  • notify
  • subscribe

这四个元参数就是我们用来定义资源相关性的, 我们回到第一个例子, 来重下个定义:

1
2
3
4
5
6
7
8
9
10
11
12
group { 'centos':
gid => 2000,
ensure => present
}

user { "centos":
gid => 2000,
uid => 2000,
shell => "/bin/sh",
home => "/home/centos",
ensure => present,
}

这里定义的存在一个严重的错误, 如果用户创建的时候 ,组不存在的话就会导致用户创建失败.

所以 这里我们需要制定一个规则, 就是在组创建之后才可以创建用户. 所以第一种方法, 就是说明在group的里面加上指明要在user前 就像这样:

1
2
3
4
5
group { 'centos':
gid => 2000,
ensure => present,
before => User['centos']
}

或者user里面加上在group之后的约束:

1
2
3
4
5
6
7
8
user { "centos":
gid => 2000,
uid => 2000,
shell => "/bin/sh",
home => "/home/centos",
ensure => present,
require => Group['centos']
}

当然最简单的方法就是:

1
2
3
4
5
6
7
8
9
10
11
12
group { 'centos':
gid => 2000,
ensure => present,
} ->

user { "centos":
gid => 2000,
uid => 2000,
shell => "/bin/sh",
home => "/home/centos",
ensure => present,
}

这样直接使用链式的方式来指明, 尤其在项目数量多的时候, 这样的链式方法更加清晰.

接下来就来应用一下通知和订阅, 还是使用Nginx来做演示, 我们先写一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package {"nginx":
ensure => installed
}

file {"/etc/nginx/nginx.conf":
ensure => present,
source => "/root/nginx.conf",
require => Package['nginx'],
notify => Service['nginx']
}

service {'nginx':
hasrestart => true,
hasstatus => true,
enable => true,
ensure => running,
require => [ Package['nginx'], File['/etc/nginx/nginx.conf'] ],
restart => "service nginx reload"
}

接着我们看一下当前的Nginx工作进程:

1
2
3
4
5
[root@master manifest]# ps aux | grep nginx
root 8002 0.0 0.0 108960 1860 ? Ss 15:16 0:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx 8005 0.0 0.0 109384 2708 ? S 15:16 0:00 nginx: worker process
nginx 8006 0.0 0.0 109384 2704 ? S 15:16 0:07 nginx: worker process
root 10370 0.0 0.0 103324 864 pts/0 S+ 23:33 0:00 grep nginx

接着我们修改一下/root/nginx.conf, 把里面的worker_processes修改成4, 接着再次执行这个puppet:

1
2
3
4
5
[root@master manifest]# puppet apply test9.pp
Notice: Compiled catalog for master in environment production in 0.55 seconds
Notice: /Stage[main]/Main/File[/etc/nginx/nginx.conf]/content: content changed '{md5}1510a037b9fb468daa9fff6d3b5bdd90' to '{md5}9a182199db0446eadfd93810907bbf09'
Notice: /Stage[main]/Main/Service[nginx]: Triggered 'refresh' from 1 events
Notice: Finished catalog run in 6.94 seconds

可以清晰的看到说明, 文件的md5值发生了改变, 再次查看一下work_processes:

1
2
3
4
5
6
7
[root@master manifest]# ps aux | grep nginx
root 8002 0.0 0.1 109620 4656 ? Ss 15:16 0:01 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx 10602 0.0 0.0 110072 3400 ? S 23:33 0:00 nginx: worker process
nginx 10603 0.0 0.0 110072 3400 ? S 23:33 0:00 nginx: worker process
nginx 10604 0.0 0.0 110072 3400 ? S 23:33 0:00 nginx: worker process
nginx 10605 0.0 0.0 110072 3400 ? S 23:33 0:00 nginx: worker process
root 10607 0.0 0.0 103324 868 pts/0 S+ 23:34 0:00 grep nginx

生效了~

刚刚说过了可以使用->来链式的说明依赖关系, 我们也可以使用~>来链式的说明通知关系.

Puppet的其他语法

变量

首先由于Puppet使用Ruby, 所以基本上变量都遵循基本规则, 他的变量都使用$开头. 接着赋值符号是=. 并且, 在Puppet支持的所有变量类型中, 除了正则表达式其他所有的都可以直接赋值.

赋值方法同样支持=+=两种.

接下来我们再来说一下Puppet的作用域.

作用域

Puppet的变量之间是可以进行隔离的, 这得益于他的作用域, 但是要先说明的是, 作用域不能隔离资源引用, 在全代码范围之内都是可以引用的.

作用域(Scope)分成全局作用域和节点作用域, 在Node Scope中, 我们将来会定义一些类, 甚至还可以进行类之间的嵌套. 即父类和子类.

因此在引用变量的时候就有两种形式的: 一种是相对路径引用, 一种是绝对路径引用 (例如: $::scope::scope::var )

接下来我们就来看一下puppet支持的变量类型有哪些吧.

变量类型

  • 字符型: 非结构化的文本字符串, 可以加上引号. 同样的, 单引号不替换变量, 双引号替换变量, 并且同样支持转义符

  • 数值型: 支持整数和浮点数, 只有在数值的上下文中在把数值当成是数值型, 一般都当做字符型.

  • 数组: 使用逗号分隔, 支持负数索引

  • 布尔: 同样也有其他数据类型可以转换成为布尔类型

  • undef: undefined的意思

  • hash: 其实hash类型就是键值对类型呀

  • 正则表达式( 非标准变量类型 )

    • 这个正则表达式的表现形式特别诡异, 首先他不能赋值给变量并且只能在一些特定的位置出现, 他的一个形式是这样的:

    • 常见的option有这些: i: 忽略字符大小写, m: 把.当成是换行符, x: 忽略模式中的空白和注释

    • (?[-]<[DIS|EN]ABLE-OPTION>:<SUBPATTERN>)
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44


      除了这些自定义的变量, Puppet还引入了很多factar变量和内置变量, 对于factor变量, 我们之前在说Ansible的时候就提到过, 这里, 我们可以使用下面的命令查看:

      ```bash
      [root@master ~]# facter -p
      filesystems => ext4,iso9660
      fqdn => master
      gid => root
      hardwareisa => x86_64
      hardwaremodel => x86_64
      hostname => master
      id => root
      interfaces => eth0,lo
      ipaddress => 59.68.29.77
      ipaddress_eth0 => 59.68.29.77
      ipaddress_lo => 127.0.0.1
      is_virtual => true
      kernel => Linux
      kernelmajversion => 2.6
      kernelrelease => 2.6.32-696.13.2.el6.x86_64
      kernelversion => 2.6.32
      ...
      macaddress => 00:50:56:AF:15:3B
      macaddress_eth0 => 00:50:56:AF:15:3B
      manufacturer => VMware, Inc.
      memoryfree => 1.05 GB
      memoryfree_mb => 1074.68
      memorysize => 3.74 GB
      memorysize_mb => 3832.41
      mtu_eth0 => 1500
      mtu_lo => 65536
      netmask => 255.255.255.0
      netmask_eth0 => 255.255.255.0
      netmask_lo => 255.0.0.0
      network_eth0 => 59.68.29.0
      network_lo => 127.0.0.0
      operatingsystem => CentOS
      operatingsystemmajrelease => 6
      operatingsystemrelease => 6.9
      ...
      selinux => true
      ....
      virtual => vmware

等等一大堆系统信息. ( 这个命令似乎已经过时了, 推荐使用puppet facts ) 上面这些都可以直接引用. 还可以引用一些内置变量例如:

1
2
3
4
5
6
7
8
## 客户端
$clientversion
$clientcert
## 服务端
$servername
$serverip
$serverversion
$module_name

条件判断

基本的条件判断puppet也都支持, 例如if, case, selector, unless

举个例子:

1
2
3
4
5
if $processorcount > 1 {
notice("Yes!")
} else {
notice("Nooo!")
}

执行结果显然是:

1
2
3
[root@master manifest]# puppet apply test10.pp
Notice: Scope(Class[main]): Yes!
...(omitted)

接下来我们再来试试正则表达加上条件判断:

1
2
3
4
5
if $operatingsystem =~ /^(?i-mx:(centos|redhat))/ {
notice("welcome to $1 server!")
} else {
notice("I don't know you.")
}

执行:

1
2
[root@master manifest]# puppet apply test11.pp
Notice: Scope(Class[main]): welcome to CentOS server!

接着一个case的例子:

1
2
3
4
5
case $operatingsystem {
/(?i-mx:(redhat|ubuntu))/: { notice("welcome to $1") }
"centos","CentOS","Centos": { notice("Centos") }
default: { notice("I dont't know.") }
}

结果也很明显:

1
2
3
4
[root@master manifest]# puppet apply test12.pp
Notice: Scope(Class[main]): Centos
Notice: Compiled catalog for master in environment production in 0.07 seconds
Notice: Finished catalog run in 0.06 seconds

接下来再来说一下Selector, 什么玩意这是? 其实就是一个根据情况赋值变量, 看个例子就知道了:

1
2
3
4
$webserver = $operatingsystem ? {
/(?i-mx:(ubuntu|debian))/ => 'apache2',
/(?i-mx:(redhat|centos|fedora))/ => 'httpd'
}

根据操作系统选择不同的值来赋给webserver.

我们可以通过创建可继承的来在puppet的全局进行调用, 在这里, 类其实就可以理解成是一段被命名的代码块.

我们结合一个例子来介绍它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class apache {
package {"httpd":
ensure => installed
}
file {"httpd.conf":
ensure => file,
path => "/etc/httpd/conf/httpd.conf",
require => Package['httpd']
}
service {"httpd":
require => Package['httpd'],
subscribe => File['httpd.conf'],
ensure => running
}
}

首先 类名必须使用小写字母开头, 可以包含小写字母, 数字, 下划线. 前面说过Scope的概念, 每次我们声明一个类, 就会引入一个新的作用域, 这个时候, 如果是要引用这个作用域中的变量的话, 就需要使用完全限定名称.

现在直接执行的话, 不会有任何效果, 原因就是现在只是定义了类, 如果想要让它起到效果的话就需要声明他,声明一个类的方式主要有四种:

  • 使用include来声明一个类
  • 像定义资源一样定义一个类(不加任何参数)来声明他
  • 使用require来声明一个类

我们来一个一个看, 首先是include

我们再刚刚定义类的那个文件的最后加上一句:

1
include apache

接着正常执行试试:

1
2
3
4
5
6
7
[root@master manifest]# puppet apply -v test13.pp
Notice: Compiled catalog for master in environment production in 5.15 seconds
Info: Applying configuration version '1511097112'
Notice: /Stage[main]/Apache/Package[httpd]/ensure: created
Notice: /Stage[main]/Apache/Service[httpd]/ensure: ensure changed 'stopped' to 'running'
Info: /Stage[main]/Apache/Service[httpd]: Unscheduling refresh on Service[httpd]
Notice: Finished catalog run in 188.24 seconds

类还可以传递参数, 就像函数一样. 没想到吧, 我们再来举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class nginx($server='nginx') {
package {$server:
ensure => installed
}

file {"/etc/nginx/nginx.conf":
ensure => present,
source => "/root/nginx.conf",
require => Package[$server],
notify => Service['nginx']
}

service {'nginx':
hasrestart => true,
hasstatus => true,
enable => true,
ensure => running,
require => [ Package[$server], File['/etc/nginx/nginx.conf'] ],
restart => "service nginx reload"
}
}

这里, 我们在class的开头定义了一个$server参数, 并且给了他一个默认值. 那么怎么调用它, 给他传参数呢? 就像这样:

1
2
3
class {"nginx":
server => 'tengine'
}

这样就可以了. ( 不过当前好像没有收录tengine的yum源, 所以肯定会报错的啦, 你可以把tengine改成nginx跑一遍.. )

使用nginx做传入参数的执行结果参考:

1
2
3
4
5
6
7
8
9
10
11
Notice: Compiled catalog for master in environment production in 0.61 seconds
Info: Applying configuration version '1511099152'
Notice: /Stage[main]/Nginx/Package[nginx]/ensure: created
Info: Computing checksum on file /etc/nginx/nginx.conf
Info: FileBucket got a duplicate file {md5}1510a037b9fb468daa9fff6d3b5bdd90
Info: /Stage[main]/Nginx/File[/etc/nginx/nginx.conf]: Filebucketed /etc/nginx/nginx.conf to puppet with sum 1510a037b9fb468daa9fff6d3b5bdd90
Notice: /Stage[main]/Nginx/File[/etc/nginx/nginx.conf]/content: content changed '{md5}1510a037b9fb468daa9fff6d3b5bdd90' to '{md5}1cf93649e4d6f892a34c08cfb26d6d6e'
Info: /Stage[main]/Nginx/File[/etc/nginx/nginx.conf]: Scheduling refresh of Service[nginx]
Notice: /Stage[main]/Nginx/Service[nginx]/ensure: ensure changed 'stopped' to 'running'
Info: /Stage[main]/Nginx/Service[nginx]: Unscheduling refresh on Service[nginx]
Notice: Finished catalog run in 14.77 seconds

子类

类之间是支持继承的, 而定义子类的方式像这样:

1
2
3
4
5
6
7
class base_class {
...code...
}

class base_class::class_name inherits base_class {
...code...
}

这里说明一下, 定义子类的名字其实没有必要些之前的base_class::这些的. 另外, 在我们定义子类的时候, 父类会被先自动的首先声明.

子类有什么用处呢, 还是以我们的Nginx来举个例子, 可能在某些场景下, 我们发Nginx是用来做代理的, 有的场景下, 我们的Nginx是用来做HTTP服务器的, 等等, 但是他们都有一个共同点, 那就是都需要安装Nginx程序包, 都需运行Nginx服务, 也就是说他们之间的不同点就在于配置文件的差异, 所以, 我们完全就可以将package和service当做父类, 为这个父类提供不同的子类(file)就可以节省代码, 实现重用. 来看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class nginx {
package {'nginx':
ensure => installed
} ->

service {'nginx':
restart => 'service nginx reload',
hasstatus => true,
hasrestart => true,
ensure => running,
enable => true
}
}

class nginx::http inherits nginx {
file { '/etc/nginx/nginx.conf':
ensure => file,
notify => Service['nginx'],
source => "/root/nginx/nginx_http.conf"
}
}

class nginx::proxy inherits nginx {
file {'/etc/nginx/nginx.conf':
ensure => file,
notify => Service['nginx'],
source => "/root/nginx/nginx_proxy.conf"
}
}

这就是像刚刚所说的, 重用安装包和服务的Nginx在不同场景下的应用.

通过子类, 我们还可以使用更高级的特性 — 覆盖父类的资源属性.

1
2
3
4
5
6
7
8
9
10
class nginx::http inherits nginx {
Package['nginx'] {
name => 'tengine'
}
file { '/etc/nginx/nginx.conf':
ensure => file,
notify => Service['nginx'],
source => "/root/nginx/nginx_http.conf"
}
}

这里, 我们先引用父类的Package资源, 接着修改nameVar, 接着引入这个子类执行它, 可以从执行结果看出, 确实已经在尝试安装tengine了:

1
2
3
4
5
6
7
otice: Compiled catalog for master in environment production in 0.58 seconds
Info: Applying configuration version '1511163767'
Error: Execution of '/usr/bin/yum -d 0 -e 0 -y list tengine' returned 1: Error: No matching Packages to list
Error: /Stage[main]/Nginx/Package[nginx]/ensure: change from absent to present failed: Execution of '/usr/bin/yum -d 0 -e 0 -y list tengine' returned 1: Error: No matching Packages to list
Notice: /Stage[main]/Nginx/Service[nginx]: Dependency Package[nginx] has failures: true
Warning: /Stage[main]/Nginx/Service[nginx]: Skipping because of failed dependencies
Notice: Finished catalog run in 77.87 seconds

同样的, 我们还可以追加新值, 使用+>来追加.

模板

和之前的Ansible所使用的Jinja2类似, Puppet使用的是Ruby的模板语言, 叫做ERB, 即Embedded RuBy. 其实和JSP以及ejs都十分相像.

1
2
3
4
5
6
<% Ruby Expression %>
<% #Comment %>
<%% == <%
%%> == %>
<%- Ruby Code %> 忽略空白字符
<% Ruby Code -%> 忽略空白行

当然在这里面也是可以引用Puppet的变量的, 但是此时就需要使用@字符开头

另外, 基本的循环和迭代语法是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 循环

<% if CONDTION -%>
some text
<% end %>

<% if CONDITION -%>
some text
<% else %>
some text
<% end %>

# 迭代

<% @ArrayName.echo do | Var_name | -%>
some text with <%= Var_name %>
<% end %>

我们使用之前的Nginx配置文件来实践一下这个:

1
2
user nginx;
worker_processes <% @processorcount %>;

接下来我们的puppet清单也要修改一下:

1
2
3
4
5
6
7
8
9
class nginx::proxy inherits nginx {
file {'/etc/nginx/nginx.conf':
ensure => file,
notify => Service['nginx'],
content => template('/root/nginx/nginx_proxy.conf')
}
}

include nginx::proxy

这里我们使用的tempalate函数会将目标文件处理成文本流文件, 所以我们要把source修改成content.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Notice: Compiled catalog for master in environment production in 0.60 seconds
Info: Applying configuration version '1511166414'
Info: Computing checksum on file /etc/nginx/nginx.conf
Info: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]: Filebucketed /etc/nginx/nginx.conf to puppet with sum 90cc6772f4dd26a421ce78358801ec95
Notice: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]/content: content changed '{md5}90cc6772f4dd26a421ce78358801ec95' to '{md5}9d140bc8d6be6be51c471322210b9393'
Info: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]: Scheduling refresh of Service[nginx]
Notice: /Stage[main]/Nginx/Service[nginx]/ensure: ensure changed 'stopped' to 'running'
Info: /Stage[main]/Nginx/Service[nginx]: Unscheduling refresh on Service[nginx]
Notice: Finished catalog run in 5.16 seconds
[root@master manifest]# head /etc/nginx/nginx.conf
# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes 2;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

# Load dynamic modules. See /usr/share/nginx/README.dynamic.

模块

最后再来说下模块的概念. 这里和Ansible和Role几乎一样的概念, 就是有层次的组织起来需要的资源清单文件, 并且分成多个文件进行模块式的调用. 具体的文件组织是这样的:

1
2
3
4
5
6
7
8
module_name/
manifest/
init.pp: 至少包含一个和模块同名的类
files: 静态文件 (puppet:///modules/module_name/file_name)
templates: 模板文件路径 (module_name/template_name)
libs: 插件目录
tests: 示例和帮助
spec: 插件的tests目录

Puppet自己有一个命令可以用来管理模块:

1
2
3
4
5
6
7
8
9
[root@master manifest]# puppet help module

USAGE: puppet module <action> [--environment production ]
[--modulepath $basemodulepath ]


This subcommand can find, install, and manage modules from the Puppet Forge,
a repository of user-contributed Puppet code. It can also generate empty
modules, and prepare locally developed modules for release on the Forge.

他自己还有一个Forge, 这意味着我们可以上传自己模块和下载别人写好的模块.

创建一个模块是十分容易的:

1
2
3
4
5
6
7
8
9
10
11
12
[root@master manifest]# mkdir -pv /etc/puppet/modules/nginx/{manifests,files,templates,tests,lib,spec}
mkdir: created directory `/etc/puppet/modules/nginx'
mkdir: created directory `/etc/puppet/modules/nginx/manifests'
mkdir: created directory `/etc/puppet/modules/nginx/files'
mkdir: created directory `/etc/puppet/modules/nginx/templates'
mkdir: created directory `/etc/puppet/modules/nginx/tests'
mkdir: created directory `/etc/puppet/modules/nginx/lib'
mkdir: created directory `/etc/puppet/modules/nginx/spec'
[root@master manifest]# puppet module list
/etc/puppet/modules
└── nginx (???)
/usr/share/puppet/modules (no modules installed)

接下来我们就可以把之前写的清单文件扔到固定的位置去了:

1
2
3
[root@master manifest]# cp test16.pp /etc/puppet/modules/nginx/manifests/init.pp
[root@master manifest]# cp /root/nginx/nginx_http.conf /etc/puppet/modules/nginx/files/
[root@master manifest]# cp /root/nginx/nginx_proxy.conf /etc/puppet/modules/nginx/templates/nginx_proxy.conf.erb

当然路径什么的是需要修改的, 并且再把声明语句去除.

此时我们就可以这么执行: ( –noop表示不执行 )

1
2
3
4
5
6
7
8
9
10
11
12
[root@master manifests]# puppet apply --noop -v -e "include nginx::proxy"
Notice: Compiled catalog for master in environment production in 0.59 seconds
Info: Applying configuration version '1511167958'
Notice: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]/ensure: current_value absent, should be file (noop)
Info: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]: Scheduling refresh of Service[nginx]
Notice: Class[Nginx::Proxy]: Would have triggered 'refresh' from 1 events
Notice: /Stage[main]/Nginx/Package[nginx]/ensure: current_value absent, should be present (noop)
Notice: /Stage[main]/Nginx/Service[nginx]/ensure: current_value stopped, should be running (noop)
Info: /Stage[main]/Nginx/Service[nginx]: Unscheduling refresh on Service[nginx]
Notice: Class[Nginx]: Would have triggered 'refresh' from 2 events
Notice: Stage[main]: Would have triggered 'refresh' from 2 events
Notice: Finished catalog run in 1.32 seconds

使用-e参数加上puppet代码语句来执行, 可以看到模块是生效的.

Puppet的master-agent模型实现

我们之前说过的master-agent模型的相关信息, agent会定期的去向服务器端去发送catalog的请求,该请求中会携带自己的fact信息和自己的节点名称 服务端收到请求之后会去辨别客户端的身份. 在这个过程中, agent和master之间会通过ssl进行双向的认证. 这里, 我们使用的是节点名称而不是IP地址. 这就是说, 在我们的Puppet主从模型中, DNS服务是一个至关重要的部分.

默认的请求间隔时间是30min. master收到之后会去查找对应的清单并去编译它, 最后将生成的catalog发送给agent. 对于master端, 他工作在8140/tcp端口

我们再master节点上安装puppet-server程序包, 来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@master ~]# rpm -ql puppet-server
/etc/puppet/environments
/etc/puppet/environments/example_env
/etc/puppet/environments/example_env/README.environment
/etc/puppet/environments/example_env/manifests
/etc/puppet/environments/example_env/modules
/etc/puppet/fileserver.conf
/etc/puppet/manifests
/etc/rc.d/init.d/puppetmaster
/etc/rc.d/init.d/puppetqueue
/etc/sysconfig/puppetmaster
/usr/share/man/man8/puppet-ca.8.gz
/usr/share/man/man8/puppet-master.8.gz

安装生成的文件很简单, 其实主要是两个配置文件和daemon.

按照惯例, 我们现在应该去看一下配置文件了, 首先就是/etc/puppet/puppet.conf这个主配置文件, 里面主要是两个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[main]
# The Puppet log directory.
# The default value is '$vardir/log'.
logdir = /var/log/puppet

# Where Puppet PID files are kept.
# The default value is '$vardir/run'.
rundir = /var/run/puppet

# Where SSL certificates are kept.
# The default value is '$confdir/ssl'.
ssldir = $vardir/ssl

[agent]
# The file in which puppetd stores a list of the classes
# associated with the retrieved configuratiion. Can be loaded in
# the separate ``puppet`` executable using the ``--loadclasses``
# option.
# The default value is '$confdir/classes.txt'.
classfile = $vardir/classes.txt

# Where puppetd caches the local configuration. An
# extension indicating the cache format is added automatically.
# The default value is '$confdir/localconfig'.
localconfig = $vardir/localconfig

其中main中的设定是用于全局的, 而agent显然就只是用于agent端的. 显然这是一个ini风格的配置, 而且还可以引用之前定义的值.

关于配置, 我们可以使用puppet config print来获取所有的当前配置, 并可以通过puppet config set来设置.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@master ~]# puppet config print
dbname = puppet
thin_storeconfigs = false
disable_per_environment_manifest = false
max_warnings = 10
ignoremissingtypes = false
cfacter = false
ssl_server_ca_auth =
yamldir = /var/lib/puppet/yaml
rundir = /var/run/puppet
genmanifest = false
smtpserver = none
http_keepalive_timeout = 4
templatedir = /var/lib/puppet/templates
...(omitted)

另外, 我们还有puppet master和puppet agent两个子命令分别来实现master和agent的运行, 其实这里主要就是一些前后台的启动以及一些启动选项.

关于上面的配置文件, 我们是可以自动的生成一份出来的, 使用的就是master的—genconfig选项 对于agent端, 我们就需要使用agent的这个选项了. 但是注意: **生成新的配置之前不能删除或者移动原有的/etc/puppet/puppet.conf **, 而且: 诡异的是, 生成的配置文件可能包含该版本不兼容的选项, 以及不兼容的默认值

如果想要获得Puppet详尽的配置文档, 可以使用puppet doc. 在这里文档是分段的, 我们通过-r 加上reference_name就可以了, name们可以通过puppet doc —list获取.

现在我们就来试试运行puppet master吧: ( 首先我们应该确保节点之间的名称解析没有问题 )

1
2
3
4
5
6
7
8
9
10
11
[root@master ~]# puppet master -v --no-daemonize
Info: Creating a new SSL key for master
Info: csr_attributes file loading from /etc/puppet/csr_attributes.yaml
Info: Creating a new SSL certificate request for master
Info: Certificate Request fingerprint (SHA256): C2:DE:BF:FC:1E:40:6D:34:AA:D1:D4:57:1B:8F:AF:93:09:BE:31:D7:4A:AF:65:97:09:A8:ED:48:FC:BC:52:FA
Notice: master has a waiting certificate request
Notice: Signed certificate request for master
Notice: Removing file Puppet::SSL::CertificateRequest master at '/var/lib/puppet/ssl/ca/requests/master.pem'
Notice: Removing file Puppet::SSL::CertificateRequest master at '/var/lib/puppet/ssl/certificate_requests/master.pem'
Notice: Starting Puppet master version 3.8.7
^CNotice: Caught INT; exiting

我们可以看到, 当前的主机已经自己成为了一个CA, 自己生成证书请求并且签署了自己的证书.

接下来我们还是把它当做一个后台服务跑起来.

1
2
3
4
5
[root@master ~]# service puppetmaster start
Starting puppetmaster: [ OK ]
[root@master ~]# ss -tnl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 5 *:8140 *:*

已经监听在了自己的端口上.

现在我们就可以尝试启动一个客户端试试: ( 这一次先不正式启动, 因此加入test和noop的dry-run参数 )

1
2
3
4
5
6
7
8
[root@agent ~]# puppet agent --verbose --noop --no-daemonize --test --server master
Info: Creating a new SSL key for agent
Info: Caching certificate for ca
Info: csr_attributes file loading from /etc/puppet/csr_attributes.yaml
Info: Creating a new SSL certificate request for agent
Info: Certificate Request fingerprint (SHA256): 30:9E:60:EA:2B:71:9D:27:11:05:57:C0:37:5E:A4:90:C5:4F:BE:96:73:1F:2E:8A:1E:F2:2F:6B:0D:AA:89:99
Info: Caching certificate for ca
Exiting; no certificate found and waitforcert is disabled

脱坑指南: 如果你的Puppet客户端出现No route to host - connect(2)之类的报错, 那么你可以先检查一下Puppet服务端的iptables是否打开了8140TCP的ACCEPT策略

我们可以从输出信息看到, 客户端生成了一个证书签署请求发送给了master端. 现在我们让客户端跑起来然后去master端看看怎么签署证书吧. 在服务端, 我们使用个cert子命令来进行证书的管理, 包括签署, 撤销, 验证, 清理等等.

1
2
[root@master ~]# puppet cert list
"agent" (SHA256) 30:9E:60:EA:2B:71:9D:27:11:05:57:C0:37:5E:A4:90:C5:4F:BE:96:73:1F:2E:8A:1E:F2:2F:6B:0D:AA:89:99

可以看到刚刚我们发出的证书请求. 接着就来把它签了吧:

1
2
3
[root@master ~]# puppet cert sign --all
Notice: Signed certificate request for agent
Notice: Removing file Puppet::SSL::CertificateRequest agent at '/var/lib/puppet/ssl/ca/requests/agent.pem'

接着我们再启动一次客户端:

1
2
3
4
5
6
7
8
9
10
[root@agent ~]# puppet agent --verbose --no-daemonize --server master
Info: Caching certificate for agent
Info: Caching certificate_revocation_list for ca
Info: Caching certificate for agent
Notice: Starting Puppet client version 3.8.7
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for agent
Info: Applying configuration version '1511263299'
Notice: Finished catalog run in 0.07 seconds

得到了证书也是就可以通信了.

接着我们之前说过, 在服务端我们需要配置需要用到的模块, 还需要一个站点清单, 这个清单文件在哪里呢?

1
2
3
4
5
[root@master ~]# ls /etc/puppet/
auth.conf environments fileserver.conf manifests modules puppet.conf
[root@master ~]# cd /etc/puppet/manifests/
[root@master manifests]# ls
[root@master manifests]#

我们需要在这里面创建一个叫做site.pp的文件. 另外, 我们之前显示过当前安装的模块有哪些, 这个搜索路径是怎么定的呢?

1
2
3
4
5
6
[root@master manifests]# puppet module list
/etc/puppet/modules
└── nginx (???)
/usr/share/puppet/modules (no modules installed)
[root@master manifests]# puppet config print modulepath
/etc/puppet/modules:/usr/share/puppet/modules

接着回到我们的site.pp, 这个文件怎么写呢 其实很简单的, 来看一个示例:

1
2
3
node "agent" {
include nginx::proxy
}

十分简单 使用node加上你的node_name 然后括号里面还是puppet_code就可以了. 这个时候我们再来尝试执行一次客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@agent ~]# puppet agent --verbose --no-daemonize --server master --noop
Notice: Starting Puppet client version 3.8.7
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for agent
Info: Applying configuration version '1511265661'
Notice: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]/ensure: current_value absent, should be file (noop)
Info: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]: Scheduling refresh of Service[nginx]
Notice: Class[Nginx::Proxy]: Would have triggered 'refresh' from 1 events
Notice: /Stage[main]/Nginx/Package[nginx]/ensure: current_value absent, should be present (noop)
Notice: /Stage[main]/Nginx/Service[nginx]/ensure: current_value stopped, should be running (noop)
Info: /Stage[main]/Nginx/Service[nginx]: Unscheduling refresh on Service[nginx]
Notice: Class[Nginx]: Would have triggered 'refresh' from 2 events
Notice: Stage[main]: Would have triggered 'refresh' from 2 events
Notice: Finished catalog run in 16.52 seconds

看, 已经起到效果了. 接下来我们就直接应用一下看看吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@agent ~]# puppet agent --verbose --no-daemonize --server master
Notice: Starting Puppet client version 3.8.7
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for agent
Info: Applying configuration version '1511266440'
Notice: /Stage[main]/Nginx/Package[nginx]/ensure: created
Info: Computing checksum on file /etc/nginx/nginx.conf
Info: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]: Filebucketed /etc/nginx/nginx.conf to puppet with sum 1510a037b9fb468daa9fff6d3b5bdd90
Notice: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]/content: content changed '{md5}1510a037b9fb468daa9fff6d3b5bdd90' to '{md5}9d140bc8d6be6be51c471322210b9393'
Info: /Stage[main]/Nginx::Proxy/File[/etc/nginx/nginx.conf]: Scheduling refresh of Service[nginx]
Notice: /Stage[main]/Nginx/Service[nginx]/ensure: ensure changed 'stopped' to 'running'
Info: /Stage[main]/Nginx/Service[nginx]: Unscheduling refresh on Service[nginx]
Notice: Finished catalog run in 146.49 seconds

但是很多时候, 我们的很多主机都需要同样的配置, 有没有什么办法把他们写在一起呢? 如果是功能相近的node名字, 我们可以这么写:

1
2
3
node /web\d*\.bili\.com/ {
... puppet code ...
}

这里我们稍微补充一个主机的命名规范:

角色 - 运营商 - 机房名 - IP.DOMAIN.LTD

也就是形如这样的: web1-mobile-xz-1.1.1.1.baidu.com

像是类使用一样, 我们的节点也是可以使用继承的方式来实现的:

1
2
3
4
5
6
7
node basenode {
include ntp
}

node web1-mobile-xz-1.1.1.1.baidu.com {
include nginx::proxy
}

这样就可以实现定义一个基节点来实现功能. 除此之外, 我们还可以在site.pp文件中进行分段的管理, 例如这样的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
/etc/puppet/manifests/
site.pp
import "webservers/*.pp"
...
webservers/
1.pp
2.pp
cacheservers/
1.pp
2.pp
appservers/
1.pp
2.pp

这样就可以实现更加清晰的管理层次.

现在, 我们仍然面临两个问题:

  • 主机名解析
  • 如何向不同环境节点分发puppet agent

关于DNS, 我们可以搭建内网动态改变的弹性DNS服务, 这个就不说了. 现在我们主要来解决第二个问题, 由于我们可能会有多个环境, ( 例如: 我们可能会有生产环境, 测试环境, 等等 ) 这个时候他们就需要不同的配置文件, 其实我们的Puppet已经有这方面的设定了, 我们来看一下:

1
2
3
4
5
6
7
8
9
10
[root@master puppet]# ls
auth.conf environments fileserver.conf manifests modules puppet.conf
[root@master puppet]# tree environments/
environments/
└── example_env
├── manifests
├── modules
└── README.environment

3 directories, 1 file

在我们的Puppet的目录下, 我们可以看到他已经能够给了一个example环境, 这样我们只需要在puppet的配置文件中加上特定的环境配置段就可以了, 就像这样:

1
2
3
4
5
6
7
8
9
[production]
manifest = /etc/puppet/environments/production/manifests/site.pp
modulepath = /etc/puppet/environments/production/modules/
fileserverconfig = /etc/puppet/fileserver.conf
[testing]
manifest = /etc/puppet/environments/testing/manifests/site.pp
modulepath = /etc/puppet/environments/testing/modules/
fileserverconfig = /etc/puppet/fileserver.conf
[...]

但是首先, 我们需要在前面加上声明, 声明master端支持哪些环境才行, 就像这样:

1
2
[master]
environment = production, testing, ...

以上的配置是master端的, 对于我们的agent端, 只需要指定环境就行了, like this:

1
2
[agent]
environment = tesing

指定自己的环境就行了. 而我们默认的agent的环境配置就是生产环境, 可以通过下面的命令获得:

1
2
3
4
[root@agent ~]# puppet config print environment
production
[root@agent ~]# puppet agent --configprint environment
production

接下来我们再来说一下fileserver.conf这个配置文件, 也就是puppet的文件服务器. 这个配置文件和puppet.conf以及auth.conf一起发挥作用, 目的其实就是对puppet的文件访问做授权的, 也就是一些安全配置, 使得agent能够或者不能访问某些文件.

这个文件主要有两个组成部分: MOUNT_POINTPERMISSIONS. 通过观察默认内容和注释 也能搞清楚个大概.

典型的配置就是:

1
2
3
4
5
[label]
path /PATH/TO/SOMEWHERE
allow XXXX
allow_ip XXXX
deny all

如果是想要实现先认证再授权的话, 就像permission中定义的那样, 加上auth yes的flag.

auth.conf就是我们的认证配置文件, 其实就是为了Puppet提供ACL功能, 主要是应用于puppet的Restful的接口.不过一般情况下, 我们没有必要去修改他.

说到Puppet的Restful接口, 我们来看看是什么样的:

1
https://master_ip:8140/{environment}/{resource}/{key}

除了这两个典型文件, 还有这样的配置:

  • namespaceauth.conf 控制命名空间的访问法则

    • puppet中的命名空间有这些: fileserver, puppetmaster, puppetrunner, puppetreports, resource
  • autosign.conf 证书自动签署

    • 直接在里面写上域名集合就行了, 例如: *.yaoxuannn.com

    我们直接来试试吧, 创建一个autosign.conf:

1
*.newthread.com

对了, 这个地方我把连个节点的名字改了一下, 都加上了后缀newthread.com. 所以你的hosts文件和原来的节点分发以及帧数都要进行修改或者清除

这个时候我们再次启动客户端, 可以看到他直接就执行了, 因为证书被自动签署了, 也可以在master端使用cert子命令进行观察.

kick模式

尽管我们说过Puppet的工作模式是客户端到服务端进行抓取. 但是在一些情况下我们也需要服务端能够主动的将catalog给agent节点.

这里注意, 尽管看起来和Ansible的push很相像, 但事实上还不是push, 实际上是puppet master向所有的节点要求你们快来pull 这样的. 也就是说实际上只不过是缩短30min的等待时间罢了.

这个功能要求客户端监听在一个套接字上, 当收到这样的kick信息时才可以来处理. 这个特殊的端口就是8139端口. 而且默认这个功能是没有启用的. 如果想要获得这个kick效果 我们还需要去配置agent.

在Puppet的文档只写的很详细了, 我们就根据他说的来做:

首先, 我们查看一下当前Puppet agent的监听配置:

1
2
[root@agent.newthread.com puppet]# puppet config print listen
false

接着, 我们修改他的配置文件, 增加listen = true:

1
2
[root@agent.newthread.com puppet]# tail -1 /etc/puppet/puppet.conf
listen = true

重启服务再检查:

1
2
3
4
[root@agent.newthread.com puppet]# netstat -antp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8139 0.0.0.0:* LISTEN 29464/ruby

就监听在8139了, 然后我们打开防火墙的8139入站访问.

接着在agent增加对其run的访问, 在auth.conf中增加:

1
2
3
path /run
method save
allow master.newthread.com

接着编辑或者新建namespaceauth.conf:

1
2
[puppetrunner]
allow *.newthread.com

接着我们再master端执行:

1
2
3
4
5
6
7
8
root@master.newthread.com ~]# puppet kick agent.newthread.com
Warning: Puppet kick is deprecated. See http://links.puppetlabs.com/puppet-kick-deprecation
Warning: Failed to load ruby LDAP library. LDAP functionality will not be available
Triggering agent.newthread.com
Getting status
status is success
agent.newthread.com finished with exit code 0
Finished

OK了.

不过需要说明的是, 正如显示的警告, 这个功能要被废弃掉.

dashboard

最后我们来说一下puppet的dashboard. 也就是他的报告展示功能. 由于使用率不是很高 所以就简单的过一遍好了.

首先安装数据库mariadb, 略. 之前说过很多次了.

接着安装必要的软件包, 主要是ruby的:

1
2
yum install rubygem-rake ruby-mysql
yum install http://yum.puppet.com/el/6/products/x86_64/puppet-dashboard-1.2.9-1.el6.noarch.rpm