Technology Topics by Brains

ブレインズテクノロジーの研究開発機関「未来工場」で働くエンジニアが、先端オープン技術、機械学習×データ分析(異常検知、予兆検知)に関する取組みをご紹介します。

WindowsのDockerで慣れないこと

Neuron開発チームの木村です。

Neuronは、ほとんどの場合Windowsにインストールされるため、テスト環境をWindows上に構築しています。 このテスト環境構築を効率化するため、Docker上でのWindows利用を模索し始めています。

今回は「Docker for Macを使っていた私が、WindowsでDockerを使う際に慣れないこと」を紹介します。

なお、利用しているWindowsのバージョンは
「Windows 10 Pro バージョン 1803 (Version 10.0.17134.48)」です。

以下、OSがWindowsであるコンテナを「Windows OSコンテナ」、OSがLinuxであるコンテナを「Linux OSコンテナ」と呼びます。

目次

どのDockerを使うべきか分からない

(この節は、Windows 10 Pro または Windows Server 2016 のバージョン 1709以上を前提とします。)

macでは、とりあえずDocker for Macをインストールすればよかったのですが、 WindowsでDockerを使用するには、以下の複数の方法があります。

  • Docker Toolbox
  • Docker for Windows
  • Hyper-Vコンテナ + LCOW
  • Windowsコンテナ

Docker Toolbox以外は、Windows OSコンテナもLinux OSコンテナも利用できますが、一体どれを使うべきなのでしょうか。

それぞれの特徴をまとめました。

Docker Toolbox

インストール方法

長所

  • (強いて言えば、)Hyper-Vなしで利用できる
    • 代わりに、VirtualBoxが使われる

短所

  • Windows OSコンテナを利用できない
  • 「Legacy desktop solution.」と説明されるなど、deprecated感がある

Docker for Windows

インストール方法

長所

  • docker composeなどのツール群が一緒にインストールされる

短所

  • Windows OSコンテナの実行にも、Hyper-Vによる仮想化が行われる
  • Linux OSコンテナ用のdocker daemonがMobyLinuxVMというHyper-Vインスタンス上で動作するため、二段の仮想化となり、コンテナ実行時にファイルのpermissionに引っかかるなど面倒なことが起きる可能性がある
    • (experimental機能(LCOW利用)を有効にすれば、MobyLinuxVMは介さないかもしれません)
  • Windows OSコンテナとLinux OSコンテナを同時に操作できない
    • 操作したいコンテナに合わせて、「モードを切り替える」または「experimental機能の--platformオプションを指定する」必要がある

Hyper-Vコンテナ + LCOW

インストール方法

長所

  • Windows OSコンテナとLinux OSコンテナを同時に操作できる
    • 現段階では、docker pullには--platformオプションが必要

短所

  • Windows OSコンテナの実行にも、Hyper-Vによる仮想化が行われる
  • LCOWは、現在experimental
  • 上記のインストール方法では、docker clientにパスを通したり、docker daemonのサービス化は自身で行うこととなる

Windowsコンテナ

インストール方法

長所

  • Windows OSコンテナについては、ホストOSのカーネルを使って実行されるため、Hyper-Vによる仮想化が行われない

