ようへいの日々精進XP

よかろうもん

AWS WAF の IPSet を管理するコマンドラインツール wafoo のそれから... 〜 gem 化してリリースしてみた 〜

これは

qiita.com

初老丸 Advent Calendar 2018 第 5 日目の記事になる予定です.

tl;dr

以前に以下のような記事を書きました.

inokara.hateblo.jp

AWS WAF の IPSet をコマンドラインで操作するツールを雑に作りはじめていて, その後, 放置気味だったのを改めて見直して gem 化してリリースしてみました.

wafoo | RubyGems.org | your community gem host

使い方自体は特に変わっておらず, export コマンドで指定した IPSets の IP アドレスリストを書き出して, 必要に応じて追加, 修正, 削除を行い apply コマンドで登録する流れです. 登録前に --dry-run で確認出来るので, 実際に運用する場合には --dry-run で確認してから apply という流れになると思います.

以前と変わったところ

IPSet 一覧表示

AWS SDK for Ruby のドキュメントを見ると, Aws::WAFAws::WAFRegional という 2 つのモジュールが提供されています. waf と waf regional の違いは, 前者が CloudFront に付与する WAF で, 後者が ELB に付与する WAF で別けられるようです. これらを一緒に一覧表示するようにしてみました.

$ bundle exec wafoo list
+-------------+--------------------------------------+----------+
| Type        | IPSets ID                            | Name     |
+-------------+--------------------------------------+----------+
| WAFRegional | xxxxxxxx-1234-5678-9000-xxxxxxxxxxxx | sample1  |
| WAFRegional | xxxxxxxx-1234-5678-9000-yyyyyyyyyyyy | sample2  |
| WAF         | xxxxxxxx-1234-5678-9000-zzzzzzzzzzzz | sample3  |
+-------------+--------------------------------------+----------+

とりあえず, こんな感じで.

オクテットではない CIDR boundary のサポート

AWS WAF より、2つの新機能のご紹介です。

dev.classmethod.jp

従来の AWS WAF の IPSet では, IP アドレスの指定は /8, /16, /24, /32 での CIDR 指定しか出来ませんでした. その為, これらのオクテット以外の IP アドレス (例えば, /29 とか) を登録した場合には, /32に分割して登録するような処理をいれていましたが, 非オクテット CIDR boundary のサポートに伴い, この処理が不要となりました.

現在では, コメントアウトしていますが, 以下のような処理を行っていました.

...
      # def split_cidr(ipset)
      #  addr = NetAddr::CIDR.create(ipset)
      # addr.enumerate
      # end
...
      # unless %w(8 16 24 32).include?(ipset.split('/').last)
      #   ips = split_cidr(ipset)
      #   ipsets_array = []
      #   ips.each do |ip|
      #     ipsets_array << {
      #                        action: 'INSERT',
      #                        ip_set_descriptor: {
      #                          type: 'IPV4',
      #                          value: ip + '/32'
      #                        }
      #                     }
      #   end
      #   return ipsets_array
      # end
...

汚いコードしか書けない自分にとっては, コードを減らせるというのはとても嬉しいです.

べた書きコードを少しずつ...

今後のメンテンスを考慮して, べた書きしていたコードを少しずつ機能毎に分割したり, テストを追加しています.

以上

wafoo の紹介でした. コマンド実行時のメッセージ出力や README の英語がアレな感じで恐縮ですが, 何卒, ご容赦ください.

よろしければ, ご利用いただきましてフィードバック等いただけると幸いです.

Ruby の YAML ライブラリで YAML をパースする vs オレオレ YAML パーサーで YAML をパースする 〜 この戦いは止むる (YAML) ことは出来ない 〜

これは

qiita.com

初老丸 Advent Calendar 2018 第 4 日目の記事になる予定です.

tl;dr

前々回, 以下のような記事を書きました.

inokara.hateblo.jp

hub という Github クライアントライブラリの長い歴史の中で, Ruby 標準の YAML ライブラリの変わりにオレオレ YAML パーサーが実装されていて, そのパーサーで解析出来ない YAML ファイルを書いてハマってしまったという話です.

github.com

コミットメッセージから察するに, コマンドラインツールにおいて, 設定ファイルの YAML ファイルを読み込む時間は 1 ミリ秒でも速くしたかった思いが伺えます.

では, どのくらい処理時間に差があるのか

調べてみました

というのが本記事です. 本記事では以下のような環境で検証を行います.

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.6
BuildVersion:   17G65

$ ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]

まず, 以下のようなスクリプトをこさえて雑に YAML ファイルを生成してみます.

require 'erb'

contents =<<"EOS"
github.com:
<%- (0..100000).each do |id| -%>
  - user: foobar<%= format('%04d', id) %>
    oauth_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<%= format('%04d', id) %>
<% end %>
EOS
erb = ERB.new(contents, nil, '-')
puts erb.result(binding).chomp

これを実行して YAML ファイルを生成します.

$ ruby generate-yaml.rb > sample.yml

以下のような無いようの YAML ファイルが生成されました.

github.com:
  - user: foobar0000
    oauth_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0000
  - user: foobar0001
    oauth_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0001
...
  - user: foobar99999
    oauth_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx99999
  - user: foobar100000
    oauth_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx100000

hub で利用する YAML ファイルのフォーマットそのままに 100000 ユーザー分のユーザー名 (user) とトークン (oauth_token) が生成されているはずです. (実際に 100000 ユーザー分を記載することは無いと思いますが...) これを sample.yml という名前で保存しておきます.

ruby-prof

調査には標準の profiler ライブラリではなく, ruby-prof という gem を利用しました.

github.com

profiler と ruby-prof の違いについては, ドキュメントや別の記事にお譲りしますが, ざっと使ってみた感じだと, プロファイルの実行速度や結果出力について profiler よりも優れているように思えます. ざっくりで恐縮ですが, ruby-prof の使い方は以下のように RubyProf.profile ブロック内に解析したいコードを差し込んで結果を RubyProf の各種プリンタクラスで表示します.

require 'yaml'
require 'ruby-prof'

result = RubyProf.profile do
  YAML.load_file('sample.yml')
end

printer = RubyProf::FlatPrinter.new(result)
printer.print(STDOUT, { :sort_method => :total_time })

上記の例だと, YAML.load_file('sample.yml') の解析結果が変数 result に格納され, RubyProf::FlatPrinter クラス (出力例はこちら) を使って結果を出力しています. 出力先として STDOUT に, total_time (対象のメソッドとそのメソッドから呼び出されたメソッドを含めた総処理時間) 順にソートされて出力します. 以下, 出力例です.

Measure Mode: wall_time
Thread ID: 70181951114860
Fiber ID: 70181959375120
Total: 0.119640
Sort by: total_time

 %self      total      self      wait     child     calls  name
  0.03      0.120     0.000     0.000     0.120        1   [global]#[no method]
  0.00      0.120     0.000     0.000     0.120        1   <Module::Psych>#load_file
  0.01      0.120     0.000     0.000     0.120        1   <Class::IO>#open
  0.01      0.119     0.000     0.000     0.119        1   <Module::Psych>#load
  0.00      0.066     0.000     0.000     0.066        1   <Module::Psych>#parse
...

尚, 出力は上記のテキスト出力だけではなく, HTML や graphviz で画像に書き出せる DOT 言語でも書き出すことが可能です. 詳細は README をご一読ください.

標準の YAML ライブラリを ruby-prof で解析してみる

コード

以下のようなコードで検証してみます.

require 'yaml'
require 'ruby-prof'

RubyProf::measure_mode = RubyProf::PROCESS_TIME
result = RubyProf.profile do
  YAML.load_file('sample.yml')
end

printer = RubyProf::FlatPrinter.new(result)
printer.print(STDOUT, { :sort_method => :self_time })

以下のように実行します.

$ bundle exec ruby parser1.rb > result1.txt

実行結果

以下のような結果となりました.

$ head -15 result1.txt
Measure Mode: process_time_time
Thread ID: 70238351920700
Fiber ID: 70238356042040
Total: 57.293642
Sort by: self_time

 %self      total      self      wait     child     calls  name
  7.07      5.715     4.050     0.000     1.665   400005   Psych::ScalarScanner#tokenize
  6.79      3.892     3.892     0.000     0.000   400005   Psych::Nodes::Scalar#initialize
  6.75      3.867     3.867     0.000     0.000   600014   Psych::TreeBuilder#event_location
  6.45     14.876     3.694     0.000    11.181   400005   Psych::TreeBuilder#scalar
  5.66     11.522     3.241     0.000     8.281   400005   Psych::Visitors::ToRuby#deserialize
  4.81     27.601     2.757     0.000    24.844        1   Psych::Parser#parse
  4.48      6.035     2.566     0.000     3.470   400005   Psych::TreeBuilder#set_location
  4.06      2.326     2.326     0.000     0.000   500009   Psych::TreeBuilder#set_start_location
