こうりんのブログ

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

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

GCEインスタンスにコンテナをデプロイしてサーバを動かしてみる

昨日の続き.
コンテナイメージをDocker Hubに公開し, GCEインスタンスを作成した際に自動でコンテナイメージを取得・展開するところまでやりました.

今日はインスタンス上にコンテナが展開された時に, アプリケーションが動作しているのを確認します.
簡単なWebサーバプログラムをリポジトリに用意し, コンテナ展開時にpullして起動するようにしてみます.

プログラムの作成とDockerfileへの追加

Dockerfileと同じ階層にWebサーバのコードと表示させるHTMLを配置し, gitにpushする.
(ソースコードは別のリポジトリでも構わない, 今回は一緒のリポジトリにした)

simpleserver.py
import http.server

server_address = ("", 8080)
handler_class = http.server.SimpleHTTPRequestHandler
simple_server = http.server.HTTPServer(server_address, handler_class)
simple_server.serve_forever()
index.html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Test</title>
  </head>
  <body>
    <p>This is Test Page</p>
  </body>
</html>

次にDockerfileを編集する.
CMDはデプロイされて実際にコンテナが起動する時に実行するコマンドを指定する.
今回はgit pullしてサーバを起動させる.

Dockerfile
FROM coreos/apache
RUN \
    apt-get update && \
    apt-get install -y wget curl git && \
    # C/C++
    apt-get install -y build-essential make && \
    # Python3.6
    apt-get install -y zlib1g-dev libssl-dev \
        libreadline-dev libsqlite3-dev libbz2-dev \
        libncurses5-dev libgdbm-dev liblzma-dev \
        tk-dev zlibc && \
    cd /usr/local/src && \
    wget --no-check-certificate https://www.python.org/ftp/python/3.6.3/Python-3.6.3.tgz && \
    tar zxvf Python-3.6.3.tgz && \
    cd Python-3.6.3 && \
    ./configure && \
    make && \
    make altinstall && \
    cd ~ && \
    curl -kL https://bootstrap.pypa.io/get-pip.py | python3.6 && \
    #WebServer
    cd /home && \
    git clone (リポジトリのアドレス)
WORKDIR /home/(リポジトリ名)
EXPOSE 8080
CMD \
    git pull && \
    python3.6 /home/(リポジトリ名)/simpleserver.py

ローカル環境で動作を確認してみる.
buildしてrunをする.
runの-dオプションを使うと, バックグラウンドでコンテナを実行する.
-pはポートフォワーディングの設定で, (ホストのポート):(コンテナのポート)で指定する.
ホストのIPアドレスと指定したポートで接続すると, 自動でコンテナの指定のポートに繋げてくれる.

docker build ./ -t (アカウント名)/(イメージ名)
docker run -d -p 8080:8080 -t (アカウント名)/(イメージ名)

macからWebサーバが起動しているか確認する.
(ホストのIP):8080で繋がるが, 今回コンテナはdocker-machine上で動作しているため, docker-machineのIPを指定する.
docker-machineのIPは以下のコマンドで確認できる.

docker-machine ip default

curlを叩いて, HTMLが吐けば正常に動作している.

curl (docker-machineのIP):8080

終了する場合は, docker psでコンテナIDを確認して, docker rmで終了.

GCEインスタンスに展開

前回同様, コンテナイメージの所で, Docker Hubのリポジトリのアドレスを指定してGCEのインスタンスを作成する.
VMインスタンスのリストから ssh を押すとブラウザ上でターミナルが開く.
docker ps でコンテナが立ち上がってるのを確認して, ローカル同様にcurlを叩く.
ifconfigをして docker0 となってるインターフェースのIPがdockerのコンテナへ繋ぐIPアドレスである.
コンテナが起動していれば, 正常にHTMLが取得できる.

ただ, ここまではGCEインスタンス内での通信なので, 最後に外部から接続できるようにする.
コンソールからネットワーキング >> ファイアウォールルールを選択し, ファイアウォールルールの作成を押す.
以下の設定は, 全ての送信元から8080ポートを使用したTCPトラフィックを受信する設定なので注意.

  • ターゲットを ネットワーク上のすべてのインスタンス にする
  • ソースフィルタを IP範囲 にする
  • ソースIPの範囲を 0.0.0.0/0 にする
  • プロトコルとポートを 指定したプロトコルとポート にし, 設定を tcp:8080 にする.

