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

Let's Encryptの証明書切替周りその後

Android7.1以前でLet's Encrypt証明書のサイトが見られなくなる」の続き。

色々動きがあって猶予もできて助かった形だけど、来年9月29日以降の対応をどうするか考えないといけない状況なのは当然変わっていません。先にまとめると以下。

前回以降の動き

go-acme/legoに--preferred-chainオプションのpull requestを取り込んでもらいました

ref. https://github.com/go-acme/lego/pull/1227

certbotでは--preferred-chainというオプションが追加されcertbot-auto --preferred-chain "DST Root CA X3" とすれば現行のチェーンを維持できるように既になっていましたが、Goの標準的なACMEクライアントである、github.com/go-acme/legoでは未対応であったためpull requestを送って取り込んでもらいました。

これで、同様に lego --preferred-chain "DST Root CA X3"とすればチェーンの維持ができるようになったと同時に、ライブラリとして利用する場合も以下のようしてチェーン指定ができるようになりました。

import "github.com/go-acme/lego/v4/certificate"

var certifier *certificate.Certifier
certifier.Obtain(certificate.ObtainRequest{
    PreferredChain: "DST Root CA X3",
    ...
})

ご利用ください。

デフォルトRoot証明書の切替が来年2021年1月11日に延期になりました

ref. https://community.letsencrypt.org/t/transition-to-isrgs-root-delayed-until-jan-11-2021/125516

これで、無対応でも、古いAndoroidでサイト閲覧で警告が出るのが来年1月11日以降に先延ばしになりました。

サイト運営者としては猶予ができて助かった形ですが、Let's Encrypt側はこれまで何度も延期しているので大変そうですね。

新しい中間証明書が発行されました

ref. https://letsencrypt.org/2020/09/17/new-root-and-intermediates.html

このエントリ自体色々書いてあって面白いので全部読むのおすすめですが、中間証明書の部分に限って言うと、Let's Encryptが以前のエントリでも予告していた動きがあったということです。

つまり、現行のルート証明書のままでの来年の9月29日まで粘れる状況は整ったということ。

Firebase Hostingなどのいくつかのサイトに対応方針を問い合わせました

自社で利用している以下のサービスに対して今後の対応方針を問い合わせました。

それぞれしっかり回答はもらったのですが、公開できる範囲で問い合わせ内容を公開します。特に、Firebase Hostingに関しては丁寧な回答をもらい、かつ内容を完全に公開してくれて構わないとも答えてもらったので、さすがGoogleであった。

Firebase Hosting

時期は決まっていないが、Let's Encryptのルート証明書変更の影響が出る前に、Google自身の認証局に切り替える予定である。多くの場合、ユーザーは特に何もする必要はないが、ドメインにCAAレコードを設定して認証局を明示的に設定している場合には、CAAレコードに"pki.goog"を追加してほしい。

とのことでした。やはり独自で認証局を持っているのは強い。CAAレコードへの対応方法まで書いてくれて親切。

WordPress.com

なるべく古い端末を尊重可能な形で切替を実施していくつもりのようでした。この辺りは流石大きなコンテンツプラットフォーマーだという印象。

Zendesk Guide

ヘルプセンターとGuideでサポートされるブラウザに書かれているように、Androidは最新のバージョンのみのサポートなので、古い端末に対する特別な対応などはおこなわない予定という感じのようでした。まあ、ソリッドな判断だと思います。

プロの道具、特にインターフェースへのこだわりの話(自転車ロードレース編)

プロの中には、道具、特に自分の体が触れるインターフェース部分に神経質な程にこだわり、そして結果を出している人がいる、という話を何回かに分けて書こうと思う。一つのエントリにしようと思っていたが、脱線しまくったのでそれを残しつつ分けることにした。

ランス・アームストロング

ランス・アームストロングという伝説的な元プロ自転車ロードレーサーがいる。2000年頃、ロードレーサー乗りにとっては、圧倒的なヒーローであり、僕にとってもそうだった。

ロードレースはヨーロッパが本場ではあるが、当時はやや慣習的で閉鎖的になっているとも感じられる部分があった。そこに、ランスはアメリカから乗り込んだ。彼の所属したUSポスタルはそれまでの常識を覆すような科学的な手法やトレーニングメソッドを取り入れた。「たかが」自転車のために風洞実験まで実施した。そして、圧倒的な強さを誇った。痛快だった。

だからこそやっかまれた。ドーピングの疑惑は絶えなかった。それでも当時は証拠は出なかった。だから、単なるやっかみであり、ドーピングなどしていないと僕は思っていた。でも彼はドーピングをしていた。それこそ「科学的」に組織ぐるみで。それを知ったとき、僕はとても悲しい気持ちになった。

ランスの道具へのこだわり

前置きが長くなったが、今回したいのはその話ではない。彼のその道具へのこだわりだ。彼のサドルやペダルへのこだわりは有名だった。

ロードレーサーであれば、多かれ少なかれサドルにはこだわりがある。レースによっては半日もその上に座り続けるのだから当然だ。

ランスは、クラシックな形状のサンマルコ社のコンコール・ライトを使いつづけた。軽量化が進むロードレースの世界では重量的にはデメリットはあったものの、それでも彼はそのサドルを使い続けた。

SHIMANOにペダルを作り直させた話

