WebブラウザのGamepadAPIをつかってみる

前々から気になっていたのですが、最近あんまりネットで効かなくなったGamepadAPIを試してみます。

【公式情報】 w3c.github.io

公式のW3Cな情報でもいいのですが、Mozillaのサイトのほうが情報がわかりやすいので以下を参照しながらコーディングをしてみようと思います。

【参考】

developer.mozilla.org

参考をみるとこの様に書かれています。

Gamepad API は開発者とデザイナーに Gamepad やコントローラーへのアクセスを提供するものです。Gamepad API は Window オブジェクトにGamepadとコントローラー(以下、Gamepad)の状態を読み取る新しいイベントをいくつか追加します。

ではこれを見ながら最小限のコーディングをしていきます。

Webに公開できるようにGithubにUPしてみました。ここでもソースコードおいてみますのでご利用ください。

github.com

ソースコード

【index.html】

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">

<style>
.buttons, .axes {
  padding: 1em;
}

.axis {
  min-width: 400px;
  margin: 1em;
}

.button {
  padding: 1em;
  border: 2px solid black;
  background-color: rgb(255, 255, 128);
}

.pressed {
  border: 2px solid black;
  background-color: red;
}
</style>
</head>
<body>
<h2 id="start">Gamepadを接続してなにかボタンを押すとスタートします。</h2>
<script type="text/javascript" src="gamepad.js"></script>
</body>
</html>

【gamepad.js】

var haveEvents = 'GamepadEvent' in window;
var controllers = {};
var rAF = window.requestAnimationFrame;

function connectHandler(e) {
    addGamepad(e.gamepad);
}
function addGamepad(gamepad) {
    // gamepadのArrayを作成
    controllers[gamepad.index] = gamepad;
    // HTMLへ接続されたGamepad毎の要素を追加(複数のgamepadにも対応)
    var d = document.createElement("div");
    d.setAttribute("id", "controller" + gamepad.index);//idはpadの番号がついた形式
    var t = document.createElement("h2");
    t.appendChild(document.createTextNode("接続Gamepad情報: "));
    d.appendChild(t);
    var info = document.createElement("h1");
    info.appendChild(document.createTextNode(gamepad.id));
    d.appendChild(info);

    //Gamepadコントロール要素(ボタンなど)表示部分
    var b = document.createElement("div");
    b.className = "buttons";
    var t = document.createElement("h2");
    t.appendChild(document.createTextNode("ボタンコントロール情報: "));
    b.appendChild(t);
    for (var i = 0; i < gamepad.buttons.length; i++) {
        var e = document.createElement("span");
        e.className = "button";
        //e.id = "b" + i;
        e.innerHTML = i;
        b.appendChild(e);
    }
    d.appendChild(b);

    //Gamepadコントロール要素(アナログジョイなど)表示部分
    var a = document.createElement("div");
    a.className = "axes";
    var t = document.createElement("h2");
    t.appendChild(document.createTextNode("アナログコントロール情報: "));
    a.appendChild(t);
    for (i = 0; i < gamepad.axes.length; i++) {
        c = document.createElement("h3");
        c.appendChild(document.createTextNode("axis" + i));
        a.appendChild(c);
        e = document.createElement("meter");
        e.className = "axis";
        //e.id = "a" + i;
        e.setAttribute("min", "-1");
        e.setAttribute("max", "1");
        e.setAttribute("value", "0");
        e.innerHTML = i;
        a.appendChild(e);
    }
    d.appendChild(a);
    document.getElementById("start").style.display = "none";
    document.body.appendChild(d);
    rAF(updateStatus);
}

function disconnectHandler(e) {
    removeGamepad(e.gamepad);
}

function removeGamepad(gamepad) {
    var d = document.getElementById("controller" + gamepad.index);
    document.body.removeChild(d);
    delete controllers[gamepad.index];
}

function updateStatus() {
    scanGamepads();
    for (j in controllers) {
        var controller = controllers[j];
        var d = document.getElementById("controller" + j);
        var buttons = d.getElementsByClassName("button");

        //ボタン情報の状態取得
        for (var i = 0; i < controller.buttons.length; i++) {
            var b = buttons[i];
            var val = controller.buttons[i];
            var pressed = val == 1.0;
            if (typeof (val) == "object") {
                pressed = val.pressed;
                val = val.value;
            }
            var pct = Math.round(val * 100) + "%";
            b.style.backgroundSize = pct + " " + pct;
            if (pressed) {
                b.className = "button pressed";
            } else {
                b.className = "button";
            }
        }

        //アナログコントロール情報の状態取得
        var axes = d.getElementsByClassName("axis");
        for (var i = 0; i < controller.axes.length; i++) {
            var a = axes[i];
            a.innerHTML = i + ": " + controller.axes[i].toFixed(4);
            a.setAttribute("value", controller.axes[i]);
        }
    }
    rAF(updateStatus);
}

function scanGamepads() {
    var gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
    for (var i = 0; i < gamepads.length; i++) {
        if (gamepads[i]) {
            if (!(gamepads[i].index in controllers)) {
                addGamepad(gamepads[i]);
                console.log("a");
            } else {
                controllers[gamepads[i].index] = gamepads[i];
                //console.log("b");
            }
        }
    }
}

if (haveEvents) {
    window.addEventListener("gamepadconnected", connectHandler);
    window.addEventListener("gamepaddisconnected", disconnectHandler);
} else {
    setInterval(scanGamepads, 500);
}

実行させてみる

GithubリポジトリをWeb公開状態に以下においてありますのでそのまま動作させてみてください。

https://ueponx.github.io/gamepadapi/

2つのファイル(index.htmlgamepad.js)をローカルPCの同じディレクトリにおいても同じ様に使用できます。

