くりにっき

フルスタックキュアエンジニアです

プリパラのライブにおける緊急対応事例 #ノンシュガー

起きたこと

3/6(土)の NonSugar スペシャルイベント「約束のてへペロピタですわ!」byプリパラ の昼夜公演に現地勢として参加してたのですが、昼公演の楽曲中に演者の1人の山下七海さんが 歌ってる最中にダッシュでステージ裏にはけた。

現地で見てた側としてはまさにこういう心境でした。

dic.pixiv.net

昼公演終了の段階では「ペッパー *1 だしそういう演出の可能性もあるかもしれないな」という感想だった。

ちなみに夜公演だと途中退場はなかったのでガチなトラブルだったらしい。

真相

これらしい。

家に帰って配信アーカイブを見返してたんだけど、確かに山下さんのブーツの紐が片方解けてステージ上で自分で外そうとした様子が映っていた。(ステージ降りた後の再入場時にはブーツの紐っぽいのがなくなってた)

配信アーカイブを見返しての感想

昼公演の様子がむっちゃ気になったので家に帰ってから配信チケットを買ってアーカイブを見た。

山下さんが歌いながらブーツを外そうとして苦労してるのは確認。

だけど山下さんがダッシュで抜けた様子は全くカメラに映ってなくて、NonSugarが一時的に2人でライブしてる間はカメラをむっちゃズームにしてステージ全体が映らないようになってた。完全に推測だけどステージ全体が映ると3人ユニットのNonSugarが2人しかいないことに配信側が気づくので現場の一瞬の判断のカメラワークだと思う。

山下さんが抜けてる最中に山下さんのコールのパートがあったのだけど残り2人が一緒にコールしてた。その直後に山下さんが再入場して歌唱再開したのですがその時にはブーツの紐っぽいのが両足ともなくなっていた。

2曲終わってMCタイムになった時に山下さんが再度ステージから抜けて戻ってきた時にはブーツの紐っぽいのが両足ともついてたので、楽曲やってる数分間でスタッフがブーツの緊急対応をやったのだと思う。

結果としてNonSugarは歌唱中にトラブルが起きて一瞬2人になったけども楽曲を止めることなく歌いきった。

時間にしたらほんの数分間の出来事なんだけどかなり情報量が多かった。

軒並みですが演者だけでなく関係者一同対応乙という感想しかないです。

サービス継続不可能になりそうな障害時にも周囲が一瞬の判断で協力して緊急対応してダウンタイムやユーザ影響を最小にしてサービスを継続するのは日々の業務に通じるものがあると思いました。

ちなみにアーカイブ配信は3/7までで円盤にも夜公演しか収録されないのでこの緊急対応内容はお蔵入りになりそう。

*1:プリパラ内で山下七海さんが演じる野生児キャラ

ItamaeのCIをTravis CIからGitHub Actionsに移行した

tl;dr;

github.com

前置き:ItamaeのCIの式年遷宮の歴史

僕がコミッタになる前はWercker + Vagrant + DigitalOceanを利用

僕とうなすけさん( id:yu_suke1994 )がコミッタになったタイミングでTravis CI( travis-ci.org ) + Dockerに移行

sue445.hatenablog.com

先日うなすけさんが travis-ci.com に移行

blog.unasuke.com

移行のモチベーション

Itamaeのリポジトリでは週1のweekly buildを実行してるのですが、1回辺りのビルド時間が長くて無料枠のクレジットを使い切ったためです

f:id:sue445:20210217214520p:plain

https://docs.travis-ci.com/user/billing-faq/#what-if-i-am-building-open-source を読むとOSSなら無料でクレジットを付与するのでメールしてねって書いてるので連絡したのですが、1週間以上経っても返事がこなかったので諦めてGitHub Actionsに移行しました。

メール送った直後にzendeskからの自動返信メールが届いたので向こうに届いてないってことはないはずなんだけどなぁ。。。(もちろんTravisからのメールが迷惑メールに振り分けられてもないです)