もっと有名なのは彼のペダルへのこだわりだ。彼のチームは日本のSHIMANO社からパーツの供給を受けていたが、ランスは最新のペダルPD-7700を使わず、旧モデルで廃盤となったPD-7401を頑として使い続けていた。年間何万kmも走る彼らからするとペダルは消耗品である。旧モデルであるPD-7401はメーカーにも在庫がなく、チームのメカニックは世界中のサイクルショップに連絡をとって在庫を確認したという逸話もある。

その頑固な彼に対してSHIMANO社も折れる。彼一人のため基本的な構造をイチから見直し、シューズもクリート(シューズにつける金具)も含めてPD-7401をベースにペダルを新たに作り直した。ツール・ド・フランス等での彼自身による実戦テストを経て、このペダルはPD-7750(SPD-SL)として発売された。

ランスが頑として使わなかったPD-7700(SPD-R)は、SHIMANOのロードペダルとしては初めてオリジナルの機構を採用した肝煎りのモデルだった。それにはシューズのソールとペダル軸を近くできるという優位な設計コンセプトがあった。そのコンセプトをあっさり破棄して、また刷新されるのだからたまったものではない。

と、思うかも知れないが実は全然そんなことはない。なにせ、あのランス・アームストロングが使っているペダルである。ツール・ド・フランスで彼がプロトタイプを使っているときから「SHIMANOが新しいペダルを開発しているぞ!」と話題になっていたくらいサイクリストの関心は高かった。実際、発売後このペダルはよく売れた。私も買いました。

そしてロングセラーに

SPD-SLシリーズは基本設計は変えず、SHIMANOのロードペダルとして15年以上生き残り続けている。

生き残るどころか、このシリーズはSHIMANOをロードペダルの分野でも世界のトップの一角を占めさせる存在になった。当時、ロードペダル業界は、スキーのビンディングでも有名なLOOK社とTIME社が2強とも言える存在だった。ランスの愛したPD-7401も実はSHIMANOが作ったわけではなくLOOK社のOEMなのであった。SPD-SLはその2強に食い込み、今や世界的に3強とも言える存在となった。日本国内ではSPD-SLが一番人気なのではないかと思う。

SHIMANOが世界的な自転車パーツブランドになるまで

冒頭にも書いたが、自転車ロードレースはヨーロッパが本場であり、自転車メーカーやパーツメーカーも一流とされるメーカーは殆どがヨーロッパ、特にイタリアメーカーである。しかし、SHIMANOは日本メーカーでながらコンポーネントと呼ばれる駆動系パーツ群の分野でイタリアのCampagnolo(カンパニョーロ)と並び、最高峰の扱いを受けている。

昔はCampagnolo一強だった。ツール・ド・フランスに出場するようなプロチームの大半はCampagnoloのパーツを使っていた。SHIMANOは20世紀後半から根気強くそこに食い込み、2強とも言える地位を獲得した。今はSRAMも入れて3強か。

SHIMANOはプロロードレースチームに道具を供給する中で選手のフィードバックを大事にし、それを素早く取り入れた。それが選手に好評を博し、Campagnoloの牙城を崩せた理由の一つだと言われている。ランスのペダルの件はまさしくそれを象徴する話である。

ランスが勝つことでロードレースにおけるSHIMANOの存在感も増していった。それもあって、僕は熱狂してロードレースを見ていたのかも知れない。

ランス・アームストロングの自転車に用いられたSHIMANOの77/78系DURA-ACE、アメリカのTREK社の5500,5900カーボンフレーム、これらは、他のヨーロッパプロが乗るイタリア車に比べると販売価格はずっと安かった。イタリア車が120万円くらいのところを、ランスが乗っていたマシンは50万で買えた。

そういうブランド価値が付きすぎていない道具で勝つ姿にも魅力を感じていたのだと思う。ブランド的なものをいけ好かないものだと感じてしまう貧乏根性故か。そして、今の僕のロードレーサーのコンポーネントもSHIMANO DURA-ACEで揃えている。

まとめ

ということで、ランスは、サドルやペダルと言った自分のお尻や足が道具と触れる部分に非常に神経を使っていた。そんな彼を神経質だと笑うことは当然できないでしょう。

次回は競技ボウリングについて書くかも知れません。

Android7.1以前でLet's Encrypt証明書のサイトが見られなくなる

追記: その後の動きについて書きました → Let's Encryptの証明書切替周りその後

このサイトはLet's Encryptで証明書発行しているのでタイトルの件が気になったのだが、どうもあまり話題になっていない。恥ずかしながらSSL周り詳しいわけじゃないので、誤っているかも知れない。識者の意見を求む。

Let's Encryptのルート証明書の変更

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

正確に言うと、ISRGは新しい認証局でそのルート証明書の普及率も当然低かったため、中間証明書はIdenTrustのルート証明書でクロスサインされており、それが標準で使われている。それを、ISRG Root X1ルート証明書でサインされた中間証明書を標準に切り替えようとしている。経緯は以下に書いてある。

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

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

切り替えの延長

当初の予定では、今年の7月8日から切り替える予定だったが、9月29日に延長になった。

https://community.letsencrypt.org/t/transition-to-isrgs-root-delayed-until-sep-29/125516

サイトが閲覧できなくなる古いAndorid環境が結構残っていることが大きな理由。Android7.1以前のバージョンにはISRGのルート証明書が入っていないことが原因。ISRGのルート証明書が入ったのはAndroid 7.1.1以降だと以下のエントリにある。なので、Android7.1もおそらくアウト。

https://community.letsencrypt.org/t/transitioning-to-isrgs-root/94056

Androidユーザーの66%しか7.1以降を使っていないという実情があり、無視できないということらしい。(そりゃそうだ…)

