Rubyの型チェッカーのSorbetを導入しました

申告チームでテックリードをやらせてもらっている id:nanjakkun です。

freeeではfreee会計をはじめ多くのプロダクトがRuby on Rails(以下Rails)のアプリケーションとして実装されています。

日々の開発の中で、Rubyでも静的な型の解決ができればなあと思うことがあります。

ということで、Rubyの型チェッカーのSorbetをfreee申告に導入してみました。

Sorbet(ソルベ)とは

sorbet.org

決済代行サービスのStripeを運営しているStripe社が公開している漸進的型チェッカーです。

※余談ですがfreeeアプリストアではStripeを決済手段としたアプリの有料販売ができます。

developers.freee.co.jp

有料アプリ販売の準備をする | freee Developers Community

漸進的型付けとは

漸進的型付け (Gradual Typing) とは動的型付けの言語に後から型宣言を追加することです。

今回取り上げるRubyの他には、Python, TypeScript, Flow などで漸進的型付けが可能です。

Sorbetでできること/できないこと

Union Types

Union TypesはT.anyでできる。

T.any(Integer,String)
nilable

nilの可能性のある型。 他の言語だとMaybeとかOptionalとか呼ばれているものですね。

T.nilable(Integer)

みたいに表現する。

ジェネリクス

ジェネリクスはHashやArrayなどの組み込みクラスに使えます。 https://sorbet.org/docs/stdlib-generics

自作クラスにもジェネリクスが使えるらしいんですが現時点では公式ドキュメントには書いていませんでした。

ソースを見ると自作クラスにもジェネリクスを使えるようです。

型境界の指定や、共変、反変の指定もできるようです。

変位指定

ドキュメントは見つけられなかったんですが、試した限りだと Sorbetのメソッドは引数が反変、戻り値が共変だと思います。 Flowなんかと同様。

ちなみにTypeScriptは関数の引数が双変、戻り値が共変。 で、ちょっと型が健全でなくて実行時エラーになるケースがあります。

Flowは関数の引数が反変、戻り値が共変。

参考: Scalaの変位指定

変位指定 | Scala Documentation

Structual Typing

Structual Typing(日本語だと構造的型付け?)を型安全に行うことはできません。 要はダックタイピングを型安全に行うことはできない。

Nominal Typingだけです。

FAQにも書いてあります。

Frequently Asked Questions · Sorbet

No. You can use an interface instead, or T.untyped if you do not control all of the code.

やりたければinterfaceという機能を使うか、T.untypedにして型安全を捨てるかしろと書いてあります。

運用状況

本番環境で運用するまでの流れ

本番環境で運用を開始するまでに以下のような流れを取りました。

  • 新機能のfeature ブランチにSorbetを導入する
  • Rubyのコードではほぼ新機能のModule以下にだけSorbetの型注釈を付ける
  • 社内の試験環境にデプロイして、数週間、新機能のQAをしたり、社内のフィードバックを受けたりする
    • 新機能は一気に試験環境にデプロイするのではなく、出来た分からデプロイしてスクラムのスプリントレビューなどでお披露目して社内で触ってもらう機会を増やす
    • このときSorbetの機能のためだけのQAは行っていない
  • feature ブランチをmasterブランチにmergeして本番環境にデプロイ
  • 本番環境で動く!

このような過程を取った理由としては以下の点が挙げられます

  • 長めに本番環境に近い状態で動作させて問題発生が起きないか様子を見たい
  • 解決できない問題が発生したときに切り戻ししやすい
  • 本番環境で既存のコードに影響を与えて不具合が起こる可能性を減らしたい
  • 新機能はすぐにトラフィックが沢山来るわけではないので不具合があった場合の被害を減らせる

本番環境で動かしてみてどうか

今の所クラッシュしたり不可解な実行時エラーが起きるのは見たことはありません。

試験環境でエラーは見たことはあって、nilableでないはずの箇所にnilがやってきたもので、

それもそもそもnilが来ないつもりで、UIの制御が足りなくて来ないはずのところに処理が来た、ってところで実行時エラーになっても問題のないケースです。

開発時のこと

良かったこと

型チェックが静的/動的に走る

当たり前なんですが、メソッド呼び出し時の型の不一致を静的/動的にチェックしてくれる。

それと意外にtypoとか、未定義変数とかスコープおかしいとかもかなり検知してくれるのが地味に嬉しい。

ドキュメントとしての意味 / YARDとの関係

Sorbetでメソッドに型注釈を付けると他人がコードを見たときに、 「どんな型の引数を受け取ってどんな型の戻り値を返すか」 というのが分かるのは副次的な効果としてあると思います。

Sorbetの型注釈をつけてさらにYARDを書くのは流石にメソッド本体じゃない部分が多すぎる上に冗長なのでやらなくて良いんじゃないかと思います。

ActiveRecordとSorbet

github.com

Modelの型付けにはsorbet-railsというgemを使っています。

これを使うとModelファイルからrbiファイルを作ってくれます。

ただし、全てのModelにこのgemの機能を適用せずに、一部だけ適用した場合、

rbiファイルがあるものからないものへの参照がある部分で型エラーになることがあって、

そこは手動でコメントアウトする必要があります。

あと、sorbet-railsにはControllerのparamsに型を付ける機能もあるんですが、これはまだ使っていません。

https://github.com/chanzuckerberg/sorbet-rails から引用

class MyCoolController < ApplicationController
  class MyActionParams < T::Struct
    const :id, T.nilable(Integer)
    const :show, T.nilable(T::Boolean)
    const :wands, T::Array[Integer]
  end
  sig { void }
  def my_action
    typed_params = TypedParams[MyActionParams].new.extract!(params)
    # T.reveal_type(typed_params) => MyActionParams
    # T.reveal_type(typed_params.show) => T.nilable(T::Boolean)
  end
