入門 Capistrano 3 ~ 全ての手作業を生まれる前に消し去りたい
はじめに
この記事はGREE Advent Calendar 2013年の21日目です。お楽しみください!
こんにちは、アゴひげがダンディーだと評判の九岡です。GREEでは、JavaやScalaを布教するための土台を固めるため、デプロイや監視の仕組みづくりなどを横断的にやっています。今回はその過程で得られた知識を「Capistrano 3の入門記事」という形で共有させていただきます。
全ての手作業を生まれる前に消し去りたい
この記事ではCapistrano 3の基礎をご紹介します。Capistrano 3はRubyをベースにしたサーバ操作およびデプロイの自動化ツールです。Capistrano 3を利用することで、デプロイなどの複雑なサーバ操作を自動化することができます。ここの記事では、特にデプロイに焦点をあてながら、Capistranoでサーバ操作を自動化する考え方と実現方法をご説明していきます。
Capistrano 3の習得にはある程度の学習コストがかかります。その学習コストを軽くするために本記事を書きました。本記事を読んでいただくことで、みなさんやそのまわりでCapistrano 3が普及して、世界から一つでも手作業が消えたらこれほどうれしい事はありません!
対象
この記事のターゲットは以下のとおりです。
- Capistranoの概要を知りたい方
- Capistranoの使ってみたい方
- 手作業を自動化したい方
- 既に他のツールで手作業を自動化している
上記のような方がCapistrano 3を使い始めるとき、最初に読んでいただけると捗るような記事を目指しました。
(Capistrano・Ansible・Fabricなどを使って、サーバ操作の自動化を既に行っている方にとっては、目新しいことはないかもしれません)
デプロイはなぜ手間がかかるのでしょうか?
Webサービスなどサーバアプリケーションを利用したサービスをユーザの皆さまにお届けするためには、サーバアプリケーションを開発するだけではなく、実際にそれを稼働させるサーバへデプロイしなければなりません。ところが、このアプリケーションのデプロイという作業には意外と手間がかかります。その理由は、そもそもデプロイに関する要件が多岐にわたるからです。
デプロイの要件とはなんでしょうか?
デプロイの要件は、以下のようなものがあります。
- アプリケーションの構成要素
- 実行環境や規模
- 許容時間
- 納期はいつまでか
- 予算はいくら使えるか
アプリケーションの構成要素
どんなプログラミング言語、フレームワーク、ミドルウェアをつかって開発されたアプリか?
実行環境や規模
どんなサーバ何台にデプロイするか?
許容時間
デプロイ開始から終了までにどれくらい時間をかけてよいか?
納期はいつまでか
自動化にかける時間があるのか?
予算はいくら使えるか
自動化にかける初期費用がでるのか?
例えば、毎回おなじプログラミング言語・フレームワーク・ミドルウェアを使って開発して、毎回同じサーバへデプロイするのであれば、話は少し簡単になります。しかし、そうではないところにデプロイの難しさがあります。
手作業でいいのでは?
前述のとおり多岐にわたるデプロイの要件について、わたしたちは自動化または手作業で対応します。
手作業は初期投資が少ないというメリットがある一方、手順書を作成したり作業を実施するための人的コストが継続的にかかったり、繰り返し作業する中でミスが発生する可能性があるというデメリットがあります。自動化したいところです。
自動化すればいいのでは?
・・・と思うところですが、自動化するためにも超えなければならならない壁があります。生産性と保守性についての壁です。
生産性の壁
〜シェルスクリプト? Ant? Ansible? Fabric? Capistrano?〜
新規開発する場合は、独自にデプロイの枠組みを考えるか、OSSなど既存のものを利用するかという選択肢があります。独自につくった部分は実装やテストのコストがかかりますし、かといって既存のものを流用する場合には学習コストがかかります。すこしでも楽に早く実装したいところです。
保守性の壁
〜引き継ぎは?育成は?〜
独自に開発すると、引き継ぎがむずかしい場合があります。設計や実装のポイント、何か設定できて何が設定できないのか・・・。十分なドキュメントを自分で書いて残すことができればいいのですが、時間や予算の都合でスクリプトだけ実装して、ドキュメントはない、ということもあるのではないでしょうか。すこしでも楽に引き継ぎや育成をできるようにしたいところです。
自動化したい
そんなわけで、実際にわたしたちがデプロイを自動化するときには、生産性、保守性、そしてそもそものデプロイの要件のトレードオフを考えなければなりません。これは難しい判断です。仮に「今回は時間がないから手作業でカバー」や「とりあえずシェルスクリプトで自動化しよう。ドキュメントは必要になってから」といった対応になってしまったとしても、それは仕方のないことです。
しかし、理想をいえば、生産性と保守性の壁をこえてサクッと自動化したいですよね。
Capistrano 3ではじめる自動化
前述の生産性と保守性の壁をこえる一つの方法は、学習と引き継ぎのコストを下げることです。そのためにこの記事をお送りします。
期待される結果
Capistrano採用による変化
Capistranoを採用すると、いろいろなサービスのデプロイなどの手順が「Capistranoの設定」という形に統一されます。
それではこのような変化によって、どんなメリット・デメリットが発生するのでしょうか?
メリット
保守性が高い ~maintainable~
シェルスクリプト等だと書いた人によって設計思想が異なるので、設計思想を読み取るところから時間がかかります。Capistranoを知っている人同士であれば、他人の書いたデプロイ設定も読みやすく、結果的に保守や引き継ぎがしやすくなります。
生産性が高い ~productive~
シェルスクリプトだと、使うツールをパッケージマネージャなどでインストール、依存スクリプトはgitで取得、とか色々な配布形態が人や組織によります。書くことはできても、再利用しづらいとか、新しい開発者が来た時にデプロイスクリプトの開発環境整えるのが大変などの問題があります。
RubyライブラリやCapistrano拡張という再配布の形式と、gemやbundlerのようなツールの存在により、デプロイによく使うロジックの再利用がしやすいです。インストールはbundle installすればCapistrano本体・ライブラリ・拡張ふくめて揃います。
柔軟性が高い ~flexible~
既にコマンドの実行手順を書いた手順書や、自動化のためのシェルスクリプトをお持ちでしょうか?Capistranoタスク自体は「コマンドを順番に実行する」だけなので、デプロイのためにシェルスクリプトでやっていたようなことはひと通りできます。nohupでバックグラウンドプロセス化・upstart/monit/daemontools/godなどでデーモン化・プロセス監視・プロセスIDの管理などシェルスクリプトやRubyコードで表現できることであればできます。したがって、デプロイに関していえば、Capistranoにロックインされることにより機能や要件が制限されることは少ないはずです。
読みやすく、書きやすい
まるで英語のような洗練されたDSLでスクリプトを書くことができます。
また、ローカルマシンとサーバへの操作がDSLの文法上、明確に区別されます。まずローカルマシンで何をして、それからサーバで何をして・・・という大局をつかみやすいスクリプトが自然と書けるように強制されます。
デメリット
習得に時間がかかる
確かに時間がかかります。特にCapistrano 3は2013年8月に公開されたばかりです。まだ情報が少ないうえ、日本語の情報となるとさらに限られます。新しいものを覚えるだけでも時間がかかるのに、情報も少ないのでは困ると思います。
そんなみなさんのために、この記事を書きました!この記事を読んでいただくことで、高速にCapistranoを習得できればうれしいです。
まず覚えること
大きく分けて3つを覚えればOKです。
- Capistranoのモデル
- Capistranoのワークフロー
- Capistranoの設定ファイルの書き方
Capistranoのモデル
具体的なコードや設定について知る前に、そもそもどういう枠組みの中で何をするのかという全体像を知っておくと理解が進みやすいと思います。それがすなわちCapistranoのモデルです。プログラムは設計書から、文章は目次から読み始めると全体像が頭にはいって理解しやすかったりしますよね!同じようにツールはモデルから入ってみましょう。
Capistranoのモデルを理解するために最小限必要な単語は以下のとおりです。
- Capistrano
- ライブラリ
- 設定ファイル
- ホスト
Capistrano
Capistranoはおおまかに以下の3要素から構成されています。
- capコマンド
- Capistranoのライブラリ
- デフォルトのデプロイタスク
わたしたちはCapistranoのライブラリやデフォルトのタスクを利用して設定ファイルを記述して、それをcapコマンドで実行します。すると、Capistranoがもろもろの操作を自動的に行なってくれます。Capistranoがオートメーションのツールであると言われるのはこのためです。
ライブラリ
Capistranoは単なるフレームワークなので、みなさんが開発・利用しているアプリケーション固有のデプロイ方法や、サーバの情報などは含まれていません。皆さんの作業を自動化するためには、Capistranoを皆さん好みにカスタマイズする必要があります。
カスタマイズするにあたって、一度だけ使うものと再利用するものがあると思います。再利用性が高いものはライブラリと呼びます。具体的には以下の2つです。
- Rubyライブラリ
- Capistrano拡張
設定ファイル
Capistranoのカスタマイズの際、一度だけ使う設定(例えばあるプロジェクトのサーバ情報、あるプロジェクト固有のタスク)は前述のライブラリではなく設定ファイルに記述します。具体的には以下の2つです。
- config/deploy.rb
- config/deploy/任意のステージ名.rb
「deploy.rb」はデプロイ等の共通的な設定や手順を書きます。「任意のステージ名.rb」は本番環境・テスト環境・開発環境などの環境ごとに異なる設定やタスクを書きます。
ホスト
デプロイにあたってコマンドを実行する対象マシンは大きく分けて、「ローカルマシン」と「サーバ」の2つがあります。どこでどのようなコマンドを実行するか決まっているわけではありません。例えばローカルマシンでアプリケーションをビルドして、サーバへアプリケーションをデプロイする、というような使い分けをイメージしていただければわかりやすいと思います。
以上、Capistrano・ライブラリ・設定ファイル・ホストの4つです。この4つはそらでも説明できるように覚えてくださいね!そのあとの詳細なところの理解が格段に進むはずです。
Capistranoのワークフロー
Capistranoを使うためには、いつ、何のために、どんな操作をしてどんなコードを書けばよいでしょうか?その全体の流れをおさらいしておき、それから具体的なコードと操作を覚えていきましょう。
- Capistranoのインストール
- 設定ファイルのひな形をつくる
- 設定ファイルのカスタマイズ
- capコマンドを実行する
以上の流れにそって、具体的な説明をしていきます。さっそくターミナルを開いて、みなさんの環境に置き換えながら、実際にCapistranoを使ってみましょう!
Capistranoを使ってみましょう
1. Capistranoのインストール
〜コマンド一発でインストールできます〜
RubyGemsをインストールしてあれば、gem install capistrano
コマンドを実行するだけでCapistranoのインストールは完了です!もしRubyGemsをroot権限ありでインストールされた場合は、sudoをお忘れなく。
Bundlerをお使いの場合は、Gemfileにgem 'capistrano', '~> 3.0.1'
の記述を追加してbundle install
を実行しましょう。Bundlerをお使いでない場合は気にしなくてOKです。
2. 設定ファイルのひな形をつくる
Capistranoのインストールが完了したら、Capistranoの設定ファイル配置先となるディレクトリを作成します。名前はなんでもOKです。
そしてcap install
を実行することで、前述の「設定ファイル」のひな形が生成されます。
1 2 3 |
$ mkdir test-project $ cd test-project $ cap install |
3. 設定ファイルのカスタマイズ
cap install
を実行すると、すると前述の「設定ファイル」が生成されます。これをカスタマイズして、自動化を実現します。
4. サンプル設定ファイルの完成イメージ
理解しやすいように、まず完成イメージを共有しておきます。これから、皆さんと一緒にこの完成イメージに向かって設定ファイルの書き方をご説明していきます。
完成イメージは以下のとおりです。現時点ではイメージが掴めなくてもOKです。
config/deploy/test.rb
1 |
作業対象サーバの設定 |
config/deploy.rb
1 2 3 4 5 6 7 |
Capistranoデフォルトタスクの消去 タスク「ソースコードの取得」の定義 タスク「ビルドとアーカイビング・パッケージング」の定義 タスク「アプリケーションのビルドとインストール」「アプリケーションの起動と停止」の定義 |
5. Capistranoデフォルトタスクの消去
まず、Capistranoのデフォルトタスクを全て消します!
前述のとおり、Capistranoにはフレームワークに則ったデフォルトタスクが存在します。デフォルトタスクには、Capistranoの推奨するデプロイ方法に則ったロジックが含まれます。例えばシンボリックリンクで以前のリリース物を残しておくなどです。
ここでシンボリックリンクを使うかどうかといったことは実装の詳細であり、この記事の範囲外ということにします。そうすることで、わたしや皆さんはまず「これまで手動、自動でおこなってきたデプロイをそのままCapistrano化するにはどうしたいいか」というタスクに集中することができます。
Capistranoが推奨するデプロイの仕様を把握し、既存のデプロイ手順をそれに合わせて変更してからCapistranoへ移行するのは手間がかかります。それよりもまず移行してしまってから、必要に応じてCapistranoの推奨スタイルへ徐々に寄せていくほうが容易なはずです。なぜなら、一度に変えなければならないことが少ないからです。それを実現するためには、Capistranoのデフォルトタスクを一度消してしまいます!
1 2 3 4 5 6 7 |
framework_tasks = [:starting, :started, :updating, :updated, :publishing, :published, :finishing, :finished] framework_tasks.each do |t| Rake::Task["deploy:#{t}"].clear end Rake::Task[:deploy].clear |
Capistranoにはデフォルトで「deploy」というタスクと、それが呼び出す複数の「deploy:サブタスク」が定義されています。Capistranoデフォルトの枠組みの中でデプロイを行う場合は、「deploy:サブタスク」を再定義するだけでよいので便利です。しかし、それらが必要ないので消去します。消去を行っているコードがRake::Task[消去したいタスク名].clear
です。実はCapistrano 3はRakeというビルドツールを応用して作られています。task
でタスクを定義して、それをコマンドラインアプリケーションから実行することができるは、Rakeの機能です。今回はその事実を応用して、Rakeの機能を使ってデフォルトタスクを消去します。
デフォルトタスクはCapistrano 3のframework.rakeに定義されています。興味のある方は読んでみてください。
6. config/deploy/ステージ名.rbを書く
ステージ名.rb(今回はステージ名をtest
とします)には、ステージ毎に異なる設定を書きます。例えば以下のようなものがあります。
- そのステージで作業対象サーバ
- そのステージだけで実行するタスク
典型的なのは、前者の「作業対象サーバ」の設定です。この設定には以下のような情報が含まれます。
- ホスト名
- サーバロール
- ログインユーザ
- SSH設定
- その他、そのサーバに紐づく任意の設定
この設定を行うためには、server
という語彙と、
server ホスト名, user: ログインユーザ名, roles: %{サーバロール}, その他の設定: 値, ...
という文法を使います。
server
を使って1つのサーバを設定する例を以下に示します。
1 |
server 'localhost', user: 'vagrant', roles: %w{web} |
この一行には以下の設定情報が含まれています。
localhost
(Capistranoを実行したホスト)を対象にする- そのサーバに「Webサーバ」というロールを与える
- そのサーバへはvagrantというユーザでログインする
7. config/deploy.rbを書く
config/deploy.rbにはステージ間で共通の設定を書きます。よくあるのは以下のような設定です。
- アプリケーション名
- レポジトリ名
- 利用するSCM
- タスク
- それぞれのタスクで実行するコマンド
このような設定をDSLで書きます。DSLには以下を定義するための文法と語彙があります。
- 設定値の変更と取得
- タスクの定義
設定値の変更と取得
アプリケーション名やレポジトリ名などの設定値はset 名前, 値
で設定し、fetch 名前
で取り出します。
一度set
した値はdeploy.rbやステージ.rbの全域で取り出すことができます。
1 2 3 4 |
set :repo_url, 'git@github.com:mumoshu/finagle_sample_app' fetch :repo_url #=> "git@github.com:mumoshu/finagle_sample_app" |
タスクの定義
個々のタスクはtask タスク名 do; ... end
のブロックで記述します。
例えば、:uptime
という名前のタスクは以下のように定義します。
1 2 3 |
task :uptime do ここにタスクの内容 end |
task
ブロックの中にはrun_locally do; ... end
か、もしくはon 対象サーバ do; ... end
ブロックを書きます(以後、それぞれ省略してrun_locally
ブロック・on
ブロックと呼びます)。
前者のrun_locally
ブロック内にはローカルマシン上で実行するコマンドについて書きます。
また、後者のon
ブロック内にはサーバ上で実行するコマンドについて書きます。
1 2 3 4 5 6 7 8 |
task :uptime do run_locally do ここにローカルマシン上で実行するコマンド end on 対象サーバ do ここにサーバ上で実行するコマンド end end |
on
ブロックの「対象サーバ」の箇所には、前述の「ステージ名.rb」で設定したサーバの条件を指定することができます。
例えば、「Webサーバ(:web)のロールが与えられているサーバ」のみを作業対象とする場合は、以下のように書きます。
1 2 3 4 5 6 7 8 |
task :uptime do run_locally do ここにローカルマシン上で実行するコマンド end on roles(:web) do ここにサーバ上で実行するコマンド end end |
executeによるコマンド実行
最後に、実行するコマンドをrun_locally
やon
ブロック内に書きます。
コマンドの実行にはexecute コマンド
の文法を使います。
例えば、それぞれのマシン上でuptime
コマンドを実行するためには以下のように書きます。
1 2 3 4 5 6 7 8 |
task :uptime do run_locally do execute "uptime" end on roles(:web) do execute "uptime" end end |
captureによるコマンド実行結果の取得
executeでコマンドを実行した場合、そのコマンドの標準出力は捨てられてしまいます。標準出力の内容を参照する必要がある場合はcapture コマンド
の文法を使います。
1 2 3 4 5 6 7 8 |
task :uptime do run_locally do output = capture "uptime" end on roles(:web) do output = capture "uptime" end end |
単にexecuteをcaptureに置き換えました。captureは引数に渡したコマンドを実行してから、実行結果(標準出力)を文字通りキャプチャして返します。したがって、上記の例ではoutputに標準出力の内容が代入されています。
infoによるログ出力
ただ代入しただけでは意味がありませんので、Capistranoでログ出力してみましょう。ログ出力のためには、run_locally
やon
ブロックの中でログレベルに応じてdebug メッセージ
・info メッセージ
・warn メッセージ
・error メッセージ
・fatal メッセージ
の文法を使います。info
を使う場合は以下のようになります。
1 2 3 4 5 6 7 8 9 10 |
task :uptime do run_locally do output = capture "uptime" info output end on roles(:web) do output = capture "uptime" info output end end |
タスクの定義:まとめ
このように、run_locally
やon
ブロック内ではexecute
・capture
・info
をはじめ、いくつかの文法を使うことができます。利用できる文法はこのソースコードで定義されています。
ご紹介していない文法のうちよく使うのは、upload! ローカルファイルへのパス, アップロード先サーバ上のパス
です。その名の通りファイルアップロードの機能があります。使い方としては、on 対象サーバ do; ... end
ブロック内で実行します。すると、Capistranoを実行しているホストから対象サーバへファイルがSCPでアップロードされます。
execute
・capture
・info
・upload!
については後の例で使いますので、覚えてきましょう!
7. タスクの例
タスクはローカルマシンで行うもの・サーバ上で行うものを分けて管理するとよいでしょう。それぞれいくつか例を挙げます。
ローカルホストで行うことの例
deploy.rbやステージ名.rbに書くタスクには以下のようなものがあります。
- ソースコードの取得
- ビルドとアーカイビング・パッケージング
ソースコードの取得
アプリケーションをデプロイする前に、どこかのタイミングでアプリケーションのソースコードを取得する必要があります。
例えば、Gitを採用しているプロジェクトであれば、以下のような設定でソースコードを取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
set :application, 'finalge_sample_app' set :repo_url, 'git@github.com:mumoshu/finagle_sample_app.git' task :update do run_locally do application = fetch :application if test "[ -d #{application} ]" execute "cd #{application}; git pull" end else execute "git clone #{fetch :repo_url} #{application}" end end end |
この例では、:update
というタスクを実行することでソースコードを取得します。後にアプリケーションをローカルマシンでビルドするため、ソースコードはローカルマシンに置くことにします。そのために、前述のrun_locally
ブロック内でgitコマンドを実行します。
run_locally
ブロック内では、ソースコードの保存先ディレクトリの有無によって、git clone
とgit pull
を使い分けています。
ディレクトリの有無によって場合わけをするためには、Rubyのif文とCapistranoのtest ファイルテスト
を使います。
ソースコードをまだ取得していない場合は、git clone
でrepo_url
で設定したレポジトリからソースコードを取得します。保存先はapplication
に設定したディレクトリへしています。
ソースコードが既に存在する場合は、clone時に作成したディレクトリへ移動してから、git pull
で更新するようにしました。
ビルドとアーカイビング・パッケージング
ソースコードのコンパイルやその他アセットの前処理を行い、結果をtarballやzip・war・debianパッケージなどの再配布可能な形式にします。
例として、前述のFinagleを使ったサンプルアプリを先ほど取得したソースコードからビルドし、アーカイブする例は以下のようになります。
ここでは、アーカイブ作成のためにsbt
というビルドツールと、sbt-pack
というsbtプラグインを利用します。これら2つについては、本記事の範囲外なので説明は省略させていただきます。興味のある方は、「scala sbt」などで検索してみてください!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
task :archive => :update do run_locally do sbt_output = capture "cd #{fetch :application}; sbt pack-archive" sbt_output_without_escape_sequences = sbt_output.lines.map { |line| line.gsub(/\e\[\d{1,2}m/, '') }.join archive_relative_path = sbt_output_without_escape_sequences.match(/\[info\] Generating (?<archive_path>.+\.tar\.gz)\s*$/)[:archive_path] archive_name = archive_relative_path.match(/(?<archive_name>[^\/]+\.tar\.gz)$/)[:archive_name] archive_absolute_path = File.join(capture("cd #{fetch(:application)}; pwd").chomp, archive_relative_path) info archive_absolute_path info archive_name set :archive_absolute_path, archive_absolute_path set :archive_name, archive_name end end |
順を追って見て行きましょう。まず、task :archive => :update do
のようにタスク名archiveの次に=> :update
という記述が続いています。これはタスクの依存関係を表して、「archiveを実行する前にupdateを実行する必要がある」という意味です。実際にarchiveタスクを実行すると、Capistranoは(厳密にはCapistranoの基礎になっているRakeが)この依存関係を考慮して、updateを自動的に実行してからarchiveタスクを実行します。この機能によって、私たちはタスクの依存関係の管理をCapistranoに任せることができます。
次にrun_locally
ブロックです。ブロックの冒頭では、sbt
とsbt-pack
を使って先ほど取得したソースコードをビルドしています。sbtはビルドが終わった後、作成したアーカイブへの相対パスを標準出力へ以下のように出力します。
1 |
[info] Generating target/finagle_sample_app-0.1-SNAPSHOT.tar.gz |
このファイルは後でアップロードしたいので、標準出力をキャプチャして正規表現で相対パスを抜き出し、絶対パスへ変換しています。
また、キャプチャ結果のうち/\e\[\d{1,2}m/
という正規表現にマッチした部分文字列を消去してしまうコードが含まれています。これはエスケープシーケンスの消去をしています。sbt
は実行環境によってANSIエスケープシーケンスを利用してログに色をつけてくれます。今回は後のアップロードのためにアーカイブのパスを抜き出そうとしていますが、エスケープシーケンスもパスの一部と解釈されてしまいます。そこで、このようにエスケープシーケンスだけを消去しています。
コード例の最後では、set
を使って後続のタスクが必要とする設定を保持しています。この設定値は後でfetch
で取得するので、覚えておいてください!
リモートホスト上で行うこと
リモートホストに対して行うタスクには以下のようなものがあります。
- アプリケーションのアップロード・インストール
- アプリケーションの起動・停止
- Webサーバやロードバランサの操作
- 監視の開始、停止
今回は、最低限必要なアプリケーションのアップロード・インストールおよび起動・停止について説明します。
アプリケーションのアップロード・インストール
アプリケーションを起動するためには、まずアップロードする必要があります。
例として、前述のFinagleを使ったサンプルアプリをアップロードするには以下のような記述をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
task :deploy => :archive do archive_path = fetch :archive_absolute_path archive_name = fetch :archive_name release_path = File.join(fetch(:deploy_to), fetch(:application)) on roles(:web) do unless test "[ -d #{release_path} ]" execute "mkdir -p #{release_path}" end upload! archive_path, release_path execute "cd #{release_path}; tar -zxvf #{archive_name}" ここでアプリケーションの起動 end end |
アプリケーションの起動・停止
実行環境や採用するフレームワークによっては、Webサーバやアプリケーションサーバとは独立してサーバアプリケーションとして動くものもあります。
例えば、以下のようなものです。
- java・nodeコマンドなどでアプリケーションそのものを起動する場合
- Play framework・Finagle・Akkaなどのフレームワークなど組み込みのサーバがあるもの
例として、前述のアップロード・インストールのあと、アプリケーションを起動するには、以下のような記述をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
task :deploy => :archive do archive_path = fetch :archive_absolute_path archive_name = fetch :archive_name release_path = File.join(fetch(:deploy_to), fetch(:application)) on roles(:web) do unless test "[ -d #{release_path} ]" execute "mkdir -p #{release_path}" end upload! archive_path, release_path execute "cd #{release_path}; tar -zxvf #{archive_name}" project_dir = File.join(release_path, capture("cd #{release_path}; ls -d */").chomp) launch = capture("cd #{project_dir}; ls bin/*").chomp execute "cd #{project_dir}; ( ( nohup #{launch} &>/dev/null ) & echo $! > RUNNING_PID)" end end |
tarコマンドでアーカイブを展開するところまでは、前述のアップロード・インストールの例と同じです。
そのあとは、「アーカイブに含まれていたディレクトリ/bin/アプリケーション名-バージョン」という名前の起動スクリプトを簡単にファイルグロブでマッチングさせ、実行しています。
例では、project_dir
という変数に「アーカイブに含まれていたディレクトリへのパス」を代入しています。
また、launch
という変数に起動スクリプトへのパスを代入しています。最後にnohupコマンドを使ってその起動スクリプトを実行しています。
nohupを使っているのは、起動スクリプトをバックグラウンドで実行し続けるためです。
CapistranoはSSHで対象サーバへ接続してコマンドを実行しますが、その後に接続を切ります。その際に、起動スクリプトにSIGHUPシグナルが送信されて起動スクリプトが終了してしまいます。
それを防ぐためにnohupを使っています。
さて、これで完成・・・ではありません。
何かお忘れではないでしょうか?
実はここまでの例では、アプリケーションを初めてデプロイすることしか想定できていませんでした。既にデプロイしたアプリケーションが動作中の場合、それをまず停止する必要があります。アプリケーションを停止する一つの方法は、プロセスをkillしてしまうことです。プロセスはプロセスIDで区別できます。しかし、前回起動したアプリケーションのプロセスIDはいくつでしょうか?
こんなこともあろうかと、前述の例ではアプリケーション起動時にプロセスIDを保存しています。deployタスクのexecuteの最後で、echo $! > RUNNING_PID
というコマンドを実行しているのに気づきましたか?このコマンドで実行したコマンドのプロセスIDをRUNNING_PIDというファイルへ出力しています。
RUNNING_PIDファイルを利用して、2回目以降のデプロイ時には起動中のアプリケーションを停止しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
task :deploy => :archive do archive_path = fetch :archive_absolute_path archive_name = fetch :archive_name release_path = File.join(fetch(:deploy_to), fetch(:application)) on roles(:web) do begin old_project_dir = File.join(release_path, capture("cd #{release_path}; ls -d */").chomp) if test "[ -d #{old_project_dir} ]" running_pid = capture("cd #{old_project_dir}; cat RUNNING_PID") execute "kill #{running_pid}" end rescue => e info "No previous release directory exists" end unless test "[ -d #{release_path} ]" execute "mkdir -p #{release_path}" end upload! archive_path, release_path execute "cd #{release_path}; tar -zxvf #{archive_name}" project_dir = File.join(release_path, capture("cd #{release_path}; ls -d */").chomp) launch = capture("cd #{project_dir}; ls bin/*").chomp execute "cd #{project_dir}; ( ( nohup #{launch} &>/dev/null ) & echo $! > RUNNING_PID)" end end |
on_roles
ブロックの冒頭にbegin; ...; rescue; ... end
ブロックを追加しました。
このブロックでは、既にデプロイ済みのアプリケーションのディレクトリ(old_project_dir)がある場合のみ、
RUNNING_PIDファイルからプロセスIDを読み込んで、そのプロセスをkillしています。
これで、2度目以降もデプロイできるようになります。
8. サンプルレシピのまとめ
前述の「サンプルレシピの完成イメージ」をおさらいしてみましょう。
ここまで、以下の完成イメージに向けて設定やタスクの書き方をご説明してきました。
イメージはつかめてきましたか?
これを踏まえて、次の「サンプルレシピの完成形」を読んでみてください。
config/deploy/test.rb
1 |
作業対象サーバの設定 |
config/deploy.rb
1 2 3 4 5 6 7 |
Capistranoデフォルトタスクの消去 タスク「ソースコードの取得」の定義 タスク「ビルドとアーカイビング・パッケージング」の定義 タスク「アプリケーションのビルドとインストール」「アプリケーションの起動と停止」の定義 |
9. サンプルレシピの完成形
これまで説明したDSLを使ったサンプルを以下に示します。
Finagleというフレームワークを使ったアプリケーションの、考えられる限り最も簡単なデプロイ設定です。
Vagrantで作成した仮想マシンで実行し、その仮想マシン自身へログインしてFinagleアプリのソースを取得して、アプリケーションをビルド・インストール・起動します。
config/deploy/test.rb
1 |
server 'localhost', user: 'vagrant', roles: %w{web} |
config/deploy.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
task :update do run_locally do application = fetch :application if test "[ -d #{application} ]" execute "cd #{application}; git pull" else execute "git clone #{fetch :repo_url} #{application}" end end end task :archive => :update do run_locally do sbt_output = capture "cd #{fetch :application}; sbt pack-archive" sbt_output_without_escape_sequences = sbt_output.lines.map { |line| line.gsub(/\e\[\d{1,2}m/, '') }.join archive_relative_path = sbt_output_without_escape_sequences.match(/\[info\] Generating (?<archive_path>.+\.tar\.gz)\s*$/)[:archive_path] archive_name = archive_relative_path.match(/(?<archive_name>[^\/]+\.tar\.gz)$/)[:archive_name] archive_absolute_path = File.join(capture("cd #{fetch(:application)}; pwd").chomp, archive_relative_path) info archive_absolute_path info archive_name set :archive_absolute_path, archive_absolute_path set :archive_name, archive_name end end task :deploy => :archive do archive_path = fetch :archive_absolute_path archive_name = fetch :archive_name release_path = File.join(fetch(:deploy_to), fetch(:application)) on roles(:web) do begin old_project_dir = File.join(release_path, capture("cd #{release_path}; ls -d */").chomp) if test "[ -d #{old_project_dir} ]" running_pid = capture("cd #{old_project_dir}; cat RUNNING_PID") execute "kill #{running_pid}" end rescue => e info "No previous release directory exists" end unless test "[ -d #{release_path} ]" execute "mkdir -p #{release_path}" end upload! archive_path, release_path execute "cd #{release_path}; tar -zxvf #{archive_name}" project_dir = File.join(release_path, capture("cd #{release_path}; ls -d */").chomp) launch = capture("cd #{project_dir}; ls bin/*").chomp execute "cd #{project_dir}; ( ( nohup #{launch} &>/dev/null ) & echo $! > RUNNING_PID)" end end |
10. capコマンドを実行する
これまで作成してきたconfig/deploy/ステージ名.rb
(サーバを1つ定義しました。今回はステージ=test)とconfig/deploy.rb
(タスクをいくつか定義しました)を使って、Capistranoを実行してみます。
Capistranoを実行するためには、前述の「Capistranoのモデル」で触れたcap
コマンドを使います。
1 |
$ cap test deploy |
cap
コマンドの第1引数がconfig/deploy/test.rb
のtest
の部分に対応します。第2引数がタスク名で、task :deploy
のdeploy
の部分に対応します。
初回の実行結果は以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
$ cap test deploy [deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message. DEBUG [41eec63b] Running /usr/bin/env [ -d finagle_sample_app ] on DEBUG [41eec63b] Command: [ -d finagle_sample_app ] DEBUG [41eec63b] Finished in 0.006 seconds with exit status 256 (failed). INFO [752235f8] Running /usr/bin/env git clone git@github.com:mumoshu/finagle_sample_app finagle_sample_app on DEBUG [752235f8] Command: git clone git@github.com:mumoshu/finagle_sample_app finagle_sample_app DEBUG [752235f8] Cloning into 'finagle_sample_app'... INFO [752235f8] Finished in 2.827 seconds with exit status 0 (successful). DEBUG [262f8e15] Running /usr/bin/env cd finagle_sample_app; sbt pack-archive on DEBUG [262f8e15] Command: cd finagle_sample_app; sbt pack-archive (省略。長大なビルドログ) DEBUG [262f8e15] [info] Generating target/finagle_sample_app-0.1-SNAPSHOT.tar.gz DEBUG [262f8e15] [success] Total time: 4 s, completed Dec 20, 2013 3:47:11 PM DEBUG [262f8e15] Finished in 21.705 seconds with exit status 0 (successful). DEBUG [88b1fbff] Running /usr/bin/env cd finagle_sample_app; pwd on DEBUG [88b1fbff] Command: cd finagle_sample_app; pwd DEBUG [88b1fbff] /home/vagrant/cap-test/finagle_sample_app DEBUG [88b1fbff] Finished in 0.005 seconds with exit status 0 (successful). INFO /home/vagrant/cap-test/finagle_sample_app/target/finagle_sample_app-0.1-SNAPSHOT.tar.gz INFO finagle_sample_app-0.1-SNAPSHOT.tar.gz DEBUG [5ea6df07] Running /usr/bin/env [ -d /opt/testapps/finagle_sample_app ] on localhost DEBUG [5ea6df07] Command: [ -d /opt/testapps/finagle_sample_app ] DEBUG [5ea6df07] Finished in 0.513 seconds with exit status 0 (successful). DEBUG Uploading /home/vagrant/cap-test/finagle_sample_app/target/finagle_sample_app-0.1-SNAPSHOT.tar.gz 0.0% INFO Uploading /home/vagrant/cap-test/finagle_sample_app/target/finagle_sample_app-0.1-SNAPSHOT.tar.gz 0.12% (省略。長大なアップロードログ) INFO [a7940994] Running /usr/bin/env cd /opt/testapps/finagle_sample_app; tar -zxvf finagle_sample_app-0.1-SNAPSHOT.tar.gz on localhost DEBUG [a7940994] Command: cd /opt/testapps/finagle_sample_app; tar -zxvf finagle_sample_app-0.1-SNAPSHOT.tar.gz (省略。tarコマンドの実行結果=展開されたファイルの一覧) INFO [a7940994] Finished in 0.259 seconds with exit status 0 (successful). DEBUG [b9d38bed] Running /usr/bin/env cd /opt/testapps/finagle_sample_app; ls -d */ on localhost DEBUG [b9d38bed] Command: cd /opt/testapps/finagle_sample_app; ls -d */ DEBUG [b9d38bed] finagle_sample_app-0.1-SNAPSHOT/ DEBUG [b9d38bed] Finished in 0.009 seconds with exit status 0 (successful). DEBUG [2978d17e] Running /usr/bin/env cd /opt/testapps/finagle_sample_app/finagle_sample_app-0.1-SNAPSHOT/; ls bin/* on localhost DEBUG [2978d17e] Command: cd /opt/testapps/finagle_sample_app/finagle_sample_app-0.1-SNAPSHOT/; ls bin/* DEBUG [2978d17e] bin/hello DEBUG [2978d17e] Finished in 0.009 seconds with exit status 0 (successful). INFO [74987948] Running /usr/bin/env cd /opt/testapps/finagle_sample_app/finagle_sample_app-0.1-SNAPSHOT/; ( ( nohup bin/hello &>/dev/null ) & echo $! > RUNNING_PID) on localhost DEBUG [74987948] Command: cd /opt/testapps/finagle_sample_app/finagle_sample_app-0.1-SNAPSHOT/; ( ( nohup bin/hello &>/dev/null ) & echo $! > RUNNING_PID) INFO [74987948] Finished in 0.008 seconds with exit status 0 (successful). |
11. サーバの状態を確認する
実際にCapistranoでデプロイしたアプリケーションのプロセスを確認してみましょう。
1 2 3 |
$ ps aux | grep finagle_sample_app vagrant 6908 1.2 0.8 4313964 70760 ? Sl 15:47 0:05 /usr/lib/jvm/java-6-openjdk-amd64/bin/java -cp /opt/testapps/finagle_sample_app/finagle_sample_app-0.1-SNAPSHOT/lib/* -Dprog.home=/opt/testapps/finagle_sample_app/finagle_sample_app-0.1-SNAPSHOT -Dprog.version=0.1-SNAPSHOT myprog.Hello vagrant 6954 0.0 0.0 8112 948 pts/0 S+ 15:54 0:00 grep --color=auto finagle_sample_app |
アプリケーションが無事に6908番のプロセスで起動しました。
実際にサーバアプリケーションと通信できるか試してみます。
1 2 |
$ curl http://localhost:10000 Hello Finagle! |
無事、サーバアプリケーションからHTTPレスポンスが返ってきました。
前述のRUNNING_PIDファイルを使って再デプロイにも対応したので、2回目以降のデプロイも同様にcap test deploy
を実行するだけです。
12. おつかれさまでした!
今回はFinagleというフレームワークを使ったWebアプリケーションの最も簡単なデプロイを例にご説明しました。
お気づきかもしれませんが、やっていること自体は「タスクとrun_locally
・on
ブロックの中でシェルコマンドを実行する」です。任意のシェルコマンドが使えるので、応用が効きます。この例をカスタマイズしてお好きなコマンドに変更していただければ、皆さんが普段実行している手順をCapistranoのタスクにそのまま置き換えられると思います。また、今回は説明を省略しましたが、Capistranoの中では任意のRubyコードが書けます。シェルスクリプトだけで対応できないような複雑な要件のデプロイなどにも対応できるはずです。ぜひ、トライしてみてください!
まとめ
- デプロイなどの手作業にはコストがかかります
- 自動化にも学習コストがかかります
- 自動化のためにCapistrano 3が利用できます
- Capistrano 3の学習コストを減らすために、Capistrano 3の基礎を説明しました
- 例として、Finagleというフレームワークを使ったアプリケーションをデプロイしました
- この例を応用して、様々な手順を自動化できます。
One more thing
今回はCapistrano 3自体の説明に注力するため、Capistranoの便利機能やデプロイのノウハウは思い切ってカットしました。
今後は以下のようなトピックで、より具体的なレシピをご紹介していきたいと思っています。お楽しみに!
- ライブラリの使い方
- Play frameworkなどを利用した実際のアプリケーションのデプロイ方法
- 無停止デプロイ(Rolling Deploy)
明日はRubyistでありネットワークエンジニアでもある大山さんによるTremaとOpenFlowの記事です。お楽しみに!