shiba-hiro’s 備忘録

技術ブログの皮を被ったただの備忘録

開発機としての Google Pixelbook

先日、プライベートで使うPCとしてPixelbookを新しく購入しました。
Pixelbookは、2017年に発売開始された、Google謹製のハイエンドノートPCです。
ソフトウェア開発でも利用するという視点から、いくつかのTipsや所感、買う前に自分が知っておきたかったポイント等をまとめようと思います。

購入したのは、i7, 16 GB RAM, 512GBストレージのモデルです。
また、この記事は2018年7月現在のものですので、参照される際は情報の鮮度にご注意ください。

www.google.com

Pixelbookを利用したかった動機

そもそもなぜPixelbookを欲しくなったのかという経緯からです。
一時期は公私ともにWindows、ここしばらくは仕事ではUbuntu/プライベートではMacという布陣でした。
独断と偏見の上にある自身の所感では、それぞれのOSに対しては下記のような感想を持っています。

Windows: プラットフォームが成熟しており一般用途としては文句なし。が、開発に使うにはストレスが多すぎる。
Bash on Ubuntu on Windowsも、(当時の)使い心地はイマイチだった。

Mac: 「一台で済ませる」という観点では最良の選択肢。
iTunes, LINEなどのアプリもネイティブで利用できる上に、コミュニティや開発基盤も充実しており、日本語情報も豊富。
Linuxアプリが使えなかったり何かにつけてXcodeのインストールが求められたりするのが珠に傷。

Linux(Ubuntu): オープンソースのアプリケーションが充実しており、開発用途としては最も使いやすい。
成果物の実行環境にLinuxを使っている場合、そのプラットフォーム上で作業ができるメリットは大きい。

なので、まあMacでも悪くはないんですが、それでも若干のストレスを感じていたのがひとつ。
またもうひとつとしてはスペック不足も感じていた(Macbook Air, 4GB RAMだった)ので、開発にも使えるプライベートPCの新調を図っていました。

いくつかの候補を探した上で、ASUSHUAWEIの上級モデルにLinuxデュアルブートで入れる、くらいの結論に落ち着きそうだったところ、この記事を読んだのが5月の話。

jp.techcrunch.com

なんとChromeOSでLinux(それもDebian!)のサポートが公式に始まるとのこと。
かつそのサポートがPixelbookから展開され始めるとのこと。
GUI含めサポートされるようなので、これは新しい選択肢としてかなりアツいのではと、しばらく悩んだのち購入に至りました。

この記事では、そのLinuxサポートの上でどれだけのことができるのか?みたいなところを中心に書いています。

セットアップ

Linux自体の立ち上げについては、日本語でもググればそれなりに記事が出てくるので割愛します。
OSのビルドを開発モードにして再起動してやれば、設定からワンクリックで立ち上げられます。

Terminal

まずはターミナルです。
若干心配していましたが、ctrl + r, ctrl + wなど、Chromeブラウザのショートカットキーにも使われるものも、期待通り動作しました。
(実際、croshだとctrl + wするとタブを閉じてしまいます)

また先述の通りGUIもサポートされているので、気に入ったアプリがあればそれを使って代替できます。
自分は仕事でも使っているTerminatorを利用しています。
デフォルトのTerminalだとタブを追加できずウィンドウを増やすしかないのですが、Terminatorであればタブの追加、分割も行えます。
インストールは、デフォルトのTerminal上で

$ sudo apt install -y terminator

を叩くだけでOKです。

こんな感じで、デフォルトのTerminalとは別にTerminatorが追加されます。 f:id:shiba-hiro:20180730002633p:plain

タブ、分割なども問題なく動作します。
f:id:shiba-hiro:20180730002721p:plain

一点、Terminatorのショートカットキーで気にしなければならないのは、タブの移動です。
ctrl + search + up/downとしてやる必要があります。

Terminatorを閉じたあとに再起動しようとして動作しないことがありますが、次のステップを踏むと直りました。
・一度shelfからTerminatorを確実にclose
・デフォルトのTerminalを起動
・再度Terminatorを起動

Editor

Visual Studio codeを利用していますが、こちらも問題ありません。
debファイルを取得してインストールすれば、すぐに使えるようになります。

$ curl -L https://go.microsoft.com/fwlink/?LinkID=760868 > ~/Temporary/code.deb
$ sudo apt install -y ~/Temporary/code.deb

f:id:shiba-hiro:20180730003752p:plain

ファイルシステム

LinuxコンテナがChromeOS上で立ち上がって動作しているようなイメージなんですが、ChromeOSのファイルシステムからLinux上のファイルへアクセス可能です。
ホストやUSBメモリからファイルをコピーしたり、GUIでポチポチしたりも簡単です。

f:id:shiba-hiro:20180730005851p:plain

画面のテスト

あと気になるのは画面のテストができるかどうかです。
コンテナで立ち上がるとなると、ちゃんとアクセスできるかはチェックポイントです。

結論からいえば、ipアドレスを利用するか、linuxhostという名前を利用するかの、いずれかの方法でアクセスできます。
先述のような仕組みの都合上、ChromeOS側からlocalhostでアクセスすることはできません。

f:id:shiba-hiro:20180730010823p:plain

linuxhostを使う方が簡単ですが、もしipアドレスを利用したい際には、hostname -Iコマンドで調べましょう。
もちろん、80以外のポートも利用可能です。

20180801追記
OSのアップデートをしたところ、linuxhostが使えなくなり、代わりにpenguin.linux.testでアクセスできるようになりました。
testとついているくらいなので、おそらくこれからも変わることでしょう。。。

アプリの起動

Terminatorを使うくだりでスクショを貼りましたが、Linux用のアプリもChromeOSから検索、起動できます。
環境の差を意識しすぎずに、ストレスなく作業を行うことができます。

