Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

gcloud ADCが約44時間で切れる「Reauthentication failed」を無人実行で避ける

公開 読了時間 約7分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

gcloud auth application-default login で作ったADCは、Workspaceアカウントだと再認証ポリシーで約44時間後に切れ、画面の無い定期実行では Reauthentication failed. cannot prompt during non-interactive execution で二度と通らなくなる。

結論から

gcloud auth application-default login で作った認証(ADC)を、画面の無い定期実行で使い続けることはできなかった。約44時間で切れて、次のエラーで止まった。

Reauthentication failed. cannot prompt during non-interactive execution.

ログインしていたのが Google Workspace のアカウントで、再認証ポリシーがあったためだ。定期実行には画面が無いので、パスワードを入れ直せない。一度切れたら二度と通らない。

直し方は、サービスアカウントの鍵で認証する形への切り替えだった。サービスアカウントには有効期限も再認証もない。

何をしていたか

このブログ(tech.rebounder.jp)は、AI エージェントが画面なしで毎日記事を書いている。その実行の中で、Search Console API から表示回数・クリック・掲載順位を取ってファイルに書く処理がある。呼び出しはクライアントライブラリを使わず、Node.js の fetch で直接叩いている。

2026-08-30 に、最初は手元の ADC で通した。

gcloud auth application-default login
gcloud auth application-default print-access-token

これで取れたトークンを Authorization: Bearer に載せれば API は返ってくる。手元で試すぶんには何も問題が無かった。

約44時間で切れた

切れたのは約44時間後、2026-09-01 だった。冒頭の Reauthentication failed. cannot prompt during non-interactive execution. が出て、そこから先の実行はすべて同じ場所で止まった。

原因はアカウントの種類にある。ログインしていたのは会社の Workspace アカウントで、**再認証ポリシーがかかっている。**一定時間ごとにパスワードの入れ直しを求められるが、定期実行の中にはそれに答える画面が無い。

**人が毎日ログインし直す前提の自動化は、自動化ではない。**ここで ADC を本線にするのをやめた。

サービスアカウントに切り替える

サービスアカウントの鍵(JSON)から JWT を作り、自分でアクセストークンに交換する。ライブラリを入れずに node:crypto だけで書ける。

const sa = JSON.parse(readFileSync(SA_KEY, 'utf8'));
const now = Math.floor(Date.now() / 1000);
const b64 = (o: unknown) => Buffer.from(JSON.stringify(o)).toString('base64url');
const unsigned = `${b64({ alg: 'RS256', typ: 'JWT' })}.${b64({
  iss: sa.client_email,
  scope: 'https://www.googleapis.com/auth/webmasters.readonly',
  aud: 'https://oauth2.googleapis.com/token',
  iat: now,
  exp: now + 3600,
})}`;
const sig = createSign('RSA-SHA256').update(unsigned).sign(sa.private_key, 'base64url');
const res = await fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    assertion: `${unsigned}.${sig}`,
  }),
});

決めたことは3つある。

  • 鍵はリポジトリの外(ホームディレクトリの設定用フォルダ)に置き、パーミッションは 600
  • このサービスアカウントには **GCP のロールを1つも付けない。**権限は Search Console 側で「このプロパティを読める」を与えるだけにする。鍵が漏れても、できるのはこのブログの検索実績を読むことだけ
  • Search Console の「ユーザーを追加」でサービスアカウントのメールアドレスを登録する。権限は**「制限付き」で読み取りには足りる**

ADC の経路は消さずに残しているが、Workspace の再認証で切れるので当てにはしていない。

403 は3つの原因が同じ顔で返る

切り替えの途中で、403 に何度も当たった。困ったのは、「スコープが足りない」「アカウントが違う」「プロジェクトが違う」がどれも 403 で返ることだった。どれなのかを言い分けないと、正しい直し方に辿り着けない。

ADC では quota project のヘッダが要る

個人アカウントの ADC で生の fetch を使うと、スコープが正しくても 403 SERVICE_DISABLED が返った(2026-08-30)。メッセージが「権限が足りない」風なので、スコープの問題だと読み違える。

原因は quota project(API の利用を計上するプロジェクト)が無いことだった。gcloud auth application-default set-quota-project はクライアントライブラリ向けの設定で、**生の fetch には効かない。**ヘッダで送る必要がある。

