Dockerイメージのgolang:1-alpineはgo getできない

とにかくコンテナのベースイメージは軽い方が良いんでしょ?みたいなノリでalpineベースのイメージを使おうとして失敗した。

gitがないからgo getできない。

% docker run --rm -it golang:1-alpine /bin/sh
/go # go get -u github.com/golang/dep/cmd/dep
go: missing Git command. See https://golang.org/s/gogetcmd
package github.com/golang/dep: exec: "git": executable file not found in $PATH
/go # which git
/go #

gitをインストールして、再度go getする。

/go # apk add --no-cache git
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/community/x86_64/APKINDEX.tar.gz
(1/5) Installing libssh2 (1.8.0-r2)
(2/5) Installing libcurl (7.60.0-r0)
(3/5) Installing expat (2.2.5-r0)
(4/5) Installing pcre2 (10.30-r0)
(5/5) Installing git (2.15.0-r1)
Executing busybox-1.27.2-r7.trigger
OK: 19 MiB in 17 packages
/go # which git
/usr/bin/git
/go # go get -u github.com/golang/dep/cmd/dep
/go # which dep
/go/bin/dep

無事、go getできた。

実際のところマルチステージビルドをやるので、ビルド環境についてはあまり軽量化は考えなくても良いのかもしれない。

Notificationは積極的に自動化してボットにやらせる

仕事する上でコミュニケーションは大事だとは思うものの、通知に相当するものはボットにやらせた方がお互い救われるんじゃないかと思っている。

なんとなく、

  • 要件を伝えた後に議論が発生することがない
  • 単純な報告

のようなものは自動化しやすいと思っている。

例えば、作業をチケット管理していたとして、

  • ステータスが変わった時に次のアクションを教える
  • ルールに逸脱した状態を指摘する
  • 期限が過ぎそう、過ぎてる、を知らせる
  • 進捗の記入を促す

とか。

チケット管理に限らず、特に、ルールが守られてない状態を指摘するのは、言うのも言われるのもなんだかしんどい。

そういう事を真面目にコツコツ波風立てずにこなすほどの能力もないので、やりたくない事はやらない、の精神で自動化に取り組んでいる。

Pythonのdocoptを使ったコマンドライン引数の処理

PythonCLIコマンドを作成する際にオプションの処理を行いたかったので、docoptを使って実装した。

docoptとは

コマンドライン引数として処理したいオプションなどをテキストベースで記述することができる。その内容から、実行時にコマンドライン引数のパーサーの生成と、パースを行う。

docoptはもともとはPythonのライブラリだったようだが、Command-line interface description languageと謳っているだけあって、各言語の実装が展開されている。

使い方

↓こんな感じで使用する。

"""Naval Fate.

Usage:
  naval_fate.py ship new <name>...
  naval_fate.py ship <name> move <x> <y> [--speed=<kn>]
  naval_fate.py ship shoot <x> <y>
  naval_fate.py mine (set|remove) <x> <y> [--moored|--drifting]
  naval_fate.py -h | --help
  naval_fate.py --version

Options:
  -h --help     Show this screen.
  --version     Show version.
  --speed=<kn>  Speed in knots [default: 10].
  --moored      Moored (anchored) mine.
  --drifting    Drifting mine.
"""

from docopt import docopt


if __name__ == '__main__':
    arguments = docopt(__doc__)
    print(arguments)

実行すると以下のようになる。

$ naval_fate.py ship Guardian move 10 50 --speed=20
{'--drifting': False,
 '--help': False,
 '--moored': False,
 '--speed': '20',
 '--version': False,
 '<name>': ['Guardian'],
 '<x>': '10',
 '<y>': '50',
 'mine': False,
 'move': True,
 'new': False,
 'remove': False,
 'set': False,
 'ship': True,
 'shoot': False}

Usageなどの規定のキーワードとフォーマットがあり、それに従ってテキストメッセージを書いておき(docstringに書くサンプルが多い)、それを利用してオプションのパーサーとすることができる。 optparserなどコード内でパースする内容を定義するものと違って、別途ヘルプを書かなくて良いので、この仕様は個人的にはありがたい。

docopt—language for description of command-line interfaces ↑こちらで実際に試すことができる。

コマンド・サブコマンドを処理したい

gitaws-cliのように、サブコマンドを使用したCLIアプリを実装したかったので調べてみた。公式などいくつかのサンプルを参考に最終的には以下のようにした。

myapp/
├── __init__.py
├── __main__.py
├── commands
│   ├── __init__.py
│   ├── cmd1.py
│   └── cmd2.py
(snip...)

