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

ククログ


Firefox 52以降でのルート証明書の自動インポート機能でできること、できないこと

Firefox ESR52以降のバージョンでは、隠し設定のsecurity.enterprise_roots.enabledtrueに設定することで、Windowsの証明書ストアに登録されたルート証明書をFirefox側で認識して使えるようになりました。

ただ、この機能の背景や使い方についてユーザー側の期待と実際の挙動との間に若干の齟齬が見られるため、ここで改めて状況を整理してみます。

この機能を有効にすると、以下の事を実現できます。

  • 組織内ネットワークに設置したSSLプロキシやSSLロガーの使用に必要な専用のルート証明書をFirefoxで認識させる。
  • 組織内ネットワークで運用しているサーバーの証明書専用のルート証明書をFirefoxで認識させる。

それに対し、以下のことは依然としてできません

  • Windowsの証明書ストアに元から含まれている証明書、例えば政府認証基盤のルート証明書をFirefoxで認識させる。 (「Firefoxでは日本の官公庁関係のWebサイトの一部が証明書エラーで見られない」という問題を解消する。)
  • 管理者でないユーザーがWindowsの証明書ストアに追加した証明書をFirefoxでも認識させる。

何故こうなっているのかは、機能の背景を知ることで理解できます。

この機能の導入経緯

上記の設定が導入されたBugを見ると、これは「エンタープライズでFirefoxを使いやすくする」という文脈に基づく機能だということが読み取れます。

「エンタープライズ」とは、従業員規模が千人や万人といった単位に達するような大規模な組織での使用ということです。このような規模の組織では組織内専用のルート証明書が必要になる事がままあり、それをFirefoxで使うためには証明書のインポート機能を提供するアドオンを使うか、集中管理の仕組みの実装の裏をかいて強引にインポートさせるかしかありませんでした。ところが、現在Firefoxは古い基盤技術からの脱却を進めているため、これらの裏技的なやり方は早晩使えなくなる見込みが立っています。そこで、裏技ではなくきちんとした正当な機能としてルート証明書をインポートする方法を設ける必要があった、というのがこれらのBugの背景にある事情です。

実際の動作

「Windowsの証明書ストア」は、実際にはそれ専用のデータベースがあるわけではありません。Windowsのレジストリ内には以下のようなレジストリキーの配下に証明書の情報が分散して格納されており、それらをマージして一覧表示した物を「証明書ストア」として見せているということになります。

  • HKEY_CURRENT_USER\Software\Microsoft\SystemCertificates
  • HKEY_LOCAL_MACHINE\Software\Microsoft\SystemCertificates
  • HKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates
  • HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates

エンタープライズ運用における「ルート証明書の追加」とは、Active Directoryなどを使って、これらの中で特に以下の位置に証明書を登録する事を指しています。

  • HKEY_LOCAL_MACHINE\Software\Microsoft\SystemCertificates\Root\Certificates
  • HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates\Root\Certificates
  • HKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates\Root\Certificates

このことから、Firefoxは前述の隠し設定が有効な場合、起動時に上記のレジストリキー配下を走査して、追加された証明書があればそれらをインポートするという設計となっています。

ただし、この機能によってインポートされた証明書は、Firefox自身の証明書ストアに永続的に保存されるわけではありません。複数のレジストリキーをマージした結果がWindowsの証明書ストアとして扱われるのと同様に、Firefoxでも「Firefox自身の元々の証明書ストアの内容」+「この機能によって認識されたWindowsの証明書ストア内の証明書」が、実際の認証処理における証明書ストアとして使われるという形となります。(そのため、不要になった証明書はWindowsの証明書ストアから削除するだけで、Firefoxからも認識されなくなります。Firefoxの証明書マネージャで証明書を削除する、という事をする必要はありません。)

動作確認の方法

この機能でインポートされた証明書はFirefoxの証明書マネージャには表示されないため、期待通りに証明書がインポートされているかどうかは、デバッグ用の詳細なログを見て判断する必要があります。

MCD用設定ファイルを使う場合には、例えば以下のようにします。

// 証明書のインポート機能を有効化する設定
lockPref('security.enterprise_roots.enabled', true);

// NSS(Firefoxのセキュリティモジュール)のログを出力するための設定
lockPref("logging.pipnss", 5);
lockPref("logging.config.LOG_FILE", "C:\\Users\\Public\\certlog.txt");
lockPref("logging.config.add_timestamp", true);
lockPref("logging.config.clear_on_startup", false);
lockPref("logging.config.sync", true);

この設定を反映した上で、実験用のダミーの証明書をHKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates\Root\Certificates配下に加えるレジストリファイルを使って「example.com」という名前の証明書をインポートした状態でFirefoxを起動すると、C:\Users\Public\certlog.txtに出力されたログに以下のような箇所が含まれるようになります。

...
2017-05-09 10:21:23.017000 UTC - [Main Thread]: D/pipnss certificate is trust anchor for TLS server auth
2017-05-09 10:21:23.017000 UTC - [Main Thread]: D/pipnss Imported 'example.com'
2017-05-09 10:21:23.017000 UTC - [Main Thread]: D/pipnss imported 1 roots
...

このD/pipnss Imported '(証明書の一般名)'というログが出ていれば、その証明書はFirefoxから見えています。逆に、そのようなログが現れていない場合、証明書は無視されているということになります。

ユーザーが自分で操作してWindowsの証明書ストアにインポートした証明書をFirefoxが認識しない理由

Windowsでルート証明書のファイルをダウンロードしてダブルクリックすると、その証明書を証明書ストアにインポートすることができます。しかし、この方法でインポートされた証明書は上記のFirefoxの機能では認識されません。何故でしょうか。

これは、証明書が保存されるレジストリ上の位置に理由があります。この方法で手動でインポートした証明書は、HKEY_CURRENT_USER配下の以下の位置に保存されます。

  • HKEY_CURRENT_USER\Software\Microsoft\SystemCertificates

それに対し、Firefoxの証明書インポート機能はHKEY_LOCAL_MACHINE配下の以下のキーのみを走査します。エンタープライズ向けの機能としてはそれで正解で、個々のユーザーがWindowsの証明書ストアに追加した物まで認識するのはお門違いだからです。

  • HKEY_LOCAL_MACHINE\Software\Microsoft\SystemCertificates\Root\Certificates
  • HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates\Root\Certificates
  • HKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates\Root\Certificates

政府認証基盤の証明書など、「Firefoxの証明書ストアには無いがWindowsの証明書ストアには含まれている」証明書がインポートされない理由と、その対処

2017年6月1日現在、政府認証基盤(GPKI)のルート証明書はFirefoxの証明書ストアには含まれていません。要望は上がっているのですが、政府側の対応がMozillaの定めるルート証明書の登録基準を満たしていないために作業が滞っているという状態です。

Windowsの証明書ストアには政府認証基盤の証明書も含まれているため、「Windowsの証明書ストアのルート証明書をFirefoxでインポートできるようになったのなら、政府認証基盤の証明書もインポートされて、証明書のエラーに悩まされることもなくなるのでは?」と期待される方もいらっしゃることでしょう。

ですが残念ながら、この機能では政府認証基盤のルート証明書はインポートされません。これは、Firefoxの証明書インポート機能の処理対象があくまで「管理者によってWindowsの証明書ストアに追加された証明書」に限られているからです。

Bugzilla上のコメント等で度々言及されていますが、Mozillaはユーザーの安全を守るために、どのルート証明書を登録するかを独自に判断しており、他社の判断を鵜呑みにはしないというポリシーで証明書ストアを管理しています。FirefoxがWindowsの全ての証明書を無条件で信頼してしまっていては、このポリシーが無意味になり、ユーザーの安全やプライバシーを保護するための判断を全てMicrosoftに委ねることになってしまいます。ですので、Mozillaとしてはあくまでユーザー(エンタープライズの文脈では、組織のシステム管理者)により追加された証明書のみをインポート対象にするというのが、この機能の趣旨となっています。

なお、以上のような趣旨のため、政府認証基盤(GPKI)のルート証明書を改めて「システム管理者が追加した証明書」としてWindowsの証明書ストアに登録すれば、これはFirefoxのインポート対象となります。

まとめ

以上、Firefox 52以降で使えるWindowsの証明書ストアからのルート証明書のインポート機能について、詳細と検証手順をご案内しました。

弊社で取り扱うFirefoxの導入・サポート案件においても、ルート証明書のインポートについては度々ご相談を頂いています。ニーズの高い機能でありながら今まで対応が進んでこなかったのには、アドオンや裏技的な方法で強引に解決してしまえるからという言い訳が立っていたからという側面は否定できないでしょう。従来からあるアドオンが切り捨てられるという方針の転換には批判の声も多く挙がっていますが、本体で対応される事が望ましい機能が本来あるべき形で実装されるきっかけとなったという事で、ここは素直に喜んでおきたいところです。

