gitの良さは未だに分からないがPullRequestの良さはちょっとだけ分かるぞい

スマートニュース見てたらこんな記事が上がってました。

crapp.hatenablog.com

まあ、gitの良いところはそれなりにありますがローカルコミットを上手に使えるようになるまでの壁はとても高いですよね。

正直言えば私もrebaseとかなるべく打ちたくないし、使いこなせてるとは言い難い。

とはいえPR(本文中はマージリクエスト)がレビューを強制するので良くないとか、trunkに直接コミットすれば良くないとかはどうなん? と思ったり。

そもそも個人的にはSVNとgitで運用方式がさほど変わるとは思えないのですよ。実際変えなかったし。

というわけで、私が使ってるコード管理方式について語りたいと思います。タイトルはちょっと釣り... というかぞいぞい言いたかっただけです!

そもそもSVNでどう運用していたか?

たぶん、多くのサービスがそうであるようにSVNからGitに移行しました。もっと言うとさらに昔はCSVでした。

それも、色々な理由でSVNから短期間でgitの運用らしくないところもありますが、今は当時無理やりgit-flowとか入れなくて良かったなぁとは思ってます。

じゃあ、そもそもSVNでどんな運用してたのよ? という話になりますがおおむね下記のような運用でした。

  1. trunkは常に本番環境と一致。リリース時にはタグを打つ
  2. 開発時にはプロジェクト毎にtrunkから開発ブランチを作る。これが同時並行で大量にできる
  3. 検証環境向けのブランチに開発ブランチをマージしてテストする。検証期間/環境の都合でリリース日の異なる複数のプロジェクトがマージされる事がある
  4. 3の前後でtrunkと開発ブランチとの差分をとってReviewBoardとかでレビューする
  5. 3,4の完了したら開発ブランチをtrunkにマージ。タグ打ってリリース

f:id:pascal256:20161004032717p:plain

実のところ結果としてGitHub Flowに結構近い運用だと思っています。違うところは3の検証ブランチでしょうけど、検証ブランチ自体はどこにもマージされないので大差は無いんじゃないかと。 まあ、この手のベストプラクティスは似通ってくるものなので不思議ではないですね。

なんで移行したのか?

移行した理由は2つあります。一つはSVNがあまりにとろく、特にマージやDiffが地獄のような重さでした。普通に数分から十数分オーダー。

これはおそらく大量のブランチを運用してたり、用意されていたインフラと管理していたリポジトリ規模がアンマッチだったことがあります。

当時の私はレビューアーとライブラリアンも担当してたので、gitに移行前からsvn-gitとかを使ってローカルリポジトリを運用してました。パフォーマンスマジ大事。

もう1点はPRですね。会社で共通基盤としてStashを導入することは知ってたので、上記のパフォーマンスの理由もあって協力に推進しました。

Gitでどのように運用しているのか?

では、本題のGitでの運用方式です。正直、パフォーマンスがダメダメなだけで運用プロセスには大きな不満があった分けではないので、ほぼそのまま移行しました。

  1. masterは常に本番環境と一致。リリース時にはタグを打つ
  2. 開発時にはプロジェクト毎にmasterから開発ブランチを作る。これが同時並行で大量にできる
  3. 検証環境向けのブランチに開発ブランチをマージしてテストする。検証期間/環境の都合でリリース日の異なる複数のプロジェクトがマージされる事がある
  4. 3の前後でmasterと開発ブランチとのPRでレビューする
  5. 3,4の完了したらPRをマージしてmasterにマージ。タグ打ってリリース

f:id:pascal256:20161004032835p:plain

外部システムのReviewBoardがPRになり、マージ依頼と統合されました。なにこれ便利すぎる! と狂喜乱舞したのは私です。

それ以外は、まったくと言って良いほど同一プロセスです。gitの操作が独特だったり複雑ってのもTortoiseSVNからTortoiseGitに変わっただけなので、あまり大きな変化はありません。

まあ「Pushを忘れるな!」と徹底するくらいですかね。rebaseでコミットログを綺麗にしようとすると、ドキドキする運用がいっぱい待ってますが「そんなことしなくて良いんです」。

確かに残念なログは入り込みますしが、良くわかってないメンバーが使ってリポジトリがぶっ壊されるよりはマシです。

別に、コミットログが多少汚れてても、リリースのタグだけ適切に管理できてればそんなに困りません。clone/checkout/pull/merge/commit/pushくらい使えれば十分ですね。

なので、これ以外の操作をするときは権限を与えた特定メンバーに依頼しろという運用にしています。ちょっと窮屈だけど、窮屈だと思える程度にスキルがある人には権限与えれば良いですしね。

で、基本的にこれで運用してましたが、いきなり5の手順でmasterに取り込んでリリースをしてましたが、切り戻しが大変というリスクがありました。

特に、一日に何度もリリースする時に朝のリリースがこけると午後のリリースの準備がパーになるのでツラい。

なので、現在はリリースブランチを作って下記のように運用しています。

  1. masterは常に本番環境と一致。リリース時にはタグを打つ
  2. 開発時にはプロジェクト毎にmasterから開発ブランチを作る。これが同時並行で大量にできる
  3. 検証環境向けのブランチに開発ブランチをマージしてテストする。検証期間/環境の都合でリリース日の異なる複数のプロジェクトがマージされる事がある
  4. 3の前後でmasterと開発ブランチとのPRでレビューする
  5. 3,4の完了したらmasterからリリースブランチを作成して開発ブランチを取り込む。3のPRは破棄。
  6. リリースブランチをリリース。無事リリースされたらmasterにマージしてタグを打つ

f:id:pascal256:20161004032842p:plain

この手順にすることで、リリース失敗時の切り戻しが簡単になる上に、リリース前にリリースブランチをリグレッションテストできるので品質面でも貢献できました。

ちなみにこの改良版の手順も含めてSVNでできないのはPRくらいでしょう。これもReviewBoardとかの外部ツールで代替はできますが。

PRによるレビューはなぜ重要なのか?

元記事ではさんざん言われてるPRによるコードレビューだけど、これは超重要だと思っています。「あなたがgitに移行するべきたった一つの理由」とか言うそれっぽい記事のタイトルにできそなほどに。

重要な理由は2点あります。

1. 保守性の担保

元記事では「採用ミスの尻ぬぐいだ!」と言わんばかりのコードレビューですが、確かに読んでバグ見つけるよりは動かして見つけた方がずっと早いし合理的です。

が、コードレビューはバグ発見より可読性や設計レベルの保守性を主にチェックする行為だと考えています。コードは読み物なので第三者が読んで読みづらいのはダメです。

これは私やおそらく元記事の方を含むほとんどのエンジニアに必要な行為で、当然ですが自分より優秀な人が書いたコードのレビュー指摘なんていくらでもしたことがあります。

