Puppet

私とPuppet ベストプラクティス編 その1 (ディレクトリ構成とその役割)

More than 3 years have passed since last update.

Puppetマニフェストを高度かつ複数人で安全に使うための規約例やテクニックおよびすべからず等について記述する。
(やっと基礎的な話が粗方書けたので、細かい話に触れられる...。長かった。)

特に10人前後の規模の人数で開発をする場合に、問題がおきにくい方法論を私の経験則から記載することを本ページの目的としている。断定口調で書いているがあくまで経験則によるものなので注意。
この方法が最適ではないと思うので、他のベスプラ等あればぜひ記事を投稿して教えてください。

本シリーズの目次

構成管理はプログラム?

ベストプラクティスに進む前に構成管理用のプログラムライクな最適化のバランスを考える必要がある。

動いた結果が正しければ良い処理を目的とするプログラムに比べて、システム毎に変わる需要に対して変更やチューニングがいる構成管理の記述言語はどちらかというと設定ファイルに近い。

構成管理用の記述言語はそれなりに構文や変数名の動的解決機能を持っている。
やろうと思えば構成管理した時点の環境に合わせて動的に構成管理を行うような高度なチューニングは可能である。
(特にChefはrubyの内部DSLとして書かれているため記述の自由度が格段に高い。)

何も考えずにプログラムライクにやっていくと、

  • if文等が多様されどのようなシステムができあがるのかがわからない。
  • 再利用がしずらい。

等の問題が頻発し、ちょっと変更するたびにマニフェスト自体のデバッグ等に追われることになる。

ついついプログラムぽくお洒落感満載に書きたくなるが、そこはぐっと抑えてpuppetマニフェストを編集するチームメンバのレベルにあわせて、十分にコード(マニフェスト)規約等でルールを共有しながらマニフェストをメンテナンスしていくというスタンスを維持すると良い。

(なお、感覚的には通常のプログラムより正規化をしすぎず、やや冗長にしたほうがわかりやすくて助かることが多かったかも。)

基本のキ

まず、インデント等のコード規約的な細かい話については、

に非常に綺麗にまとまっているのでこちらを参照されたい。

システム全体のマニフェスト(site.pp)のベストプラクティス

配備場所

実際に様々な機能を必要とする多数のサーバ向けのマニフェストは1つのクラスタ単位で同一ディレクトリにまとめるのがベストプラクティスである。

一般的には1クラスタのみを扱うのであれば、

/etc/puppet/manifests

に集約するのがベストプラクティスである。

具体的にこのディレクトリに置くのは、

  • site.pp: システム全体で共通の、Resource初期設定(Resource Defaults)やモジュールを跨いだ依存関係を定義。 クラスタ共通のトップレベル変数の定義もしても良い。
  • <ノードタイプ>.pp,default.pp: WebサーバやDBサーバ等のロール単位のnode定義をファイルを分けて定義。

のみとすると良い。

表記例(site.pp)
Class['Yum'] -> Package<| |>

上記は全てのpackageリソースを実行するまえにyum moduleの適用を終えるという意味。
複数のResource参照にマッチするResource Collectorについての詳細はこちら

node node1 {
  include apache
  include php
  class { 'subversion':
    repodir => "/var/svn2",
  }
  Class['apache'] -> Class['subversion']
  Class['apache'] -> Class['php']
}

node定義のベストプラクティス

node定義内では、

  • モジュールを跨いだ依存関係
  • class(module)の宣言

のみを書く。

node定義の「すべからず」

  • 1つのhost名を複数のnode定義にマッチさせないようにする。

    • どのnode定義が対応しているのかがわかりにくくなるため。
    • どうしても難しい場合は、node定義のマッチ条件として優先的にマッチするものを上に書いて、間違えたnode定義を見ないように配慮する。
  • 基本的にこの/etc/puppet/manifests以下では新たなクラス定義はしない
    (ModuleにClassを追い出したほうがnode定義の詳細が追いかけやすい。)

  • node定義に正規表現を使う場合、その中の特定ホストの構成を変えるためにif/case文を使うのはほどほどに。

    • puppetを見る大抵の場合、1サーバの設定がどうなるかを調べたいだけなのでnode定義中の条件分岐は邪魔に感じるだけ。
    • 特定のサーバの構成管理を知りたいときに条件分岐だらけのnode定義を見るのはとても辛い。

