gcsにgasからファイルを公開して、アクセス耐性100万倍させるゾッと

これは、Google Cloud PlatformのAdvent Calendar の9日目のエントリーです

# 静的ファイルと私

マイクロダイエットでハードダイエット中のa2cです、GoogleDeveloperExpert(Apps)を担当しています。

GAS(GoogleAppsScirpt)を使えば、公開Webサーバが作れるのでスプレッドシート上の情報を元にJSON返したり、HTML返したり結構自由な公開ライフが送れたりします。がしかし、遅い！大量アクセスさばけない（なさそう）のです。
スプレッドシートでこさえたデータをデータベースに突っ込んでそこから再送するとか有りますが、静的ファイルをGCSに配備して一般公開することで、大量アクセスも難なく捌けるし、GoogleSpreadSheetの各種関数や、時限実行、履歴機能もそのまま使える素敵な管理画面が出来ます。

とは言え、GASには標準でGCSサービスが使えない為、利用するには若干敷居が高いです。本エントリーでは、スプレッドシートの行列をJSONとして吐き出す基本的な方法を説明します。

# 最初にまとめ
結構長くなってしまったので、このエントリーを読むとどうなるのか？何が嬉しいのか？


Webアプリにありがちなちょっとした設定用のJSONファイル、公開されてたり、内部のサーバで利用されていたりするヤーツ。そんなファイル有りますよね？

エンジニアで完結して生成できるなら、srcに入れたり自動生成したり出来ますが、コンテンツ絡んだりすると運用チームが設定内容をいじれるようにしなくてはいけません。JSON公開するだけなのに、CMSに仕組み作ったりバリデーション作ったりDBいじったり、カロリー高杉！！みたいな状態になったりしてちょっとした不幸＆珍事です。

本エントリーで紹介する、GoogleAppsとGCSを使うだけで、非公開のJSONから超大量トラフィックに耐えるJSON配信サーバがいとも簡単に作成できます。また、設定はスプレッドシートから行えるため誰が何時何に変更したのかの履歴閲覧やリストアも簡単にできるようになります。しかもソースはJSだけという手軽さ、まずコレから運用開始してみて、重要度が増してきてから、オレオレCMSに移行しても遅くはないかもしれません。

というのが少しでも伝わればと思います。本スクリプトの元はすでに某サービスに導入されており、ENG組からはコミットの手間が省け、運用組は誰に依頼することもなく自分のタイミングで更新でき、みんなハッピーになりました。めでたしめでたし

それでは、はじまりはじまり〜

# GCS(GoogleCloudStorage)周りをざっくりと

## GCS(Google Cloud Storage)の概要

概要＞ https://cloud.google.com/storage/?hl=ja
料金＞ https://cloud.google.com/storage/pricing?hl=ja#storage-pricing

詳細は公式ページ見てください。S3と似たようなもんです。
違うと感じるところは、アクセスが高頻度なオブジェクトは、自動的にGoogleさんのキャッシュに乗っていい感じに高速化されるので、なんとかクラウドフロントのようなCDNの設定が一切不要で利用可能なところです。パージ出来ないとか任意に載せられないとか有りますが、ただ配信するだけであれば、メリットのほうが多いと思います。
このストレージに対して、GoogleAppsから簡単にファイルをDeploy出来るようになれば、Appsだけでは実現不可能な大量アクセスをさばけるようになります。それだけでなく、サーバーを一切立てること無く時限実行したり、各種Appsリソースに沿った情報出力や、管理画面を作成することも可能になります。GCP＋Apps便利ですね。

# GogoleAppsでやることから始めます
では、GCSに出力する元ファイルを作成しましょう。今回は、スプレッドシートから、GCSにJSONを吐き出すようにしてみます。

## やることリスト
1. スプレッドシートの作成
    1. Configシート追加
        1. プロパティー設定
    1. Script
        1. ソースコード追加
        1. ライブラリの登録

## スプレッドシートの作成

1. Appsで新規スプレッドシートを作成する。
    2. 既存のシートを `data` に改名する。(このシートが書きだされます)
    2. `gcs_config` シートを追加作成する