ちなみに、Firefox 52の段階ではこの機能はWindows限定の物となっています。macOSやLinuxでは設定を有効化しても効果を得られませんのでご注意下さい。

タグ: Mozilla
2017-06-01

RubyKaigiのCFPへの応募例 #rubykaigi

須藤です。関西Ruby会議2017が終わってからRubyKaigi 2017の発表を応募しました。

RubyKaigi 2017CFPがでています。CFPとはもともと(?学会の文脈で)はCall For Papersの略ですが、RubyKaigiの文脈ではCall For Proposalsの略で、「RubyKaigiでの発表を募集しています」という意味です。RubyKaigi 2017の発表の応募は6月17日まで受け付けています。

応募する人が増えるといいなぁと思うので、実際の応募例として私のRubyKaigi 2015からRubyKaigi 2017の分の応募内容を紹介します。どうしてRubyKaigi 2015の分からかというと、CFPアプリケーションができて応募の記録が残るようになったのがRubyKaigi 2015からだからです。

項目

実際の応募内容を紹介する前に、どのような項目を書く必要があるかを紹介します。

  • Title: 発表のタイトル(60文字以下。英語。)
  • Abstract: 発表の概要(600文字以下。英語。)
  • Details: 発表の詳細(日本語可日本語でも検討してもらえるが、RubyKaigiは国際カンファレンスなので英語が望ましい。)
  • Pitch: どうして自分がRubyKaigiでこの発表をするべきなのかの説明(日本語可日本語でも検討してもらえるが、RubyKaigiは国際カンファレンスなので英語が望ましい。)

TitleにはRubyKaigiに参加する人が「どの発表を聞くか」をパッと選ぶときに参考になりそうなものを書きます。

AbstractにはRubyKaigiに参加する人が「どの発表を聞くか」を検討するときに参考になりそうな説明を書きます。

Detailsには発表を選考する人が「どの発表を選ぶか」を検討するときに参考になりそうな発表の説明を書きます。発表を聞いた人が何を得られる発表なのかを書くとよいでしょう。

Pitchには発表を選考する人が「どの発表を選ぶか」を検討するときに参考になりそうな発表者の説明を書きます。たとえば、「私が実装した人なので私が一番詳しいです。なので、私が話すのが一番いいんです!」ということを書きます。

これらのことは応募フォームの説明にも書いているので、応募するときには説明をちゃんと読んで応募内容がずれていないか確認するとよいでしょう。

応募の書き方については以下の情報が参考になります。

明示的に書かれている情報をインターネット上に見つけられませんでしたが、RubyKaigiは「Ruby」の「Kaigi」なので、Rubyで作られたなにかの発表よりもRubyそのものの発表の方がRubyKaigiにあっていそうです。

応募例:RubyKaigi 2015

RubyKaigi 2015での応募例です。この応募は採択されました。

発表内容:The history of testing framework in Ruby

Title:

The history of testing framework in Ruby

Abstract:

This talk describes about the history of testing framework in Ruby.

Ruby 2.2 bundles two testing frameworks. They are minitest and test-unit. Do you know why two testing frameworks are bundled? Do you know that test-unit was bundled and then removed from Ruby distribution? If you can't answer these questions and you're interested in testing framework, this talk will help you.

This talk describes about the history of bundled testing framework in Ruby and some major testing frameworks for Ruby in chronological order.

Details:

このトークは参加者が「自分にとって適切なテスティングフレームワークを選ぶための知識」を持ち帰れることを目標とします。

テスティングフレームワークはRubyで開発する上で重要な役割を持つソフトウェアです。Rubyを使うと非常に柔軟にプログラムを書くことができるため、意図せずに既存の挙動を変えてしまうことが起こしやすいです。自動化されたテストも一緒に開発すると、できるだけ低いコストでその問題の発生を抑えつつ、楽しくプログラムを書きやすくなります。それを支援するのがテスティングフレームワークです。

テスティングフレームワークはRubyで楽しくプログラムを書くために重要な役割にも関わらず、自分にとって適切なテスティングフレームワークを選ぶという活動はあまり行われていないように感じています。多くの人が使っているから、というのも選んだ理由としては妥当な理由ではあるのですが、流行が変わったとき・テスティングフレームワークのAPIが変わったときなど、今までと状況が変わったときに適切な対応を取りづらくなります。大きな流れが1つではなくなるからです。

自分で「どうしてこのテスティングフレームワークを選んでいるのか」を理解していれば、たとえ状況が変わったときでも適切な基準で適切なフレームワークを選びなおすことができるでしょう。

このトークではRubyにバンドルされたテスティングフレームワークの歴史といくつかの主要なRuby用のテスティングフレームワーク(RSpec, minitest, test-unitと関連ソフトウェアをいくつか)の歴史について時系列で紹介します。歴史を知ることで、それぞれのテスティングフレームワークが何を重視しているのか、そして、それが自分が大事にしていることと合致するのかを判断する材料になるはずです。これにより、参加者が「自分にとって適切なテスティングフレームワークを選ぶための知識」を持ち帰ることの実現を目指します。

Rubyにバンドルされたテスティングフレームワークの歴史については↓にまとめたものをベースとします。
http://www.clear-code.com/blog/2014/11/6.html

Pitch:

最新のRubyである2.2でtest-unitがバンドルされた背景(*)を知る人は少ないでしょう。RubyKaigiでのトークとして、RubyのユーザーがRubyの開発の歴史(の一部)を知る機会があるのは妥当だと考えます。

(*) Test::Unit互換APIを提供するため。そうしないと既存のテストが動かなくなってRubyのバージョンアップの障害になる人がでてバージョンアップの障害になるかもしれない。Python 3のように新しいバージョンがでても古いバージョンを使い続けるユーザーがたくさんいる状況は開発チームとしてはうれしくないので避けたい。

私はRubyのテスティングフレームワークの歴史に最初から関わっているわけではありません。Ruby 1.8と1.9の間くらいからだけです。しかし、その頃からRubyのテスティングフレームワークまわりについては観測してきましたし、このトークで登場するtest-unit gemテスティングフレームワークの現在のメンテナーです。そのため、私はこのトークをする人として適切だと考えます。

応募例:RubyKaigi 2016

RubyKaigi 2016での応募例です。この応募は採択されました。

発表内容:How to create bindings 2016

Title:

How to create bindings 2016

Abstract:

This talk describes how to create Ruby bindings of a C library. I want to increase Ruby bindings developers. If you're interested in using C libraries from Ruby and/or using Ruby more cases, this talk will help you.

This talk includes the following topics:

  • List of methods to create Ruby bindings of a C library.
  • Small example of each method.
  • Pros and cons of each method.

Details:

このトークの目標は参加者が「自分のユースケースにあったRubyバインディングの作り方を知ること」です。まったく知らないという参加者でもわかるようにします。「昔は知っていたけど最近のことはわからない」という参加者でも、2016年時点での最新情報を提供することで、得られるものがあるようにします。

多くのRubyistはRubyバインディングを作る機会はありませんし、作る必要もありません。しかし、作り方を知っていればいざというときにRubyバインディングを作るという選択肢が生まれ、よりRubyを活用できる可能性が高くなります。

Cライブラリーと連携できることはRubyのよいところの1つです。Cライブラリーと連携できると、Rubyスクリプトを高速化したり、既存のCライブラリーの機能を利用したりできます。速さが足りなくてRubyを使えない、機能が足りなくてRubyを使えない、そのようなときの解決策としてRubyバインディングの作り方が役に立ちます。

このトークではRubyバインディングの作り方として次の方法を紹介します。

  • RubyのC APIを使ったRubyバインディングの作り方
  • SWIGを使ったRubyバインディングの作り方
  • libffiを使ったRubyバインディングの作り方
  • GObject Introspectionを使ったRubyバインディングの作り方

それぞれについて具体的なコードとメリット・デメリットを紹介します。この情報をもって「自分のユースケースに適切なCライブラリーのRubyバインディングの作り方を知ること」の実現を目指します。

参考URL:スクリプト言語の拡張機能の作り方とGObject Introspectionの紹介

Pitch:

多くのRubyistはRubyバインディングを作る機会はありませんし、作る必要もありません。しかし、作り方を知っていればいざというときにRubyバインディングを作るという選択肢が生まれ、よりRubyを活用できる可能性が高くなります。

Cライブラリーと連携できることはRubyのよいところの1つです。Cライブラリーと連携できると、Rubyスクリプトを高速化したり、既存のCライブラリーの機能を利用したりできます。速さが足りなくてRubyを使えない、機能が足りなくてRubyを使えない、そのようなときの解決策としてRubyバインディングの作り方が役に立ちます。

以上より、Rubyを活用する機会を増やすため、RubyKaigiでRubyバインディングを作る方法を知る機会があるのは妥当だと考えます。

私は10個以上のRubyバインディングを作った経験があり、このトークで紹介するすべての作り方を使ってきました。そのため、私はこのトークをする人として適切だと考えます。

応募例:RubyKaigi 2017

