実践Capistrano 3(1): タスク、ロール、変数(翻訳)
Capistranoは、Webアプリケーションのデプロイやメンテナンスを自動化するのに有用なツールです。私の場合、Ruby on Railsアプリケーションやその他のRackベースのアプリケーションのデプロイに用いています。依存関係のインストール、アセットのコンパイル、データベーススキーマのマイグレーションといった厄介な作業は、Capistranoがまとめて面倒を見てくれます。Capistranoを使わない場合、手動でサーバーにsshログインして必要なコマンドを手入力することになりますが、長時間に渡る忍耐力が求められますし、うっかりミスにつながりかねません。ありがたいことに、今やそうした作業は必須ではありません。
Capistranoの設計はモジュラーなので、プラグインやスクリプトを多数利用できます。これらを用いることで、要求の厳しいデプロイも可能になります。欲しい機能が見当たらなくても、Capistranoは本質的に柔軟なので、タスクを簡単に書き下ろせます。どのようなデプロイにしたいかをCapistranoに正確に指示すると、Capistoranoはサーバーを準備してアプリケーションを最新に保つよう支援します。
本シリーズにおける私の目標は、Capistranoというデプロイツールの中心となるコンセプトを解説するとともに、多くの高度な機能をご紹介することです。そのために、Rubyで書かれたWebアプリケーションをメンテナンスする場合に、現実的によくある問題のソリューションをさまざまな事例で見ていきます。これらの事例によって、作業を短時間で完了できるCapistranoスクリプトの書き方をもれなく学ぶことができ、Capistranoの内部に関する勘も養えるようになるはずです。
本記事のコードサンプルは、すべてCapistrano 3を前提としています。執筆時点では、Capistrano 3.11.2をインストールすることになります。Capistranoを自分のプロジェクトでセットアップする方法について詳しくは、公式ガイドをご覧ください。
前置きはこのぐらいにして、早速始めましょう!
Capistranoのタスク
Capistranoのコア部分では、Rakeビルドツールが提供するタスクを用いて、アプリケーションのアップデートやデプロイの操作を記述します。Capistranoはそれらのタスクを直感的な独自DSLにまとめ、ローカルコンピュータやリモートサーバー上のUnixシェルで実行しやすい形にします。
私は長い間、あっちこっちのサーバーに手動でsshログインしてはその場限りのアップデートに明け暮れていたこともありました。手動作業では、たとえばstagingサーバーにアクセスしたい場合に、デプロイユーザーだのサーバー名だのをいちいち把握した上でシェルに入力する必要がありました。これでは、つらいうえに操作ミスも起こりがちです。
「なるほど、ではもっといい方法はないものか?」
詳細情報を一切把握せずに、デプロイ環境であらゆるサーバーに同じ方法でログインできれば理想的です。つまり「stagingサーバーにログインしたい」と一言で済ませたいということです。Capistraoでは、以下のコマンドを実行することがこれに相当します。
$ cap staging login
このコマンドを「タスク」として実装する方法を見ていきましょう。
Capistranoは、デフォルトでlib/capistrano/tasks
ディレクトリのファイルを読み込みます。このディレクトリの下にlogin.rakeというファイルを作成します。*.rake
という拡張子は、Railsタスクを含むRubyファイルでよく使われます。Rakeはdesc
というメソッドを提供しますが、これはタスクにdescription(詳細)を追加できます。記述したdescriptionは、すべての有効なタスクを表示するcap --tasks
を実行すると表示されます。Rakeにはtask
というメソッドもあり、これはタスクの振る舞いを説明するのに用いられます。以下のようにdescriptionの直後にtask
ブロックを置きます。この新しい:login
タスクは取りあえず空にしておきます。
# lib/capistrano/tasks/login.rake
desc "Login into a server based on the deployment stage"
task :login do
...
end
このままではlogin
タスクで何も行われません。ログインするには、アプリケーション・サーバーのログイン情報を集める必要がありますが、ここで「ロール(role)」という概念についてお話ししておく必要があります。
Capistranoのロール
Capistranoでは、どのタスクをどのサーバーで実行すべきかをきめ細かに制御するためにロールという概念を用います。たとえば、データベースサーバーにのみ適用したいが、Webサーバーにデプロイするときはスキップしたいタスクがあるとします。ロールは、ちょうどフィルタのように動作するので、特定のロールにマッチするサーバーでタスクを実行する前に、同じロールでひとまとめにグループ化するようCapistranoに指示できます。
Capistranoタスクでは、以下の3つのロールがよく使われます。
- appロール
- アプリケーションサーバー(コンテンツを動的に生成するサーバー)で実行するタスクに用いる。これは、Railsではpumaサーバーに相当する。
Capistranoの組み込みタスクのうち、deploy:check
、deploy:publishing
、deploy:finished
はいずれもappロールで実行される。 - dbロール
- データベースサーバーで実行するタスクに用いる。
例: capistrano-railsプラグインが提供するdeploy:migrate
(Railsデータベーススキーマのマイグレーションに用いられる)。 - webロール
- 静的コンテンツを提供するWebサーバーを扱うタスクに用いる。
Nginxの場合、コミュニティの作ったcapistrano3-nginxプラグインではnginx:start
、nginx:reload
、nginx:site:disable
といったタスクはすべてwebロールを用いる。
もちろん独自のロールも定義できます。例: Redisデータベースインスタンスに関連するタスクのみを実行するのに用いるredisロール。
role :redis, "redis-server"
サーバーに指定されているロールの種類にかかわらず、タスクをあらゆるサーバーにマッチさせたい場合はallロールを用います。Capistranoの組み込みタスクでは、柔軟性を保つためにallロールが用いられています。
ここでは「アプリケーション」「Webサーバー」「データベースサーバー」をホストする1台のコンピュータにデプロイすることにし、サーバーにappロールとdbロールとwebロールをすべて適用するために、server
メソッドによるショートハンド定義を用います。
# config/deploy/staging.rb
server "staging-server.example.com", roles: %w[app db web], primary: true
上の定義をよく見ると、primary:
というプロパティがあることに気が付くでしょう。このプロパティは、タスク実行時の優先順位をCapistranoに通知します。primary: true
を指定したサーバーに関連付けられているロールを持つタスクは、最初に実行されます。これは、アプリケーションを多数のホストに分割している場合に特に便利です。そのような場合は、以下のようにロール中心の定義に組み換えられます。
# config/deploy/staging.rb
role :app, "app-server.example.com"
role :db, "db-server.example.com", primary: true
role :web, "static-server.exmaple.com"
ここで定義したロールをタスク定義に適用する方法については後述します。
ホストの設定ファイルを取得する
サーバー設定ファイルにアクセスするには、roles
ヘルパーメソッドを用います。roles
メソッドはロール定義を1つ以上受け取り、マッチするすべてのホストのインスタンスをリストにして返します。ここでは:app
ロールを指定すると、Capistrano::Configuration::Server
という1台のサーバー設定インスタンスだけがリストに含まれます。
Capistrano 3ではさまざまなイノベーションが行われていますが、よりモジュール性の高いアーキテクチャの導入もそのひとつです。Capistrano 3では、sshセッション管理もRubyのsshkit gemという別の依存関係に移行しました。SSHKitが提供するDSLを用いるとon
というメソッドが導入され、ブロックスコープで記述したコマンドをさまざまなサーバーで実行できます。on
メソッドはホスト設定オブジェクトの配列を受け取ると、SSHKit::Coordinator
を用いてコマンドをホストごとにパラレル実行します。
# lib/sshkit/dsl.rb
module SSHKit
module DSL
def on(hosts, options={}, &block)
Coordinator.new(hosts).each(options, &block)
end
end
...
end
設定済みホストが複数ある場合は、おそらく次のようにin: :sequence
を用いて各サーバーに順番にログインするようCapistranoに指示する方がよいでしょう。
on roles(:app), in: :sequence do |server|
...
end
on
のスコープ内では、サーバーに関する情報を含むホストインスタンスにアクセスできるようになります。本質的に「このサーバーで以下の作業を行え」と指示するのと同じです。
# lib/capistrano/tasks/login.rake
desc "デプロイのステージに応じてサーバーにログインする"
task :login do
on roles(:app) do |server|
...
end
end
Capistranoの変数
サーバーにssh接続する場合、「ユーザー名」「サーバー名」「ログイン先のパス」を知る必要があります。Capistranoではset
メソッドが提供されています。グローバルなタスクや特定のタスクを設定する変数をこれで設定すると、スクリプトの他の部分でも利用できるようになります。たとえば、以下のように:user
変数や:deploy_to
変数を設定できます。
# config/deploy/staging.rb
set :user, "deploy-user"
set :deploy_to, "/path/to/deploy/directory"
Capistranoが提供するfetch
メソッドを使えば、設定変数を楽に読み出せます。たとえば、以下のようにユーザー名やデプロイパスを取得できます。
# lib/capistrano/tasks/login.rake
desc "デプロイのステージに応じてサーバーにログインする"
task :login do
on roles(:app) do |server|
user = fetch(:user)
path = fetch(:deploy_to)
...
end
end
これでユーザー名とログイン先パスを取得できたので、ssh
コマンドユーティリティに引数を渡してURIを組み立てられるようになります。URI文字列は「ユーザー名」「サーバー名」「ポート番号」を結合してビルドします。以下のコードでは、ユーザー名やポート番号が指定されていない場合を扱います。
# lib/capistrano/tasks/login.rake
desc "デプロイのステージに応じてサーバーにログインする"
task :login do
on roles(:app) do |server|
user = fetch(:user)
path = fetch(:deploy_to)
uri = [
user,
user && '@',
server.hostname,
server.port && ":",
server.port
].compact.join
end
end
sshコマンド
これで、ssh
コマンドにURIとデプロイパスを渡して実行できるようになりました。先に進む前にssh周りについて軽く補足しておきます。
特に-t
フラグはsshユーティリティに「teletypeモード」で実行するよう指示します。teletypeモードとは何でしょう?Capistranoは、デフォルトではssh接続でのデプロイ時にビジュアルターミナルをアタッチしません。誰も見ようがないので、洗練されたインターフェイスを表示する必要はないということになります。しかし本記事では、ユーザーがシェル機能をすべて利用できるようにしておきたいと思います。
-t
フラグに続けてサーバーのURIを指定し、続いて、ログイン後に1回実行するシェルコマンドを引用符で囲みます。ここでは2つのシェルコマンド文字列をつなげて使います。1つ目はWebサイトのルートディレクトリに移動するコマンド、2つ目は$SHELL
変数の実行です。この$SHELL
は、デフォルトシェルの位置を表す環境変数で、通常はBash
になっています。シェルをわざわざこの形で起動する必要がある理由は何でしょうか?sshは、シェルを実行しないと接続を終了してログアウトしてしまいます。このタスクでやりたいのはログインを継続することなのです!
bash
を指定してシェルを実行する場合、-l
フラグを指定すると、ユーザーがログインしているかのようにシェルを呼び出します(対話モード)。ここでは、.bash_profileなどの隠しファイルにある設定をすべて事前に読み込んでおくために使っています。
最終的なコマンド文字列は以下のようになります。
"ssh -t #{uri} 'cd #{path}/current && exec $SHELL -l'"
Rubyのexec
を用いて、ssh
コマンドをローカルで実行します。
We use Ruby's exec
to run our ssh
command locally:
exec("ssh -t #{uri} 'cd #{path}/current && exec $SHELL -l'")
以上をまとめると、以下のようなタスクのできあがりです。
desc "Login into a server based on the deployment stage"
task :login do
on roles(:app) do |server|
user = fetch(:user)
path = fetch(:deploy_to)
uri = [
user,
user && '@',
server.hostname,
server.port && ":",
server.port
].compact.join
exec("ssh -t #{uri} 'cd #{path}/current && exec $SHELL -l'")
end
end
ここまでは、自分のコンピューターからサーバーに手早くログインする方法について説明しました。しかしCapistranoで日々の作業を強化する方法は、これだけではありません。
リモートのRailsコンソール
私の場合、サーバーのRailsコンソールを取りあえず開いて、今動いているデータベースにクエリをかけなければならなくなることがよくあります。CapistranoのログインタスクでログインしてからRailsコンソールを開くコマンドを手打ちしてもいいのですが、いっそproductionのRailsコンソールを一発で開くCapistranoスクリプトを使ったらどうでしょう?以下のようなコマンドを実行してやれるようにするというイメージです。
$ cap production console
上のコマンドを実装するには、以下の「ベアボーン」:console
タスクとdescriptionを、同じlogin.rakeファイルの中に書きます。
# lib/capistrano/tasks/login.rake
desc "デプロイのステージに応じてRailsコンソールを開く"
task :console do
...
end
ログインタスクのときと同様、「ユーザー名」「デプロイのパス」を知る必要があります。Railsコンソールを開く場合は、現在Railsを実行している環境の名前も必要になります。これはcapistrano-rails gemで設定される:rails_env
設定変数から取れます。
# lib/capistrano/tasks/login.rake
desc "デプロイのステージに応じてRailsコンソールを開く"
task :console do
on roles(:app) do |server|
env = fetch(:rails_env)
user = fetch(:user)
path = fetch(:deploy_to)
...
end
end
Railsの実行可能ファイルを探索する
sshでRailsコンソールを開く前に、もうひとつ対処が必要になります。そのままではsshの仮想ターミナルにアクセスできないので、ユーザープロファイルの設定ファイルを読み込めません。そこで、どうにかしてCapistranoがrails
実行可能ファイルを探索し、そしてインストールされているRubyを探索できるようにする必要があります。私はRubyのインストールをrbenvユーティリティで管理し、gemのインストールをBundlerで管理するのが好みです。他の方法でRubyを管理している方は、適宜パスを調整してください。
rbenvのパスについては、デプロイするユーザーの$HOME/.rbenv
で指定されるローカルのインストール場所を用いることにします。rbenvの「shims」という概念は、実際にインストールされているgem
やirb
やrails
などのRubyコマンドをrbenv exec
コマンドに対応付けるのに使われます。rails
コマンドを実行する場合、shimをbundle
して、必要な依存関係をすべて読み込むようにする必要があります。利用できるbundle
実行ファイルは$HOME/.rbenv/shims
の中にあります。
最後の手順として、-e
フラグでrails console
コマンドに現在の環境の情報を伝えます。Railsコンソールを開く完全なコマンドは以下のようになります。
console_cmd = "$HOME/.rbenv/shims/bundle exec rails console -e #{env}"
パズルの最後のピースがはまったので、以下でRailsアプリケーションにssh接続してRailsコンソールを開けるようになります。
"ssh -t #{uri} 'cd #{path}/current && #{console_cmd}'"
最後は、いよいよひとつのスクリプトにまとめてみましょう。
desc "デプロイのステージに応じてRailsコンソールを開く"
task :console do
on roles(:app) do |server|
env = fetch(:rails_env)
user = fetch(:user)
path = fetch(:deploy_to)
uri = [
user,
user && '@',
server.hostname,
server.port && ":",
server.port
].compact.join
console_cmd = "$HOME/.rbenv/shims/bundle exec rails console -e #{env}"
exec("ssh -t #{uri} 'cd #{path}/current && #{console_cmd}'")
end
end
重複の除去
本記事を鵜の目鷹の目で読んでいる方であれば、タスクとタスクの間に少々冗長な繰り返しがあることにお気づきかと思います。重複を退治してタスクを強化し、メンテナンス性を高ることにしましょう。私は、コードの繰り返し部分をいったん「完全な繰り返し」に揃えてから重複を取り除くのが好みです。こうすることで、コード内のまったく同じ部分がきれいに浮かび上がってくるからです。今回の場合、Railsの環境変数や実行するsshコマンドの部分を別にすれば、タスクは完全に同一です。
重複削除の精神に則って、セットアップ部分やsshコマンド実行部分をrun_ssh_with
という独自のメソッドに移動することにします。このメソッドは、サーバー設定と実行すべきコマンドを引数に取ります。
# lib/capistrano/tasks/login.rake
def run_ssh_with(server, cmd)
user = fetch(:user)
path = fetch(:deploy_to)
uri = [
user,
user && "@",
server.hostname,
server.port && ":",
server.port
].compact.join
exec("ssh -t #{uri} 'cd #{path}/current && #{cmd}'")
end
run_ssh_with
にまとめたおかげで、どちらのタスクもシンプルになりました。
# lib/capistrano/tasks/login.rake
desc "デプロイのステージに応じてサーバーにログインする"
task :login do
on roles(:app) do |server|
run_ssh_with(server, "exec $SHELL -l")
end
end
desc "デプロイのステージに応じてRailsコンソールを開く"
task :console do
on roles(:app) do |server|
env = fetch(:rails_env)
console_cmd = "$HOME/.rbenv/shims/bundle exec rails console -e #{env}"
run_ssh_with(server, console_cmd)
end
end
随分見違えましたね!しめくくりに、あらゆるキー入力を短く済ませたがるプログラマーの本性をなだめることにしましょう。2つのタスクにはエイリアスを作成できるのです!rakeそのものにエイリアス機能はありませんが、それらしくすることは可能です。その方法とは、実行内容が最初に別のタスクに依存する新しいタスクを定義することです。2つのタスクを1文字に短縮してみましょう。
# lib/capistrano/tasks/login.rake
task :c => :console
task :l => :login
まとめ
怒涛のようなCapistranoツアーが終わりました。Capistranoスクリプトを書いたことのない人は本記事で多くのことを学べます。願わくば、Capistranoスクリプトを書いたことがある方にとっても機能のいくつかが明確になることでしょう。本記事では、Capistranoタスクのしくみ、変数の設定方法、コマンドをローカルで実行する方法について一般的なことを学べます。
ログインタスクとコンソールタスクを使えば、繰り返しに満ちた作業を手軽に自動化できます。タスクにすることで「Railsプロジェクト間の作業を統一できる」という重要なオマケもあります。リモートサーバーにクエリをかけるのにプロジェクトファイルを急いで開いて接続情報を知る必要がなくなります。ひとつひとつのタスクによる効率化はささやかですが、時間をかけてタスクを蓄積していけばスムーズな開発フローを得られます。同じ用に便利なCapistranoタスクをご存知の方がいらっしゃいましたら、ぜひ共有してください!
私はPiotr Murachと申します。これまで蓄積したプログラミング経験をドキュメント化し、日々の作業を改善できる実用的なコード例とともに皆さんにご紹介しています。私のブログ記事やオープンソースプロジェクトをお楽しみいただけましたら、GitHub Sponsorsで応援お願いします。ニュースレターやRSSフィードもぜひどうぞ。
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。