ルート証明書の切り替え実施

9月29日以降はISRGのルート証明書を標準にするが、それまでに現行のIdenTrustのルート証明書のチェーンのままサーバー証明書を発行できるオプションを用意する予定のようだ。

つまり、9月29日以降、Let's Encryptでサーバー証明書を発行した場合、デフォルトではAndroid7.1以前で見られなくなる。オプションでルート証明書をIdenTrustのものを指定することで、回避は可能。ただし、IdenTrustのルート証明書自身の有効期限が2021年9月29日なので、それまでにはなんらかの移行プランを考える必要がある。

もしかしたら、今年の9/29のアクション方針が変更になる可能性はあるが、なにはともあれ、来年の9月29日がデッドラインだということ。

移行プラン

現状66%のユーザーにしかAndroid7.1以降が普及していないことを考えると、9月末までに状況が大きく改善されるとは思えないし、来年の9月にしたってそうでしょう。

Android7.1以前のユーザーがサイトやシステムの運営上無視できない場合はCAの移行も考えたほうが良いでしょう。実際、上に挙げた延期のアナウンスエントリでも「状況が大きく変化するとは期待できないので、古いAndroidのためにCAの変更も考えたほうが良いかもしれません。我々は、そのための十分な時間を用意します。」ということが書かれています…!

We don’t expect the Android situation to change much in the coming year, which means sites that need support for older Android devices may need to switch CAs. We’d like to give those sites plenty of time to transition.

我々はどうすればいいのか

Android7.1以前のサポートを打ち切って良いのであれば気にしなくて良いし、このサイトは個人サイトなので別にそれでも良いと思っていますが多くの場合そうもいかないので、以下のような対応になるでしょうか。

また、SaaS系のサービスで自動証明書発行システムの裏側でLet's Encryptが使われていることも多々あるので、気になる場合はそれらのサービスに対応状況を問い合わせることも必要かもしれません。

しかし、当初の予定通り今年の7/8に切り替えが実施されていたら世の中的に阿鼻叫喚だったと思うので、そうならなくて良かったですね。

KibelaのリンクをSlack上で展開するdeglacerというSlack Appを作った

https://github.com/Songmu/deglacer

KibelaのリンクをSlackに貼り付けたら展開して欲しいという気持ちがあり、公式に対応してほしいなーと思っていたのだが、なかなか対応されないのでSlack Appを習作する意味でも作ってみた。

とは言っても、以下のhigebuさんのサイトにめっちゃ詳しく書いてあって、それをそのまま参考にさせてもらいました。

Kibela のリンクを Slack に貼ったときに展開されるようにした

何故か僕が勘違いしていて、これのソースコードがhttps://github.com/otofune/slack-unfurl-kibelaだと思っていて、これがRuby製なのでそれをGoに移植した。しかし本当は上記のエントリ上のソースはhttps://github.com/higebu/slack-app-unfurl-kibelaで、これはGo製なのであった…。自分でも何を言っているのか分からない…。

動かし方

README.mdに記載の通り。Webアプリとして動かしますが、HerokuやGoogle App Engineとして動かすのがお手軽。

そもそもの動機として、社内の技術スタックとしてHerokuではなくGoogle App Engineでやってみたかったというのがあり、Google App Engineで動かしやすいように作ってある。

結果として、HerokuでもGoogle App EngineでもGoのアプリケーションを今や簡単に動かせるということが分かったので良かった。

機能

Slackに貼られたKibelaのURLを展開するが、コメントリンクの場合はコメントを展開します。また、Vefification Tokenによるリクエスト検証も必須にしてますが、今後はSigning Secretによるリクエスト検証に切り替えたほうが良さそうに思っています…。

命名について

西洋調理手法であるdéglacer(デグラッセ)から命名しました。鍋についたこびり付いた焦げのようなものをワインなど酒を使って木べらなどでこそぎながら煮溶かしてソースなどを作る手法です。それっぽくて、ちょっと気に入っています。

GoとPaaS

degacerを作って、GoのWebアプリやSlack Appを簡単にHerokuやGoogle App Engineにdeployできることが分かり、Webアプリの類を作る心理的ハードルがものすごく下がったので今後色々作っていきたい。そのあたりの解説はそのうちするかも知れません。

40歳になった話

6月5日が誕生日なのでもうひと月以上経っている。同じ日に娘が5歳になった。

40歳は社会人の折返しとも言えるし、人生の折返しとも言えるなかなかおもしろい年齢だ。20年前が20歳だったので、それを踏まえて20年後の60歳がどうなっているかなんとなく見通しが立つ気がする。気がするだけだとは思う。

40代の挑戦を考えた時に、新型コロナで世の中が大きく動き、逆に次の動きが読めない中、僕自身は去年から既にNature社での新しい挑戦を始められていて、40代もそれでスタートを切れるというのは幸いに思う。

前職の元CTOで僕の元上司である @stanaka さんが、はてなを辞めるタイミングで「10年スパンでキャリアを考えている」みたいなことを言っていて、そのときは「そんなもんかね」と思っていたが、なんだかんだで影響を受けている。

やっと大人になった気もしてくる。自分の10代と親が40代だったころがちょうどかぶっていて、よくコミュニケーションを取っていたので、自分が大人だと意識している年代がそれくらいだというのもあるだろう。

父が去年亡くなり、それ自体がそこまで影響しているわけでもないとは思うが、なんとなく子供の将来のことや老後や死後のことなども考え始めて、資産の見直しもしつつ、iDeCoや、つみたてNISAを始めるなどした。

