Azure Database for PostgreSQL Flexible Server で pg_cron を使い始めると、「cron.schedule_in_database(…) を実行した瞬間だけ permission denied になる」というハマり方をすることがあります。本記事では、この現象がなぜ起きるのかを Azure 固有の権限モデルと pg_cron の仕様から整理しつつ、実運用で採れる現実的な回避策を具体的な SQL 付きで解説します。GRANT を連打しても解決しない理由や、16 系で報告されている既知の問題への向き合い方もあわせてまとめます。
Azure PostgreSQL Flexible Server + pg_cron で起きる症状の整理
まずは、典型的に報告されている環境と症状を整理します。
| 項目 | 内容の例 |
|---|---|
| サービス | Azure Database for PostgreSQL – Flexible Server |
| PostgreSQL バージョン | 16 系(例: 16.9) |
| pg_cron のインストール先 | postgres データベース(既定) |
| サーバーパラメータ | cron.database_name = 'postgres'(メタデータ保持 DB) |
| 利用ユーザー | サーバー作成時の管理者アカウント(azure_pg_admin ロールのメンバー) |
| 実行したいジョブ | 別 DB(例: appdb)に対してメンテナンス SQL を定期実行したい |
| 呼び出し | SELECT cron.schedule_in_database(...); |
| 発生するエラー | ERROR: permission denied for function schedule_in_database |
補足として、次のような挙動も一緒に見られます。
SELECT has_function_privilege('admin_user', 'cron.schedule_in_database(text, text, text, text, text, boolean)', 'EXECUTE');がfalseを返すGRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA cron TO azure_pg_admin;を試すと、ERROR: permission denied for function alter_jobで失敗する- 拡張の再作成(DROP / CREATE EXTENSION)やユーザー作り直しでも改善しないケースがある
典型的なエラーログの例
-- ジョブの登録
SELECT cron.schedule_in_database(
'part_maint_TEST',
'0 * * * *',
'CALL partman.cron_maintain_partitions();',
'TEST'
);
ERROR: permission denied for function schedule_in_database
SQL state: 42501
-- 権限チェック
SELECT has_function_privilege(
'psqladminun',
'cron.schedule_in_database(text, text, text, text, text, boolean)',
'EXECUTE'
);
-- → false
また、Azure 公式 Q&A やフォーラムでも、PostgreSQL 16 系 Flexible Server で同様の schedule_in_database 権限エラーが複数報告されています。
Flexible Server 固有の権限モデルを押さえる
このエラーの背景には、「Flexible Server の管理者アカウントは SUPERUSER ではない」という前提があります。
3 つのデフォルトロール
Azure Database for PostgreSQL – Flexible Server では、サーバー作成時に次の 3 つのロールが用意されます。
| ロール | 概要 | ユーザーからアクセス可? |
|---|---|---|
azure_pg_admin | ユーザーが利用できる最上位の管理ロール。DB 作成やロール作成、ほとんどの管理操作が可能だが、SUPERUSER ではない。 | ◯(サーバー管理者ユーザーはこのメンバー) |
azure_superuser / azuresu | 実際の PostgreSQL SUPERUSER 権限を持つ内部ロール。サービスの制御プレーン用に予約されており、ユーザーからは利用不可。 | ✕(Microsoft 管理用のみ) |
| サーバー管理者ユーザー | サーバー作成時に指定した管理アカウント。azure_pg_admin ロールのメンバーとして作成される。 | ◯ |
公式ドキュメントにも、サーバー管理者は azure_pg_admin のメンバーだが、SUPERUSER である azure_superuser/azuresu には属さず、ユーザーは SUPERUSER を新たに作成できないと明記されています。
なぜ GRANT や ALTER が通らないのか
Flexible Server 上で pg_cron を有効化すると、多くのケースで拡張は内部ロール(azuresu 等)所有のオブジェクトとして作成されます。そのため、cron スキーマ配下の関数やテーブルに対し、ユーザー側から所有権変更や任意の GRANT を実行することはできません。実際、Azure で pg_cron を使っているユーザーからも「拡張が azuresu 所有で、他ユーザーは cron スキーマやテーブルを更新できない」といった報告があります。
その結果、次のような操作は 設計上できません。
ALTER FUNCTION cron.schedule_in_database ... OWNER TO ...;GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA cron TO azure_pg_admin;(途中でalter_jobなどに対して permission denied)UPDATE cron.job ...;でジョブ定義そのものを書き換える(権限的にも仕様的にも非推奨)
ここを「なんとか GRANT したい」と頑張っても、Flexible Server の権限モデル上ほぼ行き止まりです。そこで重要になるのが、Azure がサポートしている pg_cron の使い方に寄せるという発想です。
pg_cron と cron.database_name の動き
次に、pg_cron の基本仕様と Azure 上での動きを確認しておきます。
- pg_cron は
cronスキーマにcron.job,cron.job_run_detailsなどのメタデータを作成する拡張です。 - メタデータを保持するデータベースはサーバーパラメータ
cron.database_nameで決まり、既定はpostgresです。 - pg_cron はクラスタ内の 1 つのデータベースにしかインストールできず、他 DB でジョブを実行したい場合は
cron.schedule_in_databaseを使うように設計されています。
つまり、Flexible Server では概ね次のような構成になります。
(1)pg_cron のメタデータ DB:postgres
├─ 拡張 pg_cron がインストールされている
└─ cron.job / cron.job_run_details テーブルが存在
(2)アプリケーション DB:appdb など
├─ 実際にテーブルや関数が存在
└─ ジョブは appdb に対して実行したい
この構成で cross-DB ジョブを動かすための専用 API が cron.schedule_in_database です。
問題の核心:schedule_in_database と username 引数
cron.schedule_in_database は、pg_cron 1.4 以降で追加された関数で、シグネチャ(簡略版)は次のようになっています。
cron.schedule_in_database(
job_name text,
schedule text,
command text,
database text,
username text DEFAULT NULL,
active boolean DEFAULT TRUE
)
Azure の公式ドキュメントでは、この関数について次のように説明されています。
username引数は「任意」だが、NULL 以外の値を指定するには PostgreSQL SUPERUSER 権限が必要。- Flexible Server では SUPERUSER を使えないため、username に非 NULL を指定する呼び出しはサポートされない。
- username を 省略するか NULL にすると、「ジョブを登録したユーザーの権限」でジョブが実行される。
この仕様を踏まえると、次のような呼び出しは Azure では NG です。
-- ★NG例: username に 'postgres' を指定している
SELECT cron.schedule_in_database(
'nightly_cleanup',
'0 3 * * *',
$$DELETE FROM app.public.logs ...$$,
'appdb',
'postgres', -- ← SUPERUSER 相当のユーザー名を明示
TRUE
);
このように username に非 NULL を指定すると SUPERUSER 権限が要求されますが、azure_pg_admin は SUPERUSER ではないため、Flexible Server では permission denied for function schedule_in_database が発生します。
16 系で報告されている既知の権限制御バグ
さらにややこしいことに、PostgreSQL 16 系の Flexible Server では、username を省略または NULL にしているにも関わらず permission denied になる という事例も報告されています。
- pg_cron 拡張・
cronスキーマは正しく作成されている cron.scheduleで同一 DB 内のジョブ登録はできる- しかし
cron.schedule_in_databaseを呼ぶとpermission denied GRANT ALL ON ALL FUNCTIONS IN SCHEMA cron TO azure_pg_admin;などを試みても、途中でalter_jobなどに対する権限不足で失敗
Microsoft Q&A では、これが サービス側のデプロイに起因する既知の問題 であり、サポートチケットを通じてバックエンドのロール/権限設定を修正することで解消したと報告されています。
したがって、
- 正しい呼び出し方(username 省略 or NULL)をしてもエラーになる
- 他の pg_cron 関数(
cron.scheduleなど)は動いている
という場合は、後述の「やるべきこと」を試したうえで、Azure サポートに問い合わせるのが現実的な落としどころになります。
まず試すべき解決策:username を NULL/省略して呼び出す
前提が整理できたところで、実際の回避策を見ていきます。最初に試すべきは、username を NULL(または省略)した上で、pg_cron メタデータ DB から cron.schedule_in_database を呼び直す方法です。
基本パターン(4 引数版)
Azure 公式ドキュメントのサンプルにもあるように、username と active を省略した 4 引数版が最もシンプルです。
-- postgres データベース(cron.database_name が指す DB)に接続して実行
SELECT cron.schedule_in_database(
'nightly_cleanup', -- ジョブ名
'0 3 * * *', -- 毎日 03:00 (UTC)
$$DELETE FROM app.public.logs
WHERE created_at < now() - interval '90 days';$$,
'appdb' -- 実行先データベース
);
この呼び出しでは username が内部的に NULL と見なされ、ジョブは「登録したユーザー」の権限で appdb 上の SQL を実行します。Azure のドキュメントでも、この形が推奨されています。
active フラグも指定したい場合(6 引数版)
ジョブを登録だけして一旦無効化したい/明示的に有効フラグを立てたい場合は、username に NULL を渡す形で 6 引数版を使えます。
SELECT cron.schedule_in_database(
'nightly_cleanup',
'0 3 * * *',
$$DELETE FROM app.public.logs
WHERE created_at < now() - interval '90 days';$$,
'appdb',
NULL, -- ★ここを NULL にするのがポイント
TRUE -- ジョブを有効化
);
OK / NG パターン早見表
| 呼び出し例 | username 引数 | 必要権限 | Flexible Server での扱い |
|---|---|---|---|
cron.schedule_in_database('job', '0 3 * * *', 'VACUUM', 'appdb') | 省略(内部的には NULL) | azure_pg_admin(または相当ロール) | ◯ サポート対象 |
cron.schedule_in_database('job', '0 3 * * *','VACUUM','appdb', NULL, TRUE) | NULL | azure_pg_admin(または相当ロール) | ◯ サポート対象 |
cron.schedule_in_database('job','0 3 * * *','VACUUM','appdb','postgres',TRUE) | 'postgres' など非 NULL | SUPERUSER | ✕ 非サポート(permission denied になる) |
「必ず cron.database_name の DB から呼ぶ」を忘れない
Azure のサーバーパラメータ一覧にもある通り、cron.database_name は「pg_cron のメタデータを保持するデータベース」を表します。
- ほとんどの環境では既定値の
postgresのまま - この値と一致する DB に pg_cron のメタデータ(
cron.jobなど)が作成される - schedule_in_database は、このメタデータ DB に接続した状態で呼び出す必要がある
そのため、次のような誤りにも注意が必要です。
-- ★NGパターン: appdb に接続した状態で schedule_in_database を呼ぶ
-- (pg_cron 自体は postgres DB にしか入っていない)
\c appdb
SELECT cron.schedule_in_database(...); -- → 拡張がなくエラー or 思わぬ挙動
基本は、
cron.database_nameの値を確認する- その DB(既定は
postgres)に接続する - username を省略 or NULL にして
cron.schedule_in_databaseを呼ぶ
という流れを守っておけば、権限モデルの範囲内で cross-DB ジョブを安全に登録できます。
代替手段 A:対象 DB に拡張を作り、cron.schedule を直接使う
「そもそも cross-DB 実行をやめてしまう」という割り切りも、有力な選択肢です。Azure では、使いたいデータベースに pg_cron をインストールして、そこで cron.schedule を使う方式も推奨されています。
構成イメージ
postgres appdb
--------- -------------------
pg_cron なし pg_cron 拡張あり
├─ cron.job
└─ cron.job_run_details
この構成では、ジョブは appdb 内で完結します。
-- appdb に接続
\c appdb
-- 必要に応じて allowlist 済みであることを確認した上で拡張を作成
CREATE EXTENSION IF NOT EXISTS pg_cron;
-- 毎日 03:00 (UTC) に古いログを削除
SELECT cron.schedule(
'nightly_cleanup',
'0 3 * * *',
$$DELETE FROM app.public.logs
WHERE created_at < now() - interval '90 days';$$
);
注意点として、pg_cron はクラスタ内 1 DB にしかインストールできないため、すでに postgres にインストール済みの場合は一度 DROP してから appdb に作り直す、といった作業が必要になります。
代替手段 B:SECURITY DEFINER ラッパー関数でアプリ権限不足を吸収する
ここまでの話は「pg_cron の関数それ自体への EXECUTE 権限」の問題でした。別のパターンとして、
- pg_cron 経由で SQL を実行すると、アプリケーションテーブルへの権限不足で失敗する
というケースもあります。この場合は、pg_cron 側ではなく ジョブ本体の SQL 側の権限が問題です。
そのとき有効なのが、対象 DB 側で SECURITY DEFINER なラッパー関数を作り、ジョブからはその関数だけを呼ぶ方式です。
-- appdb に接続し、十分な権限を持つ所有者で実行
CREATE SCHEMA IF NOT EXISTS maintenance;
CREATE OR REPLACE FUNCTION maintenance.purge_old_logs()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
DELETE FROM app.public.logs
WHERE created_at < now() - interval '90 days';
END;
$$;
-- 必要に応じて search_path を固定しておくとより安全
ALTER FUNCTION maintenance.purge_old_logs()
SET search_path = maintenance, public;
この関数を pg_cron から呼び出すようにします。
-- postgres (cron.database_name の DB) に接続
SELECT cron.schedule_in_database(
'purge_old_logs',
'0 3 * * *',
$$SELECT maintenance.purge_old_logs();$$,
'appdb',
NULL,
TRUE
);
こうすることで、ジョブは maintenance.purge_old_logs を所有しているロールの権限で実行されるため、アプリケーションユーザー単体では不足している権限もカバーできます。ただし繰り返しになりますが、これは「pg_cron が実行する SQL の権限問題」の解消であり、「pg_cron の関数 (schedule_in_database など) 自体の実行権限不足」は別問題です。
運用時にチェックしておきたいポイント
ジョブ一覧と履歴を確認する
-- 登録済みジョブの一覧
SELECT * FROM cron.job ORDER BY jobid DESC;
-- 最近の実行履歴(20 件)
SELECT *
FROM cron.job_run_details
ORDER BY start_time DESC
LIMIT 20;
pg_cron の README でも案内されている通り、cron.job にはジョブ定義、cron.job_run_details には実行履歴が保存されます。
ジョブの解除・再登録
不要になったジョブや、schedule_in_database で登録し直したい場合は cron.unschedule を使います。
-- jobid を指定してジョブを解除
SELECT cron.unschedule(123);
-- jobname で解除することも可能
SELECT cron.unschedule('nightly_cleanup');
cron 関連パラメータを確認する
どの DB がメタデータ DB になっているか、どのタイムゾーンでスケジューリングされているか、といった情報は pg_settings から確認できます。
SELECT name, setting
FROM pg_settings
WHERE name LIKE 'cron.%'
ORDER BY name;
特に意識したいのは次のパラメータです。
| パラメータ | 意味 |
|---|---|
cron.database_name | pg_cron のメタデータを保持する DB 名(既定: postgres) |
cron.timezone | スケジュールの基準タイムゾーン(既定: GMT / UTC) |
cron.log_run | cron.job_run_details に実行履歴を記録するかどうか |
Azure のサンプルでは「毎日 10:00 (GMT) に VACUUM を実行」といった形で UTC 基準の cron 式が紹介されているため、必要に応じて cron.timezone やアプリ側のタイムゾーンを考慮しましょう。
よくある落とし穴とアンチパターン
cron.job を直接 UPDATE して database を書き換える
フォーラム等では、次のような操作に挑戦して権限エラーになっている例が見られます。
UPDATE cron.job SET database = 'newdb' WHERE jobid = 1;
しかし、pg_cron の設計上、ジョブの変更は cron.alter_job や cron.unschedule / cron.schedule_in_database で行うことが想定されています。内部テーブルを直接更新するのは推奨されませんし、Azure では内部ロール所有のため権限的にもブロックされることが多いです。
cron スキーマ配下に GRANT/ALTER をかけようとする
先述の通り、pg_cron の関数やテーブルは内部ロール(azure_superuser/azuresu)所有で作られ、azure_pg_admin から所有権変更や任意の GRANT を行うことは基本的にできません。
GRANT USAGE ON SCHEMA cron TO azure_pg_admin;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA cron TO azure_pg_admin;
-- ⇒ 一部の関数で WARNING / ERROR: permission denied for function alter_job
この方向での対処は、Flexible Server の権限設計とぶつかってしまうため、
- username を NULL / 省略して
cron.schedule_in_databaseを使う - もしくは対象 DB に拡張をインストールして
cron.scheduleで完結させる
という「Azure の想定するパターンに寄せる」方が、長期的にも安定します。
16 系の既知バグを疑うべきケース
最後に、どうしても以下の条件をすべて満たすのに permission denied が解消しない場合は、サービス側の既知の問題を疑ってよい場面です。
cron.schedule_in_database(...)で username を省略または NULL にしているcron.scheduleで同一 DB 内ジョブは登録・実行できている- pg_cron のバージョンや allowlist 設定に問題がない
Microsoft Q&A では、PostgreSQL 16.4 の Flexible Server で同様の問題が発生し、Azure サポート経由でバックエンドの権限設定を修正することで解決した事例が紹介されています。2024 年末時点の回答では「既知の問題であり、今後のリリースで修正を展開する」とも記載されているため、古いタイミングで作成したサーバーでのみ症状が残っている可能性も考えられます。
ケース別:どう動けばよいかのフローチャート
| ケース | やりたいこと | 推奨アクション |
|---|---|---|
| Case A | postgres から別 DB(appdb 等)に対してジョブを実行したい | cron.database_name を確認(既定は postgres)。 その DB に接続し、username を省略または NULL にして cron.schedule_in_database を呼び出す。 それでも permission denied なら、PG16 系の既知バグの可能性を疑い Azure サポートに問い合わせ。 |
| Case B | 1 つの DB だけでジョブを完結させたい | 必要であれば pg_cron をその DB にインストールし直す。 cron.schedule のみでジョブを登録する構成に切り替える。 |
| Case C | ジョブは登録できるが、SQL 本体が権限不足で失敗する | 対象 DB 側に SECURITY DEFINER ラッパー関数を作成。 ジョブからはラッパー関数だけを呼ぶように定義し直す。 |
すぐ使える最小サンプル(cross-DB / username 省略版)
最後に、Flexible Server で実際に動かしやすい「最小サンプル」を載せておきます。
-- 1. postgres データベースに接続
\c postgres
-- 2. pg_cron 拡張がインストールされていることを確認
CREATE EXTENSION IF NOT EXISTS pg_cron;
-- 3. cron.database_name をチェック(必要なら postgres に合わせる)
SELECT name, setting
FROM pg_settings
WHERE name = 'cron.database_name';
-- 4. appdb で VACUUM を毎日 10:00 (UTC) に実行するジョブを登録
SELECT cron.schedule_in_database(
'daily_vacuum',
'0 10 * * *',
'VACUUM',
'appdb', -- 実行先 DB
NULL, -- SUPERUSER 不要な呼び出しにする
TRUE
);
ここまでのポイントを押さえておけば、Azure PostgreSQL Flexible Server 上で pg_cron を「Azure が想定している方法」で安全に活用できるはずです。
まとめ:GRANT で殴らず、「Azure 流」に寄せるのが近道
- Flexible Server の管理者ロール
azure_pg_adminは SUPERUSER ではない。SUPERUSER 相当のロールはサービス内部用で、ユーザーからは利用できない。 - pg_cron の
cronスキーマ配下のオブジェクトは内部ロール所有のため、所有権変更や任意の GRANT はできない。この方向での解決策は基本的に行き止まり。 cron.schedule_in_databaseは username を省略または NULL にして使うのが Azure 公式の推奨であり、username に値を入れると SUPERUSER 権限が要求されて permission denied になる。- cross-DB 実行が不要なら、対象 DB に pg_cron をインストールして
cron.scheduleのみで完結させる構成もシンプルで運用しやすい。 - ジョブ本体の SQL が権限不足になる場合は、SECURITY DEFINER ラッパー関数で権限を肩代わりさせるのが定石。
- 正しい呼び出し方でもなお
permission denied for function schedule_in_databaseが出る場合は、PG16 系で報告されている既知の権限問題の可能性があるため、Azure サポートに問い合わせてバックエンド側の権限状態を確認してもらうとよい。
「GRANT でなんとかする」ではなく、「Azure のマネージドサービスとしての制約を前提に、その中で一番シンプルな構成に寄せていく」ことが、pg_cron を長期的に安定運用するための近道です。

コメント