AWS Chatbot + Lambda(Go)でSlackからRDSを操作する

## はじめに
RDSを常時起動させておく必要がないシステムがあります。そのシステムを使う時だけRDSを起動させたいのですが、使用する人はIAMユーザを持っていないため、コンソールから起動、停止できない、ということがありました！

そこで、SlackからRDSの操作をできるようにしました！

Chatbotを紐づけたSlackのチャンネルからコマンドを実行し、Lambda関数を呼び出すことで、RDSの操作をします。
## やりたいこと
Slackからやりたいことは以下の3つです。

+ RDSの起動
+ RDSの停止
+ RDSのステータスの確認

## 方法検討
調べてみると、Slackから実行する方法が2つ見つかりました。

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

今回は、2020年4月にGAになった、使ってみたかった、Slackとの組み合わせが素晴らしいらしいという観点から**AWS Chatbot**を使ってみることにしました！

## AWS Chatbotってなに？
[公式サイト](https://aws.amazon.com/jp/chatbot/)によると、次のように説明されています。

> AWS Chatbot は、Slack チャンネルや Amazon Chime チャットルームで AWS のリソースを簡単にモニタリングおよび操作できるようにしてくれるインタラクティブエージェントです。AWS Chatbot を使用すると、アラートを受信することや、診断情報の取得、AWS Lambda 関数の呼び出し、AWS サポートケースの作成を行うコマンドを実行することができるようになります。

### Chatbotでできること
今回は、Slackチャンネルから操作を行うようにしたいです。
AWS Chatbotは、AWSサービスの読み取り専用コマンドをサポートしています。AWSリソースを作成、削除、または構成するコマンドは実行できません。

ただし、一部のサービスは読み取りもサポートしていません。IAM、AWS Security Token Service、AWS Key Management ServiceなどはChatbotを通じて読み取りコマンドの呼び出しも行えません。もちろん作成、削除、構成のコマンドも使えません。

RDSの起動/停止はChatbotから操作できず、ステータスの確認はChatbotからコマンドで操作できます。ただし、取得できる大量の情報からステータスのみを抽出したかったので**ChatbotでLambda関数の呼び出し**を実行し、Lambda関数からそれぞれの操作を行うようにします。

#### 参考

+ [SlackチャンネルからAWS CLIコマンドを実行する](https://docs.aws.amazon.com/chatbot/latest/adminguide/chatbot-cli-commands.html)
+ [AWS Chatbot のよくある質問](https://aws.amazon.com/jp/chatbot/faqs/)

### 料金

Chatbot自体には料金はかかりません！
> AWS Chatobot には追加料金はかかりません。お支払いは基盤となるサービス (Amazon Simple Notification Service、AWS GuardDuty、AWS Security Hub など) の使用に対してのみであり、AWS Chatbot を使用していない場合と同様です。また、最低料金や前払いの義務はありません。

参考：https://aws.amazon.com/jp/chatbot/pricing/

今回の構成だとLambdaの料金のみがかかります。


## 構成図
構成は下図の通りです。本記事では、Slack、AWS Chatbot、Lambdaにフォーカスしています。


![chatbot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/b437bbd8-3b93-17f3-cb7a-a9b91df6c272.png)


## やってみる
### Lambda関数の作成
今回は操作ごとに３つのLambda関数を作成しました。

+ RDSの起動
+ RDSの停止
+ RDSのステータスの確認


#### IAMロール
ターゲットRDSの起動、停止、情報を取得するポリシーを作成してアタッチします。今回作る3つのLambda関数にこのロールを設定しました。

```json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "rds:DescribeDBInstances",
                "rds:StopDBInstance",
                "rds:StartDBInstance"
            ],
            "Resource": "＜ターゲットRDSのarn＞"
        }
    ]
}
```

#### ソースコード
**※それぞれクリックしたらソースコードが見れます。**

<details><summary>RDS起動</summary><div>

```go

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/rds"
	"github.com/aws/aws-sdk-go/service/rds/rdsiface"
)

func StartDBInstance(svc rdsiface.RDSAPI) {
	// os.Getenv（）でLambdaの環境変数を取得
	InstanceID := os.Getenv("InstanceID") // DB識別子
	InstanceIDP := aws.String(InstanceID)

	input := &rds.StartDBInstanceInput{
		DBInstanceIdentifier: InstanceIDP,
	}

	result, err := svc.StartDBInstance(input)
	if err != nil {
		panic(err.Error())
	}
	// 結果を出力
	fmt.Println(result)
}

/**************************
    処理実行
**************************/
func run() (interface{}, error) {
	log.Println("--- RDS自動起動バッチ 開始")
	log.Println("----- セッション作成")
	svc := rds.New(session.Must(session.NewSession()))

	log.Println("----- インスタンス起動　実行")
	StartDBInstance(svc)

	log.Println("--- RDS自動起動バッチ 完了")

	res := "RDSを起動しています。"
	return res, nil
}

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

```

</div></details>


<details><summary>RDS停止</summary><div>

```go

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/rds"
	"github.com/aws/aws-sdk-go/service/rds/rdsiface"
)

func StopDBInstance(svc rdsiface.RDSAPI) {
	// os.Getenv（）でLambdaの環境変数を取得
	InstanceID := os.Getenv("InstanceID") // DB識別子
	InstanceIDP := aws.String(InstanceID)

	input := &rds.StopDBInstanceInput{
		DBInstanceIdentifier: InstanceIDP,
	}

	result, err := svc.StopDBInstance(input)
	if err != nil {
		panic(err.Error())
	}
	// 結果を出力
	fmt.Println(result)
}

/**************************
    処理実行
**************************/
func run() (interface{}, error) {
	log.Println("--- RDS自動停止バッチ 開始")
	log.Println("----- セッション作成")
	svc := rds.New(session.Must(session.NewSession()))

	log.Println("----- インスタンス停止　実行")
	StopDBInstance(svc)

	log.Println("--- RDS自動停止バッチ 完了")

	res := "RDSを停止しています。"
	return res, nil
}

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

</div></details>

<details><summary>RDSステータス取得</summary><div>

```go

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/rds"
	"github.com/aws/aws-sdk-go/service/rds/rdsiface"
)

func DescribeDBStatus(svc rdsiface.RDSAPI) interface{} {
	// os.Getenv（）でLambdaの環境変数を取得
	InstanceID := os.Getenv("InstanceID") // DB識別子
	InstanceIDP := aws.String(InstanceID)

	input := &rds.DescribeDBInstancesInput{
		DBInstanceIdentifier: InstanceIDP,
	}

	result, err := svc.DescribeDBInstances(input)
	if err != nil {
		panic(err.Error())
	}

	status := *result.DBInstances[0].DBInstanceStatus
	return status
}

/**************************
    処理実行
**************************/
func run() (interface{}, error) {
	log.Println("--- RDSステータス取得バッチ 開始")
	log.Println("----- セッション作成")
	svc := rds.New(session.Must(session.NewSession()))

	log.Println("----- RDSステータス取得　実行")
	status := DescribeDBStatus(svc)

	log.Println("--- RDSステータス取得バッチ 完了")

	return status, nil
}

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

```

</div></details>


### Slackのチャンネル準備
まずは自分のDMの中で試してみようと思いましたが、上手くいきませんでした。
WebhookでSlackに通知するときは自分のDMに送信することができますが、Chatbotは自分のDMで動かすことができません！なのでチャンネルを作成するようにしましょう。

操作できる人を限定するために、今回はプライベートチャンネルを作成しました。プライベートチャンネルの場合は、下記のコマンドを対象のチャンネルで実行して、Chatbotユーザを招待してください。（試せていませんが、パブリックチャンネルの場合は不要みたいです。）

```
/invite @aws
```

<img width="483" alt="スクリーンショット 2020-08-09 14.34.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/6f0269b2-82c0-afa1-4e44-c8fa7507ed8c.png">



マスキング部分にはチャンネル名が入ります。
Slackの準備はこれでOKです！

### Chatbotの作成
#### IAMロール
Chatbotのチャンネル設定時に、いくつかポリシーテンプレートが用意されており、その中の**Lambda呼び出しコマンドのアクセス許可**というポリシーを選択してIAMロールを簡単に作成することができます。

**Lambda呼び出しコマンドのアクセス許可**のポリシーテンプレートを使用すると下記のようなポリシーが作成されます。

```json

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

ただ、リソースは選択できないので、全てのLambda関数の実行権限があるポリシーがアタッチされます。また、非推奨？とされている`invokeAsync`の権限もついています。

ポリシーテンプレートで作成したとしても作成後に編集は可能ですが、実行できるLambda関数も絞りたかったので、今回は自分で作成しました。

<img width="971" alt="スクリーンショット 2020-08-04 19.35.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/1c512401-bed1-9fb1-e727-3c452dcfabcc.png">


ユースケースは`AWS Chatbot - AWS Chatbot`を選択します。

そして、このポリシーを作成して、Chatbot用のロールにアタッチします。

```json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": [
                "＜RDS起動Lambdaのarn＞",
                "＜RDS停止Lambdaのarn＞",
                "＜RDSステータス取得Lambdaのarn＞"
            ]
        }
    ]
}
```

#### Chatbotの作成
Chatbotの作成を行います！

<img width="823" alt="スクリーンショット 2020-08-04 19.35.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/a7056ab4-fab1-b972-c8be-a15883044a9c.png">

チャットクライアントで`Slack`を選択し、`クライアント設定`をクリックします。そして、先ほど作成したチャンネルのあるワークスペースにサインインして、AWS Chatbotがワークスペースにアクセスできるように権限付与します。

<img width="973" alt="スクリーンショット 2020-08-04 23.23.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/5cf6afac-44d3-ae0d-3c71-f0b42d47334a.png">

`新しいチャネルを設定`から設定を行います。

<img width="972" alt="スクリーンショット 2020-08-04 19.36.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/cf8c057b-896d-6e1e-c082-76ed585b5849.png">

補足事項に沿って名前の設定、Slackのチャンネルの登録を行います。ちなみに、ここで設定する名前はAWS上で識別するのために使うもので、SlackのApp Nameではないです。

<img width="974" alt="スクリーンショット 2020-08-04 19.36.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/a6fd28fa-dd6d-da60-7e7a-4302219c7e8d.png">

IAMロールには先ほど作成したロールを選択します。
今回は通知は行わないので、通知欄は特に設定しません。

これでChatbotの作成も完了です！

### 実行結果

実行はSlackのメッセージで下記のようにコマンドを送信します。

```
@aws service command --options
```

今回は、ChatbotでLambda関数の呼び出しを行うので次のコマンドを実行します。

```
@aws lambda invoke --function-name ＜Lambda関数名＞ --region ap-northeast-1
```

Lambda関数名にはそれぞれの関数名を入力してください。

#### RDSの起動
##### 起動
起動用のLambda関数名を入れて上記のコマンドを実行すると**Would you like me to do so?**と確認されるので、[Yes]を選択します。
これでRDSを起動させることができます。

<img width="597" alt="スクリーンショット 2020-08-09 15.00.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/98298237-2c19-8ac3-ec7f-a1e2664e8be3.png">

##### ステータス確認
起動が完了しました。ステータスを確認してみましょう。

<img width="525" alt="スクリーンショット 2020-08-09 15.14.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/3c46a44e-0cf3-beed-0be3-60556f9702cc.png">

ターゲットのRDSステータスのみが返ってきて、Payloadに入るようになっています。**available**になっているのでRDSの起動が完了して利用可能なことがわかります！

ステータスの種類と意味は[こちら](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Status.html)へ


#### RDSの停止
##### 停止
停止用のLambda関数名を入れたコマンドを実行して、[Yes]を選択します。

<img width="511" alt="スクリーンショット 2020-08-14 15.05.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/96a237ac-489b-f381-304e-c3adbef62c18.png">


##### ステータス確認
停止が完了しました。ステータスを確認してみましょう。


<img width="511" alt="スクリーンショット 2020-08-09 15.42.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/5835a5e4-ddcc-3296-2b89-c7a7ac0c458d.png">


Payloadが**stopped**になっているのでRDSが停止していることがわかります。

無事Slackから操作ができていることを確認できました！


### その他のコマンド
Chatbotのコマンドの全体的なことが知りたいときは次のコマンドを実行します。

```
@aws help
```

#### RDSのコマンドオプションが知りたいとき
あるAWSサービスについてのコマンドオプションが知りたいときは次のコマンドを実行します。

```
@aws rds --help
```

ただ、一覧に表示されてもChatbotに実行権限をつけていないと実行はできません。また、実行権限をつけていても[Chatbotでできること](#chatbotでできること)で記載した通り、基本的には読み取り専用コマンドをサポートしているので、作成、削除、または構成するコマンドなどは実行できません。

<img width="505" alt="スクリーンショット 2020-08-09 15.42.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/7370ec69-5c5f-dd1e-a82f-b1f56e4baa2f.png">


### もっと簡単に実行したい
今回使ったSlackは無料版だったので使えなかったのですが、有料版だと[ワークフロービルダー](https://slack.com/intl/ja-jp/help/articles/360035692513-%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%83%93%E3%83%AB%E3%83%80%E3%83%BC%E3%82%AC%E3%82%A4%E3%83%89)というSlackの機能を使うことができます。このワークフローを使うとコマンドを打たなくて良く、やりたい操作を選択するだけで実行できるので良さそうです。

#### 参考

+ [AWS ChatbotからのLambda起動をCloudFormationとSlackワークフローで使いやすくする](https://dev.classmethod.jp/articles/chatbot-lambda-cfn/)


## おわりに
すごく簡単にSlackとAWSを連携することができました！Chatbot自体にもロールを付与するので、指定した操作以外はできなくなっています。IAMユーザーがない人でも、操作できるようになるのは魅力的ですね！他のサービスでも応用ができるので、今後もいろんなところで使えそうです！

### おまけ
ステータスの確認をLambdaを介さずに行ってみます。

#### Chatbotのポリシー追加
先ほど作成したChatbot用のポリシーに、RDSインスタンスの情報を取得する権限を追加します。

```json
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "rds:DescribeDBInstances",
            "Resource": "*"
        }
```

#### 実行結果

```
@aws rds describe-db-instances --db-instance-identifier ＜DB識別子＞ --region ap-northeast-1
```

<img width="542" alt="スクリーンショット 2020-08-09 14.41.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/57e785e6-327d-74ee-33a8-121a174268c2.png">


取得した情報はまだまだこの下も続いています。
結果は長いですが、`DBInstanceStatus`を確認することでステータスの確認ができます。

今回はステータスのみを抽出するためにLambda関数を使いましたが、AWS CLIのように `--query`が使えるようになるといいなあと思いました。