無印吉澤

Site Reliability Engineering(SRE)、ソフトウェア開発、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

信用できる社内 Wiki をつくるために守ってほしい、たったひとつのルール

f:id:muziyoshiz:20170109184841j:plain

このページについて

この記事は、以前書いた「社内Wikiに情報を書くときに守ってほしい、たったひとつのルール」の続編です。前回は、個々のページをどう書くべきかという話をしましたが、今回は社内 Wiki 全体を信用できるものにする方法について考えます。

muziyoshiz.hatenablog.com

想定する環境

この記事は、ソフトウェア開発プロジェクトに関する Wiki が社内にあって、そこに各人がドキュメント(仕様書や手順書など)を書けるようになっている環境を想定しています。

私自身、ソフトウェア開発のときしか Wiki を使わないので、具体例もそのような環境に寄っています。ただ、ある程度は社内 Wiki 全般に通じる話かと思います。

ルール:「更新され続ける」ページと「更新されない」ページをはっきり分ける

ここ1年ほど社内プロジェクトをいくつか渡り歩いていたのですが、個人的には、このルールが徹底されている Wiki ほど「信用できる」と感じました。

具体的な実現方法はいくつかありますが、例えば、Wiki のホーム画面の上半分を「更新され続ける」ページへのリンク、下半分を「更新されない」ページへのリンクにします。以下は、ホーム画面の例です。

# 更新され続けるページ

- [[用語一覧]]
- [[サービス仕様]]
- [[インフラ構成]]
- [[アプリケーション構成]]
- [[開発ルール]]
- [[手順書]]
- [[主要機能の解説]]

# 更新されないページ

- [[ミーティング議事録・資料]]
- [[ユーザーストーリーごと(または機能ごと)の設計]]
- [[リリース履歴]]
- [[運用作業履歴]]
- [[障害履歴]]
- [[技術検討メモ]]
- [[現在はメンテナンスされていないページ一覧]]

誤解を避けるために強調しておくと、これはWiki を「信用できる」ページと「信用できない」ページに分けよう、という話ではありません。信用できないことがはっきりしているなら、そのページは消したほうが良いでしょう。

そうではなくて、これは「完全に信用できる」少数のページと、「条件付きで参考にできる」多数のページに分けよう、という話です。以下では、説明のために、もう少し具体的な話をしてみます。

「信用できる」とは?

この記事で言う「信用できる」という形容詞は、「信用できる技術者」を指すときの「信用できる」と同じような意味だと思ってください。

私が知っている信用できる技術者を思い出すと、大抵以下のような長所を備えています。

  • 専門分野について正確で詳しい知識を持つ
  • わからないことは素直にわからないといい、必要な調査やテストを行って、あとからそれを補う

同様の状態を Wiki で実現したものが、この記事でいう「信用できる Wiki」になります。

「更新され続けるページ」については、そこを読むだけで正確な情報を知ることができて、読み手はソースコードを読んだりする時間を節約できます。

一方、「更新されないページ」については、過去のある時点では事実だった(あるいはその書き手が事実だと信じていた)情報ですが、現在では正しいかわからない情報です。その場合も、読み手は、条件付きでその情報を参考にすることで、調査にかかる時間を節約できます。

逆に「信用できない」とは?

上で挙げた信用できる技術者の長所をひっくり返すと、以下のように、信用できない技術者になります。

  • 専門分野について、曖昧な知識しか持たない
  • わからないことについても「○○に違いない」と断言する

大抵の Wiki は放っておくと、信用できない技術者に似た、信用できない Wiki になってしまいます。例えば、こんな感じです。

  • 仕様ページに書いてある仕様が、その後の機能追加の際に更新されなかったために、現在の仕様と違う
  • ローカル開発環境の構築手順書が、ゼロから動作確認されなかったために、他の人が試すと動かない

こうなると、普通の人は「ソースコードを読むしかない」、「実際に試してみるしかない」と判断して、その Wiki を避けてしまいます。信用できない技術者に、相談する人はいないですよね。

その社内 Wiki には、実際には役に立つ情報もあったかもしれないのに、全体の信用を失ってしまったために役に立つ情報まで読まれなくなるとしたら、それはもったいないことです。