index.htmlの開くと以下のように表示されますので、

f:id:ueponx:20190330221428p:plain

Gamepadを接続するか、Gamepadのいずれかのボタンを押すと表示が変更されます。

f:id:ueponx:20190330223116p:plain

今回接続したのは以下のUSB接続をゲームパッドを接続していますが、ちゃんと認識してくれているようです。

buffalo.jp

ボタンは物理的には10あるのですが、認識されているのは連射機能とそのOFFを除く8つのボタンでした。 また、方向ボタン(十字ボタン)はアナログスティックとして認識されているようです。ただ動かすとわかるのですが、デジタル的な動きをしているようです。

また複数のゲームパッドを接続しても認識してくれます。

f:id:ueponx:20190330222810p:plain

2つ目に接続したのはXBOX Oneの無線コントローラになります。

詳細は以下の通りです。

f:id:ueponx:20190330222636p:plain

おわりに

Gamepad APIを使ってみました。まだDraft状態のAPIではありますが、現行のモダンブラウザであれば比較的対応しているのでわりといい感じです。 ネットの情報ではブラウザ毎の挙動が微妙に違っているという話もありますが…。そのうち正式な対応されるでしょう。

各ブラウザの対応状況

対応状況をみると、IEは兎も角としてSafari

あと、パッドのおすすめはXBOXなコントローラーをデフォルト扱いとして開発されているようなので、今後コントローラーを購入するのであればXBOX用のものを おすすめします。

できれば、現在あるWebアプリなどにJoy2Keyみたいな感じでこの機能を簡単でアタッチする方法とかがあるといいなあとか思いました。 なにかつかえますかねえ。IotデバイスでWebインターフェースを作るようなパターンであれば行けそうな気もしますが。

今回やってて痛感しましたが、Javascriptスクリプト書くのは問題ないんですけど、HTMLを書くのがもう面倒で厳しいかも。JQueryとかBootstrapにシフトして行きたいです…その他のフレームワークに行ってもいいのかも。

【Live版Ubuntuへ移行】Dockerを公式チュートリアル”Get Started with Docker”で学ぶ【中編】

一連の流れのある【中編】になります。 ここまできてようやくチュートリアルのPart3が進められるような状況になりました。今回からはOSをChromeOSからUbuntu 18.02 LTSに変更して行っていきます。

【参考】

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

以前のエントリの内容と重複はありますが、チュートリアルを進めていきます。 ここまで飛ばしていいという部分には【ここまで読み飛ばしていいです】という目印をつけておこうと思います。

Get Started, Part 3: Services

ここではServiceに関して学ぶことになります。

docs.docker.com

事前準備

事前準備としてはDockerのインストールは当然のことながらPart1とPart2を行ってきた環境(コンテナ)があることが必要です。

加えてDocker Composeのインストールが必要になります。Mac用のDockerデスクトップとWindows用のDockerデスクトップにはプレインストールされていますが、Linux系は別途インストールする必要があります。以下のサイトを参考にインストールを行います。

docs.docker.com

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose

また、Part2で作成したfriendlyhelloイメージをレジストリにpushして公開することが必須になります。(ここではその共有イメージを使います。) 以下のようにDockerのイメージを実行し、http://localhost:8080/という感じでアクセスできることは確認しておいてください

※原文は4000ポートを使用していますがChromeOSではアクセスがNGなので8080に変更してありますので注意です。

【一般形】

$ docker run -p 8080:80 username/repo:tag

【前回の例で以下のように動作していました(ここでいうhogeは各自のユーザ名に変更してください)】

$ docker run -p 8080:80 hoge/get-started:part2

サービスとは?

このPartではアプリケーションを拡張し負荷分散を行います。そのために分散アプリケーションの階層を1レベル上に上げ、サービスという概念が必要になります。

階層的にはこんな感じ

  • Stack
  • Services (←いまここ)
  • Container (part2(前回)でやった部分)

分散アプリケーションはサービスという概念で構成されていて、例えばビデオ共有サイトでは、データベースサービス、ビデオをトランスコーディングするサービス、フロントエンド用のサービスから構成されているようなイメージになります。

サービスはコンテナから構成されます。その設定でイメージの実行方法、使用するポート、コンテナのレプリカをいくつ実行する必要があるかなどの体系化を行う必要があります。これらの設定を行う場合にはdocker-compose.ymlを記述することでこれらの定義、実行、拡張することが可能になります。

docker-compose.ymlを書いてみる

docker-compose.ymlでコンテナの動作を設定してみます。

以下に記載してymlファイルdocker-compose.ymlとして作成します。 このとき、Part2で作成したイメージをレジストリにpushしていることを確認して、自分の作成した username/repo:tagをイメージの詳細に置き換えてこの.ymlを更新します。 以下の例ではhoge/get-started:part2として設定しています。またポートも4000から8080へ変更しています。

【変更前(原文のまま)】

version: "3"
services:
  web:
    # replace username/repo:tag with your name and image details
    image: username/repo:tag
    deploy:
      replicas: 5
      resources:
        limits:
          cpus: "0.1"
          memory: 50M
      restart_policy:
        condition: on-failure
    ports:
      - "4000:80"
    networks:
      - webnet
networks:
  webnet:

【今回の変更後】

version: "3"
services:
  web:
    # replace username/repo:tag with your name and image details
    image: hoge/get-started:part2
    deploy:
      replicas: 5
      resources:
        limits:
          cpus: "0.1"
          memory: 50M
      restart_policy:
        condition: on-failure
    ports:
      - "8080:80"
    networks:
      - webnet
networks:
  webnet:

