Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

メルカリ Microservices Team による Terraform 運用とその中で開発したOSSの紹介

SRE で Microservices を推進している @b4b4r07 です。

メルカリでは全社 (US/UK/JP) 的に Microservices に舵を切る経営指針が打ち出されており、Microservices Platform Team では Microservices として切り出すにふさわしいサービスの再編のサポートや、新規サービスの Microservices 化のサポート、およびそのスタンダードなインフラ基盤の開発などをしています。

本記事ではその中で開発した Developer Productivity の向上につながる小さなツールを、メルカリでの Terraform の活用事例に交えてご紹介します。

メルカリでの Terraform 活用

冒頭に挙げたとおり、少しずついろいろなサービスが立ち上がり始めていますが、そのインフラとして主に GCP (GKE) が使われています。運用する GKE クラスタは1つですが、各種 Microservices が利用する GCP プロジェクトはサービスの数だけ作るようにしています。これは Microservices が掲げる独立性の意味も兼ねて、サービス単位で GCP のプロジェクトを分け、そこに各種サービスに必要な GCP リソース (Cloud SQL など) を作ってもらうようにしています。

これらの統一的な作成や必要なリソースセット (GCP プロジェクト、PagerDuty など) の作成を、今は Terraform を使って実現しています。

CI 経由で plan する

これらの作成や変更を適用するために、Microservices Platform Team では継続的な Terraform コマンドの実行環境として CI を利用しています。各サービスの開発者によって投げられたプルリクエストでは、変更された Microservices のみに対してブランチの段階で fmtplan を実行し、master にマージされたタイミングで apply を実行します。

このときレビューのタイミングで正しく plan が通ったか、どんな実行計画が結果として出たのかの確認のために CI のページに遷移する必要があるのですが、レビューすべきプルリクエストが増えるたびに毎回 CI ページにて確認しており手間になってきていました *1

そこで、plan 結果などは GitHub のコメントとして確認できたほうが圧倒的に便利だよね、ということで良い解決方法はないか探してみました。しかし、ニッチな需要なのか全然事例がみつからず (でも各社やっているはず...)、かろうじて「CI の結果を GitHub にコメントしている」という記事を見つけたとしても、「どうやって」もしくはどんなツールを使って GitHub や Slack などに通知させているのかがわからず、自分たちで解決する必要がでてきました。

tfnotify の紹介

そこでメルカリでは「Terraform コマンドの実行結果をパースし、任意のテンプレートに成形して GitHub や Slack などにコメントする」ツールを書きました (先行事例の紹介がなく自分たちが困ったこともあり、本ツールは OSS として公開することにしました)。

https://github.com/mercari/tfnotify

詳しい使い方などは README に載せていますが、本節ではメルカリでの利用シーンと合わせてご紹介します。

使い方

tfnotify は Go で書かれたバイナリになっており、Terraform の実行結果をパイプで繋いで実行されることを期待します。内部的に io.TeeReader を使っており、こうすることで通常通り CI コンソールの標準出力に流しつつ、tfnotify で実行結果を受け取ることができます。

以下のように実行することも可能ですが、こうしてしまうと Terraform が出す標準出力がすべてファイルに向いているので、CI のコンソールには何も流れません。

$ terraform plan > plan.log
$ cat plan.log | tfnotify plan

tee コマンドを使うと CI コンソールに流しつつ、ファイルにも出力できますが、一時ファイルを作るのも不要ではあるので、これは tfnofity 側で実装として組み込んでいます (tee でファイルへ枝分かれした出力の ANSI Color を取り除いたりする必要もあります)。

よって以下の1行で済みます。

$ terraform plan | tfnotify plan

これは CI ページで確認することが仮にあったときに、CI コンソールに何も出力されていないとそれはそれで困りそうだなというところから、このような実装になりました。

tfnotify がやっていることは以下の3点です。

  • Terraform の実行結果をパース
  • Go のテンプレートに落とし込む
  • 任意の通知先にポストする

