こうりんのブログ

主に勉強した事を書いていくつもり

HTTPトリガーのCloud FunctionsをTypeScriptで開発してローカルでデバッグしてみる

HTTPトリガーで動くCloud Functionsを, TypeScriptからトランスパイルしたコードを使って開発してみる.
更に, コードをローカル環境でデバッグしてGCPにデプロイしてみる.

今回は題材として, 下の記事で書いた機能をTypeScriptで書き直してみる.
kourin.hatenadiary.com

ローカルのOSはmacOS High Sierra10.13.4で, ターミナルはzshです.

ngrokのインストール

ngrokはlocalhostで動いているサーバに外部からアクセスするためにURLを発行してくれるツール.
今回はSlackからCloud Functionsの方にアクセスするため, このツールを使用してlocalhostにアクセスできるようにする.
ngrokを使うと外部からアクセスできるようになるので, 使う時だけ起動するように....

以下でインストールできる.

$ brew install homebrew/binary/ngrok2

Nodeのインストール

Cloud Functionsが使えるnodeのバージョンはv6.11.5らしいので, ローカル環境もそれに合わせる.
nodeのバージョン管理には nodebrew を使う.

$ brew install nodebrew
$ nodebrew setup

.bash_profile(zshは.zshrc)に以下を追加する.

$ export PATH=/usr/local/var/nodebrew/current/bin:$PATH
$ export PATH=$HOME/.nodebrew/current/bin:$PATH

source ~/.bash_profilesource ~/.zshrc をして追加した部分を反映する.

nodebrewを使って, v6.11.5をインストールする.

# インストールできるバージョンを確認
$ nodebrew ls-all

# node v6.11.5をインストール
$ nodebrew install-binary v6.11.5

# node v6.11.5を使う
$ nodebrew use v5.7.1

TypeScript開発環境構築

プロジェクトのディレクトリを作り, 以下を実行する.

$ npm install typescript tslint --save-dev

tsconfig.jsonpackage.json を追加する.

tsconfig.json
{
    "compilerOptions": {
      "target": "es6",
      "module": "commonjs",
      "outDir": "./",
      "noImplicitAny": true,
      "strictNullChecks": true,
      "sourceMap": true
    },
    "include": [
      "src/**/*.ts"
    ],
    "exclude": [
      "node_modeles"
    ]
  }
package.json
{
  "name": "slack_oauth",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc",
    "watch": "tsc -w"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "typescript": "^2.8.3"
  }
}

srcディレクトリを作り, src内にtypescriptのコードを書く.

index.ts
export const onReceiveSlackOAuth = (req, res) => {
    console.log(req);
    res.end();
};

npm run buildを実行すると, index.js が生成される.

Google Cloud Functions Emulatorの導入

Google Cloud Functions Emulatorをインストールする

$ npm install -g @google-cloud/functions-emulator

functions で使用できるが, zsh の場合は他で使われてるので, 代わりに functions-emulator を使う.

# 一般的なシェルの場合
$ functions start
$ functions deploy onReceiveSlackOAuth --trigger-http

# zshの場合(zshだとfunctionsが既に使われている)
$ functions-emulator start
$ functions-emulator deploy onReceiveSlackOAuth --trigger-http

deployすると以下のような情報が表示される.
その中からResourceの値をメモしておく.

f:id:Kourin1996:20180513152839p:plain

Resourceに書かれてるURLからポート番号をみて, ngrokを起動する.
(今回は8010番)

$ ngrok http 8010

f:id:Kourin1996:20180513153735p:plain

http://(hogehoge).ngrok.io と書かれたURLが外部から見たURLである.
このURLに外部からアクセスすると, localhost:8010に繋がる.
cloud functions emulatorのResourceに書かれてるURLから http://localhost:8010 の部分をngrokのURLに変更する.
つまり, http://(hogehoge).ngrok.io/poised-ceiling-202712/us-central1/onReceiveSlackOAuth となる.
このURLに外部からアクセスすると, functions emulatorが関数を実行する.

このURLをSlack Appで設定してみる.
OAuth & PermissionsRedirect URL にURLを設定する.

そして, https://slack.com/oauth/authorize?client_id=(クライアントID)&scope=(設定したいスコープ)にブラウザからアクセスする.

ngrokの画面にリクエストログが追加されたら, ローカル環境にアクセスできている.
関数のログを見るには以下のコマンドを実行する.

