분류 전체보기

LiteLLM vs Pydantic AI

2025. 10. 17. 09:40

LiteLLM

LLM 모델 호출 통합 레이어(wrapper)

OpenAI 포맷 통합 SDK

 

모델마다 SDK가 달라도 completion(model=..., messages=...) 같은 OpenAI 형식 입/출력으로 통합 제공

하지만 모든 모델이 원래 OpenAI API를 지원하는 건 아님

 

ex) Gemini(Google AI Studio)

: 자체 API를 사용하지만 LiteLLM은 이를 OpenAI API와 동일한 인터페이스로 감싸서 호출 가능

일부 모델에는 추가 파라미터 제공


원래는 모델별 SDK가 제각각이라 SDK마다 호출 방식, 파라미터, 반환 구조가 달라서 번거로움

from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(model="gpt-4o", messages=[...])

from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(model="claude-3-opus", messages=[...])

LiteLLM을 사용해 모델 호출을 통합된 포맷으로 감쌀 수 있음

from litellm import completion

# OpenAI 모델 호출
response = completion(
    model="gpt-4o", 
    messages=[{"role": "user", "content": "hi"}]
)

# 다른 모델 호출
response = completion(model="anthropic:claude-3", messages=[...])
response = completion(model="mistral:open-mixtral", messages=[...])

 

- 모델마다 다른 SDK API 차이를 LiteLLM이 통일된 호출 인터페이스로 감쌈

- OpenAI, Anthropic, Mistral 등 거의 모든 주요 LLM을 동일한 방식으로 호출 가능

- "순수 모델 호출 SDK" 역할이므로 Agent, Tool 호출, Memory, MCP 등은 직접 구현해야 함


PydanticAI

LLM기반 Agent Framework

from pydantic_ai import Agent

agent = Agent(model="openai:gpt-4o-mini")
result = agent.run("Summarize this text")

위 코드로 모델 호출, 응답 파싱, 타입 검증, Agent/Workflow 관리까지 자동 처리 가능

 

= 앱 수준에서 LLM을 쉽게 연결하고 타입 안전하게 결과를 다룰 수 있는 고수준 프레임워크

 

PydanticAI는 Model + Provider + profile 구조로 되어 있음

1. Model: LLM 호출 형식 정의 (OpenAIChatModel, AnthropicModel 등)

2. Provider: 실제 요청 전송 및 인증 처리 (OpenAIProvider, LiteLLMProvider 등)

3. Profile: 모델별 세부 규칙 (JSON schema 제한, temperature 기본값 등)

 


원래 PydanticAI는 모델 호출 시 각 모델 SDK에 맞춰서 호출해야 했음

 

최근 PydanticAI에서 LiteLLMProvider를 제공!

: PydanticAI에서 LiteLLM을 Provider로 연결하면 OpenAI API 포맷으로 감싸서 호출 가능

Agent/Workflow/타입 검증 등 PydanticAI 기능을은 그대로 쓰면서 실제 모델 호출은 LiteLLM이 처리 가능

 

지원하지 않는 모델은 자체 Provider 개발 

'자연어 처리 > LLM' 카테고리의 다른 글

AI Agent란?  (0) 2025.10.16

AI Agent란?

2025. 10. 16. 10:20

AI Agent

스스로 환경을 인식하고 목표를 달성하기 위해 행동을 선택하는 시스템

 

Agent를 구성하는 주요 개념들

  구성요소 역할 대표 기술/예시
기본 LLM 언어 이해, 계획, 추론 GPT-4, Claude, Gemini, Llama
실행 Tools
(Function Calls)
외부 API, RAG, DB 질의 등 실행 OpenAI Function Calling, LangChain Tools
표준화 MCP
(Model Context Protocol)
표준 연결 인터페이스
여러 도구나 시스템을 LLM에 연결하는 공통 규격
OpenAI MCP, Vercel MCP, Anthropic MCP
고도화 Memory 장기/단기 기억, 상태 유지 LangGraph Memory, vector store
지식 RAG
(Retrival-Augmented Generation)
검색 기반 지식보강 도구
LLM이 모르는 정보를 벡터DB 등에서 검색
LlamaIndex, LangChain Retrieval
확장 Multimodal 여러 입력 모달리티(텍스트 이미지, 음성, 영상) 지원 GPT-4o, Gemini, Claude 3.5등
고도화 Planning / Reasoning Loop LLM이 도구를 순서대로 사용할 계획을 세움 ReAct, Tree-of-Thoughts, AutoGPT
프레임워크 Framework / Agent Runtime 위 모든 걸 자동 orchestration LangChain, OpenAI Agents, CrewAI, LlamaIndex Agents

 

Agent = LLM + (필요한 부가 구성 요소 조합)

 

AI Agent 유형

유형 역할 / 특징 필요한 구성요소 예시
대화형 (Chat Agent) 단순 Q&A, 자연어 이해/응답 LLM
검색형 (RAG Agent) 외부 정보 검색 + 답변 생성 LLM + RAG Tool
행동형 (Action / Tool Agent) 도구 호출, 계산, API 사용 LLM + Function Calls / Tools
멀티스텝 플래너형 (Planner Agent) 목표 달성을 위해 여러 단계 계획 LLM + Planning Loop + Tools + Memory
멀티모달형 (MultiModal Agent) 텍스트, 이미지, 음성, 영상 입력/출력 LLM(Multimodal) + Tools + Interface
지속/상태 기반(Stateful Agent) 기억, 세션 유지, 이전 대화 맥락 활용 Memory + LLM + Tools
자동화/자율형 (Autonomous Agent) 목표 기반 반복 수행, 스스로 판단 Planner + Memory + Tools + Multi step Loop

