あまりよくある話ではないと思うんですが、アナリスト/Analytics Engineerの人にバッチ処理を書いてもらう機会がありました。基本的にはSQLを普段書かれていて*1、場合によってはTerraformを少し書くこともあるというバックグラウンドの方です。これに対して私はレクチャーやサポートする形になったので、メンター的でどういうことを考えていたかをこのエントリでは書こうと思います。
対象のタスク
対象のタスクは典型的なバッチ処理で、以下のようなものでした。
- 外部のAPIを呼び出し、データをJSONなりCSVなりで保存
- 保存したファイルをGCSにアップロード
- GCS上のファイルをBigQueryにロード
コードはPythonで書き、将来的にはCloud Run Jobsなどのコンテナ環境で動作する想定です。対象の方は普段dbtなどで開発はしているため、gitやGitHubの基本的な操作は問題ない状態でした。
レクチャーしたこと
Step by Stepで実装する
取り組むタスクは典型的ではあるものの「普段ほぼSQLしか書いていない」という人にとっては、どれも初めてのタスクです。そのため、タスクを細かく溶き解していく必要があります。「何が分からないかが分からない」や「自分がどこにはまっているのか分からない」という状態だと適切に検索するのも難しいというのはよくあることなので、一気にはやらず全体ロードマップを示した上で進めることにしました。典型的なタスクではあるので、正直ググったりChatGPTに聞けばそれらしきコードが見つかる/書いてくれる内容ではあるものの、コードの書き方や考え方を見に付けてもらう必要があったので、こういった進め方をしました。
また、SWEのバックグラウンドがない人にとって、序盤で一番難しいのはエラーメッセージの読み解き方かなと思います。ペアプロしながら、エラーメッセージの読み方や直し方などもレクチャーしていきました。
APIやjqに慣れる
「そもそもAPIを呼ぶとは?」からレクチャーしました。本来の目的のバッチは有料でtoken必須のAPIを叩く必要があったのですが、認証が必須のものだったりぐぐってもあまり事例がないAPIだったこともあり、GitHubのAPIで練習することにしました。pythonからAPIを叩いてもいいのですが、もっとシンプルに経験してもらいたかったので、curlとjqを使って、以下の内容をレクチャーしました。
curl -L https://api.github.com/users/syou6162のようにエンドポイントを叩くと、JSONが返ってくる- BigQueryやdbtを普段使っているAnalytics Engineerでも分かるように、
STRUCT的なデータだよと説明
- BigQueryやdbtを普段使っているAnalytics Engineerでも分かるように、
curl -L https://api.github.com/users/syou6162/reposのようなエンドポイントを叩くと、JSONの中に配列が入ってくるものもある- BigQueryでいうところのARRAYだよと説明
- SQLであれば、UNNESTしたりSTRUCTから要素を抽出するけど、APIから取得したJSONも
jqを使うとデータの抽出や加工ができる- dbtではyamlを多様するけど、yamlからデータを抽出する際は同じように
yqが使える - これが使えるようになるとCI/CDでやりたいことを簡単にできるようになったりする
- dbtではyamlを多様するけど、yamlからデータを抽出する際は同じように
|(パイプ)の説明- コマンドラインを叩く上で覚えておくと便利なやつ
curlとjqを使えば簡単にできるので、pythonで色々はまったり、APIのレスポンスがでかくてデバッグしにくいデータを扱いやすくなるなどのメリットもあるので、この段階でレクチャーすることにしました。
あとは、雑談的にChromeのDev Toolsを開きながら、以下のような話もしていました。
- ブラウザの裏側では、こういう形でAPIのcallが行なわれている
- フロントエンドはAPIのレスポンスからhtmlに加工している
- いわゆるServer / Clientモデルの話
- Cookieとかlocal storageとかの話をしだすと流石に長くなるので、その場では説明しなかった
- データの操作はブラウザからもAPIからも行なわれる、そのためロジックは基本的にサーバー側に集約する
- dbtなどのパイプラインも同じで、Business Vaultにロジックは集約して、それを利用するfact / dimやmartにはなるべくロジックを持たせないようにする、などの話を膨らませた
- SWE的な思考を持てるようになると、幅が広がる
Dockerfileを使って環境構築する
将来的にCloud Run Jobsで動かすため、最終的にはDocker化は必要です。しかし、初手で覚えてもらうかは少し迷いました。「手元でひとまず動かしてもらって〜」というのもありだなと思いつつ、pythonは環境構築の手段が色々あり過ぎますし、「エラーが再現する状況を適切に報告する」というのはコードを初めて書く人にとって割と難しいことです。
こういったこともあり、ペアプロでレクチャーしながらDocker化を最初のほうに行ないました。必要なファイルをCOPYしてRUN pip install -r requirements.txtするくらいの簡単なものですが、手元の環境とDocker内のことを意識するなどが必要になるので、一個一個解説しながら進めていきました。
また、dockerはコマンドラインで色々動かすことが多いですが、コマンドライン操作に慣れていない人だと最初はそこでつまづくことも多いので、Makefileでwrapして使ってもらう形にしました。
1時間くらいでレクチャー & pr作成まで行なったかなと思います。
正常系を実装する
シンプルなバッチ処理でも異常系で考慮すべきことはありますが、ひとまずend2endで動くようにならないとコードを書いてる感も出ないかなと思ったので、正常系を最後まで実装してもらうことにしました。後段で改善していくことを前提に、ここでは細かいことは指摘しない方針でいきました。
適切な関数やクラスに分割する
よくある話ですが、mainに全部ベタ書きされている状態だったので、適切な粒度の関数やクラスの分割していきました。
- 簡単なバッチの正常系のみの実装であれば分割しなくてもさっと読めるけど、異常系なども考慮し始めるとコードが長くなりがちなので、意味のある単位に分割する
- mainにベタ書きされている状態だと、基本的に全部global変数になるため、頭のメモリを消費する
- 関数をうまく使って変数のスコープを狭くする、ここだけ読めばいい、という状態を作る
- 「 CTEなしで書かれたSQLのメンテナンスって辛いっすよね?」あたりを例に引き出しながら説明
コマンドライン引数や環境変数を使う
サードパーティのAPIを叩く際にAPI Tokenを扱う必要があるため、そういった類のものは絶対にcommitしないでくれ、と念入りに説明しました。具体的には以下を説明したかなと思います。
git rmしたとしてもcommit自体は残っている- そういったcommitを含めて整理するのは非常にコストがかかる
- 特に公開リポジトリでやると、あっという間に悪用されること
こういったAPI Tokenなどをコード自体に含めない方法として、以下のような方法があることを伝授しました
- コマンドライン引数
- 一番お手軽ではある
- 環境変数
- direnvや
.envファイルなど - コマンド履歴に残らない
.envファイルをコミットに含めないようにする必要があったり、複数人で開発する際に.envファイルをどうやって管理していくかなどの難しさはある
- direnvや
- Secret Manager
- セットアップは一番面倒ではあるが、localやCloud Runなどの環境の差分は吸収しやすい
型アノテーションを付ける
コードの可読性を上げるために型アノテーションについて伝授しました。pythonの場合、型アノテーションがなかったり間違っていてもコードは実行できてしまいます。とはいえ、関数を提供する側としてはこの型を期待しているということが明示できますし、利用者側もそれを前提にコードが書けるので、コードに対する情報量がぐっと増えます。
コードの可読性を上げる手段としては、変数/関数名をよいものにする、適切なコメントを入れる、などもあると思いますが、適切な型があることは意思疎通をやりやすくする手段の一つでもあるので、是非身に付けて欲しいです。
また、pythonの型は正直ないよりはマシ的な側面もあるので、golangやHaskell、Scalaなどリッチな型表現がある言語も触ってみるとより型のありがたさが分かってくるよ、という話もしました。
loggerについて知る
これも必須かと言われれば必須ではないかもしれませんが、運用を見越したバッチ処理を書けるようになって欲しかったので、loggerについても触れました。Cloud Loggingなどを通じてバッチ処理のログを見ることが多いですが、ログレベルを適切に設定することで必要な情報を絞ってみることができたり、最近のloggerだと構造化した上でログを出すことができるため、運用時に問題の個所がどこか素早く分かるようになります。
異常系を考慮する
これも初手で適切に書くのは難しい部類ですし、自分が書くコードも適切に書けているかと言われるとあんまり自信がないものにはなります。try exceptを書くことが異常系の考慮ではないし、ここは割とケースバイケースでこういう風にするといいよということをコメントしていった気がします*2。具体的には「エッジケースを含め、どういう失敗ケースが考えられるか」「冪等性を意識する」「バッチが失敗した際に途中からretryできるか(resumableになっているか)」などを例に出しながら説明していました。
また、エラー処理を逆に頑張り過ぎてディフォルト値で穴埋めし過ぎるケースもあったので、「Data Ingestionであるバッチ処理はIngestionに集中して、NULLをディフォルト値で埋めるのはdbtのstaging層以降などにしておくのがいいのでは?NULLはNULLで意味があるので、それを埋め過ぎると後からデータが追えなくなってしまうこともありますよ」といったこともコメントしました。
この辺りは自分が書いたコードを長く運用してみたり、ライブラリを提供する側になって、どういう例外処理がいいかを地道に学んでいくしかないかなーと思います。
逆にいい教材があれば教えて欲しいです。
テストどうする問題
BigQueryやGCSなどクラウドに依存するバッチのテストをどうするかは悩ましい問題です。結論としてはmockやfixtureなどの準備をしっかりするのが大変なため、このケースではテストを書かないという意思決定をしました。ただ、これが普通だとは思って欲しくないため、ちゃんとやるのであれば、エミュレーターなどを使ってやるのが正しい、ということをセットでお伝えしました*3。
あとは
- CI/CDでpr毎にデータセットなどを作り直す
- dev環境でさっと試せる環境を用意する
なども合わせて伝授しました。
脱線
バッチ処理をアナリスト出身の人に書いてもらうのは適切か?
これは正解はない問だと思います。私個人としては若干否定的ではありますが、チームや組織の状況やケイパビリティ、個人がどうなっていきたいかのwillなどの変数もあり、一概に適切ではない、とは言い切れないとは思います。
とはいえ、こういったバッチ処理を書くのはSWEとしての最低限のベースの知識は必要ですし、他職種の人が簡単に身に付けられるかというと、そうでもないとは思うので、多少なりとも努力をしてもらう必要はあると思います。
アナリスト出身の人やAnalytics Engineerがこういったバッチ処理を書けるようになるメリットとして、よりデータに責任を持ちやすくなる、ということが挙げられるかと思います。BigQueryなどDWHに上がってきたデータ以降のデータ品質を頑張って担保しようとしても元データに問題があればやれることに限度があります。そういった際にこういったバッチ処理まで見れるとシフトレフトして問題にアプローチできる場合もあると思います。
バッチ処理初心者とLLMの付き合い方
「このくらいのバッチ処理であれば、LLMに依頼すれば今だと簡単にできるじゃん?」というのはある意味そうだなと思います。とはいえ、LLMに依頼する際に今回書いたようなバッチ処理で気を付けるべき観点を知っていないと適切に依頼もできないため、経験者が楽をする際には使えるが初心者が何も考えずに使うとよくないコードが生成されることもあります。
バッチ処理で気を付けるべき観点をLLMに出してもらう、というアプローチももちろん考えられます。その上でコードを書いてもらうこともできると思いますが、レビューしたり責任を持ったり、運用したりするのは自分であったりチームです。コードを書くのが自分自身かLLMかはこれからの時代は問われなくなってくるのかもしれませんが「なぜこのコードでいいと思うのか」ということを説明したり責任を持つ、ということは人間が担保する必要があると私は考えています。そういった意味でも、最初は初心者であってもLLMの補助輪なしでバッチ処理とはどういうものか、どういうことに気を付けないと運用で痛い目に合うのか、を知っておいてもらうといいと思います。