ドリコムを支えるデータ分析基盤がTD+AWSに移行した話

はじめに

これは ドリコムAdventCalendar の7日目です

6日目は、keiichironaganoさんによる iTunes 使用許諾更新のとき一旦キャンセルしてほしい話 です

【その2】ドリコム Advent Calendar 2015 もあります

自己紹介

@ka_nipan

去年の ドリコムを支えるデータ分析基盤 に引き続き、今年もドリコムのデータ分析基盤を担当しています。

分析基盤をTreasure Dataに移行

オンプレ環境の Hadoop からTreasure Data に移行しました。

また、ジョブ管理ツールやBIツールといったサーバーもAmazon EC2 に移行しており、 徐々にオンプレ環境を離れつつあります。

背景

オンプレ環境で Hadoop を運用して3年も経つと考えなければならないのが HW の寿命です。

さてどうしようかとなった時に、ほぼ迷いなく外部サービスに移行するという選択を取りました。

Treasure Data、BigQuery、Amazon Redshift のようなサービスがある中で HW の交換作業をしてまでこのままオンプレで運用し続けるメリットはあまり感じられなかったためです。

サービス選定

候補として上で挙げた Treasure Data、BigQuery、Amazon Redshift の3サービスを実際に触ってみて最終的に Treasure Data を使うことに決めました。

Redshift はちょっと触っただけなのですが、実際に触ってみた所感の比較です。 ※ 2015.1 時点

     Treasuredata presto BigQuery Amazon Redshift
リソースのスケール フルマネージ フルマネージ ユーザがノード変更
同時実行 4〜 20 5〜
課金 固定額。お高め データサイズ+クエリ数+クエリスキャンサイズ+etc 。全体的にお安い。データ抽出サイズに気をつけないとウン百万の請求も 固定額+etc
パフォーマンス 速い (件数依存、対象が数十億ならHiveにする) 件数に関係なく速い 速い (件数に依存)
テーブル(分散) 設計 不要 不要 必要
スキーマレス 固定 (json構造ならレス) 固定 (json構造ならレス)
データ投入 実質入れ放題 制約有り (回避方法あり) S3経由 (大量データのインポートが遅い)
サポート
総評 バランスが良い。専任で人をつけなくても安定した価格、性能で運用できそう パフォーマンスを保つため、制約が多いので注意が必要 (制約条件が突然変わったら爆死する可能性)。節約したい時、件数が多くてどうしようもなくなった時に使えるかも 安価で始められるDWH。大規模になるほどチューニングが必要になってくる。 スモールスタート。データマート向き。変更に弱い。

他にも比較する部分が色々あるのですが、あんまり細かくなってもしょうがないので、ざっくりとした所感で書きました。

また、弊社の分析基盤としては職人をできるだけ作らないような体制を目指しているので一番運用が楽で安定しそうな Tresure Data を使うことになりました。

新データ分析基盤全体図

f:id:ka_nipan:20151126175144p:plain

大きなところは Hadoop が Treasure Data に変わり、AWS のサーバーを徐々に使い始めているというところで、それ以外のところは去年の ドリコムを支えるデータ分析基盤 とほぼ変わっていません。

DBのスナップショットはS3へ

Hadoop の時は、アプリのログもDBから取ってきたスナップショット(tsv) もHDFS に置いていたのですが、Treasure Data にはデータ件数の制限があるので、無闇に全部 TD に置くということはせずに TD 上で集計が必要なもの以外は S3 に置くことにしています。

また、ログもTD上には直近x日まで保存して、古いものはS3に逃がすということもログ件数調整の一環としてやっています。

闇のスクレイピング技術は今も健在

去年のエントリの闇スクレピングは今も元気に動いています。。。地味に便利なのが憎らしい。。。

まとめ   

・安定した運用、職人芸を少しでも減らすために Treasure Data を分析基盤に

・特定の条件下であれば、BigQuery、Redshift はあり

・金を出した分だけ楽ができる

8日目は @sazae657 さんです

ドリコムを支えるデータ分析基盤

はじめに

これは ドリコムAdventCalendar の4日目です

3日目は、@arihh さんによる 3年くらいお菓子神社運営してきた です

自己紹介

@ka_nipan

