生涯未熟

生涯未熟

プログラミングをちょこちょこと。

Authyで発行される2段階認証コードを1Passwordでも発行する

最近はどんなサービスにも2段階認証は付いていますが、その2段階認証コードを発行するAuthenticatorであるAuthyについてのお話をば。

AuthenticatorはAuthyに限らずGoogle Authenticatorなど色々ありますが、複数のAuthenticatorを使ってるよ!って方は少ないと思います。
出来れば一つのAuthenticatorで済ませたいのですが、例えばメールのデリバリーサービスで有名なSendGridはAuthyのみが利用できるAuthenticatorとして設定できます。
SendGridもAuthyもTwilioが作っているので、そうしたくなる気持ちは分かるのですが利用者としては非常に不便であります。

また、1Passwordで2段階認証コードを発行してチームメンバーで共有するといったことをしているのですが、そういった意味でもAuthyのみの制限をかけられると非常に困るわけです。

SendGridではAuthy以外にSMSでの2段階認証も利用できるので、最初はSMS認証にして認証メッセージが届いたIFTTTでSlackに投稿するみたいな対応を取っていました。
しかし、IFTTTがいつの間にかバックグラウンドで殺されていてSlackに投稿できていなかったりという事象が多発し、これはどうにもならんなぁと匙を投げていました。

そんな折に、久々にこのあたりの問題どうにかできないかなーとインターネットを散策しているとこんなGistを発見👀

Export TOTP tokens from Authy · GitHub

うおー!探し求めていたものだ!

記事内でやっていることとしては

  • Authyのデスクトップアプリで2段階認証の登録をする
  • Authyを任意のポートを開いた状態で起動する
  • Chromeにて開いたポートに繋ぎに行く
  • Gistに貼られているスクリプトChromeの開発ツールを使って実行する
  • 表示されたQRコードを1Passwordなどで登録する

てな感じになります。

スクリプトではGist内で説明があるように、TOTPで使用する秘密鍵がAuthy側では16進数になっているので32進数に変換したり、QRコードをコンソール上に吐き出すための準備をしたりして最終的にotpauth URI を生成してQRコードを吐いてるみたいですね。
ちなみにMight workとなっているGoogle Authenticatorですが、自分の環境では動きませんでした。なんか設定しないとダメなのかな・・・?🤔

とまぁ久々にこんなハックをしてる人がいるのか!と感動しました。何事も簡単に諦めちゃいかんですね・・・!

GitHub APIでissueを連続作成してたらGraphQL error: was submitted too quicklyで怒られた

所属チームではスクラムを導入していて、スプリントプランニング時に作成されたマイルストーンに対してリリースタスクなど毎回同じタスクissueを作成していました。
これはトイルだなぁとなって、GitHub API v4を使って複数のマイルストーンに数個のissueを作成するスクリプトを書きました。

運用は上手くいっていたように見えたのですが、ある時いつもより多めのマイルストーンを作成してから実行した際にタイトルにもある「GraphQL error: was submitted too quickly」でエラーとなり、issueが作成されないといった事態が起きました。
APIリミットも十二分に残っていたので「アレー?」と思いつつも調査してみると以下のようなissueが👀

github.com

全く同じ状況だなーと思って読んでいると、GitHubの中の人が「表に出ているAPIリミットとは別に、一部のリソース作成に関してはユーザーから不可視な内部リミットが存在する」とコメントしていました。
それじゃどうしたらいいのか?ですが、中の人曰く「エラーを監視しつつ、エラーが出たら数分から1時間待機してから実行し直してね」とのこと・・・うーむ🤔

他のユーザーからはリミット上限を上げてくれ、こういったオプションを作成するといいんじゃないか?(ghの文脈で)など伝えているのですが、未だに解決には至っていないようです・・・かなしい。

突発的な過負荷に耐えるため急造で仕組み作りをしてみた

この記事は MIXI DEVELOPERS Advent Calendar 2022 18 日目の記事です。

普段はSREチックなことをやっているしがないエンジニアなのですが、とある日に「もしかしたら突発的に大量のアクセスが来るかもしれない」というお話が降って湧いたため、こりゃいかんと仕組みを作ったお話をしていきたいと思います。

要件

要件としては以下になります。

  • プッシュ通知などの誘導により、特定のページに短時間に大量のアクセスが来る可能性
  • ページへの誘導がいつ行われるのか事前の連絡は無し
  • なるたけ新鮮な情報が表示されるようにする

ザックリとこんな感じですが、一つずつ検討していきましょう。

その前にサービス構成

と、検討に入る前に扱っているサービスの構成を見ましょう。

  • フロントエンド:Next.js, TypeScript, Apollo Client, GraphQL Code Generator
  • バックエンド:Ruby/Rails, GraphQL Ruby
  • その他:GKE, NGINX

フロントエンドにはNext.jsでバックエンドにはRuby/Rails、その間の疎通はGraphQLで行い、GKEで運用しております。エンドユーザーからのリクエストはフロントエンド・バックエンドともにNGINXを通してPodにやってくるという流れですね。

では検討に戻りましょう。

特定のページに短時間に大量のアクセスが来る可能性

普通の考えだとフロントエンド/バックエンドともにPodを横展開しまくってスケールアウトしようぜ!となるのですが、バックエンドサーバに一つ問題があり、とある理由からスケールしてもあまりRPSが上がらないという状況です。これは困りました・・・

時間があればバックエンドの問題の根治にリソースをかければよいのですが、そこまで時間がないという状況でもありました。幸いにもフロントエンドはPod数に比例して性能が上がることは確認できましたので、ここはフロントエンドでどうにかするように舵を切りましょう。
Next.jsにはSSG(静的サイト生成)の機能があります。これを使って、バックエンドからのレスポンスをビルド時に取得し、サイト表示時には取得したものを使ってバックエンドへの負荷を軽減します。

ここで決まったこと

  • Next.jsのSSGを使う

ページへの誘導がいつ行われるのか事前の連絡は無し

事前連絡無しでのページ誘導については自動スケーリングに頼ってもいいのですが、極短時間で大量にリクエストが来た場合にスケールが間に合わない可能性が高いです。事前にスケールアウトすることも対策として考えられるのですが、事前連絡もないため常にスケールアウトしておくのはちょっと費用的にもNGです。

これについては、SSGを使うという方針が決まりましたのでホスティング先が大量のリクエストを捌けるならOKそうです。今回は読み込みで秒間5000rpsを保証しているGoogle Cloud Storage(以下GCS)を使ってホスティングをすることにしましたので、問題はなさそうですね👌
さらに自動スケーリングも据えているので、万が一これ以上来ても最悪の事態は回避できそうです。

