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

ククログ


OSSプロジェクトへのコントリビュートで避けるべき6つのこと:2. 一貫性を壊さない

結城です。

OSSプロジェクトへのコントリビュートの「べからず集」記事について、経験の浅い方が「自分のしようとしていることもそれにあてはまるのではないか?」と心配になってコントリビュートをためらうことがないように、具体的な例と考え方を紹介するシリーズの2本目です。今回は、2つ目の「べからず」として挙げられている、コードの一貫性について説明します。

2. 一貫性を壊さない(コーディングスタイルを合わせる)

プロジェクトオーナーが困惑するケース、変更をマージしてもらえないケースでよくあるのが、コードであったり設計方針だったり運営方針だったりと行ったさまざまなレベルで、そのプロジェクトに「馴染まない」「一貫性を壊す」変更になってしまっているケースです。

元のコードに馴染むコードを書けるかどうかは技術力の有無とはあまり関係が無く、「そもそも馴染ませようという気があるか、ないか」という意識の差異が大きいです。別の角度から言うと、コードの書き方、いわゆるコーディングスタイルが統一されていることの意義をどう捉えているか、ということです。

コーディングスタイルを合わせよう

日本語の文章では、全く同じ内容の文章でも、どこに句読点を入れるか・漢字で書くかひらがなで書くか・「プリンター」と書くか「プリンタ」と書くか、といった「表記揺れ」があり得ます。プログラムにおいても、言語によってはそれと同様に、動作の内容自体は同じでも表記揺れが起こり得る部分があります。

