プログラム

ライブコーディングアプリ「Casto」ができるまで ep.2

もう既に@まとめエントリを書いていてくれるのですが、別の視点でまとめてみようかなと思いまして書いてみます。ということで、ep.2。

"Casto"(キャスト)は一言でまとめると「リアルタイムで更新できるGist」といった形のWebアプリケーションです。

  • ブラウザへ配信したいファイルをドラッグ&ドロップでライブコーディング開始
  • 手元のファイルを編集するだけで編集内容を同じURLを見ている全ユーザに配信

を実現しています。作り始めた経緯は@のエントリを見ていただくことにして、Casto自体はnode.jsのRendrと呼ばれるライブラリ*1とSTORYBOARDSに用意したAPIで構成されています。

サーバー, APIの構成を含め、Castoは下の様な構成になっています。

Rendr

概要と印象

今回はRendrを使ったわけですが、特徴としては、

  • フロント側、サーバ側でbackbone.jsのモデル、ビューが共用される
  • 最初のアクセス時のサーバサイドでのHTML生成がbackbone.jsのmodel, viewを使って行われる
  • ブラウザ側にサーバサイド側で作られたHTMLが描画されて以降は一般的なbackbone.jsアプリと同様の動き方をする
  • DataAdapterという仕組みがあって、modelを介してRendrサーバを経由して任意のAPIへリクエストをフォワードできる
  • ドメインが異なるアプリケーションであってもRendrが一枚入ることでアプリケーションに併せて柔軟に外部APIが制御できる

といったところが挙げられます。まだ、今回利用した範囲もRendrの一部の機能ですが、以下の様な印象を持ちました。

  • サーバーサイドのテンプレートエンジンとフロントサイドのテンプレートエンジンが共用できるのはメリット大きそう
  • 当たり前ですが、node.jsなのでJavaScriptで完結する。加えてbackbone.jsのmodel, viewがサーバサイドでも利用できるのでフロントサイド, サーバサイドの作業を統一できる
  • SPAアプリでよくある「サーバーサイドの言語でDOMの枠を作る」→「backbone.jsのViewでDOMの中を埋める」が無い

ただ、ドキュメントがWeb上に少なかったり、サンプルも少ないので最初動かすまでは少し苦労しましたし、今もブラッシュアップに四苦八苦はしています...。

サーバーの準備

構成管理と構築

REMPSTORYBOARDSを現在はVPSに集約していて、VPSの上にLXCを構築して各アプリケーションを稼働させています。それぞれのLXCはChefを利用して構成管理されています。
既にSTORYBOARDSではアプリケーション自体はRuby(Padrino)で構築されていますが、一部node.jsを導入していたため、その際に準備したcookbookを利用してさっくりとコンテナを作りました。
コンテナさえ稼働させてしまえば、フロント側に用意したnginxでプロキシさせて、LXC上で稼働させたRendrサーバにつなげることで今回稼働させるサーバの準備をそれほど手間なく準備させることができました。

デプロイ

コンテナが準備できれば、あとはRendrアプリをデプロイとなって、こちらの手段もnpmのパッケージで閉じる形にしたかったのですが、リリース世代を管理したりデプロイ手順の整備にREMPやSTORYBOARDSで利用して慣れているRubyのデプロイツールであるminaを利用しました。このあたりの話は以前のブログのエントリを参照ください。CastoでRubyを利用したのは唯一この部分のみとなります。

他のnode.jsアプリのデプロイ方法もいくつか参考にしたのですが、Capistranoを利用しているケースを散見したので、方法としては妥当だったかなと思っています。

Rendrアプリの永続化

Rendrサーバ(node.jsアプリ)を単純に立ちあげたのみだとそのプロセスが死んでしまった際にそのままとなってしまうため、foreverを利用して永続化させる様にしました。

加えて、grunt-foreverというgruntプラグインを利用すると、

grunt.loadNpmTasks('grunt-forever');
grunt.initConfig(
forever: {
app: {
options: {
index: 'index.js',
logDir: 'log'
}
}
}
});

といった設定を足すだけで、

grunt.registerTask('startProduction', ['forever:app:start']);

といったタスクを定義することができる様になって、foreverを利用したサーバ起動・停止が簡単に行える様になります。

いま持っている課題

エラーページでのテンプレートの利用

404ページを用意して、404ステータスエラーが生じた時はこの様なページを表示させようとしたのですが、エラーハンドリングを行った際に任意のビューを利用したページ表示をさせたく、index.jsで、