RubyKaigi 2017での応募例です。この応募が採択されるかどうかはわかりません。

Title:

Improve extension API: C++ as better language for extension

Abstract:

This talk proposes better extension API.

The current extension API is C API. In the past, some languages such as Rust (RubyKaigi 2015), Go (Oedo RubyKaigi 05), rubex (RubyKaigi 2016) were proposed as languages to write extension.

This talks proposes C++ as a better language for writing extension. Reasons:

  • C++ API can provide simpler API than C API.
  • C++ API doesn't need C bindings because C++ can use C API including macro natively. Other languages such as Rust and Go need C bindings.
  • Less API maintenance cost. Other approaches need more works for Ruby evolution such as introduces new syntax and new API.

Details:

このトークの目標は参加者が「現在のC APIの課題を知ること」と「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」です。前者に関しては拡張ライブラリーを書いたことがない人でもわかるようにします。後者に関しては拡張ライブラリーを書いたことががない人には判断できないでしょう。よって、このトークのメインターゲットは拡張ライブラリーを書いたことがある人です。この人たちはこのトークでよりよい拡張ライブラリーのAPIについての知見を得るはずです。拡張ライブラリーを書いたことがない人たちは拡張ライブラリーのAPIそのものについての知見が増えているはずです。

このトークではC APIの課題として次のことを説明します。

  • 関数のシグネチャーを2回書かないといけず、それらがズレていてもコンパイル時に発見できないため、実行時にクラッシュしてしまう可能性が高まる。
  • 冗長である。たとえば、メソッドを登録するときにCの関数を定義してそれを別途登録するケース、eachbegin rescueを実現するためにCの関数を定義するケース、などである。
  • CのデータとRubyのデータ間の変換が面倒であり、入力チェックのコードが多くなってコードの見通しが悪くなり、メンテナンス性が下がる。
  • Rubyは例外が発生するとlongjumpするので、例外発生前に動的に確保したメモリーの開放処理が煩雑になる。

それぞれについて具体的なコードを紹介します。たとえば、関数のシグネチャーを2回書かないといけないとは次のようなコードのことです。

static VALUE
rb_sample_hello(VALUE self)
{
  return rb_str_new_static("Hello");
}

void
Init_sample(void)
{
  VALUE sample = rb_define_class("Sample", rb_cObject);
  rb_define_method(sample, "hello", rb_sample_hello, 0);
}

このコードではhelloメソッドの引数の数が0であることを示すために、rb_sample_hello(VALUE self)と関数を定義し、それをRubyに伝えるためにrb_define_method(..., 0)を実行しています。引数の数が増えた時は両方更新する必要があります。片方の更新を忘れるとクラッシュします。

このような情報をもって「現在のC APIの課題を知ること」の実現を目指します。

それぞれの課題について、課題を解決するC++ APIを提案します。APIだけでなくどうして解決できるのか、および、実装も説明します。このAPIとその解説および実装の説明により「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現を目指します。

たとえば、関数のシグネチャーを2回書かないといけないケースについては、次のようなC++ APIを提案します。

void
Init_sample(void)
{
  rb::Class("Sample", rb_cObject).
    define_method("hello", [](VALUE self) {return rb_str_new_static("Hello");});
}

このAPIではC++のラムダ式[](VALUE self)から引数が0であることがわかるため、その情報を自動的にRubyにも伝えます。これにより、シグネチャーの不整合によるクラッシュを防ぐことができます。[](const char *self)のようにシグネチャーが不正な時はコンパイル時にエラーにすることもできます。

このような情報をもって「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現を目指します。

また、RustやGoやrubexなど他のアプローチについても紹介し、それらのアプローチの課題も紹介します。

たとえば、RustやGoを使うアプローチではC APIのマクロの扱いに課題があります。これらの言語ではCのマクロを扱えないため、RSTRING_LEN()のようなC APIを使うためにはそれぞれの言語で同様の処理を実現する必要があります。rubexのアプローチではrubexという独自の言語の文法を覚える必要があるという学習コスト面と、rubex自体がRuby本体の進化(たとえば文法の追加)に追従しなければいけないというメンテナンス面の課題があります。

このような情報も「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現で役立つはずです。

もしかしたら、参考情報としてRubyのC++ APIを提供するRiceも紹介します。RiceはBoost.PythonのようなAPIを提供します。単なる拡張ライブラリーではなくバインディングを書くときにより便利です。「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現に役立つ情報なはずです。

Pitch:

Rubyは3.0で高速化を目指しています。Rubyが高速になることで、より速度を期待するユーザーが増えると予想します。Rubyレベルの高速化で満足できるユーザーも多いでしょうが、さらなる高速化を期待するユーザーも増えるはずです。

そうなったとき、より簡単に拡張ライブラリーを書けるようにすることでより簡単にRubyスクリプトを高速に実行できるようになります。そうなることで、そのようなユーザーも楽しくRubyを使えます。楽しくプログラムを書ける人が増えることはRubyのポリシーとあっています。

このトークで紹介する既存のアプローチは私のアイディアではありませんが、提案するC++ APIの部分は私のアイディアです。

私は10個以上の拡張ライブラリーを作った経験があり、C APIのよいところも課題も知っています。そのため、私はこのトークをする人として適切だと考えます。

まとめ

RubyKaigiの発表の応募例として私のここ3年の応募内容を紹介しました。RubyKaigi 2017の発表の応募は6月17日まで受け付けています。ぜひ応募してみてください。

タグ: Ruby
2017-06-06

データ分析用次世代データフォーマットApache Arrow勉強会(大阪) #osaka_arrow

須藤です。このあいだ試したらApache Arrowのリポジトリーにpushできたので私のコミット権関連の設定は完了しているようです。

東京近郊に住んでいる人向け情報:この大阪で開催したイベントと同じような内容のイベントを6月13日(明日!)に東京でも開催します!大阪だったので参加できなかったという方は、ぜひ東京でのイベントに参加してください。

2週間ほど前になりますが、2017年5月28日に大阪でApache Arrowの勉強会を開催しました。前日に関西Ruby会議2017で発表していたのですが、せっかく大阪に来たのでApache Arrowの普及活動をしようと思い、開催しました。会場はクラスメソッドさんに貸してもらいました。日曜日なのに会社を開けてもらってありがとうございます!

なお、クラスメソッドさんに会場提供をお願いしたのはOSS Gate大阪ワークショップでつながりがあったからです。思わぬところで意外なつながりが活きていて、ありがたいことです。

当日使った資料は次の通りです。中盤までは既存の情報を紹介したので、最初の方のページはそれらの情報へのリンク集になっています。終盤は今回用にまとめた情報を紹介したので、それらの情報がこのスライドのメインの内容になります。イベントの内容はクラスメソッドさんがまとめてくれたイベントレポートを参照してください。

関連リンク:

大阪で開催したApache Arrow勉強会を紹介しました。なんと、6月13日(明日!)に同様のイベントが東京でも開催されます!

会場提供はSpeeeさんです。SpeeeさんとはOSS開発支援DataScience.rbなどでつながりがあり、会場を提供してもらえました。ありがとうございます!

2017-06-12

Fluentd pluginでconfig_paramとconfig_sectionを使いこなす

Fluentdのプラグインの開発をする上で避けて通れないのが設定です。ユーザーに設定ファイルを書いてもらってプラグインの動作をカスタマイズできるようにすることは必須です。

config_paramconfig_sectionという2つのAPIを使いこなすとさまざまな設定を簡単にきれいに書くことができます。

config_param

config_paramは設定を定義するために使います。

次のように名前と型を指定するのが基本形です。

desc "description for name"
config_param :name, :string

この場合は:nameという名前の文字列型の設定を定義しています。型によって、使えるオプションが異なるので型ごとに説明します。なお、config_paramの直前にdescを書くと設定の説明を書くことができます。説明を書いておくとfluent-plugin-config-formatコマンドで説明付きで設定の定義をダンプすることができるようになります。

typeとして以下の9つの型を指定可能です。

  • :string
  • :integer
  • :float
  • :size
  • :time
  • :bool
  • :enum
  • :array
  • :hash
string, integer, float, bool

それぞれの型に変換して、指定された名前のインスタンス変数に値をセットします。

config_param :name, :string, default: "", secret: true, alias: :full_name
  • default: デフォルト値を指定します。省略するとこの設定は必須になり、Fluentd起動時に設定されているかどうかチェックされます。
  • secret: 真を指定すると、Fluentd起動時のログなどで値がマスクされます。
  • alias: この設定の別名を指定します。
size

バイト単位のサイズを設定します。k,m,g,tというサフィックスを利用可能です。それぞれ、キロ、メガ、ギガ、テラを表します。大文字、小文字は区別しません。 サイズは以下のように整数に変換されます。

limit 10  # 10 byte
limit 10k # 10240 byte
limit 10m # 10485760 byte
limit 10g # 10737418240 byte
limit 10t # 10995116277760 byte

