goaのdesignからもっといいJSコードを生成する

goa便利ですね。designからサーバーコードが生成できるし、Swaggerでドキュメントも生成できるし。

でもちょっと待って下さい。サーバーのコードが生成できたとしても、結局フロントエンドのコードは書かないといけないですよね。うーむ。と思っていたところ、goのgoaでAPIをデザインしよう(クライアント編)という記事を拝見しました。

$ goagen js -d github.com/tikasan/goa-simple-sample/design

でJSコードが生成される、という話です。おお、素晴らしい、と思って試したのですが、残念ながらあれ…という印象でした。

  1. 引数の渡し方が、Path Paramとdataの差がなくて、必要なだけ渡したい場合にちょっと使いにくい
  2. クライアントサイドValidationがない

という具合です。特にクライアントサイドでのValidationがないのがあかんですね。せっかくdesignでいろいろ制約書けるのに。

ということで作ってみました

goaにはコードを生成する部分をpluginとして自前のパッケージを指定できます。これを使ってJSコードを生成するパッケージを作成してみました。

https://github.com/shirou/goagen_js

goagen gen --pkg-path=github.com/shirou/goagen_js -d github.com/some/your/great/design

で、指定したdesignをjsディレクトリ以下に書き出します。

特徴としては、

  1. ES 2015準拠でfetch APIを使ったPromiseを返す。名前もResource名 + Action名と、分かりやすく
  2. クライアントサイドValidation
  3. flowtypeやTypeScript形式での生成も指定可能

それぞれ順番に説明します。例として、以下のようなデザインを使うとします。

