くりにっき

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

個人gemにrubygems_mfa_requiredをつけた

rubocop 1.23.0で Gemspec/RequireMFA が増えていたので rubygems_mfa_required の存在に偶然気づきました。

guides.rubygems.org

gemリリース時のMFA *1 は元から設定していたんですが、gemspecに

spec.metadata = { "rubygems_mfa_required" => "true" }

# or

spec.metadata["rubygems_mfa_required"] = "true"

みたいのを書いておくことで *2 gemのリリースや削除でMFAが必須になってさらにセキュアになるので、この機会に手持ちのgemに軒並み rubygems_mfa_required をつけました。

github.com

多分3日がかりで40〜50個のgemに適用してリリースしたと思います

*1: https://rubygems.org/settings/edit で設定できるアカウントに対するMFA

*2:最近のbundlerだとbundle gemした時に最初からspec.metadataがついてるので既存設定を上書きしない後者がいいと思います

AWS 認定ソリューションアーキテクト – アソシエイト(AWS SAA-C02)に合格した

モチベーション

業務だとAWSGCPを半々くらい触っているんですが、GCPの認定資格である Professional Cloud Architect(通称PCA) は持っているのにAWSの認定資格を持っていないのはバランスが悪いのでPCAのAWS版ということでAWS 認定ソリューションアーキテクト – アソシエイト(AWS SAA-C02) をとってみることにしました *1

aws.amazon.com

1回目の試験は落ちて2回目で合格しました

俺氏スペック

  • 実務だとAWSは6~7年くらい、GCPは2~3年くらい *2

やったこと

1回目の試験

1回目は実力試しのつもりで参考書を軽く読んでチャレンジ。準備期間は1~2週間くらい。

1回目の時はあと2~3問正解してたくらいのスコアでした。

2回目の試験

1回目の試験を受ける前後で下記のエントリが出てたので参考にしました。

kiryuanzu.hatenablog.com

developers.prtimes.jp

上記エントリを参考にし、1回落ちたので本気を出すためにまずはUdemyの動画講座を受講しました。

www.udemy.com

平日は業務前後の時間を使って1日2~3時間ずつくらい受講。業務後は疲れて寝落ちする確率が高かったので朝受講することの方が多かったです。

32時間と長丁場だったので倍速も試したんですがあまり速いと頭に入ってこないので1.25倍速で聞いてました。(1.5倍速だと厳しかった)

全部の動画を見終わるのに1ヶ月くらいかかったと思います。講座の中にはハンズオンもあったのですが講座内のハンズオンはだいたい実務でやってたので特に手は動かしていないです。

動画を見終わった後に参考書をもう1冊買って勉強しました

参考書を読み終わって試験の直前にはUdemyの講座の最後の対策問題だけをもう1周やりました。

感想

他者に対して自分のスキルを客観的に伝える術が増えてよかったです。

www.credly.com

*1:資格とってから気づいたんだけどPCAのAWS版はSAP-C01の方だったかもしれない...

*2:Google App Engineは2009~2010年くらいから使ってたんだけど業務ではないのでノーカン

ISUCON11に1人チームで参加するためにやったこと #isucon

最初に

予選落ちなので勝者エントリを読みたい人はここで回れ右を推奨。

  • 使用言語:Ruby
  • 最終スコア:0
  • 最高スコア:5822

モチベーション

ISUCONの出題範囲であれば(多少濃淡あるけど)一応1人で全部できると思ってたので腕試しのために1人チームで参加しました。

ISUCON歴

ISUCON6(2016年)とISUCON9(2019年)にそれぞれ当時の同僚と参加したので今回が3回目。(どっちも予選落ち)

事前準備

準備期間

6月中旬くらいから本番を想定した素振り(1セット8時間)をやってたのでだいたい2ヶ月くらいです。

7月は毎週末8時間素振りをやって、平日の夜に感想戦をやってました。

そしてISUCON開催週は有給で全部休んでひたすら毎日10〜18時で素振りをしてました。

やったこと

ひたすら過去問をやっていました。

古すぎると今と傾向が違うので比較的新し目のをときつつ、予選と本戦では問題の傾向が全然違う(予選は割とオーソドックスだけど本戦はトリッキーな問題が多い)ので予選を重点的にやりました。

解いた問題数だけでいうと下記。(計13セット)

  • 6予選 x 1
  • 7予選 x 2
  • 8予選 x 2
  • 9予選 x 2
  • 9本戦 x 1
  • 10予選 x 3
  • 10本戦 x 1
  • 11事前講習 x 1

7月の週末と8月第3週目に草が濃いのはISUCON素振りのためです。 *1

f:id:sue445:20210822092120p:plain

いやまぁ、2ヶ月間で100時間以上使っても予選突破できなかったのでもう少し素振りの質を上げるべきだったかなと後悔。

事前に用意したもの&やったこと

Sentryのbillingを有効化した

普段 https://sentry.io/ は無料枠で利用してるんですが、ISUCONの素振りをやったら一瞬で無料枠を使い切ったのでTeamプラン(月26ドル)にしました

SentryのSpike Protectionを無効化した

Sentryには突発的にエラーが送られてきた時にそれをブロックして課金を抑えるSpike Protectionという機能がデフォルトで有効になってます。

blog.sentry.io

通常はこれは有効でいいんですがISUCONの場合この機能を有効にしてるとベンチマーク実行時のエラーがSentryに飛んでこないということがあったので無効にしました。

ちなみにSpike Protectionはプロジェクト単位ではなくorganization全体の設定なのでISUCONのチーム専用のorganizationを作ってそこで無効化するのがいいと思います。

f:id:sue445:20210822210805p:plain

デプロイスクリプト

素振りの度にリファクタリングしつつ、実際にISUCON11の予選で使った最終版は https://github.com/sue445/isucon11-qualify/blob/main/Rakefile になります

デプロイスクリプト解説

自分がRubyで参戦するし書き慣れてるのでRakefileを使いました。Makefileと違って動的にタスクを書きやすいのが特徴。

ここまで重厚になるとさすがにcapistranoでええんちゃう?って気持ちになるんですが、スニペットリポジトリからコピペするだけで使えるの便利なんすよね、、、

ローカルで rake を叩くだけで git push -> 本番全台で git pull & 必要なデプロイ処理を全部実施しつつ、スコア記録用のissueに下記のようなコメントをつけるところまでやってます。(ベンチマーカーで出たスコアは手でissueに貼り付けてる)