設定し, GCEのインスタンスの外部IPと8080ポートを指定してブラウザでアクセスするとページが表示される.

参考

みんなのPython Webアプリ編 - PythonでWebサーバを作る | TRIVIAL TECHNOLOGIES 4 @ats のイクメン日記

Dockerfile リファレンス — Docker-docs-ja 17.06.Beta ドキュメント

run — Docker-docs-ja 17.06.Beta ドキュメント

Dockerイメージ作ってGCEにデプロイするまで

Google Compute EngineでVMインスタンスを作ろうとブラウザで設定していたら, VMインスタンスに自動でコンテナイメージをデプロイできるらしい.
Google Container Registryにリポジトリを置くこともできるが, 今回はDocker Hubに置いたリポジトリを参照してデプロイする方向でやってみようと思う.
そもそも, Dockerを使ったこと無いので, 基本的なイメージ作りから触ってみることにした.

今回は, 作成したDockerイメージをGCEのインスタンスが立ち上がった時に自動で読み込まれるところまで行う.
(動作確認とかはまた次回)

ローカルでDockerのイメージ作成

まず, Docker-machineを作る.
Docker-machineはmacとかでコンテナを動かすためのVM環境のことらしい.
つまり, macの上でDocker-machineが動き, Docker-machineの上でDockerコンテナが動くみたい.

docker-machine create --driver virtualbox default
docker-machine env default
eval "$(docker-machine env default)"

次にDockerのイメージの定義ファイルであるDockerfileを作成する.
まず始めに最小構成として以下のようにした.

Dockerfile
# Base Image
FROM coreos/apache
# User Info
MAINTAINER kourin
# RUN: run in docker build
RUN echo "now building..."
# CMD: run in docker run
CMD echo "now running..."

docker build でDockerfileを元にDockerイメージを作成できる.
docker run でCMDコマンドで設定したコマンドが実行される.

docker build ./ -t (つけたいイメージ名)
docker run  (イメージ名)

以下のようにすると作成したdockerの環境下に入れる.

docker run -it (イメージ名) /bin/bash

出るときはexitで出れるが, 環境下で変更した部分は消えてしまうので注意.
環境下でコマンドを確認しつつ, Dockerfileに環境構築のためのコマンドを追記していけば良いらしい.
そして, docker build でイメージを更新していく.

今回は、とりあえずC/C++コンパイラとPython3.6が実行できる環境を構築した.
以下がそのDockerfile
EXPOSEは外部に向けて公開するポートを記す.

Dockerfile(ver2)
# Base Image
FROM coreos/apache
# User Info
MAINTAINER kourin

# build
RUN \
    apt-get update && \
    # C/C++
    apt-get install -y build-essential make && \
    # Python3.6
    apt-get install -y zlib1g-dev libssl-dev libreadline-dev \
        libsqlite3-dev libbz2-dev libncurses5-dev libgdbm-dev liblzma-dev \
        tk-dev zlibc wget curl && \
    cd /usr/local/src && \
    wget --no-check-certificate https://www.python.org/ftp/python/3.6.3/Python-3.6.3.tgz && \
    tar zxvf Python-3.6.3.tgz && \
    cd Python-3.6.3 && \
    ./configure && \
    make && \
    make altinstall && \
    cd ~ && \
    curl -kL https://bootstrap.pypa.io/get-pip.py | python3.6

EXPOSE 80
EXPOSE 8080

DockerHubにpush

DockerHubというDockerイメージを保存できるリポジトリがある.
以下のサイトでアカウントを作成する.

https://hub.docker.com/

今回はGithubと連携してみる.
GithubでDockerfileを管理し, DockerfileをpushするとDockerHubでイメージがビルドされる.
まず, Docker HubのアカウントのSettingsからGithubとのアカウント連携を行う.
次に, 上のバーからCreate >> Create Automated Build >> Githubを選び, 任意のGithubリポジトリを選択する.

