Docker swarmを試してみた

TD; LR

docker swarmは気軽に使えてとても良いが、実運用は自分でちゃんと試してからの方がよさげ。

まえがき

Dockerの1.12.0からDocker swarmがdockerと統合され、クラスタ環境でのDockerが簡単に使えるようになりました。この記事は夏休みの自由研究として、このDocker swarm modeについて調べてみた記録です。

長いです。細かいです。前提をいろいろ飛ばしています。全部読むことはおすすめしません。

間違っている点は指摘していただけると助かります。

リポジトリ

この文章に関するリポジトリ
https://github.com/shirou/docker-swarm-test
docker engine: docker engineのCLI的なやつ。
https://github.com/docker/docker
swarmkit: この中にswarmの実際の中身が入っている
https://github.com/docker/swarmkit
swarm: 1.12までのswarm実装。今回は対象外
https://github.com/docker/swarm

docker swarm mode

docker swarm modeは、複数のdocker hostを束ねて使えるようになるモードです。従来はdocker swarmとして別物でしたが、1.12からdocker engineに統合されました。

とかいう概要はばさっと飛ばします。以下は用語やコマンドのメモです。

登場人物

manager node
管理ノード。3〜7がmanager nodeの最適個数
worker node
task実行ノード
node
一つのdocker engineが動いているサーバー
service
複数のdocker engineが協調して、一つのportでサービスを提供する一つのswarm clusterは複数のserviceを持てる
https://docs.docker.com/engine/swarm/images/swarm-diagram.png

使いかた

  • swarm
    • docker swarm init
      • swarm clustrを初期化します。つまり、このコマンドを実行したdocker hostが一番最初のmanager nodeとなります。
    • docker swarm join
      • 指定したmanager nodeが管理するswarm clusterにjoinします。--tokenでswarm cluster tokenを指定します。manager tokenを指定すればmanagerとして、worker tokenを指定すればworkerとしてjoinします。あるいは--managerで明示的にmanagerとしてjoinさせることもできます。
    • docker swarm leave
      • clusterから抜けます
  • node
    • docker node ls
      • nodeの状態を見ます
    • docker node ps
      • taskの状態を見ます
    • docker node update
      • nodeをupdate
    • docker node demote/docker node promote
      • workerに降格(demote) / managerに昇格(promote)
  • service
    • docker service create
      • serviceを作成
    • docker service ls
      • serviceの状態を見ます
    • docker service ps
      • taskの状態を見ます
    • docker service update
      • rolling updateをします
  • network
    • docker network create
      • overray networkを作成

今回使うプロセス

今回実行するプロセスは以下の通り。コードはこちら

package main

import (
     "fmt"
     "net/http"
     "strings"
     "time"
)

var wait = time.Duration(1 * time.Second)

func handler(w http.ResponseWriter, r *http.Request) {
     rec := time.Now()

     time.Sleep(wait)
     rep := time.Now()

     s := []string{
             rec.Format(time.RFC3339Nano),
             rep.Format(time.RFC3339Nano),
     }

     fmt.Fprintf(w, strings.Join(s, ","))
}

func main() {
     http.HandleFunc("/", handler)
     http.ListenAndServe(":8080", nil) // fixed port num
}

単に1秒待ってからリクエスト時とリプライ時の時刻をCSVを8080ポートで返しているだけ。1秒をblockingして待つという最悪な処理なのです。

今回buildはCircleCIに任せてあって、tar.gzを作ってあるので、各nodeでは以下のようにimportすればいい。tagは適当。

$ docker import https://<circleci artifact URL>/docker-swarm-test.tar.gz docker-swarm-test:1

ヒント

golangはglibcなどが必要なく1バイナリで実行できるので、Dockerfileとか別に要らなくてtar.gzで十分。linuxでnetを使うとdynamic linkになる件はGo 1.4でstatic binaryを作成するgolangで書いたアプリケーションのstatic link化をみてください。

$ docker service create --name web --replicas 3 --publish 8080:8080 docker-swarm-test:1 "/docker-swarm-test"
$ docker service ps web
ID                         NAME   IMAGE                NODE       DESIRED STATE  CURRENT STATE          ERROR
18c1hxqoy3gkaavwun43hyczw  web.1  docker-swarm-test:1  worker-2   Running        Running 3 minutes ago
827sjn1t4nrj7r4c0eujix2it  web.2  docker-swarm-test:1  manager-1  Running        Running 3 minutes ago
2xqzzwf2bte3ibj2ekccrt6nv  web.3  docker-swarm-test:1  worker-3   Running        Running 3 minutes ago

この状態で、コンテナが動いている worker-2,3, manager-1に対して、curlでGETすると返してくれます。それ以外に、コンテナが動いていないworker-1に対してcurlで聞いても、きちんと答えてくれます。これは、中でリクエストが転送されているからです。

rolling update

swarm clusterで--publishを付けてserviceを提供すると、一つのportで複数のnodeに対してリクエストを割り振るload balancingをやってくれます。従って、今までdockerでportが動的に変わったり、一つのnodeで同じport番号を使ってしまったりするのが問題になる場合がありましたが、その問題は生じません。また、swarm cluster内でLoad balancingを行ってくれるので、rolling updateも容易です。

% sudo docker service update --image "docker-swarm-test:1" web

というわけで、試してみました。先ほどのプロセスは1秒待つので、下手に切るとリクエストを取りこぼすはずです。

ツールにはabを使います。今回は処理能力ではなく、リクエストを取りこぼさないかどうかのテストなので、abで十分と判断しました。

% ab -rid -c 10 -n 500 http://45.76.98.219:8080/

Concurrency Level:      10
Time taken for tests:   50.146 seconds
Complete requests:      500
Failed requests:        8
   (Connect: 0, Receive: 4, Length: 0, Exceptions: 4)

ということで、取りこぼしてしまいました。残念ですね。--update-delayは次のコンテナを起動するまでの時間なので関係がなさそうです。--restart-delayも組み合わせてみましたがだめでした。nodeのステータスをdrainに手動で変更していけばうまく動くかもしれませんが、手間がかかるので試してません。

調べてみると、この辺りが関連してそうです。

次のpatch releaseで修正されるとのこと。ちょっとまだlibnetworkまで調査が足りてないのでこれで本当に直るのかは分かりませんが、今はまだ本番環境上で使うのは早そうです。

というより、nginxとか使え

というより、そもそもingress overlay networkは内部で使う用途で、外部サービスに公開する用途ではない、ということのようです。外部に公開する場合は、nginxなりを使って後述のDNS service discoveryに従って使うコンテナを決定しろ、とのことのようです。

この辺りはこれからもうちょっと調べないといけない感じです。

network

docker network createでnetworkを作成します。あとからdocker service update --network-addでnetworkを追加しようとしたら

Error response from daemon: rpc error: code = 2 desc = changing network in service is not supported

と怒られたのでserviceを作り直します。

docker service create --replicas 3 --name web --network webnet ...

その後、shellとしてalpineを立ち上げます。

$ docker service create --name shell --network webnet alpine sleep 3000
$ sudo docker service ls
ID            NAME        REPLICAS  IMAGE                COMMAND
1f9jj2izi9gr  web         3/3       docker-swarm-test:1  /docker-swarm-test
expgfyb6yadu  my-busybox  1/1       busybox              sleep 3000

execで中に入ってnslookupで同じnetworkに属しているwebserviceをDNSで探します。

$ docker exec -it shell.1.3x69i44r6elwtu02f1nukdm2v /bin/sh
/ # nslookup web

Name:      web
Address 1: 10.0.0.2

/ # nslookup tasks.web

Name:      tasks.web
Address 1: 10.0.0.5 web.3.8y9qbba8eknegorxxpqchve76.webnet
Address 2: 10.0.0.4 web.2.ccia90n3f4d2sr96m2mqa27v3.webnet
Address 3: 10.0.0.3 web.1.44s7lqtil2mk4g47ls5974iwp.webnet