$ functions-emulator logs read

とりあえず, リクエストの中身をlogに表示するだけのコードなので, リクエストの中身がログに表示されていればOK.

コードの記述

実際にコードをOAuth認証のためのコードを書いていく.
GCP上で環境変数の値を取得するための @google-cloud/rcloadenv とHTTPリクエストを投げるための request モジュールを使う.
npm install -D @types/(モジュール名) でTypeScriptに必要な型定義ファイルを持ってこれる.
(残念ながらrcloadenvの型定義ファイルは見つからなかった)

$npm install request --save
$npm install @types/request
$npm install @google-cloud/rcloadenv --save

コードを書く, 基本的には前回の記事と変わらない.

index.ts
import * as rcloadenv from '@google-cloud/rcloadenv'
import * as request from 'request'

export const onReceiveSlackOAuth = (req: any, res: any) => {
    if(req.method !== 'GET' || req.query.code === undefined) {
        const error_message = "Illegal Request, wrong method: " + req.method;
        console.error(error_message);
        res.status(405).send(error_message);
        return;
    }
    if (req.query.code === undefined) {
        const error_message = "Illegal Request, no required query: " + req.query;
        console.error(new Error(error_message));
        res.status(400).send(error_message);
        return;
    }

    const code = req.query.code;
    console.log("Correct Request: code=", code);
    rcloadenv.getAndApply('test-slackbot', {}).then((env:any) => {
        if ( env.CLIENT_ID !== undefined && env.CLIENT_SECRET != undefined ) {
            const CLIENT_ID = env.CLIENT_ID;
            const CLIENT_SECRET = env.CLIENT_SECRET;
            
            const url = 'https://slack.com/api/oauth.access';
            request.get({
                uri: url,
                headers: {
                    'Content-type': 'application/json'
                },
                qs: {
                    client_id: CLIENT_ID,
                    client_secret: CLIENT_SECRET,
                    code: code
                },
                json: true
            }, (err, req, data) => {
                if (data) {
                    if (data.ok) {
                        const access_token = data.access_token;
                        const team_name = data.team_name;
                        const team_id = data.team_id;
                        const message = "OAuth Successful"
                        res.status(200).send(message);
                    } else {
                        const error_message = 'OAuth Failure';
                        console.error(error_message, data.error);
                        res.status(500).send(error_message);
                    }
                }
                if (err) {
                    const error_message = 'OAuth Failure';
                    console.error(error_message, err);
                    res.status(500).send(error_message);
                }
            })
        } else {
            const error_message = "Failure: TOKEN_ID and TOKEN_SECRET is not set";
            console.error(new Error(error_message));
            
            res.status(500).send(error_message);
        }
    }).catch(console.error);
    return;
};

ローカル環境でGCP上と変わらずrcloadenvなどのAPIを使うために, CREDENTIALSを設定する.
gloud authでCREDENTIALSファイルを生成する.
GOOGLE_APPLICATION_CREDENTIALSに生成されたjsonファイルのパスを指定する.

$ gcloud auth application-default login
$ export GOOGLE_APPLICATION_CREDENTIALS="(上のコマンドの結果に出てきたjsonファイルのパス)"
$ functions-emulator restart

ビルドしてデプロイする.

$ npm run build
$ functions-emulator deploy onReceiveSlackOAuth --trigger-http

再度ブラウザから https://slack.com/oauth/authorize?client_id=(クライアントID)&scope=(設定したいスコープ)にアクセスする.

GCPへデプロイ

せっかくなので, ローカルのターミナルからGCPにデプロイしてみる.

$ gcloud beta functions deploy onReceiveSlackOAuth --trigger-http

GCPのコンソールからCloud Functionsを選択し, 関数が追加されていたらデプロイされている.
関数の設定画面からURLを取得し, Slack Appの設定でURLをセットしたら終わり.

参考

Mac上にngrokをインストール - Qiita

Cloud Functions Execution Environment  |  Cloud Functions Documentation  |  Google Cloud

Cloud Functions を TypeScript で書く - Qiita

GitHub - GoogleCloudPlatform/cloud-functions-emulator: A local emulator for Google Cloud Functions that allows you to deploy, run, and debug your Cloud Functions on your local machine before deploying them to the production Google Cloud Functions service.

TypeScript2.0時代の、型定義ファイル - Qiita