Amazon S3のCross-Region Replicationを使ってAWS Lambdaを発火させる

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

こんにちは、せーのです。今日は最近出た新機能を使ったちょっとした実験をご紹介します。

きっかけは黒帯

つい1ヶ月ほど前にS3に「Cross-Region Replication」という機能が追加されました。これはS3のオブジェクトを別のリージョンにレプリケーションさせる、という主にDR(災害時の復旧手段)の目的で使うことになるであろう便利な機能です。Cross-Region Replicationについて詳しくはこちらの大栗の記事を御覧ください。

この機能が発表されてからちょっとしたアイデアが頭に浮かんでいまして、日常業務にかまけてそのままタスクの隅に追いやられていたのですが、昨日「AWS Black Belt Tech Webinar」というAWSサービスの使い方やTipsなどをレクチャーしてくれるWebストリーミングを見ていまして、ちょうどテーマが「S3」だったので思い出して質問してみました。

S3のクロスリージョンレプリケーションでレプリケーション先のリージョンがLambdaが使えるリージョンの場合、レプリケーションでLambdaは発火しますか?

LambdaはGA(Generally Available : 一般公開)になり、弊社でもちょこちょこ実案件に使い出しているのですが、その時に一番ネックになるのは「lambdaは東京リージョンに適応していない」というところなんですね。例えばS3のバケットにファイルがアップロードされたことをトリガーにLambdaを走らせて何らかの処理をする、という場合前提条件として「S3のバケットはUSスタンダードかOregonリージョンに作る」となるわけです。Lambdaを使いたい為だけに他のサービスは東京リージョンで統一しているのにS3バケットだけOregonリージョン、という気持ち悪い構成を甘受しなければいけませんでした。
もしレプリケーションでLambdaが発火するのであれば、管理は東京リージョンのバケットでイケるのではないか、というのがアイデアだったんです。
私の質問に対してAWSの中の人の答えは

えー、こちら、発火します。面白そうなのでぜひやってみてください。

とのこと。これはやってみるしかないでしょう

やってみた

ということでやってみようと思います。今回は東京リージョンとUSスタンダードリージョンのそれぞれのバケットをCross-Region Replicationで結んで、東京リージョンのバケットにオブジェクトを入れ、USスタンダードリージョンのバケットにトリガーをつけてLambdaを発火、以前の記事で書いたTTLの変更処理をやってみようと思います。

s3tolambda1

まずはS3にバケットを2つ作成します。一つは東京リージョン、もうひとつはUSスタンダードリージョンに立てます。わかりやすいように名前を「chao2suketest-tokyo」「chao2suketest-us」とします。

s3tolambda2

次にchao2suketest-tokyoを選択し、「クロスリージョン レプリケーション」を選択します。クロスリージョンレプリケーションを使うにはバージョニングが有効になっている必要があるので、バージョニングを有効にします。

s3tolambda3

バージョニングを有効にしたらクロスリージョンレプリケーションの設定に入ります。送信先にUSスタンダードリージョンのバケットを選択します。S3から操作できるようにIAMロールを作成します。

s3tolambda4

IAMロールは東京リージョンのget系とlist系、USスタンダードリージョンのレプリケーションに対して許可を与えています。デフォルトでこのポリシーが設定されているので、何も考えずに[許可]ボタンを押します。

s3tolambda5

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "s3:Get*",
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::chao2suketest-tokyo",
        "arn:aws:s3:::chao2suketest-tokyo/*"
      ]
    },
    {
      "Action": [
        "s3:ReplicateObject",
        "s3:ReplicateDelete"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::chao2suketest-us/*"
    }
  ]
}

ここまで設定して[保存]ボタンを押すとこんなエラーメッセージが出ます。

s3tolambda6

このエラーはクロスリージョンレプリケーションではあるあるになるくらい有名なエラーですね。レプリケーション先のバージョニングが有効化されていない時に出るエラーです。USスタンダードリージョンの方のバケット[chao2suketest-us]のバージョニングを有効にしてもう一度保存を押すと、きちんと設定されます。

まずレプリケーションがきちんと効いているかどうか確認してみましょう。東京リージョンにテスト用のファイルを一つアップロードします。

s3tolambda7

[chao2suketest-us]を見てみると、同じファイルがありました。レプリケーションはきちんと働いているようですね。

s3tolambda8

次にこの[chao2suketest-us]に対してLambdaを書いてつなげてみたいと思います。新規でLambda functionを作成、名前は[cmS3toLambdaTest]としました。以前書いたLambdaの記事を参考にこのようなLambdaを書いてみました。

s3tolambda9

console.log('Loading event');
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var s3tokyo = new aws.S3({apiVersion: '2006-03-01', endpoint: 'https://s3-ap-northeast-1.amazonaws.com'});

exports.handler = function(event, context) {
   console.log('Received event:');
   console.log(JSON.stringify(event, null, '  '));
   // Get the object from the event and show its content type
   var bucket = event.Records[0].s3.bucket.name;
   var bucket2 = "chao2suketest-tokyo";
   var key = event.Records[0].s3.object.key;
   s3.getObject({Bucket:bucket, Key:key},
      function(err,data) {
        if (err) {
           console.log('error getting object ' + key + ' from bucket ' + bucket + 
               '. Make sure they exist and your bucket is in the same region as this function.');
           context.done('error','error getting file'+err);
        }
        else {
            console.log('logging Cache-Control : ',data.CacheControl);
            if  (typeof data.CacheControl != 'undefined'){
                console.log('cache control was already exists.');
                
                context.done(null,'skip execution.');
            }else {
               var ttl = 10;
               
               var params = {
                  Bucket: bucket, /* required */
                  CopySource: bucket + "/" + key,
                  Key: key, /* required */
                  CacheControl: "max-age=" + ttl,
                  MetadataDirective: "REPLACE",
                  ContentType: data.ContentType
                };
                console.log('replace object.');
                console.log('bucket : ' + bucket);
                console.log('CopySource : ' + bucket + "/" + key);
                
               s3tokyo.copyObject(params, function(err2, data2){
                   if (err2){
                       context.done('error','error2 getting file '+err2);
                   }else{
                       console.log('replace done! Cache-Control : ',data2.CacheControl);
                       Context.done(null,'object meta-data changed.');
                   }
               });               
            }
        }
      }
   );
};

