Rails 6.1.0.rc1がリリースされましたが*1、activesupport本体に #compact_blank
や #compact_blank!
のメソッドが入った*2ので6.1.0.rc1以降と一緒に入れられないようにしてメンテ終了宣言しました。
rubocop v1系 + rubocop-rspec v1系でエラーになる件の回避策
tl;dr;
どっちのケースの場合もrubocop-rspec v2.0.0.preで直ってるので Gemfile
に下記のように書けば直る
group :development do gem "rubocop-rspec", ">= 2.0.0.pre", require: false end
ケース1: rubocop-rspec内でエラーになる
エラー内容
-d
(デバッグ出力)を有効にしないとエラー内容が表示されないので注意
$ bundle exec rubocop -d (中略) An error occurred while RSpec/FactoryBot/CreateList cop was inspecting /Users/sue445/workspace/github.com/sue445/app-stat-api/spec/app_spec.rb:1:0. undefined method `to_sym' for nil:NilClass /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/mixin/configurable_enforced_style.rb:69:in `style' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-rspec-1.42.0/lib/rubocop/cop/rspec/factory_bot/create_list.rb:51:in `on_block' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:100:in `public_send' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:100:in `block (2 levels) in trigger_responding_cops' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:166:in `with_cop_error_handling' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:99:in `block in trigger_responding_cops' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:98:in `each' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:98:in `trigger_responding_cops' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:69:in `on_block' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-ast-1.1.0/lib/rubocop/ast/traversal.rb:20:in `walk' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:86:in `investigate' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/team.rb:157:in `investigate_partial' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/team.rb:83:in `investigate' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:315:in `inspect_file' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:259:in `block in do_inspection_loop' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:293:in `block in iterate_until_no_changes' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:286:in `loop' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:286:in `iterate_until_no_changes' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:255:in `do_inspection_loop' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:132:in `block in file_offenses' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:157:in `file_offense_cache' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:131:in `file_offenses' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:122:in `process_file' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:101:in `block in each_inspected_file' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:100:in `each' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:100:in `reduce' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:100:in `each_inspected_file' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:86:in `inspect_files' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:47:in `run' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli/command/execute_runner.rb:25:in `execute_runner' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli/command/execute_runner.rb:17:in `run' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli/command.rb:11:in `run' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli/environment.rb:18:in `run' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli.rb:65:in `run_command' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli.rb:72:in `execute_runners' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli.rb:41:in `run' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/exe/rubocop:13:in `block in <top (required)>' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/benchmark.rb:308:in `realtime' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/exe/rubocop:12:in `<top (required)>' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/bin/rubocop:23:in `load' /Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/bin/rubocop:23:in `<top (required)>' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli/exec.rb:63:in `load' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli/exec.rb:63:in `kernel_load' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli/exec.rb:28:in `run' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli.rb:476:in `exec' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/vendor/thor/lib/thor/command.rb:27:in `run' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/vendor/thor/lib/thor.rb:399:in `dispatch' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli.rb:30:in `dispatch' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/vendor/thor/lib/thor/base.rb:476:in `start' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli.rb:24:in `start' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bundler-2.1.4/libexec/bundle:46:in `block in <top (required)>' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/friendly_errors.rb:123:in `with_friendly_errors' /Users/sue445/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bundler-2.1.4/libexec/bundle:34:in `<top (required)>' /Users/sue445/.rbenv/versions/2.7.2/bin/bundle:23:in `load' /Users/sue445/.rbenv/versions/2.7.2/bin/bundle:23:in `<main>'
関連するissue
実際に対応したコミット
https://github.com/sue445/app-stat-api/pull/631/commits/7013ba612d2f1f663e2b8e60816655de6770602e
ケース2: 意図せずrubocop-rspecのバージョンが下がる
注意点
https://github.com/rubocop-hq/rubocop-rspec/blob/v2.0.0.pre/CHANGELOG.md#200pre-2020-10-22
v2で 下記のようなbreaking changeがあるので注意
(Potentially breaking) Change namespace of several cops (
Capybara/*
->RSpec/Capybara/*
,FactoryBot/*
->RSpec/FactoryBot/*
,Rails/*
->RSpec/Rails/*
).
プリマップというプリパラやプリ☆チャンが遊べるお店を探せる地図を作った
タイトルが全て
モチベーション
以前 プリ☆チャンMAP を作ったことがあるのですが、下記のような不満がありました。
- プリ☆チャンの公式店舗一覧 をスクレイピングしてスプレッドシートに入れるところまでは自動化できたが、そこからGoogleマイマップに取り込む作業が手作業
- スプレッドシートに住所カラムを作っておけばGoogleマイマップがいい感じに緯度経度に変換してくれるのだが、公式の住所のフォーマットがおかしいと稀に緯度経度が取れないので手動で補正する必要がある
- 公式のショップ一覧は定期的に更新されるのでそれに追従するのが大変
- プリ☆チャンMAP作成後にプリパラオールフレンズの稼働が始まったのだがそれの地図を作るのも大変
そのため完全に作り直しました。
使い方
https://primap.web.app/ を開くといい感じに現在地周辺のプリ☆チャンやプリパラで遊べる店舗の一覧が出ます。
技術的なこと
リポジトリ
大雑把な仕組み
開発期間はトータルで1ヶ月くらいです
サーバサイド
俗に言うサーバサイドは最初はあったけど途中で捨てました
当初の構成だとCloud RunでGoで書いたAPIやジョブをホスティングする予定だったんですが、APIはFirebaseでいいことに気づいたので途中で方針転換して、その結果サーバサイドがなくなりました。(サーバレス)
ジョブ
採用技術は下記
- Go
- Cloud Functions
- Cloud Scheduler
- Cloud Pub/Sub
- Secret Manager
- Geocoding API
- Serverless Framework
店舗が数千件あって1つのファンクションの中で全ての緯度経度の取得をするのは厳しいと判断して、PrismDBで取得した店舗の件数分だけPub/Subでqueueに積んで非同期で処理するようにしました
Geocoding APIを一度にたくさん叩きすぎるとReteLimitに引っかかるのでリトライも必要になるのですが、Pub/Subで店舗1件ずつ処理することでその辺のリトライを何も考えなくてよかったのは楽でした。(Pub/Subで呼んだファンクションがエラーになれば勝手にリトライされるので)
フロントエンド
採用技術は下記
- TypeScript
- React
- Vue.js + TSは過去にやったことあるので今回は今まで使ったことのないReactを採用
- React歴3週間くらい
- Tailwind CSS
- 社内でちょいちょい名前を聞いていたので採用
- Firebase Hosting
- Google Maps
- ReactでGoogle Mapsを使うライブラリは2〜3個検討したのですが「Google Mapsの面倒くさい部分を全部いい感じにラップしてくれている」という点で https://www.npmjs.com/package/google-maps-react を採用しました
その他使ったもの
- GitHub Actions
- Sentry
- Terraform
あたり
こだわりポイント
Cloud RunからCloud Functionsに移行して変わったこと
メリット
- デプロイフローがシンプルになった
- Cloud Runでアプリを動かすためにはDockerイメージが必要なので必然的にCIでもdocker buildしてContainer Registryにpushする必要があるのだが(GitHub Actions上で1分弱)、Cloud Functions化することでそのフローが完全に不要になった
- Cloud RunでCloud SchedulerやPub/Subのリクエストを受けるためにはhttpのエンドポイントが必要だったが、Cloud Functions化することでhttp周りの実装が不要になった
- Cloud Functions側にPub/Sub専用のエンドポイントがある
デメリット
- Cloud Run時代はデプロイが2分弱だったが*1、Cloud Functions移行後にデプロイに5〜6分かかるようになった
- Serverless Frameworkのバックエンドで使ってるDeployment Managerの様子を見る感じ純粋にCloud Functionsへのデプロイが一番時間がかかってるのだが、具体的にどこで時間がかかってるのかが特定できないので謎。(おそらくgo getかビルドのどちらかだと思うのだが...)
- 1つのバイナリに2つのファンクションが含まれているのでそれらを分割すれば早くなる可能性はありそう
- Cloud Functionsだと最新のGoが1.13
- 早く1.15使わせてほしい...
GoのstructからTypeScriptのclassを自動生成するようにした
FirestoreにはGoのstructで保存してるのですが、全く同じデータをフロントエンドで使うために tscriptify でTypeScriptのclassに変換しています。
Firestoreに緯度経度の型はあるのだが、実際にはそれだけだと検索で使えなくてハマった
現在地の緯度経度からFirestoreで検索しようとしたのですが当初うまくいきませんでした。(明らかに現在地から外れた店舗が検索にヒットする)
調べると https://stackoverflow.com/questions/46630507/how-to-run-a-geo-nearby-query-with-firestore に
Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby.
とあるように、Firestoreは実際のGeoPointクエリをサポートしていないため、latlng型で検索しても緯度しか考慮されないということでした。
そのため緯度経度から算出したGeoHashも別途Firestoreに保存することでようやく現在地周辺の検索ができるようになりました。
ディレクトリ構成を色々変えた
開発中に何回かディレクトリ構成を作り直しました
- 1段階目
- 最初はオーソドックスなGoのアプリのディレクトリ構成
- 2段階目
- frontend実装直後
- GoはそのままでTSとReact関係のソースだけfrontendに入れた形
- 3段階目
- serverとfrontendの2つのディレクトリ構成
- この時点ではmakeやnpmのコマンド実行時にいちいちcdするのが嫌だという気持ちがあってこの構成でした
- TSやReact関係のソースは全部frontendに格納できたのですが、Goはビルドの関係でトップに一部ソースが残ってます
- 4段階目(今)
- 「makeやnpmのコマンド実行時にいちいちcdするのが嫌だ」という気持ちはあったんですが、それ以上にトップの見通しが悪いのが嫌だったので完全にディレクトリを分けました(手の平クルー)
frontendとfunctionの接点がfirestoreだけだったのでここまで振り切れたかもしれないです。
本当ならfrontendとfunctionでリポジトリを分けてもよかったのですが、前述の通りGoのstructからTypeScriptのclassを生成してるためリポジトリを分けると逆に連携が難しくなるためモノリシックリポジトリにしました。
port 55301
せっかくなので(?)devServerは55301番ポートを使うようにしました。
今週一番意味不明なコミットができた pic.twitter.com/NnxmyS7t2C
— sue445 (@sue445) 2020年10月13日
副産物
プリマップでSecretManagerを使ってるのですがラッパとして途中でgcp-secretmanagerenvができました
*1:docker buildに1分半でgcloud run deployが30秒
pixela gem v2.0.0をリリースした
pixela v1.19.0 で機能削除が行われたのでgemのメジャーバージョンを上げました。
Breaking changeついでに他にも非互換の変更を入れています
変更内容と変更意図
https://github.com/sue445/pixela/blob/master/CHANGELOG.md
- faraday v0.x系のサポート終了
- https://github.com/sue445/pixela/pull/77
- faraday v1.0.0が出てもう半年以上経つのでサポート切ってもいいかなという判断
- Ruby 2.3のサポート終了
- https://github.com/sue445/pixela/pull/78
- faraday v1準拠で最低バージョンは2.3のままでいこうかなと思ったのですが、Ruby 2.3をサポートしたままだと
Hash#compact
が使えなくて微妙な感じなので最低バージョンを2.4に引き上げました
gcp-secretmanagerenvを作った
gcp-secretmanagerenvについて
GCPのSecret Managerのラッパみたいなやつで、 gcp-kmsenv のSecret Manager版です。
作った経緯
本番環境(GCP)ではSecret Managerを使うのでいいんだけど、ローカル開発時は環境変数を使いたかった *1 のですが、Secret Managerの値の取得と環境変数の取得を透過的に扱う部分をアプリケーションコード内に書きたくなくて先にライブラリ化しました。
使い方
READMEからの抜粋ですがこれがだいたい全てです。
package main import ( "context" "github.com/sue445/gcp-secretmanagerenv" ) func main() { projectID := "gcp-project-id" c, err := secretmanagerenv.NewClient(context.Background(), projectID) if err != nil { panic(err) } // get from environment variable value, err := c.GetValueFromEnvOrSecretManager("SOME_KEY", true) // => return value from environment variable or Secret Manager // When key is not found in both environment variable and Secret Manager, returned empty string (not error) value, err := c.GetValueFromEnvOrSecretManager("INVALID_KEY", false) // => "" // When key is not found in both environment variable and Secret Manager, returned error value, err := c.GetValueFromEnvOrSecretManager("INVALID_KEY", true) // => error }
GetValueFromEnvOrSecretManager
の第1引数は検索するkey- projectIDが渡されなければSecretManagerの呼び出し自体を行わない
- 本番環境のみ
GCP_PROJECT
のような環境変数を設定しておくような想定
- 本番環境のみ
GetValueFromEnvOrSecretManager
の第2引数は必須とみなすかどうかのフラグ
Cloud Functions CI/CD俺的ベストプラクティス
AWS Lambda CI/CD俺的ベストプラクティス - くりにっき のCloud Functions版です
最近Cloud Functions使うことがあってCI面からフレームワークなどを検討しました。AWS Lambdaほどガッツリ使ってないので比較対象が少ないのはご愛嬌
Terraform
- Cloud FunctionsにアップロードするソースコードをTerraformでzip圧縮する必要があって厳しい
- アップロード用のバケットも自分で作る必要がある
- 別に作ってもいいんだけど、Serverless Frameworkだとzip圧縮やバケット作成含めて勝手にやってくれるので敢えてこっちを選ばなくてもいいと思う
Deployment Manager
https://cloud.google.com/deployment-manager?hl=ja
- 公式なので信頼できる
- zip圧縮も面倒見てくれるかと思ったらpythonの中でzip圧縮してyamlを動的生成しててかなり黒魔術
- https://github.com/GoogleCloudPlatform/deploymentmanager-samples/blob/master/examples/v2/cloud_functions/python/cloud_function.py
- Functionを複数作るとして、これを複数リポジトリにコピペしてまわりたくない...(1つでも割とウッとなる)
Serverless Framework
- 今回比較した中では一番まとも
- デプロイ関係の面倒くさい処理(GCSのバケットを作ってソースをzipに固めてアップロードとか)はよしなにやってくれる
- プラグインが充実してるのも嬉しい
- 不満点
- デプロイのdry runや
serverless.yml
のvaridateができないところ- master自動デプロイにしてるとmasterにマージするまで動作確認できない
- Serverless Framework + Cloud Functions + Goで書いてるんだけど情報が英語含めてほとんどないので直感で書いてる
- デプロイのdry runや
- 凝ったことをやりたくなると(例:Cloud Functionsで使うPubsub topicをServerless側で作る)
serverless.yml
にDeployment Managerのymlを書く必要があるのだが、それはAWSで分かってたことなので諦めてた
GitHub Actions上でPRを作る時はsecrets.GITHUB_TOKENは使わない方がよさそう
事象
GitHub Actions上で使える secrets.GITHUB_TOKEN
だと別のジョブを起動できないというのが一番の理由。
具体的にどういうケースで困るのかというと、GitHub Actions上でPRを作った場合にそのPRに対するビルドが行われません。
普通だったらコミットIDの左側に✔が出るはずなんだけど出ない図
pushやPRなどのビルドをGitHub Actions以外(例:CircleCIなど)で行っている場合には問題ないです。
解決策
いくつか案はあるのですが現状だと GitHub App Token を使うのがいいと思ってます。
2023/01/08 追記:このエントリ執筆時点では3rd party製の https://github.com/tibdex/github-app-token しかなかったけど、現在は公式で https://github.com/actions/create-github-app-token があるので後者を使うのがよさそう。(引数名がちょっと違う以外は使い勝手は一緒)
App作成の手間はあるもののインストールするリポジトリを指定できるので権限の分離ができるのが一番の理由です。 (パーソナルアクセストークンだと業務で所属してるorg含めて自分が見れる全リポジトリが対象になるので漏洩時のリスクが怖い)
実際に動かした図
sue445-bot(GitHub App)でPRを作ったのでGitHub Actionsでビルドが実行されるようになってます
上記AppだとパーミッションはActionsとPull requestsにRead & writeをつけたけどActionsの方は不要だったかもしれないです。
2020/09/12追記:Pull requestsのRead & writeだけで良かった
go-mod-tidy-prでも同様の問題を抱えていたのでGitHub App Token推奨にしました。
https://github.com/sue445/go-mod-tidy-pr#note-warning
謝辞
という情報を プリッカソン のSlackで知って便利
2020/9/1 22:50 追記
GutHub Appを作った場合は今度はAppの秘密鍵の管理を考える必要がありますが、Organizationであれば Organization secrets が使えるのでこの手の秘密鍵の管理に便利そうです。(ってことを ruby-jp Slack で話していて思い出した)