SlackアプリをLambda+RestAPIGatewayで実行

# 概要
SlackアプリをAWSのサーバレス構成で動かしたいを思って調査していたら、SlackBoltでServerlessFrameworkを使用する事例が出てきました。とりあえず使用してみたらHTTP APIGatewayを作成してくれて、モジュールもLambda関数上にアップロードしてくれました。が、細かいところに手が届かず。。。

細かいところまで実装を考えTerraformで実装し、かつRestAPIGateway使ってSlackアプリを動かしてみたいと思います。
なお、SlackアプリやAWSの設定などは以下SlackBoltのmanualを見ながら設定しています。日本語なので、以下を見れば大体できるようにはなるかと思います。

https://slack.dev/bolt-js/ja-jp/tutorial/getting-started


# Slackアプリ作成
下準備ということで、Slackアプリを作成しておきます。


[Create New APP]をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/1adbad53-e36c-52e3-bc43-6ed46485a23a.png)

[From scratch]の方をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/7f6fa11c-13f1-b75b-a357-38010b20c0d2.png)

[App Name]にアプリ名を入力しします。今回は「TESTAPP」とします。
次に、[Pick a workspace to develop your app in]にアプリを追加するワークスペースを選択します。選択したら[Create App]をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/a6d31d02-bb17-4ae9-96ba-e895ff6a27e5.png)

botトークンの設定をするために、左メニューから[OAuth＆Permissions]をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/e5856c19-8fc0-d2f9-1673-f1ab5f5519f0.png)

[Scopes]項目から、[Bot Token Scopes]で[Add an OAuth Scope]をクリックし、[chat:write]を選択します。[User Token Scope]でも同様に設定をします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/9169dbd5-4096-037e-b8f4-2feb3dcda5ec.png)

ワークスペースにアプリをインストールします。
左メニューから[Install App]をクリックし、[Install App to Your Team]画面より、[Install to Workspace]をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/2071b7d0-4350-ed06-9315-bc4e750317de.png)

インストールの確認画面が表示されるため、[許可する]をクリックします。
インストール後、トークンが表示されます。[Bot User OAuth Token]をメモ帳等に記録しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/50907e10-d809-4641-5da3-f0b8950dde58.png)

次に署名のシークレットを確認するために左メニューから[Basic Information]をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/d6d5d0e7-135d-e010-2bc4-6d2869b3b758.png)

[App Credentials]項目から[Signing Secret]をメモ帳に記録しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/37ae25b8-2195-541a-585b-eed54168ea47.png)


# モジュールの準備
今回はLambdaLayerを作成します。LambdaLayerに含める外部モジュールをダウンロードしておきます。以下を実行してください。
```
mkdir package
cd package
mkdir python
pip install slack_bolt -t ./python
cd ../..
```
# Lambda関数
今回は、helloとアプリにメンションを打つと返信を返してくれるサンプルを用意します。
```slack.py
import os
from slack_bolt import App
from slack_bolt.adapter.aws_lambda import SlackRequestHandler


slackApp = App(
    process_before_response=True,
    token=os.environ.get("SLACK_BOT_TOKEN"),
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
)

@app.message("hello")
def message_hello(message, say):
    say(f"こんにちは <@{message['user']}>")

def handler(event, context):
    slack_handler = SlackRequestHandler(app=slackApp)
    return slack_handler.handle(event, context)
```


# AWS環境構築
とりあえず、こんな環境を構築します。
概要にも書いた通りAPIGatewayはあえてRestAPIを使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/7d74d3ab-71af-2ba5-d2b4-c43ce2eb59e9.png)