end

他サービスのAPIを叩く部分はどう書くべきか?

社内外の他サービスのAPIからのレスポンスをjsonで取得するとしてそれをJSON.parseしたものを そのまま扱おうとすると型がHashになります。

Hashだと、このkeyに対するvalueはこの型で・・みたいなことができず、 valueは全て同じ型になってしまいます。

とすると型システムの旨味が少なくなってしまいます。

なのでPORO(Plain Old Ruby Object)かOpenStructにマッピングして それぞれのvalueについて型を指定できる方が良いんじゃないかと思っていて、 実際には今の所POROにマッピングしています。

運用コスト

通常時の型付け

rbiに手を加えずにRubyファイルに型をつけるだけならあまりみんな困ってないんじゃないかなと思います。

ファイルごとtyped: falseにしてチェックの対象から外したり型をT.untypedにして問題を先送りにすることもできますし。 漸進的型付けの強みですね。

FlowとかTypeScriptも殆どの部分は割と雰囲気で書けるでしょう?

ライブラリのrbiファイルのメンテ

gemのinstall/updateの際はsorbet/rbi以下のファイルのメンテが必要になります。

sorbet-typedに定義が書かれているgemは良いのですが、そうでない場合は自分でrbiファイルを作る必要があります。

特に社内gemだと一番最初にSorbetを導入している都合上、ほぼ必ずrbiを作らないといけません。

あと、将来的にはRails updateのときなんかにも必要になると予想されます。

困ったこと / 不便だと感じること

初期化でコケる!
srb init

を実行してしばらくすると謎のエラーが出て止まりました(ちょっと前の話で、ログを残してなくてすいません)。

なので途中で落ちたところから自力でSorbetが動くところまで持っていきました。

https://github.com/sorbet/sorbet-typed

まずgemに対するrbiファイルをsorbet-typedから/sorbet/rbi/sorbet-typedから必要な分をコピーして持ってきます。

その状態で

bundle exec srb tc

と実行すると、Unable to resolve constantと言うエラーがまだ数千件出ます。

外部ライブラリに存在するClass, Moduleへの参照がある部分でClass, Moduleのrbiがないとエラーになっています。

Rails consoleから

ObjectSpace.each_object(Class).map{|obj| obj.name}.sort.select{|name| puts name }

として定数を洗い出して空のClass(or Module)定義をsorbet/rbi以下に置いて行けばエラーは出なくなります。 (とはいえgem単位でrbiファイルを分けるのがめんどかった・・)

この時点ではメソッドの定義までは行う必要はなくて、ClassとModuleだけrbiに定義されていれば大丈夫です。

メソッド本体と型注釈で記述が冗長

メソッド本体と型注釈部分で2回同じ引数名を書かないといけないのはちょっと面倒だと思うときがあります。

例えば↓のようなのを

sig do
  params(
    arg1: Integer,
    arg2: String
  ).void
end
def some_method(arg1:, arg2:)

end

以下のように書きたい。

def some_method(arg1: Integer, arg2: String)

end

けどこれはRubyの文法として正しくないですね。 あくまでもSorbetはalt RubyではなくRubyのライブラリとして動作するのでRubyの文法から外れられないのです。

JavaScriptに対してのaltjsであるFlowとかTypeScriptみたいにはいかないですね。

とはいえ、YARD書くときだって冗長でしょう?

今後やりたいこと

  • 既存のコードに型注釈を付けていく
  • 社内sorbet-typedみたいなリポジトリを作って、社内gemのrbiの他のprojectへの導入を楽にする(今はコピペ・・)
  • サーバーサイドの型とフロントエンドの型の共有

参考資料

Stripe Blog: Online Payment Solutions Blog

Ruby3で導入される静的型チェッカーのしくみ まつもとゆきひろ氏がRubyKaigi 2019で語ったこと - Part1 - ログミーTech

What is Gradual Typing | Jeremy Siek

他社様事例

Sorbetで「型のあるRuby」の開発体験を試そう

Ruby の型チェッカーの比較 | Wantedly Engineer Blog

新卒エンジニアが全社員が集まるミーティング"WeeklyAekyoHour"のテーマソングを作った話

はじめまして、21新卒入社のtomoriと申します。 新卒研修を終え、6月からデータ基盤チームに配属されて日々エンジニアとして奮闘しています。 ですが、今回はエンジニアリングではなく、音楽を作った話をします。

一体新卒の僕が何の音楽を作ったかというと、freeeの全社員が集うミーティング「Weekly Aekyo Hour」のテーマソングを作りました。 この記事では、作ることになった経緯や全社員が集まる場のテーマソングをどのように形にしていったか、などのお話していきます。

注:所々、音楽用語が出てきます。ご了承ください。

Weekly Aekyo Hourとは

Weekly Aekyo Hour (以下WAH) とは、freeeの全社員が月に2~3回集まる全社ミーティングです。 このミーティングでは、社員の小噺やイベントのお知らせ、CEOの一言など様々な人たちが話す場となっております。 昔は全社員が同じフロアに集まっていましたが、コロナの影響もあり現在はライブストリーミングを使った配信形式で実施されています。

詳しくはnoteにて過去にWAHの司会をされていた方が説明しています。

note.com

なぜ作ることになったのか

なぜ入社したての僕がWAHのテーマソングを作ることになったのかというと、僕が軽い気持ちで「音付けたい」と配信にコメントしたからです笑

「さらに音付けたい」というライブストリーミングのコメント

