D.

Ruby で序数 (1st, 2nd, 3rd...) を生成する。

こんな感じ。

def number_to_ordinal(num)
  num = num.to_i
  if (10...20)===num
    "#{num}th"
  else
    g = %w{ th st nd rd th th th th th th }
    a = num.to_s
    c=a[-1..-1].to_i
    a + g[c]
  end
end

number_to_ordinal(3) => "3rd"
number_to_ordinal(13) => "13th"
number_to_ordinal(23) => "23rd"

ActiveSupport あたりに同機能のヘルパー無いのかな。

マルチアーキテクチャなステージング環境で bundler を利用する。

Ruby on Rails 等で使われる bundler はアプリケーションに依存する gem パッケージをバンドルすることができ、パッケージの管理に役に立つ。しかしアーキテクチャ固有のパッケージを保持してしまうことで、ステージング環境で複数のアーキテクチャが混在している場合にデプロイや CI において混乱をきたしやすい。ここでは経験から得られたノウハウとハマリポイントをまとめる。

bundle install は path を指定して実行する。

なにも考えず bundle install すると $GEM_HOME が汚染される。そこでアーキテクチャごとに以下のようにパスを指定して実行するのがいいだろう。

bundle install --path vendor/bundler64

混乱を避けるため、アーキテクチャごとに PATH を変えておくのが良さそうだ。同一アーキテクチャであれば rsync 等で丸々ディレクトリをコピーすることが可能となり便利である。

サイトローカルな設定ファイルとパッケージをリポジトリに含めない。

Rails の場合 bundle install により $RAILS_APP 配下に Gemfile.lock, .bundle/config, vendor/bundler64 が作られる。これらはサイトローカルなものなのでバージョン管理ソフトウェアで管理しないように ignore にする。

CI やデプロイを実行する場合は各々のスクリプト内で以下のように bundle exec を付けて rake タスクを実行する。これにより .bundle/config 内部で指定されたパスにあるパッケージが利用される。

export RAILS_ENV=production # または test
bundle exec rake assets:precompile
bundle exec rake db:migrate

開発の場合も同様に bundle exec を利用する。

bundle exec rails server -p 9999 -e development

リンケージされるモジュールの場所に気をつける。

Ruby のコード自体は共通でもリンクされるモジュールが正しくない場合は当然エラーとなる。

ruby: symbol lookup error:
/var/lib/jenkins/jobs/rails_app/workspace/vendor/bundler64/ruby/1.9.1/gems/sqlite3-1.3.5/lib/sqlite3/sqlite3_native.so:
undefined symbol: sqlite3_open_v2

このような場合は Gemfile.lock と .bundle/config 及びバンドルされたパッケージを一旦消去して、再度 bundle install する。また Ruby を起動するソフトウェアのライブラリのパスをよく確認する。

たとえば上記の例で該当モジュールを ldd したところ以下の結果が得られたものとする。この場合、読み込むモジュールを LD_LIBRARY_PATH で指定すれば正常に動作しそうである。

ldd /var/lib/jenkins/jobs/rails_app/workspace/vendor/bundler64/ruby/1.9.1/gems/sqlite3-1.3.5/lib/sqlite3/sqlite3_native.so
        linux-vdso.so.1 =>  (0x00007fff86783000)
        libsqlite3.so.0 => /usr/local/lib/libsqlite3.so.0 (0x00002ab07381c000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00002ab073aa5000)
        librt.so.1 => /lib64/librt.so.1 (0x00002ab073cc0000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00002ab073ec9000)
        libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00002ab0740ce000)
        libm.so.6 => /lib64/libm.so.6 (0x00002ab074306000)
        libc.so.6 => /lib64/libc.so.6 (0x00002ab074589000)
        /lib64/ld-linux-x86-64.so.2 (0x00000032dd800000)

Jenkins CI の場合はビルド後の追加タスクとしてシェルスクリプトを別途用意して、スクリプト内部で環境変数を export すれば良い。


RHEL + Passenger の場合は Apache の起動スクリプト /etc/init.d/httpd で以下のように環境変数を指定する。

export LD_LIBRARY_PATH=/usr/local/lib:/usr/lib

Apache は設定ファイル httpd.conf で以下のように指定しないと環境変数が有効にならないのでこちらも忘れずに指定する。