通知先に指定できるのはいまのところ GitHub と Slack です。実行できる CI は Circle CI と Travis CI です。

GitHub への通知

tfnotify では YAML で簡単な設定を書くことができます。Terraform コマンドを実行するリポジトリに入れておけばよいです。$GITHUB_TOKEN などのトークンは直接ファイルに書く必要はありませんが、あらかじめ CI の Environment Variables から設定しておく必要があります。

---
ci: circleci
notifier:
  github:
    token: $GITHUB_TOKEN
    repository:
      owner: "mercari"
      name: "tfnotify"
terraform:
  plan:
    template: |
      {{ .Title }}
      {{ .Message }}
      {{if .Result}}
      <pre><code> {{ .Result }}
      </pre></code>
      {{end}}
      <details><summary>Details (Click me)</summary>
      <pre><code> {{ .Body }}
      </pre></code></details>

テンプレートでは Terraform の実行結果を任意の HTML に成形することができます。この例では plan 結果を以下のようなスタイルにして投稿することができます。

f:id:b4b4r07:20180402143632p:plain

各プレースホルダは以下の意味を持ちます。

プレースホルダ 意味
{{ .Title }} tfnotify plan の場合、デフォルトだと ## Plan result
{{ .Message }} --message オプションでコマンドラインから設定できる文字列
{{ .Result }} Plan: 1 to addNo changes などの実行結果の抽出部分
{{ .Body }} 標準入力で受け取る実行結果の全体

実行結果の全文は長くなりがちなので <details> を使って畳んでしまっています。こうすることで通知が何個もきてもコメント欄が見づらくならないようになっています。

また、同じタイトル {{ .Title }} と同じメッセージ {{ .Message }} を持つコメントが投稿された場合、古いものはデリートされて最新のコメントだけになるようになっています。これは何度コミットしてプッシュしても同じ通知結果でコメント欄が埋もれることを防いでいます。

Slack への通知

同様に slack: として Slack 用の notifier を設定すると、通知先を切り替えることができます。

f:id:b4b4r07:20180410154049p:plain

--
notifier:
  slack:
    token: $SLACK_TOKEN
    channel: C8L3AM8B0
    bot: tfnotify
terraform:
  plan:
    template: |
      {{ .Message }}
      {{if .Result}}
      ```
      {{ .Result }}
      ```
      {{end}}
      ```
      {{ .Body }}
      ```

--message flag で受け取る文字列がないと空になります。

メルカリ内での使われ方

今の段階では Microservices に関する中央集権的なリポジトリですべての Terraform コードを管理しています (ただし、Microservices ごとにディレクトリを分けており、state も Microservices ごとに管理しています。planapply も変更された Microservices のみに実行されます。中央集権的かつ分散管理をしています)。ここに上がってくるプルリクエストは Microservices Platform Team によるレビューが行われるのですが、古いブランチに対して terraform plan をしても期待した結果が得られないのでスキップさせています。同様に tfnotify による通知もスキップします (以下のスクリプトはイメージです)。

set -e

basedir="$1"
if is_there_no_change "$basedir"; then
    echo "[INFO] No changes in $basedir"
    exit 0
fi

if is_branch_behind_master; then
    echo "[INFO] Skipped because $current_branch is behind of master"
    exit 0
fi

echo "[INFO] Running 'terraform plan' for changed dirs"
for dir in $(changed_dirs "$basedir")
do
    ./script/terraform-plan "$dir" | tfnotify plan --message "$dir"
done

また、いわゆる Git の「先祖返り」状態を防ぐために GitHub でブランチに対して以下の設定を有効にしています。

f:id:b4b4r07:20180402143701p:plain

こうすることで正しい terraform plan の結果を受け取ることができます。

今後

現状、通知先は GitHub と Slack、動作環境は Circle CI と Travis CI だけですが、オープンソースにしたので今後は社内ユースにとどまらず幅広くかつ積極的にメンテナンスしていけたら良いなと思っています。

*1:先月の例だと1日に5つ以上のプルリクエストがレビュー・マージされています