株式会社クリアコード > ククログ

ククログ


Dracutの歩き方

最近、Dracutを使ってinitrdをカスタマイズしたのでその時のことをまとめます。

DracutはRed Hat系のディストリビューションで使われているinitrdをカスタマイズするためのツールです。

バージョンは041を対象とします。

ブートプロセス

本題ではないのですが、簡単に説明しておきます。

  • GRUBなどのブートローダが起動する
  • ブートローダがカーネルを読み込み。initrdを仮のrootfsとして起動する
  • その後、本物のrootfsをマウントして、本物のinitを起動する

ここ5年くらいのLinuxだと、こんな感じです。 initrdを作るツールがDracutだったり、initramfs-toolsだったり、本物のinitがsystemdだったり、シェルスクリプトだったりします。

initrd の展開方法

まずはinitrdの中身を確認する方法を紹介します。自分が作ったinitrdが意図通りできているかどうか確認したり、他の人が作ったinitrdを展開して確認したりするために必要です。

ほとんどの場合はgzipで圧縮されたcpioアーカイブなので以下のコマンドでカレントディレクトリに展開することができます。

$ mkdir -p /tmp/initrd
$ cd /tmp/initrd
$ zcat /boot/initrd.img | cpio -idmv

展開すると以下のようなディレクトリ構成になっています。 initrdは仮のrootfsなので、見覚えのある感じではないでしょうか。

$ tree -F -L 1
.
├── bin -> usr/bin/
├── dev/
├── etc/
├── init -> /usr/lib/systemd/systemd
├── lib -> usr/lib/
├── lib64 -> usr/lib64/
├── proc/
├── root/
├── run/
├── sbin -> usr/sbin/
├── shutdown*
├── sys/
├── sysroot/
├── tmp/
├── usr/
└── var/

14 directories, 2 files

Red Hat系ではinitにsystemdを採用しているのでinitが/usr/lib/systemd/systemdへのシンボリックリンクになっています。

このうちDracutに関係するファイルはusr/lib/dracut/以下にまとまっています。

initrdのカスタマイズ方法

initrdをカスタマイズする目的は、いくつかあります。

  • ISOイメージからrootfsをマウントしたい -> LiveDVD や LiveCD
  • ネットワークからrootfsをマウントしたい -> ディスクレスブート、シンクライアント
  • 暗号化されたディスクを復号してrootfsとしてマウントしたい
  • OSの起動前に、OSのバージョンチェックをしたい -> キオスク端末の自動更新

Dracutは、シェルスクリプトをモジュール化して管理しています。 initrdの中身では、Dracut関連のファイルはusr/lib/dracut/以下にあり、フックごとにまとまっているためどのモジュールのファイルなのかわかり辛くなっています。

フックの種類は以下の通りです。

  • cmdline
    • カーネルのブートオプションをパースするとき
  • pre-udev
    • udev を初期化する前にudevのルールをここで生成する
  • pre-trigger
    • デバイスの初期化をすることが多い
  • initqueue
    • 時間のかかる処理をキューに登録する
  • pre-mount
    • マウント前に必要な処理をする
  • mount
    • ファイルシステムをマウントする
  • pre-pivot
    • switch_root する前に実行する
  • cleanup
    • 本物の init を実行する前に実行される
Dracutのモジュールの作り方

Dracutのモジュールを自作すれば、上記のフックを利用して目的に合うrootfsを作ることができます。

Dracutのモジュールは作るだけなら簡単です。

dracutコマンドを実行するホスト上の/usr/lib/dracut/以下に所定のファイルを置くだけです。 例えば、LiveISOを起動するためのモジュールは以下のようになっています。

$ tree /usr/lib/dracut/modules.d/90dmsquash-live/
/usr/lib/dracut/modules.d/90dmsquash-live/
├── apply-live-updates.sh
├── checkisomd5@.service
├── dmsquash-live-genrules.sh
├── dmsquash-live-root.sh
├── dmsquash-liveiso-genrules.sh
├── iso-scan.sh
├── module-setup.sh
├── parse-dmsquash-live.sh
└── parse-iso-scan.sh

このうち必須のファイルは module-setup.sh のみです。 module-setup.sh にモジュールをどのようにインストールするかをルールに従って記述します。

以下の4つの関数を定義すると、dracutコマンドから自動的に実行されます。

  • install()
    • コマンド、スクリプト、フック等をinitrdに組込む
  • installkernel()
    • initrdに組込むカーネルモジュールを指定する
  • depends()
    • このモジュールが依存している他のモジュールを指定する
  • check()
    • initrdにこのモジュールがインストール可能かどうかチェックする

詳細はREADME.modulesを参照してください。

モジュールを所定のパスに置いたら、設定ファイルを/etc/dracut.conf.d/以下に置きます。

filesystems+="vfat msdos isofs ext4 xfs btrfs "
add_dracutmodules+=" my-module"
add_drivers+=" sr_mod sd_mod ide-cd cdrom ehci_hcd uhci_hcd ohci_hcd usb_storage usbhid uas "
hostonly="no"
dracut_rescue_image="no"

設定ファイルを用意したらdracutコマンドでinitrdを生成します。

# dracut /boot/initramfs-$(uname -r).img

まとめ

Dracutの使い方を簡単に紹介しました。

Dracutのモジュールを自作すれば、目的に合ったinitrdを作ることができますが、実際に作るためには様々な知識が必要になります。 例えば、FedoraのLiveISOを起動できるようなDracutのモジュールを自作することを考えると、次のような知識が必要となります。

  • device-mapper
  • squashfs
  • loop back device
  • udev rules
  • plymouth
  • SELinux

Dracutのリポジトリに含まれているドキュメントやモジュールを読むだけでも、かなり色々なことがわかるのでDracutのモジュールを自作する必要があるときは、Dracutのソースコードを読むことをおすすめします。

2015-06-02

Fcitxで「直接入力」する入力メソッドを簡単に切り替える方法

はじめに

インプットメソッドフレームワークの一つに Fcitx *1 があります。 複数の入力メソッドをあらかじめ設定しておくことができますが、入力メソッド自体を切り替えるのに比べて、入力メソッドの順番を変更するのはちょっと面倒です。 今回は Fcitx で「直接入力」する入力メソッドを簡単に切り替える方法を紹介します。

直接入力、切り替えていますか?

そもそも、「直接入力」って切り替えたりするものなのでしょうか。 通常、一度キーボードレイアウトとそれに対応する日本語のための入力メソッドを設定してしまえば、その後めったに変更することはありません。

切り替えが必要になるのは、レイアウトの異なるキーボードを接続して使う場合です。 典型的なのは、ノートPCにUSBキーボードを接続して使うといったケースでしょう。

例えば、ノートPCは日本語キーボードで、それに接続して使うUSBキーボードが英語キーボードだと、そのままではレイアウトの違いから打てない文字がでてしまい問題になります。 *2

そのため、fcitx-configtool を使って「直接入力」に用いるレイアウトを日本語から英語に変更します。

英語-Mozc-日本語キーボード

あるいは、Fcitx のアドオンとして提供されている Input method selector を利用して、グローバル入力メソッドを英語に変更するという回避策もあります。

ただし、その方法には直接入力を再設定する場合と違って落し穴があります。

  • 直接入力を再設定する場合

    • fcitx-configtool による変更が必要
    • 日本語入力(Mozc)に影響を与えない
  • グローバル入力メソッドを変更する場合

    • ホットキーで簡単に切り替えられる
    • 英語入力だけなら問題なし
    • 日本語入力(Mozc)に影響する

上記の日本語入力(Mozc)に影響するとはどういうことでしょうか。 これは、Mozcがグローバル入力メソッド関係なしに、「既定の入力メソッド」つまり「直接入力」として設定されている入力メソッドを利用することによります。

したがって、グローバル入力メソッドを変更する場合だと、英語キーボードを使っているのに、日本語入力にはなぜか日本語キーボードレイアウトで入力しているという中途半端なことになります。 また、その際には、ローカル入力メソッドをMozcに設定しておかないと、入力メソッドのオンオフが正しく機能しません。 *3

英語キーボードの接続のたびに、fcitx-configtool でいちいち設定を変更するのは面倒です。 そこで次に紹介するのが、fcitx-imlist です。

fcitx-imlistとは

fcitx-configtool と異なり、コマンドラインで入力メソッドの設定を変更するためのツールです。

パッケージは提供していないので、ソースコードをビルドする必要があります。 ビルドのために、Fcitxの開発用のヘッダファイルが必要なので、そのためのパッケージを事前にインストールします。 *4

% sudo apt-get install build-essential fcitx-libs-dev

インストールできたら、次に fcitx-imlist をビルドします。

% git clone https://github.com/kenhys/fcitx-imlist
% cd fcitx-imlist
% ./autogen.sh
% ./configure
% make
% sudo make install

これで、 /usr/local/bin/fcitx-imlist としてコマンドをインストールできました。

fcitx-imlist はキーボードのユニーク名をもとに、入力メソッドを指定順に変更できます。 キーボードごとのユニーク名については -l オプションを指定することで取得できます。

% fcitx-imlist -l
fcitx-keyboard-jp (キーボード - 日本語) [enabled]
mozc (Mozc) [enabled]
fcitx-keyboard-us (キーボード - 英語 (US)) [enabled]
入力メソッド ユニーク名
キーボード - 日本語 fcitx-keyboard-jp
Mozc mozc
キーボード - 英語(US) fcitx-keyboard-us

日本語キーボードのかわりに、英語キーボードを直接入力のために使うには、 -s オプションにユニーク名を指定します。 *5

% fcitx-imlist -s us
fcitx-keyboard-us (キーボード - 英語 (US)) [enabled]
fcitx-keyboard-jp (キーボード - 日本語) [enabled]
mozc (Mozc) [enabled]

これで、キーボードに対応したレイアウトを「直接入力」として正しく設定しなおすことができました。

Fcitx 5.xではどうなる?

ちなみに、Fcitxの次のメジャーバージョンである Fcitx 5.x *6 では、このあたりが改善されて入力メソッドのグループ化がサポートされる見込み *7 です。

そのため、使用するキーボードに応じてあらかじめ設定したグループを切り替えて使うことになるでしょう。また、グループ自体の切り替えもハードウェアを認識して自動的に行えるようになるらしいです。つまり以下のように、英語用キーボード用のグループと日本語キーボード用の設定をしておけば、あとはいい感じに自動で切り替えてくれるということですね。

グループ インプットメソッド
英語キーボード用 fcitx-keyboard-us,mozc
日本語キーボード用 fcitx-keyboard-jp,mozc

まとめ

Fcitx で「直接入力」に使用する入力メソッドを簡単に切り替える方法を紹介しました。Fcitx でレイアウトの異なるキーボードを切り替えて使っている人は参考にしてみてください。

*1 https://fcitx-im.org/wiki/Fcitx

*2 例えば、「_」 (アンダースコア)とか。

*3 Mozcの場合はそうなっている。他の入力メソッドは未確認。

*4 CentOSやFedoraの場合はfcitx-develをインストールします。

*5 「fcitx-keyboard-」については省略できる。

*6 ちなみに、5.xはまだドラフトの段階です。

*7 具体的には、fcitx-ngリポジトリの https://github.com/fcitx/fcitx-ng/blob/master/src/lib/fcitx/ime.h#L54 のあたりのコメント参照。

2015-06-06

SEゼミ2015 - リーダブルコード勉強会を開催

2015-06-06にプログラミングが好きな学生のためのリーダブルコード勉強会を開催しました。この勉強会について、内容を作った立場からどうしてこのような内容にしたのかについて紹介します。また、今回の内容の課題と今後の解決案についてもまとめます。

基本的な流れは昨年開催したリーダブルコード勉強会と同じで次の通りです。

  1. 概要説明(「既存のコードからリーダブルなコードを発見する」を体験するよ!)
  2. 課題実装開始(各自リーダブルなコードで書く)
  3. コードチェンジ
  4. チェンジしたコードをベースに継続して課題を実装(リーダブルなコードを探しながらリーダブルなコードで書く)

コードをチェンジ(交換)して実装を継続することで「強制的に他の人の書いたコードを読む機会を作る」ことがポイントです。

詳細な流れに興味のある方はGitHubのclear-code/sezemi-2015の中に資料があるので、自由に活用してください。当日は6種類のスライドを使いましたが、それらへのリンクも含んでいます。資料もスライドもライセンスはCreative Commons BY-SA 4.0で、著作者は「株式会社クリアコード」です。

今年と昨年の違い

今年と昨年で基本的な部分は変わっていませんが、大きく変わったことがあります。それは次のことです。

  • メンターが増えた
  • テーマがOSSの開発になった
メンターが増えた

昨年はメンター5名で学生30名くらいでやっていましたが、今年はメンター20名超で学生50名くらいでやりました。メンターは単に数が増えただけではありません。優秀度合いは去年同様高いままです。メンターのリストを見れば「学生うらやましい!社会人だけど参加したかった!」と思うことでしょう。

メンターが増えたため、メンターそれぞれにどう振る舞って欲しいかを具体的に伝えることができなくなりました。そのため、今年は方針を共有して後はいい感じに振る舞ってもらうことにしました。共有した方針は「学生さんに新しい視点を与えること」です。答えを教えるのではなく、一緒に考えて、新しい視点を与えて欲しい、とお願いしました。

たとえば、ほとんどの学生さんは「他の人のコードからリーダブルなコードを探して自分のコードに取り入れる」ということを初めて経験します。新しいことに拒否反応を示したり、やることはわかってもうまくできないかもしれません。そんなときは、学生さんから今の気持ちを聞いて考えを整理することを手伝ったり、見方をこう変えてみるとなにか見えてこないか示唆を与えたりして欲しいということです。

難しいところもいろいろあったでしょうが、メンターのフォローにはとても助けられました。

たとえば、コードを読む習慣がない人の場合、プログラミングがうまければうまいほど、他の人のコードからうまくリーダブルなコードを見つけることができません。これは、「自分が書いている書き方がよい」という意識が強くなっているからです。こうなっていると、よほど自分よりうまく書かれていない限りリーダブルなコードを見つけられません。リーダブルなコードだと認識できない、と表現したほうが近いかもしれません。

このようなリーダブルなコードを見つけにくい状態になっている場合のフォローは難しいものです。「このコード、自分もやっているリーダブルな書き方じゃない?」とか、「違う見方で考えるとリーダブルだといえない?」というように、「すぐそばにある野生のリーダブルコード」に気づくきっかけを与えてくれたことでしょう。

リーダブルコードは読む人にとってわかりやすいコードなので、「キラキラしたリーダブルコード」を探そうと思うとかえって見つけられません。リーダブルコードはキラキラしていません。地味です。特別に意識せずともわかってしまうようなコードですから。そのようなコードを見つけて自分のプログラミングに取り入れるためには、「すぐそばにある野生のリーダブルコード」に気づける視点が必要なのです。

今回の勉強会では、学生さんには「すぐそばにある野生のリーダブルコード」に気づけるようになって欲しいと期待していました。そのため、メンターのみなさんにも「新しい視点を与える」という振る舞いをお願いしました。

テーマがOSSの開発になった

昨年のSEゼミのテーマは「インターンシップの準備」というテーマ(たぶん)で、リーダブルコード勉強会は「リーダブルコードを押さえておこう」ということで開催しました。

