🤖

GCP悪戦苦闘のメモ

2024/03/31に公開

Cloud Runを使って定期Pythonジョブを実装する

Cloud Runを使って何をやりたいか

JQuants APIでデータを定期取得して、BigQueryにデータを格納し、後続の処理を定期実行するなど、一連のジョブフローを実装したい。まずはCloud Scheduerで定期的にPub/SubのTopicを発行し、それをサブスクライブしているCloudRun上のジョブが起動される、という実装方式にしたい。

全体のアーキテクチャ

以下は仮ですが、主なロジックやデータベースへのアクセス処理はライブラリとしてcommonなどのディレクトリし、ジョブからコールされるjobx/配下の.pyファイルはcommon配下のモジュールをコールする形のアーキテクチャとする。Cloud Runはコンテナをデプロイする必要があるので、各ジョブディレクトリ配下にそれぞれのジョブ用Dockerコンテナ作成用のDockerfileを作成しデプロイします。

├── common/
│   ├── __init__.py
│   ├── module1.py
│   ├── module2.py
├── job1/
│   ├── main1.py
│   ├── requirements.txt
│   ├── Dockerfile
├── job2/
    ├── main2.py
    ├── requirements.txt  
    ├── Dockerfile

Cloud SDKのインストール

curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-405.0.0-darwin-x86_64.tar.gz
tar -xvzf google-cloud-sdk-405.0.0-darwin-x86_64.tar.gz
./google-cloud-sdk/install.sh

ソースコードをDockerイメージに変換

ローカル環境では仮想環境のPython、環境名venvで実装しているので、まずはrequirements.txtが必要。

# activateしてPythonの仮想環境に切り替え
source ~/workspace/venv/bin/activate
# requirements.txtを生成しPythonの依存ライブラリリストを全て抽出する
pip freeze > ~/workspace/job1/requirements.txt

3.Dockerfileを作る
Pythonは3.12の仮想環境で作っているのでそれと一致するものを作る。これをjob1ディレクトリ配下に作る

# workspace/job1/Dockerfile
FROM python:3.12-slim

WORKDIR /app
COPY . /app

RUN pip install --no-cache-dir --upgrade -r requirements.txt

CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 main1:app

最後の行のmain1:appは、エントリーポイントになるPythonファイルと、その中で宣言しているflaskのアプリケーションオブジェクトで、例えば以下のように冒頭で宣言する。これは、Cloud Runはアーキテクチャ上、Webサービスとして登録が必要のためFlaskを使うためである。以下が例:

app = Flask(__name__)

Cloud RunにデプロイするためのGCP設定

Google Artifact-Registryへプッシュするために、事前準備。まずはArtifact Registryを有効化する

gcloud services enable artifactregistry.googleapis.com run.googleapis.com
# リポジトリを作る
gcloud artifacts repositories create [REGISTRY-NAME] \
    --repository-format=docker \
    --location=us-central1 \
    --description="Docker repository"

Dockerイメージをビルドします。jobxディレクトリに移動して以下を実行する。リージョンを含むURLを指定しているのですが、ここが味噌で、リージョン名がわずかに間違ってハマる人が多いですw気をつけましょう(us-central-1とかにしちゃう人が多い)。また、Macの人は、--platform linux/amd64を付与しないとデプロイで失敗します、しかしエラーがPORT8080が空いてないってひたすら言われるので永遠にハマる人がいますので、気をつけようw

docker build --platform linux/amd64 -t us-central1-docker.pkg.dev/[PROJECT-ID]/[REGISTRY-NAME]/[IMAGE]:[TAG] .

Dockerイメージをコンテナレジストリにプッシュ

各ジョブのディレクトリ(上記ならjobx)に移動して、以下のコマンドによりGoogle Cloud Container Registryへ、作成したDockerイメージをプッシュします。

docker push us-central1-docker.pkg.dev/[PROJECT-ID]/[REGISTRY-NAME]/[IMAGE]:[TAG]
# うまく行くと以下のような感じになる
job1 % docker push us-central1-docker.pkg.dev/[PROJECT-ID]/[REGISTRY-NAME]/[IMAGE]:[TAG]
The push refers to repository [〜〜省略〜〜]
49104467f536: Pushed 
9c7182a51b32: Pushed 
4a1475e42737: Pushed 
f9f0d5a10935: Pushed 
68cf68dc8sf7: Pushed 
106336589f98: Pushed 
9e10a39513g0: Pushed 
d64c46ff90fc: Pushed 
v1: digest: sha256:05024c5fdasf77eaf603234eeab531aa686593f9253bb64d0568f72a33c8e52fa size: 1995

