LiteLLM ProxyのRDSパスワード認証からIAM認証に切り替えてパスワードローテーションに強くしてみた

LiteLLM ProxyのRDSパスワード認証からIAM認証に切り替えてパスワードローテーションに強くしてみた

2026.05.26

こんにちは。クラウド事業本部コンサルティング部の桑野です。

以前、LiteLLM ProxyをAmazon ECS on FargateにTerraform + ecspressoでデプロイする構成を共有しました。

https://dev.classmethod.jp/articles/litellm-proxy-on-fargate/

その後あらためて構成を眺めていてとあることに気が付きました。
ECSのsecretsコンテナ定義はタスク起動時に一度だけ環境変数へ展開される仕様なため、シークレットが変更された場合は、新しいデプロイを強制するか、新しいタスクを起動して最新のシークレット値を取得する必要があります。
RDSのDB認証情報を格納しているSecrets Managerのパスワード自動ローテーションを有効にすると、ローテーションが起こった際、実行中のタスクは古いパスワードを保持し続けることになります。
つまり、ローテーションが起こるとLiteLLM ProxyはRDSに接続することができなくなってしまいます。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/specifying-sensitive-data.html

今回はその構造的な課題をRDS IAM database authenticationへの移行で解消し、パスワードが変わってもLiteLLM Proxyが稼働し続ける状態を確認できたので共有します。

前回構成の課題

前回の構成では、ECSタスクがRDSのマスターパスワードをSecrets Manager経由で環境変数に注入していました。

"secrets": [
  { "name": "DATABASE_USERNAME", "valueFrom": "<rds_master_user_secret_arn>:username::" },
  { "name": "DATABASE_PASSWORD", "valueFrom": "<rds_master_user_secret_arn>:password::" }
]

シンプルで分かりやすい一方、ECSのsecretsはタスク起動時に一度だけ環境変数へ展開される、という挙動が運用上の落とし穴になります。

  • RDSのmanage_master_user_password = trueで自動ローテーションを有効にしていると、Secrets Manager側のパスワードは定期的に書き換わる
  • しかし実行中のECSタスクは古いパスワードを保持し続ける
  • ローテーションが実行された瞬間に新しいパスワードでRDS側が応答するようになり、タスク側はAuthentication failedを返され続ける
  • タスクを再起動するまで復旧しない

運用で気をつけて再起動を回せばよいとも言えますが、自動ローテーションを有効にしている本来の意図とは噛み合いません。ローテーションをするたびにアプリの再起動が必要なら、ローテーションは手動で行えば良いよねということになるかなと考えています。もちろんその際のダウンタイム計画やオペレーションも考慮する必要が出てきます。

これは運用の注意で回避するのではなく、構造的に解消したいなという課題であると考えました。

解決方針

方法としてはシンプルでRDS IAM database authenticationへ移行すれば良いと考えました。
RDSにはIAM database authenticationという仕組みがあります。これを使うと、アプリはRDSのパスワードを保持せず、IAMロールから署名された短命トークン(有効期限15分)でPostgreSQLに接続するようになります。

  • パスワードをECSタスクの環境変数に注入する必要がなくなる
  • マスターパスワードがどう変わろうがLiteLLM ProxyからRDSへの接続には影響しない
  • Secrets Managerのマスターパスワードは引き続きbastionやアプリケーションコードで使用できる

参考にした資料は以下です。

https://github.com/BerriAI/litellm/tree/litellm_internal_staging/terraform/litellm/aws

https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.DBAccounts.html

BerriAI公式のTerraformスタックも同じ思想でIAM認証を採用しています。LiteLLM ProxyにもIAM_TOKEN_DB_AUTH=trueを渡すとboto3でトークンを取得してDATABASE_URLを組み立てる実装が入っているため、アプリ側コードに手を入れる必要はありません。

なお、AWS Solutions Library Samplesの同種ガイダンスGuidance for Multi-Provider Generative AI Gateway on AWS はmanage_master_user_passwordを使わない固定パスワード方式で、ローテーションを前提とした構成にはなっていません。

https://github.com/aws-solutions-library-samples/guidance-for-multi-provider-generative-ai-gateway-on-aws

一方、BerriAI公式のlitellm_internal_stagingブランチに最近マージされたTerraformスタックは本記事と同じIAM認証方式を採用しています。本番運用を視野に入れるならIAM認証寄りの構成を選ぶのが安全だと個人的には考えています。

前の記事からの変更内容

引き続き同じリポジトリを使っています。
今回の内容はすでにメインブランチに反映済みです。

https://github.com/k-kuwan0/litellm-proxy-on-fargate

Terraform

iam_database_authentication_enabledを有効化し、TaskロールにIAMトークン発行用の権限を追加します。

