etcd入門 (1)

Kubernetesにも採用されている分散型KVSについて,何回かに分けて勉強していく.
今回のモチベーションとして,Kubernetesのアーキテクチャを詳しく勉強したい,Terraformのstate保存をローカルではなくetcdにしたいという2つがある.

まず,インストールとクラスタの作成について見ていく.

インストール

クラスタ作成のためには,まずetcd本体のバイナリが必要になる.
GitHubのreleaseページから,自分のOSにあったバイナリをダウンロードしてくる.

$ curl -LO https://github.com/etcd-io/etcd/releases/download/v3.3.10/etcd-v3.3.10-linux-amd64.tar.gz

ダウンロードしたファイルは圧縮されており,それを解凍するといくつかのファイルの中に2つのバイナリが確認できる.

$ tar xzf etcd-v3.3.10-linux-amd64.tar.gz
$ cd etcd-v3.3.10-linux-amd64.tar.gz
$ ll etcd*
-rwxr-xr-x. 1 yamazaki yamazaki 19237536 Oct 11  2018 etcd
-rwxr-xr-x. 1 yamazaki yamazaki 15817472 Oct 11  2018 etcdctl

etcd はetcd本体, etcdctl はetcdのクライアントとなるプログラム.
これら2つをPATHが通っている場所に置いてあげる.

$ sudo mv etcd* /usr/local/bin/
$ ll /usr/local/bin/etcd*
-rwxr-xr-x. 1 yamazaki yamazaki 19237536 Oct 11  2018 /usr/local/bin/etcd
-rwxr-xr-x. 1 yamazaki yamazaki 15817472 Oct 11  2018 /usr/local/bin/etcdctl

これでインストールは完了.
きちんとインストールされているか一応確認する.

$ etcd --version
etcd Version: 3.3.10
Git SHA: 27fc7e2
Go Version: go1.10.4
Go OS/Arch: linux/amd64

クラスタの作成

まず,1台のみでetcdクラスタを作成して簡単に使い方を把握し,その後複数メンバでのetcdクラスタを作成していく.

1台のetcdクラスタ作成

基本的にGitHubに書いてある手順通りに試していく.

1台のみでetcdクラスタを作成するときは,特に何も考えずに etcd コマンドを実行する.

$ etcd

etcd コマンドを実行すると,多くのログが出力される.
ここで,作成したetcdクラスタを使って,データの登録と取得を試してみる.

etcdクラスタとのやり取りには, etcdctl コマンドを使用する.
etcd コマンドを実行しているシェルとは別にもう1つシェルを起動し,そこで etcdctl コマンドを使用していく.
なお,etcd APIにはバージョンがいくつかあるが,今回はバージョン3を使用する.
etcdctl を普通に使うとv2 APIが使われてしまうので,v3 APIを使うために ETCDCTL_API という環境変数の値を 3 に設定する必要があることに注意する.
公式のREADMEのように etcdctl コマンドを実行するごとに毎回 ETCDCTL_API を指定してもよいが,毎回記述するのも面倒なのであらかじめexportしておく.

$ export ETCDCTL_API=3

etcdクラスタにデータを登録するには,putコマンドを使用する.

# Usage
$ etcdctl put <key> <value>

$ etcdctl put mykey "this is awesome"
OK

putコマンドを実行して, OK と表示されれば成功している.

登録した値を取得するにはgetコマンドを使用する.

# Usage
$ etcdctl get <key>

$ etcdctl get mykey
mykey
this is awesome

指定したキーの名前とデータが続けて出力されれば成功.

次に,複数台のetcdクラスタを作成してみる.
その前に,今使っていたetcdクラスタを停止させておく.
etcd コマンドを実行していたシェルに戻り,Ctrl-Cで終了する.

複数メンバでのetcdクラスタ作成

今回は1つのノードの中にetcdを複数立ち上げることで,複数メンバで構成されるetcdクラスタを作成する.
もちろん,複数のノードを使ってノード1台につきetcdを1つ動作させるという形で作成することもできる.

公式には,etcdはTCP 2379番・2380番のポートを使用する.

  • 2379/tcp: クライアントとの通信
  • 2380/tcp: etcdメンバ間の通信

今回は同じノード内に複数etcdを立ち上げるため,それぞれのetcdでポートが競合しないようにしておく.
今回は3つのetcdメンバを動作させ,それぞれの名前,ポートは次の表のようにした.

メンバ クライアント通信用 メンバ間通信用
etcd1 2379 2380
etcd2 12379 12380
etcd3 22379 22380

実際に3つのetcdメンバを動かしていく.
ターミナルのウィンドウを3つ開いて,それぞれのターミナルにつき1つのetcdメンバを起動する.
それぞれのターミナルで,次のようなコマンドを実行する.

1つ目のターミナル

$ etcd --name etcd1 \
    --initial-advertise-peer-urls http://localhost:2380 \
    --listen-peer-urls http://localhost:2380 \
    --advertise-client-urls http://localhost:2379 \
    --listen-client-urls http://localhost:2379 \
    --initial-cluster etcd1=http://localhost:2380,etcd2=http://localhost:12380,etcd3=http://localhost:22380 \
    --initial-cluster-state new \
    --initial-cluster-token etcd-cluster-1

2つ目のターミナル

$ etcd --name etcd2 \
    --initial-advertise-peer-urls http://localhost:12380 \
    --listen-peer-urls http://localhost:12380 \
    --advertise-client-urls http://localhost:12379 \
    --listen-client-urls http://localhost:12379 \
    --initial-cluster etcd1=http://localhost:2380,etcd2=http://localhost:12380,etcd3=http://localhost:22380 \
    --initial-cluster-state new \
    --initial-cluster-token etcd-cluster-1

