フレクトのクラウドblog re:newal

http://blog.flect.co.jp/cloud/からさらに引っ越しています

Einstein Visionで微妙な判定の境界を探る

はじめに

エンジニアの佐藤です。こんにちは。 前回に引き続いて機械学習の話ですが。今回は Heroku Einstein Add-on について書かせていただきます。

ご存知の通り近頃はAIサービス関連のニュースを聞かない日はないほどです。ですが、この流行りに懐疑的な気持ちをお持ちの方も多いのではないでしょうか。実は筆者も、期待はしつつも楽観は危険と考える一人です。

遊びなら、失敗も愛嬌のうち。しかし失敗が「損失」に直結するビジネスでは、できるだけ失敗を避けたいと思うのが普通でしょう。では AIは本当に、十分簡単な課題なら確実に動作してくれるのでしょうか? 今回はこの点を確かめてみたいと思います。

今回紹介する Heroku Einstein Add-on はRESTインターフェイスで操作でき、機械学習の基本的な知識だけでモデルの実装までできます。詳細は他へゆずりますが、とても簡単に使えます。また、今回のような小規模用途であれば、完全無料で試すことができます。 Einstein Add-onの機能は大きく画像認識(Vision)と文書認識(Language)がありますが、今回は画像認識(Vision)を試してみます。

問題設定

画像認識のこの上なく単純な問題として、「boxかbarか」を考えてみます。これは辺の比率によって長方形を2つに分類し、細長い方をbar、他方をboxと判定させるものです。boxとは

f:id:masashi-sato-flect:20180331142540p:plain

で、barとは

f:id:masashi-sato-flect:20180331142605p:plain

です。比率の境界は1:2と決め、これと等しいか、より細長いものはbar、ほかはbox とします。背景は水色、長方形は黒です。回転なし、歪みなしの理想系でやってみましょう。これ以上ない簡単な分類と思いますが、さてEinstein Visionは 「十分簡単だから完璧に」 動作するでしょうか?

トレーニングデータと検証データ

今日流行りのAIは、大規模な機械学習でこれまで不可能だった新機能を実現するものです。機械学習とは前回ブログで書いた通り「データでアルゴリズムを削り出す作業」ですからデータが必要です。

では今回のデータとは?それは様々な比率の長方形以外にはありません。以下のようなプログラムで自動生成します。

from PIL import Image, ImageDraw

for i in range(200):
    img = Image.new('RGB', (400, 200), '#7FFFFF')

    x1 = 50
    y1 = 50
    x2 = 150 + i
    y2 = 150

    draw = ImageDraw.Draw(img)
    draw.rectangle(
        [(x1, y1), (x2, y2)],
        '#000000'
    )

    label = 'box' if i < 100 else 'bar'
    
    if i % 10 == 0:
        fileName = 'eval{:03d}.png'.format(i)
        self.__save_eval(img, label, fileName)
    else:
        fileName = 'train{:03d}.png'.format(i)
        self.__save_train(img, label, fileName)
# end of for (i)

400px x 200px の下地の上に 50px のマージンを確保し、高さは 100px固定、幅は 100pxから 300pxまで 1px刻みで変化させます。 i = 100 で比率が 1:2になりますので、これより前は「box」、これ以降は「bar」とラベル付けします。 また、 10回に 1回、つまり全 200画像の 20個を、テスト用データとしてトレーニングからはよけておきます。

f:id:masashi-sato-flect:20180331142642p:plain

詳しい手順はリファレンスにゆずりますが、このトレーニング画像集を zip形式でアーカイブし、あとは curlコマンドでちょこちょことやれば画像認識の機械学習モデルができあがります。単純極まるデータのためトレーニングも 5分もかからず終了。非常に簡単です。

トレーニング経過とモデルの確認

「非常に簡単」と書きましたが、 油断してはいけません。 これは遊びではなく、ビジネスなのです。およそコンピュータと名のつくもの、何物も疑いの目で見なければいけません。

最初にモデルのメトリクスを確認します。

