おそらくはそれさえも平凡な日々

Gitのワークフローについての私のスタンス

Gitのワークフロー、好みが分かれる分野で自転車置き場の議論にもなりがちだと感じている。基本的にはプロジェクトの流儀に素直に従い、余計なストレスを抱えないのが良いと考えている。例えば、私はマージコミットを作るのが好みだが、OSS活動等では「squash & mergeして」って言われることもあり、そういうときは当然素直に従うようにしている。

ということで、私のGitのワークフローについてのスタンスについて書いておこうと思う。私と一緒に働く人や、働くことを検討している人の参考になればと思います。もちろん、この辺りは、良い方向に変化もさせていきたい。例えばエントリー内でも触れていますが、私は昔はforce pushを禁止したいくらいでしたが、今は使っても良い、と思うようになりました。

Natureの特にGoでのバックエンド開発はこれに近い感じだとイメージしてもらえればと思います。ただ、できてないところもありますし、他のメンバーとの意見の違いもあるかもしれません。会社として画一的に統一する必要もないと考えています。あと、僕個人的には、最近はレビューをサボりがちで良くないとも思っています…。

ベースブランチ名など

コミットコメントとpull request

pull requestのマージ

pull requestの粒度

履歴改変

featureブランチのpush

ベースブランチへの追随

force push

レビューとマージ

モブプロをすればレビューは不要か?

最後に採用情報

このエントリをここまで読んで、Nature社内のワークフローのイメージが湧き、私たちと一緒に働きたいと思ってくださったあなた、採用応募してくださると嬉しいです。まずはカジュアルに話を聞いてみたいということであれば、Twitter @songmu までDMしてください。お待ちしています!

https://nature.global/careers/

続・マンション購入記(売買契約から内覧まで)

前回、購入を決めたところまで書いたが、今回は家ができて内覧するところまで。

ローン本審査と団信

前回通ったのは仮審査なので、次は売買契約と住宅ローンの本審査となる。

住宅ローンには団信(団体信用生命保険)というやつが含まれている。これは、本人が死んだり、障害を抱えるなどして返済能力が無くなったときにローンの残債がチャラになるという強力な保険だ。それだけの優遇があるので、その分、ある程度健康であることが求められる。審査申込時に健康状態の記入項目がある。

それまでは比較的健康には自信があり、実際その前年までは健康診断で引っかかったことはなかった。しかし、前年から外的要因の変化が色々あり、体重増加等が気になってはいたのだ。

果たして、その年の健康診断は初めての再検査となった。LDLコレステロール値が正常値範囲外になっていたのだ。焦ってすぐに再検査を申し込み、再検査までの1ヶ月の間、酒を断ちジム通いの回数を増やして再検査に臨み、正常値に戻すことができた。

その年は、禁酒などしていたのもその関係で、禁酒を始めたのは健康診断を受ける前だったが、なんかヤバい感じはしていたのだ。

再検査の結果も売買契約に間に合わせられたので、本検査と再検査の結果を踏まえて正直に健康状態を申告した。「これくらいだったら大丈夫だと思いますよ」とは言われたものの、心配だったが審査は通った。

当然だが、団信の健康状態の申告は正直に申告すること。虚偽申告はリスクが大きすぎる。

団信の世知辛さ

ここで分かったのは、団信が通るかどうかで天と地ほどの差があるということだ。35歳を超えたくらいから、僕がまさしくそうだったように、健康リスクも高まってくるし、35年ローンだと返済完了時には70歳を越えることになる。年々、住宅ローンが借りづらくなっていくのだ。

寿命も延びているし医療も進歩しているので、当時の僕くらいの健康状態は許容範囲だったのだろうが、明に団信が通らなくなるケースもある。

世知辛いし、大分厳しいと思うのがうつ病で、うつ病の持病や直近の既往歴があると、団信がほぼ通らない。もう少し金利の高いワイド団信というやつか、団信のない(=死亡時にチャラにならない)フラット35などでローンを組む必要が出てきてしまう。その場合ローンを組めたとしても返済額は大分高くなる。

住宅ローンは色々優遇されている分、外れてしまうと大分格差がついてしまう。35年も保証させる大分無茶な仕組みなので、そもそもどうなのかという話はあるが、一発アウト事案が結構あるのは厳しい。

前回のエントリーに対して「転職回数や勤続年数で信用判断されるのはナンセンス」という反応もいくつかあった。それもそうだなとは思うが、貸す側の立場からすると35年前には無かったような職業の人に35年ローンを貸しづらいというのも分からなくはない。なので、職業選択はある程度自己責任な部分もあるが、持病等でかなり差がついてしまうのは世知辛く思う。まあ、そもそも無理がある仕組みなのかも知れない。

ライフプランナーと保険

売買契約の会場で、ライフプランナーの無料相談を勧められて、その場でライフプランナーと話した。要は保険の営業なのだが、保険等について無頓着だったので良い機会だと思って話すことにした。

