Puppetでcertnameを使わずに条件分岐させる

# 前置き
Puppetでは、certname(設定変更してなければ通常FQDN)を元にサーバごとの定義を分けることが多いです。ただ、最近certnameで定義を分けるのに限界を感じたので、別のやり方を試してみました。**certnameを簡単に変えられないようなときようです。**

# certnameを使って困る場合
certnameを使っていて困るのは、

- サーバの役割（ロール）が変わって、別のロール定義を反映したくなったとき
- DockerとかAWSとかだと、ホスト名とか勝手に割り当たっている(設定はできるけど...)

です。１つの名前で、ネットワーク的な識別名、Puppetでの管理名、反映させるロールの元情報として使うと小回りきかない不便さがある気がします。

# certnameを使わないでカスタムfactを使う
各サーバの役割（ロール）で定義を決める方が良さそうなのでロール（webサーバ、DBサーバとか）ベースに考えます。とりあえず**各サーバにロールを記載したファイルを置いておいて、それをカスタムfactで取得して、反映する定義を決めることにします**。

## ロールを取得するカスタムfactを用意する
@udzuraさんの[Facterを単体で使う](http://qiita.com/udzura/items/085ad433849a13908778)を参考にカスタムfactを作ります。環境変数の設定なしで動くようにしたかったので、今回はカスタムfactを$LOAD_PATH/facter/*.rbに置きます。

$LOAD_PATHを確認。

```
# ruby -e 'puts $LOAD_PATH'
/usr/local/share/ruby/site_ruby
/usr/local/lib64/ruby/site_ruby
/usr/share/ruby/vendor_ruby
/usr/lib64/ruby/vendor_ruby
/usr/share/rubygems
/usr/share/ruby
/usr/lib64/ruby/
```

今回は、/usr/local/lib64/ruby/site_ruby/facter/my_role.rbで。

```
# ls -l /usr/local/lib64/ruby/site_ruby/facter/my_role.rb
-rw-r--r-- 1 root root 418 Mar 11 14:10 /usr/local/lib64/ruby/site_ruby/facter/my_role.rb
```

中身は/etc/puppet/my_role.yamlを、YAMLフォーマットで読んでそのまま返すようになっています。カスタムfactでとりたい情報を増やしたくなったときに楽なように、ファイルの中身を丸ごと取れるような設計です。

```rb:/usr/local/lib64/ruby/site_ruby/facter/my_role.rb
require 'puppet'
require 'yaml'

Facter.add(:my_role) do
  setcode do
    begin
      my_role = YAML.load_file('/etc/puppet/my_role.yaml')
      if !my_role or my_role.empty?
        Puppet.notice "Faild to getting my role: skipping."
        'error'
      else
        my_role
      end
    rescue => e
      Puppet.err "Error on getting my role: #{e.class}, #{e.message}. skipping."
      'error'
    end
  end
end
```

## カスタムfactの確認
カスタムfactで読み込むYAMLファイルに、サーバの役割情報を書いておきます。例として、environment(環境)、role(そのサーバの役割)、index(そのロールの何番目のサーバか)にします。この３つがあれば、だいたい何とかなるはず。
※マニフェスト側で適切に読めばよいので、my_role.yamlの内容は自由です。

```
# ls -l /etc/puppet/my_role.yaml
-rw-r--r-- 1 root root 44 Mar 12 01:36 /etc/puppet/my_role.yaml
```

```yaml:/etc/puppet/my_role.yaml
---
environment: debug
role: db
index: '01'
```

カスタムfactの結果を確認。my_role.yamlどおり、ハッシュでとれていそうです。

```
# facter my_role
{"environment"=>"debug", "role"=>"db", "index"=>"01"}
```

## Puppet 4.0より古い場合の注意点
Puppetのバージョンが4.0より古い場合は、デフォルトだとfactの結果は文字列に変換されてハッシュ・配列がうまく使えないので、stringify_factsをfalseにします。

```/etc/puppet/puppet.conf
[main]
    ...(略)...

    stringify_facts = false
[agent]
    ...(略)...
```

## Puppetから使えるか確認
確認用に簡易の定義を用意しました。notifyでfactの結果を表示して、roleで条件分岐できるかチェックです。

```puppet:/etc/puppet/manifests/site.pp
#notify{ $my_role: }
notify{ $my_role[environment]: }
notify{ $my_role[role]: }
notify{ $my_role[index]: }

if $my_role[role] == 'web' {
  package { 'httpd': }
}

if $my_role[role] == 'db' {
  package { 'mariadb': }
}
```

noopで実行。Notifyの表示で、うまくとれています。dbとしてPackage[mariadb]がインストールされる処理も動きます。

```
# puppet agent -t -v --server `uname -n` --noop
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for console
Info: Applying configuration version '1457711460'
Notice: /Stage[main]/Main/Notify[01]/message: current_value absent, should be 01 (noop)
Notice: /Stage[main]/Main/Notify[debug]/message: current_value absent, should be debug (noop)
Notice: /Stage[main]/Main/Notify[db]/message: current_value absent, should be db (noop)
Notice: /Stage[main]/Main/Package[mariadb]/ensure: current_value absent, should be present (noop)
Notice: Class[Main]: Would have triggered 'refresh' from 4 events
Notice: Stage[main]: Would have triggered 'refresh' from 1 events
Notice: Finished catalog run in 0.14 seconds
```

## サーバの役割（ロール）が変わった場合
サーバ上のデータを残したまま、設定変更するようなイメージです。役割違うならサーバを作り直す場合もありますが、過去の資産を残したい場合です。

カスタムfactで読んでいるファイルの情報を書き換える。roleの部分をwebに変更します。

```
# sed -i -e 's/role: .*/role: web/g' /etc/puppet/my_role.yaml
```

```/etc/puppet/my_role.yaml
---
environment: debug
role: web
index: '01'
```

webとしてPackage[httpd]がインストールされる処理が動きます。certnameやFQDNを変えずに、サーバ定義を切り替えることができました。

```
# puppet agent -t -v --server `uname -n` --noop
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for console
Info: Applying configuration version '1457711460'
Notice: /Stage[main]/Main/Notify[01]/message: current_value absent, should be 01 (noop)
Notice: /Stage[main]/Main/Notify[web]/message: current_value absent, should be web (noop)
Notice: /Stage[main]/Main/Notify[debug]/message: current_value absent, should be debug (noop)
Notice: /Stage[main]/Main/Package[httpd]/ensure: current_value absent, should be present (noop)
Notice: Class[Main]: Would have triggered 'refresh' from 4 events
Notice: Stage[main]: Would have triggered 'refresh' from 1 events
Notice: Finished catalog run in 0.14 seconds
```

# あとがき
今回カスタムfactから取得している情報は、01.web.debugみたいにcertname(FQDN)に盛り込んでおくのが普通な気もします。今回試した方法だと事前準備が多いので、変えても支障がなければcertnameを変えた方が早い気もします。ただ、SSLまわりのリセットとか別の問題もあったりするので、場合によりけりです。

# バージョン情報

```
# puppet --version
3.8.4
# facter --version
2.4.4
```

# 参考
- certnameでサーバの定義を分ける方法についてはここ
  - [Language: Node Definitions — Documentation — Puppet Labs](https://docs.puppetlabs.com/puppet/latest/reference/lang_node_definitions.html)
- facterでハッシュを使う場合についてはここ
  - [puppet - Access arrays/hashes in facter 2 facts - Server Fault](http://serverfault.com/questions/633585/access-arrays-hashes-in-facter-2-facts)