使えるオプションはstring等と同じです。

time

時間の長さを指定します。s,m,h,dというサフィックスを利用可能です。それぞれ、秒、分、時、日を表します。小文字のみ有効です。 単位を省略するとto_fした値を使用します。秒に変換されます。

interval 0.5 # 0.5秒
interval 1s  # 1秒
interval 1m  # 1分 = 60秒
interval 1h  # 1時間 = 3600秒
interval 1d  # 1日 = 86400秒

使えるオプションはstring等と同じです。

enum

列挙型です。ユーザーに特定の値のリスト内から設定値を選ばせたいときに使います。ユーザーがリスト内に存在しない値を設定した場合、Fluentd起動時にエラーが発生します。

config_param :backend_library, :enum, list: [:geoip, :geoip2_c, :geoip2_compat], default: :geoip
  • default: デフォルト値を指定します。
array

配列です。

config_param :users, :array, default: [], value_type: :string

このように書くと、以下のような設定で@users = ["user1", "user2", "user3"]と設定されます。

users user1,user2,user3
  • default: デフォルト値を指定します。
  • value_type: 値の型を指定します。:string, :integer, :float, :size, :bool, :timeのいずれかを指定できます。
hash

ハッシュです。設定ファイルの書き方は2通りあります。

config_param :key_values, :hash, default: {}, symbolize_keys: true, value_type: :string

設定例:

key_values {"key1": "value1", "key2": "value2"} # JSONで書く
key_values key1:value1,key2:value2

どちらも以下のようにパースされます。

{ key1: "value1", key2: "value2" }
  • default: デフォルト値をハッシュリテラルで指定します。
  • symbolize_keys: 真を指定するとキーをシンボル化します。
  • value_type: 値の型を指定します。全ての値で同じ型を使用します。

config_section

config_sectionconfig_paramをグループ化するために使用します。 具体例としては、組み込みのバッファーの設定、パーサーの設定などがあります。

例えば、fluent-plugin-s3では以下のように認証情報の設定、バッファーの設定、フォーマッターの設定をグループ化して記述することができます。fluent-plugin-s3では認証方法が複数あるので、認証方法ごとにセクションを作ることによって、利用している認証方法がわかりやすくなっています。このように設定をグループ化することによって設定ファイルの可読性が向上しています。

<match *.log>
  @type s3
  <assume_role_credentials>
    role_arn xxxxx
    role_session_name xxxxx
  </assume_role_crecentials>
  <buffer tag,time>
    @type file
    path /var/log/fluent/s3
    timekey 3600 # 1 hour partition
    timekey_wait 10m
    timekey_use_utc true # use utc
  </buffer>
  <format>
    @type json
  </format>
</match>

config_section のメソッドシグニチャーは以下の通りです。

config_section(name, root: false, param_name: nil, final: nil, init: nil, required: nil, multi: nil, alias: nil, &block)
  • name: セクション名をシンボルで指定します。デフォルトではここで指定した名前と同じ名前のインスタンス変数をプラグインインスタンス内で参照することができます。
  • キーワード引数
    • root: ルートセクションである場合に真を指定します。Fluentd内部で利用するもので、一般のプラグインでは利用しません。
    • param_name: プラグインのインスタンス内で利用する、インスタンス変数名を@を除いたシンボルまたは文字列で指定します。
    • final: プラグインのサブクラスでこのセクションの上書きを禁止するならば真を指定します。一般のプラグインでは、あまり意識しなくてもよいです。
    • init: このセクションでは初期値が必須であるパラメータのみを定義します。一般のプラグインでは、あまり意識しなくてもよいです。
    • required: このセクションが必須であれば真を指定します。真を指定した場合、設定ファイルにこのセクションがないとFluentdの起動時にエラーが発生します。
    • multi: このセクションを複数指定可能であれば真を指定します。
    • alias: このセクションの別名を指定します。

例: sample というプラグインに以下のようなセクションがある場合の設定ファイルの書き方を例示します

config_section :child, param_name: 'children', required: false, multi: true, alias: 'node' do
  config_param :name, :string
  config_param :power, :integer, default: nil
  config_section :item, multi: true do
    config_param :name, :string
  end
end
<source>
  @type sample
  <child>
    name  gohan
    power 100
    <item>
      name senzu
    </item>
  </child>
  <child>
    name  goten
    power 10
  </child>
</source>

まとめ

config_paramconfig_sectionを使うことで得られる利点をまとめます。

  • plus1 設定を宣言的に定義できる
  • plus1 fluent-plugin-config-formatコマンドでMarkdownやJSONで定義を出力することができる
    • Markdownで出力したものはREADMEにそのまま貼り付けることも可能 100
  • plus1 コード量を少なくすることができる

config_paramconfig_sectionを使うことで、デメリットはありません。 Fluentd組み込みのAPIを使いこなして、使いやすくメンテナンスしやすいプラグインを書きましょう。

タグ: Fluentd
2017-06-14

Firefoxの先読み機能の無効化とその確認手順

※この記事の情報はFirefox ESR52を対象としています。これより新しいバージョンではこれらの情報は当てはまらない場合がありますので、ご注意下さい。

Firefoxの先読み機能の種類と無効化の方法

FirefoxにはWebサイトの閲覧時の体感的な快適さを向上するため、実際にリクエストが行われる前から接続やデータのダウンロードを行っておくという「先読み」の機能があります。ただし、無差別に全てのリンクを辿るのではなく、いくつかの条件が揃った時に初めて先読みが発動するようになっています。具体的には以下の要領です。

ユーザーから見れば快適さが増して嬉しい機能ですが、反面、ネットワークのトラフィックやセッション数は増大する事になります。弊社のMozillaサポート事業のお客様からも、契約回線の帯域が限られているなどの理由で、情報システム管理担当者の意向として「先読み機能を無効化したい」というご相談を頂く事があります。また、「既に担当者側でそのような設定を行ったつもりだが、実際にその指定が機能しているかどうかを確認したい」というご相談もあります。

前述の先読みに類する機能を全て無効化する場合、指定は以下のようになります。

  • 全般的な制御
    • network.predictor.enabledfalse(真偽型)
    • network.predictor.enable-hover-on-sslfalse(真偽型)
    • network.predictor.enable-prefetchfalse(真偽型)
  • 次のページを示すリンクの先読み
    • network.prefetch-nextfalse(真偽型)
  • DNSプリフェッチ
    • network.dns.disablePrefetchtrue(真偽型)
    • network.dns.disablePrefetchFromHTTPStrue(真偽型):TLSを使用したページでの挙動の制御。
  • リンクの上にポインタが載った時の投機的接続の抑止
    • network.http.speculative-parallel-limit0(整数型)

では、これらの設定が期待通りに反映されている事をどのように確認すればよいのでしょうか。これがこの記事の本題です。

設定が反映されているかどうかの確認

設定の書き間違いのようなケアレスミスや、それ以外にも何らかの理由から、設定したはずの値が反映されないままになっているという事は度々あります。そのため、この手のカスタマイズはなるべく、実際の挙動から設定の反映状況を確認する事が望ましいです。

データのダウンロードまでも行う場合の確認

HTTP接続からデータのダウンロードまでを行うフルスペックの「先読み」については、先読み対象となるHTTPリクエストが実際に処理されたかどうかを見るのが確実です。例えばこのブログの2016年5月10日の記事同年5月18日の記事へのリンクを含んでいますが、このリンクにはrel="next"という属性が指定され次のページへのリンクである事が示されているため、先読みの対象となります。

このような先読みは、ブラウザコンソールで動作を確認できます。 キーボードショートカットの「Ctrl-Shift-J」を使うかパネルメニューの「開発ツール」(またはメニューバーの「Web開発」)で「ブラウザコンソール」をクリックするとブラウザコンソールのウィンドウが開かれますので、そのウィンドウ上部の「ネットワーク」がハイライトされている状態でこのブログの2016年5月10日の記事を開いてみて下さい。先読みが機能していればGET http://www.clear-code.com/blog/2016/5/18.htmlというメッセージがコンソールに出力され、機能していなければこのメッセージは出力されません。

コンソールに出力される出力が多すぎて見分けが難しいといった場合には、低レベルのログでも動作を確認できます。この方法については後述します。

DNSの名前解決のみ行われる場合の確認

先読み機能には、DNSでの名前解決のみを先行して行う機能(DNSプリフェッチ)もあります。例えば、ページ内に<link rel="dns-prefetch" href="http://dns-prefetch.example.com">のような記述が含まれていると、FirefoxはHTTPでのリクエストではなく、dns-prefetch.example.comの名前解決の問い合わせのみを単独で行います。

この機能が動作しているかどうかを確認するためは、DNSへの問い合わせを行うモジュールが内部的に発行するイベントを監視する必要があります。この確認にもブラウザコンソールを使います。