webつまりservice名を聞けばVIPを、tasks.webを聞けば各nodeを直接DNS RoundRobinで答えてくれます。

このように、同じnetworkに属しさえすれば、他のServiceを名前で引くことができるので、コンテナ間の連携がしやすいと思います。

プロトコル

raft

docker swarmでは、複数のmanager node間でのLeader選出にはRaft consensusを使っています。raftの実装はetcdのraft libraryです。docker node lsでどのManagerがLeaderかが分かります。

$ docker node ls
ID                           HOSTNAME   STATUS  AVAILABILITY  MANAGER STATUS
5g8it81ysdb3lu4d9jhghyay3    worker-3   Ready   Active
6td07yz5uioon7jycd15wf0e8 *  manager-1  Ready   Active        Leader
91t9bc366rrne19j37d0uto0x    worker-1   Ready   Active
b6em4f475u884jpoyrbeubm45    worker-2   Ready   Active

raftですので、きちんとした耐故障性を持つためにはmanager nodeは最低限3台必要ですし、3台中2台が落ちたらLeader選出ができなくなります。この場合、docker swarmでは、新規のtaskを受け付けられない状態になります。

heat beat

swarm node間はheatbeatで死活監視をしています。heatbeatは通常5秒間隔ですが、docker swarm init時に--dispatcher-heartbeat durationで指定することも出来ます。死活監視の結果はgossipで分配されます。

疑問

serviceを消したらcontainerはどうなるの?

docker service rmでserviceを消すと、containerも全部消えます。消えるまでは時間がかかるので注意

worker node以上のtaskを振られたら?

nodeが3つしかない場合に、 docker swarm scale web=10とかしたらどうなるか?

答えは一つのnodeに複数のコンテナが立ち上がります。

podの概念

なさそう。 serviceを作成するときに--constraintで配置制約としてaffinityを使うとかなのかな。

あとがき

もはやコンテナ技術そのものはどうでも良くて、Kubernetesなどの複数Node管理が重要だと個人的には思っています。Docker swarm自体は今までもあったけれども、それをDocker Engineに統合することで、コンテナだけではなくその上の管理も事実上の標準を取っていくぞという意気込みが感じられます。しかも、kubernetesは使いはじめるまでが大変ですが、その手間がほぼないため、優位と感じます。

swarm clusterを組むのは簡単ですし、cluster自体はとても安定しているように思いました。もちろん、split brainなど凝ったテストをしていないのでなんともですが、raft関係はetcdのを使っていますので安定しているのではないかと思っています。

ただ、networkについてはまだ不安定なようで、serviceを作ったり消したりnetworkを作ったり消したりしていると、名前が引けなくなったりしました(詳細は追っていません)。

networkやgracefulなど、まだこなれていない点はありますが、今後Docker swarmは普及していくのではないかな、と思います。

DockerコンテナでAnsibleをテストする

Ansible 2.0になり、Docker connection pluginが標準で入りました。これにより、Docker内にsshdを立てることなくAnsibleを直接実行できるようになりました。

すでに導入されている方も多く、かなり今更ではありますが、Dockerコンテナに対してAnsibleを実行してテストする方法についてここに記します。

Dockerに対する場合の制限

まず最初にAnsibleをDockerコンテナに対して実行する際の制限についてです。

基本的にはすべての機能が使えます。ただ、以下の制限があります。

  • /etc/hosts, /etc/resolv.conf, /etc/hostnameは書き換えできない

    これらのファイルはDockerがbind mountしており、書き換えられるが、置き換えることは出来ないため。/etc/hostnameが変更できないため、hostnameモジュールでの変更もできない。

また、実行するイメージによっては少なくとも以下の問題があります。他にもいろいろあるかもしれません。このあたりはDocker自体に関する問題で、Ansible特有の問題ではないので、なんとか解決して下さい。

  • systemdのserviceが起動できない

    D-busがないため、Failed to connect to bus: No such file or directoryと言われる。upstartやrc initは起動できる。CAP_SYS_ADMINcapabilityが必要

  • sudoがない場合がある

付け加えるならば、まっさらなイメージからテストをしていくとダウンロードなどに時間がかかってしまいますので、適宜設定を施したイメージを事前に用意しておくとテストの時間が減ると思います。

Inventory

さて、本題です。Ansibleの docker connection pluginを使うには

web ansible_connection=docker ansible_host=<コンテナID>

というように、ansible_connection=dockerとするだけですぐに使えます。しかし、ansible_hostにはコンテナIDを指定しなければいけません。DockerのコンテナIDは本当に一時的なものなのでここに書くのはデバッグ時だけです。

これを回避するためにはdockermoduleを使ってコンテナを立ち上げ、add_hostでグループを生成することも可能ですが、playbookをテスト用に編集する必要が出てきます。それでもいい場合もありますが、せっかくですからDocker dynamic inventoryを使いましょう。

Docker dynamic inventory

GitHubのansibleのリポジトリからdocker.pyを取得し実行権限を付与しておきます。docker.ymlはなくても構いません。

# docker containerを立ち上げる
$ docker run --name <hostsでの指定対象> ubuntu:16.04 /bin/sleep 3600

# 立ち上げたdocker containerに対してansibleを実行する
$ ansible-playbook -i docker.py something.yml

で現在起動しているコンテナのnameからコンテナIDを取得して、使ってくれます。docker.pyで取得できる情報の一部を以下に示します。name以外にもimageやコンテナIDでグループが作成されていることが分かります。しかし、今回はテストなので、通常のグループと同じ名前を使いたいためnameを使います。

"web": [
  "web"
],
"image_ubuntu:16.04": [
  "web"
],
"zzzzzd5bed36e033fac72d52ae115a2da12f3e08b995325182398aae2a95a": [
  "web"
],
"zzzzz114d5bed": [
  "web"
],
"running": [
  "web"
],
"docker_hosts": [
  "unix://var/run/docker.sock"
],
"unix://var/run/docker.sock": [
  "web"
],

Inventory Directory

dynamic inventoryをつかうと、inventoryファイルで指定してあるgroup_varsが使えなくなってしまうのではないか、と思うかもしれません。

その場合ディレクトリを分けてdocker.pyと静的なファイルを入れておきます。そうしておいてinventoryファイルとしてディレクトリを指定すると、静的なファイルとdynamic inventoryの両方から情報をとってくれます。CIでのみ使う場合はCI用のInventoryとして、ディレクトリを分けておくと扱いやすくなると思います。

CircleCI

では、CIを通してみましょう。CircleCIを試します。circle.ymlはこんな感じです。

machine:
  services:
    - docker
  environment:
    DOCKER_API_VERSION: "1.20"

dependencies:
  pre:
    - docker pull ubuntu:16.04
    - sudo pip install ansible ansible-lint docker-py

test:
  override:
    - docker run -d --name web ubuntu:16.04 /bin/sleep 3600
    - ansible-playbook -i inventory_docker web.yml test.yml --syntax-check
    - ansible-lint test.yml
    - ansible-playbook -i inventory_docker web.yml test.yml -l running

--syntax-checkやansible-lintもついでに行っています。DOCKER_API_VERSIONはCircleCIのdockerが古いために設定しています。また、docker runで--name webとしています。これは、通常使うplaybookではweb グループに対して実行しており、そのplaybookを変えたくないからです。

これでpushすると、

fatal: [web]: FAILED! => {"changed": false, "failed": true, "rc": 1, "stderr": "Error response from daemon: Unsupported: Exec is not supported by the lxc driver\n", "stdout": "", "stdout_lines": []}

と怒られてしまいました。そうです。CircleCIはlxc driverを使っており、Docker connection pluginが使うdocker execが使えないのです。

ということで諦めました。

他のCIサービスはwerckerとかdrone.ioありますが、これらはそもそもCIでDockerを使っており、Docker in Dockerになってしまいいろいろ大変です。

