catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

OpenCV4.0をソースコードからインストールする

想像以上に手間取りました。かたりぃなです。

OpenCVのバージョンだけなら大した問題ではなかったのですが、バージョンアップに追従してC++も新しいものにしていこうという動きがあるみたいです。 (実際OpenCV3の世代でCインターフェースはレガシーなものとして切り捨てられましたし。)

さらに4.0になってからはOpenCV自体をビルドするCMakeも要求バージョンが上がっていて、これもビルドしなおす必要があります。

CMakeもビルド自体は簡単なのですが、CMakeをビルドするためには新しいlibcが必要になっています。

新しいlibcが必要ということはgcc自体のアップデートが必要になるということです。

私がよく使うCentOSgccがかなり古いバージョンしか入っていないので、gccからビルドしなおす必要があるということです。

ついにこの時が来てしまったかといった感じですね。。。

というわけで、次の順にやっていきます。

  1. gcc8.1のビルドとインストール
  2. CMake3.13のビルドとインストール
  3. OpenCV4.0のビルドとインストール

なんか大変なことになってしまいましたが、gcc8までいくとC++20も多少は使えるようになるので、未来志向で前向きにいきたいと思います。

環境はいつものです

  • Windows10Pro
  • Docker for windows
  • centos7ベース

躓いたポイント

cmake/opencvとも、いつものようにビルドすればいいのですが一点だけ注意。

cmakeが参照するcurlhttps対応してない問題というのがあるらしいです。

https://stackoverflow.com/questions/51573524/cmake-is-unable-to-download-by-protocol-https-while-own-cmake-option-procedure

要約すると、opencvのビルド時に追加で必要なファイルをダウンロードしてきたりしているところで面倒な問題があるようです。

追加ライブラリののダウンロードにcurlが使われるのですが、cmakeはシステム組み込みのcurlではなくcmake自信が持っているcurlを使うようになっています。(デフォルトの動作)

これだけなら大したことないのですが、cmakeが持っているcurlと不随するライブラリが古いらしく、httpsサイトからのダウンロードに失敗します。(SSLの一部をサポートしていないため)

これがopencvのbuild時に発覚するので、ビルド時間の長さも相まって手戻りが半端なかったです。

環境構築のためのdockerfileはベタ書きしたので、他の環境でやるときも手順書の代わりになるかと思うので、次回以降はビルドやり直しの手間は減るかなと思います。

docker-file

まずは答えから。

インストールするものをまとめてコンテナ化するdockerfileになります。

ひたすらコマンドを実行するだけのものです。

RUN yum groupinstall -y 'Development tools'
RUN yum install -y git wget cmake
RUN yum install -y sudo

# cmakeのためにlibcとgccを入れる(C++最新を使いたい)
RUN wget http://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-8.1.0/gcc-8.1.0.tar.gz
RUN tar zxvf gcc-8.1.0.tar.gz
RUN cd gcc-8.1.0; ./contrib/download_prerequisites; mkdir build
RUN cd gcc-8.1.0; ./configure --enable-languages=c,c++ --prefix=/usr/local --disable-bootstrap --disable-multilib; make;
RUN cd gcc-8.1.0; make install
RUN cp gcc-8.1.0/x86_64-pc-linux-gnu/libstdc++-v3/src/.libs/libstdc++.so.6.0.25 /usr/lib64/
# libcのバックアップとインストール
RUN mv /usr/lib64/libstdc++.so.6 /usr/lib64/libstdc++.so.6.backup
RUN ln -s /usr/lib64/libstdc++.so.6.0.25 /usr/lib64/libstdc++.so.6
RUN rm gcc-8.1.0.tar.gz

# opencv4のためにcmake 3.5以上が欲しい
RUN yum install -y git wget cmake
RUN yum install -y sudo curl-devel zlib-devel
RUN wget https://gitlab.kitware.com/cmake/cmake/-/archive/v3.13.4/cmake-v3.13.4.tar.gz
RUN tar zxvf cmake-v3.13.4.tar.gz
RUN cd cmake-v3.13.4; ./bootstrap --system-curl && make && make install
RUN mv /bin/cmake /bin/cmake.backup
RUN ln -s /usr/local/bin/cmake /bin/cmake
RUN rm cmake-v3.13.4.tar.gz

# c++ opencv
RUN git clone https://github.com/opencv/opencv.git /root/opencv; cd /root/opencv/; git checkout -b 4.0.0 4.0.0 ;
# contrib入れると大きすぎてdockerのデフォルト容量をオーバーして失敗することがあるので、一旦除外
#RUN git clone https://github.com/opencv/opencv_contrib.git /root/opencv_contrib; cd /root/opencv_contrib; git checkout -b 4.0.0 4.0.0 ;
#RUN mkdir /root/opencv/build; cd /root/opencv/build; cmake -D OPENCV_EXTRA_MODULES_PATH=/root/opencv_contrib/modules ..
RUN mkdir /root/opencv/build; cd /root/opencv/build; cmake ..
RUN cd /root/opencv/build; make; make install;

このdockerfileの注意点として、OpenCVのcontribはつけてません。

contribはやたら大きいので、手元の環境ではdocker desktop for windowsのコンテナサイズを超えてしまいました。 なので、一旦contribなしでいきました。

各パッケージのinstall後でファイルを削除しているのも同様の理由です。

動作確認

適当にコード書きます。

OpenCVのビルド情報を表示するだけのものです。

#include <iostream>

#include <opencv2/opencv.hpp>

int main(int argc, char*argv[])
{
    auto buildinfo = cv::getBuildInformation();
    std::cout << buildinfo;
    return 0;
}

ビルドします。ここでも注意点が1つ。

OpenCV4.0ではpkg-configのサポートを終わらせるかどうか議論しているようです。

(自分でビルドとインストールするならパスは分かっているので気にしなくていいのですが、cmake使わない人=パッケージマネージャな人?だとどうするんだとか)

https://github.com/opencv/opencv/issues/13154

どうやらcmakeのオプションにpkgconfig使うように指定すれば、pcファイル吐いてくれるので、それを使えばよさそうです。

今回は自分でビルドしてインストールパスもわかっているので、直接指定します。たとえばこんな感じで。

g++ test.cpp -I/usr/local/include/opencv4/ -L/usr/local/lib64/ -lopencv_core

動かしてみる

ここでもpkg-configがsoを探してくれないので、ちょっとばかり対応が必要です。

出来上がった実行ファイルをそのまま実行すると、soが見つからないっていわれます。

[root@ce33ad34d4ba src]# ./a.out
error while loading shared libraries: libopencv_core.so.4.0: cannot open shared object file: No such file or directory

新しいライブラリ入れるとよくある現象ですね。soを参照できているか見てみます。

[root@ce33ad34d4ba src]# ldd a.out
        linux-vdso.so.1 =>  (0x00007fffd5d80000)
        libopencv_core.so.4.0 => not found
        libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f51622b6000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f5161fb4000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f5161d9e000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f51619d1000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f5162639000)
[root@ce33ad34d4ba src]# ./a.out

確かにopencvがnot found.ですね。ここでもパスは分かっているので、環境変数を指定して実行します。

LD_LIBRARY_PATH環境変数opencv*.soの置かれているパスを入れて実行。

[root@ce33ad34d4ba src]# LD_LIBRARY_PATH=/usr/local/lib64/ ./a.out

General configuration for OpenCV 4.0.0 =====================================
  Version control:               4.0.0

(略)

やったね。

トラブルシューティング

libcのバージョンが古いときは、共有ライブラリの中を確認する必要があります。

strings /usr/lib64/libstdc++.so.6 | grep GLIBCXX

これで該当するバージョンが入っているかどうか調べることができます。

gccとlibcのバージョンアップではこちらのサイトを参考にさせていただきました。

https://www.saintsouth.net/blog/update-libstdcpp-on-centos6/

感想と今後の展望

サーバ側のOpenCVは最新にすることができました。これでOpenCVのONNIXが利用できるようになります。

ONNNIXはDeepLearningの各種フレームワーク間でデータを受け渡せるようにするフォーマットで、今後はサーバ側はC++/OpenCVだけでいけるかもしれません。

とはいえサーバ側をすべてC++で書くのは骨が折れるので、PythonバインディングOpenCV叩いたほうが早い気がしなくもないです。

あとはC++が最新版になったので、いろいろと使いたい機能が使えるようになりました。

C++も20になって関数型言語っぽい香りが本格的になってきたので、時間をとっていろいろ試したいと思います。

それでは今回はこれくらいで。

websocketとhttpの共存Webアプリを作る(nginx + uwsgi + flask)

そろそろWebSocketやってみようと思いました。かたりぃなです。

まずWebSocketを概念レベルで整理してから、技術的な実装をやっていきます。サンプルコードコピペしたい人は後半まで飛んでください。(未来の自分へのメッセージ)

WebSocketの概要

WebSocketはウェブアプリケーションでの双方向通信を実現するためのプロトコルです。

と一言で表現しても抽象的すぎます。このままでは何が美味しいのかわからないので幾つかの方向から分析してみます。

HTTPとの比較

HTTPは基本的にRequest-Responseという1組のメッセージのやり取りで構成されるシンプルなものです。

クライアントからのRequestがシーケンスの起点となっているため、クライアント→サーバのメッセージは任意のタイミングで実行できますが、サーバ→クライアントのメッセージは色々と難しいという問題があります。

クライアントがポーリングするとか、ロングポーリングという解決策もありますが、HTTPのヘッダを毎回送信するのとか、ロングポーリングの時間の問題とかいろいろあったりします。