バケットにオブジェクトが入ったらcache-controlとしてmax-age=10を設定する、というものです。書き終わったらレプリケーション先のバケットとつなげます。トリガーとなるイベントタイプはレプリケーションにてオブジェクトが登録された時、ということで「PUT」を選択します。

s3tolambda10

これでLambdaの設定も完了です。設定し終わるとこんな感じになります。

s3tolambda11

では早速実験してみましょう。東京リージョンの方に新しいファイルをアップロードしてみます。

s3tolambda12

USスタンダードリージョンのバケットを覗いてみると、、、max-age=10が登録されています!!

s3tolambda13

LambdaのログはCloudWatch Logsに吐かれますのでそちらを確認してみます。確かにLambdaが走っていますね。

s3tolambda14

応用編

ここからはもう少しLambdaを触ってみましょう。今のこの設定ですとUSスタンダードリージョンにはCache-Controlがつくものの、元の東京リージョンの方にはCache-Controlはつきません。あくまでレプリケーションしたファイルに対して操作しているので当然と言えば当然ですね。

s3tolambda19

でもここで思い出してみると、元々レプリケーションでLambdaが働くかどうか興味をもったのは「Lambdaを動かすためだけにS3のバケットをUSリージョンにするのが気持ち悪い」という動機だったはずです。
となるとそもそも東京リージョンに対して処理ができてなくてはいけないのではないでしょうか。またUSリージョンに入っているファイルはそもそも要らないんじゃないでしょうか。

ということでLambdaをこのように書き換えてみました。

console.log('Loading event');
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var s3tokyo = new aws.S3({apiVersion: '2006-03-01', endpoint: 'https://s3-ap-northeast-1.amazonaws.com'});

exports.handler = function(event, context) {
   console.log('Received event:');
   console.log(JSON.stringify(event, null, '  '));
   // Get the object from the event and show its content type
   var bucket = event.Records[0].s3.bucket.name;
   var bucket2 = "chao2suketest-tokyo";
   var key = event.Records[0].s3.object.key;
   s3.getObject({Bucket:bucket, Key:key},
      function(err,data) {
        if (err) {
           console.log('error getting object ' + key + ' from bucket ' + bucket + 
               '. Make sure they exist and your bucket is in the same region as this function.');
           context.done('error','error getting file'+err);
        }
        else {
            console.log('logging Cache-Control : ',data.CacheControl);
            if  (typeof data.CacheControl != 'undefined'){
                console.log('cache control was already exists.');
                
                context.done(null,'skip execution.');
            }else {
               var ttl = 10;
               
               var params = {
                  //Bucket: bucket, /* required */
                  Bucket: bucket2,
                  //CopySource: bucket + "/" + key,
                  CopySource: bucket2 + "/" + key,
                  Key: key, /* required */
                  CacheControl: "max-age=" + ttl,
                  MetadataDirective: "REPLACE",
                  ContentType: data.ContentType
                };
                console.log('replace object.');
                console.log('bucket : ' + bucket2);
                console.log('CopySource : ' + bucket2 + "/" + key);
                
               s3tokyo.copyObject(params, function(err2, data2){
                   if (err2){
                       context.done('error','error2 getting file '+err2);
                   }else{
                       console.log('replace done! Cache-Control : ',data2.CacheControl);
                       var param2 = {
                          Bucket: bucket,
                          Key: key
                       };
               
                       console.log('delete target object.');
                       s3.deleteObject(param2, function(err3, data3){
                           if (err3){
                               context.done('error','error3 getting file '+err3.stack);
                           }else{
                               console.log('delete done!',null);
                               context.done(null,'object meta-data changed.');
                           }
                           
                       });
                   }
               });
            }
        }
      }
   );
};

Cache-controlをつけるのは東京リージョンのオブジェクトにして、Cache-ControlをつけたらUSリージョン側のオブジェクトを削除する、という実装にしてみました。 ちなみにこの場合はLambdaのRoleにはDeleteObject権限も必要になるので注意してください。あ、cross-region replicationのロールではなく、Lambdaのロールです。勘違いのないように。Lambdaのロールはこのようなポリシーに変えています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:*"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::*"
            ]
        }
    ]
}

では実験してみましょう。まずはUSリージョン側のオブジェクトを全て削除します。

s3tolambda21

次に東京リージョン側に新しいファイルをアップロードします。

s3tolambda20

USリージョン側を確認してみると、ファイルが何もありません。

s3tolambda23

東京リージョン側を確認してみると、先程アップロードしたファイルにcache-controlがついています。成功です!

s3tolambda22

CloudWatch Logsを確認してみると、綺麗にLambdaが走っていることがわかります。

s3tolambda24

まとめ

いかがでしょうか。実はこれで東京リージョンのコストだけでLambdaが使えるな、、、と思っていたのですが、よく考えるとバージョニングしているのですから削除してもコストはかかります。というか実案件でこんな回りくどい方法を使うのなら素直にUSリージョン使うだろ、という気になってきましたが、まあそこは実験、ということで。色々機能を組み合わせると何か別のものが見えてくるかも、しれませんしね。