以下の内容はhttps://numb86-tech.hatenablog.com/entry/2025/09/12/234742より取得しました。


Google Cloud のデータプロファイル機能で BigQuery のデータを継続的に検査する

Google Cloud の Sensitive Data Protection には「データプロファイル」と呼ばれる機能がある。
この機能を使うことで、機密情報が入ってしまっていないかなどの検査を、大量のデータに対して実施できる。
この記事では、データプロファイルを利用するにあたって理解しておくべき概念や仕組みについて、具体例を示しながら述べていく。

データプロファイルは Google Cloud コンソールから操作することもできるが、この記事では curl で API を叩いて操作していく。
そしてそれを行うためには gcloud CLI での認証を行っている必要があるが、それはすでに済んでいるという前提で話を進める。
gcloud CLI の基本的な使い方や認証については以下を参照のこと。

概要

データプロファイルを使うと、 Google Cloud の「組織」や「フォルダ」や「プロジェクト」といった、広い範囲に対して一斉に検査を行うことができる。

公式ドキュメントによれば、データプロファイルによって大まかにスキャン(検査)し、詳細を知りたい対象に対して Cloud DLP を実行する、という使い方を推奨している模様。

データ プロファイリングは、大量のデータを大まかにスキャンする場合に便利です。機密データのすべてのインスタンスの正確な場所など、細かな詳細を確認する必要がある場合は、検査の実行を検討してください。
https://cloud.google.com/sensitive-data-protection/docs/sensitive-data-protection-overview?hl=ja#discovery

BigQuery や Cloud Storage に入っているデータを検査できる他、 AWS の S3 に対して検査することもできる。

この記事では「プロジェクト」を対象とし、 BigQuery のテーブルをスキャンしていくことにする。

基本的な仕組み

「スキャン構成」を作成することで、スキャンが行われるようになる。
スキャン構成によって、具体的なスキャン対象やスキャンの頻度、スキャン後のアクション、などを指定する。

そしてスキャン結果は「プロファイル」として保持される。スキャンが行われるとプロファイルの作成や更新が行われるので、その内容を見ることでスキャン結果を確認できる。

つまり、

  • スキャン構成を作成する
  • スキャン構成に基づいてスキャンが行われる
  • プロファイルを見てスキャンの結果を確認する
  • 必要に応じてスキャン構成の変更なり停止なりを行う

というのが基本的な流れになる。

ここからは、実際の利用の流れを示していく。

スキャン構成を作成する

まずはスキャン構成を作成するが、そのためには予め、検査テンプレートを作っておく必要がある。
検査テンプレートとは「どのような検査を行うか」を指定するためのもので、それをスキャン構成の一部として組み込む必要がある。

検査テンプレートの API エンドポイントはhttps://dlp.googleapis.com/v2/projects/[PROJECT_ID]/locations/[REGION]/inspectTemplates
これに対して GET リクエストを送ることで検査テンプレートの一覧を取得できる。

$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/inspectTemplates" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj"
{}

この時点では何もないことを確認できた。

先程のエンドポイントに POST リクエストを送ると検査テンプレートを作成できる。
今回は以下のリクエストを送信する。

$ curl -s -X POST "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/inspectTemplates" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-H "x-goog-user-project: sample-pj" \
-d '{
  "inspectTemplate": {
    "displayName": "PERSON_NAME検査テンプレート",
    "inspectConfig": {
      "infoTypes": [
        {
          "name": "PERSON_NAME"
        }
      ]
    }
  }
}'

infoType とは「何が含まれていないかを検査するのか」を指定するためのもの。予め多くの infoType が用意されており、そこから自由に選ぶことができる。
今回はPERSON_NAMEにしたので、人名がないかが検査される。

再び一覧を取得してみると、確かに検査テンプレートが作られている。

$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/inspectTemplates" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj"
{
  "inspectTemplates": [
    {
      "name": "projects/sample-pj/locations/asia-northeast1/inspectTemplates/8070725848210850369",
      "displayName": "PERSON_NAME検査テンプレート",
      "createTime": "2025-07-10T12:05:37.842511Z",
      "updateTime": "2025-07-10T12:05:37.842511Z",
      "inspectConfig": {
        "infoTypes": [
          {
            "name": "PERSON_NAME"
          }
        ],
        "limits": {}
      }
    }
  ],
  "nextPageToken": "KmIKCQjPwtGXoLKOAwpVKlNwcm9qZWN0cy9kbHAtdGVzdC1wai9sb2NhdGlvbnMvYXNpYS1ub3J0aGVhc3QxL2luc3BlY3RUZW1wbGF0ZXMvODA3MDcyNTg0ODIxMDg1MDM2OQ"
}

