漢字でもOK?! DLP API で個人情報をマスキングしてから BigQuery にロードしてみた。
こんにちは、みかみです。
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
メソッドをコールして個人情報をマスキングします。
- Method: projects.content.deidentify | Cloud Data Loss Prevention ドキュメント
- deidentify_content | Python Client for Cloud Data Loss Prevention (DLP) API
deidentify
のパラメータ InspectConfig
で対象のデータタイプなどの検出情報を指定し、DeidentifyConfig
で検出した機密情報を置換する文字列や置換対象外の文字列などのマスキング情報を指定します。
マスキング対象外として特定の文字列を指定することもできますが、数値や記号、空白文字など、CommonCharsToIgnore
としてあらかじめ定義された enum を指定することも可能です。
今回は、メールアドレスに含まれる @
をマスキング対象外としたかったため、PUNCTUATION
(次の記号のいずれか !"#$%&'()*+,-./:;<=>?@[]^_`{|}~
)を指定しました。
- InspectConfig | Cloud Data Loss Prevention ドキュメント
- DeidentifyConfig | Cloud Data Loss Prevention ドキュメント
- CommonCharsToIgnore | Cloud Data Loss Prevention ドキュメント
- deidentify_content | Python Client for Cloud Data Loss Prevention (DLP) API
- DeidentifyContentRequest | Python Client for Cloud Data Loss Prevention (DLP) API
- InspectConfig | Python Client for Cloud Data Loss Prevention (DLP) API
- DeidentifyConfig | Python Client for Cloud Data Loss Prevention (DLP) API
- CharacterMaskConfig | Python Client for Cloud Data Loss Prevention (DLP) API
以下の 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
とのことで、実在しない住所はマスキングされないようなので、使用したサンプルデータに問題がある可能性もありますが、特にファーストネームはバリエーションあるし、辞書データなど使わないと検出が難しいかもしれません。
今回は検出対象のデータ種別に定義済みのデータタイプを指定しましたが、辞書登録や正規表現を指定してカスタマイズすることも可能なので、実データで検証しつつ、必要に応じて使い分けるのが良さそうです。
- infoType と infoType 検出器 | Cloud Data Loss Prevention ドキュメント
- infoType 検出器リファレンス | Cloud Data Loss Prevention ドキュメント
さらに、都道府県名も一部マスキングされてしまってました。
「東京都」がマスキングされてしまっているのは少し残念ではありますが(「東京都」さん?
確かに「千葉」さん、「長野」さん、などは人名でもあり得るので納得です。
今回は横着してファイルデータ全部を検出対象にしてしまいましたが、対象を特定の項目のみに絞るなどの検討は必要になりそうです。 とはいえ、単語だけではなく文中に含まれる個人情報も検出可能なことが確認できたので心強いです!
まとめ(所感)
公式ドキュメントのサンプルコードだと Java しかないものなどもあり、API リファレンスや Python クライアントライブラリのリファレンスを確認するのに少し手間取りましたが、定義済みのデータタイプ指定して API コールするだけで個人情報をマスキングしてくれる DLP API を使えば、開発の負担がだいぶ軽減できるのではないかと思いました!(ドキュメントのサンプルコードがもっと充実するとすごく嬉しいです><v
日本語(漢字)の姓名データでもちゃんと検出できるなんてさすがです!
難読苗字やキラキラネームだとどうなの?とか、半角カタカナにも対応してる?とか、文脈も判定してくれる?とか、別のデータ種別指定で、例えば FIRST_NAME
(名)や FEMALE_NAME
(一般的な女性の名前)を指定すれば検出結果は変わるの?とか、気になることがまだたくさんあるので、引き続き DLP API を検証してみたいと思います!
参考
- 変換のリファレンス | Cloud Data Loss Prevention ドキュメント
- 文字のマスキング | Cloud Data Loss Prevention ドキュメント
- Cloud DLP クライアント ライブラリ | Cloud Data Loss Prevention ドキュメント
- Cloud Data Loss Prevention (DLP) API | Cloud Data Loss Prevention ドキュメント
- Cloud DLP の全コードサンプル | Cloud Data Loss Prevention ドキュメント
- Pgoogleapis/python-dlp| GitHub
- Python Client for Cloud Data Loss Prevention (DLP) API
- New ways to manage sensitive data with the Data Loss Prevention API | Google Cloud Blog
- Getting Started with Google’s Data Loss Prevention API in Python