静的解析で基本的な部分はかなり潰れるんですが、設計レベルに由来するところは人が見ないと分からないですがねー。

一応、高いスキルを持ったエンジニアがお互いのコンテキストを良く理解しあった上で開発すればレビューとかいらない気がしますが、 そんな事例1ケースしか私は聞いたことが無いのと、それこそハイスキルな行為ですね。

2. 適切なマージがされるかの確認

これはPR最大の利点ですね。なんとgitはPR出た差分の通りにマージされるんです! ライブラリアン的な作業をしないと気づきづらいかもしれませんが、マージってのは「コンフリクトもせずに開発者の意図とは違うマージになる」可能性があり得るんです 。 gitはsvnよりずいぶんマシとはいえ、しょせんテキストベースのマージですからね。同じコードの一番上と一番したに同じメソッドを追加しても追加したら両方正しく取り込まれますけど、コンパイルエラーになりますよね?

これを防ぐにはSemanticMergeみたいなもう少しインテリジェンスなツールを使う必要があります。

通常は同じ行を修正してれば正しくコンフリクトしますが、コンフリクトの解決を適当にされると稀に「マージの時系列」や「同じ行としての判定」が狂う可能性があります。

この場合、何が怖いってSVNとかの場合は「Diffツールで見た結果と実際のマージ結果が異なる可能性がある」ってことです。せっかくレビューしたのにマージ結果が異なるとかツラい。

そのためリリース時のマージではマージした後に想定したマージ内容になってるかを差分確認という二度手間になっていました。

PRであれば、そもそもマージ申請なので見た目の差分通りに取り込まれるので、初回のレビューしっかりやるだけで良いので簡単ですし、安心感がありますね!

GitではなくSVNに回帰するべきなのか?

正直、管理をしない側がGitとSVNでたいそうな差があるとは思えません。

プロジェクト規模や体制にもよるでしょうが50%から80%くらいの人はrabaseがどうの以前にlogコマンドすら打たないんじゃないかな?

なので、管理者/ヘビーユーザに都合が良いGitから回帰するメリットは特に無いのだけど、全員がrebaseを使うような複雑な運用プロセスはやめたほうが良い気がしますね。

少数精鋭チームなら問題ないかもだけど、大半が初心者なら入れるメリットのがデメリットを上回らない気がします。

あと、これは私が「システムの整合性を保つ唯一の機械的な方法はリポジトリである」という単一リポジトリ教に入ってるせいもありますが、

SVNのようにディレクトリ単位で落とせてもいいんじゃないかなー、とは思ったり。あとshallow cloneにcommitできるとか。

正直、10GB級の巨大リポジトリを運用しようとするとcloneがHTTP Proxyに切断されるとか、サーバの方でハングするとかツラい事象が...

その辺を考慮したツールが欲しいなぁ。

余談

>コードレビュー導入は終局的には「コードはできるだけ書かない」という境地に至る。

本題とは本当に関係ないのだけど、元記事の人と意見が真逆で面白かったのでピックアップ。

「コードはできるだけ書かない」はプログラマーが目指すべき一つの境地だと思ってます。ここだけの秘密ですがコードを書くとバグが発生する可能性が上がるんですよ?

まとめ

元記事で言う「中規模以上のプロジェクトのリリースを本格的に管理する側」を経験した人間なので、ちょっとカウンター記事を書いてみました。

正直、10個も20個もあるそんなに短くないプロジェクトを並行運用するのをみんなどうやってるんだろう? という疑問をずっと持ってたので、 とりあえず自分の現状を公開してあわよくばコメントをもらおうという算段もあったります。

というか、そういうツラい運用したくないからリリーススケジュールをシリアライズして並行稼働開発をしないのが正しい組織運用かもしれないデス。

それでは、Happy Hacking!

技術的負債はリボルビング払いで返そう!

qiita.com

という記事がTwitterに上がってたので見てみましたが、大変共感できるものでした。

システム開発をしていれば技術的負債はつきものです。特にサービス運営をしていれば、「たとえ借金をしてでも今出すべき!」ってタイミングは良くあります。

技術的負債はあくまで価値を生む資産であって、有用なものなのです。返済しないといけないのと利子が付くだけで....

で、元記事の中で、「技術的負債を大きく返すパターン」「技術的負債を小さく返すパターン」が書かれていますが、これって実際の金銭的負債の考え方で言えばリボ払いですよね!

というわけで、今回は技術的負債を実際のクレジットカードの返済方式になぞらえて返し方を考えてみました。

主なクレジットカードの支払い方式

さて、なぞらえるからには前提の説明が必要でしょう。絵心無いですが簡単なイラストにしてみました。

f:id:pascal256:20160927022819p:plain

大別すると以下の三つです。

  • 一括払い : 利用日の翌月払いです。クレジットカードだと利子かかりません!
  • ボーナス払い : ボーナス月にまとめて払う方式です。特定月しか支払わなくて良いのがポイント。
  • リボ払い : 毎月決められた金額だけ支払う。ご利用は計画的に。

本当は他にも分割払いとかもありますが、正直技術的負債を分割払いで考えることは無いと思うので割愛。

技術的負債における支払い方式

さて、それではそれぞれの支払い方式で技術的負債を返済する場合を考えてみます。

一括払い

利用日の翌月とありますが、言ってしまえばプロジェクトリリース直後とかですかね。

リリース当日には無理してでも間に合わせる。出してから直せば良い。まあ、良く「考える」ケースですねー。

リリース直後に対応するのであれば、保守性の低下という手数料の支払いもゼロなので、悪く無い方式です。

一方で、それなりの支払い能力、すなわち余剰生産力が必要です。なにしろリファクタリングとか直接的には利益を生みづらい作業をするので、それをしても問題がない組織的な余裕が必要になってきます。

また、プロジェクト内で技術的負債が消化できないレベルのリリースの直後とか、どうせトラブるので人的リソースは枯渇し技術的負債は滞納されます。チーン

ボーナス払い

会社にもよりますが、繁忙期とそうでない時期があります。

そうでない月をボーナス月としましょう。この間に、たまった技術的負債を返すのです!

クレジットカードの支払い方式のイラストを見てわかる通り特定月以外は請求されないので、その分の生産力をトラブル対応や新たな機能の開発に充てることが出来るので、リソースを効果的に使いシステムやサービスの価値を高めることが出来ます。

でも、ちょっと待ってください! 本当にボーナスは支払われるのでしょうか?

年に2回しかないボーナス月です。それまでにたまった負債はとても一人が片手間に返せるものではありません。 私は社会人としてSEになって7年くらいになりますが、プロジェクトの切れ目なんて見たことありません。ましてやチーム単位で。

そう、多くの会社は(技術的負債返却という意味では)ボーナス月の存在しないブラック企業なので、技術的負債は滞納されます。チーン

リボ払い

さて、最後に悪名高きリボ払いです。