別解: 自前Dockerホストを用意する

あるいは、circle.ymlのenvironmentDOCKER_HOSTを設定することで、CircleCI外に立てたDockerホストに対して実行することもできます。次に説明するGitLabを使うよりも手軽かもしれませんが、セキュリティ設定をしっかりする点は特に気をつけて下さい。

GitLab

GitLabが最近流行りですね。最近CIも付いたのでこれを使う場合も紹介します。

gitlabやgitlab CI runner自体のインストールは省略します。Docker内で実行するCI Runnerもありますが、それではDocker in Dockerになってしまいますので、今回の用途ではshell runnerにすることを忘れないで下さい。

結論からいうと、runnerの設定がしっかりしてあれば、このような.gitlab-ci.ymlで動きます。ほとんどCircleCIと変わらず、after_scriptによるコンテナの削除が入っているぐらいです。

before_script:
  - pip install ansible ansible-lint docker-py

stages:
  - build

build_job:
  stage: build
  script:
    - docker run -d --name web ubuntu:16.04 /bin/sleep 3600
    - ansible-playbook -i inventory_docker web.yml test.yml --syntax-check
    - ansible-lint test.yml
    - ansible-playbook -i inventory_docker web.yml test.yml -l running

after_script:
  - docker kill `docker ps -aq`
  - docker rm `docker ps -aq`

runnerの設定としてはsudo gpasswd -a $USER dockerをしてsudoをしなくてもdockerを使えるようにしておくとよいと思います。

追記: Travis CI

@auchidaさんからTravis CIならば使える、ということをお聞きしました。auchidaさんのリポジトリを参考にしました。

ポイントはsudo: requiredを入れることのようです。

しかし、たぶんvirtualenvとsystemとがなにかおかしいようで、docker dynamic inventoryを実行時に以下のエラーが出ました。そのうち直したいと思います。

class AnsibleDockerClient(Client):
    NameError: name 'Client' is not defined

ありがとうございました。

まとめ

この記事では、Dockerコンテナを利用してAnsibleのテストを行う方法についてご紹介しました。

  • Ansible2.0からDockerコンテナに対して直接ansibleを実行できる
  • 一部の制限はあるが、Docker上でも問題なく動く
  • CircleCIでは動かないので以下の三つの方法を紹介
    • 自前でDockerホストを用意する
    • GitLabなどを立てる
    • Travis CIを使う

おまけ: ansible-lintのルール

最近弊社内でPlaybookの書き方を統一するためにansible-lintのルールの制定を始めました。ansible-lint-rulesにて公開しています。

Long descriptionがないなど、まだ途中ではありますが、使ってみてIssueやPRをいただけると大変ありがたく思います。

DBから直接golangのモデルを生成するxoのご紹介

Webアプリを開発している時に、DBのモデル定義の方法にはいろいろなやり方があると思います。

xoは、DBから直接golangのモデル定義を自動生成するツールです。

  • PostgreSQL
  • MySQL
  • Oracle
  • Microsoft SQL Server
  • SQLite

に対応しており、良く使われるRDBをほぼカバーしていると思います。

インストール

goのツールですので、go getでインストールできます。

$ go get -u golang.org/x/tools/cmd/goimports (依存性のため)
$ go get -u github.com/knq/xo

これでxoというコマンドがインストールされたと思います。

使い方

ではさっそく使ってみましょう。使用するDBはPostgreSQLです。

CREATE TABLE users (
    id   BIGSERIAL PRIMARY KEY,
    name TEXT,
    age  INT NOT NULL,
    weight INT,
    created_at timestamptz NOT NULL,
    updated_at timestamptz
);

CREATE INDEX users_name_idx ON users(name);

このようなテーブルとインデックスがあったとしましょう。

xoを実行します。

$ mkdir -p models  # 事前にディレクトリを作っておく
$ xo pgsql://localhost/example -o models

そうすると、models以下に

  • user.xo.go
  • xo_db.xo.go

という二つのファイルが作成されます。xoで生成したファイルは*.xo.goとなるので分かりやすいですね。

user.xo.goには以下のような内容が生成されています。NOT NULLをつけたか付けないかで型が違っているところに注目して下さい。また、jsonタグも生成されているので、そのままJSONとして出力もできます。

// User represents a row from 'public.users'.
type User struct {
        ID        int64          `json:"id"`         // id
        Name      sql.NullString `json:"name"`       // name
        Age       int            `json:"age"`        // age
        Weight    sql.NullInt64  `json:"weight"`     // weight
        CreatedAt *time.Time     `json:"created_at"` // created_at
        UpdatedAt pq.NullTime    `json:"updated_at"` // updated_at

        // xo fields
        _exists, _deleted bool
}

この生成されたUser型に対して、以下の関数が生成されています。

  • func (u*User) Exists() bool
  • func (u*User) Deleted() bool
  • func (u*User) Insert(db XODB) error
  • func (u*User) Update(db XODB) error
  • func (u*User) Save(db XODB) error
  • func (u*User) Delete(db XODB) error
  • func (u*User) Upsert(db XODB) error (PostgreSQL 9.5+以上の場合)

XODB型は xo_db.xo.go で定義されているdbに対するinterfaceです。

IDはPrimary Keyですし、nameに対してindexを貼っています。ということで、以下の二つの関数も生成されています。

  • func UserByID(db XODB, id int64) (*User, error)
  • func UsersByName(db XODB, name sql.NullString) ([]*User, error)

これらの関数を使ってSELECTする、という流れです。UsersByNameの方は、返り値がSliceということもポイントですね。

実装

ここまで自動生成されていればあとは簡単です。以下のような実装がすぐにできます。

db, err := sql.Open("postgres", "dbname=example sslmode=disable")
if err != nil {
   panic(err)
}

now := time.Now()
u := &User{
   Age:       18,
   CreatedAt: &now,
}
err = u.Insert(db)
if err != nil {
   panic(err)
}

user, err := UserByID(db, u.ID)  // Insertでu.IDがセットされている
if err != nil {
   panic(err)
}
fmt.Println(user.Age)  // -> 18が返される

SQL

InsertUpdateなどの関数の中身はどうなってるかというと、

// sql query
const sqlstr = `INSERT INTO public.users (` +
        `name, age, weight, created_at, updated_at` +
        `) VALUES (` +
        `$1, $2, $3, $4, $5` +
        `) RETURNING id`

// run query
XOLog(sqlstr, u.Name, u.Age, u.Weight, u.CreatedAt, u.UpdatedAt)
err = db.QueryRow(sqlstr, u.Name, u.Age, u.Weight, u.CreatedAt, u.UpdatedAt).Scan(&u.ID)
if err != nil {
        return err
}

というように、SQLがそのまま生成されています。挙動が分かりやすくてぼくはこの方が好きですね。

関数

xoが扱うのはテーブル定義だけではありません。関数も扱ってくれます。

CREATE FUNCTION say_hello(text) RETURNS text AS $$
BEGIN
    RETURN CONCAT('hello ' || $1);
END;
$$ LANGUAGE plpgsql;

という関数を定義したとしましょう。こうしておくと、sp_sayhello.xo.goというファイルが生成されます。Stored Procedureですね。

この中にはSayHelloというgolangの関数が定義されています。

  • func SayHello(db XODB, v0 string) (string, error)

これ、中身は

// sql query
const sqlstr = `SELECT public.say_hello($1)`

// run query
var ret string
XOLog(sqlstr, v0)
err = db.QueryRow(sqlstr, v0).Scan(&ret)
if err != nil {
        return "", err
}

というように、定義した say_hello 関数をSQLで呼ぶようになっています。ですから、

SayHello(db, "hoge")

というように、golangから呼べるようになります。

まとめ

DBのメタデータからgolangのコードを生成してくれる、xoを紹介しました。

