30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

権限借用(Impersonation)で dbt や AI 利用時の権限設計の幅を広げる

Google Cloud には権限借用( Impersonation )という仕組みがあり、これを使うと、「プリンシパル(ユーザーアカウントやサービスアカウント)が一時的に他のサービスアカウントになりすまして Google Cloud を操作する」ということができる。これにより、本来そのプリンシパルには出来ない操作を特定の状況で一時的にできるようにする、といったことが可能になる。
dbt も権限借用に対応しており、 dbt の実行時にのみ BigQuery に対する編集権限を付与することなどができるようになる。

この記事では、 dbt で権限借用を利用する方法や、それを行った際に Google Cloud のログにどのように記録されるのか、どのように活用しうるのか、などを見ていく。

この記事の内容は以下の環境で動作確認している。

  • dbt-core v1.11.8
  • dbt-bigquery v1.11.1

事前準備

Google Cloud のsample-pjというプロジェクトの BigQuery に、srcというデータセットを作り、そこにuserテーブルを用意する。データも入れておく。
以下の状況にしておく。

$ bq show sample-pj:src.user
Table sample-pj:src.user

   Last modified              Schema             Total Rows   Total Bytes   Expiration   Time Partitioning   Clustered Fields   Total Logical Bytes   Total Physical Bytes   Labels
 ----------------- ---------------------------- ------------ ------------- ------------ ------------------- ------------------ --------------------- ---------------------- --------
  11 Apr 21:39:40   |- id: integer (required)    2            28                                                                28                    1131
                    |- name: string (required)
$ bq head sample-pj:src.user
+----+-------+
| id | name  |
+----+-------+
|  1 | alice |
|  2 | bob   |
+----+-------+

次に、検証用のサービスアカウントを 2 つ用意する。

まずはdbt-runner

$ gcloud iam service-accounts create dbt-runner \
  --project=sample-pj
Created service account [dbt-runner].

$ gcloud projects add-iam-policy-binding sample-pj \
  --member="serviceAccount:dbt-runner@sample-pj.iam.gserviceaccount.com" \
  --role="roles/bigquery.jobUser"

$ gcloud projects add-iam-policy-binding sample-pj \
  --member="serviceAccount:dbt-runner@sample-pj.iam.gserviceaccount.com" \
  --role="roles/bigquery.dataEditor"

roles/bigquery.jobUserroles/bigquery.dataEditorを付与している。これにより BigQuery のジョブの実行やデータの読み書きを行えるようになっているので、dbt-runnerは BigQuery を対象としたdbtコマンドを実行できる。

$ gcloud projects get-iam-policy sample-pj --flatten="bindings[].members" --filter="bindings.members:dbt-runner@sample-pj.iam.gserviceaccount.com" --format="json(bindings.role)"
[
  {
    "bindings": {
      "role": "roles/bigquery.dataEditor"
    }
  },
  {
    "bindings": {
      "role": "roles/bigquery.jobUser"
    }
  }
]

次にnormal-user

$ gcloud iam service-accounts create normal-user \
  --project=sample-pj
Created service account [normal-user].

$ gcloud projects add-iam-policy-binding sample-pj --member="serviceAccount:normal-user@sample-pj.iam.gserviceaccount.com" --role="roles/bigquery.metadataViewer"

このサービスアカウントにはroles/bigquery.metadataViewerのみを付与している。メタデータ(テーブルのスキーマなど)は取得できるが、テーブルに格納されているデータにアクセスすることはできない。

$ gcloud projects get-iam-policy sample-pj --flatten="bindings[].members" --filter="bindings.members:normal-user@sample-pj.iam.gserviceaccount.com" --format="json(bindings.role)"
[
  {
    "bindings": {
      "role": "roles/bigquery.metadataViewer"
    }
  }
]

そして、normal-userdbt-runnerを権限借用できるようにする。
具体的には、dbt-runnerに対して、normal-userroles/iam.serviceAccountTokenCreatorとして追加する。

$ gcloud iam service-accounts add-iam-policy-binding dbt-runner@sample-pj.iam.gserviceaccount.com --member="serviceAccount:normal-user@sample-pj.iam.gserviceaccount.com" --role="roles/iam.serviceAccountTokenCreator"

