Google Cloud Run FunctionsとMongoDBを使用してPythonでWebAPIを作ってみる

# はじめに
初めての投稿です。何を書こうか考えましたが、業務関連はいろいろと書きにくいので、プライベートのことにしました。

我が家はなんちゃってIoTハウスです。
ラズパイに温湿度センサを接続して、測定値をサーバーにアップ、LINEBotから確認できるようにしています。またエアコンのON・OFFもLINEから制御できるようにしています。

現状はOracleCloudのVM上にDB、AP、WEBサーバーを立てて運用していますが
一通りサーバーレスで構築し直したいと思っています。
今回はその練習として、TODOリストをDBへ記録、取得するAPIを作ってみました。
~~タスクの完了登録はできません~~

意外と情報は少なく、ちょっとしたことで詰まりました。
皆さんCloud RunやApp Engineの方を利用しているのかな？


コードは以下においてあります

https://github.com/kbecmint/CloudRunFunctions



# 前提条件
* gcloudのインストール
  + https://cloud.google.com/sdk/docs/downloads-interactive?hl=ja#linux-mac
  + https://qiita.com/G-awa/items/e6904b040caa0096fba0#%E5%88%9D%E6%9C%9F%E8%A8%AD%E5%AE%9A
* MongoDB Atlasのサインアップ
  + https://qiita.com/n0bisuke/items/4d4a4599ee7ce9cf4fd9


# 開発環境
ちょっと特殊でOracle cloud上のVMに開発マシンを立てて、VSCodeからリモートで接続しています。クライアントは状況に応じてWindows10だったり11だったりします。

```shell-session
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.4 LTS
Release:        22.04
Codename:       jammy
```

しかもアーキテクチャはarmです。デプロイ時に1点だけ詰まりましたが、基本的にx86との違いを感じたことは殆どありません。
この環境構築も後日記事にできればと思います。

```shell-session
$ uname -m
aarch64
```



# Flaskアプリケーションの実装
## ファイル構成
```
tutorialRun
├ .env.yaml
├ .gcloudignore
├ .gitignore
├ deploy.sh
├ localDebug.sh
├ main.py
├ mongo.py
L requirements.txt
```

# MongoDB Atlasの接続文字列取得