'자연어 처리 > LLM' 카테고리의 다른 글

LiteLLM vs Pydantic AI  (0) 2025.10.17

[Git] 원격 저장소 연결

2025. 10. 13. 08:48

git init

로컬 폴더를 Git 저장소로 초기화하는 명령어

아직 코드, 브랜치, 원격 연결 없음

이 시점엔 완전히 빈 Git 저장소 상태

 

git remote add origin https://~

origin 이라는 이름으로 원격 Git 서버(GitLab, GitHub 등)랑 연결

아직 원격에서 코드는 가져오지 않음

 

git fetch origin

원격의 모든 브랜치 목록과 커밋 기록이 다운됨

로컬 브랜치로 전환되지 않음

ex) origin/main, origin/branch 같은 원격 브랜치가 생겼을 뿐 아직 내 로컬에는 아무 브랜치 없음

 

git branch -r

원격 브랜치 목록 보기

 

git checkout -b my_branch origin/my_branch

원격의 my_branch 브랜치를 기반으로 로컬에서도 my_branch라는 브랜치 만들기

- my_branch라는 로컬 브랜치가 새로 생김

- 그 브랜치 원격 origin/my_branch를 track하게 설정됨

- 동시에 그 브랜치로 전환(checkout) 됨

 

git branch

현재 로컬 브랜치 목록 보기

* 가 붙은게 현재 내가 작업 중인 브랜치

ex) my_branch만 가져온경우 로컬에 my_branch만 존재하고 main은 아직 원격에만 있음

main브랜치도 보고싶은 경우 git checkout -b main origin/main 실행

 

git push origin main 또는 git push origin my_branch

현재 로컬 main, my_branch 브랜치를 원격의 main, my_branch로 올리기

'Git' 카테고리의 다른 글

[Git] Git과 GitHub의 차이  (0) 2023.01.24

 

 

서비스 중단 없이 요청을 분산하고, 서비스 장애 시 자동으로 다른 서버로 전환되게 하려 한다.

 

Keepalived + HAProxy (또는 LVS) 조합을 통해 failover + load balancing을 구현하는 방법을 정리한다.

 

Failover 서버 하나가 죽었을 때, 다른 서버가 자동으로 역할을 대신하는 것 (고가용성 보장)
Load Balancing 여러 서버에 요청을 분산시켜서 부하를 나눠주는 것 (성능 및 부하 분산)
Keepalived 원래는 LVS 설정 자동화를 위해 만들어진 도구이지만, 현재는 VRRP를 이용한 VIP Failover 기능이 주 용도
LVS (Linux Virtual Server) 리눅스 커널 레벨에서 제공하는 고성능 로드밸런싱 기능 (TCP 연결만 분산)
HAProxy 유저 공간에서 동작하는 소프트웨어 기반 로드밸서 (TCP/HTTP 레벨)
VIP (Virtual IP) 실제 물리 IP가 아닌, 여러 서버가 공유할 수 있는 가상 IP 주소. 하나만 "활성" 상태이고 Failover 시 다른 서버로 넘어감

 

Keepalived란?

Keepalived는 서버 간의 **가상 IP(VIP)**를 공유하고, 장애가 발생했을 때 자동으로 다른 서버로 IP를 넘겨주는 기능을 함

즉, 서버 1대가 죽어도, 클라이언트 입장에선 IP가 그대로이기 때문에 중단 없이 서비스를 받을 수 있음

 

웹 서버 이중화 / 로드밸런서 이중화 / 데이터베이스 이중화 / Kubernetes HA 구성 등에 쓰인다.

 

기본 구성 요소

1. VRRP (Virtual Router Redundancy Protocol)

- Keepalived의 핵심 기능

- 여러 대의 서버 중 하나를 MASTER, 나머지를 BACKUP으로 설정

- MASTER가 죽으면 BACKUP이 VIP를 가져가서 대신 서비스를 함

 

2. Health Check

- 서버나 서비스가 살아있는지 주기적으로 확인

- 죽었으면 VIP를 넘기거나 알림을 줄 수 있음

 

3. LVS 구성 지원 (로드밸런싱)

- Keepalived는 원래 LVS의 설정을 자동화하기 위해 만들어졌음

- 현재는 VRRP 기능이 더 널리 쓰임

 

예시 시나리오

서버1 (192.168.0.101) / 서버2 (192.168.0.102) / VIP (192.168.0.100)

1. 서버1이 MASTER 역할을 맡고 VIP를 가짐

2. 클라이언트는 항상 VIP를 통해 접근함

3. 서버1이 죽으면, 서버2가 VIP를 받아서 자동으로 서비스 제공 시작

4. 클라이언트는 아무것도 모른 채 계속 서비스 이용 가능

 

# MASTER 설정 예시

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 1111
    }
    virtual_ipaddress {
        192.168.0.100
    }
}

