CapistranoとItamaeを使った複数環境へのプロビジョニング

前回ブログ再開宣言をしてから3週間たってしまった…

blog.neuro-inc.jp

ということで、再開第一弾いきます.

今回は、弊社で最近よく使われているサーバーのセットアップ方法をご紹介します.

前提

プロビジョニングツールにはitamae使ってます.

軽量なChefって感じで、困ったらソース読める規模なので安心. 機能も十分. Chefも昔使ってけど、ウチの規模だとオーバーだった.

仕組み

f:id:unosk:20170328162932p:plain

Capistranoを使って、各サーバーに対してItamae sshを実行している.

ROLES=web,app bundle exec itamae ssh ./ops/itamae/bootstrap.rb \
  --node-json xxx.json \
  --host xxx --user xxx

Itamae側の話

bootstrap.rbはItamaeのエントリーポイントで、 環境変数ROLESによって各サーバーの役割(WEBサーバーとか)に応じたレシピを読ませている.

# boostrap.rb
include_recipe './helper.rb'

(ENV['ROLES'] || '').split(',').each do |role|
  begin
    include_role role
  rescue
    nil
  end
end
# roles/web.rb
include_cookbook 'nginx'
include_cookbook 'unicorn'
# roles/worker.rb
include_cookbook 'unicorn'
# helper.rb
module RecipeHelper
  def include_cookbook(name)
    cookbook, recipe = name.split('::')
    recipe ||= 'default'
    include_recipe(::File.expand_path("../cookbooks/#{cookbook}/#{recipe}", __FILE__))
  end

  def include_role(name)
    include_recipe(::File.expand_path("../roles/#{name}", __FILE__))
  end
end
Itamae::Recipe::EvalContext.include(RecipeHelper)

ちなみItamaeの構成はベストプラクティスを参考にして、こんな感じ.

├── bootstrap.rb
├── cookbooks
│   ├── app
│   ├── apt
│   ├── elasticsearch
│   ├── java
│   ├── nginx
│   └── ...
├── helper.rb
└── roles
    ├── base.rb
    ├── search.rb
    ├── web.rb
    └── worker.rb

特に、Itamae側には特別なことはしてないはずなので、 今回説明しているCapistranoからの実行が嫌になっても簡単に別の仕組みに乗り換えられる.

Capistrano側の話

Capistrano側では、先ほど説明したbootstrap.rbitamae sshで実行させればよい.

これね↓

ROLES=web,app bundle exec itamae ssh ./ops/itamae/bootstrap.rb \
  --node-json xxx.json \
  --host xxx --user xxx

その際のポイントが3点あって、

  • Role情報を渡してやる
  • SSH情報を渡してやる
  • 環境(本番/ステージング)で異なるノード情報を渡してやる

Role情報

これは簡単.

普通にCapistranoつかってれば、こんな感じにサーバーを定義していると思う.

このrolesオプションがそのまま、itamaeに環境変数ROLESで渡した値になる.

# config/deploy/production.rb
server "example.com", user: "deploy", roles: %w{app db web}

server "example.com",
  user: "user_name",
  roles: %w{web app},
  ssh_options: {
     user: "user_name"
     keys: %w(/home/user_name/.ssh/id_rsa),
     forward_agent: false,
     auth_methods: %w(publickey password)
     # password: "please use keys"
   }

SSH情報

これも同じ. 上のサーバー定義から持ってこれる.(やり方は、下にコード置いたからそれで)

環境毎のノード情報

config/deploy/production.rbとかで、set :itamae_vars, { foo: 1 }とかして定義して、 毎実行ごと、これをJSONファイルに書き出してitamaeに渡してやる.

itamaeのフォルダー群の中にnodes/production.jsonとかつくって入れてもいいけど、 各環境の設定値は一箇所config/deploy/production.rbにまとまってたほうが管理しやすい.

あと、今回は書かないけど、暗号化された秘密情報を展開して、とかしたいので node-jsonは動的に作りたい.

コード

コメントを入れておいたので、雰囲気だけでもつたわれば.

# lib/capistrano/tasks/itamae.rake
namespace :itamae do
  desc 'Itamae plan(dry-run)'
  task :plan do
    run_itamae(dry_run: true)
  end

  desc 'Itamae apply'
  task :apply do
    run_itamae(dry_run: false)
  end

  def run_itamae(dry_run: true)
    # ノード情報が書かれたJSONファイルを生成
    node_json = itamae_var_file.path
    on roles(:all) do |server|
      # itamae自体の実行はlocal
      run_locally do
        # SSH情報をitamaeのオプションにつめる
        options = itamae_ssh_options(server)
        options << "--color"
        options << "--dry-run" if dry_run
        options << "--node-json #{node_json}"
        # Role情報を環境変数につめる
        with(ROLES: server.roles.to_a.join(',')) do
          output = capture :bundle, :exec, :itamae, :ssh, './ops/itamae/bootstrap.rb', *options
          info server
          info server.roles.to_a.join(',')
          puts output
          puts "\n"
        end
      end
    end
  end

  # 設定ファイル(config/deploy/production.rb)で定義したitamae_varsをJSONファイルに書き出す
  def itamae_var_file
    Tempfile.open(['itamae_vars', '.json']) do |fp|
      fp.write JSON.pretty_generate(fetch(:itamae_vars))
      fp
    end
  end

  # サーバー定義から、itamae sshで使える形にSSH情報をもってくる
  def itamae_ssh_options(server)
    ssh_options = fetch(:ssh_options) || {}
    ssh_options.merge!(server.ssh_options || {})
    ssh_options[:user] ||= server.user
    ssh_options[:port] ||= server.port
    ssh_options[:keys] ||= server.keys
    ssh_options[:key] = ssh_options[:keys] && ssh_options[:keys].first

    options = []
    options << "--host #{server.hostname}"
    options << "--user #{ssh_options[:user]}" if ssh_options[:user]
    options << "--port #{ssh_options[:port]}" if ssh_options[:port]
    options << "--key #{ssh_options[:key]}" if ssh_options[:key]
    options
  end
end

設定ファイルは、例えばこんな感じ.

set :itamae_vars, {
  unicorn: {
    processes: 2
  },
  sidekiq: {
    concurrency: 10
  }
}

使い方はこう.

./bin/cap production itamae:plan
./bin/cap production itamae:apply

以上です.

後半雑になってしまったので、質問あればコメントください.

会社名が変更になったので、ブログを再開するぞ宣言

先日、弊社の会社名が変更になりました.

  • 旧: 株式会社heathrow
  • 新: ニューロ株式会社

ニューロ株式会社 | 新しい楽しさを作る会社

ということで、

気持ちを新たに、エンジニアブログを再開することを宣言する.

ブログ書く書く詐欺にならないように、この記事をログとして残し、自分へのプレッシャーとする.