Misc

Q. どこで買えるの?日本版のGoogle Storeでは見つからない!

A. Amazonで買いましょう。

ちなみに自分の場合は、受け取るまでに3週間を要しました。

Q. PC本体以外にもなんか買った?

A. ケースとUSB type-cの変換アダプタは買いました。

ケースはこちらのBellroyのもの。
かなりタイトなデザインです。

https://www.amazon.co.jp/gp/product/B077YCJW7N/

アダプタは、おそらくMacのおかげでいろいろ種類が出ています。
いまのところこちらを利用しています。

https://www.amazon.co.jp/gp/product/B0797WT44V/

Q. 日本語対応は大丈夫?

A. モード、入力とも問題なし。

設定画面でポチポチやれば、OSの日本語モードへの切り替え、日本語入力の追加も簡単にできます。
漢字の見え方がたまにおかしいですが、基本は問題ありません。
このブログ記事もPixelbookから書いています。

Q. どうして日本語対応してるのにキャプチャでは英語モードなの?いちびってるの?

A. アプリ検索の利便性のためです。いちびってはない。

どういうことかというと、例えばファイルシステムを使いたいときに、
日本語だと、「ファ」とカタカナで入れてあげないとだめなんですが、
英語だと、"f"と一文字入れるだけで済むわけです。

諸々のアプリやコマンドのエラーメッセージも、英語のほうが情報量が多いので、不便を感じない限りは英語モードを使い続けようかなと思います。

なお、Google assistantも、まだ英語モードでしか使えません。。。

Q. なんかTerminal起動しても使えなくなる事象が突然起きたんですけど

A. いったん全てのLinuxアプリを破棄して再度Turn onしてください

真っ黒の画面だけ出てコマンドにも反応しなくなる事象が、実際に起きました。
ネットで上がってる方法も試したものの効果がなく、最終的にはLinuxのアプリを一旦すべて破棄し、再度コンテナを立ち上げ直すことで修復しました。

Q. Linux containerの作り直しってできる?

A. できました。

先述のとおりですが、すべて破棄したあとも作成し直し、動作させることができました。
なので、ガンガン実験できそうです。

まだ不安定版で動かなくなることもあるかと思うので、リカバリスクリプトを用意しておくと便利かもです。

所感

買う際に期待していたことですが、「一台で全部できる」感を味わえます。
Linux用アプリ(VScodeなど)も、Play storeで入手したアプリ(kindleなど)も、ChromeOS上からすべて同じようにアクセスできるのが心地よいです。
まだDev版でしかLinuxは使えないので不安定ではあるものの、コンセプトは素晴らしく、とても気に入りました。

ハードのデザインも格好良く、ディスプレイにタッチできるのも何かと便利です。
結論、よい買い物したなぁと思っており、愛用していきたいと思います。

Exception Mapping with Kemal

KemalでException Mapping機能を実現するための方法です。
参照実装をgithubへアップしています。

github.com

Kemal自体については前回記事をご覧ください。

Exception Handlerの必要性

プログラムの実装には、例外の適切な生成およびハンドリングが欠かせません。
通信先のサーバーが落ちている、与えられた値が不正なものだった、実装が不十分でランタイムエラーが発生した、などなど。
正常処理を継続できない場合には、きちんとログを残し、状況に応じたレスポンスを返してあげる必要があります。

もちろん、ビジネス上予期できる分岐に関しては、Exceptionではなくifを用いて記述する方法もあります。
そうはいっても、「例外的な内容を記述するif」よりもカスタムしたExceptionの方が素直に状況を表せるケースも多い(Validationなど)ですし、Exceptionの伝達性には実装上の便利な面もあります。

ことWebアプリにおいても例外のハンドリングは重要で、適切なロギングやレスポンスは、障害の察知やユーザビリティにメリットをもたらします。
特定の例外に対して、レベルを上げたログを必ず出力するようにすれば、障害の検知精度を上げられます。
Web APIの処理中にエラーが起きた際、その種類によってHTTPステータスコードや返却メッセージを適切に選んで返せれば、呼び出し側も状況に応じた行動が可能です。

一方、例外処理は、プログラム読みづらくすることもあります。
たとえば次のようなコードがあったとします。

.
.
.
  begin
    changeset = Repo.insert(note)
  rescue e : FirstSampleException
    halt env, status_code: 400, response: Hash{"message" => "Message for FirstSample."}.to_json
  rescue e : SecondSampleException
    halt env, status_code: 422, response: Hash{"message" => "Message for SecondSample"}.to_json
  rescue e : Exception
    LOGGER.error(e.inspect_with_backtrace)
    halt env, status_code: 500, response: Hash{"message" => "Internal server error."}.to_json
  end
.
.
.

プログラムの意味的に重要なのは、noteという値をデータベースへ保存することです。

しかしながら、DBサーバーで障害が発生している等、処理が失敗する可能性もあるでしょう。
そのため例外処理を記述するのですが、 この例外処理は「値をデータベースへ保存する」という意味を表すのには必要ないので、プログラムの見通しを悪くしてしまいます。

尤も、例外処理についての上記のデメリットは、メソッド化する等の方法で対処することもできます。
ただし、この例ではもう一点、問題になりえる点があります。
それは、プログラムの階層を壊してしまう可能性があることです。
プログラムの規模が大きくなると、階層的に、web apiのレスポンスを返す層と、データベースへ値を保存するための層は分けて記述することが多いはずです。
この例のような書き方を放置すると、ゆくゆくはプログラムの階層がカオスになってしまいます。

そこで、特定の例外に対する処理を請け負うレイヤーを分けることで、ビジネスロジック内での例外処理記述を減らす方法で問題の解決を図ります。
これがException Handlerが必要になる理由です。

