sitateru tech blog: DevOps

sitateru tech blog

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

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

2019年10月11日金曜日

GASでG Suiteグループ追加をやってみた

10月 11, 2019
どうも朝野です。

以前の記事で、作業を楽にするためにRPAを触ってみたという話を書いたのですが
その関係の取り組みの一つとして現在Google Apps Script(GAS)でアカウント管理操作を集約・省力化するという作業を進めています。

Google Apps Scriptはご存知の方も多いと思いますが、Googleの様々なサービスと連携させて動かすことができるスクリプト言語です。

シタテルではG Suiteをバリバリ使っているので、アカウント管理システムもGASを中心として作っていこうと考えています。
そこで、まずはGASで何か作ってみる第1号として、ユーザーをグループに追加するフォームを作ってみました。
グループ追加はG Suiteの管理画面からできるのですが若干作業として面倒だったりするんですね。

というわけでまずはGoogleフォームを作成。
「追加するユーザー」「追加先グループ」の入力欄を作ります。
それからオプション設定の「メールアドレスを収集する」をチェック。

つづいて右上メニューから「スクリプトエディタ」を選択するとGASのオンラインエディタが開きます。

ここでソースコードを編集します。
今回のコードはこのようなものです。
var privilegedRespondent = ['taro@sitateru.sample', 'jiro@sitateru.sample'];

// グループ追加,フォーム送信時に実行
function addUserToGroup(event) {
  console.log('addUserToGroup()');
  var message = "グループ追加フォームからの通知です\n\n";
  const respondent = event.response.getRespondentEmail();
  if(privilegedRespondent.indexOf(respondent) === -1) {
    console.log('Respondent ' + respondent + ' not allowed');
    message += 'あなた( ' + respondent + " )にはアカウントを作成する権限がありません\n";
    sendResult(respondent, message);
    return;
  }
  const response = parseResponse(event.response.getItemResponses());
  const userName = response['追加するユーザー'];
  const groups = response['追加先グループ'].split(',');
  for (var i = 0; i < groups.length; i++) {
    var groupName = groups[i].trim();
    try {
      var member = addGroupMember(userName, groupName);
      console.log(' added member', member);
      if(member == undefined) {
        message += "ユーザーをグループに追加できませんでした。入力内容を確認してください\n";
        message += ' ユーザー: ' + userName + "@sitateru.com\n";
        message += ' グループ: ' + groupName + "@sitateru.com\n\n";
      } else {
        message += "ユーザーをグループに追加しました\n";
        message += ' ユーザー: ' + userName + "@sitateru.com\n";
        message += ' グループ: ' + groupName + "@sitateru.com\n\n";
      }
    } catch(error) {
      console.log(' error', error);
      message += "ユーザーをグループに追加できませんでした。入力内容を確認してください\n";
      message += ' ユーザー: ' + userName + "@sitateru.com\n";
      message += ' グループ: ' + groupName + "@sitateru.com\n";
      message += ' エラー: ' + error.toString() + "\n\n"
    }
  }
  sendResult(respondent, message);
}

// Googleユーザーをグループに追加
function addGroupMember(userName, groupName) {
  if(userName && groupName) {
    const userEmail = userName + '@sitateru.com';
    const groupEmail = groupName + '@sitateru.com';
    console.log(' add ' + userEmail + ' to ' + groupEmail);
    const member = {
      email: userEmail,
      role: 'MEMBER'
    };
    return AdminDirectory.Members.insert(member, groupEmail);
  }
}

// メール送信
function sendResult(recipient, message) {
  MailApp.sendEmail({
    name: 'アカウント管理システム',
    to: recipient,
    subject: 'グループ追加フォーム実行結果',
    body: message
  });
  console.log('send result');
}

// フォームの入力内容をオブジェクトにして返す
function parseResponse(itemResponses) {
  var obj = {};
  for (var i = 0; i < itemResponses.length; i++) {
    var itemResponse = itemResponses[i];
    var question = itemResponse.getItem().getTitle();
    // var type = itemResponse.getItem().getType();
    var answer = itemResponse.getResponse();
    obj[question] = answer;
  }
  return obj;
}

あとはスクリプトにグループを操作する権限を与えるために、メニューの「Googleの拡張サービス」を選択して「Admin Directory API」をONにします。

コードは長いですが、GASでユーザーをグループに追加するには AdminDirectory.Members.insert() を実行すればOKです。公式のサンプルはこちら

今回のコードではグループ追加フォームとして実用上あったほうがいい機能もいろいろつけてみました。
event.response.getRespondentEmail() でフォーム送信者のメールアドレスを取得し、あらかじめ定義しておいた privilegedRespondent にいる人でなければ実行しない
・「追加先グループ」はカンマ区切りで複数指定できるように
・最後に実行結果をメール送信 MailApp.sendEmail()

と、こんなもので実際に利用を始めています。

こちらにリファレンスがありますが、GASはかなりの種類のGoogleのサービスを操れるのが強みです。↓のページで左メニューの「G Suite Services」や「Advanced Google Services」を開いてみよう!
https://developers.google.com/apps-script/reference

これからもいろいろなものを作って楽をしていこうと思います!

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年11月16日金曜日

githubイシュー/PRの管理にjasperを導入したら幸せになった話

11月 16, 2018

こんにちは!

シタテルで エンジニアをしている建山です。

主に工場向けのマイオペというシステムの開発を行っています。

シタテルではgithubを使ってソースコード・イシュー/PR管理などをしています。
その中で、困るなとおもっていたのがイシューへのアサインやコメントに気づく仕組みでした。
PRやイシューで自分にアサインやコメントされているにもかかわらず、気づくかない、気づくのが遅くなるなど、あげくのはてに、slackでメンションもらったりということもありました。

そこで、リアルタイムに通知を受け取ったり、メンションにもれなく気づけるようになりたいとおもっていたところ、社内のエンジニアにjasperよさそう!とおしえていただいて実際便利でしたので紹介します。

jasper以外にもいい方法はいくつかあるとおもいますが、今回はjasperを使った方法を記載します。

jasperとは

githubのイシュー/PRを閲覧や管理をしやすくするソフトウェアです。
Mac版/Windows版/Linux版があります。
https://jasperapp.io/

cookpadのエンジニアの方がつくられているようです(https://techlife.cookpad.com/entry/2017/03/14/100000

ほとんどここに書かれています。

jasperを使った方法

jasperにはstreamsを登録する機能があります。こちらに登録しておくと、
条件ごとに一覧で見ることができて、通知を受け取りたい場合は、特定のstreamだけ通知を受け取るということもできます。

私が通知を設定しているstreamsはこちら。

公開中のイシューで自分にメンションもらっているリスト

is:issue is:open mentions:ユーザー名

公開中のイシューで自分に レビューリクエストもらっているリスト

is:open is:pr review-requested:ユーザー名

PRで自分にアサインもらっているリスト

is:open is:pr assignee:ユーザー名 archived:false

公開中のPRで自分にメンションもらっているリスト

is:pr is:open mentions:ユーザー名

まだ使いこなせていないのですが、ほかにもたくさんの用途にstreamを分けることができます。
https://jasperapp.io/doc.html#stream

個人的には、jasperを使いだして、github上でするべきコメントを、slack上でやることが減ったと感じています。github上にもコメントで経緯ものこるので、いいなと感じています。

日々、改善しながらやっていきたいと思います!