IDCF テックブログ

IDCF テックブログ

クラウド・データセンターを提供するIDCフロンティアの公式テックブログ

Docker Swarm上で、Goのアプリを動かしてみた。

Docker Swarm概要

Docker Engineを単独に使う場合はどちらかというと開発の段階で使う場面が多いと思います。その場合、自らそれぞれのコンテナを管理しないといけないので、管理が大変だと感じることが多いのではないでしょうか?

そこで今回紹介するDocker EngineのSwarm Modeを利用すると、クラスタリングツールがコンテナの管理を行ってくれるのでコンテナ管理がとても楽になります。 具体的に次のようなことがSwarm Modeを利用で可能になります。

  • サービスのスケーラビリティの向上や単一障害点の排除。  例えばふたつのコンテナで構成されているサービスにアクセス数が多くなった場合に、簡単にコンテナを増やすことが可能。
  • あるコンテナが稼働しているサーバーが落ちた場合に、自動で別のサーバーにコンテナを再作成。
  • Docker Engineがインストールされる複数のサーバーでDocker Swarmクラスタを作成。  複数のアプリケーションそれぞれ違うポート番号使えば、共同にDocker Swarmクラスタに動かすことができます。
  • Docker Swarmクラスタを使うことによってリソースの節約や、サービスのスケーリング、アプリケーションのローリングアップデートを比較的簡単に実現。

今回は、Goで作成するサンプルアプリケーションをIDCFクラウド上に作成したDocker Swarmクラスタにて動かしてみます。

f:id:skawai488:20170522191504p:plain

IDCFクラウド上に作成する仮想マシンのスペック

IDCFクラウド上で3台のCentOS 7.3 仮想マシンを作成します。詳細は下の表に記載しています。 ※IPは動的に割り当てられたIPをそのまま使っています。

ホスト名 マシンタイプ IP OS
swarm-node-01 standard.S4 10.32.0.68 CentOS 7.3
swarm-node-02 standard.S4 10.32.0.34 CentOS 7.3
swarm-node-03 standard.S4 10.32.0.20 CentOS 7.3

Dockerのインストール

Docker Swarmクラスタを構築するにあたって、3台の仮想マシンすべてにDocker Engineをインストールします。現時点で一番新しいDocker 17.03をインストールします。

sudo yum update -y
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # Docker Community EditionのRepositoryをインストール
sudo yum makecache fast # Repositoryのメタデータが使えるように
sudo yum info docker-ce
sudo yum install docker-ce -y

sudo groupadd docker
sudo gpasswd -a ${USER} docker # 現在のユーザーがdocker コマンドを使えるように

sudo systemctl enable docker
sudo systemctl start docker # Docker Engineを起動する

Docker Swarmクラスタを構築

まずswarm-node-01でDocker Swarmクラスタを初期化をします。初期化を行うとswarm-node-01Managerになります。

[deploy@swarm-node-01 ~]$ docker swarm init --advertise-addr 10.32.0.68
Swarm initialized: current node (ynyqekkutkbj3td54e7tw9rpt) is now a manager.

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

    docker swarm join \
    --token SWMTKN-1-0ng5lwnm042158d003x0a2g78lz9263rs0upid8t3qevse53ua-cv57yx0i5spgh3j5u65o3nbhi \
    10.32.0.68:2377

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


$ docker node ls
ID                           HOSTNAME       STATUS  AVAILABILITY  MANAGER STATUS
ynyqekkutkbj3td54e7tw9rpt *  swarm-node-01  Ready   Active        Leader

残りの2台の仮想マシンをクラスタにjoinさせるため、上記コマンドが提示してくれたdocker swarm joinコマンドを入力します。これで2台の仮想マシンがWorkerとしてクラスタにJoinしました。

[deploy@swarm-node-02 ~]$ docker swarm join \
>     --token SWMTKN-1-0ng5lwnm042158d003x0a2g78lz9263rs0upid8t3qevse53ua-cv57yx0i5spgh3j5u65o3nbhi \
>     10.32.0.68:2377
This node joined a swarm as a worker.