そのためには、about:configで以下のように設定します。

  • devtools.chrome.enabledtrue(真偽型):スクリプトの実行を可能にする。
  • network.dns.notifyResolutiontrue(真偽型):DNSでの名前解決時に内部的なイベントを発行するようにする。

上記の設定を反映した状態でブラウザコンソールを開くと、コンソール下部に入力欄が出現します。この入力欄に Services.obs.addObserver(function(aSubject, aTopic, aData) { console.log(aTopic+': '+aData); }, 'dns-resolution-request', false); と入力してEnterキーを押すと、この内部的なイベントを捉えてコンソールにメッセージとして出力する事ができます。DNSプリフェッチのための指定が含まれているWebページ(テストケース)を開いた時にdns-resolution-request: dns-prefetch.example.comのようなメッセージがブラウザコンソールに出力されれば、DNSプリフェッチのための指定が処理されたと分かりますし、逆に、そのようなWebページを開いてもこのメッセージがコンソールに出力されなければ、DNSプリフェッチは無効化されていると判断できます。

TCP接続のみ行われる場合の確認

Firefoxの先読み機能の中には、データの読み込みは行わないまでも、ソケット接続のみ確立しておくという機能もあります。リンクの上にポインタが載った時点でまずソケット接続だけ先行して行っておき、クリックされた時にすぐにHTTPのリクエストを送出できるようにするというもので、この機能は投機的接続と呼ばれます。

ソケット接続を確立しただけの段階ではコンソールには何も出力されず、また、DNSプリフェッチのような内部的なイベントも発行されないため、投機的接続が行われているかどうかは、低レベルのログを解析して調べる必要があります。

Firefox 52ではログ収集対象のモジュールやログファイルの出力先は環境変数ではなく設定値で指定します。about:configで以下の設定を作成して下さい。

  • logging.nsHttp5(整数型)
  • logging.NetworkPredictor5(整数型)
  • logging.config.synctrue(真偽型)
  • logging.config.add_timestamptrue(真偽型)
  • logging.config.clear_on_startupfalse(真偽型)
  • logging.config.LOG_FILEC:\Users\Public\predict.log(文字列型)

この状態でFirefoxを再起動すると、logging.config.LOG_FILEで指定したパスの位置にpredict.log-main.123のような名前でログファイルが出力され始めます。(mainは親プロセスのログであることを示します。ファイル名の末尾の数字はプロセスIDです。)投機的接続の機能は通常のWebブラウズの中で暗黙的に行われるため、この状態のまましばらくWebページを2〜3画面分ほど見て回り、十分な量のログを溜めて下さい。

ログが溜まったら、上記のログ出力用の設定をリセットしてFirefoxを再起動し、出力されたログファイルをブラウザで開きます。この時、ログは以下のような内容になっているはずです。

2017-06-02 09:51:57.565000 UTC - [Main Thread]: I/Logger Flushing old log files
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp Creating nsHttpHandler [this=9fe1400].
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpHandler::Init
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpHandler::PrefsChanged [pref=(null)]
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpHandler::PrefsChanged Security Pref Changed (null)
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpHandler::MakeNewRequestTokenBucket this=9fe1400 child=0
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpAuthCache::Init
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpAuthCache::Init
2017-06-02 09:51:57.659000 UTC - [Main Thread]: V/nsHttp Creating nsHttpConnectionMgr @b5a1420
...

続けて、キーボードショートカットの「Ctrl-Shift-K」を使うかパネルメニューの「開発ツール」(またはメニューバーの「Web開発」)で「Webコンソール」をクリックし、ログファイルを読み込んだタブに対してコンソールを表示します。コンソール下部の入力欄に以下のログ解析用のスクリプトをコピー&ペーストし、Enterキーを押して実行します。

var body = document.body.textContent;
var requested = body.match(/nsHttpConnectionMgr::OnMsgSpeculativeConnect/g) || [];
var skipped = body.match(/Transport not created due to existing connection count/g) || [];
!requested.length ? 'not working' : requested.length == skipped.length ? 'all skipped' : 'preconnected';

すると、以下の3つのいずれかの解析結果がコンソールに出力されます。

  • preconnected:投機的接続が行われた。
  • all skipped:投機的接続が無効化されている(もしくは、同時接続数が最大値に達しているため投機的接続が行われなかった)。
  • not working:ネットワーク環境の都合等で投機的接続が行われていない。

3番目の「投機的接続が行われていない」というケースは、そのクライアントが接続しているネットワーク環境によって発生し得ます。 Firefoxは投機的接続を開始する前に接続先ホストがプライベートなネットワークに属しているかどうかを見ており、DNSで名前解決した結果が192.168.1.1010.21.0.100といったプライベートネットワーク上のIPアドレスだった場合には投機的接続の処理を中断するようになっています。 これは、投機的接続は元々、遠隔地にあるホストとのソケット接続の確立に時間がかかるためにフライングで接続しておくという趣旨の機能なので、ネットワーク的に近いホストに対しては行う意義が薄いからです。

他の先読み機能が働いているかどうかもデバッグ用ログから確認する

なお、前項で取得したログを解析すると、データの読み込みを伴う先読みが行われたかどうかも判別できます。今度は以下の解析用スクリプトをWebコンソールで実行します。

var body = document.body.textContent;
var success = body.match(/Predictor::Predict.*\n.*called on parent process.*\n.*not enabled/g) || [];
var all = body.match(/Predictor::Predict.*\n.*called on parent process/g) || [];
(success.length == all.length) ? 'all canceled' ? 'some canceled';

出力結果は以下の2つのうちいずれかです。

  • all canceled:先読み処理が無効化されている。
  • some canceled:先読み処理が有効である。

まとめ

以上、Firefox ESR52において先読みに類する機能の無効化の方法と、その検証方法をご案内しました。

ここで解説している情報の中には、まとまった情報源が無く、お客様のご要望を受けて調査した結果判明したという情報も含まれています。クリアコードではOSSのドキュメント化されていない仕様を調査して明らかにする業務も行っておりますので、導入をお考えのOSSが想定通りの動作をするかどうかに不安がある場合や、既にお使いのOSSが期待通りに動作せずお困りの場合などには、お気軽にお問い合わせ下さい

タグ: Mozilla
2017-06-16

Fluentd pluginのconfigure内でのアンチパターン

多くのFluentdのプラグインを見てきて知見が増えてきたのでまとめます。

config_sectionを使っていない

Fluentdのプラグインでは、configureメソッド内で設定を自力で解釈することによりconfig_sectionconfig_paramで定義していない設定値も扱えるようになっています。

以下のような設定ファイルを

<source>
  @type sample
  user hoge
  pass secret_passowrd
  <pattern>
    name foo
  </pattern>
  <pattern>
    name bar
  </pattern>
</source>

次のコードで自力で解釈することができます。

def configure(conf)
  super
  @patterns = conf.select {|element| element.name == "pattern" }
  @user = conf["user"]
  @pass = conf["pass"]
end

このように自力で解釈すると、テストも書かなければならないし、間違いも発生しやすくなるので以下のように、Fluentdの本体でよくテストされているconfig_sectionconfig_paramを組み合わせて使いましょう。config_sectionconfig_paramを使うと設定を宣言的に書くことができて、とてもわかりやすくなります。その上、fluent-plugin-config-formatというコマンドでMarkdownやJSONで出力することができます。descを使って説明も加えておくと、fluent-plugin-config-formatでも説明が出力されて便利です。

config_section :pattern, param_name: "patterns", multi: true do
  desc "name for pattern"
  config_param :name, :string
end
desc "user name for login"
config_param :user, :string
desc "password for login"
config_params :pass, :string, secret: true

def configure(conf)
  super
  # 何か追加の処理
end

config_paramのarrayを使っていない

<source>
  @type github-activities
  users ashie,cosmo0920,kenhys,kou
</source>

上記のような設定を以下のようなコードで、配列化して使っているプラグインがありました。

desc "user names e.g. user1,user2,user3"
config_param :users, :string, default: ""
def configure(conf)
  super

  @users = @users.split(",").map(&:strip)
end

これは以下のように書き換えることができます。

desc "user names e.g. user1,user2,user3"
config_param :users, :array, default: [], value_type: :string
def configure(conf)
  super
end

config_paramのenumを使っていない

<source>
  @type groonga
  protocol http
  # ...
</source>

上のような設定を次のようなコードでパースして使用しているプラグインがありました。

config_param :protocol, default: :http do |value|
  case value
  when "http", "gqtp", "command"
    value.to_sym 
  else
    rails Fluent::ConfigError, "must be http, gqtp or command: <#{value}>"
  end
end
def configure(conf)
  super
  # @protocol を使う
end

これは以下のように書き換えることができます。

config_param :protocol, :enum, list: [:http, :gqtp, :command], default: :http
def configure(conf)
  super
  # @protocol を使う
end

書き換えることで、コードがすっきりしました。

秘密の値をsecretにしていない

<match dummy.log>
  @type honeycomb
  writekey very-secret-key
  # ...
</match>