web apiのリクエスト/レスポンスをやりとりする層を外側、コアになるビジネスロジックやDBアクセスが配置される層を内側と表現すると、Exception Handlerは、比較的外側へ配置されるイメージです。
Exception Handlerは、ビジネスロジックなどから投げられキャッチされなかった例外を、最終的に受けて、処理する役割を果たします。

実装紹介

Exceptionに応じた処理をまとめて記述するための構造がWebアプリに有用なことはわかりましたが、実はKemalには(まだ)これをサポートする機能がありません。
そこで、今回、KemalのCustom Handlerとして記述できるException Handler(Mapper)のサンプルを作成しました。

./exception_handler/以下にマッピングさせたい処理を記述し、こちらのexception_handler.crを読み込ませるだけでOKです。
処理は次のように記述できます。

exception SampleException do |env, ex|
  LOGGER.info "SampleException is mapped."
  LOGGER.error ex.inspect_with_backtrace
  env.response.content_type = "application/json"
  halt env, status_code: 500, response: Hash{"message" => "Internal Server Error", "cause" => "SampleException"}.to_json
end

この例では、ロジック内でSampleExceptionが投げられ、キャッチされなかった場合に、ここで記述した処理が実行されます。
この仕組みにより、ビジネスロジックでは例外処理の記述を減らし、プログラムの見通しをすっきりさせることができるようになります。

雑記

先日Kemalのgitterでこちらのexception-handlerを紹介したところ、そこそこウケたので、ブログでも共有させてもらいました。
将来的にKemal自体に組み込まれてしまえば不要になりますが、それまでは有用な実装になると思います。

Crystal(Kemal)でWeb APIを実装する

仕事でがっつり使う機会があり、実験と学習を兼ねて。
簡素なCRUDを実装した成果物は、githubにアップしてあります。

github.com

Crystal Programming Languageとは

Ruby風の文法、開発効率の良さと、高速実行可能なExecutableへのコンパイルとの両立を目指した、オープンソースの言語です。
簡潔な記述が可能でありながらも、コンパイラによる型推論が、安全なプログラム作成をサポートしてくれます。

The Crystal Programming Language

以下でいくつか特徴を見てみます。
なお、以下のサンプルソースでファイル名を記載しているものは、全て上述のgitリポジトリからの引用です。

型推論の威力

Crystalは静的な型の概念がある言語です。
コンパイラは、明示的な型宣言がなくとも、値にマッチする型を見つけようとし、エラーがあればコンパイル時に検出します。

class_checker = "hello, world"
puts typeof(class_checker)
# String

class_checker = 2018
puts typeof(class_checker)
# Int32

とはいえ上記のようなシンプルな例ではあまりメリットを感じられません。
私がこの仕組みについて感動したのは、処理の分岐(エラーハンドリング含む)等を記述するシーンだったので、紹介します。

下記で出てくるhalt envは、Contextを終了する役割を果たす、Kemalというライブラリのマクロです。
ここでは、halt envが呼ばれると、その後のif以降の処理へは進まないことがポイントです。

# src/note-api/resource/note_resource.cr

  begin
    changeset = Repo.insert(note)
  rescue e
    log(e.to_s)
    halt env, status_code: 500, response: {message: "Internal server error."}.to_json
  end

  if changeset.errors.empty?
.
.
.

注目すべき点は、changesetの宣言が事前に必要ないところです。
なぜならば、Repo.insertが成功した際に返る型が何かを(Repo.insertの定義ファイルから)コンパイラは分かっており、begin~end後の処理は、changesetの値が必ずRepo.insertの返り値になっていることを推論できるからです。