多分そういう人は多いのだろうし、保険会社からしても家を買えるような人が会場に集っているということで、狙い目だと目論んでいるのだろう。だからこそ、しっかりしたベテランのライフプランナーを充ててもいるようだった。実際担当してもらった方は良い方だったし今も担当してもらっている。

その後、妻も含めて何度か説明の機会を設けてもらって、医療保険と生命保険に加入した。遺族年金等についても説明してもらい、将来のために保険に入りましょうという話をされた。良く聞く典型的なセールス文句である「保険は貯蓄」も聞きました。

最初のヒアリングシートの記入で色々書かされたのだが、僕の小遣いも書く必要があった。小遣い制ではあるものの困らない額に設定しているので問題ないのだが、これを減らされる提案されると困るな、とも思っていた。しかし、そこを減らす提案はプランナーからは出てこなかったので一安心。それで気分良く保険を契約したので、それも向こうの作戦だったのかも知れない。

それと、提示されたライフプランの中で「一般的には55歳が収入のピークなので」とか説明されて、55歳時点で結構な年収額が書かれており、いやー流石にそれはないでしょ、とか思ってしまったりした。このあたりも住宅ローンの仕組み同様、昭和の残滓を感じた。

ということで、保険の見直しも家の購入と同時にできたのは良かった。団信も含めて「死ぬ準備ができた」というのがあって、いつ死んでも家族は大丈夫だという変な安心を手に入れた感じがあった。

ただ、最近、資産の見直しをする中で、少し保険に寄せすぎている感じがしているので、アセットアロケーションの見直しをしたほうが良いな、とは思ったりしている。

棚などのオプション選定

入居までのお楽しみとして、オプション選定がある。ただ、ここで盛るとクソ高くなるし、多くは別のところで調達したほうが安上がりなので、最低限に留めるのがお約束。

また、建築状況によって申し込めるオプションの期限がある。僕の場合は、買った時点で既に申し込めなくなっているオプションがいくつかあったが、カウンターキッチンを大理石へ変更する等、そもそも余り興味がないやつだったので問題なかった。

結局申し込んだのは、デザインの統一感や収納の確保のために、キッチンの背面収納及び吊戸棚と洗濯機上の吊戸棚をつけてもらった。後はレンジフードフィルターもガラス繊維のやつを追加購入した。玄関のオーダーミラーは最初はあったほうがよさそうに思ったが、高いので要らんな、となった。

フロアの耐水コーティングを勧められたが、断った。フロアコーティングは他の業者もチラシを入れてきたりして散々勧められる。しかし、どうせ汚すだろうし、子供が独立したくらいのタイミングであまりにもひどかったら張り直すとかすればいいのではないか、という話を妻とした。そもそも、子供が小さいファミリーをターゲットとした物件で、床を標準で水に弱い素材にしているのはどうかと思う。

内覧会

そんなこんなでマンションが建つのをしばらく待っていると、内覧会がやってくる。出来上がった部屋を見せてもらって、不具合がないか含めて確認する会である。

家具の配置検討のためもあって、測量用にメジャーを持っていったが、大規模マンションだし、滅多なことはなかろうということで、専門家に同行はしてもらわなかった。一箇所、壁紙に僅かなよれがあって気になったので、貼り直してもらった。

ただ、同時期に家を買った友人が何人かいたのだが、後日話をしたら、専門家に同行したもらった人は結構多く、細かい指摘をいくつか出してもらえて良かったという話も聞いた。

なので、高い買い物であるし、ここで数万円ケチるよりかは、専門家に同行してもらうことをおすすめします。

NatureのVP of Engineeringになっていました

Nature社は今年の頭にはスタッフが20人を超え(内ハードウェアエンジニア2人、ソフトウェアエンジニア8人)、いよいよ組織らしくなってきました。そして、この4月の期替わりから組織体制が見直され、各専門領域にVPを置くことになりました。目的としては、社内のリーダーシップの強化による組織の成長や、権限を明確化することにより、それぞれのチームが自律的に速度を落とさず動けることなどが挙げられます。

それにより、僕もCTOからVPoE (VP of Engineering) という立場になりました。テック関連のVPは以下の3つに役割分担することにしたのです。ソフトウェア開発とハードウェア開発でも分かれているところがIoT企業ならではとも言えるでしょう。

僕はVPoEということで、よりハード・ソフト両面でエンジニアリング組織にフォーカスしていきますし、コーポレート部門のいわゆるDXと言われるような領域にも取り組んで行きます。エンジニア採用でも引き続きカジュアル面談等を積極的におこなっていきますので、これをご覧になっているエンジニアの方にお声がけさせていただく事もあると思います。

ちょっと面白いのは、僕は開発エンジニアとしての役割も残しますが、そこではVP of Software Developmentの元で開発をおこなうという点です。

