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

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のパッチアープデートはほぼノールックマージでもいいとは思うし、実際場合によってはチラ見でマージしています。

まとめ

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

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

smartcache ~ プリフェッチするインメモリキャッシュ

https://github.com/Songmu/smartcache

smartcacheというGoのインメモリキャッシュライブラリを書いた。

一般的に、キャッシュを実装する場合以下のような問題が起こりがちです。

smartcacheは上記の問題を以下のアプローチで解決しています。

使い方

コンストラクタにキャッシュの実際の有効期限、softな有効期限、そしてキャッシュ生成関数を渡します。

// コンストラクタ
ca := smartcache.New(
    10*time.Minute, // 実際のexpire
    time.Minute,    // soft expire
    func(ctx context.Context) (interface{}, error) {
        var val = 1
        return val, nil
    })
// キャッシュ取得
val, err := ca.Get(context.Background())

キャッシュ取得時に、soft expireを過ぎていた場合には既存のキャッシュを返しつつ、内部的に非同期でキャッシュの更新処理をおこないます。

このライブラリの特性上、あまり起きてほしくありませんが、実際の有効期限も切れていた場合には同期的にキャッシュの更新をおこない、その値を返します。これは一般的なキャッシュライブラリ同様の挙動です。soft expireに0を指定した場合は常にこの挙動となりますが、この挙動を利用することは少ないでしょう。

細かい挙動の話をすると、New時点ではキャッシュの生成はおこなわれず、初回のGet時におこなわれます。なので、キャッシュ生成に時間がかかるものを扱う場合、事前に一回Getを叩いておいてもよいでしょう。

このように、smartcacheはほぼフレッシュな値を即時に手に入れたい場合に重宝するはずです。ぜひご利用ください。

余談

こういう処理をちゃんと書くのは地味にめんどいしバグりやすいのでライブラリとしてまとめました。実際最初に書いたやつはテスト書いたらバグっていた…。既存のライブラリがありそうに思うのですが、上手く見つけられなかったので書いてみました。より良いやつがあれば教えて下さい。

このライブラリはKVSのようにキー文字列を指定するような作りにはしていません。不特定な値よりかは、ある程度温め続けて欲しい特定の値を扱うため、失効処理などは一旦必要ないと考えたからです。ただ、なにか良い感じのインターフェースがあればご提案くださると嬉しいです。

汎用キャッシュライブラリなので値が interface{} になってしまうのはちょっとダサい感じはある。この辺りは僕の設計力の問題もありますが、ジェネリクス的なものが欲しくなってしまう部分ではありますね。

ということで、結構特定用途に寄ったライブラリなので、smartcacheと言ってしまうのは名前負け感がちょっとあるのですが、その辺りはあまり良い名前が思いつかなかった…。

ecs-deployからecspressoに乗り換えた

のがもはや半年前だけど記録として書いておく。結論を書くと、ecs-deployからecspressoに乗り換えるのはすぐできるし、タスク定義が管理しやすくなるのでおすすめです。

https://github.com/kayac/ecspresso

もともとNature社では僕が入社する前からecs-deployが使われていた。これは、コンテナイメージをすげ替えてdeployするだけであればシンプルでわかりやすい。ただ、以下のような課題があった。

それに対して、ecspressoは以下のような利点があった。

タスク定義をリポジトリ管理できるようになったのが一番大きなメリットで、サイドカーの追加やパラメーターの変更などがpull request上でレビューできるようになった。

deployのフロー

社内のdeployはCircleCIから実行している。masterブランチが進むと勝手にdeployされるようになっている。これをecs-deployからecspressoを使うようにした。ecspressoのバイナリはかっこ悪いがリポジトリに突っ込んでいる。今だとOrbがあるのでそれを使うのが良さそう。

https://circleci.com/orbs/registry/orb/fujiwara/ecspresso

ちなみに、ecspressoのデフォルト挙動ではサービスが更新されてdeployの完了を待つ挙動になっているが、我々のユースケースだと、場合によってはいくつかのサービスを同時にdeployする関係上、そこでブロックして欲しくなかった。

なので、サービス更新がかかったらすぐに抜ける --no-wait オプションを追加するpull requestを送って取り込んでもらった。こっちのほうがecs-deployの挙動にも近いので乗り換える場合はおすすめです。ちなみに、--no-wait で一旦抜けた際にサービス更新完了を待ち受けたい場合は ecspresso wait コマンドを実行することでサービスの更新完了まで待機することができます。

以前git-pr-releaseのエントリに書いたように、Nature社では、featureブランチがdevelopブランチにマージされたら、developからmasterへのpull requestが自動で作られるようになっており、そのマージボタンを押したらさながらワンクリップデプロイの要領で本番に反映されるので体験が良い。

設定ファイル類の配置や課題など

設定類は今の所リポジトリの conf/ecspresso/ 配下に諸々配置している。例えば以下のような具合。

