catalinaの備忘録

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

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側との連携になりますが、サーバ側の画像検出器が少々不安定で落ちることがあるので、修正していくことになるかなと思っています。

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

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

HololensでAR(vuforiaとunity)

HoloLens上でVuforiaとUnityを使ったARを試してみました。かたりぃなです。

Vuforiaとは

VuforiaはARフレームワークです。 マーカーを登録することで、それを認識するためのライブラリが生成されます。

対応プラットフォームは

ライブラリは各種プラットフォーム用のものを出力でき、Android, iOS, UWP, Unityなどがあります。

マーカーの種類

Vuforiaで扱えるマーカーはいくつかの種類があって、次の中から選択することができます。

  1. 平面画像マーカー
  2. 直方体マーカー
  3. 円柱マーカー
  4. 立体マーカー

今回試したのは1と4です。

平面画像マーカー

平面画像マーカーは、写真などの二次元画像をマーカーとして使用できます。 写真をアップロードすると、特徴点を算出して、そのマーカーの認識しやすさを表示してくれます。 こんな感じに。 f:id:Catalina1344:20180810164557j:plain f:id:Catalina1344:20180810164602p:plain

ちょっと特徴点が少なすぎて認識できないですね。 そもそも三次元物体に対する認識を二次元でやろうというのは少々無理があるかと。

立体マーカー

今回のメインは、この立体マーカーです。立体マーカーを作る方法は2つあります。

  1. Androidの立体スキャンアプリを使って立体スキャンする
  2. CADデータをVuforia_Model_Target_Generaterで処理する

どちらも以下のURLからダウンロードしたツールを使って実現できます。 https://developer.vuforia.com/downloads/tool

今回は都合よくCADデータと、それを3Dプリントして塗装仕上げしたものがあるので2の方法でいきます。

CADデータはこんな感じです。 f:id:Catalina1344:20170913003738p:plain

3Dプリントして塗装したのはこんな感じです f:id:Catalina1344:20180810164905j:plain

技術系ブログなのに方向性がおかしくなってきてる気がしますが、突き進みます。

CADデータってどんなの?

厳密な定義はおいといて、Vuforiaが3Dマーカーとして使えるCADデータのフォーマットとしてはFBX, STLなどがあります。

ここではSTLを使います。STLデータとは3DプリンタやCNC切削機で利用できるデータ形式です。

STLデータ形式Wikipediaにある詳細説明のとおりです。

  • 頂点3つで構成されるポリゴン
  • そのポリゴンの表裏を表現する法線

これを1つの要素として、その集合として表現されるものです。

3DCGとの最大の違いは、次のような情報が含められないということです。 原理的に当然といえば当然なのですが。

  1. マテリアル
  2. テクスチャマップ
  3. 法線マップ

なので、3Dプリントするデータは生ポリゴンで全部表現する必要があります。

CADデータを取り込んでマーカーをつくる

ModelTargetGeneratorを起動するとライセンス入力を求められるので、Web上で登録して、そのライセンスコードを入力します。

使い方はこのあたりを参考にします。 https://library.vuforia.com/articles/Solution/model-target-generator-user-guide.html

そして新規プロジェクト作成でCADデータを読み込ませると、設定画面が開きます。

先にこたえを書くと、出力可能な状態にまで設定するとこんな画面になります。 f:id:Catalina1344:20180810165243p:plain

Model Target Generatorの基本設定

起動時に設定したモデルが表示されるので、次のものを設定します

  • 単位系
  • モデルの上方向

ここで単位系をあわせておかないと、Unityにもっていったときに認識サイズが大きすぎる/小さすぎるなんてことになります。 まあUnity側でスケールかければいいんですが。

マーカーの検出位置を設定

DetectionPositionというボタンを押すと、画面右側にシルエットが表示されます。

よくわかってませんが、どうやらこのシルエットが認識するオブジェクトの情報のようです。

なので、現実世界のオブジェクトのシルエット(実際に3Dプリントしたデータ)と一致しないようなシルエットが表示されるときは、CAD側で修正もしくは他オブジェクトを使うなどの対応が必要になります。

上のシルエットも、同様の理由から3Dオブジェクトの一部しか使用していません。

