Google Cloud Functions (GCF)入門! GCFを使ってGoogle Cloud Storage (GCS)のファイルを自動で圧縮する

＜Google Cloud Functions (GCF)とは＞
Google Cloud Platform(GCP)のサービスの１つで、Node.jsベース(Python3.7も対応)で動くサーバーレスのサービスです。AWSで言う所のLambdaのようなものです。
実行時間などの制約があるため、短時間で終了しかつイベントが起こった時だけ実行されるような処理に向いています。今回はGoogle Cloud Storage (GCS)と連携し、ファイルがGCSに新規作成された時だけ作成されたファイルを圧縮する機能を実装してみます。

特に良い点
・必要な時だけ起動してくれるため経済的
・GCSなど他のサービスと連携できる
・サーバーレスのためメンテ不要

悪い点
・実行時間(最大540秒)や割り当てメモリに制限(最大2048MB)があり、重い処理に向かない
・対応言語がNode.jsとPython3.7のみ


＜Google Cloud Storage (GCS)とは＞
GCPのサービスの１つで、オブジェクトストレージです。
AWSで言う所のS3のようなものです。ファイルをクラウド上のバケットと呼ばれるストレージに保存出来ます。
容量制限はありませんが、仕様上の制限があるため、ご使用前に制限事項を熟読することをお勧めします。
GCSはオブジェクトが更新されたりバケットに追加されたりした際に、アプリケーションに通知することができます。
今回はその機能を使い、GCSにファイルが置かれたらGCFが実行されるように設定します。
設定はGCFにソースコードをデプロイする際に指定します。

＜GCFでGCSのファイルを自動で圧縮してみる＞
ここからは「GCFでGCSのファイルを自動で圧縮する」ためのコードの説明や設定をしていきたいと思います。


＜今回の構成図＞
まず、はじめに今回構築するシステムの構成は下記のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386394/0fd15bb3-876e-7179-1be9-d6a5b40299f7.png)


＜作業環境＞
今回この記事で作業をする環境は下記になります。
・OS(バージョン)	
macOS High Sierra (バージョン:10.13.6（17G65）)

・Cloud SDK	
Google Cloud SDK 209.0.0
alpha 2018.07.16
app-engine-python 1.9.73
beta 2018.07.16
bq 2.0.34
core 2018.07.16
gsutil 4.33
kubectl

Cloud SDKは事前に作業環境にインストールしておいて下さい。
また、今回作成方法などは割愛しますが、GCPプロジェクトやGCSのBucketの作成も行っておいて下さい。


＜使用するソースコード＞
index.jsとpackage.jsonという下記2つのファイルを使用します。
index.jsはNode.jsで書かれたプログラム本体(ソースコード)です。
package.jsonはindex.jsで使用するライブラリの一覧です。
今回はGCSを操作するライブラリである@google-cloud/storageをindex.js中で使用するため、package.jsonに記載しています。
それら２ファイル(index.jsとpackage.json)を作成し、そのファイルがあるディレクトリでgcloudコマンドでアップロードするか、GCPのWebコンソール側からソースコードをアップロードします。
今回はGCSをNode.jsで操作するので、Node.js Client Libraryを使用しています。
ソースコード中に出てくるパラメータや、GCFやNode.jsの書式などはGCFのマニュアルをご覧ください。

処理の大まかな流れは、
1.圧縮対象ファイルをstreamで読み出しpipeで次に渡す
2.gzip圧縮しpipeで次に渡す
3.圧縮したものをstreamで書き込み
4.書き込み成功したら元ファイルを削除
となります。