# BACKUP은 state BACKUP, priority만 낮추면 됨

 

여기까지는 VIP 장애조치만 가능하고 로드밸런싱은 불가하다.

 

 

Keepalived만 사용 VIP 장애조치만 가능, 로드밸런싱은 불가
Keepalived + HAProxy FastAPI, 웹 서비스 등 HTTP 기반에 적합
Keepalived + LVS TCP 기반 단순 서비스에 고성능 분산 필요할 때 사용
HAProxy 단독 사용 로드밸런싱 가능하지만, 장애조치 기능 없음 (VIP 필요시 Keepalived 필요)

 


항목 HAProxy LVS (IPVS)
계층 L4 + L7 L4만 (TCP/UDP)
기능 HTTP 헤더 분석, 쿠키 기반 분산 가능 단순 연결 분산
성능 좋음 매우 뛰어남 (커널 수준)
설정 난이도 쉽고 직관적 다소 복잡, 커널 모듈 필요

 

HAProxy란?

소프트웨어 기반 로드밸런서

TCP, HTTP/HTTPS 레벨의 요청 분산 가능

라운드로빈, 가중치, 최소연결 등 다양한 방식 지원

설정은 단일 텍스트 파일 /etc/haproxy/haproxy.cfg

 

만약 HAProxy서버가 죽는다면?

- Keepalived로 VIP를 다른 HAProxy 서버로 넘김

 

 

  • 서버 2대 (서버1, 서버2), 각 서버에 FastAPI 실행
  • VIP로 이중화 및 로드밸런싱 (Keepalived로 VIP 장애 조치, HAProxy로 요청 분산)
# 정상 상태

[Client 요청]
     |
     v
[VIP: 192.168.0.100]  ← Keepalived가 서버1에 VIP 유지 중
     |
     v
┌────────────────────────────────────────────┐
│ 서버1 (HAProxy1 + Keepalived Master)        │
│  → 자신(127.0.0.1:8000) = FastAPI           │
│  → 서버2(192.168.0.102:8000) = FastAPI      │
│  → 라운드로빈 방식으로 둘에 요청 분산              │
└────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│ 서버2 (HAProxy2 + Keepalived Backup)                    │
│  → 대기 중, VIP 없음                                      │
│  → FastAPI 실행 중이며, 서버1의 HAProxy가 전달하는 요청을 처리함  │
│  → 하지만 VIP가 없어 클라이언트 요청을 직접 받진 않음             │
└────────────────────────────────────────────────────────┘
# 장애 발생 시
# 서버1 DOWN → VIP가 서버2로 이동됨

[Client 요청]
     |
     v
[VIP: 192.168.0.100]  ← 이제 서버2에 있음
     |
     v
┌─────────────────────────────────────────────┐
│ 서버2 (HAProxy2 + Keepalived → Master 승격)   │
│  → 자신(127.0.0.1:8000) = FastAPI            │
│  → 서버1(192.168.0.101:8000) = 연결 실패 (DOWN)│
│  → 헬스체크로 서버1 비활성화됨                     │
│  → 자신에게만 요청 전달                          │
└─────────────────────────────────────────────┘

┌────────────────────────────────────────────┐
│ 서버1 (죽음)                                 │
│  → VIP 회수됨                                │
│  → 모든 서비스 응답 불가                        │
└────────────────────────────────────────────┘

 

서버1이 다시 살아나면 Keepalived는 우선순위에 따라 VIP를 다시 서버1로 넘기거나 서버2에 유지할 수 있음

 

# HAProxy 설정 예시 (haproxy.cfg)
# 서버1 기준

frontend http_front
    bind *:80
    default_backend fastapi_backend

backend fastapi_backend
    balance roundrobin
    option httpchk GET /
    server server1 127.0.0.1:8000 check
    server server2 192.168.0.102:8000 check
  
  
# 서버2
# server server1 192.168.0.101:8000 check
# server server2 127.0.0.1:8000 check

 

서버1은 살아있지만 서버1의 FastAPI가 죽었을 때?

 

- Keepalived는 서버1 네트워크 상태가 살아있으니 VIP 이동 안함

- HAProxy가 서버1 FastAPI가 죽었다 판단 → 서버1 FastAPI를 로드밸런싱 대상에서 제외

- 요청은 서버2 FastAPI로만 전달됨

- 따라서 서비스 중단 없이 자동으로 장애 서버를 피해 요청 분산 가능

 

 

[ 헬스 체크 역할 구분 ]
* Keepalived 헬스 체크
- 주로 네트워크 레벨 (ping, 특정 포트 열림 여부 등)으로 VIP 소유 서버가 살아있는지 판단
- Keepalived에서 장애 판단 시 VIP를 다른 서버로 이동
- 보통 서버 자체가 다운됐거나 네트워크 단절 시 작동

* HAProxy 헬스 체크
- 실제 서비스 레벨(HTTP 응답 상태, 특정 엔드포인트 정상 동작 여부 등) 확인
- 특정 서버(FastAPI 인스턴스 등)가 죽었을 때 그 서버를 로드밸런싱 대상에서 제외
- VIP 이동과는 별개, VIP를 가진 서버 내에서의 로드밸런싱 대상 관리

 

 

'네트워크' 카테고리의 다른 글