これは元金にかかわらず毎月決められた額だけ支払う方式です。これをシステム開発で言うなら一定のコストを常にリファクタリング向けに許容するという事になりますね。

人的リソースを消費しますが、そもそも支払い能力すなわち組織としてプロジェクト影響が許容できる範囲を決めて払いづづければいいのです。

カードでの支払い方式のイラストでも、固定の小さな額だけ払えば良いので負担はちいさそうですよね?

自らが決めてコントロールできるので、技術的負債を返済し続けることが出来ます。そう、つまり滞納しないんです! グレート

ただ、決めた量以上が減らないため、借金する速度の方が返済速度より早ければ元金は減るどころか増え続け、無限に支払いをし続けることになります。

でも、だから何だっていうんでしょうか? サービスが死ぬまで技術的負債を払い続ける覚悟をすれば良いだけじゃないですか! 死んだら払わなくて良いのです!

f:id:pascal256:20160927030701j:plain

まあ、あんまり元金増えすぎると利子(保守性の低下)が溜まり過ぎて、死ぬんですけどね!

でも、多少は減り続けるだけ、他の方式よりましだとおもいませんか?

まとめ

さて、あおりっぽくタイトルは「技術的負債はリボルビング払いで返そう!」としましたが、他よりマシってだけで、これだけやればOKという方式でもないかなと思います。

実際には本当の金銭の負債と同様に支払い能力に余裕があるうちはプロジェクト期間内または直後の一括払いを前提として、 手元にキャッシュ(人的リソース)を残してほかの生産的な作業に充てたいときはリボ払い方式を採用しつつ、 ボーナス月が来たらおまとめ払いで元金を可能な限り減らすというのが現実的でしょう。

個人的には技術的負債はずっと付き合っていかなければならないものだと思っているので、計画的に支払い続けれるリボ払い方式を採用できる体制にしていきたいなと思っています。

まあ、カードのリボ払いは手数料高いからしたくないけどね!

それでは、Happy Hacking!

分散バッチフレームワークとしてのApache Spark

ヌラーボさんのGeeks Who Drinkで分散バッチフレームワークとしてのApache Sparkというタイトルで話させていただきました。 nulab.connpass.com

資料はこちらになります。 Apache Spark as Batch

最近、Sparkを触り始めたんですが、世の中の資料は機械学習やログ解析がほとんどで、普段作ってるようなジョブをあんまりどう書くかが議論されていません。 ただ、SparkのAPIScalaのコレクションAPI。つまり、map/reduce/filter/foreach/groupby等による普通の近代的なコレクション操作APIです。 なので、普通の業務バッチで長時間掛かってるはずやつにも適用できるはず、という観点でこの資料を作ってみました。

個人的にはSparkおよびHadoopシステムはバッチシステムとしての完成度というか作りこみが凄く、管理系の機能が豊富なのでそこまでビックなデータじゃなくても基盤として有用だと考えています。

また、今回はあまり触れてませんがJavaからもふつうに使うことが出来るので、ScalaエンジニアがいなくともJavaエンジニアだけで開発できることも大きなポイントですね。

アクティブアクティブでスケールアウトするバッチサーバを検討中なら多少オーバーヘッドがあることを加味してもありだと思いますね。 DAGの可視化と各処理の実行時間がわかるのはかなり便利だし。

ただ、新しいミドルの運用という別タスクが増えるので、このトレードオフがペイするかは要件等ですね。

ちなみに、他の発表でWebクリエイターボックスの@chibimanaさんがリモートワークについて話されてました。 リモートワークとは少し違いますが、離れた拠点と仕事することは多いので、なるほどなー、と感じることも多くてとても興味深い話しでした。

あと、LTされたKAGURAっていうソフトがかなり面白そうでした。 私も音楽センス0なので、そんな私でもそれっぽい演奏ができるんだろうか? という夢と、MIDIがつながるのでジェスチャーで色んなデバイスを触るためのUIとしても 使えるんじゃないかなー、とおもったり。というわけでbackingはしてみました。ワクワク

Geeks Who Drinkは勉強会というわけではなく、食べ飲みだべりメインでなんかあれば発表もする、というスタイルみたいですね。 これはこれで面白いので、機会があればまた行ってみたいなー、と思います。

それではHappy Hacking!

参考:

書き初めコーディング! Docker + CGI + COBOLな環境を作って温故知新

さて、今年の書き初めコーディングは「温故知新」ということで、古いものと、とても古いものと、最近のものを組み合わせてみました。 というわけで、CGI + COBOL + Dockerという異色組み合わせをしてみました。エンジニアは「最新の技術」ではなく「最適な技術」を押さえる必要があるので、どっちも使ってみないとです。

とりわけ「CGI」は忘れ去られた便利仕様だと思っています。「CGI」と聞くと多くの人は「Perl」「PHPの前の技術」「RailsとかJavaServletのことでしょ?」と色々なイメージがあると思います。 Webアプリケーション黎明期を支えた技術であり、今となってはPHPRailsJavaEEに取って代わられた技術です。

しかし、「CGI=Perl」というわけではありません。CGIは「Common Gateway Interface」。その名の通り、Apacheと外部システムを連携させる「共通仕様」であり、 標準入力と標準出力を備えた言語...いえ、実行可能モジュールなら、なんでも動きます。Perlはもちろんですし、JavaRuby, あるいはC言語Bash、そしてCOBOLおばあちゃんだって大丈夫です。

単なる標準入出力のフックなので、JNIとかより圧倒的に手軽で、LinuxコマンドにWeb I/Fを付けるなど、多くの場面で役立ちます。

そして、今回のターゲットは古代言語の代表格「COBOL」! Linux実装にはOpenCOBOL(現GnuCOBOL)を使います。 さらに、これらをDockerに包んでどこでも使えるポータブルなCOBOLのREST環境を作ります。

まずはこちらに出来たものが。

github.com

環境としてはDocker Toolboxが入ってることを前提としています。Windowsで利用する場合はVolume周りを修正してください。

まずは単純にCOBOLのHello Woldを実行してみましょう。

000100* HELLO.COB GNU Cobol FAQ example
000200 IDENTIFICATION DIVISION.
000300 PROGRAM-ID. hello.
000310 DATA DIVISION.
000320 WORKING-STORAGE SECTION.
000400 PROCEDURE DIVISION.
000500     DISPLAY "Hello, World!".
000600     STOP RUN.

