GlueのPIIデータ処理をスクリプトで実施してみる

# はじめに
皆さん、GlueのPIIデータ処理は使っているでしょうか。

そもそもPIIとは、Personally Identifiable Information（PII）の略であり、日本語では個人識別情報と訳されます。分かりやすいところでは、氏名やメールアドレス、マイナンバーなどがこれに該当します。

このような情報は持っていると漏洩リスクや法に触れる可能性があるため、削除やマスキングしたいという要件が良く出てきます。カラムを落とすだけなら処理は楽ですが、一部だけマスキングしたい、そもそもどのカラムにPIIデータが入っているか分からない、という場合は処理が複雑になりがちです。

そんなとき、GlueのPII処理を使うと、楽に実装ができます。

https://docs.aws.amazon.com/ja_jp/glue/latest/dg/detect-PII.html

上記はGlueのVisual Editorでの処理ですが、スクリプトでもこの機能は利用できます。
具体的には、Glueが提供している `detect()` および `classifyColumns()` のAPIを利用します。

https://docs.aws.amazon.com/ja_jp/glue/latest/dg/aws-glue-api-sensitive-data-example.html

それぞれの簡単な説明は以下の通りです。

- `detect()`
    - Visual EditorのPII処理と同じく、各行ごとにPII処理を行う
- `classifyColumns()`
    - 列ごとにチェックを行って、PII情報が入っているかどうかを確認する

今回は、 `detect()` をスクリプトで利用してPII処理を実行してみようと思います。
その前に、PIIデータを見つけたときにどのような処理をしてくれるかをおさらいします。

# GlueのPIIデータ処理方法
2024年10月6日現在では、GlueのPIIデータ処理の方法は大きく4つあります。
処理要件に応じて、どの処理方法にするのかを選択します。スクリプトでは、チェックするルールごとにこの処理の方法をそれぞれ選択できます。

### DETECT
Detectは、元データには手を加えず、PII情報が含まれているかどうかを新しい列に追記します。
PII情報が含まれているかどうかだけを確認したいという場合に使えますね。

### REDACT
REDACTは、PIIデータを指定の文字で置き換えます。
例えば、クレジットカード番号を`*`で置き換えるなどが考えられます。

### PARTICAL_REDACT
PARTICAL_REDACTは、基本的にはREDACTと同じく、文字で置き換える処理です。
ただし、以下のように一部だけをマスキングさせることができます。
- 前後何文字かのみをマスキングしない
    - 例えば、電話番号の下4桁だけをマスキングしないようにするなど
- 正規表現でマスキングするパターンを指定する
    - 例えば、[A-Za-z]と指定して半角のアルファベットだけをマスキングするなど

### SHA256_HASH
SHA256_HASHは、その名の通り、SHA-256でハッシュ化します。
データ分析を行うとき、値自体は使わないけど他の行や別のデータと同じ値かどうかは知りたいというケースがあると思います（他のデータと結合したり、集計したりする場合など）。そういう場合に、ハッシュ化は便利です。

# スクリプトで実行してみる
今回は、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/glue/latest/dg/aws-glue-api-sensitive-data-example.html)をもとに、以下のスクリプトをGlueで実行してみました。

日本の免許証番号、日本の銀行番号、日本のマイナンバー、IPアドレス、米国の電話番号をテストデータとしてスクリプト内で作成して、PII処理を実行しています。

:::note warn
マイナンバーについては、実在する可能性のあるマイナンバーをQiitaに記載するのを避けるため、**以下のスクリプトでは000000000000となっています**。「マイナンバー　テスト」などで調べるとデモ用のマイナンバーを作成できますので、実際にやってみたい場合はそちらで作成して、コピペして上書きしてください。
:::