[deploy@swarm-node-03 ~]$ docker swarm join \
>     --token SWMTKN-1-0ng5lwnm042158d003x0a2g78lz9263rs0upid8t3qevse53ua-cv57yx0i5spgh3j5u65o3nbhi \
>     10.32.0.68:2377
This node joined a swarm as a worker.

クラスタ中のノードの状況はdocker nodeコマンドで調べることができます。

$ docker node ls
ID                           HOSTNAME       STATUS  AVAILABILITY  MANAGER STATUS
1lqadobvtsp0yrc3s9bp9lgh8    swarm-node-03  Ready   Active
vrk6bserls04hj9rrrn8zpv9t    swarm-node-02  Ready   Active
ynyqekkutkbj3td54e7tw9rpt *  swarm-node-01  Ready   Active        Leader

さらにノードごとの詳細はdocker node inspectコマンドで調べることができます。

$ docker node inspect swarm-node-02

例としてNginxサービスをDocker Swarmクラスタに作成してみます。コンテナ自体は80番ポートにEXPOSEされています。サービスを作る際に仮想マシンの8080番ポートとコンテナの80番ポートをひも付けて公開します。 これで、外部から仮想マシンの8080番ポートにアクセスするとNginxサービスにフォワードされるようになります。

Docker Swarmモードにはrouting mesh機能があり、すべてのノードがリクエストを受け付けすることができます。ポート番号に基づいて、適切にリクエストをサービスにフォワードしてくれます。

[deploy@swarm-node-01 ~]$ docker service create --name test-nginx --replicas 2 -p 8080:80  nginx:1.10 # コンテナのレプリケーションを2にする
t929h1hugo7hk8es4oqqgqzo1

[deploy@swarm-node-01 ~]$ curl 10.32.0.68:8080 # クラスタのどのノードにアクセスしてもレスポンスが返ってくる
[deploy@swarm-node-01 ~]$ curl 10.32.0.34:8080
[deploy@swarm-node-01 ~]$ curl 10.32.0.20:8080

test-nginxサービスの詳細を見てみると、2つのコンテナがswarm-node-01swarm-node-02の2台の仮想マシン上で動いています。test-nginxサービス自身にVirtual IP(10.255.0.6)が割り当てられています。

[deploy@swarm-node-01 ~]$ docker service ps test-nginx # サービスは2つのレプリケーションで構成されている
ID            NAME          IMAGE       NODE           DESIRED STATE  CURRENT STATE           ERROR  PORTS
x6cz28kimuzx  test-nginx.1  nginx:1.10  swarm-node-02  Running        Running 24 seconds ago
n47lcgfvmm6w  test-nginx.2  nginx:1.10  swarm-node-01  Running        Running 24 seconds ago

[deploy@swarm-node-01 ~]$ docker service inspect --format="{{json .Endpoint.VirtualIPs}}" test-nginx # Virtual IPを取得
[{"NetworkID":"mifrgvabwccmkxok88ouf1z3q","Addr":"10.255.0.6/16"}]

swarm-node-01swarm-node-02で動いているコンテナはdocker inspectコマンドでIP アドレスが割り当てられていることが確認できます。 docker psコマンドでコンテナ名の取得をし、docker inspectコマンドでIPの確認をします。

[deploy@swarm-node-01 ~]$ docker ps # swarm-node-01に動いているコンテナの名前を取得
CONTAINER ID        IMAGE                                                                           COMMAND                  CREATED             STATUS              PORTS               NAMES
7e7b916c51ec        nginx@sha256:6202beb06ea61f44179e02ca965e8e13b961d12640101fca213efbfd145d7575   "nginx -g 'daemon ..."   10 minutes ago      Up 10 minutes       80/tcp, 443/tcp     test-nginx.2.n47lcgfvmm6w0gnl28khfc95k
bash:swarm-node-02
[deploy@swarm-node-02 ~]$ docker ps  # swarm-node-02に動いているコンテナの名前を取得
CONTAINER ID        IMAGE                                                                           COMMAND                  CREATED             STATUS              PORTS               NAMES
6ba7fc790572        nginx@sha256:6202beb06ea61f44179e02ca965e8e13b961d12640101fca213efbfd145d7575   "nginx -g 'daemon ..."   10 minutes ago      Up 10 minutes       80/tcp, 443/tcp     test-nginx.1.x6cz28kimuzxisopfcogaewhr

