Terraform で Slack Evetns API + API Gateway + Lambda な slackbot を構築

概要

やりたいこと

Slack Events API の Challenge 認証する API Gateway + Lambda を Terraform で作成したい。

API Gateway

API Gateway に関しては下記が大変わかりやすい。

下記記事から API Gateway 作成パターンを引用。

機能選択肢
認証・認可IAM | Lambdaオーソライザー | Cognitoオーソライザー
エンドポイントタイプエッジ | リージョン | プライベート
統合タイプLambda | HTTP | Mock | AWSサービス | VPCリンク
プロキシ統合TRUE | FALSE

以下の Lambda プロキシ統合の API Gateway を作成する(ヘッダーを受け取りたいため)。

  • 認証・認可: なし
  • エンドポイントタイプ : エッジ
  • 統合タイプ : Lambda
  • プロキシ統合 : True

Terraform

Terraform 準備

vi main.tf
provider "aws" {
  profile = "terraform"
  region  = "ap-northeast-1"
}

provider "archive" {
  version = "1.3.0"
}

terraform {
  required_version = ">= 0.12.0"
}

初期化

terraform init

Lambda 作成

ローカルで作成した Lambda 用プログラムを Terraform でデプロイする。

mkdir app
vi app/lambda.tf
import os
import json
import logging
import urllib.request

# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handle_slack_event(request: dict, context) -> str:
    logging.info(json.dumps(request))

    header = request['headers']
    slack_event = json.loads(request['body'])


    logging.info(json.dumps(header))
    logging.info(json.dumps(slack_event))

    if "challenge" in slack_event:
        #return slack_event.get("challenge")
        return {
            'statusCode': 200,
            'body': slack_event.get("challenge")
        }
vi lambda.tf
locals {
  function_name = "slackbot_function"
}

# ====================
#
# Archive
#
# ====================
data "archive_file" "function_source" {
  type        = "zip"
  source_dir  = "app"
  output_path = "archive/slackbot.zip"
}

# ====================
#
# Lambda
#
# ====================
resource "aws_lambda_function" "alert_function" {
  function_name = local.function_name
  handler       = "lambda.handle_slack_event"
  role          = aws_iam_role.lambda_role.arn
  runtime       = "python3.8"

  filename         = data.archive_file.function_source.output_path
  source_code_hash = data.archive_file.function_source.output_base64sha256

  depends_on = [aws_cloudwatch_log_group.lambda_log_group]

  tags = {
    Name = "${terraform.workspace}-slackbot"
  }
}

# ====================
#
# IAM Role
#
# ====================
data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "lambda_role" {
  name               = "SlackbotLambdaRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}


# ====================
#
# CloudWatch
#
# ====================
resource "aws_cloudwatch_log_group" "lambda_log_group" {
  name = "/aws/lambda/${local.function_name}"
}

API Gateway 作成

vi api_gateway.tf
locals {
  api_gateway_name = "slackbot_api"
}

# ====================
#
# API Gateway
#
# ====================
resource "aws_api_gateway_rest_api" "slackbot_api" {
  name = local.api_gateway_name
  endpoint_configuration {
    types = ["EDGE"]
  }
}

resource "aws_api_gateway_resource" "slackbot_api" {
  rest_api_id = aws_api_gateway_rest_api.slackbot_api.id
  parent_id   = aws_api_gateway_rest_api.slackbot_api.root_resource_id
  path_part   = "slackbot_api"
}

resource "aws_api_gateway_method" "slackbot_api" {
  resource_id   = aws_api_gateway_resource.slackbot_api.id
  rest_api_id   = aws_api_gateway_rest_api.slackbot_api.id
  http_method   = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "slackbot_api" {
  rest_api_id             = aws_api_gateway_rest_api.slackbot_api.id
  resource_id             = aws_api_gateway_resource.slackbot_api.id
  http_method             = aws_api_gateway_method.slackbot_api.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY" #Lambda proxy integration
  uri                     = aws_lambda_function.alert_function.invoke_arn
}

resource "aws_api_gateway_deployment" "slackbot_api" {
  rest_api_id = aws_api_gateway_rest_api.slackbot_api.id

  triggers = {
    # NOTE: The configuration below will satisfy ordering considerations,
    #       but not pick up all future REST API changes. More advanced patterns
    #       are possible, such as using the filesha1() function against the
    #       Terraform configuration file(s) or removing the .id references to
    #       calculate a hash against whole resources. Be aware that using whole
    #       resources will show a difference after the initial implementation.
    #       It will stabilize to only change when resources change afterwards.
    redeployment = sha1(jsonencode([
      aws_api_gateway_resource.slackbot_api.id,
      aws_api_gateway_method.slackbot_api.id,
      aws_api_gateway_integration.slackbot_api.id,
    ]))
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_stage" "slackbot_api" {
  deployment_id = aws_api_gateway_deployment.slackbot_api.id
  rest_api_id   = aws_api_gateway_rest_api.slackbot_api.id
  stage_name    = "v1"
}

resource "aws_api_gateway_method_response" "response_200" {
  rest_api_id = aws_api_gateway_rest_api.slackbot_api.id
  resource_id = aws_api_gateway_resource.slackbot_api.id
  http_method = aws_api_gateway_method.slackbot_api.http_method
  status_code = "200"

  response_models = {
    "application/json" = "Empty"
  }
  depends_on = [aws_api_gateway_method.slackbot_api]
}

resource "aws_api_gateway_integration_response" "response_200" {
  rest_api_id = aws_api_gateway_rest_api.slackbot_api.id
  resource_id = aws_api_gateway_resource.slackbot_api.id
  http_method = aws_api_gateway_method.slackbot_api.http_method
  status_code = aws_api_gateway_method_response.response_200.status_code

  response_templates = {
    "application/json" = ""
  }
  depends_on = [aws_api_gateway_integration.slackbot_api]
}

# ====================
#
# Lambda Permission
#
# ====================
resource "aws_lambda_permission" "apigw_lambda" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.alert_function.function_name
  principal     = "apigateway.amazonaws.com"

  # More: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html
  source_arn = "arn:aws:execute-api:ap-northeast-1:657885203613:${aws_api_gateway_rest_api.slackbot_api.id}/*/${aws_api_gateway_method.slackbot_api.http_method}${aws_api_gateway_resource.slackbot_api.path}"
}


# ====================
#
# Log
#
# ====================
resource "aws_cloudwatch_log_group" "slackbot_api" {
  name              = "/API-Gateway-Execution-Logs_${aws_api_gateway_rest_api.slackbot_api.id}/${aws_api_gateway_stage.slackbot_api.stage_name}"
  retention_in_days = 7
  # ... potentially other configuration ...
}

デプロイ。

terraform apply

URL をメモっておく。

Slack App 作成

Slack APP を新規で作成。

Event Subscriptions より、Enable Events 後に API Gateway の URL を入力し、Verified となれば OK。

参考

terraformでAPI Gatewayを構築してAPI Key認証を設定してみた

API Gateway + Lambda プロキシ結合の使用有無による違い

Slack へ Events API で投稿をモニタリング、投稿にアクションする Slack Bot 作成