# infra/modules/data/rds.tf
resource "aws_db_instance" "this" {
  # ...
  iam_database_authentication_enabled = true
}
# infra/modules/compute/iam.tf
data "aws_iam_policy_document" "task_rds_iam_auth" {
  statement {
    actions   = ["rds-db:connect"]
    resources = [
      "arn:aws:rds-db:${var.region}:${var.account_id}:dbuser:${var.rds_resource_id}/${var.db_app_username}"
    ]
  }
}

ここで指定するリソースARNのrds_resource_idは、クラスタのcluster_resource_idではなくDBインスタンスのresource_idである点に注意が必要です。AWSコンソールのRDS → 該当インスタンス → "Resource ID"で確認できます。

合わせて、タスク定義のsecretsで参照していたマスターユーザのSecret ARNは不要になるので削除します。Secrets Manager上のシークレット自体は削除しません。manage_master_user_password = trueでAWSが自動管理しているもので、RDSインスタンスが存在する限り残り続けます。ECSタスクが触らなくなるだけで、SecretsManagerからDB認証情報を取得し、RDSに接続するという処理をLambdaや別のアプリケーションで実行するということは可能です。

ecspresso側

task definitionからDATABASE_USERNAME / DATABASE_PASSWORDsecrets参照を外し、代わりにenvironmentへIAM認証用の設定を入れます。

"environment": [
  { "name": "AWS_REGION",         "value": "ap-northeast-1" },
  { "name": "AWS_REGION_NAME",    "value": "ap-northeast-1" },
  { "name": "IAM_TOKEN_DB_AUTH",  "value": "true" },
  { "name": "DATABASE_USER",      "value": "litellm_app" }
]
  • AWS_REGION / AWS_REGION_NAMEはboto3がIAMトークン署名で参照します
  • IAM_TOKEN_DB_AUTH=trueでLiteLLMがIAM認証モードに切り替わります
  • DATABASE_USERにはIAMトークンで接続するPostgreSQLユーザー名を指定します