$ tree conf/ecspresso
conf/ecspresso
├── production-api.yaml
├── production-worker.yaml
├── production-consul.yaml
├── production-datadog-agent.yaml
├── production-nginx.yaml
├── taskdef-api.json
├── taskdef-worker.json
├── taskdef-consul.json
├── taskdef-datadog-agent.json
└── taskdef-nginx.json

YAMLが設定ファイルで{env}-{service}.yaml、JSONがタスク定義でtaskdef-{family}.jsonという命名規則にしている。サービス定義は配置していないが、管理したくなったら配置するかも知れない。

YAMLの設定ファイルがアプリケーション及びデプロイの単位となるが、一つのリポジトリの中に複数のアプリケーションがあるのはTwelve-Factor App的には良いことではないが、サブコンポーネント的なちょっとしたアプリケーションのためだけにリポジトリを分けるのも大げさなのでこのように管理している。

上記のAPIとWorkerについてはmasterブランチが進んだら愚直に両方deployされるようにしていて、その他のサブコンポーネントの類は一部手元からecspressoコマンドを実行する形でdeployしている。

手元のコマンド実行でdeployするのはあまり良いことではないので、masterブランチが進んだ時に更新が必要なdeployを洗い出して自動的に出ていくようにするのが良いと考えていて、そのあたりはBazel的なやつを入れるのが良いのかなーとか考えたりしている。ということでモノレポ指向になっていくかもしれない。

Goでテスト中に現在時刻を差し替えたりするflextimeというのを作った

https://github.com/Songmu/flextime

flextimeはテストコードの中で現在時刻を切り替えるためのライブラリです。Sleep時に実際に時間を止めずに時間が経過したように見せかける機能もあります。

つまり、PerlのTest::MockTimeやRubyのtimecop的なことをしたいわけですが、Goだとグローバルに関数の挙動を切り替えるといったことはできないため、利用にあたってはtimeパッケージで使っている関数を、flextimeパッケージに切り替える必要があります。

具体的には、flextimeはtimeパッケージと同様のインターフェースを備える以下の9つの関数を提供しています。

now := flextime.Now()
flextime.Sleep()
d := flextime.Until(date)
d := flextime.Since(date)
<-flextime.After(5*time.Second)
flextime.AfterFunc(5*time.Second, func() { fmt.Println("Done") })
timer := flextime.NewTimer(10*time.Second)
ticker := flextime.NewTicker(10*time.Second)
ch := flextime.Tick(3*time.Second)

これらはデフォルトでは標準timeパッケージと同様の動作をするため、単純に置き換え可能です。ちょっと乱暴ですが、以下のような単純置換でも大体動くでしょう。本来静的解析でやるべきですが…。

go get github.com/Songmu/flextime
find . -name '*.go' | xargs perl -i -pe 's/\btime\.((?:N(?:ewTi(?:ck|m)er|ow)|After(?:Func)?|Sleep|Until|Tick))/flextime.$1/g'
goimport -w .

使い方

SetFix関数を使うことで上記の9つの関数の挙動を差し替えます。Restoreでもとに戻します。

heisei := time.Date(1989, time.January, 8, 0, 0, 0, 0, time.Local)
flextime.Set(time.Date(heisei) // 時刻をセットする
now := flextime.Now() // 現在時刻がセットした時間(heisei)になる
// ...
flextime.Restore() // 元の挙動に戻す

SetFixの違いは、Setは実際の時間経過の影響を受けますが、Fixは完全に固定されるところです。

仕組み

内部的に時刻を返すオブジェクト(内部clock)を差し替えることで実現しています。それは、上の9つの関数を備えるClock interfaceを満たしたオブジェクトです。flextime.Nowなどの関数はそのオブジェクトに処理を委譲している形になります。

type Clock interface {
    Now() time.Time
    Sleep(d time.Duration)
    Since(t time.Time) time.Duration
    Until(t time.Time) time.Duration
    After(d time.Duration) <-chan time.Time
    AfterFunc(d time.Duration, f func()) *Timer
    NewTimer(d time.Duration) *Timer
    NewTicker(d time.Duration) *Ticker
    Tick(d time.Duration) <-chan time.Time
}

このClock interafaceというアプローチは以下の様な先行実装を参考にしました。

そのinterfaceをより標準timeパッケージに近づけつつ、内部的に差し替えるアプローチにより、easyに使えるようにしたのが、このflextimeです。

おまけ

組み込みのSet, Fixだけではなく、独自のClockインターフェースを備えたオブジェクトを作って、それに内部clockを差し替えることもできます。

flextime.Switch(clock)

また、Clock インターフェースの関数群は、NowSleepの2つさえあれば、他の関数を実現することができます。これを NowSleeper インターフェースとして定義しており、これを満たすだけで、以下のように独自Clockを簡単に作ることができます。

var ns flextime.NowSleeper
var clock Clock = flextime.NewFakeClock(ns)
flextime.Swtich(clock)

なかなかflexibleと言えるのではないでしょうか。名前のflextimeも気に入っています。

testabilityの向上に有用ではないでしょうか。ぜひ使ってみてください。

ちなみに、弊社はフレックスタイム制を採用しています。採用にご興味があればぜひご応募ください。

https://nature.global/jp/careers