んで、この問題を解決する方法の1つがWebSocketです。

WebSocket使わずにTCPレベルでやりとりすればいいのでは?

WebSocket以外の解決策を考えた時、実装レイヤを下げてトランスポート層(TCP,UDP)でやるのもテかと思います。

しかしそうしてしまうと面倒な問題が増えてしまいます。すぐ思いつくのは疎通できるかどうかの問題です。

疎通できるかどうかで一番大きなものはルータ越えの問題(NATトラバーサル)とかでしょうか。 昔のPCでやるMMORPGなんかで見かけた「ポートxxを通過できるようルータやファイアウォールを設定してください」とかいうやつですね。

最近だとルータのUPnPを叩いてポートマッピングを自動でやったりとかが多いようですが、Webアプリで双方向通信をするためにそういうのを使うのは大げさすぎます。

なので、そんな大げさなことはせず、せっかくWeb標準としての通信規格として存在しているWebSocketを利用するのが現時点では最良と考えます。

実際のプロトコル

誤解を恐れずにいうなら「HTTP上に構成されるソケット通信プロトコル」といったところでしょうか。

実際のシーケンスとしては

  1. クライアントはHTTPリクエストに「WebSocket使いたいよ!」というUpgrade要求を含めて、サーバへ要求
  2. サーバはアップグレード要求に対する応答を返す
  3. WebSocket通信開始
  4. クライアント/サーバとも任意のトリガでソケットをread/writeすればいい

これでNATトラバーサルの話はこの時点で既に半分はクリアしていることになります。 HTTP上に構成されるプロトコルなのでWebページを見る場合と何ら変わりません。

少々気を付けるポイントとしてKeepAlive的なところで、WebSocket接続を維持するために一定時間ごとにメッセージを送受信する必要があります。

ただし、いくつかのWebSocket対応ライブラリでは自動で面倒見てくれるようになっています。

今回利用するuwsgiも自動でやりますよと明記されています。

WebSocketを試してみる

実験環境はいつものです。

システム構成

nginxをリバースプロキシとして、HTTPページとWebSocketページに振り分けます。

HTTPとWebSocketのどちらもWebアプリケーションのフレームワークとしてflaskを使用しますが、実際にはルーティング程度しかやっていません。

flaskをむき出しで運用するのはよくないので、アプリケーションサーバとしてuwsgiを使用することにします。

uwsgiとアプリケーション間のプロトコルは色々選択できるようになっています。今回はHTTPでやっちゃいます。

nginxの設定

ホスト名を見て振り分けたいところですが、ホストOSのhosts設定とか面倒なのでポート番号で振り分けることにしました。

HTTPの場合は何もせずHTTPサーバへ転送します。

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    upstream uwsgi {
        server uwsgi:3031;
    }

    # FlaskによるHTTPサーバ
    server {
        listen 8080;
        charset utf-8;

        location / {
            include uwsgi_params;
            uwsgi_pass uwsgi;
        }
    }

    upstream websocket {
        server uwsgi-ws:3032;
    }

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    # WebSocket
    server {
        listen 8081;
        charset utf-8;

        location / {
            proxy_pass http://websocket;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }
    }
}

一個だけポイントがあって、WebSocketホスト側の転送設定でproxy_set_headerしています。

これはnginxをWebSocketのリバースプロキシとして使うとき、upgradeメッセージを適切にバックエンドに渡してあげる必要があるためです。

詳細はnginxのマニュアルを参照。

https://www.nginx.com/blog/websocket-nginx/

UWSGI

uwsgiはアプリケーションサーバです。cgi関連の実装にWSGIというのがありますが、それとは別物です。

初めてuwsgiの名前を知った時、u=μ(マイクロ)だと思って「軽量なwsgiプロトコルかな?」とか思ってしまいましたが、そうではなかったようです。

今回のuwsgiの役割はpythonアプリケーションのホスティングです。

今までflaskむき出しでテストしていましたが、これからはflaskむき出しではなく、サービス化も目指してこいつを使っていこうと思います。(実際、flaskのビルトインサーバは実運用では非推奨なので。)

uwsgi環境を用意するdockerfileはこうなりました。(後述の参考URLの内容ほぼそのままです)

# ベースイメージ
FROM python:3.6

RUN mkdir /var/www
# workdirの指定
WORKDIR /var/www

# 依存Pythonライブラリ一覧コピー
COPY requirements.txt ./

# 依存Pythonライブラリインストール
RUN pip install --no-cache-dir -r requirements.txt

# ログ出力用ディレクトリ
RUN mkdir /var/log/uwsgi/

CMD ["uwsgi","--ini","/var/www/uwsgi.ini"]

依存ライブラリを指定するrequirements.txtには

Flask
uwsgi

を書いておけばOKです。

UWSGIを構成する

今回は1つのuwsgiで1つのアプリケーションをホスティングします。実質、2つのuwsgiを立てます(http用,websoket用)。

1つのuwsgiで複数のWebアプリを管理する方法がまだわからない&dockerコンテナなら1コンテナ1役割のほうがいいだろうという考えです。

コンテナならこういうのを軽く実現できるから良いですね。

コンテナ起動時にuwsgi.iniを与えて、それに従って動作します。

たとえばwebsocket側のuwsgiの設定はこうなりました

[uwsgi]
wsgi-file = /var/www/src/uwsgi_test.py
callable = app
master = true
processes = 1
http=0.0.0.0:3032
vacuum = true
die-on-term = true
py-autoreload = 1
http-websockets = true
log-5xx = true
async = 100
ugreen = true

HTTP側はこんな感じです。

[uwsgi]
wsgi-file = /var/www/src/run.py
callable = app
master = true
processes = 1
http=0.0.0.0:3031
chmod-socket = 666
vacuum = true
die-on-term = true
py-autoreload = 1

注意点として、このdockerfileでインストールされるuwsgiはasyncとupgreenパラメータの設定が必要でした。 バージョンごとにasync対応かどうかみたいなのがあるみたいですね。

flask(HTTP)

単純な仕組みなのでコードだけ記載して説明は省略します。 hello worldを返すだけのアプリケーションです。

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

flask(WebSocket)

こちらも単純な仕組みなのでコードだけ記載して説明は省略します。 これで一旦動くとこまで来ましたが、なんか違う気がしていてちょっと自信ないです。

import uwsgi
import time
import logging
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    app.logger.setLevel(logging.INFO)
    app.logger.info('flask document root accessed')
    uwsgi.websocket_handshake()
    while True:
        msg = uwsgi.websocket_recv()
        app.logger.info('uwsgi.websocket_recv result = {}'.format(msg) )
        uwsgi.websocket_send(msg)
        app.logger.info('uwsgi.websocket_send finish.')

if __name__ == '__main__':
    app.run(debug=True)

docker-compose

ここまでに作成した3つのコンテナを立てたり落としたりするdocker-composeです。

今回は実験用なので、nginx, uwsgi(websocket), uwsgi(http)の3つのホストすべてを同じネットワークに接続しています。

各サービスでnetworksを記述することで、そこに接続されたホストと通信できるようなネットワーク構成ができます。

version: "2"
services:

  # HTTPのWebアプリケーション
  uwsgi:
    build: ./app
    volumes:
      - ./app:/var/www/
    networks:
      - python-website
    ports:
      - "3031:3031"
    environment:
      TZ: "Asia/Tokyo"

  # WebSocketのWebアプリケーション
  uwsgi-ws:
    build: ./appws
    volumes:
      - ./appws:/var/www/
    networks:
      - python-website
    ports:
      - "3032:3032"
    environment:
      TZ: "Asia/Tokyo"

  # WebFront
  nginx:
    build: ./nginx
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    networks:
      - python-website
    ports:
      - "8080:8080"
      - "8081:8081"
    environment:
      TZ: "Asia/Tokyo"

networks:
  python-website:

client

WebSocketのサーバを叩くのはjsからやることにしました。ほとんど書いたことありませんが。

本当は使い慣れた言語(C++とか)から叩きたかったのですが、仕様とか調べるのが面倒だったので。

jsのコードはこちらのページを参考にさせていただきました。

https://qiita.com/hiroykam/items/c3e3d20c223b01d9f0e8

<!DOCTYPE HTML>
<html>
  <head>
    <script type="text/javascript">
      function WebSocketTest()
      {
        if ("WebSocket" in window) {
          alert("WebSocket is supported by your Browser!");
          var ws = new WebSocket("ws://localhost:8081/");
          ws.onopen = function() {
            ws.send("Hello from client");
            alert("Message is sent...");
          };
          ws.onmessage = function (evt) {
            var received_msg = evt.data;
            alert("Message is received...");
            alert(received_msg);
          };
          ws.onclose = function() {
            alert("Connection is closed...");
          };
        } else {
          alert("WebSocket NOT supported by your Browser!");
        }
      }
    </script>
  </head>
  <body>
    <input type="button" onClick="WebSocketTest();" value="WebSocket Test">
  </body>
</html>

動作確認

上記htmlを開いてボタンを押せば順にアラートが表示されます。

サーバ側のログを見たい場合はdocker-compose logsとかdocker log コンテナ名で確認できます。

websocket側のログにwebsocketで接続受けたとかメッセージ受けたとか残っています。

感想と今後の展望

ひとまずWebSocketの最低限の基盤が整いました。

今後試していきたい内容は

  • 具体的なデータ送受信(たとえばバイナリデータや、圧縮された映像、音声データのようなフレーム単位での時系列データなど)
  • ポートでHTTP/WebSocket分岐ではなく、flaskのルーティングで分岐したい
  • もう一歩進んでWebRTC