パスワードやAPIキーなどの秘密にしておきたい情報を設定ファイルに書かせるときは、secretオプションを付けましょう。 secretオプションをtrueにするとFluentdの起動時に表示されるダンプなどで値がマスクされます。

config_param :writekey, :string, secret: true

以下のようにsecretオプションを忘れてしまうと、Fluentd起動時のダンプなどにそのまま出力されてしまうので、バグ報告などのときにユーザーが自分でマスクしなければなりません。

config_param :writekey, :string

必須チェックを自分でしている

config_param :tag, :string, default: nil
def configure(conf)
  super
  raise Fluent::ConfigError, "tag is required" unless @tag
end

上のように必須の値のデフォルト値をnilにして、自分でチェックしているコードをたまに見かけます。 下のようにデフォルト値を省略するとその設定は必須になり、Fluentd起動時にチェックが実行されます。

config_param :tag, :string
def configure(conf)
  super
end

desc を書いていない

config_param :tag, :string

descを書くとfluent-plugin-config-formatというコマンドで、説明を見やすくフォーマットしたものを出力することができます。これはREADMEに設定の説明を書くときに使うととても便利です。

desc "tag for records"
config_param :tag, :string

まとめ

Fluentdで用意されている機能を活用して、メンテナンスを続けやすいプラグインを開発しましょう。

タグ: Fluentd
2017-06-21

LaravelでPostgreSQLとPGroongaを使って日本語全文検索を実現する方法

はじめに

※この記事は、Laravelを使った開発の経験がある人を対象としています。Laravelの基本的な使い方自体の説明は含まれていませんので、ご注意下さい。

Webアプリケーションを開発していると、「検索窓に入力された語句を含むレコードを一覧表示する」といった機能を付けたくなる場面がよくあります。この時よく使われる手軽な方法としてSQLのLIKEがありますが、LIKEには検索対象のレコードの数が増えれば増えるほど処理に時間がかかるという欠点があります。そこで登場するのが全文検索という手法です。全文検索では事前に用意しておいたインデックス情報を使うことにより、レコード数が増加しても安定して高速な検索を行うことができます。サービスの利用者が増加して数万・数十万といった数のレコードを取り扱うような必要が生じてきた場合、全文検索の導入を検討する価値は十分にあるでしょう。

以前、Ruby on Railsで作ったアプリケーションからPGroongaを使って日本語全文検索機能を実現する方法を紹介しました。今回はそのPHP版として、Laravelで作ったブログ風のアプリケーションにPGroongaを使った日本語全文検索機能を組み込む方法をご紹介します。

この記事では、適当なLaravelアプリケーションが手元に無い場合を想定し、解説用に用意したLaravelアプリケーションとPostgreSQLとの組み合わせに対して、全文検索機能を組み込む手順を解説しています。すでに開発中・運用中のPostgreSQLを使用したアプリケーションがある場合には、そのアプリケーションに組み込む手順としてテーブル名等を適宜読み替えて下さい。

また、タイトルにも書いてある通りですが、この記事で紹介しているPGroongaは、PostgreSQLに対して全文検索機能を提供するソフトウェアです。開発・運用中のアプリケーションがPostgreSQL以外のデータベースを使用している場合にはPGroongaを使えませんので、くれぐれもご注意下さい。(他のデータベースを使っている場合、例えばMySQLやMariaDBであれば、PGroongaの代わりにMroongaを使うことになります。その場合の手順はここでは解説していませんので、あしからずご了承ください。)

開発環境の準備

それでは、開発環境を用意していきます。まず、以下の2つを用意します。

  • Homesteadを動作させるホストマシン(ここではUbuntu 16.04LTSと仮定します)
  • 動作確認用のWebブラウザが動作するクライアント

ホストマシンとしてLinuxのデスクトップ環境を用意して、ホストマシン自身をクライアントとして使うのが最も簡単です。別々のマシンを使う場合には、両者は同じネットワーク上に存在するか、もしくはホストマシンのネットワーク上の任意のコンピュータにクライアントから自由に接続できるものとします。

Laravelには開発環境を簡単に構築するためのHomesteadという枠組みがあり、今回はそれを使ってみることにします。Vagrantが必要なので、事前にホストマシンにVagrant 1.9以上VirtualBoxをインストールしておいて下さい。

VMの準備(ホストマシン上の操作)

開発環境のboxイメージが公開されているので、まずはそれを導入します。

% vagrant box add laravel/homestead
==> box: Loading metadata for box 'laravel/homestead'
box: URL: https://atlas.hashicorp.com/laravel/homestead
This box can work with multiple providers! The providers that it
can work with are listed below. Please review the list and choose
the provider you will be working with.

1) parallels
2) virtualbox
3) vmware_desktop

ここではVirtualboxのイメージを使うので2を選択します。

Enter your choice: 2
==> box: Adding box 'laravel/homestead' (v2.1.0) for provider: virtualbox
box: Downloading: https://atlas.hashicorp.com/laravel/boxes/homestead/versions/2.1.0/providers/virtualbox.box
==> box: Successfully added box 'laravel/homestead' (v2.1.0) for 'virtualbox'!
vagrant box add laravel/homestead 14.75s user 7.42s system 19% cpu 1:53.24 total

boxを正常にダウンロードできたので次に進みます。

Homesteadの準備(ホストマシン上の操作)

Homesteadのリポジトリを以下のようにホストの作業ディレクトリにcloneします。

% mkdir ~/work
% cd ~/work
% git clone https://github.com/laravel/homestead.git

masterブランチは不安定な場合があるため、今回はリリースブランチを使います。