というのも、WAHには下記のような専用イラストがあり、配信前にはこのイラストを使ったアニメーションが流れるという演出がされています。

Weekly Aekyo Hourの専用イラスト

これを見て「音あったらもっと雰囲気出そう!」と思って上記のコメントをしました。 このコメントが司会の方々の目に入ったらしく、後日で「音楽作ってくれませんか?」と直接提案をいただきました。

ちなみに、僕は音楽なんて一度も作ったことがない...... なんてことはなく学生時代にサークルや同人活動の一環で作曲をしており、現在も趣味で曲を作って提供したりしています。 とはいえ、ちゃんと依頼を受けて楽曲制作するのは今回が初めてでした。

自分にとって挑戦的な課題になると思いましたが、入社時から「エンジニアリング以外の部分でも何か会社に貢献できたらいいな」と思っていたので、二つ返事で引き受けました。

(ちなみに僕は専門学校や音大を出ておらず、独学で作曲を身に付けた人間です。)

どのように音楽に落とし込んだか

まず司会の方々と壁打ちを行いました。 実際にどのようなシーンで使われるのか、WAHはどのようなコンセプトでやっているのか、あたりを話しました。

その内容が下記です。

使用シーン
・「それではいってみましょう!」のようなかけ声にあわせて曲が流れる
・冒頭にインパクトがあり、その後はBGMとして薄ら流れる
・オープニングテーマのような存在
WAHのコンセプト
・ワイガヤ感、かっちりしてなくてワイワイやってる
・freee社員が主体的に行っている、一体感
・ボトムアップ感

これらを踏まえて、このコンセプトに合う曲を作って欲しいとのことでした。平たく言うと「WAH感が欲しい!」ということですね。 正直言ってすごく抽象的でした笑

この取り組みでの一番難しかったポイントが、この抽象的な要望を具体化してかつ音楽に落とし込むことです。 僕は音楽をやってきてこんなことやったことなかったのですが、使える頭を振り絞ってこの課題に取り組みました。

まず、要望を具体化していきました。 1つ1つの要望を分解して、実際の音楽的な要素に当てはめていきます。下記の図が子要素に分解したものです。

(実際は頭の中でやりました。マインドマップって便利ですね。)

要望をマインドマップで子要素に分解した図

分解した子要素をまとめると下記のようになります。

リズム:4つ打ち、ラテン系
BPM:100 ~ 140, 160 ~ 180
ジャンル:Future系、複合系
曲調:ポップ、EDM
コード進行:456進行 etc
雰囲気:ラジオ、パーティイベントのオープニング

このようにまとめると、どういう曲を作ればいいのかわかりやすくなりました。 と言いつつ、2~3日はPCの前で「う〜ん」って言っていた記憶があります笑

同時に参考曲のピックアップを行いました。 上記で挙げた要素を持つ曲を探して、音の使い方などを仕入れていきます。 この作業は今回のような制作に限らず、普段の作曲でも行っていたので、難なく進めることができました。

楽曲制作

(この項は楽曲の解説をしてるだけなので読み飛ばしてOK)

ある程度、曲の全体像がイメージできたら実際の曲作りに取り組みました。 今回は完成度4~6割ぐらいのデモ楽曲を2つ作成し、それを司会の方々に聴いていただき採否を決めるという形で進めていきました。

楽曲1

ジャンル:Future Bass
リズム:2step
BPM:172
曲調:電子音を多用したEDMっぽい感じ
コード進行:456進行
雰囲気:パーティイベントのオープニング

1つ目の曲はパーティイベントのオープニングっぽさをイメージして作りました。 また、個人的にfreeeは 新時代!のような近代的なイメージがあったので、電子音を多用したFuture Bassに仕上げてみました。

結論からいうと、こちらは不採用となりました。理由としては、パーティ感が強すぎてWAHというよりかは全社祭のような大規模イベントの雰囲気になってしまったからです。

(テンションノートを多用したり、あえて部分的にスケールを外すなどしたせいかゴチャゴチャしてしまい、BGM感が薄れてしまったのが個人的な反省点)

楽曲2

ジャンル:Piano House (生楽器を多用した複合系)
リズム:4つ打ち
BPM:130
曲調:ポップ
コード進行:456進行
雰囲気:ラジオのオープニング

2つ目の楽曲はラジオのオープニングを意識して作りました。生楽器の音を多用することでいろんな楽器がいるけど一緒の曲になってる一体感をつけてみました。 また、freeeの新ブランドムービーの楽曲が生楽器中心のアコースティック系だったので、そちらの要素も入れたいなと思い取り入れました。

最終的にこちらの楽曲が採用されました。 理由としては、明るいけど明る過ぎずWAHの雰囲気に合っているとのことでした。2曲ともボツになる未来も考えていたので、1回目のデモトラック提出でOKを頂けてすごく安堵したのを覚えています笑

完成 & 公開

完成したテーマソングがこちらです!

採用された後は、デモトラックをちゃんと聴けるように仕上げ作業を行いました。 この作業に思ったより時間がかかってしまい、7月下旬にこの話をいただいていたのですが、曲が完成したのは8月末でした。 空いてる時間を使いながら作業したとはいえ、1ヶ月以上かかってしまうとは思いませんでした。

(2週間ぐらいでできるやろって思ってました笑)

初めてテーマソングが流れた配信で、freee社員の皆さんから「すごい!」などのコメントを寄せていただいてすごくホッしたのを覚えています。

(実際、僕はプロの作曲家ではないのですごくプレッシャーを感じていました笑)

やってみた感想

やってみた率直な感想としては「まさかエンジニア入社した会社で楽曲提供することになるとは!」です。 こういう面白い経験はfreeeに入社してなかったらできなかったかもと思ったり笑 実際いろいろ試行錯誤しながら曲を作り上げていくのがすごく楽しかったです。

