いろいろ書いていく

やってみたなど

dockerで簡単なNode.jsスクリプトを実行する

お題

dockerでNode.jsスクリプトを起動してみる.
nodeはホスト端末でやる場合,初期セッティングとかバージョン管理とか面倒くさい(らしい)
簡単なスクリプトであれば,docker上で動かしてみることでテストが楽になるかも.

作業環境

Docker 18.09.3 on Ubuntu 18.04 (virtualbox on macOS)
ubuntuにdockerをインストールする手順はこちら:
koimedenshi.hatenablog.com

やること

これまでの記事同様,dockerhubのドキュメントを参考に手を動かしてみる.
nodeのhow to useドキュメントは長くてdockerhubのページに収まらない(dockerhubには文字数制限があるらしい)ので,以下のgithubを見るように書いてあった.

github.com

とりあえずこれをやってみる.

手順

上記のドキュメントには3通りの方法が書いてあった.

  • Dockerfileで起動
  • docker-composeで起動
  • コマンド1行で起動

順番にやってみる.

1. Dockerfileで起動

新規のディレクトリで新規Dockerfileを作成

touch Dockerfile

Dockerfileに以下の内容を書き込む

# specify the node base image with your desired version node:<version>
FROM node:8
# replace this with your application's default port
EXPOSE 8888

docker buildでimageを作成する

$ docker build -t my-nodejs-app .

作成したimageでコンテナを起動する⇨コンテナ内のnode.js対話環境に接続され,nodeコマンドをインタラクティブに実行できる.

$ docker run -it --rm --name my-running-app my-nodejs-app
> # 対話環境に接続
> console.log("hello world");
hello world
undefined
>
> .help # ヘルプ
.break    Sometimes you get stuck, this gets you out
.clear    Alias for .break
.editor   Enter editor mode
.exit     Exit the repl
.help     Print this help message
.load     Load JS from a file into the REPL session
.save     Save all evaluated commands in this REPL session to a file
>
> .exit # 終了

-rmオプションをつけて起動したので,対話シェルの終了とともにコンテナが削除される

2. docker-composeで起動

例示されているdocker-compose.yamlは以下の通り

version: "2"
services:
  node:
    image: "node:8"
    user: "node"
    working_dir: /home/node/app
    environment:
      - NODE_ENV=production
    volumes:
      - ./:/home/node/app
    expose:
      - "8081"
    command: "npm start"

これだけではNode.jsスクリプトを実行できない.
このコンテナは起動後にnpm startコマンドを実行するが,このコマンドを実行するには同じディレクトリ内にpackage.jsonという設定ファイルが設置されている必要がある.
https://nodejs.org/en/docs/guides/nodejs-docker-webapp/ を参考に作成したpackage.jsonがこちら.

{
  "name": "docker_web_app",
  "version": "1.0.0",
  "description": "Node.js on Docker",
  "author": "XXXX XXXX",
  "main": "myfirst.js",
  "scripts": {
    "start": "node myfirst.js"
  }
}

これでコンテナ内でnode myfirst.jsが実行されるようになる.
次にmyfirst.jsを用意する.
https://www.w3schools.com/nodejs/nodejs_get_started.asp から引用したjsがこちら.

var http = require('http');

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('Hello World!');
}).listen(8080);

最終的に以下の3つのファイルを同一ディレクトリに用意することになる.

$ tree .
.
├── docker-compose.yaml
├── myfirst.js
└── package.json

このディレクトリに移動して以下のコマンドを実行する

docker-compose up -d

これでコンテナが起動しhttpサーバが待ち受け状態になるが,ポート8081でexposeしているので,自分自身またはリンクしたコンテナからしかアクセスできない.

  • 自分自身からアクセスするには以下のようにすればよい
docker exec -it <node.jsコンテナ名> sh
#/ curl localhost:8080
  • 別のコンテナからリンクさせる場合は,同一ネットワークでコンテナ同士を接続する(長くなるので省略)

  • ホストからアクセスしたい場合は,最初のdocker-compose.yamlをexposeでなくportsにすればよい

    expose:
      - "8081"

これを

    ports:
      - "8081:8080"

こうする.

この例は(チュートリアルとしては)お手軽感が薄い.

3. コマンド1行で起動

単一のNode.jsスクリプトをお手軽に実行したい場合は,コマンドオプションで指定するのが楽.
例示されているコマンドは以下

docker run -it --rm --name my-running-script -v "$PWD":/usr/src/app -w /usr/src/app node:8 node your-daemon-or-script.js

-vオプションでカレントディレクトリのファイルを/usr/src/appにコピーしているので,コマンド実行時のカレントディレクトリにNode.jsスクリプトを置いておけばよい.

例えば以下のようなスクリプトを用意する(前述のmyfirst.jsと同一)

var http = require('http');

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('Hello World!');
}).listen(8080);

このように実行したいNode.jsスクリプトがhttpサーバのような場合,-pオプションでポートを解放しないとホストからアクセスできない.
例えば-p 8081:8080オプションをつけることで,localhost:8081からアクセスできるようにする.

最終的なコマンドは以下の通り

docker run -it --rm -p 8081:8080 --name my-running-script -v "$PWD":/usr/src/app -w /usr/src/app node:8 node your-daemon-or-script.js

コマンド実行後http://localhost:8081にアクセスすればHello World!となるはず.

おわりに

dockerでNode.jsスクリプトを実行する方法を,https://github.com/nodejs/docker-node/blob/master/README.md#how-to-use-this-imageを参考にやってみた.
Node.jsのdockerコンテナ化に関しては色々ドキュメントがあるので,参考にしていきたい.

nodejs.org

github.com

swarm tutorialをdindでやってみる 完結編

やること

前回記事の続き

koimedenshi.hatenablog.com

以下のように,6台のdindコンテナによるswarm nodeが構築されるところまでやった

ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
71jzsyefebwvmiz5va29cnhn4 *   manager1            Ready               Active              Leader              18.09.3
0e3bl408q8lufo5xczwny5l9m     manager2            Ready               Active              Reachable           18.09.3
icd4ixgyqvsc0ebrdf8w5esxy     manager3            Ready               Active              Reachable           18.09.3
woo1rgys2gsgpffjtyrec7gqe     worker1             Ready               Active                                  18.09.3
kv6dnud4pyfk0269xlqauwngd     worker2             Ready               Active                                  18.09.3
i9ygvq1nvcqqpdyatpygl39ax     worker3             Ready               Active                                  18.09.3

以下,docker swarm tutorialの後半部分をこのdind版swarmでもやってみる.

手順

manager1ノードでwebというserviceを立ち上げる.
(dockerにおけるserviceという概念に関してはこのへんとかに書いてある)

docker exec -it manager1 docker service create -p 80:80 --name web nginx:latest

ブラウザからmanager1ホストのIPへアクセスすると,立ち上がったサービスを確認できる manager1のIPはhostnameコマンドなどで確認できる

$ hostname -i
172.22.0.2

f:id:koimedenshi:20190402095746p:plain

なお,manager1以外のホストのIPへアクセスしても同様に表示される.
これはswarmのRouting Meshという機能らしい

チュートリアルにはない手順だが,docker service psコマンドでサービスの状態を見てみる

$ docker exec -it manager1 docker service ps web
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
kw541zci4kni        web.1               nginx:latest        manager2            Running             Running 20 minutes ago                       
/