ことごとく最近はやりの言語と語彙が被ってませんね! とは言え読めばわかる通り「DISPLAY」が表示です。あとは全部オマジナイです(ぉ

実行はdockerでやります。

$ git clone https://github.com/koduki/example-cobol.git
$ cd example-cobol
$ docker-compose run app cobc -x -o bin/HELLO ./src/cobol/HELLO.COB 
$ docker-compose run app bin/HELLO 
Hello, World!

Dockerはデーモン(サービス)のコンテナとして使うのが通常だとは思いますが、こういうインタラクティブなコマンド実行にも使えるので、その用途も結構便利だったりします。

続いて、CGIとして実行します。

$ docker-compose run app cobc -x -o bin/APP ./src/cobol/APP.COB 
$ docker-compose up

APP.COBはCGI用に書いたCOBOLで下記のような形をしています。見よう見まねで書いてみたので、COBOLっぽくなかったらごめんなさい。

000100 IDENTIFICATION                     DIVISION.
000200 PROGRAM-ID.                        APP.
000300 DATA                               DIVISION.
000400 WORKING-STORAGE                    SECTION.
000500 01 ARG1                            PIC 9(2).
000600 01 ARG2                            PIC 9(2).
000700 01 RESULT                          PIC 9(4).
000800 PROCEDURE                          DIVISION.
000900 ARGS-INPUT                         SECTION.
001000      ACCEPT ARG1 FROM CONSOLE.
001100      ACCEPT ARG2 FROM CONSOLE.
001200 MAIN                               SECTION.
001300      COMPUTE RESULT = ARG1 + ARG2.
001400 JSON-OUTPUT                        SECTION.
001500      DISPLAY "{arg1:" ARG1 ",arg2:" ARG2 ",result:" RESULT "}" .
001600 JSON-FIN                           SECTION.
001700      STOP RUN.

ACCEPTが標準入力です。標準出力にはJSONを返しています。

ここで、CGIに詳しい方なら「あれ?」と思ったはずです。 そう、このCOBOLCGI仕様ではありません! (ぉ

ここで、少しCGIのI/Fを説明します。CGIはかなり単純で、出力が下記フォーマットなら、なんでもHTTPに乗せることが出来ます。

Content-type: text/html # text/planとかもOK
# 改行
# 本文

Content-typeを書いて、改行して、本文を入れるだけ。実にシンプル。なのですが、何故かCOBOLで書いたらうまく動きませんでした... 謎ですが、仕方が無いので、以下のようなBashで包んで、Conetent-type部分を補完しています。

#!/bin/bash

args=($(echo $QUERY_STRING|sed s/\&/" "/g|sed s/=/" "/g))

### cgi
echo "Content-type:application/json"
echo

echo -e "${args[1]}\n${args[3]}" |/app/bin/APP 

単純に、echoでContent-typeと改行を出力しているだけです。 GETパラメータなどはQUERY_STRINGという環境変数に渡されるので、そちらを分解して標準入力としてCOBOLに渡しています。

これで準備は整ったので、ブラウザで下記URLにアクセスしてみましょう。 ※ docker-machine ip が 192.168.99.100 の場合

http://192.168.99.100/app.cgi?arg1=3&arg2=2 

ちゃんと、足し算の結果が返ってきましたね? ちなみに性能としては特にチューニングしなくても1秒あたり100リクエスト程度の性能でした。 コネクション数を増やしても性能が上がらなかったので、Apache側でシリアライズされてる気がしますが、これはちゃんとチューニングすれば問題ないでしょうし、 Linuxコマンドや特殊な言語で書いたプログラムをAPI的に呼びだすだけなら、まあ十分な性能な気もします。

これで、どこでもCOBOLライフを楽しめるようになりましたね! あれ? 全然嬉しくないのはなんでだろう...

ちなみに今回は試しませんでしたがCGIは単純にApacheの1機能として振る舞いますので、HTTP2とかも動かすことができる気がします。 さすがに、Websocketは難しいでしょうけど。こういう既存のノウハウが活かせるところもCGIの良いところですね。

運用系コマンドのWeb I/FにもBashベースで作ればかなり向いてる気がします。

ただし、1点注意があって、社外からアクセスできる場所に置くときには十分に注意してください。Bashがその筆頭ですが、Webで鍛えられてないので脆弱性の塊です。ご利用は計画的に。

というわけで、古い技術の中にも便利なものはあり、新しいものと組み合わせることで更に生きる、ということを胸に今年も頑張りたいと思います!

それでは今年も、Let's Hacking!

さよならスティッキーセッション!PayaraでJavaEEでもセッションをKVSに。

この記事は Java EE Advent Calendar 2015 の 9 日目です。 昨日は btnrouge さんの「Payara Microのからくり」でした。

意図せずしてPayaraネタ連続です。

はじめに

Payara MicroはSpringBootやWildfly Swarmあるいは一日目に紹介されていたKumuluzEEと同様にJavaEE向けマイクロサービス環境です。 javaコマンドで単純に実行できるDockerフレンドリーな奴ですね。

先に上げたMWと一番の違いはPayara MicroはEJBを組み直してfat-jarを作るわけではなく、最小構成のコンテナでwarをデプロイして動かす、ということです。 具体的には下記のようなコマンドで実行します。

$  java -jar payara-micro-4.1.1.154.jar --deploy example-payara_micro.war

payara-micoro.jarがシンプルに起動だけする感じですね。単一jarでは動かないとい点はありますが、ギャップが小さい分、不具合が圧倒的に少ないですね。 Wildfly Swarmなんかは色々アグレッシブな分、まだ安定してない感じがしますが...

通常のwarと開発方法が同じなので、開発時はNetBeans + Payaraで作って実行時はPayara Microみたいな開発が楽な気がします。

さて、そんなPayara Microですがもう1点重要な点として、JavaEEの特徴の一つであるスティッキーセッションを要求されません! GlassFishを始め、JavaEEではインメモリセッションレプリケーションが基本になっており、原則同じサーバへのアクセスが要求されます。 しかし、PayaraではKVSであるHazelcastを組み込んであるため、どこのサーバへのアクセスも透過になるのでスケールアウト構成がシンプルに構築できます。

LL系だとKVSにセッションを格納するのは珍しく無いですが、JavaEEだとOracle Coherenceを担ぎ出す必要があったので大きな一歩ですね。JCacheはHTTP Sessionをカバーする仕様ではなさそうですし。

準備

下記からbootstrapになるpayara-microのjarファイルを落とします。現時点での最新版は下記からダウンロードしてください。

www.payara.fish

実践

それでは実際に試していきたいと思います。まずは、mavenプロジェクトで今回は作成したpom.xmlは下記の通り。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.orz.pascal</groupId>
    <artifactId>example-payara_micro</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>example-payara_micro</name>

    <properties>
        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.6</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>fish.payara.extras</groupId>
            <artifactId>payara-micro</artifactId>
            <version>4.1.152.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>example-payara_micro</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArguments>
                        <endorseddirs>${endorsed.dir}</endorseddirs>
                    </compilerArguments>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.6</version>
                <executions>
                    <execution>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${endorsed.dir}</outputDirectory>
                            <silent>true</silent>
                            <artifactItems>
                                <artifactItem>
                                    <groupId>javax</groupId>
                                    <artifactId>javaee-endorsed-api</artifactId>
                                    <version>7.0</version>
                                    <type>jar</type>
                                </artifactItem>
                            </artifactItems>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

基本、JavaEEベースのものですね? 続いてコードになります。せっかくなので、CDIのSessionScopeを使います。

@Data
@Named
@SessionScoped
public class User implements Serializable{
   String name;
   int age;
}

リクエストを受け取り、CDIをバインドするJAX-RSサービスは、StatelessBeansで作ります。

/**
 * REST Web Service
 *
 * @author koduki
 */
@Path("session")
@Stateless
public class SessionResource {

    @Inject
    private User user;

    /**
     * Creates a new instance of SessionResource
     */
    public SessionResource() {
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public String get() {
        String version = "2.0";
        String msg = "Application version " + version + System.lineSeparator();
        msg += String.format("I am %s. I am %d years old.", user.getName(), user.getAge());
        return msg;
    }

    @GET
    @Path("update/{name}")
    @Produces(MediaType.APPLICATION_JSON)
    public User update(@PathParam("name") String name, @QueryParam("age") int age, @Context HttpServletRequest request) {
        user.setName(name);
        user.setAge(age);

        return user;
    }
}

では、こちらを動かして見ましょう。

$ mvn package
$ java -jar  java -jar payara-micro-4.1.1.154.jar --deploy target/example-payara_micro.war
[2015-12-09T07:19:25.856+0900] [Payara 4.1] [INFO] [NCLS-CORE-00087] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1449613165856] [levelValue: 800] Grizzly Framework 2.3.23 started in: 33ms - bound to [/0.0.0.0:8080]
.
.
Members [1] {
    Member [192.168.99.1]:5901 this
}
]]
.
.
[2015-12-09T07:19:39.340+0900] [Payara 4.1] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1449613179340] [levelValue: 800] Deployed 1 wars

