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

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

きゅうり画像で回帰問題を解いてみた

みなさんこんにちは。技術開発室の岡田です。

前回の投稿では、AWS re:invent2019のレポートをしました。 いやー、楽しいイベントだったなー。来年も行きたいなー(と書き続けたら行かせてもらえないだろうか。ちなみに増員はしてもらえそうな雰囲気だった。)

cloud.flect.co.jp

さて、今回から私の担当している機械学習に関する投稿に戻りますが、今回は「画像から回帰問題を解く」をテーマにしたいと思います。

はじめに

画像を用いた機械学習といえば分類問題(Classification)が一般的ですが、実は回帰問題(Regression)を解くこともできます。 有名なところでは「顔の画像から年齢を推定する」というものが有ります。こちらの記事で詳しく紹介されている方がおられますので、ご参照ください。

FLECTでも同様の技術を用いたソリューションを提供しています。 詳しくは述べられないのですが、ざっくり一例をご紹介しますと、下記の図はとある工業製品の摩耗状態を画像から判定するモデルになります。 この工業製品の摩耗状態はとある指標で定量的に10段階で計測できるのですが、図の横軸が計測した正しい摩耗状態、縦軸が画像から推定した摩耗状態となります。かなりしっかりと推定ができているように見えると思います。

f:id:Wok:20191223195757p:plain

上図ではテストサンプルの件数が多くわかりにくいですが、推定した摩耗状態と実際の摩耗状態の誤差をバイオリンチャートで表現すると次の図のようになっています。 縦軸が0を中心に摩耗状態の段階誤差の範囲を示しています。ほぼ1段階以内の誤差で推定することができています。 (自画自賛ですがすごいと思ってる!興味のある方はご連絡を。)

f:id:Wok:20191223195822p:plain

この回帰の技術に関連して、ちょっとした実験をしてみましたのでご紹介します。

実験の動機

まず、実験をしてみた動機からご説明します。

みなさんもGoogleなどで検索していただくとわかると思いますが、画像で回帰問題を解くことについてWeb上にはそれほど多くの情報がありません。大体が上記ブログと同様に年齢推定の話です。 FLECTでも回帰問題を扱うことになりブログで情報発信をしようと思ってその時にその理由がわかったのですが、業務上扱っているデータは当然あるのですがブログで扱えるような公のデータがないのです(私の観測範囲では)。 なので、実験的に何かをしてブログを書くというのが難しかったのではないかと考えています。

で、我々も同じ理由でだいぶ長い間ブログを書けずにいたのですが、あるとき、ぱっと、あることを思い出しました。 そういえば、昔、きゅうりの仕分け(ランクづけ)を画像分類でやっていた人がいたなぁ、と。

それからずっと、このきゅうりのランクを回帰で推定できないか?というのが頭から離れずにいたのですが、この年末幸運にも少し時間が空いたので早速実験をしてみた、ということになります。

ということで、以下、きゅうりのランクを回帰で推定する話をつらつらと書いていきます。 なお、今回用いたソースコード(notebook)は文末のgit repositoryに格納しておきます。

きゅうりの仕分けとデータセット

ご存知の方も多いと思いますが、自動車部品メーカーに務められていた方が退職して、Deep Learningを用いてきゅうりの仕分けをするシステムを作りました。 当時だいぶバズって、Googleのブログにも特集されています。詳細はこの記事を見ていただくのが良いかと思います。

cloudplatform-jp.googleblog.com

この仕分けシステムでは、2LからCまで、きゅうりを9つのランクに仕分けることができるそうです。 そして、この学習用のデータはなんとgitで公開されているのです(Creative Commons)。ありがたい!

github.com

今回は、この中でもprototype_2の学習用のデータを用いて実験を行いました。 なお、このprototype_2は上下側面の3方向からの撮影したデータとなっていますが、今回は上から撮影したもののみを用いることにしました。 また、画像の中には、手が写り込んでいたりする写真などが含まれていますが、これらは事前に取り除きました(OTHERラベルがついている)。

一応、分布を確認するとこんな感じに各クラス800枚前後でまんべんなく格納されています。ちなみに横軸の数値は2L〜Cまでのランクに対応づいています。縦軸は画像数。

f:id:Wok:20191220152417p:plain

なお、本データはCIFAR10の形式で格納されていますので、読み出し方はCIFAR10のサイトかgit repositoryを参考にしてください。

ネットワーク

今回はresnet v2を用いてやってみました。ポイントはFlatten()のあとにsoftmaxではなくreluを活性化関数とした全結合層を置くことです。 また、今回はCIFAR10と同じ画像なので、Inputのサイズは32x32x3になります。

    <略>
    x = AveragePooling2D(pool_size=8)(x)
    x = Flatten()(x)
    x = Dense(8, activation='relu')(x)
    outputs = Dense(1)(x)