このdocker-compose.ymlでは次のような動きを記述しています。

  1. Part2で作成したイメージをリポジトリから取得。
  2. 取得したイメージから5つのインスタンスを生成して、webと呼ばれるサービスとして名づけます。それぞれに対してCPU資源は10%、50MBのRAMを割当て。
  3. コンテナでエラーがあった場合には再起動。
  4. webサービスの各インスタンスの80ポートはローカルホストからは8080ポート経由からアクセス可能。(オリジナルの記述は4000ポート)
  5. webnetと呼ばれる負荷分散ネットワークを生成し、ポート80を共有するようにwebのコンテナに指示します。 (内部的にはコンテナ自体が一時ポートでwebのポート80に発行)
  6. webnetネットワークをデフォルト設定(負荷分散オーバーレイネットワーク)で定義。

【ここまで読み飛ばしていいです】

負荷分散ネットワークアプリの起動

まずは以下のコマンドを起動します。コマンドの意味はPart4で説明がありますが、これを実行しないとエラーが発生します。

$ docker swarm init

【ログ】

$ docker swarm init
Swarm initialized: current node (nc03uv6m1z1dxgwxdilvh29rj) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-1dukwcs4w07qt835cw6o5w0aju191e93be2jtctejrndzj5jyd-25d5b683d7jx4hceptehbtrz9 192.168.0.7:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

つづいて以下のように実行します。この実行ではアプリはgetstartedlabと設定しています。

$ docker stack deploy -c docker-compose.yml getstartedlab

【ログ】

$ docker stack deploy -c docker-compose.yml getstartedlab
Creating network getstartedlab_webnet
Creating service getstartedlab_web

これで動作の確認になります。ここからはChromeOSではうまく動作しなかった部分になりますが… 以下のコマンドで確認します。

$ docker service ls

【ログ】

$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                      PORTS
p1dyl3harlzm        getstartedlab_web   replicated          0/5                 hoge/get-started:part2   *:8080->80/tcp

お!あれ?だめなのか?

念の為、コンテナの動作状況を以下のコマンドで確認してみます。

$ docker service ps getstartedlab_web

【ログ】

$ docker service ps getstartedlab_web 
ID                  NAME                  IMAGE                      NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
lgz0af823dtz        getstartedlab_web.1   hoge/get-started:part2   ubuntu              Running             Running 50 seconds ago                       
n2bgt0wx11kg        getstartedlab_web.2   hoge/get-started:part2   ubuntu              Running             Running 22 seconds ago                       
0b1v6d8jy5lu        getstartedlab_web.3   hoge/get-started:part2   ubuntu              Running             Running 50 seconds ago                       
sm21dfefpfgw        getstartedlab_web.4   hoge/get-started:part2   ubuntu              Running             Running 32 seconds ago                       
isx43duompwd        getstartedlab_web.5   hoge/get-started:part2   ubuntu              Running             Running 11 seconds ago 

ステータスはRunningになっています!動作しているようです。念の為もう一度Serviceの動作状況の確認を調べてみると…

$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                      PORTS
p1dyl3harlzm        getstartedlab_web   replicated          5/5                 hoge/get-started:part2   *:8080->80/tcp

無事に起動しているようです。ちょっとホッとしました。

今回の動作環境ではDockerでaufsを使用していないため、パフォーマンスが劇的に低くなっているため各コンテナの起動に時間がかかってしまったのではないかと考えます。(他の例でも、起動に関するパフォーマンスは明らかに落ちているのはわかります。)

ではブラウザでこの負荷分散サービスへアクセスしていきます。 アクセス先は同様にhttp://localhost:8080/になります。このURLにアクセスするとロードバランスを図ってくれるのでアクセスする度(リロードする度)に 表示されるHostIDが変化していくことを確認してください。

f:id:ueponx:20190322062553p:plain

f:id:ueponx:20190322062602p:plain

f:id:ueponx:20190322062611p:plain

$ docker swarm init
Swarm initialized: current node (nc03uv6m1z1dxgwxdilvh29rj) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-1dukwcs4w07qt835cw6o5w0aju191e93be2jtctejrndzj5jyd-25d5b683d7jx4hceptehbtrz9 192.168.0.7:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

$ docker stack deploy -c docker-compose.yml getstartedlab
Creating network getstartedlab_webnet
Creating service getstartedlab_web

$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                      PORTS
p1dyl3harlzm        getstartedlab_web   replicated          0/5                 hoge/get-started:part2   *:8080->80/tcp

$ docker service ps getstartedlab_web 
ID                  NAME                  IMAGE                      NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
lgz0af823dtz        getstartedlab_web.1   hoge/get-started:part2   ubuntu              Running             Running 50 seconds ago                       
n2bgt0wx11kg        getstartedlab_web.2   hoge/get-started:part2   ubuntu              Running             Running 22 seconds ago                       
0b1v6d8jy5lu        getstartedlab_web.3   hoge/get-started:part2   ubuntu              Running             Running 50 seconds ago                       
sm21dfefpfgw        getstartedlab_web.4   hoge/get-started:part2   ubuntu              Running             Running 32 seconds ago                       
isx43duompwd        getstartedlab_web.5   hoge/get-started:part2   ubuntu              Running             Running 11 seconds ago                       

$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                      PORTS
p1dyl3harlzm        getstartedlab_web   replicated          5/5                 hoge/get-started:part2   *:8080->80/tcp

f:id:ueponx:20190324071640p:plain


アプリケーションのスケールをしてみる

スケールをするのは簡単です。 docker-compose.ymlのレプリカの数を変更して保存し、再度docker stack deployコマンドを再実行するだけでアプリを拡張できます。 以下の例ではレプリカ数を5から3へ変更しています。非常にシームレスにスケールするのがわかります。

$ vim docker-compose.yml