一方、今年のSEゼミのテーマは「OSSの開発に参加しよう!」で、リーダブルコード勉強会は「OSSの開発にリーダブルコードの知識があるとよい」ということで開催しました。

これを受けて、導入での説明も変えました。「他の人のコードを読んでよいところを取り込んで自分のコードに活かす」というのはOSSの開発では当たり前のこと。リーダブルコードを書けるようになるためにも同じやり方が使えるんだから、OSSの開発に参加するならこのやり方でいこう。という方向にしました。

これからOSSの開発に参加する中で、「他の人のコードから学ぶ」というやり方を当たり前の習慣として身につけてくれることを期待して説明を変えました。

今年から始めたこと

今年から「メビュー」を始めました。「レビュー」と似ていますが違います。

「メビュー」は「Mentor's View」の略で「メンターの視点」という気持ちです。

レビューは「問題がないか」という視点でコードを読みますが、メビューは「メンターの視点を共有する」という視点でコードを読みます。

自分だとよいと思っていなかったコードでも、メンターから見ればよいコードだと教えてもらえるかもしれません。リーダブルコードの解説にも次のように書いていますが、最初はヒントなしでよいコードを見つけることは難しいです。熟練したメンターの視点をもらって自分の「よいコードを見つける視点」を養います。それがメビューです。

実際にやるとぶつかること

まず、内容を活かす場所がぜんぜんないって思うだろう。でも、それはあなたの書いたコードがすでにリーダブルだからじゃない。単に「気づかない」からだ。本書を読んでいるときは次々と出てくる小さなコードを読みながら「たしかにここはそう変えた方がいいな。ふむふむ。」なんて読んでいただろう。読みながらあなたが「ふむふむ」言っていられたのは著者がわかりやすく書いてくれていたからだ。でも、実際のコードにはそういうヒントはない。著者のヒントがないんだから、実際のコードを読んでいるときは見逃してしまうだろう。それが自分が書いている真っ最中のコードならなおさらだ。読む側の視点じゃなくて書く側の視点でコードを見ているんだから。

課題と解決案

学生さんにはアンケートを書いてもらい、メンターの何人かには直接感想を聞きました。

アンケートを見る限り多くの学生さんにはよい機会となったようです。この勉強会をきっかけにOSSのコードを読んでいこうという気になった人もたくさんいました。

メンターの人の何人かには「楽しかった」と言ってもらえました。楽しくフォローしていたのなら、学生さんにもいろいろ伝わりやすかったことでしょう。

一方、次の点では課題がありました。

  • 最初の開発環境準備
  • プログラミング言語と課題のミスマッチ
  • 交換対象のプログラムの選び方

昨年もそうでしたが、何人かは開発を始める前のところでつまづきます。不安な人は30分くらい早めに会場に来てもらって、勉強会前に開発を始める準備のところを手伝ってあげるのがよいかもしれません。

この勉強会で実装する課題はファイルの読み込み処理があります。開いたファイルの管理方法、ファイルをどこに配置するか、などで工夫ポイントがでるかもしれないと考えて入れた処理です。しかし、SwiftやWebブラウザー上で動作するJavaScriptなどファイル操作をすることがまれなプログラミング言語では実装の難易度をあげてしまいます。これは意図したことではないのでファイルを使うこと自体やめた方がよさそうです。

途中でコードを交換することはこの勉強会での大事なところですが、交換する相手の実装がほとんど進んでいない場合は読む機会が少なくなってしまいます。その場合はあまり進んでいない人のコードは交換対象とせずに、ある程度進んでいる人のコードを2人以上で使うなどの交換の仕方の方がよさそうです。

他にも、課題から自由度を減らして実装に集中しやすくする、メモのまとめ場所をGitHubのIssueにしてメモをまとめるときの敷居をさげる、といったことも検討しています。

まとめ

昨年に引き続き開催したSEゼミのリーダブルコード勉強会の内容について、昨年の内容と比べながら紹介しました。また、実際に開催したわかった課題とその解決策もまとめました。

今年のSEゼミのイベントはまだ3つあります。引き続き優秀なメンターが多数参加するので、OSSの開発に参加したそうな学生さんに教えてあげてください。

今回の勉強会に参加した学生さん・メンターの感想に興味のある方は次のWebページを見てみてください。

今回の勉強会での成果に興味のある方は次のWebページを見てみてください。

2015-06-08

ConoHa上にDroongaクラスタを設置する手順

Droongaプロジェクトの結城です。

実際に運用中のDroongaクラスタ

Droongaプロジェクトは現在の所「レプリケーション機能があるGroonga互換の全文検索システム」を当座の目標としていますが、目標達成のためには不足している機能の実装だけでなく、「安定して動作する」という運用実績を作る必要もあります。そこで、デモンストレーションも兼ねて、GroongaのパッケージサーバがあるConoHaのクラウド上でDroongaクラスタを運用してみています。

このDroongaクラスタの全体像は、以下の図の通りです。

(運用中のDroongaクラスタの構成)

実際にhttp://157.7.124.247:80/にアクセスすると、Groonga AdminによるUIを経由して、動作中のDroongaクラスタのデータベースの内容を見られます。

このクラスタには現在の所、以下のデータが継続的に格納されています。

図ではパッケージサーバのログも収集するように描かれていますが、実は今の所はまだ、DroongaクラスタのHTTPサーバ自身のログのみ収集しています。

Droongaは複数ノード構成での運用が前提なので、当然ながら何台ものサーバが必要になります。 そんな時、物理的なサーバよりも簡単に調達でき、自前の仮想マシンよりもはるかに実用的なのが、ConoHaのようなクラウド形式のVPSサービスです。

このエントリでは、上記の運用実験用クラスタの構築手順を振り返りながら、ConoHa上にDroongaクラスタを構築する具体的な手順をご紹介します。同様の構成でDroongaを試してみる際の参考にしてください。

前提条件と完了条件

今回やりたかったこととやりたくなかったことは、以下の通りです。

  • ConoHaのクラウド上に既に運用しているサーバがあり、そのログを収集してDroongaクラスタに格納させたい。 (※今回の場合はGroongaのパッケージサーバです。が、実際にはこれはまだやっていません……)
  • 将来的にはその他にも、継続的に情報を収集してDroongaクラスタに格納させたい。
  • データの書き込みは信用できる相手だけに許可して、それ以外のユーザにはリードオンリーで利用させたい。

これらを実現するために、次のような方針を定めました。

  • 基本的な通信はローカルネットワーク上で完結させる。(速度、セキュリティ、利便性のため)
  • リバースプロキシを使い、一部の機能だけをインターネット経由で利用できるように公開する。(セキュリティを保ちつつ、デモンストレーションとして活用するため)
  • ログの収集にはFluentdを使う。(別のサーバのログを簡単に収集するため)

実際の作業は、以下の要領で進めます。

  1. ローカルネットワークを作成する。
  2. Droongaノード用のVPSを作成する。
  3. リバースプロキシを設定する。
  4. Fluentdを設定する。

順番に見ていきましょう。

ローカルネットワークの作成

ConoHaのVPSは最初からグローバルIPアドレスが割り当てられており、そのままWebサービスを公開できるようになっています。 しかし、GroongaもDroongaもHTTPのGETメソッドでデータベースの内容を変更できてしまうので、グローバルIPでの運用はお薦めできません。

幸い、ConoHaでは無料でVPSをローカルネットワークに所属させられますので、DroongaのサービスはローカルネットワークのIPアドレスで起動しておいて、サーバ同士はこのローカルネットワーク内で通信するようにすれば、Droongaクラスタを安全に運用できます。 また、ConoHaではローカルネットワークの方が高速なので、ノード間の通信が多いDroongaでは性能的なメリットも大きいです。

今回の構成では、手順に従って192.168.0.0/24のプライベートネットワークを作成しました。 同時に既存のパッケージサーバも、ネットワークインターフェースを追加してこのプライベートネットワークに参加させるようにしました。 今後新たにVPSを作成した場合も、Droongaクラスタと連携する必要があればこのネットワークに参加させることになります。

Droongaノードに使うVPSの準備

OSの入れ換え

ConoHaでVPSを作成する時は、テンプレートイメージとして「汎用のCentOS 6.5」と「CentOS 6.5にWordPressとnginxをインストールした状態」のどちらかを選べます。 しかしDroongaは現在の所、CentOSについてはCentOS 7へのインストールにのみ対応しており、CentOS 6.5にインストールするのは骨が折れます。

なので、今回はVPSを作成した後で、OSを自分で入れ替えて使うことにしました。

このような使い方をする場合、VPSの作成時のテンプレートイメージはどちらを選んでも構いません。 また、VPS作成時に入力するrootパスワードは最終的には使わないので、パスワード欄には適当な文字列を入力しておいて大丈夫です。

VPSが作成されたら、すぐにシャットダウンします。 その後、手順に従ってネットワークインターフェースを追加して192.168.0.0/24のローカルネットワークに参加させます。

テンプレートイメージをそのまま使う場合はこの後VPSを起動しますが、OSを入れ替える場合はここから先の手順が違います。 今回は、ディスクイメージからのインストールでOSをUbuntu 14.04LTSにしてみました。手順は以下の通りです。

  1. Ubuntu 14.04LTS 64bitのディスクイメージを挿入して、VPSの電源を入れる。
  2. 仮想コンソールでVPSに接続し、Ubuntuをインストールする。
    • インストール中に使うネットワークインターフェースはeth0(インターネットに繋がっているインターフェース)を選択する。
    • ホスト名はdroonga0とする。
    • インストール先のパーティションは、Guided - use entire diskを選択して、/dev/vdbを選ぶ。(容量が大きい方)
    • インストールオプションは以下の2つにチェックを入れる。
      • Basic Ubuntu server
      • OpenSSH server
    • 初期ユーザ名、パスワードはお好みで。
  3. インストールが完了したら、VPSの電源を落とす。
  4. ディスクイメージを排出する。
  5. 再度VPSの電源を入れる。

……という手順でUbuntuをインストールしたのですが、その後になってから、インストールイメージを使う方法があるということに気が付きました。 使いたいディストリビューションのバージョンのインストールイメージが提供されているのなら、そちらの方を使ったほうが圧倒的に簡単なのは間違いないので、これはあくまで、インストールイメージが無い場合の進め方という事にしておきます。

Droongaノード用VPSの1台目ができたら、2台目も作成します。 設定やOS入れ替えの手順はほぼ同じですが、今度はホスト名をdroonga1にします。

VPSを2台用意できたら、分かりやすいようにConoHaの管理コンソール上での表示ラベルもdroonga0droonga1に変更しておきます。

鍵認証の設定

OS入れ換え直後のVPSはSSHを使ってパスワード認証でリモートログインできる状態になっていますが、これはセキュリティ的に脆弱ですので、より安全な公開鍵認証を強制するように設定しておきます。

公開鍵の登録にはssh-copy-idコマンドを使います。 手元のPC(Ubuntu)で、以下の要領で入力します。

$ ssh-copy-id -i 公開鍵へのパス ノードのユーザ名@ノードのIPアドレス

今回のDroongaノード用VPS(droonga0)なら、以下のようになります。

piro@localpc$ ssh-copy-id -i .ssh/id_rsa.pub piro@157.7.124.247

これで鍵認証でSSH接続できるようになったので、早速手元のPCのGNOME端末からVPSに接続してみます。

piro@localpc$ ssh piro@157.7.124.247
piro@droonga0$ 

以後の操作は手元のPCから行うということで、ConoHaの仮想コンソールは閉じてしまいます。

SSHデーモンの設定変更

droonga0droonga1/etc/ssh/sshd_configを変更して、SSHを安全に運用するための定番の設定を行う事にします。

  • Port 22Port (適当な空きポート番号)に書き換えて、SSH接続に使うポート番号を変更します。 これはセキュリティ的な対策というよりも、外部から22番ポート決め打ちでアタックされた時にログが大量に記録されてしまってウザくないように、という意味合いが主です。
  • PermitRootLogin without-passwordPermitRootLogin noに書き換えて、リモートからの直接のrootログインを禁止します。 Ubuntuの場合はそもそも普通に操作しているとrootログインする場面はありませんが、念のためです。
  • PasswordAuthentication noを追加します。 これにより、パスワード認証でのリモートログインが禁止されます。

設定を編集したら、sshdを再起動します。

$ sudo service ssh restart

次に、新しいポート番号で鍵認証によって接続できる事を確認します。 GNOME端末上の現在の接続はそのままにして、GNOME端末の新しいタブを開いてからそちらで接続します。 例えば変更後のポート番号が2222なら、以下の要領です。

piro@localpc$ ssh piro@157.7.124.247 -p 2222
piro@droonga0$ 

設定を間違えてしまっていてうまくいかない場合は、元のタブの方の接続で設定を修正します。 このように、リモートからのログインに関係する重要な設定を変更する時は、元のセッションを保持したまま作業するとトラブル対応を楽にできます。 元のセッションが切断されてしまった場合は、面倒ですがConoHaの仮想コンソールから操作するほかありません。

固定IPの設定

この時点では、ローカルネットワーク用のインターフェースであるeth1にはIPアドレスが割り当てられていません。 ConoHaのローカルネットワークには初期状態ではDHCPサーバがいないからなのですが、そもそもDroongaクラスタのような物を運用する場合はIPアドレスがコロコロ変わる方が面倒なので、ここはDHCPサーバを立てずに固定IPを自分で設定することにしました。

droonga0droonga1/etc/network/interfacesに、以下の要領でeth1の設定を追加します。

auto eth1
iface eth1 inet static
address 192.168.0.50
netmask 255.255.255.0
network 192.168.0.0
broadcast 192.168.0.255
  • addressの値はノードごとに変えます。 droonga0192.168.0.50droonga1192.168.0.51としました。 ちなみに、ConoHaの仕様により、IPアドレスは11254の範囲で設定する必要があります。
  • デフォルトゲートウェイは設定しません。

ネットワークインターフェースの設定ができたら、インターフェースeth1を有効化します。

$ sudo ifup eth1

Ubuntuならsudo service networking restartなんじゃないの……?と思われるかも知れませんが、Ubuntu 14.04LTSでは既知の不具合のため、これはエラーになってしまいます。 なので、ifupコマンドを直接使っています。

ともかくこれで、Droongaノード用のVPS同士は互いに192.168.0.50192.168.0.51というIPアドレスで通信できるようになりました。 便利なように、それぞれの/etc/hostsに以下の行を追加(および、127.0.0.1への自身のホスト名の割り当てを削除)して、互いに名前で参照できるようにしておきます。

192.168.0.50 droonga0
192.168.0.51 droonga1
Droonga Engineのインストール

ここまで来たら、後はDroongaのチュートリアルにある通りの手順でDroongaノードとしてVPSを設定するだけです。

droonga0$ curl https://raw.githubusercontent.com/droonga/droonga-engine/master/install.sh | \
            sudo HOST=droonga0 bash

HOSTの指定はノードごとに変えます。

Droonga Engineがインストールされたら、片方をもう片方にjoinさせて、クラスタを構築します。 droonga1droonga0のクラスタに参加させるなら、以下の通りです。

droonga0$ droonga-engine-join --host droonga1 --replica-source-host droonga0
Start to join a new node droonga1
       to the cluster of droonga0
                     via droonga0 (this host)
    port    = 10031
    tag     = droonga
    dataset = Default

