[GCP] Cloud FunctionsでPythonプログラムを動作させるときの備忘録

Cloud FunctionsでPythonを動かそうとして何日も溶かしてしまったので、
未来の自分がこれ以上時間を溶かさないように残しておきます。

## ログ

処理が正常終了したときには print で出力したログも Cloud Logging から確認できますが、
例外が発生した時には except 句内の print は Cloud Logging に詳細が出力されず `crash` という情報だけが残ります。
これではヒントが無く、どの処理でどういうエラーが出たのか全くわかりません。
logging モジュールを使うと出力したい場所でログがしっかり出力され、ログレベルに合わせて Cloud Logging の行ごとのアイコンも変わるので、使い勝手が良くなります。

```python
import logging
import traceback

def main():
    try:
        # 処理
        logger.info("output by logger")
        print("output by print") # 処理が正常終了すると出力される
    except:
        traceback.print_exc() # Cloud Loggingには出力されない
        logger.error("error by logger: ", exc_info=True)
        raise
```

## GCP / Googleの他のサービスとの認証周り

ADC (https://cloud.google.com/docs/authentication/production?hl=ja) があるので、 `google.auth.default()` で credential 情報を取得できます。
認証先のサービスによってはCredentialのscope情報を付与したり、必要に応じた実行サービスアカウントへのロールの設定をしておきます。

ローカルで開発する際は credential 情報を json ファイルで保存しておき、環境変数 `GOOGLE_APPLICATION_CREDENTIALS` に json ファイルのパスを指定するだけで `google.auth.default()` の認証が通るので、ローカルと Cloud Functions での処理を分岐させる必要はありません。

```python
import google.auth
from google.cloud import kms_v1
import gspread

def spreadsheet(url):
    credentials, _ = google.auth.default(scopes=['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive'])
    gc = gspread.authorize(credentials)
    return gc.open_by_url(url)

def kms_client():
    credentials, _ = google.auth.default()
    return kms_v1.KeyManagementServiceClient(credentials=credentials)
```

## Cloud Functions API から新規作成する際の 400 Bad Request エラーの原因調査

Python プログラムの Tips から少し話がそれますが、terraform を使って Cloud Functions に関数をデプロイするときにハマったので、合わせて残しておきます。

WebのGCPコンソールから関数を作成するときにはこの問題には当たらないとは思いますが、terraform で plan では問題なく差分が見れているのに apply すると

```
400 Bad Request INVALID_ARGUMENT The request has errors
```

↑こんな感じのメッセージだけ出力されて、どの項目に不備があるのかわかりません。

terraformの場合、環境変数に `TF_LOG=debug` を追加しても、得られる情報はあまり変わり有りません。
ただ、この `TF_LOG=debug` で出力されたリクエストボディの情報を控えておきます。

cloud functions api の [リファレンスページ](https://cloud.google.com/functions/docs/reference/rest/v1/projects.locations.functions/create?hl=ja) の **Try this API** に  `TF_LOG=debug` で出力したリクエストボディを入力して EXECUTE してみると、 `400 Bad Request INVALID_ARGUMENT The request has errors` 以外にも不備のあるフィールド名と、不備の詳細が message に出力されると思います。


## 定義済みの環境変数

毎回 print で出力して確認しているので、備忘録として出力をそのまま残しておきます。

```python
environ({
  'PORT': '8080',
  'X_GOOGLE_WORKER_PORT': '8091',
  'NODE_ENV': 'production',
  'X_GOOGLE_ENTRY_POINT': 'entrypoint',
  'FUNCTION_TRIGGER_TYPE': 'HTTP_TRIGGER',
  'GCLOUD_PROJECT': 'project_id',
  'DEBIAN_FRONTEND': 'noninteractive',
  'X_GOOGLE_FUNCTION_MEMORY_MB': '256',
  'FUNCTION_TIMEOUT_SEC': '60',
  'SUPERVISOR_INTERNAL_PORT': '8081',
  'ENTRY_POINT': 'entrypoint',
  'X_GOOGLE_LOAD_ON_START': 'false',
  'X_GOOGLE_FUNCTION_REGION': 'asia-northeast1',
  'X_GOOGLE_FUNCTION_VERSION': '1',
  'WORKER_PORT': '8091',
  'VIRTUAL_ENV': '/env',
  'X_GOOGLE_GCP_PROJECT': 'project_id',
  'CODE_LOCATION': '/user_code',
  'PWD': '/user_code',
  'X_GOOGLE_CONTAINER_LOGGING_ENABLED': 'false',
  'FUNCTION_NAME': 'dummy_function',
  'X_GOOGLE_CODE_LOCATION': '/user_code',
  'FUNCTION_MEMORY_MB': '256',
  'X_GOOGLE_FUNCTION_IDENTITY': 'dummy_service_account@project_id.iam.gserviceaccount.com',
  'FUNCTION_IDENTITY': 'dummy_service_account@project_id.iam.gserviceaccount.com',
  'FUNCTION_REGION': 'asia-northeast1',
  'GCP_PROJECT': 'project_id',
  'X_GOOGLE_FUNCTION_NAME': 'dummy_function',
  'X_GOOGLE_SUPERVISOR_HOSTNAME': 'supervisor',
  'HOME': '/tmp',
  'SUPERVISOR_HOSTNAME': 'supervisor',
  'PATH': '/env/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
  'X_GOOGLE_GCLOUD_PROJECT': 'project_id',
  'X_GOOGLE_FUNCTION_TRIGGER_TYPE': 'HTTP_TRIGGER',
  'X_GOOGLE_SUPERVISOR_INTERNAL_PORT': '8081',
  'X_GOOGLE_FUNCTION_TIMEOUT_SEC': '60',
  'LC_CTYPE': 'C.UTF-8',
})
```

## まとめ

ログに有用な情報を出力できると不具合調査が楽になる。
認証周りは `google.auth` に頼って、サービスアカウントのロール設定で細かく制御する。

まだまだハマリポイントありそうな予感しかしないが、勘所は掴んだ気でいます。