4月から、@soh335 がVP of Software Developmentとなりました。彼とは彼が新卒でカヤックに入る前のアルバイト時代からの付き合いです。僕はカヤック社員としては彼の先輩になりますが、カヤックスタッフとしては彼のほうが先輩でした。社会人としては僕は彼の先輩ですが、Webデベロッパとしては彼は僕の先輩くらいの存在です。

彼はNatureには僕より前から関わっていますし、その時はほぼ一人で開発を回していた時期もあり、近年のNature Remoの機能開発は彼がリードしていました。ですので、実質社内の体制面としては実は大きく変わるものでもありません。

ただ、彼の成長を見てきた身としては、組織図として明示的に彼の元で開発することになったことは楽しみです。彼は新卒の頃から抜群な技術力を持っていましたが、最近は同僚への影響の与え方を見ていても、本当に頼れる存在にもなりました。

エンジニア積極募集中です

ということで、Nature社では引き続きエンジニアを募集しているので、あなたの応募をお待ちしています。

https://nature.global/careers/

以下のような点が魅力的だと思います。興味を持って連絡していただけると嬉しいです。

Let's Encryptのルート証明書切替周り(完結編)

tl;dr

前回までのおさらい

Let's Encryptはルート証明書を自身(ISRG)の認証局のルート証明書(ISRG Root X1)に切り替えようとしています。現在は、IdenTrustのルート証明書(DST Root CA X3)が使われています。

正確に言うと、ISRGは新しい認証局なのでそのルート証明書の普及率も当然低く、中間証明書はIdenTrustのルート証明書でクロスサインされており、それが標準で使われています。標準がDSTになっているだけで、ISRGのルート証明書のチェーンの証明書も指定すれば今でも利用することができます。

それを、ISRG Root X1ルート証明書でサインされた中間証明書を標準に切り替えようとしています。経緯は以下に書いてあります。

https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html

これはそもそも、ISRG自身のルート証明書に切り替えていくのが本命だというのと、IdenTrustのルート証明書の有効期限が2021年9月29日に迫っているというのが背景にあります。

しかし、ISRGのルート証明書がそれなりに普及してきたと思いきや、まだそれなりに利用されているAndroid7.1以前のトラストストアにはこのルート証明書が入っていません。つまり、以下の困った状況が発生しているわけです。

これまでLet's Encryptは何度かデフォルトルート証明書の変更を計画してきましたが、延期をしてきました。しかし、遂に色々動こうとしています。

驚くべき解決方法

Let's Encryptを使う限りにおいては、今年の9/29日以降はAndroid7.1以前を切り捨てざるを得ないと考えられていましたが、なんと驚くべきソリューションが編み出されます。以下のエントリーにて昨年の12月21日にアナウンスされています。

https://letsencrypt.org/2020/12/21/extending-android-compatibility.html

これは、証明書チェーンのつなぎ方を工夫することで、古いAndroidでも引き続きサイト閲覧ができるようにするものです。https://letsencrypt.org/certificates/ の図が詳しいです。(日本語訳は古いので注意)

ルート証明書をDST Root CA X3からISRG Root X1に切り替えるだけではなく、ISRG Root X1をさらに、DST Root CA X3でチェーンを繋ぐということをしている。ルート証明書にさらにルート証明書をつなぐというウルトラ技。そんなことしていいのか。

何故これが問題の解決になるのか

チェーンが R3 --> ISRG Root X1 --> DST Root CA X3 になると、通常の環境では、ISRG Root X1までチェーンをたどると、それがトラストストアに入っているので、そこで証明書検証が成功するわけです。ここまでは普通の話。

そして、7.1以前のAndroidの場合は、ISRG Root X1がトラストストアに入っていないため、さらにチェーンをたどってDST Root CA X3にまでたどり着きます。そして、DST Root CA X3は旧Androidのトラストストアに入っている為証明書検証が成功するわけです。

しかし、それでもDST Root CA X3は9月29日で失効するわけだから、結局それ以降はエラーになってしまうのではないかと考えるでしょう。その考えは正しいです、しかしなんとそうではないのです。

なんと、旧Androidはトラストストアに入っている証明書の有効期限検証をおこなわないため、2021年9月29日より後も証明書検証に成功するらしいのです。何ということでしょう…。

このハックにより古いAndroidも救えるようになりました。かなりDirtyなハックでもあるのでどうかと思う人もいるかも知れません…。

これで、ほとんどの環境で、何も問題は起こらないし、何もする必要はないはずだ、とアナウンスされています。

いよいよデフォルトチェーンの切替

上の12月のエントリでは、1月末から2月にデフォルトチェーンを切り替えると書かれていましたが、それが例によって延びまして、正式なアナウンスが4月1日に出ています。

https://community.letsencrypt.org/t/providing-a-longer-certificate-chain-by-default/148738

