AWS Chatbotを使ってSlackからALBのログを検索する

この記事は[iRidge Advent Calendar 2019](https://qiita.com/advent-calendar/2019/iridge) 10日目の記事です。

先日AWS Chatbotを利用してSlackからAWSコマンドを発行する機能のβ版が発表されました。

- [AWS Chatbot now supports running commands from Slack (beta)](https://aws.amazon.com/about-aws/whats-new/2019/11/aws-chatbot-supports-running-commands-from-slack-beta/)
- [Running AWS commands from Slack using AWS Chatbot](https://aws.amazon.com/blogs/devops/running-aws-commands-from-slack-using-aws-chatbot/)

もともとAWS Chatbotを利用してCost ExplorerのBudgets AlertをSlackに流すくらいのことはやっていたのですが、コマンドが実行できることでやれることがより増えそうだなと思い、今回はよく業務で行っていた**ALBのログ検索を行い結果を取得する**のをSlackで完結できるかを試してみました。

## 今回やること
- [AWS Chatbotを設定する](#aws-chatbotを設定する)
- [Lambdaを作る](#lambdaを作る)
    - SlackからAWS Chatbot経由でLambdaを呼び出す
    - Lambdaでテーブルに対してクエリを実行
    - S3に出力された結果ファイルのオブジェクトを取得するためのPresigned URLを生成
    - 生成されたPresigned URLを返却
- [Lambdaをデプロイする](#lambdaをデプロイする)
- [SlackからLambdaを呼び出す](#slackからlambdaを呼び出す)
- [まとめ](#まとめ)

## 前提
Athenaのテーブル作成、Partitionの作成は完了している前提で書いてます。テーブルの作成等はAWSの下記のドキュメントが参考になると思います。

- https://docs.aws.amazon.com/athena/latest/ug/application-load-balancer-logs.html

## AWS Chatbotを設定する
まずはChatbotの設定をしていきます。

1. workspaceの設定をします。**Chat client**を**Slack**にして**Configure client**を押すとworkspaceにアクセスするための権限を求めてくるので、問題なければ許可します。

    ![スクリーンショット 2019-12-09 10.38.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127310/64919454-b611-8abd-59e9-8111075d74f0.png)

2. Chatbotを利用するchannelを選びます。

    ![スクリーンショット 2019-12-09 10.33.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127310/9f2898b5-4cc8-2c72-e8e8-11fe6a670935.png)

3. Chatbotが利用するIAM Roleを定義します。ある程度まとまったPolicyがテンプレートとして準備されているようです。作成したタイミングでは下記のテンプレートが準備されていました。
    - **Notification Permission**: CloudWatchからメトリックグラフを取得するのを許可する権限
    - **Read-only command permissions**: Readのコマンドを許可する権限
    - **Lambda-invoke command permissions**: サポートされているクライアントでLambdaの呼び出しを許可する権限
    - **AWS Support command permissions**: support APIを呼び出しを許可する権限

    ![スクリーンショット 2019-12-09 10.33.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127310/7a473fc8-0ae0-f92f-1a48-8cc2182a12fe.png)


    今回は、Lambdaの呼び出しだけで良かったので、**Lambda-invoke command permissions**のテンプレートを利用しました。Defaultだと、すべてのLambdaに対するinvokeが許可されているようなので、適宜呼び出せる関数は制御したほうが良さそうです。下記がテンプレートで付与されたLambdaの呼び出し用のIAM Policyです。

    ```
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "lambda:invokeAsync",
                    "lambda:invokeFunction"
                ],
                "Resource": [
                    "*"
                ]
            }
        ]
    }
    ```

あとSNSの設定項目がありますが、今回は不要なので割愛します。CloudWatchのAlarmや、CostExplorerのBudgetsに関する通知を送るときは設定が必要になってきます。


## Lambdaを作る
Slack から呼び出すLambdaを作成します。Lambdaの中では下記の処理を実装しました。

- Athenaのクエリを実行、結果をS3に出力させる
- 配置された結果ファイルに対するpresigned-urlを取得する

下記コードです。テーブル等は適当なものに変更してるのでそのままじゃ動かないです。

<details>
<summary>サンプルコード</summary>
<div>

```python:main.py
import boto3
import time

from urllib.parse import urlparse

athena_client = boto3.client('athena')
s3_client = boto3.client('s3')


query_format = '''
select * from <table>
limit 10
'''


def execute():
    response = athena_client.start_query_execution(
        QueryString=query_format,
        ResultConfiguration={
            'OutputLocation': 's3://<S3 bucket name to output result>',
        }
    )
    return response['QueryExecutionId']


def get_output_location(query_execution_id):
    while True:
        response = athena_client.get_query_execution(
            QueryExecutionId=query_execution_id
        )
        print(response['QueryExecution']['Status']['State'])
        if response['QueryExecution']['Status']['State'] in ['QUEUED', 'RUNNING']:
            time.sleep(5)
            continue
        if response['QueryExecution']['Status']['State'] == 'SUCCEEDED':
            return response['QueryExecution']['ResultConfiguration']['OutputLocation']
        print(response)
        return None


def get_presigned_url(location):
    parsed_s3path = urlparse(location)
    print(parsed_s3path)
    response = s3_client.generate_presigned_url(
                    'get_object',
                    Params={
                        'Bucket': parsed_s3path.netloc,
                        'Key': parsed_s3path.path.replace('/','')
                    },
                    ExpiresIn=3600)
    return response


def lambda_handler(context, event):
    query_execution_id = execute()
    output_location = get_output_location(query_execution_id)
    if output_location is None:
        print("Query Failed!!")
        return "Query Failed!!"
    presigned_url = get_presigned_url(output_location)
    print(presigned_url)
    return presigned_url


if __name__ == "__main__":
    context = {}
    event = {}
    lambda_handler(context, event)


```

</div>
</details>

Athenaに対してクエリを投げるときは`start_query_execution`でクエリを実行したあと、`get_query_execution`を実行して結果の状態を把握する必要があるみたいでした。とりあえずLoopの中で5秒おきに実行するようにしていますが、実行時間が長いクエリだとLambdaのコストが増えるため微妙だなと思っています。SNSとかSQSを間に挟んで待ち時間を関数の外に出せば節約できそうですが、今回は一旦Lambda関数内でsleepさせることにしました。

## Lambdaをデプロイする
[Apex](https://apex.run/)を使ってLambdaをデプロイしました。(よく使っているので)
ディレクトリ構成は下記のような感じです。

```:tree
aws-chatbot/
├── functions
│   └── athena-query-executor
│       └── main.py
└── project.json
```

```json:project.json
{
  "name": "aws-chatbot",
  "description": "",
  "runtime": "python3.7",
  "memory": 128,
  "timeout": 300,
  "handler": "main.lambda_handler",
  "role": "arn:aws:iam::<Your AWS Account ID>:role/<Your role name>",
  "nameTemplate": "{{.Function.Name}}",
  "environment": {}
}
```

デプロイは下記コマンドで行います。

```bash:deploy
apex deploy -r ap-northeast-1 -p <your profile>
```

Lambdaに割り当てるIAM Roleには**AmazonAthenaFullAccess**、**AmazonS3ReadOnlyAccess**を割り当てました。本当は権限を絞っていきたいんですが、Athenaの権限が思ったより複雑だったので検証の段階では動かすことを優先しました。ちゃんと運用するときはしっかり理解した上での設定が必要だなと感じています。

## SlackからLambdaを呼び出す
Lambdaがデプロイできたら、いよいよSlackからLambdaを呼び出してみます。

Chatbotの設定画面で設定したchannelに`/invite @aws` でchatbotのユーザーを招待します。
ユーザーが招待できたら次の内容をchannelに投下してLambdaを実行します。

`@aws lambda invoke --function-name <Your function name> --region ap-northeast-1`

![スクリーンショット 2019-12-09 12.05.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127310/27ff0407-1f03-eefc-2746-b760b997b3d0.png)


実行すると本当に実行してよいかを聞かれます。(スクリーンショットの Would you like~ あたり。) **YES** とするとLambdaの実行が始まります。
Lambdaの実行が完了すると戻り値として、**Payload**,**ExecutedVersion**,**StatusCode**が返却されます。
どうやら**Payload**はbase64エンコードされた状態で返却されるようです。。。(マスクしているので見えませんがbase64エンコードされた状態となっています。)想定ではここでpresined-urlをクリックしてAthena実行結果のcsｖをゲットする算段だったのですが、ローカルでbase64デコードする一手間が挟まってしまいました。。
しかし、デコードした結果のURLからは正しくcsvを取得することができました。

## まとめ
今回Chatbotを使ってみた所感です。

#### ChatOps環境を作りやすくなった
AWS Chatbotを使うとAWS連携がより手軽にできるようになったと感じました。以前AWS Chatbotが出る前にもSlackでスラッシュコマンド等を作ったことがあり、そのときはAPI Gateway + Lambdaを作成してバックエンドを作成したのですが、API Gatewayの設定が地味にめんどくさかった記憶があります。今回Chatbotで直接AWSコマンドを実行可能になり、API Gateway等を作る手間は削減されたのかなと感じます。個人的にオペレーション用に作るものはなるべく単純にしておきたいという思いがあるので、その点は少しスッキリしてよかったかなと思います。

#### 権限管理は慎重に
SlackからAWSへの操作をできるようになるため、Chatbotに付与する権限は用途ごとにきっちり絞ったほうがよいなと感じました。DefaultのIAMポリシーテンプレートをそのまま使うのではなく、実行できる関数を絞っておくなどして、不用意な事故を防ぐ必要があるなと感じます。Chatbotに限った話ではないですが、ここで使うIAMにも必要最小限の権限を付与するというのを意識しておけば良さそうです。

#### 個人の環境に左右されにくくなる
特定の操作をSlack上でできるので、よく行う運用作業をいくつかまとめられれば誰でもすぐログ調査等ができるようになる点はいいなと思いました。各個人に払い出されている権限、ツールのバージョン等を統一せずに調査等、必要な操作環境を提供できる点は良さそうです。

#### 戻り値のPayloadをbase64デコードした状態で表示してほしい
今回試してみた結果Payloadがbase64エンコードされて返ってくることがわかりました。。base64デコードした状態で表示してくれれば戻ってきたpresigned-urlから直接ファイルがダウンロードできるのに、、、。ちなみに、AWS CLIで`lambda invoke`を実行するとoutfileに指定したファイルに戻ってきた結果が書き込まれるのですが、それはbase64デコードされた状態となっていました。

#### 呼び出せるコマンドはまだ少ない？
当初Athenaのクエリ実行コマンドを、直接Slackから呼び出そうとしたのですが下記のエラーが出ました。まだ対応しているコマンドが限られているのかもしれません。今後呼び出せるコマンドが増えるといいなと思っています。

> I can't run the command athena start-query-execution because it isn't enabled.

- https://docs.aws.amazon.com/ja_jp/chatbot/latest/adminguide/chatbot-cli-commands.html