タスク起動時のシーケンスはこうなります。

  1. LiteLLMがIAM_TOKEN_DB_AUTH=trueを検出
  2. boto3で IAMトークンを取得(generate_db_auth_token
  3. postgresql://litellm_app:<token>@<host>/<db>を組み立ててDATABASE_URLに設定
  4. prisma migrate deployを実行(トークンを埋め込んだURLで接続)
  5. アプリ起動、PrismaWrapperがバックグラウンドで約12分毎にトークン更新

トークンの寿命は15分ですが、LiteLLM Proxyは期限切れ3分前でプロアクティブに更新する設計になっているようです。

デプロイ手順

Step 1. Terraform applyを実行する

cd infra/environments/dev
terraform apply

この時点ではRDS側にlitellm_appユーザーが存在しないため、LiteLLMはまだ起動してもDBに繋げません。

Step 2. PostgreSQL側でIAMユーザーを作成する

bastion EC2経由でマスターユーザーとして接続し、以下を一度だけ実行します。リポジトリにはllm-gateway/bootstrap/bootstrap.sqlとしてファイル化したものを置いています。

DO $$
BEGIN
  CREATE USER litellm_app;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;

-- IAMトークン認証を受け付けるためのAWS予約ロール
GRANT rds_iam TO litellm_app;

-- DB接続と作業権限
GRANT ALL PRIVILEGES ON DATABASE litellm TO litellm_app;
GRANT ALL ON SCHEMA public TO litellm_app;

-- Prisma migrationが今後作るテーブル/シーケンスにも権限が及ぶように
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO litellm_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO litellm_app;

GRANT rds_iam TO litellm_appがIAM認証をする際に重要な部分となります。rds_iamはAWSが用意した予約ロールで、これがメンバーシップに入っているPostgreSQLユーザーだけがIAMトークンでログインできるようになります。

IAMロールにrds-db:connectの権限を付与しただけでログインできるのでは?と最初は思うかもしれません。私がそうでした...。
しかし、IAMとPostgreSQLは別の世界でして、IAM側だけで権限を整えても、PostgreSQL側がIAMトークンを受け入れる準備をしていなければ認証は通らないという感じになっています。
rds_iamは IAMとPostgreSQLを中継してくれる存在で、両側を揃えてはじめてIAMトークン認証が成立します。

ALTER DEFAULT PRIVILEGESを入れているのは、LiteLLMがIAMトークンをDATABASE_URLに埋め込んだ状態でprisma migrate deployを呼ぶためです。つまりmigration実行の認証主体もlitellm_appになります。ランタイム接続ユーザーとmigration実行ユーザーが同一なので、bootstrap SQL実行時点でこれから作られるテーブルにも権限が及ぶ設定をしておかないとpermission denied for schema publicになります。

Step 3. アプリを再度デプロイする

cd llm-gateway
make deploy   # ecspresso deploy

bootstrap SQL実行後にタスクを起動すると、IAM認証で正常起動するようになります。

検証

実際に環境を作成して検証したログをもとに、4つのPhaseに分けて確認しました。

  • 環境: litellm-proxy-dev
  • LiteLLM image: ghcr.io/berriai/litellm:v1.83.14-stable.patch.3
  • DB: RDS PostgreSQL 16.6(db.t4g.micro

Phase 0. bootstrap SQL未実行で起動失敗を確認する

まず想定通りに失敗することを確かめました。
task definitionだけIAM認証対応版に切り替え、PostgreSQL側はまだlitellm_appユーザーが存在しない状態でECSサービス・タスクをデプロイします。

cd llm-gateway
make deploy

サービスの状態

{
    "status": "ACTIVE",
    "running": 1,
    "deployments": [
        { "rolloutState": "IN_PROGRESS", "failedTasks": 0 }
    ]
}

コンテナ自体は起動していますが、ALBのヘルスチェックが通らないためrolloutStateIN_PROGRESSのままrollout完了に至りません。内部ではmigrationのリトライ無限ループに陥っており、このまま放置すれば最終的にはdeployment circuit breakerのtimeoutでFAILED扱いとなりタスクが置き換えられます。

ログを見ると以下のエラーが約7秒間隔で繰り返されています。

2026-05-25T08:52:45 - Running prisma migrate deploy
2026-05-25T08:52:51 - prisma db error:
Error: P1000: Authentication failed against database server at
`litellm-proxy-dev-db.xxxxx.ap-northeast-1.rds.amazonaws.com`,
the provided database credentials for `litellm_app` are not valid.

ここから読み取れるのは以下の通りです。

  • IAMトークン自体は手元で生成できている
    • generate_db_auth_tokenはboto3がローカルで署名URLを組み立てる処理のため、AWS API への通信は発生せず、トークン生成段階ではネットワーク失敗もない
  • 生成したトークンはRDSまで届いて受理されている
    • PostgreSQL側がIAM APIで検証 → パスしている
  • ただし、PostgreSQL側でlitellm_appユーザーが存在しないため認証拒否している

つまり、タスクロールのrds-db:connect権限が正しく動作していることがわかります。
インフラ側の設定は問題なく、あとはbootstrap SQLを実行するだけ、という状態が確認できました。

Phase 1〜2. bootstrap実行し、再デプロイでDB接続が成功するか確認する

bastion経由でbootstrap SQLを流し、サービスを再デプロイします。

cd llm-gateway
make deploy

サービスの状態

{
    "rolloutState": "COMPLETED",
    "failedTasks": 0,
    "running": 1
}

rolloutStateCOMPLETEDに変化し、タスクが healthy になったことがわかります。

起動時のログを抜粋しています

2026-05-25T09:25:38 - prisma migrate deploy completed
2026-05-25T09:25:48 - Started RDS IAM token proactive refresh background task
2026-05-25T09:25:48 - RDS IAM token refresh loop started.
                      Tokens will be refreshed 180s before expiration.
2026-05-25T09:25:48 - RDS IAM token refresh scheduled in 672 seconds (11.2 minutes)
2026-05-25T09:25:49 - Application startup complete.
2026-05-25T09:25:49 - Uvicorn running on http://0.0.0.0:4000
2026-05-25T09:26:12 - GET /health/liveliness HTTP/1.1 200 OK

注目したいのは以下に示す部分です。

  • prisma migrate deploy completed: Prisma migrationがIAMトークン経由で成功している
  • Started RDS IAM token proactive refresh background task: バックグラウンドのトークン更新タスクが起動している
  • Tokens will be refreshed 180s before expiration: 期限切れの3分前でトークンの更新を行っている
  • RDS IAM token refresh scheduled in 672 seconds: トークン寿命15分から逆算して次回更新までの秒数を計算している

ALBのヘルスチェックも200 OKが継続して取れており、IAM認証でDB接続できる状態が実現しました。

Phase 3. 12分間隔でのトークン更新を追跡してみる

タスク起動から約50分間放置し、トークン更新ループが想定通り動くかを確認しました。

aws logs tail /litellm-proxy-dev/litellm/app --since 70m \
  | grep -E "(Proactively refreshing|RDS IAM token refreshed)"

観測されたトークン更新サイクル

時刻 (UTC) ログ
起動時 09:25:55 RDS IAM token refresh scheduled in 671 seconds
1回目 09:37:06 Proactively refreshing RDS IAM token...RDS IAM token refreshed successfully.
2回目 09:49:06 同上
3回目 10:01:06 同上
4回目 10:13:06 同上

正確に12分間隔で4回のトークン更新が成功しています。RDS IAMトークンの寿命は15分、期限切れの3分前でプロアクティブに更新する機能が確認できました。

トークン更新まわりについては、過去にIssue #16220 で「15分でDB接続が失敗する」という問題が長期に渡って報告されていました。

https://github.com/BerriAI/litellm/issues/16220

LiteLLMをセルフホストでRDSに繋いでいるユーザーを悩ませていたバグと見て取れますが、本検証では4回連続の更新成功を観測でき、現行バージョン(v1.83.14系)では完全に解消されていることを確認できました。今からLiteLLMをRDSに繋いで使う上では、このようなバグは気にしなくてよさそうですね。

Phase 4. パスワードローテーションを手動で起動し、LiteLLM ProxyからRDSへの接続に影響がないことを確認する

ここが本検証の最終ゴールです。Secrets Managerのマスターパスワードを強制ローテーションし、LiteLLM ProxyからRDSへの接続が影響を受けないことを確認します。

aws secretsmanager rotate-secret \
  --secret-id 'arn:aws:secretsmanager:ap-northeast-1:xxxxx:secret:rds!db-...'

レスポンス

{
    "ARN": "arn:aws:secretsmanager:...:secret:rds!db-...",
    "VersionId": "e3e76984-7307-413f-92e6-1c215f97e778"
}

新しいversion IDが発行され、内部的に新パスワード生成 → RDS更新 → 新版commitのプロセスが進行します。60秒待って状態を確認します。

{
    "LastRotated": "2026-05-25T19:24:14.227000+09:00",
    "VersionIdsToStages": {
        "e3e76984-...": ["AWSCURRENT", "AWSPENDING"],
        "ed9f89b5-...": ["AWSPREVIOUS"]
    }
}

ローテーションが成功しました。新版がAWSCURRENTに昇格し、旧版がAWSPREVIOUSに降格しています。RDSのマスターパスワードが新しい値に置き換わった状態になっています。

ローテーション完了時刻10:24:14 UTC前後のLiteLLM Proxyログを抜粋

10:24:14 - [ローテーション完了]
10:24:15 - GET /health/liveliness HTTP/1.1 200 OK
10:24:31 - GET /model/cost_map/source HTTP/1.1 200 OK
10:24:45 - GET /health/liveliness HTTP/1.1 200 OK
10:25:01 - GET /model/cost_map/source HTTP/1.1 200 OK
  • タスクIDは継続したまま(再起動なし)
  • ヘルスチェック200 OKが継続
  • Admin UIからのAPIリクエストも200 OKで処理
  • DB認証エラーやトークン関連の警告なし

サービス状態

{
    "running": 1,
    "desired": 1,
    "rolloutState": "COMPLETED",
    "failedTasks": 0
}

パスワードが変わってもLiteLLM Proxyが稼働し続ける状態の確認ができました!!

今後の改善余地

今回手動で実行したbootstrap SQLはRDS作成後に一度だけ実行すれば良いものです。
ただし、せっかくIaCで管理しているので、自動化はしたいなというのが正直な思いです。

terraform_data + local-execでECS run-taskを起動するという方法がBerriAI公式Terraformスタックが採用している方式だというのが調査をしていてわかったことです。

https://github.com/BerriAI/litellm/blob/litellm_internal_staging/terraform/litellm/aws/bootstrap.tf

検証でIAM認証の挙動が安定していることが確認できたので、上記を参考に別途自動化は導入すると良いと考えています。

まとめ

LiteLLM Proxy on FargateのDB接続を、Secrets Manager経由のパスワード認証からRDS IAM database authenticationへ切り替えました。

ポイントを整理すると以下です。

  • ECSのsecretsはタスク起動時に一度だけ展開されるため、パスワード自動ローテーションと相性が悪い
  • RDS IAM database authenticationなら、パスワードを使わず短命トークンで接続するためローテーションの影響を構造的に受けない
  • LiteLLM側にもIAM_TOKEN_DB_AUTH=trueを渡すだけで対応しており、prisma migrateもIAMトークンで通る
  • 12分間隔のトークン更新ループが機能し、パスワードローテーションを発火してもタスクは何も影響を受けない

もしRDSの認証情報をローテーション対応のSecretsManagerに格納しており、アプリケーション側がSecretsManagerとネイティブに統合されていない場合、アプリケーションをホストするECSタスクといったサービス用IAMロールへの権限付与とrds_iamロールを付与したユーザーをRDSに用意することによって、パスワードローテーションの影響を受けない認証の仕組みが実現できます。
この構成はLiteLLM Proxy以外のOSSでも応用できそうな内容かなと感じました。
LiteLLM ProxyをFargateで運用していて同じ課題に直面している方の参考になれば幸いです。

引き続き検証を進めていきますので、新しい気づきがあれば別の記事で共有します。

最後までご覧いただきありがとうございました。

この記事をシェアする

関連記事