GithubにDockerfileをpushすると, DockerHubのリポジトリも反映される.
Docker Hubのbuild detailsを見れば, イメージのbuild状況が確認できる.

GCE インスタンスの作成

GCPのコンソールからインスタンスを作成する.
マシンタイプは f1-micro にし, ゾーンをusから始まる名前にすると無料枠で利用できる.
この VM インスタンスにコンテナ イメージをデプロイします。 にチェックを入れる.
コンテナイメージは, docker.io/(アカウント名)/(リポジトリ名) とする.
今回はお試しなので, とりあえず特に他に設定せずに作成.

sshインスタンスに接続する, しばらく待ってdocker imagesを実行すると指定したコンテナイメージが出てくる.
docker runでイメージを起動してシェルの中に入れる.

DockerfileのCMDでサーバを起動するコマンドとかを指定すれば, 展開後プロセスが起動して終了しないので, docker psでプロセスが見れると思う.
次回は実際にプログラムを書いて, 自動でアプリケーションをビルドするところをしていく.

参考

「Cannot connect to the Docker daemon. 」と怒られる時の対処法 – 不動産屋のエンジニア

効率的に安全な Dockerfile を作るには - Qiita

Docker Hubを使ってGitHubにあるDockerfileからimageを自動生成する – Simple IT Life

VM およびマネージド インスタンス グループへのコンテナのデプロイ  |  Compute Engine ドキュメント  |  Google Cloud

Google Cloud PlatformでSlackBot開発【EventsAPIで受信編】

前回, GCPのCloud Functionsを使って, Slack Appの認証を行うところまでは終わりました.
今回は, Slackのイベントを受信をしてそれに対する簡単なアクションを返すところまでやってみようと思います.

AWSでの場合についてみると, Amazon API Gatewayを経由してAWS Lambdaを実行しているようです.
同様にGCPで考えると, Cloud Endpoints経由でCloud Functionsにリクエストを渡す感じかな?と思ったのですが, そもそもCloud FunctionsはHTTP リクエストをそのままイベントとして動くので, 単体でできそうな気がします.

というわけで構成はこんな感じ.

f:id:Kourin1996:20180501045824j:plain

Slackでイベントが発生するとWebhookが起動します.
フック先をCloud Functionにしておきます, Cloud FunctionではSlackのWeb APIを叩きます.
WebAPIではメッセージの送信などができます.

Cloud Functionの作成(イベント受信)

前回同様にHTTPリクエストを受信した際に実行する関数を作成します.
EventsAPIでは, hook先のアドレスを登録する際に正しいURLかどうか確認が行われます.
これは, challenge パラメータがを含むPOSTリクエストが送られた時, challenge の値だけを返せば大丈夫です.
というわけで, とりあえず以下のようにしました.

index.js
exports.onReceiveSlackEvents = (req, res) => {
    if (req.body.challenge !== undefined) {
        console.log('on Challenge Request');
        res.end(req.body.challenge);
        return;
    }
    console.log(req.body);
    res.status(200).send('hoge');
};

Slack Appの設定

サイドメニューから Event Subscriptions に移動し, Enable Events をonにします.
そして, Request URL をCloud FunctionsのURLにします.
この時URLの確認が行われます, 正しくCloud Functionsの設定ができていれば, 成功します.

続いて, Subscribe to Workspace Events から受信するイベントを選びます.
今回は, ユーザのメッセージ投下を確認するために, message.channels を選択します.
右下のSave Changesを選択して設定は完了です.

Event受信確認

botを適当なチャンネルに招待して, チャンネルにメッセージをpostしてみます.
StackdriverのLoggingから次のようなログが流れてくれば, 正しくイベントを受信しています.

f:id:Kourin1996:20180501053431p:plain

Cloud Functionの作成(メッセージ送信)

最後にメッセージ送信部分を記述します.

今回はnode.jsの以下のライブラリを用いました.

github.com

ソースは下のような感じ.
今回は購読するユーザを直書きしました.
トークンは前回の最後に取得したトークンのことです.
設定画面では OAuth & Permissions >> OAuth Tokens & Redirect URLs >> Tokens for Your Workspace >> OAuth Access Token に書いてあります.