myapp/__init__.py

"""Usage:
    myapp <command> <subcommand> [<args>...]

Commands:
    cmd1      Run cmd1
    cmd2      Run cmd2
"""

def main():
    args = docopt(__doc__)

    command_name = args.pop('<command>')

    if command_name == 'help':
        # docstringをそのまま表示できるのが良い
        print(__doc__)
        return

    # search module from <command>
    try:
        command = importlib.import_module('myapp.commands.'+command_name)
    except Exception:
        # import_moduleでExceptionが起きた場合にはコマンドがない扱いにする
        # UnknownCommandErrorは別途定義している
        raise UnknownCommandError(__doc__, command_name)

    # search function from <subcommand>
    subcommand_name = args.pop('<subcommand>')

    # commandとして読み込んだモジュールからsubcommandを取得する
    # subcommandは関数として定義している(後述)
    subcommand = getattr(command, subcommand_name, None)

    if subcommand:
        # subcommandが取得できたら、再度コマンドライン引数のパースを実行する
        # subcommandの内容はテンプレートになっているので、
        # global_optsを渡してformatして使用する
        subcommand_doc = subcommand.__doc__.format(**global_opts)
        if len(subcommand_doc.strip()) > 0:
            args = docopt(subcommand_doc, version=version)
    else:
        subcommand = gen_help(command.__doc__)

    subcommand(global_opts, args)

myapp/commands/cmd1.py

"""Usage:
    {app_name} cmd1 <subcommand> [options] [<args>...]

Options:
    --opt=<opt>       Option

Commands:
    sub1              Sub command 1
    sub2              Sub command 2
"""

def sub1(global_opts, args):
    """Usage:
    {app_name} cmd1 sub1 --opt1=<opt1> [--flag] [<args>...]

Options:
    --opt1=<opt1>     Option
    --flag            flag
"""
    # ↑app_nameはdocoptに渡される前にformatされる
    do_something()
    
def sub2(global_opts, args):
    """Usage:
    {app_name} cmd1 sub2 [<args>...]
"""
    
    do_something()

こんな感じの実装にしてみた。

今のところ自分のユースケースはカバーできているが、もう少しコマンドが増えてくると考慮しないといけないことが出てくるかもしれない。

AWS Lambda+Goで、fujiwara/ridgeからapex/gatewayへ移行する

AWS Lambdaでは以前から、非公式にNode.jsをランタイムとして、Go実装のバイナリを実行させるツールがある。例えばApexとか。さらに、Apex、API Gatewayとの組み合わせを前提としたfujiwara/ridgeというライブラリがあり、net/httpのインターフェースにそった実装ができるようになっていた。

実際に会社でも個人的にも、Apex+Go+fujiwara/ridgeという組み合わせで稼働させているものがある。現時点ではランタイムはNode.jsで動いているが、公式にGoがサポートされるようになったので、移行をしたくてしばらく検証していた。

公式のライブラリ、aws/aws-lambda-goを使用した実装に直接入れ替えるのはしんどそうな感じだったが、apex/gatewayを見つけたのでそれを試してみた。

結果、問題なさそうだったので、とりあえず個人的なやつはそちらに移行した。

移行に伴う修正点

どちらもnet/httpのインターフェースを前提としているので、スムーズに移行できる。

ridgeのサンプルを変更すると以下になる。importするパッケージは当然変わるが、Multiplexerはそのまま使用できる。直接aws-lambda-goを使う必要がないので、大きな変更がなくて助かる。

--- main_old.go  2018-04-07 00:06:44.000000000 +0900
+++ main.go   2018-04-07 00:09:00.000000000 +0900
@@ -4,7 +4,7 @@
    "fmt"
    "net/http"

-  "github.com/fujiwara/ridge"
+   "github.com/apex/gateway"
 )

 var mux = http.NewServeMux()
@@ -15,7 +15,7 @@
 }

 func main() {
-  ridge.Run(":8080", "/api", mux)
+   gateway.ListenAndServe(":8080", mux)
 }

ちょっとapex/gatewayのコードを見てみる

https://github.com/apex/gateway/blob/master/gateway.go#L16

// ListenAndServe is a drop-in replacement for
// http.ListenAndServe for use within AWS Lambda.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, h http.Handler) error {
    if h == nil {
        h = http.DefaultServeMux
    }

    lambda.Start(func(ctx context.Context, e events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
        r, err := NewRequest(ctx, e)
        if err != nil {
            return events.APIGatewayProxyResponse{}, err
        }

        w := NewResponse()
        h.ServeHTTP(w, r)
        return w.End(), nil
    })

    return nil
}