% cd homestead
% git checkout v5.4.0
...
HEAD is now at f54a9f0... Tagging 5.4.0 (#595)
(END):
Homestead.yamlの準備(ホストマシン上の操作)

ホストにて init.sh を実行します。

% bash init.sh
Homestead initialized!

すると以下の3つのファイルが同じディレクトリに作成されます。

  • Homestead.yaml
  • after.sh
  • aliases

Homestead.yaml はVMの設定ファイルです。

今回は以下の項目を変更します。

  • ip: 異なるネットワークのIPアドレスを指定する。(ホストマシンが接続しているネットワークが192.168.10.0/24なら、192.168.20.0/24などのアドレスにする)
  • authorize: ホストマシンで作成したsshの公開鍵のパスを指定する。
  • keys: ホストマシンで作成したsshの秘密鍵のパスを指定する。
  • folders: ホストマシンのディレクトリーをゲスト上から見えるようにする指定だが、ホストマシン上にはLaravelアプリケーション用のファイルを設置しないため不要なので、コメントアウトする。
  • sites: ゲスト上にこれから作成するアプリケーションに合わせてパスを書き換える。
  • networks: ゲストがホストマシンが接続しているネットワークにブリッジ接続するための設定を追加する。

上記変更を反映した Homestead.yaml は次の通りです。

Homestead.yaml
---
ip: "192.168.20.10" # ホストマシンと異なるネットワークになるようにする。
memory: 2048
cpus: 1
provider: virtualbox

authorize: ~/id_homestead.pub #公開鍵のパス

keys:
    - ~/id_homestead #秘密鍵のパス

#folders:
#    - map: ~/work/Blog
#      to: /home/vagrant/Blog

sites:
    - map: homestead.app
      to: /home/vagrant/Blog/public # 後々、この位置にファイルが作られる。

databases:
    - homestead

networks:
    - type: "public_network"
      bridge: "eth0" # "wlan0"など、ホストマシンの主なインターフェースに合わせる。

IPアドレスやブリッジのインタフェース名、鍵の情報は環境に応じて適宜読み替えてください。

VMの起動(ホストマシン上の操作)

設定ファイルとコマンドの準備ができたので、ホストマシンからVMを起動します。

% vagrant up

設定に問題なければVMが正常起動します。 続けて、クライアントからゲスト上のLaravelアプリケーションに接続するために、起動したVMのブリッジ接続でのIPアドレスを調べます。

% host_if=eth0
% host_nw="$(ip addr | grep -o "inet.*$host_if" | cut -d ' ' -f 2 | cut -d '.' -f -3)."
% vagrant ssh -- ip addr | grep "$host_nw"
    inet 192.168.10.52/24 brd 192.168.10.255 scope global enp0s9

この例では192.168.10.52が割り当てられているので、クライアントからは http://192.168.10.52/ をブラウザで開けば動作を確認できることになります。

hostsファイルの編集(クライアント上の操作)

http://192.168.10.52/のようなIPアドレスの直接入力は煩雑なので、クライアントのhostsファイルを編集して、homestead.appというホスト名で参照できるようにしておきます。クライアントのhostsに以下の内容を書き足しましょう。

192.168.10.52 homestead.app

クライアントがUbuntuのデスクトップ環境の場合、hostsは /etc/hostsです。クライアントがWindowsの場合は、hostsは C:\windows\system32\drivers\etc\hosts の位置にあります。

以上で準備完了です。クライアント上のWebブラウザで http://homestead.app/ を開いてみて下さい。以下のようなエラーページが表示されるはずです。

(エラーページのスクリーンショット)

これはHomestead上のnginxが返しているエラーで、所定の位置にまだLaravelアプリケーションが存在していないという事を示しています。

Laravelアプリケーションのセットアップ

それではLaravelによるブログ風のアプリケーションを用意していきます。といっても、Laravelでのブログ風アプリケーションの開発の仕方そのものはここでは重要ではないので、あらかじめこちらで用意したサンプルのブログ風アプリケーションを使うことにしましょう。

まず、ホストマシンからゲストマシンへsshでログインします。

% vagrant ssh
vagrant@homestead:~$

そうしたら、サンプルアプリケーションのGitリポジトリをcloneします。この時、clone先のディレクトリ名をBlogと明示して、ファイルの設置先がホストマシン上のHomestead.yamlの内容と一致するようにする必要があることに気をつけて下さい。

vagrant@homestead:~$ git clone https://github.com/clear-code/pgroonga-example-laravel.git Blog

cloneし終えたら、アプリケーションを初期化します。

vagrant@homestead:~$ cd Blog
vagrant@homestead:~/Blog$ composer install
vagrant@homestead:~/Blog$ php artisan migrate
vagrant@homestead:~/Blog$ php artisan db:seed
サンプルアプリケーションの仕様

以上の手順をすべて実施したら、クライアントから http://homestead.app/posts を開いてみて下さい。以下のような記事一覧ページが表示されるはずです。

(サンプルアプリケーションのスクリーンショット)

Homestead.yamlの設定(例えば、Laravelのアプリケーションの設置先パス)を間違えていると、先のエラーページと同じ物が表示されるかもしれません。その場合、Homestead.yamlを修正してから vagrant provision を実行し、ゲスト上のnginx等を再起動する必要があります。

記事一覧ページの右上にある検索窓に「Groonga」のようなキーワードを入力すると、そのキーワードの検索結果として、postsテーブルのレコードのうちbodyカラムにキーワードを含むレコードの一覧が表示されます。ただ、コントローラの実装(app/Http/Controllers/PostController.php)を見ると分かりますが、これは以下のような単純なSQLのLIKEによる絞り込みの結果です。Groonga OR Mroongaのような凝った検索は行えません。

app/Http/Controllers/PostController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller
{
  public function index(Request $request)
  {
    $query = $request->get('query');
    if (!empty($query)) {
      $posts = \App\Post::where('body', 'like', "%{$query}%")->orderBy('id', 'desc')->get();
    }
    else {
      $posts = \App\Post::orderBy('id', 'desc')->get();
    }
    return \View::make('posts.index')
             ->with('posts', $posts)
             ->with('query', $query);
  }
}
注意点

Homesteadの環境で普通にlaravel new Blogすると、MySQLを使うように設定されたLaravelアプリケーションが作られます。しかし今回はPGroongaの使い方の解説なので、この例では以下のように.envを編集して、データベースにはPostgreSQLを使うよう設定してあります。

commit 5f48f346ccf36e07aefdf062b2f374d83dc6151d
Author: YUKI Hiroshi <yuki@clear-code.com>
Date:   Mon Jun 26 01:56:56 2017 +0000

    Use PostgreSQL by default

diff --git a/.env b/.env
index f41ce19..ceaa34d 100644
--- a/.env
+++ b/.env
@@ -5,9 +5,9 @@ APP_DEBUG=true
 APP_LOG_LEVEL=debug
 APP_URL=http://localhost
 
-DB_CONNECTION=mysql
+DB_CONNECTION=pgsql
 DB_HOST=127.0.0.1
-DB_PORT=3306
+DB_PORT=5432
 DB_DATABASE=homestead
 DB_USERNAME=homestead
 DB_PASSWORD=secret
diff --git a/config/database.php b/config/database.php
index cab5d06..abf3d43 100644
--- a/config/database.php
+++ b/config/database.php
@@ -13,7 +13,7 @@ return [
     |
     */
 
-    'default' => env('DB_CONNECTION', 'mysql'),
+    'default' => env('DB_CONNECTION', 'pgsql'),
 
     /*
     |--------------------------------------------------------------------------

冒頭にも述べていますが、PGroongaはPostgreSQLに対して全文検索機能を提供する物なので、それ以外のデータベースに対しては使えません。ご注意下さい。

PGronngaによる全文検索機能の組み込み(ゲスト上での作業)

お待たせしました! ようやくここからが本題です。

すでにあるLaravelアプリケーションでPGroongaを使って全文検索をするには、以下の4つのステップを踏みます。

  1. PGroongaのインストール
  2. PGroongaの有効化
  3. インデックスの作成
  4. PGroongaを使って検索するように問い合わせ部分を変更

それでは順番に見ていきましょう。

PGroongaのインストール

何はともあれPGroongaのインストールが必要です。Homesteadの環境はUbuntu 16.04ベースということで、今回はUbuntu用のインストール手順を参照しました。以下は実際に実行したコマンドです。

$ sudo add-apt-repository -y universe
$ sudo add-apt-repository -y ppa:groonga/ppa
$ sudo apt-get update
$ sudo apt-get install -y -V postgresql-9.5-pgroonga

PGroongaのインストール手順は実行環境によって異なります。Ubuntu 16.04以外の環境でのインストール手順については、PGroongaのプロジェクトサイトで公開されているインストール手順の説明を参照して下さい。

PGroongaの有効化

PGroongaは、サーバー上にパッケージをインストールしただけでは使えません。機能を利用するには、PostgreSQLのデータベースに対してCREATE EXTENSION pgroonga;というSQL文を明示的に実行する必要があります。

ということで、このSQL文を実行するためのマイグレーションを作成します。

$ php artisan make:migration install_pgroonga
Created Migration: 2017_06_23_091529_install_pgroonga
database/migrations/2017_06_23_091529_install_pgroonga.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class InstallPgroonga extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
      // ここから追記
      DB::statement('CREATE EXTENSION pgroonga');
      // ここまで追記
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
      // ここから追記
      DB::statement('DROP EXTENSION pgroonga CASCADE;');
      DB::statement('DELETE FROM pg_catalog.pg_am WHERE amname = \'pgroonga\';');
      // ここまで追記
    }
}

upに書かれている内容はPGroongaのインストール手順にあるもので、downに書かれている内容はアンインストール手順にあるものです。

用意ができたら、このマイグレーションを実行します。

$ php artisan migrate

これでPGroongaを使う準備ができました。

検索対象のカラムに全文検索用のインデックスを作成する

次に、PGroongaで全文検索するためのインデックスを作ります。

Laravelのマイグレーションではindexメソッドでインデックスを定義します。

$ php artisan make:migration add_posts_body_index
Created Migration: 2017_06_23_091530_add_posts_body_index
database/migrations/2017_06_23_091530_add_posts_body_index.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddPostsBodyIndex extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
      // ここから追記
      Schema::table('posts', function($table) {
        $table->index(['id', 'body'], 'pgroonga_body_index', 'pgroonga');
      });
      // ここまで追記
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
      // ここから追記
      Schema::table('posts', function($table) {
        $table->dropIndex('pgroonga_body_index');
      });
      // ここまで追記
    }
}

PGroongaを正しく使えているという事を説明するために、PGroongaのチュートリアルで説明されている内容との対応関係を見ていきます。 このチュートリアルでの説明に倣うと、インデックス作成用のSQL文は以下のようになります。

CREATE INDEX pgroonga_body_index ON posts USING pgroonga (id, body);

ここで、pgroonga_body_indexはPGroongaでの慣習に倣って付けた任意のインデックス名、postsはテーブル名です。USING pgroonga (id, body)は、インデックスの種類としてHashやB-treeなどと同列の物としてPGroongaを選択し、インデックスにはid(主キー)とbodyの両方を含めるという意味になっています。

database/migrations/2017_06_23_091530_add_posts_body_index.php
  Schema::table('posts', function($table) {
    $table->index(['id', 'body'], 'pgroonga_body_index', 'pgroonga');
  });

Laravelのマイグレーションでは、テーブル名はSchema::table('posts'...の部分で示されています。indexメソッドでインデックスを作成しており、この第1引数の['id', 'body']がインデックスに含めるカラム名の配列、第2引数がインデックス名、第3引数がインデックスの種類を示しています。PGroongaのチュートリアルで説明されている情報が過不足無く指定できていることが分かるでしょう。

なお、インデックス名を省略してnullにするとインデックス名を自動的に決定させることもできますが、downのマイグレーションでインデックス名を指定してdropIndexメソッドを呼ぶ都合上、ここではインデックス作成時にも明示的にインデックス名を指定しています。

そして、マイグレーションを実行します。

$ php artisan migrate

これによって、postsテーブルのbodyカラムを対象とした全文検索用のインデックスが作成されます。既存のレコードに対するインデックスもこの時一緒に作成されますし、この後で作成されたレコードに対しても自動的にインデックスが作成されるようになります。

以上で、全文検索の準備が整いました。

PGroongaを使って検索するように問い合わせ部分を変更

最後に仕上げとして、全文検索にPGroongaを使うように検索処理を書き換えます。

app/Http/Controllers/PostsController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostsController extends Controller
{
  public function index(Request $request)
  {
    $query = $request->get('query');
    if (!empty($query)) {
      // 変更前
      // $posts = \App\Models\Post::where('body', 'like', "%{$query}%")->orderBy('id', 'desc')->get();
      // 変更後
      $posts = \App\Models\Post::whereRaw('body @@ ?', $query)->orderBy('id', 'desc')->get();
    }
    else {
      $posts = \App\Models\Post::orderBy('id', 'desc')->get();
    }
    return \View::make('posts.index')
             ->with('posts', $posts)
             ->with('query', $query);
  }
}

PGroongaで全文検索をするためには、PGroonga専用の演算子を使います。whereメソッドではそれらの演算子を取り扱えないので、ここではwhereRawメソッドを使ってSQLの式を直接書いています。

今回使った@@という演算子(この演算子はPGroongaの現在のドキュメントでは&?で置き換えられていることになっていますが、&?PHPの実装上の都合で使えないため、非推奨のこちらを使用しています)は、与えられた検索クエリを一般的なWebの検索エンジンの検索クエリのように取り扱う物です。よって、Groonga リリース(Groongaとリリースの両方の語を含む)やGroonga OR PGroonga(GroongatoPGroongaのどちらか片方だけでも含む)や( Mroonga OR PGroonga ) リリース(MroongaかPGroongaのどちらか片方に加えて、リリースという語句を含む)のような複雑なクエリも、検索窓に入力するだけでそのまま使うことができます。

(複雑なクエリで検索した状態のスクリーンショット)

以上で、PGroongaによる全文検索への乗り換えが完了しました。実際に検索を実行して、期待通りの結果が返ってくるか確かめてみて下さい。

まとめ

以上、LaravelアプリケーションでPGroongaを使って全文検索を行う手順の語句最初のステップをご紹介しました。

この解説を見ると分かる通り、すでにPostgreSQLを使っている環境であれば、PGroongaを使い始めるのは非常に簡単です。また、アプリケーション内の変更はごく一部だけで済むため、LIKE検索との性能比較もやりやすいでしょう。全文検索を使ったことがない人は、これを機にぜひ一度試してみて下さい。

タグ: Groonga
2017-06-26

lessの既定のオプションを変えて快適なCLI生活を手に入れる

その時表示している内容を消さずにlessを終了するには?

何かのコマンドの実行結果が長くなって画面外に溢れてしまう場合でも、grep ... | less という風にパイプライン経由でlessコマンドに結果を渡すと、結果を自由にスクロールしながら落ち着いてゆっくり読むことができます。

しかし、「q」でlessを終了すると、その時表示されていた内容は画面から消えてしまいます。コマンドの出力結果を見ながら次の作業をしようと思うと、以下のような工夫をしないといけません。

  • Ctrl-Zでlessをバックグラウンドに切り替えて、また参照したくなったらその都度fgコマンドで復帰させる。
  • tmuxなどのターミナルマルチプレクサを使い、画面を分割して片方の画面でlessの結果を表示しながら、もう片方の画面で操作を行う。
  • コマンドの出力を直接lessに渡して表示するのではなく、一旦リダイレクトでファイルに保存して、改めて必要に応じlessでファイルを開く。

ただ、毎度こういった工夫をするのは面倒です。

そこで便利なのが、lessの起動オプションの1つである--no-init(または-X)です。このオプションを指定してlessを起動すると、topを終了したときのようにその時の表示内容が画面上に残ったままになるため、それを参照しながら次の操作を行うことができます。

常に--no-initが指定された状態でlessを起動するには?

このように便利な--no-initオプションですが、毎回このオプションを指定するというのはあまり現実的ではないでしょう。そこで便利なのが環境変数LESSです。lessLESSという名前の環境変数に指定されている内容を規定のオプションとして自動的に使用するため、~/.bashrc~/.profileなどに以下のように書いておけば、単にコマンド名をlessと入力するだけで、lessを常に--no-initオプションありの状態で起動できるようになります。

~/.bashrc
export LESS='--no-init'

察しの良い方はもうお気づきかもしれませんが、ここには--no-init意外にも様々なオプションを併せて指定しておけます。他にも便利なオプションとしてお薦めなのは--shift-#)でしょう。これは左右カーソルによる横スクロールの速度を変えるもので、既定の状態では画面半分ずつ一気に横スクロールするのに対し、--shift 4のように小さめの値を明示しておけば、スクロール速度がなだらかになるため、見ていた箇所を見失う事もなくなります。

~/.bashrc
export LESS='--no-init --shift 4'

また、--LONG-PROMPT(または-M)というオプションを使うと、現在表示されているのが何行目から何行目までの範囲なのかが画面の下端に表示されるようになります。何行目あたりを見ているかが分かると、lessで内容を大雑把に確認してからvimなどで編集するということもやりやすくなりますので、これもお薦めのオプションです。

~/.bashrc
export LESS='--no-init --shift 4 --LONG-PROMPT'

git diffgit logで起動される時のlessの挙動を元に戻す

ここまでの話で終わらせても良いのですが、これをそのまま実施すると、ソフトウェア開発者の方などでgitコマンドを多用する場合に困ったことが起こります。それは、git loggit diffgit grepといった操作で自動的に起動されるlessの挙動が変わってしまうということです。具体的には、以下のような問題が起こります。

  • 結果が1画面内に収まるような場合(git diffの差分が小さかった場合や、git grepで見つかった件数が少なかった場合など)でもlessが起動してしまって、いちいち手動操作で「q」キーでlessを終了しなくてはならなくなる。
  • 結果の中にESC[33mのようなゴミが混ざるようになる。

これは何故なのでしょうか。

gitlessを起動する時の既定のオプション

実は、gitコマンドには自動的にlessを起動する場面用の既定のオプションが埋め込まれています。これはMakefileの中で以下のように定義されています。

ifndef PAGER_ENV
PAGER_ENV = LESS=FRX LV=-c
endif

ここでのLESS=FRXというのがそうで、これがgitコマンドの起動時に実行されるスクリプトの中

for vardef in @@PAGER_ENV@@
do
	var=${vardef%%=*}
	eval ": \"\${$vardef}\" && export $var"
done

という箇所に埋め込まれて、最終的に: "${LESS=FRX}" && export LESSというコマンドが実行されることで、「環境変数LESSで何かオプションが指定されていればそれをそのまま使い、無ければ既定のオプションとしてFRXを使う」という事が行われています。

FRX-F -R -Xの短縮表記で、それぞれロングオプションで書き表すと--quit-if-one-screen --RAW-CONTROL-CHARS --no-initとなります。

--quit-if-one-screen-F)は、前述の1つ目の問題(結果が1画面内に収まるような場合でもlessが起動してしまう)を解消するための指定です。このオプションが指定されていると、lessは表示した内容が1画面内に収まる場合にそのまま自動終了するようになります。これと--no-init-X)の併用により、あたかも「結果が長い時だけlessが起動され、結果が短い時はlessを使わずそのまま出力される」かのような挙動になっていたというわけです。

--RAW-CONTROL-CHARS-R)は、2つ目の問題(ESC[33mのようなゴミが混ざる)を解消する指定です。実はESC[33mなどのゴミのように見える文字列は文字の表示色を変えるための制御コードで、--RAW-CONTROL-CHARSオプションが指定された場合、lessはこの制御コードに基づいて文字の色変えや強調表示を行うようになります。

gitから起動されるlessの挙動を戻す2つの方法

以上の事を踏まえて、git diffgit grepの時のlessの挙動を元に戻す方法は2通り考えられます。

1つは、不足していたオプションを環境変数LESSで指定するようにする方法です。

~/.bashrc
export LESS='--no-init --shift 4 --LONG-PROMPT --RAW-CONTROL-CHARS --quit-if-one-screen'

lessの既定の挙動が全般的に変わってしまっても問題なければ、これが一番簡単でしょう。

全般的な既定の挙動を変えずにgitから起動された時の挙動だけを変えたい場合は、git configでこれらのオプションを加えた状態のlessをページャとして明示的に設定するという方法があります。

$ git config --global core.pager 'less --RAW-CONTROL-CHARS --quit-if-one-screen'

この場合、gitから起動されたlessはここで指定されたオプションと環境変数LESSで指定されたオプションの効果がマージされた挙動となります。

まとめ

以上、lessの既定のオプションの指定の仕方とお薦めの設定、そしてgitとの組み合わせでの注意点をご紹介しました。

lessにはここで登場した物以外にも様々なオプションがあります。man lessを見ながら必要なオプションを環境変数LESSに設定して、普段の作業効率の向上に繋げてください。

タグ: Git
2017-06-27

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
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|