Docker image を導入して protobuf を使う

こんにちは、会計freeeの開発をしている清水です。2020年4月に新卒としてfreeeに入りもうすぐ一年が経とうとしています。

この記事では私の所属チームのプロジェクトで試している、protobuf のための開発環境について書きます。

背景

現在私たちのチームでは、会計freeeのバックエンドのアーキテクチャ移行を進めています。会計freeeは長年にわたって開発が続けられているRailsアプリケーションです。現在でも社内の多くのチームのエンジニアが開発に関わっています。そんな中で実装は複雑化し、機能ごとの依存関係が分かりづらい状態になっていました。新しいアーキテクチャの目的は、そんなアプリケーションの実装の依存関係を明確にし、より安心して開発を継続できるようにすることです。

このあたりのより詳しい話は以前id:mihyaeru21が発表した以下の資料にまとまっています。

speakerdeck.com

現在移行を行っている範囲では、アーキテクチャ移行の第一段階として API Model と呼ばれる新たなインターフェースを導入しました。これは ActiveRecord に依存しないデータ構造で、バックエンドの実装内部において、モジュール同士は基本的にこのAPI Modelを介してのみやり取りを行うような設計になります。

現在のところ API Model の実装には、protobuf を採用しています。型が定義できること、また将来的に機能をマイクロサービスとして切り出す際もインターフェースとしてgRPCでそのまま流用できることがメリットです。

自動生成のためのコンテナ

API Model には protobuf を利用するため、Rails アプリケーションで実行される Ruby コードは、proto ファイルの定義から自動生成します。また読みやすい HTML 形式のドキュメントも、protoc のプラグインである protoc-gen-doc を使って、コードと一緒に定義から自動生成します。これにより常に実際に動くコードに連動してドキュメントが更新されるようになります。

例えば入力となる proto ファイルの定義と、自動生成される Ruby のコードと HTML のドキュメントは以下のような対応関係になります。

syntax = "proto3";

// コメント1
message Hoge {
  int64  number = 1; // コメント2
  string code   = 2; // コメント3
}
...
Google::Protobuf::DescriptorPool.generated_pool.build do
  add_file("sample.proto", :syntax => :proto3) do
    add_message "Hoge" do
      optional :number, :int64, 1
      optional :code, :string, 2
    end
  end
end

Hoge = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Hoge").msgclass

protoファイルから生成されるドキュメント
生成されるドキュメント(protoファイルのコメントも反映される)

開発にあたっては、エンジニアごとのローカル環境に依存して生成物に差が生まれないようにする仕組みを用意する必要がありました。そこで自動生成のためのツールのバージョンや、実行環境を固定するためにDocker imageを利用し、Amazon Elastic Container Registry (ECR) を介してそれを開発者間で共有するようにしています。

Dockerfile はだいたい以下のような感じになり、build 時に使用する protocと proto-gen-doc のバージョンを指定します。

FROM debian:buster-slim

ARG PROTOC_VERSION
ARG PROTOC_GEN_DOC_VERSION

ADD https://github.com/google/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip ./
ADD https://github.com/pseudomuto/protoc-gen-doc/releases/download/v${PROTOC_GEN_DOC_VERSION}/protoc-gen-doc-${PROTOC_GEN_DOC_VERSION}.linux-amd64.go1.15.2.tar.gz ./
RUN apt-get -q -y update && \
  apt-get -q -y install unzip && \
  unzip protoc-${PROTOC_VERSION}-linux-x86_64.zip -d ./usr/local && \
  tar xvzf protoc-gen-doc-${PROTOC_GEN_DOC_VERSION}.linux-amd64.go1.15.2.tar.gz && \
  cp protoc-gen-doc-${PROTOC_GEN_DOC_VERSION}.linux-amd64.go1.15.2/protoc-gen-doc ./usr/local/bin && \
  ...

# 生成物を手元に得るためのマウントポイント
VOLUME ["/ruby", "/docs", "/proto"]

ENTRYPOINT [ "protoc" ]

手元に落としてきた Docker イメージからコンテナを起動し、ローカルの proto ファイルから、コンテナ側でコードとドキュメントを生成します。最後にこの流れ実行できる Rake タスクを用意すれば開発環境の準備は完了です。

以上で開発者の環境による差分を気にすることなく、インターフェースの定義にだけに集中して開発が進められるようになりました。