もちろん、全社員が関わるミーティングにアウトプットを出せたのは素直に嬉しかったですし、新卒としてちょっとは会社に名を残せたかなーと勝手に思っています。

以上、新卒エンジニアが音楽作った話でした!

初代・二代目巨匠が考えるエンジニアキャリア(連載 第2回)

こんにちは、キャリアに悩めるエンジニアのichienです。先日はfreeeの巨匠制度の歴史についてこちらの記事でご紹介しましたが、今回は初代・二代目巨匠であるterashiさん、ebiさんをゲストに招いて当時の感想を聞きました。terashiさんは理系出身・生粋のスペシャリストタイプ、ebiさんは文系出身・様々な職種を経験してきたジェネラリストタイプという対照的なキャリアを歩んでおり、キャリアに対する考え方についても話してもらいました。

巨匠に選ばれて1ヶ月で何を作ったか

ichien:お二人はどんな成果物を発表したんですか?

terashi:freeeの「自動で経理」という機能で、どの勘定科目でどの品目でというのを機械学習でやっている部分があるんですが、そのサジェスト結果を評価をするための基盤を作りました。サジェストを評価するために十分なログを残す、それを評価するフレームワークを作るということをして、ログは今も活用されていますが、評価の基盤は残ってないです。当時は1ヶ月投資的にやってみるという感じだったので、引継ぎ先がなく流れてしまいましたが、後々の制度変更で改善されました。

初代巨匠 terashi 「もっと自動で経理」

「自動で経理」の推測精度測定の自動化・プロセス化、性能の定量評価を導入

f:id:elly_nskw:20211011172121p:plainf:id:elly_nskw:20211011155207p:plain
発表内容イメージ

ebi:僕は「未来のfreeeのデータモデル」というテーマでやりました。当時のfreeeプロダクトのカバーしている業務領域や機能性は、今と比べるとかなり限られたものでした。マーケティングメッセージとしては「クラウドERP」という打ち出し方を始めていた頃ですが、以前にERPを触っていたこともあり、当時のラインナップからするとERPとしてはまだまだ、という感覚を持ってました。そこで、胸を張って「freeeはERPです!」と言えるようになるためには将来どういったプロダクトになっていなければならないかを想像し、データモデルという切り口で表現しようというのが僕のテーマでした。

二代目 ebi 「未来のfreeeのデータモデル」

取引ネットワーク & ビジネスコラボレーションプラットフォームのためのデータモデルを一からあるべき姿に再設計

f:id:elly_nskw:20211011171912p:plainf:id:elly_nskw:20211011171922p:plain
発表内容イメージ

当時みんな巨匠制度に対しては、何か動くものを開発するというイメージを持っていたと思うんですけど、僕は何も動くものは作らず、アウトプットとしてはドキュメントで、データモデルの絵と説明書きだけだったんですよ。

ただ、その後freeeの機能も充実していく中で、まさに考察したような領域がぽつぽつとプロダクトの中に生まれてきて、自分が直接機能開発をしなくても、担当者がそのデータモデルを参考にして製品に反映してくれているのを見ると、すごく嬉しいしやってよかったなと思います。

terashi:ebiさんのは長期的に正しく投資しましたね。組織構造を表現する社内サービスや総勘定元帳や試算表の検索の高速化、認証認可基盤でも参考にしてますよ。

ichien:あらゆるプロダクトで活用されてるんですね!

ebi:この巨匠のテーマが、僕が所属しているERP基盤というチームにもつながっています。5年越しくらいで頭の中の構想を実現できる可能性がでてきて、非常にエキサイティングですね。

ichien:会計の他にはない中で、ERPとしてどのようなデータモデルを実現したらよいかを予測してたんですね。

terashi:いまもebiモデルと呼ばれてますよ。

ebi:直前がterashiだったのでかなりプレッシャー感じましたよ。僕はずっとプログラマーってわけでもなくて、そもそも大学も経営学科でしたからね。何かすごいもの、難易度の高いものを作るとかじゃなくて、自分の経験が活かせてかつfreeeの将来に役に立つものという意識でテーマを選びましたね。terashiのテーマがあったからこそ、テーマ選定にはとても悩みました。

非連続的進化とは非連続的進化の起点を作ること

terashi:テーマ選定は私にとっても一番重要なポイントでしたね。1ヶ月集中してやることも重要なポイントですが、むしろテーマ決めのほうが巨匠としての意味は大きかったと思います。

そもそも初期の巨匠ってとりあえず1ヶ月で何かインパクトのある非連続的進化を出してくれという無茶ぶりだったんですよ(笑)普通に考えたらそんなこと簡単にできるかって話なんですが、私はまず初めに非連続的進化っていうのを”非連続的点を作ること”だと解釈したんですよね。グラフの伸び方が途中でぽんと変わるようなポイントをうまく作ることだと思ったんです。

f:id:elly_nskw:20211011160445p:plain
非連続的点のイメージ

そこから、いまの自分のスキルと知識とfreeeの課題の中で、1ヶ月で自分が一番インパクト出せる部分をひたすら考えました。私自身の仕事に対する取り組み方も非連続的に変わったきっかけになったと思ってます

ebi:terashiが言語化してくれた”非連続的進化の起点をつくる”というのには僕もすごく腹落ちして、その観点でテーマ選定できましたね。

ichien:今後参加する人にはどんな気持ちで参加してほしいですか?