上記構成をterraformで構築します。
```main.tf
###################################
# IAM
###################################
#Lamda用の信頼されたエンティティ
data "aws_iam_policy_document" "lambda_assume_policy_doc"{
    statement {
        effect = "Allow"
        principals {
            type = "Service"
            identifiers = ["lambda.amazonaws.com"]
        }
        actions = ["sts:AssumeRole"]
    }
}
#APIGateway用の信頼されたエンティティ
data "aws_iam_policy_document" "apigateway_assume_policy" {
    statement {
        effect = "Allow"
        principals {
            type = "Service"
            identifiers = ["apigateway.amazonaws.com"]
        }
        actions = ["sts:AssumeRole"]
    }
}
#Cloudwatchへのアクセス権限
data "aws_iam_policy_document" "cw_policy_doc"{
    statement {
        effect = "Allow"
        actions = [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:DescribeLogGroups",
            "logs:DescribeLogStreams",
            "logs:PutLogEvents",
            "logs:GetLogEvents",
            "logs:FilterLogEvents"
        ]
        resources = ["*"]
    }
}
#Lambda用ロール
resource "aws_iam_role" "lambda_role" {
    name = "lambda_role"
    assume_role_policy = data.aws_iam_policy_document.lambda_assume_policy_doc.json
}
#IPAGateway用ロール
resource "aws_iam_role" "ipagateway_role" {
    name = "ipagateway_role"
    assume_role_policy = data.aws_iam_policy_document.apigateway_assume_policy.json
}
#Cloudwatch操作用ポリシー
resource "aws_iam_policy" "cw_policy" {
    name = "cw_policy"
    path = "/"
    policy = data.aws_iam_policy_document.cw_policy_doc.json
}
#Lambda用ロールへCloudwatch操作用ポリシーをアタッチ
resource "aws_iam_policy_attachment" "slack_iam_policy_attache"{
    name = "slack_iam_policy_attache"
    policy_arn = aws_iam_policy.cw_policy.arn
    roles = [
        aws_iam_role.lambda_role.name
    ]
}
#APIGateway用ロールへCloudwatch操作用ポリシーをアタッチ
resource "aws_iam_policy_attachment" "slack_apigateway_policy_attache"{
    name = "slack_apigateway_policy_attache"
    policy_arn = aws_iam_policy.cw_policy.arn
    roles = [
        aws_iam_role.ipagateway_role.name
    ]
}
###################################
# Lambda
###################################
#slack.pyをzip化
data "archive_file" "slack_archive_file"{
    type = "zip"
    source_file = "./slack.py"
    output_path = "./slack.zip"
}
#モジュールをzip化
data "archive_file" "package"{
    type = "zip"
    source_dir = "./package"
    output_path = "./package.zip"
}
#Lambda関数作成
resource "aws_lambda_function" "slack_fn"{
    function_name = "slack_fn"
    role = aws_iam_role.lambda_role.arn
    runtime = "python3.8"
    handler = "slack.handler"
    filename = data.archive_file.slack_archive_file.output_path
    source_code_hash = data.archive_file.slack_archive_file.output_base64sha256
    environment {
        variables = {
            SLACK_SIGNING_SECRET = "<署名シークレット>"
            SLACK_BOT_TOKEN = "<SlackBotトークン>"
        }
    }
    layers = ["${aws_lambda_layer_version.slack_layer.arn}"]
    depends_on = [
        aws_lambda_layer_version.slack_layer
    ]
}
#Lambda Layer作成
resource "aws_lambda_layer_version" "slack_layer" {
    layer_name = "slack_layer"
    filename = "${data.archive_file.package.output_path}"
    compatible_runtimes = ["python3.8"]
    source_code_hash = data.archive_file.package.output_base64sha256
    depends_on = [
        data.archive_file.package
    ]
}


###################################
# API Gateway
###################################
#RestAPI作成
resource "aws_api_gateway_rest_api" "api_for_slack" {
    name = "api_for_slack"
}
#APIGatewayのリソース作成
resource "aws_api_gateway_resource" "api_for_slack_resource" {
    rest_api_id = aws_api_gateway_rest_api.api_for_slack.id
    parent_id = aws_api_gateway_rest_api.api_for_slack.root_resource_id
    path_part = "api_for_slack"
}
#APIGatewayのメソッド情報定義
resource "aws_api_gateway_method" "api_for_slack_method"{
    rest_api_id = aws_api_gateway_rest_api.api_for_slack.id
    resource_id = aws_api_gateway_resource.api_for_slack_resource.id
    http_method = "ANY"
    authorization = "NONE"
    api_key_required = false
}
#APIGatewayのレスポンスメソッド情報定義
resource "aws_api_gateway_method_response" "api_for_slack_method_response"{
    rest_api_id = aws_api_gateway_rest_api.api_for_slack.id
    resource_id = aws_api_gateway_resource.api_for_slack_resource.id
    http_method = aws_api_gateway_method.api_for_slack_method.http_method
    status_code = 200
    response_models = {
        "application/x-www-form-urlencoded" = "Empty"
    }
    depends_on = [
        aws_api_gateway_method.api_for_slack_method
    ]
}
#APIGatewayの統合リクエスト情報を定義
resource "aws_api_gateway_integration" "api_for_slack_integration" {
    rest_api_id = aws_api_gateway_rest_api.api_for_slack.id
    resource_id = aws_api_gateway_resource.api_for_slack_resource.id
    http_method = aws_api_gateway_method.api_for_slack_method.http_method
    integration_http_method = "ANY"
    type = "AWS_PROXY"
    uri = aws_lambda_function.slack_fn.invoke_arn
    
}
#APIGatewayのデプロイを定義
resource "aws_api_gateway_deployment" "api_for_slack_deployment"{
    rest_api_id = aws_api_gateway_rest_api.api_for_slack.id
    lifecycle {
    create_before_destroy = true
    }
    depends_on = [
        aws_api_gateway_integration.api_for_slack_integration
    ]
}
#APIGatewayのステージを定義
resource "aws_api_gateway_stage" "api_for_slack_stage" {
    rest_api_id = aws_api_gateway_rest_api.api_for_slack.id
    deployment_id = aws_api_gateway_deployment.api_for_slack_deployment.id
    stage_name = "developer"
}
#APIGatewayの詳細設定
resource "aws_api_gateway_method_settings" "api_for_slack_setting" {
    rest_api_id = aws_api_gateway_rest_api.api_for_slack.id
    stage_name = aws_api_gateway_stage.api_for_slack_stage.stage_name
    method_path = "*/*"
    settings {
        metrics_enabled = true
        logging_level   = "INFO"
    }
    depends_on = [
        aws_api_gateway_account.api_for_slack_account
    ]
}
#Lambda実行権限設定
resource "aws_lambda_permission" "api_for_slack_lambda_permisson" {
    action        = "lambda:InvokeFunction"
    function_name = aws_lambda_function.slack_fn.function_name
    principal     = "apigateway.amazonaws.com"
    source_arn    = "${aws_api_gateway_rest_api.api_for_slack.execution_arn}/*/${aws_api_gateway_method.api_for_slack_method.http_method}/${aws_api_gateway_resource.api_for_slack_resource.path_part}"
}
#APIGatewayからCloudwatchへログ出力するためのロール設定
resource "aws_api_gateway_account" "api_for_slack_account"{
        cloudwatch_role_arn = aws_iam_role.ipagateway_role.arn
}
```