f:id:sue445:20210821213901p:plain

実際にスコア記録用に使ったissueは https://github.com/sue445/isucon11-qualify/issues/1 なのでこれ見てもらうと当日の雰囲気は伝わると思います。

こだわりポイント

  • 複数台に直列でデプロイすると遅いので multitask で並列デプロイできるようにしてる
  • サーバ全台にデプロイ後に /initialize を叩いて簡単な動作確認までやってる
    • ただ、今回のISUCONだとパラメータ無しで /initialize を叩けない仕様だったので /initialize_from_local というエンドポイントをはやしてそいつをRakefileから叩いています
  • /initialize まで終わったら 20210821-183139-sue445 のような日付ベースのtagを作ってpush
    • 最初はデプロイしたリビジョンだけissueにコメントするようにしてたんですが、それだと直前のデプロイからのcompareが取りづらいのでタイムスタンプベースのtagにしています
    • 実際に直前のデプロイからのcompareリンクを作ってるのは https://github.com/sue445/isucon11-qualify/blob/main/Rakefile#L166-L171
    • issueの最新のコメントに書かれてるリビジョンをparseしてもよかったんだけどこっちの方が手軽だったので採用
    • 昔の癖でtagにデプロイした人の名前を入れたけど1人チームだったので全然ありがたみはなかったw

スニペット

https://github.com/sue445/isucon11-qualify/tree/main/ruby/confighttps://github.com/sue445/isucon11-qualify/tree/main/infra開始直後にスニペットリポジトリからコピペした もので、configは必要に応じてrequireして使ってました。

過去問を解いてる過程であると便利だったユーティリティが https://github.com/sue445/isucon11-qualify/tree/main/ruby/config に集約されています。

スニペット系は元々個人esaに置いてたんですが、数が多くなってきたしテストコード書いてCIも回したくなってきたので専用のリポジトリを作りました。

その中からいくつかピックアップ

enable_monitoring.rb

アプリケーションコードを一切修正しなくてもファイル先頭でこれをrequireするだけでNewRelic, Sentry, Stackprofを有効化できる便利モンキーパッチ

NewRelicは便利なんだけどそれなりに重いのでどうしてもスコアが下がってしまいます。(過去問解いた感じだとNewRelicほどじゃないけどSentryもちょっと重かった)

設定が複数箇所にあると無効にするのが大変なので、ISUCON終了直前に一瞬で全部オフれるようにこのような構成になってます。

class Sinatra::Baseclass Mysql2::Clientオープンクラスしてモンキーパッチ仕込んでるのが個人的なおすすめポイントです。

実際の終了直前対応は https://github.com/sue445/isucon11-qualify/commit/fd049a5834a03df9705c83c05ff2b3f86042e6ae になんですが、アプリだけでいうと enable_monitoring のrequireをやめるだけですんでいます。

nr_mysql2_client.rb

https://github.com/newrelic/newrelic-ruby-agent は標準でmysql2をサポートしていません。

そこで下記エントリを参考に色々魔改造したのがnr_mysql2_clientです

ohbarye.hatenablog.jp

元のソースからの差分は下記

  • SQLのクエリに含まれてるテーブル名を正規表現で全部parseしつつ、joinやサブクエリなどで1つのSQL文で複数のテーブルが登場する場合には reservations,users のようにカンマ区切りで連結した文字列を返却
  • 実際に実行されたSQL/tmp/sql.log に出力
    • スロークエリログとるまでもないけど雑に見たいクエリ見たい時に便利
  • mysql2経由ではなく system "mysql ~" のようにRubyのアプリから直接システムのmysqlコマンドを叩いた時にもNewRelicにメトリクスが送ることができるようにした
    • 具体的には with_newrelicのブロック の中で system "mysql ~" を実行
    • 普通に考えたらアプリから system "mysql ~" を叩くなんてまずないんですがISUCON8の予選の過去問解いてた時に取得したレコード数が多すぎ *2 てpumaがメモリを食いすぎてサーバがOOMで死ぬということがあったのでそういうことも一応想定してました *3

最初の頃は

# Mysql2Client = Mysql2::Client
Mysql2Client = NRMysql2Client

def db
  Mysql2Client.new(
    host: @host,
    port: @port,
    username: @user,
    database: @db_name,
    password: @password,
    charset: 'utf8mb4',
    database_timezone: :local,
    cast_booleans: true,
    symbolize_keys: true,
    reconnect: true,
  )
end

のようにしてコメントで NRMysql2Client を使うかどうか切り替えてたんですが、面倒くさくなってきたので最終的に前述の enable_monitoring.rbclass Mysql2::Client に直接モンキーパッチ仕込むようにしました

redis_methods.rb , memcached_methods.rb

Rails.cache.fetch が好きすぎて with_rediswith_memcached のようなメソッドを作ってます。

Rubyだとmemcachedクライアントの dalli がバイナリプロトコルをサポートしてるのでRubyのオブジェクトをそのまま保存できるんですが、redisだとそれがない *4 のでちょっと苦労しました。

でそれぞれベンチマークをとった結果、ojが一番速かったのでredisにRubyのオブジェクトを保存する時はojを使うようにしました。

ベンチマークの結果は下記です。

github.com

nginx.conf

設定はいたって普通なんですが