serviceの起動コマンドはmanager1で実行したが,実際のサービスはmanager2ノードで立ち上がっているっぽい.
80番ポートへのアクセスは,このノードへルーティングされているようだ.

次にサービスをスケーリングする.
webというサービスを合計15個になるよう複製する
この操作で端末の動作が重たくなるので注意.(メモリ1GBのVMでも動いたけど,だいぶもっさりした)

docker exec -it manager1 docker service scale web=15

docker service lsコマンドでレプリカが作成されたことを確認

$ docker exec -it manager1 docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
4x6ycl1ftacd        web                 replicated          15/15               nginx:latest        *:80->80/tcp

docker service psコマンドでwebサービスを確認すると,15個のレプリカが6台のノードに割り振られていることがわかる

$ docker exec -it manager1 docker service ps web
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
kw541zci4kni        web.1               nginx:latest        manager2            Running             Running 28 minutes ago                       
3l7bfnzp1y09        web.2               nginx:latest        manager2            Running             Running 4 minutes ago                        
s20rm41zo6uw        web.3               nginx:latest        manager1            Running             Running 2 minutes ago                        
a99tv7ha01et        web.4               nginx:latest        manager3            Running             Running 2 minutes ago                        
qb38lx8ov9ge        web.5               nginx:latest        worker3             Running             Running 2 minutes ago                        
vlihahkxs9pv        web.6               nginx:latest        worker1             Running             Running 2 minutes ago                        
7yzl5f31a9rb        web.7               nginx:latest        worker2             Running             Running 2 minutes ago                        
4we17vy5xtmg        web.8               nginx:latest        worker2             Running             Running 2 minutes ago                        
yy5kn8mv8hvz        web.9               nginx:latest        worker1             Running             Running 2 minutes ago                        
aej45h59wln3        web.10              nginx:latest        worker3             Running             Running 2 minutes ago                        
dygbxzxrbuwc        web.11              nginx:latest        manager1            Running             Running 2 minutes ago                        
zbhsfnn1p5ux        web.12              nginx:latest        manager1            Running             Running 2 minutes ago                        
zlud7r6h9y1e        web.13              nginx:latest        manager3            Running             Running 2 minutes ago                        
q2xz28vesyy9        web.14              nginx:latest        manager2            Running             Running 4 minutes ago                        
vf5f395d6ydp        web.15              nginx:latest        worker3             Running             Running 2 minutes ago      

docker node update --availability drainコマンドで,指定したノードからサービスを放出できる
以下のコマンドにより,worker1ノードのサービスをdrainする.

docker exec -it manager1 docker node update --availability drain worker1

ふたたびサービスの状態を確認

$ docker exec -it manager1 docker service ps web
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
kw541zci4kni        web.1               nginx:latest        manager2            Running             Running 32 minutes ago                       
3l7bfnzp1y09        web.2               nginx:latest        manager2            Running             Running 8 minutes ago                        
s20rm41zo6uw        web.3               nginx:latest        manager1            Running             Running 6 minutes ago                        
a99tv7ha01et        web.4               nginx:latest        manager3            Running             Running 6 minutes ago                        
qb38lx8ov9ge        web.5               nginx:latest        worker3             Running             Running 6 minutes ago                        
199q9r5nicnz        web.6               nginx:latest        manager3            Running             Starting 4 seconds ago                       
vlihahkxs9pv         \_ web.6           nginx:latest        worker1             Shutdown            Shutdown 4 seconds ago                       
7yzl5f31a9rb        web.7               nginx:latest        worker2             Running             Running 6 minutes ago                        
4we17vy5xtmg        web.8               nginx:latest        worker2             Running             Running 6 minutes ago                        
vnqvozak8s7m        web.9               nginx:latest        worker2             Running             Starting 3 seconds ago                       
yy5kn8mv8hvz         \_ web.9           nginx:latest        worker1             Shutdown            Shutdown 4 seconds ago                       
aej45h59wln3        web.10              nginx:latest        worker3             Running             Running 6 minutes ago                        
dygbxzxrbuwc        web.11              nginx:latest        manager1            Running             Running 6 minutes ago                        
zbhsfnn1p5ux        web.12              nginx:latest        manager1            Running             Running 6 minutes ago                        
zlud7r6h9y1e        web.13              nginx:latest        manager3            Running             Running 6 minutes ago                        
q2xz28vesyy9        web.14              nginx:latest        manager2            Running             Running 8 minutes ago                        
vf5f395d6ydp        web.15              nginx:latest        worker3             Running             Running 6 minutes ago                        

worker1にいたweb.6とweb.9がシャットダウンされ,代わりにmanager3およびworker2ノードで実行されている

worker1の状態は,docker node lsコマンドでも確認できる

$ docker exec -it manager1 docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
71jzsyefebwvmiz5va29cnhn4 *   manager1            Ready               Active              Leader              18.09.3
0e3bl408q8lufo5xczwny5l9m     manager2            Ready               Active              Reachable           18.09.3
icd4ixgyqvsc0ebrdf8w5esxy     manager3            Ready               Active              Reachable           18.09.3
woo1rgys2gsgpffjtyrec7gqe     worker1             Ready               Drain                                   18.09.3
kv6dnud4pyfk0269xlqauwngd     worker2             Ready               Active                                  18.09.3
i9ygvq1nvcqqpdyatpygl39ax     worker3             Ready               Active                                  18.09.3

AVAILABILITYがDrainとなっている

次にサービスをスケールダウンする.
以下のコマンドでレプリカの数を15から10まで減らす

docker service scale web=10

サービスの状態確認結果がこちら

$ docker exec -it manager1 docker service ps web
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
kw541zci4kni        web.1               nginx:latest        manager2            Running             Running 41 minutes ago                       
3l7bfnzp1y09        web.2               nginx:latest        manager2            Running             Running 17 minutes ago                       
s20rm41zo6uw        web.3               nginx:latest        manager1            Running             Running 15 minutes ago                       
a99tv7ha01et        web.4               nginx:latest        manager3            Running             Running 15 minutes ago                       
qb38lx8ov9ge        web.5               nginx:latest        worker3             Running             Running 15 minutes ago                       
199q9r5nicnz        web.6               nginx:latest        manager3            Running             Running 8 minutes ago                        
vlihahkxs9pv         \_ web.6           nginx:latest        worker1             Shutdown            Shutdown 8 minutes ago                       
7yzl5f31a9rb        web.7               nginx:latest        worker2             Running             Running 15 minutes ago                       
4we17vy5xtmg        web.8               nginx:latest        worker2             Running             Running 15 minutes ago                       
yy5kn8mv8hvz        web.9               nginx:latest        worker1             Shutdown            Shutdown 8 minutes ago                       
aej45h59wln3        web.10              nginx:latest        worker3             Running             Running 15 minutes ago                       
dygbxzxrbuwc        web.11              nginx:latest        manager1            Running             Running 15 minutes ago    

少しわかりにくいが,worker1のweb.9がシャットダウンされているので,確かにレプリカ数は10となっている

次にさきほど放出したworker1を再びアクティブに戻す

docker exec -it manager1 docker node update --availability active worker1

ノードの状態を確認する