などでしょうか。

特にARでの利用を考えたときに物体検出などは時系列データである動画のほうが有利です。(空間方向だけでなく時間方向も推定する)

ただし、動画にしてしまうとエンコード時に多少なりとも情報が落ちる&処理の負荷があがるので、どうするのが最適かとか考えていきたいですね。

(よくある動画フォーマットならデコードせずに周波数空間で物体検出できるのでは?とか思っていますが、コード書くの面倒すぎるのでたぶんやらないと思います。)

サーバースペックとの兼ね合いもあると思うので、

それでは今回はこれくらいで。

DockerでJenkinsとテスト用のコンテナを動作させる

DockerでCI/CD環境を構築してみます。かたりぃなです。

どうやってやるか?

今回はCI実行環境やアーキテクチャについて考えます。 まずCIといえばJenkinsやTravisとかいろいろありますが、次の機能さえ備えていれば良いと思います。

  • 繰り返しテストを再現可能な状態で実行できること(CI)
  • 任意のトリガでデプロイできる(CD)

というわけで、使い慣れているJenkinsでいきます。

Jenkins環境はプラグインとか入れ始めると汚れてくるので、立て直ししやすいようDockerコンテナとして実行します。

JenkinsコンテナからホストOSに接続して、ホスト上からテストコンテナを立ち上げてテスト実行するということをやってみます。

これができると何が良いのか?

主に次の点でメリットがあると考えます。

  • テスト環境を常にクリーンな状態に保てる
    • jenkins, テスト対象コンテナともに
  • ローカルマシンでCIできる
  • CDもできるはず
  • コンテナのオーケストレーションのテストもできる

環境

環境ですが、Hyper-Vの有無で少々環境が異なります。 Windows10側は少々手こずっていて納得いっていない状態ですが、一旦良しとします。

Windows10 + Hyper-V

この環境ではdocker-desktopを稼働させます。 https://www.docker.com/products/docker-desktop ホストOSはHypser-V上のMobyLinuxとなるのですが、こいつへの接続がややこしいので、WindowsでOpenSSHを動かしてここからMobyLinuxのdockerを操作することにします。

いつの頃からかWindows10でもssh-serverが使えるようになっているので、設定からインストールして、設定しておきます。 Windows10のSSHのユーザーとパスワードはWindowsログイン時のものそのままです。

Windows7 + VirtualBox

こちらはいつもの環境です。 https://docs.docker.com/toolbox/toolbox_install_windows/ コンテナからホストへはNATのIPで接続できるので簡単でした。

詳細

ここからはdocker-toolboxでの説明です。

jenkinsに入って他の仮想マシンを立てるところまでやってみます。

Jenkinsコンテナを立てる

ssh接続のパス入力を自動でやりたいので、追加でパッケージを入れておきます。 docker-toolboxのdocker-engine側にはdocker-composeが入っていないので、いつでも追加できるようにここで保持させることにしました。

dockerfileはこうなりました。

FROM jenkinsci/jenkins:2.11

# install via apt
USER root

# install jenkins plugins
COPY plugins.txt /usr/share/jenkins/plugins.txt
RUN /usr/local/bin/plugins.sh /usr/share/jenkins/plugins.txt

# install docker-compose
RUN curl -L https://github.com/docker/compose/releases/download/1.24.0-rc1/docker-compose-Linux-x86_64 > /usr/local/bin/docker-compose
RUN chmod +x /usr/local/bin/docker-compose

# install ssh tools
RUN apt-get update
RUN apt-get -y install sshpass


# drop back to the regular jenkins user - good practice
USER jenkins

このdocker-fileを使うdocker-composeはこうなりました。

version: "3"
services:
jenkins-CI:
    build: .
    image: jenkins
    ports: 
    - "12380:8080"
    - "12322:22"
    volumes:
    - "./jenkins_home:/var/jenkins_home"
    - "./test_container:/var/test_container"

立ち上げます

docker-compose up -d

これでhttp://localhost:12380に接続すればjenkinsの初期パスワード入力画面が表示されます。 接続できない場合はHyper-Vのネットワーク設定、VirtualBoxならNAT設定あたりを見直します。

ちなみに、この設定だとdockerコンテナを再起動かけるとジョブが動かなくなってしまいます。 理由はvolumesでしているのはホスト側のパス(Windowsファイルシステム)なので、パーミッションがおかしくなるためです。 対策は後述。(volumeを使う)

docker-containerへのログイン

まずdockerのお約束的にjenkinsコンテナに入ります。 初期パスワードがjenkinsのホームディレクトリ以下に置かれているのでそれを見るために必要です。

docker exec -it コンテナ名 /bin/bash

cat /var/jenkins_home/secrets/initialAdminPassword これがjenkinsの初期パスワードです。

jenkinsコンテナから、テスト用のコンテナを立ち上げる準備

jenkinsでジョブ作れるところまできたので、 jenkinsからDockerコンテナを立ち上げる設定をします。 とはいえJenkins自体がコンテナで動作しているので、そのままではコンテナ in コンテナになってしまいます。

ここではJenkinsコンテナが動作している仮想マシン(docker-engine)に接続して、docker-engine側からコンテナを立ち上げてもらうことにします。

docker-engineに接続する

dockerが動作しているVM(デフォルト設定だとdefaultというマシン名)に接続するため、engine側のIPを調べます。

docker-containerからホスト(docker-engine)のIPを調べるのは、NATの対向IPなのでこうすればよいです。

ip route | awk 'NR==1 {print $3}'

つまりdocker-engine(ホスト)にSSHするには

ssh docker@上記で調べたIP

となります。

sshのパスワードはtcuserです。

sshのパスワード自動入力とフィンガープリントチェックの(Y/N)なしも含めるとSSHコマンドはこうなります。

machine_ip=`ip route | awk 'NR==1 {print $3}'`
sshpass -p tcuser ssh -oStrictHostKeyChecking=no docker@${machine_ip}

あとはdocker-composeをすればよいです。 他のコンテナを起動するスクリプトを書くとこうなります。

このスクリプトの前提条件は2つです

  • docker-engineにdoker-composeがインストールしてあって、パスも通っている
  • /home/docker/test_containerにdocker-compose.ymlが置かれている
#!/bin/bash

# docker-engineへの接続情報
machine_ip=`ip route | awk 'NR==1 {print $3}'`
sshuser="docker"
sshpass="tcuser"

# ホスト上でのテストスクリプト置き場
compose_path="/home/docker/test_container"

function docker_compose(){
    sshpass -p ${sshpass}\
        ssh -oStrictHostKeyChecking=no ${sshuser}@${machine_ip}\
        "cd ${compose_path}; docker-compose ${1}"
}

case "$1" in
  start)
        docker_compose "up -d"
        echo "Start"
        ;;
  stop)
        docker_compose "down"
        echo "Stop"
        ;;
  *)
        echo $"Usage: {start|stop}"
esac

docker-engineにdocker-composeをインストールする

docker-engineとは、ざっくりいうとVirtualboxから見えるマシンに相当します。 このマシンにはdocker-composeが入っていないのでインストールする必要があります。

上記jenkinsのdockerfileで持ってくるようにしたので、jenkins起動時にscpで転送してもよさそうです。

念のためインストール手順をメモします。

ここを確認する

https://github.com/docker/compose/releases

ダウンロードURLをクリップボードにコピーしておきます。

docker-engine上でrootに昇格

ダウンロードしたファイルは/usr/local/binに実行ファイルを配置したいのでrootに昇格させておきます。

sudo -i

rootになった状態で次のコマンドを打ちます。

$ curl -L https://github.com/docker/compose/releases/download/1.24.0-rc1/docker-compose-Linux-x86_64 > /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose

これでDocker用の仮想マシン上でdocker-composeコマンドが使えるようになりました。

動作確認

$ docker-compose

使い方のメッセージが出てきたらOKです。

あとは

  1. jenkinsからdocker-engineにssh
  2. doker-engine上でdocker-composeしてコンテナを起動する
  3. 必要なテストをする
  4. docker-compose downして環境をきれいにする

というジョブを書くことでいつでもきれいな環境でテストが実行できます。

2回目以降Jenkinsが立ち上がらない対策

最初に書いたJenkinsのdocker-compose.ymlでは二回目以降docker-compose up -dするとジョブが流せなくなります。 これはWindows側にJenkinsの各種ファイルを保存するので、パーミッションがおかしくなるためです。 具体的にはジョブの実行結果が書けないとか、ジョブ設定ファイルが読めないとか。

対策として、dockerにはデータを保存するボリュームという機能があるので、これを使うことになります。

データボリュームコンテナというのもあったらしいですが、現在はオススメじゃないらしいので今回はボリュームを使います。

データボリュームを使うdocker-compose.yml

これだけです。

version: "3"
services:
  jenkins-CI:
    build: .
    image: jenkins
    ports: 
      - "12380:8080"
      - "12322:22"
    volumes:
      - jenkins_data:/var/jenkins_home
      - "./test_container:/var/test_container"

# 上記コンテナで使用するデータボリュームを指定
volumes:
  jenkins_data:

ポイントはservicesと同じトップレベルにあるvolumesで、docker-compose up -dすると自動的に作成されます。 名前を付けてボリュームが作られることから、名前付きボリュームというとのこと。

コンテナのvolumes指定ではこの名前を指定してマウントします。

ボリュームってどこにあるの?

上記設定でボリュームにデータが保存されます。