たとえば、JavaScriptでは以下のような表記揺れがあります。

  • 丸括弧や波括弧などの前後での改行の有無、スペースの有無。
    • if(experssion){と書くのか?
    • if ( experssion )≪改行≫{と書くのか?
    • if (≪改行≫experssion≪改行≫)≪改行≫{と書くのか?
  • 省略可能な要素を省略するかどうか。
    • 行末のセミコロン(;)を必ず書くのか、書かないのか?
    • ifでの条件分岐で実行する内容が1つだけのとき、波括弧を使うのかどうか?
  • インデントの仕方。
    • ハードタブ*1なのか、ソフトタブ*2なのか?
    • インデントの深さはスペース2文字分なのか、4文字分なのか、8文字分なのか?
  • 仮引数の書き方。
    • 関数の定義時に、引数の既定の値の定義や、名前付きのパラメータとして受け取る書き方を使うのかどうか?

「そんなの、動けばどっちでもいいじゃないか」と思いますか? だとしたら、それはあなたが、コードの表記が統一されていないことが原因で発生するトラブルを、まだ体験したことがないからでしょう。

実際には、コードの中に表記揺れが多いと、誤読や誤記によって問題が発生する原因になります。また、コードを変更する人がその都度「こっちの書き方がいい」と思った表記に変えてしまうことが繰り返されると、行の変更理由を調べても意味ある情報を得られなくなってしまいます。コードの表記揺れは、百害あって一利なしです。プロジェクトのオーナーは、ただ意地悪や気分で「馴染まない変更」を拒絶するのではなく、プロジェクトにとって現実的なデメリットがあるから拒絶しているのです。

こういった問題に悩まされずに開発に集中できるようにするために、プロジェクト全体で一定のルールに則って書き方を統一し表記揺れをなくすことを、「コーディングスタイルを揃える」と言います。

「外部のコントリビューターがコードの書き方まで揃えるのは大変だから、プロジェクトオーナー側で変更のマージ後に直してくれればいいのに」と思う人もいるかもしれません。実際に、そのような運用を取っているプロジェクトもあります。

しかし、プルリクエストの数が多いと、プロジェクトオーナーはそのような本質的でない作業に忙殺される羽目になってしまいます。また、リポジトリのコミット履歴も本質的でない変更ばかりになってしまいます。「プロジェクトオーナーの手が回らないところを手助けしてプロジェクトに協力する」のがコントリビューションの本質なのに、そんなふうにプロジェクトの運営を妨げてしまっては本末転倒ですよね。

前述のように機械的な判別が容易な部分のコーディングスタイルは、昨今のOSSでは、ソフトウェアで自動的に揃える運用を取っていることが多いです。prettierESLintはそのようなことをするソフトウェアの代表的な例ですし、VisualStudio Codeなどのテキストエディターを適切に設定していれば、ファイルの保存時に自動的にコーディングスタイルを揃えさせることもできます。

そのような運用を取っていないプロジェクトでは、コーディングスタイルを意識して合わせる必要があります。

歴史の長いプロジェクトでは、GNUのコーディング規約Mozillaのコーディング規約のように、文章の形でコーディング規約を明記している場合があります。

そういった規約が特に定められていない場合は、そのプロジェクトの他のコードを見て、変更を加えた箇所のコードの書き方が他の箇所と同じになるように揃えることになります。つまり、変更対象の既存のコードを、変更箇所以外も含めてもう少し読み込んで、傾向を把握する必要があります。

また、これは本題から外れますが、コーディングスタイルをソフトウェアで自動的に揃える運用をまだ取っていないプロジェクトでは、現在のコーディングスタイルを強制するルールをまとめた上で、自動化の仕組みを導入する提案をしてみてもよいでしょう(当然、これはその変更だけの単独のプルリクエストにすることを強くお勧めします)。

語彙の選び方、表現の選び方を合わせよう

多くのOSSのコードでは、クラスや変数などの名前付けに英語の単語や熟語が使われています。同じことを言い表すのにも、考え方次第で色々な表現の仕方があり、どのような表現・表記を主に使うかはプロジェクトによって方針が異なります。特にポピュラーな表現のパターンを、以下にいくつか挙げてみましょう。

複数のデータを保持する変数の名前付けには、いくつかのパターンがあります。

  • itemsなどの、データの内容を表す単語の複数形。
  • itemArray、itemListなどの、データの内容+データを保持している構造を表す接尾辞。

似た例として、個数や件数などの*「数」を保持する変数やプロパティの名前付け*には、以下のようなパターンがあります。

  • item_count
  • item_length
  • item_size
  • n_items(数学や物理で「N個の」といった言い方をすることにちなむ表現)

関数名・メソッド名の動詞にもパターンがあります。たとえば、原形にするか現在形にするかは、何をさせる機能かという目的で変わることが多いです。

  • String#split(分割する)などの原形:データに変更を加える、何かをさせるとき。
  • String#includes(部分文字列を含むかどうか)などの三人称単数形:データの状態を問い合わせ、真偽値で結果を返すとき。

特定の文脈で使われやすい単語や表現にも、いくつかの流儀があります。状態を問い合わせる場面では、以下のような例があります。

  • isItem / hasItem:Yes/Noで答える疑問文のように名付ける。
  • getItemState:状態を尋ねる、という趣旨をそのまま名前に採用する。
  • valid?:Rubyのコードでよく見られるパターンで、真偽値を返す場合の接尾辞として「?」を使う。

データの取得や保存に関わる場面でも、以下のような例があります。

  • getItem / setItem:値を取得したり変更したりする物について、set-getのペアで名付ける。
  • readItem / writeItem:ファイルなど何らかのデバイスやストレージに対してデータを読み書きする場合に、そのことを強調するように名付ける。
  • fetchItem / downloadItem / pushItem / uploadItem :データをネットワーク越しに取得・保存する場合に、そのことを強調するように名付ける。
  • save!:Ruby on Rails(のActiveRecord)に見られるパターンで、破壊的な操作をしたり、操作を強行したりする場合の接尾辞として「!」を使う。

また、複数の単語からなる識別子の名前付け自体にも、以下のようなパターンがあり、それぞれよく使われる場面があります。

  • SomethingName:各単語の先頭1文字を大文字にする。Pascal Case*3、またはUpper Camel Caseと呼ばれる。クラス名で使われることが多い。
  • somethingName:2つ目以降の単語の先頭1文字を大文字にする。Camel Case*4、またはLower Camel Caseと呼ばれる。
  • something_name:単語同士をアンダースコアで連結する。Snake Case*5と呼ばれる。
  • something-name:単語同士をハイフンで連結する。ケバブケース*6、チェインケース、ハイフンケースなどと呼ばれる。ファイル名で使われる。
  • SOMETHING_NAME:単語をすべて大文字にして連結する。定数でよく使われる。

こういった点の名前付けの流儀を合わせることで、変更箇所のコードがより元のコードに「馴染む」ようになり、プロジェクトオーナーにとっても、よりマージしやすいプルリクエストになります。

プロジェクトによっては、こういった名前付けについてもコーディング規約で定めている場合があります。そのような明示的な規約がない場合は、やはり、変更対象の部分やその周囲の部分など、既存のコードを読んで傾向を把握する必要があります。

コミットログの書き方も揃えよう

コードの変更そのものとは無関係ですが、コミットログの書き方の様式も、そのプロジェクトの流儀に合わせるようにしましょう。プロジェクトによってはコミットログをリリースノート作成時の参考資料にすることがあり、あまりに様式から外れていると、リリース時に開発者に余計な苦労を強いることになってしまいます。

コミットログは基本的には、5W1H*7のうちの「Why」にフォーカスをあてて書きます。たとえば以下のような要領です。

  • 「ユーザーがグループに所属していないときにログインに失敗する問題を修正」
  • 「チケットの作成時に空のフィールドを許容しないように変更」

このような情報が含まれないコミットメッセージは、あまり喜ばれません。たとえば以下の要領です。

  • 「変数をリネームした」(何のために?)
  • 「チェックを追加した」(何のチェックを、なぜ?)
  • 「アルゴリズムを変更した」(どういう意味があって?)

プロジェクトによっては、リポジトリに含まれるドキュメントの修正のコミットメッセージ冒頭には「doc:」、自動テストの修正のコミットには「test:」、といった印(プレフィクス)を付けるよう運用で定めている場合もあります。

コードの変更に取りかかるときは、その前に、開発への参加の仕方についての説明があればそれを参照して、運用ルール自体を把握するとよいでしょう。他の変更がどんなコミットメッセージでなされているか、実際のコミットログを確認するのもおすすめです。

まとめ

以上、「一貫性を壊さない(コーディングスタイルを合わせる)」という原則について、実例を挙げて解説してみました。

この記事で紹介した内容以外にも、言語を問わず「読みやすさ」という観点にフォーカスしてコードの語彙を紹介している本として、「リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック(Theory in practice)」(オライリージャパンより、2012年刊行)があります。この本を読んでからOSSのコードに触れると、「あっ、この書き方は読みやすさのための工夫をしている!」という箇所に気付きやすくなり、プロジェクト内でどのような流儀を採用しているかをより読み取りやすくなるでしょう。

この解説は、OSSへのコントリビューションを増やすことを意図した取り組みであるOSS Gateで開催しているワークショップの中で得られた知見をまとめた本、「これでできる! はじめてのOSSフィードバックガイド」の一部を抜粋・再編集した物です。本編ではこのほかにも、問題の報告の仕方やありがちなミス、フィードバック初心者の方が戸惑いがちな点について、なるべく具体例を示しながら、幅広く解説してみています。リンク先では原稿の全文を公開していますが、手元に置いて参照しやすい形式での販売も行っていますので、読書スタイルに合った形式で参照して頂ければ幸いです。

OSS Gateでは、新型コロナウィルスの感染拡大防止の観点から、現在は東京地域を主体としたワークショップをオンライン(Discord)で開催しています。次回開催予定は10月31日(土曜)10:30からで、ビギナー(ワークショップで初めてのフィードバックを体験してみたい人)・サポーター(ビギナーにアドバイスする人)のどちらも参加者を募集中です。ご都合の付く方はぜひエントリーしてみて下さい。

また、当社では企業内での研修としてのOSS Gateワークショップの開催も承っています(例:アカツキさまでの事例)。会社としてOSSへの関わりを増やしていきたいとお考えの企業のご担当者さまは、お問い合わせフォームからご連絡頂けましたら幸いです。

*1 いわゆるタブ文字。

*2 いわゆる半角スペースを何個も書く方法。

*3 Pascalというプログラミング言語でよく見られた書き方であることから。

*4 ラクダのこぶのように見えることから。

*5 ヘビが地を這うイメージから。

*6 肉を串に刺して焼く料理「ケバブ」の様子に似ていることから。

*7 「Who(誰が)」「When(いつ)」「Where(どこで)」「What(何を)」「Why(なぜ)」「How(どのように)」という、ニュース記事を書くときに盛り込むべき情報の一覧を端的に示したもの。

2020-10-28

BrowserSelectorのすすめ - フリーソフトウェアでマルチブラウザをもっと便利にする

企業の運用では複数のブラウザを使い分けることがままあります。例えばこんな具合です。

マルチブラウザ環境の例

「どうして一つのブラウザで両方見ないのか?」という疑問を覚えるかもしれませんが、このような構成を採用する理由には、大きく分けて以下の二種類があります。

  • レガシーブラウザを使う必要がある場合。 典型的には、社内サイトが古いIEにしか対応していない場合がこれに該当します。インターネットでは、既にIEのサポートを終了していて、IEでは正常に表示できなくなっているアプリやサイトが少なくありません。このため、社内サイトの閲覧用に限って、IEを温存させる必要があるというのは、現実の運用では非常によくあるパターンです。

  • セキュリティ的に分けたい場合。 これは用途に応じてブラウザを分けてしまおうというアイデアです。従来から「Web分離」という文脈で論じられてきた対策の一つのバリエーションで、業務用とインターネット閲覧でブラウザを分けることで、堅牢性を高めようというセキュリティ戦略です。

クリアコードでは、こういったユースケースをサポートするためにBrowserSelectorというツールを開発しています。

BrowserSelectorとは何か?

簡単に言えば 「URLごとにブラウザを自動的に切り替えるツール」 です。

冒頭に掲げた図のケースであれば、次のように設定ファイルを置くだけで、自動的にブラウザを切り替えられるようになります。

[Common]
DefaultBrowser=firefox
CloseEmptyTab=1

[URLPatterns]
0001=http*://intra.example.com/*|ie

この基本機能に加えて、次のような機能も備えています。

(機能1)設定の集中管理に対応

  • 共有フォルダに設定ファイルをおくことで、一つの設定ファイルですべての端末の動作を制御できます。
  • 同じように、Windowsのグループポリシーを利用して、レジストリを通じて設定を集中管理することができます。

(機能2)モダンブラウザサポート

  • Google ChromeやMozilla Firefoxなどのブラウザに対応しています。
  • FirefoxやGoogle Chromeでリンクをクリックした際に、他のブラウザをスムーズに起動できます。

(機能3)外部アプリとの連携機能

  • BrowserSelectorをWindowsの「標準のブラウザ」に設定できます。
  • OutlookやWordなどの外部アプリケーションからURLを開いた際に、自動的にブラウザが切り替えられます。

昨年、最初のバージョンを公開してから、導入企業様のフィードバックを集めつつ、着実に機能が増えていっています。

BrowserSelectorの動作イメージ

動作イメージを掴んでもらうために動画を用意しました。

BrowserSelectorを使い始めるには

(1) BrowserSelectorのリリースページにアクセスしてください。

利用方法1

(2) ページのリンクからMSIインストーラをダウンロードして実行します。

利用方法2

(3) 設定ファイルを「C:\Program Files(x86)\ClearCode\BrowserSelector」に配置します。

利用方法3

基本的にこれで利用できるようになります。設定可能な項目の一覧などの詳しい使い方は BrowserSelector利用ガイド (docx/72kb)を配布しているので、そちらを参照してください。

ライセンスについて

BrowserSelectorはMITライセンスで配布されている自由なソフトウェアです。

サポートが必要な場合は、お問い合わせフォームからご連絡ください。

関連リンク

2020-10-27

ノータブルコード13 複数の処理系やバージョンを考慮してライブラリーを検出できるようにする

第13回目のノータブルコードでは、collectdをビルドするときに印象に残ったconfigure.acのコードを紹介します。

collectd はサーバーの情報を収集するのに便利なフリーソフトウェアです。
プラグインを有効にすることで様々なシステム情報を収集できるようになっています。
プラグインをPerl、Python、Luaといったスクリプト言語で記述することもできます。

印象に残ったconfigure.acのコードは、Luaを有効にしてビルドしようとしたときに気づいたものです。

collectdでは、Luaの各種バージョンを自動検出するようにしています。
具体的には以下のバージョンを検出するようにしています。

  • Lua 5.3
  • Lua 5.2
  • Lua 5.1

実際にライブラリーを検出するのには、PKG_CHECK_MODULES を使っています。
どれくらい頑張って検出しようとしているかというと、これぐらい頑張っています。

 PKG_CHECK_MODULES([LUA], [lua],
    [with_liblua="yes"],
    [
      PKG_CHECK_MODULES([LUA], [lua-5.3],
        [with_liblua="yes"],
        [
          PKG_CHECK_MODULES([LUA], [lua5.3],
            [with_liblua="yes"],
            [
              PKG_CHECK_MODULES([LUA], [lua53],
                [with_liblua="yes"],
                [
                  PKG_CHECK_MODULES([LUA], [lua-5.2],
                    [with_liblua="yes"],
                    [
                      PKG_CHECK_MODULES([LUA], [lua5.2],
                        [with_liblua="yes"],
                        [
                          PKG_CHECK_MODULES([LUA], [lua52],
                            [with_liblua="yes"],
                            [
                              PKG_CHECK_MODULES([LUA], [lua-5.1],
                                [with_liblua="yes"],
                                [
                                  PKG_CHECK_MODULES([LUA], [lua5.1],
                                    [with_liblua="yes"],
                                    [
                                      PKG_CHECK_MODULES([LUA], [lua51],
                                        [with_liblua="yes"],
                                        [with_liblua="no (pkg-config cannot find liblua)"]
                                      )
                                    ]
                                  )
                                ]
                              )
                            ]
                          )
                        ]
                      )
                    ]
                  )
                ]
              )
            ]
          )
        ]
      )
    ]
  )

Luaには、JITコンパイラであるLuaJITという実装もあります。
上記のコード片では、LuaJITは自動的に検出できません。

しかし、次のように LIBLUA_PKG_CONFIG_NAME を指定することでLuaJITを有効にできるようになっています。

# --with-liblua {{{
AC_ARG_VAR([LIBLUA_PKG_CONFIG_NAME], [Name of liblua used by pkg-config])
if test "x$LIBLUA_PKG_CONFIG_NAME" != "x"
then
  PKG_CHECK_MODULES([LUA], [$LIBLUA_PKG_CONFIG_NAME],
    [with_liblua="yes"],
    [with_liblua="no"]
  )
else

LuaJITの場合、.pc ファイルが luajit.pc なので、LIBLUA_PKG_CONFIG_NAME=luajit を指定してconfigureを実行すればよいです。

$ ./configure LIBLUA_PKG_CONFIG_NAME=luajit

このようにして、configure.ac で明示的に自動検出するようになっていなくても、pkg-config をサポートしている実装であれば有効にできるように配慮されています。

以上、複数の処理系やバージョンを考慮してライブラリーを検出できるようにしているコードの紹介でした。

2020-10-26

OSSプロジェクトへのコントリビュートで避けるべき6つのこと:1. プルリクエストで複数のことをしない

結城です。

皆さんは「Hacktoberfest」をご存じでしょうか。OSSにコントリビュートする人を増やしたり、OSSへのコントリビューション自体を増やしたりすることを意図して、「どんなOSSプロジェクトでも、どんな内容でも構わないので、GitHub上でプルリクエストを4件作成したらイベントロゴ入りのTシャツが貰える」というルールで毎年開催されているイベントです。

今年の10月初頭、このイベントがきっかけで大量の迷惑なプルリクエスト(spam PR)が発生するという事件がありました*1。この件を承けて書かれた記事の1つとして、OSSプロジェクトへの望ましいコントリビューションの仕方について語った 6 Things to Avoid When Contributing to Open-Source Projects - Qvault という記事があります。こちらの記事で語られている「コントリビュートするときに避けるべき6つのこと(するべき6つのこと)」は、要約すると以下の通りです。

  1. プルリクエストで複数のことをしない(プルリクエストはトピックごとに分ける)
  2. 一貫性を壊さない(コーディングスタイルや設計などを合わせる)
  3. 了承なしに作業を始めない(コンセンサスを得てから開発に着手する)
  4. 既知の問題や解決策があることについて報告しない(報告する前に、既存の報告や情報がないか探す)
  5. 途中の複数のコミットをそのままにしない(git squashで1つのコミットにまとめる)
  6. 無意味な変更を提案しない(意義のある報告や提案をする)

それぞれは妥当な話なのですが、筆者はこの記事について、期待されているような効果はあまりなく、逆に予期しない萎縮効果を生んでしまうのではないか、と懸念しています。

この記事を書いた人は恐らく、spam PRをしてしまう人達に記事を読んでもらいたかったのでしょう。しかし、実際にspam PRをしていた人達にとっては、1から5のことに気をつける動機がありません*2。それに対し、「まだコントリビュートをした経験はないが、OSSにコントリビュートしたいと思っている、向上心のある人」にとっては、この6箇条はむしろ、最初の一歩を踏み出しにくくさせるブレーキとなりえます。その結果としてOSSへの実際のコントリビュートが減ってしまっては、本末転倒です。

ということで、これから何回かのシリーズに分けて、先の記事の6箇条について、可能な限り具体的に・理由を示しながら「こういう考え方はよくない」「こう考えれば問題ない」と説明してみます。先の記事のような「べからず集」を見て萎縮してしまった心を解きほぐす一助となれば幸いです。

まず1つ目は、複数のトピックを含んだプルリクエストについてです。

1. プルリクエストで複数のことをしない(プルリクエストはトピックごとに分ける)

公開のOSSプロジェクトで問題の修正に取り組み始めてみると、「なんだかすごい物」と思っていたOSSのコードが、実は普通のコード……どころか、意外と粗だらけだということに気が付くことがあるかもしれません。

そう感じるのは、不思議なことではありません。というのも、有志の手により開発されているOSSは、「ものすごく技術力の高い人が作っている、完成度の高いソフトウェア」であるとは限らず、「切実にそれを必要としていた人がDIYの精神で作っている、間に合わせのソフトウェア」である場合も多々あるからです。もしかしたら、あなたの方がプロジェクトオーナーより技術力が高い場合すらあるかもしれません。

そういうときに、「この書き方は効率が悪いから駄目だと教わったやつだ」とか、「こういう設計は分かりにくいから避けろと言われたやつだ」といった要領で、本来やりたかったことと無関係ながらついでに直したい箇所が目につくこともあるでしょう。あるいは、「これを応用すればこういう便利な機能も追加できるな」と気が付いて、そっちにも手を出したくなるかもしれません。

ですが、当初の修正と無関係な変更まで「ついで」でやってしまうのは、プルリクエストでは避けた方がよいです。開発者側で変更内容を把握しやすくするために、プルリクエストは1回につき1つの趣旨に則った変更のみに留めましょう。

趣旨が異なる変更とは、どういうものか?

具体的にどういうものがそれにあたるのかを説明します。たとえば、以下のようなJavaScriptのコードがあったとします。

// リンク先のURL文字列を収集して返す
function getLinkURLs() {
  let links = document.querySelectorAll('a[href]');
  // ↓プロパティ名を間違えている(hre ではなく href が正しい)
  let urls = Array.from(links, link => link.hre);
  return urls;
}

// ページ内にある見出し(h1~h6)を収集して返す
function getHeadings() {
  // ↓h2とh3の間に「,」が抜けている('h1,h2,h3,h4,h5,h6' が正しい)
  return document.querySelectorAll('h1,h2h3,h4,h5,h6');
}

見ての通り2箇所に間違いがありますが、こういう場面で「自分にも直せそう!」と思って両方を一度に変更してプルリクエストしてしまうのは良くないです。というのも、この2箇所の間違いは、表面的にはどちらも「誤記」ですが、それぞれ趣旨・性質が異なるからです。問題として報告するなら、この2つは

  • 「リンク先のURLを収集する関数が、実際にはURLを返さない問題」
  • 「見出しを収集する関数が、実際にはh2とh3を収集しない問題」

と、それぞれ別個に報告するのが適切です。

もし1つ目の問題の修正のためのプルリクエストに2つ目の問題の修正が混入していたら、開発者は「えっ、この変更はリンク先のURLと関係なさそうなんだけど……自分の知らない所で、これがリンク関係に影響してたの?」と混乱して、余計な調査に時間を使わないといけなくなります*3

また、後々後退バグが見つかったときの原因調査でも、このように複数の趣旨が混ざった変更があると、調査の妨げになりがちです。複数の趣旨が混ざったコミットやプルリクエストは、プルリクエストの時点でも問題を生むし、その後も問題を生むので、原則としては百害あって一利なしと言えるでしょう。

別の変更に依存する変更、ある変更の前提になる変更

「ある変更をするために、それが依存することになる別の変更もしたい」というケースもよくあります。たとえば以下の要領です。

  • MacBook ProのTouch Barで機能を呼び出せるようにしたい。(本来やりたい変更)
    • そもそもmacOSでの動作に対応していない。そのため、macOSに対応させる所からやらないといけない。(依存する変更)
  • Windows 10の新機能と連係して動作するようにしたい。(本来やりたい変更)
    • その新機能を使うには、依存ライブラリのバージョンを上げないといけない。依存ライブラリの新バージョンは旧バージョンと互換性が無いため、ライブラリを呼び出している部分を広範囲に渡って書き換えなければいけない。(依存する変更)

このようなケースでも、まず最初に「依存する変更」を単独のプルリクエストで行うのがおすすめです。本来やりたかったこと(Touch Bar対応やWindows 10の新機能との連携)が実現されないとしても、前提となる変更自体で得られるものがある場合、それらは本質的には「趣旨の異なる変更」と見なせるからです。

複数の変更を一度にしてもよい場合
コードの見た目の変更

元々直したかった問題を直す過程で、その変更の範囲に自然に含まれる変更であれば、一度に行っても問題ないと言える場合はあります。たとえば、以下のようなコードがあったとしましょう。

function getLinkURLs() {
  let links = document.querySelectorAll('a[href]');
  // ↓プロパティ名を間違えている(hre ではなく href が正しい)
  // ↓この行だけインデントが揃っていない
    let urls = Array.from(links, link => link.hre);
  return urls;
}

この場合、「プロパティ名の間違いを直す」趣旨の変更の一環でインデントも揃えてしまう、ということはよく行われます。動作を変えるために変更しないといけなかった行については、動作を変えない範囲の変更であればついでに行っていい、というのが一般的な慣習だと言っていいでしょう。

ただし、単に「同じ行なら複数の変更をまとめてやってしまってもよい」わけではない、ということには注意してください。1行の中であっても、趣旨の異なる変更は別々のプルリクエストに分けることが望ましいです。

複数の技術レイヤーにまたがる、同じ趣旨の変更

細かいレベルでは別々の変更と言えるけれども、大局的には1つの趣旨にまとめられる、という場合には、1つのプルリクエストにしてよい場合もあります。たとえば、「Firefox 60までにしか対応していなかったFirefox用アドオンを、Firefox 68に対応させるため」という名目で以下の2つの変更を1つのプルリクエストにまとめるのは、問題無いと判断してもらえるかもしれません。

  • Firefox 68で廃止されたJavaScriptのメソッドを使っていた部分を、Firefox 68以降でも動作する書き方に修正する。
  • Firefox 68で廃止されたCSSのプロパティを使っていた部分を、Firefox 68以降でも有効な書き方に修正する。

筆者の個人的な感覚では、この2つをまとめてプルリクエストすることは問題無いと感じます。しかし、プロジェクトオーナーによっては異なる判断をするかもしれません。心配な場合は、プルリクエストのコメントの中で「変更ごとに分けた方がよいでしょうか?(Should I separate this for each change?)」と尋ねてみるか、最初から分けてプルリクエストすることをおすすめします。

まとめ

以上、「複数の事を1つのプルリクエストにしない(プルリクエストはトピックごとに分ける)」という原則について、実例を挙げて解説してみました。

この解説は、OSSへのコントリビューションを増やすことを意図した取り組みであるOSS Gateで開催しているワークショップの中で得られた知見をまとめた本、「これでできる! はじめてのOSSフィードバックガイド」の一部を抜粋・再編集した物です。本編ではこのほかにも、問題の報告の仕方やありがちなミス、フィードバック初心者の方が戸惑いがちな点について、なるべく具体例を示しながら、幅広く解説してみています。リンク先では原稿の全文を公開していますが、手元に置いて参照しやすい形式での販売も行っていますので、読書スタイルに合った形式で参照して頂ければ幸いです。

OSS Gateでは、新型コロナウィルスの感染拡大防止の観点から、現在は東京地域を主体としたワークショップをオンライン(Discord)で開催しています。次回開催予定は10月31日(土曜)10:30からで、ビギナー(ワークショップで初めてのフィードバックを体験してみたい人)・サポーター(ビギナーにアドバイスする人)のどちらも参加者を募集中です。ご都合の付く方はぜひエントリーしてみて下さい。

また、当社では企業内での研修としてのOSS Gateワークショップの開催も承っています(例:アカツキさまでの事例)。会社としてOSSへの関わりを増やしていきたいとお考えの企業のご担当者さまは、お問い合わせフォームからご連絡頂けましたら幸いです。

*1 事件の概要はGIGAZINEの記事にまとめられています。記事によると、前年までも迷惑なプルリクエストあったようですが、今年はそれと比べものにならないレベルで迷惑行為が多発した模様です。そのためHacktoberfestは運用ルールが変更され、現在は「イベントへの協賛を明示的に示したOSSプロジェクトに対してプルリクエストを行い、そのプルリクエストに承認のラベルが付けられた」状態のみカウントされるようになっています。

*2 なぜなら、彼らの動機は「Tシャツを貰うこと」にあり、それを達成するのに最も簡単な方法としてspam PRを選んだだけに過ぎません。1から5のことを守らないとTシャツを貰えないのなら、コストパフォーマンスが悪いので去るだけでしょう。

*3 開発者は今あるコードのすべてを把握できているとは限りません。変更に変更を重ねた結果、自分の意識していないところでの変更が別の所に影響するようになってしまっている、ということはよくあります。そのため、常に変更の副作用を疑う癖が付いているのです。

2020-10-23

fluent-plugin-kinesisのIRSA対応をした話

はじめに

クリアコードはFluentdの開発に参加しています。
Fluentdのプラグインの中で、AWSのkinesisというサービスに対応するプラグイン(fluent-plugin-kinesis)があります。
このプラグインはEKS(AWSのマネージドk8sサービス)でのPod毎に認証トークンを紐づける仕組み(IRSA)に対応していませんでした。
fluent-plugin-kinesisでIRSAによってサービスへのアクセスをEKS上からできるようにした話を書きます。

IAM Roles for Service Accounts (IRSA)という機能はAWSのIAMという概念が分かっていないと解説するのが難しいのでまずは軽く解説します。
その後に、IRSA対応をFluentdプラグインでやるにはどのようにするといいかを解説します。

IAMとは

IAMとはパスワードやアクセスキーを共有せず、柔軟なアクセス制限をユーザーに対して行う仕組みです。
このIAMを使うことで非rootユーザーを作成し、必要な権限を必要なだけユーザーに付与できます。
S3を参照できるだけのIAMを作成しある非ルートユーザーに紐付けると、そのユーザーはS3のバケットのコンテンツしか参照することができない、という状態を作ることができます。

IAM Roles for Service Accounts (IRSA)とは

IAM Roles for Service Accounts (IRSA)という機能はIAMの認証情報をEKS上のPod毎に割り当てられるようにする仕組みです。 *1
k8s上でIAMを割り当てる仕組みはkube2iamkiamなどがあります。
kube2iamに関してはワーカーにIAMを割り当てます。
kiamはノード全体に対してIAMを割り当てます。
これらのFLOSSのソリューションに対して、
IRSAではPodに対してIAMを割り当てられるようになっています。
つまり、Pod毎に最小のIAMロールを与えることが可能になります。

EKSクラスターを準備する

IRSAはAWSのEKSクラスターのPod内の認証方法なので、EKSクラスターを立ち上げます。

EKSクラスターの操作に必要なツールの準備

Amazon EKSクラスターの操作にはawsコマンドのインストールとkubernetesの操作を行うためのkubectlのインストールとAWS EKSのクラスターの操作のためのeksctlのインストールがそれぞれ必要です。

aws clikubectleksctlがそれぞれインストールされたものとして認証情報を入れ込んでいきます。

awsコマンドに認証情報を入れるには、

$ aws configure [--profile awesome_profile]

として対話的にアクセスキーID、シークレットを入力します。
defaultプロファイルではないプロファイルを作成して認証情報を作成する際には--profile profile_nameオプションを追加してください。
無事に入力が完了すると、aws configure listを実行すると以下の表示になります

$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************MPLE shared-credentials-file    
secret_key     ****************mPlE shared-credentials-file    
    region           ap-northeast-1      config-file    ~/.aws/config

ここでは東京リージョンをデフォルトリージョンとして設定しています。

EKSクラスターのデプロイ

AWSにEKSクラスターをデプロイしてみます。kinesis-eksという名前のk8s 1.17のクラスターをデプロイします。

$ eksctl create cluster --name kinesis-eks --approve
[ℹ]  eksctl version 0.29.1
[ℹ]  using region ap-northeast-1
[ℹ]  setting availability zones to [ap-northeast-1d ap-northeast-1c ap-northeast-1a]
[ℹ]  subnets for ap-northeast-1d - public:192.168.0.0/19 private:192.168.96.0/19
[ℹ]  subnets for ap-northeast-1c - public:192.168.32.0/19 private:192.168.128.0/19
[ℹ]  subnets for ap-northeast-1a - public:192.168.64.0/19 private:192.168.160.0/19
[ℹ]  nodegroup "ng-cb168d52" will use "ami-0aa15614ef924fd1e" [AmazonLinux2/1.17]
[ℹ]  using Kubernetes version 1.17
[ℹ]  creating EKS cluster "kinesis-eks" in "ap-northeast-1" region with un-managed nodes
[ℹ]  will create 2 separate CloudFormation stacks for cluster itself and the initial nodegroup
[ℹ]  if you encounter any issues, check CloudFormation console or try 'eksctl utils describe-stacks --region=ap-northeast-1 --cluster=kinesis-eks'
[ℹ]  CloudWatch logging will not be enabled for cluster "kinesis-eks" in "ap-northeast-1"
[ℹ]  you can enable it with 'eksctl utils update-cluster-logging --enable-types={SPECIFY-YOUR-LOG-TYPES-HERE (e.g. all)} --region=ap-northeast-1 --cluster=kinesis-eks'
[ℹ]  Kubernetes API endpoint access will use default of {publicAccess=true, privateAccess=false} for cluster "kinesis-eks" in "ap-northeast-1"
[ℹ]  2 sequential tasks: { create cluster control plane "kinesis-eks", 2 sequential sub-tasks: { no tasks, create nodegroup "ng-cb168d52" } }
[ℹ]  building cluster stack "eksctl-kinesis-eks-cluster"
[ℹ]  deploying stack "eksctl-kinesis-eks-cluster"
[ℹ]  building nodegroup stack "eksctl-kinesis-eks-nodegroup-ng-cb168d52"
[ℹ]  --nodes-min=2 was set automatically for nodegroup ng-cb168d52
[ℹ]  --nodes-max=2 was set automatically for nodegroup ng-cb168d52
[ℹ]  deploying stack "eksctl-kinesis-eks-nodegroup-ng-cb168d52"
[ℹ]  waiting for the control plane availability...
[✔]  saved kubeconfig as "~/.kube/config"
[ℹ]  no tasks
[✔]  all EKS cluster resources for "kinesis-eks" have been created
[ℹ]  adding identity "arn:aws:iam::788574296432:role/eksctl-kinesis-eks-nodegroup-ng-c-NodeInstanceRole-J3Y9CGYRQ8MB" to auth ConfigMap
[ℹ]  nodegroup "ng-cb168d52" has 0 node(s)
[ℹ]  waiting for at least 2 node(s) to become ready in "ng-cb168d52"
[ℹ]  nodegroup "ng-cb168d52" has 2 node(s)
[ℹ]  node "ip-192-168-53-252.ap-northeast-1.compute.internal" is ready
[ℹ]  node "ip-192-168-6-22.ap-northeast-1.compute.internal" is ready
[ℹ]  kubectl command should work with "~/.kube/config", try 'kubectl get nodes'
[✔]  EKS cluster "kinesis-eks" in "ap-northeast-1" region is ready

数十分待つと、デプロイが完了しました。

OIDCプロバイダーをEKSクラスターにデプロイします。

$ eksctl utils associate-iam-oidc-provider --cluster kinesis-eks --approve
[ℹ]  eksctl version 0.29.1
[ℹ]  using region ap-northeast-1
[ℹ]  will create IAM Open ID Connect provider for cluster "kinesis-eks" in "ap-northeast-1"
[✔]  created IAM Open ID Connect provider for cluster "kinesis-eks" in "ap-northeast-1"

デプロイできました。

IRSAで利用するIAMの認証情報を保持するサービスアカウントもEKSクラスターへデプロイしましょう。
KinesisプラグインのIRSA認証を試すのに、今回はKinesis Data Firehoseというサービスを使ってIRSA認証の動作確認をします。
この記事ではポリシーの作成手順を省略するため、AWS Kinesis Firehoseサービスを全て制御できる既存のポリシーを利用します。

従って、この記事で作成するEKSクラスタに作成するサービスアカウントに紐付けるKinesis Firehoseの権限にはarn:aws:iam::aws:policy/AmazonKinesisFirehoseFullAccessを指定します。 *2

$ eksctl create iamserviceaccount --name kinesis-eks-serviceaccount --namespace default --cluster kinesis-eks  --attach-policy-arn arn:aws:iam::aws:policy/AmazonKinesisFirehoseFullAccess --approve 
[ℹ]  eksctl version 0.29.1
[ℹ]  using region ap-northeast-1
[ℹ]  1 existing iamserviceaccount(s) (kube-system/aws-node) will be excluded
[ℹ]  1 iamserviceaccount (default/kinesis-eks-serviceaccount) was included (based on the include/exclude rules)
[ℹ]  1 iamserviceaccount (kube-system/aws-node) was excluded (based on the include/exclude rules)
[!]  serviceaccounts that exists in Kubernetes will be excluded, use --override-existing-serviceaccounts to override
[ℹ]  1 task: { 2 sequential sub-tasks: { create IAM role for serviceaccount "default/kinesis-eks-serviceaccount", create serviceaccount "default/kinesis-eks-serviceaccount" } }
[ℹ]  building iamserviceaccount stack "eksctl-kinesis-eks-addon-iamserviceaccount-default-kinesis-eks-serviceaccount"
[ℹ]  deploying stack "eksctl-kinesis-eks-addon-iamserviceaccount-default-kinesis-eks-serviceaccount"
[ℹ]  created serviceaccount "default/kinesis-eks-serviceaccount"

kubectlでサービスアカウントが作成できているかを確認します。

$ kubectl get serviceaccounts
NAME                         SECRETS   AGE
default                      1         17m
kinesis-eks-serviceaccount   1         3m42s

サービスアカウントが作成できていることが確認できました。

ここまでで、IRSAの動作確認をするためのEKSクラスターを立てることができました。

fluent-plugin-kinesisにIRSA対応を入れる

fluent-plugin-kinesisの開発元には、この記事を執筆している時点ではIRSA対応が入っていません。
AWS SDK Rubyのドキュメントを参照すると、IRSAの認証トークンを処理するクラスはAws::AssumeRoleWebIdentityCredentialsクラスであることがわかります。
つまり、このIRSAを処理するクラスを使って認証情報を取得すると良いことになります。

fluent-plugin-kinesisではAWSの認証情報を取り扱う共通モジュールがあります。
このモジュールにはAws::AssumeRoleWebIdentityCredentialsクラスを扱うコードパスがないため、以下のようにしてそのコードパスを追加します。

diff --git a/lib/fluent/plugin/kinesis_helper/client.rb b/lib/fluent/plugin/kinesis_helper/client.rb
index ba6da75..6870883 100644
--- a/lib/fluent/plugin/kinesis_helper/client.rb
+++ b/lib/fluent/plugin/kinesis_helper/client.rb
@@ -46,6 +46,20 @@ module Fluent
             desc "A URL for a regional STS API endpoint, the default is global"
             config_param :sts_endpoint_url, :string, default: nil
           end
+          # Refer to the following link for additional parameters that could be added:
+          # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/STS/Client.html#assume_role_with_web_identity-instance_method
+          config_section :web_identity_credentials, multi: false do
+            desc "The Amazon Resource Name (ARN) of the role to assume"
+            config_param :role_arn, :string
+            desc "An identifier for the assumed role session"
+            config_param :role_session_name, :string
+            desc "The absolute path to the file on disk containing the OIDC token"
+            config_param :web_identity_token_file, :string, default: nil #required
+            desc "An IAM policy in JSON format"
+            config_param :policy, :string, default: nil
+            desc "The duration, in seconds, of the role session (900-43200)"
+            config_param :duration_seconds, :time, default: nil
+          end
           config_section :instance_profile_credentials, multi: false do
             desc "Number of times to retry when retrieving credentials"
             config_param :retries, :integer, default: nil
@@ -149,6 +163,17 @@ module Fluent
                 credentials_options[:client] = Aws::STS::Client.new(region: @region)
             end
             options[:credentials] = Aws::AssumeRoleCredentials.new(credentials_options)
+          when @web_identity_credentials
+            c = @web_identity_credentials
+            credentials_options[:role_arn] = c.role_arn
+            credentials_options[:role_session_name] = c.role_session_name
+            credentials_options[:web_identity_token_file] = c.web_identity_token_file
+            credentials_options[:policy] = c.policy if c.policy
+            credentials_options[:duration_seconds] = c.duration_seconds if c.duration_seconds
+            if @region
+              credentials_options[:client] = Aws::STS::Client.new(:region => @region)
+            end
+            options[:credentials] = Aws::AssumeRoleWebIdentityCredentials.new(credentials_options)
           when @instance_profile_credentials
             c = @instance_profile_credentials
             credentials_options[:retries] = c.retries if c.retries

これで、以下のように<web_identity_credentials>セクションを記入すると、IRSAによる認証情報がkinesisプラグインに渡されるようになります。

    region ap-northeast-1
    <web_identity_credentials>
      role_arn          "#{ENV['AWS_ROLE_ARN']}"
      role_session_name test-kinesis-session-name
      web_identity_token_file "#{ENV['AWS_WEB_IDENTITY_TOKEN_FILE']}"
    </web_identity_credentials>

動作確認用のDockerコンテナを作成する

EKSにPodとして載せるのであれば、Dockerコンテナを作成する必要があります。
Dockerコンテナとしても動くようにするため、設定ファイルは自己完結的にします。

FROM fluent/fluentd:v1.11.3-debian-1.0

LABEL maintainer="Hiroshi Hatake <hatake@clear-code.com>"
USER root
WORKDIR /home/fluent
ENV PATH /fluentd/vendor/bundle/ruby/2.6.0/bin:$PATH
ENV GEM_PATH /fluentd/vendor/bundle/ruby/2.6.0
ENV GEM_HOME /fluentd/vendor/bundle/ruby/2.6.0
# skip runtime bundler installation
ENV FLUENTD_DISABLE_BUNDLER_INJECTION 1

COPY Gemfile* /fluentd/
COPY fluent-plugin-kinesis-*.gem /fluentd/

RUN buildDeps="sudo make gcc g++ libc-dev libffi-dev" \
  runtimeDeps="" \
      && apt-get update \
     && apt-get upgrade -y \
     && apt-get install \
     -y --no-install-recommends \
     $buildDeps $runtimeDeps net-tools \
    && gem install /fluentd/fluent-plugin-kinesis-*.gem --no-document \
    && gem sources --clear-all \
    && SUDO_FORCE_REMOVE=yes \
    apt-get purge -y --auto-remove \
                  -o APT::AutoRemove::RecommendsImportant=false \
                  $buildDeps \
 && rm -rf /var/lib/apt/lists/* \
    && gem sources --clear-all \
    && rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem

# Copy configuration files
COPY ./conf/fluent.conf /fluentd/etc/
COPY ./conf/conf.d/* /fluentd/etc/conf.d/

# Copy plugins
# COPY plugins /fluentd/plugins/
COPY entrypoint.sh /fluentd/entrypoint.sh

# Environment variables
ENV FLUENTD_OPT=""
ENV FLUENTD_CONF="fluent.conf"

# Overwrite ENTRYPOINT to run fluentd as root for /var/log / /var/lib
ENTRYPOINT ["tini", "--", "/fluentd/entrypoint.sh"]

設定ファイルなどは以下。

entrypoint.sh

Dockerコンテナのエントリポイントのスクリプトを作成します。

#!/usr/bin/env sh

exec fluentd -c /fluentd/etc/${FLUENTD_CONF} -p /fluentd/plugins --gemfile /fluentd/Gemfile ${FLUENTD_OPT}
conf/fluent.conf
@include conf.d/*.conf

<label @mainstream>
  <match **>
    @type kinesis_firehose
    @id out_kinesis_firehose
    region ap-northeast-1
    delivery_stream_name "#{ENV['FIREHOSE_STREAM_NAME'] || fluentd-streams}"
    <web_identity_credentials>
      role_arn          "#{ENV['AWS_ROLE_ARN']}"
      role_session_name test-kinesis-session-name
      web_identity_token_file "#{ENV['AWS_WEB_IDENTITY_TOKEN_FILE']}"
    </web_identity_credentials>
    <buffer>
      flush_interval 10s
      chunk_limit_size 1m
      flush_thread_burst_interval 1
      flush_thread_count 2
    </buffer>
  </match>
</label>
conf/conf.d/source.conf
<source>
  @type  forward
  @id    input1
  @label @mainstream
  port   24224
</source>

<source>
  @type sample
  @label @mainstream
  rate 1
  tag raw.sample
</source>

<filter **>
  @type stdout
</filter>

コンテナをビルドします。
今回はテスト用のコンテナを登録するリポジトリをDockerHubに作成しておきました。

docker build . -t cosmo0920/fluent-plugin-kinesis-test:latest
Sending build context to Docker daemon  57.86kB
Step 1/17 : FROM fluent/fluentd:v1.11.3-debian-1.0
 ---> 2f58cab1fbc5
Step 2/17 : LABEL maintainer="Hiroshi Hatake <hatake@clear-code.com>"
 ---> Using cache
 ---> e764fb51b05e
Step 3/17 : USER root
 ---> Using cache
 ---> ad1cbc950fc4
Step 4/17 : WORKDIR /home/fluent
 ---> Using cache
 ---> 6ab5210d0cca
Step 5/17 : ENV PATH /fluentd/vendor/bundle/ruby/2.6.0/bin:$PATH
 ---> Using cache
 ---> f73ed93424a7
Step 6/17 : ENV GEM_PATH /fluentd/vendor/bundle/ruby/2.6.0
 ---> Using cache
 ---> 8934937eb22c
Step 7/17 : ENV GEM_HOME /fluentd/vendor/bundle/ruby/2.6.0
 ---> Using cache
 ---> 1576b82df73b
Step 8/17 : ENV FLUENTD_DISABLE_BUNDLER_INJECTION 1
 ---> Using cache
 ---> a7e2b0997fc1
Step 9/17 : COPY Gemfile* /fluentd/
 ---> Using cache
 ---> df851794c5a0
Step 10/17 : COPY fluent-plugin-kinesis-*.gem /fluentd/
 ---> Using cache
 ---> 9e7ad33fb02c
Step 11/17 : RUN buildDeps="sudo make gcc g++ libc-dev libffi-dev"   runtimeDeps=""       && apt-get update      && apt-get upgrade -y      && apt-get install      -y --no-install-recommends      $buildDeps $runtimeDeps net-tools     && gem install /fluentd/fluent-plugin-kinesis-*.gem --no-document     && gem sources --clear-all     && SUDO_FORCE_REMOVE=yes     apt-get purge -y --auto-remove                   -o APT::AutoRemove::RecommendsImportant=false                   $buildDeps  && rm -rf /var/lib/apt/lists/*     && gem sources --clear-all     && rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem
 ---> Using cache
 ---> 557e659729b6
Step 12/17 : COPY ./conf/fluent.conf /fluentd/etc/
 ---> 56e9c62c231a
Step 13/17 : COPY ./conf/conf.d/* /fluentd/etc/conf.d/
 ---> a2206c909b34
Step 14/17 : COPY entrypoint.sh /fluentd/entrypoint.sh
 ---> 5d4646527c9a
Step 15/17 : ENV FLUENTD_OPT=""
 ---> Running in 00794bc3a32a
Removing intermediate container 00794bc3a32a
 ---> 6a5a3dc5496b
Step 16/17 : ENV FLUENTD_CONF="fluent.conf"
 ---> Running in aa963bc519be
Removing intermediate container aa963bc519be
 ---> 572c5a904178
Step 17/17 : ENTRYPOINT ["tini", "--", "/fluentd/entrypoint.sh"]
 ---> Running in d68c0af31d16
Removing intermediate container d68c0af31d16
 ---> c765c2784157
Successfully built c765c2784157
Successfully tagged cosmo0920/fluent-plugin-kinesis-test:latest

テスト用のコンテナを登録するリポジトリにビルドしたDockerイメージをプッシュします。

$ docker push cosmo0920/fluent-plugin-kinesis-test:latest
The push refers to repository [docker.io/cosmo0920/fluent-plugin-kinesis-test]
456dfd0e8641: Pushed 
77a5ea4872ea: Pushed 
0270d19e61c5: Pushed 
59b09b04fdb6: Layer already exists 
24cc1822f3a7: Layer already exists 
584adec8072e: Layer already exists 
b836309a29a2: Layer already exists 
6326e503330a: Layer already exists 
781b24f9ba07: Layer already exists 
1302bac58683: Layer already exists 
1563364acccb: Layer already exists 
04cbaaf60ef1: Layer already exists 
2e9de320a378: Layer already exists 
15364b93b273: Layer already exists 
d85310698a88: Layer already exists 
07cab4339852: Layer already exists 
latest: digest: sha256:17c80a7d68941636921fb4e31297b1d37c85a87e1fdf04f6c99c06dd4419f08a size: 3659

EKSクラスタを用いて動作確認する

動作確認に用いたFirehoseのサービスの構成

AWS Kinesis Data Firehoseはストリーム名をfluentd-streams、配信先をS3のfluentd-eks-firehoseバケットに、送信方法はDirect PUT and other sourcesを指定してあります。
今回の記事はIRSA対応を主に解説したい記事のため、設定方法の解説は省略します。

k8sのPodとその周辺の定義ファイルを用意する

k8sではConfigMapにより、Podで使用する設定ファイルなどの内容を差し替える機能があります。
作成したテスト用Dockerfileでも動作確認を行うことは可能ですが、in_sampleからのサンプルイベントを流した時に標準出力でイベントの内容を確認できない状態になっているため、その辺りを修正したConfigMapを定義します。

また、fluent-plugin-kinesisが動作するEKSのPodにfirehoseのIRSAの認証情報をアタッチしたいため、このサービスアカウントの情報もPodの定義ファイルに記入します。

deployment.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
  namespace: default
  labels:
    k8s-app: fluentd-logging
    version: v1
data:
  fluent.conf: |
    <source>
      @type  forward
      @id    input1
      @label @mainstream
      port   24224
    </source>

    <source>
      @type sample
      @label @mainstream
      rate 1
      tag raw.sample
    </source>

    <label @mainstream>
      <filter **>
        @type stdout
      </filter>

      <match **>
        @type kinesis_firehose
        @id out_kinesis_firehose
        region ap-northeast-1
        delivery_stream_name fluentd-streams
        <web_identity_credentials>
          role_arn          "#{ENV['AWS_ROLE_ARN']}"
          role_session_name test-kinesis-session-name
          web_identity_token_file "#{ENV['AWS_WEB_IDENTITY_TOKEN_FILE']}"
        </web_identity_credentials>
        <buffer>
          flush_interval 10s
          chunk_limit_size 1m
          flush_thread_burst_interval 1
          flush_thread_count 2
        </buffer>
      </match>
    </label>
---
apiVersion: v1
kind: Pod
metadata:
  name: fluentd-kinesis-test
  namespace: default
  labels:
    k8s-app: fluentd-logging
    version: v1
spec:
  serviceAccountName: kinesis-eks-serviceaccount
  containers:
  - image: docker.io/cosmo0920/fluent-plugin-kinesis-test:latest
    name: fluentd-kinesis-test
    volumeMounts:
    - name: config-volume
      mountPath: /fluentd/etc
  volumes:
  - name: config-volume
    configMap:
      name: fluentd-config

EKSクラスタにデプロイします。

$ kubectl apply -f deployment.yaml
configmap/fluentd-config created
pod/fluentd-kinesis-test created

デプロイした直後はコンテナが作成されているという表示になります。

$ kubectl get pods
NAME                   READY   STATUS              RESTARTS   AGE
fluentd-kinesis-test   0/1     ContainerCreating   0          10s

しばらく経つと、STATUSRunningに変わります。

$ kubectl get pods
NAME                   READY   STATUS    RESTARTS   AGE
fluentd-kinesis-test   1/1     Running   0          14s

fluent-kinesis-testという名前のPodのログを取得してみます。

$ kubectl logs fluentd-kinesis-test
2020-10-15 08:21:16 +0000 [info]: parsing config file is succeeded path="/fluentd/etc/fluent.conf"
2020-10-15 08:21:16 +0000 [info]: gem 'fluent-plugin-kinesis' version '3.2.3'
2020-10-15 08:21:16 +0000 [info]: gem 'fluentd' version '1.11.4'
2020-10-15 08:21:16 +0000 [warn]: both of Plugin @id and path for <storage> are not specified. Using on-memory store.
2020-10-15 08:21:16 +0000 [warn]: both of Plugin @id and path for <storage> are not specified. Using on-memory store.
2020-10-15 08:21:16 +0000 [info]: using configuration file: <ROOT>
  <source>
    @type forward
    @id input1
    @label @mainstream
    port 24224
  </source>
  <source>
    @type sample
    @label @mainstream
    rate 1
    tag "raw.sample"
  </source>
  <label @mainstream>
    <filter **>
      @type stdout
    </filter>
    <match **>
      @type kinesis_firehose
      @id out_kinesis_firehose
      region "ap-northeast-1"
      delivery_stream_name "fluentd-streams"
      <web_identity_credentials>
        role_arn "arn:aws:iam::123456789012:role/eksctl-kinesis-eks-addon-iamserviceaccount-d-Role1-SUPERPOWER"
        role_session_name "test-kinesis-session-name"
        web_identity_token_file "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
      </web_identity_credentials>
      <buffer>
        flush_interval 10s
        chunk_limit_size 1m
        flush_thread_burst_interval 1
        flush_thread_count 2
      </buffer>
    </match>
  </label>
</ROOT>
2020-10-15 08:21:16 +0000 [info]: starting fluentd-1.11.4 pid=6 ruby="2.6.6"
2020-10-15 08:21:16 +0000 [info]: spawn command to main:  cmdline=["/usr/local/bin/ruby", "-Eascii-8bit:ascii-8bit", "/fluentd/vendor/bundle/ruby/2.6.0/bin/fluentd", "-c", "/fluentd/etc/fluent.conf", "-p", "/fluentd/plugins", "--gemfile", "/fluentd/Gemfile", "--under-supervisor"]
2020-10-15 08:21:17 +0000 [info]: adding filter in @mainstream pattern="**" type="stdout"
2020-10-15 08:21:17 +0000 [info]: adding match in @mainstream pattern="**" type="kinesis_firehose"
2020-10-15 08:21:17 +0000 [info]: adding source type="forward"
2020-10-15 08:21:17 +0000 [info]: adding source type="sample"
2020-10-15 08:21:17 +0000 [warn]: #0 both of Plugin @id and path for <storage> are not specified. Using on-memory store.
2020-10-15 08:21:17 +0000 [warn]: #0 both of Plugin @id and path for <storage> are not specified. Using on-memory store.
2020-10-15 08:21:17 +0000 [info]: #0 starting fluentd worker pid=11 ppid=6 worker=0
2020-10-15 08:21:17 +0000 [info]: #0 [input1] listening port port=24224 bind="0.0.0.0"
2020-10-15 08:21:17 +0000 [info]: #0 fluentd worker is now running worker=0
2020-10-15 08:21:18.081312494 +0000 raw.sample: {"message":"sample"}
2020-10-15 08:21:19.082577481 +0000 raw.sample: {"message":"sample"}
2020-10-15 08:21:20.083657521 +0000 raw.sample: {"message":"sample"}
2020-10-15 08:21:21.084679969 +0000 raw.sample: {"message":"sample"}
# ...

しばらくすると、S3にfirehoseに入れられたログが入れられます。
aws s3 lsコマンドを実行すると、バケットが作成されていることが分かります。

$ aws s3 ls
# ...
2020-10-15 11:52:43 fluentd-eks-firehose

ここまでで、下記のAWS Kinesis FirehoseによるS3へのログの送信の動作が確認できました。

Fluentd Kinesis plugin Pod ---> AWS Kinesis Data Firehose ---> S3 bucket

EKSクラスタを削除する

動作確認が終了したので、EKSクラスタを削除します。

$ eksctl delete cluster --name kinesis-eks
[ℹ]  eksctl version 0.29.1
[ℹ]  using region ap-northeast-1
[ℹ]  deleting EKS cluster "kinesis-eks"
[ℹ]  deleted 0 Fargate profile(s)
[✔]  kubeconfig has been updated
[ℹ]  cleaning up AWS load balancers created by Kubernetes objects of Kind Service or Ingress
[ℹ]  3 sequential tasks: { delete nodegroup "ng-cb168d52", 2 sequential sub-tasks: { 2 sequential sub-tasks: { delete IAM role for serviceaccount "default/kinesis-eks-serviceaccount", delete serviceaccount "default/kinesis-eks-serviceaccount" }, delete IAM OIDC provider }, delete cluster control plane "kinesis-eks" [async] }
[ℹ]  will delete stack "eksctl-kinesis-eks-nodegroup-ng-cb168d52"
[ℹ]  waiting for stack "eksctl-kinesis-eks-nodegroup-ng-cb168d52" to get deleted
[ℹ]  will delete stack "eksctl-kinesis-eks-addon-iamserviceaccount-default-kinesis-eks-serviceaccount"
[ℹ]  waiting for stack "eksctl-kinesis-eks-addon-iamserviceaccount-default-kinesis-eks-serviceaccount" to get deleted
[ℹ]  deleted serviceaccount "default/kinesis-eks-serviceaccount"
[ℹ]  will delete stack "eksctl-kinesis-eks-cluster"
[✔]  all cluster resources were deleted

kinesis-eksという名前のEKSクラスタは削除されました。

開発元にIRSA対応のパッチをフィードバック

作成したIRSA対応のパッチにについてはkinesisプラグインの開発元にフィードバック済みです。
https://github.com/awslabs/aws-fluent-plugin-kinesis/pull/208
Issueチケットも前後してしまいましたが、切っています。
https://github.com/awslabs/aws-fluent-plugin-kinesis/issues/209

この記事では解説していませんがIRSA対応を行うにあたって、テストコードの追加も行っていますので興味のある方は上記URLから確認してみてください。

まとめ

fluent-plugin-kinesisのIRSA対応を実施した話を説明してみました。
AWSでは全てのリソースへリソースネーム(ARN)が振られています。
このリソースネームはEKSクラスタ上に作成したサービスアカウントにも振られています。
IRSAの仕組みに載っかってPodにサービスアカウントを紐づけると、動作しているPodにはAWS_ROLE_ARN環境変数とAWS_WEB_IDENTITY_TOKEN_FILE環境変数が紐づきます。
これらにより、それぞれサービスアカウントに紐づいたリソースネーム(ARN)と認証情報が記載されたファイルパスが参照可能になります。
この二つの環境変数はサービスアカウントを紐付けたPodであれば自動的に付与される情報のため、
EKSクラスターを使用するユーザーは定義ファイルにてこれら二つの環境変数を参照すると、適切な認証情報をPod内のプログラムに与えることができます。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。

*1 公式の詳細な解説記事はこちらです: https://aws.amazon.com/jp/blogs/news/introducing-fine-grained-iam-roles-service-accounts/

*2 当然のことながら、プロダクションで使う際にはFullAccessの権限ではなく、より細分化したIAM Roleのarnを指定するべきです

タグ: Fluentd
2020-10-21

libvirtを使ってppc64leの検証環境を立ち上げる話

はじめに

クリアコードはFluentdの開発に参加しています。
Fluentd本体といくつかのプラグインがセットになったtd-agentというパッケージが提供されていますが、
td-agentのversion 4からaarch64(arm64)やppc64le(PowerPC)といった非x86_64環境にもパッケージの提供を開始しました。

そのためtd-agent4のppc64leパッケージの動作確認をする必要がありましたが、実ハードが無いのでx86_64上のqemu-system-ppc64leで動作確認を行いました。
QEMUとは、FLOSSのプロセッサシミュレーターです。

今回はその手順を畑ケが紹介します。 *1

libvirtでppc64leのCentOS 8環境を立ち上げるには

libvirtでppc64leのCentOS 8環境をインストールする

libvirtにてppc64leのCentOS 8環境を立ち上げるには、QEMUのディスクイメージを作成する必要があります。

libvirtを使うホスト環境はUbuntu 18.04.5 LTS amd64環境です。

今回の動作確認に必要なコマンドをインストールします。

$ sudo apt install libvirt-clients virtinst libvirt-daemon-system qemu-utils qemu-system-ppc

CentOS 8 ppc64leのDVDイメージを取得します。

http://isoredirect.centos.org/centos/8.2.2004/isos/ppc64le/ より、近いミラーからCentOS 8のイメージを取得します。
ネットワークインターフェースの設定を行えばDVDイメージでなくてもインストールできますが、
今回はDVDイメージをダウンロードしてきました。

CentOS 8の最新版のDVD ISOイメージのCentOS-8.2.2004-ppc64le-dvd1.isoを入手しました。
CentOS 8をインストールする準備が整ったので、ppc64leのCentOS 8のDVDからブートしてみます。*2

$ sudo virt-install --name=centos8-ppc64le --ram=4096 --vcpus=4 \
                    --os-type=linux --os-variant=rhel7 --arch=ppc64le \
                    --machine pseries-2.12 \
                    --disk=/var/lib/libvirt/images/centos8-ppc64le.img,format=qcow2,size=20 \
                    --cdrom=CentOS-8.2.2004-ppc64le-dvd1.iso \
                    --serial=pty --console=pty --boot=hd --graphics=none

ネットワークにはlibvirtが作成しているデフォルトネットワーク(virbr0)を指定します。

CentOSのインストーラが起動するので、指示に従ってインストールします。

後続の節ではCentOS 8 ppc64le環境が出来たものとして説明します。

libvirt環境のCentOS 8 ppc64leにパッケージをインストールするには

virt-installでネットワークの設定を有効化してインストールした場合、ゲストのip aの出力は以下のようになります。

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:2b:5e:d1 brd ff:ff:ff:ff:ff:ff
    inet 192.168.123.250/24 brd 192.168.123.255 scope global dynamic noprefixroute enp0s1
       valid_lft 3462sec preferred_lft 3462sec
    inet6 fe80::2e8c:ddc7:91ff:f5fe/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

ゲストからインターネットにも出られます。

$ ping -c4 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=58 time=9.95 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=58 time=8.37 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=58 time=7.60 ms
64 bytes from 1.1.1.1: icmp_seq=4 ttl=58 time=7.72 ms

--- 1.1.1.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 9ms
rtt min/avg/max/mdev = 7.599/8.410/9.952/0.939 ms

libvirtで立ち上げているゲストへホスト環境からscpでファイルを送ります。
今回作成した仮想マシンには192.168.123.250が割り当てられています。

$ scp /path/to/td-agent-4.0.1-1.el8.ppc64le.rpm <awesome_user>@192.168.123.250:

libvirtで動作しているゲストのCentOS 8 ppc64le環境にてdnfを使ってscpを使って転送してきたパッケージをインストールします。

$ sudo dnf install -y td-agent-4.0.1-1.el8.ppc64le.rpm
[sudo] password for user: 
Failed to set locale, defaulting to C.UTF-8
CentOS-8 - AppStream                                                                  2.0 MB/s | 5.0 MB     00:02    
CentOS-8 - Base                                                                       1.9 MB/s | 1.9 MB     00:01    
CentOS-8 - Extras                                                                     9.4 kB/s | 8.1 kB     00:00    
Last metadata expiration check: 0:00:01 ago on Mon Oct 19 22:28:38 2020.
Dependencies resolved.
======================================================================================================================
 Package                   Architecture             Version                       Repository                     Size
======================================================================================================================
Installing:
 td-agent                  ppc64le                  4.0.1-1.el8                   @commandline                   29 M

Transaction Summary
======================================================================================================================
Install  1 Package

Total size: 29 M
Installed size: 96 M
Downloading Packages:
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                                                              1/1 
  Running scriptlet: td-agent-4.0.1-1.el8.ppc64le                                                                 1/1 
  Installing       : td-agent-4.0.1-1.el8.ppc64le                                                                 1/1 
  Running scriptlet: td-agent-4.0.1-1.el8.ppc64le                                                                 1/1 
Created symlink /etc/systemd/system/multi-user.target.wants/td-agent.service \u2192 /usr/lib/systemd/system/td-agent.service.
prelink detected. Installing /etc/prelink.conf.d/td-agent-ruby.conf ...

  Verifying        : td-agent-4.0.1-1.el8.ppc64le                                                                 1/1 

Installed:
  td-agent-4.0.1-1.el8.ppc64le                                                                                        

Complete!

libvirtゲスト環境にてtd-agentサービスを上げてみます。

$ sudo systemctl start td-agent

libvirtゲスト環境にて立ち上がっていることを確認します。

$ systemctl status td-agent
● td-agent.service - td-agent: Fluentd based data collector for Treasure Data
   Loaded: loaded (/usr/lib/systemd/system/td-agent.service; enabled; vendor pr>
   Active: inactive (dead)
     Docs: https://docs.treasuredata.com/articles/td-agent
$ sudo systemctl start td-agent
$ sudo systemctl status td-agent
● td-agent.service - td-agent: Fluentd based data collector for Treasure Data
   Loaded: loaded (/usr/lib/systemd/system/td-agent.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2020-10-19 22:32:44 EDT; 17s ago
     Docs: https://docs.treasuredata.com/articles/td-agent
  Process: 9127 ExecStart=/opt/td-agent/bin/fluentd --log $TD_AGENT_LOG_FILE --daemon /var/run/td-agent/td-agent.pid >
 Main PID: 9236 (fluentd)
    Tasks: 4 (limit: 22015)
   Memory: 134.6M
   CGroup: /system.slice/td-agent.service
           ├─9236 /opt/td-agent/bin/ruby /opt/td-agent/bin/fluentd --log /var/log/td-agent/td-agent.log --daemon /var>
           └─9239 /opt/td-agent/bin/ruby -Eascii-8bit:ascii-8bit /opt/td-agent/bin/fluentd --log /var/log/td-agent/td>

Oct 19 22:32:12 localhost.localdomain systemd[1]: Starting td-agent: Fluentd based data collector for Treasure Data...
Oct 19 22:32:44 localhost.localdomain systemd[1]: Started td-agent: Fluentd based data collector for Treasure Data.

無事に立ち上がっている事を確認しました。

まとめ

td-agent 4のppc64leのパッケージを題材にppc64leのQEMU環境を作成し、実際にパッケージをインストールできるか、また、サービスとして立ち上げられるかを確認した時のやり方を思い出しながらまとめました。
FLOSSを触っていると大抵の場合x86_64環境や、行ってもaarch64(arm64)環境がせいぜいのため、このような大掛かりなエミュレーション環境を準備する事は少ないかもしれません。
しかし、Fluentdのような大きなプロジェクトになるとPowerPC環境でのパッケージを必要とされることがあります。その場合にも慌てないでエミュレーション環境を作成して動作確認を実施してみてはいかがでしょうか。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。

*1 ちなみに、Travis CIではppc64leのワーカーを提供していますが、td-agentのsystemdのUnitファイルのデバッグが主目的の今回の作業には不向きでした。そのため、Travis CIでテストするというのは検討対象外でした。

*2 No Transactional Memory support in TCG, try cap-htm=offというエラーが出るため、--machineには'pseries-2.12'を指定しました。

タグ: Fluentd
2020-10-20

WiX ToolsetでQuiet Execution Custom Actionを使ってバッチ実行時のコマンドプロンプト表示を抑制するには

はじめに

クリアコードでは、Fluentdの開発に参加しています。Fluentdにはtd-agentと呼ばれるディストリビューションがあり、各種プラグインをまとめてパッケージやインストーラーが提供されています。

今回は、td-agentをWindowsでインストールする際、バッチ実行時にコマンドプロンプトの表示を抑制するのに使った方法について紹介します。

なおWiXのバージョンはこの記事を書いている時点の最新版である3.11.2を前提としています。

コマンドプロンプトの表示を抑制したい

td-agentでは、Windowsでインストールするプロセスの最後に、後始末やFluentdをサービスとして登録する目的でいくつかバッチファイルを実行しています。
その際、コマンドプロンプトが表示されてしまうので、ちょっとインストーラーの出来としてはいまいちでした。

Quiet Execution Custom Actionを使ってみる

WiXにはQuiet Execution Custom Actionというカスタムアクションを定義するためのものがあります。
それを使って、バッチ実行時のコマンドプロンプト表示を抑制するようにしたのが次の.wxsです。

<?xml version='1.0'?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">

...(途中省略)..

    <!-- td-agent-post-install.batに関するカスタムアクションを指定する -->
    <Property Id="PostInstall" Value=" "/>
    <CustomAction Id="SetPostInstallCommand"
                  Property="PostInstall"
                  Value="&quot;[PROJECTLOCATION]bin\td-agent-post-install.bat&quot;"/>
    <CustomAction Id="PostInstall"
                  BinaryKey="WixCA"
                  DllEntry="WixQuietExec64"
                  Execute="deferred"
                  Return="check"
                  Impersonate="no" />

    <!-- td-agent.batに関するカスタムアクションを指定する -->
    <Property Id="InstallFluentdWinSvc" Value=" "/>
    <CustomAction Id="SetInstallFluentdWinSvcCommand"
                  Property="InstallFluentdWinSvc"
                  Value="&quot;[PROJECTLOCATION]bin\td-agent.bat&quot; --reg-winsvc i --reg-winsvc-delay-start --reg-winsvc-auto-start --reg-winsvc-fluentdop
t &quot;-c [PROJECTLOCATION]etc\td-agent\td-agent.conf -o [PROJECTLOCATION]td-agent.log&quot;"/>
    <CustomAction Id="InstallFluentdWinSvc"
                  BinaryKey="WixCA"
                  DllEntry="WixQuietExec64"
                  Execute="deferred"
                  Return="check"
                  Impersonate="no" />

    <!-- 以下でインストール時の実行順を指定している -->
    <InstallExecuteSequence>
      <Custom Action="SetPostInstallCommand" After="InstallFiles">NOT Installed</Custom>
      <Custom Action="PostInstall" After="SetPostInstallCommand">NOT Installed</Custom>
      <Custom Action="SetInstallFluentdWinSvcCommand" After="InstallFiles">NOT Installed</Custom>
      <Custom Action="InstallFluentdWinSvc" After="SetInstallFluentdWinSvcCommand">NOT Installed</Custom>
    </InstallExecuteSequence>
 </Product>
</Wix>

上記サンプルは、3つの部分から構成されています。

  • td-agent-post-install.batに関するカスタムアクションを指定する
  • td-agent.batに関するカスタムアクションを指定する
  • インストール時のカスタムアクションの実行順序を指定する
td-agent-post-install.batに関するカスタムアクションを指定する
<!-- td-agent-post-install.batに関するカスタムアクションを指定する -->
<Property Id="PostInstall" Value=" "/>
<CustomAction Id="SetPostInstallCommand"
              Property="PostInstall"
              Value="&quot;[PROJECTLOCATION]bin\td-agent-post-install.bat&quot;"/>
<CustomAction Id="PostInstall"
              BinaryKey="WixCA"
              DllEntry="WixQuietExec64"
              Execute="deferred"
              Return="check"
              Impersonate="no" />

上記はプロパティ PostInstall の値を設定するカスタムアクション SetPostInstallCommandWinQuietExec64を使ってコマンドプロンプトを表示せずに実行するためのPostInstallカスタムアクションから構成されています。

プロパティ PostInstall の初期値が Value=" " となっているのは意図したものです。このようにしないとエラーになります。
また、実行するコマンドを指定するときには、 &quot; で囲ってあげないといけません。

WixQuietExec64 で実行するコマンドは、カスタムアクションのIdと同名のPropertyで指定するというのがポイントです。

なお、WixQuietExec64 を使えるようにするために、 light.exe 実行時に -ext WixUtilExtension オプションを明示的に指定して実行する必要があることに注意してください。

td-agent.batに関するカスタムアクションを指定する

td-agent-post-install.batに関するカスタムアクションを指定する部分とほぼ同様なので説明を省略します。

インストール時のカスタムアクションの実行順序を指定する
<!-- 以下でインストール時の実行順を指定している -->
<InstallExecuteSequence>
  <Custom Action="SetPostInstallCommand" After="InstallFiles">NOT Installed</Custom>
  <Custom Action="PostInstall" After="SetPostInstallCommand">NOT Installed</Custom>
  <Custom Action="SetInstallFluentdWinSvcCommand" After="InstallFiles">NOT Installed</Custom>
  <Custom Action="InstallFluentdWinSvc" After="SetInstallFluentdWinSvcCommand">NOT Installed</Custom>
</InstallExecuteSequence>

上記は、インストール時に実行されるカスタムアクションの実行順序を指定しています。上から順に実行されます。
それぞれのカスタムアクションの意味は次のとおりです。

  • インストール後に実行というのは After="InstallFiles"で指定し、カスタムアクションSetPostInstallCommandを実行する
  • カスタムアクションSetPostInstallCommandの後にPostInstallを実行するというのをAfter="SetPostInstallCommand"で指定する
  • インストール後に実行というのは After="InstallFiles"で指定し、カスタムアクションSetFluentdWinSvcCommandを実行する
  • カスタムアクションSetFluentdWinSvcCommandの後にInstallFluentdWinSvcを実行するというのをAfter="SetFluentdWinSvcCommand"で指定する

すでに説明した、プロパティの設定と、コマンドを実行するためのカスタムアクションのペアであることがわかります。

まとめ

今回は、WiX ToolsetでQuiet Execution Custom Actionを使ってバッチ実行時のコマンドプロンプト表示を抑制する方法を紹介しました。
td-agent 4.0.1のリリースには間にあわなかったのですが、次の4.x系のリリースに含まれるはずです。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。

2020-10-12

Groongaのダンプから不具合の再現に必要なカラムのデータのみを抽出するには

Groongaのサポートをしていると、Groongaのデータベースのダンプを提供いただいて、不具合を再現させることが多いです。
その際、特定のカラムに問題があることがわかっている場合、問題のカラムだけをダンプから抽出できたほうが効率が良いです。

例えば、データのロード中に問題が発生するなら、問題が発生するカラムのデータだけを抜き出して、再現しながらデバッグを進められれば、余計な処理が走らないのでデバッグしやすくなります。
ただ、データベースのダンプは巨大なことが多いです。巨大なファイルから該当のカラムのデータだけを見つけ出して抽出するのは困難です。

このような時に、groonga-command-filterというツールが使えます。
このツールはオプションに指定したカラムのデータだけをデータベースのダンプから抽出するツールです。

このツールの引数に問題のカラムを指定して実行すれば、簡単に問題のカラムのデータを抽出できます。
具体的には以下のように使います。

インストール方法

groonga-command-filterは以下のようにgroonga-command-parserをインストールすると使えるようになります。

$ gem install groonga-command-parser
使い方

例えば、以下のようなテーブルをダンプしたファイルがあるとします。(groonga-command-filterの動作をわかりやすくするために小さいデータを使います。)

table

ダンプファイルの中身は以下のようになっています。

table_create Blog TABLE_HASH_KEY ShortText
column_create Blog message COLUMN_SCALAR ShortText
column_create Blog title COLUMN_SCALAR ShortText

table_create IndexBlog TABLE_PAT_KEY ShortText --default_tokenizer TokenBigram --normalizer NormalizerAuto

load --table Blog
[
["_key","message","title"],
["grn1","Groonga message","Groonga test"],
["grn2","rakutan eggs 4 - 4 Groonga moritars","baseball result"],
["grn3","none","Groonga message"]
]

column_create IndexBlog index_message COLUMN_INDEX|WITH_POSITION Blog message
column_create IndexBlog index_title COLUMN_INDEX|WITH_POSITION Blog title

この中で、titleカラムのデータのみを抽出したい場合は、以下のようにgroonga-command-filterを実行します。

$ groonga-command-filter --include-column Blog.title ダンプファイル名

実行結果は以下のようになります。

table_create --flags "TABLE_HASH_KEY" --key_type "ShortText" --name "Blog"
column_create --flags "COLUMN_SCALAR" --name "title" --table "Blog" --type "ShortText"
load --table "Blog"
[
["_key","title"],
["grn1","Groonga test"],
["grn2","baseball result"],
["grn3","Groonga message"]]

下図の枠線内のデータ(titleカラムのデータ)が抽出できてきることが確認できます。
(このテーブルは、TABLE_HASH_KEYなので、_keyカラムの値は自動的に抽出されます。)

select-column

また、複数のカラムのデータを抽出するには、以下のように--include-columnオプションを複数回使用して実現します。

$ groonga-command-filter --include-column Blog.title --include-column Blog.message ダンプファイル名

実行結果は以下のようになります。

table_create --flags "TABLE_HASH_KEY" --key_type "ShortText" --name "Blog"
column_create --flags "COLUMN_SCALAR" --name "message" --table "Blog" --type "ShortText"
column_create --flags "COLUMN_SCALAR" --name "title" --table "Blog" --type "ShortText"
load --table "Blog"
[
["_key","message","title"],
["grn1","Groonga message","Groonga test"],
["grn2","rakutan eggs 4 - 4 Groonga moritars","baseball result"],
["grn3","none","Groonga message"]]

下図の枠線内のデータ(titleカラムとmessageカラムのデータ)が抽出できてきることが確認できます。
(このテーブルは、TABLE_HASH_KEYなので、_keyカラムの値は自動的に抽出されます。)

select-columns

上記のようにgroonga-command-filterを使うと特定のカラムの値を簡単に抽出することができます。
Groongaのデータベースのダンプから特定のカラムのデータを抽出したいケースがあったら、ぜひこのツールをお使いください。

タグ: Groonga
2020-09-29

Debian Developerになるには

はじめに

最近Debian Developerになった林です。
今回は、どうするとDebian Developerになれるのかについて紹介します。

Debian Maintainerになろう

まずはDebian Maintainerである必要があります。いきなりDebian Developerになることはできません。*1
Debian Developerには任意のパッケージをアップロードできる権限が与えられているからです。

Debian Maintainerとは、特定のパッケージについて、アップロードすることを許可されている人のことです。
もしまだDebian Maintainerでないなら段階を踏んでDebian Maintainerになりましょう。

以前 Debian Maintainerになるには という記事を書いたのでそれが参考になるかもしれません。

Debian Developerになるための申請をする

Debian Maintainerになったら次はDebian Developerになるための申請をしましょう。
申請は https://nm.debian.org/ から行ないます。

その際には、申請フォームから(gpgでクリアテキスト署名されている)次のような文面を送ります。

For nm.debian.org, at 2020-05-17:
I agree to uphold the Social Contract, the Debian Free Software Guidelines,
and the Debian Code of Conduct, in my Debian work.
I have read the Debian Machine Usage Policy and I accept them.

これ以降は、次のステップを順に踏んでいく必要があります。

  • Declaration of Intent
  • SC/DFSG/DMUP agreement
  • Advocate
  • Key consistency checks
  • Application Manager report
  • Front Desk or DAM approval

Declaration of Intent

なぜDebian Developerになりたいのかを表明するのがDeclaration of Intentというプロセスです。
こんなことをやりたいんだという熱い思いを記述します。

投稿した内容は、debian-newmaint@lists.debian.orgというメーリングリストへ送られます。

SC/DFSG/DMUP agreement

各種規定に同意することを表明するプロセスです。各種規定とは以下を指します。

  • Debian Social Contract (SC)
  • Debian Free Software Guidelines (DFSG)
  • Debian Code of Conduct (CoC)
  • Debian Machine Usage Policies (DMUP)

これも署名して送ります。

Advocate

既存のDebian Developerに推薦してもらうプロセスです。
あらかじめ推薦してもらえそうなあてがないとこのプロセスで詰んでしまうので、事前に根回ししておきましょう。

最近は東京Debian勉強会関西Debian勉強会が合同でオンラインの勉強会を開催しているので、ここに参加してみるとよいかもしれません。
次回はおそらく10月の中旬開催で、そのうちconnpassDebian JPにてイベントの案内がでるはずです。

Key consistency checks

申請者のGPG鍵に問題ないかチェックするプロセスです。
従来は既存のDebian Developer二人から署名(キーサイン)してもらっていないといけませんでしたが、昨今COVID-19の影響によりこの条件は緩和されました。

コミュニティでの活動で充分な成果をあげているとDebian Developerに認められ、推薦してもらえた場合もOKという条件になるようです。
もちろん従来のようにキーサインすることを推奨していますが、必須ではありません。

このあたりの詳細については、DAM Key and identity requirementsという投稿が参考になるでしょう。

Application Manager report

申請者がDebian Developerとしてふさわしいかをチェックするプロセスです。

試験官が割り当てられ、出題された問題に答えます。内容としては以下の2つの試験を受けます。

  • Debian Developer Philosophy and Procedures Test
  • Debian Developer Task and Skills Test

Debian Developer Philosophy and Procedures Testは申請者がDebianについて正しく理解しているかを確認する試験です。
こんなときはDebian Developerとしてどう振る舞うか、などが問われます。
試験官とはメールでやりとりを行い、充分な理解があると判断されたら次のステップに進みます。

Debian Developer Task and Skills Testは申請者のパッケージングに関する知識やツールを使いこなすスキルがあるかを問う試験です。
実際にバグを修正したりすることでその能力を示す必要があります。

上記の試験を終えると、試験官による合否のレポートが提出されます。

Front Desk or DAM approval

Application Manager reportの結果をもとに、Debianのアカウントを管理している人によるチェックを受けるプロセスです。
問題がなければDebian Developerとして認められ、アカウントの発行手続きが取られます。

こうしてDebian Developerになることができます。*2

まとめ

今回は、どうするとDebian Developerになれるのかについて紹介しました。
Debian Developerであるやまねさんによるなれる!Debian開発者という資料も参考になるでしょう。

あまり知られていないかもしれませんが、Debian DeveloperになるとMemberBenefitsにあるように各種優遇も受けられます。
LWNを購読できたり、Gandiで[ドメインをちょっとお安く維持(Level "E"扱いになる)できたりします。

Debianを好きな人は、ぜひDebian Developerになって活動してみませんか?

*1 よっぽど経験豊富でなんでこの人いまだに申請していないんだというケースだったら別かもしれません。

*2 Debian Developerになるのにどれくらいの期間かかるかは、人によって様々です。試験官や申請者が忙しかったりなどの要因があるからです。私の場合はちまちま進めていたので4ヶ月くらいでした。

2020-09-28

ノータブルコード12 データ構造を分かりやすくドキュメントする技

第12回目のノータブルコードでは、データ構造を分かりやすくするドキュメントのテクニックを紹介します。

C言語とシリアライズ処理

C言語でよくあるパターンとして、データをバイト列にシリアライズするという作業があります。多くの場合、この作業はデータフォーマットの仕様と密接に絡み合っています。フォーマット仕様書にある「最初の1バイトは8ビット識別用の固定値。次はフォーマットを表す3バイトの符号で埋める。その次が...」という指示に基づいて配列を埋めていくのが典型です。

シリアライズの例

この処理は、もちろん正確に実装する必要があるのですが、一方でヒューマンエラーの入り込みやすい部分でもあります。例えば、二つの隣り合うフィールドを入れ違いで埋めてしまった、データが実は1バイトずれていたというのは、実にありがちなミスです。最初は慎重に実装しても、フォーマットの拡張に対応するために、何度か手を加えるうちにいつのまにか壊れていた、というケースもままあります。できれば、こういったミスは未然に防げるようにしたいものです。

データ構造を明示するテクニック

私の好きなRFCに、GZIPのRFCがあります。その説明から少し引用すると、例えば以下のようなものです。

2.3. Member format

      Each member has the following structure:

         +---+---+---+---+---+---+---+---+---+---+
         |ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more-->)
         +---+---+---+---+---+---+---+---+---+---+
      ...

      2.3.1. Member header and trailer

         ID1 (IDentification 1)
         ID2 (IDentification 2)
            These have the fixed values ID1 = 31 (0x1f, \037), ID2 = 139
            (0x8b, \213), to identify the file as being in gzip format.

         CM (Compression Method)
            This identifies the compression method used in the file.  CM
            = 0-7 are reserved.  CM = 8 denotes the "deflate"
            compression method, which is the one customarily used by
            gzip and which is documented elsewhere.

私は、データ構造をドキュメントする方法としては、このような図*1による視覚的な説明が最も優れていると思います。形式言語や自然言語の文章による説明よりもずっと分かりやすいと感じます。この主たる理由は、おそらくプログラミングで言うところのデータフォーマットが、私たちの住む物理的な世界とは根本的に異質な概念だからだと思います。

実際、このRFCの作者であるL. ピーター・ドイチュは、プログラミングの本質的な難しさについて「(現実世界では)見回したところでアドレスやポインタみたいなものを目にすることはない」*2と説明しています。メモリアドレスやバイト列のような概念は、私たちの住む日常的な世界には存在しないので、これを視覚的なイメージで置き換えることが有効なのです。

実際のコード例

このような理由で、私はバイナリのデータフォーマットを扱う時は図でドキュメントするようにしています。例えば、Fluent BitのGELFフォーマットを扱うプラグインについては、次のようにドキュメントを付しました。

/*
 * A GELF header is 12 bytes in size. It has the following
 * structure:
 *
 * +---+---+---+---+---+---+---+---+---+---+---+---+
 * | MAGIC |           MESSAGE ID          |SEQ|NUM|
 * +---+---+---+---+---+---+---+---+---+---+---+---+
 *
 * NUM is the total number of packets to send. SEQ is the
 * unique sequence number for each packet (zero-indexed).
 */
#define GELF_MAGIC "\x1e\x0f"
#define GELF_HEADER_SIZE 12

static void init_chunk_header(uint8_t *buf, uint8_t count)
{
    uint64_t msgid = message_id();

    memcpy(buf, GELF_MAGIC, 2);
    memcpy(buf + 2, &msgid, 8);
    buf[10] = 0;
    buf[11] = count;
}

コミットメッセージにも書いていますが、実は、この改修前のバージョンのフォーマット実装には、いくつかの不具合がありました(コードレビューをすり抜けて、2年以上バグが残っていたという部分にこの種の実装の難しさを感じます)。このような誰にでも起こることが分かっているミスは、様々な工夫をこらしてぜひとも防ぎたいものです。

これと似たテクニックとして、Brubeckのコメントの付け方の記事があります。興味のある方はこちらも参照ください。

*1 この図の詳しい読み方はRFCの「2.1. Overall Convention」に説明があります。この記事の範囲では、一マスが一バイトを表していると理解すればOKです。

*2 ピーター・サイベル著、青木靖訳「Coders at work - プログラミングの技をめぐる探求」(2011年、オーム社) p415

2020-09-25

タグ:
年・日ごとに見る
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|