13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[작성자:] admin

  • [AI] Ollama로 로컬 AI 환경 구축: LLM 모델 설치 및 활용 완벽 가이드

    클라우드 AI에 지쳐서 직접 내 서버에 AI를 올려봤습니다

    솔직히 말씀드리면, 저도 처음엔 GPT API 쓰면 되지 뭘 굳이 로컬에서 돌리나 싶었거든요. 근데 쓰다 보니까 문제가 생기더라고요. 회사 코드를 붙여넣기 하기가 찜찜하고, API 비용은 은근히 쌓이고, 인터넷 연결이 불안정한 환경에서는 아예 못 쓰고. 이런 상황이 반복되다 보니 결국 로컬 AI 환경 구축을 진지하게 고민하게 됐습니다.

    그러다 발견한 게 Ollama입니다. 처음 써봤을 때 진짜 “이게 이렇게 쉬워도 되나?” 싶을 정도로 간단했어요. 명령어 몇 줄로 LLM 모델 설치가 끝나고, 바로 터미널에서 대화할 수 있거든요. 오늘은 제가 홈랩에서 Ollama를 세팅하면서 겪은 경험을 바탕으로, 처음 시작하시는 분들도 막히지 않도록 단계별로 정리해 드리겠습니다.

    ▲ Ollama를 중심으로 구성된 로컬 AI 환경의 전체 흐름. 사용자 요청이 Ollama 서버를 통해 LLM 모델로 전달되는 구조입니다.

    Ollama란? — 쉽게 말해서 LLM용 Docker

    Ollama를 처음 접하시는 분들을 위해 간단히 설명드릴게요. Ollama는 LLM(대규모 언어 모델)을 로컬 환경에서 쉽게 실행할 수 있도록 도와주는 오픈소스 도구입니다. 쉽게 말해서, 도커(Docker)가 컨테이너 이미지를 pull 받아서 실행하듯이, Ollama는 LLM 모델을 pull 받아서 바로 실행해 준다고 보시면 됩니다.

    기존에 로컬에서 LLM을 돌리려면 Python 환경 세팅하고, 모델 파일 직접 다운로드하고, 의존성 패키지 맞추고… 이게 보통 일이 아니었거든요. 저도 llama.cpp 직접 빌드하다가 CUDA 버전 안 맞아서 몇 시간 날린 적 있습니다. Ollama는 이 복잡함을 싹 없애줍니다.

    • ✅ 단일 바이너리 설치 — 별도 Python 환경 불필요
    • ✅ 모델 허브 제공 — ollama pull 모델명 한 줄로 다운로드
    • ✅ REST API 기본 제공 — 웹 앱 연동이 쉬움
    • ✅ GPU 가속 자동 감지 — NVIDIA, AMD, Apple Silicon 모두 지원
    • ✅ 완전 오프라인 동작 — 모델 다운로드 후에는 인터넷 불필요

    Ollama 지원 모델 한눈에 보기

    Ollama에서 공식적으로 제공하는 모델들 중 자주 쓰이는 것들을 정리해 봤습니다. 처음 시작하신다면 llama3나 mistral부터 해보시는 걸 추천드려요.

    모델명 파라미터 크기 특징 권장 RAM
    llama3 8B / 70B Meta의 최신 오픈소스 모델, 범용 성능 우수 8GB / 40GB+
    mistral 7B 가볍고 빠름, 코딩과 요약에 강점 8GB
    gemma 2B / 7B Google의 경량 모델, 저사양에서도 동작 4GB / 8GB
    qwen 다양한 크기 한국어와 중국어 포함 다국어 성능 양호 모델별 상이
    codellama 7B / 13B 코드 생성과 완성에 특화된 모델 8GB / 16GB
    phi3 3.8B Microsoft의 소형 고성능 모델 4GB

    💡 팁: 모델 파라미터 수가 클수록 성능은 좋지만 그만큼 VRAM과 RAM이 많이 필요합니다. 본인 장비 사양에 맞게 고르는 게 핵심이에요.

    Ollama 설치 — 생각보다 훨씬 간단합니다

    자, 이제 실전으로 넘어가 볼게요. 설치 자체는 정말 간단합니다. OS별로 방법이 조금씩 다른데, 하나씩 보여드릴게요.

    Linux와 macOS 설치

    # 공식 설치 스크립트 (Linux / macOS 공통)
    curl -fsSL https://ollama.com/install.sh | sh

    이 한 줄이면 끝납니다. 진짜로요. 설치 스크립트가 OS를 자동 감지해서 적절한 바이너리를 내려받고 서비스 등록까지 해줍니다. macOS라면 GUI 앱으로도 설치할 수 있는데, 저는 CLI가 더 편해서 위 방법을 씁니다.

    Windows 설치

    Windows는 공식 홈페이지(ollama.com)에서 설치 파일을 받아서 실행하면 됩니다. 설치 후 자동으로 백그라운드 서비스로 뜨거든요.

    Docker로 설치하기 (서버 환경 추천)

    # CPU 전용
    docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
    
    # NVIDIA GPU 사용시 (nvidia-docker 설치 필요)
    docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama

    저는 홈랩 서버에서는 Docker로 올려두고 쓰고 있어요. 관리가 편하거든요. 특히 -v ollama:/root/.ollama 볼륨 마운트를 꼭 해주셔야 모델 파일이 컨테이너 재시작 후에도 유지됩니다. 처음에 이거 빠뜨려서 모델을 다시 받았던 기억이 나네요.

    ▲ Ollama 설치 완료 후 llama3 모델을 pull 받는 터미널 화면. 도커처럼 레이어 단위로 다운로드되는 걸 확인할 수 있습니다.

    LLM 모델 설치 및 실행하기

    Ollama가 설치됐으면 이제 모델을 받아볼 차례입니다. 명령어 구조가 Docker랑 정말 비슷해서, Docker 써보신 분들은 금방 익숙해지실 거예요.

    모델 다운로드 (pull)

    # 모델 목록 확인
    ollama list
    
    # 모델 다운로드 (예: llama3 8B 모델)
    ollama pull llama3
    
    # 특정 버전과 크기 지정
    ollama pull llama3:8b
    ollama pull llama3:70b
    
    # mistral 모델
    ollama pull mistral
    
    # 경량 모델 (저사양 PC 추천)
    ollama pull phi3
    ollama pull gemma:2b

    모델 실행 및 대화하기

    # 터미널에서 바로 대화 (대화형 모드)
    ollama run llama3
    
    # 실행 후 이런 프롬프트가 뜹니다:
    # >>> 여기에 질문을 입력하세요
    # >>> 안녕하세요! 파이썬으로 피보나치 수열 짜는 법 알려주세요
    
    # 종료는 /bye 또는 Ctrl+D
    >>> /bye

    처음 ollama run llama3 입력했을 때 드디어 로컬에서 AI가 답변하는 걸 보고 진짜 신기했습니다. 인터넷 끊어도 되고, API 키도 필요 없고. 이 맛에 로컬 AI 하는 거죠 🎉

    단일 질의 (Non-interactive 모드)

    # 스크립트에서 활용할 때 유용
    ollama run llama3 "리눅스에서 디스크 사용량 확인하는 명령어 알려줘"
    
    # 파이프로 입력 전달
    echo "이 코드 리뷰해줘" | ollama run codellama

    REST API로 호출하기

    Ollama는 기본적으로 11434 포트로 REST API 서버를 띄워줍니다. 이걸 활용하면 어떤 언어에서든 Ollama와 통신할 수 있어요.

    # curl로 API 직접 호출
    curl http://localhost:11434/api/generate -d '{
      "model": "llama3",
      "prompt": "파이썬으로 Hello World 출력하는 법",
      "stream": false
    }'

    응답은 JSON 형식으로 돌아오는데, 여기서 response 필드에 AI의 답변이 들어있습니다. "stream": true로 설정하면 스트리밍 방식으로 답변을 받을 수 있어서 사용자 경험이 더 좋습니다.

    Python에서 Ollama 활용하기

    Python 개발자라면 이렇게 간단하게 Ollama를 연동할 수 있습니다.

    import requests
    import json
    
    def ask_ollama(prompt, model="llama3"):
        url = "http://localhost:11434/api/generate"
        payload = {
            "model": model,
            "prompt": prompt,
            "stream": False
        }
        response = requests.post(url, json=payload)
        return response.json()["response"]
    
    # 사용 예시
    answer = ask_ollama("Ollama가 뭔가요?")
    print(answer)

    이제 Python 스크립트에서 로컬 LLM을 쉽게 활용할 수 있습니다. API 키 없이, 인터넷 연결 없이 말이죠.

  • [AI] LLM 파인튜닝 실전 가이드: LoRA/QLoRA로 도메인 특화 모델 만들기

    “GPT한테 물어봤더니 우리 회사 용어를 하나도 모르더라고요”

    이런 경험 한 번쯤 있으시죠? 저도 작년에 사내 기술 문서 Q&A 봇을 만들려고 ChatGPT API를 붙여봤는데… 일반적인 질문엔 잘 대답하는데 우리 팀 특유의 약어나 내부 시스템 이름을 물어보면 완전 엉뚱한 소리를 하더라고요. RAG(Retrieval-Augmented Generation, 검색 기반 생성)로 어느 정도 해결은 됐는데, 근본적으로 모델 자체가 도메인을 이해하게 하고 싶다는 생각이 들었습니다.

    그래서 시작한 게 LLM 파인튜닝이었는데요. 처음엔 “GPU 수십 장 없이 가능하겠어?” 싶었는데, LoRA(Low-Rank Adaptation)와 QLoRA(Quantized LoRA) 덕분에 홈랩 서버 한 대로도 충분히 가능하다는 걸 알게 됐습니다. 오늘은 제가 직접 삽질하면서 익힌 실전 LLM 파인튜닝 과정을 공유해 드릴게요.

    ▲ LLM 파인튜닝의 전체 파이프라인 — 데이터 준비부터 학습, 추론까지의 흐름을 한눈에 볼 수 있습니다.

    LoRA / QLoRA가 뭔지부터 짚고 넘어가죠

    파인튜닝이라는 개념 자체는 간단합니다. 이미 잘 학습된 모델을 내 데이터로 추가 학습시키는 거예요. 근데 문제가 있습니다. LLaMA 3나 Mistral 같은 모델은 파라미터가 수십억 개거든요. 이걸 전부 다시 학습시키려면 A100 GPU가 여러 장 필요하고, 메모리도 수백 GB가 필요합니다. 현실적으로 홈랩에서 불가능한 수준이죠.

    그래서 나온 게 LoRA(Low-Rank Adaptation, 저랭크 적응 학습)입니다. 쉽게 말해서, 모델 전체를 학습시키는 게 아니라 “변화량”만 학습시키는 방식이에요. 원래 모델의 가중치(weight)는 그대로 얼려두고(freeze), 그 옆에 훨씬 작은 보조 행렬(adapter)만 학습시키는 거거든요.

    그리고 QLoRA(Quantized LoRA)는 여기서 한 발 더 나아가서, 원본 모델을 4비트(4-bit)로 양자화(quantization)해서 메모리를 훨씬 적게 쓰면서 LoRA 학습을 하는 방식입니다. 제가 RTX 3090 한 장으로 13B 모델을 파인튜닝할 수 있었던 게 바로 QLoRA 덕분이었어요.

    방식 전체 파인튜닝 (Full Fine-tuning) LoRA QLoRA
    학습 파라미터 수 전체 (수십억) 소수 (수백만) 소수 (수백만)
    메모리 요구량 매우 높음 중간 낮음
    원본 모델 정밀도 FP16/BF16 FP16/BF16 4-bit (NF4)
    홈랩 가능 여부 ❌ (7B 이상 어려움) △ (7B 정도 가능) ✅ (13B~34B도 가능)
    성능 손실 없음 (기준) 미미 약간 있음

    저처럼 GPU 한 장으로 실험하시는 분이라면 QLoRA가 현실적인 선택입니다. 성능 차이가 생각보다 크지 않아서 실제 서비스에서도 충분히 쓸 수 있는 수준이더라고요.

    환경 준비 — 이 부분에서 삽질 좀 했습니다 ㅎㅎ

    제 홈랩 환경 기준으로 설명드릴게요. CUDA 버전 호환성 때문에 처음에 꽤 고생했거든요. CUDA 버전과 PyTorch 버전, bitsandbytes 버전이 삼각형처럼 맞물려야 합니다. 하나라도 틀리면 에러 폭탄이 터져요.

    # Python 가상환경 먼저 만들어주세요 (conda 추천)
    conda create -n llm-finetune python=3.10
    conda activate llm-finetune
    
    # PyTorch 설치 (CUDA 11.8 기준 — 본인 환경에 맞게 조정)
    pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
    
    # 핵심 라이브러리 설치
    pip install transformers==4.40.0
    pip install peft==0.10.0          # LoRA 구현체
    pip install bitsandbytes==0.43.0  # 4-bit 양자화
    pip install accelerate==0.29.0
    pip install datasets==2.18.0
    pip install trl==0.8.6            # SFT Trainer 포함
    
    # 설치 확인
    python -c "import torch; print(torch.cuda.is_available())"

    💡 팁: bitsandbytes는 Windows에서 설치가 까다롭습니다. WSL2나 Linux 환경을 강력 추천드려요. 저도 처음에 Windows에서 삽질하다가 결국 Ubuntu로 갈아탔습니다.

    학습 데이터 준비 — 사실 이게 제일 중요해요

    파인튜닝에서 코드보다 더 중요한 게 데이터입니다. 진짜로요. 좋은 데이터 1000개가 나쁜 데이터 10만 개보다 낫다는 걸 직접 경험했거든요.

    LLM 파인튜닝용 데이터는 보통 Instruction 형식으로 만듭니다. 이렇게 생겼어요:

    [
      {
        "instruction": "우리 회사의 배포 프로세스를 설명해줘",
        "input": "",
        "output": "우리 회사의 배포 프로세스는 다음과 같습니다. 먼저 개발자가 feature 브랜치에서 작업 후 PR을 올리면..."
      },
      {
        "instruction": "다음 에러 로그를 분석해줘",
        "input": "ERROR: Connection refused to internal-db-01:5432",
        "output": "이 에러는 PostgreSQL 데이터베이스 서버에 연결이 거부된 것입니다. 주요 원인으로는..."
      }
    ]

    이걸 학습 형식으로 변환하는 코드를 보여드릴게요:

    from datasets import Dataset
    import json
    
    def format_instruction(sample):
        """Alpaca 스타일의 프롬프트 포맷"""
        if sample["input"]:
            prompt = f"""### Instruction:
    {sample["instruction"]}
    
    ### Input:
    {sample["input"]}
    
    ### Response:
    {sample["output"]}"""
        else:
            prompt = f"""### Instruction:
    {sample["instruction"]}
    
    ### Response:
    {sample["output"]}"""
        return {"text": prompt}
    
    # 데이터 로드 및 변환
    with open("my_domain_data.json", "r", encoding="utf-8") as f:
        raw_data = json.load(f)
    
    dataset = Dataset.from_list(raw_data)
    dataset = dataset.map(format_instruction)
    print(f"총 학습 데이터: {len(dataset)}개")
    print(dataset[0]["text"][:200])  # 첫 번째 샘플 미리보기

    ⚠️ 주의사항: 데이터 품질 체크는 필수입니다. 중복 데이터, 너무 짧은 응답(10자 이하), 특수문자가 깨진 데이터는 학습 전에 꼭 걸러내세요. 저는 이걸 건너뛰었다가 모델이 이상한 패턴을 학습해서 처음부터 다시 한 적 있습니다.

    QLoRA 파인튜닝 실전 코드

    드디어 본론입니다. 제가 실제로 사용하는 학습 스크립트예요. 주석을 충분히 달아놨으니 따라가 보세요.

    ▲ QLoRA 학습 진행 중 GPU 메모리 사용량과 학습 손실(loss) 변화 — RTX 3090 기준으로 13B 모델도 충분히 학습 가능합니다.

    import torch
    from transformers import (
        AutoModelForCausalLM,
        AutoTokenizer,
        BitsAndBytesConfig,
        TrainingArguments
    )
    from peft import LoraConfig, get_peft_model, TaskType
    from trl import SFTTrainer
    from datasets import load_dataset
    
    # ===== 1. 모델 설정 =====
    # 베이스 모델 선택 (Hugging Face Hub에서 다운로드)
    # 예시: meta-llama/Meta-Llama-3-8B, mistralai/Mistral-7B-v0.1 등
    BASE_MODEL = "mistralai/Mistral-7B-v0.1"  # 본인 용도에 맞게 변경
    OUTPUT_DIR = "./my-domain-model"
    
    # ===== 2. 4-bit 양자화 설정 (QLoRA의 핵심) =====
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,               # 4-bit로 모델 로드
        bnb_4bit_use_double_quant=True,  # 이중 양자화로 메모리 추가 절약
        bnb_4bit_quant_type="nf4",       # NF4 타입 (QLoRA 논문 권장)
        bnb_4bit_compute_dtype=torch.bfloat16  # 계산은 bfloat16으로
    )
    
    # ===== 3. 모델 & 토크나이저 로드 =====
    print("모델 로딩 중... (처음엔 시간이 좀 걸려요)")
    model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL,
        quantization_config=bnb_config,
        device_map="auto",  # GPU 자동 할당
        trust_remote_code=True
    )
    model.config.use_cache = False  # 학습 시엔 꺼야 합니다
    
    tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True)
    tokenizer.pad_token = tokenizer.eos_token  # 패딩 토큰 설정
    tokenizer.padding_side = "right"  # 오른쪽 패딩 (중요!)
    
    # ===== 4. LoRA 어댑터 설정 =====
    lora_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        r=16,             # 랭크(rank) — 클수록 더 많이 학습, 메모리도 더 씀
        lora_alpha=32,    # 스케일링 파라미터 (보통 r의 2배)
        target_modules=[  # 어떤 레이어에 LoRA 적용할지
            "q_proj", "k_proj", "v_proj", "o_proj",
            "gate_proj", "up_proj", "down_proj"
        ],
        lora_dropout=0.05,
        bias="none"
    )
    
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()  # 학습 파라미터 비율 확인
    # 출력 예시: trainable params: 20,185,088 || all params: 3,772,923,904 || trainable%: 0.5348
    
    # ===== 5. 학습 인자 설정 =====
    training_args = TrainingArguments(
        output_dir=OUTPUT_DIR,
        num_train_epochs=3,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,  # 유효 배치 = 4 * 4 = 16
        learning_rate=2e-4,
        fp16=False,
        bf16=True,           # bfloat16 사용 (Ampere 이상 GPU)
        logging_steps=10,
        save_steps=100,
        warmup_ratio=0.03,
        lr_scheduler_type="cosine",
        report_to="none",    # wandb 쓰시면 "wandb"로 변경
        optim="paged_adamw_8bit"  # 메모리 효율적인 옵티마이저
    )
    
    # ===== 6. 데이터셋 로드 =====
    # 앞서 준비한 데이터셋 사용
    dataset = load_dataset("json", data_files="my_domain_data.json", split="train")
    dataset = dataset.map(format_instruction)  # 앞서 정의한 함수
    
    # ===== 7. SFT Trainer로 학습 시작 =====
    trainer = SFTTrainer(
        model=model,
        train_dataset=dataset,
        peft_config=lora_config,
        dataset_text_field="text",
        max_seq_length=2048,
        tokenizer=tokenizer,
        args=training_args,
    )
    
    print("학습 시작!")
    trainer.train()
    
    # ===== 8. 어댑터 저장 =====
    trainer.model.save_pretrained(OUTPUT_DIR)
    tokenizer.save_pretrained(OUTPUT_DIR)
    print(f"\n✅ 학습 완료! 모델 저장 위치: {OUTPUT_DIR}")

    여기서 중요한 포인트! r 값(랭크)을 너무 크게 잡으면 과적합(overfitting) 위험이 있고, 너무 작으면 학습이 덜 됩니다. 저는 도메인 데이터 양에 따라 데이터 1000개 이하면 r=8, 5000개 이상이면 r=16~32를 주로 씁니다.

    학습된 모델로 추론하기

    학습이 끝났으면 실제로 써봐야죠! 저장된 LoRA 어댑터를 베이스 모델에 합쳐서 추론하는 코드입니다.

    from transformers import AutoModelForCausalLM, AutoTokenizer
    from peft import PeftModel
    import torch
    
    BASE_MODEL = "mistralai/Mistral-7B-v0.1"
    ADAPTER_PATH = "./my-domain-model"
    
    # 베이스 모델 로드 (추론 시엔 양자화 없이 할 수도 있어요)
    tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
    model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL,
        torch_dtype=torch.float16,
        device_map="auto"
    )
    
    # LoRA 어댑터 합치기
    model = PeftModel.from_pretrained(model, ADAPTER_PATH)
    model = model.merge_and_unload()  # 어댑터를 모델에 완전히 병합
    model.eval()
    
    def generate_response(instruction, input_text="", max_new_tokens=512):
        if input_text:
            prompt = f"### Instruction:\n{instruction}\n\n### Input:\n{input_text}\n\n### Response:\n"
        else:
            prompt = f"### Instruction:\n{instruction}\n\n### Response:\n"
        
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
        
        with torch.no_grad():
            outputs = model.generate(
                **inputs,
                max_new_tokens=max_new_tokens,
                temperature=0.7,
                do_sample=True,
                top_p=0.9,
                repetition_penalty=1.1  # 반복 방지
            )
        
        response = tokenizer.decode(outputs[0], skip_special_tokens=True)
        # 프롬프트 부분 제거하고 응답만 반환
        return response.split("### Response:")[-1].strip()
    
    # 테스트
    response = generate_response("우리 회사의 배포 프로세스를 설명해줘")
    print(response)

    ⚠️ 실제로 겪은 트러블슈팅 모음

    이 섹션이 사실 제일 중요할 수도 있어요. 저처럼 삽질 안 하시라고 정리해봤습니다.

    문제 1: CUDA out of memory 에러

    가장 흔한 문제입니다. 해결책은 이렇습니다:

    • per_device_train_batch_size를 줄이고 gradient_accumulation_steps를 늘리세요 (유효 배치 크기는 유지하면서)
    • max_seq_length를 줄여보세요. 2048 → 1024로만 줄여도 메모리가 확 줄어듭니다
    • 학습 전에 torch.cuda.empty_cache() 호출

    문제 2: 모델이 Instruction을 무시하고 이상한 말을 함

    데이터 포맷이 안 맞는 경우가 대부분입니다. 학습 데이터의 프롬프트 형식과 추론 시 프롬프트 형식이 완전히 동일해야 합니다. 공백 하나, 줄바꿈 하나도 다르면 모델이 헷갈려해요.

    문제 3: Loss가 안 줄어듦

    Learning rate 문제일 가능성이 높습니다. 2e-4가 일반적이지만, 데이터가 적으면 1e-4로 낮춰보세요. Warmup 비율도 0.03 → 0.05로 올려보는 것도 방법입니다.

    문제 4: 학습 후 모델이 기존 능력을 잃음 (Catastrophic Forgetting)

    이건 LoRA의 장점 중 하나인데, 그래도 데이터에 너무 도메인 특화 내용만 있으면 발생할 수 있어요. 일반 대화 데이터를 10~20% 섞어주면 많이 해결됩니다. Alpaca 데이터셋이나 OpenHermes 같은 공개 데이터셋에서 일부 샘플링해서 섞어주세요.

    ▲ 파인튜닝 전후 도메인 특화 질문에 대한 응답 비교 — 학습 후 도메인 용어와 맥락을 정확히 이해하는 것을 확인할 수 있습니다.

    🎉 결과 확인 — 드디어 됐다!

    제가 실제로 사내 기술 문서 약 2,000개로 파인튜닝했을 때 결과를 공유하면, 파인튜닝 전에는 우리 팀 특유의 시스템 이름이나 약어를 물어보면 모르거나 엉뚱한 답을 했는데, 파인튜닝 후에는 정확하게 내부 컨텍스트에 맞는 답변을 해주더라고요. 특히 “이 에러 코드가 뭔 뜻이야?”류의 질문에서 차이가 극명했습니다.

    모델 평가는 정성적 평가(직접 질문해보기)와 더불어, 보류해둔 테스트 셋으로 간단히 정량 평가도 해보세요. 저는 ROUGE 스코어보다는 실제 사용자 피드백이 더 의미 있다고 생각하는 편이에요.

    # 학습된 모델 파일 구조 확인
    ls -la ./my-domain-model/
    # adapter_config.json    — LoRA 설정 정보
    # adapter_model.safetensors  — 학습된 가중치 (수십~수백 MB)
    # tokenizer.json
    # tokenizer_config.json
    # special_tokens_map.json
    
    # 파일 크기 확인 (베이스 모델 대비 훨씬 작습니다)
    du -sh ./my-domain-model/
    # 예시: 약 80~300MB (베이스 모델은 수 GB인 것과 비교)

    이게 LoRA의 또 다른 장점인데요, 어댑터 파일만 배포하면 되니까 용량이 매우 작습니다. 베이스 모델은 공유하고 어댑터만 바꿔끼는 방식으로 여러 도메인 모델을 관리할 수 있어서 정말 편해요.

    ▲ LoRA/QLoRA 파인튜닝 핵심 파라미터 요약 — r, alpha, target_modules 등 주요 설정값 선택 가이드

    정리 — 오늘 배운 것들

    처음에 “GPU 한 장으로 LLM 파인튜닝이 가능할까?”라는 의심으로 시작했는데, QLoRA 덕분에 충분히 가능하다는 걸 직접 확인했습니다. 핵심만 정리해볼게요.

    • ✅ QLoRA는 소비자급 GPU로도 수십억 파라미터 모델을 파인튜닝할 수 있게 해줍니다
    • ✅ 데이터 품질이 양보다 중요합니다. 적더라도 잘 만든 데이터가 훨씬 낫습니다
    • ✅ LoRA rank(r)는 데이터 양에 맞게 조절하세요. 처음엔 r=16이 무난합니다
    • ✅ 프롬프트 형식을 학습과 추론에서 완전히 동일하게 유지하세요
    • ✅ 일반 데이터를 일부 섞어서 Catastrophic Forgetting을 방지하세요

    다음 글에서는 이렇게 만든 모델을 Ollama나 vLLM으로 서빙(serving)하는 방법을 다룰 예정입니다. API 서버로 띄워서 실제 서비스에 붙이는 과정까지 이어서 정리해드릴게요. 이전 글에서 RAG 구축 과정을 다뤘으니 참고하시면 파인튜닝과 함께 쓰는 방법도 이해하기 쉬울 거예요.

    궁금한 점은 댓글로 남겨주세요. 저도 아직 공부 중이라 같이 논의하면 더 좋을 것 같습니다 😊

    자주 묻는 질문 (FAQ)

    Q. 최소 얼마나 많은 데이터가 필요한가요?
    A. 경험상 최소 500~1000개 정도의 고품질 instruction 데이터면 의미 있는 파인튜닝이 됩니다. 물론 많을수록 좋지만, 품질이 더 중요합니다.
    Q. 어떤 베이스 모델을 선택하는 게 좋을까요?
    A. 한국어 도메인이라면 한국어 데이터로 사전학습된 모델을 베이스로 쓰는 게 유리합니다. EEVE-Korean, EXAONE 등 국내에서 공개한 모델들도 좋은 선택지입니다. 영어 도메인이라면 Mistral 7B나 LLaMA 3 8B가 무난합니다.
    Q. 파인튜닝 한 번에 얼마나 걸리나요?
    A. 데이터 양과 GPU 성능에 따라 다르지만, RTX 3090 기준 1000개 데이터, 3 에폭 학습에 1~2시간 정도 예상하시면 됩니다.
    Q. LoRA 어댑터를 베이스 모델에 완전히 병합해야 하나요?
    A. 추론 시 편의를 위해 merge_and_unload()로 병합할 수 있지만, 어댑터를 분리 유지하면 나중에 다른 어댑터로 교체하기 쉽습니다. 여러 도메인 모델을 관리한다면 분리 유지를 추천합니다.
  • [HomeLabs] pfSense/OPNsense 홈랩 방화벽 구축: 네트워크 보안 강화 완벽 가이드

    홈랩에 방화벽이 필요한 이유 — 공유기로는 부족하더라고요

    홈서버를 처음 구축했을 때 저도 그랬어요. “집에서 쓰는 건데 뭐, 공유기 방화벽이면 충분하지 않나?” 싶었거든요. 그런데 포트포워딩 몇 개 열어두고 Jellyfin이랑 Nextcloud 운영하다 보니까 생각이 바뀌었습니다. 어느 날 서버 로그를 들여다봤더니 중국, 러시아 IP에서 SSH 브루트포스 시도가 하루에도 수백 건씩 들어오고 있는 거예요. 그때부터 진지하게 홈랩 방화벽 구축을 고민하기 시작했죠.

    일반 가정용 공유기의 NAT(Network Address Translation, 네트워크 주소 변환) 기능이 기본적인 외부 접근은 막아주지만, 세밀한 트래픽 제어나 IDS/IPS(침입 탐지/방지 시스템), VPN 게이트웨이 기능 같은 건 기대하기 어렵거든요. 그래서 선택한 게 바로 pfSense와 OPNsense였습니다.

    이 글에서는 제가 직접 구축하면서 겪었던 경험을 바탕으로, 두 솔루션의 차이점부터 실제 설치·설정까지 단계별로 정리해드릴게요. 홈서버 보안을 한 단계 끌어올리고 싶으신 분들께 도움이 됐으면 합니다.

    ▲ 일반적인 홈랩 방화벽 구성 — 인터넷 → 방화벽(pfSense/OPNsense) → 내부 네트워크 세그먼트로 트래픽이 흐르는 전체 아키텍처

    pfSense vs OPNsense — 뭐가 다른 건가요?

    둘 다 FreeBSD 기반의 오픈소스 방화벽 소프트웨어인데, 뿌리는 같지만 지향점이 조금 달라요. 저도 처음엔 “그냥 비슷한 거 아냐?” 했는데, 직접 써보니 체감 차이가 꽤 있더라고요.

    항목 pfSense CE (Community Edition) OPNsense
    기반 OS FreeBSD FreeBSD (HardenedBSD 기반)
    UI 스타일 전통적, 기능 중심 모던, 직관적
    업데이트 주기 비교적 느림 격주 릴리즈 (빠른 보안 패치)
    IDS/IPS Snort, Suricata Suricata (기본 통합)
    플러그인 생태계 pfSense 패키지 OPNsense 플러그인 (더 활발)
    라이선스 Apache 2.0 BSD 2-Clause
    상업적 지원 Netgate (유료 플랜) Deciso (유료 플랜)

    솔직히 말씀드리면, 저는 최근에는 OPNsense를 메인으로 쓰고 있어요. 이유는 단순한데, UI가 훨씬 깔끔하고 보안 업데이트가 빠르거든요. 특히 CVE(Common Vulnerabilities and Exposures, 공개된 보안 취약점)가 나왔을 때 OPNsense 쪽이 패치 대응이 빠르더라고요. 다만 pfSense가 커뮤니티 문서나 유튜브 튜토리얼이 훨씬 많아서, 처음 입문하시는 분들한테는 pfSense가 진입 장벽이 낮을 수 있어요.

    하드웨어 선택 — 어디에 설치할 건가요?

    방화벽 소프트웨어를 고르는 것만큼 중요한 게 하드웨어 선택이에요. 저는 세 가지 방법을 다 써봤는데, 각각 장단점이 있더라고요.

    옵션 1: 전용 x86 미니PC / 어플라이언스

    • Intel NIC(Network Interface Card, 네트워크 인터페이스 카드)가 달린 미니PC 추천 — Realtek NIC는 FreeBSD 드라이버 이슈가 있어서 고생할 수 있어요
    • 듀얼 포트 이상의 NIC가 필요 (WAN 1개, LAN 1개 이상)
    • 전력 소비가 낮고 팬리스 모델이면 24/7 운영에 유리
    • 💡 팁: AES-NI(하드웨어 암호화 가속) 지원 CPU를 선택하면 VPN 성능이 크게 올라가요

    옵션 2: 기존 홈서버의 VM (가상머신)

    • Proxmox 같은 하이퍼바이저에서 VM으로 운영 가능
    • NIC 패스스루(PCIe Passthrough)나 VLAN 트렁킹 설정이 필요
    • 서버가 꺼지면 인터넷도 끊기는 단점 있음 ⚠️

    옵션 3: Raspberry Pi (제한적)

    • pfSense/OPNsense 공식 지원은 x86/amd64 기준 — Pi는 공식 지원 아님
    • 소규모 트래픽 환경에서만 대안 고려 가능 (이 경우 OpenWrt 쪽이 더 현실적)

    OPNsense 설치 — 단계별 가이드

    이제 본격적으로 설치해봅시다. 제가 설치할 때 가장 헷갈렸던 부분을 중심으로 설명할게요.

    ▲ OPNsense 웹 관리 인터페이스(WebGUI) — 설치 후 처음 접속하면 보이는 대시보드 화면. 시스템 상태, 인터페이스 정보, 게이트웨이 상태를 한눈에 확인할 수 있어요.

    1. ISO 다운로드: opnsense.org 공식 사이트에서 최신 DVD ISO 이미지 다운로드 (amd64 기준)
    2. 부팅 USB 제작: Rufus(Windows) 또는 dd 명령어(Linux/Mac)로 부팅 USB 제작
    3. 설치 진행: 기본 계정으로 로그인 후 installer 실행
    4. 인터페이스 할당: WAN / LAN 인터페이스 지정 (가장 중요!)
    5. WebGUI 접속: LAN IP(기본 192.168.1.1)로 브라우저 접속

    부팅 USB 제작할 때 dd 명령어 쓰시는 분들 참고하세요:

    # Linux/macOS에서 USB 부팅 디스크 만들기
    # /dev/sdX 부분은 본인 USB 장치 경로로 변경 필요 (lsblk 명령어로 확인)
    sudo dd if=OPNsense-버전-dvd-amd64.iso of=/dev/sdX bs=4M status=progress
    sync

    ⚠️ 주의: dd 명령어는 잘못 입력하면 다른 디스크를 덮어쓸 수 있어요. of= 경로를 반드시 두 번 확인하세요. 저도 처음에 식은땀 흘렸던 기억이 있어요 ㅎㅎ

    기본 방화벽 규칙 설정 — 여기가 핵심이에요

    설치 후 WebGUI에 접속하면 Setup Wizard(설정 마법사)가 뜨는데, 이걸 따라가면 기본 설정은 금방 끝나요. 근데 여기서 멈추면 안 됩니다. 진짜 보안 강화는 방화벽 규칙 설정에서 시작하거든요.

    WAN 규칙 — 기본 원칙은 “모두 차단, 필요한 것만 허용”

    OPNsense/pfSense 모두 기본적으로 WAN에서 들어오는 모든 인바운드(Inbound, 외부→내부) 트래픽을 차단해요. 이 기본값을 유지하는 게 맞습니다.

    # OPNsense Shell에서 현재 방화벽 규칙 확인 (pfctl 명령어)
    pfctl -sr
    
    # 특정 인터페이스의 규칙만 보기
    pfctl -sr | grep WAN

    LAN 규칙 — VLAN으로 네트워크 세분화하기

    홈랩에서 제가 가장 효과적으로 쓰는 방법이 VLAN(Virtual LAN, 가상 네트워크 분리)이에요. 예를 들어 이런 식으로 나눌 수 있어요:

    • VLAN 10 (신뢰 네트워크): 개인 PC, 노트북
    • VLAN 20 (서버 네트워크): 홈서버, NAS
    • VLAN 30 (IoT 네트워크): 스마트 TV, 공기청정기 등 IoT 기기
    • VLAN 40 (게스트 네트워크): 방문객 Wi-Fi

    IoT 기기들을 별도 VLAN으로 격리하는 게 진짜 중요해요. 스마트 TV나 IP 카메라 같은 장치들은 보안이 취약한 경우가 많거든요. 이것들이 메인 네트워크에 있으면 한 기기가 뚫렸을 때 전체가 위험해지는 거니까요.

    OPNsense WebGUI에서 VLAN 추가하는 경로: Interfaces → Other Types → VLAN

    # 방화벽 규칙 예시 — IoT VLAN에서 서버 VLAN으로의 접근 차단
    # OPNsense WebGUI의 Firewall → Rules → VLAN30 에서 설정
    # Action: Block
    # Interface: VLAN30
    # Source: VLAN30 net
    # Destination: VLAN20 net
    # Description: IoT to Server VLAN Block

    Suricata IDS/IPS 활성화

    OPNsense에는 Suricata(수리카타)가 기본 플러그인으로 포함돼 있어요. IDS(Intrusion Detection System, 침입 탐지 시스템)와 IPS(Intrusion Prevention System, 침입 방지 시스템) 기능을 제공하는데, 활성화해두면 알려진 악성 트래픽 패턴을 자동으로 탐지하고 차단해줘요.

    활성화 경로: Services → Intrusion Detection → Administration

    • Enabled 체크
    • IPS Mode 체크 (탐지만 할지, 실제 차단까지 할지 결정)
    • Ruleset: ET Open 룰셋 무료로 사용 가능 — 이걸로도 충분해요

    💡 처음에 IPS 모드를 바로 켜면 정상 트래픽이 차단될 수 있어요. IDS 모드로 며칠 모니터링하면서 False Positive(오탐)를 확인한 다음에 IPS 모드로 전환하는 걸 추천합니다.

    VPN 게이트웨이 설정 — WireGuard로 원격 접속

    홈랩을 외부에서 안전하게 접속하려면 VPN이 필수예요. 예전엔 OpenVPN 많이 썼는데, 요즘은 WireGuard(와이어가드)로 완전히 넘어왔어요. 설정이 훨씬 간단하고 성능도 좋거든요.

    OPNsense에서 WireGuard 설치: System → Firmware → Plugins → os-wireguard 설치

    # WireGuard 키 쌍 생성 (서버에서 실행)
    wg genkey | tee server_private.key | wg pubkey > server_public.key
    wg genkey | tee client_private.key | wg pubkey > client_public.key
    
    # 키 내용 확인
    cat server_private.key
    cat server_public.key

    생성된 키는 OPNsense WebGUI의 VPN → WireGuard → Local / Endpoints 설정에 입력하면 돼요. 스마트폰에서는 WireGuard 공식 앱으로 QR 코드 스캔하면 바로 연결 가능하고, 이게 진짜 편하더라고요. 드디어 외부에서 집 서버에 안전하게 붙을 수 있게 됐을 때 기분이 얼마나 좋던지 🎉

    ▲ OPNsense 방화벽 규칙 설정 화면 및 Suricata IDS 경보 로그 — 실시간으로 차단된 트래픽과 탐지된 위협을 모니터링할 수 있어요.

    ⚠️ 삽질 모음 — 제가 겪은 트러블슈팅

    설치하면서 제가 실제로 막혔던 부분들이에요. 이거 보고 같은 삽질 반복하지 않으셨으면 해서 솔직하게 적어봤어요.

    문제 1: WAN 인터페이스 인식 안 됨

    증상: 설치 후 WAN 쪽 인터페이스가 잡히지 않음
    원인: Realtek NIC의 FreeBSD 드라이버 이슈 (FreeBSD에서 Realtek은 가끔 문제가 생겨요)
    해결: Intel 기반 NIC로 교체. em(Intel PRO/1000 계열) 또는 igb 드라이버가 인식되는 NIC 사용 추천

    문제 2: NAT Reflection 설정 안 해서 내부에서 외부 도메인 접속 불가

    증상: 외부에서는 되는데 내부 LAN에서 자기 도메인으로 접속 안 됨
    원인: NAT Reflection(헤어핀 NAT) 미설정
    해결: Firewall → Settings → Advanced → Reflection for port forwards: Enable

    문제 3: Suricata 켜고 나서 일부 사이트 접속 불가

    증상: IPS 모드 활성화 후 특정 스트리밍 사이트 접속이 끊김
    원인: 공격 패턴과 유사한 정상 트래픽을 오탐(False Positive)으로 차단
    해결: 해당 룰 ID를 Suppress List에 추가하거나, 해당 IP를 Whitelist에 등록

    # OPNsense Shell에서 Suricata 로그 실시간 확인
    tail -f /var/log/suricata/eve.json | python3 -m json.tool | grep -E '"alert"|"src_ip"|"dest_ip"'

    문제 4: 재부팅 후 방화벽 규칙이 일부 초기화

    증상: 수동으로 추가한 규칙이 재부팅 후 사라짐
    원인: WebGUI가 아닌 Shell에서 직접 pfctl로 임시 추가한 규칙
    해결: 반드시 WebGUI를 통해 규칙 추가. Shell 명령어는 테스트용으로만 사용

    ✅ 구축 결과 확인 — 이렇게 검증하세요

    설정 다 했다고 끝이 아니에요. 실제로 제대로 동작하는지 검증하는 게 중요해요.

    외부에서 포트 스캔으로 노출 여부 확인

    Shodan(shodan.io)이나 nmap으로 외부에서 내 IP를 스캔해보세요. 방화벽이 제대로 동작한다면 열어둔 포트 외에는 아무것도 안 보여야 해요.

    # 다른 네트워크(모바일 데이터 등)에서 내 WAN IP로 포트 스캔
    nmap -sV -p 1-1024 [내 WAN IP 주소]
    
    # 결과 예시 — 방화벽이 잘 동작하면 이렇게 나와야 함
    # All 1024 scanned ports on [IP] are in ignored states.

    Firewall Logs 확인

    OPNsense WebGUI에서 Firewall → Log Files → Live View로 실시간 차단 로그를 볼 수 있어요. 여기서 외부에서 들어오는 수상한 접근 시도를 확인할 수 있는데, 처음 보시면 “이렇게 많이 들어오고 있었어?” 하고 놀라실 거예요. 저도 그랬거든요 ㅎㅎ

    Dashboard 위젯으로 실시간 모니터링

    • Gateway 상태 (WAN 연결 안정성)
    • Interface 트래픽 그래프
    • Suricata 알림 카운트
    • 시스템 리소스 (CPU, 메모리)

    ▲ 방화벽 구축 완료 후 OPNsense 대시보드 — 게이트웨이 상태, 인터페이스 트래픽, IDS 탐지 현황을 실시간으로 모니터링하는 화면

    마무리 — 홈랩 네트워크 보안, 이제 시작이에요

    pfSense/OPNsense 기반의 홈랩 방화벽을 구축하고 나면 네트워크를 바라보는 눈이 달라져요. 예전엔 그냥 “인터넷 되면 됐지” 였다면, 이제는 트래픽 흐름이 보이기 시작하거든요. 어디서 뭐가 들어오고 나가는지, 어떤 시도가 차단되고 있는지.

    정리하자면 이렇게 구성하시면 됩니다:

    • ✅ Intel NIC 기반 하드웨어 선택
    • ✅ OPNsense 또는 pfSense 설치
    • ✅ WAN 기본 차단 정책 유지
    • ✅ VLAN으로 네트워크 세분화 (IoT 격리 필수!)
    • ✅ Suricata IDS/IPS 활성화
    • ✅ WireGuard VPN으로 원격 접속 구성
    • ✅ 외부 포트 스캔으로 노출 여부 검증

    다음 단계로는 pfBlockerNG(pfSense) 또는 OPNsense의 Unbound DNS 블랙리스트를 활용한 DNS 기반 광고/악성 도메인 차단을 다뤄볼 예정이에요. 이걸 적용하면 집 안 모든 기기에서 광고가 사라지는 마법을 경험할 수 있거든요 — Pi-hole 없이도요. 그 내용은 다음 글에서 자세히 다루겠습니다.

    궁금한 점이나 막히는 부분 있으시면 댓글로 남겨주세요. 같은 삽질을 덜 하셨으면 하는 마음으로 최대한 답변드릴게요 😊

    자주 묻는 질문 (FAQ)

    Q. pfSense와 OPNsense 중 초보자에게 어떤 걸 추천하나요?

    커뮤니티 자료가 많은 pfSense CE가 처음 입문하기엔 조금 더 수월해요. 다만 보안 업데이트 주기와 UI 현대성을 고려하면 OPNsense도 충분히 좋은 선택입니다. 둘 다 무료로 사용 가능하니 VM에서 먼저 테스트해보시는 걸 추천합니다.

    Q. 최소 사양이 어떻게 되나요?

    공식 권장 사양은 CPU 1GHz 이상, RAM 1GB 이상이지만, IDS/IPS나 VPN까지 쓰려면 최소 4GB RAM에 멀티코어 CPU를 권장해요. 기가비트 라인 풀로 쓰려면 더 여유 있는 스펙이 필요합니다.

    Q. 기존 공유기는 어떻게 되나요?

    pfSense/OPNsense가 공유기 역할을 대신하는 구성이 일반적이에요. 기존 공유기는 AP(액세스 포인트) 모드로 전환해서 Wi-Fi 전용으로 활용하면 됩니다.

    Q. Cloudflare Tunnel과 같이 쓸 수 있나요?

    네, 함께 사용 가능해요. Cloudflare Tunnel을 쓰면 WAN 포트포워딩 없이도 외부 접속이 가능해서 보안상 더 좋은 구성을 만들 수 있어요. 이 조합도 나중에 다뤄볼게요.

  • [HomeLabs] 홈랩 UPS 선택 및 전원 관리 가이드: 정전 대비 완벽 전략

    정전이 터진 그 순간, 홈랩이 그냥 꺼졌습니다 😱

    몇 년 전 여름, 갑작스러운 낙뢰로 아파트 전체가 순간 정전됐던 적이 있었어요. 그때 저는 홈랩에서 NAS 마이그레이션 작업을 한창 진행 중이었거든요. 결과는… 파일시스템 손상에 RAID 재구성까지, 꼬박 이틀을 날렸습니다. 그 뒤로 홈랩 UPS는 저한테 선택이 아니라 필수가 됐어요.

    혹시 이런 경험 있으신가요? 밤새 돌려놓은 컴파일 작업이 정전 한 방에 날아간다거나, 홈서버 데이터가 손상되는 상황이요. 이 글에서는 13년간 홈랩을 운영하면서 직접 겪고 배운 홈서버 UPS 선택 기준과 전원 관리 전략을 솔직하게 공유해 드리려 합니다. 정전 대비, 제대로 해봅시다.

    ▲ 일반적인 홈랩 전원 구성도. UPS는 서버와 네트워크 장비 사이에서 전원을 보호하는 핵심 역할을 합니다.


    UPS(무정전 전원 공급 장치)가 뭔지 제대로 알고 가자

    UPS는 Uninterruptible Power Supply의 약자로, 우리말로는 무정전 전원 공급 장치라고 부릅니다. 쉽게 말해, 상용 전원이 끊겼을 때 배터리로 일정 시간 동안 전기를 계속 공급해주는 장치죠.

    근데 UPS가 단순히 “정전 때 버텨주는 장치”라고만 생각하면 절반만 아는 거거든요. 실제로는 세 가지 핵심 기능이 있어요.

    • 정전 대비 (Backup Power): 상용 전원 차단 시 배터리로 전환하여 장비를 계속 가동
    • 전압 안정화 (Voltage Regulation): 순간적인 전압 강하(Sag)나 전압 급등(Surge)으로부터 장비 보호
    • 노이즈 필터링 (Line Conditioning): 전력선 노이즈를 제거해 민감한 전자 장비 보호

    저도 처음엔 “그냥 배터리 아닌가?” 싶었는데, 실제로 쓰다 보니 전압 안정화 기능이 얼마나 중요한지 체감하게 되더라고요. 특히 오래된 아파트나 상가 건물에서 홈랩을 운영하면 전압이 생각보다 많이 흔들립니다.

    홈랩 UPS의 세 가지 방식: 어떤 걸 고르면 좋을까?

    방식 특징 전환 시간 가격대 홈랩 적합도
    Standby (대기형) 평소엔 상용 전원 직접 공급, 정전 시 배터리 전환 4~8ms 저렴 ⭐⭐⭐
    Line-Interactive (라인 인터랙티브형) AVR로 전압 조정, 정전 시 배터리 전환 2~4ms 중간 ⭐⭐⭐⭐⭐
    Online Double Conversion (온라인 이중 변환형) 항상 배터리를 통해 전원 공급 (전환 없음) 0ms 고가 ⭐⭐⭐⭐ (대용량 구성 시)

    홈랩 용도라면 저는 Line-Interactive 방식을 강력 추천합니다. AVR(Automatic Voltage Regulator, 자동 전압 조정 기능)이 내장되어 있어서 전압이 불안정한 환경에서도 장비를 안정적으로 보호해주거든요. 가격 대비 성능도 가장 균형이 잡혀 있고요.


    홈랩 UPS 용량 계산하는 법 — 이거 모르면 낭패

    UPS를 고를 때 가장 많이 실수하는 부분이 바로 용량 계산이에요. 저도 처음에 “대충 1000VA면 되겠지” 하고 샀다가 나중에 바꾼 경험이 있습니다 ㅎㅎ.

    1단계: 연결할 장비의 소비 전력 파악

    우선 UPS에 연결할 모든 장비의 소비 전력(와트, W)을 파악해야 해요. 장비 뒷면 스티커나 제조사 스펙 시트에서 확인할 수 있습니다. 실측이 가장 정확한데, 저는 스마트 플러그의 전력 모니터링 기능을 활용합니다.

    # 예시: 일반적인 홈랩 구성 소비 전력 목록
    # (실제 수치는 장비마다 다르므로 반드시 직접 확인하세요)
    
    미니 PC / NUC 타입 서버:    30~65W (유휴 기준)
    타워형 홈서버 (HDD 포함):   80~150W
    네트워크 스위치 (8포트):    10~20W
    공유기/방화벽 어플라이언스: 15~30W
    NAS (4베이):                30~60W
    모니터 (필요시):            20~40W
    
    # 합산 예시 (미니 PC 2대 + NAS + 스위치 + 공유기)
    # 65 + 65 + 50 + 15 + 20 = 215W (유휴 상태)
    # 피크 부하는 유휴의 1.3~1.5배로 추산

    2단계: VA 용량 계산

    UPS 용량은 VA(Volt-Ampere)로 표기되는데, 실제 소비 전력(W)과는 약간 달라요. 역률(Power Factor)이라는 개념이 있거든요. 일반적으로 홈랩 장비는 역률이 0.6~0.8 정도입니다.

    # VA 용량 계산 공식
    # 필요 VA = 총 소비전력(W) ÷ 역률(Power Factor)
    
    # 예시 계산
    총 소비전력 = 215W
    역률 = 0.7 (일반적인 홈랩 장비 기준)
    필요 VA = 215 ÷ 0.7 ≈ 307VA
    
    # 안전 마진 적용 (UPS 용량의 70~80%만 사용 권장)
    # 307VA ÷ 0.75 ≈ 409VA
    
    # 결론: 최소 500VA, 여유 있게 650~800VA 제품 선택

    💡 팁: 저는 항상 계산된 필요 용량의 1.5~2배 여유분을 두고 선택합니다. 나중에 장비가 늘어날 수도 있고, UPS 배터리는 시간이 지나면서 용량이 줄어들거든요.

    3단계: 백업 시간 목표 설정

    UPS 백업 시간은 연결된 부하와 배터리 용량에 따라 달라집니다. 홈랩 UPS의 목표는 보통 두 가지예요.

    • 단기 정전 버티기: 수 초~수 분의 순간 정전 대응 (대부분의 경우)
    • 안전한 셧다운 시간 확보: 장시간 정전 시 서버가 안전하게 종료될 시간 (최소 5~10분)

    솔직히 홈랩 UPS로 몇 시간씩 버티려고 하면 배터리 용량이 엄청나게 커져서 비현실적이에요. 핵심은 안전한 셧다운 자동화입니다. 이건 뒤에서 자세히 다룰게요.


    실전: APC, Eaton 등 주요 UPS 브랜드와 선택 기준

    ▲ 홈랩에서 많이 사용되는 UPS 제품군. 브랜드별로 소프트웨어 지원 방식이 다르므로 구매 전 확인이 필요합니다.

    UPS 브랜드는 여러 곳이 있는데, 홈랩 커뮤니티에서 가장 많이 언급되는 건 역시 APC(by Schneider Electric)와 Eaton입니다. 두 브랜드 모두 오랜 역사를 가진 검증된 제조사예요.

    브랜드별 특징 비교

    항목 APC Eaton
    관리 소프트웨어 PowerChute (Windows/Linux) Intelligent Power Manager (IPM)
    Linux/오픈소스 연동 NUT(Network UPS Tools) 지원 우수 NUT 지원 (일부 모델)
    USB 통신 대부분 모델 지원 대부분 모델 지원
    국내 AS 비교적 용이 공식 유통 확인 필요
    교체 배터리 구하기 국내 구매 용이 모델에 따라 다름

    제 경험상 홈랩 입문자에게는 APC의 Back-UPS 또는 Smart-UPS 라인이 무난합니다. NUT(Network UPS Tools)와의 호환성이 좋고, 교체 배터리를 국내에서 쉽게 구할 수 있거든요. 배터리 수명이 보통 3~5년이라 교체 접근성이 중요해요.

    홈랩 규모별 추천 방향

    • 소형 홈랩 (미니PC 1~2대 + NAS): 650VA~1000VA, Line-Interactive, USB 통신 지원 모델
    • 중형 홈랩 (타워 서버 + 다수 장비): 1000VA~1500VA, Line-Interactive, USB/RS-232 통신 지원
    • 대형 홈랩 (랙 마운트 서버 다수): 1500VA 이상, 랙마운트형 UPS 고려, Online Double Conversion 검토

    ⚠️ 주의: 특정 모델의 현재 가격이나 최신 스펙은 제가 확인한 시점과 다를 수 있으니, 반드시 구매 전에 공식 사이트나 판매처에서 최신 정보를 확인하세요.


    NUT(Network UPS Tools)로 자동 셧다운 구현하기

    이게 진짜 핵심이에요. UPS가 있어도 정전 시 서버가 자동으로 안전하게 꺼지지 않으면 절반짜리 대책입니다. NUT(Network UPS Tools)는 리눅스 기반 홈랩에서 UPS를 제어하는 사실상 표준 오픈소스 도구거든요.

    NUT 설치 및 기본 설정 (Debian/Ubuntu 기준)

    # NUT 설치
    sudo apt update
    sudo apt install nut nut-client nut-server -y
    
    # UPS 자동 감지 시도
    sudo nut-scanner -U
    
    # USB로 연결된 UPS 확인
    lsusb | grep -i ups
    # 또는
    lsusb | grep -i apc
    # /etc/nut/nut.conf 설정
    # UPS가 이 서버에 직접 연결된 경우
    MODE=standalone
    # /etc/nut/ups.conf 설정 (APC USB 연결 예시)
    [myups]
      driver = usbhid-ups
      port = auto
      desc = "Home Lab UPS"
      # 폴링 간격 (초)
      pollinterval = 2
    # /etc/nut/upsd.conf 설정
    LISTEN 127.0.0.1 3493
    LISTEN ::1 3493
    
    # 네트워크의 다른 서버에서도 접근하려면:
    # LISTEN 0.0.0.0 3493
    # /etc/nut/upsd.users 설정
    [admin]
      password = your_secure_password
      actions = SET
      instcmds = ALL
    
    [monitor]
      password = your_monitor_password
      upsmon master
    # /etc/nut/upsmon.conf 설정
    MONITOR myups@localhost 1 monitor your_monitor_password master
    
    # 배터리 잔량이 이 값 이하로 떨어지면 셧다운
    MINSUPPLIES 1
    SHUTDOWNCMD "/sbin/shutdown -h +0"
    
    # 정전 후 배터리로 전환된 상태에서 대기 시간 (초)
    # 이 시간 동안 전원이 복구되지 않으면 셧다운 시작
    FINALDELAY 5
    
    # 배터리 잔량 경고 임계값 (%)
    POWERDOWNFLAG /etc/killpower
    # NUT 서비스 시작 및 활성화
    sudo systemctl enable nut-server nut-client
    sudo systemctl start nut-server
    sudo systemctl start nut-client
    
    # UPS 상태 확인
    upsc myups@localhost
    
    # 주요 상태값 확인
    upsc myups@localhost ups.status
    # 정상 시: OL (On Line)
    # 배터리 전환 시: OB (On Battery)
    # 배터리 부족 시: LB (Low Battery)
    
    upsc myups@localhost battery.charge  # 배터리 잔량 (%)
    upsc myups@localhost input.voltage   # 입력 전압 (V)
    upsc myups@localhost ups.load        # 현재 부하 (%)

    🎉 여기까지 하면 기본적인 자동 셧다운 설정이 완료됩니다! 정전 감지 → 배터리 잔량 모니터링 → 임계값 도달 시 자동 셧다운 흐름이 완성되는 거예요.

    여러 서버에 NUT 적용하기 (Master-Slave 구성)

    홈랩에 서버가 여러 대라면, UPS가 연결된 서버를 마스터(Master)로, 나머지를 슬레이브(Slave)로 구성하면 됩니다. 마스터가 정전을 감지하면 슬레이브들에게 셧다운 신호를 보내는 구조예요.

    # 슬레이브 서버의 /etc/nut/nut.conf
    MODE=netclient
    
    # 슬레이브 서버의 /etc/nut/upsmon.conf
    # 마스터 서버 IP로 변경
    MONITOR [email protected] 1 monitor your_monitor_password slave
    SHUTDOWNCMD "/sbin/shutdown -h +0"
    FINALDELAY 5

    여기서 한 가지 주의할 점이 있어요. 슬레이브 서버들은 마스터보다 먼저 셧다운되어야 합니다. 마스터가 마지막에 UPS에 셧다운 신호를 보내는 역할을 하거든요. NUT의 기본 동작이 이렇게 설계되어 있으니 걱정은 안 하셔도 되지만, 네트워크 방화벽 설정에서 3493 포트를 열어두는 걸 잊지 마세요.


    ⚠️ 삽질 경험담: 이런 것들 조심하세요

    이론은 이쯤 하고, 제가 실제로 겪은 문제들을 공유할게요. 미리 알면 시간을 많이 아낄 수 있습니다.

    삽질 1: USB 드라이버 인식 문제

    APC UPS를 USB로 연결했는데 NUT에서 인식을 못 하는 경우가 있었어요. lsusb로는 보이는데 NUT 드라이버가 안 붙는 상황이었거든요. 해결책은 udev 규칙 설정이었습니다.

    # USB 장치 권한 문제 해결
    # /etc/udev/rules.d/99-nut-ups.rules 파일 생성
    SUBSYSTEM=="usb", ATTRS{idVendor}=="051d", MODE="0660", GROUP="nut"
    
    # udev 규칙 리로드
    sudo udevadm control --reload-rules
    sudo udevadm trigger

    삽질 2: 배터리 용량 표시 오류

    처음 NUT를 설정했을 때 배터리 잔량이 항상 100%로 표시되는 문제가 있었어요. 알고 보니 UPS와 서버 간의 통신이 제대로 이루어지지 않았던 거였습니다. 이럴 땐 NUT 데몬을 재시작하고 로그를 확인해보면 돼요.

    # NUT 서비스 재시작
    sudo systemctl restart nut-server nut-client
    
    # 로그 확인
    sudo journalctl -u nut-server -n 50
    sudo journalctl -u nut-client -n 50

    삽질 3: 셧다운 명령 권한 문제

    NUT가 정전을 감지했는데 셧다운이 안 되는 경우도 있었어요. upsmon 프로세스가 root 권한으로 실행되지 않아서였습니다. 다음을 확인해보세요.

    # upsmon 프로세스 확인
    ps aux | grep upsmon
    
    # nut 사용자의 sudoers 설정 확인
    sudo visudo
    
    # 다음 라인 추가 (root 비밀번호 없이 shutdown 실행 가능)
    nut ALL=(ALL) NOPASSWD: /sbin/shutdown

    홈랩 UPS 운영 팁: 배터리 관리와 정기 점검

    UPS를 설치했다고 끝이 아니에요. 배터리는 소모품이거든요. 몇 가지 운영 팁을 공유합니다.

    정기 점검 리스트

    • 월 1회: NUT로 UPS 상태 확인 (배터리 잔량, 입력 전압 등)
    • 분기 1회: 배터리 자가진단 테스트 실행 (UPS 본체 버튼 또는 소프트웨어)
    • 반년 1회: 간단한 부하 테스트 (실제 정전 상황 시뮬레이션)
    • 매년: 배터리 교체 여부 판단 (보통 3~5년 수명)

    배터리 수명 연장 팁

    UPS 배터리는 고온 환경에서 빨리 열화됩니다. 가능하면 서늘한 곳에 배치하세요. 저는 홈랩 선반 아래쪽에 UPS를 두고, 위에 환풍구를 설치해 공기가 잘 통하도록 했습니다. 이렇게 하니 배터리 수명이 눈에 띄게 늘었어요.


    정리: 홈랩 UPS, 이렇게 선택하고 운영하세요

    정전 대비는 홈랩 운영의 필수 요소입니다. 정리하면 다음과 같아요.

    1. UPS 방식은 Line-Interactive형 — 전압 안정화와 정전 대비를 모두 충족
    2. 용량 계산은 신중하게 — 현재 부하의 1.5~2배 여유분 확보
    3. APC 또는 Eaton 추천 — 국내 AS와 배터리 구매 용이
    4. NUT로 자동 셧다운 구현 — 정전 시 데이터 손상 방지
    5. 정기 점검과 배터리 관리 — 배터리 수명 3~5년, 환기 중요

    처음엔 복잡해 보일 수 있지만, 한 번 제대로 설정해두면 마음이 놓이더라고요. 여름철 낙뢰나 갑작스러운 정전으로부터 홈랩을 지킬 수 있으니까요. 혹시 설정 중에 막히는 부분이 있으면 댓글로 질문해 주세요. 도와드리겠습니다!

  • [Cloud] Pulumi vs Terraform 비교: IaC 도구 선택 가이드

    IaC 도구 선택, 왜 이렇게 어렵냐고요 😅

    팀에서 인프라 자동화 도구를 새로 도입하려고 할 때, 가장 먼저 나오는 질문이 있죠. “Terraform 쓸까요, Pulumi 쓸까요?”

    저도 몇 년 전에 이 선택 앞에서 한참 고민했거든요. 당시엔 Terraform이 거의 IaC(Infrastructure as Code, 코드로 인프라를 정의하고 관리하는 방식)의 표준처럼 여겨지던 시절이었는데, Pulumi라는 새로운 녀석이 등장하면서 “이거 써야 하나?” 싶었던 기억이 납니다.

    결론부터 말씀드리면 — 둘 다 써봤고, 둘 다 장단점이 뚜렷해요. Pulumi vs Terraform 비교는 단순히 “어느 게 더 좋냐”의 문제가 아니라, 팀 상황과 프로젝트 성격에 따라 달라지는 문제더라고요. 오늘은 13년 동안 인프라 엔지니어로 일하면서 직접 겪은 경험을 바탕으로, 이 두 IaC 도구를 제대로 비교해드리겠습니다.

    Pulumi와 Terraform의 전체적인 구조와 접근 방식 차이를 보여주는 개요 다이어그램

    Terraform과 Pulumi, 각각 어떤 도구인가요?

    Terraform — IaC의 베테랑

    Terraform은 HashiCorp에서 만든 오픈소스 IaC 도구로, 2014년에 처음 출시됐습니다. HCL(HashiCorp Configuration Language)이라는 자체 DSL(Domain-Specific Language, 특정 목적을 위해 만들어진 언어)을 사용하는 게 특징이에요.

    쉽게 말해서, “인프라를 선언적으로 정의”하는 방식입니다. “EC2 인스턴스 이렇게 만들어줘”라고 적어두면, Terraform이 알아서 현재 상태와 비교해서 필요한 작업만 수행하는 거죠.

    # Terraform 예시 — AWS EC2 인스턴스 생성
    resource "aws_instance" "web_server" {
      ami           = "ami-0c55b159cbfafe1f0"
      instance_type = "t3.micro"
    
      tags = {
        Name        = "web-server"
        Environment = "production"
      }
    }
    
    output "instance_ip" {
      value = aws_instance.web_server.public_ip
    }

    처음 보면 “이게 뭔 언어야?” 싶을 수 있는데, 몇 번 써보면 꽤 직관적이더라고요. 특히 인프라 구성을 “읽는” 관점에서는 진짜 편합니다.

    Pulumi — 개발자 친화적인 도전자

    Pulumi는 2018년에 등장한 비교적 젊은 IaC 도구입니다. 가장 큰 차별점은 실제 프로그래밍 언어를 그대로 사용한다는 점이에요. Python, TypeScript, Go, C#, Java 등을 지원하거든요.

    처음 이걸 봤을 때 “오, 이거 개발자들 좋아하겠다” 싶었어요. 인프라 엔지니어보다 소프트웨어 개발자 출신이 많은 팀이라면 특히요.

    # Pulumi 예시 — Python으로 AWS EC2 인스턴스 생성
    import pulumi
    import pulumi_aws as aws
    
    web_server = aws.ec2.Instance(
        "web-server",
        ami="ami-0c55b159cbfafe1f0",
        instance_type="t3.micro",
        tags={
            "Name": "web-server",
            "Environment": "production",
        }
    )
    
    pulumi.export("instance_ip", web_server.public_ip)

    같은 결과물인데, Python 개발자라면 아래 코드가 훨씬 익숙하게 느껴질 겁니다. 이게 Pulumi의 핵심 가치예요.

    Pulumi vs Terraform 핵심 차이점 비교

    자, 이제 본격적으로 비교해 봅시다. 제가 직접 두 도구를 써보면서 느낀 차이점들을 정리했어요.

    비교 항목 Terraform Pulumi
    언어 HCL (자체 DSL) Python, TypeScript, Go, C# 등
    학습 곡선 HCL은 쉽지만, 복잡한 로직은 어려움 언어는 익숙하지만 Pulumi 개념 학습 필요
    상태 관리 로컬 파일 또는 원격 백엔드 Pulumi Cloud 또는 자체 백엔드
    프로바이더 생태계 매우 방대 (성숙한 생태계) 성장 중 (Terraform 프로바이더 브리지 지원)
    테스트 제한적 (Terratest 등 별도 도구 필요) 언어 기본 테스트 프레임워크 활용 가능
    커뮤니티 매우 활발, 레퍼런스 풍부 성장 중
    라이선스 BSL 1.1 (v1.5.5부터 변경) Apache 2.0 (오픈소스)
    엔터프라이즈 기능 Terraform Cloud/Enterprise Pulumi Cloud

    ⚠️ 라이선스 변경 이슈 주의! Terraform은 2023년 8월에 라이선스를 MPL 2.0에서 BSL(Business Source License) 1.1로 변경했습니다. 이 때문에 OpenTofu라는 포크 프로젝트가 생겼을 정도로 커뮤니티에서 논란이 됐었어요. 상업적 목적으로 사용할 때는 라이선스 조건을 꼭 확인하세요.

    Terraform의 plan/apply 워크플로우와 Pulumi의 preview/up 워크플로우를 나란히 비교한 다이어그램

    실전에서 느끼는 차이 — 직접 써보니까요

    Terraform이 빛나는 순간들

    제가 Terraform을 처음 도입했을 때가 2019년쯤이었는데, 당시 팀에 인프라 엔지니어가 저 포함 3명이었어요. 개발팀과 협업할 일이 많았는데, HCL 파일을 코드 리뷰할 때 개발자들도 꽤 잘 읽더라고요. 선언적 문법이라 “이 리소스가 이렇게 생겼구나”를 직관적으로 파악하기 좋거든요.

    특히 이런 상황에서 Terraform이 강점을 발휘했어요:

    • 💡 팀에 인프라 전문가 비율이 높을 때 — HCL에 익숙해지면 오히려 더 깔끔하게 관리됨
    • 💡 레퍼런스가 중요할 때 — 거의 모든 클라우드 리소스에 대한 예제가 넘쳐남
    • 💡 안정성이 최우선일 때 — 10년 넘은 도구라 예측 가능한 동작이 보장됨
    • 💡 모듈 재사용이 핵심일 때 — Terraform Registry의 공개 모듈 생태계가 압도적
    # Terraform 모듈 활용 예시
    module "vpc" {
      source  = "terraform-aws-modules/vpc/aws"
      version = "~> 5.0"
    
      name = "my-vpc"
      cidr = "10.0.0.0/16"
    
      azs             = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
      private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
      public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
    
      enable_nat_gateway = true
    
      tags = {
        Environment = "production"
        Terraform   = "true"
      }
    }

    Terraform Registry에서 검증된 VPC 모듈 몇 줄로 완성이에요. 이런 거 보면 “역시 생태계가 갑이다” 싶죠.

    Terraform의 아픈 부분 😅

    근데 솔직히 말씀드리면, 복잡한 로직 처리할 때는 좀 힘들어요. 예를 들어 “조건에 따라 리소스 개수를 동적으로 결정”하려면…

    # Terraform에서 동적 리소스 생성 — 이게 직관적이지 않음
    resource "aws_security_group_rule" "ingress_rules" {
      count = length(var.ingress_ports)
    
      type              = "ingress"
      from_port         = var.ingress_ports[count.index]
      to_port           = var.ingress_ports[count.index]
      protocol          = "tcp"
      cidr_blocks       = ["0.0.0.0/0"]
      security_group_id = aws_security_group.main.id
    }
    
    # for_each를 쓰면 좀 낫지만, 여전히 복잡한 로직엔 한계가 있음
    resource "aws_instance" "servers" {
      for_each = var.server_configs
    
      ami           = each.value.ami
      instance_type = each.value.instance_type
    
      tags = {
        Name = each.key
      }
    }

    count, for_each 같은 메타 인수를 쓰다 보면 “이게 프로그래밍이 아니라 퍼즐 푸는 느낌”이 들 때가 있어요. 저도 처음엔 이게 뭔가 싶었는데, 익숙해지는 데 시간이 좀 걸렸습니다 ㅎㅎ

    Pulumi가 빛나는 순간들

    반면에 Pulumi는 개발자 팀에서 진가를 발휘하더라고요. 제가 사이드 프로젝트로 홈랩 인프라를 Pulumi(Python)로 관리해봤는데, 이런 점이 진짜 좋았어요:

    # Pulumi Python — 복잡한 로직도 그냥 Python으로
    import pulumi
    import pulumi_aws as aws
    
    # 환경별 설정을 딕셔너리로 관리
    env_configs = {
        "production": {"instance_type": "t3.medium", "count": 3},
        "staging":    {"instance_type": "t3.small",  "count": 1},
        "dev":        {"instance_type": "t3.micro",  "count": 1},
    }
    
    config = pulumi.Config()
    env = config.require("environment")
    current_config = env_configs[env]
    
    # 그냥 for 루프로 인스턴스 생성 — 이게 얼마나 자연스러운지!
    instances = []
    for i in range(current_config["count"]):
        instance = aws.ec2.Instance(
            f"web-server-{i}",
            ami="ami-0c55b159cbfafe1f0",
            instance_type=current_config["instance_type"],
            tags={
                "Name": f"web-server-{i}",
                "Environment": env,
            }
        )
        instances.append(instance)
    
    # 결과 출력도 Python 리스트 컴프리헨션으로
    pulumi.export("instance_ids", [inst.id for inst in instances])

    이거 처음 써봤을 때 “드디어 됐다!” 싶었어요. 프로그래밍 언어의 모든 기능을 그대로 쓸 수 있으니까, 복잡한 조건 처리나 반복 작업이 훨씬 자연스럽거든요.

    Pulumi가 특히 유리한 상황:

    • 💡 팀이 개발자 중심일 때 — TypeScript, Python 등 이미 아는 언어로 바로 시작 가능
    • 💡 인프라 로직이 복잡할 때 — 조건문, 반복문, 함수 등을 자유롭게 사용
    • 💡 테스트 자동화가 중요할 때 — pytest, Jest 등 기존 테스트 도구 그대로 활용
    • 💡 기존 코드베이스와 통합할 때 — 앱 코드와 인프라 코드를 같은 언어로 관리

    Pulumi의 아픈 부분 ⚠️

    근데 Pulumi도 완벽하진 않아요. 제가 겪은 몇 가지 불편한 점들:

    • 레퍼런스 부족 — 문제 생겼을 때 구글링해도 Terraform만큼 자료가 안 나와요. 특히 엣지 케이스는 직접 디버깅해야 하는 경우가 많았음
    • Pulumi Cloud 의존성 — 기본 상태 관리가 Pulumi Cloud를 통하는 방식이라, 자체 관리하려면 별도 설정이 필요
    • 언어별 지원 수준 차이 — TypeScript 지원이 가장 성숙하고, 다른 언어는 업데이트 시점이 조금씩 달라요
    • 팀 온보딩 — 인프라 엔지니어가 특정 프로그래밍 언어에 익숙하지 않으면 오히려 진입 장벽이 될 수 있음

    Pulumi Cloud 콘솔에서 스택 상태와 리소스 배포 결과를 확인하는 화면

    ⚠️ 실전에서 주의해야 할 것들

    Terraform 상태 파일(State File) 관리

    Terraform 쓰면서 가장 많이 삽질하는 부분이 상태 파일 관리예요. 처음에 로컬에 `terraform.tfstate` 파일 두고 팀에서 같이 쓰다가 충돌 났던 경험, 한 번쯤은 다들 있으실 거예요 ㅎㅎ

    # 반드시 원격 백엔드 설정하세요!
    terraform {
      backend "s3" {
        bucket         = "my-terraform-state"
        key            = "production/terraform.tfstate"
        region         = "ap-northeast-2"
        encrypt        = true
        dynamodb_table = "terraform-state-lock"  # 동시 수정 방지용 잠금
      }
    }
    

    DynamoDB로 상태 잠금(State Locking) 설정 안 해두면, 두 사람이 동시에 apply 실행할 때 상태 파일이 꼬일 수 있어요. 이거 한 번 겪어보면 절대 안 잊어버리게 됩니다 😅

    Pulumi 스택(Stack) 분리 전략

    Pulumi에서는 환경별로 스택을 분리하는 게 기본 패턴인데, 초반에 이걸 제대로 설계 안 하면 나중에 꽤 고생해요.

    # Pulumi 스택 생성 및 관리
    pulumi stack init production
    pulumi stack init staging
    pulumi stack init dev
    
    # 현재 스택 확인
    pulumi stack ls
    
    # 스택 전환
    pulumi stack select production
    
    # 배포 미리보기 (Terraform의 plan에 해당)
    pulumi preview
    
    # 실제 배포
    pulumi up

    💡 팁: Pulumi에서 환경별 설정값은 `pulumi config set`으로 관리하세요. 민감한 값은 `–secret` 플래그로 암호화해서 저장할 수 있어요.

    # 일반 설정값
    pulumi config set aws:region ap-northeast-2
    pulumi config set environment production
    
    # 민감한 정보는 암호화 저장
    pulumi config set --secret db_password "super-secret-password"

    Terraform 라이선스 변경 이후 고민

    앞서 언급했지만, 2023년 Terraform의 BSL 라이선스 전환은 꽤 큰 이슈였어요. 이 때문에 OpenTofu라는 오픈소스 포크가 Linux Foundation 산하에서 시작됐고, 많은 조직에서 마이그레이션을 고려하기 시작했죠. Pulumi로 넘어가는 팀들도 일부 있었고요.

    만약 상업적 사용이나 Terraform 기반 서비스 제공을 고려하고 있다면, BSL 1.1 조건을 법무팀과 함께 꼼꼼히 검토하시길 권장합니다.

    결국 뭘 선택해야 할까요? — 상황별 가이드

    제가 컨설팅이나 팀 내 논의에서 자주 받는 질문이 “그래서 뭐 써야 해요?”인데요. 정답은 없지만, 제 기준을 공유해 드릴게요.

    ✅ Terraform을 선택하면 좋은 경우

    • 팀에 인프라 전문가 비율이 높고, HCL에 거부감이 없을 때
    • 커뮤니티 레퍼런스와 안정성이 최우선일 때
    • Terraform Registry의 공개 모듈을 적극 활용하고 싶을 때
    • 기존 Terraform 코드베이스가 있는 팀에 합류할 때
    • 비교적 단순한 인프라 구성이 주를 이룰 때

    ✅ Pulumi를 선택하면 좋은 경우

    • 팀이 소프트웨어 개발자 중심이고, 특정 언어에 능숙할 때
    • 인프라 로직이 복잡해서 프로그래밍적 표현이 필요할 때
    • 인프라 코드에 유닛 테스트를 적용하고 싶을 때
    • 앱 코드와 인프라 코드를 같은 언어로 통합 관리하고 싶을 때
    • 라이선스 이슈에서 자유로운 완전 오픈소스 도구가 필요할 때

    팀 상황과 프로젝트 특성에 따른 Terraform vs Pulumi 선택 가이드 인포그래픽

    자주 묻는 질문 (FAQ)

    Q. Terraform에서 Pulumi로 마이그레이션할 수 있나요?

    네, Pulumi에서 공식적으로 변환 도구를 제공해요. `pulumi convert –from terraform` 명령으로 기본적인 리소스 변환이 가능합니다. 완벽하지는 않지만, 간단한 구성은 꽤 잘 변환되더라고요. 다만 복잡한 모듈이나 커스텀 프로바이더는 수동 작업이 필요할 수 있어요.

    Q. 둘 다 멀티 클라우드(Multi-Cloud)를 지원하나요?

    둘 다 AWS, Azure, GCP 등 주요 클라우드 프로바이더를 지원합니다. Terraform은 프로바이더 생태계가 더 방대하고, Pulumi는 Terraform 프로바이더를 브리지(Bridge)해서 쓸 수 있어서 커버리지 차이가 많이 줄어들었어요.

    Q. Pulumi는 무료인가요?

    Pulumi 자체는 오픈소스(Apache 2.0)로 무료입니다. Pulumi Cloud의 경우 개인 사용은 무료 플랜이 있고, 팀 협업 기능이나 고급 기능은 유료 플랜이 필요해요. 자체 백엔드(S3, Azure Blob Storage 등)를 사용하면 Cloud 없이도 운영 가능합니다.

    Q. 둘 다 Kubernetes(쿠버네티스) 관리에 적합한가요?

    네, 둘 다 Kubernetes 리소스 관리를 지원합니다. 다만 Kubernetes 전용으로는 Helm이나 Kustomize와의 조합이 더 일반적이에요. 클라우드 인프라(EKS 클러스터 생성 등)와 Kubernetes 리소스를 함께 관리할 때 IaC 도구가 유용하게 쓰입니다.

    마무리 — 결국 중요한 건 팀과 컨텍스트

    Pulumi vs Terraform 비교를 오래 해봤지만, 솔직히 말씀드리면 둘 다 훌륭한 IaC 도구입니다. 어느 쪽이 “객관적으로 더 좋다”라고 말하기가 어려워요.

    제 개인적인 포지션을 말씀드리면:

    • 🏢 엔터프라이즈 환경, 인프라 팀 주도 → Terraform (안정성, 레퍼런스, 생태계)
    • 🚀 스타트업, 개발자 팀, 복잡한 로직 → Pulumi (유연성, 언어 친숙도)
    • 🏠 홈랩, 개인 프로젝트 → 둘 다 배워보세요! 경험치 쌓기엔 최고

    저는 요즘 홈랩은 Pulumi(Python)로, 업무 환경은 Terraform으로 관리하고 있어요. 두 도구를 병행하면서 각각의 강점을 더 명확하게 느끼게 됐달까요.

    혹시 이미 둘 중 하나를 쓰고 계신 분들은, 반대편 도구도 간단한 사이드 프로젝트로 한번 써보시길 추천드려요. 비교해봐야 진짜 차이가 느껴지거든요.

    다음 글에서는 Terraform 상태 관리와 원격 백엔드 설정을 더 깊이 다뤄볼 예정이에요. Terraform 쓰다가 상태 파일로 삽질한 경험이 있으신 분들이라면 도움이 될 겁니다. 이전 글에서 다뤘던 클라우드 인프라 자동화 기초도 함께 참고해보세요!

    질문이나 의견은 댓글로 남겨주세요. 저도 아직 배우는 중이라, 여러분의 경험도 궁금합니다 😊

  • [Cloud] Ansible 클라우드 보안 자동화: 취약점 관리 및 규정 준수 가이드

    클라우드 보안, 손으로 하나하나 설정하다가 사고 날 뻔했습니다

    솔직히 말씀드리면, 저도 처음엔 보안 설정을 수작업으로 했어요. EC2 인스턴스 하나하나 들어가서 패키지 업데이트 확인하고, 보안 그룹 규칙 검토하고… 인스턴스가 10개쯤 됐을 때까진 그나마 버텼는데, 50개 넘어가니까 진짜 손이 두 개로는 부족하더라고요. 어느 날 감사(Audit) 리포트 보다가 패치 안 된 서버가 12대나 있다는 걸 발견했을 때 등에 식은땀이 흘렀습니다.

    그때부터 Ansible 보안 자동화를 본격적으로 파기 시작했어요. 취약점 관리(Vulnerability Management)부터 규정 준수(Compliance) 검증까지, Ansible로 어떻게 자동화할 수 있는지 실무 경험을 바탕으로 풀어드릴게요. 클라우드 환경에서 보안을 체계적으로 관리하고 싶으신 분들께 실질적인 도움이 됐으면 합니다.

    ▲ Ansible을 중심으로 한 클라우드 보안 자동화 전체 흐름 — 인벤토리 수집부터 취약점 패치, 규정 준수 보고까지 한눈에


    Ansible 보안 자동화, 이게 왜 필요한가요?

    쉽게 말해서, 클라우드 환경은 서버가 수시로 생겼다 없어지거든요. 온프레미스(자체 서버실)처럼 고정된 인프라가 아니에요. 오늘 오토스케일링으로 인스턴스 10개 생겼다가 내일 5개로 줄어들 수 있잖아요. 이런 환경에서 수작업 보안 관리는 현실적으로 불가능합니다.

    Ansible 보안 자동화가 필요한 이유를 정리해보면:

    • 일관성(Consistency): 모든 서버에 동일한 보안 정책 적용 — 사람이 하면 실수가 생기지만 플레이북(Playbook)은 실수 안 함
    • 속도(Speed): 수백 대 서버에 패치 적용을 몇 분 안에 완료
    • 감사 추적(Audit Trail): 언제, 어떤 변경이 있었는지 코드로 기록됨
    • 반복 가능성(Repeatability): 같은 작업을 언제든 동일하게 재현 가능
    • 드리프트 감지(Configuration Drift Detection): 설정이 원하는 상태에서 벗어나면 자동으로 교정

    근데 여기서 중요한 포인트! Ansible은 에이전트리스(Agentless) 방식이에요. 관리 대상 서버에 별도 소프트웨어를 설치할 필요가 없고, SSH(또는 WinRM)만 열려 있으면 됩니다. 클라우드 환경에서 이게 엄청난 장점이거든요.


    Ansible Vault로 민감 정보 보호하기

    보안 자동화를 하면서 제일 먼저 부딪히는 문제가 뭔지 아세요? 바로 시크릿(Secret) 관리입니다. API 키, 데이터베이스 패스워드, 클라우드 자격증명… 이걸 플레이북 파일에 평문으로 넣으면 안 되잖아요.

    Ansible에는 Ansible Vault라는 내장 암호화 도구가 있어요. 파일이나 변수를 AES-256으로 암호화해서 Git에 안전하게 올릴 수 있게 해줍니다. 제가 실제로 쓰는 방식을 보여드릴게요.

    Vault 파일 생성 및 사용

    # vault 암호화 파일 생성
    ansible-vault create secrets/cloud_credentials.yml
    
    # 기존 파일 암호화
    ansible-vault encrypt vars/sensitive_vars.yml
    
    # 암호화된 파일 내용 확인
    ansible-vault view secrets/cloud_credentials.yml
    
    # 암호화된 파일 수정
    ansible-vault edit secrets/cloud_credentials.yml
    
    # 플레이북 실행 시 vault 패스워드 파일 사용
    ansible-playbook security_hardening.yml --vault-password-file ~/.vault_pass.txt
    # secrets/cloud_credentials.yml (암호화 전 내용 예시)
    ---
    aws_access_key: "AKIAIOSFODNN7EXAMPLE"
    aws_secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    db_password: "super_secret_password_here"
    api_token: "your_api_token_here"

    💡 팁: 운영 환경에서는 vault 패스워드 파일을 직접 관리하는 것보다 HashiCorp Vault나 AWS Secrets Manager 같은 전용 시크릿 관리 서비스와 연동하는 게 훨씬 안전합니다.


    취약점 관리 자동화 실전

    이제 본격적으로 들어가볼게요. Ansible 취약점 관리의 핵심은 세 가지입니다: 취약 패키지 탐지 → 패치 적용 → 결과 보고. 제가 실제 환경에서 쓰는 플레이북 구조를 공유할게요.

    1단계: 인벤토리 동적 구성 (Dynamic Inventory)

    클라우드 환경에서는 정적 인벤토리(Static Inventory)가 아니라 동적 인벤토리(Dynamic Inventory)를 써야 해요. AWS 기준으로 보면:

    # inventory/aws_ec2.yml
    ---
    plugin: amazon.aws.aws_ec2
    regions:
      - ap-northeast-2  # 서울 리전
    filters:
      instance-state-name: running
      tag:Environment:
        - production
        - staging
    keyed_groups:
      - key: tags.Role
        prefix: role
      - key: tags.Environment
        prefix: env
    hostname_source: private-ip-address
    compose:
      ansible_host: private_ip_address
    # 동적 인벤토리 확인
    ansible-inventory -i inventory/aws_ec2.yml --list
    ansible-inventory -i inventory/aws_ec2.yml --graph

    2단계: 취약점 스캔 및 패치 플레이북

    # playbooks/vulnerability_management.yml
    ---
    - name: 취약점 스캔 및 보안 패치 자동화
      hosts: all
      become: yes
      gather_facts: yes
      vars_files:
        - ../secrets/cloud_credentials.yml
    
      vars:
        patch_reboot_required: false
        security_only: true
        report_path: "/var/log/ansible/security_report"
    
      tasks:
        - name: 리포트 디렉토리 생성
          file:
            path: "{{ report_path }}"
            state: directory
            mode: '0750'
            owner: root
            group: root
    
        - name: 패키지 캐시 업데이트 (Debian/Ubuntu)
          apt:
            update_cache: yes
            cache_valid_time: 3600
          when: ansible_os_family == "Debian"
    
        - name: 패키지 캐시 업데이트 (RHEL/CentOS/Amazon Linux)
          yum:
            update_cache: yes
          when: ansible_os_family == "RedHat"
    
        - name: 보안 업데이트 목록 확인 (Debian/Ubuntu)
          shell: apt list --upgradeable 2>/dev/null | grep -i security
          register: security_updates_deb
          changed_when: false
          failed_when: false
          when: ansible_os_family == "Debian"
    
        - name: 보안 업데이트 목록 확인 (RHEL 계열)
          shell: yum check-update --security 2>/dev/null | tail -n +3
          register: security_updates_rhel
          changed_when: false
          failed_when: security_updates_rhel.rc not in [0, 100]
          when: ansible_os_family == "RedHat"
    
        - name: 보안 패치 적용 (Debian/Ubuntu)
          apt:
            upgrade: dist
            update_cache: yes
            only_upgrade: yes
          when:
            - ansible_os_family == "Debian"
            - security_only | bool
          register: apt_upgrade_result
    
        - name: 보안 패치 적용 (RHEL 계열)
          yum:
            name: "*"
            state: latest
            security: yes
          when:
            - ansible_os_family == "RedHat"
            - security_only | bool
          register: yum_upgrade_result
    
        - name: 재부팅 필요 여부 확인 (Ubuntu)
          stat:
            path: /var/run/reboot-required
          register: reboot_required_file
          when: ansible_os_family == "Debian"
    
        - name: 결과 리포트 생성
          template:
            src: ../templates/security_report.j2
            dest: "{{ report_path }}/report_{{ ansible_date_time.date }}.txt"
            mode: '0640'
          vars:
            hostname: "{{ ansible_hostname }}"
            os_family: "{{ ansible_os_family }}"
            kernel_version: "{{ ansible_kernel }}"
            reboot_needed: "{{ reboot_required_file.stat.exists | default(false) }}"
    
        - name: 재부팅 필요 시 알림
          debug:
            msg: "⚠️ {{ ansible_hostname }} 서버는 패치 적용 후 재부팅이 필요합니다!"
          when:
            - ansible_os_family == "Debian"
            - reboot_required_file.stat.exists | default(false)

    ▲ Ansible 플레이북 실행 결과 — 각 호스트별 패치 적용 현황과 재부팅 필요 여부가 한눈에 표시됨

    3단계: 리포트 템플릿 (Jinja2)

    # templates/security_report.j2
    === 보안 패치 리포트 ===
    호스트명: {{ hostname }}
    OS 계열: {{ os_family }}
    커널 버전: {{ kernel_version }}
    실행 일시: {{ ansible_date_time.iso8601 }}
    재부팅 필요: {{ reboot_needed }}
    
    {% if ansible_os_family == 'Debian' %}
    적용된 업데이트:
    {{ apt_upgrade_result.stdout | default('변경 없음') }}
    {% elif ansible_os_family == 'RedHat' %}
    적용된 업데이트:
    {{ yum_upgrade_result.results | default('변경 없음') }}
    {% endif %}
    ========================

    규정 준수 자동화 — CIS 벤치마크 적용

    취약점 패치만큼 중요한 게 바로 Ansible 규정 준수 자동화예요. CIS(Center for Internet Security) 벤치마크나 NIST 프레임워크 같은 보안 기준을 서버에 자동으로 적용하고 검증하는 거죠.

    저는 주로 CIS 리눅스 벤치마크를 기반으로 하드닝(보안 강화) 플레이북을 만들어서 씁니다. 핵심 항목들을 추려서 보여드릴게요.

    # playbooks/cis_compliance.yml
    ---
    - name: CIS 벤치마크 기반 보안 하드닝
      hosts: "{{ target_hosts | default('all') }}"
      become: yes
      gather_facts: yes
    
      vars:
        cis_level: 1  # 1 또는 2
        disable_unused_services: true
        configure_auditd: true
    
      tasks:
        # === 파일시스템 설정 ===
        - name: "CIS 1.1.1 - /tmp 파티션 nodev 마운트 옵션 설정"
          mount:
            name: /tmp
            src: tmpfs
            fstype: tmpfs
            opts: "defaults,rw,nosuid,nodev,noexec,relatime"
            state: mounted
          tags: [filesystem, cis_1_1]
    
        # === SSH 보안 설정 ===
        - name: "CIS 5.2 - SSH 보안 설정 강화"
          lineinfile:
            path: /etc/ssh/sshd_config
            regexp: "{{ item.regexp }}"
            line: "{{ item.line }}"
            state: present
            backup: yes
          loop:
            - { regexp: '^#?Protocol', line: 'Protocol 2' }
            - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
            - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
            - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
            - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 4' }
            - { regexp: '^#?PermitEmptyPasswords', line: 'PermitEmptyPasswords no' }
            - { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' }
            - { regexp: '^#?ClientAliveCountMax', line: 'ClientAliveCountMax 0' }
            - { regexp: '^#?LoginGraceTime', line: 'LoginGraceTime 60' }
          notify: restart sshd
          tags: [ssh, cis_5_2]
    
        # === 감사 로그(Auditd) 설정 ===
        - name: "CIS 4.1 - auditd 설치 및 활성화"
          package:
            name: auditd
            state: present
          when: configure_auditd | bool
          tags: [auditd, cis_4_1]
    
        - name: "CIS 4.1 - auditd 규칙 설정"
          copy:
            content: |
              # 시스템 콜 감사 규칙
              -a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time-change
              -a always,exit -F arch=b32 -S adjtimex -S settimeofday -S stime -k time-change
              -w /etc/localtime -p wa -k time-change
              -w /etc/group -p wa -k identity
              -w /etc/passwd -p wa -k identity
              -w /etc/gshadow -p wa -k identity
              -w /etc/shadow -p wa -k identity
              -w /etc/sudoers -p wa -k scope
              -w /var/log/sudo.log -p wa -k actions
              -w /sbin/insmod -p x -k modules
              -w /sbin/rmmod -p x -k modules
              -w /sbin/modprobe -p x -k modules
              -e 2
            dest: /etc/audit/rules.d/cis_hardening.rules
            mode: '0640'
            owner: root
            group: root
          notify: reload auditd
          when: configure_auditd | bool
          tags: [auditd, cis_4_1]
    
        # === 패스워드 정책 ===
        - name: "CIS 5.4.1 - 패스워드 만료 정책 설정"
          lineinfile:
            path: /etc/login.defs
            regexp: "{{ item.regexp }}"
            line: "{{ item.line }}"
          loop:
            - { regexp: '^PASS_MAX_DAYS', line: 'PASS_MAX_DAYS   90' }
            - { regexp: '^PASS_MIN_DAYS', line: 'PASS_MIN_DAYS   7' }
            - { regexp: '^PASS_WARN_AGE', line: 'PASS_WARN_AGE   14' }
          tags: [password_policy, cis_5_4]
    
        # === 불필요한 서비스 비활성화 ===
        - name: "CIS 2.2 - 불필요한 서비스 비활성화"
          service:
            name: "{{ item }}"
            state: stopped
            enabled: no
          loop:
            - telnet
            - rsh
            - rlogin
            - rexec
            - tftp
            - xinetd
          failed_when: false
          when: disable_unused_services | bool
          tags: [services, cis_2_2]
    
      handlers:
        - name: restart sshd
          service:
            name: sshd
            state: restarted
    
        - name: reload auditd
          service:
            name: auditd
            state: restarted

    ⚠️ 삽질 경험: 이런 실수 하지 마세요

    저도 처음엔 꽤 고생했거든요. 실제로 겪은 문제들을 공유할게요.

    실수 1: 프로덕션에 –check 없이 바로 실행

    이거 진짜 아찔했습니다. 테스트 환경에서 잘 돌아가던 플레이북을 프로덕션에 그냥 실행했다가 SSH 설정 변경으로 일부 서버 접근이 막힌 적이 있어요. 지금은 반드시 이 순서를 지킵니다:

    # 1단계: 드라이런 — 실제 변경 없이 시뮬레이션
    ansible-playbook cis_compliance.yml -i inventory/aws_ec2.yml --check --diff
    
    # 2단계: 특정 태그만 먼저 테스트
    ansible-playbook cis_compliance.yml -i inventory/aws_ec2.yml --tags ssh --check
    
    # 3단계: 한 대만 먼저 적용
    ansible-playbook cis_compliance.yml -i inventory/aws_ec2.yml --limit "specific_host_ip" 
    
    # 4단계: 전체 적용
    ansible-playbook cis_compliance.yml -i inventory/aws_ec2.yml

    실수 2: 멱등성(Idempotency) 무시

    Ansible의 핵심 철학이 멱등성(같은 작업을 여러 번 실행해도 결과가 동일)인데, 초반에 shell 모듈을 너무 남발했어요. shell로 스크립트 실행하면 매번 “changed” 상태가 되거든요. 가능하면 Ansible 내장 모듈(file, lineinfile, service 등)을 쓰는 게 맞습니다.

    실수 3: Vault 패스워드 관리 소홀

    팀원 여러 명이 같은 vault 패스워드를 공유하다가, 퇴사자 발생 시 전체 재암호화해야 하는 상황이 생겼어요. 지금은 CI/CD 파이프라인에서 환경변수로 관리하고, 팀원별 접근은 AWS IAM으로 통제합니다.

    문제 상황 잘못된 방법 올바른 방법
    민감 정보 관리 평문으로 vars 파일에 저장 Ansible Vault + Secrets Manager 연동
    플레이북 테스트 프로덕션에 직접 실행 –check –diff로 드라이런 먼저
    명령 실행 shell/command 모듈 남용 전용 모듈 우선 사용 (멱등성 보장)
    대규모 적용 전체 호스트에 한 번에 실행 –limit으로 단계적 적용
    에러 처리 failed_when 미설정 적절한 failed_when/ignore_errors 설정

    규정 준수 검증 및 리포팅 자동화

    보안 설정을 적용했으면, 실제로 잘 됐는지 검증하고 리포트를 뽑아야죠. 저는 Ansible과 함께 OpenSCAP(보안 설정 자동화 프로토콜 기반 도구)을 연동해서 씁니다.

    # playbooks/compliance_report.yml
    ---
    - name: 규정 준수 검증 및 HTML 리포트 생성
      hosts: all
      become: yes
      gather_facts: yes
    
      vars:
        report_dir: "/var/log/compliance_reports"
        scap_profile: "xccdf_org.ssgproject.content_profile_cis"
    
      tasks:
        - name: OpenSCAP 및 SCAP 보안 가이드 설치 (RHEL 계열)
          yum:
            name:
              - openscap-scanner
              - scap-security-guide
            state: present
          when: ansible_os_family == "RedHat"
    
        - name: 리포트 디렉토리 생성
          file:
            path: "{{ report_dir }}"
            state: directory
            mode: '0750'
    
        - name: SCAP 스캔 실행 및 HTML 리포트 생성
          command: >
            oscap xccdf eval
            --profile {{ scap_profile }}
            --results {{ report_dir }}/results_{{ ansible_hostname }}_{{ ansible_date_time.date }}.xml
            --report {{ report_dir }}/report_{{ ansible_hostname }}_{{ ansible_date_time.date }}.html
            /usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml
          register: scap_result
          failed_when: scap_result.rc not in [0, 2]
          when: ansible_os_family == "RedHat"
    
        - name: 스캔 결과 요약 출력
          debug:
            msg: "{{ ansible_hostname }} 스캔 완료 — 리포트: {{ report_dir }}/report_{{ ansible_hostname }}_{{ ansible_date_time.date }}.html"
    
        - name: 리포트 파일 로컬로 가져오기
          fetch:
            src: "{{ report_dir }}/report_{{ ansible_hostname }}_{{ ansible_date_time.date }}.html"
            dest: "./compliance_reports/"
            flat: no

    ▲ 규정 준수 검증 결과 대시보드 — 각 CIS 벤치마크 항목별 통과/실패/해당없음 현황을 서버별로 한눈에 파악 가능

    이렇게 하면 매주 자동으로 각 서버의 규정 준수 현황을 HTML 리포트로 뽑을 수 있어요. 경영진이나 감사팀에 제출할 때도 편하고, 뭐가 문제인지 한눈에 보이거든요.


    CI/CD 파이프라인과 보안 자동화 연동

    사실 이게 제일 강력한 부분이에요. 깃허브 액션(GitHub Actions)이나 젠킨스(Jenkins)와 연동하면, 코드 변경이 있을 때마다 자동으로 보안 검증이 돌아가거든요.

    # .github/workflows/security_automation.yml
    name: 보안 자동화 파이프라인
    
    on:
      schedule:
        - cron: '0 2 * * *'  # 매일 새벽 2시 실행
      push:
        branches: [main]
        paths:
          - 'playbooks/**'
          - 'inventory/**'
    
    jobs:
      security-scan:
        runs-on: ubuntu-latest
        steps:
          - name: 코드 체크아웃
            uses: actions/checkout@v3
    
          - name: Python 및 Ansible 설치
            run: |
              pip install ansible ansible-lint
              ansible-galaxy collection install amazon.aws community.general
    
          - name: Ansible Lint 실행 (플레이북 문법/베스트프랙티스 검사)
            run: ansible-lint playbooks/
    
          - name: Vault 패스워드 설정
            run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ~/.vault_pass.txt
    
          - name: 드라이런 실행 (실제 변경 없이 검증)
            run: |
              ansible-playbook playbooks/vulnerability_management.yml \
                -i inventory/aws_ec2.yml \
                --vault-password-file ~/.vault_pass.txt \
                --check
            env:
              AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
              AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    
          - name: 승인 후 실제 적용 (main 브랜치만)
            if: github.ref == 'refs/heads/main'
            run: |
              ansible-playbook playbooks/vulnerability_management.yml \
                -i inventory/aws_ec2.yml \
                --vault-password-file ~/.vault_pass.txt
            env:
              AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
              AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

    이렇게 구성하면 드리프트(원하는 상태에서 벗어남)가 발생해도 다음 날 새벽에 자동으로 교정이 되거든요. 정말 마음이 편해집니다.


    정리: Ansible 클라우드 보안 자동화 핵심 체크리스트

    ▲ Ansible 클라우드 보안 자동화 핵심 구성 요소 요약 — Vault, 취약점 관리, 규정 준수, CI/CD 연동의 4가지 축

    실무에서 느낀 건, 보안은 한 번 설정하고 끝나는 게 아니라는 거예요. 지속적으로 검증하고, 자동화해야 실수가 없습니다. Ansible 보안 자동화로 구성한 체계를 정리해보면:

    1. ✅ Ansible Vault로 모든 민감 정보 암호화 — 평문 시크릿은 절대 금지
    2. ✅ 동적 인벤토리(Dynamic Inventory)로 클라우드 환경 자동 탐지
    3. ✅ 취약점 관리 플레이북으로 정기적 보안 패치 자동화
    4. ✅ CIS 벤치마크 기반 하드닝으로 일관된 보안 기준 적용
    5. ✅ OpenSCAP 연동으로 규정 준수 검증 및 리포트 자동 생성
    6. ✅ CI/CD 파이프라인 연동으로 드리프트 자동 교정
    7. ✅ 드라이런(–check –diff)을 습관화해서 실수 방지

    혹시 이 글을 보고 궁금한 점이 생기셨나요? 다음 글에서는 HashiCorp Vault와 Ansible 연동으로 더 고도화된 시크릿 관리 방법을 다뤄볼 예정이에요. Ansible 기초 인프라 자동화도 함께 보시면 이해가 더 쉬울 거예요.

    뭔가 막히는 부분 있으시면 댓글로 편하게 물어보세요. 저도 삽질하면서 배운 거라, 같이 고민해드릴 수 있습니다.


    자주 묻는 질문 (FAQ)

    Q. Ansible로 Windows 서버 보안도 자동화할 수 있나요?

    네, 가능합니다. Windows는 SSH 대신 WinRM(Windows Remote Management)을 통해 연결하고, win_updates, win_service 같은 Windows 전용 모듈을 사용합니다. 다만 설정이 리눅스보다 조금 더 복잡한 편이에요.

    Q. Ansible Tower(AWX)를 써야 하나요?

    소규모 환경(서버 50대 이하)이면 CLI만으로도 충분합니다. 팀 규모가 커지고, 역할 기반 접근 제어(RBAC)나 스케줄링, 웹 UI가 필요해지면 오픈소스인 AWX나 상용 버전인 Ansible Automation Platform을 고려해보세요.

    Q. 플레이북 실행 중 에러가 나면 어떻게 되나요?

    기본적으로 에러가 발생한 호스트에서 플레이북 실행이 중단됩니다. ignore_errors: yes나 failed_when 조건을 적절히 설정해서 에러 처리를 세밀하게 제어할 수 있어요. 중요한 보안 작업은 에러 발생 시 즉시 알림을 받도록 Slack이나 이메일 핸들러를 연동하는 걸 권장합니다.

  • [k8s] Istio 서비스 메시 완벽 가이드: 설치부터 트래픽 관리, 보안까지

    마이크로서비스가 늘어날수록 머리가 아파지더라고요

    처음 마이크로서비스(Microservices) 아키텍처를 도입했을 때만 해도 “이제 서비스별로 독립 배포되니까 편하겠다!” 싶었거든요. 근데 서비스가 10개, 20개를 넘어가면서 슬슬 문제가 생기기 시작했습니다. 서비스 A가 서비스 B를 호출하다 타임아웃이 나는데, 어디서 문제가 생겼는지 추적이 안 되고… 보안 정책은 각 서비스마다 따로 관리해야 하고… 트래픽을 특정 버전으로 라우팅하고 싶은데 코드를 건드려야 하고…

    그때 Istio 서비스 메시(Service Mesh)를 처음 접했습니다. 솔직히 처음엔 “또 새로운 복잡한 거 배워야 하나…” 싶었는데, 써보고 나서 생각이 완전히 바뀌었어요. 오늘은 Istio 설치부터 트래픽 관리, 보안까지 제가 직접 삽질하며 쌓은 경험을 공유해 드리려 합니다.

    Istio 서비스 메시의 전체 아키텍처 — 컨트롤 플레인(istiod)과 데이터 플레인(Envoy 사이드카)의 관계를 보여줍니다.

    Istio 서비스 메시가 뭔지 먼저 짚고 가죠

    서비스 메시(Service Mesh)란?

    쉽게 말해, 마이크로서비스들 사이의 통신을 인프라 레벨에서 투명하게 관리해주는 레이어입니다. 각 서비스 옆에 Envoy(엔보이)라는 프록시를 붙여놓고, 모든 네트워크 트래픽이 이 프록시를 거치게 하는 방식이에요. 개발자는 코드를 건드릴 필요 없이 운영팀에서 트래픽 정책을 관리할 수 있게 됩니다.

    Istio는 현재 가장 널리 쓰이는 서비스 메시 솔루션으로, CNCF(Cloud Native Computing Foundation)의 그래듀에이티드 프로젝트입니다.

    Istio 핵심 구성 요소

    • istiod: 컨트롤 플레인(Control Plane). Pilot, Citadel, Galley가 합쳐진 단일 바이너리. 정책 관리의 두뇌 역할을 합니다
    • Envoy Sidecar(엔보이 사이드카): 각 Pod에 자동 주입되는 프록시. 실제 트래픽을 처리하는 데이터 플레인(Data Plane)입니다
    • Ingress Gateway(인그레스 게이트웨이): 클러스터 외부에서 들어오는 트래픽의 진입점입니다
    • Egress Gateway(이그레스 게이트웨이): 클러스터 외부로 나가는 트래픽을 제어합니다
    구성 요소 역할 위치
    istiod 설정 배포, 인증서 관리, 서비스 디스커버리 컨트롤 플레인 (istio-system 네임스페이스)
    Envoy Proxy 실제 트래픽 처리, 메트릭 수집 각 Pod 내 사이드카 컨테이너
    Ingress Gateway 외부 트래픽 수신 및 라우팅 데이터 플레인 (별도 Pod)

    Istio 설치 — 제가 권장하는 방법

    사전 준비

    Istio 설치 전에 Kubernetes 클러스터가 준비되어 있어야 합니다. 저는 홈랩에서 k3s 위에 올려서 테스트했고, 실무에서는 EKS, GKE 환경에서도 써봤어요. 공통적으로 잘 동작하더라고요.

    • Kubernetes 1.21 이상 (권장)
    • kubectl 설치 및 클러스터 접근 설정
    • 충분한 리소스 (컨트롤 플레인용 최소 4GB RAM 권장)

    istioctl 설치

    Istio 공식 CLI 도구인 istioctl을 먼저 설치합니다. 가장 간단한 방법은 공식 설치 스크립트를 사용하는 거예요.

    # Istio 최신 버전 다운로드 및 설치
    curl -L https://istio.io/downloadIstio | sh -
    
    # 다운로드된 디렉토리로 이동 (버전 번호는 다를 수 있습니다)
    cd istio-*
    
    # PATH에 istioctl 추가
    export PATH=$PWD/bin:$PATH
    
    # 영구 적용을 원하면 ~/.bashrc 또는 ~/.zshrc에 추가
    echo 'export PATH=$HOME/istio-*/bin:$PATH' >> ~/.bashrc
    
    # 설치 확인
    istioctl version

    Istio 프로파일 선택 및 설치

    Istio는 여러 설치 프로파일(Profile)을 제공합니다. 처음엔 이게 뭔 차이인지 몰라서 그냥 default로 했다가 나중에 조정했었는데요. 미리 파악해두시면 정말 도움이 됩니다.

    프로파일 특징 권장 사용처
    default istiod + Ingress Gateway 포함 프로덕션 환경
    demo 모든 기능 활성화, 모니터링 포함 학습/테스트
    minimal istiod만 설치 커스텀 구성
    external 외부 컨트롤 플레인 연결용 멀티 클러스터
    # 사전 체크 (클러스터 호환성 확인)
    istioctl x precheck
    
    # demo 프로파일로 설치 (학습 목적)
    istioctl install --set profile=demo -y
    
    # 설치 확인
    kubectl get pods -n istio-system
    
    # 출력 예시:
    # NAME                                   READY   STATUS    RESTARTS   AGE
    # istiod-xxxxxxxxx-xxxxx                 1/1     Running   0          2m
    # istio-ingressgateway-xxxxxxxxx-xxxxx   1/1     Running   0          2m
    # istio-egressgateway-xxxxxxxxx-xxxxx    1/1     Running   0          2m

    네임스페이스에 사이드카 자동 주입 활성화

    이 단계를 빠뜨리면 나중에 “왜 메시가 안 되지?” 하고 한참 헤맵니다. 저도 처음에 이거 놓쳐서 30분을 날렸어요 ㅎㅎ

    # default 네임스페이스에 사이드카 자동 주입 레이블 추가
    kubectl label namespace default istio-injection=enabled
    
    # 확인
    kubectl get namespace default --show-labels

    Envoy 사이드카가 Pod에 자동 주입되는 과정 — 모든 인바운드/아웃바운드 트래픽이 사이드카 프록시를 거쳐 처리됩니다.

    Istio 트래픽 관리 — 이게 진짜 핵심입니다

    트래픽 관리가 Istio를 쓰는 가장 큰 이유 중 하나라고 생각해요. 카나리 배포(Canary Deployment), 블루/그린 배포(Blue/Green Deployment), A/B 테스트를 코드 변경 없이 할 수 있거든요.

    VirtualService와 DestinationRule 이해하기

    Istio 트래픽 관리의 두 핵심 리소스입니다.

    • VirtualService(버추얼 서비스): “이 트래픽을 어디로 보낼지” 라우팅 규칙을 정의합니다
    • DestinationRule(데스티네이션 룰): “목적지 서비스를 어떻게 나눌지” 서브셋(Subset) 및 로드밸런싱 정책을 정의합니다

    카나리 배포 설정 예시

    실제로 제가 가장 많이 쓰는 패턴입니다. 새 버전(v2)에 10%만 트래픽을 보내고, 안정적이면 점진적으로 늘리는 방식이에요.

    # destination-rule.yaml
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
      name: my-service-destination
    spec:
      host: my-service
      subsets:
      - name: v1
        labels:
          version: v1
      - name: v2
        labels:
          version: v2
      trafficPolicy:
        loadBalancer:
          simple: ROUND_ROBIN
    # virtual-service.yaml — 90% v1, 10% v2로 트래픽 분배
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: my-service-vs
    spec:
      hosts:
      - my-service
      http:
      - route:
        - destination:
            host: my-service
            subset: v1
          weight: 90
        - destination:
            host: my-service
            subset: v2
          weight: 10
    # 적용
    kubectl apply -f destination-rule.yaml
    kubectl apply -f virtual-service.yaml
    
    # 확인
    kubectl get virtualservice
    kubectl get destinationrule

    헤더 기반 라우팅 — A/B 테스트에 유용해요

    특정 헤더가 있는 요청만 새 버전으로 보내는 방식입니다. QA팀이나 내부 테스터에게만 새 버전을 보여줄 때 정말 유용하더라고요.

    # 헤더 기반 라우팅 VirtualService
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: my-service-header-routing
    spec:
      hosts:
      - my-service
      http:
      - match:
        - headers:
            x-test-user:
              exact: "true"
        route:
        - destination:
            host: my-service
            subset: v2
      - route:
        - destination:
            host: my-service
            subset: v1

    서킷 브레이커(Circuit Breaker) 설정

    장애가 전파되는 걸 막는 패턴입니다. 특정 서비스가 느려지거나 에러가 많이 나면 자동으로 차단해서 전체 시스템을 보호해주는 거예요.

    # circuit-breaker.yaml
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
      name: my-service-cb
    spec:
      host: my-service
      trafficPolicy:
        outlierDetection:
          consecutive5xxErrors: 5        # 연속 5xx 에러 5회
          interval: 30s                   # 30초 간격으로 체크
          baseEjectionTime: 30s           # 30초 동안 제외
          maxEjectionPercent: 100         # 최대 100% 제외 가능
        connectionPool:
          tcp:
            maxConnections: 100
          http:
            http1MaxPendingRequests: 100
            http2MaxRequests: 1000

    Istio 보안 — mTLS로 서비스 간 통신 암호화

    mTLS(Mutual TLS) 이해하기

    Istio 보안의 핵심은 mTLS(상호 TLS 인증)입니다. 클라이언트와 서버 양쪽 모두 인증서로 신원을 증명하는 방식이에요. 일반 TLS는 서버만 인증서를 제시하지만, mTLS는 양방향 인증이라 훨씬 강력합니다.

    Istio 1.5 버전부터는 기본적으로 mTLS가 PERMISSIVE(허용) 모드로 설정됩니다. 암호화된 연결과 평문 연결 모두 허용하는 모드예요. 마이그레이션할 때 유용하지만, 프로덕션에서는 STRICT 모드로 바꾸는 걸 강력히 권장합니다.

    # peer-auth-strict.yaml — STRICT mTLS 적용
    apiVersion: security.istio.io/v1beta1
    kind: PeerAuthentication
    metadata:
      name: default
      namespace: default
    spec:
      mtls:
        mode: STRICT
    # 적용
    kubectl apply -f peer-auth-strict.yaml
    
    # 검증 — 사이드카 없는 Pod에서 호출하면 실패해야 함
    kubectl exec -it [pod-name] -c istio-proxy -- curl http://my-service:8080

    AuthorizationPolicy — 세밀한 접근 제어

    mTLS로 암호화는 됐는데, 어떤 서비스가 어떤 서비스를 호출할 수 있는지 제어하고 싶을 때 AuthorizationPolicy(인가 정책)를 사용합니다.

    # authz-policy.yaml — frontend 서비스만 backend 서비스 호출 허용
    apiVersion: security.istio.io/v1beta1
    kind: AuthorizationPolicy
    metadata:
      name: backend-authz
      namespace: default
    spec:
      selector:
        matchLabels:
          app: backend
      action: ALLOW
      rules:
      - from:
        - source:
            principals:
            - "cluster.local/ns/default/sa/frontend-service-account"
        to:
        - operation:
            methods: ["GET", "POST"]
            paths: ["/api/*"]

    JWT 인증 설정

    # request-auth.yaml — JWT 토큰 검증
    apiVersion: security.istio.io/v1beta1
    kind: RequestAuthentication
    metadata:
      name: jwt-auth
      namespace: default
    spec:
      selector:
        matchLabels:
          app: my-service
      jwtRules:
      - issuer: "https://your-auth-server.com"
        jwksUri: "https://your-auth-server.com/.well-known/jwks.json"

    ⚠️ 삽질 경험 — 이것만 조심하세요

    제가 Istio 도입하면서 겪었던 주요 문제들 공유합니다. 미리 알고 계시면 정말 시간 많이 절약하실 거예요.

    문제 1: 사이드카가 주입이 안 된다

    # Pod 확인 — READY가 2/2가 아니라 1/1이면 사이드카 미주입
    kubectl get pods
    
    # 네임스페이스 레이블 확인
    kubectl get namespace default --show-labels
    
    # istio-injection=enabled 레이블이 없으면 추가
    kubectl label namespace default istio-injection=enabled
    
    # 기존 Pod는 재시작해야 사이드카가 주입됨!
    kubectl rollout restart deployment/my-deployment

    문제 2: mTLS STRICT 모드 전환 후 통신 두절

    STRICT 모드로 바꿨더니 레거시 서비스들이 다 죽어버린 경험이 있습니다. 사이드카가 없는 서비스는 mTLS를 못 하거든요.

    # 어떤 서비스가 mTLS를 안 하고 있는지 확인
    istioctl x describe service my-service
    
    # 특정 네임스페이스만 STRICT, 나머지는 PERMISSIVE 유지하는 방법
    # 네임스페이스별로 PeerAuthentication 적용 가능

    문제 3: VirtualService 적용했는데 트래픽 분배가 안 된다

    VirtualService의 hosts 필드와 실제 Kubernetes Service 이름이 정확히 일치해야 합니다. FQDN(완전 정규화 도메인 이름)으로 쓰거나 짧은 이름으로 통일해야 해요.

    # Istio 설정 검증
    istioctl analyze
    
    # 특정 Pod의 Envoy 설정 확인
    istioctl proxy-config cluster [pod-name]
    istioctl proxy-config route [pod-name]
    
    # 트래픽 흐름 확인
    istioctl proxy-config listeners [pod-name]

    문제 4: 리소스 사용량이 예상보다 높다

    사이드카가 모든 Pod에 붙으니까 메모리, CPU 사용량이 꽤 올라갑니다. 소규모 환경에서는 부담이 될 수 있어요. 저는 홈랩에서 처음 올렸을 때 메모리가 모자라서 한참 고생했습니다.

    • Envoy 프록시 하나당 약 50~100MB 메모리 사용 (환경마다 다름)
    • 사이드카 리소스 제한 설정을 통해 조절 가능
    • 꼭 필요한 네임스페이스에만 주입하는 것도 방법

    Kiali로 서비스 메시 시각화 확인하기

    Kiali(키알리)는 Istio의 공식 관찰성(Observability) 대시보드입니다. 서비스 간 트래픽 흐름을 그래프로 보여줘서 정말 유용해요. 처음 켰을 때 “오, 이게 되네!” 하고 감탄했습니다.

    # Kiali 설치 (demo 프로파일이면 이미 설치되어 있을 수 있음)
    kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.17/samples/addons/kiali.yaml
    
    # Prometheus, Grafana, Jaeger도 함께 설치 권장
    kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.17/samples/addons/prometheus.yaml
    kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.17/samples/addons/grafana.yaml
    kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.17/samples/addons/jaeger.yaml
    
    # Kiali 대시보드 열기
    istioctl dashboard kiali

    Kiali 대시보드에서 서비스 간 트래픽 흐름을 시각적으로 확인 — 실시간 RPS, 에러율, 레이턴시를 한눈에 볼 수 있습니다.

    Istio 도입 전후 비교 정리

    항목 Istio 도입 전 Istio 도입 후
    트래픽 라우팅 코드 변경 필요, 재배포 필요 YAML 수정만으로 즉시 적용
    서비스 간 암호화 개별 서비스에서 TLS 직접 구현 mTLS 자동 적용
    장애 추적 로그 뒤지며 수동 추적 분산 트레이싱(Distributed Tracing)으로 자동 추적
    접근 제어 각 서비스에서 개별 구현 AuthorizationPolicy로 중앙 관리
    카나리 배포 Ingress 설정 복잡, 별도 도구 필요 VirtualService weight 조정만으로 가능
    서킷 브레이커 라이브러리 직접 구현 (Hystrix 등) DestinationRule로 선언적 설정

    Istio 서비스 메시 도입 전후 비교 — 트래픽 관리, 보안, 관찰성 측면에서의 변화를 한눈에 정리한 인포그래픽입니다.

    자주 묻는 질문 (FAQ)

    Q. Istio는 소규모 환경에도 도입할 만한가요?

    솔직히 말씀드리면, 서비스가 5개 미만이면 오버엔지니어링일 수 있습니다. Istio 자체의 운영 복잡도가 있거든요. 서비스가 10개 이상이고, 트래픽 관리나 보안 요구사항이 복잡해질 때 도입을 고려하는 걸 권장합니다.

    Q. Istio 업그레이드는 어떻게 하나요?

    istioctl upgrade 명령어로 인플레이스(In-place) 업그레이드가 가능합니다. 다만 프로덕션에서는 카나리 업그레이드 방식을 권장해요. 이 부분은 다음 글에서 자세히 다룰 예정입니다.

    Q. Linkerd와 비교하면 어떤가요?

    Linkerd는 더 가볍고 단순하지만 기능이 제한적입니다. Istio는 기능이 풍부하지만 복잡도가 높아요. 팀의 기술 역량과 요구사항에 따라 선택하시면 됩니다. 서비스 메시 솔루션 비교는 별도 글로 정리해 드리겠습니다.

    Q. 사이드카 없이 Istio를 쓸 수 있나요?

    Istio 1.15부터 Ambient Mesh(앰비언트 메시)라는 사이드카 없는 방식이 알파로 도입됐습니다. 사이드카의 리소스 오버헤드를 줄이는 방향으로 발전하고 있어요. 아직 프로덕션에 쓰기엔 이르지만 지켜볼 만한 기술입니다.

    마무리 — 처음엔 어렵지만 익숙해지면 없어서 못 삽니다

    처음 Istio를 배울 때는 개념도 많고, CRD(Custom Resource Definition)도 낯설고, 트러블슈팅도 어렵게 느껴집니다. 저도 처음 3개월은 꽤 힘들었어요. 근데 한 번 익숙해지고 나면, 이게 없는 환경으로 돌아가기가 싫어지더라고요.

    오늘 다룬 내용을 정리하면:

    1. ✅ istioctl로 Istio 설치 및 네임스페이스 레이블 설정
    2. ✅ VirtualService + DestinationRule로 카나리 배포, A/B 테스트 구현
    3. ✅ PeerAuthentication STRICT으로 mTLS 강제 적용
    4. ✅ AuthorizationPolicy로 서비스 간 접근 제어
    5. ✅ Kiali + Prometheus로 서비스 메시 가시성 확보

    다음 글에서는 Istio를 활용한 멀티 클러스터 서비스 메시 구성과 Istio 업그레이드 전략을 다뤄볼 예정입니다. 그리고 이전 글에서 다뤘던 Kubernetes 네트워크 정책(NetworkPolicy)과 Istio를 함께 쓰는 방법도 참고하시면 좋아요.

    궁금한 점이나 막히는 부분 있으시면 댓글로 남겨주세요. 같이 삽질해봐요! 🎉

  • [k8s] 쿠버네티스 Pod 트러블슈팅: CrashLoopBackOff, OOMKilled 완벽 해결

    쿠버네티스 Pod가 계속 죽는다면? 당신만 그런 게 아닙니다

    쿠버네티스를 처음 운영하다 보면 꼭 한 번씩 마주치는 상황이 있어요. 열심히 작성한 배포 파일을 kubectl apply로 올렸는데, Pod 상태가 CrashLoopBackOff거나 OOMKilled로 떠 있는 겁니다. 처음엔 진짜 당황스럽죠. 로그를 봐도 뭔가 무한루프처럼 에러가 쌓이고, 뭘 고쳐야 할지 막막하고요.

    저도 입사 초반에 이거 때문에 밤새 삽질한 기억이 있습니다. 당시엔 kubectl describe pod 명령어조차 제대로 활용 못 했거든요. 지금은 쿠버네티스 Pod 트러블슈팅이 거의 반사적으로 손가락이 움직일 정도가 됐지만, 처음엔 정말 막막했어요. 이번 글에서는 제가 직접 겪고 해결한 경험을 바탕으로, CrashLoopBackOff와 OOMKilled 두 가지 대표적인 Pod 오류를 어떻게 진단하고 해결하는지 단계별로 풀어드릴게요.

    ▲ 쿠버네티스 Pod의 주요 상태 전환 흐름. Pending → Running → CrashLoopBackOff / OOMKilled 경로를 시각화한 다이어그램

    CrashLoopBackOff와 OOMKilled, 이게 대체 뭔가요?

    CrashLoopBackOff — “계속 죽었다 살아났다 반복”

    쉽게 말해서, 컨테이너가 시작됐다가 바로 죽고, 쿠버네티스가 다시 살리고, 또 죽고를 반복하는 상태예요. 쿠버네티스는 기본적으로 컨테이너가 죽으면 재시작(restart)을 시도하는데, 계속 실패하면 재시작 간격을 점점 늘리면서 BackOff 상태로 전환됩니다. 10초, 20초, 40초… 이런 식으로요.

    원인은 정말 다양합니다:

    • 애플리케이션 자체 버그 (예외 처리 안 된 panic, exit code 1)
    • 잘못된 환경 변수(Environment Variable) 설정
    • ConfigMap이나 Secret 마운트 실패
    • Liveness Probe(생존 확인 프로브) 설정 오류
    • 의존 서비스(DB, 외부 API 등)에 연결 못 하는 경우

    OOMKilled — “메모리를 너무 많이 먹어서 강제 종료”

    OOM은 Out Of Memory의 약자입니다. 컨테이너가 설정된 메모리 Limit(제한)을 초과하면, 리눅스 커널의 OOM Killer가 해당 프로세스를 강제로 죽여버려요. kubectl describe pod로 확인하면 OOMKilled라고 딱 찍혀 있고, exit code는 137이에요.

    이건 크게 두 가지 경우인데요:

    • 메모리 Limit이 너무 낮게 설정된 경우: 실제 앱이 필요한 메모리보다 Limit이 작아서 죽는 거예요
    • 메모리 누수(Memory Leak)가 있는 경우: Limit은 적절한데 앱이 메모리를 계속 잡아먹고 반환 안 하는 경우
    오류 유형 주요 원인 Exit Code 확인 명령어
    CrashLoopBackOff 앱 오류, 설정 문제, Probe 실패 1, 2 등 다양 kubectl logs, kubectl describe
    OOMKilled 메모리 Limit 초과, 메모리 누수 137 kubectl describe, metrics-server

    1단계: 상황 파악 — 일단 뭐가 문제인지 보자

    쿠버네티스 Pod 트러블슈팅의 첫 번째 단계는 항상 현재 상태 파악이에요. 저는 이 순서대로 확인합니다.

    Pod 상태 전체 확인

    # 네임스페이스 전체 Pod 상태 확인
    kubectl get pods -n <네임스페이스> -o wide
    
    # 모든 네임스페이스에서 문제 있는 Pod만 필터링
    kubectl get pods -A | grep -v Running | grep -v Completed

    여기서 RESTARTS 컬럼 숫자가 높으면 CrashLoopBackOff 의심이에요. 한 자리면 괜찮은데, 두세 자리 넘어가면 심각한 거거든요.

    Pod 상세 이벤트 확인 — 이게 핵심입니다

    kubectl describe pod  -n <네임스페이스>

    출력 맨 아래 Events: 섹션을 꼭 보세요. 여기에 실제로 무슨 일이 있었는지 타임라인이 찍혀 있어요. OOMKilled라면 이런 식으로 보입니다:

    Events:
      Type     Reason     Age                From               Message
      ----     ------     ----               ----               -------
      Warning  BackOff    2m (x5 over 5m)   kubelet            Back-off restarting failed container
      Normal   Pulled     6m                kubelet            Successfully pulled image
      Normal   Started    6m                kubelet            Started container app
      Warning  OOMKilling 5m                kubelet            Memory limit reached, killing container

    로그 확인 — 현재 로그와 이전 컨테이너 로그

    # 현재 컨테이너 로그
    kubectl logs  -n <네임스페이스>
    
    # 이전에 죽은 컨테이너 로그 (이게 더 중요할 때가 많아요!)
    kubectl logs  -n <네임스페이스> --previous
    
    # 실시간 로그 스트리밍
    kubectl logs -f  -n <네임스페이스>
    
    # 멀티 컨테이너 Pod인 경우 컨테이너 지정
    kubectl logs  -c  -n <네임스페이스> --previous

    💡 팁: --previous 플래그를 꼭 써보세요. 현재 로그에는 아무것도 없어도, 이전에 죽을 때의 로그가 여기 남아 있거든요. 저도 처음엔 이걸 몰라서 한참 헤맸습니다 ㅎㅎ

    ▲ kubectl describe pod 명령어 실행 결과. Events 섹션에서 OOMKilled 및 CrashLoopBackOff 원인을 확인하는 화면

    2단계: CrashLoopBackOff 원인별 해결법

    케이스 1: 애플리케이션 자체 오류

    로그에서 스택 트레이스(stack trace)나 panic, exception 같은 키워드가 보인다면 앱 코드 문제예요. 이건 개발팀에 공유해야 하는 케이스고, 인프라 엔지니어 입장에서 할 수 있는 건 명확한 로그를 전달하는 거예요.

    # 마지막 100줄 로그 확인
    kubectl logs  --previous --tail=100 -n <네임스페이스>

    케이스 2: Liveness Probe 설정 오류

    이게 은근히 많은 케이스더라고요. Liveness Probe(생존 확인 프로브)가 너무 엄격하게 설정되어 있으면, 앱이 정상인데도 쿠버네티스가 죽었다고 판단해서 재시작시켜 버립니다.

    # 잘못된 설정 예시 — 너무 빠른 초기 딜레이
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 3   # 앱 시작에 10초 걸리는데 3초만 기다림
      periodSeconds: 5
      failureThreshold: 1      # 1번만 실패해도 재시작
    
    ---
    
    # 올바른 설정 예시
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 30  # 앱 시작 시간보다 여유 있게
      periodSeconds: 10
      failureThreshold: 3      # 3번 연속 실패해야 재시작
      timeoutSeconds: 5        # 응답 타임아웃도 여유 있게

    ⚠️ 주의: initialDelaySeconds는 컨테이너 시작 후 처음 Probe를 시도하기 전 대기 시간이에요. 앱이 완전히 뜨는 데 걸리는 시간보다 넉넉하게 잡아야 합니다. 저는 보통 실제 시작 시간의 1.5~2배로 잡아요.

    케이스 3: ConfigMap / Secret 마운트 실패

    # ConfigMap 존재 여부 확인
    kubectl get configmap -n <네임스페이스>
    
    # Secret 존재 여부 확인
    kubectl get secret -n <네임스페이스>
    
    # describe에서 Volume 마운트 실패 메시지 확인
    kubectl describe pod  -n <네임스페이스> | grep -A5 "Volumes"

    describe 출력에서 이런 메시지가 보이면 ConfigMap이나 Secret이 없는 겁니다:

    Warning  FailedMount  10s   kubelet  MountVolume.SetUp failed for volume "config" :
    configmap "my-app-config" not found

    케이스 4: 환경 변수 누락

    # 실행 중인 Pod의 환경 변수 확인
    kubectl exec  -n <네임스페이스> -- env | sort
    
    # Pod가 죽어서 exec가 안 되는 경우 — 임시 디버그 Pod 실행
    kubectl run debug-pod --image=busybox -it --rm -- /bin/sh

    3단계: OOMKilled 해결법

    현재 메모리 사용량 확인

    먼저 실제로 얼마나 쓰고 있는지 봐야죠. metrics-server가 설치되어 있다면:

    # Pod 리소스 사용량 확인
    kubectl top pod  -n <네임스페이스>
    
    # 컨테이너별 상세 확인
    kubectl top pod  -n <네임스페이스> --containers

    현재 설정된 Limit 확인:

    kubectl get pod  -n <네임스페이스> -o jsonpath='{.spec.containers[*].resources}'

    메모리 Limit 조정

    실제 사용량이 Limit에 근접하거나 초과한다면 Limit을 올려야 해요. 이건 Deployment를 수정해야 합니다:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-app
    spec:
      template:
        spec:
          containers:
          - name: app
            image: my-app:latest
            resources:
              requests:
                memory: "256Mi"   # 스케줄링 기준 (최소 보장량)
                cpu: "250m"
              limits:
                memory: "512Mi"   # 최대 허용량 (이 이상 쓰면 OOMKilled)
                cpu: "500m"

    💡 Request와 Limit의 차이:

    • requests: 쿠버네티스가 Pod를 노드에 배치할 때 기준이 되는 최소 보장량이에요. 이 공간이 있는 노드에만 배치됩니다.
    • limits: 컨테이너가 절대 넘을 수 없는 상한선이에요. 메모리 Limit을 넘으면 OOMKilled, CPU Limit을 넘으면 쓰로틀링(throttling)이 발생해요.

    메모리 누수 의심 케이스

    Limit을 올렸는데도 계속 OOMKilled가 난다면 메모리 누수를 의심해야 해요. 이 경우엔 시간에 따른 메모리 증가 패턴을 봐야 합니다. Prometheus + Grafana 조합이 있다면 메모리 사용량 그래프를 확인하세요. 선형으로 계속 올라간다면 누수 가능성이 높습니다.

    # 메모리 사용량 변화를 5초 간격으로 모니터링
    watch -n 5 kubectl top pod  -n <네임스페이스>

    누수가 확인되면 개발팀에 공유하고, 임시 방편으로는 Deployment에 주기적 재시작을 설정하는 방법도 있어요 (권장하진 않지만요):

    # CronJob으로 주기적 재시작 — 근본 해결책은 아닙니다!
    apiVersion: batch/v1
    kind: CronJob
    metadata:
      name: restart-my-app
    spec:
      schedule: "0 4 * * *"  # 매일 새벽 4시
      jobTemplate:
        spec:
          template:
            spec:
              containers:
              - name: kubectl
                image: bitnami/kubectl
                command:
                - kubectl
                - rollout
                - restart
                - deployment/my-app
              restartPolicy: OnFailure

    ▲ Grafana 대시보드에서 Pod 메모리 사용량 추이를 모니터링하는 화면. 메모리 누수 시 선형 증가 패턴이 확인됨

    4단계: 고급 디버깅 — 그래도 모르겠을 때

    임시 디버그 컨테이너 (kubectl debug)

    쿠버네티스 1.23 이상이라면 kubectl debug를 활용할 수 있어요. 이미지에 쉘이 없거나, distroless 이미지를 쓰는 경우에도 쿠버네티스 Pod를 디버깅할 수 있습니다.

    # 실행 중인 Pod에 디버그 컨테이너 추가
    kubectl debug -it  -n <네임스페이스> \
      --image=busybox \
      --target=
    
    # Pod를 복사해서 디버그 버전으로 실행
    kubectl debug  -n <네임스페이스> \
      -it \
      --copy-to=debug-pod \
      --image=ubuntu \
      -- bash

    Pod가 계속 죽어서 exec가 안 될 때

    이건 진짜 난감한 상황인데요. 이럴 때 쓰는 트릭이 있어요. 컨테이너 커맨드를 sleep infinity로 덮어써서 앱이 실행되지 않은 상태에서 내부를 들여다보는 겁니다:

    # 임시로 커맨드를 sleep으로 덮어쓰기
    kubectl debug  -n <네임스페이스> \
      --copy-to=debug-pod \
      --image= \
      -- sleep infinity
    
    # 그 다음 exec로 들어가서 환경 확인
    kubectl exec -it debug-pod -n <네임스페이스> -- bash

    네트워크 연결 문제 확인

    DB나 외부 서비스 연결 실패로 CrashLoopBackOff가 나는 경우도 많아요:

    # 네임스페이스 내 Service 확인
    kubectl get svc -n <네임스페이스>
    
    # DNS 해석 확인 (임시 Pod 사용)
    kubectl run dns-test --image=busybox -it --rm -n <네임스페이스> \
      -- nslookup my-db-service
    
    # TCP 연결 확인
    kubectl run tcp-test --image=busybox -it --rm -n <네임스페이스> \
      -- nc -zv my-db-service 5432

    검증 — 제대로 고쳐졌는지 확인하기

    수정 후에는 꼭 아래 순서로 확인해요:

    1. Pod 상태가 Running으로 안정적으로 유지되는지 확인
    2. RESTARTS 카운트가 더 이상 올라가지 않는지 확인
    3. 로그에 정상 동작 메시지가 찍히는지 확인
    4. Readiness Probe(준비 확인 프로브)가 통과하는지 확인
    # 실시간으로 Pod 상태 변화 모니터링
    kubectl get pods -n <네임스페이스> -w
    
    # Rollout 상태 확인
    kubectl rollout status deployment/ -n <네임스페이스>
    
    # 최근 이벤트 확인
    kubectl get events -n <네임스페이스> --sort-by='.lastTimestamp' | tail -20

    🎉 kubectl get pods에서 STATUS가 Running이고 READY가 1/1이면서 RESTARTS가 안 올라가면 성공입니다!

    쿠버네티스 Pod 트러블슈팅 체크리스트 정리

    확인 항목 CrashLoopBackOff OOMKilled 명령어
    Pod 상태 확인 ✅ ✅ kubectl get pods
    이벤트 로그 확인 ✅ ✅ kubectl describe pod
    이전 컨테이너 로그 ✅ ✅ kubectl logs –previous
    Probe 설정 확인 ✅ – kubectl get pod -o yaml
    메모리 사용량 확인 – ✅ kubectl top pod
    리소스 Limit 조정 – ✅ kubectl edit deployment
    ConfigMap/Secret 확인 ✅ – kubectl get cm,secret
    네트워크 연결 확인 ✅ – kubectl run (임시 Pod)

    ▲ CrashLoopBackOff와 OOMKilled 트러블슈팅 플로우차트. 증상별 진단 경로와 해결 방법을 한눈에 정리한 인포그래픽

    마무리 — 결국 로그가 답이더라고요

    여러 해 동안 수많은 인프라 사고를 겪어오면서 느낀 건, 쿠버네티스 Pod 트러블슈팅의 핵심은 결국 로그와 이벤트를 제대로 읽는 것이에요. 화려한 도구가 없어도, kubectl describe와 kubectl logs --previous 두 개만 잘 써도 대부분의 문제는 해결됩니다.

    CrashLoopBackOff는 원인이 다양하니까 로그를 꼼꼼히 보고 하나씩 제거해 나가는 방식이 좋아요. OOMKilled는 일단 메모리 사용량 측정부터 시작해서 Limit 조정이냐, 누수 수정이냐를 판단하면 되고요.

    정리하면:

    • ✅ CrashLoopBackOff: kubectl logs --previous로 죽기 직전 로그 확인 → Probe 설정, 환경 변수, 마운트 순서로 체크
    • ✅ OOMKilled: kubectl top pod로 실제 사용량 확인 → Limit 상향 조정 또는 누수 수정
    • ✅ 막막할 땐 kubectl debug로 임시 컨테이너 붙여서 내부 확인

    다음 글에서는 Pending 상태에서 Pod가 뜨지 않는 경우 (노드 리소스 부족, Taints/Tolerations 문제 등)를 다뤄볼 예정입니다. Pod가 아예 시작조차 안 된다면 그쪽 글을 참고해 주세요!

    혹시 이 글에서 다루지 않은 케이스로 고생하고 계신 분 있으시면 댓글로 남겨주세요. 같이 고민해 볼게요 😊

  • [Proxmox] VE 8.2 네트워크 고급 설정: VLAN, 브릿지, nftables 방화벽 완벽 가이드

    홈랩 네트워크, 이대로 괜찮은가요?

    솔직히 말씀드리면, 저도 처음엔 Proxmox VE를 설치하고 그냥 기본 브릿지 하나에 VM 다 때려넣고 썼거든요. “뭐, 집에서 쓰는 건데 보안이 무슨 상관이야” 싶었죠. 근데 어느 날 홈랩에서 돌리던 개발용 컨테이너 하나가 이상한 트래픽을 뿜는 걸 발견했을 때… 그때부터 제대로 네트워크를 나눠야겠다는 생각이 들었습니다.

    Proxmox 네트워크 설정을 제대로 이해하면 VM/컨테이너별로 네트워크를 격리하고, 불필요한 트래픽을 차단하고, 관리 인터페이스를 보호할 수 있어요. 오늘은 Proxmox VE 8.2 기준으로 VLAN 설정, 브릿지 구성, 그리고 nftables 방화벽까지 한 번에 정리해 드릴게요. 꽤 긴 글이 될 것 같지만, 차근차근 따라오시면 충분히 이해하실 수 있을 겁니다.

    ▲ 오늘 구성할 Proxmox 네트워크 전체 구조입니다. VLAN별로 트래픽을 분리하고 nftables로 방화벽 규칙을 적용하는 흐름을 한눈에 볼 수 있어요.

    Proxmox 네트워크 핵심 개념 먼저 짚고 가기

    설정 들어가기 전에 개념을 잡아두는 게 중요해요. 저도 처음엔 브릿지랑 본드(Bond)랑 VLAN이 뭐가 다른지 헷갈려서 삽질을 꽤 했거든요.

    Linux Bridge(리눅스 브릿지)란?

    쉽게 말해, 소프트웨어로 만든 스위치입니다. Proxmox에서 VM이나 컨테이너를 만들면 각각의 가상 NIC(네트워크 인터페이스 카드)가 이 브릿지에 연결되는 구조예요. 물리 스위치의 포트처럼 동작한다고 보시면 됩니다.

    VLAN(Virtual LAN, 가상 로컬 네트워크)이란?

    하나의 물리 네트워크를 논리적으로 분리하는 기술이에요. 예를 들어 관리용 VM, 개발용 VM, IoT 장비를 같은 물리 스위치에 연결해도 VLAN ID로 완전히 다른 네트워크처럼 동작하게 만들 수 있습니다. Proxmox 8.2에서는 VLAN-aware bridge(VLAN 인식 브릿지) 방식을 권장하고 있어요.

    nftables란?

    기존 iptables를 대체하는 리눅스 방화벽 프레임워크입니다. Proxmox VE 7.x 이후부터 내부적으로 nftables를 사용하고 있고, 8.x에서는 더 완성도 있게 통합됐어요. 문법이 처음엔 낯설지만, 익숙해지면 iptables보다 훨씬 직관적이더라고요.

    구성 요소 역할 비고
    Linux Bridge VM/CT 간 L2 연결 (소프트웨어 스위치) vmbr0, vmbr1 등으로 명명
    VLAN 논리적 네트워크 분리 (태그 기반) 802.1Q 표준, ID 1~4094
    Bond 물리 NIC 이중화 / 대역폭 확장 active-backup, LACP 등
    nftables 호스트 및 VM 트래픽 방화벽 iptables 대체, Proxmox 통합

    Proxmox VLAN 설정: VLAN-aware Bridge 구성하기

    이제 본격적으로 설정을 시작해 볼게요. 제가 실제로 운영 중인 홈랩 구성을 기반으로 설명드리겠습니다.

    시나리오 설명

    • VLAN 10: 관리 네트워크 (Proxmox 관리 인터페이스, 네트워크 장비)
    • VLAN 20: 서버 네트워크 (웹서버, DB 등 서비스 VM)
    • VLAN 30: 개발/테스트 네트워크 (개발 VM, 컨테이너)
    • VLAN 99: IoT/비신뢰 네트워크 (외부 접근 허용 최소화)

    1단계: /etc/network/interfaces 설정

    Proxmox의 네트워크 설정은 /etc/network/interfaces 파일로 관리됩니다. GUI에서도 설정할 수 있지만, 파일을 직접 편집하는 게 더 정확하고 빠르더라고요.

    # 기존 설정 백업 먼저!
    cp /etc/network/interfaces /etc/network/interfaces.bak
    
    # 편집
    nano /etc/network/interfaces

    아래 내용으로 설정합니다. 물리 NIC 이름은 환경마다 다를 수 있으니 ip link show로 먼저 확인하세요.

    auto lo
    iface lo inet loopback
    
    # 물리 NIC (업스트림 스위치와 연결되는 트렁크 포트)
    auto enp3s0
    iface enp3s0 inet manual
    
    # VLAN-aware Bridge (VLAN 인식 브릿지)
    auto vmbr0
    iface vmbr0 inet static
        address 192.168.10.100/24      # VLAN 10 (관리망) IP
        gateway 192.168.10.1
        bridge-ports enp3s0
        bridge-stp off
        bridge-fd 0
        bridge-vlan-aware yes          # 핵심! VLAN 인식 활성화
        bridge-vids 2-4094             # 허용할 VLAN ID 범위
    
    # VLAN 인터페이스 (호스트가 각 VLAN에 직접 접근할 때 필요)
    auto vmbr0.20
    iface vmbr0.20 inet static
        address 192.168.20.1/24
    
    auto vmbr0.30
    iface vmbr0.30 inet static
        address 192.168.30.1/24
    
    auto vmbr0.99
    iface vmbr0.99 inet static
        address 192.168.99.1/24

    ⚠️ 중요! bridge-vlan-aware yes를 설정하면 기존에 VLAN 없이 연결된 VM들이 통신이 끊길 수 있어요. 반드시 각 VM의 네트워크 설정에서 VLAN 태그를 지정해 줘야 합니다.

    2단계: 설정 적용

    # 네트워크 재시작 (원격 접속 중이라면 주의!)
    ifreload -a
    
    # 또는 재부팅
    reboot

    💡 팁: 원격으로 작업 중이라면 ifreload -a가 더 안전합니다. 그래도 혹시 모르니 콘솔 접근이 가능한 상태에서 작업하는 걸 강력 추천드려요. 저도 한 번 원격이 끊겨서 IDC까지 달려간 적 있거든요.

    3단계: VM에 VLAN 태그 설정

    VM 설정에서 네트워크 장치를 추가할 때 VLAN Tag를 지정합니다.

    # CLI로 VM 네트워크 설정 변경 (VM ID 101, VLAN 20 할당)
    qm set 101 --net0 virtio,bridge=vmbr0,tag=20
    
    # 컨테이너(LXC)의 경우
    pct set 201 --net0 name=eth0,bridge=vmbr0,tag=30,ip=192.168.30.10/24,gw=192.168.30.1

    ▲ Proxmox GUI에서 VM 네트워크 설정 시 VLAN Tag 항목에 VLAN ID를 입력하면 됩니다. bridge-vlan-aware 설정이 되어 있어야 이 옵션이 동작해요.

    Proxmox 브릿지 고급 설정: 다중 브릿지 구성

    VLAN-aware 브릿지 하나로 대부분 해결되지만, 경우에 따라 브릿지를 여러 개 나누는 게 더 나을 때도 있어요. 예를 들어 완전히 격리된 내부 네트워크(외부 연결 없는 NAT 망)가 필요할 때죠.

    NAT 브릿지 추가 (인터넷 연결 없는 격리망)

    # /etc/network/interfaces에 추가
    auto vmbr1
    iface vmbr1 inet static
        address 10.10.10.1/24
        bridge-ports none              # 물리 NIC 없음 = 완전 격리
        bridge-stp off
        bridge-fd 0
        post-up echo 1 > /proc/sys/net/ipv4/ip_forward
        post-up iptables -t nat -A POSTROUTING -s '10.10.10.0/24' -o vmbr0 -j MASQUERADE
        post-down iptables -t nat -D POSTROUTING -s '10.10.10.0/24' -o vmbr0 -j MASQUERADE

    이렇게 하면 vmbr1에 연결된 VM들은 인터넷은 되지만(NAT를 통해), 외부에서 직접 접근은 불가능한 구조가 됩니다. 개발/테스트 환경에 딱이에요.

    nftables 방화벽 설정: Proxmox 방화벽 제대로 활용하기

    자, 이제 핵심 중의 핵심입니다. Proxmox 방화벽 설정인데요, 사실 Proxmox에는 GUI 기반의 자체 방화벽 기능이 있고, 이게 내부적으로 nftables를 사용해요. 두 가지 방법을 모두 알아두는 게 좋습니다.

    방법 1: Proxmox 내장 방화벽 (GUI/설정 파일)

    Proxmox 방화벽은 세 가지 레벨로 동작합니다.

    1. Datacenter 레벨: 클러스터 전체에 적용되는 공통 규칙 (/etc/pve/firewall/cluster.fw)
    2. Host 레벨: Proxmox 호스트 자체에 적용 (/etc/pve/nodes/[노드명]/host.fw)
    3. VM/CT 레벨: 개별 가상머신/컨테이너에 적용 (/etc/pve/firewall/[VMID].fw)

    Proxmox 방화벽 활성화

    # 방화벽 설정 파일 위치 확인
    ls /etc/pve/firewall/
    
    # 클러스터 방화벽 설정
    cat /etc/pve/firewall/cluster.fw

    GUI에서는 Datacenter → Firewall → Options에서 Enable을 체크하면 됩니다. 호스트별로도 같은 방법으로 활성화할 수 있어요.

    방화벽 규칙 파일 직접 편집

    # /etc/pve/firewall/cluster.fw 예시
    [OPTIONS]
    enable: 1
    policy_in: DROP      # 기본 인바운드 정책: 차단
    policy_out: ACCEPT   # 기본 아웃바운드 정책: 허용
    
    [RULES]
    # Proxmox 웹 GUI 접근 (관리망에서만)
    IN ACCEPT -source 192.168.10.0/24 -p tcp --dport 8006 -log warning
    # SSH 접근 (관리망에서만)
    IN ACCEPT -source 192.168.10.0/24 -p tcp --dport 22 -log warning
    # ICMP (ping) 허용
    IN ACCEPT -p icmp
    # 이미 맺어진 연결 허용
    IN ACCEPT -m conntrack --ctstate RELATED,ESTABLISHED

    방법 2: nftables 직접 설정 (고급 사용자용)

    Proxmox 내장 방화벽으로 커버 안 되는 복잡한 규칙이 필요할 때는 nftables를 직접 건드려야 합니다. 근데 여기서 주의할 점! Proxmox가 자체 방화벽을 관리하기 때문에, 직접 nftables 규칙을 추가하면 충돌이 날 수 있어요.

    그래서 권장하는 방법은 custom nftables 설정 파일을 별도로 만드는 겁니다.

    # /etc/nftables.conf 에 커스텀 규칙 추가
    nano /etc/nftables.conf
    #!/usr/sbin/nft -f
    
    # 기존 규칙 초기화
    flush ruleset
    
    table inet filter {
        # 체인(chain): 패킷 처리 흐름 정의
        chain input {
            type filter hook input priority 0; policy drop;
    
            # 루프백 인터페이스 허용
            iif lo accept
    
            # 이미 맺어진 연결 및 관련 트래픽 허용
            ct state established,related accept
    
            # ICMP 허용 (ping 등)
            ip protocol icmp accept
            ip6 nexthdr icmpv6 accept
    
            # 관리망(VLAN 10)에서 SSH 허용
            iifname "vmbr0.10" tcp dport 22 accept
    
            # 관리망에서 Proxmox 웹 GUI 허용
            iifname "vmbr0.10" tcp dport 8006 accept
    
            # 나머지는 로그 남기고 차단
            log prefix "[nft-drop] " limit rate 5/minute
            drop
        }
    
        chain forward {
            type filter hook forward priority 0; policy drop;
    
            # 이미 맺어진 연결 허용
            ct state established,related accept
    
            # VLAN 20 (서버망) → 인터넷 허용
            iifname "vmbr0.20" oifname "vmbr0" accept
    
            # VLAN 30 (개발망) → 인터넷 허용
            iifname "vmbr0.30" oifname "vmbr0" accept
    
            # VLAN 간 통신 차단 (보안 격리)
            # 개발망 → 서버망 차단
            iifname "vmbr0.30" oifname "vmbr0.20" drop
    
            # IoT망 → 다른 VLAN 차단
            iifname "vmbr0.99" drop
        }
    
        chain output {
            type filter hook output priority 0; policy accept;
        }
    }
    
    # NAT 테이블 (vmbr1 사용 시)
    table ip nat {
        chain postrouting {
            type nat hook postrouting priority 100;
            oifname "vmbr0" masquerade
        }
    }
    # nftables 서비스 활성화 및 시작
    systemctl enable nftables
    systemctl start nftables
    
    # 규칙 적용 확인
    nft list ruleset
    
    # 특정 테이블만 확인
    nft list table inet filter

    ⚠️ 경고: Proxmox 내장 방화벽과 nftables를 동시에 사용하면 규칙이 충돌할 수 있습니다. 둘 중 하나만 선택하거나, Proxmox 방화벽을 비활성화한 후 nftables를 직접 관리하는 방식을 권장합니다. 저는 단순한 환경에서는 Proxmox 내장 방화벽만 쓰고, 복잡한 규칙이 필요할 때는 nftables 직접 설정을 사용하고 있어요.

    ▲ nftables 방화벽 규칙을 적용했을 때 VLAN 간 트래픽 허용/차단 흐름입니다. IoT망(VLAN 99)은 다른 VLAN으로의 접근이 완전히 차단됩니다.

    ⚠️ 트러블슈팅: 실제로 겪은 문제들

    설정하다 보면 분명히 막히는 부분이 생기거든요. 제가 겪은 것들 공유합니다.

    문제 1: VLAN 설정 후 VM 네트워크 먹통

    증상: bridge-vlan-aware 활성화 후 기존 VM들이 네트워크가 안 됨

    원인: VLAN-aware 브릿지에서는 VLAN 태그가 없는 트래픽이 기본적으로 차단됩니다.

    해결: 각 VM 네트워크 설정에 VLAN 태그를 추가하거나, 태그 없이 쓰고 싶다면 bridge-pvid(포트 VLAN ID)를 설정하세요.

    # 특정 브릿지 포트의 PVID 확인
    bridge vlan show
    
    # VM 포트에 PVID 설정 (태그 없는 트래픽을 VLAN 20으로)
    bridge vlan add dev vmbr0 vid 20 pvid untagged

    문제 2: nftables 규칙 적용 후 Proxmox GUI 접근 불가

    증상: nftables 설정 후 8006 포트로 접근이 안 됨

    원인: input 체인의 기본 정책이 drop인데, 8006 포트 허용 규칙이 잘못된 인터페이스에 적용됨

    해결: 인터페이스 이름을 ip link show로 정확히 확인하고, 임시로 모든 8006 트래픽을 허용한 뒤 규칙을 다듬으세요.

    # 임시로 8006 전체 허용 (디버깅용)
    nft add rule inet filter input tcp dport 8006 accept
    
    # 현재 적용된 규칙 확인
    nft list chain inet filter input
    
    # 인터페이스 이름 확인
    ip link show | grep -E "^[0-9]+:"
    bridge vlan show

    문제 3: VM 간 통신이 방화벽 규칙과 무관하게 됨

    증상: forward 체인에서 차단했는데 VM끼리 통신이 됨

    원인: 같은 브릿지 내 VM 간 트래픽은 L2(2계층)에서 바로 처리되어 forward 체인을 거치지 않을 수 있음

    해결: 브릿지에서 nf_call_iptables(또는 nftables)를 활성화해야 합니다.

    # 브릿지 트래픽이 netfilter를 통과하도록 설정
    echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables
    echo 1 > /proc/sys/net/bridge/bridge-nf-call-ip6tables
    
    # 영구 적용 (/etc/sysctl.conf)
    echo "net.bridge.bridge-nf-call-iptables = 1" >> /etc/sysctl.conf
    echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.conf
    sysctl -p

    ✅ 설정 검증: 제대로 됐는지 확인하기

    설정을 다 했으면 검증이 필수입니다. “됐겠지”하고 넘어갔다가 나중에 보안 구멍 발견하면 더 힘들거든요.

    VLAN 설정 검증

    # 브릿지 VLAN 상태 확인
    bridge vlan show
    
    # 예상 출력:
    # port    vlan ids
    # vmbr0    1 PVID Egress Untagged
    #          10
    #          20
    #          30
    #          99
    
    # 각 VLAN 인터페이스 IP 확인
    ip addr show vmbr0.20
    ip addr show vmbr0.30
    
    # VLAN 10 (관리망) VM에서 VLAN 20 (서버망) VM으로 ping 테스트
    # 허용된 경로라면 응답이 와야 함
    ping -c 4 192.168.20.10

    방화벽 규칙 검증

    # 현재 nftables 규칙 전체 출력
    nft list ruleset
    
    # 패킷 카운터 확인 (규칙이 실제로 매칭되는지)
    nft list ruleset | grep -A2 "counter"
    
    # 로그 확인 (차단된 패킷)
    journalctl -f | grep "nft-drop"
    
    # 특정 포트 접근 테스트
    nc -zv 192.168.10.100 8006   # 성공해야 함
    nc -zv 192.168.10.100 22     # 성공해야 함
    nc -zv 192.168.10.100 80     # 차단되어야 함 (규칙 없으면)

    VLAN 간 격리 검증

    # IoT망 VM(192.168.99.x)에서 서버망 VM(192.168.20.x)으로 ping
    # 차단 규칙이 있다면 응답 없어야 함
    ping -c 4 192.168.20.10  # 타임아웃이 나야 정상!
    
    # 개발망 VM에서 서버망으로 접근 시도
    ping -c 4 192.168.20.10  # 마찬가지로 차단되어야 함

    🎉 모든 테스트가 예상대로 동작한다면 설정 완료입니다! 처음 이 구성을 완성했을 때 진짜 뿌듯했거든요. 네트워크가 깔끔하게 정리되는 느낌이랄까요.

    ▲ 설정 완료 후 각 VLAN 간 통신 허용/차단 검증 결과입니다. 초록색은 허용, 빨간색은 차단을 의미하며 설계한 대로 정확히 동작하는 것을 확인할 수 있습니다.

    자주 묻는 질문 (FAQ)

    Q. Proxmox 방화벽과 nftables를 동시에 써도 되나요?

    권장하지 않습니다. Proxmox 내장 방화벽이 활성화되면 자체적으로 nftables 규칙을 생성하는데, 여기에 커스텀 nftables 설정을 추가하면 규칙 순서와 충돌 문제가 생길 수 있어요. 간단한 환경이라면 Proxmox 내장 방화벽만, 복잡한 규칙이 필요하다면 내장 방화벽을 끄고 nftables를 직접 관리하는 것을 추천합니다.

    Q. VLAN 설정 시 업스트림 스위치도 설정해야 하나요?

    네, 반드시 필요합니다. Proxmox와 연결된 스위치 포트는 Trunk 포트로 설정하고, 사용할 VLAN ID들을 허용해야 합니다. 관리형 스위치(Managed Switch)가 없다면 VLAN 기능을 사용하기 어렵습니다.

    Q. VM이 많아지면 방화벽 규칙 관리가 힘들지 않나요?

    맞아요, 그래서 IP set(IP 집합)과 Security Group(보안 그룹) 기능을 활용하는 게 좋습니다. Proxmox 내장 방화벽에서 IP set으로 그룹을 만들어두면 규칙 관리가 훨씬 수월해집니다.

    마무리: 네트워크 분리, 귀찮아도 꼭 해야 합니다

    처음에 이야기했던 것처럼, 저도 처음엔 “홈랩인데 뭘 그렇게까지” 싶었어요. 근데 막상 제대로 구성해 놓고 나니까, 어떤 VM에 문제가 생겨도 다른 VM이나 호스트에 영향이 없으니까 마음이 편하더라고요. 특히 외부에서 접근 가능한 서비스를 운영할 때는 정말 필수입니다.

    오늘 다룬 내용을 정리하면:

    • ✅ VLAN-aware Bridge로 논리적 네트워크 분리
    • ✅ 다중 브릿지로 완전 격리 NAT 망 구성
    • ✅ Proxmox 내장 방화벽 또는 nftables 직접 설정으로 트래픽 제어
    • ✅ VLAN 간 격리 검증으로 설정 완료 확인

    다음에는 Proxmox 클러스터 구성과 고가용성(HA, High Availability) 설정에 대해 다룰 예정이에요. 네트워크 기반이 잘 잡혀 있어야 클러스터도 안정적으로 운영할 수 있거든요. 궁금한 점이나 다른 경험 있으신 분은 댓글로 공유해 주세요!

    💡 다음 단계 제안: 이 설정을 기반으로 SDN(Software Defined Networking) 기능도 탐구해 보세요. Proxmox 8.x에서 SDN 기능이 많이 발전했는데, VXLAN 오버레이 네트워크 같은 더 고급 구성도 가능합니다. 이전 글에서 다룬 Proxmox 기본 설치 가이드와 함께 보시면 더 도움이 될 거예요.

  • [Proxmox] Proxmox VE 설치 후 필수 초기 설정: 8단계 완벽 가이드

    Proxmox VE 설치, 그 다음이 진짜 시작입니다

    Proxmox VE를 처음 설치하고 나서 웹 UI에 접속했을 때 느낌, 기억하시나요? 저는 처음에 “오, 뭔가 있어 보이는데?” 하면서 신나게 로그인했다가… 뭘 해야 할지 몰라서 한참 멍하니 화면만 바라봤었거든요. ㅎㅎ

    사실 Proxmox VE 설치 자체는 그렇게 어렵지 않아요. ISO 받아서 부팅하고, 몇 가지 옵션 선택하면 끝이니까요. 근데 설치 직후 초기 설정을 제대로 안 하면 나중에 진짜 고생합니다. 보안 구멍이 생기거나, 업데이트가 안 되거나, 스토리지 설정이 엉켜서 VM 하나 만들 때마다 삽질하게 되거든요.

    13년 동안 인프라 엔지니어로 일하면서, 그리고 집에서 홈랩을 운영하면서 겪은 경험을 바탕으로 Proxmox VE 설치 후 반드시 해야 할 필수 초기 설정을 정리해봤습니다. 초보자분들도 따라할 수 있도록 최대한 쉽게 설명할게요.

    ▲ Proxmox VE 설치 후 초기 설정 전체 흐름 — 순서대로 따라가면 됩니다


    Proxmox VE가 뭔지 잠깐만요 — 처음 들어보시는 분을 위해

    혹시 Proxmox VE가 처음이신 분도 계실 것 같아서 간단히 설명하고 넘어갈게요.

    Proxmox VE(Virtual Environment)는 오픈소스 기반의 하이퍼바이저(Hypervisor, 가상화 플랫폼)입니다. 쉽게 말해, 컴퓨터 한 대 위에서 여러 개의 가상 컴퓨터(VM)를 돌릴 수 있게 해주는 소프트웨어예요. VMware ESXi와 비슷한 역할을 하는데, 무료에 웹 UI가 꽤 잘 만들어져 있어서 홈랩이나 소규모 인프라에서 많이 씁니다.

    • KVM(Kernel-based Virtual Machine): 완전 가상화 VM을 돌릴 때 사용
    • LXC(Linux Containers): 컨테이너 방식으로 더 가볍게 리눅스 환경 구성
    • Ceph, ZFS: 스토리지 클러스터링 지원
    • 웹 기반 관리 UI: 브라우저로 모든 걸 관리

    자, 이제 본론으로 들어가겠습니다. Proxmox VE 설치는 됐다고 가정하고, 필수 초기 설정을 시작할게요!


    1단계: 무료 리포지토리 설정 — 업데이트 받기

    여기서 많은 분들이 처음에 당황하시는 부분이에요. Proxmox VE를 설치하면 기본적으로 엔터프라이즈 리포지토리(Enterprise Repository)로 설정이 되어 있거든요. 근데 이건 유료 구독을 해야 쓸 수 있어서, 구독 없이 업데이트 시도하면 인증 오류가 떠버립니다.

    홈랩이나 개인 서버에서 쓰신다면 무료 버전인 no-subscription 리포지토리로 바꿔주셔야 해요. 프로덕션 환경에서는 유료 구독을 권장하지만, 개인 용도라면 충분합니다.

    SSH로 Proxmox 서버에 접속하거나, 웹 UI의 Shell을 열어서 아래 명령어를 실행하세요.

    엔터프라이즈 리포지토리 비활성화

    # 엔터프라이즈 리포지토리 파일 비활성화
    echo "# disabled enterprise repo" > /etc/apt/sources.list.d/pve-enterprise.list
    
    # Ceph 엔터프라이즈 리포지토리도 비활성화 (있는 경우)
    echo "# disabled" > /etc/apt/sources.list.d/ceph.list

    무료 리포지토리 추가

    # Proxmox VE no-subscription 리포지토리 추가
    echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" > /etc/apt/sources.list.d/pve-no-subscription.list
    
    # 패키지 목록 업데이트 및 시스템 업그레이드
    apt update && apt dist-upgrade -y

    💡 Tip: Proxmox VE 8.x 버전은 Debian 12(Bookworm) 기반입니다. 버전에 따라 코드명이 다르니, 설치된 버전을 먼저 확인하세요. pveversion 명령어로 확인할 수 있어요.

    업데이트가 다 끝나면 재부팅 한 번 해주는 게 좋아요. 커널 업데이트가 포함될 수 있거든요.

    reboot

    2단계: 로그인 화면 구독 알림 제거 (선택사항)

    이건 기능에는 전혀 영향이 없는데, 웹 UI 로그인할 때마다 “유효한 구독이 없습니다”라는 팝업이 뜨거든요. 매번 확인 누르는 게 은근히 귀찮아서 저도 처음 설정할 때 바로 없애버렸습니다.

    # Proxmox VE 8.x 기준
    sed -i.bak "s/data.status.toLowerCase() !== 'active'/false/g" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
    
    # 웹 서비스 재시작
    systemctl restart pveproxy

    ⚠️ 주의: 이 수정은 Proxmox 업데이트 후에 원복될 수 있어요. 업데이트 후에 다시 팝업이 뜨면 같은 명령어를 다시 실행하면 됩니다.


    3단계: 네트워크 브릿지 확인 및 설정

    가상화 서버 구축에서 네트워크 설정은 정말 중요한 부분이에요. 처음에 이 부분에서 삽질을 꽤 많이 했습니다. VM들이 외부 네트워크와 통신하려면 리눅스 브릿지(Linux Bridge)가 제대로 설정되어 있어야 하거든요.

    Proxmox는 설치 시 기본적으로 vmbr0라는 브릿지를 만들어줘요. 웹 UI에서 확인하는 방법은 이렇습니다:

    1. 웹 UI 좌측 트리에서 서버 노드 선택
    2. System(시스템) → Network(네트워크) 클릭
    3. vmbr0 브릿지와 IP 설정 확인

    설정 파일을 직접 확인하고 싶다면:

    cat /etc/network/interfaces

    정상적인 설정이라면 이런 형태가 나와야 해요:

    auto lo
    iface lo inet loopback
    
    auto eno1
    iface eno1 inet manual
    
    auto vmbr0
    iface vmbr0 inet static
            address 192.168.1.100/24
            gateway 192.168.1.1
            bridge-ports eno1
            bridge-stp off
            bridge-fd 0

    여기서 bridge-ports에 실제 물리 NIC 이름이 잘 들어가 있는지 확인하세요. 서버마다 NIC 이름이 다를 수 있어요 (eth0, eno1, enp3s0 등).

    ▲ Proxmox VE 웹 UI의 네트워크 설정 화면 — vmbr0 브릿지 구성을 확인하세요


    4단계: 스토리지 설정 최적화

    기본 설치 후 스토리지 설정이 좀 아쉬운 부분이 있어요. 특히 local-lvm 파티션 관련해서 처음에 헷갈리는 분들이 많더라고요.

    기본 스토리지 구성 이해하기

    스토리지 이름 타입 용도 저장 위치
    local Directory ISO 이미지, 백업, CT 템플릿 /var/lib/vz
    local-lvm LVM-Thin VM 디스크, CT 볼륨 LVM Thin Pool

    근데 여기서 local 스토리지에 VM 디스크 이미지를 저장하고 싶다면 추가 설정이 필요해요. 기본적으로 local은 VM 디스크(qcow2, raw)를 저장할 수 없도록 제한되어 있거든요.

    웹 UI에서 변경하는 방법:

    1. Datacenter(데이터센터) → Storage(스토리지) 클릭
    2. local 선택 후 Edit(편집)
    3. Content(콘텐츠)에서 Disk image(디스크 이미지) 항목 체크
    4. OK 클릭

    또는 CLI로 직접:

    # local 스토리지에 VM 디스크 이미지 저장 허용
    pvesm set local --content iso,vztmpl,backup,images,rootdir

    ZFS를 사용하는 경우 — 추가 최적화

    설치 시 ZFS를 선택하셨다면, ARC(Adaptive Replacement Cache, ZFS 캐시) 메모리 설정을 해주는 게 좋아요. 기본값으로 두면 ZFS가 RAM을 꽤 많이 잡아먹거든요.

    # ZFS ARC 최대 메모리 제한 (예: 4GB로 제한)
    echo "options zfs zfs_arc_max=4294967296" >> /etc/modprobe.d/zfs.conf
    update-initramfs -u

    5단계: 보안 강화 — 이거 꼭 해주세요 ⚠️

    이 부분이 진짜 중요한데 의외로 그냥 넘어가는 분들이 많아요. 특히 외부에서 접근 가능한 환경이라면 더더욱 신경 써야 합니다.

    SSH 보안 설정

    # SSH 설정 파일 편집
    nano /etc/ssh/sshd_config

    아래 항목들을 확인하고 설정하세요:

    # root 직접 로그인 비활성화 (키 인증만 허용하거나 완전 비활성화)
    PermitRootLogin prohibit-password
    
    # 비밀번호 인증 비활성화 (SSH 키 설정 후 적용)
    # PasswordAuthentication no
    
    # SSH 포트 변경 (기본 22에서 변경 권장)
    Port 2222
    # SSH 서비스 재시작
    systemctl restart sshd

    ⚠️ 경고: SSH 포트를 바꾸거나 비밀번호 인증을 끄기 전에, 반드시 SSH 키 로그인이 정상 동작하는지 먼저 확인하세요. 잘못하면 서버에 접속을 못하게 될 수 있어요. 저도 한 번 이렇게 자폭한 적이 있습니다… ㅎㅎ

    Proxmox 웹 UI 포트 방화벽 설정

    Proxmox는 기본적으로 8006 포트로 웹 UI에 접근합니다. Proxmox 내장 방화벽을 활성화하고 필요한 포트만 열어두세요.

    웹 UI에서 설정하는 방법:

    1. Datacenter(데이터센터) → Firewall(방화벽) → Options(옵션)
    2. Firewall: Yes로 변경
    3. 노드 레벨 방화벽도 동일하게 활성화
    4. 필요한 포트(8006, SSH 포트, 마이그레이션 포트 등) 규칙 추가

    강력한 비밀번호 설정 확인

    # root 비밀번호 변경
    passwd root

    웹 UI에서 별도 사용자를 만들어 root 직접 사용을 줄이는 것도 좋은 방법이에요. Datacenter → Permissions → Users에서 관리자 계정을 별도로 만들 수 있습니다.


    6단계: 시간 동기화 설정 확인

    이거 별거 아닌 것 같아도 나중에 로그 분석하거나 클러스터 구성할 때 시간이 맞지 않으면 진짜 골치 아파요. 꼭 확인하세요.

    # 현재 시간 동기화 상태 확인
    timedatectl status
    
    # systemd-timesyncd 서비스 상태 확인
    systemctl status systemd-timesyncd

    타임존 설정이 잘못되어 있다면:

    # 타임존을 서울로 설정
    timedatectl set-timezone Asia/Seoul
    
    # 동기화 확인
    timedatectl show-timesync --all

    NTP 서버를 커스텀으로 지정하고 싶다면 /etc/systemd/timesyncd.conf를 편집하세요:

    [Time]
    NTP=time.bora.net 0.pool.ntp.org 1.pool.ntp.org
    FallbackNTP=2.pool.ntp.org
    systemctl restart systemd-timesyncd

    7단계: 백업 정책 설정 — 미래의 나를 위해

    “백업은 나중에 해야지” 하다가 VM 날려먹은 경험… 저만 있는 건 아니겠죠? ㅎㅎ Proxmox는 기본적으로 꽤 쓸 만한 백업 기능을 내장하고 있어요. 처음부터 스케줄 잡아두는 걸 강력 추천합니다.

    백업 스케줄 설정 (웹 UI)

    1. Datacenter(데이터센터) → Backup(백업) 클릭
    2. Add(추가) 버튼 클릭
    3. 스케줄, 저장소, 대상 VM, 보존 정책 설정
    4. Create(생성) 클릭

    주요 설정 항목:

    • Storage(저장소): 백업 파일을 저장할 위치
    • Schedule(스케줄): cron 형식으로 지정 (예: 매일 새벽 2시 → 02:00)
    • Mode(모드): Snapshot(스냅샷), Suspend(일시정지), Stop(중지) 중 선택
    • Max Backups(최대 백업 수): 보존할 백업 개수 (스토리지 용량 고려)

    💡 Tip: VM이 중요하다면 Snapshot 모드를 권장해요. 서비스 중단 없이 백업이 가능하거든요. 단, 게스트 OS에 QEMU 에이전트가 설치되어 있어야 완전한 일관성이 보장됩니다.


    8단계: QEMU Guest Agent 설정

    VM을 만들 때 꼭 챙겨야 하는 설정인데, 기본 가이드에서 빠져있는 경우가 많아요. QEMU Guest Agent(게스트 에이전트)는 호스트(Proxmox)와 게스트(VM) 사이의 통신 채널을 제공해줘요.

    이게 있으면:

    • VM의 IP 주소를 Proxmox UI에서 바로 확인 가능
    • 정상적인 셧다운/재시작 명령 전달
    • 백업 시 파일시스템 동기화(freeze/thaw) 지원
    • 메모리 사용량 등 상세 정보 수집

    VM 설정에서 Guest Agent 활성화

    1. VM 선택 → Options(옵션)
    2. QEMU Guest Agent 항목 더블클릭
    3. Enabled(활성화) 체크 → OK

    게스트 OS에 에이전트 설치

    # Ubuntu/Debian 게스트에서
    apt install qemu-guest-agent -y
    systemctl enable qemu-guest-agent
    systemctl start qemu-guest-agent
    # CentOS/RHEL/Rocky Linux 게스트에서
    dnf install qemu-guest-agent -y
    systemctl enable qemu-guest-agent
    systemctl start qemu-guest-agent

    ▲ QEMU Guest Agent가 정상 동작하면 VM의 IP 주소와 상세 정보가 대시보드에 표시됩니다


    ⚠️ 자주 겪는 문제와 해결법

    문제 1: 웹 UI 접속이 안 돼요

    설치 직후 https://서버IP:8006으로 접속이 안 된다면, 브라우저에서 HTTP가 아닌 HTTPS로 접속하고 있는지 확인하세요. Proxmox는 기본적으로 자체 서명 인증서(Self-signed certificate)를 사용하기 때문에 브라우저에서 보안 경고가 뜨는 건 정상이에요. “고급” 클릭 후 “계속 진행”하면 됩니다.

    # pveproxy 서비스 상태 확인
    systemctl status pveproxy
    
    # 서비스 재시작
    systemctl restart pveproxy

    문제 2: apt update 시 인증 오류

    “401 Unauthorized” 오류가 나온다면 엔터프라이즈 리포지토리가 아직 활성화되어 있는 거예요. 1단계로 돌아가서 리포지토리 설정을 다시 확인하세요.

    문제 3: VM 생성 시 스토리지 선택이 안 됨

    VM 디스크를 저장할 스토리지가 보이지 않는다면, 해당 스토리지의 Content(콘텐츠) 설정에 Disk image가 포함되어 있는지 확인하세요. 4단계 스토리지 설정 부분을 다시 참고해주세요.

    문제 4: 네트워크 설정 변경 후 연결이 끊김

    이건 진짜 당황스럽죠… 웹 UI에서 네트워크 설정 변경 시 Apply Configuration(설정 적용) 버튼을 누르기 전에 설정을 꼭 다시 검토하세요. 잘못된 IP나 게이트웨이를 입력하면 연결이 끊길 수 있어요. 이럴 때를 대비해 물리적 접근(KVM 콘솔, IPMI 등)이나 직접 모니터+키보드 연결 방법을 미리 확보해두는 게 좋습니다.


    설정 완료 후 확인 체크리스트 ✅

    모든 설정을 마쳤다면 아래 체크리스트로 한 번 더 확인해보세요.

    ▲ Proxmox VE 초기 설정 완료 체크리스트 — 하나씩 확인해보세요

    • ✅ no-subscription 리포지토리로 변경 완료
    • ✅ apt update && apt dist-upgrade 실행 완료
    • ✅ 네트워크 브릿지(vmbr0) 정상 동작 확인
    • ✅ 스토리지 설정 및 콘텐츠 타입 확인
    • ✅ SSH 보안 설정 완료
    • ✅ 방화벽 기본 규칙 설정
    • ✅ 시간 동기화(NTP) 정상 동작 확인
    • ✅ 백업 스케줄 설정 완료
    • ✅ 테스트 VM 생성 및 QEMU Guest Agent 동작 확인

    마무리: 이제 진짜 가상화 서버 구축의 시작입니다 🎉

    여기까지 따라오셨다면 이제 Proxmox VE 기반의 안정적인 가상화 서버 구축을 위한 기초가 완성된 거예요. 처음에는 설정할 게 많아 보여도, 한 번 제대로 해두면 나중에 진짜 편해집니다.

    저도 처음 홈랩에 Proxmox를 올렸을 때는 이런 가이드가 없어서 여기저기 찾아다니며 조각조각 맞추느라 꽤 오래 걸렸어요. 이 글이 처음 시작하시는 분들께 조금이나마 도움이 됐으면 좋겠네요.

    다음 단계로 추천하는 것들:

    • 🖥️ 첫 번째 VM 만들어보기 (Ubuntu Server, Rocky Linux 등)
    • 📦 LXC 컨테이너로 가벼운 서비스 돌려보기
    • 🔗 Let’s Encrypt 인증서로 웹 UI HTTPS 설정하기
    • 💾 외부 NAS 스토리지 연동 (NFS, SMB)
    • 🔄 Proxmox 클러스터 구성 (서버가 여러 대라면)

    다음 글에서는 Proxmox VE에서 첫 번째 VM 만들기를 다뤄볼 예정이에요. Ubuntu Server를 설치하고 기본 설정까지 하는 과정을 처음부터 끝까지 보여드릴게요. 기대해주세요!

    궁금한 점이나 제가 놓친 부분이 있으면 댓글로 남겨주세요. 같이 삽질하며 배우는 게 제일 재미있으니까요 😄