heroku上にsinatraで作ったアプリ上げるときのハマりどころ

herokuもsinatraも初心者な自分がハマった所と、対処法書いときます。

基本は、ユーザ登録をして
https://devcenter.heroku.com/articles/ruby
の通りに進めていけば良いのですが

HTMLのテンプレート使いたい

風の噂で、hamlがサクサク書けて良いと聞いたので、hamlを選択

Gemfileにhamlを追加
gem 'haml'
bundle installでhamlをインストール
bundle install
APPファイル(web.rb)でhaml読み込み
require 'haml'


これで、準備OK。

hamlテンプレートを指定する
get '/' do
  haml :index
end

と書いてやれば、index.hamlファイルをテンプレートとして利用可能。

# -*- coding: utf-8 -*-

require 'rubygems'
require 'sinatra'
require 'haml'

# ここだと、渡せないよ
@hoge = "この位置で宣言しても渡せません"

get '/' do
  # ここで、宣言すれば
  # 変数の受け渡しも可能
  @foo = "bar"
  haml :index
end
テンプレートはどこに置けば良いの?

sinatraのデフォルトだと、viewsディレクトリを見に行くらしいので

mkdir views

ディレクトリ作成。
viewsの"s"をお忘れなく。

cssとかは?

静的ファイルは、publicディレクトリ

mkdir public

同じようなコードで毎回テンプレート名指定するのがめんどくさい

パスと同じテンプレートファイル名ならば、これでいけた。

get '/:path' do
  path = params[:path]
  haml path.intern
end

hamlでちょっと凝ったことしたい

HTML5のフォーマットで書きたい

hamlのrequire文の下辺りに

set:haml, :format => :html5

で、フォーマットを指定する。

HTMLエスケープも自動でしたい

エスケープ設定も追加で

set:haml, :format => :html5, :escape_html => true
共通部分はレイアウトとしてまとめたい

layout.hamlという名前でレイアウトファイルを作成すれば
自動的にレイアウトとして認識してくれます。
ビューファイルは、= yieldで呼べる。
以下、body宣言までのレイアウトが、ビューを呼ぶ例。

!!!
%html{:lang => "ja"}
  %head
    %meta{:charset => "UTF-8"}
  %body
    / ここでビューを呼ぶ
    / =ではなく、!=にしないとエスケープされるので注意
    != yield

呼ばれるビューファイル側のインデントは、インデント無しの状態から始める。
bodyの子要素だからと、インデント2つ分から始めたら怒られた。

要素の属性値がたくさんあって、1行が長くなるのがいや

rubyのハッシュに詰めてから渡すと多少改善される

:ruby
  opts = {
    :src => "https://www.google.com/calendar/embed?src=XXX
    :style => "border: 0",
    :width => "800",
    :height => "600",
    :frameborder => "0",
    :scrolling => "no"
  }
%iframe{opts}

herokuでアプリの名前変えたらコミットできなくなっちゃった

# 現在の設定確認
% git remote -v 
heroku     git@heroku.com:strong-day-3354.git (fetch)
heroku     git@heroku.com:strong-day-3354.git (push)
# 変更
# アプリ名をAAAに変えた場合
% git remote set-url heroku git@heroku.com:AAA.git
# 反映確認
% git remote -v                                    
heroku     git@heroku.com:AAA.git (fetch)
heroku     git@heroku.com:AAA.git (push)

感想

使ってみての感想は、sinatraは記述量が少なくて便利。
命名規則は知らないとさっぱり。


hamlは、噂通りサクサクかける。
<、>と閉じタグ書かなくて良いのが楽。
rubyコードと記法が似ているから、覚えやすい。


裏側は、サクサクだな。
後は、表のデザインセンスがあれば、、、

社内の開発環境とローカルの開発環境の連携方法

どんな時に便利か?

全てのサービスをローカル環境として構築できれば良いのですが
それがあまり現実的ではなく、基本は外部のアプリを利用して
一部アプリだけローカルのアプリを利用したいとい時に便利です。

ローカル環境構成図