curl -X GET -H "Authorization: Bearer $EV_TOKEN" -H "Cache-Control: no-cache" https://api.einstein.ai/v2/vision/models/[MODEL_ID] | jq .
{
  "metricsData": {
    "f1": [ 1,  1 ],
    "labels": [ "bar", "box" ],
    "testAccuracy": 1,
    "trainingLoss": 0.5564,
    "confusionMatrix": [
      [ 7, 0 ],
      [ 0, 8 ]
    ],
    "trainingAccuracy": 0.6963
  },
  (一部省略)
}

Einstein Visionのトレーニングでは、トレーニング用にアップロードされたデータのランダム選抜された10%がトレーニング結果の評価のために使われます。その結果は「testAccuracy: 1」つまり、全問正解だったとあります。他にもいろいろありますが、ひとまずは良さそうです。

次は学習曲線です。

curl -X GET -H "Authorization: Bearer $EV_TOKEN" -H "Cache-Control: no-cache" https://api.einstein.ai/v2/vision/models/[MODE_ID]/lc | jq -r -f lc.jq

lc.jqは Einstein Vision RESTレスポンスの調整に使用したもので、以下のような内容です。 .data[] | "(.epoch), (.metricsData.trainingAccuracy), (.metricsData.testAccuracy)"

epoch, trainingAccuracy, testAccuracy
1,  0.665,  1
2,  0.6978, 0.9333
3,  0.6909, 1
4,  0.7021, 0.9333
5,  0.6812, 1
6,  0.7095, 0.9333
7,  0.7075, 0.9333
8,  0.7021, 0.9333
9,  0.6963, 1
10, 0.7002, 0.9333
11, 0.7061, 0.9333

Epoch 1からいきなりtestAccuracyが 1(最高値)になり、以後ほとんど変化しません。 要するにトレーニングは「超 楽勝だった」ようです。 本当でしょうか?

本番テスト

では、今度はトレーニングに使用していない検証用データで判定させてみましょう。結果は以下のようになりました。

/eval/box/eval000.png  box:0.83328694 bar:0.16671309 
/eval/box/eval010.png  box:0.8120093  bar:0.18799073 
/eval/box/eval020.png  box:0.79243547 bar:0.20756452 
/eval/box/eval030.png  box:0.7771254  bar:0.22287455 
/eval/box/eval040.png  box:0.74343425 bar:0.25656578 
/eval/box/eval050.png  box:0.7180637  bar:0.28193632 
/eval/box/eval060.png  box:0.66442096 bar:0.33557907 
/eval/box/eval070.png  box:0.64682674 bar:0.35317332 
/eval/box/eval080.png  box:0.5968405  bar:0.4031595 
/eval/box/eval090.png  box:0.5487534  bar:0.45124662 

/eval/bar/eval100.png  bar:0.5115235  box:0.48847654 
/eval/bar/eval110.png  bar:0.55833775 box:0.44166228 
/eval/bar/eval120.png  bar:0.6156298  box:0.38437027 
/eval/bar/eval130.png  bar:0.5786646  box:0.42133546 
/eval/bar/eval140.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval150.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval160.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval170.png  bar:0.8948712  box:0.105128884
/eval/bar/eval180.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval190.png  bar:0.8948712  box:0.105128884 

雑ですいませんが、一番上が正方形(前述の画像作成コードで i = 0 で作成された画像)で、以後下へ行くほど長方形の長辺が伸長し、/eval/bar/eval100.png の時にちょうど 短辺:長辺 = 1:2 になります。

判定結果は、確かに完璧です。 しかも boxと barの境界である 短辺:長辺 = 1:2 に近づくほど、 boxラベルと barラベルの probabilityが拮抗し(つまり判定の確からしさが減少し)、我々人間が感じる感触に近くなっていて親近感がありますね。。。 興味深いのは、「文句なく細長く」なった /eval/bar/eval140.png以降は probabilityの変化がないことです。これはいわば「残りの領域は見る必要がない」という感じでしょうか(筆者の主観ですが)。

それでも疑ってみる