この他、PostgreSQLで定義した型をgolangの型に変換してくれるなど、かなりいたれりつくせりです。また、コードはtemplateで作られており、このtemplateは自分で定義することもできるので、SQL文を変えたり、関数を追加したりなども自由にできます。

ちょうど同じようなものを作ったのですが、xoの方が断然高機能なので、xoを使ったほうが良いかと思います。

書評: 初めてのAnsible

「初めてのAnsible」 という本がオライリージャパンから発売されました。その本を頂いたので、読んでみました。

結論

結論から述べますと、この本は「初めての」と付きますが、これから使いたい人だけでなく、今現在も使っている人にとっても買うべき本だと思います。

Ansibleの実行方法、Playbook、Task、InventoryといったAnsibleを使う上での重要なところが一から順序良く書かれており、すぐに理解できるようになると思います。そういう点で初心者向けです。

それでいて、かなり注釈が多く、初心者向け、ということだけではなく、YAMLの引っかかりやすい文法上の問題や、localhostが暗黙的にinventoryに追加されるというような細かいところまできちんと書いており、現在使っている人に取っても有意義だと思います。特に、筆者はこう考えてこういうルールを適用してplaybookを書いている、といった設計に関することが書かれているのはとてもありがたいです。

また、翻訳もこなれており、読んで引っかかるところはありません。

2.0対応

2.0への対応状況というのが気になるところだと思います。この本は、Ansible: Up and Runningという本の翻訳です。原著が執筆された時にはAnsible 2.0はまだ出ていませんでしたので、当然内容は1.9対応です。

しかし、日本語訳された時に、書かれているPlaybookがすべて2.0.0.2で動作確認をしたとのことです。あまり時間もなかったでしょうに、素晴らしい。ということで、1.9互換の箇所については何の問題もありません。

さらに、付録として「Ansible 2.0」が付いており、ブロックなどの新機能について説明されています。

各章のみどころ

ここからは、各章について独断と偏見で述べていきます。これだけ読んでもよく分からないと思いますので、気になる方はぜひ買って読んでみてください。

1章: イントロダクション

Ansibleをなぜ選択するか、といったことやインストール方法などが書かれています。

Inventoryファイルはやっぱりリポジトリ内にあったほうがいいですよね。なんでデフォルトが/etc/ansible/hostsなんだろう。

2章: Playbook: はじめてみよう

Playbookの書き方と、タスクやPlayといったAnsible用語の定義です。特にYAMLの引用符が必要になるところは、ハマることが結構あるので読んでおくと良いですね。

惜しむらくはcowsayについてあんまり書いてないことです。しかも無効化する方法を書くなんて!

ハンドラについては、ぼくも必須とまでは思っていなかったので、同意ですね。あったほうがtask自体はシンプルになるので良いのですが、確かにハマるところでもあります。ちなみに、--force-handlersオプションを付けることで強制的にハンドラーを起動させることが出来ますが、ハマるときはそういうことに気が付かないものです。

3章: インベントリ: サーバーの記述

インベントリファイルです。Vagrantを使って説明しているのは良いですね。

特に、動的なインベントリについて例を上げて記述されているのが良いです。動的なインベントリは10台以上の場合は使ったほうがいいとぼくも思っています。group_byは…ぼくも使ったことが無いですねぇ…。Ansible Towerのインストールなどには使われているので、「このディストリビューションの時はまとめてこれ」とかではいいのかもしれません。

4章: 変数とファクト

変数とファクトです。とても重要です。

ローカルファクトがちゃんと書いてあるのはいいですね。これ、設置に一手間必要なのであんまり使われてないかもしれませんが、かなり便利ですよ。あと、優先順位「このリストに載っていないすべての方法」とざっくりまとめられてちょっと吹き出しましたが、正しいですね。

5章: Mezzanineの紹介

かなり純粋にMezzanineの紹介です。すいません、これ知りませんでした。

6章: Mezzanineのデプロイ

Vagrantを使った実際のデプロイです。この章を読めば、実本番環境でも対応できるようになると思います。まあ、djangoアプリに寄ってますので、その他の場合はいろいろ変更が必要だとは思いますが、基本的な流れは十分に分かります。

xip.ioは知りませんでした。便利なサービスですね。今度使おう。

7章: 複雑なplaybook

6章で説明しなかった機能の解説です。local_actionrun_oncechange_when/failed_whenなど、重要な機能がいっぱいです。とはいえ、6章と7章を読めば一通りなんでもできるようになりますね。

この章の見どころは「独自のフィルタの作成」と「独自のルックアッププラグインの作成」ですね。さっくりと飛ばしていますが、言及があるのが良いです。自分でフィルタやルックアップを作れるようになると、Ansibleでできることが飛躍的に大きくなります。「YAMLプログライミングで複雑だ」と思っている人は一度自分でフィルタを作ってみると良いと思います。

ちなみに、この章ではルックアップの次に複雑なループが書いてあります。これ、実はwith_hogeなどのループはルックアッププラグインとして実装されているからなのですが、きちんと書いてあるのが良いですね。

8章: ロール

ロールです。ロールはAnsibleの一番大事な機能です。ロールがきっちりと理解できていると、複雑な構成に対しても簡潔なplaybookで対応できるようになります。

varsとdefaultsの差は、結構悩みどころです。著者は上書きされる可能性があるものはdefaultsで、通常の変数はvarsで、という話を書いています。それは正しいのですが、往々にして後から上書きしたくなることが出てくるものです。個人的にはすべてdefaultsでいいのではないか、と思っています。

ちなみに、Ansible Galaxyについてちょっと触れていますが、ansible-galaxyコマンドはAnsible Galaxyサイト以外も指定できるようになったので、共有ロールをプライベートなgit repositoryにおいておき、組織内でansible-galaxyコマンドで共有する、というのができるようになりました。

さらに、-rオプションで、どこからどのロールを入れるかということをテキストファイルで指定できるようになったので、リポジトリに一個そのファイルを置いておくだけで、初期設定ができるようになっています。この点から考えても、ロールに分けることの重要性が分かるかと思います。

9章: Ansibleの高速化

ssh multiplexingとかです。EC2等の場合に、too long for Unix domain socketというエラーが返されることがある点について解説しているのは良いですね。pipeliningについても書いてあるのは良いです。

ファクトのキャッシュは、あんまり使ったことないですね。ファクトの収集は、通常のマシンだと(他の処理と比例して)そんなに時間がかからないので、キャッシュの不整合の方を気にしています。ただ、本当にちょっとしたtaskしか実行しない、あるいは遅いマシンの場合だと劇的に効く場合がありますので、「Ansible遅いんだけど」という方はためしてみるのもいいかと思います。

10章: カスタムモジュール

はい、カスタムモジュールです。Ansibleをちょっと深く使おうとするならば、モジュールの自作が一番です。YAMLでなんとかしようと頑張るのはナンセンスです。

この章では、Pythonでのモジュールの作り方に加えて、少しですがbashでのモジュールの作成方法も記載されています。Pythonでのモジュール作成はかなり充実したヘルパー関数が用意されているので、多くのことに対応できるため、本格的に作るならばpython一択となります。ただし、ちょっとした操作をモジュール化するのであれば、使い慣れた言語の方が良いですので、bashでの作成方法が書かれているのはとても便利だと思います。

11章: Vagrant

Vagrantとansibleプロビジョナの説明です。Vagrantで並列プロビジョンをやるやり方は知らなかったので勉強になりました。

12章: Amazon EC2

EC2です。とはいえ、AWSに限らずセキュリティグループやAMIの取得まで記載されています。この章のうち、tagとグループの関係については、頭に入れておいたほうが良いでしょう。あと、packerについても記載があるので、使ってみるのもいいかもしれません。

メモ: P.230の脚注がなにか編集段階のが残ったままになっている?

13章: Docker

Dockerです。この章では、AnsibleとDockerの関わりについて以下の二点があると書かれています。

  1. 複数のDockerを指定した順番で確実に起動するために使う
  2. Docker imageを作成する時にAnsibleを使う