var UserCreatePayload = Type("UserCreatePayload", func() {
        Attribute("name", String, "name", func() {
            MinLength(4)
            MaxLength(16)
        })
        Attribute("age", Integer, "age", func() {
            Minimum(20)
            Maximum(70)
        })
        Attribute("email", String, "email", func() {
            Pattern(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
        })
        Attribute("sex", String, "sex", func() {
            Enum("male", "female", "other")
        })
    Required("name")
})

var _ = Resource("user", func() {
    BasePath("user")
    Response(InternalServerError)

    Action("create", func() {
        Routing(POST("create/:Type"))
        Params(func() {
            Param("Type", String, "type of user", func() {
                Enum("normal", "admin")
            })
            Required("UserID")
        })
        Payload(UserCreatePayload, func() {
            Example(map[string]interface{}{
                "name": "fooboo",
            })
        })
        Response(OK)
    })

使い方

goagen genでコードを生成すると、以下のような関数を持つ、api_request.jsが生成されます。

// UserCreate
// type_(string): type of user
// payload(object): payload
export function UserCreate(type_, payload) {
  const url = urlPrefix + `/user/create/${type_}`;
  let e = undefined;
  e = v.validate(v.UserCreate.Type, type_);
  if (e) {
    return Promise.reject(e);
  }
  e = v.validate(v.UserCreate.payload, payload);
  if (e) {
    return Promise.reject(e);
  }
  return post(url, payload);  // 同時に生成されるヘルパー関数
}

ですので、以下のように使えます。

import * as api from "./api_request.js";

const payload = {age: 30, name: "shirou"}
api.UserCreate("admin", payload).then((response) => {
    ...
});

クライアントサイドValidation

先程の関数の中で、v.validate(v.UserCreate.Type, type_)というのがありました。これがValidationです。api_validator.jsというファイルに以下のコードが生成されています。

export const UserCreate = {
   "Type": {
     "kind": "string",
     "enum": [
       "normal",
       "admin"
     ]
   },
   "payload": {
     "age": {
       "kind": "number",
       "minimum": 20,
       "maximum": 70
     },
     "email": {
       "kind": "string",
       "pattern": "^[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,4}$"
     },
     "name": {
       "kind": "string",
       "min_length": 4,
       "max_length": 16
     },
     "sex": {
       "kind": "string",
       "enum": [
         "male",
         "female",
         "other"
       ]
     }
   }
 };

これを、同時に生成されているvalidateというヘルパー関数に食わせてあげます。返り値がundefinedであれば問題なし。問題があれば文字列が返ってきます。

リクエストとvalidationを分離しているのは、例えば入力のたびにon-the-flyでチェックする、という用途がフロントエンドではよくあるからです。

import * as v from "../api_validator.js";

// name だけチェックする
if v.validate(v.UserCreate.payload.name, value) !== undefined {
   // validationエラー
}

という感じで使えます。エラー時にどういう文言が返ってくるかはapi_validator.jsの中に定義されてるので、適宜使ってください。

これにより、フロントエンドとサーバーとで同じvalidationを二回書く、ということがなくなります。

flowtypeやTypeScript形式

goaはgolangで書いていますので、型情報があるわけです。しかし、JavaScriptにした瞬間にせっかくの型情報が失われてしまいます。あまりにももったいない。ということで、flowtypeやtypescriptで生成することもできるようにしました。

 goagen gen --pkg-path=github.com/shirou/goagen_js -d github.com/shirou/goagen_js/example/design -- --target flow

or

 goagen gen --pkg-path=github.com/shirou/goagen_js -d github.com/shirou/goagen_js/example/design -- --target type --genout ts

typescriptの場合、tsというディレクトリ名が普通かな、と思いますのでgenoutオプションで吐き出すディレクトリ名を指定しています。なお、拡張子もtsになります。

こうすると、

export function UserCreate(type_: string, payload: UserCreatePayload): Promise<any> {

という感じで型情報がくっつきます。UserCreatePayloadは同時に生成されるapi.d.tsファイルに以下のように定義されます。

interface UserCreatePayload {
  name: string;
  age: number;
  email: string;
  sex: ["male","female","other"];
}

本来はResponseの型情報も定義したいところですが、まだそこまで手が回っていません。

発表資料

ということをgoa勉強会で発表しました。

資料はGenerate better JavaScript From Goa Designです。

swaggerから生成したほうが良くね?

といったところで、ふと気が付きました。

goaはswaggerを吐き出せるんだから、swaggerからJSコード生成すればよくね?

調べてみると、swagger-codegenでJSどころかTypeScriptのコードも生成できるではないですか。

おおう。orz...

ということで、ここまで作ったところで実はちょっと継続して作成するモチベーションが下がってしまいました。codegenだとValidationがないので、Validationだけ別で生成するかもしれません。

まとめ

  • 標準のJSコード生成は正直微妙
  • goagen_js作ったYO
  • クライアントValidationやTypeScript生成もできちゃうー
  • あれ、swagger-codegenのほうがよくね…?
  • goaはいいぞ!

goaを書く時に知っておくとちょっと便利なこと

goaを書く時に知っておくと便利なことを記しておきます。

ちなみに文中のsnipとは省略のことです。

Defaultの値を指定したい

Defaultを指定します。ただし、goa 1.2でないと効かないので注意してください。

Attribute("lang", String, "言語", func() {
         Default("ja")
})

ちなみに、Defaultを指定するなら、指定なしを示すためのpointerにする必要はないだろう、という気もするのですが、普通にPointerになります。

ContentTypeを指定したい

goaは標準では"application/vnd.foo+json"などのように、MediaTypeで設定した文字列がContentTypeに使われます。これをapplication/jsonというようにしたい場合はContentTypeを明示的に指定します。

var Foo = MediaType("application/vnd.foo+json", func() {
        ContentType("application/json")
        Attributes(func() {
        })
    ...

しかし、CollectionOfを使っている場合だと、MediaTypeに指定しても効果がありません。そういう場合はこんな感じでResponseCollectionOfに指定します。

Response(OK, func() {
    Media(CollectionOf(Foo, func() {
        ContentType("application/json")
    }))
})

Attributeを簡単に指定したい

同じExampleやValidationを何度も何度も書くのは大変です。一箇所で指定してしまいましょう。

var FooDSL = func() {
        Description("FooFooFoo")
        Example(100)
        Minimum(10)
        Maximum(4200)
}

(snip)

Attributes(func() {
    Attribute("bar", Integer, FooDSL)
    Attribute("boo", Integer, FooDSL)
    Attribute("bee", Integer, FooDSL)
}

共通のHTTP Statusを定義したい

例えば、InternalServerErrorはすべてのActionで返す場合、個々のActionで指定するのではなく、Resource指定してしまえば、その配下のすべてのActionで指定する必要がなくなります。

var _ = Resource("foo", func() {
        BasePath("/:FooID")
        Response(InternalServerError)  // Here

        Action("index", func() {
             ( snip..... )
        })

        Action("search", func() {
             Routing(GET("/search"))
             ( snip..... )
        })

共通処理をくくり出したい

もっと汎用的な共通処理のくくり出し方として、Traitがあります。

まず最初にTraitで任意の名前を付けて定義します。ここではAttributeを指定するMedia用のTraitと、Responseを指定するAction用のTraitを二つ定義しています。

var _ = API("TraitTest", func() {
    Trait("MediaTrait", func() {
        Attributes(func() {
            Attribute("foo")
        })
    })
    Trait("ActionTrait", func() {
        Response(NotFound)
        Response(BadRequest)
    })
})

定義したTraitはUserTraitで使用します。

var TraitUser = MediaType("application/vnd.user+json", func() {
    UseTrait("MediaTrait")
    Attributes(func() {
        Attribute("id", Integer)
    })
    View("default", func() {
        Attribute("id")
        Attribute("foo")
    })
})

var _ = Resource("example", func() {
    Action("index", func() {
        Routing(GET("/"))
        UseTrait("ActionTrait")
        Response(OK)
    })
})

これにより、 TraitUserというMediaでは、fooが定義されていませんが使えるようになります。また、example ActionではNotFoundBadRequestを定義していませんが、使えるようになります。

ただし、実際にはMediaTypeの方では Viewを指定していないので指定する必要はあります。Trait側でViewを指定するとすでに定義済み、と言われてしまいます。

なお、UseTraitはUseTrait("Trait1", "Trait2")と複数指定可能です。

複数のViewを定義する

同じMediaだけれども、場合によって違う情報を返したい、というときにはdefault以外のViewを指定します。

例えば、以下は通常ユーザー用と管理者ユーザー用で異なる情報を返すようにしています。

var User = MediaType("application/vnd.user+json", func() {
    Attributes(func() {
        Attribute("id", Integer)
        Attribute("super_secret", Integer)
    })
    View("default", func() {
        Attribute("id")
    })
    View("admin", func() {
        Attribute("id")
        Attribute("super_secret")
    })
})

こう定義すると、以下のようにUserの他にUserAdminという型が定義されますので、Actionの実装中でどちらかを返せばよくなります。

// User media type (admin view)
//
// Identifier: application/vnd.user+json; view=admin
type UserAdmin struct {
        ID          *int `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"`
        SuperSecret *int `form:"super_secret,omitempty" json:"super_secret,omitempty" xml:"super_secret,omitempty"`
}

// User media type (default view)
//
// Identifier: application/vnd.user+json; view=default
type User struct {
        ID *int `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"`
}

返す時は、OKAdminと、それぞれのViewごとに専用のResponseを返す関数を使う必要がありますので、注意してください。

return ctx.OKAdmin(&app.UserAdmin{ID: 10, SuperSecret: 10})

Viewを再利用

せっかく作ったViewですので、さらに使いましょう。

var Foo = MediaType("application/vnd.foo+json", func() {
    Attributes(func() {
        Attribute("id", Integer)
        Attribute("user", User)  // UserはAttributeとして指定する
        Link("user", "admin")  // UserのAdminViewを定義
    })
    View("default", func() {
        Attribute("id")
        Attribute("links") // "links"という名前で呼び出す
    })
})

こうしておくと、Foo MediaTypeにLinksというフィールドが作成され、その中にUserAdminへのリンクが保持されます。

// Foo media type (default view)
//
// Identifier: application/vnd.foo+json; view=default
type Foo struct {
        ID *int `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"`
        // Links to related resources
        Links *FooLinks `form:"links,omitempty" json:"links,omitempty" xml:"links,omitempty"`
}

// FooLinks contains links to related resources of Foo.
type FooLinks struct {
        User *UserAdmin `form:"user,omitempty" json:"user,omitempty" xml:"user,omitempty"`
}

この例では一個だけですが、Linksを使うことで

Links(func() {
    Link("user") // 指定なしだと `link` Viewを使う
    Link("bar")
})

と、複数のMedia TypeからViewを集めることもできます。

一旦FooLinksという型を通さないといけないのは、このLinksがあるせいかな、という気がします。

APIKeyによる認証制御を実装したい

APIKeySecurityを定義しResourceでSecurityを定義することで、HTTPヘッダによる認証を簡単に実装できます。この例ではX-Shared-Secretヘッダでの認証となります。/secureはヘッダが必要で、/unsecureNoSecurity()を指定しているのでヘッダが必要ありません。

var APIKey = APIKeySecurity("api_key", func() {
    Header("X-Shared-Secret")
})
var _ = Resource("secure_resource", func() {
    Security(APIKey)
    Action("secure", func() {
        Routing(GET("/secure"))
        Response(OK)
    })
    Action("unsecure", func() {
        Routing(GET("/unsecure"))
        NoSecurity()
        Response(OK)
    })
})

こうしておいて、以下のようなミドルウェアを実装します。SomethingSecureCheckerはDB引いたりなんなりと、ご自身で実装してください。

// Error definition
var ErrUnauthorized = goa.NewErrorClass("unauthorized", 401)

func NewAPIKeyMiddleware() goa.Middleware {
    scheme := app.NewAPIKeySecurity() // 使用するHeaderなどを得る
    return func(h goa.Handler) goa.Handler {
        return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
            key := req.Header.Get(scheme.Name) // 最初に得たschemeからkeyを得る

            if SomethingSecureChecker(key) { // なにか
                return ErrUnauthorized("auth failed") // 認証失敗
            }
            // 合格
            return h(ctx, rw, req)
        }
    }
}

最後はmainでこのMiddlewareを使うように指定します。

app.UseAPIKeyMiddleware(service, NewAPIKeyMiddleware())

こんな感じで、簡単にAPI ヘッダでの認証を実装できます。同じような感じで

  • OAuth2
  • JWT
  • BasicAuth

による認証も実装できます。

まとめ

goaはDSLなので、こういう細々としたtipsを知っておく必要があります。そのため、最初敷居が高く、また、使い回しが効かない、という点はあります。

ただ、DSLといってもGo言語の形式(内部DSL)なので、gofmtも効きますし、補完も効きます。goのライブラリと組み合わせられるのはとても便利です。また、どんなDSLにも言えることですが、慣れると特になにも考えずに書けるようになります。

ということで、goaを書く時のちょっとしたtipsでした。

#kaneの話

(この記事はpyspaアドベントカレンダー2016の14日目です)

ファイナンシャルプランナー3級のr_rudiです。3級程度じゃ意味ないので、来年はとりあえず2級を取ろうと思ってます。

今回は、普段のこのブログとは大きく異なり、ぼくの投資スタンスの話を書きます。といっても、特に面白みはないかもしれません。

経験

株を始めとする投資を始めたのは、ちょうど10年、2006年です。それから2007年のサブプライムローン、2008年のリーマンショックなどを経験してきました。あのときのお金がどんどん減る恐怖、自動損切りが発動して損が確定したときの喪失感と安堵感をはっきりと覚えています。

そんなに多くのお金を入れていたわけではないし、いい経験をしたなということで、そこから投資をむしろ増やしていきました。

0. 原則

投資をしてお金を増やすためには、時間が必要です。しかし、技術者ならば、その時間を勉強や仕事に振り分けて年収をあげたほうが断然効率が良いです。これは大原則として最初から思っています。ですので、投資そのものにはなるべく時間をかけないようにしていますし、値動きが気になって仕事が手に付かないことでは本末転倒ですので、気にしないような仕組みにしています。あくまで仕事による給与収入が一番大事であり、投資による収入はおまけです。

もともと投資でお金を増やすことそのものにはそこまで熱心ではなく、むしろ投資を通じて世界経済の動きを知ることのほうが楽しいので、今まで続けてきました。つまり、趣味です。他の人が趣味にお金と時間を費やすのと同じことだと思っています。

1. 個人拠出型年金

元々大企業にいたので、企業型年金に入っていました。その後転職して個人拠出型年金に切り替え、そのままずっと続けています。個人拠出型年金は単に将来退職時の年金に使えるということだけではなく、拠出した金額分税金の軽減ができるので、非常にお得です。特に保育園等、払った税金に応じて金額が変わるようなことがある場合は、結構な金額が変わる倍があります。

配分ですが、

  • 先進国株式 70%
  • 先進国債券 20%
  • 新興国株式 10%

ぐらいの割合で振り分けています。年に2,3回ぐらい見直して割合を変えていますが、先進国株式重視なのは変えていません。

2. 投資信託

先進国株式インデックスの積立型投資信託をやっています。ノーロードで信託報酬が低いもの、配当は年一回のものを選んでいます。

積立型投資は決まった額の投資信託を毎月買う方式です。金額固定の方式(ドルコスト平均法)は

  • 相場が下がった場合は多く株を買える
  • 相場が上がった場合はその分儲けられる

です。10年単位の超長期、かつ、サイクル理論が成り立つ限りは、どちらに転んでも最終的には利益はでます。

投資信託のメリットは自動的に買ってくれるという点につきます。残高をたまにチェックするだけであとは自動的に買ってくれるので、時間をかけなくてすみます

投資信託も年に2回ぐらい見直して、切り替えたり、売ったりしています。

../../../_images/sbi-total-return.png

SBIでは2009年以降のトータルリターンが表示できます

海外証券会社

ETFの自動積立があると良いのですが、使っている証券会社にはないのです。firstradeなどの海外証券会社ではできるものもあるので、そろそろ海外証券会社を開こうかなぁ、とも思っています。

3. 国内株式

国内株式に対しては悲観的なので優先度はかなり低めです。むしろETFがメインです。ETFもインデックスだけじゃなくて例えば原油ETFを原油価格が30円の時に買ったりしました。NISAも使っています。

「原則」で示したように選んでいる時間がもったいないため、個別株はほとんどやっていません。例外は優待で、以下のような企業を優待目当てでずっと保有しています。

  • イオン (8267)
    • キャッシュバックとお客様感謝デーの5%OFF
  • マックスバリュー (8198など)
    • 1000円ごとに使える、100円割引券。イオンのキャッシュバックと併用可能
    • マックスバリューは東海などいろいろあり、それぞれ100株でも優待がもらえるので良い
  • ビックカメラ (3048)
    • ネットでも使えます
    • 長く持ってると追加でもらえる
  • ゼンショー (7550)
    • すき家やはま寿司で使える
    • ワンオペ問題で売ろうかと思ったが、改善の姿勢があったので保持中
  • キャンドゥ (2698)
    • 100円ショップ
  • キリンHD (2503)
    • ビール

優待は投資家にとってあまり良くないものです。本来であれば企業業績を伸ばして配当で貰うほうが良いです。しかし、優待が届くとそれをきっかけにして購買行動に動くということが(出不精としては)個人的にはメリットなので、保持しています。

なお、情報通信系はいつどこでインサイダーでひっかかるか分からないため、一切投資していません。

4. 外国為替証拠金取引(FX)

ぼくの投資のメインはこのFXです。FXは一般的に危ないと思われていますが、ストップを置くだけで危険はかなり少なくなります。むしろ、以下の点でぼくの投資スタンスに合っていると思います。

  • 24時間動くので、仕事以外の時間で見やすい
  • 流動性が高いので、ストップを超えることはめったにない。株だと売るに売れないという状況が起こり得る
  • 通貨ペアはわずかしかないので、銘柄探しに費やす時間がない
  • 参加者が膨大のため、株のように仕手勢が入る余地が少ない(0ではない)
  • 値動きと世界の経済状況とが直結している

特に最後の点が重要で、「なんでこういう値動きになったんだろう」というのを調べることはとても面白いです。

ポジションを持っていると気になるので、基本的に順張りで、「ファンダメンタルズもチャートもこういう流れなので、絶対こう行く」という時しかポジションを作りません。そんなにいい機会は年に数回程度しかないので、ポジションをもっていない時時間帯は多いです。その間にがっつりと動くことは多々ありますが、「あの時買っておけばよかった」とかは気にしないことにしています。それでもトレンドをうまく捉えられればそこそこの利益を出せます。おかげで最初の投資金額はすべて回収し、出金していますので仮に全額なくなったとしてもトータルでは損はありません。

今年は円高になると予想してたので年初から売りポジションを持ってたりしたのですが、100円近辺まで下がるとは思っておらず利確が早すぎたのと、あとから追加で売ったら完全に予想外だったトランプ相場でやられてしまったのでちょっとプラスぐらいで終わりです。

自動売買

MT4というツールで自動売買をしようと思っていた時期もありました。しかし、単なる条件判定だと書いててあんまりおもしろくなく、これだったら普通に仕事してるほうがいいな、と思ってやめてしまいました。

今は、機械学習での自動売買ならば新しい技術の習得も兼ねられるのでおもしろそうだなぁ、と思っているところです。と言ってもまだなにもしていませんが。

まとめ

今回はいつもの技術系記事とは異なり、現在の投資スタンスについて説明してきました。かなり安全側に倒した何の変哲もない話ですが、誰かのなにかの参考になれば。と思います。基本はコツコツ麺です。


最近気になっているのはヨーロッパとアメリカにおける保護主義の動きです。これはこの50年の動きを否定する動きです。つまり、たかが10年程度の蓄積では対応できないレベルで世界経済が激変する可能性があります。

とはいえ、一個人にできることは、常日頃の情報収集とその時々の対応しかありません。これからも引き続き投資はしていこうと思っています。

なんせ趣味ですから。

go-swaggerを使う

この記事はGo (その2) Advent Calendar 2016の7日目の記事です。

APIからのコード自動生成ツールとして、swaggerが最近流行ってます(異論は認めます)。

この分野では、goaが有名ですが、goaはあくまでGoのDSLから生成するという方式です。swagger定義がすでにある場合には使えません。

swagger定義ありきの場合、今回説明するgo-swaggerを使うと便利です。

なお、この内容はhttps://github.com/shirou/swagger_sampleにて公開しています。

go-swaggerとは

go-swaggerは、swagger定義からgoのサーバーおよびクライアントを生成してくれるツールです。

インストールはgo getでインストールしてもよいのですが、goの環境がない場合は

などが用意されています。

go-swaggerの使い方 -- サーバー編

例として、以下のようなswagger.ymlで定義したとしましょう。

produces:
  - application/json
paths:
  '/search':
    get:
      summary: get user information
      consumes:
        - application/x-www-form-urlencoded
      tags:
        - user
      parameters:
        - name: user_id
          type: integer
          in: formData
          description: user id
      responses:
        200:
          schema:
            $ref: '#/definitions/User'
        default:
          schema:
            $ref: '#/definitions/Error'
definitions:
  User:
    type: object
    required:
      - user_id
    properties:
      user_id:
        type: integer
        description: user id
      name:
        type: string
        description: name
  Error:
    type: object
    properties:
      message:
        type: string

サーバーサイドのコードを生成するには以下のコマンドを実行します。

$ swagger generate server -f swagger.yml -A swaggertest

これで、

  • cmd
  • models
  • restapi

というディレクトリが作成され、その中に必要なソースコードが生成されます。具体的にはこんな感じです。

restapi/
|-- configure_swaggertest.go
|-- doc.go
|-- embedded_spec.go
|-- operations
|    |-- swaggertest_api.go
|    `-- user
|         |-- get_search.go
|         |-- get_search_parameters.go
|         |-- get_search_responses.go
|         `-- get_search_urlbuilder.go
`-- server.go

-A swaggertestはアプリ名です。指定すると、いろいろなところで使われるようになります。

operationsの下のuserはtagから取ってきています。複数のtagを設定していると複数のディレクトリに同じ内容が生成されてしまうので気をつけてください。

以降は、後ほど説明するconfigure_swaggertest.go以外のファイルは触りません。

Handlerの実装

生成されたファイルには触らず、パッケージ配下に別のファイルを作成しましょう。restapi/operations/user/search.goというファイルを作成し、以下のようなコードを実装します。

package user

import (
    middleware "github.com/go-openapi/runtime/middleware"
    "github.com/go-openapi/swag"

    "github.com/shirou/swagger_sample/models"
)

func Search(params GetSearchParams) middleware.Responder {
    payload := &models.User{
            UserID: params.UserID,
            Name:    fmt.Sprintf("name_%d", swag.Int64Value(params.UserID)),
    }

    return NewGetSearchOK().WithPayload(payload)
}

paramsが渡ってくるパラメータです。swagger定義に従って事前にvalidateされていますので、安心して使えます。

NewGetSearchOKはswagger定義での200の場合に返す構造体を作成します。payloadとして、Userを定義して渡しておきます。

swagパッケージはポインタと実体とを変換したり、pathを検索したりといった便利関数を提供している便利ライブラリです。goの言語上の制限として、値なしとデフォルト値を区別するためにポインタにする必要があり、そのためにswagでの変換を使う必要があります。

Handlerの登録

restapi/configure_swaggertest.goに今実装したHandlerを付け加えます。 最初はmiddleware.NotImplementedが書かれているので、先程実装したHandlerに置き換えます。

api.UserGetSearchHandler = user.GetSearchHandlerFunc(user.Search)

configure.goは一度生成するとその後自動では変更されないので、swagger.ymlを更新して、Handlerを追加したりするときには注意が必要です。ださいですが、一旦renameして、再度生成されたファイルからコピーしたりしています。

なお、configure.goにはsetupのコードも含まれているので、必要に応じて事前設定を追加したり、ミドルウェアを追加したりするとよいでしょう。

実行

これで準備は整いました。cmd/swaggertest-server以下でbuildしましょう。

$ cd cmd/swaggertest-server && go build -o ../../server

$ ./server -port=8000

デフォルトではランダムなportで起動するので、port引数で指定しています。

あとは普通にアクセスできるようになります。

$ curl "http://localhost:8080/search?user_id=10"
{"name":"name_10","user_id":10}

簡単ですね。

生Request

paramsの中にはhttp.Requestがありますので、Requestそのものを得たい場合はここから得られます。contextもここから得ればよいかと思います。

go-swaggerの使い方: クライアント編

go-swaggerはサーバーだけではなく、以下のコマンドでクライアントを生成出来ます。

$ swagger generate client -f swagger.yml -A swaggertest

あとは以下のようなコードを書けば、clientとして実行できます。

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/go-openapi/swag"

    apiclient "github.com/shirou/swagger_sample/client"
    "github.com/shirou/swagger_sample/client/user"
)

func main() {

    // make the request to get all items
    p := &user.GetSearchParams{
            UserID: swag.Int64(10),
    }

    resp, err := apiclient.Default.User.GetSearch(p.WithTimeout(10 * time.Second))
    if err != nil {
            log.Fatal(err)
    }
    fmt.Println(resp.Payload.Name)
}

この例ではパラメータ固定で叩くだけですが、引数とかをちゃんと設定していけば、CLIツールが簡単に実装できます。

model

swaggerではmodelを定義して、それを返答に含めたり出来ます。definitions以下ですね。

今回使っている例のUserモデルはmodels/user.goで以下のように生成されます。

// User user
// swagger:model User
type User struct {
     // name
     Name string `json:"name,omitempty"`

     // user id
     // Required: true
     UserID *int64 `json:"user_id"`
}

models/user.go そのものは再生成すると書きつぶされてしまうので、models以下に user_hoge.go などを作ってそちらでいろいろと関数を追加していくと良いと思います。

テスト

サーバー実装のテストはHandlerに対して指定されたパラメータを渡してあげれば良いです。ただし、http.Requestを入れる必要があるので、そのときにはhttptest.NewRecorder()を使って返り値を記録するようにします。あとは、各Handlerは単なる関数なので、その関数を実行するだけでよくて、httptest.Serverを立ち上げる必要はありません。

func TestSearch(t *testing.T) {
    req, err := http.NewRequest("GET", "", nil)
    if err != nil {
            t.Fatal(err)
    }

    params := GetSearchParams{
            HTTPRequest: req,
            UserID:      swag.Int64(10),
    }
    r := Search(params)
    w := httptest.NewRecorder()
    r.WriteResponse(w, runtime.JSONProducer())
    if w.Code != 200 {
            t.Error("status code")
    }
    var a models.User
    err = json.Unmarshal(w.Body.Bytes(), &a)
    if err != nil {
            t.Error("unmarshal")
    }
    if swag.Int64Value(a.UserID) != 10 {
            t.Error("wrong user id")
    }
}

まとめ

go-swaggerを使ってswagger定義ファイルからサーバーとクライアントのコードを生成する例を示しました。

swaggerはswagger uiがcurlのコマンドが出てくるなどかなり使いやすく、定義ファイルも慣れれば難しくありません(ファイル分割が出来ないという問題はありますが)。

クライアントとサーバーとで別れて開発しているときなど、swaggerとswagger uiを使ってプロトコル定義を確認しつつ開発を進めていくと、理解の不整合が起きにくくなります。

Goaから生成するのも良いですが、swagger定義ファイルから生成する場合はgo-swaggerも検討してみると良いのではないでしょうか。

eawsyのaws-lambda-goを使ってみる

AWS lambdaは昨今人気ですね。しかし、実行環境がpython、java、nodeだけです。ぼくはpythonも結構やってきましたが、近頃ずっとgoを使っており、goで実行できると良いなと思っていました。

ということで、昨日eawsy/aws-lambda-goというgoでlambdaを記述できるライブラリを知ったので、試してみました。 (以下eawsyと呼びます)

apexとの比較

lambdaをgoで書きたいなと言う気持ちは過去にはAWS Lambdaで効率的にgoバイナリを実行するという記事を書いてました。このときは、lambda_procというライブラリを使っていたのですが、その後いろいろでてきて、最近だとapexが有名です。

では、eawsyとapexとの違いはなんでしょうか。eawsyはPythonのC拡張を用いて実行します。

  • apex
    • lambdaはruntimeとしてnodeを呼び出し、さらにnodeがgoのバイナリをspawnで呼び出す。
    • 実行するのは普通のバイナリ。Linuxでそのまま実行できる
  • eawsy
    • goを-buildmode=c-sharedでshared libraryとしてbuild
    • lambdaはpythonを実行。pythonはgoをC拡張として読み込んで実行

つまり、apexでは二回のプロセス生成が入るのに比べて、eawsyでは一回だけとなります。その代わり、buildにcgo環境が必要になります。しかし、eawsyはDockerコンテナを提供していますので、cgo環境を用意する必要はありません。

使ってみる

maing.goはこんな感じで書きます。

package main

import (
      "encoding/json"
      "log"
      "time"

      "github.com/eawsy/aws-lambda-go/service/lambda/runtime"
)

func handle(evt json.RawMessage, ctx *runtime.Context) (interface{}, error) {
      ret := make(map[string]string)
      ret["now"] = time.Now().UTC().Format(time.RFC3339)

      return ret, nil
}

func init() {
      runtime.HandleFunc(handle)
}

func main() {}

mainは空で、initでhandleを登録します。json.RawMessageにはパラメータが入って渡ってきます。

retはinterface{}なので任意の型を返せます。これがJSONになって返答されます。

ベンチマーク

apexでほぼおなじコードを書いてみました。

package main

import (
     "encoding/json"
     "time"
     apex "github.com/apex/go-apex"
)

type message struct {
   Time string `json:"time"`
}

func main() {
     apex.HandleFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) {
             var m message
             if err := json.Unmarshal(event, &m); err != nil {
                     return nil, err
             }
             m.Time = time.Now().Format(time.RFC3339)
             return m, nil
     })
}

以下のように直接実行して、CloudWatchのログで実行時間を見てみます。

eawsy
$ aws lambda invoke --function-name preview-go output.txt
apex
$ apex invoke hello
ベンチマーク
回数 eawsy apex
一回目 16.38 ms 41.11 ms
二回目 0.48 ms 1.21 ms
三回目 0.50 ms 0.64 ms

lambdaは一回目はコンテナ(かどうか不明ですが)を起動するので時間がかかります。二回目以降は起動済みなので早いです。ということで大事な一回目なのですが、apexが40msecに比べてeawsyが16msecと、約半分になっています。めんどいので一回だけの結果しか載せませんが、数回やって基本的な傾向は同じでした。

一度起動が完了したら、1msec以下になり、これは両者とも同じです。



しかし、ぶっちゃけ、40msecが16msecになったところで一回だけです。これが重要なワークロードもあるでしょうが、あんまり意味があるとは思えません。そもそもlambdaの実行時間が不安定なので、数msec速くなったところで、それがどうしたよ、という感じです。

eawsyの利点はベンチマークではなく、ログ出力やruntimeの関数を呼べることです。(ということを、redditのFinally vanilla Go on AWS Lambda (no serverless!) startup <5msで作者が述べています。)

eawsyの利点

ログ出力

apexはnodeのruntimeを通している関係上、ログ出力はstdoutに出したものだけとなります。そのため、一手間必要になってしまっていました。それに対して、eawsyは標準のlogパッケージが使えます。

log.Printf("Log stream name: %s", ctx.LogStreamName)
log.Printf("Log group name: %s", ctx.LogGroupName)
log.Printf("Request ID: %s", ctx.AWSRequestID)
log.Printf("Mem. limits(MB): %d", ctx.MemoryLimitInMB)
log.Printf("RemainingTime: %d", ctx.RemainingTimeInMillis)

このように普通のlog出力と同じようにしておくと、CloudWatchのログに以下のように出ます。

13:19:55 START RequestId: 9bf7d852-a0b3-11e6-b64b-7dec169bb683 Version: $LATEST
13:19:55 2016-11-02T04:19:55.919Z     9bf7d852-a0b3-11e6-b64b-7dec169bb683    Log stream name: 2016/11/02/[$LATEST]1e58f3ef77894283988110ea452dc931
13:19:55 2016-11-02T04:19:55.919Z     9bf7d852-a0b3-11e6-b64b-7dec169bb683    Log group name: /aws/lambda/preview-go
13:19:55 2016-11-02T04:19:55.919Z     9bf7d852-a0b3-11e6-b64b-7dec169bb683    Request ID: 9bf7d852-a0b3-11e6-b64b-7dec169bb683
13:19:55 2016-11-02T04:19:55.919Z     9bf7d852-a0b3-11e6-b64b-7dec169bb683    Mem. limits(MB): 128
13:19:55 END RequestId: 9bf7d852-a0b3-11e6-b64b-7dec169bb683
13:19:55 REPORT RequestId: 9bf7d852-a0b3-11e6-b64b-7dec169bb683
Duration: 16.38 ms
Billed Duration: 100 ms Memory Size: 128 MB   Max Memory Used: 8 MB

Fatalfだとエラーに、Panicだとstacktraceも表示されます。

エラー処理

handleの返り値にerrorを入れると

return ret, fmt.Errorf("Oops")

以下のようなログがCloudWatchに書き出されます。

Oops: error
Traceback (most recent call last):
File "/var/runtime/awslambda/bootstrap.py", line 204, in handle_event_request
result = request_handler(json_input, context)
error: Oops

runtimeの関数を呼べる

apexのアプローチでは、あくまで実行はgoのため、nodeに提供されている情報を得ることは出来ませんでした。しかし、 eawsyではruntimeに提供されている情報をgoから得ることが出来ます。

上のログ出力の例にctx.RemainingTimeInMillisという残り時間を得る箇所があります。これがpython runtimeに提供されている情報を利用できている証拠です。

まとめ

PythonからC拡張経由でgoを呼び出すというアプローチが面白かったので使ってみました。

といっても、ベンチマーク的には(元から速いぶん)決定的な違いではなく、runtimeの関数を呼べたり標準logパッケージが使えるのはちょっとうれしいかもしれませんが、プログラミングモデルとしてもそこまで大きな差はありません。

apexはgolangに限らないことと、apex deployというように管理ツールとしても優れていることから、現時点ではapexの方に軍配が上がると思います。

おまけ

proxy.cがruntimeの実体ですね。handleでgoを呼んでます。

普通にC拡張なので、rustやC++などでも同じアプローチが出来ます。rustで書いてみるのも面白いかもしれません。

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のコードを改変しないかぎりその必要はない、という例外条項です。ライセンスを確認していただけると分かるように、商用プログラムに対して適用できると明確に書かれています。