短所

  • Windows Server 2016が必要
  • Windows OSコンテナについて、コンテナのWindowsバージョンとホストのWindowsバージョンが、ビルド番号まで一致している必要がある(バージョンが異なる場合、起動がブロックされる


どれを使えばよいかは要件次第なので、今回は、

  • Windows OSコンテナとLinux OSコンテナの両方を使いたい
  • OSをなるべく意識せずに、dockerコマンドを実行したい
  • テスト環境構築に使用するため、パフォーマンスは多少犠牲にしてもよい
  • (experimentalでも使ってみたい)

といった理由から、「Hyper-Vコンテナ + LCOW」を使うこととしました。


Windowsのdockerイメージはどれなのか

まだDocker上でWindowsが動かなかった頃は、どのdockerイメージをpullしてもLinuxのイメージであり、話は単純でした。
さて、では現在、Windowsのイメージをどう見分けてpullするのでしょうか。

結論から言うと、「これはWindows、これはLinux」といった目印は見つけられませんでした。

Windowsのイメージを探す時は、dockerHubのwinamd64microsoftをあたっていくと良いと思います(ただし、microsoftのほうはLinuxのイメージも多い)。

希望のイメージが見つからない場合は、以下のWindows Server 2016基底イメージを使ってDockerfileを書いてゆきましょう。

なお、Windows Server 2012など旧Windowsのイメージは存在しないと思われます。


DockerfileのRUN/CMDは、コマンドプロンプトで実行される

当然なのですが、DockerfileからWindowsのイメージをビルドする時、RUNCMDの内容は、Windows上で実行されます。 つまり、Linuxのshellコマンドをそのまま使うことはできません。

こればかりは、コマンドプロンプト(または、そこから呼び出すPowerShell)のコマンドに慣れてゆくしかありません…
さらに、パス区切りがややこしく、COPYなど/でよい命令と、WORKDIRなど\でなければならない命令が混在しています。

(bashのコマンドからWindowsのコマンドに変換する何かを、docker buildに挟みたい)


おわりに

今回は、dockerのインストールやdockerイメージの作成について、WindowsでDockerを使う際に慣れないことを紹介しました。
次回は、実際にコンテナを動かしていく際のことを紹介する予定です。 何事もなくすんなり使えればよいですが…

実践編に続きます。


ブレインズテクノロジーでは「共に成長できる仲間」を募集中です
採用ページはこちら

参考

Dockerコンテナで起動するElasticsearchの性能情報を取得した話

ブレインズテクノロジーの加藤です。

今回は、Dockerコンテナ上で起動するjavaアプリケーションの性能情報の取得方法についてまとめます。
例としてElasticsearchを使用しましたが、javaで起動しているアプリケーションであれば何にでも応用が効くはずです。

経緯

あるお客様の環境で、Dockerコンテナ上にElasticsearchを起動してサービスを提供しているのですが、GC系のERRORが頻発するようになりました。

原因の特定と解決策の検討のため、jstatによる性能情報を取得しようとしたのですが、ホストから見たElasticsearchのプロセスIDが異なるからか、ホスト側からは取得ができず。
仕方なくDockerコンテナに入って取得しようとしたのですが、今度はDockerコンテナにjdkが入っていないため、インストールするところから始めました。

以下はその手順です。

Dockerコンテナにログインする

Elasticsearchが起動していること、およびプロセス名を確認します。
今回のプロセス名は"brave_jepsen"のようです。

$ docker ps
CONTAINER ID        IMAGE                  COMMAND                  CREATED                  STATUS              PORTS                NAMES
3596bfd346d4        elasticsearch:latest   "/docker-entrypoint.…"   Less than a second ago   Up 2 seconds        9200/tcp, 9300/tcp   brave_jepsen

docker exec コマンドにより、elasticsearchのコンテナにログインします。

$ docker exec -it brave_jepsen bash
root@3596bfd346d4:/usr/share/elasticsearch#

ログインできました。

Dockerコンテナにjdkをインストールする

試しにDockerコンテナの中で、jstatコマンドを実行してみます。

$ jstat
bash: jstat: command not found

入ってないですね。仕方ないのでインストールします。
インストールにはapt-getコマンドを使用します。

まずはapt-getを最新化しましょう。

$ apt-get update
Ign:1 http://deb.debian.org/debian stretch InRelease
Get:2 http://deb.debian.org/debian stretch-updates InRelease [91.0 kB]
・・・(略)
Fetched 10.3 MB in 3s (3173 kB/s)
Reading package lists... Done

ちなみに環境によっては、apt-getを実行しても失敗することがあります。
その際は、環境変数「http_proxy」にプロキシサーバを設定すれば通るかもしれません。

export http_proxy=${プロキシサーバのホスト}:${プロキシサーバのポート}

続いて、apt-fileをインストールします。

$ apt-get install -y apt-file
Reading package lists... Done
Building dependency tree
Reading state information... Done
・・・(略)
Setting up apt-file (3.1.4) ...
The system-wide cache is empty. You may want to run 'apt-file update'
as root to update the cache.

apt-fileをupdateしろと言っているので、仰せのままに実行します。

$ apt-file update
Ign:1 http://deb.debian.org/debian stretch InRelease
Get:2 http://security.debian.org stretch/updates InRelease [63.0 kB]
・・・(略)
Building dependency tree
Reading state information... Done
3 packages can be upgraded. Run 'apt list --upgradable' to see them.

apt-fileのsearchコマンドにより、jstatが含まれるパッケージを見つけます。

$ apt-file search jstat
libmpj-java: /usr/bin/mpjstatus
openjdk-8-jdk-headless: /usr/lib/jvm/java-8-openjdk-amd64/bin/jstat
openjdk-8-jdk-headless: /usr/lib/jvm/java-8-openjdk-amd64/bin/jstatd
openjdk-8-jdk-headless: /usr/lib/jvm/java-8-openjdk-amd64/man/ja_JP.UTF-8/man1/jstat.1.gz
openjdk-8-jdk-headless: /usr/lib/jvm/java-8-openjdk-amd64/man/ja_JP.UTF-8/man1/jstatd.1.gz
openjdk-8-jdk-headless: /usr/lib/jvm/java-8-openjdk-amd64/man/man1/jstat.1.gz
openjdk-8-jdk-headless: /usr/lib/jvm/java-8-openjdk-amd64/man/man1/jstatd.1.gz

「openjdk-8-jdk-headless」に含まれるようです。
なので対象のパッケージをインストールします。

$ apt-get install -y openjdk-8-jdk-head
Reading package lists... Done
Building dependency tree
・・・(略)
update-alternatives: using /usr/lib/jvm/java-8-openjdk-amd64/bin/wsgen to provide /usr/bin/wsgen (wsgen) in auto mode
update-alternatives: using /usr/lib/jvm/java-8-openjdk-amd64/bin/jcmd to provide /usr/bin/jcmd (jcmd) in auto mode

jpsコマンドにより、Elasticsearchのプロセスが表示されれば成功です。
elasticsearchの場合、プロセスIDは1になるようです。

$ jps -v
1 Elasticsearch -Xms2g -Xmx2g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError -Des.path.home=/usr/share/elasticsearch
802 Jps -Dapplication.home=/usr/lib/jvm/java-8-openjdk-amd64 -Xms8m

Javaの性能情報取得

jstatコマンドにより性能情報を取得します。
使用可能なオプションの種類や、出力項目の詳細は、下記のオラクルのページに載っています。

docs.oracle.com

今回は gccauseオプションを使用したいと思います。
コマンドの意味としては、"-h1000"で1000行ごとにヘッダーを表示。
途中の1でelasticsearchのプロセスIDを指定。
1000は、1秒ごとに1000回繰り返すことでその間の性能情報を取得することができます。

$ jstat -gccause -h1000 1 1000 > /usr/share/elasticsearch/logs/gccause.txt
$ cat /usr/share/elasticsearch/logs/gccause.txt
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC
100.00   0.00  98.21   0.53  93.29  83.32      2    0.115     2    0.037    0.152 CMS Final Remark     No GC
100.00   0.00  98.21   0.53  93.29  83.32      2    0.115     2    0.037    0.152 CMS Final Remark     No GC
100.00   0.00  98.21   0.53  93.29  83.32      2    0.115     2    0.037    0.152 CMS Final Remark     No GC
100.00   0.00  98.21   0.53  93.29  83.32      2    0.115     2    0.037    0.152 CMS Final Remark     No GC
100.00   0.00  98.21   0.53  93.29  83.32      2    0.115     2    0.037    0.152 CMS Final Remark     No GC
100.00   0.00  98.21   0.53  93.29  83.32      2    0.115     2    0.037    0.152 CMS Final Remark     No GC
100.00   0.00  98.21   0.53  93.29  83.32      2    0.115     2    0.037    0.152 CMS Final Remark     No GC

GCログはDockerコンテナ内に出力されるので、volume mappingでホスト側と共有しておくと良いです。

ちなみにお客様の環境ではdocker-composeを使っています。
docker-compose.ymlの設定は以下のような感じです。

(抜粋)
  volumes:
        - /home/logs/elasticsearch:/usr/share/elasticsearch/logs

この設定をしておけば、ホスト側の「/home/logs/elasticsearch」ディレクトリにgccause.txtが出力されているはずです。
gccauseだと実行時刻が出力されないため、ホスト側で下記のようなコマンドを実行するといい感じです。

$ tail -f /home/logs/elasticsearch/gccause.txt | awk '{print strftime("%Y/%m/%d %H:%M:%S"), $0; fflush()}' > /home/logs/elasticsearch/gccause_new.txt

(出力結果)
2018/02/23 15:54:50   S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC                 
2018/02/23 15:54:50  99.35   0.00  81.41   5.55  98.06  94.74     40    2.183     2    0.108    2.292 Allocation Failure   No GC 
2018/02/23 15:54:50  99.35   0.00  81.43   5.55  98.06  94.74     40    2.183     2    0.108    2.292 Allocation Failure   No GC
2018/02/23 15:54:50  99.35   0.00  81.45   5.55  98.06  94.74     40    2.183     2    0.108    2.292 Allocation Failure   No GC
2018/02/23 15:54:50  99.35   0.00  81.46   5.55  98.06  94.74     40    2.183     2    0.108    2.292 Allocation Failure   No GC
2018/02/23 15:54:50  99.35   0.00  81.48   5.55  98.06  94.74     40    2.183     2    0.108    2.292 Allocation Failure   No GC

終わりに

というわけで、無事にDockerコンテナで起動するElasticsearchの性能情報を取得できました。
冒頭でも書きましたが、javaアプリケーションであれば、Elasticsearch以外でもこの方法で取得できるはずです。

この記事が誰かの役に立てればいいと思うー。そだねー。


ブレインズテクノロジーでは「共に成長できる仲間」を募集中です
採用ページはこちら

ABEJA Cloud AI Night 〜エッジコンピューティング編〜で発表しました

ブレインズテクノロジー 林です。
ご縁がありまして、ABEJA Cloud AI Night 〜エッジコンピューティング編〜で発表させていただきました。

発表資料はこちらになります。
speakerdeck.com

発表されている内容も共感する内容あり、また、とても勉強になる内容でした。
Q&Aも技術的に深い内容であり、費用対効果をどう考えるかなどビジネス的な観点の質問あり、
多種多様な方々の考えに触れることができ、多くの刺激を受けました。

まだまだ、クラウド x エッジの取り組みは未開拓の領域も多いですが、
技術の移り変わりも早く、新しい動きが起きているので非常に楽しみな分野です。

こういった機会に触れながら、より良いサービスを提供できるよう精進してまいります。
少し短い文章ですが、今回はこのあたりで失礼いたします。


ブレインズテクノロジーでは「共に成長できる仲間」を募集中です
採用ページはこちら

Amazon SageMakerのDeepARアルゴリズムで時系列予測

データ分析担当の青木です。

Amazon SageMakerでDeepARというアルゴリズムがリリースされました。今回の記事では下記のページを参考に、Amazon SageMakerのDeepARアルゴリズムを使ってみた内容を書きたいと思います。

aws.amazon.com

Amazon SageMaker

Amazon SageMakerは、機械学習におけるモデルの構築やデプロイ等の処理を容易に行えるようにしたフルマネージドな機械学習サービスです。SageMakerを使うことで、開発者やデータサイエンティスト等の分析者は、機械学習における一連の処理を手軽に素早くできるようになります。

DeepAR

DeepARはRecurrent Neural Networkをベースにした時系列予測の教師ありアルゴリズムで、ARIMAや指数平滑化法などの従来の予測手法に比べて高い精度が期待できる手法です。このDeepARは既存の時系列データがほぼない状況で予測を行うコールドスタート予測や、予測値の信頼区間を算出するなど、実際のビジネスシナリオに即した機能をサポートしています。

SageMakerでDeepARを試す

それではSageMakerでDeepARを試してみたいと思います。

SageMakerでのモデル作成および予測までの流れは、

データ準備 → 学習ジョブの設定・実行 → エンドポイントへのモデルデプロイ→ 予測

といった感じです。今回はコンソール画面から実行する方法を簡単に紹介したいと思います。

データ準備

DeepARでのデータフォーマットは、JSONとParquetの2つのファイルをサポートしています。JSONでの例は

{"start": "2016-01-01 00:00:00", "target": [4.9, 5.3, 3.4, 5.9, ...]}
{"start": "2016-03-01 00:00:00", "target": [2.0, 3.4, 4.5, 3.8, ...]}
{"start": "2016-03-09 00:00:00", "target": [5.6, 9.3, 3.4, 4.7, ...]}

のようになります。

startは開始時刻、targetは実際の数値配列となりますが、各フィールドの詳細はこちらを参照ください。

今回はこちらのサンプルノートブックにあるデータをそのまま使って試してみます。

f:id:brains_aoki:20180112215351p:plain

上記は乱数で発生させた1時間毎のデータで、データ数は400です。前半352個のデータをtrain、後半48個をtestとします。

さらにこのように発生させたデータ系列を200セット用意し、trainデータとtestデータをS3に準備しておきます。

学習ジョブの設定・実行

学習ジョブの設定・実行方法はいくつかありますが、今回はマネージメントコンソールから実行していきます。

まずは、ジョブ名やアルゴリズム、リソース等の設定です。AlgorithmはDeepAR forecastingを選択します。

f:id:brains_aoki:20180112232729p:plain

次にアルゴリズムのハイパーパラメータの設定です。

f:id:brains_aoki:20180112230356p:plain

続いて入力データの設定です。

まずはtrainデータの設定。

f:id:brains_aoki:20180112230516p:plain

続いてtestデータの設定。テストデータの設定は任意です。

f:id:brains_aoki:20180112230636p:plain

以上の設定が完了したらジョブ実行ボタンを押下します。

エンドポイントへのモデルデプロイ

作成したモデルを選択し、エンドポイントを作成してモデルをデプロイします。

f:id:brains_aoki:20180112234119p:plain

予測

notebookから予測を試してみます。

f:id:brains_aoki:20180115012253p:plain

output_typeはquantilesとしてみました。結果がちゃんと得られていますね。

終わりに

Amazon SageMakerでDeepARを試してみましたが、データ準備からDeepARによる予測まで手軽に素早く行うことができました。一連の面倒な処理もこれだけ簡単にできれば、機械学習による検証もより素早く行えるようになりそうだという印象でした。今後はいろいろなデータでこのSageMakerを試して見たいと思います。


ブレインズテクノロジーでは「共に成長できる仲間」を募集中です
採用ページはこちら

docker-composeで環境変数を使う

こんにちは。エンジニアの中西です。
今回は docker-compose の yml ファイルにて、環境変数が展開されずにハマった事について書きたいと思います。
docker-compose は Impulse でも使っています。複数のコンテナから構成される Impulse を yml ファイルで管理できて便利です。
yml ファイルに環境変数を定義しておき、実行環境毎に変数値を設定する事も可能なのですが、環境によっては設定した値が展開されず。。という事象に遭遇しました。

実行環境

  • OS : Ubuntu 16.04
  • docker : v17.09.0-ce
  • docker-compose : v1.17.0
  • docker-compose.yml : v3.3

準備

それでは、イメージのバージョンを環境変数で設定できるようにymlファイルを記述してみます。

version: '3.3'

services:
  envtest:
    image: envtest-node:${IMAGE_VERSION:-invalid}
    :
  (省略)

イメージのバージョンを環境変数 IMAGE_VERSION で変更可能としています。 環境変数の指定がない場合は invalid が設定されます。

続いて、イメージが存在している事を確認します。

$ docker images envtest-node
REPOSITORY     TAG     IMAGE ID      CREATED      SIZE
envtest-node   latest  fedc1d9b97ce  1 weeks ago  933MB

バージョンは latest です。

実行してみる

まずは環境変数を指定せずに実行してみます。

$ docker-compose up -d envtest
Pulling envtest (envtest-node:invalid)...
ERROR: pull access denied for envtest-node, repository does not exist or may require 'docker login'

環境変数が設定されていないので、デフォルトのinvalidが設定されました。
当然ですが、envtest-node:invalid というイメージは存在していないので docker pull で取得しようとしていますが、ログインしていないためエラーとなっています。

では、環境変数を指定して実行します。

# バージョン`latest`を設定
$ export IMAGE_VERSION=latest

# 実行
$ docker-compose up -d envtest
Pulling envtest (envtest-node:invalid)...
ERROR: pull access denied for envtest-node, repository does not exist or may require 'docker login'

エラー。
バージョン部分がデフォルト値の invalid となっていますね。。
設定した環境変数が反映されていないようですので、確認してみます。

$ echo ${IMAGE_VERSION}
latest

ちゃんと設定されていますね。

では、docker-compose.yml の environment に設定値含めて追加してみます。

version: '3.3'

services:
  envtest:
    image: envtest-node:${IMAGE_VERSION:-invalid}
  environment:
    - IMAGE_VERSION=latest
    :
  (省略)

再度実行します。

$ docker-compose up -d envtest
Pulling envtest (envtest-node:invalid)...
ERROR: pull access denied for envtest-node, repository does not exist or may require 'docker login'

エラー。同じですね。

どうすれば展開されるのか

docker-composeは .env という環境ファイルにデフォルトの環境変数を定義できるようなので、設定してみます。

$ vi .env
IMAGE_VERSION=latest

上記の1行を設定して保存し、実行してみます。

$ docker-compose up -d envtest
Creating envtest ...
Creating envtest ... done

おぉ、起動したようです! 確認してみます。

$ docker ps
CONTAINER ID  IMAGE                COMMAND                 CREATED        STATUS        PORTS                   NAMES
452f84eed286  envtest-node:latest  "/bin/sh -c 'node sr…"  2 minutes ago  Up 5 seconds  0.0.0.0:3333->3333/tcp  envtest

バージョンlatestで起動されています。

もしかして、.envに環境変数が設定されていれば、実行時の環境変数設定値が反映されるのか?
公式サイトによると、
実行時に環境変数を指定すると、常に .env ファイル中で定義した変数を上書きします。同様にコマンドラインの引数で値を指定した場合も、指定した値を優先します。
とあります。

環境ファイル — Docker-docs-ja 17.06.Beta ドキュメント

試してみます。

$ export IMAGE_VERSION=none

存在しないバージョンを設定したので起動されないはずです。

$ docker-compose up -d envtest
Creating envtest ...
Creating envtest ... done

起動してしまいました。。

$ docker ps
CONTAINER ID  IMAGE                COMMAND                 CREATED        STATUS        PORTS                   NAMES
f50c6736d2e7  envtest-node:latest  "/bin/sh -c 'node sr…"  4 seconds ago  Up 3 seconds  0.0.0.0:3333->3333/tcp  envtest

バージョンは.envに設定したlatestですね。
実行時の環境変数設定が反映されていない事がわかります。

原因は何?

とりあえず、 .envに書いておけば反映される 事は分かりましたが、冒頭で述べた通り環境によっては実行時の環境変数設定が反映されたりと、気持ちの悪い状態が続いていました。
そんな中、何気なくdockerのイメージ一覧を確認していたときでした。

$ docker images
REPOSITORY      TAG     IMAGE ID      CREATED      SIZE
docker/compose  1.17.0  b5a188e247b9  3 weeks ago  19.6MB

docker-compose のイメージが存在しています。
そういえば以前から環境によっては存在していた気がしますが、あまり気にしてはいませんでした。
ちょっと公式サイトを確認してみましょう。

Docker Compose のインストール — Docker-docs-ja 17.06.Beta ドキュメント

docker-composeのインストール方法は3パターン存在するようです。

  1. curlコマンド実行によるインストール
  2. pipコマンド実行によるインストール
  3. dockerコンテナとしてインストール

※ビルドする方法もありますがここでは除外します

今回の環境は3番目ですね。
そういえば、実行時の環境変数設定が反映されている環境はどうなんだろう。
docker-composeのコンテナは存在しているのか確認してみる。

$ docker images docker/compose
REPOSITORY      TAG     IMAGE ID      CREATED      SIZE

ありませんね。 curl、又はpipでインストールされている事になります。

まさか、インストールの方法で動きが変わる???
試しに、NGな環境のdocker-composeを削除し、curlで再インストールしてみましょう。

$ docker rmi <docker-composeのイメージID>
:
$ curl -L --fail https://github.com/docker/compose/releases/download/1.17.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
$ docker-compose version
docker-compose version 1.17.0, build ac53b73
:
$ docker images
REPOSITORY     TAG     IMAGE ID      CREATED      SIZE
envtest-node   latest  fedc1d9b97ce  1 weeks ago  933MB

正しくインストールされ、docker-composeのイメージも存在していないですね。
実行する前に、環境変数の値を再確認しておきます。

# .envファイル
IMAGE_VERSION=latest

# 実行時の環境変数
$ echo ${IMAGE_VERSION}
none

デフォルトはlatestですが、実行時にnoneとしているので、noneとなるのが理想です。
実行してみます。

$ docker-compose up -d envtest
Pulling envtest (envtest-node:none)...
ERROR: pull access denied for envtest-node, repository does not exist or may require 'docker login'

なんと! イメージのバージョン指定がnoneとなっています!
これは、.envの設定値を実行時の設定値で上書きされている事になります。
意図した動作をしてくれました。

終わりに

なぜコンテナとしてインストールすると環境変数の値が反映されないのかは調査できていませんが、 もし、同様の事象にハマった場合には、一度dockerのイメージ一覧を確認してみる事をおすすめします。
そこにdocker/composeのイメージが存在していたら。。。


ブレインズテクノロジーでは「共に成長できる仲間」を募集中です
採用ページはこちら

PowerShellDSCでハマったポイント

すっかり寒くなってきました。
こんにちは、ブレインズテクノロジーの白石です。

今回、業務の中でPowerShellDSCに触れることがありましたので、それについて得た知見を書きます。
というのも、PowerShellDSCの情報ってあんまり載ってないので。。。
こういうやり方あるよ、こういうの知ってるよ、ていうのがあれば是非是非コメントお願いします。

ちなみに、使用するPowerShellのバージョンは5.0以上ですが、一部5.0未満が対象の話題もあります。
特に理由がなければ5.0に上げることをオススメします。

PowerShellDSCとは

PowerShellDSCとは、Windows専用の構成自動化ツールです。
詳しい紹介はこちら。
www.atmarkit.co.jp

Configuration, Node, Resource, ConfigurationData あたりが理解できてると以降の内容は読みやすくなると思います。

管理マシンと構成対象のマシンでPSのバージョンは揃える

そりゃそうだ、という話ですが、直面すると意外に気づきません。
バージョンが異なるとモジュールの配置場所が違ったり仕様が異なったりしていて、あるマシンではうまく動くのに別のマシンでは全く動かない、という奇妙なバグに悩まされます。(体験談)

使用してるマシンのバージョンは $PSVersionTable で確認できるので、DSCで構築を行う前に対象のマシンと管理マシンのバージョンが揃っているか確認しておきましょう。

外部のモジュールは構築対象のマシンにも入れておく

これも言われれば、そりゃそうなんやろね、という感じですが。

PowerShellを使ってると、こういう機能ないの?と色々欲が出ます。そうした場合、同じように考えた人がやはりいて、大抵の場合はライブラリとして提供されているわけです。
そういったライブラリは Install-Module xxx とかで取り込んでいくのですが、
それらは取り込んだマシンでしか使えません。つまりローカルテストでは動きますが、リモートマシンで同じスクリプトを動かそうとすると、そのマシンには対応するライブラリがインストールされてないのでエラーになります。

この辺りは、そのモジュールをインストールするPSスクリプトを書いてリモートで適用するなり、
該当するライブラリをリモートで配置するなりして対応しましょう。

リソースは一意性がある

まず、「リソース」とはなんぞや。
「リソース」とは、PowerShellDSCの用語の1つで、マシンに適用する状態や操作のことだと思ってください。
プログラムを経験してる方には関数といえば何となく伝わるでしょうか。
例えば、フォルダを作る、ファイルを消す、Zipを展開する、環境変数を定める、などなど。

で、このリソースというもの。構成の中で一意性を保証する必要があり、そのためにキーがあります。
なんのこっちゃという感じなのでとりあえず例を。

File CreateDirectory {
  Ensure='Present'
  Type='Directory'
  DestinationPath='C:\sample'
}

これはディレクトリを作るリソース(関数)で、リソース名(=CreateDirectory)がキーとなっております。
もちろん動きます。

さて、もう1個ディレクトリ作りたいなーと思い、このリソースを使い回すことを考えたとします。

File CreateDirectory {
  Ensure='Present'
  Type='Directory'
  DestinationPath='C:\sample'
}

File CreateDirectory {
  Ensure='Present'
  Type='Directory'
  DestinationPath='C:\sample2'
}

エラーになります。2回呼び出しただけなのに、どういうことなのか。
先ほど上でリソースにはリソース名がキーとなっていると申し上げました。
つまり、リソース名(例だとCreateDirectory)は適用する構成全体の中でユニークである必要があるのです。
なのに同じリソース名を使ったため、一意性が失われてエラーになりました。
(ちなみに、リソースが違えばリソース名が同じでも大丈夫です。あくまで同じ種類のリソースで一意ということです。)

なるほど。というわけで、例えば以下のようにして解決します。

$destinationArray=@('C:\sample', 'C:\sample2')
foreach($path in $destinationArray){
 File (CreateDirectory + $path) {
   Ensure='Present'
   Type='Directory'
   DestinationPath=$path
 }
}

リソース名に変数をつけてやれば、リソース名の一意性は保たれます。
foreachとかで回したい時のちょっとしたテクニックです。
関数の感覚で使うと、この仕様はちょっと面食らいます。

キーはリソース名だけじゃない

上の話題は序の口です。
今度は例として、環境変数HOGEを作って削除するという何の意味もない操作を作ることにしましょう。

Environment CreateHoge {
 Ensure='Present'
 Name='HOGE'
 Value='C:\hoge'
}
Environment RemoveHoge {
 Ensure='Absent'
 Name='HOGE'
}

リソース名は別にしたのに怒られます。なぜなのか。
操作をよく見ると、HOGEという環境変数を「存在する」「存在しない」ようにする操作が混在してますね。
このEnvironmentというリソースは、環境変数名であるNameパラメータもキーとなっているため、リソース名だけでなく、Nameもユニークである必要があるのです。
この辺りは各リソースの仕様に依存しているので、設定を見るなり、エラーにぶつかりながら学ぶしかありません。

ちなみに、これを解決するにはScriptリソースを使って無理やり書くしかありません。
もしくはConfigurationそのものを分けるか。


そもそもこういう操作をDSCに求めるのが間違っている、ということだと私は思ってます(それでもやるときはやる)。

Scriptリソース内だと変数展開のタイミングが違う(powershell 5.0未満限定)

まだあるのか!!あるんです。ただし、PowerShell 5.0未満を使っている人だけです。
ここまででリソースにはキーがあり、それはリソース名だけでなく指定するパラメータにもあることを紹介しました。
そして、その関連で最後にハマるであろう箇所がScriptリソースを使用した時です。

本題の前にScriptリソースとはなんぞやという話をしておきましょう。

通常、FileリソースやEnvironmentリソースなど、予め用意されたリソースに所定のパラメータを渡すことによって目的とする操作を行うことができます。
ただ、そうした操作が用意されていない場合、もしくは、もっと細かい操作を書きたい、となった場合、
Scriptリソースを使います。
これ、中の記述で行いたい操作のPowerShellスクリプトを直接書いてしまいます。
詰まる所Scriptリソースさえあれば力づくで何だってできるわけです。
(これをやりだすとべき等性とかどうでもよくなってくる。。。)

話を元に戻します。
Scriptを使って複数のディレクトリを作る処理を書いてみます。
(通常はFileリソース使いますが、説明のためScriptで書きます)

foreach($path in @('C:\test1', 'C:\test2')){
 Script $path {
  GetScript={
   return $null;
  }
  TestScript={
   return (Test-Path $using:path);
  }
  SetScript={
   New-Item $using:path -ItemType Directory;
  }
 }
}

$using:pathとは、Scriptの外から変数を渡す際につける接頭語です。$pathでは解釈されません。
一見うまくいきそうです。少なくとも、Fileリソースで作っていた場合は問題ありませんでした。

これを実行すると以下のようなエラーが出ます。

Add-NodeKeys : キー プロパティの組み合わせ ' (中略) ' が、ノード 'localhost' 内のリソース 'Script' のキー 'GetScript,SetScript,TestScript' で重複しています
。キー プロパティは、ノード内の各リソースで一意になるようにしてください。

なんやそれ。。。
これは、Scriptリソース内で指定した$using:変数の展開がリソースのキーチェックの後に行われるため、キーが重複したと判断されているからです。
これをどう解決すればいいのかというと、一つはPowerShellのバージョンを5.0に上げてしまうことです。
5.0ではmofファイル生成時に$usingで使用している変数について、スクリプトの先頭に代入文が挿入されて解決されるようになっています。
何かしら事情があって5.0へ上げられない人はどうするのか。
安心してほしい。実はこれを解決した先人がいるのです。
www.briantist.com

リンク先にある関数を宣言して、GetScript, TestScript, SetScriptの後ろに " | Replace-Using " と書くと、キーの評価前に変数が展開され、エラーを回避できます。素晴らしい。

コンパイル時の変数展開はコンパイルしたマシン上で行われる

話を変えて、変数の展開、特に環境変数についての話をします。
表題だけ見ると、何言うてんねん、と言いたくなりますが、私はハマってしまいました。
PowerShellスクリプトにおいて、環境変数へは$env:変数名でアクセスすることができます。

で、これは実際に私が書いたコードですが、JAVA_HOME環境変数がnullかどうかでJavaのインストールを行う処理を書いてました。
(パッケージのインストールについてはcChoco(ChocolateyのDSC版)を使うと楽なのですが、当時はライセンス明記が不明瞭だったので使用を避けてました。今はApache v2ライセンスになってるので問題なさそうです。)

if($env:JAVA_HOME -ne null){
 Script InstallJava{
  (Javaインストール処理)
 }
}

ローカルでテストしたら、うまく動いてるようです。
ですが、リモートで別のマシンに適用するとうまく動きません。
ん? そう思ったらうまく動いてる時もあります。一体何がどうなっているのか。

実は、$env:JAVA_HOMEの評価はこのスクリプトコンパイルした際のマシンの値を参照しているのです。
PowerShellDSCでConfigurationファイルをコンパイルする際、変数値は展開され、適用するマシンそれぞれに対応したmofファイルが作られます。
この時、マシン内の情報を参照するような変数を扱った場合、それらはコンパイルしたマシンの情報が適用されてしまいます。

つまり、適用したいマシンのJAVA_HOMEをチェックして書いたつもりが、実はそれはコンパイルを行なったマシンのJAVA_HOMEをチェックしているだけで、適用したいリモートマシンの情報は見れていないのです。

こういう場合、Invoke-Commandなどで事前に情報を取っておいて、
ConfigurationDataに情報を載せるのが良いと思います。
(ただし、結構コードがゴチャゴチャします。もっと楽な方法があるといいのですが・・・。)

ConfigurationDataのNode ‘*’ と、それ以外のNodeのKeyValueについて

PowerShellDSCにはConfigurationDataという、マシンの名前やパラメータの値を書いておく設定ファイルを用意できます。
この時、NodeName=‘*’ とした部分のKeyValueは全ノードに適用されます。

これは、デフォルト値としても利用出来ます。

AllNodes=@(
@{
NodeName=‘*’
Something=‘default’
},
@{
NodeName=‘localhost
Something=‘custom’
}
)

こう書けば、NodeName=‘localhost’の時のSomethingの値はcustomとなっていて、他のノードではデフォルト値としてdefaultが入ります。
ここで注意することは、これがKeyValueの上書きであるということです。
特に、構造体に対して適用する時は本当に注意しましょう。

AllNodes=@(
@{
NodeName=‘*’
Something=@{
arg0=‘sample00.txt’
arg1=‘sample01.txt'
}
},
@{
NodeName=‘localhost
Something=@{
arg0=‘extra.txt’
arg2=‘sample02.txt'
}
}
)

こう書いた場合、localhost のノードでは、Something.arg0の値は上書きされています。
そして、arg2が定義されてるのでそれにもアクセスできるようになります。

ただ、arg1は$null値となります。なぜなら、KeyValueが上書きされてarg1は未定義となるからです。
(PowerShellでは定義されていないKeyにアクセスすると $null が返されます。)
これがバグに繋がっていなければいいですが、いかんせんコンパイル時にエラーにならないのが怖いところです。

マージや継承のような感覚では書けないので注意です。

ConfigurationDataをJSONで書く

上で述べたConfigurationDataですが、
これはコード内に書いて宣言することもできますし、外部ファイルに書いておいて読み出すことも可能です。

いずれにしてもこの設定ファイル、記述がJSONによく似ています。
いっそのことJSONで管理してしまいたい。

JSONを扱うためのCmdletとして、ConvertFrom-JSONが用意されてます。
使い方は以下が詳しいです。
tech.guitarrapc.com


ただし、ConvertFrom-Jsonで読み込んだ場合、その変数の型は PSObject です。
PowerShellDSCでコンパイル時に渡しているConfigurationDataの型は HashTable なので、PSObjectからHashTableに変換する必要があります。
下記リンクを参考に関数を用意。
Topic: Configuration Data for DSC not in JSON

function ConvertPSObjectToHashtable
{
 param (
  [Parameter(ValueFromPipeline)]
  $InputObject
 )

 process
 {
  if ($null -eq $InputObject) { return $null }
  if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]){
   $collection = @(
    foreach ($object in $InputObject) { ConvertPSObjectToHashtable $object }
   )
   Write-Output -NoEnumerate $collection
  } elseif ($InputObject -is [psobject]){
   $hash = @{}

   foreach ($property in $InputObject.PSObject.Properties){
    $hash[$property.Name] = ConvertPSObjectToHashtable $property.Value
   }
   $hash
  } else {
   $InputObject
  }
 }
}