var server = rendr.createServer({
dataAdapterConfig: config.api,
errorHandler: function (err, req, res, next){
if (err.status == 404) {
// [TODO] ここで任意のビューを利用して404ページを描画させたい!!
}

とすればよさそうなのですが、今もRendr自体のソースを読みながら手探り中です。


と、あれこれ書きましたが、チームでRendrという新しいライブラリを利用して手早くアプリケーションが構築できて、且つメリット・デメリットも多く発見できたこと。個人的には今回初のnode.jsアプリだったのですが、そのサーバサイド側の準備からRendrを利用した際のAPI接続がまずは一通り把握できたことはとても良かったと思います。

更にブラッシュアップはチームで継続していくつもりですので、是非使ってみて感想あれば是非、twitterアカウントIssues · hikarock/casto · GitHubでお知らせください。

*1:あえて作者がフレームワークと呼んでいないのでこの呼び方で。

Rendrで作られたnode.jsアプリをminaでデプロイする

Rendrで新しくアプリを作ろうとしていて、rendr-cli
プロジェクトの雛形を作ったので、では実際にデプロイどうしようと考えた時にminaを使ってやってみたので
その際のメモ。

手順としては、

  • rendrアプリをデーモン化して稼働させるためにforeverで動く様にする
  • minaでデプロイするタスクを書く

minaのデプロイはここあたりに書いているので詳細は省略。

foreverでデーモン化する

rendr-cliで作られたアプリのベースには既にgruntを使って開発環境用のサーバを
稼働させたり、stylusをコンパイルするためのgruntタスクが既に用意されていて、開発環境であれば

$ grunt server

とすれば、一通りのコンパイルと手元でサーバ起動、そしてstyles等の更新を監視して再コンパイルをしてくれるのですが、
これをこのままproduction環境で動かす訳にはいかないので、foreverでデーモン起動、停止をする
タスクを書き足します。

まず、foreverが使える様にするためにアプリケーションプロジェクトのルートで、

$ npm install forever --save

としてforeverをインストール。 --save を付けて package.json も更新します。

次にrendr-cliが作った Gruntfile.js にforeverを使ったアプリケーション起動、停止をするタスクを足します。

// Gruntfile.js
grunt.registerTask('startProductionNode', function () {
grunt.util.spawn({
cmd: 'node',
args: ['./node_modules/forever/bin/forever', 'start', 'index.js'],
opts: {
stdio: 'inherit'
}
}, function () {
grunt.fail.fatal(new Error("forever quit"));
});
});
grunt.registerTask('stopProductionNode', function () {
grunt.util.spawn({
cmd: 'node',
args: ['./node_modules/forever/bin/forever', 'stop', 'index.js'],
opts: {
stdio: 'inherit'
}
}, function () {
grunt.fail.fatal(new Error("forever quit"));
});
});

ここまで設定すると、アプリケーションをデーモン化して起動、停止ができます。

起動は、*1

$ grunt startProductionNode
Running "startProductionNode" task
warn:    --minUptime not set. Defaulting to: 1000ms
warn:    --spinSleepTime not set. Your script will exit if it does not stay up for at least 1000ms
info:    Forever processing file: index.js
Done, without errors.

一方停止は、

$ grunt stopProductionNode
Running "stopProductionNode" task
info:    Forever stopped process:
data:        uid  command                                            script   forever pid   logfile                             uptime
[0] sKj9 /Users/hideack/.nodebrew/node/v0.11.11/bin/node index.js 34008   34012 /Users/hideack/.forever/sKj9.log 0:0:1:15.836
Done, without errors.

これで、Rendrアプリをデーモン化して動かせる様になりました。

minaでのデプロイ

続けて実際にデプロイする際には、リポジトリから最新のコードを取ってきてstylus等をコンパイルして上で作ったgruntタスクを利用してアプリケーションを再起動させます。

gitリポジトリから引っ張ってくるところまではRubyのアプリケーションの場合と同様ですが、アプリケーションサーバの起動のタスクは独自で書く必要があるので以下の様なタスクを書き足します。

# rendr
# =============
namespace :rendr do
task :compile do
queue 'echo "-----> Start compile tasks."'
queue! %{
      cd #{deploy_to}/current
      grunt compile
    }
end
task :start do
queue 'echo "-----> Start server."'
queue! %{
      cd #{deploy_to}/current
      grunt startProductionNode
    }
end
task :stop do
queue 'echo "-----> Stop server."'
queue! %{
      cd #{deploy_to}/current
      grunt stopProductionNode
    }
end
end
desc "Deploys the current version to the server."
task :deploy => :environment do
deploy do
# Put things that will set up an empty directory into a fully set-up
# instance of your project.
invoke :'git:clone'
invoke :'deploy:link_shared_paths'
to :launch do
invoke :'rendr:compile'
invoke :'rendr:stop'
invoke :'rendr:start'
queue "touch #{deploy_to}/tmp/restart.txt"
end
end
end

ここまで準備できれば手元から、

$ bundle exec mina deploy

でRendrアプリのデプロイ&アプリの再起動*2が行えます。

*1:warning出ているのはアプリのプロセスの監視条件を設定していないためなので設定するべきですね...

*2:ただこのままではgracefulな再起動になってないので今後直していく予定

MacOSにパッケージから入れたnode.jsをnodebrewに移行した

node.jsは公式サイトでMac用のインストーラパッケージが配布されていて非常にインストールが楽なのだけど、これをやめてnode.jsのバージョンマネージャ*1であるnodebrewを導入したのでその際のメモ。

パッケージでインストールされたnode.jsを削除する

各所で紹介されている以下の様なコマンドで削除。

lsbom -f -l -s -pf /var/db/receipts/org.nodejs.pkg.bom \
| while read i; do
sudo rm /usr/local/${i}
done
sudo rm -rf /usr/local/lib/node \
/usr/local/lib/node_modules \
/var/db/receipts/org.nodejs.*

nodebrewのインストール

これはgithubリポジトリのreadmeに従って完了するのですが一応メモとして残しておきます。

まずはインストール

$ curl -L git.io/nodebrew | perl - setup

完了したら環境変数(~/.zshrcや~/.bashrc)にパスを通す

export PATH=$HOME/.nodebrew/current/bin:$PATH

そして一度環境変数を更新

$ source ~/.zshrc

そしてここから実際にインストール。インストールできるnode.jsのバージョンを得るには

$ nodebrew ls-remote
v0.0.1    v0.0.2    v0.0.3    v0.0.4    v0.0.5    v0.0.6
(略)
v0.11.0   v0.11.1   v0.11.2   v0.11.3   v0.11.4   v0.11.5   v0.11.6   v0.11.7
v0.11.8   v0.11.9   v0.11.10  v0.11.11

こんな感じで一覧が把握できる。そして実際にインストールするには、

$ nodebrew install-binary v0.11.11
$ nodebrew use v0.11.11
use v0.11.11
$ node -v
v0.11.11

といった具合でインストールできます。*2

参照

Mac OS X から Node.js をアンインストールする方法 - SONICJAM Developerz Blog

*1:Rubyでのrbenv的な位置づけ

*2:install-binaryでバイナリからインストールするため数秒でインストール完了します。オプションをinstallとした場合はソースからコンパイルするので時間を要します。

JavaScriptのハッシュをマージする

  var _hashMerge = function(target) {
var sources = [].slice.call(arguments, 1);
sources.forEach(function (source) {
for (var prop in source) {
target[prop] = source[prop];
}
});
return target;
};

を定義しておけば、

var origin1 = {a: "foo"};
var origin2 = {b: "hoge"};
var merged = _hashMerge({}, origin1, origin2);
// => Object {a: "foo", b: "hoge"}

といった具合でマージできる

JavaScriptのハッシュのキーへ任意の変数名を指定する場合

JavaScriptのハッシュのキーへ任意の変数名を設定したいつもりで、以下の様に書くと意図した挙動にならない。

例えば下の場合、

var key = "foo";
var data = {key: "hoge"};
// data
// Object {key: "hoge"}

dataというハッシュに foo というキーを設定したかったができていない。

そこで、以下の様に関数を定義してあげると所望する処理を実現できる。

var hash = function(key, value) {
var h = {};
h[key] = value;
return h;
};
var key = "foo";
var data = hash(key, "hoge");
// data
// Object {foo: "hoge"}

dataのキーにfooを設定できている。

Mocha と should.js で例外が生じるケースのテストを書く

Mochashould.jsでテストを書いていて例外が起こるテストを書いていて、少し詰まったのでメモ。

例えばhogeというメソッドに空文字列を渡すと例外が生じるというケースのテストを書きたいときに、

foo.hoge('').should.throw();

という書き方をすると、これは誤りで

(function(){ foo.hoge('') }).should.throw();

と書くのが正解。*1

実際に書いてみると下の様な形になる。(確認用なのでテストの中にクラス入れているが、ここは本来requireして呼び出す形。)

これでmochaを実行すると、

$ node_modules/mocha/bin/mocha --reporter spec
Foo
#sample1
✓ equal test
#sample2
✓ exception test
2 passing (8ms)

といった形で例外を起こすテストができている。

*1:https://github.com/visionmedia/should.js/#throw-and-throwerror に書いてあるのだが....

node.jsのプロジェクトのパッケージ管理用のファイル

2度調べたので記録。

node.jsではパッケージ管理にnpmがあって、更に package.json というjsonファイルがあるとそこに定義してあるパッケージ依存関係に基いてパッケージのインストールがされる*1 のですが、これそもそもどうやって作るの?と思って調べたのでメモ。

$ npm init

これでOKだった。 これで package.json が作成される。

で続けて、プロジェクトに必要なパッケージをインストールする場合は、

$ npm install (パッケージの名前) --save-dev

とすれば、自動的に package.json に必要なパッケージが追記された上でプロジェクトディレクトリに node_modules というディレクトリが作成されてインストールが行われます。

*1:RubyのGemfileの様に

Minaでデプロイした際にIRCへ通知する

minaでデプロイした際にIRCへ通知を行う様にしたのでその際の対応のメモ。ゴールとしては、IRCの任意のチャンネルにデプロイ開始時等にメッセージを流したいと思います。

f:id:hideack:20131113093442p:plain

そういえばIRCで流す文字に色を付けたい

上の様に貼った画像の様な形でIRC上に色つき文字で表示されると可視性も高くて良いのでStringIrc という便利GemをGemfileに追加しておきます。

# Gemfile
gem 'string-irc'

追加したら bundle install します。

config/deploy.rb にIRC通知用のtaskを追加

minaの設定中に、IRC通知用の定義を追加します。今回、自分は下の様な形で書いてみました。

require 'string-irc'
namespace :util do
desc "#hoge IRC notice. (start)"
task :irc_notice_for_start do
messages = [
StringIrc.new('[hoge Deploy] Start!').white('orange').bold.to_s,
"---> #{Time.now.to_s}",
"---> Deploy to: #{domain} / Git: #{repository} / Branch: #{branch}"
]
messages.each do |message|
irc_notice '#hoge', message
end
end
end

このタスク中にある、irc_notice というメソッドは別途定義していますが省略します。役割的には任意のチャンネルに任意の文字列を通知できる様にしています。*1

試しにこのタスクだけ動作を確認したければ、

$ bundle exec mina util:irc_notice_for_start

で確認できます。

deployタスク中にIRC通知用タスク追加

上まで準備できたら実際にMinaの deploy タスク中に上で新設したIRC通知用のタスクを追加します。

# config/deploy.rb
# (中略)
desc "Deploys the current version to the server."
task :deploy => :environment do
deploy do
# Put things that will set up an empty directory into a fully set-up
# instance of your project.
invoke :'util:irc_notice_for_start'   # ここを追加した
invoke :'git:clone'
invoke :'deploy:link_shared_paths'
invoke :'bundle:install'

以上で準備が完了です。実際にMinaで

$ bundle exec mina deploy

とすれば、設定したIRCのチャンネルへデプロイ通知された上でデプロイが行われます。

*1:Ikachan環境があればそのホストに向けてPOSTする形

Chefで ~/.ssh/authorized_keys を配置する

Chefでコンフィグレーションをさせることを先日から取り組んでいて、その中でサーバに接続する予定のユーザの公開鍵を予め配置しておきたいと思っていたところ以外とあっさりとできたのでそのメモ。

状況的にはチームで共有しているユーザがあって、

  • 対象のサーバに存在するそのユーザを利用して複数の端末から公開鍵認証でのSSH接続を行いたい
  • そのため各端末の公開鍵を予め管理してコンフィグレーションする際に自動的に設定したい

が、満たせれば満足。これができれば自分たちの状況であれば、LXCを作るたびにこのレシピを適用すればすぐにチームメンバがSSH接続を特定のユーザでログインできる様になります。

まず、公開鍵を配置するdefinitionを新しく定義します。

# site-cookbooks/base/definition/authorized_keys_for.rb
define :authorized_keys_for, :keys => "public-keys", :group => nil, :home => nil do
user  = params[:name]
group = params[:group] || user
home  = params[:home]  || "/home/#{user}"
keys  = data_bag params[:keys]
if keys.any?
ssh_public_keys = keys.map do |key|
data_bag_item(params[:keys], key)
end
directory "#{home}/.ssh" do
owner user
group group
mode 0700
action :create
only_if "test -d #{home}"
end
file "#{home}/.ssh/authorized_keys" do
owner user
group group
content ssh_public_keys.collect{ |x| "#{x['key']} ##{x['id']}" }.join("\n")
end
end
end

次に配置したい公開鍵をdata bagとして定義します。今後配置したい鍵が増えればその毎に定義を増やしていけばよくなります。

data_bags/public-keys/(ユーザ名).json に足していきます。

# data_bags/public-keys/hideack.json
{
"id": "hideack",
"key": "ssh-rsa AAAAB3…."
}

ここまで準備できたら、上のdefinitionをレシピ中で使えるので、記述します。

# site-cookbooks/base/recipes/default.rb
authorized_keys_for 'remper'   # ← これが上のdefinition呼出
%w{}.each do |pkg|
package pkg do
action :install
end
end

実行してみます。

$ knife solo cook remper@*****
Running Chef on ****...
Checking Chef version...
Enter the password for remper@*****:
Uploading the kitchen...
Generating solo config...
Running Chef...
Starting Chef Client, version 11.8.0
Compiling Cookbooks...
Converging 10 resources
Recipe: base::default
* directory[/home/remper/.ssh] action create
- change mode from '0775' to '0700'
* file[/home/remper/.ssh/authorized_keys] action create
- update content in file /home/remper/.ssh/authorized_keys from d0209a to 5cf54f
(略)

これで、Chefを適用したサーバに対してSSH接続の認証に利用するための公開鍵の配置が行われました。地味に楽になりました...。

参照

デプロイツールminaを使ってみた

いま複数あるPadrinoのアプリケーションのデプロイでmina を使ってみようと試した際のメモ。
仕事ではCapistrano(+ Webistrano)を利用していますが、RubyのプロジェクトなのとソースコードもGit管理されているので試してみました。

準備

  • プロジェクトのGemfileに gem 'mina' を追加した後、bundle install する
  • プロジェクトルートで以下のコマンド実行してデプロイ用の設定ファイルを生成します
  • 中身にかなりコメント等も含めたテンプレートが出力されるので非常にわかりやすいです
$ bundle exec mina init
-----> Created ./config/deploy.rb
Edit this file, then run `mina setup` after.

接続先側(リモート側)

  • あとデプロイ先のホスト(リモート側)に公開鍵認証のSSH接続をできる様にしておく必要があります
  • また、更にデプロイ対象のソースコード取得にGithubを利用するのであれば、その接続先ホストからcloneできる様にする必要があります。
  • リモート先の公開鍵をgithubに登録しておく。 ssh-keygen -t rsa で作成して https://github.com/settings/ssh で登録
  • 登録したら、リモート側のホストでssh -T git@github.com で接続チェックしてホスト登録。*1

実行

ここまでできれば、あとはminaからsetupコマンドを実行して、リモート側のディレクトリ構成等を準備します。

$ bundle exec mina setup
-----> Setting up /home/hideack/sample
total 16
drwxrwxr-x 4 hideack hideack 4096 Nov  2 05:03 .
drwxr-xr-x 5 hideack hideack 4096 Nov  2 05:02 ..
drwxrwxr-x 2 hideack hideack 4096 Nov  2 05:03 releases
drwxrwxr-x 2 hideack hideack 4096 Nov  2 05:03 shared
-----> Done.
-----> Be sure to edit 'shared/config/database.yml'.
Elapsed time: 0.00 seconds

引き続きデプロイしてみます。上に書いたタスクだと、

  • githubからclone
  • bundle install の実行

が順に行われます。

$ bundle exec mina deploy
-----> Creating a temporary build path
-----> Fetching new git commits
-----> Using git branch 'master'
Cloning into '.'...
done.
-----> Using this git commit
-----> Symlinking shared paths
-----> Installing gem dependencies using Bundler
Fetching gem metadata from http://rubygems.org/.......
Fetching gem metadata from http://rubygems.org/..
Using rake (10.0.4)
(略)
-----> Build finished
-----> Moving build to releases/1
-----> Updating the current symlink
-----> Launching
-----> Done. Deployed v1
Elapsed time: 32.00 seconds

意外とあっさりとデプロイできてしまった。デフォルトでCapistranoで差分をデプロイする様になっている様で2回目以降のデプロイは相当スムーズです。(リポジトリの丸ごと clone は最初だけ、次回以降は差分のみ)

ほとんど引っかかることがなかったけど、使い始めたらいろいろ引っかかるかもしれないので、都度整理。

但し、ここまでではunicornが起動していない状態なので、タスクを更に追加していきます。(また今度)

参考

*1:これをしておかないと、デプロイタスクを繋いだ際にgit cloneできないエラーが出るので...。