Ubuntu 18.04 上の GitLab CI で Docker イメージをビルドして GCP の Container Registry に登録するまで

環境

さくらVPSUbuntu 18.04 をOSカスタムインストールしていて、GitLab CE をインストールします。GitLab CI Runner も同ホストで動かします。 CI で Docker イメージをビルドして、Google Cloud Platform (GCP) の Container Registry にプッシュします。

GitLab のインストール

下記のページの通りです。EEとCEの差はライセンスを適用するかどうかで変わりますが、絶対にCEのままというならインストールスクリプトのURLを変えるとそちらでインストールされます。

about.gitlab.com

GitLab CI 機能の設定

Docker CE をインストール

単に apt からインストールもできますが、バージョンが古いと嵌りやすいので最新の Docker CE をインストールします。

docs.docker.com

GitLab Runner のインストール

こちらは、Dockerへのインストールではなく、リポジトリからインストールしました。GitLab Runner はほぼ CI Runner のコマンド管理ツールで、実際ジョブ処理する Runner (ないし Executor) は次でインストールという認識です。

docs.gitlab.com

実際の GitLab Runner(Executor) として動作するDockerコンテナを登録

下記の Building Docker images with GitLab CI/CD ページ内の Use docker-in-docker executor* を実行します。 https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker-executor

リポジトリの CI 設定

GCP にストレージ管理者権限を持つサービスアカウントを作成し、JSONキーファイルを取得します。 https://cloud.google.com/container-registry/docs/advanced-authentication#json_key_file

Settings -> CI / CD -> Variables

  • GCLOUD_PROJECT_ID - GCP のプロジェクトID
  • GCLOUD_SERVICE_KEY - JSONキーファイルの中身を貼り付ける

.gitlab-ci.yml

下記のように設置する。例では gcr.io/<GCP project ID>/<repository group name>/<repository name> でイメージが push されます。

image: tilfin/gitlab-ci-to-gcr

services:
  - docker:dind

variables:
  DOCKER_DRIVER: overlay
  IMAGE_NAME: "$CI_PROJECT_PATH:latest"

before_script:
  - echo $GCLOUD_SERVICE_KEY > ${HOME}/gcr-key.json
  - gcloud auth activate-service-account --key-file ${HOME}/gcr-key.json
  - docker login -u _json_key --password-stdin https://gcr.io < ${HOME}/gcr-key.json

stages:
  - publish

publish:
  stage: publish
  script:
    - docker build -t $IMAGE_NAME .
    - docker tag $IMAGE_NAME "gcr.io/$GCLOUD_PROJECT_ID/$IMAGE_NAME"
    - docker push "gcr.io/$GCLOUD_PROJECT_ID/$IMAGE_NAME"
  only:
    - master
  • dindDocker IN Docker の略です。
  • overlay ストレージ・ドライバは Ubuntu 18.04 であれば利用できます。Docker 入れ子でもストレージは透過的にホストにアクセスすることでスピードが下がらないようにします。

補足) tilfin/gitlab-ci-to-gcr

tilfin/gitlab-ci-to-gcr イメージは、私が公式 Docker Hub にプッシュしてる docker:stableGoogle Cloud SDK と gcloud コマンドをインストールしたものです。

https://hub.docker.com/r/tilfin/gitlab-ci-to-gcr/

FROM docker:stable 
ARG CLOUD_SDK_VERSION=224.0.0
ENV CLOUD_SDK_VERSION=$CLOUD_SDK_VERSION

ENV PATH /google-cloud-sdk/bin:$PATH
RUN apk --no-cache add \
        curl \
        python \
        py-crcmod \
        bash \
        libc6-compat \
        openssh-client \
        git \
        gnupg \
    && curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-${CLOUD_SDK_VERSION}-linux-x86_64.tar.gz && \
    tar xzf google-cloud-sdk-${CLOUD_SDK_VERSION}-linux-x86_64.tar.gz && \
    rm google-cloud-sdk-${CLOUD_SDK_VERSION}-linux-x86_64.tar.gz && \
    ln -s /lib /lib64 && \
    gcloud components install kubectl && \
    gcloud config set core/disable_usage_reporting true && \
    gcloud config set component_manager/disable_update_check true && \
    gcloud config set metrics/environment github_docker_image && \
    gcloud --version
VOLUME ["/root/.config"]

総括

もともと Registry 機能自体も GitLab にありストレージだけ GCP を利用することもできます。しかし、マルチドメインでの運用でうまくいかない場合があったのと、可用性の面で直接 GCP の Registry を参照する方がいいため、このようにして使っています。