変換して実行する部分はこんな感じ。$jsonFileはもちろん読み込みたいJSONファイル名

$tmpObject = cat $jsonFile | convertFrom-Json
$hash = $tmpObject | ConvertPSObjectToHashtable

Sample -ConfigurationData $hash
Start-DscConfiguration .\Sample -Wait -Force

PowerShell5.0の便利機能

こちらが大変詳しい。
www.buildinsider.net

詳しすぎて特にここで言うこともないのですが、それでもこれだけは強調しておきます。

Get-DscConfigurationStatus超便利

上に貼ったリンク先でも特にオススメされてる機能ですが、本当に助かります。
PowerShellはうまくいかなかった時、「うまくいきませんでした。」くらいの内容しか返してくれなくて(大げさですが)、調査に時間を取られるのですが、このコマンドを打てばどのリソースが成功してどのリソースが失敗したのかが一発でわかります。

特に複数のConfigurationを適用するときは、合間にこのコマンドを打ってログとして残しておくことでどこまで進んだのかわかりますし、
条件分岐をしていた場合にもどのリソースが実行されたかが記録として残されるので、テストにもってこいです。

終わり

PowerShellDSCはあまり情報が載ってなくて、さらにはエラー内容も比較的不親切なこともあって、結構苦戦しました。
1個1個、解決した内容はメモして再発させないことを心がけると幸せになれると思います。