テストのログなんかはとっておきたいので、念のためたどこに置かれているか確認する必要があります。

ボリュームの場所は次のように確認します。

$ docker volume ls
local               jenkins-ci_jenkins_data
$ docker volume inspect jenkins-ci_jenkins_data
[
    {
        "CreatedAt": "2019-02-05T23:58:11Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "jenkins-ci",
            "com.docker.compose.version": "1.24.0-rc1",
            "com.docker.compose.volume": "jenkins_data"
        },
        "Mountpoint": "/mnt/sda1/var/lib/docker/volumes/jenkins-ci_jenkins_data/_data",
        "Name": "jenkins-ci_jenkins_data",
        "Options": {},
        "Scope": "local"
    }
]

どうやら /mnt/sda1/var/lib/docker/volumes/jenkins-ci_jenkins_data/_data にあるらしいですね。

rootじゃないと読み書きできないので、sudo -iしてから確認してみましょう。

前述のパスワードファイルとかよさそうです。

ls /mnt/sda1/var/lib/docker/volumes/jenkins-ci_jenkins_data/_data/
config.xml                           jobs                                 secrets
copy_reference_file.log              logs                                 updates
hudson.model.UpdateCenter.xml        nodeMonitors.xml                     userContent
hudson.plugins.git.GitTool.xml       nodes                                users
identity.key.enc                     plugins                              war
init.groovy.d                        secret.key                           workflow-libs
jenkins.install.UpgradeWizard.state  secret.key.not-so-secret

jenkinsのファイルが一通り入ってそうですね。

これでパスワードファイルも読めます。

cat /mnt/sda1/var/lib/docker/volumes/jenkins-ci_jenkins_data/_data/secrets/initialAdminPassword

最後に、あらかじめ以下ボリュームを作成しておくこともできるようです。$ docker volume create --name jenkins_data

感想と今後の展望

CI/CD環境が整ってきました。 これでサーバーサイドは好きなようにテスト流せますし、複数台の場合のオーケストレーションテストも実施できそうです。

dockerでElasticsearch+logstash+kibanaを立ち上げる

新年一発目のエントリは少々新しいことに挑戦しようということにしました。かたりぃなです。

今回はElasticsearchとC#でのログ分析基盤の作成をやってみます。

まだ実践的なことは何もできていない状態ですが、できたことを少しずつブログに残していこうと思います。

(完成してからとかやっていると、昨年末みたいに更新頻度が下がってしまいますので。)