$ docker stack deploy -c docker-compose.yml getstartedlab
Updating service getstartedlab_web (id: p1dyl3harlzm5ghawpj04noay)

$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                      PORTS
p1dyl3harlzm        getstartedlab_web   replicated          3/3                 hoge/get-started:part2   *:8080->80/tcp

$ docker service ps getstartedlab_web 
ID                  NAME                      IMAGE                      NODE                DESIRED STATE       CURRENT STATE             ERROR               PORTS
7qeqrcn6etna        getstartedlab_web.1       hoge/get-started:part2   ubuntu              Running             Running 20 seconds ago                        
lgz0af823dtz         \_ getstartedlab_web.1   hoge/get-started:part2   ubuntu              Shutdown            Shutdown 24 seconds ago                       
y52g099pbi4f        getstartedlab_web.2       hoge/get-started:part2   ubuntu              Running             Starting 2 seconds ago                        
n2bgt0wx11kg         \_ getstartedlab_web.2   hoge/get-started:part2   ubuntu              Shutdown            Shutdown 2 seconds ago                        
0b1v6d8jy5lu        getstartedlab_web.3       hoge/get-started:part2   ubuntu              Running             Running 10 minutes ago

負荷分散させるアプリケーションがdocker-comporse.ymlの変更を行ってコマンド一発でスケールできるのは非常に便利です!

後片付け

Serviceの終了を指せる場合には以下のコマンドを使用します。

$ docker stack rm getstartedlab
$ docker swarm leave --force

これで今回の環境は完全に終了します。

【ログ】

$ docker stack rm getstartedlab
Removing service getstartedlab_web
Removing network getstartedlab_webnet

$ docker swarm leave --force
Node left the swarm.

f:id:ueponx:20190322062709p:plain

これでPart3は終了です。

Get Started, Part 4: Swarms

ここからはPart4でSwarmに関する部分になります。(Part3でもやってましたけど) Part4では、Swarmとはどういうものか、Swarm内のノードがマネージャまたはワーカーになる方法、Swarmを作成、その上にアプリをデプロイを行っていきます。 また、その環境下でPart3同様に異なるマシンで構成でDockerの基本機能、ネットワーク機能、負荷分散やスケールできることを実験してみます。

docs.docker.com

前提としてはPart3までが完了していればOKということになるのですが、新たにdocker-machineコマンドが必要になります。 WindowsOSXであればDocker環境をインストールするとすでに入るのですが、Linuxの場合には別途インストールが必要になります。 以下のサイトを見ながら作業となります。

docs.docker.com

抜粋すると以下のコマンドになります。(versionは0.16.0の場合)

f:id:ueponx:20190327055356p:plain

$ base=https://github.com/docker/machine/releases/download/v0.16.0 && curl -L $base/docker-machine-$(uname -s)-$(uname -m) >/tmp/docker-machine && sudo install /tmp/docker-machine /usr/local/bin/docker-machine

インストール後の動作チェックは以下で大丈夫です。

$ docker-machine version
docker-machine version 0.16.0, build 702c267f

f:id:ueponx:20190327055412p:plain

クラスタの作成

ドキュメントを読んでいくと…

You need a hypervisor that can create virtual machines (VMs), so install Oracle VirtualBox for your machine’s OS.

なんとvirtualboxをインストールしてVMを起動しろと言われます。(ChromeOSではここまで来ると本末転用のような状況…OS環境を変えて正解だったのかも) 仕方ないので、以下のリンクからUbuntu用のUbuntu 18.04 (Bionic) / Ubuntu 18.10 (Cosmic) / Debian Unstableパッケージをダウンロードします。

www.oracle.com

Ubuntuの場合、ブラウザ(Firefox)でダウンロードすると.debファイルを認識してパッケージのインストールを行ってくれますので、 そのままインストールしてしまいましょう。

インストールが完了したら、以下のように2台のVMを起動します。--driverで指定したVM環境が使用されVMが作成されます。

$ docker-machine create --driver virtualbox myvm1
$ docker-machine create --driver virtualbox myvm2

f:id:ueponx:20190327055425p:plain

f:id:ueponx:20190327055439p:plain

起動したVMmyvm1myvm2という名前で起動しています。このVMIPアドレスは以下のコマンドで調べることができます。

$ docker-machine ls
ubuntu@ubuntu:~$ docker-machine ls
NAME    ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER     ERRORS
myvm1   -        virtualbox   Running   tcp://192.168.99.101:2376           v18.09.3   
myvm2   -        virtualbox   Running   tcp://192.168.99.102:2376           v18.09.3   

f:id:ueponx:20190327055451p:plain

VMIPアドレスがわかったら、2つともIPアドレスを控えておきます。そのうちの1台(今回はmyvm1側)をSwarmのマネージャホストにします。 該当するホストでdocker-machine ssh経由でdocker swarm initコマンドを実行します。実行時に表示されるtokenはメモしておきます。というかコマンドそのものをコピーしておいたほうがいいかもしれません。(docker swarm join ほげほげの部分)

$ docker-machine ssh myvm1 "docker swarm init --advertise-addr <myvm1 ip>"
Swarm initialized: current node <node ID> is now a manager.

To add a worker to this swarm, run the following command:

  docker swarm join \
  --token <token> \
  <myvm ip>:<port>

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

f:id:ueponx:20190327055503p:plain

【ログ】

$ docker-machine ssh myvm1 "docker swarm init --advertise-addr 192.168.99.101"
Swarm initialized: current node (76irf1ec7b14xfns1oym7638x) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-0hdt9lh014fow8y0yrzw6sruqdzb275sw2ohwhiolzfy0ev05z-42bhd4wxtwd9hu7ndx9prrzzd 192.168.99.101:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