これでスキャン構成を作るための準備ができた。
スキャン構成のエンドポイントはhttps://dlp.googleapis.com/v2/projects/[PROJECT_ID]/locations/[REGION]/discoveryConfigsで、検査テンプレートと同様、 GET で一覧を取得し POST で作成する。

これも検査テンプレートと同様だが、作成時はリクエストボディで作成内容を指定する。
今回はローカルに JSON ファイルを作成し、それをリクエストボディとして送信することにする。

JSON ファイルの中身は以下。詳細は後述する。

{
  "discoveryConfig": {
    "displayName": "PERSON_NAME検査",
    "status": "RUNNING",
    "targets": [
      {
        "bigQueryTarget": {
          "filter": {
            "otherTables": {}
          },
          "cadence": {
            "schemaModifiedCadence": {
              "frequency": "UPDATE_FREQUENCY_NEVER"
            },
            "refreshFrequency": "UPDATE_FREQUENCY_DAILY"
          }
        }
      }
    ],
    "inspectTemplates": [
      "projects/sample-pj/locations/asia-northeast1/inspectTemplates/8070725848210850369"
    ]
  }
}

このファイルを用意した状態で以下のリクエストを送信すると、その内容に基づいてスキャン構成が作成される。

$ curl -s -X POST "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/discoveryConfigs" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-H "x-goog-user-project: sample-pj" \
-d @request.json

スキャン構成の書き方

先程の JSON ファイルを再掲する。

{
  "discoveryConfig": {
    "displayName": "PERSON_NAME検査",
    "status": "RUNNING",
    "targets": [
      {
        "bigQueryTarget": {
          "filter": {
            "otherTables": {}
          },
          "cadence": {
            "schemaModifiedCadence": {
              "frequency": "UPDATE_FREQUENCY_NEVER"
            },
            "refreshFrequency": "UPDATE_FREQUENCY_DAILY"
          }
        }
      }
    ],
    "inspectTemplates": [
      "projects/sample-pj/locations/asia-northeast1/inspectTemplates/8070725848210850369"
    ]
  }
}

displayNamestatusは難しいことはないと思う。
inspectTemplatesは、使用する検査テンプレート。先ほど作成した検査テンプレートのnameを指定する。

あとはtargetsだが、これはDiscoveryTargetというオブジェクトの配列。
DiscoveryTargetによって、スキャンの対象とスキャンの頻度を設定する。この対象はこの頻度でスキャンする、この対象はスキャンしない、といったことを書いていく。上記の例ではDiscoveryTargetをひとつだけ書いている。

DiscoveryTargetに何を書いていくのかはスキャン対象によって異なり、今回のように BigQuery が対象の場合は"bigQueryTarget": BigQueryDiscoveryTargetを書けばいい。
上記した例だと以下のオブジェクトがBigQueryDiscoveryTargetにあたる。

{
  "filter": {
    "otherTables": {}
  },
  "cadence": {
    "schemaModifiedCadence": {
      "frequency": "UPDATE_FREQUENCY_NEVER"
    },
    "refreshFrequency": "UPDATE_FREQUENCY_DAILY"
  }
}

filterフィールドは必須で、ここで対象のデータセットやテーブルについて記述していく。
今回設定している"otherTables": {}は特殊で、「discoveryConfig.targetsの最後に配置し、他のフィルターにマッチしないすべてのテーブルをキャッチする」。
これはどういうことかというと、discoveryConfig.targets配列の順番はそのまま優先度になっており、先頭から見ていって最初にfilterの内容にマッチしたBigQueryDiscoveryTargetが適用される。配列の最後に"otherTables": {}を設定したBigQueryDiscoveryTargetを書いておくことで、他のBigQueryDiscoveryTargetにマッチしなかった全てのテーブルがここでキャッチされる。
今回は"otherTables": {}を設定したBigQueryDiscoveryTargetしか書いていないので、全てのテーブルがこのBigQueryDiscoveryTargetの対象となる。

