Cloud Functionsで関数をデプロイするチュートリアルをやっていく

# 1. はじめに
本記事では、FirebaseのCloudFunctionsのチュートリアルについて説明していきます。
具体的にはこのリンクの内容をそのままやります。

https://firebase.google.com/docs/functions/get-started?hl=ja&gen=2nd

上記サイトからそのまま引用しますが、関数を作成してFirebaseにデプロイするところが本記事のゴールとなります。

>Cloud Functions を使用するにあたり、まずはチュートリアルを実践してください。このチュートリアルでは、必要な設定タスクから始め、次の 2 つの関連する関数を作成、テスト、デプロイします。
メッセージを追加する関数。テキスト値を受け取る URL を公開して Cloud Firestore に書き込みます。
テキストを大文字に変換する関数。Cloud Firestore の書き込みでトリガーされ、テキストを大文字に変換します。

# 2. 実行環境
以降の内容については、以下の実行環境にて実施しました。
- OS：Windows 10
- ブラウザ：Google Chrome
- Python：11.0.3

なお、プログラム自体はVScodeを使って記述していきます。
（といっても、本記事ではサンプルのコピペで済みます。）

# 3. 準備
#### 3-1. Firebaseプロジェクトの作成
下記リンクの「1 Firebaseプロジェクトを作成する」に記載の通り進めてください。

:::note info
Google Analyticsについては、チュートリアルなので省略してOKです。
:::

https://firebase.google.com/docs/functions/get-started?hl=ja&gen=2nd#create-a-firebase-project

プロジェクトの作成が完了したら、このタイミングでFirestoreのデータベースも作成します。
左側のカラムから、構築＞Firestore Databaseを選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552900/369ec12c-9205-51e1-bf96-71da3dddcb23.png)


ロケーション、セキュリティ保護ルールを設定して、
「有効にする」をクリックしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552900/33a4844f-525b-8352-df19-f58e40c6e963.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552900/58279954-ce76-b490-aa44-08f2ae23cc2f.png)


#### 3-2. ローカル側環境構築
Pythonのインストールについてはここでは省略します。

ローカル環境にて、プロジェクト用のディレクトリを作成してください。
Ex) D:/work/fbtest-project　など

作成したディレクトリをVScodeで開き、メニュー＞ターミナル＞新しいターミナルからターミナル(Windows Power Shell)を開いてください。

コンソール上で、先ほど作成したプロジェクト用のディレクトリに移動し、以下のコマンドを用いてpythonの仮想環境を作成してください。
仮想環境の名前は任意です。ここでは「fb-env」としています。

```console
> cd D:/work/firebase-project
> python -m venv fb-env
```

仮想環境の作成が完了したら、仮想環境を起動します。

```console
> cd ./fb-env/Scripts
> ./activate.ps1
```

コンソールの頭に (fb-test) のような表示が出たらOKです。

:::note warn
仮想環境を分けることは必須ではないですが、
その場合はパッケージ依存関係のエラーに注意してください。
:::

#### 3-3. Firebase CLIのインストール

基本的にこのサイトの通りにやればよいです。
npmコマンドはよく使うので、Node.jsを用いた方法がおすすめです。

https://firebase.google.com/docs/cli?hl=ja#windows-npm

なお、仮想環境のみでfirebase-toolsを使う場合、コマンドの-gオプションを外してください。

:::note warn
最新のfirebase-toolsを使用すると、バグが発生することがあります。
※私はこれで2時間ほど悩みました...
:::

https://github.com/firebase/firebase-functions-python/issues/136

12.5.3にダウングレードすることで解消するので、以下のコマンドでインストール推奨です。
```console
(fb-env)> npm install firebase-tools@12.5.3
```

#### 3-4. プロジェクトの初期化
まずは、Firebase CLIの認証を行います。

以下のコマンドを実行後、ブラウザが立ち上がり、Googleアカウントを選択する画面が出るので、
認証を行うアカウントをクリックし、権限の設定を行ってください。

