15,000カラムの機密性を守る - CTOと実践したデータガバナンスの実現

この記事は、 CADDi Tech/Product Advent Calendar 2025 の23日目の記事です。

DataManagementチームの福田です。弊社ではCTOと共に機密データの取り扱いを決定し、その方針をBigQuery Policy TagsとDataContractで自動的に反映する体制を構築しています。今回は約15,000カラム以上のデータで実践した経営層を巻き込んだデータガバナンスの仕組みを解説します。

はじめに

企業向けSaaSでは、顧客データに対して通常よりも厳格なアクセス制御が求められます。 例えば、機密性の高いデータを一般の社員には閲覧させず、特定のユースケースにおいてのみ限定されたグループのみがアクセスできるようにする必要があります。 このような要件に対して、BigQueryの Policy Tags と Data Masking Policy を活用することで、クエリ時に透過的なマスキングを実現できます。さらにDataContractで機密性を宣言的に管理することである程度の自動化と監査可能性を両立した基盤を構築しました。

BigQueryのPolicy Tags機能

Policy Tagsとは

BigQueryのPolicy Tagsは、カラムレベルでアクセス制御とデータマスキングを実現するGA機能です。Taxonomyを作成し、その中にPolicy Tagを定義します。Policy TagにData Masking Policyを紐付け、テーブルのカラムに適用することで、IAM権限に応じて自動的にデータをマスキングできます。

透過的マスキングの仕組み

Policy Tagsの最大の特徴は、同じテーブルに同じクエリを実行しても、ユーザーの権限によって返されるデータが自動的に変わることです。マスキングビューを別途作成する必要はありません

具体例:顧客連絡先テーブルへのクエリ

この例では、emailphoneカラムにPolicy Tagが適用されており、customer_nameは非機密カラムとして扱われています。

-- 特権グループのユーザーも一般ユーザーも、同じSQLを実行
SELECT
  customer_name,
  email,
  phone
FROM `my-project.customer_data.customer_contacts`
WHERE customer_id = 12345

特権グループのユーザー(roles/datacatalog.categoryFineGrainedReader権限あり):

customer_name email phone
田中太郎 tanaka-example@caddi.com 03-1234-5678

一般ユーザー(権限なし):

customer_name email phone
田中太郎 ************************ ************

customer_nameは非機密カラムのため両グループとも表示されますが、emailphoneは機密カラムとしてマスキングされます。このように、カラム単位で細かくアクセス制御を設定できます。

テーブルスキーマへのPolicy Tags適用

Policy Tagsをテーブルのカラムに適用するには、BigQueryのスキーマ定義にPolicy Tag IDを追加します。

Terraform での設定例:

resource "google_bigquery_table" "customer_contacts" {
  dataset_id = "customer_data"
  table_id   = "customer_contacts"

  schema = jsonencode([
    {
      name = "customer_id"
      type = "STRING"
      mode = "REQUIRED"
    },
    {
      name = "customer_name"
      type = "STRING"
      # Policy Tagなし(非機密カラム)
    },
    {
      name = "email"
      type = "STRING"
      policyTags = {
        names = [google_data_catalog_policy_tag.confidential.name]
      }
    },
    {
      name = "phone"
      type = "STRING"
      policyTags = {
        names = [google_data_catalog_policy_tag.confidential.name]
      }
    }
  ])
}

このように、機密性レベルに応じて一部のカラムにのみPolicy Tagsを適用することで、柔軟なアクセス制御が可能になります。

CADDiにおけるデータガバナンス体制

弊社では、機密データの取り扱いに関する意思決定をCTOと共に行っています。ここで決定した、どのデータを誰がどのような条件でアクセスできるかについて会社組織全体と合意形成を行っています。

この体制により、技術的な実装だけでなく、ビジネス要件とセキュリティ要件を両立させた意思決定が可能になっています。特に顧客データを預かるSaaS企業として、データの取り扱いについて経営層を巻き込んだガバナンスは不可欠です。

データガバナンスのプロセス

機密性分類の決定から技術的な適用まで、以下のプロセスで運用しています:

  1. データガバナンス委員会で合意: CTO・データオーナーが参加し、機密性分類について議論・決定
  2. Spreadsheetで管理: 全カラムの機密性レベルを管理
  3. DataContractでコード化: CIでSpreadsheetからDataContract YAMLを自動生成しGit管理
  4. 様々な用途に自動反映: Policy Tags、データカタログなど複数の用途に展開

大規模運用における課題とDataContractの必要性

大規模なデータ基盤で手動運用すると、全ての機密カラムに対してTerraformで1つずつ設定する必要があり、設定ミスのリスクが高くなります。これはマスタとなるスプレッドシートを用意し、スクリプトなどでTerraform向けJSONを生成するという選択肢も考えられます。しかし、我々のチームではDataContractを中間層として挟むアプローチを選択しました。

DataContractとは

DataContractは、データの「契約書」として、スキーマだけでなく、データ品質、機密性、所有権などのメタデータをコードで管理する仕組みです。データオーナーが「このカラムはどのように扱うべきか」を宣言的に定義します。

DataContractの基本構造:

dataContractSpecification: 1.2.0
id: company.customers
models:
  customers:
    description: 顧客マスタ
    fields:
      customer_id:
        type: string
        description: 顧客ID
        quality:
          - type: not_null
          - type: unique

      email:
        type: string
        description: メールアドレス
        confidentials:
          classification: 'pii'  # 個人情報として分類

Data Contract CLIとは

cli.datacontract.com