ListenAndServeの中では、lambda.Startを呼び出しつつnet/httpのインターフェースに沿うように対応されている。w.Endevents.APIGatewayProxyResponseが返却されるはず。もう少し読み込んでみる。

https://github.com/apex/gateway/blob/master/response.go#L13

// ResponseWriter implements the http.ResponseWriter interface
// in order to support the API Gateway Lambda HTTP "protocol".
type ResponseWriter struct {
    out           events.APIGatewayProxyResponse
    buf           bytes.Buffer
    header        http.Header
    wroteHeader   bool
    closeNotifyCh chan bool
}

ResponseWriterはこんな構造になっている。

https://github.com/apex/gateway/blob/master/response.go#L39

// Write implementation.
func (w *ResponseWriter) Write(b []byte) (int, error) {
    if !w.wroteHeader {
        w.WriteHeader(http.StatusOK)
    }

    return w.buf.Write(b)
}

WriteのなかではResponseWriter.bufに対しての書き込みが行われるようになっている。

https://github.com/apex/gateway/blob/master/response.go#L77

// End the request.
func (w *ResponseWriter) End() events.APIGatewayProxyResponse {
    w.out.IsBase64Encoded = isBinary(w.header)

    if w.out.IsBase64Encoded {
        w.out.Body = base64.StdEncoding.EncodeToString(w.buf.Bytes())
    } else {
        w.out.Body = w.buf.String()
    }

    // notify end
    w.closeNotifyCh <- true

    return w.out
}

ヘッダーを見て、バイナリのデータかどうか確認しているっぽい。AWS API Gatewayではバイナリデータを返す際にはBase64エンコードされている必要があるので、それにも対応している。

out.Bodyが実際のレスポンスになるわけで、そこにResponseWriter.bufに書き込まれたデータが代入される。

ResponseWriter.closeNofityChtrueが送信されるようになっているが、これはapex/gateway内部では利用していないっぽい。利用側に通知するためのチャネルのようだ。(経緯としてはfeat(Response) Add CloseNotify support. by wolfeidau · Pull Request #7 · apex/gatewayに書いてある)

最終的には、ResponseWriteroutevents.APIGatewayProxyResponseになっているので、それを返すようにしている。

気になるところ

ちゃんとコード全体を見ていないのだけど。

fujiwara/ridge ではできていた以下の点が apex/gatewayでできるのかきちんと確認してない(必要性もないので)

  • Proxy Integrationに対応していて、リクエストがあったパスの情報をきちんとMultiplexerで処理できるようになっていた
    • 自分の要求としては/を前提にしているので、そこまで確認してない
  • ローカルでの起動と実行
    • 自分ではそこまで必要性がなく、実際今まで使ったことがないんだけど

参考

VSCodeをメモアプリとして使う

Boostnoteを使ってたんだけど、プレビュー周りの挙動が好きじゃなかったので、VSCodeでメモできるようにスクリプトを書いてみた。

雑な感じでzshのfunctionとして設定してみた。

function memo() {
  if [ -d $MEMO_HOME ]; then
    cd $MEMO_HOME
    local filename=$(date '+%Y%m%d_%H%M%S').md
    echo -e "# $(date '+%Y-%m-%d %H:%M:%S')\n" > ./src/$filename
    vs --wait --extensions-dir ./ext --goto src/$filename:2 ./src
    git add -A .
    git commit -m "add $filename"
  fi
}

前提

  • MEMO_HOMEとしてどこか指定しておく
  • 配下にextsrcを作成しておく
  • extは拡張機能を置く
    • できるだけ起動を軽くするため、普段使っている拡張機能は有効にならないようにしたい
  • srcはメモのファイルを置く
  • gitでも管理する
    • extはコミットしないようにgitignoreに追記しておく

挙動

  1. 規定のディレクトリに移動する
  2. 日時のMarkdownファイルを作成し、ヘッダーに日時を埋めておく
  3. VSCodeを起動する
  4. VSCodeが終了したらgitのコミットを行う

VSCodeは以下のオプションをつけている。

  • 終了するまで待つ(--wait)
  • extを拡張機能ディレクトリにする(--extensions-dir)
  • ファイルの2行目を指定して開く(--goto)

実際使ってみると、ファイルの一覧が欲しくなったり、Alfredと連携したくなってきたので、そのうちまた修正したい。