Slash Commandsで自分が使用している端末のグローバルIPは取得できるのか【API Gateway + Lambda(Go)】

## はじめに
こんにちは。先月[AWS ChatbotからRDSを操作していて](https://qiita.com/maika_kamada/items/c5ea2b20ce4681ca4aea)、とても簡単にSlackから実行できて感動したので、第２弾です！今回はSlackから自分のグローバルIPを取得できるのかを、試してみました！

## 結論
残念ながら、Slackから自分のグローバルIPは取得できませんでした。

Slackで自分のグローバルIPを取得するべく、Slash CommandsというSlackのapiと、AWSのAPI Gateway、Lambdaを使って試みたので、その過程について備忘録です。

## 方法検討
自分のグローバルIPを取得して、それを引数としてLambdaを実行したかったので、AWSを使う方針で進めていきます。

調査したところ、SlackからLambdaを実行する方法を2つ見つけました。

+ API Gatewayを使う方法（Slash Commandsの作成）
+ AWS Chatbotを使う方法

API Gatewayはアクセス元のグローバルIPを取得できるというのを知ったので、今回は**API Gatewayを使う方法（Slash Commandsの作成）**で実装することにしました！

## 構成図
構成は下図の通りです。
ちなみに、Slash commandsは3000ms以内に応答を返す必要があるので、処理時間が長いものに関しては先にSlackに応答を返す必要があるみたいです。今回はアクセス元IPを返すだけなので、こちらの構成で実装しました。
参考：https://api.slack.com/interactivity/slash-commands

![Slash_commands.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/3d278b6c-d988-8e0f-a5f3-21ddfe059c39.png)


## やってみる
### Lambda関数の作成
今回はチャンネルを識別し、指定チャンネルから実行されていれば、アクセス元のグローバルIPを返すというLambda関数を作りました。

チャンネルを識別する意図としては、Slash Commandsは、ワークスペース内の全チャンネル、全DMで使えるもので、チャンネルを指定することができません。今回作成するSlash CommandsはグローバルIPを返すだけなので、どこで実行できても問題はないのですが、実際には指定したプライベートチャンネルのメンバーのみしか実行できないようにしたかったので、実行チャンネルを識別し、実行権限があるのかを判断する処理を入れています。

#### IAMロール
特別必要な権限はないので、基本的なLambdaアクセス権限をアタッチしたロールを使用します。

#### Slackから送られるデータと構造
Slackから送られるデータと構造は下記の通りです。
※ Valueは削除しています。本来はそれぞれ値が入っています。
※ 後述のAPI Gatewayのマッピングテンプレートを使用した場合の構造です。


```json
{
  "querystring":{
    "api_app_id":"",
    "channel_id":"",
    "channel_name":"",
    "command":"",
    "response_url":"",
    "team_domain":"",
    "team_id":"",
    "text":"",
    "token":"",
    "trigger_id":"",
    "user_id":"",
    "user_name":""
  },
  "source_ip":""
}

```

#### ソースコード

```go

package main

import (
	"github.com/aws/aws-lambda-go/lambda"
	"os"
)

type MyEvent struct {
	Querystring Querystring `json:"querystring"`
	SourceIp string `json:"source_ip"`
}

type Querystring struct {
	ChannelId string `json:"channel_id"`
}

/**************************
    処理実行
**************************/
func run(event MyEvent) (interface{}, error) {
	// os.Getenv（）でLambdaの環境変数を取得
	if event.Querystring.ChannelId == os.Getenv("channelId") {
		return event.SourceIp, nil
	}
	res := "実行権限がありません。"
	return res, nil
}

/**************************
    メイン
**************************/
func main() {
	lambda.Start(run)
}
```


### API Gatewayの作成

API Gatewayの作成を行います。
POSTメソッドを作成し、先ほど作成したLambda関数を紐付けます。

そして、マッピングテンプレートの設定をしていきます。
[統合リクエスト]をクリックします。

![スクリーンショット 2020-09-06 23.39.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/0a63cb5c-cddb-4e60-445c-6f0c8eef0cec.png)

最下部にあるマッピングテンプレートを開き、リクエスト本文のパススルーは**テンプレートが定義されていない場合 (推奨)**を選択します。そして、[マッピングテンプレートの追加]をクリックします。

![スクリーンショット 2020-09-06 23.45.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/21ba8c16-da6a-0c77-3504-55876b50161f.png)

[マッピングテンプレートの追加]を押したら出てくるテキストボックスに`application/x-www-form-urlencoded`を入力、保存します。

テンプレートの入力フォームが出てくるので、下記のマッピングテンプレートを貼り付け、[保存]をクリックします。


![スクリーンショット 2020-09-06 23.43.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/14c70537-391e-764b-ca09-af1605fc1b18.png)

【マッピングテンプレート】

```
#set($rawAPIData = $input.path('$'))
## escape any quotes
#set($rawAPIData = $rawAPIData.replace('"', '\"'))

## first we get the number of "&" in the string, this tells us if there is more than one key value pair
#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())

## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
 #set($rawPostData = $rawAPIData + "&")
#end

## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawAPIData.split("&"))

## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])

## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
 #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
 #if ($countEquals == 1)
  #set($kvTokenised = $kvPair.split("="))
  #if ($kvTokenised[0].length() > 0)
   ## we found a valid key value pair. add it to the list.
   #set($devNull = $tokenisedEquals.add($kvPair))
  #end
 #end
#end

{
"querystring" : {
#foreach( $kvPair in $tokenisedEquals )
  ## finally we output the JSON for this pair and append a comma if this isn't the last pair
  #set($kvTokenised = $kvPair.split("="))
  #if($kvTokenised.size() == 2 && $kvTokenised[1].length() > 0)
    #set($kvValue = $kvTokenised[1])
  #else
    #set($kvValue = "")
  #end
  #if( $foreach.hasNext )
    #set($itemDelimiter = ",")
  #else
    #set($itemDelimiter = "")
  #end
 "$kvTokenised[0]" : "$kvValue"$itemDelimiter
#end
},
"source_ip" : "$context.identity.sourceIp"
}
```

最後にリソースのデプロイを行い、発行されたURLをコピーしておきます。

![スクリーンショット 2020-09-07 0.17.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/29cc6250-7074-99b4-dc87-de5af5e6ec4e.png)


### Slash Commandsの作成
#### Slack appの作成
[Slack api](https://api.slack.com/apps)からSlack appを作成していきます。

[Create New App]をクリックするとAppの作成画面に遷移します。

![スクリーンショット 2020-09-05 13.26.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/70b4d04c-9b59-ade6-3005-a927e05e49f7.png)

ここで、**Slack Appの名前**と**Appを実行したいワークスペース**を選択します。
そして、[Create App]をクリックするとSlack Appが完成します。

![スクリーンショット 2020-09-05 13.27.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/a35cb88e-30df-3503-e5ed-25f7ce45a04e.png)

#### Slash commandsの作成
Appの設定ページに移動して、Slash commandsを作成します。

左側のメニューから[Slash commands]を選択し、[Create New Commands]をクリックします。

![スクリーンショット 2020-09-05 13.28.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/8ce1b48e-fa40-c019-1f04-2ee2aaa9b3b9.png)

次に、**Slackから送信するコマンド名**、**前述のAPI Gatewayで取得したリクエストURL**、**説明文**の3つを入力します。今回は、`/get_ip`というコマンド名を設定しました。

![スクリーンショット 2020-09-05 13.44.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/4db2c687-0441-7968-c6da-15ae7b27272c.png)

今回は引数を入力しないのでヒントは入れませんでしたが、引数など入力時の補足がある場合は**Usage Hint**に入力しておきます。**Escape channels, users, and links sent to your app**にチェックを入れることで、引数にユーザ名を入れる時に、ユーザ名だけではなくユニークなユーザIDも送ることができ、アカウントを識別できるそうです。今回はアカウントの識別は不要なので、チェックは入れませんでした。

それぞれ入力完了後、[Save]をクリックすると、Slash commandsが完成します。

#### Botの名前設定
このままAppのインストールに進むと、**（App名）にはインストールするボットユーザーがありません。**というエラーが出て、Appをインストールできなかったので、Appの設定ページから、Botを作成します。

ちなみに、App名が小文字英数字の場合は、自動でBotが設定されるみたいので、初期値が入っていればこのフローは不要です。わたしは**IP取得マン**というApp名にしていたためボットユーザが作られなかったのだと思われます。

左側のメニューから[App Home]を選択し、**Your App's Presence in Slack**の[Edit]をクリックします。

![スクリーンショット 2020-09-06 18.19.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/e5945f8c-f140-4633-ecf6-1327371a7e5e.png)

**Botの表示名**と**ユーザ名**を入力して[Add]をクリックすると、Botの名前が設定されます。

![スクリーンショット 2020-09-06 18.19.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/b660ca20-2262-1176-b676-9870b2c08ab8.png)


#### Appのインストール
いよいよAppのインストールです。

左側のメニューから[Install App]を選択し、[Install App to Workspace]をクリックします。

![スクリーンショット 2020-09-06 18.20.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/7a1f9e51-9248-4548-7864-046265b5ff53.png)

作成したAppが指定したワークスペースにアクセスするのを許可すると、グローバルIPを取得するコマンドの完成です。

### 実行結果
#### 実行方法
今回作成したコマンド`/get_ip`をメッセージとして送信します。

![スクリーンショット 2020-09-13 19.22.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/7cbe2413-c697-86a0-26d1-108ebe5278f1.png)


#### 指定チャンネルから実行した場合
指定チャンネルから実行した場合のレスポンスは次の通りです。

![スクリーンショット 2020-09-06 19.13.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/74e272b6-857c-757a-7037-f66d174d1015.png)

IPアドレスが返ってきました！！！

ただ、自分のグローバルIPを調べてみると・・・違う。。
これはSlackのグローバルIPアドレスですね。。。

残念ながら、自分のグローバルIPアドレスを取得することはできませんでした。


#### 指定外のチャンネルから実行した場合
ちなみに、指定外のチャンネルから実行した場合のレスポンスは次の通りです。

![スクリーンショット 2020-09-06 19.13.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/0b877a03-09c3-1f3b-48eb-fe0285b589ad.png)

これで、実行チャンネルを識別し、指定チャンネル外からは実行できないことが確認できました！


## おわりに
結果、SlackからグローバルIPを取得することはできませんでしたが、SlackのIPは取得することができました。ただ、実行する度に違うIPアドレスが返ってくることも確認できました。今回とは関係ないですが、Slackは複数のグローバルIPアドレスを持っていることがわかりました。

[前回](https://qiita.com/maika_kamada/items/c5ea2b20ce4681ca4aea)に引き続き、SlackからLambdaを実行してみましたが、AWS Chatbotと比較してSlash commandsの良いところは個人DMで使えるというところかなと感じました。反対に、Slash commandsの場合は処理時間が3000msを越えるものは、まず応答を返すようにしないといけないところは少し面倒かな、、と思いました。

今回は、残念な結果に終わってしまったので、また違う方法を考えます。SlackからグローバルIPを取得するいい方法があるよっていう方いらっしゃいましたら、教えていただけると嬉しいです！

### 参考
+ [API Gateway + Lambda で slack (2) ~ slash commandとinteractive message ~](https://qiita.com/kanagi/items/6d86afdb6944046cdc3c)
+ [誰でもわかるはず！本気でSlashコマンドの作り方を解説してみた！](https://belltree.life/slash-command/)