userを指定しないと, botのメッセージもイベントで受信して返事しちゃうので, botのメッセージ送信が止まらなくなる.
ちゃんとしたコードを書くときは, 受信したイベントがbot自身かチェックする必要がある.
また, 最後にres.end(null);をしないとタイムアウトまで終わらないのでメッセージが2回送信されることもある.

index.js
exports.onReceiveSlackEvents = (req, res) => {
    if (req.body.challenge !== undefined) {
        console.log('on Challenge Request');
        res.end(req.body.challenge);
        return;
    }

    const { WebClient } = require('@slack/client');
    const channelID = (送信するチャンネルID);
    const token = (トークン);
    const user = (対象のユーザ);

    console.log('onReceive Slack Event ', req.body);
    let web = new WebClient(token);

    if (req.body.event.type === 'message' && req.body.event.user === user) {
        let msgBody = req.body.event.text;
        web.chat.postMessage({
                channel: channelID,
                text: msgBody
            })
            .then((res) => {
                console.log('Message sent: ', res);
            })
            .catch(console.error);
    };
    res.end(null);
    return;
};
package.json
{
  "name": "Test-SlackApp-EventsAPI",
  "version": "0.0.1",
   "dependencies": {
    "@slack/client": "^4.2.0"
  }
}

botをチャンネルに招待しメッセージをチャンネルに投稿して, メッセージが返ってきたら終わり.

参考

qiita.com

Google Cloud PlatformでSlackBot開発【Slack App認証編】

この前Herokuでbot動かすとこまでやったけど, 今回はGCP(Google Cloud Platform)で動かそうと思います.
GCP動かしてみたかった. Events APIも使ってみようというのが理由です.

