Raspberry Pi3からCloud Functions(GCP)へのお引っ越し
概要
Raspberry Pi3で動いている定期実行タスクを、GCPへ引っ越した。
今回は、ウェブサイトへアクセスしてデータを取得しGoogle SpreadSheetへ追記するプログラムをCloud Functions(GCP)で実行できるようにした。
なぜ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が担っていた同等部分である。
処理内容は以下のとおり。
- ローカルのパソコンから、ZIPアーカイブしたプログラムをステージング(デプロイ)
- Cloud Scedulerが定期実行のトリガーをかけ、Pub/Sub経由でCloud Functionsを起動する
- ウェブサイトからデータを取得
- データをGoogle SpreadSheetへ追記
- 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