数年くらい前から思っているのは「どうやら自分は死ぬらしい」ということ。つまり、この感じだと「完全電脳化は間に合わない」。僕としてはあまり「電脳化をして不老不死になりたい」みたいな強い欲求があるわけではない。ただ、そのような技術が一般的になれば死なない可能性もあるのかな?とか考えていたくらいなのだが、どうもそれが自分の死までには間に合わなさそうだな、と。

電脳化は単に技術だけの話じゃなくて、所謂「倫理」と言われるような人類の価値観のアップデートも必要になる。多分自分はその価値観のアップデートができない側になるだろうな、とも。例えば、一回電源が落ちて再起動してきた自分の電脳を「死んでない」と受け入れられるかというと、僕には難しいんじゃないかと。将来的に人類はそれを易易と受け入れるだろうし、それは全く悪いことだとも思わない。

イーロン・マスクらが進めているように「電脳化手術」自体は僕の生きているうちには始まるでしょう。当初は不死を目的するものではないにせよ。それらはきっとレーシック手術が受け入れられていった過程のように、それより時間はかかるけど、世に膾炙していくのでしょう。その黎明が見られそうなことは楽しみにしている。

Goでpublicである必要がないメソッドを一括でprivateにする雑Perlスクリプト

ボツネタかつ、golintが非推奨になりそうなので、急いで書きます。

golintを守ってれば、最低限Effective Goぽいコードを書くことができるので、初心者取っ掛かりのlintとしては良いよなーと個人的には思っており、非推奨には寂しい気持ちもあります。とはいえ、Goが言語自体も変化をしていく中で妥当な判断だと思います。しかし、他言語でも見られる光景ですが、lintをメンテナンスする難しさというものを感じてしまいますね…。

本題

さて、プロジェクトに途中からgolintを導入しようとすると(もはや導入すべきなのかどうか分かりませんが)、やたら警告が出てきて心が折れてしまうことで知られています。特に、 "exported $hoge should have comment or be unexported" と言うメッセージは量も多く、対応も大変なので困りものです。

これは「publicインターフェースには適切にドキュメントコメントを書くか、privateに変更しろ」というメッセージなのですが、これにちゃんと対応しようとすると、ドキュメントをちゃんと書く必要があるので大変なのです。まあ、サボってきたツケなのですが…。

以前の職場で、これを一括置換してダミーコメントで埋めてお茶を濁して、同僚に嫌な顔をされたことがあります。今では反省しております。

不必要なpublicインターフェースをprivateにする

とりあえず、まずはpublicである必要がないインターフェースはprivateにしてしまえばよいではないか、という雑なアイデアを思いつきました。プロジェクト内の別パッケージから利用されていないのであれば、privateにできるはずです。

静的解析でできればかっこいいのですが、めんどくさくてまずはPerlによる文字列置換でやってみた。アプローチは以下の通り。

golintで指摘されたインターフェース名一つ一つに対して以下をおこなう。

  1. インターフェース名をprivateにして(lcfirstして)一括置換する
  2. プロジェクトをテストコード含めてコンパイルが通るか試す
  3. コンパイルが通ったら、git commit する。通らなかったら git checkout . で戻す
  4. 次のインターフェースへ

つまり「置換してみてコンパイルが通ったら問題なかろう」という雑アプローチです。

実際のコード

これをGoのプロジェクト内で golint $(go list ./...) | perl unexport.pl とかやって使う。結構豪快なことをやっているので、実際に自分のプロジェクトで試してみたいときは、いつものコードベースとは別の場所にcloneしてから試すと良いかも知れません。結構時間かかります。

go test -c を使ってテストは実行しないもののコンパイルしているのが特徴です。

ボツになりました

手元のプロジェクトでやったところ、コンパイルは通ってビルドはできるものの、テストが全然通らなくなってしまったので諦めました。もう少し頑張ればなんとかなったのかも知れませんが…。

どうしてもreflectを使っているところで動作が変わり得てしまうので、その辺りがネックになりました。独自でreflect使ってなくても、JSON等のMarshal/Unmarshalでは内部的に使われているので。

その辺りがネックになることは事前に分かっていて、微調整で済めばワンチャン行けるかと思っていたのですがダメでした。

まあこういう雑な仕事をPerlでやるのは楽しいですね。正しい静的解析アプローチでどなたかが改めて解決してくれないか願っています。

今後のlint

社内プロジェクトに何らかのlintを入れたいんですが、staticcheck を手元で試した感じ良さそうなので、reviewdogと併せて導入しようかと考えています。

GoのアプリケーションをOpenMetricsを使って監視する

前のエントリでDatadogについて書いたが、実際にGoのアプリケーションがOpenMetricsを吐くようにするのはどうのようすれば良いかをもう少し解説します。

OpenMetricsとは?

元々[Prometheus]が利用しているフォーマット。Prometheusは"Promethues exporter"と呼ばれる監視対象からメトリクスを集約する作りになっている。

Prometheus exporterは実は「単なるHTTPのエンドポイント」であり、そのレスポンスが独自のテキストフォーマットになっている。このフォーマットを標準化しようとして提唱されているのがOpenMetrics。

https://openmetrics.io/

実際問題としては、Prometheusのドキュメントの方がまだまだ充実している。

DatadogにはOpenMetricsのインテグレーションがあり、自前でPrometheusサーバーを建てずともPrometheus exporterからのデータ収集をやってくれる。

