別アカウントのS3バケットのイベントに基づいて、Lambdaを起動してS3 Syncしてみた

S3バケットのクロスアカウントレプリケーションができない場合、あるアカウントのS3イベントから別のアカウントのLambda起動してS3 Syncしたいと思ったので、試してみた。
2022.06.16

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

データアナリティクス事業本部の笠原です。

今回は別アカウントのS3バケットのイベントから、こちらのアカウントのLambdaを起動して、S3 Syncを使ってバケット間のオブジェクト同期を試してみました。

3行まとめ

  • Lambdaはs3 syncが使えるようにaws cli v2をlayerで導入する
  • Lambda関数のリソースベースのアクセス権限ポリシーを更新して、S3呼び出しアクセス許可を設定する
  • Lambda関数を呼び出す、S3イベント通知を作成する

構成図

今回はこんな感じの環境にいます。 送信元バケットから、送信先バケットに対して s3 sync コマンドを Lambda関数上で実行します。 送信元バケットと送信先バケットは、別のAWSアカウントになります。 また、Lambda関数は今回送信先バケット側に存在します。

バケット作成

送信元バケット、送信先バケットを事前に作成しておきます。

今回は、レプリケーションではないのでバージョニングは無効のままにしておきます。

  • 同一リージョン (今回はap-northeast-1) で作成
  • バージョニングは無効化
  • パブリックアクセスブロック有効化

Lambda関数の作成

Lambda関数ば送信元バケットと同じAWSリージョンに存在する必要があります。

今回は ap-northeast-1 で作成します。

ランタイムは「Amazon Linux 2でユーザー独自のブートストラップを提供する」にします。

関数ができたら、ファイルを修正します。

  • bootstrap.sample はリネームして bootstrapにします。
  • s3sync.sh を作成します。
  • 忘れずに Deploy します。

s3sync.sh の内容はこんな感じです。

function handler () {
    EVENT_DATA=$1
    aws s3 sync s3://${SOURCE_BUCKET}/ s3://${TARGET_BUCKET}/
}

環境変数 SOURCE_BUCKET 及び TARGET_BUCKET に、送信元バケット名及び送信先バケット名を登録しておきましょう。

また、ランタイム設定にてハンドラを s3sync.handler に変更しておきましょう。

IAMロールにS3アクセスポリシーをアタッチ

作成したLambda関数のIAMロールには、S3アクセスポリシーを忘れずにアタッチしましょう。

今回はAWSマネージドポリシーの AmazonS3FullAccess をIAMロールにアタッチします。

LayerでAWS CLIを導入

今回はAWS CLIをLambda Layerで導入してみます。

DockerコンテナにてAWS CLIを展開し必要なファイルをlayerに持たせておきます。

Dockerfileはこんな感じで用意します。

FROM amazonlinux:latest as awslinux

ARG LAMBDA_LAYER_BASE=/opt
ARG AWS_FOLDER_NAME=awscliv2
ARG AWS_DIR=${LAMBDA_LAYER_BASE}/${AWS_FOLDER_NAME}
ARG BIN_DIR=${LAMBDA_LAYER_BASE}/bin

WORKDIR /root

RUN yum update -y && yum install -y \
    unzip \
    zip \
    wget \
    curl \
    tar \
    gzip

ADD https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip ./awscliv2.zip
RUN unzip awscliv2.zip
RUN ./aws/install -i ${AWS_DIR} -b ${BIN_DIR}

# Reduce the size by removing unnecessary files
RUN find ${AWS_DIR} -type d -name examples -exec rm -r {} + \
    && find ${AWS_DIR} -type f -name aws_completer -delete \
    && find ${BIN_DIR} -type l -name aws_completer -delete

# Confirm the installation
RUN ${BIN_DIR}/aws --version

# Archive
# AWS Lambda Layers are extracted to the /opt directory in the function execution environment
WORKDIR ${LAMBDA_LAYER_BASE}
RUN zip -r --symlinks layer.zip ${AWS_FOLDER_NAME} bin

