このモジュールガイドでは、ベストプラクティスとなる 標準とアーキテクチャを採用することで、優れたモジュールを作成する方法を紹介します。
このガイドの作成者は、長年にわたってPuppetモジュールを作成しており、あらゆる間違いやすい点につまずいた経験があります。 このガイドの目的は、同じ間違いをユーザが繰り返さないようにするために、理解しやすい方法でモジュールのベストプラクティスを紹介することです。
このガイドを読む前に、Puppet言語の基礎を理解し、クラス構成を把握し、基本的なモジュールの作成方法を学習して、Puppetになじんでおくことを推奨します。
モジュールの目的の定義
モジュール作成を開始する前に、モジュールの機能を定義する必要があります。
モジュール機能の範囲を先に定義しておくと、知らず知らずのうちにモジュールが手に負えない大きさになって使いにくくなることを回避できます。 モジュールの役割は1つだけにすることを推奨します。 たとえば、適切なモジュールは、MySQLのインストールのみを扱い、MySQLを必要とするその他のプログラムやサービスのインストールには対応しません。
モジュールを適切に計画するために、以下の内容について検討してください。
- モジュールで実行する必要のあるタスクは何か。
- モジュールが対応する処理はどのようなものか。
- Puppet環境内の上位レベルでモジュールが対応すべき機能は何か。
ヒント:モジュールの機能を記述するときに「および(and)」という言葉を使っている場合、その部分でモジュールを分割することを推奨します。
通常、Puppet環境では200個以上のモジュールが使用されます。 環境内の各モジュールには、動作の実行を可能にする関連リソースを含める必要がありますが、モジュールは単純であるほど使いやすくなります。 妥当な範囲で、できるだけ多くのモジュールを作成することを強く推奨します。 的を絞った小さいモジュールを多数作成すると、コードの再利用が促されて、全面的なソリューションではなく構成要素としてモジュールを利用できるようになります。
例として、puppetlabs/puppetdbモジュールを確認してみましょう。 このモジュールが扱うのは、PuppetDBのセットアップ、設定、管理のみです。 PuppetDBはデータをPostgreSQLデータベースに格納しますが、 このモジュールでPostgreSQLを管理するのではなく、puppetlabs/postgresqlモジュールを依存モジュールとして指定することで、postgresqlモジュールのクラスとリソースを利用して、PuppetDBの正しい設定を記述しています。 同様に、PuppetDBを操作するためには、puppetDBモジュールからpuppet.confを編集する必要があります。 puppetDBモジュールの内部でこれを処理する代わりに、puppetlabs/inifileモジュールを利用して、puppetDBモジュールがpuppet.confの必要な部分のみを編集するようにしています。
モジュールの構成
理想的なモジュールは、ソフトウェアの一部を、インストールからセットアップ、設定、サービス管理までの全段階で管理します。
ただし、モジュールが管理できるソフトウェアには多数のバリエーションがあります。 大部分のモジュールは、ベストプラクティスに沿った構造に従うことを推奨しますが、 一部のモジュールにとってこの構造が適切ではない場合もあるでしょう。 このような不適合がある場合、その旨を明記し、できる限り代わりのベストプラクティスを推奨します。
このセクションの内容は以下のとおりです。
ベストプラクティスとなる実際の標準モジュールを示すため、puppetlabs/ntpモジュールの内容を確認します。
クラスの設計
優れたモジュールも、1つのタスクのみを実行する、自己完結型の小さいクラスで構成されます。 モジュール内のクラスはプログラムにおける関数と似ており、パラメータを使用して関連するステップを実行し、全体として一貫性のある処理を実行します。
命名規則に関する一般的なベストプラクティスとして、ファイル名にはクラスまたはそこに含まれる定義と同じ名前を付け(メインクラスを除く)、クラス名にはその機能にちなんだ名前を付けます。
クラス構造は以下のようにすることを推奨します(詳細は後述)。