Docker compose extra_hosts: 컨테이너 내부 DNS 설정  (0) 2025.03.10
[네트워크] CDN이란?  (0) 2023.01.20
[네트워크] REST API란  (0) 2023.01.20

 

개발 환경에서 호스트 경로에 소스 코드를 두고 컨테이너 볼륨으로 지정했다.

컨테이너를 올린 후 코드 수정을 해서

commit하고 save하고 load했더니 수정된 코드가 반영이 안됐다.

 

= 볼륨 데이터는 commit에 포함되지 않음!

 


Docker 컨테이너의 파일 시스템 구조

 

▶︎ 이미지 레이어

여러 개의 읽기 전용 레이어들이 쌓여 있는 구조

각각의 레이어는 Dockerfile의 명령어 (FROM, RUN, COPY 등)를 실행하면서 만들어짐

컨테이너를 만들면 이 이미지 위에 쓰기 가능한 컨테이너 레이어가 하나 얹혀짐

 

▶︎ 컨테이너 레이어

컨테이너 실행 중에 생기는 수정, 생성, 삭제된 모든 파일/디렉터리가 저장됨

단, 마운트된 경로는 제외!

> 해당 경로는 애초에 호스트 외부를 바라보도록 리디렉션되어 있음

그래서 볼륨 데이터는 이미지에도 컨테이너 레이어에도 없음

 

docker commit

docker commit은 컨테이너 레이어(=컨테이너 자체의 파일 시스템)만 저장

따라서 마운트된 경로는 커밋 대상이 아님

 


마운트된 데이터도 포함시키려면?

docker commit으로는 불가능

1. 마운트 경로 → 임시 경로 → commit → 복원

[전제 조건]
# 호스트 경로: /myproject
# 마운트: /myproject ↔ 컨테이너 내 /app/core
docker run -v /myproject:/app/core -it ubuntu bash

[1] 최초 컨테이너 /app/core → (호스트에 마운트됨)
                           /app/temp → (비어 있음, commit 반영을 위해 임시 폴더 만들기)

[2] 파일 수정
      /app/core/main.py ← 수정됨 (호스트에 반영됨)

[3] /app/temp에 복사
      /app/temp/main.py ← 복사됨 (컨테이너 내부에 존재)

[4] docker commit → temp만 이미지에 포함됨