... 以下略

処理時間は Total: を確認すると良さそうです. 約 57 秒掛かっていることが分かります. また, Psych::ScalarScanner#tokenize というメソッドが 400005 回コールされて, 4 秒程処理に要していることが分かります. 1 回のコールあたり 9 マイクロ秒の時間で処理されていることになります. この数字だけ見ると遅いという印象は特に感じません.

ただ, YAML が解析されるまでに非常に多くのメソッドが呼ばれていることが結果から読み取れます. これを ruby-prof を利用することで可視化することが可能です. 検証コードを以下のように修正して Graphviz で読み取れる DOT 言語で結果をプリントするようにします.

require 'yaml'
require 'ruby-prof'

RubyProf::measure_mode = RubyProf::PROCESS_TIME
result = RubyProf.profile do
  YAML.load_file('sample.yml')
end

printer = RubyProf::DotPrinter.new(result)
printer.print(STDOUT, { :sort_method => :self_time })

以下のように実行して, Graphviz で画像に変換してみます. (事前に Graphviz をインストールしておきます.)

$ bundle exec ruby parser1.rb > result1.dot
$ dot -Tpng result1.dot -o result1.png
$ open result1.png

以下のように多くのメソッドが呼ばれていることが解ります. すごい.

f:id:inokara:20181128152125p:plain

オレオレ YAML パーサーを ruby-prof で解析してみる

コード

以下のようなコードで検証してみます.

require 'yaml'
require 'ruby-prof'

