あきろぐ

いろいろめもするよ

Google CalendarをCLIで扱えるnpmを作成しました

作ったもの

Google CalendarCLIで扱えるnpmを公開しました。機能はシンプルですが、ターミナル上でGoogle Calendarのイベントを表示、登録、編集、削除することができます。

www.npmjs.com

作成動機

最近Javascriptを勉強していたこともあり、外部APIを使って何かCLIツールとして便利なものを作ってみたいと思ったからです。 他にも色々案は考えていましたが、大体二番煎じになってしまうものしか思いつかず、一番アイディアだしに苦労しました。

結構無難ではあるけど、自分の観測内ではGoogle Calendarをいい感じに扱えるnpmは存在してなさそうだったので、これに決定しました。 Google Calendar APIを使うためにGCPでService accountを作成したりと、普段AWSしか触っていない身としては勉強になりそうだなと思ったのも1つの動機です。

Google Calendar + Javascriptをいい感じに組み合わせてgoogler-jsと命名しました。

悩んだところ

GCPよく分からん

GCP側のOAuth認証の同意画面で何が原因か分からないエラーに遭遇し、数時間時間を溶かしました。 結局アプリ名がよくなかったみたいです、ハイフン使うとだめだったみたい?(エラーメッセージに含めてほしい)

OAuth認証使うかサービスアカウントを使うか

OAuth認証だと期限が切れると毎回認証作業が発生するのがめんどくさいので、サービスアカウント使う方法を調べていたのですが、サンプルには載っていなくて探すのに時間かかりました。

レポジトリのREADMEに記載あったので無事サービスアカウント使った認証方法で実装できました。

github.com

CLIツールとしての使いやすさ

CLIGoogle Calendarを操作できるとはいえ、やっぱり使いにくいとブラウザ上で操作したほうが早いとなってしまい意味がないので、ある程度の操作性が担保されるように実装するのは考えどころでした。

ワンライナーで必要な情報を指定して実行するのもありですが、対話的インターフェースの方が操作しやすいかと思ったのでenquirerを導入し、入力値に不正な値が含まれていたら弾かれるようにバリデーションもしっかり実装しました。

後は、使っていてほっこりするようにネコチャンを召喚させてます。

実行イメージはこちら(途中入力ミスしてますがw)

googler-jsの今後

まだ最低限しか機能がないので、イベントを検索するなどもう少し機能拡張したいなと思っています。

CodeDeployで"TaskDefinitionTemplateArtifact"パラメータを見直してくれって言われたときの回避策

どんなエラー?

遭遇したエラーは以下の通り。

タスク定義の検証に失敗したので、CodePipelineのアクションに記述されている"TaskDefinitionTemplateArtifact"のタスク定義を見直してほしいとのこと。 別のデプロイパイプラインも同じ構成となっているのに関わらず、特定のデプロイパイプラインのCodeDeployだけ失敗する状況だった。

Failed to validate the task definition. Check the task definition in the
"TaskDefinitionTemplateArtifact" parameter for your pipeline action
and verify the configuration details for the ECS service.

デプロイパイプラインはCodePipeline+CodeBuild+CodeDeployで実現されており、ECSのタスク定義はCodeBuildのArtifactとして出力されたものをCodeDeployで使っている構成となっている。

f:id:akngo22:20220305001258p:plain
デプロイパイプラインのアーキテクチャ

原因

このエラーの原因について調べたところ、タスク定義の環境変数でマルチバイト文字が含まれているとエラーになるらしいことが発覚した。 該当のタスク定義を見直してみると全角の環境変数の値が含まれていたのでこれが原因そう。

zuntan02.hateblo.jp

回避策

環境変数の値をマルチバイト文字から変更することはできないので、タスク定義で直接環境変数の値をベタ書きするのではなく、ParameterStoreの中にマルチバイト文字を入れ参照する形に変更した。 このように環境変数の参照方法を変更することで上記のエラーが出なくなったことが確認できた。

ParameterStoreに値を入れるとSecrets配列の中に対象の環境変数を定義することになるが、現状はこれが手っ取り早い回避策かなと思う。

Rubyのoptparseについて掘り下げてみる

これは、「フィヨルドブートキャンプ Part 2 Advent Calendar 2021」2日目の記事です。

adventar.org

part1もあるので、こちらもどうぞ!

adventar.org

はじめに

何かしらのプログラミング言語を使ってコマンドを実装する場合、付属するオプションを自前で解析するのは時間がかかりますよね。 Rubyではオプション解析を良い感じにやってくれるライブラリが用意されており、それがoptparseです。

スクリプトを書くときにoptparseを何回か使ったことがあるのですが、いまいち理解しきれていないと感じているので、この機会にoptparseライブラリについて少し深掘りしたいと思います。

