Python
Node.js
GoogleCloudStorage
GoogleCloudPlatform
GoogleCloudFunctions

Google Cloud Functionsを使ってGCSに新規アップロードされたファイルを即座に取得する

Google Cloud Functionsとは

  • HTTPリクエスト(POST,PUT, GET, DELETE, OPTIONS)
  • GCS の状態変更
  • Cloud Pub / Sub

をトリガーにして、事前に作成しておいたスクリプトを実行できます。
(ちなみにFirebaseにも対応したCloud Functions for Firebaseでは、上記に加え以下のものもトリガー可能です。データベースへのwrite、所持ドメイン内の任意パスへのアクセス、ユーザの作成・削除、アナリティクスのイベント、モバイルアプリのクラッシュ)

環境構築

  • Cloud Functionsのインストール
    • ここを参考に。
    • gcloudからbetaのcomponents をinstall
      gcloud components update beta
  • Emulatorインストール
    • ここを参考に。
    • nodeはnodebrewとかで入れたほうがいいかも。
    • Emulatorのインストールが完了したら、consoleでwhich functionsできることを確認。
  • 注意
    • 2017/12現在、Google Cloud Functionsはnode v6.11.5しか対応していないので注意。

簡単なサンプルをローカルで動かす

  • HTTP Trriger
    • 以下のようなjsを用意し、コマンドを実行
helloWorld.js
exports.helloWorld = (req, res) => res.send("Hello, World!");
functions start
functions deploy helloWorld --trigger-http
functions call helloWorld
  • EmulatorでホストされているFunctionsを確認
functions list
  • GCS Trriger
    • 以下のようなjsを用意し、コマンドを実行
index.js
exports.index = function (event, callback) {
    const file = event.data;
    if (file.resourceState === 'not_exists') {
        console.log('File ' + file.name + ' not_exists.');
    } else {
        if (file.metageneration === '1') {
            // この判定を入れることで、ファイルの新規作成なのかそれ以外(変名、削除)なのかの
            // 判別が可能です。
            console.log('File + ' + file.name + ' created.');
            // ファイルが作成されたらやりたい処理 //
        } else {
            console.log('File + ' + file.name + ' metadata updated.');
        }
    }
    callback();
};
functions start
functions deploy index --trigger-bucket=my-bucket-project.appspot.com
functions call index --data='{"name":"upload.zip", "bucket":"my-bucket-project.appspot.com", "metageneration": "1"}'

ちなみに、GCSのBucket内にディレクトリ構造を持たせている場合、
以下のようにして指定すれば再現可能です。

functions call index --data='{"name":"functions_files/upload.zip", "bucket":"my-bucket-project.appspot.com", "metageneration": "1"}'

なお、Deployのtargetはindex.js内でexportsしているmodule名称で指定します。
index.js内に複数のexportしても、それぞれDeployすれば問題ありません。
逆にindex.js以外の名称のjsを読み込むことはできなさそうです。

実行結果のログは functions logs read で確認できます。

GoogleCloudStrage, CloudFunctions, サーバーサイドAPIの連携

まずはStorageの準備

GCSに適当なバケットを作成し、そこに適当なファイルを配置。
以下はサンプル。

  • GCP Project
    • sample-project
  • バケット
    • artifacts.sample-project.appspot.com
  • 階層
    • functions_files /
      • test.zip

Cloud Functionsでhookされるjs

  • このjsでは、バックエンドサーバーへPOSTリクエストを行います。
  • パラメータとして、ファイル名とバケット名を渡します。
index.js
var request = require('request');

exports.index = function (event, callback) {
    const file = event.data;

    if (file.resourceState === 'not_exists') {
        console.log('File ' + file.name + ' not_exists.');
        callback();
        return
    } else {
        if (file.metageneration === '1') {
            var data = {
                'file_name': file.name,
                'bucket_name': file.bucket
            };

            var options = {
                url: 'http://localhost:8000',
                method: 'POST',
                headers: {'Content-Type':'application/json'},
                json: data
             };

            request(options, function(err, response, body) {
                if (response && response.statusCode === 200 && body) {
                    console.log('backend request success.');
                } else {
                    console.log('backend error.');
                }
                callback();
            });
        } else {
            console.log('File + ' + file.name + ' metadata updated.');
            callback();
            return
        }
    }
};

バックエンドAPI

  • バックエンドAPIでは、localhost:8000 にてPythonサーバーを起動して、パラメータで指定されたファイルをGCSから取得してローカルサーバーに保存します。
sample.py
# -*- coding: utf-8 -*-
import json
from bottle import request, HTTPResponse
from google.cloud import storage

file_types = {
    'zip' : 'application/zip'
}

class Sample():
    @classmethod
    def main(cls):
        try:
            file_name = request.json.get('file_name')
            bucket_name = request.json.get('bucket_name')

            if '.' in file_name:
                file_ext = file_name.split('.')[len(file_name.split('.')) - 1]
            else:
                file_ext = ''

            result, validated_result = get_validated_file(file_name, bucket_name, file_ext)

            if result:
                body = { 'result': 'ok' }
            else:
                body = { 'result': validated_result }

            # レスポンスを返却する
            response = HTTPResponse(status=200, body=json.dumps(body))
            response.set_header('Content-Type', 'application/json')
            return response



def get_file(file_name, bucket_name):
    """GCSからファイルを取得
    """
    storage_client = storage.Client(project='sample-project')
    bucket = storage_client.get_bucket(bucket_name)
    return bucket.get_blob(file_name)


def is_accept_type(file, file_ext):
    """想定ファイルかどうかチェック
    """
    if file_ext not in file_types:
        return False
    else:
        return file.content_type == file_types[file_ext]


def get_validated_file(file_name, bucket_name, file_ext):
    """対象ファイルのバリデーション
    :return: バリデーションの成否, バリデーション結果(成功ならファイルパス)
    """
    file = get_file(file_name, bucket_name)

    if not file or not file.exists():
        return False, 'is not exist'

    if not is_accept_type(file, file_ext):
        return False, 'content_type'

    outfile_name = '/hoge/hoge/text.zip'
    # GCSのBlobオブジェクトをローカルのファイルオブジェクトとして保存
    file.download_to_filename(outfile_name)

    return True, outfile_name

CloudFunctions Emulatorで試す

functions deploy index --trigger-bucket=artifacts.sample-project.appspot.com
functions call index --data='{"name":"functions_files/test.zip", "bucket":"artifacts.sample-project.appspot.com", "metageneration": "1"}'

あらかじめ localhost:8000 でPythonサーバーを起動しておき、上記コマンドでfunctionsをローカルから叩いてPythonへリクエストが通れば成功。

Python側で指定ファイルがGCSに存在するかなどのチェックを行っているため、無い場合はちゃんとエラーが返ります。

これで動作確認が出来れば、GCSにファイル配置をトリガーにしてマスタインポートなどの処理が可能となります。

GCP上にCloudFunctionsをデプロイする

  • index.js があるディレクトリで以下のコマンドを実行すればDeploy可能です。
gcloud beta functions deploy index --stage-bucket test-stage-bucket --trigger-bucket=artifacts.sample-project.appspot.com

※GCFはバケットに展開されるため、stage-bucket はあらかじめ作成しておくこと。

上記にてDeploy完了したら、以後GCSをトリガーにしてFunctionsが実行されます。