また、bootstrap もこんな感じで用意します。

#!/bin/sh

set -euo pipefail

# Initialization - load function handler
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

# Processing
while true
do
  HEADERS="$(mktemp)"
  # Get an event. The HTTP request will block until one is received
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")

  # Extract request ID by scraping response headers received above
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # Run the handler function from the script
  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

  # Send the response
  curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "$RESPONSE"
done

build.shdeploy.sh はこんな感じです。

#!/bin/bash

# Exit immediately if a command exits with a non-zero status.
set -euo pipefail

FILE='layer.zip'
TAG='aws-cli-lambda-layer'
rm -rf ${FILE}

docker build -t ${TAG} .
docker cp $(docker run -d ${TAG} false):/opt/layer.zip ${FILE}
zip ${FILE} bootstrap
docker rm -f $(docker run -d ${TAG} false)
#!/bin/bash
Profile=$1
aws lambda publish-layer-version --layer-name awscliv2-layer --zip-file fileb://layer.zip --profile ${Profile}

用意できたら、 build.sh を実行して layer.zipを作成して、 deploy.sh を実行してAWSに layer.zip を登録します。 レイヤー名は awscliv2-layer になります。

登録したら、Lambda関数にレイヤーを追加します。

注意点としては、カスタムランタイムの場合レイヤーの選択肢に出てこなかったので、作成したレイヤーのARNを指定すると追加できます。

S3の呼び出しアクセス許可を設定

作成したLambda関数は、別アカウントのS3イベントにて呼び出してもらう必要があるので、そのための準備を行います。

まずは、Lambda関数にS3の呼び出しアクセス許可を設定します。

「設定」タブの「アクセス権限」メニューから、「リソースベースのポリシー」にある「アクセス権限の追加」をクリックします。この中のポリシーステートメントでアクセス権限を追加していきます。

  • 「AWSのサービス」をクリック
  • サービス: s3
  • ステートメントID: 適宜設定します。今回は policy-statement-1にしました。
  • プリンシパル: デフォルト値のままです。
  • ソースアカウント: 送信元バケットのアカウントID
  • ソースARN: 送信元バケットのARN
    • arn:aws:s3:::<バケット名> の形になっています。
  • アクション: lambda:invokeFunction を選択

Lambda関数を呼び出すS3イベント通知を作成

次に、送信元バケットのイベント通知を設定します。

送信元バケットのプロパティから、「イベント通知」の「イベント通知の作成」をクリックします。

以下のように設定します。

  • イベント名: 適宜設定します。今回は invokeS3sync としました。
  • プレフィックス/サフィックス: 今回は空のままとします。
  • イベントタイプ: 今回は すべてのオブジェクト作成イベント としました。
  • 送信先
    • Lambda関数
    • 「Lambda関数ARNを入力」を選択し、呼び出すLambda関数のARNを設定

この変更を保存すれば、S3イベントに応じてLambda関数が起動します。

送信元バケットのバケットポリシー作成

今回はS3 syncコマンドでバケット間のオブジェクトを同期取得を行います。

そのためには、送信元バケットのポリシーで、Lambda関数のアクセスを許可してあげる必要があります。

送信元バケットのポリシーは以下のように設定しました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "1",
      "Action": [
        "s3:Get*",
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::<送信元バケット>",
        "arn:aws:s3:::<送信元バケット>/*"
      ],
      "Principal": {
        "AWS": [
          "arn:aws:iam::<送信先アカウントID>:role/<Lambda関数実行IAMロール>"
        ]
      }
    }
  ]
}

動作確認

ここまでできたら動作確認します。

送信元バケットにファイルをアップロードし、しばらくすると送信先バケットにファイルが転送されていることが確認できます。

もし、しばらく待っても転送されていないようでしたら、Lambda関数の実行ログを確認してどこかエラーが発生していないか確認してみましょう。

参考