```console
(fb-env)> cd D:/work/firebase-project
(fb-env)> firebase login
```

次に、Firestoreの初期化を行います。
Firebase initialization complete!と表示されればOKです！

```console
(fb-env)> firebase init firestore

//Firebaseをインストールするディレクトリを聞かれるので、間違いがないことを確認して、Y入力
You're about to initialize a Firebase project in this directory:
  D:\work\00_git\private\study\fbtest-project
? Are you ready to proceed? (Y/n) 

//デフォルトとして選択するプロジェクトを指定します。
//ここでは、先ほど作成したプロジェクトを指定してみます。
? Please select an option:
  Use an existing project　（下矢印キーで選択、Enterで決定）
  Create a new project
  Add Firebase to an existing Google Cloud Platform project
  Don't set up a default project

? Select a default Firebase project for this directory: 
  project1-xxxxx (project1)　（下矢印キーで先ほど作成したプロジェクトを選択、Enterで決定）
  project2-xxxxx (project2)
  ...

//Firestore Rulesとして使うファイルの名前を入力する。デフォルト場合はそのままEnter入力。
? What file should be used for Firestore Rules? firestore.rules

//Firestore indexesとして使うファイルの名前を入力する。デフォルト場合はそのままEnter入力。
? What file should be used for Firestore indexes? firestore.indexes.json

~~省略~~
+  Firebase initialization complete!
```

続いて、Functionsの初期化を行います。
ここも、Firebase initialization complete!と表示されればOKです！

```console
(fb-env)> firebase init functions

//Firebaseをインストールするディレクトリを聞かれるので、間違いがないことを確認して、Y入力
You're about to initialize a Firebase project in this directory:
  D:\work\00_git\private\study\fbtest-project
? Are you ready to proceed? (Y/n) 

//Cloud Functionsで使う言語を聞かれる。Pythonを選択。
? What language would you like to use to write Cloud Functions? 
  JavaScript
  TypeScript
> Python　（下矢印キーで選択し、Enterで決定）

//必要なライブラリをインストールしてよいか聞かれる。yを入力。
? Do you want to install dependencies now? 　（yを入力）

~~省略~~
+  Firebase initialization complete!
```


Functionsの初期化が完了すると、functionsというフォルダが作成されていると思います。
このフォルダの中にある、main.pyの中にプログラムを書いていきます！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552900/b765d3e9-822d-36a1-d15b-824234e2b48b.png)

----

:::note info
Functionsの初期化の際、pipのupgradeを促される場合があります。
その場合は、以下のコマンドを実行してください。
:::

```console
(fb-env)> cd D:/work/firebase-project
(fb-env)> ./fb-env/Scripts/python.exe -m pip install --upgrade pip
(fb-env)> ./functions/venv/Scripts/python.exe -m pip install --upgrade pip
```

# 4. プログラムの記述
チュートリアルのプログラムをコピペして、main.pyに貼り付けます。

```python:main.py
# The Cloud Functions for Firebase SDK to create Cloud Functions and set up triggers.
from firebase_functions import firestore_fn, https_fn

# The Firebase Admin SDK to access Cloud Firestore.
from firebase_admin import initialize_app, firestore
import google.cloud.firestore

app = initialize_app()

@https_fn.on_request()
def addmessage(req: https_fn.Request) -> https_fn.Response:
~~省略~~

@firestore_fn.on_document_created(document="messages/{pushId}")
def makeuppercase(
~~省略~~

```
# 5. プログラム動作テスト

#### 5-1. エミュレータの起動
プログラムの記述が完了したら、関数をエミュレータで実行します。
エミュレータを起動するには下記のコマンドを実行してください。

※カレントディレクトリがfirebase-projectとなっている場合は、cdしなくてもOKです！

```console
(fb-env)> cd D:/work/firebase-project
(fb-env)> firebase emulators:start
```

