为什么要开发puppet
系统管理员都喜欢自己写点小工具来让自己的工作完成的更快或者更好, 不管是在大企业管理大量的服务器还是只管理两三台机器. 但是很少人会把他们的工具发布出来. 也就是是说极少有工具能被重用,或者说很多工具就只能在所在的组织内部有用.拷贝给别的组织,他们也用不上. 也就是说,每个系统管理员,在一个新的公司,都会另起炉灶开发一套基于ssh,for循环的"系统"来帮助自己完成系统管理任务.
开发puppet是为了让系统管理员社区可以相互交流和共享成熟的工具,避免重复的劳动. 通过以下两个特性来实现这一目标:
- 提供一个简洁的但是强大的框架来完成系统管理任务
- 系统管理任务可以描述成puppet语言,因此可以相互分享代码,就像分享其他语言的代码一样,比如python, c等
因此,作为系统管理员的你可以更快的完成工作,因为你可以用puppet来处理所有的管理细节. 甚至你还可以下载其他管理员的puppet代码来让你的工作完成的更快.
作为工具的 puppet
puppet是一个配置管理工具, 典型的, puppet是一个C/S结构, 当然,这里的C可以有很多,因此,也可以说是一个星型结构. 所有的puppet客户端同一个服务器端的puppet通讯. 每个puppet客户端每半小时(可以设置)连接一次服务器端, 下载最新的配置文件,并且严格按照配置文件来配置服务器. 配置完成以后,puppet客户端可以反馈给服务器端一个消息. 如果出错,也会给服务器端反馈一个消息. 下图展示了一个典型的puppet配置的数据流动情况.
稳定性
puppet与其他手工操作工具有一个最大的区别就是 puppet的配置具有稳定性,因此你可以多次执行puppet, 一旦你更新了你的配置文件,puppet就会根据配置文件来更改你的机器配置,通常每30分钟检查一次. puppet会让你的系统状态同配置文件所要求的状态保持一致. 比如你配置文件里面要求ssh服务必须开启. 假如不小心ssh服务被关闭了,那么下一次执行puppet的时候,puppet会发现这个异常,然后会开启 ssh 服务. 以使系统状态和配置文件保持一致.puppet就象一个魔术师,会让你的混乱的系统收敛到puppet配置文件所想要的状态.
可以使用puppet管理服务器的整个生命周期,从初始化到退役.不同于传统的例如sun的Jumpstart或者redhat的Kickstart, puppet可以长年让服务器保持最新状态.只要一开始就正确的配置他们,然后再也不用去管他们.通常puppet用户只需要给机器安装好puppet并让他们运行,然后剩余的工作都由puppet来完成.
puppet的细节和原理
puppet的目的是让你只集中于你要管理的目标,而忽略实现的细节,例如命令名,参数或者文件格式. puppet把系统里面的用户,软件包,服务 看作是"资源", puppet的作用就是管理这些资源以及资源之间的相互联系.
底层支撑工具 Providers
puppet有很多的资源类型,例如文件,用户,软件包,服务, 不同的操作系统上对资源的管理命令是不一样的,例如debian下面用apt-get安装软件,redhat下面用yum安装软件.
因此 puppet 对同一资源的管理可以有多个实现,配置资源的时候,可以明确的指定用什么provider. 例如在redhat上配置一个package资源的时候,可以指定provide是yum.
修改系统配置
puppet 通过管理资源的方式来管理系统, 例如管理某个软件是否要安装,是安装最新的还是安装了就行. 管理某个服务是否开启, 管理某个文件的属性,内容等等. 所有的资源都有对应的几个属性可以设置. 通过设置属性的方式来管理资源. 有一种特殊的属性可以用在所有的资源上面,这种属性叫做 metaparams ( 元参数或者元属性).
资源之间的关系
支持资源之间的关系配置是puppet的关键特性之一. 一个资源的变更可以对另一个资源产生一个动作.例如 /etc/apache.conf这个资源有改动,可以让/etc/init.d/apache 这个资源 reload一下.
假如一个资源依赖另一个资源,那么puppet会优先配置被依赖的资源,因此如果你的配置文件没有准备好,对应的 服务是不会先启动的.
exec 资源
有时候,没有合适的资源来管理服务器上的配置, 为了处理这个情况,puppet 提供一个叫做 exec的资源类型. 利用这个资源,你可以执行外部命令. 例如你可以用svnadmin命令来创建一个svn库.
puppet 语言
资源
puppet的全部就是管理资源,因此puppet语言的焦点就是处理这些资源,下面是一个基本的管理单个资源的例子.
file {"/etc/hosts":
owner = root,
group = root,
mode = 644
}
上面的列子给出了定义一个资源所需要的所有组件,类型,名字和属性. 定义了一个 file 资源, 资源的title(标题)是 "/etc/hosts", 资源的属性里面设置了该文件属于那个用户和组,以及文件的权限.
也可以在一个大括号里面定义多个资源,通过分号来区分.
file {
"/etc/sudoers":
owner = "root",
group = "root",
mode = 644;
"/usr/sbin/sudo":
owner = "root",
group = "root",
mode = 4111
}
避免重复配置
puppet的编译器会避免在不同的代码段里面管理同一个资源, 如果在不同的代码段对同一个资源进行配置,执行puppet的时候你会得到一个语法错误.
puppet探测这种冲突的情况是通过判断资源类型和资源的title(标题 ); 如果两个资源有相同的资源类型和title; 那么就认为这两个资源是表示同一个资源.
类 class
下面讨论如何组合各种资源, 把多个相关的资源定义在一起,组成一个类. 例如下面的代码.
class sudo {
package { sudo: ensure = installed }
file {
"/etc/sudoers":
owner = "root",
group = "root",
mode = 644;
"/usr/sbin/sudo":
owner = "root",
group = "root",
mode = 4111
}
}
你在别的代码段 include sudo 这个类,就会把sudo这个软件包(package 那里定义)安装好,以及两个配置文件设置好.
Inheritance 继承
puppet 支持有限制的类的继承, 但是它只有一个唯一的作用就是: 子类里面的属性可以覆盖父类里面的属性. 下面是一个勉强正确的例子.
class base {
file { "/my/file": content = template("base.erb") }
}
class sub inherits base {
# override the content
File["/my/file"] { content = template("other.erb") }
}
注意,在子类里面的资源类型的定义是用的大写的 File ;表示对这个资源重新定义,如果用小写的 file;就会引起前面说的资源的重复配置的情况.得到一个语法错误.
一个复杂的真实的例子
生活总是不容易阿, 真实环境中,一个包经常关联到几个服务,同时又关联到不同的配置文件. 几乎所有的类unix系统里面都有ssh服务. 通常,你不只是想安装ssh包,而且你还想启动ssh 服务.看看下面的例子
class ssh {
package { ssh: ensure = installed }
file { sshd_config:
name = $operatingsystem ? {
Darwin = "/etc/sshd_config",
Solaris = "/opt/csw/etc/ssh/sshd_config",
default = "/etc/ssh/sshd_config"
},
source = "puppet://server.domain.com/files/ssh/sshd_config"
}
service { ssh:
name = $operatingsystem ? {
Solaris = openssh,
default = ssh
},
ensure = running,
subscribe = [Package[ssh], File[sshd_config]]
}
}
上面的代码安装好ssh包,并开启ssh服务,因为在不同的操作系统上面,ssh的配置文件名字不一样,所以还可以通过判断操作系统的类型来指定资源的title(标题). 那么操作系统的类型是怎么判断的呢? 下面就会解释这个问题.
Facter Variables facter 变量
在上面的ssh 代码里面,我们引入了一些新的东西,首先是 $operatingsystem 这个变量, 这个变量被引用,在puppet分析代码的时候,会把从 facter 传送过来的对应的值赋值给这个变量. 你可以单独手工执行facter这个命令,这个命令会打印出它所收集到的关于主机的信息,例如ip地址等等. facter把收集到值发送个puppet服务器端,服务器端就可以根据不同的条件来对不同的机器生成不同的puppet配置文件. 最重要的一个就是服务器的主机名.
selectors 选择器
另一个新东西就是 ? { … } 这个语法, 通过对 ? 前面的变量与括号里面的选择项进行对比,然后把相应的值赋值给资源,例如上面的例子, $operatingsystem 如果是solaris ,那么ssh这个资源的 name 属性就是 "openssh" ;如果不匹配,就赋值成defualt的指, 匹配将区分大小写.
title vs name (标题和名字)
如果你足够细心,你会发现我们资源指定了第 2个名字 ,例如上面的 service 资源 ssh. 注意,冒号前面的名字叫做 title (标题), 标题的作用让作者标识不同的资源,是写给人以及puppet语法分析器看的. 因此配置资源相互关系的时候,也是用资源的title. name是指定这个资源的具体路径,是写给计算机看的. 通常,name 会默认和title的值一样. 因此,你可以省略name. 只有情况特殊的时候才设置name属性.
资源之间的关系
最后,我们来分析所有资源直接的关系,配置文件,包,服务.最后一行的 File[sshd_config] 语法指定了一个资源参考,注意这里是用的资源的title ,这样你就不用指定资源的全路径.
subscribe = [Package[ssh], File[sshd_config]] 这个语句表示,如果package "ssh" 或者 文件" sshd_config"有修改, service ssh需要重启.
语言的其他特性
真相
几乎所有的东西和符号在puppet里面都被看作是字符串,包括数字和布尔值. 但是如果你用引号把true和false引起来,他们会被当做字符串,例如你想赋值给某个资性"yes"的 字符串.
变量
我们已经看过什么是变量了,当然,你还可以直接给他赋值,例如
$myvar = value
puppet 不允许你在同一个类里面对一个变量进行两次赋值.
更多的条件语句
我们已经在前面介绍过了选择器(selectors),在为变量选择特定的值的时候非常有用,Puppet同时也支持条件语句,使得你能根据不同的条件导入不同的资源定义(resource specifications):
case $operatingsystem {
Darwin: { file { "/some/file": ensure => present } }
default: { file { "/other/file": ensure => present } }
}
与选择器(selectors)配合, case 语句可以进行 case-insensitive 匹配。
这里还有一个简单的if/else结构:
if $should {
file { "/some/file": ensure => present }
} else {
file { "/other/file": ensure => present }
}
另外,Puppet从版本0.24.6开始支持比较运算符。
数组
puppet 非常有限的支持数组这种类型,你可以创建数组,并且给他们赋值,但是你不能删除它们.数组用的最多的情况就是上面ssh例子里面,资源依赖哪种情况. 或者是一次管理多个相同类型的资源.例如:
user { [bin, adm]: ensure = present }
函数
puppet 支持简单的函数语法,例如
notice("This is a log message")
puppet提供一些有用的函数,例如template利用erb模板来生成文件内容,这样就可以根据不同主机的情况,生成不同的配置文件.例如配置squid的内存缓存大小,可以利用facter返回的内存值做一个简单的数学计算,然后写入到squid的配置文件,就是通过template来完成的. 另外一个函数include 可以读入另外的puppet配置文件或者类.这样可以把puppet的文件分割的更有规律.
puppet 语言高级特性
定义
puppet里面有一个非常有用的语法结构,叫做"definitions", 通过 definitions 可以把多个资源包装成一个资源,或者把一个资源包装成一个模型,便于使用.例如,在debian里面管理一个apache虚拟机非常简单,把一个虚拟主机的配置文件放到/etc/sites-available/里面,然后做一个符号链接到/etc/sites-enabled目录. 你可以为你每个虚拟主机复制同样的配置代码,但是如果你使用下面的代码就会更好和更简单.
define virtual_host($docroot, $ip, $order = 500, $ensure = "enabled") {
$file = "/etc/sites-available/$name.conf"
# The template fills in the docroot, ip, and name.
file { $file:
content => template("virtual_host.erb"),
notify => Service[apache]
}
file { "/etc/sites-enabled/$order-$name.conf":
ensure => $ensure ? {
enabled => $file,
disabled => absent
}
}
}
然后,你就可以使用这个定义来管理一个apache虚拟主机,如下面代码所示
virtual_host { "reductivelabs.com":
order => 100,
ip => "192.168.0.100",
docroot => "/var/www/reductivelabs.com/htdocs"
}
你可以在其他地方重用这个定义, 另一个定义的用法就是包装一组exec资源,让使用者更加清晰思路.例如下面的代码实现了一个svn库的创建的定义.
# Create a new subversion repository.
define svnrepo($path) {
exec { "create-svn-$name":
command => "/usr/bin/svnadmin create $path/$name",
creates => "$path/$name" # only run if this file does not exist
}
}
然后,你可以在其他地方用下面的代码来创建一个svn库
svnrepo { puppet: path => "/var/lib/svn" }
nodes 节点
最后一个关于puppet语言的语法是 节点定义(node definition), 节点定义很象类定义,也支持继承特性. 当一个节点(puppet客户端)连接到puppet服务器端,puppet解析器会查找这个节点的node代码片断,然后利用这个代码片断来生成该客户端的配置代码. puppet里面主机名来标明一个主机,因此主机名在puppet里面相当重要. 如果puppet找不到匹配该主机名的node定义,就会用默认的节点定义来配置该主机. 在node里面使用主机名,需要用单引号把主机名括起来.
node 'www.example.com' {
include publickey_auth
}
在上面的代码中,如果www.example.com这个主机连接到puppet服务器,puppet服务器就会按照上面的代码来配置www.example.com这个机器.
典型的puppet使用方法
puppet既可以在单机上使用,也可以以c/s结构使用.在大规模使用puppet的情况下,通常使用c/s结构.在这种结构中puppet客户端只是指运行puppet的服务器,puppet服务器端是只运行puppetmaster的服务器.
puppet客户端首先会连接到puppet服务器端,并且通过facter工具把客户端的配置信息发送给服务器端. 服务器端通过分析客户端的主机名,通过node 定义,找到该主机的配置代码,然后编译配置代码,把编译好的配置代码发回客户端. 客户端执行代码完成配置.并且把代码执行情况反馈给puppet服务器端.
总结
以上列举了许多非常有用的用法,但所有的这些只是Puppet功能的一个大概介绍。这里有一份全面的[文档],里面附带了许多例子:为了让你能过更加深入的了解Puppet。我们会一直维护这个文档,所以如果你有什么具体的要求,请通过puppetlabs.com与我们联系。
原文 Puppet Introduction
翻译 huangmingyou、hougp、SegmentFault
许可 CC-by-sa
adapted from Puppet Wiki