module
モジュールのメインクラスにはモジュールと同じ名前を付けて、init.ppファイル内に記述します。 メインモジュールクラスの名前および場所は、 自動ロード機能で使用されるので、これらは極めて重要です。 メインクラスはモジュールのインターフェースポイントとして、可能な限り、唯一のパラメータ化クラスにします。 メインクラスだけをパラメータ化クラスにすると、1つのクラスをincludeするだけで、モジュール全体の使用を制御することができます。 ユーザがモジュールのincludeを使用しやすいように、このクラスには合理的なデフォルトを設定します。
たとえば、ntpモジュール内のメインクラスntpは次のようになります。
class ntp (
Boolean $broadcastclient,
Stdlib::Absolutepath $config,
Optional[Stdlib::Absolutepath] $config_dir,
String $config_file_mode,
Optional[String] $config_epp,
Optional[String] $config_template,
Boolean $disable_auth,
Boolean $disable_dhclient,
Boolean $disable_kernel,
Boolean $disable_monitor,
Optional[Array[String]] $fudge,
Stdlib::Absolutepath $driftfile,
...
module::install
installクラスはinstall.ppファイル内に記述します。 モジュールが管理するソフトウェアをノードにインストールするために必要なすべての関連リソースを含めます。
installクラスには モジュール名::installという名前を付けます。次に、ntpモジュールの例を示します。
class ntp::install {
if $ntp::package_manage {
package { $ntp::package_name:
ensure => $ntp::package_ensure,
}
}
}
module::config
インストールしたソフトウェアの設定に関連するリソースは、configクラス内に記述します。 configクラスにはmodule::configという名前を付け、config.ppファイル内に記述します。
次に、ntpモジュールのmodule::configクラスを示します。
class ntp::config {
#The servers-netconfig file overrides NTP config on SLES 12, interfering with our configuration.
if $facts['operatingsystem'] == 'SLES' and $facts['operatingsystemmajrelease'] == '12' {
file { '/var/run/ntp/servers-netconfig':
ensure => 'absent'
}
}
if $ntp::keys_enable {
case $ntp::config_dir {
'/', '/etc', undef: {}
default: {
file { $ntp::config_dir:
ensure => directory,
owner => 0,
group => 0,
mode => '0775',
recurse => false,
}
}
}
file { $ntp::keys_file:
ensure => file,
owner => 0,
group => 0,
mode => '0644',
content => epp('ntp/keys.epp'),
}
}
...
module::service
serviceクラスには、残りのサービスリソースと、ソフトウェアの実行状態に関するすべての要素を指定します。 serviceクラスにはmodule::serviceという名前を付け、service.ppファイル内に記述します。
以下に例を示します。
class ntp::service {
if ! ($ntp::service_ensure in [ 'running', 'stopped' ]) {
fail('service_ensure parameter must be running or stopped')
}
if $ntp::service_manage == true {
service { 'ntp':
ensure => $ntp::service_ensure,
enable => $ntp::service_enable,
name => $ntp::service_name,
provider => $ntp::service_provider,
hasstatus => true,
hasrestart => true,
}
}
}
パラメータ
パラメータは、モジュールのパブリックAPIを構成します。
公開するインターフェースの中でもっとも重要なので、ユーザがモジュールとの相互作用をカスタマイズできるように、パラメータの数と種類のバランスに配慮する必要があります。 ここからは、パラメータの命名と開発に関するベストプラクティスについて説明します。
パラメータの命名
コミュニティの理解を得て、トラブルシューティングと協業の容易なモジュール開発を可能にするためには、一貫性のある命名が不可欠です。
ベストプラクティスとして、thing_propertyパターンに従ったパラメータの命名を推奨します。
次に、ntpモジュールの例を示します。
class ntp::install {
if $ntp::package_manage {
package { $ntp::package_name:
ensure => $ntp::package_ensure,
}
}
}
パラメータで機能全体のオン/オフを切り替える場合、thing_manageという命名規則を使用できます。 これは特に、モジュールがインストール全体を管理する場合など、ブールを使用した切り替えに適用できます。 thing_manageという規則に従うことで、上のntpの例のように、if $package_manage {}テスト内にすべてのリソースをラッピングすることができます。
モジュール間で一貫性のある名前を使用すると、コードの読みやすさと再利用性が高くなります。
パラメータ数
モジュールを最大限に再利用しやすくするため、パラメータを追加して柔軟なモジュールにします。 パラメータを使用することで、ユーザはモジュールの使用法をカスタマイズすることができます。
モジュール内にデータをハードコードする代わりに、できるだけ多くのパラメータを使用することを推奨します。 モジュール内にデータをハードコードするとモジュールの柔軟性が低くなり、わずかに状況が異なる場合も、マニフェストを変更する必要が生じます。
テンプレートをオーバーライドするパラメータの追加は避けてください。 パラメータがテンプレートをオーバーライドする場合、ハードコードされた追加パラメータを含むカスタムテンプレートによって元のテンプレートがオーバーライドされる場合があります。 テンプレート内にハードコードされたパラメータがあると、将来的に柔軟性が妨げられます。 カスタマイズしたテンプレートで元のテンプレートをオーバーライドするよりも、パラメータの数を増やして元のテンプレートを変更するか、または任意のテキストをテンプレートに追加するパラメータを使用する方がずっと適切です。
多数のパラメータを利用したモジュールの例については、puppetlabs/apacheを参照してください。
順序
ベストプラクティスとして、順序に関連するすべての依存関係(requireやbeforeなど)を、リソースではなくクラスに基づいて指定します。 クラスに基づいて順序付けすると、各クラスの詳細な実装をその他のクラスから隠蔽することができます。
以下に例を示します。
file { 'configuration':
ensure => present,
require => Class['module::install'],
}
複数のパッケージに対してrequireを指定する代わりに、上記のような順序にすると、変更に合わせてその他のクラスのマニフェストを調整しなくても、module::installをリファクタリングして改善することができます。
包含とアンカリング
作成するモジュールに対してその他のモジュールが順序関係を構築できるようにするには、メインクラスで宣言する下位クラスに対して明示的にcontainを指定します。
クラス内で宣言されたクラスが自動的に含まれるわけではありません。 なぜなら、クラスは、includeなどの関数を使用して複数の箇所で宣言できるからです。 クラスを含めるには、contain関数を使用します。 包含についての詳しい情報は、包含ドキュメントを参照してください。
次に、ntpモジュールがメインクラスntp内でcontainを使用している例を示します。
contain ntp::install
contain ntp::config
contain ntp::service
Class['::ntp::install'] ->
Class['::ntp::config'] ~>
Class['::ntp::service']
包含がサポートされるのは、Puppet 3.4以降です。 Puppet 3.4 (またはPuppet Enterprise 3.2)より前のバージョンをサポートする場合、これらのクラスを所定の位置に保持するには、anchorパターンを使用します。 アンカリングを使用するには、puppetlabs-stdlibが必要です。
依存関係
モジュールの機能が別のモジュールに依存している場合、これらの依存モジュールを直接包含する必要があります。
つまり、メインクラス内でinclude xを使用して、依存モジュールがカタログに含まれるようにする必要があります。 また、モジュールのmetadata.jsonと.fixtures.ymlに対して、この依存モジュールを追加します。 (.fixtures.ymlはrspecだけが使用するファイルで、ユニットテストの正しい実行に必要な依存モジュールを抽出するために使用されます )
モジュールのテスト
次に、さまざまな条件下でモジュールが正しく動作し、モジュールに含まれるオプションとパラメータが適切な結果をもたらすことを確認します。
ユニットテストと受け入れテストの作成に使用できるテストフレームワークがいくつか提供されています。 Puppet開発キットに付いているツールもあります。 (PDK; PDK マニュアルを参考).
rspec-puppet
RSpec-Puppetは、Puppet用のユニットテストフレームワークを提供します。 テスト専用フレームワークであるRSpecでPuppetカタログが認識されるように、このフレームワークを拡張したものです。 以下の例に従ってテストを作成すると、モジュールの各側面が期待どおりに動作することをテストできます。
it { should contain_file('configuration') }
RSpecでは、各種シナリオでモジュールをテストするために、osfamilyなどのfactsを指定できます。
典型的な使用法は、一連のオペレーティングシステムに対する繰り返し処理ですが、この場合、サポートされるすべてのオペレーティングシステムに対して、カタログ内にパッケージおよびサービスが存在することが前提となります。
詳しい情報は、http://rspec-puppet.com/を参照してください。
puppetlabs-spec-helper
puppetlabs-spec-helperは、モジュールのテストに必要なタスクの一部を自動化できるツールです。
これは特に、rspec-puppetと組み合わせて使用すると便利です。 Puppet-spec-helperはデフォルトのrakeタスクを提供して、モジュール間でテストを標準化するとともに、rspec-puppetと実際のモジュール間のグルーコードをいくつか提供します。 通常、必要な処理は、プロジェクトのGemfileへの追加と、Rakefileへの以下の追加だけです。
require 'puppetlabs_spec_helper/rake_tasks'
Beaker-rspec
Beaker-rspec は受け入れ/統合テストフレームワークです。
各種ハイパーバイザ(Vagrantなど)上に仮想マシンを提供し、実際の環境にPuppetモジュールを適用した結果をチェックします。
serverspec
Serverspecは、beaker-rspec向けに追加のテスト用構造体(be_runningやbe_installedなど)を提供します。 これにより、テスト時に、基盤となるディストリビューションの詳細を抽象化することができます。 以下のようにテストを記述します。
describe service('httpd') do
it { should be_running }
end
各種のディストリビューションに合わせて、be_runningがシェルコマンドに変換されます。
モジュールのバージョン管理
その他のソフトウェアコンポーネントと同様に、モジュールにはバージョン管理と変更時のリリースが必要です。 PuppetはモジュールのバージョニングにSemantic Versioning (semver.org)を使用しています。SemVerでは、メジャーバージョンとマイナーバージョンをインクリメントするための具体的なルールが定められています。
新しいバージョン番号を決定したら、metadata.json内のバージョン番号を更新します。
これにより、特定バージョンの依存モジュールとの依存関係リストをmetadata.json内に作成できるので、古い依存関係でモジュールが使用されて動作しなくなることを回避できます。 また、バージョン管理によって、環境ごとに使用するモジュールのバージョンを簡単に変更できるので、ワークフロー管理が可能になります。
モジュールのドキュメント化
READMEを使用してモジュールをドキュメント化することを推奨します。READMEには、モジュールの動作に加えて、モジュールのクラス、定義型、関数、リソース型、プロバイダに関する情報を記述したReferenceセクションを含めます。
詳しくは、モジュールのドキュメント化に関するガイドと、Puppet言語スタイルガイドのドキュメント化セクションを参照してください。
モジュールのリリース
Puppet Forgeでモジュールを公開することを推奨しています。
モジュールを共有すると、公開されたモジュールに対する改善を別のユーザがフィードバックできるので、事実上、無償でモジュールの改善が提供されます。
また、モジュールをForgeで公開することで、Puppetユーザ間のコミュニティが育成され、別のPuppetコミュニティメンバーがこのモジュールをダウンロードして使用することができます。 Puppetコミュニティが定期的にForgeでモジュールをリリースして繰り返し改善すると、利用できるPuppetモジュールの品質が劇的に高くなり、ユーザがより多くのモジュールをダウンロードし、それぞれの目的に合わせて変更できるようになります。 Forgeでのモジュールの公開方法について、詳しくはこちらを参照してください。
コミュニティリソース
初めてのモジュールの作成者に対するリソースもあります。
IRCでの#puppet