AWS Glue Databrewを利用した個人データ自動マスキングのデータパイプラインを検証してみた

2022.04.29

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

どーも、データアナリティクス事業本部 コンサルティングチームのsutoです。

今回は、データセット内の個人情報(PII(Personally identifiable informationの略))を自動検出し、データをマスキングする一連の処理を実行するワークフローがあるので、実際にやってみながら各処理の流れや動作を理解していこうと思います。

最近、Glue DatabrewのジョブをStep Functionsのワークフローで自動実行させる仕組みを検討・作成していたところだったため、最近公開されたAWS Blogのアーキテクチャを実践して理解を深めようと思います。

アーキテクチャ

まずは構成図を各機能の内容と流れについて概要をまとめます。

① データセットをInput用S3バケットにアップロードする

② S3バケットへのファイルアップロードをEvent Bridgeで検知、それをトリガーとしてStep Functionsステートマシンを実行する

③ Databrewデータセットの登録→データプロファイルジョブ実行によってプロファイルデータをOutput用S3バケットへ保存

④  プロファイルデータを元に、PIIを含むと判定される列名を抽出し、結果として返すLambda関数を実行(PII列が”無し”とされた場合、ここでワークフローは終了します)

⑤ 抽出した列データに対して値をマスキングするDatabrewレシピジョブを実行

⑥ マスキング方法は、Secret Managerに格納したテキストプレーン(base64でエンコードされた文字列など)を使ってハッシュしている

⑦ レシピジョブ完了後、結果をOutput用S3バケットへ出力されマスキング後のデータセットとして保存される

上記の内容やただStep Functionsステートマシンを動かしただけでは”ふ〜ん”で終わってしまうので、次項から実際に検証しながらプロセスの詳細を見ていきます。

実際にやってみた

データセットの準備

Mockarooというサイトを利用して、以下のような9列・1000行のデータセットを作成しました。今回はこのデータセットで実施していきます。

これとは別に日本語(漢字やカタカナ表記など)の氏名や住所が入ったデータセットで試したのですが、以下の記事にもあるようにDatabrewのプロファイルジョブによる自動判定ではうまくPII列を認識されなかったため、上記の英語のデータセットで検証することにしました。

パイプラインの作成

こちらのリンクをクリックしてアーキテクチャを自動構築します。

Cloudformationで入力するパラメータは今回は以下のようにします。

HashingSecretValue:Secret Managerに格納するハッシュ値を決めるための文字列。任意の文字列でかまわない。

PIIMatchingThresholdValue:DatabrewプロファイルジョブにてPII列であると判定する閾値。(今回は80%とする)

スタックがCOMPLETEになったら、Input用S3バケットへ移動します。

作成したデータセットをアップロードします。

すると、Event Bridgeでイベントが検知され、Step Functionsステートマシンが実行されます。

ここからステートマシンの定義の内容をコードを見ながら解説していきます。(全部説明すると長いので、単純なアクションを行うステップの部分はコード記載を省略させていただきます)

Create Glue DataBrew DatasetでSDKサービスのAPIコールとしてDatabrewデータセットの作成を行なっています。(参考:AWS SDK のサービスの統合)

次にCreate Glue DataBrew Profile JobStart Glue DataBrew Profile Jobでは、同じくSDKのAPIとしてDatabrewデータプロファイルジョブの作成とジョブの実行を行なっています。

AllowedStatisticsでは、Databrewデータセットの”列の統計”タブで計算する統計構成の指定をしており、EntityTypesでは検出するエンティティタイプを指定しています。(USA_ALLとPERSON_NAMEを指定しているので、要するにDATE以外の全てのエンティティを検出しています)

    "Create Glue DataBrew Profile Job": {
      "Type": "Task",
      "Parameters": {
        "DatasetName.$": "$.Name",
        "Name.$": "States.Format('{}-PII-Detection-Job',$.Name)",
        "OutputLocation": {
          "Bucket": "gluedatabrew-pii-data-output-0252afa0"
        },
        "Configuration": {
          "EntityDetectorConfiguration": {
            "AllowedStatistics": [
              {
                "Statistics": [
                  "AGGREGATED_GROUP",
                  "TOP_VALUES_GROUP",
                  "CONTAINING_NUMERIC_VALUES_GROUP"
                ]
              }
            ],
            "EntityTypes": [
              "USA_ALL",
              "PERSON_NAME"
            ]
          }
        },
        "RoleArn": "arn:aws:iam::<<my acount id>>:role/automate-pii-handling-data-RoleGlueDataBrewPIITask-1S1R8ZV4WH2ZE"
      },
      "Resource": "arn:aws:states:::aws-sdk:databrew:createProfileJob",
      "Next": "Start Glue DataBrew Profile Job"
    },
    "Start Glue DataBrew Profile Job": {
      "Type": "Task",
      "Resource": "arn:aws:states:::databrew:startJobRun.sync",
      "Parameters": {
        "Name.$": "$.Name"
      },
      "Next": "Process Profile Result with Lambda Function",
      "ResultSelector": {
        "DatasetName.$": "$.DatasetName",
        "Outputs.$": "$.Outputs"
      }
    },