微妙に間違っていると、以下のような404のエラーが出て、超ハマるので気をつけましょう!!

unknown: <!DOCTYPE html>
<html lang=en>
  <meta charset=utf-8>
  <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  <title>Error 404 (Not Found)!!1</title>
  <style>
    *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
  </style>
  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>
  <p><b>404.</b> <ins>That’s an error.</ins>
  <p>The requested URL <code>/v2/[PROJECT-ID]/[REGISTRY-NAME]/[IMAGE]/blobs/uploads/</code> was not found on this server.  <ins>That’s all we know.</ins>

Cloud Runの設定を行う

Cloud Runジョブかがあるので、あとはそれを作るだけです。

参考リンク

認証周りは面倒なことが多い。この辺を見れば解決するだろう。
Artifact Registryの権限設定まわり

Cloud functions第2世代の権限周りで苦戦したことのメモ

CLoud Functionsの第2世代をデプロイし、実行すると権限周りでひたすら失敗した。アクションとしては、以下の2つが必要。

1.Clound Functionsが(Cloud Schedulerにより内部的に使われる)Cloud Run起動元に関する権限の設定が必要リンク
2.Cloud SchedulerからCloud Functions第2世代をHTTPで呼び出す認証の設定が必要リンク

Cloud Run起動元権限の設定

リンク先に以下のような記載がある。

ターゲットが Google Cloud 内にある場合は、必要な IAM ロールをサービス アカウントに付与します。Google Cloud 内の各サービスには特定のロールが必要で、受信側のサービスは生成されたトークンを自動的に検証します。たとえば、Cloud Run と第 2 世代の Cloud Functions 関数では、Cloud Run Invoker ロールを追加する必要があります。

つまり、Cloud Schedulerを実行するプリンシプルに対して、Cloud Runの起動元権限というものを付与する必要がある。Cloud Schedulerを触っていると、???、となるポイントが、実行しているサービスアカウントがどこを見てもわからない。実は、デフォルトのサービスアカウントPROJECT_ID@appspot.gserviceaccount.comが実行しているらしいので、これに対してcloud run起動元権限を付与すれば良い。以下のコマンドを発行した。

gcloud functions add-iam-policy-binding ファンクション名 \
  --member='serviceAccount:プロジェクト名@appspot.gserviceaccount.com' \
  --role='roles/cloudfunctions.invoker'

これでもしPermission Deniedになるようであれば、上記リンク先に記載がありますが、Cloud Run側へのPermission設定も必要なのかも(どっちもやってしまったので、どちらが正確に必要なのか不明)。ちょっとドキュメントがわかりにくいですよね、結局第二世代のCloud Functionの場合は、上記のコマンドだけで良さそうな感じにも読み取れますが、どうなんでしょうか。

HTTP認証の設定

リンク先認証を使用するスケジューラ ジョブを作成するに記載がある。これは、スケジュールの設定を行うところなのだが、よく見るとAuthヘッダーのところにHTTP認証の設定があり、ここでサービスアカウントを指定して設定が可能になっており、デフォルトサービスアカウントを使ってHTTP認証をODICベースでやれば良い。
なお、ODICなのかOathなのか、については、

[Auth ヘッダー] リストでトークンタイプを選択します。一般には OIDC が使用されます。ただし、*.googleapis.com でホストされている Google API は例外で、OAuth(アクセス)トークンが使用されます。

と記載がある。私が今やっているのが一般的なのか、*.googleapis.comでホストされているGoogle APIなのかがよくわからない。調べたところ、以下の通りのもよう。わかりにくいな。いずれにせよODICで良いのだ。

*.googleapis.com でホストされている Google の API(例: Google Cloud Storage, BigQuery など)へのアクセスには、OAuth トークン(具体的には OAuth 2.0 アクセス トークン)を使用します。これらの API は Google の内部リソースにアクセスするため、OAuth トークンが適切です。

Google Cloud Functionsをローカルで開発、テスト、デプロイする

Macで発生するウザいエラー

以下の二つを参考にすると、本来最も簡単にローカルでCloudFunctionsの実行ができるはずだが、仮にデプロイして成功するコードだとしてもMac OSだとうまくいかないケースがある。これはMacのセキュリティに起因するものである。
https://cloud.google.com/functions/docs/deploy?hl=ja
https://dev.classmethod.jp/articles/try-function-framework/

俺たちのChatGPTちゃんによると以下の理由が考えられるとのこと。

  1. 環境変数の設定
    まず、問題が発生しているターミナルセッションで次の環境変数を設定します。これは fork() 呼び出し時の問題を回避するためのものです。
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES

このコマンドを実行後、Python スクリプトを再実行してみてください。

んで、実際に上記を実行の上でCloud Functionsの関数をローカルデプロイしたところ、うまくいきました。

いやーーー、めんどくさw

gcloudコマンドによるデプロイ

gcloudコマンドによるデプロイは以下の通り。元のドキュメントはこちら
ポイントはあまりないが、--sourceを指定省略すると作業ディレクトリが対象になる。ソースコードディレクトリの構造はこちらに記載がある。TRIGGER_FLAGSは、httpなのか、pub/subなのかイベントドリブンなのか、などの関数トリガーを記載するもので、通常はHTTPかPub/Subだけしか使わないと思うが、Eventarcトリガーは個人的に気になっているので使ってみたい。

gcloud functions deploy YOUR_FUNCTION_NAME \
[--gen2] \
--region=YOUR_REGION \
--runtime=YOUR_RUNTIME \
--source=YOUR_SOURCE_LOCATION \
--entry-point=YOUR_CODE_ENTRYPOINT \
TRIGGER_FLAGS

実行サンプルは以下の通り。

% gcloud functions deploy <<省略>> \
--gen2 \
--region=us-central1 \
--runtime=python312 \ 
--entry-point=<<省略>> \
--trigger-http
Preparing function...done.
X Updating function (may take a while)...[Build] Logs are available at
<<省略>>
[INFO] A new revision will be deployed serving with 100% traffic.
You can view your function in the Cloud Console here: https://console.cloud.google.com/functions/details/us-central1/<<省略>>

buildConfig:
  build: <<省略>>
  dockerRegistry: ARTIFACT_REGISTRY
  dockerRepository: <<省略>>
  entryPoint: <<省略>>
  runtime: python312
  source:
    storageSource:
      bucket: <<省略>>
      generation: <<省略>>
      object: <<省略>>
  sourceProvenance:
    resolvedStorageSource:
      bucket: <<省略>>
      generation: '1711848345949372'
      object: <<省略>>/function-source.zip
createTime: '2024-03-30T06:29:55.789815577Z'
environment: GEN_2
labels:
  deployment-tool: console-cloud
name: <<省略>>
serviceConfig:
  allTrafficOnLatestRevision: true
  availableCpu: 167m
  availableMemory: 256Mi
  ingressSettings: ALLOW_ALL
  maxInstanceCount: 100
  maxInstanceRequestConcurrency: 1
  revision: <<省略>>
  service: <<省略>>
  serviceAccountEmail: <<省略>>
  timeoutSeconds: 60
  uri: <<省略>>
state: ACTIVE
updateTime: '2024-03-31T01:27:24.135383692Z'
url: https://us-central1-<<省略>>.cloudfunctions.net/<<省略>>

cloud functionsの基本メモ

備忘録として参考文献だけ貼っておく
https://cloud.google.com/functions/docs/writing?hl=ja#directory-structure-python

Python周りのお役立ちメモ

仮想環境関連

Pyenvの最も基本的な使い方のメモ

pyenvでインストールできるPythonのバージョン一覧を確認する

$ pyenv install --list

pyenvの環境にインストールされているPythonを確認する

$ pyenv versions

こんな感じで一覧が表示される。今の環境は*の部分が現在の環境になってる。

(py_virtual_env) user_name@user_name bin % pyenv versions
  system
  3.10.13
* 3.11.0 (set by /Users/user_name/.pyenv/version)
  3.12.0

pyenvインストール後、Pythonのバージョンを指定してインストールする。

$ pyenv install 3.7.3

PC起動後、特に何もしていない場合のバージョンは、以下のとおりglobalのキーワードで指定する。
これにより、現在のPythonの環境のバージョンが変化する。

$ pyenv global 3.5.1

所望ぼPythonのバージョンに切り替えた状態で、以下のコマンドで仮想環境を作成すると、そのバージョンで仮想環境が作られる。

$ mkdir 仮想環境名
$ cd 仮想環境名
$ python -m venv 仮想環境名

作成すると、仮想環境ディレクトリ配下に必要ファイルが作られる。
bin配下にactivateスクリプトが作成され、これを実行することで仮想環境が切り替えられるようになる。
具体的には、仮想環境を指定してPythonの実行環境を切り替えるには以下の通りコマンドを発行する。

source <仮想環境名>/bin/activate

これで所望の仮想環境に切り替わる。

削除

ディレクトリ丸ごと、インストール先のディレクトリを削除すればOK

参考文献

1
2
3

Discussion