2016/08/03

AWS LambdaでサーバーレスFacebook定期ポストを作ってみる

盛大なる技術のムダ使いですが…


今回つくった仕組みの構成図。正直、拍子抜けするくらいカンタンにできました…。


小売業で仕事をしていると、月曜日ってけっこう忙しいんですよね。週のうち最も売上が高い週末を踏まえ、前週までのパフォーマンスを振り返って今週以降のアクションを決めていく。主要メンバーを集めて「さて、どうすっぺ」を考え、話し合い、決めていくのが月曜日です。

そしてアメリカでは珍しいこととは思いますが、日本に居た頃の慣習で月初ってのもこれまた忙しい。月締めの請求書がワラワラとメールで届くのでそれを支払処理に回しつつ、支払いの承認決裁をしたり、チェックつまり小切手にサインしたり(ビジネスの世界でこんなに小切手が使われているとは、私もアメリカに来るまで知りませんでした…)。


そんなわけで、月初日の月曜日ってのは、モロモロに追い立てられるような一日になるわけです。

なので、そのモヤモヤした気持ちを私は「誰だ月初日と月曜日を一緒にしたヤツは!」などとFacebookに投げつけていたわけですが、このポストに対する反応がそこそこあるので、もしかしたら普遍的に社会人が持つ気持ちなのかなぁ、と思っています。ゆえに、これを自動化できたら月初月曜日のモヤモヤも少し晴れるかなぁ、などと考えた次第です。


ちょうどAWSについて勉強したかったし、AWS Lambdaに何となく可能性を感じていてPythonも勉強したかったので、この「月初月曜日のモヤモヤを自動的にサーバーレスでFacebookに投げつける」という仕組みを作ってみようと思い立ちました。


…はい、そこ。


「そんなことして何になるの?」とか聞かない。


仕事じゃないんだから、手段と目的がゴッチャになったって別にいいんです。趣味ですから。


最終的な構造の説明をしますと


今回の盛大なる技術のムダ使いをするにあたって色々なやり方を考えてみたんですが、最終的には以下の図のようになりました。冒頭の図と同じです。


処理の流れと逆にはなりますが、右から順に構造をご説明します。

  1. とりあえず私が一番良く使うソーシャルメディアであるFacebookへのポストを自動化することをゴールに設定します
  2. AWS Lambdaから直接Facebookに投げたいのですが、Facebook側が直接HTTPなどでポストを受け付ける口を持っておらず、OAuthコールバックAPIを私側に用意しなければならないとのことで、サーバーレスに出来ないようです
  3. 回避策として、間にTwitterを挟んでみました。TwitterにはPythonのAPIがあり、Twitter側に事前にLambdaプロセスをアプリとして登録しておけば難なくツイートが可能。そして、ツイートを自動的にFacebookに流す設定はTwitter本家が持っています
  4. Lambda上での処理は私が自分でプログラミングします。しかし、月初月曜日というのはそう頻繁には来ないので、何らかのエラーで処理が止まっていても気が付かない可能性があります
  5. なので、毎月の実行結果をAmazon SNSを使ってメールで私に送る処理も書いています
  6. 最後に、この一連の流れをキックするのはAmazon CloudWatchです

これが構造です。で、処理の流れの順に説明すると、

  1. CloudWatchから毎月月初日に、Lambdaの処理をキックします
  2. Lambda処理で「今日は月初月曜日?」を判定します
  3. 判定結果が真でも偽でも、とりあえずSNSを通じて私にメールします
  4. 判定が真、つまり月初月曜日だった場合、定型文をTwitterにポストします
  5. Twitter上の設定により、その定型文がFacebookにも流れてきます


ということになります。さて、さっそく実装に取り掛かります。


AWSの設定とかPythonのセットアップとか


この仕組みの実装にあたっては、AWSのアカウントを作ったりとか私のPCにPythonをセットアップしたりとか色々あるんですが、すでにWeb上にたくさんのリソース、情報、ブログなどがあるのでここでは省略します。私は以下のようなページを参考にしました。


Pythonまわり

AWSまわり

PythonのTwitterライブラリまわり



ホント、ちょっとググるだけでザクザク情報が出てくる。いい世の中です。


Pythonコードの解説


さて、私が書いた(一部サンプルソースをコピペもしてますが)Pythonコードの解説です。


# -*- coding: utf-8 -*-
import sys
import traceback
from datetime import datetime
from TwitterTokens import tokens
import pytz
import tweepy
import boto3

sns = boto3.client('sns')

def sendSNS(message):
    topic = 'arn:aws:sns:りーじょん:えーあーるえぬ:MondayBlue'
    day = datetime.today()
    subject = '%04d/%02d/%02d MondayBlue result' % (day.year, day.month, day.day)
    response = sns.publish(TopicArn=topic, Message=message, Subject=subject)