実際に試してみる

百聞は一見にしかず、ということで、公式のガイドのGoアプリケーション実装サンプルを動かしてみる。

package main

import (
    "net/http"

    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(":2112", nil)
}

これだけでれっきとした、Prometheus exporter機能を備えたGoのアプリケーションとなる。このコードを go run main.go すると2112ポートが開いてHTTPアクセスを受け付けるので、そこの/metricsにアクセスしてみたのが以下。

$ curl http://localhost:2112/metrics
...(snip)...
# HELP go_memstats_sys_bytes Number of bytes obtained from system.
# TYPE go_memstats_sys_bytes gauge
go_memstats_sys_bytes 7.1649288e+07
# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 8
# HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.
# TYPE promhttp_metric_handler_requests_in_flight gauge
promhttp_metric_handler_requests_in_flight 1
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 0
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0

go_memstats_sys_bytes 7.1649288e+07 のように、メトリクスキーと値がスペースで区切られているシンプルなテキストフォーマット。# で始まる行はコメントアノテーションでメトリクスの説明やデータタイプが記載されている。

メトリクスタイプのはここでは"gauge"と"counter"がある。gaugeはそのままの測定値で、counterはカウンター値である。他にも"summary"と"histogram"がある。ref. https://prometheus.io/docs/concepts/metric_types/

promhttp_metric_handler_requests_total{code="200"} 0 のように、メトリクスキーにラベルを付与できる点も特徴で、これによりmulti demensionalなメトリクス解析ができる。

メトリクスキーやラベルの命名についてはhttps://prometheus.io/docs/practices/naming/を見ると参考になる。

promhttpあれこれ

前項のサンプルの promhttp.Handler() をしたら、Goのアプリケーションの基本的なメトリクスは取れる。まずは最低限そこから始めても良いが、アプリケーション独自のメトリクスを取りたくなるのが人情であり、本命である。

promhttp.Handler() のレスポンスに独自のメトリクスを追加するには、github.com/prometheus/client_golang/prometheus/promautoパッケージを使うのがお手軽。例えば以下のような具合である。

// 宣言
var totalConnMetric = promauto.NewCounter(prometheus.CounterOpts{
    Name: "ws_conn_total",
    Help: "total connections",
})
// インクリメント
totalConnMetric.Inc()

メトリクスにラベルを付与したい場合は以下のようにすれば良い。

// 宣言
var totalConnMetric = promauto.NewCounterVec(prometheus.CounterOpts{
    Name: "ws_conn_total",
    Help: "total connections",
}, []string{"pod_id"})
// インクリメント
totalConnMetric.WithLabelValues("xxxx-pod-id-xxxx").Inc()
// 上と同様のインクリメント(ラベルが複数あるときは`With`の方が分かり良い)
totalConnMetric.With(prometheus.Labels{"pod_id":"xxxx-pod-id-xxxx"}).Inc()

(小ネタ)ECSのtask idを取得する

ECSでDatadog Agentに監視対象コンテナをdiscoveryさせている場合、その監視対象のtask idやcontainer id的なものをメトリクスに付与してくれない。これは少し困るのだが、上のように独自で"pod_id"ラベルを付与することでカバーしている。

pod idの取得は以下のようにアプリケーション起動前にラッパースクリプト内で、ECS TaskのARNを取得して環境変数にぶちこんでいる。

export MYAPP_POD_ID=$(wget -qO- -T 5 ${ECS_CONTAINER_METADATA_URI}/task | jq -r '.TaskARN' | cut -f 2 -d'/')
exec /bin/myapp

アプリケーションコンテナにはaplineを使っている。wgetは標準で入っているやつを使っているが、このようにリトライをサクッと設定できるのは便利。またjqapk add jqで入るので驚いた。

cutを使っているのは、Task Arnは "arn:aws:ecs:us-west-2:012345678910:task/2b88376d-aba3-4950-9ddf-bcb0f388a40c" のようなフォーマットなので、UUIDの部分だけを取り出したかったため。

コンテナからアプリケーションを起動する時に、Goのバイナリをダイレクトで起動したい気持ちはあるものの、このようにラッパーシェルを噛ましておくと、こういう雑なワンライナーを事前に呼び出すこととかができるので便利。

promhttp.Handler() を使わずに独自メトリクスのみを出力する

promhttp.Handler() は便利だが、標準で色々出力してくれるので、それがnoisyなこともある。そういう場合は自分でサラからハンドラーを作る。

この場合は、独自のprometheus.Registryを定義し、それにメトリクスを登録すし、そのRegistryを使ってハンドラーを作る。具体的には以下のような具合。

