
LiteLLM ProxyをECS Fargateにデプロイする構成をTerraform + ecspressoで組んでみた
こんにちは。クラウド事業本部コンサルティング部の桑野です。
前回の記事では、LiteLLM Proxyのリクエストログをpgsty/minioを使ってローカルで再現する構成をご紹介しました。
今回はローカルで動作確認が出来ていたものをAWS上にデプロイするにはどうしようという部分を考えました。
LiteLLM Proxy公式およびAWS Solutions Libraryのリファレンス実装を参考にしつつ、Amazon ECS on FargateにLiteLLM Proxyをデプロイする構成をTerraform + ecspressoで組んでみたので、その内容を共有します。
LiteLLMとは?
LiteLLMは、OpenAI、Anthropic、Amazon Bedrockなど100以上のLLMプロバイダを統一的なOpenAI互換APIで呼び出せるようにするゲートウェイです。Pythonライブラリ形式のSDKと、サーバーとして動かすProxy形式の2つの利用形態があります。
今回はProxy形式を、Amazon ECS on Fargateで動かします。
ベースとした公式リファレンス
構成を組むにあたって、以下の2つの公式リファレンスを参考にしました。
LiteLLM公式のECSデプロイサンプル
AWS Solutions Libraryの「Multi-Provider Generative AI Gateway on AWS」
これらをベースに、本番運用を想定したベストプラクティスを可能な限り反映させています。
- VPCはフルprivate構成、外向き通信はVPC Endpointを経由して行う
- Secrets Managerでシークレットを集中管理する(RDS master user secretはAWS managed)
- Bedrock呼び出しはタスクロール経由、IAMは使用モデルのARNに絞る
- リクエストログは
s3_v2コールバックでS3に出力する - ECS Exec API呼び出しはCloudTrailで最低限監査する(対話シェル内のトランスクリプトはLiteLLMのimage base制約で取得しない)
- bastion経由でSSM Session ManagerによるALB / コンテナアクセス を行う
前提
以下の条件で検証しています。
- OS:macOS Tahoe バージョン 26.4.1
- チップ:Apple M4
- Colima:0.8.4
- Docker Client:28.4.0
- Terraform:1.10以上
- ecspresso:v2.8.3
- aws-vault:7.2.0
- aws-cli:2.32.31
- Session Manager plugin: 1.2.804.0
以下のモデルアクセスを有効化したAWSアカウントが必要です。
- リージョン:
ap-northeast-1 - モデル:
amazon.nova-lite-v1:0
Terraform / ecspressoの実行はaws-vault経由を想定しています。aws-vaultの利用方法は以下の記事をご参照ください。
構成
検証に使ったコードは以下のリポジトリにあります。
アクセス経路
ローカルPC
↓ aws ssm start-session (PortForwardingToRemoteHost)
bastion (private subnet, public IPなし)
↓ ALB DNS:4000 へ forward
internal ALB (private subnet)
↓
ECS Service (LiteLLM Proxy on Fargate, port 4000)
↓ DATABASE_*
RDS (private subnet, PostgreSQL 16)
ALBはinternal配置で、ローカルからの動作確認はbastion経由のSSM port forwardで行います。
責務分担
土台部分はTerraformで、LiteLLMのデプロイライフサイクル部分はecspressoで管理しています。
| 層 | ツール | 担当範囲 |
|---|---|---|
| 土台 | Terraform | VPC / VPC Endpoint / RDS / Secrets / S3 / ECS Cluster / ALB / Target Group / LogGroup / ECR / IAM Role / bastion |
| デプロイ | ecspresso | ECS Service / Task Definition / image tag rollout |
ecspressoはTerraformのoutputsをtfstate plugin経由で参照する構成にしているため、Terraformで作った値(ECRリポジトリURL、Secrets ARN、Subnet ID等)をecspresso側のJSONテンプレートに自動で流し込めます。
ディレクトリ構成
litellm-proxy-on-fargate/
├── infra/ # Terraform(土台)
│ ├── bootstrap/ # state用S3バケット作成スクリプト
│ ├── modules/ # 環境間で共有
│ │ ├── network/ # VPC / Subnet / SG / VPC Endpoint
│ │ ├── data/ # RDS / Secrets Manager / S3 / LogGroup
│ │ ├── compute/ # ECS Cluster / ALB / ECR / IAM Role
│ │ └── bastion/ # EC2 + SSM
│ └── environments/dev/ # 環境エントリポイント
└── llm-gateway/
├── docker-compose.yaml # ローカル開発用(前回記事の構成)
├── Makefile # build / push / deploy
├── litellm/ # Dockerfile / config.yaml
└── ecspresso/ # ECS Service / TaskDefinition
├── ecspresso.yml
├── service-def.json
└── task-def.json
構築手順
1. リポジトリをクローンする
git clone https://github.com/k-kuwan0/litellm-proxy-on-fargate.git
cd litellm-proxy-on-fargate
以降のコマンドはすべてリポジトリルートを起点に実行します。
2. Terraform state用のS3バケットを作成する
infra/bootstrap/create-state-bucket.shで、tfstateを保存するS3バケットを作成します。
cd infra/bootstrap
aws-vault exec <profile> -- ./create-state-bucket.sh
デフォルトのバケット名はlitellm-proxy-tfstate、リージョンはap-northeast-1です。変更したい場合はBUCKET/REGION環境変数で上書きできます。
3. Terraformで土台を構築する
cd infra/environments/dev
aws-vault exec <profile> -- terraform init
aws-vault exec <profile> -- terraform apply
VPC / VPC Endpoint / RDS / Secrets / S3 / ECS Cluster / ALB / ECR / bastionなどが作られます。所要時間は約10〜15分(RDS作成に時間がかかります)。
4. ECRにLiteLLMイメージをpushしてecspressoでデプロイする
llm-gateway/Makefileに、ECRログイン → image build → push → ecspresso deployを一括で実行するターゲットを用意しています。
cd llm-gateway
aws-vault exec <profile> -- make deploy
内部で以下が走ります。
terraform outputからECR URL / regionを取得git rev-parse --short=7 HEADでimage tagを生成- ECRにログイン
docker build --platform linux/arm64でarm64イメージをビルド- ECRにpush
IMAGE_TAG=<ECR>:<SHA> ecspresso deploy
ECS Serviceのstable待ちまで含めて5〜10分程度で完了します。
動作確認
1. bastion経由でALBにアクセスする
ALBがinternal配置のため、bastionをSSM port forwardで経由してアクセスします。
BASTION_ID=$(cd infra/environments/dev && terraform output -raw bastion_instance_id)
ALB_DNS=$(cd infra/environments/dev && terraform output -raw alb_dns_name)
aws-vault exec <profile> -- aws ssm start-session \
--target ${BASTION_ID} \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "host=${ALB_DNS},portNumber=4000,localPortNumber=4000"
別シェルで疎通確認します。
curl -i http://localhost:4000/health/liveliness
# 期待: HTTP/1.1 200 OK
2. Admin UIにログインする
port forwardが動いている状態でブラウザからhttp://localhost:4000/uiを開きます。
- ユーザー:
admin - パスワード:
LITELLM_MASTER_KEYの値(Secrets Managerに格納されている)
master keyはSecrets Managerから取得します。
MASTER_KEY_ARN=$(cd infra/environments/dev && aws-vault exec <profile> -- terraform output -raw master_key_secret_arn)
aws-vault exec <profile> -- aws secretsmanager get-secret-value \
--secret-id "${MASTER_KEY_ARN}" \
--query SecretString --output text
もしくはマネジメントコンソールからも確認ができます。