optparseとは

  • Rubyには、最初から数多くのライブラリが標準ライブラリとして用意されている
  • 特に使用頻度が高いものは組み込みライブラリとして提供されているため、requireしなくても呼び出すことが可能
  • optparseは標準ライブラリの1つで、コマンドラインのオプションを取り扱うために使われる
  • optparseを使うときは基本的に以下のステップを踏む
    • OptionParserオブジェクトoptを生成
    • オプションを扱うブロックを登録
    • opt.parse(ARGV)でコマンドラインをパースする

基本的な使い方

まずは、Rubyのリファレンスに記載されているサンプルコードを使って挙動を確認していきます。

require 'optparse'

# create opt object
opt = OptionParser.new

# register options
opt.on('-a') {|v| p v }
opt.on('-b') {|v| p v }

# check arguments
p ARGV

# parse
opt.parse!(ARGV)
p ARGV

実行結果

$ ruby sample.rb -a foo -b bar
true
true
["-a", "foo", "-b", "bar"]
["foo", "bar"]

onメソッドは、optオフジェクトに登録するオプションを定義しています。ARGVにはコマンドラインで渡されたオプション、引数が配列として渡されます。パースすることで引数だけ取り出すことができます。

ARGVからオプションを取り除く必要がない場合は、破壊的メソッドは使わずにopt.parse(ARGV)すればOKです。

上記のサンプルプログラムでは、オプションa,bを定義しましたが、ヘルプオプションhはデフォルトで定義されています。

$ ruby sample.rb -h
["-h"]
Usage: sample [options]
    -a
    -b

ここから、このサンプルプログラムをベースに少しずつ拡張していきます。

ロングオプションを定義する

例えば、ショートオプションだけでななく、ロングオプションを定義したい場合は、onメソッドでオプション定義するときにショートオプションと一緒にロングオプションとして渡したい値を指定します。

以下のように指定することで、ショート、ロングバージョンのどちらでもオプションを指定できるようになります。

require 'optparse'
opt = OptionParser.new

opt.on('-a', '--all') {|v| p v }
opt.on('-b', '--before') {|v| p v }
opt.parse(ARGV)
p ARGV
$ ruby sample.rb --all foo --before bar
true
true
["--all", "foo", "--before", "bar"]

ヘルプを見てみてもショート、ロングバージョンのどちらも表示されていることがわかります。

$ ruby sample.rb -h
Usage: sample [options]
    -a, --all
    -b, --before

オプションの説明を追記する

ヘルプを表示したときに使えるオプションだけではなく、そのオプションの説明を追加したいと思います。 その場合は、ロングオプション同様にonメソッドに説明文を渡してあげればOKです。

require 'optparse'
opt = OptionParser.new

opt.on('-a', '--all', 'description:a') {|v| p v }
opt.on('-b', '--before', 'description:b') {|v| p v }
opt.parse(ARGV)
p ARGV

ヘルプオプションを指定すれば説明文が表示されるようになります。

$ ruby sample.rb -h
Usage: sample [options]
    -a, --all                        description:a
    -b, --before                     description:b

どのオプションが指定されたか記憶する

OptionParserはどのオプションが指定されたかは記憶していません。そのため、どのオプションがコマンドラインで渡されてきたか覚えておく必要がある場合は、コンテナに格納しておきます。

require 'optparse'
opt = OptionParser.new

option = {}
opt.on('-a', '--all', 'description:a') {|v| option[:a] = v }
opt.on('-b', '--before', 'description:b') {|v| option[:b] = v }
opt.parse(ARGV)
p ARGV
p option

オプションが指定された場合は、コンテナにkey,valueが保存されます。オプションが指定されなかった場合は、nilが返ります。

$ ruby sample.rb -a foo -b bar
["-a", "foo", "-b", "bar"]
{:a=>true, :b=>true}

$ ruby sample.rb -a foo
["-a", "foo"]
{:a=>true}

オプションに引数が必須か否か指定する

引数を必須にする場合は、onメソッドでオプション登録するときに末尾に何かしら文字列を追加します。

require 'optparse'
opt = OptionParser.new

option = {}
# 引数なし
opt.on('-a', '--all', 'description:a') {|v| option[:a] = v }
# 引数必須
opt.on('-b VAL', '--before', 'description:b') {|v| option[:b] = v }
# 引数必省略可能
opt.on('-c [VAL]', '--ccc', 'description:c') {|v| option[:c] = v }
opt.parse!(ARGV)

引数必須な場合に引数を渡さないと以下のようなエラーが発生します。