var (
    promReg         = prometheus.NewRegistry()
    totalConnMetric = promauto.NewCounter(prometheus.CounterOpts{
        Name: "ws_conn_total",
        Help: "total connections",
    }
)

func init() {
    promreg.MustRegister(totalConnMetric)
}

func main() {
    http.Handle("/metrics", promhttp.HandlerFor(promReg, promhttp.HandlerOpts{}))
    http.ListenAndServe(":2112", nil)
}

前のエントリで紹介した、監視コンテナ"watchdog"はこの方法を使っている。

ちょっとした難点

ということでgithub.com/prometheus/client_golangは便利なのだが、依存が多いのが少し困りもの。特にOpenMetricsはこのエントリで説明したテキスト形式だけではなく、protobufでの出力も対応しているが、僕らの用途ではテキストだけでよいのに、protobuf関連の依存もアプリケーションに入ってしまうのが少しだるい。

テキストフォーマット出すだけなら、依存の少ないパッケージを独自で作ることも可能そうだが、そこまでやる気力は現状ないので、今はこれを使っている。公式ライブラリだし。

ECSとGoで構築したシステムにDatadogを導入する

追記: GoのアプリケーションをOpenMetricsを使ってObservableにする方法については別エントリを書きました。 → https://songmu.jp/riji/entry/2020-05-18-go-openmetrics.html

ECSとGoで運用しているシステムに対するDatadogの日本語知見があまり無さそうだったので書いてみる。ちなみに以下の環境です。

背景として3月にNature Remoのインフラアーキテクチャ改善をしていて、その前にもうちょっと監視を整えたほうが良いな、ということでDatadogを導入したのがある。テストがないとリファクタリングできないように、監視がないとアーキテクチャのアップデートもやりづらいという話。元々はCloudwatchで頑張っていたが厳しくなった。今もまだ併用はしているがDatadogに寄せたいと考えている。

元々Datadogで便利そうだなーと思っていたのは、アプリケーション固有のメトリクス監視を簡単に設定できそうであったこと、具体的には「アプリケーションコンテナがhealth endpointでOpenMetrics等を吐いておけば、datadog agentが自動でそれをディスカバリしてDatadog側にメトリクスを送信してくれる」という点。OpenMetricsというのはPrometheusで使われているメトリクスフォーマットのことです。

で、実際以下を実現できたので良かった。

やったことは主に以下の2つ。メトリクス監視中心に設定した。それぞれ解説していく。

Datadog Agentの導入

ECSのEC2モードだとDaemon setのコンテナを各EC2に立てることになる(ちなみにFargateだと当然sidecarになる)。Datadog公式のECSのヘルプに丁寧に設定方法が書いてあるが、ここにタスク定義の雛形があるので、これをコピペして、ECSのDaemonとして立てればいいだけなので非常に簡単。コンテナとして起動するわけだけど、EC2のホストOSの監視もやってくれるので便利。

Datadog Agent側には以降設定を追加する必要もない。アプリケーション側でディスカバリの設定をすれば勝手にAgentが監視対象を見つけて、メトリクスを収集してDatadog側に投げてくれる。これは非常に便利。

アプリケーションコンテナの監視設定

ECSに限らずDatadogでコンテナのカスタム監視設定はDocker Labelを使っておこなう。これは、Docker Integrations Autodiscoveryに書いてある。

ECSの場合については書かれてないが、アプリケーション側のタスク定義のcontainerDefinitionsキー配下に以下のような感じで設定を書けばいいということが分かった。

"dockerLabels": {
  "com.datadoghq.ad.instances": "[{\"prometheus_url\":\"http://%%host%%:%%port%%/status/metrics\",\"namespace\":\"api\",\"metrics\":[\"*\"]}]",
  "com.datadoghq.ad.check_names": "[\"openmetrics\"]",
  "com.datadoghq.ad.init_configs": "[{}]",
  "name": "api"
},

これは、APIのコンテナの、http://%%host%%:%%port%%/status/metrics にアクセスすれば、OpenMetricsフォーマットでメトリクスが取れるということ。%%host%%%%port%%はこれでよくて、動的に補完されてそれをDatadog Agentがよしなに見つけてくれる。

com.datadoghq.ad.instancesのmetricsに、ここでは["*"] を指定してエンドポイントが吐く全てのメトリクスを取り込むようにしているが、個々の設定を調整して必要なメトリクスのみを取り込むようにできるのも地味に嬉しいポイント。

ちなみに、"com.datadoghq.ad.check_names"に設定できる項目に"openmetrics"じゃなくて"prometheus"というのもあるが、自前でprometheusを立ててる場合でないのなら、"openmetrics"を使うのが今は正解のようです。

com.datadoghq.ad.instancesへの設定値は各インテグレーションのヘルプページを参照する。OpenMetricsの場合はここ

OpenMetrics以外の監視にも当然対応している。社内で導入したのは以下。

memcachedは以下の具合。(実際には社内でmemcachedを使っているわけでなく、katsubushiの監視のためにこの設定を入れている)

"dockerLabels": {
  "com.datadoghq.ad.check_names": "[\"mcache\"]",
  "com.datadoghq.ad.init_configs": "[{}]",
  "com.datadoghq.ad.instances": "[{\"url\": \"%%host%%\",\"port\": \"11212\"}]"
},

また、Nginxだと以下のような感じ。これはタスク定義ではなくDockerfile上にLABELを直書きしているが、こういうことも可能である。

LABEL "com.datadoghq.ad.check_names"='["nginx"]'
LABEL "com.datadoghq.ad.instances"='[{"nginx_status_url":"http://%%host%%:%%port%%/nginx_status"}]'
LABEL "com.datadoghq.ad.init_configs"='[{}]'

このように、監視対象側が監視の口を空けておけば、Datadog Agentが勝手に見つけてメトリクスを収集してくれるのは非常に良い体験だった。

GoアプリケーションがOpenMetricsを吐くようにする

あとはGoアプリケーションがOpenMetricsを吐くようにすればOK。ちなみにDatadogにはexpvarを使ったGoアプリケーション監視のインテグレーションもあるのだが、これは使わなかった。以下の理由。

GoのアプリケーションにOpenMetricsを吐くためには、github.com/prometheus/client_golang/prometheus/promhttpを使うのがお手軽。以下のようにするだけで基本的なメトリクスが出力できる。

import (
    "http"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    ...
    http.Handle("/status/metrics", promhttp.Handler())
    ...
}

カスタムメトリクスの追加もここでは触れませんが簡単です。Prometheusのデータモデルをある程度理解する必要がありますが、その辺りはPromethesの公式ヘルプや書籍に軽く目を通すと良いでしょう。

タグ付け等、メトリクスに対するMulti dimension data modelの考え方はDatadogでも共通なので、この辺りを理解しておけば、Datadog上でダッシュボードを作る際にも役立ちます。この辺りはまた別途エントリを書こうと思う。

監視コンテナの追加

アプリケーションに従属しないカスタムメトリクスを取りたい場合も、監視コンテナを一つ立ててOpenMetricsを吐かせれば簡単に実現できる。これもECSクラスタ上に"watchdog"というサービスを作って1タスクを動かしている。これもecspresso管理でサクッとできるのでお手軽。

(余談)プロビジョニングが消えた

ecspressoとDatadog導入、その他システム改善により、マネージドサービス以外のほとんどのものをECS上で管理できるようにした結果、EC2のプロビジョニングが不要になった。

元々は、監視スクリプトの追加などのためにプロビジョニングを実施していて、Packerを使って定期的にゴールデンイメージを焼く運用だった。しかしそこも仕組み化されきっておらず古いイメージを使い続けてしまうなどの問題もあった。

現在は、Amazon ECS-optimized AMIの最新を使うだけにして、独自のAMI管理をせずとも良くなったので楽になった。

結果として、インスタンスの入れ替えの手間や心理的障壁も減り、2018年から残存していたインスタンスがあったのだがそれを滅ぼすことができた。なんなら2019年に起動したインスタンスもいなくなり、インスタンス台数もかなり削減できた。やっぱ監視系含めてこの辺りちゃんとやるの大事ですね。

Datadog所感

最後にDatadogの所感を書き出しておきます。

Pros

Cons

Consに対しては公式の対応を望むのと、なんか良い方法あれば教えて下さい。

また、この辺りのシステム改善に取り組みたいという方も歓迎なので、ぜひ採用に応募してきて下さい。お待ちしています。

https://nature.global/jp/careers

GoのWebアプリでクライアントIPを検出するrealipモジュール

github.com/natureglobal/realip

これはngx_http_realip_moduleと同様の挙動を、Goのhttpハンドラをラップするミドルウェアレイヤで実現するものです。

アプリケーションが信頼できるNginx等のproxy配下にあれば、X-Real-IP ヘッダなどをそのままクライアントIPとして採用すればよいのですが、クラウドのロードバランサー、例えばALBなどに直接Goのアプリケーションをぶら下げている場合、ALBはX-Real-IPを付けてくれないので、アプリケーション側でクライアントのIP検出をおこなう必要があります。そういったときにこのモジュールが有用です。

X-Forwarded-Forを見れば良いという話ではあるのですが、HTTPヘッダは簡単に偽装できますし、CDNを使っているなど多段になっているケースでも判別は地味に厄介です。ヘッダを付けてきたREMOTE_ADDRが信頼できるか、数珠つなぎになっているX-Forwarded-Forヘッダの、どのエントリをクライアントIPとみなせばよいか、などの問題があります。

冒頭にも書いたとおり、Nginxではngx_http_realip_moduleを使うことによりクライアントIP検出が実現できますが、realipモジュールはそれと同様の設定項目を使ってクライアントIPの検出をします。

利用例

_, ipnet, _ := net.ParseCIDR("172.16.0.0/12")
var middleware func(http.Handler) http.Handler = realip.MustMiddleware(&realip.Config{
    RealIPFrom:      []*net.IPNet{ipnet},
    RealIPHeader:    "X-Forwarded-For",
    SetHeader:       "X-Real-IP",
    RealIPRecursive: true,
})
var handler http.HandlerFunc = func(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, req.Header.Get("X-Real-IP"))
})
handler = middleware(handler)