ebi:結果を求められるものではないので、しりごみしないでチャレンジしてみてもらいたいですね。もちろんいい成果が出てプロダクトの発展に貢献できれば素晴らしいけど、一定期間ひとつのテーマに集中していつもと違う仕事をするのは個人の成長にもつながることだと思うので、ぜひ積極的に取り組んでみてもらいたいですね。

terashi:この制度は内容の変更も度々ありましたが、失敗してもナイストライ!と言われる環境で”チャレンジしてほしい”っていうのは一貫しています。初期の巨匠制度は投資的な側面が強かったんですけど、いまはその人の現在のキャリアに合わせて現時点からチャレンジできるようにしています。そもそも、いまのfreeeの規模だとすごいことやるならやっぱりチームでやるべきなんですよ。でも個人のチャレンジは別であるべきだし、その環境をこの巨匠制度の後継であるマジ価値DeepDiverやワンマンNavyで実現しようとしています。いまの自分を一歩超えるというのを目標にしてチャレンジする場だ思っていただけると嬉しいですね

ichien:自分を一歩超えるっていいですね。プロダクトの成長と個人の成長を両立できると幸せだなって感じました。

エンジニアになったきっかけ

ichien:お二人のいままでのキャリアってどんな感じだったんですか?

terashi:私はプログラミング自体は小学生の頃からやっていて、父親が当時としては珍しくPCを持っていたんで、ベーシックとかをやってみたのが事の始まりです。中学高校と独学で、大学時代は情報工学科でコンピュータサイエンスを勉強して、サークルでゲーム開発したりとか、競技プログラミングのICPCに出たりしました。C++が大好きで、幸せに開発するためのツール作ろうとしてましたね。いまサービス基盤を作ってるのもそこが原点かもしれないです。

ebi:僕も最初は小学生で、初代ファミコンのファミリーベーシックっていうキーボードのオプションをクリスマスプレゼントでもらったんですよね。中学生でMSXに乗り換えて、自分でゲーム作ったりしてましたね。

高校に入ってからもプログラマーになりたいとは何となく思ってて、理系コースを選びました。ただ途中で「将来社長になろう」と思って、社長=経営という安易な考えから経営学科目指しちゃったんですよ。

で、経営学科入ったんだけど、やっぱりつまらなくなっちゃって。たまたまゼミの先生がなぜか人工知能の先生で、それがきっかけでもう一度コンピューターの世界に戻ってきたんです。当時Prologという、いまとは違う人工知能がありましてね(笑)

経路探索のようなアルゴリズムらしいアルゴリズムやUnixをちゃんと勉強して、とにかく頭捻って面白かったですね。それでエンジニアとして就職することにしました。

2人のエンジニアキャリアの歩み

ichien:社会人になってからはどんなキャリアを歩んできたんですか?

terashi:新卒で入ったのがGoogleで、主にGoogleマップのデータまわりを7年弱やっていたんですが、同じことやり続けるのもどうかなと思って、チーム異動と転職を比較検討していました。

転職といってもどこがいいのかよく分からないまま、フリーランスはどうかと調べていたところ、確定申告の仕方みたいなページに「確定申告ならfreee」って書いてあったんですよね。

実はGoogle時代にDSさん(freee CEO)と働いていたことがあって、正直DSさんが起業してたことはすっかり忘れていたんですけど(笑)税金まわりのお金の計算とか好きだったので、自分の趣味にも合うと思って真面目に考え始めました。

f:id:elly_nskw:20211011161329p:plain
初代巨匠terashiさん

DSさんのことは知ってるからいいとして、私にとって重要なのはテクニカルに楽しくやれるかというところだからYJさん(freee CTO)に会わせてもらおうと思ってメールを送りました。話していく中で、オープンな技術を学ぶ機会を得つつ、freee自体はこれから大きくしていくフェーズだったのでそこで実装力を発揮して、ゆくゆくは大規模なデータを触る経験も活かせるだろうということで転職を決めました。

はじめはfreee会計の開発をしていたんですが、そのあとは大規模な移行プロジェクトを担当することが多くて、課金基盤の移行、データアグリゲーション基盤、認証認可基盤の移行、いまはマイクロサービスの共通基盤を作っています。

ebi:僕は新卒で日本オラクルに入って、データベースのコンサルタントをやっていました。「未来のfreeeデータモデル」にもその時身に着けたデータモデリングのスキルを活かしています。その後SONYに転職、しばらくしてスタートアップに2社連続で転職しました。

スタートアップはやっぱりスピード感だったり自分のアイデアをすぐ実行に移せる裁量の大きさが魅力でしたね。ITコンサル、情シス、プロダクト開発、プリセールスなどとにかく何でもやってきました。うまく会社のビジネスが伸びれば経済的メリットも大きいだろうし。ただ、どっちでも自分は成功できなくてちょっと休憩・・と思って前職のSalesforceに転職しました。

f:id:elly_nskw:20211011161609p:plain
二代目巨匠ebiさん

SalesforceではITコンサルに戻ったんですが、3~4年やってるとまた元気が戻ってきて(笑)スタートアップでチャレンジしたいなと思うようになったのと、自分で手を動かすプログラミングの仕事が減っていたのもあり、もう一回外に出ようと思いました。

freee入ったときまだエンジニアも20人くらいでいまのJM(ジャーマネ: freee におけるマネージャー)制度もなかったし、フラットに20人並んでいるって感じ。よく言えば自由、悪く言えば統制がとれていないみたいな。はじめにやったのは前職の流れもあって、freeeにSalesforceを導入するってことでしたね。

それからfreee人事労務のオフィシャルリリースに向けて、最初はfreee会計にならって課金の実装をしていたんですが、それがどうにも自由度がなく、実装をがらっと作り変えました。そうしたら、それなりにいい出来だったのか評価がよくて、freee会計にも同じものを入れていこうという話になり、そこからずっとこの間の6月まで課金まわりの開発を担当し続けていました。