上記を適用して、AWSコンソールにログインし、APIGatewayのURLを確認し、メモ帳に記録しておきます。

# 再びSlackAPP 設定
このままでは、SlackAppはAPIを実行してくれません。
SlackAppをメンションしたら、APIを実行してくれるように設定をします。

ブラウザからSlack App設定画面にアクセスし、左メニューから[Event Subscriptions]をクリックし、[Enable Event]を有効化します。次に[Request URL]にAPIGatewayのURLを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/9055e1d7-b63f-0608-d391-4617dc276d57.png)

その下の項目に[Subscribe to bot events]という項目があります。[Add Bot User Event]をクリックし、[message.channels]と[message.groups]を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/58467ffa-1455-8ea7-7cf6-3c2778841dec.png)
選択が完了したら、一番下にある[Save Changes]をクリックし、設定を保存します。

設定を保存すると、再インストールする必要があると一番上に表示されるため、アプリの再インストールを実行します。メッセージ内の[reinstall your app]をクリックすると、再インストールを実行できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/6f0e29ff-937f-c340-a140-8fc35bc035c4.png)

次に、左メニューから[Interactivity&Shortcuts]をクリックし、[Interactivity]を有効化します。[Request URL]にAPIGatewayのURLを入力し、[Save Changes]をクリックし、設定を保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/d6440a7b-2a35-ccf3-472b-7305b507c921.png)

# SlackAPP実行

SlackAPPをチームに追加します。

Slackを実行し、アプリを追加するチームを選択し、画面上部のチーム名をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/41d6e27b-ff03-0638-27c8-c53acabe4b06.png)

[インテグレーション]タブを開き、[アプリを追加する]をクリックします。
※以下は追加済みの画像になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/ab6fcd30-a2e5-5f84-83e4-b84723d930ef.png)

アプリ一覧から作成したアプリを追加します。
追加が完了したら、チーム内でアプリにメンションします。今回は「hello」と入力すると、メンションし返してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3143329/c779af29-93d2-0066-c8a4-1849a2a136c7.png)

# まとめ
今回はあえてTerraformとRestAPIを使っているわけですが、Terraformが長くなりますね。。。要件がなければServerlessFrameworkを使った方が超楽ですね。