AWS Bedrock 으로 사내 LLM Gateway(Litellm) 구축하기 -1-
1.LiteLLm 구축
많은 기업들이 AX 를 하겠다고 발벗고 나선 가운데 정작 어떤 모델을 사내에 공급할지가 아마도 큰 고민이었을 거라 생각한다. 보안이 중요한 곳에서는 로컬 서버에서 공개된 모델을 돌리는 곳도 봤고, 벤처 같은 경우에는 쿨하게 직원들에게 정기구독을 끊어주는 곳도 많이 봤다.
처음 LiteLLM 을 구축할때만 해도 대게 공개 모델들의 성능이 주력으로 쓰기에는 부족한 부분들이 많아서 건너건너 볼멘소리를 많이 들었더랬다.
지금이야 GLM 5.2 나 DeepSeek V4 라든지 주력으로 써도 손색이 없는 공개모델들이 정말 많다. 충분한 리소스만 있다면 집에서 로컬로 돌리고 싶다. 이야기가 엉뚱한 곳으로 흘렀는데 여튼 몸담고 있는 회사도 예외는 아니었다.
대게 보안과 감사 그리고 비용, 3가지에 대한 이슈가 가장 컸고 당시 핫했던 앤트로픽 Claude 국내 공식 채널이 모호하기도 하고 대고객 서비스 중 AWS Bedrock 을 사용하고 있었던 터라 이를 직원들에게 제공할 생각으로 LLM Gateway 를 찾기 시작했던 걸로 기억한다. LiteLLM 은 우리가 원하는 모든 서비스를 제공하고 있었다.
1) 로그감사
LLM 대화 목록은 ISMS(정보보호 관리체계) 인증에서 감사 및 보호 대상에 포함되므로 스토리지에 로그를 남겨야 한다. 물론 Bedrock 에도 이러한 요구사항을 다음과 같이 반영 가능하다.
Bedrock 에는 Invocation Logs 설정을 하면 S3 버킷에 로그를 저장하기도 하는데 내용을 확인하려면 Athena 같은 서비스를 또 써야 해서 LiteLLM 기본 기능을 쓰기로 했다.
LiteLLM 의 로깅은 다음과 같다. 세션별로 묶음으로 볼 수 있고 각각의 대화는 Request, Response 로 상세 로그를 확인가능하며 캐싱여부와 Cost 까지 확인이 가능하다.
이러한 로그에 대한 설정은 config.yaml 에서 설정 가능하고 다음과 같이 설정할 수 있다.
general_settings:
maximum_spend_logs_retention_period: "60d"
maximum_spend_logs_retention_interval: "60d"2) 모니터링/통계확인
LiteLLM 의 모니터링은 누가 언제 어떤 모델을 얼만큼 어떤 내용으로 썼는지 확인이 가능할 정도로 지원하고 있고 특별히 부족함을 느낄만한 부분은 없었다.
3) 개인정보 가드레일
LiteLLM 에 전화번호, 주민등록번호, 여권번호 등 개인정보를 넣는 경우를 위해서 가드레일을 설정 할 수 있는데 처음에는 아래와 같이 Bedrock Guardrails 를 설정해서 썼다.
Bedrock Guardrails 는 다양한 필터를 제공하지만 정규식으로만 처리를 할 생각이고 LiteLLM 에 다음 예시와 같이 설정하고 파이썬 함수를 호출해서 설정가능하다.
guardrails:
- guardrail_name: "pii-masking"
litellm_params:
guardrail: custom_callbacks.PrivacyMaskingGuardrail
mode: pre_and_post_call
default_on: true
- guardrail_name: "pii-blocking"
litellm_params:
guardrail: custom_callbacks.PrivacyBlockingGuardrail
mode: pre_call
default_on: trueconfig.yaml
import re
from litellm.integrations.custom_guardrail import CustomGuardrail
EMAIL_RE = re.compile(r"\b[\w.-]+@[\w.-]+\.\w+\b")
PHONE_RE = re.compile(r"\b01[016789]-?\d{3,4}-?\d{4}\b")
def mask_text(text: str) -> str:
text = EMAIL_RE.sub("[EMAIL]", text)
text = PHONE_RE.sub("[PHONE]", text)
return text
def has_pii(text: str) -> bool:
return bool(EMAIL_RE.search(text) or PHONE_RE.search(text))
def get_user_text(data: dict) -> str:
messages = data.get("messages", [])
return "\n".join(
message.get("content", "")
for message in messages
if isinstance(message.get("content"), str)
)
class PrivacyMaskingGuardrail(CustomGuardrail):
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
for message in data.get("messages", []):
if isinstance(message.get("content"), str):
message["content"] = mask_text(message["content"])
return data
class PrivacyBlockingGuardrail(CustomGuardrail):
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
text = get_user_text(data)
if has_pii(text):
raise ValueError("PII detected. Request blocked.")
return data4) 비용통제
litellm 의 가장 핵심적이고 주요한 기능중의 하나인 예산관련된 사항은 처음 접할때 상당히 헛갈리는 구성이었다.
위와 같이 간단하게 그림으로 만들어 봤는데 크게 요소에 대한 설명을 하면 다음과 같다.
- budget : 사용비용을 의미하고 USD 기준으로 설정 가능하다. 예를 들어 3 은 3 USD 와 같은 의미고 max_budget 은 해당사용자가 최대로 사용할 수 있는 비용을 의미한다.
- budget_duration : 사용량 리셋 구간을 의미한다. 예를 들어 이 값이 1d 면 사용량이 매일 갱신된다. 만약 max_budget 이 3 이면 매일 3달러씩 쓸 수 있다는 의미다
- virtual key : 개인별로 발급받는 API Key 이다. 앤트로픽 claude code 사용자는 환경변수로 다음과 같이 설정하면 된다.
한 사람이 여러 virtual key 를 발급 받을 수 있고 요청 속도 또한 설정이 가능하다.
export ANTHROPIC_BASE_URL="https://litellm.host.co.kr" # gateway 도메인
export ANTHROPIC_AUTH_TOKEN="sk-abcd......" # virtual key- team : 개개인을 팀에 소속시킬 수 있고 아래와 같이 팀안에서 사용자별로 비용을 따로 관리 할 수도 있다.
5) LiteLLM 전체 설정내용
custom_callbacks 이하의 함수들은 다음 포스트에 서술하거나 위에서 이미 언급한 내용들이다.
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
store_model_in_db: os.environ/STORE_MODEL_IN_DB
store_prompts_in_spend_logs: true
allow_all_keys: true
forward_client_headers_to_llm_api: true
use_x_forwarded_for: true
maximum_spend_logs_retention_period: "60d"
maximum_spend_logs_retention_interval: "60d"
alerting: ["email"]
router_settings:
routing_strategy: simple-shuffle
redis_host: os.environ/REDIS_HOST
redis_password: os.environ/REDIS_PASSWORD
redis_port: os.environ/REDIS_PORT
enable_pre_call_checks: true
allowed_fails: 3
cooldown_time: 30
disable_cooldowns: true
enable_tag_filtering: true
tag_filtering_match_any: true
litellm_settings:
timezone: "Asia/Seoul"
callbacks: ["smtp_email", custom_callbacks.proxy_handler_instance]
request_timeout: 1200
num_retries: 0
retry_policy:
AuthenticationErrorRetries: 0
BadRequestErrorRetries: 0
cache: true
cache_params:
type: redis
ssl: true
ssl_cert_reqs: null
ssl_check_hostname: false
host: os.environ/REDIS_HOST
port: os.environ/REDIS_PORT
password: os.environ/REDIS_PASSWORD
namespace: "litellm.caching.caching"
max_connections: 300
extra_spend_tag_headers:
- "x-forwarded-for"
guardrails:
- guardrail_name: "pii-masking"
litellm_params:
guardrail: custom_callbacks.PrivacyMaskingGuardrail
mode: pre_and_post_call
default_on: true
- guardrail_name: "pii-blocking"
litellm_params:
guardrail: custom_callbacks.PrivacyBlockingGuardrail
mode: pre_call
default_on: true