PAC(Proxy Access-Control)ファイルを利用して
まずは、ローカルとそれ以外へのアクセスを振り分けています。


ローカルでは、nginxがアクセスを集約しています。
これは、80番や443番ポートを複数のアプリで利用するためで
ホスト名によって各アプリへリクエストの振り分けをしています。


PACファイル

開発環境用に社内プロキシが無い場合は、設定不要です。
JavaScriptの文法で、proxyの設定が書けるファイルです。


Firefoxだと、下記の場所からから設定できます。
オプション > ネットワーク > 接続設定 > 自動プロキシ設定スクリプト URL


proxyを通さない場合(今回はローカル環境)は、"DIRECT"という文字列を返しておきます。
ドメイン名に"hatena.ne.jp"が入る場合は、開発環境用のproxyを通したい。という場合は、
組み込みのshExpMatch関数を使い、host名とマッチングをかけ、PROXYのIPとポートを返します。
セミコロンでproxyもしくは、"DIRECT"をつなげると、順番に接続を試みてくれます。

function FindProxyForURL(url, host) {
  if (isInNet(host, "127.0.0.1", "255.255.255.255")) {
    return "DIRECT";
  } else if (isDevHost(host)) {
    // dev.proxy:9999
    return "PROXY 172.16.0.2:9999";
  }

  // default.proxy:9999
  return "PROXY 172.16.0.3:9999; DIRECT";
}

function isDevHost(host) {
  return shExpMatch(host, "*.hatena.ne.jp");
}

hostsの設定

isInNet(host, "127.0.0.1", "255.255.255.255")でローカル環境として判定をさせるために
/etc/hosts で、ローカルアプリのドメインのIPを、127.0.0.1として登録しておきます。

# local env
127.0.0.1          d.hatena.ne.jp


これで、http://d.hatena.ne.jp/とアクセスした場合は、127.0.0.1:80へのアクセスに
それ以外のはてなドメインは、172.16.0.2:9999のproxy経由でのアクセスが可能になりました。

nginxの設定

ほぼ、デフォルトの設定のままですが、肝はinclude文です。
これで、各アプリに振り分けるようの設定を読み込んでいます。