ルールを運用するためのヒント

このルールをチーム全体できちんと運用するためのヒントを、いくつか挙げておきます。

ヒント(1):「更新され続けるページ」は極力少なくする

このルールを採用する場合、何か機能をリリースするたびに、その機能に関連する「更新され続けるページ」はすぐ更新する必要があります。忙しい時期でもこのルールを守れるように、更新され続けるページは極力少なくしておいたほうがよいでしょう。

例えば、以下のような情報は、更新し続けるメリットが、更新作業のコストを上回ると思います。

  • 用語一覧
    • そのプロジェクト固有の用語一覧。主に、新メンバへの説明用。
  • サービス仕様
    • 顧客に提供するユーザーマニュアルや、サポート対象 OS・ブラウザの情報など。
  • インフラ構成
    • ネットワーク構成図や、システム構成図、IP アドレス一覧など。
  • アプリケーション構成
    • コンポーネント一覧、バッチ一覧、データベース定義書など。
  • 開発ルール
    • そのプロジェクトで採用する開発プロセス、開発用ツール、コーディング規約、テスト基準など。
  • 手順書
    • システム構築のための手順や、日常的な運用作業の手順など。
  • 主要機能の解説
    • そのプロジェクトで特に難しい機能や、わかりにくい機能の理解を助けるための解説記事。

ヒント(2):更新し続けられないと判断したら、「更新されないページ」に移す

理想的には、「更新され続けるページ」はできるだけ多いほうが良いです。ただ、開発が多忙になったり、逆に開発が一段落して開発者の人数が減ったりすると、更新にかけられる時間は減ります。

そういう場合は、どのページを残すかチーム内で議論して、更新しきれないページは「更新されないページ」に移しましょう。Wiki のページ数よりも、「その Wiki は信用できるかどうか」のほうを重視すべきです。

例えば、以下の階層にある「機能Aの解説」ページを最近更新できてない、と気付いたとします。

  • 更新され続けるページ
    • 主要機能の解説
      • 機能Aの解説

この場合、そのページを「更新されないページ」以下の階層に移す、という具合です。「機能Aの解説」ページの先頭(「このページについて」欄)に、更新できなくなった時期とその事情を書いておくと、読み手の助けになるでしょう。

  • 更新されないページ
    • 現在はメンテナンスされていないページ一覧
      • 機能Aの解説

ヒント(3):「更新されないページ」を「更新され続けるページ」に格上げしたいときは、必ず書き換える

更新されないページを書いているうちに、分量が多くなり、内容も充実したように思えるので、「更新され続けるページ」に格上げしたくなることがあります。例えば、以下のような場合です。

  • ある機能の設計に関するページが、その機能に関するよいまとめに思えるので、主要機能の解説ページに格上げしたい。
  • ある障害の原因を特定するためにソースコードを調査したときのメモが、ソースコードのよい解説に思えるので、主要機能の解説ページに格上げしたい。

しかし、元々これらのページを書き始めた目的は「主要機能の解説」ではないため、余分な情報が多く含まれている可能性が高いです。分量が多いほど更新が大変になるので、元のページとは別のページを作り、更新し続ける必要がある部分だけ残して、リライトしたほうがよいでしょう。

また、そもそも、有用な情報をすべて「更新され続けるページ」にする必要はありません。「更新されないページ」のなかに「技術メモ」のようなカテゴリを作り、そのなかに「2017年1月時点の技術メモ」のような形で残しておくという方法もあります。

ヒント(4):ルールを守るための責任者を立てる

上記のルールを徹底するためには、ある程度の強権が必要になります。

例えば、チームメンバが以下のようなことを始めてしまったときに、誰かがあるべき状態に戻す必要があります。このような行動は、各人の自主性に任せていてもなかなか防げません。意見の衝突が起こるからです。

  • 「自分がよく使うページだから」という理由で、ホーム画面にリンクを書き足す。
  • 「誰かが更新すべき情報だ(自分は更新しないけど)」という理由で、「更新され続けるページ」にページを足す。
  • 自分が必要だから更新し続けているページがあるが、責任を押し付けられたくないので、「更新されないページ」の中に残し続ける。

