ChatWorkのAPI使用回数をLambdaとCloudWatch Logsを使って記録する


ChatWorkでBOTアカウントを運用しているのですが、そのBOTのAPIの残り使用回数が知りたいので作りました。

概要

  1. CloudWatch EventsでLambdaを10分毎に実行
  2. LambdaからChatWorkのAPIを叩き、レスポンスヘッダーからAPI使用回数を取得
  3. 取得した使用回数をCloudWatch Logsへ記録

基本的にAWSのリソースは全てTerraform(インフラの構成をコードで記述し、コマンド一発で起動するツール)を使っています。

最終的なコードはGitHubへ上げています。
https://github.com/hareku/terraform-logging-chatwork-api-limit

LambdaからChatWork APIの使用回数をCloudWatch Logsへ保存

今回作成したLambda Functionでの大まかなフローはこちら。

  1. ChatWorkのAPIを叩いて、次に使用回数がリセットされる時間を取得する
  2. API回数がリセットされる10秒前まで待機する
  3. 再度APIを叩いて使用回数を取得し、CloudWatch Logsへ記録

ChatWorkのAPIドキュメントを見れば分かりますが、ChatWorkでは5分間に100回までのAPI使用制限があり、残りの回数などはレスポンスヘッダーで返ってきます。
それを利用しCloudWatch Logsへ「APIの残り回数」を吐き出しています。

ChatWorkのTokenは環境変数で設定しています。

index.js
const AWS = require('aws-sdk')
const moment = require('moment-timezone')
const axios = require('axios')
axios.defaults.headers.common['X-ChatWorkToken'] = process.env.ChatWorkToken

exports.handler = async (event, context) => {
  try {
    // 次にAPI制限がリセットされる10秒前まで待機する
    const responseForWaiting = await axios.get('https://api.chatwork.com/v2/me')
    const nextResetUnixTime = Number(responseForWaiting.headers['x-ratelimit-reset'])
    const waitUnixTime = nextResetUnixTime - moment().tz('Asia/Tokyo').unix() - 10
    await new Promise(resolve => setTimeout(resolve, waitUnixTime * 1000))

    const responseForLogging = await axios.get('https://api.chatwork.com/v2/me')
    const remaining = Number(responseForLogging.headers['x-ratelimit-remaining'])

    const putLogParams = {
      logEvents: [
        {
          message: `${remaining}`,
          timestamp: nextResetUnixTime * 1000
        }
      ],
      logGroupName: 'ChatWorkAPI',
      logStreamName: 'APIRemaining'
    }

    const logsClient = new AWS.CloudWatchLogs()
    const sequenceTokenIfError = await logsClient.putLogEvents(putLogParams).promise()
      .catch(error => {
        if (error && error.code === 'InvalidSequenceTokenException') {
          // nextSequenceToken
          return error.message.match(/[0-9]+/).pop()
        }
        return Promise.reject(error)
      })

    if (sequenceTokenIfError) {
      putLogParams.sequenceToken = sequenceTokenIfError
      await logsClient.putLogEvents(putLogParams).promise()
    }
  } catch (error) {
    console.error(`[Error]: ${JSON.stringify(error)}`)
    return error
  }

sequenceToken

CloudWatch LogsにはsequenceTokenというものがあります。

これは二回目以降のログ時(ログストリームの2つ目のログ以降)に必ず必要なパラメーターです。
ログ時のレスポンスに毎回sequenceTokenが返ってきますので、次のログ時のパラメーターとして使います。

DynamoDBにsequenceTokenを記録して次回のログで利用する、というやり方もできそうですが、面倒なのでCloudWatch LogsのAPIを2回叩き、1回目をsequenceToken取得用、2回目をログ吐き出し用としています。

dependencies

package.jsonのdependenciesは以下。

package.json
{
  "dependencies": {
    "aws-sdk": "^2.270.1",
    "axios": "^0.18.0",
    "moment": "^2.22.2",
    "moment-timezone": "^0.5.21"
  }
}

Terraformでの環境構築

今回はTerraformを使って構築しました。
terraform applyコマンドですぐに構築することができ、またterraform destroyコマンドによってapplyから構築されたリソースを全て削除できます。ちょっと遊んだ後の片付けが捗りますね。
またLambda Functionのzip化なども自動で行うことが可能です。

今回書いたtfファイルはこちらです。

main.tf
# IAM Role for Lambda
resource "aws_iam_role" "this" {
  name = "RoleForLambda"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Action": "sts:AssumeRole",
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      }
    }
  ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "terraform_lambda_iam_policy_basic_execution" {
  role       = "${aws_iam_role.this.id}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# Lambda Function
data "archive_file" "this" {
  type        = "zip"
  source_dir  = "lambda_function"
  output_path = "lambda_function.zip"
}

resource "aws_lambda_function" "this" {
  filename         = "${data.archive_file.this.output_path}"
  function_name    = "logging_chatwork_api_rate_limit"
  role             = "${aws_iam_role.this.arn}"
  handler          = "index.handler"
  source_code_hash = "${data.archive_file.this.output_base64sha256}"
  runtime          = "nodejs8.10"
  timeout          = 300

  environment {
    variables = {
      ChatWorkToken = "${var.chatwork_token}"
    }
  }
}

# CloudWatch Event
resource "aws_cloudwatch_event_rule" "this" {
  name                = "schedule-check-chatwork-api-limit"
  description         = "Schedule the remaining number of the ChatWork API"
  schedule_expression = "rate(10 minutes)"
}

resource "aws_cloudwatch_event_target" "this" {
  rule = "${aws_cloudwatch_event_rule.this.name}"
  arn  = "${aws_lambda_function.this.arn}"
}

resource "aws_lambda_permission" "this" {
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.this.function_name}"
  principal     = "events.amazonaws.com"
  statement_id  = "AllowExecutionFromCloudWatch"
  source_arn    = "${aws_cloudwatch_event_rule.this.arn}"
}

# CloudWatch Log
resource "aws_cloudwatch_log_group" "this" {
  name = "ChatWorkAPI"
}

resource "aws_cloudwatch_log_stream" "this" {
  name           = "APIRemaining"
  log_group_name = "${aws_cloudwatch_log_group.this.name}"
}

resource "aws_cloudwatch_log_metric_filter" "this" {
  name           = "ChatWorkAPIRemaining"
  pattern        = "[remaining]"
  log_group_name = "${aws_cloudwatch_log_group.this.name}"

  metric_transformation {
    name      = "APIRemaining"
    namespace = "ChatWork"
    value     = "$remaining"
  }
}

resource "aws_cloudwatch_metric_alarm" "living_related_50x_critical" {
  alarm_name          = "chatwork-api-remaining"
  comparison_operator = "LessThanOrEqualToThreshold"
  evaluation_periods  = "1"
  metric_name         = "APIRemaining"
  namespace           = "ChatWork"
  period              = "900"
  statistic           = "Minimum"
  threshold           = "15"
  alarm_description   = "This metric monitor API remaining"
}

あとがき

今回のTerraformとLambdaをまとめたリポジトリをGitHubに上げているので、ご自由にお使いください。
https://github.com/hareku/terraform-logging-chatwork-api-limit

2018年7月10日 修正

DynamoDBへAPIの使用回数や残り回数などを記録していましたが、CloudWatch Logsへ記録するように修正しました。
メトリクスを使うことによってグラフ化やアラートを設定することができるため、ユースケースとしてはCloudWatch Logsの方が正しそうだからです。