$ docker exec -it manager1 docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
71jzsyefebwvmiz5va29cnhn4 *   manager1            Ready               Active              Leader              18.09.3
0e3bl408q8lufo5xczwny5l9m     manager2            Ready               Active              Reachable           18.09.3
icd4ixgyqvsc0ebrdf8w5esxy     manager3            Ready               Active              Reachable           18.09.3
woo1rgys2gsgpffjtyrec7gqe     worker1             Ready               Active                                  18.09.3
kv6dnud4pyfk0269xlqauwngd     worker2             Ready               Active                                  18.09.3
i9ygvq1nvcqqpdyatpygl39ax     worker3             Ready               Active                                  18.09.3

worker1の状態がDrain -> Activeと変化した

サービスの状態を確認する

$ docker exec -it manager1 docker service ps web
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE               ERROR               PORTS
kw541zci4kni        web.1               nginx:latest        manager2            Running             Running about an hour ago                       
3l7bfnzp1y09        web.2               nginx:latest        manager2            Running             Running 25 minutes ago                          
s20rm41zo6uw        web.3               nginx:latest        manager1            Running             Running 23 minutes ago                          
a99tv7ha01et        web.4               nginx:latest        manager3            Running             Running 23 minutes ago                          
qb38lx8ov9ge        web.5               nginx:latest        worker3             Running             Running 23 minutes ago                          
199q9r5nicnz        web.6               nginx:latest        manager3            Running             Running 17 minutes ago                          
vlihahkxs9pv         \_ web.6           nginx:latest        worker1             Shutdown            Shutdown 17 minutes ago                         
7yzl5f31a9rb        web.7               nginx:latest        worker2             Running             Running 23 minutes ago                          
4we17vy5xtmg        web.8               nginx:latest        worker2             Running             Running 23 minutes ago                          
yy5kn8mv8hvz        web.9               nginx:latest        worker1             Shutdown            Shutdown 17 minutes ago                         
aej45h59wln3        web.10              nginx:latest        worker3             Running             Running 23 minutes ago                          
dygbxzxrbuwc        web.11              nginx:latest        manager1            Running             Running 23 minutes ago   

worker1ノードをクラスタに復帰させたが,中のサービスは停止しており,レプリカ数に変化はない

次にmanager1ノードをクラスタから退出させる.

docker exec -it manager1 docker swarm leave --force

クラスタからリーダーを退出させた.これによりswarmは,次のリーダーを自動的に立ててくれる
manager2でdocker node lsコマンドを実行し,ノードの状態を確認する

$ docker exec -it manager2 docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
71jzsyefebwvmiz5va29cnhn4     manager1            Down                Active              Unreachable         18.09.3
0e3bl408q8lufo5xczwny5l9m *   manager2            Ready               Active              Leader              18.09.3
icd4ixgyqvsc0ebrdf8w5esxy     manager3            Ready               Active              Reachable           18.09.3
woo1rgys2gsgpffjtyrec7gqe     worker1             Ready               Active                                  18.09.3
kv6dnud4pyfk0269xlqauwngd     worker2             Ready               Active                                  18.09.3
i9ygvq1nvcqqpdyatpygl39ax     worker3             Ready               Active                                  18.09.3

manager1の状態がDownとなっており,MANAGER STATUSもUnreachableとなっている.
manager2が代わりのリーダーとなっている.

最後にサービスを削除する
サービスの削除は以下のコマンドで行う

docker exec -it manager2 docker service rm web

各ノードに分散していたサービスを一括削除できる.

このチュートリアルはこれで終了となる.
不要となったコンテンツを削除する場合は以下のコマンドを実行する

docker stop manager1 manager2 manager3 worker1 worker2 worker3
docker rm manager1 manager2 manager3 worker1 worker2 worker3
docker network rm dind-net

おわりに

docker swarm tutorialを試してみた.
この分野に全く詳しくないので,便利だなーくらいの感想しかない.
きっとk8sはもっと便利なのだろう.
なお今回のチュートリアルではdindコンテナを使ってみた.
ほとんど同じような挙動をしているので,雰囲気をつかむくらいであればdindでもできそうではある.
VMを立てるのと比べてリソースを減らせたかというとよくわからない.スケールした時に動作がもっさりしたりする.
試してはいないが,低スペックなPCやサーバの場合,起動時間は多少減らせるかもしれない.

swarm tutorialをdindでやってみる

お題

dockerのオーケストレーションツールのひとつにswarmというのがある

docs.docker.com

複数のdockerホストをクラスタリングしたり,ロードバランシングしたりできるらしい.
これ系は今はKubernetesが優勢だとは思うが,デフォルトでdocker engineに組み込まれているということもあり,せっかくなので少し使ってみようと思った

やること

素人なので,いつも通りチュートリアルをやる.
docker swarm tutorialは公式のものだと以下が見つかった

github.com

注意書きにある通り,ここにはplay-with-dockerでできるチュートリアルをローカルでやる手順が書かれている.
具体的にはdocker-machine createというコマンドで,dockerがインストールされたVMを複数台立て,swarmでクラスタリングし,webというサービスをデプロイしている.
MacWindows(linuxでもいいけど)のふつうのPCにvirtualboxをインストールして,ここに書かれている通りやればだいたいうまくいくと思われる.

自分はなんとなくホストPCのMacに手を入れたくなかったので,前回使ってみたdind(docker in docker)を使って似たようなことができないか試してみた
結果,docker-machineを使ったチュートリアルとだいたい同じような挙動を追うことができた
完成形を図にするとたぶんこんなかんじ

f:id:koimedenshi:20190401220643p:plain

以下,上記の公式チュートリアルbash scriptとだいたい同じことをdindでやった場合の手順を書いていく. (今回の記事ではクラスタを立てるところまでで,次回の記事でこのクラスタにサービスをデプロイしたり,nodeを退出させたりするところをやる予定.)

注意

  • あくまでチュートリアルなので,セキュリティ設定等は考慮していない
  • dindはdockerと異なる仕組みで動いているため,環境によってはこの通り動かない

環境

Docker 18.09.3 on Ubuntu 18.04 (virtualbox on macOS)
ubuntuにdockerをインストールする手順はこちら:
koimedenshi.hatenablog.com

手順

元のbash scriptを参考に作成したbash scriptはこちら.
細かい点を除けば,docker-machineがやっていることをdocker container rundocker exec -itで置き換えているだけ
このスクリプトをローカルに保存して実行すれば,上図のようなdindクラスタが構築される

#!/bin/bash

## output debug_output
# exec 5> debug_output.txt
# BASH_XTRACEFD="5"
# PS4='$LINENO: '
# set -x

# create user defined bridge network
echo "======> Creating $nw network ...";
nw=dind-net
docker network create -d bridge $nw

# Swarm mode using Docker in Docker

managers=3
workers=3

# create manager machines
echo "======> Creating $managers manager machines ...";
for node in $(seq 1 $managers);
do
    echo "======> Creating manager$node machine ...";
    docker run -it --privileged --network $nw --name manager$node --hostname manager$node -d docker:stable-dind;
done

# create worker machines
echo "======> Creating $workers worker machines ...";
for node in $(seq 1 $workers);
do
    echo "======> Creating worker$node machine ...";
    docker run -it --privileged --network $nw --name worker$node --hostname worker$node -d docker:stable-dind;
done

# list all machines
docker container ls

