Amazon S3 Object Lambda によるデータのマスキング処理を AWS CDK で構築してみた

2023.11.06

こんにちは、CX 事業本部 Delivery 部の若槻です。

Amazon S3 Object Lambda を使用すると、S3 からオブジェクトを取得する際に、Lambda 関数を介してオブジェクト変換を行うことができます。

今回は、Amazon S3 Object Lambda によるデータのマスキング処理をする仕組みを AWS CDK で構築してみました。

試してみた

Lambda 関数(サンプルコード)の作成

まずは、下記のドキュメントで紹介されているサンプルコードを試してみます。マスキング処理の Lambda は記事後半で別途実装します。

サンプルコードを参考に、指定のヘッダーがない場合は、HTTP ステータスコード 403 を指定のエラーコードおよびメッセージで応答する Lambda 関数を作成します。

lib/cdk-sample-stack.Handler.ts

import { S3Client, WriteGetObjectResponseCommand } from '@aws-sdk/client-s3';
import axios from 'axios';

const s3Client = new S3Client({
  region: 'ap-northeast-1',
  apiVersion: '2006-03-01',
});

export const handler = async (event: any): Promise<{ statusCode: 200 }> => {
  const { userRequest, getObjectContext } = event;
  const { outputRoute, outputToken, inputS3Url } = getObjectContext;

  const isTokenPresent = Object.keys(userRequest.headers).includes(
    'SuperSecretToken'
  );

  if (!isTokenPresent) {
    await s3Client.send(
      new WriteGetObjectResponseCommand({
        RequestRoute: outputRoute,
        RequestToken: outputToken,
        StatusCode: 403,
        ErrorCode: 'NoSuperSecretTokenFound',
        ErrorMessage: 'The request was not secret enough.',
      })
    );
  } else {
    const presignedResponse = await axios.get(inputS3Url);
    await s3Client.send(
      new WriteGetObjectResponseCommand({
        RequestRoute: outputRoute,
        RequestToken: outputToken,
        Body: presignedResponse.data,
      })
    );
  }

  return { statusCode: 200 };
};

CDK コード

@aws-cdk/aws-s3objectlambda-alpha というアルファ版の CDK L2 コンストラクトを使って、S3 Object Lambda を構築します。

次のように定義するだけで、前述の Lambda 関数を使用した S3 Object Lambda を構築できます。

lib/cdk-sample-stack.ts

import {
  aws_lambda_nodejs,
  aws_lambda,
  aws_s3,
  Stack,
  StackProps,
  RemovalPolicy,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3objectlambda from '@aws-cdk/aws-s3objectlambda-alpha';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const bucket = new aws_s3.Bucket(this, 'Bucket', {
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    const handler = new aws_lambda_nodejs.NodejsFunction(this, 'Handler', {
      runtime: aws_lambda.Runtime.NODEJS_LATEST,
      architecture: aws_lambda.Architecture.ARM_64,
    });

    new s3objectlambda.AccessPoint(this, 'MyObjectLambda', {
      bucket,
      handler,
      accessPointName: 'my-access-point',
    });
  }
}

動作確認

サンプルデータを用意します。

data1

{"userId":"u001","mail":"tanaka.taro@example.com"}
{"userId":"u002","mail":"suzuki.ichiro@example.com"}
{"userId":"u003","mail":"wakatsuki.ryuta@example.com"}

上記のサンプルデータを S3 バケットにアップロードします。

aws s3 cp data1 s3://${BucketName}/data1

Amazon S3 のマネジメントコンソールで、Object Lambda Access Points より今回作成した S3 Object Lambda アクセスポイントを選択します。

アップロードされているオブジェクトを選択し、Open から開きます。

するとオブジェクトを開くことができず、Lambda 関数内で定義したエラーコードおよびメッセージが表示されました。

S3 Object Lambda が動作していることを確認できました。

データのマスキング処理

続いてデータのマスキング処理です。今回は、S3 バケットから取得したオブジェクトの JSON Line のメールアドレスのユーザー名部分をマスキングする処理を実装します。

lib/cdk-sample-stack.Handler.ts

import { S3Client, WriteGetObjectResponseCommand } from '@aws-sdk/client-s3';
import axios from 'axios';

const s3Client = new S3Client({
  region: 'ap-northeast-1',
  apiVersion: '2006-03-01',
});

export const handler = async (event: any): Promise<{ statusCode: 200 }> => {
  const { getObjectContext } = event;
  const { outputRoute, outputToken, inputS3Url } = getObjectContext;

  const presignedResponse = await axios.get(inputS3Url);
  const data = presignedResponse.data;

  await s3Client.send(
    new WriteGetObjectResponseCommand({
      RequestRoute: outputRoute,
      RequestToken: outputToken,
      Body: maskEmailsInJsonLines(data),
    })
  );

  return { statusCode: 200 };
};

const maskEmailsInJsonLines = (data: any) => {
  let lines = data.split('\n');

  for (let i = 0; i < lines.length; i++) {
    if (lines[i].trim() === '') continue; // 空行をスキップ

    let obj = JSON.parse(lines[i]);

    let emailParts = obj.mail.split('@');
    obj.mail = '*****@' + emailParts[1];

    lines[i] = JSON.stringify(obj);
  }

  return lines.join('\n');
};

Object Lambda Access Points からオブジェクトを開くと、メールアドレスのユーザー名部分がマスキングされていることが確認できました!

{"userId":"u001","mail":"*****@example.com"}
{"userId":"u002","mail":"*****@example.com"}
{"userId":"u003","mail":"*****@example.com"}

おわりに

Amazon S3 Object Lambda によるデータのマスキング処理をする仕組みを AWS CDK で構築してみました。

アクセスポイントからオブジェクトを Get するたびに Lambda 関数が実行されることになるのでコスト面やパフォーマンスは気になりますが、マスキングなどの柔軟なデータの変換処理が行えるのは便利ですね。

以上