ハマったこと

ItamaeのCIだと docker run -d で起動したコンテナに対してItamaeを実行しているのですが、Itamae実行中にtmpディレクトリの中身が消えたりファイルが作れないという事象のせいでビルドがコケる現象が頻繁に発生してました。

代表例はこれ。/tmp/itamae_tmp/ が存在しなくてエラーになってそうなんだけど、Itamaeの処理の冒頭で /tmp/itamae_tmp/ を作ってるのにそうはならんやろ。。。

 INFO :   execute[echo -n 3 >> /tmp/subscribes] executed will change from 'false' to 'true'
ERROR :     stderr | mkdir: cannot create directory '/tmp/itamae_tmp/1613488120.203623': No such file or directory
ERROR :     Command `mkdir /tmp/itamae_tmp/1613488120.203623` failed. (exit status: 1)
ERROR :   remote_file[/tmp/remote_file] Failed.

https://github.com/sue445/itamae/runs/1911563358?check_suite_focus=true

/tmp がおかしそうというのはあたりがついてたのでパーミッション変えてみたりホスト側から /tmp をmountしてみたりと色々やってみたのですが、最終的には https://github.com/itamae-kitchen/itamae/pull/335/commits/69d6dfb13688609cfd96193122ae173b7c712da6 で解決しました。

良かったこと

  • GitHub Actionsの並列数で殴る構成にしたので1回あたりのビルド時間が7分から3~4分になった

悪かったこと

  • YAMLが41行から200行に増えた。(つらい)
    • いい加減アンカーとマージを使えるようにしてほしい
    • CircleCIならもっとリファクタリングできるんだけどなぁと思いながらYAMLを書いてた

VagrantとVirtualBoxは別物

Twitterを見てるとたまにVagrantVirtualBoxを混同してる発言があってその度にイラッとしてるのでメモ。

tl;dr;

  • DockerとVagrantはカバーしてる分野が異なるツールなので比較するのは間違い
  • DockerとVagrantを比較してる場合、9割くらいの確率でVagrantVirtualBoxを混同している可能性が高い

雑な説明

VirtualBoxとは

www.virtualbox.org

WikiPedia より引用

Oracle VM VirtualBox (オラクル ブイエム バーチャルボックス)とは、x86ならびにAMD64/Intel64にかかる仮想化ソフトウェアパッケージの一つ。

要はVMの実行環境です。

似たような領域をカバーするツールとしての比較対象は下記

Vagrantとは

www.vagrantup.com

WikiPedia) より引用

仮想機械の提供自体は、VirtualBoxをはじめとする仮想化ソフトウェアが行う。Vagrantは、これらソフトウェアが用意されているのを前提として、仮想機械の設定や立ち上げに特化している。

要は色々な種類のVMの操作を同一の設定ファイルとインターフェースで操作するためのソフトウェア。

当たり前ですがEC2とGCEとではAPIが全く違うので、それらをVagrantを介して同じコマンドでVMの起動や停止ができるのは便利です。

Vagrantが提供してるのは各VMの操作をするためのインターフェースの部分だけなので VirtualBoxやEC2などのVMの実行環境そのものは提供していません。

Vagrant単体で使うことはなく、

のように各VM環境に対応したproviderと組み合わせて使うことが多いです。

ここで注意してほしいのはVagrantにはDockerのproviderもある(Vagrant + Dockerの組み合わせがある)ことです。 *2

www.vagrantup.com

つまりDockerとVagrantは対立するものではないし、ツールとしての性質が違うので比較するのが間違いです。

Vagrantと似たような領域をカバーするツールを敢えて言うと(Dockerではなく)Docker ComposeやDocker Machineだと思います。(異論は認める)

Vagrant + VirtualBoxがよくある組み合わせだと思うのだけど、そのせいでVagrantVirtualBoxが混同されている気がします。

