本記事は Akatsuki Games Advent Calendar の2日目の記事です。
Elixir の Google Cloud サービスアカウント認証
アカツキゲームスの一部のゲームタイトルでは Elixir をサーバー開発言語として利用しています。
BigQuery 等のサービスを利用する際、Google Cloud のユーザー認証が必要になりますが、GitHub Actions などの CI からサービスにアクセスするような場合、Workload Identity を利用してサービスアカウントの認証が行えるように構成するのがベストプラクティスとされています。これは、秘密鍵の管理が不要になりセキュリティの向上が図れるためです。
参考: Workload Identity Federation | IAM Documentation | Google Cloud
さて、Elixir からサービスアカウントの認証を行う場合には、 Goth というライブラリが利用されることが多く、 Google API を呼び出すためのライブラリ GoogleApis の README でも Goth を使用してトークンを取得する手順が記載されています。
この Goth ですが、現在未リリースではあるものの、 master ブランチでは Workload Identity を利用可能にする Pull Request がマージされています。本記事ではこれを利用してみたので、その方法を紹介します。
Workload Identity の設定
今回は GitHub Actions から利用したかったので、「他の ID プロバイダとの Workload Identity 連携を構成する」の手順で設定を行いました。
Workload Identity Pool を作成し、そこにプロバイダとして GitHub を追加します。外部 IdP としては OIDC を利用します。そのほか設定に必要な「発行元(URL)」等の情報は GitHub のドキュメントを参照しながら進めます。
Google Cloud Platform での OpenID Connect の構成 - GitHub Docs
属性マッピングには 「OpenID Connect を使ったセキュリティ強化について - GitHub Docs」の「Understanding the OIDC token」にある値が使用でき、これを利用して認証が行えるレポジトリやワークフローを制限することができます。
ここでは一例として、以下のようにマッピングとアクセス制限を設定しました。
加えて利用したいサービスアカウントに対して、プールの画面にある「アクセスを許可」→「サービス アカウントの権限借用を使用してアクセス権を付与する」から利用できるように設定を行います。この時も「プリンシパルの選択」で「フィルタに一致するIDのみ」を選択し、条件として先ほどのマッピングを利用して、例えば attribute.repository が 「org名/repository名」であるなどといった制限を指定しておくことが推奨されています。
最後に、プールの「接続済みサービス アカウント」から認証情報の構成が記述されたファイルをダウンロードします。この時に、「OIDC ID tokenのパス」というものを入力する必要がありますが、これは後の Actions でトークンの受け渡しに使うファイルパスを記入します。(以下では仮に /tmp/token としますが、あまり /tmp は使わないほうがいいかもしれません。)
ダウンロードした構成ファイルのパスを環境変数 GOOGLE_APPLICATION_CREDENTIALS で渡すか、その中身のテキストを GOOGLE_APPLICATION_CREDENTIALS_JSON で渡すことで、Goth が Workload Identity を利用できるように設定されます。
GitHub Actions でのトークン取得
OIDC ID トークンは、GitHub Actions で permission として id-token: write を指定すると有効化される環境変数を使って、以下のようにcurlで取得できます。
取得したトークンをファイルとして上記で指定した「OIDC ID tokenのパス」に配置して、 Goth を利用した Elixir アプリケーションを呼び出すことで、このトークンをサービスアカウントのトークンと交換して認証が可能になります。
name: goth-test on: push: permissions: id-token: write contents: read jobs: test: runs-on: ubuntu-latest container: hexpm/elixir:1.17.3-erlang-27.0.1-alpine-3.20.3 steps: - name: Package Install run: | apk update apk add bash tar curl git openssh jq - name: Checkout code uses: actions/checkout@v4 - name: Get Id Token run: | AUDIENCE=$(echo "$GOOGLE_APPLICATION_CREDENTIALS_JSON" | jq -r .audience) curl -sf -m 10 -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$AUDIENCE" | jq -r .value > /tmp/token - name: Execute application run: ... # Elixir アプリケーションを呼び出す
他のscopeを利用する場合
Google Cloud のサービスを利用する場合には上記で大丈夫なのですが、Google Drive 等の他のサービスを利用したい場合には scope の指定が必要になります。
残念ながら現状の Goth では Workload Identity 用の scope がデフォルトのGCP用に固定されてしまっていました。
そこで、scope を指定可能にするための Pull Request を出しました。
Pull Request #178: Support the scopes option in workload identity
これを利用すると、例えば以下のようにアプリケーションを初期化することで指定したスコープのトークンを取得できるようになります。
defmodule MyApp.Application do use Application def start(_type, _args) do credentials = "GOOGLE_APPLICATION_CREDENTIALS_JSON" |> System.fetch_env!() |> Jason.decode!() source = {:workload_identity, credentials, scopes: [ "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/drive" ]} children = [ {Goth, name: MyApp.Goth, source: source} ] Supervisor.start_link(children, strategy: :one_for_one) end end
token = Goth.fetch!(MyApp.Goth)
まとめ
本記事では Elixir で Workload Identity を利用して Google Cloud のサービスアカウントの認証を行う方法を紹介しました。
Goth の Workload Identity 機能はまだtrunkに入ったばかりで、他にも curl を使わずに直接指定URLからトークンを取得する機能の Pull Request などが提案されているようです。*1 今後の進展が気になります。
*1:多分私のPull Requestとコンフリクトしてしまいそうですが…