こうするとnormal-userは、必要に応じて「dbt-runnerとして認証されるアクセストークン」を発行しそれを利用することができるようになる。

これ以降はnormal-userとして各種操作を行うので、認証を行う。
まずはnormal-userのサービスアカウントキーを発行する。

$ gcloud iam service-accounts keys create normal-user-key.json --iam-account=normal-user@sample-pj.iam.gserviceaccount.com

normal-user-key.jsonが発行されたので、これを使って gcloud CLI の認証を行う。

$ gcloud auth login --cred-file=normal-user-key.json

dbt では ADC の認証情報を使うので、それも設定しておく。

$ export GOOGLE_APPLICATION_CREDENTIALS=normal-user-key.json

GOOGLE_APPLICATION_CREDENTIALSとは何か、そもそも ADC とは何か、ということについては以下の記事を参照。

numb86-tech.hatenablog.com

normal-user の権限を確認する

normal-userとして認証されている状態になったので、このアカウントで何ができるのか、何ができないのか、確認しておく。

まず、データセット一覧やテーブルのスキーマを得ることはできる。
これは、roles/bigquery.metadataViewerを付与されているため。

$ bq ls --project_id=sample-pj
       datasetId
 ---------------------
  src

$ bq show sample-pj:src.user
Table sample-pj:src.user

   Last modified              Schema             Total Rows   Total Bytes   Expiration   Time Partitioning   Clustered Fields   Total Logical Bytes   Total Physical Bytes   Labels
 ----------------- ---------------------------- ------------ ------------- ------------ ------------------- ------------------ --------------------- ---------------------- --------
  11 Apr 21:51:07   |- id: integer (required)    2            28                                                                28                    1131
                    |- name: string (required)

しかし、テーブルの中身をみることはできない。roles/bigquery.metadataViewerにはbigquery.tables.getDataは含まれていないためである。

$ bq head sample-pj:src.user
BigQuery error in head operation: Access Denied: Table sample-pj:src.user: Permission bigquery.tables.getData denied on table sample-pj:src.user (or it may not exist).

次にdbt runを実行できるか確認する。

以下のような dbt プロジェクトを用意する。

# profiles.yml
impersonation:
  target: dev
  outputs:
    dev:
      type: bigquery
      method: oauth
      project: sample-pj
      dataset: dest
      threads: 1
      location: asia-northeast1
# models/sources.yml
sources:
  - name: src
    database: sample-pj
    schema: src
    tables:
      - name: user
        columns:
          - name: id
            data_type: integer
          - name: name
            data_type: string
-- models/user.sql
select
    id,
    name,
    upper(name) as name_upper
from {{ source('src', 'user') }}

この内容でdbt runを実行すると以下のエラーになる。

Database Error in model user (models/user.sql)
      Access Denied: Project sample-pj: User does not have bigquery.jobs.create permission in project sample-pj.

normal-userに付与されているロールはroles/bigquery.metadataViewerのみであり、このロールはbigquery.jobs.createを持っていないため、エラーになっている。

権限借用を利用する

profiles.ymlで定義しているdev target の設定を変え、dbt-runnerの権限を借用してdbtコマンドを実行するようにする。
具体的には、impersonate_service_accountフィールドを用意し、権限を借りるサービスアカウント、つまりdbt-runnerを設定する。

impersonation:
  target: dev
  outputs:
    dev:
      type: bigquery
      method: oauth
      project: sample-pj
      dataset: dest
      threads: 1
      location: asia-northeast1
      impersonate_service_account: dbt-runner@sample-pj.iam.gserviceaccount.com

この状態でdbt runを実行すると、成功する。

意図通り、destデータセットにuserが作成されている。

$ bq show sample-pj:dest.user
Table sample-pj:dest.user

   Last modified           Schema           Type   Expiration   Labels
 ----------------- ----------------------- ------ ------------ --------
  11 Apr 23:30:40   |- id: integer          VIEW
                    |- name: string
                    |- name_upper: string

impersonate_service_accountを設定したことで、dbtコマンド実行時は、roles/bigquery.jobUserroles/bigquery.dataEditorを付与されたdbt-runnerとして振る舞うようになったためである。