DataContractの作成・管理・活用を支援するオープンソースのCLIツールです。YAML形式のDataContract定義ファイルを中心に、データガバナンスを自動化する包括的なエコシステムを提供します。主な機能として以下が挙げられます。

  • インポーター: BigQuery、Snowflake、PostgreSQLなどからスキーマを自動取得してDataContract生成
  • Export機能: dbtスキーマ、OpenAPI、Avro、SQLなどへの変換
  • カタログ生成: DataContractからHTMLデータカタログを自動生成
  • バリデーション: 実データとDataContractの整合性チェック
  • カスタム拡張: Importerクラスを継承して独自のデータソースに対応可能

今回我々はカスタムインポーター機能を使ってBigQueryに用意したテーブルからDataContractを自動生成し、標準のExport機能では対応していないTerraform向けJSON抽出は独自実装しています。

なぜData Contract CLIを採用したか

dbt sourceファイルには機密性情報を含めていません。両者は目的が異なるためです。両者を分離することで関心の分離を実現し、それぞれの目的に特化した管理が可能になります。

  • dbt source: dbtモデル構築のためのメタデータ(データ型、テーブル構造)
  • DataContract: データガバナンスのためのメタデータ(機密性分類、品質ルール)

DataContractの作成と管理

前述のデータガバナンス委員会で決定された機密性分類は、まずSpreadsheetで管理されBigQueryの外部テーブルとしても参照できるようになっています。 DataContract YAMLファイルの作成・更新は、Data Contract CLIのカスタムインポーター機能を使って完全に自動化しています。 Data Contract CLIのImporterクラスを継承し、BigQueryに用意した外部テーブル(スプレッドシート)から機密性分類を取得してDataContract YAMLを自動生成します。 CIによりカスタムインポーターを実行し、DataContractファイルに差分があれば自動的にPRを作成します。

簡略化したコード例:

from datacontract.imports.importer import Importer

class DatalakeColumnImporter(Importer):
    def import_source(self, data_contract_specification, source, import_args):
        """BigQuery外部テーブルからDataContractを自動生成"""
        query = """
        SELECT
            dataset_name,
            table_name,
            column_name,
            data_type,
            confidential
        FROM `my-project.governance.spreadsheet_of_column_confidential_level`
        """

        # DataContract YAMLを自動生成
        for row in bq_client.query(query):
            field_spec = {
                "data_type": row.data_type,
                "confidentials": {
                    "confidential": row.confidential
                }
            }

DataContractから機密カラムを抽出

前述の通り、Data Contract CLIの標準Export機能では、「DataContractから特定の機密性分類を持つカラム一覧をTerraform向けにJSON形式で抽出する」ことができません。Policy Tags適用のためには、以下の情報が必要です。

  • プロジェクトID、データセット名、テーブル名
  • 機密カラム名とそのデータ型
  • Terraform for_eachで処理できるJSON構造

そのため、独自のPythonスクリプトを実装しました。

抽出スクリプト例

def extract_confidential_columns(datacontract_path: Path) -> list[dict[str, str]]:
    """
    DataContract YAMLから confidential='yes' のカラムを抽出
    """
    with open(datacontract_path, encoding="utf-8") as f:
        contract_data = yaml.safe_load(f)

    # パスから dataset_name と table_name を取得
    # 例: datacontract/contracts/customer_data/customer_contacts/datacontract.yml
    dataset_name = datacontract_path.parts[-3]
    table_name = datacontract_path.parts[-2]

    confidential_columns = []
    models = contract_data.get("models", {})

    for model_name, model_info in models.items():
        for column_name, field_info in model_info["fields"].items():
            confidentials = field_info.get("confidentials", {})
            if confidentials.get("confidential") == "yes":
                confidential_columns.append({
                    "dataset_name": dataset_name,
                    "table_name": table_name,
                    "column_name": column_name,
                })

    return confidential_columns

生成されるJSON: confidential_columns.json

[
  {
    "project_id": "my-project",
    "dataset_name": "customer_data",
    "table_name": "customer_contacts",
    "columns": [
      {"name": "customer_name", "type": "STRING"},
      {"name": "email", "type": "STRING"},
      {"name": "phone", "type": "STRING"},
      {"name": "address", "type": "STRING"}
    ]
  }
]

実行:

cd terraform/modules/data_masking/scripts
uv run python -m src.fetch_confidential_from_contract --env prod

この生成したJSONをTerraformで読み込み、Policy TagsとData Masking Policyを作成してテーブルの機密カラムに自動適用することで、透過的なマスキングを実現します。

導入時の課題

この仕組みと運用ににおいて、最大の面倒くさいポイントは初期構築時に利用したい全てのカラムをスプレッドシートで精査し、機密性分類する必要があることです。 弊社の場合、全てのテーブルを合わせて15,000カラム以上を精査しました。しかしこれは安全に運用するための必要コストと割り切ってやりきりました。

一方でBigQueryや他社DWHにはAIで自動的に機密カラムを検出する機能が備わっています。ここで検出できる機密は当たり前ですがemailや住所などの個人情報です。我々はdescription や tag などのようなデータを保護したい機密データとして定義しているので適用は難しいです。このため我々は最終的には人間がレビューして正しい機密性分類をすることが重要と考えています。

まとめ

BigQueryのPolicy TagsとDataContractを組み合わせることで機密データに対する厳格なアクセス制御を実現しました。 本アプローチは、機密度の高いデータ基盤において、知らない間に機密データが分析可能になるようなリスクを回避しつつ、適切なメンバーには分析可能な基盤を提供するために設計されました。 一方で、運用負荷とのトレードオフとして、連携している全てのデータに対してカラムレベルで機密性を判断・定義し続ける必要があります。 しかし、顧客からデータを預かる立場として安全に運用することはデータガバナンスの責務であり、この運用負荷は受け入れるべきコストと考えています。同様の課題がある皆様の参考になれば幸いです。

CADDiでは一緒に働くメンバーを絶賛募集中です! カジュアル面談などお気軽にご連絡ください。