[deploy@swarm-node-01 ~]$ docker inspect test-nginx.2.n47lcgfvmm6w0gnl28khfc95k --format="{{json .NetworkSettings.Networks.ingress.IPAddress}}" # コンテナ test-nginx.2のIPを取得
"10.255.0.8"

[deploy@swarm-node-02 ~]$ docker inspect test-nginx.1.x6cz28kimuzxisopfcogaewhr --format="{{json .NetworkSettings.Networks.ingress.IPAddress}}" # コンテナ test-nginx.1のIPを取得
"10.255.0.7"

test-nginxサービスの2つのコンテナは別々の仮想マシンにありますが、Overlay Networkを通して、同じネットワークにあるように通信できています。各コンテナからサービスのVirtual IPにもアクセスできます。

[deploy@swarm-node-01 ~]$ docker exec -it test-nginx.2.n47lcgfvmm6w0gnl28khfc95k /bin/bash
root@7e7b916c51ec:/# ping 10.255.0.7 # もうひとつの別のコンテナをPingすることができる
...
64 bytes from 10.255.0.7: icmp_seq=0 ttl=64 time=0.298 ms
...

root@7e7b916c51ec:/# apt-get update
root@7e7b916c51ec:/# apt-get install curl
root@7e7b916c51ec:/# curl 10.255.0.6 # サービスのVirtual IPにもアクセスできます。
...
<p><em>Thank you for using nginx.</em></p>
...

Nginxのイメージ1.10から1.11へアップデートしたい場合はサービスをアップデートします。イメージタグの変更以外にも、いろいろなオプションを使うことができます。たとえば、-limit-memoryオプションを使うとコンテナの使用できるメモリが制限されます。コンテナが実際使用しているメモリの量はctopコマンドで簡単に確認できます。また、--update-delayオプションを使うことによって、コンテナは10秒間隔で仮想マシンごとにアップデートしていきます。

[deploy@swarm-node-01 ~]$ docker service update test-nginx --image nginx:1.11 --limit-memory 2G --update-delay 10s

Docker Swarmクラスタのノードは2種類に分けられています。ひとつはManager、もうひとつはWorkerです。Managerノードはクラスタのステータスやスケジュールリングなどを管理しています。今回3台の仮想マシンでクラスタを組んでいるため、すべてのノードをManagerにすることをおすすめします。すべてのノードをManagerにすることで、1台のManagerが落ちても、クラスタのスケジューリング管理に問題が出なくなります。

それではswarm-node-02swarm-node-03Managerにしていきます。その後、1台の1仮想マシンのDocker Engineを停止して(systemctl stop docker)、そこに動いているコンテナは別の仮想マシンに作成されるかを見てみましょう。

[deploy@swarm-node-01 ~]$ docker node update --role manager swarm-node-02
[deploy@swarm-node-01 ~]$ docker node update --role manager swarm-node-03

GoアプリをDocker Swarmにデプロイ

今回はGoで作成したサンプルのHTTPSアプリケーションを、Docker Swarmにデプロイしてみます。 ローカルの開発環境でアプリケーションを作成したので、手順を紹介します。 まずは、HTTPS通信のアプリケーションを作るのでサーバーの秘密鍵server.keyと証明書server.crtを作成します。

$ openssl genrsa -out server.key 2048
$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

つぎに /tmp/play.goファイルを作成します。

package main

import (
        "log"
        "net/http"
        "os"
)

func greet(w http.ResponseWriter, req *http.Request) {
        log.Println("Hey there!") // app.logに書き込む
        w.Write([]byte("Hey there!"))
}

