BigQueryでServiceAccountに成り代わってクエリを実行する

# はじめに

この記事は[🎄GMOペパボ エンジニア Advent Calendar 2024](https://adventar.org/calendars/10317)の18日目の記事です。
なんか[BigQuery Advent Calendar 2024](https://qiita.com/advent-calendar/2024/bigquery) の 12 日目の枠も空いてたので、そこにも入れました。

# 追記

`CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT` という環境変数を使って Google Cloud 公式 CLI ツール `bq` で権限借用の状態でクエリ出来るとタレコミがあり、確認したところ使えましたので、それを使った方が良いです。

https://x.com/tosh2230/status/1871132483993186324

# 追記２

ドキュメントに確認しにいったら、どうやら公式では `gcloud config set auth/impersonate_service_account SERVICE_ACCOUNT_NAME` で認証して `bq` コマンドを使うという想定でした。
https://cloud.google.com/bigquery/docs/bq-command-line-tool?hl=ja#bq_service_account
ただ、いちいち `gcloud config unset auth/impersonate_service_account` をするのも面倒ですし、環境変数を使ってインラインで権限借用したほうが便利だと思います。

# 課題

BigQuery をバッチから扱う際、ServiceAccount に権限を付与しておいてバッチの実行時ユーザーにするのがよくあるパターンだと思います。ただ、ServiceAccount にちゃんと権限が付いたかどうかを確認するの、地味に面倒なんですよね。ServiceAccountの鍵を生成して手元に持ってきたりとか、あんまりしたくないじゃないですか。そこで、それを簡単にする方法をご紹介します。

# Service Account Impersonation
[サービス アカウントの権限借用](https://cloud.google.com/docs/authentication/use-service-account-impersonation?hl=ja)という機能を使うと対象 ServiceAccount の権限を簡単にふるうことができるようになります。`gcloud auth print-access-token --impersonate-service-account=hoge@example.iam.gserviceaccount.com` みたいなコマンドで一時的に使える鍵が生成されます。これを使うには対象の ServiceAccount に対する `roles/iam.serviceAccountTokenCreator` が必要ですので、適宜つけておきましょう。ちゃんとやるなら [PAM](https://cloud.google.com/iam/docs/pam-overview?hl=ja) を使って一時的に権限昇格する仕組みを活用するのもいいでしょう。

# 実践
下記のようなスクリプトを `bq_query_as` みたいな名前で path が通ったところに置いておけば捗ります。`gem install` 無しで動くという縛りで書いているので、`print-access-token` は `gcloud` コマンドの呼び出しで実現しています。

```rb
#!/usr/bin/env ruby
require 'net/http'
require 'uri'
require 'open3'
require 'json'

PROJECT = ENV['GOOGLE_PROJECT']
raise("ENV['GOOGLE_PROJECT'] が必要") unless PROJECT
ENDPOINT = URI.parse("https://bigquery.googleapis.com/bigquery/v2/projects/#{PROJECT}/queries")

service_account, query = ARGV
raise "なんかヤバい: #{service_account}" unless URI::MailTo::EMAIL_REGEXP.match?(service_account)

command = "gcloud auth print-access-token --impersonate-service-account=#{service_account}"
_, access_token, _, _ = *Open3.popen3(command)
header = {
  'Authorization' => "Bearer #{access_token.read.chomp}",
  'Content-Type' => 'application/json'
}
body = {
  query: query,
  useLegacySql: false
}

http = Net::HTTP.new(ENDPOINT.host, ENDPOINT.port)
http.use_ssl = true
request = Net::HTTP::Post.new(ENDPOINT.request_uri, header)
request.body = body.to_json

response = http.request(request)
puts response.body
```
実行の仕方の例↓。`SELECT` `INSERT` `UPDATE` `DELETE` など、確認したい権限に応じた SQL を投げてやれば、権限エラーが出ないかどうかだけでも確認ができます。
```sh
$ bq_query_as batch-user@example.iam.gserviceaccount.com "SELECT 1 AS n"
```

このように impersonate で実行すると、ログには誰が誰の権限を使って実行したかが出てくるので、後々、必要に応じて監査することもできます。

```json
"authenticationInfo": {
  "principalEmail": "batch-user@example.iam.gserviceaccount.com"
  "serviceAccountDelegationInfo": [
    {
      "firstPartyPrincipal": {
        "principalEmail": "yancya@nantyara.com"
      }
    }
  ]
}
```

# 本当は

出来れば、↑みたいなお手製のツールではなく、`bq query --impersonate-service-account=...` とか `gcloud bq query --impersonate-service-account=...` が存在してほしいですね。今後、もしそれらが出てきたらそれを使います。