しかし、、、疑い深い筆者は考えてしまいます。これは何かの偶然ではないのかと。そこで もう一度、完全に同じデータでトレーニングとテストをやり直して見ます。

結果は以下のようになりました。

2つ目のモデルのメトリクス

{
  "metricsData": {
    "f1": [ 1, 1 ],
    "labels": [ "bar", "box" ],
    "testAccuracy": 1,
    "trainingLoss": 0.5796,
    "confusionMatrix": [
      [ 7, 0 ],
      [ 0, 8 ]
    ],
    "trainingAccuracy": 0.6826
  },
  (一部省略)
}

学習曲線

epoch, trainingAccuracy, testAccuracy
1,  0.6367, 0.8667
2,  0.6934, 1
3,  0.6826, 1
4,  0.6904, 0.9333
5,  0.6753, 0.9333
6,  0.6982, 0.9333
7,  0.6992, 0.9333
8,  0.6904, 0.8667
9,  0.6851, 0.8667
10, 0.6963, 0.9333
11, 0.7148, 0.9333

テスト結果

/eval/box/eval000.png  box:0.7758191  bar:0.22418085 
/eval/box/eval010.png  box:0.7284226  bar:0.27157742 
/eval/box/eval020.png  box:0.6806543  bar:0.31934574 
/eval/box/eval030.png  box:0.6620312  bar:0.33796886 
/eval/box/eval040.png  box:0.6430078  bar:0.35699224 
/eval/box/eval050.png  box:0.61368895 bar:0.38631108 
/eval/box/eval060.png  box:0.57791686 bar:0.4220831 
/eval/box/eval070.png  box:0.5619922  bar:0.43800783 
/eval/box/eval080.png  box:0.5478586  bar:0.4521414 
/eval/box/eval090.png  box:0.5113313  bar:0.48866868 

/eval/bar/eval100.png  bar:0.51804864 box:0.4819514 
/eval/bar/eval110.png  bar:0.5393514  box:0.4606486 
/eval/bar/eval120.png  bar:0.55827296 box:0.44172707 
/eval/bar/eval130.png  bar:0.56668216 box:0.43331784 
/eval/bar/eval140.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval150.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval160.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval170.png  bar:0.8327772  box:0.16722284
/eval/bar/eval180.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval190.png  bar:0.8327772  box:0.16722284 

比べてみると、全体的傾向は同じですが、個々の数字は微妙に違います。

数値をExcelでプロットして見ましょう。

学習曲線については、差は小さいですが、

f:id:masashi-sato-flect:20180331142724p:plain

意外と違っていたのがテスト時の probability値です。

f:id:masashi-sato-flect:20180331142738p:plain

どちらも boxと barの判定は期待通りですが、プロットしてみると 2つのラベルで probabilityが反転している点が異なり、 bar1 / box1の交点は bar2 / box2の交点より x値が大きいところにあります。

なぜこのような違いが生じるのでしょうか?筆者は主に以下の2つの要因があると考えています。

トレーニングのランダムの結果が異なるから

一般的にディープラーニングのトレーニングでは乱数を利用するステップがいくつかあり、乱数の結果によってトレーニングの進行が異なります。もちろん、 トレーニングデータが十分にあればこれらの結果による影響は少なく、トレーニング結果は一定の範囲に収束するはずです。

トレーニング結果の評価のためのデータの選抜結果が異なるから

トレーニングに再しては、アップロードされたデータの10%をトレーニング評価用に使いますが、この選抜方法はランダムであり、指定することはできず、選抜結果を取得することもできません(本稿執筆時点)。したがって、今回180画像アップロードした画像のどれがトレーニング評価用に選択されたかで、 barと boxの probabilityを反転させる最適ポイントが変わります。 したがって、今回の場合 テストデータの 10番と 11番の周辺に、トレーニング評価用のデータが 1つもなかった場合は、テストデータの 10番か 11番のどちらかの判定に失敗していた可能性があると思います。 しかしこういうケースの発生確率は小さく、 たいていは 大丈夫なのです。