3. Step2のシート内にバケット名と公開Pathの設定をする
    4. バケット名は、現時点では不明。
    5. 次ステップで作成するバケット名を記載する
<img width="676" alt="gas2gcs_sample_-_Google_スプレッドシート.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/0975cd38-2276-e088-8878-1fd38e7a13a3.png">


|GCS情報     |   |
|--------|---|
|Bucket名 | `gas2gcs_201512` |
|ファイルPath| `/public/sample.json` |
|公開Path   | ```=JOIN("", "https://storage.googleapis.com/", B2, B3)``` |


## Configセルに対して、名前付き範囲を設定する。
スプレッドシート上の `メニュー` ＞ `データ` ＞ `名前付き範囲`
を選択し、上記Configシートで用意した Bucket名セルとファイルPathセルにKeyNameを設定する。

<img width="933" alt="gas2gcs_sample_-_Google_スプレッドシート.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/c58cddf7-09a6-6901-fa98-85738998b01b.png">

これで、GASからスプレッドシートにアクセスする準備が整いました。

# GAS (GoogleAppsScript)の設定をする・

## ソースコード追加
スプレッドシート上の `ツール` ＞ `スクリプトエディタ` から新規スクリプトを作成する。適当な名前で保存してOK

今回は、2つの .gs を作成する。
GASエディターのメニュー＞ ファイル＞新規作成＞スクリプトファイル
新しいスクリプトを `gsutil.gs` で保存する。

イカソース

```:javascript
// スクリプトプロパティからクライアント情報を取得する
// ClientIDかClientSecretのどちらかが欠けている場合はfalsyな値を返す
function getClientConfig() {
  var prop = PropertiesService.getScriptProperties()
  var clientId = prop.getProperty("clientId");
  var clientSecret = prop.getProperty("clientSecret");
  if(clientId && clientSecret){
    return {
      clientId: clientId,
      clientSecret: clientSecret
    };
  }
  return null;
}

// GCSにcontentを保存する
function storeContentIntoGCS(content, bucketName, filePath) {
  var service = getStorageService();
  if (!service.hasAccess()) {
    // 未認証
    var authorizationUrl = service.getAuthorizationUrl();
    var template = HtmlService.createTemplate('<center><a href="<?= authorizationUrl ?>" target="_blank">Authorize</a>. </center>' + 
                                              '<center>認証完了にもう一度実行してください</center>');
    template.authorizationUrl = authorizationUrl;
    var page = template.evaluate();
    // 認証URLへ飛ばす
    SpreadsheetApp.getUi().showModalDialog(page, "Google API認証");
    return false
  } else {
    // データを送信
    var url='https://storage.googleapis.com/'+bucketName+filePath;
    UrlFetchApp.fetch(url,{
      headers: {
        Authorization: "Bearer " + service.getAccessToken(),
        // ACL設定
        'x-goog-acl':'public-read'
      },
      method: "PUT",
      contentType: "application/javascript;charset=utf-8",
      host: bucketName + ".storage.googleapis.com",
      payload: content
    });
  }
  return true;
}

// 認証用のサービスを作成する
function getStorageService() {
  var cfg = getClientConfig();
  return OAuth2.createService('provisioning')
  // 全API共通
  .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
  .setTokenUrl('https://accounts.google.com/o/oauth2/token')
  
  // GCP毎に設定
  .setClientId(cfg.clientId)
  .setClientSecret(cfg.clientSecret)
  
  // コールバック関数名
  .setCallbackFunction('authCallback')
  
  // 認証情報の保存
  .setPropertyStore(PropertiesService.getScriptProperties())
  
  // scope:GCSフルコントロール
  .setScope('https://www.googleapis.com/auth/devstorage.full_control')
  
  // ログイン時のヒント  
  .setParam('login_hint', Session.getActiveUser().getEmail())
}

// OAuth認証のコールバック
function authCallback(request) {
  var service = getStorageService();
  var isAuthorized = service.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('<center>Success! You can close this tab.</center>');
  } else {
    return HtmlService.createHtmlOutput('Denied. You can close this tab');
  }
}
```