Deployed 1 warsになれば完了です。

途中、Membersと出ているところがHazelcastのクラスタです。 これは、マルチキャストグループで範囲が指定されていて自動的にひっかけてきます。 登録作業がいらないのでDokcerとかでダイナミックにインスタンスが増減しても、問題にならないのはいいですね。

では、検証コマンドを。ブラウザだと確認が面倒なのでcurlで実施しました。

$ curl -c cookie.txt -b cookie.txt -i http://localhost:8080/example-payara_micro/webresources/session
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Set-Cookie: JSESSIONID=3ccb47b039bb17f872d2216c9cde; Path=/example-payara_micro; HttpOnly
Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:0; Path=/example-payara_micro; HttpOnly
Content-Type: application/json
Date: Tue, 08 Dec 2015 22:53:29 GMT
Content-Length: 52

Application version 2.0
I am null. I am 0 years old.

最初は、セッショに何も値が入ってないのでnullが返ります。続いて値の登録して、結果を確認して見ます。

$ curl -c cookie.txt -b cookie.txt -i "http://localhost:8080/example-payara_micro/webresources/session/update/Nanoha?age=9"
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:1; Path=/example-payara_micro; HttpOnly
Content-Type: application/json
Date: Tue, 08 Dec 2015 22:55:24 GMT
Content-Length: 25

{"age":9,"name":"Nanoha"}%

$ curl -c cookie.txt -b cookie.txt -i http://localhost:8080/example-payara_micro/webresources/session
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:2; Path=/example-payara_micro; HttpOnly
Content-Type: application/json
Date: Tue, 08 Dec 2015 22:55:57 GMT
Content-Length: 54

Application version 2.0
I am Nanoha. I am 9 years old.

まずは、セッションが想定どおりに動いているのが確認できました。続いて、もう一つ立ち上げます。

$ java -jar ~/Downloads/payara-micro-4.1.1.154.jar --deploy /tmp/example-payara_micro.war --port 8180
Dec 09, 2015 7:57:46 AM org.glassfish.security.services.impl.authorization.AuthorizationServiceImpl initialize
.
.
Members [2] {
    Member [192.168.99.1]:5901
    Member [192.168.99.1]:5903 this
}
]]
.
.
[2015-12-09T07:58:06.839+0900] [Payara 4.1] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1449615486839] [levelValue: 800] Deployed 1 wars

今回は同一サーバに複数個立ち上げるのでポートを変更してあります。 また、Membersに新しく追加されたのが確認できるかと思います。

では、新規に立ち上げた方のサーバにアクセスしてみます。

$ curl -c cookie.txt -b cookie.txt -i http://localhost:8180/example-payara_micro/webresources/session
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:3; Path=/example-payara_micro; HttpOnly
Content-Type: application/json
Date: Tue, 08 Dec 2015 23:01:07 GMT
Content-Length: 54

Application version 2.0
I am Nanoha. I am 9 years old.%

nullではなく、適切にレプリケーションされた値が入ってることが確認できました! これでラウンドロビンでアクセスができますね!

Docker対応

やはり、この手のものはDocker経由で使いたいので、Dockerでの動きを検証してみました。

github.com

上記のDockerファイルを元に作成して、問題なく動作しました。

少なくとも同一ホストであれば、特になんの設定もしなくても Docker間でのマルチキャストによるディスカバリも含めてセッションレプリケーションも問題なく動作しました。

複数グループ

複数のセッション/クラスタグループを作るには、マルチキャストグループを変更することで可能です。

payara-microの場合は、IPが224.2.2.4がデフォルトのようなので

$ java -jar payara-micro-4.1.1.154.jar --deploy example-payara_micro.war -mcAddress 224.2.2.4

こんな感じで指定すればいいかと思いきや、現状では下記のようなエラーが出ます。

Dec 09, 2015 8:32:16 AM org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 5.1.2.Final
Exception in thread "main" fish.payara.micro.BootstrapException: PlainTextActionReporterFAILURENo configuration found for server.hazelcast-runtime-configuration
    at fish.payara.micro.PayaraMicro.bootStrap(PayaraMicro.java:714)
    at fish.payara.micro.PayaraMicro.main(PayaraMicro.java:105)
Caused by: org.glassfish.embeddable.GlassFishException: PlainTextActionReporterFAILURENo configuration found for server.hazelcast-runtime-configuration
    at com.sun.enterprise.glassfish.bootstrap.ConfiguratorImpl.configure(ConfiguratorImpl.java:75)
    at com.sun.enterprise.glassfish.bootstrap.GlassFishImpl.configure(GlassFishImpl.java:71)
    at com.sun.enterprise.glassfish.bootstrap.GlassFishImpl.<init>(GlassFishImpl.java:65)
    at com.sun.enterprise.glassfish.bootstrap.StaticGlassFishRuntime$1.<init>(StaticGlassFishRuntime.java:116)
    at com.sun.enterprise.glassfish.bootstrap.StaticGlassFishRuntime.newGlassFish(StaticGlassFishRuntime.java:116)
    at fish.payara.micro.PayaraMicro.bootStrap(PayaraMicro.java:694)
    ... 1 more

