データチームの@komi_edtr_1230です。
突然ですが、heyがメインで使ってるプログラミング言語は何か知っていますでしょうか? heyはECと決済、予約など複数事業の事業を展開しているのですが、ECと予約はRuby (+ Rails)で決済はJavaを使っています。 また、アカウント共通基盤ではGoを使っています。
今回データチームでは事業成績を日次でSlackに通知してくれるアプリをRustで開発しました。
この記事ではどのようにして開発を進めたのか、ツラいポイントはなんだったか、何が良かったかなどについてまとめます。
何を作ったのか
今回作ったSlackアプリはどのような要件を抱えていたかというと、
- 様々な項目についてのデータを取得
- Slackに投稿できるようJSONを整形
- 日次で稼働する(CRON Job)
というような具合でした。
現在heyのSlackでは、以下のように毎朝6時に前日の実績のサマリが投稿されるようになっています。
これ以外にもデータをもとに事業成績の日次推移を画像として出力する機能もあったりします。 この機能については後述します。
なぜRustを選んだの?
今回の要件として特徴的なのが、Slackで通知しなければいけないデータが非常に多いという点です。
具体的に、今のところ通知している内容としては前日と今月の流通額やトランザクション数、会員情報など多岐にわたります(これ以外にも何個も項目がある)
こうした各項目について都度SQLをBigQueryにPOSTして処理を行うという実装を行えばコードが冗長になるのは想像に難くないと思います。
また、Slack通知の内容はビジネス的制約によって条件が変更される可能性が出てくるわけです。
自分はこうした問題では型による振る舞いの共通化と制約が有効であると判断しました。
というのも、最初に型としてどのような振る舞いをして欲しいかを記述しておけばあとはそのガイドラインに則って実装を進めていけば良くなるので頭を使わなくて済むし、アスペクト指向プログラミング的に関心の分離を実現することもできます。
こうしたメリットは非常に有用です。 そもそもSlackアプリなんてそんなアクティブにメンテナンスするものでもないし、必要とあらばたまに触っていじるみたいなものですよね。 なので一度触って次にまた開発に着手するのは半年後なんてこともザラにあるわけです。
半年前に書いたコード、覚えていますか? 僕は覚えていません。 そうしたコードはもはや別人が実装したに等しいものです。
なので時間が経ってSlackアプリに改修を入れて欲しいなんて依頼が入ったときに、コードがどのように振る舞っているかを見るには型が重要なヒントになると思います。 もちろんドキュメントやディレクトリ構成、変数命名なども超重要なんですけどね。
ということで型がしっかりした言語でありコードが冗長になりにくい、そうした点でRustを選定しました。
実装のポイント
以下では実装の際に気をつけたことを色々まとめていきます。
CI/CDではキャッシュをフル活用
Rustのビルドが遅いというのはよく聞く話です。
ですが、そのビルドが遅いのは初回だけで、2回目以降はIncremental Buildが有効になるのもあってかなり速いです。 逆に、初回はクレートを外部から持ってきて色々するのでそりゃ時間もかかるわなという感想ですね。
で、コードをリポジトリにPushした際に毎回テストを回すにもビルドして云々して、というのはかなり時間がかかります。 大した量のコードを書いてないのにテストが回り切るまで5分待たなきゃいけないというのは我慢なりません。
そこでキャッシュを利用します。 GitHub Actionsだとcacheというのがあり、これを利用します。
これをやるだけでcargo build
が一瞬で終わります。
このキャッシュ機構はDockerのビルドにも有効で、キャッシュを効かせるためにDockerのステップを意図的に外部クレートのビルドとアプリ本体のビルドで分けておくとDockerのビルドもかなり速くなります。
FROM rust:1.54.0 RUN cargo new --lib gmv-notifier RUN mkdir /work RUN mv gmv-notifier/Cargo.toml gmv-notifier/src/ /work COPY Cargo.toml /work/ WORKDIR /work RUN cargo build --release # Set project file COPY build.rs /work/ COPY src/ /work/src COPY sql/ /work/sql COPY config/ /work/config WORKDIR /work RUN ls src/lib.rs RUN cargo build --release CMD cargo run --release
このファイルでは最初にCargo.toml
とsrc/lib.rs
だけセットしてビルドするだけで外部クレートがビルドされるようにしています。
今回はこのようにしてビルドを高速化しましたが、他にもcargo-chefというのを使って高速化する方法もあります。
Cargoを利用した複数の実行バイナリの生成
Rustでは以下のようなディレクトリ構成の時に複数の実行バイナリを持つことができます。
├── Cargo.toml └── src ├── hoge.rs └── lib.rs └── bin └── foo.rs └── bar.rs
実行の際に以下のようにすることで異なるバイナリが実行できます。
$ cargo run --bin foo # => fooが実行される $ cargo run --bin bar # => barが実行される
今回実装したSlackアプリでは、PMが日次で実績が欲しいという要望がある一方で別のPMから新規顧客リストが欲しいという要望もあり、これらは投稿するチャンネルや時間が異なるということもあってバイナリを分けることによって対応しました。
Cargoのビルド副作用機構を利用したディレクトリの作成
Cargoではbuild.rs
というファイルを用意しておくとビルド時に副作用としてbuild.rsの内容を実行してくれます。
今回のSlackアプリではデータをもとにグラフを描画して画像に保存、Slackに投稿するという要件があり、その画像を一時的に保存しておくディレクトリを作成するようにしました。
use std::fs; use std::path::Path; fn main() -> std::io::Result<()> { if !Path::new("./images").exists() { fs::create_dir("./images")?; } Ok(()) }
ちょっと大変だったポイント
基本的にRustでの開発体験はすごく良くて楽しいものなのですが、今回の実装でちょっと詰まったポイントがいくつかありました。
内容としてどんなものだったかというと、もうこれは全エンジニアがぶつかって困る事象なんじゃないかと思うのですが、OSSにバグがあったのです。
今回ぶつかったバグは全部自分自身で本家にBug FixのPRを出して解決したのですが、それらについていくつか紹介していきます。
BigQueryクライアントでの大きな整数に対してパースエラー
BigQueryでは大きな整数は3.13E10
のように指数表記になります。
この指数表記は浮動小数点でも整数でもなります。
一方で、Rustのネイティブのパーサーだと指数表記のものは浮動小数点としてならパースが可能で、整数型としてパースしようとするとエラーを吐きます。
なので今回の実装の中で、大きな整数を扱うケースではパース可能であるべきなのにパースできないというバグがありました。
これは以下のPRで解決しました。
GKE Autopilotでの認証
今回のSlackアプリは日次で実行される必要があり、データチームが保有するKubernetes環境でCronJobで行うことにしました。
データチームのKubernetesはGKE Autopilotを利用していて、サービスアカウントの認証方法としてGKE AutopilotではWorkload Identityがデフォルトとなっています。
通常の認証とWorkload Identityの違いは何かというと、通常の認証ではサービスアカウントのJSONファイル(=秘密鍵)が利用するかどうかです。
JSONファイルを持ち回すのはあまりセキュリティ的に良くないため、Workload Identityを利用することによってKubernetesの特定の名前空間にデフォルトでサービスアカウントが認証されているようにできます。
ただ、この違いはGCPのAPIを叩く際のアクセストークンの払い出し方法に影響があり、具体的な話は少し長くなるのでまた別の記事にまわそうと思います。
ひとまずWorkload Identity環境でも認証がうまくいくようにPRを出して解決しました。
画像生成の際にx軸がDateTime型だと描画がおかしくなる
Rustのグラフ描画ライブラリにはplottersがあります。
PythonでいうMatplotlibのように何かしらのデータを入れるとグラフを描画してくれるすごいクレートなのですが、グラフのx軸にDateTime型のVecを突っ込むとそのデータが一週間以内に完結しているという特定の条件下で描画が壊れるというバグを見つけました。
これは内部的に日付の配列をグラフにおける開始位置と終端位置にパースする中で週のカウントのロジックがおかしいのが原因で、ここらへんは自分もかつてCommon Lispで同様のグラフ描画ライブラリを開発した経験があり。ある程度ロジックの修正ができそうだったので簡単にPRを出しました。
まとめ
今回Rustを利用してSlackアプリを実装しました。
非常にエキサイティングな体験をしつつ、将来的にも負債となりにくそうな開発ができたので個人的には満足しています。
あとOSSのバグはツラいですね。 ただ僕らもOSSにはとてつもなくお世話になってるはずなので、何か困ったポイントがあったらツイッターで嘆くのではなくちゃんと開発元にPRを出して直してあげましょう。
heyでは(まだ)Rustを本格採用しているわけではないのですが、あらゆる手段を用いて問題解決ができるクールなエンジニアを募集しています。
また、僕らデータチームもデータエンジニアとデータアナリストを募集しているので、ツイッター等で気軽に声をかけていただければカジュアル面談等セッティングしますのでよろしくお願いします。