Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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の方が正しそうだからです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away