terashi:その課金基盤の移行プロジェクトで、私も一緒にやっていました。ebiさんははじめプレーヤーだったけど途中からJMになったんでしたよね。

ebi:そうそう、あのときはterashi含め3人でプロジェクトを進めるという状況で、タスク管理や他チームとのコミュニケーションで誰かがリーダーシップをとったほうが動きやすいんじゃないかと思い、手をあげました。

僕自身はプレーヤーとして価値発揮したくてfreeeに入ったけど、あの当時はマネジメントに時間を割いたほうがチームの効率が良くなると思って、自分なりに納得してそういう役割を担っていました。

3人しかいなかったのでそんなに苦労したわけじゃなかったんですけどね(笑)

terashi:ぶっちゃけあのときのチームはマネジメントがなくてもなんとかまわるようなチームではありましたが、それでも役割としてチーム外とのコミュニケーションをebiさんがやってくれてかなり楽になったのは覚えてますね。

私はマネジメント業務には関わっていなくて、とはいえ個人としてがりがりコード書いてどんどん開発するスタイルで力を発揮していたつもりなんですが、あるプロジェクトでJMはいるけどテックリードが2人くらい異動や転職で抜けてしまい、自分が動くしかないと思ってテックリード的な動きをやった経験があり、テックリードとしての成功イメージにはなっているかもしれないですね。

マネージャーとスペシャリストというそれぞれの立場で大切にしていること

ichien:JMとテックリードというそれぞれの活躍の仕方をお互いに見ていて、感じることはありましたか?

f:id:elly_nskw:20211011163321p:plain
2021年5月入社のichienさん

ebi:課金基盤の移行のとき思っていたのはterashiはとにかくコード書くのが早い(笑)なおさら自分が頑張るよりもterashiのスピードを保てるように自分が他のタスクやったほうがチームとしてスピードが上がるという思いはありましたね。

terashi:ebiさんの動きでいうと、ebiさん自身はプレーヤーとしてやりたいという思いを持ちつつも、JMやったほうが最適だからJMをやっていたし、でも最終的には後任のJMの採用を進めて、いままさに引継ぎをしてきちんとプレーヤーに戻ってきたって感じですよね。状況見て、組織として必要なことも自分のやりたいことも満たすという進め方が尊敬できるなと思ってます。

ebi:照れますね(笑)

terashi:私はけっこう我を通す人ですから(笑)

ichien:キャリアを考えるうえで大事にしてること、キャリアに悩んでいる人へのアドバイスはありますか?

ebi:僕はこの会社でプレーヤー⇒JM⇒プレーヤーと、いくつかの立場を経験させてもらってますが、チャンスがあればぜひ一度JMをやってみてもらいたいなと思いますね。やってはじめて分かることもありますし、なんにせよこの会社では一度マネジメントに移ったらそのあともずっと、っていうことはないです。僕もそうだし他にもJMからプレーヤーに戻るというキャリアを歩んでいる人がいるわけで、だからこそぜひチャレンジしてほしいなと思います。

マネジメントにはマネジメントの面白さがあって自分が頑張って自分の成果出すのも気分いいですけど、チームで頑張ってチームで良い成果出すほうが僕は喜びが大きかったですね。JM経験してプレーヤーに戻ると、自分のJMがどういうこと考えているか、どういう悩み持っているかとか想像しやすくなるのでよきメンバーにもなれる気がします(笑)

terashi:自分が意識しているのは、JMと同じだけのインパクトをスペシャリストとして出さないといけないってことですね。

JMというのはチームをいかにスケールさせていくかということを考えるので、スペシャリストの場合はコードを書いてそれを実現させないといけないんですよ。チームや全社に対してインパクトを出すようなコードを書かなきゃいけないし、それは単純な話ではないので頑張って考えないといけないなと思っていて。

マネジメントの仕方がいろいろあるのと同様にスペシャリストのインパクトの出し方も幅は広くて、典型例としてはアーキテクトとして全社に影響を与えるような設計をしていくこと。あとはフェローとしてすごく重要なコンポーネントを作りあげるみたいなこと。

後者のほうはイメージが難しいかもしれないですけれども、究極の例としてはJeff DeanというGoogleのシニアフェローで、具体的にはMapReduceやBigTableを作るというようなレベルなので、すぐにできないし一生かけてもできなそうなんですけど(笑)私自身生涯かけて追及していきたい姿ではあります。

コードを書くことによってインパクトを出したいという人がいれば私も及ばずながら相談に乗れますし、freeeにはそういったスペシャリストを目指す人が楽しく仕事できる場所であり続けてほしいですね。

ichien: お二人の違ったキャリアについて聞くことができました。今日はありがとうございました。

次回は8月に行われたマジ価値DeepDiverで発表を務めたliaoさんに、その際の取り組み内容や学びについて書いてもらいます。ぜひご覧ください!

データ処理パイプラインの Argo Workflows 移行を検討した話

AirflowからArgo Workflowsへ
AirflowからArgo Workflowsへ

freee の AI ラボというチームでエンジニアをしている id:nagomiso と⾔います。好きな飲み物はストロング系チューハイです。オススメはキリン・ザ・ストロングのコーラサワーと SAPPORO 99.99 のクリアレモンです。

さて, あまりイメージがないかも知れませんが実は freee の AI ラボでは機械学習やデータを活用したサービスの検討・開発だけではなく, 開発や運用を効率的に行うためのインフラ整備にも取り組んでいます。(取り組みの一部は 開発スピードを止めない機械学習インフラ基盤――freeeに学ぶAI開発で本質的価値を提供する方法 でも紹介しています)