モデル全体は次のような感じです。 なお、ある程度エイヤで作っているネットワークなので、チューニングの余地は有りますので、興味のある方は適当にいじってみるといいと思います。

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_8 (InputLayer)            (None, 32, 32, 3)    0                                            
__________________________________________________________________________________________________
conv2d_218 (Conv2D)             (None, 32, 32, 16)   448         input_8[0][0]                    
__________________________________________________________________________________________________
batch_normalization_197 (BatchN (None, 32, 32, 16)   64          conv2d_218[0][0]                 
__________________________________________________________________________________________________
activation_197 (Activation)     (None, 32, 32, 16)   0           batch_normalization_197[0][0]    
__________________________________________________________________________________________________
conv2d_219 (Conv2D)             (None, 32, 32, 16)   272         activation_197[0][0]             
__________________________________________________________________________________________________
batch_normalization_198 (BatchN (None, 32, 32, 16)   64          conv2d_219[0][0]                 
__________________________________________________________________________________________________
activation_198 (Activation)     (None, 32, 32, 16)   0           batch_normalization_198[0][0]    
__________________________________________________________________________________________________
conv2d_220 (Conv2D)             (None, 32, 32, 16)   2320        activation_198[0][0]             
__________________________________________________________________________________________________
batch_normalization_199 (BatchN (None, 32, 32, 16)   64          conv2d_220[0][0]                 
__________________________________________________________________________________________________
activation_199 (Activation)     (None, 32, 32, 16)   0           batch_normalization_199[0][0]    
__________________________________________________________________________________________________
conv2d_222 (Conv2D)             (None, 32, 32, 64)   1088        activation_197[0][0]             
__________________________________________________________________________________________________
conv2d_221 (Conv2D)             (None, 32, 32, 64)   1088        activation_199[0][0]             
__________________________________________________________________________________________________
add_64 (Add)                    (None, 32, 32, 64)   0           conv2d_222[0][0]                 
                                                                 conv2d_221[0][0]                 
__________________________________________________________________________________________________
batch_normalization_200 (BatchN (None, 32, 32, 64)   256         add_64[0][0]                     
__________________________________________________________________________________________________
activation_200 (Activation)     (None, 32, 32, 64)   0           batch_normalization_200[0][0]    
__________________________________________________________________________________________________
conv2d_223 (Conv2D)             (None, 32, 32, 16)   1040        activation_200[0][0]             
__________________________________________________________________________________________________
batch_normalization_201 (BatchN (None, 32, 32, 16)   64          conv2d_223[0][0]                 
__________________________________________________________________________________________________
activation_201 (Activation)     (None, 32, 32, 16)   0           batch_normalization_201[0][0]    
__________________________________________________________________________________________________
conv2d_224 (Conv2D)             (None, 32, 32, 16)   2320        activation_201[0][0]             
__________________________________________________________________________________________________
batch_normalization_202 (BatchN (None, 32, 32, 16)   64          conv2d_224[0][0]                 
__________________________________________________________________________________________________
activation_202 (Activation)     (None, 32, 32, 16)   0           batch_normalization_202[0][0]    
__________________________________________________________________________________________________
conv2d_225 (Conv2D)             (None, 32, 32, 64)   1088        activation_202[0][0]             
__________________________________________________________________________________________________
add_65 (Add)                    (None, 32, 32, 64)   0           add_64[0][0]                     
                                                                 conv2d_225[0][0]                 
__________________________________________________________________________________________________
batch_normalization_203 (BatchN (None, 32, 32, 64)   256         add_65[0][0]                     
__________________________________________________________________________________________________
activation_203 (Activation)     (None, 32, 32, 64)   0           batch_normalization_203[0][0]    
__________________________________________________________________________________________________
conv2d_226 (Conv2D)             (None, 32, 32, 16)   1040        activation_203[0][0]             
__________________________________________________________________________________________________
batch_normalization_204 (BatchN (None, 32, 32, 16)   64          conv2d_226[0][0]                 
__________________________________________________________________________________________________
activation_204 (Activation)     (None, 32, 32, 16)   0           batch_normalization_204[0][0]    
__________________________________________________________________________________________________
conv2d_227 (Conv2D)             (None, 32, 32, 16)   2320        activation_204[0][0]             
__________________________________________________________________________________________________
batch_normalization_205 (BatchN (None, 32, 32, 16)   64          conv2d_227[0][0]                 
__________________________________________________________________________________________________
activation_205 (Activation)     (None, 32, 32, 16)   0           batch_normalization_205[0][0]    
__________________________________________________________________________________________________
conv2d_228 (Conv2D)             (None, 32, 32, 64)   1088        activation_205[0][0]             
__________________________________________________________________________________________________
add_66 (Add)                    (None, 32, 32, 64)   0           add_65[0][0]                     
                                                                 conv2d_228[0][0]                 
__________________________________________________________________________________________________
batch_normalization_206 (BatchN (None, 32, 32, 64)   256         add_66[0][0]                     
__________________________________________________________________________________________________
activation_206 (Activation)     (None, 32, 32, 64)   0           batch_normalization_206[0][0]    
__________________________________________________________________________________________________
conv2d_229 (Conv2D)             (None, 16, 16, 64)   4160        activation_206[0][0]             
__________________________________________________________________________________________________
batch_normalization_207 (BatchN (None, 16, 16, 64)   256         conv2d_229[0][0]                 
__________________________________________________________________________________________________
activation_207 (Activation)     (None, 16, 16, 64)   0           batch_normalization_207[0][0]    
__________________________________________________________________________________________________
conv2d_230 (Conv2D)             (None, 16, 16, 64)   36928       activation_207[0][0]             
__________________________________________________________________________________________________
batch_normalization_208 (BatchN (None, 16, 16, 64)   256         conv2d_230[0][0]                 
__________________________________________________________________________________________________
activation_208 (Activation)     (None, 16, 16, 64)   0           batch_normalization_208[0][0]    
__________________________________________________________________________________________________
conv2d_232 (Conv2D)             (None, 16, 16, 128)  8320        add_66[0][0]                     
__________________________________________________________________________________________________
conv2d_231 (Conv2D)             (None, 16, 16, 128)  8320        activation_208[0][0]             
__________________________________________________________________________________________________
add_67 (Add)                    (None, 16, 16, 128)  0           conv2d_232[0][0]                 
                                                                 conv2d_231[0][0]                 
__________________________________________________________________________________________________
batch_normalization_209 (BatchN (None, 16, 16, 128)  512         add_67[0][0]                     
__________________________________________________________________________________________________
activation_209 (Activation)     (None, 16, 16, 128)  0           batch_normalization_209[0][0]    
__________________________________________________________________________________________________
conv2d_233 (Conv2D)             (None, 16, 16, 64)   8256        activation_209[0][0]             
__________________________________________________________________________________________________
batch_normalization_210 (BatchN (None, 16, 16, 64)   256         conv2d_233[0][0]                 
__________________________________________________________________________________________________
activation_210 (Activation)     (None, 16, 16, 64)   0           batch_normalization_210[0][0]    
__________________________________________________________________________________________________
conv2d_234 (Conv2D)             (None, 16, 16, 64)   36928       activation_210[0][0]             
__________________________________________________________________________________________________
batch_normalization_211 (BatchN (None, 16, 16, 64)   256         conv2d_234[0][0]                 
__________________________________________________________________________________________________
activation_211 (Activation)     (None, 16, 16, 64)   0           batch_normalization_211[0][0]    
__________________________________________________________________________________________________
conv2d_235 (Conv2D)             (None, 16, 16, 128)  8320        activation_211[0][0]             
__________________________________________________________________________________________________
add_68 (Add)                    (None, 16, 16, 128)  0           add_67[0][0]                     
                                                                 conv2d_235[0][0]                 
__________________________________________________________________________________________________
batch_normalization_212 (BatchN (None, 16, 16, 128)  512         add_68[0][0]                     
__________________________________________________________________________________________________
activation_212 (Activation)     (None, 16, 16, 128)  0           batch_normalization_212[0][0]    
__________________________________________________________________________________________________
conv2d_236 (Conv2D)             (None, 16, 16, 64)   8256        activation_212[0][0]             
__________________________________________________________________________________________________
batch_normalization_213 (BatchN (None, 16, 16, 64)   256         conv2d_236[0][0]                 
__________________________________________________________________________________________________
activation_213 (Activation)     (None, 16, 16, 64)   0           batch_normalization_213[0][0]    
__________________________________________________________________________________________________
conv2d_237 (Conv2D)             (None, 16, 16, 64)   36928       activation_213[0][0]             
__________________________________________________________________________________________________
batch_normalization_214 (BatchN (None, 16, 16, 64)   256         conv2d_237[0][0]                 
__________________________________________________________________________________________________
activation_214 (Activation)     (None, 16, 16, 64)   0           batch_normalization_214[0][0]    
__________________________________________________________________________________________________
conv2d_238 (Conv2D)             (None, 16, 16, 128)  8320        activation_214[0][0]             
__________________________________________________________________________________________________
add_69 (Add)                    (None, 16, 16, 128)  0           add_68[0][0]                     
                                                                 conv2d_238[0][0]                 
__________________________________________________________________________________________________
batch_normalization_215 (BatchN (None, 16, 16, 128)  512         add_69[0][0]                     
__________________________________________________________________________________________________
activation_215 (Activation)     (None, 16, 16, 128)  0           batch_normalization_215[0][0]    
__________________________________________________________________________________________________
conv2d_239 (Conv2D)             (None, 8, 8, 128)    16512       activation_215[0][0]             
__________________________________________________________________________________________________
batch_normalization_216 (BatchN (None, 8, 8, 128)    512         conv2d_239[0][0]                 
__________________________________________________________________________________________________
activation_216 (Activation)     (None, 8, 8, 128)    0           batch_normalization_216[0][0]    
__________________________________________________________________________________________________
conv2d_240 (Conv2D)             (None, 8, 8, 128)    147584      activation_216[0][0]             
__________________________________________________________________________________________________
batch_normalization_217 (BatchN (None, 8, 8, 128)    512         conv2d_240[0][0]                 
__________________________________________________________________________________________________
activation_217 (Activation)     (None, 8, 8, 128)    0           batch_normalization_217[0][0]    
__________________________________________________________________________________________________
conv2d_242 (Conv2D)             (None, 8, 8, 256)    33024       add_69[0][0]                     
__________________________________________________________________________________________________
conv2d_241 (Conv2D)             (None, 8, 8, 256)    33024       activation_217[0][0]             
__________________________________________________________________________________________________
add_70 (Add)                    (None, 8, 8, 256)    0           conv2d_242[0][0]                 
                                                                 conv2d_241[0][0]                 
__________________________________________________________________________________________________
batch_normalization_218 (BatchN (None, 8, 8, 256)    1024        add_70[0][0]                     
__________________________________________________________________________________________________
activation_218 (Activation)     (None, 8, 8, 256)    0           batch_normalization_218[0][0]    
__________________________________________________________________________________________________
conv2d_243 (Conv2D)             (None, 8, 8, 128)    32896       activation_218[0][0]             
__________________________________________________________________________________________________
batch_normalization_219 (BatchN (None, 8, 8, 128)    512         conv2d_243[0][0]                 
__________________________________________________________________________________________________
activation_219 (Activation)     (None, 8, 8, 128)    0           batch_normalization_219[0][0]    
__________________________________________________________________________________________________
conv2d_244 (Conv2D)             (None, 8, 8, 128)    147584      activation_219[0][0]             
__________________________________________________________________________________________________
batch_normalization_220 (BatchN (None, 8, 8, 128)    512         conv2d_244[0][0]                 
__________________________________________________________________________________________________
activation_220 (Activation)     (None, 8, 8, 128)    0           batch_normalization_220[0][0]    
__________________________________________________________________________________________________
conv2d_245 (Conv2D)             (None, 8, 8, 256)    33024       activation_220[0][0]             
__________________________________________________________________________________________________
add_71 (Add)                    (None, 8, 8, 256)    0           add_70[0][0]                     
                                                                 conv2d_245[0][0]                 
__________________________________________________________________________________________________
batch_normalization_221 (BatchN (None, 8, 8, 256)    1024        add_71[0][0]                     
__________________________________________________________________________________________________
activation_221 (Activation)     (None, 8, 8, 256)    0           batch_normalization_221[0][0]    
__________________________________________________________________________________________________
conv2d_246 (Conv2D)             (None, 8, 8, 128)    32896       activation_221[0][0]             
__________________________________________________________________________________________________
batch_normalization_222 (BatchN (None, 8, 8, 128)    512         conv2d_246[0][0]                 
__________________________________________________________________________________________________
activation_222 (Activation)     (None, 8, 8, 128)    0           batch_normalization_222[0][0]    
__________________________________________________________________________________________________
conv2d_247 (Conv2D)             (None, 8, 8, 128)    147584      activation_222[0][0]             
__________________________________________________________________________________________________
batch_normalization_223 (BatchN (None, 8, 8, 128)    512         conv2d_247[0][0]                 
__________________________________________________________________________________________________
activation_223 (Activation)     (None, 8, 8, 128)    0           batch_normalization_223[0][0]    
__________________________________________________________________________________________________
conv2d_248 (Conv2D)             (None, 8, 8, 256)    33024       activation_223[0][0]             
__________________________________________________________________________________________________
add_72 (Add)                    (None, 8, 8, 256)    0           add_71[0][0]                     
                                                                 conv2d_248[0][0]                 
__________________________________________________________________________________________________
batch_normalization_224 (BatchN (None, 8, 8, 256)    1024        add_72[0][0]                     
__________________________________________________________________________________________________
activation_224 (Activation)     (None, 8, 8, 256)    0           batch_normalization_224[0][0]    
__________________________________________________________________________________________________
average_pooling2d_8 (AveragePoo (None, 1, 1, 256)    0           activation_224[0][0]             
__________________________________________________________________________________________________
flatten_8 (Flatten)             (None, 256)          0           average_pooling2d_8[0][0]        
__________________________________________________________________________________________________
dense_15 (Dense)                (None, 8)            2056        flatten_8[0][0]                  
__________________________________________________________________________________________________
dense_16 (Dense)                (None, 1)            9           dense_15[0][0]                   
==================================================================================================
Total params: 848,497
Trainable params: 843,281
Non-trainable params: 5,216
__________________________________________________________________________________________________

レーニン

ネットワークを定義できたら、トレーニングを開始します。損失関数は今回はMSEにしています。 また、全サンプル数は7861で、これをTraining, Validation, Testでそれぞれ6:2:2に分割しています。 またこちらもエイヤですがエポック数は200にしています。トレーニングの結果は次の通り。 青線がlossでオレンジ線がval_lossです。大体130エポックで収束しているようですね。

f:id:Wok:20191220160258p:plain

評価

では、トレーニングしたモデルで評価してみましょう。 推定ランクをプロットした図です。なかなかうまく推定できているように見えます。 f:id:Wok:20191220160321p:plain

ただ、やはり、サンプル数が多いのでどの程度分散しているのかわかりにくいですね。 なので、こちらのバイオリンチャートを見ていただきましょう。 f:id:Wok:20191220160335p:plain

ご覧になっておわかりになると思いますが、ほぼ正しいランクの位置をてっぺんに正規分布していますね。 やや裾が重いところがあり、他のクラスと認識されてしまっているものが有るようですが、そこそこ正しく推定されたと見て良いと思います。 (作成者の方もおっしゃっていますが、人間がやっているのでランク付けもある程度えいやなところがあるらしいので。)

TensorFlowでディープラーニングによる『キュウリ』の仕分け | Workpiles

今回の場合は、各分類クラス間が離れていないため、人間がやっても“2LとL”や“LとBL”の判別は難しい(けっこう適当)だったりします。

といことで、今回の実験はひとまず成功したかなと思います。画像からの回帰、みなさんもチャレンジしてみてはいかがでしょうか。 結果をシェアしてもらえるとありがたいです。

しきい値調整

推定されたランクは浮動小数点となっていますが、実際のランクは整数値です。 この整数値に変換するためにしきい値を決める必要が有ります。 デフォルトで小数点第1位が5のところでしきい値にしてもいいですが、このしきい値をいい感じに調整してくれる方法も有ります。 今回は画像から回帰をすることろまでが主題となりますので、そこまでは踏み込みません。また機会があればご紹介します。

余談ですが、、、

resnet v2でクラス分類させたら、全然クラス分類のほうが精度が高かったです。 タスクによって解き方をしっかり使い分けることが重要です。この辺の話も機会があれば別途。

最後に

今回は、きゅうりの画像のランク付けを題材に、画像から回帰問題を解く方法についてご紹介しました。 最初にご紹介したとおり、画像から連続値を推定する技術は様々な分野に応用可能であり、FLECTでも実績があります。 ぜひご活用いただければありがたいと思います。

次回は、また別の技術をご紹介する予定です。ではでは。

リンク

github.com

HerokuでTensorFlowのAPIをホストした話

この記事は、Heroku Advent Calendar 201920日目の記事です。

19日目は@mttrsさんさんによる「Heroku Changelog 2019」でした。 21日目は@sizukutamagoさんです。

こんにちは、技術開発室の藤野です。 今回はHeroku上でTensorFlowの推論を行うWeb APIを作った話についてです。

はじめに

今回の話のあらましは、TensorFlowでディープラーニングのモデルを作ったので、Web APIとして使えるようにしたい、というものです。 APIはリクエストに対して推論結果を返すシンプルなもので、なるべく早く構築しデプロイや運用の手間は省きたいと思っていました。

これに対して、Herokuは下記の利点があります。

  • インフラ、ミドルウェアが用意されており、デプロイが容易
  • 運用のための仕組みがあらかじめ提供されている
  • CI/CDの機能を利用できる

一方で、ディープラーニングアルゴリズムGPU等を使って高速化することが一般的だと思います。 Herokuではこのようなアクセラレータは利用できず、CPUのみで推論処理を行うことになります。

そこで、今回はこのような推論処理を含むAPIをHeroku上で動かしたときのパフォーマンスはどのくらいなのか?を主眼に見ていきたいと思います。

続きを読む

CariotはNew RelicとAmazon EventBridgeにトラスト構想の夢を見る

こんにちは、遠藤@Cariot事業部です。
今回はCariot SREチームの一員として投稿します。

はじめに

フレクトが提供している車のIoTサービス「Cariot (Car+IoT)」では、先日New Relic社と共同でプレスリリースを発表し、サービスの性能に関する透明性を高めるべく、取り組みをはじめました。

www.flect.co.jp

わたしの所属するCariot事業部 製品開発部でも、上記、大きなビジョンの実現を目指しながら、少しずつできることを進めています。
今回は、現在進行中の取り組みについて紹介します。

続きを読む

Auth0の話

こんにちは。としやさんじゃない方の齋藤です。Advent Calendar の季節がやってきました。

qiita.com

HerokuのDynoやアドオンの構成は、定番がありつつもプロジェクトごとの微妙な色彩の違いにフィットさせる必要があり、いつも座組みに頭を悩ませるところでありますが、個人的にはカードゲームのデッキ構築みたいなものだと思っています。Creditぎりぎりまで強いカードを少しでも多く入れたい。

ずっと前から存在は知っていたAuth0にようやく触れる機会が取れたので、検討している方に少しでもヒントになればと思ってAuth0の紹介をしてみたいと思います。

Auth0とは

f:id:shns:20191217224440p:plain https://auth0.com/jp/

IDaaS/CIAMとして最もポピュラーなサービスの一つで、たとえば認証・認可、セキュリティにまつわる 面倒な実装をせず、強力な認証基盤をすぐさま自身のアプリに取り込むことができます。

Documentationにある Overview を見ると、

Auth0 provides authentication and authorization as a service.(Auth0は認証と認可をサービスとして提供します)

https://auth0.com/docs/getting-started/overview

とありますが、のみならず

  • ログイン履歴をダッシュボードで見れたり
  • 会員機能にまつわるメール機能は一通り備えていたり
  • 既存の認証基盤とつなげやすかったり

といったように、使いやすさや拡張性、開発者目線の機能が豊富です。

特徴

特にSDK、サンプルコード、対応するソーシャルログインなど、品揃えが圧倒的。

f:id:shns:20191217224856p:plain f:id:shns:20191217231118p:plain

それと今年はLINEやSign in with Appleが標準で提供されたりしました。

auth0.com auth0.com

また、個人的な印象としては、管理者向けのダッシュボードがとにかく始めやすいように作られているので、従来のエンタープライズ系SSO製品と比べて、0を1にする部分の苦しみから開発者が圧倒的に解き放たれる感があります。OpenAMの複雑な管理者画面の遷移の仕方を覚えたり、ちょっと設定を変えたいだけなのに/var/lib/tomcat/webapps界隈に足を運ばないと行けなかったり・・といった手間はないので大事なコードに集中できる感は圧倒的に勝っています。

引き算の美学

いつもシステム構成の下敷きとして定期的に見直しているpostに以下があります。

https://engineering.videoblocks.com/web-architecture-101-a3224e126947

上のポンチ絵には出てこないのですが、初学を終えてこの絵が分かる頃に ちょうど認証認可の仕組みの理解が必要になるタイミングがあると思うのですよね。

Javaでいうと、スレッドプログラミングができたら中級者みたいなそんな感じかな・・(個人の意見です)

現代のWebアプリにはログイン機能は当たり前のようにありますが、自前で作る場合は思ったよりも機能が爆発したり、OpenAMなどのオープンソースを使うにしても非機能要件を考慮した結果それなりにインフラ面が大掛かりになってしまったりと、トータルで見たときに手間が大きくなってしまうことがあります。

一方で、ID基盤が提供するところのログインというユーザ体験そのものとしては、

  • かなりの頻度で通る(実装次第ですが)
  • 複数経路があり、エントリーポイントが多い
  • 比較的早い回遊タイミングで到達されることが多い

といった点があり、最初から入れるにはそこそこ大玉で、かといって後から入れるには 全体への基盤改修リスクが大きい、といった結果として跳ね返ってきます。

コンバージョンを継続的に改善したいB2C系サービスや、 顧客情報は集めたいけど顧客のストレスを増やしたくないと考えているサービスにとって、 導線検討やセキュリティ面の考慮というのは避けて通れない。

こういった手間がかかるが逃げられない、といったジレンマが 強力な標準提供サービス(SDK、テンプレートコードなど)によって 抽象化ないしスキップできてしまうのがAuth0の良いところかなと。

CRMとの親和性

CIAMとして見て、やはりCRMとの親和性の高さがポイントの1つかなと思っています。

ユースケースとしては、認証・認可の仕組みとして利用しつつ、 Auth0ログイン時などにログをイベントとして受け取れるので、KafkaやAmazon Eventbridgeを配管して function codeやバッチ処理からCRM・MAに取り込み、ネイティブにプッシュ配信、など。

auth0.com

Log driven なので、セキュリティオートメーションでも重宝しますね。

あるいはCRM上のビジネスユーザは別のテナントなりアプリケーションで SAML FederatedにSSOしてWebアプリ側にも行き来しやすくする、といったアプローチも取れます。

Auth0の良いところは、Rulesを使って認証時の様々なフック処理を書けるところです。 こんな感じで。

f:id:shns:20191217225549p:plain

管理者のコンソールでコード片のデバッグが出来るのは地味に強い。それはそれとして、Auth0標準のカスタムデータベースとソーシャルログインのユーザをマージする前提でWebアプリを組むときや、外のサービスを呼びつつid_tokenに必要な情報を詰め込みたいとかログインのうち何回かは外部DBからの最新情報を取ってきてキャッシュしたいとか、色々なユースケースで活躍します。

主要なイベントについてはテンプレートがあるので、流用開発ベースで進めることができ、どれくらい手間がかかるのかの見立てが立てやすいところがポイント。

収集・集約・アプローチのタッチポイントをAddonインストール一発で作れるのは非常にありがたいところなのではないでしょうか。

Add-onsとしての利用

今年、yonyonさんとSWTTでHerokuの話をしたとき※には認証周りの話題はしなかったのですが、当然Herokuのアプリで認証込みでサービスを作るときにはAuth0が第一の選択肢になってきます。

見積から開発・運用まで!Herokuの基本とTips - Qiita

当たり前ですがHerokuのユーザで各ステージのアプリにシングルサインオンできるし、テナントのユーザ権限もHerokuユーザに付与できるので、ちょっと管理者ユーザとして他の人に見てほしいときに、Collaboratorとして招待できますし。

Add-onプランとしては以下があります。

https://elements.heroku.com/addons/auth0

  • Free
  • Silver
  • Gold
  • Enterprise B2C

AD連携やマルチデータソース戦略を取るわけでなければ、意外とキャップは少なくFreeで出来ることが多いです。なのでステージ構成としては、development,stagingはFree/Silverで、productionから GoldもしくはB2C Enterpriseという構成などでも良いかなと思います。

Planを考えるときの閾値については、MAU(Monthly Active Users)がベースとなります。つまり、月1回でもログインすればそのユーザは1カウントとして計算され、プランごとの上限値はそのMAUで決まります。

f:id:shns:20191217230432p:plain

この例で言うと、Enterprise B2C の 10,000 Usersに対して先月は2,500ユーザ。まだまだ余裕があります。

一応 Dashboardのトップでもログインユーザのサマリが確認できるのですが、各月のアクティブユーザについては以下から当月のQuotaを参照できます。

User Dropdown > Account Usage > Home > Quota Utilization > Regular Active Users

なお、Auth0でユーザ登録する(Database User)とソーシャルログインを併用しており、両者をマージルールで1ユーザとしてマージした場合には、どちらのログインを利用してもMAUは1カウントになります。逆になると別カウントなので、マージ可否は要件として決めておいた方が良いです。

リミットを超えてきそうな場合にはプランの引き上げ(再検討)が必要になりますが、最初からGold以上で行くなら最初にキャパシティ設計をしっかりやっておきます。だいたいHerokuはスモールスタートもしくは3-5年くらいを目安に非機能設計しておくのが多いと思いますが、予想よりサービスがうまくいくと、それでも不足するケースもあります。

しかし、重要なのはビジネス成功に伴ってPlanを拡張するというポジティブな拡張。

とはいえ、RFMっぽくlast_loginが一定以上な「休眠ユーザ向け」にキャンペーンを打って月のログインがスパイクするようなケースなど、予期しきれないユーザ激増などもあると思うので、適度に流量と容量は見ておいた方が良いです。

モニタリングの選択肢としては、シンプルにダッシュボードを見るようにするのが手っ取り早いですが、

  • Log->Amazon Eventbridge IntegrationからSQSに貯めて、一定量キューが溜まったらMAU計算して通知するfunctionを書く
  • 1-off Dyno + Management APIで定期的に以下略

あたりができると良いと思います。思いついただけで作ったわけではないけど・・・

Freeで出来ることが非常に多いアドオンなので、とりあえず使ってみてから考えればよいかなと思います。

小西さんの書いた弊社過去ブログやサンプルサイトにも載っているので、ご参考までに。

ちょっとだけ気になるところ

ドキュメントやリファレンスはオープンかつそれなりに充実しているものの、使ってみて多少癖のあるところや認証基盤ならではの沼っぽいハマり方をするケースも時々あります。

よくあるトラブルシューティングやベストプラクティスがわかるようなナレッジベースが充実するとより良いな・・・と思ってます。

とはいえこれだけの機能をそれなりに小ささでも使うことができるのはやっぱりサービスとしての力があるのだと思いますし、こまめなアップデートも結構好きなので、たぶん来年も追っかけていくとは思います。それでは。

Image processing algorithm - Index -

技術開発室の馮 志聖(マイク)です。

I will list some image processing algorithm. And I will introduce detail from next time. It is important for understand how computer know the real world. Computer vision include acquiring, processing, analyzing and understanding digital images.

Edge detection

  • Canny
    f:id:fengchihsheng:20191213145831p:plain
    Canny
  • Sobel
    f:id:fengchihsheng:20191213145906p:plain
    Sobel X
    f:id:fengchihsheng:20191213145926p:plain
    Sobel Y
    f:id:fengchihsheng:20191213145948p:plain
    Sobel
  • Prewitt
    f:id:fengchihsheng:20191213150011p:plain
    Prewitt
  • Laplacian
    f:id:fengchihsheng:20191213150037p:plain
    Laplacian
  • Laplacian of Gaussian
    f:id:fengchihsheng:20191213150057p:plain
    Laplacian of Gaussian

Edge detection (RGB)

f:id:fengchihsheng:20191216101605p:plain
Edge detection (RGB)
f:id:fengchihsheng:20191216101626p:plain
Edge detection (RGB)
f:id:fengchihsheng:20191216101646p:plain
Edge detection (RGB)

Background subtraction

  • Differential
    f:id:fengchihsheng:20191213150431p:plain
    Differential
  • Otsu
    f:id:fengchihsheng:20191213150453p:plain
    Otsu
  • Binary
    f:id:fengchihsheng:20191213150512p:plain
    Binary
  • Adaptive Mean Thresholding
    f:id:fengchihsheng:20191213150532p:plain
    Adaptive Mean Thresholding
  • Adaptive Gaussian Thresholding
    f:id:fengchihsheng:20191213150546p:plain
    Adaptive Gaussian Thresholding

Corner detection

  • Harris operator
    f:id:fengchihsheng:20191213154003p:plain
    Harris operator
  • Shi and Tomasi
    f:id:fengchihsheng:20191213154026p:plain
    Shi and Tomasi
  • Features from accelerated segment test (FAST)
    f:id:fengchihsheng:20191213163337p:plain
    Features from accelerated segment test (FAST)

Blob detection

  • Laplacian of Gaussian (LoG)
    f:id:fengchihsheng:20191213173044p:plain
    Laplacian of Gaussian (LoG)
  • Difference of Gaussians (DoG)
    f:id:fengchihsheng:20191213173106p:plain
    Difference of Gaussians (DoG)
  • Determinant of Hessian (DoH)
    f:id:fengchihsheng:20191213173125p:plain
    Determinant of Hessian (DoH)
  • Maximally stable extremal regions (MSER)
    f:id:fengchihsheng:20191213180130p:plain
    Maximally stable extremal regions (MSER)
  • Optical character recognition (OCR)
    f:id:fengchihsheng:20191213182619p:plain
    Optical character recognition (OCR)

Ridge detection

  • Hessian matrix
    f:id:fengchihsheng:20191216095911p:plain
    Hessian matrix

Hough transform

  • Generalized Hough transform
    f:id:fengchihsheng:20191216113113p:plain
    Generalized Hough transform
  • Hough transform
    f:id:fengchihsheng:20191216115716p:plain
    Hough transform
  • Line Segment Detector (LSD)
    f:id:fengchihsheng:20191216120038p:plain
    Line Segment Detector (LSD)
  • Fast Line Detector (FLD)
    f:id:fengchihsheng:20191216120057p:plain
    Fast Line Detector (FLD)

Structure tensor

  • Gradient Structure tensor (GST)
    f:id:fengchihsheng:20191216145635p:plain
    Gradient Structure tensor (GST)

Feature description

  • Oriented FAST and rotated BRIEF (ORB)
    f:id:fengchihsheng:20191217105404p:plain
    Oriented FAST and rotated BRIEF (ORB)
  • AgastFeatureDetector
    f:id:fengchihsheng:20191217112104p:plain
    AgastFeatureDetector
  • Features from accelerated segment test (FAST)
    f:id:fengchihsheng:20191217112133p:plain
    Features from accelerated segment test (FAST)
  • Maximally stable extremal regions (MSER)
    f:id:fengchihsheng:20191217112200p:plain
    Maximally stable extremal regions (MSER)
  • KAZE Features
    f:id:fengchihsheng:20191217112231p:plain
    KAZE Features
  • Accelerated-Kaze Features (A-Kaze)
    f:id:fengchihsheng:20191217105341p:plain
    Accelerated-Kaze Features (A-Kaze)
  • Binary Robust Invariant Scalable Keypoints (BRISK)
    f:id:fengchihsheng:20191217105322p:plain
    Binary Robust Invariant Scalable Keypoints (BRISK)
  • Scale-invariant feature transform (SIFT)
    f:id:fengchihsheng:20191217105256p:plain
    Scale-invariant feature transform (SIFT)
  • Speeded up robust features (SURF)
    f:id:fengchihsheng:20191217112304p:plain
    Speeded up robust features (SURF)
  • Histogram of oriented gradients (HOG)
    f:id:fengchihsheng:20191216151117p:plain
    Histogram of oriented gradients (HOG)
  • Local Binary Pattern (LBP)
    f:id:fengchihsheng:20191217132849p:plain
    Local Binary Pattern (LBP)
  • Grey Level Co-occurrence Matrices (GLCM)
    f:id:fengchihsheng:20191216150811p:plain
    GLCM black space

Signal processing

  • Fast Fourier transform (FFT)
    f:id:fengchihsheng:20191217140122p:plain
    Fast Fourier transform (FFT) Magnitude Spectrum
    f:id:fengchihsheng:20191217140431p:plain
    High Pass Filtering
  • Discrete Fourier transform (DFT)
    f:id:fengchihsheng:20191217140451p:plain
    Discrete Fourier transform (DFT) Magnitude Spectrum
    f:id:fengchihsheng:20191217140755p:plain
    Low Pass Filtering

Shape detection

  • Morphological Snakes
    f:id:fengchihsheng:20191217132032p:plain
    Morphological Active Contours without Edges (MorphACWE)
    f:id:fengchihsheng:20191217132107p:plain
    Morphological Geodesic Active Contours (MorphGAC)

最後に

I will update more and more algorithm in this page. If I add new blog for detail algorithm, I will also update here too.

Face Recognition on JavaScript

技術開発室の馮 志聖(マイク)です。

前回の投稿。 cloud.flect.co.jp

I will introduce face recognition on javascript. And some algorithm for feature detection. Recently,JavaScript engines and browsers have become more powerful that building full-blown applications in JavaScript is not only feasible, but increasingly popular.

Feature detection

In computer vision and image processing feature detection includes methods for computing abstractions of image information and making local decisions at every image point whether there is an image feature of a given type at that point or not. The resulting features will be subsets of the image domain, often in the form of isolated points, continuous curves or connected regions.

Machine learning

  • Neural Structured Learning (NSL)
    f:id:fengchihsheng:20191216142248p:plain
    Neural Structured Learning (NSL)

https://www.tensorflow.org/neural_structured_learning

Google Tensorflow Blog have some content about "Neural Structured Learning in TensorFlow".

https://medium.com/tensorflow/introducing-neural-structured-learning-in-tensorflow-5a802efd7afd

Visualizing ML training using TensorFlow.js and Baseball data.

observablehq.com

https://medium.com/tensorflow/predicting-balls-and-strikes-using-tensorflow-js-2acf1d7a447c

Face Detection & Recognition

Check this video from Youtube and see how Haar working.

www.youtube.com

face-api.js

Face-api.js is a JavaScript API for face detection and face recognition in the browser implemented on top of the tensorflow.js core API. It implements a series of convolutional neural networks (CNNs), optimized for the web and for mobile devices. face-api.js implements the models SSD Mobilenet V1, Tiny Face Detector, and the experimental MTCNN.

f:id:fengchihsheng:20191217143524j:plain
Face future
https://www.sciencedirect.com/science/article/pii/S1077314215000727

f:id:fengchihsheng:20191217143806p:plain
Facial Landmark
https://www.semanticscholar.org/paper/Facial-Landmark-Tracking-by-Tree-Based-Deformable-Uric%C3%A1r-Franc/eb97fabbe07999fe799f352dedf62acc7a65b6f0

DEMO

  • Detect Feeling
    f:id:fengchihsheng:20191217150424p:plain
    Neutral
    f:id:fengchihsheng:20191217150455p:plain
    Happy
    f:id:fengchihsheng:20191217150518p:plain
    Sad
    f:id:fengchihsheng:20191217150554p:plain
    Surprised
  • Verification
    f:id:fengchihsheng:20191217150627p:plain
    Verified
    f:id:fengchihsheng:20191217150809p:plain
    Unverified
  • Video Human emotions detection


Human emotions detection

Web verification base on face recognition


Web verification base on face recognition

Some idea for using case

f:id:fengchihsheng:20191212163919p:plain
Realtime face verification on Website

最後に

Face recognition can use on many verification case.

Reference

Feature detection
Face Recognition
JS library

AWS Step Function をユニットテストするには

エンジニアの佐藤です。こんにちは。今回は「AWS Step Function のユニットテスト」を構想し、実装までに直面した課題と私が考えた解決方法についてお話ししたいと思います。

AWS Step Functionとは

公式ページに書かれてますが、 Step Functions は様々なサービスをつなげて「サーバーレス・ワークフロー」を編成する仕掛けです。 AWS には古来から様々なワークフロー支援サービスがありますが、その中では最も新しいものです。

ことの始まり

筆者はある日、とある業務システムをどうやって実装したものかと考えていました。AWSでやってくれという話でしたので、AWSの各種サービスを比較検討し、最終的にStep Functionsを選択しました。重視したポイントは以下のようなものです。

  1. 拡張性が高い。
  2. 高可用性設計。
  3. ワークフローの進捗が視覚的に確認できる。
  4. ワークフロー定義が読みやすい。

今回の業務システムには短時間で完了する軽いタスクと、リソースも時間も必要な重いタスクの2種類があり、筆者は前者をLambda Functionに、後者をECS(Fargate)で実行することにしました。

Step Functionのユニットテスト・・・って?

ところで昨今のシステム開発は、テストコードを記述してユニットテストを行うのが一般的です。筆者はこのStep Functionsで作成されるワークフロー設定(ステートマシン)についてのユニットテストを構想しました。ところが、考えていくうちに、これが結構深いタスクであることに、徐々に気がついてきました。一般的な話ですが、システムを人間の体に例えると、手足に相当する機能のユニットテストについては、準備作業はそれほど難しくありません。一方で中枢的な機能のテストのための準備作業は大きくなりがちです。動作が確実な仮の末端部品を接続してあげる必要があるからです。「ワークフロー管理」機能は、この観点から言うとシステムの最も中枢的な部分であり、その単体テストのためには仮の末端部品の接続が必要ということになります。今回で言えば、Lambda FunctionとECSに仮のモジュールを入れてあげる必要がある。

Lambda FunctionとECSの仮のモジュール、と構想したところで、筆者は更に寒気を覚えました。よく考えてみるとこの2つ、機能コードを配置するまでの作業がそれなりに手間なのです。例えばLambda Functionの場合、以下のようなステップがあります。

  1. ライブラリを含めた機能コードのアーカイブ
  2. アーカイブのS3への配置
  3. IAMの設定
  4. Lambda Functionの設定

ECSの場合はこうなります。

  1. 機能コードを含むDockerコンテナのビルド
  2. ECRへコンテナを配置
  3. IAMの設定
  4. タスク定義の作成

この「Lambda Functionの設定」と「タスク定義の作成」には、環境変数などの設定も含まれます。テスト用の仮のモジュールなどと言いながら、一式全部必要なわけです。ステートマシンのテストをするために「システムの設計方針に従って各種の設定を発行・注入する仕掛け」の開発が必要になる・・・そう気がついた筆者は、とんでもないことを始めてしまったのではないかと、暗い気持ちになりました。中でも筆者が負担に感じたのがIAMです。Lambda FunctionもECSも機能コードの実行環境であり、実行の際に「インフラ的にできること(実行権限)」の範囲をIAMロールとして定義する必要があります。しかしこれがまた、慎重さと地道な努力を要する作業なのです。「とりあえずAdmin!」とやっつけたい気持ちはやまやまですが、これは悪魔の囁き。権限設定はミニマムからスタートしないと、権限のダイエットはとても大変なのです。

なお、Lambda FunctionとStep Functionsについては、AWSがローカルランタイムを配布していますが、今回は利用を見送りました。ローカル版のFargateはありませんし、ローカル環境固有の設定負担や互換性問題もあるでしょう。それなら最初から本物を使ったほうがいい、ユニットテスト用のAWSアカウントを短時間使うだけならそんなに経費もかからない、と判断しました。

各種設定の発行・注入する仕掛け・・・って?

で、その仕掛けをどうしたのかというと、この点は大変悩んだところでした。

筆者が最初に検討したのはAWS Cloud Formationでした。しかし調べていくと、これはこれで良くないところがあると思いました。まず引っかかったのが、テンプレートの入出力がいずれもリストとなっており、階層的にできない点です。また、最終的にJSONYAML形式のテンプレートにする必要があり、いくつか見て回った範囲ではこのテンプレートの調整負担が大きく見え、Colud Formationは見送りました。

次に検討したのがCDKでした。re:Invent18で発表されたもので、基本的にNodeJSやPythonなどのプログラム言語でCloud Formationのテンプレートを効率的に作成する仕掛けです。しかしこちらはStep Functionsの実装がまだプレビュー状態で、またステートマシンの定義方法が独特で良くないと思いました。また、ランダムな付加文字列の入ったリソース名となる点も、やり込むと散らかって良くないだろうなと思えました。

筆者が最終的に選択したのは、以下のような方式でした。

  1. まずプロジェクト名に連なる一連の名前体系を決め、これを環境変数で設定する。
  2. Python言語でdeploy.pyとundeploy.pyを記述し、ここからboto3 SDKを用いて直接リソースの作成と削除を行う。リソース定義は、これらのソースコードに記述する。
  3. ユニットテストはdeploy.py実行 -> テスト -> undeploy.py実行のフローで行う。

幸いなことに今回のシステムの開発言語はPythonでした。そしてAWSリソースの定義は、たいていJSONで書かれており、Python言語のdictionaryとほとんど同義(しかもコメントも挿入できる)。そして複数のリソース定義を体系的に作っていくにはプログラムフローが効率的で、デプロイ自体もその延長でSDKを呼び出してやってしまえば、小さくまとまらないかと目論んだわけです。

最初に「プロジェクト名に連なる一連の名前体系を決める」ですが、今回構想した「Step FunctionsステートマシンがLambda FunctionとECSコンテナを実行する」実装の場合、以下のようになりました。

export AWS_ACCOUNT_ID=`aws sts get-caller-identity --output json | jq -r -j ".Account"`
export AWS_DEFAULT_REGION=`aws configure get default.region | tr -d \\n`
export PROJECT_NAME=project01

export LAMBDA_FUNCTION_NAME="${PROJECT_NAME}_lambda"
export LAMBDA_FUNCTION_ARCHIVE_NAME="${FUNCTION_NAME}.zip"
export S3_LAMBDA_ARCHIVE_PATH="${PROJECT_NAME}/${FUNCTION_ARCHIVE_NAME}"
export BUCKET_NAME=s3_bucket_name12345s
export LAMBDA_POLICY_NAME="${PROJECT_NAME}LambdaPolicy"
export LAMBDA_ROLE_NAME="${PROJECT_NAME}LambdaRole"
export LAMBDA_FUNCTION_ARN="arn:aws:lambda:${AWS_DEFAULT_REGION}:${AWS_ACCOUNT_ID}:function:${LAMBDA_FUNCTION_NAME}:prd"

export ECR_REPOSITORY_NAME=$PROJECT_NAME
export ECS_CLUSTER_NAME=$PROJECT_NAME
export ECS_CONTAINER_TAG="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${ECR_REPOSITORY_NAME}:latest"
export ECS_TASK_ROLE_NAME="${PROJECT_NAME}TaskRole"
export ECS_TASK_POLICY_NAME="${PROJECT_NAME}TaskPolicy"
export ECS_TASK_EXECUTION_ROLE_NAME="${PROJECT_NAME}TaskExecutionRole"
export ECS_TASK_EXECUTION_POLICY_NAME="${PROJECT_NAME}TaskExecutionPolicy"
export ECS_TASK_DEFINITION_NAME="${PROJECT_NAME}Task"
export ECS_SUBNETS="subnet-12345678"

export SFN_NAME=$PROJECT_NAME
export SFN_ROLE_NAME="${PROJECT_NAME}SfnRole"
export SFN_POLICY_NAME="${PROJECT_NAME}SfnPolicy"

もういきなりお腹いっぱいな感じがありますが、ここに列挙したどれ一つ不要なものはありません。機能コードをサーバーレスに埋めていくとは、こういう手間を伴うものなのです。

デプロイコードとユニットテスト

そしてここからLambda Function、ECSコンテナ、そしてStep Functionsステートマシンをデプロイするコードを書いていくわけですが、ステートマシンをデプロイする部分をご紹介すると、以下のようになりました。

def deploy():
    print('deploying...')

    # (中略)

    containerDef = {
        'CLUSTER_ARN': f"arn:aws:ecs:{AWS_DEFAULT_REGION}:{AWS_ACCOUNT_ID}:cluster/{ECS_CLUSTER_NAME}",
        'TASK_DEFINITION_NAME': ECS_TASK_DEFINITION_NAME,
        'CONTAINER_TAG': ECS_CONTAINER_TAG,
        'ENVIRONMENTS': [
            {
                "Name":"TASK_TOKEN",
                "Value.$":"$$.Task.Token"
                },
            {
                "Name":"DATA",
                "Value.$":'$'
            }
        ]
    } # end of containerDef

    sm = get_steps(
        lambdaFunctionArn=LAMBDA_FUNCTION_ARN,
        containerDefinition=containerDef
    )

    sfnClient = boto3.client('stepfunctions')
    print('creating state machine...')
    # print(json.dumps(sm, indent=4))
    res = sfnClient.create_state_machine(
        name=SFN_NAME,
        definition=json.dumps(sm, indent=4),
        roleArn=f'arn:aws:iam::{AWS_ACCOUNT_ID}:role/{SFN_ROLE_NAME}',
        tags=[TAG_sl]
    )
# end of deploy

Step Functionsのステートマシン自体は、JSON文書として記述されます。ここへステートマシンから実行するLambda FunctionやECSのタスク情報を含めていくわけですが、これをステートマシン定義取得関数get_stepsの引数で指定するようにしておきました。 こうすれば、ユニットテストからこのデプロイコードが呼び出されたときに、本番とは違う仮のLambda FunctionやECSタスク情報を設定することができるだろうとの目論見です。

get_steps自体は以下のような実装になりました。

def get_steps(
    lambdaFunctionArn=None,
    containerDefinition=None
):
    SFN_NAME = os.environ['SFN_NAME']

    sm = {
        "Comment": SFN_NAME,
        "StartAt": "STEP1",
        "TimeoutSeconds": 300,
        "States": {
            "STEP1": {
                "Type": "Task",
                "Resource": lambdaFunctionArn,
                "TimeoutSeconds": 10,
                "InputPath": "$.input",
                "ResultPath": "$.res1",
                "OutputPath": "$",
                "Next": "STEP2",
                "Catch": [ {
                    "ErrorEquals": [ "States.ALL" ],
                    "Next": "FAILED"
                }]
            }, # end of STEP1
            "STEP2": {
                "Type": "Task",
                "Resource": "arn:aws:states:::ecs:runTask.waitForTaskToken",
                "TimeoutSeconds": 300,
                "Parameters": __create_ecs_parameters(containerDefinition),
                "InputPath": "$.res1",
                "ResultPath": "$.res2",
                "OutputPath": "$",
                "Next": "SUCCEEDED",
                "Catch": [ {
                    "ErrorEquals": [ "States.ALL" ],
                    "Next": "FAILED"
                }]
            }, # end of STEP2
            "SUCCEEDED": {
                "Type": "Succeed"
            },
            "FAILED": {
                "Type": "Fail"
            }
        } # end of States
    } # end of sm

    return sm
# end of get_steps

これはステートマシンのユニットテストの方法論の開発のためのテストのために仮組みしたものですが、これが下の図のようなステートマシンになります。

f:id:masashi-sato-flect:20191209141151p:plain
ステートマシン

予想外に苦労したのが __create_ecs_parameters の部分です。ステートマシンからECSを呼び出すにはタスク定義のARNを指定する必要がありますが、このタスク定義は更新の度に「project01Task:77」のように末尾の序数が加算されていき、この数字を指定する手段はありません。つまり目的のコンテナが指定されているタスク定義を(たいていは最新のタスク定義でしょうが)掘り出すしかないようなのです。これを掘り出してステートマシンのコンテナ定義を作成するのが、下記のような内容の __create_ecs_parameters 関数になります。

def __create_ecs_parameters(container_definitions):

    ECS_SUBNETS          = os.environ['ECS_SUBNETS'].split(',')
    TASK_DEFINITION_NAME = container_definitions['TASK_DEFINITION_NAME']
    CONTAINER_TAG        = container_definitions['CONTAINER_TAG']
    
    ecsClient = boto3.client('ecs')

    lTd = []
    nextToken = None
    while True:
        params = {
            'familyPrefix': TASK_DEFINITION_NAME,
            'status':       'ACTIVE',
            'sort':         'DESC'
        }
        if nextToken != None:
            params['nextToken'] = nextToken
        res = ecsClient.list_task_definitions(**params)

        if 'nextToken' in res:
            nextToken = res['nextToken']
        lTd.extend(res['taskDefinitionArns'])
        if nextToken is None:
            break
    # end of while (true)

    tdArn = ''
    for td in lTd:
        tdDesc = ecsClient.describe_task_definition(
            taskDefinition=td
        )
        image = tdDesc['taskDefinition']['containerDefinitions'][0]['image']
        # print(image)
        if image == CONTAINER_TAG:
            tdArn = td
            break
    # end of for (td)
    if tdArn == '':
        raise Exception(f'no task definition {TASK_DEFINITION_NAME} found for container {CONTAINER_TAG}')

    ret = {
        'LaunchType': 'FARGATE',
        'Cluster': container_definitions['CLUSTER_ARN'],
        'TaskDefinition': tdArn,
        'Overrides': {
            'ContainerOverrides': [{
                'Name': TASK_DEFINITION_NAME,
                'Environment': container_definitions['ENVIRONMENTS']
            }] # end of ContainerOverrides
        }, # end of Overrides
        'NetworkConfiguration': {
            'AwsvpcConfiguration': {
                'Subnets': ECS_SUBNETS,
                'AssignPublicIp': 'ENABLED'
            }
        } # end of NetrowkConfiguration
    }
    return ret
# end of __create_ecs_parameters

このような工夫を経て、ようやく当初の目的の「ステートマシンのユニットテスト」に到達しました。テストコード全体は以下のようになりました。

class TestSfn(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        deploy(test=True)
    # end of setUpClass

    @classmethod
    def tearDownClass(cls):
        undeploy()
    # end of tearDownClass   
    
    def test_sfn(self):
        
        AWS_ACCOUNT_ID     = os.environ['AWS_ACCOUNT_ID']
        AWS_DEFAULT_REGION = os.environ['AWS_DEFAULT_REGION']
        SFN_NAME           = os.environ['SFN_NAME']
        
        sfnClient = boto3.client('stepfunctions')
        
        execName = SFN_NAME + '_' + str(datetime.now().timestamp())
        input = {
            'input': {"Number1":10,"Number2":5}
        }
        
        res = sfnClient.start_execution(
            stateMachineArn=f'arn:aws:states:{AWS_DEFAULT_REGION}:{AWS_ACCOUNT_ID}:stateMachine:{SFN_NAME}',
            name=execName,
            input=json.dumps(input)
        )
        execArn = res['executionArn']
        res = {}
        while True:
            res = sfnClient.describe_execution(
                executionArn=execArn
            )
            status = res['status']
            if status != 'RUNNING':
                print(res)
                break
            time.sleep(5)
        # end of while True
        output = json.loads(res['output'])
        self.assertEqual(output['res2']['Difference'], 5)
    # end of test_sfn
# end of class TestClass01

このテストコードを実行することで、当初の目論見であったステートマシンのユニットテストが実行できるようになりました。テスト用のLambda Functionやコンテナと組み合わせれば、複雑な状態管理の作り込みと繰り返しの動作確認が可能になったと思います。

ふり返って、どうか

正直なところ、なかなか複雑な気持ちです。ステート管理・高可用性・柔軟かつ効率的なリソースアロケーションを目標として計画した目論見でしたが、結局CI/CDみたいなことをやることになってしまいました。Step Functionを本格的に業務活用しようとすると、自動デプロイの仕掛けを何かしら用意する必要があると感じました。設計目標はクリアできそうですが、けっこう大変でした。

特に設定が煩雑になったのはECSを呼び出す部分です。この点については以下のような支援機能がStep Functionに用意されていると、より開発作業が効率的になるのではないかと思いました。

  • ステートマシンとECSの入出力の高機能化。ステートマシンの入力値をJSONのまま受け渡しできるAPIの追加など。(現状では環境変数文字列にする必要がある。)
  • 個別のステートマシンからはECRのコンテナのみを指定して実行できるように簡略化。ステートマシン全体に既定のコンテナクラスタやタスク定義を設定する機能など。

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