このうち、1.に関しては、docker-composeのほうが良い気がします。2.についてですが、Ansibleを入れたコンテナを使う方法が記載されています。しかし、Ansible 2.0では、docker connection pluginが標準で添付されていることから、直接dockerのイメージを扱ったほうが良いでしょう。しかし、13.5.5で書かれているように、筆者はdockerイメージ作成はビルドプロセスの一部だという考えがあり、AnsibleでDocker Imageを構築はしていません。ぼくもこの考えに賛成です。

14章: Playbookのデバッグ

playbookのデバッグに役立つtipsが書かれています。ちなみに、ぼくが以前Software DesignでAnsibleに関する記事を書かせて頂いた時に、debugモジュールを真っ先に挙げました。これは、debugモジュールはデバグに必ず使うからです。

まとめ

本書はAnsibleを初めて使う人だけでなく、今実際に使っている人にもオススメの一冊です。ぜひ買いましょう。

Goのバイナリサイズを削減する

Goは1バイナリになってとても良いのですが、その1バイナリが結構大きいのがちょっとネックになる場合もあります。

The Go binary dietという記事を読み、Goのバイナリをダイエットすることにしてみました。

ldflags="-w"

最初に示されていたのは-ldflags="-w"によってDWARFのシンボルテーブルを生成しないようにする方法です。

% go build -ldflags="-w"

ldflags="-s"

次に、-ldflags="-s"によってデバッグのために使われるシンボルテーブルを全部生成しないようにする方法です。

% go build -ldflags="-s"

ここまではよくあるのですが、次に示されていたのはThe Ultimate Packer for eXecutablesを使う方法でした。

UPX

Wikipedia