cadenceは、対象のリソース(本記事の場合は BigQuery のテーブル)の再スキャンについて設定するためのもの。何をトリガーにして、どのような頻度で再スキャンするのか、を設定する。
設定できるトリガーとして、「テーブルに新しいカラムが追加された」「テーブルの最終更新日時が更新された」などがある。
頻度は、以下のいずれかを指定する。

  • UPDATE_FREQUENCY_UNSPECIFIED
    • 未指定
  • UPDATE_FREQUENCY_NEVER
    • 更新しない
  • UPDATE_FREQUENCY_DAILY
    • 24 時間に 1 回まで更新できる
  • UPDATE_FREQUENCY_MONTHLY
    • 30 日間に 1 回まで更新できる

今回の例ではcadenceに 2 つのフィールドがある。
schemaModifiedCadenceフィールドでは、「スキーマが変更された際の頻度」を設定する。デフォルトはUPDATE_FREQUENCY_MONTHLYだが、UPDATE_FREQUENCY_NEVERを指定したことで再スキャンされなくなる。
refreshFrequencyは「リソースの変更とは関係なく再スキャンする頻度」で、UPDATE_FREQUENCY_DAILYを設定したので「24 時間に 1 回まで更新される」になる。

つまり今回の記述したBigQueryDiscoveryTargetは、「全てのテーブルを対象」に、「テーブルや検査テンプレートの変更の有無とは関係なく毎日スキャンを実行」する、という設定になっている。

ちなみにこのスキャン構成を作成すると、 Google Cloud コンソールでは、以下のように表示される。

初見では何を言っているか分からないかもしれないが、ここまでの説明を踏まえて読めば、意味が分かると思う。

プロファイルを確認する

スキャン構成を作成すると、設定内容や権限などに問題がない限り、スキャンが実行される。
そしてスキャンを実行すると「プロファイル」が作成される。今回の設定内容の場合、プロジェクト、テーブル、カラム、に対するプロファイルがそれぞれ作成される。

プロファイルも API で取得できるので、見ていく。

まずはプロジェクト。
https://dlp.googleapis.com/v2/projects/[PROJECT_ID]/locations/asia-northeast1/projectDataProfilesに GET リクエストを送ることで、東京リージョンにあるPROJECT_IDのプロジェクトデータプロファイルの一覧を得られる。

$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/projectDataProfiles" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj"
{
  "projectDataProfiles": [
    {
      "name": "projects/sample-pj/locations/asia-northeast1/projectDataProfiles/8758125712168864305",
      "projectId": "sample-pj",
      "profileLastGenerated": "2025-08-07T04:26:34.066667Z",
      "sensitivityScore": {
        "score": "SENSITIVITY_MODERATE"
      },
      "dataRiskLevel": {
        "score": "RISK_MODERATE"
      },
      "profileStatus": {
        "status": {},
        "timestamp": "2025-08-07T04:26:34.066691Z"
      },
      "tableDataProfileCount": "5"
    }
  ],
  "nextPageToken": "Kl4KAggUClgqVnByb2plY3RzL2RscC10ZXN0LXBqL2xvY2F0aW9ucy9hc2lhLW5vcnRoZWFzdDEvcHJvamVjdERhdGFQcm9maWxlcy84NzU4MTI1NzEyMTY4ODY0MzA1"
}

URL の末尾にプロファイルの ID を付与すると、対象のプロファイルのみを取得できる。

$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/projectDataProfiles/8758125712168864305" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj"
{
  "name": "projects/sample-pj/locations/asia-northeast1/projectDataProfiles/8758125712168864305",
  "projectId": "sample-pj",
  "profileLastGenerated": "2025-08-07T04:26:34.066667Z",
  "sensitivityScore": {
    "score": "SENSITIVITY_MODERATE"
  },
  "dataRiskLevel": {
    "score": "RISK_MODERATE"
  },
  "profileStatus": {
    "status": {},
    "timestamp": "2025-08-07T04:26:34.066691Z"
  },
  "tableDataProfileCount": "5"
}

プロジェクトデータプロファイルは 1 つ存在し、このプロジェクトのリスクレベルはRISK_MODERATEであることが分かる。
リスクレベルについては以下を参照のこと。