# initialize swarm mode and create a manager
echo "======> Initializing first swarm manager ..."
docker exec -it manager1 docker swarm init --listen-addr $(docker exec -it manager1 hostname -i | tr -d '\r') --advertise-addr $(docker exec -it manager1 hostname -i | tr -d '\r')

# get manager and worker tokens
manager_token=$(docker exec -it manager1 docker swarm join-token manager -q | tr -d '\r')

worker_token=$(docker exec -it manager1 docker swarm join-token worker -q | tr -d '\r')

echo "manager_token: $manager_token"
echo "worker_token: $worker_token"

# other masters join swarm
for node in $(seq 2 $managers);
do
    echo "======> manager$node joining swarm as manager ...";
    docker exec -it manager$node \
        docker swarm join \
        --token $manager_token \
        --listen-addr $(docker exec -it manager$node hostname -i | tr -d '\r') \
        --advertise-addr $(docker exec -it manager$node hostname -i | tr -d '\r') \
        $(docker exec -it manager1 hostname -i | tr -d '\r');
done

# show members of swarm
docker exec -it manager1 docker node ls

# workers join swarm
for node in $(seq 1 $workers);
do
    echo "======> worker$node joining swarm as worker ..."
    docker exec -it worker$node \
    docker swarm join \
    --token $worker_token \
    --listen-addr $(docker exec -it worker$node hostname -i | tr -d '\r') \
    --advertise-addr $(docker exec -it worker$node hostname -i | tr -d '\r') \
    $(docker exec -it manager1 hostname -i | tr -d '\r');
done

# show members of swarm
docker exec -it manager1 docker node ls

順を追って説明する.

まずユーザ定義ネットワークを作成する.
ネットワークタイプはbridgeを選択

# create user defined bridge network
echo "======> Creating $nw network ...";
nw=dind-net
docker network create -d bridge $nw

swarmに参加させるノードコンテナを起動.
swarmでは必ずmanagerかworkerかのいずれかのロールを割り当てる.

docs.docker.com

それぞれの役割は読んで字のごとくという感じ?(雑)

manager node, worker nodeをそれぞれ3台ずつ,dindコンテナで起動する

# Swarm mode using Docker in Docker

managers=3
workers=3

# create manager machines
echo "======> Creating $managers manager machines ...";
for node in $(seq 1 $managers);
do
    echo "======> Creating manager$node machine ...";
    docker run -it --privileged --network $nw --name manager$node --hostname manager$node -d docker:stable-dind;
done

# create worker machines
echo "======> Creating $workers worker machines ...";
for node in $(seq 1 $workers);
do
    echo "======> Creating worker$node machine ...";
    docker run -it --privileged --network $nw --name worker$node --hostname worker$node -d docker:stable-dind;
done

# list all machines
docker container ls
  • --privilegedをつけるのは元のチュートリアルに倣っている.これをつけないと自分以外のコンテナの設定を変えることができない.
  • --name と --hostname で二回名前を指定しており冗長な感じもするが,--name がコンテナ名,--hostname がホスト名.前者はコンテナ同士の名前解決に用いる名前で,後者はこの後に出てくるnode lsなどの際に表示するホスト名となる.
  • 使用するコンテナイメージはdocker:stable-dind.(dindの中のdockerバージョンは"DockerVersion": "18.06.1-ce")

この時点では6台のdockerホストを立てただけ.ここからswarmクラスタを構築していく.
manager1を最初のswarmの参加者に指定する(リーダーになる)

# initialize swarm mode and create a manager
echo "======> Initializing first swarm manager ..."
docker exec -it manager1 docker swarm init --listen-addr $(docker exec -it manager1 hostname -i | tr -d '\r') --advertise-addr $(docker exec -it manager1 hostname -i | tr -d '\r')

--listen-addrおよび--advertise-addrに自身のホストIPを指定する.

manager1がswarmクラスタのmanager nodeになったので,クラスタ参加に必要なトークンを払い出す.

# get manager and worker tokens
manager_token=$(docker exec -it manager1 docker swarm join-token manager -q | tr -d '\r')

worker_token=$(docker exec -it manager1 docker swarm join-token worker -q | tr -d '\r')

echo "manager_token: $manager_token"
echo "worker_token: $worker_token"

見ての通り,ロールによってトークンが異なる.

払い出したトークンで,他のnodeをswarmクラスタに参加させる.

# other masters join swarm
for node in $(seq 2 $managers);
do
    echo "======> manager$node joining swarm as manager ...";
    docker exec -it manager$node \
        docker swarm join \
        --token $manager_token \
        --listen-addr $(docker exec -it manager$node hostname -i | tr -d '\r') \
        --advertise-addr $(docker exec -it manager$node hostname -i | tr -d '\r') \
        $(docker exec -it manager1 hostname -i | tr -d '\r');
done

# show members of swarm
docker exec -it manager1 docker node ls

# workers join swarm
for node in $(seq 1 $workers);
do
    echo "======> worker$node joining swarm as worker ..."
    docker exec -it worker$node \
    docker swarm join \
    --token $worker_token \
    --listen-addr $(docker exec -it worker$node hostname -i | tr -d '\r') \
    --advertise-addr $(docker exec -it worker$node hostname -i | tr -d '\r') \
    $(docker exec -it manager1 hostname -i | tr -d '\r');
done

# show members of swarm
docker exec -it manager1 docker node ls

正常に終了していれば,6台のノードがmanager/workerいずれかのロールでswarmクラスタに参加している状態となる.

ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
vjgd1myk9r10msdhi1x835kou *   manager1            Ready               Active              Leader              18.09.3
10ddklb0bysyhpadxjr6lvyar     manager2            Ready               Active              Reachable           18.09.3
q0cxi5rkcayiuac2kopjsg1j1     manager3            Ready               Active              Reachable           18.09.3
i78pa70deqx1aspefqvmjs4bj     worker1             Ready               Active                                  18.09.3
r2zv3coxlzvn6oh9xul8bf8ss     worker2             Ready               Active                                  18.09.3
quzmoin1qesj7gnqh9ffvq0st     worker3             Ready               Active                                  18.09.3

以上,dindでswarmクラスタを立てるところまでを実施した.
次回はこのクラスタにサービスをデプロイしたり,nodeを退出させたりするところをやる.
完結編はこちら↓

koimedenshi.hatenablog.com

うまくいかないとき

環境によりけりなので,エラーを潰していくしかない.
先述の通りdindは完全ではないらしいので,最初から無理という可能性もある.
その場合は元のチュートリアルをやるか,ふつうにplay-with-dockerをやるしかないかも

後片付け

用済みであれば,docker stop -> docker rm / network rmすればok

docker stop manager1 manager2 manager3 worker1 worker2 worker3
docker rm manager1 manager2 manager3 worker1 worker2 worker3
docker network rm dind-net

P.S.

正直docker-machineの方が便利かも.

docker in docker(dind)を使ってみる

お題

Dockerにはdind = docker-in-dockerというものがあって,dockerホストの中にdockerホストを立てることができる.
https://hub.docker.com/_/docker

今回は上記URLのHow to use this imageをベースに,dindを触ってみる.

なぜ