結論として、「boxかbarか」という本課題については、 Einstein Visionは十分確実な仕事をしてくれると言えそうです。 ですが、現実の画像判定ソリューションの場合、トレーニングデータもテストデータももっと複雑であり、その内容と評価基準について今回のように考察することはほとんど不可能でしょう。結局のところ、本番で高確率で成功したければ、「実戦に近く、かつ、豊富な経験」しかないという、言われるまでもない自明の真理に戻ってきたような気がします。AIと言えど、マジックはないようです。

最後までお読みいただき、ありがとうございました。

プログラム開発技術者によるTensorflow超基礎:「1 + ? = 2」を機械学習で解かせるには?

はじめに

みなさんこんにちは。エンジニアの佐藤です。今回はTensorflowの話を書かせていただきたいと思います。

みなさんもきっと既にご存知でしょう「Tensorflow」、ディープラーニングフレームワークとして最もポピュラーなもののひとつで、昨今のAIブームの折、その応用技術を身に着けたいと思っている方もきっと多いことでしょう。筆者もその一人です。ポピュラーでありますから、ちょっとググればさまざまな事例や解説が山のように出てきます。このいくつかを読めば、だいたい勘所がわかるだろう。そう筆者は思っていました。 ところが、、、読めども読めども今ひとつよくわからないのでした。いえ、大体はわかるのですよ。筆者は機械学習についての基本的概念は理解しているつもりで、ニューラルネットワークも、業務で取り組んできた経験があります。なのでTensorflowの事例も、概念や考え方については何の疑問もありません。 わからないのは、Tensorflowがどうやって、人間の思考をコードに落としているかです。つまりプログラムがはいスタートとなってから、CPUがどういう順番でライブラリの中を駆け巡って、最終的なゴールにたどり着くのか。その実行フローのイメージが、まるでつかめなかった。しかし、公式サイトをほとんど読了したある日、ようやくわかったような気がしました。 おそらくTensorflowの世界に足を踏み入れた時、私と同じ悩みを抱えるシステム開発系の開発技術者は結構多いのでは?と思い、このブログを書かせていただきました。

ミニマムで考えてみる

私の理解では、機械学習とは「データでアルゴリズムを削り出す」作業です。難しい話は他へ譲るとして、ミニマムな事例を考えてみましょう。

「1 + x = 2。 xに入る数字は?」

言うまでもなく、答えは1です。我々はすぐにわかります。しかし、「データでアルゴリズムを削りだす」ポリシーを貫くとすると、どうでしょうか。 「何かを足すが、それはいくつなのか?」というのが課題で、データとはすなわち「足す相手が1だった場合、足し算の結果は2になる。」という事前情報です。 この事前情報があれば、試行錯誤が可能になります。「10を足すのかな?」「0.5を足すのかな?」というトライアルが計画でき、このトライアルと事前情報を比較することで、「2であるべき足し算の結果が、12になってしまった。10多かった。」「同、1.5になってしまった。0.5少なかった。」という評価があり、評価を繰り返しフィードバックすることで、「答えは1」に近づくことができます。 少々くどいですが、機械学習のミニマムストーリーができました。

では、機械学習フレームワークであるTensorflowで、やってみましょう。 「やってみる」とはつまり、以下のような処理計画の実行のことです。

  1. この課題と事前情報をインプットして、
  2. トライアルと評価結果のフィードバックを繰り返し行うプログラムフローを記述し
  3. 実行して動作の過程と結果を観察すること。

プログラム開発の経験がある皆さんなら、プログラムの構想はすぐに立つでしょう。そう、終了条件を定めてループで回せばいいのですよ。

