私とPuppet ベストプラクティス編 その2 (多種多様なサーバの構成管理) 

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

10人前後の中規模開発をする場合のベストプラクティスを目指している。
各TIPSは断定口調で書いているがあくまで経験則によるものなので注意。
(この方法が最適ではないと思うので、他のベスプラ等あればぜひ記事を投稿して教えてください。)

**なお、見出しに(Hiera以前)と書かれた章/節はHiera導入するとよりコンパクトにすることが可能。**

# 本シリーズの目次

* [Infrastructure as Code 概念の理解](http://qiita.com/takeuchikzm/items/fb9d1701ea19be337131)
* [Puppet環境の準備](http://qiita.com/takeuchikzm/items/eb37a918b39b069f22b3)
* [Puppetの基本の理解その1 Resource,Ordering,Class,Define,Node](http://qiita.com/takeuchikzm/items/cfa012571fdedcf0792b)
* [Puppetの基本の理解その2 Module,File,Templates,Variable](http://qiita.com/takeuchikzm/items/6b65af9e0cf8d317c9b3)
* [Puppetベストプラクティスの理解その1 ディレクトリ構成とその役割](http://qiita.com/takeuchikzm/items/5e303e03a48ab2f0db1a)
* Puppetベストプラクティスの理解その2 多種多様なサーバの構成管理(本ページの内容)
* [Puppetベストプラクティスの理解その3 Hieraで世界が変わる?] (http://qiita.com/takeuchikzm/items/e04e6cbe84a185eb1f26)

# 多様なサーバ間の構成の差分とは？

むしろ、多様なサーバ間で設定差分があるのは当然である。
Puppetで構成管理をしていて良く出くわす構成差分は以下。

* サーバ毎に、必要な機能(Module)が違う。
* サーバ毎に、機能(Module)のResourceのパラメタが違う。
* サーバ毎に、機能(Module)の設定ファイルの内容が一部だけ違う。
* サーバ毎に、機能(Module)の設定ファイルの内容が全体的に違う。
* サーバ毎に、作成するファイル/ディレクトリ数が違う。
* 複数の機能(Module)が同じファイルを状態管理したい。
* 商用環境と試験環境で各サーバのパラメタを変えたい。

差分が発生する理由としては、

* そのクラスタの運用管理オペレーションの要件
* そもそものサーバの役割の違い(web,dbサーバ等)
* サーバのハードウェアスペックの違い（CPU数、ディスク数、メモリ数)

等があるだろう。

以下、それぞれのケースのベストプラクティスと「すべからず」について見てみよう。

## ケース: 「サーバ毎に、必要な機能(Module)が違う。」(Hiera以前)

**ケース例**

* Webサーバはapacheのセットアップをしたいけど、DBサーバはpostgresqlのセットアップをしたい

**対処1. node定義を各サーバ種別毎に用意してincludeするclassを変える。**

基本編でも説明したとおり。

```puppet:
# /etc/puppet/manifests/web.pp
node /^web.*$/ {
  include os
  include apache
}

# /etc/puppet/manifests/db.pp
node /^web.*$/ {
  include os
  include postgresql
}
```

### すべからず

* 複数種類のサーバに1つの`node`定義をマッチさせすぎる。
  (ex. 複数のサーバの`node`定義内部でif/caseで個別ホスト向けの差分をつける。)
* `node`定義の継承(`inherits`)をする。

どれもnode定義の見通しを悪化させ、構成を修正したい場合のコストを上げる。

## ケース: 「サーバ毎に、機能(Module)のResourceのパラメタが違う。」

**ケース例**

* 監視サーバのみapacheのドキュメントルート直下の`html`ディレクトリのグループを`monitor_group`にしたい。

これも簡単。

<a name="param_change"></a>
**対処1. 変更箇所をModuleパラメタ化する。**

以下はModule構成でのパラメタ追加例。`..`は略の意味。

```puppet:/etc/puppet/modules/apache/manifests/params.pp,init.pp,config.pp
#/etc/puppet/modules/apache/manifests/params.pp
class apache::params (
  ..
  $html_gid = 'nobody', 
  # 追加。標準インストール時のdefault値を設定する
) {
 ..
}

#/etc/puppet/modules/apache/manifests/init.pp
class apache::init (
  ..
  $html_gid = $apache::params::html_gid, #追加
) {
  ..
  class {'apache::config':
    ..
    html_gid => $html_gid, #追加
  } ->
  ..     
}
  # Moduleベスプラに従いparamsの冗長な定義追加をする...。

#/etc/puppet/modules/apache/manifests/config.pp
class apache::config (
  ..
  $html_gid = $apache::params::html_gid, #追加
) {
  ..
  # 追加したパラメタでfile Resourceの定義を変更できるようにする。
  file { "${document_root}/html":     ..
    ..
    html_gid => $html_gid,
    ..
  }
  ..     
}
```

あとは、監視サーバの`node`定義中のClass宣言時にこのParameterを変更すれば良い。

```puppet:/etc/puppet/manifests/monitor.pp
node monitor-server-01 {
  ..
  class { 'apache':
    ..
    html_gid => 'monitor_group',
    ..
  } 
}
```

### すべからず

* 業務アプリに特化しすぎた`file`リソースをapache等の汎用Moduleに入れこまない。

業務アプリ資材の配布をするModuleは個別に用意したほうが良い。

## ケース: 「サーバ毎に、機能(Module)の設定ファイルの内容が一部だけ違う。」

**ケース例**

* パッケージ配布サーバだけapacheの`httpd.conf`のドキュメントルートを変えたい。
* apacheの`worker`スレッド数をサーバのコア数で変化させたい。
* webサーバだけ`/etc/sysctl.conf`に`net.core.somaxconn`や`vm.swappiness`設定等を複数行追記したい。

[前のケース](#param_change)と同様にParameterをModuleに追加し、`httpd.conf`テンプレートに変数をERBタグで埋め込めば良い。
2つ目のケースで、スレッド数がサーバのコア数から機械的に決定できるのであればfacterを使って自動設定させることができる。

(Moduleベストプラクティスを守るためのinit.pp周りでのparamの引き渡しは、どのケースでも基本的に[前のケース](#param_change)と同じなので省略する。)

<a name="facter"></a>
**対処1. 変更箇所をfacterと組み合わせてModuleパラメタ化する。**

Classパラメタのスレッド数に対応するパラメタのデフォルト値をfacter変数から設定する。

```puppet:/etc/puppet/modules/apache/manifests/params.pp
class apache::params (
  ..
  $thread_num = $::processor_count * 10, 
  # 追加。facterを$::で参照と計算をしてthread_num変数に設定する。
) {
 ..
}
```

テンプレートファイルではこの変数をERBタグで参照させる。

```puppet:/etc/puppet/modules/apache/templates/httpd.conf
..
<IfModule worker.c>
StartServers         4
MaxClients        <%= @thread_num %>
MinSpareThreads     25
MaxSpareThreads     75 
ThreadsPerChild     25
MaxRequestsPerChild  0
</IfModule>
..
```

**対処2. 変更箇所をModuleでarrayパラメタ化する。**

Module実装時にどのくらいの行数を追加するかわからない場合は、arrayパラメタとその展開を使うとうまく書ける。

```puppet:/etc/puppet/modules/os/manifests/params.pp
class os::params (
  ..
  $sysctl_addon = [],
  # 追加。デフォルト値は空arrayにしておく。
) {
 ..
}
```

テンプレートファイルはこの変数をERBタグで展開する。

```puppet:/etc/puppet/modules/os/templates/sysctl.conf
..
# Controls the maximum number of shared memory segments, in pages
kernel.shmall = 4294967296

<% @sysctl_addon.each do |v| -%>
<%= v %>
<% end -%>
..
```

`sysctl.conf`に追加をしたい`node`定義で追加行を`sysctl_addon`変数に渡せば良い。

```puppet:/etc/puppet/manifests/web.pp
node /^web.*$/ {
  ..
  class { 'os':
    sysctl_addon => ["net.core.somaxconn = 512", "vm.swappiness = 0"],
    ..
  }
}
```

### すべからず

* テンプレートファイル中のERBタグで直接facterを参照する。

直接参照してしまうと、業務上の理由等でfacter定義以外の値をクラス宣言時に設定をしたい場合の変更が大変。
（後述のテンプレートの差し替えが必要となる。)

## ケース: 「サーバ毎に、機能(Module)の設定ファイルの内容が全体的に違う。」

**ケース例**

* webサーバだけ/etc/hostsの内容を全く違うものに変えたい。

こまごまとテンプレートファイル中のERBタグでParameter定義するのでは追いつかないレベルの大量の差分があるような場合を想定している。

(Moduleベストプラクティスを守るためのinit.pp周りでのparamの引き渡しは[前のケース](#param_change)と同じなので省略する。)

**対処. file Resourceのtemplate引数もしくはsource値のModuleパラメタ化およびテンプレートファイルの差し替え**

Parameterの追加手順自体は[前のケース](#param_change)と全く同じ。

まず、`file` Resourceの`content`属性の`template`関数の引数や`source`属性の右辺のvalueをParameterとしてModuleに追加し、class宣言時に変更可能とする。
(例ではテンプレートファイルとしてhostsファイルを管理しているものとする。)

テンプレートの差し替え用の変数は*_tmplというような名前、source値の引き渡し用の変数は*_sourceとするなどの変数名ルールがおすすめ。

```puppet:/etc/puppet/modules/os/manifests/params.pp
class os::params (
  ..
  $hosts_tmpl = 'os/hosts',
  # 追加。多数派のテンプレートファイルをデフォルト値にしておく。
) {
 ..
}
```

`/etc/hosts`の`file` Resourceの`content`の`template`引数を上記の変数にする。

```puppet:/etc/puppet/modules/os/manifests/config.pp
class apache::config (
  ..
  $hosts_tmpl = $os::params::hosts_tmpl, #追加
) {
  ..
  # もともとない場合はデフォルトが通常と同じになるように
  # file Resourceをつくる。
  file { "/etc/hosts": 
    ..
    content => template($hosts_tmpl),
    ..
  }
  ..     
}
```

次にwebサーバ用のテンプレートファイル`hosts.web`を`os`モジュールに用意する。(`/etc/puppet/modules/os/templates/hosts.web`)
webサーバの`node`定義の`os`クラスの宣言時にこのファイルをテンプレートとして指定する。

```puppet:/etc/puppet/manifests/web.pp
node /^web.*$/ {
  ..
  class { 'os':
    hosts_tmpl => 'os/hosts.web',
    # 新しく用意したテンプレートファイルに差し替え
    ..
  }
}
```

### すべからず(この方式には無い？）

数千台規模でも設定ファイルが根幹から違うようなロール(役割)が数千種類ということはあまりない。
この方式は適用される設定ファイルのわかりやすさと改変しやすさからかなりオススメの方式である。

ホスト単位のテンプレートファイルを`<設定ファイル名>.<ホスト名>`という名前で用意すると、どのサーバにどの設定が適用されるのか？という観点では究極にわかりやすい。
極端に言えばテンプレート中の変数も完全に無くすことすらできる。(そうなるとテンプレートである必要も無いが。)

設定ファイルのレビュを事前にしたいお堅い会社等は、全ホスト毎のソースファイルなりテンプレートを用意したほうがいいと感じるかもしれない。
(敢えて、どこの会社とは言いませんが。）

## ケース: 「サーバ毎に、作成するファイル数が違う。」

**ケース例**

* 搭載しているディスク数Nだけ/mnt/disk1～Nを作りたい。(Nは整数)

facterからfor文を回せばうまくできそうなのだが、Puppet記述言語はfor文が無いので一筋縄では行かない。
やや動的さに欠けるがパラメタとしてArray変数を渡す方式が簡単。
(ちなみにfacterにはpartitionsというディスクのパーティション状況を示したmap構造を返すものはある。)


**対処. Array変数を渡してリソースを任意数作成する。**

puppetでは、Resource titleにArrayが来るとその要素1つ1つにResource定義をするのと同じ意味になる。

```puppet:Array変数による任意数のリソースの定義
class os::params (
  ..
  $disk_dirs = ["/mnt/disk1"],
) {
}

class os::config(
  ..
  $disk_dirs = $os::params::disk_dirs,
) {
  ..
  file { $disk_dirs: #arrayの数だけfileリソースができる。
    ..
    mode   => 755,
    ensure => directory,
  }
}
```

`node`定義でディレクトリリストを渡せば良い。

```puppet:
node web01 {
  ..
  class { 'os':
    disk_dirs => ["/mnt/disk1","/mnt/disk2","/mnt/disk3"],
  }
}
```

（現在調査中なのだが、`file` Resourceのファイルタイプ(`ensure => file,`)の場合にうまくcontentをリソースタイトルで変更する方法がわかっていない。手元の環境(3.7.0)で試した場合に、`content => template("module_name/$title")`と書いても`$title`部分が`file` Resourceのtitleではなく、クラス名になってしまう(上記例だと`os::config`になってしまう。)。
Puppetの説明を見る限りだと`$title`、`$name`、`$path`などと書くとResource titleに置換されるように見えるのだが...。全てのファイルが同一テンプレートでまかなえる場合やディレクトリの場合を除きこの手法は使わない方がとりあえずは無難??

わかり次第加筆したい。

### すべからず

* custom facterで頑張りすぎる。

Custom facterを作ればできるのは間違いないが、Rubyコードを書くコストが高いし、見通しも悪くなるのであまりオススメできない。
（未確認だがfunctionsのsplitやcreate_resource等を組み合わせればもっと楽に実現できるかも。
このあたりの[ブログ](http://ttboj.wordpress.com/2013/11/17/iteration-in-puppet/)を読めばなんとなく分かりそうだが..。)

## ケース: 「複数の機能(Module)が同じファイルを状態管理したい。」

**ケース例**

* `subversion`モジュールも`php`モジュールも`/etc/httpd.conf`を編集したい。

最も構成管理がしずらいケースの１つ。このような場合、puppetはそもそも`/etc/httpd/conf/httpd.conf`の`file` Resourceの競合でエラーを出す。

そもそものapache等のソフトウェアの設定ファイル方式に起因している場合、puppet的に綺麗に解決できないこともある。

このケースのマニフェスト設計上の良くない点は以下となる。

* `httpd.conf`のようなapache等の基本Moduleがいかにも触りそうなものを他Moduleで定義している。
  (Moduleベストプラクティスの「すべからず」のModule外のResource参照とほぼ同等。)
* `subversion.conf`はapache連携を前提としたファイルなので`subversion`モジュールの基本クラス宣言(`include subversion`)で入るのが妥当ではない。

対処法としては、

* `mod_dav_svn`を構成管理するサブモジュール`subversion::mod_dav_svn`を作る。
* 各Moduleの設定を`/etc/httpd/conf.d/subversion.conf`や`/etc/httpd/conf.d/php.conf`等のようにファイルとして分割しておく。
* 各moduleのクラス宣言をするnode定義が双方のmoduleがうまく動く`httpd.conf`をテンプレート差し替えする。

を全て行うことである。

なお、例としてうまいケースがなかったので`httpd.conf`を復数のモジュールが弄りたいというケースを書いたが、apacheは`/etc/httpd/conf.d`に機能別に設定を書き分けられるように基本的になっているので厳密にはこのケースは該当しない。`/etc/init/`(/etc/inittabに相当)や`/etc/security/limits.d/`(/etc/security/limits.confに相当)等を使うと、実はファイルを分けて設定ができる場合も多い。

(Moduleベストプラクティスを守るためのinit.pp周りでのparamの引き渡しは[前のケース](#param_change)と同じなので省略する。)

**対処. 設定ファイルのconf.d的ディレクトリでの別ファイル管理とスーパークラス側での調停**

まず、`subversion`単体を構成管理する部分と`mod_dav_svn`を構成管理する部分を分ける。

```console:subversionモジュールのファイル構成
subversion/
|-- manifests
|   |-- config.pp      # subversionのコンフィグ管理
|   |-- init.pp        # subversion, subversion::mod_dav_svnクラスの定義
|   |-- install.pp     # subversionパッケージのインストール
|   |-- mod_dav_svn    # subversion::mod_dav_svn構成管理用のサブフォルダ
|   |   |-- config.pp  # subversion::mod_dav_svn::configの定義
|   |   `-- install.pp # subversion::mod_dav_svn::paramsの定義
|   `-- params.pp      # subversion::paramsの定義
`-- templates
    `-- subversion.conf
```

`mod_dav_svn`向けのサブクラス定義の追加に関しての主な編集箇所である 
`subversion/manifests/init.pp`、`subversion/manifests/mod_dav_svn/install.pp`、`subversion/manifests/mod_dav_svn/config.pp`の内容を以下に示す。

```puppet:subversion/manifests/init.pp
class subversion (
) inherits subversion::params 
{
  anchor { 'subversion::begin': } ->
  class { 'subversion::install': } ->
  class { 'subversion::config': } ->
  anchor { 'subversion::end': }
}

class subversion::mod_dav_svn inherits subversion::params 
{
  anchor { 'subversion::mod_dav_svn::begin': 
    require => Class['subversion'],
  } ->
  class { 'subversion::mod_dav_svn::install': } ->
  class { 'subversion::mod_dav_svn::config': } ->
  anchor { 'subversion::mod_dav_svn::end': }
}
```

```puppet:subversion/manifests/mod_dav_svn/install.pp
class subversion::mod_dav_svn::install {
  package { 'mod_dav_svn':
    ensure => latest,
  }
}
```

```puppet:subversion/manifests/mod_dav_svn/config.pp
class subversion::mod_dav_svn::config {
  file { '/etc/httpd/conf.d/subversion.conf':
    ..
    content => template('subversion/subversion.conf'),
  }
}
```

`php`側も同様に`/etc/httpd/conf.d`に設定を置くようにする。

```puppet:php/manifests/config.pp
class php::config {
  file { '/etc/httpd/conf.d/php.conf':
    ..
    content => template('php/php.conf'),
  }
}
```

`node`定義では以下のように`subversion`と`subversion::mod_dav_svn`クラスを宣言する。

```puppet:subversionとphpを双方必要とするwebサーバのnode定義
node web01 {
  class { 'apache':
    # 
    $httpd_conf_tmpl => "apache/httpd.conf_for_svn_and_php",
  } -> 
  class { 'php': } ->
  class { 'subversion': } ->
  class { 'subversion::mod_dav_svn': }
}
```

パッケージインストールの順序関係は利用者が把握しなくてはならないので結構大変である。

### すべからず、というかこの場合はどうするのが正解なんだろう

このような設計をしなくてはならないような状況自体が「すべからず」という感じ。
なお、この例では触れていないがModule間のResource競合の解決方法の１つとして[Virtual Resource](https://docs.puppetlabs.com/puppet/3.7/reference/lang_virtual.html)というPuppet機能がある。
しかし、わかりにくさが更に増すので使わないほうがいいだろう。

### 復数Moduleに隠れ依存するパッケージの構成管理について

`mod_dav_svn`のような復数のModule化されたパッケージ(この場合は、`subversion`と`apache`)に依存するパッケージの構成管理をする際、宣言する利用者が依存を管理しなくてはならない。
例では`subversion`モジュールのサブクラスとしたが、`mod_dav_svn`という名前のModuleを用意したほうがわかりやすい場合もある。
あまり綺麗に書こうとしても限界があるので注意書き等を残して、とりあえず系として動くようにして落とし所とすると良い。

## ケース: 「商用環境と試験環境でサーバのパラメタを変えたい」(Hiera以前)

前提として、商用環境と試験環境でホスト名は同じとする。

**ケース例**

* 試験環境のみ`tomcat`のJVMのヒープMAXサイズを1GBにしたい。

一見簡単そうに見えるこのケースだが、ホスト名が同じだと`node定義`が同じものにマッチしてしまうため取れるアプローチが少ない。
もし、試験環境と本番環境のメモリサイズ等から機械的にヒープサイズを計算できるのであれば[前述](#facter)のfacterで構成差分を吸収する方法が使える。
そうでない場合に実現する方法としては以下がある。

**対処1. 試験環境か商用環境かを示すtopレベル変数を用意し、node定義中でif文で制御する。**

```puppet:node定義内でのif文制御
# /etc/puppet/manifests/site.pp
$production = true #production変数で商用環境かどうかを判断

# /etc/puppet/manifests/web.pp
node web01 {
  if $production {
    #商用環境の設定
    include tomcat
  } else {
    #試験環境の設定
    class { 'tomcat':
      heapmem => "1GB",
    }
  }
  ..
}
```

**対処2. 試験環境用と商用環境用のsite.pp・(ノードタイプ).ppを復数組用意し、使い分ける。**

別クラスタとしてマニフェストのディレクトリを分ける。

* `/etc/puppet/manifests/production`: 商用環境のsite.pp, (ノードタイプ).ppを置くディレクトリ
* `/etc/puppet/manifests/test`: 試験環境のsite.pp, (ノードタイプ).ppを置くディレクトリ

実行時には、

`$ sudo puppet apply /etc/puppet/manifests/production` 

や

`$ sudo puppet apply /etc/puppet/manifests/test` 

等と呼び分ける。

### すべからず

そもそも論になってしまうがこの方式はPuppet記述言語マニフェストの文法だけでは見通し良くできないのでHieraやENCの利用を検討すべき。
紹介したどちらの対処方も試験環境と商用環境で実行される`node`定義が全く違うものになってしまうためマニフェストの試験にならない。

# マニフェストをどこまで作りこんでおくべきか？

本ページにてPuppetが具体的な構成差分を**どのレイヤ**で**どのように吸収する**かが大体把握できたことだろう。

マニフェストを作成する際は、**多様なサーバ間でどこが共用できるのか？**を考えて、サーバ間で差分となりそうな部分のクラス変数やテンプレートファイル化をすることが重要である。

しかし、まだシステムに必要ではない部分の変数/テンプレート化を最初からする必要が無いのは一般的プログラムと同様である。

# Puppetの学習に便利なリンク

* [PuppetLabs - Docs: Puppet 3.N Reference Manual] (https://docs.puppetlabs.com/puppet/latest/reference/)
  
  + より細かい言語仕様について書かれたページ基本編を越えた状態であれば読み解けるはず。
  + このマニュアルを一通り理解したメンバのみでPuppetマニフェストを作れれば相当幸せになれる。

* [PuppetLabs - Lerning Puppet](https://docs.puppetlabs.com/puppet/#learning-puppet)

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