13년차의 서버실

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

[작성자:] admin

  • [HomeLabs] 씬클라이언트 홈서버 구축: 저전력 미니PC 활용 완벽 가이드

    전기세 걱정 없이 24시간 서버 돌리고 싶다면?

    홈서버를 처음 구축할 때 저도 제일 먼저 든 걱정이 “이거 전기세 얼마나 나오지?”였거든요. 그때 마침 회사 IT 창고에 쌓여 있던 씬클라이언트(Thin Client) 장비들을 보면서 “이걸 집에 가져다 쓰면 어떨까?” 싶었습니다. 결론부터 말씀드리면, 씬클라이언트 홈서버 구축은 저전력 + 저비용 + 충분한 성능이라는 세 마리 토끼를 꽤 잘 잡는 선택이에요.

    요즘 미니PC 홈서버에 관심 갖는 분들이 늘어나고 있는데, 막상 어떤 장비를 고르고 어떻게 세팅해야 할지 막막하신 분들을 위해 13년간 서버실에서 일해온 경험을 정리해봤습니다.

    씬클라이언트 기반 홈랩의 전형적인 네트워크 구성도 — 공유기, 씬클라이언트 서버, NAS, 그리고 외부 접근 경로를 한눈에 볼 수 있습니다.

    씬클라이언트(Thin Client)가 뭔가요? 쉽게 풀어보면

    씬클라이언트는 원래 기업 환경에서 VDI(Virtual Desktop Infrastructure, 가상 데스크탑 인프라)나 원격 서버에 접속하기 위한 단말기로 쓰이던 장비예요. 쉽게 말해서, 자체적으로 무거운 연산은 안 하고 서버에서 처리한 결과만 화면에 뿌려주는 “얇은(Thin)” 클라이언트죠.

    이게 홈서버 용도로 매력적인 이유가 있어요.

    • 기업에서 대량으로 쓰다 교체한 중고 물량이 시장에 꽤 많이 풀림
    • 원래 설계 자체가 저전력, 저소음, 소형 폼팩터(Form Factor)
    • x86 아키텍처 기반이라 일반 리눅스 서버 OS 설치 가능
    • eMMC나 SSD가 내장된 모델이 많아 별도 저장장치 없이도 부팅 가능

    저도 처음엔 “이게 진짜 서버 역할을 할 수 있어?” 반신반의했는데, 지금은 홈랩의 핵심 노드로 잘 쓰고 있습니다 ㅎㅎ.

    씬클라이언트 vs 일반 미니PC — 뭐가 다를까?

    구분 씬클라이언트 일반 미니PC NUC 계열
    주 용도 기업 단말기 (중고 활용) 개인 사용 범용 소형 데스크탑/서버
    가격대 (중고) 매우 저렴 중간 비교적 고가
    소비 전력 매우 낮음 낮음~중간 낮음~중간
    확장성 제한적 중간 높음
    소음 매우 조용 (무팬 모델 있음) 조용한 편 모델마다 다름
    신뢰성 기업용 내구성 소비자용 높음

    💡 팁: 씬클라이언트는 원래 하루 종일 켜놓는 기업 환경에서 쓰이던 장비라 24시간 연속 운영에 강한 내구성을 가진 경우가 많습니다.

    어떤 씬클라이언트를 골라야 할까? — 선택 기준 정리

    여기서 솔직히 말씀드리면, 특정 모델을 무조건 추천하기보다는 어떤 기준으로 고를지를 아는 게 더 중요해요. 중고 시장 물량은 지역마다, 시기마다 달라지거든요.

    체크해야 할 핵심 스펙

    1. CPU 아키텍처 확인: x86_64(AMD64) 아키텍처인지 반드시 확인하세요. ARM 기반 씬클라이언트는 지원되는 소프트웨어가 제한됩니다.
    2. RAM 용량 및 확장 가능 여부: 최소 4GB, 가능하면 8GB 이상. 솔더드(Soldered, 납땜 고정) RAM인지 교체 가능한 SO-DIMM 슬롯인지 꼭 확인하세요.
    3. 저장장치 인터페이스: M.2 NVMe, M.2 SATA, 2.5인치 SATA 슬롯 여부. eMMC만 있는 모델은 확장이 어렵습니다.
    4. 네트워크 포트: 기가비트(1GbE) 이더넷 포트가 있는지 확인. 일부 구형 모델은 100Mbps에 그치기도 해요.
    5. USB 포트 수량 및 규격: USB 3.0 이상 포트가 몇 개인지 — 외장 저장장치 연결에 중요합니다.
    6. BIOS/UEFI 접근 가능 여부: 부팅 순서 변경, PXE 부팅 설정 등을 위해 BIOS에 접근할 수 있어야 합니다.
    7. 소비 전력 TDP: CPU의 TDP(Thermal Design Power, 열 설계 전력)가 낮을수록 전기세 절약.

    ⚠️ 주의: 일부 기업용 씬클라이언트는 BIOS가 잠겨 있거나, 특정 OS만 부팅되도록 제한이 걸려 있는 경우가 있어요. 중고 구입 전에 판매자에게 BIOS 접근 가능 여부를 꼭 확인하세요. 저도 한번 이걸 놓쳐서 삽질했습니다 😅.

    OS 설치 — Ubuntu Server로 시작하기

    씬클라이언트 홈서버 구축에서 OS는 Ubuntu Server LTS(Long Term Support, 장기 지원 버전)를 강력 추천합니다. 커뮤니티가 크고, 레퍼런스가 많아서 문제가 생겼을 때 해결책 찾기가 훨씬 수월하거든요.

    부팅 USB 만들기

    # balenaEtcher나 dd 명령어로 USB 부팅 디스크 생성
    # Linux/macOS에서 dd 사용 시 (주의: of= 경로 틀리면 데이터 날아갑니다!)
    sudo dd if=ubuntu-server-*.iso of=/dev/sdX bs=4M status=progress
    sync

    ⚠️ 중요: of=/dev/sdX에서 X는 여러분의 USB 드라이브 경로입니다. lsblk 명령어로 먼저 확인하고 진행하세요. 잘못된 경로 입력 시 데이터 복구 불가!

    설치 후 기본 세팅

    # 시스템 업데이트
    sudo apt update && sudo apt upgrade -y
    
    # 필수 패키지 설치
    sudo apt install -y \
      curl \
      wget \
      git \
      htop \
      net-tools \
      openssh-server \
      ufw
    
    # SSH 서비스 활성화 및 시작
    sudo systemctl enable ssh
    sudo systemctl start ssh
    
    # UFW(Uncomplicated Firewall) 기본 설정
    sudo ufw allow ssh
    sudo ufw enable
    sudo ufw status

    고정 IP 설정 — Netplan으로

    홈서버는 IP가 바뀌면 난감하니까 고정 IP(Static IP)를 잡아줘야 해요. Ubuntu 18.04 이후부터는 Netplan(네트플랜)을 씁니다.

    # /etc/netplan/00-installer-config.yaml
    network:
      version: 2
      ethernets:
        eth0:  # 실제 인터페이스명으로 변경 (ip a 명령어로 확인)
          dhcp4: no
          addresses:
            - 192.168.1.100/24  # 원하는 고정 IP
          gateway4: 192.168.1.1  # 공유기 IP
          nameservers:
            addresses:
              - 1.1.1.1
              - 8.8.8.8
    # 설정 적용
    sudo netplan apply
    
    # 적용 확인
    ip a

    Ubuntu Server 설치 완료 후 SSH로 접속한 터미널 화면 — 이 화면이 뜨면 절반은 성공입니다.

    Docker로 서비스 올리기 — 홈서버의 꽃

    저전력 서버 구축에서 Docker(도커)는 거의 필수예요. 자원을 효율적으로 쓰면서 여러 서비스를 격리해서 운영할 수 있거든요. VM(Virtual Machine, 가상 머신)보다 오버헤드가 훨씬 적어서 저전력 환경에 딱 맞습니다.

    Docker 설치

    # Docker 공식 설치 스크립트 사용
    curl -fsSL https://get.docker.com -o get-docker.sh
    sudo sh get-docker.sh
    
    # 현재 사용자를 docker 그룹에 추가 (sudo 없이 docker 명령 사용)
    sudo usermod -aG docker $USER
    
    # 변경 사항 적용을 위해 재로그인 후 확인
    newgrp docker
    docker --version

    Docker Compose로 서비스 스택 구성

    여기서 제가 실제로 씬클라이언트 홈서버에서 돌리고 있는 기본 스택을 공유할게요. Portainer(포테이너)는 Docker 컨테이너를 웹 UI로 관리할 수 있게 해주는 도구인데, 초보자한테 정말 유용합니다.

    # docker-compose.yml
    version: '3.8'
    
    services:
      portainer:
        image: portainer/portainer-ce:latest
        container_name: portainer
        restart: unless-stopped
        ports:
          - "9000:9000"
          - "9443:9443"
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
          - portainer_data:/data
    
      nginx-proxy-manager:
        image: jc21/nginx-proxy-manager:latest
        container_name: nginx-proxy-manager
        restart: unless-stopped
        ports:
          - "80:80"
          - "443:443"
          - "81:81"  # 관리자 UI
        volumes:
          - npm_data:/data
          - npm_letsencrypt:/etc/letsencrypt
    
    volumes:
      portainer_data:
      npm_data:
      npm_letsencrypt:
    # 스택 시작
    docker compose up -d
    
    # 실행 중인 컨테이너 확인
    docker compose ps
    
    # 로그 확인
    docker compose logs -f

    홈서버에서 유용한 서비스 목록

    씬클라이언트 홈서버에서 실제로 잘 돌아가는 서비스들을 정리해봤어요.

    • Portainer: Docker 관리 웹 UI
    • Nginx Proxy Manager: 리버스 프록시(Reverse Proxy) + SSL 인증서 자동 관리
    • Pi-hole: DNS 기반 광고 차단 서버
    • Uptime Kuma: 서비스 모니터링 대시보드
    • Vaultwarden: Bitwarden 호환 셀프호스팅 패스워드 매니저
    • Nextcloud: 셀프호스팅 클라우드 스토리지 (RAM이 충분할 때)
    • Home Assistant: 스마트홈 허브

    💡 팁: RAM이 4GB인 모델이라면 한 번에 너무 많은 서비스를 올리지 마세요. free -h 명령어로 메모리 사용량을 주기적으로 확인하면서 서비스를 하나씩 추가하는 게 안전합니다.

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

    13년 동안 서버 만지면서 느낀 건데, 홈랩은 항상 예상치 못한 곳에서 막힙니다. 제가 씬클라이언트 홈서버 구축하면서 겪은 주요 문제들을 공유할게요.

    문제 1: 부팅 후 USB 인식 안 됨

    씬클라이언트 일부 모델은 기본 BIOS 설정에서 USB 부팅이 비활성화되어 있어요. BIOS 진입 키(보통 F2, F10, Del 중 하나)를 눌러서 Boot Order(부팅 순서)에서 USB를 최우선으로 변경해야 합니다. Secure Boot(보안 부팅)도 비활성화해야 Ubuntu 설치가 잘 되더라고요.

    문제 2: 네트워크 인터페이스 이름이 이상함

    Ubuntu에서 네트워크 인터페이스 이름이 eth0이 아니라 enp2s0이나 eno1처럼 나오는 경우가 많아요. Netplan 설정할 때 실제 이름을 먼저 확인해야 합니다.

    # 실제 네트워크 인터페이스 이름 확인
    ip link show
    # 또는
    ip a

    문제 3: 전원 차단 후 자동 부팅이 안 됨

    정전이나 실수로 전원이 끊겼을 때 자동으로 다시 켜지게 하려면 BIOS에서 “Power On After Power Failure” 또는 “AC Power Recovery” 옵션을 “On” 또는 “Last State”로 설정해야 해요. 이걸 안 하면 정전 후에 서버가 죽어 있는 상황이 생깁니다.

    문제 4: Docker 컨테이너가 재시작 후 안 올라옴

    Docker 서비스가 시스템 시작 시 자동 실행되도록 설정이 필요합니다.

    # Docker 서비스 자동 시작 등록
    sudo systemctl enable docker
    sudo systemctl enable containerd
    
    # 확인
    sudo systemctl is-enabled docker

    문제 5: eMMC 저장장치 용량 부족

    내장 eMMC가 32GB 이하인 모델은 OS + Docker 이미지로 금방 꽉 차요. 외장 USB SSD나 M.2 슬롯 추가 SSD로 용량을 늘리고, Docker의 데이터 디렉터리를 변경해주는 게 좋습니다.

    # Docker 데이터 디렉터리 변경
    # /etc/docker/daemon.json 파일 생성 또는 수정
    sudo nano /etc/docker/daemon.json
    {
      "data-root": "/mnt/external-ssd/docker"
    }
    # Docker 재시작
    sudo systemctl restart docker
    
    # 확인
    docker info | grep "Docker Root Dir"

    모니터링 설정 — 서버 상태 한눈에 보기

    저전력 서버 구축을 하고 나서 “잘 돌아가고 있나?” 확인하고 싶은 게 당연하죠. Uptime Kuma(업타임 쿠마)와 Glances(글랜시스)를 같이 쓰면 꽤 만족스러운 모니터링 환경이 됩니다.

    # monitoring-stack.yml
    version: '3.8'
    
    services:
      uptime-kuma:
        image: louislam/uptime-kuma:latest
        container_name: uptime-kuma
        restart: unless-stopped
        ports:
          - "3001:3001"
        volumes:
          - uptime_kuma_data:/app/data
    
      glances:
        image: nicolargo/glances:latest
        container_name: glances
        restart: unless-stopped
        pid: host
        ports:
          - "61208:61208"
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock:ro
        environment:
          - GLANCES_OPT=-w  # 웹 서버 모드로 실행
    
    volumes:
      uptime_kuma_data:

    Uptime Kuma와 Glances로 구성한 홈서버 모니터링 화면 — 각 서비스 응답 시간과 CPU/메모리 사용률을 실시간으로 확인할 수 있습니다.

    전력 최적화 — 진짜 저전력으로 만들기

    씬클라이언트 홈서버의 장점을 최대한 살리려면 OS 레벨에서도 전력 최적화를 해주면 좋아요.

    불필요한 서비스 비활성화

    # 서버에서 불필요한 GUI 관련 서비스 비활성화
    sudo systemctl disable bluetooth
    sudo systemctl disable cups  # 프린터 서비스
    sudo systemctl disable avahi-daemon  # mDNS (필요 없다면)
    
    # 현재 실행 중인 서비스 목록 확인
    sudo systemctl list-units --type=service --state=running

    CPU 주파수 조절 (powersave 모드)

    # cpufrequtils 설치
    sudo apt install -y cpufrequtils
    
    # 현재 CPU 거버너(Governor) 확인
    cpufreq-info | grep "current policy"
    
    # powersave 모드로 변경 (저전력 우선)
    sudo cpufreq-set -g powersave
    
    # 부팅 시 자동 적용 설정
    echo 'GOVERNOR="powersave"' | sudo tee /etc/default/cpufrequtils

    홈랩 구축 결과 정리 — 이 정도면 충분합니다

    씬클라이언트 홈서버 구축을 완료하고 나면 어떤 게 가능해지는지 정리해볼게요.

    항목 씬클라이언트 홈서버 일반 데스크탑 서버
    24시간 운영 전기세 부담 매우 낮음 높음
    소음 거의 없음 (무팬 모델) 팬 소음 있음
    초기 비용 중고 시 매우 저렴 상대적으로 높음
    Docker 서비스 운영 5~10개 경량 서비스 가능 제한 없음
    확장성 제한적 높음
    공간 차지 매우 작음 큼

    🎉 드디어 됐다! 씬클라이언트 홈서버가 완성되면, Pi-hole로 집 전체 광고를 차단하고, Vaultwarden으로 패스워드를 직접 관리하고, Uptime Kuma로 모든 서비스 상태를 모니터링하는 나만의 홈랩이 완성됩니다.

    씬클라이언트 홈서버 구축 전체 과정 요약 — 장비 선택부터 OS 설치, Docker 세팅, 모니터링까지 한눈에 보는 로드맵.

    자주 묻는 질문 (FAQ)

    Q. 씬클라이언트로 NAS도 만들 수 있나요?

    가능합니다! USB 3.0 포트에 외장 HDD나 SSD를 연결하고, Samba(삼바) 또는 NFS(Network File System)를 설정하면 기본적인 NAS 기능을 구현할 수 있어요. 다만 USB 인터페이스의 속도 한계가 있으니 고성능 NAS를 원하신다면 전용 NAS 장비를 고려하시는 게 좋습니다.

    Q. RAM이 4GB인 모델에서 돌릴 수 있는 서비스 수는?

    경량 서비스 기준으로 5~8개 정도는 무리 없이 돌아가더라고요. Pi-hole, Portainer, Vaultwarden, Uptime Kuma, Nginx Proxy Manager 조합은 4GB에서도 안정적입니다.

    Q. 외부에서 접근하려면 어떻게 해야 하나요?

    Cloudflare Tunnel(클라우드플레어 터널)을 활용하면 공인 IP 없이도 안전하게 외부 접근이 가능합니다. 이 부분은 내용이 많아서 다음 글에서 자세히 다룰 예정이에요.

    Q. 씬클라이언트는 어디서 구매하나요?

    중고나라, 당근마켓, eBay 등에서 기업 방출 중고 물량을 찾을 수 있어요. 검색할 때 “씬클라이언트”, “thin client”, “기업 방출 미니PC” 키워드로 찾아보세요.

    마무리 — 작게 시작해서 크게 배우는 홈랩

    씬클라이언트 홈서버 구축, 처음엔 뭔가 복잡해 보이지만 막상 해보면 생각보다 훨씬 재미있습니다. 저도 첫 홈서버가 씬클라이언트였는데, 그게 지금 이 블로그의 시작점이 됐거든요.

    중요한 건 완벽한 장비를 기다리는 것보다 지금 있는 장비로 일단 시작하는 것입니다. 저전력 서버 구축을 통해 리눅스 명령어도 익히고, 네트워크도 배우고, Docker도 배우다 보면 어느 순간 홈랩이 내 가장 좋은 실습 환경이 되어 있을 거예요.

    • ✅ 씬클라이언트 선택 기준 이해
    • ✅ Ubuntu Server 설치 및 기본 설정
    • ✅ Docker + Compose로 서비스 운영
    • ✅ 전력 최적화로 진짜 저전력 서버 완성
    • ✅ 모니터링으로 안정적인 운영 환경 구축

    다음 글에서는 Cloudflare Tunnel을 이용한 외부 접근 설정을 다룰 예정입니다. 공인 IP 없이도 집 서버에 안전하게 접근하는 방법인데, 씬클라이언트 홈랩 구축의 완성판이라고 할 수 있어요. 기대해주세요! 😊

    혹시 구축하다가 막히는 부분 있으시면 댓글로 남겨주세요. 경험자로서 최대한 도움 드리겠습니다!

  • [Nas] TrueNAS CORE vs SCALE: 홈랩 및 소규모 비즈니스 NAS 선택 가이드

    [Nas] TrueNAS CORE vs SCALE: 홈랩 및 소규모 비즈니스 NAS 선택 가이드

    안녕하세요, 13년차 서버실입니다.

    홈랩 운영하시는 분들이나 소규모 비즈니스를 위한 NAS를 고민하시는 분들이라면 한 번쯤 TrueNAS라는 이름을 들어보셨을 거예요. 저도 처음엔 단순히 ‘FreeNAS’ 시절부터 쭉 써왔던 터라 익숙한 이름이었는데, 어느새 TrueNAS CORE와 TrueNAS SCALE이라는 두 가지 버전으로 나뉘어 있더라고요. ‘도대체 뭘 선택해야 하지?’ 저도 꽤 고민하고 삽질 좀 했거든요. 그래서 오늘은 제가 직접 경험한 것을 바탕으로 TrueNAS CORE vs SCALE의 차이점과 여러분의 환경에 맞는 선택 가이드를 알려드릴게요. 이 글이 여러분의 현명한 TrueNAS 선택에 도움이 되길 바라요.

    TrueNAS, CORE와 SCALE 개념 이해하기

    TrueNAS는 FreeBSD 기반의 TrueNAS CORE와 Linux 기반의 TrueNAS SCALE로 나뉩니다. 둘 다 ZFS 파일 시스템을 기반으로 안정적인 데이터 저장과 관리 기능을 제공하는 NAS 운영체제(OS)죠. 쉽게 말해, 여러분의 하드웨어를 강력한 네트워크 스토리지 서버로 만들어주는 소프트웨어라고 생각하면 돼요. TrueNAS CORE는 전통적인 NAS 기능에 충실하고, TrueNAS SCALE은 여기에 컨테이너(Container)와 가상 머신(Virtual Machine, VM) 기능을 더해 확장성을 높인 버전이라고 보면 돼요. 사실상 NAS OS 비교의 핵심이죠.

    TrueNAS CORE 깊이 보기: 전통의 강자

    TrueNAS CORE는 오랫동안 FreeNAS라는 이름으로 사랑받아온 전통적인 버전입니다. FreeBSD 운영체제를 기반으로 하고 있죠.

    장점:

    • 안정성(Stability): FreeBSD 기반이라 굉장히 안정적이에요. 오랜 기간 검증된 만큼 데이터 안정성을 최우선으로 생각한다면 이만한 게 없어요. 제가 홈랩에서 10년 넘게 굴려봤는데, 정말 든든하더라고요.
    • 성숙한 기능(Mature Features): ZFS 파일 시스템, 다양한 프로토콜(SMB, NFS, iSCSI, FTP 등), 플러그인(Plugins) 기반의 추가 기능 등 NAS에 필요한 모든 기능이 완벽하게 구현되어 있어요.
    • 낮은 리소스 요구량(Lower Resource Footprint): SCALE에 비해 상대적으로 낮은 하드웨어 사양에서도 좋은 성능을 보여줘요. 특히 오래된 하드웨어로 NAS를 구축하려는 분들께는 아주 매력적인 선택지죠.

    단점:

    • 확장성 제한(Limited Extensibility): 플러그인 외에는 추가 기능을 확장하기가 쉽지 않아요. Docker 컨테이너나 가상 머신을 직접 구동하기 어렵다는 점이 가장 큰 단점이에요.
    • 상대적으로 적은 커뮤니티 자료(Smaller Community for Advanced Features): 전통적인 NAS 기능에는 자료가 많지만, 최신 개발 환경을 위한 자료는 SCALE에 비해 적은 편이에요.

    간단히 말해, ‘나는 그냥 튼튼한 데이터 저장소가 필요하고, 부가 기능은 크게 중요하지 않다!’ 하시는 분들께 CORE는 최고의 선택이 될 거예요. 제가 처음에 홈랩을 구성할 때도 CORE를 썼는데, 정말 별 탈 없이 잘 썼거든요. 안정성 하나는 끝내줍니다. ✅

    TrueNAS SCALE 깊이 보기: 미래 지향적인 올인원

    자, 다음은 요즘 핫한 TrueNAS SCALE입니다. CORE와 달리 Linux 기반(Debian)으로 만들어졌어요. 여기서 큰 차이가 발생하죠.

    장점:

    • 컨테이너 및 가상화 지원(Container & Virtualization Support): Docker 컨테이너와 Kubernetes(쿠버네티스)를 통한 앱 배포, KVM 기반의 가상 머신을 직접 구동할 수 있다는 점이 가장 큰 장점이에요. 홈랩에서 다양한 서비스를 한 번에 돌리고 싶을 때 정말 강력해요. 제가 Nextcloud, Plex, Home Assistant 등을 전부 SCALE 위에서 컨테이너로 돌리고 있는데, 관리도 편하고 성능도 아주 만족스러워요. 🎉
    • 확장성(Scalability): 기존 CORE보다 훨씬 유연하게 기능을 확장할 수 있어요. 특히 TrueCharts 같은 커뮤니티 앱 스토어를 통해 수많은 앱을 쉽게 설치하고 관리할 수 있죠.
    • 클러스터링(Clustering) 가능: 여러 TrueNAS SCALE 서버를 묶어 스케일-아웃(Scale-out) 스토리지를 구축할 수 있어요. 소규모 비즈니스 환경에서 나중에 확장을 고려한다면 이 기능이 정말 중요할 수 있죠.

    단점:

    • 상대적으로 높은 리소스 요구량(Higher Resource Footprint): Linux 커널과 컨테이너 환경 때문에 CORE보다 더 많은 RAM과 CPU 리소스가 필요해요. 오래된 하드웨어에서는 버벅일 수 있죠.
    • 새로운 기능의 안정성(New Features Stability): CORE에 비해 역사가 짧기 때문에, 새로운 기능들이 아직 완벽하게 안정화되지 않았을 가능성도 있어요. 물론 지금은 많이 안정화되었지만, 초기에는 잔버그도 좀 있었거든요. 삽질 좀 했습니다 ㅎㅎ
    • 복잡성(Complexity): 컨테이너, VM, Kubernetes 등 다룰 수 있는 기능이 많아지면서 CORE에 비해 학습 곡선이 가파를 수 있어요. 초보자에게는 좀 어렵게 느껴질 수도 있죠.

    SCALE은 ‘나는 NAS 기능뿐만 아니라, 홈 서버나 개발 서버 역할까지 한 번에 다 하고 싶다!’ 하시는 분들을 위한 올인원(All-in-one) 솔루션이라고 보면 돼요. 저도 결국 SCALE로 넘어왔는데, 정말 만족하면서 쓰고 있어요. 💡

    TrueNAS CORE와 SCALE의 핵심 차이점을 한눈에 볼 수 있는 아키텍처 비교 다이어그램입니다.

    홈랩 사용자를 위한 TrueNAS 선택 가이드

    이제 여러분의 상황에 맞춰 어떤 TrueNAS를 선택해야 할지 고민해볼 시간이에요. 먼저 홈랩(Homelab) 환경부터 살펴볼까요?

    TrueNAS CORE를 추천하는 경우:

    • 안정적인 파일 서버가 최우선: 오직 데이터 저장과 공유 기능만 필요하고, 안정성이 가장 중요하다고 생각한다면 CORE가 좋아요.
    • 하드웨어 리소스가 제한적: 오래된 PC나 저사양 서버를 쓸 계획이라면 CORE가 리소스를 덜 먹어서 효율적이에요.
    • NAS 기능 외의 복잡한 설정은 피하고 싶다: Docker나 VM에 대한 지식이 없거나, 굳이 배우고 싶지 않다면 CORE가 훨씬 직관적이에요.

    TrueNAS SCALE을 추천하는 경우:

    • NAS 외에 다양한 서비스를 한 서버에서 운영하고 싶다: Plex 미디어 서버, Nextcloud 개인 클라우드, Home Assistant 스마트 홈 허브 등 다양한 애플리케이션을 컨테이너로 돌리고 싶다면 SCALE이 압도적으로 유리해요.
    • 가상 머신(VM)을 활용할 계획: 윈도우나 리눅스 VM을 TrueNAS 위에서 구동하고 싶다면 KVM을 지원하는 SCALE이 유일한 선택지에요.
    • 새로운 기술과 확장성에 관심이 많다: Docker, Kubernetes 같은 최신 기술을 홈랩에서 직접 경험해보고 싶다면 SCALE이 제격이에요. 저도 이 점 때문에 SCALE로 넘어왔죠!

    홈랩에서 TrueNAS CORE와 SCALE을 어떻게 활용할 수 있는지 보여주는 인포그래픽입니다.

    소규모 비즈니스 사용자를 위한 TrueNAS 선택 가이드

    그럼 소규모 비즈니스(Small Business) 환경에서는 어떨까요? 여기서는 안정성과 확장성, 그리고 관리의 용이성이 더욱 중요해져요.

    TrueNAS CORE를 추천하는 경우:

    • 주요 목적이 파일 서버 및 백업: 직원 간 파일 공유, 중요 데이터 백업 등 기본적인 NAS 기능이 핵심이라면 CORE의 검증된 안정성이 큰 장점이에요.
    • IT 인력이 제한적: 복잡한 컨테이너 환경 관리 부담 없이, 안정적인 스토리지만 운영하고 싶을 때 CORE가 더 적합할 수 있어요.
    • 비용 효율성: 초기 하드웨어 투자 비용을 절감하고 싶을 때, CORE는 낮은 사양에서도 충분한 성능을 제공해요.

    TrueNAS SCALE을 추천하는 경우:

    • 내부 서비스(Internal Services) 확장 계획: 사내 Git 서버, 프로젝트 관리 툴, CI/CD 파이프라인 등 다양한 내부 서비스를 NAS 위에서 통합 운영하고 싶다면 SCALE이 효율적이에요.
    • 미래 확장성을 고려: 향후 데이터 증가나 서비스 확장에 대비하여 스케일-아웃(Scale-out) 클러스터링을 염두에 둔다면 SCALE이 유리해요.
    • 가상화 환경 통합: 기존에 운영 중인 가상화 환경(VMware, Proxmox 등)에 TrueNAS를 통합하거나, TrueNAS 자체에서 VM을 구동해야 한다면 SCALE이 답이에요.

    사실 소규모 비즈니스에서도 ‘무조건 CORE가 좋다’ 또는 ‘무조건 SCALE이 좋다’고 말하기는 어려워요. 비즈니스의 특성과 미래 계획에 따라 신중하게 선택해야 하거든요. 저도 컨설팅을 진행할 때 항상 고객사의 상황을 먼저 파악해요. ⚠️

    주의사항 및 삽질 경험 공유

    제가 직접 TrueNAS를 사용하면서 겪었던 몇 가지 삽질 경험과 주의사항을 공유해 드릴게요.

    1. 하드웨어 요구사항:
      특히 TrueNAS SCALE은 CORE보다 RAM과 CPU를 더 많이 써요. 제가 처음엔 CORE 쓰던 서버에 그냥 SCALE을 올렸다가 버벅이는 경험을 했거든요. 특히 Docker 컨테이너나 VM을 많이 돌릴 계획이라면 최소 16GB RAM (개인적으로는 32GB 이상 권장)과 멀티코어 CPU는 필수라고 생각해요. 저도 결국 RAM 업그레이드를 했더니 훨씬 쾌적해졌어요.
    2. 마이그레이션(Migration) 고려:
      만약 기존에 TrueNAS CORE를 쓰고 계시다가 SCALE로 넘어가고 싶으시다면, 데이터 백업은 필수에요. CORE에서 SCALE로의 직접적인 인플레이스(In-place) 업그레이드는 가능하지만, 항상 예기치 않은 문제가 발생할 수 있거든요. 저도 괜히 한번 믿었다가 데이터 날릴 뻔했어요… 😱 꼭 백업하시고, 가능하다면 새로운 하드웨어에 SCALE을 새로 설치하고 데이터를 옮기는 걸 추천해요.
    3. ZFS 설정의 중요성:
      TrueNAS의 핵심은 ZFS에요. 풀(Pool) 구성, 데이터셋(Dataset) 설정, 스냅샷(Snapshot) 주기 등을 처음부터 잘 계획해야 나중에 후회하지 않아요. 특히 스냅샷은 랜섬웨어(Ransomware) 같은 위협으로부터 데이터를 보호하는 데 정말 중요하니까요. 저도 예전에 한번 실수로 중요한 데이터를 지운 적이 있었는데, 스냅샷 덕분에 살린 경험이 있어요. 💡

    TrueNAS CORE에서 SCALE로 안전하게 마이그레이션하는 과정을 보여주는 흐름도입니다.

    두 버전 한눈에 비교하기

    두 버전을 한눈에 비교할 수 있도록 표로 정리해 봤어요.

    구분 TrueNAS CORE TrueNAS SCALE
    기반 OS FreeBSD Debian Linux
    주요 기능 안정적인 NAS (ZFS, SMB, NFS, iSCSI, 플러그인) NAS + 컨테이너 (Docker, Kubernetes) + 가상 머신 (KVM)
    주요 용도 순수 파일 서버, 데이터 백업, 고신뢰성 스토리지 올인원 홈 서버, 개발 환경, 클러스터링 스토리지
    하드웨어 요구사항 상대적으로 낮음 (8GB RAM 권장) 상대적으로 높음 (16GB RAM 이상 권장)
    확장성 플러그인 위주, 제한적 컨테이너 앱, VM, 클러스터링으로 유연하게 확장
    학습 곡선 쉬운 편 상대적으로 가파름

    TrueNAS CORE와 SCALE의 주요 특징을 비교한 표를 시각적으로 보여주는 차트입니다.

    마무리: 당신의 TrueNAS는?

    오늘은 TrueNAS CORE와 SCALE 중에서 어떤 것을 선택해야 할지, 제 13년차 인프라 엔지니어 경험을 바탕으로 이야기해 봤어요.

    결론적으로 말씀드리면, ‘정답은 없다!’ 이에요.

    • 안정적인 파일 저장 기능만 필요하고, 하드웨어 사양이 낮다면 TrueNAS CORE.
    • 다양한 컨테이너 앱이나 가상 머신을 돌리고 싶고, 충분한 하드웨어 리소스가 있다면 TrueNAS SCALE.

    이렇게 정리할 수 있을 것 같아요.

    어떤 버전을 선택하시든, TrueNAS는 강력한 NAS 솔루션임에는 틀림없어요. 여러분의 환경과 목적에 맞는 최적의 선택을 하시길 바라면서, 궁금한 점이 있다면 언제든지 댓글로 남겨주세요! 다음번에는 TrueNAS SCALE에서 Docker 컨테이너를 활용하는 방법에 대해 더 자세히 다뤄볼까 합니다. 기대해주세요! 😄

  • [HomeLabs] 홈랩 NAS OS 비교: TrueNAS, Unraid, OpenMediaVault 선택 가이드

    NAS OS, 뭘 골라야 할지 모르겠다면 — 이 글이 딱입니다

    홈랩을 처음 시작하거나, 기존 홈서버를 업그레이드하려고 할 때 가장 먼저 맞닥뜨리는 고민이 있죠. 바로 “NAS OS를 뭘 써야 하지?”라는 질문입니다.

    저도 딱 그랬거든요. 13년 전에 처음 홈랩을 꾸릴 때, 지금처럼 선택지가 많지 않았는데도 뭘 써야 할지 한참 고민했었습니다. 지금은 오히려 선택지가 너무 많아서 더 헷갈리는 상황이 됐더라고요. 홈랩 NAS OS 비교를 제대로 해놓은 한국어 글이 정말 드물어서, 제가 직접 써봤던 경험을 토대로 정리해 드리려고 합니다.

    오늘 다룰 세 가지는 TrueNAS, Unraid, OpenMediaVault(OMV)입니다. 이 셋이 현재 홈서버 커뮤니티에서 가장 널리 쓰이는 NAS 전용 OS들이에요. 각각 철학도 다르고, 강점도 다르고, 적합한 사용자 유형도 다릅니다. 끝까지 읽으시면 본인 상황에 맞는 선택이 보일 거예요.

    TrueNAS, Unraid, OpenMediaVault — 홈랩 NAS OS 3대장의 구조와 특성을 한눈에 비교한 다이어그램


    먼저 알아야 할 개념들 — NAS OS가 뭐가 다른 거야?

    일반 OS(Ubuntu, Windows)에 Samba나 NFS 서버를 올려도 NAS는 됩니다. 근데 왜 굳이 NAS 전용 OS를 쓰냐고요? 쉽게 말해서, 파일 서버에 특화된 기능들이 처음부터 내장되어 있기 때문입니다.

    • 스토리지 풀(Storage Pool): 여러 개의 디스크를 하나의 논리적 공간으로 묶는 기능
    • RAID / 패리티(Parity): 디스크 장애 시 데이터를 보호하는 기술
    • 스냅샷(Snapshot): 특정 시점의 파일 시스템 상태를 저장해두는 기능
    • SMB / NFS / AFP 공유: Windows, Linux, macOS와 파일을 주고받는 프로토콜
    • WebUI: 터미널 없이 웹 브라우저로 관리하는 인터페이스

    이런 것들을 일반 OS에서 직접 구성하려면 시간이 꽤 걸리는데, NAS OS는 이걸 다 묶어서 제공해 줍니다. 물론 각 OS마다 접근 방식이 완전히 다르거든요. 그게 핵심 차이입니다.


    TrueNAS — 기업급 신뢰성을 홈랩에

    TrueNAS가 뭔지 간단히

    TrueNAS는 iXsystems라는 회사에서 만든 오픈소스 NAS OS입니다. 예전에 FreeNAS라는 이름으로 불렸는데, 2020년에 TrueNAS CORE(FreeBSD 기반)와 TrueNAS SCALE(Linux/Debian 기반)로 분리됐어요.

    • TrueNAS CORE: FreeBSD 기반, ZFS 파일시스템, 오랜 역사와 안정성
    • TrueNAS SCALE: Linux(Debian) 기반, ZFS + 쿠버네티스(Kubernetes) 앱 지원

    핵심은 ZFS(제트파일시스템)입니다. ZFS는 데이터 무결성 검증, 스냅샷, 압축, 중복 제거 등 엔터프라이즈급 기능을 모두 갖춘 파일시스템이에요. 실제로 기업 스토리지에서도 쓰이는 기술이거든요.

    직접 써본 느낌

    제가 처음 TrueNAS CORE를 설치했을 때, 솔직히 WebUI가 좀 무겁다는 느낌을 받았습니다. 근데 한 번 익숙해지니까 이게 진짜 탄탄하더라고요. ZFS 풀을 구성하고 스냅샷 자동화 설정해두면, 데이터 보호 측면에서는 진짜 안심이 됩니다. 실제로 디스크 하나가 죽었을 때 데이터 손실 없이 교체한 경험이 있어요. 그때 TrueNAS 믿음이 생겼습니다 ㅎㅎ.

    TrueNAS 장단점

    • ✅ ZFS의 강력한 데이터 무결성 보호
    • ✅ 스냅샷, 복제(Replication) 기능이 훌륭함
    • ✅ 기업급 안정성, 오랜 커뮤니티
    • ✅ SCALE 버전은 Docker/쿠버네티스 앱 지원
    • ⚠️ ZFS 특성상 ECC 메모리 권장 (없어도 쓸 수는 있지만…)
    • ⚠️ RAM을 꽤 먹음 — ZFS ARC 캐시가 메모리를 적극 활용함
    • ⚠️ 디스크를 나중에 개별로 추가하기가 까다로움 (풀 단위로 관리)
    • ⚠️ 초보자에게는 진입 장벽이 있음

    이런 분께 추천

    데이터 보호를 최우선으로 생각하시는 분, 스냅샷과 복제를 활용한 백업 체계를 구축하고 싶은 분, 그리고 서버 하드웨어에 투자할 여유가 있는 분께 딱 맞습니다.


    Unraid — 유연함의 끝판왕

    Unraid는 왜 독특한가

    Unraid는 Lime Technology라는 회사의 상용 소프트웨어입니다. 무료가 아니에요 — 라이선스 구매가 필요합니다. 근데 홈랩 커뮤니티에서 엄청난 인기를 끌고 있는 이유가 있습니다.

    Unraid의 핵심 철학은 “다른 크기의 디스크를 자유롭게 섞어 쓸 수 있다”는 겁니다. 일반 RAID는 같은 크기의 디스크로 묶어야 하는데, Unraid는 그냥 디스크를 하나씩 배열(Array)에 추가하면 돼요. 4TB 하나, 8TB 하나, 12TB 하나 — 이렇게 섞어도 됩니다. 패리티(Parity) 디스크가 보호해주는 구조거든요.

    그리고 Docker 컨테이너와 VM(가상머신) 지원이 정말 잘 되어 있어요. Community Applications(CA)라는 앱 스토어 개념이 있어서, 클릭 몇 번으로 Plex, Jellyfin, Home Assistant 같은 앱을 설치할 수 있습니다. 처음 써봤을 때 “이거 진짜 편하다”는 생각이 절로 들더라고요.

    Unraid의 WebUI — 디스크 배열(Array), Docker 컨테이너, VM을 하나의 인터페이스에서 관리할 수 있다

    Unraid 장단점

    • ✅ 다른 크기의 디스크를 자유롭게 혼합 가능
    • ✅ 디스크 개별 추가/제거가 매우 유연함
    • ✅ Docker + VM 지원이 홈랩에 최적화
    • ✅ Community Applications(커뮤니티 앱 스토어)로 앱 설치 간편
    • ✅ WebUI가 직관적이고 초보자도 접근하기 쉬움
    • ⚠️ 유료 라이선스 필요 (USB 기반 라이선스 구조)
    • ⚠️ 배열 디스크가 개별 파일시스템(XFS/BTRFS)으로 관리됨 — ZFS 같은 통합 무결성 검증 없음
    • ⚠️ 패리티 검사(Parity Check) 중에는 성능이 떨어짐
    • ⚠️ 오픈소스가 아님

    이런 분께 추천

    다양한 크기의 디스크를 점진적으로 추가하며 스토리지를 늘리고 싶은 분, NAS + 미디어 서버 + 홈 자동화를 한 박스에서 다 해결하고 싶은 분, 그리고 설정의 편의성을 중요하게 생각하는 분께 강력 추천합니다.


    OpenMediaVault — 가볍고 오픈소스, 입문자의 친구

    OMV는 어떤 OS인가

    OpenMediaVault(이하 OMV)는 Debian Linux 기반의 완전 오픈소스 NAS OS입니다. 무료예요. FreeNAS(현 TrueNAS)의 초기 개발자 중 한 명이 만든 프로젝트라는 배경이 있습니다.

    OMV의 가장 큰 특징은 가볍다는 겁니다. 라즈베리 파이(Raspberry Pi)에도 설치할 수 있어요. 실제로 홈랩 입문자들이 라즈베리 파이 4에 OMV를 올려서 간단한 파일 서버를 구축하는 사례가 많습니다. 저도 테스트용으로 Pi에 올려봤는데, 설치 자체는 정말 간단하더라고요.

    플러그인(Plugin) 시스템으로 기능을 확장하는 구조입니다. OMV-Extras라는 서드파티 플러그인 저장소를 추가하면 Docker(Portainer), ZFS, 다양한 기능들을 추가할 수 있어요.

    OMV 장단점

    • ✅ 완전 무료 오픈소스
    • ✅ 저사양 하드웨어에서도 동작 (라즈베리 파이 포함)
    • ✅ Debian 기반이라 Linux 친숙한 분들에게 편함
    • ✅ 플러그인으로 기능 확장 가능
    • ✅ 기본 NAS 기능(SMB, NFS, FTP, rsync)은 충실하게 지원
    • ⚠️ 고급 스토리지 기능(ZFS 등)은 플러그인으로 추가해야 함
    • ⚠️ Unraid나 TrueNAS에 비해 커뮤니티 규모가 작음
    • ⚠️ Docker 환경 구성이 다른 OS에 비해 번거로울 수 있음
    • ⚠️ 대규모 스토리지 환경에는 적합하지 않을 수 있음

    이런 분께 추천

    예산이 빠듯한 입문자, 라즈베리 파이나 구형 PC를 활용하고 싶은 분, 단순 파일 공유 서버로만 쓸 분께 딱입니다. Linux를 어느 정도 다뤄본 분이라면 더욱 편하게 쓸 수 있어요.


    세 OS 한눈에 비교 — 어디서 뭐가 다른지

    TrueNAS vs Unraid vs OpenMediaVault 주요 항목 비교 — 홈랩 NAS OS 선택의 핵심 기준을 정리했다

    항목 TrueNAS CORE/SCALE Unraid OpenMediaVault
    기반 OS FreeBSD / Debian Linux Slackware Linux Debian Linux
    가격 무료 (오픈소스) 유료 (라이선스 구매 필요) 무료 (오픈소스)
    파일시스템 ZFS (기본) XFS / BTRFS (배열), ZFS(캐시) EXT4, XFS, BTRFS, ZFS(플러그인)
    디스크 혼합 어려움 (풀 단위 관리) 매우 자유로움 ⭐ 가능 (개별 마운트)
    Docker 지원 SCALE 버전 지원 기본 내장, 매우 편리 ⭐ 플러그인으로 가능
    VM 지원 SCALE 버전 지원 기본 내장 제한적
    데이터 무결성 ZFS 기반, 매우 강력 ⭐ 패리티 보호, 보통 파일시스템 의존
    최소 RAM 8GB 이상 권장 4GB 이상 권장 1GB도 가능 (Pi 등)
    진입 장벽 중~상 중 (WebUI 직관적) 하~중
    적합한 사용자 데이터 보호 중시, 중급 이상 홈랩 올인원, 중급 입문자, 저사양 환경

    실제 설치 — 이것만 알면 삽질 줄어듭니다

    TrueNAS 설치 시 주의사항

    TrueNAS는 설치 디스크(OS용)와 데이터 디스크를 반드시 분리해야 합니다. OS용으로 SSD나 USB를 별도로 쓰세요. 저도 처음에 이걸 몰라서 데이터 디스크에 OS 깔려고 했다가 낭패를 봤습니다 ㅎㅎ.

    # TrueNAS 설치 후 ZFS 풀 상태 확인
    zpool status
    
    # 풀 생성 예시 (RAIDZ1, 3개 디스크)
    zpool create tank raidz1 /dev/da1 /dev/da2 /dev/da3

    ⚠️ ZFS와 ECC 메모리 논쟁: 공식적으로 iXsystems는 ECC 메모리를 권장합니다. 단, 실제로 ECC 없이 쓰는 홈랩 유저도 많아요. 중요한 데이터라면 ECC를 갖추는 게 마음 편합니다.

    Unraid 설치 시 주의사항

    Unraid는 USB 드라이브에서 부팅하는 독특한 구조입니다. 라이선스가 USB 장치에 묶이거든요. 고품질 USB를 쓰세요 — 싸구려 USB 썼다가 부팅 안 되는 상황이 생길 수 있습니다.

    # Unraid에서 Docker 컨테이너 상태 확인 (터미널)
    docker ps
    
    # Unraid 배열 상태 확인
    mdcmd status
    
    # 디스크 목록 확인
    lsblk

    💡 팁: Unraid에서 캐시 풀(Cache Pool)을 SSD로 구성하면, 빠른 쓰기 후 배열로 이동하는 방식으로 성능을 크게 높일 수 있어요. 이걸 Mover라고 부릅니다.

    OpenMediaVault 설치 시 주의사항

    OMV는 Debian 설치하듯 진행됩니다. 라즈베리 파이에 설치할 때는 공식 스크립트를 사용해요.

    # 라즈베리 파이에 OMV 설치 스크립트 (공식 지원)
    wget -O - https://github.com/OpenMediaVault-Plugin-Developers/installScript/raw/master/install | sudo bash
    
    # OMV-Extras 플러그인 추가 (Docker 등 확장 기능용)
    wget -O - https://github.com/OpenMediaVault-Plugin-Developers/packages/raw/master/install | sudo bash
    
    # OMV 서비스 상태 확인
    systemctl status openmediavault-engined

    ⚠️ OMV 메이저 버전 업그레이드 시 플러그인 호환성 문제가 간혹 발생합니다. 업그레이드 전에는 꼭 백업하세요.


    설치 후 기본 검증 — 이것만 확인하세요

    NAS OS 설치 완료 후 WebUI에서 스토리지 풀 상태, 공유 폴더, 서비스 상태를 확인하는 화면

    어떤 OS를 설치했든, 기본 동작 확인은 이렇게 해보세요.

    1. 스토리지 풀/배열 상태 확인: 모든 디스크가 정상적으로 인식되었는지, RAID/패리티 구성이 올바른지 확인
    2. SMB 공유(Samba) 테스트: Windows 파일 탐색기에서 \\NAS-IP\공유폴더로 접근되는지
    3. NFS 공유 테스트: Linux 클라이언트에서 마운트 확인
    4. 읽기/쓰기 속도 테스트: 대용량 파일 복사로 기본 성능 확인
    5. 스냅샷 또는 백업 설정: 데이터 보호 체계가 실제로 동작하는지
    # Linux에서 NFS 마운트 테스트
    sudo mount -t nfs NAS-IP:/mnt/pool/share /mnt/test
    df -h /mnt/test
    
    # 간단한 쓰기 속도 테스트
    dd if=/dev/zero of=/mnt/test/testfile bs=1G count=1 oflag=direct
    
    # SMB 연결 테스트 (smbclient)
    smbclient //NAS-IP/share -U username

    여기까지 다 됐다면 🎉 기본 설정은 완료입니다. 다음은 서비스별 세부 튜닝과 백업 자동화 설정이 남아 있어요. 이 부분은 다음 글에서 각 OS별로 자세히 다룰 예정입니다.


    자주 묻는 질문 (FAQ)

    Q. TrueNAS와 Unraid 중 뭐가 더 낫나요?

    데이터 보호와 스토리지 신뢰성이 최우선이면 TrueNAS, 다양한 앱을 돌리는 올인원 홈서버를 원하고 디스크를 유연하게 추가하고 싶다면 Unraid가 더 맞습니다. 둘 다 훌륭한 선택이에요.

    Q. OpenMediaVault는 홈랩에서 쓰기 부족하지 않나요?

    단순 파일 공유 목적이라면 전혀 부족하지 않습니다. 단, 고급 스토리지 기능이나 Docker 환경이 복잡해지면 다른 OS로 넘어가는 분들도 있어요. 입문용으로는 충분히 좋습니다.

    Q. 나중에 OS를 바꿀 수 있나요?

    기술적으로는 가능하지만, 데이터 마이그레이션이 필요합니다. 특히 ZFS 풀은 TrueNAS에서 Unraid로 바로 이전이 안 돼요. 처음 선택을 신중하게 하시는 게 좋습니다.

    Q. 하드웨어 최소 사양은 어떻게 되나요?

    OpenMediaVault는 라즈베리 파이 4(4GB RAM)에서도 잘 돌아갑니다. TrueNAS는 8GB RAM 이상, Unraid는 4GB 이상을 권장하는데, 실제로 Docker나 VM을 많이 돌리면 16GB 이상이 편합니다.


    마무리 — 결국 정답은 “내 상황에 맞는 것”

    13년 동안 다양한 NAS OS를 써온 결론을 한 줄로 정리하면 이렇습니다.

    “데이터 보호 최우선 → TrueNAS, 유연한 올인원 홈랩 → Unraid, 가볍게 시작 → OpenMediaVault”

    사실 어떤 OS를 선택하든, 제대로 백업 체계를 갖추는 게 가장 중요합니다. NAS OS가 아무리 좋아도 백업이 없으면 소용없거든요. 3-2-1 백업 규칙(원본 1개 + 로컬 사본 1개 + 오프사이트 사본 1개)은 어떤 OS에서든 지켜주세요.

    저는 현재 메인 NAS는 TrueNAS SCALE로, 미디어 서버 겸 실험용 박스는 Unraid로 운영하고 있습니다. 두 개를 같이 쓰는 것도 방법이에요 ㅎㅎ.

    다음 글에서는 TrueNAS SCALE에서 ZFS 스냅샷 자동화와 원격 복제 설정하는 방법을 자세히 다룰 예정입니다. 궁금한 점은 댓글로 남겨주세요. 제가 직접 경험한 범위 안에서는 최대한 답변 드리겠습니다! 😄

  • [Proxmox] Proxmox VE 8.2 업그레이드 완벽 가이드 — 안전한 업데이트 방법

    [Proxmox] Proxmox VE 8.2 업그레이드 완벽 가이드 — 안전한 업데이트 방법

    안녕하세요, 13년차의 서버실 운영자입니다. 오늘은 홈랩이나 작은 서버실에서 사용하고 계실 Proxmox VE(Virtual Environment), 그중에서도 최신 버전인 Proxmox VE 8.2 업그레이드 과정과 업데이트에 대한 이야기를 나눠볼까 합니다.

    저는 13년 넘게 인프라 엔지니어로 일하면서 수많은 서버를 만져봤는데, 제 손으로 직접 구축하고 운영하는 홈랩만큼 애착 가는 곳은 없더라고요. Proxmox VE는 그런 저의 홈랩 운영에 있어 정말 빠질 수 없는 핵심 솔루션입니다. 그런데 새로운 버전이 나올 때마다 ‘업데이트를 해야 하나 말아야 하나’, ‘혹시 문제가 생기면 어쩌지?’ 하는 고민, 다들 한 번쯤 해보셨을 거예요. 특히 저처럼 여러 VM과 컨테이너(LXC)가 돌아가는 환경이라면 더욱 그렇죠.

    저도 Proxmox 버전 업그레이드하다가 몇 번 삽질했거든요. 네트워크 설정이 꼬이거나, 특정 VM이 부팅이 안 되는 경우도 있었고요. 하지만 그 모든 경험들이 지금의 저를 만들었다고 생각합니다. 그래서 오늘은 제가 직접 Proxmox VE 8.2로 업그레이드하면서 겪었던 과정과, 어떻게 하면 좀 더 안전하고 확실하게 시스템을 Proxmox 업그레이드할 수 있는지 그 노하우를 자세히 알려드릴게요. 자, 그럼 Proxmox VE 8.x 계열의 최신 버전을 향한 여정, 함께 떠나볼까요? 🎉

    Proxmox VE 8.2 업데이트를 위한 아키텍처 다이어그램. 현재 시스템 상태와 업데이트 후 예상되는 시스템 변경 사항을 시각적으로 보여줍니다.

    Proxmox VE 8.2, 뭐가 달라졌을까요? (핵심 개념 파악)

    본격적인 Proxmox VE 8.2 업그레이드 전에, 이번 버전의 핵심 변화를 짚고 넘어가야 합니다. Proxmox VE 8.x 버전은 기반 운영체제(Underlying OS)로 Debian 12 “Bookworm”을 사용하고 있거든요. 이전 7.x 버전이 Debian 11 “Bullseye” 기반이었던 것을 생각하면, 내부적으로 정말 많은 변화가 있었다는 걸 짐작할 수 있죠. 커널 버전도 Linux Kernel 6.8로 업그레이드되면서, 최신 하드웨어 지원과 성능 개선이 이루어졌습니다.

    쉽게 말해, Proxmox VE는 가상화 플랫폼이기 때문에 그 아래서 돌아가는 운영체제의 안정성과 최신 기술 지원이 정말 중요합니다. Debian 12로의 전환은 더 나은 보안, 개선된 패키지 관리, 그리고 최신 드라이버 지원을 의미하죠. 물론 UI나 기능적으로도 소소한 개선점들이 있지만, 가장 큰 변화는 바로 이 기반 OS의 업그레이드라고 보시면 됩니다.

    이런 변화들은 기존 시스템에서 Proxmox 업그레이드를 진행할 때 몇 가지 주의할 점이 생긴다는 뜻이기도 합니다. 특히 서드파티 저장소(Third-party repository)를 추가해서 사용하고 계셨다면 더더욱 신경 써야 합니다. 저도 예전에 그래픽 카드 패스스루(PCI Passthrough) 관련 드라이버 때문에 고생했던 기억이 나네요. 😅

    본격적인 Proxmox VE 8.2 업그레이드 과정 (단계별 가이드)

    이제 실전입니다. Proxmox VE 8.2 업그레이드를 위한 단계별 가이드를 시작해볼게요. 제가 직접 해보니 몇 가지 중요한 사전 작업과 순서가 있더라고요. 하나씩 따라오시면 무리 없이 진행하실 수 있을 겁니다. 💡

    1. 사전 준비 및 백업 (가장 중요! ⚠️)

    업그레이드 전에는 무조건 백업이 필수입니다. “설마 문제가 생기겠어?”라고 생각하다가 크게 후회하는 경우가 많거든요. 저도 예전에 백업 없이 진행했다가 밤새 복구한다고 씨름했습니다. 여러분은 저 같은 삽질은 하지 마세요! 모든 VM과 LXC를 백업하고, 가능하다면 Proxmox 설정 파일까지 백업해두세요.

    • VM/LXC 백업: Proxmox 웹 UI에서 각 VM/LXC를 선택 후 ‘Backup’ 메뉴를 이용합니다. 외부 스토리지에 저장하는 것을 권장합니다.
    • Proxmox 설정 백업: 핵심 설정 파일들을 수동으로 백업해두는 것도 좋습니다.
    
    # SSH로 Proxmox 호스트에 접속
    # /etc/pve 디렉토리 전체를 압축하여 백업
    tar -cvzf /root/pve-config-backup-$(date +%F).tar.gz /etc/pve
    # 네트워크 설정 백업 (필요시)
    cp /etc/network/interfaces /root/interfaces-backup-$(date +%F)
    # 다른 중요한 설정 파일들도 백업 (예: /etc/fstab, /etc/default/grub)
    

    그리고 또 중요한 것이, 현재 시스템의 모든 패키지를 최신 상태로 업데이트하는 겁니다. 깨끗한 상태에서 업그레이드를 시작해야 충돌을 줄일 수 있거든요.

    
    apt update
    apt full-upgrade -y
    apt autoremove -y
    

    업데이트 후에는 재부팅(Reboot)을 꼭 한 번 해주세요. 최신 커널이나 드라이버가 적용되지 않을 수 있거든요.

    
    reboot
    

    2. Proxmox 저장소 변경

    Proxmox VE 8.x는 Debian 12 기반이기 때문에, 기존 7.x(Debian 11)용 저장소(Repository) 설정이 맞지 않습니다. 이 부분을 꼭 바꿔줘야 해요. 특히 Enterprise 저장소를 사용하지 않는 홈랩 사용자라면, `pve-no-subscription` 저장소로 변경하는 게 중요합니다.

    
    # 기존 enterprise 저장소 주석 처리 (있다면)
    sed -i 's/^deb/#deb/' /etc/apt/sources.list.d/pve-enterprise.list
    
    # no-subscription 저장소 추가 또는 확인
    echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" > /etc/apt/sources.list.d/pve-no-subscription.list
    
    # Ceph 저장소를 사용 중이라면 (선택 사항)
    # Ceph Quincy는 Debian 11용, Ceph Reef는 Debian 12용입니다.
    # 만약 Ceph Reef로 업그레이드할 예정이라면 Ceph Reef 저장소를 추가해야 합니다.
    # echo "deb http://download.proxmox.com/debian/ceph-reef bookworm no-subscription" > /etc/apt/sources.list.d/ceph.list
    # 기존 Ceph Quincy 저장소 주석 처리
    # sed -i 's/^deb/#deb/' /etc/apt/sources.list.d/ceph.list
    

    그리고 Debian 자체 저장소도 `bullseye`에서 `bookworm`으로 변경해야 합니다.

    
    # /etc/apt/sources.list 파일 수정
    sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list
    

    💡 팁: `nano`나 `vi` 에디터에 익숙하시다면 직접 파일을 열어서 수정하는 것도 방법입니다. 저는 `sed` 명령어를 즐겨 쓰는데, 초보자분들은 에디터가 더 편할 수도 있어요. 💻

    Proxmox VE 웹 UI의 ‘Updates’ 섹션에서 현재 버전과 사용 가능한 업데이트를 확인하는 화면입니다. 업데이트 진행 전후 버전을 비교할 때 유용합니다.

    3. Debian 및 Proxmox VE 업그레이드

    이제 본격적으로 Proxmox VE 8.2 업그레이드를 진행할 차례입니다. 저장소를 변경했으니, 다시 패키지 목록을 업데이트하고 전체 업그레이드를 실행하면 됩니다.

    
    apt update
    apt dist-upgrade -y
    

    이 명령은 Debian 12로의 시스템 업그레이드와 Proxmox VE 8.x 패키지들을 한 번에 처리합니다. 시간이 꽤 걸릴 수 있으니 기다려주세요. 중간에 설정 파일 관련 질문이 나올 수 있는데, 일반적으로는 “N” 또는 “keep the local version currently installed”를 선택해서 기존 설정을 유지하는 것이 안전합니다. 새로운 설정 파일을 적용해야 하는 특별한 경우가 아니라면 말이죠.

    4. 불필요한 패키지 정리 및 재부팅

    업그레이드가 완료되면, 더 이상 필요 없는 이전 버전의 패키지들을 정리해주는 것이 좋습니다. 그리고 반드시 재부팅을 해야 모든 변경 사항이 적용되고 새로운 커널로 부팅됩니다.

    
    apt autoremove -y
    reboot
    

    재부팅 후에는 시스템이 제대로 부팅되는지, Proxmox VE 서비스들이 정상적으로 작동하는지 확인해야 합니다. 이때 가장 심장이 쫄깃하죠. 😬

    ⚠️ 트러블슈팅: 제가 겪었던 문제들 (그리고 해결법)

    업그레이드가 항상 순조롭게만 진행되면 얼마나 좋을까요? 저도 몇 번 Proxmox 업그레이드하다가 예상치 못한 문제에 부딪혔습니다. 여러분도 겪을 수 있는 몇 가지 일반적인 문제와 그 해결법을 공유합니다.

    • “Unable to locate package proxmox-ve” 오류: 이 오류는 대부분 저장소 설정이 잘못되었을 때 발생합니다. `/etc/apt/sources.list.d/pve-no-subscription.list` 파일의 내용이 정확한지, 그리고 `bookworm`으로 제대로 설정되었는지 다시 확인해보세요.
    • 네트워크 인터페이스 문제: 업그레이드 후 네트워크 연결이 안 되는 경우가 있습니다. `/etc/network/interfaces` 파일을 백업해둔 것을 참고하여 수동으로 재설정하거나, 기존 설정이 새로운 커널/드라이버와 충돌하는지 확인해야 합니다. 저 같은 경우는 브릿지(Bridge) 설정이 꼬여서 한참 헤맸거든요. 😫
    • VM/LXC 부팅 문제: 특정 VM이나 LXC가 부팅되지 않을 수 있습니다. 특히 LXC의 경우, 컨테이너 템플릿(Container Template)이 구 버전인 경우 문제가 생길 수 있습니다. 새로운 템플릿으로 다시 만들거나, 컨테이너 설정을 조정해야 할 수도 있어요.
    • GRUB 부트로더 문제: 드물게 부트로더(GRUB bootloader) 설정이 꼬여서 부팅이 안 되는 경우가 있습니다. 이럴 때는 Live CD/USB로 부팅하여 GRUB을 재설치해야 합니다. (이건 정말 최후의 수단입니다!)

    문제 발생 시 가장 먼저 해야 할 일은 로그(Log) 확인입니다. `/var/log/apt/term.log`나 `journalctl -xe` 명령으로 어떤 오류가 발생했는지 자세히 살펴보세요. 대부분의 답은 로그 안에 있습니다. 🕵️‍♂️

    Proxmox VE 8.2로 성공적으로 업데이트된 후의 웹 UI 대시보드 화면입니다. 시스템 정보에 8.2 버전과 Debian 12가 표시되어 있습니다.

    Proxmox VE 8.2 업그레이드 결과 확인 ✅

    재부팅 후, 이제 시스템이 Proxmox VE 8.2로 성공적으로 업그레이드되었는지 확인해봅시다. 웹 UI에 접속하면 대시보드(Dashboard)에서 시스템 정보를 확인할 수 있을 거예요.

    • Proxmox VE 버전 확인: 웹 UI 좌측 상단 또는 ‘Summary’ 패널에서 ‘PVE Manager’ 버전을 확인합니다. 8.2.x와 같이 표시되어야 합니다.
    • Debian 버전 확인: SSH로 접속하여 다음 명령으로 확인할 수 있습니다.
    
    cat /etc/debian_version
    # 12.x (bookworm) 과 같이 표시되어야 합니다.
    
    # 커널 버전 확인
    uname -r
    # 6.8.x 와 같이 표시되어야 합니다.
    

    모든 것이 정상적으로 표시된다면, 여러분의 Proxmox 업그레이드는 성공적으로 완료된 겁니다! 🎉 이제 최신 버전의 Proxmox VE가 제공하는 향상된 성능과 안정성을 만끽하시면 됩니다.

    저는 업그레이드 후에 모든 VM과 LXC를 하나씩 켜보면서 제대로 작동하는지 꼼꼼히 확인했습니다. 특히 네트워크 설정이나 저장소 연결 같은 부분이 중요하죠. 다행히 이번에는 큰 문제 없이 잘 마무리되어서 정말 뿌듯했네요! 😊

    Proxmox VE 7.x와 8.x 버전의 주요 기능, 기반 OS, 커널 버전 등을 비교하는 표입니다. 업그레이드의 이점을 한눈에 보여줍니다.

    마무리하며: 13년차 엔지니어의 한마디

    오늘은 Proxmox VE 8.2 업그레이드 과정에 대해 제가 겪었던 경험과 함께 자세히 설명해드렸습니다. 사실 서버 관리라는 게 언제나 예측 불가능한 변수들이 존재해서, 이렇게 가이드를 드려도 막상 현장에서는 또 다른 문제가 터질 때가 많아요. 저도 수십 번 겪어본 일이라 공감합니다.

    하지만 중요한 건, 문제가 생겼을 때 당황하지 않고 차근차근 해결해나가는 능력이라고 생각해요. 백업을 철저히 하고, 공식 문서나 커뮤니티의 도움을 받는 것이 중요하죠. 그리고 가장 중요한 건, “내가 이 시스템을 이해하고 있다”는 자신감입니다. 저도 처음엔 Proxmox가 마냥 어렵게만 느껴졌는데, 하나하나 삽질해가면서 배우다 보니 이제는 제 손안의 작은 데이터센터처럼 느껴지더라고요. 😎

    이번 Proxmox VE 8.2 업그레이드 가이드가 여러분의 홈랩이나 서버실 운영에 조금이나마 도움이 되었으면 좋겠습니다. 다음번에는 Proxmox VE 8.2에서 새롭게 추가된 기능들을 활용하는 방법에 대해서도 한번 다뤄볼 생각입니다. 궁금한 점이나 겪었던 삽질 경험이 있으시다면 댓글로 공유해주세요! 그럼 다음 글에서 또 만나요! 👋

  • [k8s] Helm 차트 개발 및 배포 모범 사례: 효율적인 쿠버네티스 애플리케이션 관리

    [k8s] Helm 차트 개발 및 배포 모범 사례: 효율적인 쿠버네티스 애플리케이션 관리

    Helm 차트 개발 및 배포 모범 사례: 효율적인 쿠버네티스 애플리케이션 관리

    안녕하세요, 13년차의 서버실입니다. 오늘은 쿠버네티스(Kubernetes) 환경에서 애플리케이션을 효율적으로 관리하는 핵심 도구인 Helm(헬름) 차트 개발과 배포 모범 사례에 대해 이야기해보려 합니다. 혹시 쿠버네티스에 배포할 애플리케이션이 늘어나면서 수많은 YAML 파일을 일일이 관리하는 데 어려움을 겪고 계신가요? 저도 처음엔 Deployment, Service, Ingress(인그레스, 외부 트래픽 진입점) 등 수많은 YAML 파일을 만들고, 애플리케이션을 업데이트할 때마다 모든 파일을 수정하느라 삽질 좀 했습니다 ㅎㅎ. Helm은 이런 복잡성을 해결해주는 정말 강력한 패키지 매니저(Package Manager)거든요. 마치 리눅스에서 APT나 YUM으로 소프트웨어를 설치하듯이, 쿠버네티스에서는 Helm으로 애플리케이션을 손쉽게 배포하고 관리할 수 있으니까요.

    이번 글을 통해 Helm 차트 개발의 기초부터 실제 운영에서 제가 겪었던 경험과 팁까지 모두 알려드릴게요. 드디어 YAML 지옥에서 벗어날 때가 온 거죠!

    Helm의 기본적인 작동 방식을 보여주는 아키텍처 다이어그램입니다. Helm 3부터는 Tiller가 사라져 더 간소화되었죠.

    Helm, 왜 필요할까요? (개념 설명)

    Helm은 쿠버네티스용 패키지 매니저라고 쉽게 생각하시면 됩니다. 애플리케이션을 구성하는 모든 쿠버네티스 리소스(Resource)들을 차트(Chart)라는 하나의 묶음으로 정의하고, 이 차트를 통해 배포, 업그레이드, 롤백(Rollback) 등 라이프사이클(Lifecycle) 관리를 자동화하죠. 쉽게 말해, 복잡한 쿠버네티스 애플리케이션을 마치 하나의 설치 파일처럼 다룰 수 있게 해주는 거예요.

    Helm의 주요 구성 요소

    • Chart (차트): 하나의 쿠버네티스 애플리케이션을 정의하는 파일들의 묶음입니다. Docker 이미지, 환경 변수, 서비스, 인그레스 등 모든 구성 요소를 담고 있죠.
    • Release (릴리즈): 쿠버네티스 클러스터에 배포된 차트의 인스턴스를 말합니다. 하나의 차트로 여러 개의 릴리즈를 생성할 수 있습니다. 예를 들어, Nginx 차트 하나로 개발 환경용 Nginx와 운영 환경용 Nginx를 각각 다른 릴리즈로 배포할 수 있거든요.
    • Repository (레포지토리): 차트들을 저장하고 공유하는 공간입니다. Artifact Hub나 개인 S3 버킷 등을 활용할 수 있습니다.

    Helm 차트의 핵심 구성 요소

    Helm 차트는 여러 파일과 디렉토리로 구성되지만, 특히 중요한 몇 가지가 있습니다.

    구성 요소 설명
    Chart.yaml 차트의 이름, 버전, 설명 등 메타데이터(Metadata)를 정의합니다. 차트의 ‘신분증’ 같은 거죠.
    values.yaml 차트 템플릿에 주입할 기본 설정 값들을 정의합니다. 배포 환경에 따라 달라지는 값들을 외부에서 쉽게 변경할 수 있게 해줍니다. 제가 제일 많이 만지는 파일이기도 해요.
    templates/ 쿠버네티스 리소스 정의(Deployment, Service 등) 파일들이 들어있는 디렉토리거든요. Go Template 문법을 사용해서 values.yaml의 값들을 동적으로 주입합니다.
    _helpers.tpl 재사용 가능한 템플릿 조각이나 함수들을 정의하는 파일입니다. 차트가 복잡해질수록 코드 중복을 줄이고 가독성을 높이는 데 정말 유용하더라고요.

    실전! Helm 차트 개발 및 배포

    이제 실제로 간단한 Nginx 애플리케이션을 배포하는 Helm 차트를 만들어보겠습니다. 제가 직접 해보니 이렇게 단계별로 따라 하는 게 가장 이해하기 쉽더라고요.

    1단계: 차트 초기화

    Helm CLI를 사용하면 기본적인 차트 구조를 자동으로 생성해줍니다. 정말 편하죠!

    helm create my-nginx-chart

    이 명령어를 실행하면 my-nginx-chart라는 디렉토리 안에 기본적인 차트 파일들이 생성됩니다. 이 구조를 기반으로 우리 애플리케이션에 맞게 수정하는 거죠.

    2단계: values.yaml 설정

    생성된 my-nginx-chart/values.yaml 파일을 열어 Nginx 이미지와 서비스 포트 등을 설정해봅시다. 기본적으로 생성된 내용이 많지만, 간단하게 Nginx 관련 부분만 수정할게요.

    # my-nginx-chart/values.yaml
    replicaCount: 1
    
    image:
      repository: nginx
      pullPolicy: IfNotPresent
      # Overrides the image tag whose default is the chart appVersion.
      tag: "latest"
    
    service:
      type: ClusterIP
      port: 80
    
    # ... (나머지 부분은 기본값 유지 또는 주석 처리)
    

    여기서 image.repository와 image.tag를 Nginx 이미지로 설정하고, service.port를 80으로 지정했습니다. 나중에 배포할 때 이 값들을 변경하고 싶으면 helm install이나 helm upgrade 명령어에서 --set 옵션을 사용하면 돼요.

    3단계: 템플릿 수정 (Deployment, Service)

    templates/ 디렉토리 안에는 deployment.yaml, service.yaml 등이 있습니다. 이 파일들을 values.yaml에 정의한 값들을 사용하도록 수정해야 합니다. {{ .Values.image.repository }}와 같은 Go Template 문법으로 값을 가져올 수 있거든요.

    my-nginx-chart/templates/deployment.yaml 파일의 image 부분을 이렇게 수정합니다.

    # my-nginx-chart/templates/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {{ include "my-nginx-chart.fullname" . }}
      labels:
        {{- include "my-nginx-chart.labels" . | nindent 4 }}
    spec:
      replicas: {{ .Values.replicaCount }}
      selector:
        matchLabels:
          {{- include "my-nginx-chart.selectorLabels" . | nindent 6 }}
      template:
        metadata:
          {{- with .Values.podAnnotations }}
          annotations:
            {{- toYaml . | nindent 8 }}
          {{- end }}
        labels:
          {{- include "my-nginx-chart.selectorLabels" . | nindent 6 }}
        spec:
          containers:
            - name: {{ .Chart.Name }}
              image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
              imagePullPolicy: {{ .Values.image.pullPolicy }}
              ports:
                - name: http
                  containerPort: 80
                  protocol: TCP
              # ... (나머지 부분은 기본값 유지)
    

    그리고 my-nginx-chart/templates/service.yaml 파일의 port 부분을 이렇게 수정합니다.

    # my-nginx-chart/templates/service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: {{ include "my-nginx-chart.fullname" . }}
      labels:
        {{- include "my-nginx-chart.labels" . | nindent 4 }}
    spec:
      type: {{ .Values.service.type }}
      ports:
        - port: {{ .Values.service.port }}
          targetPort: http
          protocol: TCP
          name: http
      selector:
        {{- include "my-nginx-chart.selectorLabels" . | nindent 4 }}
    

    이렇게 하면 values.yaml에서 정의한 이미지와 포트가 Deployment와 Service에 동적으로 적용됩니다. 코드를 보면 {{ include "my-nginx-chart.fullname" . }} 같은 구문이 보이는데, 이건 _helpers.tpl에 정의된 재사용 가능한 템플릿 함수거든요. 이런 식으로 차트의 가독성과 유지보수성을 높일 수 있더라고요.

    Helm 차트의 기본적인 구조와 개발, 배포 과정을 한눈에 볼 수 있는 워크플로우입니다.

    4단계: 차트 배포

    이제 우리가 만든 차트를 쿠버네티스 클러스터에 배포해봅시다. helm install 명령어를 사용합니다.

    helm install my-nginx ./my-nginx-chart
    • my-nginx는 우리가 배포하는 릴리즈(Release)의 이름입니다.
    • ./my-nginx-chart는 우리가 개발한 차트가 있는 로컬 경로를 의미합니다.

    명령어를 실행하면 Helm이 차트 템플릿을 렌더링(Rendering)하여 쿠버네티스 API 서버에 리소스들을 생성합니다. 🎉 드디어 Nginx 애플리케이션이 클러스터에 배포된 거예요!

    5단계: 차트 업데이트

    만약 Nginx 버전을 바꾸거나 Replica 수를 늘리고 싶다면 어떻게 할까요? values.yaml을 수정하고 helm upgrade 명령어를 사용하면 돼요.

    예를 들어, my-nginx-chart/values.yaml에서 replicaCount: 3으로 변경한 후:

    helm upgrade my-nginx ./my-nginx-chart

    이렇게 하면 Helm이 변경된 내용만 감지하여 기존 릴리즈를 안전하게 업데이트합니다. 정말 편리하죠? 롤백(Rollback)도 쉽게 할 수 있어서 운영 환경에서 장애 발생 시 빠르게 이전 버전으로 돌아갈 수 있습니다.

    ⚠️ 삽질 방지! Helm 차트 트러블슈팅 팁

    제가 직접 Helm 차트를 개발하면서 가장 많이 겪었던 삽질 중 하나는 바로 values.yaml과 templates 간의 변수 매핑(Mapping) 오류였습니다. YAML 문법은 들여쓰기(Indentation) 하나만 잘못돼도 바로 에러를 뿜어내거든요. 특히 복잡한 구조의 값을 참조할 때 경로를 잘못 지정하거나, 타입(Type)이 맞지 않아 문제가 생기곤 했습니다.

    주요 트러블슈팅 팁

    • helm template --debug --dry-run . 활용: 이 명령어는 실제로 배포하지 않고 차트가 렌더링(Rendering)된 최종 YAML 파일을 보여줍니다. 어디서 어떤 값이 잘못 들어갔는지, 템플릿 문법 오류는 없는지 확인하는 데 정말 큰 도움이 돼요. 제가 가장 많이 쓰는 디버깅(Debugging) 도구입니다.
    • --set 옵션으로 값 오버라이딩(Override): 배포 전에 특정 values.yaml 값을 테스트하고 싶을 때, helm install/upgrade --set image.tag=1.21 my-nginx ./my-nginx-chart 처럼 명령줄에서 값을 직접 오버라이딩해서 테스트해볼 수 있습니다.
    • YAML 문법 검사기 사용: Visual Studio Code 같은 IDE에서 YAML 린터(Linter) 확장을 사용하면 문법 오류를 실시간으로 잡을 수 있거든요.
    • _helpers.tpl 디버깅: _helpers.tpl에 복잡한 로직을 넣었다면, 해당 헬퍼를 사용하는 템플릿 파일에 임시로 {{ include "my-chart.myHelper" . | toYaml }}처럼 넣어서 출력 결과를 확인해보세요.

    배포 확인 및 결과 검증

    차트 배포가 성공적으로 완료되었다면, 이제 쿠버네티스 클러스터에서 Nginx 애플리케이션이 잘 실행되고 있는지 확인해야겠죠?

    배포된 릴리즈 확인

    helm list 명령어로 현재 클러스터에 배포된 모든 Helm 릴리즈를 확인할 수 있습니다.

    helm list
    NAME        NAMESPACE   REVISION    UPDATED                               STATUS      CHART               APP VERSION
    my-nginx    default     1           2023-10-27 10:00:00.123456 +0900 KST deployed    my-nginx-chart-0.1.0 1.25.1
    

    쿠버네티스 리소스 확인

    kubectl 명령어를 사용해서 실제 쿠버네티스 리소스들이 잘 생성되었는지 확인합니다.

    kubectl get pods -l app.kubernetes.io/instance=my-nginx
    NAME                          READY   STATUS    RESTARTS   AGE
    my-nginx-my-nginx-chart-xyz   1/1     Running   0          5m
    
    kubectl get svc -l app.kubernetes.io/instance=my-nginx
    NAME                TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
    my-nginx-my-nginx   ClusterIP   10.X.X.X     <none>        80/TCP    5m
    

    이렇게 Nginx Pod와 Service가 정상적으로 실행되는 것을 볼 수 있습니다. 만약 Ingress를 설정했다면 외부에서 접속도 가능하겠죠!

    쿠버네티스 대시보드에서 my-nginx 릴리즈로 배포된 Nginx 애플리케이션의 Pod와 Service 상태를 확인하는 모습입니다.

    Helm 차트 개발 및 배포 모범 사례

    Helm 차트를 효과적으로 사용하려면 몇 가지 모범 사례(Best Practices)를 따르는 것이 좋습니다. 제가 13년 동안 인프라를 만지면서 느낀 점은, 잘 만들어진 템플릿 하나가 수많은 야근을 줄여준다는 거예요 ㅎㅎ. 다음은 제가 꼭 지키려고 노력하는 모범 사례들입니다.

    • values.yaml을 통한 설정 외부화: 환경별로 달라지는 값들은 반드시 values.yaml에 정의하고, 템플릿에서는 이 값들을 참조하도록 만듭니다. 하드코딩(Hard-coding)은 피해야 합니다.
    • 템플릿 분리 및 재사용: 복잡한 템플릿은 _helpers.tpl에 함수 형태로 분리하여 재사용성을 높이고 가독성을 확보합니다.
    • Semantic Versioning(시맨틱 버저닝): Chart.yaml에 정의된 차트 버전은 MAJOR.MINOR.PATCH 규칙을 따르는 것이 좋습니다. 변경 사항을 명확히 하고 롤백 시 혼란을 줄일 수 있거든요.
    • Liveness/Readiness Probes (활성/준비 프로브) 설정: 애플리케이션의 헬스 체크(Health Check)를 위한 프로브를 반드시 설정하여 안정적인 서비스 운영을 돕습니다.
    • Resource Limits (리소스 제한) 명시: Pod가 사용할 CPU, 메모리 리소스를 제한하여 클러스터의 안정성을 확보합니다. 의도치 않은 리소스 고갈을 방지할 수 있거든요.
    • README.md 문서화: 차트의 사용법, 설정 옵션, 예시 등을 README.md 파일에 상세히 작성하여 다른 사용자들이 쉽게 이해하고 활용할 수 있도록 합니다.
    • 보안 고려: 민감한 정보는 Secret(시크릿)으로 관리하고, RBAC(Role-Based Access Control) 설정을 통해 최소 권한 원칙을 지킵니다.

    Helm 차트 개발 및 배포 시 고려해야 할 핵심 모범 사례들을 인포그래픽으로 정리해봤습니다.

    마무리하며: 효율적인 쿠버네티스 관리의 시작

    오늘은 Helm 차트 개발부터 배포, 그리고 몇 가지 모범 사례까지 쭉 훑어봤습니다. Helm은 쿠버네티스 애플리케이션 배포와 관리를 정말 쉽고 효율적으로 만들어주는 강력한 도구입니다. 처음엔 낯설 수 있지만, 한 번 익숙해지면 수많은 YAML 파일 지옥에서 벗어날 수 있을 거예요. 저도 처음엔 이게 뭔가 싶었는데, 지금은 없으면 허전할 정도로 잘 쓰고 있습니다.

    Helm을 잘 활용하면 CI/CD 파이프라인(Pipeline)과도 쉽게 통합하여 배포 자동화를 구축할 수 있습니다. 여러분의 쿠버네티스 환경이 더욱 스마트해지고 안정적으로 운영되는 데 Helm이 큰 역할을 할 거라고 확신합니다. 다음 글에서는 Helm 차트를 OCI 레지스트리(Registry)에 저장하고 관리하는 방법에 대해 다뤄볼 예정이니, 기대해주세요! 😊

  • [k8s] 쿠버네티스 보안 강화: RBAC와 네트워크 정책 실전 설정 가이드

    [k8s] 쿠버네티스 보안 강화: RBAC와 네트워크 정책 실전 설정 가이드

    쿠버네티스 보안 강화: RBAC, 네트워크 정책 설정 실전 가이드

    안녕하세요, 13년차 서버실 지킴이, 인프라 엔지니어 “13년차의 서버실”입니다. 오늘도 여러분의 튼튼한 인프라를 위한 이야기를 들고 왔습니다.

    도입부: 왜 쿠버네티스 보안이 중요할까요?

    요즘 클라우드 환경에서 쿠버네티스(Kubernetes, k8s)는 정말 대세 중의 대세죠. 저도 홈랩에서 다양한 애플리케이션들을 쿠버네티스 위에 올려서 실험하고 운영하고 있거든요. 그런데 이렇게 편리하고 강력한 도구일수록 보안(Security)은 더욱 중요해집니다.

    솔직히 처음엔 저도 쿠버네티스 보안? 그냥 방화벽 잘 치고, SSL/TLS 잘 적용하면 되는 거 아냐? 라고 쉽게 생각했었어요. 근데 이게 웬걸, 클러스터 내부의 파드(Pod)나 서비스(Service) 간의 통신, 그리고 누가 어떤 리소스(Resource)에 접근할 수 있는지 같은 복잡한 문제들이 산적해 있더라고요. 클라우드 환경은 기존 온프레미스(On-premise)와는 또 다른 공격 표면(Attack Surface)을 가지거든요. 특히나 클러스터 보안(Cluster Security)은 한번 뚫리면 전체 시스템이 위험해질 수 있어서 각별히 신경 써야 합니다.

    그래서 오늘은 쿠버네티스 보안을 강화하기 위한 핵심 두 가지, 바로 RBAC(Role-Based Access Control, 역할 기반 접근 제어)와 네트워크 정책(Network Policy) 설정에 대한 실전 가이드를 준비해 봤습니다. 끊임없이 변화하는 클라우드 환경에서 항상 최신 보안 트렌드를 반영하는 방법을 익혀두면 좋겠죠? 제가 직접 삽질하며 배운 내용들을 아낌없이 공유해 드릴게요!

    쿠버네티스 클러스터는 여러 보안 계층으로 보호됩니다. RBAC는 ‘누가 무엇을 할 수 있는가’를, 네트워크 정책은 ‘누가 누구와 통신할 수 있는가’를 제어하며 핵심적인 역할을 합니다.

    쿠버네티스 RBAC, 왜 필요할까요? (개념 설명)

    RBAC(Role-Based Access Control)는 말 그대로 ‘역할 기반 접근 제어’입니다. 쉽게 말해, “누가 쿠버네티스 클러스터 내에서 무엇을 할 수 있는가?”를 정의하는 메커니즘이에요. 예를 들어, 개발팀은 자기네 네임스페이스(Namespace)에 파드만 배포할 수 있고, 운영팀은 클러스터 전체의 모든 리소스를 관리할 수 있도록 권한을 나누는 거죠.

    RBAC의 주요 구성 요소는 다음과 같습니다:

    • Role (역할): 특정 네임스페이스 내에서 허용되는 권한 집합을 정의합니다. (예: ‘default’ 네임스페이스에서 파드를 조회, 생성할 수 있는 권한)
    • ClusterRole (클러스터 역할): 클러스터 전체에 걸쳐 허용되는 권한 집합을 정의합니다. 네임스페이스에 종속되지 않는 리소스(노드, 퍼시스턴트 볼륨 등)나 모든 네임스페이스에 대한 권한을 부여할 때 사용합니다.
    • RoleBinding (역할 바인딩): Role을 특정 사용자(User), 그룹(Group), 또는 서비스 계정(ServiceAccount)에 연결하여 해당 네임스페이스 내에서 권한을 부여합니다.
    • ClusterRoleBinding (클러스터 역할 바인딩): ClusterRole을 사용자, 그룹, 또는 서비스 계정에 연결하여 클러스터 전체에 걸쳐 권한을 부여합니다.

    이걸 왜 써야 하냐고요? 권한을 너무 많이 주면 보안 사고로 이어지기 쉽고, 너무 적게 주면 개발이나 운영이 힘들어지거든요. 최소 권한 원칙(Principle of Least Privilege)을 지키면서 효율적인 협업 환경을 만드는 데 RBAC가 필수적입니다.

    RBAC 설정, 저도 처음엔 삽질 좀 했습니다 (실전 구현)

    자, 이제 RBAC를 직접 설정해볼까요? 제가 홈랩에서 개발팀과 운영팀의 권한을 분리했던 경험을 바탕으로 설명해 드릴게요. 처음엔 Role과 ClusterRole, Binding의 차이가 헷갈려서 삽질 좀 했습니다 ㅎㅎ.

    1. 개발팀 전용 네임스페이스 생성

    먼저 개발팀이 사용할 네임스페이스를 만듭니다. 여기서는 <code>dev-team이라는 네임스페이스를 사용할게요.

    kubectl create namespace dev-team
    

    2. 개발팀 역할(Role) 정의

    dev-team 네임스페이스 내에서 파드와 디플로이먼트(Deployment)를 조회, 생성, 업데이트, 삭제할 수 있는 권한을 부여하는 Role을 만듭니다. 이 Role은 dev-team 네임스페이스에만 적용됩니다.

    # dev-team-role.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: dev-team-pod-deployment-manager
      namespace: dev-team
    rules:
    - apiGroups: ["", "apps"]
      resources: ["pods", "deployments"]
      verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
    - apiGroups: [""]
      resources: ["pods/log"]
      verbs: ["get"]
    

    파일을 저장하고 적용합니다.

    kubectl apply -f dev-team-role.yaml
    

    3. 개발팀 서비스 계정(ServiceAccount) 생성 및 RoleBinding

    실제 사용자 대신 서비스 계정(ServiceAccount)을 만들어서 권한을 부여하는 게 일반적입니다. 여기서는 dev-user-sa라는 서비스 계정을 만들고, 위에서 정의한 Role을 이 서비스 계정에 바인딩(Binding)합니다.

    # dev-team-sa.yaml
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: dev-user-sa
      namespace: dev-team
    ---
    # dev-team-rolebinding.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: dev-user-pod-deployment-binding
      namespace: dev-team
    subjects:
    - kind: ServiceAccount
      name: dev-user-sa
      namespace: dev-team
    roleRef:
      kind: Role
      name: dev-team-pod-deployment-manager
      apiGroup: rbac.authorization.k8s.io
    

    파일을 저장하고 적용합니다.

    kubectl apply -f dev-team-sa.yaml
    

    이제 dev-user-sa 서비스 계정은 dev-team 네임스페이스에서 파드와 디플로이먼트를 관리할 수 있게 됩니다. 이 서비스 계정의 토큰을 활용해 개발자는 해당 권한으로 클러스터에 접근할 수 있어요. 💡 팁: 실제 사용자에게는 OIDC(OpenID Connect) 연동을 통해 사용자 인증 후 RoleBinding을 연결하는 경우가 많습니다.

    쿠버네티스 RBAC는 사용자/서비스 계정, Role/ClusterRole, 그리고 이들을 연결하는 RoleBinding/ClusterRoleBinding으로 구성되어 권한 흐름을 제어합니다.

    네트워크 정책(Network Policy), 통제된 소통의 시작 (개념 설명)

    RBAC가 “누가 무엇을 할 수 있는가”를 정의한다면, 네트워크 정책(Network Policy)은 “쿠버네티스 파드(Pod) 간에 누가 누구와 통신할 수 있는가”를 정의하는 기능입니다. 기본적으로 쿠버네티스 클러스터 내의 모든 파드는 서로 아무런 제약 없이 통신할 수 있어요. 하지만 프로덕션 환경에서는 이게 너무 위험할 수 있겠죠?

    예를 들어, 웹 프론트엔드 파드가 데이터베이스 파드에 직접 접근할 필요는 없을 거예요. API 백엔드 파드만 DB에 접근해야 하고요. 이럴 때 네트워크 정책을 사용해서 파드 간의 통신을 세밀하게 제어할 수 있습니다.

    네트워크 정책은 특정 파드(podSelector)에 적용되며, 해당 파드로 들어오는 트래픽(Ingress)과 나가는 트래픽(Egress)을 정의할 수 있습니다. 한 번 정책이 적용되면, 명시적으로 허용된 트래픽 외에는 모두 차단됩니다. 이게 핵심이에요!

    Network Policy 적용, 드디어 통신이 막히네요! (실전 구현)

    이번에는 간단한 웹 서비스 환경에서 네트워크 정책을 적용해 볼게요. 프론트엔드, 백엔드, 데이터베이스 파드가 있다고 가정해 봅시다. 목표는 다음과 같습니다:

    • 프론트엔드는 백엔드에만 접근 가능해야 합니다.
    • 백엔드는 프론트엔드와 데이터베이스에만 접근 가능해야 합니다.
    • 데이터베이스는 그 어떤 파드로부터의 Egress 트래픽도 허용하지 않습니다 (오직 백엔드로부터의 Ingress만 허용).

    먼저 예시 파드들을 생성합니다. 각각 app: frontend, app: backend, app: database 라벨을 가지고 있다고 가정할게요.

    1. 기본 차단 정책(Default Deny) (선택적)

    특정 네임스페이스의 모든 Ingress/Egress 트래픽을 기본적으로 차단하는 정책입니다. 이렇게 해두면 명시적으로 허용한 트래픽만 통과하게 되므로 보안을 강화할 수 있어요. 저는 보통 이걸 먼저 적용하고 시작합니다.

    # default-deny.yaml
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: default-deny-all
      namespace: default
    spec:
      podSelector: {}
      policyTypes:
      - Ingress
      - Egress
    
    kubectl apply -f default-deny.yaml
    

    ⚠️ 주의: 이 정책을 적용하면 해당 네임스페이스의 모든 파드 통신이 끊기므로, 이후 허용 정책을 바로 적용해야 합니다!

    2. 백엔드 접근 허용 정책

    프론트엔드 파드(app: frontend)가 백엔드 파드(app: backend)에 접근하도록 허용하는 정책입니다. 백엔드 파드 입장에서는 프론트엔드로부터의 Ingress를 허용해야겠죠?

    # backend-allow-frontend.yaml
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: backend-allow-frontend
      namespace: default
    spec:
      podSelector:
        matchLabels:
          app: backend
      policyTypes:
      - Ingress
      ingress:
      - from:
        - podSelector:
            matchLabels:
              app: frontend
        ports:
        - protocol: TCP
          port: 8080 # 백엔드 서비스 포트
    
    kubectl apply -f backend-allow-frontend.yaml
    

    3. 데이터베이스 접근 허용 정책

    백엔드 파드(app: backend)가 데이터베이스 파드(app: database)에 접근하도록 허용하는 정책입니다. 데이터베이스 파드 입장에서는 백엔드로부터의 Ingress를 허용합니다.

    # database-allow-backend.yaml
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: database-allow-backend
      namespace: default
    spec:
      podSelector:
        matchLabels:
          app: database
      policyTypes:
      - Ingress
      ingress:
      - from:
        - podSelector:
            matchLabels:
              app: backend
        ports:
        - protocol: TCP
          port: 5432 # 데이터베이스 포트 (예: PostgreSQL)
    
    kubectl apply -f database-allow-backend.yaml
    

    이제 app: backend 파드는 app: frontend 파드와 app: database 파드에만 접근할 수 있게 됩니다. app: frontend 파드는 app: backend 파드에만 접근할 수 있고요. 다른 파드들과의 통신은 모두 차단됩니다. 드디어 통제된 소통이 시작되는 거죠!

    네트워크 정책을 통해 프론트엔드, 백엔드, 데이터베이스 파드 간의 통신 흐름을 세밀하게 제어하여 불필요한 접근을 차단하는 모습입니다.

    ⚠️ RBAC & Network Policy 설정 시 주의할 점 (트러블슈팅)

    제가 직접 겪었던 삽질 경험들을 바탕으로 몇 가지 주의사항을 알려드릴게요.

    RBAC 관련 주의사항

    • 최소 권한 원칙(Principle of Least Privilege): 항상 필요한 최소한의 권한만 부여해야 합니다. *(모든 권한)을 남발하면 안 돼요. 저도 처음에 귀찮아서 *로 줬다가 나중에 감사(Audit) 때 식은땀 좀 흘렸습니다 😅.
    • ClusterRole 오남용 금지: ClusterRole은 클러스터 전체에 영향을 미치기 때문에 정말 신중하게 사용해야 합니다. 웬만하면 Role과 RoleBinding으로 네임스페이스 단위로 권한을 관리하는 것이 좋습니다.
    • ServiceAccount 권한 관리: 파드 내부에서 쿠버네티스 API와 통신할 때 ServiceAccount가 사용됩니다. 각 파드에 적절한 ServiceAccount를 할당하고, 해당 ServiceAccount에 최소한의 권한을 가진 RoleBinding을 연결해야 합니다.
    • kubectl auth can-i 활용: 특정 서비스 계정이나 사용자가 어떤 작업을 할 수 있는지 확인하는 데 이 명령어가 정말 유용합니다. 설정 후 꼭 확인해 보세요!

    Network Policy 관련 주의사항

    • Default Deny 정책의 영향: 네임스페이스에 podSelector: {}와 policyTypes: [Ingress, Egress]가 포함된 Network Policy를 적용하면 해당 네임스페이스의 모든 파드 통신이 기본적으로 차단됩니다. 이때 시스템 파드나 kube-dns 같은 핵심 파드의 통신까지 막힐 수 있으니, 적용 전에 충분히 테스트하고 필요한 허용 정책을 바로 이어서 적용해야 합니다. 저도 이걸 모르고 적용했다가 클러스터가 먹통이 돼서 밤샘 삽질 좀 했습니다…
    • 라벨링(Labeling)의 중요성: Network Policy는 파드의 라벨을 기반으로 동작합니다. 따라서 일관성 있고 의미 있는 라벨을 파드에 잘 붙이는 것이 매우 중요합니다. 라벨링이 엉망이면 정책 적용이 어렵거나 잘못 적용될 수 있어요.
    • CNI 플러그인 확인: 모든 CNI(Container Network Interface) 플러그인이 Network Policy를 지원하는 것은 아닙니다. Calico, Cilium, Weave Net 등 대부분의 인기 있는 CNI는 지원하지만, 사용 중인 CNI가 지원하는지 확인해야 합니다.

    확인하고 넘어가시죠! (검증/결과)

    설정만 하고 끝내면 안 되겠죠? 제대로 적용되었는지 꼭 확인해야 합니다.

    RBAC 검증

    특정 서비스 계정이 특정 리소스에 대해 어떤 권한을 가지고 있는지 확인하려면 kubectl auth can-i 명령어를 사용합니다.

    # dev-team 네임스페이스에서 dev-user-sa 서비스 계정이 파드를 생성할 수 있는지 확인
    kubectl auth can-i create pods --as=system:serviceaccount:dev-team:dev-user-sa -n dev-team
    
    # dev-team 네임스페이스에서 dev-user-sa 서비스 계정이 노드를 조회할 수 있는지 확인 (권한 없음 예상)
    kubectl auth can-i get nodes --as=system:serviceaccount:dev-team:dev-user-sa -n dev-team
    

    RoleBinding이나 ClusterRoleBinding의 상세 정보를 조회하여 어떤 Role이 누구에게 바인딩되었는지도 확인할 수 있습니다.

    kubectl describe rolebinding dev-user-pod-deployment-binding -n dev-team
    

    Network Policy 검증

    먼저 적용된 네트워크 정책 목록을 확인합니다.

    kubectl get netpol -n default
    

    그리고 실제 파드에 접속해서 curl 등으로 통신 테스트를 해보는 것이 가장 확실합니다. 예를 들어, 프론트엔드 파드에서 데이터베이스 파드로 접속을 시도하면 실패해야 합니다.

    # 프론트엔드 파드에서 백엔드 파드로 요청 (성공 예상)
    k exec -it <frontend-pod-name> -- curl backend-service:8080
    
    # 프론트엔드 파드에서 데이터베이스 파드로 요청 (실패 예상)
    k exec -it <frontend-pod-name> -- curl database-service:5432
    

    이런 식으로 실제 시나리오를 만들어 검증해보면 됩니다. 🎉 드디어 됐다! 라고 외칠 때의 그 쾌감은 정말 최고죠!

    RBAC는 ‘누가 무엇을 할 수 있는가’를, 네트워크 정책은 ‘누가 누구와 통신할 수 있는가’를 제어하는 핵심적인 쿠버네티스 보안 메커니즘입니다.

    마무리하며: 안전한 쿠버네티스 클러스터를 향한 여정

    오늘은 쿠버네티스 보안(Kubernetes Security)을 강화하기 위한 핵심 요소인 RBAC 설정(Role-Based Access Control)과 네트워크 정책(Network Policy)에 대해 자세히 알아봤습니다. RBAC는 인적 오류나 악의적인 접근으로부터 클러스터 리소스를 보호하고, 네트워크 정책은 파드 간의 불필요한 통신을 차단하여 내부 공격 표면을 줄이는 데 큰 역할을 합니다.

    이 두 가지는 k8s 보안 가이드(k8s Security Guide)의 가장 기본적인 출발점이라고 할 수 있습니다. 물론 쿠버네티스 보안은 이것이 다가 아닙니다. 시크릿(Secret) 관리, Pod Security Admission(PSA), 컨테이너 이미지 보안 스캐닝, 그리고 클러스터 로깅 및 모니터링 등 고려해야 할 부분이 정말 많아요.

    하지만 오늘 다룬 RBAC와 네트워크 정책만 제대로 적용해도 여러분의 클러스터 보안(Cluster Security) 수준은 한 단계 높아질 겁니다. 제가 직접 겪어보니, 처음에는 어렵고 복잡해 보여도 하나하나 적용해 보면서 얻는 경험이 가장 중요하더라고요. 여러분도 이 가이드를 바탕으로 안전하고 튼튼한 쿠버네티스 클러스터를 만들어 가시길 응원합니다!

    다음 글에서는 쿠버네티스 시크릿 관리나 Pod Security Admission에 대해 더 깊이 다뤄볼 예정이니, 많은 관심 부탁드립니다!

  • [Cloud] GitHub Actions 모노레포: 매트릭스 빌드로 CI/CD 최적화하기

    모노레포에서 CI/CD, 이게 생각보다 복잡하더라고요

    처음 팀에서 모노레포(Monorepo)로 전환했을 때 솔직히 자신 있었거든요. “뭐, CI/CD 파이프라인 하나 잘 짜면 되는 거 아니야?” 했는데… 현실은 달랐습니다. 프론트엔드, 백엔드 API, 공통 라이브러리가 한 레포에 다 들어가 있으니까 서로 상관없는 변경사항인데도 전체 빌드가 돌고, 빌드 시간은 점점 길어지고. 결국 개발자들이 “PR 올리면 20분 기다려야 해요”라고 불만을 터뜨리기 시작했죠.

    그때 제대로 파고든 게 GitHub Actions 모노레포 환경에서의 매트릭스 빌드(Matrix Build) 전략이었습니다. 인프라 업무를 오래 하면서 CI/CD 툴을 여럿 써봤는데, GitHub Actions의 매트릭스 전략은 모노레포 환경에서 정말 강력하더라고요. 오늘은 그 경험을 바탕으로 실전에서 바로 쓸 수 있는 가이드를 공유해 드리려 합니다.

    ▲ GitHub Actions 모노레포 환경의 전체 CI/CD 파이프라인 흐름 — 변경된 패키지만 선택적으로 빌드/테스트하는 구조

    GitHub Actions 모노레포와 매트릭스 빌드, 쉽게 이해해 봅시다

    모노레포(Monorepo)란?

    쉽게 말해, 여러 개의 프로젝트나 패키지를 하나의 Git 저장소에서 관리하는 방식입니다. 예전에는 프로젝트마다 레포를 따로 만드는 멀티레포(Multi-repo) 방식이 흔했는데, 요즘은 Google, Meta, Microsoft 같은 대형 기업들도 모노레포를 적극 활용하고 있죠.

    • ✅ 코드 공유와 재사용이 쉬움
    • ✅ 의존성 관리가 일원화됨
    • ✅ 변경 사항의 영향 범위를 한눈에 파악 가능
    • ⚠️ CI/CD 파이프라인 설계가 까다로워짐
    • ⚠️ 레포 크기가 커질수록 빌드 시간이 늘어날 수 있음

    GitHub Actions 매트릭스 빌드(Matrix Build)란?

    GitHub Actions의 strategy.matrix 기능은 여러 변수 조합에 대해 병렬로 잡(Job)을 실행하는 기능입니다. 예를 들어 Node.js 16, 18, 20 버전에서 동시에 테스트를 돌린다거나, 여러 패키지를 동시에 빌드할 때 쓰죠. 모노레포에서는 이걸 “변경된 패키지 목록”과 조합해서 쓰면 진짜 강력해집니다.

    방식 빌드 대상 빌드 시간 장단점
    전체 빌드 (Naive) 모든 패키지 길다 설정 단순, 시간 낭비 큼
    매트릭스 + 변경 감지 변경된 패키지만 짧다 설정 복잡, 효율 극대화
    캐시 + 매트릭스 변경된 패키지만 매우 짧다 가장 최적화된 방식

    GitHub Actions 모노레포 CI/CD 실전 구현: 단계별로 따라해 보세요

    1단계: 레포 구조 잡기

    먼저 제가 실제로 운영하는 모노레포 구조를 보여드릴게요. 완벽할 필요는 없고, 이런 식으로 패키지가 분리되어 있으면 됩니다.

    my-monorepo/
    ├── .github/
    │   └── workflows/
    │       ├── ci.yml          # 메인 CI 워크플로우
    │       └── detect-changes.yml
    ├── packages/
    │   ├── frontend/           # React 프론트엔드
    │   │   ├── package.json
    │   │   └── src/
    │   ├── api-server/         # Node.js API 서버
    │   │   ├── package.json
    │   │   └── src/
    │   └── shared-lib/         # 공통 라이브러리
    │       ├── package.json
    │       └── src/
    ├── package.json            # 루트 package.json (워크스페이스 설정)
    └── pnpm-workspace.yaml     # pnpm 워크스페이스 설정

    2단계: 변경된 패키지 감지하기

    여기가 핵심입니다. 어떤 패키지가 바뀌었는지 감지해야 매트릭스를 동적으로 구성할 수 있거든요. 저는 git diff와 간단한 스크립트를 조합해서 씁니다.

    # .github/workflows/ci.yml
    name: Monorepo CI
    
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main, develop]
    
    jobs:
      # 1. 변경된 패키지 목록을 동적으로 생성
      detect-changes:
        runs-on: ubuntu-latest
        outputs:
          matrix: ${{ steps.set-matrix.outputs.matrix }}
          has-changes: ${{ steps.set-matrix.outputs.has-changes }}
        steps:
          - name: Checkout
            uses: actions/checkout@v4
            with:
              fetch-depth: 0  # 전체 히스토리 필요 (diff 비교용)
    
          - name: Detect changed packages
            id: set-matrix
            run: |
              # PR이면 base 브랜치와 비교, push면 이전 커밋과 비교
              if [ "${{ github.event_name }}" = "pull_request" ]; then
                BASE_SHA=${{ github.event.pull_request.base.sha }}
              else
                BASE_SHA=${{ github.event.before }}
              fi
    
              CHANGED_PACKAGES='[]'
    
              # packages/ 하위 디렉토리를 순회하며 변경 여부 확인
              for pkg_dir in packages/*/; do
                pkg_name=$(basename $pkg_dir)
                changed=$(git diff --name-only $BASE_SHA ${{ github.sha }} -- $pkg_dir | head -1)
                if [ -n "$changed" ]; then
                  CHANGED_PACKAGES=$(echo $CHANGED_PACKAGES | jq --arg p "$pkg_name" '. + [$p]')
                fi
              done
    
              echo "Changed packages: $CHANGED_PACKAGES"
    
              if [ "$CHANGED_PACKAGES" = "[]" ]; then
                echo "has-changes=false" >> $GITHUB_OUTPUT
              else
                echo "has-changes=true" >> $GITHUB_OUTPUT
              fi
    
              echo "matrix={\"package\":$CHANGED_PACKAGES}" >> $GITHUB_OUTPUT

    💡 팁: fetch-depth: 0 설정이 없으면 git diff가 제대로 안 됩니다. 처음에 이거 빠뜨려서 삽질 좀 했어요. 얕은 클론(shallow clone)에서는 비교 기준이 되는 커밋을 못 찾거든요.

    3단계: 매트릭스 빌드 잡 구성

    이제 감지된 패키지 목록으로 매트릭스를 구성합니다. needs 키워드로 앞 단계에 의존하게 만들고, if 조건으로 변경 사항이 없을 때는 스킵하게 하면 됩니다.

      # 2. 변경된 패키지만 병렬로 빌드 & 테스트
      build-and-test:
        needs: detect-changes
        if: needs.detect-changes.outputs.has-changes == 'true'
        runs-on: ubuntu-latest
        strategy:
          matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
          fail-fast: false  # 하나 실패해도 나머지는 계속 실행
        steps:
          - name: Checkout
            uses: actions/checkout@v4
    
          - name: Setup Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'pnpm'
    
          - name: Install pnpm
            uses: pnpm/action-setup@v3
            with:
              version: 8
    
          - name: Install dependencies
            run: pnpm install --frozen-lockfile
    
          # 캐시 설정 — 빌드 시간 단축에 핵심!
          - name: Cache build artifacts
            uses: actions/cache@v4
            with:
              path: packages/${{ matrix.package }}/.next
              key: ${{ runner.os }}-${{ matrix.package }}-${{ hashFiles('packages/${{ matrix.package }}/package.json') }}
              restore-keys: |
                ${{ runner.os }}-${{ matrix.package }}-
    
          - name: Build package
            run: pnpm --filter ${{ matrix.package }} build
    
          - name: Run tests
            run: pnpm --filter ${{ matrix.package }} test
    
          - name: Upload test results
            if: always()  # 테스트 실패해도 결과는 업로드
            uses: actions/upload-artifact@v4
            with:
              name: test-results-${{ matrix.package }}
              path: packages/${{ matrix.package }}/test-results/

    ▲ 매트릭스 빌드가 실행될 때 GitHub Actions 화면 — 변경된 패키지들이 병렬로 동시에 빌드되는 것을 확인할 수 있음

    4단계: 의존성 있는 패키지 처리

    근데 여기서 문제가 하나 생깁니다. shared-lib가 변경되면 이걸 쓰는 frontend나 api-server도 다시 빌드해야 하거든요. 이 의존성 체인을 처리하는 게 모노레포 CI/CD의 진짜 어려운 부분입니다.

    #!/bin/bash
    # scripts/detect-affected.sh
    # 변경된 패키지 + 그에 의존하는 패키지까지 모두 감지
    
    CHANGED_DIRECT=$1  # 직접 변경된 패키지 (JSON 배열 문자열)
    AFFECTED_PACKAGES=$CHANGED_DIRECT
    
    # 각 패키지의 package.json을 읽어서 의존성 체인 추적
    for pkg_dir in packages/*/; do
      pkg_name=$(basename $pkg_dir)
      pkg_json="$pkg_dir/package.json"
    
      if [ -f "$pkg_json" ]; then
        # 이 패키지가 변경된 패키지에 의존하는지 확인
        for changed in $(echo $CHANGED_DIRECT | jq -r '.[]'); do
          deps=$(cat $pkg_json | jq -r '.dependencies // {} | keys[]' 2>/dev/null)
          if echo "$deps" | grep -q "$changed"; then
            # 의존성이 있으면 affected 목록에 추가
            AFFECTED_PACKAGES=$(echo $AFFECTED_PACKAGES | jq --arg p "$pkg_name" '. + [$p] | unique')
          fi
        done
      fi
    done
    
    echo $AFFECTED_PACKAGES

    이 스크립트를 detect-changes 잡에서 호출하면 됩니다. 물론 Nx나 Turborepo 같은 모노레포 전용 툴을 쓰면 이런 의존성 그래프를 자동으로 처리해 주기도 하죠. 규모가 커지면 Turborepo로 마이그레이션하는 걸 고려하고 있어요.

    ⚠️ 실제로 겪은 GitHub Actions 모노레포 트러블슈팅

    문제 1: BASE_SHA가 비어있는 경우

    첫 번째 커밋이거나 새 브랜치를 push할 때 github.event.before가 0000000000000000000000000000000000000000(제로 SHA)로 오는 경우가 있습니다. 이때 git diff가 에러를 뱉거든요.

          - name: Detect changed packages
            id: set-matrix
            run: |
              # 제로 SHA 처리
              BASE_SHA=${{ github.event.before }}
              ZERO_SHA="0000000000000000000000000000000000000000"
    
              if [ "$BASE_SHA" = "$ZERO_SHA" ] || [ -z "$BASE_SHA" ]; then
                # 첫 커밋이면 모든 패키지를 빌드 대상으로
                echo "First push or new branch — building all packages"
                ALL_PACKAGES=$(ls packages/ | jq -R -s -c 'split("\\n") | map(select(length > 0))')
                echo "matrix={\"package\":$ALL_PACKAGES}" >> $GITHUB_OUTPUT
                echo "has-changes=true" >> $GITHUB_OUTPUT
              else
                # 기존 로직 실행
                # ... (이전 코드)
                :
              fi

    문제 2: 매트릭스가 빈 배열일 때 워크플로우 에러

    변경된 패키지가 없어서 매트릭스가 빈 배열 []이 되면 GitHub Actions가 에러를 냅니다. has-changes 출력값으로 if 조건을 걸어두는 게 중요한 이유가 여기 있어요. 또한 최종 상태 체크 잡을 별도로 두는 것도 좋은 패턴입니다.

      # 모든 빌드 완료 후 최종 상태 확인 잡
      ci-success:
        needs: [detect-changes, build-and-test]
        if: always()
        runs-on: ubuntu-latest
        steps:
          - name: Check all jobs status
            run: |
              if [ "${{ needs.detect-changes.result }}" != "success" ]; then
                echo "detect-changes job failed"
                exit 1
              fi
    
              # build-and-test가 스킵됐거나 성공한 경우 모두 OK
              if [ "${{ needs.build-and-test.result }}" = "failure" ]; then
                echo "Some builds failed!"
                exit 1
              fi
    
              echo "All checks passed! 🎉"

    문제 3: pnpm 워크스페이스에서 filter가 안 먹힐 때

    패키지 이름이 package.json의 name 필드와 달라서 --filter가 안 먹히는 경우가 있었습니다. 디렉토리명으로 필터하려면 --filter ./packages/패키지명 형식을 써야 합니다.

          - name: Build package
            run: |
              # 디렉토리 경로로 필터 (이름 불일치 문제 방지)
              pnpm --filter ./packages/${{ matrix.package }} build

    전체 GitHub Actions 워크플로우 완성본

    지금까지 설명한 내용을 하나로 합친 완성본입니다. 실제로 제가 운영 중인 설정을 기반으로 정리했어요.

    # .github/workflows/ci.yml
    name: Monorepo CI/CD
    
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main, develop]
    
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: true  # 같은 브랜치 중복 실행 방지
    
    jobs:
      detect-changes:
        runs-on: ubuntu-latest
        outputs:
          matrix: ${{ steps.set-matrix.outputs.matrix }}
          has-changes: ${{ steps.set-matrix.outputs.has-changes }}
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
    
          - name: Detect changed packages
            id: set-matrix
            run: |
              if [ "${{ github.event_name }}" = "pull_request" ]; then
                BASE_SHA=${{ github.event.pull_request.base.sha }}
              else
                BASE_SHA=${{ github.event.before }}
              fi
    
              ZERO_SHA="0000000000000000000000000000000000000000"
              CHANGED_PACKAGES='[]'
    
              if [ "$BASE_SHA" = "$ZERO_SHA" ] || [ -z "$BASE_SHA" ]; then
                CHANGED_PACKAGES=$(ls packages/ | jq -R -s -c 'split("\\n") | map(select(length > 0))')
              else
                for pkg_dir in packages/*/; do
                  pkg_name=$(basename $pkg_dir)
                  changed=$(git diff --name-only $BASE_SHA ${{ github.sha }} -- $pkg_dir | head -1)
                  if [ -n "$changed" ]; then
                    CHANGED_PACKAGES=$(echo $CHANGED_PACKAGES | jq --arg p "$pkg_name" '. + [$p]')
                  fi
                done
              fi
    
              echo "Affected packages: $CHANGED_PACKAGES"
    
              if [ "$CHANGED_PACKAGES" = "[]" ]; then
                echo "has-changes=false" >> $GITHUB_OUTPUT
              else
                echo "has-changes=true" >> $GITHUB_OUTPUT
              fi
              echo "matrix={\"package\":$CHANGED_PACKAGES}" >> $GITHUB_OUTPUT
    
      build-and-test:
        needs: detect-changes
        if: needs.detect-changes.outputs.has-changes == 'true'
        runs-on: ubuntu-latest
        strategy:
          matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
          fail-fast: false
        steps:
          - uses: actions/checkout@v4
    
          - uses: pnpm/action-setup@v3
            with:
              version: 8
    
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'pnpm'
    
          - name: Install dependencies
            run: pnpm install --frozen-lockfile
    
          - name: Cache build
            uses: actions/cache@v4
            with:
              path: |
                packages/${{ matrix.package }}/.next
                packages/${{ matrix.package }}/dist
              key: ${{ runner.os }}-build-${{ matrix.package }}-${{ github.sha }}
              restore-keys: |
                ${{ runner.os }}-build-${{ matrix.package }}-
    
          - name: Build
            run: pnpm --filter ./packages/${{ matrix.package }} build
    
          - name: Test
            run: pnpm --filter ./packages/${{ matrix.package }} test
    
          - name: Upload artifacts
            if: always()
            uses: actions/upload-artifact@v4
            with:
              name: results-${{ matrix.package }}
              path: packages/${{ matrix.package }}/test-results/
              retention-days: 7
    
      ci-success:
        needs: [detect-changes, build-and-test]
        if: always()
        runs-on: ubuntu-latest
        steps:
          - name: Final status check
            run: |
              if [ "${{ needs.build-and-test.result }}" = "failure" ]; then
                echo "Build failed!"
                exit 1
              fi
              echo "CI passed! 🎉"

    ▲ 완성된 CI/CD 파이프라인 실행 결과 — detect-changes → build-and-test (병렬) → ci-success 순서로 실행되는 것을 확인

    ✅ 결과: 빌드 시간이 얼마나 줄었을까요?

    이 방식을 적용하고 나서 체감이 꽤 컸습니다. 물론 레포마다 상황이 다르겠지만, 제 경우엔 이랬어요.

    • 📦 전체 패키지 수: 6개
    • ⏱️ 기존 전체 빌드 시간: 약 18~22분
    • ⚡ 매트릭스 빌드 적용 후 (1~2개 패키지 변경 시): 4~7분
    • 🎉 개발자들 PR 머지 속도 체감상 확 빨라짐

    특히 공통 라이브러리 변경이 없는 일반적인 기능 개발 PR에서 효과가 컸습니다. 프론트엔드만 건드렸을 때 백엔드 빌드까지 기다릴 필요가 없으니까요. 당연한 말 같지만, 이걸 자동화로 구현하는 게 핵심이죠.

    추가로 concurrency 설정으로 같은 브랜치에서 중복 실행을 방지한 것도 빌드 큐 낭비를 줄이는 데 도움이 됐습니다. GitHub Actions 무료 플랜은 동시 실행 잡 수에 제한이 있으니까요.

    💡 CI/CD 최적화 추가 팁

    캐시 전략으로 빌드 시간 단축

    • actions/cache의 key를 package.json 해시 기반으로 설정하면 의존성이 바뀔 때만 캐시가 무효화됩니다
    • 빌드 결과물(dist, .next)도 캐시하면 재빌드 시 시간 절약 가능
    • 캐시 hit rate는 Actions 실행 로그에서 확인 가능 — 이걸 모니터링하는 습관을 들이세요

    Turborepo와 함께 쓰기

    패키지가 10개 이상으로 늘어나면 Turborepo(터보레포) 같은 모노레포 빌드 시스템을 도입하는 게 좋습니다. 의존성 그래프 분석, 원격 캐시, 병렬 실행 최적화를 자동으로 해주거든요. 다음 글에서 Turborepo와 GitHub Actions를 함께 쓰는 방법을 다룰 예정입니다.

    Branch Protection Rules 연동

    앞서 만든 ci-success 잡을 GitHub 브랜치 보호 규칙(Branch Protection Rules)의 필수 상태 체크로 등록해 두세요. 매트릭스 빌드가 스킵된 경우에도 ci-success는 항상 실행되므로, PR 머지 조건으로 쓰기에 딱입니다.

    ▲ GitHub Actions 모노레포 CI/CD 최적화 전략 요약 — 변경 감지, 매트릭스 빌드, 캐시 전략의 세 가지 축

    자주 묻는 질문

    Q. Nx를 쓰면 이런 GitHub Actions 설정이 필요 없나요?

    Nx(엔엑스)를 쓰면 nx affected 명령어로 변경된 패키지 감지를 자동화할 수 있습니다. 다만 Nx 자체 학습 곡선이 있고, 기존 프로젝트에 도입하는 비용이 있어요. 팀 규모와 프로젝트 복잡도에 따라 선택하시면 됩니다. 오늘 설명한 방식은 외부 툴 없이 GitHub Actions만으로 구현할 수 있다는 게 장점이에요.

    Q. PR이 아닌 직접 push에서도 잘 되나요?

    네, 됩니다. 다만 github.event.before가 제로 SHA로 오는 케이스(새 브랜치 최초 push)를 반드시 처리해야 합니다. 위 코드에 그 처리가 포함되어 있으니 참고하세요.

    Q. 모노레포에서 Docker 이미지 빌드도 같은 방식으로?

    동일한 패턴으로 적용 가능합니다. 매트릭스의 각 항목에 대해 docker build를 실행하고 레지스트리에 push하면 되죠. 이 내용은 이전 글에서 다룬 Docker 멀티 스테이지 빌드 최적화와 함께 보시면 더 이해가 쉬울 거예요.

    마무리: GitHub Actions 모노레포 CI/CD, 처음엔 복잡해 보여도

    처음에 이 구조를 짤 때 “이게 맞나?” 싶은 순간이 여러 번 있었습니다. 특히 동적 매트릭스 생성 부분에서 fromJson이 제대로 안 먹혀서 한참 헤맸던 기억이 나네요. 근데 한번 제대로 잡아두면 이후에는 거의 손댈 일이 없어요.

    핵심을 정리하면 이렇습니다.

    1. 변경 감지: git diff로 변경된 패키지만 추려냄
    2. 동적 매트릭스: 감지 결과를 JSON으로 출력해서 매트릭스에 주입
    3. 병렬 빌드: strategy.matrix로 변경된 패키지들을 동시에 빌드
    4. 캐시 활용: actions/cache로 반복 빌드 시간 단축
    5. 안전장치: ci-success 잡으로 브랜치 보호 규칙과 연동

    혹시 레포 규모가 더 크거나, Turborepo/Nx 같은 툴과 함께 쓰는 방법이 궁금하신 분들은 댓글로 남겨주세요. 다음 글에서 다뤄볼게요. 오늘도 긴 글 읽어주셔서 감사합니다! 🎉

  • [Game] Steam Deck 외부 모니터 연결 및 최적화 가이드: 휴대용 게임 환경 구축

    [Game] Steam Deck 외부 모니터 연결 및 최적화 가이드: 휴대용 게임 환경 구축

    Steam Deck 외부 모니터 연결 및 최적화 가이드: 휴대용 게임 환경 구축

    안녕하세요, 13년차 서버실에서 인프라 삽질 중인 엔지니어입니다. 오늘은 많은 분들이 궁금해하실 법한 Steam Deck 외부 모니터 연결에 대한 이야기를 풀어볼까 합니다. 휴대용 게임기로서 Steam Deck의 매력은 정말 엄청나죠. 어디서든 AAA급 게임을 즐길 수 있다는 건 정말 혁신적인 경험이거든요.

    근데 집에서 게임을 하다 보면, 가끔씩 ‘아, 이 게임을 좀 더 큰 화면으로 즐길 수 있다면 얼마나 좋을까?’ 하는 생각이 들 때가 있습니다. 저도 그랬거든요. 작은 화면으로 컨트롤러 잡고 게임하는 것도 좋지만, 고해상도 모니터나 TV에 연결해서 웅장하게 즐기고 싶은 마음은 게이머라면 누구나 같을 겁니다. 그래서 제가 직접 Steam Deck을 외부 모니터에 연결하고, 최적의 게임 환경을 구축하기 위해 삽질했던 경험들을 솔직하게 공유해드리려고 합니다. 혹시 저처럼 큰 화면에서 Steam Deck을 즐기고 싶으셨던 분들이라면, 오늘 이 글이 큰 도움이 될 겁니다!

    Steam Deck을 외부 모니터에 연결하여 사용하는 모습입니다. 휴대성과 큰 화면의 즐거움을 동시에 누릴 수 있죠.

    개념 설명: Steam Deck 독 모드(Dock Mode), 핵심은 USB-C

    Steam Deck 외부 모니터 연결의 핵심은 바로 독(Dock)입니다. Steam Deck은 USB-C 포트를 통해 다양한 기능을 확장할 수 있거든요. 특히 DisplayPort Alternate Mode (줄여서 DP Alt Mode, 디스플레이포트 대체 모드)와 Power Delivery (PD, 전원 공급) 기능이 통합되어 있어서, USB-C 케이블 하나로 영상 출력과 전원 공급을 동시에 할 수 있습니다. 쉽게 말해, 노트북처럼 USB-C 독에 연결하면 외부 모니터, 키보드, 마우스는 물론이고 유선 LAN까지 한 번에 연결할 수 있는 거죠.

    이런 독을 사용하면 Steam Deck을 마치 미니 데스크톱처럼 활용할 수 있게 되는데, 이걸 흔히 Steam Deck 독 모드(Dock Mode)라고 부릅니다. 공식 독도 있고, 수많은 서드파티 독 제품들이 시중에 나와 있어요. 어떤 독을 선택하든 중요한 건 다음과 같습니다:

    • 영상 출력 포트: HDMI (High-Definition Multimedia Interface) 또는 DisplayPort (디스플레이포트)가 필수입니다. 모니터 규격에 맞춰 선택하세요.
    • USB-A 포트: 키보드, 마우스, 외장하드 등 주변기기를 연결할 때 필요합니다.
    • Power Delivery (PD): Steam Deck에 전원을 공급하면서 게임을 즐기려면 독이 PD 기능을 지원해야 합니다. 최소 45W 이상을 지원하는 게 좋습니다.
    • 이더넷 포트 (선택 사항): 안정적인 네트워크 환경을 원한다면 유선 LAN 포트가 있는 독을 선택하는 것도 좋습니다.

    준비물: 완벽한 연결을 위한 필수 아이템

    본격적으로 연결하기 전에 필요한 준비물들을 확인해볼까요? 제가 직접 사용해보니 이 정도는 갖춰야 쾌적한 환경을 만들 수 있더라고요.

    1. Steam Deck: 당연하죠!
    2. 외부 모니터 또는 TV: HDMI나 DisplayPort 입력이 되는 모니터라면 어떤 것이든 괜찮습니다.
    3. USB-C 독 (Dock): 위에서 설명드린 필수 포트들이 잘 갖춰진 제품으로 준비해주세요. 저는 PD 60W 이상 지원하는 제품을 추천합니다.
    4. HDMI 또는 DisplayPort 케이블: 고해상도(4K) 및 고주사율(120Hz 이상)을 지원하려면 HDMI 2.0/2.1 또는 DisplayPort 1.4 이상 규격의 케이블을 사용해야 합니다. 구형 케이블 쓰다가 화면 안 나와서 고생 좀 했습니다.
    5. Steam Deck 정품 전원 어댑터 또는 고출력 PD 충전기: 독에 전원을 공급하고, 다시 Steam Deck으로 전원을 안정적으로 공급하기 위해 필수입니다. 최소 45W 이상을 권장합니다.
    6. (선택 사항) 키보드 및 마우스: 데스크톱 모드(Desktop Mode)를 활용하거나 복잡한 설정을 할 때 매우 유용합니다.

    실전 연결 가이드: 단계별로 따라하기

    자, 이제 준비물이 다 갖춰졌다면, 제가 직접 연결했던 순서대로 따라오시면 됩니다. 생각보다 간단해요!

    1. 독에 전원 어댑터 연결: 가장 먼저 독에 Steam Deck 정품 어댑터 또는 고출력 PD 충전기를 연결해주세요. 독에 충분한 전원이 공급되어야 Steam Deck도 충전되고, 외부 모니터로 안정적인 영상 출력이 가능합니다.

    2. 독과 외부 모니터 연결: HDMI 또는 DisplayPort 케이블을 사용하여 독과 외부 모니터를 연결합니다. 이때 케이블이 모니터의 올바른 입력 포트에 꽂혔는지 확인하는 것이 중요합니다.

    3. Steam Deck을 독에 연결: Steam Deck 하단의 USB-C 포트를 독의 USB-C 입력 포트에 연결합니다. ‘딸깍’ 소리가 나도록 완전히 꽂아주세요.

    4. 모니터 입력 소스 변경: 외부 모니터의 OSD(On-Screen Display) 메뉴를 통해 입력 소스(Input Source)를 Steam Deck이 연결된 HDMI 또는 DisplayPort 포트로 변경합니다. 잠시 후 Steam Deck의 화면이 모니터에 나타날 겁니다. 드디어 됐다!

    SteamOS 디스플레이 설정: 화면 최적화의 시작

    외부 모니터에 연결되었다면, 이제 SteamOS에서 디스플레이 설정을 최적화할 차례입니다. 이게 또 은근히 중요하거든요.

    1. 설정 메뉴 진입: Steam Deck의 Steam 버튼을 누른 후, 왼쪽 메뉴에서 설정(Settings)을 선택합니다.

    2. 디스플레이 설정: 설정 메뉴에서 디스플레이(Display) 탭으로 이동합니다. 여기에 외부 모니터에 대한 다양한 설정 옵션들이 나타날 겁니다.

    3. 해상도 및 주사율 조정: 연결된 외부 모니터의 해상도(Resolution)와 주사율(Refresh Rate)을 모니터가 지원하는 최댓값으로 설정합니다. 예를 들어, 4K 모니터라면 3840×2160, 120Hz 모니터라면 120Hz로 맞춰주는 거죠. 이때 케이블 규격이 미달이면 원하는 해상도나 주사율이 표시되지 않을 수 있습니다.

    4. HDR 및 VRR 확인: 모니터가 HDR(High Dynamic Range)이나 VRR(Variable Refresh Rate)을 지원한다면, 해당 옵션들을 활성화하여 더욱 부드럽고 생생한 화면을 즐길 수 있습니다. SteamOS 업데이트를 통해 이런 기능들이 점점 더 개선되고 있더라고요.

    Steam Deck의 디스플레이 설정 화면입니다. 여기서 외부 모니터의 해상도와 주사율을 조절할 수 있습니다.

    삽질 경험 공유: 트러블슈팅과 해결책

    제가 직접 Steam Deck 외부 모니터 연결하면서 겪었던 몇 가지 문제들과 그 해결책들을 공유합니다. 사실 이런 삽질 경험이 진짜배기 정보 아니겠어요?

    • [주의] 화면이 안 나오거나 깜빡거려요! (전원 공급 문제)
      제가 처음 겪었던 문제입니다. 독에 폰 충전기를 연결했더니 화면이 나오다 말다, 깜빡이고 난리도 아니더라고요. 원인은 독에 충분한 전원이 공급되지 않아서였습니다. Steam Deck 본체 충전과 영상 신호 전송에 필요한 전력이 부족했던 거죠.
      💡 해결책: 반드시 Steam Deck 정품 어댑터(45W) 또는 그 이상의 고출력 PD 충전기를 독에 연결해주세요. 독 자체의 PD 지원 와트도 확인하는 것이 좋습니다.

    • [주의] 고해상도/고주사율이 적용이 안 돼요! (케이블 문제)
      4K 모니터에 연결했는데 해상도가 1080p밖에 안 뜨고, 144Hz 모니터인데 60Hz가 최대라고 뜰 때가 있습니다. 이건 대부분 케이블 규격 미달 때문입니다. 구형 HDMI 1.4 케이블로는 4K 60Hz나 1440p 120Hz 이상을 지원하기 어렵습니다.
      💡 해결책: HDMI 2.0/2.1 또는 DisplayPort 1.4 이상 규격을 지원하는 고품질 케이블을 사용하세요. 특히 ‘High-Speed’ 또는 ‘Ultra High-Speed’ 등급의 케이블인지 확인하는 것이 좋습니다.

    • [주의] 오디오가 모니터 스피커로 안 나와요! (오디오 출력 설정)
      화면은 잘 나오는데 소리가 Steam Deck 자체 스피커에서 나오거나 아예 안 나올 때가 있습니다.
      💡 해결책: Steam 버튼 → 설정(Settings) → 오디오(Audio) 탭으로 이동해서 ‘오디오 출력 장치(Audio Output Device)’를 외부 모니터나 독으로 명시된 장치로 수동으로 선택해주세요.

    • [주의] 특정 게임에서 화면 깨짐이나 버그가 보여요! (SteamOS 또는 게임 업데이트)
      간혹 특정 게임에서 외부 모니터 연결 시 이상한 그래픽 오류가 발생할 수 있습니다.
      💡 해결책: SteamOS를 최신 버전으로 업데이트하고, 문제가 되는 게임의 업데이트도 확인해보세요. Steam Deck은 계속해서 소프트웨어 개선이 이루어지고 있거든요.

    게임 성능 최적화: FSR과 프레임 제한으로 쾌적하게

    Steam Deck 외부 모니터에 연결해서 높은 해상도로 게임을 즐기려면, 아무래도 Steam Deck의 내장 디스플레이로 플레이할 때보다 더 많은 성능이 요구됩니다. 이때는 몇 가지 게임 성능 최적화 팁을 활용하는 게 좋습니다.

    1. FSR (FidelityFX Super Resolution) 활용: AMD의 FSR 기술은 낮은 해상도로 렌더링된 이미지를 업스케일링하여 화질 저하를 최소화하면서 성능을 향상시키는 기술입니다. Steam Deck에서 이 기능을 적극적으로 활용하는 것을 추천합니다. Steam 버튼을 누른 후, 성능(Performance) 탭에서 FSR을 활성화할 수 있습니다. 저는 주로 1080p에서 FSR을 켜고 4K 모니터로 업스케일링해서 사용하는데, 이거 진짜 물건이더라고요!

    2. 게임 내 그래픽 설정 조정: 당연한 이야기지만, 게임 내 그래픽 옵션을 조절하는 것이 가장 확실한 방법입니다. 해상도를 한 단계 낮추거나, 그림자(Shadows), 텍스처(Textures), 안티앨리어싱(Anti-Aliasing) 등의 품질을 ‘중간’ 또는 ‘낮음’으로 설정하면 프레임이 크게 개선됩니다.

    3. 프레임 제한 (Frame Rate Limit): Steam Deck의 성능 탭에서 프레임 제한을 30FPS나 40FPS 등으로 설정하면, 프레임 변동을 줄여 더 안정적이고 일관된 게임 플레이 경험을 제공합니다. 특히 콘솔 게임처럼 부드러운 화면 전환이 중요한 게임에서 효과적입니다.

    Steam Deck에서 FSR(FidelityFX Super Resolution)을 활성화하여 외부 모니터에서 게임을 플레이하는 모습입니다. 낮은 해상도에서 높은 해상도로 업스케일링하여 성능과 화질을 동시에 잡을 수 있습니다.

    드디어 완성!: 더 커진 화면으로 즐기는 Steam Deck

    이 모든 과정을 거치고 나면, 드디어 여러분의 Steam Deck 활용법이 한 단계 업그레이드됩니다. 작은 화면에서 즐기던 게임들을 대화면에서 쾌적하게 즐길 수 있게 되는 거죠. 제가 실제로 ‘사이버펑크 2077’이나 ‘엘든 링’ 같은 게임들을 외부 모니터로 플레이해보니, 그 몰입감이 정말 차원이 다르더라고요. 휴대용 게임기라는 한계를 넘어선, 거의 콘솔 게임기에 가까운 경험을 선사해줍니다.

    이런 확장성 덕분에 Steam Deck은 단순한 휴대용 게임기를 넘어, 저에게는 일종의 ‘미니 PC’ 같은 역할도 해주고 있습니다. 가끔은 데스크톱 모드로 전환해서 가벼운 작업이나 웹 서핑을 할 때도 있거든요. 저의 홈랩에서 이 작은 기기가 이렇게나 다양한 역할을 할 수 있다는 게 신기하기도 하고, 또 한편으로는 인프라 엔지니어로서 이런 기기를 최적화해서 사용하는 과정이 정말 즐거웠습니다.

    시중에 출시된 다양한 Steam Deck 독(Dock) 제품들의 주요 기능을 비교한 표입니다. 본인의 사용 환경에 맞는 독을 선택하는 데 도움이 될 것입니다.

    마무리: 휴대용 게임기의 무한한 확장성

    오늘은 Steam Deck 외부 모니터 연결과 Steam Deck 독 모드를 활용한 최적화 가이드를 소개해드렸습니다. 몇 가지 준비물과 설정만 잘 갖추면 누구나 손쉽게 휴대용 게임 환경을 대화면으로 확장할 수 있다는 것을 아셨을 겁니다. 저도 처음엔 시행착오가 많았지만, 결국 제 홈랩에서 멋진 게임 환경을 구축했네요. 휴대용 게임기의 새로운 가능성을 발견한 기분입니다.

    Steam Deck은 정말 매력적인 기기예요. 단순히 게임을 넘어, 리눅스 기반의 오픈된 시스템 덕분에 무궁무진한 Steam Deck 활용법이 존재하거든요. 다음 글에서는 Steam Deck에 Windows를 설치하거나, 다양한 에뮬레이션을 활용하는 방법도 다뤄볼까 합니다. 궁금하신 점이 있다면 언제든지 댓글로 남겨주세요. 다음 포스팅에서 또 유익한 정보로 찾아뵙겠습니다!

  • [Cloud] Terraform AWS EKS 모듈 활용: 프로덕션 클러스터 배포 및 관리 가이드

    [Cloud] Terraform AWS EKS 모듈 활용: 프로덕션 클러스터 배포 및 관리 가이드

    안녕하세요, 13년차 서버 인프라 엔지니어입니다. 오늘은 Terraform AWS EKS 모듈을 활용해서 프로덕션 환경에 바로 적용할 수 있는 쿠버네티스(Kubernetes) 클러스터를 배포하고 관리하는 방법을 나눠볼게요. 사실 저도 쿠버네티스를 처음 접했을 땐 ‘이걸 어떻게 프로덕션에 안정적으로 올리지?’ 고민이 정말 많았거든요. 수많은 설정과 의존성 때문에 삽질을 꽤 했었습니다. ㅎㅎ

    그런데 Terraform(테라폼)과 AWS EKS 모듈을 써보니까, 정말 복잡했던 배포 과정이 깔끔하게 정리되더라고요. 마치 복잡한 미로에서 벗어나는 기분이었죠. 오늘은 제가 직접 경험했던 노하우를 바탕으로, Terraform EKS 모듈이 왜 프로덕션 환경에 필수적인지, 그리고 어떻게 활용하는지 자세히 알려드릴게요.

    Terraform으로 관리되는 AWS EKS 클러스터의 일반적인 아키텍처입니다.

    개념 정리: 왜 Terraform과 EKS 모듈이 필요할까요?

    본격적인 실전으로 들어가기 전에, 핵심 개념들을 간단히 짚고 넘어갈게요. 이미 잘 알고 계신 분들도 있겠지만, 혹시 모르니까 멘토처럼 쉽게 설명해드릴게요.

    • IaC (Infrastructure as Code, 코드형 인프라스트럭처): 인프라를 코드로 관리한다는 뜻이에요. 예전에는 서버 한 대 놓으려면 직접 전산실 가서 설치하고 케이블 연결하고 그랬잖아요? 요즘은 그런 모든 과정을 코드로 정의하고 자동화합니다. Terraform이 바로 이 IaC를 구현하는 대표적인 도구거든요. 코드로 인프라를 관리하면 반복 가능하고, 버전 관리도 되고, 무엇보다 휴먼 에러가 확 줄어든다는 게 최고의 장점입니다. 제가 직접 써보니 정말 그렇더라고요!

    • AWS EKS (Amazon Elastic Kubernetes Service): AWS에서 제공하는 관리형 쿠버네티스 서비스예요. 쿠버네티스는 컨테이너화된 워크로드를 배포하고 관리하는 오픈소스 시스템인데, EKS는 이 쿠버네티스 컨트롤 플레인(Control Plane)을 AWS가 대신 관리해준다는 뜻입니다. 덕분에 우리는 마스터 노드 관리 부담 없이 워커 노드(Worker Node)에만 집중할 수 있죠. 프로덕션 환경에선 안정성이 생명인데, AWS가 관리해주니 정말 마음이 든든합니다.

    • Terraform AWS EKS 모듈: Terraform 오픈소스 커뮤니티가 만들어 놓은 ‘모듈(Module)’이 있습니다. 이 EKS 모듈은 AWS EKS 클러스터를 배포하는 데 필요한 모든 리소스(VPC, 서브넷, IAM 역할, EKS 클러스터 자체, 노드 그룹 등)를 미리 정의해둔 템플릿 모음이에요. 이걸 사용하면 수백 줄이 넘을 수 있는 Terraform 코드를 몇 줄로 줄일 수 있거든요. 처음엔 직접 다 만들었었는데, 이 모듈을 발견하고 나서는 ‘아, 이래서 다들 모듈을 쓰는구나!’ 하고 무릎을 탁 쳤습니다. 생산성 향상에 정말 최고예요. 🚀

    실전 구현: Terraform으로 EKS 클러스터 배포하기

    이제 실제로 Terraform AWS EKS 모듈을 사용해서 프로덕션용 EKS 클러스터를 배포해볼게요. 단계별로 차근차근 따라오면 됩니다. 제가 홈랩에서 여러 번 테스트해보고 가장 안정적인 방법을 알려드릴게요.

    1. 프로젝트 구조 및 초기 설정

    먼저 다음과 같은 디렉토리 구조를 만들어주세요.

    mydir/
    ├── main.tf
    ├── variables.tf
    └── outputs.tf
    

    main.tf 파일에 AWS Provider(프로바이더)를 설정하고, Terraform EKS 모듈을 정의합니다.

    # main.tf
    
    terraform {
      required_version = ">= 1.0.0"
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.0"
        }
      }
    }
    
    provider "aws" {
      region = var.aws_region
    }
    
    module "vpc" {
      source  = "terraform-aws-modules/vpc/aws"
      version = "~> 5.0"
    
      name = "${var.cluster_name}-vpc"
      cidr = "10.0.0.0/16"
    
      azs             = data.aws_availability_zones.available.names
      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
      single_nat_gateway = true
    
      tags = {
        "kubernetes.io/cluster/${var.cluster_name}" = "owned"
        "kubernetes.io/role/internal-elb"           = "1"
        "kubernetes.io/role/elb"                    = "1"
      }
    }
    
    module "eks" {
      source  = "terraform-aws-modules/eks/aws"
      version = "~> 20.0" # Terraform Registry에서 최신 안정 버전을 확인하세요!
    
      cluster_name    = var.cluster_name
      cluster_version = var.cluster_version
    
      vpc_id     = module.vpc.vpc_id
      subnet_ids = module.vpc.private_subnets
    
      # EKS 클러스터 로깅 활성화 (프로덕션 필수!)
      cluster_enabled_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
    
      # 관리형 노드 그룹 (Managed Node Groups)
      managed_node_groups = {
        general = {
          name            = "general-nodes"
          instance_types  = ["t3.medium"]
          min_size        = 2
          max_size        = 5
          desired_size    = 3
          disk_size       = 50
          labels          = { env = "production", role = "general" }
          capacity_type   = "ON_DEMAND"
          # EKS 노드에 SSH 접속이 필요하다면 아래 주석 해제 후 키 페어 이름 설정
          # key_name = "your-ssh-key-name"
        }
        # 추가 노드 그룹이 필요하면 여기에 정의
        # spot = {
        #   name          = "spot-nodes"
        #   instance_types  = ["t3.small", "t3.medium"]
        #   min_size        = 0
        #   max_size        = 10
        #   desired_size    = 1
        #   capacity_type   = "SPOT"
        #   disk_size       = 20
        # }
      }
    
      tags = {
        Project     = "EKS-Production"
        Environment = "Prod"
      }
    }
    
    data "aws_availability_zones" "available" {}
    

    ⚠️ 주의사항: t3.medium은 테스트용으로 괜찮지만, 실제 프로덕션 워크로드에는 더 큰 인스턴스 타입(예: m5.large, c5.large)을 고려하세요. 그리고 version = "~> 20.0" 부분은 항상 Terraform Registry에서 최신 안정 버전을 확인해서 사용하세요. Terraform AWS EKS 모듈이 워낙 빠르게 업데이트돼서 제가 작성한 시점과 다를 수 있거든요.

    variables.tf 파일에는 클러스터 이름, AWS 리전, EKS 버전 등 변경될 수 있는 값들을 정의합니다.

    # variables.tf
    
    variable "aws_region" {
      description = "AWS region."
      type        = string
      default     = "ap-northeast-2" # 서울 리전
    }
    
    variable "cluster_name" {
      description = "Name of the EKS cluster."
      type        = string
      default     = "my-prod-eks-cluster"
    }
    
    variable "cluster_version" {
      description = "Kubernetes version."
      type        = string
      default     = "1.28" # 사용 가능한 EKS 버전 확인 후 지정
    }
    

    outputs.tf 파일에는 배포 후 필요한 정보를 출력하도록 설정합니다. 클러스터 엔드포인트나 kubeconfig 명령어 같은 것들이죠.

    # outputs.tf
    
    output "cluster_endpoint" {
      description = "Endpoint for EKS Control Plane."
      value       = module.eks.cluster_endpoint
    }
    
    output "kubeconfig_command" {
      description = "Command to configure kubectl."
      value       = "aws eks update-kubeconfig --region ${var.aws_region} --name ${var.cluster_name}"
    }
    
    output "cluster_security_group_id" {
      description = "Security group ID of the EKS cluster."
      value       = module.eks.cluster_security_group_id
    }
    

    Terraform EKS 모듈을 위한 주요 구성 파일들의 역할과 관계를 보여줍니다.

    2. Terraform 명령 실행

    파일들을 모두 작성했다면, 터미널을 열고 해당 디렉토리로 이동해서 다음 명령어를 실행합니다.

    1. Terraform 초기화 (Initialize): 필요한 프로바이더와 모듈을 다운로드합니다.

      terraform init
      
    2. Terraform 실행 계획 (Plan): 실제로 어떤 리소스들이 생성/변경/삭제될지 미리 보여줍니다. 이 단계에서 항상 꼼꼼히 확인하는 습관을 들이셔야 해요. 저는 여기서 실수 몇 번 해보고 크게 깨달았습니다. 😅

      terraform plan
      
    3. Terraform 적용 (Apply): 계획대로 리소스를 AWS에 배포합니다. 시간이 좀 걸릴 수 있어요. 커피 한 잔 마시면서 기다려보세요. 😊

      terraform apply --auto-approve
      

      --auto-approve 옵션은 실제 프로덕션 환경에서는 신중하게 사용해야 합니다. 보통은 terraform apply만 실행해서 직접 승인하는 과정을 거치는 게 안전하거든요.

    ⚠️ 주의사항: 삽질 경험과 해결책

    제가 13년간 인프라 엔지니어로 일하면서 느낀 건, 아무리 잘 만들어진 도구라도 ‘삽질’은 피할 수 없다는 거예요. Terraform EKS 모듈도 마찬가지입니다. 몇 가지 흔한 문제와 제가 겪었던 해결책을 공유해드릴게요.

    • VPC Subnet Tag 누락: EKS 클러스터가 서브넷을 제대로 인식하지 못해서 배포가 실패하는 경우가 종종 있어요. 특히 다른 모듈로 VPC를 만들었거나 수동으로 서브넷을 구성했을 때 그렇더라고요. EKS는 워커 노드를 프로비저닝할 때 특정 태그(kubernetes.io/cluster/YOUR_CLUSTER_NAME과 kubernetes.io/role/internal-elb 또는 kubernetes.io/role/elb)가 있는 서브넷을 찾습니다. main.tf의 VPC 모듈 설정에서 태그를 꼭 넣어주세요.

      tags = {
        "kubernetes.io/cluster/${var.cluster_name}" = "owned"
        "kubernetes.io/role/internal-elb"           = "1"
        "kubernetes.io/role/elb"                    = "1"
      }
      
    • IAM 권한 부족: Terraform을 실행하는 IAM 사용자 또는 역할에 EKS, EC2, IAM, VPC 관련 권한이 충분히 부여되지 않으면 문제가 생길 수 있습니다. 특히 EKS Administrator와 유사한 관리자 권한을 가진 정책을 사용하거나, 필요한 최소 권한을 직접 설정해야 하죠. 저는 처음에 너무 최소 권한만 주려다가 여러 번 권한 에러를 만났습니다. 그럴 땐 일단 잠시 넓은 권한을 줘서 문제가 권한 때문인지 확인하고, 잘 되면 다시 최소 권한으로 조이는 방법을 썼어요.

    • 모듈 버전 충돌 또는 비호환성: Terraform AWS EKS 모듈은 빠르게 업데이트됩니다. 특정 Terraform 버전, AWS Provider 버전, EKS 클러스터 버전과의 호환성을 항상 확인해야 해요. Terraform Registry에서 해당 모듈의 Required Providers와 Requirements 섹션을 꼭 확인하세요. 버전이 맞지 않으면 예상치 못한 에러가 발생할 수 있거든요.

    • 노드 그룹의 인스턴스 타입/AMI 문제: EKS 워커 노드가 정상적으로 클러스터에 조인되지 않는 경우가 있습니다. 주로 EKS 버전과 호환되지 않는 AMI(Amazon Machine Image)를 사용했거나, 인스턴스 타입이 해당 리전에서 지원되지 않을 때 발생합니다. Terraform EKS 모듈은 기본적으로 EKS 최적화 AMI를 사용하지만, 사용자 지정 AMI를 사용할 경우 주의해야 합니다.

    검증 및 결과: 클러스터 확인하기

    Terraform apply가 성공적으로 완료되었다면, 이제 EKS 클러스터가 잘 배포되었는지 확인해볼 차례예요. 🎉

    1. Kubeconfig 설정

    먼저 outputs.tf에서 출력된 kubeconfig_command를 실행해서 kubectl이 EKS 클러스터에 접속할 수 있도록 설정합니다. 이 명령어를 실행하면 ~/.kube/config 파일이 업데이트될 겁니다.

    aws eks update-kubeconfig --region ap-northeast-2 --name my-prod-eks-cluster
    

    2. 노드 확인

    이제 kubectl 명령어로 클러스터 노드들을 확인해봅시다. 워커 노드들이 Ready 상태로 잘 올라와 있어야 합니다.

    kubectl get nodes
    
    NAME                                           STATUS   ROLES    AGE     VERSION
    ip-10-0-1-123.ap-northeast-2.compute.internal   Ready    <none>   5m20s   v1.28.x
    ip-10-0-2-234.ap-northeast-2.compute.internal   Ready    <none>   5m15s   v1.28.x
    ip-10-0-3-345.ap-northeast-2.compute.internal   Ready    <none>   5m10s   v1.28.x
    

    3. AWS Console에서 확인

    AWS Management Console(관리 콘솔)에 로그인해서 EKS 서비스로 이동하면, 방금 배포한 클러스터가 목록에 보일 거예요. 클러스터 이름을 클릭해서 상세 정보를 확인하고, 노드 그룹 탭에서 워커 노드들이 정상적으로 실행 중인지도 확인해볼 수 있습니다.

    AWS EKS 콘솔에서 배포된 클러스터와 노드 그룹의 상태를 확인하는 모습입니다.

    마무리, 그리고 다음 단계

    오늘은 Terraform AWS EKS 모듈을 활용해서 프로덕션 레디(Production-Ready) 쿠버네티스 클러스터를 배포하고 관리하는 방법을 자세히 알아봤습니다. 제가 직접 겪은 삽질 경험과 해결책도 함께 공유해드렸는데, 도움이 되셨으면 좋겠네요.

    Terraform EKS 모듈 덕분에 우리는 복잡한 EKS 인프라를 빠르고 안정적으로 구축할 수 있게 됐어요. IaC의 강력함을 다시 한번 느낄 수 있었던 경험이었죠. 처음엔 진입 장벽이 좀 있다고 느낄 수 있지만, 한번 익숙해지면 이만큼 편리한 게 없습니다. 정말 강력한 도구거든요.

    Terraform AWS EKS 모듈을 사용했을 때 얻을 수 있는 주요 이점들을 시각적으로 요약했습니다.

    다음 단계로는 이렇게 배포된 EKS 클러스터에 Ingress Controller(인그레스 컨트롤러, 외부 트래픽을 클러스터 내부 서비스로 라우팅), Cert-Manager(인증서 관리), Prometheus(프로메테우스, 모니터링 시스템) 같은 필수 애드온(Add-on)들을 Terraform으로 함께 배포하는 방법을 다뤄볼 예정이에요. 그리고 CI/CD 파이프라인(Continuous Integration/Continuous Deployment Pipeline, 지속적 통합/배포 파이프라인)과 연동해서 인프라 변경을 자동화하는 방법도 흥미로운 주제가 될 것 같습니다.

    궁금한 점이나 추가적인 삽질 경험이 있으시다면 댓글로 자유롭게 남겨주세요! 함께 고민하고 해결해나가는 게 인프라 엔지니어의 묘미 아니겠습니까? 😊

  • [AWS 배포] GitHub Actions로 S3/CloudFront 정적 웹사이트 배포 자동화 가이드

    수동 배포, 이제 그만 — GitHub Actions로 S3/CloudFront 배포 자동화하기

    혹시 이런 경험 있으신가요? 프론트엔드 코드 수정하고, 빌드하고, AWS 콘솔 열고, S3에 파일 올리고, CloudFront 캐시 무효화(Invalidation)하고… 이 반복 작업을 매번 손으로 하다가 어느 순간 ‘내가 지금 뭘 하고 있나’ 싶은 그 느낌. 저도 예전에 딱 그랬거든요.

    13년 동안 인프라 일 하면서 느낀 건데, 반복 작업은 반드시 자동화해야 합니다. 사람이 하면 언젠가는 실수가 납니다. 저도 한 번은 CloudFront 캐시 무효화 깜빡해서 사용자들이 한참 동안 옛날 버전 보고 있었던 적 있어요. 그날 이후로 배포 자동화는 선택이 아니라 필수라고 생각하게 됐습니다.

    오늘은 GitHub Actions를 활용해서 AWS S3와 CloudFront에 정적 웹사이트를 자동으로 배포하는 CI/CD 파이프라인을 처음부터 끝까지 만들어보겠습니다. React나 Vue, Next.js 정적 빌드 등 어떤 프레임워크든 적용할 수 있는 방법이에요.

    ▲ GitHub Actions → S3 업로드 → CloudFront 캐시 무효화로 이어지는 전체 배포 파이프라인 구조

    GitHub Actions AWS 배포의 핵심 개념 — CI/CD, S3, CloudFront

    일단 개념부터 간단히 정리하고 갈게요. 저도 처음엔 이 용어들이 뒤섞여서 헷갈렸거든요.

    GitHub Actions란?

    GitHub Actions는 GitHub에서 제공하는 CI/CD(지속적 통합/지속적 배포) 자동화 플랫폼입니다. 쉽게 말해서, 코드를 push하거나 PR을 올리는 등 특정 이벤트가 발생했을 때 미리 정의해둔 작업을 자동으로 실행해주는 도구예요. 빌드, 테스트, 배포까지 전부 코드로 관리할 수 있고, 무료 플랜도 꽤 넉넉해서 소규모 프로젝트엔 비용 걱정 없이 쓸 수 있습니다.

    S3 + CloudFront 조합이 왜 좋은가?

    AWS S3(Simple Storage Service)는 파일 저장소인데, 정적 웹사이트 호스팅 기능도 있어요. CloudFront는 AWS의 CDN(콘텐츠 전송 네트워크)으로, 전 세계 엣지 서버에 콘텐츠를 캐싱해서 사용자에게 빠르게 전달해줍니다. 이 둘을 조합하면 서버 없이도 빠르고 안정적인 웹사이트를 운영할 수 있어요. 저도 여러 프로젝트에서 이 구조를 썼는데, 정말 편하더라고요.

    항목 S3 단독 호스팅 S3 + CloudFront
    HTTPS 지원 ❌ (별도 설정 복잡) ✅ ACM 인증서 연동
    글로벌 속도 단일 리전 기준 CDN 엣지 캐싱으로 빠름
    커스텀 도메인 제한적 자유롭게 가능
    비용 저렴 약간 추가 (트래픽 기준)

    실제 프로덕션 환경이라면 S3 + CloudFront 조합을 강력 추천합니다. 오늘 가이드도 이 조합 기준으로 진행할게요.

    사전 준비 — AWS IAM 권한 설정부터

    GitHub Actions에서 AWS 리소스에 접근하려면 적절한 권한을 가진 IAM(Identity and Access Management) 사용자가 필요합니다. 여기서 많은 분들이 귀찮다고 AdministratorAccess 권한을 통째로 주는 경우가 있는데, 보안상 절대 권장하지 않아요. 최소 권한 원칙(Principle of Least Privilege)을 지켜야 합니다.

    IAM 정책 만들기

    AWS IAM 콘솔에서 아래 내용으로 커스텀 정책을 만들어주세요. S3 버킷 이름과 CloudFront Distribution ID는 본인 것으로 바꾸세요.

    {\n  "Version": "2012-10-17",\n  "Statement": [\n    {\n      "Effect": "Allow",\n      "Action": [\n        "s3:PutObject",\n        "s3:PutObjectAcl",\n        "s3:DeleteObject"\n      ],\n      "Resource": "arn:aws:s3:::your-bucket-name/*"\n    },\n    {\n      "Effect": "Allow",\n      "Action": [\n        "cloudfront:CreateInvalidation"\n      ],\n      "Resource": "arn:aws:cloudfront::YOUR-AWS-ACCOUNT-ID:distribution/YOUR-DISTRIBUTION-ID"\n    }\n  ]\n}