bqコマンド実行時は、引き続きデータにアクセスできない。

$ bq head sample-pj:src.user
BigQuery error in head operation: Access Denied: Table sample-pj:src.user: Permission bigquery.tables.getData denied on table sample-pj:src.user (or it may not exist).

$ bq head sample-pj:dest.user
BigQuery error in head operation: Access Denied: Table sample-pj:dest.user: Permission bigquery.tables.getData denied on table sample-pj:dest.user (or it may not exist).

dbtコマンドの実行時以外は権限借用が行われず、normal-userとしてコマンドを実行するためである。

Google Cloud のログにはどのように記録されるか

権限借用して dbt を実行した場合、 BigQuery のINFORMATION_SCHEMAには、dbt-runnerがジョブの実行者として記録される。

SELECT
  job_id,
  user_email,
  state
FROM
  `region-asia-northeast1`.INFORMATION_SCHEMA.JOBS
WHERE
  project_id = 'sample-pj'
  AND creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
  AND EXISTS (
    SELECT 1 FROM UNNEST(labels) AS label
    WHERE label.key = 'dbt_invocation_id'
  )
ORDER BY
  creation_time DESC
LIMIT 100
job_id user_email state
9ca4f964-4f7f-45eb-b6e0-39703193626b dbt-runner@sample-pj.iam.gserviceaccount.com DONE

Cloud Logging にはより詳細な情報が記録されており、「normal-userが権限借用した」ということが分かるようになっている。

$ gcloud logging read \
  'resource.type="bigquery_project" AND protoPayload.methodName="google.cloud.bigquery.v2.JobService.InsertJob" AND protoPayload.authenticationInfo.principalEmail="dbt-runner@sample-pj.iam.gserviceaccount.com"' \
  --project=sample-pj \
  --freshness=1d \
  --limit=1 \
  --format=json | jq '.[0].protoPayload.authenticationInfo'
{
  "oauthInfo": {
    "oauthClientId": "111552143688807246465"
  },
  "principalEmail": "dbt-runner@sample-pj.iam.gserviceaccount.com",
  "serviceAccountDelegationInfo": [
    {
      "firstPartyPrincipal": {
        "principalEmail": "normal-user@sample-pj.iam.gserviceaccount.com"
      }
    }
  ]
}

principalEmailが BigQuery のジョブを実行したプリンシパルで、serviceAccountDelegationInfoが権限借用に関する情報。
normal-userdbt-runnerの権限を借用しジョブを実行した、ということが読み取れる。

権限設計の選択肢が広がる

扱っているデータの内容にもよるが、 BigQuery のアクセス制御には求められる要件や考慮事項が多い。
データ利活用しやすいように、利活用者が便利に、かつ安全にデータを利用できるようになっていることが望ましい。
近年では AI を使ったデータ利活用のニーズも高まっており、アクセス制御はますます重要になっている。積極的に AI を利用していきたい一方で、データガバナンスやコンプライアンスといった観点における考慮事項が増加している。

今回の例の場合、normal-userdbtの実行時以外は BigQuery のデータにアクセスできない。
そのため以下のような権限設定をしたプロジェクトを用意して Claude Code を使えば、現在どのようなデータ構造なのか、どのようなテーブルがあるのか、といったことを Claude Code に調査させたり、目的のために今後どのようなテーブルやデータがあるとよさそうかを会話したり、といったことができる。

{
  "permissions": {
    "allow": [
      "Read(/**)",
      "Edit(/**)",
      "Write(/**)",
      "Bash(bq ls *)",
      "Bash(bq show *)"
    ]
  }
}

Claude Code にメタデータを渡すのは問題ないが実データは渡したくない、といった要件のときなどに使える。
Claude Code に調査をさせつつ、自分はdbtを使った開発を行う、ということが可能になる。

これはあくまでも説明のための例であり、ここまで単純なケースや要件は少ないかもしれない。
しかし、権限借用は便利な仕組みであり、カラムレベルでのアクセス制御など他のアクセス制御の手法と組み合わせることで、権限設計の幅が広がるはずである。