そして、できればその内容を共有をしていただけると、同じように悩む人も幸せになれるので、是非是非お願いいたします。

Pythonによる時系列データの異常検知

インターン生の松井(B4)です.時系列データの異常検知手法をまとめました.

入門 機械学習による異常検知という本の7章が時系列データの異常検知を扱っています.(本書の内容をまとめたWeb記事もあります.)
www.coronasha.co.jp
この本のサンプルコードはすべてRで書かれているため,Python (+numpy, scikit-learn) で書き直してみました.

後半では,深層学習を用いた時系列データの異常検知手法について,知られている所をまとめました.

k近傍法による異常部位検出

時系列データの異常検知手法の中でも比較的シンプルなやり方です.
訓練用の時系列データをスライド窓によりベクトル化しておき,k近傍法を用いて新たな時系列の異常度を計算します.

k=1として最近傍法を実装したコードがこちら.

# -*- coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
from sklearn.neighbors import NearestNeighbors

def main():
    data = np.loadtxt("your path/qtdbsel102.txt",delimiter="\t")

    train_data = data[1:3000, 2]
    test_data = data[3001:6000, 2]

    width = 100
    nk = 1

    train = embed(train_data, width)
    test = embed(test_data, width)

    neigh = NearestNeighbors(n_neighbors=nk)
    neigh.fit(train)
    d = neigh.kneighbors(test)[0]

    # 距離をmax1にするデータ整形
    mx = np.max(d)
    d = d / mx

    # プロット
    test_for_plot = data[3001+width:6000, 2]
    fig = plt.figure()
    ax1 = fig.add_subplot(111)
    ax2 = ax1.twinx()

    p1, = ax1.plot(d, '-b')
    ax1.set_ylabel('distance')
    ax1.set_ylim(0, 1.2)
    p2, = ax2.plot(test_for_plot, '-g')
    ax2.set_ylabel('original')
    ax2.set_ylim(0, 12.0)
    plt.title("Nearest Neighbors")
    ax1.legend([p1, p2], ["distance", "original"])
    plt.savefig('./results/knn.png')
    plt.show()


