Google Cloud Functionsの関数を定期的に実行したのですが、AWSのLambdaにくらべてめんどくさかったのでメモとして残しておきます。
Cloud Functionsについては以下で。
https://cloud.google.com/functions/?hl=ja
AWSのLambdaとくらべてどうこうということではなく、シンプルにGoogle Cloudを試してみたかったというだけです。
Google Cloud Functionsを定期的に実行するために必要な前提条件
Google Cloud Functionsには、このサービス自体にCron機能はありませんので、GCPの以下の各サービスを組み合わせて実行する必要があります。
正直言ってAWSのLambdaよりだいぶめんどくさい。笑
Google App Engine
Cron機能はGoogle App Engine(以下GAE)にあるので、それを利用する。
ただし、コマンドを実行するとかはできないようで、GAE上にデプロイされたウェブアプリの指定したURLを蹴っ飛ばすという仕様なので、蹴っ飛ばされるためのウェブアプリを用意する必要がある。
Cloud Functionsを動作させるには、そのウェブアプリからさらに蹴っ飛ばす感じ。
Cloud Pub/Sub
上述のようにCloud Functionsを定期的に実行するには、GAE上のウェブアプリの特定のURLへのアクセスをトリガーにしており、そのウェブアプリからさらにCloud Functionsを蹴っ飛ばすには、このCloud Pub/Subを利用する。
GAEに配置するNodeアプリを開発する
設定ファイルをつっくる
なにはともあれ npm init
で、package.json
を作りましょう。
$ npm init
つぎにapp.yaml
というファイルをつくる。内容は以下のような感じ。
runtime: nodejs
env: flex
manual_scaling:
instances: 1
resources:
cpu: 1
memory_gb: 0.5
disk_size_gb: 10
今回のケースではブラウザでアクセスされることは想定していないのでインスタンスの性能は控えめ。
app.yaml
については以下を参照してください。
次に cron.yaml
ファイルをつくる。
cron:
- description: Push a "publish" onto pubsub every minutes
url: /publish
schedule: every 1 minutes
この例では /pubsub
というURLに毎分アクセスしてねということ。
cron.yaml
についてのドキュメントは以下。
Nodeのウェブアプリ本体をつくる
今回のアプリの例では、/publish
というURLにアクセスできればいいので以下のように割りとシンプルです。
まず依存関係があるモジュールをインストールする。
$ npm install @google-cloud/pubsub express --save
次にapp.js
というファイルを作ります。内容は以下の通り。
'use strict';
const express = require( 'express' );
const publisher = require( './lib/publisher' );
const app = express();
app.get( '/publish', ( req, res ) => {
if ( 'true' === req.headers['x-appengine-cron'] || "8080" === process.env.PORT
) {
const pub = new publisher( '<project-id>', '<topic>' );
pub.send( { message: 'Hello World' }, function( results ) {
console.log( results )
} );
res.status( 200 ).send( 'OK' ).end();
} else {
res.status( 403 ).send( 'Forbidden' ).end();
}
} );
app.get( '/*', ( req, res ) => {
res.status( 403 ).send( 'Forbidden' ).end();
} );
// Start the server
const PORT = process.env.PORT;
app.listen( PORT, () => {
console.log( `App listening on port ${PORT}` );
console.log( 'Press Ctrl+C to quit.' );
} );
まず、./lib/publisher
というファイルを以下のようにrequire
していますが、これはPub/Subのpub
をやるための独自モジュールを読み込んでいます。
この内容については後述します。
そしてapp.get()
でルーティングを2種類定義していますが、一つは/publish
というURLでこれがCronで定期的に蹴っ飛ばされるURLです。
それ以外のURLでは、問答無用で403 Forbiddenを返しているだけです。
/publish
に対してアクセスがあくせすがあったときに、'true' === req.headers['x-appengine-cron']
という条件分岐がありますが、これはCronからのアクセスがあったときだけに付与されるリクエストヘッダーでGAE以外からのアクセスでは、仮にこのヘッダーがあったとしても削除されるそうです。
この X-Appengine-Cron ヘッダーは、Google App Engine により初期設定されています。リクエスト ハンドラでこのヘッダーを検出した場合は、リクエストが cron リクエストであると判断できます。 このヘッダーが、外部ユーザーからアプリケーションへのリクエストに指定されていた場合は、削除されます。アプリケーションの管理者がログインして要求した場合を除き、テストするために、このヘッダーを設定することができます。
https://cloud.google.com/appengine/docs/flexible/nodejs/scheduling-jobs-with-cron-yaml?hl=ja
npm start
等でローカルにNodeサーバーを立ち上げてテストする際には、このヘッダーをつかって動作確認できますが、いちいちリクエストヘッダーを付与するのはめんどくさいので、ポート8080でアクセスがあったときにも許可するようにしています。
<project-id>
と<topic>
は、プロジェクトごとに書き換えるべき値です。
-
<project-id>
- Google CloudのプロジェクトのID -
<topic>
- Pub/Subメッセージの送信先となるCloud Functionsの関数名
あと、{ message: 'Hello World' }
というデータを送ってますが、これはCloud Functions側で受け取れるデータです。
今回はCronで定期実行したいだけなので内容はどうでもいいです。DBから取った値をなげるとかのユースケースはありかもですね。
Pub/Subでメッセージを送信するモジュールを作成する
次に lib
というディレクトリをつくって、その中にpublisher.js
というファイルを作成してください。
$ mkdir lib
$ touch lib/publisher.js
lib/publisher.js
の内容は以下のような感じです。
'use strict';
const PubSub = require('@google-cloud/pubsub');
const Publisher = function( projectId, topicName ) {
this.projectId = projectId;
this.topicName = topicName;
}
Publisher.prototype.send = function( data, callback ) {
const projectId = this.projectId;
const topicName = this.topicName;
const dataBuffer = Buffer.from( JSON.stringify( data ) );
const pubsub = new PubSub( {
projectId: projectId,
} );
pubsub
.topic( topicName )
.publisher()
.publish( dataBuffer )
.then( results => {
callback( results )
} )
.catch( err => {
console.error( 'ERROR:', err );
} );
}
module.exports = Publisher;
Cloud Functionsを実行できればいいだけなので、特に変更するべきところはないと思います。
Cloud Functions用の関数をつくる
まずCloud Functions用のCLIをインストールします。
$ npm install -g firebase-tools
ところでここでいきなりFirebaseってなによってなりますよね。Cloud Fubctions = Firebase というサービスのようですがわかりにくいっちゅうねん。
コマンドラインでやる限りではほとんど気にしなくてよいので、このまま続けます。(笑)
次にfirebase
コマンドでプロジェクトをつくります。
$ mkdir sample_function && cd sample_function
$ firebase init functions
firebase init functions
を実行したあとで、色々聞かれますが。言語にJavaScript
を選ぶ以外は全部イエスでオッケーだと思います。
完了するとfunctions/index.js
というファイルができていますのでそのファイルに以下のように記述してください。
'use strict';
const functions = require( 'firebase-functions' )
exports.topic = functions.pubsub.topic( '<topic>' ).onPublish( ( event ) => {
const data = JSON.parse( Buffer.from( event.data.data, 'base64' ).toString() );
console.log( data );
return true;
} );
この中で <topic>
というのがありますが、これは先ほどのGAE上のアプリで設定した<topic>
と同じ文字列であるべきです。
動作確認
GAEとCloud Functionsをデプロイしたら期待通りに動作しているかどうかをログで確認できます。
先ほどのCloud Fubctionsのディレクトリに移動して以下のコマンドでログを見れます。
$ firebase functions:log