def yaml_load(string)
  hash = {}
  host = nil
  string.split("\n").each do |line|
    case line
    when /^---\s*$/, /^\s*(?:#|$)/
      # ignore
    when /^(.+):\s*$/
      host = hash[$1] = []
    when /(^[- ]) (.+?): (.+)/
      key, value = $2, $3
      host << {} if $1 == '-' or $2 =~ /^\s*-\s*/
      key.gsub!(/^\s*-\s*|^\s*/, '')
      # require 'byebug'; byebug
      host.last[key] = value.gsub(/^'|'$/, '')
    else
      raise "unsupported YAML line: #{line}"
    end
  end
  hash
end

RubyProf::measure_mode = RubyProf::PROCESS_TIME
result = RubyProf.profile do
  File.open("sample.yml", "r") do |f|
    yaml_load(f.read)
  end
end

printer = RubyProf::FlatPrinter.new(result)
printer.print(STDOUT, {})

以下のように実行します.

$ bundle exec ruby parser2.rb > result2.txt

実行結果

以下のような結果となりました.

$ cat result2.txt
Measure Mode: process_time_time
Thread ID: 70351828807260
Fiber ID: 70351829107000
Total: 7.317221
Sort by: self_time

 %self      total      self      wait     child     calls  name
 49.76      7.242     3.641     0.000     3.601        1   Array#each
 27.34      2.001     2.001     0.000     0.000   800011   Regexp#===
 12.90      0.944     0.944     0.000     0.000   200002   String#gsub!
  5.46      0.400     0.400     0.000     0.000   200002   String#gsub
  3.50      0.256     0.256     0.000     0.000   200002   Array#last
  0.91      0.067     0.067     0.000     0.000        1   String#split
  0.12      0.009     0.009     0.000     0.000        1   IO#read
  0.00      0.000     0.000     0.000     0.000        1   File#initialize
  0.00      7.317     0.000     0.000     7.317        1   [global]#[no method]
  0.00      7.317     0.000     0.000     7.317        1   <Class::IO>#open
  0.00      7.308     0.000     0.000     7.308        1   Object#yaml_load
  0.00      0.000     0.000     0.000     0.000        1   IO#close
  0.00      0.000     0.000     0.000     0.000        1   IO#closed?

* indicates recursively called methods

トータルの処理時間は 7.3 秒. Regex クラス=== メソッドが 800011 回コールされていることが分かります. === メソッドは左辺と右辺の String を正規表現マッチするメソッドなので, オレオレ YAML パーサーでは愚直に文字列比較を行って YAML を解析していると思われます. 処理時間は YAML ライブラリの 8 倍程度速く処理出来ていますので, hub の作者が嘆いていた理由は分かりました.

一応, YAML ライブラリと同様に, 呼ばれているメソッドを可視化したところ, 以下の通りとなりました.

f:id:inokara:20181128152402p:plain

YAML ライブラリと比較すると実にシンプルであることが分かりました.

以上

YAML ライブラリと hub で実装されていたオレオレ YAML パーサーの処理時間の比較を通して, ruby-prof を用いて, どのような処理が行われているかを把握し, 可視化することが出来ました.

速度を比較した結果は YAML ライブラリの方がオレオレ YAML パーサーよりも処理時間が長くなりましたが, これを一概に遅いと言えないと思いますし, 多くのメソッドが呼ばれるにはそれなりの意味があると考えています.

今回はこれ以上は踏み込んで調査は行いませんが, 機会があれば YAML ライブラリのソースコードをちゃんと読んで理解を深めたいと思います.

有難うございました.

2018 年 11 月 28 日 (水)

ジョギング

  • 引き続き, 左足踵に痛みがあったのでお休み
  • 湿布を張っていたらカブれて辛い...
  • 休養だと思ってしっかりと直すことにする

日課

  • お休み
  • そろそろ, 再開する... まじで, まじで..., まじで..., まじで...

Alternative Architecture DOJO Offline #1

久しぶりにもくもく会でもなく, 自分が話すわけではない勉強会に参加した. Microservice の文脈で語られる k8s の話が 7 割くらい. k8s は勉強しておかなければと改めて思った. 今日の収穫の一つ.

全てのセッションにおいて, ツイートするのも忘れて聞き入ってしまったし勉強になった.

2018 年 11 月 27 日 (火)

ジョギング

  • 左足踵に痛みがあったのでお休み
  • 休養だと思ってしっかりと直すことにする

日課

  • お休み
  • そろそろ, 再開する... まじで, まじで..., まじで...

白菜ミルフィーユ鍋

実家から多くの野菜を送ってもらった. その中で大きな白菜があったので豚バラと白菜のミルフィーユ鍋を作った. 出しは利尻昆布からとった出汁を使ったので更に美味しかった.

re:Invent 2018

クラスメソッドさんのブログが速報性が高くて良い. 幾つか気になった記事を.

dev.classmethod.jp

dev.classmethod.jp

dev.classmethod.jp

2018 年 11 月 26 日 (月)

ジョギング

  • 山王公園往復 (だいたい 30 分くらい)
  • 引き続き, 左踵に痛み, 右足甲に痛み, 色々と痛みがあったのでゆっくりと走る

日課

  • お休み
  • そろそろ, 再開する... まじで, まじで..., まじで...

ランチ

引っ越して以来, ずっと気になっていたお鮨屋さんに. うっかり 1.5 人前をオーダーしたり, 赤出汁飲み放題につられて 2 杯も赤出汁を飲んでお腹いっぱいになった. お鮨の味はさておき, 大将と奥さんが気さくに話しかけてくださったのが嬉しかった.

ティファール

昨日, ヨドバシで購入したティファールを早速使ってみた.

驚きしかなかった.

2018 年 11 月 25 日 (日)

ジョギング

  • 山王公園
  • 左踵に痛み, 右足甲に痛み, 色々と痛みがあったのでゆっくりと走る

日課

  • お休み
  • そろそろ, 再開する... まじで, まじで...

博多にちょっと

2018 年 11 月 24 日 (土)

ジョギング

  • 産山村の民宿からやまなみハイウェイを少し登ったり, 阿蘇の外輪山を少し走ったりして 40 分
  • 朝 6 時半スタートだったけど, 手足が冷たすぎて走りづらかったが, 朝日がのぼり始めると少しずつ温まって, とても気分が良かった

日本一のマラソン練習コース

日本一のマラソン練習コースに来た。次回は走ろう。

近くまできたので, 日本一のマラソン練習コースに立ち寄った.

次回来た時には走ろうと思った次第.

2018 年 11 月 23 日 (金)

ジョギング

  • 移動の為, お休み
  • 左足踵の痛みが続く...

日課

  • お休み
  • そろそろ, 再開する... まじで

阿蘇

黒川温泉を更に山奥に進んでやまなみハイウェイを少し下ったところに良い温泉宿を見つけた.

産山温泉 奥阿蘇の宿 やまなみ

43 歳の誕生日に奥さんが予約してくれたお宿で秘境感満載でお湯もさることながら, 夕食で出してもらった阿蘇赤牛ステーキが最高だった.

最近の furikake 〜 ご飯のおかわりは 2 杯まで 〜

これは

qiita.com

初老丸 Advent Calendar 2018 第 2 日目の記事になる予定です.

tl;dr

先日, 作成した furikake について, 実際にギョームで使ってみて, 自分が欲しいなあと思った機能の追加や発見したバグ等をコツコツ修正しています.

github.com

furikake | RubyGems.org | your community gem host

ということで

トッピング機能

アドオン的な何か

詳細は README をご一読下さい.

github.com

カレントディレクトリに addons ディレクトリを作成して, addons ディレクトリ以下に Ruby スクリプトを放り込むと, Wiki に載せたいドキュメントを簡単に追加出来るようになってます. 尚, 以下のように furikake setup を実行した際に addons ディレクトリを作成するかどうか確認されますので, 合わせてご確認下さい.

$ bundle exec furikake setup
[setup] creating .furikake.yml...
[setup] .furikake.yml already exists.
[setup] create `addons` directory? [y/n]
y
[setup] creating `addons` directory.
[setup] `addons` directory created.

サンプル

以下 Ruby スクリプトの簡単なサンプルです. 変数 values[['value1', 'value2'], ['value3', 'value4']] と変数 contents のハッシュの構造だけを守っていただければ, お好きな情報を投稿することが可能になります.

module Furikake::Resources
  module Addons
    class Example
      def self.report(format = nil)
        values = [['value1', 'value2'], ['value3', 'value4']]
        contents = {
          title: 'Example',
          resources: [
            {
               subtitle: '',
               header: ['Title1', 'Title2'],
               resource: values
            }
          ]
        }
        Furikake::Formatter.shaping(format, contents)
      end

      def self.get_resources
        # ここは頑張って実装する必要があります
      end
    end
  end
end

addons ディレクトリにスクリプトを放り込むことで, 従来は AWS のリソースのみに対応していたところを, Azure や GCP 等の他のクラウドリソース等も Wiki に一覧として投稿することが可能になったはずです.

デーモン小暮

従来は

以下のようにコマンドラインから実行することしか出来ませんでした.

bundle exec furikake publish

cron や Fargate のタスク機能を利用することで自動的に定期投稿は可能でした. (実際には試せていませんが...)

デーモン化により

以下のように実行することで, furikake publish がバックグラウンドで実行されるようになります.

bundle exec furikake monitor --detach

定期的に (デフォルトは 3600 秒毎) リソースを一覧を取得して, 指定した wiki に情報を投稿します. この機能により, cron の設定等を行うことなくドキュメントの自動更新が可能となります.

有難うございます

デーモン化に伴い, 以下のコードをスーパー参考にさせて頂きました.

Ruby script testing to daemonize a ruby script. · GitHub

この場を借りまして, お礼を申し上げます. 有難うございます.

Docker 化

docker-compose を介して利用出来るように, 超簡単に Dockerfile を書き, docker-compose.yml を揃えました.

github.com

よろしければ, ご利用下さいませ.

リリースに octorelease を利用するようにしました

github.com

awspec でも利用させて頂いて, いい感じでリリースページを作ってもらえるので利用してみました.

以下のブログ記事が参考になります.

k1low.hatenablog.com

以上

furikake の更新情報でした. よろしければ, furikake を使って頂きましてフィードバック頂けると嬉しいです.

2018 年 11 月 22 日 (木)

ジョギング

  • 山王公園往復
  • 引き続き, 左の踵に痛みがあるのでペースは抑え気味で
  • スタートして雨が振り始め, ゴールして雨が止んだ (笑うしかない)

日課

  • お休み
  • そろそろ, 再開する

会食

  • お世話になる方々と会食
  • 色々と突っ込んだ話をさせて頂いて, 自分とそれほど歳が変わらないのに人間的な違いを感じてちょっと凹んだ
  • と言いつつも来年から頑張っていこうと思う

Mac

新しい Mac を新調することになり, 幾つかのレビューサイトをチェックしていて気づいたのは, 自分は Mac のことを何も知らないこと.

最新の macOS の名前くらいは判るが, Intel CPU のなんちゃらかんちゃらとか GPU がどーだこーだとか, このアプリは手放せない神アプリだとか, 検索結果に踊る文言を読んでいるとチンプンカンプンなので, どうしたもんだか... 久しぶりに MacFan とか雑誌を買ってみようかと思う.

とりあえず, メモリは大盛りの方が幸せになれるということは解っているものの, 後は, 今の自分の Mac の使い方やライフスタイルを鑑みて検討する必要があるなあ...くらいしか想像が及ばない.

何を基準に選べば良いのやら.