Puppet-秘籍第三版-全-
Puppet 秘籍第三版(全)
原文:
annas-archive.org/md5/ba230b27cef8804fa19648be9e950702译者:飞龙
前言
配置管理已经成为系统管理员的必备技能。掌握如何使用配置管理工具,如 Puppet,使管理员能够充分利用自动化配置系统和云资源。从执行任务、编写脚本到在 Puppet 中创建模块,或将任务 Puppet 化,是一个自然的进阶过程。
本书带你走出基础,深入探索 Puppet 的强大功能,详细展示如何解决各种实际问题和应用。每一步都会清晰地告诉你需要输入的命令,并为每个食谱提供完整的代码示例。它将你从对 Puppet 的初步了解,带入对 Puppet 最新和最先进功能、社区最佳实践、扩展和性能的全面与专家级理解。本书新版包括了配置和使用 Hiera、puppetdb 以及操作集中式 puppetmaster 配置的食谱。
本书还包括来自生产系统的真实示例和一些世界上最大规模 Puppet 安装中使用的技术。它将展示如何使用 Puppet 以不同方式完成任务,并指出这些方法的一些优缺点。
这本书的结构设计使你可以随时从任意章节开始,尝试执行一个食谱,而不需要从头到尾逐一阅读。无论你具备何种水平的 Puppet 经验,这本书都能提供适合你的内容——从简单的工作流技巧到高级的高性能 Puppet 架构。
Puppet 是一个不断变化的工具生态系统。我尽力包括了今天我认为重要的所有工具,例如 r10k。#puppet IRC 频道、puppetlabs 博客(puppetlabs.com/blog)和 Forge(forge.puppetlabs.com)是很好的资源,能帮助你随时了解 Puppet 的变化。
本书内容
第一章,Puppet 语言与风格,介绍了 Puppet 语言,并展示了如何编写清单。Puppet 的静态检查工具 puppet-lint 也被介绍,我们回顾了编写 Puppet 代码的最佳实践。元参数通过示例进行说明。我们还通过使用未来解析器预览了对 Puppet 语言的拟议更改。
第二章,Puppet 基础设施,主要讲解如何在你的环境中部署 Puppet。我们介绍了两种主要的安装方法:集中式和去中心化(无主机模式)。我们展示了如何使用 Git 来集中管理代码,还会配置 puppetdb 和 Hiera。
第三章,编写更好的清单,讲述了如何组织你的 Puppet 清单。清单用于构建模块;我们介绍了角色和配置文件的概念,用于抽象模块如何应用到机器上。引入了参数化类。我们还展示了如何使用资源数组和资源默认值高效地定义资源。
第四章,处理文件和包,展示了如何使用片段(碎片)管理文件。我们介绍了 Ruby(ERB)和 Puppet(EPP)模板的模板化能力。我们还探讨了如何保护存储在 Puppet 清单中的信息。
第五章,用户和虚拟资源,涉及虚拟和导出资源的高级主题。虚拟资源是一种定义资源但默认不应用的方式。导出资源类似,但用于将一台机器上的资源应用到一台或多台其他机器上。
第六章,资源和文件管理,讲述了如何处理目录以及清除不受 Puppet 控制的资源。我们展示了如何在不同的机器上应用不同的文件资源。管理/etc/hosts中主机条目的方法通过导出的资源示例进行了展示。
第七章,应用管理,展示了如何使用 Puppet 管理你部署的应用程序。通过使用公共 Forge 模块,我们配置了 Apache、nginx 和 MySQL。
第八章,节点间协调,探讨了导出资源。我们使用导出资源来配置 NFS、haproxy 和 iptables。
第九章,外部工具与 Puppet 生态系统,展示了如何通过自定义类型和提供者扩展 Puppet,如何创建自定义事实,以及一些更高级的工具,如 Puppet-librarian 和 r10k。
第十章,监控、报告与故障排除,是最后一章,我们展示了如何利用 Puppet 查看基础设施中的问题所在。书中展示了一些常见问题及其解决方案。
本书的需求
你需要一台能够运行 Linux 虚拟机的计算机。书中的示例使用基于 Debian 和 Enterprise Linux 的发行版。你还需要一个互联网连接,以便使用 puppetlabs 提供的仓库。
本书适合人群
本书假设你已经熟悉 Linux 管理。示例需要一定的命令行使用经验和基本的文本文件编辑技能。虽然有益,但不要求有编码经验。
约定
在本书中,你会看到许多不同风格的文本,以区分不同类型的信息。以下是这些风格的示例及其含义的解释。
文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特账号会按如下方式显示:“你可以使用+操作符连接数组,或者用<<操作符附加它们。”
代码块如下所示:
slice ($firewall_rules,2) |$ip, $port| {firewall {"$port from $ip": dport => $port, source => "$ip", action => 'accept', }
}
任何命令行输入或输出都如下所示:
Notice: 1
Notice: 2
Notice: 3
Notice: 4
Notice: 5
# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample
/etc/asterisk/cdr_mysql.conf
新术语和重要词汇以粗体显示。你在屏幕上看到的、菜单或对话框中的词语会像这样出现在文本中:“在这个图表中,很容易看到Package['ntp']是第一个应用的资源,接着是File['/etc/ntp.conf'],最后是Service['ntp']。”
注意
警告或重要说明将以框框显示,如此所示。
提示
提示和技巧将如下所示。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对本书的看法——你喜欢什么,或者不喜欢什么。读者反馈对我们至关重要,因为它帮助我们开发出你真正受益的书籍。
要向我们发送一般反馈,只需通过电子邮件 <feedback@packtpub.com> 联系我们,并在邮件主题中提及书名。
如果你在某个话题上有专长,并且有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南:www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲拥有者,我们为你提供了许多资源,帮助你最大化从购买中获得的收益。
下载示例代码
你可以从你在www.packtpub.com购买的所有 Packt 书籍的账户中下载示例代码文件。如果你是从其他地方购买的这本书,可以访问www.packtpub.com/support并注册,将代码文件直接通过电子邮件发送给你。
勘误
尽管我们已尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在我们的书籍中发现错误——可能是文本或代码中的错误——我们将非常感激你能向我们报告。通过这样做,你可以帮助其他读者避免沮丧,并帮助我们改进该书的后续版本。如果你发现任何勘误,请访问www.packtpub.com/submit-errata报告错误,选择你的书籍,点击勘误提交表单链接,并填写勘误详情。一旦你的勘误被验证,提交将被接受,错误信息将被上传到我们的网站,或添加到该书的勘误部分的现有勘误列表中。
要查看先前提交的勘误表,请访问 www.packtpub.com/books/content/support 并在搜索框中输入书名。所需信息将显示在勘误表部分。
盗版
互联网上侵犯版权材料的盗版问题在所有媒体上持续存在。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现我们任何形式的作品的非法副本,请立即向我们提供位置地址或网站名称,以便我们采取措施。
如果您发现了涉嫌盗版材料,请通过 <copyright@packtpub.com> 提供链接。
我们感谢您帮助保护我们的作者以及为您带来有价值的内容的能力。
问题
如果您在书的任何方面遇到问题,请通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章:Puppet 语言与风格
| "计算机语言设计就像在公园里散步,只不过是侏罗纪公园。" | ||
|---|---|---|
| --Larry Wall |
在本章中,我们将涵盖以下内容:
-
向节点添加资源
-
使用 Facter 描述节点
-
在启动服务之前安装包
-
安装、配置并启动服务
-
使用社区 Puppet 风格
-
创建清单
-
使用 Puppet-lint 检查你的清单
-
使用模块
-
使用标准命名约定
-
使用内联模板
-
遍历多个项
-
编写强大的条件语句
-
在 if 语句中使用正则表达式
-
使用选择器和案例语句
-
使用
in操作符 -
使用正则表达式替换
-
使用未来解析器
介绍
在本章中,我们将从 Puppet 语法的基础开始,展示如何使用 Puppet 中的一些语法糖。接着我们将介绍 Puppet 如何处理依赖关系,以及如何让 Puppet 为你完成工作。
我们将探讨如何按照社区约定组织和结构化你的代码,将其分成模块,以便其他人能轻松阅读和维护你的代码。我还会展示一些 Puppet 语言的强大功能,这些功能可以让你编写简洁而富有表现力的清单。
向节点添加资源
本配方将介绍语言并展示如何编写 Puppet 代码的基础。初学者可能希望参考 Puppet 3: 初学者指南、John Arundel、Packt Publishing,以补充本节内容。Puppet 代码文件被称为清单;清单声明资源。Puppet 中的资源可以是类型、类或节点。类型类似于文件、包或任何在语言中声明过类型的事物。标准类型的当前列表可以在 Puppet Labs 网站上找到,地址是 docs.puppetlabs.com/references/latest/type.html。我发现自己经常参考这个网站。你可以定义自己的类型,使用类似于子程序的机制,称为 定义类型,或者你可以使用自定义类型扩展语言。类型是语言的核心;它们描述了组成节点的事物(节点是 Puppet 用来表示客户端计算机/设备的词汇)。Puppet 使用资源来描述节点的状态;例如,我们将使用站点清单(site.pp)为节点声明以下包资源。
如何操作...
创建一个 site.pp 文件,并在其中放置以下代码:
node default {
package { 'httpd':
ensure => 'installed'
}
}
提示
下载示例代码
你可以从你的账户中下载所有购买的 Packt 书籍的示例代码文件,地址是 www.packtpub.com。如果你从其他地方购买了这本书,你可以访问 www.packtpub.com/support 注册后,文件将直接通过电子邮件发送给你。
它是如何工作的...
这个清单将确保在应用该清单的任何节点上都会安装一个名为'httpd'的包。default关键字是 Puppet 的通配符;它将节点默认定义中的所有内容应用于任何节点。当 Puppet 将清单应用到一个节点时,它使用资源抽象层(RAL)将包类型转换为目标节点的包管理系统。这意味着我们可以使用相同的清单在任何为 Puppet 提供包类型Provider的系统上安装httpd包。Providers 是执行实际应用清单的代码部分。当将之前的代码应用到运行基于 YUM 的发行版的节点时,将使用 YUM 提供程序来安装httpd的 RPM 包。当相同的代码应用到运行基于APT的发行版的节点时,将使用 APT 提供程序来安装httpd的 DEB 包(该包可能不存在,大多数基于 Debian 的系统将该包称为apache2;我们稍后会处理这种命名问题)。
使用 Facter 描述节点
Facter 是一个独立的实用程序,Puppet 依赖于它。它是 Puppet 用于收集目标系统(节点)信息的系统;facter将这些信息片段称为事实。您可以从命令行运行facter以获取系统的实时信息。
如何做...
-
使用
facter来查找系统的当前运行时间,运行时间事实:t@cookbook ~$ facter uptime 0:12 hours -
与 Linux 的 uptime 命令输出进行比较:
t@cookbook ~$ uptime 01:18:52 up 12 min, 1 user, load average: 0.00, 0.00, 0.00
它是如何工作的...
当facter安装(作为 Puppet 的依赖项)时,默认会安装若干事实定义。您可以从命令行通过名称引用这些事实。
还有更多...
运行facter而不带任何参数会导致facter打印系统已知的所有事实。我们将在后面的章节中看到,facter可以通过自定义事实进行扩展。所有事实都可以作为变量供您使用;变量将在下一节中讨论。
变量
Puppet 中的变量用美元符号(\()标记。在清单中使用变量时,建议将变量放在大括号`"\){myvariable}"中,而不是"\(myvariable"`。所有来自`facter`的事实都可以作为顶级作用域变量引用(我们将在下一节讨论作用域)。例如,节点的**完全限定域名**(**FQDN**)可以通过`"\){::fqdn}"来引用。变量只能包含字母字符、数字和下划线字符(_`)。作为一种风格,变量应以字母字符开头。永远不要在变量名中使用连字符。
作用域
在 还有更多… 部分中解释的变量示例中,完全限定的域名被引用为 ${::fqdn},而不是 ${fqdn};双冒号是 Puppet 区分作用域的方式。最高级别的作用域,即顶级作用域或全局作用域,通过变量标识符前面的两个冒号(::)来表示。为了减少命名空间冲突,请始终在清单中使用完全作用域的变量标识符。对于 Unix 用户来说,顶级作用域变量就像是 /(根)级别。你可以使用双冒号语法来引用变量,就像你通过完整路径引用目录一样。对于开发者而言,顶级作用域变量可以看作是全局变量;然而,不同于全局变量,你必须始终使用双冒号符号来引用它们,以确保局部变量不会遮蔽顶级作用域变量。
在启动服务之前安装一个包
为了展示排序是如何工作的,我们将创建一个清单,首先安装 httpd,然后确保 httpd 包服务正在运行。
如何做…
-
我们从创建一个定义服务的清单开始:
service {'httpd': ensure => running, require => Package['httpd'], } -
服务定义引用了一个名为
httpd的包资源;我们现在需要定义这个资源:package {'httpd': ensure => 'installed', }
它是如何工作的…
在这个例子中,包会在服务启动之前安装。通过在 httpd 服务的定义中使用 require,可以确保包首先安装,无论在清单文件中的顺序如何。
大小写
在 Puppet 中,大小写非常重要。在我们之前的示例中,我们创建了一个名为 httpd 的包。如果我们以后想要引用这个包,我们需要将它的类型(package)大写,像这样:
Package['httpd']
要引用一个类,例如已经在你的清单中包含/定义的 something::somewhere 类,你可以按照以下方式使用完整路径引用它:
Class['something::somewhere']
当你有一个定义类型时,例如以下定义的类型:
example::thing {'one':}
上述资源稍后可以这样引用:
Example::Thing['one']
了解如何引用之前定义的资源对于下一节关于元参数和排序非常重要。
学习元参数和排序
所有用于定义节点的清单都会编译成一个目录。目录是将应用于配置节点的代码。需要记住的是,清单不会按顺序应用于节点。清单的应用没有固有的顺序。考虑到这一点,在之前的 httpd 示例中,如果我们想确保 httpd 进程在 httpd 包安装后启动,该怎么办?
我们不能依赖于 httpd 服务在清单中紧跟着 httpd 包。我们必须使用元参数来告诉 Puppet 我们希望资源应用到节点的顺序。元参数是可以应用于任何资源的参数,并且不特定于任何一种资源类型。它们用于目录编译,并作为 Puppet 的提示,但并不定义任何与其附加的资源相关的内容。在处理顺序时,有四个元参数可供使用:
-
before -
require -
notify -
subscribe
before 和 require 元参数指定了直接的顺序;notify 隐含了 before,而 subscribe 隐含了 require。notify 元参数仅适用于服务;notify 的作用是告诉服务在通知资源应用到节点后重新启动(这通常是一个包或文件资源)。在文件的情况下,一旦文件在节点上创建,notify 参数会重新启动任何提到的服务。subscribe 元参数具有相同的效果,但它是在服务上定义的;服务会订阅该文件。
三重关系
前面提到的包与服务之间的关系是 Puppet 的一个重要且强大的范式。再加入一个资源类型文件,便形成了木偶师所说的 三重关系。几乎所有系统管理任务都围绕这三种资源类型展开。作为系统管理员,你安装一个包,使用文件配置该包,然后启动服务。

三重关系图(文件需要包来创建目录,服务需要文件和包)
幂等性
Puppet 的一个关键概念是,当目录被应用到节点时,系统的状态不能影响 Puppet 运行的结果。换句话说,在 Puppet 运行结束时(如果运行成功),系统将处于已知状态,且对目录的任何进一步应用将导致系统处于相同的状态。Puppet 的这一特性称为幂等性。幂等性 是指无论你做多少次某事,系统保持与第一次操作相同的状态。例如,如果你有一个灯开关,你指示将其打开,灯会亮。如果你再次给出同样的指令,灯依然保持亮着。
安装、配置并启动服务
网上有很多关于这种模式的例子。在我们简单的例子中,我们将在 /etc/httpd/conf.d/cookbook.conf 下创建一个 Apache 配置文件。 /etc/httpd/conf.d 目录在 httpd 包安装之前是不存在的。文件创建后,我们希望 httpd 重新启动以察觉到更改;我们可以通过 notify 参数实现这一点。
如何操作...
我们需要和上一个示例相同的定义;我们需要安装包和服务。现在我们需要两个新的要素。我们需要创建配置文件和索引页面(index.html)。为此,我们按照以下步骤进行:
-
与之前的示例一样,我们确保服务正在运行,并指定该服务需要
httpd包:service {'httpd': ensure => running, require => Package['httpd'], } -
然后我们按如下方式定义包:
package {'httpd': ensure => installed, } -
现在,我们创建
/etc/httpd/conf.d/cookbook.conf配置文件;/etc/httpd/conf.d目录在安装httpd包之前是不存在的。require元参数告诉 Puppet,这个文件需要先安装httpd包,才能创建:file {'/etc/httpd/conf.d/cookbook.conf': content => "<VirtualHost *:80>\nServernamecookbook\nDocumentRoot/var/www/cookbook\n</VirtualHost>\n", require => Package['httpd'], notify => Service['httpd'], } -
然后我们继续在
/var/www/cookbook为我们的虚拟主机创建index.html文件。这个目录尚未存在,因此我们还需要创建它,使用以下代码:file {'/var/www/cookbook': ensure => directory, } file {'/var/www/cookbook/index.html': content => "<html><h1>Hello World!</h1></html>\n", require => File['/var/www/cookbook'], }
它是如何工作的…
文件资源的require属性告诉 Puppet,我们需要先创建/var/www/cookbook目录,然后才能创建index.html文件。要记住的重要概念是,我们不能假设目标系统(节点)有什么预设条件。我们需要定义目标依赖的所有内容。每当你在清单中创建文件时,你必须确保包含该文件的目录已经存在。每当你指定某个服务应当运行时,你必须确保提供该服务的包已被安装。
在这个示例中,使用元参数,我们可以确保无论节点在运行 Puppet 之前处于什么状态,Puppet 运行后,以下内容将会成立:
-
httpd将正在运行。 -
VirtualHost配置文件将存在 -
httpd将重新启动并且意识到VirtualHost文件。 -
DocumentRoot目录将存在。 -
index.html文件将存在于DocumentRoot目录中。
使用社区 Puppet 风格
如果其他人需要阅读或维护你的清单文件,或者你想与社区分享代码,最好尽可能遵循现有的风格约定。这些约定涵盖了代码的布局、空格、引用、对齐和变量引用等方面,Puppet 官方的风格推荐可以在docs.puppetlabs.com/guides/style_guide.html找到。
如何做…
在这一部分,我将展示一些更重要的示例,并确保你的代码符合风格要求。
缩进
使用两个空格(而不是制表符)缩进你的清单文件,如下所示:
service {'httpd':
ensure => running,
}
引用
始终引用资源名称,如下所示:
package { 'exim4':
然而,我们不能按以下方式进行:
package { exim4:
对于所有字符串,使用单引号,除非:
-
字符串包含像
"${::fqdn}"这样的变量引用。 -
字符串包含像
"\n"这样的字符转义序列。
请考虑以下代码:
file { '/etc/motd':
content => "Welcome to ${::fqdn}\n"
}
Puppet 不会处理变量引用或转义序列,除非它们在双引号内。
始终为那些不是 Puppet 保留字的参数值加上引号。例如,以下值不是保留字:
name => 'Nucky Thompson',
mode => '0700',
owner => 'deploy',
然而,这些值是保留字,因此不需要加引号:
ensure => installed,
enable => true,
ensure => running,
假
在 Puppet 中,只有一件事情是假的,即没有引号的 false。字符串 "false" 评估为 true,而字符串 "true" 也评估为 true。实际上,除了字面上的 false,其他所有东西都会被评估为 true(当作为布尔值处理时):
if "false" {
notify { 'True': }
}
if 'false' {
notify { 'Also true': }
}
if false {
notify { 'Not true': }
}
当通过 puppet apply 执行此代码时,前两个通知会被触发。最后一个通知不会被触发;它是唯一一个评估为 false 的通知。
变量
引用变量名时,始终在字符串中使用大括号({}),例如,按如下方式:
source => "puppet:///modules/webserver/${brand}.conf",
否则,Puppet 的解析器将不得不猜测哪些字符属于变量名,哪些字符属于周围的字符串。使用大括号可以使其明确。
参数
声明参数的行结尾始终加上逗号,即使是最后一个参数也不例外:
service { 'memcached':
ensure => running,
enable => true,
}
Puppet 允许使用符号链接,并且如果您以后想添加参数或重新排列现有参数,这样做会更方便。
当声明一个只有一个参数的资源时,将整个声明写在一行,并且不要加尾随逗号,如以下代码片段所示:
package { 'puppet': ensure => installed }
如果有多个参数,将每个参数放在单独一行:
package { 'rake':
ensure => installed,
provider => gem,
require => Package['rubygems'],
}
为了使代码更易于阅读,将参数箭头与最长的参数对齐,如下所示:
file { "/var/www/${app}/shared/config/rvmrc":
owner => 'deploy',
group => 'deploy',
content => template('rails/rvmrc.erb'),
require => File["/var/www/${app}/shared/config"],
}
箭头应按资源对齐,而不是跨整个文件对齐,否则这会使您从一个文件复制并粘贴代码到另一个文件时变得困难。
符号链接
声明为符号链接的文件资源时,使用 ensure => link 并设置目标属性,如下所示:
file { '/etc/php5/cli/php.ini':
ensure => link,
target => '/etc/php.ini',
}
创建清单
如果您已经有一些 Puppet 代码(称为 Puppet 清单),可以跳过此部分并进入下一部分。如果没有,我们将看到如何创建和应用一个简单的清单。
如何操作...
创建并应用简单的清单,请按照以下步骤操作:
-
首先,在您的计算机上本地安装 Puppet,或者创建一台虚拟机并在该机器上安装 Puppet。对于基于 YUM 的系统,请使用
yum.puppetlabs.com/,对于基于 APT 的系统,请使用apt.puppetlabs.com/。您也可以使用 gem 安装 Puppet。对于我们的示例,我们将在 Debian Wheezy 系统(主机名:cookbook)上使用 gem 安装 Puppet。为了使用 gem,我们需要rubygems包,如下所示:t@cookbook:~$ sudo apt-get install rubygems Reading package lists... Done Building dependency tree Reading state information... Done The following NEW packages will be installed: rubygems 0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded. Need to get 0 B/597 kB of archives. After this operation, 3,844 kB of additional disk space will be used. Selecting previously unselected package rubygems. (Reading database ... 30390 files and directories currently installed.) Unpacking rubygems (from .../rubygems_1.8.24-1_all.deb) ... Processing triggers for man-db ... Setting up rubygems (1.8.24-1) ... -
现在,使用
gem安装 Puppet:t@cookbook $ sudo gem install puppet Successfully installed hiera-1.3.4 Fetching: facter-2.3.0.gem (100%) Successfully installed facter-2.3.0 Fetching: puppet-3.7.3.gem (100%) Successfully installed puppet-3.7.3 Installing ri documentation for hiera-1.3.4 Installing ri documentation for facter-2.3.0 Installing ri documentation for puppet-3.7.3 Done installing documentation for hiera, facter, puppet after 239 seconds -
安装了三个 gem。现在,Puppet 已安装,我们可以创建一个目录来存放我们的 Puppet 代码:
t@cookbook:~$ mkdir -p .puppet/manifests t@cookbook:~$ cd .puppet/manifests t@cookbook:~/.puppet/manifests$ -
在您的
manifests目录中,创建site.pp文件,并填入以下内容:node default { file { '/tmp/hello': content => "Hello, world!\n", } } -
使用
puppet apply命令测试您的清单。这将告诉 Puppet 读取清单,比较机器的当前状态,并对该状态进行必要的更改:t@cookbook:~/.puppet/manifests$ puppet apply site.pp Notice: Compiled catalog for cookbook in environment production in 0.14 seconds Notice: /Stage[main]/Main/Node[default]/File[/tmp/hello]/ensure: defined content as '{md5}746308829575e17c3331bbcb00c0898b' Notice: Finished catalog run in 0.04 seconds -
要查看 Puppet 是否按预期操作(创建
/tmp/hello文件并包含Hello, world内容),运行以下命令:t@cookbook:~/puppet/manifests$ cat /tmp/hello Hello, world! t@cookbook:~/puppet/manifests$
注意
请注意,在/tmp目录下创建文件不需要特殊权限。我们没有通过sudo运行 Puppet。Puppet 不需要通过sudo运行;在某些情况下,通过非特权用户运行可能会很有用。
还有更多内容…
当多人共同开发一个代码库时,风格不一致的问题很容易出现。幸运的是,有一个工具可以自动检查你的代码是否符合风格指南:puppet-lint。我们将在下一节中了解如何使用这个工具。
使用 Puppet-lint 检查你的清单文件
puppetlabs 官方风格指南列出了多种 Puppet 代码的风格规范,其中一些我们在前面的小节中已经提到过。例如,根据风格指南,清单文件应该:
-
必须使用两个空格的软制表符
-
禁止使用字面制表符字符
-
禁止包含多余的空白字符
-
行宽不应超过 80 个字符
-
应该在代码块内对齐参数箭头(
=>)
遵循风格指南将确保你的 Puppet 代码易于阅读和维护,如果你打算将代码发布给公众,遵循风格规范是至关重要的。
puppet-lint工具将自动检查你的代码是否符合风格指南。下一节将解释如何使用它。
准备工作
以下是安装 Puppet-lint 所需的步骤:
-
我们将通过 gem 提供程序安装 Puppet-lint,因为 gem 版本比 APT 或 RPM 包更为更新。创建一个
puppet-lint.pp清单,如下所示:package {'puppet-lint': ensure => 'installed', provider => 'gem', } -
在
puppet-lint.pp清单上运行puppet apply命令,如下所示:t@cookbook ~$ puppet apply puppet-lint.pp Notice: Compiled catalog for node1.example.com in environment production in 0.42 seconds Notice: /Stage[main]/Main/Package[puppet-lint]/ensure: created Notice: Finished catalog run in 2.96 seconds t@cookbook ~$ gem list puppet-lint *** LOCAL GEMS *** puppet-lint (1.0.1)
如何操作…
按照以下步骤使用 Puppet-lint:
-
选择一个你想用 Puppet-lint 检查的 Puppet 清单文件,然后运行以下命令:
t@cookbook ~$ puppet-lint puppet-lint.pp WARNING: indentation of => is not properly aligned on line 2 ERROR: trailing whitespace found on line 4 -
如你所见,Puppet-lint 发现了清单文件中的一些问题。修正错误,保存文件,然后重新运行 Puppet-lint 以确保一切正常。如果成功,你将看不到任何输出:
t@cookbook ~$ puppet-lint puppet-lint.pp t@cookbook ~$
还有更多内容...
你可以在github.com/rodjek/puppet-lint了解更多关于 Puppet-lint 的信息。
是否遵循 Puppet 风格指南,并保持你的代码符合风格规范?这由你决定,但有几点需要考虑:
-
使用一些风格规范是有意义的,特别是当你与他人合作编写代码时。如果你和同事无法就空格、制表符、引号、对齐等标准达成一致,代码将变得混乱,难以阅读或维护。
-
如果你选择了要遵循的一套风格规范,最合适的选择就是由 puppetlabs 发布并被社区采纳用于公共模块的风格规范。
话虽如此,如果你选择不在代码库中采纳某些检查,也可以告诉 Puppet-lint 忽略这些检查。例如,如果你不想让 Puppet-lint 警告你关于代码行超过 80 个字符的事情,你可以使用以下选项运行 Puppet-lint:
t@cookbook ~$ puppet-lint --no-80chars-check
运行 puppet-lint --help 查看完整的检查配置命令列表。
另见
-
第二章中的使用 Git hooks 进行自动语法检查食谱,Puppet 基础设施
-
第九章中的使用 rspec-puppet 测试 Puppet 清单食谱,外部工具与 Puppet 生态系统
使用模块
使 Puppet 清单更清晰、更易维护的最重要的事情之一就是将它们组织成模块。
模块是自包含的 Puppet 代码包,包含实现某个功能所需的所有文件。模块可以包含普通文件、模板、Puppet 清单、自定义事实声明、augeas 镜头以及自定义 Puppet 类型和提供程序。
将功能分离成模块使得代码更易于重用和共享;这也是组织清单的最合乎逻辑的方式。在这个例子中,我们将创建一个管理 memcached 的模块,memcached 是一个通常与 Web 应用程序一起使用的内存缓存系统。
如何操作…
以下是创建示例模块的步骤:
-
我们将使用 Puppet 的模块子命令来为我们的新模块创建目录结构:
t@cookbook:~$ mkdir -p .puppet/modules t@cookbook:~$ cd .puppet/modules t@cookbook:~/.puppet/modules$ puppet module generate thomas-memcached We need to create a metadata.json file for this module. Please answer the following questions; if the question is not applicable to this module, feel free to leave it blank. Puppet uses Semantic Versioning (semver.org) to version modules.What version is this module? [0.1.0] --> Who wrote this module? [thomas] --> What license does this module code fall under? [Apache 2.0] --> How would you describe this module in a single sentence? --> A module to install memcached Where is this module's source code repository? --> Where can others go to learn more about this module? --> Where can others go to file issues about this module? --> ---------------------------------------- { "name": "thomas-memcached", "version": "0.1.0", "author": "thomas", "summary": "A module to install memcached", "license": "Apache 2.0", "source": "", "issues_url": null, "project_page": null, "dependencies": [ { "version_range": ">= 1.0.0", "name": "puppetlabs-stdlib" } ] } ---------------------------------------- About to generate this metadata; continue? [n/Y] --> y Notice: Generating module at /home/thomas/.puppet/modules/thomas-memcached... Notice: Populating ERB templates... Finished; module generated in thomas-memcached. thomas-memcached/manifests thomas-memcached/manifests/init.pp thomas-memcached/spec thomas-memcached/spec/classes thomas-memcached/spec/classes/init_spec.rb thomas-memcached/spec/spec_helper.rb thomas-memcached/README.md thomas-memcached/metadata.json thomas-memcached/Rakefile thomas-memcached/tests thomas-memcached/tests/init.pp此命令创建模块目录并创建一些空文件作为起点。要使用该模块,我们将创建指向模块名称(memcached)的符号链接。
t@cookbook:~/.puppet/modules$ ln –s thomas-memcached memcached -
现在,编辑
memcached/manifests/init.pp并将文件末尾的类定义更改为以下内容。注意,puppet module generate创建了许多注释行;在生产环境的模块中,你可能想要编辑这些默认的注释:class memcached { package { 'memcached': ensure => installed, } file { '/etc/memcached.conf': source => 'puppet:///modules/memcached/memcached.conf', owner => 'root', group => 'root', mode => '0644', require => Package['memcached'], } service { 'memcached': ensure => running, enable => true, require => [Package['memcached'], File['/etc/memcached.conf']], } } -
创建
modules/thomas-memcached/files目录,然后创建一个名为memcached.conf的文件,内容如下:-m 64 -p 11211 -u nobody -l 127.0.0.1 -
将你的
site.pp文件更改为以下内容:node default { include memcached } -
我们希望这个模块安装 memcached。我们需要以 root 权限运行 Puppet,为此我们将使用 sudo。我们需要确保 Puppet 能在我们的主目录中找到该模块;我们可以在运行 Puppet 时在命令行中指定这一点,代码片段如下所示:
t@cookbook:~$ sudo puppet apply --modulepath=/home/thomas/.puppet/modules /home/thomas/.puppet/manifests/site.pp Notice: Compiled catalog for cookbook.example.com in environment production in 0.33 seconds Notice: /Stage[main]/Memcached/File[/etc/memcached.conf]/content: content changed '{md5}a977521922a151c959ac953712840803' to '{md5}9429eff3e3354c0be232a020bcf78f75' Notice: Finished catalog run in 0.11 seconds -
检查新服务是否正在运行:
t@cookbook:~$ sudo service memcached status [ ok ] memcached is running.
它是如何工作的…
当我们使用 Puppet 的 module generate 命令创建模块时,我们使用了名称 thomas-memcached。连字符前的名称是你的用户名或你在 Puppet forge(一个在线模块库)上的用户名。由于我们希望 Puppet 能够通过名称 memcached 找到该模块,我们在 thomas-memcached 和 memcached 之间创建了一个符号链接。
模块有一个特定的目录结构。并不是所有这些目录都需要存在,但如果它们存在,它们应该按如下方式组织:
modules/
└MODULE_NAME/ *never use a dash (-) in a module name*
└examples/ *example usage of the module*
└files/ *flat files used by the module*
└lib/
└facter/ *define new facts for facter*
└puppet/
└parser/
└functions/ *define a new puppet function, like sort()*
└provider/ *define a provider for a new or existing type*
└util/ *define helper functions (in ruby)*
└type/ *define a new type in puppet*
└manifests/
└init.pp *class MODULE_NAME { }*
└spec/ rSpec *tests*
└templates/ *erb template files used by the module*
所有清单文件(包含 Puppet 代码的文件)都位于 manifests 目录中。在我们的示例中,memcached类定义在manifests/init.pp文件中,该文件将自动导入。
在memcached类中,我们引用了memcached.conf文件:
file { '/etc/memcached.conf':
source => 'puppet:///modules/memcached/memcached.conf',
}
上述source参数告诉 Puppet 在以下位置查找文件:
MODULEPATH/ (/home/thomas/.puppet/modules)
└memcached/
└files/
└memcached.conf
还有更多内容…
学会喜爱模块,因为它们会让你的 Puppet 使用体验更轻松。它们并不复杂,然而,实践和经验会帮助你判断何时应该将事物组织到模块中,以及如何最好地安排模块结构。正如我们在接下来的两节中所见,模块不仅可以包含清单和文件。
模板
如果你需要将模板作为模块的一部分使用,将其放在模块的 templates 目录中,并按如下方式引用它:
file { '/etc/memcached.conf':
content => template('memcached/memcached.conf.erb'),
}
Puppet 会在以下位置查找文件:
MODULEPATH/memcached/templates/memcached.conf.erb
事实、函数、类型和提供者
模块还可以包含自定义事实、函数、类型和提供者。
有关这些内容的更多信息,请参见第九章,外部工具与 Puppet 生态系统。
第三方模块
你可以下载其他人提供的模块,并像使用自己创建的模块一样将其应用到自己的清单中。更多内容,请参见第七章中的《使用公共模块》食谱,管理应用程序。
模块组织
有关如何组织模块的更多细节,请参见 puppetlabs 网站:
docs.puppetlabs.com/puppet/3/reference/modules_fundamentals.html
另请参见
-
第九章中的创建自定义事实食谱,外部工具与 Puppet 生态系统
-
第七章中的使用公共模块食谱,管理应用程序
-
第九章中的创建你自己的资源类型食谱,外部工具与 Puppet 生态系统
-
第九章中的创建你自己的提供者食谱,外部工具与 Puppet 生态系统
使用标准命名约定
为模块和类选择适当且富有信息性的名称,在维护代码时会有很大帮助。如果其他人需要阅读并处理你的清单,这一点尤为重要。
如何做到这一点…
下面是一些关于如何命名清单中内容的建议:
-
按照它们管理的软件或服务来命名模块,例如,
apache或haproxy。 -
在模块内(子类)根据它们为模块提供的功能或服务命名类,例如,
apache::vhosts或rails::dependencies。 -
如果模块中的类禁用了该模块提供的服务,则应将其命名为
disabled。例如,禁用 Apache 的类应命名为apache::disabled。 -
创建一个角色和配置文件的模块层次结构。每个节点应具有一个由一个或多个配置文件组成的角色。每个配置文件模块应配置一个单独的服务。
-
管理用户的模块应命名为
user。 -
在用户模块中,将虚拟用户声明在
user::virtual类中(有关虚拟用户和其他资源的更多信息,请参见 第五章中的 使用虚拟资源 章节)。 -
在用户模块中,特定用户组的子类应以该组的名称命名,例如
user::sysadmins或user::contractors。 -
使用 Puppet 部署不同服务的配置文件时,应以服务名称命名文件,但要附加一个后缀,指示文件的类型,例如:
-
Apache 初始化脚本:
apache.init -
Rails 的 Logrotate 配置片段:
rails.logrotate -
我的 mywizzoapp 的 Nginx 虚拟主机文件:
mywizzoapp.vhost.nginx -
独立服务器的 MySQL 配置:
standalone.mysql
-
-
如果你需要根据操作系统版本部署不同版本的文件,例如,你可以使用如下的命名约定:
memcached.lucid.conf memcached.precise.conf -
你可以通过以下方式让 Puppet 自动选择适当的版本:
source = > "puppet:///modules/memcached /memcached.${::lsbdistrelease}.conf", -
如果你需要管理不同的 Ruby 版本,例如,按版本命名类,如
ruby192或ruby186。
还有更多…
Puppet 社区维护了一套关于 Puppet 基础设施的最佳实践指南,其中包括一些命名约定的提示:
docs.puppetlabs.com/guides/best_practices.html
有些人更喜欢通过使用逗号分隔的列表而不是单独的 include 语句将多个类包含到节点中,例如:
node 'server014' inherits 'server' {
include mail::server, repo::gem, repo::apt, zabbix
}
这是一种风格问题,但我更喜欢使用单独的 include 语句,每行一个,因为这样可以更方便地在节点之间复制和移动类的包含,而无需每次整理逗号和缩进。
我在前面的几个示例中提到了继承;如果你不确定这是什么意思,别担心,我将在下一章详细解释。
使用内联模板
模板是使用 嵌入式 Ruby (ERB) 动态构建配置文件的一种强大方式。你也可以通过调用 inline_template 函数直接使用 ERB 语法,而无需使用单独的文件。ERB 允许你使用条件逻辑、遍历数组并包含变量。
如何操作…
下面是如何使用 inline_template 的示例:
将你的 Ruby 代码传递给 Puppet 清单中的 inline_template,如下所示:
cron { 'chkrootkit':
command => '/usr/sbin/chkrootkit >
/var/log/chkrootkit.log 2>&1',
hour => inline_template('<%= @hostname.sum % 24 %>'),
minute => '00',
}
它是如何工作的…
传递给 inline_template 的字符串中的任何内容都会像 ERB 模板一样执行。也就是说,位于 <%= 和 %> 分隔符之间的内容会作为 Ruby 代码执行,其余部分则作为字符串处理。
在这个示例中,我们使用 inline_template 为每台机器计算不同的小时数,以便相同的作业不会在所有机器上同时运行。有关此技术的更多信息,请参见第六章中的 高效分发 cron 作业 例子,管理资源和文件。
还有更多……
在 ERB 代码中,无论是在模板文件内还是 inline_template 字符串中,你都可以通过在名称前加 @ 前缀直接访问 Puppet 变量(如果它们位于当前作用域或顶级作用域(事实)中):
<%= @fqdn %>
要引用另一个作用域中的变量,请使用 scope.lookupvar,如下所示:
<%= "The value of something from otherclass is " + scope.lookupvar('otherclass::something') %>
你应该谨慎使用内联模板。如果你确实需要在清单中使用一些复杂的逻辑,考虑使用自定义函数(参见第九章中的 创建自定义函数 例子,外部工具和 Puppet 生态系统)。
另请参见
-
在第四章中的 使用 ERB 模板 例子,与文件和包一起工作
-
在第四章中的 在模板中使用数组迭代 例子,与文件和包一起工作
遍历多个项
数组是 Puppet 中的一个强大功能;任何时候你需要对一组事物执行相同操作时,数组都可能派上用场。你只需将内容放入方括号中即可创建一个数组:
$lunch = [ 'franks', 'beans', 'mustard' ]
如何做到…
这里有一个常见的数组使用示例:
-
将以下代码添加到你的清单中:
$packages = [ 'ruby1.8-dev', 'ruby1.8', 'ri1.8', 'rdoc1.8', 'irb1.8', 'libreadline-ruby1.8', 'libruby1.8', 'libopenssl-ruby' ] package { $packages: ensure => installed } -
运行 Puppet,并注意到每个包现在应该已被安装。
它是如何工作的……
当 Puppet 遇到数组作为资源的名称时,它会为数组中的每个元素创建一个资源。在这个示例中,会为 $packages 数组中的每个包创建一个新的 package 资源,使用相同的参数(ensure => installed)。这是实例化多个相似资源的一种非常简洁的方式。
还有更多……
虽然数组在 Puppet 中可以帮你解决很多问题,但了解一种更灵活的数据结构:哈希,也是非常有用的。
使用哈希
哈希类似于数组,但每个元素都可以通过名称(称为键)存储并查找,例如(hash.pp):
$interface = {
'name' => 'eth0',
'ip' => '192.168.0.1',
'mac' => '52:54:00:4a:60:07'
}
notify { "(${interface['ip']}) at ${interface['mac']} on ${interface['name']}": }
当我们在此运行 Puppet 时,输出中会显示以下通知:
t@cookbook:~/.puppet/manifests$ puppet apply hash.pp
Notice: (192.168.0.1) at 52:54:00:4a:60:07 on etho
哈希值可以是你能够赋值给变量、字符串、函数调用、表达式,甚至是其他哈希或数组的任何内容。哈希非常适合存储关于某一特定事物的大量信息,因为通过使用键来访问哈希的每个元素,我们可以迅速找到我们要寻找的信息。
使用 split 函数创建数组
你可以使用方括号声明字面数组,如下所示:
define lunchprint() {
notify { "Lunch included ${name}":}": }
}
$lunch = ['egg', 'beans', 'chips']
lunchprint { $lunch: }
现在,当我们运行 Puppet 对前面的代码时,我们在输出中看到以下通知消息:
t@mylaptop ~ $ puppet apply lunchprint.pp
...
Notice: Lunch included chips
Notice: Lunch included beans
Notice: Lunch included egg
然而,Puppet 也可以使用 split 函数从字符串中为你创建数组,如下所示:
$menu = 'egg beans chips'
$items = split($menu, ' ')
lunchprint { $items: }
在新的清单上运行 puppet apply,我们在输出中看到相同的消息:
t@mylaptop ~ $ puppet apply lunchprint2.pp
...
Notice: Lunch included chips
Notice: Lunch included beans
Notice: Lunch included egg.
请注意,split函数接受两个参数:第一个参数是要分割的字符串。第二个参数是分割的字符;在此示例中是一个空格。当 Puppet 遍历字符串时,当它遇到空格时,会将其解释为一个项的结束和下一个项的开始。因此,给定字符串 'egg beans chips',它将被分割成三个项。
用于分割的字符可以是任何字符或字符串:
$menu = 'egg and beans and chips'
$items = split($menu, ' and ')
该字符也可以是一个正则表达式,例如,一组由 |(管道符)分隔的替代项:
$lunch = 'egg:beans,chips'
$items = split($lunch, ':|,')
编写强大的条件语句
Puppet 的 if 语句允许你根据变量或表达式的值改变清单的行为。使用它,你可以根据节点的某些事实(例如操作系统或内存大小)应用不同的资源或参数值。
你还可以在清单中设置变量,这些变量可以改变包含类的行为。例如,数据中心 A 中的节点可能需要使用不同的 DNS 服务器,而不是数据中心 B 中的节点,或者你可能需要为 Ubuntu 系统包含一组类,为其他系统包含另一组类。
如何做……
这里有一个有用的条件语句示例。将以下代码添加到你的清单中:
if $::timezone == 'UTC' {
notify { 'Universal Time Coordinated':}
} else {
notify { "$::timezone is not UTC": }
}
它是如何工作的……
Puppet 将 if 关键字后面的内容视为一个表达式并进行求值。如果该表达式求值为 true,Puppet 将执行大括号内的代码。
可选地,你可以添加一个 else 分支,当表达式求值为 false 时,它将被执行。
还有更多……
以下是一些使用 if 语句的额外技巧。
Elseif 分支
你可以使用 elseif 关键字添加进一步的测试,如下所示:
if $::timezone == 'UTC' {
notify { 'Universal Time Coordinated': }
} elseif $::timezone == 'GMT' {
notify { 'Greenwich Mean Time': }
} else {
notify { "$::timezone is not UTC": }
}
比较
你可以使用 == 语法检查两个值是否相等,正如我们示例中的那样:
if $::timezone == 'UTC' {
}
或者,你可以检查它们是否不相等,使用 !=:
if $::timezone != 'UTC' {
…
}
你还可以使用 < 和 > 比较数值:
if $::uptime_days > 365 {
notify { 'Time to upgrade your kernel!': }
}
要测试一个值是否大于(或小于)另一个值,使用 <= 或 >=:
if $::mtu_eth0 <= 1500 {
notify {"Not Jumbo Frames": }
}
组合表达式
你可以将前面描述的简单表达式组合成更复杂的逻辑表达式,使用 and、or 和 not:
if ($::uptime_days > 365) and ($::kernel == 'Linux') {
…
}
if ($role == 'webserver') and ( ($datacenter == 'A') or ($datacenter == 'B') ) {
…
}
另见
-
本章中的 使用 in 运算符 配方
-
本章中的 使用选择器和 case 语句 配方
在 if 语句中使用正则表达式
你还可以在 if 语句和其他条件语句中测试正则表达式。正则表达式是一种强大的方式,通过模式匹配来比较字符串。
如何做……
这是在条件语句中使用正则表达式的一个示例。将以下内容添加到你的清单中:
if $::architecture =~ /64/ {
notify { '64Bit OS Installed': }
} else {
notify { 'Upgrade to 64Bit': }
fail('Not 64 Bit')
}
它是如何工作的…
Puppet 将斜杠之间的文本视为正则表达式,指定要匹配的文本。如果匹配成功,if表达式将为真,因此第一个花括号之间的代码将被执行。在这个例子中,我们使用了正则表达式,因为不同的发行版对64bit的命名方式不同;有的使用amd64,而有的使用x86_64。我们唯一可以依赖的是事实中存在数字 64。一些包含版本号的事实会被 Puppet 视为字符串。例如,$::facterversion。在我的测试系统中,它是2.0.1,但当我尝试将它与2进行比较时,Puppet 无法完成比较:
Error: comparison of String with 2 failed at /home/thomas/.puppet/manifests/version.pp:1 on node cookbook.example.com
如果你想在文本不匹配时执行某些操作,使用!~而不是=~:
if $::kernel !~ /Linux/ {
notify { 'Not Linux, could be Windows, MacOS X, AIX, or ?': }
}
还有更多…
正则表达式非常强大,但可能难以理解和调试。如果你发现自己使用的正则表达式过于复杂,以至于一眼看不出它的作用,可以考虑简化设计,使其更容易理解。然而,正则表达式的一个特别有用的功能是能够捕获模式。
捕获模式
你不仅可以使用正则表达式匹配文本,还可以捕获匹配的文本并将其存储在变量中:
$input = 'Puppet is better than manual configuration'
if $input =~ /(.*) is better than (.*)/ {
notify { "You said '${0}'. Looks like you're comparing ${1}
to ${2}!": }
}
上述代码会产生以下输出:
你说过“Puppet 比手动配置更好”。看起来你正在将 Puppet 与手动配置进行比较!
变量$0存储整个匹配的文本(假设整体匹配成功)。如果你在正则表达式的任何部分加上括号,它会创建一个组,任何匹配的组也会存储在变量中。第一个匹配的组将是$1,第二个是$2,以此类推,如前面的示例所示。
正则表达式语法
Puppet 的正则表达式语法与 Ruby 相同,因此解释 Ruby 正则表达式语法的资源也将帮助你理解 Puppet。你可以在这个网站找到 Ruby 正则表达式语法的一个很好的介绍:
www.tutorialspoint.com/ruby/ruby_regular_expressions.htm。
另请参见
- 请参考本章中的使用正则表达式替换小节
使用选择器和case语句
虽然你可以使用if编写任何条件语句,但 Puppet 提供了几种额外的形式,帮助你更容易地表达条件:选择器和case语句。
如何操作…
以下是一些选择器和case语句的示例:
-
将以下代码添加到你的清单中:
$systemtype = $::operatingsystem ? { 'Ubuntu' => 'debianlike', 'Debian' => 'debianlike', 'RedHat' => 'redhatlike', 'Fedora' => 'redhatlike', 'CentOS' => 'redhatlike', default => 'unknown', } notify { "You have a ${systemtype} system": } -
将以下代码添加到你的清单中:
class debianlike { notify { 'Special manifest for Debian-like systems': } } class redhatlike { notify { 'Special manifest for RedHat-like systems': } } case $::operatingsystem { 'Ubuntu', 'Debian': { include debianlike } 'RedHat', 'Fedora', 'CentOS', 'Springdale': { include redhatlike } default: { notify { "I don't know what kind of system you have!": } } }
它是如何工作的…
我们的示例同时演示了选择器和case语句,接下来我们将详细了解它们是如何工作的。
选择器
在第一个示例中,我们使用了选择器(?操作符)来根据$::operatingsystem的值为$systemtype变量选择一个值。这类似于 C 或 Ruby 中的三元运算符,但你可以根据需要选择任意多个值,而不仅仅是两个可能的值。
Puppet 会将$::operatingsystem的值与我们提供的 Ubuntu、Debian 等可能的值进行比较。这些值可以是正则表达式(例如,用于部分字符串匹配或使用通配符),但在我们的示例中,我们只是使用了字面字符串。
一旦找到匹配项,选择器表达式将返回与匹配字符串相关联的任何值。如果$::operatingsystem的值是 Fedora,例如,选择器表达式将返回redhatlike字符串,并将其赋值给变量$systemtype。
Case 语句
与选择器不同,case语句不返回值。当你想根据某个表达式的值执行不同的代码时,case语句非常有用。在我们的第二个示例中,我们使用case语句根据$::operatingsystem的值包含debianlike或redhatlike类。
再次,Puppet 将$::operatingsystem的值与一系列潜在的匹配项进行比较。这些匹配项可以是正则表达式或字符串,或者像我们的例子一样,可以是用逗号分隔的字符串列表。当找到匹配项时,大括号中的相关代码会被执行。所以,如果$::operatingsystem的值是Ubuntu,则包括debianlike的代码将会执行。
还有更多…
一旦你掌握了选择器和case语句的基本用法,你可能会发现以下提示很有用。
正则表达式
与if语句一样,你可以在选择器和case语句中使用正则表达式,并且你还可以捕获匹配组的值,并使用$1、$2等引用它们:
case $::lsbdistdescription {
/Ubuntu (.+)/: {
notify { "You have Ubuntu version ${1}": }
}
/CentOS (.+)/: {
notify { "You have CentOS version ${1}": }
}
default: {}
}
默认值
选择器和case语句都允许你指定一个默认值,当其他选项都不匹配时,将选择默认值(样式指南建议你始终定义一个默认子句):
$lunch = 'Filet mignon.'
$lunchtype = $lunch ? {
/fries/ => 'unhealthy',
/salad/ => 'healthy',
default => 'unknown',
}
notify { "Your lunch was ${lunchtype}": }
输出如下:
t@mylaptop ~ $ puppet apply lunchtype.pp
Notice: Your lunch was unknown
Notice: /Stage[main]/Main/Notify[Your lunch was unknown]/message: defined 'message' as 'Your lunch was unknown'
当默认操作通常不应发生时,可以使用fail()函数停止 Puppet 的运行。
使用in操作符
in操作符测试一个字符串是否包含另一个字符串。这里有一个例子:
if 'spring' in 'springfield'
如果spring字符串是springfield的子字符串,则前面的表达式为真,实际上它确实是。in操作符也可以测试数组的成员资格,如下所示:
if $crewmember in ['Frank', 'Dave', 'HAL' ]
当in与哈希一起使用时,它测试字符串是否为哈希的键:
$ifaces = { 'lo' => '127.0.0.1',
'eth0' => '192.168.0.1' }
if 'eth0' in $ifaces {
notify { "eth0 has address ${ifaces['eth0']}": }
}
如何操作…
以下步骤将展示如何使用in操作符:
-
将以下代码添加到你的清单中:
if $::operatingsystem in [ 'Ubuntu', 'Debian' ] { notify { 'Debian-type operating system detected': } } elseif $::operatingsystem in [ 'RedHat', 'Fedora', 'SuSE', 'CentOS' ] { notify { 'RedHat-type operating system detected': } } else { notify { 'Some other operating system detected': } } -
运行 Puppet:
t@cookbook:~/.puppet/manifests$ puppet apply in.pp Notice: Compiled catalog for cookbook.example.com in environment production in 0.03 seconds Notice: Debian-type operating system detected Notice: /Stage[main]/Main/Notify[Debian-type operating system detected]/message: defined 'message' as 'Debian-type operating system detected' Notice: Finished catalog run in 0.02 seconds
还有更多…
in表达式的值是布尔值(真或假),因此你可以将其赋值给一个变量:
$debianlike = $::operatingsystem in [ 'Debian', 'Ubuntu' ]
if $debianlike {
notify { 'You are in a maze of twisty little packages, all alike': }
}
使用正则表达式替换
Puppet 的regsubst函数提供了一种简便的方式来操作文本、在字符串中搜索和替换表达式,或者从字符串中提取模式。我们通常需要使用从事实中获取的数据,或者从外部程序中获取数据来执行这些操作。
在本例中,我们将看到如何使用regsubst提取 IPv4 地址的前三个八位字节(假设它是一个/24类 C 地址,即网络部分)。
如何执行……
按照以下步骤构建示例:
-
将以下代码添加到你的清单中:
$class_c = regsubst($::ipaddress, '(.*)\..*', '\1.0') notify { "The network part of ${::ipaddress} is ${class_c}": } -
运行 Puppet:
t@cookbook:~/.puppet/manifests$ puppet apply ipaddress.pp Notice: Compiled catalog for cookbook.example.com in environment production in 0.02 seconds Notice: The network part of 192.168.122.148 is 192.168.122.0 Notice: /Stage[main]/Main/Notify[The network part of 192.168.122.148 is 192.168.122.0]/message: defined 'message' as 'The network part of 192.168.122.148 is 192.168.122.0' Notice: Finished catalog run in 0.03 seconds
它是如何工作的……
regsubst函数至少需要三个参数:源、模式和替换。在我们的示例中,我们将源字符串指定为$::ipaddress,在这台机器上,它如下所示:
192.168.122.148
我们按如下方式指定pattern函数:
(.*)\..*
我们按如下方式指定replacement函数:
\1.0
模式捕获字符串中的所有内容,直到最后一个句点(\.),并将其存储在\1变量中。然后我们匹配.*,它匹配字符串的所有内容,直到结尾,因此当我们将字符串的结尾替换为\1.0时,最终得到的仅是 IP 地址的网络部分,其值为以下内容:
192.168.122.0
当然,我们也可以通过其他方式得到相同的结果,包括以下方式:
$class_c = regsubst($::ipaddress, '\.\d+$', '.0')
在这里,我们只匹配最后一个八位字节,并将其替换为.0,这样就可以在不捕获的情况下实现相同的结果。
还有更多……
pattern函数可以是任何正则表达式,使用与if语句中正则表达式相同的(Ruby)语法。
另请参见
-
第三章中的导入动态信息方法,编写更好的清单
-
第三章中的获取环境信息方法,编写更好的清单
-
本章中的在 if 语句中使用正则表达式方法
使用未来解析器
Puppet 语言目前正在发展中;许多预期将在下一个主要版本(4)中包含的功能,如果启用未来解析器,已经可以使用。
准备就绪
-
确保已安装
rgengem。 -
在
puppet.conf的[main]部分中设置parser = future(对于开源 Puppet,请将其设置为/etc/puppet/puppet.conf,对于 Puppet Enterprise,请设置为/etc/puppetlabs/puppet/puppet.conf,对于以非 root 用户身份运行 Puppet 的用户,请设置为~/.puppet/puppet.conf)。 -
要临时使用未来解析器进行测试,请在命令行中使用
--parser=future。
如何执行……
许多实验性功能处理代码的评估方式,例如,在一个早期示例中,我们将$::facterversion事实的值与一个数字进行比较,但由于值被视为字符串,代码无法编译。使用未来解析器时,值会被转换,并且没有报告错误,如以下命令行输出所示:
t@cookbook:~/.puppet/manifests$ puppet apply --parser=future version.pp
Notice: Compiled catalog for cookbook.example.com in environment production in 0.36 seconds
Notice: Finished catalog run in 0.03 seconds
向数组添加元素并连接数组
你可以使用+运算符连接数组,或者使用<<运算符将它们附加到数组中。在以下示例中,我们使用三元运算符将特定的包名称分配给$apache变量。然后,我们使用<<运算符将该值附加到数组中:
$apache = $::osfamily ? {
'Debian' => 'apache2',
'RedHat' => 'httpd'
}
$packages = ['memcached'] << $apache
package {$packages: ensure => installed}
如果我们有两个数组,我们可以使用+运算符将这两个数组连接起来。在这个示例中,我们定义了一个系统管理员数组($sysadmins)和另一个应用程序所有者数组($appowners)。然后,我们可以将这两个数组连接起来,并作为允许用户的参数:
$sysadmins = [ 'thomas','john','josko' ]
$appowners = [ 'mike', 'patty', 'erin' ]
$users = $sysadmins + $appowners
notice ($users)
当我们应用这个清单时,我们看到这两个数组已经合并,如下所示的命令行输出:
t@cookbook:~/.puppet/manifests$ puppet apply --parser=future concat.pp Notice: [thomas, john, josko, mike, patty, erin]
Notice: Compiled catalog for cookbook.example.com in environment production in 0.36 seconds
Notice: Finished catalog run in 0.03 seconds
Merging Hashes
如果我们有两个哈希表,我们可以使用与数组相同的+运算符将它们合并。考虑我们之前示例中的$interfaces哈希表,我们可以向哈希表中添加另一个接口:
$iface = {
'name' => 'eth0',
'ip' => '192.168.0.1',
'mac' => '52:54:00:4a:60:07'
} + {'route' => '192.168.0.254'}
notice ($iface)
当我们应用这个清单时,我们看到路由属性已合并到哈希表中(您的结果可能不同,哈希表打印的顺序是不可预测的),如下所示:
t@cookbook:~/.puppet/manifests$ puppet apply --parser=future hash2.pp
Notice: {route => 192.168.0.254, name => eth0, ip => 192.168.0.1, mac => 52:54:00:4a:60:07}
Notice: Compiled catalog for cookbook.example.com in environment production in 0.36 seconds
Notice: Finished catalog run in 0.03 seconds
Lambda 函数
Lambda 函数是应用于数组或哈希表的迭代器。你会遍历数组或哈希表,并对数组的每个元素或哈希表的每个键应用如each、map、filter、reduce、slice等迭代函数。一些 Lambda 函数会返回计算后的数组或值;而如each之类的函数只会返回输入的数组或哈希表。
像map和reduce这样的 Lambda 函数使用临时变量,这些变量在 Lambda 函数完成后会被丢弃。Lambda 函数的使用最好通过示例来展示。在接下来的几个章节中,我们将展示每个 Lambda 函数的示例用法。
缩减
缩减用于将数组缩减为单一值。它可以用来计算数组的最大值或最小值,或者在这种情况下,计算数组元素的总和:
$count = [1,2,3,4,5]
$sum = reduce($count) | $total, $i | { $total + $i }
notice("Sum is $sum")
这段代码会计算$count数组的总和,并将其存储在$sum变量中,如下所示:
t@cookbook:~/.puppet/manifests$ puppet apply --parser future lambda.pp
Notice: Sum is 15
Notice: Compiled catalog for cookbook.example.com in environment production in 0.36 seconds
Notice: Finished catalog run in 0.03 seconds
过滤器
过滤器用于基于 Lambda 函数中的测试来过滤数组或哈希表。例如,以下是如何过滤我们的$count数组:
$filter = filter ($count) | $i | { $i > 3 } notice("Filtered array is $filter")
当我们应用这个清单时,我们看到结果中只有第 4 和第 5 个元素:
Notice: Filtered array is [4, 5]
映射
映射用于对数组的每个元素应用一个函数。例如,如果我们想(出于某些未知的原因)计算数组中所有元素的平方,我们会按如下方式使用map:
$map = map ($count) | $i | { $i * $i } notice("Square of array is $map")
应用这个清单的结果是一个新数组,其中原数组的每个元素都被平方(自乘),如下所示的命令行输出:
Notice: Square of array is [1, 4, 9, 16, 25]
切片
当你有相关的值按顺序存储在同一个数组中时,切片非常有用。例如,如果我们有防火墙的目的地和端口信息存储在一个数组中,我们可以将它们拆分成一对对的值,并对这些对进行操作:
$firewall_rules = ['192.168.0.1','80','192.168.0.10','443']
slice ($firewall_rules,2) |$ip, $port| { notice("Allow $ip on $port") }
应用后,这个清单会生成以下通知:
Notice: Allow 192.168.0.1 on 80
Notice: Allow 192.168.0.10 on 443
为了使这个例子更具实用性,请在切片块内创建一个新的防火墙资源,而不是使用notice:
slice ($firewall_rules,2) |$ip, $port| { firewall {"$port from $ip": dport => $port, source => "$ip", action => 'accept', }
}
Each
Each用于遍历数组的元素,但不像其他函数那样具有捕获结果的能力。Each是最简单的情况,通常用于对数组中的每个元素执行某些操作,如以下代码片段所示:
each ($count) |$c| { notice($c) }
如预期所示,它对$count数组中的每个元素执行notice,如下所示:
Notice: 1
Notice: 2
Notice: 3
Notice: 4
Notice: 5
其他特性
在使用未来解析器时,Puppet 语言还有其他新特性。一些特性提高了代码的可读性或紧凑性。欲了解更多信息,请参考 Puppetlabs 网站上的文档:docs.puppetlabs.com/puppet/latest/reference/experiments_future.html。
第二章:Puppet 基础设施
| “未来的计算机可能只有 1,000 个真空管,重量仅为 1.5 吨。” | ||
|---|---|---|
| --《Popular Mechanics》,1949 年 |
在本章中,我们将讨论:
-
安装 Puppet
-
使用 Git 管理你的清单
-
创建分散式 Puppet 架构
-
编写 papply 脚本
-
从 cron 运行 Puppet
-
使用 bash 引导 Puppet
-
创建集中式 Puppet 基础设施
-
创建具有多个 DNS 名称的证书
-
从 passenger 运行 Puppet
-
设置环境
-
配置 PuppetDB
-
配置 Hiera
-
使用 Hiera 设置节点特定数据
-
使用 hiera-gpg 存储机密数据
-
使用 MessagePack 序列化
-
使用 Git 钩子进行自动语法检查
-
使用 Git 推送代码
-
使用 Git 管理环境
介绍
在本章中,我们将介绍如何以集中式和分散式方式部署 Puppet。在每种方法中,我们将看到最佳实践、我的个人经验和社区解决方案的结合。
我们将配置并使用 PuppetDB 和 Hiera。PuppetDB 与导出的资源一起使用,我们将在第五章,用户与虚拟资源中进行详细讲解。Hiera 用于将变量数据与 Puppet 代码分离。
最后,我将介绍 Git,并展示如何使用 Git 来组织我们的代码和基础设施。
由于 Linux 发行版(如 Ubuntu、Red Hat 和 CentOS)在软件包名称、配置文件路径以及许多其他细节上有所不同,我决定出于篇幅和清晰度考虑,本书选择一个发行版(Debian 7,代号 Wheezy)并坚持使用它。然而,Puppet 可以在大多数流行的操作系统上运行,因此你应该能够轻松地将这些示例适配到你自己喜爱的操作系统和发行版上。
在本书写作时,Puppet 3.7.2 是最新的稳定版本,本书中使用的就是这一版本的 Puppet。Puppet 命令的语法变化较为频繁,因此请注意,虽然较旧版本的 Puppet 仍然可以正常使用,但它们可能不支持本书中描述的所有功能和语法。正如我们在第一章,Puppet 语言与风格中所见,未来的解析器展示了计划在 Puppet 4 版本中成为默认功能的语言特性。
安装 Puppet
在第一章,Puppet 语言与风格中,我们通过 gem 安装安装了 Puppet 作为 rubygem。在部署到多个节点时,这可能不是最佳方法。使用你所选择的发行版的包管理器,是保持所有节点上 Puppet 版本一致的最佳方法。Puppet Labs 为基于 APT 和 YUM 的发行版维护了软件仓库。
准备工作
如果你的 Linux 发行版使用 APT 作为包管理,访问 apt.puppetlabs.com/ 下载适合你发行版的 Puppet Labs 发布包。对于我们的 wheezy cookbook 节点,我们将使用 apt.puppetlabs.com/puppetlabs-release-wheezy.deb。
如果你使用的是一个使用 YUM 作为包管理的 Linux 发行版,访问 yum.puppetlabs.com/ 下载适合你发行版的 Puppet Labs 发布包。
如何操作...
-
一旦找到适合你发行版的 Puppet Labs 发布包,安装 Puppet 的步骤对于 APT 和 YUM 都是相同的:
-
安装 Puppet Labs 发布包
-
安装 Puppet 包
-
-
一旦安装了 Puppet,请按照下面的示例验证 Puppet 的版本:
t@ckbk:~ puppet --version 3.7.2
现在我们已经有了在节点上安装 Puppet 的方法,接下来需要关注的是如何保持 Puppet 清单的有序管理。在接下来的章节中,我们将展示如何使用 Git 来保持代码的组织性和一致性。
使用 Git 管理你的清单
将 Puppet 清单放入版本控制系统(如 Git 或 Subversion)是一个非常好的主意(Git 是 Puppet 的事实标准)。这样做有几个优势:
-
你可以撤销更改,并恢复到任何先前的清单版本
-
你可以使用分支来尝试新特性
-
如果多人需要修改清单,他们可以在各自的工作副本中独立进行更改,然后再合并更改。
-
你可以使用
git log功能查看更改的内容,以及更改发生的时间(和更改者)
准备工作
在这一部分中,我们将把你现有的清单文件导入到 Git 中。如果你在之前的部分中创建了 Puppet 目录,请使用该目录,否则使用你现有的清单目录。
在本示例中,我们将在一个所有节点都能访问的服务器上创建一个新的 Git 仓库。为了将代码托管在 Git 仓库中,我们需要执行几个步骤:
-
在中央服务器上安装 Git。
-
创建一个用户来运行 Git 并拥有仓库。
-
创建一个仓库来存放代码。
-
创建 SSH 密钥以允许基于密钥的访问仓库。
-
在节点上安装 Git 并从我们的 Git 仓库下载最新版本。
如何操作...
按照以下步骤操作:
-
首先,在你的 Git 服务器(在我们的示例中是
git.example.com)上安装 Git。最简单的方式是使用 Puppet。创建以下清单,命名为git.pp:package {'git': ensure => installed } -
使用
puppet apply git.pp应用此清单,这将安装 Git。 -
接下来,创建一个 Git 用户,供节点登录并获取最新代码。我们仍然使用 Puppet 来完成这个操作。我们还将创建一个目录来存放我们的仓库(
/home/git/repos),如下面的代码片段所示:group { 'git': gid => 1111, } user {'git': uid => 1111, gid => 1111, comment => 'Git User', home => '/home/git', require => Group['git'], } file {'/home/git': ensure => 'directory', owner => 1111, group => 1111, require => User['git'], } file {'/home/git/repos': ensure => 'directory', owner => 1111, group => 1111, require => File['/home/git'] } -
应用该清单后,作为 Git 用户登录,并使用以下命令创建一个空的 Git 仓库:
# sudo -iu git git@git $ cd repos git@git $ git init --bare puppet.git Initialized empty Git repository in /home/git/repos/puppet.git/ -
为 Git 用户设置密码,我们将在下一步后远程登录:
[root@git ~]# passwd git Changing password for user git. New password: Retype new password: passwd: all authentication tokens updated successfully. -
现在回到本地机器,创建一个
ssh密钥供我们的节点用来更新仓库:t@mylaptop ~ $ cd .ssh t@mylaptop ~/.ssh $ ssh-keygen -b 4096 -f git_rsa Generating public/private rsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in git_rsa. Your public key has been saved in git_rsa.pub. The key fingerprint is: 87:35:0e:4e:d2:96:5f:e4:ce:64:4a:d5:76:c8:2b:e4 thomas@mylaptop -
现在,将新创建的公钥复制到
authorized_keys文件中。这将允许我们使用这个新密钥连接到 Git 服务器:t@mylaptop ~/.ssh $ ssh-copy-id -i git_rsa git@git.example.com git@git.example.com's password: Number of key(s) added: 1 -
现在尝试使用以下命令登录到机器:"ssh 'git@git.example.com'",并检查确保只有你希望添加的密钥被加入。
-
接下来,配置
ssh在访问 Git 服务器时使用你的密钥,并将以下内容添加到你的~/.ssh/config文件中:Host git git.example.com User git IdentityFile /home/thomas/.ssh/git_rsa -
将仓库克隆到你的机器上,放入一个名为 Puppet 的目录中(如果你没有使用
git.example.com,请替换为你的服务器名称):t@mylaptop ~$ git clone git@git.example.com:repos/puppet.git Cloning into 'puppet'... warning: You appear to have cloned an empty repository. Checking connectivity... done.我们已经创建了一个 Git 仓库;在我们向仓库提交任何更改之前,最好在 Git 中设置你的名字和电子邮件。你的名字和电子邮件将附加到你做的每个提交上。
-
当你在一个大型团队中工作时,知道是谁做了哪些更改非常重要;为此,请使用以下代码片段:
t@mylaptop puppet$ git config --global user.email"thomas@narrabilis.com" t@mylaptop puppet$ git config --global user.name "ThomasUphill" -
你可以使用以下代码片段验证你的 Git 设置:
t@mylaptop ~$ git config --global --list user.name=Thomas Uphill user.email=thomas@narrabilis.com core.editor=vim merge.tool=vimdiff color.ui=true push.default=simple -
现在我们已经正确配置了 Git,切换到你的仓库目录,并按照以下代码片段创建一个新的站点清单:
t@mylaptop ~$ cd puppet t@mylaptop puppet$ mkdir manifests t@mylaptop puppet$ vim manifests/site.pp node default { include base } -
这个站点清单将在每个节点上安装我们的基础类;我们将像在第一章中一样使用 Puppet 模块创建基础类,Puppet 语言与风格:
t@mylaptop puppet$ mkdir modules t@mylaptop puppet$ cd modules t@mylaptop modules$ puppet module generate thomas-base Notice: Generating module at /home/tuphill/puppet/modules/thomas-base thomas-base thomas-base/Modulefile thomas-base/README thomas-base/manifests thomas-base/manifests/init.pp thomas-base/spec thomas-base/spec/spec_helper.rb thomas-base/tests thomas-base/tests/init.pp t@mylaptop modules$ ln -s thomas-base base -
最后一步,我们在
thomas-base目录和base之间创建一个符号链接。现在,为了确保我们的模块能够执行有用的操作,请在thomas-base/manifests/init.pp中定义的base类的主体中添加以下内容:class base { file {'/etc/motd': content => "${::fqdn}\nManaged by puppet ${::puppetversion}\n" } } -
现在使用
git add和git commit将新的基础模块和站点清单添加到 Git,如下所示:t@mylaptop modules$ cd .. t@mylaptop puppet$ git add modules manifests t@mylaptop puppet$ git status On branch master Initial commit Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: manifests/site.pp new file: modules/base new file: modules/thomas-base/Modulefile new file: modules/thomas-base/README new file: modules/thomas-base/manifests/init.pp new file: modules/thomas-base/spec/spec_helper.rb new file: modules/thomas-base/tests/init.pp t@mylaptop puppet$ git commit -m "Initial commit with simple base module" [master (root-commit) 3e1f837] Initial commit with simple base module 7 files changed, 102 insertions(+) create mode 100644 manifests/site.pp create mode 120000 modules/base create mode 100644 modules/thomas-base/Modulefile create mode 100644 modules/thomas-base/README create mode 100644 modules/thomas-base/manifests/init.pp create mode 100644 modules/thomas-base/spec/spec_helper.rb create mode 100644 modules/thomas-base/tests/init.pp -
目前,你对 Git 仓库的更改已经在本地提交;你现在需要将这些更改推送到
git.example.com,以便其他节点可以获取更新的文件:t@mylaptop puppet$ git push origin master Counting objects: 15, done. Delta compression using up to 4 threads. Compressing objects: 100% (9/9), done. Writing objects: 100% (15/15), 2.15 KiB | 0 bytes/s, done. Total 15 (delta 0), reused 0 (delta 0) To git@git.example.com:repos/puppet.git * [new branch] master -> master
它是如何工作的...
Git 跟踪文件更改,并存储所有更改的完整历史记录。仓库的历史由提交(commits)构成。一个提交代表某个特定时间点仓库的状态,你可以通过git commit命令创建并附加一条信息来标注提交。
你现在已经将 Puppet 清单文件添加到仓库并创建了第一个提交。这更新了仓库的历史记录,但仅在你本地的工作副本中。要将更改与git.example.com副本同步,git push命令会推送自上次同步以来所做的所有更改。
还有更多内容...
既然你已经有了一个中央的 Git 仓库来存储 Puppet 清单,你就可以在不同的位置检出多个副本,并在提交更改之前进行工作。例如,如果你在一个团队中工作,每个成员可以有自己本地的仓库副本,并通过中央服务器与其他成员同步更改。你也可以选择使用 GitHub 作为你的中央 Git 仓库服务器。GitHub 为公开仓库提供免费的 Git 仓库托管,如果你不希望 Puppet 代码公开,可以支付 GitHub 的高级服务费用。
在接下来的章节中,我们将使用我们的 Git 仓库来配置集中式和去中心化的 Puppet 配置。
创建一个去中心化的 Puppet 架构
Puppet 是一个配置管理工具。你可以使用 Puppet 配置并防止大量客户端计算机出现配置漂移。如果你的所有客户端计算机都可以通过一个中心位置轻松访问,你可以选择让中央 Puppet 服务器控制所有客户端计算机。在集中式模型中,Puppet 服务器被称为 Puppet 主控。我们将在接下来的几节中介绍如何配置一个中央 Puppet 主控。
如果你的客户端计算机分布广泛,或者无法保证客户端计算机与中心位置之间的通信,那么去中心化架构可能适合你的部署。在接下来的几节中,我们将看到如何配置一个去中心化的 Puppet 架构。
正如我们所见,我们可以直接在清单文件上运行 puppet apply 命令来让 Puppet 应用它。这个方法的问题是我们需要将清单传输到客户端计算机上。
我们可以使用上一节中创建的 Git 仓库,将我们的清单传输到我们创建的每个新节点。
准备工作
创建一个新的测试节点,给这个新节点命名,你可以随意命名,我这里用 testnode。按照之前的方法在机器上安装 Puppet。
如何操作...
创建一个 bootstrap.pp 清单,该清单将在我们的新节点上执行以下配置步骤:
-
安装 Git:
package {'git': ensure => 'installed' } -
在 Puppet 用户的主目录(
/var/lib/puppet/.ssh/id_rsa)中安装ssh密钥,以访问git.example.com:File { owner => 'puppet', group => 'puppet', } file {'/var/lib/puppet/.ssh': ensure => 'directory', } file {'/var/lib/puppet/.ssh/id_rsa': content => " -----BEGIN RSA PRIVATE KEY----- … NIjTXmZUlOKefh4MBilqUU3KQG8GBHjzYl2TkFVGLNYGNA0U8VG8SUJq -----END RSA PRIVATE KEY----- ", mode => 0600, require => File['/var/lib/puppet/.ssh'] } -
从
git.example.com下载ssh主机密钥(/var/lib/puppet/.ssh/known_hosts):exec {'download git.example.com host key': command => 'sudo -u puppet ssh-keyscan git.example.com >> /var/lib/puppet/.ssh/known_hosts', path => '/usr/bin:/usr/sbin:/bin:/sbin', unless => 'grep git.example.com /var/lib/puppet/.ssh/known_hosts', require => File['/var/lib/puppet/.ssh'], } -
创建一个目录来存放 Git 仓库(
/etc/puppet/cookbook):file {'/etc/puppet/cookbook': ensure => 'directory', } -
将 Puppet 仓库克隆到新机器上:
exec {'create cookbook': command => 'sudo -u puppet git clone git@git.example.com:repos/puppet.git /etc/puppet/cookbook', path => '/usr/bin:/usr/sbin:/bin:/sbin', require => [Package['git'],File['/var/lib/puppet/.ssh/id_rsa'],Exec['download git.example.com host key']], unless => 'test -f /etc/puppet/cookbook/.git/config', } -
现在,当我们在新机器上运行 Puppet apply 时,
ssh密钥将为 Puppet 用户安装。然后,Puppet 用户会将 Git 仓库克隆到/etc/puppet/cookbook:root@testnode /tmp# puppet apply bootstrap.pp Notice: Compiled catalog for testnode.example.com in environment production in 0.40 seconds Notice: /Stage[main]/Main/File[/etc/puppet/cookbook]/ensure: created Notice: /Stage[main]/Main/File[/var/lib/puppet/.ssh]/ensure: created Notice: /Stage[main]/Main/Exec[download git.example.com host key]/returns: executed successfully Notice: /Stage[main]/Main/File[/var/lib/puppet/.ssh/id_rsa]/ensure: defined content as '{md5}da61ce6ccc79bc6937bd98c798bc9fd3' Notice: /Stage[main]/Main/Exec[create cookbook]/returns: executed successfully Notice: Finished catalog run in 0.82 seconds注意
你可能需要禁用
sudo的tty要求。如果/etc/sudoers文件中有Defaults requiretty这一行,请将其注释掉。或者,你可以在
'create cookbook' exec类型中设置user => Puppet。请注意,使用 user 属性会导致命令的任何错误信息丢失。 -
现在,你的 Puppet 代码已经可以在新节点上使用,你可以通过
puppet apply来应用它,并指定/etc/puppet/cookbook/modules包含这些模块:root@testnode ~# puppet apply --modulepath=/etc/puppet/cookbook/modules /etc/puppet/cookbook/manifests/site.pp Notice: Compiled catalog for testnode.example.com in environment production in 0.12 seconds Notice: /Stage[main]/Base/File[/etc/motd]/content: content changed '{md5}86d28ff83a8d49d349ba56b5c64b79ee' to '{md5}4c4c3ab7591d940318279d78b9c51d4f' Notice: Finished catalog run in 0.11 seconds root@testnode /tmp# cat /etc/motd testnode.example.com Managed by puppet 3.6.2
它是如何工作的...
首先,我们的 bootstrap.pp 清单确保 Git 已安装。该清单接着确保 Git 用户在 git.example.com 上的 ssh 密钥已安装到 Puppet 用户的主目录(默认是 /var/lib/puppet)。接下来,清单确保 git.example.com 的主机密钥被 Puppet 用户信任。配置好 ssh 后,启动脚本确保 /etc/puppet/cookbook 存在并且是一个目录。
然后我们使用 exec 命令让 Git 克隆仓库到 /etc/puppet/cookbook。所有代码就绪后,我们再次调用 puppet apply 来部署仓库中的代码。在生产环境中,你可以将 bootstrap.pp 清单分发到所有节点,可能通过内部 web 服务器,使用类似 curl [puppet/bootstrap.pp >bootstrap.pp && puppet apply bootstrap.pp](http://puppet/bootstrap.pp >bootstrap.pp && puppet apply bootstrap.pp) 的方式。
编写一个 papply 脚本
我们希望尽可能快速和简单地在机器上应用 Puppet;为此,我们将编写一个小脚本,将 puppet apply 命令与它所需的参数封装在一起。我们将使用 Puppet 本身在需要的地方部署这个脚本。
如何操作...
按照以下步骤操作:
-
在你的 Puppet 仓库中,创建 Puppet 模块所需的目录:
t@mylaptop ~$ cd puppet/modules t@mylaptop modules$ mkdir -p puppet/{manifests,files} -
创建
modules/puppet/files/papply.sh文件,内容如下:#!/bin/sh sudo puppet apply /etc/puppet/cookbook/manifests/site.pp \--modulepath=/etc/puppet/cookbook/modules $* -
创建
modules/puppet/manifests/init.pp文件,内容如下:class puppet { file { '/usr/local/bin/papply': source => 'puppet:///modules/puppet/papply.sh', mode => '0755', } } -
按如下方式修改你的
manifests/site.pp文件:node default { include base include puppet } -
将 Puppet 模块添加到 Git 仓库并提交更改,如下所示:
t@mylaptop puppet$ git add manifests/site.pp modules/puppet t@mylaptop puppet$ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: manifests/site.pp new file: modules/puppet/files/papply.sh new file: modules/puppet/manifests/init.pp t@mylaptop puppet$ git commit -m "adding puppet module to include papply" [master 7c2e3d5] adding puppet module to include papply 3 files changed, 11 insertions(+) create mode 100644 modules/puppet/files/papply.sh create mode 100644 modules/puppet/manifests/init.pp -
现在记得将更改推送到
git.example.com上的 Git 仓库:t@mylaptop puppet$ git push origin master Counting objects: 14, done. Delta compression using up to 4 threads. Compressing objects: 100% (7/7), done. Writing objects: 100% (10/10), 894 bytes | 0 bytes/s, done. Total 10 (delta 0), reused 0 (delta 0) To git@git.example.com:repos/puppet.git 23e887c..7c2e3d5 master -> master -
如下所示,拉取 Git 仓库的最新版本到你的新节点(对我来说是
testnode):root@testnode ~# sudo -iu puppet puppet@testnode ~$ cd /etc/puppet/cookbook/puppet@testnode /etc/puppet/cookbook$ git pull origin master remote: Counting objects: 14, done. remote: Compressing objects: 100% (7/7), done. remote: Total 10 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (10/10), done. From git.example.com:repos/puppet * branch master -> FETCH_HEAD Updating 23e887c..7c2e3d5 Fast-forward manifests/site.pp | 1 + modules/puppet/files/papply.sh | 4 ++++ modules/puppet/manifests/init.pp | 6 ++++++ 3 files changed, 11 insertions(+), 0 deletions(-) create mode 100644 modules/puppet/files/papply.sh create mode 100644 modules/puppet/manifests/init.pp -
手动应用清单一次以安装
papply脚本:root@testnode ~# puppet apply /etc/puppet/cookbook/manifests/site.pp --modulepath /etc/puppet/cookbook/modules Notice: Compiled catalog for testnode.example.com in environment production in 0.13 seconds Notice: /Stage[main]/Puppet/File[/usr/local/bin/papply]/ensure: defined content as '{md5}d5c2cdd359306dd6e6441e6fb96e5ef7' Notice: Finished catalog run in 0.13 seconds -
最后,测试脚本:
root@testnode ~# papply Notice: Compiled catalog for testnode.example.com in environment production in 0.13 seconds Notice: Finished catalog run in 0.09 seconds
现在,每当你需要运行 Puppet 时,只需运行 papply。将来,当我们应用 Puppet 更改时,我会让你运行 papply 而不是完整的 puppet apply 命令。
它是如何工作的...
正如你所见,为了在机器上运行 Puppet 并应用指定的清单文件,我们使用 puppet apply 命令:
puppet apply manifests/site.pp
当你使用模块时(例如我们刚才创建的 Puppet 模块),你还需要告诉 Puppet 在哪里查找模块,使用 modulepath 参数:
puppet apply manifests/nodes.pp \--modulepath=/home/ubuntu/puppet/modules
为了以所需的根权限运行 Puppet,我们必须在所有命令前加上 sudo:
sudo puppet apply manifests/nodes.pp \--modulepath=/home/ubuntu/puppet/modules
最后,传递给 papply 的任何附加参数将通过 $* 参数传递给 Puppet 本身:
sudo puppet apply manifests/nodes.pp \--modulepath=/home/ubuntu/puppet/modules $*
这需要大量的输入,因此将其放入脚本中是合理的。我们添加了一个 Puppet 文件资源,将脚本部署到 /usr/local/bin 并使其可执行:
file { '/usr/local/bin/papply': source => 'puppet:///modules/puppet/papply.sh', mode => '0755',}
最后,我们在默认节点声明中包含了 Puppet 模块:
node default {
include base
include puppet
}
你可以对任何其他由 Puppet 管理的节点做同样的操作。
从 cron 运行 Puppet
你可以利用现有的设置做很多事情:作为团队共同处理 Puppet 清单,通过中央 Git 仓库通信更改,并使用 papply 脚本手动在机器上应用它们。
然而,你仍然需要登录到每台机器上以更新 Git 仓库并重新运行 Puppet。拥有每台机器自动更新并应用更改的功能会很有帮助。然后你只需推送更改到仓库,它会在一定时间内自动传送到所有机器。
最简单的方法是使用 cron 任务定期从仓库拉取更新,并在有任何更改时运行 Puppet。
准备工作
你将需要我们在使用 Git 管理清单和创建去中心化的 Puppet 架构食谱中设置的 Git 仓库,以及来自编写 papply 脚本食谱的 papply 脚本。你需要应用我们创建的 bootstrap.pp 清单,以安装 ssh 密钥并下载最新的仓库。
如何操作...
按照以下步骤操作:
-
将
bootstrap.pp脚本复制到任何你希望注册的节点。bootstrap.pp清单包括用于访问 Git 仓库的私钥,生产环境中应该保护该密钥。 -
创建
modules/puppet/files/pull-updates.sh文件,内容如下:#!/bin/sh cd /etc/puppet/cookbook sudo –u puppet git pull && /usr/local/bin/papply -
修改
modules/puppet/manifests/init.pp文件,在papply文件定义后添加以下代码片段:file { '/usr/local/bin/pull-updates': source => 'puppet:///modules/puppet/pull-updates.sh', mode => '0755', } cron { 'run-puppet': ensure => 'present', user => 'puppet', command => '/usr/local/bin/pull-updates', minute => '*/10', hour => '*', } -
如之前所示,提交更改并推送到 Git 服务器,命令行如下:
t@mylaptop puppet$ git add modules/puppet t@mylaptop puppet$ git commit -m "adding pull-updates" [master 7e9bac3] adding pull-updates 2 files changed, 14 insertions(+) create mode 100644 modules/puppet/files/pull-updates.sh t@mylaptop puppet$ git push Counting objects: 14, done. Delta compression using up to 4 threads. Compressing objects: 100% (7/7), done. Writing objects: 100% (8/8), 839 bytes | 0 bytes/s, done. Total 8 (delta 0), reused 0 (delta 0) To git@git.example.com:repos/puppet.git 7c2e3d5..7e9bac3 master -> master -
在测试节点上发出 Git pull 命令:
root@testnode ~# cd /etc/puppet/cookbook/ root@testnode /etc/puppet/cookbook# sudo –u puppet git pull remote: Counting objects: 14, done. remote: Compressing objects: 100% (7/7), done. remote: Total 8 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (8/8), done. From git.example.com:repos/puppet 23e887c..7e9bac3 master -> origin/master Updating 7c2e3d5..7e9bac3 Fast-forward modules/puppet/files/pull-updates.sh | 3 +++ modules/puppet/manifests/init.pp | 11 +++++++++++ 2 files changed, 14 insertions(+), 0 deletions(-) create mode 100644 modules/puppet/files/pull-updates.sh -
在测试节点上运行 Puppet:
root@testnode /etc/puppet/cookbook# papply Notice: Compiled catalog for testnode.example.com in environment production in 0.17 seconds Notice: /Stage[main]/Puppet/Cron[run-puppet]/ensure: created Notice: /Stage[main]/Puppet/File[/usr/local/bin/pull-updates]/ensure: defined content as '{md5}04c023feb5d566a417b519ea51586398' Notice: Finished catalog run in 0.16 seconds -
检查
pull-updates脚本是否正常工作:root@testnode /etc/puppet/cookbook# pull-updates Already up-to-date. Notice: Compiled catalog for testnode.example.com in environment production in 0.15 seconds Notice: Finished catalog run in 0.14 seconds -
验证
cron任务是否成功创建:root@testnode /etc/puppet/cookbook# crontab -l -u puppet # HEADER: This file was autogenerated at Tue Sep 09 02:31:00 -0400 2014 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: run-puppet */10 * * * * /usr/local/bin/pull-updates
它是如何工作的...
当我们创建 bootstrap.pp 清单时,我们确保 Puppet 用户可以使用 ssh 密钥签出 Git 仓库。这使得 Puppet 用户可以在食谱目录中无人值守地运行 Git pull。我们还添加了 pull-updates 脚本,它会执行此操作,并在拉取到任何更改时运行 Puppet:
#!/bin/sh
cd /etc/puppet/cookbook
sudo –u puppet git pull && papply
我们通过 Puppet 将此脚本部署到节点上:
file { '/usr/local/bin/pull-updates':
source => 'puppet:///modules/puppet/pull-updates.sh',
mode => '0755',
}
最后,我们创建了一个 cron 任务,它定期(每 10 分钟,但如果需要可以更改)运行 pull-updates:
cron { 'run-puppet':
ensure => 'present',
command => '/usr/local/bin/pull-updates',
minute => '*/10',
hour => '*',
}
还有更多...
恭喜,你现在拥有一个完全自动化的 Puppet 基础架构!一旦你应用了 bootstrap.pp 清单,运行 Puppet 在仓库中;机器将设置为拉取任何新的更改并自动应用它们。
比如,如果你想为所有机器添加一个新的用户账户,你需要做的就是在工作副本的清单中添加该账户,然后提交并推送到中央 Git 仓库。在 10 分钟内,它会自动应用到所有运行 Puppet 的机器上。
使用 bash 启动 Puppet
本书的早期版本使用 Rakefiles 来启动 Puppet。使用 Rake 配置节点的问题在于,您是从笔记本电脑上运行命令;您假设已经有 ssh 访问该机器的权限。大多数启动过程通过在节点已被配置后执行一个容易记住的命令来完成。在本节中,我们将展示如何使用 bash 和 web 服务器以及启动脚本来启动 Puppet。
准备工作
在一个中心可访问的服务器上安装 httpd,并创建一个受密码保护的区域来存储启动脚本。在我的示例中,我将使用之前设置的 Git 服务器 git.example.com。首先在 web 服务器的根目录下创建一个目录:
# cd /var/www/html
# mkdir bootstrap
现在执行以下步骤:
-
将以下位置定义添加到您的 apache 配置中:
<Location /bootstrap> AuthType basic AuthName "Bootstrap" AuthBasicProvider file AuthUserFile /var/www/puppet.passwd Require valid-user </Location> -
重新加载您的 web 服务器以确保位置配置生效。使用 curl 验证您无法在没有身份验证的情况下从 bootstrap 目录下载:
[root@bootstrap-test tmp]# curl http://git.example.com/bootstrap/ <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>401 Authorization Required</title> </head><body> <h1>Authorization Required</h1> -
创建您在 apache 配置中引用的密码文件(
/var/www/puppet.passwd):root@git# cd /var/www root@git# htpasswd –cb puppet.passwd bootstrap cookbook Adding password for user bootstrap -
验证用户名和密码是否允许访问 bootstrap 目录,如下所示:
[root@node1 tmp]# curl --user bootstrap:cookbook http://git.example.com/bootstrap/ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <html> <head> <title>Index of /bootstrap</title>
如何操作...
现在,您有了一个安全的位置来存储启动脚本,在 bootstrap 目录中为您支持的每个操作系统创建一个启动脚本。在这个例子中,我将展示如何为基于 Red Hat Enterprise Linux 6 的发行版创建启动脚本。
提示
尽管 bootstrap 位置需要密码保护,但由于我们没有在服务器上配置 SSL,因此没有加密。没有加密的话,这个位置并不太安全。
在 bootstrap 目录中创建一个名为el6.sh的脚本,内容如下:
#!/bin/bash
# bootstrap for EL6 distributions
SERVER=git.example.com
LOCATION=/bootstrap
BOOTSTRAP=bootstrap.pp
USER=bootstrap
PASS=cookbook
# install puppet
curl http://yum.puppetlabs.com/RPM-GPG-KEY-puppetlabs >/etc/pki/rpm-gpg/RPM-GPG-KEY-puppetlabs
rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-puppetlabs
yum -y install http://yum.puppetlabs.com/puppetlabs-release-el-6.noarch.rpm
yum -y install puppet
# download bootstrap
curl --user $USER:$PASS http://$SERVER/$LOCATION/$BOOTSTRAP >/tmp/$BOOTSTRAP
# apply bootstrap
cd /tmp
puppet apply /tmp/$BOOTSTRAP
# apply puppet
puppet apply --modulepath /etc/puppet/cookbook/modules /etc/puppet/cookbook/manifests/site.pp
工作原理...
Apache 配置仅允许使用用户名和密码组合访问 bootstrap 目录。我们通过将--user参数传递给 curl 来提供这些信息,从而获得对文件的访问权限。我们使用管道符号(|)将 curl 的输出重定向到 bash,这样 bash 就会执行该脚本。我们像编写任何其他 bash 脚本一样编写我们的 bash 脚本。该脚本下载我们的bootstrap.pp清单并应用它。最后,我们从 Git 仓库应用 Puppet 清单,并将机器配置为我们去中心化基础设施的一部分。
更多内容...
要支持另一个操作系统,我们只需要创建一个新的 bash 脚本。所有的 Linux 发行版都支持 bash 脚本,Mac OS X 也支持。由于我们将大部分逻辑放入了bootstrap.pp清单中,因此启动脚本非常简洁,且易于移植到新的操作系统。
创建一个集中式 Puppet 基础设施
像 Puppet 这样的配置管理工具在管理大量机器时最为有效。如果所有机器都能连接到一个中心位置,使用集中式的 Puppet 基础设施可能是一个好方案。不幸的是,Puppet 在节点数量较多时扩展性较差。如果你的部署有不到 800 台服务器,假设你的目录不复杂(每个目录编译时间少于 10 秒),单一的 Puppet 主服务应该能够处理这个负载。如果你的节点数量更大,我建议参考 Mastering Puppet 中描述的负载均衡配置,Thomas Uphill 著,Packt Publishing 出版。
Puppet 主服务是一个 Puppet 服务器,充当 Puppet 的 X509 证书颁发机构,并将编译后的清单(catalogs)分发给客户端节点。Puppet 配带了一个内置的 Web 服务器 WEBrick,可以处理非常少量的节点。在本节中,我们将看到如何使用该内置服务器来控制少量(少于 10)节点。
准备就绪
Puppet 主服务进程通过运行 puppet master 启动;大多数 Linux 发行版将 Puppet 主服务的启动和停止脚本放在一个单独的包中。为了开始,我们将创建一台名为 puppet.example.com 的新的 Debian 服务器。
如何操作...
-
在新服务器上安装 Puppet,然后使用 Puppet 来安装 Puppet 主服务包:
# puppet resource package puppetmaster ensure='installed' Notice: /Package[puppetmaster]/ensure: created package { 'puppetmaster': ensure => '3.7.0-1puppetlabs1', } -
现在启动 Puppet 主服务,并确保它在启动时自动启动:
# puppet resource service puppetmaster ensure=true enable=true service { 'puppetmaster': ensure => 'running', enable => 'true', }
它是如何工作的...
Puppet 主服务包包括启动和停止 Puppet 主服务的脚本。我们使用 Puppet 来安装这个包并启动服务。一旦服务启动,我们可以将另一个节点指向 Puppet 主服务(你可能需要禁用机器上的基于主机的防火墙)。
-
从另一个节点运行
puppet agent启动一个puppet agent,它将联系服务器并请求新的证书:t@ckbk:~$ sudo puppet agent -t Info: Creating a new SSL key for cookbook.example.com Info: Caching certificate for ca Info: Creating a new SSL certificate request for cookbook.example.com Info: Certificate Request fingerprint (SHA256): 06:C6:2B:C4:97:5D:16:F2:73:82:C4:A9:A7:B1:D0:95:AC:69:7B:27:13:A9:1A:4C:98:20:21:C2:50:48:66:A2 Info: Caching certificate for ca Exiting; no certificate found and waitforcert is disabled -
现在在 Puppet 服务器上,签署新的密钥:
root@puppet:~# puppet cert list pu "cookbook.example.com" (SHA256) 06:C6:2B:C4:97:5D:16:F2:73:82:C4:A9:A7:B1:D0:95:AC:69:7B:27:13:A9:1A:4C:98:20:21:C2:50:48:66:A2 root@puppet:~# puppet cert sign cookbook.example.com Notice: Signed certificate request for cookbook.example.com Notice: Removing file Puppet::SSL::CertificateRequestcookbook.example.com at'/var/lib/puppet/ssl/ca/requests/cookbook.example.com.pem' -
返回到烹饪书节点,并再次运行 Puppet:
t@ckbk:~$ sudo puppet agent –vt Info: Caching certificate for cookbook.example.com Info: Caching certificate_revocation_list for ca Info: Caching certificate for cookbook.example.comInfo: Retrieving pluginfacts Info: Retrieving plugin Info: Caching catalog for cookbook Info: Applying configuration version '1410401823' Notice: Finished catalog run in 0.04 seconds
还有更多...
当我们运行 puppet agent 时,Puppet 会查找名为 puppet.example.com 的主机(因为我们的测试节点位于 example.com 域中);如果找不到该主机,它会继续查找名为 Puppet 的主机。我们可以通过在 puppet agent 中使用 --server 选项来指定要联系的服务器。当我们安装 Puppet 主服务包并启动 Puppet 主服务时,Puppet 会根据我们的主机名创建默认的 SSL 证书。在接下来的部分中,我们将看到如何为我们的 Puppet 服务器创建一个包含多个 DNS 名称的 SSL 证书。
创建包含多个 DNS 名称的证书
默认情况下,Puppet 会为你的 Puppet 主服务器创建一个只包含服务器完全限定域名的 SSL 证书。根据你的网络配置,服务器被其他名称识别可能很有用。在本食谱中,我们将为 Puppet 主服务器创建一个包含多个 DNS 名称的新证书。
准备就绪
如果你还没有安装 Puppet 主包,请安装它。然后,你需要至少启动一次 Puppet 主服务以创建证书颁发机构(CA)。
如何操作...
步骤如下:
-
使用以下命令停止正在运行的 Puppet 主进程:
# service puppetmaster stop [ ok ] Stopping puppet master. -
删除(
clean)当前的服务器证书:# puppet cert clean puppet Notice: Revoked certificate with serial 6 Notice: Removing file Puppet::SSL::Certificate puppet at '/var/lib/puppet/ssl/ca/signed/puppet.pem' Notice: Removing file Puppet::SSL::Certificate puppet at '/var/lib/puppet/ssl/certs/puppet.pem' Notice: Removing file Puppet::SSL::Key puppet at '/var/lib/puppet/ssl/private_keys/puppet.pem' -
使用
--dns-alt-names选项通过 Puppet 证书生成命令创建新的 Puppet 证书:root@puppet:~# puppet certificate generate puppet --dns-alt-names puppet.example.com,puppet.example.org,puppet.example.net --ca-location local Notice: puppet has a waiting certificate request true -
签署新证书:
root@puppet:~# puppet cert --allow-dns-alt-names sign puppet Notice: Signed certificate request for puppet Notice: Removing file Puppet::SSL::CertificateRequest puppet at '/var/lib/puppet/ssl/ca/requests/puppet.pem' -
重启 Puppet 主进程:
root@puppet:~# service puppetmaster restart [ ok ] Restarting puppet master.
它是如何工作的...
当你的 Puppet 代理连接到 Puppet 服务器时,它们会查找名为Puppet的主机,然后查找名为Puppet.[你的域名]的主机。如果你的客户端位于不同的域中,你需要让 Puppet 主服务器对所有正确的名称做出回应。通过删除现有证书并生成新证书,你可以让 Puppet 主服务器对多个 DNS 名称做出响应。
从 Passenger 运行 Puppet
我们在前一部分配置的 WEBrick 服务器无法处理大量节点。为了处理大量节点,需要一个可扩展的 Web 服务器。Puppet 是一个 Ruby 进程,因此我们需要一种在 Web 服务器中运行 Ruby 进程的方法。Passenger是解决此问题的方案。它允许我们在 Web 服务器中运行 Puppet 主进程(默认使用 Apache)。许多发行版都提供了一个 puppetmaster-passenger 软件包,可以为你配置这个功能。在本节中,我们将使用这个软件包来配置 Puppet 在 Passenger 中运行。
准备工作
安装 puppetmaster-passenger 软件包:
# puppet resource package puppetmaster-passenger ensure=installed
Notice: /Package[puppetmaster-passenger]/ensure: ensure changed 'purged'
to 'present'
package { 'puppetmaster-passenger':
ensure => '3.7.0-1puppetlabs1',
}
注意
使用puppet resource安装软件包可确保相同的命令在多个发行版上有效(前提是软件包名称相同)。
如何操作...
步骤如下:
-
确保 Puppet 主站点在你的 Apache 配置中已启用。根据你的发行版,它可能位于
/etc/httpd/conf.d或/etc/apache2/sites-enabled。配置文件应该已经为你创建,并包含以下信息:PassengerHighPerformance on PassengerMaxPoolSize 12 PassengerPoolIdleTime 1500 # PassengerMaxRequests 1000 PassengerStatThrottleRate 120 RackAutoDetect Off RailsAutoDetect Off Listen 8140 -
这些行是 Passenger 的调优设置。然后,文件指示 Apache 在端口 8140 上监听,这是 Puppet 主端口。接着,创建一个
VirtualHost定义,加载 Puppet CA 证书和 Puppet 主证书:<VirtualHost *:8140> SSLEngine on SSLProtocol ALL -SSLv2 -SSLv3 SSLCertificateFile /var/lib/puppet/ssl/certs/puppet.pem SSLCertificateKeyFile /var/lib/puppet/ssl/private_keys/puppet.pem SSLCertificateChainFile /var/lib/puppet/ssl/certs/ca.pem SSLCACertificateFile /var/lib/puppet/ssl/certs/ca.pem SSLCARevocationFile /var/lib/puppet/ssl/ca/ca_crl.pem SSLVerifyClient optional SSLVerifyDepth 1 SSLOptions +StdEnvVars +ExportCertData提示
根据你所使用的 puppetmaster-passenger 软件包的版本,这里可能会有更多或更少的 SSL 配置行。
-
接下来,设置几个重要的头信息,以便 Passenger 进程可以访问客户端节点发送的 SSL 信息:
RequestHeader unset X-Forwarded-For RequestHeader set X-SSL-Subject %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-DN %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-Verify %{SSL_CLIENT_VERIFY}e -
最后,给出了 Passenger 配置文件
config.ru的位置,以及DocumentRoot位置,具体如下:DocumentRoot /usr/share/puppet/rack/puppetmasterd/public/ RackBaseURI / -
config.ru文件应位于/usr/share/puppet/rack/puppetmasterd/,并应包含以下内容:$0 = "master" ARGV << "--rack" ARGV << "--confdir" << "/etc/puppet" ARGV << "--vardir" << "/var/lib/puppet" require 'puppet/util/command_line' run Puppet::Util::CommandLine.new.execute -
配置好 passenger apache 配置文件,并正确配置
config.ru文件后,启动 apache 服务器,并验证 apache 是否在 Puppet master 端口上监听(如果你之前配置了独立的 Puppet master,必须现在停止该进程,使用命令service puppetmaster stop):root@puppet:~ # service apache2 start [ ok ] Starting web server: apache2 root@puppet:~ # lsof -i :8140 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME apache2 9048 root 8u IPv6 16842 0t0 TCP *:8140 (LISTEN) apache2 9069 www-data 8u IPv6 16842 0t0 TCP *:8140 (LISTEN) apache2 9070 www-data 8u IPv6 16842 0t0 TCP *:8140 (LISTEN)
它是如何工作的...
passenger 配置文件使用现有的 Puppet master 证书监听 8140 端口,并处理服务器与客户端之间的所有 SSL 通信。一旦证书信息处理完毕,连接就会被交给一个由 passenger 启动的 ruby 进程,该进程使用来自 config.ru 文件的命令行参数。
在这种情况下,$0变量被设置为master,而参数变量被设置为--rack --confdir /etc/puppet --vardir /var/lib/puppet;这相当于从命令行运行以下内容:
puppet master --rack --confdir /etc/puppet --vardir /var/lib/puppet
还有更多...
你可以向 config.ru 文件添加额外的配置参数,以进一步改变 Puppet 在通过 passenger 运行时的行为。例如,要在 passenger Puppet master 上启用调试,请在 config.ru 文件中的 Puppet::Util::CommandLine.new.execute 之前添加以下行:
ARGV << "--debug"
设置环境
Puppet 中的环境是包含不同版本 Puppet 清单的目录。在 Puppet 3.6 版本之前,环境并不是 Puppet 的默认配置。在较新版本的 Puppet 中,环境是默认配置的。
每当一个节点连接到 Puppet master 时,它会通知 Puppet master 它的环境。默认情况下,所有节点都报告到 production 环境。这会导致 Puppet master 在生产环境中查找清单。你可以在运行 puppet agent 时通过 --environment 设置指定一个备用环境,或者在 /etc/puppet/puppet.conf 的 [agent] 部分中设置 environment = newenvironment。
准备工作
通过在 /etc/puppet/puppet.conf 的 [main] 部分添加以下行,设置你的安装的 environmentpath 功能:
[main]
...
environmentpath=/etc/puppet/environments
如何操作...
步骤如下:
-
在
/etc/puppet/environments下创建一个production目录,包含modules和manifests目录。然后创建一个site.pp文件,该文件在/tmp中创建一个文件,如下所示:root@puppet:~# cd /etc/puppet/environments/ root@puppet:/etc/puppet/environments# mkdir -p production/{manifests,modules} root@puppet:/etc/puppet/environments# vim production/manifests/site.pp node default { file {'/tmp/production': content => "Hello World!\nThis is production\n", } } -
在主节点上运行 puppet agent 以连接到主节点,并验证生产代码是否已交付:
root@puppet:~# puppet agent -vt Info: Retrieving pluginfacts Info: Retrieving plugin Info: Caching catalog for puppet Info: Applying configuration version '1410415538' Notice: /Stage[main]/Main/Node[default]/File[/tmp/production]/ensure: defined content as '{md5}f7ad9261670b9da33a67a5126933044c' Notice: Finished catalog run in 0.04 seconds # cat /tmp/production Hello World! This is production -
配置另一个环境
devel。在devel环境中创建一个新的清单:root@puppet:/etc/puppet/environments# mkdir -p devel/{manifests,modules} root@puppet:/etc/puppet/environments# vim devel/manifests/site.pp node default { file {'/tmp/devel': content => "Good-bye! Development\n", } } -
通过运行以下命令,使用
--environment devel选项来应用新环境:root@puppet:/etc/puppet/environments# puppet agent -vt --environment devel Info: Retrieving pluginfacts Info: Retrieving plugin Info: Caching catalog for puppet Info: Applying configuration version '1410415890' Notice: /Stage[main]/Main/Node[default]/File[/tmp/devel]/ensure: defined content as '{md5}b6313bb89bc1b7d97eae5aa94588eb68' Notice: Finished catalog run in 0.04 seconds root@puppet:/etc/puppet/environments# cat /tmp/devel Good-bye! Development
提示
你可能需要重新启动 apache2 才能启用新环境,这取决于你的 Puppet 版本和 puppet.conf 中的 environment_timeout 参数。
还有更多...
每个环境可以有自己的 modulepath,只要在环境目录中创建一个 environment.conf 文件。关于环境的更多信息可以在 Puppet Labs 网站上找到,链接是 docs.puppetlabs.com/puppet/latest/reference/environments.html。
配置 PuppetDB
PuppetDB 是一个用于存储与 Puppet 主服务器连接的节点信息的数据库。PuppetDB 还是导出资源的存储区域。导出资源是指在节点上定义但应用于其他节点的资源。安装 PuppetDB 的最简单方法是使用 Puppet Labs 提供的 PuppetDB 模块。从这一点开始,我们假设你正在使用 puppet.example.com 机器,并且拥有基于 Passenger 的 Puppet 配置。
准备工作
在之前步骤中创建的生产环境中安装 PuppetDB 模块。如果你没有创建目录环境,也不用担心,使用 puppet module install 将会把模块安装到你的安装目录的正确位置,执行以下命令:
root@puppet:~# puppet module install puppetlabs-puppetdb
Notice: Preparing to install into /etc/puppet/environments/production/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/etc/puppet/environments/production/modules
└─┬ puppetlabs-puppetdb (v3.0.1)
├── puppetlabs-firewall (v1.1.3)
├── puppetlabs-inifile (v1.1.3)
└─┬ puppetlabs-postgresql (v3.4.2)
├─┬ puppetlabs-apt (v1.6.0)
│ └── puppetlabs-stdlib (v4.3.2)
└── puppetlabs-concat (v1.1.0)
如何操作...
现在我们的 Puppet 主服务器已经安装了 PuppetDB 模块,我们需要将 PuppetDB 模块应用到 Puppet 主服务器。我们可以在站点清单中完成此操作。在你的(生产)site.pp 文件中添加以下内容:
node puppet {
class { 'puppetdb': }
class { 'puppetdb::master::config':
puppet_service_name => 'apache2',
}
}
运行 puppet agent 来应用 puppetdb 类和 puppetdb::master::config 类:
root@puppet:~# puppet agent -t
Info: Caching catalog for puppet
Info: Applying configuration version '1410416952'
...
Info: Class[Puppetdb::Server::Jetty_ini]: Scheduling refresh of Service[puppetdb]
Notice: Finished catalog run in 160.78 seconds
它是如何工作的...
PuppetDB 模块是一个很好的例子,展示了如何将复杂的配置任务自动化。通过将 puppetdb 类添加到 Puppet 主节点,Puppet 安装并配置了 postgresql 和 puppetdb。
当我们调用 puppetdb::master::config 类时,我们将 puppet_service_name 变量设置为 apache2,这是因为我们通过 Passenger 运行 Puppet。如果没有这一行,我们的代理会尝试启动 puppetmaster 进程,而不是 apache2。
代理接着为 PuppetDB 设置了配置文件,并配置了 Puppet 使用 PuppetDB。如果你查看/etc/puppet/puppet.conf,你会看到以下两行新的配置:
storeconfigs = true
storeconfigs_backend = puppetdb
还有更多内容...
现在 PuppetDB 已经配置完毕,并且我们成功运行了代理,PuppetDB 将会有我们可以查询的数据:
root@puppet:~# puppet node status puppet
puppet
Currently active
Last catalog: 2014-09-11T06:45:25.267Z
Last facts: 2014-09-11T06:45:22.351Z
配置 Hiera
Hiera 是 Puppet 的信息仓库。使用 Hiera,你可以对关于节点的数据进行分层分类,这些数据保存在清单之外。这对于共享代码和处理任何 Puppet 部署中不可避免的例外情况非常有用。
准备工作
Hiera 应该已经作为 Puppet 主服务器的依赖项安装。如果还没有安装,可以通过 Puppet 来安装它:
root@puppet:~# puppet resource package hiera ensure=installed
package { 'hiera':
ensure => '1.3.4-1puppetlabs1',
}
如何操作...
-
Hiera 是通过一个 yaml 文件
/etc/puppet/hiera.yaml进行配置的。创建该文件,并添加以下内容作为最小配置:--- :hierarchy: - common :backends: - yaml :yaml: :datadir: '/etc/puppet/hieradata' -
创建在层次结构中引用的
common.yaml文件:root@puppet:/etc/puppet# mkdir hieradata root@puppet:/etc/puppet# vim hieradata/common.yaml --- message: 'Default Message' -
编辑
site.pp文件并基于 Hiera 值添加一个通知资源:node default { $message = hiera('message','unknown') notify {"Message is $message":} } -
将清单应用于测试节点:
t@ckbk:~$ sudo puppet agent -t Info: Retrieving pluginfacts Info: Retrieving plugin ... Info: Caching catalog for cookbook-test Info: Applying configuration version '1410504848' Notice: Message is Default Message Notice: /Stage[main]/Main/Node[default]/Notify[Message is Default Message]/message: defined 'message' as 'Message is Default Message' Notice: Finished catalog run in 0.06 seconds
它是如何工作的...
Hiera 使用层次结构搜索一组 yaml 文件,以找到合适的值。我们在 hiera.yaml 中定义了这个层次结构,并且只包含了 common.yaml 的条目。我们在 site.pp 中使用了 hiera 函数来查找消息的值并将其存储在变量 $message 中。用于定义层次结构的值可以是系统上定义的任何 facter 数据。常见的层次结构示例如下:
:hierarchy:
- hosts/%{hostname}
- os/%{operatingsystem}
- network/%{network_eth0}
- common
还有更多...
Hiera 可以用于带有参数化类的自动参数查找。例如,如果你有一个名为 cookbook::example 的类,其中有一个名为 publisher 的参数,你可以在 Hiera 的 yaml 文件中加入以下内容来自动设置这个参数:
cookbook::example::publisher: 'PacktPub'
另一个常用的 fact 是 environment,你可以通过 %{environment} 引用客户端节点的 environment,如下所示的层次结构:
:hierarchy:
hosts/%{hostname}
os/%{operatingsystem}
environment/%{environment}
common
提示
一个好的经验法则是将层次结构限制为 8 层或更少。请记住,每次使用 Hiera 搜索参数时,所有层次都会被搜索,直到找到匹配项。
默认的 Hiera 函数返回与搜索键匹配的第一个结果,你还可以使用 hiera_array 和 hiera_hash 来搜索并返回 Hiera 中存储的所有值。
Hiera 也可以从命令行进行搜索,如以下命令所示(请注意,目前命令行的 Hiera 工具使用 /etc/hiera.yaml 作为其配置文件,而 Puppet 主控使用 /etc/puppet/hiera.yaml):
root@puppet:/etc/puppet# rm /etc/hiera.yaml
root@puppet:/etc/puppet# ln -s /etc/puppet/hiera.yaml /etc/
root@puppet:/etc/puppet# hiera message
Default Message
注意
欲了解更多信息,请参阅 Puppet labs 网站 docs.puppetlabs.com/hiera/1/。
使用 Hiera 设置特定节点数据
在我们在 hiera.yaml 中定义的层次结构中,我们基于主机名 fact 创建了一个条目;在本节中,我们将在 Hiera 数据的 hosts 子目录中创建 yaml 文件,其中包含特定主机的信息。
准备工作
按照上一节的步骤安装并配置 Hiera,并使用前面配方中定义的层次结构,该层次结构包括一个 hosts/%{hostname} 条目。
如何操作...
以下是步骤:
-
在
/etc/puppet/hieradata/hosts创建一个文件,该文件的名称与测试节点的主机名相同。例如,如果主机名为cookbook-test,则文件应命名为cookbook-test.yaml。 -
在此文件中插入特定消息:
message: 'This is the test node for the cookbook' -
在两个不同的测试节点上运行 Puppet,以注意其中的差异:
t@ckbk:~$ sudo puppet agent -t Info: Caching catalog for cookbook-test Notice: Message is This is the test node for the cookbook [root@hiera-test ~]# puppet agent -t Info: Caching catalog for hiera-test.example.com Notice: Message is Default Message
它是如何工作的...
Hiera 会在层次结构中搜索与 facter 返回的值匹配的文件。在此情况下,通过将节点的主机名替换到搜索路径/etc/puppet/hieradata/hosts/%{hostname}.yaml中,找到了 cookbook-test.yaml 文件。
使用 Hiera,可以大大减少 Puppet 代码的复杂性。我们将使用 yaml 文件来存储分开的值,而不再需要像以前那样写大型的 case 语句或嵌套的 if 语句。
使用 hiera-gpg 存储机密数据
如果您使用 Hiera 存储配置数据,可以使用名为hiera-gpg的 gem,它为 Hiera 添加了一个加密后端,允许您保护存储在 Hiera 中的值。
准备就绪
设置 hiera-gpg,请按照以下步骤操作:
-
安装
ruby-dev软件包;它将用于构建hiera-gpggem,如下所示:root@puppet:~# puppet resource package ruby-dev ensure=installed Notice: /Package[ruby-dev]/ensure: ensure changed 'purged' to 'present' package { 'ruby-dev': ensure => '1:1.9.3', } -
使用 gem 提供程序安装
hiera-gpggem:root@puppet:~# puppet resource package hiera-gpg ensure=installed provider=gem Notice: /Package[hiera-gpg]/ensure: created package { 'hiera-gpg': ensure => ['1.1.0'], } -
按如下方式修改
hiera.yaml文件::hierarchy: - secret - common :backends: - yaml - gpg :yaml: :datadir: '/etc/puppet/hieradata' :gpg: :datadir: '/etc/puppet/secret'
如何操作...
在此示例中,我们将创建一段加密数据,并使用hiera-gpg按如下方式检索它:
-
在
/etc/puppet/secret处创建secret.yaml文件,内容如下:top_secret: 'Val Kilmer' -
如果您还没有 GnuPG 加密密钥,请按照第四章中使用 GnuPG 加密机密的步骤进行操作,与文件和软件包一起工作。
-
使用以下命令将
secret.yaml文件加密为此密钥(将puppet@puppet.example.com替换为您在创建密钥时指定的电子邮件地址)。这将生成secret.gpg文件:root@puppet:/etc/puppet/secret# gpg -e -o secret.gpg -r puppet@puppet.example.com secret.yaml root@puppet:/etc/puppet/secret# file secret.gpg secret.gpg: GPG encrypted data -
删除明文的
secret.yaml文件:root@puppet:/etc/puppet/secret# rm secret.yaml -
按如下方式修改
site.pp文件中的默认节点:node default { $message = hiera('top_secret','Deja Vu') notify { "Message is $message": } } -
现在在节点上运行 Puppet:
[root@hiera-test ~]# puppet agent -t Info: Caching catalog for hiera-test.example.com Info: Applying configuration version '1410508276' Notice: Message is Deja Vu Notice: /Stage[main]/Main/Node[default]/Notify[Message is Deja Vu]/message: defined 'message' as 'Message is Deja Vu' Notice: Finished catalog run in 0.08 seconds
它是如何工作的...
安装hiera-gpg时,它为 Hiera 添加了解密.gpg文件的功能。因此,您可以将任何机密数据放入.yaml文件中,然后使用 GnuPG 将其加密到相应的密钥。只有拥有正确密钥的机器才能访问这些数据。
例如,您可以使用hiera-gpg加密 MySQL root 密码,并仅在数据库服务器上安装相应的密钥。尽管其他机器可能也有secret.gpg文件的副本,但除非它们拥有解密密钥,否则无法读取此文件。
还有更多内容...
您可能还想了解hiera-eyaml,这是另一个 Hiera 的秘密数据后端,它支持加密 Hiera 数据文件中的单个值。如果您需要在单个文件中混合加密和未加密的事实数据,这将非常有用。了解更多关于 hiera-eyaml 的信息,请访问github.com/TomPoulton/hiera-eyaml。
另见
- 第四章中使用 GnuPG 加密机密的步骤,与文件和软件包一起工作。
使用 MessagePack 序列化
在集中式架构中运行 Puppet 会在节点之间产生大量流量。大部分流量是 JSON 和 yaml 数据。Puppet 最新版本的实验性功能允许使用MessagePack(msgpack)对这些数据进行序列化。
准备就绪
将 msgpack gem 安装到您的 Puppet 主节点和节点上。使用 Puppet 资源让 Puppet 为您完成这项工作。此时,您可能需要在节点/服务器上安装ruby-dev或ruby-devel软件包:
t@ckbk:~$ sudo puppet resource package msgpack ensure=installedprovider=gem
Notice: /Package[msgpack]/ensure: created
package { 'msgpack':
ensure => ['0.5.8'],
}
如何操作...
在节点puppet.conf文件的[agent]部分,将preferred_serialization_format设置为msgpack:
[agent]
preferred_serialization_format=msgpack
它是如何工作的...
当节点开始与主机通信时,主机会收到此选项。任何支持与 msgpack 进行序列化的类将通过节点与主机之间的 msgpack.Serialization 数据传输。理论上,这会通过优化传输的数据,提高节点之间的通信速度。此功能仍在实验阶段。
使用 Git 钩子进行自动语法检查
如果我们能够在提交之前就知道清单中是否有语法错误,那该多好。你可以使用 puppet parser validate 命令让 Puppet 检查清单的语法:
t@ckbk:~$ puppet parser validate bootstrap.pp
Error: Could not parse for environment production: Syntax error at
'File'; expected '}' at /home/thomas/bootstrap.pp:3
这尤其有用,因为清单中的任何错误都会导致 Puppet 在任何节点上停止运行,即使是那些没有使用该部分清单的节点也是如此。因此,提交一个有问题的清单可能会导致 Puppet 停止向生产环境应用更新,直到问题被发现,而这可能会带来严重后果。避免这种情况的最佳方法是通过在版本控制仓库中使用预提交钩子(precommit hook)来自动化语法检查。
如何操作…
按照以下步骤操作:
-
在 Puppet 仓库中创建一个新的
hooks目录:t@mylaptop:~/puppet$ mkdir hooks -
创建文件
hooks/check_syntax.sh,内容如下(基于 Puppet Labs 的脚本):#!/bin/sh syntax_errors=0 error_msg=$(mktemp /tmp/error_msg.XXXXXX) if git rev-parse --quiet --verify HEAD > /dev/null then against=HEAD else # Initial commit: diff against an empty tree object against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi # Get list of new/modified manifest and template files to check (in git index) for indexfile in 'git diff-index --diff-filter=AM -- name-only --cached $against | egrep '\.(pp|erb)'' do # Don't check empty files if [ 'git cat-file -s :0:$indexfile' -gt 0 ] then case $indexfile in *.pp ) # Check puppet manifest syntax git cat-file blob :0:$indexfile | puppet parser validate > $error_msg ;; *.erb ) # Check ERB template syntax git cat-file blob :0:$indexfile | erb -x -T - | ruby -c 2> $error_msg > /dev/null ;; esac if [ "$?" -ne 0 ] then echo -n "$indexfile: " cat $error_msg syntax_errors='expr $syntax_errors + 1' fi fi done rm -f $error_msg if [ "$syntax_errors" -ne 0 ] then echo "Error: $syntax_errors syntax errors found, aborting commit." exit 1 fi -
使用以下命令为
hook脚本设置执行权限:t@mylaptop:~/puppet$ chmod a+x hooks/check_syntax.sh -
现在,将脚本通过符号链接或复制到 hooks 目录中的预提交钩子(precommit hook)中。如果你的 Git 仓库在
~/puppet目录下,则按以下方式在~/puppet/hooks/pre-commit创建符号链接:t@mylaptop:~/puppet$ ln -s ~/puppet/hooks/check_syntax.sh.git/hooks/pre-commit
它是如何工作的…
当 check_syntax.sh 脚本作为 Git 的预提交钩子使用时,它将防止你提交任何包含语法错误的文件:
t@mylaptop:~/puppet$ git commit -m "test commit"
Error: Could not parse for environment production: Syntax error at
'}' at line 3
Error: Try 'puppet help parser validate' for usage
manifests/nodes.pp: Error: 1 syntax errors found, aborting commit.
如果将 hooks 目录添加到 Git 仓库中,任何有仓库检出的用户都可以将脚本复制到本地的 hooks 目录,以获得此语法检查功能。
使用 Git 推送代码
正如我们在去中心化模型中看到的,Git 可以通过结合使用 ssh 和 ssh 密钥在机器之间传输文件。让 Git 钩子在每次成功提交到仓库时执行相同的操作也是有用的。
存在一个叫做 post-commit 的钩子,可以在成功提交到仓库后运行。在本配方中,我们将创建一个钩子,用于将代码从 Git 服务器上的 Git 仓库更新到 Puppet 主机上的代码。
准备工作
按照以下步骤开始:
-
创建一个
ssh密钥,使其能够访问 Puppet 主机上的 Puppet 用户,并将该密钥安装到git.example.com上 Git 用户的帐户中:[git@git ~]$ ssh-keygen -f ~/.ssh/puppet_rsa Generating public/private rsa key pair. Your identification has been saved in /home/git/.ssh/puppet_rsa. Your public key has been saved in /home/git/.ssh/puppet_rsa.pub. Copy the public key into the authorized_keys file of the puppet user on your puppetmaster puppet@puppet:~/.ssh$ cat puppet_rsa.pub >>authorized_keys -
修改 Puppet 帐号以允许 Git 用户按以下方式登录:
root@puppet:~# chsh puppet -s /bin/bash
如何操作…
执行以下步骤:
-
现在 Git 用户可以作为 Puppet 用户登录到 Puppet 主机,修改 Git 用户的
ssh配置,使其默认使用新创建的ssh密钥:[git@git ~]$ vim .ssh/config Host puppet.example.com IdentityFile ~/.ssh/puppet_rsa -
使用以下命令将 Puppet 主机添加为 Git 服务器上的 Puppet 仓库的远程位置:
[git@git puppet.git]$ git remote add puppetmaster puppet@puppet.example.com:/etc/puppet/environments/puppet.git -
在 Puppet 主服务器上,将
production目录移开,并检出你的 Puppet 仓库:root@puppet:~# chown -R puppet:puppet /etc/puppet/environments root@puppet:~# sudo -iu puppet puppet@puppet:~$ cd /etc/puppet/environments/ puppet@puppet:/etc/puppet/environments$ mv production production.orig puppet@puppet:/etc/puppet/environments$ git clone git@git.example.com:repos/puppet.git Cloning into 'puppet.git'... remote: Counting objects: 63, done. remote: Compressing objects: 100% (52/52), done. remote: Total 63 (delta 10), reused 0 (delta 0) Receiving objects: 100% (63/63), 9.51 KiB, done. Resolving deltas: 100% (10/10), done. -
现在我们在 Puppet 服务器上有一个本地裸仓库,可以将其推送,并将其远程克隆到
production目录:puppet@puppet:/etc/puppet/environments$ git clone puppet.git production Cloning into 'production'... done. -
现在从 Git 服务器执行 Git 推送到 Puppet 主服务器:
[git@git ~]$ cd repos/puppet.git/ [git@git puppet.git]$ git push puppetmaster Everything up-to-date -
在 Git 服务器上的仓库的
hooks目录中创建一个 post-commit 文件,内容如下:[git@git puppet.git]$ vim hooks/post-commit #!/bin/sh git push puppetmaster ssh puppet@puppet.example.com "cd /etc/puppet/environments/production && git pull" [git@git puppet.git]$ chmod 755 hooks/post-commit -
从你的笔记本提交一个更改到仓库,并验证该更改是否传播到 Puppet 主服务器,步骤如下:
t@mylaptop puppet$ vim README t@mylaptop puppet$ git add README t@mylaptop puppet$ git commit -m "Adding README" [master 8148902] Adding README 1 file changed, 4 deletions(-) t@mylaptop puppet$ git push X11 forwarding request failed on channel 0 Counting objects: 5, done. Delta compression using up to 4 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 371 bytes | 0 bytes/s, done. Total 3 (delta 1), reused 0 (delta 0) remote: To puppet@puppet.example.com:/etc/puppet/environments/puppet.git remote: 377ed44..8148902 master -> master remote: From /etc/puppet/environments/puppet remote: 377ed44..8148902 master -> origin/master remote: Updating 377ed44..8148902 remote: Fast-forward remote: README | 4 ---- remote: 1 file changed, 4 deletions(-) To git@git.example.com:repos/puppet.git 377ed44..8148902 master -> master
它是如何工作的...
我们在 Puppet 主服务器上创建了一个裸仓库,然后将其作为 git.example.com 上仓库的远程。然后我们将该裸仓库克隆到 production 目录中。我们将 puppet.example.com 上的裸仓库添加为 git.example.com 上裸仓库的远程。接着,我们在 git.example.com 上的仓库中创建一个 post-receive 钩子。
钩子对 Puppet 主服务器上的裸仓库执行 Git 推送。然后,我们从 Puppet 主服务器上的更新的裸仓库更新 production 目录。在下一节中,我们将修改钩子以使用分支。
使用 Git 管理环境
分支是一种在单个源代码仓库中保持多个开发轨迹的方法。Puppet 环境与 Git 分支非常相似。你可以在不同的分支之间拥有相同的代码,并且稍有不同,就像你可以为不同的环境创建不同的模块一样。在本节中,我们将展示如何使用 Git 分支在 Puppet 主服务器上定义环境。
准备工作
在上一节中,我们创建了一个基于主分支的 production 目录;现在我们将删除该目录:
puppet@puppet:/etc/puppet/environments$ mv production production.master
如何操作...
修改 post-receive 钩子以接受分支变量。该钩子将使用此变量在 Puppet 主服务器上创建一个目录,步骤如下:
#!/bin/sh
read oldrev newrev refname
branch=${refname#*\/*\/}
git push puppetmaster $branch
ssh puppet@puppet.example.com "if [ ! -d
/etc/puppet/environments/$branch ]; then git clone
/etc/puppet/environments/puppet.git
/etc/puppet/environments/$branch; fi; cd
/etc/puppet/environments/$branch; git checkout $branch; git pull"
再次修改你的 README 文件,并推送到 git.example.com 上的仓库:
t@mylaptop puppet$ git add README
t@mylaptop puppet$ git commit -m "Adding README"
[master 539d9f8] Adding README
1 file changed, 1 insertion(+)
t@mylaptop puppet$ git push
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 374 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: To puppet@puppet.example.com:/etc/puppet/environments/puppet.git
remote: 0d6b49f..539d9f8 master -> master
remote: Cloning into '/etc/puppet/environments/master'...
remote: done.
remote: Already on 'master'
remote: Already up-to-date.
To git@git.example.com:repos/puppet.git
0d6b49f..539d9f8 master -> master
它是如何工作的...
这个钩子现在读取 refname 并解析出正在更新的分支。我们使用这个分支变量将仓库克隆到一个新的目录,并检出该分支。
还有更多...
现在,当我们想创建一个新环境时,可以在 Git 仓库中创建一个新分支。这个分支将在 Puppet 主服务器上创建一个目录。Git 仓库的每个分支都代表 Puppet 主服务器上的一个环境:
-
按照以下命令行创建
production分支:t@mylaptop puppet$ git branch production t@mylaptop puppet$ git checkout production Switched to branch 'production' -
更新
production分支并推送到 Git 服务器,步骤如下:t@mylaptop puppet$ vim README t@mylaptop puppet$ git add README t@mylaptop puppet$ git commit -m "Production Branch" t@mylaptop puppet$ git push origin production Counting objects: 7, done. Delta compression using up to 4 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 372 bytes | 0 bytes/s, done. Total 3 (delta 1), reused 0 (delta 0) remote: To puppet@puppet.example.com:/etc/puppet/environments/puppet.git remote: 11db6e5..832f6a9 production -> production remote: Cloning into '/etc/puppet/environments/production'... remote: done. remote: Switched to a new branch 'production' remote: Branch production set up to track remote branch production from origin. remote: Already up-to-date. To git@git.example.com:repos/puppet.git 11db6e5..832f6a9 production -> production
现在,每当我们创建一个新分支时,环境目录中将创建一个对应的目录。环境和分支之间建立了一对一的映射关系。
第三章。编写更好的清单
| “通过代码行数衡量编程进展,就像通过重量衡量飞机建造进展一样。” | ||
|---|---|---|
| --比尔·盖茨 |
在本章中,我们将涵盖:
-
使用资源数组
-
使用资源默认值
-
使用定义类型
-
使用标签
-
使用运行阶段
-
使用角色和配置文件
-
向类传递参数
-
从 Hiera 传递参数
-
编写可重用的跨平台清单
-
获取有关环境的信息
-
导入动态信息
-
向 shell 命令传递参数
介绍
你的 Puppet 清单是你整个基础设施的活文档。保持它们整洁和有序是维护和理解的好方法。Puppet 为你提供了以下工具来实现这一点:
-
数组
-
默认值
-
定义类型
-
依赖关系
-
类参数
我们将看到如何使用这些工具以及更多内容。在阅读本章时,尝试运行示例并查看你自己的清单,看看这些功能如何帮助你简化和改进 Puppet 代码。
使用资源数组
你可以对资源做的任何事,也可以对资源数组做。利用这个思路来重构你的清单,使它们更简洁明了。
如何操作…
下面是使用资源数组重构的步骤:
-
在你的清单中找出一个包含多个相同类型资源实例的类,例如,软件包:
package { 'sudo' : ensure => installed } package { 'unzip' : ensure => installed } package { 'locate' : ensure => installed } package { 'lsof' : ensure => installed } package { 'cron' : ensure => installed } package { 'rubygems' : ensure => installed } -
将它们组合在一起,并用一个数组替换为单个软件包资源:
package { [ 'cron', 'locate', 'lsof', 'rubygems', 'sudo', 'unzip' ]: ensure => installed, }
如何操作…
Puppet 的大多数资源类型可以接受一个数组,而不是单个名称,并会为数组中的每个元素创建一个实例。你为资源提供的所有参数(例如,ensure => installed)都会分配给每个新的资源实例。这个简写方法只有在所有资源具有相同属性时才有效。
另见
- 第一章中的 迭代多个项目 配方,Puppet 语言与风格
使用资源默认值
Puppet 模块是一组相关的资源,通常用于配置特定服务。在模块中,你可以定义多个资源;资源默认值允许你为资源指定默认属性值。在此示例中,我们将展示如何为 File 类型指定资源默认值。
如何操作…
为了展示如何使用资源默认值,我们将创建一个 apache 模块。在这个模块中,我们将指定默认的拥有者和组是 apache 用户,如下所示:
-
创建一个 apache 模块并为
File类型创建一个资源默认值:class apache { File { owner => 'apache', group => 'apache', mode => 0644, } } -
在
/var/www/html目录下创建 html 文件:file {'/var/www/html/index.html': content => "<html><body><h1><a href='cookbook.html'>Cookbook! </a></h1></body></html>\n", } file {'/var/www/html/cookbook.html': content => "<html><body><h2>PacktPub</h2></body></html>\n", } -
将此类添加到你的默认节点定义中,或者使用
puppet apply将模块应用到你的节点。我将使用我们在上一章中配置的方法,将代码推送到 Git 仓库,并使用 Git 钩子将代码部署到 Puppet 主节点,如下所示:t@mylaptop ~/puppet $ git pull origin production From git.example.com:repos/puppet * branch production -> FETCH_HEAD Already up-to-date. t@mylaptop ~/puppet $ cd modules t@mylaptop ~/puppet/modules $ mkdir -p apache/manifests t@mylaptop ~/puppet/modules $ vim apache/manifests/init.pp t@mylaptop ~/puppet/modules $ cd .. t@mylaptop ~/puppet $ vim manifests/site.pp t@mylaptop ~/puppet $ git status On branch production Changes not staged for commit: modified: manifests/site.pp Untracked files: modules/apache/ t@mylaptop ~/puppet $ git add manifests/site.pp modules/apache t@mylaptop ~/puppet $ git commit -m 'adding apache module' [production d639a86] adding apache module 2 files changed, 14 insertions(+) create mode 100644 modules/apache/manifests/init.pp t@mylaptop ~/puppet $ git push origin production Counting objects: 13, done. Delta compression using up to 4 threads. Compressing objects: 100% (6/6), done. Writing objects: 100% (8/8), 885 bytes | 0 bytes/s, done. Total 8 (delta 0), reused 0 (delta 0) remote: To puppet@puppet.example.com:/etc/puppet/environments/puppet.git remote: 832f6a9..d639a86 production -> production remote: Already on 'production' remote: From /etc/puppet/environments/puppet remote: 832f6a9..d639a86 production -> origin/production remote: Updating 832f6a9..d639a86 remote: Fast-forward remote: manifests/site.pp | 1 + remote: modules/apache/manifests/init.pp | 13 +++++++++++++ remote: 2 files changed, 14 insertions(+) remote: create mode 100644 modules/apache/manifests/init.pp To git@git.example.com:repos/puppet.git 832f6a9..d639a86 production -> production -
将模块应用到节点或运行 Puppet:
Notice: /Stage[main]/Apache/File[/var/www/html/cookbook.html]/ensure: defined content as '{md5}493473fb5bde778ca93d034900348c5d' Notice: /Stage[main]/Apache/File[/var/www/html/index.html]/ensure: defined content as '{md5}184f22c181c5632b86ebf9a0370685b3' Notice: Finished catalog run in 2.00 seconds [root@hiera-test ~]# ls -l /var/www/html total 8 -rw-r--r--. 1 apache apache 44 Sep 15 12:00 cookbook.html -rw-r--r--. 1 apache apache 73 Sep 15 12:00 index.html
它是如何工作的……
我们定义的资源默认值指定了此类中的所有文件资源的所有者、组和模式(也称为此范围内的所有者、组和模式)。除非你特别覆盖资源默认值,否则属性的值将从默认值中获取。
还有更多……
你可以为任何资源类型指定资源默认值。你还可以在site.pp中指定资源默认值。我发现指定Package和Service资源的默认操作非常有用,如下所示:
Package { ensure => 'installed' }
Service {
hasrestart => true,
enable => true,
ensure => true,
}
使用这些默认值时,每当你指定一个软件包时,该软件包将被安装。每当你指定一个服务时,该服务将启动并启用在启动时运行。这些是你指定软件包和服务的常见原因,大多数时候这些默认值会做你希望的事情,你的代码也会更加简洁。当你需要禁用一个服务时,只需覆盖默认值。
使用定义类型
在上一个示例中,我们看到如何通过将相同的资源分组到数组中来减少冗余代码。然而,这种技术仅限于所有参数相同的资源。当你有一组共享一些参数的资源时,你需要使用定义类型将它们组合在一起。
如何操作……
以下步骤将向你展示如何创建定义:
-
将以下代码添加到你的清单中:
define tmpfile() { file { "/tmp/${name}": content => "Hello, world\n", } } tmpfile { ['a', 'b', 'c']: } -
运行 Puppet:
[root@hiera-test ~]# vim tmp.pp [root@hiera-test ~]# puppet apply tmp.pp Notice: Compiled catalog for hiera-test.example.com in environment production in 0.11 seconds Notice: /Stage[main]/Main/Tmpfile[a]/File[/tmp/a]/ensure: defined content as '{md5}a7966bf58e23583c9a5a4059383ff850' Notice: /Stage[main]/Main/Tmpfile[b]/File[/tmp/b]/ensure: defined content as '{md5}a7966bf58e23583c9a5a4059383ff850' Notice: /Stage[main]/Main/Tmpfile[c]/File[/tmp/c]/ensure: defined content as '{md5}a7966bf58e23583c9a5a4059383ff850' Notice: Finished catalog run in 0.09 seconds [root@hiera-test ~]# cat /tmp/{a,b,c} Hello, world Hello, world Hello, world
它是如何工作的……
你可以把定义类型(通过define关键字引入)看作一个模具。它描述了一个模式,Puppet 可以用来创建许多相似的资源。每次你在清单中声明一个tmpfile实例时,Puppet 会插入所有包含在tmpfile定义中的资源。
在我们的例子中,tmpfile的定义包含一个file资源,其内容为Hello, world\n,路径为/tmp/${name}。如果你声明了一个名为foo的tmpfile实例:
tmpfile { 'foo': }
Puppet 会创建一个路径为/tmp/foo的文件。换句话说,定义中的${name}将被 Puppet 被要求创建的任何实际实例的name替换。它几乎就像我们创建了一种新类型的资源:tmpfile,它有一个参数——它的name。
就像常规资源一样,我们不必仅传递一个标题;如前面的示例所示,我们可以提供一个标题数组,Puppet 将创建所需数量的资源。
提示
关于名称,namevar:你创建的每个资源必须有一个唯一的名称,namevar。这与标题不同,标题是 Puppet 在内部引用资源的方式(尽管它们通常是相同的)。
还有更多……
在这个例子中,我们创建了一个定义,其中唯一在实例之间变化的参数是name参数。但我们可以添加任何我们想要的参数,只要我们在name参数后面的括号中声明它们,如下所示:
define tmpfile($greeting) {
file { "/tmp/${name}": content => $greeting,
}
}
接下来,在声明资源实例时为其传递值:
tmpfile{ 'foo':
greeting => "Good Morning\n",
}
你可以将多个参数声明为逗号分隔的列表:
define webapp($domain,$path,$platform) {
...
}
webapp { 'mywizzoapp':
domain => 'mywizzoapp.com',
path => '/var/www/apps/mywizzoapp',
platform => 'Rails',
}
你还可以为任何未提供的参数声明默认值,从而使它们成为可选参数:
define tmpfile($greeting,$mode='0644') {
...
}
这是一种强大的技术,用于抽象出某些资源的共同部分,并将其保存在一个地方,以便你不要重复自己。在前面的例子中,webapp中可能包含许多独立的资源:软件包、配置文件、源代码检出、虚拟主机等。但它们对于每个webapp实例都是相同的,除了我们提供的参数。这些参数可能会在模板中引用,例如,用于设置虚拟主机的域名。
另见
- 本章中的将参数传递给类示例
使用标签
有时,一个 Puppet 类需要了解另一个类,或者至少需要知道它是否存在。例如,一个管理防火墙的类可能需要知道该节点是否为 Web 服务器。
Puppet 的tagged函数会告诉你一个命名的类或资源是否出现在该节点的目录中。你还可以向节点或类应用任意标签,并检查这些标签的存在。标签是另一个元参数,类似于我们在第一章中介绍的require和notify,Puppet 语言与风格。元参数用于 Puppet 目录的编译,但不是附加到资源上的属性。
如何实现……
为了帮助你发现自己是否在特定节点或节点类上运行,所有节点都会自动被标记为节点名称及其包含的任何类的名称。以下是一个例子,展示如何使用tagged获取这些信息:
-
将以下代码添加到你的
site.pp文件中(将cookbook替换为你机器的hostname):node 'cookbook' { if tagged('cookbook') { notify { 'tagged cookbook': } } } -
运行 Puppet:
root@cookbook:~# puppet agent -vt Info: Caching catalog for cookbook Info: Applying configuration version '1410848350' Notice: tagged cookbook Notice: Finished catalog run in 1.00 seconds节点也会自动标记它们所包含的所有类的名称,以及其他一些自动标签。你可以使用
tagged来查找节点上包含了哪些类。你不仅仅限于检查 Puppet 自动应用的标签。你还可以添加自己的标签。要在节点上设置任意标签,请使用
tag函数,如以下示例所示: -
按照以下方式修改你的
site.pp文件:node 'cookbook' { tag('tagging') class {'tag_test': } } -
添加一个
tag_test模块,并提供以下init.pp(或者懒惰一些,将以下定义添加到你的site.pp中):class tag_test { if tagged('tagging') { notify { 'containing node/class was tagged.': } } } -
运行 Puppet:
root@cookbook:~# puppet agent -vt Info: Caching catalog for cookbook Info: Applying configuration version '1410851300' Notice: containing node/class was tagged. Notice: Finished catalog run in 0.22 seconds -
你还可以使用标签来确定应用清单的哪些部分。如果你在 Puppet 命令行中使用
--tags选项,Puppet 将仅应用那些带有你指定标签的类或资源。例如,我们可以用两个类来定义cookbook类:node cookbook { class {'first_class': } class {'second_class': } } class first_class { notify { 'First Class': } } class second_class { notify {'Second Class': } } -
现在,当我们在
cookbook节点上运行puppet agent时,我们会看到两个通知:root@cookbook:~# puppet agent -t Notice: Second Class Notice: First Class Notice: Finished catalog run in 0.22 seconds -
现在将
first_class和add --tags函数应用于命令行:root@cookbook:~# puppet agent -t --tags first_class Notice: First Class Notice: Finished catalog run in 0.07 seconds
还有更多...
你可以使用标签创建一个资源集合,然后将该集合作为其他资源的依赖。例如,假设某个服务依赖于由多个文件片段构建的配置文件,如下所示:
class firewall::service {
service { 'firewall': ...
}
File <| tag == 'firewall-snippet' |> ~> Service['firewall']
}
class myapp {
file { '/etc/firewall.d/myapp.conf': tag => 'firewall-snippet', ...
}
}
在这里,我们指定如果任何标记为 firewall-snippet 的文件资源被更新,firewall 服务应收到通知。我们只需要将特定应用或服务的配置片段标记为 firewall-snippet,Puppet 就会完成其余工作。
尽管我们可以为每个片段资源添加一个 notify => Service["firewall"] 函数,但如果 firewall 服务的定义发生变化,我们将不得不逐一查找并更新所有片段。标签允许我们将逻辑封装在一个地方,使得将来的维护和重构变得更容易。
注意
<| tag == 'firewall-snippet' |> 语法是什么?这是所谓的资源收集器,它通过搜索关于资源的一些数据来指定一组资源;在这个例子中,是标签的值。你可以在 Puppet Labs 网站上了解更多关于资源收集器和 <| |> 运算符(有时称为飞船操作符)的信息:docs.puppetlabs.com/puppet/3/reference/lang_collectors.html。
使用运行阶段
一个常见的需求是在其他组之前应用某一组资源(例如,安装软件包仓库或自定义 Ruby 版本),或者在其他组之后应用(例如,在安装完依赖项后部署应用)。Puppet 的运行阶段功能使你能够实现这一点。
默认情况下,清单中的所有资源都应用于一个名为 main 的单一阶段。如果你需要某个资源在其他资源之前应用,你可以将其分配到一个新的运行阶段,该阶段指定在 main 之前运行。类似地,你也可以定义一个在 main 之后运行的阶段。实际上,你可以根据需要定义任意数量的运行阶段,并告诉 Puppet 它们应该按什么顺序应用。
在这个示例中,我们将使用阶段来确保一个类首先应用,另一个类最后应用。
如何做...
以下是使用运行 stages 的示例步骤:
-
创建文件
modules/admin/manifests/stages.pp,内容如下:class admin::stages { stage { 'first': before => Stage['main'] } stage { 'last': require => Stage['main'] } class me_first { notify { 'This will be done first': } } class me_last { notify { 'This will be done last': } } class { 'me_first': stage => 'first', } class { 'me_last': stage => 'last', } } -
修改你的
site.pp文件,如下所示:node 'cookbook' { class {'first_class': } class {'second_class': } include admin::stages } -
运行 Puppet:
root@cookbook:~# puppet agent -t Info: Applying configuration version '1411019225' Notice: This will be done first Notice: Second Class Notice: First Class Notice: This will be done last Notice: Finished catalog run in 0.43 seconds
它是如何工作的…
让我们详细检查一下这段代码,看看发生了什么。首先,我们声明了运行阶段 first 和 last,如下所示:
stage { 'first': before => Stage['main'] }
stage { 'last': require => Stage['main'] }
对于first阶段,我们已经指定它应该在main之前执行。也就是说,标记为first阶段的每个资源将在任何main阶段的资源之前应用(main是默认阶段)。
last阶段需要main阶段,因此在main阶段的每个资源都应用后,才能应用last阶段的资源。
然后我们声明一些类,稍后将为这些类分配运行阶段:
class me_first {
notify { 'This will be done first': }
}
class me_last {
notify { 'This will be done last': }
}
现在我们可以将所有内容结合起来,并在节点上包含这些类,同时为每个类指定运行阶段:
class { 'me_first': stage => 'first',
}
class { 'me_last': stage => 'last',
}
请注意,在me_first和me_last的class声明中,我们不需要指定它们采用stage参数。stage参数是另一个元参数,意味着它可以应用于任何类或资源,而无需显式声明。当我们在 Puppet 节点上运行puppet agent时,me_first类中的通知会在first_class和second_class中的通知之前应用。me_last类的通知会在main阶段之后应用,因此它会在first_class和second_class的两个通知之后应用。如果你多次运行puppet agent,你会看到first_class和second_class的通知可能不会总是按相同的顺序出现,但me_first类将始终最先出现,me_last类将始终最后出现。
还有更多内容…
你可以定义任意数量的运行阶段,并为它们设置任何顺序。这可以大大简化一个复杂的清单,否则需要在资源之间设置大量显式依赖关系。然而要小心,避免意外引入依赖循环;当你将某个资源分配到一个运行阶段时,你自动将它与之前阶段中的所有内容关联起来。
你可能希望在site.pp文件中定义阶段,这样在清单的顶部就能清楚地看到哪些阶段是可用的。
Gary Larizza 在他的网站上写了一个有用的关于使用运行阶段的介绍,包含一些现实世界的示例:
garylarizza.com/blog/2011/03/11/using-run-stages-with-puppet/
一个警告:许多人不喜欢使用运行阶段,觉得 Puppet 已经提供了足够的资源排序控制,并且不加区分地使用运行阶段会使您的代码变得非常难以理解。在可能的情况下,应尽量减少使用运行阶段。有几个关键的例子可以证明使用阶段可以减少复杂性。最明显的是当一个资源修改用于在系统上安装软件包的系统时。在默认的 main 阶段定义包时,您的清单可以依赖于存在更新的包管理配置信息。例如,对于基于 Yum 的系统,您可以创建一个在 main 阶段之前的 yumrepos 阶段。您可以使用链箭头指定这种依赖关系,如下面的代码片段所示:
stage {'yumrepos': }
Stage['yumrepos'] -> Stage['main']
然后,我们可以创建一个创建 Yum 仓库 (yumrepo) 资源并将其分配给 yumrepos 阶段的类,如下所示:
class {'yums': stage => 'yumrepos',
}
class yums {
notify {'always before the rest': }
yumrepo {'testrepo': baseurl => 'file:///var/yum', ensure => 'present',
}
}
对于基于 Apt 的系统,相同的例子将是一个定义 Apt 源的阶段。阶段的关键在于将其定义在您的 site.pp 文件中,这样它们就会非常可见,并且只在确保不会引入依赖循环的情况下才使用它们。
参见
-
使用标签 配方,在本章中
-
绘制依赖图 配方在 第十章,监控、报告和故障排除
使用角色和配置文件
组织良好的 Puppet 清单易于阅读;模块的目的应在其名称中明显。节点的目的应在一个单一类中定义。这个单一的类应包含执行该目的所需的所有类。Craig Dunn 写了一篇关于这种分类系统的文章,他将其称为 "角色和配置文件" (www.craigdunn.org/2012/05/239/)。在这种模型中,角色是节点的单一目的,一个节点只能有一个角色,一个角色可以包含多个配置文件,而一个配置文件包含与单个服务相关的所有资源。在这个例子中,我们将创建一个使用多个配置文件的 web 服务器角色。
如何做到…
我们将创建两个模块来存储我们的角色和配置文件。角色将包含一个或多个配置文件。每个角色或配置文件将被定义为子类,例如 profile::base
-
决定您的角色和配置文件的命名策略。在我们的例子中,我们将创建两个模块,
roles和profiles,分别包含我们的角色和配置文件:$ puppet module generate thomas-profiles $ ln -s thomas-profiles profiles $ puppet module generate thomas-roles $ ln -s thomas-roles roles -
开始定义我们
webserver角色的组成部分作为配置文件。为了保持这个例子简单,我们将创建两个配置文件。首先是一个base配置文件,包括我们的基本服务器配置类。其次是一个apache类来安装和配置 Apache Web 服务器 (httpd),如下所示:$ vim profiles/manifests/base.pp class profiles::base { include base } $ vim profiles/manifests/apache.pp class profiles::apache { $apache = $::osfamily ? { 'RedHat' => 'httpd', 'Debian' => 'apache2', } service { "$apache": enable => true, ensure => true, } package { "$apache": ensure => 'installed', } } -
为我们的
webserver角色定义一个roles::webserver类,如下所示:$ vim roles/manifests/webserver.pp class roles::webserver { include profiles::apache include profiles::base } -
将
roles::webserver类应用到一个节点。在集中式安装中,你可以使用外部节点分类器(ENC)将类应用到节点,或者使用 Hiera 来定义角色:node 'webtest' { include roles::webserver }
它是如何工作的……
将 Web 服务器配置的各个部分分解成不同的配置文件,使我们能够独立地应用这些部分。我们创建了一个基本配置文件,可以扩展以包括我们希望应用到所有节点的所有资源。我们的roles::webserver类简单地包含了base和apache类。
还有更多内容……
正如我们将在下一节中看到的,我们可以将参数传递给类,以改变它们的工作方式。在我们的roles::webserver类中,我们可以使用类实例化语法,而不是include,并通过类中的parameters覆盖它。例如,要将参数传递给base类,我们可以使用:
class {'profiles::base':
parameter => 'newvalue'
}
在我们之前使用过的地方:
include profiles::base
提示
在本书的早期版本中,节点和类继承用于实现类似的目标,即代码重用。节点继承在 Puppet 版本 3.7 及更高版本中已被弃用。应该避免使用节点和类继承。使用角色和配置文件可以达到相同的可读性,并且更容易跟随。
向类传递参数
有时候,将类的某些方面参数化非常有用。例如,你可能需要管理不同版本的gem包,而不必为每个不同版本号创建单独的类,你可以将版本号作为参数传入。
如何操作……
在这个例子中,我们将创建一个接受参数的定义:
-
将参数声明为类定义的一部分:
class eventmachine($version) { package { 'eventmachine': provider => gem, ensure => $version, } } -
使用以下语法将类包含在节点上:
class { 'eventmachine': version => '1.0.3', }
它是如何工作的……
类定义class eventmachine($version) {就像普通的类定义,唯一不同的是它指定了该类需要一个参数:$version。在类内部,我们定义了一个package资源:
package { 'eventmachine':
provider => gem,
ensure => $version,
}
这是一个gem包,我们请求安装版本$version。
在节点上包含类,而不是使用通常的include语法:
include eventmachine
这样做时,会出现一个class语句:
class { 'eventmachine':
version => '1.0.3',
}
这样做的效果是一样的,但同时也为参数version设置了一个值。
还有更多内容……
你可以为类指定多个参数,如下所示:
class mysql($package, $socket, $port) {
然后以相同的方式提供它们:
class { 'mysql':
package => 'percona-server-server-5.5',
socket => '/var/run/mysqld/mysqld.sock',
port => '3306',
}
指定默认值
你还可以为一些参数提供默认值。当你在不设置参数的情况下包含类时,将使用默认值。例如,如果我们创建了一个具有三个参数的mysql类,我们可以为任何或所有参数提供默认值,如代码片段所示:
class mysql($package, $socket, $port='3306') {
或者全部:
class mysql(
package = percona-server-server-5.5",
socket = '/var/run/mysqld/mysqld.sock',
port = '3306') {
默认值允许你使用一个默认值,并在需要时覆盖该默认值。
与定义不同,一个参数化类只能在一个节点上存在一个实例。因此,如果你需要多个不同的资源实例,应该改用define。
从 Hiera 传递参数
就像我们在上一章引入的 defaults 参数一样,Hiera 可用于为类提供默认值。此功能要求 Puppet 版本 3 及以上。
准备工作
安装并配置 hiera,就像我们在 第二章 Puppet 基础设施 中所做的那样。创建一个全局或公共的 yaml 文件;这将作为所有值的默认值。
如何操作…
-
创建一个没有默认值的参数类:
t@mylaptop ~/puppet $ mkdir -p modules/mysql/manifests t@mylaptop ~/puppet $ vim modules/mysql/manifests/init.pp class mysql ( $port, $socket, $package ) { notify {"Port: $port Socket: $socket Package: $package": } } -
在 Hiera 中更新你的公共
.yaml文件,加入mysql类的默认值:--- mysql::port: 3306 mysql::package: 'mysql-server' mysql::socket: '/var/lib/mysql/mysql.sock'将类应用于节点,你现在可以将
mysql类添加到你的默认节点中。node default { class {'mysql': } } -
运行
puppet agent并验证输出:[root@hiera-test ~]# puppet agent -t Info: Caching catalog for hiera-test.example.com Info: Applying configuration version '1411182251' Notice: Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server Notice: /Stage[main]/Mysql/Notify[Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server]/message: defined 'message' as 'Port: 3306 Socket: /var/lib/mysql/mysql.sock Package: mysql-server' Notice: Finished catalog run in 1.75 seconds
它是如何工作的...
当我们在清单中实例化 mysql 类时,并没有为任何属性提供值。Puppet 知道去 Hiera 查找与 class_name::parameter_name: 或 ::class_name::parameter_name: 匹配的值。
当 Puppet 找到一个值时,它将其作为类的参数。如果 Puppet 未能在 Hiera 中找到值并且没有定义默认值,目录失败将导致以下命令行:
Error: Could not retrieve catalog from remote server: Error 400 on SERVER: Must pass package to Class[Mysql] at /etc/puppet/environments/production/manifests/site.pp:6 on node hiera-test.example.com
这个错误表示 Puppet 需要为参数 package 提供一个值。
还有更多...
你可以定义一个 Hiera 层次结构,并根据事实为参数提供不同的值。例如,你可以在层次结构中使用 %{::osfamily},并根据 osfamily 参数(如 RedHat、Suse 和 Debian)有不同的 yaml 文件。
编写可重用的跨平台清单
每个系统管理员都梦想拥有统一的、同质化的基础设施,所有机器都运行相同版本的相同操作系统。然而,正如生活中的其他领域一样,现实通常是混乱的,并且与计划不符。
你可能负责一堆不同年龄和架构的服务器,这些服务器运行着不同内核的不同操作系统,通常分布在不同的数据中心和 ISP 上。
这种情况应该会让 SSH 中的系统管理员感到恐惧,因为在每台服务器上执行相同的命令可能会导致不同的、不可预测的,甚至是危险的结果。
我们当然应该努力使旧服务器保持最新,并尽可能在单一参考平台上工作,以简化管理、降低成本并提高可靠性。但在我们达到这个目标之前,Puppet 使得应对异构环境稍微容易一些。
如何操作…
下面是一些使你的清单更具可移植性的示例:
-
当你需要将相同的清单应用于不同操作系统分发版的服务器时,主要的差异可能是包和服务的名称,以及配置文件的位置。尝试通过使用选择器设置全局变量,将所有这些差异捕捉到一个类中:
$ssh_service = $::operatingsystem? { /Ubuntu|Debian/ => 'ssh', default => 'sshd', }你不必担心清单中任何其他部分的差异;当你引用某些内容时,可以放心地使用变量,它将在每个环境中指向正确的内容:
service { $ssh_service: ensure => running, } -
我们经常需要处理混合架构;这可能会影响共享库的路径,并且可能需要不同版本的软件包。同样,尽量将所有所需的设置封装在一个架构类中,该类设置全局变量:
$libdir = $::architecture ? { /amd64|x86_64/ => '/usr/lib64', default => '/usr/lib', }然后你可以在清单中或甚至在模板中使用这些变量,任何需要架构相关值的地方:
; php.ini [PHP] ; Directory in which the loadable extensions (modules) reside. extension_dir = <%= @libdir %>/php/modules
它是如何工作的...
这种方法(可以称之为自上而下)的优点是你只需要做一次选择。另一种选择是自下而上的方法,即每次使用设置时,都需要有一个选择器或case语句:
service { $::operatingsystem? {
/Ubuntu|Debian/ => 'ssh', default => 'sshd' }: ensure => running,
}
这样不仅会导致大量重复,还会使代码更难以阅读。而且,当新操作系统被加入时,你需要在整个清单中进行修改,而不是仅仅在一个地方进行修改。
还有更多…
如果你正在为公共发布编写模块(例如,在 Puppet Forge 上),尽可能使你的模块跨平台,将使其对社区更有价值。尽可能在许多不同的发行版、平台和架构上进行测试,并添加适当的变量,以确保它能在各处运行。
如果你使用了一个公共模块并将其调整为适应自己的环境,考虑在认为这些更改可能对其他人有帮助时,将这些更改更新到公共版本中。
即使你不打算发布一个模块,也要记住它可能会在生产环境中使用很长时间,并且可能需要适应环境中的许多变化。如果从一开始就设计好应对这些变化的功能,将使你或最终维护你代码的人轻松许多。
| "永远编写代码,假设维护你代码的人是一个暴力的精神病患者,知道你住在哪里。" | ||
|---|---|---|
| --戴夫·卡哈特 |
另见
-
第七章中的使用公共模块配方,管理应用程序
-
第二章中的配置 Hiera配方,Puppet 基础架构
获取环境信息
在 Puppet 清单中,你经常需要了解你所在机器的一些本地信息。Facter 是与 Puppet 一起使用的工具,提供了一种标准的方法来从环境中获取信息(事实),例如以下内容:
-
操作系统
-
内存大小
-
架构
-
处理器数量
要查看系统上可用的事实的完整列表,运行:
$ sudo facter
architecture => amd64
augeasversion => 0.10.0
domain => compute-1.internal
ec2_ami_id => ami-137bcf7a
ec2_ami_launch_index => 0
注意
虽然从命令行获取这些信息很方便,但 Facter 的真正强大之处在于能够在 Puppet 清单中访问这些事实。
一些模块定义了自己的事实;要查看任何已本地定义的事实,可以在运行 facter 时添加-p (pluginsync)选项,如下所示:
$ sudo facter -p
如何实现…
以下是使用 Facter 事实在清单中的示例:
-
像引用其他变量一样在清单中引用 Facter 事实。事实是 Puppet 中的全局变量,因此它们应以双冒号(
::)为前缀,如以下代码片段所示:notify { "This is $::operatingsystem version $::operatingsystemrelease, on $::architecture architecture, kernel version $::kernelversion": } -
当 Puppet 运行时,它将为当前节点填充适当的值:
[root@hiera-test ~]# puppet agent -t ... Info: Applying configuration version '1411275985'Notice: This is RedHat version 6.5, on x86_64 architecture, kernel version 2.6.32 ... Notice: Finished catalog run in 0.40 seconds
它是如何工作的…
Facter 提供了一种标准方式,使清单能够获取它们所应用节点的信息。当你在清单中引用一个事实时,Puppet 将查询 Facter 以获取当前值并将其插入清单。Facter 事实是顶级作用域变量。
小贴士
始终使用前导的双冒号引用事实,以确保你使用的是事实而不是本地变量:
$::hostname 而不是 $hostname
还有更多…
你还可以在 ERB 模板中使用事实。例如,你可能想将节点的主机名插入到文件中,或者根据节点的内存大小更改应用程序的配置设置。当在模板中使用事实名称时,请记住它们不需要美元符号,因为这是 Ruby 而不是 Puppet:
$KLogPath <%= case @kernelversion when '2.6.31' then
'/var/run/rsyslog/kmsg' else '/proc/kmsg' end %>
引用事实时,请使用@语法。在与模板函数调用相同作用域中定义的变量也可以使用@语法引用。作用域外的变量应使用scope函数。例如,要引用我们在mysql模块中之前定义的mysql::port变量,请使用以下代码:
MySQL Port = <%= scope['::mysql::port'] %>
应用此模板会生成以下文件:
[root@hiera-test ~]# puppet agent -t
...
Info: Caching catalog for hiera-test.example.com
Notice: /Stage[main]/Erb/File[/tmp/template-test]/ensure: defined content as '{md5}96edacaf9747093f73084252c7ca7e67'
Notice: Finished catalog run in 0.41 seconds [root@hiera-test ~]# cat /tmp/template-test
MySQL Port = 3306
另见
- 第九章中的创建自定义事实示例,外部工具与 Puppet 生态系统
导入动态信息
尽管有些系统管理员喜欢通过堆积一堆旧打印机将自己与办公室的其他部分隔离开来,但我们都需要不时地与其他部门交换信息。例如,你可能希望将来自外部来源的数据插入到 Puppet 清单中。generate函数非常适合这种情况。函数在编译目录的机器上执行(对于集中式部署是主节点);像这里示例的代码只适用于无主配置。
准备中
按照以下步骤准备运行示例:
-
创建脚本
/usr/local/bin/message.rb,其内容如下:#!/usr/bin/env ruby puts "This runs on the master if you are centralized" -
使脚本可执行:
$ sudo chmod a+x /usr/local/bin/message.rb
如何做到这一点…
此示例调用我们之前创建的外部脚本并获取其输出:
-
创建包含以下内容的
message.pp清单:$message = generate('/usr/local/bin/message.rb') notify { $message: } -
运行 Puppet:
$ puppet apply message.pp ... Notice: /Stage[main]/Main/Notify[This runs on the master if you are centralized ]/message: defined 'message' as 'This runs on the master if you are centralized
它是如何工作的…
generate函数运行指定的脚本或程序并返回结果,在此例中是来自 Ruby 的一个愉快消息。
目前来看这并不是非常有用,但你明白其思路了。脚本可以做的任何事情,比如打印、获取或计算数据(例如数据库查询的结果),都可以通过generate引入到你的清单中。当然,你也可以运行标准的 UNIX 工具,例如cat和grep。
还有更多…
如果你需要向由generate调用的可执行文件传递参数,可以将它们作为额外的参数添加到函数调用中:
$message = generate('/bin/cat', '/etc/motd')
Puppet 会通过限制在调用中可以使用的字符来保护你免受恶意的 Shell 调用,例如,不允许使用 Shell 管道和重定向。最简单且最安全的方法是将所有逻辑放入脚本中,然后调用这个脚本。
另见
-
第九章中的创建自定义事实教程,外部工具与 Puppet 生态系统
-
第二章中的配置 Hiera教程,Puppet 基础设施
向 Shell 命令传递参数
如果你想将值插入命令行(例如由exec资源运行),它们通常需要加引号,尤其是当它们包含空格时。shellquote函数可以接收任意数量的参数,包括数组,并对每个参数加引号,然后将它们作为空格分隔的字符串返回,你可以将这个字符串传递给命令。
在这个示例中,我们想设置一个exec资源来重命名文件;但源文件名和目标文件名都包含空格,因此需要在命令行中正确加引号。
如何做到…
下面是使用shellquote函数的示例:
-
使用以下命令创建
shellquote.pp清单:$source = 'Hello Jerry' $target = 'Hello... Newman' $argstring = shellquote($source, $target) $command = "/bin/mv ${argstring}" notify { $command: } -
运行 Puppet:
$ puppet apply shellquote.pp ... Notice: /bin/mv "Hello Jerry" "Hello... Newman" Notice: /Stage[main]/Main/Notify[/bin/mv "Hello Jerry" "Hello... Newman"]/message: defined 'message' as '/bin/mv "Hello Jerry" "Hello... Newman"'
它是如何工作的…
首先,我们定义了$source和$target变量,这两个变量分别是我们在命令行中使用的文件名:
$source = 'Hello Jerry'
$target = 'Hello... Newman'
接着,我们调用shellquote将这些变量拼接成一个加引号的、空格分隔的字符串,如下所示:
$argstring = shellquote($source, $target)
然后,我们组合成最终的命令行:
$command = "/bin/mv ${argstring}"
结果将会是:
/bin/mv "Hello Jerry" "Hello... Newman"
现在,可以使用exec资源运行此命令行。如果我们不使用shellquote,会发生什么?
$source = 'Hello Jerry'
$target = 'Hello... Newman'
$command = "/bin/mv ${source} ${target}"
notify { $command: }
Notice: /bin/mv Hello Jerry Hello... Newman
这将无法正常工作,因为mv命令期望接收空格分隔的参数,因此它会将这条命令解释为将三个文件Hello、Jerry和Hello...移动到名为Newman的目录中,而这可能不是我们想要的结果。
第四章:处理文件和软件包
| "作家的职责是做好,不是做得差;真实,不是虚假;生动,不是乏味;准确,不是充满错误的。" | ||
|---|---|---|
| --E.B. White |
在本章中,我们将覆盖以下方案:
-
快速编辑配置文件
-
使用 puppetlabs-inifile 编辑 INI 风格的文件
-
使用 Augeas 可靠地编辑配置文件
-
使用代码片段构建配置文件
-
使用 ERB 模板
-
在模板中使用数组迭代
-
使用 EPP 模板
-
使用 GnuPG 加密机密数据
-
从第三方仓库安装软件包
-
比较软件包版本
介绍
在本章中,我们将学习如何对文件进行小范围编辑,如何使用Augeas工具以结构化的方式进行更大范围的更改,如何从拼接的代码片段构建文件,以及如何从模板生成文件。我们还将学习如何从附加的仓库安装软件包,并管理这些仓库。此外,我们将学习如何使用 Puppet 存储和解密机密数据。
快速编辑配置文件
当你需要让 Puppet 修改配置文件中的某个特定设置时,通常的做法是直接通过 Puppet 部署整个文件。然而,这并不总是可行,尤其是当这是一个 Puppet 清单的多个部分可能需要修改的文件时。
一个有用的做法是提供一个简单的方案,向配置文件中添加一行(如果该行尚不存在)。例如,向/etc/modules添加模块名称,以便在启动时让内核加载该模块。有几种方法可以做到这一点,最简单的是使用puppetlabs-stdlib模块提供的file_line类型。在这个例子中,我们安装了stdlib模块并使用该类型向文本文件追加一行。
准备工作
使用 Puppet 安装puppetlabs-stdlib模块:
t@mylaptop ~ $ puppet module install puppetlabs-stdlib
Notice: Preparing to install into /home/thomas/.puppet/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/thomas/.puppet/modules
└── puppetlabs-stdlib (v4.5.1)
这会将模块从 forge 安装到我的用户 Puppet 目录;如果要安装到系统目录中,可以以 root 身份运行命令或使用sudo。为了方便本示例,我们将继续作为当前用户操作。
如何做...
使用file_line资源类型,我们可以确保某一行在配置文件中存在或不存在。使用file_line我们可以快速对文件进行编辑,而无需控制整个文件。
-
创建一个名为
oneline.pp的清单文件,该文件将使用file_line对/tmp中的文件进行操作:file {'/tmp/cookbook': ensure => 'file', } file_line {'cookbook-hello': path => '/tmp/cookbook', line => 'Hello World!', require => File['/tmp/cookbook'], } -
在
oneline.pp清单上运行puppet apply:t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp Notice: Compiled catalog for mylaptop in environment production in 0.39 seconds Notice: /Stage[main]/Main/File[/tmp/cookbook]/ensure: created Notice: /Stage[main]/Main/File_line[cookbook-hello]/ensure: created Notice: Finished catalog run in 0.02 seconds -
现在验证
/tmp/cookbook是否包含我们定义的那一行:t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook Hello World!
它是如何工作的...
我们将puppetlabs-stdlib模块安装到了 Puppet 的默认模块路径中,因此当我们运行puppet apply时,Puppet 知道去哪里找到file_line类型的定义。然后,Puppet 会在/tmp/cookbook文件不存在时创建该文件。由于文件中没有找到Hello World!这一行,Puppet 将这行添加到文件中。
还有更多…
我们可以定义更多的file_line实例,并向文件中添加更多行;我们可以让多个资源修改同一个文件。
修改oneline.pp文件并添加另一个file_line资源:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-hello':
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-goodbye':
path => '/tmp/cookbook',
line => 'So long, and thanks for all the fish.',
require => File['/tmp/cookbook'],
}
现在再次应用清单并验证新行是否已追加到文件中:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.36 seconds
Notice: /Stage[main]/Main/File_line[cookbook-goodbye]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
file_line类型也支持模式匹配和行删除,正如我们将在下面的示例中展示的:
file {'/tmp/cookbook':
ensure => 'file',
}
file_line {'cookbook-remove':
ensure => 'absent',
path => '/tmp/cookbook',
line => 'Hello World!',
require => File['/tmp/cookbook'],
}
file_line {'cookbook-match':
path => '/tmp/cookbook',
line => 'Oh freddled gruntbuggly, thanks for all the fish.',
match => 'fish.$',
require => File['/tmp/cookbook'],
}
在运行 Puppet 之前,验证/tmp/cookbook的内容:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Hello World!
So long, and thanks for all the fish.
应用更新后的清单:
t@mylaptop ~/.puppet/manifests $ puppet apply oneline.pp
Notice: Compiled catalog for mylaptop in environment production in 0.30 seconds
Notice: /Stage[main]/Main/File_line[cookbook-match]/ensure: created
Notice: /Stage[main]/Main/File_line[cookbook-remove]/ensure: removed
Notice: Finished catalog run in 0.02 seconds
验证该行已被删除,且“goodbye”行已被替换:
t@mylaptop ~/.puppet/manifests $ cat /tmp/cookbook
Oh freddled gruntbuggly, thanks for all the fish.
使用file_line编辑文件对于结构简单的文件效果很好。结构化文件可能在不同部分有相似的行,但其含义不同。在接下来的部分中,我们将向你展示如何处理一种特定类型的结构化文件——使用INI 语法的文件。
使用 puppetlabs-inifile 编辑 INI 风格的文件
INI 文件在许多系统中都有使用,Puppet 在puppet.conf文件中使用 INI 语法。puppetlabs-inifile模块创建了两种类型,ini_setting和ini_subsetting,它们可以用来编辑 INI 风格的文件。
准备就绪
按照以下方式从 Forge 安装模块:
t@mylaptop ~ $ puppet module install puppetlabs-inifile
Notice: Preparing to install into /home/tuphill/.puppet/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/tuphill/.puppet/modules
└── puppetlabs-inifile (v1.1.3)
如何操作……
在这个示例中,我们将创建一个/tmp/server.conf文件,并确保该文件中设置了server_true:
-
创建一个
initest.pp清单,内容如下:ini_setting {'server_true': path => '/tmp/server.conf', section => 'main', setting => 'server', value => 'true', } -
应用清单:
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp Notice: Compiled catalog for burnaby in environment production in 0.14 seconds Notice: /Stage[main]/Main/Ini_setting[server_true]/ensure: created Notice: Finished catalog run in 0.02 seconds -
验证
/tmp/server.conf文件的内容:t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf [main] server = true
它是如何工作的……
inifile模块定义了两种类型,ini_setting和ini_subsetting。我们的清单定义了一个ini_setting资源,它在ini文件的主部分中创建了一个server = true设置。在我们的例子中,文件不存在,所以 Puppet 创建了该文件,然后创建了main部分,最后将设置添加到main部分。
还有更多……
使用ini_subsetting,你可以将多个资源添加到一个设置中。例如,我们的server.conf文件中有一行 server,我们可以让每个节点将其主机名追加到这行 server 后面。将以下内容添加到initest.pp文件的末尾:
ini_subsetting {'server_name':
path => '/tmp/server.conf',
section => 'main',
setting => 'server_host',
subsetting => "$hostname",
}
应用清单:
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for mylaptop in environment production in 0.34 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop
现在临时更改你的主机名并重新运行 Puppet:
t@mylaptop ~/.puppet/manifests $ sudo hostname inihost
t@mylaptop ~/.puppet/manifests $ puppet apply initest.pp
Notice: Compiled catalog for inihost in environment production in 0.43 seconds
Notice: /Stage[main]/Main/Ini_subsetting[server_name]/ensure: created
Notice: Finished catalog run in 0.02 seconds
t@mylaptop ~/.puppet/manifests $ cat /tmp/server.conf
[main]
server = true
server_host = mylaptop inihost
提示
在处理 INI 语法文件时,使用inifile模块是一个极好的选择。
如果你的配置文件不是 INI 语法格式,可以使用另一个工具 Augeas。在接下来的部分中,我们将使用augeas来修改文件。
使用 Augeas 可靠地编辑配置文件
有时候似乎每个应用程序都有自己微妙不同的配置文件格式,而编写正则表达式来解析和修改这些文件可能是件乏味的事。
幸好有 Augeas 来帮忙。Augeas 是一个旨在简化不同配置文件格式处理的系统,它将所有文件呈现为一个简单的值树。Puppet 对 Augeas 的支持允许你创建augeas资源,这些资源能够智能且自动地进行所需的配置更改。
如何操作…
按照以下步骤创建一个示例augeas资源:
-
按如下方式修改你的
base模块:class base { augeas { 'enable-ip-forwarding': incl => '/etc/sysctl.conf', lens => 'Sysctl.lns', changes => ['set net.ipv4.ip_forward 1'], } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Applying configuration version '1412130479' Notice: Augeasenable-ip-forwarding: --- /etc/sysctl.conf 2014-09-04 03:41:09.000000000 -0400 +++ /etc/sysctl.conf.augnew 2014-09-30 22:28:03.503000039 -0400 @@ -4,7 +4,7 @@ # sysctl.conf(5) for more details. # Controls IP packet forwarding -net.ipv4.ip_forward = 0 +net.ipv4.ip_forward = 1 # Controls source route verification net.ipv4.conf.default.rp_filter = 1 Notice: /Stage[main]/Base/Augeas[enable-ip-forwarding]/returns: executed successfully Notice: Finished catalog run in 2.27 seconds -
检查设置是否已正确应用:
[root@cookbook ~]# sysctl -p |grep ip_forward net.ipv4.ip_forward = 1
它是如何工作的……
我们声明一个名为enable-ip-forwarding的augeas资源:
augeas { 'enable-ip-forwarding':
我们指定希望在文件/etc/sysctl.conf中进行更改:
incl => '/etc/sysctl.conf',
接下来,我们指定要在该文件上使用的镜头。Augeas 使用名为“镜头”的文件,将配置文件转换为对象表示。Augeas 默认附带多个镜头,位于/usr/share/augeas/lenses目录。指定augeas资源中的镜头时,镜头名称会被大写并带有.lns后缀。在本例中,我们将指定Sysctl镜头,具体如下:
lens => 'Sysctl.lns',
changes参数指定我们希望进行的更改。它的值是一个数组,因为我们可以一次提供多个更改。在这个示例中,只有一个更改,因此其值是一个包含一个元素的数组:
changes => ['set net.ipv4.ip_forward 1'],
一般来说,Augeas 的更改形式如下:
set <parameter> <value>
在这种情况下,设置将在/etc/sysctl.conf中被翻译为如下所示的一行:
net.ipv4.ip_forward=1
还有更多…
我选择/etc/sysctl.conf作为示例,因为它可以包含多种内核设置,你可能希望出于各种不同的目的以及在不同的 Puppet 类中更改这些设置。比如在示例中,你可能希望为路由器类启用 IP 转发,但你也可能希望为负载均衡器类调整net.core.somaxconn的值。
这意味着仅仅将/etc/sysctl.conf文件以文本文件形式传递并分发是行不通的,因为根据你要修改的设置,可能会有多个不同且冲突的版本。在这里,Augeas 是一个正确的解决方案,因为你可以在不同的位置定义augeas资源,这些资源会修改同一个文件,且它们不会发生冲突。
关于如何使用 Puppet 和 Augeas 的更多信息,请参阅 Puppet Labs 网站上的页面projects.puppetlabs.com/projects/1/wiki/Puppet_Augeas。
另一个使用 Augeas 的项目是Augeasproviders。Augeasproviders 使用 Augeas 定义了几种类型。其一是sysctl类型,使用此类型可以在不需要了解如何在 Augeas 中编写更改的情况下进行 sysctl 更改。有关更多信息,请访问forge.puppetlabs.com/domcleal/augeasproviders。
一开始学习如何使用 Augeas 可能有点令人困惑。Augeas 提供了一个命令行工具augtool,可以用来熟悉在 Augeas 中进行更改。
使用片段构建配置文件
有时你不能将整个配置文件一次性部署,而逐行编辑又不够。通常,你需要从由不同类管理的各种配置片段中构建配置文件。你可能还会遇到需要将本地信息导入到文件中的情况。在这个示例中,我们将使用本地文件以及我们在清单中定义的片段来构建配置文件。
准备工作
虽然我们可以创建自己的系统从各个部分构建文件,但我们将使用 Puppetlabs 支持的 concat 模块。我们将从安装 concat 模块开始,在之前的示例中我们将模块安装到了本地机器中。在这个示例中,我们将修改 Puppet 服务器配置,并将模块下载到 Puppet 服务器。
在你的 Git 仓库中创建一个 environment.conf 文件,内容如下:
modulepath = public:modules
manifest = manifests/site.pp
创建公共目录并将模块下载到该目录,方法如下:
t@mylaptop ~/puppet $ mkdir public && cd public
t@mylaptop ~/puppet/public $ puppet module install puppetlabs-concat --modulepath=.
Notice: Preparing to install into /home/thomas/puppet/public ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/thomas/puppet/public
└─┬ puppetlabs-concat (v1.1.1)
└── puppetlabs-stdlib (v4.3.2)
现在将新模块添加到我们的 Git 仓库中:
t@mylaptop ~/puppet/public $ git add .
t@mylaptop ~/puppet/public $ git commit -m "adding concat"
[production 50c6fca] adding concat
407 files changed, 20089 insertions(+)
然后推送到我们的 Git 服务器:
t@mylaptop ~/puppet/public $ git push origin production
如何操作……
现在我们已经在服务器上可用 concat 模块,我们可以在 base 模块中创建一个 concat 容器资源:
concat {'hosts.allow':
path => '/etc/hosts.allow',
mode => 0644
}
为新文件的头部创建一个concat::fragment模块:
concat::fragment {'hosts.allow header':
target => 'hosts.allow',
content => "# File managed by puppet\n",
order => '01'
}
创建一个包含本地文件的concat::fragment:
concat::fragment {'hosts.allow local':
target => 'hosts.allow',
source => '/etc/hosts.allow.local',
order => '10',
}
创建一个将在文件末尾的 concat::fragment 模块:
concat::fragment {'hosts.allow tftp':
target => 'hosts.allow',
content => "in.ftpd: .example.com\n",
order => '50',
}
在节点上,创建 /etc/hosts.allow.local,内容如下:
in.tftpd: .example.com
运行 Puppet 以创建文件:
[root@cookbook ~]# puppet agent -t
Info: Caching catalog for cookbook.example.com
Info: Applying configuration version '1412138600'
Notice: /Stage[main]/Base/Concat[hosts.allow]/File[hosts.allow]/ensure: defined content as '{md5}b151c8bbc32c505f1c4a98b487f7d249'
Notice: Finished catalog run in 0.29 seconds
验证新文件的内容如下:
[root@cookbook ~]# cat /etc/hosts.allow
# File managed by puppet
in.tftpd: .example.com
in.ftpd: .example.com
它是如何工作的……
concat 资源定义了一个容器,将包含所有随后的 concat::fragment 资源。每个 concat::fragment 资源都将 concat 资源作为目标。每个 concat::fragment 还包括一个 order 属性。order 属性用于指定将片段添加到最终文件的顺序。我们的 /etc/hosts.allow 文件由头行、本地文件的内容以及我们定义的 in.tftpd 行组成。
使用 ERB 模板
尽管你可以像简单的文本文件一样使用 Puppet 部署配置文件,但模板更为强大。模板文件可以进行计算、执行 Ruby 代码,或引用 Puppet 清单中的变量值。你可以在任何使用 Puppet 部署文本文件的地方,使用模板代替。
在最简单的情况下,模板可以只是一个静态文本文件。更有用的是,你可以使用 ERB(嵌入式 Ruby)语法将变量插入其中。例如:
<%= @name %>, this is a very large drink.
如果模板在一个变量 $name 包含 Zaphod Beeblebrox 的上下文中使用,那么模板将评估为:
Zaphod Beeblebrox, this is a very large drink.
这种简单的技术对于生成大量仅在一个或两个变量值上有所不同的文件非常有用,例如虚拟主机,并且可以将值插入到脚本中,比如数据库名称和密码。
如何操作……
在这个例子中,我们将使用一个 ERB 模板将密码插入到备份脚本中:
-
创建文件
modules/admin/templates/backup-mysql.sh.erb,内容如下:#!/bin/sh /usr/bin/mysqldump -uroot \ -p<%= @mysql_password %> \ --all-databases | \ /bin/gzip > /backup/mysql/all-databases.sql.gz -
按照如下方式修改你的
site.pp文件:node 'cookbook' { $mysql_password = 'secret' file { '/usr/local/bin/backup-mysql': content => template('admin/backup-mysql.sh.erb'), mode => '0755', } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412140971' Notice: /Stage[main]/Main/Node[cookbook]/File[/usr/local/bin/backup-mysql]/ensure: defined content as '{md5}c12af56559ef36529975d568ff52dca5' Notice: Finished catalog run in 0.31 seconds -
检查 Puppet 是否正确地将密码插入模板:
[root@cookbook ~]# cat /usr/local/bin/backup-mysql #!/bin/sh /usr/bin/mysqldump -uroot \ -psecret \ --all-databases | \ /bin/gzip > /backup/mysql/all-databases.sql.gz
它是如何工作的……
在模板中引用的每个变量,例如 <%= @mysql_password %>,Puppet 将用相应的值 secret 替换它。
还有更多……
在示例中,我们只使用了一个变量,但你可以根据需要使用多个变量。它们也可以是事实:
ServerName <%= @fqdn %>
或者是 Ruby 表达式:
MAILTO=<%= @emails.join(',') %>
或者是你想要的任何 Ruby 代码:
ServerAdmin <%= @sitedomain == 'coldcomfort.com' ? 'seth@coldcomfort.com' : 'flora@poste.com' %>
另请参阅
-
本章中的 使用 GnuPG 加密机密信息 配方
在模板中使用数组迭代
在前面的示例中,我们看到你可以使用 Ruby 根据表达式的结果在模板中插入不同的值。但是,你并不局限于一次插入一个值。你可以将多个值放入 Puppet 数组中,然后通过循环让模板为数组中的每个元素生成内容。
如何实现…
按照以下步骤构建一个数组迭代的示例:
-
按照以下方式修改你的
site.pp文件:node 'cookbook' { $ipaddresses = ['192.168.0.1', '158.43.128.1', '10.0.75.207' ] file { '/tmp/addresslist.txt': content => template('base/addresslist.erb') } } -
创建文件
modules/base/templates/addresslist.erb,并写入以下内容:<% @ipaddresses.each do |ip| -%> IP address <%= ip %> is present <% end -%> -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412141917' Notice: /Stage[main]/Main/Node[cookbook]/File[/tmp/addresslist.txt]/ensure: defined content as '{md5}073851229d7b2843830024afb2b3902d' Notice: Finished catalog run in 0.30 seconds -
检查生成文件的内容:
[root@cookbook ~]# cat /tmp/addresslist.txt IP address 192.168.0.1 is present. IP address 158.43.128.1 is present. IP address 10.0.75.207 is present.
它是如何工作的…
在模板的第一行,我们引用了数组 ipaddresses,并调用了它的 each 方法:
<% @ipaddresses.each do |ip| -%>
在 Ruby 中,这会创建一个循环,每次迭代都会执行一次,并且每次循环时,变量 ip 会被设置为当前元素的值。
在我们的示例中,ipaddresses 数组包含三个元素,因此接下来的行将执行三次,每次针对一个元素:
IP address <%= ip %> is present.
这将生成三行输出:
IP address 192.168.0.1 is present.
IP address 158.43.128.1 is present.
IP address 10.0.75.207 is present.
最后一行结束循环:
<% end -%>
注意
请注意,第一行和最后一行以 -%> 结束,而不是我们之前看到的 %>。- 的作用是抑制每次循环时生成的新行,这样就不会在文件中产生不必要的空行。
还有更多内容…
模板还可以对哈希或哈希数组进行迭代:
$interfaces = [ {name => 'eth0', ip => '192.168.0.1'},
{name => 'eth1', ip => '158.43.128.1'},
{name => 'eth2', ip => '10.0.75.207'} ]
<% @interfaces.each do |interface| -%>
Interface <%= interface['name'] %> has the address <%= interface['ip'] %>.
<% end -%>
Interface eth0 has the address 192.168.0.1.
Interface eth1 has the address 158.43.128.1.
Interface eth2 has the address 10.0.75.207.
另请参阅
- 本章中的 使用 ERB 模板 配方
使用 EPP 模板
EPP 模板是 Puppet 3.5 及更新版本中的新特性。EPP 模板使用类似于 ERB 模板的语法,但不通过 Ruby 编译。定义了两个新函数来调用 EPP 模板,epp 和 inline_epp。这两个函数分别是 ERB 函数 template 和 inline_template 的 EPP 等效函数。EPP 模板的主要区别是,变量是使用 Puppet 语法引用的,$variable 而不是 @variable。
如何实现…
-
在
~/puppet/epp-test.epp中创建一个 EPP 模板,内容如下:This is <%= $message %>. -
创建一个
epp.pp清单,使用epp和inline_epp函数:$message = "the message" file {'/tmp/epp-test': content => epp('/home/thomas/puppet/epp-test.epp') } notify {inline_epp('Also prints <%= $message %>'):} -
应用清单时,请确保使用未来解析器(未来解析器是定义
epp和inline_epp函数所必需的):t@mylaptop ~/puppet $ puppet apply epp.pp --parser=future Notice: Compiled catalog for mylaptop in environment production in 1.03 seconds Notice: /Stage[main]/Main/File[/tmp/epp-test]/ensure: defined content as '{md5}999ccc2507d79d50fae0775d69b63b8c' Notice: Also prints the message -
验证模板是否按预期工作:
t@mylaptop ~/puppet $ cat /tmp/epp-test This is the message.
它是如何工作的...
使用未来解析器,定义了 epp 和 inline_epp 函数。EPP 模板与 ERB 模板的主要区别在于,变量是以与 Puppet 清单中相同的方式进行引用的。
还有更多内容…
epp和inline_epp都允许在函数调用中重写变量。函数调用的第二个参数可以用于为函数作用域内使用的变量指定值。例如,我们可以用以下代码重写$message的值:
file {'/tmp/epp-test':
content => epp('/home/tuphill/puppet/epp-test.epp',
{ 'message' => "override $message"} )
}
notify {inline_epp('Also prints <%= $message %>',
{ 'message' => "inline override $message"}):}
现在,当我们运行 Puppet 并验证输出时,我们看到$message的值已被重写:
t@mylaptop ~/puppet $ puppet apply epp.pp --parser=future
Notice: Compiled catalog for mylaptop.pan.costco.com in environment production in 0.85 seconds
Notice: Also prints inline override the message
Notice: Finished catalog run in 0.05 seconds
t@mylaptop ~/puppet $ cat /tmp/epp-test
This is override the message.
使用 GnuPG 加密机密信息
我们通常需要 Puppet 访问机密信息,例如密码或加密密钥,以便它能够正确配置系统。但如何避免将这些机密信息直接放入 Puppet 代码中呢?这样会导致任何具有读取权限的人都能看到这些信息。
第三方开发人员和承包商通常需要通过 Puppet 进行更改,但他们绝对不应看到任何机密信息。类似地,如果您使用的是如第二章所描述的分布式 Puppet 设置,Puppet 基础设施,那么每台机器都有整个仓库的副本,其中包括它不需要且不应拥有的其他机器的机密信息。我们如何防止这种情况发生?
一个方法是使用GnuPG工具加密机密信息,以便在没有适当密钥的情况下,Puppet 仓库中的任何机密信息都无法被解密(在实际操作中)。然后,我们将密钥安全地分发给需要它的人或机器。
准备工作
首先,您需要一个加密密钥,请按照以下步骤生成一个。如果您已经有一个想要使用的 GnuPG 密钥,可以跳过此部分,进入下一节。要完成此部分,您需要安装 gpg 命令:
-
使用
puppet资源安装 gpg:# puppet resource package gnupg ensure=installed提示
根据目标操作系统的不同,您可能需要使用 gnupg2 作为包名称。
-
运行以下命令。按照提示回答问题,除了将我的名字和电子邮件地址替换为您的信息。当系统提示输入密码短语时,直接按Enter:
t@mylaptop ~/puppet $ gpg --gen-key gpg (GnuPG) 1.4.18; Copyright (C) 2014 Free Software Foundation, Inc. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Please select what kind of key you want: (1) RSA and RSA (default) (2) DSA and Elgamal (3) DSA (sign only) (4) RSA (sign only) Your selection? 1 RSA keys may be between 1024 and 4096 bits long. What keysize do you want? (2048) 2048 Requested keysize is 2048 bits Please specify how long the key should be valid. 0 = key does not expire <n> = key expires in n days <n>w = key expires in n weeks <n>m = key expires in n months <n>y = key expires in n years Key is valid for? (0) 0 Key does not expire at all Is this correct? (y/N) y You need a user ID to identify your key; the software constructs the user ID from the Real Name, Comment and Email Address in this form: "Heinrich Heine (Der Dichter) <heinrichh@duesseldorf.de>" Real name: Thomas Uphill Email address: thomas@narrabilis.com Comment: <enter> You selected this USER-ID: "Thomas Uphill <thomas@narrabilis.com>" Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o You need a Passphrase to protect your secret key.在这里按两次回车以设置空的密码短语
You don't want a passphrase - this is probably a *bad* idea! I will do it anyway. You can change your passphrase at any time, using this program with the option "--edit-key". gpg: key F1C1EE49 marked as ultimately trusted public and secret key created and signed. gpg: checking the trustdb gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u pub 2048R/F1C1EE49 2014-10-01 Key fingerprint = 461A CB4C 397F 06A7 FB82 3BAD 63CF 50D8 F1C1 EE49 uid Thomas Uphill <thomas@narrabilis.com> sub 2048R/E2440023 2014-10-01 -
如果您的系统没有配置随机源,您可能会看到类似这样的消息:
We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy. -
在这种情况下,安装并启动一个随机数生成守护进程,如
haveged或rng-tools。将刚刚创建的 gpg 密钥复制到 Puppet 主机上puppet用户的账户中:t@mylaptop ~ $ scp -r .gnupg puppet@puppet.example.com: gpg.conf 100% 7680 7.5KB/s 00:00 random_seed 100% 600 0.6KB/s 00:00 pubring.gpg 100% 1196 1.2KB/s 00:00 secring.gpg 100% 2498 2.4KB/s 00:00 trustdb.gpg 100% 1280 1.3KB/s 00:00
如何操作...
当您的加密密钥安装在puppet用户的密钥环中(上一节描述的密钥生成过程会为您完成这项工作)时,您已经准备好配置 Puppet 来解密机密信息。
-
创建以下目录:
t@cookbook:~/puppet$ mkdir -p modules/admin/lib/puppet/parser/functions -
创建文件
modules/admin/lib/puppet/parser/functions/secret.rb,并添加以下内容:module Puppet::Parser::Functions newfunction(:secret, :type => :rvalue) do |args| 'gpg --no-tty -d #{args[0]}' end end -
创建文件
secret_message,并添加以下内容:For a moment, nothing happened. Then, after a second or so, nothing continued to happen. -
使用以下命令加密此文件(使用您在创建 GnuPG 密钥时提供的电子邮件地址):
t@mylaptop ~/puppet $ gpg -e -r thomas@narrabilis.com secret_message -
将生成的加密文件移动到 Puppet 仓库中:
t@mylaptop:~/puppet$ mv secret_message.gpg modules/admin/files/ -
删除原始的(明文)文件:
t@mylaptop:~/puppet$ rm secret_message -
按如下方式修改您的
site.pp文件:node 'cookbook' { $message = secret('/etc/puppet/environments/production/ modules/admin/files/secret_message.gpg') notify { "The secret message is: ${message}": } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1412145910' Notice: The secret message is: For a moment, nothing happened. Then, after a second or so, nothing continued to happen. Notice: Finished catalog run in 0.27 seconds
它是如何工作的...
首先,我们创建了一个自定义函数,允许 Puppet 使用 GnuPG 解密秘密文件:
module Puppet::Parser::Functions
newfunction(:secret, :type => :rvalue) do |args|
'gpg --no-tty -d #{args[0]}'
end
end
上述代码创建了一个名为secret的函数,该函数接受一个文件路径作为参数并返回解密后的文本。它不管理加密密钥,因此您需要确保puppet用户已安装必要的密钥。您可以通过以下命令检查:
puppet@puppet:~ $ gpg --list-secret-keys
/var/lib/puppet/.gnupg/secring.gpg
----------------------------------
sec 2048R/F1C1EE49 2014-10-01
uid Thomas Uphill <thomas@narrabilis.com>
ssb 2048R/E2440023 2014-10-01
设置好secret函数和所需的密钥后,我们现在为该密钥加密一条消息:
tuphill@mylaptop ~/puppet $ gpg -e -r thomas@narrabilis.com secret_message
这会创建一个加密文件,只有拥有密钥访问权限的人(或者在已安装密钥的机器上运行 Puppet 的人)才能读取。
然后我们调用secret函数解密此文件并获取内容:
$message = secret(' /etc/puppet/environments/production/modules/admin/files/secret_message.gpg')
还有更多内容…
您应该使用secret函数,或类似的功能,来保护您 Puppet 仓库中的任何机密数据:密码、AWS 凭证、许可证密钥,甚至其他秘密密钥如 SSL 主机密钥。
您可以决定使用一个单一的密钥,并在构建机器时将其推送到机器上,或许可以作为引导过程的一部分,像第二章中的使用 Bash 引导 Puppet食谱所描述的那样。为了更高的安全性,您可能会为每台机器或机器组创建一个新的密钥,并仅针对需要它的机器加密给定的秘密。
例如,您的 web 服务器可能需要某个秘密信息,而您不希望该信息在其他任何机器上可以访问。您可以为 web 服务器创建一个密钥,并仅针对该密钥加密数据。
如果您想使用加密数据与 Hiera 配合使用,可以使用一个 Hiera 的 GnuPG 后端,详情见 www.craigdunn.org/2011/10/secret-variables-in-puppet-with-hiera-and-gpg/。
另见
-
第二章中的配置 Hiera食谱,Puppet 基础设施
-
第二章中的使用 hiera-gpg 存储秘密数据食谱,Puppet 基础设施
从第三方仓库安装软件包
最常见的是,您会希望从主分发仓库安装软件包,因此一个简单的软件包资源就足够了:
package { 'exim4': ensure => installed }
有时,您可能需要一个仅在第三方仓库中找到的软件包(例如 Ubuntu PPA),或者您可能需要比分发版提供的更近期的包版本,而这些版本可以从第三方获取。
在手动管理的机器上,通常通过将仓库源配置添加到/etc/apt/sources.list.d(如有必要,还需添加仓库的 gpg 密钥)来进行操作,然后再安装软件包。我们可以通过 Puppet 很容易地自动化此过程。
如何操作…
在这个示例中,我们将使用流行的 Percona APT 仓库(Percona 是一家 MySQL 咨询公司,他们维护并发布自己专门的 MySQL 版本,更多信息请访问 www.percona.com/software/repositories):
-
创建文件
modules/admin/manifests/percona_repo.pp,并添加以下内容:# Install Percona APT repo class admin::percona_repo { exec { 'add-percona-apt-key': unless => '/usr/bin/apt-key list |grep percona', command => '/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -', notify => Exec['percona-apt-update'], } exec { 'percona-apt-update': command => '/usr/bin/apt-get update', require => [File['/etc/apt/sources.list.d/percona.list'], File['/etc/apt/preferences.d/00percona.pref']], refreshonly => true, } file { '/etc/apt/sources.list.d/percona.list': content => 'deb http://repo.percona.com/apt wheezy main', notify => Exec['percona-apt-update'], } file { '/etc/apt/preferences.d/00percona.pref': content => "Package: *\nPin: release o=Percona Development Team\nPin-Priority: 1001", notify => Exec['percona-apt-update'], } } -
按照以下方式修改你的
site.pp文件:node 'cookbook' { include admin::percona_repo package { 'percona-server-server-5.5': ensure => installed, require => Class['admin::percona_repo'], } } -
运行 Puppet:
root@cookbook-deb:~# puppet agent -t Info: Caching catalog for cookbook-deb Notice: /Stage[main]/Admin::Percona_repo/Exec[add-percona-apt-key]/returns: executed successfully Info: /Stage[main]/Admin::Percona_repo/Exec[add-percona-apt-key]: Scheduling refresh of Exec[percona-apt-update] Notice: /Stage[main]/Admin::Percona_repo/File[/etc/apt/sources.list.d/percona.list]/ensure: defined content as '{md5}b8d479374497255804ffbf0a7bcdf6c2' Info: /Stage[main]/Admin::Percona_repo/File[/etc/apt/sources.list.d/percona.list]: Scheduling refresh of Exec[percona-apt-update] Notice: /Stage[main]/Admin::Percona_repo/File[/etc/apt/preferences.d/00percona.pref]/ensure: defined content as '{md5}1d8ca6c1e752308a9bd3018713e2d1ad' Info: /Stage[main]/Admin::Percona_repo/File[/etc/apt/preferences.d/00percona.pref]: Scheduling refresh of Exec[percona-apt-update] Notice: /Stage[main]/Admin::Percona_repo/Exec[percona-apt-update]: Triggered 'refresh' from 3 events
它是如何工作的……
要安装任何 Percona 包,我们首先需要在机器上安装仓库配置。这就是为什么 percona-server-server-5.5 包(Percona 版本的标准 MySQL 服务器)需要 admin::percona_repo 类的原因:
package { 'percona-server-server-5.5':
ensure => installed,
require => Class['admin::percona_repo'],
}
那么,admin::percona_repo 类做了什么呢?它:
-
安装 Percona APT 密钥,安装包将使用该密钥进行签名
-
将 Percona 仓库 URL 配置为
/etc/apt/sources.list.d中的文件 -
运行
apt-get update来检索仓库的元数据 -
在
/etc/apt/preferences.d中添加 APT pin 配置
首先,我们安装 APT 密钥:
exec { 'add-percona-apt-key':
unless => '/usr/bin/apt-key list |grep percona',
command => '/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -',
notify => Exec['percona-apt-update'],
}
unless 参数检查 apt-key list 的输出,以确保 Percona 密钥尚未安装,如果已经安装,就不需要执行任何操作。如果没有安装,command 命令将执行:
/usr/bin/gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && /usr/bin/gpg -a --export CD2EFD2A | apt-key add -
此命令从 GnuPG 密钥服务器获取密钥,以 ASCII 格式导出并将其传递给 apt-key add 命令,从而将其添加到系统密钥链中。对于大多数需要 APT 签名密钥的第三方仓库,可以使用类似的模式。
安装密钥后,我们添加仓库配置:
file { '/etc/apt/sources.list.d/percona.list':
content => 'deb http://repo.percona.com/apt wheezy main',
notify => Exec['percona-apt-update'],
}
然后运行 apt-get update,用新仓库的元数据更新系统的 APT 缓存:
exec { 'percona-apt-update':
command => '/usr/bin/apt-get update',
require => [File['/etc/apt/sources.list.d/percona.list'], File['/etc/apt/preferences.d/00percona.pref']],
refreshonly => true,
}
最后,我们为仓库配置 APT pin 优先级:
file { '/etc/apt/preferences.d/00percona.pref':
content => "Package: *\nPin: release o=Percona Development Team\nPin-Priority: 1001",
notify => Exec['percona-apt-update'],
}
这样可以确保从 Percona 仓库安装的包永远不会被其他地方(例如主 Ubuntu 发行版)安装的包所替代。否则,你可能会遇到依赖关系损坏的问题,无法自动安装 Percona 包。
还有更多内容……
APT 包框架特定于 Debian 和 Ubuntu 系统。对于管理 apt 仓库,有一个 forge 模块,forge.puppetlabs.com/puppetlabs/apt。如果你使用的是 Red Hat 或 CentOS 系统,可以直接使用 yumrepo 资源来管理 RPM 仓库:
docs.puppetlabs.com/references/latest/type.html#yumrepo
比较包版本
包版本号是一个奇怪的东西。它们看起来像十进制数,但实际上并不是:版本号通常是类似 2.6.4 的形式。例如,如果你需要比较两个版本号,你不能直接进行字符串比较:2.6.4 会被解读为大于 2.6.12。而且数值比较也行不通,因为它们并不是有效的数字。
Puppet 的 versioncmp 函数来解救了。如果你传递两个看起来像版本号的东西,它会比较它们并返回一个值,指示哪个更大:
versioncmp( A, B )
返回:
-
如果 A 和 B 相等,则返回 0
-
如果 A 大于 B,则返回大于 1
-
如果 A 小于 B,则返回小于 0
如何做…
这是使用 versioncmp 函数的示例:
-
按如下方式修改你的
site.pp文件:node 'cookbook' { $app_version = '1.2.2' $min_version = '1.2.10' if versioncmp($app_version, $min_version) >= 0 { notify { 'Version OK': } } else { notify { 'Upgrade needed': } } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Notice: Upgrade needed -
现在更改
$app_version的值:$app_version = '1.2.14' -
再次运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Notice: Version OK
它是如何工作的…
我们指定了最小可接受版本($min_version)是 1.2.10。因此,在第一个示例中,我们要将其与 $app_version 的 1.2.2 进行比较。简单的字母顺序比较这两个字符串(例如在 Ruby 中)会得出错误的结果,但 versioncmp 正确地判断 1.2.2 小于 1.2.10,并提醒我们需要升级。
在第二个示例中,$app_version 现在是 1.2.14,versioncmp 正确地识别它大于 $min_version,因此我们得到了消息 版本正常。
第五章:用户与虚拟资源
“没有问题,直到它成为问题。”
本章我们将介绍以下配方:
-
使用虚拟资源
-
使用虚拟资源管理用户
-
管理用户的 SSH 访问
-
管理用户的自定义文件
-
使用导出的资源
引言
用户可能会非常麻烦。我不是说那些人,尽管这有时确实是事实。但如果没有某种集中的配置管理,保持 UNIX 用户账户和文件权限在一网络中同步,尤其是当网络中有些机器运行不同操作系统时,会非常具有挑战性。
每个新加入组织的开发者都需要在每台机器上拥有一个账户,且需要sudo权限和组成员资格,并且需要将他们的 SSH 密钥授权给多个不同的账户。需要手动处理这些事务的系统管理员将整天忙碌,而使用 Puppet 的系统管理员只需几分钟就能完成工作,并去享受早午餐。
本章将介绍一些管理用户及其相关资源的实用模式和技巧。用户也是虚拟资源的最常见应用之一,所以我们将详细探讨这些内容。在最后一节中,我们将介绍与虚拟资源相关的导出资源。
使用虚拟资源
Puppet 中的虚拟资源看起来可能复杂且令人困惑,但实际上它们非常简单。它们和普通资源完全一样,但在实现("使其成为现实"的意义上)之前,它们并不会生效;而普通资源每个节点只能声明一次(例如,两个类不能声明相同的资源)。虚拟资源则可以被实现多次,任你所愿。
当你需要在机器之间迁移应用程序和服务时,这非常有用。如果两个使用相同资源的应用程序最终共享一台机器,除非你将该资源虚拟化,否则它们会导致冲突。
为了更清楚地说明这一点,让我们来看一个典型的场景,在这个场景中,虚拟资源可能会派上用场。
你负责两个流行的 Web 应用程序:WordPress 和 Drupal。它们都是运行在 Apache 上的 Web 应用程序,因此都需要安装 Apache 软件包。WordPress 的定义可能类似于以下内容:
class wordpress {
package {'httpd':
ensure => 'installed',
}
service {'httpd':
ensure => 'running',
enable => true,
}
}
Drupal 的定义可能如下所示:
class drupal {
package {'httpd':
ensure => 'installed',
}
service {'httpd':
ensure => 'running',
enable => true,
}
}
一切顺利,直到你需要将两个应用程序整合到同一台服务器上:
node 'bigbox' {
include wordpress
include drupal
}
现在,Puppet 会抱怨,因为你试图定义两个同名的资源:httpd。

你可以从其中一个类中删除重复的 Apache 包定义,但没有包含 Apache 的节点将会失败。你可以通过将 Apache 包放入它自己的类中,并在需要的地方使用include apache来避免这个问题;Puppet 不介意你多次包含同一个类。实际上,将 Apache 放入它自己的类中解决了大多数问题,但总的来说,这种方法的缺点是每个可能冲突的资源都必须有自己的类。
虚拟资源可以用来解决这个问题。虚拟资源就像一个普通的资源,只是它以@字符开头:
@package { 'httpd': ensure => installed }
你可以将它视为一个占位符资源;你想定义它,但不确定是否会使用它。Puppet 会读取并记住虚拟资源的定义,但直到你调用realize,它才会真正创建该资源。
要创建资源,使用realize函数:
realize(Package['httpd'])
你可以在资源上调用realize任意多次,它不会导致冲突。因此,当多个不同的类都需要相同的资源,并且它们可能需要在同一个节点上共存时,虚拟资源是一个不错的选择。
如何操作...
这是使用虚拟资源构建示例的方法:
-
创建一个包含以下内容的虚拟模块:
class virtual { @package {'httpd': ensure => installed } @service {'httpd': ensure => running, enable => true, require => Package['httpd'] } } -
创建一个包含以下内容的 Drupal 模块:
class drupal { include virtual realize(Package['httpd']) realize(Service['httpd']) } -
创建一个包含以下内容的 WordPress 模块:
class wordpress { include virtual realize(Package['httpd']) realize(Service['httpd']) } -
修改你的
site.pp文件如下:node 'bigbox' { include drupal include wordpress } -
运行 Puppet:
bigbox# puppet agent -t Info: Caching catalog for bigbox.example.com Info: Applying configuration version '1413179615' Notice: /Stage[main]/Virtual/Package[httpd]/ensure: created Notice: /Stage[main]/Virtual/Service[httpd]/ensure: ensure changed 'stopped' to 'running' Info: /Stage[main]/Virtual/Service[httpd]: Unscheduling refresh on Service[httpd] Notice: Finished catalog run in 6.67 seconds
它是如何工作的...
你将包和服务定义为虚拟资源,放在一个地方:virtual类。所有节点都可以包含这个类,你可以将所有虚拟服务和包放在其中。在你调用realize之前,任何包都不会真正安装在节点上,服务也不会启动:
class virtual {
@package { 'httpd': ensure => installed }
}
每个需要 Apache 包的类都可以在此虚拟资源上调用realize:
class drupal {
include virtual
realize(Package['httpd'])
}
Puppet 知道,因为你将资源设为虚拟,它知道你打算多次引用相同的包,而不是无意中创建两个同名的资源。因此,它会做出正确的处理。
还有更多内容...
要实现虚拟资源,你也可以使用集合spaceship语法:
Package <| title = 'httpd' |>
这种语法的优势在于你不局限于资源名称;你也可以使用标签,例如:
Package <| tag = 'web' |>
或者,你也可以通过将查询部分留空,直接指定资源类型的所有实例:
Package <| |>
使用虚拟资源管理用户
用户是一个很好的例子,它可能需要被多个类实现。考虑以下情况。为了简化大量机器的管理,你为两类用户定义了类:developers(开发者)和sysadmins(系统管理员)。所有机器都需要包括sysadmins,但只有部分机器需要developers:
node 'server' {
include user::sysadmins
}
node 'webserver' {
include user::sysadmins
include user::developers
}
然而,一些用户可能同时是多个组的成员。如果每个组只是将其成员声明为常规的user资源,这会在节点同时包含developers和sysadmins类时导致冲突,正如在webserver示例中所见。
为了避免这种冲突,一种常见的做法是将所有用户设为虚拟资源,并在一个单独的user::virtual类中定义,该类会被每台机器包含,然后根据需要在需要的地方实现这些用户。这样,如果一个用户是多个组的成员,就不会产生冲突。
如何实现……
按照以下步骤创建user::virtual类:
-
创建文件
modules/user/manifests/virtual.pp,并包含以下内容:class user::virtual { @user { 'thomas': ensure => present } @user { 'theresa': ensure => present } @user { 'josko': ensure => present } @user { 'nate': ensure => present } } -
创建文件
modules/user/manifests/developers.pp,并包含以下内容:class user::developers { realize(User['theresa']) realize(User['nate']) } -
创建文件
modules/user/manifests/sysadmins.pp,并包含以下内容:class user::sysadmins { realize(User['thomas']) realize(User['theresa']) realize(User['josko']) } -
修改你的
nodes.pp文件如下:node 'cookbook' { include user::virtual include user::sysadmins include user::developers } -
运行 Puppet:
cookbook# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413180590' Notice: /Stage[main]/User::Virtual/User[theresa]/ensure: created Notice: /Stage[main]/User::Virtual/User[nate]/ensure: created Notice: /Stage[main]/User::Virtual/User[thomas]/ensure: created Notice: /Stage[main]/User::Virtual/User[josko]/ensure: created Notice: Finished catalog run in 0.47 seconds
它是如何工作的……
当我们包含user::virtual类时,所有用户都会被声明为虚拟资源(因为我们包括了@符号):
@user { 'thomas': ensure => present }
@user { 'theresa': ensure => present }
@user { 'josko': ensure => present }
@user { 'nate': ensure => present }
也就是说,这些资源存在于 Puppet 的目录中;它们可以被其他资源引用和链接,并且在各方面与常规资源完全相同,只是 Puppet 并不会实际在机器上创建相应的用户。
为了让这种情况发生,我们需要在虚拟资源上调用realize。当我们包含user::sysadmins类时,会得到以下代码:
realize(User['thomas'])
realize(User['theresa'])
realize(User['josko'])
在虚拟资源上调用realize告诉 Puppet,“我现在想使用这个资源”。正如我们从运行输出中看到的那样,这正是它所做的:
Notice: /Stage[main]/User::Virtual/User[theresa]/ensure: created
然而,Theresa 同时属于developers和sysadmins两个类!这是不是意味着我们会在同一资源上调用realize两次?
realize(User['theresa'])
...
realize(User['theresa'])
是的,确实如此,而且没问题。你被明确允许多次实现资源,而且不会产生冲突。只要某个类的某处调用了realize,Theresa 的账户就会被创建。未实现的资源在目录编译时会被丢弃。
还有更多……
当你使用这种模式来管理自己的用户时,每个节点都应该包含user::virtual类,作为你基本配置的一部分。这个类会声明你组织或站点中的所有用户(作为虚拟资源)。这也应该包括那些仅用于运行应用程序或服务的用户(例如Apache、www-data或deploy等)。然后,你可以根据需要在各个节点或特定类中实现这些用户。
对于生产环境使用,你可能还需要为每个用户或组指定 UID 和 GID,以确保这些数字标识符在整个网络中同步。你可以通过user资源的uid和gid参数来实现这一点。
注意
如果你没有指定用户的 UID,例如,你将只获得给定机器上下一个可用的 ID 号码,因此同一个用户在不同机器上的 UID 会不同。这可能会导致在使用共享存储或在机器之间移动文件时出现权限问题。
在将用户定义为虚拟资源时,一种常见的做法是根据用户在组织中的角色为他们分配标签。然后,你可以使用 collector 语法代替 realize 来收集具有特定标签的用户。
例如,参见以下代码片段:
@user { 'thomas': ensure => present, tag => 'sysadmin' }
@user { 'theresa': ensure => present, tag => 'sysadmin' }
@user { 'josko': ensure => present, tag => 'dev' }
User <| tag == 'sysadmin' |>
在前面的示例中,只有用户 thomas 和 theresa 会被包括在内。
另请参阅
-
本章中的 使用虚拟资源 配方
-
本章中的 管理用户自定义文件 配方
管理用户的 SSH 访问
一种合理的服务器访问控制方法是使用带有密码保护的 SSH 密钥的命名用户帐户,而不是让用户共享一个具有广泛已知密码的帐户。Puppet 通过内置的 ssh_authorized_key 类型,使得管理变得非常简单。
如上一节所述,为了将其与虚拟用户结合使用,你可以创建一个 define,该定义包含 user 和 ssh_authorized_key 资源。这在为每个用户添加自定义文件和其他资源时也会非常有用。
如何操作...
按照以下步骤扩展你的虚拟用户类以包含 SSH 访问:
-
创建一个新的模块
ssh_user来包含我们的ssh_user定义。创建modules/ssh_user/manifests/init.pp文件如下:define ssh_user($key,$keytype) { user { $name: ensure => present, } file { "/home/${name}": ensure => directory, mode => '0700', owner => $name, require => User["$name"] } file { "/home/${name}/.ssh": ensure => directory, mode => '0700', owner => "$name", require => File["/home/${name}"], } ssh_authorized_key { "${name}_key": key => $key, type => "$keytype", user => $name, require => File["/home/${name}/.ssh"], } } -
修改你的
modules/user/manifests/virtual.pp文件,注释掉之前为用户thomas定义的部分,并用以下内容替换:@ssh_user { 'thomas': key => 'AAAAB3NzaC1yc2E...XaWM5sX0z', keytype => 'ssh-rsa' } -
修改你的
modules/user/manifests/sysadmins.pp文件如下:class user::sysadmins { realize(Ssh_user['thomas']) } -
修改你的
site.pp文件如下:node 'cookbook' { include user::virtual include user::sysadmins } -
运行 Puppet:
cookbook# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413254461' Notice: /Stage[main]/User::Virtual/Ssh_user[thomas]/File[/home/thomas/.ssh]/ensure: created Notice: /Stage[main]/User::Virtual/Ssh_user[thomas]/Ssh_authorized_key[thomas_key]/ensure: created Notice: Finished catalog run in 0.11 seconds
它是如何工作的...
对于我们的 user::virtual 类中的每个用户,我们需要创建:
-
用户帐户本身
-
用户的主目录和
.ssh目录 -
用户的
.ssh/authorized_keys文件
我们可以为每个用户声明单独的资源来实现所有这些功能,但更简单的做法是创建一个定义,将它们包装成一个单独的资源。通过为我们的定义创建一个新模块,我们可以在任何地方(任何作用域内)引用 ssh_user:
define ssh_user ($key, $keytype) {
user { $name:
ensure => present,
}
创建用户后,我们可以创建主目录;我们需要先有用户,这样在分配所有权时就可以使用用户名,owner => $name:
file { "/home/${name}":
ensure => directory,
mode => '0700',
owner => $name,
require => User["$name"]
}
提示
Puppet 可以通过向用户资源添加 managehome 属性来创建用户的主目录。依赖于此机制在实践中存在问题,因为它没有考虑到在 Puppet 之外创建的没有主目录的用户。
接下来,我们需要确保在用户的主目录中存在.ssh目录。我们需要主目录,File["/home/${name}"],因为在创建该子目录之前,这个主目录需要先存在。这意味着用户已经存在,因为主目录需要用户:
file { "/home/${name}/.ssh":
ensure => directory,
mode => '0700',
owner => $name ,
require => File["/home/${name}"],
}
最后,我们创建了ssh_authorized_key资源,再次要求包含该文件夹(File["/home/${name}/.ssh"])。我们使用$key和$keytype变量为ssh_authorized_key类型分配密钥和类型参数,如下所示:
ssh_authorized_key { "${name}_key":
key => $key,
type => "$keytype",
user => $name,
require => File["/home/${name}/.ssh"],
}
}
当我们为thomas定义ssh_user资源时,我们传递了$key和$keytype变量:
@ssh_user { 'thomas':
key => 'AAAAB3NzaC1yc2E...XaWM5sX0z',
keytype => 'ssh-rsa'
}
提示
前面代码片段中的key值是 ssh 密钥的公钥值;它通常存储在id_rsa.pub文件中。
现在,一切都定义好了,我们只需要在thomas上调用realize,让所有这些资源生效:
realize(Ssh_user['thomas'])
注意,这次我们实现的虚拟资源不仅仅是像之前那样的user资源,而是我们创建的ssh_user定义类型,它包括了用户和设置 SSH 访问所需的相关资源:
Notice: /Stage[main]/User::Virtual/Ssh_user[thomas]/User[thomas]/ensure: created
Notice: /Stage[main]/User::Virtual/Ssh_user[thomas]/File[/home/thomas]/ensure: created
Notice: /Stage[main]/User::Virtual/Ssh_user[thomas]/File[/home/thomas/.ssh]/ensure: created
Notice: /Stage[main]/User::Virtual/Ssh_user[thomas]/Ssh_authorized_key[thomas_key]/ensure: created
还有更多内容...
当然,你可以向ssh_user定义中添加任何资源,让 Puppet 为新用户自动创建这些资源。在下一个例子中,我们会看到如何做,管理用户的自定义文件。
管理用户的自定义文件
用户倾向于定制他们的 shell 环境、终端颜色、别名等。这通常通过在他们的主目录中放置多个dotfiles来实现,例如.bash_profile或.vimrc。
你可以通过扩展我们在本章中开发的虚拟用户设置,使用 Puppet 来同步和更新每个用户的 dotfiles 到多台机器上。我们将启动一个新模块admin_user,并使用文件类型的recurse属性将文件复制到每个用户的主目录中。
如何做到这一点...
你需要做的事情如下:
-
在
modules/admin_user/manifests/init.pp文件中创建admin_user定义类型(define admin_user),如下所示:define admin_user ($key, $keytype, $dotfiles = false) { $username = $name user { $username: ensure => present, } file { "/home/${username}/.ssh": ensure => directory, mode => '0700', owner => $username, group => $username, require => File["/home/${username}"], } ssh_authorized_key { "${username}_key": key => $key, type => "$keytype", user => $username, require => File["/home/${username}/.ssh"], } # dotfiles if $dotfiles == false { # just create the directory file { "/home/${username}": ensure => 'directory', mode => '0700', owner => $username, group => $username, require => User["$username"] } } else { # copy in all the files in the subdirectory file { "/home/${username}": recurse => true, mode => '0700', owner => $username, group => $username, source => "puppet:///modules/admin_user/${username}", require => User["$username"] } } } -
修改
modules/user/manifests/sysadmins.pp文件,如下所示:class user::sysadmins { realize(Admin_user['thomas']) } -
修改
modules/user/manifests/virtual.pp中的thomas定义,如下所示:@ssh_user { 'thomas': key => 'AAAAB3NzaC1yc2E...XaWM5sX0z', keytype => 'ssh-rsa', dotfiles => true } -
在
admin_user模块中为用户thomas的文件创建一个子目录:$ mkdir -p modules/admin_user/files/thomas -
为用户
thomas创建 dotfiles,放在你刚刚创建的目录中:$ echo "alias vi=vim" > modules/admin_user/files/thomas/.bashrc $ echo "set tabstop=2" > modules/admin_user/files/thomas/.vimrc -
确保你的
site.pp文件如下所示:node 'cookbook' { include user::virtual include user::sysadmins } -
运行 Puppet:
cookbook# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413266235' Notice: /Stage[main]/User::Virtual/Admin_user[thomas]/User[thomas]/ensure: created Notice: /Stage[main]/User::Virtual/Admin_user[thomas]/File[/home/thomas]/ensure: created Notice: /Stage[main]/User::Virtual/Admin_user[thomas]/File[/home/thomas/.vimrc]/ensure: defined content as '{md5}cb2af2d35b18b5ac2539057bd429d3ae' Notice: /Stage[main]/User::Virtual/Admin_user[thomas]/File[/home/thomas/.bashrc]/ensure: defined content as '{md5}033c3484e4b276e0641becc3aa268a3a' Notice: /Stage[main]/User::Virtual/Admin_user[thomas]/File[/home/thomas/.ssh]/ensure: created Notice: /Stage[main]/User::Virtual/Admin_user[thomas]/Ssh_authorized_key[thomas_key]/ensure: created Notice: Finished catalog run in 0.36 seconds
它是如何工作的...
我们创建了一个新的admin_user定义,它会递归地定义主目录,如果$dotfiles值不是false(默认值)的话:
if $dotfiles == 'false' {
# just create the directory
file { "/home/${username}":
ensure => 'directory',
mode => '0700',
owner => $username,
group => $username,
require => User["$username"]
}
} else {
# copy in all the files in the subdirectory
file { "/home/${username}":
recurse => true,
mode => '0700',
owner => $username,
group => $username,
source => "puppet:///modules/admin_user/${username}",
require => User["$username"]
}
}
我们在admin_user模块中创建了一个目录来存放用户的 dotfiles;该目录中的所有文件都会被复制到用户的主目录中,如下所示在 Puppet 运行输出中的命令行:
Notice: /Stage[main]/User::Virtual/Admin_user[thomas]/File[/home/thomas/.vimrc]/ensure: defined content as '{md5}cb2af2d35b18b5ac2539057bd429d3ae'
Notice: /Stage[main]/User::Virtual/Admin_user[thomas]/File[/home/thomas/.bashrc]/ensure: defined content as '{md5}033c3484e4b276e0641becc3aa268a3a'
使用recurse选项允许我们为每个用户添加任意多的 dotfiles,而不必修改用户的定义。
还有更多内容...
我们可以指定主目录的source属性为用户可以放置自己点文件(dotfiles)的目录。这样,每个用户可以修改自己的点文件,并将其传输到网络中所有节点,而无需我们参与其中。
另见
- 本章中的管理虚拟资源的用户配方
使用导出资源
到目前为止,我们的所有配方都处理的是单台机器。使用 Puppet,可以使一个节点的资源影响另一个节点。此交互由导出资源进行管理。导出资源就像您为节点定义的任何资源,但它们不是应用于创建它们的节点,而是导出供环境中所有节点使用。导出资源可以被看作是虚拟资源,超越了节点所在的范畴,存在于节点之外。
导出资源有两个操作。当一个导出资源被创建时,它被称为已定义。当所有导出资源被收集时,它们被称为已收集。定义导出资源类似于虚拟资源;相关资源的前面会加上两个@符号。例如,要将文件资源定义为外部资源,使用@@file。收集资源是通过飞船操作符<<| |>>来完成的;这被认为看起来像一艘飞船。要收集导出的文件资源(@@file),可以使用File <<| |>>。
有许多使用导出资源的示例;最常见的一个是涉及 SSH 主机密钥。使用导出资源,可以让所有运行 Puppet 的机器与其他连接的节点共享其 SSH 主机密钥。这里的想法是,每台机器导出自己的主机密钥,然后收集其他机器的所有密钥。在我们的示例中,我们将创建两个类;首先是一个从每个节点导出 SSH 主机密钥的类。我们将把这个类包含在我们的基类中。第二个类是一个收集类,它收集 SSH 主机密钥。我们将把这个类应用到我们的跳板机或 SSH 登录服务器上。
注意
跳板机(Jumpboxes)是具有特殊防火墙规则的机器,允许它们登录到不同的位置。
准备工作
要使用导出资源,您需要在 Puppet 主节点上启用 storeconfigs。可以在无主(去中心化)部署中使用导出资源;然而,我们假设您在本示例中使用的是集中式模型。在第二章,Puppet 基础架构中,我们使用 forge 中的 puppetdb 模块配置了 puppetdb。如果需要,也可以使用其他后端;但这些除 puppetdb 外都已被弃用。更多信息请访问以下链接:projects.puppetlabs.com/projects/puppet/wiki/Using_Stored_Configuration。
确保您的 Puppet 主节点配置为使用 puppetdb 作为 storeconfigs 容器。
如何做...
我们将创建一个 ssh_host 类来导出主机的 ssh 密钥,并确保它包含在我们的基础类中。
-
创建第一个类,
base::ssh_host,我们将在我们的基础类中包含它:class base::ssh_host { @@sshkey{"$::fqdn": ensure => 'present', host_aliases => ["$::hostname","$::ipaddress"], key => $::sshdsakey, type => 'dsa', } } -
记得从基础类定义内部包含这个类:
class base { ... include ssh_host } -
为
jumpbox创建一个定义,可以在类中或jumpbox的节点定义中:node 'jumpbox' { Sshkey <<| |>> } -
现在在几个节点上运行 Puppet,以创建导出的资源。在我的例子中,我在 Puppet 服务器和我的第二个示例节点(
node2)上运行了 Puppet。最后,在jumpbox上运行 Puppet,验证其他节点的 SSH 主机密钥是否已收集:[root@jumpbox ~]# puppet agent -t Info: Caching catalog for jumpbox.example.com Info: Applying configuration version '1413176635' Notice: /Stage[main]/Main/Node[jumpbox]/Sshkey[node2.example.com]/ensure: created Notice: /Stage[main]/Main/Node[jumpbox]/Sshkey[puppet]/ensure: created Notice: Finished catalog run in 0.08 seconds
它是如何工作的...
我们为节点创建了一个 sshkey 资源,使用了 facter 提供的 fqdn、hostname、ipaddress 和 sshdsakey 事实。我们使用 fqdn 作为导出资源的标题,因为每个导出资源必须有唯一的名称。我们可以假设一个节点的 fqdn 在我们的组织中是唯一的(尽管有时它们可能不是;Puppet 在最不经意时也能找到这种情况)。然后我们继续定义节点可能被识别的别名。我们使用主机名变量作为一个别名,机器的主 IP 地址作为另一个别名。如果您有其他节点命名约定,可以在这里添加其他别名。我们假设主机使用 DSA 密钥,因此我们在定义中使用了 sshdsakey 变量。在大型安装中,您会将此定义包装在测试中,以确保 DSA 密钥存在。如果存在 RSA 密钥,您也可以使用它们。
定义并导出 sshkey 资源后,我们接着创建了一个 jumpbox 节点定义。在这个定义中,我们使用了航天飞机语法 Sshkey <<| |>> 来收集所有定义的导出 sshkey 资源。
还有更多内容...
在定义导出资源时,您可以为资源添加标签属性,以创建导出资源的子集。例如,如果您的网络中有开发和生产区域,您可以为每个区域创建不同的 sshkey 资源组,如以下代码片段所示:
@@sshkey{"$::fqdn":
host_aliases => ["$::hostname","$::ipaddress"],
key => $::sshdsakey,
type => 'dsa',
tag => "$::environment",
}
然后,您可以修改 jumpbox,例如,只收集生产环境的资源,如下所示:
Sshkey <<| tag == 'production' |>>
处理导出资源时需要记住的两件重要事情:首先,每个资源在您的安装中必须有唯一的名称。在标题中使用 fqdn 域名通常足以保持定义的唯一性。其次,任何资源都可以是虚拟的。即使是您创建的定义类型也可以被导出。导出的资源可以用来实现一些复杂的配置,这些配置会在机器发生变化时自动调整。
注意
在处理大量节点(超过 5,000 个)时,需要特别注意的是,导出的资源可能需要很长时间才能收集和应用,尤其是当每个导出的资源都会创建一个文件时。
第六章:资源和文件管理
| "简单的艺术是复杂的谜题" | ||
|---|---|---|
| --道格拉斯·霍顿 |
在本章中,我们将介绍以下内容:
-
高效分配 cron 作业
-
资源应用的调度
-
使用主机资源
-
使用导出的主机资源
-
使用多个文件来源
-
分配和合并目录树
-
清理旧文件
-
审计资源
-
临时禁用资源
介绍
在上一章中,我们介绍了虚拟资源和导出资源。虚拟资源和导出资源是管理资源如何应用到节点的一种方式。在本章中,我们将讨论何时以及如何应用资源。在某些情况下,您可能只希望在非工作时间应用资源,而在其他情况下,您可能希望只审计资源但不做任何更改。在其他情况下,您可能希望根据使用代码的节点应用完全不同的资源。正如我们将看到的,Puppet 有足够的灵活性来处理所有这些场景。
高效分配 cron 作业
当你有许多服务器执行相同的 cron 作业时,通常最好不要让它们同时运行。如果所有作业都访问一个共同的服务器(例如,执行备份时),这可能会给该服务器带来过大的负载,即使它们没有同时访问,所有服务器同时忙碌也可能会影响它们提供其他服务的能力。
像往常一样,Puppet 可以提供帮助;这次,我们使用 inline_template 函数来为每个作业计算一个唯一的时间。
如何实现...
以下是如何让 Puppet 为每台机器在不同时间安排相同作业的方法:
-
修改你的
site.pp文件,如下所示:node 'cookbook' { cron { 'run-backup': ensure => present, command => '/usr/local/bin/backup', hour => inline_template('<%= @hostname.sum % 24 %>'), minute => '00', } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413730771' Notice: /Stage[main]/Main/Node[cookbook]/Cron[run-backup]/ensure: created Notice: Finished catalog run in 0.11 seconds -
运行
crontab查看作业配置:[root@cookbook ~]# crontab -l # HEADER: This file was autogenerated at Sun Oct 19 10:59:32 -0400 2014 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: run-backup 0 15 * * * /usr/local/bin/backup
它是如何工作的...
我们希望将 cron 作业的执行时间分配到所有节点上。我们选择一个在所有机器上唯一的值,并将其转换为数字。这样,值将在节点之间分布,并且每个节点的值不会变化。
我们可以使用 Ruby 的 sum 方法来进行转换,该方法从一个唯一于机器的字符串(在此例中是机器的主机名)计算出一个数值。sum 函数会生成一个大整数(例如,字符串 cookbook 的和为 855),而我们希望 hour 的值在 0 到 23 之间,因此我们使用 Ruby 的 %(取模)运算符将结果限制在这个范围内。根据主机名的不同,我们应该能得到一个合理的(尽管不是统计学上均匀的)值分布。这里的另一个选择是使用 fqdn_rand() 函数,它的工作方式与我们的示例类似。
如果所有机器都有相同的名称(确实会发生),不要指望这个技巧能奏效!在这种情况下,你可以使用其他在机器上唯一的字符串,比如 ipaddress 或 fqdn。
还有更多...
如果每台机器上有多个 cron 任务,并且你希望它们相隔特定的小时数执行,可以在 hostname.sum 资源中添加此数字,然后取模。假设我们想在某个任意时间运行 dump_database 任务,并在一小时后运行 run_backup 任务,可以使用以下代码片段实现:
cron { 'dump-database':
ensure => present,
command => '/usr/local/bin/dump_database',
hour => inline_template('<%= @hostname.sum % 24 %>'),
minute => '00',
}
cron { 'run-backup':
ensure => present,
command => '/usr/local/bin/backup',
hour => inline_template('<%= ( @hostname.sum + 1) % 24 %>'),
minute => '00',
}
这两个任务将导致每台运行 Puppet 的机器有不同的 hour 值,但 run_backup 总是会在 dump_database 后一小时执行。
大多数 cron 实现都包含了用于每小时、每天、每周和每月任务的目录。目录 /etc/cron.hourly、/etc/cron.daily、/etc/cron.weekly 和 /etc/cron.monthly 在我们的 Debian 和 Enterprise Linux 机器上都有。这些目录包含可执行文件,它们会根据参考的调度(每小时、每天、每周或每月)执行。我发现最好将这些文件夹中的所有任务描述清楚,并将任务作为 file 资源推送。盒子上的管理员可以通过在这些目录中使用 grep 查找你的脚本。为了在这里使用相同的技巧,我们可以将 cron 任务推送到 /etc/cron.hourly,然后验证这个小时是否是任务执行的正确时间。要使用 cron 目录创建 cron 任务,按照以下步骤操作:
-
首先,在
modules/cron/init.pp中创建一个cron类:class cron { file { '/etc/cron.hourly/run-backup': content => template('cron/run-backup'), mode => 0755, } } -
在
site.pp中的烹饪书节点中包含cron类:node cookbook { include cron } -
创建一个模板来保存 cron 任务:
#!/bin/bash runhour=<%= @hostname.sum%24 %> hour=$(date +%H) if [ "$runhour" -ne "$hour" ]; then exit 0 fi echo run-backup -
然后,运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413732254' Notice: /Stage[main]/Cron/File[/etc/cron.hourly/run-backup]/ensure: defined content as '{md5}5e50a7b586ce774df23301ee72904dda' Notice: Finished catalog run in 0.11 seconds -
验证脚本的值是否与我们之前计算的
15相同:#!/bin/bash runhour=15 hour=$(date +%H) if [ "$runhour" -ne "$hour" ]; then exit 0 fi echo run-backup
现在,这个任务将每小时运行,但只有当 $(date +%H) 返回的小时等于 15 时,脚本的其他部分才会执行。将 cron 任务作为文件资源创建在大型组织中,可以让其他管理员更容易找到它们。当你的机器数量非常庞大时,在任务开始时添加另一个随机等待时间可能会有好处。你需要修改 echo run-backup 之前的那一行,添加以下内容:
MAXWAIT=600
sleep $((RANDOM%MAXWAIT))
这将最多等待 600 秒,但每次运行时等待的时间不同(假设你的随机数生成器工作正常)。这种随机等待在你有成千上万台机器运行相同任务时非常有用,尤其是当你需要尽可能分散任务的运行时。
另见
- 第二章中的 从 cron 运行 Puppet 方案,Puppet 基础设施
调度资源应用的时间
到目前为止,我们查看了 Puppet 可以做什么,以及它做事情的顺序,但没有查看它做事的时间。控制这一点的一种方法是使用 schedule 元参数。当你需要限制某个资源在指定时间段内应用的次数时,schedule 可以提供帮助。例如:
exec { "/usr/bin/apt-get update":
schedule => daily,
}
理解 schedule 最重要的一点是,它只能阻止资源的应用。它不能保证资源以特定的频率被应用。例如,前面代码片段中显示的 exec 资源设置了 schedule => daily,但这只是表示 exec 资源每天可以运行的最大次数。它不会每天运行超过一次。如果你根本没有运行 Puppet,那么资源将完全不被应用。例如,在配置为每 4 小时运行一次代理(通过 runinterval 配置设置)的机器上,使用每小时计划是没有意义的。
也就是说,schedule 最好用于限制在不应该运行或不需要运行时阻止资源运行;例如,你可能希望确保 apt-get update 不会在一个小时内运行超过一次。你可以使用一些内置的计划:
-
hourly -
daily -
weekly -
monthly -
never
然而,你可以修改这些并创建你自己的自定义计划,使用 schedule 资源。我们将在以下示例中看到如何操作。假设我们希望确保一个表示维护作业的 exec 资源在办公时间内不会运行,因为它可能会干扰生产。
如何操作...
在这个例子中,我们将创建一个自定义的 schedule 资源,并将其分配给该资源:
-
按如下方式修改你的
site.pp文件:schedule { 'outside-office-hours': period => daily, range => ['17:00-23:59','00:00-09:00'], repeat => 1, } node 'cookbook' { notify { 'Doing some maintenance': schedule => 'outside-office-hours', } } -
运行 Puppet。你看到的结果将取决于当天的时间。如果现在是在你定义的办公时间之外,Puppet 会按如下方式应用资源:
[root@cookbook ~]# date Fri Jan 2 23:59:01 PST 2015 [root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413734477' Notice: Doing some maintenance Notice: /Stage[main]/Main/Node[cookbook]/Notify[Doing some maintenance]/message: defined 'message' as 'Doing some maintenance' Notice: Finished catalog run in 0.07 seconds -
如果时间在办公时间内,Puppet 将不会做任何操作:
[root@cookbook ~]# date Fri Jan 2 09:59:01 PST 2015 [root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413734289' Notice: Finished catalog run in 0.09 seconds
它是如何工作的...
一个计划由三部分信息组成:
-
period(hourly、daily、weekly或monthly) -
range(默认为整个时间段,但可以是其较小的一部分) -
repeat次数(在范围内允许应用资源的频率;默认为 1 或每个周期一次)
我们的自定义计划 outside-office-hours 提供了这三项参数:
schedule { 'outside-office-hours':
period => daily,
range => ['17:00-23:59','00:00-09:00'],
repeat => 1,
}
period 是 daily,range 被定义为一个由两个时间间隔组成的数组:
17:00-23:59
00:00-09:00
现在可以使用名为 outside-office-hours 的计划,它就像 daily 或 hourly 计划一样,可以与任何资源一起使用。在我们的示例中,我们通过 schedule 元参数将此计划分配给 exec 资源:
notify { 'Doing some maintenance':
schedule => 'outside-office-hours',
}
如果没有这个 schedule 参数,资源将在每次运行 Puppet 时都被应用。使用此参数后,Puppet 会检查以下参数,以决定是否应用该资源:
-
是否时间在允许的范围内
-
是否资源在此期间已经运行了最大允许次数
例如,假设我们考虑在某一天的下午 4 点、5 点和 6 点 Puppet 会发生什么:
-
下午 4 点:在允许的时间范围之外,因此 Puppet 什么也不做
-
下午 5 点:它在允许的时间范围内,并且在此期间资源尚未运行,因此 Puppet 会应用该资源
-
下午 6 点:它在允许的时间范围内,但资源在此期间已运行最大次数,因此 Puppet 不会执行任何操作
依此类推,直到第二天。
还有更多...
repeat 参数控制在给定调度其他约束条件下资源会被应用多少次。例如,要使资源每小时最多应用六次,可以使用如下调度:
period => hourly,
repeat => 6,
请记住,这并不能保证作业每小时执行六次。它只是设置了一个上限;无论 Puppet 运行多频繁,或者其他任何事情发生,如果本小时已经运行过六次作业,它将不会再运行。如果 Puppet 仅每天运行一次,该作业将只会执行一次。因此,schedule 最好用来确保某些操作不会在特定时间发生(或不会超过给定的频率)。
使用主机资源
在云基础设施中,使用 DNS 将机器名称映射到 IP 地址并不总是实用或方便,特别是那些地址可能经常变化的情况。然而,如果你改为使用 /etc/hosts 文件中的条目,那么你就会面临如何将这些条目分发到所有机器并保持更新的问题。
这里有一个更好的方法;Puppet 的主机资源类型控制一个单独的 /etc/hosts 条目,你可以利用它轻松地将主机名映射到整个网络中的 IP 地址。例如,如果所有机器需要知道主数据库服务器的地址,你可以使用 host 资源来管理它。
如何操作...
按照以下步骤创建一个示例 host 资源:
-
按照以下方式修改你的
site.pp文件:node 'cookbook' { host { 'packtpub.com': ensure => present, ip => '83.166.169.231', } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413781153' Notice: /Stage[main]/Main/Node[cookbook]/Host[packtpub.com]/ensure: created Info: Computing checksum on file /etc/hosts Notice: Finished catalog run in 0.12 seconds
它是如何工作的...
Puppet 将检查 target 文件(通常是 /etc/hosts),查看主机条目是否已经存在,如果不存在,则添加它。如果该主机名的条目已经存在但地址不同,Puppet 将更改地址以匹配清单。
还有更多...
将你的主机资源组织成类别可能会很有帮助。例如,你可以将所有数据库服务器的主机资源放入一个名为 admin::dbhosts 的类别中,然后该类别被所有 Web 服务器引用。
当机器可能需要在多个类别中定义时(例如,数据库服务器也可能是一个仓库服务器),虚拟资源可以解决这个问题。例如,你可以在一个类别中将所有主机定义为虚拟:
class admin::allhosts {
@host { 'db1.packtpub.com':
tag => 'database'
...
}
}
你可以在不同的类别中实现所需的主机:
class admin::dbhosts {
Host <| tag=='database' |>
}
class admin::webhosts {
Host <| tag=='web' |>
}
使用导出的主机资源
在前面的例子中,我们使用了太空船语法来收集类型为数据库或网页的主机的虚拟主机资源。你可以使用相同的方法来处理导出资源。使用导出资源的好处是,随着你添加更多的数据库服务器,收集器语法会自动拉取新创建的服务器导出主机条目。这使得你的/etc/hosts条目更加动态。
准备工作
我们将使用导出的资源。如果你还没有做,设置好 puppetdb 并启用 storeconfigs 以按照第二章的说明使用 puppetdb,Puppet 基础设施。
如何操作...
在这个例子中,我们将配置数据库服务器和客户端之间的通信。我们将利用导出资源进行配置。
-
创建一个新的数据库模块,
db:t@mylaptop ~/puppet/modules $ mkdir -p db/manifests -
为你的数据库服务器创建一个新的类,
db::server:class db::server { @@host {"$::fqdn": host_aliases => $::hostname, ip => $::ipaddress, tag => 'db::server', } # rest of db class } -
为你的数据库客户端创建一个新的类:
class db::client { Host <<| tag == 'db::server' |>> } -
将数据库服务器模块应用于某些节点,例如在
site.pp中:node 'dbserver1.example.com' { class {'db::server': } } node 'dbserver2.example.com' { class {'db::server': } } -
在带有数据库服务器模块的节点上运行 Puppet,以创建导出资源。
-
将数据库客户端模块应用于 cookbook:
node 'cookbook' { class {'db::client': } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413782501' Notice: /Stage[main]/Db::Client/Host[dbserver2.example.com]/ensure: created Info: Computing checksum on file /etc/hosts Notice: /Stage[main]/Db::Client/Host[dbserver1.example.com]/ensure: created Notice: Finished catalog run in 0.10 seconds -
验证
/etc/hosts中的主机条目:[root@cookbook ~]# cat /etc/hosts # HEADER: This file was autogenerated at Mon Oct 20 01:21:42 -0400 2014 # HEADER: by puppet. While it can still be managed manually, it # HEADER: is definitely not recommended. 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 83.166.169.231 packtpub.com 192.168.122.150 dbserver2.example.com dbserver2 192.168.122.151 dbserver1.example.com dbserver1
它是如何工作的...
在db::server类中,我们创建了一个导出的主机资源:
@@host {"$::fqdn":
host_aliases => $::hostname,
ip => $::ipaddress,
tag => 'db::server',
}
这个资源使用应用所在节点的完全限定域名($::fqdn)。我们还使用短主机名($::hostname)作为该节点的别名。别名会在fqdn后面打印到/etc/hosts中。我们使用节点的$::ipaddress事实作为主机条目的 IP 地址。最后,我们为资源添加标签,以便以后根据该标签进行收集。
这里需要记住的关键点是,如果主机的 IP 地址发生变化,导出资源会更新,收集该导出资源的节点将相应更新它们的主机记录。
我们在db::client中创建了一个收集器,该收集器只收集已标记为'db::server'的导出主机资源:
Host <<| tag == 'db::server' |>>
我们为两个节点 dbserver1 和 dbserver2 应用了db::server类,随后通过应用db::client类将它们收集到 cookbook 中。主机条目被放置在/etc/hosts中(默认文件)。我们可以看到,主机条目包含了 dbserver1 和 dbserver2 的 fqdn 和简短主机名。
还有更多...
以这种方式使用导出资源非常有用。另一个类似的系统是创建一个 NFS 服务器类,生成它导出的挂载点的导出资源(通过 NFS)。然后你可以使用标签让客户端收集服务器的适当挂载点。在前面的例子中,我们利用了一个标签来帮助我们收集导出资源。值得注意的是,当资源被创建时,会自动添加几个标签,其中一个是资源创建的作用域。
使用多个文件源
Puppet file 资源的一个优点是可以为 source 参数指定多个值。Puppet 会按顺序进行查找。如果找不到第一个源,它会继续查找下一个,以此类推。你可以利用这一点指定默认替代文件,或者甚至指定一系列逐渐通用的替代文件。
如何实现……
本示例展示了如何使用多个文件源:
-
按如下方式创建一个新的问候模块:
class greeting { file { '/tmp/greeting': source => [ 'puppet:///modules/greeting/hello.txt', 'puppet:///modules/greeting/universal.txt'], } } -
创建文件
modules/greeting/files/hello.txt,内容如下:Hello, world. -
创建文件
modules/greeting/files/universal.txt,内容如下:Bah-weep-Graaaaagnah wheep ni ni bong -
将类添加到节点:
node cookbook { class {'greeting': } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413784347' Notice: /Stage[main]/Greeting/File[/tmp/greeting]/ensure: defined content as '{md5}54098b367d2e87b078671fad4afb9dbb' Notice: Finished catalog run in 0.43 seconds -
检查
/tmp/greeting文件的内容:[root@cookbook ~]# cat /tmp/greeting Hello, world. -
现在,从 Puppet 仓库中移除
hello.txt文件,并重新运行代理:[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413784939' Notice: /Stage[main]/Greeting/File[/tmp/greeting]/content: --- /tmp/greeting 2014-10-20 01:52:28.117999991 -0400 +++ /tmp/puppet-file20141020-4960-1o9g344-0 2014-10-20 02:02:20.695999979 -0400 @@ -1 +1 @@ -Hello, world. +Bah-weep-Graaaaagnah wheep ni ni bong Info: Computing checksum on file /tmp/greeting Info: /Stage[main]/Greeting/File[/tmp/greeting]: Filebucketed /tmp/greeting to puppet with sum 54098b367d2e87b078671fad4afb9dbb Notice: /Stage[main]/Greeting/File[/tmp/greeting]/content: content changed '{md5}54098b367d2e87b078671fad4afb9dbb' to '{md5}933c7f04d501b45456e830de299b5521' Notice: Finished catalog run in 0.77 seconds
它是如何工作的……
在第一次运行 Puppet 时,Puppet 按照给定的顺序搜索可用的文件源:
source => [
'puppet:///modules/greeting/hello.txt',
'puppet:///modules/greeting/universal.txt'
],
hello.txt 文件位于列表的第一位,并且存在,因此 Puppet 使用该文件作为 /tmp/greeting 的来源:
Hello, world.
在第二次运行 Puppet 时,hello.txt 缺失,因此 Puppet 会继续查找下一个文件 universal.txt。该文件存在,因此它成为 /tmp/greeting 的来源:
Bah-weep-Graaaaagnah wheep ni ni bong
还有更多……
你可以在任何使用 file 资源的地方使用这个技巧。一个常见的例子是部署在所有节点上的服务,如 rsyslog。除了 rsyslog 服务器外,所有主机的 rsyslog 配置都是相同的。创建一个包含 rsyslog 配置文件的 rsyslog 类并使用 file 资源:
class rsyslog {
file { '/etc/rsyslog.conf':
source => [
"puppet:///modules/rsyslog/rsyslog.conf.${::hostname}",
'puppet:///modules/rsyslog/rsyslog.conf' ],
}
然后,你将默认配置放入 rsyslog.conf 中。对于你的 rsyslog 服务器 logger,创建一个 rsyslog.conf.logger 文件。在机器 logger 上,rsyslog.conf.logger 会先于 rsyslog.conf 被使用,因为它在文件源数组中的顺序靠前。
另见:
- 第三章中的向类传递参数配方,编写更好的清单
分发和合并目录树
如我们在上一章所见,文件资源具有 recurse 参数,这使得 Puppet 可以传输整个目录树。我们使用此参数将管理员用户的点文件复制到他们的主目录。在本节中,我们将展示如何使用 recurse 和另一个参数 sourceselect 来扩展我们之前的示例。
如何实现……
如下修改我们的管理员用户示例:
-
移除
$dotfiles参数,删除基于$dotfiles的条件。向主目录file资源添加第二个源:define admin_user ($key, $keytype) { $username = $name user { $username: ensure => present, } file { "/home/${username}/.ssh": ensure => directory, mode => '0700', owner => $username, group => $username, require => File["/home/${username}"], } ssh_authorized_key { "${username}_key": key => $key, type => "$keytype", user => $username, require => File["/home/${username}/.ssh"], } # copy in all the files in the subdirectory file { "/home/${username}": recurse => true, mode => '0700', owner => $username, group => $username, source => [ "puppet:///modules/admin_user/${username}", 'puppet:///modules/admin_user/base' ], sourceselect => 'all', require => User["$username"], } } -
创建一个基础目录并从
/etc/skel复制所有系统默认文件:t@mylaptop ~/puppet/modules/admin_user/files $ cp -a /etc/skel base -
创建一个新的
admin_user资源,定义时不包括目录:node 'cookbook' { admin_user {'steven': key => 'AAAAB3N...', keytype => 'dsa', } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413787159' Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[steven]/User[steven]/ensure: created Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[steven]/File[/home/steven]/ensure: created Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[steven]/File[/home/steven/.bash_logout]/ensure: defined content as '{md5}6a5bc1cc5f80a48b540bc09d082b5855' Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[steven]/File[/home/steven/.emacs]/ensure: defined content as '{md5}de7ee35f4058681a834a99b5d1b048b3' Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[steven]/File[/home/steven/.bashrc]/ensure: defined content as '{md5}2f8222b4f275c4f18e69c34f66d2631b' Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[steven]/File[/home/steven/.bash_profile]/ensure: defined content as '{md5}f939eb71a81a9da364410b799e817202' Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[steven]/File[/home/steven/.ssh]/ensure: created Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[steven]/Ssh_authorized_key[steven_key]/ensure: created Notice: Finished catalog run in 1.11 seconds
它是如何工作的……
如果 file 资源设置了 recurse 参数,并且它是一个目录,Puppet 不仅会部署该目录本身,还会部署它的所有内容(包括子目录及其内容)。正如我们在之前的例子中看到的,当文件有多个来源时,找到的第一个源文件将用于满足请求。这也适用于目录。
还有更多...
通过将参数 sourceselect 设置为 'all',所有源目录的内容将会合并。例如,将 thomas admin_user 添加回你的节点定义 site.pp 中的 cookbook:
admin_user {'thomas':
key => 'ABBA...',
keytype => 'rsa',
}
现在再运行 Puppet 来更新 cookbook:
[root@cookbook thomas]# puppet agent -t
Info: Caching catalog for cookbook.example.com
Info: Applying configuration version '1413787770'
Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[thomas]/File[/home/thomas/.bash_profile]/content: content changed '{md5}3e8337f44f84b298a8a99869ae8ca76a' to '{md5}f939eb71a81a9da364410b799e817202'
Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[thomas]/File[/home/thomas/.bash_profile]/group: group changed 'root' to 'thomas'
Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[thomas]/File[/home/thomas/.bash_profile]/mode: mode changed '0644' to '0700'
Notice: /File[/home/thomas/.bash_profile]/seluser: seluser changed 'system_u' to 'unconfined_u'
Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[thomas]/File[/home/thomas/.bash_logout]/ensure: defined content as '{md5}6a5bc1cc5f80a48b540bc09d082b5855'
Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[thomas]/File[/home/thomas/.bashrc]/content: content changed '{md5}db2a20b2b9cdf36cca1ca4672622ddd2' to '{md5}033c3484e4b276e0641becc3aa268a3a'
Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[thomas]/File[/home/thomas/.bashrc]/group: group changed 'root' to 'thomas'
Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[thomas]/File[/home/thomas/.bashrc]/mode: mode changed '0644' to '0700'
Notice: /File[/home/thomas/.bashrc]/seluser: seluser changed 'system_u' to 'unconfined_u'
Notice: /Stage[main]/Main/Node[cookbook]/Admin_user[thomas]/File[/home/thomas/.emacs]/ensure: defined content as '{md5}de7ee35f4058681a834a99b5d1b048b3'
Notice: Finished catalog run in 0.86 seconds
因为我们之前在 cookbook 中应用了 thomas admin_user,所以该用户已存在。在 Puppet 服务器上的 thomas 目录中定义的两个文件已经存在于主目录中,因此只创建了额外的文件 .bash_logout、.bash_profile 和 .emacs。使用这两个参数,你可以拥有可以轻松覆盖的默认文件。
有时候,你可能希望将文件部署到现有目录中,但移除任何不是由 Puppet 管理的文件。一个好的例子是,如果你在环境中使用 mcollective。存放客户端凭证的目录应只包含来自 Puppet 的证书。
purge 参数可以为你完成这个任务。将目录定义为 Puppet 中的资源:
file { '/etc/mcollective/ssl/clients':
purge => true,
recurse => true,
}
recurse 和 purge 的组合将移除 /etc/mcollective/ssl/clients 中所有不是由 Puppet 部署的文件和子目录。然后,你可以通过将文件放置在 Puppet 服务器的适当目录中,将自己的文件部署到该位置。
如果有子目录包含你不想清理的文件,只需将该子目录定义为 Puppet 资源,它将被忽略:
file { '/etc/mcollective/ssl/clients':
purge => true,
recurse => true,
}
file { '/etc/mcollective/ssl/clients/local':
ensure => directory,
}
注意
请注意,至少在当前的 Puppet 实现中,递归文件复制可能会非常慢,并且会给服务器带来较重的内存负载。如果数据不经常变化,最好还是部署并解压一个 tar 文件。可以通过文件资源来管理 tar 文件,并使用 exec 来解压归档文件。对于包含小文件的递归目录,问题不大。Puppet 并不是一个高效的文件服务器,所以使用 Puppet 来创建并分发大型 tar 文件并不是一个好主意。如果你需要复制大文件,使用操作系统的打包工具是更好的解决方案。
清理旧文件
Puppet 的 tidy 资源将帮助你清理旧的或过时的文件,从而减少磁盘使用量。例如,如果你启用了 Puppet 报告功能,如生成报告部分所述,你可能希望定期删除旧的报告文件。
如何操作...
开始吧。
-
按如下方式修改你的
site.pp文件:node 'cookbook' { tidy { '/var/lib/puppet/reports': age => '1w', recurse => true, } } -
运行 Puppet:
[root@cookbook clients]# puppet agent -t Info: Caching catalog for cookbook.example.com Notice: /Stage[main]/Main/Node[cookbook]/File[/var/lib/puppet/reports/cookbook.example.com/201409090637.yaml]/ensure: removed Notice: /Stage[main]/Main/Node[cookbook]/File[/var/lib/puppet/reports/cookbook.example.com/201409100556.yaml]/ensure: removed Notice: /Stage[main]/Main/Node[cookbook]/File[/var/lib/puppet/reports/cookbook.example.com/201409090631.yaml]/ensure: removed Notice: /Stage[main]/Main/Node[cookbook]/File[/var/lib/puppet/reports/cookbook.example.com/201408210557.yaml]/ensure: removed Notice: /Stage[main]/Main/Node[cookbook]/File[/var/lib/puppet/reports/cookbook.example.com/201409080557.yaml]/ensure: removed Notice: /Stage[main]/Main/Node[cookbook]/File[/var/lib/puppet/reports/cookbook.example.com/201409100558.yaml]/ensure: removed Notice: /Stage[main]/Main/Node[cookbook]/File[/var/lib/puppet/reports/cookbook.example.com/201408210546.yaml]/ensure: removed Notice: /Stage[main]/Main/Node[cookbook]/File[/var/lib/puppet/reports/cookbook.example.com/201408210539.yaml]/ensure: removed Notice: Finished catalog run in 0.80 seconds
它是如何工作的...
Puppet 会在指定路径中搜索任何符合 age 参数的文件;在此情况下为 2w(两周)。它还会搜索子目录(recurse => true)。
所有符合你标准的文件将被删除。
还有更多…
你可以通过使用一个字符来指定时间单位(秒、分钟、小时、天或周),如下所示:
-
60s -
180m -
24h -
30d -
4w
你可以指定删除大于某个大小的文件,方式如下:
size => '100m',
这将删除大于或等于 100 MB 的文件。对于千字节,使用 k,对于字节,使用 b。
注意
请注意,如果你同时指定了年龄和大小参数,它们会被视为独立的标准。例如,如果你指定以下内容,Puppet 将删除所有至少一天前的文件,或者至少 512 KB 大小的文件:
age => "1d",
size => "512k",
审计资源
使用 --noop 开关的干运行模式,是一种简单的方式来审计 Puppet 控制下的机器上的任何更改。然而,Puppet 也有一个专门的审计功能,可以报告资源或特定属性的更改。
如何操作…
这是一个展示 Puppet 审计功能的示例:
-
按照如下方式修改你的
site.pp文件:node 'cookbook' { file { '/etc/passwd': audit => [ owner, mode ], } } -
运行 Puppet:
[root@cookbook clients]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413789080' Notice: /Stage[main]/Main/Node[cookbook]/File[/etc/passwd]/owner: audit change: newly-recorded value 0 Notice: /Stage[main]/Main/Node[cookbook]/File[/etc/passwd]/mode: audit change: newly-recorded value 644 Notice: Finished catalog run in 0.55 seconds
它是如何工作的…
audit 元参数告诉 Puppet 你希望记录和监控资源的某些内容。该值可以是你想要审计的参数列表。
在这种情况下,当 Puppet 运行时,它将记录 /etc/passwd 文件的所有者和权限。在未来的运行中,Puppet 会检查这两个内容是否发生了变化。例如,如果你运行:
[root@cookbook ~]# chmod 666 /etc/passwd
Puppet 会在下次运行时捕获这个更改并记录下来:
Notice: /Stage[main]/Main/Node[cookbook]/File[/etc/passwd]/mode: audit change: previously recorded value 0644 has been changed to 0666
还有更多…
这个功能非常有用,可以用来审计大规模网络中机器的任何变化,无论是恶意的还是意外的。它还非常方便用于监控那些不受 Puppet 管理的内容,例如生产服务器上的应用程序代码。你可以在这里了解更多关于 Puppet 审计功能的信息:
puppetlabs.com/blog/all-about-auditing-with-puppet/
如果你只是想审计某个资源的所有内容,可以使用 all:
file { '/etc/passwd':
audit => all,
}
另请参见
- 无操作 - 不改变任何设置 配方见 第十章,监控、报告和故障排除
临时禁用资源
有时候你可能希望暂时禁用一个资源,以免它干扰到其他工作。例如,你可能想在将配置文件推送到 Puppet 之前,在服务器上调整配置,直到你得到确切的设置。你不希望 Puppet 在此期间用旧版本覆盖它,所以你可以在该资源上设置 noop 元参数:
noop => true,
如何操作…
这个示例展示了如何使用 noop 元参数:
-
按照如下方式修改你的
site.pp文件:node 'cookbook' { file { '/etc/resolv.conf': content => "nameserver 127.0.0.1\n", noop => true, } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1413789438' Notice: /Stage[main]/Main/Node[cookbook]/File[/etc/resolv.conf]/content: --- /etc/resolv.conf 2014-10-20 00:27:43.095999975 -0400 +++ /tmp/puppet-file20141020-8439-1lhuy1y-0 2014-10-20 03:17:18.969999979 -0400 @@ -1,3 +1 @@ -; generated by /sbin/dhclient-script -search example.com -nameserver 192.168.122.1 +nameserver 127.0.0.1 Notice: /Stage[main]/Main/Node[cookbook]/File[/etc/resolv.conf]/content: current_value {md5}4c0d192511df253826d302bc830a371b, should be {md5}949343428bded6a653a85910f6bdb48e (noop) Notice: Node[cookbook]: Would have triggered 'refresh' from 1 events Notice: Class[Main]: Would have triggered 'refresh' from 1 events Notice: Stage[main]: Would have triggered 'refresh' from 1 events Notice: Finished catalog run in 0.50 seconds
它是如何工作的…
noop 元参数设置为 true,因此对于这个特定的资源,它就像你需要用 --noop 标志运行 Puppet 一样。Puppet 注意到该资源本来会被应用,但实际上什么都没有做。
在测试模式下运行代理(-t)的一个好处是,Puppet 会输出如果没有 noop 的话,它会执行的差异(你可以通过 --show_diff 在不使用 -t 的情况下让 Puppet 显示差异;-t 涉及许多不同的设置):
--- /etc/resolv.conf 2014-10-20 00:27:43.095999975 -0400
+++ /tmp/puppet-file20141020-8439-1lhuy1y-0 2014-10-20 03:17:18.969999979 -0400
@@ -1,3 +1 @@
-; generated by /sbin/dhclient-script
-search example.com
-nameserver 192.168.122.1
+nameserver 127.0.0.1
这在调试模板时非常有用;你可以在不实际应用更改的情况下,先进行修改,然后查看它们在节点上会是什么样子。通过查看差异,你可以判断更新后的模板是否产生了正确的输出。
第七章:管理应用程序
| 每个人都知道,调试比编写程序要困难两倍。那么,如果你在编写时尽可能聪明,你又怎么调试它呢? | ||
|---|---|---|
| --Brian W. Kernighan. |
在本章中,我们将介绍以下食谱:
-
使用公共模块
-
管理 Apache 服务器
-
创建 Apache 虚拟主机
-
创建 nginx 虚拟主机
-
管理 MySQL
-
创建数据库和用户
引言
没有应用程序,服务器只是一个非常昂贵的取暖器。在本章中,我将展示一些使用 Puppet 管理特定软件的食谱:MySQL、Apache、nginx 和 Ruby。我希望这些食谱本身对你有所帮助。然而,它们所使用的模式和技术适用于几乎任何软件,因此你可以轻松地将其调整为适合自己的目的。这些应用程序的一个共同点是它们很常见。大多数 Puppet 安装都需要处理一个 Web 服务器,Apache 或 nginx。大多数情况下,甚至所有情况下,都会有数据库,其中一些将使用 MySQL。当每个人都需要处理一个问题时,社区解决方案通常比自家开发的解决方案经过更好的测试和更彻底的完善。我们将在本章中使用来自 Puppet Forge 的模块来管理这些应用程序。
当你从头开始编写自己的 Apache 或 nginx 模块时,你需要关注你所支持的发行版的细微差别。一些发行版将 Apache 包称为 httpd,而其他发行版则使用 apache2;MySQL 也是如此。此外,基于 Debian 的发行版使用启用文件夹方法来启用 Apache 中的自定义站点,这些站点是虚拟站点,而基于 RPM 的发行版则没有这种方法。有关虚拟站点的更多信息,请访问 httpd.apache.org/docs/2.2/vhosts/。
使用公共模块
当你编写一个 Puppet 模块来管理某些软件或服务时,你不必从头开始。许多流行应用程序的社区贡献模块可以在 Puppet Forge 网站上找到。有时,社区模块正是你所需要的,你可以直接下载并开始使用。在大多数情况下,你需要做一些修改以适应你特定的需求和环境。
和所有社区项目一样,Forge 上有一些优秀的模块,也有一些不太优秀的模块。你应该阅读模块的 README 部分,决定该模块是否适用于你的安装。至少要确保你的发行版是受支持的。Puppetlabs 引入了一些受支持的模块,也就是说,如果你是企业客户,他们会支持你在安装中使用这些模块。此外,大多数 Forge 模块都涉及多个操作系统、发行版和大量用例。在很多情况下,不使用 Forge 模块就像是重新发明轮子。不过,有一点需要注意的是,Forge 模块可能比你本地的模块更复杂。你应该阅读代码并了解模块的作用。了解模块如何工作将帮助你在之后进行调试。
如何操作…
在这个例子中,我们将使用puppet module命令来查找并安装有用的stdlib模块,该模块包含许多帮助你开发 Puppet 代码的工具函数。它是 puppetlabs 提供的上述受支持模块之一。我将把模块下载到我的用户主目录,并手动将其安装到 Git 仓库中。要安装 puppetlabs stdlib 模块,请按照以下步骤操作:
-
执行以下命令:
t@mylaptop ~ $ puppet module search puppetlabs-stdlib Notice: Searching https://forgeapi.puppetlabs.com ... NAME DESCRIPTION AUTHOR KEYWORDS puppetlabs-stdlib Puppet Module Standard Library @puppetlabs stdlib stages -
我们已经验证了我们所需的模块,因此现在将使用
module install命令安装它:t@mylaptop ~ $ puppet module install puppetlabs-stdlib Notice: Preparing to install into /home/thomas/.puppet/modules ... Notice: Downloading from https://forgeapi.puppetlabs.com ... Notice: Installing -- do not interrupt ... /home/thomas/.puppet/modules └── puppetlabs-stdlib (v4.3.2) -
该模块现在已准备好在你的清单中使用;大多数优秀模块都会附带一个
README文件,告诉你如何操作。
它是如何工作的…
你可以使用 puppet module search 命令搜索与你感兴趣的软件包或软件匹配的模块。要安装特定的模块,使用 puppet module install。你可以添加 -i 选项来告诉 Puppet 在哪里找到你的模块目录。
你可以浏览 Forge,查看可用的模块:forge.puppetlabs.com/。
有关受支持模块的更多信息,请访问 forge.puppetlabs.com/supported。
当前支持的模块列表可以在 forge.puppetlabs.com/modules?endorsements=supported 查看。
还有更多内容…
Forge 上的模块包括一个 metadata.json 文件,描述该模块以及模块支持的操作系统。此文件还包含该模块所需的其他模块列表。
注意
该文件之前被命名为 Modulefile,且不采用 JSON 格式;旧的 Modulefile 格式在 3.6 版本中已被弃用。
正如我们在下一节中将看到的,当从 Forge 安装一个模块时,所需的依赖项也会自动安装。
并非所有公开可用的模块都在 Puppet Forge 上。一些很棒的模块可以在 GitHub 上找到:
尽管 Puppet Cookbook 网站本身不是一个模块集合,但它包含了许多有用且富有启发性的代码示例、模式和提示,由令人钦佩的 Dean Wilson 维护:
管理 Apache 服务器
Apache 是世界上最受欢迎的 Web 服务器,因此您的 Puppet 工作职责中很可能包括安装和管理 Apache。
如何操作...
我们将安装并使用 puppetlabs-apache 模块来安装并启动 Apache。这一次,当我们运行 puppet module install 时,我们将使用 -i 选项告诉 Puppet 将模块安装到我们 Git 仓库的模块目录中。
-
使用
puppet modules install安装模块:t@mylaptop ~/puppet $ puppet module install -i modules puppetlabs-apache Notice: Preparing to install into /home/thomas/puppet/modules ... Notice: Downloading from https://forgeapi.puppetlabs.com ... Notice: Installing -- do not interrupt ... /home/thomas/puppet/modules └─┬ puppetlabs-apache (v1.1.1) ├── puppetlabs-concat (v1.1.1) └── puppetlabs-stdlib (v4.3.2) -
将模块添加到您的 Git 仓库并推送:
t@mylaptop ~/puppet $ git add modules/apache modules/concat modules/stdlib t@mylaptop ~/puppet $ git commit -m "adding puppetlabs-apache module" [production 395b079] adding puppetlabs-apache module 647 files changed, 35017 insertions(+), 13 deletions(-) rename modules/{apache => apache.cookbook}/manifests/init.pp (100%) create mode 100644 modules/apache/CHANGELOG.md create mode 100644 modules/apache/CONTRIBUTING.md ... t@mylaptop ~/puppet $ git push origin production Counting objects: 277, done. Delta compression using up to 4 threads. Compressing objects: 100% (248/248), done. Writing objects: 100% (266/266), 136.25 KiB | 0 bytes/s, done. Total 266 (delta 48), reused 0 (delta 0) remote: To puppet@puppet.example.com:/etc/puppet/environments/puppet.git remote: 9faaa16..395b079 production -> production -
在
site.pp中创建一个 Web 服务器节点定义:node webserver { class {'apache': } } -
运行 Puppet 来应用默认的 Apache 模块配置:
[root@webserver ~]# puppet agent -t Info: Caching certificate for webserver.example.com Notice: /File[/var/lib/puppet/lib/puppet/provider/a2mod]/ensure: created ... Info: Caching catalog for webserver.example.com ... Info: Class[Apache::Service]: Scheduling refresh of Service[httpd] Notice: /Stage[main]/Apache::Service/Service[httpd]: Triggered 'refresh' from 51 events Notice: Finished catalog run in 11.73 seconds -
验证您是否可以访问
webserver.example.com:[root@webserver ~]# curl http://webserver.example.com <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <html> <head> <title>Index of /</title> </head> <body> <h1>Index of /</h1> <table><tr><th><img src="img/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr><tr><th colspan="5"><hr></th></tr> <tr><th colspan="5"><hr></th></tr> </table> </body></html>
它是如何工作的...
从 Forge 安装 puppetlabs-Apache 模块会同时安装 puppetlabs-concat 和 puppetlabs-stdlib 到我们的模块目录中。concat 模块用于将文件片段按特定顺序拼接在一起。它被 Apache 模块用于创建主要的 Apache 配置文件。
然后我们定义了一个 Web 服务器节点,并将 Apache 类应用到该节点上。我们使用了所有默认值,并让 Apache 模块将我们的服务器配置为 Apache Web 服务器。
然后,Apache 模块会重写我们所有的 Apache 配置文件。默认情况下,该模块会清除 Apache 目录中的所有配置文件(/etc/apache2 或 /etc/httpd,具体取决于发行版)。该模块可以配置多种不同的发行版,并处理每个发行版的细微差别。作为该模块的用户,您无需了解您的发行版如何处理 Apache 模块配置。
在清除并重写配置文件后,该模块会确保 apache2 服务正在运行(在企业级 Linux(EL)系统上是 httpd)。
然后我们使用 curl 测试了 Web 服务器。返回的只有一个空的索引页面。这是预期的行为。通常,当我们在服务器上安装 Apache 时,有一些文件会显示默认页面(在 EL 系统上是 welcome.conf),由于模块清除了这些配置,我们只看到了一个空页面。
在生产环境中,您将修改 Apache 模块应用的默认设置;README 中建议的配置如下:
class { 'apache':
default_mods => false,
default_confd_files => false,
}
创建 Apache 虚拟主机
Apache 虚拟主机是通过 apache 模块和定义的类型 apache::vhost 创建的。我们将在 Apache Web 服务器上创建一个新的虚拟主机,命名为 navajo,这是 Apache 部落之一。
如何操作...
按照以下步骤创建 Apache 虚拟主机:
-
创建一个新的 navajo
apache::vhost定义,如下所示:apache::vhost { 'navajo.example.com': port => '80', docroot => '/var/www/navajo', } -
为新的虚拟主机创建一个索引文件:
file {'/var/www/navajo/index.html': content => "<html>\nnavajo.example.com\nhttp://en.wikipedia.org/wiki/Navajo_people\n</html>\n", mode => '0644', require => Apache::Vhost['navajo.example.com'] } -
运行 Puppet 来创建新的虚拟主机:
[root@webserver ~]# puppet agent -t Info: Caching catalog for webserver.example.com Info: Applying configuration version '1414475598' Notice: /Stage[main]/Main/Node[webserver]/Apache::Vhost[navajo.example.com]/File[/var/www/navajo]/ensure: created Notice: /Stage[main]/Main/Node[webserver]/Apache::Vhost[navajo.example.com]/File[25-navajo.example.com.conf]/ensure: created Info: /Stage[main]/Main/Node[webserver]/Apache::Vhost[navajo.example.com]/File[25-navajo.example.com.conf]: Scheduling refresh of Service[httpd] Notice: /Stage[main]/Main/Node[webserver]/File[/var/www/navajo/index.html]/ensure: defined content as '{md5}5212fe215f4c0223fb86102a34319cc6' Notice: /Stage[main]/Apache::Service/Service[httpd]: Triggered 'refresh' from 1 events Notice: Finished catalog run in 2.73 seconds -
验证您是否可以访问新的虚拟主机:
[root@webserver ~]# curl http://navajo.example.com <html> navajo.example.com http://en.wikipedia.org/wiki/Navajo_people </html>
它是如何工作的...
apache::vhost定义的类型为 Apache 创建了一个虚拟主机配置文件,文件名为25-navajo.example.com.conf。该文件通过模板创建;文件名开头的25是该虚拟主机的“优先级”级别。当 Apache 首次启动时,它会遍历其配置目录,并按照字母顺序执行文件。以数字开头的文件会先于以字母开头的文件被读取。通过这种方式,Apache 模块确保虚拟主机按照特定顺序读取,而这个顺序可以在定义虚拟主机时进行指定。该文件的内容如下:
# ************************************
# Vhost template in module puppetlabs-apache
# Managed by Puppet
# ************************************
<VirtualHost *:80>
ServerName navajo.example.com
## Vhost docroot
DocumentRoot "/var/www/navajo"
## Directories, there should at least be a declaration for /var/www/navajo
<Directory "/var/www/navajo">
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
Allow from all
</Directory>
## Load additional static includes
## Logging
ErrorLog "/var/log/httpd/navajo.example.com_error.log"
ServerSignature Off
CustomLog "/var/log/httpd/navajo.example.com_access.log" combined
</VirtualHost>
如你所见,默认文件已经创建了日志文件,设置了目录访问权限和选项,此外还指定了监听端口和DocumentRoot。
vhost 定义创建了DocumentRoot目录,该目录作为'root'传递给apache::virtual定义。目录在虚拟主机配置文件之前创建;文件创建之后,通知触发器会发送给 Apache 进程以重新启动。
我们的清单文件包含了一个需要Apache::Vhost['navajo.example.com']资源的文件;在目录和虚拟主机配置文件创建之后,我们的文件也随之创建。
当我们在新网站上运行 curl 时(如果你没有在 DNS 中创建主机名别名,你需要在本地/etc/hosts文件中为navajo.example.com创建一个,或者将主机指定为curl -H 'Host: navajo.example.com' <navajo.example.com 的 ip 地址>),我们会看到我们创建的索引文件内容:
file {'/var/www/navajo/index.html':
content => "<html>\nnavajo.example.com\nhttp://en.wikipedia.org/wiki/Navajo_people\n</html>\n",
mode => '0644',
require => Apache::Vhost['navajo.example.com']
}
[root@webserver ~]# curl http://navajo.example.com
<html>
navajo.example.com
http://en.wikipedia.org/wiki/Navajo_people
<\html>
还有更多...
定义的类型和模板都考虑了虚拟主机的多种配置场景。几乎不可能找到这个模块没有涵盖的设置。你应该查看apache::virtual的定义,以及可能的各种参数。
该模块还为你处理了几个设置。例如,如果我们将navajo虚拟主机的监听端口从80更改为8080,该模块将在/etc/httpd/conf.d/ports.conf中进行如下更改:
Listen 80
+Listen 8080
NameVirtualHost *:80
+NameVirtualHost *:8080
在我们的虚拟主机文件中:
-<VirtualHost *:80>
+<VirtualHost *:8080>
这样我们就可以在端口8080上使用 curl,并看到相同的结果:
[root@webserver ~]# curl http://navajo.example.com:8080
<html>
navajo.example.com
http://en.wikipedia.org/wiki/Navajo_people
</html>
当我们尝试使用端口80时:
[root@webserver ~]# curl http://navajo.example.com
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Index of /</title>
</head>
<body>
<h1>Index of /</h1>
<table><tr><th><img src="img/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr><tr><th colspan="5"><hr></th></tr>
<tr><th colspan="5"><hr></th></tr>
</table>
</body>
</html>
如我们所见,虚拟主机不再监听端口80,我们收到的是之前示例中看到的默认空目录列表。
创建 nginx 虚拟主机
Nginx 是一个快速、轻量级的 Web 服务器,在许多场景中,比 Apache 更受欢迎,特别是在性能要求较高的环境中。Nginx 的配置方式与 Apache 略有不同;不过,像 Apache 一样,有一个 Forge 模块可以帮助我们配置 nginx。与 Apache 不同的是,推荐使用的这个模块并不是由 puppetlabs 提供的,而是由 James Fryman 提供的。这个模块使用了一些有趣的技巧来进行自我配置。该模块的早期版本使用了 R.I. Pienaar 的 module_data 包。这个包用于在模块内配置 hieradata,为 nginx 模块提供默认值。现在我不建议从这个模块开始使用,但它是一个很好的例子,展示了模块配置未来可能的发展方向。赋予模块修改 hieradata 的能力可能会变得非常有用。
如何操作...
在这个示例中,我们将使用一个 Forge 模块来配置 nginx。我们将下载该模块,并使用它来配置虚拟主机。
-
从 Forge 下载
jfryman-nginx模块:t@mylaptop ~ $ cd ~/puppet t@mylaptop ~/puppet $ puppet module install -i modules jfryman-nginx Notice: Preparing to install into /home/thomas/puppet/modules ... Notice: Downloading from https://forgeapi.puppetlabs.com ... Notice: Installing -- do not interrupt ... /home/thomas/puppet/modules └─┬ jfryman-nginx (v0.2.1) ├── puppetlabs-apt (v1.7.0) ├── puppetlabs-concat (v1.1.1) └── puppetlabs-stdlib (v4.3.2) -
将 webserver 的定义替换为 nginx 配置:
node webserver { class {'nginx':} nginx::resource::vhost { 'mescalero.example.com': www_root => '/var/www/mescalero', } file {'/var/www/mescalero': ensure => 'directory''directory', mode => '0755', require => Nginx::Resource::Vhost['mescalero.example.com'], } file {'/var/www/mescalero/index.html': content => "<html>\nmescalero.example.com\nhttp://en.wikipedia.org/wiki/Mescalero\n</html>\n", mode => 0644, require => File['/var/www/mescalero'], } } -
如果 Apache 仍在 Web 服务器上运行,请停止它:
[root@webserver ~]# puppet resource service httpd ensure=false Notice: /Service[httpd]/ensure: ensure changed 'running' to 'stopped' service { 'httpd': ensure => 'stopped', } Run puppet agent on your webserver node: [root@webserver ~]# puppet agent -t Info: Caching catalog for webserver.example.com Info: Applying configuration version '1414561483' Notice: /Stage[main]/Main/Node[webserver]/Nginx::Resource::Vhost[mescalero.example.com]/Concat[/etc/nginx/sites-available/mescalero.example.com.conf]/File[/etc/nginx/sites-available/mescalero.example.com.conf]/ensure: defined content as '{md5}35bb59bfcd0cf5a549d152aaec284357' Info: /Stage[main]/Main/Node[webserver]/Nginx::Resource::Vhost[mescalero.example.com]/Concat[/etc/nginx/sites-available/mescalero.example.com.conf]/File[/etc/nginx/sites-available/mescalero.example.com.conf]: Scheduling refresh of Class[Nginx::Service] Info: Concat[/etc/nginx/sites-available/mescalero.example.com.conf]: Scheduling refresh of Class[Nginx::Service] Notice: /Stage[main]/Main/Node[webserver]/Nginx::Resource::Vhost[mescalero.example.com]/File[mescalero.example.com.conf symlink]/ensure: created Info: /Stage[main]/Main/Node[webserver]/Nginx::Resource::Vhost[mescalero.example.com]/File[mescalero.example.com.conf symlink]: Scheduling refresh of Service[nginx] Notice: /Stage[main]/Main/Node[webserver]/File[/var/www/mescalero]/ensure: created Notice: /Stage[main]/Main/Node[webserver]/File[/var/www/mescalero/index.html]/ensure: defined content as '{md5}2bd618c7dc3a3addc9e27c2f3cfde294' Notice: /Stage[main]/Nginx::Config/File[/etc/nginx/conf.d/proxy.conf]/ensure: defined content as '{md5}1919fd65635d49653273e14028888617' Info: Computing checksum on file /etc/nginx/conf.d/example_ssl.conf Info: /Stage[main]/Nginx::Config/File[/etc/nginx/conf.d/example_ssl.conf]: Filebucketed /etc/nginx/conf.d/example_ssl.conf to puppet with sum 84724f296c7056157d531d6b1215b507 Notice: /Stage[main]/Nginx::Config/File[/etc/nginx/conf.d/example_ssl.conf]/ensure: removed Info: Computing checksum on file /etc/nginx/conf.d/default.conf Info: /Stage[main]/Nginx::Config/File[/etc/nginx/conf.d/default.conf]: Filebucketed /etc/nginx/conf.d/default.conf to puppet with sum 4dce452bf8dbb01f278ec0ea9ba6cf40 Notice: /Stage[main]/Nginx::Config/File[/etc/nginx/conf.d/default.conf]/ensure: removed Info: Class[Nginx::Config]: Scheduling refresh of Class[Nginx::Service] Info: Class[Nginx::Service]: Scheduling refresh of Service[nginx] Notice: /Stage[main]/Nginx::Service/Service[nginx]: Triggered 'refresh' from 2 events Notice: Finished catalog run in 28.98 seconds -
验证是否能访问新的虚拟主机:
[root@webserver ~]# curl mescalero.example.com <html> mescalero.example.com http://en.wikipedia.org/wiki/Mescalero </html>
它是如何工作的...
安装 jfryman-nginx 模块会导致 concat、stdlib 和 APT 模块被安装。我们在主节点上运行 Puppet,让这些模块创建的插件被添加到正在运行的主节点中。stdlib 和 concat 模块包含了需要安装的 facter 和 Puppet 插件,以确保 nginx 模块能够正常工作。
插件同步后,我们可以在我们的 Web 服务器上运行 puppet agent。作为预防措施,如果 Apache 之前已经启动,我们先停止 Apache(因为我们不能让 nginx 和 Apache 都监听端口 80)。运行 puppet agent 后,我们确认 nginx 正在运行,且虚拟主机已配置好。
还有更多内容...
这个 nginx 模块正在积极开发中。模块中采用了几种有趣的解决方案。之前的版本使用了 ripienaar-module_data 模块,该模块通过 hiera 插件允许模块为其各种属性包含默认值。尽管仍处于开发的早期阶段,但该系统已经可以使用,并且代表了 Forge 上最前沿的模块之一。
在接下来的章节中,我们将使用一个支持的模块来配置和管理 MySQL 安装。
管理 MySQL
MySQL 是一个非常广泛使用的数据库服务器,几乎可以确定你在某个时刻需要安装并配置一个 MySQL 服务器。puppetlabs-mysql 模块可以简化 MySQL 部署。
如何操作...
按照以下步骤创建示例:
-
安装
puppetlabs-mysql模块:t@mylaptop ~/puppet $ puppet module install -i modules puppetlabs-mysql Notice: Preparing to install into /home/thomas/puppet/modules ... Notice: Downloading from https://forgeapi.puppetlabs.com ... Notice: Installing -- do not interrupt ... /home/thomas/puppet/modules └─┬ puppetlabs-mysql (v2.3.1) └── puppetlabs-stdlib (v4.3.2) -
为您的 MySQL 服务器创建一个新的节点定义:
node dbserver { class { '::mysql::server': root_password => 'PacktPub', override_options => { 'mysqld' => { 'max_connections' => '1024' } } } } -
运行 Puppet 安装数据库服务器并应用新的 root 密码:
[root@dbserver ~]# puppet agent -t Info: Caching catalog for dbserver.example.com Info: Applying configuration version '1414566216' Notice: /Stage[main]/Mysql::Server::Install/Package[mysql-server]/ensure: created Notice: /Stage[main]/Mysql::Server::Service/Service[mysqld]/ensure: ensure changed 'stopped' to 'running' Info: /Stage[main]/Mysql::Server::Service/Service[mysqld]: Unscheduling refresh on Service[mysqld] Notice: /Stage[main]/Mysql::Server::Root_password/Mysql_user[root@localhost]/password_hash: defined 'password_hash' as '*6ABB0D4A7D1381BAEE4D078354557D495ACFC059' Notice: /Stage[main]/Mysql::Server::Root_password/File[/root/.my.cnf]/ensure: defined content as '{md5}87bc129b137c9d613e9f31c80ea5426c' Notice: Finished catalog run in 35.50 seconds -
验证是否能连接到数据库:
[root@dbserver ~]# mysql Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 11 Server version: 5.1.73 Source distribution Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>
它是如何工作的...
MySQL 模块安装 MySQL 服务器并确保服务器正在运行。然后,它配置 MySQL 的 root 密码。这个模块还为你做了很多其他事情。它创建了一个.my.cnf文件,其中包含 root 用户密码。当我们运行mysql客户端时,.my.cnf文件会设置所有默认值,因此我们无需提供任何参数。
还有更多...
在下一部分中,我们将展示如何创建数据库和用户。
创建数据库和用户
管理数据库意味着不仅要确保服务正在运行;没有数据库,数据库服务器什么都不是。数据库需要用户和权限。权限是通过GRANT语句来处理的。我们将使用puppetlabs-mysql包来创建一个数据库,并为该数据库创建一个用户。我们将创建一个名为 Drupal 的 MySQL 用户,以及一个名为 Drupal 的数据库。接着,我们将创建一个名为 nodes 的表,并将数据插入该表。
如何操作...
按照以下步骤创建数据库和用户:
-
在
dbserver类中创建一个数据库定义:mysql::db { 'drupal': host => 'localhost', user => 'drupal', password => 'Cookbook', sql => '/root/drupal.sql', require => File['/root/drupal.sql'] } file { '/root/drupal.sql': ensure => present, source => 'puppet:///modules/mysql/drupal.sql', } -
允许 Drupal 用户修改 nodes 表:
mysql_grant { 'drupal@localhost/drupal.nodes': ensure => 'present', options => ['GRANT'], privileges => ['ALL'], table => 'drupal.nodes'nodes', user => 'drupal@localhost', } -
创建一个包含以下内容的
drupal.sql文件:CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255), body TEXT); INSERT INTO users (id, title, body) VALUES (1,'First Node','Contents of first Node'); INSERT INTO users (id, title, body) VALUES (2,'Second Node','Contents of second Node'); -
运行 Puppet 以创建用户、数据库和
GRANT:[root@dbserver ~]# puppet agent -t Info: Caching catalog for dbserver.example.com Info: Applying configuration version '1414648818' Notice: /Stage[main]/Main/Node[dbserver]/File[/root/drupal.sql]/ensure: defined content as '{md5}780f3946cfc0f373c6d4146394650f6b' Notice: /Stage[main]/Main/Node[dbserver]/Mysql_grant[drupal@localhost/drupal.nodes]/ensure: created Notice: /Stage[main]/Main/Node[dbserver]/Mysql::Db[drupal]/Mysql_user[drupal@localhost]/ensure: created Notice: /Stage[main]/Main/Node[dbserver]/Mysql::Db[drupal]/Mysql_database[drupal]/ensure: created Info: /Stage[main]/Main/Node[dbserver]/Mysql::Db[drupal]/Mysql_database[drupal]: Scheduling refresh of Exec[drupal-import] Notice: /Stage[main]/Main/Node[dbserver]/Mysql::Db[drupal]/Mysql_grant[drupal@localhost/drupal.*]/ensure: created Notice: /Stage[main]/Main/Node[dbserver]/Mysql::Db[drupal]/Exec[drupal-import]: Triggered 'refresh' from 1 events Notice: Finished catalog run in 10.06 seconds -
验证数据库和表是否已创建:
[root@dbserver ~]# mysql drupal Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 34 Server version: 5.1.73 Source distribution Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show tables; +------------------+ | Tables_in_drupal | +------------------+ | users | +------------------+ 1 row in set (0.00 sec) -
现在,验证我们的默认数据是否已加载到表中:
mysql> select * from users; +----+-------------+-------------------------+ | id | title | body | +----+-------------+-------------------------+ | 1 | First Node | Contents of first Node | | 2 | Second Node | Contents of second Node | +----+-------------+-------------------------+ 2 rows in set (0.00 sec)
它是如何工作的...
我们从新 Drupal 数据库的定义开始:
mysql::db { 'drupal':
host => 'localhost',
user => 'drupal',
password => 'Cookbook',
sql => '/root/drupal.sql',
require => File['/root/drupal.sql']
}
我们指定将从 localhost 连接(也可以从另一台服务器连接到数据库),使用 drupal 用户。我们为该用户提供密码,并指定一个将在数据库创建后应用的 SQL 文件。我们要求这个文件已经存在,并接下来定义该文件:
file { '/root/drupal.sql':
ensure => present,
source => 'puppet:///modules/mysql/drupal.sql',
}
然后,我们确保用户拥有适当的权限,通过mysql_grant语句:
mysql_grant { 'drupal@localhost/drupal.nodes':
ensure => 'present',
options => ['GRANT'],
privileges => ['ALL'],
table => 'drupal.nodes',
user => 'drupal@localhost',
}
还有更多...
使用 puppetlabs-MySQL 和 puppetlabs-Apache 模块,我们可以创建一个完整的功能性 Web 服务器。puppetlabs-Apache 模块将安装 Apache,我们还可以包括 PHP 模块和 MySQL 模块。然后,我们可以使用 puppetlabs-MySQL 模块来安装 MySQL 服务器,并创建所需的 Drupal 数据库,再将数据填充到数据库中。
部署一个新的 Drupal 安装就像在节点上包含一个类一样简单。
第八章:节点间协调
| “休息不是懒散,有时在夏日的午后躺在树下,听着水流的潺潺声,或看着云朵飘过天空,绝不是浪费时间。” | ||
|---|---|---|
| --约翰·拉博克 |
在本章中,我们将介绍以下内容:
-
使用 iptables 管理防火墙
-
使用 Heartbeat 构建高可用服务
-
管理 NFS 服务器和文件共享
-
使用 HAProxy 对多个 Web 服务器进行负载均衡
-
使用 Puppet 管理 Docker
介绍
Puppet 强大的功能可以管理单台服务器的配置,但当需要协调多台机器时,它的作用更加明显。在本章中,我们将探讨如何使用 Puppet 来帮助你创建高可用集群、在网络中共享文件、设置自动化防火墙,并通过负载均衡来最大化现有机器的效能。我们将使用导出的资源作为节点之间的通信手段。
使用 iptables 管理防火墙
在本章中,我们将开始配置需要通过网络进行主机间通信的服务。大多数 Linux 发行版默认会运行基于主机的防火墙 iptables。如果你希望主机之间可以互相通信,你有两个选择:关闭 iptables 或者配置 iptables 允许通信。
我倾向于保持 iptables 开启并配置访问控制。启用 iptables 就是为你的网络防线增加了一层保护。iptables 并不是一剂能够让系统变得绝对安全的“魔法药丸”,但它会阻止你无意间暴露给网络的服务。
正确配置 iptables 是一项复杂的任务,需要对网络有深入的了解。这里展示的示例是简化版。如果你不熟悉 iptables,建议在继续之前先研究一下 iptables。更多信息可以参考 wiki.centos.org/HowTos/Network/IPTables 或 help.ubuntu.com/community/IptablesHowTo。
准备工作
在以下示例中,我们将使用 Puppet Labs 防火墙模块来配置 iptables。通过 puppet module install 将该模块安装到你的 Git 仓库中进行准备:
t@mylaptop ~ $ puppet module install -i ~/puppet/modules puppetlabs-firewall
Notice: Preparing to install into /home/thomas/puppet/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
/home/thomas/puppet/modules
└── puppetlabs-firewall (v1.2.0)
如何实现...
为了配置防火墙模块,我们需要创建一组规则,这些规则会在所有其他规则之前应用。作为一个简单的例子,我们将创建以下规则:
-
允许所有流量通过回环 (lo) 接口
-
允许所有 ICMP 流量
-
允许所有属于已建立连接的流量(ESTABLISHED, RELATED)
-
允许所有 TCP 流量通过 22 端口(ssh)
我们将创建一个 myfw(我的防火墙)类来配置防火墙模块。然后,我们将在节点上应用 myfw 类,来配置该节点上的 iptables:
-
创建一个类来包含这些规则,并将其命名为
myfw::pre:class myfw::pre { Firewall { require => undef, } firewall { '0000 Allow all traffic on loopback': proto => 'all', iniface => 'lo', action => 'accept', } firewall { '0001 Allow all ICMP': proto => 'icmp', action => 'accept', } firewall { '0002 Allow all established traffic': proto => 'all', state => ['RELATED', 'ESTABLISHED'], action => 'accept', } firewall { '0022 Allow all TCP on port 22 (ssh)': proto => 'tcp', port => '22', action => 'accept', } } -
当流量不匹配任何先前的规则时,我们希望有一条最终的规则来丢弃流量。创建
myfw::post类来包含默认的丢弃规则:class myfw::post { firewall { '9999 Drop all other traffic': proto => 'all', action => 'drop', before => undef, } } -
创建一个
myfw类,其中包括myfw::pre和myfw::post来配置防火墙:class myfw { include firewall # our rulesets include myfw::post include myfw::pre # clear all the rules resources { "firewall": purge => true } # resource defaults Firewall { before => Class['myfw::post'], require => Class['myfw::pre'], } } -
将
myfw类附加到节点定义上;我将对我的食谱节点执行此操作:node cookbook { include myfw } -
在食谱上运行 Puppet,以查看防火墙规则是否已应用:
[root@cookbook ~]# puppet agent -t Info: Retrieving pluginfacts Info: Retrieving plugin Info: Loading facts Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1415512948' Notice: /Stage[main]/Myfw::Pre/Firewall[000 Allow all traffic on loopback]/ensure: created Notice: /File[/etc/sysconfig/iptables]/seluser: seluser changed 'unconfined_u' to 'system_u' Notice: /Stage[main]/Myfw::Pre/Firewall[0001 Allow all ICMP]/ensure: created Notice: /Stage[main]/Myfw::Pre/Firewall[0022 Allow all TCP on port 22 (ssh)]/ensure: created Notice: /Stage[main]/Myfw::Pre/Firewall[0002 Allow all established traffic]/ensure: created Notice: /Stage[main]/Myfw::Post/Firewall[9999 Drop all other traffic]/ensure: created Notice: /Stage[main]/Myfw/Firewall[9003 49bcd611c61bdd18b235cea46ef04fae]/ensure: removed Notice: Finished catalog run in 15.65 seconds -
使用
iptables-save验证新规则:# Generated by iptables-save v1.4.7 on Sun Nov 9 01:18:30 2014 *filter :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [74:35767] -A INPUT -i lo -m comment --comment "0000 Allow all traffic on loopback" -j ACCEPT -A INPUT -p icmp -m comment --comment "0001 Allow all ICMP" -j ACCEPT -A INPUT -m comment --comment "0002 Allow all established traffic" -m state --state RELATED,ESTABLISHED -j ACCEPT -A INPUT -p tcp -m multiport --ports 22 -m comment --comment "022 Allow all TCP on port 22 (ssh)" -j ACCEPT -A INPUT -m comment --comment "9999 Drop all other traffic" -j DROP COMMIT # Completed on Sun Nov 9 01:18:30 2014
它是如何工作的...
这是一个很好的例子,展示了如何使用元参数以较少的努力实现复杂的顺序。我们的myfw模块实现了以下配置:

myfw::pre类中的所有规则都确保在我们定义的任何其他防火墙规则之前执行。myfw::post中的规则则确保在任何其他防火墙规则之后执行。因此,我们首先执行myfw::pre中的规则,然后是其他规则,最后是myfw::post中的规则。
我们为myfw类的定义设置了这个依赖关系,使用资源默认值:
# resource defaults
Firewall {
before => Class['myfw::post'],
require => Class['myfw::pre'],
}
这些默认设置首先告诉 Puppet,任何防火墙资源应该在myfw::post类中的任何内容之前执行。其次,它们告诉 Puppet,任何防火墙资源应该要求myfw::pre中的资源已经执行过。
当我们定义myfw::pre类时,我们在防火墙资源的资源默认值中移除了 require 语句。这样可以确保myfw::pre类中的资源在执行之前不会相互依赖(否则 Puppet 会抱怨我们创建了一个循环依赖):
Firewall {
require => undef,
}
我们在myfw::post定义中使用了相同的技巧。在这种情况下,我们在 post 类中只有一条规则,因此我们简单地移除了before要求:
firewall { '9999 Drop all other traffic':
proto => 'all',
action => 'drop',
before => undef,
}
最后,我们加入一条规则,用于清除系统上所有现有的 iptables 规则。这样做是为了确保我们拥有一套一致的规则;只有 Puppet 中定义的规则会被保留:
# clear all the rules
resources { "firewall":
purge => true
}
还有更多...
正如我们所提示的,我们现在可以在我们的清单中定义防火墙资源,并在初始化规则(myfw::pre)之后应用到 iptables 配置中,但在最终的丢弃规则(myfw::post)之前。例如,要允许我们的食谱机器上 HTTP 流量,请按照以下方式修改节点定义:
include myfw
firewall {'0080 Allow HTTP':
proto => 'tcp',
action => 'accept',
port => 80,
}
在食谱上运行 Puppet:
[root@cookbook ~]# puppet agent -t
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Loading facts
Info: Caching catalog for cookbook.example.com
Info: Applying configuration version '1415515392'
Notice: /File[/etc/sysconfig/iptables]/seluser: seluser changed 'unconfined_u' to 'system_u'
Notice: /Stage[main]/Main/Node[cookbook]/Firewall[0080 Allow HTTP]/ensure: created
Notice: Finished catalog run in 2.74 seconds
验证新规则是否已添加到最后一条myfw::pre规则之后(端口 22,ssh):
[root@cookbook ~]# iptables-save
# Generated by iptables-save v1.4.7 on Sun Nov 9 01:46:38 2014
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [41:26340]
-A INPUT -i lo -m comment --comment "0000 Allow all traffic on loopback" -j ACCEPT
-A INPUT -p icmp -m comment --comment "0001 Allow all ICMP" -j ACCEPT
-A INPUT -m comment --comment "0002 Allow all established traffic" -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p tcp -m multiport --ports 22 -m comment --comment "0022 Allow all TCP on port 22 (ssh)" -j ACCEPT
-A INPUT -p tcp -m multiport --ports 80 -m comment --comment "0080 Allow HTTP" -j ACCEPT
-A INPUT -m comment --comment "9999 Drop all other traffic" -j DROP
COMMIT
# Completed on Sun Nov 9 01:46:38 2014
提示
Puppet Labs 的防火墙模块有一个内建的顺序概念,我们的所有防火墙资源标题都以数字开头。这是一个要求。该模块会尝试根据标题来排序资源。在命名防火墙资源时,您应该牢记这一点。
在下一节中,我们将使用我们的防火墙模块确保两个节点可以按要求进行通信。
使用 Heartbeat 构建高可用性服务
高可用服务是那些能够在单台机器或网络连接故障的情况下仍能继续运行的服务。高可用性的主要技术是冗余,也就是通过增加硬件来解决问题。虽然单台服务器最终会发生故障是不可避免的,但两台服务器同时发生故障的可能性非常小,这为大多数应用提供了良好的冗余水平。
构建冗余服务器对的最简单方法之一是让它们共享一个 IP 地址,通过 Heartbeat 实现。Heartbeat 是一个在两台机器上运行的守护进程,它们之间会定期交换信息——心跳。一个服务器是主服务器,通常拥有资源;在这种情况下,是 IP 地址(称为虚拟 IP,或 VIP)。如果从主服务器未能接收到心跳,辅助服务器可以接管该地址,从而确保服务的连续性。在实际场景中,你可能希望更多的机器参与 VIP,但在这个示例中,使用两台机器已经足够。
在这个食谱中,我们将使用 Puppet 设置这两台机器,并解释如何使用它提供高可用服务。
准备工作
当然,你需要两台机器,以及一个额外的 IP 地址来作为 VIP。通常,你可以向 ISP 请求这个地址(如果需要的话)。在这个示例中,我将使用名为cookbook和cookbook2的两台机器,其中cookbook是主机。我们将把这些主机添加到 heartbeat 配置中。
如何做……
按照以下步骤构建示例:
-
创建文件
modules/heartbeat/manifests/init.pp,内容如下:# Manage Heartbeat class heartbeat { package { 'heartbeat': ensure => installed, } service { 'heartbeat': ensure => running, enable => true, require => Package['heartbeat'], } file { '/etc/ha.d/authkeys': content => "auth 1\n1 sha1 TopSecret", mode => '0600', require => Package['heartbeat'], notify => Service['heartbeat'], } include myfw firewall {'0694 Allow UDP ha-cluster': proto => 'udp', port => 694, action => 'accept', } } -
创建文件
modules/heartbeat/manifests/vip.pp,内容如下:# Manage a specific VIP with Heartbeat class heartbeat::vip($node1,$node2,$ip1,$ip2,$vip,$interface='eth0:1') { include heartbeat file { '/etc/ha.d/haresources': content => "${node1} IPaddr::${vip}/${interface}\n", require => Package['heartbeat'], notify => Service['heartbeat'], } file { '/etc/ha.d/ha.cf': content => template('heartbeat/vip.ha.cf.erb'), require => Package['heartbeat'], notify => Service['heartbeat'], } } -
创建文件
modules/heartbeat/templates/vip.ha.cf.erb,内容如下:use_logd yes udpport 694 autojoin none ucast eth0 <%= @ip1 %> ucast eth0 <%= @ip2 %> keepalive 1 deadtime 10 warntime 5 auto_failback off node <%= @node1 %> node <%= @node2 %> -
按如下方式修改你的
site.pp文件。将ip1和ip2地址替换为你两台节点的主 IP 地址,将vip替换为你将使用的虚拟 IP 地址,并将node1和node2替换为两台节点的主机名。(Heartbeat 使用节点的完全限定域名来确定它是否是集群的一部分,因此node1和node2的值应与每台机器上facter fqdn给出的值匹配。)node cookbook,cookbook2 { class { 'heartbeat::vip': ip1 => '192.168.122.132', ip2 => '192.168.122.133', node1 => 'cookbook.example.com', node2 => 'cookbook2.example.com', vip => '192.168.122.200/24', } } -
在每台服务器上运行 Puppet:
[root@cookbook2 ~]# puppet agent -t Info: Retrieving pluginfacts Info: Retrieving plugin Info: Loading facts Info: Caching catalog for cookbook2.example.com Info: Applying configuration version '1415517914' Notice: /Stage[main]/Heartbeat/Package[heartbeat]/ensure: created Notice: /Stage[main]/Myfw::Pre/Firewall[0000 Allow all traffic on loopback]/ensure: created Notice: /Stage[main]/Myfw::Pre/Firewall[0001 Allow all ICMP]/ensure: created Notice: /File[/etc/sysconfig/iptables]/seluser: seluser changed 'unconfined_u' to 'system_u' Notice: /Stage[main]/Myfw::Pre/Firewall[0022 Allow all TCP on port 22 (ssh)]/ensure: created Notice: /Stage[main]/Heartbeat::Vip/File[/etc/ha.d/haresources]/ensure: defined content as '{md5}fb9f5d9d2b26e3bddf681676d8b2129c' Info: /Stage[main]/Heartbeat::Vip/File[/etc/ha.d/haresources]: Scheduling refresh of Service[heartbeat] Notice: /Stage[main]/Heartbeat::Vip/File[/etc/ha.d/ha.cf]/ensure: defined content as '{md5}84da22f7ac1a3629f69dcf29ccfd8592' Info: /Stage[main]/Heartbeat::Vip/File[/etc/ha.d/ha.cf]: Scheduling refresh of Service[heartbeat] Notice: /Stage[main]/Heartbeat/Service[heartbeat]/ensure: ensure changed 'stopped' to 'running' Info: /Stage[main]/Heartbeat/Service[heartbeat]: Unscheduling refresh on Service[heartbeat] Notice: /Stage[main]/Myfw::Pre/Firewall[0002 Allow all established traffic]/ensure: created Notice: /Stage[main]/Myfw::Post/Firewall[9999 Drop all other traffic]/ensure: created Notice: /Stage[main]/Heartbeat/Firewall[0694 Allow UDP ha-cluster]/ensure: created Notice: Finished catalog run in 12.64 seconds -
验证 VIP 是否正在某个节点上运行(此时它应该在 cookbook 上;注意,你需要使用
ip命令,ifconfig不会显示该地址):[root@cookbook ~]# ip addr show dev eth0 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 52:54:00:c9:d5:63 brd ff:ff:ff:ff:ff:ff inet 192.168.122.132/24 brd 192.168.122.255 scope global eth0 inet 192.168.122.200/24 brd 192.168.122.255 scope global secondary eth0:1 inet6 fe80::5054:ff:fec9:d563/64 scope link valid_lft forever preferred_lft forever -
如我们所见,cookbook 上激活了
eth0:1接口。如果你停止cookbook上的 heartbeat,cookbook2将创建eth0:1并接管:[root@cookbook2 ~]# ip a show dev eth0 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 52:54:00:ee:9c:fa brd ff:ff:ff:ff:ff:ff inet 192.168.122.133/24 brd 192.168.122.255 scope global eth0 inet 192.168.122.200/24 brd 192.168.122.255 scope global secondary eth0:1 inet6 fe80::5054:ff:feee:9cfa/64 scope link valid_lft forever preferred_lft forever
它是如何工作的……
我们首先需要安装 Heartbeat,使用heartbeat类:
# Manage Heartbeat
class heartbeat {
package { 'heartbeat':
ensure => installed,
}
...
}
接下来,我们使用heartbeat::vip类来管理特定的虚拟 IP:
# Manage a specific VIP with Heartbeat
class
heartbeat::vip($node1,$node2,$ip1,$ip2,$vip,$interface='eth0:1') {
include heartbeat
如你所见,该类包含一个interface参数;默认情况下,VIP 将配置在eth0:1上,但如果你需要使用不同的接口,可以通过此参数传入。
我们配置的每一对服务器都将使用相同参数的 heartbeat::vip 类,这些参数将用于构建 haresources 文件:
file { '/etc/ha.d/haresources':
content => "${node1} IPaddr::${vip}/${interface}\n",
notify => Service['heartbeat'],
require => Package['heartbeat'],
}
这告诉 Heartbeat 该管理的资源(这是一个 Heartbeat 资源,例如 IP 地址或服务,而不是 Puppet 资源)。生成的 haresources 文件可能如下所示:
cookbook.example.com IPaddr::192.168.122.200/24/eth0:1
该文件由 Heartbeat 按如下方式解释:
-
cookbook.example.com:这是主节点的名称,应该是资源的默认所有者 -
IPaddr:这是要管理的资源类型;在这种情况下,是一个 IP 地址 -
192.168.122.200/24:这是 IP 地址的值 -
eth0:1:这是要配置的虚拟接口,带有管理的 IP 地址
欲了解更多关于如何配置 Heartbeat 的信息,请访问高可用性网站 linux-ha.org/wiki/Heartbeat。
我们还将构建 ha.cf 文件,以告诉 Heartbeat 如何在集群节点之间通信:
file { '/etc/ha.d/ha.cf':
content => template('heartbeat/vip.ha.cf.erb'),
notify => Service['heartbeat'],
require => Package['heartbeat'],
}
为此,我们使用模板文件:
use_logd yes
udpport 694
autojoin none
ucast eth0 <%= @ip1 %>
ucast eth0 <%= @ip2 %>
keepalive 1
deadtime 10
warntime 5
auto_failback off
node <%= @node1 %>
node <%= @node2 %>
这里的关键值是两个节点的 IP 地址(ip1 和 ip2)以及两个节点的名称(node1 和 node2)。
最后,我们在两台机器上创建 heartbeat::vip 实例,并传递相同的参数集,如下所示:
class { 'heartbeat::vip':
ip1 => '192.168.122.132',
ip2 => '192.168.122.133',
node1 => 'cookbook.example.com',
node2 => 'cookbook2.example.com',
vip => '192.168.122.200/24',
}
还有更多内容...
按照示例中所描述的设置 Heartbeat,虚拟 IP 地址默认将配置在 cookbook 上。如果发生干扰(例如,如果你停止或重启 cookbook,或者停止 heartbeat 服务,或机器失去网络连接),cookbook2 将立即接管虚拟 IP。
ha.cf 中的 auto_failback 设置决定了接下来的行为。如果 auto_failback 设置为 on,当 cookbook 恢复可用时,它将自动接管 IP 地址。如果没有设置 auto_failback,IP 地址将保持当前位置,直到你手动使其故障(例如,通过停止 cookbook2 上的 heartbeat)。
Heartbeat 管理的虚拟 IP 的一个常见用途是提供高度可用的网站或服务。为此,您需要将服务的 DNS 名称(例如 cat-pictures.com)指向虚拟 IP。对该服务的请求将被路由到当前拥有虚拟 IP 的服务器。如果该服务器出现故障,请求将转发到另一台服务器,用户不会察觉到服务中断。
Heartbeat 在之前的示例中效果很好,但这种形式并不广泛使用。Heartbeat 仅在两节点集群中有效;对于 n 节点集群,应使用更新的 pacemaker 项目。有关 Heartbeat、pacemaker、corosync 及其他集群软件包的更多信息,请访问 www.linux-ha.org/wiki/Main_Page。
管理集群配置是导出资源有用的一方面。集群中的每个节点将导出有关自身的信息,然后其他集群成员可以收集这些信息。使用 puppetlabs-concat 模块,您可以使用来自集群中所有节点的导出 concat 碎片构建配置文件。
在开始自己的模块之前,记得查看 Forge。如果没有其他用途,你至少可以获得一些可以在自己模块中使用的想法。Corosync 可以通过 Puppet labs 模块进行管理,网址为 forge.puppetlabs.com/puppetlabs/corosync。
管理 NFS 服务器和文件共享
NFS(网络文件系统)是一种从远程服务器挂载共享目录的协议。例如,一组 Web 服务器可能会挂载相同的 NFS 共享,用于提供静态资源,如图片和样式表。尽管 NFS 通常比本地存储或集群文件系统更慢且不太安全,但它的易用性使其在数据中心中成为常见的选择。我们将使用之前的 myfw 模块来确保本地防火墙允许 nfs 通信。我们还将使用 Puppet 的 labs-concat 模块来编辑 nfs 服务器上导出的文件系统列表。
如何操作...
在这个例子中,我们将配置一个 nfs 服务器,通过 NFS 共享(导出)某些文件系统。
-
创建一个
nfs模块,包含以下nfs::exports类,它定义了一个 concat 资源:class nfs::exports { exec {'nfs::exportfs': command => 'exportfs -a', refreshonly => true, path => '/usr/bin:/bin:/sbin:/usr/sbin', } concat {'/etc/exports': notify => Exec['nfs::exportfs'], } } -
创建
nfs::export定义类型,我们将使用这个定义来创建任何nfs导出:define nfs::export ( $where = $title, $who = '*', $options = 'async,ro', $mount_options = 'defaults', $tag = 'nfs' ) { # make sure the directory exists # export the entry locally, then export a resource to be picked up later. file {"$where": ensure => 'directory', } include nfs::exports concat::fragment { "nfs::export::$where": content => "${where} ${who}(${options})\n", target => '/etc/exports' } @@mount { "nfs::export::${where}::${::ipaddress}": name => "$where", ensure => 'mounted', fstype => 'nfs', options => "$mount_options", device => "${::ipaddress}:${where}", tag => "$tag", } } -
现在创建
nfs::server类,其中将包括服务器的操作系统特定配置:class nfs::server { # ensure nfs server is running # firewall should allow nfs communication include nfs::exports case $::osfamily { 'RedHat': { include nfs::server::redhat } 'Debian': { include nfs::server::debian } } include myfw firewall {'2049 NFS TCP communication': proto => 'tcp', port => '2049', action => 'accept', } firewall {'2049 UDP NFS communication': proto => 'udp', port => '2049', action => 'accept', } firewall {'0111 TCP PORTMAP': proto => 'tcp', port => '111', action => 'accept', } firewall {'0111 UDP PORTMAP': proto => 'udp', port => '111', action => 'accept', } firewall {'4000 TCP STAT': proto => 'tcp', port => '4000-4010', action => 'accept', } firewall {'4000 UDP STAT': proto => 'udp', port => '4000-4010', action => 'accept', } } -
接下来,创建
nfs::server::redhat类:class nfs::server::redhat { package {'nfs-utils': ensure => 'installed', } service {'nfs': ensure => 'running', enable => true } file {'/etc/sysconfig/nfs': source => 'puppet:///modules/nfs/nfs', mode => 0644, notify => Service['nfs'], } } -
在我们的
nfs仓库的文件目录中为 RedHat 系统创建/etc/sysconfig/nfs支持文件(modules/nfs/files/nfs):STATD_PORT=4000 STATD_OUTGOING_PORT=4001 RQUOTAD_PORT=4002 LOCKD_TCPPORT=4003 LOCKD_UDPPORT=4003 MOUNTD_PORT=4004 -
现在为 Debian 系统创建支持类
nfs::server::debian:class nfs::server::debian { # install the package package {'nfs': name => 'nfs-kernel-server', ensure => 'installed', } # config file {'/etc/default/nfs-common': source => 'puppet:///modules/nfs/nfs-common', mode => 0644, notify => Service['nfs-common'] } # services service {'nfs-common': ensure => 'running', enable => true, } service {'nfs': name => 'nfs-kernel-server', ensure => 'running', enable => true, require => Package['nfs-kernel-server'] } } -
创建 Debian 的 nfs-common 配置(将放置在
modules/nfs/files/nfs-common中):STATDOPTS="--port 4000 --outgoing-port 4001" -
将
nfs::server类应用到一个节点,然后在该节点上创建一个导出:node debian { include nfs::server nfs::export {'/srv/home': tag => "srv_home" } } -
为前面代码片段中创建的
nfs::server类导出的资源创建一个收集器:node cookbook { Mount <<| tag == "srv_home" |>> { name => '/mnt', } } -
最后,在 Debian 节点上运行 Puppet 来创建导出的资源。然后,在 cookbook 节点上运行 Puppet 来挂载该资源:
root@debian:~# puppet agent -t Info: Caching catalog for debian.example.com Info: Applying configuration version '1415602532' Notice: Finished catalog run in 0.78 seconds [root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1415603580' Notice: /Stage[main]/Main/Node[cookbook]/Mount[nfs::export::/srv/home::192.168.122.148]/ensure: ensure changed 'ghost' to 'mounted' Info: Computing checksum on file /etc/fstab Info: /Stage[main]/Main/Node[cookbook]/Mount[nfs::export::/srv/home::192.168.122.148]: Scheduling refresh of Mount[nfs::export::/srv/home::192.168.122.148] Info: Mountnfs::export::/srv/home::192.168.122.148: Remounting Notice: /Stage[main]/Main/Node[cookbook]/Mount[nfs::export::/srv/home::192.168.122.148]: Triggered 'refresh' from 1 events Info: /Stage[main]/Main/Node[cookbook]/Mount[nfs::export::/srv/home::192.168.122.148]: Scheduling refresh of Mount[nfs::export::/srv/home::192.168.122.148] Notice: Finished catalog run in 0.34 seconds -
使用
mount验证挂载:[root@cookbook ~]# mount -t nfs 192.168.122.148:/srv/home on /mnt type nfs (rw)
它是如何工作的...
nfs::exports 类定义了一个 exec,它运行 'exportfs -a',以导出 /etc/exports 中定义的所有文件系统。接下来,我们定义一个 concat 资源来包含 concat::fragments,我们将在接下来的 nfs::export 类中定义它。Concat 资源指定碎片要放置的文件;在这个例子中是 /etc/exports。我们的 concat 资源有一个通知,用于执行上一个 exec。这样,每次更新 /etc/exports 时,我们都会重新运行 'exportfs -a' 来导出新的条目:
class nfs::exports {
exec {'nfs::exportfs':
command => 'exportfs -a',
refreshonly => true,
path => '/usr/bin:/bin:/sbin:/usr/sbin',
}
concat {'/etc/exports':
notify => Exec['nfs::exportfs'],
}
}
然后我们创建了一个nfs::export定义类型,完成所有工作。该定义类型通过concat::fragment资源向/etc/exports添加了一条条目:
define nfs::export (
$where = $title,
$who = '*',
$options = 'async,ro',
$mount_options = 'defaults',
$tag = 'nfs'
) {
# make sure the directory exists
# export the entry locally, then export a resource to be picked up later.
file {"$where":
ensure => 'directory',
}
include nfs::exports
concat::fragment { "nfs::export::$where":
content => "${where} ${who}(${options})\n",
target => '/etc/exports'
}
在定义中,我们使用$where属性来定义我们要导出的文件系统。我们使用$who来指定谁可以挂载该文件系统。$options属性包含导出选项,例如rw(读写)、ro(只读)。接下来,我们有将要放入客户端机器的/etc/fstab中的选项,挂载选项存储在$mount_options中。这里包括了nfs::exports类,以便concat::fragment有一个定义的拼接目标。
接下来,创建了导出的挂载资源;这是在服务器上完成的,因此${::ipaddress}变量保存了服务器的 IP 地址。我们用这个定义挂载的设备。设备由服务器的 IP 地址、一个冒号,然后是被导出的文件系统组成。在这个例子中,它是'192.168.122.148:/srv/home':
@@mount { "nfs::export::${where}::${::ipaddress}":
name => "$where",
ensure => 'mounted',
fstype => 'nfs',
options => "$mount_options",
device => "${::ipaddress}:${where}",
tag => "$tag",
}
我们重用了myfw模块并将其包含在nfs::server类中。这个类说明了编写模块时需要考虑的一个问题。并非所有 Linux 发行版都是相同的。Debian 和 RedHat 在处理 NFS 服务器配置时有很大的不同。nfs::server模块通过包含特定操作系统的子类来处理这个问题:
class nfs::server {
# ensure nfs server is running
# firewall should allow nfs communication
include nfs::exports
case $::osfamily {
'RedHat': { include nfs::server::redhat }
'Debian': { include nfs::server::debian }
}
include myfw
firewall {'2049 NFS TCP communication':
proto => 'tcp',
port => '2049',
action => 'accept',
}
firewall {'2049 UDP NFS communication':
proto => 'udp',
port => '2049',
action => 'accept',
}
firewall {'0111 TCP PORTMAP':
proto => 'tcp',
port => '111',
action => 'accept',
}
firewall {'0111 UDP PORTMAP':
proto => 'udp',
port => '111',
action => 'accept',
}
firewall {'4000 TCP STAT':
proto => 'tcp',
port => '4000-4010',
action => 'accept',
}
firewall {'4000 UDP STAT':
proto => 'udp',
port => '4000-4010',
action => 'accept',
}
}
nfs::server模块为 NFS 通信打开了多个防火墙端口。NFS 流量始终通过端口 2049 传输,但辅助系统,如锁定、配额和文件状态守护进程,默认情况下使用由端口映射器选择的临时端口。端口映射器本身使用端口 111。所以我们的模块需要允许 2049、111 和其他几个端口。我们尝试将辅助服务配置为使用 4000 到 4010 之间的端口。
在nfs::server::redhat类中,我们修改了/etc/sysconfig/nfs以使用指定的端口。我们还安装了 nfs-utils 包并启动了 nfs 服务:
class nfs::server::redhat {
package {'nfs-utils':
ensure => 'installed',
}
service {'nfs':
ensure => 'running',
enable => true
}
file {'/etc/sysconfig/nfs':
source => 'puppet:///modules/nfs/nfs',
mode => 0644,
notify => Service['nfs'],
}
}
我们在nfs::server::debian类中对 Debian 系统做了相同的操作。包和服务的名称不同,但总体过程类似:
class nfs::server::debian {
# install the package
package {'nfs':
name => 'nfs-kernel-server',
ensure => 'installed',
}
# config
file {'/etc/default/nfs-common':
source => 'puppet:///modules/nfs/nfs-common',
mode => 0644,
notify => Service['nfs-common']
}
# services
service {'nfs-common':
ensure => 'running',
enable => true,
}
service {'nfs':
name => 'nfs-kernel-server',
ensure => 'running',
enable => true,
}
}
一切就绪后,我们包括了服务器类来配置 NFS 服务器,然后定义一个导出:
include nfs::server
nfs::export {'/srv/home':
tag => "srv_home" }
这里重要的是我们定义了tag属性,该属性将在我们在以下代码片段中收集的导出资源中使用:
Mount <<| tag == "srv_home" |>> {
name => '/mnt',
}
我们使用飞船语法(<<| |>>)收集所有具有我们先前定义的标签(srv_home)的导出挂载资源。然后,我们使用一种叫做“收集时覆盖”的语法来修改挂载的名称属性,指定挂载文件系统的位置。
使用这种带有导出资源的设计模式,我们可以更改导出文件系统的服务器,并且任何挂载该资源的节点会自动更新。我们可以有多个不同的节点收集导出的挂载资源。
使用 HAProxy 对多个 Web 服务器进行负载均衡
负载均衡器用于在多个服务器之间分配负载。硬件负载均衡器仍然相对昂贵,而软件负载均衡器可以实现大部分硬件解决方案的优点。
HAProxy是大多数人首选的软件负载均衡器:快速、强大且高度可配置。
如何操作…
在本示例中,我将向你展示如何构建一个 HAProxy 服务器,用于在多个 Web 服务器之间负载均衡 Web 请求。我们将使用导出的资源来构建haproxy配置文件,就像我们为 NFS 示例所做的那样。
-
创建文件
modules/haproxy/manifests/master.pp,并将以下内容写入其中:class haproxy::master ($app = 'myapp') { # The HAProxy master server # will collect haproxy::slave resources and add to its balancer package { 'haproxy': ensure => installed } service { 'haproxy': ensure => running, enable => true, require => Package['haproxy'], } include haproxy::config concat::fragment { 'haproxy.cfg header': target => 'haproxy.cfg', source => 'puppet:///modules/haproxy/haproxy.cfg', order => '001', require => Package['haproxy'], notify => Service['haproxy'], } # pull in the exported entries Concat::Fragment <<| tag == "$app" |>> { target => 'haproxy.cfg', notify => Service['haproxy'], } } -
创建文件
modules/haproxy/files/haproxy.cfg,并将以下内容写入其中:global daemon user haproxy group haproxy pidfile /var/run/haproxy.pid defaults log global stats enable mode http option httplog option dontlognull option dontlog-normal retries 3 option redispatch timeout connect 4000 timeout client 60000 timeout server 30000 listen stats :8080 mode http stats uri / stats auth haproxy:topsecret listen myapp 0.0.0.0:80 balance leastconn -
修改你的
manifests/nodes.pp文件,内容如下:node 'cookbook' { include haproxy } -
在
haproxy::slave类中创建从服务器配置:class haproxy::slave ($app = "myapp", $localport = 8000) { # haproxy slave, export haproxy.cfg fragment # configure simple web server on different port @@concat::fragment { "haproxy.cfg $::fqdn": content => "\t\tserver ${::hostname} ${::ipaddress}:${localport} check maxconn 100\n", order => '0010', tag => "$app", } include myfw firewall {"${localport} Allow HTTP to haproxy::slave": proto => 'tcp', port => $localport, action => 'accept', } class {'apache': } apache::vhost { 'haproxy.example.com': port => '8000', docroot => '/var/www/haproxy', } file {'/var/www/haproxy': ensure => 'directory', mode => 0755, require => Class['apache'], } file {'/var/www/haproxy/index.html': mode => '0644', content => "<html><body><h1>${::fqdn} haproxy::slave\n</body></html>\n", require => File['/var/www/haproxy'], } } -
在
haproxy::config类中创建concat容器资源,如下所示:class haproxy::config { concat {'haproxy.cfg': path => '/etc/haproxy/haproxy.cfg', order => 'numeric', mode => '0644', } } -
修改
site.pp以定义主节点和从节点:node master { class {'haproxy::master': app => 'cookbook' } } node slave1,slave2 { class {'haproxy::slave': app => 'cookbook' } } -
在每个从节点上运行 Puppet:
root@slave1:~# puppet agent -t Info: Caching catalog for slave1 Info: Applying configuration version '1415646194' Notice: /Stage[main]/Haproxy::Slave/Apache::Vhost[haproxy.example.com]/File[25-haproxy.example.com.conf]/ensure: created Info: /Stage[main]/Haproxy::Slave/Apache::Vhost[haproxy.example.com]/File[25-haproxy.example.com.conf]: Scheduling refresh of Service[httpd] Notice: /Stage[main]/Haproxy::Slave/Apache::Vhost[haproxy.example.com]/File[25-haproxy.example.com.conf symlink]/ensure: created Info: /Stage[main]/Haproxy::Slave/Apache::Vhost[haproxy.example.com]/File[25-haproxy.example.com.conf symlink]: Scheduling refresh of Service[httpd] Notice: /Stage[main]/Apache::Service/Service[httpd]/ensure: ensure changed 'stopped' to 'running' Info: /Stage[main]/Apache::Service/Service[httpd]: Unscheduling refresh on Service[httpd] Notice: Finished catalog run in 1.71 seconds -
在主节点上运行 Puppet 来配置并运行
haproxy:[root@master ~]# puppet agent -t Info: Caching catalog for master.example.com Info: Applying configuration version '1415647075' Notice: /Stage[main]/Haproxy::Master/Package[haproxy]/ensure: created Notice: /Stage[main]/Myfw::Pre/Firewall[0000 Allow all traffic on loopback]/ensure: created Notice: /Stage[main]/Myfw::Pre/Firewall[0001 Allow all ICMP]/ensure: created Notice: /Stage[main]/Haproxy::Master/Firewall[8080 haproxy statistics]/ensure: created Notice: /File[/etc/sysconfig/iptables]/seluser: seluser changed 'unconfined_u' to 'system_u' Notice: /Stage[main]/Myfw::Pre/Firewall[0022 Allow all TCP on port 22 (ssh)]/ensure: created Notice: /Stage[main]/Haproxy::Master/Firewall[0080 http haproxy]/ensure: created Notice: /Stage[main]/Myfw::Pre/Firewall[0002 Allow all established traffic]/ensure: created Notice: /Stage[main]/Myfw::Post/Firewall[9999 Drop all other traffic]/ensure: created Notice: /Stage[main]/Haproxy::Config/Concat[haproxy.cfg]/File[haproxy.cfg]/content: ... +listen myapp 0.0.0.0:80 + balance leastconn + server slave1 192.168.122.148:8000 check maxconn 100 + server slave2 192.168.122.133:8000 check maxconn 100 Info: Computing checksum on file /etc/haproxy/haproxy.cfg Info: /Stage[main]/Haproxy::Config/Concat[haproxy.cfg]/File[haproxy.cfg]: Filebucketed /etc/haproxy/haproxy.cfg to puppet with sum 1f337186b0e1ba5ee82760cb437fb810 Notice: /Stage[main]/Haproxy::Config/Concat[haproxy.cfg]/File[haproxy.cfg]/content: content changed '{md5}1f337186b0e1ba5ee82760cb437fb810' to '{md5}b070f076e1e691e053d6853f7d966394' Notice: /Stage[main]/Haproxy::Master/Service[haproxy]/ensure: ensure changed 'stopped' to 'running' Info: /Stage[main]/Haproxy::Master/Service[haproxy]: Unscheduling refresh on Service[haproxy] Notice: Finished catalog run in 33.48 seconds -
在 Web 浏览器中检查主服务器端口
8080上的 HAProxy 统计界面(http://master.example.com:8080),确保一切正常(用户名和密码分别为haproxy.cfg、haproxy和topsecret)。还可以尝试访问被代理的服务。请注意,每次重新加载页面时,页面内容会发生变化,因为服务会从 slave1 被重定向到 slave2(http://master.example.com)。
它是如何工作的…
我们从前面几个部分的各种组件构建了一个复杂的配置。做得越多,这种类型的部署就越容易。总体而言,我们配置了主节点来收集从节点导出的资源。从节点将其配置信息导出,以便haproxy可以在负载均衡器中使用它们。随着从节点的增加,它们可以导出资源并自动添加到负载均衡器中。
我们使用myfw模块配置了从节点和主节点上的防火墙,以允许通信。
我们使用了 Forge Apache 模块来配置从服务器上的 Web 服务器监听。我们只用五行代码就生成了一个完全功能的网站(再加上 10 行代码将index.html放到网站上)。
这里涉及几个方面。除了haproxy配置外,我们还进行了防火墙配置和 Apache 配置。我们将重点关注导出资源和haproxy配置是如何结合在一起的。
在haproxy::config类中,我们为haproxy配置创建了concat容器:
class haproxy::config {
concat {'haproxy.cfg':
path => '/etc/haproxy/haproxy.cfg',
order => 'numeric',
mode => 0644,
}
}
我们在haproxy::slave中引用了这个:
class haproxy::slave ($app = "myapp", $localport = 8000) {
# haproxy slave, export haproxy.cfg fragment
# configure simple web server on different port
@@concat::fragment { "haproxy.cfg $::fqdn":
content => "\t\tserver ${::hostname} ${::ipaddress}:${localport} check maxconn 100\n",
order => '0010',
tag => "$app",
}
我们在这里使用了一个小技巧,使用concat时没有在导出资源中定义目标。如果我们定义了,所有从节点都会尝试创建一个/etc/haproxy/haproxy.cfg文件,但从节点没有安装haproxy,因此会导致目录失败。我们做的是在haproxy::master中收集资源时修改它:
# pull in the exported entries
Concat::Fragment <<| tag == "$app" |>> {
target => 'haproxy.cfg',
notify => Service['haproxy'],
}
除了在收集资源时添加目标外,我们还添加了一个通知,确保在将新主机添加到配置时重新启动 haproxy 服务。另一个重要点是,我们将从属配置的顺序属性设置为 0010,当我们定义 haproxy.cfg 文件的头部时,我们使用顺序值 0001 来确保头部位于文件的开头:
concat::fragment { 'haproxy.cfg header':
target => 'haproxy.cfg',
source => 'puppet:///modules/haproxy/haproxy.cfg',
order => '001',
require => Package['haproxy'],
notify => Service['haproxy'],
}
剩下的 haproxy::master 类负责配置防火墙,方法与之前的示例相同。
还有更多内容...
HAProxy 有非常广泛的配置参数,你可以探索这些参数;请查看 HAProxy 网站 haproxy.1wt.eu/#docs。
尽管 HAProxy 最常用于 Web 服务器,但它不仅可以代理 HTTP,还能代理更多类型的流量。它可以处理任何类型的 TCP 流量,因此你可以用它来平衡 MySQL 服务器、SMTP、视频服务器或任何你想要的服务负载。
我们可以使用我们展示的设计来解决多个服务器之间服务协调的问题。这类交互非常常见;你可以将它应用于负载均衡或分布式系统的许多配置。你可以使用之前描述的相同工作流程,让节点导出防火墙资源(@@firewall)以允许其自身访问。
使用 Puppet 管理 Docker
Docker 是一个用于快速部署容器的平台。容器类似于轻量级的虚拟机,可能只运行一个进程。Docker 中的容器称为 Dock,并且通过称为 Dockerfile 的文件来配置。Puppet 可以用于配置一个节点,不仅使其运行 Docker,还可以配置和启动多个 Dock。然后,你可以使用 Puppet 来确保你的 Dock 正在运行并且配置一致。
准备工作
从 Forge 下载并安装 Puppet Docker 模块(forge.puppetlabs.com/garethr/docker):
t@mylaptop ~ $ cd puppet
t@mylaptop ~/puppet $ puppet module install -i modules garethr-docker
Notice: Preparing to install into /home/thomas/puppet/modules ...
Notice: Downloading from https://forgeapi.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/home/thomas/puppet/modules
└─┬ garethr-docker (v3.3.0)
├── puppetlabs-apt (v1.7.0)
├── puppetlabs-stdlib (v4.3.2)
└── stahnma-epel (v1.0.2)
将这些模块添加到你的 Puppet 仓库中。stahnma-epel 模块是企业版 Linux 发行版所必需的,它包含了企业版 Linux YUM 仓库中的额外包。
如何操作...
执行以下步骤来使用 Puppet 管理 Docker:
-
要在节点上安装 Docker,我们只需包含
docker类。我们做的不仅仅是安装 Docker,还会下载一个镜像并在我们的测试节点上启动一个应用程序。在本示例中,我们将创建一个名为shipyard的新机器。将以下节点定义添加到site.pp:node shipyard { class {'docker': } docker::image {'phusion/baseimage': } docker::run {'cookbook': image => 'phusion/baseimage', expose => '8080', ports => '8080', command => 'nc -k -l 8080', } } -
在你的 Shipyard 节点上运行 Puppet 来安装 Docker。这也会下载
phusion/baseimage docker镜像:[root@shipyard ~]# puppet agent -t Info: Retrieving pluginfacts Info: Retrieving plugin Info: Loading facts Info: Caching catalog for shipyard Info: Applying configuration version '1421049252' Notice: /Stage[main]/Epel/File[/etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-6]/ensure: defined content as '{md5}d865e6b948a74cb03bc3401c0b01b785' Notice: /Stage[main]/Epel/Epel::Rpm_gpg_key[EPEL-6]/Exec[import-EPEL-6]/returns: executed successfully ... Notice: /Stage[main]/Docker::Install/Package[docker]/ensure: created ... Notice: /Stage[main]/Main/Node[shipyard]/Docker::Run[cookbook]/File[/etc/init.d/docker-cookbook]/ensure: created Info: /Stage[main]/Main/Node[shipyard]/Docker::Run[cookbook]/File[/etc/init.d/docker-cookbook]: Scheduling refresh of Service[docker-cookbook] Notice: /Stage[main]/Main/Node[shipyard]/Docker::Run[cookbook]/Service[docker-cookbook]: Triggered 'refresh' from 1 events -
使用
docker ps验证容器是否在 Shipyard 上运行:[root@shipyard ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f6f5b799a598 phusion/baseimage:0.9.15 "/bin/nc -l 8080" About a minute ago Up About a minute 0.0.0.0:49157->8080/tcp suspicious_hawking -
使用
docker ps验证 Dock 是否在 8080 端口上运行 netcat,通过连接之前列出的端口(49157):[root@shipyard ~]# nc -v localhost 49157 Connection to localhost 49157 port [tcp/*] succeeded!
它是如何工作的...
我们首先通过 Forge 安装了 docker 模块。这个模块会在我们的节点上安装 docker-io 包,并处理所有必要的依赖关系。
然后,我们定义了一个 docker::image 资源。这指示 Puppet 确保指定的镜像已被下载并且可以供 Docker 使用。在第一次运行时,Puppet 会让 Docker 下载该镜像。我们使用了 phusion/baseimage 作为示例,因为它非常小巧、知名,并且包含了我们在示例中使用的 netcat 守护进程。关于 baseimage 的更多信息,请访问 phusion.github.io/baseimage-docker/。
接着,我们定义了一个 docker::run 资源。这个示例并不是特别有用,它只是简单地在 8080 端口启动了 netcat 的监听模式。我们需要将该端口暴露到我们的机器上,因此我们定义了 docker::run 资源的 expose 属性。docker::run 资源有很多其他可用选项,更多详情请参考源代码。
接着,我们使用 docker ps 命令列出了我们船厂机器上运行的容器。我们提取了本地机器上监听的端口,并验证了 netcat 是否在监听。
还有更多内容...
Docker 是一个非常适合快速部署和开发的工具。即使在最简单的硬件上,你也可以启动任意数量的容器。Docker 的一个很好的用途是让容器作为你模块的测试节点。你可以创建一个包含 Puppet 的 Docker 镜像,然后让 Puppet 在容器内运行。欲了解更多关于 Docker 的信息,请访问 www.docker.com/。
第九章:外部工具与 Puppet 生态系统
| “尽管如此,您可以随时偏离道路。道路的真正用途正是:到达个别选择的出发点。” | ||
|---|---|---|
| --Robert Bringhurst,《排版风格元素》 |
在本章中,我们将覆盖以下内容:
-
创建自定义事实
-
添加外部事实
-
将事实设置为环境变量
-
使用 Puppet 资源命令生成清单
-
使用其他工具生成清单
-
使用外部节点分类器
-
创建您自己的资源类型
-
创建您自己的提供者
-
创建自定义函数
-
使用 rspec-puppet 测试您的 Puppet 清单
-
使用 librarian-puppet
-
使用 r10k
介绍
Puppet 本身是一个非常有用的工具,但如果将 Puppet 与其他工具和框架结合使用,您可以获得更大的收益。我们将讨论一些将数据输入 Puppet 的方法,包括自定义 Facter 事实、外部事实以及从现有配置自动生成 Puppet 清单的工具。
您还将学习如何通过创建自定义函数、资源类型和提供者来扩展 Puppet;如何使用外部节点分类器脚本将 Puppet 与您基础设施的其他部分集成;以及如何使用 rspec-puppet 测试您的代码。
创建自定义事实
虽然 Facter 内置的事实很有用,但实际上添加您自己的事实非常简单。例如,如果您有位于不同数据中心或托管服务商的机器,您可以为此添加自定义事实,这样 Puppet 就可以判断是否需要应用任何本地设置(例如,本地 DNS 服务器或网络路由)。
如何操作...
这是一个简单的自定义事实示例:
-
创建目录
modules/facts/lib/facter,然后创建文件modules/facts/lib/facter/hello.rb,并填入以下内容:Facter.add(:hello) do setcode do "Hello, world" end end -
修改您的
site.pp文件如下:node 'cookbook' { notify { $::hello: } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Notice: /File[/var/lib/puppet/lib/facter/hello.rb]/ensure: defined content as '{md5}f66d5e290459388c5ffb3694dd22388b' Info: Loading facts Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1416205745' Notice: Hello, world Notice: /Stage[main]/Main/Node[cookbook]/Notify[Hello, world]/message: defined 'message' as 'Hello, world' Notice: Finished catalog run in 0.53 seconds
它是如何工作的...
Facter 事实在与 facter 一起分发的 Ruby 文件中定义。Puppet 可以通过在模块的 lib/facter 子目录中创建文件,向 facter 添加附加事实。然后,这些文件会像我们之前看到的 puppetlabs-stdlib 模块一样被传输到客户端节点。要让命令行工具 facter 使用这些 puppet 事实,请像以下命令行所示一样在 facter 后添加 -p 选项:
[root@cookbook ~]# facter hello
[root@cookbook ~]# facter -p hello
Hello, world
提示
如果您使用的是旧版本的 Puppet(低于 3.0),您需要在 puppet.conf 文件中启用 pluginsync,如下所示命令行:
[main]
pluginsync = true
事实可以包含任何 Ruby 代码,并且在 setcode do ... end 块中评估的最后一个值将是该事实返回的值。例如,您可以创建一个更有用的事实,它返回当前登录系统的用户数量:
Facter.add(:users) do
setcode do
%x{/usr/bin/who |wc -l}.chomp
end
end
要在清单中引用事实,只需像内置事实一样使用其名称:
notify { "${::users} users logged in": }
Notice: 2 users logged in
你可以向任何 Puppet 模块添加自定义事实。在创建多个模块会使用到的事实时,将它们放置在事实模块中可能更有意义。在大多数情况下,自定义事实与特定模块相关,应将其放置在该模块中。
还有更多……
持有事实定义的 Ruby 文件的名称并不重要。你可以随意命名这个文件;事实的名称来自于Facter.add()函数的调用。你也可以在单个 Ruby 文件中多次调用该函数,根据需要定义多个事实。例如,你可以grep /proc/meminfo文件,并根据内存信息返回多个事实,如以下代码片段中的meminfo.rb文件所示:
File.open('/proc/meminfo') do |f|
f.each_line { |line|
if (line[/^Active:/])
Facter.add(:memory_active) do
setcode do line.split(':')[1].to_i
end
end
end
if (line[/^Inactive:/])
Facter.add(:memory_inactive) do
setcode do line.split(':')[1].to_i
end
end
end
}
end
将此文件同步到节点后,memory_active和memory_inactive事实将如下面所示可用:
[root@cookbook ~]# facter -p |grep memory_
memory_active => 63780
memory_inactive => 58188
你可以扩展事实的使用,构建一个完全没有节点的 Puppet 配置;换句话说,Puppet 可以仅根据事实的结果决定应用哪些资源到机器上。Jordan Sissel 在www.semicomplete.com/blog/geekery/puppet-nodeless-configuration.html上写过关于这种方法的文章。
你可以在 Puppetlabs 网站上了解更多关于自定义事实的内容,包括如何确保操作系统特定的事实仅在相关系统上工作,以及如何加权事实,以确保它们按照特定顺序进行评估:
docs.puppetlabs.com/guides/custom_facts.html
另请参见
-
在第三章的导入动态信息操作说明中,编写更好的清单。
-
在第二章的配置 Hiera操作说明中,Puppet 基础设施。
添加外部事实
创建自定义事实的操作说明描述了如何添加用 Ruby 编写的额外事实。你也可以选择从简单的文本文件或脚本中创建事实,使用外部事实替代。
外部事实存放在/etc/facter/facts.d目录中,并具有简单的key=value格式,如下所示:
message="Hello, world"
准备工作
这是你需要做的准备工作,以便为系统添加外部事实:
-
你需要 Facter 版本 1.7 或更高版本才能使用外部事实,因此可以查找
facterversion的值,或使用facter -v命令:[root@cookbook ~]# facter facterversion 2.3.0 [root@cookbook ~]# facter -v 2.3.0 -
你还需要创建外部事实目录,使用以下命令:
[root@cookbook ~]# mkdir -p /etc/facter/facts.d
如何做到……
在这个示例中,我们将创建一个简单的外部事实,返回一条信息,如创建自定义事实操作说明所示:
-
创建文件
/etc/facter/facts.d/local.txt,并加入以下内容:model=ED-209 -
运行以下命令:
[root@cookbook ~]# facter model ED-209好了,这很简单!你当然可以像下面这样,向相同的文件或其他文件添加更多的事实:
model=ED-209 builder=OCP directives=4然而,如果你需要以某种方式计算事实,例如,计算已登录用户的数量怎么办?你可以创建可执行的事实来完成这个任务。
-
创建文件
/etc/facter/facts.d/users.sh,并包含以下内容:#!/bin/sh echo users=`who |wc -l` -
使用以下命令使这个文件变为可执行:
[root@cookbook ~]# chmod a+x /etc/facter/facts.d/users.sh -
现在使用以下命令检查
users值:[root@cookbook ~]# facter users 2
它是如何工作的...
在这个例子中,我们将通过在节点上创建文件来创建一个外部事实。我们还将展示如何覆盖之前定义的事实。
-
当前版本的 Facter 会查找
/etc/facter/facts.d目录下的.txt、.json或.yaml类型的文件。如果 Facter 找到文本文件,它会解析文件中的key=value对,并将键作为一个新的事实:[root@cookbook ~]# facter model ED-209 -
如果文件是 YAML 或 JSON 格式,Facter 会按照相应的格式解析文件中的
key=value对。例如,对于 YAML 文件:--- registry: NCC-68814 class: Andromeda shipname: USS Prokofiev -
结果输出如下:
[root@cookbook ~]# facter registry class shipname class => Andromeda registry => NCC-68814 shipname => USS Prokofiev -
对于可执行文件,Facter 会假设它们的输出是
key=value对的列表。它将执行facts.d目录中的所有文件,并将它们的输出添加到内部事实哈希中。提示
在 Windows 中,批处理文件或 PowerShell 脚本可以像在 Linux 中使用可执行脚本一样使用。
-
在
users示例中,Facter 将执行users.sh脚本,生成以下输出:users=2 -
然后,它会在这个输出中搜索
users并返回匹配的值:[root@cookbook ~]# facter users 2 -
如果为您指定的键有多个匹配项,Facter 会根据权重属性决定返回哪个事实。在我使用的版本中,外部事实的权重为 10,000(在
facter/util/directory_loader.rb中定义为EXTERNAL_FACT_WEIGHT)。这个高权重值是为了确保您定义的事实能够覆盖已提供的事实。例如:[root@cookbook ~]# facter architecture x86_64 [root@cookbook ~]# echo "architecture=ppc64">>/etc/facter/facts.d/myfacts.txt [root@cookbook ~]# facter architecture ppc64
还有更多...
由于所有外部事实的权重为 10,000,它们在/etc/facter/facts.d目录中被解析的顺序决定了它们的优先级(最后被遇到的事实具有最高优先级)。要创建一个优先于其他事实的事实,您需要将它创建在按字母顺序最后的文件中:
[root@cookbook ~]# facter architecture
ppc64
[root@cookbook ~]# echo "architecture=r10000" >>/etc/facter/facts.d/z-architecture.txt
[root@cookbook ~]# facter architecture
r10000
调试外部事实
如果你在让 Facter 识别外部事实时遇到问题,可以以调试模式运行 Facter,看看发生了什么:
ubuntu@cookbook:~/puppet$ facter -d robin
Fact file /etc/facter/facts.d/myfacts.json was parsed but returned an empty data set
X JSON 文件被解析,但返回了一个空数据集错误,这意味着 Facter 没有在文件中找到任何key=value对,或者(在可执行事实的情况下)在其输出中没有找到。
注意
请注意,如果您有外部事实存在,Facter 每次查询时都会解析或运行/etc/facter/facts.d目录中的所有事实。如果其中一些脚本运行时间较长,这可能会显著拖慢任何使用 Facter 的程序(使用--iming开关运行 Facter 以排除故障)。除非某个特定的事实每次查询时都需要重新计算,否则考虑使用 cron 作业定期计算该事实并将结果写入 Facter 目录中的文本文件。
在 Puppet 中使用外部事实
你创建的任何外部事实都将同时对 Facter 和 Puppet 可用。要在 Puppet 清单中引用外部事实,只需像使用内建或自定义事实一样使用事实名称:
notify { "There are $::users people logged in right now.": }
除非你特别尝试覆盖已定义的事实,否则应避免使用预定义事实的名称。
另见
-
第三章中的导入动态信息食谱,编写更好的清单
-
第二章中的配置 Hiera食谱,Puppet 基础设施
-
本章中的创建自定义事实食谱
将事实设置为环境变量
另一种将信息传递给 Puppet 和 Facter 的便捷方式是使用环境变量。任何以FACTER_开头的环境变量都会被解释为一个事实。例如,使用以下命令询问 facter hello 的值:
[root@cookbook ~]# facter -p hello
Hello, world
现在通过环境变量覆盖值,并重新询问:
[root@cookbook ~]# FACTER_hello='Howdy!' facter -p hello
Howdy!
它同样适用于 Puppet,因此让我们通过一个示例来操作。
如何操作...
在这个示例中,我们将使用环境变量设置一个事实:
-
保持 cookbook 的节点定义与我们上一个示例相同:
node cookbook { notify {"$::hello": } } -
运行以下命令:
[root@cookbook ~]# FACTER_hello="Hallo Welt" puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1416212026' Notice: Hallo Welt Notice: /Stage[main]/Main/Node[cookbook]/Notify[Hallo Welt]/message: defined 'message' as 'Hallo Welt' Notice: Finished catalog run in 0.27 seconds
使用 Puppet 资源命令生成清单
如果你有一台已经配置好的服务器,或者几乎配置完成,你可以将该配置捕获为 Puppet 清单。Puppet 资源命令会从系统的现有配置生成 Puppet 清单。例如,你可以使用puppet resource生成一个清单,创建系统中所有找到的用户。这对于拍摄一个正常工作的系统快照并将其配置快速导入 Puppet 非常有用。
如何操作...
下面是使用puppet resource从运行中的系统获取数据的一些示例:
-
要为特定用户生成清单,运行以下命令:
[root@cookbook ~]# puppet resource user thomas user { 'thomas': ensure => 'present', comment => 'thomas Admin User', gid => '1001', groups => ['bin', 'wheel'], home => '/home/thomas', password => '!!', password_max_age => '99999', password_min_age => '0', shell => '/bin/bash', uid => '1001', } -
对于某个特定的服务,运行以下命令:
[root@cookbook ~]# puppet resource service sshd service { 'sshd': ensure => 'running', enable => 'true', } -
对于包,运行以下命令:
[root@cookbook ~]# puppet resource package kernel package { 'kernel': ensure => '2.6.32-431.23.3.el6', }
还有更多...
你可以使用puppet resource来检查 Puppet 中可用的每种资源类型。在前面的示例中,我们为资源类型的特定实例生成了一个清单,但你也可以使用puppet resource来输出所有该资源类型的实例:
[root@cookbook ~]# puppet resource service
service { 'abrt-ccpp':
ensure => 'running',
enable => 'true',
}
service { 'abrt-oops':
ensure => 'running',
enable => 'true',
}
service { 'abrtd':
ensure => 'running',
enable => 'true',
}
service { 'acpid':
ensure => 'running',
enable => 'true',
}
service { 'atd':
ensure => 'running',
enable => 'true',
}
service { 'auditd':
ensure => 'running',
enable => 'true',
}
这将输出系统中每个服务的状态;这是因为每个服务都是可枚举的资源。当你尝试对一个不可枚举的资源执行相同命令时,会收到错误信息:
[root@cookbook ~]# puppet resource file
Error: Could not run: Listing all file instances is not supported. Please specify a file or directory, e.g. puppet resource file /etc
让 Puppet 描述系统中的每个文件是行不通的;这最好交给像tripwire这样的审计工具(这是一个设计用来查找系统中每个文件变化的工具,www.tripwire.com)。
使用其他工具生成清单
如果你想快速捕获一个正在运行的系统的完整配置作为 Puppet 清单,可以使用一些工具来帮助你。在这个例子中,我们将使用 Blueprint,它旨在检查一台机器并将其状态输出为 Puppet 代码。
准备工作
下面是你需要做的准备工作,以便在系统中使用 Blueprint。
运行以下命令以安装 Blueprint;我们将使用 puppet resource 来更改 python-pip 包的状态:
[root@cookbook ~]# puppet resource package python-pip ensure=installed
Notice: /Package[python-pip]/ensure: created
package { 'python-pip':
ensure => '1.3.1-4.el6',
}
[root@cookbook ~]# pip install blueprint
Downloading/unpacking blueprint
Downloading blueprint-3.4.2.tar.gz (59kB): 59kB downloaded
Running setup.py egg_info for package blueprint
Installing collected packages: blueprint
Running setup.py install for blueprint
changing mode of build/scripts-2.6/blueprint from 644 to 755
...
Successfully installed blueprint
Cleaning up...
提示
如果你的食谱节点上尚未安装 Git,你可能需要先安装它。
如何操作...
这些步骤将向你展示如何运行 Blueprint:
-
运行以下命令:
[root@cookbook ~]# mkdir blueprint && cd blueprint [root@cookbook blueprint]# blueprint create -P blueprint_test # [blueprint] searching for APT packages to exclude # [blueprint] searching for Yum packages to exclude # [blueprint] caching excluded Yum packages # [blueprint] parsing blueprintignore(5) rules # [blueprint] searching for npm packages # [blueprint] searching for configuration files # [blueprint] searching for APT packages # [blueprint] searching for PEAR/PECL packages # [blueprint] searching for Python packages # [blueprint] searching for Ruby gems # [blueprint] searching for software built from source # [blueprint] searching for Yum packages # [blueprint] searching for service dependencies blueprint_test/manifests/init.pp -
阅读
blueprint_test/manifests/init.pp文件以查看生成的代码:# # Automatically generated by blueprint(7). Edit at your own risk. # class blueprint_test { Exec { path => '/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin', } Class['sources'] -> Class['files'] -> Class['packages'] class files { file { '/etc': ensure => directory; '/etc/aliases.db': content => template('blueprint_test/etc/aliases.db'), ensure => file, group => root, mode => 0644, owner => root; '/etc/audit': ensure => directory; '/etc/audit/audit.rules': content => template('blueprint_test/etc/audit/audit.rules'), ensure => file, group => root, mode => 0640, owner => root; '/etc/blkid': ensure => directory; '/etc/cron.hourly': ensure => directory; '/etc/cron.hourly/run-backup': content => template('blueprint_test/etc/cron.hourly/run-backup'), ensure => file, group => root, mode => 0755, owner => root; '/etc/crypttab': content => template('blueprint_test/etc/crypttab'), ensure => file, group => root, mode => 0644, owner => root;
还有更多...
Blueprint 只是对当前系统进行快照;它不会做出智能的决策,并且 Blueprint 会捕获系统上的所有文件和所有软件包。它将生成一个可能比你实际需要的配置要大的配置。例如,当配置一个服务器时,你可能会指定要安装 Apache 包。Apache 包的依赖项将自动安装,而你不需要指定它们。当使用像 Blueprint 这样的工具生成配置时,你将捕获所有这些依赖项,并锁定当前安装在你系统上的版本。从我们生成的 Blueprint 代码来看,我们可以看到确实如此:
class yum {
package {
'GeoIP':
ensure => '1.5.1-5.el6.x86_64';
'PyXML':
ensure => '0.8.4-19.el6.x86_64';
'SDL':
ensure => '1.2.14-3.el6.x86_64';
'apr':
ensure => '1.3.9-5.el6_2.x86_64';
'apr-util':
ensure => '1.3.9-3.el6_0.1.x86_64';
如果你自己创建这个清单,你可能会指定ensure => installed,而不是具体的版本号。
包安装默认版本的文件。Blueprint 并没有意识到这一点,会将所有文件添加到清单中,甚至那些没有变化的文件。默认情况下,Blueprint 会毫不分辨地捕获 /etc 中的所有文件作为文件资源。
Blueprint 和类似的工具通常有很小的应用场景,但它们可以帮助你熟悉 Puppet 语法,并给你一些关于如何指定自己清单的思路。然而,我并不建议盲目地使用这个工具来创建系统。
良好的配置管理没有捷径,那些希望通过复制和粘贴别人的代码(如公开模块)来节省时间和精力的人,可能会发现这既不能节省时间,也不能减少精力。
使用外部节点分类器
当 Puppet 在节点上运行时,它需要知道应该将哪些类应用于该节点。例如,如果它是一个 Web 服务器节点,可能需要包含一个 apache 类。将节点映射到类的正常方式是在 Puppet 清单本身中,例如,在你的 site.pp 文件中:
node 'web1' {
include apache
}
或者,你可以使用外部节点分类器(ENC)来完成这项工作。ENC 是任何可执行程序,它可以接受完全限定域名(FQDN)作为第一个命令行参数($1)。该脚本应返回一个类、参数以及一个可选的环境列表,用于应用到节点。输出应该是标准的 YAML 格式。在使用 ENC 时,你应该记住,通过标准site.pp清单应用的类将与 ENC 提供的类合并。
注意
ENC 返回的参数可作为顶级作用域变量提供给节点。
ENC 可以是一个简单的 Shell 脚本,或者是一个更复杂的程序或 API 的包装器,能够决定如何将节点映射到类。Puppet 企业版和 The Foreman 提供的 ENC(theforeman.org/)都是简单的脚本,它们连接到各自系统的 Web API。
在这个示例中,我们将构建最简单的 ENC,一个 Shell 脚本,简单地打印出包含的类列表。我们将从包含一个enc类开始,该类定义了notify,它将打印出一个顶级作用域变量$enc。
准备工作
我们将从创建我们的enc类开始,并与enc脚本一起使用:
-
运行以下命令:
t@mylaptop ~/puppet $ mkdir -p modules/enc/manifests -
创建文件
modules/enc/manifests/init.pp,内容如下:class enc { notify {"We defined this from $enc": } }
如何操作...
这是如何构建一个简单的外部节点分类器的步骤。我们将在我们的 Puppet 主服务器上执行所有这些步骤。如果你是在无主模式下运行,请在节点上执行这些步骤:
-
创建文件
/etc/puppet/cookbook.sh,内容如下:#!/bin/bash cat <<EOF --- classes: enc: parameters: enc: $0 EOF -
运行以下命令:
root@puppet:/etc/puppet# chmod a+x cookbook.sh -
按照以下方式修改你的
/etc/puppet/puppet.conf文件:[main] node_terminus = exec external_nodes = /etc/puppet/cookbook.sh -
重启 Apache(重启主服务器)以使更改生效。
-
确保你的
site.pp文件中有以下默认节点的空定义:node default {} -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1416376937' Notice: We defined this from /etc/puppet/cookbook.sh Notice: /Stage[main]/Enc/Notify[We defined this from /etc/puppet/cookbook.sh]/message: defined 'message' as 'We defined this from /etc/puppet/cookbook.sh' Notice: Finished catalog run in 0.17 seconds
它是如何工作的...
当在puppet.conf中设置 ENC 时,Puppet 将使用节点的 FQDN(技术上是certname变量)作为第一个命令行参数调用指定的程序。在我们的示例脚本中,这个参数被忽略,它只输出一个固定的类列表(实际上,只有一个类)。
很明显,这个脚本并不是特别有用;一个更复杂的脚本可能会检查数据库以查找类列表,或者在哈希表中查找节点,或者查阅外部文本文件或数据库(通常是一个组织的配置管理数据库,CMDB)。希望这个示例足以帮助你开始编写你自己的外部节点分类器。记住,你可以使用任何你喜欢的语言编写脚本。
还有更多内容...
ENC 可以提供一整类需要包含在节点中的类,格式如下(YAML):
---
classes:
CLASS1:
CLASS2:
CLASS3:
对于需要参数的类,你可以使用以下格式:
---
classes:
mysql:
package: percona-server-server-5.5
socket: /var/run/mysqld/mysqld.sock
port: 3306
你也可以使用以下格式的 ENC 生成顶级作用域变量:
---
parameters:
message: 'Anyone home MyFly?'
你以这种方式设置的变量将在清单中使用常规的顶级作用域变量语法,例如$::message,可以访问这些变量。
另见
- 参见 puppetlabs ENC 页面,了解更多关于编写和使用 ENC 的信息:
docs.puppetlabs.com/guides/external_nodes.html
创建你自己的资源类型
如你所知,Puppet 有许多有用的内置资源类型:包、文件、用户等。通常,你可以通过使用这些内置资源的组合,或者使用 define(你可以像使用资源一样使用它)来完成你需要做的所有事情(有关定义的更多信息,请参见第三章,编写更好的清单)。
在 Puppet 的早期,创建自定义资源类型更为常见,因为核心资源的列表比今天要短。在你考虑创建自己的资源类型之前,我建议你先在 Forge 中搜索替代解决方案。即使你只找到部分解决问题的项目,你也会通过扩展和帮助这个项目得到更好的服务,而不是试图创建自己的资源类型。然而,如果你确实需要创建自己的资源类型,Puppet 使这变得非常容易。原生类型是用 Ruby 编写的,因此你需要对 Ruby 有一定的基础了解才能创建自己的资源类型。
让我们回顾一下类型与提供者的区别。类型描述了一个资源及其可以拥有的参数(例如,package 类型)。提供者告诉 Puppet 如何为特定平台或情况实现资源类型(例如,apt/dpkg 提供者为类似 Debian 的系统实现了 package 类型)。
一个单一类型(package)可以有多个提供者(APT、YUM、Fink 等)。如果在声明资源时没有指定提供者,Puppet 将根据环境选择最合适的提供者。
在本节中,我们将使用 Ruby;如果你不熟悉 Ruby,可以访问www.ruby-doc.org/docs/Tutorial/或www.codecademy.com/tracks/ruby/。
如何实现...
在本节中,我们将看到如何创建一个自定义类型,用于管理 Git 仓库,而在接下来的部分中,我们将编写一个提供者来实现这个类型。
创建文件 modules/cookbook/lib/puppet/type/gitrepo.rb,内容如下:
Puppet::Type.newtype(:gitrepo) do
ensurable
newparam(:source) do
isnamevar
end
newparam(:path)
end
它是如何工作的...
自定义类型可以存在于任何模块中,位于 lib/puppet/type 子目录下,并且文件名应与类型名称相对应(在我们的例子中是 modules/cookbook/lib/puppet/type/gitrepo.rb)。
gitrepo.rb 的第一行告诉 Puppet 注册一个名为 gitrepo 的新类型:
Puppet::Type.newtype(:gitrepo) do
ensurable 这一行会自动为该类型添加 ensure 属性,类似于 Puppet 的内置资源:
ensurable
现在我们将为该类型添加一些参数。目前,我们只需要一个 source 参数来指定 Git 源 URL,以及一个 path 参数来告诉 Puppet 应该在哪个文件系统路径下创建该仓库:
newparam(:source) do
isnamevar
end
isnamevar 声明告诉 Puppet source 参数是该类型的名称变量(namevar)。所以,当你声明此资源的实例时,不论你给定什么名字,它都会成为 source 的值,例如:
gitrepo { 'git://github.com/puppetlabs/puppet.git':
path => '/home/ubuntu/dev/puppet',
}
最后,我们告诉 Puppet 该类型接受 path 参数:
newparam(:path)
还有更多...
在决定是否创建自定义类型时,你应该问自己一些问题,关于你试图描述的资源,例如:
-
资源是可枚举的吗?你能轻松地获取系统上所有该资源实例的列表吗?
-
资源是原子性的吗?你能确保系统上只存在一份该资源吗(当你想对该资源使用
ensure=>absent时,这一点尤为重要)? -
是否有其他资源描述此资源?在这种情况下,基于现有资源定义类型通常是更简单的解决方案。
文档
我们的示例故意保持简单,但当你开始为生产环境开发实际的自定义类型时,应该添加文档字符串来描述该类型及其参数的功能,例如:
Puppet::Type.newtype(:gitrepo) do
@doc = "Manages Git repos"
ensurable
newparam(:source) do
desc "Git source URL for the repo"
isnamevar
end
newparam(:path) do
desc "Path where the repo should be created"
end
end
验证
你可以使用参数验证来生成有用的错误消息,当有人试图向资源传递无效值时。例如,你可以验证存储库创建目录是否实际存在:
newparam(:path) do
validate do |value|
basepath = File.dirname(value)
unless File.directory?(basepath)
raise ArgumentError , "The path %s doesn't exist" % basepath
end
end
end
你还可以指定该参数可以接受的值的列表:
newparam(:breakfast) do
newvalues(:bacon, :eggs, :sausages)
end
创建你自己的提供者
在前一节中,我们创建了一个新的自定义类型 gitrepo,并告诉 Puppet 它需要两个参数,source 和 path。但是,到目前为止,我们还没有告诉 Puppet 如何实际检出该仓库;换句话说,就是如何创建此类型的特定实例。这时,提供者(provider)就派上用场了。
我们看到一个类型通常会有多个可能的提供者。在我们的示例中,实例化 Git 仓库只有一种合理的方法,所以我们只提供一个提供者:git。如果你要将此类型通用化——比如叫做仓库(repo)——可以很容易地想象,根据仓库的类型创建多个不同的提供者,例如 git、svn、cvs 等等。
如何操作...
我们将添加 git 提供者,并创建一个 gitrepo 资源实例来检查是否一切正常。为了让它工作,你需要安装 Git,但如果你正在使用在第二章中描述的基于 Git 的清单管理设置,Puppet 基础设施,我们可以安全地假设 Git 是可用的。
-
创建文件
modules/cookbook/lib/puppet/provider/gitrepo/git.rb,内容如下:require 'fileutils' Puppet::Type.type(:gitrepo).provide(:git) do commands :git => "git" def create git "clone", resource[:source], resource[:path] end def exists? File.directory? resource[:path] end end -
按照以下方式修改你的
site.pp文件:node 'cookbook' { gitrepo { 'https://github.com/puppetlabs/puppetlabs-git': ensure => present, path => '/tmp/puppet', } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Notice: /File[/var/lib/puppet/lib/puppet/type/gitrepo.rb]/ensure: defined content as '{md5}6471793fe2b4372d40289ad4b614fe0b' Notice: /File[/var/lib/puppet/lib/puppet/provider/gitrepo]/ensure: created Notice: /File[/var/lib/puppet/lib/puppet/provider/gitrepo/git.rb]/ensure: defined content as '{md5}f860388234d3d0bdb3b3ec98bbf5115b' Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1416378876' Notice: /Stage[main]/Main/Node[cookbook]/Gitrepo[https://github.com/puppetlabs/puppetlabs-git]/ensure: created Notice: Finished catalog run in 2.59 seconds
工作原理...
自定义提供程序可以存放在任何模块中,位于 lib/puppet/provider/TYPE_NAME 子目录下,并命名为提供程序的名称。(提供程序是实际在系统上运行的程序;在我们的示例中,程序是 Git,而提供程序位于 modules/cookbook/lib/puppet/provider/gitrepo/git.rb。请注意,模块的名称并不重要。)
在 git.rb 的初始 require 语句后,我们告诉 Puppet 使用以下语句注册 gitrepo 类型的新提供程序:
Puppet::Type.type(:gitrepo).provide(:git) do
当你在清单中声明 gitrepo 类型的实例时,Puppet 会首先检查该实例是否已经存在,通过调用提供程序的 exists? 方法。所以我们需要提供这个方法,并编写代码来检查 gitrepo 类型的实例是否已经存在:
def exists?
File.directory? resource[:path]
end
这不是最复杂的实现;它仅仅在存在与实例的 path 参数匹配的目录时返回 true。更好的 exists? 实现可能会检查,例如,是否存在 .git 子目录,并且它包含有效的 Git 元数据。但现在这样已经足够。
如果 exists? 返回 true,那么 Puppet 将不再采取任何进一步的操作,因为指定的资源已经存在(至少 Puppet 是这么认为的)。如果返回 false,Puppet 假设资源尚不存在,并会尝试通过调用提供程序的 create 方法来创建它。
因此,我们提供一些代码给 create 方法,它调用 git clone 命令来创建仓库:
def create
git "clone", resource[:source], resource[:path]
end
该方法可以访问实例的参数,我们需要了解从哪里检出代码仓库,并且在哪个目录下创建它。我们通过查看 resource[:source] 和 resource[:path] 来获得这些信息。
还有更多内容...
你可以看到,Puppet 中的自定义类型和提供程序非常强大。事实上,它们可以做任何事情——至少是 Ruby 可以做的任何事情。如果你正在使用复杂的 define 语句和 exec 资源来管理基础设施的某些部分,你可能想考虑用自定义类型来替代它们。然而,正如前面所说,在实现自己的之前,值得四处看看,看看是否有人已经做过类似的事情。
我们的示例非常简单,关于编写自定义类型还有很多内容需要学习。如果你打算将代码分发给别人使用,或者即使不打算分发,包含测试代码也是一个好主意。puppetlabs 有一个关于自定义类型与提供程序接口的有用页面:
docs.puppetlabs.com/guides/custom_types.html
关于实现提供程序:
docs.puppetlabs.com/guides/provider_development.html
以及一个完整的实例,展示了开发自定义类型和提供程序的过程,比书中展示的内容稍微复杂一些:
docs.puppetlabs.com/guides/complete_resource_example.html
创建自定义函数
如果你已经阅读过第四章中的使用 GnuPG 加密秘密,工作与文件和软件包,那么你应该已经见过一个自定义函数的例子(在那个例子中,我们创建了一个secret函数,调用了 GnuPG)。现在我们更详细地了解custom函数,并构建一个示例。
如何实现...
如果你已经阅读过第六章中的高效分发 cron 任务,管理资源与文件,你可能还记得我们使用了inline_template函数根据节点的主机名设置随机时间来运行 cron 任务。在这个例子中,我们将这个想法转化为一个名为random_minute的自定义函数:
-
创建文件
modules/cookbook/lib/puppet/parser/functions/random_minute.rb,并包含以下内容:module Puppet::Parser::Functions newfunction(:random_minute, :type => :rvalue) do |args| lookupvar('hostname').sum % 60 end end -
按如下方式修改你的
site.pp文件:node 'cookbook' { cron { 'randomised cron job': command => '/bin/echo Hello, world >>/tmp/hello.txt', hour => '*', minute => random_minute(), } } -
运行 Puppet:
[root@cookbook ~]# puppet agent -t Info: Retrieving pluginfacts Info: Retrieving plugin Notice: /File[/var/lib/puppet/lib/puppet/parser/functions/random_minute.rb]/ensure: defined content as '{md5}e6ff40165e74677e5837027bb5610744' Info: Loading facts Info: Caching catalog for cookbook.example.com Info: Applying configuration version '1416379652' Notice: /Stage[main]/Main/Node[cookbook]/Cron[custom fuction example job]/ensure: created Notice: Finished catalog run in 0.41 seconds -
使用以下命令检查
crontab:[root@cookbook ~]# crontab -l # HEADER: This file was autogenerated at Wed Nov 19 01:48:11 -0500 2014 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: run-backup 0 15 * * * /usr/local/bin/backup # Puppet Name: custom fuction example job 15 * * * * /bin/echo Hallo, welt >>/tmp/hallo.txt
它是如何工作的...
自定义函数可以存在于任何模块中,存放在lib/puppet/parser/functions子目录中的一个文件里,文件名与函数名相同(在我们的例子中是random_minute.rb)。
函数代码放在一个module ... end块中,如下所示:
module Puppet::Parser::Functions
...
end
然后我们调用newfunction来声明我们的新函数,传递名称(:random_minute)和函数类型(:rvalue):
newfunction(:random_minute, :type => :rvalue) do |args|
:rvalue部分仅表示此函数返回一个值。
最后,函数代码本身如下:
lookupvar('hostname').sum % 60
lookupvar函数让你通过名称访问事实和变量;在这个例子中,使用hostname来获取我们运行的节点名称。我们使用 Ruby 的sum方法获取字符串中字符的数值总和,然后进行整数除法取模 60,以确保结果在0..59的范围内。
还有更多...
当然,你可以通过自定义函数做更多的事情。实际上,任何你能在 Ruby 中做的事情,都可以在自定义函数中做。你还可以访问在 Puppet 清单中函数被调用时作用域内的所有事实和变量,通过调用lookupvar就像示例中展示的那样。你还可以操作参数,例如创建一个通用的哈希函数,它接受两个参数:哈希表的大小和可选的哈希内容。创建modules/cookbook/lib/puppet/parser/functions/hashtable.rb,并包含以下内容:
module Puppet::Parser::Functions
newfunction(:hashtable, :type => :rvalue) do |args|
if args.length == 2
hashtable=lookupvar(args[1]).sum
else
hashtable=lookupvar('hostname').sum
end
if args.length > 0
size = args[0].to_i
else
size = 60
end
unless size == 0
hashtable % size
else
0
end
end
end
现在我们将为hashtable函数创建一个测试,并按如下方式修改site.pp:
node cookbook {
$hours = hashtable(24)
$minutes = hashtable()
$days = hashtable(30)
$days_fqdn = hashtable(30,'fqdn')
$days_ipaddress = hashtable(30,'ipaddress')
notify {"\n hours=${hours}\n minutes=${minutes}\n days=${days}\n days_fqdn=${days_fqdn}\n days_ipaddress=${days_ipaddress}\n":}
}
现在,运行 Puppet 并观察返回的值:
Notice: hours=15
minutes=15
days=15
days_fqdn=4
days_ipaddress=2
我们的简单定义在我们增加了参数功能后迅速增长。与所有编程一样,在处理参数时要特别小心,确保没有错误情况。在之前的代码中,我们专门检查了size变量为 0 的情况,以避免除零错误。
要了解更多关于自定义函数的内容,请参阅 puppetlabs 网站:
docs.puppetlabs.com/guides/custom_functions.html
使用 rspec-puppet 测试你的 Puppet 清单
如果我们能验证我们的 Puppet 清单是否满足某些预期,而不必运行 Puppet,那将是极好的。rspec-puppet是一个很棒的工具,可以实现这一目标。基于 RSpec,一个 Ruby 程序的测试框架,rspec-puppet让你为 Puppet 清单编写测试用例,特别有助于捕捉回归问题(修复另一个 bug 时引入的 bug)和重构问题(重组代码时引入的 bug)。
准备工作
下面是安装rspec-puppet时你需要做的步骤:
运行以下命令:
t@mylaptop~ $ sudo puppet resource package rspec-puppet ensure=installed provider=gem
Notice: /Package[rspec-puppet]/ensure: created
package { 'rspec-puppet':
ensure => ['1.0.1'],
}
t@mylaptop ~ $ sudo puppet resource package puppetlabs_spec_helper ensure=installed provider=gem
Notice: /Package[puppetlabs_spec_helper]/ensure: created
package { 'puppetlabs_spec_helper':
ensure => ['0.8.2'],
}
如何操作...
让我们创建一个示例类thing,并为其编写一些测试。
-
定义
thing类:class thing { service {'thing': ensure => 'running', enable => true, require => Package['thing'], } package {'thing': ensure => 'installed' } file {'/etc/thing.conf': content => 'fubar\n', mode => 0644, require => Package['thing'], notify => Service['thing'], } } -
运行以下命令:
t@mylaptop ~/puppet]$cd modules/thing t@mylaptop~/puppet/modules/thing $ rspec-puppet-init + spec/ + spec/classes/ + spec/defines/ + spec/functions/ + spec/hosts/ + spec/fixtures/ + spec/fixtures/manifests/ + spec/fixtures/modules/ + spec/fixtures/modules/heartbeat/ + spec/fixtures/manifests/site.pp + spec/fixtures/modules/heartbeat/manifests + spec/fixtures/modules/heartbeat/templates + spec/spec_helper.rb + Rakefile -
创建文件
spec/classes/thing_spec.rb,并写入以下内容:require 'spec_helper' describe 'thing' do it { should create_class('thing') } it { should contain_package('thing') } it { should contain_service('thing').with( 'ensure' => 'running' ) } it { should contain_file('/etc/things.conf') } end -
运行以下命令:
t@mylaptop ~/.puppet/modules/thing $ rspec ...F Failures: 1) thing should contain File[/etc/things.conf] Failure/Error: it { should contain_file('/etc/things.conf') } expected that the catalogue would contain File[/etc/things.conf] # ./spec/classes/thing_spec.rb:9:in `block (2 levels) in <top (required)>' Finished in 1.66 seconds 4 examples, 1 failure Failed examples: rspec ./spec/classes/thing_spec.rb:9 # thing should contain File[/etc/things.conf]
它是如何工作的...
rspec-puppet-init命令为你创建一个目录框架,以便你将你的测试程序(specs)放入其中。现在,我们只关心spec/classes目录。这里是你将放置每个类的测试文件的地方,每个文件以类名命名,例如,thing_spec.rb。
spec代码本身以以下语句开始,设置了运行测试的 RSpec 环境:
require 'spec_helper'
然后,跟着一个describe块:
describe 'thing' do
..
end
describe标识了我们要测试的类(thing),并将关于该类的断言列表放入一个do .. end块中。
断言是我们对thing类的预期。例如,第一个断言如下:
it { should create_class('thing') }
create_class断言用于确保命名的类确实被创建。接下来的行:
it { should contain_package('thing') }
contain_package断言意味着它字面上的意思:该类应该包含一个名为thing的包资源。
接下来,我们测试thing服务的存在:
it { should contain_service('thing').with(
'ensure' => 'running'
) }
上述代码实际上包含了两个断言。首先,类包含一个thing服务:
contain_service('thing')
第二,服务具有一个ensure属性,值为running:
with(
'ensure' => 'running'
)
你可以使用with方法指定任何你想要的属性和值,作为逗号分隔的列表。例如,以下代码断言了一个file资源的多个属性:
it { should contain_file('/tmp/hello.txt').with(
'content' => "Hello, world\n",
'owner' => 'ubuntu',
'group' => 'ubuntu',
'mode' => '0644'
) }
在我们的thing示例中,我们只需要测试文件thing.conf是否存在,使用以下代码:
it { should contain_file('/etc/thing.conf') }
当你运行rake spec命令时,rspec-puppet将编译相关的 Puppet 类,运行它找到的所有测试,并显示结果:
...F
Failures:
1) thing should contain File[/etc/things.conf]
Failure/Error: it { should contain_file('/etc/things.conf') }
expected that the catalogue would contain File[/etc/things.conf]
# ./spec/classes/thing_spec.rb:9:in `block (2 levels) in <top (required)>'
Finished in 1.66 seconds
4 examples, 1 failure
如你所见,我们在测试中定义的文件是/etc/things.conf,但清单中的文件是/etc/thing.conf,因此测试失败。编辑thing_spec.rb,将/etc/things.conf改为/etc/thing.conf:
it { should contain_file('/etc/thing.conf') }
现在再次运行 rspec:
t@mylaptop ~/.puppet/modules/thing $ rspec
....
Finished in 1.6 seconds
4 examples, 0 failures
还有更多...
使用 rspec,你可以验证许多条件。任何资源类型都可以通过 contain_<资源类型>(标题) 进行验证。除了验证你的类是否能正确应用外,你还可以通过在 spec 目录内使用相应的子目录(类、定义或函数)来测试函数和定义。
你可以在 rspec-puppet.com/ 上找到更多关于 rspec-puppet 的信息,包括可用断言的完整文档和教程。
当你想开始测试你的代码如何应用于节点时,你需要使用另一个工具——Beaker。Beaker 与各种虚拟化平台配合,创建临时虚拟机,并将 Puppet 代码应用到这些虚拟机上。然后,结果将用于 Puppet 代码的验收测试。这种一边开发一边测试的方法被称为 测试驱动开发(TDD)。有关 Beaker 的更多信息,请访问 GitHub 上的 github.com/puppetlabs/beaker。
另见
- 第一章中的 使用 puppet-lint 检查你的清单 示例,Puppet 语言与风格
使用 librarian-puppet
当你开始在 Puppet 基础设施中包含来自 Forge 的模块时,跟踪你安装的版本并确保所有测试区域的一致性可能会变得有些棘手。幸运的是,我们将在接下来的两节中讨论的工具可以为你的系统带来秩序。我们首先将介绍 librarian-puppet,它使用一个名为 Puppetfile 的特殊配置文件来指定各种模块的源位置。
准备工作
我们将安装 librarian-puppet 来完成这个示例。
使用 Puppet 在 Puppet 主机上安装 librarian-puppet,当然是通过 Puppet 安装:
root@puppet:~# puppet resource package librarian-puppet ensure=installed provider=gem
Notice: /Package[librarian-puppet]/ensure: created
package { 'librarian-puppet':
ensure => ['2.0.0'],
}
提示
如果你在一个无主机环境中工作,请在你将管理代码的机器上安装 librarian-puppet。如果 Ruby 开发包在主机上不可用,你的 gem 安装可能会失败;安装 ruby-dev 包可以解决这个问题(使用 Puppet 来安装)。
如何操作...
在这个示例中,我们将使用 librarian-puppet 下载并安装一个模块:
-
为自己创建一个工作目录;librarian-puppet 默认会覆盖你的模块目录,因此我们暂时将在临时位置工作:
root@puppet:~# mkdir librarian root@puppet:~# cd librarian -
创建一个新的 Puppetfile,内容如下:
#!/usr/bin/env ruby #^syntax detection forge "https://forgeapi.puppetlabs.com" # A module from the Puppet Forge mod 'puppetlabs-stdlib'注意
另外,你可以使用
librarian-puppet init来创建一个示例 Puppetfile,并编辑它以匹配我们的示例:root@puppet:~/librarian# librarian-puppet init create Puppetfile -
现在,运行 librarian-puppet 来下载并安装
puppetlabs-stdlib模块到modules目录:root@puppet:~/librarian# librarian-puppet install root@puppet:~/librarian # ls modules Puppetfile Puppetfile.lock root@puppet:~/librarian # ls modules stdlib
它是如何工作的...
Puppetfile 的第一行让 Puppetfile 看起来像一个 Ruby 源代码文件。这些完全是可选的,但它强迫编辑器将该文件当作 Ruby 文件来处理(事实上它就是 Ruby 文件):
#!/usr/bin/env ruby
接下来我们定义 Puppet Forge 的位置;如果你有本地镜像,也可以在这里指定一个内部 Forge:
forge "https://forgeapi.puppetlabs.com"
现在,我们添加了一行以包括 puppetlabs-stdlib 模块:
mod 'puppetlabs-stdlib'
有了 Puppetfile 后,我们运行了 librarian-puppet,它从 Forge 行中提供的 URL 下载了模块。当模块下载时,librarian-puppet 创建了一个 Puppetfile.lock 文件,其中包含作为源使用的位置和下载模块的版本号:
FORGE
remote: https://forgeapi.puppetlabs.com
specs:
puppetlabs-stdlib (4.4.0)
DEPENDENCIES
puppetlabs-stdlib (>= 0)
还有更多...
Puppetfile 允许你从除 Forge 以外的来源拉取模块。你可以使用本地 Git URL,甚至是 GitHub URL,来下载那些不在 Forge 上的模块。关于 librarian-puppet 的更多信息可以在 GitHub 网站上找到:github.com/rodjek/librarian-puppet。
请注意,librarian-puppet 会创建模块目录并默认删除你放入其中的任何模块。大多数使用 librarian-puppet 的安装选择将本地模块放在 /local 子目录中(/dist 或 /companyname 也常用)。
在下一节中,我们将讨论 r10k,它比 librarian 更进一步,管理整个环境目录。
使用 r10k
Puppetfile 是一种非常好的格式,用于描述你希望在环境中包含哪些模块。在 Puppetfile 的基础上,另一个工具是 r10k。r10k 是一个完整的环境管理工具。你可以使用 r10k 将本地 Git 仓库克隆到你的 environmentpath 中,然后将 Puppetfile 中指定的模块放入该目录。这个本地 Git 仓库被称为主仓库;r10k 期望在此找到你的 Puppetfile。r10k 还理解 Puppet 环境,并会将 Git 分支克隆到 environmentpath 的子目录中,从而简化多个环境的部署。r10k 特别有用的一点是它使用本地缓存目录来加速部署。通过配置文件 r10k.yaml,你可以指定缓存的存储位置以及主仓库的位置。
准备工作
我们将在控制机器(通常是主机)上安装 r10k。这将是我们控制所有已下载和已安装模块的地方。
-
在你的 Puppet 主机上安装 r10k,或者在你希望管理
environmentpath目录的任何机器上安装:root@puppet:~# puppet resource package r10k ensure=installed provider=gem Notice: /Package[r10k]/ensure: created package { 'r10k': ensure => ['1.3.5'], } -
创建一个新的 Git 仓库副本(可选,建议在你的 Git 服务器上进行):
[git@git repos]$ git clone --bare puppet.git puppet-r10k.git Initialized empty Git repository in /home/git/repos/puppet-r10k.git/ -
在你的本地机器上检出新的 Git 仓库,并将现有的模块目录移动到新的位置。在本例中,我们使用
/local:t@mylaptop ~ $ git clone git@git.example.com:repos/puppet-r10k.git Cloning into 'puppet-r10k'... remote: Counting objects: 2660, done. remote: Compressing objects: 100% (2136/2136), done. remote: Total 2660 (delta 913), reused 1049 (delta 238) Receiving objects: 100% (2660/2660), 738.20 KiB | 0 bytes/s, done. Resolving deltas: 100% (913/913), done. Checking connectivity... done. t@mylaptop ~ $ cd puppet-r10k/ t@mylaptop ~/puppet-r10k $ git checkout production Branch production set up to track remote branch production from origin. Switched to a new branch 'production' t@mylaptop ~/puppet-r10k $ git mv modules local t@mylaptop ~/puppet-r10k $ git commit -m "moving modules in preparation for r10k" [master c96d0dc] moving modules in preparation for r10k 9 files changed, 0 insertions(+), 0 deletions(-) rename {modules => local}/base (100%) rename {modules => local}/puppet/files/papply.sh (100%) rename {modules => local}/puppet/files/pull-updates.sh (100%) rename {modules => local}/puppet/manifests/init.pp (100%)
如何操作...
我们将创建一个 Puppetfile 来控制 r10k 并在我们的主机上安装模块。
-
在新的 Git 仓库中创建一个包含以下内容的
Puppetfile:forge "http://forge.puppetlabs.com" mod 'puppetlabs/puppetdb', '3.0.0' mod 'puppetlabs/stdlib', '3.2.0' mod 'puppetlabs/concat' mod 'puppetlabs/firewall' -
将
Puppetfile添加到你的新仓库中:t@mylaptop ~/puppet-r10k $ git add Puppetfile t@mylaptop ~/puppet-r10k $ git commit -m "adding Puppetfile" [production d42481f] adding Puppetfile 1 file changed, 7 insertions(+) create mode 100644 Puppetfile t@mylaptop ~/puppet-r10k $ git push Counting objects: 7, done. Delta compression using up to 4 threads. Compressing objects: 100% (5/5), done. Writing objects: 100% (5/5), 589 bytes | 0 bytes/s, done. Total 5 (delta 2), reused 0 (delta 0) To git@git.example.com:repos/puppet-r10k.git cf8dfb9..d42481f production -> production -
回到你的主机,创建
/etc/r10k.yaml文件,内容如下:--- :cachedir: '/var/cache/r10k' :sources: :plops: remote: 'git@git.example.com:repos/puppet-r10k.git' basedir: '/etc/puppet/environments' -
运行 r10k 以填充
/etc/puppet/environments目录(提示:首先备份你的/etc/puppet/environments目录):root@puppet:~# r10k deploy environment -p -
确认你的
/etc/puppet/environments目录下有一个production子目录。在该目录内,/local目录将存在,并且模块目录中会列出Puppetfile中所有的模块:root@puppet:/etc/puppet/environments# tree -L 2 . ├── master │ ├── manifests │ ├── modules │ └── README └── production ├── environment.conf ├── local ├── manifests ├── modules ├── Puppetfile └── README
它是如何工作的...
我们首先创建了一个 Git 仓库的副本;这样做只是为了保留之前的工作,并不是必须的。需要记住的重要事项是,r10k 和 librarian-puppet 都假设它们控制着/modules子目录。我们需要将模块移开,并为模块创建一个新的位置。
在r10k.yaml文件中,我们指定了新仓库的位置。当我们运行 r10k 时,它首先将这个仓库下载到本地缓存中。一旦 Git 仓库被下载到本地,r10k 会遍历每个分支,并查找该分支中的Puppetfile。对于每个branch/Puppetfile组合,里面指定的模块首先下载到本地缓存目录(cachedir),然后下载到r10k.yaml中指定的basedir。
还有更多内容...
你可以使用r10k来自动化部署你的环境。我们用来运行r10k并填充我们的环境目录的命令,可以轻松地放入 Git 钩子中,自动更新你的环境。还有一个marionette collective(mcollective)插件(github.com/acidprime/r10k),可以让r10k在任意一组服务器上运行。
使用这些工具中的任何一个将有助于保持你的网站一致,即使你没有利用 Forge 上可用的各种模块。
第十章:监控、报告和故障排除
| "展示一个完全顺利的操作,我就能向你展示一个掩盖错误的人。真正的船总是会摇晃的。" | ||
|---|---|---|
| --弗兰克·赫伯特,《沙丘救世主》 |
在本章中,我们将覆盖以下内容:
-
Noop: 不做任何更改的选项
-
记录命令输出
-
记录调试信息
-
生成报告
-
生成自动 HTML 文档
-
绘制依赖关系图
-
理解 Puppet 错误
-
检查配置设置
介绍
我们都曾有过这样的经历:坐在一个令人兴奋的关于新技术的演讲中,迫不及待地回家玩它。当然,一旦你开始尝试它,你立刻会遇到问题。出了什么问题?为什么不行?我怎么知道底层发生了什么?本章将帮助你回答一些问题,并为你提供解决常见 Puppet 问题的工具。
我们还将看到如何生成有用的报告,了解你的 Puppet 基础设施,以及 Puppet 如何帮助你监控和排查整个网络的问题。
Noop – 不做任何更改的选项
有时你的 Puppet 清单没有完全按预期工作,或者可能是别人提交了你不知道的更改。无论如何,提前知道 Puppet 将要做什么总是好的。
当你将 Puppet 集成到现有的基础设施中时,你可能不知道 Puppet 是否会更新config文件或重启生产服务。任何此类变更都可能导致计划外的停机时间。此外,有时会在服务器上手动进行配置更改,而这些更改可能会被 Puppet 覆盖。
为了避免这些问题,你可以使用 Puppet 的 noop 模式,它意味着不操作或什么都不做。运行带有 noop 选项时,Puppet 只会报告它本来会做什么,但实际上什么也不做。这里有一个注意点,即即使在 noop 运行期间,pluginsync 仍会运行,模块中的任何lib目录都会同步到节点。这将更新外部事实定义,并可能更新 Puppet 的类型和提供者。
如何操作...
你可以通过在运行puppet agent或puppet apply时添加--noop开关来运行 noop 模式。你也可以在puppet.conf文件的[agent]或[main]部分创建一行noop=true。
-
创建一个
noop.pp清单,生成一个如下的文件:file {'/tmp/noop': content => 'nothing', mode => 0644, } -
现在运行带有
noop开关的 puppet agent:t@mylaptop ~/puppet/manifests $ puppet apply noop.pp --noop Notice: Compiled catalog for mylaptop in environment production in 0.41 seconds Notice: /Stage[main]/Main/File[/tmp/noop]/ensure: current_value absent, should be file (noop) Notice: Class[Main]: Would have triggered 'refresh' from 1 events Notice: Stage[main]: Would have triggered 'refresh' from 1 events Notice: Finished catalog run in 0.02 seconds -
现在运行不带
noop选项,以查看文件是否已创建:t@mylaptop ~/puppet/manifests $ puppet apply noop.pp Notice: Compiled catalog for mylaptop in environment production in 0.37 seconds Notice: /Stage[main]/Main/File[/tmp/noop]/ensure: defined content as '{md5}3e47b75000b0924b6c9ba5759a7cf15d'
它是如何工作的...
在noop模式下,Puppet 会做它通常会做的所有事情,除了实际对机器进行任何更改(例如,exec资源不会运行)。它会告诉你本来会做什么,你可以将其与预期的结果进行比较。如果存在任何差异,仔细检查清单或机器的当前状态。
注意
请注意,当我们使用--noop运行时,Puppet 警告我们将创建/tmp/noop文件。 这可能是我们想要的,也可能不是,但预先知道这一点很有用。 如果您正在更改应用于生产服务器的代码,建议使用--noop选项运行 puppet agent,以确保您的更改不会影响生产服务。
还有更多...
你还可以使用 noop 模式作为简单的审计工具。 它会告诉你自 Puppet 上次应用其清单以来是否对机器进行了任何更改。 一些组织要求所有配置更改都必须使用 Puppet 进行,这是实施变更控制流程的一种方式。 可以使用 noop 模式检测到 Puppet 管理的资源的未经授权的更改,然后您可以决定是否将更改合并回 Puppet 清单或撤销它们。
运行 puppet agent 时,您还可以使用--debug开关查看 Puppet 在代理运行期间进行的每个更改的详细信息。 当试图弄清楚 Puppet 如何应用某些 exec 资源或查看事情发生的顺序时,这可能会很有帮助。
如果您正在运行主控,可以使用--trace选项编译节点的目录以及--debug。 如果目录编译失败,则此方法也将无法编译目录(如果您的 cookbook 节点的旧定义失败,请尝试在运行此测试之前将其注释掉)。 这将生成大量的调试输出。 例如,为了为我们的 cookbook 主机在主控上编译目录并将结果放入/tmp/cookbook.log:
root@puppet: ~#puppet master --compile cookbook.example.com --debug --trace --logdest /tmp/cookbook.log
Debug: Executing '/etc/puppet/cookbook.sh cookbook.example.com'
Debug: Using cached facts for cookbook.example.com
Info: Caching node for cookbook.example.com
Debug: importing '/etc/puppet/environments/production/modules/enc/manifests/init.pp' in environment production
Debug: Automatically imported enc from enc into production
Notice: Compiled catalog for cookbook.example.com in environment production in 0.09 seconds
Info: Caching catalog for cookbook.example.com
Debug: Configuring PuppetDB terminuses with config file /etc/puppet/puppetdb.conf
Debug: Using cached certificate for ca
Debug: Using cached certificate for puppet
Debug: Using cached certificate_revocation_list for ca
Info: 'replace catalog' command for cookbook.example.com submitted to PuppetDB with UUIDe2a655ca-bd81-4428-b70a-a3a76c5f15d1
{
"metadata": {
"api_version": 1
},
"data": {
"edges": [
{
"target": "Class[main]",
"source": "Stage[main]"
...
注意
编译目录后,Puppet 将在命令行上打印目录。 日志文件(/tmp/cookbook.log)将包含有关如何编译目录的大量信息。
另请参阅
-
在第六章中的审计资源食谱,管理资源和文件
-
在第二章中的使用 Git 挂钩进行自动语法检查食谱,Puppet 基础设施
-
本章节中的生成报告食谱
-
在第九章中的使用 rspec-puppet 测试您的 Puppet 清单食谱,外部工具和 Puppet 生态系统
记录命令输出
当您使用exec资源在节点上运行命令时,如果命令返回非零退出状态,Puppet 将会给出如下错误消息:
Notice: /Stage[main]/Main/Exec[/bin/cat /tmp/missing]/returns: /bin/cat: /tmp/missing: No such file or directory
Error: /bin/cat /tmp/missing returned 1 instead of one of [0]
Error: /Stage[main]/Main/Exec[/bin/cat /tmp/missing]/returns: change from notrun to 0 failed: /bin/cat /tmp/missing returned 1 instead of one of [0]
如您所见,Puppet 不仅报告命令失败,还显示其输出:
/bin/cat: /tmp/missing: No such file or directory
这对于弄清楚命令为何无法正常工作很有用,但有时命令实际上是成功的(即返回零退出状态),但仍未达到我们的预期。 在这种情况下,您如何查看命令输出? 您可以使用logoutput属性。
如何做...
按顺序执行以下步骤以记录命令输出:
-
定义一个带有
logoutput参数的exec资源,如下代码片段所示:exec { 'exec with output': command => '/bin/cat /etc/hostname', logoutput => true, } -
运行 Puppet:
t@mylaptop ~/puppet/manifests $ puppet apply exec.pp Notice: Compiled catalog for mylaptop in environment production in 0.46 seconds Notice: /Stage[main]/Main/Exec[exec with outout]/returns: mylaptop Notice: /Stage[main]/Main/Exec[exec with outout]/returns: executed successfully Notice: Finished catalog run in 0.06 seconds -
正如你所看到的,即使命令成功,Puppet 仍然会打印输出:
mylaptop
它是如何工作的……
logoutput 属性有三种可能的设置:
-
false:这个选项永远不会打印命令输出。 -
on_failure:只有在命令失败时才打印输出(默认设置) -
true:无论命令是否成功,这个选项总是会打印输出。
还有更多……
你可以通过在 site.pp 文件中定义以下内容,设置 logoutput 的默认值,使所有 exec 资源都显示命令输出:
Exec {
logoutput => true,
注意
资源默认值:这是什么 Exec 语法?它看起来像是一个 exec 资源,但其实不是。当你使用大写的 Exec 时,你是在设置 exec 的资源默认值。你可以通过将资源类型的首字母大写来设置任何资源的默认值。只要 Puppet 在当前作用域或嵌套子作用域中看到该资源,它就会应用你定义的默认值。
如果你不希望看到命令输出,无论命令成功还是失败,请使用:
logoutput => false,
更多信息请参见 docs.puppetlabs.com/references/latest/type.html#exec。
日志调试信息
当调试问题时,如果你能够在清单中的某个点打印出信息,那将非常有帮助。这是一种很好的方式,举个例子,可以用来检查一个变量是否未定义或有意外的值。有时候,仅仅知道某段代码已经被执行也是有用的。Puppet 的 notify 资源让你能够打印出这样的消息。
它是如何做的……
在你想要调试的地方,在清单中定义一个 notify 资源:
notify { 'Got this far!': }
它是如何工作的……
当这个资源应用时,Puppet 会打印出消息:
notice: Got this far!
还有更多……
除了简单的消息外,我们还可以在 notify 语句中输出变量。此外,我们可以将 notify 调用当作其他资源来处理,使它们依赖于其他资源或被其他资源依赖。
打印变量值
你可以在消息中引用变量:
notify { "operatingsystem is ${::operatingsystem}": }
Puppet 会在输出中插入值:
Notice: operatingsystem is Fedora
在事实名称前的双冒号 (::) 告诉 Puppet 这是一个在顶级作用域中的变量(所有类都能访问),而不是局部变量。关于 Puppet 如何处理变量作用域的更多信息,请参见 Puppet Labs 文章:
docs.puppetlabs.com/guides/scope_and_puppet.html
资源顺序
Puppet 会将你的清单编译成一个目录;资源在客户端(节点)上执行的顺序可能与源文件中的资源顺序不同。当你使用 notify 资源进行调试时,你应该使用资源链式调用,确保 notify 资源在你的失败资源之前或之后执行。
例如,如果 failing exec 执行失败,你可以将一个 notify resource 链接到失败的 exec 资源之前,正如下面所示:
notify{"failed exec on ${hostname}": }->
exec {'failing exec':
command => "/bin/grep ${hostname} /etc/hosts",
logoutput => true,
}
如果你没有将资源链接起来,或者没有使用像 before 或 require 这样的元参数,那么不能保证你的 notify 语句会在你关注的其他资源附近执行。关于资源排序的更多信息可以在 docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html 找到。
例如,要让你的 notify resource 在前面的代码片段中 'failing exec' 后运行,使用:
notify { 'Resource X has been applied':
require => Exec['failing exec'],
}
然而,请注意,在这种情况下,由于 exec 失败,notify resource 将无法执行。当一个资源失败时,所有依赖于该资源的其他资源将被跳过:
notify {'failed exec failed':
require => Exec['failing exec']
}
当我们运行 Puppet 时,我们看到 notify resource 被跳过:
t@mylaptop ~/puppet/manifests $ puppet apply fail.pp
...
Error: /bin/grepmylaptop /etc/hosts returned 1 instead of one of [0]
Error: /Stage[main]/Main/Exec[failing exec]/returns: change from notrun to 0 failed: /bin/grepmylaptop /etc/hosts returned 1 instead of one of [0]
Notice: /Stage[main]/Main/Notify[failed exec failed]: Dependency Exec[failing exec] has failures: true
Warning: /Stage[main]/Main/Notify[failed exec failed]: Skipping because of failed dependencies
Notice: Finished catalog run in 0.06 seconds
生成报告
如果你管理着大量机器,Puppet 的报告功能可以为你提供一些关于实际发生情况的有价值信息。
如何操作...
要启用报告,只需在客户端的 puppet.conf 中的 [main] 或 [agent] 部分添加以下内容:
report = true
小提示
在 Puppet 的最新版本(大于 3.0)中,report = true 是默认设置。
它是如何工作的...
启用报告后,Puppet 会生成一个报告文件,其中包含以下数据:
-
运行的日期和时间
-
运行的总时间
-
在运行期间输出的日志消息
-
客户端清单中所有资源的列表
-
Puppet 是否更改了任何资源,以及更改了多少
-
运行是否成功或失败
默认情况下,这些报告会保存在节点的 /var/lib/puppet/reports 目录中,并以主机名命名,你也可以使用 reportdir 选项指定不同的目标位置。你可以创建自己的脚本来处理这些报告(它们是标准的 YAML 格式)。当我们在 cookbook.example.com 上运行 Puppet agent 时,以下文件会在主服务器上创建:
/var/lib/puppet/reports/cookbook.example.com/201411230717.yaml
还有更多内容...
如果你有多个主服务器,可以通过在 puppet.conf 的 [agent] 部分指定 report_server,将所有报告发送到同一服务器。
如果你只想要一个报告,或者你不想始终启用报告功能,你可以在手动运行 Puppet agent 时,向命令行添加 --report 开关:
[root@cookbook ~]# puppet agent -t --report
Notice: Finished catalog run in 0.34 seconds
你不会看到任何额外的输出,但报告文件会在 report 目录中生成。
你还可以通过提供 --summarize 开关来查看 Puppet 运行的一些总体统计信息:
[root@cookbook ~]# puppet agent -t --report --summarize
Notice: Finished catalog run in 0.35 seconds
Changes:
Total: 2
Events:
Total: 2
Success: 2
Resources:
Total: 10
Changed: 2
Out of sync: 2
Time:
Filebucket: 0.00
Schedule: 0.00
Notify: 0.00
Config retrieval: 0.94
Total: 0.95
Last run: 1416727357
Version:
Config: 1416727291
Puppet: 3.7.3
其他报告类型
Puppet 可以使用puppet.conf中的[main]或[master]部分的reports选项,在你的 Puppet 主服务器上生成不同类型的报告。内置报告类型可以在docs.puppetlabs.com/references/latest/report.html中找到。除了内置报告类型外,还有一些社区开发的报告非常有用。例如,Foreman(theforeman.org)提供了一种 Foreman 报告类型,可以启用并将节点报告转发到 Foreman。
另请参见
- 第六章中的审计资源配方,管理资源和文件
生成自动 HTML 文档
随着清单变得越来越大和复杂,使用 Puppet 的自动文档工具puppet doc为节点和类生成 HTML 文档是非常有帮助的。
如何执行...
按照以下步骤为清单生成 HTML 文档:
-
运行以下命令:
t@mylaptop ~/puppet $ puppet doc --all --outputdir=/tmp/puppet --mode rdoc --modulepath=modules/ -
这将生成一组 HTML 文件到
/tmp/puppet。用浏览器打开顶级index.html文件(file:///tmp/puppet/index.html),你将看到类似以下截图的内容:![如何执行...]()
-
点击左侧的类链接并选择 Apache 模块,类似如下内容将被显示:
![如何执行...]()
它是如何工作的...
puppet doc命令创建一个结构化的 HTML 文档树,类似于RDoc(流行的 Ruby 文档生成器)生成的文档。这使得理解清单的不同部分之间的关系变得更加容易。
还有更多...
puppet doc命令将生成当前清单的基础文档,但你可以通过在清单文件中添加注释,使用标准的 RDoc 语法来包括更多有用的信息。当我们使用puppet module generate创建基础类时,这些注释是自动为我们创建的:
# == Class: base
#
# Full description of class base here.
#
# === Parameters
#
# Document parameters here.
#
# [*sample_parameter*]
# Explanation of what this parameter affects and what it defaults to.
# e.g. "Specify one or more upstream ntp servers as an array."
#
# === Variables
#
# Here you should define a list of variables that this module would require.
#
# [*sample_variable*]
# Explanation of how this variable affects the funtion of this class and if
# it has a default. e.g. "The parameter enc_ntp_servers must be set by the
# External Node Classifier as a comma separated list of hostnames." (Note,
# global variables should be avoided in favor of class parameters as
# of Puppet 2.6.)
#
# === Examples
#
# class { base:
# servers => [ 'pool.ntp.org', 'ntp.local.company.com' ],
# }
#
# === Authors
#
# Author Name <author@domain.com>
#
# === Copyright
#
# Copyright 2014 Your name here, unless otherwise noted.
#
class base {
生成 HTML 文档后,我们可以看到基础模块的结果,如下图所示:

绘制依赖关系图
依赖关系可能会迅速变得复杂,容易陷入循环依赖(例如 A 依赖 B,而 B 又依赖 A),这会导致 Puppet 报错并停止工作。幸运的是,Puppet 的--graph选项可以轻松生成资源及其依赖关系的图表,这对于解决此类问题非常有帮助。
准备工作
安装graphviz包以查看图表文件:
t@mylaptop ~ $ sudo puppet resource package graphviz ensure=installed
Notice: /Package[graphviz]/ensure: created
package { 'graphviz':
ensure => '2.34.0-9.fc20',
}
如何执行...
按照以下步骤为清单生成依赖关系图:
-
为新的
trifecta模块创建目录:ubuntu@cookbook:~/puppet$ mkdir modules/trifecta ubuntu@cookbook:~/puppet$ mkdir modules/trifecta/manifests ubuntu@cookbook:~/puppet$ mkdir modules/trifecta/files -
创建文件
modules/trifecta/manifests/init.pp,并包含以下代码,代码中有一个故意的循环依赖(你能发现吗?):class trifecta { package { 'ntp': ensure => installed, require => File['/etc/ntp.conf'], } service { 'ntp': ensure => running, require => Package['ntp'], } file { '/etc/ntp.conf': source => 'puppet:///modules/trifecta/ntp.conf', notify => Service['ntp'], require => Package['ntp'], } } -
创建一个简单的
ntp.conf文件:t@mylaptop~/puppet $ cd modules/trifecta/files t@mylaptop~/puppet/modules/trifecta/files $ echo "server 127.0.0.1" >ntp.conf -
由于我们将本地处理此问题,创建一个
trifecta.pp清单,其中包括已打破的三位一体类:include trifecta -
运行 Puppet:
t@mylaptop ~/puppet/manifests $ puppet apply trifecta.pp Notice: Compiled catalog for mylaptop in environment production in 1.32 seconds Error: Could not apply complete catalog: Found 1 dependency cycle: (File[/etc/ntp.conf] => Package[ntp] => File[/etc/ntp.conf]) Try the '--graph' option and opening the resulting '.dot' file in OmniGraffle or GraphViz -
按照建议使用
--graph选项运行 Puppet:t@mylaptop ~/puppet/manifests $ puppet apply trifecta.pp --graph Notice: Compiled catalog for mylaptop in environment production in 1.26 seconds Error: Could not apply complete catalog: Found 1 dependency cycle: (File[/etc/ntp.conf] => Package[ntp] => File[/etc/ntp.conf]) Cycle graph written to /home/tuphill/.puppet/var/state/graphs/cycles.dot. Notice: Finished catalog run in 0.03 seconds -
检查图形文件是否已创建:
t@mylaptop ~/puppet/manifests $ cd ~/.puppet/var/state/graphs t@mylaptop ~/.puppet/var/state/graphs $ ls -l total 16 -rw-rw-r--. 1 thomasthomas 121 Nov 23 23:11 cycles.dot -rw-rw-r--. 1 thomasthomas 2885 Nov 23 23:11 expanded_relationships.dot -rw-rw-r--. 1 thomasthomas 1557 Nov 23 23:11 relationships.dot -rw-rw-r--. 1 thomasthomas 1680 Nov 23 23:11 resources.dot -
使用以下方式创建图形:
dot命令:ubuntu@cookbook:~/puppet$ dot -Tpng -o relationships.png /var/lib/puppet/state/graphs/relationships.dot -
图形看起来像这样:
![如何操作...]()
它是如何工作的……
当您运行puppet agent --graph(或者在puppet.conf中启用graph选项)时,Puppet 将生成三张 DOT 格式的图(图形语言):
-
resources.dot:此文件展示了您的类和资源的层级结构,但没有依赖关系。 -
relationships.dot:此文件展示了资源之间的依赖关系,用箭头表示,正如前面图像所示。 -
expanded_relationships.dot:这是一个更详细的关系图版本。
dot工具(graphviz包的一部分)将把这些转换为 PNG 等图像格式供查看。
在关系图中,清单中的每个资源都显示为气球(称为顶点),并通过箭头连接以指示依赖关系。您可以看到在我们的例子中,File['/etc/ntp.conf']和Package['ntp']之间的依赖关系是双向的。当 Puppet 尝试决定从哪里开始应用这些资源时,它可以从File['/etc/ntp.conf']开始,查找哪些依赖于File['/etc/ntp.conf'],最终回到Package['ntp']。当 Puppet 查找依赖关系时
对于Package['ntp'],最终会回到File['/etc/ntp.conf'],形成一个循环路径。此类问题被称为循环依赖问题;Puppet 无法决定从哪里开始,因为这两个资源相互依赖。
要解决循环依赖问题,您需要做的就是删除其中一行依赖关系并打破循环。以下代码解决了这个问题:
class trifecta {
package { 'ntp':
ensure => installed,
}
service { 'ntp':
ensure => running,
require => Package['ntp'],
}
file { '/etc/ntp.conf':
source => 'puppet:///modules/trifecta/ntp.conf',
notify => Service['ntp'],
require => Package['ntp'],
}
}
现在,当我们使用--graph选项运行puppet apply或agent时,生成的图形不再有任何循环路径(环):

在这个图中,很容易看到Package[ntp]是第一个应用的资源,然后是File[/etc/ntp.conf],最后是Service[ntp]。
提示
如前所示的图形被称为有向无环图(DAG)。将资源减少为 DAG 可以确保 Puppet 在线性时间内计算所有顶点(资源)的最短路径。有关 DAG 的更多信息,请查看en.wikipedia.org/wiki/Directed_acyclic_graph。
还有更多……
即使在没有 bug 需要排查的情况下,资源和关系图也是非常有用的。例如,如果你有一个非常复杂的类和资源网络,研究资源图可以帮助你看到哪里可以简化。同样,当依赖关系变得过于复杂,无法从阅读清单中理解时,图形可以成为有用的文档形式。例如,一个图形会清晰地显示出哪些资源有最多的依赖关系,哪些资源被最多的其他资源所依赖。被大量资源依赖的资源会有许多箭头指向它们。
另见
- 第三章中的使用运行阶段配方,写更好的清单
理解 Puppet 错误
Puppet 的错误信息有时会让人感到困惑。如果你使用的是 3.0 版本之前的任何版本,更新和越来越有帮助的错误信息是升级 Puppet 安装的一个理由。
以下是一些你可能遇到的最常见错误,以及如何处理它们。
如何解决...
通常,第一步就是搜索网络,查看错误信息文本,并找到可能的解释,以及一些修复建议。以下是一些最常见的难解错误及其可能的解释:
Could not retrieve file metadata for XXX: getaddrinfo: Name or service not known
在XXX是文件资源的情况下,你可能在文件源中不小心输入了puppet://modules...而不是puppet:///modules...(注意三斜杠):
Could not evaluate: Could not retrieve information from environment production source(s) XXX
源文件可能不存在,或者在 Puppet 仓库中位置不正确:
Error: Could not set 'file' on ensure: No such file or directory XXX
文件路径可能指定了一个不存在的父目录(或多个目录)。你可以在 Puppet 中使用独立的文件资源来创建这些目录:
change from absent to file failed: Could not set 'file on ensure: No such file or directory
这通常是由于 Puppet 尝试将文件写入一个不存在的目录。检查该目录是否已存在,或者在 Puppet 中已定义,并确保文件资源要求该目录(以便目录始终先被创建):
undefined method 'closed?' for nil:NilClass
这个不太有用的错误信息大致可以翻译为出了点问题。它往往是由许多不同问题引起的一个通用错误,但你可以通过资源、类或模块的名称来确定出问题的原因。一个技巧是添加--debug开关,以获取更有用的信息:
[root@cookbook ~]# puppet agent -t --debug
如果你查看 Git 历史记录,查看最近更改的内容,这可能是找出问题所在的另一种方法:
Could not parse for environment --- "--- production": Syntax error at end of file at line 1
这可能是由于命令行选项拼写错误造成的,例如,如果你输入了puppet -verbose而不是puppet --verbose。这种错误很难被发现:
Duplicate definition: X is already defined in [file] at line Y; cannot redefine at [file] line Y
这个问题曾让我困惑一阵子。Puppet 报告重复定义的错误,通常如果你有两个相同名称的资源,Puppet 会帮助你指出它们被定义的位置。但在这个例子中,它却显示了相同的文件和行号。那么,怎么可能有一个资源是它自己重复定义呢?
答案是,如果它是一个已定义的类型(通过define关键字创建的资源)。如果你创建了一个已定义类型的两个实例,你将拥有所有资源的两个实例,而这些资源需要有不同的名称。例如:
define check_process() {
exec { 'is-process-running?':
command => "/bin/ps ax |/bin/grep ${name} >/tmp/pslist.${name}.txt",
}
}
check_process { 'exim': }
check_process { 'nagios': }
当我们运行 Puppet 时,出现了相同的错误两次:
t@mylaptop ~$ puppet apply duplicate.pp
Error: Duplicate declaration: Exec[is-process-running?] is already declared in file duplicate.pp:4; cannot redeclare at duplicate.pp:4 on node cookbook.example.com
Error: Duplicate declaration: Exec[is-process-running?] is already declared in file duplicate.pp:4; cannot redeclare at duplicate.pp:4 on node cookbook.example.com
由于exec资源被命名为is-process-running?,如果你尝试创建多个该定义的实例,Puppet 将拒绝,因为结果将是两个具有相同名称的exec资源。解决方案是在每个资源的标题中包含实例名称(或其他唯一值):
exec { "is-process-${name}-running?":
command => "/bin/ps ax |/bin/grep ${name} >/tmp/pslist.${name}.txt",
}
每个资源必须有一个唯一的名称,确保这一点的一个好方法是将${name}变量插入到标题中。注意,我们已经从单引号切换到双引号来定义资源标题:
"is-process-${name}-running?"
当你希望 Puppet 将变量的值插入字符串时,必须使用双引号。
另见
-
本章中的生成报告食谱
-
本章中的Noop: 不做任何更改选项食谱
-
本章中的记录调试信息食谱
检查配置设置
你可能知道 Puppet 的配置设置存储在puppet.conf中,但有很多参数,而那些没有在puppet.conf中列出的参数将采用默认值。那么,如何查看任何配置参数的值,无论它是否在puppet.conf中显式设置呢?答案是使用puppet config print命令。
如何做到这一点...
运行以下命令。这会产生大量输出(如果你希望浏览可用的配置设置,使用less进行分页可能会很有帮助):
[root@cookbook ~]# puppet config print |head -25
report_serialization_format = pson
hostcsr = /var/lib/puppet/ssl/csr_cookbook.example.com.pem
filetimeout = 15
masterhttplog = /var/log/puppet/masterhttp.log
pluginsignore = .svn CVS .git
ldapclassattrs = puppetclass
certdir = /var/lib/puppet/ssl/certs
ignoreschedules = false
disable_per_environment_manifest = false
archive_files = false
hiera_config = /etc/puppet/hiera.yaml
req_bits = 4096
clientyamldir = /var/lib/puppet/client_yaml
evaltrace = false
module_working_dir = /var/lib/puppet/puppet-module
tags =
cacrl = /var/lib/puppet/ssl/ca/ca_crl.pem
manifest = /etc/puppet/manifests/site.pp
inventory_port = 8140
ignoreimport = false
dbuser = puppet
postrun_command =
document_all = false
splaylimit = 1800
certificate_expire_warning = 5184000
它是如何工作的...
运行puppet config print将输出每个配置参数及其当前值(并且它们非常多)。
要查看特定参数的值,可以将其作为参数添加到puppet config print命令:
[root@cookbook ~]# puppet config print modulepath
/etc/puppet/modules:/usr/share/puppet/modules
另见
- 本章中的生成报告食谱





浙公网安备 33010602011771号