最近 Rust を少し学んでいたが、難しくて少し挫折しかけたのと、結局仕事への導入を考えるなら Go のほうが既に書ける人が何人かいる、というのもあり Go を書き始めた。
手初めてに欲しい CLI アプリケーションがあったのでそれをサクッと Go で書いてみた。
tsub/s3-edit: Edit directly a file on Amazon S3
モチベーション
仕事で S3 に環境変数を置いておいてそれを開発環境なり本番環境なりで使うことがあるのだが、そのファイルを編集する際に aws-cli を使って以下のコマンドを叩くのが面倒だった。
$ aws s3 cp s3://mybucket/myenvfile ./
# エディタでファイルを編集
$ nvim myenvfile
$ aws s3 cp myenvfile s3://mybucket/myenvfile
そこで、 S3 からファイルをダウンロードしてきてエディタを開いた後、編集してエディタを閉じたら S3 に再びアップロードしてくれる、というツールを作った。
$ s3-edit edit s3://mybucket/myenvfile
上記一コマンドだけで完結できる。
cobra が良かった
多くの CLI アプリケーションで採用されている cobra というライブラリが非常に良かった。
spf13/cobra: A Commander for modern Go CLI interactions
公式の README にも書かれている通り、kubectl や hugo, docker など色々なツールで採用されている。
個人的にはこれらのツールのインターフェースは非常に使いやすいと感じていたため、それと全く同じような構成が cobra を使えば簡単に作れるのには驚いた。
ちょっと記述するだけで以下のようなインタフェースとヘルプメッセージなどを提供してくれる。
$ s3-edit --help
Edit directly a file on Amazon S3
Usage:
s3-edit [flags]
s3-edit [command]
Available Commands:
edit Edit directly a file on S3
help Help about any command
version Print the version of s3-edit
Flags:
-h, --help help for s3-edit
-v, --version print the version of s3-edit
Use "s3-edit [command] --help" for more information about a command.
今後も CLI アプリケーションを作る際は cobra を使っていきたいところ。
躓いたところ
S3 から受け取ったファイルの中身をどうやってローカルファイルに書き込むか、というところでわりと躓いた。
ドキュメントで言うと s3.GetObjectOutput
の Body
が io.ReadCloser
型のインターフェースを返すのだが、それをどうやってファイルに書き込めばいいのだろう?というところ。
http://docs.aws.amazon.com/sdk-for-go/api/service/s3/#GetObjectOutput
io.ReadCloser
インターフェースは Reader
と Closer
インターフェースを持っていて、それぞれ Read()
と Close()
という関数が実装されていれば良い、というところまでは理解した。
https://golang.org/pkg/io/#ReadCloser
ただ、具体的に s3.GetObjectOutput.Body
からどう値を取ればいいんだろうか、というところで困った。
調べていくと bytes.Buffer
の ReadFrom()
関数の引数が io.Reader
型だったためそこに渡せば良さそう、というところに辿りついたのでやってみたらなんとかできた。
https://golang.org/pkg/bytes/#Buffer.ReadFrom
s3-edit の実装でいうとこの辺り。
// https://github.com/tsub/s3-edit/blob/v0.0.5/cli/s3/object.go#L26-L31
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(res.Body); err != nil {
fmt.Println(err)
os.Exit(1)
}
return buf.Bytes()
}
こういった、io の扱いとかが自分はまだ理解できていない。
以下の記事を読んだ感じでは、おそらく今回とった方法もあまり効率的ではない気もする。
Golangでのstreamの扱い方を学ぶ - Carpe Diem
楽しかったところ
Go のダックタイピングが個人的には楽しかった。
S3 周りのテストを書いていてモックを作る時、ああこうやって書けばいいのか、というのを理解した時の楽しさはやばかった。
s3-edit の実装でいうとこの辺り。
// https://github.com/tsub/s3-edit/blob/v0.0.5/cli/s3/object_test.go#L12-L26
type mockedGetObject struct {
s3iface.S3API
Resp []byte
}
func (m *mockedGetObject) GetObject(input *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
pr, pw := io.Pipe()
go func() {
pw.Write(m.Resp)
pw.Close()
}()
return &s3.GetObjectOutput{Body: pr}, nil
}
// https://github.com/tsub/s3-edit/blob/v0.0.5/cli/s3/object.go#L15
func GetObject(svc s3iface.S3API, path Path) []byte {
関数の引数にインターフェースを受け取ることで、テスト側でそのインターフェースを拡張して引数として渡せば任意の処理ができてしまう。
最近仕事で Ruby を書いていて Dependency injection やダックタイピングなどを意識してコードを書いているが、Ruby ではいまいち納得感がない。
Go で書いたらそれらがすんなりと理解できた感じがある。
リリースフロー
リリースフローは以下の手順で行うようにした。
- GitHub で release を作る
- dep で依存ライブラリをダウンロード
- gox でクロスコンパイル (CircleCI)
- 各バイナリを tar.gz に圧縮
- CHECKSUMS ファイルを生成
- ghr から 1 で作った release にバイナリをアップロード (CircleCI)
なお、CircleCI 内で扱う dep, gox, ghr は全て Docker でコンテナを動かす形にしてみた。
Go と直接関係ないが CircleCI では以下のところでハマった。
-v
オプションによるローカルファイルのマウントができない
CircleCI 2.0 ではコンテナ内にファイルをマウントする場合に -v
オプションが使えない。
そのため、公式のドキュメントに書いてあるような方法をとる必要がある。
https://circleci.com/docs/2.0/building-docker-images/#mounting-folders
docker create
でボリュームをマウントするためのコンテナを動かし、docker cp
でそこにローカルファイルをコピーする。
その後、 --volumes-from
オプションでコンテナ間でファイルをマウントすればようやく参照できる。
自分が書いた設定だと以下のようになった。
# https://github.com/tsub/s3-edit/blob/v0.0.5/.circleci/config.yml#L18-L26
- run: &create_dummy_container
name: Create dummy container for mounting files and copy project files
command: |
docker create -v /go/src/github.com/tsub --name project busybox /bin/true
docker cp $PWD project:/go/src/github.com/tsub/s3-edit
- run:
name: Vendoring go packages
command: docker run --volumes-from project -w /go/src/github.com/tsub/s3-edit supinf/go-dep ensure
Git tag でビルドをキックする
CircleCI 2.0 では Git tag によるビルドのキックは少し癖がある。
まず、Workflows を使う必要がある。
https://circleci.com/docs/2.0/workflows/#git-tag-job-execution
Workflows で filters.tags.only
を指定しなければデフォルトでは Git tag を付けてもビルドがキックされない。
さらにハマりどころなのが、Workflows で複数の job を指定する際に tag によって制限したい job が依存している job にも filters.tags.only
を指定する必要がある。
自分が書いた設定だと以下のようになった。
# https://github.com/tsub/s3-edit/blob/v0.0.5/.circleci/config.yml#L78-L94
workflows:
version: 2
build_and_deploy:
jobs:
- build:
filters:
tags:
only: /.*/
- deploy:
requires:
- build
filters:
tags:
only: /.*/
branches:
ignore: /.*/
なお、branch で動かしたくない場合は fiters.branches.ignore
の指定も必要である。
Git tag が付いた時だけビルドを動かしたいだけなのにわりと書くことが多くややこしい。
まとめ
とまあ、Go の話というより CircleCI の話になってしまったが、とりあえず作りたいものがサクッと作れたし、楽しく書けたので Go は今後もやっていく予定。
次は Go で何を作ろうか考え中…