notebook

都内でWEB系エンジニアやってます。

Functions Frameworkを使ってCloud Functionsに入門する

4月はGCP素振り月間と銘打っていろいろと触り始めたのでまぁそうだよねみたいな話多めですがやったこと残しておきます

Cloud Functions + Pub/Sub + GCS

f:id:swfz:20200412182856p:plain

Functions Framework

Cloud Functionsなどの関数を開発するためのフレームワーク

公式の説明は下記

Functions Framework  |  Google Cloud Functions に関するドキュメント

cloud.google.com

Functions Frameworkを用いることでCloud FunctionsやCloud Runなどで使う関数をローカルで開発できる

クイックスタートではHTTPリクエストを受け付ける関数の想定のよう

とりあえずクイックスタートは数分で行えるのでやっみて「なるほど」って感じ

注: 現在、Function Frameworks は Node と Go の関数に対応しています。

今回はNodeで進める

GoogleCloudPlatform/functions-framework-nodejs: FaaS (Function as a service) framework for writing portable Node.js functions

github.com

Pub/Subと連携するCloud Functions

今回やってみたかったのは「軽量タスクをPub/Sub経由で実行する」関数

探してみたところ下記記事でFunctions FrameworkとPub/Subで開発するときのTipsが乗っていたのでそれに習ってやってみた

functions-framework を利用したGoogle Cloud Functionsにおいてpubsubのテストをする方法 - selmertsxの素振り日記

selmertsx.hatenablog.com

ローカルでの開発

  • インストール
cd func_dir
npm init
npm install -D @google-cloud/functions-framework
  • 関数を書く

最初の数行はWeb UIからPub/Subイベントを購読する関数を作成した場合についてくるコメント

例としてはてなブックマークのAPIをたたいてレスポンスをそのままGCSへ保存する関数を書いた

/**
 * Triggered from a message on a Cloud Pub/Sub topic.
 *
 * @param {!Object} event Event payload.
 * @param {!Object} context Metadata for the event.
 */

const fetch = require('node-fetch');
const {Storage} = require('@google-cloud/storage');

exports.collectHatenaCount = async (event, context) => {
  const data = Buffer.from(event.data, 'base64').toString();
  const params = JSON.parse(data);

  const urlBase = 'http://b.hatena.ne.jp/entry/jsonlite/?url=';
  const requestUrl = `${urlBase}${params.url}`;

  const bucketName = process.env['BUCKET'];
  const storage = new Storage();

  const res = await fetch(requestUrl).then((res) => {
    return res.json();
  });

  const bucket = storage.bucket(bucketName);
  const file = bucket.file(`share-count/service=${params.service}/${encodeURIComponent(params.url)}`);

  await file.save(JSON.stringify(res));
};