この場合、X-Forwarded-For ヘッダから、クライアントIPの検出をおこない、それをX-Real-IPヘッダにセットしてから内側のハンドラにリクエストを渡します。なので内部のハンドラではX-Real-IPヘッダを見ればクライアントIPを知ることができます。

また、上記の設定だと、172.16.0.0/12 のIPレンジを信頼できるレンジとみなし、そのIP帯から来たリクエストヘッダを信頼するとともに、X-Forwarded-Forの途中経路にそのレンジにアドレスが入っている場合はそれを飛ばし、「最初の信頼できないIPアドレス」をクライアントIPとして採用するという挙動になります。

作ったきっかけとか

Nginxを使わずに、ALBに後ろにGoのアプリケーションをぶら下げようとしているのでこういうの必要だよなーと思って作りました。先行実装が無いことはなかったんですが、案外ちゃんとしたやつがなかったのと、ngx_http_realip_moduleの設定に合わせてある、というところが独自の優位性です。

同時に、ngx_http_realip_moduleについての理解も整理できたのでその解説も書こうかと思ってたんですが、いつまで経っても書けそうにないのでまずはモジュールの紹介に留めます。

本当は、ALBがngx_http_realip_module同様の設定ができるようになってくれると嬉しいんですけどね…。どうですかね、AWSさん…?

まあ、このモジュール自体は便利だと思うのでぜひご利用ください。

Dependabotを使ってGoプロジェクトの依存を更新するノウハウ

