漢字でもOK?! DLP API で個人情報をマスキングしてから BigQuery にロードしてみた。

2021.06.02

こんにちは、みかみです。

BigQuery のデータから個人情報を検出するために Cloud DLP を利用すると便利ですが、マスキングまで実行したい場合、どうすればいいの?

と思って確認してみましたが、現在のところ、画面 UI からのジョブ作成では機密データの検査とリスク分析しか実行できず、機密情報を削除したりマスキングするためには、DLP API を利用する必要があるようです。

ということで。

やりたいこと

  • BigQuery にロードするデータの個人情報をマスキングしたい
  • DLP API で個人情報をマスキングするにはどうすればいいのか知りたい

概要

GCS に配置されたファイルを Cloud Functions でマスキングしてから BigQuery にロードする、以下のバッチジョブを作成してみました。

GCS にファイルが配置されると1つ目の Cloud Functions が起動し、DLP API を使ってファイルデータの中の個人情報をマスキングして、別の GCS バケットに CSV ファイルを出力します。 マスキングされた CSV ファイルが出力されると2つ目の Cloud Functions が起動し、マスキング済みデータを BigQuery にロードします。

GCS バケットを準備

ソースデータを配置する1つ目の Cloud Functions のトリガーとなる dlp-mikami-src と、1つ目の Cloud Functions がマスキング済みの CSV ファイルを PUT して2つ目の Cloud Functions のトリガーとなる dlp-mikami-dst、2つの GCS バケットを作成しました。

Cloud Functions 関数を作成

以下が1つ目の Cloud Functions のソースコードです。 GCS に配置された CSV ファイルのデータを取得し、DLP API でマスキングしたしたデータを別の GCS バケットに CSV ファイルとして出力します。

import google.cloud.dlp
from google.cloud.dlp import CharsToIgnore
from google.cloud import storage
import os
from datetime import datetime

def deidentify(project, input_str, info_types, masking_character=None, number_to_mask=0, ignore_commpn=None):
    dlp = google.cloud.dlp_v2.DlpServiceClient()
    parent = f"projects/{project}"
    item = {"value": input_str}

    inspect_config = {"info_types": [{"name": info_type} for info_type in info_types]}
    deidentify_config = {
        "info_type_transformations": {
            "transformations": [
                {
                    "primitive_transformation": {
                        "character_mask_config": {
                            "masking_character": masking_character,
                            "number_to_mask": number_to_mask,
                            "characters_to_ignore":[{
                                "common_characters_to_ignore": ignore_commpn
                            }]
                        }
                    }
                }
            ]
        }
    }

    response = dlp.deidentify_content(
        request={
            "parent": parent,
            "deidentify_config": deidentify_config,
            "inspect_config": inspect_config,
            "item": item,
        }
    )
    return response.item.value

def mask_data(data, context):
    # get target file info.
    bucket_src = data['bucket']
    file_src = data['name']
    print('target file: {}'.format(file_src))

    # get project and destination bucket name.
    GCP_PROJECT = os.getenv('GCP_PROJECT')
    BUCKET_DST = os.getenv('BUCKET_DST', None)
    if not GCP_PROJECT or not BUCKET_DST:
        raise Exception('Missing "project id" or "backup bucket name" in env.')

    # download csv data from GCS.
    client = storage.Client()
    bucket = client.get_bucket(bucket_src)
    blob = bucket.blob(file_src)
    stream = blob.download_as_string().decode('utf-8')

    # masking data.
    stream_masked = deidentify(GCP_PROJECT, stream, ['PERSON_NAME', 'EMAIL_ADDRESS'], masking_character='*', ignore_commpn=CharsToIgnore.CommonCharsToIgnore.PUNCTUATION.value)

    # put masking data as CSV file.
    file_dst = '{}_{}.csv'.format(os.path.splitext(file_src)[0], datetime.now().strftime('%Y%m%d%H%M%S'))
    bucket_dst = client.get_bucket(BUCKET_DST)
    blob_dst = bucket_dst.blob(file_dst)
    blob_dst.upload_from_string(stream_masked, content_type='text/csv')
    print('put file: {}'.format(file_dst))