5月4日にデフォルトチェーンを切り替えるというアナウンスです。これはさすがに延期されないんじゃないでしょうか。

このアナウンスでは、この新規のチェーンをlong chainと呼んでいます。このlong chainのハックを嫌って、古いAndroidを切り捨てても良い、ということであればISRG Root X1がルートになる"short chain"を利用することも可能なようです。この場合はお使いのACMEクライアントのpreferred chainオプションに"ISRG Root X1"を指定する形になります。

ちなみに、DST Root CA X3がルートになるshort chainを発行する方法はどうも無くなってしまいそうなので、チェーン変更による影響が心配な場合は、念の為切替前に一度証明書を更新しておいても良いかも知れません。

まとめ

何事も起こらないと良いですね。

ところで、会社の方でもEngineering Blogを始めたので、そちらもご覧いただけると嬉しいです。

https://engineering.nature.global/

エンジニアも積極採用中ですので、興味ある方は是非ご連絡ください。

https://nature.global/careers/job/

マンション購入記(勢いで購入を決めるまで)

家を買うつもりはあまりなかったが、ライフステージの変更に伴い買った、という良くある話です。2016年までの昔話です。

一応今の住所はあまり積極的にはネット上では公開していません。ただ、分かる人にはどのあたりか分かってしまいそうなので、その場合はそっとしてもらえると嬉しいです。

二子新地時代の購入未遂事件

結婚当初は僕は持ち家志向は強くなく、妻の方が比較的強かった。ただ、当時住んでいた二子新地が非常に気に入っていたこともあり、一度、近くの不動産屋に行ったことがあった。2011年頃の話。

確か二子玉の再開発絡みで周辺にマンションや建売住宅が建ちはじめていて、その中で当時の近所に建設中の分譲住宅が悪くない金額で売りに出されていたことがあった。それをふらっと内見させてもらい、それが良かったため、興奮した妻に連れられて不動産屋にまでも行くことになった。

「あーこれは買わされる流れかもな」と思っていたのだが、果たしてその物件は売約済みであり、その他いくつか紹介されたもののピンとくるものはなかった。

そして「もしかしたら近々会社が上場するかもしれないんですよね」とか話したら「でしたら、それまでお待ちになったほうがローン審査も通りやすいかも知れませんね」と不動産の方も言ってくれてその場は収まり助かった。当時はカヤックに勤めており上場前だった。

その後カヤックは上場まで時間がかかり、妻の持ち家志向も段々と目減りしていったこともあり、家の購入話は出なくなった。しかし、この頃の予算感では数年後には二子新地で家を買うことなど不可能になり、同等の金額で郊外にマンションを買うことになってしまうのだから東京のマンション高騰恐るべしである。

残念に思ってはいないが「カヤックがもっと早く上場してたら二子新地に家を買えたんだけどなー」と冗談を言うことがある。

ライフステージの変化

2015年に35歳にして子供が生まれ、当時住んでいた賃貸が手狭になることが見込まれたため、引っ越しを意識し始める。ただ、たまに検索して調べるくらいで、本腰では検討はしておらず、買うつもりもなかった。妻もそう思っていた。二人目をつくる可能性もあったし、一旦もう少し広めの賃貸に引っ越して、その後、家族構成を決めたら家を買うかも知れない、という感覚だった。

とは言え、今から思うと、この頃は以前と比べて家を買うことに対して抵抗感自体は少なくなっていた。収入が安定してきて、今後も安定させられるだろうと多少は自信が持てるようになってきたからだ。

また、妻としては子育てや老後までの生活は日本、そして実家が近い東京近郊が良いという意向があり、そこに対して僕も強い反対意見もなかったので、その後の人生において東京近郊が拠点にはなるんだろうな、とは思うようになっていた。

逆に、ソフトウェアエンジニアとして海外で働く可能性も当時は考え始めていて(今もたまに考えている)、その選択肢は残したいとも思っていた。ただ、東京はワンホップでいろいろなところに飛べるし、例え居を構えたとしても、単身赴任なども選択肢に入れれば、身軽さがそこまで損なわれる事も無かろうとも考えていた。

モデルルーム見学と即決購入

2016年の年始、妻が実家から一枚のチラシを持ち帰ってきた。マンションのモデルルームの案内である。場所としては妻の実家からもそこそこ近い郊外エリアで、立地は良く、好みのエリアで、値段も悪くない。週末冷やかしに行ってみよう、という話になった。それで、2016年1月16日にモデルルームの見学に行ってみたのだ。

結論を言うと、モデルルームでまんまと気に入ってしまい、その場で買うことを決めた。これまでの話の通り、他にどこも比較検討していなかったが、妻も非常に気に入っていたし、周辺の相場から大きく外れているようなことがなければ、別に迷うこともなかろうという判断だった。迷い出すとキリがない気もしたので。

