2021.01.12

GCP Cloud Functions と Cloud Firestore を用いたサーバーレスアプリケーションの構築

次世代システム研究室の Y.I です。 クラウドを利用したアプリケーション開発の知見を得るために、Google Cloud Platform (GCP) の Cloud Functions を使って簡単なアプリケーションを動作させてみました。

利用技術
  • GCP
  • Cloud Functions
  • Cloud Firestore
  • Python
  • Functions-framework
アプリケーション
Cloud Functions のトリガーにて起動する Web ページをスクレイピングして Firestore へ保存するアプリケーションを作成します。

前提として GCP project が作成されていて、Google Cloud SDK が利用できる状態になっているものとします。

1.Firebase project 作成

Firestoreを利用するために Firebase project を作成します。GCP project 1つに対して Firebase project を1つ紐づける形で作ります。

なお、Firebase project を削除すると紐づいている GCP project も合わせて削除されます。ですが、30日以内ならば GCP project の復旧は可能ですが、注意して project を作成してください。


作成手順

Firebase project 作成はこちらから行います

Firebase project と紐づける GCP project を選択します

Analytics 利用有無を選択します

Analytics リージョンを指定します

Firebase project の作成完了です

Firebase project ダッシュボードです



2.Firestore 作成

引き続き Firestore を作成します。 Firestore はフルマネージドのドキュメント型データストアであり、100 万件の同時接続と、毎秒 10,000 回の書き込みまでスケールできます。将来的には、これらの制限を引き上げる予定とのことで様々な案件の要件を満たすのではないでしょうか。

また、大きな特徴としてリアルタイムリスナーを利用して Firestore とクライアント間のデータ同期ができます。 Firestore のデータ変更が合った際に Firestore からクライアント SDK へデータの同期をプッシュすることができます。今回はサーバー間通信での動作ですが、ネイティブアプリから利用する状況ですとより有用です。

作成手順

Firestore 作成はこちらから行います

セキュリティルールをアクセス制限をかけた本番向けと制限なしのテスト向けを選択します。セキュリティルールは後から変更することができるのでどちらを選んでも構いません。

ロケーション(リージョン)を選択します。ロケーションは変更できないので利用されているリージョンを選ぶようにしてください

Firestore の作成完了です。


セキュリティルールについて補足

セキュリティルールは認証ユーザーで制限したり様々な条件で制限をかけることができます。また、 Firestore SDK は Admin SDK と Client SDK があり、 Admin SDK はルールで CRUD 禁止になっていても制限対象にならず CRUD できます。セキュリティルールに慣れるまでは、ルールを全て拒否にしておいて Admin SDK を用いてアクセスするのも良いかもしれません。

# ルール全て拒否
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

3.スクレイピングアプリケーション開発

GCP 上で利用するサービスの準備ができたので、アプリケーションの開発を行います。動作確認用のシンプルなコードですが下記を用意します。スクレイピングするサイトは弊社GMOインターネットプレスリリースページとしています。

– main.py
import firebase_admin
import urllib.request as req
from firebase_admin import credentials
from firebase_admin import firestore
from bs4 import BeautifulSoup
from datetime import datetime as dt

# エントリーポイント
def scraype_entry(request):
    msg = "Hello world!"
    html = scraype()
    save_firestore(html)
    return msg

# Use the application default credentials
def save_firestore(html):
    # ※1
    if (not len(firebase_admin._apps)):
        cred = credentials.ApplicationDefault()
        firebase_admin.initialize_app(cred, {
            'projectId': '{プロジェクトIDを指定}',
        })

    db = firestore.client()

    # scraypeした内容をFirestoreへ保存
    doc = db.collection(u'scraype_results').document(u'gmo-news')
    doc.set({
        u'html': html
    })

    dt_str = dt.now().strftime('%Y/%m/%d %H:%M:%S')

    # sub collectionへ保存
    doc = db.collection(u'layer1').document(u'doc1').collection(u'layer2').document(u'doc2')
    doc.set({
        u'field1': u'val1',
        u'field2': 2,
        u'field3': [1, 2, 3, 'str4'],
        u'field4': dt_str
    })
    
def scraype():
    url = "https://www.gmo.jp/news/"
    res= req.urlopen(url)
    soup = BeautifulSoup(res, "html.parser")
    
    return format(soup.select("section"))
– requirements.txt
firebase_admin == 4.5.0
bs4 == 0.0.1