コンテナを用いたサービスを冗長化したい場合,複数のdockerホストを立てクラスタリングするというのが定石らしい.
これを手元でやってみようとなった時,ホストの数だけ仮想マシンを立てる方法もあるが,
そのようなリソースがない場合,dindを使うと仮想マシン1台でもお手軽に体験できる.
(より正直に言うと,swarmやkubernetesなどのツールをお手軽に使ってみたいという願望がある)

注意

上記のDockerHubページに,使う前にここを読んでねと以下のリンクが置いてある.
jpetazzo.github.io

debian系以外ではダメかも・・という話と,内側のdockerを外側のdockerと同じ感覚で使おうとすると色々問題がおきる模様.
便利だけど色々問題もあるから,検証目的や継続利用の場合は慎重に(というか他の方法を考えた方がいいよ)という感じ.
今回はお試しなのでまあOKということで.

環境

Docker (18.09.3) on Ubuntu 18.04 (Oracle VirtualBox VM on macOS)
ubuntuにdockerをインストールする手順はこちら:
koimedenshi.hatenablog.com

手順

DockerHubを参考にする.
https://hub.docker.com/_/docker

ホスト端末のターミナル上で以下のコマンドを実行し,dindコンテナを起動する.

docker run -it --privileged --name some-docker -d docker:stable-dind

--privilegedオプションで特権を与える.これがないと正しく動作しない.ホストマシンの操作権を与えることになるので注意.
少しややこしいが,イメージ名が"docker"でタグが"stable-dind" .

docker execコマンドで,作成したdindコンテナのshellに接続

docker exec -it some-docker sh

OSの情報を確認すると,Alpine Linuxとなっている

/ # cat /etc/*release
3.9.2
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.9.2
PRETTY_NAME="Alpine Linux v3.9"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"

docker versionコマンドを実行すると,dind内のdockerホストのバージョン情報が確認できる.

/ # docker version
Client: Docker Engine - Community
 Version:           18.09.3
 API version:       1.39
 Go version:        go1.10.8
 Git commit:        774a1f4
 Built:             Thu Feb 28 06:32:01 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.3
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.8
  Git commit:       774a1f4
  Built:            Thu Feb 28 06:40:51 2019
  OS/Arch:          linux/amd64
  Experimental:     false

この時のstable-dindのバージョンは18.09.3となっており,実際にインストールされているものと一致している.

dindコンテナ内におけるコンテナ起動

dind内でhello-worldコンテナを起動してみる.
docker execコマンドでsome-dockerコンテナのshellに接続:

docker exec -it some-docker sh

dind内でhello-worldコンテナを起動:

/ # docker container 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.
 〜省略〜

dind内であっても,通常のdockerホストでの操作と同じ感覚でコンテナイメージを取得して起動できた.
この時イメージはどこから取得しているのかというと,ホストがローカルに保存しているイメージをダウンロードするのではなく,ふつうに外部のdockerレジストリ(https://index.docker.io/v1/)から取得していると思われる.
dind内のdocker設定を見るとレジストリとしてdocker公式レジストリが設定されている.

/# docker info
 〜省略〜
Registry: https://index.docker.io/v1/
 〜省略〜

storage-driverの設定

--storage-driverはvfsを選択するようになっている.「vfsはスペックとしては微妙だが,唯一安定して動作するため,しかたなくこれにしている.
実用的にはホスト端末での--storage-driverと同じ値を設定するのが良い」的なことが書かれている.
ホスト端末での--storage-driverはdocker infoで確認できる.

~ $ docker info
Containers: 11
 Running: 1
 Paused: 0
 Stopped: 10
Images: 21
Server Version: 18.09.3
Storage Driver: overlay2 #ここ
 〜以下略〜

本環境ではoverlay2となっているので,それに合わせたdindコンテナを立てる場合は以下のコマンドとなる.

docker run --privileged --name some-overlay-docker -d docker:stable-dind --storage-driver=overlay2

別コンテナからdindへの接続

dind自体はホスト端末(ここではubuntu)上のdockerから見ればコンテナのひとつなので,ホストのdockerに立てた別のコンテナから--linkオプションで接続できる.
(例)新たにedge-dindコンテナを立て,先ほど立てた"some-docker"コンテナに接続する場合:

docker run --privileged --link some-docker:docker --name some-docker-edge-dind -d docker:edge-dind

データをどこに保存するか

様々な方法があるが,dindコンテナ内にするか,ホストのディレクトリをマウントして保存先とするかが挙げられている.
ホストディレクトリ"/my/own/var-lib-docker"をマウントする場合のコンテナ起動方法は:

docker run --privileged --name some-docker -v /my/own/var-lib-docker:/var/lib/docker -d docker:dind

この辺りも使い方次第でベストプラクティス的なものは変わるかもしれない.

まとめ

dindによって複数のdockerホストを単一の仮想マシン上に構築することができた.
これを使えばswarmによるクラスタ管理などをお手軽に体験できそう.

Dockerの仮想ネットワーク内でOSSのIDS(suricata)を試す

お題

ふとsuricataというOSSのIDSソフトを試してみたくなり,Dockerの仮想ネットワークの範囲で出来そうだったので,簡単なサーバ+クライアント構成を構築してみた.
suricataはコンフィグ次第でIPSモードなど色々な機能を実現できるが,今回はHTTPサーバの前段に設置して攻撃アクセスを検知する最低限の機能を再現する.
ネットで探すとsuricataのコンテナイメージやDockerfileも出てくるが,今回はインストールするところからやりたいというのもあり,あえてそれらは使用せず,公式Ubuntuイメージにインストールすることにした.

suricataとは

suricataはLinux/Mac/FreeBSD/UNIX/Windowsなど様々なプラットフォームにインストールでき,セキュリティの分野ではsnortと並んで有名なIDS/IPSソフトである(多分).

suricata-ids.org

注意

  • 後半に攻撃コマンドが含まれるが,自分の所轄外の外部ネットワークなどに対し実行してはいけない.
  • 今回の構成でセキュリティを担保できるわけではないので,この設定だけで公開サーバを立てたりしてはいけない.あくまで自己責任で.

環境

Docker 18.09.1 on Ubuntu 18.04 (Oracle VirtualBox VM on macOS)
UbuntuでのDockerインストール方法はこちら↓
koimedenshi.hatenablog.com

構成

以下のコンテナおよびネットワークを構築し,簡単なサーバ(IDS有)+クライアント構成を組み立てる.
* suricataがインストールされた被害者役サーバコンテナ
* 攻撃役端末コンテナ
* 上記のコンテナを接続する,仮想ブリッジネットワーク

手順

仮想ネットワーク作成

まず,コンテナを接続するためのユーザ定義ブリッジネットワークisolated_nwを作成する.
デフォルトのブリッジネットワークに接続してもよいが,なんとなく分けた方が管理しやすいかと思ったので.

docker network create --driver bridge isolated_nw

攻撃用端末の構築

次に攻撃役端末コンテナを構築する.
ベースイメージはcurlなどのコマンドを使えればなんでもよいが,ここではdebianstable-slimイメージを引用する.
-ditオプションでインタラクティブなシェルを立ち上げる. また--networkオプションで作成したisolated_nwに接続する.

docker container run -dit --name debian-slim --network isolated_nw debian:stable-slim

作成したコンテナのbashに接続し,疎通確認やHTTPアクセスで使用するコマンドをインストールする

docker exec -it debian-slim bash
#/ apt-get update
#/ apt-get install curl wget iproute2 iputils-ping

ipコマンドをインストールしたので,サーバのifの名前とipアドレスを確認する.(後で使う)

/# ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0
       valid_lft forever preferred_lft forever

この例では,eth0に172.18.0.3というIPが割り当てられている.

被害者端末HTTPサーバ(NGINX+suricata)の構築

 続いて被害者役のサーバコンテナを構築する.
-dit--net=オプション指定は攻撃者役コンテナと同様だが,新たに--cap-add=NET_ADMINというオプションを指定する.
--cap-add=NET_ADMINオプションはコンテナにネットワーク周りのroot権限を付与するオプションで(雑な説明かも),これを入れないと後ほど出てくるsuricataの起動時に権限がないと怒られる.
部分的な権限付与とはいえコンテナにホスト端末の設定変更を許可することになるので,不特定多数が触るような環境では注意する.
ベースイメージはubuntu:18.04を指定する.これもdebianなどでもいけるかもしれないが,suricataの対応バージョンが異なってくるかもしれない.
ubuntu:18.04はsuricataの最新バージョン4.xに対応していた.

docker container run -dit --name ubuntu-suricata --cap-add=NET_ADMIN --net=isolated_nw ubuntu:18.04

コンテナが起動したらbashに接続し,必要なパッケージをインストールする.
software-properties-commonについてはadd-apt-repositoryというaptレポジトリ追加のためのコマンドを後で実行するので追加している.
サイズが大きいし,もう少しスマートな方法があるかもしれない.
suricataで保護するHTTPサーバとして(これも立ち上がればなんでも良いが)NGINXをインストールしておく.

docker exec -it ubuntu-suricata bash # コンテナのbashに接続
/# apt-get update
/# apt-get install -y iproute2 iputils-ping software-properties-common vim curl  tail jq nginx

ipコマンドをインストールしたので,サーバのifの名前とipアドレスを確認する.(後で使う)

/# ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
       valid_lft forever preferred_lft forever

この例では,eth0に172.18.0.2というIPが割り当てられている.

その後,suricataをバイナリパッケージからインストールする.
suricataに開発オプションを指定する場合はソースからインストールする必要があるが,今回はとりあえず動けばいいのでバイナリパッケージを使う.
add-apt-repositoryコマンドでsuricataの安定板のppaを追加し,apt-getでインストールする.

/# add-apt-repository ppa:oisf/suricata-stable 
(確認メッセージが出るのでENTERを押す)
/# apt-get update
/# apt-get install -y suricata

以下のエラーが出るが,suricataが自動的に起動されなかったため?
とりあえず無視してもたぶん動く.

invoke-rc.d: could not determine current runlevel
invoke-rc.d: policy-rc.d denied execution of start.

 インストールが完了したら,suricataの初期設定を行う. まずsuricataのルール(検知パターン.シグネチャ的なもの)をダウンロードする.
これも方法は色々あるが,公式ドキュメントにはsuricata-updateコマンドを使用する手順が記載されていたので,そちらを踏襲する.
suricata-updateはその名の通りsuricataルールをアップデートするコマンドで,suricataのインストールと同時にインストールされる(というのはバージョン4.1以降の話で,それ以前のバージョンでは手動でインストールする必要がある).

/# suricata-update

これで/var/lib/suricata/rules/にルールが保存されるはず.

次にsuricataの初期コンフィグを行う.
suricataの設定変更は/etc/suricata/suricata.yamlを編集する.
色々と細かい設定ができるが,ここでは何かしらの攻撃アクセスを検知できれば良いので,最低限の設定として検査対象のIPアドレスを指定する.
検査対象のIPアドレスはsuricata.yamlHOME_NETおよびEXTERNAL_NETにより制限できる.
suricataはHOME_NETに指定されたIPを保護するネットワークとして認識し,EXTERNAL_NETを外部ネットワークとして認識する.
一般的にはHOME_NETに保護するサーバの属するネットワーク(プライベートIP)を指定し,EXTERNAL_NETにはそれ以外(!$HOME_NET)を設定するものと思われるが,今回は攻撃者が被害者と同じネットワークに属しているため,サーバのIPと攻撃者端末のIPを単体で指定する.

まずHOME_NETubuntu-suricataのIPアドレス(ここでは172.18.0.3)を指定する.
suricata.yamlの以下の行をコメントアウトする.

    HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]"