def embed(lst, dim):
    emb = np.empty((0,dim), float)
    for i in range(lst.size - dim + 1):
        tmp = np.array(lst[i:i+dim])[::-1].reshape((1,-1)) 
        emb = np.append( emb, tmp, axis=0)
    return emb

if __name__ == '__main__':
    main()

心電図のデータをサンプルとして用いています.
周期的で正常な心電が見られる部分を訓練データとし,乱れている部分を含むようにテストデータの区間を取りました.
結果のプロットは以下のようになりました.
f:id:Nori_matsu:20170930222409p:plain
異常を含む区間で異常度 (グラフ中distanceと表記) が高くなっていることがわかります.

k近傍法では,注目波形パターンが,訓練データ中の波形パターン群の中でk番目に近いものと,どれだけ異なるかを表しています.
この手法は,綺麗な周期的データが予測される場合には有用ですが,周期的に繰り返しつつも長期的に上昇していくようなトレンドを持つ波形の場合にうまくいきません.

特異スペクトル変換法による変化点検知

一方,ある点の前後の波形を比較して変化点検出を行う特異スペクトル変換法という手法は,波形の長期的なトレンドにも強いと考えられます.
特異スペクトル変換法については,こちらのWebサイトにわかりやすくまとまっています.
numpyを用いて特異スペクトル変換法を実装したコードはこちら.