ドリコムに新卒で入社し、Android開発、BtoBtoC のwebサービス開発を経て、現在は弊社アプリのログ収集から集計、可視化、その他周辺ツールといった分析基盤の面倒を見ています

本日はそのデータ基盤の話を書きます

データ分析基盤全体図

f:id:ka_nipan:20141128203507p:plain

弊社では Hadoop をオンプレで運用していて、そこにログや分析用のデータを置いています

メリット

運用コストが安い

Treasure Data、Big Query、Amazon Redshift 等の外部サービスを使うよりは安く済みます

自由度が高い

各サービスには容量をはじめ色々と制限があったり、こちらの要求仕様にマッチしない部分が少なからずありますが、自前の場合その辺は融通がききやすいです

分析する人が生データにアクセスしやすい

後述しますが、分析チームの人が R言語 で処理しやすいように、Hadoop にデータを集積しています

なので、基本的に HDFS には gzip 圧縮した tsv ファイル等を置いていて、SequenceFile は置いていません

分析用のサーバーに Fuse で mount しているので通常のファイルシステムのように扱えます

デメリット

メンテナンスが辛い

HDDに障害があったり、運用年数が経ってくると新しいものに入れ替えたり、リカバリ作業をする必要があります

また、HadoopやOSのバージョンアップをしようと思ったら、NameNode や DataNode 全台、その他すべて含めると結構な台数になってくるので精神がガリガリ削られていきます

HDFS全体の容量とかブロックの数とか気にしてあげないといけない点も結構あります

データ取得・保存

ログ送信

ログの送信は fluentd を使用しています

鉄板ですね

図では端折ってますが、web → log collector → Hadoop のように一旦ログを集約してから Hadoop に送信するようにしています

テスト環境のログ排除

テスト環境のログが Hadoop に送られてしまうと集計値がくるってしまうので、ホスト名に test, staging のような文字列が入っている場合 Hadoop に送信しないようにしています

ログ送信成否の確認

webサーバー追加したけど設定ミスでログ送れてませんでしたー!等を突き止めやすくするために、web → collector に送る時にホスト名を送るようにしています

事前処理

送られてきたログに対して、集計に必要な付加情報を加える処理をしています

ここでフォーマットのチェックも行っていて、エラーとなったログは集計対象から除外されます

集計は hive を使用しており、事前処理が終わったらロードされます

スナップショット取得

アプリのDBにSQLを投げて、その結果をファイルとしてHDFSに保存しています

DBのフェイルオーバー対策

DBにクエリを投げる際、SLAVE に投げるわけですが、DBがフェイルオーバーするとSLAVEのIPアドレスも変わってしまいます

そこで、SLAVE のIP情報を管理用のサーバーのDBに入れておき、アプリのDBがフェイルオーバーすると管理用DBにあるIPも書き換わるという仕組みが用意してあります

この仕組みにより常にSLAVEへとクエリを投げることが可能になっています

データの持ち方としてはこんな感じ