さくらVPSで物理HDD障害後にUbuntuのFile SystemがRead onlyになったときの対応方法

さくらインターネットからVPSの障害報告があり、サービス復旧の連絡があり、SSHできたので気にしてなかった。 しかし後日 unable to open /var/xxx: Read-only file system みたいな感じで軒並みVPS内のサービスが止まってしまっていた。 問い合わせところ既に物理HDD障害は交換で対応済みだが、仮想OSにおいてディスクのファイルシステムに問題があったためだった。 CentOSのデフォルトの方法はサポートの方に紹介いただいたが、元々カスタムOSでUbuntuを入れていたので自力で直したのでメモしておく。

  1. ブラウザでさくらのVPSコントロールパネルを開く。
  2. 対象のサーバのページを開く。
  3. シリアルコンソール(β版)を開く
  4. 強制再起動する。即座にシリアルコンソールで Shift キーを押し続ける
  5. grub メニューで Ubuntu ~ recovery mode というのを選択する。
  6. (initramfs) と入力待ちになったら e2fsck -f -y -v /dev/vda1 を行う。 vda1 のところは仮想環境によるかもしれないので5.でのログを見ておく。
  7. 完了したら reboot で再起動すると普通に起動してくれるはず。

※シリアルコンソールの表示が反応しない時もあるので、手順4以外はVNCコンソールを同時に開いて使っても良い。

参考ページ

RubyでYARD定義を使って実行時にメソッド引数と戻り値の型チェックを試みる

Ruby のカンファレンスの度に、毎度話題になる?) Ruby に型が欲しい件ですが、個人的な見解を書いておこうと思います。ちなみに私は RubyKaigi 2018 に参加しておりません。Twitterのタイムラインでの賑わいを見ていただけです。

最近、Swift を触っててそのコンパイルの重さにうんざりしているので、 型推論 は現代のマシンスペックでは基本的に辛いと思っています(カフェでノマドコーディングしたいので)。またメタプログラミングし放題の Ruby に導入するのは困難という認識です。

では、どういうときに 型定義 が欲しくなるのか考えてみます。

  1. コードを書いて実行して確認というトライアンドエラーを減らしたい、実行前にエラーをなるべく洗い出したい。
  2. 型定義起因による補完を効かせながらコーディングしたい。これはエディタやIDEのサポートも必要です。

1 について、Rubyの場合テストでカバレッジを稼いで、なるべくエラーの芽を潰しておこうとします。しかしユニットテストにおいて end-to-end テストが存在しない場合、単純なユーティリティ関数でなければ、モックやスタブに頼るようになりチェックが甘くなります。そのためテストは通過するものの、実際通しで動かしたとき想定とは異なる型のデータ受渡しが発生してしまうことがあります。

2 について、メソッド名や引数名から型をコードの読解で推測することは可能ですが、それなりの規模のアプリケーションやライブラリではコードコメントでドキュメント定義していないと(昔の自分や)他人の書いたコードを扱うのが困難になることが多いと思います。 そして例えば YARD で引数や戻り値の型をコメントに定義して、ドキュメントを生成したりIDEでコード補完に用いることが多いでしょう。

さてここから本題ですが、1 で型チェックしないで検証から漏れてしまう問題は、テストでメソッド呼び出しの引数と戻り値をよりチェックするアサーションを明示的に書いていく必要があります。一方 2 でYARDによるドキュメントの型定義は必ずしも実際のソースコードで走る処理と一致してるか保証されない問題もあります。

そこで『YARD定義によるメソッド引数と戻り値の型チェック』を実行時に行ってみたらどうかと考えてみました。

YARDはテンプレートで書きだす直前の解析データを、YARD::Registry から参照する機能を提供しています。またあらゆるメソッド呼び出し(:call)と戻り(:return)は、TracePoint 機構を使ってフックすることができます。これを組み合わせれば、割と簡単に、実行時に定義どおりに適切な型の引数が指定されてメソッド呼び出され、適切な戻り値が返っているかをチェックすることできると思います。

ということで、試してみましょう。

lib/dog.rb

これが YARD 定義を書いた検証対象のクラスになります。

module Animal
  #
  # This class is Dog
  #
  class Dog
    # @param name [String] a name
    # @param weight [Numeric] weight
    def initialize(name, weight)
      @name = name
      @weight = weight
      @children = []
    end

    # Add a child dog
    # 
    # @param dog [Animal::Dog] a child dog
    def add_child(dog)
      @children.push(dog)
    end

    # Run.
    # 
    # @param distance [Integer]
    # @return [String] message
    def run(distance)
      "#{@name} runs #{distance}."
    end

    # dummy method returns wrong type value
    # 
    # @return [Integer]
    def dummy
      "a string"
    end
  end