念の為、Swarmを使用する場合にはポート2377とポート2376を使用する可能性があるので、使用しないように注意が必要です。 また、上記のdocker-machine sshでコマンドを送信できない場合にはホストにネイティブにインストールされているsshコマンドを使用する事もできます その場合には--native-ssh sshオプションを追加設定します。具体的には以下のようになります。

$ docker-machine --native-ssh ssh myvm1  "docker swarm init --advertise-addr <myvm1 ip>"

続いて2台目のVM(今回はmyvm2)をワーカーとして登録します。その際は先程実行したdocker swarm initの戻りでtokenが表示されているので それをオプション指定してdocker swarm joinをホストに送信します。例のは適切なものに変更を行ってください。

$ docker-machine ssh myvm2 "docker swarm join --token <token> <ip>:2377"

f:id:ueponx:20190327055520p:plain

$ docker-machine ssh myvm2 "docker swarm join --token SWMTKN-1-0hdt9lh014fow8y0yrzw6sruqdzb275sw2ohwhiolzfy0ev05z-42bhd4wxtwd9hu7ndx9prrzzd 192.168.99.101:2377"
This node joined a swarm as a worker.

これでSwarmが生成できました。以下のコマンドで生成されたノードを表示してみます。

$ docker-machine ssh myvm1 "docker node ls"

f:id:ueponx:20190327055530p:plain

ここでもし最初からやり直す場合にはdocker swarm leaveを実行するとSwarmが終了し元に戻ります。

アプリのデプロイを行う

Swarmができあがったので、アプリをデプロイしていきます。

これまではdocker-machine sshで実行していましたが、これではいちいちコマンドを実行していく必要があるので結構手間です。そこでdocker-machine envで得られる情報を元にローカルのdocker-compose.ymlファイルを使用してデプロイする方法もあります。今回はそれを使用してみます。

環境変数の設定はdocker-machine envで取得できるので

$ docker-machine env myvm1
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.101:2376"
export DOCKER_CERT_PATH="/home/ubuntu/.docker/machine/machines/myvm1"
export DOCKER_MACHINE_NAME="myvm1"
# Run this command to configure your shell: 
# eval $(docker-machine env myvm1)

この情報をevalで取り込んで有効化します。

$ eval $(docker-machine env myvm1)

f:id:ueponx:20190327055542p:plain

すると、今回作成したmyvm1がアクティブ状態になります。

$ docker-machine ls
NAME    ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER     ERRORS
myvm1   *        virtualbox   Running   tcp://192.168.99.101:2376           v18.09.3   
myvm2   -        virtualbox   Running   tcp://192.168.99.102:2376           v18.09.3   

f:id:ueponx:20190327055552p:plain

Part3と同様にアプリのデプロイして、動作を確認してみます。

【注意】デプロイを行う前にdocker-compose.ymlファイルのあるパスに移動しておいてください。 myvm1のホスト上で以下のコマンドを実行すると

$ cd ./friendlyhello/
$ docker stack deploy -c docker-compose.yml getstartedlab

【ログ】

$ docker stack deploy -c docker-compose.yml getstartedlab
Creating network getstartedlab_webnet
Creating service getstartedlab_web

myvm1myvm2にアプリが分散配置されて起動されます。以下のコマンドで分散の状況を確認することができます。

$ docker stack ps getstartedlab

f:id:ueponx:20190327062929p:plain

$ docker stack ps getstartedlab
ID                  NAME                  IMAGE                      NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
886onhvdec3j        getstartedlab_web.1   ueponx/get-started:part2   myvm2               Running             Running 3 minutes ago                        
ukvh7zbk85e8        getstartedlab_web.2   ueponx/get-started:part2   myvm1               Running             Running 3 minutes ago                        
tgvo877hsaox        getstartedlab_web.3   ueponx/get-started:part2   myvm2               Running             Running 3 minutes ago                        
ffzmzokyv6v7        getstartedlab_web.4   ueponx/get-started:part2   myvm1               Running             Running 3 seconds ago                       
jq5q4ukq9w6b        getstartedlab_web.5   ueponx/get-started:part2   myvm1               Running             Running 3 seconds ago

ここまできたらブラウザなどを使ってmyvm1またはmyvm2IPアドレスにアクセスを行います。

【myvm1へのアクセス】

f:id:ueponx:20190327055657p:plain

f:id:ueponx:20190327055708p:plain

f:id:ueponx:20190327055721p:plain

【myvm2へのアクセス】

f:id:ueponx:20190327055612p:plain

f:id:ueponx:20190327055623p:plain

f:id:ueponx:20190327055633p:plain

f:id:ueponx:20190327055646p:plain

この環境でスケールさせる場合にはdocker-compose.ymlを修正し、docker stack deployを実行することでスケールが用意にできます。またノードの追加に関しても今回のmyvm2と同様に追加することでホスト単位のスケールも用意にできるようになります。

stackの終了方法

stackの終了方法は今回の場合は以下のようになります。

$ docker stack rm getstartedlab

環境変数のクリア

docker-machine envで設定した設定は以下のように行うとクリアできます。

$ eval $(docker-machine env -u)

ローカルホストを終了させればdockerのマシンも終了します。状態を確認する場合には以下のコマンドとなります。

$ docker-machine ls

停止したノードを再起動させる場合にはホスト名を指定して以下のように再起動ができます。

$ docker-machine start <machine-name>

おわりに

OSを変更することでようやくPart3とPart4がおわらせられました。しかし、このチュートリアルかなり事前準備の要求が高いですね。あともう少し頑張ってみます。

【参考エントリー】

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

Ubuntu/DebianのUSB起動のLIVE版でDockerを動作させる