シルエットが一致しないケースとして

  • 組み立て時の誤差。3Dプリントして塗装、接着して組み立てるとどうしても誤差が出ます。
  • 光沢が大きな素材。マニュアルにはこう書かれてるのですが、実際どうなのかはよくわからないです。背景が白だと光沢出たときに穴になるというのはわかるのですが。
  • 遮蔽物による影響。手で持つ部分などは基本的に遮蔽されるので、認識には向かないです

などがありますので、適宜工夫する必要がありそうです。

エクスポート

ModelTargetGeneratorのメニュー右端の「Generate Target」ボタンを押すと、マーカーとして認識するライブラリを生成できます。(Unity)

無料ライセンスでは10回までという制限があるので、その点だけ気を付けましょう。

Unity側でARの設定をする

エフェクト出したいので、Unityで作業していきます。こういうのはC++でやるの辛すぎるので。

unity 2018.2.2です。

Vuforiaの設定をする

事前にHololens用の設定を済ませておきます。

そのうえで、このページを参考にVuforiaの設定をします。 https://library.vuforia.com/articles/Training/Developing-Vuforia-Apps-for-HoloLens

ModelTargetを設定する

先ほどModelTargetGeneratorで出力したパッケージをインポートします。

インポートしたパッケージをシ、ーンヒエラルキに追加してから、必要な項目を設定します。

f:id:Catalina1344:20180810165557p:plain

UnityのPlayerモードで動作確認

ここまでできると、Unity上からマーカーを認識するかなど、確認できるようになります。 シーンに追加したModelTargetの子オブジェクトとして任意の3Dオブジェクトを配置して、正しく認識するか確認します。

もし認識できていれば配置した3Dオブジェクトが表示されます。 認識できていない場合は次の点を見直すとよいです。今回はこの2つが原因でした。

  • マーカーの質が悪い。光沢がありすぎたりすると、マーカーとして認識しづらいようです
  • 3Dオブジェクトが大きすぎる マーカーの大きさより小さくなるようスケール設定しましょう。画面からはみ出していると何も表示されません。

エフェクトを出す

大した事してないので、簡単に説明します。

3Dモデル(天球儀)の周りに回転するタロットカードを出したいので、その一枚ポリゴンを配置します。 とりあえず4枚くらいあればいいでしょう。

タロットカードはニコニコモンズにあがってる無料素材を使わせていただきました。

あとは雰囲気だしたいので適当なパーティクルを置きます。

オクルージョンの設定

現実世界では、視点からみて奥にある物体は、手前にある物体にさえぎられると見えなくなります。あたりまえですね。

仮にコレが無い場合、今回のようなマーカーの周りを周回するエフェクトは、すべてマーカーより手前にあるように見えてしまいます。

このあたりまえをUnityに設定してあげる必要があります。

即ち - Hololensの空間マッピング機能でえられたマップを取得する - そのマップをZバッファに反映するよう指示する

まあ、このドキュメントのVisualizationあたりまでやっておけばその通りになります。 https://docs.microsoft.com/en-us/windows/mixed-reality/holograms-230

(実際に可視化の段階でZバッファにも書き込まれるので。)

空間マップの詳細説明はまだ理解が追い付いてないのでまたの機会にします。

できた!

感想と今後の展望

技術ブログのはずが、段々サブカル要素が混ざってカオスになってきましたが気にしないことにします。

これからの時代、一つのカテゴリだけで生き残るのではなく複数カテゴリのコンビネーションができたほうが有利だと思います。

というわけで今回はこれくらいで。

C++のHTTPサーバ(boost::beast)を試す

毎年恒例の夏バテの時期がやってきました。かたりぃなです。 今回はC++でhttpやってみたいと思います。

C++でhttpサーバ

今はC++単体でラクにHTTPサーバが実装できるようになりました。

使うライブラリはboost::beastです。

https://www.boost.org/doc/libs/1_67_0/libs/beast/doc/html/index.html

boost1.66で入ったので、結構最近です。

exampleをビルドして動かす

とりあえず適当な仮想マシン上でサンプルコードをビルドして、実行してみます。

example/http/server/sync/http_server_sync.cpp あたりが単純で分かりやすいです。

これを実行したディレクトリのファイルをHTTP-GETできます。

まるでPythonのSimpleHTTPServerみたいですね。

足りない

私がやりたいことに少々足りません。

