くりにっき

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

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ほんとうにわかってなかった」

GitHub Actionsのpushイベントとpull_requestイベントではGITHUB_SHAが異なる

tl;dr;

タイトルが全て

検証内容

サンプルコード

GitHub Actionsで使える(事前定義済みの)環境変数 *1を列挙するだけのシンプルなワークフローです

on:
  - push
  - pull_request

jobs:
  show_env:
    runs-on: ubuntu-latest

    steps:
      - run: env | grep GITHUB_ | sort

https://github.com/sue445/github_actions_sandbox_20210106/blob/master/.github/workflows/sandbox.yml

masterブランチに普通にpushした時

コミットグラフはこんな感じ

f:id:sue445:20210106234932p:plain

https://github.com/sue445/github_actions_sandbox_20210106/runs/1656991711 には

GITHUB_SHA=f308d17b80228b432d841e96624b583f632d2411

と出ているので、 master ブランチ *2 のHEADと一致していることが分かると思います。(分かる)

PullRequestに対してpushした場合

検証用のPR https://github.com/sue445/github_actions_sandbox_20210106/pull/1

onpushpull_request を指定してるのでそれぞれイベントが発火します。

f:id:sue445:20210106235514p:plain

この時点でコミットグラフはこんな感じ

f:id:sue445:20210106235440p:plain

pushイベントの結果

https://github.com/sue445/github_actions_sandbox_20210106/pull/1/checks?check_run_id=1657017337 には

GITHUB_SHA=8d1396316903bd747589ff2a81930e659cca1a4d

と出ているので、 tmp/pr_test ブランチのHEADと一致しています。(分かる)

pull_requestイベントの結果

https://github.com/sue445/github_actions_sandbox_20210106/pull/1/checks?check_run_id=1657017535

GITHUB_SHA=9b0ee21ee4668fab1d29961f60108b9ef3c1946b

え!!!???

解説

https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#push によるとpushイベントでの GITHUB_SHA

Commit pushed, unless deleting a branch (when it's the default branch)

とのこと。

https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#pull_request によるとpull_requestイベントでの GITHUB_SHA

Last merge commit on the GITHUB_REF branch

とのこと。

これしか書いていなくていまいち意味が分からなかったのですが id:r7kamura さんが RuboCop Problem Matchers

pull_request イベントを起点に動かす場合、actions/checkout はそのPull Requestが生成しようとしているmerge commitをチェックアウトする。

と書いてるように、この GITHUB_SHA はそのPull Requestで生成しようとしてるmerge commitのSHAだと思われます。

ちなみにこの挙動はTravis CIと同じような仕様です。

2021/01/08 追記

  • Travis CI以外にもCircleCIやJenkinsのGithub Pull Request Builderが同じ挙動
  • BitriseはGitHubのPullRequestのマージコミットではなく、内部でマージした時のマージコミットをcheckoutしてる
  • WerckerはブランチのHEADを使ってる

トノコト

GITHUB_SHAが異なることで何が困るか

ほとんどのケースではそれほど困ることはないと思います。*3

しかしジョブの中で GITHUB_SHA を使おうとした場合に注意が必要です。

具体的には tfnotify でTerraformの実行結果をPull Requestのコメントに出そうとした時に、pull_requestイベントを使うとPullRequestと直接紐付かない前述のマージコミットに対してコメントがつくため、初見だと謎のコミットに対してコメントが付く状態になります。(PullRequest上に登場するコミットに対してコメントがつけばPullRequest上で表示されます)

f:id:sue445:20210107001641p:plain

f:id:sue445:20210107001655p:plain

余談:tfnotifyでpull_requestイベントの時にもPullRequestにコメントをつけたい

半年くらいずっと例の謎コミットにコメントがついて困ってたのですがつい先日下記で解決しました。

- name: terraform plan
  run: |
    # NOTE: tfnotify uses GITHUB_SHA, but GITHUB_SHA can't be override in env
    if [ -n "$PR_HEAD_SHA" ]; then
      export GITHUB_SHA=$PR_HEAD_SHA
    fi
    terraform plan -input=false | tfnotify plan
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}

実際にPull Requestに対してtfnotifyのコメントがついた図

f:id:sue445:20210107004749p:plain