Javaっぽく書いたものと比べてみると・・・

  ChangeSet changeset = null;
  try {
    changeset = noteRepository.insert(note);
  } catch(Exception e) {
    logger.error(e);
    return Response.Status.INTERNAL_SERVER_ERROR.buid();
  }

  if (changeset != null && !changeset.hasError()) {
.
.
.

changesetの宣言やnullチェックの点でコードの記述を減らせることがわかります。
(尤も、このケースではnoteRepository.insertの返り値がnullにならないことを、「ソースコードから人間が読み取れば」、nullチェックを省くこともできます)

それでいて、たとえば下記のようにエラーキャッチ後も処理を続行するようになっていると、Crystalのコンパイラは指摘をくれます。

  begin
    changeset = Repo.insert(note)
  rescue e
    log(e.to_s)
    # comment out here and continue process
    # halt env, status_code: 500, response: {message: "Internal server error."}.to_json
  end

  if changeset.errors.empty?
.
.
.
# try to compile with above inappropriate code
$ crystal build src/note-api.cr 
.
.
.
in src/note-api/resource/note_resource.cr:28: undefined method 'errors' for Nil (compile-time type is (Crecto::Changeset::Changeset(Model::Note) | Nil))

  if changeset.errors.empty?

コンパイラによる推論が働かないと、こういったケースでは変数がnullになる可能性を見逃してしまう可能性があります。
一方Crystalでは、このような仕様のおかげで、(説明のために極端に言ってしまえば)「意味のある記述だけを」「安全に」行うことができます。

拡張性

Crystalは、非常に拡張性の高い言語です。
全てがオブジェクトであり、演算子(+, -など)もメソッドとして振る舞いが定義されています。

また、ひとつのクラスについて、いくつかのファイルに分割して記述することができます。
そのため、標準定義やライブラリ内の定義も、必要に応じて手軽に上書きすることができます。
もちろん、逆のことを言えば、どこで上書きがされるか分からない怖さもあります。

# src/note-api/repo.cr

module Crecto::Adapters::Mysql
  private def self.instance_fields_and_values(query_hash : Hash)
    {fields: query_hash.keys, values: query_hash.values.map { |v| v.is_a?(Time) ? v.to_s("%Y-%m-%dT%T.%L") : v.as(DbValue) }}
  end
end

この例では、Time型の値からSQLを記述する際に、ミリ秒まで含められるように変更を加えています。

マクロ

マクロを利用することで、煩雑なソースコードの記述を削減することができます。
コンパイル時に評価され、規則に応じたCrystalのソースコードが生成されます。

# src/note-api/filter.cr

macro reject(env, status_code, response)
  {{env}}.response.status_code = {{status_code}}
  {{env}}.response.print {{response}}
  raise Kemal::Exceptions::CustomException.new({{env}})
end

上記の例は、あまりうまくメリットを表現したものではないかも知れませんが、いくつか典型的なユースケースは考えられます。

たとえば、ソースコードの階層化を目指すと必ず出てくる、オブジェクトAからBへのコンバート処理。
もとのオブジェクトから、同じ名前のフィールドを文字列にして別オブジェクトを返すだけのコンバーターなどは、あるあるかも知れません。
そして抽象化クラスなどをうまく作れない場合、オブジェクトの数だけこういった処理を記述しなければならないこともあります。
下記はイメージですが、マクロを用いて、field_namesからコードを生成させる仕組みにすれば、あらゆる型のオブジェクトで応用できます。

macro convert(original, created, field_names)
  {% for field_name in field_names %}
    {{created}}.{{field_name}} = {{original}}.{{field_name}}.to_s
  {% end %}
end
.
.
.
created = Created.new
convert original, created, [id, name]

あるいは、メソッド化することが適さないようなもの。
同じような処理が必要だけれど、かといってオブジェクトやサービスのメソッドにしてしまうと階層が壊れてしまう、、、
こういったケースでは、シンプルに記述するためのマクロを定義するだけで解決します。
ソースコードの管理や見通しを犠牲にすることなく、設計を保つことができます。

Kemalの始め方

Crystalの特徴がざっくりわかったところで、アプリケーションの実装に取り掛かります。
Crystalにはプロジェクト作成のためのコマンドが備わっているので、まずはこれを実行します。

$ crystal init app note-api-by-crystal
$ cd note-api-by-crystal/
$ ls
LICENSE  README.md  shard.yml  spec  src

ライブラリのインストール

続いて、WebフレームワークであるKemalをインストールして利用してみます。

kemalcr.com

軽量で高速なWebフレームワークであることをウリにしており、シンプルなAPIであればすぐに実装できます。

先ほど自動生成されたshard.ymlへ依存を記述することで、ライブラリをインストールすることができます。

# shard.yml

dependencies:
  kemal:
    github: kemalcr/kemal
    branch: master

コマンドにはshardsを使います。

shards/README.md at master · crystal-lang/shards · GitHub

該当のライブラリが依存している他のライブラリも、併せてインストールしてくれます。
libディレクトリが作成され、その中にライブラリは格納されます。

$ shards install
Fetching https://github.com/kemalcr/kemal.git
Fetching https://github.com/luislavena/radix.git
Fetching https://github.com/jeromegn/kilt.git
Installing kemal (0.22.0 at master)
Installing radix (0.3.8)
Installing kilt (0.4.0)

$ ls lib/
kemal  kilt  radix

実装

それぞれのWebAPIは下記のような実装になります。
特に難しいことはなく、HTTPメソッドとエンドポイントを記述して処理を囲むだけです。

# src/note-api/resource/note_resource.cr

get "/v1/notes" do |env|
  begin
    Repo.all(Model::Note).map { |note| Interface::NoteResponse.new(note) }.to_json
  rescue e
    log(e.to_s)
    halt env, status_code: 500, response: {message: "Internal server error."}.to_json
  end
end

また、HTTPメソッドに相当する個別の処理だけでなく、全てのリクエストへ影響するbefore_allなども用意されています。
これを利用することで、filterを実装することもできます。
ここでは、特定のリクエストヘッダーが含まれているかどうかをチェックしています。

# src/note-api/filter/header_filter.cr

before_all do |env|
  next unless Kemal.config.env == "production"

  some_header = env.request.headers["some_header"]?
  if some_header.to_s.empty?
    reject env, status_code: 400, response: {message: "some_header is needed."}.to_json
  end
end

メインになるクラスでKemal.runを実行させれば、サーバーが起動します。

# src/note-api.cr

require "uuid"

require "kemal"
require "mysql"
require "crecto"

require "./note-api/*"

Kemal.config.port = Config::PORT.to_i
Kemal.run

その他

Crystalには、すでにたくさんのライブラリの選択肢があります。
下記のページから、用途に沿ったものを選定することができます。

GitHub - veelenga/awesome-crystal: A collection of awesome Crystal libraries, tools, frameworks and software

ちなみに、RailsそっくりなAmberというフレームワークも存在します。
Quick Startなんてほとんど同じです。

amberframework.org

なお、今回、ORMにはCrectoを利用しています。
Webフレームワークと同様、既に幾つかの選択肢が出ていますが、その中ではcommit数の多いもので、使い勝手も良好です。
associationsなども組むことができます。

www.crecto.com

所感

私にとっては初めての型推論の言語で、特にnilチェックはやりすぎなんじゃないかと思うほどの指摘をコンパイラから受けつつの実装でした。
ただ、最初こそ戸惑ったものの、慣れればかなり快適に実装ができるなぁという印象です。
何よりコンパイル後のnative codeが高速に動作するのが気持ちよく、スケール「アウト」が求められたりマイクロサービスとして軽量なAPIを立ち上げたりといった分散型のシステムとは、比較的相性がよいように思えます。

リポジトリにはDockerfileやテストケースもアップしています。
運用面が気になる方はそちらもご参照ください。

GitHub - shiba-hiro/note-api-by-crystal: note-api-by-crystal is a sample web api project implemented by Crystal

warファイルを作成してTomcatへデプロイする

前2回の記事でTomcatの使い方について理解できたので、別のデプロイ方法に移ります。
今回はJavaのウェブアプリの成果物としてwarファイルを作成し、これをTomcatへデプロイして動作させる方法をとります。
デプロイするプログラムは、とりあえず先日と同じく、GETのHTTPリクエストに対してレスポンスを返すものとします。

shiba-hiro.hatenablog.com

shiba-hiro.hatenablog.com

warファイルについて

ファイルを生成する前に、そもそもwarファイルって何なんでしょうか。
これは、Web application ARchiveの略称で、Webアプリの実行に必要な諸々がzip形式で圧縮されたものになります。
「諸々」には、webアプリで利用されるためのクラスファイルや設定系のファイル(web.xmlとか)、jsp、ライブラリなどが含まれます。
これらが、特定の階層構造に沿って配置されることが想定されています。

IBM Knowledge Center

Webアプリケーションをwarファイルでまとめる:JavaTips 〜JSP/サーブレット編 - @IT

似た名称のものとしてjarファイル(Java ARchive)があります。
こちらもやはりzip形式で圧縮されたものですが、複数のクラスファイルなどがひとつにまとめられたアーカイブになります。

JAR ファイルの概要

圧縮形式はzipなわけですが、特定の決まりや機能、構造を持たせて拡張子を統一することで、役割をわかりやすくしているわけですね。

プロジェクトの作成

Mavenを利用してプロジェクトを作成します。
Mavenの導入方法は下記の記事を参照のこと。

shiba-hiro.hatenablog.com

今回はmaven-archetype-webappというarchetypeを指定します。

$ mvn archetype:generate \
> -DgroupId=com.sample.webproject \
> -DartifactId=webproject \
> -DarchetypeArtifactId=maven-archetype-webapp

webprojectというディレクトリができるので、Javaのソースの置き場所を作成します。

$ cd webproject/
$ mkdir -p src/main/java/com/sample/webproject

作成したディレクトリへ、次のようにSampleAPIController.javaを置きます。

package com.sample.webproject;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SampleAPIController extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        
        response.setContentType("application/json");
        PrintWriter out = response.getWriter();
        out.println(" { \"key\" : \"value\" } ");

        return;
    }
}