なお、roleprofileという複数のmoduleを統合するsuper moduleを用意し、node定義を簡略に書くことをベスプラとする流派もいる。Designing Puppet – Roles and Profiles.
ただし、このやり方はhieraを組み合わせないとサーバ単位での構成差分をかなりつけにくいので安直に真似しないこと。

Moduleのベストプラクティス(Hiera以前)

module/manifests配下の推奨マニフェスト構成

Puppetのmoduleのmanifestsのマニフェスト構成は以下を基本に据える。
この例ではapache_best Moduleを作る例を紹介する。

module配下のマニフェスト構成
/etc/puppet/modules/apache_best/manifests/
+- config.pp
+- init.pp
+- install.pp
+- params.pp
+- service.pp
`- user.pp

それぞれのファイルに記載すべき内容を以下とすることをルールとして共有すると他人のマニフェストを読む際に非常に捗る。

  • params.pp

    • そのmodule内で使われる変数、default値の定義
    • OS毎に変更が必要な変数等、classのブロックで条件分岐(conditional)を使っての生成
    • このクラス内ではclass宣言をしない
    • 主に利用するResource: 無(params.ppではResource宣言はしない)
  • init.pp

    • Puppet言語仕様上必須。
    • module名と同名のClassをincludeすると標準的な構成が作れるように class apache_best (..) inherits apache_best::params { .. }を記述する。
    • 同一モジュール内のその他classの適用順序関係を記載
    • puppet-3.5以前ではanchor Resourceとarrowオペレータを使って書くのが推奨。
    • なお、anchor Resourceはsudo puppet module install puppetlabs-stdlibを実行しないと使えるようにならないので注意。
    • 主に利用するResource: anchor, class
  • user.pp

    • このmoduleでインストールするソフトウェアが必要とするアカウント情報(userやgroup)の定義
    • 一般的なアカウントを作成するrpmファイル等はuidやgidが既に利用されていると自動的にID番号をずらすが、 システム全体でID番号が合わないと問題が起きることが多い。
    • userのホームディレクトリを確実に準備するためにfile Resourceで定義しても良い。
    • 主に利用するResource: user, group, file
  • install.pp

    • moduleに必要なパッケージインストールのためのResourceの定義
    • 主に利用するResource: package, exec
  • config.pp

    • ソフトウェアのセットアップに必要となる設定ファイルの配備、ディレクトリの作成等のためのResourceの定義
    • 標準的に必要なセットアップ手順をexec Resourceで定義することもある。
    • 主に利用するResource: file, exec
  • service.pp

    • Moduleの含むデーモン(apache, mysql-server等)のservice Resourceの定義
    • 主に利用するResource: service
  • (共通)

    • params以外のクラスはparams.ppで定義した変数と同名の変数をParameterized Classの引数としないといけない。

上記のような役割分担で記述すると、user.pp, install.pp, config.pp, service.ppという適用順序でうまく動くようになるためinit.ppの定義は以下のようになる。

apache_bestのinit.pp
class apache_best (
  $uid             = $apache_best::params::uid,
  $gid             = $apache_best::params::gid,
  $version         = $apache_best::params::version,
  $httpd_conf_tmpl = $apache_best::params::httpd_conf_tmpl,
  $document_root   = $apache_best::params::document_root,
  $service_state   = $apache_best::params::service_state,
) inherits apache_best::params {
  anchor { 'apache_best::begin': } ->
  class { 'apache_best::user':
    uid => $uid,
    gid => $gid,
  } ->
  class { 'apache_best::install':
    version => $version,
  } ->
  class { 'apache_best::config':
    httpd_conf_tmpl => $httpd_conf_tmpl,
    document_root   => $document_root,
  } ->
  class { 'apache_best::service':
    service_state => $service_state,
  } ->
  anchor { 'apache_best::end': }
}
apache_bestのparams.pp
class apache_best::params (
  $uid             = '48',
  $gid             = '48',
  $version         = latest,
  $httpd_conf_tmpl = 'apache_best/httpd.conf',
  $document_root   = "/var/www/html",
  $service_state   = running,
) {
}
apache_bestのuser.pp
class apache_best::user (
  $uid = $apache_best::params::uid,
  $gid = $apache_best::params::gid,
) {
  group { 'apache':
    ensure  => present,
    gid     => $gid,
  } ->
  user { 'apache':
    ensure  => present,
    uid     => $uid,
    gid     => $gid,
    comment => "Apache",
    home    => '/var/www',
    shell   => '/sbin/nologin',
  }
}
apache_bestのinstall.pp
class apache_best::install (
  $version = $apache_best::params::version,
) {
  package { 'httpd':
    ensure  => $version,
  } 
}
apache_bestのconfig.pp
class apache_best::config (
  $httpd_conf_tmpl = $apache_best::params::httpd_conf_tmpl,
  $document_root   = $apache_best::params::document_root,
) {
  file { '/etc/httpd/conf/httpd.conf':
    ensure  => file,
    owner   => 'root',
    group   => 'root',
    mode    => '644',
    content => template($httpd_conf_tmpl),
    # テンプレートファイルの中でdocument_root変数が使われている想定。
  } 
}
apache_bestのservice.pp
class apache_best::service (
  $service_state = $apache_best::params::service_state,
) {
  service { 'httpd':
    ensure  => $service_state,
  } 
}

(なお、若干差異はあるが、Moduleビギナー向けのドキュメントとしては、PuppetLabs - Beginner's Guide to Modulesにも似たようなベストプラクティス情報がある。)

コラム:Parameter取り回しの煩雑さについての言い訳

前節の通り、パラメタ宣言が非常に冗長になってしまうのPuppetマニフェストなのである..。
もちろん、Puppet以外の機能(ENC, Hiera、PuppetDB)を前提とすれば綺麗に書くことはできる。
(これらの機能が無いと構成差分がつけにくい状態となるが。)

正直、このあたりのPuppet言語仕様が行けてないのは先行者ゆえでもある。
後発のansibleは言語仕様としてこのあたりなかなか書きやすくできている。

Module宣言のベストプラクティス

実際の宣言の仕方を見てみよう。

apache_best
include apache_best
apache_best
class { 'apache_best':
  document_root => "/opt/apache/htdocs",
}

これだけである。まあまあスマートに感じられるのではないだろうか?

何故、モジュール宣言中で同名変数を引き回していたかの理由はテクニカルには、

  • 「Module中でのスコープ指定無しの変数はModule中で定義されたものである」

という規約としたいからだと考えている。
スコープ無しの変数は同一Module内を漁れば定義箇所や利用箇所がわかるという前提は結構うれしい。

だが、<module名>::params::<変数名>というフルパス変数参照でinstall.ppやconfig.pp等を作成するスタイルの人もpuppetlabsにいるのでこのあたり結構揺れている気はするが。(puppetlabs-mysqlモジュールとか。)

Moduleのすべからず

  • 自Module以外のclassを宣言・参照しない。

    • これをModule配下でした瞬間にModuleの再利用性は極度に低下する。
  • モジュール内でのclass宣言の入れ子構造は高々2層程度

    • 複雑な入れ子構造にclass宣言になる場合はmodule構造を見直すべき。

Puppetの学習に便利なリンク

  • PuppetLabs - Docs: Puppet 3.N Reference Manual

    • より細かい言語仕様について書かれたページ基本編を越えた状態であれば読み解けるはず。
    • このマニュアルを一通り理解したメンバのみでPuppetマニフェストを作れれば相当幸せになれる。
  • PuppetLabs - Lerning Puppet

    • 順を追って書かれており初学者が体系的な学習にお勧め。
    • ぶっちゃけこのページはこのリンク先を圧縮したようなものである。
    • このチュートリアル用のVMも無料ダウンロードできる。(ただし登録が必要)