Source Cluster ID: 88260971556bc9203087d476c0566c9da0114695

Changing role of the joining node...
Configuring the joining node as a new replica for the cluster...
Registering new node to existing nodes...
Changing role of the source node...
Getting the timestamp of the last processed message in the source node...
The timestamp of the last processed message at the source node: 2015-05-13T05:04:20.317923Z
Setting new node to ignore messages older than the timestamp...
Copying data from the source node...
0% done (maybe 00:00:00 remaining)
Restoring role of the source node...
Restoring role of the joining node...
Done.

これで、2ノードからなるDroongaクラスタができました。

安全なクラスタである事の確認

このDroongaクラスタが本当に外部から接続できない安全なクラスタかどうかを、念のため確認しておきます。

チュートリアルの通りにスキーマを定義した状態から、以下のコマンドを実行して、ローカルネットワークのIPアドレス宛にレコードの追加と検索のリクエストを送ってみます。

droonga0$ echo '{"type":"add","body":{"table":"Store","key":"dummy-store0","values":{"name":"Dummy Store 0"}}}' | \
            droonga-send --server droonga:192.168.0.50:10031/droonga

droonga0$ droonga-groonga select --table Store --limit -1 --output_columns name --pretty
[
  [
    0,
    1431500987.197475,
    1.1444091796875e-05
  ],
  [
    [
      [
        1
      ],
      [
        [
          "name",
          "ShortText"
        ]
      ],
      [
        "Dummy Store 0"
      ]
    ]
  ]
]

selectの結果を見ると、ちゃんとレコードが追加されている事が分かります。

今度は、ローカルネットワーク以外のIPアドレス宛にレコードの追加と検索のリクエストを送ってみます。

droonga0$ echo '{"type":"add","body":{"table":"Store","key":"dummy-store2","values":{"name":"Dummy Store 1"}}}' | \
            droonga-send --server droonga:localhost:10031/droonga