end

definition.rb

YARD::Registry から ClassObject と属する MethodObject を解析してチェックしやすい定義クラス MethodDefinition の集合 DefinitionStore に変換します。

require 'rubygems'
require 'yard'

class MethodDefinition
  def initialize(method_obj)
    @name = method_obj.name(true)
    @args = []
    @ret = nil

    load_docstr(method_obj.docstring)
  end

  def validate_arguments(args)
    errors = []

    args.each_with_index do |arg, i|
      arg_def = @args[i]

      ts = arg_def.types
      result = ts.find { |t|
          klass = Object.const_get(t)
          arg.is_a?(klass)
        }
      
      if result.nil?
        if ts.count > 1
          errors.push "#{arg_def.name}: #{arg.inspect} isn't any of " + ts.join(',')
        else
          errors.push "#{arg_def.name}: #{arg.inspect} isn't #{ts[0]}"
        end
      end
    end

    errors.empty? ? nil : "#{@name}(" + errors.join(', ') + ")"
  end

  def validate_return(ret_val)
    return nil if @ret.nil?

    ts = @ret.types
    result = ts.find { |t|
        klass = Object.const_get(t)
        ret_val.is_a?(klass)
      }

    if result.nil?
      if ts.count > 1
        "#{@name} returned #{ret_val.inspect} isn't any of " + ts.join(',') + ")"
      else
        "#{@name} returned #{ret_val.inspect} isn't #{ts[0]})"
      end
    else
      nil
    end
  end

  def load_docstr(docstr)
    docstr.tags.each do |tag|
      tag_name = tag.tag_name

      if tag_name == 'param'
        @args.push OpenStruct.new({
            name: tag.name,
            types: tag.types            
          })
      elsif tag_name == 'return'
        @ret = OpenStruct.new({ types: tag.types })
      end
    end
  end
end

class DefinitionStore
  def initialize
    @store = {}

    registry = YARD::Registry.load!
    registry.all(:class).each do |class_obj|
      add(class_obj)
    end
  end

  def add(class_obj)
    method_map = @store[class_obj.path] = {}
    class_obj.meths.each do |mt|
      method_map[mt.name] = MethodDefinition.new(mt)
    end
  end

  def get(klass_name, method_name)
    klass_def = @store[klass_name]
    klass_def[method_name]
  end
end

checker.rb

TracePoint でメソッド呼び出しと戻りをトレースして、引数または戻り値がYARD定義と適合してるかチェックします。

require 'tracer'
require_relative 'definition'

definition_store = DefinitionStore.new

TracePoint.trace(:call, :return) do |tp|
  klass_name = tp.defined_class.name
  method_def = definition_store.get(klass_name, tp.method_id)
  
  if tp.event == :call
    args = tp.binding.local_variables.map do |name|
             tp.binding.local_variable_get(name)
           end

    if err = method_def.validate_arguments(args)
      puts "Invalid call #{klass_name}#{err} on #{tp.path}:#{tp.lineno}"
    end
  elsif tp.event == :return && tp.method_id != :initialize
    if err = method_def.validate_return(tp.return_value)
      puts "Invalid return #{klass_name}#{err} on #{tp.path}:#{tp.lineno}"
    end
  end
end

# 試行
require_relative 'lib/dog'

dog1 = Animal::Dog.new(nil, "4.5")
p dog1.run(20)
dog1.dummy

dog2 = Animal::Dog.new("Taro", 6.5)
dog2.add_child(dog1)
dog2.add_child(nil)
p dog2.run(nil)

実行例

yard で解析データを生成してから、下記のようにチェッカーを実行してみます。

$ ruby cheker.rb
Invalid call Animal::Dog#initialize(name: nil isn't String, weight: "4.5" isn't Numeric) on ./lib/dog.rb:8
" runs 20."
Invalid return Animal::Dog#dummy returned "a string" isn't Integer) on ./lib/dog.rb:34
Invalid call Animal::Dog#add_child(dog: nil isn't Animal::Dog) on ./lib/dog.rb:17
Invalid call Animal::Dog#run(distance: nil isn't Integer) on ./lib/dog.rb:25
"Taro runs ."

とりあえずこの簡単な例で試すことは成功しました。もちろん引数が特定のモジュールを mix-in してるかといったチェックなど全然足りてはいません。ただこのような機構を、テストや開発向けデプロイ環境で動かすことで型違いを起因とするバグを減らせる手法の1つとして、提示できたんではないかと思います。

