AWS Lambdaを使って、S3のデータ利用量を制限する方法

はじめに

たまに「S3のデータ量に上限をつけたい」という話を聞きますが、今回はタイトル通り、Lambdaでこれが実現できないかどうか試してみました。

仕組み

S3へデータがアップロードされた時にLambda Functionが呼び出されるので、その呼び出しを使って、Putされたデータ量をDynamoDBに記録していきます。
ある一定以上のデータ量が加算されたところで、LambdaからS3バケットに対してバケットポリシーの変更を行い、以降はPutできなくします。仕組みは以下の通りです。

実装

はじめに、DynamoDBにテーブルを作成します。キーは文字列型のBucketとしておきます。
適当な名前で、S3バケットを作ります。S3バケットとDynamoDBは同じリージョンで作成します。
以下のコードをLambda Functionとして登録して、最後にLambda FunctionとS3を関連づけます。

var aws = require('aws-sdk');

var dynamoTableName = "S3バケット名";
var limitDataSize = 5000;//最大データサイズ

exports.handler = function(event, context) {
  var record = event.Records[0];
  var size = record.s3.object.size;
  var region = record.awsRegion;
  var bucket = record.s3.bucket.name;

  var dynamo = new aws.DynamoDB({
    region : region
  });
  var params = {
    TableName : dynamoTableName,
    Key : {
      bucket : {
        'S' : bucket
      }
    },
    AttributeUpdates : {
      size : {
        Action : 'ADD',
        Value : {
          'N' : size
        }
      }
    },
    ReturnValues : "UPDATED_NEW"
  };
  dynamo.updateItem(params, function(err, data) {
    if (err) {
      context.done(err, "error");
    } else {
      console.log("data updated," + JSON.stringify(data));
      var updatedSize = data.Attributes.size.N;
      checkLimit(context, updatedSize, region, bucket);
    }
  });
};
function checkLimit(context, updatedSize, region, bucket) {
  if (updatedSize < limitDataSize) {
    context.done(null, "size=" + updatedSize + " limit=" + limitDataSize);
    return;
  }
  console.log("limit exceeded size=" + updatedSize + " limit="
      + limitDataSize);
  // change S3 bucket policy
  var s3 = new aws.S3({
    params : {
      region : region
    }
  });
  var policy = {
    "Version" : "2012-10-17",
    "Statement" : [ {
      "Sid" : "ignorePut",
      "Effect" : "Deny",
      "Principal" : {
        "AWS" : "*"
      },
      "NotAction" : [ "s3:Get*", "s3:List*", "s3:Delete*" ],
      "Resource" : [ "arn:aws:s3:::" + bucket + "/*" ]
    } ]
  };
  var param = {
    Bucket : bucket,
    Policy : JSON.stringify(policy)
  };

  s3.putBucketPolicy(param, function(err, data) {
    if (err) {
      context.done(err, "error");
    } else {
      context.done(null, "done");
    }
  });
}

PutされるごとにDynamoDBにデータサイズが加算され、一定以上になったときにS3バケットにpolicyが適用されます。

ほかの使い道

Lambdaからポリシーを変えるというのが今回のキモになっていますので、たとえば指定IPから一定量のアクセスがあったらDenyするとか、CloudTrailと連携して、一定回数マネジメントコンソールにログインに失敗したら、翌日までログインできなくする、というようなこともできると思います(これは次回トライしたいです)

オチ

実は、S3からのPut通知は、新規にオブジェクトを作成したときだけでなく、ファイルを上書きしたり、消したときにも通知され、知る限り見分ける方法がありません。ですので実は今の段階では、保存されているデータ量の制限ではなく、操作したデータの総量での制限になってしまいます。今後S3 notificationがより細かく情報を取れるようになれば、削除や更新時に適切にデータ量を足し引きすることで、保存データ量による制限ができるようになると思います。