見学一通り終わった後に「買うので予約させてください」と言ったら、逆に販売の担当者に「え…?もう決めちゃって良いんですか?」と困惑されてしまうくらいの即決ぶりであった。あまり「毛並みの良い客」だと見られなかったというのもあるのかも知れない。ただ、この販売の方が、ガツガツしてなかったのも逆に良いポイントではあった。

で、そこからローンの仮審査申し込みで「今の会社は勤続1年5ヶ月で4社目です」と申告したら、この担当者に絶句されて「えっ…、2社目ってことになりませんか…?」と謎の反応をされたのも面白かった。「え?そういうことして良いんですか?」って聞いたら「多くの方は精々2社とかですし、仮審査ですし…」とか言われたものの、流石にまずいだろうと思って(当たり前)、素直に申告した。どう考えてもまずい。

ちなみに当時は、はてな社に勤めていたが未上場だった。実はその翌週に上場承認が発表されるのだが、それは知っていたものの、逆に言ってはいけない状況なのであった。だから見学に行った日付も覚えているのである。

なので「カヤック」だとか「はてな」だとかフザけた名前の会社が職歴に並んでいて、未上場企業勤務で勤続年数も浅い人間が怪しまれるのも最もなことであった。タイミングが悪かったし、そんなタイミングでモデルルームに行ったのだからそもそも行く前は買う気はなかったのである。

何にせよ、初めて行ったモデルルームで即決購入とか、一般的にはやってはいけないことだと思います。

仮審査通過

仮審査はいくつか出して結局全部通った。担当者の方は、ネットで僕の経歴を調べたようだ、というようなことを言っていた。

大江戸Ruby会議04に参加した時に、amatsudaさんがHacking (My) Homeという面白い発表をしていて印象に残っていたのだが、その中の "They use Internet!" / "「社会的信用」on the net" が本当にあるのだなぁと感じた瞬間でもありました。

ということで、かなり勢いでマンション購入を決めたのである。

同じソースツリーでテストが通っていたらテストをスキップする

tl;dr

git rev-parse HEAD^{tree} でツリーオブジェクトのハッシュ値が取れるので、ブランチが異なる場合でも同じソースツリーであるかどうかを判定できます。

これを利用して、すでにテストを通ったtreeのハッシュ値をどこかに記録しておいて、同一のソースツリーに対するテストをスキップできます。

本題

よく使われている、develop/mainブランチ運用をしている場合に、ちょっとした修正を本番に入れたい場合には以下のようなフローを踏むことになるでしょう。

  1. featureブランチをdevelopブランチの先頭から切って修正を作ってテストが通るのを待つ
  2. developブランチにfeatureブランチにマージしてテストが通るのを待つ
  3. mainブランチにdevelopブランチをマージしてテストが通ったらdeployする

さて、この時、他の作業が混ざらない限りにおいては1,2,3のソースツリーの内容は実は全く同じです。それなのにテストが3回も回ってしまいます。これは無駄ですし、テスト時間が長い場合にはストレスです。

ソースツリーの内容が同じであればテストをスキップしたい

なので、ソースツリーの内容が同じであればテストをスキップすれば良いでしょう。幸いGitでツリーオブジェクトのハッシュ値を git rev-parse HEAD^{tree} で取得でき、これをソースツリーの内容の高速な同一判定に利用できます。

ref. https://git-scm.com/book/en/v2/Git-Internals-Git-Objects#_tree_objects

テストが通ったツリーのハッシュ値を記録する

後はテストが通った後に、テストをパスしたハッシュ値をどこかに保存しておいて、テスト実行前にそれを確認し、すでに通っていればテストをスキップすればよいでしょう。

Nature社では、CIにはCircleCIを使っているため、少し大げさな気がしますがハッシュ値の保存にはS3を使うことにしました。他に良いアイデアがあれば教えて欲しい!

ハッシュ値の保存

ハッシュ値をそのままファイル名として0byteのファイルをS3にputしています。

store-success-hash:
  docker:
    - image: cimg/base:stable
  steps:
    - checkout
    - run:
        name: install awscli v2
        command: |
          curl -s https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o aws.zip
          unzip aws.zip
          sudo ./aws/install
    - run:
        name: stores a hash of the tree object that has been successfully tested
        command: |
          set +e
          repotree_hash=$(git rev-parse HEAD^{tree})
          touch ${repotree_hash}
          aws s3 mv ${repotree_hash} s3://${treehash_bucket}/${repotree_hash}
          true # This step is always considered a success because it doesn't matter if it fails.

テスト前の確認

S3のpublic accessを開けておき、curlで取得して200が返ってきたらテストをスキップします。

test:
  docker:
    - image: golang:1.16.0
    - checkout
    - run:
        name: go test
        command: |
          repotree_hash=$(git rev-parse HEAD^{tree})
          set +e
          curl -f https://${treehash_bucket}.s3.amazonaws.com/${repotree_hash} 2>/dev/null
          status=$?
          set -e
          if [ $status = "0" ]; then
            echo 'skip testing because the test for the same source tree has already passed'
            exit 0
          fi
          go test ./...