※1 firebase_admin.initialize_app()エラーについて
Fucntions を短時間で複数回呼び出すと initialize_app()が2回実行されてしまい下記エラーが発生します。対策として下記 if 文のように apps が初期化済みか判定することで回避可能です。
– エラー内容
ValueError: The default Firebase app already exists. 
This means you called initialize_app() more than once without providing an app name as the second argument. 
In most cases you only need to call initialize_app() once. 
But if you do want to initialize multiple apps, pass a second argument to initialize_app() to give each app a unique name.
– 対策
if (not len(firebase_admin._apps)):

4.Functions-Framework で動作確認

Cloud Funtions へデプロイする前に、ローカル環境で動作確認を行います。 GCP 提供の Functions-Framework を使うと Cloud Functions の動作確認をローカル PC で行えます。

github functions-framework-python

インストール
pip install functions-framework
起動
functions-framework --target scraype_entry --debug
  target : エントリーポイント(上記コードの scraype_entry関数)
  debug : デバッグモードで起動(詳細エラー表示/コード修正Live update)
起動時のターミナル出力
functions-framework --target scraype_entry --debug
 * Serving Flask app "scraype_entry" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
 * Restarting with fsevents reloader
 * Debugger is active!
 * Debugger PIN: 486-189-976
動作確認
curl "localhost:8080"

urlへアクセスするとscraype_entry関数が実行されます。 このように Functions-Framework を使うと簡単に動作確認ができます。また、 main.py のコードを修正して保存すると Live upate され、修正が反映されます。



5.デプロイ

動作を確認できたら、実際に Cloud Functions へデプロイします。デプロイは Google Cloud SDK の gcloud コマンドを使って行います。

GCP PJ確認/変更
# 確認
gcloud config list
 表示結果のproject箇所がデプロイ目的のprojectIdになっているか確認
 もし異なっていたら以下のコマンドで変更する

# 変更
gcloud config set project {projectId}

デプロイコマンド
gcloud functions deploy scraype_entry --runtime python38 --trigger-http --region asia-northeast1 --allow-unauthenticated
トリガーは http タイプ、 リージョンは asia-northeast1 (東京リージョン)、未認証のアクセスを許可としてデプロイします。

デプロイ失敗
デプロイを失敗させてみます。 requirements.txt がない状態でデプロイコマンドを実行すると下記エラーになりました。どのように解決するかというと、GCP Web Console にてログを確認できます。エラーログを確認して対策します。
gcloud functions deploy scraype_entry --runtime python38 --trigger-http --region asia-northeast1 --allow-unauthenticated
Deploying function (may take a while - up to 2 minutes)...⠹
...
Deploying function (may take a while - up to 2 minutes)...failed.
ERROR: (gcloud.functions.deploy) OperationError: code=3, message=Function failed on loading user code. This is likely due to a bug in the user code. Error message: Error: please examine your function logs to see the error cause: https://cloud.google.com/functions/docs/monitoring/logging#viewing_logs. Additional troubleshooting documentation can be found at https://cloud.google.com/functions/docs/troubleshooting#logging. Please visit https://cloud.google.com/functions/docs/troubleshooting for in-depth troubleshooting documentation.

Web Console Cloud Functions ログ

「import firebase_admin ModuleNotFoundError: No module named ‘firebase_admin’」 とありました。 firebase_admin moduleをインストールする必要があることがわかり、 requirements.txt で firebase_admin を指定してデプロイ実行するとインストールされデプロイに成功します。



6.Cloud Functions 動作確認

Cloud Functions にデプロイした scraype_entry Function を実行して、 Firestore にスクレイピング結果とサブコレクションにデータが登録されていることを確認します。

Function実行
curl "https://asia-northeast1-{プロジェクトID}.cloudfunctions.net/scraype_entry"

Firestore スクレイピング結果
スクレイピングした結果がFirestoreに保存されていることを確認できます

Firestore サブコレクション
layer2コレクションのdoc2に値が保存されていることを確認できます



7.まとめ

今回は知見を得ることが目的でしたので、未使用の技術のみを用いてアプリケーションを構築してみました。 Cloud Functions と Cloud Firestore そして Python もほぼ未使用でしたが、数日でなんとか形になりました。今回の構成だとサーバーの負荷対策や不具合は GCP へ任せる形が取れてアプリケーション開発に専念できます。

また、Functions-Framework が用意されているおかげで、 Cloud Functions 向けコードを簡単に動作確認できました。debugモードでの Live update が手軽にコード修正を行えてすぐに動作確認できるのが便利でした。

最後に、次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。

Pocket

関連記事