Stanby Tech Blog

求人検索エンジン「スタンバイ」を運営するスタンバイの開発組織やエンジニアリングについて発信するブログです。

Taskfile で proto ファイルの生成を管理してみよう

こんにちは、スタンバイで求人の取り込みシステムを開発・運用をしている鈴木です。 今回は proto ファイルの定義からモデルファイルを作成する際に taskfile を利用してみたお話です。

背景

私たちは、求人取り込みのプロセスを役割に応じて分割したコンポーネントプロジェクトを連携したマイクロサービスで実現しています。 データストアに保存するデータ型の定義やコンポーネント間のインターフェースは、コンポーネント毎に同じものを定義してしまうと不具合の元になるため、プロトファイルとして定義して各コンポーネントで同じになるよう利用しています。 このプロトファイルの運用を改善していきました。

概要

今回の作業をする前の前提や経緯は次のとおりでした。

  • ver 1.0 Taskfile でコマンド化
    • モデルやコンポーネント間のIF定義をprotoファイルで定義し管理している
  • ver 2.0 バージョン差分の対応
    • それぞれのローカル環境で実行すると protoc のバージョンにより不要な差分が発生した
    • コンテナで生成することによりバージョン差分が発生しない運用にした
  • ver 3.0 プラグインの対応
    • コンポーネントごとに依存関係のあるファイルはそれぞれ異なる
    • 一部のコンポーネントのみ利用しているプラグインがある
    • コンポーネントごとの固有の定義を設定するタスクとそこから呼ばれる共通の生成タスクの構成に変更した
  • おまけ
    • タスク一覧が増えて可視性が悪くなったため peco で絞り込みと実行ができるようコマンドを作成

ver 1.0 Taskfile でコマンド化

proto ファイルの導入当初は作業者の端末にインストールされた protoc コマンドを直接実行して .pb.go ファイルを作成していました。 始めのうちは対象のファイル数も少なく、メンバーも同じタイミングで環境構築をしていたので特に問題は発生していませんでした。 ただ、それでも変更があった場合に手動で protoc コマンドを実行して再作成するのは手間で更新漏れの元になるため、 Taskfile でコンポーネントごとに一括して全体の更新もできるように作りました。

以下のように実行していたコマンドが

protoc -I=../proto --go_out=. modA.proto modB.proto modC.proto

このようになりました。

task proto:component1

変更があった場合に Taskfile の定義も更新する必要がありますが、生成対象が抜けてしまうリスクは多少下がりました。 一旦これで運用していたのですが、新メンバー(私)が参加したことで protoc コマンドのバージョン差異が発生してしまい、作成された .pb.go ファイル内のバージョン情報が差分として検出されてしまうようになりました。

ver 2.0 バージョン差分の対応

バージョン差分問題の対応として、端末にインストールされた protoc を直接実行するのではなく、 protoc が含まれた Docker イメージを作成して .pb.go の作成はそのイメージを利用する方針にしました。 それにあたりイメージを作成するための Dockerfile と起動するための docker-compose.yml を追加しました。

ファイルやディレクトリの構成はざっくりこのようになりました。

project_root
├── app
│   ├── component1
│   │   ├── modA.pb.go
│   │   ├── modB.pb.go
│   │   └── modC.pb.go
│   ├── component2
│   │   ├── modB.pb.go
│   │   └── modC.pb.go
│   └── component3
│       └── modC.pb.go
├── proto
│   ├── modA.proto
│   ├── modB.proto
│   ├── modC.proto
│   └── modD.proto
└── taskfile
    └── protoc
        ├── Dockerfile
        ├── docker-compose.yml
        └── Taskfile.yml

Taskfile.yml 内ではコンポーネント毎のタスクから .pb.go を生成する共通タスクを呼び出すような構成にしました。

version: 3

tasks:
  generate:
    summary: |
      パラメータを受け取って .pb.go の生成を行います
    dir: '{{.USER_WORKING_DIR}}'
    cmd: protoc -I=../proto --go_out=. ${PROTO_FILES}
  component1:
    desc: component1 に必要なパラメータを設定して generate を呼び出し .pb.go を生成します
    vars:
      # コンポーネントのディレクトリを指定します
      WORKING_DIR: /path/to/component1
      # 作成対象の proto ファイルのリストを指定します
      PROTO_FILES:
        - ./modA.proto
        - ./modB.proto
        - ./modC.proto
    # .pb.go ファイルを作成するコンテナで生成処理を実行します
    cmd: |
      docker compose run -T --rm -w {{.WORKING_DIR}} \
        -e PROTO_FILES="$(echo {{.PROTO_FILES}} | tr -d '[]')" \
        protoc task generate
  component2:
    desc: component2 に必要なパラメータを設定して generate を呼び出し .pb.go を生成します
    vars:
      WORKING_DIR: /path/to/component2
      PROTO_FILES:
        - ./modB.proto
        - ./modC.proto
    cmd: docker compose run -T --rm -w {{.WORKING_DIR}} -e PROTO_FILES="$(echo {{.PROTO_FILES}} | tr -d '[]')" protoc task generate
  component3:
    desc: component3 に必要なパラメータを設定して generate を呼び出し .pb.go を生成します
    vars:
      WORKING_DIR: /path/to/component3
      PROTO_FILES:
        - ./modC.proto
    cmd: docker compose run -T --rm -w {{.WORKING_DIR}} -e PROTO_FILES="$(echo {{.PROTO_FILES}} | tr -d '[]')" protoc task generate

