OpenStack을 공부하다 보면 대부분 Neutron(네트워킹)에서 벽을 만납니다. 저도 그랬습니다. 컴퓨트·스토리지는 직관적인데, 네트워크는 개념이 겹겹이라 헷갈리죠. 이번 글은 홈랩 랩에서 삽질하며 정리한 Neutron 핵심 개념 지도입니다. 이거 하나 잡으면 OpenStack의 절반은 끝납니다.
1. 왜 Neutron이 어려운가
Neutron은 소프트웨어로 네트워크(스위치·라우터·방화벽)를 통째로 구현합니다(SDN). 물리 네트워크 지식 + 가상 네트워크 개념 + 여러 에이전트가 한꺼번에 얽혀서 어렵게 느껴지는 겁니다. 그래서 개념을 계층으로 쪼개서 봐야 합니다.
2. 두 가지 네트워크 — Provider vs Tenant
Provider 네트워크
Tenant(Project) 네트워크
정체
기존 물리망에 직접 연결
프로젝트별 격리된 가상망
관리 주체
관리자
사용자(테넌트)
용도
외부와 바로 통신
내부 인스턴스끼리
격리
없음(공용)
VLAN/VXLAN으로 격리
쉽게 말해 Provider는 “회사 실제 네트워크에 꽂는 선”, Tenant는 “내 프로젝트만의 사설망”입니다. 실제 클라우드에선 테넌트망(사설) + 라우터 + 외부 provider망 조합을 가장 많이 씁니다.
3. 핵심 구성요소
ML2 플러그인 + L2 에이전트: 실제 스위칭 담당(Open vSwitch 또는 Linux Bridge). VLAN/VXLAN으로 망을 나눔.
L3 에이전트: 가상 라우터. 테넌트망 ↔ 외부망 라우팅, Floating IP 처리.
DHCP 에이전트: 인스턴스에 IP 자동 할당.
Security Group: 인스턴스 단위 방화벽(포트 허용/차단).
4. Floating IP — 외부 접속의 핵심
인스턴스는 보통 사설 IP만 가집니다. 외부에서 접속하려면 Floating IP(외부망의 공인/실 IP)를 인스턴스에 “붙였다 뗐다” 합니다. 개념이 NAT과 같아서, Floating IP ↔ 인스턴스 사설 IP를 라우터(L3 에이전트)가 매핑합니다.
외부 사용자 → Floating IP(외부망) → [L3 라우터/NAT] → 인스턴스 사설 IP
“인스턴스는 만들었는데 SSH가 안 돼요”의 99%는 Floating IP 미할당 + Security Group에서 22번 미허용입니다. 이 둘을 먼저 확인하세요.
5. 홈랩 랩에서의 배치
컨트롤러: neutron-server(API) + DHCP/L3 에이전트
컴퓨트: L2 에이전트(OVS/Linux Bridge)만
외부망은 Proxmox의 Linux Bridge(vmbr)에 물려 provider 네트워크로 노출
즉 Proxmox의 브리지가 OpenStack provider 네트워크의 물리 기반이 됩니다. 중첩 구조라 처음엔 헷갈리지만, “Proxmox 브리지 = 물리망, 그 위에 Neutron 가상망”으로 이해하면 정리됩니다.
6. 공부 순서
Provider 네트워크부터: 기존 망에 인스턴스를 직접 붙여 “통신되는 것”을 먼저 경험.
Tenant 네트워크 + 라우터: 사설망을 만들고 라우터로 외부와 연결.
Floating IP + Security Group: 외부 접속과 방화벽까지.
7. 정리
Neutron은 “Provider(실제망) vs Tenant(가상 사설망)”와 “L2(스위칭)·L3(라우팅/Floating IP)·Security Group(방화벽)”이라는 두 축으로 나눠 보면 정리됩니다. 외부 접속이 안 될 땐 Floating IP와 Security Group부터. 이 구조가 손에 익으면 OpenStack이 훨씬 만만해집니다.
로컬 LLM 성능을 비교할 때 가장 위험한 문장은 ‘이 GPU면 충분히 빠르겠지’입니다. 실제 운영에 가까워질수록 병목은 GPU 코어 하나로 설명되지 않거든요. 프롬프트를 읽는 prefill, 토큰을 하나씩 생성하는 decode, KV cache가 차지하는 메모리, tokenizer와 HTTP 클라이언트, 동시 요청 스케줄링이 같이 움직입니다.
저는 벤치마크를 볼 때 숫자 하나만 믿지 않습니다. 초당 토큰 수가 좋아도 첫 토큰이 늦으면 채팅 UX는 답답합니다. 반대로 TTFT가 괜찮아도 동시 요청을 올렸을 때 p95 지연 시간이 크게 튀면 내부 서비스로 쓰기 불안하더라고요.
이 글은 vLLM을 띄우는 법을 소개하는 데서 멈추지 않습니다. 어떤 값을 고정하고, 무엇을 바꿔야 비교가 성립하는지, vLLM 옵션이 성능·비용·안정성에 어떤 영향을 주는지, 결과를 보고 운영선을 어떻게 잡는지까지 정리합니다. 특정 GPU의 성능 수치는 지어내지 않겠습니다. 대신 여러분 장비에서 재현할 수 있는 로컬 LLM 성능 측정 절차와 판단 기준을 남기겠습니다.
로컬 GPU 서버에서 vLLM API 서버를 띄우고, 벤치마크 클라이언트가 요청을 보내며, GPU/시스템 지표를 함께 수집하는 흐름입니다.
vLLM 벤치마크 전에 성능 지표를 운영 관점으로 잡기
vLLM은 OpenAI 호환 API 서버로 사용할 수 있는 LLM 추론 엔진입니다. 핵심은 여러 요청을 효율적으로 스케줄링하고, KV cache를 관리하며, 서버형 추론에서 처리량을 끌어올리는 데 있습니다. 여기서 성능 지표는 사전식 정의보다 ‘어떤 장애를 미리 알려주는가’로 보는 편이 훨씬 쓸모 있습니다.
TTFT(Time To First Token): 사용자가 요청한 뒤 첫 토큰이 나오기까지의 시간입니다. 길면 모델이 똑똑해도 느리게 느껴집니다. 긴 입력, prefill 부하, 큐 대기 시간이 주로 영향을 줍니다.
TPOT(Time Per Output Token): 첫 토큰 이후 출력 토큰 하나를 생성하는 평균 시간입니다. 답변이 길어질수록 체감 속도를 좌우합니다.
ITL(Inter Token Latency): 토큰 사이 간격입니다. 스트리밍 응답이 뚝뚝 끊기는지 확인할 때 봅니다.
E2E Latency: 요청부터 최종 응답 완료까지의 전체 시간입니다. 배치 요약, 문서 처리, 자동화 워크플로에서는 이 값이 더 중요할 수 있습니다.
Throughput: 초당 처리한 요청 수 또는 토큰 수입니다. 사용자 수를 늘릴 때 비용 효율을 판단하는 기준입니다.
Concurrency: 동시에 처리 중인 요청 수입니다. vLLM의 장점은 보통 단일 요청보다 이 구간에서 더 잘 드러납니다.
GPU Memory: 모델 가중치, KV cache, CUDA graph, 런타임 오버헤드가 함께 차지하는 메모리입니다. OOM은 대개 모델 크기 하나가 아니라 컨텍스트 길이와 동시성이 합쳐져 발생합니다.
현장에서 가장 많이 본 착시는 ‘초당 토큰이 높으니 빠르다’는 판단입니다. 문서 요약처럼 입력이 긴 워크로드는 prefill 비용이 커서 TTFT가 먼저 무너질 수 있습니다. 반대로 짧은 채팅을 많이 받는 서비스는 decode보다 큐 대기와 스케줄링이 더 큰 문제가 되기도 합니다. 그래서 LLM 벤치마크는 항상 워크로드를 먼저 정하고 들어가야 합니다.
LLM 벤치마크 환경은 README처럼 고정하세요
LLM 벤치마크에서 비교가 깨지는 가장 흔한 이유는 실험 조건이 매번 조금씩 달라지는 겁니다. 모델만 같아도 토크나이저 버전, dtype, max model length, 요청 분포, temperature, 스트리밍 여부가 바뀌면 숫자는 달라집니다. 실험을 시작하기 전에 아래 항목을 먼저 적어두세요. 이거 진짜 편합니다. 나중에 결과가 이상할 때 기록이 없으면 원인 추적이 거의 감으로 변하거든요.
‘테스트 데이터셋’이라는 말에 너무 겁먹을 필요는 없습니다. 사내 문서 요약용이면 실제 문서에서 민감정보를 제거한 샘플 30~100개, 코딩 보조용이면 짧은 질문·중간 길이 수정 요청·긴 파일 기반 요청을 나눠 준비하면 됩니다. 핵심은 매번 같은 입력 분포를 쓰는 것입니다.
vLLM 서버는 옵션 파일로 남기세요
설치는 환경 의존성이 큽니다. CUDA, PyTorch, 드라이버 조합은 공식 설치 문서를 확인하는 편이 안전합니다. 아래 명령은 NVIDIA GPU가 있는 Linux 환경에서 시작점을 잡는 예시로 보세요. 벤치마크 재현성을 위해 서버 실행 옵션은 셸 히스토리에만 남기지 말고 YAML 파일로 고정하는 걸 권합니다.
아래는 단일 GPU에서 시작할 때의 보수적인 예시입니다. 모델 경로는 여러분 환경에 맞게 바꾸세요. vLLM의 --config는 YAML 설정 파일을 읽을 수 있으며, 같은 값이 CLI와 설정 파일에 동시에 있으면 CLI 인자가 우선합니다. 그래서 실제 운영에서는 중복을 줄이고, 실험에서는 어떤 값이 적용됐는지 로그로 한 번 더 확인하는 습관이 좋습니다.
max_model_len은 서버가 받아들일 입력+출력의 최대 토큰 길이입니다. 무작정 크게 잡으면 긴 문서를 받을 수는 있지만 KV cache 여유가 줄어 동시성이 줄고 OOM 가능성이 커집니다. gpu_memory_utilization은 vLLM이 모델 실행에 사용할 GPU 메모리 비율입니다. 높이면 더 큰 KV cache를 확보할 수 있지만, 드라이버·라이브러리·다른 프로세스가 쓸 여유 공간은 줄어듭니다.
max_num_seqs는 한 번의 스케줄링 반복에서 처리할 수 있는 시퀀스 수의 상한으로 이해하면 쉽습니다. 짧은 채팅을 많이 받는 서버라면 늘려볼 가치가 있고, 긴 문서 요약처럼 요청 하나가 큰 경우에는 과하게 늘릴수록 지연 시간과 메모리 압박이 커질 수 있습니다. enable_prefix_caching은 같은 시스템 프롬프트나 반복 prefix가 많은 워크로드에서 효과를 기대할 수 있지만, 요청 prefix가 매번 완전히 다르면 체감이 작을 수 있습니다.
vLLM 서버 실행 옵션과 벤치마크 클라이언트가 연결되는 구조를 한눈에 볼 수 있는 구성도입니다.
추론 성능 측정은 워밍업, 단일 요청, 동시성 순서로 갑니다
서버가 떠도 바로 측정값을 믿으면 안 됩니다. 첫 요청에는 모델 로딩 이후 남은 초기화, CUDA kernel 준비, 캐시 관련 비용이 섞일 수 있습니다. 저는 보통 헬스 체크, 워밍업, 단일 요청 확인, 동시성 테스트 순서로 갑니다. 이 순서를 지키면 서버가 느린 건지, 첫 요청만 느린 건지, 클라이언트가 병목인지 분리하기 쉽습니다.
curl -s http://127.0.0.1:8000/v1/models | jq .
for i in 1 2 3; do
curl -s http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "/models/your-llm-model",
"messages": [{"role": "user", "content": "warmup"}],
"max_tokens": 16,
"temperature": 0
}' > /dev/null
echo warmup-$i-done
done
간단한 smoke test에서는 응답이 오는지만 보지 말고 usage 필드도 확인하세요. 프롬프트 토큰과 completion 토큰이 예상보다 크게 다르면 테스트 입력이 통제되지 않은 것입니다. 특히 한국어와 영어는 토크나이저에서 토큰 수가 다르게 나올 수 있으니, 같은 글자 수가 곧 같은 부하라는 착각을 피해야 합니다.
직접 만든 부하 스크립트도 유용합니다. 아래 코드는 스트리밍 응답에서 첫 chunk가 도착하는 시점과 전체 완료 시간을 분리해 기록합니다. 전문 도구를 대체하진 않지만, TTFT와 E2E가 왜 다르게 움직이는지 눈으로 확인하기 좋습니다.
import asyncio
import json
import time
import httpx
URL = 'http://127.0.0.1:8000/v1/chat/completions'
MODEL = '/models/your-llm-model'
payload = {
'model': MODEL,
'messages': [
{'role': 'user', 'content': 'vLLM으로 로컬 LLM 성능을 측정하는 기준을 설명해줘.'}
],
'max_tokens': 128,
'temperature': 0,
'stream': True,
}
async def one_request(client, idx):
start = time.perf_counter()
first_chunk_at = None
chunks = 0
async with client.stream('POST', URL, json=payload, timeout=120) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if not line.startswith('data: '):
continue
data = line.removeprefix('data: ').strip()
if data == '[DONE]':
break
chunks += 1
if first_chunk_at is None:
first_chunk_at = time.perf_counter()
end = time.perf_counter()
ttft = None if first_chunk_at is None else first_chunk_at - start
return {'request': idx, 'ttft_sec': ttft, 'e2e_sec': end - start, 'chunks': chunks}
async def run(concurrency):
limits = httpx.Limits(max_connections=concurrency, max_keepalive_connections=concurrency)
async with httpx.AsyncClient(limits=limits) as client:
results = await asyncio.gather(*(one_request(client, i) for i in range(concurrency)))
for item in results:
print(json.dumps(item, ensure_ascii=False))
if __name__ == '__main__':
asyncio.run(run(concurrency=4))
이 스크립트를 concurrency=1, 2, 4, 8로 바꿔가며 돌려보면 패턴이 보입니다. TTFT만 급격히 늘면 큐 대기나 prefill 경쟁을 의심하고, TTFT는 괜찮은데 E2E가 길어지면 decode 구간의 경쟁이나 출력 길이 분포를 봅니다. 에러가 섞이면 평균값은 잠깐 내려놓고 실패율부터 봐야 합니다.
vLLM 내장 벤치마크로 AI 성능 비교 기준선 만들기
직접 만든 스크립트는 원인을 좁히는 데 좋고, 반복 가능한 비교에는 vLLM의 벤치마크 명령이 편합니다. 버전에 따라 세부 옵션은 달라질 수 있으니 vllm bench serve --help를 먼저 확인하세요. 핵심은 backend, endpoint, model, dataset 또는 synthetic 입력 조건, 동시성, 프롬프트 수를 명시하는 것입니다.
여기서 주의할 점은 ‘벤치마크 클라이언트에서 측정한 지연 시간’이라는 사실입니다. 서버 내부 처리 시간, 네트워크, 클라이언트 이벤트 루프, JSON 파싱이 한 덩어리로 섞일 수 있습니다. 그래서 같은 서버에서 한 번, 다른 머신에서 한 번, 가능하면 클라이언트 CPU 사용률까지 같이 보는 편이 좋습니다.
데이터셋 기반 테스트를 할 때는 입력 길이 분포를 꼭 기록하세요. 평균 토큰만 보면 안 됩니다. p95 입력 길이가 긴 데이터셋은 짧은 요청 다수보다 훨씬 거칠게 서버를 흔듭니다. 특히 문서 요약형 워크로드는 평균보다 꼬리가 문제입니다. 운영 장애는 보통 평균 요청이 아니라 긴 요청 몇 개가 큐를 막을 때 시작됩니다.
로컬 LLM 성능 테스트에서 자주 보이는 실패 모드
OOM과 느린 첫 응답은 표면 증상입니다. 같은 OOM이라도 원인은 모델이 너무 큰 경우, 컨텍스트를 과하게 연 경우, 동시성을 너무 높인 경우, logprobs 같은 옵션이 메모리를 더 쓰는 경우로 나뉩니다. 해결책도 다릅니다. 무조건 작은 모델로 내리면 문제는 사라지지만, 품질과 비용의 균형점을 찾을 기회를 잃습니다.
운영에서는 ‘성능을 더 뽑는 옵션’보다 ‘실패할 때 예측 가능한 옵션’이 더 가치 있을 때가 많습니다. 예를 들어 gpu_memory_utilization을 올리면 실험실 처리량은 좋아질 수 있지만, 같은 GPU에서 모니터링 에이전트나 다른 프로세스가 함께 돌면 여유 메모리가 부족해질 수 있습니다. 벤치마크가 목적이면 한계를 밀어보고, 서비스가 목적이면 일부러 여유를 남기는 선택이 맞습니다.
검증과 결과 해석: 평균값보다 꺾이는 지점을 찾으세요
추론 성능 측정에서 제가 가장 신뢰하는 그림은 동시성별 TTFT, TPOT, E2E latency, tokens/sec, 실패율을 한 표에 놓은 것입니다. 평균만 있으면 위험합니다. 최소한 p50과 p95를 같이 보세요. p50은 평소 체감, p95는 불만이 시작되는 구간에 가깝습니다.
단일 요청에서 TTFT가 제품 UX에 맞는가?
동시성을 올렸을 때 처리량이 증가하는 구간과 지연만 늘어나는 구간이 어디인가?
먼저 오는 한계가 GPU 메모리인지, decode 처리량인지, 클라이언트 병목인지 구분했는가?
실패율이 0에 가까운 구간과 단순히 평균이 좋은 구간이 같은가?
예를 들어 내부 문서 요약 챗봇을 만든다고 해보겠습니다. 사용자는 한 번에 긴 문서를 넣고 답변을 기다립니다. 이 경우 짧은 프롬프트로 초당 토큰 수만 재면 거의 도움이 안 됩니다. 2천 토큰, 8천 토큰, 16천 토큰처럼 입력 길이를 나누고, 출력 길이는 고정한 뒤 TTFT와 E2E를 봐야 합니다. 반면 짧은 고객 응대 챗봇이면 입력 길이보다 동시 요청과 p95 TTFT가 더 중요합니다.
동시성 증가에 따른 응답 시간, 처리량, GPU 메모리 사용량을 함께 보여주는 결과 대시보드 이미지입니다.
비교 목적
고정할 값
바꿀 값
판단 기준
모델 후보 비교
프롬프트, max_tokens, 동시성, vLLM 옵션
모델 경로 또는 revision
품질 평가는 별도로 두고, 여기서는 지연·처리량·메모리만 비교합니다.
동시성 한계 확인
모델, 입력 길이, 출력 길이
max-concurrency, 클라이언트 수
처리량 증가가 둔화되고 p95 지연이 급등하기 직전이 운영 후보입니다.
긴 컨텍스트 영향
모델, 출력 길이, 동시성
입력 토큰 길이, max_model_len
TTFT와 GPU 메모리가 함께 증가하는지 봅니다.
서버 옵션 튜닝
모델, 테스트 데이터, 클라이언트
gpu_memory_utilization, max_num_seqs
평균 처리량보다 실패율과 p95 안정성을 우선합니다.
prefix cache 효과 확인
시스템 프롬프트, 모델, 동시성
반복 prefix 여부, enable_prefix_caching
같은 prefix가 많은 워크로드에서만 의미 있게 비교합니다.
설정값 선택 기준: 빠르게보다 맞게 고르는 게 먼저입니다
vLLM 옵션은 레버처럼 움직입니다. 하나를 올리면 다른 쪽에서 비용을 냅니다. max_model_len을 키우면 긴 입력을 받을 수 있지만 KV cache 예산이 커지고, max_num_seqs를 키우면 동시 처리 가능성은 늘지만 요청 하나의 지연 시간이 튈 수 있습니다. 그래서 설정은 ‘최대 성능’이 아니라 ‘내 워크로드에 맞는 실패 방식’을 고르는 일에 가깝습니다.
상황
추천 방향
피해야 할 선택
개인 실험, 단일 사용자
작은 max_num_seqs, 필요한 만큼의 max_model_len, 단일 요청 TTFT 확인
운영도 안 할 긴 컨텍스트를 크게 열어 메모리를 낭비하는 것
짧은 채팅을 여러 사용자가 사용
동시성 단계 테스트, p95 TTFT 기준 운영선 설정
평균 tokens/sec만 보고 동시성 상한을 높게 잡는 것
긴 문서 요약
입력 길이 버킷별 측정, max_model_len 보수적 설정, 긴 요청 별도 큐 고려
짧은 프롬프트 벤치마크 결과를 긴 문서 처리에 적용하는 것
동일 시스템 프롬프트 반복
enable_prefix_caching 효과를 별도 실험
prefix가 매번 다른데 캐시 효과를 기대하는 것
한 GPU에 다른 프로세스도 실행
gpu_memory_utilization을 낮춰 여유 확보
벤치마크 순간 최대 처리량만 보고 메모리를 끝까지 쓰는 것
제 기준의 기본값은 이렇습니다. 처음에는 max_model_len을 실제 요청 p95보다 약간 큰 수준으로 잡고, gpu_memory_utilization은 여유를 남긴 값에서 시작합니다. 그다음 동시성을 올리며 p95 TTFT와 실패율을 봅니다. 처리량이 조금 더 나오더라도 p95가 급격히 튀는 지점은 운영선으로 잡지 않습니다.
자주 묻는 질문
Q. vLLM만 쓰면 무조건 빨라지나요?
아닙니다. vLLM은 서버형 추론, 특히 동시 요청 처리에서 강점이 큽니다. 하지만 단일 요청을 짧게 돌리는 실험에서는 차이가 작을 수 있고, 모델이 GPU 메모리에 빠듯하게 올라가는 환경에서는 설정을 잘못 잡으면 오히려 불안정해질 수 있습니다.
Q. 로컬 LLM 성능 비교에서 가장 먼저 볼 지표는 뭔가요?
채팅 UX라면 TTFT와 p95 TTFT를 먼저 봅니다. 배치 요약이나 내부 자동화라면 E2E latency와 처리량을 더 봅니다. 여러 사용자가 붙는 서비스라면 평균보다 실패율과 p95가 우선입니다.
Q. 블로그에 있는 벤치마크 숫자를 믿어도 되나요?
참고는 됩니다. 다만 그대로 의사결정에 쓰면 위험합니다. GPU, 드라이버, vLLM 버전, 모델 revision, 입력 길이, 출력 길이, 동시성, 스트리밍 여부가 다르면 결과가 바뀝니다. 같은 절차로 내 장비에서 다시 재는 것이 가장 확실합니다.
Q. 양자화 모델은 항상 좋은 선택인가요?
메모리 절감에는 유리할 수 있지만 품질, 지원 커널, decode 성능, 운영 안정성을 함께 봐야 합니다. ‘올라간다’와 ‘서비스하기 좋다’는 다릅니다. 양자화 여부를 바꿀 때는 모델 품질 평가와 추론 성능 평가를 분리하세요.
Q. GPU 사용률이 낮으면 vLLM 설정 문제인가요?
항상 그렇지는 않습니다. 클라이언트가 요청을 충분히 못 넣거나, CPU 토크나이징이 막히거나, 네트워크/HTTP 연결이 병목일 수 있습니다. 서버 옵션을 바꾸기 전에 클라이언트가 실제로 원하는 동시성을 만들고 있는지 확인하는 편이 빠릅니다.
마지막 판단: 운영선은 가장 빠른 지점이 아니라 덜 흔들리는 지점입니다
로컬 LLM 성능에서 중요한 질문은 ‘어떤 모델이 제일 빠른가’가 아니라 ‘내 서버가 어떤 요청 분포까지 예측 가능하게 버티는가’입니다. 저는 단일 최고 점수보다 반복 측정에서 비슷하게 나오는 구간을 더 신뢰합니다. 운영자는 평균값이 아니라 나쁜 날의 p95를 상대해야 하니까요.
개인 실험용이면 작은 모델부터 시작해 TTFT와 GPU 메모리 여유를 확인하세요. 여러 사용자가 붙는 내부 서비스라면 vLLM 서버를 띄운 뒤 동시성별 p50/p95 TTFT, E2E latency, tokens/sec, 실패율을 함께 기록하세요. 긴 문서 요약처럼 입력이 긴 워크로드라면 짧은 채팅 벤치마크를 버리고 입력 길이 버킷을 따로 만들어야 합니다.
제가 실제로 권하는 절차는 단순합니다. 워밍업을 제외하고, 같은 프롬프트 세트와 같은 출력 길이로, 동시성을 1에서 시작해 단계적으로 올립니다. 처리량이 더 늘지 않거나 p95 지연 시간이 갑자기 커지거나 실패가 섞이는 지점이 나오면, 그 직전 단계를 운영 후보로 잡습니다. 그다음 max_model_len, max_num_seqs, gpu_memory_utilization을 하나씩만 바꿔 다시 측정합니다.
다음 글에서는 Prometheus와 Grafana를 붙여 vLLM 추론 서버의 요청 지표, GPU 메모리, 지연 시간 분포를 시각화하는 방법을 다룰 예정입니다. 관련 글로는 GPU 모니터링, 모델 서빙 아키텍처, LLM 비용 최적화 글을 함께 연결하면 독자가 다음 단계로 넘어가기 좋습니다. 벤치마크는 한 번 찍는 스크린샷이 아니라 운영 기준을 만드는 과정입니다.
TTFT, 처리량, 동시성, GPU 메모리 기준으로 로컬 LLM 성능 비교 방법을 요약한 인포그래픽입니다.
제 기준의 한 줄 권고는 이렇습니다. 빠른 숫자 하나를 찾지 말고, 같은 조건에서 동시성을 올리며 p95 지연 시간과 실패율이 꺾이는 지점을 찾으세요. 그 직전이 여러분 로컬 LLM 서버의 현실적인 운영선입니다.
OpenStack을 처음 보면 Nova, Neutron, Cinder, Keystone, Glance… 낯선 이름에 압도됩니다. 하지만 각각이 “클라우드의 한 부품”이라고 생각하면 의외로 명료합니다. 제가 홈랩 랩에서 공부하며 정리한, 핵심 컴포넌트 지도를 공유합니다.
1. 컴포넌트 한눈에
컴포넌트
역할
익숙한 비유
Keystone
인증·권한(Identity)
로그인·출입증
Glance
OS 이미지 저장소
설치 ISO 창고
Nova
컴퓨트(인스턴스 생성)
VM을 찍어내는 공장
Neutron
네트워킹(가상 네트워크)
가상 스위치·라우터
Cinder
블록 스토리지(볼륨)
붙였다 뗐다 하는 디스크
Horizon
웹 대시보드
관리 콘솔
여기에 오브젝트 스토리지 Swift, 오케스트레이션 Heat 등이 더 있지만, 위 6개가 “인스턴스 하나 띄우기”의 핵심입니다.
2. 인스턴스 하나 띄울 때 무슨 일이 벌어지나
컴포넌트가 어떻게 맞물리는지는 “VM 하나 생성” 흐름을 따라가면 단번에 이해됩니다.
1) 사용자 → Keystone 에 로그인 → 토큰 발급 (이후 모든 요청에 사용)
2) Nova 에 "인스턴스 생성" 요청
3) Nova → Glance 에서 OS 이미지 가져옴
4) Nova → Neutron 에 네트워크/포트 요청 (IP 할당)
5) (선택) Nova → Cinder 에서 볼륨 붙임
6) Nova 스케줄러가 적당한 compute 노드 선택 → VM 부팅
7) Horizon 대시보드에서 상태 확인
즉 Keystone(인증)으로 문을 열고 → Nova(컴퓨트)가 지휘하면서 → Glance(이미지)·Neutron(네트워크)·Cinder(볼륨)를 불러다 조립하는 구조입니다. 이 흐름 하나만 머리에 넣으면 나머지가 술술 풀립니다.
컴퓨트 노드: Nova-compute·Neutron 에이전트 — 실제 인스턴스가 여기서 돎
스토리지: Cinder 백엔드(LVM/Ceph 등)
그래서 컨트롤러는 CPU·메모리를, 컴퓨트는 인스턴스용 자원을 더 챙겨줘야 합니다. 제 랩에서 컴퓨트 노드에 RAM을 더 준 이유가 이것입니다.
4. 공부 순서 추천
Keystone부터: 인증이 안 되면 아무것도 안 됩니다. 토큰·프로젝트·롤 개념 먼저.
Glance → Nova: 이미지 올리고 인스턴스 하나 띄워보기(성취감 큼).
Neutron: 가장 어렵습니다. 네트워크가 되면 절반은 끝난 것.
Cinder: 볼륨 붙이기까지 하면 기본은 완성.
5. 정리
OpenStack은 이름이 많아 겁나 보이지만, “인증(Keystone) → 컴퓨트(Nova)가 이미지·네트워크·볼륨을 조립”이라는 큰 그림 하나면 충분히 잡힙니다. 각 컴포넌트를 따로 외우지 말고, 인스턴스 생성 흐름 속에서 이해하세요. 홈랩 랩에서 직접 인스턴스를 한 번 띄워보면 이 모든 게 손에 익습니다.
[Kubernetes] Cluster Autoscaler에서 Karpenter로 마이그레이션: 언제, 어떻게 전환할까요?
안녕하세요, 13년차 서버실 지킴이입니다. 오늘은 Kubernetes(쿠버네티스) 환경에서 노드 오토스케일링(Auto Scaling)을 고민하는 분들을 위한 이야기를 해볼까 합니다. 특히 EKS (Amazon Elastic Kubernetes Service)를 운영하면서 Cluster Autoscaler (CA)의 한계를 느끼고 Karpenter로의 마이그레이션을 고민하는 분들이라면, 제가 직접 겪었던 경험과 삽질 끝에 얻은 노하우가 도움이 될 거예요.
클라우드 환경에서 Kubernetes 클러스터를 운영하다 보면, 워크로드(Workload)의 변화에 따라 노드를 유연하게 늘리고 줄이는 것이 정말 중요합니다. 비용 최적화는 물론이고, 서비스 안정성에도 직결되거든요. 처음에는 Cluster Autoscaler(클러스터 오토스케일러)가 만능인 줄 알았는데, 막상 써보니 아쉬운 점들이 꽤 있더라고요. 특히 급작스러운 트래픽 증가나 스팟 인스턴스(Spot Instance) 활용 시 Karpenter가 보여주는 퍼포먼스는 저를 깜짝 놀라게 했습니다.
오늘은 Cluster Autoscaler에서 Karpenter로의 전환을 언제 고려해야 하는지, 그리고 실제 전환은 어떻게 진행해야 하는지에 대한 저만의 결정 기준과 전략을 솔직하게 공유해 드릴게요. 삽질 과정도 가감 없이 보여드릴 테니, 함께 살펴보시죠!
Karpenter가 도입된 Kubernetes 클러스터의 노드 오토스케일링 아키텍처 개요도
Cluster Autoscaler와 Karpenter: 핵심 개념 차이
본격적인 마이그레이션 이야기를 하기 전에, 두 오토스케일링 도구가 어떻게 다른지 간단히 짚고 넘어갈게요. 쉽게 말해 접근 방식 자체가 다릅니다.
Cluster Autoscaler (CA): 그룹 기반 오토스케일링
작동 방식: Cluster Autoscaler는 AWS Auto Scaling Group (ASG)을 기반으로 작동합니다. 즉, 미리 정의된 ASG의 최소/최대 인스턴스 개수 범위 내에서 노드를 추가하거나 줄이죠.
장점: 비교적 설정이 간단하고, 오랫동안 사용되어 안정성이 검증되었습니다. 기존 EC2 인스턴스 관리 방식에 익숙하다면 접근하기 쉽습니다.
단점:
느린 스케일 아웃(Scale Out) 속도: ASG가 새 인스턴스를 프로비저닝(Provisioning)하는 데 시간이 걸립니다. 워크로드 요청이 급증할 때 노드 부족 현상이 생길 수 있어요.
비효율적인 리소스 활용: 특정 파드(Pod)에 필요한 리소스 타입을 정확히 맞추기 어렵습니다. ASG는 특정 인스턴스 타입이나 몇 가지 인스턴스 타입 묶음으로 구성되니까요. 이 때문에 kube-scheduler (쿠베 스케줄러)가 파드를 노드에 배치하지 못하는 ‘스케줄링 불가(unschedulable)’ 상태가 발생할 수 있습니다.
작동 방식: Karpenter는 스케줄링되지 않은 파드를 직접 감지하고, 해당 파드에 가장 적합한 EC2 인스턴스를 직접 프로비저닝합니다. ASG를 거치지 않고 EC2 RunInstances API를 직접 호출해서 노드를 띄우는 거죠.
장점:
압도적인 스케일 아웃 속도: 필요한 노드를 거의 실시간으로 생성하기 때문에 워크로드 변화에 매우 빠르게 대응합니다. 제가 직접 써보니 CA보다 훨씬 빨랐습니다.
최적의 리소스 활용: 파드가 요구하는 CPU, 메모리, GPU 등 리소스에 맞춰 가장 효율적인 EC2 인스턴스 타입을 선택합니다. 스팟 인스턴스 활용도 극대화해서 비용 절감 효과가 커요.
단순한 관리: ASG를 직접 관리할 필요 없이, Karpenter의 Provisioner (프로비저너) 설정 하나로 노드 프로비저닝 정책을 관리할 수 있습니다.
단점:
초기 학습 곡선: 새로운 개념과 설정이 필요해서 처음에는 좀 낯설 수 있습니다.
클라우드 종속성: 현재는 AWS EKS에 특화되어 있습니다 (다른 클라우드 지원도 개발 중이지만, 현재로선 AWS가 주력입니다).
Karpenter로의 마이그레이션 결정 기준: 언제 전환해야 할까요?
제가 13년차 인프라 엔지니어로서 여러 환경을 경험해보니, 마이그레이션은 항상 신중해야 하더라고요. 무조건 좋다고 따라가는 것보다, 우리 서비스에 어떤 이점이 있을지 명확히 파악하는 게 중요합니다. 다음 질문들에 해당한다면 Karpenter로의 전환을 적극적으로 고려해볼 때입니다.
잦은 스케일링 이벤트와 느린 노드 확장 속도에 불만이 있다:
특히 이벤트 기반(Event-driven) 서비스나 예측 불가능한 트래픽 패턴을 가진 서비스라면 CA의 스케일 아웃 속도가 병목이 될 수 있습니다. "파드가 Pending(대기) 상태인데 노드가 왜 이렇게 늦게 뜨지?" 라는 질문을 자주 하셨다면 Karpenter가 답이 될 수 있습니다.
클라우드 비용 최적화가 시급하다:
CA는 ASG가 정해준 인스턴스 타입 내에서만 노드를 띄우다 보니, 파드의 리소스 요구사항과 정확히 일치하는 인스턴스를 찾기 어렵습니다. Karpenter는 파드에 딱 맞는 최적의 인스턴스를 찾아 스팟 인스턴스까지 적극적으로 활용하기 때문에, 불필요한 리소스 낭비를 줄여줍니다. 저희 홈랩에서도 스팟 인스턴스 활용률이 훨씬 높아지면서 비용이 꽤 절감됐습니다.
노드 타입 관리가 복잡하다고 느낀다:
CA를 사용하면 다양한 워크로드를 위해 여러 ASG를 만들고 관리해야 할 때가 많습니다. Karpenter는 Provisioner (프로비저너) 하나로 다양한 인스턴스 타입을 유연하게 관리할 수 있어 운영 부담이 줄어듭니다.
특정 리소스(GPU, 고성능 CPU 등) 요구사항이 있는 파드가 많다:
머신러닝(Machine Learning) 워크로드처럼 특정 GPU나 고성능 CPU를 요구하는 파드들이 있다면, Karpenter가 해당 파드에 맞는 노드를 즉시 프로비저닝해서 스케줄링 효율을 극대화할 수 있습니다.
Karpenter로의 전환 전략: 실전 구현
이제 Karpenter로 마이그레이션하는 실제 과정을 단계별로 살펴보겠습니다. 저는 EKS 환경을 기준으로 설명드릴게요.
단계 1: Karpenter 설치 및 IAM 권한 설정
Karpenter는 AWS API를 직접 호출해서 EC2 인스턴스를 생성하고 관리해야 하므로, 적절한 IAM(Identity and Access Management) 권한이 필수입니다. 공식 문서에 따라 IAM Role과 Service Account를 생성하고, Karpenter 컨트롤러를 설치합니다. 이 부분은 공식 문서가 워낙 잘 되어 있어서 그대로 따라가시면 됩니다. 핵심은 Karpenter가 EC2, IAM, SSM 등의 리소스에 접근할 수 있는 권한을 부여하는 것입니다.
kubectl get pods -n karpenter
# 예시 출력:
# NAME READY STATUS RESTARTS AGE
# karpenter-controller-xxxxx 1/1 Running 0 5m
단계 2: Provisioner 설정
Karpenter의 핵심은 바로 Provisioner 리소스입니다. 어떤 종류의 노드를, 어떤 조건으로 생성할지 여기에 정의합니다. 처음에는 너무 복잡하게 생각하지 마시고, 가장 기본적인 설정으로 시작하는 것을 추천합니다.
Karpenter Provisioner YAML 설정 예시와 주요 파라미터 설명
apiVersion: karpenter.sh/v1beta1
kind: Provisioner
metadata:
name: default
spec:
# Karpenter가 사용할 AMI Family를 정의합니다. EKS 최신 Optimized AMI를 사용하세요.
# (예: AL2, AL2023, Bottlerocket, Ubuntu)
# Karpenter는 여기에 지정된 AMI Family의 최신 EKS Optimized AMI를 자동으로 선택합니다.
# official EKS AMIs: https://docs.aws.amazon.com/eks/latest/userguide/retrieve-ami-id.html
amiFamily: AL2 # Amazon Linux 2 (EKS Optimized AMI) 또는 AL2023
# 인스턴스 프로비저닝에 대한 요구사항을 정의합니다.
# 여기서는 amd64 아키텍처의 온디맨드 또는 스팟 인스턴스를 사용하도록 설정했습니다.
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand", "spot"] # 스팟 인스턴스 활용을 적극 권장합니다!
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"] # 컴퓨팅, 메모리, 범용 인스턴스 카테고리
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["c5", "c6i", "m5", "m6i", "r5", "r6i"] # 자주 사용하는 인스턴스 패밀리
- key: karpenter.k8s.aws/instance-size
operator: NotIn
values: ["nano", "micro"] # 너무 작은 인스턴스는 제외
# 노드가 어떤 서브넷(Subnet)에 생성될지 태그(Tag)로 정의합니다.
# EKS 클러스터가 사용하는 서브넷 태그와 일치해야 합니다.
subnetSelector:
karpenter.sh/discovery: "your-eks-cluster-name" # EKS 클러스터 이름으로 교체
# 노드가 어떤 보안 그룹(Security Group)을 사용할지 태그로 정의합니다.
securityGroupSelector:
karpenter.sh/discovery: "your-eks-cluster-name" # EKS 클러스터 이름으로 교체
# 노드에 적용될 테인츠(Taints)를 정의합니다.
# 여기에 테인트가 있으면, 해당 테인트를 허용하는 파드만 스케줄링됩니다.
# - key: my-app
# effect: NoSchedule
# 노드 템플릿입니다. SSH 키페어, IAM 인스턴스 프로파일 등을 지정할 수 있습니다.
providerRef:
name: default
# 노드 삭제 정책 (consolidation).
# 기본적으로 파드가 없거나, 더 효율적인 노드로 대체될 수 있으면 노드를 삭제합니다.
consolidation:
enabled: true
# consolidateAfter: 5m # 노드 생성 후 최소 5분은 유지 (선택 사항)
# 노드 TTL(Time To Live) 설정. 노드가 일정 시간 동안 유휴 상태이거나
# 최대 수명을 넘으면 삭제됩니다.
ttlSecondsAfterEmpty: 300 # 노드에 파드가 없으면 300초(5분) 후 삭제
ttlSecondsUntilExpired: 604800 # 노드는 최대 7일(604800초)까지만 유지 (수명 관리)
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: AWSNodeTemplate
metadata:
name: default
spec:
# Karpenter가 사용할 IAM 인스턴스 프로파일을 지정합니다.
# EKS 노드에 필요한 권한이 포함된 프로파일이어야 합니다.
# 보통 `eksctl create iamserviceaccount` 등으로 생성됩니다.
instanceProfile: "KarpenterNodeInstanceProfile-your-cluster-name" # 실제 프로파일 이름으로 교체
# SSH 키페어를 지정하여 노드에 SSH 접속이 가능하도록 합니다. (선택 사항)
# sshName: your-ssh-key-name
# 노드에 추가적인 태그를 붙일 수 있습니다.
tags:
environment: production
managed-by: karpenter
karpenter.sh/cluster-name: "your-eks-cluster-name" # 클러스터 이름으로 교체
위 YAML을 적용하면 Karpenter가 이제 파드를 감지하고 노드를 프로비저닝할 준비가 됩니다. 여기서 중요한 포인트는 requirements 섹션입니다. 어떤 인스턴스 타입을 사용할지, 스팟(Spot)을 쓸지 온디맨드(On-Demand)를 쓸지 등 Karpenter의 동작 방식을 결정하는 핵심 설정입니다. 저는 스팟 인스턴스를 적극적으로 활용해서 비용을 절감하는 방향으로 설정했습니다.
단계 3: Cluster Autoscaler (CA) 노드 그룹 점진적 비활성화
Karpenter가 잘 작동하는지 확인하면서, 기존 CA가 관리하던 노드 그룹(ASG)을 점진적으로 스케일 다운(Scale Down)해야 합니다. 한 번에 모든 노드 그룹을 비활성화하면 서비스 중단 위험이 있으니, 워밍업(Warm-up) 기간을 두는 것이 좋습니다.
새로운 파드를 배포하거나 기존 파드를 재시작하여 Karpenter가 새 노드를 잘 띄우는지 관찰합니다.
Karpenter가 프로비저닝한 노드가 충분히 확보되었다고 판단되면, CA가 관리하는 ASG의 desired capacity를 0으로 설정합니다.
ASG의 최소/최대 인스턴스 수도 0으로 설정하여 CA가 더 이상 노드를 관리하지 않도록 합니다.
# EKS 노드 그룹 목록 확인 (eksctl 사용 예시)
eksctl get nodegroup --cluster your-eks-cluster-name
# 특정 노드 그룹의 desired capacity를 0으로 설정
# (eksctl 명령은 ASG도 함께 조정합니다)
eksctl scale nodegroup --cluster your-eks-cluster-name --name your-nodegroup-name --nodes 0 --nodes-min 0 --nodes-max 0
# 또는 AWS CLI로 ASG 직접 수정 (ASG 이름 확인 필요)
# aws autoscaling update-auto-scaling-group --auto-scaling-group-name your-asg-name --desired-capacity 0 --min-size 0 --max-size 0
이렇게 하면 CA가 관리하던 노드들은 서서히 드레인(Drain)되고 종료되며, Karpenter가 그 역할을 완전히 넘겨받게 됩니다. 이 과정에서 파드들이 새로운 Karpenter 노드로 잘 재스케줄링되는지 꼼꼼히 확인해야 합니다.
주의사항 및 트러블슈팅: 삽질 경험
제가 Karpenter를 도입하면서 겪었던 몇 가지 삽질 경험과 주의사항을 공유합니다. 이걸 미리 아셨다면 여러분은 저보다 훨씬 적게 고생하실 거예요. 😅
⚠️ IAM 권한은 정말 중요합니다!
Karpenter가 EC2 인스턴스를 직접 다루기 때문에 IAM 권한이 조금만 잘못되어도 노드가 뜨지 않습니다. 특히 ec2:RunInstances, ec2:TerminateInstances, iam:PassRole 등의 권한이 제대로 부여되었는지 반드시 확인하세요. 저는 iam:PassRole을 빼먹어서 한참 헤맸습니다. Karpenter Controller 로그를 확인하면 어떤 권한이 부족한지 명확하게 나옵니다.
# Karpenter Controller 로그 확인
kubectl logs -f -n karpenter $(kubectl get pods -n karpenter -l app.kubernetes.io/name=karpenter -o name)
Pod Disruption Budget (PDB) 고려:
Karpenter는 노드 통합(Consolidation) 기능을 통해 불필요한 노드를 효율적으로 제거합니다. 이때 PDB (Pod Disruption Budget)가 설정된 파드들은 Karpenter의 노드 종료 작업을 방해할 수 있습니다. PDB가 너무 엄격하면 Karpenter가 노드를 줄이지 못하고 비용이 낭비될 수 있으니, 워크로드의 특성에 맞춰 PDB를 적절히 설정해야 합니다.
Initial Scale-up Delay (초기 스케일업 지연) 착시:
Karpenter는 ASG를 사용하지 않고 바로 EC2 인스턴스를 띄우기 때문에, 처음 노드가 올라올 때까지의 시간이 CA보다 오히려 길게 느껴질 수 있습니다. 하지만 이는 인스턴스 부팅 및 EKS 클러스터 조인(Join) 과정이 포함되기 때문이고, 실제 “스케줄링되지 않은 파드”를 “노드에 할당”하는 과정은 훨씬 빠릅니다. 이 차이를 이해하고 인내심을 가져야 합니다. 🙂
Spot Instance Interruptions (스팟 인스턴스 중단) 대비:
Karpenter는 스팟 인스턴스를 적극 활용하여 비용을 절감하지만, 스팟 인스턴스는 언제든지 AWS에 의해 중단될 수 있습니다. 중요한 스테이트풀(Stateful) 워크로드에는 스팟 인스턴스를 사용하지 않거나, 중단에 강한 아키텍처(예: 분산 시스템, 재시작 가능한 작업)를 구성하는 것이 중요합니다.
검증 및 결과 확인
Karpenter 마이그레이션이 성공적으로 이루어졌는지 확인하는 방법은 다음과 같습니다.
노드 상태 확인:kubectl get nodes 명령으로 노드가 잘 뜨고 준비(Ready) 상태인지 확인합니다. Karpenter가 띄운 노드에는 특정 레이블(Label)이나 태그가 붙어있으니 쉽게 구분할 수 있습니다.
kubectl get nodes -l karpenter.sh/provisioner-name=default
# 예시 출력:
# NAME STATUS ROLES AGE VERSION
# ip-10-0-x-x.ap-northeast-2.compute.internal Ready <none> 5m v1.28.x
파드 스케줄링 확인: 새로운 파드를 배포하거나, 기존 파드의 리소스 요청을 늘려서 Karpenter가 노드를 빠르게 프로비저닝하고 파드를 스케줄링하는지 확인합니다.
Karpenter Provisioner 상태 확인:
kubectl describe provisioner default
# 이 명령을 통해 Karpenter가 어떤 노드를 프로비저닝하고 있는지,
# 그리고 어떤 이벤트가 발생했는지 상세히 볼 수 있습니다.
AWS EC2 콘솔 확인: EC2 콘솔에서 인스턴스가 Karpenter에 의해 생성되고 종료되는 것을 직접 관찰합니다. 태그를 통해 Karpenter가 관리하는 인스턴스인지 쉽게 알 수 있습니다.
비용 모니터링:AWS Cost Explorer를 통해 마이그레이션 전후의 비용 변화를 모니터링합니다. 특히 스팟 인스턴스 활용률이 높아지면서 비용이 얼마나 절감되었는지 확인하는 것이 중요합니다.
Karpenter 도입 후 EKS 클러스터의 노드 및 파드 스케일링 성능 대시보드
Cluster Autoscaler vs. Karpenter: 비교 및 선택 기준
제가 경험한 바를 바탕으로 두 오토스케일러의 핵심 차이점을 표로 정리해봤습니다. 어떤 상황에서 어떤 도구가 더 유리한지 판단하는 데 도움이 될 거예요.
구분
Cluster Autoscaler (CA)
Karpenter
작동 방식
Auto Scaling Group (ASG) 기반
스케줄링되지 않은 파드 직접 감지, EC2 API 호출
노드 프로비저닝 속도
상대적으로 느림 (ASG의 인스턴스 생성 시간)
매우 빠름 (파드 요구사항에 맞춰 즉시 생성)
리소스 효율성
ASG 인스턴스 타입 내에서 선택, 비효율 가능성
파드 요구사항에 가장 적합한 인스턴스 선택, 높은 효율성
비용 최적화
ASG 설정에 의존, 스팟 활용 제한적
스팟 인스턴스 적극 활용, 비용 절감 효과 큼
관리 복잡성
여러 ASG 관리 필요
Provisioner 리소스 하나로 관리, 단순함
주요 사용 사례
워크로드 변동이 적고 안정적인 환경, 온디맨드 위주
잦은 스케일링, 비용 최적화, 스팟 인스턴스 적극 활용 환경
트러블슈팅 난이도
ASG 문제, CA 로그
Karpenter Controller 로그, IAM 권한, Provisioner 설정
Kubernetes Cluster Autoscaler와 Karpenter의 주요 특징 및 마이그레이션 결정 기준 비교
마무리하며: 더 나은 클러스터 운영을 위한 선택
Cluster Autoscaler에서 Karpenter로의 마이그레이션은 단순히 도구를 바꾸는 것을 넘어, Kubernetes 클러스터의 리소스 관리 효율성과 비용 최적화를 한 단계 끌어올리는 중요한 결정입니다. 제가 직접 경험해보니, 특히 워크로드 변동성이 크고 스팟 인스턴스 활용을 통해 비용을 절감하고자 하는 환경이라면 Karpenter가 단연코 훌륭한 선택지였습니다.
물론 새로운 도구를 도입하는 데는 초기 설정과 학습 비용이 따릅니다. 하지만 Karpenter가 제공하는 유연성과 효율성을 고려하면 충분히 투자할 가치가 있다고 생각합니다. 이 글이 여러분의 Karpenter 마이그레이션 여정에 작은 등불이 되었기를 바랍니다. 혹시 더 궁금한 점이나 제가 겪지 못했던 다른 삽질 경험이 있다면 댓글로 공유해주세요. 함께 배우고 성장하는 것이 인프라 엔지니어의 숙명이 아니겠습니까! 다음번에는 Karpenter의 고급 기능이나 다른 클라우드 환경에서의 활용 방안에 대해서도 이야기해볼 수 있으면 좋겠네요. 🎉
클라우드 엔지니어링을 제대로 공부하려면 결국 OpenStack을 손으로 만져봐야 합니다. 그런데 클라우드를 배우자고 클라우드에 돈을 쓰긴 아깝죠. 그래서 저는 Proxmox 홈랩 안에 OpenStack 멀티노드 랩을 꾸며 두고, 공부할 때만 켭니다. 실제 제 랩 구성과 프라이빗 클라우드 학습 환경을 만드는 법을 공개합니다.
1. 왜 집에서 OpenStack인가
프라이빗 클라우드의 실체를 이해하려면 컨트롤러·컴퓨트·네트워크·스토리지가 어떻게 맞물리는지 직접 봐야 합니다.
퍼블릭 클라우드는 “결과”만 보여주지만, OpenStack은 그 아래 구조를 다 열어 줍니다.
자격증·실무(프라이빗 클라우드 운영) 준비에 이만한 실습 환경이 없습니다.
2. 내 랩의 실제 토폴로지
저는 Proxmox 위에 노드 역할을 나눈 멀티노드 구성과, 간단히 굴리는 올인원 학습용을 함께 둡니다.
노드(VM)
역할
vCPU / RAM
deploy
배포·오케스트레이션
1 / 2GB
control
API·스케줄러·DB·메시지큐
4 / 2GB
compute01~03
실제 인스턴스(VM) 구동
각 1 / 4GB
all-in-one(study)
단일 노드 전체 스택
6 / 16GB
멀티노드는 “진짜 구조”를 배우는 데 좋고, 올인원은 “빠르게 기능만” 볼 때 좋습니다. 둘 다 갖춰두면 학습 목적에 맞게 골라 쓸 수 있습니다.
3. 핵심 전제 — 중첩 가상화(Nested Virtualization)
OpenStack의 컴퓨트 노드는 그 안에서 또 VM(인스턴스)을 돌립니다. 즉 “VM 안의 VM”이라, Proxmox 호스트에서 중첩 가상화가 켜져 있어야 합니다.
# Proxmox 호스트에서 중첩 가상화 확인 (Intel)
cat /sys/module/kvm_intel/parameters/nested # Y 여야 함
# 컴퓨트 노드 VM의 CPU 타입을 host로 (가상화 기능 노출)
qm set 105 --cpu host
이게 빠지면 인스턴스가 아예 안 뜨거나 QEMU 소프트웨어 에뮬레이션으로 기어갑니다. compute 노드엔 반드시 --cpu host를 주세요.
4. 자원 현실 — OpenStack은 무겁다
솔직히 말하면 OpenStack은 홈랩 기준으로 무겁습니다. 올인원만 해도 16GB를 잡아먹고, 멀티노드는 노드 5개가 동시에 떠야 합니다. 그래서 제 원칙은 —
평소엔 꺼둡니다. 학습할 때만 부팅하고, 끝나면 종료해 RAM을 회수합니다.
멀티노드와 올인원을 동시에 켜지 않습니다.
미니 PC 한 대의 RAM 예산 안에서 굴리려면 “쓸 때만 켠다”가 필수입니다.
홈랩에서 상시 운영이 아니라 학습용 랩으로 접근하는 게 현실적입니다.
5. 배포 방식 고르기
방식
특징
추천
DevStack
스크립트로 올인원, 개발·기능 확인용
빠른 입문
Kolla-Ansible
컨테이너 기반 멀티노드, 운영에 가까움
제대로 배우기
수동 설치
컴포넌트 하나씩, 가장 깊이 이해
자격증·심화
입문은 DevStack 올인원으로 “돌아가는 걸” 먼저 보고, 구조를 이해했으면 Kolla-Ansible로 멀티노드에 도전하는 흐름을 권합니다.
6. 정리
OpenStack은 무겁지만, 프라이빗 클라우드의 내부를 이해하는 데 이만한 교재가 없습니다. Proxmox의 중첩 가상화 위에 노드를 나눠 올리고, 필요할 때만 켜는 학습 랩으로 운영하면 미니 PC 한 대로도 충분히 공부할 수 있습니다. 다음 글에서는 OpenStack과 Proxmox 중 무엇을 선택해야 하는지, 그리고 핵심 컴포넌트를 하나씩 뜯어보겠습니다.
Proxmox 호스트를 재부팅했는데, 앱은 떴지만 그 앱이 붙어야 할 데이터베이스는 아직 안 떠서 서비스가 깨진 경험, 있으신가요? onboot=1만 켜두면 이런 일이 생깁니다. 이번 글은 부팅 자동 시작과 시작 순서(startup order) 이야기입니다 — 제 홈랩의 (아직 안 잡은) 현실과 함께요.
1. onboot — 자동 시작의 기본
Proxmox에서 onboot=1은 “호스트가 켜지면 이 게스트도 자동으로 시작”입니다. 제 홈랩의 주요 서비스는 전부 켜져 있습니다.
# 컨테이너/VM 자동 시작 켜기
pct set 113 --onboot 1 # LXC
qm set 106 --onboot 1 # VM
# 현재 설정 확인
pct config 113 | grep onboot
여기까지는 대부분 합니다. 문제는 “언제, 어떤 순서로”가 빠져 있다는 겁니다.
2. 솔직한 고백 — 내 홈랩엔 순서가 없다
제 홈랩을 점검해 보니 주요 서비스가 모두 onboot=1이지만 startup 순서·지연은 하나도 설정돼 있지 않았습니다. 즉 호스트가 부팅되면 다 같이 우르르 시작합니다.
이게 왜 위험하냐면 — 예를 들어 앱 컨테이너가 DB 컨테이너보다 먼저 떠버리면, 앱이 DB에 붙지 못하고 에러를 뱉거나 죽습니다. 스토리지(NAS)에 의존하는 서비스가 NAS보다 먼저 뜨는 것도 마찬가지고요. 지금은 운 좋게 문제가 안 났지만, 의존성이 있는 서비스라면 시간문제입니다.
3. 해결 — startup order와 지연
Proxmox는 게스트별로 시작 순서(order)·시작 후 지연(up)·종료 대기(down)를 지정할 수 있습니다.
# DB 먼저(order 낮을수록 먼저), 30초 뒤 다음
pct set 124 --startup order=1,up=30 # meal-db (먼저)
pct set 125 --startup order=2 # meal-app (DB 다음)
# 스토리지/NAS는 가장 먼저
pct set 100 --startup order=0,up=20 # nas-fileserver
파라미터
의미
order
시작 순서(작을수록 먼저, 종료는 역순)
up
이 게스트 시작 후 다음까지 대기(초)
down
종료 시 강제 종료 전 대기(초)
핵심 원칙은 “의존 대상을 먼저”입니다. 스토리지 → DB → 애플리케이션 순으로 order를 매기고, 뒤 서비스가 준비될 시간을 up으로 벌어 주면 됩니다.
4. 실전 팁
up 지연을 아끼지 말 것: DB가 완전히 준비되는 데 시간이 걸립니다. 10~30초 여유를 두면 경합이 사라집니다.
종료는 역순: Proxmox가 order 역순으로 종료하므로, 앱이 먼저 내려가고 DB가 나중에 내려갑니다(정상).
order 없는 게스트는 맨 마지막: 순서를 지정하지 않으면 지정된 것들 뒤에 시작됩니다. 그래서 중요 의존성엔 반드시 order를 명시하세요.
5. 정리
onboot=1은 “자동 시작”일 뿐, “올바른 순서”까지 보장하지 않습니다. DB·스토리지에 의존하는 서비스가 있다면 --startup order,up으로 순서와 지연을 잡아 주세요. 저처럼 “지금까지 운 좋게 안 터진” 상태라면, 다음 재부팅에서 당하기 전에 미리 잡아두시길 권합니다. 저도 이 글을 쓰며 정리해야 할 숙제로 남겨둡니다.
Terraform State 파일 오류는 대개 Terraform이 기억하는 세계와 실제 클라우드에 존재하는 세계가 어긋날 때 시작됩니다. 에러 메시지는 lock, drift, import, backend처럼 흩어져 나오지만, 현장에서 보면 원인은 보통 세 가지입니다. 같은 state를 동시에 만졌거나, 콘솔·스크립트·다른 파이프라인이 리소스를 바꿨거나, 내가 생각한 backend가 아닌 다른 state를 보고 있는 경우입니다.
저도 홈랩의 Proxmox VM과 AWS 테스트 계정을 Terraform으로 같이 관리하다가 state가 꼬인 적이 있습니다. plan은 말이 안 되는 삭제를 보여주고, apply는 lock에 막히고, 이미 콘솔에서 지운 인스턴스를 Terraform은 아직 관리 중이라고 믿고 있더라고요. 그때 배운 건 단순합니다. Terraform 디버깅은 명령어를 많이 아는 게임이 아니라, state, configuration, real infrastructure 중 어느 쪽이 틀렸는지 분리하는 작업입니다.
이 글은 공식 문서 요약보다 운영 중 손을 멈춰야 할 지점, 바로 실행해도 되는 명령, 마지막까지 미뤄야 할 명령을 구분하는 실전 기준에 가깝습니다. 특히 state rm, import, state mv, force-unlock은 모두 강력하지만 잘못 쓰면 문제를 감추거나 더 키울 수 있습니다. 내부 위키에 IaC 배포 절차나 장애 복구 문서가 있다면 이 글과 함께 연결해 두면 리뷰 때 진짜 편합니다.
Terraform CLI, 원격 백엔드, 실제 클라우드 리소스, state lock이 어떻게 연결되는지 보여주는 개요 이미지입니다.
Terraform State 파일 오류는 “지도”보다 “소유권 장부” 문제입니다
Terraform state를 단순히 현재 인프라의 지도라고만 설명하면 절반만 맞습니다. 실무에서는 state를 Terraform이 어떤 실제 객체를 어느 리소스 주소로 관리하는지 기록한 소유권 장부로 보는 편이 더 정확합니다. 예를 들어 aws_instance.web이라는 주소가 실제 AWS 인스턴스 ID i-...에 묶여 있다면, Terraform은 그 주소를 기준으로 변경·삭제·재생성 판단을 합니다.
문제는 이 장부가 코드와 자동으로 완벽히 동기화되지 않는다는 점입니다. Terraform은 .tf 파일, state, provider가 API로 읽어온 실제 리소스 정보를 비교해서 plan을 만듭니다. 그래서 “코드는 맞는데 plan이 이상하다”는 말은 충분히 가능합니다. 코드가 틀린 게 아니라 state가 다른 환경을 가리키거나, 실제 리소스가 콘솔에서 바뀌었거나, provider가 읽어온 값이 이전 실행과 달라졌을 수 있거든요.
Configuration: 우리가 원하는 선언입니다. .tf 파일, 변수, module 호출이 여기에 해당합니다.
State: Terraform이 이미 관리한다고 믿는 객체 목록과 속성입니다.
Real infrastructure: AWS, Azure, GCP, Kubernetes, vSphere 등에 실제로 존재하는 리소스입니다.
Provider: 실제 API를 조회하고 생성·수정·삭제를 수행하는 계층입니다. provider 버전 차이도 plan 결과를 바꿀 수 있습니다.
제가 운영 환경에서 먼저 확인하는 건 코드가 아니라 내가 지금 올바른 backend와 workspace를 보고 있는지입니다. prod 작업이라고 믿고 있었는데 staging state를 보고 있으면, 이후의 모든 분석은 정교한 착각이 됩니다.
흔한 Terraform State 파일 오류 유형과 근본 원인
증상만 보고 바로 명령어를 치면 위험합니다. 같은 plan 이상이라도 원인이 drift인지, backend 오지정인지, 리팩터링 후 주소 변경인지에 따라 복구 방법이 달라집니다.
오류 유형
대표 증상
근본 원인
먼저 볼 것
권장 대응
State Lock
Error acquiring the state lock
다른 Terraform 실행이 state 쓰기 권한을 잡았거나 이전 실행이 비정상 종료됨
CI 실행 상태, lock ID, 실행자 정보, backend lock 저장소
실행 중 작업을 확인한 뒤 고아 lock일 때만 force-unlock
Drift
예상하지 못한 ~, -, -/+가 plan에 표시됨
콘솔 수동 변경, 외부 자동화, provider 기본값 변화
terraform plan -refresh-only, 클라우드 변경 이력
코드로 흡수할지, 실제 값을 되돌릴지 결정
State와 실제 리소스 불일치
삭제된 리소스를 계속 추적하거나 기존 리소스를 새로 만들려 함
수동 삭제, import 누락, state rm 오남용, workspace 혼동
terraform state list, terraform state show, 실제 리소스 ID
.terraform/terraform.tfstate의 backend 메타정보, terraform workspace show
terraform init -reconfigure 후 plan 재검증
리팩터링 후 주소 변경
동일 리소스를 삭제 후 새로 만들려 함
resource 이름, module 경로, for_each key 변경
이전 주소와 새 주소, plan의 -/+ 여부
moved block 또는 terraform state mv
Terraform 디버깅 루틴: apply 전에 보는 7개 체크포인트
문제가 터졌을 때 저는 apply를 바로 다시 실행하지 않습니다. 첫 번째 실행이 실패한 이유를 모른 채 두 번째 실행을 하면 state가 더 헷갈려집니다. 아래 루틴은 로컬 backend, S3 backend, HCP Terraform/Terraform Enterprise 모두에서 큰 틀은 같습니다.
Terraform 버전과 provider lock 파일을 확인합니다.
현재 workspace가 의도한 환경인지 확인합니다.
backend가 어느 state 객체를 보고 있는지 확인합니다.
state에 들어 있는 리소스 주소를 목록화합니다.
실제 리소스 조회와 refresh-only plan으로 drift를 분리합니다.
삭제 또는 재생성 액션이 있으면 영향 큰 리소스부터 읽습니다.
state 변경 명령은 백업 또는 원격 backend 버전 복구 가능성을 확인한 뒤 실행합니다.
# 0. 버전과 provider 잠금 파일 확인
terraform version
ls -la .terraform.lock.hcl
# 1. 내가 보고 있는 workspace 확인
terraform workspace show
terraform workspace list
# 2. backend 재초기화가 필요한 상황인지 확인
terraform init -reconfigure
# 3. state에 등록된 주소 확인
terraform state list
terraform state list 'module.network.*'
# 4. 특정 리소스가 어떤 실제 ID에 연결되어 있는지 확인
terraform state show aws_instance.web
# 5. 실제 인프라를 읽어 state와의 차이만 확인
terraform plan -refresh-only
# 6. 일반 plan은 파일로 저장해서 리뷰 가능하게 남김
terraform plan -out=tfplan
terraform show tfplan
terraform plan -refresh-only는 인프라를 바꾸겠다는 뜻이 아니라 실제 객체를 읽어서 state가 어떻게 갱신될지를 보여주는 모드입니다. 다만 terraform apply -refresh-only까지 실행하면 state가 실제 값에 맞게 업데이트될 수 있습니다. 운영에서는 plan과 apply의 차이를 분명히 나눠야 합니다. 조회만 하려면 plan -refresh-only에서 멈추고, state 동기화까지 의도한 경우에만 apply 단계로 갑니다.
반대로 terraform plan -refresh=false는 실제 리소스 조회를 건너뜁니다. 빠를 수는 있지만 오래된 state를 기준으로 plan을 만들기 때문에 drift 디버깅에는 부적합합니다. 저는 대규모 환경에서 provider API 호출 비용이나 속도가 부담될 때 임시 비교용으로만 쓰고, 운영 적용 판단에는 거의 쓰지 않습니다.
터미널에서 workspace, state list, refresh-only plan을 순서대로 확인하는 장면을 표현한 이미지입니다.
IaC 상태 관리의 핵심: 원격 백엔드와 S3 key
팀 작업에서는 로컬 state보다 원격 backend가 기본값에 가깝습니다. 로컬 파일은 간단하지만 동시 실행 제어, 접근 제어, 백업, 감사 추적이 약합니다. AWS 환경이라면 S3 backend를 많이 씁니다. 현재 Terraform S3 backend는 use_lockfile = true로 S3 기반 state locking을 사용할 수 있고, DynamoDB 기반 locking은 deprecated 상태라 신규 구성에서는 S3 lockfile을 우선 검토하는 편이 좋습니다.
여기서 가장 자주 터지는 건 bucket보다 key입니다. 같은 bucket 안에 dev/network/terraform.tfstate, staging/network/terraform.tfstate, prod/network/terraform.tfstate를 넣는 구조는 흔합니다. 그래서 key 경로를 한 글자만 잘못 넣어도 전혀 다른 환경의 장부를 펼쳐놓고 작업하게 됩니다. 제 기준으로 prod backend는 코드 리뷰에서 bucket보다 key를 더 오래 봅니다.
init -reconfigure는 현재 디렉터리의 backend 설정을 다시 읽게 합니다. state를 다른 위치로 옮기는 migration과는 다릅니다. Terraform이 state migration 여부를 묻는 상황에서는 메시지를 끝까지 읽어야 합니다. 운영 state를 잘못 옮기면 plan 이상보다 복구가 더 피곤해집니다.
State Lock 해결: force-unlock은 마지막에 씁니다
State Lock 해결을 검색하면 대부분 terraform force-unlock LOCK_ID가 먼저 보입니다. 하지만 이 명령은 lock을 푸는 도구이지, 현재 실행 중인 apply가 안전하게 끝났다는 증거가 아닙니다. 누군가 실제로 apply 중인데 강제로 풀면 두 개의 실행이 같은 state를 쓰려고 할 수 있습니다.
ps aux | grep '[t]erraform'
terraform workspace show
terraform force-unlock LOCK_ID
terraform force-unlock -force LOCK_ID
제가 force-unlock을 허용하는 기준은 꽤 보수적입니다. 첫째, 현재 로컬·CI/CD·동료 작업 중 실행 중인 plan 또는 apply가 없어야 합니다. 둘째, lock 에러의 실행자·시간·operation 정보가 현재 살아 있는 작업과 맞지 않아야 합니다. 셋째, unlock 후 바로 apply하지 않고 plan부터 다시 봐야 합니다.
상황
해야 할 일
하지 말아야 할 일
CI job이 아직 실행 중
job 종료 또는 취소 결과를 기다림
lock ID가 보인다는 이유만으로 force-unlock
노트북이 꺼져 apply가 중단됨
클라우드 리소스 생성 상태와 state 반영 여부를 확인
unlock 후 바로 apply 재시도
오래된 고아 lock으로 확인됨
terraform force-unlock LOCK_ID 후 plan 검증
backend 저장소를 직접 수정하는 것으로 시작
lock 저장소 권한 오류
S3 또는 DynamoDB 권한, profile, region을 확인
lock 기능을 꺼서 우회
S3 lockfile을 쓴다면 lock 파일에 대한 s3:GetObject, s3:PutObject, s3:DeleteObject 권한을 확인해야 합니다. DynamoDB lock을 쓰는 기존 구성이라면 lock 테이블 권한과 region을 봐야 합니다. lock 문제를 Terraform 문제가 아니라 IAM 문제로 풀어야 하는 경우도 꽤 많습니다.
State 불일치 복구: rm, import, mv를 섞어 쓰지 마세요
Terraform State 파일 오류 중 가장 헷갈리는 구간입니다. state rm, import, state mv는 모두 state를 만지지만 목적이 다릅니다. 저는 이 셋을 “추적 끊기, 추적 시작하기, 주소 바꾸기”로 외웁니다. 이 구분만 잡아도 사고 확률이 확 줄더라고요.
상황
사용 명령
의미
사용하면 안 되는 경우
실제 리소스는 삭제됐는데 state에만 남음
terraform state rm
Terraform의 추적만 제거
리소스를 실제로 삭제하려는 목적이면 부적합
실제 리소스가 있는데 Terraform이 모름
terraform import
기존 객체를 state 주소에 연결
코드가 없거나 주소가 틀린 상태에서 성급히 실행
리소스 이름·module 경로만 바뀜
terraform state mv 또는 moved block
같은 실제 객체를 새 Terraform 주소로 이동
실제 객체를 교체해야 하는 변경에는 부적합
설정에서 더 이상 관리하지 않음
removed block 또는 state rm
Terraform 관리 대상에서 제외
팀이 변경 이력을 코드로 남겨야 하는데 임시 명령만 실행
terraform state list
terraform state show aws_instance.web
terraform state rm aws_instance.web
terraform import aws_instance.web i-0123456789abcdef0
terraform state mv aws_instance.web module.compute.aws_instance.web
state rm은 실제 리소스를 삭제하지 않습니다. 이 사실은 단순하지만 사고를 많이 막아줍니다. 반대로 terraform destroy는 실제 리소스를 삭제할 수 있습니다. “state에서 빼고 싶다”와 “클라우드에서 없애고 싶다”는 완전히 다른 요구입니다.
리팩터링이라면 가능하면 코드에 moved block을 남기는 방식을 선호합니다. 이유는 간단합니다. terraform state mv는 실행한 사람의 터미널 기록에만 맥락이 남지만, moved block은 저장소에 의도가 남습니다.
moved {
from = aws_instance.web
to = module.compute.aws_instance.web
}
state에서 제거, 가져오기, 주소 이동이 각각 어떤 상황에 맞는지 정리한 흐름도 이미지입니다.
재현 시나리오: 콘솔에서 삭제된 EC2를 다시 만들려는 경우
가장 흔하고 교육용으로도 좋은 시나리오입니다. Terraform으로 EC2를 만들었는데 누군가 AWS 콘솔에서 직접 삭제했다고 가정하겠습니다. 이후 terraform plan을 실행하면 Terraform은 state에 남아 있는 인스턴스 ID를 기준으로 실제 객체를 조회합니다. provider는 “없다”고 답하고, Terraform은 설정 파일에 리소스가 여전히 있으니 다시 만들 계획을 세웁니다.
Terraform으로 aws_instance.web를 생성합니다.
AWS 콘솔 또는 외부 스크립트로 해당 인스턴스를 삭제합니다.
terraform plan -refresh-only로 state와 현실의 차이를 봅니다.
그 인스턴스가 계속 필요한지, 이미 폐기한 리소스인지 결정합니다.
필요한 리소스면 terraform apply로 재생성을 검토하고, 폐기한 리소스면 코드와 state를 함께 정리합니다.
terraform state show aws_instance.web
terraform plan -refresh-only
terraform plan -out=tfplan
terraform show tfplan
terraform apply tfplan
terraform state rm aws_instance.web
terraform plan
여기서 자주 하는 실수는 state rm을 먼저 실행하는 겁니다. 설정 파일에 리소스 블록이 그대로 남아 있으면 다음 plan에서 Terraform은 “state에는 없지만 코드에는 있으니 새로 만들자”고 판단합니다. 그래서 정말 폐기하려면 코드 제거와 state 정리가 같이 가야 합니다. 필요한 리소스라면 반대로 state를 지우지 말고 Terraform이 재생성하도록 plan을 검토하는 편이 자연스럽습니다.
plan 출력 해석: 기호보다 리소스 종류가 더 중요합니다
Terraform plan의 기호는 기본 문법입니다. 하지만 운영 판단은 기호만으로 하지 않습니다. 같은 ~라도 태그 변경과 데이터베이스 엔진 옵션 변경은 무게가 다릅니다. 같은 -/+라도 무상태 인스턴스와 영구 디스크는 위험도가 다릅니다.
+: 새 리소스 생성입니다. 비용과 quota를 확인합니다.
~: 기존 리소스 변경입니다. in-place 변경인지, provider가 어떤 필드를 바꾸는지 봅니다.
terraform plan -out=tfplan
terraform show tfplan
terraform show -json tfplan > tfplan.json
jq '.resource_changes[] | select(.change.actions | index("delete")) | {address, actions: .change.actions}' tfplan.json
저는 plan 리뷰 때 리소스를 세 그룹으로 나눕니다. 첫째, 재생성되면 장애가 될 수 있는 리소스입니다. VPC, subnet, route table, DB, disk, IAM role, cluster 같은 것들입니다. 둘째, 비용이 바로 붙는 리소스입니다. 인스턴스, NAT gateway, load balancer, managed database가 여기에 들어갑니다. 셋째, 변경되어도 영향이 제한적인 메타데이터입니다. 태그나 설명이 대표적입니다. 이 분류를 해두면 plan 리뷰가 감상이 아니라 판단이 됩니다.
생성, 변경, 삭제, 재생성 항목을 색상별로 구분해 보여주는 Terraform plan 결과 해석 이미지입니다.
성능·비용·안정성에 영향을 주는 결정 포인트
State 디버깅은 단순 복구 작업처럼 보이지만, backend 설계와 운영 습관은 비용과 안정성에 영향을 줍니다. 숫자를 지어낼 필요는 없습니다. 어떤 결정이 어떤 방향의 비용을 만드는지만 알아도 충분히 실수를 줄일 수 있습니다.
결정
안정성 영향
비용·성능 영향
추천 기준
state를 환경별로 분리할지 여부
장애 범위를 줄임
state 수가 늘어 관리 포인트 증가
prod, staging, dev는 최소한 key 또는 workspace로 분리
하나의 거대한 state 사용
한 번의 plan 영향 범위가 커짐
provider 조회가 많아져 plan이 느려질 수 있음
네트워크, 플랫폼, 앱 계층을 무리 없이 나눔
-refresh=false 사용
drift를 놓칠 수 있음
조회가 줄어 빨라질 수 있음
운영 apply 판단에는 사용하지 않음
state lock 비활성화
동시 apply 충돌 위험 증가
구성은 단순해짐
팀 환경에서는 lock 없는 운영을 피함
원격 state 암호화·버전 관리
복구와 보안에 유리
스토리지 정책 관리 필요
민감 값 가능성을 전제로 암호화와 접근 제어 적용
특히 state를 너무 크게 키우는 패턴은 나중에 발목을 잡습니다. 모든 리소스를 하나의 root module과 하나의 state에 넣으면 처음엔 편합니다. 하지만 plan이 느려지고, 작은 변경도 큰 영향 범위를 갖고, lock 대기 시간이 길어집니다. 그렇다고 너무 잘게 쪼개면 remote state 참조와 의존성 관리가 늘어납니다. 저는 “같이 생성되고 같이 롤백되어도 괜찮은 단위”를 state 분리 기준으로 잡습니다.
자주 묻는 질문: Terraform State 파일 오류 FAQ
Q1. terraform.tfstate 파일을 직접 수정해도 되나요?
가능은 하지만 운영에서는 거의 마지막 수단입니다. JSON이라 열어볼 수는 있어도 Terraform 내부 구조, provider schema, 민감 값 처리, serial 값이 얽혀 있습니다. 먼저 terraform state 하위 명령을 쓰고, 원격 backend라면 버전 복구 가능성을 확인한 뒤 진행하세요.
Q2. state 파일을 Git에 올려도 되나요?
일반적으로 올리지 않는 편이 맞습니다. state에는 리소스 속성뿐 아니라 민감한 값이 평문에 가까운 형태로 남을 수 있습니다. 팀 환경에서는 원격 backend, 암호화, 접근 제어, 감사 가능한 실행 경로를 쓰는 쪽이 안전합니다.
Q3. lock 오류가 나면 무조건 force-unlock 하면 되나요?
아닙니다. lock은 귀찮은 장벽이 아니라 state 동시 수정을 막는 안전장치입니다. 실행 중인 작업이 없고 고아 lock이라고 판단될 때만 terraform force-unlock LOCK_ID를 사용하세요.
Q4. import만 하면 코드도 자동으로 완성되나요?
terraform import는 기존 리소스를 state에 연결하는 명령입니다. 코드 작성과 속성 정리는 별도 작업으로 보는 편이 안전합니다. import 후에는 반드시 terraform plan으로 코드와 실제 리소스 차이를 맞춰야 합니다.
Q5. 리소스 이름만 바꿨는데 왜 삭제 후 생성이 뜨나요?
Terraform 주소가 바뀌었기 때문입니다. aws_instance.web를 aws_instance.app으로 바꾸면 Terraform은 기본적으로 다른 리소스로 봅니다. 같은 실제 객체를 유지하려면 moved block이나 terraform state mv로 주소 이동을 알려줘야 합니다.
운영에서 바로 쓰는 판단 기준
Terraform State 파일 오류를 만나면 명령어보다 순서가 중요합니다. 먼저 workspace와 backend를 확인합니다. 그다음 state list, state show, plan -refresh-only로 state와 현실의 차이를 분리합니다. lock 에러라면 실행 중인 작업이 있는지 확인한 뒤 고아 lock일 때만 해제합니다.
이럴 땐 이렇게 하시면 됩니다. 콘솔에서 지운 리소스를 Terraform이 다시 만들려 한다면, 계속 필요한 리소스인지 먼저 결정하세요. 필요하면 plan 검토 후 apply, 필요 없으면 코드 제거와 state 정리를 같이 합니다. 이름이나 module 경로만 바꿨다면 삭제·재생성을 허용하지 말고 moved block 또는 state mv를 씁니다. lock이 걸렸다면 Terraform이 나를 괴롭히는 게 아니라 state를 보호하는 중이라고 보고, 실행 중인 작업부터 찾습니다.
제가 운영에서 절대 넘기지 않는 신호는 -와 -/+입니다. 특히 네트워크, 데이터베이스, 디스크, IAM, 클러스터 리소스에 보이면 손을 멈추고 이유를 설명할 수 있어야 합니다. 설명할 수 없는 apply는 복구 계획이 없는 변경과 비슷합니다.
정확한 옵션과 backend 동작은 HashiCorp의 Terraform CLI 문서, S3 backend 문서, state 명령 문서를 기준으로 확인하는 습관을 권합니다. 문서는 명령 문법을 확인하는 곳이고, 운영 판단은 현재 state와 실제 인프라를 놓고 해야 합니다. 이 둘을 분리해서 보면 Terraform state 문제는 훨씬 덜 무섭습니다.
Proxmox에서 어떤 서비스를 올릴 때, 무거운 VM을 통째로 띄우는 대신 가벼운 LXC 컨테이너 안에서 Docker를 돌리는 방법이 있습니다. 저는 사진 서버(Immich)도, 이 블로그 자동화 봇도 이 방식으로 운영합니다. 실제 설정과 반드시 알아야 할 함정을 정리합니다.
1. 왜 VM이 아니라 LXC + Docker인가
가볍다: LXC는 호스트 커널을 공유해서 VM보다 메모리·오버헤드가 훨씬 적습니다.
빠르다: 부팅이 거의 즉시고, 자원 낭비가 적습니다.
Docker 생태계 그대로: compose 파일과 이미지를 그대로 씁니다.
단, “다른 커널이 필요하다”거나 “강한 격리가 필수”라면 그땐 VM이 맞습니다. 일반적인 웹서비스·봇·셀프호스팅 앱이라면 LXC + Docker가 가성비 최고입니다.
2. 핵심 — 두 개의 기능 플래그
기본 LXC에 Docker를 깔면 안 돌아갑니다. 두 가지 features를 켜야 합니다.
# 컨테이너(예: 113)에 nesting + keyctl 활성화
pct set 113 --features nesting=1,keyctl=1
플래그
역할
nesting=1
컨테이너 안에서 또 컨테이너(Docker) 실행 허용
keyctl=1
Docker가 쓰는 커널 키링 접근 허용
실제로 제 컨테이너들은 이렇게 잡혀 있습니다 — 봇 컨테이너는 nesting=1,keyctl=1, Immich 컨테이너는 nesting=1. 그리고 둘 다 unprivileged(비특권) 컨테이너입니다. 보안상 가능하면 unprivileged로 두는 걸 권합니다.
3. Docker 설치
플래그를 켜고 컨테이너를 재시작한 뒤, 안에서 평범하게 Docker를 설치하면 됩니다.
# LXC 내부에서 (공식 스크립트)
curl -fsSL https://get.docker.com | sh
docker --version # 예: Docker version 29.4.0
docker compose version
4. 반드시 밟는 함정 — 좀비 프로세스
여기서 많은 분이 당합니다. 컨테이너 안에서 자식 프로세스를 반복 생성하는 앱(브라우저 자동화, 이미지 변환 등)을 Docker로 돌리면, 죽은 자식이 좀비로 쌓입니다. 컨테이너의 PID 1이 이를 거두지 않기 때문입니다(저는 이걸로 좀비 307개를 만든 적이 있습니다 — 관련 글).
# docker-compose.yml — tini를 PID 1로 넣어 좀비 자동 수거
services:
app:
image: my-app:latest
init: true
데이터는 볼륨으로: 컨테이너를 갈아엎어도 데이터가 남게, 영구 데이터는 호스트 경로/NAS 볼륨에 마운트.
백업: LXC 자체를 Proxmox 스냅샷/백업하면 Docker 스택까지 통째로 보존됩니다.
6. 정리
LXC + Docker는 홈랩에서 VM의 무게 없이 Docker 생태계를 쓰는 최적의 절충안입니다. 핵심은 딱 두 가지 — nesting=1과 keyctl=1을 켜는 것, 그리고 서브프로세스 앱엔 init: true를 잊지 않는 것. 이 두 개만 챙기면 미니 PC 한 대에 서비스를 훨씬 많이 얹을 수 있습니다.
홈 오토메이션을 시작하면 “Home Assistant를 어디에 올리지?”부터 막힙니다. 저는 Proxmox 위에 VM으로 올렸습니다. LXC나 Docker가 아니라 VM인 데는 분명한 이유가 있습니다. 실제 제 구성과 그 선택의 근거를 정리합니다.
1. Home Assistant 설치 방식과 선택
Home Assistant는 설치 방식이 여러 가지입니다.
방식
애드온·Supervisor
홈랩 적합도
HAOS (전용 OS)
완전 지원
VM으로 올리면 최고
Container(Docker)
Supervisor 없음
애드온 못 씀
Core(파이썬)
없음
관리 번거로움
애드온과 자동 업데이트를 편하게 쓰려면 HAOS(Home Assistant OS)가 정답인데, HAOS는 Supervisor를 포함한 전용 운영체제 전체입니다. 즉 자기 커널과 OS를 통째로 들고 있어서 LXC(호스트 커널 공유)로는 제대로 못 돌리고, VM이 맞습니다.
2. 실제 VM 구성
제 홈랩의 HAOS VM은 이렇게 잡혀 있습니다.
항목
값
이유
머신 타입
q35 + OVMF(UEFI)
HAOS 공식 권장(UEFI 부팅)
vCPU / RAM
2코어 / 4GB
중간 규모 자동화에 넉넉
디스크
local-zfs(SSD)
반응 속도·스냅샷
QEMU Guest Agent
enabled
정상 종료·IP 보고
포인트는 OVMF(UEFI) + q35입니다. HAOS 이미지는 UEFI 부팅을 전제로 하므로, 구형 SeaBIOS로 만들면 부팅이 안 됩니다. 그리고 QEMU Guest Agent를 켜두면 Proxmox에서 “정상 종료”를 보낼 수 있고 VM의 IP도 대시보드에 표시됩니다.
3. USB 장치(지그비·지웨이브)는?
제 구성엔 USB 패스스루가 없습니다. Wi-Fi·LAN 기반 기기와 네트워크 통합 위주로 쓰기 때문입니다. 만약 Zigbee/Z-Wave USB 동글을 쓴다면, Proxmox에서 해당 USB를 VM에 패스스루해야 합니다.
# USB 동글을 HAOS VM(예: 106)에 패스스루
qm set 106 -usb0 host=1234:5678
동글은 VID:PID로 지정하는 게 포트 번호보다 안정적입니다(포트를 바꿔 꽂아도 유지).
4. 백업 — 업데이트 전 스냅샷이 생명
HAOS는 업데이트가 잦고, 가끔 통합이 깨집니다. 그래서 저는 업데이트 전 Proxmox 스냅샷을 습관처럼 찍습니다.
# HAOS 업데이트 전 VM 스냅샷
qm snapshot 106 pre_haos_update
# 문제 생기면 롤백
qm rollback 106 pre_haos_update
여기에 더해 HAOS 자체 백업(설정 스냅샷)도 병행하면 이중 안전망이 됩니다. VM 스냅샷은 “OS·통합까지 통째로”, HAOS 백업은 “설정만” 되돌립니다.
5. 정리
HAOS는 VM으로. 전용 OS라 LXC로 무리하지 말 것.
q35 + OVMF(UEFI)로 만들고 QEMU Guest Agent를 켤 것.
USB 동글은 VID:PID로 패스스루.
업데이트 전 VM 스냅샷은 무조건.
Proxmox 홈랩이 있다면 Home Assistant를 VM으로 올리는 게 가장 안정적입니다. 2코어·4GB면 충분히 시작할 수 있으니, 홈 오토메이션의 첫 서버로 강력 추천합니다.