システムを運用していく以上、ライブラリは常に最新を使いたい。最近は依存ライブラリの更新を検知してくれる便利なサービスがいくつかあって、Nature社ではDependabotを使っている。

https://dependabot.com/

Renovateの方が便利そう、という話も聞くのだが、とりあえずDependabotはGitHubが買収して、privateリポジトリでも無料で使えるので利用している。

導入自体は簡単で、画面のガイドどおりに進んでいけば、良い感じに言語や依存管理ツールも自動検出してくれる。設定ファイルは特に置いていない。慣れてきたり、設定を横展開したくなった場合に置くと便利そう。

参考: Dependabotの設定ファイルを置くようにした

動作の様子

前提としてGo Modulesで依存管理をしているが、依存ライブラリの更新があると以下のようにpull requestを上げてくれる。

差分等も見やすく出してくれるし、まあまあ便利。pull request上にコメントしてbotを操作するのも面白いが、その手引も丁寧に書いてある。

しかし、いくつか使いづらい点があるので自前でハックしてなんとかしている。この辺りは諸々公式対応を望む部分。

go mod tidyして欲しい

これが一番大きな問題。以下のissueでやり取りされている。

https://github.com/dependabot/feedback/issues/215

dependabotのpull requestは、go get <module>@<new-version> しかしていない状態で上がってくる。つまり推移依存の解決は全くやってくれない。本来であれば、go mod tidyを実行して依存ツリーの構築をし直して欲しいところなのだがそれをやってくれない。

とは言え、実はgo mod tidyを実行するのはそう簡単ではない。公開ライブラリだけ使っていれば話は簡単だが、プライベートモジュールがある場合それらへのアクセス権がないとgo mod tidyが失敗してしまうからだ。

上のissueもだいぶロングランで苦戦している。去年の5月の以下のコメントがちょっと愉快。

We've been busy with the acquisition recently so haven't made progress on this yet. I'm hoping we'll be able to build out better Go support over the next few months though. -- https://github.com/dependabot/feedback/issues/215#issuecomment-496516828

自前でgo mod tidyして更新をpushする

どう対処しているかというと、dependabotのpull requestの更新をフックして、CircleCI上でpull requestの上書き更新をしている。具体的には、以下のようなシェル芸を.circleci/config.ymlに仕込んでいる。

command: |
  if $(echo {{CI{RCLE_BRANCH}" | grep '^dependabot/' >/dev/null) && [ "${CIRCLE_USERNAME}" = "" ]; then
    go mod tidy
    go mod vendor
    git add vendor go.sum go.mod
    export GIT_COMMITTER_NAME=nature-bot
    export GIT_AUTHOR_NAME=nature-bot
    export EMAIL='nature-bot@example.com'
    git commit -m "make vendor"
    git push origin "${CIRCLE_BRANCH}"
  fi

以前のエントリで説明したとおり、CircleCI上ではMachine account(この場合nature-bot)に、privateリポジトリへのアクセス権限を与えているので、その権限を使って、go mod tidy および go mod vendor (vendoringしているので) を実行して、その更新差分のcommitとpushをおこなっているという具合。pushできる権限渡してしまうのは嫌な感じがするが致し方ない。

dependabotによる更新かどうかを状況証拠的に判別している。上のif条件がそれだが以下の通り。

特殊なbotによる更新であるためか、$CIRCLE_USERNAMEが空文字列なのである。これはどこにもドキュメントはされていないので急に変わる可能性はある。

ちなみに、自前でpull requestブランチに更新をかけているので、Dependabotに対して一部追加の操作がさせられなくなる。例えば追加の依存が降ってきた場合にそれを更新させられないなど。その場合は @dependabot recreate とpull requestにコメントすることで、Dependabotにpull requestを作り直させている。

セマンティックバージョニングされていないライブラリは更新対象外となる

これも地味に困る点。タグが適宜打たれていないライブラリはDependabotは更新を抽出してくれない。また、ライブラリのセマンティックバージョン以外の地点(コミットハッシュ)に依存している場合も更新されない。

後者の場合は、単純にセマンティックバージョニングされた地点まで依存ライブラリを自力で更新すれば、以後はDependabotの更新対象となる。

問題は前者。基本的にはライブラリ作者に働きかけてバージョニングをしてもらうのが良いし、現在は多くが対応していますが、ポリシーとしてバージョニングをしていないライブラリ類が困りもの。

つまりはよく使う golang.org/x 系がバージョニングしないポリシーなので困るのです。

この辺りは推移依存で上がっていくのに任せるか、go get golang.org/x/net@latest みたいなコマンドを定期的に実行して手動で依存を更新する感じになります。ここもDependabotの公式対応が望まれます。

更新がうるさい場合

これは些末な問題だし、頑張って対応するのが本道。ただ、ライブラリによっては関係ない依存更新がバンバン飛んできてめんどくさいことがあります。まあ、aws-sdk-go のことなんですけど。

その場合の対処方法の一つとして、 @dependabot ignore this minor version というコメント操作があります。これは「このマイナーバージョンの間は更新を無視する」という意味で、次にマイナーバージョンが更新されるまではDependabotによる当該ライブラリに対するpull request起票がなくなります。

まあ、aws-sdk-goのパッチアープデートはほぼノールックマージでもいいとは思うし、実際場合によってはチラ見でマージしています。

まとめ

不便な点もあるが、なかった時代よりかは格段に便利なので助かってはいる。変なハック無しで公式で全部まかなえるようになって欲しい。

ただあまりアップデートが活発ではないような…?というところが気になっている。