保存してケロ、そして、元からある `コード.gs` に
イカソースをぺちょる

```:javascript:
var SHEET_NAME = 'data';

///// GCS アクセス関係 /////////////////////////////////////////////////////////////////////////////////////////////

// gcsのアップロード先設定を取得する。
// どれか一つでも欠けていたらfalsyな値を返す
function getGCSSettings(){
  var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
  var settings = {};
  settings.bucketName = spreadSheet.getRangeByName("gcsBucketName").getValue();
  settings.filePathForServer = spreadSheet.getRangeByName("gcsFilePathForServer").getValue();
  if(settings.bucketName && settings.filePathForServer){
    Logger.log("settings: " + JSON.stringify(settings));
    return settings;
  }
  return null;
}


// data.jsonファイルをGCSにアップロードする
function uploadHeroJsonFile(){
  var mapJson = convertSheet2JsonText();
  var content = JSON.stringify(mapJson, null, "  ")
  var gcs = getGCSSettings();
  if(storeContentIntoGCS(content, gcs.bucketName, gcs.filePathForServer)){
    Browser.msgBox("アップロードが完了しました。");
  }
}


// oauth2.provisioning のプロパティを削除する。
function removePropertyOAuth(){
  var x = PropertiesService.getScriptProperties().deleteProperty("oauth2.provisioning");
}


///// JSON 生成 /////////////////////////////////////////////////////////////////////////////////////////////////////
function convertSheet2JsonText(){
  var sheet = getMainSheet();
  
  var colStartIndex = 1;
  var rowNum = 1;
  var firstRange = sheet.getRange(1, 1, 1, sheet.getLastColumn());
  var firstRowValues = firstRange.getValues();
  var titleColumns = firstRowValues[0];
  
  var lastRow = sheet.getLastRow();
  var rowValues = [];
  for(var rowIndex=2; rowIndex<=lastRow; rowIndex++){
    var colStartIndex = 1;
    var rowNum = 1;
    var range = sheet.getRange(rowIndex, colStartIndex, rowNum, sheet.getLastColumn());
    var values = range.getValues();
    rowValues.push(values[0]);
  }
  
  var jsonArray = [];
  for(var i=0; i<rowValues.length; i++) {
    var line = rowValues[i];
    var json = new Object();
    for(var j=0; j<titleColumns.length; j++) {
      json[titleColumns[j]] = line[j];
    }
    jsonArray.push(json);
  }
  Logger.log("JsonData : \n" + JSON.stringify(jsonArray, null, " "));
  return jsonArray;
}

function previewJsonFile(){
  var unitJson = convertSheet2JsonText();
  var template = HtmlService.createTemplate('<pre><?= json ?></pre>');
  template.json = JSON.stringify(unitJson, null, "　");
  var page = template.evaluate();
  SpreadsheetApp.getUi().showModalDialog(page, "プレビュー");
}


// プレビュー用のファンクション
function callPreviewJsonProd(){
  previewJsonFile();
}

// ファイルアップロード用のファンクション
function callUploadJsonProd(){
  uploadHeroJsonFile();
}

// HeroUnitデータのシートを取得
function getMainSheet(){
  return SpreadsheetApp.getActiveSpreadsheet().getSheetByName( SHEET_NAME );
}


// メニューアイテム追加  ///////////////////////////////////////////////////////////////////////////////////////////
function onOpen(){
    var gcpConfig = getClientConfig();
  if(!gcpConfig){
    Browser.msgBox("GCP設定が不足しています。スクリプトプロパティに`clientId`と`clientSecret`を設定してください");
  }
  var gcsSettings = getGCSSettings();
  if(!gcsSettings){
    Browser.msgBox("GCS設定が不足しています。GCSアップロード設定のシートを確認してください");
  }

  var ui = SpreadsheetApp.getUi();
  ui.createMenu('デプロイ')
      .addSubMenu(ui.createMenu('data')
          .addItem('プレビュー', 'callPreviewJsonProd')
          .addItem('アップロード', 'callUploadJsonProd'))
      .addSeparator()
      .addSubMenu(ui.createMenu('reset')
          .addItem('reset OAuth key', 'removePropertyOAuth'))
      .addToUi();  
}

```