PassEnv LD_LIBRARY_PATH

Linux ディストリビューションを安全に利用するための基本。

これらはすべて基本中の基本、絶対に守るべき原則と言っても良い内容である。しかしながら企業や学校の内部等ではこれらが遵守されていないような場面も多々見受けられるため、ここに書き記しておく。

root ログインを避け、管理作業には sudo を利用する。

root ログインを避け sudo を使うようにする。最近の Linux ディストリビューションでは標準で sudo が入っていることが多い。 /etc/sudoers で以下のように sudo 利用可能なグループを定義する。

root ALL=(ALL) ALL
%wheel ALL=(ALL) ALL
%admin ALL=(ALL) ALL

wheel グループに属するユーザーは su を利用できる設定になっていることが多い。そこで新しく admin グループを追加し、管理者の通常利用ユーザーをそのグループに所属させるのが良いだろう。

パスワード認証を禁止して公開鍵認証のみ利用可能にする。

sudo を利用可能になったら公開鍵認証を設定した上で /etc/ssh/sshd_config で以下のようにパスワード認証を利用禁止にし sshd を再起動する。またポート番号を変更するのが望ましい (ここでは仮にポート番号を 11111 とするがこの例のまま適用しないこと) 。

PasswordAuthentication no
UsePAM no
PubkeyAuthentication yes
PermitRootLogin without-password
Port 11111

秘密鍵は各ユーザーごとに ssh-keygen -t rsa コマンドで作成する。なおこの際、平文の電子メールのようにセキュアでない媒体で秘密鍵を各利用者に配布するのは避ける。万一、平文の電子メールで秘密鍵を配布した場合、受け取った者は安全性を確保するためすぐに秘密鍵を再作成して差し替えるのが常識である。

iptables でポートを閉じる。

RHEL 互換ディストリビューションの場合 /etc/sysconfig/iptables に設定を記述すれば反映される。このファイルは root しか参照できないようにパーミッションを設定するべきである。設定後 iptables を再起動することで反映される。

:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:LOG_PINGDEATH - [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -f -j LOG --log-prefix "[IPTABLES FRAGMENT] : "
-A INPUT -f -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -j LOG_PINGDEATH
-A INPUT -d 255.255.255.255 -j DROP
-A INPUT -d 224.0.0.1 -j DROP
-A INPUT -p tcp -m tcp --dport 113 -j REJECT --reject-with tcp-reset
-A INPUT -p tcp --dport 11111 -m state --state NEW -m recent --set --name SSH
-A INPUT -p tcp --dport 11111 -m state --state NEW -m recent --update --seconds 600 --hitcount 10 --rttl --name SSH -j LOG --log-prefix "SSH attack: "
-A INPUT -p tcp --dport 11111 -m state --state NEW -m recent --update --seconds 600 --hitcount 10 --rttl --name SSH -j DROP
-A INPUT -p tcp -m tcp --dport 11111 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
-A INPUT -j DROP
-A FORWARD -j DROP
-A LOG_PINGDEATH -m limit --limit 1/sec --limit-burst 4 -j ACCEPT
-A LOG_PINGDEATH -j LOG --log-prefix "[IPTABLES PINGDEATH] : "
-A LOG_PINGDEATH -j DROP
COMMIT

この例では 11111 番ポートへの総当たり攻撃を防止している (600 秒以内に 10 回以上アクセスしたらログに記録して破棄する)。また他にも各種攻撃への対策が盛り込まれている。個々の意味は iptables のマニュアルを参照すること。これをテンプレートとした上で、そのまま利用せず要件にあわせて必ずカスタマイズした上で適用する。

LVM のサイズをオンラインのまま変更する。

最近のメジャーな Linux ディストリビューションでは標準で LVM が利用可能となっている。この LVM のオンラインリサイズを利用すると、システムの停止無しにファイルシステムのサイズを変更できる。これは非常に便利なのでやり方をまとめておく。


ここでは Debian Squeeze を利用しファイルシステムには ext3 を利用しているものとする。なお UbuntuRHEL 等のディストリビューションext4 等を利用している場合も同様である。ユースケースとして /home に多めに割り当てていたサイズを縮小し /usr や /var 等に配分する方法を想定する。

ファイルシステムのサイズ縮小

LVM のパーティションをアンマウントして、まずファイルシステム領域を縮小する。ext3/4 の場合は resize2fs で縮小できる。ここでは仮に 283GB の /home から 10GB 縮小するものと仮定する。

df -T -h
umount /dev/mapper/hostname-home
e2fsck -f /dev/mapper/hostname-home
resize2fs /dev/mapper/hostname-home 273G

mount -a でマウントしなおして df -T -h で確認すると領域が 10GB 減少しているのがわかる。

LVM のオンラインリサイズ

次に LV を縮小する。 lvdisplay であらかじめ現在の LV の状態を確認したあと lvresize -L で領域を 10GB 縮小する。

lvresize -L -10G /dev/mapper/hostname-home

ファイルシステムのサイズより小さい値まで LV を縮小してしまうとシステムの破壊につながるので気をつける。特にオプションの指定ミスはやりがちなので注意する。LV の縮小に成功すると VG 全体に空き領域が生まれるはずなので vgdisplay で確認する。今度はこれを各パーティションに割り振るため LV を拡大する。

lvresize -L +5G /dev/mapper/hostname-usr
lvresize -L +1G /dev/mapper/hostname-root
lvresize -L +3G /dev/mapper/hostname-var
lvresize -L +1G /dev/mapper/hostname-tmp

ファイルシステムのサイズ拡大

あとは引数無しで resize2fs を実行し LV で割り当てられた上限サイズまでファイルシステムを拡張すれば良い。

resize2fs /dev/mapper/hostname-usr
resize2fs /dev/mapper/hostname-root
resize2fs /dev/mapper/hostname-var
resize2fs /dev/mapper/hostname-tmp
resize2fs /dev/mapper/hostname-home


Debian の標準のインストーラーで LVM のパーティーションを構築すると /home 以外のサイズはかなりストイックに設定されるが、オンラインリサイズすればいつでも補充できるので心配する必要は無い。


ちなみにこの記事は以前に書いた記事のサルベージである。

Kaminari で超お手軽にページネートを実装する。

Kaminari を使うと超簡単にページネートが実装できる。

sudo gem install kaminari

Gemfile で gem 'kaminari' を指定して bundle install する。


あとはコントローラーで Model.all の代わりに .scoped メソッドを利用する。ページ数はモデルで指定しても良いのだけど面倒なので一気に指定。

@records = Record.scoped(:include => [:related_table_1, :related_table_2],
:order => "created_at DESC").page(params[:page]).per(15)


ビューは Haml で書くとこんな感じ。たったこれだけでページネートを実装できる。

= paginate @records, :window => 4, :outer_window => 1


Rails + Bootstrap なアプリの場合は class 名称などを調整する必要があるが、これも出来合いのモノがすでに存在する。以下を git clone してアプリのビューに配置するだけで良い。
https://github.com/gabetax/twitter-bootstrap-kaminari-views


ページネートを日本語化したければ config/locales/ja.yml などで以下のように指定する。

ja:
  views:
    pagination:
      previous: "<< 前ページ"
      next: "次ページ >>"
      first: "最初"
      last: "最後"
      truncate: ""

こんな感じに Bootstrap らしいページネートが実装される。

二度押し防止機能をデフォルトで付ける。

昔からよくある Form が Submit された瞬間にボタンを disable にするというアレ。これをデフォルトの挙動にする。


config/initializers/submit_with_disable.rb

# -*- encoding: utf-8 -*-
module ActionView
  module Helpers
    module FormTagHelper
      alias_method :original_submit_tag, :submit_tag
      def submit_tag(value=nil, options={})
        options[:disable_with] = '処理中...' unless options[:disable_with]
        original_submit_tag(value, options)
      end
    end
  end
end

これですべての送信ボタンに標準で :disable_with が付く。カスタマイズしたいときはそのビューだけ :disable_with を明示的に指定すれば良い。

このハックは便利すぎてすべての Rails アプリにデフォで組み込みたいレベル。ただ jquery-ujs を利用していると :disable_with が効かないという情報もあるので注意が必要だ。
http://d.hatena.ne.jp/holysugar/20101124/p1

参考

これはさんざん既出な情報で、基本的に以下の記事をそのまま参考にした。
http://pinzolog.blogspot.com/2011/06/submit-disablewith.html