xbuildのpuppet化にあたり考えたこと

#puppet #xbuild

xbuildをpuppetで管理しようと思って、その構成について考えたことです。

この記事で書かないこと

  • なぜpuppetなのか
  • puppetの書き方・使い方の基本
  • puppetの本番での活用方法

この辺りは、栗林さん著の『入門Puppet』がオススメです。

入門Puppet - Automate Your Infrastructure
入門Puppet - Automate Your Infrastructure

modularize xbuild

xbuild

xbuild@tagomorisさんが開発したproduction環境向けプログラミング言語インストーラです。 現在はPerl, Ruby, Node.js, PHP, Pythonをサポートしています。

$ xbuild/ruby-install 2.1.0 ~/local/ruby-2.1.0

このようなコマンドを実行すると、ruby-2.1.0が指定ディレクトリにインストールされます。あとはPATHを追加するだけでruby-2.1.0が使えるようになる、という仕組みです。

今回はこのxbuildをpuppetで管理しようと思った時に、そのmanifests構成についてあれこれ考えたことをまとめています。

manage as a module

puppetで管理するのであれば、modulesとして以下のように管理しましょう。

puppet_dir/
|-- manifests
|   `-- a_role.pp
|-- modules
|   `-- xbuild
|       `-- manifests
|           |-- init.pp
|           `-- install.pp
`-- roles
    `-- a_role

この所以はpuppetlabsが公式にアナウンスしているBest Practiceです。

Best Practices — Documentation — Puppet Labs

Use Modules When Possible
Puppet modules are something everyone should use. If you have an application you are managing, add a module for it, so that you can keep the manifests, plugins (if any), source files, and templates together.

xbuild manifests

xbuildのmanifestsを構成する際は、以下の様な実装が考えられます。
※これは弊社ペパボでも実際に使われている、栗林さんによる実装です。

modules/xbuild/
`-- manifests
    |-- init.pp
    `-- install.pp
class xbuild {

  include xbuild::install

}
class xbuild::install {

  exec { 'install xbuild':
    path    => '/usr/bin',
    command => 'git clone git@github.com:tagomoris/xbuild.git /usr/local/xbuild',
    creates => '/usr/local/xbuild',
    require => Package['git'],
  }

}

あとはroleクラスでinclude xbuildすれば、xbuildが利用可能になります。

xbuild経由の言語のmodule化

ここからが悩んだことなんですが、xbuild経由での言語インストールを構成管理化する際に、manifestsはどのように設計すべきでしょうか。

lang modulesの配置案その1

例えば以下のような実装を考えてみましょう。

modules/
|-- ruby
|   `-- manifests
|       `-- init.pp
`-- xbuild
    `-- manifests
        |-- init.pp
        `-- install.pp

この構成におけるmodules/ruby/manifests/init.ppは以下のような実装とします。

define ruby (
  $version    = $title,
  $installdir = '/usr/local',
) {

  exec { "ruby-build ${version}":
    path    => ['/bin', '/usr/bin', '/usr/local/xbuild'],
    command => "ruby-install ${version} ${installdir}/ruby-${version}",
    creates => "${installdir}/ruby-${version}",
    timeout => 0,
  }

}

この実装によれば、例えばruby-2.1.0を扱いたいroleにおいて、::ruby { '2.1.0': }と書けばインストールが可能です。

あるいはこの実装のファイル名をmodules/ruby/manifests/install.ppに変えて、define名をdefine ruby::installとすると、呼び出し側は::ruby::install { '2.1.0': }と書くことができるので、こちらのほうが可読性が高いとも言えます。

しかし、上記の設計には次のような難所を抱えています。

module同士の依存

あるmoduleが別のmoduleを宣言するようなmodule同士の依存はあまり良くありません。 これは再配布のしにくさや、ちょっとした変更・修正からの思わぬコンフリクトを生みかねないからです。

これはpuppetlabsの公式ドキュメントにも明記されています。

Module Fundamentals — Documentation — Puppet Labs

Best Practices

The classes, defined types, and plugins in a module should all be related, and the module should aim to be as self-contained as possible.

Manifests in one module should never reference files or templates stored in another module.

Be wary of having classes declare classes from other modules, as this makes modules harder to redistribute. When possible, it’s best to isolate “super-classes” that declare many other classes in a local “site” module.

出来る限り、module同士の依存は避けるべきです。

lang modulesの配置案その2

そこで考えたのは次の案です。
modules/xbuild/manifests/{init,install}.ppの実装は上述のとおりです。

modules/xbuild/
`-- manifests
    |-- init.pp
    |-- install.pp
    |-- node
    |   `-- install.pp
    |-- perl
    |   `-- install.pp
    |-- php
    |   `-- install.pp
    |-- python
    |   `-- install.pp
    `-- ruby
        `-- install.pp