Hololensから画像を投げて、それを受け取って処理するWebサーバが欲しいのです。そう、PUTとPOSTの実装がサンプルコードにはないのです。

ないなら作りましょう。

サンプルコードの改造でサクッと。

POSTを受け付ける

サンプルコードではhandle_requestという関数があります。 第二引数がまさにHTTPリクエストで、一通りの情報が入ってます。

http::request<Body, http::basic_fields<Allocator>>&& req

サンプルコードではreq.method() == http::verb::getとかやってるので真似しましょう。

    if( req.method() == http::verb::post){
        std::cerr << "POST request" << std::endl;
    }

実際のHTTPメソッドの定義はこうなってます。 https://www.boost.org/doc/libs/develop/boost/beast/http/verb.hpp

データを投げるテストをするならこんな感じで。

curl -X POST localhost:port --data-binary @/home/catalina/imgfile.jpg --verbose

これでサーバを起動したマシン側でPOST requestという文字列が出力されます。

POSTのbodyでバイナリを受け付ける用意をする

サンプルコードでは、HTTP-Requestには文字列が入ってくる前提になっています。画像を受けたいのでここの型を変更します。

まだこのライブラリ使い慣れてないので、bodyにpngファイルがバイナリで1つだけ入っている という前提でいきます。

実現方法としては型を変更してあげればよいです。 大本の呼び出し元の型を変えてあげれば、関連するテンプレート関数が一通り置き換わっていくことになります。

いいですね。テンプレート。(コンパイルエラーになるとメッセージが長すぎて怒られてる気がしてくるけど。)

sessionクラス内で定義されているreq_の型を変更します。

    http::request<http::vector_body<unsigned char> > req_;

これでHTTP-bodyはstd::vectorとしてアクセスできます。

HTTP-Bodyを取り出す

さきほど作ったPOSTの条件分岐のところでBodyを取り出します。

こんな感じで

    // デバッグ用。サイズが一致しているか目視確認したい
    auto size = req.body().size();
    std::cerr << "bodysize = " << size << std::endl;

    // 受信した画像ファイルをOpenCVのmatにする
    cv::Mat rawdata(req.body() );
    auto img = cv::imdecode(rawdata, 1);
    cv::imwrite("received_image.png", img);

今回は直接cv::Matを作りましたが、当然std::vectorで取り出すこともできます。

できた?

思ったより簡単にできてしまいました。

せっかくなので、レスポンスも作っちゃいましょう。

サンプルコードから適当なところをコピって貼れば動く気がします。雑に見えるのは飽きてきたからです。

適当なJSONレスポンスを返す

サーバで画像を受け取りたい理由は、計算資源が潤沢なサーバで画像を解析して、結果をHololensに返したいからでした。

というわけで、「返すべきデータを何か準備できる」という想定のもとJSONを返してみます。

        // レスポンスを返す
        http::string_body::value_type   body;
        body = "{'result':'ok'}";               // TODO : ここでbodyを設定する
        http::response<http::string_body> res{
            std::piecewise_construct,
            std::make_tuple(std::move(body)),
            std::make_tuple(http::status::ok, req.version())};
        res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
        res.set(http::field::content_type, "application/json");
        res.prepare_payload();                  // これ呼ばないとBodyがカラになる
        res.keep_alive(req.keep_alive());
        return send(std::move(res));

なんかあっさりいけました。

感想と今後の展望

最近ご無沙汰していたBoostですが、やっぱり楽しいですね。

エラーメッセージが長すぎて泣きそうになりますが、追っていくとメタな実装になっていて「そのテがあったか」と思わされることが多々あります。

またコンパイル時に型チェックが入るので、仕様から実装した段階で抜けがあると気づけるのもいいところだと思っています。

(たとえばrequestのbodyをfile_body型にすると、std::vectorみたいな使い方はできなくなるので、実装時点でマズイということに気づくきっかけになったりとか。)

今後の展望としては、もはやC++だけでなんでもできるので、実験がはかどります。

クライアント側もサーバ側も同じ言語(C++)で作れるので、性能面はプロトタイプ実装が終わってからでも大丈夫だろうと思っています。モジュールの配置場所を変えるだけなので。 (ただし、モジュール間の結合が疎であるという大前提が必要)

Hololensもバージョンアップきてましたし、いろいろ試してみたいですね。それでは今回はこれくらいで。