public accessを空けておくのは乱暴ですが、bucket上にはハッシュ値ファイル名の空ファイルしか存在しないので大きな問題はなかろうという判断です。

またbucket上のファイルは3日でexpireする設定にしています。

まとめ

ちょっとした修正をリリースしたい時の待ち時間が減って最高です。ただ、store-success-hashの時間が地味にかかるので、短縮したいのですが、これも良いアイデアあれば募集です。

ちなみに、gitの状態に依存したテストを実施している場合等、この方法を避けたほうが良いケースもあるでしょう。hackishなテクニックでもあるので、自分の環境に適しているかどうかを確認してからご活用ください。

Thanks to

git rev-parse HEAD^{tree} でtreeのハッシュが取れるというのは @k_hanazuki さんに教えてもらいました!ありがとうございます!

関係ないボヤキ

採用情報

Natureでは一緒に高速にソフトウェア開発をするエンジニアを募集しています!

https://nature.global/careers/

GoでSQLにトレーシングコメントを埋め込んで実行する

アプリケーションが発行するSQLにコメントが埋め込めると便利です。例えば、 /* path/to/logic.go:334 */ SELECT ... のようにSQLに発行元の情報をコメントとして埋め込んでからExecすれば、DB側のログ(general log等)にも記録されるため、SREやDREサイドからも、負荷の高いSQLがアプリケーションのどこから発行されているかが分かりやすくなります。

Goには github.com/shogo82148/go-sql-proxy という、SQL実行をトレースし、フック処理を差し込める便利なライブラリがありますが、今回それにpull requestを送って、SQL実行前にクエリの書き換えができるようにしました。

これらの変更により、SQLにコメントを埋め込んでから実行できるようになりました。例えば、以下のように、PrePreparePreExecPreQueryを定義してクエリ発行前にフックしてSQLを書き換えます。

ここで呼び出されている sqlComment 関数にコメントを埋め込む処理を定義します。PreExecPreQueryif stmt.Stmt == nil の判定をしているのは、事前にPrepareされている場合にはそこでコメントが埋め込まれているため二重にコメントが埋め込まれるのを避けるためです。

sqlCommentの実装は例えば以下のようになります。stmt.QueryString を書き換えることで発行するSQLを事前に書き換えられるという寸法です。

ここで、呼び出し元のファイル名と行番号を埋め込むために findCallerという関数を呼び出していますが、この処理は例えば以下のようになります。これは go-sql-proxyのソースコードからコピペして調整したものです。skipForSQLCommentでそこまで遡るかの調整ができますが、中身は利用者側で調整してください。

ということでSQLに呼び出し元のソースコードのファイル名と行情報を埋め込めるようになりました。それ以外にもトレーシングIDなどをコメントに埋め込めると便利です。Hook関数にcontextが渡ってきているためそこから取得すれば良いでしょう。

最近本番にも投入して、非常に便利に使っています。まだ導入のための記述量が多いので、ライブラリとしてもっと簡単に導入できるようになると良いとは思っていますが、便利だと思うので、ぜひ同様に活用してみてください。

余談

このSQLに呼び出し元の情報を埋め込むというテクニックは、日本のPerl界隈ではポピュラーで、TengDBIx::Sunnyでは大昔に実装されていたテクニックです。Goでもかねてから実現したかったのですが、この度やっと追いつくことができたと感じています。

また、Google、ORMが生成するSQLが遅いときの調査を容易にする「sqlcommenter」をオープンソースで公開。Rails、Spring、Djangoなど主要なフレームワークに対応 という記事が公開されて話題になりそうだったので、焦って(?)このエントリを公開しました。

database/sqlのDB接続パラメータをアプリケーション内で明に指定する

tl;dr

Goのdatabase/sqlのDSN内のsql_modeやLocation等、固定したほうが良いパラメータ設定は、設定値に持たせるのではなく、アプリケーション内部で決め打ちしたほうが安全です。

本論

社内でMySQLを使っているので、それを例にとって書きます。

いわゆる、DSN(dataSourceName)呼ばれる、sql.Open に渡すDB接続文字列があります。これは、環境変数 DATABASE_URL 等に入れてアプリケーション内で読み出してDBに接続するでしょう。

DSNにはホスト名、ユーザー名、パスワードなどの接続先情報の他に、様々なオプションパラメータを記述することができます。以下にDSNの例を出しますが、この中の?以降がオプションパラメータです。

user:pass@tcp(myhost:3306)/dbname?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&sql_mode=%27TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY%27

しかしサービス開発において、それらのパラメータの全てを環境変数に直に設定することはおすすめしません。柔軟に色々設定できすぎてしまうことが逆に危険だからです。

