Ruby
itamae

プロビジョニングツール Itamae を速くする

More than 1 year has passed since last update.

TL;DR

  • Itamae を使ってプロビジョニングしていたけど実行時間が長くて困っていた
  • ssh接続を使いまわすように改造したら40分かかっていたのが2分くらいになった(20倍の高速化)

問題

  • Itamae は内部的にサーバー側でコマンドを実行するところが多いみたい
  • コマンドを実行するたびに ssh で接続、コマンド実行、ssh 接続を切断、している(実際に仕事をしているのは Specinfra)
  • 1つ1つのコマンド実行時間は長くても数秒なので、TCPソケットのリソースがきれいに開放されるよりも早い(印象)
  • ソケットリソースが少しづつ逼迫し、スローダウンしていく(ように見える)
lib/itamae/backend.rb
def create_specinfra_backend
  Specinfra::Backend::Ssh.new(
    request_pty: true,
    host: ssh_options[:host_name],
    disable_sudo: disable_sudo?,
    ssh_options: ssh_options,
    shell: @options[:shell],
    login_shell: @options[:login_shell],
  )
end
lib/specinfra/backend/ssh.rb
module Specinfra
  module Backend
    class Ssh < Exec
      def run_command(cmd, opt={})
        cmd = build_command(cmd)
        cmd = add_pre_command(cmd)

        if get_config(:ssh_without_env) #ssh_exec メソッドでよしなにしてくれていればよかったけどそういうわけではなかった
          ret = ssh_exec!(cmd)
        else
          ret = with_env do
            ssh_exec!(cmd)
          end
        end

# snip

      def ssh_exec!(command)
        stdout_data = ''
        stderr_data = ''
        exit_status = nil
        exit_signal = nil
        retry_prompt = /^Sorry, try again/


        if get_config(:ssh).nil? # この辺で毎回ssh接続してる
          set_config(:ssh, create_ssh)
        end

        ssh = get_config(:ssh)

        ssh.open_channel do |channel|
          if get_config(:sudo_password) or get_config(:request_pty)
            channel.request_pty do |ch, success|
              abort "Could not obtain pty " if !success
            end
          end
          channel.exec("#{command}") do |ch, success| # この辺でコマンド実行
            abort "FAILED: couldn't execute command (ssh.channel.exec)" if !success
            channel.on_data do |ch, data|
              if data.match retry_prompt
                abort "Wrong sudo password! Please confirm your password on #{get_config(:host)}."
              elsif data.match /^#{prompt}/
                channel.send_data "#{get_config(:sudo_password)}\n"

一般的な解決策

Itamae のリソースをサーバー上に配置してローカルモードで実行する。
Capistranoを使ってItamaeを複数ホストに対して実行する

上記の例ではitamae sshを使わずitamae localを利用しています。itamae sshはitamae localに比べ速度が遅いため,ある程度の規模になったらlocalを使うのがお勧めです。itamae localは対象ホストにitamaeがインストールされている必要があるので,パッケージなどでインストールしましょう。

(Rails じゃないサーバーのプロビジョニングをしているので Ruby を入れるところから準備するのはだるかった……)

たぶん一番速い解決策

sshkit(Capistrano が利用している ssh 接続用ライブラリ)のコネクションプール実装を使用する Specinfra のバックエンドを追加します。

(Github) yujiorama/itamae

  • lib/itamae/backend/sshkit.rb を追加しました。
    • 本当は create_ssh メソッドをオーバーライドするだけでどうにかしたかったけどダメだった……
    • ssh_exec をオーバーライドして、lib/sshkit/backends/netssh.rb を参考に with_ssh で囲みこむようにしました
    • また、バックエンドオブジェクトのコンストラクタに ssh_without_env を追加しています
      • sshkit のコネクションプール実装は、プールのキーとして(接続先ホスト、ユーザー名、接続オプション)を文字列化した値を使用します
      • Specinfra の ssh 実装は with_env メソッドを通ると接続オプションを書き換えてしまうため、キーが変わってしまいます
      • そうすると毎回新しく ssh 接続を始めてしまうことになり意味がないので、ssh_without_env オプションでその動作を変えています
  • lib/itamae/cli.rb に(itame ssh のオプションに)手を入れました
    • --pool オプションを追加して、バックエンドを切り替えられるようにしました。いけてない

動作検証

動作環境は Vagrant(Virtualbox)、OS は CentOS 6 です。
コネクションプールを使わない状態ではこんな感じ。

$ time be rake itamae:ssh host=batch3.local > run.log
bundle exec itamae ssh --no-dry-run --log-level=debug --ssh-config=./ssh.config --sudo ./main.rb --node-yaml=nodes/local/ls.yml --host=batch3.local
ansi: 'gem install win32console' to use color on Windows

real    42m13.011s
user    0m0.015s
sys     0m0.015s

コネクションプールを使うようにした状態ではこんな感じ。

$ time be rake itamae:ssh host=batch3.local > run.log
bundle exec itamae ssh --pool --no-dry-run --log-level=debug --ssh-config=./ssh.config --sudo ./main.rb --node-yaml=nodes/local/ls.yml --host=batch3.local
ansi: 'gem install win32console' to use color on Windows

real    1m54.798s
user    0m0.015s
sys     0m0.047s

秒単位で並べてみるとずいぶん速くなっていることがわかります。

コネクションプールを使わない コネクションプールを使う
2533秒(42分13秒) 114秒(1分54秒)