def tweetBlue(date):
    # Twitter API tokens
    CONSUMER_KEY = tokens['consumer_key']
    CONSUMER_SECRET = tokens['consumer_secret']
    ACCESS_TOKEN = tokens['access_token']
    ACCESS_TOKEN_SECRET = tokens['access_token_secret']

    auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
    auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
    api = tweepy.API(auth)

    msg = '%04d/%02d/%02d %02d:%02d\n' % (date.year, date.month, date.day, date.hour, date.minute)
    msg += u'【定期ポスト】誰だ月初日と月曜日を一緒にしたヤツは!'

    try:
        api.update_status(msg)
        sendSNS("Tweeted. It's blue Monday today...")

    except tweepy.error.TweepError:
        sendSNS(traceback.format_exc(sys.exc_info()[2]).split('\n'))
        return traceback.format_exc(sys.exc_info()[2]).split('\n') 
#        print traceback.format_exc(sys.exc_info()[2]).split('\n')

    return 'Posted successfully'


def lambda_handler(event, context):

    now = datetime.now(pytz.timezone('US/Pacific'))
#    now = datetime(2016,8,1, now.hour, now.minute)        # デバッグ用

    # 0 = Monday
    if now.weekday() == 0:
        return tweetBlue(now)

    sendSNS("Phew, it's not blue Monday today!")
    return "Ignored, it's not blue Monday today"

if __name__ == '__main__':
    lambda_handler(None, None)



わりと真面目にTweepyが投げるExceptionなんかもCatchしてますが、その辺の理由は後ほど。

たったこれだけのコードで、図解した処理が実現できちゃうんですねー。便利便利。
もちろん、CloudWatchのイベントトリガーとか、LambdaからSNSに通知出すためのIAM作成とか、専用Twitterアカウント(念のため鍵付き)作って私のFacebookと連動させたりとか、Pythonコード以外で色々とやってはいますが。

パソコン1台とクレジットカード(と、あとAWSアカウントのアクティベーションのための電話番号)さえあれば、ここまでのことが出来てしまう。何て便利な世の中。


この仕組みから見えてくること


さて、盛大に技術をムダ使いするだけじゃAWSのソリューションアーキテクトさんたちに怒られてしまうので、少しだけ考察をば。

企業の業務システムを完全サーバーレスにすることはカンタンではないし、その意味があるのかも分かりませんが、例えばこの仕組みを応用すると以下のようなことが出来そうですよね。


  • 週末の夕方4時の時点で目標売上高の7割に届いていなかったら、店長の携帯メール宛に社長名義で自動的にハッパをかける
  • ある商品の在庫が基準値を下回ったら、メールで自動的に仕入先に発注をかける
  • オフィスのウォーターサーバー内のタンクの重さを測る秤を入れておき、ある重さを下回ったらオフィスマネジャーの携帯メール宛に連絡してタンクを補充してもらう


…なんでこんな社畜アイデアしか出てこないんだ俺は…というのはさておき、ちょっとした便利ツールをヒョイっとカンタンに実装できてしまいそうです。上記アイデアのいくつかはホントに作ってみようかな。


ハマったこと


さて、以下はオマケとして、今回の仕組みを作る上でハマった(解決するのに時間がかかった)ポイントをまとめておきます。


ハマったこと1: LambdaはZip内のサブフォルダまでFunctionを探しに行ってはくれない


Pythonのコード群をLambdaにアップロードする際にはZip圧縮します。Zipの中でさらにサブフォルダにファイル格納する挙動をするアーカイバがあって、その場合はLambdaが.pyファイルを見つけられなくなるようで、Lambda上で「Functionが見つからない」というエラーになってしまいます。

Windowsの場合、Zipアーカイブしたいファイルを全選択後、メインの.pyファイルを右クリックしてZipファイルを作るのが良さそう。こうすることで、Zipファイルの中に直接ファイル群が並ぶ形にできます。


ハマったこと2: Twitterは完全同一内容の連続ポストは403 Forbiddenを返す


私のソースコードで、TweepyのExceptionをCatchしている理由がこれです。最初は理由が全然分からなかった…。

もしTweepyを使ってTwitterにポストする際に「Status is a duplicate.」というエラーが表示される場合はこれに該当していると思ってよさそう。

どうもTwitterは投稿前に、直前10程度のツイートと同じ場合には投稿がエラーになるような制御が入っている模様。ほとんどの場合は問題にならない(むしろ無駄ポスト対策には良い)と思いますが、今回のようなツールをテストする際には、だいたい同一内容を連続ポストすることになるので要注意。

私は、とりあえずツイートに日付を入れることで回避しました。


ハマったこと3: Pythonの型指定はわりと厳密


Lambda上でPythonコードをテストしている際、以下のエラーが出ました。

"errorType": "TypeError",
"errorMessage": "list indices must be integers, not str"

エラーメッセージを和訳すると「リストのインデックス(添え字)は文字列じゃなくて整数で指定してね!」というエラー。いやでもリストなんて使ってない…いや、使ってた!

最終的な実装からは削除していますが、サンプルコードにリスト型とディクショナリ型の両方を使っているところがあり、そこで発生していた模様。

Pythonのディクショナリ型(Perlでいう連想配列)は dict = {} のように中かっこで初期化するものの、値の参照は [] つまり大かっこを使う。てっきり値の参照で大かっこを使っていたものだから、ディクショナリ型で初期化しなきゃいけないことに気が付いてませんでした…。これで1時間くらいロスした。ああもったいない。