ここで決まったこと

なるたけ新鮮な情報が表示されるようにする

SSGを使った場合、GraphQLからのレスポンスはビルド時に返ってきたものを使うので、ビルド後にDB上のデータが更新されたとしても反映されません。また定期的にビルドをするようにして、ある程度のフレッシュさを保つことはできますが、出来ることならもう少しリアルタイムな情報がほしいところです。
ISRの導入も考慮したのですが、SSGしてGCSでホスティングする場合にはStatic HTML Exportを行う必要があり、ISRはサポートされていないことで断念しました。Static HTML Exportは割と制限が多いので、もし使われる方は一度以下のドキュメントに目を通すと良いかもしれません。

nextjs.org

この辺りで「普段はリアルタイムな情報を返し、過負荷時はビルド時に取得した鮮度に欠ける情報を確実に返す」というアイデアの芽が生えてきました。

ここで決まったこと

  • 過負荷かどうかによって鮮度の違う情報を返す

決まったこと

ここまでで決まったことは以下になります。

  1. Next.jsのSSGを使う
  2. GCSでホスティングする
  3. 過負荷かどうかによって鮮度の違う情報を返す

①と②はNext.jsの領分、③は今回で言うとNGINXの領分になりそうです。

ちなみに、③については過負荷にならなければ新鮮なデータを返し、過負荷であれば鮮度に欠けるデータを返すのですが、こういった負荷制限をかけてそれを超えた場合には安定しているが不確実なデータなどを返すような仕組みをGraceful Degradationと呼んだりします。(SRE本を読んだ時に知りました

※ 今のところ頭にあるザックリイメージ

さて、やることは決まりましたので実際に組んでいきましょう!

Next.jsのSSGを使う

それではNext.jsのSSGを使って組んでいきましょう。まずは仮に作成した土台のページはこちらになります。

なんの変哲もない、小さなページを作りました。 useTestQuery というGraphQLのQueryを呼んで、中身を表示しています。これがバックエンドとやり取りしているところですね。

では、これをSSG対応していきます。

getStaticProps を使うことで、事前にどういったデータを取得しておくのか?を定義することが出来ます。今回は TestQuery のレスポンスを取得しておきたかったので、 getStaticProps 内で取得しています。

ここで、先程のコードで使っていた useTestQuerygetStaticProps の中で何故使っていないのか?ですが、理由はReactのhooks rulesに違反するためです。

reactjs.org

なので、わざわざApollo Clientを生成して取得しているというわけです。
そして、取得したデータを関数コンポーネントの引数として渡せばSSG対応の出来上がりです。簡単ですね!

ここまで出来ましたら一度 next build を実行してみて、キチンと通るかどうかだけ確認しておきましょう👌

●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

↑ が表示されたらOKです!

GCSでホスティングする

次は、GCSでホスティングするところをやっていきましょう。
まずは next export というコマンドを実行して、静的なHTMLとしてエクスポートをしていくのですが、その前にnext.config.js に設定を足して特定ページのみエクスポートされるようにしてみましょうか。これは必ずやらなくても良いのですが、一応こういうことも出来ますよという例として参考にしてみてください。

さて、それでは next export を実行して、ホスティングに必要なファイルをエクスポートしてみましょう。 next export の実行が終わると out ディレクトリが生成されているかと思います。これをGCSに載せてGCS側のホスティングの設定をすればOK・・・というわけもいかず。
out ディレクトリに生成されたファイルを見てみると test.html があると思いますが、これをGCS側でアクセスする際のURLは /test.html になります。しかし出来れば /test といった形でアクセスできるようにしたいですよね?
それを解決するために、trailing slashの設定も追加してみましょう。

これで再度 next export をしてみると、 test ディレクトリが生成されてその中に index.html があることがわかりますね!しかし、まだこれだけではやりたいことは達成できず、GCS側の設定も必要になるのですがそちらは後述とさせていただきます。

Next.js側でホスティングの用意が出来ましたので、GCS側の設定に入っていきましょう。こちらはそう難しくないので、GCPに慣れている方は以下のドキュメントを参考に進めていくと良いかと思います。

cloud.google.com

設定するのは以下の3点です。

  • バケットの作成
  • 一般公開
  • ウェブサイト構成の編集

バケットの作成

何はなくともバケットが無いと始まりませんので、バケットを作成しましょう。以下の画像のように設定し、作成してみてください。

一般公開

次に、一覧から作成したバケットを選択して、権限タブから一般公開できるようにしてみましょう。

そして、プリンシパルallUsers を選択し、Storageレガシーオブジェクト読み取り の権限を付与します。

これで一般ユーザーにもバケットのオブジェクトが読み取りできるようになりました。

ウェブサイト構成の編集

最後に、ウェブサイトの構成編集をしましょう。この設定がtrailing slashの話で後述すると言っていた設定になります。
バケットの一覧画面からケバブメニューをクリック。

そして、ウェブサイトの構成の編集をクリックすると・・・

ここを以下のように設定してください。

これでGCS側の設定は完了しました!と言いたいところですが、最後にドメインを割り当てたり出来るように、Cloud Load Balancingを作成してバケットと繋いでみましょう。こちらもGCSに慣れている方は以下のドキュメントを参考に進めてみてください。

cloud.google.com

設定するのは以下の5点です。

  • Cloud Load Balancingの作成
    • フロントエンドの作成
    • バックエンドの作成

Cloud Load Balancingの作成

Cloud Load Balancingのページへ行き、作成を行いましょう。

ここからロードバランサとそれに紐付くフロントエンド、バックエンドの設定を行います。
まずはロードバランサの名前を決めておきましょう。

フロントエンドの作成

次にフロントエンドの作成ですが、この設定中に別途、静的IPアドレスの予約と証明書の作成を行います。
ひとまず名前の設定と、プロトコルHTTPSにしておきましょう。完了したらIP addressを選択し、作成してください。

証明書も同じく選択すると作成へのリンクがありますので、そちらから作成してください。

ここまで出来ましたら残りはHTTP -> HTTPSのリダイレクトをONにして、完了しましょう。

バックエンドの作成

バックエンドはフロントエンドに比べると簡単で、先程作成したバケットをバックエンドバケットとして設定するだけです。

これでバックエンドの作成も完了。

そして、あとはLoad Balancingも完了して終了となります。別途、必要なDNSのAレコード設定などはよしなに済ませておいてください。

ここまで出来ましたら、あとはバケットout ディレクトリの中身を置くだけです!置いた後に、 /test でアクセスできるかどうか確認してみてください。
注意点として、証明書のプロビジョニングに時間がかかったりして、最終的にロードバランサが使えるようになるまで1時間ほどかかりますので、そこだけご注意ください。

過負荷かどうかによって鮮度の違う情報を返す

今回の最大の肝の部分にやってきました。
改めてやりたいことを振り返ってみますと

  • 過負荷ではない時にはGKEからサイトを表示
  • 過負荷の時にはGCSでホスティングしたサイトを表示

でした。それではまずは過負荷かどうかのチェックからやってみましょう。

過負荷かどうかのチェック

最初に述べたように、ここからはNGINXの領分ですので nginx.conf を編集していきます。NGINXにはリクエスト数によって処理をハンドリングする ngx_http_limit_req_module という便利なモジュールがありますので、そちらを利用して過負荷かどうかをチェックします。

mogile.web.fc2.com

まずは、どのくらいのリクエスト数を過負荷と判定するのか?を設定します。

今回は「ユーザー全体からのリクエスト数が20rpsを超えたら過負荷とする」という設定にしています。ちなみに limit_req_zonekey_all を指定するとユーザー全体のリクエスト数が対象となりますが、 $binary_remote_addr を指定するとIPアドレス毎のリクエスト数になりますので1つのIPから大量にリクエストが来た場合の対処などに使えますね。
また、 zone=z_all:10m はゾーン名の指定とゾーンの共有メモリ割当数を指定しています。共有メモリについてはワーカープロセスと共有しているメモリ領域で、limit_req_zoneでは各IPアドレスごとの状態と、limit_req_zoneが適用されたパスへのリクエスト回数が保持されます。大体、16000件分のIPアドレスで1メガバイトを消費するので10mの指定だと160000件分のIPアドレスが保持できます。

ちなみに今回のようにNGINXを扱う場合や、NGINX ingress Controllerを扱って流量制御する場合にはインスタンス毎のリクエスト数による制御となります。つまり、今回のように20rpsで設定した場合、インスタンスが3つあると全体で20 * 3 = 60rpsのリクエストを捌くことになるという計算になりますのでご注意ください。

medium.com

さて、 limit_req_zone を指定しただけではまだ過負荷判定が出来ているとは言えません。次はどこのパスに対してこの設定を適用するのか?を設定します。

これで /test に対して過負荷かどうかの判定ができるように設定しました。
ですが、このままでは超過したリクエストが503になるだけです。ここから次は「過負荷の時にはGCSでホスティングしたサイトを表示」をやっていきましょう。

過負荷の時にはGCSでホスティングしたサイトを表示

ここでは少しトリッキーなことを設定し、GCSへリクエストを遷移させます。まず、 limit_req モジュールには limit_req_status というディレクティブがあり、これをlocation内で指定すると超過したリクエストのHTTPステータスコードを503から変更することが出来ます。これを使って超過した時に503から555を返すようにしてみましょう。

これだけでは何のこっちゃい?といった感じですが、併せて error_page ディレクティブを使っていきます。これは指定したHTTPステータスコードに紐付くエラーページを設定するもので、よくあるのがカスタマイズした404や503ページを表示するのに使うと思います。
これを使ってHTTPステータスコードが555の時に○○を表示する、といったことをやってみましょう。

こんな風になりました。さてここまで来たら「〇〇を表示する」のところを「GCSでホスティングしたサイトを表示する」に変更したら実現できそうですね。
ここで必要となってくる新しい要素に named location というものがあります。 named location とは通常のリクエストでは使われず、内部リダイレクトのみで使われるlocationです。内部リダイレクトとはNGINX内で完結するリダイレクト処理で、グローバルネットワークに出ることなくリダイレクト処理を行うことが出来ます。

この named location を使ってこんな風に書いてみましょう。

このような形で、過負荷判定だった場合にHTTPステータスコード 555を返し、 error_page では named location を使って内部リダイレクトを行い、 named location では proxy_pass をGCSのドメインにすることでGCSへの表示に切り替えることが出来ます。もう一つ追加しているのが、 server_name で今回はGKEとGCSの2つのドメインが入り混じるので追加しておきました。
ちなみにこの手法の利点として、ドメインのGCSへ飛ばしているのにURLは変わらないことにあります。これは proxy_pass を使っているおかげではあるのですが、詳しい説明はドキュメントを参照してください。

nginx.org

この状態で一度負荷ツールを使って、負荷をかけつつ実際にGCSにリクエストが飛ぶかどうか確認してみてください。そのままだとGCSに飛んだかが分からないので、GCSにある test/index.html を少し改造して、 <h1>Test Page in GCS</h1> とすると分かりやすいかもしれません。

ここまででかなり形にはなりました。しかし、先程負荷をかけて確認した方は気付いたと思いますが、 /test にアクセスしたら /test/index.html にリダイレクトされたかと思います。これはGCSの仕様で、 /test の場合には /test/index.html へリダイレクト、 /test/ の場合には /test/index.html の内容が表示されるといった挙動になっているためです。
これでは少し見た目的にまずいので、 named location の中身を少し改造して /test と来たら /test/ とリライトしてしまいましょう。

さて、これで過負荷の時にはGCSでホスティングしたサイトを表示するという要件は満たせましたので、次は過負荷ではない時にはGKEからサイトを表示にトライしましょう。

過負荷ではない時にはGKEからサイトを表示

本記事も長くなりましたが、最後に過負荷ではない時にGKEからサイト表示するようにしましょう。こちらはNGINXだけでなく、Next.jsにも手を入れていきます。
またまた振り返ってみるとGKEからサイトを表示をやりたいモチベーションは「なるたけ新鮮な情報が表示されるようにする」でした。では、現在はどうなっているかというと、 getStaticProps を指定しているためビルド時に取得したデータを返すようになっています。これではいけません。

アレコレと頭を悩ました結果、「NGINXで何かしらのリクエストヘッダーを付与し、Next.js側のMiddlewareでリクエストヘッダーがあればCookiesを発行し、CookiesがあればGraphQLへリクエストする。無ければビルド時のデータを返す」という妙案が浮かびました。
まず、Next.jsでGraphQLへリクエストする/しないを判定するのに「NGINXを通っているかどうか」の情報が必要であり、それにはNGINXで何かしらリクエストヘッダーを付けてあげればいいと考えました。そして、Next.jsでリクエストヘッダーを受け取るにはMiddlewareが必要なので、Middlewareで受け取った後にクライアント側で判断できる"何か"を渡さないといけない。それを今回はCookiesでやってみようと思い至った次第です。

なので、やることとしては以下の3点になります。

  • NGINXでリクエストヘッダーを付与する
  • Next.jsでMiddlewareを作成する
  • Cookiesの有無でGraphQLへのリクエストするかどうかを決める

それでは最後のパートをやっていきましょう。

NGINXでリクエストヘッダーを付与する

これは非常に簡単で、 /test のlocationでリクエストヘッダーを付けるだけです。

しれっと追加しましたが、通常のappへの proxy_pass を書くの忘れていたのでここで追加しておきました。(すいません)
で、その proxy_pass 先へ proxy_set_header でリクエストヘッダーを設定しています。

Next.jsでMiddlewareを作成する

次にリクエストヘッダーをMiddlewareで受け止めてみましょう。Next.jsの src ディレクトリの下に middleware.ts を書きます。

Next.jsのmiddlewareはリクエストとレスポンスを参照し、編集することができます。まずリクエストの NextRequest から渡ってきたヘッダーの X-Pass-Nginx-Request を参照して、もし存在すればGraphQLのリクエストを行うためのCookiesを設定しなければいけません。そのため、今度はレスポンスの NextResponce に対して RequestGraphql を設定しています。
maxAge をoptionとして付けているのは、消えるようにしておかないと過負荷時にGCSを見に行ったとしてもGraphQLへリクエストしてしまうので、それを防ぐためにやっています。

Cookiesの有無でGraphQLへのリクエストするかどうかを決める

最後に先程発行したCookiesを参照して、GraphQLへリクエストするかどうか決める処理を書きましょう。

やっていることとしては、「RequestGraphql Cookiesを取得」して、「存在すればuseTestQueryでGraphQLリクエストを行う」「無ければprops.dataを利用する」といった感じになります。これでGCSではビルド時の情報を返し、GKEでは表示時にリクエストして返ってきた情報を返すといったことが出来ました。

一旦完結

ここまで作ってきたもので一旦は完となります。試しに負荷ツールで負荷をかけつつ、チームメンバー総出で手動で負荷をかけてもらいましたが、難なく捌けて表示にも問題ないことが確認できました👌

とりあえず無い知恵を絞って仕組み作りをしてみましたが、如何だったでしょうか?もしかしたら穴があったり、もっと良い方法があったりするかもしれないのでそういったご意見ははてブの方で寄せていただけますと幸いです🙏

ちなみにこの話のオチですが、紆余曲折があり結局この仕組みは世に出ることはありませんでした。残念。
ただし、こういった云々かんぬん考えた間に得た知見は腐ることがないので、いい経験になりました。

さて一旦はここで完結ですが、最後にこの仕組みのCI対応のお話と、Next.jsで若干ハマった pages 以外でのSSG対応のお話を供養として載せておきますので、まだ記事を読む気力のある方は引き続き読んでいただけると幸いです🙇

CI対応

読んでいただける方、ありがとうございます。ここからは本題からは少し逸れたボーナストラックになります。
今回はNext.js側で色々と変更点を加えたことで、既存のCIの修正が必要となりました。どういった修正が必要だったかのお話をしていきます。ちなみにCircleCIを使っているので、コードはそちら準拠となります。

修正を加える点としては以下の2点でした。

  • out ディレクトリの中身をGCSへアップロード
  • trailingSlash を切り替える

CirlceCIの設定ファイル例を見る前に、 next.config.js を少し変えておきます。

trailingSlash をtrue -> falseにしておきました。
で、CircleCIの設定ファイル例はこちら。

out ディレクトリの中身をGCSへアップロードするのは簡単ですね。単純にGCSへアップロードするだけなので、権限を与えたサービスアカウントのキーを使ってAuthし、 gsutil を使ってアップロードするだけです。(最近は gcloud storage がGAになったのでそちらを使うでもOK)

trailingSlash を切り替える処理は sed を使って、 trailingSlash を false -> true にしてから next export して、また戻しています。何故やっているのかというとGCSでホスティングする際には trailingSlash を有効化して、GKE側では trailingSlash を無効化にしておかないとGKE側で /test のようにアクセスできなくなってしまいます。個人的にこれはあまりよろしくなかったので、こういう処理をしています。

pages 以外でのSSG対応

これ結構悩んだのですが、実際に仕組み作りをしている中で pages ディレクトリ以外に存在するReactカスタムフックの中でGraphQLへリクエストしている箇所があり、これも「GCSではビルド時のレスポンスを返し、GKEではGraphQLへリクエストして取得する」という出し分けをしなければいけません。
しかし、getStaticPropspages 以下にあるファイルでしか用いることが出来ないため、また違った方法を探さなければいけません。

Googleを探し歩いた結果、「プリビルドで取得したレスポンスをJSONファイルとして保存し、GCSでそれを返す」といった方法を発見しました。割とやっている人が多かったので、これくらいしか解決方法がないのでしょう。(原始的な方法ですが・・・)
気を取り直してやっていきましょう。

まずはプリビルドで取得するところからです。プリビルドは npm scriptprebuild を用い、レスポンスを取得してJSONとして保存するスクリプトts-node で動かします。

こんな感じでApollo Clientを通して取得したレスポンスを data ディレクトリにJSONファイルとして出力しています。
このJSONファイルをRequestGraphQLのCookiesがなかった場合に以下のようにして返してください。

testData = await import("data/test.json");

pages 以外でのSSG対応はこのような流れになります。これについてはもっと良い方法はないものかと思ってはいるのですが・・・うーむ。

本当に完

これにて本当に完結です。ここまで読んでいただけた方は改めてありがとうございます。
やっている中でもっと何か小ネタがあった気がするのですが、とりあえず思い付くだけ書いてみました。誰かの参考になれば幸いです🙏

k6を使いこなしてみよう

この記事は MIXI DEVELOPERS Advent Calendar 2022 6 日目の記事です。

負荷試験を行う機会が年に何度かあるのですが、以前まではvegetaを使っていましたがちょっと高めの負荷をかけた時の挙動がよろしくなく、k6を試してみたところ不満が無かったので最近はk6を常用しています。

そんなk6をもうちょっと使いこなすために色々とまとめてみようかと思います。

k6とは?

Grafana Labsが開発した負荷ツール。

github.com

ツール自体はGo製で、負荷シナリオをJavaScriptで書きます。
負荷シナリオはk6 Browser RecorderというChrome拡張を使えばブラウジングしているだけで作成可能で、k6 Cloudを使ったWeb上でのシナリオ作成・管理・実行が可能です。
わざわざGitHub上でシナリオを管理しなくてもいいというのは個人的に便利だなと思った点ではあります👀 (どこのリポジトリに置くの?問題とか考えなくて済む)

chrome.google.com

k6.io

それ以外にもHTTP、WebSocket、gRPCなどのマルチプロトコルサポートや、カスタムビルドを使うことにより例えばIAPの突破をk6自体に組み込んだりなどの拡張性を持たせることが出来ます。(詳しくはxk6を参照)

github.com

その他、充実したドキュメントやマメに更新されているブログなどユーザーに対してのサポートも抜かりがないです。

k6.io

基本的な使い方

まずはインストールから。Macの場合は brew install k6 でok、それ以外はこちらを参照。

インストールが終わりましたら、次にシナリオの作成です。ドキュメントにもある最小の形でまずは試してみましょう。
シナリオを格納するディレクトリとして k6 という名称でディレクトリを作成し、その直下に以下の内容の test.js を作成してください。

k6側でテスト用に用意されている https://test.k6.io に対してGetリクエストを投げ、その1秒後にテストを終了するシナリオになります。では、そのようになっているのか実行してみましょう。

$ cd k6
$ k6 run test.js

          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: test.js
     output: -

  scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
           * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)