そして以下のようにIPを指定する.

    #HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]"
    HOME_NET: "[172.18.0.3/32]"

次にEXTERNAL_NETdebian-slimのIPアドレス(ここでは172.18.0.2)を指定する.
以下の行をコメントアウトして

    EXTERNAL_NET: "!$HOME_NET"

以下のように新たに指定して保存する.

    #EXTERNAL_NET: "!$HOME_NET"
    EXTERNAL_NET: "[172.18.0.2/32]"

設定を変更したら,以下のコマンドでコンフィグ確認を行う.

#/ suricata -T

エラーが出たら適宜修正する.
先に行ったルールのダウンロードがされていない場合,ここでエラーが出る.

終わったら,以下のコマンドでsuricataを実行する.
-cオプションでコンフィグファイル(先ほど編集したsuricata.yaml)を指定し,-iオプションで検査するif(ここではeth0)を指定する.

#/ suricata -c /etc/suricata/suricata.yaml -i eth0 --init-errors-fatal
X/X/2019 -- 13:27:14 - <Notice> - This is Suricata version 4.1.2 RELEASE
X/X/2019 -- 13:27:31 - <Notice> - all 1 packet processing threads, 4 management threads initialized, engine started. #suricataが正常に起動した

起動したら,HTTPサーバを立ち上げる. ホストマシンで別のターミナルを開き,コンテナに接続してNGINXを起動する.

docker exec -it ubuntu-suricata bash
#/ service nginx status
#/ service nginx start
#/ curl http://localhost/
(NGINXのデフォルトページが返ってくることを確認)

HTTPサーバを立ち上げたら,suricataログの表示準備する. ホストマシンで別のターミナルを開き,コンテナに接続して以下のコマンドを実行する.
suricataログはアラートも含め/var/log/suricata/eve.jsonに書き込まれる.
デフォルトの設定だとアップデート通信などアラート以外の通信も書き込まれて見づらいので,jqで表示をフィルタする.

docker exec -it ubuntu-suricata bash
#/  tail -f /var/log/suricata/eve.json | jq -r -c 'select(.event_type=="alert")'
(event_type=="alert"のみが表示・更新される)

suricata検知の検証

監視準備ができたので,いよいよ攻撃コードを送ってみる.
攻撃者であるdebian-slimコンテナに接続し,curlで攻撃コードを実行する.
攻撃コードはsuricataルールに含まれていればなんでも良いが,ここではWindows Serverなどに対するリモートコマンド実行を想定したリクエストを送ってみる(間違って外部に実行しないこと)

docker exec -it debian-slim bash
#/ curl http://172.18.0.3/cmd.exe
(cmd.exeは存在しないので404エラーが返ってくる)

検知していれば,eve.jsonに次のようなログが追加される.
送信元IPや宛先IP,シグネチャ名,url, User-Agentなどが記載されている.
検知したシグネチャ名は"ET WEB_SERVER cmd.exe In URI - Possible Command Execution Attempt"となっている.

