2019年2月18日月曜日
Google Cloud Functionsで画像を自動リサイズする
こんにちは、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
2018年11月14日水曜日
リリース作業を少し楽にした開発(Slack+Lambda+Golang)
こんにちは、シタテルの鶴巻です。
devops&インフラ担当です。
シタテルのプロダクトのリリース作業を少し楽にするために、
Slack+Lambda+Golangを用いて開発した話をします。
リリースの流れ
シタテルでは、GitフローやGitHubフローを採用しています。
また、本番環境へのデプロイは、以下の流れで実施しています。
- developブランチからreleaseブランチを作成
- ステージング環境でreleaseブランチをデプロイ&動作確認
- 本番環境にデプロイ
ちなみに、1.でreleaseブランチを作成すると、CircleCIによって2.のステージング環境へのデプロイは自動で行われます。
地味に面倒なリリース作業
ステージング環境へのデプロイは自動化されているとはいえ、小さな手作業が合間に発生し、地味に面倒でした。
具体的には以下のような作業です。
- developブランチからreleaseブランチの作成
- ローカルPCでdevelopブランチを最新の状態にpullして、releaseブランチ作成してGitHubにpush
- releaseブランチのプルリクエスト作成
- GitHubのUIをポチポチ
- リリースタグの作成
- 前回のリリース以降でマージされたプルリクエストを見て、リリース内容を記述
手作業はSlackからコマンドを叩くだけにしました
Slackから以下のコマンドを叩くだけで、地味に面倒くさかった作業を自動で行うようにしました。
/release <repository> <release branch>
実際には、以下の内容を実施するGolangプログラムをLambdaで実行しています。
- 対象のリポジトリをクローンし、developブランチからreleaseブランチを作成し、GitHubにpush
- releaseブランチのプルリクエストを作成
- 前回のリリース以降にマージされたプルリクエストのタイトルのリストを記述したリリースタグのドラフトを作成
構成
Slack -> API Gateway -> Lambda01(同期) -> Lambda02(非同期) -> GitHub
- Slackからコマンドを叩くと、Lambda01にリクエストを送信します。
- Lambda01は、Lambda02に対象レポジトリとブランチ名を付与してリクエストを送信します。
- Lambda02で作業を実行します。
Lambdaを2つ使用している理由
Slackはリクエスト送信後3秒でタイムアウトするためです。
実行したいプログラムは3秒以上かかるため、実際の作業は非同期で実施する必要がありました。
そのため、Lambda01はLambda02に処理を投げて、Slackに200を返し、実際の処理はLambda02で実施します。
Lambda01はLambda02を非同期実行で呼び出すので、リクエストをキューに入れた後は、Lambda01は関数を終了することができます。
Lambdaの呼び出しタイプ(同期・非同期)
Lambdaは、呼び出しタイプ(InvocationType)を指定することで、同期実行や非同期実行を選択することができます。
例:InvocationTypeで"Event"を指定することで、非同期実行でLambdaを呼び出せます。
input := &lambda.InvokeInput{
FunctionName: aws.String("xxxx"),
Payload: jsonBytes,
InvocationType: aws.String("Event"),
}
※同期実行は"RequestResponse"を指定します
今回使用したGitHub操作のためのGoライブラリ
- go-git https://github.com/src-d/go-git
- インメモリでgitクローンするために使用
- go-github https://github.com/google/go-github
- google製
- プルリク作成やリリースタグ作成に使用
その他ハマったポイント
- APIGatewayの統合リクエストのマッピングテンプレートを使いこなせず。デバッグのやり方もわからず、使うのを諦めました。
まとめ
- 少しの手間が自動化されるだけで、すごく楽になります。これからももっとやっていきたいです。
- GitHub操作のライブラリはとても便利でした。
- シタテルのサービスはRails + vue.jsが主ですが、自分の好きなGo言語を使えたので楽しかったです。