保存して、ソースコード部分は終了。

1. ライブラリの登録
今回GCSにOAuthでアクセするために、OAuth2ライブラリを利用する。GASでは、公開されている外部ライブラリを取り込む機能とUIが提供されているためコレを利用する。

リソース＞ライブラリを選択する

<img width="633" alt="gas2gcs_a2c.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/bc41d88f-b1fa-91fe-036a-8708f06d33a9.png">


ライブラリ管理画面が立ち上がるので、下記IDでライブラリを検索し追加する。バージョンが未選択になっているので、最新版を選択しておく（2015/12/07時点では17）

[OAuth2ライブラリ詳細]("https://script.google.com/macros/library/versions/d/MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48")
ライブラリID : `MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48`

<img width="637" alt="gas2gcs_a2c.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/da34ec9b-0816-5fcb-7c57-df4196825cab.png">

これでスクリプトの設定は完了

## [重要]プロジェクト キーを取得し、プロパティーを追加する。

次ステップのバケットへのアクセス設定に必要なプロジェクトキーを取得しておく。

GASエディターメニュー＞ファイル＞ プロジェクトプロパティーを選択し、プロジェクトキーをメモしておく

<img width="584" alt="gas2gcs_a2c.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/5fc91bbe-cef0-a50e-28e0-96bb43cfb6c1.png">

クライアントIDとクライアントシークレットを、ソースコードやスプレッドシートに記載したくない為、スクリプト毎に利用できるプロパティー領域に保存する。そのために、プロパティーを追加しておく。

全ステップで使用したプロジェクトのプロパティーウィンドウの、 `スクリプトのプロパティー` タブを開く

`clientId` と `clientSecret` プロパティーを追加する。
Valueは、OAuthアカウントが未発行の為まだ未記入でよい。

<img width="572" alt="スクリーンショット_2015-12-07_14_08_53_png.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/208f3e59-45a9-90b3-bbc2-297f9d57888f.png">


## GCS(GoogleCloudStorage)でやること
 - GCSバケットの作成（GCPプロジェクトは事前にある前提）
    - OAuthアカウントの生成
        - ClientIDとClientSecret
やっていきましょう。

1. GCSの管理コンソールから「バケットを作成する」 
    2. `https://console.developers.google.com/storage/browser?project=<YOUR_PROJECT_NAME>`
3. <img width="497" alt="スクリーンショット 2015-12-07 13.11.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/0a7a2832-93a0-fce8-f14d-c712d3477cf5.png">
    4. リアライン（一番安いやつ）
    5. リージョン：アジア

入れ物が出来ました。アクセスする為の、OAuthアカウントを作成していきましょう。GCPには、Googleアカウント以外にOAuthアカウントを別途簡単に作成できるページが用意されていますので、そこで今回のアプリケーション用のアカウントを発行します。

1. メインメニューを開きます
    2. <img width="994" alt="ホーム_-_Google_Storage_Project.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/5ac60405-24f1-7495-f0c9-0fa6fd4bce61.png">
3. API Managerを選択ます
    4. <img width="353" alt="API_ライブラリ_-_Google_Storage_Project.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/7cfe4961-53fe-0fbf-4a6b-2d865f0f1626.png">
5. 認証情報を選択します。
    6. <img width="610" alt="認証情報_-_Google_Storage_Project.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/cc4f37f8-430a-32fc-977d-a5a0efdc634a.png">
7. 認証情報を追加します。
    8. OAuth2.0クライアントID
    9. <img width="447" alt="認証情報_-_Google_Storage_Project.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/1e4bb30a-7539-5f0c-5d93-453a8e3fe527.png">