{"timestamp":"2019-X-XT13:40:15.681575+0000","flow_id":881816844067101,"in_iface":"eth0","event_type":"alert","src_ip":"172.18.0.2","src_port":51516,"dest_ip":"172.18.0.3","dest_port":80,"proto":"006","tx_id":0,"alert":{"action":"allowed","gid":1,"signature_id":2009361,"rev":5,"signature":"ET WEB_SERVER cmd.exe In URI - Possible Command Execution Attempt","category":"Attempted Information Leak","severity":2,"metadata":{"updated_at":["2010_07_30"],"created_at":["2010_07_30"]}},"http":{"hostname":"172.18.0.3","url":"/cmd.exe","http_user_agent":"curl/7.52.1","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":404,"length":178},"app_proto":"http","flow":{"pkts_toserver":4,"pkts_toclient":3,"bytes_toserver":353,"bytes_toclient":548,"start":"2019-02-22T13:40:15.680221+0000"}}

少し見づらいので,たくさんのログを処理する場合は整形するなどしてもよいし,GUI(snorbyなど)と連携してもよいと思う.

おわりに

以上で最低限攻撃検知するところまで再現できた.
/etc/suricata/rules/に含まれているルールを読んでどんな攻撃を検知するかを試したり,自分なりのカスタムルールを追加してみたり色々遊ぶと勉強になると思う.
なおsuricataを攻撃遮断をするIPSモードで実行する場合はソースファイルをオプション付きでコンパイルする必要があるっぽいので,今回作った環境では実現できないかもしれない(できるかもしれないが,試していない)

参考

suricataのインストール方法などは公式ドキュメントに記載されている.
suricata.readthedocs.io

インストール完了後の基本設定は以下の公式ドキュメントを参考にした.
redmine.openinfosecfoundation.org

Docker network tutorial (host編)

Docker公式ドキュメントのhost network tutorialをやってみる.
docs.docker.com
[注意] host networkはDocker Linuxのみ対応で,MacWindows環境には未対応とのこと.

環境

Docker 18.09.1 on Ubuntu 18.04 (Oracle VirtualBox VM on macOS)
 インストール方法はこちら↓
koimedenshi.hatenablog.com

やること

Dockerホストのネットワーク上でport:80で待ち受けるNginxサービスを立てる.
ネットワークの観点においてはDockerホストから隔離されていないが,ストレージや名前空間など,ネットワーク以外のリソースはホストから独立して存在することになる.

手順

  • 以下の--rmオプション付きコマンドを実行し,nginxコンテナを起動する
$ docker run --rm -d --network host --name my_nginx nginx

--rmオプションで立ち上げたコンテナはexit/stopすると同時に削除される.
-dオプションでプロセスをバックグラウンドで実行状態にする.

  • ホストマシン上で以下のコマンドを実行し,インターフェースが追加されていないことを確認
$ ip addr show
(既存のIFのみ.
 ブリッジネットワークだと,ここでインターフェースが追加される)
  • 以下のコマンドを実行して,ポート80番がどのプロセスに紐づいているかを確認
$ sudo netstat -tulpn | grep :80
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      26149/nginx: master 
  • チュートリアルにはない手順)network inspectコマンドでhostネットワークの設定を確認