サーブレットAPIを利用するために、pom.xmlへも次の依存を追記して・・・

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.4</version>
      <scope>provided</scope>
    </dependency>

src/main/webapp/WEB-INF/web.xmlを次のように書き換えれば準備完了です。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
  <servlet>
    <servlet-name>SampleAPIControllerName</servlet-name>
    <servlet-class>com.sample.webproject.SampleAPIController</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>SampleAPIControllerName</servlet-name>
    <url-pattern>/get-api</url-pattern>
  </servlet-mapping>
</web-app>

warファイルの生成とTomcatへのデプロイ

ではwarファイルを生成してTomcatへデプロイしましょう。
warファイルは、ホストOSで作ったものを仮想環境へ共有しても、仮想環境上でmvnコマンドを使って生成してもどちらでも構いません。

コマンドを実行する前に、pom.xmlを再度確認しておきましょう。
<packaging>war</packaging>という記述があることがわかると思います。
ここで指定されているものが、このプロジェクトの成果物の形になります。

ではプロジェクト配下でmvn packageを実行します。

$ mvn package

すると、targetディレクトリ配下にwebproject.warファイルができます。
このファイルを(Tomcatをインストールした仮想マシンの)/var/lib/tomcat8/webapps/ディレクトリ以下に移してTomcatを再起動しましょう。
そして指定のURLへGETリクエストを投げると・・・

$ sudo mv YOUR_DIR/webproject.war /var/lib/tomcat8/webapps/
$ sudo service tomcat8 restart

$ curl http://localhost:8080/webproject/get-api
 { "key" : "value" } 

見事レスポンスを得られました。
ビルドツールを基軸にして、プロジェクトの作成からデプロイ対象の成果物生成までをスムーズに行えました。
また、成果物をTomcatへデプロイして動作確認することもできました。
次回はDockerを用いた形でのデプロイを行ってみます。

Tomcat(常駐のアプリケーションサーバープロセス)について 〜 昔はみんなサーバーレスだった

前回の記事では、Javaを利用してAPIを公開するために、いきなりTomcatを持ち出しましたが、今回はTomcatについて少し解説&考察してみます。

shiba-hiro.hatenablog.com

Tomcatの役割

Tomcatは、Javaプログラムを動作させることで、所謂アプリケーションサーバーという役割を果たすことを期待されています。
アプリケーションサーバーは、アプリケーションとして必要なロジックを保持し、リクエストに対して動的にレスポンスを生成して返却します。

対してWebサーバーは、静的なコンテンツを返却するサーバーです。
リクエストの一次受けとルーティングを行うケースもあります。

アプリケーションサーバーやWebサーバーは役割に基づく名前であって、必ずしもツールに張り付く名前ではありません。
なのでTomcatをWebサーバー的に活用することも可能です。
実際、前回の記事では静的なHTMLを返却させることも行ってみました。

Tomcat以前の課題を振り返ると、その役割の意義をより理解しやすいように思います。
登場の背景はこの記事がわかりやすいです。

Tomcatって何ですか? | Think IT(シンクイット)

すなわち、当初はサーバーサイドへのリクエストがあるたびに、サーバー外部のプロセスの起動を行っていました。
が、大量アクセスによるレスポンスの鈍化が見過ごせなくなり、解決策が必要になってきました。
そこで、「逐一プロセスの起動を行わない方法」が出てきます。

