SNSとLambdaを利用したS3のファイル同期を実装する

データアナリティクス事業本部@札幌の佐藤です。
私は今までAWSでEC2でVPNサーバーやバッチサーバーなどを構築することが多く、AWSが用意しているサーバーレスな環境構築の経験がありませんでした。
今回は入門として、Amazon Simple Notification Service (SNS) とAWS Lambdaを使用し、サーバーレスな環境構築を行ってみました。

今回の実装イメージ


以下のような形で、S3にファイルがPUTされたことをトリガーとし別リージョンのS3へファイルを格納します。

S3には標準でS3クロスリージョンレプリケーションがあります。
今回は同一リージョンでは利用できない制約を避けるため、AWS Lambdaを利用した実装を行いました。

初期準備としてINPUT側とOUTPUT側それぞれにS3を用意しておきます。
とりあえずバケットが存在している状態で大丈夫です。


各機能の連携の関係でAWS Lambdaから作成していきます。

実装

AWS Lambda

平野さんが丁寧に記載されている手順がありますので、こちらを参考にしつつ実装します。

今度こそ理解する!俺式Lambda入門

関数を作成します。

画面ができました。
Amazon CloudWatch Logsの下の点線部にS3を表示させたいので、点線部に記載されている通りロールにS3の権限を付与させます。

画面を下にスクロールすると出てくる実行ロールのロールの表示リンクをクリックします。

今回はS3に対するフル権限を与えていますが、実業務で使用する場合は正しく権限を与える必要があります。

Amazon CloudWatch Logsの下の点線部にS3を表示されました。
Amazon SNSの処理で使用するのでARNをコピーしておきます。

Amazon SNS

ここではAmazon SNS側での「S3→Amazon SNS連携」と「Amazon SNS→AWS Lambda連携」の準備を行います。

トピックを作成します。

トピックを作成していきます。

S3から接続させるためにアクセスポリシーを変更していきます。
AWSのドキュメントに記載の通り、Conditionの中にS3バケット名を追加します。

イベント通知を受け取る送信先を設定する方法

{
  "Version": "2008-10-17",
  "Id": "__default_policy_ID",
  "Statement": [
    {
      "Sid": "__default_statement_ID",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": [
        "SNS:Publish",
        "SNS:RemovePermission",
        "SNS:SetTopicAttributes",
        "SNS:DeleteTopic",
        "SNS:ListSubscriptionsByTopic",
        "SNS:GetTopicAttributes",
        "SNS:Receive",
        "SNS:AddPermission",
        "SNS:Subscribe"
      ],
      "Resource": "arn:aws:sns:ap-northeast-1:XXXXXXXX:cm-sato-tsuyoshi-sns",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:s3:::cm-sato-tsuyoshi-sns-lambda-input"
        }
      }
    }
  ]
}

作成できました。
これでAmazon SNS側でのS3→Amazon SNSの連携の準備は完了です。
次にAmazon SNS→AWS Lambdaの連携の準備をしたいと思います。

サブスクリプションを作成します。

今回はLambdaへの連携になるためプロトコルはAWS Lambdaを選択。
ARNはAWS Lambdaの処理でコピーしていたARNを入力します。

Amazon SNS→AWS Lambdaの連携の準備が完了です。
Amazon SNSのARNは後続の処理で使用するのでARNをコピーしておきます。

S3(input側)

S3側での「S3→Amazon SNSの連携準備」を行います。
input側のディレクトリ構成はs3://cm-sato-tsuyoshi-sns-lambda-input/file/とします。

プロパティタブを選択します。

いくつか設定できるところがありますが、今回はそのうちのEventsの設定を行います。

今回はファイルがPUTされたのをトリガーにファイル連携させていきたいので、イベントはPUTのみ選択します。
プレフィックスは上に書いた通り、file/を入力、サフィックスは今回はテキストファイルをPUTするので、.txtを入力します。
SNSはAmazon SNSの名前を入力します。

作成できました。

保存した際に以下のようなエラーが出力した場合、Amazon SNS側のポリシー設定にミスがあると思われますので見直してください。

AWS Lambda

最初に作成したAWS Lambdaに対して追加で設定を行います。
ここで「Amazon SNS→AWS Lambda連携」と「AWS Lambda→S3連携」の準備を行います。

トリガーを追加して、Amazon SNSからの連携を追加します。

トリガーの設定でSNSを選択し、ARNはAmazon SNSのでコピーしたARNを入力します。

作成できると、AWS Lambdaへの連携にAmazon SNSが追加されます。

続いて、S3への連携を実装します。 上記画面と同じ画面の関数コードに実装していきます。

画面のlambda_function.pyに以下を実装します。
以下で実装されていた内容を一部修正します。
今度こそ理解する!俺式Lambda入門

import boto3
import json

s3 = boto3.client('s3')

def read_file(bucket_name, file_key):
    response = s3.get_object(Bucket=bucket_name, Key=file_key)
    return response[u'Body'].read().decode('utf-8')

def upload_file(bucket_name, file_key, bytes):
    out_s3 = boto3.resource('s3')
    s3Obj = out_s3.Object(bucket_name, file_key)
    res = s3Obj.put(Body = bytes)
    return res

def lambda_handler(event, context):
    print("Lambda Start")

    di = json.loads(event['Records'][0]['Sns']['Message'])
    print(di['Records'][0]['s3']['bucket']['name'])
    print(di['Records'][0]['s3']["object"]['key'])

    input_bucket=di['Records'][0]['s3']['bucket']['name']
    input_key=di['Records'][0]['s3']["object"]['key']
    # S3からファイルを読み込み
    text = read_file(input_bucket, input_key)
    # outputのキーを設定
    output_key = "output/" + input_key
    # ファイルをS3に出力
    upload_file('cm-sato-tsuyoshi-sns-lambda-output', output_key, bytes(text, 'UTF-8'))

    print("Lambda end")

s3://cm-sato-tsuyoshi-sns-lambda-output/output/配下に格納されるように実装しています。
今回はS3バケットをハードコーディングしています

event['Records'][0]['Sns']['Message']で返される型が文字列型で要素を取り出せなかったため、json.loadsを間で実施しています。

実装完了後、保存して完了です。

動かしてみる

実際に動かして正しく動いているかチェックしてみます。

input側S3のs3://cm-sato-tsuyoshi-sns-lambda-input/file/配下にファイル(今回は「test001.txt」)をPUTします。

デバッグを入れているので、CloudWatch Logsでうまく動いているか確認します。
ログを見る限りうまく動いているようです。

output側S3のs3://cm-sato-tsuyoshi-sns-lambda-output/output/配下にファイルが格納されました。

感想

サーバーレス初心者でも簡単にできたなというのが触ってみた感想です。
Amazon SNSのポリシーの設定がはまりどころかなとは思いますが、それ以外は画面の入力欄に従っていけばうまくできました。

ARNで機能間を連携しているのが実際に体験できたため、サーバーレス未経験の方はまずAmazon SNS→AWS Lambdaの連携を試してみていただければと思います。