コマンド実行後、ターミナルに次のようなログが出れば、ターミナルの起動は成功です！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552900/83b91b1d-f1ea-709b-9742-47cd4a32593c.png)


```console
//この部分は、HTTPリクエストを送るURLになります。
+  functions: Loaded functions definitions from source: addmessage, makeuppercase.
+  functions[us-central1-addmessage]: http function initialized (http://127.0.0.1:5001/fbtest-projcet/us-central1/addmessage).
+  functions[us-central1-makeuppercase]: firestore function initialized.

//ここの部分は、エミュレータをブラウザ上で見ることができるアドレスになります。
┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://127.0.0.1:4000/               │
└─────────────────────────────────────────────────────────────┘

//エミュレータのうち、FunctionsとFirestoreはそれぞれ以下のアドレスになります。
┌───────────┬────────────────┬─────────────────────────────────┐
│ Emulator  │ Host:Port      │ View in Emulator UI             │
├───────────┼────────────────┼─────────────────────────────────┤
│ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├───────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
└───────────┴────────────────┴─────────────────────────────────┘
```

#### 5-2. 関数にリクエストを送る
無事にエミュレータの起動が完了したら、実際に関数を動かします。

エミュレータ起動時のログから、addmessage関数にHTTPリクエストを送るためのURLを確認し、
そのURLに対して、クエリ文字列「?text=testmesssage」を付加してリクエストを送信します。
ブラウザを開いて、アドレスバーにクエリ文字列を付加したURLを入力してください。

ブラウザ上に、「Message with ID ～～～ added.」と表示されたらOKです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552900/591c5a9a-c95e-c19a-0fae-ae9a81c822a6.png)

#### 5-3. 関数実行結果の確認
まずは、Firestoreに文字列が格納されているかを確認しましょう。
エミュレータのFirestore画面を確認してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552900/0e1c5a4a-1183-5e53-cc96-4197db736c79.png)

フィールドにoriginalとuppercaseがあり、それぞれ小文字と大文字に変換されたものが
格納されているため、コピペしたプログラムは無事に動きました！

また、コンソールのほうにも、正常終了ログが出力されています。

```console
i  functions: Beginning execution of "us-central1-addmessage"
>  127.0.0.1 - - [25/Oct/2023 15:55:05] "GET / HTTP/1.1" 400 -
i  functions: Finished "us-central1-addmessage" in 4.0356ms
i  functions: Beginning execution of "us-central1-addmessage"
>  127.0.0.1 - - [25/Oct/2023 15:55:18] "GET /?text=testmessage HTTP/1.1" 200 -
i  functions: Finished "us-central1-addmessage" in 145.0236ms


i  functions: Beginning execution of "us-central1-makeuppercase"
>  Uppercasing GrufKravSLuzKKHcCtBZ: testmessage
>  127.0.0.1 - - [25/Oct/2023 15:55:19] "POST /functions/projects/ HTTP/1.1" 200 -
i  functions: Finished "us-central1-makeuppercase" in 28.1055ms
```

# 6. 本番環境にデプロイ
エミュレータで関数の動作確認が完了したら、本番環境にデプロイしていきます。

:::note info
本番環境にデプロイするには、Blaze料金プランにする必要があります。
※要はクレジットカードを登録して従量課金にしないといけないです。
:::

デプロイを行うには、エミュレータを終了（Ctl＋C）し、以下のコマンドを実行します。
```console
(fb-env)> firebase deploy --only functions
```

デプロイが完了すると、エミュレータ実行時同様に、リクエスト送信用URLを出力します。
このアドレスに、クエリ文字列を付加してリクエストを送信してみましょう。
```console
Function URL (addmessage(us-central1)): https://addmessage-xxxxxxx-uc.a.run.app
```

Firebaseのコンソールを開き、Firestoreから関数が正しく動作しているかを確認しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552900/9b3143b3-7309-48bb-f6fd-94c4ca8f641f.png)