[こちら](https://www.mongodb.com/ja-jp/docs/languages/python/pymongo-driver/get-started/create-a-connection-string/)を参考に接続文字列を作成します。

作成した文字列をローカルデバック用の環境変数に保存しておきます。
クラウド用の設定ファイルは後で別に作成します。
```.env
CONNECTION= "mongodb+srv://<username>:<password>@myapp.abcd.mongodb.net/?retryWrites=true&w=majority&appName=myapp"
```

## コードの抜粋

### datetimeオブジェクトのシリアライズ化
json.dumpsにdatatemeをわたすとTypeErrorが発生します。
そのため変換関数を定義します。
```python : main.py
def json_serial(obj):
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()
    raise TypeError(f"Type {obj} not serializable")
```
[参考サイト](https://qiita.com/takuya-andou/items/59c493227629f63a9ddc)

### 環境変数の取得
```python : mongo.py
CONNECTION = os.environ.get("CONNECTION")
if CONNECTION is None:
    # ローカルデバック用
    from dotenv import load_dotenv

    load_dotenv()
    CONNECTION = os.environ.get("CONNECTION")
```
上で取得したDBの接続情報をあらかじめ環境変数に設定しておき、実行時に取得しています。
環境変数はデプロイ時に別ファイルで設定しますが、デバック時に取得する方法がわからなかったため、dotenvを利用しています。




### GETリクエストの処理
データベースから取得してきたデータをjson形式で返します。
```python : main.py
if method == "GET":
        works = mongo.getToDo()
        return Response(
            json.dumps(works, ensure_ascii=False, default=json_serial),
            mimetype="application/json",
        )
```
日本語を含んだjsonをjsonifyで返すと文字化けします。
対策を調べるとFlaskのconfigに
```python
app.config['JSON_AS_ASCII'] = False
```
を設定すると良いようですが、自前でFlaskオブジェクトを生成していないので設定方法がわかりませんでした。
下策かもしれませんが、自前でResponseを組み立ててreturnしています。
~~誰かいい方法教えてください~~

### POSTリクエストの処理
POSTされたデータをデータベースに書き込みます。
こちらもGETと同じく文字化け対策でResponseを組み立てています。
```python : main.py
if method == "POST" and contentType == "application/json":
        data = request.get_json()
        mongo.record(data["place"], data["todo"])

        responseData = {"result": "Successed", "data": data}

        return Response(
            json.dumps(responseData, ensure_ascii=False, default=json_serial),
            mimetype="application/json",
            status=201,
        )
```

# ローカルデバッグ

## 仮想環境を作成する
```shell-session
$ python3 -m venv venv
```

## 仮想環境の有効化

```shell-session
$ source ./venv/bin/activate
```

## パッケージのインストール
```shell-session
$ pip3 install -r requirements.txt
```

## ローカルサーバーの起動
```shell-session
$ functions-framework --target todo --signature-type=http --port=8090 --debug
```
8080ポートは別用途で使用中ですので8090番を使用しています。それぞれの環境に応じて適宜書き換えてください。
毎回コマンドを打ち込むのは面倒ですので、私はシェルスクリプト(localDebug.sh)として保存しています。シェルスクリプトにする場合は実行権限の付与を忘れずに。

## ローカルサーバーへのアクセス
### 登録
POSTできれば何でもいいのですが、今回はcurlを使用します。
別ターミナルで
```shell-session
curl -X POST -H "Content-Type: application/json" -d '{"place": "家", "todo": "ゴミを出す"},' localhost:8080
```
を実行してデータを登録します。
登録できたかの確認はMongoDB Atlasの管理画面から確認可能です。
![mongo_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3898894/a1efed5e-b752-055b-efbe-cbe313226918.png)




### 読み出し
こちらもブラウザでもできますが、curlを使用します。
```shell-session
$ curl localhost:8090
[{"place": "家", "todo": "ゴミを出す", "created_at": "2024-10-01T08:06:52.078000"}]
```
先程のデータが登録されていることが確認できればOKです。


# Google Cloud Run Functionsへのデプロイ
## 環境変数
[公式ドキュメント](https://cloud.google.com/functions/docs/configuring/env-var?hl=ja)によるとデプロイ時に.yamlを渡して環境変数を設定できます。
あらかじめ確認しておいたMongoDBAtlasのアクセス情報を保存しておきます。
```yaml:.env.yaml
CONNECTION: "mongodb+srv://<username>:<password>@myapp.abcd.mongodb.net/?retryWrites=true&w=majority&appName=myapp"
```



## デプロイ除外ファイル設定
.gitignoreに設定したファイルはデプロイ時に除外されるようですので基本的には.gitignoreに環境変数ファイルなどを記述しておけばいいかと思います。
私はデバッグ用のスクリプトファイルなどをgloudignoreに記載しました。


## デプロイ
下のコマンドでデプロイを実行します。
```shell-session
$ gcloud functions deploy tutorial \
--gen2 \
--region=asia-northeast2 \
--runtime=python312 \
--entry-point=todo \
--trigger-http \
--allow-unauthenticated \
--env-vars-file .env.yaml
```
env-vars-fileオプションで環境変数の設定、allow-unauthenticatedで未認証の呼び出しを許可しています。
オプションの詳細は[公式](https://cloud.google.com/sdk/gcloud/reference/functions/deploy)で確認してください。

ローカルデバッグと同じく毎回コマンドを打ち込むのは面倒ですので、私はシェルスクリプトとして保存しています。

デプロイに成功したら、出力の最後に関数のURLが表示されますので、
そちらからブラウザでアクセスしてみましょう。
ローカルデバックの読み出しと同じ結果が表示されればOKです。

# さいごに
少しクセはありますがやり方さえわかってしまえば、お手軽にWEBAPIを構築できました。

またはじめてNoSQLをさわりましたが、ログなど単純なデータだけであればRDBよりお手軽でいいですね。
次は温湿度を登録するAPIを作って、ラズパイから測定結果を保存できるようにしたいと思います。