Amazon Web Services ブログ

Amazon S3とAWS Lambdaを使って差分チェッカーを構築してみよう

この投稿は、AWSソリューションアーキテクトであるJames Beswickによって書かれました。

異なるバージョンのファイルやオブジェクトを保存する際、自動的にバージョン間の差分を検知して記録する仕組みがあれば有用かもしれません。一般的に言うと差分チェッカーツールというのは、設定変更によって起こるJSONファイルの変更や、ユーザーによって作られたドキュメントの変更を、記録したりすることができます。

このブログ記事ではAmazon S3AWS Lambdaを使ってスケーラブルな差分チェッカーサービスをビルド・デプロイする方法を示します。今回のアプリケーション例ではAWS Serverless Application Model(AWS SAM)を使用しているため、あなたの保有しているAWSアカウント上に簡単にデプロイすることができます。

このガイドでは AWS無料利用枠でカバーできるようなリソース作成の構成となっていますが、無料利用枠を超える利用を許可している場合、料金が発生する場合があります。このアプリケーション例のセットアップは、こちらのGitHubリポジトリREADME.mdのインストラクションを参考に構築を行ってください。

概要

S3のデフォルトの設定では、すでに存在しているオブジェクトと同じ名前でオブジェクトをアップロードすると、新しいオブジェクトが既存のものを上書きする、という挙動をします。しかし、S3バケットでのバージョニングを有効化すると、S3サービスはオブジェクトの全てのバージョンを蓄積するようになります。バージョニングを使用すると、ミスや事故による削除や上書きが発生した際に、効率的にオブジェクトを復元することができるようになります。最新のバージョンと前のバージョンの比較も行えるようになるため、オブジェクトの変更を検知できるようにもなります。

今回のアプリケーション例では、新しいオブジェクトのバージョンがアップロードされるたびに、S3バケットがLambda関数をトリガーします。トリガーされたLambda関数は、最新バージョンと一つ前のバージョンを比較し、Amazon CloudWatch Logsにその差分を書き込みます。

追加機能として、このアプリケーションは設定可能な環境変数を用意しており、過去の何バージョンまでオブジェクトを保持するかの設定をすることができます。デフォルトでは最新の3バージョンを保持するようになっています。Lambda関数が設定されているものよりも古いバージョンを消去するため、オブジェクトライフサイクルの実装を効率化することができます。

以下にオブジェクトの複数のバージョンがアップロードされた際のアプリケーションのフローを示します:

Application flow

  1. v1がアップロードされた時は、比較対象である前のバージョンがありませんので何も行いません。
  2. v2がアップロードされた時、Lambda関数はv1と差分比較し、ログに残します。
  3. v3がアップロードされた時、Lambda関数はv2と差分比較し、ログに残します。
  4. v4がアップロードされた時、Lambda関数はv3と差分比較し、ログに残します。その後、設定で指定されているバージョンよりも古いバージョンであるv1を消去します。

デプロイの方法

(訳者註:デプロイの仕方は元ブログにはありませんがGitHubリポジトリREADME.mdに英語で書かれていますのでこちらに翻訳して記載します。)

事前準備として以下のものが必要です。

  • AWSアカウントをお持ちでなくログインしていない場合、作成する必要があります。使用するIAMユーザは、この記事で使用するAWSサービスのAPIコールやAWSリソースを管理するのに十分なパーミッションを持っている必要があります。
  • AWS CLIがローカルにインストール・設定済み
  • Gitのインストール
  • AWS Serverless Application Model(AWS SAM)のインストール

まずローカルにターミナルから新しくディレクトリを作ってそのディレクトリに移動し、GitHubリポジトリをクローンします:

git clone https://github.com/aws-samples/s3-diff-checker

コマンドラインからAWS SAMを使ってtemplate.ymlファイルで指定されたパターンの通りにAWSリソースをビルド・デプロイします(訳者註:事前にsam buildでビルドを行う必要があります。オプションによってローカルに用意されているnpmを使用したり、ビルド用のコンテナをローカルに一時的に立てたりすることもできます。必要な準備やオプションについてはこちらをご覧ください。):

sam deploy --guided

プロンプト上では以下の入力の必要があります:

  • ユニークなS3バケット名
  • 稼働させたいAWSリージョン
  • SAM CLIに必要なパーミッションを備えたIAMロールを新たに作成する許可

sam deploy --guidedを一度実行して設定ファイル(samconfig.toml)に引数を保存したら、次回以降の実行にはsam deployのコマンドのみでデフォルトとして保存した引数を使用することができます。

AWS SAMテンプレートを理解する

