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

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

二十四節気スプリントシステムのススメ

tl;dr

二十四節気スプリントシステムとは

今年から、Nature社でもスクラムっぽくスプリントを回し始めたのですが、前職のチームでも採用していた二十四節気スプリントシステムを導入しました。

二十四節気スプリントシステムというと大げさですが、これは単に、各2週間スプリントの名前に二十四節気を割り当てるものです。二十四節気のWikipediaのリンクを以下に載せておきますが、最近はGoogle検索の結果にも出てくるので驚きです。

二十四節気 - Wikipedia

小寒

ちなみに、これは、Mackerelチームでスクラムを採用し始めた頃に、当時のスクラムマスターであったid:motemenが発案したものです。

参考: はてなMackerelチームの開発フロー(スクラム、リモート)について話しました

これが地味に便利なのでこのエントリをしたためています。

二十四節気スプリントシステムのメリット

スプリントに適切な名前がついていると、会話のときに「〇月〇日から〇月〇日までのスプリント」とか言わずに済みますが、その名前がほぼ自動で決まるので便利です。例えば、「次の大寒スプリントでは云々」みたいな話ができる。

ちなみに今は小寒スプリントです。

季節感があるのも良くて、振り返りのときとかに「次は立春スプリントです!」とか言うと「オオッ」っと何故か妙に盛り上がったりします。ちなみに、立春スプリントは2月の頭から始まります。旧暦ベースなので季節の移り変わりが一足先に感じられるのも、若干前倒しの意識で事を進められる雰囲気が出るので地味に気に入っているポイントです。

あまり聞き馴染みのない節気のほうが逆に記憶に残るというのもあり、例えば芒種は、Mackerelチームが2014年に実際にスプリントを回し始めた最初の節気だったらしく、それが当初メンバーの記憶に残っており、節気が一周したときの振り返りで「そう言えば芒種って聞き覚えがある」みたいな話が出て、スクラム1周年に気づくことができてめでたい気持ちになった、というのがあったりしました。

ズレへの対処

二十四節気は一年を24分割するわけですが、これを2週間で単純に回すと48週間なので、一年間で4週間ずれてしまいます。適宜調整すればよいのですが、その辺りも年末年始とゴールデンウィークとシルバーウィーク辺りで結構吸収できます。

地味なポイントとしては「年始は小寒で始める」ようには調整したほうが良くて、そうすれば「2020小寒スプリント」のように、年をまたいでもスプリント名のユニーク性をを担保できます。

まとめ

ということで、二十四節気スプリントシステムの紹介でした。2週間スプリントを敷いている場合、ぜひ導入してみてください。

弊社は今後外国人が開発チームに加わる可能性もありますが、その場合でも二十四節気の趣を感じて貰えればよいかと思っています。このように開発フローの改善にも取り組んでいる昨今ですが、エンジニアを絶賛募集中ですので、ご興味ありましたら採用にご応募ください!

https://nature.global/jp/careers

ghq v1リリースとghq-handbookのお知らせ

https://github.com/motemen/ghq/releases/tag/v1.0.0

年末にアナウンスしていた通り、先程ghq v1.0.0をリリースしました。変更点は以下のエントリでお知らせしていたとおりです。その他Subversion周りの対応を無駄に頑張って強化したりしました。

https://songmu.jp/riji/entry/2019-12-28-ghq.html

是非ご利用ください。

ghq-handbookのお知らせ

年末年始休暇中にドキュメントを書いていたのですが、思ったよりもしっかりとした分量になったので、思い立って電子書籍にして販売してみることにしました。

https://leanpub.com/ghq-handbook

日本語で20ページほどです。値段は$1.99くらいにしたかったのですが、Leanpubで収益を上げる場合には$4.99が下限のようなので、その額に設定させてもらいました。

この本は、ソースのMarkdownを以下のリポジトリで公開しているため、無料で読むことも可能です。pull requestももちろん歓迎です。

https://github.com/Songmu/ghq-handbook

ghqの使い方を網羅した必読の一冊となっております。「こう使ってほしい」が書かれているので、ghqユーザー全員に読んでほしいと思っています。

現状、Leanpubの無料プランを使っており、デザインなども簡素です。この辺りも収益を元にブラッシュアップしたいと思っています。具体的には以下のようなものに投資したい。是非購入をご検討ください。

もちろん、この辺りの項目について協力してくださる方も大歓迎です。

今後ともghqを便利にご利用ください!

2019 -> 2020

2019年は転職がやはり一番大きな転機。Nature Remo Eをなんとか年内にリリースできたのでかろうじて及第点というところ。

Nature Remo Eはこれから電力事業に舵を切る上で非常に大事で、野心的な商品です。会社として非常に面白いフェーズなので興味があれば採用にご応募ください。

https://nature.global/jp/careers

去年は一瞬で過ぎた。ライフイベント含めて色々あった。近年は、毎年去年より激動だな、と思っているのでこれは今年もきっとそうなるのだろう。

個人事業主やISUCONの話はBlogに書けてない。追って書くかもしれない。

何かを成し遂げたい

これだけ書くと具体性がなくすごくバカっぽい。いや、僕がこういう「ビッグになりたい」みたいなノリを馬鹿にしていたし、今でもどこかで馬鹿にしているからそう感じるのだろう。

去年末に、Findyに寄稿させてもらい、それを書く中で、改めて自分がこれまで何も成し遂げてないことに気がついた。

https://engineer-lab.findy-code.io/neet-to-cto

回り道をした結果、今更そういうことを思うようになった。そういう事が多い人生である。それもまた良しだとも思っているのだが。

これから2年を目処に何かを成し遂げようと思う。今の会社Natureがそのステージになる。Natureをどうにかしないといけない。Natureは順調だし、だからこそしくじることはできない。別に僕の力がなくても成功するだろう。だからこそだ。

Mackerelがそのステージになるのかと思っていた。それなりにやった感じもあるが、道半ば感は否めず「成し遂げた」と感じられるレベルには至らなかった。

中途半端な人生を送ってきたことが自分のしこりになっているのだ、ということにも気がついた。

小学校のときに日能研の全国模試で2位になったことが2回あるが、トップにはなれずじまいだった。その時は、ろくに勉強もしないでその成績が出せる自分に満足していた。ただ、あのとき本気を出していれば、トップを取れたんじゃないかと今からすると思う。結局あの時が一種のピークで、その後何かのトップになれたことも無い。

その後の人生でそれなりに真面目に取り組んできたことが、全てそこそこ止まり。チェスもボウリングも自転車ロードレースもサイキックフォース2012も中国語もプログラミングも。一流になりきれない。

それで良いと思ってそれなりに満足して生きていたんだけど、ここに来て欲が出てきた。半端者でも良いから、今の会社で全方位に何でもやっていこうと思う。開発以外にもやらないといけないこと、やりたいこともたくさんある。楽しみである。

やることが多くて迷っている時間が多いとも自覚しているので、そこは改善していきたいし、もっとやれるはずだとも常々思っている。

経営者としてレベル1のペーペーだというところが大きな課題なので、そこは精進していく。勉強したり、他者の経営者とお話させてもらったりというところを増やしていきたい。