S3 Event Notificationを別アカウントのAWS Lambdaに送信する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、虎塚です。

Amazon S3では、オブジェクトの生成や削除をトリガーに、SNSSQSAWS LambdaにEvent Notificationを送信できます。送信先のAWSリソースは、S3と別のアカウントでも問題ありません。

今回は、あるAWSアカウントのS3に画像がアップロードされた時に、別のアカウントのAWS Lambdaを起動して画像をリサイズする処理を例に、S3 Event Notificationをクロスアカウントで利用する設定を説明します。

処理の概要

AWS Lambdaの公式ドキュメントには、次のようなチュートリアルがあります。

AWS LambdaのS3チュートリアル概念図

(1) S3バケットに画像がアップロードされると、(2) S3がイベントを検知して (3) あらかじめ登録されたLambda関数を呼び出します。(4) Lambda関数は画像を取得し、(5) リサイズ処理をして、(6) 別のS3バケットにリサイズした画像を保存します。

上の処理に登場するAWSリソースが異なるAWSアカウントに所属していた場合、概念図は次のようになるでしょう。

チュートリアルの処理をクロスアカウントで実行

アカウントAには、オリジナルの画像を保存するS3バケットがあります。アカウントBには、画像処理用のLambdaと、リサイズした画像を保存するS3バケットがあります。権限設定を適切におこなえば、上の図のような構成でも問題なく処理を実行できます。

これができると何が嬉しいでしょうか。たとえば、あなたがログ収集プロダクトのSaaSをAWS上で展開するサービサーだとします。顧客のAWSアカウントでS3にログファイルが出力されたことをトリガーに、サービサー自身のアカウントでAWS Lambdaを起動して、顧客のログにアクセスし、ログを取得、加工、再保存することができます。

以降の説明では、アカウントAを顧客アカウント、アカウントBをサービサーアカウントと呼び分けます。

手順

1. S3バケットの作成

まず、顧客アカウントに、オリジナル画像をアップロードするS3バケット (source2016) を作成します。次に、サービサーアカウントに、リサイズした画像をアップロードするS3バケット (source2016resized) を作成します。

オリジナル画像用のS3バケットの名前は任意でよいですが、リサイズ画像をアップロードするS3バケットは、オリジナル画像用のS3バケット名の直後に「resized」を続けた名前にします。ステップ2で作成するLambdaファンクションのコード内のロジックが、そのように定義しているためです。

2. Lambdaコードの作成

公式チュートリアルにしたがって、Lambdaコードを作成します。今回はPythonを使います。サービサーアカウントにEC2を起動して、その中でLambdaのコードをビルドします。

ビルド用インスタンスの準備

まず、Amazon Linux AMI 2015.09.1 (HVM) のインスタンスを1台起動して、ec2-userユーザでSSHログインします。

Lambdaコードの作成

次に、ec2-userのルートディレクトリ (/home/ec2-user/) で、次のPythonスクリプトを作成します。

import boto3
import os
import sys
import uuid
from PIL import Image

s3_client = boto3.client('s3')

def resize_image(image_path, resized_path):
    with Image.open(image_path) as image:
        image.thumbnail(tuple(x / 2 for x in image.size))
        image.save(resized_path)

def handler(event, context):
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']
        download_path = '/tmp/{}{}'.format(uuid.uuid4(), key)
        upload_path = '/tmp/resized-{}'.format(key)

        s3_client.download_file(bucket, key, download_path)
        resize_image(download_path, upload_path)
        s3_client.upload_file(upload_path, '{}resized'.format(bucket), key)

パッケージのインストールと更新

それから、ビルドに必要なパッケージをインストールまたは更新します。

$ sudo yum install python27-devel python27-pip gcc
$ sudo yum install libjpeg-devel zlib-devel

今回は、次のバージョンのパッケージがインストールされました。

  • python27-devel.x86_64 0:2.7.10-4.120.amzn1
  • python27-pip-6.1.1-1.21.amzn1.noarch
  • gcc.noarch 0:4.8.3-3.20.amzn1
  • libjpeg-turbo-devel.x86_64 0:1.2.90-5.14.amzn1
  • zlib-devel.x86_64 0:1.2.8-7.18.amzn1

virtualenv環境を作成してアクティベートします。

$ virtualenv ~/shrink_venv
$ source ~/shrink_venv/bin/activate

ライブラリをインストールします。

$ pip install Pillow
$ pip install boto3

今回は、次のバージョンのライブラリがインストールされました。

  • Pillow-3.1.0
  • boto3-1.2.3

Lambdaコードのパッケージング

最後に、次のコマンドを実行して、Lambdaコードをライブラリと一緒にzip圧縮します。

$ zip -r ~/lambda_function.zip shrink_image.py
$ cd $VIRTUAL_ENV/lib/python2.7/site-packages
$ zip -r9 ~/lambda_function.zip *
$ cd $VIRTUAL_ENV/lib64/python2.7/site-packages
$ zip -r9 ~/lambda_function.zip *

Lambdaコードをパッケージングしたlambda_function.zipが作成されます。インスタンスからログアウトし、ローカル環境にlambda_function.zipをコピーしておきます。

$ scp -i ~/.ssh/key-pair.pem ec2-user@public-ip-address:lambda_function.zip .

EC2はもう使いませんので、stopしてかまいません。

