API Gateway + Lambda で slack (1) ~ slack app を登録 ~

# API Gateway + Lambda で slack app

## slack app

slack appとはbotやcommandsなどの集合体で、これを登録すれば、appに含まれているbotやcommandsが使えるようになるものです。

tokenが一意で済むのと、interactive message

https://api.slack.com/docs/message-buttons

を使うことができるようになります。これを使うことで、interactiveに操作を進めることが可能です。
slack appについて詳しくは下記を参照してください。

https://api.slack.com/slack-apps

作成したslack appは全体に公開することもできます。が、今回は完全にprivate目的なので自チームにだけ登録して利用します。

## api gateway + Lambda

API Gateway + Lambdaとするのは、常に起動している必要がないからサーバがいらない構成にしたいためです。

さらにslash commandsを使う場合、botと違ってslash commandsを実行した時だけ呼び出されるため、料金的にも安心です。


# 必要なもの

* apex

http://apex.run/


lambdaのデプロイに使います。invokeとlogsが便利です。
terraformとも連携できますが、terraformとapi gateway + lambdaの連携がつらいので今回は見送りました。

インストールはこれだけです。

```
curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh
```

* aws credentials

`aws configure` でアクセスキーとシークレットアクセスキーを設定しておきます。
apexがデフォルトだとiamも配置するので、lambdaとiam関係の権限がなければなりません。
また、tokenなどの保持にkmsを利用するのでその権限も必要です。

複数credentialを持っている場合は、apexの設定からprofile指定もできます。詳しくは公式ページに。



* slack


# インストール

## apexでlambdaの配置

まずは空のディレクトリで

```
apex init
```
をすることで、apexのプロジェクトファイル、apexが利用するIAMの作成が行われます。
project nameはここでは`commands`としておきます。

apexに慣れておきたい場合には公式のGetting startedにしたがって、helloを配置してみるといいでしょう。

## oauth callbackの作成

slack appの登録にoauthのcallbackが必要です。手でやれないこともないですが、せっかくなのでこれもapi gateway + lambdaで処理してみます。

### 20170324追記
slack appが開発で使う分にはoauth callbackを使わなくても権限を追加したりtokenをもらったりすることができるようになってました。
lambdaとapi gatewayの配置方法例として以下は残しておきますが、自分たちで使うようなslack appを配置するだけならoauth callbackの配置は不要になってます。

## slack appの作成

https://api.slack.com/apps

[Create New App] から新しいAppを作ります。 `I plan to submit this app the Slack App Directory` はこのアプリを公開する場合なので今回はチェックは不要です。

![Slack_API__Applications___Slack.png](https://qiita-image-store.s3.amazonaws.com/0/63168/5d786a0c-7538-7a8d-115a-02802ba9139f.png)

作成後に表示されるBasic Infomationにある`client_id`と`client_secret`を控えておきます。
また、ここで変更できるiconはコマンドを実行した時に表示されるものなので、目的に応じたものに変えるといいでしょう。

## KMS

秘密にしなければならないclient_secretなどを載せるようになるので、githubに上げることを考えて、KMSで暗号化します。

1. IAMから[暗号化キー]の[キーの作成]
2. エイリアス名に適当な名前（今回は `slack` とします）を与えて「次のステップ」
3. キー管理アクセス許可に、コマンドラインで使っているcredentialのものを選択して「次のステップ
4. キーの使用アクセス許可は一旦飛ばして「次のステップ」
5. 「完了」

コンソールがaws credentialsが作成したKMSにアクセスできるものなら、下記のコマンドで暗号化されたテキストが取得できるはずです。

```
aws kms encrypt --key-id alias/slack --plaintext "暗号化したいテキスト" --query CiphertextBlob --output text
```

これで`client_id`と`client_secret`の暗号化テキストを作ります。

lambda functionがkmsのキーを読み取れないといけないので、作成されたIAMのポリシーに`kms:Decrypt`を追加します。apexで登録したlambdaが使うIAMは`project.json`に記載されています。

functionごとにIAMを変えられるので権限を絞れますが、どうせ全体で使うので手抜きです。
さらに、keyを限定したい場合は`Resouce`をちゃんと指定しましょう。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": "*"
        }
    ]
}
```

## lambdaの配置

暗号化したテキストは下記のようにproject.jsonの環境にいれてしまいます。これをlambdaのコードのなかでkmsを呼んで復号すればもとの`client_id`と`client_secret`が呼出せます。

```json:project.json
{
  "name": "commands",
  "description": "slack commands",
  "memory": 128,
  "timeout": 5,
  "role": "arn:aws:iam::account-id:role/slack-commands_lambda_function",
  "environment": {
    "ENCRYPTED_CLIENT_ID": "暗号化したclient_id",
    "ENCRYPTED_CLIENT_SECRET": "暗号化したclient_secret"
  }
}
```

lambdaの処理は、javascript苦手でnodeがさっぱりなので全部pythonで行きます。



`functions/callback/` を作成して、`main.py`を配置します。一応全体公開されるので余計なexceptionを返さないことを目的としてます。

GETされた文字列はマッピングテンプレートに従って、`event`の`params`->`querystring`内に入っているのでそこから値を取り出します。

slackが送ってくるqueryについては下記を参照してください。

https://api.slack.com/docs/oauth



```python:main.py
import logging
import json
import os
import boto3
import requests
from base64 import b64decode

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handle(event, context):
    try:
        kms = boto3.client('kms')
        client_id = kms.decrypt(CiphertextBlob = b64decode(os.environ["ENCRYPTED_CLIENT_ID"]))['Plaintext']
        client_secret = kms.decrypt(CiphertextBlob = b64decode(os.environ["ENCRYPTED_CLIENT_SECRET"]))['Plaintext']
    except:
        logger.error("required environments not found")
        return {"text": "error"}

    try:
        code = event['params']['querystring']['code']
    except:
        logger.error("code not found")
        return {"text": "error"}

    try:
        response = requests.get(
          'https://slack.com/api/oauth.access',
          params={'client_id': client_id, 'client_secret': client_secret, 'code': code})
    except:
        logger.error("oauth request error. response: %s", response.json())
        return {"text": "error"}
    logger.info(response.json())
    return { "text": "ok" }
