Puppet Facterのカスタムファクト徹底活用!環境固有情報の抽出とマニフェスト分岐の極意
テックリードの私たちが日々のインフラ運用で直面する最大の悪夢は何か?
それは、「本番、ステージング、開発、そしてオンプレミスのレガシー環境が、微妙に異なるコンテキストを抱えたまま同じマニフェストでデプロイされ、本番環境でだけサイレントに爆発する」という現象だ。
Puppetのデフォルト `facter` は優秀だ。OSのバージョン、IPアドレス、メモリ容量など、基本的なハードウェア・OS情報は一通り取得できる。しかし、「そのノードがAWS上のどのライフサイクル環境に属しているのか」「社内独自でアサインされた物理ラックの固有IDは何か」「GPUアクセラレーションの型番に応じた特殊なカーネルパラメータは何か」といった、ビジネスとインフラのコンテキストが交差する生々しいデータは持っていない。
ここで「if文をマニフェストにベタ書きする」ような愚行に走ってはいけない。それはIaCの美学に対する冒瀆であり、保守性の死を意味する。
今回は、PythonとRubyを用いて完璧なカスタムファクト(Custom Facts)を実装し、完全な冪等性を担保しながら環境差異を美しく吸収する、極限の実践テクニックを伝授する。
—
1. チーム開発と開発スピードを爆発させる「Puppet開発環境」の極意
本題に入る前に、プロのSREチームが共通して導入している「開発効率を極限まで高めるエコシステム」を共有しよう。ここが整っていないチームは、コードの検証に無駄な時間を溶かしている。
🚀 開発スピードを劇的に高めるキーボードショートカット & ツール
- VS Code + Puppet Extension: シンタックスハイライトはもちろん、リアルタイムでAST(抽象構文木)のバリデーションを走らせる。
- ショートカット (macOS / Linux):
- `Cmd + Shift + P` (Mac) / `Ctrl + Shift + P` (Linux): コマンドパレットから `Puppet: Compile Current Manifest` を即座に呼び出し、カタログ生成テストを0.5秒で行う。
- 絶対入れるべき神プラグイン:
- Puppet Lint: コミットフック(Huskyなど)と連携させ、コーディング規約違反を物理的にブロックする。
- rspec-puppet: カスタムファクトやクラスの単体テスト(Unit Test)を高速に回すためのデファクトスタンダード。
📂 チーム共有すべき `.editorconfig` とベストプラクティス構成
インフラコードのインデントの乱れはチームの心理的安全性の乱れに直結する。リポジトリのルートに以下の `.editorconfig` を配置し、全メンバーの環境を強制的に統一せよ。
.editorconfig
root = true
[.{pp,rb}]
charset = utf-8
indent_style = soft
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[.{yaml,yml,json}]
indent_style = soft
indent_size = 2
—
2. カスタムファクト実装の深淵:Ruby vs Python
Puppet Facter(Facter 4以降)では、RubyだけでなくPythonなど任意の言語でファクトを記述できる(外部ファクト / External Facts、あるいはカスタムRubyファクト)。それぞれの特性を理解し、ユースケースに応じて使い分けるのが一流のアーキテクトだ。
パターンA: クラウドメタデータから動的にタグを抽出し、安全に構造化データ(Hash)を返す(Ruby版)
AWSのIMDSv2(Instance Metadata Service Version 2)から動的にタグやインスタンス情報を取得し、Facterのキャッシュメカニズムを利用して高速かつ安全に返すRubyカスタムファクトの実装例だ。
/etc/puppetlabs/code/environments/production/modules/profile/lib/facter/aws_metadata.rb
require ‘net/http’
require ‘uri’
require ‘json’
Facter.add(:aws_metadata) do
# キャッシュを設定することで、Puppetランごとに毎回APIを叩く遅延を防ぐ(冪等性とパフォーマンスの両立)
confine kernel: ‘Linux’
setcode do
metadata = {}
begin
# IMDSv2のトークンを取得
uri_token = URI.parse(‘http://169.254.169.254/latest/api/token’)
http = Net::HTTP.new(uri_token.host, uri_token.port)
http.open_timeout = 1 # 1秒でタイムアウトさせる(オンプレ環境でのハングを防ぐ鉄則)
http.read_timeout = 1
req_token = Net::HTTP::Put.new(uri_token.request_uri)
req_token[‘X-aws-ec2-metadata-token-ttl-seconds’] = ’60’
res_token = http.request(req_token)
if res_token.code == ‘200’
token = res_token.body
# インスタンスIDの取得
uri_id = URI.parse(‘http://169.254.169.254/latest/meta-data/instance-id’)
req_id = Net::HTTP::Get.new(uri_id.request_uri)
req_id[‘X-aws-ec2-metadata-token’] = token
res_id = http.request(req_id)
metadata[‘instance_id’] = res_id.body if res_id.code == ‘200’
# アベイラビリティゾーンの取得
uri_az = URI.parse(‘http://169.254.169.254/latest/meta-data/placement/availability-zone’)
req_az = Net::HTTP::Get.new(uri_az.request_uri)
req_az[‘X-aws-ec2-metadata-token’] = token
res_az = http.request(req_az)
metadata[‘availability_zone’] = res_az.body if res_az.code == ‘200’
metadata[‘is_cloud’] = true
else
metadata[‘is_cloud’] = false
end
rescue => e
# クラウド外(オンプレミス等)でタイムアウトや例外が発生した場合のフォールバック
Facter.debug(“AWS Metadata service not available: #{e.message}”)
metadata[‘is_cloud’] = false
end
metadata
end
end
パターンB: レガシー環境の特殊ハードウェア情報をPythonで安全にパースする(外部ファクト/JSON版)
Pythonで複雑なOSコマンドやレガシー機器のシリアル、独自センサー情報を取得し、JSON形式で標準出力に吐き出すことで、Facterに読み込ませる「外部ファクト(External Facts)」の手法だ。言語の慣れ親しんだエコシステム(`subprocess` や `pandas` など)を使えるため、開発スピードが圧倒的に上がる。
!/usr/bin/env python3
/etc/puppetlabs/facter/facts.d/legacy_hardware_identifier.py
実行権限 (+x) を付与し、標準出力にJSONを返すだけでFacterが自動認識する。
import json
import subprocess
import sys
import os
def get_legacy_rack_id():
try:
# 例: 社内独自のハードウェア管理コマンドからラックIDを取得
result = subprocess.run(
[‘/opt/corp/bin/query-rack-sensor’, ‘–silent’],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=2
)
if result.returncode == 0:
return result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
# フォールバックとして環境変数や設定ファイルを見る
return os.environ.get(‘CORP_RACK_ID’, ‘unknown-rack’)
def main():
facts = {
“legacy_hardware”: {
“rack_id”: get_legacy_rack_id(),
“sensor_active”: os.path.exists(“/var/run/corp_sensor.lock”),
“identifier_version”: “2.4.1”
}
}
# JSONとして標準出力へ出力(これがFacterに取り込まれる)
print(json.dumps(facts))
if __name__ == ‘__main__’:
main()
—
3. マニフェスト側での高度な条件分岐とクラス設計の極定
カスタムファクトによって取得した構造化データ(Hash)を、Puppetのマニフェスト内でどうエレガントに扱うか。ここでHieraとカスタムファクトを組み合わせた、最もモダンで堅牢な設計パターンを提示する。
🗂️ 設定ファイル・データのベストプラクティス構成例
modules/
└── profile/
├── lib/
│ └── facter/
│ └── aws_metadata.rb # 先ほどのRubyカスタムファクト
└── manifests/
└── monitoring/
└── agent.conf.erb # テンプレートファイル
🧠 宣言的マニフェストでのスマートな条件分岐(SelectorとClass)
環境差異を `if/else` でスパゲッティ化させてはならない。カスタムファクトの値をキーにして、Hieraデータやクラスのパラメータ注入を美しくコントロールする。
modules/profile/manifests/monitoring/agent.pp
class profile::monitoring::agent (
String $server_endpoint = ‘monitoring.internal.net’,
Boolean $enable_advanced_metrics = false,
) {
# カスタムファクト $aws_metadata をベースにした環境判定の例
$is_aws = $facts[‘aws_metadata’] ? {
Undef => false,
Default => $facts[‘aws_metadata’][‘is_cloud’],
}
$env_tier = $is_aws ? {
true => “cloud-${$facts[‘aws_metadata’][‘availability_zone’]}”,
false => “legacy-${$facts[‘legacy_hardware’][‘rack_id’]}”,
}
# リソースの宣言(完全に冪等、かつ環境ごとに最適化された設定を注入)
file { ‘/etc/corp/agent.conf’:
ensure => file,
owner => ‘root’,
group => ‘root’,
mode : ‘0644’,
content => epp(‘profile/monitoring/agent.conf.epp’, {
‘server_endpoint’ => $server_endpoint,
‘enable_advanced_metrics’ => $enable_advanced_metrics,
‘environment_tier’ => $env_tier,
}),
notify => Service[‘corp-agent’],
}
service { ‘corp-agent’:
ensure => running,
enable => true,
hasrestart => true,
}
}
—
4. テスト駆動インフラ(TDD):rspec-puppetによるカスタムファクトの検証
カスタムファクトを導入した際、最も恐ろしいのは「ファクトの変更によって既存のカタログコンパイルが壊れること」だ。これをCI/CDパイプライン上で完全に防ぐため、`rspec-puppet` を用いたテストコードを必ず記述せよ。
modules/profile/spec/classes/monitoring_agent_spec.rb
require ‘spec_helper’
describe ‘profile::monitoring::agent’ do
context ‘when running on AWS environment’ do
let(:facts) {
{
kernel: ‘Linux’,
aws_metadata: {
‘is_cloud’ => true,
‘instance_id’ => ‘i-1234567890abcdef0’,
‘availability_zone’ => ‘ap-northeast-1a’
}
}
}
it { is_expected.to compile }
it {
is_expected.to contain_file(‘/etc/corp/agent.conf’).with_content(/environment_tier = cloud-ap-northeast-1a/)
}
end
context ‘when running on Legacy environment’ do
let(:facts) {
{
kernel: ‘Linux’,
aws_metadata: {
‘is_cloud’ => false
},
legacy_hardware: {
‘rack_id’ => ‘rack-b4-02’,
‘sensor_active’ => true
}
}
}
it { is_expected.to compile }
it {
is_expected.to contain_file(‘/etc/corp/agent.conf’).with_content(/environment_tier = legacy-rack-b4-02/)
}
end
end
—
5. プロのSREが守るべき運用の鉄則
1. タイムアウトを絶対に設定せよ: 外部APIやレガシーハードウェアを叩くカスタムファクトにおいて、タイムアウト設定の欠如はPuppetエージェント全体の停止(ハング)を招く。ネットワークI/Oを伴う処理は必ず1〜2秒のタイムアウトを設けよ。
2. ファクトの名前空間を汚染するな: 組織独自のカスタムファクトは、プレフィックス(例: `corp_`, `company_` など)を付与するか、ハッシュ構造(例: `$facts[‘aws_metadata’]`)にまとめてグローバルなファクトスペースとの衝突を防げ。
3. CIで静的解析とテストを義務化せよ: プルリクエスト時には `puppet-lint` と `rspec-puppet` がGitHub Actions等のCIで自動実行されるパイプラインを構築し、人間の目によるレビューコストを極小化せよ。
ここまで実装できれば、あなたの管理するインフラストラクチャは、クラウドの動的なスケーリングにも、レガシーな物理環境の制約にも、一切揺るがない「真に自律的なシステム」へと昇華されるはずだ。さあ、コードを書き、テストを回し、優雅なインフラ自動化ライフを謳歌してほしい。