Tomcatの動作

「逐一プロセスの起動を行わない方法」として、サーブレットという技術が開発されます。
サーブレットでは、単一のプロセスが常に起動しており、リクエストに応じてプロセス内部のスレッドを起動します。
スレッドの起動負荷はプロセスのそれと比較して小さく、見事問題を解決することができました。

サーブレットを動作させるためには、サーブレットコンテナというサーバーが必要です。
Tomcatは、このサーブレットコンテナのひとつです。

先ほど紹介した記事内では、サーブレットのライフサイクルについても言及されています。
前回、拡張して利用したHttpServletが持っているservice()doGet()といったメソッドがどのように活用されているかをよく理解できると思います。

HttpServlet (Java(TM) EE 7 Specification APIs)

昔はみんなサーバーレスだった?

「常駐するプロセスを用意するのは、リクエストのたびにプロセスを逐一起動するのを避けるためだった」
私はこれを知ったとき、「リクエストごとに起動する方向へ、サーバーレスって名前で回帰してるやん!」と思ってしまいました。

Tomcatはセッション管理機能も有しています。
これも、リクエストごとにプロセスが起動して破棄されるようでは状態を管理できないからと、当時の課題を解決する形でTomcatに備えられたもののようです。
しかしながら、現代的には、アプリケーションサーバーにセッション管理をさせるのは筋が悪いと言えるでしょう。
いまは、大量リクエストを捌くためには、アプリケーションサーバーを水平にスケールさせます。 その際、プログラムのコードと関係ない各サーバーの状態を、物理的に新しく立てるサーバーへコピーするというのは現実的ではありません。
当該セッション情報を利用したいのが、最初にリクエストを受け取ったアプリケーションだけとも限りません。
ログイン情報などはインメモリのキャッシュサーバーを別途立てて管理するのが通例でしょう。

そして水平にサーバーを稼働させるのであれば、要請に応じてすぐ起動できるようにアプリケーションを構成する必要があります。
より粒度の高いモジュール編成やデプロイが追求されます。
(起動にかかる時間を見越して、特定の閾値を超えれば前のめりでサーバーを起動させるのが普通でしょうけれども)
結果、すぐに起動できるプロセスになるのであれば、なおのこと常駐のプロセスなんて要らないよね、という話になります。

常駐プロセスの登場もスケーラビリティの要請に沿ったものなのにも関わらず、さらにスケーラビリティの要請に応えようとするとそういった技術に頼らない構成になる。
これは、とても興味深い事象だなと感じます。
(もちろん、巨大なサーバーが常駐することで発生する無駄なリソースの消費を抑えたいという別面の要請もありますが)

この変遷は、個人的には、プロセスを動作させるハード面の性能向上とネットワーク環境の向上、クラウド環境の充実によるものなのかなと考えています。
(言わずもがな、その根底には、デバイスの普及やコンテンツへの要求水準向上もあったでしょう)
前提が変わると、選択可能な手段はドラスティックに変わるということですね。
基盤に近いインフラの進化がもたらす変化の大きさを理解できる事例だなとも思います。

JavaプログラムをTomcatへデプロイする

前回は、Mavenを用いてJavaの簡単なプログラムを作成し、実行してみました。
今回は、そういったJavaのプログラムをTomcatというサーバーへデプロイし、ごくごく簡素なWeb APIをつくってみます。

shiba-hiro.hatenablog.com

動作確認の準備

初回はVagrantのボックスリストでトップにきていたtrusty64(14.04)のUbuntuを使っていましたが、
今回からはゲストOSのバージョンをxenial64(16.04)へ変更します。
(自分が普段使っているPCのバージョンに合わせたいだけです。。。trustyのままでも操作は大差ありません)
適当なディレクトリを作成し、次のコマンドでVagrantfileを作成します。

$ mkdir ~/SandBox/vagrant-practice/for-tomcat
$ cd ~/SandBox/vagrant-practice/for-tomcat

$ vagrant init ubuntu/xenial64

さらに、効率的に実験を行うために、先んじてVagrantfileへいくつかの設定を追記しておきます。

ひとつめは、ポートのフォワーディングです。
仮想マシン内に立てたサーバーとホスト側とで通信を行うためです。
利用するTomcatは、HTTPのリクエストを8080番ポートで受けるというのがデフォルトの設定になっています。
そこで、ゲストOSの8080番ポートへリクエストを転送できるよう、次のような設定を書き足します。

config.vm.network "forwarded_port", guest: 8080, host: 7878

ふたつめは、共有フォルダの設定です。
今回からはエディターやIDEで編集した各種ファイルを使う前提で進めます。
ファイル共有の方法は、先日のgitを導入した記事でも紹介していますが、ローカルPCで完結することではあるので(かつ今回は共有方法がクリティカルな問題になる内容ではないので)、ホストOSとゲストOSをつなぐ共有ディレクトリを作成してしまいましょう。

今回使う共有用のディレクトリはsharedと名付けて、Vagrantfileと同じ階層へ作成しておきます。

config.vm.synced_folder "./shared", "/home/vagrant/shared", create: true, owner: "vagrant", group: "vagrant"

shiba-hiro.hatenablog.com

設定を更新できたら、 vagrant up仮想マシンを立ち上げます。

次のようなログが流れたのを確認できるはずです。

...
==> default: Forwarding ports...
    default: 8080 (guest) => 7878 (host) (adapter 1)

...
==> default: Mounting shared folders...
default: /home/vagrant/shared => /Users/shiba_hiro/SandBox/vagrant-practice/for-tomcat/shared

また、vagrant sshすると、ホームディレクトリにsharedディレクトリを確認できます。

$ ls
shared

Tomcatのインストールと起動

