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

2019.07.16

データアナリティクス事業本部@札幌の佐藤です。 私は今まで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の連携を試してみていただければと思います。