Lean Baseball

No Engineering, No Baseball.

ちょっと気が早いですが, Cloud Functions第2世代を試してみた - 現バージョンからの移行とその注意点

今日のテーマ

仕事もプライベートもよくGoogle Cloud(GCP)を使っている人です.

最近はGoogle Cloudの資格取得, 頑張ってます*1.

  • ちょっとしたSlack Botを作りたい
  • ちょっとしたデータ収集クローラー(Webクローラー&スクレイピング)がほしい
  • ちょっとした「CSVとかJSONのファイルをBigQueryに放り込む)簡単なETLがほしい

なんて時に, Cloud FunctionsというGoogle Cloudのサーバレスな従量課金FaaS(Function as a Service)でシュッと関数作って運用しているのですが, つい最近そんなCloud Functionsの第2世代が発表されました.

cloud.google.com

このブログを執筆した2022年6月12日時点ではpre-GA(いわゆるプレビュー・正式リリース前の状態)なのでどう使うかは使う方の自己責任ですが, 面白そうなのでひとまず使ってみました.

このエントリーでは,

  • 現在のバージョン(便宜上, このエントリーでは第1世代と呼びます)から移行(Migration)したときにやったこと
  • 第2世代と第1世代の違い
  • 使い所・今後の展開など, 私個人の所感・感想

をサクッと残したいと思います.

TL;DR

現バージョンからの移行は思ったよりも楽勝だったので, 気になる人はまず試すと良いのではないでしょうか?GA(正式リリース)前だとしても.

ただし, まだまだ制約事項があるので移行前にちょっと考えたほうがいいかも.

このエントリーの前提条件

大前提として, Cloud Functionsそのものの解説はしません.

cloud.google.com

知らないよ, という方は上記の公式リンクを読んで頂き, 可能ならQuick Startをすると良いかもです.

AWSを普段使いしている方は, 「AWS LambdaのGoogle Cloud版」と思ってもらえれば読めるんじゃないかなって思います*2.

このエントリーにおけるCloud Functionsの実装はPython(3.9)を前提としております.