また、"tableDataProfileCount": "5"となっており、このプロジェクトのテーブルデータプロファイルが 5 つあることも分かる。

東京リージョンにあるテーブルデータプロファイルを取得する API のエンドポイントはhttps://dlp.googleapis.com/v2/projects/[PROJECT_ID]/locations/asia-northeast1/tableDataProfiles
出力される内容が多いので、以下の例ではnameフィールドのみを出力している。

$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/tableDataProfiles" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj" | jq '.tableDataProfiles // [] | map({name: .name})'
[
  {
    "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/4267280860077497562"
  },
  {
    "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/5453100759744414630"
  },
  {
    "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/7026304259168839966"
  },
  {
    "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/2509471496123466218"
  },
  {
    "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/5535032390308691205"
  }
]

URL にfilterクエリをつけることも可能で、例えばprofile_last_generatedを使うとプロファイルの最終生成日時で絞り込むことができる。

$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/tableDataProfiles?filter=profile_last_generated%3E%222025-08-01T00:00:00Z%22" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj" | jq '.tableDataProfiles // [] | map({name: .name, profileLastGenerated: .profileLastGenerated})'
[
  {
    "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/4267280860077497562",
    "profileLastGenerated": "2025-08-07T04:23:51.918131Z"
  },
  {
    "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/5453100759744414630",
    "profileLastGenerated": "2025-08-06T18:37:02.937348Z"
  },
  {
    "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/2509471496123466218",
    "profileLastGenerated": "2025-08-07T04:23:51.918131Z"
  },
  {
    "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/5535032390308691205",
    "profileLastGenerated": "2025-08-07T04:23:51.918131Z"
  }
]
$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/tableDataProfiles?filter=profile_last_generated%3E%222025-09-01T00:00:00Z%22" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj" | jq '.tableDataProfiles // [] | map({name: .name, profileLastGenerated: .profileLastGenerated})'
[]

プロジェクトデータプロファイルと同様に、 URL の末尾で対象を指定することもできる。以下は4267280860077497562の例。

$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/tableDataProfiles/4267280860077497562" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj" | \
jq '{name: .name, table: .fullResource, profileLastGenerated: .profileLastGenerated, sensitivityScore: .sensitivityScore.score, riskLevel: .dataRiskLevel.score, predictedInfoTypes: .predictedInfoTypes}'
{
  "name": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/4267280860077497562",
  "table": "//bigquery.googleapis.com/projects/sample-pj/datasets/first/tables/user",
  "profileLastGenerated": "2025-08-07T04:23:51.918131Z",
  "sensitivityScore": "SENSITIVITY_MODERATE",
  "riskLevel": "RISK_MODERATE",
  "predictedInfoTypes": [
    {
      "infoType": {
        "name": "PERSON_NAME",
        "sensitivityScore": {
          "score": "SENSITIVITY_MODERATE"
        }
      }
    }
  ]
}

このプロファイルはsample-pj.first.userテーブルに対するものであり、そのリスクレベルはRISK_MODERATEであることがわかる。

最後に、カラムデータプロファイルを見る。
東京リージョンのカラムデータプロファイルを取得するエンドポイントはhttps://dlp.googleapis.com/v2/projects/[PERSON_NAME]/locations/asia-northeast1/columnDataProfiles

しかしこのエンドポイントはフィルターが必須であり、つけないとエラーになる。

$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/columnDataProfiles" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj"
{
  "error": {
    "code": 400,
    "message": "Filter must have at least `project_id`, `dataset_id`, and `table_id` set but none was set.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "3",
        "domain": "dlp.googleapis.com"
      }
    ]
  }
}

今回はtable_data_profile_nameフィルターをつけることにする。先ほど見た、userテーブルデータプロファイルのnameであるprojects/sample-pj/locations/asia-northeast1/tableDataProfiles/4267280860077497562を指定する。そうすると、このテーブルのカラムのプロファイルを取得できる。

