Raspberry Pi3からCloud Functions(GCP)へのお引っ越し

f:id:hate_nattou:20210221223928j:plain:w800

概要

Raspberry Pi3で動いている定期実行タスクを、GCPへ引っ越した。
今回は、ウェブサイトへアクセスしてデータを取得しGoogle SpreadSheetへ追記するプログラムをCloud Functions(GCP)で実行できるようにした。 f:id:hate_nattou:20210509013107p:plain

なぜGCPなのか?

AWSやAzure, HerokuではなくてGCPにした理由を記載する。
最大の理由は、「無料枠が大きい」に尽きます。

  • 無料枠が大きい
    現在、Raspberry Piにおいてはウェブアプリケーションも動かしており、それらはコンテナアプリとしてデプロイしたい。実現する手段として、AWSではFargate/ECS、GCPではCloud Runがある。Fargate/ECSには無料枠がないが、Cloud Runは無料枠(AlwaysFree)があり個人サービスレベルなら無料枠内で利用可能であったため。

  • Googleサービスの中でなるべく完結させたい
    GoogleDrive内においた、Google SpreadSheetへデータを保存する予定であった。それは、もともとRaspberry Piで動作させていたときに、CSVファイルをGoogleDriveへアップロードしていたという背景もあり、その流れを踏襲したかったため。AWSなどを使ったとしてもGoogleDriveへのアクセスは可能だが、なんとなく気持ちの問題。

  • 会社の研修でGCPでサービスを作った事例を聞いたこと
    分散技術およびコンテナ技術であれば、GCPに一日の長があると聞いたため。

実現したこと

GCPアーキテクチャは下図のとおり。
Google Cloudと記載のある灰色の箱が、これまでRaspberry Pi3が担っていた同等部分である。

f:id:hate_nattou:20210509015831p:plain

処理内容は以下のとおり。

  1. ローカルのパソコンから、ZIPアーカイブしたプログラムをステージング(デプロイ)
  2. Cloud Scedulerが定期実行のトリガーをかけ、Pub/Sub経由でCloud Functionsを起動する
  3. ウェブサイトからデータを取得
  4. データをGoogle SpreadSheetへ追記
  5. Google ColaboratoryからSpreadSheetを読み込んで可視化(データ分析)

データを保存する場所

GCPに限らず、機能をサーバレスで実現した場合にはデータを保存する場所が重要になる。
これまでは、Raspberry PiのローカルSDに保存したうえで、GoogleDriveにデータを同期していた。サーバレスではデータベースに保存するのが一般的。今回は既存の仕組みとの差分が少なくなるように、Google SpreadSheetへ保存することにした。もっとデータ数が大きかったり、更新頻度が高い場合にはデータベースの採用を検討する必要がある。

発生した問題

GoogleCloudFunctionsでSeleniumを使う

Seleniumを使う場合には、chromedriverとheadless-chromiumを適切な場所に配置して権限変更する必要がある。
詳しくはこちらのサイトを参考にした。 qiita.com

ZIPアーカイブ

先述のchromedriverとheadless-chromiumソースコードと一緒にZIPアーカイブとしてアップロードする必要がある。
main.pyが入っているフォルダーを指定して圧縮するのはNG。
main.pyやconfigなどの複数ファイルを直接指定して、zip圧縮すること。
zipファイルを展開した時に、フォルダーが生成されるのはNGという意味。

ステージングバケット

ZIPアーカイブをアップロードする場所。
今回は、自分で勝手にフォルダーを作ってみた。
アップロード後にデプロイをすると、自動で他の場所へコピーされている。そのため、デプロイ後が成功したあとは、ファイルの削除が可能(だと思う)。
この方法がベストプラクティスかどうかは不明。

Error: memory limit exceeded

Error: memory limit exceeded. Function invocation was interrupted.

ランタイムの設定でメモリーを256MiB->1GiBへ変更。
128MiB/256MiB/512MiB/1GiB/2GiBのなかから選択することが可能。
512MiBを割り当てた場合は、エラーが出る場合と出ない場合とがあって不安定になる。
そのため、今回は1GiBを選択。

sys.exit()

Cloud Functionsでsys.exit()はしない。
pythonインタープリターを止めるのと同じ?
return True/Falseとflgを使って処理継続の判定をした。

タイムゾーンに気をつける

datetime.now()はタイムゾーンがついていない形式(naive)なので、サーバーに設定されたタイムゾーンが使われてしまう。

今回はアメリカのリージョン内にあるCloud Functionでプログラムを実行しているので、単にdatetime.now()とするとアメリカのタイムゾーンとなる。datetime型にタイムゾーンを付与した形式(aware)にしないと、datetime型同士の比較の際にエラーが発生する。

タイムゾーンを付与する方法

datetime.now()
JST = timezone(timedelta(hours=+9), 'JST')
datetime.now(JST)

datetime.now()で現在時刻を取得する場合は、タイムゾーンを作成して、datetime.now(JST)というように日本のタイムゾーンをつけて実行。

文字列
time = "2021-05-05"
datetime.strptime(time + "+0900", "%Y-%m-%d%z")

元が文字列データの場合は、文字列の後ろに"+0900"を足して、"%z"のフォーマットで読み込む。

今回はawareで統一した。
naiveとawareを混在させると、時刻の比較をすることができずにエラーとなる。
naiveかawareのどちらかで統一する必要がある。

参考

自動化に欠かせない定期実行の仕組みを3大クラウドで実践! www.youtube.com

Cloud FunctionsからGoogleDriveへのデータ保存 note.com

GoogleSpreadSheetを操作するための初期設定(サービスアカウント) zak-papa.com

PythonでSpreadSheetに値を挿入するときの注意点 qiita.com

Pythonタイムゾーンを扱う方法 www.soudegesu.com