func main() {
        file, err := os.OpenFile("app.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
        if err != nil {
                log.Fatal("failed to open file :", err.Error())
        }

        log.SetOutput(file)

        http.HandleFunc("/greet", greet) // greet関数が実行される
        err = http.ListenAndServeTLS(":7443", "server.crt", "server.key", nil)
        if err != nil {
                log.Fatal("ListenAndServe: ", err)
        }
}

go build -o play && ./playコマンドを実行して、 ブラウザからhttps://localhost:8443/greetにアクセスするとHi there!というレスポンスが返ってきます。

Docker Swarmにデプロイする際にはLinuxでアプリを動かすことになるため、GoのCross compilation機能を利用してビルドします。

$ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -ldflags '-s' -installsuffix cgo -o play play.go

アプリのイメージをビルド

この簡単なアプリケーションをDocker Swarmにデプロイしてみますが、バイナリファイルを実行するだけで数百MBのCentOSイメージを使う必要がありません。 できる限り不要なファイル削減したいので、アプリのイメージサイズを小さくして(10Mぐらい)、scratchイメージをベースに作成します。

scratchイメージ自体のサイズは0バイトで、Shellもありません。ですのでコンテナ立ち上げても、docker exec -it <container-id> /bin/bashで入れません。

HTTPS通信なので、Trust Storeルート証明書ca-certificates.crtが必要となるので Mozillaのデータをダウンロードします。

$ curl -o ca-certificates.crt https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt

ログ収集するときに時間が出力されるので、タイムゾーンの情報も必要です。zoneinfo.tar.gzは仮想マシンにログインして取得します。

$ tar cfz zoneinfo.tar.gz /usr/share/zoneinfo

/tmp/Dockerfileを作成します。内容は次の通りで、必要な情報は証明書、タイムゾーン情報、鍵、コンパイルされたバイナリファイルです。

FROM scratch

ADD ca-certificates.crt /etc/ssl/certs/
ADD zoneinfo.tar.gz /

ADD server.crt /
ADD server.key /

ADD play /

WORKDIR /
CMD ["/play"]

イメージをビルドした後にDocker HubにPushします。

$ docker build -t wzj8698/go-swarm:1.0 .

$ docker login # PushするまえにDocker Hubにログインする必要がある
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: wzj8698
Password:
Login Succeeded

$ docker push wzj8698/go-swarm:1.0
The push refers to a repository [docker.io/wzj8698/go-swarm]
7de27f392ae7: Pushed
f6dde78f2228: Pushed
3d99cecfd591: Pushed
1.0: digest: sha256:53ab1aa6295da87ee303041fc161e66649c9839789c712a3c26b1fa881ea2071 size: 948

それでは、ローカル環境から、仮想マシン環境へログインしてください。 これからデプロイに入ります。3台の仮想マシンからwzj8698/go-swarm:1.0をPullしておきます。docker service createコマンドでデプロイし、--mountオプションも一緒に使うため、3台の仮想マシンにあらかじめログファイルapp.logを作成しておく必要があります。--mountオプションを使うことで、仮想マシン側のファイルをコンテナ内のファイルと結びつけることができます。これで、仮想マシン側でもログを確認できるようになります。

[deploy@swarm-node-01 ~]$ docker pull wzj8698/go-swarm:1.0
[deploy@swarm-node-02 ~]$ docker pull wzj8698/go-swarm:1.0
[deploy@swarm-node-03 ~]$ docker pull wzj8698/go-swarm:1.0

[deploy@swarm-node-01 ~]$ cd && touch app.log
[deploy@swarm-node-02 ~]$ cd && touch app.log
[deploy@swarm-node-03 ~]$ cd && touch app.log

[deploy@swarm-node-01 ~]$ docker service create  --name go-swarm --replicas 2  -p 7443:7443 --mount type=bind,source=/home/deploy/app.log,destination=/app.log  wzj8698/go-swarm:1.0

これで外から7443番ポートにアクセスできるようになっています。Docker Swarm 上でアプリが稼働している状態になりました。 今回は Go のアプリケーションをDocker Swarm 上にて動かしていますが、ぜひ自分で作成したアプリケーションで試してみてください。 いろいろなコンテナをたくさん管理する場合は、Docker Swarmを使うとコンテナ管理が捗りますよ。

Copyright © IDC Frontier Inc.