それではTomcatをインストールしましょう。

$ sudo apt update
$ sudo apt install -y tomcat8

# apt installで取得したアプリは自動で起動するので、
# runningのステータスを確認できる。
# qを入力すると終了できる。
$ sudo service tomcat8 status

Tomcatの起動を確認できたら、早速リクエストを送ってみましょう。
ホストOSのブラウザで下記のURLへアクセスすると、 "It works !"と記載されたページを表示できるはずです。

http://localhost:7878/

静的なページを返す

まずはオリジナルの静的なページを返却させてみましょう。
次のようなHTMLファイルを作成して、sharedディレクトリ経由で仮想マシンに渡します。

  • index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Sample HTML</title>
  </head>
  <body>
    <div class="html-body">

      <main>
        <div>
          <span>simple html document</span>
        </div>
      </main>

    </div>
  </body>
</html>

作成したら、/var/lib/tomcat8/webapps/下に新しくディレクトリを作って、ファイルを配置します。

$ sudo mkdir /var/lib/tomcat8/webapps/sample
$ sudo cp ~/shared/index.html /var/lib/tomcat8/webapps/sample/

配置したら、ホストOSで下記のURLへアクセスすると、作成したhtmlを表示できます。

http://localhost:7878/sample/

こんな感じで、Webサーバー的にTomcatを利用できることが確認できました。

サーブレットを利用してみる

次は、ごくごく簡単なweb apiを作ってみます。
サーブレットと呼ばれるjavaプログラムの仕組みに乗っかって、レスポンスを返すプログラムをTomcat上で動かします。

下記のようなjavaファイルを用意しましょう。

  • SampleAPIController.java
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SampleAPIController extends HttpServlet {

  public void doGet(HttpServletRequest request, HttpServletResponse response)
          throws IOException, ServletException {

    response.setContentType("application/json");
    PrintWriter out = response.getWriter();
    out.println(" { \"key\" : \"value\" } ");
    return;
  }
}

doGetはHttpServletが持っているメソッドで、HTTPのGETリクエストに応答します。

javaファイルの作成ができたら、仮想マシン側にJDKをインストールして、クラスファイルを作成します。

$ sudo apt install -y default-jdk

# tomcat付属のライブラリとともにコンパイル
$ javac -cp /usr/share/tomcat8/lib/servlet-api.jar ~/shared/SampleAPIController.java

# sample配下にWEB-INFディレクトリを作成して配置
$ sudo mkdir -p /var/lib/tomcat8/webapps/sample/WEB-INF/classes
$ sudo cp ~/shared/SampleAPIController.class /var/lib/tomcat8/webapps/sample/WEB-INF/classes/

さらに、次のweb.xmlファイルも作成して配置します。
このファイルを置くと、記述に沿って、Tomcatサーブレットを動作させてくれます。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
  <servlet>
    <servlet-name>SampleAPIControllerName</servlet-name>
    <servlet-class>SampleAPIController</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>SampleAPIControllerName</servlet-name>
    <url-pattern>/get-api</url-pattern>
  </servlet-mapping>
</web-app>
# web.xmlも、WEB-INF配下へおく
$ sudo cp ~/shared/web.xml /var/lib/tomcat8/webapps/sample/WEB-INF/

ここまでできたら、次のURLへアクセスしてみてください。

http://localhost:7878/sample/get-api

あるいは、curlコマンドでリクエストしても構いません。
いずれにしても、{ "key" : "value" }という表示を返してくれるはずです。

これで、とてもシンプルなものですが、Javaのプログラムをサーバー上で動作させることができるようになりました。

引数を利用できるようにアレンジを加える

せっかくなので、もう少しアレンジを加えてみます。
引数を利用してプログラムを動作させてみましょう。

まずはjavaファイルを、引数を利用するように書き換えて配置しなおします。

  • SampleAPIController.java
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SampleAPIController extends HttpServlet {

  public void doGet(HttpServletRequest request, HttpServletResponse response)
          throws IOException, ServletException {
    String arg = System.getProperty("testval");

    response.setContentType("application/json");
    PrintWriter out = response.getWriter();
    out.println(" { \"key\" : \"" + arg + "\" } ");
    return;
  }
}
$ javac -cp /usr/share/tomcat8/lib/servlet-api.jar ~/shared/SampleAPIController.java
$ sudo cp ~/shared/SampleAPIController.class /var/lib/tomcat8/webapps/sample/WEB-INF/classes/SampleAPIController.class 

さらに、setenv.shというファイルを作成して、次のような記述をおきます。

CATALINA_OPTS="-Dtestval=this-is-arg"

作成後は、/usr/share/tomcat8/bin/の下に配置します。
こうすることで、起動時にスクリプトが読み込まれるようになります。

$ sudo cp ~/shared/setenv.sh /usr/share/tomcat8/bin/

では、tomcatを再起動させて、再度get-apiへアクセスしてみましょう。

$ sudo service tomcat8 stop
$ sudo service tomcat8 start

# 仮想マシン内では8080ポートを利用しているので、
# 8080へリクエスト
$ curl http://localhost:8080/sample/get-api
 { "key" : "this-is-arg" }

このように、引数で設定した値を取り出して表示できていたら、実験は成功です。
TomcatJavaプログラムを乗せて動作させる感覚がつかめてきたので、今度はMavenと組み合わせて使ってみます。

Ubuntuでvagrant upした際にフリーズしてしまう事象の解消

Ubuntu 16.04上でvagrant upした際にPC(ホストOS)がフリーズしてしまう問題が起きていました。

# vagrantを利用すると、ここで止まってしまう
==> default: Booting VM...

代替案としてDockerを利用していたものの、どうも使いづらく感じることが多かったので、やはりvagrantを復旧したいと思い、情報を漁っていました。
寄り道も多かったのですが、復旧できたのでその方法を記載します。