――――――――――――――――――――――――――――――――――
…
exports.GCSFileCompress = (event, callback) => {
  const {Storage}      = require('@google-cloud/storage'); //使用するライブラリ
  const srcProjectId  = process.env.GCP_PROJECT; //環境変数
  const file                = event.data; //関数実行時に渡されるパラメータ
  const context         = event.context; //同上
 
//圧縮対象の設定
  const srcBucketName = `${file.bucket}`; 
  const srcFilename       = `${file.name}`;
  const srcStorage         = new Storage({ projectId: srcProjectId });
  const srcBucket           = srcStorage.bucket(srcBucketName);
  const srcRemoteFile    = srcBucket.file(srcFilename);
//圧縮したファイルの書き込み先設定
  const dstBucketName = `${file.bucket}`;
  const dstFilename   = srcFilename + '.gz';
  const dstStorage    = new Storage({ projectId: srcProjectId });
  const dstBucket     = dstStorage.bucket(dstBucketName);
  const dstRemoteFile = dstBucket.file(dstFilename);
//gzip圧縮の準備
  const zlib = require('zlib');
  const gzip = zlib.createGzip();
  gzip.on('error', function (err) {
    callback(new Error("Gzip failed!"));
  });
//圧縮後にアップしたファイルによる再実行抑止
  if ( srcFilename.match(/.gz$/) ) {
    console.log('Nothing Object to compress');
    callback();
    return;
  }
//streamでの書き込み処理の定義
  const out = dstRemoteFile.createWriteStream({
                metadata: {
                  contentType: 'application/x-gzip'
                }
              })
              .on('error', function(err) {
                callback(new Error("Upload failed!"));
              })
              .on('finish', function() { //書き込み終了時処理
                srcRemoteFile.exists(function(err, exists) {
                  srcRemoteFile.delete(function(err, apiResponse) {});
                });
                console.log(`Compress event ${context.eventId} completed`);
                callback();
              });
  //実行            
  srcRemoteFile.createReadStream().pipe(gzip).pipe(out);
}
…
 
package.json
…
{
  "name": "google-cloud/storage",
  "version": "2.0.3",
  "dependencies": {
    "@google-cloud/storage": "^2.0.3"
  }
}
…
――――――――――――――――――――――――――――――――――

＜gcloudコマンドでGCFへデプロイ＞
gcloudコマンドというGCP用のSDKを使ってGCFへデプロイします。
基本的な使用方法などは、少し前の記事になりますがCloud SDKコンポーネント、gcloudコマンドの使い方を解説！！をご覧ください。
https://www.apps-gcp.com/gcloud-command/


GCFを使用するには、まずGCPのプロジェクトの作成やAPIの有効化などの作業が必要になります。手順はマニュアルのクイックスタートに沿っていただくのが良いので、一読してみて下さい。

途中からNode.jsなどの開発環境構築の話になり初心者には辛いですが、簡単にまとめると、NVMでNode.js自身のバージョンを管理し、npmでNode.jsのプログラム中で使用するライブラリなどのパッケージを管理します。
Node.jsをインストールするとnpmが同時にインストールされます。
NVMをインストールすればNode.js本体はNVMを使ってインストールすることが出来るので、まずはNVMをインストールすることをお勧めします。
NVMのインストールが出来たら、インストール可能なNode.jsのバージョンを確認し、使用するNode.jsを適宜インストールします。
今回はNode.js 6を使用するので、Node.js 6の最新版をインストールします。

NVMインストール
――――――――――――――――――――――――――――――――――
…
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
…
――――――――――――――――――――――――――――――――――

インストール出来たら、~/.bash_profileにNVMのパスが追加されているので、新しいターミナルを開くか、bashやsourceコマンドを実行してパスを通します。
インストール可能なNode.jsの確認
――――――――――――――――――――――――――――――――――
…
nvm ls-remote
…
――――――――――――――――――――――――――――――――――

インストール可能なNode.jsが一覧で表示されるので、そこから適宜環境にあったものを指定しインストールします。
――――――――――――――――――――――――――――――――――
…
nvm install v6.14.4
…
――――――――――――――――――――――――――――――――――

インストール出来たか確認
――――――――――――――――――――――――――――――――――
…
nvm ls
node -v
…
――――――――――――――――――――――――――――――――――

package.jsonの生成
――――――――――――――――――――――――――――――――――
…
npm init --y
…
――――――――――――――――――――――――――――――――――