# -*- coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt

def main():
    data = np.loadtxt("your path/qtdbsel102.txt",delimiter="\t")

    train_data = data[1:3000, 2]
    test_data = data[3001:6000, 2]

    w = 50 # width
    m = 2
    k = w/2
    L = k/2 # lag
    Tt = test_data.size
    score = np.zeros(Tt)

    for t in range(w+k, Tt-L+1+1):
        tstart = t-w-k+1
        tend = t-1
        X1 = embed(test_data[tstart:tend], w).T[::-1, :] # trajectory matrix
        X2 = embed(test_data[(tstart+L):(tend+L)], w).T[::-1, :] # test matrix

        U1, s1, V1 = np.linalg.svd(X1, full_matrices=True)
        U1 = U1[:,0:m]
        U2, s2, V2 = np.linalg.svd(X2, full_matrices=True)
        U2 = U2[:,0:m]

        U, s, V = np.linalg.svd(U1.T.dot(U2), full_matrices=True)
        sig1 = s[0]
        score[t] = 1 - np.square(sig1)

    # 変化度をmax1にするデータ整形
    mx = np.max(score)
    score = score / mx

    # プロット
    test_for_plot = data[3001:6000, 2]
    fig = plt.figure()
    ax1 = fig.add_subplot(111)
    ax2 = ax1.twinx()

    p1, = ax1.plot(score, '-b')
    ax1.set_ylabel('degree of change')
    ax1.set_ylim(0, 1.2)
    p2, = ax2.plot(test_for_plot, '-g')
    ax2.set_ylabel('original')
    ax2.set_ylim(0, 12.0)
    plt.title("Singular Spectrum Transformation")
    ax1.legend([p1, p2], ["degree of change", "original"])
    plt.savefig('./results/sst.png')
    plt.show()