```

また、pythonをapexでデプロイする場合には、`requirements.txt`で必要なmoduleを記載し、`function.json`のhookでインストールしてやります。ついでに`.apexignore`で余分なファイルを配置しないようにできます。

```:functions/callback/requirements.txt
boto3
requests
```
```json:functions/callback/function.json
{
  "description": "oauth callback",
  "hooks":{
    "build": "pip install -r requirements.txt -t ."
  }
}
```
```:functions/callback/.apexignore
*.dist-info/
```

最後にcallbackをデプロイします。

```
apex deploy callback
```
するとlambdaに`commands_callback`が配置されるはずです。


## IAM で KMSの権限を追加

lambdaがKMSのキーを読み取る必要があるので権限を追加します。
再度IAMの[暗号化キー]画面からキーユーザの[追加]

![IAM_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/63168/7228eba5-9a21-7eee-8156-6c4da1a8a26a.png)

lambdaが使っているIAMのロール(今回はcommands_lambda_function)を選択して[追加]

![IAM_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/63168/96429812-fa84-c41c-8bf9-cb86c7ea538b.png)

これでlambdaがこのkmsのキーを読み取れます。

今回はプライベートでしか使わないのでやっていませんが、lambdaのfunctionごとに異なるIAMを設定することは可能です。各functionの`function.json`で`role`を指定できます。

## api gateway

* 新規なら[今すぐ始める]、既に使っているなら一覧画面から[APIの作成]

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/63168/134d30bf-b0a2-b6f3-898e-40ea8aa2cfa3.png)

* API名に適当な名前をいれて(ここではcommands)、[APIの作成]

![API_Gateway_new.png](https://qiita-image-store.s3.amazonaws.com/0/63168/c30fd6a7-88f7-8cd9-e953-640e0d3c78eb.png)


* [アクション] -> [リソースの作成]

![API_Gateway_new_method.png](https://qiita-image-store.s3.amazonaws.com/0/63168/80131f73-5668-9440-c463-8da760bd3c92.png)


* リソース名に `callback` をいれて [リソースの作成]

![API_Gateway_resource.png](https://qiita-image-store.s3.amazonaws.com/0/63168/7bbec1e0-5b53-24c2-700e-dc97bdc7dc1f.png)

* [アクション] -> [メソッドの作成] -> GETを選んでチェックをクリック

![API_Gateway_method_get.png](https://qiita-image-store.s3.amazonaws.com/0/63168/3a5452d3-5db2-b5df-ae10-df3f4e990a61.png)


* セットアップ画面になるので、先ほど作成したlambdaを選んで[保存]

![API_Gateway_newmethod.png](https://qiita-image-store.s3.amazonaws.com/0/63168/99f717f3-b5bf-5f1e-afac-20e2bc494c4e.png)


* 権限許可確認画面がでるので[OK]

![API_Gateway_resource3.png](https://qiita-image-store.s3.amazonaws.com/0/63168/acbf967f-dec5-685e-2f07-331968ebbde8.png)


* メソッドの実行画面で[統合リクエスト]を選択

![API_Gateway_method_ex.png](https://qiita-image-store.s3.amazonaws.com/0/63168/d93b2a19-49cd-d8e0-fc8b-c24c0c9eab94.png)


* [本文マッピングテンプレート]をクリックして詳細を出してから、[マッピングテンプレーとの追加] -> `application/json` を入力してチェックをクリック

![API_Gateway_mapping_template.png](https://qiita-image-store.s3.amazonaws.com/0/63168/89002ef8-ef7f-2f78-6069-f728b613138f.png)


* パススルー動作の変更画面がでるので [はい、この統合を保護します]

![API_Gateway_paththrough.png](https://qiita-image-store.s3.amazonaws.com/0/63168/c0f2e97a-c8d8-74e0-da21-a6401aae4f59.png)

* GETの中身が欲しいだけなので`テンプレートの生成:`で[メソッドリクエストのパススルー]を選んで[保存]

![API_Gateway_mappingtemplate2.png](https://qiita-image-store.s3.amazonaws.com/0/63168/364dd51c-3117-dbce-cbbf-2a2f00f221df.png)

* API gatewayはデプロイしないと反映されないので[アクション] -> [APIのデプロイ] -> APIのデプロイウィンドウがでるので、`デプロイされるステージ`から[新しいステージ]、ステージ名を適当に（これはAPI gatewayのURLに含まれます。ここでは`prod`）いれて[デプロイ]

![API_Gateway_deploy.png](https://qiita-image-store.s3.amazonaws.com/0/63168/c3a018a7-95ef-a2cc-554a-54d321f1e1c9.png)

ステージで`URLの呼び出し:`で示されるURLを取っておきます。

![API_Gateway_callback.png](https://qiita-image-store.s3.amazonaws.com/0/63168/742a11e6-c913-dd13-88a1-356aa1924c33.png)


## oauth

まずは、slack appのページの[OAuth & Permissions] 画面で、RedirectURLにAPI Gatewayで作った呼び出しのURLを入れ[Save Changes]します。

![Slack_API__Applications___wacul_Slack.png](https://qiita-image-store.s3.amazonaws.com/0/63168/3187572a-329f-1669-4b63-757deca8fd58.png)

その後、ブラウザでいいので下記を開きます。

https://slack.com/oauth/authorize?scope=commands&client_id=クライアントID

scopeは必要な権限を入れてください。ここでは、コマンドを実行できる`commands`を許可してます。
oauthのscope一覧は下記です。

https://api.slack.com/docs/oauth-scopes

`クライアントID`はslack app画面のclient_idとして、このURLをブラウザでいいので直接遷移すると下記のような画面に行くはずです。


![Authorize_access_to_your_account___wacul_Slack.png](https://qiita-image-store.s3.amazonaws.com/0/63168/26805a97-62d0-017f-a547-aee6cf9c7b95.png)

[Authorize]すると、先ほどのcallback URLに遷移します。うまくいっていれば

`{"text":"ok"}` とだけ表示されるはずです。


```
apex logs callback
```

を叩いてログ確認します。失敗している場合でもこれでなぜ失敗したかを確認して修正します。
そもそもログでていない場合、API Gatewayからlambdaが呼ばれていないのでAPI Gatewayの設定を見直しましょう。

うまくいっていれば、ログから`access_token`がもらえているはずです。`bot`や`incoming-webhook`を有効にしていればそれらも含まれています。

これらも暗号化して`project.json`に放り込みます。

他のslackチームで使うわけでもない場合は、`callback`をAPI Gatewayから削除してしまってもいいでしょう。

これでslack appの登録の完了です。

次でslash commandとinteractive messageを登録します。

# TIPS

## incoming-webhookのkms

incoming-webhookを使う場合、使う文字列は `https://` で始まりますが、awsコマンドで`file://` や `http://` などで始まる文字列の場合は問答無用で、そのファイルやURLを参照しにいくので、一旦ファイルにテキストを保存して暗号化します。
カレントディレクトリの`tmp`にテキストを入れたとして
```
aws kms encrypt --key-id alias/slack --plaintex file://./tmp
```

## .gitignore

rootに言語の.gitignore、pythonの場合は取得したモジュールがディレクトリに置かれるのでfunctionごとに.gitignoreしてます。

```:.gitignore
.Python
build/
develop-eggs/
dist/
eggs/
.eggs/
parts/
sdist/
*.egg-info/
*.dist-info/
.installed.cfg
*.egg
```

```:functions/callback/.gitignore
boto3/
botocore/
s3transfer/
requests/
jmespath/
docutils/
dateutil/
six.py
```