$ ruby sample.rb -b
sample.rb:8:in `<main>': missing argument: -b (OptionParser::MissingArgument)

また、引数をオプションと一緒に渡すことで引数をそのままHashにkeyとvalueを格納できます。 引数なしのオプションには真偽値が返り、オプションb,cに引数を渡すと引数がHashに入ります。

$ ruby sample.rb -a -b test -c test
{:a=>true, :b=>"test", :c=>"test"}

受け取る引数のクラスを指定する

あるオプションの引数で受け取るクラスを限定したい場合は、以下のように記述します。

require 'optparse'
opt = OptionParser.new

option = {}
# 引数はIntegerのみ
opt.on('-a', '--all=N', Integer, 'description:a') {|v| option[:a] = v }
# 引数はStringのみ
opt.on('-b', '--before=S', String, 'description:b') {|v| option[:b] = v }
opt.on('-c [VAL]', '--ccc', 'description:c') {|v| option[:c] = v }
opt.parse!(ARGV)
p option

Intergerクラスを指定すると、文字列が引数として渡ってきたらエラーを返します。また、Stringクラスを指定すると、数値を引数に渡した場合でも文字列として受け取ります。

$ ruby sample.rb -a 1 -b aaa
{:a=>1, :b=>"aaa"}

$ ruby sample.rb -a aaa
sample3.rb:7:in `<main>': invalid argument: -a aaa (OptionParser::InvalidArgument)

$ ruby sample.rb -b 1
{:b=>"1"}

便利なARGVの機能

今まで紹介した方法でコマンドのオプションや引数を処理することが可能になりますが、もう少し楽にオプションや引数を扱えるのがOptionParser::Arguableモジュールのgetoptsメソッドです。 getoptsメソッド使うとワンラインでコードでオプションと引数を指定することができ、受け取ったものをHashとして受け取ることができます。

getoptsメソッドにはショートオプション、またはロングオプションを指定することができます。引数が必要なオプションにはコロンをオプションの後ろにつけます。オプションはopt.onのときと異なり、1つ1つ指定するのではなくまとめて記述するので、ぱっと見わかりずらいかと思いますが、オプションや引数をシンプルに扱うだけであればgetoptsメソッド使うととても楽になります。

require 'optparse'

# 引数なしのオプションa, 引数ありのオプションbを指定
params = ARGV.getopts("ab:")
p params
$ ruby sample2.rb -h
Usage: sample2 [options]
    -a
    -b VAL

$ ruby sample2.rb -a -b bbb
{"a"=>true, "b"=>"bbb"}

ロングオプションを使いたい場合も同様に引数が必要なオプションにはコロンをオプションの後ろにつけますが、ショートオプションのときのようにまとめてオプションは指定せず、分けて書きます。ショートオプションが不要な場合は、""のように空文字を渡してあげる必要があります。

require 'optparse'
# ショートオプションなし、ロングオプション引数なし or あり
params = ARGV.getopts("", "all", "before:")
p params
$ ruby sample2.rb -h
Usage: sample2 [options]
        --all
        --before=VAL

$ ruby sample2.rb --all --before 1
{"all"=>true, "before"=>"1"}

また、オプションのデフォルト値も指定することが可能です。

require 'optparse'
# ショートオプションなし、ロングオプション引数あり、デフォルト値あり
params = ARGV.getopts("", "all", "before:", "maxsize:1024")
p params
$ ruby sample2.rb -h
Usage: sample2 [options]
        --all
        --before=VAL
        --maxsize=1024

$ ruby sample2.rb
{"all"=>false, "before"=>nil, "maxsize"=>"1024"}

デフォルト値はコマンド実行時に上書きすることも可能です。

$ ruby sample2.rb --maxsize=100
{"all"=>false, "before"=>nil, "maxsize"=>"100"}

まとめ

実際に調べて使ってみた感想としては、こんな感じです。

  • optparse使うと自動的にヘルプオプションついてるの助かる(コマンドのバージョン表示オプションvもついてるらしい)
  • 引数クラス指定できるので、後の処理で引数のクラスどうこう考えなくて良いの助かる
  • 細かくオプション定義する必要がなければ、getoptsメソッドを使うとめっちゃ楽になる

なんとなく理解して使っていたライブラリも調べてみると新しい発見もあり理解度があがるので、また気になるライブラリ等あれば深掘りする時間を取ろうと思いました。

参考文献

docs.ruby-lang.org

docs.ruby-lang.org

aws-cli + pecoを使ってセッションマネージャー接続を楽にする(aws-vault ver.)

aws-cliを使ってEC2インスタンスに接続するの面倒くさいので、簡単に接続できるようにMakefileを書きました。 タグで特定のenvironmentの稼働中インスタンスの名前とインスタンスIDをpecoに渡して、選択したインスタンスIDをHOSTに入れてます。

ssm-ec2:
        echo "start session manager in ec2..."
        $(eval HOST = `aws ec2 describe-instances --region ap-northeast-1 --output json --filters "Name=instance-state-code,Values=16" --filters "Name=tag:environment,Values=xxx" | jq -r '.Reservations[].Instances[] | [.Tags[] | select(.Key == "Name").Value][] + "\t" + .InstanceId'  | sort | peco | cut -f 2`)
        aws ssm start-session --target $(HOST)

aws-vaultを使うとこんな感じ。

aws-vault exec <profile> -- make ssm-ec2

おしまい。