読者です 読者をやめる 読者になる 読者になる

#10 Consulと連携するpull型デプロイツール stretcher

tech.kayac.com Advent Calendar 2014 10日目担当の @fujiwara です。

最近書いている stretcher というデプロイツールの紹介をしたいと思います。

長いので3行で

  • push型デプロイはホスト台数が増減しやすい環境に適さない
  • 各種問題を解決するpull型デプロイツールを書いた
  • Consul と連携するよ

中央ホスト配布(push)型デプロイの問題点

カヤックの自社サービスでは久しく Archer というツールを利用し、中央ホストから各デプロイ対象ホストに rsync でファイルを配布する形のデプロイを行っていました。ここではこれを push 型と呼びます。

push型のデプロイは、ホスト台数が頻繁に増減する環境で以下のような問題があります。

  • 新しくホストが起動してきた場合に、中央ホストからデプロイを行ったあとでないと (古い状態で起動しているため) 利用できない
  • デプロイ中にホストが落ちてしまうと、rsync 等でエラーが発生する
  • ホスト台数が数十以上になると、ある程度の並列数で rsync を行っても順次実行になるため時間がかかる

これを解決するためには、各ホストがデプロイ対象物を取得しに行く pull型 のデプロイを行う必要があります。

リポジトリからpullするデプロイの問題点

pull型のデプロイというとすぐ思い浮かぶのは、Gitリポジトリ等に配布物を格納し、各ホストから pull する形態です。しかしこの形態には以下のような問題があります。

1: リポジトリに全てのデプロイ対象ファイルを格納する場合

  • Goで書かれたアプリケーションのバイナリ
  • JavaScript, CSS などのビルドツールの生成物
  • (Perlの場合) CPAN モジュールがインストールされた extlib ディレクトリ

など、全てリポジトリに commit することになり、特にバイナリファイルによりリポジトリが肥大化する恐れがあります。

2: リポジトリにはソースのみ格納し、各ホストで build を行う場合

  • buildを行う環境の細かい差異により、同一の生成物が得られない可能性があります
  • 全台で同じことを行うのはリソースの無駄です

stretcher による pull型デプロイ

上記のような問題を解決する、pull型のデプロイツール stretcher を Go で書きました。

github.com/fujiwara/stretcher

デプロイ手法的には、AWS CodeDeploymamiya に類似しています。

  • 配布物を全て一つのアーカイブにまとめて、Amazon S3など(http, fileも可)に保存する
  • デプロイ手順を定義した manifest (YAML) を作成し、S3などに保存する
  • consul event を利用して各ホストにイベント通知を行う
  • 各ホストで動作している stretcher agent が、イベントを受けて配布物を取得しデプロイを実行する

stretcher.png

これにより、以下の問題が解決されます。

  • 新しいホストが古い状態で起動する → 各ホストでは起動時に最新の manifest を取得することで最新状態を反映できる
  • デプロイ中にホストが落ちる → consul クラスタから離脱すればイベントは届かないので実行されない
  • ホスト台数が多い場合の負荷 → イベントは gossip protocol で伝わり、配布物は各自が S3 などから取得するので中央サーバの負荷はない
  • リポジトリ肥大化 → リポジトリに生成物は含まれないので肥大化しない

stretcher によるデプロイの流れ

  • 中央ホスト(buildホスト)で buildを行い、デプロイする配布物を tar.gz に固めて Amazon S3 などに配置します
  • 以下のような manifest ファイル(YAML)を生成し、S3 などに配置します
src: s3://example.com/app.tar.gz
checksum: e0840daaa97cd2cf2175f9e5d133ffb3324a2b93
dest: /home/stretcher/app
commands:
  pre:
    - echo 'staring deploy'
  post:
    - svc -h /service/app
excludes:
  - "*.pid"
  - "*.socket"
  • consul event コマンドを実行してイベントを作成し、manifest ファイルの URL を各ホストの consul agent に通知します
$ consul event -name deploy "[manifest URL]"

ここまでの処理は stretcher は関知しないので、各自お好みのツールで (Shell script, perl, ruby, go…) 実装してください。

stretcher による実行

stretcher は以下のように各ホストで起動しておきます。フォアグラウンドで起動するので、deamon化はお好みの方法で。

$ consul watch -type event -name deploy /path/to/stretcher

consul event が発生すると、そのイベントを入力にして stretcher コマンドが起動され、manifest ファイルの定義に従ってデプロイが実行されます。

  1. src に記述されたURLから配布物を取得し、テンポラリディレクトリに保存
  2. commands.pre に記述されたコマンドを実行
  3. 配布物をテンポラリディレクトリに展開し、dest に記述されたディレクトリにローカルで rsync -a –delete して同期
  4. commands.post に記述されたコマンドを実行 (アプリケーションの再起動など)

ごく簡単な例を stretcher/examples に置いてありますのでそちらもご参照ください。

応用例

応用例として、Chef Serverを利用しない環境でも chef-solo(zero) をpull型で実行することができます。

  1. cookbooks, roles, nodes 等が含まれたアーカイブと command.post で chef-solo(zero) を実行する manifest を作成
  2. 各ホストで起動時に最新の manifest を取得して反映

これは一昨日、既にとある環境に投入して、実際に動いています。

運用時の注意点

デプロイイベント発生時に、たまたま Consul クラスタから fail 状態で外れていた(再起動など)ホストには、クラスタ復帰時にイベントが届くので、何もしないでも最新状態を反映可能です。

ただしクラスタから明示的に leave していたり、新規に参加したホストには過去のイベントは届かないため、最新の manifest URL を Consul KVS に保存して起動時に実行するなどの工夫が必要になります。

デプロイイベント作成時にKVSに URL を保存

# curl -X PUT -d s3://example.com/20141210-112233.yml localhost:8500/v1/kv/deploy/latest

起動時にKVSから URL を取得して実行 (event通知されるJSON形式に jq で加工しています)

# curl -s localhost:8500/v1/kv/deploy/latest | jq '.[0].Value | [{Payload: .}]' | stretcher

最後に

明日は、最近 モンスターストライク(モンスト) に動画 SDK が採用されるなど絶賛成長中の自社サービス Lobi の若きリードエンジニア、@handlename さんの記事です。楽しみですね!