masterでは修正されたようなので、しばらく更新を待つしかないですね。 ちなみに、マイクロじゃない方のPayaraでは問題なく動作しましたので、すぐ使いたいならそちらで。

バージョニング

今回の構成ではラウンドロビンでバランシングができるので、シンプルなリリース要件ならブルーグリーンデプロイメントで問題ないと思います。 ただし、リリース中に一切ログインさせない、不整合も起こさせないといった完全なZero-downtime Deploymentを実施するならバージョン毎に別のセッションが必要です。

しかし、現行のGlassFish及びそれをベースとしたPayaraにはアプリバージョン毎のセッション等は作成できないので、 そちらに関しては別途の仕組みの検討が要りますし、いずれにしてもスティッキー性が必要になってきます。

まとめ

今回はPayara Microでのセッションレプリケーションを確認しました。実にDocker時代と相性の良さそうな機能ですね。 将来的には、mod_mrubyと組み合わせてZero-downtime Deploymentも作ってみたいと思います。

では、明日はsusumuisさんの「Javaプログラマー12年の僕がSpring童貞卒業的なこと書きます!」です!

Happy Hacking!

参考

WildFly Swarmではじめる「パーツとしてのJavaEE」

天神LT勉強会 on ZusaarWildFly Swarmに関して話してきましたので、ブログにまとめ直してみます。

JavaEEコンテナの世界観

昔ながらのJavaEEコンテナはすべてがJavaEEで完結することを目指して作られていました。 なので、WeblogicGlassFish, WildFly等には下記のような機能があります。

あと、リソースを効率的に使うために、一つのマシンに複数のアプリを乗せる事を前提とした機能も多いです。

「同一のミドルなのでシステムはうまく結合していて、管理も簡単!」

JavaEEコンテナさえあればインフラは他には不要!」

というやさしい世界を目指してる感じなのですが、実際は

f:id:pascal256:20150708221439j:plain

「そんなのないよ、ありえない」

という感じです。

従来のJavaEEコンテナの限界

どうありえないかですが、だいたい下記のような問題があります。

  • ブルーグリーンデプロイメントとかを考えるとクラスタリング機能が弱い(URLが同じアプリを複数持てない)
  • OSを含めてJavaEE以外のシステムの監視もあるので、アラートはZabbixとか別でやりたい
  • 無停止デプロイ系は困ったちゃん(信頼するには精度が微妙で、理解するにはブラックボックス過ぎる)
  • そもそも、JavaEEだけでは完結しない(Apacheが必要だったり、ローカルファイルが必要だったり)

すべてがJavaEEで完結するやさしい世界なら他のミドルも要らないし、幸せなのですが、現実は過酷です。色々なツールを組み合わせて使ってることが多いでしょう。

当然、そうなると組合せを最適化するための支援ツールが必要になってくるのですが、それがDockerであったり、Consulであったり、あるいはOpenShiftやCloudFoundryのようなPaaSというのが現代の流れです。

個々のシステムを上記に挙げたような基盤でくっつけていく感じですね。この流れにJavaEEコンテナも統合したい。

別に、現状でも統合できるのですが、単なる実行以外の機能もたくさん付いていので、無駄に重かったり、デプロイしづらかったりと、扱いづらかったりするのが現状の課題です。

WildFly Swarm

そこでWildFly Swarmです。

これは乱暴に言えば、SpringBootのJavaEE版です。WildFlyコンポーネントと組合せて、実行可能なFat-JARを作成します。

クラスタ機能とかそういったものは一切なくなるので、Docker等とかと組み合わせて、クラスタリングやスケジューリング、デプロイなんかを考えるのが前提です。

内部的には同じくJBossプロジェクトで出しているJavaEEのテストツールであるArquillianでも使われているShrinkwrapを使ってFat-JARを作成しているようです。

WildFly 9 betaをベースとしていて、Swarm自身もAlpha3ということもあって、JSFなんかはまだ正常動作しませんが、CDIJAX-RSJPAなんかは動きます。

パーツとしてのJavaEE

WildFly Swarmを使うことで、DockerやConsulなどの複合システムでアプリケーション実行という機能を提供する「パーツ」として使いやすくなります。

結果として、JavaEEコンテナで完結するよりは複雑な構成になりますが、JavaEEとそれ以外で2重に色々なものが存在する状態よりはずっと楽になります。

似たようなアプローチとしては、GlassFishベースのPayara MicroとWebSphareのLiberty Profileがあります。

逆に、対極のアプローチが次期Weblogicに搭載される予定のマルチテナントです。同一のWeblogic複数アプリをより互いの影響を小さくした状態にして相乗りさせ、効率的に集約をする機能です。

従来のJavaEE的な考え方を追及する方向なので、そもそもJavaEEのベースになっているWeblogicとしては自然な進化の気もします。OS仮想化やDockerによるコンテナ化よりもずっと効率的でしょうし。

使い方

前置きが長くなってしまいましたが、使い方です。JAX-RS, CDI, JPA(h2database利用)を使ったシンプルなプログラムをGitHubにおいてあります

WildFlyのインストールやその他準備は不要で、Mavenがあれば動きます。NetBeansとかMavenと連携できるIDE使えば開発も楽ちんです。

まずは、特に工夫をせず、シンプルにJAX-RS, CDI, JPAを使ったコードをそれぞれ書きます。

リソースはEmployeeServiceのfindAllお読んで結果を返す感じ。

@ApplicationScoped
@Path("/employees")
public class EmployeeResource {

    @Inject
    private EmployeeService employeeService;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Employee> findAll() {
        List<Employee> results = employeeService.findAll();
        System.err.println(results);
        return results;
    }

}

サービスは単純にJPAを呼ぶだけ。

@ApplicationScoped
public class EmployeeService {

    @Inject
    EntityManager em;

    public List<Employee> findAll() {
        return em.createQuery("SELECT e FROM Employee e", Employee.class).getResultList();
    }

}

EntityはLombok使いながらシンプルなJPA

@Entity
@Data
@AllArgsConstructor
public class Employee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    public Employee() {    }
}

ここまでは普通のJavaEEな書き方。

ここからが、Swarmの書き方。

まずはBootStrapになるクラスを作成。Arquillianと雰囲気にてますね。

public class Main {

    public static void main(String[] args) throws Exception {

        Container container = new Container();
        registDataSource(container);

        JAXRSDeployment deployment = new JAXRSDeployment(container);

        // set config
        deployment.getArchive().addClass(JpaResources.class);
        deployment.addResource(JaxRsApplication.class);

        // load resouces
        deployment.getArchive().addAsWebInfResource(new ClassLoaderAsset("META-INF/beans.xml", Main.class.getClassLoader()), "beans.xml");
        deployment.getArchive().addAsWebInfResource(new ClassLoaderAsset("META-INF/persistence.xml", Main.class.getClassLoader()), "classes/META-INF/persistence.xml");
        deployment.getArchive().addAsWebInfResource(new ClassLoaderAsset("META-INF/load.sql", Main.class.getClassLoader()), "classes/META-INF/load.sql");

        // load classes
        deployment.addResource(EmployeeResource.class);
        deployment.getArchive().addPackage("cn.orz.pascal.example.wilfly.swarm.domain");

        container.start().deploy(deployment);
    }

データベースの登録はこんな感じ。