$ docker network inspect host
[
    {
        "Name": "host",
        "Id": "6dc567989d6f78e9586117f18de3e4318cf3dcf80fe4dcdb06cb64b48cd546b2",
        "Created": "2018-12-01T15:49:43.284522247+09:00",
        "Scope": "local",
        "Driver": "host",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": []
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "7362beaa0c7725455893330ddcf3f690db53628fc06bf9cd98343cb4f5cbbb55": {
                "Name": "my_nginx",
                "EndpointID": "125632f721623e64896c4f0bff4b6cf394f84f652b79c72a6196a26fd1a68e64",
                "MacAddress": "",
                "IPv4Address": "",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

bridgeネットワークの時と違って,コンテナのIPアドレスMACアドレスが設定されていない.
(ホストとネットワーク資源を共有するため,なくてもつながる)

  • 終わったら,以下のコマンドを実行してコンテナを削除
$ docker container stop my_nginx

まとめ

hostネットワークに接続されたNginxコンテナを起動した.
チュートリアルにあまり詳しいことが書かれておらず,使い方もよくわからなかった.知識不足.
Docker17.06以降のバージョンでは, swarmモードでもhostネットワークを利用できる.
その場合,例えば80番ポートで立ち上げることのできるサービスは1個のみなどといった制約が生じることになるそうな.

以上

Docker network tutorial (bridge編)

Docker公式ドキュメントのbridge network tutorialをやってみた.
docs.docker.com

環境

Docker 18.09.1 on Ubuntu 18.04 (Oracle VirtualBox VM on macOS)
 インストール方法はこちら↓
koimedenshi.hatenablog.com

  • default bridge network

デフォルトブリッジはその名の通りデフォルトで用意されているネットワーク.
ネットワークを指定せずにコンテナを立ち上げると, ここに接続される.
ここでは2台のalpineコンテナをデフォルトブリッジに接続する.

初期状態の確認

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
f0e09069add5        bridge              bridge              local
6dc567989d6f        host                host                local
dd596238709d        none                null                local

2台のalpineコンテナの立ち上げ

$ docker run -dit --name alpine1 alpine ash

$ docker run -dit --name alpine2 alpine ash

起動したコンテナの確認

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
4081fada5d19        alpine              "ash"               2 minutes ago       Up 2 minutes                            alpine2
f9926cee8b76        alpine              "ash"               2 minutes ago       Up 2 minutes                            alpine1

ブリッジネットワークの接続状況をinspectで確認

$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "f0e09069add5f694454f4b91aa92b249834c51f990436233d3d1302cd14f1bcc",
        "Created": "2019-01-22T09:55:52.056584987+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "4081fada5d19e08d10b7a903261c12f66d42d962b639851256525babc933c7f7": {
                "Name": "alpine2",
                "EndpointID": "f94f5c25a244f5f921e066f00950cc9926675d131c84cfdbfec86319e398ce78",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            },
            "f9926cee8b761f44da6f8646132c6e6d2a1b4ece6c7d46e49b8f6e98d577efee": {
                "Name": "alpine1",
                "EndpointID": "784c9240218a496ce26d6301354ec0051fbd5f9ef657ded18736e079bf89ed6f",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

alpine1コンテナ, alpine2コンテナはbridgeネットワークに接続され, デフォルトゲートウェイ172.17.0.1/16を介してDocker hostと通信する.

f:id:koimedenshi:20190124122754p:plain
default-bridge

実際にコンテナの中から確認する.
attachコマンドでalpine1に接続する.

$ docker attach alpine1

/ #

ipコマンドでインターフェースの確認

/ # ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
13: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

loopbackとeth0(172.17.0.2/16)が立ち上がっている.

外向けにpingを打ってみる

/ # ping -c 2 google.com
PING google.com (216.58.197.142): 56 data bytes
64 bytes from 216.58.197.142: seq=0 ttl=61 time=9.415 ms
64 bytes from 216.58.197.142: seq=1 ttl=61 time=10.116 ms

--- google.com ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 9.415/9.765/10.116 ms

alpine2のIPアドレスpingを打ってみる

/ # ping -c 2 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.117 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.099 ms

--- 172.17.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.099/0.108/0.117 ms

'alpine2'にpingを打つと失敗する

/ # ping -c 2 'alpine2'
ping: bad address 'alpine2'

デフォルトブリッジではコンテナ名指定で疎通しないことがわかる.
alpine1のシェルを抜けてから(CTRL+p + qの順に押す),使用したコンテナを削除する

$ docker container stop alpine1 alpine2
$ docker container rm alpine1 alpine2

デフォルトブリッジのチュートリアル終わり.

  • user-defined bridge networks

次にユーザ定義ネットワークのチュートリアルを行う.
まずalpine-netという名前のネットワークを作成

$ docker network create --driver bridge alpine-net
3fe4ed300746ac93ff0eb283f239d3482aeedc88a9aa0e2eb1354a6ca79527ac

ネットワークが追加されたことを確認

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
3fe4ed300746        alpine-net          bridge              local
b005254ef24e        bridge              bridge              local
6dc567989d6f        host                host                local
dd596238709d        none                null                local

inspectコマンドでalpine-netの接続状況を確認. この段階では何もつながっていない

$ docker network inspect alpine-net 
[
    {
        "Name": "alpine-net",
        "Id": "3fe4ed300746ac93ff0eb283f239d3482aeedc88a9aa0e2eb1354a6ca79527ac",
        "Created": "2019-01-24T11:04:19.021413436+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.19.0.0/16",
                    "Gateway": "172.19.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

ネットワークアドレスは172.19.0.0/16となっている.
自動生成されるアドレス帯は環境によって異なる.

以下のコマンドで4台のコンテナを起動する.

$ docker run -dit --name alpine1 --network alpine-net alpine ash
26673686e6ff88c401c6e5b9dd007d1519ac603f86d1251195392a846f398ac5
$ docker run -dit --name alpine2 --network alpine-net alpine ash
6d36c4792f2b5c16b4a381a930d51b7456ab216efcc7af4363f9204c3a27a16a
$ docker run -dit --name alpine3 alpine ash
7747f25eb4e7729b83741be135f9fdc13c84695519abd77544f1f01fbe400d17
$ docker run -dit --name alpine4 --network alpine-net alpine ash
f39f29ac4f23bd0bb41bf39e80961d542a23f3cb79f91b93bbc00bee690a2e1b
$ docker network connect bridge alpine4
$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
f39f29ac4f23        alpine              "ash"               31 seconds ago       Up 29 seconds                           alpine4
7747f25eb4e7        alpine              "ash"               49 seconds ago       Up 48 seconds                           alpine3
6d36c4792f2b        alpine              "ash"               About a minute ago   Up About a minute                       alpine2
26673686e6ff        alpine              "ash"               About a minute ago   Up About a minute                       alpine1

ざっくり言うと以下のようなネットワーク構成になっている

f:id:koimedenshi:20190124115430p:plain
user-defined

inspectコマンドでalpine-netを確認すると,コンテナが3台追加されている

$ docker network inspect alpine-net 
[
    {
        "Name": "alpine-net",
        "Id": "3fe4ed300746ac93ff0eb283f239d3482aeedc88a9aa0e2eb1354a6ca79527ac",
        "Created": "2019-01-24T11:04:19.021413436+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.19.0.0/16",
                    "Gateway": "172.19.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "26673686e6ff88c401c6e5b9dd007d1519ac603f86d1251195392a846f398ac5": {
                "Name": "alpine1",
                "EndpointID": "10d86d5b8ac9a132fa018c1ef2af71fa605ebcc48db24c1176628f36fc8cee08",
                "MacAddress": "02:42:ac:13:00:02",
                "IPv4Address": "172.19.0.2/16",
                "IPv6Address": ""
            },
            "6d36c4792f2b5c16b4a381a930d51b7456ab216efcc7af4363f9204c3a27a16a": {
                "Name": "alpine2",
                "EndpointID": "ceb49779dff5690117bc9ab18995bf443a8f77d2c956a2152179561676ca0a3c",
                "MacAddress": "02:42:ac:13:00:03",
                "IPv4Address": "172.19.0.3/16",
                "IPv6Address": ""
            },
            "f39f29ac4f23bd0bb41bf39e80961d542a23f3cb79f91b93bbc00bee690a2e1b": {
                "Name": "alpine4",
                "EndpointID": "49f46643172030e0de68dd093aca27a45d6c8ddf606cdc8a7c1eee8228b8f360",
                "MacAddress": "02:42:ac:13:00:04",
                "IPv4Address": "172.19.0.4/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

alpine1の中からpingを打ってみると,今度はコンテナ名でも通じることがわかる.

$ docker container attach alpine1

/ # ping -c 2 alpine2

--- alpine2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.095/0.097/0.099 ms


/ # ping -c 2 alpine4
PING alpine4 (172.19.0.4): 56 data bytes
64 bytes from 172.19.0.4: seq=0 ttl=64 time=0.255 ms
64 bytes from 172.19.0.4: seq=1 ttl=64 time=0.156 ms

--- alpine4 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.156/0.205/0.255 ms


/ # ping -c 2 alpine1
PING alpine1 (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.073 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.122 ms

--- alpine1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.073/0.097/0.122 ms

この機能はautomatic service discoveryという.

一方alpine3はネットワークが異なるため,名前解決できないし,IPアドレスでも到達できない.

/ # ping -c 2 alpine3
ping: bad address 'alpine3'
/ # ping -c 2 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes

--- 172.17.0.2 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss

CTRL+p+qでalpine1を抜けて,今度はalpine4からpingを打ってみる.

$ docker container attach alpine4
/ # ping -c 2 alpine1
PING alpine1 (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.067 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.086 ms

--- alpine1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.067/0.076/0.086 ms


/ # ping -c 2 alpine2
PING alpine2 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.090 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.097 ms

--- alpine2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.090/0.093/0.097 ms


/ # ping -c 2 alpine3
ping: bad address 'alpine3'
/ # ping -c 2 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.115 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.155 ms

--- 172.17.0.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.115/0.135/0.155 ms


/ # ping -c 2 alpine4
PING alpine4 (172.19.0.4): 56 data bytes
64 bytes from 172.19.0.4: seq=0 ttl=64 time=0.035 ms
64 bytes from 172.19.0.4: seq=1 ttl=64 time=0.105 ms

--- alpine4 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.035/0.070/0.105 ms

デフォルトブリッジとユーザ定義ブリッジの両方に接続しているので,すべてのコンテナに到達できる.
ただしalpine3はデフォルトブリッジのため,名前解決はできない.

ブリッジネットワークに接続しているので,全てのコンテナがグローバルアドレスへ到達可能.

/ # ping -c 2 google.com
PING google.com (172.217.25.238): 56 data bytes
64 bytes from 172.217.25.238: seq=0 ttl=61 time=10.855 ms
64 bytes from 172.217.25.238: seq=1 ttl=61 time=9.578 ms

--- google.com ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 9.578/10.216/10.855 ms

 終わったら作成したコンテナとネットワークを削除

$ docker container stop alpine1 alpine2 alpine3 alpine4

$ docker container rm alpine1 alpine2 alpine3 alpine4

$ docker network rm alpine-net

まとめ

ブリッジネットワークのチュートリアルを実施した.
商用環境ではdefault bridge networkは非推奨とのこと.
逆に開発環境でコンテナ1台お手軽に試すとかならdefaultでも十分そう.

以上