Lambdaで動くupdate_nameをGoで書いてみた

Go言語の練習ついでに作ってみた.リポジトリは以下.

GitHub - cohalz/update_name: update_name by Go Lambda

仕様

  • CloudWatch Eventから毎分のスケジュールを設定する.
  • 変更ルールとAPIキー情報を入力として渡してLambdaを実行する.
    • トリガーのタイプ(前方一致か後方一致か),トリガーとなるワード,変更後の名前にトリガーを含むか,リプライの内容(%sの部分に変更後の名前が埋め込まれる)を設定できる.
    • 1つのLambdaに対しAPIキーの違うイベントを複数Lambda渡すだけでマルチユーザでの利用が可能.
  • 入力例
{
    "rules": [
        {
            "triggerType": "suffix",
            "triggerword": "はる"
        },
        {
            "triggerType": "prefix",
            "triggerWord": "@cohalz update_name ",
            "omitTriggerWord": true,
            "replyFormat": "%sになりました"
        }
    ],
    "credential": {
        "accessToken": "",
        "accessTokenSecret": "",
        "consumerKey": "",
        "consumerSecret": ""
    }

ここからは実装についての話をする.

UserStream無しでupdate_nameを実現するために

8月にUserStreamが終了し,リアルタイムで反応をすることが出来なくなった.

そのため,UserStreamでないAPIを使って出来る限り早い反応を返すようにしなければならない.

Twitter APIのRate Limitは15分に15回までの制限が掛けられている.

参考: Rate Limiting — Twitter Developers

つまりは1分に1回のペースで動かすということになる.

幸いにも,CloudWatch Eventはトリガーとして毎分が選べるため採用することにした.

取得するツイートが被らないようにするために

毎分取得するとして前回取得したツイートと被らないようにする必要がある.

被ってしまうとTLの流速が遅いときなどは毎回反応してしまうという事が起きてしまう.

それを防ぐ方法がパラメータで用意されている.

GET statuses/user_timeline — Twitter Developers

パラメータにsince_idとしてツイートのidを追加することで,そのツイートid以降のツイートのみを取得することができる.

そのため,取得するツイート数の最大値であるcountパラメータを200にしつつ,since_idを設定することにより最大限TLが重複なく拾えるようになる.

当然,1分に200以上のツイートが流れている場合は反応できない可能性があるので注意が必要.

Lambdaから前回の状態を取得する

では,状態を持たないLambdaでどうやってsince_idを保存・取得するかということについてはLambdaの環境変数に書き込むという方法を取った.

外部のシステムに依存しないKVSとして簡単に利用することができるが,いくつか制限があるため,利用する際は気をつけないといけない. 例としては,

  • キーの名前や保存容量に気を付ける
    • 特に容量は合計で4KBまでなので大量にデータを保存しておく事はできない.
    • 公式のドキュメントに制限が書いてある. docs.aws.amazon.com
  • IAMに権限の追加が必要
    • lambda:UpdateFunctionConfiguration の権限が追加で必要になる.
  • 環境変数をアップデートする際,自分で定義した環境変数のみをすべて渡す必要がある
    • Lambdaの環境変数はシステム側で用意された環境変数が含まれている.
    • 自分で定義した環境変数のみを渡すために,自分で定義する環境変数はprefixを決めておくと扱いやすくなる.
  • ライブラリの名前空間が衝突する
    • 今回は"github.com/aws/aws-lambda-go/lambda""github.com/aws/aws-sdk-go/service/lambda"が同じlambdaという名前になるためリネームが必要.

ここまで説明した環境変数に書き込む部分の実装例がこちら.

import (
    "log"
    "os"
    "strconv"
    "strings"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    lambda_sdk "github.com/aws/aws-sdk-go/service/lambda"
)

func setSinceIDToEnv(functionName string, screenName string, sinceID int64) {
    sinceIDStr := strconv.FormatInt(sinceID, 10)

    sess := session.Must(session.NewSession())

    svc := lambda_sdk.New(
        sess,
        aws.NewConfig().WithRegion("ap-northeast-1"),
    )

    m := make(map[string]*string)

    envs := os.Environ()

    // 自分で定義した環境変数のみをkey-value形式のmapに保存
    for _, env := range envs {
        if !strings.HasPrefix(env, "sinceID_") {
            continue
        }
        envKeyValue := strings.SplitN(env, "=", 2)
        m[envKeyValue[0]] = &envKeyValue[1]
    }

    m["sinceID_"+screenName] = &sinceIDStr

    env := &lambda_sdk.Environment{
        Variables: m,
    }

    input := &lambda_sdk.UpdateFunctionConfigurationInput{
        FunctionName: &functionName,
        Environment:  env,
    }

    _, err := svc.UpdateFunctionConfiguration(input)

    if err != nil {
        log.Fatal(err)
    }

}

名前変更部分について

update_nameの要である名前変更についてもすぐに実装ができたわけではなかった.

ChimeraCoder/anacondaというライブラリを使うことにしたのだけれど,名前を変更するエンドポイントに対する関数が実装されていなかった.

もう少し探してみると,Pull Requestはあるが余計な機能追加によりコンフリクトが起きていて放置されていたという状態だということがわかった.

そのため,その機能のみに絞って自分がPRを送ってみることにした.

github.com

送ったあとに気がついたのだが,このライブラリはしばらくメンテされていない状態だった.

他のPRも放置されていてマージされる気配もないので,今はforkした自分のリポジトリから使うようにしている.

[追記] マージされていた.

その他

ローカルで実行確認をするために,SAMでテンプレートを書いた.

ローカルではfunctionNameがtestという名前になる他に,環境変数の保存ができない気がしたためにローカルでは実行していない.

SAMのデプロイ・テストにはMakefileを用意するのがやはり便利だと感じた.

余談

前回update_nameを作ったのは三年半ほど前のことで,何故か今あまり書いていないRubyだったのもありメンテナンスできていなかった.

今回の機会にそれがGoとLambdaでメンテナンスしやすい形に変更できたので良かった.

Goでなにか書いてみるのは初めてだったけど,VS Codeの拡張もあり意外とスムーズに実装できた.

今後おもちゃを作ってみるときはGoで書いてみようと思う.

github.com