みんGO を読んでec2インスンスリストをタグ検索するコマンドラインツールを作ってみた

プロジェクトでGO言語に触れながら学習のためにコマンドラインツールを作り拡張させながら言語理解を深めようと目標を立てた。「みんなのGO言語」を参考にしながら自作のコマンドラインツールを作ったのでまとめます。

みんなのGo言語[現場で使える実践テクニック]

みんなのGo言語[現場で使える実践テクニック]

どんなコマンドラインツールを作ったか

ec2インスタンスをタグ検索してインスタンス情報を取得できるコマンドラインツールを作りました。
生成したインスタンスリストをpecoでインクリメンタルサーチできるようにして選択したインスタンスsshできるようなzsh関数も合わせて作りました。
作ったサブコマンドとpecoを組み合わせればインスタンスへのssh接続が快適になります。

ec2インスタンスリストを生成してくれるコマンドラインツール

作りたいイメージは次のようなものです。

describe_es2 tag -tag-key Name '*myweb*'

コマンドを実行したイメージは次のようになります。

$ describe_es2 tag -tag-key Name '*myweb*'
Completed saving file ./myweb001_i-xxxxxxxxxxxxxx, that content is ec2-xx-xx-xx-xx.region.compute.amazonaws.com
Completed saving file ./myweb002_i-xxxxxxxxxxxxxx, that content is ec2-xx-xx-xx-xx.region.compute.amazonaws.com
Completed saving file ./myweb003_i-xxxxxxxxxxxxxx, that content is ec2-xx-xx-xx-xx.region.compute.amazonaws.com
$ find . -type f
./myweb001_i-xxxxxxxxxxxxxx
./myweb002_i-xxxxxxxxxxxxxx
./myweb003_i-xxxxxxxxxxxxxx
$ cat ./myweb001_i-xxxxxxxxxxxxxx
ec2-xx-xx-xx-xx.region.compute.amazonaws.com // インスタンスのパブリックDNSがファイルの中身に保存されている


tagのサブコマンドには次のオプションを指定できるようにします。

  • aws認証ファイルパス(デフォルトはLinux/OSXであれば"$HOME/.aws/credentials)
  • aws認証プロフィール(デフォルトは'default')
  • region(デフォルトは'ap-northeast-1')
  • 検索対象のタグキー(デフォルトは'Name')

aws認証プロフィールをmyprojectに指定して検索する場合は次のようなコマンドになります。

describe_es2 tag -credential-profile myproject '*myweb*'

サブコマンドにはgoogle/subcommandsを使った

今回はtag検索のみのツールですが今後の拡張でEC2 Container Serviceのクラスタ名を指定すればインスタンスリストが生成されるような拡張を考えているためサブコマンド化したかった。「みんなのGO言語」の「4.4 サブコマンドをもったCLIツール」ではmitchellh/cliの使い方を紹介いただいてますが情報が少なそうな「google/subcommands」を使ってみた。

github.com

サブコマンドをインターフェースとして定義できるので定義したサブコマンドのオプションのコード化など見通しの良いコードが書ける。
次からはgoogle/subcommandsを利用したコードの説明をしていきます。

google/subcommandsの実装例

サブコマンドのオプションを定義する

サブコマンドのオプションをstructで定義します

// オプションのaws認証情報とタグキー、regionを定義
type tagCmd struct {
	credential credential
	tagKey     string
	region     string
}

// aws認証情報は個別にstructで定義
type credential struct {
	profile  string
	filename string
}
subcommands.Commandのインターフェースを実装する
package subcommands

// A Command represents a single command.
type Command interface {
	// Name returns the name of the command.
	Name() string

	// Synopsis returns a short string (less than one line) describing the command.
	Synopsis() string

	// Usage returns a long string explaining the command and giving usage
	// information.
	Usage() string

	// SetFlags adds the flags for this command to the specified set.
	SetFlags(*flag.FlagSet)

	// Execute executes the command and returns an ExitStatus.
	Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) ExitStatus
}

tagのサブコマンドがsubcommands.Commandを実装している例

func (*tagCmd) Name() string {
	return "tag"
}

func (*tagCmd) Synopsis() string {
	return "Fetch the ec2 instance public dns name by tag search, then that stored to text file in the current directory."
}