こうしたインフラ整備の一環としてデータ処理パイプラインの Argo Workflows 移行を進めているので今回はその話をしようと思います。

動機

もともと AI ラボではデータ処理パイプライン用のワークフローエンジンとして Apache Airflow を採用していました。 Airflow は Python スクリプトでワークフロー(= DAG; Directed Acyclic Graph)を定義できるので定義の柔軟性・実装の容易さといった面では優れているのですが, 一方で運用していくうちに

  • DAG の定義が書かれたスクリプトを所定のディレクトリへ配置する以外にワークフローをデプロイする方法がなくモダンな CI/CD フローが構築しづらい(少なくともいまはできていない)
    • Twelve-Factor App で言うところの「単一のコードベースと複数のデプロイ」のような状態を実現しづらい
    • ワークフロー自体は各アプリケーションのリポジトリで分散管理したいがその場合 Airflow の環境とリポジトリの内容を同期する仕組みを整えるのが大変(そのためいまは複数アプリケーションのワークフローを単一のリポジトリで管理している)
  • Python スクリプトで DAG の定義が書けるがゆえにワークフローの定義にロジックが入り込みやすく関心が分離されていない状態に陥りやすい

という点が気になるようになりました。特に 1 点目は少人数で複数アプリケーションのリポジトリを管理している AI ラボの開発効率を向上させるため今のうちに改善しておきたいと考え, ワークフローエンジン移行検討に踏み切ったのでした。

技術選定

移行先のワークフローエンジンを選ぶためにまずは AI ラボの用途と課題感に合わせて要件を考えました。観点が少し散らかっていますがおおよそ以下のようなことができることを要件として技術選定しました。

  • Airflow の現運用と同じようなことができる
    • ワークフローの定義をリポジトリで管理できる
    • ワークフローの実行スケジューラが内蔵されている
    • リトライ機構が備わっている
    • Web UI でワークフローの実行状態を確認できる / 失敗時手動リトライができる
    • 単純なタスクのステップ実行だけではなくある程度の分岐や並列実行があるワークフローが定義できる
  • 単一のコードベースから複数の環境へ簡単にデプロイできる
    • ワークフローは環境(多くの場合は ステージング / 本番)に応じた差分以外の定義を共通化できる
    • デプロイ時はデプロイ先に環境に応じた値を簡単に切り替えられる
  • ワークフローの定義を各アプリケーションのリポジトリで分散管理しやすい
    • 複数のリポジトリで管理されているワークフローを簡単に動作環境へデプロイできる
  • その他
    • ワークフローの定義と各タスクにおけるロジックの関心が分離できる方が嬉しい(programable 過ぎないほうが良い)

特に 2 番目のデプロイと 3 番目の分散管理のことを考えて Cloud Native なワークフローエンジンがよさそうということで候補にあがったのが Argo WorkflowsTekton pipeline でした*1

Argo Workflows も Tekton pipeline もワークフローエンジンとしての基本機能やマニフェストの書き方は大差がなかったのですが

  • 導入に使うマニフェストの構造が比較的単純でチームの用途に合わせた設定のカスタマイズがやりやすそう
  • スケジューラや Web UI が標準で装備されている

という点を鑑みて最終的には Argo Workflows に絞って移行検討を行うことにしました *2

Airflow と Argo Workflows の比較

技術選定が終わったので次は Airflow 上で稼働していたデータ処理パイプラインのひとつを実際に Argo Workflows 化して利用感を比較してみました。ここでは「使用感が結構違うな」と思った部分に触れていこうと思います。

なお検証時点での Argo Workflows のバージョンは v3.1.8 です。今後は状況が変わるということも十分にありえるということを予めお断りしておきます。

日時操作

ワークフローエンジンを使っていると「トリガーされた時刻の前日」や「トリガーされた時刻の3時間前」などトリガーされた時刻基点での日時操作をしたくなることがあります。このような操作が必要になったときの使用感が Airflow と Argo Workflows では大きく異なりました。

Airflow の場合

トリガー時刻は {{ next_execution_date }} で取得できるかつその操作も組み込みのマクロ関数が用意されているので簡単に操作ができます。例えば「トリガーされた日付の 1 週間前」を取得したいときは以下のように書けば取得できます。簡単ですね *3

one_week_ago = "{{ macros.ds_add(next_execution_date, -7) }}"

Argo Workflows の場合

トリガー時刻自体は {{ workflow.creationTimestamp }} で取得できるのですがそれを操作する関数までは用意されていません。Workflow Variables を読むと一見 Sprig 関数が使えるように見えるのですが

# ❌ これは動かない
{{= workflow.creationTimestamp | date_modify '-168h' }}
# ❌ これも動かない
{{= sprig.dateModify('-168h', workflow.creationTimestamp) }}

のように書いても日付操作してくれません。

これは workflow.creationTimestampk8s.io/apimachinery/pkg/apis/meta/v1metav1.Time 型の値を返しているのに対して Spring 関数の sprig.dateModify() は Go 標準の time.Time 型の値を受け取ることを想定しているという実装のミスマッチが原因のようです*4

以下のように sprig.toDate()sprig.date() を駆使すれば一応操作はできるようになりますが記述が冗長です。

# ⭕ こうすると一応動く
{{= sprig.dateModify('-168h', sprig.toDate('2006-01-02T15:04:05Z07:00', sprig.date('2006-01-02T15:04:05Z07:00', workflow.creationTimestamp))) }}

上記以外の方法で解決する場合は「実行する Step/Task 側のロジック」で解決するか別途操作用のテンプレートを作る必要があります。