Buffer.from(.....でbase64デコードしているのはPub/Subのイベントデータがbase64エンコードされた値で送られてくるから

他は特にURLもらってAPIたたいてそのままレスポンスをGCSへ保存するという流れ

インストール手順とは別途で下記もインストールする

npm install @google-cloud/storage node-fetch

サーバ立ち上げ

--signature-typehttpeventが選択できる、今回はPub/Subを想定しているのでeventを指定

--targetは実行する関数名

$ npx functions-framework --target=collectHatenaCount --signature-type=event
> shared-count-hatena@1.0.0 start /home/vagrant/sandbox/shared-count/hatena
> functions-framework --target=collectHatenaCount --signature-type=event

Serving function...
Function: collectHatenaCount
URL: http://localhost:8080/

イベント確認

Pub/Subから送られてくるデータはbase64エンコードされているのでCurlで確認する際も下準備が必要

これは実際にPub/Subへメッセージをpublishして開発と同じコードにできるよう調整した

curl -XPOST http://localhost:8080/ -H 'Content-Type:application/json; charset=utf-8' -d "{\"data\": {\"data\": \"$(cat event.json|base64|tr -d '\n')\"}}"
  • event.json
{
  "url": "https://swfz.hatenablog.com/entry/2019/03/03/233624",
  "service": "hatena"
}

デプロイまで

ローカルでの開発が終わったらデプロイまでやりたいところですね

以降はgcloudコマンドで必要なリソースなどを作成していきます

Pub/SubのTopic作成

gcloud pubsub topics create  |  Cloud SDK のドキュメント  |  Google Cloud

cloud.google.com

$ gcloud pubsub topics create memo-collector-hatena

これだけでTopicができる、めちゃくちゃ簡単…

関数のデプロイ

ローカルマシンからのデプロイ  |  Google Cloud Functions に関するドキュメント

cloud.google.com

詳しいオプションなどはこちら

gcloud functions deploy  |  Cloud SDK のドキュメント  |  Google Cloud

cloud.google.com

$ gcloud functions deploy collectHatenaCount --trigger-topic=memo-collector-hatena --runtime nodejs10 --region asia-northeast1 --set-env-vars BUCKET=memo-raw-data

--trigger-topicでTopicを指定するとそのTopicにメッセージが送られたそのメッセージの内容を読み取って関数が実行される

事前に作成していたTopic名を指定する

環境変数の設定は--set-env-varsで行う、環境によってバケット名を変えているので指定が必要

これで該当のPub/Sub Topicを購読する関数がデプロイできた

ほか

puppeteer使う場合はそれなりにメモリも消費するようでデフォルト(256MB)だとエラーが発生した

Error: memory limit exceeded. Function invocation was interrupted.

というエラーになってしまったのでdeploy時のオプションで対応

--memory=512MB

npmモジュールの扱い

package.jsonに記述したモジュールをよしなにインストールしてくれる模様

Node.js での依存関係の指定  |  Google Cloud Functions に関するドキュメント

cloud.google.com

Lambdaだと大きめのモジュール入れる際は容量制限がある、そのためLayer使うにしてもそちらでも容量制限があるのでpuppeteer使うのすごく面倒だった記憶がある

一方Cloud Functionsはデプロイまでに意識することが少なく

  • npm installしてローカルで開発
  • 動作確認してgcloud functions deployたたく
  • ちゃんと動く!

と、数ステップでデプロイまで行えたのでとてもやりやすい印象を持った

公式を見ると

注: Functions Framework を使用して、Node.js Cloud Functions のコンテナ化されたバージョンを Cloud Run にデプロイすることもできます。

ちょっと手を加えればCloud Runにもデプロイできるのか? ということで時間作って試してみようと思っています

CLIからメッセージを送る

クイックスタート: gcloud コマンドライン ツールの使用  |  Cloud Pub/Sub ドキュメント  |  Google Cloud

動作確認のためCLIからPub/Subへメッセージを送ってみる(URLは別のURLにしています)

$ gcloud pubsub topics publish memo-collector-hatena --message '{"url":"https://example.com","service":"hatena"}'

無事保存されました

f:id:swfz:20200412182903p:plain

まとめ

  • Functions Frameworkでローカル開発を簡単に行えた
  • gcloudコマンドでCloud Functionsのデプロイまで簡単に行えた
  • せっかくCloud Functionをローカルで開発できるようにしたがStorageに関しては開発用にバケット作る必要があるのでモックなどは今後調べて取り組む
  • 今の時点(2020-04-12)でnode10が最新なのは少し微妙感がある…
  • 意識することがLambdaと比べて少ないので簡単な関数だったらCloud Functionsのほうが良いなと感じた

余談

Twitterやはてぶなどの数値はGAS経由で取得していたものの、いくつかエラーでずっと数値が取れない現象に陥ってしまいずっと放置していました

GASだとデバッグなどやりづらいなーと思っていたのとスプレッドシート運用もそろそろどうかなと思っていたのでこの機会に刷新しようと試み中

大分オーバースペック感はあるもののテーマ決めてそれに向けて開発するのは楽しいですね

最後に今回使用したコードを貼っておきます

shared-count/hatena at master · swfz/shared-count

リポジトリ自体はCloud Functions以外も試しているので興味があれば見ていただければと思います

swfz/shared-count: shared-count

github.com