よくある間違い

  • Vagrantはオワコン、時代はDocker!
    • Vagrantが下火なのは否定しないが比較対象が違う
    • VirtualBoxとDockerを比較してるなら話が通じる
  • 開発環境をVagrantからDockerに移行した
    • 「開発環境をVagrant( + VirtualBox)からDocker( + Docker Compose)に移行した」ということを言いたいんだろうけど、なんかもにょる

*1:VMとコンテナも別物なんだけどアプリケーションの実行環境という点では共通してるので比較対象に含めています

*2:ドキュメント読む限りあまり推奨されてない感はあるが

tanuki_reminder 0.6.0をリリースした

gitlab.com

GitLab 13.7でMergeRequestのReviewers*1が無料版に入ってきたので対応しました。

about.gitlab.com

Assignsの他にReviewersにもメンションが飛ぶようになっています。

f:id:sue445:20210203085006p:plain

CHANGELOG全文 https://gitlab.com/sue445/tanuki_reminder/-/blob/master/CHANGELOG.md#anchor-060

*1:GitHubのReviewersと同じ

sentry-ravenを使ってるアプリは基本的にsentry-rubyに移行した方がよさそう

tl;dr;

経緯

正月休み中にRubyで趣味アプリを作っててSentryを導入しようとしたところ、いつもと違うセットアップ手順だったので気づきました。

詳しいこと

v3までがsentry-ravenでv4以降がsentry-rubyらしいです。

f:id:sue445:20210127203822p:plain

https://rubygems.org/gems/sentry-raven/versions

f:id:sue445:20210127203753p:plain

https://rubygems.org/gems/sentry-ruby/versions

先月上旬にはv4が出ていたらしい。

dependabotとかで自動bundle updateしてるだけだと絶対に気づかないやつ...

古いsentry-ravenについて

https://github.com/getsentry/sentry-ruby/tree/master/sentry-raven によると

Future Support: sentry-raven has entered maintenance mode, which means it won't receive any new feature supports or aggressive bug fixes.

とのことなので移行した方がよさそうです

新しいsentry-rubyについて

github.com

移行方法は https://docs.sentry.io/platforms/ruby/migration を参照。

注意点

社だとSaaS版ではなくオンプレミス版のSentryを自前でホスティングして運用してるのですが、社で使っているSentryのバージョンが古くてsentry-rubyが動かなくてハマりました。

詳しくは下記のissueを参照

github.com

https://github.com/getsentry/sentry-ruby/issues/1185#issuecomment-755441492 によると

in the new SDK we've switched the reporting endpoint from /store to /envelope, which requires 20.6.0 and above version if you use the on-premise installation.

とのこと。

余談:投げたパッチ一覧

同じgemでメジャーバージョンが上がったら気づくけどgemの名前が変わるとさすがに移行に気づかないので、gem install 後にdeprecation messageを出すPRを投げました。

github.com

【追記】マージされて3.1.2に入りました。

https://github.com/getsentry/sentry-ruby/blob/master/sentry-raven/CHANGELOG.md#312

あとついでに rake build がコケる問題もついでに直してPR投げました。

github.com

上記パッチは2つともマージはされているんだけど、deprecatedなgemなのでいつリリースされるかは不明です

あとsentry-rubyの方も使ってて気になった箇所があったのでPR投げました。

github.com

こっちは4.1.5のマイルストーンに入ってるので順調に行けば次バージョンに入るかなと思ってます

マージされて4.1.5に入りました。

https://github.com/getsentry/sentry-ruby/blob/master/sentry-ruby/CHANGELOG.md#415

Cloud FunctionsでRubyを使う時はdevelopmentやtestのgemはインストールされない

前置き

Cloud Functionsにデプロイする時には gcloud functions deploy を使います。

そこで bundle install も自動で実行されるのだけど、その時に --without development test 的なことをやってくれるのか調べてみました。(調べた範囲では公式ドキュメントには記載はなかった)