$ curl -s -X GET "https://dlp.googleapis.com/v2/projects/sample-pj/locations/asia-northeast1/columnDataProfiles?filter=table_data_profile_name%3D%22projects%2Fsample-pj%2Flocations%2Fasia-northeast1%2FtableDataProfiles%2F4267280860077497562%22" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "x-goog-user-project: sample-pj"
{
  "columnDataProfiles": [
    {
      "name": "projects/sample-pj/locations/asia-northeast1/columnDataProfiles/1761375420940097414",
      "profileLastGenerated": "2025-08-07T04:23:51.918131Z",
      "tableDataProfile": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/4267280860077497562",
      "tableFullResource": "//bigquery.googleapis.com/projects/sample-pj/datasets/first/tables/user",
      "column": "name",
      "sensitivityScore": {
        "score": "SENSITIVITY_MODERATE"
      },
      "dataRiskLevel": {
        "score": "RISK_MODERATE"
      },
      "columnInfoType": {
        "infoType": {
          "name": "PERSON_NAME",
          "sensitivityScore": {
            "score": "SENSITIVITY_MODERATE"
          }
        }
      },
      "columnType": "TYPE_STRING",
      "profileStatus": {
        "status": {},
        "timestamp": "2025-08-07T04:26:33.240828Z"
      },
      "state": "DONE",
      "estimatedNullPercentage": "NULL_PERCENTAGE_VERY_LOW"
    },
    {
      "name": "projects/sample-pj/locations/asia-northeast1/columnDataProfiles/9094823111675310899",
      "profileLastGenerated": "2025-08-07T04:23:51.918131Z",
      "tableDataProfile": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/4267280860077497562",
      "tableFullResource": "//bigquery.googleapis.com/projects/sample-pj/datasets/first/tables/user",
      "column": "note",
      "sensitivityScore": {
        "score": "SENSITIVITY_MODERATE"
      },
      "dataRiskLevel": {
        "score": "RISK_MODERATE"
      },
      "columnInfoType": {
        "infoType": {
          "name": "PERSON_NAME",
          "sensitivityScore": {
            "score": "SENSITIVITY_MODERATE"
          }
        }
      },
      "columnType": "TYPE_STRING",
      "profileStatus": {
        "status": {},
        "timestamp": "2025-08-07T04:26:33.240828Z"
      },
      "state": "DONE",
      "estimatedNullPercentage": "NULL_PERCENTAGE_VERY_LOW"
    },
    {
      "name": "projects/sample-pj/locations/asia-northeast1/columnDataProfiles/3800903675484169089",
      "profileLastGenerated": "2025-08-07T04:23:51.918131Z",
      "tableDataProfile": "projects/sample-pj/locations/asia-northeast1/tableDataProfiles/4267280860077497562",
      "tableFullResource": "//bigquery.googleapis.com/projects/sample-pj/datasets/first/tables/user",
      "column": "id",
      "sensitivityScore": {
        "score": "SENSITIVITY_LOW"
      },
      "dataRiskLevel": {
        "score": "RISK_LOW"
      },
      "columnType": "TYPE_STRING",
      "profileStatus": {
        "status": {},
        "timestamp": "2025-08-07T04:26:33.240828Z"
      },
      "state": "DONE",
      "estimatedNullPercentage": "NULL_PERCENTAGE_VERY_LOW"
    }
  ],
  "nextPageToken": "Im50YWJsZV9kYXRhX3Byb2ZpbGVfbmFtZT0icHJvamVjdHMvZGxwLXRlc3QtcGovbG9jYXRpb25zL2FzaWEtbm9ydGhlYXN0MS90YWJsZURhdGFQcm9maWxlcy80MjY3MjgwODYwMDc3NDk3NTYyIipdCgIICgpXKlVwcm9qZWN0cy9kbHAtdGVzdC1wai9sb2NhdGlvbnMvYXNpYS1ub3J0aGVhc3QxL2NvbHVtbkRhdGFQcm9maWxlcy8zODAwOTAzNjc1NDg0MTY5MDg5"
}

namenoteidの 3 つのカラムがあり、idのみがRISK_LOWでそれ以外はRISK_MODERATEであることが分かる。

このように、スキャンを実行するとプロファイルが作成される。
再スキャンが実行された場合は、既存のプロファイルが更新される。つまり、スキャン毎にプロファイルが新規作成されるわけではない。
もちろん再スキャンのタイミングで新しくテーブルやカラムが追加されていれば、それに対応したプロファイルは新規作成される。

参考資料




以上の内容はhttps://numb86-tech.hatenablog.com/entry/2025/09/12/234742より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14