3つ目のターミナル

$ etcd --name etcd3 \
    --initial-advertise-peer-urls http://localhost:22380 \
    --listen-peer-urls http://localhost:22380 \
    --advertise-client-urls http://localhost:22379 \
    --listen-client-urls http://localhost:22379 \
    --initial-cluster etcd=http://localhost:2380,etcd2=http://localhost:12380,etcd3=http://localhost:22380 \
    --initial-cluster-state new \
    --initial-cluster-token etcd-cluster-1

正しくコマンドを入力できていれば,正常にetcdクラスタが起動しているはず.
出力されるログでエラー等が出ていなければひとまずOK.

コマンドラインオプションをたくさん使っているので複雑に見えるが,一度理解してしまうとそこまで難しくはないと思う.
今回指定しているコマンドラインオプションは,大きく分けて次の2つに分かれている.

  • 新しいクラスタを作成するときに使用するもの(Clustering flags
  • 他のメンバやクライアントとの通信や,etcdの設定に関するもの(Member flags

Clustering flagsでは,新しいクラスタを一から作成するときに必要となる情報を指定する.
今回使用したものは次の通り.

flags description
--initial-advertise-peer-urls クラスタ内の他のetcdメンバからの通信を受け付けるURLを指定する
--advertise-client-urls クライアントからの通信を受け付けるURLを指定する
--initial-cluster クラスタを構成するetcdメンバの情報をカンマ区切りで指定する
--initial-cluster-state 新しいクラスタを作成する場合は new を指定する
--initial-cluster-token クラスタ作成中に用いられるトークン.複数クラスタを管理している際,意図せず別のクラスタに影響を与えるのを防ぐ

Member flagsでは,作成するメンバに関する情報等の設定ができる.
今回使用したものは次の通り.

flags description
--name メンバ名
--listen-peer-urls クラスタ内の他のetcdメンバからの通信を受け付けるURLを指定する
--listen-client-urls クライアントからの通信を受け付けるURLを指定する

注意点として, --initial-cluster で指定しているメンバ情報は, --name--listen-peer-urls で指定した名前とURLに一致させなければならない.
もし一致していないと,クラスタ起動時にエラーとなる.

次のコマンドで,きちんと3つのメンバが表示されたら成功.

$ etcdctl member list
29727fd1bdf9fb62, started, etcd1, http://localhost:2380, http://localhost:2379
44dd3cd8faa339d0, started, etcd3, http://localhost:22380, http://localhost:22379
b59b01c27098773e, started, etcd2, http://localhost:12380, http://localhost:12379

次は,作成したクラスタを使ってもう少しetcdctlの使い方を勉強し,その後TLSの設定やDiscoveryを使ったクラスタ作成,無停止でのメンバアップグレード等について勉強していく予定.

GoのType AssertionとType Switches

Goでは,型を interface{} として宣言してあげることで,とりあえずどんな値でも格納することが出来る.
SlackのEvent API等,メッセージの形式がEventごとに異なるといった場合に使うと便利.

Example

func eventHandler(w http.ResponseWriter, r *http.Request) {
    var event map[string]interface{}
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&event); err != nil {
        log.Fatalln(err)
    }
}

この例だと,変数 event に受け取ったイベントが格納される.
eventmap[string]interface{} として宣言されているので, event["token"] のように値を取得しても返ってくるのは interface{} 型である.
そのため,値を使いたいときに適切な型へ変換してあげる必要がある.

このような場合に,Goの言語仕様として用意されているType AssertionやType Switchesというものを使ってあげるとうまく型変換が出来る.

Type assertions

Type assertionsを使うと, interface{} から指定した型に変換することが出来る.
Type assertionsでは,次のような順番で処理が行われる.

  1. 値がnilであるかどうかを検査
  2. 指定した型に変換して値を返却

書き方は次の2つある.

1. 基本形

var x interface{} = 7   // interface{}としてxを宣言
i := x.(int)            // Type assertionsを使ってint型に変換

Type assertionsが成功すると,指定した型に変換された値が返り値として返される(上の例だと int に変換された 7 が返ってくる).
もし次の例のように正しく変換できなかった場合は,panicが発生する.

var x interface{} = "hello" // interface{}としてxを宣言
i := x.(int)                // xの値 "hello" はint型に変換できない => panic発生

2. 別の書き方

別の書き方として,次のように返り値を2つ受け取るバージョンがある.

var x interface{} = 7   // interface{}としてxを宣言
i, ok := x.(int)        // okには正しく変換できたかどうかがboolで格納される.成功した場合はtrue

この書き方を使うと,失敗したときでもpanicは発生せず, okfalse が設定されるだけとなる.
この時,1つめの返り値は指定した型のゼロ値となる.

var x interface{} = "hello" // interface{}としてxを宣言
i, ok := x.(int)            // xの値 "hello" はint型に変換できない => okはfalse
fmt.Println(i)              // 0(intのゼロ値)が出力される

Type Switches

ドキュメントでは,Type SwitchesはType assertionの特別形であると説明されている.
Type assertionでは変換したい型名を指定していたが,Type Switchesでは代わりに type を指定する.

var x interface{} = "hello"
switch i := x.(type) {
    case int:
        fmt.Println("x is int")
    case string:
        fmt.Println("x is string")
}

これを使うと,複数の型候補がある場合にswitch文を使って分岐させることが出来る.

参考文献