Argo Workflows は WorkflowTemplate を使うと共通のテンプレートを定義できるのでそれを使って次のようなテンプレートを作成してそれを使うことで対応しました。

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: utilities
spec:
  templates:
  - name: date-operator
    inputs:
      parameters:
      - name: baseDate
      - name: interval
      - name: outputLevel
        default: date
    script:
      image: ubuntu:20.04
      env:
      - name: TZ
        value: Asia/Tokyo
      command: [bash]
      source: |
        date \
          --date='{{ inputs.parameters.baseDate }} {{ inputs.parameters.interval }}' \
          --iso-8601='{{ inputs.parameters.outputLevel }}'

タスクの定義

ワークフローエンジンは実行の最小単位(ここではタスクと呼びます)を定義してその依存関係を記述します。このタスクの定義方法が Airflow と Argo Workflows ではかなり違います。

Airflow の場合

XXOperator のインスタンスを生成することでタスクを定義します。PythonOperator などを使用する際は Airflow 環境に全 PythonOperator で使用する依存関係をインストールする必要があるため各タスク間の依存関係が競合しやすくなります。

また他の Operator を使ったとしてもその処理内容を直接 DAG 定義に書き込むことになるのでワークフローとロジックの関心の分離が難しいです。

Argo Workflows の場合

template オブジェクトを定義することでタスク(あるいはステップ)を定義します。基本的にはあるコンテナイメージを参照して実行する形なので各タスク間で依存関係が競合することは殆どありえません。

また script を使わない限りはワークフロー定義内にロジックが入り込むことがないので関心の分離がしやすいです。一方でこの利点と表裏一体で各 steps/tasks 内で処理を直接記述する方法がなく, まずは spec 内でテンプレートを定義してからその実行時の順序を記述することになるので 1 回しか参照されないような タスクが多いワークフローの場合は定義がかなり冗長になります。ただし v3.2 系では inline template(steps/tasks 内で直接実行コマンドなどが指定できるテンプレート)が使えるようになる予定なので将来的には解消される内容かと思います。

ワークフローデプロイ方法

移行の動機ともなったワークフローのデプロイ方法です。Cloud Native な Argo Workflows と Airflow とではかなり使い勝手が異なります。

Airflow の場合

所定のディレクトリに DAG 定義が書かれた Python スクリプトを配置することでワークフローがデプロイされます。 GitOps ライクに Git リポジトリの中身を環境に反映させる方法を採用すると Airflow 環境を分けるかブランチを分けない限りステージングデプロイと本番デプロイを分離することが難しいです。まさに同一のコードベースから複数のデプロイを実現するのが難しい状況にあると思います。

Argo Workflows の場合

kubectl コマンドか argo コマンドでワークフローをデプロイします。コマンド実行時に Namespace や Context を切り替えれば同一のワークフロー定義を複数の環境にデプロイするのが Airflow と比較して容易に実現できます。

デプロイ環境毎に設定の差分がある場合は Kustomize や任意のテンプレートエンジンを使えば共通部分と最小限の差分を定義するだけで複数の環境にデプロイすることができます。AI ラボでは Kustomize を使って適宜値を切り替えることで宣言的に複数の環境へのデプロイができるようにしました。

また一つの実行環境に対して kubectl コマンドか argo コマンドを実行すればデプロイ出来るのでワークフロー定義を分散管理しやすいというメリットがあります。

結論

Airflow と Argo Workflows の比較の項で触れたように日付操作に若干のクセがありましたが

  • ワークフロー定義を分散管理しやすい
  • 単一のコードベースから複数の環境へデプロイしやすい

という利点が大きくAI ラボの用途においては Airflow を運用し続けるよりも Argo Workflows に移行した方が中長期的にメリットが大きいという判断になりました。

まとめ

検証の結果データ処理パイプラインに使用するワークフローエンジンを Argo Workflows に移行することに決めました。今後は既存のワークフロー定義を Argo Workflows に移行したり自動 CI/CD の仕組みを整備していこうと思います。

個人的な見解ですが Kubernetes を使うことによって得られるパイプラインのスケーラビリティや定義の分散管理可能性, デプロイの容易さを考えると今後は Tekton を含めた Cloud Native なワークフローエンジンをメインで使用するケースが増えてくるのではないでしょうか。Airflow にも Kubernetes Executor がありますが元々 Kubenetes での利用を想定した Argo Workflows や Tekton pipeline を使うほうが素直な運用になるでしょう。Airflow や Digdag といったワークフローエンジンを使っている方は用途にもよりますが一度移行を考えてみても良いかも知れません。


*1:Airflow とよく比較されるワークフローエンジンとして Digdag がありますがこちらはワークフローの定義とロジックの関心が分離できるものの Cloud Native なワークフローエンジンでなく, 分散管理と CI/CD パイプライン構築の容易さを考えて今回は選外としました。また Cloud Native なワークフローエンジンとして Kubeflow Pipelines という選択肢もありましたがこちらは ML 用の色合いが強く, ML 以外のデータ処理用途でも使用できる汎用ワークフローエンジンを求めていたのでこちらも選外にしています。

*2:Tekton はスケジューラは Triggers(の Cron), Web UI は Dashboard をそれぞれ別個に導入する必要があるので構築ハードルが少し高そうな印象を持ちました

*3:トリガーされた日付が欲しいと言っているのに next_execution_date?と思われる方もいるかと思いますがこれは Airflow のスケジューリングの考え方が特殊なことに由来しています。こちらの内容は https://kencharos.hatenablog.com/entry/2020/05/07/161558 が詳しいです。

*4:原因については我らが頼れるマネージャーのRoyさんが調べてくれました。感謝!