本記事は Rust Advent Calender の9日目の記事です。
今回はRustでCLI を作ったので、機能紹介と作る上でのポイントなどを紹介します。
書こうと思っていたネタが、7日目の 2020 年版 Command Line Tool を作ってみる in Rust - Qiita と結構被ってしまったのですが、
どうやってコードを書くかというより、自分がハマったどうやってデプロイするか周りをメインに紹介します。
なぜ作ろうと思ったか
今回このCLI を作ったモチベーションとしてはRustでなにか作りたいという気持ちがメインではありますが、
kubectlでget nodeしたとき、どういうインスタンス かわからない
EC2にssm-agentで入りたいときにインスタンス IDを調べるのが大変
port-forwardしたいときにIPわからん
aws cli はシュッと使うにはわかりづらい
という気持ちがあったのでEC2の検索CLI を作ろうと思いました。
実際、自分で作ってから便利に使えてるのでよかったと思いました。
ec2-search
ec2-searchというCLI を作りました。
github.com
実際のコマンドはec2sです。
簡単にEC2の検索ができるようになるべく使い心地的にはkubectlのような感じにしたく、設計しました。
当初はインスタンス だけの検索にしようかと思ったのですが、TargetGroupなどEC2関連の検索全般も欲しくなったのでカバー範囲としてはざっくりEC2周りです。
機能一覧
現状ではそこまで機能は多くないのですが、
EC2インスタンス の検索は info というコマンドで検索できます。
$ ec2s instance info -q api
ID Name Status Type
i-012345678 test-api1 running t2.micro
i-023456789 test-api2 running t3.small
counts: 2
instanceのエイリアス として ec2s i info -q api とも検索できます
この -q はAWS コンソールの検索ボックスと同じようにNameやID、Private DNS Nameで引けるようになっています。
他にもdns やipを出力させるコマンドがあります
$ ec2s i help
ec2s-instance 0.7.0
search instance
USAGE:
ec2s instance <SUBCOMMAND>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
SUBCOMMANDS:
dns-name search instance DNS name with query. [aliases: dns]
help Prints this message or the help of the given subcommand(s)
info search instance basic info with query.
instance-ids search instance ids with query. [aliases: ids]
ips search instance ips with query.
RustでCLI を作るためのパーツ
cli といえばまずコマンドライン パーサです。
今回はstructoptを使いました。
こちらマクロにより自動でflag parse部分などを作ってくれるので大変便利でした。
またサブコマンドも簡単に作れるのでオススメです。
enum でコマンドを定義し、structでoptionを定義します。
以下具体例で見ていきます。
#[derive( Debug , StructOpt)]
pub enum InstanceOpt {
#[structopt(about = "search instance basic info with query." )]
Info (SearchQueryOpt),
}
#[derive( Debug , StructOpt)]
pub struct SearchQueryOpt {
#[structopt(
short = "q" ,
long,
help = "ambiguous search with asterisk on tag name. if set comma, search OR"
)]
query: String ,
}
enum で定義されたInfoはそのままサブコマンドの info としてマクロ生成されます。
structで定義しているqueryは -q オプションとしてマクロ生成されます。
2020/12現在 rustのAWS 公式SDK はないので、デファクトスタンダード となっているrusotoを使います。
一つ難点としてはrusoto_ec2ではIDE (IntelliJ のrust plugin)の補完が効かないので、rust docとにらみ合いながら格闘しました。
rusoto_elbv2などは問題なかったのでマクロ生成されていると補完が効かないようです。
IntelliJ ではデフォルトではファイルサイズが2.5MBを超えるものはパースされないので、設定で変更する必要があります。
Help > Edit Custom Properties
から以下の値を設定することで補完されるようになります。
# default is 20000 (単位はKB)
idea.max.content.load.filesize=1000000
# default is 2500 (単位はKB)
idea.max.intellisense.filesize=1000000
ただエディタのメモリ消費が増えるので設定には気をつけてください
実行バイナリ
実際の実行バイナリは [[bin]]
で決められています。
nameの値を変更するとその名前でバイナリが吐かれます。
レポジトリ名はec2-searchですが長いのでバイナリとしては ec2s
で吐いています。
[[bin]]
name = "ec2s"
path = "src/main.rs"
表示
やはり kubectl みたいにキレイに表示したいですよね。
ec2-searchでは cli-table
というライブラリを使ってテーブル表示を行っています。
ライブラリを使うことで自分でスペースなどを計算しなくて済むのでCLI 作りでは必須でしょう。
使い方は以下のような関数でwrapしてますが以下のように作ります。
cli -tableでは
文字のスタイル
テーブルborderのスタイル
テーブルseparatorのスタイル
が指定できるので、好きなスタイルで表示できます
pub fn print_table (header: Vec < &str > , rows: Vec < Vec < String >> ) {
let bold = CellFormat :: builder ().bold (true ).build ();
let h: Vec < Row> = vec! [Row :: new (
header.iter ().map (| h| Cell :: new (h, bold)).collect (),
)];
let rows: Vec < Row> = rows
.iter ()
.map (| r| Row :: new (r.iter ().map (| c| Cell :: new (c, Default :: default ())).collect ()))
.collect ();
let r: Vec < Row> = h.into_iter ().chain (rows).collect ();
let border = Border :: builder ().build ();
let separator = Separator :: builder ().build ();
let format = TableFormat :: new (border, separator);
let _ = match Table :: new (r, format) {
Ok (t) => t,
Err (e) => panic! ("{:?}" , e),
}
.print_stdout ();
}
cli の出力
CI
Rustの公式ドキュメントではTravisCIが紹介されていますが、最近のTravisCIはめちゃくちゃ遅いのでGitHubActionをオススメします。
CIでは以下2つを別で走らせています。
- style check
- test
style checkとして以下を実行しています。
- rust fmt: インデントなどのコードフォーマットチェック
- clippy: コード上でこう書いたほうがいいというサジェストをしてくれるlinter
jobs :
style :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v2
- uses : actions-rs/toolchain@v1
with :
toolchain : nightly
components : rustfmt, clippy
override : true
- name : Check the format
run : cargo +nightly fmt --all -- --check
- name : Run clippy
run : cargo clippy -- -D warnings
test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v1
- uses : actions/cache@v2
with :
path : |
target
key : ${{ runner.os }}-cargo-check-test-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
- name : Build
run : cargo check
env :
CARGO_INCREMENTAL : 0
RUSTFLAGS : "-C debuginfo=0 -D warnings"
- name : Run tests
run : cargo test --workspace
if : ${{ runner.os == 'Linux' }}
env :
CARGO_INCREMENTAL : 0
RUSTFLAGS : "-C debuginfo=0 -D warnings"
実際のCIは以下リンクです。
masterにPRを送るとCIが走ります。
ec2-search/test.yml at master · mocyuto/ec2-search · GitHub
CD
cli を使ってもらおうと思ったときに、「git cloneしてcargoコマンドをいれてcargo installしてください」と言われたらまぁ使ってもらえないですよね。
なので、brew でインストールできるようにします。
brew でインストールできるようにしてもらうには以下の手順が必要です。
linux にも一応brew があるので、一旦linux もbrew でいれてもらう想定です。
以下が手順になります。
cli のバイナリを作成する
GitHub のリリースページなどバイナリをダウンロードできる場所に置く
brew 本家はstarの数などで制限があるため、自前のbrew レポジトリを作る
homebrew-hoge というレポジトリを作る
GitHub - mocyuto/homebrew-tap がhomebrewを使うためのレポジトリ
しかし、これを毎度手動でやるのは大変なので、CDフローに任せましょう。
GitHubActionではtagを切ると
バイナリリリース
バイナリ作成(osx , linux , windows それぞれのバイナリを作ります)
releaseページにアップロード
crate.ioにpublish
をやってくれるようになっています。
バイナリのリリース
ビルドするときopenssl周りでビルドエラーが起こりうるので、opensslをOSでダウンロードしていますが、
それ以外はわかりやすいかなと思います。
secrets.GITHUB_TOKEN
は特に何も設定しなくてもGitHub 側で注入してくれるので楽ちんです。
publish :
strategy :
matrix :
os : [ macos-latest, ubuntu-latest, windows-latest]
rust : [ stable]
include :
- os : macos-latest
artifact_prefix : macos
target : x86_64-apple-darwin
binary_postfix : ""
- os : ubuntu-latest
artifact_prefix : linux
target : x86_64-unknown-linux-gnu
binary_postfix : ""
- os : windows-latest
artifact_prefix : windows
target : x86_64-pc-windows-msvc
binary_postfix : ".exe"
runs-on : ${{ matrix.os }}
steps :
- uses : actions/checkout@v1
- name : Installing Rust toolchain
uses : actions-rs/toolchain@v1
with :
toolchain : ${{ matrix.rust }}
override : true
- name : Installing needed macOS dependencies
if : matrix.os == 'macos-latest'
run : brew install openssl@1.1
- name : Installing needed Ubuntu dependencies
if : matrix.os == 'ubuntu-latest'
run : |
sudo apt-get update
sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
- uses : actions/cache@v2
with :
path : |
target
key : ${{ runner.os }}-cargo-publish-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
- name : Running cargo build
uses : actions-rs/cargo@v1
with :
command : build
toolchain : ${{ matrix.rust }}
args : --release --target ${{ matrix.target }}
- name : Packaging final binary
shell : bash
run : |
cd target/${{ matrix.target }}/release
strip ec2s${{ matrix.binary_postfix }}
tar czvf ec2-search-${{ matrix.artifact_prefix }}.tar.gz ec2s${{ matrix.binary_postfix }}
if [[ ${{ runner.os }} == 'Windows' ]] ; then
certutil -hashfile ec2-search-${{ matrix.artifact_prefix }}.tar.gz sha256 | grep -E [ A-Fa-f0-9]{ 64 } > ec2-search-${{ matrix.artifact_prefix }}.sha256
else
shasum -a 256 ec2-search-${{ matrix.artifact_prefix }}.tar.gz > ec2-search-${{ matrix.artifact_prefix }}.sha256
fi
- name : Releasing assets
uses : softprops/action-gh-release@v1
with :
files : |
target/${{ matrix.target }}/release/ec2-search-${{ matrix.artifact_prefix }}.tar.gz
target/${{ matrix.target }}/release/ec2-search-${{ matrix.artifact_prefix }}.sha256
env :
GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
このリリースビルドを作る際に以下のレポジトリをすごく参考にしました。(というかほぼ同じ)
感謝 !!!
GitHub - Rigellute/spotify-tui: Spotify for the terminal written in Rust 🚀
crates.ioへのpublish
GitHub のレポジトリのSECRETにcrates.ioで発行されるAPI _KEYをコピーして貼りつけると使えるようになります。
crates.ioへのpublishはCargo.tomlのversionを見ているのでリリース前に変更するのを忘れないようにしましょう。
publish-cargo :
name : Publishing to Cargo
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@master
- uses : actions-rs/toolchain@v1
with :
toolchain : stable
override : true
- run : |
sudo apt-get update
sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
- uses : actions-rs/cargo@v1
with :
command : publish
args : --token ${{ secrets.CARGO_API_KEY }} --allow-dirty
実際はこんな感じです。
ec2-search/release.yml at master · mocyuto/ec2-search · GitHub
homebrew
homebrewからダウンロードしてもらうには、homebrew-hoge のレポジトリを作る必要があります。
ec2-searchのものは homebrew-tap/ec2-search.rb at master · mocyuto/homebrew-tap · GitHub です。
hoge 部分の命名 は基本何でもいいのですが、有名レポジトリは大抵 homebrew-tap
という命名 をしているのでtapがオススメです。
自分はec2-searchとつけてしまいました。。
以下例です
このレポジトリの命名 によって tapコマンドの引数が変わります。
# mocyuto/homebrew-ec2-searchなので
$ brew tap mocyuto/ec2-search
# もし mocyuto/homebrew-tap なら
$ brew tap mocyuto/tap
細かい仕組みなどは割愛しますが、
リリースフローとしては
main.rbのversionを変更
バイナリ生成と同時に作成されるsha256のハッシュ値 をセット
PUSH
です。
バージョンを変更することでbrew upgradeで最新バージョンに更新できるようになります。
まとめ
Rustの内容というよりはCLI の作り方になってしまいましたが、Rustの事始めとしてのCLI 作成は良い題材だと思います。
作っている際にCLI のバイナリを作るところまでの解説は多かったのですが、実際に運用するためのCI/CDの記述が少なく困ったので今回このような記事にさせていただきました。
ec2-searchも使っていただけると嬉しいです。
追記
yuutookun.hatenablog.com