AppEngine
GoogleCloudPlatform
gcp
PubSub
cloudfunctions

Google Cloud FunctionsをGoogle App Engine + Cloud Pub/Sub + Nodeで定期的に実行する

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を動作させるには、そのウェブアプリからさらに蹴っ飛ばす感じ。

https://cloud.google.com/appengine/?hl=ja

Cloud Pub/Sub

上述のようにCloud Functionsを定期的に実行するには、GAE上のウェブアプリの特定のURLへのアクセスをトリガーにしており、そのウェブアプリからさらにCloud Functionsを蹴っ飛ばすには、このCloud Pub/Subを利用する。

https://cloud.google.com/pubsub/?hl=ja

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については以下を参照してください。

https://cloud.google.com/appengine/docs/standard/python/config/appref

次に cron.yaml ファイルをつくる。

cron:
- description: Push a "publish" onto pubsub every minutes
  url: /publish
  schedule: every 1 minutes

この例では /pubsub というURLに毎分アクセスしてねということ。

cron.yamlについてのドキュメントは以下。

https://cloud.google.com/appengine/docs/flexible/nodejs/scheduling-jobs-with-cron-yaml?hl=ja

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