使用 Puppet 管理 Linux
在某些时候,您可能在服务器或桌面 PC 上安装或配置过软件。既然您阅读《Linux Journal》,您可能已经做了很多这样的事情,并且开发了一系列粘合 shell 脚本、Perl 代码片段和 cron 任务。
除非您比我更有条理,否则每台服务器都有一个独特的、手工制作的配置文件和脚本版本。它可能就像一个简单的备份监控脚本,但每个脚本仍然需要管理和安装。
安装新服务器通常涉及从另一台服务器复制配置文件和粘合脚本,直到它们“工作”。如果特定条件不经常出现,则可能存在细微的问题。任何改进通常都是在特定机器上临时进行的,并且没有简单的方法将改进应用于所有服务器或桌面。
最后,在典型场景中,投入到这些脚本和配置文件中的所有学习和知识都分散在每个 Linux 系统上的文件系统中。这意味着没有简单的方法来了解任何软件是如何定制的。
如果您安装了一台服务器,并在三年后回来想知道您做了什么,或者管理一组桌面或私有云虚拟机,配置管理和 Puppet 可以帮助简化您的生活。
进入配置管理配置管理是解决此问题的一种方案。完整的解决方案提供了一个中央存储库,用于定义和记录事情是如何完成的,可以轻松且可重复地应用于任何系统。改进可以根据需要简单地推广到系统。结果是,一位管理员可以轻松管理大量服务器。
Puppet存在许多不同的 Linux(和其他平台)配置管理工具。Puppet 是最流行的工具之一,也是我在本文中介绍的工具。类似的工具包括 Chef、Ansible 和 Salt 以及许多其他工具。虽然它们在细节上有所不同,但总体目标是相同的。
Puppet 的基本理念是,您使用 Puppet 的编程语言告诉它您想要的最终结果(所需状态),而不是您希望如何完成它(过程)。例如,您可能会说“我希望 ssh 密钥 XYZ 能够登录到用户帐户 foo。”您不会说“将此字符串 cat 到 /home/foo/.ssh/authorized_keys。”事实上,我定义的简单过程甚至远非可靠或正确,因为 .ssh 目录可能不存在,权限可能不正确以及许多其他问题。
您可以使用 Puppet 的语言在名为清单的文件中声明您的需求,后缀为 .pp。您的清单使用 Puppet 的内置模块或您自己的自定义模块(也存储在清单文件中)声明机器(虚拟或真实)的需求。Puppet 由此清单集合驱动,很像程序是从代码构建的一样。当运行 puppet apply
命令时,Puppet 将编译程序,确定机器状态与所需状态的差异,然后进行任何必要的更改,使机器与需求保持一致。
这种方法意味着,如果您在与当前清单保持同步的机器上运行 puppet apply
,则不应发生任何事情,因为没有要进行的更改。
Puppet 是一个工具(实际上是一整套工具),包括 Puppet 执行程序、Puppet master、Puppet 数据库和 Puppet 系统信息实用程序。有很多不同的使用方法,适合不同的环境。
在本文中,我将解释 Puppet 的基础知识以及我们如何使用它来管理我们的服务器和桌面,以简化的形式。我使用术语“机器”来指代桌面、虚拟机和虚拟机监控程序主机。
我在此处概述的方法非常适用于 1-100 台相当相似但在各个方面有所不同的机器。如果您正在管理一个由 1,000 台虚拟机组成的云,这些虚拟机是相同的或以非常可预测的方式不同,则此方法未针对这种情况进行优化(并且您应该为下一期《Linux Journal》撰写一篇文章)。
此方法基于 John Arundel 出色的著作《Puppet 3 Beginners Guide》中概述的思想。基本思想是这样的
-
将您的 Puppet 清单存储在 git 中。这提供了一种管理、跟踪和分发更改的好方法。我们也将其用作服务器获取其清单的方式(我们不使用 Puppet master)。您可以轻松地使用 Subversion、Mercurial 或任何其他 SCM。
-
为每台机器使用单独的 git 分支,以使机器保持稳定。
-
然后,每台机器定期轮询 git 存储库,并在有任何更改时运行
puppet apply
。 -
每台机器都有一个清单文件,用于定义所需状态。
就本文而言,我将使用配置开发人员桌面的示例。示例桌面机器是一个干净的 Ubuntu 12.04,主机名为 puppet-test;但是,任何版本的 Linux 都应该可以工作,几乎没有区别。我将使用私有 git 服务器上的空 git 存储库进行工作。如果您要为此使用 GitHub,请勿在那里放置任何敏感信息,尤其是密钥或密码。
Puppet 安装在目标机器上,使用的命令如清单 1 所示。安装只是设置 Puppet Labs 存储库并安装 git 和 Puppet。请注意,我使用了特定版本的 puppet-common
和 puppetlabs/apt
模块。不幸的是,我发现 Puppet 倾向于破坏以前有效的代码,甚至在小版本升级时也会破坏其自身的模块。因此,我的所有机器都锁定到特定版本,并且以受控方式进行升级。
wget https://apt.puppetlabs.com/puppetlabs-release-precise.deb
dpkg -i puppetlabs-release-precise.deb
apt-get update
apt-get install -y man git puppet-common=3.7.3-1puppetlabs1
puppet module install puppetlabs/apt --version 1.8.0
现在 Puppet 已安装,让我们用它做一些事情。
开始入门我通常在我的桌面上编辑清单,然后将其提交到 git 并推送到 origin 存储库。我已经将我的 存储库 上传到 GitHub 作为方便参考,您可能希望复制、fork 等等。
在您的 git 存储库中,创建文件 manifests/puppet-test.pp,如清单 2 所示。此文件说明了几个要点
-
文件名与主机名匹配。这不是必需的;它只是有助于组织您的清单。
-
它导入了 apt 包,这是一个允许您操作已安装软件的模块。
-
顶级项是“node”,这意味着它定义了服务器的状态。
-
节点名称是“puppet-test”,它与服务器名称匹配。这是 Puppet 确定应用此特定节点的方式。
-
清单声明它想要安装 vim 包,并且 emacs 包不存在。让火焰战争开始吧!
include apt
node 'puppet-test' {
package { 'vim':
ensure => 'present'
}
package { 'emacs':
ensure => 'absent'
}
}
现在您可以在机器本身上使用此 Puppet 配置。如果您 ssh
进入机器(您可能需要 ssh -A agent
转发,以便您可以向 git 验证身份),您可以运行清单 3 中的命令,将 gitserver
替换为您自己的服务器。
git clone git@gitserver:Puppet-LinuxJournal.git
↪/etc/puppet/linuxjournal
puppet apply /etc/puppet/linuxjournal/manifests
↪--modulepath=/etc/puppet/linuxjournal/
↪modules/:/etc/puppet/modules/
此代码将 git 存储库克隆到 /etc/puppet/linuxjournal,然后使用自定义清单目录运行 puppet apply
命令。puppet apply
命令查找具有匹配名称的节点,然后尝试使机器的状态与该节点中指定的状态匹配。在这种情况下,这意味着安装 vim(如果尚未安装)并删除 emacs。
创建开发人员用户会很好,这样您就可以设置该配置。清单 4 显示了一个更新的 puppet-test.pp,它按照 developer 变量创建用户(这不是一个好方法,但为了本示例而这样做)。请注意,变量前面带有 $。此外,变量被替换为使用 " 而不是 " 引号引起来的字符串中,方式与 bash 相同。
清单 4. /manifests/puppet-test.pp
include apt
node 'puppet-test' {
$developer = 'david'
package { 'vim':
ensure => 'present'
}
package { 'emacs':
ensure => 'absent'
}
user { "$developer":
ensure => present,
comment => "Developer $developer",
shell => '/bin/bash',
managehome => true,
}
}
让我们通过拉取更改并按照清单 5 重新运行 puppet apply
,在桌面上应用新更改。您现在应该创建了一个新用户。
cd /etc/puppet/linuxjournal
git pull
puppet apply /etc/puppet/linuxjournal/manifests
↪--modulepath=/etc/puppet/linuxjournal/
↪modules/:/etc/puppet/modules/
创建模块
将所有这些代码放在节点内部不是很可重用。让我们将用户移动到 developer_pc
模块中,并从您的节点调用该模块。为此,在 git 存储库中创建文件 modules/developer_pc/manifests/init.pp,如清单 6 所示。这将创建一个名为 developer_pc
的新模块,该模块接受一个名为 developer name
的参数,并使用它来定义用户。
class developer_pc ($developer) {
user { "$developer":
ensure => present,
comment => "Developer $developer",
shell => '/bin/bash',
managehome => true,
}
}
然后,您可以在您的节点中使用该模块,如清单 7 所示。请注意您如何传递 developer
参数,然后在模块内部可以访问该参数。
node 'puppet-test' {
package { 'vim':
ensure => 'present'
}
package { 'emacs':
ensure => 'absent'
}
class { 'developer_pc': developer => 'david' }
}
再次应用更改,应该没有任何变化。您所做的只是重构了代码。
创建静态文件假设您想要标准化所有开发人员的 vim 配置,并通过设置他们的 .vimrc 文件来停止自动换行。要在 Puppet 中执行此操作,请在 /modules/developer_pc/files/vimrc 中创建要使用的文件,如清单 8 所示,然后在 /modules/developer_pc/manifests/init.pp 中添加文件资源,如清单 9 所示。文件资源可以紧挨着用户资源放置。
清单 8. /modules/developer_pc/files/vimrc
# Managed by puppet in developer_pc
set nowrap
清单 9. /modules/developer_pc/manifests/init.pp
file { "/home/$developer/.vimrc":
source => "puppet:///modules/developer_pc/vimrc",
owner => "$developer",
group => "$developer",
require => [ User["$developer"] ]
}
file
资源定义了一个文件 /home/$developer/.vimrc,它将从您之前创建的 vimrc 文件中设置。您还设置了该文件的所有者和组,因为 Puppet 通常以 root 身份运行。
文件上的 require
子句接受资源数组,并声明必须先处理这些资源,然后才能处理此文件(请注意大写的首字母;这是 Puppet 引用资源而不是声明资源的方式)。此依赖项使您可以阻止 Puppet 在用户创建之前尝试创建 .vimrc 文件。当资源相邻时,例如用户和文件,它们也可以使用 ->
运算符“链接”在一起。
再次应用更改,您现在可以期望看到您的自定义 .vimrc 设置完成。如果您稍后运行 puppet apply
,如果源 vimrc 文件没有更改,则 .vimrc 文件也不会更改,包括修改日期。如果其中一位开发人员更改了 .vimrc,则下次运行 puppet apply
时,它将被还原为 Puppet 中的版本。
稍后,假设其中一位开发人员询问他们是否也可以在 vim 中搜索时忽略大小写。您可以轻松地将其推广到所有桌面。只需更改 vimrc 文件以包含 set ignorecase
,提交并在每台机器上运行 puppet apply
。
通常您会想要创建内容是动态的文件。Puppet 支持 .erb 模板,这些模板是包含 Ruby 代码片段的模板,类似于 jsp 或 php 文件。代码可以访问 Puppet 中的所有变量,语法略有不同。
例如,我们的构建过程使用一个名为 $HOME/Projects/override.properties 的文件,其中包含构建根目录的名称。这通常只是用户的 home 目录。您可以使用 .erb 模板在 Puppet 中设置此项,如清单 10 所示。erb 模板与静态文件非常相似,除了它需要位于模板文件夹中,并且它使用 <%= %>
表示表达式,<% %>
表示代码,变量使用 @
前缀引用。
# Managed by Puppet
dir.home=/home/<%= @developer %>/
您可以使用清单 11 中显示的规则来使用 .erb 模板。首先,您必须确保存在 Projects 目录,然后您需要 override.properties 文件本身。->
运算符用于确保您先创建目录,然后再创建文件。
file { "/home/$developer/Projects":
ensure => 'directory',
owner => "$developer",
group => "$developer",
require => [ User["$developer"] ]
}
->
file { "/home/$developer/Projects/override.properties":
content => template('developer_pc/override.properties.erb'),
owner => "$developer",
group => "$developer",
}
自动运行 Puppet
每次想要进行更改时都运行 Puppet,对于少量机器来说效果不佳。为了解决这个问题,您可以让每台机器自动检查 git 的更改,然后运行 puppet apply
(您可以仅在 git 更改时执行此操作,但这是可选的)。
接下来,您将定义一个名为 puppetApply.sh 的文件,该文件执行您想要的操作,然后设置一个 cron 作业,每十分钟调用一次它。这是在一个名为 puppet_apply 的新模块中分三个步骤完成的
-
在 modules/puppet_apply/files/puppetApply.sh 中创建您的 puppetApply.sh 模板,如清单 12 所示。
-
创建 puppetApply.sh 文件并设置 crontab 条目,如清单 13 所示。
-
从您的节点在 puppet-test.pp 中使用您的
puppet_apply
模块,如清单 14 所示。
# Managed by Puppet
cd /etc/puppet/linuxjournal
git pull
puppet apply /etc/puppet/linuxjournal/manifests
↪--modulepath=/etc/puppet/linuxjournal/modules/
↪:/etc/puppet/modules/
清单 13. /modules/puppet_apply/manifests/init.pp
class puppet_apply () {
file { "/usr/local/bin/puppetApply.sh":
source => "puppet:///modules/puppet_apply/puppetApply.sh",
mode => 'u=wrx,g=r,o=r'
}
->
cron { "run-puppetApply":
ensure => 'present',
command => "/usr/local/bin/puppetApply.sh >
↪/tmp/puppetApply.log 2>&1",
minute => '*/10',
}
}
清单 14. /manifests/puppet-test.pp
class { 'puppet_apply': ; }
您将需要确保服务器具有对 git 存储库的读取访问权限。您可以使用通过 Puppet 分发的 SSH 密钥和 /root/.ssh/config 中的 IdentityFile
条目来执行此操作。
如果您现在应用更改,您应该看到 root 的 crontab 中有一个条目,并且每十分钟 puppetApply.sh 应该运行一次。现在您只需将您的更改提交到 git,并在十分钟内,它们将被推广。
修改配置文件很多时候,您不想替换配置文件,而是要确保某些选项设置为某些值。例如,我可能想要将 SSH 端口从默认的 22 更改为 2022,并禁止密码登录。与其使用 Puppet 管理整个配置文件,我可以使用 augeas
资源来设置多个配置选项。
请参阅清单 15,了解可以添加到您之前创建的 developer_pc
类中的一些代码。代码执行三件事
-
安装 openssh-server(不是真正必需的,但为了完整性而存在)。
-
确保 SSH 作为服务运行。
-
在 /etc/ssh/sshd_config 中设置
Port 2022
和PasswordAuthentication no
。 -
如果文件更改,
notify
子句会导致 SSH 重新加载配置。
package { 'openssh-server':
ensure => 'present'
}
service { 'ssh':
ensure => running,
require => [ Package["openssh-server"] ]
}
augeas { 'change-sshd':
context => '/files/etc/ssh/sshd_config',
changes => ['set Port 2022', 'set PasswordAuthentication no'],
notify => Service['ssh'],
require => [ Package["openssh-server"] ]
}
一旦 puppetApply.sh 自动运行,任何后续的 SSH 会话都需要在端口 2022 上连接,并且您将无法再使用密码。
删除规则在 Puppet 中定义规则时,请务必记住,删除资源的规则与删除该资源的规则不同。例如,假设您有一个规则,用于为“developerA”创建一个授权的 SSH 密钥。稍后,“developerA”离开了,因此您删除了定义密钥的规则。不幸的是,这不会从 authorized_keys
中删除该条目。在大多数情况下,Puppet 资源中定义的状态不被认为是最终状态;允许在 Puppet 之外进行更改。因此,一旦删除了 developerA 密钥的规则,就无法知道它是手动添加的还是 Puppet 应该删除它。
在这种情况下,您可以使用 ensure => 'absent'
规则来确保软件包、文件、目录、用户等被删除。原始清单 1 显示了一个示例,说明如何使用此规则删除 emacs 软件包。确保 emacs 不存在与没有规则声明之间存在明显的差异。
在我们的办公室,当开发人员或管理员离开时,我们会将其 SSH 密钥替换为无效密钥,然后立即更新该开发人员的每个条目。
现有模块Puppet Forge 上列出了许多模块,几乎涵盖了所有可以想象到的问题。有些非常好,有些则不太好。始终值得搜索一下是否有好的东西,然后决定是最好定义自己的模块还是重用现有模块。
管理 Git我们不会让所有机器都停留在 master 分支上。我们使用修改后的 gitflow 方法来管理我们的存储库。每台服务器都有自己的分支,其中大多数都指向 master。少数位于 develop 分支的前沿。我们会定期将新版本从 develop 滚动到 master,然后将每台机器的分支从旧版本向前移动到新版本。为每台服务器保留单独的分支可以灵活地将特定服务器保留在后面,并确保更改不会以临时方式推广到服务器。
我们使用脚本来管理我们所有的分支,并将它们快进到新版本。对于大约 100 台机器,它对我们来说很有效。在更大的规模上,每台服务器的单独分支可能是不切实际的。
使用与所有服务器共享的单个存储库并不理想。将敏感信息加密存储在 Hiera 中是一个好主意。2015 年 3 月刊的《Linux Journal》中有一篇关于此的优秀文章:“将 Hiera 与 Puppet 结合使用”,作者是 Scott Lackey。
随着您的机器数量的增长,使用单个 git 存储库可能会成为一个问题。对我们来说,主要问题是可重用模块与机器特定配置之间存在大量的“提交噪音”。其次,您可能不希望所有管理员都能够编辑所有模块或机器清单,或者您可能不希望所有清单都推广到每台机器。我们的解决方案是使用多个存储库,一个用于通用模块,一个用于机器/客户特定的配置,另一个用于全局信息。这使我们的核心模块保持分离并处于适当的版本管理之下,同时也使我们能够轻松发布关键的全局更改。
向上扩展/权衡本文中概述的方法对我们来说效果很好。我希望它对您也有效;但是,您可能需要考虑一些其他要点。
由于我们的服务器在不一致的方面有所不同,因此使用 Facter 或元数据来驱动配置不适合我们。但是,如果您有 100 台 Web 服务器,则使用 nginx-prod-099 的主机名来确定安装要求将节省大量时间。
很多人使用 Puppet master 来推广和推送更改,这是许多在线教程中提到的一般方法。您可以将其与 PuppetDB 结合使用,以在机器之间共享信息——例如,一台服务器的公钥可以共享到另一台服务器。
结论本文仅触及了使用 Puppet 可以完成的工作的表面。几乎所有关于您的机器都可以使用各种 Puppet 内置资源或模块进行管理。在短期使用后,您将体验到使用几个命令构建第二台服务器或在几分钟内将更改推广到多台服务器的简易性。
一旦您可以如此轻松地跨服务器进行更改,尽可能好地构建事物就会变得更有价值。例如,监视您的 cron 作业和备份可能比实际任务本身花费更多的工作,但是通过配置管理,您可以构建一个可重用的模块,然后将其用于所有事物。
对我而言,Puppet 已将系统管理从一项苦差事转变为一项有益的活动,因为它为您带来了巨大的杠杆作用。试一试;一旦您这样做,您将永远不会回头!