検証コード

個人アプリから適当に抜粋

Gemfile

# frozen_string_literal: true

source "https://rubygems.org"

ruby "~> 2.7.0"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "functions_framework"
gem "google-cloud-firestore"
gem "google-cloud-secret_manager"
gem "moji"
gem "parallel"
gem "sentry-ruby"
gem "slack-notifier"
gem "tweet_sanitizer"
gem "twitter"
gem "twitter_retry", ">= 0.2.1"

group :development do
  gem "dotenv"
end

group :test do
  gem "rspec"
end

app.rb

HTTPのリクエストがきた時に bundle list の結果を標準出力に出すだけのシンプルなコード。

GCP公式の Functions Framework を使っている以外は見慣れたRubyのコードです。

require "functions_framework"

begin
  require "dotenv/load"
rescue LoadError
end

FunctionsFramework.http("test") do |request|
  puts `bundle list`
  "test\n"
end

ローカルでの実行結果

ログに rspecdotenv がいるのを確認

$ bundle exec functions-framework-ruby --verbose --target test
I, [2021-01-23T23:32:08.699683 #4427]  INFO -- : FunctionsFramework v0.7.0
I, [2021-01-23T23:32:08.699761 #4427]  INFO -- : FunctionsFramework: Loading functions from "./app.rb"...
I, [2021-01-23T23:32:10.147408 #4427]  INFO -- : FunctionsFramework: Looking for function name "test"...
I, [2021-01-23T23:32:10.147486 #4427]  INFO -- : FunctionsFramework: Starting server...
I, [2021-01-23T23:32:10.201662 #4427]  INFO -- : FunctionsFramework: Serving function "sentry_test" on port 8080...
I, [2021-01-23T23:32:16.165209 #4427]  INFO -- : FunctionsFramework: Handling HTTP GET request
Gems included by the bundle:
  * addressable (2.7.0)
  * buftok (0.2.0)
  * cloud_events (0.1.2)
  * concurrent-ruby (1.1.8)
  * diff-lcs (1.4.4)
  * domain_name (0.5.20190701)
  * dotenv (2.7.6)
  * equalizer (0.0.11)
  * faraday (1.3.0)
  * faraday-net_http (1.0.1)
  * ffi (1.14.2)
  * ffi-compiler (1.0.1)
  * functions_framework (0.7.0)
  * gapic-common (0.3.4)
  * google-cloud-core (1.5.0)
  * google-cloud-env (1.4.0)
  * google-cloud-errors (1.0.1)
  * google-cloud-firestore (2.4.1)
  * google-cloud-firestore-v1 (0.2.3)
  * google-cloud-secret_manager (1.0.1)
  * google-cloud-secret_manager-v1 (0.5.1)
  * google-cloud-secret_manager-v1beta1 (0.6.6)
  * google-protobuf (3.14.0)
  * googleapis-common-protos (1.3.10)
  * googleapis-common-protos-types (1.0.5)
  * googleauth (0.14.0)
  * grpc (1.35.0)
  * grpc-google-iam-v1 (0.6.10)
  * http (4.4.1)
  * http-cookie (1.0.3)
  * http-form_data (2.3.0)
  * http-parser (1.2.3)
  * http_parser.rb (0.6.0)
  * jwt (2.2.2)
  * memoist (0.16.2)
  * memoizable (0.4.2)
  * moji (1.6)
  * multi_json (1.15.0)
  * multipart-post (2.1.1)
  * naught (1.1.0)
  * nio4r (2.5.4)
  * os (1.1.1)
  * parallel (1.20.1)
  * public_suffix (4.0.6)
  * puma (4.3.7)
  * rack (2.2.3)
  * rake (13.0.3)
  * rbtree (0.4.4)
  * rspec (3.10.0)
  * rspec-core (3.10.1)
  * rspec-expectations (3.10.1)
  * rspec-mocks (3.10.1)
  * rspec-support (3.10.1)
  * ruby2_keywords (0.0.4)
  * sentry-ruby (4.1.4)
  * signet (0.14.0)
  * simple_oauth (0.3.1)
  * slack-notifier (2.3.2)
  * thread_safe (0.3.6)
  * tweet_sanitizer (0.2.0)
  * twitter (7.0.0)
  * twitter_retry (0.2.1)
  * unf (0.1.4)
  * unf_ext (0.0.7.7)
Use `bundle info` to print more detailed information about a gem

Cloud Functions上での動作確認結果

コンソールから関数を実行

f:id:sue445:20210124000003p:plain

出力されたログにはdotenvやrspecがいないので bundle install --without development test が実行されてることが分かります。

f:id:sue445:20210123235704p:plain

f:id:sue445:20210123235626p:plain

あくまでドキュメント化されていない*1仕様なので今後この挙動は変わる可能性はあります。

*1:僕が見つけられなかったところに載ってる可能性はあります

至極の難問YAMLクイズ

前置き

  • 社内勉強会のLTで発表したら好評だったので投下
  • 自称YAMLエンジニアのsue445が今まで踏んだ罠をクイズにしました
  • Ruby 3.0.0の Psych で動作確認していますが他言語での挙動は調べていません
    • Psychがlibyamlベースなので他の言語のパーサでもだいたい同じ挙動をすると思うけど

練習問題

Q: 出力されるものは?

yaml = <<YAML
a: 1
YAML

YAML.load(yaml)
#=> ?
  1. {"a"=>"1"}
  2. {"a"=>1}
  3. シンタックスエラー
  4. その他

回答 2

YAMLの数字っぽい文字列はその言語の数字の型(Rubyだと Integer )として解釈されます。 文字列として解釈させたい場合は "1" (ダブルクオーテーション)や '1' (シングルクォーテーション)のように囲んでください

問題1

Q: 出力されるものは?

yaml = <<YAML
splash: ふたりはプリキュア Splash Star
yes: Yes!プリキュア5
yes_gogo: Yes!プリキュア5GoGo
fresh: フレッシュプリキュア!
YAML

data = YAML.load(yaml)
"#{data['splash']},#{data['yes']},#{data['yes_gogo']},#{data['fresh']}"
#=> ?
  1. "ふたりはプリキュア Splash Star,Yes!プリキュア5,Yes!プリキュア5GoGo,フレッシュプリキュア!"
  2. "ふたりはプリキュア Splash Star,,Yes!プリキュア5GoGo,フレッシュプリキュア!"
  3. ",Yes!プリキュア5,Yes!プリキュア5GoGo,フレッシュプリキュア!"
  4. シンタックスエラー

回答 2

true, false, yes, no, on, off はダブルクオーテーションなどで囲まない限りYAMLでは全て 真偽値 として扱われます

data = YAML.load(yaml)
=> {"splash"=>"ふたりはプリキュア Splash Star", true=>"Yes!プリキュア5GoGo!", "yes_gogo"=>"Yes!プリキュア5GoGo", "fresh"=>"フレッシュプリキュア!"}
data = YAML.load("{1: yes, 2: Yes, 3: on, 4: On, 5: true, 6: True}")
#=> {1=>true, 2=>true, 3=>true, 4=>true, 5=>true, 6=>true}

data = YAML.load("{1: no, 2: No, 3: off, 4: Off, 5: false, 6: False}")
#=> {1=>false, 2=>false, 3=>false, 4=>false, 5=>false, 6=>false}

問題2

Q: 出力されるものは?

yaml = <<YAML
default: &default
  slack:
    webhook_url: "https://example.com/"
    channel: "random"

production:
  <<: *default
  slack:
    channel: "production_notify"
YAML

data = YAML.load(yaml)
data["production"]
#=> ?
  1. {"slack"=>{"webhook_url"=>"https://example.com/", "channel"=>"random"}}
  2. {"slack"=>{"webhook_url"=>"https://example.com/", "channel"=>"production_notify"}}
  3. {"slack"=>{"channel"=>"production_notify"}}
  4. シンタックスエラー

回答 3

& (アンカー)と *エイリアス)で定義済みの値をいい感じに共通化できるのはYAMLのよくあるリファクタリング手法ですが、<<: (マージ)はdeep merge(要素内に別の要素があった時に再帰的にマージされる)ではなく第1要素だけを上書きするマージ(代入に近い)なので、今回の場合 default の内容が打ち消されます。

イメージ的にはこんな感じ

data["slack"] = {"webhook_url"=>"https://example.com/", "channel"=>"random"}
data["slack"] = {"channel"=>"production_notify"}

余談ですが https://github.com/railsware/global はdeep mergeしてくれるのがかなり便利で、前職のRailsアプリにはだいたい入っていました

問題3

Q: 出力されるものは?

yaml = <<YAML
go:
  - 1.9
  - 1.10
  - 1.11
  - 1.12
YAML

data = YAML.load(yaml)
data["go"]
#=> ?
  1. [1.9, 1.10, 1.11, 1.12]
  2. [1.9, 1.1, 1.11, 1.12]
  3. [1.90, 1.10, 1.11, 1.12]
  4. シンタックスエラー

回答 2

クオーテーションで囲んでいないので小数として解釈されるため 1.101.1 として解釈されます

厳密に 1.10 として評価するには "1.10" のように囲む必要があります。

余談ですがGo 1.10が出た時に .travis.yml1.10 を追加したらGo 1.1でCIが実行されてビルドが失敗したことがあります。

https://github.com/sue445/zatsu_monitor/commit/fc6b8ab806a3c48617cb084437d2d1c018777ee9#comments

問題4

Q: この中でシンタックスエラーになるのはどれか?

a

excludes:
  - *_test.rb

b

excludes:
  - test/**
  1. aのみシンタックスエラー
  2. bのみシンタックスエラー
  3. aとb両方シンタックスエラー
  4. シンタックスエラーは無い

回答 1

a

yaml = <<YAML
excludes:
  - *_test.rb
YAML

YAML.load(yaml)
Traceback (most recent call last):
        8: from /Users/sue445/.rbenv/versions/3.0.0/bin/irb:23:in `<main>'
        7: from /Users/sue445/.rbenv/versions/3.0.0/bin/irb:23:in `load'
        6: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
        5: from (irb):62:in `<main>'
        4: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/3.0.0/psych.rb:280:in `load'
        3: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/3.0.0/psych.rb:390:in `parse'
        2: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/3.0.0/psych.rb:456:in `parse_stream'
        1: from /Users/sue445/.rbenv/versions/3.0.0/lib/ruby/3.0.0/psych.rb:456:in `parse'
Psych::SyntaxError ((<unknown>): did not find expected alphabetic or numeric character while scanning an alias at line 2 column 3)

b

yaml = <<YAML
excludes:
  - test/**
YAML

YAML.load(yaml)
#=> {"excludes"=>["test/**"]}

*_test.rb* が前述のエイリアスとして評価されるのですが、対応する _test.rb という名前のアンカーが存在しないためシンタックスエラーになります。

シンタックスエラーにしないためには "*_test.rb" のように囲む必要があります。

参考文献

おまけ:LT直後のみんなの反応

  • 「囲んでも囲まなくてもいいみたいなsyntaxやだ…」
  • 「問題として聞かれるとわかるけど、実際に yaml 書いてたらミスりそう」
  • YAMLの特別な挙動にうごかされないようにするのは、ダブルクォートつけるのがいいのかな。」
    • -> sue「『それはそう』なんですが、実際にそれやろうとするとかなり冗長になってだいぶ見づらくなるんですよね」
  • 6E-1 が(文字列として判定されてほしいのに) 指数表記で判定されてハマった」
  • YAMLほんとうにわかってなかった」