DB Driverのライブラリレベルでは利用者の環境に合わせて柔軟に設定できて然るべきですが、Driverを利用する側のアプリケーションレベルでは決め打ちにした方が安全な項目も多いため、全てを設定可能にしておく必要はありません。

例えば、parsetime=trueが期待されているアプリケーションはそのパラメータが無いと動かないのでそもそも設定させる必要がないとか、sql_modeでkamipo TRADITIONALを強制したい、loc(Location)は決め打ちにしておきたい、などの要件があります。

この辺りは、開発環境、CI環境、ステージング、本番環境で揃っていたほうが変な事故も少なくなるため、設定させる余地を与えないほうが安全です。それらを設定可能にしておいて、ひょんなタイミングでずれたら怖い。特に本番運用中のDBにつなぐLocation設定が突然変わってDBの時刻がずれたら大事故です。

ですので、アプリケーションレベルでそれらを揃えてしまうのが良いでしょう。例えば以下のような具合です。

func DatabaseURL() (string, error) {
    databaseURL := os.Getenv("DATABASE_URL")
    c, err := mysql.ParseDSN(databaseURL)
    if err != nil {
        return "", err
    }
    c.Loc = time.UTC
    c.ParseTime = true
    c.Collation = "utf8mb4_general_ci"
    if c.Params == nil {
        c.Params = map[string]string{}
    }
    c.InterpolateParams = true
    // enforce kamipo TRADITIONAL!
    c.Params["sql_mode"] = "'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'"
    return c.FormatDSN(), nil
}

値を決め打ちにするのではなく、おかしな値が設定されていたらエラーを返す手もあるでしょうし、os.Getenv を内部で呼び出すのではなくパラメーター渡しにしたほう良いという指摘も出てきそうですが、その辺りはお好みで。

これを以下のようにして使えばよいだけです。

dsn, err := DatabaseURL()
if err != nil {
    // エラー処理
}
db, err := sql.Open("mysql", dsn)
...

コードにしておけば以下のようなテストコードを書くことができ、設定されて欲しいオプションが間違いなく設定されていることが担保できるため、その点でも安心です。

func TestDatabaseURL(t *testing.T) {
   orig := os.Getenv("DATABASE_URL")
   defer os.Setenv("DATABASE_URL", orig)

   os.Setenv("DATABASE_URL", "root@tcp(localhost:3306)/testdb")
   expect := "root@tcp(localhost:3306)/testdb?parseTime=true&interpolateParams=true&sql_mode=%27TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY%27"
   dsn, _ := DatabaseURL()
   if e, g := expect, dsn; e != g {
       t.Errorf("got %s, expect: %s", g, e)
   }
}

ところで最近は、秘匿情報を保管場所を分けて管理することが一般的になり、それ自体は良いことだと思いますが、逆にそれらの値がvalidかどうかのテストが難しくなっているようにも感じています。昔は各環境の設定ファイルをテストコード内で読み込んで正しいかどうかの確認などをしていましたが、それが難しくなったと感じています。その辺り何か工夫している方がいれば教えて下さい。

閑話休題。この方法で嬉しい副作用として設定値が見やすくなることがあります。オプションパラメーターを環境変数に長々と書く必要がなくなるため、例えば以下のようにDATABASE_URLを設定するだけで良くなります。

DATABASE_URL=user:pass@tcp(myhost:3306)/dbname

DSNが長くなりすぎて何を設定しているかわかりづらくなるとか、実はtypoしてパラメーター指定が誤っていたなどの事故も防げるので安心です。

実はその昔、Perlで似たような話を書いていましたが、今回はGoでのお話でした。

採用情報

Natureでは、バックエンドエンジニア、スマートフォンアプリエンジニア、その他職種を絶賛募集中ですので、ご応募お待ちしています!

https://nature.global/jp/careers

bashのシグナルハンドラ内でシグナルを特定する

シェルスクリプトのシグナルハンドラ内で受け取ったシグナルが何なのか知りたいことあると思います。しかし、単純に trap cleanup INT TERM ERR EXIT PIPE とするだけでは、cleanup関数内で受け取ったシグナルの種類を知ることができません。そこで、以下のようにすればシグナルの種類を知ることができます。

#!/bin/bash
set -Eeuo pipefail

cleanup() {
    echo "Trapped signal: $1"
    # script cleanup here
}

trap_sig() {
    for sig ; do
        trap "cleanup $sig" "$sig"
    done
}

trap_sig INT TERM ERR EXIT PIPE

# your script below

ループを回してtrapしたいシグナル毎にtrapを呼び出しています。

bashのテンプレートが話題になっていたのでなんとなく思い出したので書いてみました。

このテクニック自分で編み出したわけではなくて、出典を思い出してみると以下のstackoverflowで知ったようでした。

https://stackoverflow.com/questions/9256644/identifying-received-signal-name-in-bash