include /home/isucon/webapp/infra/nginx/sites-enabled/*.conf;

のように /etc/nginx/nginx.conf から直接 /home/isucon/webapp 配下のconfをincludeするのが個人的なマイブームです。

チェックリスト

ISUCON11の事前講習 で講師の人が「『nginxのworker_connectionsが1024になってるか確認』ぐらいの細かい粒度でチェックリストを作るのがいい」と言ってたのでそれにならってチェックリスト作りました。

esa-pages.io

個人esaにチェックリストのテンプレを作って過去問を解く時に利用し、ちょいちょい更新していったら最終的にこんな感じになりました。

サーバ構築用のItamae

過去問だとサーバ1台構成である程度動かせた後にAMIを作って2台目以降はそれベースで作っていたので、個人esaに構築手順のコマンドをメモってコピペで構築していました。

しかし直前に「当日はCloudFormationを使ってサーバを構築する」というアナウンスがありました。

運営の人に質問したところCloudFormation以外でサーバを作ってもいいけどサポート対象外だしあまり推奨しないという旨の回答をもらったので急遽構築手順をItamaeでコード化しました。

github.com

このItamaeでやってるのは主に下記です

  • 各サーバ毎にhostnameの設定
  • 必要そうなpackageのインストール
  • 必要に応じてserviceの有効化と無効化
    • 自分はRubyで参加のためRubyの参考実装のserviceのみItamaeで有効化してる
    • redisとmemcachedもインストールはするけど使う時まで無効にして、デプロイスクリプト側で有効化
  • /etc/security/limits.confsoft nofilehard nofile を65536に設定
  • /etc/newrelic-infra.ymlenable_process_metrics (起動してるプロセスのメトリクスを送信)を有効化
  • /lib/systemd/system/mysql.serviceLimitNOFILE=65535 を設定
    • これをやらないと max_connections が増えなかったので脳死で設定
  • /home/isuconssh秘密鍵を作ったり自分のdotfilesリポジトリからtigやtmuxの設定ファイルをダウンロード
  • 最新のRubyをインストール
    • 今回の予選は実行環境に3.0.2がインストール済だったんですが、過去問のAMIだと当然Rubyのバージョンが古いです。素振りの時にRactorが使えないかの検証もやりたかったので過去問も常にRuby 3系でやってました

過去問素振り用のTerraform

個人のAWSアカウントで素振りするために下記のようなTerraformを書きました。

gist.github.com

Terraform 1.0.1, Terraform Provider for AWS 3.25.0で動作確認してますが多少古くても大丈夫なはず。

VPC, Subnet, SecurityGroup辺り全部入りなのでこれを既存のTerraformのリポジトリにコピペすればだいたい動くと思います。

過去問毎に想定スペックがまちまちだし前述の通り当初は1台ずつサーバ起動する想定だったのでEC2はTerraform化していません。

【おまけ】stackprof-webnavをRuby 3.0対応した

github.com

Ruby 3.0で素振りをしてた時にstackprof-webnavが動かなくて困ったのでいくつかパッチを投げました。

github.com

github.com

github.com

無事マージされてISUCON直前にRuby 3.0対応版がリリースされています。

github.com

余談ですがgemリリースが間に合わなかった場合を想定して自分のPR全部入りの状態で手元でビルドしてISUCON当日に gem install --local して使う手順も一応用意してました

当日やったこと

予選落ちなので多くは語りません。

リポジトリは公開してるのでコードの意図を知りたい場合はブログのコメントやTwitterとかで聞いてください。

github.com

敗因は下記

  • sinatraで返してる静的ファイルをnginxで返すのに苦労した
  • 途中から急に context deadline exceeded (Client.Timeout exceeded while awaiting headers) が出始めて原因調査に時間を取られた
    • 原因はstackprofだったんですが、まさか初手で入れてるstackprofのせいで途中から動かなくなるとは思わなかった。(過去問の素振りでも一切問題なかったので余計にハマった)

1人チームで参加した感想

過去に複数人チームで参加した時と比べて下記のようなメリデメがありました

1人チームのメリット

  • 他のメンバーとの共有作業が不要
    • 複数人だとぱっと思いつくだけでリポジトリAWS、NewRelic、Sentry、作業時のチェックリストやスニペット集あたりの共有が必要だと思う
  • コードレビュー不要
    • 普通のチームだとトピックブランチ作ってpushしてPR出してレビュー後にマージという流れになるが、1人チームだとレビューしようがないしブランチ作るのも面倒なので必然的にmainブランチでガンガンコミットしていくことになる
  • 複数人での認識共有や意思疎通などのコミュニケーション作業が不要
  • 他メンバーの予定を気にせずに好きな時間に好きなだけ素振りができる
  • 実行環境を壊しても誰にも迷惑かけない
    • ローカルでアプリを動かす必要がないのでガンガンデプロイしてガンガンベンチまわせる

1人チームのデメリット

  • 1人

おまけ:素振りで使ったAWSの費用

ISUCON終わってAWSの個人アカウントの予算アラートの設定値も元に戻すのでその前に記念スクショ

f:id:sue445:20210823094917p:plain

f:id:sue445:20210823094932p:plain

*1:https://github.com/sue445

*2:https://github.com/isucon/isucon8-qualify/blob/master/webapp/ruby/lib/torb/web.rb#L423-L459

*3: https://isucon.net/archives/52520045.html によるとmysql_use_resultって選択肢もあったんだけどmysql2でmysql_use_resultを使う方法がよく分からなかったのでアプリから SELECT ... INTO OUTFILE を使ってた

*4:redisに保存すると基本的に全部stringになる

*5:後述のstackprof-webnavで必要だった

ISUCONの素振りでisucon11-priorをやった

先週末にisucon11-prior(事前講習のハンズオン課題)をやったのでメモ

github.com

isucon.net

結果

  • 使用言語:Ruby
  • 初期スコア:1765
  • 最終スコア:13226(8時間で出せたスコア)
  • 感想戦後:15101

最終的なサーバ構成

スペックはいずれもc5.large

  • front *1 兼DB x 1 *2
  • AP*3 x 2
  • bench x 1

コンテキスト

  • 1人チーム

事前にやったこと:AMI作成

AMIの作り方は https://github.com/matsuu/vagrant-isucon11-prior がむっちゃ参考になりました。

github.com

作成手順

  1. ubuntu-focalでEC2インスタンスを作る
    • マニュアル によれば演習時のインスタンスタイプは c5.large を想定してるらしいが、AMI作成時は関係ないのでスペック上げておけば時間短縮できる
      • 参考:c5.large だと3のコマンド実行で15分くらいかかる
    • ボリュームサイズはデフォルトの8GiBでOK
      • ボリュームサイズを増やしすぎるとそのAMIから作るEC2インスタンスもそれに引きづられるのでむしろダメ
      • 例:50GiBでAMIを作るとEC2インスタンスは50GiB以上じゃないと作れなくなる
    • セキュリティグループで最低限22番ポートだけは開放する
  2. sshでログイン
    • ssh -i /path/to/ec2_key_pair.pem ubuntu@PUBLIC_IP
  3. EC2インスタンス内で https://github.com/matsuu/vagrant-isucon11-prior/blob/655df852ee1539e763b1fb82c0251a43d333c33a/Vagrantfile#L78-L91 を1行ずつ実行
    • エラーになったらsudoをつけて再実行
  4. ログアウトしてAMIを作成開始
  5. AMIの作成後にEC2インスタンスを止めるか削除する

AMIから起動したEC2インスタンスへisuconユーザでログインする方法

ubuntuユーザでログイン後にauthorized_keysをコピーするのが楽

  1. ubuntuユーザでログイン
    • ssh -i /path/to/ec2_key_pair.pem ubuntu@PUBLIC_IP
  2. EC2インスタンス内で下記を実行
    • sudo cp /home/ubuntu/.ssh/authorized_keys /home/isucon/.ssh/authorized_keys
  3. isuconユーザでログイン
    • ssh -i /path/to/ec2_key_pair.pem isucon@PUBLIC_IP

8時間の素振りでやったこと一覧

  • 時系列順
  • 本番を想定して10〜18時で素振りをしてます

AMI起動してベンチ即実行

この時点ではbenchとAPが同一

10:12:14.150419 ERR: validation: invalid: overbooking at 01FCM7DTTC8C6Y3B4PVR073T4C
10:12:14.447652 score: 1765(1815 - 50) : pass
10:12:14.447664 deduction: 50 / timeout: 0

NewRelicやSentryを仕込む

11:28:15.032062 SCORE: create-schedule: 18
11:28:15.032074 SCORE: create-reservation: 1239
11:28:15.032086 SCORE: login: 235
11:28:15.032094 SCORE: signup: 234
11:28:15.032164 score: 1554(1654 - 100) : pass
11:28:15.032171 deduction: 100 / timeout: 0

APとDBとbenchを分離

この時点のAPサーバのAMIを作って、そのAMIからDBサーバを作成

11:46:06.535986 SCORE: login: 242
11:46:06.536002 SCORE: signup: 241
11:46:06.536005 SCORE: create-schedule: 19
11:46:06.536007 SCORE: create-reservation: 1298
11:46:06.536010 score: 1730(1730 - 0) : pass
11:46:06.536012 deduction: 0 / timeout: 0

pumaの設定調整

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/config/puma.rb を下記のように修正

root = File.expand_path('..', __dir__)

# workers 1

directory root
rackup File.join(root, 'config.ru')
bind 'tcp://0.0.0.0:9292'
environment ENV.fetch('RACK_ENV') { 'development' }
pidfile File.join(root, 'tmp', 'puma.pid')

threads 0, 32

workers 32

preload_app!

log_requests false

# for puma 5+
# Recommended 0.001~0.010(default 0.005)
wait_for_less_busy_worker 0.005

nakayoshi_fork true

wait_for_less_busy_workernakayoshi_fork は下記参照

isucon.net

11:51:03.549932 SCORE: create-reservation: 1581
11:51:03.549946 SCORE: login: 292
11:51:03.549952 SCORE: signup: 291
11:51:03.549962 SCORE: create-schedule: 22
11:51:03.550041 score: 2092(2093 - 1) : pass
11:51:03.550050 deduction: 1 / timeout: 2

NewRelicの設定がうまく反映されていなかったので修正

12:03:43.493894 deduction: 0 / timeout: 1
12:03:55.091536 SCORE: login: 220
12:03:55.091551 SCORE: signup: 219
12:03:55.091554 SCORE: create-schedule: 17
12:03:55.091557 SCORE: create-reservation: 1157
12:03:55.091591 score: 1547(1547 - 0) : pass
12:03:55.091598 deduction: 0 / timeout: 1

get_reservationsのN+1を修正

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L46-L50 がN+1だったので下記のようにして1クエリでとれるように修正

    def get_reservations(schedule)
      sql = <<~SQL
        SELECT 
          r.id AS reservation_id,
          r.schedule_id AS reservation_schedule_id,
          r.user_id AS reservation_user_id,
          r.created_at AS reservation_created_at,
          u.id AS user_id,
          u.email AS user_email,
          u.nickname AS user_nickname,
          u.staff AS user_staff,
          u.created_at AS user_created_at
        FROM `reservations` AS r
        INNER JOIN users u ON u.id = r.user_id
        WHERE r.schedule_id = ?
      SQL
      rows = db.xquery(sql, schedule[:id])
      reservations = rows.each_with_object([]) do |row, reservations|
        reservation = {
          id: row[:reservation_id],
          schedule_id: row[:reservation_schedule_id],
          user_id: row[:reservation_user_id],
          created_at: row[:reservation_created_at],
        }
        reservation[:user] = {
          id: row[:user_id],
          email: row[:user_email],
          nickname: row[:user_nickname],
          staff: row[:user_staff],
          created_at: row[:user_created_at],
        }
        reservation[:user][:email] = '' if !current_user || !current_user[:staff]

        reservations << reservation
      end
12:18:30.912796 SCORE: create-schedule: 18
12:18:30.912813 SCORE: create-reservation: 1294
12:18:30.912825 SCORE: login: 250
12:18:30.912832 SCORE: signup: 249
12:18:30.912839 score: 1724(1724 - 0) : pass
12:18:30.912846 deduction: 0 / timeout: 0

POST /api/reservationsのループを修正

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L149-L152 のループが無駄だったので下記のようにSQLでcountとるようにした

reserved = tx.xquery('SELECT COUNT(*) as cnt FROM `reservations` WHERE `schedule_id` = ?', schedule_id).first[:cnt].to_i
12:40:14.739025 SCORE: login: 272
12:40:14.739040 SCORE: signup: 271
12:40:14.739233 SCORE: create-schedule: 22
12:40:14.739238 SCORE: create-reservation: 1437
12:40:14.739271 score: 1929(1929 - 0) : pass
12:40:14.739277 deduction: 0 / timeout: 2

POST /api/reservationsでINSERT後にSELECTしてるのが無駄だったのでINSERT時にcreated_atを入れるようにした

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L156-L157

      created_at = Time.now
      tx.xquery('INSERT INTO `reservations` (`id`, `schedule_id`, `user_id`, `created_at`) VALUES (?, ?, ?, ?)', id, schedule_id, user_id, created_at)

のようにした

12:43:37.171543 SCORE: login: 242
12:43:37.171557 SCORE: create-schedule: 14
12:43:37.171567 SCORE: signup: 241
12:43:37.171570 SCORE: create-reservation: 1280
12:43:37.171576 score: 1662(1662 - 0) : pass
12:43:37.171585 deduction: 0 / timeout: 0

current_userのメモ化

NewRelic見てたらむっちゃusersへのSELECTが多くて気になって調べたら current_user で毎回SELECTしてたのでメモ化した

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L42-L44

    def current_user
      @current_user ||= db.xquery('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', session[:user_id]).first
    end

のようにするだけでスコアが倍になって大爆笑した記憶

12:48:49.106668 SCORE: create-schedule: 38
12:48:49.106677 SCORE: signup: 492
12:48:49.106728 SCORE: create-reservation: 2604
12:48:49.106767 SCORE: login: 493
12:48:49.106930 score: 3326(3477 - 151) : pass
12:48:49.106938 deduction: 151 / timeout: 0

GET /api/schedules のN+1修正(不完全)

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L164-L167 のN+1を下記のように修正

    sql = <<~SQL
      SELECT 
        s.id AS id,
        s.title AS title,
        s.capacity AS capacity,
        s.created_at AS created_at,
        r.reserved AS reserved
      FROM `schedules` AS s
      INNER JOIN (
        SELECT 
          schedule_id,
          COUNT(*) AS reserved
        FROM reservations
        GROUP BY schedule_id
      ) r ON s.id = r.schedule_id
      ORDER BY s.id DESC
    SQL
    schedules = db.xquery(sql)

スコアは増えてるんだけど同時にベンチマーカーでpanic出すようになって謎だった。この時点ではよくわからんのでそのまま進めた

13:00:14.156606 deduction: 0 / timeout: 4
13:00:17.157713 SCORE: create-schedule: 710
13:00:17.157731 SCORE: login: 7
13:00:17.157735 SCORE: signup: 6
13:00:17.157986 score: 7107(7107 - 0) : pass
13:00:17.157995 deduction: 0 / timeout: 4
panic: runtime error: index out of range [-1]

goroutine 75533 [running]:
main.(*Scenario).Validation.func1(0x7d08e8, 0xc0008d4420, 0xffffffffffffffff)
/home/isuadmin/src/isucon11-prior/benchmarker/scenario.go:304 +0xeec
github.com/isucon/isucandar/worker.(*Worker).processInfinity.func1(0x7d08e8, 0xc0008d4420)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/worker/worker.go:72 +0x46
github.com/isucon/isucandar/parallel.(*Parallel).Do.func1(0xc000a58d80, 0xc000312a80, 0x0)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:68 +0x73
created by github.com/isucon/isucandar/parallel.(*Parallel).Do
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:66 +0xa6

sinatraでindex.htmlを返さないようにした

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L182-L184sinatraで静的ファイルを読み込んで返しているのが無駄だったので、下記のようにnginxで返すようにした

  location /initialize {
    try_files $uri @webapp;
  }

  location /api/ {
    try_files $uri @webapp;
  }

  location ^~ / {
  }
13:16:12.892609 SCORE: signup: 6
13:16:12.892626 SCORE: login: 7
13:16:12.892631 SCORE: create-schedule: 855
13:16:12.893000 score: 8557(8557 - 0) : pass
13:16:12.893008 deduction: 0 / timeout: 5
panic: runtime error: index out of range [-1]

goroutine 185836 [running]:
main.(*Scenario).Validation.func1(0x7d08e8, 0xc0004a7440, 0xffffffffffffffff)
/home/isuadmin/src/isucon11-prior/benchmarker/scenario.go:304 +0xeec
github.com/isucon/isucandar/worker.(*Worker).processInfinity.func1(0x7d08e8, 0xc0004a7440)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/worker/worker.go:72 +0x46
github.com/isucon/isucandar/parallel.(*Parallel).Do.func1(0xc000bb8140, 0xc000c2a090, 0x0)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:68 +0x73
created by github.com/isucon/isucandar/parallel.(*Parallel).Do
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:66 +0xa6

generate_idでSELECTするのをやめた

マニュアル には

各エンドポイントの URI の変更は認められませんが、以下の点については明確に許可されます。

・ID 発行形式の変更

って書いてあったので https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L26-L32 でSELECTするのをやめて

    def generate_id(table, tx)
      SecureRandom.uuid
    end

のように修正。(呼び出し元を変更するのが面倒だったので引数はそのまま)

ULIDからUUIDに変えた理由はよく覚えてない。

13:26:43.131061 SCORE: login: 7
13:26:43.131078 SCORE: signup: 6
13:26:43.131083 SCORE: create-schedule: 851
13:26:43.131215 score: 8517(8517 - 0) : pass
13:26:43.131220 deduction: 0 / timeout: 5
panic: runtime error: index out of range [-1]

goroutine 184194 [running]:
main.(*Scenario).Validation.func1(0x7d08e8, 0xc0004d1da0, 0xffffffffffffffff)
/home/isuadmin/src/isucon11-prior/benchmarker/scenario.go:304 +0xeec
github.com/isucon/isucandar/worker.(*Worker).processInfinity.func1(0x7d08e8, 0xc0004d1da0)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/worker/worker.go:72 +0x46
github.com/isucon/isucandar/parallel.(*Parallel).Do.func1(0xc0006135c0, 0xc000a23660, 0x0)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:68 +0x73
created by github.com/isucon/isucandar/parallel.(*Parallel).Do
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:66 +0xa6

POST /api/signupでINSERT後にSELECTするのをやめた

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L98-L99 にも POST /api/reservations と同じ問題があったので

      created_at = Time.now
      tx.xquery('INSERT INTO `users` (`id`, `email`, `nickname`, `created_at`) VALUES (?, ?, ?, ?)', id, email, nickname, created_at)

のように修正

13:30:30.845231 SCORE: login: 7
13:30:30.845248 SCORE: signup: 6
13:30:30.845253 SCORE: create-schedule: 874
13:30:30.846415 score: 8735(8747 - 12) : pass
13:30:30.846424 deduction: 0 / timeout: 127
panic: runtime error: index out of range [-1]

goroutine 185720 [running]:
main.(*Scenario).Validation.func1(0x7d08e8, 0xc000181c80, 0xffffffffffffffff)
/home/isuadmin/src/isucon11-prior/benchmarker/scenario.go:304 +0xeec
github.com/isucon/isucandar/worker.(*Worker).processInfinity.func1(0x7d08e8, 0xc000181c80)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/worker/worker.go:72 +0x46
github.com/isucon/isucandar/parallel.(*Parallel).Do.func1(0xc000a2cd00, 0xc0009e7950, 0x0)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:68 +0x73
created by github.com/isucon/isucandar/parallel.(*Parallel).Do
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:66 +0xa6

netdataをDBサーバに流す

netdata が重いかもと思って、そんなに負荷の高くなかったDBサーバに流すようにしたけどそんなにスコア変わらなかった

13:39:49.441780 SCORE: create-schedule: 871
13:39:49.441799 SCORE: login: 7
13:39:49.441802 SCORE: signup: 6
13:39:49.442042 score: 8717(8717 - 0) : pass
13:39:49.442068 deduction: 0 / timeout: 4

panic: runtime error: index out of range [-1]

goroutine 185720 [running]:
main.(*Scenario).Validation.func1(0x7d08e8, 0xc000181c80, 0xffffffffffffffff)
/home/isuadmin/src/isucon11-prior/benchmarker/scenario.go:304 +0xeec
github.com/isucon/isucandar/worker.(*Worker).processInfinity.func1(0x7d08e8, 0xc000181c80)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/worker/worker.go:72 +0x46
github.com/isucon/isucandar/parallel.(*Parallel).Do.func1(0xc000a2cd00, 0xc0009e7950, 0x0)
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:68 +0x73
created by github.com/isucon/isucandar/parallel.(*Parallel).Do
/home/isuadmin/go/pkg/mod/github.com/isucon/isucandar@v0.0.0-20210609100057-baa52484de01/parallel/parallel.go:66 +0xa6

前の方にやった「GET /api/schedules のN+1修正(不完全)」をrevert

ずっとpanic出てるのもアレだったので本格的になおすためにいったんrevertした

13:48:51.002083 SCORE: create-schedule: 41
13:48:51.002098 SCORE: signup: 521
13:48:51.002111 SCORE: create-reservation: 2878
13:48:51.002120 SCORE: login: 522
13:48:51.002146 score: 3810(3810 - 0) : pass
13:48:51.002154 deduction: 0 / timeout: 1

GET /api/schedules のN+1修正(完全版)

最終的にこれでなおった。( INNER JOINLEFT JOIN に変えた)

    sql = <<~SQL
      SELECT 
        s.id AS id,
        s.title AS title,
        s.capacity AS capacity,
        s.created_at AS created_at,
        IFNULL(r.reserved, 0) AS reserved
      FROM `schedules` AS s
      LEFT JOIN (
        SELECT 
          schedule_id,
          COUNT(*) AS reserved
        FROM reservations
        GROUP BY schedule_id
      ) r ON s.id = r.schedule_id
      ORDER BY s.id DESC
    SQL
    schedules = db.xquery(sql)

panicしてた時ほどスコアは増えなかったのでスコア8000はバグだったのか...

13:56:18.541470 SCORE: create-schedule: 40
13:56:18.541484 SCORE: create-reservation: 3265
13:56:18.541494 SCORE: login: 600
13:56:18.541497 SCORE: signup: 599
13:56:18.541607 score: 4262(4265 - 3) : pass
13:56:18.541614 deduction: 3 / timeout: 2

reservationsにindexつけた

ALTER TABLE reservations ADD INDEX schedule_id (`schedule_id`);
ALTER TABLE reservations ADD INDEX user_id (`user_id`);
14:09:07.199826 SCORE: create-reservation: 3352
14:09:07.199843 SCORE: login: 612
14:09:07.199847 SCORE: signup: 611
14:09:07.199853 SCORE: create-schedule: 45
14:09:07.199858 score: 4414(4414 - 0) : pass
14:09:07.199863 deduction: 0 / timeout: 0

ブログ書きながら気づいたんだけど https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L146 があったので schedule_iduser_id の複合indexでもよかったかも

POST /api/schedules でINSERT後にSELECTしないようにした

他エンドポイントで修正済のやつを見逃してたので https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L129-L130

      created_at = Time.now
      tx.xquery('INSERT INTO `schedules` (`id`, `title`, `capacity`, `created_at`) VALUES (?, ?, ?, ?)', id, title, capacity, created_at)

のようになおした

14:16:39.149303 SCORE: create-reservation: 3460
14:16:39.149331 SCORE: login: 626
14:16:39.149337 SCORE: signup: 625
14:16:39.149347 SCORE: create-schedule: 47
14:16:39.149420 score: 4555(4556 - 1) : pass
14:16:39.149428 deduction: 1 / timeout: 1

2台目のAP追加

14:27:18.376503 SCORE: login: 747
14:27:18.376518 SCORE: signup: 746
14:27:18.376523 SCORE: create-schedule: 55
14:27:18.376526 SCORE: create-reservation: 4102
14:27:18.376530 score: 5399(5399 - 0) : pass
14:27:18.376533 deduction: 0 / timeout: 0

このあとpumaのworkersとかthreadsを色々いじったがそんなに改善しなかった

DBとnginxを同居

この時点では

  • front + AP
  • AP
  • DB

だったと思うんだけど(うろ覚え)、冒頭にも書いた通りDBの負荷がそんなに高くなかったのでfrontとDBを同居させて

  • front + DB
  • AP
  • AP

のような構成にした

14:51:04.770444 SCORE: login: 619
14:51:04.770470 SCORE: signup: 618
14:51:04.770475 SCORE: create-schedule: 47
14:51:04.770480 SCORE: create-reservation: 3445
14:51:04.770488 score: 4534(4534 - 0) : pass
14:51:04.770491 deduction: 0 / timeout: 0

netdataを3台に増やした

14:59:57.771297 SCORE: login: 723
14:59:57.771312 SCORE: signup: 722
14:59:57.771323 SCORE: create-schedule: 53
14:59:57.771326 SCORE: create-reservation: 3903
14:59:57.771329 score: 5156(5156 - 0) : pass
14:59:57.771334 deduction: 0 / timeout: 0

parallelismを40にした

この時点で各サーバともCPU使用率50%前後止まりでスコアが伸び悩んでいたのでダメ元で -parallelism (ベンチマーカーのパラメータ)を増やしたらスコアが増えた(が、timeoutも増えた...)

15:19:27.198058 SCORE: login: 462
15:19:27.198083 SCORE: signup: 461
15:19:27.198093 SCORE: create-schedule: 84
15:19:27.198100 SCORE: create-reservation: 4875
15:19:27.206160 score: 6059(6177 - 118) : pass
15:19:27.206173 deduction: 3 / timeout: 1154

設定調整祭

この辺でひたすらpumaやnginxの設定を調整しまくってた

parallelismを20に戻した

timeoutが多くてスコアが伸び悩んでそうだったのでダメ元でparallelismを20に戻したらスコアが改善した。どうして...(現場猫顔)

17:45:53.615559 SCORE: signup: 679
17:45:53.615574 SCORE: create-schedule: 98
17:45:53.615576 SCORE: create-reservation: 6925
17:45:53.615579 SCORE: login: 680
17:45:53.616384 score: 8550(8585 - 35) : pass
17:45:53.616392 deduction: 34 / timeout: 18

終了直前対応(その1)

nginxとmysqlのログを止めて、SentryとNewRelic(APMとinfra agentの両方)を無効化

17:51:08.566625 SCORE: login: 1093
17:51:08.566640 SCORE: signup: 1092
17:51:08.566650 SCORE: create-schedule: 138
17:51:08.566665 SCORE: create-reservation: 10580
17:51:08.566901 score: 13051(13053 - 2) : pass
17:51:08.566910 deduction: 1 / timeout: 16

puma設定修正

「NewRelic止めたからpumaのworker数増やせるのでは?」と思って増やしたらスコアが減った...

17:56:47.793917 SCORE: create-reservation: 10370
17:56:47.794019 SCORE: login: 1240
17:56:47.794073 SCORE: signup: 1239
17:56:47.794124 SCORE: create-schedule: 138
17:56:47.796378 score: 12725(12990 - 265) : pass
17:56:47.796530 deduction: 250 / timeout: 152

終了直前対応(その2)

pumaのworker数増やす設定をrevertして終了時間ギリギリでデプロイ

17:59:03.124528 SCORE: login: 1259
17:59:03.124632 SCORE: signup: 1258
17:59:03.124642 SCORE: create-schedule: 143
17:59:03.124651 SCORE: create-reservation: 10539
17:59:03.125527 score: 13226(13228 - 2) : pass
17:59:03.125539 deduction: 0 / timeout: 21

感想戦

メトリクス見れるようにするために終了直前対応をrevertしてスコア計測

21:14:05.874582 SCORE: login: 952
21:14:05.874616 SCORE: signup: 951
21:14:05.874624 SCORE: create-schedule: 104
21:14:05.874630 SCORE: create-reservation: 6597
21:14:05.874955 score: 8585(8589 - 4) : pass
21:14:05.874963 deduction: 3 / timeout: 19

usersにemail index追加

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L110email 使ってるのにindex無いやんけって気づいてindex追加

ALTER TABLE users ADD INDEX email (`email`);
21:36:18.462310 SCORE: login: 848
21:36:18.462325 SCORE: signup: 847
21:36:18.462329 SCORE: create-schedule: 93
21:36:18.462332 SCORE: create-reservation: 7049
21:36:18.462454 score: 8826(8827 - 1) : pass
21:36:18.462465 deduction: 0 / timeout: 10

json_encoderをActiveSupport::JSONからojに変更

ActiveSupport::JSON が重そうだったので https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L15 をojに変更

json_encoderをojに差し替える方法は下記を参考にした

qiita.com

21:57:01.325176 SCORE: login: 1044
21:57:01.325190 SCORE: signup: 1043
21:57:01.325201 SCORE: create-schedule: 119
21:57:01.325203 SCORE: create-reservation: 8102
21:57:01.325620 score: 10325(10336 - 11) : pass
21:57:01.325628 deduction: 10 / timeout: 18

再度終了直前対応

22:08:45.723600 SCORE: create-reservation: 12100
22:08:45.723613 SCORE: login: 1290
22:08:45.723615 SCORE: signup: 1289
22:08:45.723618 SCORE: create-schedule: 151
22:08:45.723879 score: 14899(14900 - 1) : pass
22:08:45.723887 deduction: 1 / timeout: 3

sinatraのログを無効化

https://github.com/isucon/isucon11-prior/blob/aea2af2bdfdeab5905f60bdb79292b0ef44dd354/webapp/ruby/app.rb#L9 を見てて「sinatraのログ出力(出力先はsystemd)」も重そうだなあと思ってdisableにした

22:27:05.030386 SCORE: login: 1267
22:27:05.030401 SCORE: signup: 1266
22:27:05.030407 SCORE: create-schedule: 167
22:27:05.030412 SCORE: create-reservation: 12165
22:27:05.030627 score: 15101(15102 - 1) : pass
22:27:05.030632 deduction: 1 / timeout: 9

感想

1人チームなのでstakprofやalpみたいな手元で解析するツールは諦めて解析は全部NewRelicに任せてたんだけど、限界が出てきたのでそろそろstackprofくらいは使えるようにならないとなあという気持ちになってる

*1:ベンチマーカーのリクエストを受けるnginxがいるサーバ

*2:色々改善した結果DBの負荷がそんなに高くなかったのでnginxと同居させた

*3:pumaが動いてるアプリケーションサーバ

至極の難問AWS & GCPクイズ

社内勉強会で発表したら好評だったので公開

注意

  • 全問解けても仕事の役にはたちません
  • ググるのは禁止

問題1

Q: 正しい組み合わせはどれか?

  1. AWS: CloudShell / GCP CloudShell
  2. AWS: Cloud Shell / GCP CloudShell
  3. AWS: CloudShell / GCP Cloud Shell
  4. AWS: Cloud Shell / GCP Cloud Shell

回答

正解: 3 (AWS: CloudShell / GCP Cloud Shell)

  • AWSがCloudShell(空白が入らない)で、GCPがCloud Shell(空白が入る)です
  • AWSは単語と単語の間に空白を入れない印象がある

問題2

Q: 正しい組み合わせはどれか?

  1. Amazon Workspace / Google Workspace
  2. Amazon Workspaces / Google Workspace
  3. Amazon Workspace / Google Workspaces
  4. Amazon Workspaces / Google Workspaces

回答 正解: 2 (Amazon Workspaces / Google Workspace)

  • Amazon Workspacesのみ複数形です
  • 名前は似ていますが両者は全く違うサービスです

問題3

Q: AWS KMSとGCP KMSの正式名称で正しい組み合わせはどれか?

  1. AWS Key Management Service / Google Cloud Key Management Service
  2. AWS Key Management System / Google Cloud Key Management Service
  3. AWS Key Management Service / Google Cloud Key Management System
  4. AWS Key Management System / Google Cloud Key Management System

回答 正解: 1 (AWS Key Management Service / Google Cloud Key Management Service)

  • KMSとは両方とも機微情報の管理に使うマネージドサービスです
  • KMSは両方とも「Key Management Service」が正式名称でした(引っ掛け問題)

問題4

Q: 正しい組み合わせはどれか?

  1. AWS: Secret Manager / GCP: Secret Manager
  2. AWS: Secrets Manager / GCP: Secret Manager
  3. AWS: Secret Manager / GCP: Secrets Manager
  4. AWS: Secrets Manager / GCP: Secrets Manager

回答 正解: 2 (AWS: Secrets Manager / GCP: Secret Manager)

  • 問題2と同様にAWSのみ複数形です
  • 両方とも機微情報管理のマネージドサービスです

問題5

Q: 下記の中で2021年5月末時点でGA(General Availability)済のサービスはどれか?

  1. Amazon EC2 Anywhere
  2. Amazon ECS Anywhere
  3. Amazon EKS Anywhere
  4. 1〜3全部

回答

正解: 2 (Amazon ECS Anywhere)

  • EC2 Anywhereは2021年5月末時点で名前は一切出ていない
  • ECS Anywhereはオンプレミスのサーバ上でECSを動かすためのサービス。2021年5月27日GA済
  • EKS Anywhereはオンプレミスのサーバ上でEKSを動かすためのサービス。2021年リリース予定だが2021年5月末時点ではまだGAされてない

おまけ:発表中の社内の反応

  • これは仕事の役に立たないwwwwwwwwwwwwww
  • えぐいやつだった
  • 選択肢の違いがわからない????
  • 何もわからない

余談

1年前の僕

2ヶ月前の僕

関連クイズ

sue445.hatenablog.com

GitHub-native DependabotでもAuto mergeをやりたかった

tl;dr;

いくつかやり方はあるんだけどどれも一長一短で決定打はなかった

前置き

Dependabot Previewの終了がアナウンスされました。

github.blog

Auto-merge: We always recommend verifying your dependencies before merging them; therefore, auto-merge will not be supported for the foreseeable future. For those of you who have vetted your dependencies, or are only using internal dependencies, we recommend adding third-party auto-merge apps, or setting up GitHub Actions to merge.

と書いてるようにGitHub-native Dependabot(新しい方)ではAuto mergeがサポートされないようです。

Dependabot Preview(古い方)では automerged_updates を使ってたので本腰入れて調べてみました。

ちなみに自動マージをやりたいモチベーションは個人的な理由なので大抵の人には当てはまらないと思います。(20個以上の個人アプリでdependabotを入れてると大量にPRきた時にマージするだけでも大変だし一気にマージするとデプロイのqueueが詰まる(CircleCIのjobの同時実行数はアカウント単位)ので、開発系のライブラリや本番系のライブラリでもパッチバージョンのバージョンアップ時は自動マージを許したい)

GitHub Actionsでやる方法

https://github.com/marketplace?type=actions&query=dependabot+merge で調べてStar数の多いものや定期的に更新されてるものをピックアップしました

Dependabot Auto Merge

github.com

pull_requestイベントかpull_request_targetイベントで実行される。執筆時点での最新版は2.4.0

メリット

  • Dependabot Previewautomerged_updates 相当の設定がある
  • APIから @dependabot merge みたいなコメントをして実現してるのでGitHub-native Dependabotと100%の互換性がある

デメリット

  • 前述の仕様のためPersonal Access Token(PAT)が必要
    • publicリポジトリだけなら public_repo スコープだけでいいのでギリギリ許容できなくはないが、トークン漏洩時のリスクを考えるとなるべくスコープを狭くしたい
    • ためしにApp Tokenを使ってみたらコメント自体は成功したけどDependabot側でエラーになったので、おそらくPATのユーザにリポジトリのWrite権限があるかをチェックしてるように見える

f:id:sue445:20210504181545p:plain

備考

GitHub-native Dependabotの仕様としてpublicリポジトリのpull_requestイベントだとsecretsが取れないのでPATの受け渡しができずにエラーになるのでpull_request_targetイベントを使う必要があるのですが、pull_request_targetイベントだと今度はセキュリティリスクがあるので注意する必要があります。

github.com

https://github.com/ahmadnassri/action-dependabot-auto-merge/issues/60#issuecomment-806170152 みたいに github.actor でチェックするしかなさそう。

Merge me!

github.com

workflow_runイベントによって別のジョブの終了時に実行される。執筆時点での最新版は1.2.62

メリット

  • secrets.GITHUB_TOKEN が使える

デメリット

  • presetで DEPENDABOT_MINORDEPENDABOT_PATCH はあるものの現時点ではDependabot Previewほどの柔軟さはない
  • workflow_runの仕様上リポジトリ上はGitHub Actionsで統一していないと使えない
    • CircleCIのジョブ終了時にGitHub Actionsを発火させるのはできなくはないが大変そう

Dependabot Pull Request Action

github.com

scheduleイベントで定期実行する。執筆時点での最新版は1.0.0

メリット

  • secrets.GITHUB_TOKEN が使える

デメリット

  • 設定は豊富だが現時点ではDependabot Previewautomerged_updates を完全に置き換えるのは難しそう
  • dependabotを実行する時間にあわせてscheduleの時間も調整する必要がある
    • 寝てる間にいい感じにマージされていればいいのであれば雑に30分後とか1時間後にセットすればいいけど、そのラグが許容できないとストレスかも

番外編

Renovateを使う

github.com

Renovateであればauto merge対応してるのでDependabotから移行するのも1つの手なのだが、DependabotはGitHub公式という強みがあるので移行するかどうかはかなり悩ましい

docs.renovatebot.com

近況

手持ちのリポジトリ全部移行する前にDependabot Auto MergeとMerge me!を1リポジトリずつ入れて試験運用してみた結果、下記の理由でDependabot Auto Mergeを採用しました

  • PATを各リポジトリにコピペしてまわる手間はあるものの automerged_updates 対応してるのは強い
  • 手持ちのリポジトリだとCircleCIも使っているのでMerge me!が使えない

gitpanda v0.9.5をリリースした

github.com

https://github.com/sue445/gitpanda/blob/master/CHANGELOG.md#v095

gitpanda的にはこれといって新機能は無いのですが GitHub Container Registryが secrets.GITHUB_TOKEN に対応した ためDockerイメージをGitHub Container Registryにホスティングするようにしてみました。

https://github.com/users/sue445/packages/container/package/gitpanda

ビルド済のDockerイメージがあるのでCloud Run辺りで動かしやすいかもしれません。