あきろぐ

いろいろめもするよ🐈🐈🐈

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

おしまい。

Terraform v0.12からv0.15にupgradeするときにつまったところ

やっとTerraformのアップグレードに手をつけました。 そのときにつまったときのメモを残しておきます。

リソースに存在しない属性がサポートされていない

aws_codedeploy_appリソースに存在しない属性がサポートされていないというエラーが0.13にアップグレードしたときにでました。 terraform state show aws_codedeploy_app.testで属性を確認してもunique_idは存在していません。 色々と調べたところ、AWSプロバイダv3のバグっぽかったので、AWSプロバイダを3.24->3.50.0まであげることで解決しました。

Instance aws_codedeploy_app.test data could not be decoded from the state:
unsupported attribute "unique_id".

github.com

プロバイダアドレスがレガシーで無効

terraform 0.13upgradeコマンドを実行しているのに、以下のエラーがでました。

Error: Invalid legacy provider address
This configuration or its associated state refers to the unqualified provider "aws".
You must complete the Terraform 0.13 upgrade process before upgrading to later versions.

よくよくUpgradeガイドを見てみると、v0.13以前とv0.14移行だとTerraformのステートスナップショットのフォーマットが異なるらしく、一度v0.13でterraform applyコマンドを差分がなくても実行しておく必要がありました。 このコマンドを実行し、v0.14にあげると上記のエラーは消えました。

Terraform v0.14 does not support legacy Terraform state snapshot formats from prior to Terraform v0.13, so before upgrading to Terraform v0.14 you must have successfully run terraform apply at least once with Terraform v0.13 so that it can complete its state format upgrades.

www.terraform.io

ignore_changesが無視されてリソースを削除しようとする

これは、v0.13->v.014にアップグレードするときに遭遇したエラーです。こちらも調べたところv0.14.0のバグでした。 ignore_changesのマップキーが複数存在すると、一部がignore_changesの対象にならないっぽいです。 v0.14.1のbug fixによってこの問題は解消されているので、アップグレードすることで解決しました。

github.com

AWS WAFv2導入でつまずいた&考慮したほうがいいところ(rate limit編)

概要

AWS WAFv2を実際に使って検証したので、その際につまずいたところや考慮したほうがいいところなどまとめます。

今回はレートベースのルールを使ってリクエストの発生元の IP アドレスが同一のものからリクエストが閾値以上になったらブロックする設定にしました。 レートベースのルールは、5分間あたり100リクエストから閾値に設定することが可能となっているので、低頻度のクローラー対策にも対応できます。 aws.amazon.com

構成

今回はWAFをCloudfrontにアタッチする想定で検証しました。 AWSリソースは、Terraformでコード管理します。 f:id:akngo22:20210707211920p:plain

Terraform

Terraformコードサンプルはこちらです。WAF Classicのときよりシンプルに設定が書けるようになっています。

github.com

今回はCloudfrontにWAFをアタッチするので、aws_wafv2_web_aclリソースのprovider設定をprovider = aws.eastするのを忘れないようにしましょう。 忘れがちなのですが、WAFのログ設定をするときも同じくproviderはaws.eastにする必要があるので注意です。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration

つまずいたところ

デフォルトのアクションをBlockにしたままだった

本当に些細な部分を見落としていました。WebACLのアクションは、WAFをアタッチしたときのデフォルトのアクションとルール上のアクションは分けて設定するのですが、デフォルトアクションをBlockにしたままCloudfrontにアタッチしてしまったので、反映完了後ページ閲覧ができず403エラーが返ってくるようになっていました。

デフォルトのアクションがBlockのままだとルールが適用されない場合にアクセスがブロックされる設定になっているので、デフォルトアクションはAllowでルール上でBlockアクションを選択する必要があります。

docs.aws.amazon.com

考慮したほうがいいこと

既に運用中のサービスに適用する場合は、まずCountアクションで様子見する

運用中のサービスが急に閲覧できなくなるといった恐ろしい事象をひかないためにも、一度デフォルトアクションはAllow、ルール上のアクションはCountにして様子見したほうがいいと思います。WAFのリソース反映直後は、ルールが実際に適用されるまでにタイムラグがあるらしいので一旦寝かしておくと良さそうです。 1日程度様子見すると、ルールにひっかかったIPアドレスがSampledRequestで確認できるので、これらが問題なクローラーかどうか確認できると思います。(WebACLのルールでsampled_requests_enabledを有効にすれば確認できます)

AWS CLIを使ってカウントされたざっと取り出すこともできます。 ※CLI使う時もregionオプションで--region us-east-1を指定する必要があります

aws wafv2 get-sampled-requests --web-acl-arn <arn> --rule-metric-name <rule-name> --scope CLOUDFRONT --time-window StartTime=yyyy-mm-ddThh:mmZ,EndTime=yyyy-mm-ddThh:mmZ --max-items 500 --region us-east-1 | jq -r .SampledRequests[].Request.ClientIP | sort | uniq > ip.txt

docs.aws.amazon.com

閾値しきい値に設定する場合は、正規のクローラーがブロックされないか確認する

既にサービスを運用中であれば、Googlebotからのリクエストがサイトに来ていると思うので、収集しているログからGooglebotの頻度がどの程度が念の為に確認しておくと良いと思います。

公式ドキュメントにUserAgentの文字列が記載されているので、ログ検索すればある程度Googlebotからどれくらいの頻度でリクエストが来ているのか、設定しようとしているルールの閾値にひっかからないか確認しておきましょう。 developers.google.com

閾値にもタイムラグがある

WebACLのルールの検証をする際に、何回目からリクエストブロックされ403エラーがでるのか検証したのですが、閾値きっかりにブロックされることはありませんでした。

1秒間隔でリクエストを同一IPから送り続けたところ、約30-50回程度閾値から上回った値で403エラーが返りました。1秒間隔より短い頻度でリクエストを送ると閾値から約100回程度上回った値でないとブロックされなかったので、高頻度になるとタイムラグが大きくなる可能性がありそうです。

おわり!