ActionCable と react-rails でサンプル作ってみた
Tweetはじめに
Rails5 の新機能である ActionCable を試してみて、ついでに、react-rails を使って、WebUI付きの開発用DNSサーバを作ってみました。
作ったのは、オレオレDNSという開発用DNSサーバです。
最初にそのアプリの機能や目的から説明しますが、ActionCableに興味がある方は、後半の「実装について」のところまで飛ばして読んでください。
ActionCableとreact-railsを使うことで、従来の静的なページ中心のアプリのノウハウを活用しながら、SPA的な動的なアプリに段階的に移行していくことができます。それがどんな感じになるのか確かめるのにちょうどいいサイズのアプリだと思います。
オレオレDNS とは
オレオレDNS とは、複数の設定を内部に持ち、次のようなWebUIで切り替えられるDNSサーバです。
ローカルのDocker配下で起動して、開発時に、指定のURLを本来と別のサーバに向かわせることと、同じURLで複数のサーバに切り替えてアクセスすることを目的としています。
設定情報をDBに保存し、Rails の Webアプリでそれを管理します。その設定情報を元に、RubyDNS ベースのDNSサーバを起動します。
この RubyDNS ベースのDNSサーバの管理の機能を ActionCable を使って作りました。
本来のDNSサーバと違うウソの情報でクライアントを騙すので「オレオレDNS」という名前にしました。
解決しようとしている問題
DNSサーバの設定情報を切り替える必要があるのは、ステージングサーバ上でのテストのためです。
デジカでは、Blue Green Deployment をやっているので、stagingサーバが二台あります。リリースまでには、本番用のサーバを含めて、3台のサーバを切り替えて動作確認する必要があります。
ただ、多くの機能が、URLのホスト名、つまり、リクエストヘッダの Host:
に依存しているので、ホスト名を変えて接続先サーバを切り替える方法では充分なテストができません。Host: store.degica.com
のように、リクエストヘッダは本番用のままで、実際にアクセスするサーバのアドレスを切り替えることが、日常的に必要になっています。このためには、DNSレベルでブラウザをだます必要があります。
今までは、Dnsmasq を使って、テスト用のDNSを起動し、テストするマシンのDNSリゾルバにこのサーバのアドレスを設定して切り替えていました。しかし、テスト用のDNSサーバを複数台運用するのも大変だし、切り替えるのも面倒です。また、Dnsmasqには、CNAMEレコードが設定できないという制限があって、これをELBに対して使うには、常時変化しているIPアドレスに追随しなければいけないという問題もあります。
そこで、自分たちのニーズに合うような簡易DNSサーバを開発して、これをローカルで起動して対応することにしました。
機能
「オレオレDNS」は、ほとんどの問い合わせを Upstreamサーバに転送します。唯一、指定したテスト用のホスト名に対しては、設定した stagingサーバのアドレスを返答します。
設定情報はDBに保存して、この内容から、RubyDNSのスクリプトを動的に生成して、別プロセスでDNSサーバとして起動します。
現在の所は、設定できる内容は、我々のステージングテストに特化したものになっていますが、設定項目を増やし、Bindのような普通のDNSサーバに合わせて行けば、RubyDNSの機能の範囲内で、普通にDNSサーバとして機能をサポートするように改良できると思います。
今後、リクエストがあれば、そのような改良を検討したいと思います。
使い方
quay.ioでDocker Image を公開しているので、Dockerから起動するのが一番簡単です。Dockerをインストールして、下記のコマンドを実行してください。以下、Macを前提として説明しますが、他のOSでもほぼ同様に使用できると思います。
$ mkdir -p ore_ore_dns/db
$ docker run -ti -p 80:3000 -p 53:5300/udp -v /Users/[your id]/ore_ore_dns/db/:/db --rm quay.io/essa/ore_ore_dns
ore_ore_dns/db
のところは、適当な空のディレクトリを指定してください。実行すると、ここに、sqlite3のDBファイルができて、DNS設定情報が保存されます。
起動したら、http://[ip of docker container]/ にアクセスします。Macのdocker-machineの1台目のVMなら、http://192.168.99.100/ になります。
“New Fake Dns Server” をクリックして、下記の情報を入力してください。
- name: テスト環境の名前
- Target Server: stagingサーバ(テスト対象サーバのIPアドレス)
- Upstream: 上位DNSサーバ(プロバイダのサーバか8.8.8.8)
- Hooking hostnames: Target Server に向けるべきホスト名
- LogLevel: DEBUG/INFO/WARN/ERROR
”Create DNS Server” をクリックすると、サーバ定義が作成され、コンソール画面になるので、ここのStart Server
をクリックすると、DNSサーバが起動してブラウザ上のコンソールにログが表示されます。
あとは、ローカルマシンのDNSサーバの設定をDocker が走っているVMのアドレスにします。
Macであれば、下記のコマンドになります。
$ sudo networksetup -setdnsservers Wi-Fi 192.168.99.100
DNSのキャッシュをクリアします。
$ sudo killall -HUP mDNSResponder
これで、テスト用の環境になります。dig
で確認できます。
$ dig @192.168.99.100 your.hostname.com
....
;; ANSWER SECTION:
your.hostname.com. 0 IN CNAME your.staging-server.com.
実装について
このツールは、自分の必要のために作ったのですが、同時に、ActionCableがどんなものか試してみたいというのも目的でした。Rails5 beta1 上で作っているのですが、実際に試してみると、予想以上に簡単で、問題なく動いています。
また、react-railsとのコンビネーションもよくて、新しいパラダイムでのWebアプリ作成も見えてきました。
- 全体的なページ全体の構成は、従来の静的なページとほぼ同じ仕組み(ERBベース)で作る
- ページ内に、必要に応じて動的な「Component」を配置する
- 「Component」はActionCableを通してサーバ側のコードと直接接続することで、独立性の高い部品にすることができる
- 動的な部分もRuby主体でサーバ側のコードとして書くことができる
- 最終的な動作形態やパフォーマンスとしては、JavaScript主体で作成したSPAアプリケーションに近くなる
ActionCable で動的更新している場所
このアプリで ActionCable を使っているのは、次の二点です。
- DNSサーバの起動、終了に伴うボタンやステータスの更新
- DNSサーバからのログ配信とブラウザ上への表示
どちらも、トリガーとなるイベントは、クライアントのリクエスト(ユーザの操作)ではなくて、サーバ内の状態変化です。
サーバの起動だけは、ユーザのクリックがトリガーですが、複数のコンソールを開いている場合には、クリックしたページ(タブ)だけでなく、他の画面も更新する必要があるので、やはりサーバからのイベントに対応して、画面を更新する必要があります。
Componentの配置
たとえば、一覧画面は以下のようになります。(以下、引用したコードは、説明のために実際のコードを一部省略、変形しています)
app/views/fake_dns_servers/index.html.erb
<tbody>
<% @fake_dns_servers.each do |s| %>
<tr key="<%= s.id %>">
<td><%= s.name %></td>
<td>
<%= react_component('StatusMessage', {server_id: s.id, initialStatus: s.status}, {tag: 'span'} ) %>
</td>
<td>
<%= react_component('StartButton', {server_id: s.id, initialStatus: s.status}, { tag: 'span'} ) %>
</td>
....
</tr>
<% end %>
</tbody>
react_component
が react-rails の提供するヘルパーメソッドで、ここに Component が配置され、この部分は動的に更新されます。それ以外は、普通の ERB のテンプレートなので、従来の HTML/CSSのノウハウをほぼそのまま適用することができます。
なお、key
というアトリビュートは、同じ種類のComponentを一つのページ上に複数配置する場合に必要となるもので、何かユニークに識別できる値を設定します。普通は、レコードのIDで良いと思います。
Component の実装
上のページに配置されている StartButton
という Component は以下の機能を持ちます。
- クリックすると、サーバ上で指定された設定情報によってDNSサーバを起動する
- 対応するDNSサーバプロセスが起動されている時には、Disabledの状態になってクリックできなくなる
app/assets/javascripts/components/action_buttons.es6.jsx
class StartButton extends React.Component {
constructor(props) {
super(props);
this.state = { status: props.initialStatus || {}};
}
componentDidMount() {
this.statusListener = (data)=>this.setState({status: data.status});
App.dns.onUpdateStatus(this.statusListener);
}
componentWillUnmount() {
App.dns.offUpdateStatus(this.statusListener);
}
render () {
const Button = window.ReactPure.Button;
return (
<Button key={this.props.key} onClick={this.onClick.bind(this)} disabled={this.disabled()}>{this.props.text}</Button>
);
}
disabled() {
return this.props.server_id == this.state.status.server_id && this.state.status.running;
}
onClick() {
App.dns.start_server(this.props.server_id);
}
}
Buttonが大文字になっているのは、PureというCSSフレームワーク(のReact対応バージョン)を使っているためですが、ここでは、普通の <button>
タグと思ってかまいません。
クリックイベントの処理で、
App.dns.start_server(this.props.server_id);
という処理行っていますが、これによって、サーバ上の
class DnsChannel < ApplicationCable::Channel
...
def start_server(params)
...
end
end
というメソッドが起動されます。
両者を接続するコードは、標準のジェネレータで生成されるし、そんなに複雑なものではないので、感覚的には、ボタンのイベントハンドラをサーバ上のRubyのコードで書いているような感じです。接続部分 (Channel Object) については後述します。
そして、これによって、サーバが起動されるとこのボタンは Disabled の状態になるのですが、そのための通知は、逆にサーバから Component の状態を直接変更しているような感じになります。
app/services/ruby_dns_service.rb
ActionCable.server.broadcast 'dns_channel', status: { running: true }
サーバ側の上記のコードによって、クライアント側の、下記のイベントハンドラが起動され、status.running
という状態が変化します。
componentDidMount() {
this.statusListener = (data)=>this.setState({status: data.status});
App.dns.onUpdateStatus(this.statusListener);
}
setState
は react に Componentのレンダリングを要求するメソッドです。これによって、新しい設定された状態 (status.running == true )でボタンがレンダリングされて Disabled の状態になります。
Channel の実装
ActionCableでは、クライアントサーバ間に WebSocket のコネクションを一本だけ張って、それをアプリケーションで Channel という単位で多重化して使います。
クライアント側とサーバ側でペアとしてそれぞれ生成される Channel というオブジェクトが、Ruby と JavaScript を接続しているのですが、このコードは次のようになります。
app/assets/javascripts/channels/dns.es6.jsx
App.dns = App.cable.subscriptions.create("DnsChannel", {
emitter: new EventEmitter2(),
received (data) {
if (data.status) {
this.emitter.emit("cable.dns.status", data);
}
},
start_server(server_id) {
this.perform('start_server', { server_id: server_id });
},
onUpdateStatus(listner) { this.emitter.on('cable.dns.status', listner) },
offUpdateStatus(listner) { this.emitter.on('cable.dns.status', listner) },
});
クライアント側のページがロードされた時に、このオブジェクトがシングルトンとして生成され、Componentはここに対して、Subscribe を行うことで、イベントの通知を受け取ります。また、サーバのメソッド実行もここを通して行います。
また、TurboLinksが有効になっているので、この Channel オブジェクトは最初のページのロード時に一度だけ生成され、このアプリ内のページ遷移の間、ずっと使われます。
Cabled Components Pattern?
これによって、「Cabled Components Pattern」とでも呼ぶべき、今までできなかった方法で、SPA的なアプリを書くことができます。部品の配線がサーバまでつながっているイメージです。
- コンポーネントは、直接サーバ上のRubyコードを呼ぶことができる(ように記述できる)
- サーバ上のRubyコードは、直接クライアントのコンポーネントの state を設定できる(ように記述できる)
- コンポーネントは、自律的にサーバと通信するので、ページテンプレートの作成者は、最初の配置だけ行えば、あとは勝手に動く
- コンポーネントの動作は、Rubyによって、従来の Controller/View と切り離して記述できる
この方法が、実際の業務にスケールするかどうかはわかりませんが、ページの中の動的な部分が限定されていて独立性が高い場合には、非常に有効だと思います。
また、この「オレオレDNS」のように、サーバの状態やイベントを複数のクライアントが共有しているようなアプリでは、素直にかけると思います。
次のスクリーンショットでは、奥のSafariでコンソール画面を開いて、手前のChromeで一覧画面を開いてサーバの起動の操作を行っています。両方の画面が連動して動いていることがわかると思います(stagingが起動した状態で、passthroughを起動してますが、その時は先にstagingを終了させてからpassthroughを起動しています)。
Start Button 等がサーバ側の起動状態によって表示を変えていますが、この部分が「単にページに部品を貼り付けるとあとは勝手に部品が状態に応じて表示を変えてくれる」という感覚があって、気持ちよく素直に書けます。
また、両方のページはコンポーネントを共有しています。このような同じ機能を持つコンポーネントが、複数のページに配置されるケースにも対応しやすいと思います。
まとめ
- Rails5 beta1 で開発用DNSサーバを書いた
- このスケールでは、ActionCable は問題なく動いている
- react-rails と組み合わせると、Cabled Components Pattern とでも呼ぶべき新しいパラダイムでSAP的なアプリを書くことができる
- この組み合わせで、従来のRailsアプリのノウハウから連続的に動的なWebアプリに移行できる(可能性が高い)