error_log  /usr/local/var/logs/nginx/error.log;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       params/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /usr/local/var/logs/nginx/access.log  main;

    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;

    keepalive_timeout  65;
    sendfile on;
    gzip on;

    include    /usr/local/var/nginx/conf/extra/backends/*.conf;
}
/usr/local/var/nginx/conf/extra/backends/d-hatena-ne-jp.confの場合

d.hatena.ne.jp用のtomcatアプリが、8080ポートで動いている場合は
以下のような設定になります。

server {
    listen      80;
    server_name d.hatena.ne.jp;

    location / {
        proxy_pass http://127.0.0.1:8080;
    }
}


アプリを増やしたい場合は、hostsファイルへの追記とconfファイルの新規作成で簡単に行えます。

魔術でデプロイ

1文字に飽きたら、詠唱でも

準備

# ホームディレクトリへ移動
function I(){ cd ~/ }
# バックアップ
function Steel(){ cp /usr/local/tomcat/webapps/UBW.war ./UBW.war }
# リリースファイルをSTGサーバから取得
function Unknown(){ scp stg:UBW.war ~/UBW.war }
# tomcat停止
function Nor(){ sudo -u tomcat /usr/local/tomcat/bin/shutdown -force }
# アプリ削除
function Have(){ sudo -u tomcat rm -rf /usr/local/tomcat/webapps/UBW }
# warファイル入れ替え
function Yet,(){ sudo -u tomcat cp ./UBW.war /usr/local/tomcat/webapps/UBW.war }
# tomcat開始
function So(){ sudo -u tomcat /bin/sh usr/local/tomcat/bin/startup.sh }

詠唱

I am the bone of my sword.
Steel is my body, and fire is my blood.
I have created over a thousand blades.
Unknown to Death.
Nor known to Life.
Have withstood pain to create many weapons.
Yet, those hands will never hold anything.
So as I pray, unlimited blade works.

1文字シリーズ

毎日、毎日タイプするコマンドならば、タイプ数を減らすに限るよね。
抜けてるところには、何を入れよう?

1文字alias

alias a="sudo aptitude"
alias b=""
alias c="cat"
alias d="diff"
alias e="echo"
alias f="find . -type f -name"
alias g="grep"
alias h="head"
alias i="/sbin/ifconfig"
alias j=""
alias k=""
alias l="ls -ltrah"
alias m=""
alias n="node -e"
alias o=""
alias p="perl -wnl -e"
alias q=""
alias r="reset"
alias s="sqlplus user_name/password@host[:port][/service_name]"
alias t="tail -f"
alias u=""
alias v="view"
alias x="xargs"
alias y=""
alias z=""

1文字変数

aliasよりさらにネタが無いな

export a="access_`date +%Y%m%d`.log"
export b=""
export c=""
export d="`date +%d`"
export e="error_`date +%Y%m%d`.log"
export f=""
export g=""
export h=""
export i=""
export j=""
export k=""
export l=""
export m="`date +%m`"
export n=""
export o=""
export p=""
export q=""
export r=""
export s=""
export t=""
export u=""
export v=""
export w=""
export x=""
export y="`date +%Y`"
export z=""

node.jsでsocket通信

みんな大好きエコーサーバ

Install websocket server module

 % npm install websocket-server

Server side

httpとwsプロトコルで、返す値を変えられる。
サーバの監視用にする、クライアント側のコードを返すなどの利用方法が考えられる。

var http = require('http');
var ws   = require('websocket-server');

// http
server = http.createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});    
  res.end("<html><body><h1>It's Works!</h1></body></html>");
});
server.listen(10003);

// WebSocket
var socket = ws.createServer({'server': server});
socket.on('connection', function(conn){
  conn.on('message', function(msg){
    conn.send(msg);
  }); 
  conn.on('close', function(){
    console.log('closed');  
  })  
});

Client side

今回は、サーバのコードとは分けてあるので、httpでアクセスできる場所に適当に置く。
ローカルファイル(file://)だと、セキュリティに引っかかりWebSocket通信ができない。

<script>
  var ws = new WebSocket('ws://127.0.0.1:10003/');
  ws.onopen = function() {
    ws.send('hello world'); 
  }
  ws.onmessage = function(evt) {
    console.log(evt.data);
  }
  ws.onclose = function() {
    console.log('closed');  
  }
</script>

カスタムfindコマンド

alias f="find"


でも、そこそこ便利だけれど
.gitディレクトリを、検索対象外にしたいときに

f . -name .git -prune -print


となり、長いし忘れやすいので

#!/bin/bash
case $# in
1)
  path='.'
  name=$1
  ;;
2)
  path=$1
  name=$2
  ;;
esac

#echo "find \"$path\" -name .git -prune -o -name \"$name\" -print"
find "$path" -name .git -prune -o -name "$name" -print


デフォルトで、.gitディレクトリを抜いてfindさせるシェルスクリプトを作成。
引数が1つの場合は、カレントディレクトリから引数で与えられたnameを検索。
引数が2つの場合は、第一引数のディレクトリから、第二引数のnameを検索。

node.jsを使ってWebページからテキストを取得

jsdomモジュール使うと、http周りの記述しなくて良いから楽だね。
レスポンスヘッダもしくは、メタ要素で指定された文字コード見てUTF-8に変換すれば
もうちょっとましになりそう。

ソース

// getText.js
var jsdom = require('jsdom');

function removeElements(list) {
  for (var i = 0, l = list.length; i < l; i++) {
    var elem = list[i];
    elem.parentNode.removeChild(elem);
  }
}

jsdom.env(process.argv[2], function(errors, window) {
  var body = window.document.body;
  removeElements(body.getElementsByTagName('script'));
  removeElements(body.getElementsByTagName('style'));

  console.log(body.textContent);
});

使い方

% node getText.js http://d.hatena.ne.jp/TakiTake/