def embed(lst, dim):
    emb = np.empty((0,dim), float)
    for i in range(lst.size - dim + 1):
        tmp = np.array(lst[i:i+dim]).reshape((1,-1))
        emb = np.append( emb, tmp, axis=0)
    return emb

if __name__ == '__main__':
    main()

結果のプロットは以下のようになりました.
f:id:Nori_matsu:20170930230428p:plain
波形が乱れている点において,より鋭く異常度のピークが出ています.

どちらの手法でよりうまく異常検知ができるかはデータの性質に依存しますが,k近傍法の方が計算量は少なくて済むようです.

深層学習を用いた異常検知手法

LSTM (Long short-term memory) を用いた手法

時系列データを深層学習させる手法としては,RNNや,それを発展させたLSTMが知られています.
RNNで来月の航空会社の乗客数を予測する
わかるLSTM ~ 最近の動向と共に

LSTMは,与えられた波形から,次の時刻での値を予測するモデルです.
LSTMを用いて時系列データの異常検知をするアプローチとしては,異常例のデータセットが十分に集められるかそうでないかによって,二種類のアプローチがあります.
異常例のデータセットが少ない場合,直前までの波形から予測された値が,実際の値とどの程度異なるかによって,異常判定ができます.
一方,異常例のデータセットが十分にある場合には,LSTMを直接分類器として用いる方法も用いられるようです.
How to use LSTM Networks for time-series anomaly detection - Quora

Autoencoder (自己符号化器) を用いた手法

Autoencoderは,ニューラルネットワークを用いて次元圧縮をする手法であり,入力データをそのまま出力 (=復元) させるように教師あり学習をすることで,信号の特徴を抽出することができます.
deepage.net
正常な波形をデータセットとしてAutoencoderを学習させれば,テストデータとして正常波形を用いた時,元の形に近い波形を出力することができます.
一方このモデルに異常な波形を入力した場合,異常な波形に対する特徴を学習していないために,出力波形は大きく異なるでしょう.
このように,部分時系列データをうまく復元できたかどうかによって,異常判定を行います.

結び

時系列データから異常検知を行うためのアルゴリズムとして,k近傍法,特異スペクトル変換法を実装しました.
また,深層学習を用いたアプローチも紹介しました.
Autoencoderによる異常検知アルゴリズムの実装も行いたいところでしたが,時間切れとなってしまったので今回はここまでで...orz
リアルタイムで異常検知を行うためには,精度のみならず,どれだけの計算時間が許されるかという所にも着目して,アルゴリズムを選択する必要があるようです.


ブレインズテクノロジーでは「共に成長できる仲間」を募集中です
採用ページはこちら