タスクの概要です。

generate 環境変数で受け取ったファイルパスを元に .pg.go ファイルを生成します。

component1,2,3 各コンポーネント毎のタスクで依存関係のあるファイルを定義しておき、コンテナの実行時に環境変数として渡すようにしています。

これでバージョン情報が差分として検出されることがなくなりました。 ですが、追加でカスタムタグを埋め込みたいという要件が発生しました。

ver 3.0 プラグインの対応

機能の追加で .pb.go の定義に xml タグとの関連付けをする必要性が出てきました。 go で実装する場合は構造体の定義にタグを付けることでマッピングできますが、protoc の機能ではサポートされていませんでした。 そのため、protoc で .pb.go ファイルを作成した後に protoc-go-inject-tag のプラグインを使ってタグを埋め込むことにしました。

version: 3

tasks:
  generate:
    summary: |
      Generate .pb.go files from .proto files called by each component.
      environment variables:
        - INJECT_FILES: List of relative paths to .pb.go files that need to inject object tags
        - PROTO_FILES: List of .proto files that need to generate .pb.go files
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      # .pb.go を生成します
      - protoc -I=../proto --go_out=. ${PROTO_FILES}
      # 追加のタグが必要な場合にタグを追加します
      - cmd: |
          if [ -n "${INJECT_FILES}" ]; then
            IFS=' '
            for file in ${INJECT_FILES}; do
              protoc-go-inject-tag -input="${file}"
              gofmt -w "${file}"
            done
          fi
  component1:
    desc: component1 に必要な .pb.go を生成します
    vars:
      # コンポーネントのディレクトリを指定します
      WORKING_DIR: /path/to/component1
      # タグの追加が必要な .pb.go を指定します
      INJECT_FILES:
        - ./modA.pb.go
      # 作成対象の proto ファイルのリストを指定します
      PROTO_FILES:
        - ./modA.proto
        - ./modB.proto
        - ./modC.proto
    # .pb.go ファイルを作成するコンテナで生成処理を実行します
    cmd: |
      docker compose run -T --rm -w {{.WORKING_DIR}} \
        -e PROTO_FILES="$(echo {{.PROTO_FILES}} | tr -d '[]')" \
        -e INJECT_FILES="$(echo {{.INJECT_FILES}} | tr -d '[]')" \
        protoc task generate
  # component2: は修正なし
  # component3: は修正なし

これで必要に応じて .pg.go にカスタムタグを付与できるようになりました。 また、各コンポーネントのタスクはコンテナを呼び出す形式のため、 proto のパスなのか inject のパスなのか区別して渡すことができました。 当初はタスクから直接別のタスクを呼び出すやり方を調べていたため苦戦していました。

おまけ: peco で task コマンドのフィルタリング

Taskfile は便利な反面、定義が増えると必要なものが見つけにくくなります。 実際にメンバーからそういった意見が上がったため peco を使ってフィルタリングできるように検証をしています。

cmd=$(task -l | grep '*' | peco | awk '{print $2}' | sed "s/:$//")
if [ "$cmd" != "" ]; then
    task $cmd
fi

コマンドの内容は以下になります

task -l | grep '*' タスク一覧からヘッダを除外して取得しています。

| peco タスク一覧から1行を選択します。タスク定義や description に含まれる文字でフィルタリングができます。

| awk '{print $2}' | sed "s/:$//" task コマンドで実行できるようにタスク名を取得しています。

終わりに

proto ファイルの生成を taskfile にまとめることでコンポーネントごとの依存関係が明確になったり、作業者ごとの環境の差分を吸収できるようになりました。 手作業でやるには間違いが発生しそうな作業をまとめておくのに taskfile はとても便利ですね。 ただし、taskfile の修正、作成が職人芸にならないよう管理していく必要がありそうです。

スタンバイでは、常に新しいアイデアや技術を調査し、試しています。新しいことに挑戦したい方や、素晴らしいプラットフォームで素晴らしい仲間と仕事をしたい方は、ぜひ採用ページをご覧ください!

スタンバイのプロダクトや組織について詳しく知りたい方は、お気軽にご相談ください。 www.wantedly.com