10. 種類で `ウェブアプリケーション`を選択する
    11. 名前を適当につける
    12. 承認済みのリダイレクトURIに、GASのプロパティーからメモったプロジェクトIDをURLに混ぜて記入する。
        13.  `https://script.google.com/macros/d/＜Project Key＞/usercallback`
    14.  <img width="626" alt="クライアント_ID_の作成_-_Google_Storage_Project.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/fd8ae7b7-9a6c-49a9-eef2-0c1cf059f494.png">
15.  クライアント情報が発行されるのでメモる
    16.  ClientIDとClientSecretをメモっておく
    17.  <img width="530" alt="認証情報_-_Google_Storage_Project.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/ef33754c-7efa-a503-9407-91c7777b64a8.png">
18.  スプレッドシートに各種情報を書き戻す
    19.  GASプロジェクトのスクリプトプロパティー
        20.  IDとシークレットを、GASのプロパティーに書き戻す。
    19. バケットの名前をConfigシートに記入する

以上で、GCSの設定が終了

# レッツ 書き出しing！
では、ここまでやったことをチェックしてみましょう

## チェックリスト

- スプレッドシート
    - Configシートに、バケットネームは書いてありますか？
    - スクリプトが2つ作成されていますか？
        - `コード.gs` と `gsutil.gs`
    - スクリプトプロパティーが設定されていますか？
        - `clientId` と `clientSecret`
- GCS
    - バケットが作成されていますか？
    - 認証情報
        - OAuthクライアントがWebアプリケーションが作成されていますか？
        - 承認済みリダイレクトURIが設定されていますか？
            - リダイレクトURIに、GASのスクリプトプロジェクトKeyが設定されていますか？


すべて設定されていたら、書き出しが出来ます。

スプレッドシートの、`data`シートの 1段目がキー名になります。A,B・・・とプロパティーが増やせます。2段目以降が、実際のデータになります。

例えばこういうデータだったとして

<img width="362" alt="gas2gcs_sample_-_Google_スプレッドシート.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/fa28d856-5d0f-f3dd-f28d-6f9902543258.png">

吐き出す前に、事前にプレビューしてみましょう。
メニューバーに `デプロイ` が追加されていますので data> プレビュー してみます。

<img width="387" alt="gas2gcs_sample_-_Google_スプレッドシート.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/1d894ecd-6e28-2b30-f902-7562d12756c0.png">

新規ダイアログが表示され、JSONが適切に表示されるはずです。思ったとおりになっているか確認してください。

<img width="595" alt="gas2gcs_sample_-_Google_スプレッドシート.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/23900bd2-f5ed-0dfe-56f9-2dead94a31de.png">

さぁ、ここまで来たらあとは、GCSに書き出してみましょう。
まだ、一度もGCSに書き出して無い場合には、承認フローが必要です。
初回の `アップロード` 時にのみ発動します。では、アップロードしてみましょう。

<img width="585" alt="gas2gcs_sample_-_Google_スプレッドシート.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/9e536439-db27-c984-7df9-782e928e34b0.png">

アップロードすると、承認してくださいダイアログが開くので、 `Authorize` をクリックすると別ウィンドウが開き、アクセスを許可するかGoogleから聞かれます。

`Success! You can close this tab. `

が表示されたら成功です。そのウィンドウは閉じてください。
承認してくださいダイアログを一旦閉じて、再度アップロードします。このタイミングで初めてファイルがアップロードされます。

<img width="399" alt="スクリーンショット 2015-12-07 16.01.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/6cddfdc0-9fed-02ad-ec94-387aa0206d3c.png">

無事に、アップロードに成功しました。
gcs_configシートの B4 セルに公開Pathが乗っていますのでそこにアクセスると

<img width="582" alt="https___storage_googleapis_com_gas2gcs_201512_public_sample_json.png" src="https://qiita-image-store.s3.amazonaws.com/0/4036/6a50e994-90fc-d732-0ccd-cd42edae3630.png">

タダーーーッ！！
無事に、GCS経由でJSONが公開されています。

あとは、CORSの設定して外部からアクセスしたらり、サービスアカウント認証情報追加して、GCEからバリバリ使ったり自由自在です。