GoのWebアプリケーションでステータスコード499を記録する

この記事は、Go 4 Advent Calendar 2020の16日目の記事です。

さて、ある日、Goで書かれたHTTP APIサーバーのdeployをしたところ、急に500エラーの発生率が上がったことがありました。しかし幸いにもユーザー影響は出ていません。どうしたのでしょうか?

ALBログの調査

このシステムはAWSのApplication Load Balancer(ALB)からプロキシされていますが、エラーリクエストに対応するALBのログを調べるとステータスコード460が記録されていました。460を調べると以下のように書いてあります。

The load balancer received a request from a client, but the client closed the connection with the load balancer before the idle timeout period elapsed.
https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html

「ははぁ、これはNginxの499だな」とピンときました。サーバーがレスポンスを返す前に、クライアントが接続を切ってしまっている。例えば、スマートフォンが地下鉄など電波の届かないところに急に入ってレスポンスを受け取る前に切れてしまう等です。だからユーザー影響は出ていなかったのでした。

ところで、何故ALBはNginxと同様にクライアント断を499にしなかったのだろうと思ってしまいます。確かに499自体もそもそもは独自httpステータスコードではあるのですが、昨今4xx系のステータスコードも空きが減ってきているので、あまり独自ステータスコードを増やすのはお行儀が良くないのではないかと。ALBのエラー画面などは以前はNginxぽい感じでしたし、かなりNginxを意識して作られたサービスだと思うので余計にそう思います。

500エラーの原因判明

閑話休題。このシステムが500を返すのは、ハンドルできないerrorが上位に上がってきた場合なのですが、エラーログを確認すると果たして"context canceled"というログが記録されていました。つまり、contextのキャンセルエラーが伝播されてきて、それがハンドリングできていなかったということです。

このシステムでは、http handler内部でメインの処理に req.Context() を受け渡しています。近年GoでWebアプリケーションを書く場合には一般的なプラクティスでしょう。

ではこの req.Context() がキャンセルされるのはどの様な場合でしょうか?もちろんちゃんと公式ドキュメントに書かれています。

For incoming server requests, the context is canceled when the client's connection closes, the request is canceled (with HTTP/2), or when the ServeHTTP method returns.
https://golang.org/pkg/net/http/#Request.Context

クライアントが接続を切ったか、ハンドラーが終了したときにキャンセルされると書いてあります。今回の場合はハンドラー内部でキャンセルエラーを受け取っているため、クライアントが接続を切ったと考えると良さそうです。ALBに460が記録されているという事実とも合致します。

つまり、クライアントの接続断が起因でcontextがキャンセルされ、ハンドラー内部の処理もキャンセルされて、エラーが上がってきたというわけです。

実際、当該deployには、処理の内部でcontextを引き回せてない部分をちゃんと引き回すようにする修正が入っていました。それが原因でした。

解決方法

ユーザー影響は出ていないとは言え、この様なときに500エラーを返しっぱなし、正確に言うとクライアントには返っていないので記録しっぱなしにしておくのは、オペレーションの観点からもよろしくありません。ここは、Nginxに倣って、499を記録することにしましょう。

参考のため再掲: https://httpstatuses.com/499

errors.Is(err, context.Canceled) とチェックをしてその場合に、499を返せばよいと思うかも知れませんが、そう単純ではありません。処理内部で内部で context.WithCancel が使われるなどして、そのキャンセルエラーが上がってくる可能性があるためです。つまり、context.Canceledであっても必ずしもreq.Context() 自体がキャンセルされたとは限らないのです。

req.Context() がキャンセルされたかどうかを判断するためには、req.Context()が完了しているかどうかを判定すればよいでしょう。以下のように selectdefault を使うと良さそうです。リクエストが完了していない場合はdefaultに入るため、その下の処理に流れていきます。

select {
case <-req.Context().Done():
    w.WriteHeader(499)
    return
default:
}

まとめると、メインの処理が返してきたerrorをハンドリングして、必要な場合に499を記録するコードは以下のようになるでしょう。

const httpStatusClientClosedRequest = 499

func(w http.ResponseWriter, req *http.Request) {
    if err := mainProcess(req.Context()); err != nil {
        if errors.Is(err, context.Canceled) {
            select {
            case <-req.Context().Done():
                w.WriteHeader(httpStatusClientClosedRequest)
                return
            default:
            }
        }
        http.Error(w, "Internal Server Error!", http.StatusInternalServerError)
        return
    }
    ...
}

これで、500エラーが出なくなったためアラートも収まり、安心して眠れるようになりました。めでたしめでたし。

採用情報

ということで、スマートリモコンNature RemoなどのIoT製品を開発している、Natureでは、このように細かい割れ窓も素早く察知し、詰めをしっかり実装できるエンジニアを募集しています。興味のある方はぜひご応募ください。

https://nature.global/jp/careers