こんにちは、データプラットフォームチームでデータエンジニアをしています、鳥山(@to_lz1)です。
本記事は datatech-jp Advent Calendar 2023 11日目の記事です。
データ基盤を運用する皆さま、ジョブの実行管理基盤には何を用いているでしょうか?
様々なOSSやサービスが群雄割拠するこの領域ですが、Google Cloud Platform(以下、GCP) のCloud Composerは選択肢の一つとして人気が根強いのではないかと思います。
一方、同サービスには時折「運用がつらい」とか「費用が高い(故に、開発者が気軽に触れる開発環境を用意できない)」といった言葉も聞かれるように思います*1。
自分も入社以来触ってきて、そういった側面があることは完全には否定できないと考えています。しかしそれでも、ツールを用いてそうした負担を大幅に軽減することは可能です。今回はGCPが公式に提供するCloud Composerローカル開発CLIツールを実際の開発フローに導入したことと、それによって得られたメリットをご紹介します。
Cloud Composerローカル開発CLIツールについて
Cloud Composerローカル開発CLIツール(以下、ローカル開発ツール)は、GCPが公式に提供するローカルAirflow環境構築ツールです。
Composer ローカル開発 CLI ツールは、Airflow 環境をローカルで実行することにより、Cloud Composer 2 向けの Apache Airflow DAG 開発を合理化します*2。
とある通り、Cloud Composer 2 向けのツールになっているのでその点はご注意ください*3。リポジトリは以下です。
利用手順については既存の記事が多くあるため割愛しますが、Python製のスクリプトをCLIツールとして使えるようにしているのみで、シンプルな作りです。グローバルのPython環境を汚したくない場合は適宜pyenvやvenvを利用すると良いでしょう。公式ページの手順では pip install
をいきなり実行する手順となっているため、この点が気になる方はDevelopers.IOの以下記事を参考にすると良いと思います。
「Composer ローカル開発 CLI ツール」を試してみました | DevelopersIO
導入前の課題
私たちのプロジェクトでは依存管理に Poetry を使っていましたが、ローカル開発ツールを導入するより前は以下のような課題がありました。
Cloud Composer環境の再現がつらい
Cloud Composerには特定のPython依存関係をインストールすることができます。
Cloud Composer 用の Python 依存関係をインストールする | Google Cloud
ただ、Cloud Composerには各々のバージョンでデフォルトで入っているPyPIパッケージ群が存在します。そのリストは全て公開されています*4が、自分たちが追加でインストールしたパッケージが既存のパッケージやその推移的依存まで含めてどのような影響を及ぼすかは実際にやってみなければわかりません。
テストやCIがうまく動くことを保証するためにCloud Composerと同じ環境を用意したい...わかります。Cloud Composerの依存は公開されているのだから、同じものを pyproject.toml に列挙すれば安定性を確保できそうだ...その通りですね。
が、Cloud Composerの依存はリストを見ると分かりますが300以上あるのです。これを依存管理に全て加えた結果、私たちの pyproject.toml は大変見通しが悪いものになっていました。
それでもテストやCIは動くわけで、すぐになんとかしないといけないわけでもない。でも依存の追加や変更は大変にやりづらい...そんな状況が続いていました。
Cloud Composerのアップグレードがつらい
Cloud Composerのアップグレード頻度は比較的早く、リリースから12ヶ月でサポートから外れるため頻繁なアップグレードは必須といえます。
Cloud Composer のバージョニングの概要 | Google Cloud
しかし、先述したような pyproject.toml がある状態で「Cloud Composerをアップグレードしよう」となったとすると...、そう、列挙した依存を全て更新する必要が生じます。
また、関連パッケージが大量にあるため必然的にCIも遅くなります。それで更新を待って検証環境に適用して切り戻しが必要になった...などなると大変です。こうした事情からCloud Composerのバージョンアップ作業も「怖い」「リスクが高い」作業と見なされがちでした。
向き不向きに関する考察
ローカル開発ツールでできることと、それが上述したような現実の開発フローにおける課題にどれくらいマッチするかを考えてみます。
PyPIパッケージのインストール・検証🙆
パッケージの追加や更新がうまくいくかやってみないと分からないのなら試してみよう、となるのですが、Pythonの依存関係インストールは「Composer環境の更新」というオペレーションにラップされており、一度の更新に数十分かかることはザラです。
ライブラリの追加や削除のたびにこれほどの時間待たされるのは、フィードバックループが長すぎると言えます。ローカル開発ツールがあれば「Cloud Composer環境の再現」という責務をツール側にある程度寄せられるわけですから、改善に踏み切って大きく体験向上する余地がありそうです。
バージョンアップの検証🙆
Cloud Composerのバージョン変更もComposer環境の更新が必須です。環境がまるまる壊れることは流石にないにしても、何かトラブルが起こる可能性があるのならばそれを早期に検知したいところです。
ローカル開発ツールはGCRリポジトリにて管理されるGoogle公式のCloud Composerイメージを使用します。執筆時点で2023年11月にリリースされたcomposer-2.5.1-airflow-2.6.3までが使えるようになっており、メンテナンスの状況も今のところ大きな不安がありません。
❯ composer-dev list-available-versions --include-past-releases ╷ Image version │ Release Date ╶──────────────────────────────┼──────────────╴ composer-2.5.1-airflow-2.6.3 │ 12/11/2023 composer-2.5.1-airflow-2.5.3 │ 12/11/2023 composer-2.5.0-airflow-2.6.3 │ 05/11/2023 composer-2.5.0-airflow-2.5.3 │ 05/11/2023 composer-2.4.6-airflow-2.6.3 │ 23/10/2023 composer-2.4.6-airflow-2.5.3 │ 23/10/2023 composer-2.4.6-airflow-2.4.3 │ 23/10/2023 composer-2.4.5-airflow-2.5.3 │ 11/10/2023 composer-2.4.5-airflow-2.4.3 │ 11/10/2023 composer-2.4.4-airflow-2.5.3 │ 05/10/2023
Cloud Composerのアップグレードに先立ち、スモークテスト的にDAGがparseできるか等を事前検証するのも良い使い所と言えるでしょう。
向かないと思われる用途
ローカル開発ツールではアプリケーションのデフォルト認証情報(ADC)を用いて認証が行われます。
アプリケーションのデフォルト認証情報を設定する | Google Cloud
ADCを利用する場合は、自身のGoogleアカウントに関連付けられた認証情報を用いて他のサービスを利用するのが一般的です。よって、サービスアカウントに正しく権限を付与できているか、といった検証には向かないでしょう。
また、ローカル環境上に構築するため、当然ながらインフラや実行環境の面では様々な制約があり、例えばExecutorはSequential Executorに固定されます。
これは、DBをsqliteにすることで起動を簡素化しているためではないかと思われます。こうした事情から、GKEクラスタ上でのDAG実行パフォーマンスを測定するといった用途にもこのツールは不向きと判断できます。
以上のような検証要件がある場合は、GCP上に構築した検証環境を利用する方が適切でしょう。
総合的に見て、ローカル開発ツールの特性は私たちのチームが持つ課題であればうまくマッチしてくれそうなので、この辺りに留意しながら導入を進めました。
ローカル環境構築のスクリプト化
ローカル開発ツールはCLIとして提供されているため利用も再利用も簡単ですが、私のチームでは再利用性を更に高めるためにローカル環境構築用のスクリプトを用意しました。特に、
- 新しいバージョンで環境構築する際の工数最小化
- Poetryを用いた依存管理との両立
といった点を意識しています。
#!/bin/bash set -eu export DAG_PROJECT_ROOT={{ DAGが置いてあるリポジトリのルートへのPath }} export DAG_PATH={{ DAG_PROJECT_ROOTからDAGが置いてあるディレクトリへのPath }} export LOCAL_DEV_TOOL_PROJECT_ROOT={{ ローカル開発ツールのディレクトリへのPath }} export COMPOSER_VERSION=2.x.x export AIRFLOW_VERSION=2.x.x export LOCAL_ENV_NAME=localdev-${COMPOSER_VERSION//./_}-${AIRFLOW_VERSION//./_} cd "$LOCAL_DEV_TOOL_PROJECT_ROOT" if [ ! -d .venv ]; then python -m venv .venv fi source .venv/bin/activate pip install . composer-dev create \ --from-image-version "composer-${COMPOSER_VERSION}-airflow-${AIRFLOW_VERSION}" \ --dags-path "$DAG_PROJECT_ROOT/$DAG_PATH" \ "$LOCAL_ENV_NAME" # 後述 1. Variablesのダミーの準備 cd $DAG_PROJECT_ROOT cp variables.composer-local.env "$LOCAL_DEV_TOOL_PROJECT_ROOT/composer/$LOCAL_ENV_NAME/variables.env" # 後述 2. Poetry Export poetry export \ --without-hashes \ --without-urls \ | awk '{ print $1 }' FS=';' \ > "$LOCAL_DEV_TOOL_PROJECT_ROOT/composer/$LOCAL_ENV_NAME/requirements.txt" cd "$LOCAL_DEV_TOOL_PROJECT_ROOT" && composer-dev start "$LOCAL_ENV_NAME"
ポイントは大きく2点あります。
1. Variablesのダミーの準備
Airflowには任意のkey-valueのペアを設定できるVariableという機能があります。
Variables — Airflow Documentation
DAG中でこのVariableを参照している場合、何かしらの値を設定しておかないと参照した時点でエラーとなります。以下のようなファイルをローカル開発ツールの環境配下にvariables.envという名前で設置してあげると環境変数を経由してダミー値を設定できるので、起動やDAGの実行が安全に行えます。
AIRFLOW_VAR_HOGE=dummy AIRFLOW_VAR_FUGA=dummy
2. Poetry Export
Cloud Composerの依存の更新を行う場合には、Cloud Composerにrequirements.txt形式のファイルを渡す必要があります。Poetryにはrequirements.txt形式のテキストファイルを出力する機能があり、私たちのチームではこれを採用しています*5。
このコマンドを使って、プロジェクト内ではpyproject.tomlに依存を書き、Cloud Composerに適用するときだけはrequirements.txtを用いる、といった運用が可能です。
このとき注意したい点ですが、poetry export で出力したままのrequirements.txtには以下のようなenvironment markerとhashが入っています。
boto3==1.26.72 ; python_full_version == "3.8.12" \ --hash=sha256:5d6e19d148c4a9d5d85f0d96570d11264f23db610f1e3c9a8b7e8b6898424691 \ --hash=sha256:990997248716f12b296d7d30b3119a93347d73b7a4831c015e53aaebbd074a77 botocore==1.29.165 ; python_full_version == "3.8.12" \ --hash=sha256:6f35d59e230095aed7cd747604fe248fa384bebb7d09549077892f936a8ca3df \ --hash=sha256:988b948be685006b43c4bbd8f5c0cb93e77c66deb70561994e0c5b31b5a67210
Cloud Composerはこれを受け付けてくれないため、スクリプトでは --without-hash
等のオプションと awk
コマンドでの加工を挟んで以下のような形式に変換しています。
boto3==1.26.72 botocore==1.29.165
上のような形式であればローカル環境であってもGCP上の環境であっても依存の反映が可能です。私たちのプロジェクトでは同様のプロセスをCIにも組み込んで、developにマージされた依存の変更が即座に反映できるようにしています。
導入した結果
先述した通り、私たちの pyproject.toml にはCloud Composerが必要とする依存とカスタムで追加したい依存とが混在しており、どれが必要でどれが不要かも判断が難しくなっていました。今回このツールを導入することでこうした切り分けがスムーズに進行でき、依存を大幅に整理することができました。
Classiではrenovateも広く導入している*6ため、「PRが上がってくるけどこれはCloud Composerの依存だから上げても意味ない...」といった事象がたびたび発生していたのですが、こうした無駄も激減したと思っています。また、依存を変更した際のCIが高速化した点も嬉しいポイントです。
Cloud Composerのバージョンアップ作業も、依存の整理とローカル開発ツールの活用によって実働1日未満で行えることが実証できました。今後のアップグレードへの追随もかなり安定して進められるだろうと期待しています。
一方、今後の課題として以下のような点は残りそうだと考えています。
残った依存とrenovateとの相性
単体テストやコード補完を正しく動かすために、Airflowに関連する依存をいくつかpyproject.tomlに記述している状態です。
Airflow本体などコアなライブラリに関してはrenovateのPRを抑止しているのですが、推移的依存まで網羅してignoreDepsのリストに列挙するのは現実的でなく、たまに意味のないPRが上がってきてしまっています。この点はより良い運用を模索しても良いかもしれません。
そもそも依存をインストールすべきか?について
Cloud Composerを使う際は、そもそもカスタムパッケージを一切インストールせず、各タスクの実行を別のDockerイメージに固める方が良いという意見もあると思われます。これについては、新たなタスクはDockerイメージとして開発してKubernetesPodOperatorで実行する、という方向を緩やかに推進中です。
しかし、軽量なパッケージを入れることで解決する課題があるのならその手段は残しておきたいという思いもまたあり、今回ご紹介した運用はしばらく活躍し続けるでしょう。
まとめ
以上、Cloud Composerローカル開発ツールを日常の開発に取り入れた実例と、その効果について紹介しました。ローカルゆえの限界もあるものの、用途によっては利便性・実用性とも十分に高いツールだと考えています。
昨今Cloud Composerの利用しているデータ基盤の記事も増えてきたように思います。比較的歴史のあるツールではありますが、version2が比較的利用しやすいこともあって再評価の機運があるのではないでしょうか。運用の一手間をツールで解決する例として、Cloud Composerに触れている、または触れるかもしれない皆さんの参考になれば幸いです。
*1:例えば、Cloud Composer の辛さと、それに負けない開発フローの構築 ~Composer はなぜ辛い?編~
*2:https://cloud.google.com/composer/docs/composer-2/run-local-airflow-environments?hl=ja
*3:弊社では2年ほど前にCloud Composer2へのアップグレードを終えています。その時の記事はこちらです
*4:https://cloud.google.com/composer/docs/concepts/versioning/composer-versions
*5:将来のバージョンでplugin側に完全に切り離されるようなので、利用する場合はpluginのインストールを忘れずに