Cloud Functionsは,

  • Node.js
  • Python
  • Go
  • Java
  • .NET(C#)
  • Ruby
  • PHP

以上の言語をサポートしており, 言語が変わってもこのエントリーの知見はそのまま活かせる可能性が高いですが, 必ずしも全てをフォローするものではない, ということだけご了承ください.

おしながき

第2世代と第1世代の違い

第2世代の仕様はGoogle Cloud公式のガイドに載っているのでそちらに譲りますが, 私が見た限りの明確な違い上げると,

  • 1リクエスト当りの実行時間が延びた. 第1世代では最長9分間の実行時間だったのが, 第2世代では60分間に延びた.
  • 第1世代と比べて大きなインスタンスを使える. メモリ・CPU共に大きく使えるので, 第1世代より並行処理だったりリソースを使う処理が実行できる.
  • 一つの関数内での同時実行できる処理が増えた. ベースとなるインフラがCloud Runに変更された為実現した機能(と思われる), 参考情報はこちら, なおPythonではこの恩恵には(pre-GA版の時点では)授かることができません(と, 第2世代のページに記載されています).
  • 最小インスタンス数の定義に対応(≒コールドスタートを最小限にすることができる), 複数リビジョンでのトラフィック管理対応(≒いわゆる, ABテストやカナリアリリース的な運用が可能)

一番わかり易いユースケースだと, 実行時間が9分から60分に延びたのは非常に大きいと思います.

データ基盤のちょっとしたETLだったらCloud Functions上で作れてしまうのでは?と思いますし.

Pub/Subをトリガーとした長めの非同期処理(≒事実上のバッチ処理)もCloud Fuctionsでやりやすくなりそうだな...とも思います.

何が言いたいかと言うと, 実行時間が延びたことによりユースケースに幅が広がった(≒使えるシチュエーションが増えた)って所でしょうか.

また, WebサイトやWeb APIを構築・運用する方の視点だと,

  • 複数リビジョンでのトラフィック分割は魅力的. ちょっとしたABテストやカナリア的なリリース・配布ができるように(ちなみに試していません)
  • インスタンスの細やかなスケールに対応. 細かい設定ができるのが美味しい(これもまだ試してはいない).

この辺は相当イケてる気がします.

...と, ここまで読んだ方で「え?それってCloud Runと変わらないのでは?」と思った方もいるでしょう.

Cloud Functions第2世代はCloud Functionsの皮をかぶったCloud Runである(なぜならインフラがCloud Runだから), と思って頂いて良いと思います(個人の見解です).

第2世代への移行 - pre-GA版

このブログを執筆した2022年6月12日時点での移行(Migration)についてちょこっと触れます.

試しに昨年作った「Pub/Subをトリガーに, Baseball Savantのトラッキングデータ(CSV)を取得し, Cloud Storageに保存するクローラー」をCloud Functions第2世代に移行してみました.

shinyorke.hatenablog.com

こちらで出てきたGCFタスクのアプリが対象となります.

ちなみに移行前のmain.pyの実装はこちらです(mlb_statcast_exporter 関数がPub/Subのエントリーポイント).

from datetime import datetime, timedelta
import base64

import pandas as pd

from savant import query, PlayerType
from gcp.storage import Storage

storage = Storage()


def extract(player_type: PlayerType, game_dt: datetime):
    """
    crawl & upload
    :param player_type: Player Type
    :param game_dt: Game Day
    """
    df = pd.read_csv(query(player_type, game_dt, game_dt))
    storage.upload_from_string(
        path=f'statcast/{game_dt.strftime("%Y-%m-%d")}',
        filename=f'{player_type.value}.csv', text=df.to_csv(index=False)
    )


def mlb_statcast_exporter(event, context):
    """
    Cloud Pub/Sub entrypoint
    :param event: pub/sub event
    :param context: pub/sub context
    """
    game_dt = base64.b64decode(event['data']).decode('utf-8')
    game_dt = datetime.strptime(game_dt, '%Y-%m-%d')
    run(game_dt)


def run(game_dt: datetime):
    """
    run crawler
    :param game_dt: Game Day
    """
    extract(player_type=PlayerType.PITCHER, game_dt=game_dt)
    extract(player_type=PlayerType.BATTER, game_dt=game_dt)

Functions Frameworkを導入する

最近のCloud Functionsの推奨に従い, Functions Frameworkを導入しました.

cloud.google.com

github.com

これはCloud FunctionsやCloud Run(含むCloud Run for Anthos*3)で, 「HTTPSにせよPub/Subにせよ, Cloud Eventsで対応してるアプリケーションをシュッと軽く作って動かせるよ」という便利なWeb Frameworkです&当面の間, Cloud FunctionsやCloud Runを使う前提では必須になるものと思われます(ちなみに他言語の実装も存在します).

マイグレーションそのものは秒で終わりました, 先に完成したコードをお見せします.

from datetime import datetime
import base64

import pandas as pd
import click
from functions_framework import cloud_event

from savant import query, PlayerType
from gcp.storage import Storage

storage = Storage()


def extract(player_type: PlayerType, game_dt: datetime):
    """
    crawl & upload
    :param player_type: Player Type
    :param game_dt: Game Day
    """
    df = pd.read_csv(query(player_type, game_dt, game_dt))
    storage.upload_from_string(
        path=f'statcast/{game_dt.strftime("%Y-%m-%d")}',
        filename=f'{player_type.value}.csv', text=df.to_csv(index=False)
    )


@cloud_event
def mlb_statcast_exporter(event):
    """
    Cloud Pub/Sub entrypoint
    :param event: pub/sub cloud_event
    """
    game_dt = base64.b64decode(event.data["message"]["data"]).decode('utf-8')
    game_dt = datetime.strptime(game_dt, '%Y-%m-%d')
    run(game_dt)


def run(game_dt: datetime):
    """
    run crawler
    :param game_dt: Game Day
    """
    extract(player_type=PlayerType.PITCHER, game_dt=game_dt)
    extract(player_type=PlayerType.BATTER, game_dt=game_dt)

変更したのは,

  • Functions Frameworkをインストール(pipなど, 私はpoetryで管理しているのでpoetryで入れました)
  • from functions_framework import cloud_event これで必要な関数をimport
  • エントリーポイントの mlb_statcast_exporter 関数をちょこっと書き換え

書き換えたと言っても, cloud_event をデコレータで指定, 引数を変更しただけです.

@cloud_event
def mlb_statcast_exporter(event):
    """
    Cloud Pub/Sub entrypoint
    :param event: pub/sub cloud_event
    """
    game_dt = base64.b64decode(event.data["message"]["data"]).decode('utf-8')
    game_dt = datetime.strptime(game_dt, '%Y-%m-%d')
    run(game_dt)

(勿論関数の作りによりますが)移行そのものはメッチャ楽かつ, Cloud Functionsの第1世代でも使えるので書き換えだけでも早めにしていいかもしれません.

デプロイする

デプロイのコマンドの差分を見てみましょう.

まず, マイグレーションする前の第1世代のコマンドはこちらとなります.

gcloud functions deploy mlb_statcast_exporter \
--trigger-topic mlb_statcast \
--runtime=python39 \
--region=asia-northeast1 \
--memory=256MB \
--env-vars-file .env.yaml \
--timeout=540

こちらは第2世代だとこうなりました, beta functions deploy とbetaが必要かつ, --gen2 と第2世代である旨を指定する必要があります.

gcloud beta functions deploy mlb-statcast-exporter \
--gen2 \
--runtime python39 \
--trigger-topic mlb-statcast \
--region asia-northeast1  \
--entry-point mlb_statcast_exporter \
--memory 256MB \
--env-vars-file .env.yaml \
--source .

(ハマりどころ含めて)差分を紹介すると,

  • --entry-point , --sourceを明示的に指定
  • --timeout 引数の削除(いらなくなった)
  • _区切りのスネークケースだと何故か怒られたので, -区切りに変更
  • =区切りはいらないっぽいのでスペースで空けた

これはもう, 「そういうモノなので」としか言いようがないです&CI/CD(GitHub Actionsでデプロイなど)はまだ試していないのでこれは今後の宿題にしようかと思ってます.

ここまで抑えておけば, ひとまず使うことができます!

現時点で出来ないこと - pre-GA版

pre-GAで出来ないこともあります.

  • Secret Managerが現時点では使えない(少なくともGUIから指定できない)
  • いくつかのpre-GA版制約, 詳細はこちら

Secret Managerが使えない(今は)

これがもっともらしい現時点(pre-GA, 2022/6/12現在)でのウィークポイントかもしれません*4.

秘密鍵を環境変数に持たせたくないのでSecret Managerから参照を...というアプローチには未対応(ちなみに第1世代は利用可能です).

これは地味に痛くて, 今回挙げたサンプル関数とは別の関数をマイグレーションしたときにSecret Managerが使えずしょんぼりしました.

これはGA版ではどうにかなってほしいのと, (Cloud Functionsに限らず)セキュアなアプリケーションの管理でSecret Managerが使えないのは痛いのでどうにかなってほしい.

ちなみにAPI(コマンド)での有無は確認してないので, そっちだと使えるかも?

pre-GA版の制約

こちらに載ってるやつでいくつか上げると,

  • 使えるリージョンが限られている. 例えば東京リージョン(asia-northeast1 )は使えるが, 大阪リージョン(asia-northeast2)は未提供.
  • Eventarcトリガーで動くものでまだ未対応のものがある. 例えばPub/Sub, HTTPS, 監査ログトリガーは動くが, Firebaseなどは未対応.

これらはページにあるとおり, 「pre-GAなので」っていう文脈も大いにありそうなので待てばなんとかなりそうな気がします.

結び

というわけで, 「ちょっと気が早いですが, Cloud Functions第2世代を試してみた」をテーマにちょっとしたTipsを残しました.

Cloud FunctionsやAWS Lambdaといった関数系サーバレスな環境は便利かつよく使うケースも多いかと思う(少なくとも自分の個人開発では縦横無尽に活躍している)ので, 今後も新しい情報をウォッチしつつアップデートしていきたいと思います.

Cloud Functions第2世代が(Secret Managerを使える状態で)GAになったら嬉しいな...Googleさんよろしくお願いします!

おまけ - Google Cloudをいい感じに使う

いつか独立した記事で書きたいのですが, いい感じに使うコツとして.

  • とにかく手を動かして開発してみる
  • ベスプラを知る

だと思っていて, ベスプラについては最近こちらの書籍を参考にして色々学びました.

*1:きっかけは会社のオススメだった訳ですが, やってみると意外と面白いので, 悪い気持ちではないです.

*2:個人的には知らない方にはこういう説明していますが, そんなにズレていない認識ではいます. ユースケースはほぼ一緒なので.

*3:AnthosのGKEクラスタ上でホスティングするCloud Runの事で「Cloud Runをフルマネージドじゃなくて自分でリソース調整してホストしたいよー」というユースケースに応えるためのもの.

*4:これは明確に不便でした, 最もこれがサービスアカウントをどうにかするとかだったらWorkload Identityとかを使う抜け道もありそうな予感はしています, 調べてないけど.