func (*tagCmd) Usage() string {
	return `tag [-credential-profile default] [-credential-filename '~/.aws/credentials'] [-region ap-northeast-1] [-tag-key Name] '*dev*' :
  Created or updated text file
`
}

func (p *tagCmd) SetFlags(f *flag.FlagSet) {
	f.StringVar(&p.credential.filename, "credential-filename", "", "optional: aws credential file name, when filename is empty, that will use '$HOME/.aws/credentials'")
	f.StringVar(&p.credential.profile, "credential-profile", "default", "optional: aws credential profile, default value is 'default'")
	f.StringVar(&p.region, "region", "ap-northeast-1", "optional: aws region, default value is 'ap-northeast-1'")
	f.StringVar(&p.tagKey, "tag-key", "Name", "target tag key, default value is 'Name'")
}

func (p *tagCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
	// 省略
	return subcommands.ExitSuccess
}

"Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) ExitStatus"のメソッドはサブコマンドのメインの処理です。詳細は"github.com/nsoushi/describe-ec2"を参照してください。

定義したtagCmdをsubcommands.Registerで追加する
package main

func main() {
	subcommands.Register(subcommands.HelpCommand(), "")
	subcommands.Register(subcommands.FlagsCommand(), "")
	subcommands.Register(subcommands.CommandsCommand(), "")
	subcommands.Register(&tagCmd{}, "")

	flag.Parse()
	ctx := context.Background()
	os.Exit(int(subcommands.Execute(ctx)))
}
インターフェースを実装することでコマンドの使い方やヘルプがまとまる

上記のコードにある通りName()、Synopsis()、Usage()、SetFlags(*flag.FlagSet)を実装することでtagサブコマンドのヘルプが綺麗に出力されます。

subcommands.Registerで登録したサブコマンドが列挙されている

describe-ec2
Usage: describe-ec2 <flags> <subcommand> <subcommand args>

Subcommands:
	commands         list all command names
	flags            describe all known top-level flags
	help             describe subcommands and their syntax
	tag              Fetch the ec2 instance public dns name by tag search, then that stored to text file in the current directory.


Use "describe-ec2 flags" for a list of top-level flags

tagサブコマンドのヘルプ出力

./describe-ec2 help tag help tag
tag [-credential-profile default] [-credential-filename '~/.aws/credentials'] [-region ap-northeast-1] [-tag-key Name] '*dev*' :
  Created or updated text file
  -credential-filename string
    	optional: aws credential file name, when filename is empty, that will use '$HOME/.aws/credentials'
  -credential-profile string
    	optional: aws credential profile, default value is 'default' (default "default")
  -region string
    	optional: aws region, default value is 'ap-northeast-1' (default "ap-northeast-1")
  -tag-key string
    	target tag key, default value is 'Name' (default "Name")
subcommands.Commandを使ってみて
  • 導入は難しくなく簡単にサブコマンドを増やせるので拡張しやすくヘルプも綺麗にまとまり保守性も良さそうです。
「みんなのGO言語」を参考にして
  • ossで誰かに使われる意識を持ってエラーメッセージやヘルプなど詳細に記載した
  • ライブラリをメインの成果物とする場合のディレクトリ構成を参考に 'cmd/describe-ec2/'配下にmainパッケージを置いた

作ったサブコマンドで生成したインスンスリストをpecoでインクリメンタルサーチする

次のようなzsh関数を使って生成したインスタンスリストをインクリメンタルサーチして選択したインスタンスsshできます。

peco-describe-ec2() {
  local MAXDEPTH=${1:-1}
  local BASE_DIR="${2:-`pwd`}"

  local FILENAME=$(find ${BASE_DIR} -maxdepth ${MAXDEPTH} -type f -exec basename {} ';' | peco | head -n 1)

  if [ -n "$FILENAME" ] ; then
    local HOST=`cat $BASE_DIR/$FILENAME`
    echo "ssh $HOST"
    BUFFER="ssh ${HOST}"
    zle accept-line
  fi
  zle clear-screen
}

ソースを公開しています

github.com

使い方

$ go get github.com/nsoushi/describe-ec2/cmd/describe-ec2
$ source $GOPATH/src/github.com/nsoushi/describe-ec2/.zsh.describe_ec2
$ describe-ec2 tag '*AWS*'
$ peco-describe-ec2 // '*AWS*'がtag.Nameに含まれるインスタンスリストをpecoでインクリメンタルサーチして選択したらsshします