でも, HTTPを用いるEvent APIは30,000 Events/hが上限らしいので, 一応注意.
(https://api.slack.com/events-api#rate_limiting)

という事で, GCP(Google Cloud Platform)上でbotを作成していきます.
今回は認証まで.

Slack Appの作成

前回とほぼ同じだと思うけど, 一応書く.
Slack APIのトップページから Start Building >> Create New App で新しいSlackアプリを作成できる.
Slack API | Slack

アプリ作成直後のページがこんな感じ.
左のサイドメニューから各種設定をする.

f:id:Kourin1996:20180430221639p:plain

まず, Basic InformationApp Credentials をみると, Client IDClient Secret というのが書いてある.
これは, アプリ自体の固有なIDの事らしい.

次に, Bot Users ではBotの設定ができる.
Add a Bot User を押すと, 名前などを設定でき, Add Bot User と押すと, アプリを追加した際にワークスペース上にBotが登場する.

次に, OAuth & PermissionsScopes の設定.
ここでは, アプリが使用できるAPIの種類を設定する.
あまり沢山のスコープを設定すると, アプリをSlackに公開する際に大変らしいので, 必要最低限の権限だけ追加する.
APIのメソッドとスコープの対応関係は以下に記載されている.

OAuth Scopes | Slack

そして, 最後にOAuth Tokens& Redirect URLs だが, GCPとの連携が必要なので後ほど述べる.

以上でSlack Appの最低限の設定は終わり.
次はGCPの設定に移っていく.

GCPの登録

GCPのページに行くとトップにTry It Freeと書いてある.
GCPではお試しとして, 12ヶ月$300分が使えるらしい.
(2018年4月30日現在)
お試し期間を過ぎてしまっても, 急に請求が来ることは無いらしい. (本当か?)
一番性能の低いVM1個だけだったら無料で使えたりというような, サービス毎に無料枠が設定されてることもある.
無料枠の範囲内ならお試し期間過ぎても大丈夫そう.

Try It Free を押すと登録画面に移動する.
利用規約の承諾をして進むと, 住所とクレジットカードの番号を入力する画面に移動する.
必要事項を入力して, 無料トライアルを始める を押すと登録が完了する.
クレジットカード番号を入力するが, もし仮に課金が開始されようとなるときはちゃんとGoogleから連絡来て承認してかららしい.
とりあえず, 安心.
(多分, 自分以外誰も使わないレベルでの運用なら課金なんて微々たる気がするけど)

GCPとSlack Appの連携

今回はSlack Appを動かすために必要な認証をGCP上で行う.
Slack Appはワークスペースに追加する際に, 特定のURLへリダイレクトをできる.
このリダイレクトを用いて認証する事で, HTTPの通信でトークンを発行する事ができる.

以下に今回の構成図を示す.
f:id:Kourin1996:20180430224246p:plain
先ほど言ったリダイレクト先をGCPのCloud Functionsに設定する.
プログラムのログとかはStackdriverで確認できるみたい.

GCP SDKのインスール

ローカルのターミナル上でGCPを操作するために, GCP SDKをインストールする.
以下のURLにアクセスして, 自分の環境にあったファイルをダウンロードする.

Installing Google Cloud SDK  |  Cloud SDK Documentation  |  Google Cloud

以下のコマンドを実行してインストールする.
ちなみに, gcloud initをするとブラウザが立ち上がるので使用するGoogleアカウントを選択すると自動でプロジェクトの一覧とか読んできてくれる.
すごくべんり

cd ~/Downloads
tar zxvf ./google-cloud-sdk-199.0.0-darwin-x86_64.tar.gz
./google-cloud-sdk/install.sh
./google-cloud-sdk/bin/gcloud init

環境変数の設定

認証にはAppのIDが必要だけど, ソースに直書きするのも色々問題なので, 環境変数から読み込むことにする.

GCP上での環境変数の設定はconfigという単位で管理される.
以下でconfigを作れる, 最後の引数がconfig名である.

gcloud beta runtime-config configs create (config名)

configのリストは以下で確認できる.

gcloud beta runtime-config configs list

今回は Client IDClient Secret環境変数にセットする.
SlackのAppの設定画面の Basic Information >> App Credentials から値を確認する.

環境変数の設定は以下のコマンドで実行できる.

gcloud beta runtime-config configs variables set (変数名) (値) --config-name (config名) --is-text

セットされた変数の名前と変数の値はそれぞれ以下のコマンドで確認できる.

gcloud beta runtime-config configs variables list --config-name (config名)

gcloud beta runtime-config configs variables get-value (変数名) --config-name (config名)

Cloud Functionsの作成

本題のCloud Functionを作成する.
GCPのホームから Cloud Functions を選択して関数を作成する.
名前と割り当てるメモリを選ぶ.
メモリは分からなかったので最小の128MBを選んだ.
トリガーはリダイレクトで実行するのでHTTPとする.
トリガーの以下にURLが表示されるので, これを後でリダイレクトURLとして設定.

次に実行する関数を記述する.
SlackAppの認証の流れは次の通りである.

  1. ワークスペースにアプリを追加する.(ユーザがブラウザ上で実行)
  2. 設定したサーバのURLにリダイレクト, リクエストはcodeというgetパラメータを持つ
  3. サーバはSlackAPIにcode, client id, client secretを付加したGETリクエストを送る
  4. Slackからjsonが返ってくる(okパラメータがtrueだったら認証成功)

今回はとりあえず, 認証を行った後は最終的に返ってくるjsonをそのまま表示するようにした.

index.js
/**
 * Responds to any HTTP request that can provide a "message" field in the body.
 *
 * @param {!Object} req Cloud Function request context.
 * @param {!Object} res Cloud Function response context.
 */

const request = require('request');
const rcloadenv = require('@google-cloud/rcloadenv');

exports.onRedirectSlackOAuth = (req, res) => {
    //リダイレクトのリクエストの検証
    if (req.method !== 'GET') {
        let error_message = "Illegal Request, wrong method";
        console.log(error_message, req.method);
        res.status(405).send(error_message);
        return;
    }
    if (req.query.code === undefined) {
        let error_message = "Illegal Request, no required query"
        console.log(error_message, req.query);
        res.status(400).send(error_message);
        return;
    }

    let code = req.query.code;

    //環境変数の取得
    rcloadenv.getAndApply('test-slackbot', {}).then((env) => {
        if (env.CLIENT_ID === undefined || env.CLIENT_SECRET === undefined) {
            console.error('Failure: TOKEN_ID and TOKEN_SECRET is not set');
            let error_message = 'Sorry Failure OAuth by Server Error';
            res.status(500).send(error_message);
            return;
        }

        //Slack APIを叩く
        let client_id = env.CLIENT_ID;
        let client_secret = env.CLIENT_SECRET;
        let 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
        }, function(err, req, data) {
            if (data) {
                if (data.ok) {
                    //認証成功
                    let access_token = data.access_token;
                    let team_name = data.team_name;
                    let team_id = data.team_id;
                    let message = "OAuth Successful"
                    console.log(message, data);
                    res.status(200).send(message);
                } else {
                    let error_message = 'OAuth Failure';
                    console.error(error_message, data.error);
                    res.status(500).send(error_message);
                }
            }
            if (err) {
                let error_message = 'OAuth Failure';
                console.error(error_message, err);
                res.status(500).send(error_message);
            }
        });
    }).catch((err) => {
            console.error('Failure: Config test-slackbot is not found');
            let error_message = 'Sorry Failure OAuth by Server Error';
            res.status(500).send(error_message);
    });
};
package.json
{
  "name": "Test-SlackApp-OAuth",
  "version": "0.0.1",
  "dependencies": {
    "@google-cloud/rcloadenv": "^0.2.1",
    "request":"^2.8.0"
  }
}