    private static void registDataSource(Container container) {
        container.subsystem(new DatasourcesFraction()
                .driver(new Driver("h2")
                        .datasourceClassName("org.h2.Driver")
                        .xaDatasourceClassName("org.h2.jdbcx.JdbcDataSource")
                        .module("com.h2database.h2"))
                .datasource(new Datasource("InMemoryPersistenceUnit")
                        .driver("h2")
                        .connectionURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE")
                        .authentication("sa", "sa"))
        );

        // Prevent JPA Fraction from installing it's default datasource fraction
        container.fraction(new JPAFraction()
                .inhibitDefaultDatasource()
                .defaultDatasourceName("InMemoryPersistenceUnit")
        );
    }

つづいて、pom.xmlwildfly-swarm-pluginでMainクラスを指定するのがポイント。

  <groupId>cn.orz.pascal</groupId>
    <artifactId>example-wilfly-swarm</artifactId>
    <version>0.1</version>
    <packaging>jar</packaging>

    <name>example-wildfly-swarm</name>

    <properties>
        <version.wildfly-swarm>1.0.0.Alpha3</version.wildfly-swarm>

        <maven.min.version>3.2.1</maven.min.version>

        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>wildfly-swarm-plugin</artifactId>
                <version>${version.wildfly-swarm}</version>
                <configuration>
                    <mainClass>cn.orz.pascal.example.wilfly.swarm.Main</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
  

ちなみに、NetBeans 8の組み込みMavenのバージョンは3.0.5なので実行すると下記のようなエラーがでます。 3.2以上のMavenを入れて指定しましょう。というか今時なんでこんなバージョンを...

WARNING: Error injecting: org.wildfly.swarm.plugin.PackageMojo
java.lang.NoClassDefFoundError: org/eclipse/aether/resolution/ArtifactResolutionException
    at java.lang.Class.getDeclaredConstructors0(Native Method)
    at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671)

アプリケーションの実行は

$ mvn package

をすると${app-name}-swarm.jarができるので

$ java -jar ${app-name}=swarm.jar

で実行するだけです。IDEからなら単純にMainクラスを実行するだけですね。実行したら

http://localhost:8080/employees

にアクセス。中々いい感じに動きます。

まとめ

SpringBoot開発も運用も軽量で良いなー、とJavaEE派としては羨ましがってたんですが、これはなかなか良さそうです。

Dockerとの組み合わせも簡単になりますしね。とはいえ、アルファ版なので完成度とかは今後に期待。Payara Microの方が現時点の完成度は高そうだし、KVSとの連携もできるみたいだから、そっちも確認して見ようかな?

しかし、JavaEE8ではマルチテナントなんかより、こっちの機能を標準化して欲しい...

参考

Dockerってなんですか? それ、JavaEEで分かるよ。

f:id:pascal256:20150404165419p:plain

はじめに

最近、Dockerが大人気ですね。GoogleMicrosoftも参入してることはもちろんですが、Oracle Weblogicのサポート対象に入ってたりミドルウェア側、それもエンタープライズ系製品が対応してきたのは面白い動きです。

とはいえDockerって何?っていう人もまだまだ結構多いと思います。

Dockerとはvmwareなどの技術の先にある軽量な仮想マシンである、と捉えてもあまり間違ってないと思いますが、個人的にはJavaEEコンテナの類似品というか、それと同じ文脈で考えたほうがしっくりきました。

なので、自分の理解を深めるためにも、JavaEEエンジニアはどのようにDocker及びその周辺技術を理解すればいいのか、という視点でまとめてみました。

そもそもDockerって?

Dockerとvmwareのような従来の仮想化で何が違うかですが、vmwareがOSの仮想化をしているのに対し、 DockerはOSの上でアプリケーションとミドルウェアを仮想化するコンテナ技術です。 まあ、chrootやjailの偉いやつです。Solarisでいうところのゾーンですね。

OSレイヤーを仮想化しないので、オーバーヘッドが非常に少なく高速に動作します。 それでいて、アプリケーションの動作環境をパッケージングしてるので、 ライブラリのバージョンや依存関係を含めて競合しない形で配布できるので 開発環境と本番環境が違う、みたいな問題も従来の仮想環境同様に解決できます。

むしろ、従来の仮想環境ではそうは言っても複数台立ち上げるのはコストが高いので、 似たようなロールは同じサーバに置く運用でしたが、Dockerの場合は軽量なのでロール毎に厳密に分けるのに向いていますね。その辺が人気の秘密です。

Dockerとwarとアプリケーションのパッケージング

さて、アプリケーションを依存ライブラリも含めてパッケージングと言われると、どこかで聞いたことがある気がしますね?

そう、「war(Web Application Archive)」です。 JavaEEの場合、依存するライブラリ(jar)を含めてパッケージングし、異なるwar間での依存を切り離します。 なので、同じサーバにデプロイしていても、あるアプリケーションはバージョン1系を使い、 別のアプリケーションはバージョン2系を使うというようなケースでも、問題が起こることはありません。

JavaEEJavaとして閉じることで、これを実現していましたが、DockerはLinuxアーキテクチャで動作するものであればパッケージングが可能です。 なので、例えば「Apache + ImageMagic + Rails + アプリコード」をまとめてパッケージングする、という形になります。 デプロイもDockerコンテナに配置するだけなので、ImageMagicが無いとか、Apacheの設定が違うとかに悩まされることもなく、すぐに動作します。 この辺も、GlassFishにwarを配置するときとイメージは同じです。

JavaEEのwarと一番の違いはミドルウェアもパッケージングしちゃうところですね。 なので、JavaEEアーキテクチャで作成した、アプリをDocker化する場合はTomcatやJetty, あるいはGlassFishWeblogicごとパッケージングする形になります

DockerfileとMavenと構成管理

warを作るためには通常はMavenかGradleを使いますね? この辺を使えば、手順をコード化出来るのはもちろんのこと、依存関係も自動解決してくれて便利ですよね。

Dockerの場合はDockerfileが概ねこれに当たります。 また、依存関係の解決事態はDockerの中で使うyumだとかaptだとかのパッケージマネージャに任せるのが基本となります。

より高度なビルドをしたいときにはPackerやChef/Ansibleなんかを組み合わせるケースもあります。

MavenMavenリポジトリがあり各ライブラリを自動で落とせるように、 DockerにもDockerHubがあり必要なイメージを自動でダウンロードして使うことが出来ます。