ちなみにコード中のコメントでも書いていますが

env:
  GITHUB_SHA: ${{ github.event.pull_request.head.sha }}

のように書いても GITHUB_SHA を上書きできないので、別名として取得してジョブの中で代入して上書きするしかないです。

FAQ

Q. だったらpull_requestは不要では?

業務リポジトリのように開発者全員が同一リポジトリ内でPullRequestをやり取りする場合にはpushイベントだけで問題ないです。

しかしpushイベントだとforkされたリポジトリからPullRequestがきた時にイベントが発火しなくてCIのジョブが実行されないのでOSSリポジトリで困ります。(経験済)

かといって今回のサンプルみたいに

on:
  - push
  - pull_request

で書くとPullRequestがきた時に2つジョブが実行されて無駄です。

色々検証した結果個人リポジトリだと下記に落ち着きました。

on:
  push:
    branches:
      - master
  pull_request:
    types:
      - opened
      - synchronize
      - reopened

40個以上のリポジトリで上記設定を半年以上使ってますが今の所不満はないです。

今の心境

*1:公式の説明は https://docs.github.com/en/free-pro-team@latest/actions/reference/environment-variables

*2:最近GitHubではmainブランチがデフォルトになってますが毎回両方併記のは大変なので本エントリでは masterブランチで統一します

*3:トピックブランチを作った時点でのmasterブランチとPullRequestを出した時のmasterブランチが大きく剥離してるとローカルではテストが通るのにCIだとテストがコケるという事象は発生するかもしれません

rubicure 2.0.0をリリースした

https://github.com/sue445/rubicure/blob/master/CHANGELOG.md#v200

rubocopを最新にしようと思ったら最新のrubocopだとRuby 2.4以降必須になってたのでこの機会にRuby 2.2系と2.3系のサポートを切りました。

rubocopのバージョンを上げたのでソース上の差分が結構大きいですが機能面での変更点は無いです

今年作ったもの(2020年版)

今年新しく作ったものを作った順に紹介

go-mod-tidy-pr

sue445.hatenablog.com

今年の頭に作ったやつでたまにPRきてる。

最近GitHub本体のdependabotの方で go mod tidy 正式対応されたので go-mod-tidy-prはお役御免かなと思ってdeprecateにしました

github.blog

prismdb-ruby, faker-pretty_series

下記エントリ参照

sue445.hatenablog.com

gcp-kmsenv

下記エントリ参照

sue445.hatenablog.com

gcp-secretmanagerenv

下記エントリ参照

sue445.hatenablog.com

プリマップ

個人的には今年のイチオシ

sue445.hatenablog.com

sue445.hatenablog.com

今まで使ったことのないアーキテクチャで作りつつも、割と爆速で動くというのがウリ。

しかもGCPの費用が1ヶ月で1ドル切ってるのでお財布的にも嬉しい。(リリース直後にプチバズってアクセス増えたけどそれでも1ドルちょっと超えたくらい)

f:id:sue445:20201222205635p:plain

Cloud Functions実行時に毎回Secret Managerから機微情報を読み込んでるのでそこをGoのビルド時にいい感じに埋め込めばほぼ0円にできそうだが*1、今のままでも十分安いのでそこまでのモチベーションはないw

*1:FirestoreのQuota超えることもあるので完全に0円は無理

今年買ってよかったもの(2020年版)

Amazon楽天などの購入履歴からピックアップ

エクスジェル アウルカンフィ3Dプレミアム

exgel.jp

家だと座椅子なのですが座椅子で仕事しててクッションに不満があった時に同僚に勧められて購入。

長時間座ってても尻が痛くならないのが良い

Oculus Rift S

www.oculus.com

ハッピーおしゃれタイム をやりたくて購入。*1

けどGoogle Earth VRで昔住んでた場所にVR帰省したり、バーチャルデスクトップでライブのBDや配信イベントの鑑賞してる方が多いかもしれない。

仮想大画面でライブ見るのいいですね。(目がむっちゃ疲れるけど)

プリパラ LIVE COLLECTION

avex.jp

avex.jp

avex.jp

「プリパラミュージックコレクションがあるならライブ版もあるのでは?」ということでググって見つけたので購入。