E, [2015-05-13T14:24:01.896664 #14333] ERROR -- : Failed to connect fluentd: Connection refused - connect(2)
E, [2015-05-13T14:24:01.896899 #14333] ERROR -- : Connection will be retried.
E, [2015-05-13T14:24:01.908777 #14333] ERROR -- : FluentLogger: Can't send logs to localhost:10031: Connection refused - connect(2)

droonga0$ echo '{"type":"add","body":{"table":"Store","key":"dummy-store2","values":{"name":"Dummy Store 1"}}}' | \
            droonga-send --server droonga:127.0.0.1:10031/droonga
E, [2015-05-13T14:24:01.896664 #14333] ERROR -- : Failed to connect fluentd: Connection refused - connect(2)
E, [2015-05-13T14:24:01.896899 #14333] ERROR -- : Connection will be retried.
E, [2015-05-13T14:24:01.908777 #14333] ERROR -- : FluentLogger: Can't send logs to 127.0.0.1:10031: Connection refused - connect(2)

droonga0$ echo '{"type":"add","body":{"table":"Store","key":"dummy-store2","values":{"name":"Dummy Store 1"}}}' | \
            droonga-send --server droonga:157.7.124.247:10031/droonga
E, [2015-05-13T14:24:01.896664 #14333] ERROR -- : Failed to connect fluentd: Connection refused - connect(2)
E, [2015-05-13T14:24:01.896899 #14333] ERROR -- : Connection will be retried.
E, [2015-05-13T14:24:01.908777 #14333] ERROR -- : FluentLogger: Can't send logs to 157.7.124.247:10031: Connection refused - connect(2)

$ droonga-groonga select --table Store --limit -1 --output_columns name --pretty

127.0.0.1(ループバックアドレス)も157.7.124.247(グローバルIPアドレス)も、指定のポートに接続できないというエラーになっています。 selectによる検索はエラーが出ていませんが、これは接続エラーが内部でハンドルされているためで、結果はやはり得られていません。 もう1度192.168.0.50を指定して検索してみると、当然ながら結果に変化は無く、レコード追加のリクエストは処理されていないことが分かります。 (このような結果になるのは、Droonga Engineのサービスが初期化時に指定されたホスト名およびそれに紐付けられたIPアドレスにのみバインドされているからです。)

ということで、Droongaクラスタがローカルネットワーク向けにのみサービスを提供していることを確認できました。 ちなみに、Droonga Engine同士が互いの死活状態を把握するために使っているSerfのサービスも、同様にローカルネットワーク内でのみ利用できるようになっています。

droonga0$ sudo -u droonga-engine -H ~droonga-engine/droonga/serf -rpc-addr 192.168.0.50:7373
droonga0:10031/droonga     192.168.0.50:7946  alive  role=service-        provider,type=engine,cluster_id=5531f182f96b4699b55f0aa7cb100a118c73945a,internal-name=droonga0:46944/droonga

droonga0$ sudo -u droonga-engine -H ~droonga-engine/droonga/serf members  -rpc-addr localhost:7373
Error connecting to Serf agent: dial tcp 127.0.0.1:7373: connection refused

droonga0$ sudo -u droonga-engine -H ~droonga-engine/droonga/serf members  -rpc-addr 127.0.0.1:7373
Error connecting to Serf agent: dial tcp 127.0.0.1:7373: connection refused

droonga0$ sudo -u droonga-engine -H ~droonga-engine/droonga/serf members  -rpc-addr 157.7.124.248:7373
Error connecting to Serf agent: dial tcp 157.7.124.248:7373: connection refused
Droonga HTTP serverのインストール

Droonga EngineだけではHTTP接続できないので、Droonga HTTP serverもインストールしておきます。

droonga0$ curl https://raw.githubusercontent.com/droonga/droonga-http-server/master/install.sh | \
            sudo HOST=droonga0 ENGINE_HOST=droonga0 bash

HOSTENGINE_HOSTの指定はノードごとに変える必要があります。

Droonga HTTP serverをインストールできたら、設定を変えて、サービスを0.0.0.0ではなくローカルネットワークのIPアドレスにバインドするようにします。

droonga0$ sudo droonga-http-server-configure 
Do you want the configuration file "droonga-http-server.yaml" to be regenerated? (y/N): y
IP address to accept requests from clients (0.0.0.0 means "any IP address") [0.0.0.0]: 192.168.0.50
...
enable "trust proxy" configuration (y/N): y
...

上記以外の箇所はすべて既定値のままにします。 これで、Droonga HTTP serverもDroonga Engine同様に外部からのアクセスを受け付けなくなります。

droonga0$ curl http://localhost:10041/engines
curl: (7) Failed to connect to localhost port 10041: Connection refused

droonga0$ curl http://157.7.124.247:10041/droonga/system/status
curl: (7) Failed to connect to 157.7.124.247 port 10041: 接続を拒否されました

ここまでの結果を図にすると、以下のような構成になっているという事になります。

(ローカルネットワーク向けにDroongaクラスタが構成されている様子の図)

サーバを安全に運用する、という話になるとファイアウォールの設置やiptablesといった話題が出てきがちですが、Droongaノードはこの種の技術との相性が悪いです。 というのも、Droongaはランダムに決まったポート番号でメッセージを受け付ける部分があるため、どのポートを開放するかということを事前に完全には決めきれないのです。 なので、今回の例のように、外界から隔離されたネットワーク内で互いに自由に通信できるDroongaノード同士でクラスタを構成しておき、セキュリティ対策は別のレイヤで講じるのが、Droongaクラスタの推奨運用スタイルということになります。 (言い換えると、複数拠点間をインターネット越しに繋いだDroongaクラスタの構築は非常に面倒という事になります。速度的なデメリットもありますので、そのような構成は非推奨というのが正直な所です。)

リバースプロキシの設定

さて、安全なDroongaクラスタを構築できたわけですが、このままだとConoHa上のローカルネットワークに属しているVPSからでないとDroongaクラスタに接続できません。 インターネット越しにDroongaクラスタに接続するには、SSHでポートフォワードするなどの工夫が必要になります。 このクラスタはDroongaの運用デモンストレーションに使いたいので、これでは困ります。

そこで、リバースプロキシとしてnginxを設定しました。

(nginxをリバースプロキシとして利用して、80番ポートへのアクセスをDroonga HTTP serverに繋いでいる様子の図)

リバースプロキシとして設定されたnginxは、グローバルIPアドレスの80番ポートで接続を待ち受けて、受け付けたリクエストのうち安全な物だけをDroonga HTTP serverに引き渡すという動作をします。 これで、Droongaクラスタを意図しない変更から守りつつ、内容をインターネットで公開することができます。

nginxの導入は、Ubuntuであればaptでインストールして設定を作成するだけで済みます。 Droonga HTTP server用の設定は、Ningx本体の一般的な設定とは別のファイルに分けておくと管理が容易です。

droonga0$ sudo apt-get install nginx
droonga0$ sudo vi /etc/nginx/conf.d/droonga-http-server.conf

/etc/nginx/conf.d/droonga-http-server.confの内容は以下の通りです。

upstream droonga-http-server {
    server 192.168.0.50:10041;
}

server {
    listen 80;
    server_name 157.7.124.247;
    location ~ ^/($|favicon\.|(js|css|images|scripts|styles|views|[^\.]+\.(html|txt))/|engines|connections|cache|d/(select|table_list|column_list|status|suggest)|droonga/(search|system/status|system/statistics)) {
        proxy_pass http://droonga-http-server;
    }
}

locationに定義しているのは、この正規表現にマッチするパスのリクエストだけを192.168.0.50に転送するという指定です。 機能のうち極限られた部分だけが安全で、それ以外は全て危険、という場合にはこのようにホワイトリスト形式でリバースプロキシを設定するのが定石です。

ファイルを編集し終えたら、nginxを再起動します。

droonga0$ sudo service nginx restart

これで、安全なリードオンリーのリクエストのみ、インターネット上からnginxを経由してDroonga HTTP serverに送れるようになりました。 同様の手順でdroonga1にもnginxを設定しておけば、droonga0droonga1のどちらもDroongaクラスタへの公開エンドポイントとして利用できるようになります。 ちなみに、この記事の冒頭に記載したリンクも、このリバースプロキシ経由でDroonga HTTP serverに接続するための物です。

nginxのログを収集してDroongaクラスタに格納する

これだけだと本当にただ単にDroongaクラスタがあるだけなので、デモンストレーションとしては役に立ちません。 また、継続的にリクエストがある状態にしておかないと、「ちゃんと安定して動作しているかどうか」という検証にもなりません。 そこで、手始めにFluentdを使ってdroonga0自体のnginxのアクセスログを収集するようにしてみました。

Fluentdでは、いくつかのプラグインを組み合わせて「情報ソースからデータを取得してくる」→「データを加工する」→「データを出力する」という事を行います。 この時、データの出力先として他のノードを指定したり、データの情報ソースとして他のノードからの流入を受け付けたりすることで、複数ノードからの情報を一箇所に簡単に集められます。

今回の事例では、droonga0droonga1のそれぞれについてnginxのログを収集して、droonga0のDroonga HTTP serverを使ってログをデータベースに格納するように設定しました。 図にすると以下の要領です。

(Droongaクラスタ、リバースプロキシのnginx、Fluentdの3者が連携している様子の図)

Fluentdのインストール

何はともあれ、Fluentdを各ノードにインストールする必要があります。 droonga0droonga1の両方に、以下の手順でFluentdをインストールします。

$ sudo apt-get install ntp
$ sudo service ntp restart
$ curl -L http://toolbelt.treasuredata.com/sh/install-ubuntu-trusty-td-agent2.sh | sudo sh
ログを監視するノードでの設定

続けて、nginxのログを監視したいすべてのノード(ここではdroonga0droonga1)で、必要な設定を行います。 まず、nginxのログをFluentdから読めるようにグループとパーミッションを設定します。

$ sudo chmod -R g+r /var/log/nginx
$ sudo chgrp -R adm /var/log/nginx
$ sudo usermod -a -G adm td-agent

td-agentユーザやグループに対して読み取りの権限を与えるのではなく、admグループに読み取りの権限を与えた上でtd-agentadmグループに所属させる、という操作をしているのがポイントです。

admというのは端的に言うと「ログを見る権限のあるユーザ」を示すグループ名です。 ログファイルのグループをtd-agentに変更する方法だと、ログローテーションが発生すると新しいログファイルのグループは自動的にadmに設定されるため、ログローテーションの度にファイルのアクセス権を設定し直さないといけないということになってしまいます。

次に、必要なFluentdプラグインを導入します。 多くのFluentdプラグインはGemパッケージとして提供されていますが、gemではなくtd-agent-gemコマンドを使うことで、Fluentd専用にそれらのパッケージをインストールできます。

$ sudo td-agent-gem install fluent-plugin-config-expander
$ sudo td-agent-gem install fluent-plugin-parser
$ sudo td-agent-gem install fluent-plugin-anonymizer

各プラグインの役割は以下の通りです。

  • fluent-plugin-config-expander:設定の中で変数を使えるようにします。
  • fluent-plugin-parser:nginxのログファイルの各行を、扱いやすいように各フィールドの名前をキーとしたハッシュに変換します。
  • fluent-plugin-anonymizer:特定のフィールドの値をハッシュ化し、プライバシーを保ちつつ、元は別々の値であったことを分かるようにします。nginxのログにはプライバシーに関わる情報が含まれるため、これを使って匿名化しておきます。

必要なプラグインが揃ったら、Fluentdの設定ファイル(/etc/td-agent/td-agent.conf)に以下の設定を書き加えます。

$ sudo vi /etc/td-agent/td-agent.conf
# nginxのアクセスログを監視する
<source>
  type config_expander
  <config>
    type tail
    path /var/log/nginx/access.log
    pos_file /var/log/td-agent/nginx.pos
    # 後で分かりやすいように、タグにホスト名を含める。
    tag nginx.log.${hostname}
    format nginx
  </config>
</source>

# プライバシーに関わる情報を匿名化する
<match nginx.log.*.**>
  type anonymizer
  # 公開するログサーバなので、refererも匿名化する
  sha1_keys remote, user, referer
  # 値をハッシュ化する際のsaltなので、ここは任意の文字列に変えておく
  # (各ノードで共通の値にする)
  hash_salt droonga-log-cluster
  add_tag_prefix anonymized.
</match>

droonga0以外のノードでは、以下の設定も付け足して、ログをdroonga0に送るようにします。

# ログをdroonga0宛に転送する
<match anonymized.*.log.*.**>
  type forward
  <server>
    host droonga0
  </server>
</match>
ログをDroongaクラスタに格納するための設定

他のノードから送られてきたログを受け取ってDroongaクラスタに格納するためのエンドポイントとなるdroonga0では、さらに追加の設定が必要です。

まず、必要なFluentdプラグインを導入します。

droonga0$ sudo td-agent-gem install fluent-plugin-record-reformer
droonga0$ sudo td-agent-gem install fluent-plugin-groonga

各プラグインの役割は以下の通りです。

  • fluent-plugin-record-reformer:タグ名から取り出した値などを使って、フィールドの値を再設定します。
  • fluent-plugin-groonga:ログをGroongaのレコードとしてloadします。GroongaサーバにはHTTPでリクエストを送る事ができるため、Droongaにもそのまま利用できます。

fluent-plugin-groongaのバッファ保存先に使うために、Fluentdが読み書きできるディレクトリを作成します。

droonga0$ sudo mkdir -p /var/spool/td-agent/buffer/
droonga0$ sudo chown -R td-agent:td-agent /var/spool/td-agent/

Fluentdの設定ファイルに、ログをDroongaクラスタに格納するための設定を書き加えます。

droonga0$ sudo vi /etc/td-agent/td-agent.conf
# 他ノードからforwardされてきたログを取り込む
<source>
  type forward
</source>

# 各ホストから流入してきたログに、ホストを識別するための情報を付与する
<match anonymized.*.log.*.**>
  type record_reformer
  enable_ruby false

  tag ${tag_parts[2]}

  <record>
    host ${tag_suffix[3]}
    type ${tag_parts[1]}
    timestamp ${time}
  </record>
</match>

# ここまでの加工が全て終わったログのみを記録する
<match log>
  type groonga
  store_table Logs

  protocol http
  host droonga0

  buffer_type file
  buffer_path /var/spool/td-agent/buffer/groonga
  flush_interval 1s

  <table>
    name Terms
    flags TABLE_PAT_KEY
    key_type ShortText
    default_tokenizer TokenBigram
    normalizer NormalizerAuto
  </table>

  <table>
    name Hosts
    flags TABLE_PAT_KEY
    key_type ShortText
  </table>

  <table>
    name Timestamps
    flags TABLE_PAT_KEY
    key_type Time
  </table>

  <mapping>
    name host
    type Hosts
    <index>
      table Hosts
      name logs_index
    </index>
  </mapping>

  <mapping>
    name timestamp
    type Time
    <index>
      table Timestamps
      name logs_index
    </index>
  </mapping>

  <mapping>
    name path
    type Text
    <index>
      table Terms
      name logs_message_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name referer
    type Text
    <index>
      table Terms
      name logs_message_index
      flags WITH_POSITION
    </index>
  </mapping>
</match>
ログの収集を開始する

設定が終わったら、各ノードのFluentdを再起動します。

$ sudo service td-agent restart

これで、Fluentdがnginxのログを読み始めて、結果がfluent-plugin-groongaを経由してDroongaクラスタに格納されるようになります。

まとめ

以上、ConoHa上にDroongaクラスタを構築する手順と、そのDroongaクラスタに継続的に情報を格納する例としてnginxのログをFluentdで収集する手順を紹介しました。

Droongaはまだまだ開発途上のプロジェクトのため、Groongaに対して実装が追いついていない部分や、Droonga独自の部分で不具合が残っている部分があります。 お恥ずかしい話ですが、今回の公開用クラスタの構築中にもいくつかの不具合を見つけて修正しました。 継続的な運用を通じてこういった不安要素を排除していき、安心して誰でも使えるようなクオリティの物にDroongaを育てていきたいと思っています。 また、実際にDroongaを試してみて不具合に遭遇したりドキュメントの不備を見つけられた方は、GitHubのイシュートラッカーに問題を報告したり、修正内容をプルリクエストで送ったりして頂ければ幸いです。

タグ: Groonga
2015-06-09

GitHub上のアクティビティを定点観測してみませんか?

先日、fluent-plugin-github-activitiesSharetaryという2つのソフトウェアをリリースしました。 これらを組み合わせると、GitHub上の個々人の活動をウォッチして簡単に共有することができます。

実際にどのように動作するのか、ClearCode Inc. Organizationに所属しているアカウントの公開アクティビティを収集するSharetaryの運用サンプルをご覧下さい。 (ちなみに、この運用サンプルは別のエントリで紹介したデモンストレーション用のDroongaクラスタをデータの格納先として利用しています。)

この仕組みは、SEゼミの2015年度の取り組みを支援するために開発しました(実は、先日のリーダブルコード勉強会でも会場の片隅で試験運用していました)。 今年のSEゼミではOSS Hack Weekendと題して、学生の皆さんに実際のOSSプロジェクトに参加してもらう予定ですが、Sharetaryの画面を会場内のスクリーンに出しておくことで、イベントの中で実際にどのような開発が行われているかを会場内で目に見える形で共有できますし、プルリクエストが採用されればその様子が一目で分かるというわけです。

GitHub上のアクティビティをクローリングするfluent-plugin-github-activities

fluent-plugin-github-activitiesは、GitHub上の公開アクティビティをクローリングすることに特化したFluentdプラグインです。 以下のような<source>を定義しておくと、設定で列挙したユーザの公開アクティビティを一定間隔で取得して、新規に取得したアクティビティの情報をFluentdのストリームに流すようになります。

<source>
  type github-activities
  access_token xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  users ashie,co-me,cosmo0920,hayamiz,hhatto,kenhys,kou,min-shin,monkey-mas,mooz,okkez,piroor,ridec,s-yata,t2y
  clients 4
  interval 1
  base_tag github-activity.
  pos_file /var/log/td-agent/github-activities.json
</source>

この時レコードとして流れるのは、Events APIの個々のEventです。 どんな種類のアクティビティがあってどんな情報を取得できるのかは、GitHubのAPIドキュメントを参照して下さい。

このプラグインの働きとしてはただそれだけの物なので、Sharetary以外にも様々な用途に流用できます。 例えば社内チャットにアクティビティを自動投稿するというのも、1つの使い方でしょう。

使用の際の注意点として、OAuthのアクセストークンを取得して設定ファイルに書いておく必要があるという点が挙げられます。 GitHub APIは手順に則って取得したアクセストークンを使わない場合、クロールの頻度が最大で60リクエスト毎時までに制限されてしまいます(アクセストークンがあれば5000リクエスト毎時までリクエストできます)。 fluent-plugin-github-activitiesはpushに対応するアクティビティを見付けたときはそれに含まれる各commitの詳細も個別に取得するようになっているため、アクセストークン無しの状態だとすぐにリクエスト過多と判断されてしまいます。

収集されたイベントをタイムライン&アーカイブ表示するSharetary

Sharetaryというのは「Share(共有)」と「Secretary(書記、秘書)」の組み合わせによる造語です。 バックエンドとしてはGroongaまたはDroongaのHTTPサーバを利用する想定になっており、所定の形式に則って定義された「イベント情報」のテーブルに対して検索を行って、新たに増えたイベントのレコードをTwitterなどのタイムライン風にリアルタイム表示することができます。

また、タイムライン表示とは別に、アーカイブ表示というモードもあります。 このモードでは、その時点までに取得済みのイベントのレコードをTogetterのように時系列順で表示することができます。 あるイベントに関連付けられたイベント群はスレッド風に表示されるので、タイムライン表示では追いにくい議論やコメントのやり取りなども、このビューであれば俯瞰して見る事ができます。

なお、Sharetaryで表示するための個々のイベントのレコードには「返信先URI」にあたるフィールドを持たせることができ、例えばGitHub上での「コミット」に対応するイベントに対して「コミットに対するコメントの入力フォーム」のURIを返信先として紐付けておけば、SharetaryのUI上で「このイベントに返信する」というような操作を行った際にそのコメント入力フォームへ遷移させることができます。 そうして追加されたコメントはfluent-plugin-github-activitiesによってクロールされ、Sharetaryのタイムライン上に表れることになります。 このように、Sharetaryはそれ自体に固有の情報をなるべく保持せずに、公開の情報だけをソースとして運用できる設計となっています。

fluent-plugin-github-activitiesとSharetaryの連携

Sharetary自体はデータをクロールする仕組みを持っていないため、fluent-plugin-github-activitiesのように何らかのクローラと併用することが前提になります。 冒頭でも紹介した実際の運用例は、以下の図のような構成になっています。

(droonga1の上に、サービスを提供しているSharetaryと、クローリングしているFluentdが存在する)

この図を見ると、Sharetaryとfluent-plugin-github-activities(Fluentd)は、共通のデータベースとしてDroongaクラスタにアクセスしてはいますが、互いに独立して動作している事が見て取れるでしょう。

fluent-plugin-github-activitiesは収集したアクティビティの情報をそのままレコードとして流すだけなので、それだけではSharetaryの情報ソースには使えません。 実際には、それらのアクティビティをSharetary用の適切なイベント形式のレコードに変換し、fluent-plugin-groongaを使ってSharetary用のデータベースにloadさせる必要があります。

Fluentdでのレコードの形式変換に使えるプラグインはいくつかありますが、元レコードの情報を使って全く新しいレコードを組み立てるという用途には、fluent-plugin-mapが適しているようです。 例えば、「Issueを新たに作成した」というアクティビティをfluent-plugin-github-activitiesがクロールしてきた際に、それを使ってSharetaryのイベントとなるレコードを組み立てる(GitHubのアクティビティをSharetaryのイベントに変換する)ルールは以下のように書けます。

<match github-activity.issue-open>
  type map
  tag "\"sharetary.\" + tag"
  time time
  record (require("time") || true) && { "_key" => record["payload"]["issue"]["html_url"], "type" => "issue-open", "source_icon" => "https://github.com/favicon.ico", "class" => "major", "scope" => record["repo"]["name"], "scope_icon" => record["$github-activities-related-organization-logo"], "title" => "Open", "description" => "Opened new issue: " + "#" + record["payload"]["issue"]["number"].to_s + " " + record["payload"]["issue"]["title"] + ": " + (record["payload"]["issue"]["body"] || ""), "actor" => record["actor"]["login"], "actor_uri" => "https://github.com/#{record["actor"]["login"]}/", "actor_icon" => record["$github-activities-related-avatar"], "actor_class" => "major", "uri" => record["payload"]["issue"]["html_url"], "reply_uri" => "#{record["payload"]["issue"]["html_url"].split("#").first}#new_comment_field", "created_at" => Time.now.to_i, "timestamp" => Time.parse(record["created_at"]).to_i, "parent" => nil }
</match>

fluent-plugin-mapでは変換ルールを文字列として定義しますが、Fluentdの設定ファイルでは文字列型の設定値を改行できないため、このように横長の記述になってしまっています。

基本的にはこの要領でSharetaryに表示させたいアクティビティ用の変換ルールを定義していくのですが、typeがタグと同じ内容だったり、各ルールでactor_classsource_iconが同じだったりする場合、この段階でフィールドを定義するよりも、後でまとめて設定した方がスッキリしますし、今後の変更も容易になります。 レコードのタグなどを使って追加のフィールドを定義するfluent-plugin-record-reformarプラグインを使うと、これらの共通フィールドは以下のようにして一気に設定できます。

<match github-activity.issue-open>
  type map
  tag "\"sharetary.\" + tag"
  time time
  # type, actor_class, source_iconはこの段階では埋めていない
  record (require("time") || true) && { "_key" => record["payload"]["issue"]["html_url"], "class" => "major", "scope" => record["repo"]["name"], "scope_icon" => record["$github-activities-related-organization-logo"], "title" => "Open", "description" => "Opened new issue: " + "#" + record["payload"]["issue"]["number"].to_s + " " + record["payload"]["issue"]["title"] + ": " + (record["payload"]["issue"]["body"] || ""), "actor" => record["actor"]["login"], "actor_uri" => "https://github.com/#{record["actor"]["login"]}/", "actor_icon" => record["$github-activities-related-avatar"], "uri" => record["payload"]["issue"]["html_url"], "reply_uri" => "#{record["payload"]["issue"]["html_url"].split("#").first}#new_comment_field", "created_at" => Time.now.to_i, "timestamp" => Time.parse(record["created_at"]).to_i, "parent" => nil }
</match>

<match sharetary.github-activity.**>
  type record_reformer
  enable_ruby false
  tag completed.${tag}
  <record>
    type ${tag_parts[2]}
    actor_class major
    source_icon https://github.com/favicon.ico
  </record>
</match>

こうして組み立てたレコードは、fluent-plugin-groongaを使ってGroongaまたはDroongaに格納します。 以下はfluent-plugin-groonga用の設定例です。

<match completed.sharetary.**>
  type groonga
  store_table Events

  protocol http
  host droonga0

  buffer_type file
  buffer_path /var/spool/td-agent/buffer/groonga
  flush_interval 1s

  <table>
    name Events
    flags TABLE_PAT_KEY
    key_type ShortText
  </table>

  <table>
    name Timestamps
    flags TABLE_PAT_KEY
    key_type Time
  </table>

  <table>
    name Terms
    flags TABLE_PAT_KEY
    key_type ShortText
    default_tokenizer TokenBigram
    normalizer NormalizerAuto
  </table>

  <mapping>
    name type
    type ShortText
  </mapping>

  <mapping>
    name class
    type ShortText
  </mapping>

  <mapping>
    name title
    type ShortText
    <index>
      table Terms
      name Events_title_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name description
    type Text
    <index>
      table Terms
      name Events_description_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name extra_description
    type Text
    <index>
      table Terms
      name Events_extra_description_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name scope
    type ShortText
    <index>
      table Terms
      name Events_scope_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name scope_icon
    type ShortText
  </mapping>

  <mapping>
    name uri
    type ShortText
    <index>
      table Terms
      name Events_uri_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name source_icon
    type ShortText
  </mapping>

  <mapping>
    name reply_uri
    type ShortText
  </mapping>

  <mapping>
    name actor
    type ShortText
    <index>
      table Terms
      name Events_actor_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name actor_uri
    type ShortText
  </mapping>

  <mapping>
    name actor_icon
    type ShortText
  </mapping>

  <mapping>
    name actor_class
    type ShortText
  </mapping>

  <mapping>
    name timestamp
    type Time
    <index>
      table Timestamps
      name Events_timestamp_index
    </index>
  </mapping>

  <mapping>
    name created_at
    type Time
    <index>
      table Timestamps
      name Events_created_at_index
    </index>
  </mapping>

  <mapping>
    name parent
    type ShortText
    <index>
      table Terms
      name Events_parent_index
      flags WITH_POSITION
    </index>
  </mapping>
</match>

この構成においてはdroonga0droonga1のそれぞれでFluentdが動作していますが、Sharetary関係の物はすべてdroonga1に置いておくという方針で、今回はdroonga1にもfluent-plugin-groongaをインストールして、そこからSharetary用のイベントレコードをloadするようにしています。

Issueの作成以外の各種アクティビティも含めた変換ルールの設定例の完全版は、Sharetaryのリポジトリに含まれていますので、参考にしてみて下さい。 ……というか、認証のための情報やfluent-plugin-groongaの接続先ホスト以外の全ての設定は、上記運用例でもこれをそのまま使っています。 皆さんも、試してみる時はこの設定をスタート地点として、ご自分のニーズに合うように微調整していくとよいでしょう。

ところで、上記の説明からも分かる通り、適切なイベント形式のレコードに変換さえできるのなら、Sharetaryの情報ソースはGitHubのアクティビティでなくても構いません。 この設定例は、イベント同士を関連付けたり、イベントの種類に応じてSharetaryの画面上での目立たせ方を変えたりといった事のサンプルにもなっていますので、他の情報ソースを使う場合の参考になるのではないでしょうか。

まとめ

以上、最近開発・公開したfluent-plugin-github-activitiesSharetaryという2つのソフトウェアの概要と使い方について簡単にご紹介しました。

この両者の組み合わせは利用形態の一例に過ぎないとはいえ、現時点では最も代表的な利用形態でもあります。 GitHubを開発の場所として利用するハッカソン形式のイベントを開催される際は、会場内での賑やかしとして、またイベント期間中の出来事を後から振り返る際のログとして、是非利用してみてください。

また、Sharetaryの運用例としてクリアコード関係者の公開アクティビティの定点観測所についてもご案内しました。 クリアコードが日常の業務の中でOSSやフリーソフトウェアを開発・公開したり既存プロジェクトに協力したりしている様子を俯瞰してご覧頂けますので、弊社オフィスに遊びに来られるような感覚でフラッと覗いてみて頂ければ幸いです。

2015-06-10

Firefox 29で削除されたセキュリティポリシー機能と、その代替手段

概要

Firefox 29において、古くから存在していたセキュリティポリシー設定のための機能(Configurable Security Policies、またはCapabilities、略してCAPS)の大部分が削除されました。 このエントリでは、CAPSで行っていた設定を、サイト別設定機能とクリアコードが開発・提供しているアドオンの組み合わせで代用する方法について解説します。

CAPSとは?

Firefoxには古くから、Webサイトごとに詳細な権限設定を行うためのCAPSと呼ばれるセキュリティポリシー機能が存在していました。 これは以下のような形で利用する物でした。

// ポリシー一覧を定義
pref("capability.policy.policynames", "trusted");

// 既定のポリシーを定義
// JavaScriptの実行を禁止
pref("capability.policy.default.javascript.enabled", "noAccess");
// file:// で始まるURLの読み込みを禁止
pref("capability.policy.default.checkloaduri.enabled", "noAccess");
// クリップボードへの書き込み操作を禁止
pref("capability.policy.default.Clipboard.cutcopy", "noAccess");
// クリップボードの内容の読み取り操作を禁止
pref("capability.policy.default.Clipboard.paste", "noAccess");
// ウィンドウを開く事を禁止
pref("capability.policy.default.Window.open", "noAccess");
...

// 信頼済みサイト用のポリシーを定義
// ポリシーを適用するサイトの一覧を定義
pref("capability.policy.trusted.sites", "http://www.mozilla.org www.mozilla.com");
// 既定のポリシーで禁止した操作を許可
pref("capability.policy.trusted.javascript.enabled", "allAccess");
pref("capability.policy.trusted.checkloaduri.enabled", "allAccess");
pref("capability.policy.trusted.Clipboard.cutcopy", "allAccess");
pref("capability.policy.trusted.Clipboard.paste", "allAccess");
pref("capability.policy.trusted.Window.open", "allAccess");
...

この機能は設定のためのUIが存在しておらず、アドオンNoScriptが内部的に利用する以外の場面では、一般にはほとんど使われていませんでした。 そのため、Firefox 29において大部分の機能が削除されました。 (参考:Firefox 29の互換性情報における記述) 現在は以下の機能だけが残されています。

  • JavaScriptの実行の可否の設定:capability.policy.(ポリシー名).javascript.enabled
  • File URLの読み込みの可否の設定:capability.policy.(ポリシー名).checkloaduri.enabled (Firefox 29では本体だけではこの機能は動作せず、アドオンcaps-fileuriのインストールが必要です。)

これら以外の機能は、Firefox 29以降では全く利用できません。

サイト別設定による代用

一方で、現在のFirefoxには「サイト別設定」と呼ばれる機能が備わっており、ポップアップブロック機能やパスワードの保存などについて、ホスト名単位で許可または禁止の権限を設定する事ができます。 これらのサイト別設定は、初期状態の権限は「不明」となっており、ポップアップブロック機能や、パスワードマネージャによるパスワードの保存機能などが発動した際に、ユーザにその後の操作が委ねられます。 その際に「今後は確認しない」などの選択を取ると、選択の結果がサイト別設定として保存され、以後は保存された設定が暗黙的に利用されるようになります。

実際に現在どのようなサイト別設定が保存されているかは、ロケーションバーに「about:permissions」と入力する事で確認できます。また、ポップアップブロック機能の許可サイト一覧やパスワードマネージャの例外サイト一覧も、本質的にはこの仕組みに基づいて提供されています。

以上のように、サイト別設定とCAPSは挙動もコンセプトも全く異なっています。 しかしながら、「プライバシー情報へのアクセスやユーザの利便性を損ない得る特別な操作は、ユーザが明示的に許可されない限りは禁止する」「特定のWebサイトについて、特別な操作を実行する許可を与える事ができる」という点に着目すると、用途によってはある程度はCAPSの代用として利用できそうです。

サイト別設定をCAPSの代用に使うには、以下のような事ができる必要があります。

  • サイト別設定を管理者が一括して管理できるようにする。
  • ユーザが任意にサイト別設定を保存できないようにする。
  • 保存済みのサイト別設定をユーザが削除できないようにする。

具体的な方法はいくつか考えられますが、ここではクリアコードが開発・提供しているアドオンを使った場合の手順をご紹介します。

サイト別設定を管理者が一括して管理する

FirefoxにはMission Control Desktop(MCD)と呼ばれる集中管理のための仕組みが備わっており、これを使うと、「プリファレンス」と呼ばれる仕組みに基づいて管理されている様々な設定を、管理者が一括して管理することができます。

しかしながら、サイト別設定はプリファレンスとは別の仕組みに基づいて管理されているため、MCDで集中管理する事ができません。 また、MCD以外の方法でサイト別設定を集中管理する事もできません。

クリアコードで開発しているPermissions Auto Registererというアドオンを使用すると、サイト別設定をMCDで集中管理できるようになります。 具体的には、MCDで定義したサイト別設定を各クライアントに配布し、各クライアント上で当該アドオンによってその設定を実際のサイト別設定として反映する、ということができます。

Permission Auto Registerer用の設定をCAPS風に定義した例が、以下のコードです。 「sites」以外の設定の値は、「0」が「ユーザに尋ねる(初期状態)」、「1」が「許可」、「2」が「禁止」です。

// 信頼済みサイトの一覧(スペース区切り)
pref("extensions.autopermission.policy.trusted.sites",
                                "www.example.com www.example.jp");
// Cookieの保存の可否
pref("extensions.autopermission.policy.trusted.cookie",         1);
// デスクトップ通知の可否
pref("extensions.autopermission.policy.trusted.desktop-notification", 1);
// DOMフルスクリーンの利用の可否
pref("extensions.autopermission.policy.trusted.fullscreen",     1);
// 位置情報APIへのアクセスの可否
pref("extensions.autopermission.policy.trusted.geo",            1);
// 画像の読み込みの可否
pref("extensions.autopermission.policy.trusted.image",          1);
// オフラインストレージの利用の可否
pref("extensions.autopermission.policy.trusted.indexedDB",      1);
// アドオンのインストールの可否
pref("extensions.autopermission.policy.trusted.install",        1);
// Webアプリケーションのオフラインキャッシュの利用の可否
pref("extensions.autopermission.policy.trusted.offline-app",    1);
// パスワードマネージャの利用の可否
pref("extensions.autopermission.policy.trusted.password",       1);
// マウスポインタを隠す事の可否
pref("extensions.autopermission.policy.trusted.pointerLock",    1);
// 広告などのポップアップウィンドウを開く事の可否
pref("extensions.autopermission.policy.trusted.popup",          1);
//// 以下はCAPS連携機能で、「sites」の設定に基づいて自動的に
//// 相当するCAPSの設定に変換されます。
// capability.policy.*.javascript.enabled に対応
pref("extensions.autopermission.policy.trusted.javascript",     1);
// capability.policy.*.checkloaduri.enabled に対応
pref("extensions.autopermission.policy.trusted.localfilelinks", 1);

見ての通り、CAPSの設定とほぼ同じ要領で設定できるようになっています。

ユーザ自身によるサイト別設定の変更を禁止する

Permission Auto Registererによってサイト別設定を集中管理できるようになっただけでは、CAPSの代用としては不完全です。 というのも、このままでは、サイト別設定をユーザが任意に変更したり削除したり追加したりできてしまうからです。 サイト別設定をCAPSの代用として使うためには、管理者以外はサイト別設定を変更できないように制限する必要があります。

サイト別設定をユーザが変更できないようにする最も単純な方法は、スタイルシートを使ってUI要素を非表示にするというものです。 クリアコードで開発しているglobalChrome.cssというアドオンを使うと、ユーザースタイルシート相当のカスタマイズを管理者が行う事ができます。

サイト別設定に関する設定UIは各所に分散しています。 すべてのサイト別設定に関するUIを非表示にするスタイルシートの例は、以下の通りです。 (※見落としがある場合には、訂正しますのでご指摘下さい)

/* Cookie */
:root#BrowserPreferences #cookiesBox,
:root#BrowserPreferences #acceptThirdPartyRow,
:root#BrowserPreferences #keepRow,
:root[windowtype="Browser:page-info"]
  #perm-cookie-row,
/* デスクトップ通知 */
:root[windowtype="Browser:page-info"]
  #perm-desktop-notification-row,
/* DOMフルスクリーン */
:root[windowtype="navigator:browser"]
  #full-screen-remember-decision,
:root[windowtype="Browser:page-info"]
  #perm-fullscreen-row,
/* 位置情報API */
:root[windowtype="navigator:browser"]
  #geolocation-notification menuitem[label="常に許可"],
:root[windowtype="navigator:browser"]
  #geolocation-notification menuitem[label="常に許可しない"],
:root[windowtype="Browser:page-info"]
  #perm-geo-row,
/* 画像の読み込み */
:root[windowtype="Browser:page-info"]
  #blockImage,
:root[windowtype="Browser:page-info"]
  #perm-image-row,
/* オフラインストレージ */
:root#BrowserPreferences #offlineNotify,
:root#BrowserPreferences #offlineNotifyExceptions,
:root[windowtype="Browser:page-info"]
  #perm-indexedDB-row,
/* アドオンのインストール許可 */
:root#BrowserPreferences #addonInstallBox,
:root[windowtype="navigator:browser"]
  #addon-install-blocked-notification menuitem[label="常に許可"],
:root[windowtype="navigator:browser"]
  #addon-install-blocked-notification menuitem[label="常に許可しない"],
:root[windowtype="Browser:page-info"]
  #perm-install-row,
/* パスワード保存 */
:root#BrowserPreferences #savePasswordsBox,
/* マウスポインタを隠す */
:root[windowtype="Browser:page-info"]
  #perm-pointerLock-row,
/* ポップアップブロック */
:root#BrowserPreferences #popupPolicyRow,
:root[windowtype="navigator:browser"]
  menuitem[observes="blockedPopupAllowSite"],
:root[windowtype="navigator:browser"]
  menuitem[observes="blockedPopupEditSettings"],
:root[windowtype="navigator:browser"]
  menuitem[observes="blockedPopupDontShowMessage"],
:root[windowtype="navigator:browser"]
  menuseparator[observes="blockedPopupsSeparator"],
:root[windowtype="Browser:page-info"]
  #perm-popup-row
{
  display: none !important;
  visibility: collapse !important;
}
タグ: Mozilla
2015-06-12

AuFSの代替としてのOverlayFS,unionfs-fuseを使うには

はじめに

Unionファイルシステムと呼ばれる、ファイルシステムを重ねあわせて透過的に扱うための仕組みがあります。この仕組みを実現する実装は複数あり、その一つがAuFS *1 です。

AuFSはDebian標準のカーネルではデフォルトで有効になっていたのですが、最近のカーネルでは削除され *2 ました。

この記事を書いている時点では、以下の2つのlinux-imageが64bit向けに提供されており、AuFSが使えるのは、3.16.0-4のほうのみ *3 です。

  • linux-image 4.0.0-1
  • linux-image 3.16.0-4

AuFSの代替としてのOverlayFS,unionfs-fuse

AuFSがDebianの提供している最新のカーネルから削除されたとはいえ、まったく使えないわけではありません。依然としてAuFS自体はカーネル 4.x向けにメンテナンスされています。しかしパッチをあてて自分でビルドするのはしんどいということもあるでしょう。

AuFSの代替案 *4 はいくつかあります。

  • OverlayFS
  • unionfs-fuse

前者のOverlayFSはカーネル 3.18以降でなら標準で使えます。ただし、モジュールを追加でロードする必要があります。

sudo modprobe overlay

正常にロードできていれば、/proc/filesystems にoverlayがいるはずです。

ここで、何故overlayfsじゃないのかと不思議に思った人はLXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術 第18回 Linuxカーネルのコンテナ機能 [7] ─ overlayfsを参照するとよいでしょう。そのあたりの経緯も詳しい説明があります。

後者の unionfs-fuse はパッケージを追加でインストールする必要があります。Debianではパッケージも提供されているので、導入も容易です。

sudo apt-get install unionfs-fuse

マウント方法の違い

準備ができたところで、ディレクトリAとディレクトリBを重ねあわせて使う例を示します。

ディレクトリを重ねあわせる条件は次の通りとします。

  • ディレクトリAは書き込み可能
  • ディレクトリBは読み込み専用
  • 重ねあわせたマウント先はディレクトリB

ディレクトリBに書きこんだら、その内容はディレクトリAにすべて反映される構成です *5

  • AuFSの場合
sudo mount -t aufs -o br:(ディレクトリA):(ディレクトリB)=ro none (ディレクトリB)
  • OverlayFSの場合
sudo mount -t overlay -o lowerdir=(ディレクトリB),upperdir=(ディレクトリA),workdir=(作業用のディレクトリ) overlay (ディレクトリB)

workdirは空のディレクトリである必要があります。

  • unionfs-fuseの場合
unionfs-fuse (ディレクトリA)=rw (ディレクトリB)

AuFSやOverlayFSの場合とくらべると、オプション指定も少なくすっきりしています。 ただし、ユーザーランドでの実装のため、AuFSやOverlayFSに比べると速度の面で劣ります*6

まとめ

AuFSの代替として、OverlayFSや unionfs-fuse コマンドでディレクトリを重ねあわせて使う方法を紹介しました。 そうそう頻繁に使う機会はないかも知れませんが、知っていると便利なコマンドです。 機会があれば、活用してみてください。

*1 http://aufs.sourceforge.net/

*2 3.18-1~exp1以降が該当。

*3 Debian Unstableの場合。

*4 https://en.wikipedia.org/wiki/UnionFS

*5 マスターとなるディレクトリと作業しているディレクトリがわかれていて、作業ディレクトリではマスターとなるディレクトリAのファイルを参照する必要があるという想定です。

*6 https://github.com/rpodgorny/unionfs-fuse#why-not-choose-it

2015-06-15

東京〜札幌間の勉強会参加時にLCCで安価に移動する際の注意点

横山です。この週末にオープンソースカンファレンス2015 Hokkaidoに参加してきたのですが、その際に交通手段としてLCC(格安航空会社)を利用したので、感想と注意点についてレポートしたいと思います(OSC本編のレポートは後日書く予定です)。

往路(山手線〜成田空港)

LCCは主に羽田空港ではなく成田空港を利用しています。例えば、JR山手線大塚駅からだと、山手線日暮里駅まで約10分、日暮里駅から京成電鉄のスカイライナーに乗り換えて約40分、乗り換え時間を含めても合計1時間ほどで国内線の発着駅である空港第2ビル駅に着きます。日暮里から空港第2ビルまでは無停車で直通なので快適です。そのため、意外なことに羽田空港までと比べて所要時間はほとんど変わりませんでした(出発駅と乗り換え時間によってはむしろ早く着く)。

ただし、スカイライナーは20〜40分間隔で運行しているのと、スカイライナーはIC運賃1,235円(きっぷ運賃1,240円)の他に特急券1,230円が必要です。スカイライナーを使わない場合、京成本線のアクセス特急や快速特急の成田空港行に乗れば特急券が不要なので安く済みます(停車駅が多い分、30分ほど到着が遅くなります)。

往路(成田空港〜新千歳空港)

行きの航空会社はジェットスターを使いました。Webで出発の10日ほど前に予約したのですが、曜日や時間帯によって運賃にばらつきがあるのは他の航空会社と変わらないようです。金曜日の午後は比較的安かったので、14:55発の便にしました。

ジェットスターなど多くのLCCには、基本運賃に上乗せすることでオプションを付けることができるシステムがあります。例えば、日時変更手数料が安くなったり、座席指定ができたり、大きな荷物を預けやすくなったりします。今回はそういったオプションは利用せず、基本運賃のみで片道7,300円でした。

また、ジェットスターを含むLCCの発着場は成田第2ターミナルではなく第3ターミナルにあるので、電車や車で行く場合はターミナル間を約500メートル歩かなければなりません。バスやタクシーであれば直接第3ターミナルに行くことも可能です。なお、第3ターミナルにも売店や食事コーナーはあります。

レストラン・ショップ | 成田国際空港第3ターミナル

私が乗った便は特に座席が狭かったり運行が遅れたりといったことはなく、無事に新千歳空港へ到着しました。

復路

帰りの航空会社はバニラエアを使いました。ジェットスターと同じくLCCです。前の便の到着が遅れたため、30分ほど遅れての出発になりました。ただ、この日は羽田空港でも1時間ほど遅れた便があったようなので、LCCだから遅れたというわけではないのかもしれません。運賃は安い最終便(定刻20:10発)を選んだので6,870円でした。注意点としては、あまり遅い時間になると成田空港から東京への脱出が難しくなります。具体的には、23:00頃までなら電車やバスで脱出できますが、それ以降はタクシーくらいしか選択肢がないようです。最終便は安いので選んでしまいがちですが、そういったリスクがあるのを考慮しておく必要があります。

まとめ

東京から札幌の勉強会に参加したり、札幌から東京の勉強会に参加する際に、LCCを使って安価に移動する際の注意点をまとめました。手続きや乗り心地などはほとんど気にならなかったのですが、夜の便で成田空港到着が遅れた場合のリスクが一番の注意点かなと感じました。スケジュールに余裕があってその点が問題にならない場合にはLCCも有力な選択肢になりそうです。

2015-06-16

2015年6月27日(土)にOSS開発参加未経験学生向けOSS開発イベントを開催予定

要約:6月27日(土)にOSS開発参加未経験の学生向けに、OSSの開発に参加する方法をワークショップ形式で教えるイベントを開催します。「OSSの開発に参加してみたいけど漠然とした不安があり手を出せていない…」という学生向けに「具体的にこういう方法で始めるといいよ」という方法を伝えます。ワークショップ形式で実際に手を動かしながら学べるうえに、現役の超優秀エンジニアが多数メンターとしてサポートするので、OSS開発への漠然とした不安を払拭できるはずです。OSSの開発に参加してみたいけど手を出せていなかった学生の人はイベントページから応募してください。背中を押してあげたい学生を知っている人は教えてあげてください。締め切りは6月22日(月)です。

2015年のSEゼミの第3弾のイベントです。2015年のSEゼミは「OSS開発」をテーマにしています。第1弾のリーダブルコード勉強会(開催済み)ではコードを書いてOSSの開発に参加するときに必要となる(開発者にとって)読みやすいコードの書き方を学びました。第2弾のGitHub勉強会(6/20なので今週末開催)ではGitHubを使っているOSSの開発に参加する時に必要となるpull requestの仕方を学びます。

それらに続く第3弾のOSS Hack 4 BeginnersではOSSの開発に参加したことのない学生を対象に、OSSの開発への参加方法を伝えます。このイベントの目的は「OSSの開発に参加してみたいけど漠然とした不安があり手を出せていない…」という学生の「不安」を取りのぞくことです。

不安を取りのぞいた後に控える第4弾のイベントOSS Hack Weekend(7月11日(土)・12日(日)開催)ではのびのびと楽しくOSSの開発に参加しましょう!

内容

OSS Hack 4 Beginnersは学生の不安を取りのぞくために、次のことを大事にした内容になっています。

  • 「やり方」を伝える。
  • 「やり方」を自分でやってみる。
  • やるときは「失敗」しても大丈夫な状況でやる。

これは次の点が「不安」の原因になっていると予想したからです。

  • 「やり方」を知らないから「不安」なのではないか。
  • やったことがないから「不安」なのではないか。
  • 「失敗が怖い」から「不安」なのではないか。

これらの不安の原因を次の方法で解消する内容になっています。

  • オススメのOSS開発参加方法を紹介
  • オススメの方法を実践
  • 対象OSSはメンターが開発に関わっているOSS
  • 超優秀現役エンジニアがメンターとしてサポート

それぞれ説明します。

オススメのOSS開発参加方法を紹介

まず、最初にOSS開発への参加の仕方を説明します。参加の仕方は無数にありますが、「好きなようにやっていいんだよ」と言うと不安が増す原因になってしまいます。そこで、トレーナーがオススメの方法を1つだけ説明し、このイベントではその方法を使って「やってみます」。あえて1つに限定することにより迷いポイントが減り、不安を軽減できるのではないかという考えです。

具体的な方法は次の通りです。

  1. まず動かす
  2. 開発用にインストール
  3. テストを実行

「まず動かす」というのは非常に重要な手順です。重要なことなのに言及している情報を見かけません。一方、pull requestの方法などはよく見かけます。当たり前すぎて誰も言及しないだけなのかもしれませんが、OSSの開発に参加する時に最初にやる一番大事なことは「動かす」ことです。

動かすためにはインストール手順を確認する必要があります。ドキュメントがあればそれを読んで、その手順に従ってインストールし、動かします。なかったら…、開発に参加するチャンスです!

OSSの開発に関わっていない人は、「ドキュメントが足りなくてわかんねぇよ!」とグチって終わりになる場面でしょう。しかし、OSSの開発に参加しようと思っている人にはチャンスです。グチっている時間で直せばよいのです。そうすれば、次にインストールしようとした人は「ドキュメントにちゃんと書いてあってスムーズにインストールできた」と思うことでしょう。直接あなたに感謝の言葉が届くことは稀かもしれませんが、「世界を(ちょっとかもしれないけど)変える」(@_ko1さんからのこのイベント参加者へのメッセージより)ことをしたのです。少しかもしれませんが、その少しが積み重なって大きな変化になっていきます。そうやってOSSはよくなっていくのです。

(なお、このイベントではすぐに対応せずにメモに残しておいて後で対応する、という進め方をします。いきなり「動かす」と「直す」を一緒にやると大変なので、まずは「動かす」に集中します。未経験者向けのイベントなので敷居を下げています。)

「OSSの開発に参加」というと、「コードを読んでバリバリコードを書かないと!」というイメージがあるかもしれませんが、そんなことばかりではありません。コードを書く前の段階でも、ユーザーの目線でフィードバックできることはたくさんあるのです。OSSの開発に参加したことがない人はそれを知らずに敷居が高いと感じてしまっていることでしょう。このイベントを通じて、「そんなことはないんだ、開発に参加したいならできることはいろいろあるんだ」ということをわかってもらいたいです。

「動かす」ことができたら「開発用にインストール」します。「動かす」ときはパッケージを使ってインストールするといったように、ユーザーとしてインストールします。一方、「開発用にインストール」するときはソースコードを取得して、そこから使える状態にもっていきます。

一般的に、「開発用にインストール」するときの方が手間がかかります。しかし、ソースコードを変えて手元で変更を確認できる環境が手に入るというメリットがあります。開発に参加するときはこれは非常に大きなメリットです。

なお、「開発用にインストール」するときもドキュメントがあればそれを参考に作業します。なかったら…、自分がやった手順をメモして、まとめたものを開発者に報告しましょう。開発に参加するチャンスですね。

「開発用にインストール」できたら「テスト」を実行します。多くのOSSではテストが用意されていて、開発者が手元で正しい挙動を確認できるようになっています。テストが用意されていたら実行して、自分の手元でも正しい挙動になっているか確認しましょう。

以上がオススメのOSS開発参加方法です。

イベントではこの内容を説明した上で、実際にトレーナーが前でやってみせます。実際にやっている様子を見ることで理解が進むことを期待しています。

オススメの方法を実践

オススメのOSS開発参加方法を知った後は実際にオススメの方法を実践します。

実践する際はグループを作って、グループごとに進めます。1つのグループは多くても4人から6人です。同じグループでは同じOSSを開発対象にします。これは、グループ内で相談できるようにするためです。

OSSの開発では困ったことがあったら相談しながら進めることは当たり前です。多くの場合、時間的・距離的に離れていることが多いので相談するときはインターネット越しになります。ただし、いきなりインターネット越しに相談しながらやってもいいんだよ、といっても敷居が高いでしょうから、今回は目の前の人たちと相談しやすいように同じグループで同じOSSを開発対象にしました。相談することに慣れてきたらインターネット越しでも相談できるようになることを期待しています。

なお、イベントでは現役の超優秀エンジニアが多数メンターとしてサポートします。メンターはグループ内での相談を促したり、困っていることに対してヒントを与えてくれたりします。メンターとも協力してオススメのOSS開発参加方法を実践し、できるという感触を自分のものにしましょう。

対象OSSはメンターが開発に関わっているOSS

今回開発対象にするOSSはメンターが開発に関わっているOSSにします。当初は学生からの希望も考慮して決めようかと検討していたのですが、学生の「不安」を取り除くことを考えた結果、メンターが開発に関わっているOSSにすることにしました。

理由は次の通りです。

  • 「失敗」に対する「不安」を小さくできそう
    • たとえば新しくissueを作るときに、「こんなissueを立てて嫌がられないかな…」と「不安」に思ってしまったとしても、目の前にいるメンターから「それをやっても大丈夫だよ」と言ってもらえるなら行動に移せるのではないかと期待しています。
  • その場で反応をもらえそう
    • はじめてアクションを起こしたときは相手の反応がくるまでドキドキしてしまうものです。メンターはイベントに参加しているのですぐに反応してくれるはずです。反応待ちでドキドキして作業を進められないといったことを短くすることを期待しています。

まとめ

6月27日(土)に開催するOSS開発参加未経験の学生向けに、OSSの開発に参加する方法をワークショップ形式で教えるイベントを紹介しました。「OSSの開発に参加してみたいけど漠然とした不安があり手を出せていない…」という学生の人はイベントページから応募してください。背中を押してあげたい学生を知っている人は教えてあげてください。あるいは、現役の超優秀エンジニアメンターへのサポートを受けさせてあげたい学生を知っている人はこのイベントへの参加を勧めてください。

なお、締め切りは6月22日(月)です。

すでにOSSの開発に参加していて、現役の超優秀エンジニアメンターのやり方を参考にしたいという学生の人は7月11日(土)・12日(日)に開催するOSS Hack Weekendに応募してください。こちらは学生が希望したOSSを開発対象にしますし、メンターからいろいろ学べる時間がたっぷりあるので、充実した週末になるはずです。

2015-06-17

SharetaryのためのGitHubアクティビティ定点観測の設定・改

先日、「GitHub上のアクティビティを定点観測してみませんか?」と題してfluent-plugin-github-activitiesSharetaryという2つのソフトウェアを紹介しましたが、Sharetaryの改善に伴っていくつか設定の仕方に変化がありましたので、改めて、最新バージョンでの推奨設定について解説します。 ClearCode Inc. Organizationに所属しているアカウントの公開アクティビティを収集するSharetaryの運用サンプルも、この解説と同じ手順でSharetary 0.5.0に移行しました。

なお、この記事はSharetaryの利用のための技術情報が主ですが、Fluentdのストリームにおける1つの入力を元にして、fluent-plugin-groongaで複数テーブルにレコードを追加する方法の解説にもなっています。 fluent-plugin-groongaを使ったGroongaサーバーのレプリケーションに対して、in_groongaを使わずに直接レコードを登録するやり方に興味がある方も、参考にしてみて下さい。

Sharetary 0.4.0(0.5.0)での変更点

バージョン0.4.0以降のバージョンには様々な変更点がありますが、最大の変更点はスキーマ定義の変更です。

バージョン0.3.0までではほとんどの情報がEventsテーブルに集中していましたが、これにはデータの重複が増えてしまうという欠点がありました。 また、Eventsテーブルのカラム数が増えれば増えるほど管理が煩雑になってしまうという問題もあります。 そこでバージョン0.4.0からは、イベントに直接関係ない情報は別のテーブルに分けて管理するように改めました。

また、それに伴ってイベントに複数のタグを紐付けられるようにし、イベントの属するscope(GitHubのアクティビティに対応するイベントであれば、リポジトリの情報など)はタグの1つとして管理するようにしました。 これにより、「このイベントはfooというリポジトリに関連付けられており、barというハッカソンの期間中に行われた」というような複数の切り口での分類が可能になりました。

(図:Events, Actors, Tagsの各テーブル同士の関係)

このスキーマ変更によって、これまでにクロール済みのイベント情報はそのままでは使えなくなっています。 後述の手順に則って、データを移行する必要があります。

Sharetary 0.3.0からのアップグレード手順

既に設置済みのSharetaryのバージョンを0.3.0から0.4.0以上に更新する手順は以下の通りです。

  1. SharetaryのHTTPサーバ、クローラを停止する。
  2. スキーマ定義変更およびデータ移行用スクリプトをダウンロードする。
  3. ダウンロードしたmigrate-0.3.0-to-0.4.0を、Bashスクリプトとして実行し、データを移行する。
  4. クローラの設定を、新しいスキーマに合わせて更新する。
  5. クローラの動作を再開させる。
  6. sudo npm install -g sharetaryで最新バージョンのSharetaryをインストールする。
  7. SharetaryのHTTPサーバを起動する。

データ移行用のスクリプトは、以下のオプションを指定できます。

  • -h <hostname> : Groongaサーバ(またはDroongaクラスタ)のホスト名。省略時はlocalhost
  • -p <port> : Groongaサーバ(またはDroongaクラスタ)のHTTPサーバがlistenしているポート番号。省略時は10041
  • -t <追加で設定するタグ> : 移行された全てのイベントに固定で付与するタグ。
  • -d : 指定するとdry-runモードとなり、実際にはデータベースに変更を行わないで、どのような変更が行われるのかをシミュレートする。

例えばGroongaサーバがgroongaというホストの10041番ポートで動作していて、収集済みのイベントに「2015-sezemi-readable-code」というタグを付与して移行する場合、コマンドラインは以下のようになります。

$ wget https://raw.githubusercontent.com/clear-code/sharetary/master/bin/migrate-0.3.0-to-0.4.0
$ chmod +x ./migrate-0.3.0-to-0.4.0
$ ./migrate-0.3.0-to-0.4.0 -h groonga -t 2015-sezemi-readable-code -d

意図しないデータ破壊を避けるためは、まず1回dry-runして、適用される変更を先に把握しておくとよいでしょう。

追加するタグの指定は必須ではありませんが、指定しておくのがお薦めです。 過去の運用で何かのハッカソンに関係するイベントを収集済みなのであれば、そのハッカソンの名前をタグとして追加しておくと、それ以後収集するイベントを古いイベントと区別しやすくなります。

クローラの設定をどのように更新するかは、どんなクローラを使っているかによって変わります。 以下は、先日のエントリで紹介したfluent-plugin-github-activitiesの場合の設定(Fluentdの設定)の解説となります。 設定の参考にしてみて下さい。

Sharetary 0.4.0以降のバージョン向けの、fluent-plugin-github-activitiesの設定

先のエントリでも解説していますが、fluent-plugin-github-activitiesは本質的にはSharetaryとは全く独立した存在です。 fluent-plugin-github-activitiesはGitHubのアクティビティをクロールして取得してきてFluentdのストリームに取り込むだけの物で、それを加工してSharetary用のイベント情報としてデータベースに格納するのは、他のプラグインの仕事という事になります。

ここでは、Sharetaryのリポジトリに含まれている設定例の内容を引き合いに出しながら、fluent-plugin-github-activitiesの出力をどのように加工すればSharetary 0.4.0以降のイベント情報として使えるのかを解説します。

レコードの取得

まず最初にfluent-plugin-github-activities用の<source>の設定ですが、ここは先のエントリで紹介した通りで変化はありません。

<source>
  type github-activities
  access_token access-token-of-github
  users ashie,cosmo0920,kenhys,kou,min-shin,myokoym,okkez,piroor
  clients 4
  interval 1
  base_tag github-activity.
  pos_file /var/log/td-agent/github-activities.json
</source>

これによってレコードとして取り込まれたアクティビティ情報を加工する指定が、この後に続く事になります。

レコードを「イベント情報用」「人物用」「タグ用」の3つに複製し、ストリームを分岐する

FluentdでSharetary用のデータベースにレコードを格納するにはfluent-plugin-groongaを使いますが、fluent-plugin-groonga、特にその中でFluentdのストリームからGroongaサーバへの書き込みを行うout_groongaは、流入してきたレコードを特定のテーブル1つのレコードとして格納するという物です。

しかしSharetary 0.4.0以降用には、1つのイベントにつき、イベント情報そのものを格納するEventsテーブル、人物のメタ情報を格納するActorsテーブル、タグのメタ情報を格納するTagsテーブルという具合に、最大で3つのテーブルに同時にレコードを追加する必要があります。

(図:Events, Actors, Tagsの各テーブル同士の関係)

そこで、ストリームを「イベント用」「人物用」「タグ用」の3つに分岐して、それぞれ別々の設定のfluent-plugin-groongaによってレコードとして各テーブルに格納することになります。

(図:Events, Actors, Tagsの各テーブル用にストリームを分岐する様子)

このようなストリームの分岐には、Fluentdに標準添付されているcopyというプラグインを使います。

<match github-activity.**>
  type copy

  # Eventsテーブル用のレコード
  <store>
    type        record_reformer
    enable_ruby false
    tag         event.${tag}
  </store>

  # Actorsテーブル用のレコード
  <store>
    type        record_reformer
    enable_ruby false
    tag         actor.${tag}
  </store>

  # Tagsテーブル用のレコード
  <store>
    type        record_reformer
    enable_ruby false
    tag         repository-tag.${tag}
  </store>
</match>

copyプラグインは、<store>に書かれた別のプラグイン用の指定に基づいて加工を行ったレコードを、Fluentdのストリームに出力します。 この例では、レコードを加工するプラグインであるfluent-plugin-record-reformerを使って、それぞれのタグ名を「何に使うためのレコードか?」という事を示す物に変えてから出力しています。 3つの<store>が書かれているため、これでストリームは3つに分かれることになります。

人物用のレコードを作成し、Actorsテーブルに格納する

話を単純にするために、外部テーブルの方から解説していきます。 まずはActorsテーブルです。

このテーブルのレコードは_keyが人物の名前で、その人物のプロフィールページのURIとなるuri、アイコン画像のicon、人物の重要度を示すclassの3つのフィールドを持ちます。

copyプラグインによって分岐された3つのストリームのうち、Actorsテーブル用に出力しているレコードについて、fluent-plugin-record-reformerの変換ルールを以下のように更新し、上記のフィールドを持ったレコードを出力するようにします。

<match github-activity.**>
  type copy
  ...
  # Actorsテーブル用のレコード
  <store>
    type         record_reformer
    enable_ruby  true
    renew_record true
    tag          sharetary.actor
    <record>
      _key  ${(actor || committer)["login"]}
      uri   https://github.com/${(actor || committer)["login"]}/
      icon  ${self["$github-activities-related-avatar"]}
      class major
    </record>
  </store>
  ...
</match>

この時の設定のポイントは以下の通りです。

  • Acotrsテーブル用のレコードはゼロから組み立てるので、renew_record trueとして、加工元レコードのフィールドを引き継がないようにしています。
  • enable_ruby trueは、セキュリティや速度などの点でデメリットがありますが、今回の用途では必要な設定です。 というのも、fluent-plugin-record-reformerは、プレースホルダーを使って加工元のレコードのフィールドの値を参照できますが、fluent-plugin-github-activitiesが出力するレコード(GitHubのAPIが返すJSONそのままの形式)のようにそれぞれのフィールドが階層化されている場合、欲しい情報を取り出す事ができません。 ですので、必要な情報を参照できるようにするために、この設定を有効化しています。
  • Actorsテーブル用のレコードにはアクティビティの種類を示す情報は必要なくなるので、タグはsharetary.actorに統一しています。
  • 出力するレコードのフィールドのうち_keyuriは、どちらも元のレコードに含まれているユーザアカウント名を使いたいのですが、個々のコミットに対応するレコードと、それ以外のアクティビティのレコードとでは、持っている情報の構成が若干異なるため、参照先を決め打ちできません。 ですので、ここでは||を使って、レコードに存在する方のフィールドを取り出しています。
  • アイコン画像のURIを参照するために、fluent-plugin-github-activitiesが独自に出力している$github-activities-related-avatarというフィールドを参照していますが、値を参照するために${$github-activities-related-avatar}と書くとエラーになってしまいます。これは、フィールド名の先頭の$が特別な意味を持ってしまうためです。ここでは、各フィールドの値を保持している名前空間オブジェクトのOpenStruct自身の[]メソッドを明示的に呼び出す事で、フィールドの値を参照しています。
  • classフィールドの値は、簡単のためmajorに決め打ちしています。元レコードの情報に基づいてclassの値を適宜切り替えると、「受講者」と「メンター」のように、人物によってイベントの表示の仕方に強弱を付ける事もできます。

次は、できあがったレコードをfluent-plugin-groongaを使ってActorsテーブルに格納するための設定です。

<match sharetary.actor>
  type groonga
  store_table Actors

  protocol http
  host localhost

  buffer_type file
  buffer_path /var/spool/td-agent/buffer/groonga-sharetary-actor
  flush_interval 1s

  <table>
    name Actors
    flags TABLE_HASH_KEY
    key_type ShortText
  </table>

  <mapping>
    name uri
    type ShortText
  </mapping>

  <mapping>
    name icon
    type ShortText
  </mapping>

  <mapping>
    name class
    type ShortText
  </mapping>
</match>
  • このテーブルの情報はSharetaryでは直接検索する対象にはしないので、インデックスは作っていません。
  • buffer_pathに指定するパスは、この設定のための固有のパスにする必要があります。 他のテーブル用のfluent-plugin-groongaの設定との間でこの設定の値が衝突すると、どちらか片方の設定しか有効化されませんので、注意して下さい。

これで、人物の情報がアイコン画像のURIなどのメタ情報を伴って保存されるようになります。

タグ用のレコードを作成し、Tagsテーブルに格納する

人物の次は、イベントのタグ付けのためのタグそのものを格納するTagsテーブルです。

fluent-plugin-github-activitiesで取得したアクティビティから取り出せる情報を使って適切なタグを自動設定すれば、収集したイベントを効率よく分類できるようになります。 最もベタな例は、「そのアクティビティに関連しているリポジトリ」という切り口でのタグ付けでしょう。

このテーブルのレコードは_keyがタグの名前で、タグ固有のアイコン画像を示すiconというフィールドを持ちます。 copyプラグインによって分岐された3つのストリームのうち、Tagsテーブル用に出力しているレコードについて、fluent-plugin-record-reformerの変換ルールを以下のように更新し、上記のフィールドを持った「そのアクティビティに関連しているリポジトリ」のタグにあたるレコードを出力するようにします。

<match github-activity.**>
  type copy
  ...
  # Tagsテーブル用のレコード
  <store>
    type         record_reformer
    enable_ruby  true
    renew_record true
    tag          sharetary.tag
    <record>
      _key  ${tag.end_with?(".commit") ? url.match(/repos\/([^\/]+\/[^\/]+)\/commits/)[1] : tag.end_with?(".fork") ? payload["forkee"]["full_name"] : repo["name"]}
      icon  ${self["$github-activities-related-organization-logo"]}
    </record>
  </store>
</match>
  • Acotrsテーブル用のレコードと同様に、Tags用のレコードもゼロから組み立てるので、renew_record trueとしています。
  • 先述の通り、複雑な構造のレコードから必要な情報を取り出す必要があるので、enable_ruby trueとしています。
  • Tagsテーブル用のレコードにもアクティビティの種類を示す情報は不要なので、タグはsharetary.tagに統一しています。
  • 出力するレコードのフィールドのうち_keyの値は、ほとんどのアクティビティではrepo["name"]で参照できますが、コミットに対応するレコードとForkに対応するレコードだけは違う値を使う必要があります。ここでは、3項演算子を使って、元レコードのどのフィールドの値を使うかを振り分けています。

次は、できあがったレコードをfluent-plugin-groongaを使ってTagsテーブルに格納するための設定です。

<match sharetary.tag>
  type groonga
  store_table Tags

  protocol http
  host localhost

  buffer_type file
  buffer_path /var/spool/td-agent/buffer/groonga-sharetary-tag
  flush_interval 1s

  <table>
    name Tags
    flags TABLE_HASH_KEY
    key_type ShortText
  </table>

  <mapping>
    name icon
    type ShortText
  </mapping>
</match>
  • このテーブルもSharetaryでは検索対象にしないので、インデックスは作っていません。
  • 先述の通り、buffer_pathに指定するパスはこの設定のための固有のパスにする必要があります。

これで、タグの情報も保存できるようになりました。

イベント用のレコードを作成し、Eventsテーブルに格納する

最後に、イベントそのものの情報を格納するEventsテーブルです。

GitHubのアクティビティは様々な種類があるため、Sharetary用のイベント情報に変換するためのルールはアクティビティの種類分だけ定義する必要があります。 全てのアクティビティについて完全な変換ルールを定義するのは煩雑なので、共通化できる所はなるべく共通化する方向で設定を組み立てていく事にします。

共通フィールドを埋める

まず最初に、多くのアクティビティに共通している情報を使って共通のフィールドを埋めます。

copyプラグインによって分岐された3つのストリームのうち、Eventsテーブル用に出力しているレコードについて、fluent-plugin-record-reformerの変換ルールを以下のように更新し、共通のフィールドを埋めた状態でレコードを出力するようにします。

<match github-activity.**>
  type copy

  # Eventsテーブル用のレコード
  <store>
    type        record_reformer
    enable_ruby true
    tag         event.${tag}
    <record>
      type        ${tag_suffix[-1]}
      actor       ${actor && actor["login"]}
      source_icon https://github.com/favicon.ico
      timestamp   ${created_at}
      created_at  ${time}
      _repository ${repo && repo["name"]}
    </record>
  </store>
  ...
</match>

設定のポイントは以下の通りです。

  • この後でアクティビティごとに個別の変換ルールを適用するにあたって、元のレコードの各フィールドをその時に参照したいので、今度はrenew_record trueは指定していません。 <record>で定義した各フィールドは、元のレコードへのフィールド追加、または同名フィールドの値の置き換えになります。
  • イベントの種類を示すtypeフィールドの値は、fluent-plugin-github-activitiesが出力したレコードがgithub-activity.pushのようなタグになっている事を利用して、タグの最後のパートをそのまま使います。 tag_suffix[-1]というプレースホルダを使うと、タグ名の最後のパートを簡単に参照できます。
  • イベントの主体となった人物を示すactorフィールドの値はActorsテーブルに格納したレコードの_keyと同じ値を設定する必要があります。 大多数のアクティビティではこれはactor["login"]の値ですが、個々のコミットに対応するレコードでだけは、違うフィールドを参照しなくてはなりません。 そのような特例については、個々のコミットに対応するレコードの変換ルールを定義するときにまとめて処理する事にして、ここではシンプルに「フィールドを参照できるならその値を使い、参照できなければnilにしておく」という書き方にしています。
  • イベントの由来となっているサービスを表すsource_iconの値は、GitHubのfaviconのURIで決め打ちにしています。 他のサービス由来のイベントも収集する場合は、適切な値をその都度指定しましょう。
  • イベントの日時を示すtimestampフィールドの値は、元レコードのcreated_atの値があればそれを使い、無ければnilにするという書き方にしています。 これも、例外となるアクティビティについては別途値を設定します。
  • イベント情報が記録された日時を示すcreated_atフィールドの値は、現在時刻の代替として、Fluentdのレコードに紐付けられている日時情報を参照しています。 (※元レコードのcreated_atと名前が同じですが、意味合いが異なる物で、こちらはあくまでSharetary用のEventsテーブルのカラム名です。)
  • イベントへのタグ付けのために、Tagsテーブルに格納したレコードの_keyと同じ値をtagsフィールドに設定する必要があります。 これもactor同様に、アクティビティの種類によって参照先のフィールドを変えなくてはなりません。 ただ、tagsフィールドの値は後から少し加工をしたいので、ここでは一旦_repositoryという仮のフィールドに値を設定するに留めています。 (後加工の段階で、これを使ってtagsフィールドの値を埋める事になります。)
アクティビティの種類ごとに固有のフィールドを埋める

次に、個々のアクティビティ用の変換ルールを定義します。

例えば、git pushに対応するイベントの変換ルールは、fluent-plugin-record-reformerを使って以下のように書けます。

<match event.github-activity.push>
  type         record_reformer
  enable_ruby  true
  renew_record true
  keep_keys    type,actor,source_icon,timestamp,created_at,_repository
  tag          sharetary.event
  <record>
    _key        https://github.com/${repo["name"]}/compare/${payload["before"]}...${payload["head"]}
    class       minor
    title       Push
    description Pushed ${payload["size"].to_s} commits to ${repo["name"]}
    uri         https://github.com/${repo["name"]}/compare/${payload["before"]}...${payload["head"]}
    reply_uri   https://github.com/${repo["name"]}/compare/${payload["before"]}...${payload["head"]}#commits_bucket
  </record>
</match>
  • この段階では共通のフィールドが埋まった状態のレコードが渡ってくるため、ここではpushに固有の情報を埋めるだけの変換ルールになっています。
  • Eventsテーブル用のレコードはfluent-plugin-github-activitiesが出力するレコードとは全く異なる形式になるため、renew_record trueとして、ゼロからレコードを組み立てるようにしています。 ただし、前の段階ですでに加工を終えた共通のフィールドについてだけは、keep_keys type,actor,source_icon,timestamp,created_at,_repositoryとして値を引き継いでいます。 このようにする事で、「前の段階で共通のフィールドを埋めておいて、この段階では固有のフィールドだけを埋める」という効率の良い設定が可能となります。
  • Eventsテーブル用のレコードとして形式を変換済みということで、タグはsharetary.eventに統一しています。
  • push以外にも、Issueの投稿、コメントの投稿など、多くのアクティビティの変換ルールは同様の要領で定義する事ができます。

別の例として、git commitに対応するイベントの変換ルールも示します。

<match event.github-activity.commit>
  type         record_reformer
  enable_ruby  true
  renew_record true
  keep_keys    type,source_icon,created_at
  tag          sharetary.event
  <record>
    _key        ${html_url}
    class       normal
    title       Commit ${stats["total"].to_s} changes
    description ${commit["message"]}
    extra_description ${files.collect{|file| "#{file["filename"]} (#{file["status"]})" }.join("\n, ")}\n\n${files.collect{|file| file["patch"] }.join("\n\n")}
    actor       ${committer["login"]}
    uri         ${html_url}
    reply_uri   ${html_url}#new_commit_comment_field
    timestamp   ${commit["committer"]["date"]}
    parent      ${self["$github-activities-related-event"] && "https://github.com/#{self["$github-activities-related-event"]["repo"]["name"]}/compare/#{self["$github-activities-related-event"]["payload"]["before"]}...#{self["$github-activities-related-event"]["payload"]["head"]}" || "" }
    _repository ${url.match(/repos\/([^\/]+\/[^\/]+)\/commits/)[1]}
  </record>
</match>
  • 基本的にはpushの場合と同じなのですが、keep_keysでそのまま流用している共通フィールドが、type,source_icon,created_atだけになっています。 これは、commitのイベントの元となるGitHub APIの戻り値が、ここまでで参照してきたアクティビティの共通フォーマットとは異なる形式を取っているからです。 actor, timestamp, _repositoryの各フィールドの値は、ここで改めて、commit専用のフォーマットになっているレコードから適切な値を参照するようにしています。

Sharetaryリポジトリにある完全版の設定ファイルのサンプルには、fluent-plugin-github-activitiesが対応している他のアクティビティも含めた全ての変換ルールが記述されています。 そちらも併せてご参照下さい。

データベース格納前の微調整を行う

アクティビティごとの変換を終えたら、今度はレコードの内容の微調整のための後処理を行います

まず、仮フィールドの_repositorytagsフィールドに変換して、イベントをタグ付けします。 これは、Fluentdに標準添付のrecord_transformerプラグインを使って、以下のような<filter>で実現できます。

<filter sharetary.event>
  type        record_transformer
  enable_ruby false
  remove_keys _repository
  <record>
    tags ["${_repository}"]
  </record>
</filter>

record_transformerの設定は、元になったfluent-plugin-record-reformerと同じような書式で記述します。

  • tagsフィールドの値を、_repositoryフィールドの値が単一の要素になっている配列として定義しています。
  • それと同時に、不要になった仮フィールドの_repositoryを、remove_keys _repositoryでレコードから削除しています。

これだけだと、この段階で変換を行う意味はあまり無いように見えます。 実際、共通フィールドを埋める段階や、各アクティビティ固有のフィールドを埋める段階でも、tagsフィールドを配列として定義することは可能です。

しかし、このようにtagsフィールドの変換を最後の段階に分けておくと、元の情報に含まれていなかった情報を使った、固定のタグの追加が可能になります。 例えば下のようにすれば、各イベントには関連するリポジトリ名のタグだけでなく、sezemi-2015-readable-codeというタグも固定で付与されるようになります。

<filter sharetary.event>
  type         record_transformer
  enable_ruby  false
  remove_keys  _repository
  <record>
    tags ["${_repository}", "sezemi-2015-readable-code"]
  </record>
</filter>

SEゼミの2015年度の取り組みでは4つのサブイベントを開催しますが、サブイベントの名前のタグをこのようにして自動付与すれば、後からサブイベントごとの情報を効率よく検索できるようになります。 また、1つのサブイベントが終わったら上記の後加工段階のフィルタだけを設定し直す事で、参加メンバーが同じだとしても、別のサブイベントの情報を簡単に識別できます。

(最初からtagsフィールドを配列型にしておいて、後処理の段階でtagsの配列に要素を追加する、という形にできればそれが最もスマートなのですが、残念ながら、そのような変換を行えるFluentdプラグインは現時点では無い模様です……そこで、次善の策としてこのようなやり方を考えてみた次第です。)

タグ付けが終わったら、最後の仕上げとして、日時を示すフィールドの値をunixtimeを表す整数値に変換します。 これは、fluent-plugin-filter_typecastプラグインを使って以下のように実現できます。

<filter sharetary.event>
  type  typecast
  types timestamp:time,created_at:time
</filter>

これによって、timestampcreated_atのフィールドの値が、2015-06-15T00:00:00のような文字列から、対応するUTCのunixtimeに変換されます。 Groongaでは日時はunixtimeの整数で表現されますので、この変換は必ず行わなくてはなりません。 (でないと、日付の範囲を指定した絞り込みができません。)

イベント情報をEventsテーブルに格納する

後処理を終えて全てのフィールドが揃ったら、後はEventsテーブルにレコードを格納するだけです。

<match sharetary.event>
  type groonga
  store_table Events

  protocol http
  host localhost

  buffer_type file
  buffer_path /var/spool/td-agent/buffer/groonga-sharetary-event
  flush_interval 1s

  <table>
    name Events
    flags TABLE_HASH_KEY
    key_type ShortText
  </table>

  <table>
    name Timestamps
    flags TABLE_PAT_KEY
    key_type Time
  </table>

  <table>
    name Tags
    flags TABLE_HASH_KEY
    key_type ShortText
  </table>

  <table>
    name Actors
    flags TABLE_HASH_KEY
    key_type ShortText
  </table>

  <table>
    name Terms
    flags TABLE_PAT_KEY
    key_type ShortText
    default_tokenizer TokenBigram
    normalizer NormalizerAuto
  </table>

  <mapping>
    name type
    type ShortText
  </mapping>

  <mapping>
    name class
    type ShortText
  </mapping>

  <mapping>
    name title
    type ShortText
    <index>
      table Terms
      name Events_title_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name description
    type Text
    <index>
      table Terms
      name Events_description_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name extra_description
    type Text
    <index>
      table Terms
      name Events_extra_description_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name tags
    type Tags
    <index>
      table Tags
      name Events_tags_index
    </index>
  </mapping>

  <mapping>
    name uri
    type ShortText
    <index>
      table Terms
      name Events_uri_index
      flags WITH_POSITION
    </index>
  </mapping>

  <mapping>
    name source_icon
    type ShortText
  </mapping>

  <mapping>
    name reply_uri
    type ShortText
  </mapping>

  <mapping>
    name actor
    type Actors
    <index>
      table Actors
      name Events_actor_index
    </index>
  </mapping>

  <mapping>
    name timestamp
    type Time
    <index>
      table Timestamps
      name Events_timestamp_index
    </index>
  </mapping>

  <mapping>
    name created_at
    type Time
    <index>
      table Timestamps
      name Events_created_at_index
    </index>
  </mapping>

  <mapping>
    name parent
    type ShortText
    <index>
      table Terms
      name Events_parent_index
      flags WITH_POSITION
    </index>
  </mapping>
</match>
  • buffer_pathに指定するパスをこの設定のための固有のパスにしている点に注意して下さい。
  • 処理の進行状況によっては、人物やタグのレコードが登録される前にイベントのレコードが登録される事があり得ます。 そのような場合も想定して、ここには外部の参照先であるActorsTagsの両テーブルの定義も含めています。

まとめ

ここまでで設定したすべてのプラグインの関係を図にすると、以下のようになります。

(図:登場した全てのプラグインの関係)

以上、Sharetary 0.3.0をSharetary 0.4.0以降にバージョンアップする際の手順と、Sharetary 0.4.0以降向けにGitHubのアクティビティをクロールするためのFluentdの設定の仕方を解説しました。 その中で、Fluentdのストリームから1つの入力を分岐して複数テーブルにレコードを追加するfluent-plugin-groonga向けの設定の仕方も解説しました。

先日のリーダブルコード勉強会ではまだお試し運用でしたが、今月27日のOSS Hack 4 Beginners来月に予定されているOSS Hack Weekendでは、会場内で行われる開発の様子を横断して眺められることでしょう。 これらのイベントに参加を予定されている方は、会場内のサブスクリーンにもご注目ください。

また、クリアコード関係者の公開アクティビティを収集しているSharetary運用サンプルも引き続き運用しております。 クリアコードの普段の開発の様子も是非ご覧になってみて下さい。

2015-06-18

Groongaがキャッシュしている内容を確認する方法

はじめに

Groongaにはcache_limitというコマンドがあります。 cache_limit を使うと、最新のN件の select コマンドの結果をキャッシュさせることができます。

しかし、実行した select コマンドをキャッシュしたのかどうかや、今どんな内容がキャッシュされているかについてはわかりません。 そこで、今回はプラグインを導入することで、簡単にGroongaのキャッシュの中身を覗く方法を紹介します。

プラグインをインストールしよう

/usr/local 以下にGroongaをインストールしてあるという前提で説明します。

% git clone https://github.com/kenhys/groonga-plugin-grncache.git
% cd groonga-plugin-grncache.git
% ./autogen.sh
% ./configure --with-groonga-source-dir=PATH_TO_GROONGA_SOURCE_DIRECTORY
% make
% sudo make install

PATH_TO_GROONGA_SOURCE_DIRECTORY にはソースアーカイブのパスを指定します。*1

この記事を書いている時点では、Groonga 5.0.4が最新ですので、もし /tmp 以下にソースアーカイブを展開しているなら、configure の指定は次のようにします。

% ./configure --with-groonga-source-dir=/tmp/groonga-5.0.4

インストールまで完了すると、/usr/local/lib/groonga/plugins/grncache/grncache.so が存在するはずです。

古いバージョンのGroongaと一緒に試す場合、Groonga 3.0.8*2以降であればこのプラグインが動作するはずです。この制限はGrncacheが使用しているAPIが3.0.8以降に追加されたためです。

キャッシュの中身を覗いてみよう

プラグインを登録する

実際にキャッシュの中身を覗いてみるまえに、プラグインを登録しておきましょう。

プラグインの登録には plugin_register コマンド *3 を実行します。 Groongaを対話的に起動している場合は次のようにします。

> plugin_register grncache/grncache

HTTPサーバーとして起動している状態なら、次のようにします。

% curl http://localhost:10041/d/plugin_register?name=grncache/grncache
動作確認用のデータを登録する

動作を確認するのに、サンプルとなるデータを登録しておきましょう。次のコマンドをGroongaの対話モードで実行します。

> table_create Site TABLE_HASH_KEY ShortText
> column_create --table Site --name title --type ShortText
> load --table Site
[
{"_key":"http://example.org/","title":"This is test record 1!"},
{"_key":"http://example.net/","title":"test record 2."},
{"_key":"http://example.com/","title":"test test record three."},
{"_key":"http://example.net/afr","title":"test record four."},
{"_key":"http://example.org/aba","title":"test test test record five."},
{"_key":"http://example.com/rab","title":"test test test test record six."},
{"_key":"http://example.net/atv","title":"test test test record seven."},
{"_key":"http://example.org/gat","title":"test test record eight."},
{"_key":"http://example.com/vdw","title":"test test record nine."},
]
キャッシュされたかを確認する

キャッシュの状態を確認するには、grncache status を実行します。

> grncache status
[[0,1435303725.13405,0.000181674957275391],{"cache_entries":0,"max_cache_entries":100,"cache_fetched":0,"cache_hit":0,"cache_hit_rate":0.0}]

それぞれの項目の内容は次の通りです。

項目 説明
cache_entries キャッシュされた検索結果の数
max_cache_entries キャッシュ可能な検索結果の最大数。cache_limit で変更できる
cache_fetched キャッシュを参照した回数。select を実行するたびに増加する
cache_hit キャッシュにヒットした回数
cache_hit_rate キャッシュにヒットした割合 (cache_hit/cache_fetchedで算出される)

初期状態なので max_cache_entriesを除いてすべて0です。

キャッシュの中身をダンプすることもできます。それには、grncache dump を実行します。初期状態なのでこちらも0件です。

> grncache dump
[[0,1435303923.9083,8.46385955810547e-05],[[0]]]

では、select コマンドを実行してみましょう。

> select Site
[[0,1435303997.02583,0.000574111938476562],[[[9],[["_id","UInt32"],["_key","ShortText"],["title","ShortText"]],[1,"http://example.org/","This is test record 1!"],[2,"http://example.net/","test record 2."],[3,"http://example.com/","test test record three."],[4,"http://example.net/afr","test record four."],[5,"http://example.org/aba","test test test record five."],[6,"http://example.com/rab","test test test test record six."],[7,"http://example.net/atv","test test test record seven."],[8,"http://example.org/gat","test test record eight."],[9,"http://example.com/vdw","test test record nine."]]]]

grncache status を実行してみると、cache_entries が 1に増え、select Site の結果がキャッシュされたことがわかります。

> grncache status
[[0,1435304001.04143,0.000191450119018555],{"cache_entries":1,"max_cache_entries":100,"cache_fetched":1,"cache_hit":0,"cache_hit_rate":0.0}]

キャッシュの中身をダンプしてみましょう。

> grncache dump
[[0,1435304009.74493,0.000130653381347656],[[1],[{"grn_id":1,"nref":0,"timeval":"2015-06-26 16:33:17.025827","value":"[[[9],[[\"_id\",\"UInt32\"],[\"_key\",\"ShortText\"],[\"title\",\"ShortText\"]],[1,\"http://example.org/\",\"This is test record 1!\"],[2,\"http://example.net/\",\"test record 2.\"],[3,\"http://example.com/\",\"test test record three.\"],[4,\"http://example.net/afr\",\"test record four.\"],[5,\"http://example.org/aba\",\"test test test record five.\"],[6,\"http://example.com/rab\",\"test test test test record six.\"],[7,\"http://example.net/atv\",\"test test test record seven.\"],[8,\"http://example.org/gat\",\"test test record eight.\"],[9,\"http://example.com/vdw\",\"test test record nine.\"]]]"}]]

[[1], [{"grn_id":1,"nref":0,"timeval":...,"value":... という箇所があるのがわかります。 [1] というのがキャッシュエントリの数です。 "value":... というのがキャッシュされた検索結果です。先程実行した select Site と結果が一致していることがわかります。

もう一度同じ検索をした後だとどうなるでしょうか。

> grncache status
[[0,1435304396.55938,0.000149011611938477],,{"cache_entries":1,"max_cache_entries":100,"cache_fetched":2,"cache_hit":1,"cache_hit_rate":50.0}]

予想通り、cache_fetchedcache_hit が増え、キャッシュヒット率が 50% となりました。同じ検索なので、キャッシュ済みの内容をそのまま返してきたことがわかります。

まとめ

Groongaがキャッシュしている内容をプラグインを使って確認する方法を紹介しました。 今どんな内容がGroongaによってキャッシュされているのかを確認したいときには参考にしてみてください。

*1 GrncacheはGroongaの内部構造に強く依存しているため、ソースコードを参照する必要があります。

*2 ちなみにGroonga 3.0.8がリリースされたのは、2013年9月29日のこと。

*3 Groonga 5.0.0以前の場合には register コマンド

タグ: Groonga
2015-06-26

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|