python クライアントライブラリ経由で DLP API の deidentify メソッドをコールして個人情報をマスキングします。

deidentify のパラメータ InspectConfig で対象のデータタイプなどの検出情報を指定し、DeidentifyConfig で検出した機密情報を置換する文字列や置換対象外の文字列などのマスキング情報を指定します。 マスキング対象外として特定の文字列を指定することもできますが、数値や記号、空白文字など、CommonCharsToIgnore としてあらかじめ定義された enum を指定することも可能です。 今回は、メールアドレスに含まれる @ をマスキング対象外としたかったため、PUNCTUATION(次の記号のいずれか !"#$%&'()*+,-./:;<=>?@[]^_`{|}~)を指定しました。

以下の requirements.txt も準備して、Cloud Functions の関数を作成しました。

google-cloud-dlp>=3.0.1
google-cloud-storage>=1.38.0


mikami_yuki@cloudshell:~/sample/func_1 (cm-da-mikami-yuki-258308)$ gcloud functions deploy mask_data \
> --runtime python37 \
> --trigger-resource dlp-mikami-src \
> --trigger-event google.storage.object.finalize \
> --set-env-vars BUCKET_DST=dlp-mikami-dst
Deploying function (may take a while - up to 2 minutes)...⠹
For Cloud Build Stackdriver Logs, visit: https://console.cloud.google.com/logs/viewer?project=cm-da-mikami-yuki-258308&advancedFilter=resource.type%3Dbuild%0Aresource.labels.build_id%3Dd57aaf97-85e0-47c1-b341-8e330459bdc0%0AlogName%3Dprojects%2Fcm-da-mikami-yuki-258308%2Flogs%2Fcloudbuild
Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
buildId: d57aaf97-85e0-47c1-b341-8e330459bdc0
entryPoint: mask_data
environmentVariables:
  BUCKET_DST: dlp-mikami-dst
eventTrigger:
  eventType: google.storage.object.finalize
  failurePolicy: {}
  resource: projects/_/buckets/dlp-mikami-src
  service: storage.googleapis.com
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/cm-da-mikami-yuki-258308/locations/us-central1/functions/mask_data
runtime: python37
serviceAccountEmail: cm-da-mikami-yuki-258308@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-2365985c-f7e0-4882-9f01-971d82702e1f/95dec4ff-d1fa-4ddc-9856-5e8fee551bdd.zip?GoogleAccessId=service-797147019523@gcf-admin-robot.iam.gserviceaccount.com&Expires=1622631010&Signature=ZYoe9rkTu%2BZc4Pz8%2BcBlZiEzdNPE%2FpuXEbMVeWUqFPlRGPtDtx33T3jSb8yk3ldLKeoMYzeRXwWeLnHrUMvSNKOQ4r4764ak8mRklguKw109oNRkDlsw%2FRfYIbC441nQnjlEN7O4AOURBrREr8X94TBEZThPolGJ0ZySizuWHz8O3cUtUwgAfhoqJyML9n4a8ipWBY9W5dXbtUNUXrSQXDT8mK3aOzbRziJ1kMbZg7QIChDFAPtllf0aUwwNMRwb7%2FJPWqGPzLCDwaksjwM5mlqppL%2FfzyhM3DeHsIL8WEoBABZOVFL%2FGj3KZ0BVLFtPbZcTpWXR9fWlMTza652f%2FQ%3D%3D
status: ACTIVE
timeout: 60s
updateTime: '2021-06-02T10:21:42.176Z'
versionId: '2'


2つ目の Cloud Functions のソースコードは以下です。

from google.cloud import bigquery
import os

def load_data(data, context):
    # check content-type
    if data['contentType'] != 'text/csv':
        raise Exception('Not supported file type: {}'.format(data['contentType']))
    # get file info
    bucket_name = data['bucket']
    file_name = data['name']
    uri = 'gs://{}/{}'.format(bucket_name, file_name)

    # get target table info.
    GCP_PROJECT = os.getenv('GCP_PROJECT')
    DATASET_ID = os.getenv('DATASET_ID', None)
    TABLE_ID = os.getenv('TABLE_ID', None)
    if not GCP_PROJECT or not DATASET_ID or not TABLE_ID:
        raise Exception('Missing "project id" or "dataset id" or "table id" in env.')

    # load data to BigQuery.
    print('load {} to {}.{}'.format(uri, DATASET_ID, TABLE_ID))
    client = bigquery.Client()
    job_config = bigquery.LoadJobConfig(
        autodetect = True,
        source_format = bigquery.SourceFormat.CSV,
        write_disposition = 'WRITE_TRUNCATE'
    )
    load_job = client.load_table_from_uri(
        uri, '{}.{}.{}'.format(GCP_PROJECT, DATASET_ID, TABLE_ID), job_config=job_config
    )
    load_job.result()
    print('load complete.')

マスキング済みの CSV データを BigQuery にロードします。

requirements.txt は以下です。

google-cloud-bigquery>=2.16.1

こちらも以下の CLI コマンドでデプロイしました。

gcloud functions deploy load_data \
--runtime python37 \
--trigger-resource dlp-mikami-dst \
--trigger-event google.storage.object.finalize \
--set-env-vars DATASET_ID=dataset_1,TABLE_ID=dlp_load_with_masking

個人情報入り CSV ファイルを作成してジョブを実行

以下のサイトで、氏名や email などの個人情報入りの CSV データを作成しました。

以下の個人情報が含まれた100件のデータです。

  • 姓名(漢字 半角スペース区切り)
  • 姓(漢字)
  • 名(漢字)
  • ふりがな(全角カタカナ 半角スペース区切り)
  • email アドレス

作成した CSV ファイルを GCS バケットに配置して、Cloud Functions 関数を実行してみます。

完了したようなので、BigQuery のテーブルデータを確認してみます。

姓名(漢字 半角スペース区切り)と email アドレスは 100/100 件マスキングされました!(漢字もマスキングしてくれるなんてすごいv

姓のみ、名のみの漢字データは若干(姓のみで6%、名のみで19%)マスキングされないままのデータが残ってしまい、カタカナの氏名だとマスキング率は40%でしたが、マルチバイト文字(日本語)も検出してくれてます!

DLP will not recognize or mask fake addresses so randomly generated locations would not work for this demonstration

とのことで、実在しない住所はマスキングされないようなので、使用したサンプルデータに問題がある可能性もありますが、特にファーストネームはバリエーションあるし、辞書データなど使わないと検出が難しいかもしれません。

今回は検出対象のデータ種別に定義済みのデータタイプを指定しましたが、辞書登録や正規表現を指定してカスタマイズすることも可能なので、実データで検証しつつ、必要に応じて使い分けるのが良さそうです。

さらに、都道府県名も一部マスキングされてしまってました。

「東京都」がマスキングされてしまっているのは少し残念ではありますが(「東京都」さん?

確かに「千葉」さん、「長野」さん、などは人名でもあり得るので納得です。

今回は横着してファイルデータ全部を検出対象にしてしまいましたが、対象を特定の項目のみに絞るなどの検討は必要になりそうです。 とはいえ、単語だけではなく文中に含まれる個人情報も検出可能なことが確認できたので心強いです!

まとめ(所感)

公式ドキュメントのサンプルコードだと Java しかないものなどもあり、API リファレンスや Python クライアントライブラリのリファレンスを確認するのに少し手間取りましたが、定義済みのデータタイプ指定して API コールするだけで個人情報をマスキングしてくれる DLP API を使えば、開発の負担がだいぶ軽減できるのではないかと思いました!(ドキュメントのサンプルコードがもっと充実するとすごく嬉しいです><v

日本語(漢字)の姓名データでもちゃんと検出できるなんてさすがです!

難読苗字やキラキラネームだとどうなの?とか、半角カタカナにも対応してる?とか、文脈も判定してくれる?とか、別のデータ種別指定で、例えば FIRST_NAME(名)や FEMALE_NAME(一般的な女性の名前)を指定すれば検出結果は変わるの?とか、気になることがまだたくさんあるので、引き続き DLP API を検証してみたいと思います!

参考