デプロイ自動化とServerspec

Serverspec本の献本ありがとうございました.とても面白かったです.詳しい書評はすでに素晴らしい記事がいくつかあるので,僕は現チームでどのようにServerspecを導入したか,どのように使っているかについて書きたいと思います.

Serverspec導入の背景

今のチームではサーバーのセッアップおよびデプロイにChefを使っている.本にも書かれているようにこのような構成管理ツールを使っている場合はそのツールを信頼するべきであり,Serverspecのようなテストツールは必要ない.僕らのチームもそのような理由でServerspecの導入には至っていなかった.

しかしアプリケーションが複雑になりChefのレシピも混沌とするようになるとそれは成立しなくなる.見通しの悪いレシピはChefへの信頼度を落とす.信頼度の低下はデプロイ不信に繋がり人手(筋肉)によるテストが始まる.

サーバーの数がそこまで多くなければなんとか運用できるかもしれない.しかしサーバーの数が膨大になるとデプロイ担当者が登場し,人手によるテストに時間がかかり,本来やるべき仕事が失われる.

このような状況を解決するためにServerspecを導入した.具体的には以下を行うことを目的とした.

  • Chefレシピのリファクタリング
  • デプロイ時の人手による確認作業の自動化

1つ目に関しては現在インフラCI環境の構築中でまだ成果は出ていない(し,他に良い資料がたくさんある).2つ目のデプロイ自動化への組み込みに関しては成果が出ているのでそちらについて工夫したことなどを簡単に書いておく.

デプロイの3ステップ

デプロイは以下の3つのステップに分割できる.

  1. サービスアウト
  2. セットアップ
  3. サービスイン

まず,サービスアウトではサーバーをロードバランサから切り離してユーザ影響をなくし,デーモン化したジョブを停止してセットアップの準備を行う.次に,セットアップではChefなどを用いてミドルウェアのインストールやアプリケーションのアップデート,設定値の更新などを行う.最後に,サービスインでは遮断したアクセスの復帰,デーモンジョブの再開を行いサーバーを本来のあるべき状態に戻す.

うまく自動化できてない場合,ステップごとに人手のテストを行うことになる.これをなくすために各ステップで専用のServerspecを組み込むようにした.例えばサービスアウトをテストする場合は,以下のようにする.

$ rake spec:service_out:all 

テストが失敗した場合は,そのステップでデプロイを停止するようにした.これによりステップが確実に成功した上で次のステップ進んでいることを保証できるし,失敗した場合は問題を切り分けられるようになった.

ディレクトリの構造は以下のようになる.

├── Rakefile
└── spec
    ├── role1
    ├── role2
    ├── role3
    ├── service_in
    │   ├── role1
    │   ├── role2
    │   └── role3
    └── service_out
        ├── role1
        ├── role2
        └── role3

サービスが大きいとサービスインのやり方でさえも異なることがあるので,以下で説明するロールごとにディレクトリを分けるという戦略をサービスイン,サービスアウトでも使えるようなディレクトリ構造を採用した.

失敗を書き出す

サーバーの数が多いとホストごとにディレクトリを準備するServerspecデフォルトのやり方では限界がある.そういう場合は,ロール毎,モジュール毎ににspecをまとめ,ホストとそのロール情報を別ファイル(JSON形式など)で準備し,それを読み込みRakeタスクを定義するのが良い.

今のチームではそもそもホストとそのロールのリストを準備しそれをもとにChefを実行するという運用があったので,そのリストをそのまま利用することにした.

さらにどのホストでSpecが失敗したかを知るのも大切である.これは以下のように新しくタスククラスを作って失敗したホスト情報をファイルに書き出すようにした.

class ServerspecTask < RSpec::Core::RakeTask
  attr_accessor :target
  attr_accessor :failed_list

  def run_task(verbose)
    success = system("#{spec_command}")
    save_failed_vm if not success
  end

  def save_failed_vm
    file = File.open(failed_list, 'a')
    file.puts target
    file.close()
  end
end
ServerspecTask.new(host.to_sym) do |t|
  ENV['TARGET_HOST'] = host
  t.pattern = '...'
  t.target = '...'
  t.failed_list = ENV["FAILED_VMLIST"]
end

やると良いは,実行時に利用したホスト情報ファイルと同じ形式で失敗したファイルも作ることである.例えば,JSON形式でホスト情報を準備するなら同じJSON形式で失敗したホスト情報も書き出す.こうしておくと後で失敗したホストのみに再びServerspecを実行するということができるようになる.

上で失敗したときにデプロイを止めるようにしていると書いたが,これはこのファイルが生成されているか否かで判断するようにしている.

通知

サーバーの台数が多いとデプロイに時間がかかる.常にデプロイの様子を見ているわけにもいかないし,その間に書くべきコードがある.ので問題があったとき,各ステップが無事終了したときにそれを通知として受け取れると良い.

例えばHipChatの場合は以下のようなスクリプトを準備しておくと良い.シェルスクリプトなので何にでも組み込める.

notify() {
  local USER=$1
  local MESSAGE=$2
    
  JSON="{
    \"message_format\": \"text\",
    \"message\": \"@${USER} ${MESSAGE}\",
    \"notify\": true
  }"
    
  curl -X POST \
      -H "Content-Type: application/json" \
      -d "${JSON}" \
      https://api.hipchat.com/v2/room/...
}

以下のように使う.

$ notify Taichi "Service out is done!"

通知もServerspecの結果に基づいて行うようにしている.

不安を取り除く

そこまで多くの環境で自動化をやってきたわけではないので一概には言えないが,人手による運用が入ってるところは過去の不安が凝縮されていることが多い.不安が継承され無駄な手順が積み重なっていることが多い.

自分が無駄だと思う手順をいきなり決して自動化を推し進めてもチームとしては意味がない(やめられるならやめてるはず).なので,まずは無駄だと思うものも含めて機械にやらせるようにしてみるのが大切だと思う.「これ!ここ今まで人手で確認してたのをこうやって置き換えてます」と言えるようにしておくのが良い.

こういうことをやるのにServerspecはとてもやりやすかった.誰でも読めるし,裏でやっていることも説明もしやすい/理解しやすい.またとにかく導入が簡単であるところも良かった.シンプルなので今回のようなデプロイの一部に組み込むことも簡単だった(想定とは違う使い方かもしれないが…).