```python
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job

from awsglue.dynamicframe import DynamicFrame
from awsglueml.transforms import EntityDetector

## @params: [JOB_NAME]
args = getResolvedOptions(sys.argv, ['JOB_NAME'])

sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)

# データを作成
data = [
    ("123456789012", "1234567890", "000000000000", "192.168.1.1", "212-555-0123"),
    ("987654321098", "9876543210", "000000000000", "10.0.0.1", "718-555-4567"),
    ("456789012345", "5678901234", "000000000000", "172.16.0.1", "347-555-8901"),
    ("210987654321", "2109876543", "000000000000", "192.168.0.100", "212-555-2345"),
    ("876543210987", "8765432109", "000000000000", "10.0.0.254", "646-555-6789"),
    ("543210987654", "5432109876", "000000000000", "172.16.255.255", "917-555-0123"),
    ("109876543210", "1098765432", "000000000000", "192.168.1.100", "212-555-4567"),
    ("765432109876", "7654321098", "000000000000", "10.0.0.100", "718-555-8901"),
    ("321098765432", "3210987654", "000000000000", "172.16.0.100", "347-555-2345"),
    ("098765432109", "9876543210", "000000000000", "192.168.0.1", "212-555-6789")
]

# スキーマを定義
schema = "Driving_Licence STRING, Bank_Account STRING, My_Number STRING, IP_Address STRING, Phone_Number STRING"

# データフレームを作成
df = spark.createDataFrame(data, schema)

# データフレームを表示 ★(1)
df.show()

# DynamicFrameに変換
dyf = DynamicFrame.fromDF(df, glueContext)

# PII Detectionの設定
detectionParameters = {
      "JAPAN_DRIVING_LICENSE": [{
        "action": "PARTIAL_REDACT",
        "sourceColumns": ["Driving_Licence"],
        "actionOptions": {
          "matchPattern": "[0-9]",
          "redactChar": "*"
        }
      }],
      "JAPAN_BANK_ACCOUNT": [{
            "action": "DETECT",
            "sourceColumns": ["*"]
      }],
      "JAPAN_MY_NUMBER": [{
        "action": "SHA256_HASH",
        "sourceColumns": ["My_Number"]
      }],
      "IP_ADDRESS": [{
        "action": "REDACT",
        "sourceColumns": ["IP_Address"],
        "actionOptions": {"redactText": "*****"}
      }],
      "PHONE_NUMBER": [{
        "action": "PARTIAL_REDACT",
        "sourceColumns": ["Phone_Number"],
        "actionOptions": {
          "numLeftCharsToExclude": "1",
          "numRightCharsToExclude": "0",
          "redactChar": "*"
        }
      }]
}

# PII Detection実行
entity_detector = EntityDetector()
frameWithDetectedPII = entity_detector.detect(dyf, detectionParameters, "DetectedEntities", "HIGH")

# 結果を表示（DynamicFrame） ★(2)
frameWithDetectedPII.show()

# データフレームに変換
df_pii = frameWithDetectedPII.toDF()

# 結果を表示（データフレーム） ★(3)
df_pii.show()

job.commit()
```

スクリプトの中の★(1)~(3)での出力結果を以下に記載します。

### データフレームを表示 ★(1)
テストデータを読み込んだ直後の `show()` の実行結果です。テストデータがそのまま出ています。
マイナンバーは、マイナンバーのテストデータを実際に入れているので、私の方ですべて000000000000に置き換えています。

```
+---------------+------------+------------+--------------+------------+
|Driving_Licence|Bank_Account|   My_Number|    IP_Address|Phone_Number|
+---------------+------------+------------+--------------+------------+
|   123456789012|  1234567890|000000000000|   192.168.1.1|212-555-0123|
|   987654321098|  9876543210|000000000000|      10.0.0.1|718-555-4567|
|   456789012345|  5678901234|000000000000|    172.16.0.1|347-555-8901|
|   210987654321|  2109876543|000000000000| 192.168.0.100|212-555-2345|
|   876543210987|  8765432109|000000000000|    10.0.0.254|646-555-6789|
|   543210987654|  5432109876|000000000000|172.16.255.255|917-555-0123|
|   109876543210|  1098765432|000000000000| 192.168.1.100|212-555-4567|
|   765432109876|  7654321098|000000000000|    10.0.0.100|718-555-8901|
|   321098765432|  3210987654|000000000000|  172.16.0.100|347-555-2345|
|   098765432109|  9876543210|000000000000|   192.168.0.1|212-555-6789|
+---------------+------------+------------+--------------+------------+

```

