こんにちは、DevOpsチームの甲斐です。
今回は、Google Cloud Functions(以下、GCF)で画像を自動リサイズする手順を紹介したいと思います。
GCFとはAWSでいうところのLambdaです。いわゆる、サーバーレスってやつですね。
最近はGCPを触ることが多いのですが、先日GCP上のWordPressのアップロード画像をリサイズするために、
GCFを使って自動的に画像をリサイズするようにしました。
AWSのLambdaも触ったことありますが、GCFのほうが手間が軽い印象を持ちました。
まだGCFをお使いになったことがない方は、ぜひご参考にしていただければと思います。
前準備
GCFを使う上で必要な環境は以下のとおりです。
- GCPプロジェクトの作成
- 課金の有効化
- Cloud Functions APIの有効化
- Cloud SDKのインストール
詳細は、以下に書かれていますので、こちらをご参照ください。
https://cloud.google.com/functions/docs/quickstart-console?hl=ja
今回の仕様
今回の仕様は以下のとおりです。
- Google Cloud Storage(以下、GCS)の当該バケットのuploadsディレクトリにアップロードされた画像を自動的にリサイズ
- リサイズする画像フォーマットはimage/jpegのみ
- オリジナル画像は".orig"拡張子をつけてバックアップ
- リサイズされた画像はオリジナルの画像と同じ名前で上書き
実装
それでは、早速実装の手順を紹介していきたいと思います。
今回はNode.js v8で書きました。それ以外にもNode.js v6, Python(beta), Go(beta)が使えます。
1. 作業ディレクトリの作成
mkdir -p gcf/convert_image
cd !$
2. npm init
npm init
3. 必要なパッケージをインストール
今回は、@google-cloud/storage(GCS関連パッケージ)とgm(画像編集パッケージ)をインストールします。
npm install @google-cloud/storage gm --save
4. コーディング
コードは以下のとおりです。
最初に少しハマったところとしては、リサイズした画像でオリジナル画像を上書きすると
再びイベントが発火されるので無限ループに陥ってしまったことです。
対策としてメタデータにコンバート済みである旨を記述し、処理の最初にメタデータをチェックすることで判断するようにしました。
'use strict';
const gm = require('gm').subClass({imageMagick: true});
const fs = require('fs');
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
exports.convertImage = data => {
if (data.resourceState === 'not_exists') {
console.log(`Skip because not_exists`);
return;
} else if (!data.name) {
console.log(`Skip because no name`);
return;
} else if (!data.name.startsWith('uploads')) {
console.log(`Skip because not uploads: ${data.name}`);
return;
} else if (data.name.endsWith('.orig')) {
console.log(`Skip because original image: ${data.name}`);
return;
} else if (data.contentType !== 'image/jpeg') {
console.log(`Skip because not image/jpeg: ${data.contentType}`);
return;
}
const file = storage.bucket(data.bucket).file(data.name);
console.log(`Uploaded image : ${file.name}`);
const tempLocalPath = `/tmp/${path.parse(file.name).base}`;
return file
.getMetadata()
.then(data => {
console.log('[Check already converted or not]');
const metadata = data[0];
if (metadata.metadata && metadata.metadata.isConverted) {
console.log(`${file.name} is already converted.`);
Promise.reject();
} else {
console.log(`${file.name} is not yet converted.`);
Promise.resolve();
}
})
.catch(err => {return;})
.then(() => {
console.log('[Download file]');
return file
.download({destination: tempLocalPath})
.catch(err => {
console.error('Failed to download file.', err);
return Promise.reject(err);
});
})
.then(() => {
console.log('[Backup a image]');
return file.copy(`${file.name}.orig`);
})
.then(() => {
console.log('[Convert a image]');
return new Promise((resolve, reject) => {
gm(tempLocalPath)
.samplingFactor(2, 2)
.strip()
.quality(85)
.interlace('Line')
.colorspace('sRGB')
.write(tempLocalPath, (err, stdout) => {
if (err) {
console.error('Failed to convert image.', err);
reject(err);
} else {
resolve(stdout);
}
});
});
})
.then(() => {
console.log('[Upload a converted image]');
const options = {
destination: file,
resumable: false,
metadata: {
metadata: {
isConverted: true
}
}
};
return file.bucket
.upload(tempLocalPath, options)
.catch(err => {
console.error('Failed to upload a converted image.', err);
return Promise.reject(err);
});
})
.then(() => {
console.log('[Remove a temporary file]');
return new Promise((resolve, reject) => {
fs.unlink(tempLocalPath, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
});
};
5. デプロイ
デプロイは以下のようにします。
今回の場合、runtimeはnodejs8になります。また、メモリやタイムアウトもオプションで設定できます。
gcloud functions deploy <name> --entry-point <entry-point> --runtime <runtime> --trigger-bucket <trigger-bucket> --region <region> [--memory <memory> --timeout <timeout>]
デプロイが完了したら、以下のコマンドで正常に登録されたことを確認します。
gcloud functions list
6. テスト
サンプル画像を当該バケットにアップロードし、正常にリサイズされることを確認します。
% gsutil cp sample.jpg gs://<bucket>/uploads/
% gsutil ls -l "gs://<bucket>/uploads/sample.jpg*"
2601078 2019-02-14T02:37:45Z gs://<bucket>/uploads/sample.jpg
14583723 2019-02-14T02:37:43Z gs://<bucket>/uploads/sample.jpg.orig
TOTAL: 2 objects, 17184801 bytes (16.39 MiB)
まとめ
簡単ですが、GCFで画像をリサイズする実装手順を紹介しました。
今回はGCSをトリガーにしていましたが、それ以外にもGoogle Cloud Pub/SubやHTTPリクエストをトリガーにすることも出来ます。
GCFの詳細については本家ドキュメントをご参照ください。
https://cloud.google.com/functions/docs/?hl=en
また、以下のGitリポジトリにGCFのサンプルプログラムがありますので参考にしてみてください。
https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/master/functions