正しく動作していますね！
これで本番環境へのデプロイ完了となります！

# 7. 終わりに
ここまでお疲れさまでした。
Firebaseを初めて触る人はいろいろなエラーに悩まされたんじゃないでしょうか？

私自身、Pythonを書いたことはあったものの、
こういったインフラ面の環境構築をクラウド上で済ませてしまうのは初めてだったので、
いろいろなエラーに悩まされたり、初めて知ることがたくさんあったりで大変でした。

参考までに、以下にエラー内容と解消方法や、初めて知ったことをメモしておきます。
誰かの役に立ちますように！


※記事に関する誤字・脱字、間違った内容へのご指摘等ございましたら
　お気軽にコメントお願いします。

---

#### Pythonの仮想環境について
今までPaizaやColaboratory使っていたので、ローカルに環境構築することがなかったのですが、
今回はじめてローカルに環境作った際に、外部ライブラリをたくさん入れちゃっていました。

いろいろ調べていくうちに仮想環境というものがあるのを知りまして。
これは便利だなと。いうことで共有します。（おそらくほとんどの人が知っていると思いますが…）

https://camp.trainocate.co.jp/magazine/venv-python/

#### VScode上のターミナル（Powershell）で認証関連のエラー
```console
+ .\venv\Scripts\activate
+ ~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : セキュリティ エラー: (: ) []、PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess
```
###### 解消法

https://qiita.com/ryu22e/items/520b35db6a444d8289da

#### エミュレータがポート重複で起動しないエラー
```console
firestore: Port 8080 is not open on localhost, could not start Firestore Emulator.
firestore: To select a different host/port, specify that host/port in a firebase.json config file:
      {
        // ...
        "emulators": {
          "firestore": {
            "host": "HOST",
            "port": "PORT"
          }
        }
      }
i  emulators: Shutting down emulators.

Error: Could not start Firestore Emulator, port taken.
```

###### 解消法

https://qiita.com/C_HERO/items/c2e9ab1a7bb3537d4cb4

#### makeuppercase関数が動かないエラー
```console
ERROR in app: Exception on /functions/projects/ [POST]
Traceback (most recent call last):
  File MY-DIRECTORY/functions/venv/Lib/site-packages/flask/app.py", line 2190, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File MY-DIRECTORY/functions/venv/Lib/site-packages/flask/app.py", line 1486, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File MY-DIRECTORY/functions/venv/Lib/site-packages/flask/app.py", line 1484, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File MY-DIRECTORY/functions/venv/Lib/site-packages/flask/app.py", line 1469, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File MY-DIRECTORY/functions/venv/Lib/site-packages/functions_framework/__init__.py", line 174, in view_func
    function(event)
  File MY-DIRECTORY/functions/venv/Lib/site-packages/firebase_functions/firestore_fn.py", line 308, in on_document_created_wrapped
    return _firestore_endpoint_handler(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File MY-DIRECTORY/functions/venv/Lib/site-packages/firebase_functions/firestore_fn.py", line 115, in _firestore_endpoint_handler
    is_nanoseconds = _util.is_precision_timestamp(time)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File MY-DIRECTORY/functions/venv/Lib/site-packages/firebase_functions/private/util.py", line 332, in is_precision_timestamp
    _, s_fraction = time.split(".")
    ^^^^^^^^^^^^^
ValueError: not enough values to unpack (expected 2, got 1)
```
3-3にも注意書きしましたが、firebase-toolsのバージョン12.5.4で起きるエラー。
flaskでエラーが出てるように見えるのでエラー原因が全然わからずめちゃくちゃ悩みました。

https://github.com/firebase/firebase-functions-python/issues/136

エラーメッセージベタ貼りで検索かけた人が引っかかるように、貼っておきます。

###### 解消法

```console
(fb-env)> npm install firebase-tools@12.5.3
```