構造化ログのススメとRuby向けロガーOugaiを作った理由

構造化ログ

構造化ログ とは、機械的に処理しやすいログのことであり、その機構(ロギング)である。 英語圏では、 Structured Logging と表記される。たとえば Google Cloud の Stackdriver のドキュメントには下記の説明ページがあります。(残念ながら執筆時点で、これの日本語ページがまだできてないので、Google がどう訳すか興味深い)

Structured Logging  |  Stackdriver Logging  |  Google Cloud

普通のログと構造化ログの比較

普通のログは、基本的に タイムスタンプレベル 、そして メッセージ の文字列だけである。ログとして残す事象(イベント)のコンテキストになる情報はメッセージに適当に埋め込む。コンソール等で人が読みやすいものである。

構造化ログは、メッセージに埋め込んでいたコンテキストになる情報をそれぞれログ構造のフィールドに独立して持たせる。そのため後から解析がしやすい。そして出力するログはテキストベースで JSON にすることが多い。

では、見比べてみましょう。通常のRubyのLoggerと自作のOugaiでのログは次のようになります。『ユーザが記事を作成した』というログです(冠詞、削ってます)。

logger.info "User created article  (user_id=#{user.id} article_id=#{article.id}"
I, [2018-05-13T17:51:08.772542 #6253]  INFO -- : User created article  (user_id=123 article_id=45)
logger.info "User created article", user_id: user.id, article_id: article.id
{"pid":6253,"level":20,"time":"2018-05-13T17:52:25.147+09:00","msg":"User created article","user_id":123,"article_id":45}"

※デフォルトフォーマットとは異なります

見てわかる通り、普通のログは埋め込んで付帯情報を文字列化しつつ書かなくてはなりません。一方、構造化ログの方が JSON にした影響で長くなるのでコンソールでは読みづらいです。しかし読みづらいことはフォーマッタの動作環境での切替やparse機構を持つログビューワを使えば問題になりません。

構造化ログの方が解析しやすいというのは、例えば普通のログでは「あるユーザのログだけ抽出したいとき」に単に grep "user_id=10" とすると user_id が 101 など他のものまで引っかけてしまいます。構造化ログでは(主に JSONPath を使って)フィルタが $.user_id = 10 のように簡単に確実に絞り込めます。

ログ管理サービスと構造化ログ

AWS CloudWatch Logs, Google Cloud の Stackdriver LoggingLoggly, Logentries といったログ管理や解析向け SaaS はどれも構造化ログをサポートしていますし、むしろそうしないとそれらのサービスの機能を活かし切れません。ただ各サービスが提供するライブラリ自体は、構造化出力に対応してなかったりするので、Fluentd で中継させて送るのが現状ベストと思います。

Ougai を作った理由

元々 フロントAPIサーバを Node.js で書いて、バックエンドの管理サービスを Ruby/Rails で作るというプロジェクトによく参加してました。Node.js には Bunyan という有名な JSON ロガーがあり、これを好んで使っていました。しかしRubyにはいくつかの構造化ログを扱うライブラリはあるものの、各々のライブラリがフレームワークになっていました。Node.js 自体に console.info のようなコンソール出力向け機能しかないのですが、Ruby にはそれ自体に Logger があるのにも関わらず。もう一つマイクロサービスにおいて複数の言語で各サービスを実装することは多いですが、できれば横断的にログを解析したいためにそのフォーマットも統一したいと思いました。

これらの背景もあり、Ruby のオリジナル Logger ベースで、前述の Bunyan 互換でログ出力するフォーマットを持つロガーを自作しました。それが Ougai です。(Bunyan がイギリスの文学者名が由来だそうなので日本の文学者から名付けました)

github.com

Ruby のオリジナルの Logger クラスの拡張なのでいきなり導入しても、メッセージフィールドに出るだけでクラッシュすることはありません。徐々にコンテキストを独立したフィールドに移行することができます。フォーマットは標準で、JSON 出力用は Node.js の Bunyan と Pino 互換のもの、awesome_print を利用してターミナルでカラフルに見やすい Readable が入っています。JSON 用のはそれぞれ Bunyan と Pino が持つ専用ログビューワコマンドでもちろん閲覧できます。フックによって共通情報を入れやすくしたりする機能もあります。

Home · tilfin/ougai Wiki · GitHub

Wiki には Rails, Sidekiq, Fluentd 等と組み合わせた使うための設定例も載せてあります。

ougai | RubyGems.org | your community gem host

なお既にリリースしたから1年以上経っているので、知ってる範囲でも、知らないところでもそこそこ使ってもらえています。

Transgate という Node.js 製エージェントベースのタスクフローフレームワーク

Transgate というNode.js製のエージェントベースのタスクフロフレームワークを作った。

どうして作ったのか?

自宅の家電を操作するためのプログラムを書いていたら、色々なフローがごちゃごちゃになったから。 Dysonのファンから定期的に温度湿度を取得してデータベースに保存したり、Google Home/Assistant + IFTTT から来るメッセージを処理してIRKit を操作する。そのうち温度に従って自動的に IRKit 経由でエアコンを操作したくもなった、さてどう書こうかと?

どんなもの?

突然だけど空港などの荷物の仕分けをイメージしてください。エージェントは、ゲートから出てくるアイテムを受け取り、処理して別のゲートに送る。ゲートの向こう側がどうなっているかは、エージェントは何も知らない。エージェントは空のアイテムを来たら作業を終える。アーキテクチャのイメージはこんな感じです。

エージェントはゲートからアイテムを受け取ることと、別のゲートに新たにアイテムを送ることができる。アイテムはシンプルなオブジェクトだ。エージェントは自身のタスクに集中できる。だから前工程や次工程が増えても減ってもアイテムの構成が変わらなければ問題なく動く。そして入出力がシンプルなためユニットテストも簡単にかける。エージェントはゲートの実体を知らないので、入力元ゲートをスタブに、出力先ゲートをモックに、簡単に置き換えられる。

フレームワークに出てくる概念のまとめ

  • ゲート(Gate)はファイルストレージやデータベース、キュー、APIサービスといった入出力のエンドポイントです。
  • エージェント(Agent)はゲート間でアイテムを処理するタスクワーカーです。ゲートが何に通じているかは関知しません。
  • アイテム(Item)はゲート間を流れるシンプルなオブジェクトでエージェントが処理する対象です。

使用例

今回のフレームワークを作るきっかけになったホームコントロールプログラムを通じて説明してみます。 ちなみにこのプログラムは靴箱の中の Raspberry PI 上でデーモンとして動いています。

構成図

f:id:tilfin:20171123144501p:plain

メインプログラム (main.js)

const {
  Agent,
  HttpClientGate,
  HttpServerGate,
  IntervalGate,
  JointGate,
  StdoutGate,
  duplicator,
  mixer,
} = require('transgate');

const pino = require('pino')();
const config = require('konfig-yaml')();

const MongoGate = require('./lib/mongo_gate');
const IRKitGate = require('./lib/irkit_gate');

// Agent
const AnalysisCommander = require('./lib/analysis_commander');
const DysonCoolLinkRecorder = require('./lib/dyson/cool_link_recorder');
const EnvironmentalAnalyzer = require('./lib/environmental_analyzer');

// Gate
const slackGate = new HttpClientGate({ endpoint: config.slack.webhook_url });
const iftttGate = new HttpServerGate({ port: config.port });
const irkitGate = new IRKitGate(config.irkit.endpoint);
const intervalGate = new IntervalGate(60);
const mongoGate = new MongoGate(config.mongodb.endpoint, config.mongodb.collection);
const drToEaGate = new JointGate();

(async () => {
  try {
    await Agent.all(
      new AnalysisCommander(iftttGate, { irkitGate, slackGate }),
      new DysonCoolLinkRecorder(intervalGate, duplicator(mongoGate, drToEaGate)),
      new EnvironmentalAnalyzer(drToEaGate, { irkitGate, slackGate }),
    );
  } catch(err) {
    pino.error(err);  
    await iftttGate.close();
    await mongoGate.close();
  }

  intervalGate.clear();
})()
.catch(err => {
  pino.error(err);
});

7つのゲート

  • slackGate は slack にテキストメッセージをポストします。HttpClientGate のインスタンスで、アイテムとなるJSON{ "text": "<text message>" } です。
  • iftttGate は IFTTT の webhook から受け取った JSON をアイテムとして利用します。アイテムとなるJSON{ "target": "TV", "text": "<speaking words>" } です。
  • irkitGate はHTTPインターフェイスを備える赤外線送信器に命令します。アイテムとなるJSON{ "command": "celling_light_off" } です。
  • intervalGate は一定の間隔でアイテムを生成します。アイテムは { "time": <Date instance> } です。この場合は 1 分おきにエージェントの処理を走らせます。
  • mongoGate は MongoDB の指定のコレクションに送信されたアイテムを登録します。
  • drToEaGate は後述の DysonCoolLinkRecorder から EnvironmentalAnalyzer にアイテムの流すジョイントです。

3つのエージェント

  • AnalysisCommander は IFTTT の webhook から来た JSON をアイテムとして受け取り、操作対象とテキストから IRKit に対して送信すべき赤外線信号を指定します。slack には文言が解釈できなかったときにポストします。
  • DysonCoolLinkRecorder は Dyson PureCoolLink ファンから1分おきに温度と湿度を取得して、duplicator という複製機を挟んで MongoDB への書き込みとジョイントとなるゲートに送ります。
  • EnvironmentalAnalyzer はそのジョイントを通じて来た温度から閾値を超えていたらエアコンの操作を IRKit に要求します。自動的に操作をしたときは slack に記録します。

エージェントの実装

Agentのサブクラスを作ります。main メソッドで受け取ったアイテムを処理して指定先のゲートに新たなアイテムを送る処理を書きます。before/after のフックメソッドを使って、初期化処理や別に利用するプロセス(例えば headless chrome) をここで制御(起動・停止)します。

下記は EnvironmentalAnalyzer の実装例でです。室温が摂氏17度以下になったらエアコンをオンにします。

const { Agent } = require('transgate');

module.exports = 
class EnvironmentalAnalyzer extends Agent {
  async before() {
    this._preTemp = null;
    this._airconAlive = false;
  }

  async main(item, { irkitGate, slackGate }) {
    const curTemp = item.temp;

    if (this._preTemp && this._preTemp > 17 && curTemp <= 17) {
      if (!this._airconAlive) {
        await irkitGate.sendAll({ command: 'aircon_on' });
        this._airconAlive = true;
        await slackGate.send({ text: `Turn on aircon because temp is down to ${curTemp}` });          
      }
    }

    this._preTemp = curTemp;
  }
}

コンストラクタとアイテムの入力元ゲートが隠蔽されているのは、 null を受け取ると次のゲートに送り、自身は終了するという仕様の実装を意識させないためです。

特徴のまとめ

  • 複雑なデーモンやバッチプログラムに向いている。
  • 同じエージェントを並列で動かすようなことは想定していないので、大量に捌く処理には向いてない。
  • メインプログラムで登場するゲートとエージェントと、アイテムのタスクフローが定義できる。そのためこれだけで全体が把握できる。
  • エージェントの処理は async/await で擬似的に同期に書けつつ、エージェントが多くても Node.js なのでスレッドベースのように重くならない。
  • ゲートの置き換えが容易なので、エージェントのユニットテストが書きやすく、部分的な実行の確認もしやすい。

予想される疑問と答え

参照先サービスは全部ゲートになるのか? 

Noです。ゲート間は一方通行に限定されます。エージェントはその先を知らない。つまりリクエストを投げて、それに対するレスポンスを得ることはできません。往復ではなく、ループにすることは可能ですが、ステートレスなのでどの送り出したアイテム(リクエスト)に対してのレスポンスかはわからないのです。ゲートは、エージェントにとってトリガーとなるものを出す部分と成果を送る部分になります。

一連のフローが終わったら時にキッカーに完了を通知するには?

キューシステムはタスクが完了したら完了通知を送る必要が往往にあります。こういった場合は、アイテムにそのコンテキストを持たせてフローに流して、最後のゲートが完了通知を送る役割を担うようにします。

ロガーはゲートにすべきか?

ログがアウトプットそのものになるならゲートにすべきです。そうすれば後からゲートをさらに Agent にジョイントするものに置き換えて、そこからログ解析サービスに投げるといった修正も容易にできます。

ゲートにどこまでロジックを含めていいのか?

ゲートはできる限りシンプルな方が良いです。エージェントはテストしやすいように設計しますが、ゲートそのものにロジックを入れてしまうと入出力先を付け替えてテストできなくなります。ただプロジェクト共通のロジックでそれがフォーマット程度であれば、ゲートに実装してもいいでしょう。複雑ならばそれ用のエージェントを作ってゲートの前に置き、ジョイントで繋げるだけです。

Transgate に興味を持っていただけたら幸いです。

English version Transgate is Agent-based taskflow framework for Node.js

Windows 上に Python 環境を構築して Tensorflow GPU + Keras で日本古典籍字形の文字認識を試すまで

tilfin.hatenablog.com

こちらの記事で書いたように Windows で Tensorflow を GPU で試してみました。この中で Python をオフィシャルのインストーラから入れて virtualenv で動作環境を作成したのですが、後々 matplotlib や OpenCV を入れようとしたところ Windows のために諸々嵌ることがあったので、より簡単に All-in-One 導入できる Anaconda から入れてみることにしました。

www.procrasist.com

またこちらの記事で見た Keras が、今後 Tensorflow で色々と試す上で非常分かりやすそうだったので、導入してみることにしました。

Anaconda で Python 3.5 をセットアップ

既にインストールしていた Python は「プログラムと機能」からアンインストールしておきました。

Download Anaconda Now! | Continuum から [Python 3.5 version 64-BIT INSTALLER] をダウンロードして実行します。インストール ウィザードで環境変数を通しておきます。(デフォルトのインストール場所ではなく D:\Anaconda35 に自分は入れました。)

Tensorflow + Keras をセットアップ

環境構築

ここから先は PowerShell で行います。

conda で work という名前の環境を作ります。

PS D:\> conda create -n work python=3.5 anaconda

…省略…

Extracting packages ...
[      COMPLETE      ]|##################################################| 100%
Linking packages ...
        1 個のファイルをコピーしました。##################               |  71%
[      COMPLETE      ]|##################################################| 100%
#
# To activate this environment, use:
# > activate work
#
# To deactivate this environment, use:
# > deactivate work
#
# * for power-users using bash, you must source
#

Tensorflow の導入

work 環境にして pip で tensorflow と tensorflow-gpu をインストールします。

PS D:\> activate work

PS D:\> pip install tensorflow
Collecting tensorflow
  Using cached tensorflow-0.12.1-cp35-cp35m-win_amd64.whl
Requirement already satisfied (use --upgrade to upgrade): wheel>=0.26 in d:\anaconda3\lib\site-packages (from tensorflow)
Collecting protobuf>=3.1.0 (from tensorflow)
  Using cached protobuf-3.1.0.post1-py2.py3-none-any.whl
Installing collected packages: protobuf, tensorflow
Successfully installed protobuf-3.1.0.post1 tensorflow-0.12.1

PS D:\> pip install tensorflow-gpu
Collecting tensorflow-gpu
  Using cached tensorflow_gpu-0.12.1-cp35-cp35m-win_amd64.whl
Installing collected packages: tensorflow-gpu
Successfully installed tensorflow-gpu-0.12.1

Keras をセットアップ

Keras Documentation は numpy, scipy, pyyaml OpenCV, HDF5, hdf5 に依存しています。この中で OpenCV は anaconda で入らないので別途インストール必要があります。

OpenCV 3.2 をインストール

hikuichi.hatenablog.com 単純に pip でインストールしても Windows では動かないので、こちらを参考にビルド済みパッケージを使って、OpenCV をインストールします。 http://www.lfd.uci.edu/~gohlke/pythonlibs/ から opencv_python‑3.2.0+contrib‑cp35‑cp35m‑win_amd64.whl をダウンロードして、 pip で絶対パスを指定してインストールします。

PS D:\> pip install D:\opencv_python-3.2.0+contrib-cp35-cp35m-win_amd64.whl
Processing d:\opencv_python-3.2.0+contrib-cp35-cp35m-win_amd64.whl
Installing collected packages: opencv-python
Successfully installed opencv-python-3.2.0+contrib

Keras をインストール

PS D:\> pip install keras
Collecting keras
  Downloading Keras-1.2.0.tar.gz (167kB)
    100% |################################| 174kB 911kB/s
Collecting theano (from keras)
  Downloading Theano-0.8.2.tar.gz (2.9MB)
    100% |################################| 2.9MB 471kB/s
Building wheels for collected packages: keras, theano
  Running setup.py bdist_wheel for keras ... done
  Stored in directory: C:\Users\someone\AppData\Local\pip\Cache\wheels\f8\2c\8e\ffffff128220b1acf6fcd5e692269c6e8d607
44c15fd160c2c
  Running setup.py bdist_wheel for theano ... done
  Stored in directory: C:\Users\someone\AppData\Local\pip\Cache\wheels\96\2b\3d\ffffff24a7171a4afb7144d1e944a7be643b4
48b23a35b9937
Successfully built keras theano
Installing collected packages: theano, keras
Successfully installed keras-1.2.0 theano-0.8.2

Matplotlib, OpenCV, Keras を試す

Matplotlib を試す

REPL からシグモイド関数をグラフ描画して表示します。

PS D:\> python
Python 3.5.2 |Anaconda 4.2.0 (64-bit)| (default, Jul  5 2016, 11:41:13) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> import matplotlib.pylab as plt
>>> def sigmoid(x):
...   return 1 / (1 + np.exp(-x))
...
>>> x = np.arange(-5.0, 5.0, 0.1)
>>> y = sigmoid(x)
>>> plt.plot(x, y)
SetProcessDpiAwareness(2) failed: COM error 0xffffffff80070005  (Unknown error 0x0ffffffff80070005), using 2
[<matplotlib.lines.Line2D object at 0x000002370B81EA20>]
>>> plt.ylim(-0.1,1.1)
(-0.1, 1.1)
>>> plt.show()
>>>
>>> ^Z

f:id:tilfin:20170108213321p:plain

OpenCV を試す

REPL から画像ファイルを開いて表示してみます。

PS D:\> python
Python 3.5.2 |Anaconda 4.2.0 (64-bit)| (default, Jul  5 2016, 11:41:13) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
>>> cv2.imread('pic.jpg')
>>> img = cv2.imread('./pic.jpg')
>>> cv2.imshow('win1', img)
>>> cv2.waitKey(0)
13
>>>
>>> cv2.destroyWindow('win1')
>>> ^Z

f:id:tilfin:20170108213343p:plain

cv2.waitKey(0) の後、ウィンドウに対して Enter を入力すると表示されます。

日本古典籍字形の文字認識で Keras を試す

www.procrasist.com Keras を実際に試してみます。こちらの記事を参考に日本古典籍字形の文字認識を試してみました。

書名一覧 | 日本古典籍字形データセット の「機械学習による文字認識」から サンプルコード(TAR+GZ 24.57 MB) をダウンロードします。 D:\pmjt_sample_20161116 に展開しました。

PS D:\pmjt_sample_20161116> python .\run.py
Using TensorFlow backend.
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cublas64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cudnn64_5.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library cufft64_80.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library nvcuda.dll locally
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\stream_executor\dso_loader.cc:128] successfully opened CUDA library curand64_80.dll locally
X_train shape: (19909, 28, 28, 1)
19909 train samples
3514 test samples
Train on 19909 samples, validate on 3514 samples
Epoch 1/12
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:885] Found device 0 with properties:
name: GeForce GTX 960
major: 5 minor: 2 memoryClockRate (GHz) 1.253
pciBusID 0000:01:00.0
Total memory: 2.00GiB
Free memory: 1.64GiB
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:906] DMA: 0
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:916] 0:   Y
I c:\tf_jenkins\home\workspace\release-win\device\gpu\os\windows\tensorflow\core\common_runtime\gpu\gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 960, pci bus id: 0000:01:00.0)
19909/19909 [==============================] - 5s - loss: 1.4426 - acc: 0.5135 - val_loss: 0.7607 - val_acc: 0.7803
Epoch 2/12
19909/19909 [==============================] - 3s - loss: 0.7044 - acc: 0.7817 - val_loss: 0.4843 - val_acc: 0.8677
Epoch 3/12
19909/19909 [==============================] - 3s - loss: 0.5310 - acc: 0.8401 - val_loss: 0.3561 - val_acc: 0.8973
Epoch 4/12
19909/19909 [==============================] - 2s - loss: 0.4451 - acc: 0.8646 - val_loss: 0.3038 - val_acc: 0.9124
Epoch 5/12
19909/19909 [==============================] - 2s - loss: 0.3969 - acc: 0.8815 - val_loss: 0.2888 - val_acc: 0.9121
Epoch 6/12
19909/19909 [==============================] - 2s - loss: 0.3606 - acc: 0.8928 - val_loss: 0.2586 - val_acc: 0.9266
Epoch 7/12
19909/19909 [==============================] - 2s - loss: 0.3389 - acc: 0.9012 - val_loss: 0.2390 - val_acc: 0.9306
Epoch 8/12
19909/19909 [==============================] - 2s - loss: 0.3208 - acc: 0.9065 - val_loss: 0.2283 - val_acc: 0.9308
Epoch 9/12
19909/19909 [==============================] - 2s - loss: 0.2999 - acc: 0.9116 - val_loss: 0.2147 - val_acc: 0.9351
Epoch 10/12
19909/19909 [==============================] - 2s - loss: 0.2819 - acc: 0.9176 - val_loss: 0.2038 - val_acc: 0.9397
Epoch 11/12
19909/19909 [==============================] - 2s - loss: 0.2677 - acc: 0.9181 - val_loss: 0.1995 - val_acc: 0.9394
Epoch 12/12
19909/19909 [==============================] - 2s - loss: 0.2605 - acc: 0.9219 - val_loss: 0.1968 - val_acc: 0.9405
Test score: 0.196767529901
Test accuracy: 0.940523619806