CREATE TABLE IF NOT EXISTS `slavedb` (
  `app_key`               varchar(64)    NOT NULL ,
  `host_name`             varchar(64)    NOT NULL ,
  `slave_ipaddress`       varchar(64)    NOT NULL ,
  `db_name`               varchar(64)    NOT NULL ,
  `updated_at`            timestamp      NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`app_key`,`host_name`,`db_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

帯域への配慮

結構な数のクエリを投げたり、結果がでかいものがあったりで何も考えずに一気に流すとネットワークの帯域を食いつぶす可能性があった (実際やばかった) ので、

一度に流すクエリの量を抑える仕組みを作って安全に配慮してます

クエリの管理

アプリのDBへ投げるクエリは ActiveAdmin で管理しています

また、ここで管理されているクエリは管理画面から Explain を実行してその結果を見ることができるように手を加えています

クエリのシンタックスエラーのチェックもできて地味に便利

f:id:ka_nipan:20141203155910p:plain

バックアップ

ログ、スナップショットファイルは Hadoop に取り込まれると同時にバックアップサーバにコピーされます

集計

前述のとおり集計はみんな大好き hive を使っています

presto はテスト的に使ってみたりしてます

集計したデータは可視化のため MySQL

集計単位

  • hourly
  • dayily
  • monthly

ジョブ数

集計以外の全ジョブ含め約600個

可視化

有料BIツールを使っていますが、パフォーマンス、アカウント制限等々の理由で結局BIツール自作しました

f:id:ka_nipan:20141128203959p:plain

黒背景にするとなんかカッコよく見えますね!

分析チーム

弊社にはアプリの分析を行う分析専門のチームがいて、

ハイスペックな分析用サーバ数台に HadoopFuse マウントして自由に Hadoop 上のファイルを使えるような環境を用意しています

基本的に R で分析を行い、彼らの定期ジョブは分析サーバーで Jenkis で実行しています

少し前まで cron を使っていましたが、さすがに管理が辛くなってきました

スクレイピング

ログの集計値の正当性を担保するために、プラットフォームの管理画面で見ることができる数字(売上等)を取ってこなければいけません

(ちなみにあまりにも誤差があると、ログと管理画面からDLできる課金明細をつき合せていくことになります...)

しかし、プラットフォームによってサイトの作りがまちまちです

単純にスクレイピング用ライブラリではすべてを解決できませんでした (そのへんのAPIは提供されていない)

そこで色々もがいた結果、以下のような環境が出来上がりました

使用しているもの

f:id:ka_nipan:20141128205157p:plain

Grease monkey とは firefoxプラグインのひとつで、特定のURLに対して任意のコード (javascript) を実行できます

仕組み

  • Linux に作った GUI 環境から指定のURLに firefox でアクセスする
  • グリモンで目的のページまでたどり着いたら、そのページをテキストファイルとしてダウンロードします
  • そのテキストファイルから正規表現で必要な部分を抜いてDBに入れます

もはやスクレイピングでもなんでもないですね

最高に力技ですが、この方法でURLの末尾にランダムなハッシュがつくサイトも非同期処理をするサイトでもなんでも抜いてこれるようになりました!

サンプルコード (雰囲気だけ感じてもらえれば...)

// ==UserScript==
// @name           kani_scraper
// @namespace      imperial_cross
// @include        https://developer.hogee.jp/
// @include        https://developer.hogee.jp/*
// ==/UserScript==


(function()
{


if(location.href == "https://developer.hogee.jp/"){
    document.getElementsByName("login_id")[0].value = login_id;
    document.getElementsByName("password")[0].value = password;
    document.getElementsByName("form")[0].submit();
}else if(location.href == "https://developer.hogee.jp/home"){

// http://d.hatena.ne.jp/a_bicky/20110718/1311027391 参照
writeHtmlFile =  function () {
    function writeToLocal (filename, content){
    var ua = navigator.userAgent.toLowerCase();

    try {
        if (ua.indexOf('firefox') != -1) {  // Firefox
                //filename = (ua.indexOf('windows') != -1 ? 'C:\\tmp\\' : '/tmp/') + filename;
        if (ua.indexOf('windows') != -1){
            filename = "C:\\tmp\\" + filename;
        }
        else {
                    filename = path + filename;
        }

        unsafeWindow.netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
        // ファイルコンポーネントの取得+ローカルファイル操作用のインターフェイスの取得;
        var file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile);
        file.initWithPath(filename);

        var fileStream = Components
            .classes['@mozilla.org/network/file-output-stream;1']
            .createInstance(Components.interfaces.nsIFileOutputStream);
        // ファイルが存在しない場合は664の権限で新規作成して書き込み権限で開く
        // cf. https://developer.mozilla.org/en/NsIFileOutputStream
        //     http://www.oxymoronical.com/experiments/apidocs/interface/nsIFileOutputStream;
        fileStream.init(file,
                0x02 | 0x08,  // 0x01: 読み取り専用, 0x02: 書き込み, 0x03: 読み書き, 0x08: 新規作成, 0x10: 追記
                0664,         // mode
                0             // 第4引数は現在サポートしていないとか
                   );

        // cf. http://www.oxymoronical.com/experiments/apidocs/interface/nsIConverterOutputStream
        var converterStream = Components
            .classes['@mozilla.org/intl/converter-output-stream;1']
            .createInstance(Components.interfaces.nsIConverterOutputStream);
        converterStream.init(fileStream, 'UTF-8', content.length,
                     Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
        converterStream.writeString(content);

        converterStream.close();
        fileStream.close();
        //alert('書き込みが完了しました!');
        } else if (ua.indexOf('chrome') != -1) {  // Google Chrome
               // 起動オプションに --unlimited-quota-for-files --allow-file-access-from-files をつける必要あり
        function errorCallback(e) {
            alert('Error: ' + e.code);
        }

        function fsCallback(fs) {
            fs.root.getFile(filename, {create: true}, function(fileEntry) {
            fileEntry.createWriter(function(fileWriter) {
                fileWriter.onwriteend = function(e) {
                alert('書き込みが完了しました!');
                };

                fileWriter.onerror = function(e) {
                alert('Failed: ' + e);
                };

                var bb = new WebKitBlobBuilder();
                bb.append(content);
                var output = bb.getBlob('text/plain');
                fileWriter.write(output);
            }, errorCallback);
            }, errorCallback);
        }
        // 現時点ではたぶん第1引数はPERSISTENTもTEMPORARYディレクトリ名が異なるだけだし、
        // 第2引数は極端な話0でもOK
        webkitRequestFileSystem(PERSISTENT, 1024, fsCallback, errorCallback);
        } else if (ua.indexOf('msie')) {  // MS IE
        filename = 'C:\\tmp\\' + filename;
        // インターネットオプションで「スクリプトを実行しても安全だとマークされていない
        // ActiveX コントロールの初期化とスクリプトの実行(セキュリティで保護されていない)」
        // を有効にする必要あり
        var fso = new ActiveXObject('Scripting.FileSystemObject');

        // ファイルを新規作成して書き込みモードで開く (文字コードはUTF-16)
        // cf. http://msdn.microsoft.com/ja-jp/library/cc428044.aspx
        //     http://msdn.microsoft.com/ja-jp/library/cc428042.aspx
        var file = fso.OpenTextFile(filename,
                        2,     // 1: 読み取り専用, 2: 書き込み, 8: 追記
                        true,  // ファイルが存在しなければ新規作成するかどうか
                        -1     // -2: OSのデフォルト文字コード, -1: UTF-16, 0: ASCII
                       );
        file.Write(content);

        file.Close();
        alert('書き込みが完了しました!');

        /*
         * ADODB.Stream を使う場合(レジストリをいじっても何故か書き込めない・・・)
         */
        // var adodbStream = new ActiveXObject('ADODB.Stream');
        // adodbStream.type = 2;  // テキストファイル(バイナリは1)
        // adodbStream.charset = 'UTF-8';
        // adodbStream.open(filename);
        // adodbStream.writeText(content);
        // adodbStream.saveToFile(filename, 2);  // 上書き保存(1だと新規作成のみが対象)
        // adodbStream.close();
        } else {
        alert('エラー: ローカルファイルへの書き込み方がわかりません・・・');
        }
    } catch (e) {
        alert('Error: ' + e);
    }
   }    


    writeToLocal('test1.txt', "hogehoge");


}
}
setTimeout( writeHtmlFile, 20000);//20秒後にページを閉じる
})();

闇っぽい雰囲気だけ感じてください

Hadoop まわりの運用

Hadoop、hive での運用で気を使っている点や以前困っていた点を紹介します

細かいファイルは集約する

HDFS のデフォルトブロックサイズは 64MB と大きく、小さいファイルをたくさん置くのはあまり効率が良くありません

サイズの小さいものは1日単位などに集約すると結構ブロック数を減らせます

簡単に再集計できる仕組み

再集計をかけるタイミングが必ず存在します。しかも意外に高い頻度で

ジョブスケジューラで簡単にできればそれがベストですが、

なんらかの事情やそんな機能がなかった場合は、再集計するコードを書いておくと幸せになります

hive の json カラムには気をつけろ

hive には json 型があり、スキーマレスっぽい感覚で使えます

しかし使えるからと言って何でもつっこんで、1行なのに画面いっぱいの json で埋まるみたいなことをされるとさすがに hive さんも落ちます気を付けてください

まとめ

  • fluentd 最高
  • Hadoop の自前運用には割と手間暇かかる
  • 分析チームの人がデータをRで好きにいじれる環境を作った
  • プラットフォーム管理画面のスクレイピングは業が深い

次は id:GUSSAN さんです。

-- 追記 --

HDFS 上のファイル集約のくだりですが、ご指摘を頂いたのでほそくすると、

容量を節約したいからではなく、NameNode のメモリが節約できるということです

あと、mapタスクの数を減り mapの起動コストも減ることになります (こっちは失念してました!)

カニでもできるRailsでアクセスログ実装

こんにちはこんにちは

Rails 4 でさくっとアクセスログ出そうと思ったけど、思ったよりさくっといかなかったので実装方法を残しておく

ログの項目

これくらいの簡単な内容

  • 時間
  • ユーザー名
  • リクエストURI
  • USER AGENT

独自ログの設定

まず、Railsのログじゃなくて独自のログを出したかったので、

config/environment.rb

    # Load the Rails application.
    require File.expand_path('../application', __FILE__)
    
    class AccessLogger < ::Logger
      class NoHeaderLogDevice < ::Logger::LogDevice
        def add_log_header(file)
        end
      end
    
      class AccessFormatter < ::Logger::Formatter
        def call(severity, timestamp, progname, msg)
          "#{msg}\n"
        end
      end
    
      def initialize(logdev, shift_age = 0, shift_size = 1048576)
        super(nil, shift_age, shift_size)
    
        @formatter = AccessFormatter.new
    
        if logdev
          @logdev = NoHeaderLogDevice.new(
            logdev,
            :shift_age => shift_age,
            :shift_size => shift_size
          )
        end
      end
    
    end
    
    # Initialize the Rails application.
    Kaniapp::Application.initialize!

解説

ソースの中身を解説すると、

ログファイル作成時に出る邪魔なヘッダー(# Logfile created on ...)を抹殺

  class NoHeaderLogDevice < ::Logger::LogDevice
    def add_log_header(file)
    end
  end

ログフォーマットを指定

  class AccessFormatter < ::Logger::Formatter
    def call(severity, timestamp, progname, msg)
      "#{msg}\n"
    end
  end

config/environments/production.rb に

config.access_logger = AccessLogger.new('log/skv_access.log')

を追加

呼び出し

ApplicationController から

before_filter { access_log if Rails.env.production? }

private
def access_log
  return if session['warden.user.user.key'].nil?
  @user = User.find_by_id(session['warden.user.user.key'][0][0])
  return if @user.nil?
  now = Time.now.to_s.gsub(/ \+0900/,'')
  request_uri = URI.unescape(request.env['REQUEST_URI'])
  remote_ip = request.env['HTTP_X_FORWARDED_FOR'] || request.remote_ip
  user_agent = request.env['HTTP_USER_AGENT']
  row = "#{now}\t#{@user.username}\t#{request_uri}\t#{remote_ip}\t#{user_agent}"
  Kaniapp::Application.config.access_logger.info(row)
end

このやり方で君だけの最強のアクセスログを作ろう!!

devise_ldap_authenticatable でグループ制限したい人生だった (Rails 4)

とあるアプリ (でLDAP認証を実現するために、 cschiewek/devise_ldap_authenticatable · GitHub を使っていて幸せに暮らしていたけど、ある日突然○○グループのみ認証を通したくなったので、設定ファイルをいじったら簡単にできるやろって思ったらできなかったので、ちょこっと頑張ったお話。

devise_ldap_authenticatable でグループ認証したい人向けのお話です

先に結論から

  • config/initializers/devise_ldap_authenticatable_customizer.rb を作成
  • config/ldap.yml を編集

で完了

gemのバージョンたち

net-ldap (0.5.1)
devise (3.1.1)
devise_ldap_authenticatable (0.8.1) # 現時点で rubygems 的に最新のgemのバージョン

モンキーパッチを当てる

config/initializers/devise_ldap_authenticatable_customizer.rb

module DeviseLdapAuthenticatableCustomizer

  begin
    class Railtie < ::Rails::Railtie

      config.after_initialize do

        Devise::LDAP::Connection.class_eval do

          def initialize(params = {})
            ldap_config = YAML.load(ERB.new(File.read(::Devise.ldap_config || "#{Rails.root}/config/ldap.yml")).result)[Rails.env]
            ldap_options = params
            ldap_config["ssl"] = :simple_tls if ldap_config["ssl"] === true
            ldap_options[:encryption] = ldap_config["ssl"].to_sym if ldap_config["ssl"]

            @ldap = Net::LDAP.new(ldap_options)
            @ldap.host = ldap_config["host"]
            @ldap.port = ldap_config["port"]
            @ldap.base = ldap_config["base"]
            @attribute = ldap_config["attribute"]
            @ldap_auth_username_builder = params[:ldap_auth_username_builder]

            @group_base = ldap_config["group_base"]
            @check_group_membership = ldap_config.has_key?("check_group_membership") ? ldap_config["check_group_membership"] : ::Devise.ldap_check_group_membership
            # この行を追加
            @check_group_membership_without_admin = ldap_config.has_key?("check_group_membership_without_admin") ? ldap_config["check_group_membership_without_admin"] : ::Devise.ldap_check_group_membership_without_admin
            @required_groups = ldap_config["required_groups"]
            @required_attributes = ldap_config["require_attribute"]

            @ldap.auth ldap_config["admin_user"], ldap_config["admin_password"] if params[:admin]

            @login = params[:login]
            @password = params[:password]
            @new_password = params[:new_password]
          end

          def in_group?(group_name, group_attribute = LDAP::DEFAULT_GROUP_UNIQUE_MEMBER_LIST_KEY)
            in_group = false

            if @check_group_membership_without_admin
              group_checking_ldap = @ldap
            else
              group_checking_ldap = Connection.admin
            end

            unless ::Devise.ldap_ad_group_check
              group_checking_ldap.search(:base => group_name, :scope => Net::LDAP::SearchScope_BaseObject) do |entry|
                #元のコード
                #if entry[group_attribute].include? dn
                if entry[group_attribute].include? dn.gsub(/uid=([^,]+),.*$/,'\1') 
                  in_group = true
                  DeviseLdapAuthenticatable::Logger.send("User #{dn} IS included in group: #{group_name}")
                end
              end
            else
              # AD optimization - extension will recursively check sub-groups with one query
              # "(memberof:1.2.840.113556.1.4.1941:=group_name)"
              search_result = group_checking_ldap.search(:base => dn,
                                :filter => Net::LDAP::Filter.ex("memberof:1.2.840.113556.1.4.1941", group_name),
                                :scope => Net::LDAP::SearchScope_BaseObject)
              # Will return  the user entry if belongs to group otherwise nothing
              if search_result.length == 1 && search_result[0].dn.eql?(dn)
                in_group = true
                DeviseLdapAuthenticatable::Logger.send("User #{dn} IS included in group: #{group_name}")
              end
            end

            unless in_group
              DeviseLdapAuthenticatable::Logger.send("User #{dn} is not in group: #{group_name}")
            end

            return in_group
          end

        end
      end
    end
  rescue Exception(err_msg)
    puts (" --- error => #{err_msg} --- ")
  end
end

LDAP設定ファイル

config/ldap.yml グループとかLDAPのホストとかは適宜変えてください

## Authorizations
# Uncomment out the merging for each environment that you'd like to include.
# You can also just copy and paste the tree (do not include the "authorizations") to each
# environment if you need something different per enviornment.
authorizations: &AUTHORIZATIONS
  group_base: ou=groups,dc=test,dc=com
  ## Requires config.ldap_check_group_membership in devise.rb be true
  # Can have multiple values, must match all to be authorized
  required_groups:
    # If only a group name is given, membership will be checked against "uniqueMember"
    - cn=admins,ou=groups,dc=test,dc=com
    - cn=users,ou=groups,dc=test,dc=com
    # If an array is given, the first element will be the attribute to check against, the second the group name
    - ["moreMembers", "cn=users,ou=groups,dc=test,dc=com"]
  ## Requires config.ldap_check_attributes in devise.rb to be true
  ## Can have multiple attributes and values, must match all to be authorized
  require_attribute:
    objectClass: inetOrgPerson
    authorizationRole: postsAdmin

## Environment

development:
  host: ldap.network.kani.dc
  port: 389
  attribute: uid
  base: ou=users,dc=kani,dc=co,dc=jp
  ssl: false
  group_base: ou=groups,dc=kani,dc=co,dc=jp
  check_group_membership: true
  check_group_membership_without_admin: true
  required_groups:
    - ["memberuid", "cn=hogegroup,ou=groups,dc=kani,dc=co,dc=jp"]

test:
  host: ldap.network.kani.dc
  port: 389
  attribute: uid
  base: ou=users,dc=kani,dc=co,dc=jp
  ssl: false

production:
  host: ldap.network.kani.dc
  port: 389
  attribute: uid
  base: ou=users,dc=kani,dc=co,dc=jp
  ssl: false
  group_base: ou=groups,dc=kani,dc=co,dc=jp
  check_group_membership: true
  check_group_membership_without_admin: true
  required_groups:
    - ["memberuid", "cn=hogegroup,ou=groups,dc=kani,dc=co,dc=jp"]

解説

LDAP認証の流れ

  1. 個人アカウントでLDAP認証してオブジェクト取得
  2. そのオブジェクトを使ってグループ認証

ざっくりこんな感じ

2 を行う際、ldap.yml に check_group_membership_without_admin がないと、グループ認証する際に、入力されたユーザーではなく admin のアカウントとパスワードでバインドしてしまうので、invalid bind みたいな怒られ方をします。

そんな設定なかった

で、config に書くんですが、rubygemsから落とせる最新版 (0.8.1) にはその設定がなく、githubのmasterブランチ (0.8.3) にはありました。

ただ、認証系のgemをmasterから取ってくるのはイヤだったので、0.8.1にモンキーパッチを当てることにしました。

ポイントは2か所

  1. Devise::LDAP::Connection の initialize で ldap.yml の設定を読んでいたので、check_group_membership_without_admin を読ませる

  2. グループに属しているかチェックしている in_group? メソッドで、uid で認証して欲しかったので、正規表現で切り取って認証

手を加えたのは Devise::LDAP::Connection の中の2行だけ

これでグループ認証があなたのもとへ、やったね!

カニでもわかるWindows 7新規インストール

windows機のHDDが突然お亡くなりになったので、新規HDDにwindows DSP版を入れて若干四苦八苦したところをメモる。

環境

マシン

ThinkPad Edge E420

HDD

壊れたHDD

ST9750420AS

http://www.tigerdirect.com/applications/SearchTools/item-details.asp?EdpNo=6324326

調べたら非AFTらしい。

AFTってなんじゃい

このへんを見ました。

http://www.pc-master.jp/jisaku/aft-hdd.html

なる、ほ、ど。。。

わからんが、AFTのものを買った方が良さそう。それに合わせてwindows7 SP1 適応済みにしたほうがよさげ

新しく買ったHDD

WD7500BPVX

http://shop.tsukumo.co.jp/goods/4515479660908/

これはちょっと調べるのに苦労したけど、AFTっぽい

OS

Microsoft Windows7 Home Premium 64bit Service Pack 1 日本語 DSP版 DVD LCP 【紙パッケージ版】

http://www.amazon.co.jp/gp/product/B00HSC9E78/ref=oh_details_o00_s00_i00?ie=UTF8&psc=1

なんかよくわかんないけど、一番安そうなやつ買いました

インストール

インストール自体は何の問題もなく、ディスクを入れて電源を入れたら勝手にインストール画面になったので、キーボードとか色々設定をポチポチしていくだけ

ここからが本当の地獄だ...

さてとりあえず chrome 入れるかと思って IE 立ち上げたらネットつながんねーわ

ディスプレイも外部ディスプレイ検出しねーわ

Bluetooth ヘッドセットも接続できねーわ etc...

なんか色々できない!

デバイスドライバを見ると、とりあえずネットワークアダプターねーじゃんよ

まずはインターネットから

ぐぐってたらレノボのサポートサイトから必要なデバイスドライバーを入れてみることに

http://support.lenovo.com/ja_JP/research/hints-or-tips/detail.page?DocID=HT037710

幸いメインPCはMacさんなのでそっちからネットワークドライバーさんを落として、USBメモリからwin機に入れたらインターネットできました、素敵!!

ディスプレイ

ディスプレイもドライバー入れたら検出してくれました

最後の難関 Bluetooth ヘッドセット

これはほぼ自分用

MW600 を使ってるんですが、PCと接続しようとするとデバイス認証するところまではできた。

でも接続はできない

なぜだ

ぐぐったり、製品のQ&Aを見に行ったりして悶絶してたら、電源ボタン+電話マークのボタン (何て言うんだこれ) 同時長押しで何かがリセットされたっぽくて、接続できるようになりました。

というわけで、ちょっと悶絶しましたが無事 windows 使えるようになりました

カニでもわかるRails 3 (nginx + unicorn) 開発環境構築

今まで Rails 1系だけ触った事があったのですが、Rails 3系をやる機会があったので、開発環境構築手順をまとめました。

手順として、

  • rvm インストール
  • Rails インストール
  • unironのインストールと設定 (nginxとの連携)
  • nginx インストールと設定 (unicornとの連携)

という感じでいきます。

rvm インストール

ちなみにサーバーは Debian (squeeze) です。
今回は開発環境なので、自分だけ使う前提で自分のアカウントでrvmをインストールします。

curl -L get.rvm.io | bash -s stable
# パスを通す
source ~/.rvm/scripts/rvm
# ruby インストール
rvm install 1.9.3
# gemset 作成 
rvm gemset create kanipan_rails
# kanipan_rails gemset を使う
rvm gemset use kanipan_rails

gemset use してgem installすると、

/home/kanipan/.rvm/gems/ruby-(version)@(gemset名)/gems/

以下にgemが入ります。

Rails インストール

今回は最新版 (3.2) をインストールします。

gem install rails
#バージョン確認
rails -v
Rails 3.2.11
# rails プロジェクト作成 databaseをmysqlに指定
rails new kanipan_project --database=mysql

unicornのインストールと設定

vim Gemfile

Gemfileに以下を追加

gem 'unicorn'

Gemfile保存後、

bundle install

# unicornの設定ファイル作成
vim config/unicorn.rb

サンプルとして

listen "/tmp/unicorn.sock"
pid "/tmp/unicorn.pid"

worker_processes 1
preload_app true

if ENV['RAILS_ENV'] == 'production'
  shared_path = "/var/www/shared"
  stderr_path = "#{shared_path}/log/unicorn.stderr.log"
  stdout_path = "#{shared_path}/log/unicorn.stdout.log"
end

# ログ
stderr_path File.expand_path('log/unicorn.log', ENV['RAILS_ROOT'])
stdout_path File.expand_path('log/unicorn.log', ENV['RAILS_ROOT'])

preload_app true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!

  old_pid = "#{ server.config[:pid] }.oldbin"
  unless old_pid == server.pid
    begin
      Process.kill :QUIT, File.read(old_pid).to_i
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

unicornを起動します。

# -D : デーモンとして動かす, -E : Railsの環境, -c : unicorn設定ファイルのパス
unicorn_rails -D -E development -c config/unicorn.rb

unicornはmasterプロセスと複数のwokerプロセスから成り立っています。 unicornを停止するにはmasterのプロセスを止めます。

pidはunicorn.rbにパスを指定しているので、

kill -9 `cat /tmp/unicorn.pid`

nginx インストールと設定

sudo apt-get install nginx
sudo vim /etc/nginx/ngin
# kanipan_project用の設定ファイル
sudo vim /etc/nginx/sites-available/kanipan_project

以下サンプル

# statements for each of your virtual hosts
upstream kanipan_project{
  server unix:/tmp/unicorn.sock;
}

server {
  listen 80;
  server_name localhost;

  access_log /var/log/nginx/kanipan_project/access.log combined2;
  error_log /var/log/nginx/kanipan_project/error.log warn;

  root /var/www/kanipan_project/current/public;

  #rails params用
  proxy_buffer_size          128k;
  proxy_buffers            4 256k;
  proxy_busy_buffers_size    256k;
  proxy_temp_file_write_size 256k;

  location / {

     proxy_pass http://kanipan_project;

}

kanipan_project用設定へのシンボリックリンクを sites-enabled以下に置きます。

sudo ln -s /etc/nginx/sites-available/kanipan_project /etc/nginx/sites-enabled/kanipan_project

ドキュメントルートにkanipan_projectのpublicフォルダへのシンボリックリンクを置きます。

# /var/wwwにシンボリックリンクをはる
sudo ln -s kanipan_project/public /var/www/current/kanipan_project

これで準備完了!