必要パッケージのインストール(他に必要なパッケージがあれば適宜インストール)
今回はサンプルとして提供してあるpackage.jsonを使用すればサンプルプログラムが動くので、特に実行しなくても大丈夫です。
――――――――――――――――――――――――――――――――――
…
npm install --save @google-cloud/storage
…
――――――――――――――――――――――――――――――――――

Node.js側の準備が出来たら、Google Cloud SDK (gcloudコマンド)でNode.jsのソースコードをGCFにアップロードするという流れになります。
環境構築周りの準備が整ったら、上記index.jsとpackage.jsonの2つのファイルを下記gcloudコマンドでGCFにアップロードします。
ソースコードのあるディレクトリで下記を実行します。
――――――――――――――――――――――――――――――――――
…
gcloud beta functions deploy GCSFileCompress --entry-point=GCSFileCompress --memory=128MB --timeout=540 --trigger-resource gs://{Google Cloud StorageのBucket名} --trigger-event google.storage.object.finalize --region=us-central1
…
――――――――――――――――――――――――――――――――――


今回使用したgcloudコマンドの各引数の意味は。。。

引数：説明
beta：GCF用のコマンドがgcloudコマンドでβ扱いなので付けます
functions：GCFへの操作のため指定しています
deploy：ソースコードアップロード
GCSFileCompress：関数表示名(GCFのWebUI側での表示名)
–entry-point：実行関数名(index.jsのexports.GCSFileCompressの部分)
–memory：GCFインスタンスに割り当てるメモリ量(Max:2048MB)
–timeout：関数実行可能時間(Max:540秒)
–trigger-resource：GCFをトリガーするリソースを指定
–trigger-event：指定したリソースで何が起こったらトリガーされるか指定
–region：GCFインスタンスが起動されるリージョンを指定

trigger-resourceの部分は事前に作成したGoogle Cloud StorageのBucketを指定してください。
その他の引数についてはCloud SDKのマニュアルを参照し、必要があれば適宜設定してください。


＜GUI(Webコンソール)でGCFへデプロイ＞
1.GCPにログインし、画面左上のナビゲーションメニューから「コンピューティング」以下に配置されているCloud Functionsを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386394/96889723-703f-450b-a7f2-03705085684d.png)

2. 「関数を作成」ボタンをクリックし適宜名前などを設定します。
今回のサンプルを動かす場合は下記画像と同じに設定してください。
対象バケットは事前に準備したものをご指定ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386394/4af01195-95bb-c8fd-61f2-d2d341b7d209.png)

設定できたら「作成」ボタンを押して終了です。

＜動作確認＞
GCPのWebコンソールでGCSを開き、対象のバケットを選択し、ファイルをドラッグ&ドロップします。

gsutil コマンドでアップロードしても良いです。
gsutil コマンドの使い方はgsutilコマンド全部試したので解説する(part1)をご覧ください。
https://www.apps-gcp.com/gsutil-command-explanation-part1/#gsutil-2

――――――――――――――――――――――――――――――――――
…
gsutil cp アップロードファイル 対象バケット
…
――――――――――――――――――――――――――――――――――

小さいファイルだと数秒程度でgzip圧縮されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386394/3361bf0d-12d8-9c0c-fb99-6f3276a3eb33.png)


＜まとめ＞
以上、いかがでしたでしょうか。

今回はファイルがGCSにアップロードされたらGCFがトリガーされ、アップロードされたファイルが圧縮されるという流れでしたが、トリガーは他にもHTTPやCloud Pub/Subなど随時追加されており、使用可能な言語もNode.js以外にもPythonが使えるようになったり、できることがどんどん広がっています。

トリガーにHTTPを使えば簡易APIとして使えますし、今回のGCSトリガーもファイルを出力するバッチ処理のようなものと組み合わせれば、より複雑な処理を簡単にフルマネージドで完全自動化できます。

ぜひ、この機会に1度お試ししてはいかがでしょうか。