このような衝突を解決するために、ルールを守るための責任者をチーム内に立てて、その責任者は「このルールを守るためであればどのようにページを編集してもよい」と明言しておくのが良いと思います。メンバから異論がある場合はその責任者を中心に議論し、ときどき細かい運用を見直します。

この責任者は強権が必要で、かつ Wiki のほとんどすべての編集にざっと目を通す必要があるため、チームリーダが兼務するのが一番良いと思います。それが難しい場合は、簡潔な文章を書くのが得意な人を任命するのがよいでしょう。

まとめ

今回は、信用できる社内 Wiki をつくるために、私が個人的に実践している基本的なルールをご紹介しました。

このルールの目的は、「読み手が Wiki に期待するレベルを適切にコントロールすることで、読み手が Wiki に幻滅し、無視してしまうのを防ぐこと」です。Wiki のページを「完全に信用できる」少数のページと、「条件付きで参考にできる」多数のページに分けることで、読み手にとって信用できる Wiki をつくることができる、ということを具体例を挙げてお話ししました。

もしも、自分が後から参加したプロジェクトに「信用できない社内 Wiki」があったらどうするか? 元から使われている Wiki を大きく書き換えることもできないので、その場合は特定のページ以下を「信用できるサブ Wiki」として育てるのがよいと思います。このルールを色々アレンジしてみてください。

今回は社内 Wiki 全体の話をしましたが、個々のページを「信用できる」ように書く方法については、以前書いた以下の記事をご参考ください。

muziyoshiz.hatenablog.com

Phoenix Framework に関する有名なベンチマーク同士の関係

f:id:muziyoshiz:20161224204900p:plain

Phoenix Framework(以下、Phoenix)は、Elixir のための Web アプリケーションフレームワークです。