[5] 새 컨테이너 실행 후 복원 cp /app/temp/* /app/core/

 

2. Dockerfile에서 COPY를 통한 이미지 재생성

FROM ubuntu
COPY ./core /app/core

 

'Docker & Kubernetes > [Docker]' 카테고리의 다른 글

[Docker] 서버에 Dockerfile 생성 후 배포  (0) 2023.06.01

 

↓ OpenSearch 주요 네트워크 설정

더보기

opensearch 주요 네트워크 설정 설명

설정 키 역할 예시 설명
network.host 서버가 바인딩할 IP 127.0.0.1

1.1.1.1
(내 IP)

0.0.0.0
OpenSearch가 어떤 IP에 바인딩될지 지정.
0.0.0.0은 모든 인터페이스(모든 IP 주소)를 의미
network.publish_host 다른 노드가 이 노드를
어떻게 볼지 지정하는 IP
1.1.1.1 클러스터 내 다른 노드들이 이 노드와 통신할 때 사용하는 IP 
일반적으로 외부에서 접근 가능한 IP로 설정
transport.tcp.port 노드 간 통신용 기본 포트
(바인드용)
9300 클러스터 내부의 노드들끼리 통신하는 데 사용하는 포트
대부분 9300
transport.publish_port 노드 간 통신용
외부에 노출할 포트
10093 다른 노드가 이 노드에 접근할 때 사용할 포트 번호
보통 transport.tcp.port와 같지만, 필요 시 다르게 지정
http.port REST API 요청을 받는 포트
(바인드용)
9200 사용자가 OpenSearch에 API 요청을 보내는 포트
웹 브라우저나 curl로 접속할 때 사용
http.publish_port REST API 요청을 받을
외부 노출 포트
10092 외부 클라이언트가 이 노드에 접근할 때 사용할 포트
보통 http.port와 같지만, 포트 충돌 피하려고 다르게 지정 

 

바인딩 (binding)

이 IP와 PORT로 들어오는 요청을 듣겠다는 선언

(접속을 열어둔다, 포트 리슨한다 개념과 비슷)

설정 값 의미 외부에서 접속 가능 여부
127.0.0.1 로컬 전용 ❌ (로컬에서만 접속 가능)
1.1.1.1 이 서버의 실제 IP ✅ (해당 IP로 들어오는 외부 요청도 응답)
0.0.0.0 모든 IP에 바인딩 ✅ (해당 서버가 가진 모든 IP로 요청 수신함)

 

2개 server, 각 1 node

서버A (1.1.1.1) - node1 (master)

서버B (2.2.2.2) - node2 (master)

# 서버 A

version: '3'
services:
  sso-os-node1:
    image: opensearchproject/opensearch:2.18.0
    container_name: sso-os-node1
    environment:
      - cluster.name=sso-cluster                     # OpenSearch 클러스터의 이름
      - node.name=sso-os-node1                       # 이 노드의 이름 (클러스터 내 고유)
      - discovery.seed_hosts=1.1.1.1:10093,2.2.2.2:10093  # 클러스터 내 노드들을 찾기 위한 시드 호스트들 (IP:transport 포트)
      - cluster.initial_master_nodes=sso-os-node1,sso-os-node2  # 클러스터 초기 마스터 노드 후보들 (첫 클러스터 부트스트랩 시 필요)
      - network.host=0.0.0.0                          # 모든 네트워크 인터페이스에서 바인딩 허용
      - network.publish_host=1.1.1.1                  # 다른 노드와 통신 시 공개할 IP 주소
      - transport.publish_port=10093                 # 노드 간 클러스터 통신에 사용되는 transport 포트 (기본은 9300)
      - http.publish_port=10092                      # 외부에 노출할 HTTP 포트 (REST API용, 기본은 9200)
      - bootstrap.memory_lock=true                   
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"     
      - "DISABLE_INSTALL_DEMO_CONFIG=true"           # 데모 보안 설정 설치 비활성화
      - "DISABLE_SECURITY_PLUGIN=true"               # 보안 플러그인 (OpenSearch Security Plugin) 비활성화
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - opensearch-data1:/usr/share/opensearch/data
    ports:
      - 10092:9200
      - 10096:9600
      - 10093:9300
    networks:
      - sso-opensearch-net

volumes:
  opensearch-data1:

networks:
  sso-opensearch-net:
# 서버B

version: '3'
services:
  sso-os-node2:
    image: opensearchproject/opensearch:2.18.0
    container_name: sso-os-node2
    environment:
      - cluster.name=sso-cluster
      - node.name=sso-os-node2
      - discovery.seed_hosts=1.1.1.1:10093,2.2.2.2:10093
      - cluster.initial_master_nodes=sso-os-node1,sso-os-node2
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=0.0.0.0
      - network.publish_host=2.2.2.2
      - transport.publish_port=10093
      - http.publish_port=10092
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - opensearch-data2:/usr/share/opensearch/data
    ports:
      - 10092:9200
      - 10096:9600
      - 10093:9300
    networks:
      - sso-opensearch-net

volumes:
  opensearch-data2:

networks:
  sso-opensearch-net:

 

curl -X GET "http://localhost:9200"
curl -X GET "http://localhost:10092/_cat/nodes?v"

 


2개 server, 각 3 node

서버A (1.1.1.1) - node1 (master), node2, node3

서버B (2.2.2.2) - node4 (master), node5, node6

 

# 서버 A

version: '3'

services:
  250512-cluster-node1:
    image: opensearchproject/opensearch:2.18.0
    container_name: 250512-cluster-node1
    environment:
      - cluster.name=250512-cluster
      - node.name=250512-cluster-node1
      - discovery.seed_hosts=1.1.1.1:10092,1.1.1.1:10095,1.1.1.1:10098,2.2.2.2:10092,2.2.2.2:10095,2.2.2.2:10098
      - cluster.initial_master_nodes=250512-cluster-node1,250512-cluster-node4
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=0.0.0.0
      - network.publish_host=1.1.1.1
      - transport.publish_port=10092
      - http.publish_port=10091
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - 3node-opensearch-data1:/usr/share/opensearch/data
    ports:
      - 10091:9200
      - 10093:9600
      - 10092:9300
    networks:
      - 250512-opensearch-net

  250512-cluster-node2:
    image: opensearchproject/opensearch:2.18.0
    container_name: 250512-cluster-node2
    environment:
      - cluster.name=250512-cluster
      - node.name=250512-cluster-node2
      - discovery.seed_hosts=1.1.1.1:10092,1.1.1.1:10095,1.1.1.1:10098,2.2.2.2:10092,2.2.2.2:10095,2.2.2.2:10098
      - cluster.initial_master_nodes=250512-cluster-node1,250512-cluster-node4
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=0.0.0.0
      - network.publish_host=1.1.1.1
      - transport.publish_port=10095
      - http.publish_port=10094
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - 3node-opensearch-data2:/usr/share/opensearch/data
    ports:
      - 10094:9200
      - 10096:9600
      - 10095:9300
    networks:
      - 250512-opensearch-net

  250512-cluster-node3:
    image: opensearchproject/opensearch:2.18.0
    container_name: 250512-cluster-node3
    environment:
      - cluster.name=250512-cluster
      - node.name=250512-cluster-node3
      - discovery.seed_hosts=1.1.1.1:10092,1.1.1.1:10095,1.1.1.1:10098,2.2.2.2:10092,2.2.2.2:10095,2.2.2.2:10098
      - cluster.initial_master_nodes=250512-cluster-node1,250512-cluster-node4
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=0.0.0.0
      - network.publish_host=1.1.1.1
      - transport.publish_port=10098
      - http.publish_port=10097
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - 3node-opensearch-data3:/usr/share/opensearch/data
    ports:
      - 10097:9200
      - 10099:9600
      - 10098:9300
    networks:
      - 250512-opensearch-net

volumes:
  3node-opensearch-data1:
  3node-opensearch-data2:
  3node-opensearch-data3:

networks:
  250512-opensearch-net:
# 서버 B

version: '3'

services:
  250512-cluster-node4:
    image: opensearchproject/opensearch:2.18.0
    container_name: 250512-cluster-node4
    environment:
      - cluster.name=250512-cluster
      - node.name=250512-cluster-node4
      - discovery.seed_hosts=1.1.1.1:10092,1.1.1.1:10095,1.1.1.1:10098,2.2.2.2:10092,2.2.2.2:10095,2.2.2.2:10098
      - cluster.initial_master_nodes=250512-cluster-node1,250512-cluster-node4
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=0.0.0.0
      - network.publish_host=2.2.2.2
      - transport.publish_port=10092
      - http.publish_port=10091
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - 3node-opensearch-data4:/usr/share/opensearch/data
    ports:
      - 10091:9200
      - 10093:9600
      - 10092:9300
    networks:
      - 250512-opensearch-net

  250512-cluster-node5:
    image: opensearchproject/opensearch:2.18.0
    container_name: 250512-cluster-node5
    environment:
      - cluster.name=250512-cluster
      - node.name=250512-cluster-node5
      - discovery.seed_hosts=1.1.1.1:10092,1.1.1.1:10095,1.1.1.1:10098,2.2.2.2:10092,2.2.2.2:10095,2.2.2.2:10098
      - cluster.initial_master_nodes=250512-cluster-node1,250512-cluster-node4
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=0.0.0.0
      - network.publish_host=2.2.2.2
      - transport.publish_port=10095
      - http.publish_port=10094
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - 3node-opensearch-data5:/usr/share/opensearch/data
    ports:
      - 10094:9200
      - 10096:9600
      - 10095:9300
    networks:
      - 250512-opensearch-net

  250512-cluster-node6:
    image: opensearchproject/opensearch:2.18.0
    container_name: 250512-cluster-node6
    environment:
      - cluster.name=250512-cluster
      - node.name=250512-cluster-node6
      - discovery.seed_hosts=1.1.1.1:10092,1.1.1.1:10095,1.1.1.1:10098,2.2.2.2:10092,2.2.2.2:10095,2.2.2.2:10098
      - cluster.initial_master_nodes=250512-cluster-node1,250512-cluster-node4
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=0.0.0.0
      - network.publish_host=2.2.2.2
      - transport.publish_port=10098
      - http.publish_port=10097
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - 3node-opensearch-data6:/usr/share/opensearch/data
    ports:
      - 10097:9200
      - 10099:9600
      - 10098:9300
    networks:
      - 250512-opensearch-net

volumes:
  3node-opensearch-data4:
  3node-opensearch-data5:
  3node-opensearch-data6:

networks:
  250512-opensearch-net:

 

curl -X GET "http://localhost:10091"
curl -X GET "http://10.10.224.2:10091/_cat/nodes?v"


기록..

 

노드 1개씩 먼저 테스트 후 노드 3개로 늘려서 테스트 했었는데

discovery(각 노드들이 서로 발견)는 했지만 election(마스터 노드 선출)을 하지 못해서 클러스터 구성이 완료되지 않았었다.

 

docker logs를 통해 node1의 로그를 확인해보니

[WARN ][ClusterFormationFailureHelper] cluster-manager not discovered or elected yet,
an election requires a node with id [N3k2No9ATkSEu8mUQ8Uk3A],

have discovered:
{node1}, {node2}, {node3}, {node4}, {node5}, {node6}
→ 즉, node1~6까지는 살아 있음

하지만 id N3k2... 은 없음 → 그래서 마스터를 못 뽑음

→ discovery will continue... (계속 찾고는 있음)

 

이전 클러스터 상태에 꼭 있어야할 노드 ID가 (N3k2...)가 기록되어 있었고

예전 상태가 남아 있어 해당 노드가 없기 때문에 마스터를 못 뽑고 있던 상황이었다.

 

원인은

노드1개, 3개를 올렸던 docker-compose에서 볼륨을 opensearch-data1로 같이 쓰고 있었고

docker-compose down으로 볼륨까지 삭제됐다고 생각했지만 컨테이너와 네트워크를 삭제시켜주고 볼륨은 삭제되지 않았다.

 

따라서 opensearch-node1의 /user/share/opensearch/data 볼륨은 그대로 남아있었고

그래서 클러스터 stat 파일(nodes/, cluster-state*)도 그대로 남아있었다.

 

docker-compose down -v볼륨까지 삭제했어야했다.

 

 

---

 

여기까지 한 서버가 죽어도 다른 서버의 OpenSearch 노드는 계속 동작하므로 이중화가 되었다.

이중화의 핵심은 OpenSearch 자체가 클러스터링 기능을 제공하기 때문에 가능하다.

 

(예를 들어, FastAPI 같은 일반 앱은 이중화를 직접 구성해야 하며

로드 밸런서, DB 연결, 세션 공유 등을 별도로 설계해야하니까!)

 

이중화 외 자동 복구 기능을 제공하지 않는다.

따라서 컨테이너 자체가 죽거나 서버가 죽으면 수동으로 재시작이 필요하다.

-> 서비스 이중화는 되지만, 인프라 자동 복구는 없음

 

자동 복구 설정 (Docker Compose 기준)

컨테이너 단위 자동 복구를 위해 restart:always를 추가하면 된다.

단, 서버 자체가 죽는 경우까지 자동 복구하려면 systemd 등록 등 별도 작업이 필요하다.

services:
  sso-os-node1:
    ...
    restart: always  # 컨테이너 자동 복구 설정
    
services:
  sso-os-node2:
    ...
    restart: always  # 컨테이너 자동 복구 설정

 

restart까지 추가하면 OpenSearch의 클러스터링 기능 기반 이중화 + Docker Compose 수준의 자동 복구까지 포함한 구조가 된다.

 


 

지금까지는 Docker의 기본 네트워크 모드인 bridge 모드를 사용했다.

 

ports 매핑 없이 더 간편하게 docker-copmose를 구성하고자 한다면 host 모드를 사용할 수 있다.

 

  bridge host
IP/포트 컨테이너 고유 IP 사용
ports:로 호스트와 매핑
호스트 IP/포트를 그대로 공유
매핑 불필요
외부 접근 ports: "9200:9200" 필요 컨테이너 내 localhost:9200 → 곧바로 호스트 9200
격리성 컨테이너끼리 격리 가능 격리 없음 (보안 약함)
포트 충돌 없음 (외부 포트 다르게 매핑 가능) 호스트 포트 직접 사용 → 충돌 위험
성능 NAT 있음 (약간 느림) NAT 없음 (조금 더 빠름)
사용 예 운영환경, 포트 분리 필요 시 개발환경, 간편 테스트 시

 

 

아래는 host를 사용한 docker-compose 다.

2개 server, 각 2 node

서버A (1.1.1.1) - node1 (master), node2

서버B (2.2.2.2) - node3 (master), node4

 

# 서버 A

version: '3'
services:
  host-os-node1:
    image: opensearchproject/opensearch:2.18.0
    network_mode: host  # host 네트워크 사용 → 컨테이너가 호스트의 IP와 포트를 그대로 사용 (포트 포워딩 불필요)
    container_name: host-os-node1
    environment:
      - cluster.name=host-cluster  
      - node.name=host-os-node1 
      - discovery.seed_hosts=1.1.1.1:9300,2.2.2.2:9300,1.1.1.1:9301,2.2.2.2:9301  # 클러스터를 구성할 때 초기 노드 목록 (서로 연결될 수 있게 IP:PORT 지정)
      - cluster.initial_master_nodes=host-os-node1,host-os-node3  # 최초 마스터 후보 노드들 (클러스터 최초 부트스트랩 시 한 번만 사용됨)
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=1.1.1.1  # 이 노드가 어떤 IP에서 요청을 수신할지 지정 (호스트 IP로 설정해야 외부 통신 가능)
      - transport.publish_port=9300  # 이 노드가 다른 노드들에게 자신을 알릴 때 사용할 포트
      - http.publish_port=10191  # 클라이언트(사용자)가 이 노드에 REST API로 접속할 때 사용할 포트
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - host-opensearch-data1:/usr/share/opensearch/data

  host-os-node2:
    image: opensearchproject/opensearch:2.18.0
    network_mode: host
    container_name: host-os-node2
    environment:
      - cluster.name=host-cluster
      - node.name=host-os-node2
      - discovery.seed_hosts=1.1.1.1:9300,2.2.2.2:9300,1.1.1.1:9301,2.2.2.2:9301
      - cluster.initial_master_nodes=host-os-node1,host-os-node3
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=1.1.1.1
      - transport.publish_port=9301  # 같은 서버의 두 번째 노드이므로 포트를 다르게 지정
      - http.publish_port=10192     # 이 노드의 REST API 포트
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - host-opensearch-data2:/usr/share/opensearch/data

volumes:
  host-opensearch-data1:
  host-opensearch-data2:

 

transport.publish_port=9300 

 

이 노드가 다른 노드들에게 자신을 알릴 때 사용할 포트
        → OpenSearch는 기본적으로 첫 번째 노드는 9300 포트를 사용하고,
 같은 서버에 두 번째 노드를 띄우면 9301, 그 다음은 9302... 이렇게 자동 증가함.
       명확히 통신 포트를 지정해주기 위해 수동으로 지정함.

 

http.publish_port=10191  

 

클라이언트(사용자)가 이 노드에 REST API로 접속할 때 사용할 포트
        → 같은 서버에 여러 노드가 있으므로 포트 충돌 방지 및 명확한 구분을 위해 지정

`network_mode: host`로 인해 이 포트가 곧바로 외부에 노출됨
       

`network.publish_host` 또는 `transport.publish_port`를 따로 지정하지 않은 이유:

기본적으로 `network.host`, `transport.tcp.port`값을 따라가므로 따로 지정하지 않아도 됨.

# 서버 B

version: '3'
services:
  host-os-node3:
    image: opensearchproject/opensearch:2.18.0
    network_mode: host
    container_name: host-os-node3
    environment:
      - cluster.name=host-cluster
      - node.name=host-os-node3
      - discovery.seed_hosts=1.1.1.1:9300,2.2.2.2:9300,1.1.1.1:9301,2.2.2.2:9301
      - cluster.initial_master_nodes=host-os-node1,host-os-node3
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=2.2.2.2
      - transport.publish_port=9300
      - http.publish_port=10191
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - host-opensearch-data3:/usr/share/opensearch/data
  host-os-node4:
    image: opensearchproject/opensearch:2.18.0
    network_mode: host
    container_name: host-os-node4
    environment:
      - cluster.name=host-cluster
      - node.name=host-os-node4
      - discovery.seed_hosts=1.1.1.1:9300,2.2.2.2:9300,1.1.1.1:9301,2.2.2.2:9301
      - cluster.initial_master_nodes=host-os-node1,host-os-node3
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
      - network.host=2.2.2.2
      - transport.publish_port=9301
      - http.publish_port=10192
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - host-opensearch-data4:/usr/share/opensearch/data
volumes:
  host-opensearch-data3:
  host-opensearch-data4:

 

 

DNS (Domain Name System)

인터넷에서 도메인 이름(예: example.com)을 IP 주소(예: 1.2.3.4)로 변환하는 시스템

웹 브라우저나 애플리케이션이 도메인 이름으로 서버에 접속할 때,

DNS 서버에 요청하여 해당 도메인의 IP 주소를 알아냄

 

/etc/hosts 파일

컴퓨터 내부에서 DNS 조회 없이 도메인 이름과 IP 주소를 직접 매핑하는 파일

이 파일에 특정 도메인 이름과 IP 주소를 등록하면, 해당 컴퓨터에서는 DNS 서버에 문의하지 않고

/etc/hosts 파일의 정보를 사용하여 접속함

 

import aiohttp
import asyncio

async with aiohttp.ClientSession() as session:
    async with session.get('http://example.com') as response:
        print("응답 상태 코드:", response.status)
        print("응답 내용:", await response.text())

위 코드는 url에 지정된 주소로 HTTP POST 요청을 보냄

 

url에 도메인 이름(예: example.com)이 사용되었다면,

애플리케이션은 해당 도메인의 IP 주소를 알아내야 함

DNS 설정이 제대로 되어 있지 않으면 Name or Service not known 오류가 발생할 수 있음

 

url에 IP 주소(예: 1.2.3.4)가 직접 사용되었다면,

도메인 이름을 IP 주소로 변환하는 과정이 필요하지 않음

 

Docker Compose에서 extra_hosts 설정은

컨테이너 내부의 /etc/hosts 파일에 특정 호스트 이름과 IP 주소의 매핑을 추가할 수 있음

 

version: "3"
services:
  web:
    image: nginx:latest
    extra_hosts:
      - "example.com:1.2.3.4"

 

 

'네트워크' 카테고리의 다른 글

Keepalived + HA Proxy 이중화 구성  (0) 2025.06.12
[네트워크] CDN이란?  (0) 2023.01.20
[네트워크] REST API란  (0) 2023.01.20

 

1. SSH 키 생성 (ssh-keygen)

먼저 로컬 머신에서 SSH 키를 생성해야 한다.

 

* 키가 이미 존재하는지 확인

ls -l ~/.ssh/

id_rsa, id_ecdsa, id_ed25519 등 여러 키가 있을 수도 있음

보통 id_rsa 또는 id_ed25519가 가장 많이 사용됨

파일이 존재하면 건너뛰고, 없으면 새로 생성

 

* SSH 키 생성

ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

옵션

-t rsa : RSA 방식의 키 생성

-b 4096 : 4069비트 길이 (더 강력한 보안)

-C "email@example.com" : 키에 주석 추가 (선택 사항)

 

키 생성 후 ~/.ssh 디렉토리에 id_rsa(개인키)와 id_rsa.pub(공개키)가 생성됨

 

2. SSH 에이전트에 키 추가 (ssh-add)

SSH 키를 사용하려면 SSH 에이전트에 추가해야 한다.

(이미 추가된 경우 생략 가능!)

 

* 키 추가하기

ssh-add ~/.ssh/id_rsa

* 추가된 키 확인

ssh-add -l

* 출력 예시

4096 SHA256:xxxxx... user@hostname (RSA)

 

3. 서버에 공개키 복사 (ssh-copy-id)

생성한 SSH 키를 서버로 전송하여 비밀번호 없이 접속할 수 있도록 설정

ssh-copy-id 계정@host

비밀번호를 한 번 입력하면 자동으로 ~/.ssh/authorized_keys 파일에 공개키가 추가된다.

 

* 정상적으로 추가되었는지 확인

ssh 계정@host "cat ~/.ssh/authorized_keys"

4.  SSH 자동 로그인 테스트

이제 SSH 접속 시 비밀번호 없이 자동 로그인이 되어야 한다.

ssh user@192.168.1.100

만약 계속 비밀번호를 묻는다면, 다음 사항을 확인해야 한다.

 

- 서버의 ~/.ssh/authorized_keys에 공개 키가 제대로 추가되었는지

- ~/.ssh 디렉터리와 authorized_keys 파일의 권한이 올바른지

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
 

5. Alias 설정

매번 ssh user@1.2.3.4를 입력하는 대신, A 한 글자로 빠르게 접속할 수 있도록 alias를 설정할 수 있다.

echo "alias A='ssh A@1.2.3.4'" >> ~/.bashrc
source ~/.bashrc

이후 터미널에 A만 입력하여 바로 접속 가능하다.

'업무 > 메모' 카테고리의 다른 글

[vegeta] 부하 테스트  (0) 2023.11.14
[Gunicorn]max_requests와 max_requests_jitter  (0) 2023.11.09
[개발환경 구성] 모놀로식 아키텍처  (0) 2023.07.04
[mathplotlib] 한글 깨짐  (0) 2023.06.12
[Python] 이모지 제거  (0) 2023.02.08