前回の挫折を受けてChromeOSからUbuntuなどへの移行を考え、折角インストールしたChromeOSを消すのも、もったいないのでUSB版のLive環境をつかって継続してDockerを学ぶように方針変更をしたらまたトラブルが…という内容です。キッカケになった挫折に関しては以下参照で。

【参考】

uepon.hatenadiary.com

CentOSでも良かったのですが、USBメモリへの変更設定の保存などはできなかったので、今回はUbuntu18.02 LTSを使用することにしました。Debian系のLinuxはLive版でも設定やファイルをUSBメモリに保存できるのがいいですよね。ということでUbuntuの起動可能なメディアを作成します。ISOイメージがあれば以下のようなツールを使用することでUSBメモリ起動メディアが作成できます。

このツールを使用すると良いかなと思います。(ただし、イメージのダウンロードに関してはこのツール経由で行わない方が高速にできます)

unetbootin.github.io

作成したUSBメモリからPCを起動したら、ネットワークやキーボードマップなど設定を行いそのあとにDockerをインストールしていきます。公式ドキュメントもしっかりしているのでこちらを参考にしてもらえればと思います。

【公式ドキュメント】

docs.docker.com

コマンドだけをまとめると以下のような感じになります。

$ sudo apt-get remove docker docker-engine docker.io containerd runc
$ sudo apt-get update
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
$ sudo docker run hello-world

これで完了するはずなのですが…また罠発生。インストール確認用の最後のテストコマンドであるsudo docker run hello-worldを実行してみると

$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete 
Digest: sha256:2557e3c07ed1e38f26e389462d03ed943586f744621577a99efb77324b0fe535
Status: Downloaded newer image for hello-world:latest
docker: Error response from daemon: error creating aufs mount to /var/lib/docker/aufs/mnt/b329231382bf8b24662da048500b454af804796ecdf11d92002f97186141984e-init: invalid argument.
See 'docker run --help'.

docker: Errorが発生します。このエラーはUbuntu 16.02 LTSDebian(最新)でも同じ状況になるようです。ググってもあまり情報がないことをみるとどうもUSB版のLive環境に固有の状況の模様。

エラーの内容からするとaufsというファイルシステムにコンテナイメージが生成できないよっていうエラーな感じです。

Wikipediaによれば

ja.wikipedia.org

aufs (AnotherUnionFS) は Linuxファイルシステムサービスであり、複数の異なるファイルシステム (ブランチと呼ばれる) のファイルやディレクトリ同士を透過的に重ねる (マージする) ことができる技術である。UnionFS を完全に書き換えるもので、信頼性とパフォーマンスの改善を狙いとしている。

とのことですが、このあとに

DebianUbuntu 系には aufs を利用し、ファイルシステムへの変更をメモリ ( tmpfs ) に保存するためのツール、fsprotec が入っている。これにより、実験的に設定ファイルを変更した状態でサーバを動作させ、再起動さえすれば元の状態に復元出来る、といった使い方も可能になる。コンテナ型仮想化技術であるDockerでは aufs をストレージドライバとして利用し、同技術の特徴である差分管理を実現している。

いろいろ調べてみるとDockerで使用するストレージドライバに問題があるような感じのようです。

さらにググってみたらこういう情報が!

stackoverflow.com

この情報を元にDockerの起動オプションでストレージドライバの変更を行います。Dockerの起動オプションを設定するファイルは/lib/systemd/system/docker.serviceとなるのでこれを編集して--storage-driver=vfsを追加することにします。

$ sudo vim /lib/systemd/system/docker.service

次の行を追加します。

…(略)...
ExecStart=/usr/bin/dockerd --storage-driver=vfs -H fd://
…(略)...

実際に追加したファイルは次の様になります。

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
BindsTo=containerd.service
After=network-online.target firewalld.service containerd.service
Wants=network-online.target
Requires=docker.socket