プリパラやプリ☆チャンのCGライブが好きなのでCGライブだけをずっと見れるのは神。

これをBGM(Back Ground Movie)でコードを書くとテンションMAX!

難点は推し*2のパートで手が完全に止まること。

KPro01

www.owltech.co.jp

Makuake で支援してたのが先月届いたので使ってます。

操作に慣れるまでに時間がかかったけど慣れたらかなり快適。

フル充電で9時間持つので仕事で使うには十分

*1:Quest 2じゃなくてRift Sなのは購入時点でQuest 2が発表されていなかったため

*2:アロマゲドンとガァルマゲドン

細かすぎて伝わりづらいプリマップの改修ポイント #プリッカソン

これは #プリッカソン Advent Calendar 2020 - Adventar の2日目です。

10月の プリマップ 公開以降見た目はほとんど変わってないのですが裏側をちょいちょい改善してるのでメモ

sue445.hatenablog.com

ジオコーディング前に住所をいい感じにしている

公式の店舗リストの住所には「東京都新宿区西新宿1-11-1ゲームホビー館」のように建物名が入ってることが多いのですが、この手の住所をそのままジオコーディング(住所から緯度経度を取得)すると高確率で意図しない緯度経度が返ってくるため「東京都新宿区西新宿1-11-1」のように建物名を削ってやる必要があります。

この手の前処理はリリース前から実装自体はしてたものの、日本の住所表記はバリエーションが豊富なのでリリース以降も気づきベースでちょいちょい修正してました

プロダクトコードだけ見ると正規表現がアレなのでテストコード見た方が分かりやすいと思います。

データ集計処理の高速化

2200件近い店舗情報をFirestoreに一度に保存する関係上GCPのPub/Subのtopic *1 を使ってCloud Functionsで大量に並列処理をしているのですが、topicに積むところが遅かったのでパフォーマンスチューニングしました。

デバッグログ仕込んで各処理の実行時間を計測したら改善できそうな処理が2箇所あったのでそれぞれ修正。

github.com

2200件もあるとPub/Subのtopicに積むだけで遅いのでgoroutineで並列化しました。ここだけで50秒から1~7秒くらいに改善。(実行時間があまり安定しない・・・)

github.com

削除された店舗一覧を調べるために「Firestoreに保存済の店舗一覧 - prismdbで取得した店舗一覧」みたいなことをやるためにGoのslice同士の引き算を行ってるのですが、そこの処理が遅かったのを改修。 ベンチマークレベルだと300倍速くなっていてCloud Functions上でも9秒から1秒前後に改善。

改修前はトータルで1分弱かかってた処理が4~8秒で終わるようになりました。

公式のデータが間違っていたのでタカラトミーアーツさんのサポートに報告して直してもらった

プリマップでは間接的*2に公式の店舗リストを使ってるのですが公式のデータが一部間違ってるせいでプリマップのデータも変なことになってたのでタカラトミーアーツさんに報告して直してもらいました。

件数多かったけど報告したその日のうちに全部直してもらったので不満を持ってる人はどんどん報告すればいいと思います。

余談ですがタカラトミーアーツサポートへの連絡先は2つありました。

faq.takaratomy-arts.co.jp

www.takaratomy-arts.co.jp

1つ目は返信不要なやつで2つ目は返信を受け取りたい時のものかと思われます。僕は修正報告を受けたかったので後者で報告しました。

プリパラとプリ☆チャンで店舗名が微妙に食い違ってるんで名寄せした

github.com

プリマップでは店舗名をFirestoreのコレクションのKeyにしてるのでKeyが食い違うと同一店舗なのにプリパラとプリ☆チャンで1つずつ出てくる状態になって不便です。(こういうのが2200件中50件くらいありました)

明らかに情報が間違ってる場合には公式に直してもらえばいいんですが、今回のケースは完全にこっちの都合なのでプリマップ側で名寄せしてます。

ほとんどの場合「△△△△△(ゲームセンターや量販店の名前) ○○(支店名)店」と「△△△△△ ○○」で表記ゆれしてたので機械的に「店」を削るだけで済んだのですが*3、それでも名寄せできなかったものがあったのでそこだけ名寄せ用のマッピングを作りました *4