3. Bedrockモデルを登録して呼び出す
general_settings.store_model_in_db: trueによりモデル定義はDBで管理されるため、Admin UIから登録します。
- 左メニュー「Models + Endpoints」 → 「+ Add Model」
- 以下を入力
- Provider:
Bedrock - LiteLLM Model Name(s):
bedrock/amazon.nova-lite-v1:0 - Public Model Name:
nova-lite - AWS Region Name:
ap-northeast-1 - AWS Access Key ID / Secret:空欄(タスクロール経由のため不要)
- Provider:

登録後、Test Key(またはLLM Playground)でnova-liteを選択してhelloと送信すると、Nova Liteから応答が返ってきます。

4. S3に出力されたリクエストログを確認する
LiteLLMのs3_v2コールバックがTerraformで作成したS3バケットにリクエストログを書き出しています。
以下のコマンドで確認ができます。
S3_BUCKET=$(cd infra/environments/dev && aws-vault exec terraform-runner -- terraform output -raw s3_log_bucket_name)
aws-vault exec <profile> -- aws s3 ls "s3://${S3_BUCKET}/request_logs/" --recursive | tail
マネジメントコンソールからも確認ができます。

ログの中身(StandardLoggingPayload形式)については、前回の記事もご参照ください。
5. ECS Execでコンテナに入る
トラブルシュート用にECS Execでコンテナに直接入れる構成にしています。
CLUSTER=$(cd infra/environments/dev && aws-vault exec <profile> -- terraform output -raw ecs_cluster_name)
TASK=$(aws-vault exec <profile> -- aws ecs list-tasks --cluster ${CLUSTER} --service-name litellm --query 'taskArns[0]' --output text)
aws-vault exec <profile> -- aws ecs execute-command \
--cluster ${CLUSTER} \
--task ${TASK} \
--container litellm \
--interactive \
--command "/bin/sh"
ECS Exec API呼び出し自体(誰が・いつ・どのタスク/コンテナに・非対話モードなら何を実行したか)は CloudTrailのExecuteCommandイベント に自動記録されます。
コンテナに入ると、prisma migrate status等でDBマイグレーションの状態を直接確認することもできます。
cd /app/.venv/lib/python3.13/site-packages/litellm_proxy_extras
ENC_PW=$(python3 -c "import os, urllib.parse; print(urllib.parse.quote(os.environ['DATABASE_PASSWORD'], safe=''))")
DATABASE_URL="postgresql://${DATABASE_USERNAME}:${ENC_PW}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}" \
prisma migrate status --schema schema.prisma
構成の解説
ここまでで動作確認は完了しました。
ポイントとなる部分をいくつか補足しておきます。
Terraformとecspressoの責務分割
土台部分(VPC / RDS / IAM / ECRリポジトリ等)はTerraformで一度作ってあまり変えないものとし、デプロイの度に動くTask Definition / ECS ServiceはecspressoのJSONで管理しています。
ecspresso側のJSONからはTerraformのoutputsを参照しています。
# llm-gateway/ecspresso/ecspresso.yml
region: ap-northeast-1
cluster: litellm-proxy-dev
service: litellm
service_definition: service-def.json
task_definition: task-def.json
timeout: 10m0s
plugins:
- name: tfstate
config:
url: s3://litellm-proxy-tfstate/terraform/dev/terraform.tfstate
// llm-gateway/ecspresso/task-def.json(抜粋)
"executionRoleArn": "{{ tfstate `output.execution_role_arn` }}",
"taskRoleArn": "{{ tfstate `output.task_role_arn` }}",
...
"secrets": [
{ "name": "DATABASE_USERNAME", "valueFrom": "{{ tfstate `output.rds_master_user_secret_arn` }}:username::" },
{ "name": "DATABASE_PASSWORD", "valueFrom": "{{ tfstate `output.rds_master_user_secret_arn` }}:password::" },
{ "name": "LITELLM_MASTER_KEY", "valueFrom": "{{ tfstate `output.master_key_secret_arn` }}" }
]
{{ tfstate `output.<name>` }}のテンプレート構文で、TerraformでapplyしたばかりのARNやIDをそのままTask Definitionに流し込めます。
VPC EndpointによるPrivate構成
NAT Gatewayを置かず、必要な通信はすべてVPC Endpoint経由としています。
| Endpoint | 用途 |
|---|---|
s3(Gateway) |
LiteLLMのリクエストログPUT |
ecr.api / ecr.dkr |
コンテナイメージpull |
logs |
CloudWatch Logs送信 |
secretsmanager |
Task Definition secretsの取得 |
ssm / ssmmessages / ec2messages |
bastion / ECS Exec |
bedrock-runtime |
Bedrock InvokeModel |
RDS master user secretをAWS managedで扱う
RDSのmanage_master_user_password = trueで、master user用のSecretをAWSが自動生成・ローテーション管理しています。
ECS Task Definitionのsecretsでは、Secret内の特定フィールドを<secret-arn>:<key>::形式で個別に環境変数に注入できます。これを使い、usernameとpasswordを分けてDATABASE_USERNAME / DATABASE_PASSWORDという別々の環境変数として渡しています。
"secrets": [
{ "name": "DATABASE_USERNAME", "valueFrom": "<rds_secret_arn>:username::" },
{ "name": "DATABASE_PASSWORD", "valueFrom": "<rds_secret_arn>:password::" }
]
LiteLLMはこれらを使ってDB接続文字列を組み立てます。
パスワードのURLエンコード問題
AWS managedのRDS master user passwordは、AWSが自動生成するため#や:などの PostgreSQL接続URLの予約文字 が含まれることがあります。postgresql://user:password@host:port/db形式の接続文字列に 生のまま埋め込むと接続失敗 する可能性があるため、通常はアプリ側でURLエンコードする必要があります。
LiteLLMはこの問題に対応済みで、DATABASE_USERNAME / DATABASE_PASSWORD / DATABASE_NAMEを内部でurllib.parse.quote_plus()を使ってエンコードしてから接続文字列を組み立てます。
# litellm/proxy/utils.py: construct_database_url_from_env_vars()
database_username_enc = urllib.parse.quote_plus(database_username)
database_password_enc = (
urllib.parse.quote_plus(database_password) if database_password else ""
)
database_name_enc = urllib.parse.quote_plus(database_name)
database_url = f"postgresql://{database_username_enc}:{database_password_enc}@{database_host}/{database_name_enc}"
そのため、AWS managedで生成された予約文字入りパスワードもそのまま渡してOKです。ローテーション後の新パスワードも同様に処理されます。
過去PostgreSQLと自動ローテーション対応のSecretsManagerのシークレットでうまく接続できないというトラブルに遭遇したことがあったので、エンコーディングが行われているのか気になって調べてみました。
Bedrockの最小権限
タスクロールに付与するBedrockのInvokeModel権限は、使用するモデルのARNに絞っています。
# infra/modules/compute/iam.tf(抜粋)
statement {
sid = "BedrockInvoke"
actions = [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
]
resources = [
"arn:aws:bedrock:ap-northeast-1::foundation-model/amazon.nova-lite-v1:0",
]
}
別のモデルを使いたくなった場合は、ここに該当モデルのARNを追加します。
ECS Execの有効化と使用ケース
本構成で有効化しているのは、 例外フローでのDBオペレーション を想定しているためです。
ECS Execは通常運用では使うことはないと考えています。
具体的には、LiteLLMの起動時にUSE_PRISMA_MIGRATE=Trueによってprisma migrate deployが自動実行されますが、 失敗したマイグレーションの手動レスキュー が必要になるケースがあります。例えば、LiteLLMのバージョンを巻き戻したいときに、新バージョンで適用した未対応のマイグレーションを「ロールバック扱い」としてマークする必要がある、といった操作です。
# 想定する例外フロー(ECS Exec経由)
prisma migrate status --schema schema.prisma
prisma migrate resolve --rolled-back <migration_name> --schema schema.prisma
LiteLLM公式ドキュメントのSafe Rollback Guideにも、_prisma_migrationsテーブルの手動操作についての記述があります。
RDSはprivate subnetに配置されていて踏み台からも直接続させない方針なので、こうした操作はECS Exec経由のコンテナ内から行います。
ECS Execの監査方針
ECS Execを使ってRDSへマイグレーション操作を実施するため、そこの記録は残しておきたいものです。
実運用としては、CloudTrailのアクセス記録 + IAMによる接続条件の絞り込み(例:タグ条件、Deny ssm:StartSessionでECS Exec経路に限定)で監査要件をカバーできないかと考えています。
ECS Exec API呼び出しはCloudTrailのExecuteCommandイベントに自動記録されます。userIdentity(誰が)、eventTime(いつ)、sourceIPAddress(どこから)、requestParameters(どのクラスター/タスク/コンテナに対して、非対話モードならcommandに渡した文字列)まで一通り取得できます。
# infra/modules/compute/ecs_cluster.tf
resource "aws_ecs_cluster" "this" {
configuration {
execute_command_configuration {
logging = "NONE"
}
}
}
一方、対話シェル内で打った各コマンドと出力(セッショントランスクリプト)をCloudWatch / S3に残すには、コンテナイメージに BSD版のscriptコマンドが必要 とAWS公式ドキュメントに明記されています。
The container image requires
scriptandcatto be installed in order to have command logs uploaded correctly to Amazon S3 or CloudWatch Logs.
LiteLLM公式imageのベースである Wolfi/Chainguard にはBSD版scriptが提供されておらず(util-linux版のscriptは引数構文がSSM agentと非互換)、トランスクリプトの記録はできません。本構成ではlogging = "NONE"を明示し、シェル内の詳細ログは取得しない方針としています。
Admin UIへのアクセス手段
AWS-StartPortForwardingSessionToRemoteHostドキュメントを使い、bastionを踏み台にしてALBに対するport forwardを実行できます。SSHキーの管理が不要で、IAMで接続制御が完結します。
aws ssm start-session \
--target ${BASTION_ID} \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "host=${ALB_DNS},portNumber=4000,localPortNumber=4000"
これによって、権限を持った方がLiteLLM ProxyのAdmin UIにアクセスできるようになります。
なお、今回の構成では、AgentCore runtimeやバックエンドサーバーなど同じAWS環境から呼び出されることを想定してLiteLLM Proxyをプライベートに配置しています。
停止・クリーンアップ
Bedrockの利用料金は呼び出した分のみですが、RDSやVPC Endpointなどは時間課金です。検証が終わったら以下の手順でクリーンアップします。
# 1. ECS Service / Task Definitionを削除
cd llm-gateway/ecspresso
aws-vault exec <profile> -- ecspresso delete --force --terminate
# 2. AWSリソースを破棄
cd ../../infra/environments/dev
aws-vault exec <profile> -- terraform destroy
state用S3バケットだけはバージョニング有効なので、versionsも含めて手動で削除してください。
まとめ
いかがだったでしょうか。
LiteLLM ProxyをAmazon ECS on FargateにTerraform + ecspressoで構築する構成を共有しました。
本番環境を意識して一通り盛り込んでみましたが、LiteLLMやLLMOps周辺はまだまだ検証ポイントが多いなと感じています。引き続き試行錯誤していこうと思いますし、新しい気づきがあれば別の記事で共有します。
最後までご覧いただきありがとうございました。