今回利用するパッケージや技術要素はこんな感じです。

  • Elasticsearch
  • kibana
  • logstash
  • docker
  • .Net core(C#)

なぜログ分析が重要か

ログというのは、何が起きたかを分析するのに重要です。 特に問題が起きた場合にはログから分析する場合がほとんどで、これは分野を問わず同じだと思っています。(WebならApacheログ、組み込みならカーネルログとか)

ログ分析で得た結果は、エンジニア・経営どちらにも波及していくものだと考えています。

具体的には

  • サービスの構成や仕組みを変える必要がある
  • サービス運用方針を変える

などです。

エンジニア視点と経営視点でそれぞれ何をしたいかを明確にします。

エンジニア視点

不具合調査などで必要です。

私が考えているカードゲームARのサービス構成では、複数のAPIとクライアントアプリが存在してるので、バグなどが見つかった場合それぞれのログを順に見ていく必要があります。 これが結構手間です。たとえば、、、

  • Hololensのログを抽出して、APIを叩いた順序を確認
  • 叩かれたサービス(物体検出 or カテゴリ分類)とパラメタを特定
  • 各サービスのログを収集
  • ログを結合して、全体のシーケンスを見直す

と、いろいろと面倒です。

一か所で集中的にログを解析できればもっと効率上がると思いますし、サービス開始した後でもこういったことは必要になってきますので、今のうちに基盤を整えたいといったところです。

経営視点

個人開発サービスではリスクをできるだけ避けたいです。

リスクを避けるためにはスモールスタートが基本だと思っていて、ユーザーが増えてきたらインスタンスを増やすなどの対策をしていけばよいと考えます。 この「ユーザーが増えてきたら」というのはログ分析などからも得られるので、ログはどんどん蓄積していきたいところです。

なぜElasticSearch?(しかもC#)

ログ収集と分析に使うOSSは個人的には何でもよくて、AWSGCPなどのクラウドサービスでも提供されているログ監視サービスでもいいんじゃないかなと思っています。

Elasticsearchでなければならないということは全くありません。本業でも応用できる可能性があるためですが、これも確定ではないので本当に好みの問題かと思います。

クライアントソフトの実装言語としてC#を選んだ理由も特にありません。しいて言うならHololens+Unityでも使うから、少しずつ慣れたいなといったところです。

Elasticsearchを立ててみる(dockerコマンドで直接)

早速立ててみます。 Elasticsearchはdockerイメージが公開されているので、チュートリアルどおりにやればOKです。

https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-prod-cluster-composefile

手元の環境は次のとおりです

ElasticSearchをDockerで動かして、Windows上の.NetCoreからこのDockerを叩いていきます。

さっそくDockerコンテナを動かしてみましょう。

docker pull docker.elastic.co/elasticsearch/elasticsearch:6.5.4

docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.5.4
# ログがいっぱい流れる
# OpenJDKがAVX要求してるけど、マシンがAVX対応してないからオプション指定しろとか
# OpenJDKのオプションにdeprectedなのが指定されてるからうんたら
# あとは色々な情報

これでElasticSearchは立ち上がったみたいなので、まずはステートを問い合わせてみます。 WSLのbashからcurlでいきます。

curl http://127.0.0.1:9200/_cat/health
1472225929 15:38:49 docker-cluster green 2 2 4 2 0 0 0 0 - 100.0%

チュートリアルのとおりですね。ひとまず立ち上がったようです。 次のステップとしてdocker-composeで起動できるようにしたいので、コンテナは一旦落とします。

Elasticsearchを立てる(docker-compose)

公式のインストールガイドにも書かれていますが、そのままやればOKです。 ただし公式のガイドでは2つのコンテナでクラスタを組むようになっている点に注意です。

ここでは入門として、一台のdockerで最低限の設定だけで立てることにします。つまり、先のコマンドをdocker-compose化するだけです。

version: "3"
services:
  elastic-search:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.5.4
    ports: 
      - "9200:9200"
      - "9300:9300"
    environment:
      - discovery.type=single-node
    volumes:
      - "<windows側の適当なディレクトリ>:/usr/share/elasticsearch/data"

できました。 このファイルをdocker-compose.ymlとして保存し、

docker-compose up -d

で起動します。

動作確認コマンドは先のものと同じCurlからのチェックで、注意点は2点です。

  1. デタッチモードなのでElasticSearchの起動時ログが出てきません。 ログなしだと起動完了したか見えなくて困りますが、elasticsearchが起動するまで数秒かかるので、起動完了を待ちます。 先のコマンドをしばらく叩いていればそのうち応答返ってくるようになります。
  2. Elasticsearchは複数インスタンスクラスタ組むのが基本らしく、ステートがyellowと返ってくることがあります。 勉強段階であれば単一インスタンスのほうが色々とやりやすいので、今回はstate=yellowでも良しとします。

ログ収集用のクライアントを作成するための準備

ログ収集といえばfluentdですが、入れるの面倒なので後回しです。 一旦SCPでAWS上からコピーしてくることにします。 .NetCoreの勉強もしたいので、C#でいきます。

プロジェクトを作る

.NetCoreのプロジェクトを作って、必要なパッケージを入れます。

dotnet new console
dotnet add package SSH.NET --version 2016.1.0 
dotnet add package Elasticsearch.Net --version 6.4.1 
dotnet add package NEST --version 6.4.1

プロジェクトディレクトリをVSCodeで開くとデバッガもインストールされます(選択肢でYesを選択すればよい) デバッガは本家VisualStudioと同じ感覚で操作できます。

scpでログ収集してくる

AWSに接続してログを集めてくることにします。 SSH.NETでだいたい面倒見てくれるので、外からパラメータ与えてAPIを順に叩いていくだけです。

  • SCPで接続するホスト、ポート、ユーザー、秘密鍵を指定して設定を行う
  • SSHClientに設定を渡して接続する
  • SSHClientのDownloadでファイルをダウンロード -> ストリームになる
  • ストリームからローカルファイルに保存する

といった流れです。

using System;
using System.IO;
using System.Linq;
using Renci.SshNet;
using Elasticsearch;
using Nest;

  // memo : ここらでHostAddressをとかを適当に設定する

  static void Main(string[] args)
  {
    // SSH接続のための設定
    var ConnNfo = new ConnectionInfo(HostAddress, HostPort, SshUser,
        new AuthenticationMethod[]{
            new PrivateKeyAuthenticationMethod(SshUser,new PrivateKeyFile[]{
                new PrivateKeyFile(SshPrivateKeyFileName, SshPassword)
            }),
        }
    );

    // SCPでログ収集
    using (var client = new ScpClient(ConnNfo)){
        client.RemotePathTransformation = RemotePathTransformation.ShellQuote;
        client.Connect();
        try
        {
            if (!client.IsConnected)
            {
                Console.WriteLine("[NG] SSH Connection failed!!");
            }
            using (var ms = new MemoryStream())
            {
                client.Download("ログファイル名", ms);
                using (FileStream file = new FileStream("testfile", FileMode.Create, FileAccess.Write)) {
                    ms.WriteTo(file);
                }
            }
        }
        finally
        {
        }
    }
  }

コードを書いたらF5でデバッグして、問題なければ次のコマンドで実行できます。

dotnet run

C#からElasticSearchを叩く

dotnet用のパッケージが入っているので、それを使って叩くことができました。

気を付けるポイントとして、ElasticSearchのクライアントは単一ホストで構築されたクラスタであってもコレクションを返してくる(要素数=1)ため、Linqで片づけます。

Linq初めてなのでよくわかりませんが、このあたりをよしなにしてくれるもののようです。

コード

using System;
using System.IO;
using System.Linq;
using Renci.SshNet;
using Elasticsearch;
using Nest;

  static void Main(string[] args)
  {
    //Elasticsearchの状態確認
    var elastic_settings = new ConnectionSettings(new Uri("http://127.0.0.1:9200"));
    var esclient = new ElasticClient(elastic_settings);
    var health = esclient.CatHealth().Records.SingleOrDefault();
    Console.WriteLine("status : " + health.Status);
  }

これで "status : green"と返ってくればOKです。C#からElasticsearchを叩く用意が整いました。

ログを注入する用意

Elasticsearch単体では大した事はできません。ただのRDBだけでは面白くもなんともないのと同じです。

ログの集計等を試したいところですが、そのためにはログが必要ですので注入していきましょう。

どうやってログをElasticsearchに入れていくのか

方法は色々あるみたいで、軽く調べた感じ次の方法がありました

  • ElasticsearchのAPI
  • fluentd
  • logstash

まあfluentdもlogstashもAPI叩いているのだろうとは思うので、使うとしたらこのどちらかかなと思います。

今回はlogstashを使ってみます。

logstashはElasticsearch開発元のElastic社が作ってるので、公式リファレンスが充実していて、Elasticsearchと組み合わせたときに問題が起きにくいと考えられます。

たとえばElastic公式ではこういうdockerサンプルを出してたりするので、何か困ったらこいつを動かせば済むと思います。

https://github.com/elastic/logstash-docker

logstashのインストールと設定

docker-compose.ymlにlogstashも追加するとこんな感じのファイルになりました。

version: "3"
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.5.4
    ports: 
      - "9200:9200"
      - "9300:9300"
    environment:
      - discovery.type=single-node
    volumes:
      - "./data:/usr/share/elasticsearch/data"
    networks: 
      - elastic-stack

  logstach:
    image: docker.elastic.co/logstash/logstash:6.5.4
    volumes:
      - "./pipeline/:/usr/share/logstash/pipeline/"
    networks:
      - elastic-stack

networks:
  elastic-stack:

logstashそのままでは意味がないので、一定周期でログを送り続けるパイプライン設定ファイルを食べさせます。

設定ファイルはこれをコピーしてきました。

https://github.com/elastic/logstash-docker/blob/master/examples/logstash.conf

このファイルをdocker-compose.ymlディレクトリをカレントとして、./pipeline/logstash.confに配置します。

logstash側が見てくれる設定ファイルの置き場所は

https://www.elastic.co/guide/en/logstash/current/dir-layout.html

とのことなので、/usr/share/logstash/pipeline/logstash.confに先ほど作った./pipeline/logstash.confが来るようにマウントします。

最後にdocker-compose up -dすればOKです。

logstashの動作確認のためにkibanaをインストール

logstashが5秒周期でelasticsearchにログを送っていると思うので動作確認をしたいです。

先の手順であったサンプルコード見たときに、docker-composeに書いてあるのを見つけたので試してみた次第です。

API叩こうにもまだそこまで理解が進んでいないので、GUIから簡単に操作できるkibanaを使ってみます。 kibanaもelastic社がつくってるので、たぶん動くでしょう。

https://github.com/elastic/logstash-docker/blob/master/templates/docker-compose.yml.j2

pythonのjinjaテンプレート展開でごにょごにょしてますが、必要な設定だけ持ってくればOKでした。

というわけでdocker-compose.ymlはこうなりました。

version: "3"
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.5.4
    ports: 
      - "9200:9200"
      - "9300:9300"
    environment:
      - discovery.type=single-node
    volumes:
      - "./data:/usr/share/elasticsearch/data"
    networks: 
      - elastic-stack

  kibana:
    image: docker.elastic.co/kibana/kibana:6.5.4
    ports:
      - "5601:5601"
    networks: 
      - elastic-stack

  logstach:
    image: docker.elastic.co/logstash/logstash:6.5.4
    volumes:
      - "./pipeline/:/usr/share/logstash/pipeline/"
    networks:
      - elastic-stack

networks:
  elastic-stack:

これでdocker-compose upしたDockerホストの5601にブラウザでアクセスすれば、kibanaの画面が開きます。

ログを確認

GUI操作はSS撮るの面倒なので文字でメモだけ残します。

最低限の設定

managementからkibanaのindex Patternsを設定します。 これはログを成形して内部的に管理しやすい形にするものです。

今回の動作確認をするだけなら"*"で全部見れるようになるので、これで十分です。

ログを見る

discoverから先ほど設定したフィルタを選択します("*")。これで送られてきたログをすべてみることができます。

https://github.com/elastic/logstash-docker/blob/master/examples/logstash.conf

のinputにある文字列がintervalの周期で送られてきていればOKです。

まとめ

今回はサービスを運用していくためのログ分析基盤であるElasticSearchを試してみました。 まだデータは入れていませんが、今後どんどんログ入れていこうと思います。

ただし、無秩序にログを入れると分析の時に泣きを見るので検討してからですね。

dockerなので環境は作って壊してを簡単にできる利点を生かして、どんどん試していきたいと思います。

それでは今回はこれくらいで。

2018年振り返り

年の瀬も迫ってきたので、今年一年を振り返ってみようと思います。かたりぃなです。

よかったこと

ちょっと残念

  • DeepLearning

  • Unity

  • C++オンリーでHololens開発の限界

よかったこと詳細

インフラまわり色々できた

なんやかんやで、今年はインフラをいじってることが多かったと思います。 インフラの具体的な技術要素としては

こんなところでしょうか

2018年に身に着けた技術で特に大きかったのはAWSとansibleですね。

Hololensなどのアプリを作る場合のアーキテクチャ設計を考えたとき、クラウドとの連携は必要だと考えています。 これはHololensに限らずモバイルアプリ全般に言えることです。

そのための土台作りができたということで、かなり大きな前進です。

これらをうまく活用して開発を進めていきたいですね。

開発環境のコンテナ化

ローカル開発環境はdockerを本格的に使うようになりました。

くーべねてぃす?とやらはまだ試していませんが、dockerfile, docker-composeあたりを駆使して色々な環境を試しています。

軽く試したコンテナとしては

などです。

dockerでやってみて個人的にいいなと思ったのが次のものです

  • 環境の固定, 実験の再現のしやすさ
  • マイクロサービス

前者はこのブログでも時々書いてますが、docker-compose up すれば状況を再現できるので、すごく楽です。

後者のマイクロサービスはコンテナ化と合わせて考慮していきたい事項です。

マイクロサービスを目指すモチベーションは、これまでの開発で個人的に悲しい案件が多々あって、それを回避する手段として使えそうだなというところです。

これまでの開発で個人的に経験した悲しい事案として

  • chainerとflaskを組み合わせたい
    • flask最新版では動かない
  • rubyのバージョン上げた
    • 同居してるfluentdが死んだ
  • PHPのバージョンを上げた

こんなのがありあす。

そもそも上記悲しい事案は複数サービスが1つの環境に同居しているから起きるのであって、分離してしまえば何も問題はないはずです。

分離の方法としては言語ごとにあるenvで回避できそうな気もします(pythonだとpyenvとか)が、言語ごと/モジュールごとにそれをやっていくのはどうなの?的な思いがあります。

各言語のプロフェッショナルならenv使って上手に回避できるのかもしれませんが、私の母国語はC,C++ということもあってライブラリの管理が少々面倒というところもあって、個人的にはマイクロサービス化とコンテナ化推しです。

そのうちAWSのほうもdockerで動かしてみたいですね。ECS(elastic-container-service)というサービスらしいです。

通信プロトコル

クラウドとHololensの連携との話をさらに掘り下げると、具体的にWebフロントどうするかとか、プロトコルどうするとかあります。 webサーバはnginxで色々できるようになってきたので、これでいけそうと思ってます。apacheでもいいですけど。

通信プロトコルの現状の実装はHTTPSのRESTですが、セッション張り直しのコストとか考えるとWebSocketかなと思ってます。

というわけで、カードゲームのARのための最低限の土台が整ったと思います。 性能面とか考慮しつつ、今後どうするか色々試していきたいですね。

WebとHololensの連携

まだ完成していないので記事にしていないのですが、HololensとWebで少しずつ連携できるようになってきました。

通信部分はただのRESTではありますが、重要なのはWebリクエストを出した時点でのHololensの姿勢を使うというところです。

Webと連携すると多少なりとも遅延は発生するので、そのあたりをどうやって解決していくかという実験をするための土台が整ったといえます。

実際に動かした感じでは秒単位の遅延が起きることが分かっています。 これは通信レイテンシだけでなく物体検出・カテゴリ分類などの、処理が重いことも影響しています。

リアルタイムのARアプリでWebと連携する場合、いかにユーザーにストレスや違和感を与えずに演出を組み込んでいくかなど、そういった部分を今後色々と試していきたいですね。

開発環境について

少々蛇足になりますが、貴重な時間を有効活用するために、通勤時間や昼休みにSurfaceBook2で作業しています。

初期投資に多額の資金を投入するリスクを負うのは怖かったので、RAMは8GBのモデルです。

動作は快適なのですが、8Gだと少々つらくて、それぞれの開発環境を個別に動かすことはできますが、同時実行ができないです。

こういう状況なので、Webとの連携が進み始めた段階でAWSも使い始めることができたのは幸運だったなぁと思う次第です。

技術の融合

個人的には私の好きな分野で技術を活用できた一年だったと考えています。

技術の融合の話に入る前に、まず「技術」もしくは「テクノロジー」について私の思想について少し書きます。

私は「テクノロジーはエンドユーザーに価値を提供してこそ意味があるもの」だと思っています。

エンドユーザーに提供する価値というと、具体的には「製品」や「体験」といったものです。 店で売っているような商品や、同人作品などは製品そのものですし、アプリで遊んでもらう(=楽しむ)なんてのは体験ですね。

今年は「技術を活用する、価値を提供する」領域を広げるために「コスプレでのAR利用」を試し始めました。

「コスプレでのAR利用」は、イベント会場で興味を持ってくれた人もいたみたいで、今後も表現方法の1つとして色々試したいと思います。

コスプレでAR, 3Dプリンタの利用

ます「コスプレ」は仮想世界(ゲーム, アニメなど)にあるものを現実世界に再現して、キャラになりきる行為です。

言うなれば「ごっこあそび」ですね。子供の頃やっていたであろう「ドラゴンボールごっこ」とかの進化形です。

こういった分野で個人的にやりたいこと(製品そのもの+アプリ)の実現に一歩近づいた年だと思います。

具体的には「ARとコスプレの融合ができた」というのが大きいと思います。イベント会場ではほとんど使えませんでしたが。(猫耳が引っかかってHololens装備できなかった。。。)

技術要素はこんな感じでしょうか

仮想世界にあるものを現実世界で再現する(たとえば3Dプリンタ)という行為は、ARとは異なる技術で現実世界を楽しくしてくれます。 というわけで、コスプレとARは相性が良いだろうと考えています。

特に3Dプリンタは一昔前は「家も建てることができる夢の装置」みたいにもてはやされましたが、プリント時間やコスト・耐久性などを考えると、そんなことは全然ありません。家庭用なら尚のことです。

3Dプリンタだけでは大して面白いものは作れませんが、ARデバイスと組み合わせることでとても楽しいものができるということは見えてきました。

特に私の場合はギミック(光る、動く、変形、合体)とかが大好きですから、そういうものの再現にも使えそうです。

他の技術でもいいのでは?

VRでいいのでは?と思うこともありますが、個人的には実物のほうが好きです。

たとえば、コスプレの武器って実際に持ってみると「すごいもの持ってる」感があって、VRのコントローラとは全然違った感覚です。

(VR自体は家電量販店でやってるデモしか体験したことないので、私の中での誤解があるかもしれませんが。)

現行のVRでは再現できないけれども、コスプレ用の武器で再現できる(優れている)点について考えてみました。

具体的には次の点がコスプレの武器が優れていると思っています。

  • 持ち手部分の触った感じ
  • 振ると重さを感じる

前者は武器製作者のセンスにも依存しますが、金属的な冷たさとか、革のようなじっとりした感じは、実物ならではです。

後者の「重さ」というのは、ただの重量だけに留まらず、実世界での動きに関係してきます。というのも、コスプレ用の武器というのは装飾がついていたりして、重心が不安定です。

  • 杖の先端のほうが重い
  • 斧は刃がついてる側のほうが重い

とかですね。

こういうものを振ったり構えたりすると遠心力でさらに不安定になるため、独特の「持ってる感じ」を受けます。これは現状のVRコントローラでは再現できてなくて、少々物足りない感じになります。

もちろん人によって受け止め方は違っていて、

  • そういうのは要らない派(たとえばゲームコントローラの振動もいらない派)
  • VRでもそういうのを再現できればよい派(研究をしている方々)
  • もっと実物の再現度上げたい派(ARすら不要派)

みたいに色々な考え方がありそうです。

私は実物+AR派でいこうと思っていますし、各個人が目的にあったテクノロジーを使えば幸せになれそうだなと思います。

色々と堅苦しいことも書きましたが、自分が楽しめることが一番大事だったりします。 継続できるのは楽しいからこそです。

年齢を重ねても夢は忘れないようにしたいですね。

ちょっと残念だったこと

やってみたけど上手くいかなかったり、やる予定だったけど実現できなかったことを書いてみます。

DeepLearning

まだ記事にしていませんが、chainerを使ってVGG16からのMTGのカード分類のファインチューニングはじめました。 試しに自宅のゲーミングPCで一週間くらいかけて40Epoc回したところ、50~60%の正答率でした。 学習・教師データともに少ないので過学習してる気はします。

Unity

2018年内にネイティブプラグイン試しに作るくらいやりたかったのですが、手付かずでした。。。

C++の限界

これも記事にしてない案件ですね。 Unityでやればすぐ終わることがC++だとめっちゃ手間かかるので、あきらめました。

これはC++がダメというわけではなくて、プログラムコードだけでは3D空間を把握できないという事実です。

文字列だけでのプログラミングの限界なのかなぁと。

具体的には3D空間での衝突判定とかで、bulletとeigenを使おうとビルドして組み込んではみたものの、うまく動いてくれなくてデバッグに手間かかりすぎです。

ベクトルの値なんかをグラフィカルに表示出来ればいのですが、結局ノートに鉛筆で書いたりとかしてるうちに「Unityでいいじゃない」って結論に辿り着きました。

C++のみの実装自体は断念しましたが、線形代数を復習する良い機会だったと思います。

やったこと自体は来年の目標であるネイティブプラグイン開発にも引き継げると思います。

2019年やりたいこと

来年の目標というか、やってみたい願望を書いてみます。

こういうところは現実主義なので、目標の段階で「無理やん?」みたいなのは自然に除外しているみたいです。

目標を達成していくことで次の2つが実現できると考えています。

  • カードゲームのAR化
  • コスプレでのAR活用

Unityネイティブプラグイン (継続)

とりあえず趣味レベルとはいえ2年近くC++/CXとUnityを触ってみてわかったことは、どちらも一長一短ですね。 Unityだとエンジンがほぼ全部面倒見てくれて、C++だと細かいところまで手が届く。

なので、これらの美味しいところ取りをしたいと思っています。

どういった場合にどちらが優れているかという点は見えてきたので、Unityをフロントとして、C++プラグインとして使っていく方向で2019年は動いてみたいと思います。

納得できなければまたC++からDirectX叩けばいいですし。

DeepLearning (継続)

ご無沙汰してる間にDeepLearning界隈も進んだようです。 カードゲームのAR化での画像分類やカード検出で使おうと試しているものをさらに進めてみたいと思います。

やりたいこととしては2つあって、1つ目は学習用のデータ収集です。 まずはアプリ側のプロトタイプ実装を終わらせて、それを使って学習用データを収集するところからですね。 現状のWebAPIから拾ってきたカード画像だけでは限界があります。

2つ目はOpenCVのONNIXローダを試したいですね。

ONNIXというのはDeepLearningのデータ交換用フォーマットです。、乱暴に表現するなら「Web技術でいうJSON/XMLのDeepLearning版」といった感じです。

何ができるかというと、Chainerで学習させたネットワークをONNIXで出力し、そのONNIXをOpenCV(C++)で読み込めるようになります。

これはどういうことかというと、

  • 学習はクラウドの強いマシンでChainerを使う
  • 識別はモバイル端末でOpenCVのネイティブ実装を使う

といったことができそうです。

Hololensだと、識別フェーズの処理ですら厳しいかもしれませんが、試す価値はあると思っています。

次期Hololensに搭載されるという噂のAIチップでONIIXを使えるAPIを公開してくれることを祈りましょう。(そういう用途ではなさそうですし、公開されないでしょうけれど)

電子工作魔法陣 (新規)

ずっとご無沙汰してた電子工作を復活させたいと思います。

catalina1344.hatenablog.jp

今更感ありますが、現状のAR/VRには致命的な問題があると思っていて、それを解決するのが目的です。

AR/VR最大の課題は「デバイスを持っていない人は楽しくない」というところです。

どちらも外野から見ると「何やってんだ?」状態ですし、「装着者が見ている映像はコレです」と見せても、面白さは伝わりにくいです。

Hololensでは映像体験を共有する仕組みがありますが「デバイスorそれに準ずるものを持っていること」という前提条件ありきです。

私としてはコスプレとかのサブカルをテクノロジーで盛り上げたいと思っているので、外野のギャラリー(=デバイスを持っていない人)にも「面白い」「楽しい」と感じてもらいたいです。

通りがかりに「ちょっと写真一枚撮らせてほしい」という時に、「Hololensでぜひ」というやり取りはムリがあります。

なので、「ARデバイスがなくても楽しい」「傍から見てても楽しい」という物理魔法陣をまた頑張ってみようかなと思った次第です。

魔法(物理)ですね。

ちなみに「デバイスを持っていない人が楽しくない」のであれば「全員がデバイスを持っている環境を作ればいい」という発想で進めているのがHadoとか、VRエンタテインメントなんかの施設だと考えます。

個人活動ではハコモノ作れるわけもなく、そもそも貸し出し用デバイスを購入する資金もありませんので、別のアプローチでいくのが正解だと思っています。

まとめ

今年も色々と試行錯誤して勉強になりました。

自分で手を動かして失敗しないと覚えないタイプなので、順調に進められた一年だったかなと思います。

このブログも更新が少ないのに読者になってくださる方もいてありがたい限りです。

スターとかブクマとかもらえたときにはめっちゃやる気UPしてたりします。

ARそのものをお仕事にできれば楽しそうだとは思っています。

それでは今回はこれくらいで。

皆様よいお年を。

NginxでHTTPS(SSL)対応する

暗号/復号関係は苦手です。かたりぃなです。

苦手な理由は、これ系は期待動作していない場合のデバッグが非常にやりにくいからなんですよね。

アルゴリズムそのものは実績がある前提だとすると、うまく動かないときはモジュール外から与えるパラメータが間違えているわけです。

鍵、IV、証明書、暗号アルゴリズム、暗号利用モード、暗号開始アドレスetc...こういうパラメータを確認して一つづつ見直しをするしかないわけです。

で、パラメータを変更して動作を追っても「あってる or 間違えている」しかわからないので、本当につらい。

数学が得意な人はもっと効率的に作業できたりするんでしょうか?

さて、今回はHTTPSとその周辺技術を触ってみます。 個人的にサーバ立ててサービス化するときも役立つと思いますので。

HTTPSとは

一言でいうとHTTPをセキュアにしたものです。

まずプロトコルの観点から整理します。

プロトコルスタック

だいたいこんな感じです。 TCPレイヤとHTTPレイヤの間にTLSレイヤを設けて、ここで暗号/復号などが行われます。

レイヤ プロトコル
アプリケーション HTTP
アプリケーション TLS/SSL
トランスポート TCP
ネットワーク IP

Python-FlaskでHTTPS対応してみる(断念)

ちょっと面倒なので断念です。

HTTPS化するだけのはずが、本題以外のことを考えないといけなくて、手間かかりすぎでした。

どういうところが手間だったかというと

  • アプリケーションとインフラとの結合が密になってしまう
    • 当然、コードが入り乱れてきてしまう
    • コード混乱の結果、リポジトリの管理どうするかとか、本題と関係ないところで悩む

アプリケーションはビジネスロジックとかに注力すべきであって、インフラ周りと密に結合した実装にするのはよくないですよね。

アプリケーションのモジュール構成をしっかり考えるという対策もありますが、今回は一般的なWebサーバを活用する方向で考えたいと思います。

というわけで、apacheとnginxを試していきます。

apacheでやってみる

apacheの設定こんな感じにすれば、とりあえずHTTPS対応できます。

証明書と鍵は後で説明します。

apacheの設定ファイル(httpd.conf)

# vhost
NameVirtualHost *:80
NameVirtualHost *:443
Include pass/apache/conf.d/*.conf

virtualhost(pass/apache/conf.d/httpd_vhosts_site.conf)

<VirtualHost *:80>
    ServerName local.my.api.mtg_card_detect
    # ~~ 略 ~~
<VirtualHost>

<VirtualHost *:443>
    ServerName local.my.api.mtg_card_detect:443
    SSLEngine On
    SSLCertificateFile pass/apache/server.crt
    SSLCertificateKeyFile pass/apache/server.key
<VirtualHost>

で、この後段にお好きなアプリケーションサーバを置いておけばよさそうです。

PHPなんかだとこの方法でちゃちゃっとやっつけてもいいかもしれませんね。

アプリケーションとwebサーバを分離したい

ここから、今回のエントリの本題です。一般的にSSLオフローダ、リバースプロキシと呼ばれる方式です。

クライアントからのアクセスを一旦Webサーバで受けて、WebサーバはSSLを解きます。

SSLを解いたら、Webサーバからアプリケーションサーバへ(HTTPSではなく)HTTPでリクエストを出します。

これで次のことが実現できます

いい感じの役割分担になってきました。

nginxリバースプロキシでSSLオフロードをする検討

以前やったnginxのリバースプロキシにSSLを解かせます。

一点だけ懸念がありますが、今回は気にしないことにします。まだ使わない機能なので。

何が懸念かというと、アプリケーションに必ずHTTPでリクエストが来るということは、リクエスURIが書き換わるということを意味します。

リクエスURIが書き換わると、アプリケーションが実装している盗聴対策, 認証, 認可などのセキュリティでエラーとして弾かれる可能性があります。

具体的にはOAuthとか。うろ覚えですがOAuthはリクエスURIを含めて何か作ってた気がします。またの機会に調査します。

nginxでSSLを解く

まず証明書と鍵が必要です。実験用なので自分で鍵を作って、自分で署名しましょう(通称:オレオレ証明書)。

$cd /usr/local/nginx/conf.d
$openssl genrsa 2048 > server.key
$openssl req -new -key server.key > server.csr
# ※以下のような質問が出るが、オレオレ証明書なので全てEnterでスキップでOK。
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
 
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
 
# 署名する
# 実験中に期限くると困るので10年(365 x 10=3650)くらいの有効期限で
$ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt

次にnginxの設定ファイルを書きましょう。 以前やったリバースプロキシの設定を書き換えています。

user  nginx;
worker_processes  1;

pid    /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http{
  server {
    listen       443 ssl ;
    server_name  local.my.proxy.server;

    ### SSL有効化と証明書の保存場所指定
    ssl           on;
    ssl_certificate     /etc/nginx/cert/server.crt;
    ssl_certificate_key /etc/nginx/cert/server.key;

    ### プロキシ先の指定とApacheに渡すリクエストヘッダーの指定
    location / {
        proxy_pass http://192.168.xx.xx:port/;
        proxy_redirect                         off;
        proxy_set_header Host                  local.my.api.mtg_card_detect;
        proxy_set_header X-Real-IP             $remote_addr;
        proxy_set_header X-Forwarded-Host      $host;
        proxy_set_header X-Forwarded-For       $proxy_add_x_forwarded_for;
    }
  }
}

これでOKです。 nginxにHTTPSでリクエストを出すとmtg_card_detectというアプリケーションサーバにHTTPリクエストが出るようになりました。

感想と今後の展望

SSL対応もできたので、今後カードゲームARアプリのゲームサーバ(カード検出、分類)に対するリクエストもHTTPSで実現できる目途は立ちました。

本物の証明書はCAに発行してもらわないといけないので、実際にどうするのかは必要になってから調べることにします。

サーバ構成も少しづついじれるようになってきたので、今後のアプリづくりで有効活用したいですね。

それでは今回はこれくらいで。

AWSを試してみる

AWSを契約してVPCとか仮想サーバとか試してみました。かたりぃなです。

AWSのアカウント作成

登録はクレジットカードが必要になります。

ユーザー登録自体は無料なのですが、無料登録枠からはみ出して課金となった場合、この登録しているクレジットカードから引き落としされるようなので注意が必要です。

(まだ実際に課金は発生していないので、具体的にどうなるのかは見たことありません。)

無料利用枠

新規登録から一年間は、無料利用枠というものがあります。

たとえば基本的なクラウドサーバであるEC2の最小構成であれば、無料利用枠で利用できたりします。

しかし残念ながらDockerコンテナをデプロイできるサービスは無料利用枠には含まれていないようです。 (Elastic Container Service = ECS)

なので、今回はまずEC2を利用します。

AWSの初期設定

セキュリティを考慮して構成を考えると、次のような構成になるかと思います。

  1. 管理用EC2インスタンス
  2. 実際のユーザーアクセスを受け付けるEC2インスタンス(複数あり)

どうしてこうなるかというと、どこからでもSSHでログインできるインスタンスというのはなかなか怖いものがあります。(AWSの管理コンソールでも警告が出ています。)

1でやりたいことは、管理用インスタンスは専用の固定IPからのみアクセス可能としておいて、他からのアクセスは禁止します。

2はパブリックIPを割り当てずにVPC(AWS上の自分のLANとでも思ってください)内からアクセスするものとします。 これはSSHの経路です。 HTTPで外から来る人向けにロードバランサ経由でプライベートIPへと転送させます。

これでなかなかよさげな構成(一般的なのかな)になりますが、複数インスタンスが必要だったりして、それなりにお金がかかってきてしまいます。 なので、簡易的な対処でいったんEC2を立てて、実験してみます。

  • EC2インスタンスは1個のみ
  • SSH接続のポート番号は変えておく
  • HTTPポートは自分が作業するときのみ開ける

SSHのポート番号を変えておくというのは、なかなか原始的な手法ですが、何もないよりは安心といったところですね。

自分がアタックする立場なら、22番とりあえず叩いてみるとかするでしょうし、それなりに効果はあるらしいです。

AWS-EC2の設定

AWSの操作はもっと有用な情報源があるので、ここでは省略します。 無料利用枠のデフォルト値から変更した箇所のみ記載します。

セキュリティグループ - HTTPあけた, SSHポート番号を変えた ディスクサイズ 30GiB。無料利用枠いっぱい

Linuxマシンの設定

まずSSHポートの設定を変更します。

/etc/ssh/sshd_config の上のほうに Port 22 という表記があるので、これを変えてsshdを再起動します。

sudo service sshd reload

ローカルマシンの設定(Windows)

今回はWSLのsshを使ってAWSに接続します。 AWSインスタンスを作成するときにキーペアがないとアクセスできなくなるよ?みたいに言われたかと思います。 この鍵をローカルマシンに保存します。

必要に応じて.ssh/configとか書いてアクセスできればOKです。 今回はこんな感じで書きました

Host aws-ec2
        HostName パブリックIPアドレス
        IdentityFile    ~/.ssh/aws-first-keypair.pem
        User    ec2-user
        Port    ポート番号

なお、ここで指定した鍵はパーミッション設定間違えやすいです。(Windows側からコピーして持ってくると、実行権限がついてたりとか)

chmox 600 ですかね。とりあえずは。

ansible

ansibleはプロビジョニングツールと呼ばれるもので、コマンドを実行することで対象のマシンに必要な設定をしてまわる便利なやつです。 今回はこれを使ってみます。

ansibleの追加パッケージを使うとEC2をたてたりする機能も使えるようになるらしいですが、はまだ試していないのであしからず。

さて、EC2でこれを使う目的は、いくつかありますが、代表的なものは以下の2つかと思います。

  1. マシンの環境を再現しやすくすること
  2. 複数台マシンにスケールアウトする場合のプロビジョニングの手間の削減

1はよくある「ライブラリ依存の問題」の解消などがわかりやすいと思います。 昔のWindowsだとよくあったトラブルで「なんかよくわからないけどアップデート走ったら治った」みたいなやつです。

趣味で適当にやるだけならいいですが、お客さんに納品するシステムなんかで「よくわかりませんが治りました」って嫌ですよね。(少なくとも私はそういう業者は信用しません)

ここではライブラリの組み合わせなど、環境を固定できるのが大きなメリットです。

手作業でやってたりオレオレ.shを自作したりすると後で見たとき泣きそうになりますが、ansibleで一定のフォーマットが担保されるなら少しは安心かなと思います。

2はスケールアウトを考えたとき、複数台に同じ処理を流していくのって手間かかりますし、ミスが紛れ込みます。 たとえばhost1にコマンド打ち込んだつもりが、host2に打ち込んでいたとか。 そういうミスを減らすためにもこういうツールはどんどん使っていくべきだと思います。

Ansible実行環境

WindowsではAnsibleが走ってくれません。Linux由来のパッケージではよくあることなので、あきらめて「ansibleを実行するためのdockerコンテナ」をたてます。

FROM centos

RUN yum -y install initscripts MAKEDEV

# 開発ツール
RUN yum groupinstall -y 'Development tools'
RUN yum install -y git wget cmake
RUN yum install -y sudo
RUN yum install -y ansible

# 文字コード
#RUN yum -y reinstall glibc-common
RUN localedef -v -c -i ja_JP -f UTF-8 ja_JP.UTF-8; echo "";
env LANG=ja_JP.UTF-8
RUN rm -f /etc/localtime
RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

# python 3
RUN yum install -y https://centos7.iuscommunity.org/ius-release.rpm
RUN yum install -y python36u python36u-libs python36u-devel python36u-pip
RUN pip3.6 install --upgrade pip
RUN pip3.6 install boto

# dockerコンテナとホスト間での共有ディレクトリ
RUN mkdir /tmp/data -p

# 共有ディレクトリの準備
RUN mkdir -p /tmp/data

# 起動オプション。CMDに指定できるのは一つだけ。
EXPOSE 22
CMD bash -c "/usr/sbin/sshd -D"

sshを許している理由は、docker内に鍵を置きたくないからです。事故らせる自信あります。(ぇー)

Windows側からsshするとき-Aでフォワードかけて、その鍵を使ってAWSに接続する。という使い道を想定しています。

playbookをかく

playbookの詳細はもっと有用なサイトに譲るとして、やってみた内容だけ書いていきます。

まずplaybook本体です。

開発ツール、C++用のライブラリ(boost, opencv)、Python用ライブラリ(chainer, chainerUI)をインストールすることを記載します。

---
- name: 
  hosts: all
  user: root
  become: yes
  become_user: root
  roles:
    - devtools
    - cpp-libs
    - chainer

cpp-libsロールのmain.ymlでは、opencvとboostをインストールしたいので、それぞれわけて書きました。

---
- include: tasks/opencv.yml
- include: tasks/boost.yml

opencvのインストールはこんな感じで書いてます。 git cloneしてcmakeしてビルドですね。

ちょっと古いdockerfileからコピってきたのでバージョン古いままです。

- name: Install opencv
  git:
    repo: 'https://github.com/opencv/opencv.git'
    dest: '/root/opencv/'
    version: 3.3.1

- name: Install opencv_contrib
  git:
    repo: 'https://github.com/opencv/opencv_contrib.git'
    dest: '/root/opencv_contrib'
    version: 3.3.1

- name: make opencv build dir
  file:
    path: /root/opencv/build/
    state: directory
    mode: 0755

- name: check cmake configure
  stat:
    path: /root/opencv/build/CMakeCache.txt
  register: cmake_cache

- name: configure opencv
  shell: >
    cd /root/opencv/build;
    cmake -DOPENCV_EXTRA_MODULES_PATH=/root/opencv_contrib/modules ..;
  when: not cmake_cache.stat.exists

boost側は似たようなもんなので省略します。

ansible-playbookの実行

ここまでで一通りのplaybookができました。 こんな感じで実行します。

ansible-playbook -i inventory/hosts playbook.yml

playbookのトラブルシューティング

Opencv, boostとも大きなライブラリなので、結構容量食います。 この大きさ故にEC2のT2-microのデフォルト設定では容量不足で終了します。

T2-microインスタンスののデフォルトストレージ容量は8GByteしかありません。 AWS無料利用枠の上限として合計30GiByteまでいけるようなので、おとなしくインスタンス作り直し、大きめの容量をとってリトライすればうまくいきました。

ちなみに、AWSのストレージ容量表記はSI単位系ではなく二進接頭辞(「キビバイト(KiB)」とか)になっていました。 まあサーバ系でギリギリの容量で運用することは普通やらない(組み込みソフトならいざ知らず…)ので、あまり気にしなくてもいいかもしれませんが、頭の片隅に置いておきましょう。

起動、動作確認

以前のブログ記事でやったソースコードをアップロードして、ビルド、実行です。

つまり、PNGファイルをHTTP-POSTしてあげれば、画像解析をして結果をJSONで返してくれるわけです。

実験用クライアントアプリ

最終的にHololensからアクセスする予定のRESTなので、Hololensと同様のUWPで作ります。 少しずつですが慣れてきたC++/CXです。

XAMLにボタンとか適当において、イベントハンドラに次のコードを記述します。

 FileOpenPicker^ openPicker = ref new FileOpenPicker();
    openPicker->ViewMode = PickerViewMode::Thumbnail;
    openPicker->SuggestedStartLocation = PickerLocationId::PicturesLibrary;
    openPicker->FileTypeFilter->Append(".jpg");
    openPicker->FileTypeFilter->Append(".jpeg");
    openPicker->FileTypeFilter->Append(".png");

    create_task(openPicker->PickSingleFileAsync()).then([this](StorageFile^ file){
        if (file) {
            // ファイルが選択された
            // todo : キャンセル処理が必要。
        }
        return file->OpenReadAsync();
    }).then([](Windows::Storage::Streams::IRandomAccessStream ^ stream) {
        // こうすればPOSTするためのHTTPリクエスト作れるらしい
        auto uri = ref new Windows::Foundation::Uri(L"http://EC2のパブリックIP/");

        auto streamContent = ref new Windows::Web::Http::HttpStreamContent(stream);
        auto request = ref new Windows::Web::Http::HttpRequestMessage(Windows::Web::Http::HttpMethod::Post, uri);
        request->Content = streamContent;

        auto filter = ref new Windows::Web::Http::Filters::HttpBaseProtocolFilter();
        filter->CacheControl->ReadBehavior = Windows::Web::Http::Filters::HttpCacheReadBehavior::MostRecent;
        auto client = ref new Windows::Web::Http::HttpClient(filter);
        auto headers = client->DefaultRequestHeaders;
        return create_task(client->SendRequestAsync(request));
    }).then([this](task<Windows::Web::Http::HttpResponseMessage ^ > previousTask) {
        try
        {
            auto response =  previousTask.get();

            // ここではHTTPリクエストの動作確認が目的なので、ログに出して終わる。
            auto body = response->Content->ToString()->Data();
            auto header = response->Headers->ToString()->Data();
            OutputDebugString(header);
            OutputDebugString(body);
        }
        catch (const task_canceled&)
        {
            // HTTPリクエストがキャンセルされた場合はここに来る
            OutputDebugString(L"http request task chanceld\n");
        }
        catch (Exception^ ex)
        {
            // HTTPリクエストで何らかのエラーが発生した場合はここ
            OutputDebugString(L"http request task error\n");
        }

    });

やっていることは次のとおりです。

  1. イメージピッカーを表示して、画像ファイル選択のUIを表示します
  2. 選択された画像ファイルを読みだし用に開き、ストリームを得ます。
  3. HTTPクライアントのリクエストにストリームを関連付け、リクエストを開始します
  4. HTTPレスポンスをログに出力します。

この手順の3番目以降はHololensに移植してもそのまま使える部分ですね。

つまり、リクエストに載せるストリームをカメラ映像由来のものにすると。

できた!

AWSのサーバにリクエストを投げて、レスポンスを受け取るという基本的な部分ができました。

動作結果は以前に Hololens - デスクトップPCでやったのと同じです。

まだサーバ側に認証・認可はつけてないので、不用意にアクセスされないよう、使わないときは落としておく運用とします。

感想と今後の展望

プロトタイプとはいえ段々とシステムを作っている感じが出てきました。

まだまだやることあ多いですが、少しずつ自分のペースでやっていきたいと思います。

次はHololens側との連携になりますが、サーバ側の画像検出器が少々不安定で落ちることがあるので、修正していくことになるかなと思っています。

こういうときに今回作ったようなダミーモジュールがあると便利ですね。

それでは今回はこれくらいで。