sitateru tech blog: Google

sitateru tech blog

シタテルの技術やエンジニアの取り組みを紹介するテックブログです。

ラベル Google の投稿を表示しています。 すべての投稿を表示
ラベル Google の投稿を表示しています。 すべての投稿を表示

2019年2月18日月曜日

Google Cloud Functionsで画像を自動リサイズする

2月 18, 2019

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年12月26日水曜日

プロジェクトの振り返りをやってみた

12月 26, 2018

kaizen
こんにちは、シタテルの藤本です。
主にSCS(Sitateru-Control-System)という生産管理システムのバックエンド(Rails)を担当しています。

シタテルに入って小さい機能の追加、改修を行ってきたのですが
最近要件定義からリリースまでが3ヶ月程度のプロジェクトが終了しました。
そのプロジェクトに対して振り返りを行ってみたので内容について書いてみたいと思います。

やり方

シタテルの開発では2週間のタイムボックスで動いており、最終日にKPTにて振り返りを行っています。
今回のプロジェクトの振り返りでは繰り返し性がないことから
KPTの拡張版でやってみました。参考にさせていただいたのは以下です

プロジェクトの振り返り(KPTの様な何か)

各種情報

参加人数:4人
場所:熊本(2拠点)、東京
テレビ電話:Zoom
ボード:Google スプレッドシート

KPTから追加されている項目

KPTとの違いとして以下の項目が追加しております

  • Good
    よかったことをあげる、続けたいものをKeepにする
  • Issue
    Problemから導き出された課題
  • Risk
    今回具体的には発生していないが危ないと考えられること、将来起こりそうな問題
  • Solution
    IssueとRiskに対しての解決策を考える
  • Task(今回は対象外の項目とした)
    Solutionから導かれる具体的なアクション

進行順序

  1. Good、Problem、Riskを考えて、付箋に記載
  2. 各々の内容を確認してグルーピング
  3. GoodをKeepにするものを選択
  4. Problem(Risk)をIssueにするものを選択
  5. 4.で選択したものからIssueを考える
  6. Issueを共有する
  7. Solutionを話し合いながら出し合う
  8. 次に対応するSolutionを決める

付箋を張っていったボードは以下のような形です
board

やってみて

アウトプット

Good:14
Keep:4(Goodからグルーピングしている)
Problem:21
Risk:5
Issue:22

対象としたProblemとRiskは共に1つで
Solutionを合わせて6つ出し合った

よかったこと

  • みんなが考えていたことが共有できる
    自分の考えだけだと狭い範囲になりますがみんなの考えていること出してもらい共有することで
    各々の考えが掛け算になりより良いSolutionを出すことができた
  • 単発ものの振り返りの形式としては良い
    KPTだと繰り返すことで仕分けを流れになっていますが今回の形式では
    「続けたいこと」と「原因となったこと」を明確にして解決策を導くことができた
  • Solutionを話し合いながら考えることができて前向きになる
    みんなが思っていた問題はたくさん出たけど最後にSolutionについて議論をしていると
    前向きに次はもっと良くしよう、良くできると思うことができた

反省点

  • 予定した時間が甘すぎた
    1時間は流石に少なすぎました、大幅にオーバーしてしまい倍の2時間かかってしまいました
    (参加した方には申し訳なかったです、かつ快く続行していただき、ありがとうございました!)
    単発での実施かつ対象とする期間が大きいので最初から時間を長めに取っておくべきでした
  • 各項目の意味がパッとわからない
    ProblemとRiskってどう違うということが出たりしました、事前にちょっと時間をとり
    認識を合わせておいたほうがスムーズに入ることができたかなと思いました
  • 思い出す時間がなかった
    Good、Problem、Riskについて事前にちょっと考えておくとかのアナウンスをしていなかったので
    思い出す時間がちょっと少なすぎかなと思いました

まとめ

プロジェクトを振り返ることでよかったこと、問題となったことを再認識しその解決策まで
出すことができたことはとてもよかったと思います。
ただし出してよかったで終わると何も変わっていかないので次やる時に
今回上がったことを見直して実際の行動に移して、少しでも良いプロジェクトにしていきたいと思います。