そしてですが、Tensorflowでこのミニマムストーリーを記述すると、こうなります。

     1   import tensorflow as tf
     2  
     3  x      = tf.constant(1.0)
     4  y_true = tf.constant(2.0)
     5  
     6  w      = tf.get_variable('w', shape=())
     7  y_pred = x + w
     8  
     9  loss      = tf.losses.mean_squared_error(labels=y_true, predictions=y_pred)
    10  optimizer = tf.train.GradientDescentOptimizer(0.1)
    11  train     = optimizer.minimize(loss)
    12  
    13  with tf.Session() as sess:
    14      sess.run(tf.global_variables_initializer())
    15      while True:
    16          _, loss_value, val_w = sess.run((train, loss, w))
    17          print('loss:{} w:{}'.format(
    18              str(loss_value),
    19              str(val_w)
    20          ))
    21          if loss_value < 0.01:
    22              print('END')
    23              break

​ 「わかるような、わからないような」と感じられる方も多いのではないでしょうか。私も今でこそ課題設定とコードの対応関係がはっきりわかりますが、初めはさっぱりわかりませんでした。疑問の中心は、「試行錯誤の過程では、事前情報の参照とトライアル結果の評価があり、想定値(この場合は1へ近づいていく数)の修正があるはずだが、唯一のループ処理(15行目〜)の中にはこれらを実行している気配がない」点です。そんなはずはありません。当然仕組まれています。

順番に見ていきましょう。最初の行は

3 x      = tf.constant(1.0)
4 y_true = tf.constant(2.0)

で、これは「事前情報のインプット」に相当する処理なのですが、この行の処理はプログラム処理言語の「代入処理」 ではありません。 実はこの行「x が参照された時、常に数値1.0を供給する約束をするが、その供給処理の実行はこの時点では保留」という意味を持っています(y_trueについても同様)。どうしてこんなに回りくどいことになっているのか。それはその後を見ていくと徐々にわかると思います。

いきなり?なことを言ってしまったかもしれませんが、次も結構敷居の高い内容です。

6 w = tf.get_variable('w', shape=())

get_variableだから、wは変数です。しかし、このget_variableで返されるw、 ただのプログラム言語の変数ではありません。オプティマイザが調整できる変数」なのです。オプティマイザについてはこのあと解説します。

そして次が、Tensorflowの中でもCoreと呼ばれる基本機能のひとつで、Tensorflowを理解する上での肝(と筆者が思う部分)です。

7 y_pred = x + w

この行は、「足してみる」というトライアル内容に相当する部分です。が、 ここで足し算が実行されるわけではなく、y_predに計算結果が代入されるわけでもありません。y_predに代入されるのは、上記の通り固定的に1.0が返されるxと、他から調整されるwとの足し算を「将来実行する約束」です。 「+」演算子Python言語の機能で再定義され、元々の算術演算子とは違うものになっているのです。

次の行は関数実行で、その役割はトライアル結果の数値化を「将来実行する約束」を返すことです。

9 loss = tf.losses.mean_squared_error(labels=y_true, predictions=y_pred)

「mean square」ですから、簡単に言えば、その数値化方法は正解とトライアル結果の距離の計測であり、引数もその通りの内容となっています。引数はいずれも「数字を出力する約束」のオブジェクトで、なので結果は必然的に、変数lossに代入されるのは、またしても「評価を将来実行する約束」です。

次の行はアルゴリズムの選択です。

10 optimizer = tf.train.GradientDescentOptimizer(0.1)

まぁ、よくあるまともなアルゴリズムですということで、これ以上は追求しないことにしたいと思います。

これまで「約束、約束」と連鎖してきましたが、次でようやく終着点です。 次の行は直前行で選択したアルゴリズム(GradientDescentOptimizer)で、「最適化」を「実行する約束を」するメソッド呼び出しです。

11 train = optimizer.minimize(loss)

ところで、何を最適化するのでしょうか?それは試しに足してみる数値「w」以外にありません。しかし、オブジェクトoptimizerは、変数wを参照できるのでしょうか?普通にプログラムを見ているとできないような気がしますが、それが可能なのです。 これまでつないできた「約束の連鎖」が、optimizerとxをつないでいるのです。 この部分がTensorflowのサンプルを読むときの一つの重要留意点と筆者は思っています。なんとなく代入が繰り返され、その終端変数だけポンと後処理に回されることが多いのですが、実はこの 終端変数には、これまで代入されてきた変数がすべて、背後で紐づけられているのです。

