みんGO を読んでec2インスンスリストをタグ検索するコマンドラインツールを作ってみた
プロジェクトでGO言語に触れながら学習のためにコマンドラインツールを作り拡張させながら言語理解を深めようと目標を立てた。「みんなのGO言語」を参考にしながら自作のコマンドラインツールを作ったのでまとめます。
どんなコマンドラインツールを作ったか
ec2インスタンスをタグ検索してインスタンス情報を取得できるコマンドラインツールを作りました。
生成したインスタンスリストをpecoでインクリメンタルサーチできるようにして選択したインスタンスにsshできるようなzsh関数も合わせて作りました。
作ったサブコマンドとpecoを組み合わせればインスタンスへのssh接続が快適になります。
ec2インスタンスリストを生成してくれるコマンドラインツール
作りたいイメージは次のようなものです。
1
|
describe_es2 tag -tag-key Name *myweb*
|
- describe_es2はインスタンスを検索できる
- describe_es2はサブコマンドを提供する
- サブコマンドのtagはインスタンスをtag検索できる
- pecoでインクリメンタルサーチしたいので、検索したインスタンスごとにローカルにファイルを生成して中身にはパブリックDNSを保存されるようにする
コマンドを実行したイメージは次のようになります。
1
2
3
4
5
6
7
8
9
10
|
$ 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に指定して検索する場合は次のようなコマンドになります。
1
|
describe_es2 tag -credential-profile myproject '*myweb*'
|
サブコマンドにはgoogle/subcommandsを使った
今回はtag検索のみのツールですが今後の拡張でEC2 Container Serviceのクラスタ名を指定すればインスタンスリストが生成されるような拡張を考えているためサブコマンド化したかった。「みんなのGO言語」の「4.4 サブコマンドをもったCLIツール」ではmitchellh/cliの使い方を紹介いただいてますが情報が少なそうな「google/subcommands」を使ってみた。
サブコマンドをインターフェースとして定義できるので定義したサブコマンドのオプションのコード化など見通しの良いコードが書ける。
次からはgoogle/subcommandsを利用したコードの説明をしていきます。
google/subcommandsの実装例
サブコマンドのオプションを定義する
サブコマンドのオプションをstructで定義します
1
2
3
4
5
6
7
8
9
10
11
12
|
// オプションのaws認証情報とタグキー、regionを定義
type tagCmd struct {
credential credential
tagKey string
region string
}
// aws認証情報は個別にstructで定義
type credential struct {
profile string
filename string
}
|
subcommands.Commandのインターフェースを実装する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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を実装している例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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
}
|
1
|
"Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) ExitStatus
|
↑のメソッドはサブコマンドのメインの処理です。詳細は”github.com/nsoushi/describe-ec2”を参照してください。
定義したtagCmdをsubcommands.Registerで追加する
1
2
3
4
5
6
7
8
9
10
11
12
|
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)))
}
|
インターフェースを実装することでコマンドの使い方やヘルプがまとまる
上記のコードにある通り
1
|
Name()、Synopsis()、Usage()、SetFlags(*flag.FlagSet)
|
↑を実装することでtagサブコマンドのヘルプが綺麗に出力されます。subcommands.Registerで登録したサブコマンドが列挙されている
1
2
3
4
5
6
7
8
9
10
11
|
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サブコマンドのヘルプ出力
1
2
3
4
5
6
7
8
9
10
11
|
./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できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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
}
|
ソースを公開しています
https://github.com/soushin/describe-ec2
使い方
1
2
3
4
|
$ 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します
|