[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd --storage-driver=vfs -H fd:// --containerd=/run/containerd/containerd.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always

# Note that StartLimit* options were moved from "Service" to "Unit" in systemd 229.
# Both the old, and new location are accepted by systemd 229 and up, so using the old location
# to make them work for either version of systemd.
StartLimitBurst=3

# Note that StartLimitInterval was renamed to StartLimitIntervalSec in systemd 230.
# Both the old, and new name are accepted by systemd 230 and up, so using the old name to make
# this option work for either version of systemd.
StartLimitInterval=60s

# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity

# Comment TasksMax if your systemd version does not supports it.
# Only systemd 226 and above support this option.
TasksMax=infinity

# set delegate yes so that systemd does not reset the cgroups of docker containers
Delegate=yes

# kill only the docker process, not all processes in the cgroup
KillMode=process

[Install]
WantedBy=multi-user.target

起動が終わったらDockerサービスをリスタート(PCの再起動でも可)してDockerのテストコマンドを実行すると

Dockerサービスのリスタートコマンドは以下の通りです。

$ sudo systemctl restart docker

【Dockerのテスト実行の結果】

$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete 
Digest: sha256:2557e3c07ed1e38f26e389462d03ed943586f744621577a99efb77324b0fe535
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

見事に実行できました!ありがとう!

おわりに

Live環境でも通常OSと環境的には大差ないだろうと思っていたのですが、やっぱりそうでもなかったということでした。 しかし、はまりどころに良くあたる…これで2日つぶした。

(注意)ストレージドライバをaufsからvfsに変更するとDockerのパフォーマンスがかなり悪くなります。特にイメージをpullするときやbuildするときなどは顕著な様です。Live環境なのでそんな酷使することはないと思いますが、そのあたりを頭に入れて使用するのが良いようです。

【ChromeOSインストール版】Dockerを公式チュートリアル”Get Started with Docker”で学ぶ【挫折編】

前回の続きでChromeOSにDockerをインストールしてみたので公式のドキュメントを見ながら学んで見たいと思います。

【関連エントリ】

uepon.hatenadiary.com

今回はPart3の部分になります。(本当はPart4まで進めたかったが)

ただ、読みすすめるとわかると思いますが、うまくいきません。

原因は未だにわからないのですが、同じPCで別のOS(UbuntuDebianCentOSのそれぞれUSB起動のLive版で確認)では起動できているのでChromeOSならではの問題かと思います。

Get Started, Part 3: Services

ここではServiceに関して学ぶことになります。

事前準備

事前準備としてはDockerのインストールは当然のことながらPart1とPart2を行ってきた環境(コンテナ)があることが必要です。

加えてDocker Composeのインストールが必要になります。Mac用のDockerデスクトップとWindows用のDockerデスクトップにはプレインストールされていますが、Linux系は別途インストールする必要があります。以下のサイトを参考にインストールを行います。

docs.docker.com

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose

【インストールログ】

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   617    0   617    0     0    859      0 --:--:-- --:--:-- --:--:--   858
100 11.2M  100 11.2M    0     0  2250k      0  0:00:05  0:00:05 --:--:-- 2910k

$ sudo chmod +x /usr/local/bin/docker-compose

$ docker-compose --version
docker-compose version 1.23.2, build 1110ad01

また、Part2で作成したfriendlyhelloイメージをレジストリにpushして公開することが必須になります。(ここではその共有イメージを使います。) 以下のようにDockerのイメージを実行し、http://localhost:8080/という感じでアクセスできることは確認しておいてください

※原文は4000ポートを使用していますがChromeOSではアクセスがNGなので8080に変更してありますので注意です。

【一般形】

$ docker run -p 8080:80 username/repo:tag

【前回の例で以下のように動作していました(ここでいうhogeは各自のユーザ名に変更してください)】

$ docker run -p 8080:80 hoge/get-started:part2

サービスとは?

このPartではアプリケーションを拡張し負荷分散を行います。そのために分散アプリケーションの階層を1レベル上に上げ、サービスという概念が必要になります。

階層的にはこんな感じ

  • Stack
  • Services (←いまここ)
  • Container (part2(前回)でやった部分)

分散アプリケーションはサービスという概念で構成されていて、例えばビデオ共有サイトでは、データベースサービス、ビデオをトランスコーディングするサービス、フロントエンド用のサービスから構成されているようなイメージになります。

サービスはコンテナから構成されます。その設定でイメージの実行方法、使用するポート、コンテナのレプリカをいくつ実行する必要があるかなどの体系化を行う必要があります。これらの設定を行う場合にはdocker-compose.ymlを記述することでこれらの定義、実行、拡張することが可能になります。

docker-compose.ymlを書いてみる

docker-compose.ymlでコンテナの動作を設定してみます。

以下に記載してymlファイルdocker-compose.ymlとして作成します。 このとき、Part2で作成したイメージをレジストリにpushしていることを確認して、自分の作成した username/repo:tagをイメージの詳細に置き換えてこの.ymlを更新します。 以下の例ではhoge/get-started:part2として設定しています。またポートも4000から8080へ変更しています。

【変更前(原文のまま)】

version: "3"
services:
  web:
    # replace username/repo:tag with your name and image details
    image: username/repo:tag
    deploy:
      replicas: 5
      resources:
        limits:
          cpus: "0.1"
          memory: 50M
      restart_policy:
        condition: on-failure
    ports:
      - "4000:80"
    networks:
      - webnet
networks:
  webnet:

【今回の変更後】

version: "3"
services:
  web:
    # replace username/repo:tag with your name and image details
    image: hoge/get-started:part2
    deploy:
      replicas: 5
      resources:
        limits:
          cpus: "0.1"
          memory: 50M
      restart_policy:
        condition: on-failure
    ports:
      - "8080:80"
    networks:
      - webnet
networks:
  webnet:

このdocker-compose.ymlでは次のような動きを記述しています。

  1. Part2で作成したイメージをリポジトリから取得。
  2. 取得したイメージから5つのインスタンスを生成して、webと呼ばれるサービスとして名づけます。それぞれに対してCPU資源は10%、50MBのRAMを割当て。
  3. コンテナでエラーがあった場合には再起動。
  4. webサービスの各インスタンスの80ポートはローカルホストからは8080ポート経由からアクセス可能。(オリジナルの記述は4000ポート)
  5. webnetと呼ばれる負荷分散ネットワークを生成し、ポート80を共有するようにwebのコンテナに指示します。 (内部的にはコンテナ自体が一時ポートでwebのポート80に発行)
  6. webnetネットワークをデフォルト設定(負荷分散オーバーレイネットワーク)で定義。

負荷分散ネットワークアプリの起動

まずは以下のコマンドを起動します。コマンドの意味はPart4で説明がありますが、これを実行しないとエラーが発生します。

$ docker swarm init

つづいて以下のように実行します。この実行ではアプリはgetstartedlabと設定しています。

$ docker stack deploy -c docker-compose.yml getstartedlab

【ログ】

$ docker swarm init
Swarm initialized: current node (lj9h2t6lfbifgh9xs3ade0d5g) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-15hfgnp2gthm9vvlu3sk7l628c8q8dthz639bpv3f1ziqogx6w-1djxiz46bjblng15l3qhno4cq 100.115.92.198:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

$ docker stack deploy -c docker-compose.yml getstartedlab
Creating network getstartedlab_webnet
Creating service getstartedlab_web

サービスでは5つのコンテナを起動しているのでそれを確認します。確認方法は以下のようになります。

$ docker service ls

【ログ】

$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                      PORTS
padrydlvz757        getstartedlab_web   replicated          0/5                 hoge/get-started:part2   *:4000->80/tcp

おや?0/5ってなってる…起動できていない…いつまでたってもこの状態…

サービス内で実行されているコンテナはタスクと呼ばれます。タスクには、docker-compose.ymlで定義したレプリカの数まで、数値が増加する固有のIDが与えられます。 ってことなんですが、起動と再起動を繰り返しているっぽい。

$ docker service ps getstartedlab_web
ID                  NAME                      IMAGE                      NODE                DESIRED STATE       CURRENT STATE                 ERROR                              PORTS
so2dl24jsz9g        getstartedlab_web.1       hoge/get-started:part2   penguin             Ready               Assigned 3 seconds ago                                           
zl2amqebzsaq         \_ getstartedlab_web.1   hoge/get-started:part2   penguin             Shutdown            Failed 5 seconds ago          "starting container failed: fa…"   
8ok1l8k9t5qc         \_ getstartedlab_web.1   hoge/get-started:part2   penguin             Shutdown            Rejected 41 seconds ago       "error creating external conne…"   
miejmm2yf3r6         \_ getstartedlab_web.1   hoge/get-started:part2   penguin             Shutdown            Rejected about a minute ago   "error creating external conne…"   
tlhz1mzxkw4g         \_ getstartedlab_web.1   hoge/get-started:part2   penguin             Shutdown            Failed about a minute ago     "starting container failed: fa…"   
t0a3hi9trnev        getstartedlab_web.2       hoge/get-started:part2   penguin             Ready               Assigned 3 seconds ago                                           
ajg4c4ws13f7         \_ getstartedlab_web.2   hoge/get-started:part2   penguin             Shutdown            Failed 5 seconds ago          "starting container failed: fa…"   
klph7llfrxzd         \_ getstartedlab_web.2   hoge/get-started:part2   penguin             Shutdown            Failed 33 seconds ago         "starting container failed: fa…"   
lxb65skkypfp         \_ getstartedlab_web.2   hoge/get-started:part2   penguin             Shutd

コンテナ数を変更しても、Dockerfileを編集して構成を変えてもだめ。ChromeOSではこれ以降はうまくできないようです。(2019.03.18現在) Swarm系の動作はダメなんでしょうか…

現状進めることはできないので、とりあえず以下のコマンドで終了させます。

$ docker stack rm getstartedlab
$ docker swarm leave --force

挫折してみて…

原因はまだよくわかっていませんが、本来の目的はDockerを学ぶことなので放置します。 ChromeOSに関してはやっぱり通常のLinuxとは違うのでしょうか。sshなターミナルマシンとしてはいい感じなんですけどなー

次はOSをUbuntu18.04 LTSのUSB Live版に変更して継続していこうと思います。 もう一回同じPartから進めていこうと思います。

Windows10のデフォルトコマンドでUSBメモリのパーティション関連をクリーンにする

USBメモリを使用してLinuxなどのLive環境やChromeOSのインストールなどを使用していると、そのUSBメモリの再利用するために 再フォーマットをしようとするとパーティション関連が特異な状態になってしまいWindowsの【ディスクの管理】からでは 結構面倒な操作をする必要などが出てきてしまいます。

f:id:ueponx:20190316154950p:plain

単純にRaspbianなどのイメージ書き込みするだけであれば、そこまでの必要性はないのすが…UNetbootinなどのLiveUSB作成ツールでは そこまでやってくれないので自分で対応する必要がでてくるわけです。

【参考】 unetbootin.sourceforge.net

もう少し楽な感じでやれないかなと思って調べてみました。

diskpart.exeを起動する

cmd.exeもしくは【ファイル名を指定して実行】(Winキー + r)からdiskpart.exeを起動します。

f:id:ueponx:20190316155059p:plain

するとdiskpart.exeが起動します。

f:id:ueponx:20190317081339p:plain

接続されたディスクのリストを表示して、作業対象になるUSBメモリのディスク番号を確認します。

ディスク一覧を表示する

DISKPART> list disk

このリストの中からUSBメモリ確認した番号でを選択し作業を行っていくことになります。

f:id:ueponx:20190317081357p:plain

番号指定はしっかりと確認しましょう。間違えるとウルトラやばいです。(パーティションを消してしまいますのでホント気をつけましょう)

作業対象のディスクを選択する

続いて実際に作業対象の設定を行います。

DISKPART> select disk [ディスク番号]

f:id:ueponx:20190317081433p:plain

ディスク情報の詳細を確認する

念の為、選択したディスクの情報を表示し、間違いないことを確認します。

DISKPART> detail disk

f:id:ueponx:20190317081458p:plain

ディスクから構成情報を削除する

ディスクのパーティション情報をクリアします。

DISKPART> clean

cleanコマンドでディスクから構成情報を全て削除します。 (実行するとパーティーションが何もない状態になります。)

f:id:ueponx:20190317081627p:plain

実行したら念の為ディスクの詳細を確認します。

DISKPART> detail disk

detail diskで表示を確認する。

f:id:ueponx:20190317095121p:plain

プライマリパティーションを作成する。

これでUSBはきれいな状態になっているのでプライマリパーティションを作成して使えるようにしていきます。

DISKPART> create partition primary

f:id:ueponx:20190317081716p:plain

あとは、通常の手順で好きなフォーマットでフォーマットすればOKです。

【フォーマット前】 f:id:ueponx:20190317081745p:plain

FAT32でフォーマット後】 f:id:ueponx:20190317081751p:plain

おわりに

いろんなOSのUSB起動をやっているとUSB内のパーティション構造が特殊になっていくのでその対応に関して調べてみました。 一般的な使い方をしている場合には、そんなに使わなくてもいいはずなんですが…