Chef脱落者が、Itamaeで快適インフラ生活する話

こんにちは。[株式会社ベーシック](https://basicinc.jp)のCTOやってる[@zaru](http://twitter.com/zaru)です。今年はじめてQiitaのアドベントカレンダーに参加しました。25日埋まるようなんとか頑張ります。また、ベーシックのエンジニアについて興味のある人は[ベーシックエンジニアのQiita記事](http://qiita.com/organizations/basicinc)に目を通してみてください。それなりに面白い記事があると思います。

# 長い前置き

Chef、めっちゃ流行って今や定番ツールになってますね。僕はChefに挑戦したものの脱落したダメエンジニアです。なんで脱落したかというと、セコセコ作ったレシピを保守できなかったんですね。Chefさわれる人が社内に全然いない＆教えようにも自分がよく分かっていないという、ツールに振り回されてダメダメという状況になってしまいました。

そこでもういっそ、VagrantBoxを直接配布したり、AWSならAMI化されたものを使いまわしたりしてたわけです。そこまで大規模なインフラでもないし、これで十分かーとか言い訳しつつ。コマンド一つで実行できるから教える手間もないしね。

でもやっぱり、時は残酷なもので、新しいメンバーが入ったり、テスト環境をサクッと作らなきゃいけない事があったり、Webサーバのミドルウェアを入れ替えたりといった、インフライベントがあるわけで、そんなの人力手動で乗り切るなんてツライわけです。

前置きが長かったですが、救世主とも言えるItamaeさんによって快適なインフラ生活を過ごすことができるようになりました。

## そもそもの問題

- ローカルとステージングとプロダクション環境がバラバラ
- インフラ変更に伴うレビュー体制がない
- Chefをチームメンバー全員が覚える余裕はない

ベーシックでは、大きく3つの問題がありました。

## 環境がバラバラ

手動でインフラを管理したり、ローカルのVagrant環境はエンジニアによってChef + Berkshelf使ったり、誰かからかもらったVagrantBoxを使っていたり、お手製だったりバラバラでした。豚バラですね。

これは、Chefを代表とするプロビジョニングツールで解決できますね。

## レビュー体制がない

今まではインフラが得意なエンジニアがサーバの面倒を見て、手動でゴニョゴニョしてたわけなんですが、そうなると変更作業に対して他のエンジニアの目が入りにくいんです。せいぜい隣りにいるエンジニアに目検するくらい。こうなると、サービスを支える大事なインフラがどんな構成になっているのか把握できなくなってしまいますし、ミスってサービスがダウンすることもあるわけです。

これも、Chefの要素の1つである「Infrastructure as Code」で解決できます。インフラをコード化する。素晴らしい。プルリクで改善されるサーバ達。

## Chefを覚えられない

上記2つの問題だけであれば、Chefで良いじゃんとなるわけですが、Chefをチームメンバー全員に覚えてもらうコストがバカにならないな、と自分でChefを学習していて思いました。

ベーシックではインフラ専任のエンジニアはいないため、メイン業務はアプリケーションの開発です。片手間でも使いこなせるくらいの簡易なツールでないとダメでした。

そこで軽量版Chefと呼ばれるItamaeをさわってみると、簡単というレベルじゃない。マニュアルをサラッと読むだけで普通に使えて驚きました。おどろ木ももの木さんしょの木。

Chefを覚えられないなら、Itamaeを覚えれば良いじゃない。

# Itamaeの使い方

Itamaeは、`itamae`コマンド。これ1つだけで完結します。素晴らしい。レシピはRuby DSLで記述します。ここはChefっぽいです。

基本的にItamaeは実行環境にだけインストールされていれば、SSH経由でリモートサーバに適用するため、リモート環境にはインストールされている必要ありません。

（それなりの数のサーバに適用する場合は、SSH経由だと遅いため、対象サーバにItamaeをインストールしてレシピを転送する方法もあります）

```
$ itamae ssh -h 192.168.1.100 -u user recipe.rb
```

このコマンド一発で、指定レシピが実行されます。

## Itamaeベストプラクティス

```
$ gem i itamae
```

でインストールしても良いんですが、ItamaeのプラグインGemなどもインストールする可能性があるため、プロジェクトごとにGemfileを作ってやるのが良いと思います。

```Gemfile
source "https://rubygems.org"

gem "itamae"
```

```
$ bundle install --path vendor/bundler
```

Itamaeはディレクトリ構成などしばりはないんですが、みんなで使うならルールはあったほうが良いということで、公式でベストプラクティスが紹介されています。

[Best Practice](https://github.com/itamae-kitchen/itamae/wiki/Best-Practice)

なるべく再利用可能になるようサービス単位で分割するのが良いです。そして`include_recipe`を使って読みこめばOK。板前なので`sabaku.rb`（捌く）っていうファイル名にして、いろんなレシピをぶち込んでます。

また、下の方で紹介しているテンプレートファイルなどで使用可能な変数を指定するノードファイルは、環境ごとに分けると楽です。

```
.
└── nodes
    ├── development.yml
    ├── production.yml
    └── staging.yml
```

### Dry-runで事前確認

本番サーバに対していきなり`itamae`コマンドをぶち込むのは流石に怖い。コードレビューが通っていたとしても、念のため確認したい。なぜなら、本番サーバに対してテスト環境のノードファイルとかレシピを指定しちゃっているかもしれないから！

そんな時のために、`--dry-run`オプションが用意されています。これは、変更はせず差分だけ確認ができるというもの。これで変更内容を確認して問題なければ適用するようにしましょう。

## Itamae逆引き

レシピを書くにはいくつかのリソースを覚えれば大抵のことは実現できます。

### パッケージのインストール

#### package

```ruby:recipe.rb
package "nginx" do
  action :install
end

%w(readline readline-devel).each do |pkg|
  package pkg
end
```

### RPMファイルを直接指定してインストール

#### package

yumのデフォルトリポジトリに存在しているパッケージなら、パッケージ名を指定すれば良いんですが、ない場合もあります。そんな時でもURL指定でRPMファイルをインストールすることができます。

冪等性を保つためにも、`not_if`でインストールされていなければという条件をつけたほうが良いでしょう。まぁ、この場合は`not_if`してもしなくても変化はないと思いますが。

```ruby:recipe.rb
package "http://yum.postgresql.org/9.3/redhat/rhel-6.7-i386/pgdg-redhat93-9.3-2.noarch.rpm" do
  not_if 'rpm -q pgdg-redhat93'
end
```

### 指定リポジトリを有効にするオプションを付けたい

#### package

`package`に限った話じゃないですが、コマンドにオプションを付けることが出来ます。今回の例だと、`yum`コマンドに`enablerepo`オプションを付けています。

```ruby:recipe.rb
package "jpegoptim" do
  options "--enablerepo=epel"
end
```

### サービスの起動／停止／自動起動

#### service

いわゆる`chkconfig on`とサービスの起動です。

```ruby:recipe.rb
service 'nginx' do
  action [:enable, :start]
end
```

指定ファイルを監視して、更新されたら再起動する`subscribes`や`notify`が便利です。

```ruby:recipe.rb
service "nginx" do
  subscribes :restart, "template[/etc/nginx/conf.d/virtual.conf]"
end

template "/etc/nginx/conf.d/virtual.conf" do
  notifies :restart, "service[nginx]"
end
```


### ファイルの転送

#### template

テンプレートファイル（ERB）を転送することが出来ます。オーナーやパーミッションなども指定が可能です。テンプレートファイルというくらいなので変数で中の数値を変更できます。`itamae`コマンド実行時にYAML/JSONファイルで変数を指定できます。

```ruby:recipe.rb
template "/etc/nginx/conf.d/virtual.conf" do
  owner "root"
  group "root"
  mode "644"
  source "./templates/etc/nginx/conf.d/virtual.conf.erb"
end
```

下記は、YAMLファイル形式で指定した場合の例です。

```
$ itamae ssh -h 192.168.1.100 -u user -y nodes/development.yml recipe.rb
```

```nodes/development.yml
hoge:
  piyo: "fuga"
```

普通にERBで書けるので馴染み深いですね。

```example_template.erb
<%= node[:hoge][:piyo] %>
```

#### remote_file / remote_directory

テンプレートファイルではなく、単純にファイルを転送したい場合は`remote_file`や`remote_directory`を使うと良いです。ディレクトリ単位でも転送できるので楽です。

```ruby:recipe.rb
remote_file "/etc/nginx/conf.d/.htpasswd" do
  owner "nginx"
  group "nginx"
  source "./templates/etc/nginx/conf.d/htpasswd"
end

remote_directory "./files/etc/nginx/ssl" do
  action :create
  path "/etc/nginx/ssl"
  source "./files/etc/nginx/ssl"
  mode "600"
  owner "root"
  group "root"
end
```

### ディレクトリを作成したい

#### directory

もう単純に`directory`指定でディレクトリを作成できます。

```
directory "/home/user/hoge"
```


### 条件を指定して、コマンドを実行したい

#### execute

自由にコマンドを打ちたい時ってあると思います。レシピゴリゴリ書いて最適化させていこうとすると、どうしてもコマンド書かなきゃいけない時が来る気がします。その場合、コマンドを実行すべきかどうかを判定しなければ、Itamaeを実行するたびに重複して変になってしまいます。そこで`not_if`です。便利です。

指定ファイルに指定文字列があったら実行しない、という感じです。

```ruby:recipe.rb
execute "add rbenv path" do
  command <<-"EOH"
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> /home/#{node[:server][:user]}/.bashrc
echo 'eval "$(rbenv init -)"' >> /home/#{node[:server][:user]}/.bashrc
EOH
  not_if "grep -q rbenv /home/#{node[:server][:user]}/.bashrc"
end
```

### コマンドの実行結果を受け取りたい

#### run_command

コマンドを実行した後の出力を受け取って、レシピの中で利用したい時ってあると思います。`run_command`を使えば標準出力を受け取れます。第2引数で`error: false`を指定してエラーを無視すると良い感じに動きました。

```ruby:recipe.rb
result = run_command('sudo -i -u postgres cat .bash_profile | grep PGDATA=', error: false)
PGDATA = result.stdout.strip.gsub('PGDATA=', '')
```

### 対象サーバの情報を知りたい

ItamaeはSpecinfraというGemを利用していて、Specinfraで取得できるサーバの情報を利用することが出来ます。

```ruby:recipe.rb
p node[:platform] # redhat, ubuntu, darwin etc...
p node[:platform_version] # 7.0
p node[:hostname] # localhost
```

参考: [Specinfra Host Inventory](http://serverspec.org/host_inventory.html)

また、Ohaiにも対応していてOhaiで取得する情報も利用できます。`--ohai`オプションを付けるだけです。

```
$ itamae ssh -h 192.168.1.100 -u user --ohai recipe.rb
```

参考: [Itamae | Ohai 連携オプション](http://qiita.com/tbpgr/items/6eb6947cfca190184d84)


### ノードファイルのバリデーションをしたい

ノードファイルの値にバリデーションを定義できるようですが、僕はまだ使ったことが無いです。

参考: [Use node.validate! in recipes that will be included.](https://github.com/itamae-kitchen/itamae/wiki/Best-Practice#use-nodevalidate-in-recipes-that-will-be-included)

## Itamaeプラグインを作る

ItamaeはChefと比べるとエコシステムがまだまだ弱くプラグインの数が少ないです。まぁ、プラグインを使わなくともある程度のことは簡単にできるっていうのもありますが、プラグインがあるなら使いたい！というわけで、皆がプラグインを作ってGemで公開すれば幸せになるので是非作りましょう。作り方も簡単。

Gem名は、`itamae-plugin-recipe-hoge`のような形式にします。bunldeコマンドでさくっとGemのテンプレを作ります。

```
$bundle gem itamae-plugin-recipe-hoge -t
```

僕が作ったzabbixプラグインのディレクトリ構成です。単純なレシピであれば`itamae/plugin/recipe/hoge.rb`で完結してもいいし、機能別に分けて`hoge::piyo`と呼び出させても良いと思います。

参考: [itamae-plugin-recipe-zabbix](https://github.com/zaru/itamae-plugin-recipe-zabbix)

```
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── itamae-plugin-recipe-zabbix.gemspec
└── lib
    └── itamae
        └── plugin
            └── recipe
                ├── zabbix
                │   ├── agent.rb
                │   ├── templates
                │   │   └── etc
                │   │       └── zabbix
                │   │           └── zabbix_agentd.conf.erb
                │   └── version.rb
                └── zabbix.rb
```



## 独自のリソースを定義する

プラグインを作っていると、独自のリソースを定義したくなることがありますよね。`define`で簡単に実現できます。

```ruby:recipe.rb
define :piyo, version: [] do
  name = params[:name]
  package params[:name] do
    version params[:version]
    action :install
  end
end

piyo 'nginx' do
  version '1.6.1'
end
```

プラグインの書き方は、すでに公開されているプラグインのソースを参考にすると良いかもしれません。

## お世話になっているプラグインたち

- [rbenv](https://github.com/rutan/itamae-plugin-recipe-rtn_rbenv)
- [selinux](https://github.com/mizzy/itamae-plugin-recipe-selinux)
- [nodebrew](https://github.com/toihrk/itamae-plugin-recipe-nodebrew)

### 僕が作ったやつ

CentOS / RedHat系しか考慮されてないので、改善せねば…

- [unicorn](https://github.com/zaru/itamae-plugin-recipe-unicorn)
- [zabbix](https://github.com/zaru/itamae-plugin-recipe-zabbix)

# さいごに

僕がItamaeに出会って試して実戦投入するまでの期間は約1週間弱くらいです。使い方は1日程度で把握でき、あとは運用の仕方を肌感つかむまでトライアンドエラーを繰り返した感じです。

また、他のメンバーでRubyにあまり縁がないエンジニアでもレシピを改善したりと保守面でも問題なく行けている感じがします。

次は、CloudFormationから、Terraformに行ってAWSなどのクラウドサービスも含めたコード化を進めたいところです。

明日の担当は…[@zaru](http://twitter.com/zaru)です。がんばります。