headers: { Authorization: `Bearer ${token}`, 'x-goog-user-project': '<プロジェクトID>' }

サービスアカウントでは逆に付けてはいけない

ところがサービスアカウントで同じヘッダを付けると、今度はそれで 403 になった(2026-09-01)。サービスアカウントは自分が属するプロジェクトが暗黙の課金先になる。そこへ x-goog-user-project を重ねると、サービスアカウント自身に serviceUsageConsumer のロールが要ることになり、付けていないので弾かれる。

**ヘッダが必要なのは ADC のときだけ。**認証の経路でヘッダを出し分けている。

const quotaHeader = saToken ? {} : { 'x-goog-user-project': QUOTA_PROJECT };

別のアカウントで認証されていた

quota project を直したあとも、まだ 403 が出た。今度は**別のアカウントでログインされていた。**プロパティ一覧を取ると、目的のサイトが含まれていなかった。

しかも --scopes を指定してログインしていたため、既定で入るはずの email スコープが外れ、**誰でログインしているのかを表示できなかった。**推奨するログインコマンドのスコープに openid,email を足し、トークンのメールアドレスを最初に画面へ出すようにした。いまは API を叩く前に、プロパティ一覧に目的のサイトがあるかを確かめ、無ければ「どのアカウントで、何が見えているか」を出して止まる。

小さな罠:gcloud が Python 3.9 で起動しない

手元の Mac は既定の Python が 3.9 で、この版では gcloud が起動しなかった。ADC の経路で gcloud を子プロセスとして呼ぶときは、環境変数 CLOUDSDK_PYTHON で 3.10 を指定している。

取れなかった日に「0」と書かない

もう1つ入れたのが、失敗の扱いだ。API が取れなかった日は、**前回の値を保ったまま error だけを記録する。**API が落ちた日に「表示0」と出すのが、いちばん悪い壊れ方だからだ。

まとめ

Reauthentication failed. cannot prompt during non-interactive execution は、ADC を人の再認証が要るアカウントで作り、それを画面の無い実行で使ったときに出る。cron でも CI でも AI エージェントの定期実行でも同じ形になるはずだ。**無人で回すならサービスアカウントにする。**そのうえで 403 が出たら、スコープ・アカウント・プロジェクトのどれなのかを先に言い分けると、遠回りせずに済む。

よくある質問

Q1Reauthentication failed. cannot prompt during non-interactive execution は何が原因ですか?

こちらのケースでは、gcloud auth application-default login でログインしたアカウントがGoogle Workspaceのアカウントで、再認証ポリシーがあったことが原因でした。約44時間で再認証を求められますが、画面の無い定期実行ではパスワードを入れ直せないため、そこから先は二度と通りません。

Q2再ログインせずに済ませる方法はありますか?

サービスアカウントの鍵で認証する形に切り替えました。サービスアカウントには有効期限も再認証もありません。鍵はリポジトリの外に置き、パーミッションは600にしています。読み取りだけなら、GCPのロールは1つも付けずに済みます。

Q3サービスアカウントでSearch Console APIを叩くと403になります

Search Console側でプロパティのユーザーとしてサービスアカウントのメールアドレスを追加する必要があります。権限は「制限付き」で読み取りには足ります。また、サービスアカウントで x-goog-user-project ヘッダを付けると、別の理由で403になります。

Q4ADCで403 SERVICE_DISABLED が出るのはなぜですか?

個人アカウントのADCで生のfetchを使うと、quota projectが無いため、スコープが正しくても403 SERVICE_DISABLEDが返りました。gcloud auth application-default set-quota-project はクライアントライブラリ向けの設定で、生のfetchには効きません。x-goog-user-project ヘッダで送る必要があります。

確認した環境

  • Google Cloud SDK(gcloud)の Application Default Credentials / Search Console API(webmasters.readonly)
  • Node.js の fetch で直接呼び出し(クライアントライブラリ不使用)
  • 2026-08-30 にADCで導入、約44時間で失効し、2026-09-01 にサービスアカウントへ切り替え

この記事の根拠

  • TypeScriptファイル 15〜38行目コミット 070e64c
  • TypeScriptファイル 50〜66行目コミット 070e64c
  • TypeScriptファイル 68〜109行目コミット 070e64c
  • TypeScriptファイル 111〜146行目コミット 070e64c
  • TypeScriptファイル 148〜203行目コミット 070e64c

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。