なお結局起こっていた問題としては、(vagrant経由であるなしに関わらず)VirtualBoxでゲストOSをブートするタイミングでフリーズする、というものです。
バージョン確認などのvagrantコマンドが全く使えなくなるとかではないし、VirtualBoxGUIで操作してゲストOSを起動しようとする際にもフリーズは発生しました。

VMWareを使う方法もあったんですが、スムーズにいかなかったので諦めました。
ここでは、引き続きVirtualBoxを使う形で復旧します。

解決方針の決定

とりあえずキーワードでググったところ次の記事が見つかりました。

Ubuntu 16.04 freezes on vagrant up - Ask Ubuntu

事象としては全く同じだったので、ここに記載されているよう、
Vagrantのバージョンを2.0.1へ、
VirtualBoxのバージョンを5.2へ、
それぞれ上げる方向で解決します。

なお、トラブル発生時点、修正前の各ソフトのバージョンは次の通り。

$ vagrant -v
Vagrant 1.8.1

$ vboxmanage -v
5.0.40_Ubuntur115130

インストールをやり直す

ここでは、一度アンインストールを行ってから再度インストールを行います。

何はともあれ、いまのバージョンをいったん捨てます。

$ sudo apt purge -y vagrant

が、実行したところ、「/var/lib/dpkg/lockが取得できないよ」と怒られてしまいました。。。
自分のケースでは裏側でVirtualBoxが起動していたのが原因だったので、これを止めてアンインストールを進めます。

# VirtualBoxの起動状況を確認し、必要なら停止させる
$ sudo service virtualbox status
$ sudo service virtualbox stop

# まずVagrantをアンインストール
$ sudo apt purge -y vagrant
$ rm -r ~/.vagrant.d

# 続いてVirtualBoxもアンインストール
$ sudo apt purge -y virtualbox

続いて、それぞれをバージョン指定しつつインストールします。

# まずはVirtualBox更新のための諸々を進める
$ sudo add-apt-repository "deb http://download.virtualbox.org/virtualbox/debian xenial contrib"
$ wget -q https://www.virtualbox.org/download/oracle_vbox_2016.asc -O- | sudo apt-key add -
$ wget -q https://www.virtualbox.org/download/oracle_vbox.asc -O- | sudo apt-key add -

$ sudo apt update
$ sudo apt install -y virtualbox-5.2 dkms

# VirtualBoxインストールの成功を確認
$ vboxmanage -v
5.2.6r120293


# 続いてVagrant
$ wget https://releases.hashicorp.com/vagrant/2.0.1/vagrant_2.0.1_x86_64.deb
$ sudo apt install -y ./vagrant_2.0.1_x86_64.deb

# Vagrantインストールの成功を確認
$ vagrant -v
Vagrant 2.0.1

ドライバの設定をリフレッシュする

上記までで、希望していたバージョンのソフトウェアをインストールできました。
では適当なディレクトリを作成して早速起動、と思ったところでまた問題が。。。

$ vagrant init ubuntu/trusty64
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Box 'ubuntu/trusty64' could not be found. Attempting to find and install...
    default: Box Provider: virtualbox
    default: Box Version: >= 0
==> default: Loading metadata for box 'ubuntu/trusty64'
    default: URL: https://vagrantcloud.com/ubuntu/trusty64
==> default: Adding box 'ubuntu/trusty64' (v20180206.0.0) for provider: virtualbox
    default: Downloading: https://vagrantcloud.com/ubuntu/boxes/trusty64/versions/20180206.0.0/providers/virtualbox.box
==> default: Successfully added box 'ubuntu/trusty64' (v20180206.0.0) for 'virtualbox'!
==> default: Importing base box 'ubuntu/trusty64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'ubuntu/trusty64' is up to date...
==> default: Setting the name of the VM: for-ubuntu_default_1518682274211_45707
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Booting VM...
There was an error while executing `VBoxManage`, a CLI used by Vagrant
for controlling VirtualBox. The command and stderr is shown below.

Command: ["startvm", "ad479413-2179-4177-a944-be0ca8a8e145", "--type", "headless"]

Stderr: VBoxManage: error: The virtual machine 'for-ubuntu_default_1518682274211_45707' has terminated unexpectedly during startup with exit code 1 (0x1)
VBoxManage: error: Details: code NS_ERROR_FAILURE (0x80004005), component MachineWrap, interface IMachine

再度調べてみたところ、どうやらドライバーのバージョンが更新されていないらしい・・・?

virtualbox - VBoxManage is unable to start vm code NS_ERROR_FAILURE ubuntu 16.04 - Ask Ubuntu

実際にmodinfo vboxdrvというコマンドを叩いてみると、versionの値が5.2系であることを期待していたものの、5.0系のようでした。
上記記事の回答では設定ファイルを削除(!)して再起動、再インストールするというアグレッシブな解決が提示されています。
さすがに怖すぎるので、再設定系のコマンドを見つけてきて実行することにしました。

$ sudo dpkg-reconfigure virtualbox-dkms
$ sudo dpkg-reconfigure virtualbox-5.2

$ modinfo vboxdrv
version:        5.2.6

そして一度ホストOSを再起動し、Vagrant実験用のディレクトリも丸ごと作成し直しました。
すると、無事に起動することを確認できました。

# PCの再起動後、Vagrantfile含むテスト用ディレクトリも作り直し
$ rm -r for-ubuntu
$ mkdir for-ubuntu
$ cd for-ubuntu

# ようやく成功。。。
$ vagrant init ubuntu/trusty64
$ vagrant up

結構時間をかけてしまったので、同じ苦労を踏む人が減ればなーって感じです。