API Gateway と DynamoDB でかんたん Slack slash command をつくった
API Gateway はダイレクトに DynamoDB へアクセスできます。動的処理を含まない簡単な文章を返すだけの Slash command ならば API Gateway と DynamoDB で完結できます。
ということで実際に登録して使っていますが、便利です。特に絶対に使うけれどもたまにしか使わないので忘れるというものをサッと取りだせるのがいいですね。Slack は他の通知系を集約していたりするので、そこに機能を集めていくと便利さが増すような気がします。
日記代わりに書き残しておきます。
さて
API Gateway と DynamoDB の準備は Terraform で、DynamoDB への値の登録は TypeScript を用いました。しかし設定内容はそれほど多くないので、いずれも手作業で行えるでしょう。特に DynamoDB はコンソールから手軽に編集できるため、管理が容易だと感じました。
まず Terraform ファイルを用意します。簡単な内容なので実際は一枚ものにしていますが、各セクションに分解しました。
一枚もの https://gist.github.com/mmmpa/4d472e0d844d894ebbdc4de8166010f3
環境変数を展開
ss_token
には Slack slash command が送信する token を設定します。
$ TF_VAR_ss_token=xxxx terraform apply
variable "api_name" { default = "librarian_api" }
variable "table_name" { default = "librarian_db" }
variable "ss_token" {}
DynamoDB を用意する
doc_name
で GetItem
し、content
というプロパティからコマンドで表示するテキストを取りだす想定です。
resource "aws_dynamodb_table" "db" {
name = "${var.table_name}"
read_capacity = 1
write_capacity = 1
hash_key = "doc_name"
attribute {
name = "doc_name"
type = "S"
}
}
API Gateway の構築
基礎部分
resource "aws_api_gateway_rest_api" "main" {
name = "${var.api_name}"
description = "${var.api_name}"
}
resource "aws_api_gateway_method" "method" {
rest_api_id = "${aws_api_gateway_rest_api.main.id}"
resource_id = "${aws_api_gateway_rest_api.main.root_resource_id}"
http_method = "GET"
authorization = "NONE"
request_parameters = {
"method.request.querystring.text" = false
"method.request.querystring.token" = true
}
}
クエリ文字列を変形し、DynamoDB へリクエストを送信する部分
Slack が送信する token
の検証は #if($input.params('token') != "${var.ss_token}")
で行い、正しくなければ処理を切りあげます。
resource "aws_api_gateway_integration" "integration" {
rest_api_id = "${aws_api_gateway_rest_api.main.id}"
resource_id = "${aws_api_gateway_rest_api.main.root_resource_id}"
http_method = "${aws_api_gateway_method.method.http_method}"
type = "AWS"
uri = "arn:aws:apigateway:ap-northeast-1:dynamodb:action/GetItem"
integration_http_method = "POST"
credentials = "${aws_iam_role.role.arn}"
passthrough_behavior = "NEVER"
request_templates = {
"application/json" = <<EOF
#if($input.params('token') != "${var.ss_token}")
#stop
#end
#if($input.params('text') == '')
#set($key = 'help')
#else
#set($key = $input.params('text'))
#end
{
"TableName": "${var.table_name}",
"Key": {
"doc_name": {
"S": "$key"
}
}
}
EOF
}
}
DynamoDB からのレスポンスを Slack のコメントフォーマットに変形する部分
resource "aws_api_gateway_method_response" "res" {
rest_api_id = "${aws_api_gateway_rest_api.main.id}"
resource_id = "${aws_api_gateway_rest_api.main.root_resource_id}"
http_method = "${aws_api_gateway_method.method.http_method}"
status_code = "200"
response_models = {
"application/json" = "Empty"
}
}
resource "aws_api_gateway_integration_response" "integration_res" {
depends_on = [
"aws_api_gateway_integration.integration",
]
rest_api_id = "${aws_api_gateway_rest_api.main.id}"
resource_id = "${aws_api_gateway_rest_api.main.root_resource_id}"
http_method = "${aws_api_gateway_method.method.http_method}"
status_code = "${aws_api_gateway_method_response.res.status_code}"
selection_pattern = "200"
response_templates = {
"application/json" = <<EOF
#set($text = $input.path('$.Item.content').S)
#if($text == "")
{ "text": "command not found" }
#else
{ "text": $text }
#end
EOF
}
}
API Gateway が DynamoDB にアクセスするためのロール
特に信頼関係と称される AssumeRole
を忘れがちなので注意しましょう。
resource "aws_iam_role" "role" {
name = "${var.api_name}_api_role"
path = "/"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_iam_role_policy" "policy" {
name = "get-sample"
role = "${aws_iam_role.role.id}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem"
],
"Resource": [
"${aws_dynamodb_table.db.arn}"
]
}
]
}
EOF
}
API Gateway を terraform apply ごとにデプロイし、URL を出力する
resource "aws_api_gateway_deployment" "deploy" {
depends_on = [
"aws_api_gateway_integration.integration",
]
rest_api_id = "${aws_api_gateway_rest_api.main.id}"
stage_name = "doc"
description = "Deployed at ${timestamp()}"
}
output "endpoint" {
value = "${aws_api_gateway_deployment.deploy.invoke_url}"
}
Terraform の設定内容は以上です。
DynamoDB へ登録する
ここからは値の登録です。
今回は JSON を用意して TypeScript (ts-node
) で登録しました。
{
"doc1": {
"content": "doc1 の内容"
},
"doc2": {
"content": "doc2 の内容"
}
}
import { DynamoDB } from 'aws-sdk'
import * as fs from 'fs'
export type DocT = {
description: string
content: string
}
export type DocsT = { [key: string]: DocT }
const { TABLE_NAME: TableName = 'librarian_db' } = process.env
function put (db: DynamoDB, item: DynamoDB.PutItemInput): Promise<DynamoDB.PutItemOutput> {
return new Promise((resolve, reject) =>
db.putItem(item, (err, data) => (err ? reject(err) : resolve(data))))
}
async function main (): Promise {
const db = new DynamoDB()
const data: DocsT = JSON.parse(fs.readFileSync('./built/content.json').toString())
const items = Object.keys(data).map(k => ({
TableName,
Item: {
doc_name: { S: k },
content: { S: JSON.stringify(data[k].content) },
},
}))
for (let i = 0, l = items.length; i < l; i += 1) {
const item = items[i]
await put(db, items[i]).catch(console.error)
console.log(`put: ${JSON.stringify(item, null, ' ')}`)
}
}
main().catch(console.error)