### 結果を表示（DynamicFrame） ★(2)
PII処理を実行した後のDynamicFrameを `show()` した結果の一部です。元データのフィールドの値は、`Bank_Account`以外はそれぞれ設定したPII処理が実行されてますね。

また、`DetectedEntities`フィールドが追加されており、それぞれのフィールドがどのPIIと判定されたかが記載されています。`Bank_Account`は、元データに処理は実行されていませんが、`JAPAN_BANK_ACCOUNT`と判定されていますね。これは、処理の方法として`DETECT`を選択したためです。

```json
{
    "Driving_Licence": "************",
    "Bank_Account": "1234567890",
    "My_Number": "3f549c2caaeba19fdcde2b9d281535408832fa81c6782c19eb907737ae9eee2a",
    "IP_Address": "*****",
    "Phone_Number": "2***********",
    "DetectedEntities": {
        "Bank_Account": [
            {
                "entityType": "JAPAN_BANK_ACCOUNT",
                "actionUsed": "DETECT",
                "start": 0,
                "end": 10
            }
        ],
        "Driving_Licence": [
            {
                "entityType": "JAPAN_DRIVING_LICENSE",
                "actionUsed": "PARTIAL_REDACT",
                "start": 0,
                "end": 12
            }
        ],
        "My_Number": [
            {
                "entityType": "JAPAN_MY_NUMBER",
                "actionUsed": "SHA256_HASH",
                "start": 0,
                "end": 12
            }
        ],
        "IP_Address": [
            {
                "entityType": "IP_ADDRESS",
                "actionUsed": "REDACT",
                "start": 0,
                "end": 11
            }
        ],
        "Phone_Number": [
            {
                "entityType": "PHONE_NUMBER",
                "actionUsed": "PARTIAL_REDACT",
                "start": 0,
                "end": 12
            }
        ]
    }
}
...
```

### 結果を表示（データフレーム） ★(3)
DynamicFrameをデータフレームに変換してからの`show()`の実行結果です。
`DetectedEntities`フィールドは省略されていますが、それ以外の値はこちらの方が見やすいですね。

```
+---------------+------------+--------------------+----------+------------+--------------------+
|Driving_Licence|Bank_Account|           My_Number|IP_Address|Phone_Number|    DetectedEntities|
+---------------+------------+--------------------+----------+------------+--------------------+
|   ************|  1234567890|3f549c2caaeba19fd...|     *****|2***********|{Bank_Account -> ...|
|   ************|  9876543210|4e6e9150c4c6d5951...|     *****|7***********|{Bank_Account -> ...|
|   ************|  5678901234|33d9286b207d033a7...|     *****|3***********|{Bank_Account -> ...|
|   ************|  2109876543|0b766bc489f71df30...|     *****|2***********|{Bank_Account -> ...|
|   ************|  8765432109|ca1a43ded4139312b...|     *****|6***********|{Bank_Account -> ...|
|   ************|  5432109876|f6577543febf990ee...|     *****|9***********|{Bank_Account -> ...|
|   ************|  1098765432|5969cb6041008924e...|     *****|2***********|{Bank_Account -> ...|
|   ************|  7654321098|815f6290005b13679...|     *****|7***********|{Bank_Account -> ...|
|   ************|  3210987654|7a0bcaa5dbaffea47...|     *****|3***********|{Bank_Account -> ...|
|   ************|  9876543210|7b61c4dc594d8651a...|     *****|2***********|{Bank_Account -> ...|
+---------------+------------+--------------------+----------+------------+--------------------+
```

# おわりに
あまり試したことがない機能でしたが、スクリプトでも意外と簡単に使えるなぁと思いました。
また、マスキングする文字の変更や、前後指定の文字数だけマスキングしない設定、正規表現でのマスキング定義などカスタマイズも結構できて良いですね。

Visual Editorを使うとお手軽ですが、どうしてもスクリプトで書きたい処理というのは出てくると思います。そんな時でも、 `detect()` APIを使えば今回の記事のように処理が書けるので、ぜひお試しください。

# 参考

https://qiita.com/suzukihi724/items/f130e51f7caaacb5e6eb

https://dev.classmethod.jp/articles/aws-glue-studio-pii-sha-256-hash/