3. AWS Lambdaの実行ロールの作成

サービサーアカウントでIAM Roleを作成します。このIAM Roleは、ステップ4で作成するLambda関数の実行ロールとして利用します。

AWS Lambdaタイプの実行ロールの作成

ここでの手順は、公式チュートリアルどおりです。

まず、AWS Management ConsoleのIAMダッシュボードを開きます。次に、左メニューの[Roles]→[Create New Role]ボタンを選択し、Role Name (lambda-cross-execution) を入力します。

それから、[Select Role Type]画面で、AWS Service RoleのAWS Lambdaを選択します。最後に、[Attach Policy]画面で、AWSLambdaExecuteポリシーを選択して、IAM Roleを作成します。

4. Lambda関数の作成

ステップ2で作成したLambdaコードをアップロードして、Lambda関数を作成します。その際に、ステップ3で作成した実行ロールを指定します。

次のコマンドを実行して、サービサーアカウント (AWSアカウントIDは000000000000とします) にLambda関数を作成します。

$ aws lambda create-function \
--region ap-northeast-1 \
--function-name CreateThumbnail \
--zip-file fileb://lambda_function.zip \
--role arn:aws:iam::00000000000:role/lambda-cross-execution \
--handler shrink_image.handler \
--runtime python2.7 \
--timeout 10 \
--memory-size 1024

作成したLambda関数のARNが戻り値に表示されるので、控えておきます。この値はステップ6で使用します。

5. Lambdaのアクセスポリシー設定

サービサーアカウントのLambda関数のアクセスポリシーに、顧客アカウント (999999999999) のS3バケット (source2016) から呼び出すことを許可するアクセス権限を追加します。

$ aws lambda add-permission \
--region ap-northeast-1 \
--function-name CreateThumbnail \
--statement-id sts20160127s3cross \
--principal s3.amazonaws.com \
--action lambda:InvokeFunction \
--source-arn arn:aws:s3:::source2016 \
--source-account 999999999999

公式チュートリアルのように同じAWSアカウントのS3からLambdaを呼び出す場合、source-accountオプションの値はサービサーアカウントのID (000000000000) になります。しかし、ここでは顧客アカウントのS3バケットからの呼び出しを許可するため、999999999999を設定します。

6. イベント通知の有効化

顧客アカウントにあるオリジナル画像用のS3バケット (source2016) で、イベント通知を有効化します。イベントの送信先はサービサーアカウント (000000000000) のLambdaです。ステップ4で控えておいたLambda関数のARNを指定します。

今回は、設定をJSONファイルで与えました。なお、ステップ5までに権限を適切に設定していれば、この操作をAWS Management Console上で実行することもできます。

aws s3api put-bucket-notification-configuration \
--cli-input-json file://notification_config
{
    "Bucket": "source2016",
    "NotificationConfiguration": {
        "LambdaFunctionConfigurations": [
            {
                "Id": "20160127",
                "LambdaFunctionArn": "arn:aws:lambda:ap-northeast-1:000000000000:function:CreateThumbnail",
                "Events": [
                    "s3:ObjectCreated:*"
                ]
            }
        ]
    }
}

これで、オリジナル画像用のS3バケットにファイルがアップロードされると、サービサーアカウントのLambdaに通知が送信され、Lambdaファンクションが呼び出されるようになります。

7. S3バケットポリシーの付与

サービサーアカウント (000000000000) のLambdaから、顧客アカウントのS3バケット (source2016) にアクセスできるように、バケットポリシーを設定します。この権限は、Lambda関数がリサイズ対象のファイルを取得するために必要です。

AWS Management ConsoleでS3バケットを選択して、[Permissions]を開き、[Add bucket policy]ボタンをクリックします。[Bucket Policy Editor]で次のポリシーを設定します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "GetObject",
      "Action": [
        "s3:GetObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::source2016/*",
      "Principal": {
        "AWS": "000000000000"
      }
    },
    {
      "Sid": "ListBucket",
      "Action": [
        "s3:GetBucketLocation",
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::source2016",
      "Principal": {
        "AWS": "000000000000"
      }
    }
  ]
}

これは、S3バケットを他のアカウントに公開するときの一般的な設定です。

以上で完了です。

顧客アカウントのS3バケットに画像をアップロードすると、サービサーアカウントのLambdaが起動して画像をリサイズし、サービサーアカウントのS3バケットにアップロードします。

おわりに

今回紹介した手順のポイントは、次の3つです。

  1. Lambda関数の実行ロールで、AWS Lambdaサービスからの利用を許可します (ステップ3)
  2. Lambdaのアクセスポリシーで、別アカウントのS3からアクセスするための権限を付与します (ステップ5)
  3. 他のアカウントにS3バケットを公開するために、バケットポリシーを設定します (ステップ7)

公式ドキュメントに記述された例を組み合わせることで、S3 Event Notificationを別アカウントのLambdaに飛ばすことができました。

便利な一方で、AWSアカウントのセキュリティを維持するためには、次のことに気をつける必要がありそうですね。

  • 意図しないAssumeRoleの設定がIAM Roleに存在しないこと
  • 意図しないバケットポリシーがS3バケットに存在しないこと

IAM、S3、AWS Lambdaのアクセス制御の仕組みを理解して、安全かつ便利にAWSを利用しましょう。

それでは、また。