Puppet 是一款用 Ruby 编写的开源 IT 管理工具,用于 Google、Twitter、纽约证券交易所等机构的数据中心自动化和服务器管理。它主要由 Puppet Labs 维护,该公司也是该项目的创始者。Puppet 可以管理少至 2 台机器,多至 50,000 台机器,团队可以是一个系统管理员,也可以是数百个系统管理员。
Puppet 是一种用于配置和维护计算机的工具;在它的简单配置语言中,您可以向 Puppet 解释您希望机器如何配置,它会根据需要更改它们以匹配您的规范。随着时间的推移,您更改了规范(例如,包更新、新用户或配置更新),Puppet 会自动更新您的机器以匹配。如果它们已经按预期配置,则 Puppet 不会执行任何操作。
通常,Puppet 会尽其所能使用现有的系统功能来完成工作;例如,在 Red Hat 上,它将使用 yum
来管理包,使用 init.d
来管理服务,但在 OS X 上,它将使用 dmg
来管理包,使用 launchd
来管理服务。Puppet 的指导目标之一是使它所做的工作无论您是在查看 Puppet 代码还是系统本身,都能让人理解,因此遵循系统标准至关重要。
Puppet 来自多种其他工具的传统。在开源世界中,它受 CFEngine 的影响最大,CFEngine 是第一个开源通用配置工具,而 ISconf 使用 make
来完成所有工作,这启发了 Puppet 将显式依赖关系作为整个系统中的一个重点。在商业领域,Puppet 是对 BladeLogic 和 Opsware(后来都被更大的公司收购)的回应,当 Puppet 开始开发时,这两家公司在市场上都取得了成功,但它们都专注于向大型公司的管理人员销售产品,而不是直接为系统管理员构建伟大的工具。Puppet 的目标是解决与这些工具类似的问题,但它专注于一个非常不同的用户。
以下是一段关于如何使用 Puppet 的简单示例代码,它可以确保安全 shell 服务 (SSH) 正确安装和配置。
class ssh { package { ssh: ensure => installed } file { "/etc/ssh/sshd_config": source => 'puppet:///modules/ssh/sshd_config', ensure => present, require => Package[ssh] } service { sshd: ensure => running, require => [File["/etc/ssh/sshd_config"], Package[ssh]] } }
这将确保包已安装,文件已到位,并且服务正在运行。请注意,我们在资源之间指定了依赖关系,因此我们始终按正确的顺序执行任何工作。然后,此类可以与任何主机关联,以将此配置应用于该主机。请注意,Puppet 配置的构建块是结构化的对象,在本例中是 package
、file
和 service
。在 Puppet 中,我们称这些对象为资源,Puppet 配置中的所有内容都归结为这些资源以及它们之间的依赖关系。
一个正常的 Puppet 站点将包含数十甚至数百个这些代码片段,我们称之为类;我们将这些类存储在磁盘上的文件中,这些文件称为 manifests
,并将它们收集到称为模块的相关组中。例如,您可能有一个 ssh
模块,其中包含此 ssh
类以及任何其他相关类,以及 mysql
、apache
和 sudo
模块。
大多数 Puppet 交互都是通过命令行或长时间运行的 HTTP 服务进行的,但对于某些事情(如报告处理)存在图形界面。Puppet Labs 还围绕 Puppet 生产商业产品,这些产品往往更倾向于基于 Web 的图形界面。
Puppet 的第一个原型是在 2004 年夏天编写的,并在 2005 年 2 月成为全职重点。它最初由 Luke Kanies 设计和编写,他是一位系统管理员,拥有丰富的编写小型工具的经验,但没有编写超过 10,000 行代码的工具的经验。从本质上说,Luke 在编写 Puppet 的过程中学会了如何成为一名程序员,这从其架构中显而易见,既有积极的方面,也有消极的方面。
Puppet 最初的目的是成为系统管理员的工具,使他们的生活更轻松,并使他们能够更快、更高效地工作,并减少错误。第一个旨在实现这一目标的关键创新是上面提到的资源,它们是 Puppet 的基元;它们既可以在大多数操作系统上移植,又能抽象出实现细节,允许用户专注于结果,而不是如何实现结果。这套基元是在 Puppet 的资源抽象层中实现的。
Puppet 资源在一个给定主机上必须是唯一的。您只能有一个名为“ssh”的包、一个名为“sshd”的服务和一个名为“/etc/ssh/sshd_config”的文件。这可以防止配置的不同部分相互冲突,而且您会在配置过程的早期阶段发现这些冲突。我们通过它们的类型和标题来引用这些资源;例如,Package[ssh]
和 Service[sshd]
。您可以拥有一个名称相同但类型不同的包和服务,但不能拥有两个名称相同的包或服务。
Puppet 中的第二个关键创新提供了直接指定资源之间依赖关系的能力。以前的工具侧重于要完成的单个工作,而不是各个工作之间的关系;Puppet 是第一个明确说明依赖关系是配置的第一级部分,必须以这种方式建模的工具。它构建了一个资源及其依赖关系的图,作为核心数据类型之一,本质上 Puppet 中的所有内容都挂在该图(称为目录)及其顶点和边上。
Puppet 中的最后一个主要组件是它的配置语言。这种语言是声明式的,旨在更多地用作配置数据,而不是完整的编程语言——它最类似于 Nagios 的配置格式,但也受到 CFEngine 和 Ruby 的很大影响。
除了功能组件之外,Puppet 在其开发过程中一直遵循两项指导原则:它应该尽可能简单,始终优先考虑可用性,即使以牺牲功能为代价;它应该首先构建一个框架,然后才是应用程序,以便其他人可以根据需要在 Puppet 的内部机制上构建自己的应用程序。人们了解到,Puppet 的框架需要一个杀手级应用程序才能被广泛采用,但重点始终是框架,而不是应用程序。大多数人认为 Puppet 是那个应用程序,而不是它背后的框架。
当 Puppet 的原型首次构建时,Luke 本质上是一位精通 Perl 的程序员,拥有丰富的 shell 经验和一些 C 经验,主要在 CFEngine 中工作。奇怪的是,他拥有构建简单语言解析器的经验,他曾在较小的工具中构建了两个解析器,并且还从头重写了 CFEngine 的解析器,以使其更容易维护(由于存在一些小问题,这段代码从未提交到该项目中)。
基于更高的开发人员生产力和上市时间,Puppet 的实现很容易决定使用动态语言,但选择语言却很困难。最初用 Perl 编写的原型无济于事,因此人们开始寻找其他语言进行实验。人们尝试过 Python,但 Luke 发现这种语言与他的思维方式很不协调。基于从朋友那里听到的一个关于效用的传言,Luke 尝试了 Ruby,并在四个小时内构建了一个可用的原型。当 Puppet 在 2005 年成为全职工作时,Ruby 还是一个完全未知的语言,因此决定坚持使用它是一个很大的风险,但再次,程序员的生产力被认为是语言选择的首要驱动因素。Ruby 的主要区别特征,至少与 Perl 相比,是构建非层次结构类关系的容易程度,但它也与 Luke 的大脑非常匹配,事实证明这一点至关重要。
本章主要介绍 Puppet 实现的架构(即我们用来使 Puppet 执行其应执行的操作的代码),但它值得简要讨论其应用程序架构(即各个部分如何通信),以便实现可以理解。
Puppet 是在两种模式下构建的:一种是使用中央服务器和在独立主机上运行的代理的客户端/服务器模式,另一种是无服务器模式,其中单个进程执行所有工作。为了确保这些模式之间的一致性,Puppet 在内部始终具有网络透明性,因此无论它们是通过网络运行还是不通过网络运行,两种模式都使用相同的代码路径。每个可执行文件可以根据需要配置本地或远程服务访问,但除此之外,它们的行为完全相同。请注意,您也可以在相当于客户端/服务器配置的无服务器模式下使用 Puppet,方法是将所有配置文件拉到每个客户端并让它直接解析它们。本节将重点介绍客户端/服务器模式,因为它更容易理解为独立组件,但请记住,这对于无服务器模式也是正确的。
Puppet 应用程序架构中的一个决定性选择是,客户端不应该访问原始 Puppet 模块;相反,它们应该获得专门为它们编译的配置。这带来了多种好处:首先,您遵循最小特权原则,即每个主机只知道它需要知道的内容(它应该如何配置),但它不知道任何其他服务器是如何配置的。其次,您可以完全分离编译配置所需的权限(可能包括访问中央数据存储)与应用该配置所需的权限。第三,您可以在断开连接的模式下运行主机,在该模式下,主机反复应用配置,而无需与中央服务器联系,这意味着即使服务器关闭或客户端断开连接(例如,在移动安装中,或当客户端在 DMZ 中时),您也可以保持合规性。
鉴于此选择,工作流程变得相对简单。
因此,代理可以访问其自己的系统信息、配置和它生成的每个报告。服务器拥有所有这些数据的副本,以及访问所有 Puppet 模块,以及编译配置可能需要的任何后端数据库和服务。
除了构成此工作流程的组件之外,我们将在下文中进行讨论,Puppet 还使用多种数据类型进行内部通信。这些数据类型至关重要,因为它们是所有通信的方式,并且是任何其他工具都可以使用或生成 的公开类型。
最重要的数据类型是
除了事实、清单、目录和报告之外,Puppet 还支持用于文件、证书(它用于身份验证)和其他内容的数据类型。
Puppet 运行中遇到的第一个组件是 agent
进程。它传统上是一个名为 puppetd
的独立可执行文件,但在版本 2.6 中,我们将它简化为一个可执行文件,因此现在它使用 puppet agent
调用,类似于 Git 的工作方式。代理本身的功能很少;它主要是配置和代码,用于实现上述工作流程中的客户端方面。
在代理之后,下一个组件是名为 Facter 的外部工具,这是一个非常简单的工具,用于发现它正在运行的主机的信息。这些数据包括操作系统、IP 地址和主机名,但 Facter 很容易扩展,因此许多组织添加了自己的插件来发现自定义数据。代理将 Facter 发现的数据发送到服务器,此时服务器接管工作流程。
在服务器上,遇到的第一个组件是我们所说的外部节点分类器,或 ENC。ENC 接受主机名并返回一个简单的数据结构,其中包含该主机的顶层配置。ENC 通常是一个单独的服务或应用程序:另一个开源项目(如 Puppet Dashboard 或 Foreman)或与现有数据存储(如 LDAP)的集成。ENC 的目的是指定给定主机属于哪些功能类,以及应使用哪些参数来配置这些类。例如,给定主机可能属于 debian
和 webserver
类,并且参数 datacenter
设置为 atlanta
。
请注意,从 Puppet 2.7 开始,ENC 不是必需组件;用户可以改为在 Puppet 代码中直接指定节点配置。对 ENC 的支持是在 Puppet 发布两年后添加的,因为我们意识到对主机进行分类与配置它们本质上是不同的,将这些问题分成单独的工具比扩展语言以支持这两种功能更有意义。ENC 始终推荐使用,并且很快将成为必需组件(届时 Puppet 将附带一个足够有用的 ENC,因此该要求不会成为负担)。
一旦服务器从 ENC 接收分类信息,并从 Facter(通过代理)接收系统信息,它会将所有信息捆绑到一个 Node 对象中,并将其传递给编译器。
如上所述,Puppet 拥有一种用于指定系统配置的自定义语言。它的编译器实际上是三个部分:一个 Yacc 风格的解析器生成器和一个自定义词法分析器;一组用于创建抽象语法树 (AST) 的类;以及 Compiler 类,它处理所有这些类的交互,并充当系统这部分的 API。
编译器最复杂的部分是,大多数 Puppet 配置代码在首次引用时才惰性加载(以减少加载时间和与丢失但不需要的依赖项相关的无关日志记录),这意味着没有真正明确调用加载和解析代码。
Puppet 的解析器使用一个正常的 Yacc 风格的解析器生成器,它使用开源 Racc 工具构建。不幸的是,在 Puppet 开始时没有开源词法分析器生成器,因此它使用自定义词法分析器。
由于我们在 Puppet 中使用 AST,因此 Puppet 语法中的每个语句都评估为一个 Puppet AST 类的实例(例如,Puppet::Parser::AST::Statement
),而不是直接采取行动,并且这些 AST 实例在语法树减少时被收集到一个树中。此 AST 在单个服务器为许多不同节点编译配置时提供了性能优势,因为我们可以解析一次但编译多次。它还使我们有机会对 AST 进行一些自省,这为我们提供了在解析直接操作的情况下无法获得的信息和功能。
在 Puppet 开始时,很少有易于理解的 AST 示例可用,因此它经历了许多演变,并且我们最终得到了一个似乎相当独特的公式。我们不是为整个配置创建一个 AST,而是创建了许多小的 AST,并根据它们的名字进行键控。例如,这段代码
class ssh { package { ssh: ensure => present } }
创建一个包含单个 Puppet::Parser::AST::Resource
实例的新 AST,并将该 AST 存储在特定环境的所有类的哈希表中,名为“ssh”。(我省略了关于类似于类的其他构造的详细信息,但这些对于本次讨论来说是不必要的。)
在给定 AST 和 Node 对象(来自 ENC)的情况下,编译器会查找节点对象中指定的类(如果有),并评估它们。在此评估过程中,编译器正在构建一个变量作用域树;每个类都有自己的作用域,该作用域附加到创建的作用域。这相当于 Puppet 中的动态作用域:如果一个类包含另一个类,那么被包含的类可以直接在包含的类中查找变量。这始终是一场噩梦,我们一直在努力摆脱这种能力。
作用域树是临时的,并在编译完成后被丢弃,但是编译的工件也是在编译过程中逐渐构建的。我们称此工件为 Catalog,但它只是一个资源及其关系的图。变量、控制结构或函数调用都不会保留在 Catalog 中;它只是纯数据,可以很容易地转换为 JSON、YAML 或几乎任何其他内容。
在编译期间,我们创建包含关系;一个类“包含”所有来自该类的资源(例如,上面的 ssh 包被 ssh 类包含)。一个类可能包含一个定义,该定义本身包含更多定义或单个资源。Catalog 往往是一个非常水平的、断开连接的图:许多类,每个类不超过两层深度。
此图的尴尬之处之一是,它还包含“依赖关系”,例如服务需要一个包(可能因为包安装实际上创建了服务),但这些依赖关系实际上是在资源上的参数值中指定的,而不是作为图结构中的边。我们的图类(出于历史原因称为 SimpleGraph)不支持在同一个图中同时包含包含边和依赖边,因此我们必须在各种目的之间进行转换。
一旦 Catalog 完全构建(假设没有失败),它就会传递给 Transaction。在一个具有单独客户端和服务器的系统中,Transaction 在客户端运行,客户端通过 HTTP 拉取 Catalog,如 图 18.2 所示。
Puppet 的事务类提供了实际影响系统的框架,而我们讨论的所有其他内容只是构建和传递对象。与数据库等更常见系统中的事务不同,Puppet 事务没有原子性等行为。
事务执行一个相对简单的任务:按各种关系指定的顺序遍历图,并确保每个资源都同步。如上所述,它必须将图从包含边(例如,Class[ssh]
包含 Package[ssh]
和 Service[sshd]
)转换为依赖边(例如,Service[sshd]
依赖 Package[ssh]
),然后对图执行标准的拓扑排序,依次选择每个资源。
对于给定资源,我们执行一个简单的三步过程:检索该资源的当前状态,将其与所需状态进行比较,并进行任何必要的更改以解决差异。例如,给定这段代码
file { "/etc/motd": ensure => file, content => "Welcome to the machine", mode => 644 }
事务检查 /etc/motd 的内容和模式,如果它们与指定的状态不匹配,它将修复其中一个或两个。如果 /etc/motd 以某种方式成为目录,那么它将备份该目录中的所有文件,将其删除,并用具有适当内容和模式的文件替换它。
进行这些更改的过程实际上由一个简单的 ResourceHarness 类处理,该类定义了 Transaction 和 Resource 之间的整个接口。这减少了类之间的连接数量,并且使对两者独立进行更改变得更容易。
Transaction 类是使用 Puppet 完成工作的核心,但是所有工作实际上都是由资源抽象层 (RAL) 完成的,从架构的角度来看,它也是 Puppet 中最有趣的组件。
RAL 是 Puppet 中创建的第一个组件,除了语言之外,它最清楚地定义了用户可以做什么。RAL 的工作是定义成为资源的含义以及资源如何在系统上完成工作,Puppet 的语言专门构建为指定 RAL 建模的资源。因此,它也是系统中最重要的组件,也是最难改变的组件。我们希望在 RAL 中修复很多东西,并且多年来对它进行了很多关键改进(最重要的是添加了 Provider),但长期来看,RAL 还有很多工作要做。
在编译器子系统中,我们使用单独的类(方便地命名为 Puppet::Resource
和 Puppet::Resource::Type
)来模拟资源和资源类型。我们的目标是让这些类也成为 RAL 的核心,但是现在这两种行为(资源和类型)是在一个类 Puppet::Type
中模拟的。(该类命名不当,因为它明显早于我们使用“资源”一词,并且当时我们在主机之间通信时直接序列化内存结构,因此更改类名实际上相当复杂。)
当 Puppet::Type
首次创建时,将资源和资源类型行为放在同一个类中似乎是合理的;毕竟,资源只是资源类型的实例。然而,随着时间的推移,很明显,资源与其资源类型之间的关系在传统的继承结构中没有得到很好的模拟。资源类型定义了资源可以具有的参数,但不定义它是否接受参数(它们都接受),例如。因此,我们的 Puppet::Type
基类具有确定资源类型行为的类级别行为,以及确定资源实例行为的实例级别行为。它还负责管理资源类型的注册和检索;如果您需要“user”类型,则调用 Puppet::Type.type(:user)
。
这种行为混合使 Puppet::Type
非常难以维护。整个类不到 2,000 行代码,但在三个级别(资源、资源类型和资源类型管理器)工作,使其变得复杂。这显然是它成为主要重构目标的原因,但它比面向用户的代码更像管道,因此始终难以证明在这里而不是直接在功能上投入精力是合理的。
除了 Puppet::Type
之外,RAL 中还有两种主要的类,其中最有趣的是我们所说的 Provider。当 RAL 首次开发时,每个资源类型将参数的定义与知道如何管理它的代码混合在一起。例如,我们将定义“content”参数,然后提供一个可以读取文件内容的方法,以及另一个可以更改内容的方法
Puppet::Type.newtype(:file) do ... newproperty(:content) do def retrieve File.read(@resource[:name]) end def sync File.open(@resource[:name], "w") { |f| f.print @resource[:content] } end end end
此示例被大大简化(例如,我们使用内部校验和,而不是完整的字符串内容),但您明白了意思。
随着我们需要支持给定资源类型的多种类型,这变得无法管理。Puppet 现在支持 30 多种类型的包管理,并且在单个 Package 资源类型中支持所有这些是不可能的。相反,我们提供了一个干净的接口,将资源类型的定义(本质上是资源类型的名称及其支持的属性)与如何管理该类型的资源区分开来。Provider 为所有资源类型的属性定义 getter 和 setter 方法,并以明显的方式命名。例如,这是上述属性的 provider 的外观
Puppet::Type.newtype(:file) do newproperty(:content) end Puppet::Type.type(:file).provide(:posix) do def content File.read(@resource[:name]) end def content=(str) File.open(@resource[:name], "w") { |f| f.print(str) } end end
在最简单的情况下,这需要多一些代码,但更容易理解和维护,尤其是当属性数量或 provider 数量增加时。
在本节开头,我提到过事务本身并不会直接影响系统,而是依赖于 RAL 来实现。现在很明显,实际工作是由提供者完成的。事实上,总的来说,提供者是 Puppet 中唯一真正接触系统的部分。事务请求一个文件的內容,提供者则收集它;事务指定要更改一个文件的內容,提供者则进行更改。但是,需要注意的是,提供者永远不会决定是否要影响系统——事务拥有决策权,提供者执行工作。这使得事务能够完全控制,而无需了解任何关于文件、用户或软件包的信息,这种分离使得 Puppet 能够拥有完整的模拟模式,从而在很大程度上保证系统不会受到影响。
RAL 中的第二个主要类类型负责参数本身。实际上,我们支持三种参数:元参数,影响所有资源类型(例如,是否应该在模拟模式下运行);参数,是不在磁盘上反映的值(例如,是否应该遵循文件中的链接);属性,是对资源的各个方面的建模,这些方面可以在磁盘上进行更改(例如,文件的內容,或者服务是否正在运行)。属性和参数之间的区别对人们来说尤其令人困惑,但是如果你仅仅将属性视为提供者中具有 getter 和 setter 方法的属性,就会变得相对直观。
当事务遍历图并使用 RAL 更改系统的配置时,它会逐步构建一个报告。这份报告主要包含由系统更改生成的事件。这些事件反过来是所完成工作的全面反映:它们保留了资源更改的时间戳、先前值、新值、生成的任何消息,以及更改是否成功或失败(或是否处于模拟模式)。
这些事件被包装在一个 ResourceStatus 对象中,该对象映射到每个资源。因此,对于给定的事务,您可以知道所有运行的资源,以及发生的任何更改,以及有关这些更改的所有可能需要的元数据。
事务完成后,会计算一些基本指标并存储在报告中,然后将其发送到服务器(如果配置了)。发送完报告后,配置过程就完成了,代理程序会进入休眠状态或进程会直接结束。
现在我们已经深入了解了 Puppet 的功能和工作原理,那么花点时间了解一下那些没有表现为功能但对完成任务至关重要的部分是很有意义的。
Puppet 的一大优点是它的可扩展性非常强。Puppet 中至少有 12 种不同的可扩展性,其中大多数旨在供任何人使用。例如,您可以为以下领域创建自定义插件:
但是,Puppet 的分布式特性意味着代理程序需要一种方法来检索和加载新的插件。因此,在每次 Puppet 运行开始时,我们首先要做的是下载服务器提供的所有插件。这些插件可能包括新的资源类型或提供者、新的事实,甚至新的报告处理器。
这使得即使不更改核心 Puppet 包,也可以对 Puppet 代理程序进行大量升级。这对高度定制的 Puppet 安装来说尤其有用。
您可能已经发现,我们有一种在 Puppet 中使用糟糕类名的传统,而根据大多数人的说法,这个类名是最糟糕的。Indirector 是一个相对标准的控制反转框架,具有很强的可扩展性。控制反转系统允许您将功能开发与控制使用哪种功能的方式分离。在 Puppet 的情况下,这使我们能够拥有许多提供不同功能的插件,例如通过 HTTP 访问编译器或在进程中加载它,并通过小的配置更改而不是代码更改在它们之间切换。换句话说,Puppet 的 Indirector 基本上是服务定位器的实现,正如在 "控制反转" 的维基百科页面中所描述的那样。所有从一个类到另一个类的传递都通过 Indirector 进行,通过一个标准的类似 REST 的接口(例如,我们支持 find、search、save 和 destroy 作为方法),而将 Puppet 从无服务器切换到客户端/服务器,主要是配置代理程序使用 HTTP 端点检索目录,而不是使用编译器端点。
因为它是一个控制反转框架,其中配置与代码路径严格分离,所以这个类也很难理解,特别是在调试为什么使用了特定代码路径时。
Puppet 的原型是在 2004 年夏天编写的,当时最大的网络问题是选择使用 XMLRPC 还是 SOAP。我们选择了 XMLRPC,它运行良好,但存在着与其他所有人相同的多数问题:它没有鼓励组件之间使用标准接口,而且由于它倾向于很快变得过于复杂,导致结果很糟糕。我们还遇到了严重的内存问题,因为 XMLRPC 所需的编码导致每个对象在内存中至少出现两次,对于大型文件来说,这很快就会变得很昂贵。
对于我们的 0.25 版本(从 2008 年开始),我们开始将所有网络切换到类似 REST 的模型,但我们选择了比仅仅更换网络更复杂的方式。我们开发了 Indirector 作为组件间通信的标准框架,并将 REST 端点作为一种选项。花费了两个版本才完全支持 REST,而且我们还没有完成将所有序列化都改为使用 JSON(而不是 YAML)。我们进行切换到 JSON 是出于两个主要原因:首先,YAML 处理 Ruby 非常慢,而纯 Ruby 处理 JSON 快得多;其次,网络似乎都在转向 JSON,而且它比 YAML 更易于移植。当然,在 Puppet 的情况下,YAML 的首次使用并不跨语言移植,而且常常不跨不同版本的 Puppet 移植,因为它本质上是内部 Ruby 对象的序列化。
我们下一代 Puppet 将最终删除所有 XMLRPC 支持。
在实现方面,我们最自豪的是 Puppet 中存在的各种分离:语言与 RAL 完全分离,事务不能直接接触系统,RAL 无法自行决定进行工作。这使应用程序开发人员能够对应用程序工作流程进行大量控制,并能访问有关正在发生的事情以及原因的大量信息。
Puppet 的可扩展性和可配置性也是其主要资产,因为任何人都可以很容易地在 Puppet 之上构建,而无需修改核心。我们始终使用与我们推荐用户使用的相同的接口来构建自己的功能。
Puppet 的简单性和易用性一直是其主要吸引力。它仍然很难运行,但比市场上的任何其他工具都要容易得多。这种简单性需要付出很多工程成本,特别是在维护和额外设计工作方面,但为了让用户能够专注于自己的问题而不是工具,这是值得的。
Puppet 的可配置性是一个真正的功能,但我们有点过头了。你可以用太多种方法将 Puppet 连接在一起,而且很容易在 Puppet 之上构建一个会让你痛苦不堪的工作流程。我们近期主要目标之一是大幅减少可以在 Puppet 配置中使用的旋钮数量,这样用户就不能那么容易地错误配置它,而且我们也可以更容易地随时间推移对其进行升级,而无需担心模糊的边缘情况。
我们通常也变化太慢。我们想要做很多年的重大重构,但从未真正着手去做。这从短期来看意味着我们的用户拥有更加稳定的系统,但也意味着一个更难维护的系统,而且更难为其做出贡献。
最后,我们花太长时间才意识到,我们的简单性目标最好用设计语言来表达。当我们开始谈论设计而不是仅仅谈论简单性时,我们获得了一个更好的框架来决定添加或删除功能,以及一个更好的方法来传达这些决定的理由。
Puppet 是一个简单而复杂的系统。它有很多活动部件,但它们连接得相当松散,而且自 2005 年成立以来,每个部件都发生了相当大的变化。它是一个可以用于各种配置问题的框架,但作为一个应用程序,它既简单又易于使用。
我们未来的成功取决于这个框架变得更加牢固和更加简单,以及这个应用程序在获得功能的同时保持易于使用。