このアプリケーションのAWS SAMテンプレート では、バージョニングを有効化したバケットをVersioningConfiguration属性を使用して設定しています:

  SourceBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      VersioningConfiguration:
        Status: Enabled      

また、Lambda関数を定義する際、環境変数のKEEP_VERSIONSを参照して何世代のバージョンを保持するかを決めています:

  S3ProcessorFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: src/
      Handler: app.handler
      Runtime: nodejs14.x
      MemorySize: 128
      Environment:
        Variables:
          KEEP_VERSIONS: 3

このテンプレートはAWS SAMポリシーテンプレートを使用して、バケット内のオブジェクトに対してS3ReadPolicyを持つLambda関数を構築しています。バージョン取り扱うロジックは、対象のバケットに対してs3:ListBucketVersionsの許可と、バケット内のオブジェクトに対してs3:DeleteObjectVersionの許可が必要です。それぞれの許可がバケットに対して適用されるのかバケット内のオブジェクトに適用されるのか意識しておくことが重要です。今回のテンプレートでは、構築するLambda関数のポリシーとして三つのパーミッションタイプを定義しています:

      Policies:
        - S3ReadPolicy:
            BucketName: !Ref BucketName      
        - Statement:
          - Sid: VersionsPermission
            Effect: Allow
            Action:
            - s3:ListBucketVersions
            Resource: !Sub "arn:${AWS::Partition}:s3:::${BucketName}" 
        - Statement:
          - Sid: DeletePermission
            Effect: Allow
            Action:
            - s3:DeleteObject
            - s3:DeleteObjectVersion
            Resource: !Sub "arn:${AWS::Partition}:s3:::${BucketName}/*" 

今回のアプリケーション例はテキストファイルでしか使用できませんが、同じロジックを使って他の種類のファイルを処理することもできます。今回はイベントの定義内で、‘.txt’で終わるオブジェクトのみがLambda関数を実行することを保証しています:

      Events:
        FileUpload:
          Type: S3
          Properties:
            Bucket: !Ref SourceBucket
            Events: s3:ObjectCreated:*
            Filter: 
              S3Key:
                Rules:
                  - Name: suffix
                    Value: '.txt'     

S3バケットからイベント処理を行う

S3はオブジェクトが作成された際にLambda関数にイベントを送信します。このイベントは、オブジェクトの中身ではなくオブジェクトのメタデータを含んだものとなります。Lambdaハンドラから関数のビジネスロジックを分離するのは良いプラクティスです。それに倣って、Lambda関数の最初の呼び出し先であるapp.js内のハンドラについても、 内部でイベント内のレコードに対し繰り返し処理を行い、その繰り返しの中でそれぞれのレコードに対してカスタムロジックを呼び出すようにして分離を行います。

const { processS3 } = require('./processS3')

exports.handler = async (event) => {
  console.log (JSON.stringify(event, null, 2))

  await Promise.all(
    event.Records.map(async (record) => {
      try {
        await processS3(record)
      } catch (err) {
        console.error(err)
      }
    })
  )
}

processS3.jsファイルには、バケット内に格納されているオブジェクトのバージョンを取得し、受信したイベントデータを並べ替える関数が含まれています。S3 APIのlistObjectVersionsメソッドにはs3:ListBucketVersionsの許可が必要ですが、これはあらかじめAWS SAMテンプレートで定義されています:

    // Decode URL-encoded key
    const Key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "))

    // Get the list of object versions
    const data = await s3.listObjectVersions({
      Bucket: record.s3.bucket.name,
      Prefix: Key
    }).promise()

   // Sort versions by date (ascending by LastModified)
    const versions = data.Versions
    const sortedVersions = versions.sort((a,b) => new Date(a.LastModified) - new Date(b.LastModified))

最後に、ccompareS3.jsファイルではS3オブジェクトの最新の2つのバージョンをロードし、npmのdiffライブラリを使用して比較を行う関数が定義されています:

const compareS3 = async (oldVersion, newVersion) => {
  try {
    console.log ({oldVersion, newVersion})

    // Get original text from objects 
    const oldObject = await s3.getObject({ Bucket: oldVersion.BucketName, Key: oldVersion.Key }).promise()
    const newObject = await s3.getObject({ Bucket: newVersion.BucketName, Key: newVersion.Key }).promise()

    // Convert buffers to strings
    const oldFile = oldObject.Body.toString()
    const newFile = newObject.Body.toString()

    // Use diff library to compare files (https://www.npmjs.com/package/diff)
    return Diff.diffWords(oldFile, newFile)

  } catch (err) {
    console.error('compareS3: ', err)
  }
}

S3オブジェクトの古いバージョンのライフサイクル管理