寡聞にして存じませんでしたが、UPXは1998年からあるソフトウェアのようです。Windows/Linux(i386/ARM/MIPS)/Mac/*BSDとほぼすべての主要プラットフォームで動作します。ライセンスはGPL([注1])です。UPXは実行形式を保ったまま、バイナリを圧縮します。

実際の動作としては、7-Zipなどで使われるLZMAで圧縮しておき、実行時に伸長してメモリ上に直接書き出してから実行します。メモリ上の置き換えが出来ない場合は一時ファイルに書きだしてそれを実行する形式となります。

伸長に必要なコードはわずか数百バイトです。もちろん、伸長時にはCPUやメモリは必要となりますが、一回だけですしGoのバイナリ程度の大きさであれば問題になることはさほどないかと思います。

結果

手元のそこそこ大きいプロジェクトのコードを対象に試してみました。OSはMac OSで、go 1.6を使いました。

対象 バイト数
26MB (27090756)
"-w" 19MB (20414276)
"-s" 26MB (27090756)
"-w" + UPX(-9時) 5.2M (5443584)
"-w" + UPX(-1時) 6.4M (6684672)

あれ、 "-s"では変わってないですね…darwin環境ではでないのかななld周りのなにかだと思うのでそれはあとで追うとして、元々が26MBだったのが、5.2MBまで減りました。

圧縮にupx -9を使った場合、かかった時間は15.70秒でそこそこ時間がかかりますね。3回ほど実行してだいたい同じぐらいでした。伸長時は0.10秒ほどでした。もちろんメモリなどにも依存しますので、この結果は鵜呑みには出来ませんが、あくまで目安として。

さらにいうと、upx -1で圧縮した場合は 0.78秒しかかかりません。それでいて、6.4MBと充分な圧縮効率となりました。この辺りはターゲットとする環境に合わせて決めればいいと思いますが、-1で十分な気もします。

まとめ

Goのバイナリが大きい問題は、ldflagsとUPXを使うことである程度解決できるのではないか、という話でした。UPX知らなかったですけど、コード見るとかなりすごい感じですね。

[注1]ライセンスについて。UPXはGPLに例外事項をつけたライセンスをとっています。これは、GPLであるUPXがバイナリにリンクする形となるため、圧縮対象となるバイナリも通常はGPLになる必要があるのですが、リンクするUPXのコードを改変しないかぎりその必要はない、という例外条項です。ライセンスを確認していただけると分かるように、商用プログラムに対して適用できると明確に書かれています。

AWS Lambdaで効率的にgoバイナリを実行する

最近LambdaとAPI Gatewayを結構使ってて、これはいいな、と思っていたところ、AWS LambdaでJavaとNode.jsとGoの簡易ベンチマークをしてみたという記事を見かけたので、関連した記事を書いてみます。

前提条件

AWS Lambdaではnodejsなどが使えますが、golangは(今のところ)使えません。従って、golangで書きたい場合は、上記記事にある通り、

nodejsを起動 -> nodejsがgolangのバイナリをchild_process.spawn で起動

というやりかたとなります。

そのため、一回一回のリクエストのたびにプロセスを立ち上げるコストが掛かり、毎回500msecほどかかってしまう、という事になってしまいます。

この問題点を解消するライブラリがlambda_procです。

lambda_proc

Lambdaは独自のコンテナで起動されています。起動したコンテナは一定時間存続し続け、リクエストのたびにそのコンテナが使いまわされます。では、一回一回goのプロセスを起動し直すのではなく、起動しっぱなしにしておけばgoの起動コストはなくなるのではないか、というやり方がlambda_procです。

nodeとgoのやりとりはstdin/stdoutを使います。具体的にはこのような感じです。

リクエスト
client -> node --(標準入力)--> go

リプライ
go --(標準出力)--> node -> client

lambda_procでは、node側でJSONを整形し、goに対して一行のJSONとして渡します (Line Delimited JSON)。go側のヘルパーライブラリがJSONを整形し、goのメイン関数に渡します。

返答は、適当なstructを返すとlambda_procのヘルパーライブラリがJSONに整形してnodeに返してくれます。

実際にベンチマークにgoのソースコードは以下となります。標準出力に書いてしまうとnode側に渡ってしまうので、ログは標準エラー出力に書く必要があります。

package main

import (
     "encoding/json"
     "log"
     "os"

     "github.com/aws/aws-sdk-go/aws"
     "github.com/aws/aws-sdk-go/aws/session"
     "github.com/aws/aws-sdk-go/service/dynamodb"
     "github.com/bitly/go-simplejson"
     "github.com/jasonmoo/lambda_proc"
)

// 標準エラー出力に書き出す
var logObj = log.New(os.Stderr, "", 0)

// 元記事のメイン部分の関数
func parse(jsonStr string) {
     js, _ := simplejson.NewJson([]byte(jsonStr))
     records := js.Get("Records")
     size := len(records.MustArray())

     for i := 0; i < size; i++ {
             record := records.GetIndex(i)

             logLn(record.Get("eventName").MustString())  // fmt.Printlnは使えない
             logLn(record.Get("eventId").MustString())
             logLn(record.Get("dynamodb").MustMap())
     }

     ddb := dynamodb.New(session.New(), aws.NewConfig().WithRegion("ap-northeast-1"))
     tableName := "mytest"
     keyValue := "test"
     attribute := dynamodb.AttributeValue{S: &keyValue}
     query := map[string]*dynamodb.AttributeValue{"id": &attribute}

     getItemInput := dynamodb.GetItemInput{
             TableName: &tableName,
             Key:       query,
     }

     obj, _ := ddb.GetItem(&getItemInput)
     logLn(obj)
}

// fmt.Printlnをして標準出力に書き込むと、js側でparseしてしまうので、標準エラー出力に書き出す
func logLn(a ...interface{}) {
     logObj.Println(a...)
}

// なにかstructを返さなければいけないのでダミーの構造体を作成。普通に書くと、むしろstructを返せたほうがいいでしょう
type Return struct {
     Id    string
     Value string
}

// メインとなる関数
func handlerFunc(context *lambda_proc.Context, eventJSON json.RawMessage) (interface{}, error) {
     parse(string(eventJSON))
     return Return{Id: "test", Value: "somevalue"}, nil
}

// mainではlambda_procに登録する
func main() {
     lambda_proc.Run(handlerFunc)
}

ベンチマーク

Lambdaには標準でテスト用のJSONが用意されています。今回はDynamoDB Updateのテスト用JSONを手元に保存しました。curlから叩ける用にLambdaのAPI Endpointを用意し、以下のスクリプトで0.5秒おきにcurlを10回起動しました。

for I in `seq 1 10`
do
curl -X POST -H "Content-type: application/json" --data @body.json https://hoge.execute-api.ap-northeast-1.amazonaws.com/prod/benchmark
sleep 0.5
done

これを実行すると、

Duration Billed Duration Used Memory
367.42 ms 400 ms 14 MB
36.92 ms 100 ms 14 MB
44.00 ms 100 ms 14 MB
46.05 ms 100 ms 14 MB
61.44 ms 100 ms 15 MB
50.48 ms 100 ms 15 MB

Duration Billed Duration Used Memory
393.30 ms 400 ms 14 MB
44.13 ms 100 ms 14 MB
47.99 ms 100 ms 14 MB
52.30 ms 100 ms 14 MB

という二つのログストリームがCloudWatchに出てきました。

このログから2つのコンテナが使われていることが分かります。また、初回は400msecと時間がかかりますが、以降のリクエストに対しては40msec程度しかかかっていません。もちろんメモリも最小限です。今回は10回しか呼び出しをしていませんが、もっと大量でも構いません。また、一回の実行制限時間を長くすればもっと長生きになり、起動コストはかなり無視できる範囲に収まると思います。

ログ出力には注意

上記コード中にも記載していますが、ログ出力にfmt.Printlnを使ってしまうと標準出力に書きだされてしまい、node側に伝わってしまいます。そのため、標準エラー出力に書き出すようにしています。これで解決はできますが、loggingライブラリを使う時は気をつけたほうがいいでしょう。

goのプログラムがシンプルになった

なお、今回lambda_procを使った副次的な効果として、goのプログラムがシンプルになりました。

普通のアプリケーションサーバーだと、HTTPを意識してcontextやらいろいろなことを扱う必要がありました。しかし、この方式では、標準入力/出力しか関係ありません。HTTPに関連することはすべてAWS Lambda (およびAPI Gateway)がまかないます。goは標準入出力だけを見ればよく、しかもその形式は標準的となったJSONです。

この制限により、実装する範囲を狭めることになり、非常に処理が書きやすくなりました。

また、テストもしやすくなりました。従来であれば、"net/http/httptest"を立てて、とか考える必要がありますが、標準入出力だけですみます。

まとめ

lambda_procを使うことで、AWS Lambda上でgoのプログラムを効率的に呼び出せることを示しました。これにより、たまに行う処理だけでなく、がんがんくるリクエストをさばく用途にもgoが使えるのではないか、と思います。

lambdaは無料でかなりのコンピュータ資源を使えるので、うまく使って費用を節約していきたいですね。

CPUやメモリなどの情報を取得するgopsutilのご紹介

Go Advent Calendar 2015の16日目です。

Pythonにはpsutilという、CPUやメモリなどの情報を取得するライブラリがあります。拙作gopsutilはこのpsutilをgolangに移植しようと始まりました。

gopsutilは、以下の特徴があります。

  • Linux/Darwin/FreeBSD/Windowsで動作します
    • もちろん、対応状況はかなり違います
  • (ほぼ) pure golangで実装されています。そのため、クロスコンパイルが容易です
    • ほぼ、というのはdarwinのCPU利用率だけcgoを使っています。cgoを使わない場合は単にnot implementedが返ってきます。
  • psutilにない情報も取れます
    • docker(cgroup)の情報だったり、仮想化状況だったり、好き勝手に機能を追加しています

gopsutilは1年半以上前からこつこつと開発を続けており、おかげさまで今ではgithubのスターが800以上を超えました。

また、

といったソフトウェアからライブラリとして使用されています。

使い方

使い方はREADMEに書いてありますが、以下の通りです。github.com/shirou/gopsutil/memなどをimportして、パッケージに置いてあるメソッドを呼び出すだけです。

import (
    "fmt"

    "github.com/shirou/gopsutil/mem"
)

func main() {
    v, _ := mem.VirtualMemory()

 // structが返ってきます。
    fmt.Printf("Total: %v, Free:%v, UsedPercent:%f%%\n", v.Total, v.Free, v.UsedPercent)

 // PrintするとJSON形式の結果が返ってきます
    fmt.Println(v)
}

これを実行するとこんな感じになります。

Total: 3179569152, Free:284233728, UsedPercent:84.508194%

{"total":3179569152,"available":492572672,"used":2895335424,"usedPercent":84.50819439828305, (以下省略)}

structとして取得できるので、あとは好きな様にいじって下さい。あるいは、PrintしてあげればJSONとして扱えますよ、という感じです。

取得できる情報

結構いろいろな情報が取れるのですが、その一部を紹介します。

  • CPU
    • CPU使用率、CPUのハードウェア情報
  • memory
    • メモリ使用率、スワップ使用率
  • disk
    • パーティション情報、I/O、ディスク使用率、ディスクのシリアル番号
  • host
    • ホスト名、起動時刻、OSや仮想化方式、
    • ログインユーザー情報
  • load
    • Load1, 5, 15
  • Process
    • 個々のプロセスのPIDや状態、起動プロセス名、メモリやCPU使用率など
  • Docker
    • コンテナ内部のCPU使用率やメモリ使用率など

要望があれば既存APIを壊さない範囲であればどんどん増やしていこうかな、と思っています。

中身

gopsutilは非常に泥臭いことをたくさんやっています。まず、pure goでいく、という大原則を立てているため、cgoは使えません。また、Linux/BSD/Windowsでは方式が大きく異なります。

Linux
procファイルシステムなど、ファイルベース
FreeBSD/Darwin
sysctl
Windows
DLL及びWMI

これらはcpu_darwin.goなどのようにファイル名で分けています。

Linux

基本的にテキストファイルベースなので結構楽ですね。

と思いきや、Linuxのバージョンによって取れる情報が違ったり、コンテナ内部では/sysが使えないのでパスを入れ替えられるようにする必要があるなど、細かな点が異なります。

また、ユーザー情報は/var/run/utmpでバイナリ(utmp構造体)で格納されていますので、ちゃんとparseしてあげる必要があります。このあたりは2015年6月のGoConで公開しました(発表はしてません)。

FreeBSD/Darwin

BSD系はsysctlコマンドで各種の情報が取得できます。sysctl vm.stats.vm.v_page_sizeでページサイズが取れたりですね。

ただし、sysctlコマンドで取得できるのはテキスト形式の情報だけです。Proc構造体の情報などはコマンドからは叩けないので、syscall.Syscall6などを使って叩きます。 (余談ですが、godocで出てくるのはLinuxのコードだけですので、Linux以外を知りたい場合はソースコードを読む必要があります)

mib := []int32{CTLKern, KernProc, KernProcProc, 0}
miblen := uint64(len(mib))

// まずlengthを0にして叩き、必要となるバッファ量を得る
length := uint64(0)
_, _, err := syscall.Syscall6(
    syscall.SYS___SYSCTL,
    uintptr(unsafe.Pointer(&mib[0])),
    uintptr(miblen),
    0,
    uintptr(unsafe.Pointer(&length)),
    0,
    0)

// 必要な情報を得る
buf := make([]byte, length)
_, _, err = syscall.Syscall6(
    syscall.SYS___SYSCTL,
    uintptr(unsafe.Pointer(&mib[0])),
    uintptr(miblen),
    uintptr(unsafe.Pointer(&buf[0])),
    uintptr(unsafe.Pointer(&length)),
    0,
    0)

ただし、Darwinはsysctlで取れる情報はFreeBSDに比べてかなり少ないので諦めたところもあります。

Windows

DLLを呼び出して情報を取得しています。

procGetDiskFreeSpaceExW := modkernel32.NewProc("GetDiskFreeSpaceExW")

diskret, _, err := procGetDiskFreeSpaceExW.Call(
     uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))),
     uintptr(unsafe.Pointer(&lpFreeBytesAvailable)),
     uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)),
     uintptr(unsafe.Pointer(&lpTotalNumberOfFreeBytes)))

という感じですね。ただし、さすがにこれはいろいろツライので、github.com/StackExchange/wmiを使ってWMIを叩くようにしています。

type Win32_Processor struct {
    LoadPercentage            *uint16
    Family                    uint16
    Manufacturer              string
    Name                      string
    NumberOfLogicalProcessors uint32
    ProcessorId               *string
    Stepping                  *string
    MaxClockSpeed             uint32
}

func get() {
    var dst []Win32_Processor
    q := wmi.CreateQuery(&dst, "")
    err := wmi.Query(q, &dst)
    if err != nil {
        return ret, err
    }
    fmt.Println(dst)
}

性能

測ってはいませんが、外部コマンドを呼んだりなどを気軽にしているため、そんなに性能はでないはずです。ものすごい高頻度で実行するとホスト側に負荷がかかるでしょう。その点は使う側で適宜キャッシュするなどをして頂ければと思います。

まとめ

ホストのCPUやメモリなどの情報を取得するgopsutilの紹介をしました。

作り始めたのがgoを使い始めて間もないころであり、さらにいろいろなプラットフォームに対する知見は後から得たりしていたので、統一感がなかったりします。そのうちちゃんとしたいと思ってはいるのですが…

もしもgoでシステムの情報を得たいと思った場合には、gopsutilのことを思い出していただけるとありがたく思います。また、PRは随時お待ちしております。

Ansible Docker Connection Pluginを使う

1年以上前の2014年4月にこんな記事を書きましたdocker containerに対して直接ansibleを実行するそれからいろいろあって、Ansible 2.0では標準でDocker Connection Pluginが入っています。(といってもぼくの実装ではありませんが)

Docker Connection Pluginとは

まず、Connection Pluginについて説明します。Ansibleでは通常SSHを使って対象となるホストに接続します。しかし、 Connection Plugin を使うことで、接続方式を切り替えることができます。

代表的なものはlocal connectionです。以下のように書くと、sshではなく、localhostでそのまま実行されます。sshのlocalhostと異なる点は、sshはまったく使わずに、そのままのユーザーがのまま実行する、という点です。開発時に便利ですね。

- hosts: all
  connection: local
  tasks:
    - file: path=/tmp/this_is_local state=directory

その他、以下のconnection pluginが用意されています。paramikoやwinrmは使ったことがある方も多いかと思います。

accelerate
accelaretモード(過去の遺産なので覚える必要なし)
chroot
chroot
funcd
Func: Fedora Unified Network Controller 経由
zone
SolarisのZone
jail
FreeBSDのJail
libvirt_lxc
virtのLXC
paramiko
sshのpython実装
winrm
Windows

この中の一つがdocker connection pluginです。

Docker connection pluginの利点

Docker Connection Pluginを使うことで、Dockerコンテナに対して直接Ansibleを実行できます。具体的にはdocker execでコマンド実行を、ファイルのコピーはdocker cpを実行します。Dockerコンテナの中にsshdを建てる必要はありません。

DockerfileによるBuildが一番シンプルであることは確かです。しかし、

  • Layerを増やさないために\で何行も増やしていく場合がある
  • templateがないため、複数種類のimageを作り分けるのがめんどう
  • 他はAnsibleで管理しているのに、ここだけDockerfileになると管理が分断されてしまう

などの理由で、Ansibleを使いたくなる場合がありますので、そういう場合に有用です。

なお、私はDockerfileで済ませられるのであればその方が良いと思います。Ansibleをわざわざ使う必要はないでしょう。しかし、複雑な構成になってくると、Ansibleの方が便利な場合もあると考えられるため、ここで紹介している次第です。

Docker connection pluginを使う

能書きはこのあたりにして、早速使ってみましょう。ほとんどの人はAnsible 2.0RC1を使っていると思いますので、新たなインストールは必要ないですが、万が一 1.9.4を使っている人はこちらからdocker.pyをダウンロードし、connection_pluginsというディレクトリを作成してその中に置きましょう。以下のような構成になります。

.
|-- connection_plugins
|   `-- docker.py
|-- hosts
`-- site.yml

また、 pipでdocker-pyをインストールしておきましょう。(ansible v2.0では必要ないです)

あとは以下のようにplaybookを書きます。

- name: Dockerコンテナを起動
  hosts: localhost
  connection: local
  vars:
    base_image: ubuntu:latest
    docker_hostname: test

  tasks:
    - name: Dockerコンテナを起動
      local_action: docker image={{ base_image }} name={{ docker_hostname }} detach=yes tty=yes command=bash
    - name: ホストを追加
      add_host: name={{ docker_hostname }}

- name: Dockerコンテナ内を構成
  hosts: test
  connection: docker   # ここで docker connectionを指定
  tasks:  # 好きなように書きます
    - file: path=/tmp/docker state=directory
    - file: path=/tmp/ansible state=directory
    - group: name=admin state=present
    - user: name=johnd comment="John Doe" uid=1040 group=admin
    - copy: src=site.yml dest=/tmp/ansible/

  post_tasks:
    - local_action: shell /usr/local/bin/docker commit {{ inventory_hostname }} ubuntu:ansible

この例のplaybookは以下の二つから構成されています。

  1. Dockerコンテナを起動
  2. 起動したDockerコンテナ内部を構成管理

1の方では、dockerモジュールを使って起動します。ここは普通にlocal接続です。2の方がDocker接続を使用しています。

重要なのは、connection: dockerという行だけが異なっており、そのほかは通常のPlaybookと何ら変わりないという点です。

最後にdokcer commitを実行して、イメージとして保存しています。それまでの箇所はすべて docker execなどで行われているため、保存されず、layerは全体で最後にdocker commitを実行した時に出来る一つだけとなります。これにより、Dockerfileで何行も続けたり、ということをしなくてすみます。

commitも自動化

先ほどの例ではpost_tasksとして、docker commitを実行しています。しかし、Ansible を使って Docker コンテナーをプロビジョニングするという記事では、callback pluginを使って毎回のtask実行毎にcommitをする例を示しています。

この方式はDockerfileによる方式と同じくlayerがたくさんできることになります。その代わり、キャッシュされるため、次回は高速になるという利点もあります。

RemoteのDockerホストを使う

Dockerホストは手元だけでなく、リモートでも構いません。

export DOCKER_HOST=tcp://192.168.0.10:4243

DOCKER_HOSTを環境変数で設定すれば、そのホストを経由してDockerコンテナにアクセスします。試していませんが、Swarmなどでもきちんと動くと思います。

これで、

  • インスタンス起動などのクラウドサービスの利用
  • dockerホスト自体の構築
  • dockerコンテナ/イメージの構築
  • ELBの付け外しなど、デプロイに必要な機能

のすべてがAnsibleで可能となります。

まとめ

この記事ではAnsibleからDockerコンテナを直接触れる Docker Connection Pluginを紹介しました。一つのpythonファイルを置くだけで、Dockerコンテナにたいして通常のsshホストと同じことができるようになります。また、Dockerホストはローカルだけでなく、リモートに対しても可能です。

最後に。

最初の方に述べましたが、Dockerfileで済ませられるのであればその方が良いでしょう。なんでもAnsibleでやりたくなる気持ちも分かりますが、Ansibleを使わなくてはならない理由はありません。適材適所、不要な苦労をしないために、もう一度考えましょう。

そして、そもそもDockerコンテナ内部が複雑な時点で間違っていると思います。golangをDockerでデプロイするで示したように、golangであれば、1バイナリだけ置けば動くため、「プロビジョニング」なんてものは存在しなくなります。現在はGoogleに転職したIanさんも(より)小さいDockerイメージを作ろうという記事を書いており、必要な最小限のファイルだけ置くのが理想です。

自動化をするまえに、「そもそも本当に必要なのか?」を考えましょう。

AWS IoTにmqttcliからつなぐ

mqttcliという、CLIで動くMQTTクライアントを開発し、公開しています。

AWS IoTに対して、このmqttcliから接続してみましょう。

mqttcliのダウンロード

mqttcli filesから、

  • Linux (arm/amd64)
  • FreeBSD (arm/amd64)
  • darwin (amd64)
  • Windows (amd64)

が用意してありますので自身のアーキテクチャにあったバイナリをダウンロードして下さい。その後、chmod u+x mqttcliと実行権限を付与して下さい。

AWS IoTでthingsを作る

  1. AWSコンソールからAWS IoTを開きます。
  2. Create ResourceからCreate Thingを選びます
  3. Nameを入力して、Createを押します
  4. 下の一覧に先ほど入力した名前のThingが出てくるので、選択し、右のタグからConnect a Deviceをクリックします。
  5. Connect a Deviceという画面が出てくるので、NodeJSを押し、Generate Certificate and Policyを押します
  6. 10秒ぐらい経つと、
  • Download Public Key
  • Download Private Key
  • Download Certificate

という三つのファイルをダウンロードするように指示されるので、全部ダウンロードしておきます。

  1. Confirm & Start Connectingを押します。すると、以下の様なJSONが表示されるので、コピーして、ファイルに保存しておきます。

    {
      "host": "A3HVHEAALED.iot.ap-northeast-1.amazonaws.com",
      "port": 8883,
      "clientId": "something",
      "thingName": "something",
      "caCert": "root-CA.crt",
      "clientCert": "2a338xx2xxf-certificate.pem.crt",
      "privateKey": "aad380efffx-private.pem.key"
    }
    
  2. root-CA.crtというファイルをAWS IoT SDKのここに書いてあるとおり、こちらのSymantecのページから取得してください。

  3. 先ほどダウンロードした三つのファイルとJSONファイル、そして、 root-CA.crt を同じディレクトリに置いて下さい。

以上で準備は終わりです。

AWS IoTに接続

ファイルを入れたディレクトリに移動し、 以下のようにmqttcliを起動します。-tでtopicを指定しますが、$をエスケープする必要があるかもしれません。--confで指定するのは先ほど保存したJSONファイルです。-dはデバッグ用です。

$ mqttcli sub -t "\$aws/things/something/shadow/update" --conf something.json -d
INFO[0000] Broker URI: ssl://A3HVHEAALED.iot.ap-northeast-1.amazonaws.com:8883
INFO[0000] Topic: $aws/things/something/shadow/update
INFO[0000] connecting...
INFO[0000] client connected

と出れば成功です。MQTTで接続できています。

Thing Shadowを更新

Thing Shadowを更新するには以下の様なJSONを送ります。

{
  "state": {
    "reported": {
      "from": "mqttcli"
    }
  }
}

mqttcliで送ってみましょう

echo '{"state": {"reported": {"from": "mqttcli"} } }"' | mqttcli pub -t "\$aws/things/something/shadow/update" --conf something.json -d -s

これで、AWS Consoleから見ると、stateが書き換わっているはずです。

このように、mqttcliとを使って、AWS IoTを触ることが出来ました。同様のことがmosquitto_subでもできることを確認しています。

さいごに

MQTTを直に叩くのではなく、AWS IoTのSDKを使いましょう。そうすればこんなふうにMQTTを意識する必要はありません。

AWS IoTおよびThing Shadowsに関する雑感

注意: この文章には思い込みが含まれています。

AWS IoTが発表されました。AWS IoTはManagedなMQTT Serverを用意し、運用が大変となるMQTT Serverを面倒見てくれる、というMQTTに対する評価がちらほら聞こえます。

しかし、違います。AWS IoTの本質は、Thing Shadowsの仕組みです。

とはいえ、まだちゃんと使ったわけではないですし、間違いがあったらご指摘をお願いします。

Thing Shadows

AWS IoTでは、Thingsというものが定義されています。このThingsには二種類が存在します。

Things
実際の物理的なモノ。デバイス
Thing Shadow
Thingsの状態をネットワーク(AWS)上に持たせたもの

Thingsはそのままですね。新しいのはThing Shadowです。これは、「物理的なデバイスを、仮想的にネットワーク上に写した(写像)もの」です。

ThingsとThing Shadowは1対1で結びついています。Thingsになにか変更があれば、Thing Shadowsにも変更が起こります。逆もまたしかり。Thing Shadowsに変更を起こした場合、それはThingsにも変更を起こすのです。

これはすなわち、

  • 物理空間
  • 仮想空間

とが一体化したことにほかなりません。

Thing Shadowの情報

Thing Shadowの実装は、単なるJSONです。:

{
    "state" : {
        "desired" : {
          "color" : "RED",
          "sequence" : [ "RED", "GREEN", "BLUE" ]
        },
        "reported" : {
          "color" : "GREEN"
        }
    },
    "metadata" : {
        "desired" : {
            "color" : {
                "timestamp" : 12345
            },
            "sequence" : {
                "timestamp" : 12345
            }
        },
        "reported" : {
            "color" : {
                "timestamp" : 12345
            }
        }
    },
    "version" : 10,
    "clientToken" : "UniqueClientToken",
    "timestamp": 123456789
}

ここ重要なのは stateとmetadataそれぞれが持つ、desiredreportedです。

1. Thingが更新された場合

物理的なものである、Thingが更新された場合、その情報はMQTTやHTTPSでThing Shadowに通知されます。

これに伴い、reportedの状態が更新されます。

2. Thing Shadowが更新された場合

Thing Shadowは、MQTTやHTTPによって、仮想空間上で更新できます。その場合、desiredの情報が更新されます。

この時、desiredreportedに差があった場合、 このThing ShadowをsubscribeしているThing(一つとは限らない)に対してメッセージが送られます。Thingはこれを受け取り、自分自身の情報を更新することができるのです。そして、更新出来たら、reportedとしてThing Shadowを更新します。


これらの動作により、ThingとThing Shadowsは同期が取れます。もしも、repoteddesiredが異なっている場合は同期がとれていない、ということになります。

さらに言うと、APIとして、 update/get/deleteが用意されており、それぞれに対してacceptedrejectedが用意されています。このため、Things Shadowsを更新しようとしたが、できなかった、ということも分かるようになっています。

MQTTとの違い

ここまで、ThingsとThing Shadowsについて説明してきました。ところで、AWS IoTでは、MQTTの以下の機能がありません。

  • Retain
  • Will
  • QoS2

なぜでしょうか。それはThing Shadowがあるからです。

  • RetainはShadowそのものです
  • Wllはそもそもオフラインという状態が存在しないので必要ありません
  • QoS2による、同期はShadowのdesired/reportedを使えば実現できます

AWS IoTはMQTTというメッセージプロトコルではなく 「状態」を扱うためのものである、という点を重要視するといいのではないでしょうか。

まとめ

AWS IoTを単なるManagedなMQTT Serverとだけ捕らえてしまうと、本質を見誤る気が致します。仮想空間と物理空間の融合、Internet of Things、とはなにか、というのをもう一度見つめなおしてみると面白いかもしれません。

また、今回はまだRuleに関して踏み込んでいません。Thing Shadows/Ruleを組み合わせることにより、人間が介在しない、Machine to Machine、 Things to Things の世界を作ることが出来るはずです。

という、あまりこのblogには書いていない、ちょっとエモーショナルな話でした。(実はこういう研究を10年以上前にやっていたのです。それがここまで広がったのか、とつい嬉しくなり、勢いに任せて書いた次第です)