AppEngine 使ってバッチ実行環境をクラウドにもっていくメモ

AppEngine とは
---------------

### 使いどころ

- 実験的な内容をサーバで直ちに動かしたい。取り急ぎ、スマホで画面見たいなど。
- 本番には向かない。

### 便利なところ

- node とか python とかもろもろインストールすることなく、直ちに試せる。
- 直ちにサーバで動かせる。
- 直ちに一般公開できる。


### 一方で、不便なところ

- サーバ上のコード、生成物などは自分のみしか触れない(/home/自分/ の下しか置けない)。
- サーバに置いたから誰かバッチ実行して、みたいなことはできない。
- そうしたいなら git で共有して、その人のアカウント環境で実装してもらう。
- 本番 deploy には時間がかかるので、頻繁にデプロイするのには向かない。:8080 で自分だけの実行なら時間はかからない。
- ログやデータをローカルに保持できない。


ワード
----------

- サービス: AppEngine では各種機能をマイクロサービスとしてデプロイすることになる。デプロイの単位。ソースコード＋設定で構成。
- バージョン: ソースコードは複数のスナップショットを持てる。これがバージョン。デプロイ時にどのバージョンを使うか指定する。
- インスタンス: サービス x バージョン でデプロイする際に、実際に起動する仮想サーバ。
- スケーリング: 自動、手動、基本がある。基本的には無しでも。
- 無料の場合の制限:   5 サービス/アプリ、 15 バージョン/アプリ
- 有料の場合の制限: 105 サービス/アプリ、210 バージョン/アプリ
- Standard Environment / Flexible Environment: マシンを細かく指定したいか。通常は Standard で。


導入手順
----------

### 想定(下記を実現してみる)

- AppEngine でバッチ実行をする。(Node.js v8)
- 実行の引数を Web 画面で指定し、実行のログをリアルタイムに受け取る。(Node Express, Firebase Realtime Database)

### 手順

- GCP でプロジェクトを作り、App Engine の画面に行く。
- Google Cloud Shell 起動（画面右上の `>_` アイコン）。
- `git clone` してソースを持ってくる。その中には下記が含まれている。
    - `app.js`: メインで動く、express のファイル。
    - `app.yaml`: デプロイ時の設定
    - `asset/log.js`: Firebase Realtime Database 経由でログを受け取るJS
    - `firebase-config.json`: Firebase Realtime Database を使うためのコンフィグ
    - `package.json`: npm install するための設定
- clone したディレクトリに移動。
- `npm install`
- `node app`
- 右上にある 8080 でプレビューで画面確認。


とりあえず起動
----------

```
git pull
npm install
node app
```

- その後、右上にある 8080 でプレビューで画面確認。


デプロイ
----------

```
# 最新に更新
git pull

# デプロイ（けっこう時間かかる）
gcloud app deploy

# ログ
gcloud app logs tail
```

- ダッシュボードでもエラーログみれる。


ファイルの中身
----------

- app.js

```js
'use strict';

const util = require('util');
const path = require('path');
const firebase = require('firebase/app');
require('firebase/database');
const firebaseConfig = require('./firebase-config');

// base
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({limit: '5mb', extended: true}));

// static files
app.use(express.static(path.join(__dirname, 'asset')));

// 起動画面
app.get('/', function (req, res) {
    res.send(`<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.3.1/firebase.js"></script>
    <script>
        var config = ${JSON.stringify(firebaseConfig)};
    </script>
    <script src="logging.js"></script>
</head>
<body>
<form action="execute" method="post">
    <textarea name="data" cols="120" rows="20"></textarea><br/>
    <input type="submit" value="送信">
</form>
<pre id="log"></pre>
</body>
</html>`);
});

app.post('/execute', function (req, res) {
    // ログ出力準備
    const logRef = firebase.database().ref('logRef');
    logRef.set(null); // ログ初期化
    const logger = {};
    logger.log = (...args) => {
        args = args.map(a => typeof a === 'object' ? util.inspect(a, false, null) : a);
        logRef.push().set(args.join(' '));
        console.log(args.join(' '));
    };
    logger.error = (...args) => {
        logger.log('ERROR', ...args);
        console.error(...args);
    };
    // いったん返す(ログ初期化後)
    res.redirect('/');
    // Dialogflow 準備
    (async function () {
        try {
            // データ受け取り
            let str = req.body.data;
            // データ処理
            // ...
            // ログ出力
            logRef.push().set('ログ出力！');
        } catch (e) {
            logRef.push().set('ERR: ' + util.inspect(e, false, null));
        }
    })();
});

if (module === require.main) {
    const server = app.listen(process.env.PORT || 8080, () => {
        const port = server.address().port;
        console.log(`App listening on port ${port}`);
    });
}

module.exports = app;
```

- app.yaml

```yaml
runtime: nodejs8

env_variables:
  SOME_VAR_NAME: "SOME_VAR_VALUE"
```

- `asset/log.js`: Firebase Realtime Database 経由でログを受け取るJS

```js
firebase.initializeApp(config);
$(function () {
    let $log = $('#log');
    firebase.database().ref('logUpdateIntentsRef').on('child_added', function(data) {
        $log.text($log.text() + data.val() + "\n");
    });
});
```

- `firebase-config.json`: Firebase Realtime Database を使うためのコンフィグ

```json
{
  "apiKey": "xxx",
  "authDomain": "xxx.firebaseapp.com",
  "databaseURL": "https://xxx.firebaseio.com",
  "projectId": "xxx",
  "storageBucket": "xxx.appspot.com",
  "messagingSenderId": "xxx"
}
```

- `package.json`: npm install するための設定

```json
{
  "name": "xxx",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.16.3",
    "firebase": "^5.3.1"
  }
}
```

### app.yaml の書き方

https://cloud.google.com/appengine/docs/standard/nodejs/configuring-your-app-with-app-yaml?hl=ja