では、約束の連鎖を駆動するのはいつなのか?Session.runがそれにあたります。このメソッドの引数に列挙された「約束」が、紐解かれ、実行されるのです。 Session.runは2箇所で実行されています。

14       sess.run(tf.global_variables_initializer())
...
16          _, loss_value, val_w = sess.run((train, loss, w))

14行目はTensorflowで実際の計算処理をスタートする際のおまじないと思ってスキップしてください。 本丸は16行目です。ここで引数に渡されるのは train, loss, w で、いずれも何らかの計算を実行する「約束」です。 Session.runでは、その約束を実際に実行し、最後の計算結果を戻り値で出力します。 この場合

  • trainはopitimizer.minimizeを実行し、loss値を小さくするためにloss値に関する唯一の変数wを変更します。(戻り値は捨てられます)
  • lossは、この変更されたwでloss値を計算した結果を出力(loss_value)し、
  • wは、この変更されたw自身を、戻り値として出力(val_w)します。

そしてこの trainを繰り返し実行することで、変数wが徐々に調整され、つまり機械学習アルゴリズムが良くなっていく というのが、いわゆる「機械学習モデルのトレーニング」そのものなのです。

このコードを実行すると、結果は以下のようになります。(実際の数値はwの初期値によって異なり、この場合初期値はランダムですので、場合によって違います。)

loss:4.47416 w:-0.692177
loss:2.86346 w:-0.353741
loss:1.83262 w:-0.082993
loss:1.17287 w:0.133606
loss:0.750639 w:0.306885
loss:0.480409 w:0.445508
loss:0.307462 w:0.556406
loss:0.196776 w:0.645125
loss:0.125936 w:0.7161
loss:0.0805993 w:0.77288
loss:0.0515836 w:0.818304
loss:0.0330135 w:0.854643
loss:0.0211286 w:0.883715
loss:0.0135223 w:0.906972
loss:0.00865427 w:0.925577
END

wは、我々人間が一瞬で見抜いた答え「1」に徐々に近づき、十分(loss < 0.01)近づいたところでプログラム終了です。

これまでの過程で、Tensorflowが計算する約束を「一旦実行保留のまま配置」し、のちにそれらの約束を実行することで目的の計算処理を実行していることがおわかりいただけたと思います。実際には機械学習の課題はもっともっと複雑なのが普通ですし、実装も様々に高抽象度化されていますから、ここまでコードフローを細かく気にする必要は無いことが多いのが現実でしょう。しかしそれらの場合も、地下で動いている仕掛けの基本は同じはずです。この基本を押さえておくことで、目に見えているコードの実行順序をより的確に判断できるようになるのではないかと思います。

ところで、なぜこんな回りくどい実装になっているのでしょうか。筆者の考えでは、それは「実装を変更することなく分散処理環境へ移行するため」だと思います。機械学習モデルのトレーニングは、今日ではGPU上で実行することが一般的です。しかし小規模な試行錯誤はCPU上で実行されるでしょうから、これまで見てきたように計算の約束をTensorflowのオブジェクトの相互参照という形で一旦表現し、Session.runとなったところで適切な実行環境へそれぞれ「展開」できるようになったのでしょう。

最後まで読んでいただき、ありがとうございました。

Jenkins × AWS CodeBuild × GitHubで複数コンテナを利用したビルドを試してみた

こんにちは、Cariot事業部の遠藤です。
Salesforceブログに続き、クラウドblogでも初投稿になります。

f:id:flect-endo:20170630235508p:plain

今回は、Jenkins × AWS CodeBuild × GitHubを組み合わせて、CodeBuild上で複数コンテナを使ったビルドを実行する仕組みを試してみたので、紹介します。
RDBMSやRedisなどの外部リソースを利用したテストを、CodeBuild上で走らせたい」「クリーンな実行環境でCI/CDを回したい」という方のヒントになれば幸いです。

それではどうぞ↓↓

続きを読む

de:code2017に行ってきました!〜前夜祭とDay1〜

こんにちは、なかやまです。