Phoenix の開発者 José Valim 氏は Ruby on Rails のコミッタだったため(Rails Contributors - #5 José Valim を見る限り、2014年まで?)、使い勝手はとても Rails に似ています。そのため、Phoenix は Rails とよく比較され、「Rails よりも10倍近く速い」という評判を時々目にします。

ただ、その評判の出処になったベンチマークについて、僕は具体的な内容を知りませんでした。また、記事によってベンチマークのリンク先がまちまちで、それぞれの関係がよくわかりませんでした。自分で Phoenix を使ってみるにあたり、このあたりを少し調べてみたので、その結果をメモしておきます。

最初のベンチマーク

Phoenix と Rails に関する最初のベンチマークは、Chris McCord(Programming Phoenix: Productive, Reliable, Fast の著者)による2014年7月のブログ記事のようです。

littlelines.com

この記事は「Phoenix は Rails より10.63倍速い」というベンチマーク結果を示しています。ちなみに、これは Elixir vs Ruby Showdown というシリーズの2番目のポストで、最初のポスト(Elixir vs Ruby Showdown - Part One)では「Ruby の i18n gem と比較して Elixir 版は73倍速い」という結果を出してます。

このベンチマークの内容は以下のようなものです。この特徴は、後述する他のベンチマークにも引き継がれています。

  • URL ‘/:title’ の :title 部分をコントローラに渡す
  • コントローラは、モデルの返り値を想定したマップ(固定値)の配列をビューに渡す
  • ビューは、受け取った配列を回して HTML を生成し、クライアントに返す
  • データベースへのアクセスは行わない
  • ログ出力は行わない
  • ENV = production で動作させる
  • ベンチマークツールには wrk を使って、30秒間テストする

具体的なコードの説明も載っているので、後述するベンチマークを読むにしても、まずこのブログ記事は読んでおいた方が良いです。

ベンチマーク対象の拡大

その後、このベンチマークに触発されて、ベンチマーク対象を他のフレームワークに広げたものが Matthew Rosenberg 氏により公開されました。

github.com

Phoenix は、Rails よりむしろ Sinatra に似た軽量フレームワークではないか、との理由から、比較対象は主に Sinatra にインスパイヤされたフレームワークから選ばれています。また、Phoenix の実装の基盤である Plug というライブラリを素で使った場合も追加されています。

  • Phoenix (Elixir)
  • Plug (Elixir)
  • Rails (Ruby)
  • Sinatra (Ruby)
  • Express, Express Cluster (JavaScript)
  • Martini (Go)
  • Gin (Go)
  • Play Framework (Java)

このテストは、Phoenix のバージョンアップに合わせて、何度か実施されています。

Round 1 と 2-3 はスペックが違うので単純に比較できません。

  • Round 1: 3.4GHZ Core i7 (quad core), 12GB RAM
  • Round 2-3: 4.0GHZ Core i7 (quad core), 32GB RAM

Round 1 はスペックが低いので、単純比較できるのは Round 2 と 3 のみです。Phoenix の結果だけ比較すると、Round 3 は 2 よりも若干遅くなっています。Plug の結果はほとんど変わっていないので、Phoenix に機能が増えたからでしょうか?

Round 3 の結果を見ると、Phoenix は Play より Consistency は優れているものの、Throughput は Play の約 0.47 倍となっています。Elixir は stop-the-world GC がないので Java より高速、と思っていたので、最初にこの結果を見たときはかなりがっかりしました。

高スペックなベアメタルサーバ上での結果

2015年7月に、上記の phoenix-showdown を Rackspace のベアメタルサーバ(CPU Dual 2.8 Ghz, 10 core, RAM 128 GB)で実行した結果が公開されました。

Comparative Benchmark Numbers @ Rackspace

スペックが高いほど Elixir の真価が発揮されるのか、このベンチマークでは、Phoenix の throughput は Play とほぼ同じか若干上で、Consistency では引き続き上回っています。Rails との差は更に開いて、Phoenix の throughput は Rails の15倍になっています。

感想(と職場ブログの宣伝)

素の状態では、Phoenix は Rails よりも10倍以上高速なのは確かなようです。ただ、他のコンパイル言語(Java や Go)と比較すると、throughput については決定的な差は無さそうです。

また、これらのベンチマークはデータベースアクセスやログ出力といった、普通のアプリには絶対にある機能を省いているので、周辺ライブラリの充実度によっては、開発者の多い Java & Play Framework で実装した方が、総合的には速くなるのかもしれません。Rails 開発者が多い環境なら Phoenix に飛びついてもよさそうですが、Java 開発者が多い環境では Play のほうが速い、ということもあるのかもしれません。

そのあたりが気になって、Elixir (その2)とPhoenix Advent Calendar 2016 の24日目として、Phoenix で書いた簡単なアプリケーションサーバに、HBase アクセスや、ファイルへのログ出力を足したら、性能はどう変わるか、という話を書きました。時間の都合で Play との比較まではできていませんが、今後気が向いたらそこまで試してみたいと思っています。

recruit.gmo.jp

記事の公開後に教えてもらったページ

togetter.com

Exrm を使った Phoenix アプリケーションのデプロイ方法を ansible-elixir-stack から学ぶ

f:id:muziyoshiz:20161122004038p:plain

これまでのあらすじ

Elixir の世界には、Ruby での Ruby on Rails に相当する "Phoenix" という Web アプリケーションフレームワークがあります。しかし、Capistrano に相当するものは無くて、デプロイの考え方は Rails とはだいぶ違いそうです。

前回は Elixir の世界のビルドツール Elixir Release Manager (Exrm) で作った tarball をデプロイする方法について紹介しました。ただし、upgrade コマンドで無停止アップグレードしたいなら、ビルド環境には最新のソースコードだけでなく、アップグレードする前のバージョンのビルド結果も置いておかなければいけない(!)という話をしました。

muziyoshiz.hatenablog.com

今回は、この Exrm を使ったビルド方法の話の続きです。

Phoenix アプリケーションの情報に関するネット上の情報

Phoenix アプリケーションのデプロイについてネット上の情報を探したところ、(検索ヒット数はかなり少なかったのですが)ansible-elixir-stack という Ansible role と、その紹介記事が見つかりました。この Ansible role のコードを読んでみたところ、デプロイ方法がやっと理解できたので今回ご紹介します。

ansible-elixir-stack の使い方

github.com

ansible-elixir-stack は ansible-galaxy からインストールできます。

$ ansible-galaxy install HashNuke.elixir-stack

Phoenix アプリケーションの mix.exs に exrm を追加してから、

$ curl -L http://git.io/ansible-elixir-stack.sh | bash

を実行すると、その Phoenix アプリケーションのディレクトリ内に、Ansible の実行に必要なファイル(playbook や inventory など)が自動生成されます。自動生成された playbook は ansible-elixir-stack を呼び出して、inventory ファイルに記載されたサーバに Phoenix アプリケーションをデプロイします。

この ansible-elixir-stack という role は基本的にオールインワンなので、Nginx サーバなども自動的にインストールしてしまいます。開発環境の構築に使うならこのままでいいかもしれませんが、本番環境を構築する場合、これを参考に独自の playbook を書く必要がありそうです。

ansible-elixir-stack を実行するための手順については、以下のブログ記事で詳しく紹介されています。そのため、この記事では特に触れません。

https://blog.johanwarlander.com/2015/07/30/deploying-a-phoenix-application-using-ansible-elixir-stackblog.johanwarlander.com

ansible-elixir-stack は自動アップグレードをどうやって実現しているのか?

deploy_type 変数での動作の切り替え

ansible-elixir-stack を普通に使うと、前回のブログ記事に書いた「方法1. ソースコードをサーバに置いて、mix phoenix.server で起動」と同じように、サーバを1回停止して、再起動します。

ただし、deploy_type という変数に "upgrade" という値をセットしておくことで、無停止アップグレードの動作に切り替わります。この動作の切り替えについては Hot code-reloading のページ に記載されていました。今回はこちらの動作を解説します。

role 内部で実行されるコマンド

ansible-elixir-stack では、初回デプロイ時の playbook(setup.yml)と、2回目以降のデプロイ時の playbook(deploy.yml)が分かれています。ただ、いずれの場合も project.yml の以下の部分で git clone を実行し、サーバ上に Phoenix アプリケーションのソースコード一式をダウンロードします。

- name: "clone project"
  git:
    repo: "{{ repo_url }}"
    version: "{{ git_ref }}"
    dest: "{{ project_path }}"
    accept_hostkey: True
    force: True
    remote_user: "{{ deployer }}"

デフォルトでは project_path は /home/deployer/projects/{{ app_name }} です。

そして、release.yml の以下の部分で、Phoenix アプリケーションをビルドします。ちなみに、mix は ~/.mix 以下にインストールされたファイルを使うため、bash -lc の指定は必須です。

- name: "compile and release"
  command: bash -lc 'SERVER=1 mix do compile, release' chdir="{{ project_path }}"
  remote_user: "{{ deployer }}"
  environment:
    MIX_ENV: "{{ mix_env }}"
    PORT: "{{ app_port }}"

そして、最後の部分で、「git clone した最新バージョンのバージョン番号取得」、および「upgrade コマンドの実行」を行います。

- when: deploy_type == "upgrade"
  name: get app version
  command: bash -lc "mix run -e 'IO.puts Mix.Project.config[:version]'" chdir="{{ project_path }}"
  remote_user: "{{ deployer }}"
  register: app_version

- when: deploy_type == "upgrade"
  name: set upgrade command
  set_fact: upgrade_command='rel/{{ app_name }}/bin/{{ app_name }} upgrade "{{ app_version.stdout }}"'

- when: deploy_type == "upgrade"
  name: upgrade app
  command: bash -lc "{{ upgrade_command }}" chdir="{{ project_path }}"
  remote_user: "{{ deployer }}"
  environment:
    MIX_ENV: "{{ mix_env }}"
    PORT: "{{ app_port }}"

上記の upgrade コマンドの実行は、仮にアプリケーション名を sample_app、バージョン番号を 0.0.2 とすると、以下と同じ意味になります*1

$ cd /home/deployer/projects/sample_app
$ SERVER=1 mix do compile, release
$ rel/sample_app/bin/sample_app upgrade 0.0.2

デプロイ作業を常にこの playbook で実行しているなら、デプロイ先サーバの以下のディレクトリには、前回のバージョンのビルド結果が残っているはずです。

/home/deployer/projects/sample_app/rel/sample_app/release/0.0.1

そのため、前回のブログ記事で問題に挙げた「ビルド環境に、アップグレードする前のバージョンのビルド結果も置いておかなければいけない」という条件はクリアされます。

しかし、そう考えると新しく追加したサーバにいきなりバージョン 0.0.2 をデプロイする、という場合にはどうなるのか?が気になります。そういうケースでは、git clone しても上記の 0.0.1 ディレクトリは作られません。ただ、その場合はアプリはまだ動いていないため、単にバージョン 0.0.2 のアプリが新たに起動されるだけで、特に問題は起こらないようです。

この方法の懸念点

サーバごとのコードの状態の違い

Capistrano の場合は、通常はデプロイするたびに新しいディレクトリで git clone が実行されます。そのため、すべてのサーバで、余計なファイルがない環境で最新のコードが実行されるという安心感があります。

一方、上記の方法では、force=Yes で git clone が実行されるとはいえ、すべてのサーバ上のファイルが同じにはならない可能性があるのが気になります。まあ、そんなことを気にするなら、無停止アップグレードは諦めて Docker でも使え、という話かもしれません。

バージョン番号を上げるのを忘れそう

この方法に限らず、Exrm の upgrade コマンドを使う場合に共通した問題ですが、デプロイのたびに毎回必ず mix.exs 内のバージョン番号を上げて、git push する必要があります。ちょっとした更新のたびにこれを実行するのはかなり面倒です。

def project do
  [app: :hello_phoenix,
   version: "1.4.1",
   elixir: "~> 1.0",
   ...

この問題への対応として、ansible-elixir-stack の作者は Hot code-reloading のページ にて「バージョン番号の末尾に Git のコミットハッシュ値を自動的につける」という方法を提案しています。

def project do
  {result, _exit_code} = System.cmd("git", ["rev-parse", "HEAD"])

  # We'll truncate the commit SHA to 7 chars. Feel free to change
  git_sha = String.slice(result, 0, 7)

  [app: :hello_phoenix,
   version: "1.4.1-#{git_sha}",
   elixir: "~> 1.0",
   ...

もしこの方法を採用するなら、この対策は絶対に入れておいた方がよさそうです。

*1:SERVER=1 というのは ansible-elixir-stack が勝手に作ったフラグなので気にしないで OK です。

Exrm(Elixir Release Manager)を使った Phoenix アプリケーションのデプロイ

f:id:muziyoshiz:20161122004038p:plain

Elixir の勉強中

最近、職場の飲み会で同僚に「Erlang VM はいいぞ」と熱弁されたのをきっかけに、「プログラミング Elixir」を買って Elixir を触りはじめました。Elixir でサンプルコードを動かすだけだと身に付かなそうなので、「Phoenix で API サーバを書く」というのを当面の目標にしています。

Phoenix というのは Web フレームワークの名前で、Elixir 版の Rails みたいなものです。ディレクトリ構造なども Rails に近く、Rails 経験者にはとっつきやすい代物です。実際、MySQL から取得したデータをそのまま JSON で返すだけの単純な API サーバはすぐ書けました。

そして、この API サーバをレンタルサーバにデプロイしようとしたんですが、Phoenix には Capistrano に相当するものがないんですね。でもまあ、同じような処理を Ansible で書けば済むだろう……と最初は思ってたんですが、Erlang VM の機能をフルに使おうと思ったら Capistrano のような流儀ではうまくいかないことがわかってきました。今回はそんな話をします。

Phoenix アプリケーションのデプロイ方法

Phoenix のサイトにあるガイドでは、Phoenix アプリケーションを(Heroku とかではない)普通のサーバにデプロイする方法が2通り紹介されています。

方法1. ソースコードをサーバに置いて、mix phoenix.server で起動

1つ目の方法は、Deployment / Introduction にある方法です。

Phoenix アプリケーションをローカルで開発するときは mix phoenix server というコマンドで起動するのですが、本番環境でも同じように起動する、という方法です。以下のようにすると、デーモンとしても起動できます。

MIX_ENV=prod PORT=4001 elixir --detached -S mix phoenix.server

この方法は単純でわかりやすいのですが、アプリケーションの起動・停止や、決まったディレクトリへのログ出力、といった Web アプリで普通必要になるものを、自前で用意する必要がでてきます。

例えば、Phoenix アプリをデーモンとして起動した場合、PID を記録しておかないと停止できませんが、そういう処理を自分で書く必要があります。私の探した範囲では、How to reload the server in production. · Issue #1288 · phoenixframework/phoenix にある方法で、起動と同時に PID を記録できるようです。

elixir --detached -e "File.write! 'pid', :os.getpid" -S mix phoenix.server

また、ログ出力についても、onkel-dirtus/logger_file_backend などを使って、出力先ファイルを指定しておく必要があります。

方法2. Exrm でビルドした結果をサーバに置いて、Exrm が自動生成したスクリプトで起動

そしてもう1つは、Deployment / Exrm Releases にある方法です。

Elixir Release Manager (Exrm) というのは、Elixir で書かれたアプリケーションを、配布可能な tarball にまとめるためのビルドツールです。開発マシンやビルドサーバ上で mix release コマンドを実行すると、以下のようなディレクトリ構成でファイルが生成されます。ここでは、ビルドしたアプリケーションの名前を、仮に admiral_stats_api とします。

rel
└── admiral_stats_api
    ├── bin  (アプリケーション管理用のスクリプト群)
    │   ├── admiral_stats_api
    │   ├── admiral_stats_api.bat
    │   ├── install_upgrade.escript
    │   ├── nodetool
    │   └── start_clean.boot
    ├── erts-8.1  (Erlang ランタイム)
    │   ├── bin
    │   ├── doc
    │   ├── include
    │   ├── lib
    │   └── man
    ├── lib  (アプリケーションが依存するすべてのモジュール)
    └── releases
        ├── 0.0.1
        │   ├── admiral_stats_api.bat
        │   ├── admiral_stats_api.boot
        │   ├── admiral_stats_api.rel
        │   ├── admiral_stats_api.script
        │   ├── admiral_stats_api.sh
        │   ├── admiral_stats_api.tar.gz (※)
        │   ├── start.boot
        │   ├── start_clean.boot
        │   ├── sys.config
        │   └── vm.args
        ├── RELEASES
        └── start_erl.data

上記のツリーで (※) を付けた admiral_stats_api.tar.gz には、このファイル自身を除く配布物すべてが入っています。この tarball をデプロイ先(仮に /var/www/admiral_stats_api とする)で解凍して、

$ bin/admiral_stats_api start

を実行するとサーバが起動し、

$ bin/admiral_stats_api stop

を実行するとサーバが停止します。ログファイルは、自動生成される log ディレクトリ以下に出力されます。

また、Exrm の凄い点として、アプリケーションの無停止アップグレードが可能です。これは、Capistrano がやるような「一瞬止めて、シンボリックリンクの向き先を変えて、再起動」という無停止っぽいアップグレードではなくて、本当に無停止で、内部状態も含めてアップグレードする、という機能です。

例えば、新しいバージョン 0.0.2 をリリースしたいときは、さっきと同じように mix release で tarball を作り、その tarball をデプロイ先の /var/www/admiral_stats_api/releases/0.0.2/admiral_stats_api.tar.gz に置いてから、

$ bin/admiral_stats_api upgrade 0.0.2

を実行すると、以下のようなメッセージが表示されて、バージョン 0.0.2 が動作し始めます。

$ bin/admiral_stats_api upgrade 0.0.2
Release 0.0.2 not found, attempting to unpack releases/0.0.2/admiral_stats_api.tar.gz
Unpacked successfully: "0.0.2"
Generating vm.args/sys.config for upgrade...
sys.config ready!
vm.args ready!
Release 0.0.2 is already unpacked, now installing.
Installed Release: 0.0.2
Made release permanent: "0.0.2"

単純な API サーバで使うにはオーバースペックな機能な気もしますが、せっかく Erlang VM を使うんだし、今回はこちらの方法でデプロイすることにしました。

しかし、自動化しようとするとうまくいかない(なんで??)

上記の手順をコマンドで手で打ちながら確認して、「なるほど完全に理解した。この手順を単に Ansible で自動化すればデプロイ自動化できるよな!」と思って、こういう Ansible playbook を書きました。

  • git clone で最新版をダウンロード
  • Git リポジトリ上に置いてない、パスワードなどを含むファイル(prod.secret.exs)を自動生成
  • コンパイル(mix do deps.get, deps.compile, compile
  • リリース用の tarball を作成(mix release
  • tarball をデプロイ先にコピー
  • tarball を /var/www/admiral_stats_api/releases/バージョン番号 に配置(解凍はしない)
  • upgrade コマンドを実行(bin/admiral_stats_api upgrade バージョン番号

で、この playbook を実行したところ、最後の upgrade コマンド実行のところでエラーメッセージが出て失敗しました。playbook と同じコマンドを手で打ったところ、こんなエラーメッセージでした。

$ bin/admiral_stats_api upgrade 0.0.2
Release 0.0.2 not found, attempting to unpack releases/0.0.2/admiral_stats_api.tar.gz
Unpacked successfully: "0.0.2"
Generating vm.args/sys.config for upgrade...
sys.config ready!
vm.args ready!
Release 0.0.2 is already unpacked, now installing.
escript: exception error: no case clause matching
                 {error,{enoent,"/var/www/admiral_stats_api/releases/0.0.1/relup"}}

一体何なのこれ……。

Phoenix のサイトや Exrm のサイトを読んでも理由が分からず、「プログラミング Elixir」にもそれらしい説明はなく、エラーメッセージで検索した結果をひたすら探し回ってわかったのですが、どうやら

「0.0.1 から 0.0.2 にアップグレードするための tarball を作るためには、 rel/アプリケーション名/releases/0.0.1 ディレクトリ以下が残っている状態で mix release コマンドを実行しなければならない」

らしいです。

改めて確認したところ、0.0.1 ディレクトリがない状態で 0.0.2 の mix release を実行したところ、コンソール出力は以下のようになっていました。

$ MIX_ENV=prod mix release
Building release with MIX_ENV=prod.
==> The release for admiral_stats_api-0.0.2 is ready!
==> You can boot a console running your release with `$ rel/admiral_stats_api/bin/admiral_stats_api console`

その一方、0.0.1 ディレクトリがある状態で mix release を実行したところ、以下のように 0.0.1 から 0.0.2 へのアップグレードのためのファイルが生成されたことを示すメッセージが増えました。

$ MIX_ENV=prod mix release
Building release with MIX_ENV=prod.
This is an upgrade, verifying appups exist for updated dependencies..
==> All dependencies have appups ready for release!
==> Generated .appup for admiral_stats_api 0.0.1 -> 0.0.2
==> The release for admiral_stats_api-0.0.2 is ready!
==> You can boot a console running your release with `$ rel/admiral_stats_api/bin/admiral_stats_api console`

ここまでのまとめ:結局どうしたらいいの?

要するに、Exrm を使って Phoenix をリリースするためには、ビルド時に、少なくとも1つ前のバージョンのビルド結果をローカルに置いておく必要があります。upgrade コマンドを使ってアップグレードする限り、これは必須のようです。

世間の人はどうやって Exrm を使っているのか調べてみたところ、例えば andrewvy/ansible-elixir という role では、ビルド結果をすべて git push していました。ただ、Phoenix アプリのビルド結果には秘密情報(prod.secret.exs)も含まれるので、Phoenix アプリを OSS にするならこの手は使えません。秘密情報をすべて環境変数から読み込むように書き換えるという手はありますが、それでも、本来 Git に登録する必要がないファイルを Git に登録しなければならない、というデメリットは残ります。

Phoenix アプリケーションを開発してる人って、普通はどうやってデプロイしてるんでしょうか? Exrm なんて使わずに、mix phoenix.server で起動する方法を採用して、起動スクリプトは自分で書いてるんでしょうか。経験者の方にぜひ教えてほしいです……。

この記事の続き

続きを書きました。

muziyoshiz.hatenablog.com

参考ページ

参考書籍

プログラミングElixir

プログラミングElixir

第18章「OTP:アプリケーション」の18.6節「EXRM − Elixir のリリースマネージャ」に、Exrm の解説が8ページほど書かれています。内部状態をマイグレートするために必要なコードについても若干説明あり。

Programming Phoenix: Productive |> Reliable |> Fast

Programming Phoenix: Productive |> Reliable |> Fast

まだざっと読んだだけですが、デプロイに関する話題は(少なくとも独立した節は)なさそうです。