今回のプロファイルジョブの結果、PII列と判定された列は5つで、いずれも88%以上となりました。

また、意図していない挙動だったが、”company_name”列がPERSON NAMEとしてPII列に判定されたようです。

※そのほかに別件で電話番号を携帯電話表記(xx+-xxx-xxx-xxxx)のデータセットで検証したのですが、PIIデータとして判定されませんでした。こちらはまだサポートされていないようです。

Process Profile Result with Lambda Functionでは、データプロファイルのjsonからPII列のみを抽出するLambda関数を呼び出しています。

    "Process Profile Result with Lambda Function": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "arn:aws:lambda:ap-northeast-1:<<my acount id>>:function:automate-pii-handling-dat-FunctionGlueDataBrewProf-N1ix5ooVCdFs",
        "Payload.$": "$"
      },
      "Retry": [
        {
          "ErrorEquals": [
            "Lambda.ServiceException",
            "Lambda.AWSLambdaException",
            "Lambda.SdkClientException"
          ],
          "IntervalSeconds": 2,
          "MaxAttempts": 6,
          "BackoffRate": 2
        }
      ],
      "Next": "Validate if the Dataset Contains PII Columns",
      "ResultPath": "$.LambdaTaskResult",
      "ResultSelector": {
        "pii-columns.$": "$.Payload"
      }
    },

Lambda関数は、下記の抜粋のとおりCloudformationで入力した”PIIMatchingThresholdValue”(80%)と各列のPIIデータの割合を比較してPII列を特定していることがわかります。

PIIColumnsList = []
  for item in columnsProfiled:
    if "entityTypes" in item["entity"]:
      if (item["entity"]["rowsCount"]/glueDataBrewProfileResult["sampleSize"]) >= int(os.environ.get("threshold"))/100:
        PIIColumnsList.append(item["name"])

  if PIIColumnsList == []:
    return 'No PII columns found.'
  else:
    return PIIColumnsList

Validate if the Dataset Contains PII ColumnsでPII列の有無によって条件分岐します。無しの場合ワークフローが終了し、有りの場合Create Glue DataBrew PII Data Masking Recipeに進みます。

Lambda関数で返された列のリストを対象に「ハッシュ(CRYPTOGRAPHIC_HASH)」処理を実施するレシピを作成しています。

使用するハッシュ値はSecret Managerから呼び出していることがわかります。

    "Create Glue DataBrew PII Data Masking Recipe": {
      "Type": "Task",
      "Parameters": {
        "Name.$": "States.Format('{}-PII-Masking-Recipe',$.DatasetName)",
        "Steps": [
          {
            "Action": {
              "Operation": "CRYPTOGRAPHIC_HASH",
              "Parameters": {
                "secretId": "arn:aws:secretsmanager:ap-northeast-1:<<my acount id>>:secret:GlueDataBrewPIITaskSecret-NBe7X4",
                "sourceColumns.$": "$.LambdaTaskResult.pii-columns"
              }
            }
          }
        ]
      },
      "Resource": "arn:aws:states:::aws-sdk:databrew:createRecipe",
      "Next": "Create Glue DataBrew Project",
      "ResultPath": "$.Recipe"
    },

あとはCreate Glue DataBrew ProjectでDatabrewプロジェクト作成、Create Glue DataBrew Recipe Jobでレシピジョブの作成、Start Glue DataBrew Recipe Jobでレシピジョブの実行をそれぞれ行っています。

ステートマシン完了後はマスキング後のデータセットが出力されています。(今回のステートマシンの実行時間は約6分でした。)

以上、PIIデータを自動マスキングするデータパイプラインの検証でした。

S3イベント検知→Step Functions内でDatabrewジョブ実行という実にキレイなアーキテクチャだったので、是非とも今後の参考させていただきたいと思います。