普段はSalesforceエンジニアですが、マイクロソフトの大きなイベントが有ると聞きつけ、参加させてもらいました!

このブログはde:code参加中にTwitter投稿していたメモをまとめたものになります。弊社からは5名で参加してきましたが、

Techな話は他メンバーに任せます。(`・ω・´)キリッ

では、de:codeの雰囲気をお楽しみください!

続きを読む

Dockerで即席HTTPSエンドポイント

エンジニアの佐藤です。こんにちは。

先日Java Springフレームワークで、とあるWebサーバをテストしていた時のことです。 ふと「HTTPSでテストしたい」と思い立ちました。

ところがです。

よく考えてみると筆者は、しばらくの間WebサーバーにHTTPSエンドポイントを設定した覚えがありません。今日ではWebサーバーがHTTPSを直受けすることはまれで、ロードバランサがHTTPSを受け、配下のWebサーバーはHTTPで機能実装というパターンがほとんどです。(このような時代の変化もあってか、近頃はググってもWebサーバーにHTTPSエンドポイントを設定するトピックは減ってきたように思います。) 今回の案件もそうでしたので、ならばできるだけ小さい作業負担で、HTTPSを受けてHTTPにしてくれる仕掛けを設定…と考えてDockerを活用することを思いつきました。今回はこのトピックをお話したいと思います。

要するにHTTPSでリクエストを受け、HTTPのパス全体(/)をHTTPで別サイトに投げるWebサーバー、つまりリーバスプロキシサーバー(リバプロ) なら良いのです。 そこで筆者の頭に思い浮かんだのは、nginxです。(そう言えばこれに近いGoogle Cloud Platform のCloud Endpoint は、nginxベースだと書いてありました。)

Dockerコンテナは…と思うと、ありました。同じことを考える人はたくさんいたようです。 https://hub.docker.com/_/nginx/

読み進めていくと、まさに今回の用途にピッタリの使い方が書いてあります。設定ファイルを用意するだけで、コマンド一発で即スタートというありがたさです。

$ docker run -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx 

あとはオレオレ証明書と設定ファイルです。オレオレ証明書の作成は割愛しますが、設定ファイルnginx.confは以下のようにしました。

http {
    server {
        listen 443;

        ssl on;
        ssl_certificate /etc/secrets/proxycert;
        ssl_certificate_key /etc/secrets/proxykey;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-RSA-RC4-SHA:AES128-GCM-SHA256:HIGH:!RC4:!MD5:!aNULL:!EDH:!CAMELLIA;
        ssl_prefer_server_ciphers on;
        
        ssl_session_cache shared:SSL:10m;
        
        location / {
            proxy_pass              http://localhost:8080/; 
        }
    }
}

events {
    
}

証明書と秘密キーをマッピングするオプションを追加した以下のコマンドでスタート。(ここでは「—network host」スイッチで、コンテナをホストのネットワークに直付けします。)

$ docker run -d --network host -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro -v /host/path/cert.pem:/etc/secrets/proxycert:ro -v /host/path/key_nopass.pem:/etc/secrets/proxykey:ro nginx

開発中のWebサーバーをlocalhost:8080で待機させてからhttps://localhostにアクセスすると…できました! f:id:masashi-sato-flect:20170520131643p:plain

yumやaptでインストールする場合と比べてどうか

Nginxはyumやaptといったパッケージマネージャでも容易にインストールできます。設定の手間はというと、実はほとんど同じです。 ではDockerを使うメリットは何かというと、「まるごと捨てられる」という点だと思います。筆者はローカルマシンのアプリが増えるのを好みません。Dockerコンテナなら、ホスト環境を汚染しませんし、問題があったらコンテナ削除で完全にリセットできます。

開発環境ではELBの代わりに

ELBはAWSが提供するロードバランサで、無料証明書が使えたりして便利なのですが、そこそこの費用がかかります。本番環境ならELBが適当でしょうが、開発用ではそこまでの設備は不要です。どこかに間借りして今回のnginxコンテナを上げれば十分でしょう。