S3のライフサイクル管理の中でオブジェクトの移行アクションを設定することによって、自動的にライフサイクルのルールを適用することもできます。このアプローチでは、生存期間によってオブジェクトの有効期限を定め、S3サービスが非同期に消去処理を行うこととなります。ルール化されたライフサイクルはS3によって完全に管理されるため、カスタムコードは一切必要ありません。今回の例では異なるアプローチを取っていて、生存期間ではなく、保持しているバージョンの数をもとにオブジェクトを消去するというコードを実装しています。

バージョニングがバケットに対して有効化されている場合、オブジェクトには作成時にVersionId属性が追加されます。この識別子は、順序性を持つものではなくランダムな文字列として提供されます。オブジェクトのバージョンのリストを取得するとLastModified属性が一緒に返却されてきて、バージョンの順番を決める際にこの属性を使用することができます。また、レスポンスで返ってくる配列の長さは、オブジェクトに対して有効なバージョンの数として使用することができます:

[
  {
    Key: 'test.txt',
    VersionId: 'IX_tyuQrgKpMFfq5YmLOlrtaleRBQRE',
    IsLatest: false,
    LastModified: 2021-08-01T18:48:50.000Z,
  },
  {
    Key: 'test.txt',
    VersionId: 'XNpxNgUYhcZDcI9Q9gXCO9_VRLlx1i.',
    IsLatest: false,
    LastModified: 2021-08-01T18:52:58.000Z,
  },
  {
    Key: 'test.txt',
    VersionId: 'RBk2BUIKcYYt4hNA5hrTVdNit.MDNMZ',
    IsLatest: true,
    LastModified: 2021-08-01T18:53:26.000Z,
  }
]

簡便のため、このコードでは順序を表すバージョン数の属性を追加します。この属性は日付で配列をソートすることによって決めることができます。deleteS3 functionは、S3 APIのdeleteObjectsメソッドを使って複数のオブジェクトを1アクションで消去しています。順序を持つバージョンID属性を使って、削除対象のバージョンのキーをリストとして持つParamという属性を作り、どのバージョンを消去するかのフラグとして利用します:

const deleteS3 = async (versions) => {

  const params = {
    Bucket: versions[0].BucketName, 
    Delete: {
     Objects: [ ]
    }
  }

  try {
    // Add keys/versions from objects that are process.env.KEEP_VERSIONS behind
    versions.map((version) => {
      if ((versions.length - version.VersionNumber) >= process.env.KEEP_VERSIONS ) {
        console.log(`Delete version ${version.VersionNumber}: versionId = ${version.VersionId}`)
        params.Delete.Objects.push({ 
          Key: version.Key,
          VersionId: version.VersionId
        })
      }
    })

    // Delete versions
    const result = await s3.deleteObjects(params).promise()
    console.log('Delete object result: ', result)

  } catch (err) {
    console.error('deleteS3: ', err)
  }
}

アプリケーションのテスト

このアプリケーションをテストするには、AWSマネジメントコンソールかAWS CLIを使ってサンプルテキストをアップロードする必要があります:

aws s3 cp sample.txt s3://myS3bucketname

テストファイルを変更し、同じコマンドでアップロードをもう一度行ってください。この操作でバケットに2つ目のバージョンを作成することができます。このプロセスを複数回繰り返し、オブジェクトの複数のバージョンを作成してください。バージョン間の差分や古いバージョンの消去を、Lambda関数のログで確認することができます:

Log activity

また、test.js関数を用いてローカルでオブジェクトのテストを行い、テストイベントを供給することができます。ローカルでのデバッグやテストに有効に使っていただけます。

結論

このブログ記事では、S3バケットに格納されたオブジェクトに対して使用できる、スケーラブルな差分チェックツールを紹介してきました。S3のバケットに対してオブジェクトの新しいバージョンが書き込まれると、Lambda関数が呼び出されます。また、今回の例では保持したいバージョン数を定義することによって、古いバージョンを消去する方法も示しました。

今回のアプリケーション例をデプロイできるAWS SAMテンプレートの一連の動きを見ていき、実装で使われたSDK上のS3 APIメソッドのうちの重要なものを紹介してきました。また、VersionIDのS3上での挙動を説明し、LastModifiedの日付属性と組み合わせて順序性を持つバージョニング行う実装の方法を示しました。

S3からLambdaを利用するベストプラクティスをもっと知りたい場合は、Lambda Operator Guideを参照してください。サーバーレス全般についての記事は、Serverless Landをご覧になってみてください。

この記事の翻訳はソリューションアーキテクトの野村 侑志が担当しました。原文はこちらですが、一部デプロイの部分でこちらのソースリポジトリを参照し、翻訳しています。