これはxbuild moduleのinplementationとして実装する方法です。

そしてmodules/xbuild/manifests/ruby/install.ppは以下の様に実装します。

define xbuild::ruby::install (
  $version    = $title,
  $installdir = '/usr/local',
) {

  exec { "ruby-build ${version}":
    path    => ['/bin', '/usr/bin', '/usr/local/xbuild'],
    command => "ruby-install ${version} ${installdir}/ruby-${version}",
    creates => "${installdir}/ruby-${version}",
    timeout => 0,
  }

}

全体像が見やすくなるよう、GitHubにリポジトリを用意しました。

この実装はmodule同士の依存がなく、名前空間的にも言語インストールにxbuildを経由していることが分かりやすくになっています。

しかし、この実装にもやはり気になるところがあります。

各言語のinstall.ppが決め打ちになっている

::xbuild::ruby::installというクラスは一見分かりやすいですが、やや具体的になりすぎています。 ここで言う具体的とは、悪く言えば限定的な設計で、変化に弱いということになります。

puppet manifestsによる構成管理を考える上で、またxbuildがサポートを拡張する可能性※も考慮すると、もう少し設計を抽象化させた方が使いやすさの向上に繋がると考えました。

※xbuildのサポート拡張可能性は私が勝手に考えているだけのことですが、プロダクトは常に変化していくものである以上、対象プロダクトに問わず適宜バランスを取るべきだと思います。

lang modulesの配置案その3

最終的に自分の中で落ち着いた実装は以下になります。

modules/xbuild/
`-- manifests
    |-- init.pp
    |-- install.pp
    `-- lang
        `-- install.pp

modules/xbuild/lang/install.ppの実装は次の通りです。

define xbuild::lang::install (
  $language   = $title
  $version,
  $installdir = '/usr/local',
) {

  exec { "{$language}-build ${version}":
    path    => ['/bin', '/usr/bin', '/usr/local/xbuild'],
    command => "${language}-install ${version} ${installdir}/${language}-${version}",
    creates => "${installdir}/${language}-${version}",
    timeout => 0,
  }

}

このmanifestの使い方は次の通りです。

::xbuild::lang::install { 'ruby':
  version    => '2.1.0',
}

こちらもGitHubにリポジトリを用意しています。

適切な抽象化と再配布可能性について

長々と遠回りをしてきた気もしますが、これぐらいの抽象度をもっておけば、例えばxbuildが新たな言語をサポートした時も対応できますし、無効な言語を指定してもxbuild側で吸収してくれます。

会社で使ってるpuppetにはまだ適応していない(休み中に思いついたので…)ですが、個人用のpuppet manifestsには早速利用しています。

puppetで管理すべき内容と、module自体(今回で言えばxbuild)で制御すべき内容についての議論は様々ありますが、今回の落とし所はこの辺りじゃないかなあと考えています。

この実装についてご意見等あればツイッター等で是非お願いします。

補足

timeoutについて

ビルドには時間がかかるため、timeoutを0(無効)にするか、大きめの数字を取る方が良いです。
※default timeoutは300秒 (ref: Type Reference — Documentation — Puppet Labs)。

  timeout => 0,

インストールディレクトリについて

今回はインストールディレクトリを/usr/local/<LANG>-<VERSION>/としていますが、これはサービスのproduction環境によって異なると思うので、利用別に検討すべきだと思います。