running (00m01.7s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs  00m01.7s/10m0s  1/1 iters, 1 per VU

     data_received..................: 17 kB 10 kB/s
     data_sent......................: 442 B 260 B/s
     http_req_blocked...............: avg=518.34ms min=518.34ms med=518.34ms max=518.34ms p(90)=518.34ms p(95)=518.34ms
     http_req_connecting............: avg=175.25ms min=175.25ms med=175.25ms max=175.25ms p(90)=175.25ms p(95)=175.25ms
     http_req_duration..............: avg=179.72ms min=179.72ms med=179.72ms max=179.72ms p(90)=179.72ms p(95)=179.72ms
       { expected_response:true }...: avg=179.72ms min=179.72ms med=179.72ms max=179.72ms p(90)=179.72ms p(95)=179.72ms
     http_req_failed................: 0.00% ✓ 0        ✗ 1
     http_req_receiving.............: avg=465µs    min=465µs    med=465µs    max=465µs    p(90)=465µs    p(95)=465µs
     http_req_sending...............: avg=50µs     min=50µs     med=50µs     max=50µs     p(90)=50µs     p(95)=50µs
     http_req_tls_handshaking.......: avg=330.41ms min=330.41ms med=330.41ms max=330.41ms p(90)=330.41ms p(95)=330.41ms
     http_req_waiting...............: avg=179.21ms min=179.21ms med=179.21ms max=179.21ms p(90)=179.21ms p(95)=179.21ms
     http_reqs......................: 1     0.587392/s
     iteration_duration.............: avg=1.7s     min=1.7s     med=1.7s     max=1.7s     p(90)=1.7s     p(95)=1.7s
     iterations.....................: 1     0.587392/s
     vus............................: 1     min=1      max=1
     vus_max........................: 1     min=1      max=1

予想通りにGetリクエストを1度投げてますね!このようにCLIスクリプトを実行すると統計データが表示されます。
では表示された項目について一つずつ見ていきましょう。

running (00m01.7s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs  00m01.7s/10m0s  1/1 iters, 1 per VU

テスト実行時のプログレスバーですが、ここにはVUS(virtual users:仮想ユーザー)の数と実行時間、イテレート数と仮想ユーザーの実行数になります。

  • data_received:総データ受信数
  • data_sent:総データ送信数
  • http_req_blocked:TCP接続がブロッキングされた時間
  • http_req_connecting:TCP接続の確立にかかった時間
  • http_req_duration:リクエストを送信してレスポンスを受け取るまでの時間(http_req_sending + http_req_waiting + http_req_receiving)
  • expected_response:http_req_durationの中でもHTTPステータスコードが200~399のリクエストのみの値
  • http_req_failed:リクエストが失敗した数
  • http_req_receiving:レスポンスを受信していた時間
  • http_req_sending:リクエストを送信していた時間
  • http_req_tls_handshaking:TLSハンドシェイクにかかった時間
  • http_req_waiting:レスポンスを待っていた時間(TTFBのこと)
  • http_reqs:送信されたリクエスト数
  • iteration_duration:シナリオで設定された1イテレーションにかかった全ての時間
  • iterations:1VUが実行したイテレーションの数
  • vus:終了時点でのVU数
  • vus_max:実行中での最大VU数

色々ありますが、個人的に見ている指標は http_req_failedhttp_req_duration くらいですかね。勿論、他の指標も何かしらの異常な値が出ていれば見ますが、基本はこの2つを見ていればいいかなと思います。
シナリオをCLIで実行した場合はこのような出力ですが、k6 Cloudではこの指標がグラフで可視化された図を確認することができます。

※ただし、詳細に出力されるのはCLIの方なので特別なこだわりが無ければCLIで実行するのをオススメします(高負荷の場合はk6 Cloudの方がいいかも)

また、この指標は開発者がカスタムすることが出来、これは後ほど説明致します。

ここまでが最小の形でのシナリオ作成→実行までのフローになります。

k6 run

さきほど実行の際に利用した k6 run についてもう少し詳しく見ていきます。

k6 run はオプションでシナリオの実行の仕方を変えることができます。例えば、さきほどはシナリオが一回終われば実行を終了していましたが、「30sの間、50rpsで負荷をかけたい」などといった要望は出てくると思います。そういった時にオプションを付けてみましょう。
今回はテストのためにもう少し軽い負荷にしてみます。

$ k6 run test.js -d 5s --rps 20
          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: test.js
     output: -

  scenarios: (100.00%) 1 scenario, 1 max VUs, 35s max duration (incl. graceful stop):
           * default: 1 looping VUs for 5s (gracefulStop: 30s)


running (05.2s), 0/1 VUs, 4 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs  5s

     data_received..................: 52 kB 9.9 kB/s
     data_sent......................: 736 B 141 B/s
     http_req_blocked...............: avg=120.06ms min=6µs      med=11.5µs   max=480.22ms p(90)=336.16ms p(95)=408.19ms
     http_req_connecting............: avg=44.02ms  min=0s       med=0s       max=176.08ms p(90)=123.26ms p(95)=149.67ms
     http_req_duration..............: avg=182.34ms min=179.94ms med=182.49ms max=184.42ms p(90)=184.05ms p(95)=184.24ms
       { expected_response:true }...: avg=182.34ms min=179.94ms med=182.49ms max=184.42ms p(90)=184.05ms p(95)=184.24ms
     http_req_failed................: 0.00% ✓ 0        ✗ 4
     http_req_receiving.............: avg=143.75µs min=86µs     med=137.49µs max=214µs    p(90)=201.7µs  p(95)=207.84µs
     http_req_sending...............: avg=36.25µs  min=26µs     med=30µs     max=59µs     p(90)=50.6µs   p(95)=54.8µs
     http_req_tls_handshaking.......: avg=75.71ms  min=0s       med=0s       max=302.86ms p(90)=212ms    p(95)=257.43ms
     http_req_waiting...............: avg=182.16ms min=179.81ms med=182.26ms max=184.31ms p(90)=183.9ms  p(95)=184.11ms
     http_reqs......................: 4     0.766934/s
     iteration_duration.............: avg=1.3s     min=1.18s    med=1.18s    max=1.66s    p(90)=1.51s    p(95)=1.59s
     iterations.....................: 4     0.766934/s
     vus............................: 1     min=1      max=1
     vus_max........................: 1     min=1      max=1

実行してみましたが、20rpsを指定したのに0.7rpsしか出ていませんね。vusに注目していただくとvusが1しか生成されていないので、20rpsを実行するためにvusを増やしてみましょう。

$ k6 run test.js -d 5s --rps 20 -u 40
          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: test.js
     output: -

  scenarios: (100.00%) 1 scenario, 60 max VUs, 35s max duration (incl. graceful stop):
           * default: 60 looping VUs for 5s (gracefulStop: 30s)


running (08.0s), 00/60 VUs, 135 complete and 0 interrupted iterations
default ✓ [======================================] 60 VUs  5s

     data_received..................: 1.9 MB 237 kB/s
     data_sent......................: 34 kB  4.3 kB/s
     http_req_blocked...............: avg=158.32ms min=3µs      med=13µs     max=465.14ms p(90)=358.31ms p(95)=364.97ms
     http_req_connecting............: avg=77.78ms  min=0s       med=0s       max=183.75ms p(90)=177.89ms p(95)=180.75ms
     http_req_duration..............: avg=193.77ms min=171.88ms med=179.31ms max=362.84ms p(90)=188.16ms p(95)=346.3ms
       { expected_response:true }...: avg=193.77ms min=171.88ms med=179.31ms max=362.84ms p(90)=188.16ms p(95)=346.3ms
     http_req_failed................: 0.00%  ✓ 0        ✗ 135
     http_req_receiving.............: avg=15.43ms  min=40µs     med=122µs    max=178.5ms  p(90)=418.8µs  p(95)=170.85ms
     http_req_sending...............: avg=47.08µs  min=12µs     med=44µs     max=122µs    p(90)=83.2µs   p(95)=95.3µs
     http_req_tls_handshaking.......: avg=80.48ms  min=0s       med=0s       max=280.26ms p(90)=180.7ms  p(95)=184.18ms
     http_req_waiting...............: avg=178.29ms min=171.72ms med=178.65ms max=188.4ms  p(90)=184.01ms p(95)=185.03ms
     http_reqs......................: 135    16.97481/s
     iteration_duration.............: avg=2.85s    min=1.63s    med=2.65s    max=4.5s     p(90)=3.81s    p(95)=4.15s
     iterations.....................: 135    16.97481/s
     vus............................: 18     min=18     max=60
     vus_max........................: 60     min=60     max=60

vusを40にしてみたところ、20rpsに近付きましたね!このように実行する環境をオプションによって調整することが出来ます。様々なオプションが用意されているので是非とも k6 run --help でオプション一覧を眺めて頂きたいのですが、その中から使い所の多いオプションを抜粋します。

// --user-agent:ユーザーエージェントの指定
$ k6 run test.js --user-agent "test-agent"

// --out:指標の詳細データを出力、リクエスト毎にHTTPステータスコードやリクエスト/レスポンスの日時などの情報が確認できる
$ k6 run test.js --out json=result.json

// --log-output:画面上でのログの表示設定、リクエストのタイムアウトなど頻発する出力を抑制するのに使う
$ k6 --log-output none run test.js

k6 Browser Recorderを利用したシナリオの作成

k6とは?で紹介したk6Chrome拡張・をk6 Browser Recorder使ってシナリオを作成してみましょう。今回はexample.comを使ってシナリオを作成してみます。
事前作業として k6 Browser Recorderをインストールし、https://app.k6.io/account/login にアクセスし、ログインまたは新規登録を済ましておいてください。

まずは、example.comに飛びましょう。

k6 Browser Recorderを実行してみます。

Start recordingを押下します。

ツールウィンドウが読み込み状態になり、暫くすると上記の画面になりますので、そうしましたらページを一度リロードしてみてください。 その後、完全にページの読み込みが完了したらStopを押下します。

すると、新規タブが開きk6 Cloudが表示されます。

Test builderとScript editorが最初にあるのですが、個人的にはScript editorを選択するのをオススメしています。後から変えられるのでどちらでもいいのですが、開発者が扱うにはScript editorの方が馴染みあるJSの書かれたエディターが開かれるのでこちらを選ぶのが良いかと。
Generate sleepは500ms以上かかっている処理間に自動でsleepを入れてくれるのですが、自分は必要ないと感じているのでいつも選択していません。

また、今回は表示がされていませんがcssやjsなどのstatic assetsが存在する場合は、そのアクセス処理もスクリプトに含めるかどうか、外部ドメインへのリクエストも含めるかどうかも選択できます。外部ドメインへのリクエストは自分で管理しているもの以外は除くのが良さそうで、static assetsに関しては基本は含んでおいて状況によっては除いたりするのが良いかなと思います。

選択が終わりましたらSaveでScript editorに移動します。

ここからRun Testを押下することでそのまま実行できますし、一度生成されたスクリプトを手元でjsファイルにしてCLIで実行するも良し。状況によって使い分けてみてください。

シナリオの作成例

ここからはもう少しシナリオの作成を具体的にやっていきます。さきほど作成したexapmle.comへのリクエストを土台に改造していきます。

生成されたこのコードを見たときに注目すべきは export const options です。 k6 run のオプションを紹介しましたが、さらに細かい調整等を可能にするのがこのoptionsです。
現在は「vusが10で5分間負荷をかける」という条件でシナリオが書かれていますが、これを「40vus(必要があれば増やす)で20rpsの負荷を5秒間かける」としてみましょう。

確認のため、こちらを実行してみると

$ k6 run test.js

          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: test.js
     output: -

  scenarios: (100.00%) 1 scenario, 100 max VUs, 35s max duration (incl. graceful stop):
           * test_scenario: 20.00 iterations/s for 5s (maxVUs: 40-100, gracefulStop: 30s)


running (06.1s), 000/040 VUs, 101 complete and 0 interrupted iterations
test_scenario ✓ [======================================] 000/040 VUs  5s  20 iters/s

     █ page_1 - http://example.com/

     data_received..................: 162 kB 26 kB/s
     data_sent......................: 11 kB  1.8 kB/s
     group_duration.................: avg=159.18ms min=108.62ms med=117.98ms max=247.32ms p(90)=231.3ms  p(95)=238.23ms
     http_req_blocked...............: avg=44.95ms  min=4µs      med=15µs     max=121.48ms p(90)=114.97ms p(95)=117.98ms
     http_req_connecting............: avg=44.88ms  min=0s       med=0s       max=121.35ms p(90)=114.84ms p(95)=117.84ms
     http_req_duration..............: avg=113.97ms min=108.32ms med=114.34ms max=129.69ms p(90)=120.07ms p(95)=120.83ms
       { expected_response:true }...: avg=113.97ms min=108.32ms med=114.34ms max=129.69ms p(90)=120.07ms p(95)=120.83ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 101
     http_req_receiving.............: avg=124.53µs min=44µs     med=125µs    max=254µs    p(90)=182µs    p(95)=186µs
     http_req_sending...............: avg=75.37µs  min=24µs     med=57µs     max=426µs    p(90)=138µs    p(95)=168µs
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=113.77ms min=108.13ms med=114.13ms max=129.51ms p(90)=119.93ms p(95)=120.63ms
     http_reqs......................: 101    16.524309/s
     iteration_duration.............: avg=1.16s    min=1.1s     med=1.11s    max=1.24s    p(90)=1.23s    p(95)=1.23s
     iterations.....................: 101    16.524309/s
     vus............................: 40     min=40      max=40
     vus_max........................: 40     min=40      max=40

16.5rpsということで20rpsに近い値が出せていますね!それでは土台からの変更点を見てみます。

export const options = {
  scenarios: {
    test_scenario: {

さて、optionsの中で scenarios というのが設定されています。これはVUSやイテレーションの具体的な設定を行う際に使われるもので、詳しくはこちらに書いてありますが、エグゼキューターと呼ばれる実行エンジンの指定や、実行パターンの詳細な設定でより現実的な負荷計測が可能になります。
optionsに rps という設定項目もあるのですが、非推奨となっているため constant-arrival-rate エグゼキューターを使った一定のリクエスト送出を行う方法を取っています。
test_scenario は任意のキーとなっていますので、適当なシナリオに基づく名前を付けてください。

// rate, timeUnitを実行するためのExecutor
executor: 'constant-arrival-rate',

duration: '5s',

// timeUnitで指定された時間毎に反復するテスト回数
rate: 20,

// rateを反復させる時間
timeUnit: '1s',

// 初期に割り当てられるVUS数
preAllocatedVUs: 40,

// VUSが足りなかった場合に増える最大VUS数
maxVUs: 100,

scenarios で設定されている内容ですが、コメントに書いてある通りの設定をしている感じです。設定値はoptionsでも設定できる値は設定でき、エグゼキューター毎に設定できるものが違ったりするので、まずはエグゼキューターの把握。それから各エグゼキューターの設定値を確認していくのがいいでしょう。

あと、私が必ず付けているオプションがありまして discardResponseBodies: true を付けておくのをオススメします。こちらはレスポンスボディを破棄するかどうかの設定で、デフォルトでは破棄しない設定になっているのですが必要がないことがほとんどだと思いますので、テスト負荷を減らすために破棄をする設定にしておきましょう。

もう一つ、こちらはオプションではないのですが http.setResponseCallback(http.expectedStatuses({ min: 200, max: 399 })); という「何のHTTPステータスコードを成功とみなすのか?」を設定することができます。デフォルトで200~399までは成功扱いなのですが、場合によっては特定のHTTPステータスコード以外は成功としたくないという時に使えます。
試しに201~399までは成功とした場合にどうなるかやってみましょう。

          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: test.js
     output: -

  scenarios: (100.00%) 1 scenario, 100 max VUs, 35s max duration (incl. graceful stop):
           * test_scenario: 20.00 iterations/s for 5s (maxVUs: 40-100, gracefulStop: 30s)


running (06.1s), 000/040 VUs, 101 complete and 0 interrupted iterations
test_scenario ✓ [======================================] 000/040 VUs  5s  20 iters/s

     █ page_1 - http://example.com/

     data_received..............: 162 kB  26 kB/s
     data_sent..................: 11 kB   1.8 kB/s
     group_duration.............: avg=160.16ms min=107.11ms med=118.32ms max=253.38ms p(90)=235.91ms p(95)=239.88ms
     http_req_blocked...........: avg=45.4ms   min=3µs      med=15µs     max=132.49ms p(90)=117.31ms p(95)=118.75ms
     http_req_connecting........: avg=45.25ms  min=0s       med=0s       max=121.77ms p(90)=117.18ms p(95)=118.65ms
     http_req_duration..........: avg=114.49ms min=106.91ms med=115.03ms max=123.16ms p(90)=119.73ms p(95)=120.56ms
     http_req_failed............: 100.00% ✓ 101       ✗ 0
     http_req_receiving.........: avg=117.94µs min=19µs     med=93µs     max=2.62ms   p(90)=135µs    p(95)=152µs
     http_req_sending...........: avg=73.29µs  min=17µs     med=56µs     max=281µs    p(90)=147µs    p(95)=167µs
     http_req_tls_handshaking...: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s
     http_req_waiting...........: avg=114.3ms  min=106.79ms med=114.85ms max=122.99ms p(90)=119.6ms  p(95)=120.32ms
     http_reqs..................: 101     16.504622/s
     iteration_duration.........: avg=1.16s    min=1.1s     med=1.11s    max=1.25s    p(90)=1.23s    p(95)=1.24s
     iterations.................: 101     16.504622/s
     vus........................: 40      min=40      max=40
     vus_max....................: 40      min=40      max=40

想定通り、 http_req_failed が100%で全て失敗となっていますね。

カスタムメトリクスの作成

負荷の実行後に表示される指標をカスタマイズしてみましょう。設定できるカスタムメトリクスには以下の4種類があります。

  • Counter:累積値を判定する指標。例えば、HTTPステータスコード上は成功しているが、エラーコードをレスポンスで返している場合にそういったレスポンスが1件でもあれば失敗とする、みたいなことが出来る。
  • Gauge:最新の値を常に判定する指標。例えば、レスポンスボディのサイズが〇〇以上だったら失敗、のようなことが出来る。
  • Rate:追加した値の割合を判定する指標。例えば、エラー率が10%以上なら失敗のようなことが出来る。
  • Trend:追加した値の統計(最小、最大、平均、パーセンタイル)を判定する指標。例えば、平均レスポンスタイムが〇〇ms以下なら失敗のようなことが出来る。

それぞれのカスタムメトリクスに対して、 thresholds という条件を記述することで機能するのですが、ここで abortOnFail を設定すれば失敗した時点で負荷計測を中断することが出来たり、 delayAbortEval で失敗後も指定時間計測は続けるといったことも可能です。

この4つのカスタムメトリクスを加えたシナリオを書いてみましたので、参考にしてみてください。

まだまだ奥が深いk6

今回紹介したのはk6の要素のまだまだ一部です。他にも setup teardown や、 check を使ったシナリオの書き方などもありますので、ドキュメントを読んで習熟してみてください!

k6.io

Googleのソフトウェアエンジニアリングを読んだ

www.amazon.co.jp

を読みました。ソフトウェアエンジニアリングを広範に取り扱っためちゃくちゃ良い本だったので、下の連ツイを基にちまちま感想などを書いていく。
※ 684ページという大ボリューム&プライベートで様々なイベントがあったので読むのに3ヶ月ほどかかりました・・・

どんな本だったか?

さっくり言うと「ソフトウェアリングという広範的な物事における考え方・やり方をしっかり学べる本」になります。
全体構成としては5部に分かれており、

  1. 主題
  2. 文化
  3. プロセス
  4. ツール
  5. 結論

となっています。

主題では「ソフトウェアエンジニアリングの定義・著者が発見した法則・時間的変遷による事象」などを絡めつつ、この本の前哨戦として何を学んでいかなくてはいかないのか?を説明されています。
文化は「メンバーとして、またリーダーとしてどういった文化を築かなければならないか」、プロセス・ツールでより具体的なスタイルガイド・コードレビュー・ドキュメンテーション・テストなどなどの話。
そして結論、という流れでソフトウェアエンジニアについて過不足なく学べる内容だったと思います。

個人的にどこが良かったか?

"Googleの"と銘打っていますが、「Googleではこんなクールなやり方をしているぜ!」といった本ではなく、最初期のGoogleが我々と同じような苦しみを抱え、時には失敗し、その結果どういったやり方に帰結したか?という流れをキチンと説明されているところがとても印象が良かったです。
3部 14.1.2.2 設定の問題にて、「Googleでは設定変更が大障害の原因の第一位である。」とあるように、誰もが経験することを勿論Googleも経験しているしそれが重大インシデントに繋がりこういった事故を起こした、といったことが赤裸々に書かれています。
Googleも人並みなミスを今までしてきた。ただそのミスをどういった考え方・やり方で根絶しているのかという流れは、自分自身に翻ってみても活かせることでしょう。

響いたところ

ここからは本書内で響いてメモったところを書いていきます。全部が全部を書いているわけではないので、気になった方は買って読んでみてください。

独自の法則

Hyrumの法則

APIに対してどういう契約仕様で作られたかをユーザーは考慮せず、ただ振る舞いに依存する。
つまり、設計者の意図を考慮せずにユーザーは利用する、ということであり"仕様"と"振る舞い"の乖離が大きくなればなるほど設計者とユーザーの距離が遠くなっていく。

このことによるデメリットは時間的経過によってより甚大なものになるのは想像に難くないでしょう。本書ではこの法則が様々な観点からちょこちょこと登場してきます。

Beyonceルール

「そんなに好きなら(≒ 破綻して欲しくないなら)そいつにテスト付けときゃよかったのに」
ミッションクリティカルであるなど、破綻が許せないなら必ずテストしましょうというルール。特にインフラストラクチャの文脈で語られるルールです。
インフラストラクチャの変更で何らかの障害が起きるといった可能性はありますが、それをCIで防げないのであればCIの落ち度であり変更には全く責任が無いとも言い換えることができるでしょう。

リーダーとは斯くあるべきか

チームリーダーについて語られている章は、個人的には本書内で熟読する価値が非常に高いと考えている一つで、「何をしなくちゃいけなくて、何をしてはいけないのか?」が書かれています。

リーダーが負う責務

「リーダーはプロジェクトの要所要所においてトレードオフを明確化させ、それをどのように行うべきかの決定における責務を負わなければならない」。これには非常に納得でした。
リーダーが決定に対して責務が負えないのであれば、チームとしての迷いが常に漂ったまま仕事をし続けなければいけなくなり非常に不健全です。メンバーに意見を乞うことは積極的にしなくてはいけませんが、決定は必ずリーダーがやるべきものです。

逆に「リーダーは完璧超人であるべき責務は負わなくていい。またその振る舞いをしてはいけない」、とはっきり書いてあります。それよりも、合意形成をよりスムーズに行うための"触媒"であったり、適切な答えを知っている人を知っている、といったサポート役としての振る舞いが重要視されます。

去れるリーダーになろう

あと面白いなーと思った考え方は第2部 6.2に書かれている「いつでも去れ」です。私も常々思っていたことですが、「自分が明日事故に遭い、働けなくなった場合にこのチームは何も支障なく回り続けられるだろうか?出来ないなら何が必要だろうか?」という考え方です。本書ではそういった考え方を"バス係数"と呼んでいます。
「プロジェクトが破綻するのに要するプロジェクト内でバスに轢かれる者の人数」がバス係数で、自分やメンバーがバス係数としてカウントされないような自律的組織を目指しましょうといった流れで解説されています。自分がいなくなっても回るように整備し、自動運転できることを見届けたら去り、また別のチームを整備していく・・・目指すべきリーダー像のひとつですね。

ブログの下書きに入れっぱなしになってたので、一旦公開。
また読み返した時に追記します。