オーケストレーションクラスタリング

JavaEEの仕様では無いですが、GlassFishWildFlyWeblogicといった一般的なJavaEEコンテナはクラスタリング機能を持っています。

これは複数のサーバを束ね、グループとして扱うことで、設定の同期やデプロイの一元化あるいは負荷分散や高可用性を提供します。

Dockerは基本的にはパッケージングに特化した技術なので、同等の機能はありませんが、SerfやConsulのようなオーケストレーションツールを使うことで実現できます。

オーケストレーションツールが何を提供するかといえば、基本的にはグルーピング(クラスタリング)とサービスディスカバリ、トリガの管理です。

詳細は後述しますが、簡単に言えばGlassFishWeblogicクラスタ機能/管理サーバを「自作するための機能」です。

残念ながら現時点でGlasFishのクラスタ機能と同じものを完成品として提供するツールは無いと思います。(使ったことがないけどGoogleのk8sはそれに当たるかも?) まあ、もうちょっと標準化されたものは今後出てくるでしょうが、根本的にインフラ構成に依存するので、 Consulをベースとした「Java + Dockerのソリューション」とか「Rails + Dockerのソリューション」みたいな感じで出てくるんじゃないかと思います。

これだけ書くと微妙ですが、クラウド、特にDockerを使うケースだとインスタンスの追加と削除を非常に頻繁に行うことになるので、 既存のGlassFishなどのクラスタ機能では不向きな部分がありますが、この点によくマッチします。

個人的にはGlassFishWeblogicクラスタ機能が微妙なので、この辺りは気になるところです。

では、クラスタリング、サービスディスカバリ、トリガ管理に関して、Consulを例に記載します。

クラスタリング

Consulでのクラスタリングは単なる名前付けです。主に後述するトリガ管理で有用な機能ですが、 Consulの場合は各クラスタに関してDNS登録をするので、単純な負荷分散はこの時点で対応できます。

サービスディスカバリ

サービスディスカバリはサービス、今回の話で言えばDockerコンテナを見つけるてクラスタに登録する機能です。

と言っても、難しいことをしているわけではなく、各DockerコンテナにConsul Agentをインストールし、 Consul Serverに起動時にリクエストを投げることで発見をします。

発見だけではなく、ヘルスチェックも提供していて、落ちれば自動的にクラスタから外されます。

トリガ管理

トリガ管理はイベントを起点に何らかの処理をクラスタに対して実行する機能です。

イベントはサービスディスカバリによりクラスタに追加されたり、削除されることはもちろん、外部から任意に実行することも出来ます。

トリガ機能を使うことで、クラスタに対するデプロイやZabbixやSensuといった監視システムに対して自動で登録削除をすることが出来ます。

上記の機能を組み合わせてGlassFishクラスタ機能や管理ツールと同等の機能を作成していくことになります。JavaEEコンテナの提供するクラスタ機能だと、当然JavaEEで完結するものにしか提供されません。

結果として、同じシステムに対してGlassFishクラスタとZabbixのクラスタが二重管理されるような形になります。Consul等を使うことでこれを集約して管理できるのは大きな利点です。

リソース管理

各コンテナが使うリソースが適切に配分されるように多くのGlassFishWeblogicでは同時にさばけるリクエスト数はDBコネクション数などのリソース制御を行います。

これと同様の仕組みはDocker単独では提供していませんが、Mesosやk8s(Kubernetes)を組み合わせて使います。このあたりは正直まだ成熟してないので、今後に期待。

フェイルオーバー

Dockerにはフェイルオーバーに類する機能はありません。こちらはDockerコンテナ内の各ミドルウェアの機能に頼ることになります。

例えばWebシステムであればセッションさえフェイルオーバーできれば基本は問題ないので、 KVSで外部に出して、各APPサーバ、各KVSサーバ自体は状態を持たないかレプリを持つのが基本となります。

JavaEEであればGlassFish, WeblogicであればCohrerence、WildflyであればInfinispanを使うことになります。 もちろんクラスタを作ってインメモリレプリケーションをすることも出来ます。

個人的にはインメモリレプリケーションJavaEEコンテナ側でするよりも、外部に取りたいのでSpring-Sessin + RedisがJSF等でも使えないか検討中です。

Immutable InfrastructureとJavaEE

Dockerそのものとは関係ありませんが、Dockerを取り巻く概念の一つにImmutable Infrastructureがあります。

これは、その名の通り、サーバなどのインフラに対して一切設定変更を行わず、修正やアプリケーションのデプロイをしたい場合はサーバごと作りなおす、という手法です。

これにより、Chefなどのように冪等性を気にせずスクリプトが作れますし、ローカルにファイルを出力するなどの状態が無いのでサーバの増減が非情に簡単になります。

もちろん、手動オペレーション + 物理サーバでこんなことが出来るはずがなく、自動構成管理と仮想化が基本になるのですが、Dockerはこの運用に非常にマッチします。

一見新しい概念なのですが、実はJavaEEは元々これを強く意識したアーキテクチャーです。

最近は出来ますが、JavaEEではトランザクションとポータビリティを意識してEJBへのローカルファイルへのアクセスは禁止でした。今は利便性のため可能ですが、やはりDBなどを使うのが基本となります。

EJBがJNDI Lookupで引いた場合、クラスタ内のどのサーバで動くか分からないため、そのような作ります。warまたはearをデプロイすればどこのサーバでも全く同様に動くのがJavaEEの基本思想です。 これって、Immutable Infrastructureと左程変わらないですよね? 当時と違いDockerはJava以外のものもパッケージングできるため適用対象が増えただけで、概念としては同様です。

なので、JavaEEエンジニアであればImmutable Infrastructureは考え方としてしっくりくるので、さほど警戒する必要は無いと思います。

まとめ

今回はJavaEEエンジニアはどのようにDocker及びその周辺技術を理解すればいいのか、という視点でまとめてみました。

Dockerやオーケストレーション、Immutable Infrastructureは非情にホットな技術で新しい用語も多いですが、 基本的には、JavaEEでも考慮されていたユースケースをより汎用的に、現代的に解決するための手段なので、 今回みたいに対応づけて考えると分かりやすいんじゃないかなー、と思います。

逆に言えば被ってる部分も多いので、機能の使い分けが重要になってくると思います。特にクラスタ周り。 Docker時代のJavaEEインフラをどう構成するのが良いのかは、なかなか面白そうなので継続して考えていきたいです。

この辺を考えると、未来のJavaEEコンテナはクラスタとかデプロイとかの運用要件を外部システムに任せて軽量なアプリケーションコンテナとして生きる道もあると思います。

また、現時点では要素技術が良くも悪くも独立していますが、Google Compute EngineのようなPublic PaaSやOpenShift 3.0のようなPrivate PaaSがPaaSという形で統合する統合する形になるかと思います。

それではHappy Hacking!