Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

実践Capistrano 3(1): タスク、ロール、変数(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

実践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:checkdeploy:publishingdeploy:finishedはいずれもappロールで実行される。
dbロール
データベースサーバーで実行するタスクに用いる。
例: capistrano-railsプラグインが提供するdeploy:migrate(Railsデータベーススキーマのマイグレーションに用いられる)。
webロール
静的コンテンツを提供するWebサーバーを扱うタスクに用いる。
Nginxの場合、コミュニティの作ったcapistrano3-nginxプラグインではnginx:startnginx:reloadnginx: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」という概念は、実際にインストールされているgemirbrailsなどの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フィードもぜひどうぞ。

関連記事

productionやdevelopment、stagingという言葉の使い分けについて


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。