動作確認

Cloud Functionsの設定にあるURLをSlack Appに設定する.
OAuth & Permissions >> OAuth Tokens & Redirect URLs >> Redirect URLs で Add a new Redirect URLでCloud FunctionsのURLをセットする.

そして以下のURLにブラウザからアクセスする.

https://slack.com/oauth/authorize?client_id=(アプリのClient ID)&scope=(アプリのスコープ)

以下のページが出たら, Authorizeを押す.

f:id:Kourin1996:20180501013055p:plain

以下のようなJSONが返ってきたら, 認証は成功.

f:id:Kourin1996:20180501013316p:plain

参考

azriton.github.io

qiita.com

Herokuで始めるSlackBot

PythonでSlackBotを記述して, Herokuで動かすところまで.(健忘録)
今回は以下のライブラリを使う.
Slack Web API と Real Time Messaging (RTM) が使えるらしい.

github.com

ちなみに, ローカル環境はmacOS High Sierraです.

SlackのAPIトークン取得

SlackAPIからアプリを作成して, APIトークンを取得する.
Slack API | Slack

SlackAPIのトップから Start Building >> Create an App と移動する.
アプリ名と運用するワークスペースが選択するとアプリが作成される.

サイドメニューからBot Usersを選択し, Add Bot Userを押す.

アプリをインストールするためには, スコープを設定する必要がある.
サイドメニューから OAuth & Permissions に移動, Scopesからbotの使用する権限を選ぶ.
今回はとりあえず, 以下の権限を付与した.

  • Access information about user’s public channels
  • Send messages as (app名)
  • Add a bot user

SaveChangesを押し, 上の方のInstall App to Workspaceを押す.
インストールが完了すると, Bot User OAuth Access Tokenにトークンが表示される, これを後々使用する.

Slack上でbotを動かしたいチャンネルに招待しておく.
これでbotの基本的な準備は完了.

ローカル環境設定

Githubで適当にリポジトリを作ってクローンする.
クローンしたディレクトリに入り, virtualenvを使ってpythonの仮想環境を構築する.

virtualenvでPython仮想環境作成
# virtualenvのインストール
pip install virtualenv

# 仮想環境の作成
virtualenv app_env

# 仮想環境の有効化
source app_env/bin/activate

# (仮想環境の無効化)
deactivate

# (仮想環境の消去)
rm -rf app_env

次にbotライブラリをpipでインストールする.

botライブラリのインストール
pip install slackclient

先ほどのトークンを環境変数に設定する.

トークンの設定
export SLACK_API_TOKEN='(先ほどのTOKEN)'

とりあえず, channelリストを取得するAPIを叩いてみる.

getChannel.py
import os
from slackclient import SlackClient

slack_token = os.environ["SLACK_API_TOKEN"]
sc = SlackClient(slack_token)

response = sc.api_call("channels.list")
print(response)

実行するとワークスペース内のチャンネル情報が取得できる.
次にgeneralチャンネルにメッセージを投稿してみる.

postTest.py
import os
from slackclient import SlackClient

