【Slack de AWS CLI】SlackのChannelからAWS CLIを実行できるようにしてみました!

#はじめに
　去年Amazon API Gatewayを触り始めてから、Slack、API Gateway、Lambda、EC2 Run commandを使って、EC2に対してSlackからシェルコマンドを送るようにやってみたことがあります。その後、SlackのChannelにAWS CLIを送ったら、Lambdaが実行してくれるといいなと思っていました。
　LambdaでAWS CLIを実行する方法が既に知っていましたが、ずっとやれていなくて（最近仕事中に全然DeepDiveできなくて）、今回せっかく三連休ですので、SlackのChannelにAWS CLIを送って、Lambdaが実行するようにしてみました!
　Qiita上には、既にSlackからAWS CLIを実行する投稿がありましたが、個人的もう少し簡単に実現したいので、自分のやり方でやりました。
　今回は**自分なりのやり方**を皆さんに共有します。Lambdaのコードは[GitHub](https://github.com/jobbin/Slack_de_AWSCLI)に上げておきました。

#概要
##利用するサービス

1. Slack + Slash Command
2. Amazon API Gateway
3. Lambda( python + AWS CLI )

##全体像
　何をどのようにやったかを簡単に説明しますと、

```
　Slash CommandでSlackのChannelにAWS CLIのコマンド(message)を送ったら、
　そのAWS CLIのコマンドがAPI Gateway経由でLambdaに送られ、
　Lambdaが送信されたAWS CLIのコマンドを実行して、実行結果をSlackに返す
```
　ということです。
　全体像は図に示す通りです。
　<Font color="red">(なぜ、2つのLambdaを利用している理由はあとで説明します。)</font>
<img width="637" alt="スクリーンショット 0028-07-16 19.11.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/117156/310b9613-5252-6914-4788-7f6402707b43.png">

#何が嬉しいのか
　そもそも何でSlackのChannelからAWS CLIを実行できるようにしたいのかと言いますと、正直に言って、特に明確な目的がありません.......が、こんな嬉しいことがあると思っています。

1. ローカル環境でAWS CLIを叩かなくてもいい
 - Credencial情報をローカル環境に置かなくてもいいので、漏れる心配もない
1. 複数メンバーがいる場合、AWS CLI実行環境を統一できる(かもしれません)
1. PCであろうがスマホーであろうがタブレットであろうが、Slackがインストールされていれば、AWS CLIを使えます。まさに、**Multi Device対応**（凄くない？！）


#おさえておきたいこと
　下記４点を是非皆さんにおさえておきたいです。

- ①　**Slash CommandsのTimeout時間は3000ms**
　一連の処理(特にLambda上のコマンド実行)が少し時間がかかりますので、Slash CommandsがTimeoutしないように、<Font color="red">Front Lambda</font>で一旦Slackからの情報（AWS CLIのコマンドを含め）を受け取り、Slackに返信すると同時に、受け取った情報を<Font color="red">Execution Lambda</font>に渡すようにしています。これが、２つのLambdaを利用している理由です。

- ②　**Slackから送信される情報**
以下の情報がSlackからAPI Gatewayに送られますが、注意点２点あります.
 1. **text**の内容にspaceが入っている場合,**space** は **+** に置き換えられます
 1. **response_url**は**エンコード**されますので、Lambdaで**デコード**する必要があります 

```
token=3mVXdAC5EFNbsXYGNk0sah3M
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
user_id=U2147483697
user_name=Steve
command=/weather
text=94070
response_url=https://hooks.slack.com/commands/1234/5678
```
- ③　**API Gatewayでデータマッピング**  
Slackから送られてきた情報(**文字列**)を**JSON**にマッピングする必要があります。[VTL](http://velocity.apache.org/engine/devel/vtl-reference.html)という言語を使っているそうです。  
マップピング用のテンプレートは以下になります。

```
## &区切りでPOSTされてきたデータを分割する
#set($httpPost = $input.path('$').split("&"))

## 出力されるJSONデータの生成
{
#foreach( $keyValue in $httpPost )
 ## "key=value"というデータをkeyとvalueに分ける
 #set($data = $keyValue.split("="))
 ## JSONのフォーマットに整形する（まだ走査するデータがあれば","を挿入する）
 "$data[0]" : "$data[1]"#if( $foreach.hasNext ),#end
#end
}
```

- ④ **Lambda上のAWS CLI環境**  
デフォルトのLambdaの環境では、AWS CLIが入っていないので、AWS CLIをパッケージ化して、Lambdaにアップロードする必要があります。  
やり方は[【AWS Lambda Python で AWS CLI を実行する方法】](http://www.magtranetwork.com/aws/aws_lambda_python_aws_cli.html)にてご確認ください。

#設定手順
　さって、さっそくSlackからAWS CLIを実行できるように設定していきましょう！特に難しいことはやっていませんので、ご安心ください!
##Lambda
　今回権限まわりをちゃんと調査していないので、LambdaのRoleに<font color="red">PowerUserAccess</font>をつけてあります。(自分はIAMにまだ弱いです)
###Front Lambda
　詳細な作成手順は省きますが、1点だけ説明します。
　Front Lambdaの役割は受付(情報を受け取って、Execution Lambdaに渡す)だけですので、利用するRoleは最小権限で良いと思っています。
　Front Lambdaのコードは下記の通りです。

```lambda_function.py
# -*- coding: utf-8 -*-
from __future__ import print_function
import boto3
import json

print('Loading function')

def lambda_handler(event, context):
    print("Received event: " + json.dumps(event, indent=2))
    #messageが　"+"で連結されたため,"+"をスペースに戻す
    event['text'] = event['text'].replace('+',' ')
    
    clientLambda = boto3.client("lambda")
    clientLambda.invoke(
        #invokeするLambda Name
        FunctionName="Execution-Lambda",
        InvocationType="Event",
        Payload=json.dumps(event)
    )
    return event # Echo back the first key value
```

###Execution Lambda
　[【AWS Lambda Python で AWS CLI を実行する方法】](http://www.magtranetwork.com/aws/aws_lambda_python_aws_cli.html)を参考に、やって頂ければ、LambdaでAWS CLIを実行できるようになりますので、詳細な手順は割愛します。２点だけ説明します。

1. <font color="red">lambda_function.py</font>は記事の内容と違っています。
1. Execution LambdaがAWS CLIを実行しますので、適切な権限を与える必要があります。  
自分の場合は、検証ですので、<font color="red">PowerUserAccess</font>を与えています。



　lambda_function.pyのコードは下記の通りです。

```lambda_function.py
# -*- coding: utf-8 -*-
import commands
import json
import os
from cStringIO import StringIO
import re
import urllib
from urllib2 import Request, urlopen, URLError, HTTPError

print('Loading function')

def _(cmd):
    return commands.getoutput(cmd)

def lambda_handler(event, context):
    print("Received event: " + json.dumps(event, indent=2))

    #response_urlのデコード
    response_url = urllib.unquote(event['response_url']).encode('raw_unicode_escape').decode('utf-8')
    print(response_url)

    slack_message = {
        'channel': event['channel_name'],
        'response_type': 'ephemeral',
        'isDelayedResponse': 'true',
        'text': "response for: `aws " + event['text'] + "`\n" + _("./aws " + event['text'])
    }
    req = Request(response_url)
    req.add_header('Content-Type', 'application/json')
    try:
        response = urlopen(req, json.dumps(slack_message))
        response.read()
        print("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        print("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        print("Server connection failed: %s", e.reason)

```

##API Gateway

1. 新しいAPIを作成します。
<img width="1358" alt="1.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/9e276002-c871-42c7-8abb-a9226f4f665c.jpeg">

1. 今回はAPI Nameをslackにします。 適宜変更してください。
<img width="1656" alt="2.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/06726151-9e6a-d9c1-203d-4e6b493aa99a.jpeg">

1. APIを作成されたら、「Action」→「Create Method」をクリックして、メソッドを選択します。<img width="1614" alt="3.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/792b851e-5e76-4f38-a6ec-b4d2d4ec88f3.jpeg">

1. 今回はPOSTメソッドを使います。<img width="1426" alt="4.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/66489b74-aeb8-e035-809d-cee891568d4b.jpeg">

1. 「Lambda Function」をクリックして、Regionを選択します。
<img width="692" alt="スクリーンショット 0028-07-17 0.29.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/117156/9c983ef1-89a7-50cb-ee82-440c5e5b1939.png">

1. 自分の場合は、Tokyo Regionのfront-lambdaを利用します。
<img width="1672" alt="5.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/f5a31531-2ce1-dbe8-5181-9cb9fe09754e.jpeg">

1. Permissionを聞かれますが、「OK」で良いです。
<img width="1186" alt="6.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/a9b7a9a1-4a97-8fe3-6648-832315b3276d.jpeg">

1. 下記の画面が表示されますと、「Integration Request」をクリックします。
![7.pic.jpg](https://qiita-image-store.s3.amazonaws.com/0/117156/438d91ec-506f-f726-bb36-ee4afe76aa1c.jpeg)
1. 「Body Mapping Templates」にて、mapping templateを追加します。Request body passthroughはAWS推奨の項目にします。
<img width="1112" alt="8.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/a5a263a6-34b3-5689-118f-080b23998d40.jpeg">

1. Content-Typeに**application/x-www-form-urlencoded**を追加します。
<img width="1054" alt="9.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/cea9e7a6-0866-acee-725f-956a1599613d.jpeg">

1. templateを定義して、保存します。
<img width="400" alt="10.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/47cdc705-a6ea-506d-bd4c-ce5cbd68e69a.jpeg">

1. templateが保存されましたら、「Actions」の「Deploy API」を選択します。
<img width="1326" alt="11.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/4fef9e18-ff5b-4e57-9934-ef18b633c97b.jpeg">

1. Stage nameは適宜でよいです。
<img width="450" alt="スクリーンショット 0028-07-17 0.50.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/117156/d7b5e4f8-167b-8f3e-959c-c3b7e90fa4b1.png">

1. APIをデプロイされたことを確認し、**Invoke URL**をメモってください。後でSlackのSlack Commandsに設定します。<img width="1626" alt="12.pic_hd.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/192ac907-c7ea-95d7-7538-8ad38362da42.jpeg">

1. API Gatewayの準備が完了

##SlackのSlash Commands
　Slack Commandsについては、Slackの[Documentation( Slack Commands )](https://api.slack.com/slash-commands)を確認してください。

1. [Slash Commands](https://my.slack.com/services/new/slash-commands)にアクセスして、新しいSlash Commandを作成します。
(例では、Slash Command **/aws** を作成します)<img width="1646" alt="13.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/d8e1071c-add0-88c5-589f-ca85e23be41e.jpeg">

1. URL欄には**API GatewayのInvoke URL**を入力し、Method欄は**POST**にして、(残りの設定はお好みで)、「Save Integration」を押します。<img width="1708" alt="14.pic_hd.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/96120261-68fe-e335-6ce6-3c2d3bb83418.jpeg">

1. SlackのSlash Commandsの設定は完了

#利用イメージ
　SlackのChannelにAWS CLIを投げると、実行してくれます.
　以下は利用例です。

- <font color="red">**aws s3 ls**<font>
<img width="1274" alt="21.pic_hd.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/5ff84fa0-8472-65ba-6b1c-4e0fc16f1def.jpeg">
　

- <font color="red">**aws ec2 describe-regions**</font>
<img width="1390" alt="20.pic_hd.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/3fa84e56-50c3-7e71-1eac-ca6db27dd60b.jpeg">

#最後
　SlackのChannelからAWS CLIを実行できるようになりました。AWS CLIのコマンドを投げて、実行結果を返されるまで、少し時間がかかってしまい、多少ストレスが感じるかもしれませんが、**Serverless**且つ**Multi Device**でAWS CLIを実行できることが凄く便利だと思っています。是非、使ってみてください!
　最後に１つ注意点だけ皆さんに伝えたいです、API Gatewayには認証とかできていないので、エンドポイント(Invoke URL)がバレてしまうと、**だれでもAPIを叩ける**状態になっています。ですので、少なくてもfront-lambdaに簡単な**判断ロジック**を入れいる必要があると思います。<font color="red">例えば、決められたSlackのChannel(team_id&channel_id)からのコマンドしか受け付けないようにすることです。</font>
　最後の最後となりますが、まだ実運用されていないので、正直に言って、どこまで実用性があるかはまだ分かりませんが、少なくても、飲み会のネタにはビッタリではないでしょうか。
　今回はここまでにします。