slack_token = os.environ["SLACK_API_TOKEN"]
sc = SlackClient(slack_token)

sc.api_call(
  "chat.postMessage",
  channel="(先ほど取得したチャンネルのID)",
  text="Hello from Python!",
  reply_broadcast=True
)

generalチャンネルにメッセージが投稿されていれば, 成功.

次にReal Time Messaging APIの動作を確認してみる.
以下のソースコードはRTMで取得したイベント結果をそのまま標準出力に出力するプログラムである.
ちゃんと, slackに接続できれば, helloイベントが出力される.

rtmTest.py
import os
import time
from slackclient import SlackClient

slack_token = os.environ["SLACK_API_TOKEN"]
sc = SlackClient(slack_token)

if sc.rtm_connect():
  while sc.server.connected is True:
        request = sc.rtm_read()
        print(request)
        time.sleep(1)
else:
    print("Connection Failed")

次にユーザの投稿をそのままオウム返しするようなbotを書いてみる.
今回は対象のユーザのIDを直打ちした.

rtmReturn.py
import os
import time
from slackclient import SlackClient

slack_token = os.environ["SLACK_API_TOKEN"]
sc = SlackClient(slack_token)

if sc.rtm_connect():
  while sc.server.connected is True:
        request = sc.rtm_read()

        for event in request:
            if 'user' in event:
                if event['user'] == (対象ユーザのID) and event['type'] == 'message':
                    sc.api_call(
                        "chat.postMessage",
                        channel="(先ほど取得したチャンネルID)",
                        text=event['text'],
                        reply_broadcast=True
                    )
        time.sleep(1)
else:
    print("Connection Failed")

実行してslack上でメッセージを送ると, botからオウム返しが返ってくる.

Herokuの設定

Herokuのアカウントを作成し, ログインするとパーソナル画面に移動する.
Create New Appを選択する.
App nameとregionを選択する.
Add to pipelineを押すとパイプラインを追加できる.
Pipelineを追加することで, Githubリポジトリにpushした時に自動的にherokuの方にも反映してくれるようになる.
pipelineを追加して進む.

Appの作成が完了すると, Appの設定画面が開く.
Deployタブを開いて, Deployment methodからGithubを選ぶ.
自分のGithubアカウントと紐付けすると, 自分のGithubリポジトリから検索できるので, 紐付けたいリポジトリを選択する. Automatic deploysでEnbale Automatic Deploysを押す.

Settingsタブを開いて, Config Variablesから環境変数をセットする.
ブラウザ上での設定は以上で終了.

Herokuへのデプロイ

次にローカルでheroku向けのファイルを作成する.
HerokuではProcfile, runtime.txt, requirements.txtの3つのファイルがルートディレクトリ以下にある必要がある.

Procfile

実行するコマンドを設定する

woker: python rtmReturn.py
runtime.txt

実行中のpythonのバージョンの定義

python-3.6.0
requirements.txt

Pythonの依存ライブラリを記述する.
以下のコマンドで生成できる.

pip freeze > requirements.txt

以上の3つのファイルを追加して, リモートリポジトリへgit pushする.
HerokuのページのOverviewタブからデプロイに成功したかどうか見れる.
ページをリロードすると, Dyno formationにProcfileで定義したコマンド名が現れる.
Configure Dynosに移動して, 選択したappをonにする.

ローカル環境と同様に, slackでメッセージを送信すると, botがオウム返しするようになる.

参考

qiita.com
www.utali.io

ブログ始めます

この度、ブログを始めることにしました

理由としては

  • アウトプットした方が良いよなぁ
  • 自分の興味志向を後から振り返りたい

という感じです

なので、わりと適当に書くかもしれません

むしろ、適当に書きます

 

できるだけ、ゆるく長く続けたいと思います

昔の自分だったら3日坊主どころか、1日坊主だったので不安ですが・・・

(過去に何回かブログを作るも、1~2postで終了)

 

昔は一夜漬けとかバンバンする短期集中型でしたが、最近はコツコツやる方が良いよなぁと痛感しました

なので、毎日小さいタスクをコツコツやってブログに書いていけたら良いなと思ってます

 

それでは、よろしくお願いします