13년차의 서버실

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

[작성자:] admin

  • [HomeLabs] Home Assistant IP 카메라 보안 체크리스트: 홈랩 연동 모범 사례

    [HomeLabs] Home Assistant IP 카메라 보안 체크리스트: 홈랩 연동 모범 사례

    Home Assistant IP 카메라 보안 체크리스트

    Home Assistant IP 카메라 조합은 생각보다 빨리 복잡해집니다. 화면만 뜨면 끝이라고 보기 쉬운데, 실제 운영에서 발목을 잡는 건 대개 영상 품질보다 권한, 네트워크 경계, 로그 품질이거든요. 저도 홈랩에서 몇 번 다시 깔아보니, ONVIF로 장치가 잡히고 스트림이 재생되는 순간보다 그다음 몇 주가 더 중요하더라고요.

    지금은 기준을 하나로 잡습니다. 카메라는 신뢰하는 장비가 아니라, 필요한 범위 안에서만 일하게 만드는 장비로 보는 겁니다. 이 관점으로 보면 설계가 훨씬 단순해집니다. 카메라는 영상을 보내고, Home Assistant는 상태와 자동화를 묶고, 외부 접근은 VPN이나 엄격한 리버스 프록시로만 받습니다. 이번 글은 그 기준으로 정리한 Home Assistant IP 카메라 보안 실전 체크리스트입니다.

    Home Assistant IP 카메라 분리 네트워크 아키텍처 이미지

    Home Assistant 서버, 카메라 전용 네트워크, 관리용 단말, VPN 경로를 한눈에 보여주는 개요 이미지입니다.

    1. 왜 Home Assistant IP 카메라 연동이 보안 이슈로 이어질까

    IP 카메라는 단순한 영상 소스가 아닙니다. 스트림 서버이자 웹 관리 장비이고, 경우에 따라 이벤트 발행자이기도 합니다. Home Assistant는 그 위에 자동화를 얹는 오케스트레이션 레이어에 가깝고요. 여기서 자주 놓치는 건, Home Assistant에 붙였다고 해서 카메라 자체의 공격면이 자동으로 줄어드는 건 아니라는 점입니다.

    제가 현장에서 제일 자주 본 실패는 세 가지였습니다.

    • 노출 실패: RTSP, 웹 관리 포트, ONVIF 포트가 의도치 않게 다른 VLAN이나 인터넷 쪽에서 보입니다.
    • 권한 실패: Home Assistant 연동 계정이 관리자 권한이라 PTZ, 사용자 관리, 설정 변경까지 다 열려 있습니다.
    • 가시성 실패: 프록시, 카메라, Home Assistant 시간이 어긋나거나 원본 접속 정보를 교차 확인할 로그가 부족해 사고 분석이 어려워집니다.

    이 셋이 겹치면 꽤 번거로워집니다. 예를 들어 현관 카메라가 외부에 노출돼 있고, 관리자 계정으로 ONVIF를 붙여놨고, 프록시 access log 없이 Home Assistant 로그만 본다고 해보겠습니다. 그러면 누가 어디서 접근했는지 추적이 흐려지고, 연동 편의 때문에 열어둔 기능이 오히려 리스크를 키웁니다. 제 경험상 홈랩 감시 시스템은 “잘 나오느냐”보다 잘 제한돼 있느냐를 먼저 봐야 나중이 편합니다.

    2. Home Assistant IP 카메라 연동의 핵심 개념: ONVIF, RTSP, 네트워크 분리

    처음엔 ONVIF와 RTSP를 한 묶음으로 보기 쉬운데, 운영 관점에서는 역할을 분리해서 봐야 판단이 빨라집니다. ONVIF는 장치 발견, 프로필 조회, 이벤트, PTZ 같은 제어 채널에 가깝고, RTSP는 영상 전달 채널입니다. 참고로 Home Assistant의 ONVIF 통합은 현재 기준으로 ONVIF Profile S 호환 장치를 대상으로 하며, 영상 처리를 위해 FFmpeg 통합이 필요합니다.

    구성 방식 언제 쓰는지 장점 주의점 제가 권하는 판단
    RTSP만 사용 라이브 뷰나 NVR 쪽 스트림만 필요할 때 공격면이 비교적 단순함 이벤트, PTZ, 장치 메타데이터 연동이 줄어듦 움직임 감지는 별도 센서나 NVR이 맡는 구조면 이쪽이 더 깔끔합니다.
    ONVIF 중심 사용 장치 검색과 이벤트 연동이 핵심일 때 초기 통합은 편함 권한 과다 부여가 흔하고, 영상 경로 검증은 별도로 필요함 화면보다 이벤트가 중요할 때만 제한적으로 권합니다.
    ONVIF + RTSP 병행 이벤트와 라이브 뷰를 둘 다 쓸 때 기능은 가장 풍부함 권한, 포트, 디버깅 포인트가 늘어남 대부분 여기로 가지만, 계정과 네트워크 분리를 같이 하지 않으면 금방 지저분해집니다.
    카메라 전용 VLAN/서브넷 카메라 수가 2대 이상이거나 장기 운영할 때 격리, 룰 관리, 문제 분리가 쉬움 초기 라우팅과 방화벽 정책 설계가 필요함 하나만 먼저 하라면 저는 이걸 고릅니다.

    네트워크 분리는 단순히 “분리했다”에서 끝나지 않습니다. 누가 누구에게 먼저 말을 걸 수 있는지를 줄이는 작업에 가깝습니다. 카메라는 Home Assistant에 필요한 포트만 열고, 관리용 PC는 관리 페이지에만 접근하고, 인터넷으로 직접 나가는 경로는 기본 차단하는 식이죠. 이 구조를 먼저 잡아두면 카메라가 늘어나도 판단 기준이 흔들리지 않습니다.

    3. 제가 쓰는 연동 전 체크리스트

    1. 펌웨어부터 확인: 최신 안정 버전으로 맞추고, 업데이트 직후 ONVIF 프로필과 RTSP 경로가 바뀌지 않았는지 같이 봅니다.
    2. 기본 계정 정리: 기본 admin 계정은 비활성화하거나 강한 비밀번호로 바꾸고, Home Assistant용 계정은 별도로 만듭니다.
    3. 권한 최소화: Home Assistant 계정에는 영상 조회와 필요한 이벤트 권한만 주고, 사용자 관리나 네트워크 설정 권한은 빼둡니다.
    4. 관리 평면 분리: 카메라 웹 UI는 관리 단말 또는 관리 VLAN에서만 열리게 하고, 일반 사용자 대역에서는 막습니다.
    5. 시간 동기화: 카메라, Home Assistant, 리버스 프록시, NVR이 있다면 그 장비까지 NTP 기준을 맞춥니다.
    6. 스트림 전략 결정: 대시보드에는 서브 스트림, 녹화나 분석은 메인 스트림처럼 역할을 나눕니다.
    7. 외부 접속 원칙 결정: 직접 포트포워딩은 피하고, VPN 또는 인증이 명확한 프록시 경로 중 하나만 표준으로 정합니다.
    8. 검증 경로 준비: nmap, ffprobe, 패킷 캡처, Home Assistant 로그 확인 순서를 미리 정해둡니다.

    여기서 특히 많이 놓치는 게 시간 동기화입니다. 연동은 되는데 이벤트 타임라인이 엇갈리고, 녹화 파일 시각과 자동화 시각이 다르면 원인 찾는 속도가 확 떨어집니다. “움직임 감지됐다는데 왜 같은 시각 로그가 없지?” 하고 헤매는 경우가 은근 많더라고요. 저는 카메라 세팅 끝내면 NTP부터 꼭 확인합니다.

    4. 실전 구현 1: 네트워크와 포트부터 줄입니다

    실전에서는 카메라가 노출한 서비스부터 확인합니다. 제 기준은 간단합니다. 예상한 포트만 열려 있어야 하고, 예상한 대상에게만 닿아야 합니다. 카메라가 Home Assistant에 스트림을 제공하는 건 괜찮지만, 외부 DNS나 제조사 클라우드 주소로 계속 통신하고 있으면 다시 봐야 합니다. P2P 기능이 숨어 있는 장비도 생각보다 많았습니다.

    제가 보통 먼저 돌리는 확인 명령은 아래 순서입니다.

    # 1) 카메라가 실제로 어떤 TCP 포트를 열고 있는지 확인
    nmap -Pn -sT -sV -p- 192.168.30.10
    
    # 2) RTSP 스트림이 정상인지 Home Assistant 바깥에서 먼저 검증
    ffprobe -v error -rtsp_transport tcp \
      -show_entries stream=index,codec_name,codec_type,width,height \
      -show_entries format=format_name \
      -of default=noprint_wrappers=1 \
      "rtsp://ha_viewer:[email protected]:554/stream1"
    
    # 3) 카메라가 예상 밖 목적지와 통신하는지 관찰
    sudo tcpdump -ni any host 192.168.30.10

    이 세 개만 제대로 봐도 원인 분리가 꽤 빨라집니다. 코드 블록은 예시라서, 실제 환경에서는 IP와 계정명, RTSP 경로를 장비에 맞게 바꿔서 확인하시면 됩니다.

    • nmap에서 웹 관리 포트, ONVIF 포트, RTSP 외에 낯선 서비스가 보이면 먼저 정리합니다.
    • ffprobe가 여기서 실패하면 Home Assistant 문제가 아니라 스트림 URL, 계정 권한, 코덱, 네트워크 경로 문제일 확률이 높습니다.
    • tcpdump에서 카메라가 Home Assistant 말고 다른 내부 장비나 외부 공인 IP와 자주 대화하면, 의도한 경로인지부터 따져봐야 합니다.

    방화벽은 장비마다 문법이 다르니 원칙을 먼저 잡는 게 낫습니다. 저는 보통 이렇게 나눕니다.

    • 카메라 VLAN -> Home Assistant: RTSP와 필요한 이벤트 포트만 허용
    • 관리 VLAN -> 카메라: 웹 UI 등 관리 포트만 허용
    • 카메라 VLAN -> 인터넷: 기본 거부
    • 일반 사용자 VLAN -> 카메라: 기본 거부

    리눅스 기반 방화벽이라면 아래처럼 시작하는 편이 이해가 쉽습니다. 인터페이스명과 IP는 환경에 맞게 바꿔야 하지만, 논리는 그대로 가져가면 됩니다.

    # nftables 예시: 기본은 막고, 필요한 경로만 허용
    sudo nft add table inet camfilter
    sudo nft 'add chain inet camfilter forward { type filter hook forward priority 0; policy drop; }'
    sudo nft add rule inet camfilter forward ct state established,related accept
    sudo nft add rule inet camfilter forward iifname "vlan30_cam" ip daddr 192.168.10.20 tcp dport { 554 } accept
    sudo nft add rule inet camfilter forward iifname "vlan10_mgmt" ip daddr 192.168.30.0/24 tcp dport { 80, 443 } accept

    제가 이 단계에서 중요하게 보는 건 “열어둘 포트 목록”보다 “닫아둘 기본 방향”입니다. 카메라는 업데이트할 때만 잠깐 인터넷이 필요할 수 있습니다. 그래서 상시 허용보다 일시 허용 후 다시 차단이 운영이 훨씬 편하더라고요.

    Home Assistant IP 카메라 방화벽 정책 흐름 이미지

    카메라 전용 대역에서 Home Assistant로만 제한적으로 접근하도록 설계한 예시입니다.

    5. 실전 구현 2: Home Assistant 쪽 설정도 같이 잠급니다

    카메라만 분리해도 절반은 정리되지만, Home Assistant가 느슨하면 사고 기록이 흐려집니다. 특히 리버스 프록시를 거치는 환경에서는 Trust X-Forwarded-For, Trusted proxies, 로그인 차단 정책을 같이 봐야 합니다. 다만 2026년 현재 Home Assistant의 HTTP 서버 설정은 보통 Settings > System > Network에서 관리하므로, 예전처럼 configuration.yaml의 http: 블록만 기준으로 보면 오히려 헷갈릴 수 있습니다.

    실전 점검 포인트는 이렇습니다.

    • Trusted proxies에는 실제 앞단 프록시 IP나 정확한 CIDR만 넣습니다. 대충 넓게 잡으면 나중에 해석이 꼬입니다.
    • Enable IP banning과 로그인 시도 제한은 켜두되, 먼저 프록시를 신뢰하는 방식이 맞는지 확인합니다.
    • CORS allowed origins는 필요한 도메인만 넣습니다. 편하다고 넓게 열어두면 괜히 문제만 늘어납니다.
    • SSL profile은 특별한 호환성 문제가 있을 때만 조정합니다. 기본값을 괜히 건드릴 필요는 거의 없었습니다.

    하나 더 짚고 갈 부분이 있습니다. 로그인 실패나 차단 로그는 환경에 따라 프록시 주소 위주로 보일 수 있어서, Home Assistant 로그만으로 원본 IP를 항상 정확히 복원할 수 있다고 기대하면 안 됩니다. 이럴 때는 프록시 access log와 같이 봐야 훨씬 정확합니다. 이거 한 번 이해해두면, 나중에 분석할 때 진짜 편합니다.

    카메라 통합 계정도 저는 분리합니다. 공식 ONVIF 문서도 Home Assistant 전용 사용자를 따로 만들고, 현재 기능 범위에서는 표준 사용자 권한이면 충분하다고 안내합니다. PTZ를 안 쓰면 PTZ 권한을 안 주고, 이벤트를 안 쓰면 ONVIF 자체를 줄이거나 끄는 편이 낫습니다. 연동 편의와 공격면은 거의 항상 같이 움직입니다.

    # automations.yaml
    - id: camera_motion_notification
      alias: Front Door Motion Notification
      triggers:
        - trigger: state
          entity_id: binary_sensor.front_door_motion
          to: "on"
      conditions:
        - condition: state
          entity_id: input_boolean.alerts_enabled
          state: "on"
      actions:
        - action: notify.mobile_app_phone
          data:
            title: "Front Door Motion"
            message: "현관 카메라 이벤트가 감지됐습니다."
      mode: single

    이 자동화 예시는 현재 Home Assistant YAML 문법 기준에도 맞는 형태입니다. 자동화는 결국 이벤트를 만든 주체와, 알림을 보낸 주체를 분리해서 추적할 수 있게 짜두는 게 핵심입니다.

    외부 접속 방식은 이렇게 고릅니다

    방식 장점 주의점 추천 상황
    직접 포트포워딩 당장 붙이기는 쉬움 관리 포트와 인증 표면이 인터넷에 그대로 노출될 수 있음 사실상 비추천
    VPN 내부망 기준으로 통제하기 쉬움 초기 설정, 기기별 접속 경로 관리가 필요함 개인 홈랩, 소규모 운영, 보안 우선 환경
    리버스 프록시 + 접근 통제 서비스별 정책 분리, 인증 연동이 쉬움 프록시 신뢰 범위, 원본 IP 전달, 인증 체계 설계가 중요함 여러 내부 서비스를 함께 운영할 때

    제 추천은 분명합니다. 혼자 운영하는 홈랩이면 VPN이 먼저입니다. 반대로 Home Assistant 외에도 여러 웹 서비스를 묶어 외부에 내보내야 하고, 접근 제어를 한 곳에서 관리해야 한다면 리버스 프록시 + 엄격한 trusted proxy 설정이 더 맞습니다. 직접 포트포워딩은 임시 테스트에서도 습관이 되면 별로더라고요.

    Home Assistant IP 카메라 ONVIF 연동 설정 이미지

    Home Assistant에서 카메라 통합을 추가하고 이벤트 자동화를 연결하는 흐름을 설명하는 이미지입니다.

    6. 자주 겪는 문제와 해결법

    이 구간은 제가 실제로 가장 많이 손댄 부분입니다. “아예 안 됨”보다 “가끔 됨”이 더 어렵습니다. 그래서 증상보다 근본 원인으로 묶어보는 편이 훨씬 낫습니다.

    1) ONVIF는 잡히는데 영상이 안 나오는 경우

    이건 보통 제어 채널과 스트림 채널이 분리돼 있을 때 생깁니다. 장치 정보는 읽히는데, 실제 스트림 URL이나 코덱 지원이 표시 경로와 안 맞는 겁니다.

    • 근본 원인: ONVIF 프로필 조회는 성공했지만 RTSP 경로가 다르거나 인증 정보가 다릅니다.
    • 근본 원인: 카메라 기본 스트림이 H.265 위주인데, 표시 경로나 중간 구성요소가 기대대로 처리하지 못합니다.
    • 해결 순서: ffprobe로 스트림 자체 검증 -> VLC 등 별도 플레이어 검증 -> Home Assistant 통합 문제 확인 순으로 나눕니다.

    2) 로그인 실패 차단이 정상 사용자에게 걸리는 경우

    이건 거의 항상 프록시 신뢰 설정이나 앱 URL 설정 문제였습니다. Home Assistant가 실제 사용자 흐름을 충분히 구분하지 못하면, 여러 사용자의 실패 시도가 한 지점에 뭉쳐 보일 수 있습니다.

    # Home Assistant OS/SSH 애드온 환경에서 로그 보기
    ha core logs
    
    # 컨테이너 배포라면
    docker logs -f homeassistant
    
    # 프록시가 있다면 access log에서도 실제 전달 흐름 확인
    sudo tail -f /var/log/nginx/access.log
    • 같은 프록시 주소만 반복되면 프록시 로그와 Home Assistant 설정을 같이 확인합니다.
    • 짧은 시간에 여러 계정 실패가 보이면 외부 스캔인지, 비밀번호 관리자 자동 재시도인지 먼저 분리합니다.
    • 특정 모바일 앱만 반복 실패하면 저장된 이전 세션이나 잘못된 내부 URL을 물고 있는 경우가 많습니다.

    3) 홈랩 감시 시스템이 느려지는 경우

    이때 Home Assistant를 범인으로 단정하면 해결이 늦습니다. 병목은 대개 스트림 디코딩, 저장소 I/O, 데이터베이스 기록, 네트워크 재전송 중 하나에서 납니다.

    • 라이브 뷰가 느리다: 메인 스트림 대신 서브 스트림을 대시보드에 붙입니다.
    • 자동화는 되는데 화면이 버벅인다: 영상 처리와 상태 자동화를 같은 계층에서 무리하게 같이 하고 있지 않은지 봅니다.
    • DB가 커진다: recorder 범위를 줄여 카메라 관련 부수 이벤트까지 전부 기록하지 않게 합니다.
    • 가끔만 끊긴다: 무선 구간, 케이블, 스위치 상태를 보기 전에 tcpdump나 스위치 통계로 재전송 징후를 확인합니다.

    저는 보통 이렇게 결정합니다. 대시보드용 영상은 보기 편한 서브 스트림, 분석이나 녹화는 별도 구성요소, Home Assistant는 상태와 자동화 중심. 이렇게 역할을 나누면 성능 이슈가 생겨도 어디를 손봐야 할지 금방 좁혀집니다.

    7. 검증은 이렇게 합니다: 연결 성공보다 중요한 확인 포인트

    연동이 끝난 직후보다, 하루 지나고 일주일 지나도 같은 상태가 유지되는지가 더 중요합니다. 저는 아래 항목을 통과해야 “붙였다”고 봅니다.

    1. 외부 노출 검증: 외부 네트워크에서 카메라 RTSP와 웹 관리 포트가 직접 보이지 않는지 확인합니다.
    2. 권한 검증: Home Assistant용 계정으로 카메라 설정 변경, 사용자 추가, 네트워크 변경이 안 되는지 확인합니다.
    3. 이벤트 검증: 실제 움직임을 만들어 엔티티 상태 변화, 알림, 로그 타임스탬프가 이어지는지 봅니다.
    4. 재부팅 복구 검증: 카메라 재부팅 후 스트림 재연결과 자동화 복구가 되는지 확인합니다.
    5. 로그 품질 검증: 프록시 로그, Home Assistant 로그, 카메라 로그에서 시간과 출발지 정보가 어떻게 남는지 교차 확인합니다.

    판단 기준도 애매하게 두지 않는 게 좋습니다. 저는 이렇게 봅니다. 엔티티 상태가 안 바뀌면 이벤트 수집 문제, 상태는 바뀌는데 알림이 안 오면 자동화 문제, 알림은 오는데 영상만 안 보이면 스트림 문제입니다. 이 순서로 자르면 디버깅 시간이 꽤 줄어듭니다.

    Home Assistant IP 카메라 대시보드 검증 이미지

    연동 후 검증해야 할 대시보드 요소와 이벤트 이력을 보여주는 결과 예시입니다.

    8. 운영하면서 지키면 좋은 습관

    • 카메라별 정책 분리: 실내 카메라와 실외 카메라를 같은 권한, 같은 노출 정책으로 묶지 않습니다.
    • 문서화: 카메라 모델, IP, 스트림 URL, Home Assistant 계정, 관리 계정 위치를 남겨둡니다.
    • 변경 이력 관리: 펌웨어 업데이트 뒤 RTSP 경로, ONVIF 동작, 이벤트 엔티티 이름이 바뀌었는지 확인합니다.
    • 정기 점검: 포트 스캔, 계정 점검, 방화벽 정책, 외부 노출 여부를 주기적으로 다시 봅니다.
    • 백업 분리: Home Assistant 설정 백업과 카메라 설정 백업을 같은 위치에만 두지 않습니다.

    운영 습관에서 제일 체감이 컸던 건 문서화였습니다. 카메라가 1대일 때는 기억으로 버티지만, 3대만 넘어가도 “이 스트림이 메인인가 서브인가”, “이 계정이 관리자였나 읽기 전용이었나” 금방 헷갈립니다. 그때부터는 문제 해결보다 기억 복구에 시간을 더 쓰게 되더라고요.

    같이 읽으면 좋은 글도 내부 링크로 묶어두세요. 예를 들어 Home Assistant 관련 가이드나 네트워크 분리, 리버스 프록시 설정 글을 함께 연결해두면 검색 유입 후 체류 시간이 확실히 좋아집니다.

    9. FAQ와 마지막 권고

    Q. Home Assistant에서 카메라만 보이면 끝 아닌가요?

    아닙니다. 보이는 건 기능 확인일 뿐이고, 보안 확인은 따로 해야 합니다. 관리 포트 노출, 계정 권한, 외부 접근 경로, 로그 품질까지 같이 봐야 실제 운영 기준을 통과합니다.

    Q. ONVIF를 끄고 RTSP만 쓰는 게 더 안전한가요?

    PTZ나 카메라 이벤트가 필요 없으면 공격면을 줄이는 데 도움이 됩니다. 반대로 이벤트 연동이 필요하면 ONVIF를 쓰되, 관리자 계정 대신 최소 권한 계정으로 붙이는 편이 훨씬 현실적입니다.

    Q. 가장 먼저 하나만 하라면 뭘 하시겠어요?

    카메라 네트워크 분리입니다. 그다음이 Home Assistant 전용 읽기 계정, 마지막이 외부 접근 경로 정리입니다. 이 순서가 체감 효과가 제일 컸습니다.

    제 권고는 명확합니다. 집에서 소규모로 운영하고, 실사용 안정성이 더 중요하다면 VPN + 카메라 전용 네트워크 + Home Assistant 전용 읽기 계정으로 가세요. 반대로 여러 내부 서비스를 함께 공개해야 하고 인증과 접근 제어를 중앙에서 관리해야 한다면 리버스 프록시 + 좁게 잡은 trusted proxy + 로그 검증 체계가 맞습니다. 직접 포트포워딩은 편한 시작처럼 보여도 운영 부채가 큽니다. 저는 이제 카메라가 잘 뜨는지보다, 몇 달 뒤에도 조용히 제한된 상태로 잘 도는지를 더 중요하게 봅니다.

    Home Assistant IP 카메라 보안 체크리스트 요약 이미지

    네트워크 분리, 계정 최소 권한, 외부 접속 방식 선택을 한 장으로 정리한 요약 이미지입니다.

  • [HomeLabs] Frigate NVR 도입 사례: 홈랩 보안 카메라 설계 체크포인트

    [HomeLabs] Frigate NVR 도입 사례: 홈랩 보안 카메라 설계 체크포인트

    Frigate NVR 도입 사례: 홈랩 보안 카메라 설계 체크포인트

    Frigate NVR를 검토하게 된 이유는 단순했습니다. 카메라를 몇 대만 붙여도 녹화는 되는데, 막상 필요한 장면을 찾는 시간이 너무 오래 걸리더라고요. 저도 처음엔 RTSP만 받아 저장하면 끝일 줄 알았는데, 실제 운영에선 이벤트 기준으로 좁혀보는 기능이 없으면 사람이 영상을 관리하는 시간이 훨씬 더 많이 들었습니다.

    그래서 본격적으로 본 게 Frigate NVR였습니다. 오픈소스라는 점도 매력적이지만, 더 중요한 건 객체 감지 이벤트를 기준으로 녹화, 검색, 자동화를 한 흐름으로 묶기 쉽다는 점이었습니다. 다만 이건 설치형 앱보다는 운영형 시스템에 가깝습니다. 잘 맞는 환경에 넣으면 진짜 편한데, 안 맞는 환경에 억지로 올리면 전용 NVR보다 더 손이 갑니다.

    이번 글은 기능 소개보다 도입 판단 기준에 초점을 맞췄습니다. 홈랩에서 제가 실제로 먼저 보는 항목, 어디서 병목이 생기는지, 어떤 구성은 오래 가고 어떤 구성은 금방 흔들리는지를 경험 중심으로 정리했습니다. Home Assistant 자동화나 홈랩 네트워크 설계를 함께 보고 계신다면, 이 글을 기준 글로 삼아 연결해서 읽어보셔도 좋습니다.

    Frigate NVR 기반 홈랩 보안 카메라 전체 아키텍처 이미지

    카메라, Frigate, MQTT, Home Assistant, 저장소가 어떻게 연결되는지 한눈에 보여주는 구성도입니다.

    1. 왜 Frigate NVR를 보게 되었나

    전용 NVR은 장점이 분명합니다. 설치가 빠르고 제조사 앱이 있고, 웬만하면 바로 녹화가 되니까요. 그런데 홈랩을 굴리다 보면 아쉬움이 생깁니다. 특정 브랜드에 종속되거나, 이벤트를 외부 시스템으로 자연스럽게 내보내기 어렵거나, 저장 정책이 내 운영 방식과 딱 안 맞는 경우가 생각보다 많습니다.

    Frigate NVR를 보게 되는 분들은 보통 비슷한 지점에서 고민합니다. 영상 자체보다 영상에서 발생한 사건을 다루고 싶을 때죠. 사람이 지나갔는지, 차량이 들어왔는지, 특정 시간대에만 알림을 줄 건지, Home Assistant에서 조명이나 사이렌과 연결할 건지 같은 요구가 붙는 순간 전용 장비의 편의성보다 데이터 흐름의 유연성이 더 중요해집니다. 저도 딱 그 지점에서 Frigate가 훨씬 현실적으로 느껴졌습니다.

    반대로 영상 보관만 안정적으로 되면 충분한 환경이라면 얘기가 달라집니다. 카메라 2~4대 정도에서 문제가 생기면 타임라인만 훑어보는 수준이라면 전용 NVR이 더 잘 맞을 수 있습니다. Frigate의 진짜 가치는 AI라는 단어 자체보다 운영 자동화와 검색 비용 절감에 있습니다. 이 축이 중요하지 않으면, 도입 난이도 대비 이익이 크지 않을 수 있습니다.

    2. Frigate NVR 핵심 개념 쉽게 정리

    처음 보면 기능이 많아 보여도, 실제로는 세 가지 흐름만 이해하면 됩니다. 입력 영상, 감지 파이프라인, 저장과 이벤트 소비 경로입니다. 이걸 한 번에 보면 복잡한데, 나눠 보면 의외로 단순합니다.

    • 입력 영상: 카메라가 내보내는 메인 스트림과 서브 스트림입니다. 코덱, 해상도, 프레임레이트가 전체 시스템 성격을 거의 정합니다.
    • 감지 파이프라인: Frigate가 FFmpeg로 스트림을 읽고, 감지용 프레임을 detector에 넘기고, 추적 결과를 이벤트로 묶는 흐름입니다.
    • 이벤트 소비 경로: 녹화 저장, 스냅샷 생성, MQTT 발행, Home Assistant 자동화가 여기에 걸립니다.

    실무적으로 중요한 건 Frigate가 얼마나 똑똑한가보다 어떤 입력을 어떤 비용으로 처리하게 만들었는가입니다. 저는 보통 카메라 설정 50, 저장소 30, Frigate 설정 20 정도 감각으로 봅니다. 숫자는 비유지만, 체감상 병목은 Frigate 바깥에서 먼저 시작되는 경우가 많았습니다. 특히 서브 스트림 없이 메인 스트림 하나로 detect와 record를 같이 처리하는 구성은 처음엔 쉬워 보여도 운영 피로도가 빨리 올라가더라고요.

    어떤 환경에 특히 잘 맞나

    시나리오 Frigate 적합도 실무 판단
    Home Assistant 자동화 중심 홈랩 높음 이벤트를 MQTT, 대시보드, 알림, 조명 자동화로 자연스럽게 연결하기 좋습니다.
    그냥 녹화만 필요한 단순 환경 중간 이하 검색성과 자동화가 핵심 요구가 아니라면 전용 NVR이 구축과 운영 모두 단순합니다.
    여러 제조사 카메라 혼합 운영 높음 RTSP 기준으로 묶기 쉬워 벤더 종속을 줄이기 좋습니다.
    저전력 장비 한 대에 감지, 녹화, 자동화 몰아넣기 주의 CPU보다 디코딩, 디스크 I/O, 네트워크 품질, 하드웨어 가속 지원에서 먼저 무너질 가능성이 큽니다.
    보안 장비처럼 손 덜 대고 오래 굴리고 싶은 환경 낮음 업데이트, 로그 확인, 저장 정책 조정이 계속 필요할 수 있어 운영형 시스템에 가깝습니다.

    3. Frigate NVR 설계에서 먼저 볼 것

    처음 붙일 때 CPU와 메모리부터 보는 경우가 많은데, 제 경험상 우선순위는 조금 다릅니다. 먼저 봐야 할 건 스트림 분리 가능 여부, 카메라 코덱 설정, 저장소 분리, 네트워크 안정성입니다. 여기서 한 번 잘못 잡히면 Frigate 설정을 아무리 손봐도 운영이 쉽게 안정되지 않습니다.

    1. 카메라가 메인 스트림과 서브 스트림을 따로 제공하는지 확인합니다.
    2. 감지는 서브 스트림, 녹화는 메인 스트림으로 분리할 수 있는지 먼저 봅니다.
    3. 카메라가 H.264처럼 브라우저와 환경에서 다루기 쉬운 코덱을 안정적으로 내보내는지 확인합니다.
    4. 저장소를 OS 디스크와 분리할지, 최소한 미디어 경로를 분리할지 결정합니다.
    5. Home Assistant 연동이 필요하면 MQTT 브로커 위치를 먼저 정합니다.
    6. 원격 접근은 공개 포트 직결보다 VPN 또는 신뢰된 역방향 프록시 뒤에 둘지 결정합니다.

    여기서 자주 나오는 실수가 두 가지 있습니다. 첫째, 감지에 고해상도 메인 스트림을 그대로 넣는 경우입니다. 이렇게 하면 detect 정확도가 무조건 좋아지는 게 아니라, 디코딩 비용과 지연만 커지는 경우가 더 많습니다. 둘째, 녹화까지 서브 스트림으로 처리하는 경우입니다. 이벤트는 잘 잡혀도 나중에 확인할 때 디테일이 부족해 아쉬움이 크게 남습니다.

    결국 detect는 처리비용, record는 증거품질이라는 기준으로 따로 생각해야 합니다. 저도 예전에 현관 카메라 하나에 메인 스트림만 걸고 detect와 record를 동시에 맡겼다가 밤에 크게 돌아간 적이 있습니다. 낮에는 얼핏 괜찮았는데 밤이 되니까 UI 반응이 느려지고 감지 시점도 뒤로 밀리더라고요. 한참 Frigate 설정만 만졌는데, 실제 원인은 카메라 메인 스트림이 과했고 저장 디스크까지 같이 바빴던 거였습니다.

    4. 실전 구현: Docker Compose로 Frigate 띄우기

    홈랩에서는 컨테이너 기반 운영이 가장 다루기 편한 편입니다. 다만 compose 파일은 실행만 되는 예시보다, 왜 이렇게 두는지 설명되는 예시가 훨씬 중요합니다. 아래 예시는 현재 Frigate 공식 문서 흐름에 맞춘 최소 구성 기준으로 정리했습니다.

    1) 디렉터리 준비

    sudo mkdir -p /opt/frigate/config
    sudo mkdir -p /srv/frigate/media
    sudo mkdir -p /srv/frigate/db
    
    # 권한 점검: 컨테이너가 읽고 쓸 수 있는지 먼저 확인
    sudo chown -R $USER:$USER /opt/frigate /srv/frigate
    ls -ld /opt/frigate/config /srv/frigate/media /srv/frigate/db
    

    저는 설정 경로와 미디어 경로를 분리합니다. 백업 정책이 다르기 때문입니다. 설정과 DB는 보존 가치가 높고 용량은 작습니다. 반면 이벤트 클립과 녹화 파일은 금방 커집니다. 이 둘을 같은 디스크, 같은 백업 정책으로 묶어두면 나중에 운영이 꽤 지저분해집니다.

    2) docker-compose.yml 예시

    services:
      frigate:
        container_name: frigate
        image: ghcr.io/blakeblackshear/frigate:stable
        restart: unless-stopped
        stop_grace_period: 30s
        privileged: true
        shm_size: "512mb"
        ports:
          - "8971:8971"
          - "8554:8554"
          - "8555:8555/tcp"
          - "8555:8555/udp"
        environment:
          FRIGATE_RTSP_PASSWORD: "change-me"
        volumes:
          - /etc/localtime:/etc/localtime:ro
          - /opt/frigate/config:/config
          - /srv/frigate/media:/media/frigate
          - type: tmpfs
            target: /tmp/cache
            tmpfs:
              size: 1000000000
    

    shm_size를 대충 두면 디코딩과 버퍼 처리에서 애매한 문제가 납니다. UI만 보면 감지 문제처럼 보일 수 있는데, 실제로는 프레임 버퍼가 부족한 경우도 많습니다. tmpfs를 쓰는 이유도 비슷합니다. 공식 문서 기준으로도 녹화 세그먼트 캐시는 메모리 기반 /tmp/cache를 권장합니다. 물론 메모리 여유가 없는 장비라면 여기 숫자를 무리해서 크게 잡을 필요는 없습니다.

    하나 더 짚고 갈 부분이 있습니다. 최근 Frigate는 인증 UI와 API 접근에 8971 포트를 쓰는 구성을 기본으로 안내합니다. 5000 포트는 내부 네트워크용 비인증 접근으로 쓰는 흐름이라, 외부 노출 기준으로는 8971 쪽을 보는 게 더 안전합니다.

    Frigate NVR에서 메인 스트림과 서브 스트림을 분리한 설정 이미지

    메인 스트림은 녹화, 서브 스트림은 객체 감지에 쓰는 구성을 시각화한 이미지입니다.

    3) frigate 설정 파일 예시

    mqtt:
      enabled: true
      host: 192.168.0.10
      port: 1883
      topic_prefix: frigate
      client_id: frigate
    
    record:
      enabled: true
      motion:
        days: 3
      alerts:
        retain:
          days: 14
          mode: motion
      detections:
        retain:
          days: 14
          mode: active_objects
    
    snapshots:
      enabled: true
      retain:
        default: 7
    
    cameras:
      front_door:
        ffmpeg:
          inputs:
            - path: rtsp://user:{FRIGATE_RTSP_PASSWORD}@192.168.0.20:554/stream1
              roles:
                - record
            - path: rtsp://user:{FRIGATE_RTSP_PASSWORD}@192.168.0.20:554/stream2
              roles:
                - detect
        detect:
          enabled: true
          width: 1280
          height: 720
          fps: 5
        objects:
          track:
            - person
            - car
        snapshots:
          enabled: true
          bounding_box: true
          retain:
            default: 7
    

    여기서 봐야 할 건 문법보다 의미입니다. roles가 잘못 연결되면 기록은 남는데 감지가 안 되거나, 감지는 되는데 녹화 품질이 이상해질 수 있습니다. 그리고 예전 글에서 자주 보이던 record.events 형태보다, 현재 문서 기준으로는 record.alerts와 record.detections 구조를 기준으로 보는 편이 맞습니다.

    보존 기간도 처음부터 길게 잡지 않는 쪽을 권합니다. 파일 증가 속도를 먼저 확인하고 늘리는 게 훨씬 덜 피곤합니다. 특히 H.265는 저장 효율은 좋지만, 브라우저 호환성과 재생 환경을 같이 따져야 해서 홈랩이라면 초반엔 H.264 쪽이 더 다루기 편할 때가 많습니다.

    4) 컨테이너 실행과 상태 확인

    docker compose up -d
    docker compose ps
    docker logs --tail=200 frigate
    docker logs -f frigate
    

    설치 직후에는 에러 유무만 보지 말고, 어느 계층에서 끊기는지를 구분해서 봐야 합니다.

    • 연결 실패: 인증 오류, RTSP 경로 오타, 카메라 동시 접속 제한을 먼저 봅니다.
    • 디코딩 오류: 코덱 설정, 스트림 안정성, 공유 메모리 부족을 의심합니다.
    • 반복 재시작: 설정 문법, 볼륨 권한, 경로 쓰기 실패를 우선 확인합니다.
    • UI만 느림: 감지 엔진보다 디스크 I/O 또는 썸네일, 클립 생성 부하일 가능성이 큽니다.

    RTSP 스트림 자체가 안정적인지는 Frigate 바깥에서 꼭 따로 확인하는 게 좋습니다. 이걸 생략하면 원인이 뭉개져 보일 때가 많더라고요.

    ffprobe -hide_banner rtsp://user:[email protected]:554/stream1
    ffprobe -hide_banner rtsp://user:[email protected]:554/stream2
    

    이 단계에서 해상도, 코덱, 프레임 정보가 일관되지 않거나 접속이 들쑥날쑥하면, Frigate 이전 계층에서 이미 문제가 시작된 겁니다.

    5. Frigate NVR와 Home Assistant 연동 포인트

    Home Assistant 연동은 Frigate를 단순 NVR에서 운영 시스템으로 바꿔줍니다. 다만 실제로 중요한 건 연동 자체보다 어떤 이벤트를 어떤 문맥에서 쓸 것인가입니다. 기준 없이 다 알림으로 보내면 며칠 안 가서 사람이 알림을 무시하게 됩니다. 이거 진짜 금방 소음이 되더라고요.

    제가 권하는 방식은 이벤트를 세 단계로 나누는 겁니다. 기록용, 대시보드용, 즉시 알림용입니다. 예를 들어 사람 감지는 전부 기록하되, 심야 시간대 person 이벤트만 알림으로 보내고, 차량은 대시보드 위주로 소비하는 식입니다. Frigate가 잘 잡는다고 해서 모든 이벤트 가치가 같은 건 아니니까요.

    현재 기준으로는 Home Assistant의 공식 Frigate 통합을 우선 쓰는 편이 가장 깔끔합니다. 다만 MQTT 메시지 구조를 직접 확인하고 싶다면 아래처럼 먼저 브로커에서 실제 토픽을 보는 게 안전합니다.

    mqtt:
      sensor:
        - name: "Front Door Person Active Count"
          state_topic: "frigate/front_door/person/active"
          icon: mdi:account-alert
    

    수동 MQTT 센서를 쓸 수도 있지만, 먼저 실제 메시지가 올라오는지 눈으로 확인하는 게 더 중요합니다. 특히 frigate/front_door/person과 frigate/front_door/person/active는 의미가 다르기 때문에, 무턱대고 binary sensor로 고정하면 해석이 꼬일 수 있습니다.

    mosquitto_sub -h 192.168.0.10 -t 'frigate/#' -v
    
    # 특정 카메라 관련 이벤트만 좁혀보기
    mosquitto_sub -h 192.168.0.10 -t 'frigate/front_door/#' -v
    

    이 과정을 생략하면 Home Assistant 설정이 틀린 건지, MQTT가 안 오는 건지, Frigate가 이벤트를 못 만드는 건지 구분이 잘 안 됩니다. 저도 초반엔 HA 자동화가 문제인 줄 알고 붙잡고 있었는데, 실제론 Frigate 쪽 객체 추적 대상이 기대와 다르게 잡혀서 메시지가 아예 안 올라오던 적이 있었습니다.

    Home Assistant 연동으로 Frigate NVR 이벤트를 확인하는 대시보드 이미지

    객체 감지 이벤트가 센서와 알림으로 연결된 결과 예시를 보여주는 대시보드 이미지입니다.

    6. 운영 중 자주 만나는 문제와 해결법

    설치보다 운영이 더 어렵습니다. Frigate는 시작은 빠를 수 있어도, 오래 굴릴수록 문제의 성격이 달라집니다. 초반엔 설정 문법과 접속 오류가 많고, 조금 지나면 저장소, 네트워크, 카메라 품질 문제가 본격적으로 드러납니다. 아래는 제가 자주 본 실패 모드와 근본 원인입니다.

    사례 1: 녹화는 되는데 감지가 들쑥날쑥한 경우

    이 상황은 대개 Frigate 설정보다 입력 영상 품질 문제에서 시작합니다. 메인 스트림 하나에 detect와 record를 같이 물려두면 낮에는 버티다가 밤에 흔들리는 경우가 많습니다. 헤드라이트, IR 전환, 노이즈 억제, WDR 같은 카메라 내부 처리 영향이 생각보다 큽니다.

    1. detect용 스트림을 서브 스트림으로 분리합니다.
    2. 카메라의 WDR, 노이즈 억제, 야간 모드 전환 시점을 다시 봅니다.
    3. 객체 종류를 너무 넓게 잡지 말고 필요한 클래스만 추적합니다.
    4. 로그와 이벤트 시간을 비교해 지연이 감지 문제인지 디코딩 지연인지 구분합니다.

    핵심은 Frigate를 더 만지기 전에 카메라를 먼저 의심하는 것입니다. 영상이 흔들리는데 감지만 더 똑똑해지길 기대하면 답이 잘 안 나옵니다.

    사례 2: 디스크가 버벅이면서 UI까지 느려지는 경우

    연속 녹화, 스냅샷, 이벤트 클립이 쌓이면 저장소 쓰기 패턴이 꽤 거칠어집니다. 특히 OS, 컨테이너 데이터, 미디어 파일을 같은 디스크에 몰아두면 녹화는 되는데 보는 순간 느린 상태가 나옵니다. 이건 CPU 부족처럼 보여도 실제론 I/O 대기인 경우가 많습니다.

    iostat -x 1
    df -h
    du -sh /srv/frigate/media
    find /srv/frigate/media -type f | wc -l
    

    저는 여기서 %util, await, 파일 개수 증가 속도를 같이 봅니다. 이벤트가 몰릴 때만 순간적으로 튀는 건 자연스러울 수 있지만, 평시에도 디스크가 숨이 차 있으면 저장 정책을 먼저 줄여야 합니다. retain을 공격적으로 길게 잡는 것보다 실제 이벤트 밀도에 맞춰 보수적으로 시작하는 편이 훨씬 낫습니다.

    사례 3: ffmpeg 관련 오류가 반복되는 경우

    로그에 스트림 재연결이나 디코딩 오류가 반복될 때는 순서를 지켜서 봐야 합니다. Frigate 컨테이너만 재시작하는 건 보통 해결책이라기보다 증상 가리기에 가깝습니다.

    1. 카메라 RTSP URL이 정확한지 다시 확인합니다.
    2. 같은 스트림을 ffprobe로 직접 열어봅니다.
    3. 카메라 코덱과 해상도 설정이 과하지 않은지 봅니다.
    4. PoE 스위치, 케이블, 링크 상태를 포함한 네트워크 층을 확인합니다.
    ffprobe rtsp://user:[email protected]:554/stream1
    ping -c 20 192.168.0.20
    docker logs --tail=200 frigate | grep -iE 'error|fail|ffmpeg|restart'
    

    Frigate 문제처럼 보여도 원인이 카메라 펌웨어, 링크 품질, 전원 불안정인 경우가 꽤 많습니다. 저도 예전에 PoE 스위치 포트 상태가 애매해서 RTSP가 간헐적으로 끊기는 걸 한참 뒤에 찾았습니다. 이런 문제는 설정 파일만 만진다고 해결되지 않습니다.

    사례 4: 알림은 오는데 정작 볼 만한 이벤트가 너무 많은 경우

    이건 성능 이슈가 아니라 설계 이슈입니다. 시스템은 정상인데 사람이 못 쓰는 상태죠. 이벤트를 많이 잡는 것과 유용하게 잡는 건 다릅니다. 낮 시간 현관, 도로 변 카메라, 반사면이 많은 창가 카메라에서 특히 자주 생깁니다.

    1. 알림용 이벤트와 기록용 이벤트를 분리합니다.
    2. 밤 시간대, 특정 구역, 특정 객체만 알림 대상으로 좁힙니다.
    3. Home Assistant에서 조건부 자동화를 먼저 걸고 Frigate는 과도하게 만지지 않습니다.

    여기서 중요한 건 Frigate를 오탐 0% 도구로 기대하지 않는 겁니다. 운영자는 탐지 정확도보다 알림 가치 밀도를 높이는 쪽으로 설계하는 게 훨씬 현실적입니다.

    7. 검증: 무엇을 보고 정상 동작으로 판단할까

    정상 동작을 웹 UI가 뜬다로만 판단하면 결국 다시 손보게 됩니다. 최소한 이벤트 생성, MQTT 전달, 저장 정책, 재시작 복구, 조회 응답성까지 봐야 합니다. 저는 배포 직후와 카메라 추가 직후에 같은 체크리스트를 반복합니다.

    • 이벤트 생성 흐름: 사람이 지나갈 때 이벤트, 스냅샷, 클립이 일관되게 남는지 봅니다.
    • MQTT 전파: 이벤트가 브로커까지 실제로 전달되는지 확인합니다.
    • 스토리지 적재: /media/frigate 아래 파일이 예상한 보존 정책대로 누적, 정리되는지 확인합니다.
    • 재부팅 복구: 호스트 또는 컨테이너 재시작 후 자동으로 회복되는지 봅니다.
    • UI 응답성: 이벤트 목록 조회와 최근 영상 접근이 과도하게 느리지 않은지 확인합니다.

    검증할 때는 단순 모니터링보다 문제 분리용 명령이 필요합니다. 저는 아래 정도는 바로 돌려봅니다.

    docker stats
    ls -lah /srv/frigate/media
    du -sh /srv/frigate/media
    docker inspect frigate --format '{{.State.Status}}'
    docker restart frigate
    docker logs --since=2m frigate
    

    여기서 중요한 건 숫자 자체보다 변화 시점입니다. 사람이 지나갈 때만 CPU가 오르는 건 자연스러울 수 있습니다. 하지만 평소에도 높게 유지되면 감지 대상이 과하거나 스트림 구성이 비효율적일 가능성이 큽니다. 디스크도 마찬가지입니다. 총 용량보다 하루에 얼마나 늘어나는지, 이벤트 밀도가 높은 날에 정리 정책이 제대로 작동하는지가 더 중요합니다.

    이벤트 리스트, 클립 저장, 리소스 사용 상태를 함께 확인하는 운영 점검 이미지입니다.

    8. Frigate NVR 도입 전에 꼭 따져볼 선택 기준

    Frigate를 쓸지, 전용 NVR을 쓸지, 혼합 구성을 쓸지는 좋다 나쁘다로 고를 문제가 아닙니다. 운영 목표가 무엇인지에 따라 답이 달라집니다. 아래 표는 실제로 많이 받는 질문을 기준으로 정리한 것입니다.

    고민 포인트 이럴 땐 Frigate 이럴 땐 전용 NVR 또는 혼합 구성
    자동화가 중요한가 Home Assistant, MQTT, 조건부 알림까지 엮고 싶을 때 앱에서 보기만 하면 충분할 때
    설정보다 간편함이 중요한가 설정 파일과 로그 확인을 감수할 수 있을 때 초기 구축 시간을 짧게 가져가야 할 때
    카메라 제조사가 섞여 있는가 표준 스트림 기준 통합 운영이 필요할 때 한 브랜드로 통일돼 제조사 앱 완성도가 충분할 때
    저장소와 네트워크를 직접 관리할 자신이 있는가 스토리지, 백업, 접근제어까지 직접 설계할 때 운영 책임을 줄이고 싶을 때
    문제 해결 방식 로그와 계층별 점검으로 원인을 추적하는 데 익숙할 때 장비 교체 또는 벤더 지원 중심으로 해결하고 싶을 때

    제가 실무적으로 추천하는 기준은 단순합니다. 이벤트를 다른 시스템과 연결해 써야 하면 Frigate NVR, 영상 보관 자체가 목적이면 전용 NVR입니다. 그리고 이 둘을 혼합하는 것도 충분히 현실적인 선택입니다. 예를 들어 안정적인 연속 녹화는 전용 장비에 맡기고, 특정 카메라의 이벤트 자동화만 Frigate로 가져가는 구성도 꽤 괜찮습니다.

    즉, Frigate는 제품 하나 설치해서 끝나는 장비라기보다 카메라 품질, 네트워크 안정성, 스토리지, 자동화 요구사항을 묶어 운영하는 플랫폼에 가깝습니다. 홈랩을 즐기는 분께는 이 유연성이 큰 장점이지만, 손이 덜 가는 보안 장비를 원한다면 장점이 곧 관리 포인트가 되기도 합니다.

    9. 마무리: 이런 분께는 Frigate, 이런 분께는 다른 선택

    제가 직접 굴려보며 얻은 판단은 꽤 선명합니다. 카메라 2~4대 규모에서 Home Assistant 연동, 객체 감지 기반 알림, 이벤트 검색이 핵심이라면 Frigate NVR 쪽이 잘 맞습니다. 이 경우에는 처음부터 detect와 record를 분리하고, 미디어 저장 경로를 OS와 분리하고, MQTT 이벤트를 먼저 검증하는 순서로 들어가면 시행착오를 많이 줄일 수 있습니다.

    반대로 목표가 무조건 안정적으로 녹화하고 필요할 때 영상만 찾겠다는 쪽에 가깝다면 전용 NVR이나 제조사 클라우드 조합이 더 현실적입니다. 설정 파일 만지는 시간이 부담스럽고, 장애가 났을 때 로그를 따라가며 원인을 찾고 싶지 않다면 더더욱 그렇습니다. Frigate는 강력하지만, 공짜로 단순해지지는 않더라고요.

    한 줄로 정리하면 이렇습니다. 이벤트를 시스템으로 쓰고 싶으면 Frigate, 영상을 장비로 보관하고 싶으면 전용 NVR이 더 낫습니다. 저는 홈랩이라면 전자를 권하지만, 전제는 분명합니다. 스트림 분리, 저장소 분리, 이벤트 설계까지 같이 볼 생각이 있을 때 이야기입니다.

    바로 다음 단계로 넘어가실 분이라면, Frigate를 띄운 뒤 가장 먼저 할 일은 설정을 더 늘리는 게 아닙니다. 현관 카메라 하나만 붙여서 이벤트 생성, MQTT 전달, 디스크 증가 패턴, 재시작 복구까지 확인해보는 겁니다. 이 작은 파일럿을 통과하면 확장하면 되고, 여기서부터 흔들리면 카메라나 저장소 설계를 먼저 고치는 편이 훨씬 덜 돌아갑니다.

    Frigate NVR 도입 판단 기준을 정리한 홈 보안 카메라 비교 이미지

    어떤 환경에서 Frigate가 맞고, 어떤 경우 전용 NVR이 더 나은지 한 장으로 정리한 요약 이미지입니다.

  • [3D Printer] 고속 3D 프린터 비교: Bambu Lab P1S vs Creality K1 Max

    [3D Printer] 고속 3D 프린터 비교: Bambu Lab P1S vs Creality K1 Max

    고속 3D 프린터 비교: Bambu Lab P1S vs Creality K1 Max

    고속 3D 프린터 비교를 찾는 분들이 많은 이유는 단순합니다. 이제는 출력 속도 숫자보다, 빠른 장비가 작업 흐름 전체를 얼마나 덜 끊어 먹는지가 더 중요해졌거든요. 저도 홈랩에서 브래킷, 덕트, 하우징류를 반복 수정해 출력하다 보니 이 차이가 꽤 크게 느껴졌습니다. 같은 하루라도 한 번 더 수정해서 다시 뽑을 수 있는 장비와, 출력은 빨라도 세팅 확인과 재시도로 시간을 잡아먹는 장비는 체감이 완전히 다르더라고요.

    이번 글은 Bambu Lab P1S와 Creality K1 Max 사이에서 고민하는 분을 위한 실전형 가이드입니다. 스펙만 나열하지 않고, 내 작업에서 어떤 병목을 줄이고 싶은지를 기준으로 비교해보겠습니다. 공식 사양 기준으로 보면 P1S는 256 x 256 x 256mm급 빌드 볼륨과 AMS 연동, K1 Max는 300 x 300 x 300mm 빌드 볼륨과 대형 출력 대응이 핵심입니다. 이 관점으로 보면 선택이 훨씬 쉬워집니다.

    이 글은 “누가 더 좋냐”보다 “내 작업에서 뭐가 덜 발목을 잡느냐”를 가려내는 데 초점을 맞췄습니다.

    고속 3D 프린터 비교에서 왜 이 두 모델이 자주 묶일까요?

    둘 다 실제 판매 중인 CoreXY 계열 고속 FDM 장비라서 같은 후보군에 자주 올라옵니다. 다만 체감 포인트는 꽤 다릅니다. 비교 축 자체가 다르기 때문입니다. P1S는 장비 운영에 들어가는 개입 시간을 줄이는 쪽으로 많이 평가되고, K1 Max는 큰 파츠를 한 덩어리로 처리할 수 있는 공간 때문에 계속 언급됩니다.

    여기서 먼저 봐야 할 질문은 이겁니다. 내가 자주 겪는 불편이 출력 실패인지, 출력물 분할인지요. 전자는 워크플로우와 자동화 완성도 쪽으로 이어지고, 후자는 빌드 볼륨과 접합 공정 문제로 이어집니다. 실제 만족도는 이 차이에서 많이 갈립니다.

    • Bambu Lab P1S: 반복 성공률, 슬라이서-장비 연동, AMS 기반 다색 운용을 우선하는 사람에게 잘 맞습니다.
    • Creality K1 Max: 헬멧, 대형 외장, 긴 덕트, 일체형 커버처럼 분할이 싫은 작업에서 장점이 먼저 드러납니다.
    • 둘 다 빠른 장비지만, 최종 선택은 출력 공간, 세팅 개입 허용치, 운영 피로도, 후처리 공정에서 갈립니다.

    고속 3D 프린터 비교에서 꼭 봐야 할 기준

    속도 수치나 자동 보정 문구보다 먼저 봐야 할 건 네 가지입니다. 이건 스펙표보다 실제 만족도와 더 강하게 연결됩니다.

    1. 실패 1회당 비용: 필라멘트 값보다 다시 손대는 시간이 더 큰 손실인지 봐야 합니다.
    2. 최대 파츠 크기: 한 번에 출력해야 의미가 있는 부품인지, 분할 후 접합이 가능한 부품인지 구분해야 합니다.
    3. 재료 운용 범위: PLA 위주인지, PETG와 ABS/ASA까지 갈지에 따라 폐쇄형 구조의 체감 가치가 달라집니다.
    4. 운영 동선: 업로드, 대기, 확인, 재출력, 로그 확인까지 포함한 흐름이 매끄러운지 봐야 합니다.

    고속 3D 프린터 비교에서 흔한 실수는 “최고 속도”를 곧 “최고 생산성”으로 보는 겁니다. 실제 생산성은 첫 레이어 안정성, 냉각 여유, 진동 억제, 재료 상태가 같이 받쳐줄 때만 나옵니다. 속도를 올렸더니 링잉이나 모서리 번짐이 심해지면, 장식물은 넘어가도 끼워맞춤 하우징은 바로 재출력 사유가 되거든요.

    Bambu Lab P1S vs Creality K1 Max 핵심 비교 표

    비교 항목 Bambu Lab P1S Creality K1 Max 실무 판단 포인트
    구매 이유가 되는 핵심 장점 반복 작업의 마찰을 줄이기 쉬운 워크플로우 더 큰 출력물에 대응하기 쉬운 300mm급 공간 병목이 재시도면 P1S, 분할/접합이면 K1 Max 쪽이 더 잘 맞습니다.
    출력 공간 256 x 256 x 256mm 300 x 300 x 300mm 베드를 자주 꽉 채우는지 슬라이서에서 먼저 확인해보는 게 좋습니다.
    운영 성격 결과물을 빠르게 반복 확보하는 쪽에 강점 장비와 세팅을 만지면서 공간 이점을 살리는 쪽에 강점 장비를 도구로 볼지, 취미의 일부로 볼지에 따라 체감이 크게 달라집니다.
    다색 출력 AMS 연계가 실제 선택 사유가 되기 쉬움 비교의 중심은 다색보다 대형 출력 쪽 다색이 반복 작업이나 납품물에 들어가면 P1S 쪽 무게가 커집니다.
    실패했을 때 복구 체감 원인 분리가 비교적 단순하게 느껴질 수 있음 기기 상태와 튜닝 개입도에 따라 편차가 생길 수 있음 원인을 빨리 좁혀야 하는 환경이면 P1S가 더 편하게 느껴질 수 있습니다.
    추천 사용자 반복 출력, 다색, 작업 효율 중심 사용자 큰 외장, 대형 코스프레 파츠, 일체형 부품 중심 사용자 자주 만드는 물건의 형상이 최종 결정권자입니다.

    실사용 관점에서 한 줄로 줄이면 이렇습니다. P1S는 시간 낭비를 줄이는 프린터이고, K1 Max는 분할과 접합을 줄이는 프린터입니다. 이 프레임으로 보면 판단이 꽤 또렷해집니다.

    실전으로 보는 구매 판단법: 어떤 작업을 할 건가요?

    프린터를 먼저 고르면 후회 포인트가 생길 때가 많습니다. 먼저 적어야 하는 건 모델명보다 내가 반복해서 만드는 물건의 패턴입니다. 이걸 써보면 생각보다 금방 답이 나옵니다.

    1. 최근 10개 출력물 중 가장 큰 파츠가 무엇이었는지 적습니다.
    2. 그 파츠가 한 덩어리여야 하는지, 분할 후 접합해도 되는지 구분합니다.
    3. 한 번 실패했을 때 다시 돌리기 귀찮은 작업인지 판단합니다.
    4. PLA 중심인지, PETG/ABS/ASA 비중이 있는지 적습니다.
    5. 다색이 장식용 재미인지, 실제 납품물이나 프로토타입 가독성에 필요한지 봅니다.

    예를 들어 네트워크 장비 브래킷, 센서 하우징, 팬 덕트, 케이블 가이드처럼 치수 수정이 잦은 기능성 파츠를 계속 만진다면 P1S 쪽 점수를 더 높게 줄 만합니다. 이런 작업은 한 번 크게 뽑는 것보다 수정-재출력-재조립 사이클이 빠른 게 훨씬 중요하거든요. 반대로 헬멧 쉘, 긴 덮개, 일체형 패널처럼 분할 흔적과 접합선이 바로 품질 문제가 되는 작업이라면 K1 Max의 큰 빌드 공간이 훨씬 직접적인 이점이 됩니다.

    고속 3D 프린터 비교에서 출력 크기와 재료 기준으로 선택하는 의사결정 이미지

    이렇게 작업 형상과 실패 비용을 같이 보면, “더 큰 게 무조건 좋다”는 착시가 꽤 빨리 사라집니다.

    실전 구현: 슬라이서와 G-code로 먼저 검증해보는 방법

    장비를 사기 전에 꼭 권하고 싶은 절차가 있습니다. 내 STL이 실제로 어느 정도 베드를 먹는지, 분할이 필요한지, 서포트가 얼마나 붙는지를 슬라이서에서 먼저 확인하는 겁니다. 이 단계만 해도 “나는 큰 프린터가 꼭 필요하다”는 생각이 사실인지 꽤 빨리 드러납니다.

    # PrusaSlicer CLI 옵션 확인
    prusa-slicer --help
    
    # 기존 설정 파일을 불러와 G-code 생성
    # profile.ini에는 프린터/필라멘트/프린트 설정이 저장되어 있어야 합니다.
    prusa-slicer --load profile.ini --export-gcode enclosure_bracket.stl
    
    # 여러 부품을 한 번에 넣어 베드 점유와 배치 가능 여부 확인
    prusa-slicer --load profile.ini part_a.stl part_b.stl part_c.stl --export-gcode

    여기서 봐야 할 건 세 가지입니다. 첫째, 단일 모델이 베드 가장자리를 자주 침범하는지. 둘째, 자동 회전이나 배치 없이는 들어가지 않는지. 셋째, 서포트와 브림이 붙는 순간 실사용 면적이 얼마나 커지는지입니다. 모델 본체만 간신히 들어가는 경우는 실제 출력 여유가 부족한 경우가 많습니다.

    시작 G-code는 장비를 바꿔도 해석 기준이 크게 달라지지 않습니다. 아래처럼 가장 기본적인 온도, 홈, 속도 배율, 유량 배율부터 보면 감이 빨리 잡힙니다.

    ; baseline start sequence for a test print
    M140 S60      ; set bed temperature
    M104 S200     ; set nozzle temperature
    M190 S60      ; wait for bed to reach target
    M109 S200     ; wait for nozzle to reach target
    G28           ; home all axes
    M220 S100     ; speed factor 100%
    M221 S100     ; flow factor 100%
    • G28 직후 위치가 매번 어긋나면 단순 설정보다 센서, 엔드스톱, 기구 오차 쪽을 먼저 봐야 합니다.
    • M190과 M109 대기 시간이 비정상적으로 길면 히터 출력, 전원 상태, 주변 온도, 펌웨어 제한을 의심해볼 만합니다.
    • M220과 M221는 출력 도중 임시 조정에 쓰이지만, 이 값으로 문제를 덮는 습관은 좋지 않습니다. 반복해서 값을 만져야 한다면 원인은 대개 프로파일, 냉각, 유량 보정 쪽에 있습니다.

    장비 비교 때 G-code를 굳이 같이 보는 이유도 여기에 있습니다. 좋은 프린터는 출력물만 잘 나오는 게 아니라 문제 원인도 빨리 좁혀줘야 하거든요. 똑같이 실패해도 원인을 읽기 쉬운 장비가 실전에서는 더 강합니다.

    실전 구현 2: 네트워크와 로그 관점에서 보는 운영 편의성

    고속 장비를 쓰기 시작하면 출력 시간만 줄어드는 게 아닙니다. 자연스럽게 원격 업로드, 상태 확인, 카메라 모니터링, 실패 알림 의존도가 올라갑니다. 이때 체감 품질은 기계 사양보다 네트워크 안정성과 파일 전송 흐름에서 갈리는 경우가 많습니다.

    # 프린터 응답 확인
    ping -c 4 192.168.0.50
    
    # 웹 UI 또는 업로드 포트 확인
    nc -vz 192.168.0.50 80
    nc -vz 192.168.0.50 443
    
    # HTTP 헤더 응답이 오는지 확인
    curl -I http://192.168.0.50/
    
    # 업로드 지연이 네트워크 구간 문제인지 경로 확인
    traceroute 192.168.0.50

    이 절차는 꽤 실용적입니다. 슬라이서 업로드가 느릴 때 프린터 문제로 오해하는 경우가 많지만, ping 손실이나 curl -I 지연이 보이면 프린터보다 AP 상태, 무선 간섭, DHCP 재할당, 메시 로밍 같은 바깥 원인을 먼저 봐야 합니다. 출력 실패가 아니라 전송 실패인데 프린터 탓으로 돌리면 시간만 날리기 쉽습니다.

    로그를 읽을 때도 기준이 있어야 합니다.

    • ping 패킷 손실이 있으면 장비 세팅보다 네트워크 안정화가 먼저입니다.
    • nc -vz 실패는 서비스 포트 비활성, 방화벽, IP 변경 가능성을 먼저 의심하는 게 맞습니다.
    • 업로드는 느린데 장비 UI 접속은 빠르면 저장장치 쓰기나 무선 전송 병목 가능성이 있습니다.
    • 원격 화면은 뜨는데 명령 반영이 늦으면 장비 CPU 점유나 내부 큐 적체도 같이 봐야 합니다.
    고속 3D 프린터 비교 글의 홈랩 원격 모니터링 작업 환경 이미지

    운영 편의성은 스펙표에 잘 안 드러납니다. 그런데 실제 만족도는 이런 주변 흐름에서 크게 갈립니다. 재료별 세팅이나 네트워크 구성 팁이 궁금하다면 블로그의 다른 3D 프린터 구매 가이드 글도 같이 보면 도움이 됩니다.

    주의사항: 속도보다 먼저 봐야 하는 실패 패턴

    빠른 장비라고 해서 대충 돌려도 버텨줄 거라고 기대하면 거의 틀립니다. 오히려 고속 영역으로 갈수록 원래 있던 약점이 더 또렷하게 드러납니다. 자주 보는 실패 패턴은 아래 셋입니다.

    1. 대형 출력이 잦은데 중형 베드에서 계속 분할로 버티는 경우

    분할 자체는 가능하지만, 문제는 그다음입니다. 접합면 보정, 정렬, 퍼티 작업, 강도 저하, 표면 후처리까지 다 포함하면 실제 작업 시간은 오히려 더 길어질 수 있습니다. 분할이 비용인지 기술인지를 먼저 따져야 합니다. 장식용 코스프레 부품은 후처리로 버틸 수 있어도, 하중 받는 기능성 파츠는 접합부가 바로 약점이 되기 쉽습니다.

    2. ABS/ASA를 쓸 계획인데 챔버 관리와 건조를 가볍게 보는 경우

    둘 다 폐쇄형 구조의 이점은 있지만, 그걸로 끝나진 않습니다. 고속 출력에서 ABS/ASA 문제가 반복될 때 프린터 자체보다 더 먼저 봐야 하는 건 필라멘트 수분, 챔버 온도 안정성, 외풍, 배기 흐름입니다. 워핑과 레이어 분리는 재료와 환경이 같이 만든 문제인 경우가 많습니다. 장비가 좋아도 건조 안 된 필라멘트는 표면과 접착에서 바로 티가 나더라고요.

    3. 멀티컬러를 막연히 “있으면 좋지” 수준으로 고르는 경우

    다색 출력은 분명 매력적이지만, 모든 사용자에게 효율적인 건 아닙니다. 색 전환이 잦은 모델은 시간, 필라멘트 낭비, 실패 시 재출력 부담이 같이 커집니다. 색이 정보 전달용인지, 완성품 미관용인지, 도색 대체인지를 먼저 나눠야 합니다. 프로토타입에서 부품 구분용으로 쓰는 다색은 가치가 크지만, 장식 목적이면 후처리 쪽이 더 나은 경우도 있습니다.

    결국 프린터 선택 실패는 속도 부족보다 자기 작업 유형을 잘못 정의한 데서 시작하는 경우가 많습니다. P1S와 K1 Max는 누가 절대 우위라기보다, 서로 다른 문제를 먼저 해결해주는 장비라고 보는 편이 더 정확합니다.

    검증 포인트: 출력 결과를 어떻게 읽고 판단할까?

    장비를 들인 뒤에는 “잘 나온다”로 끝내면 개선이 멈춥니다. 출력 결과를 네 항목으로 나눠 읽어보면 세팅 방향이 꽤 선명해집니다.

    1. 첫 레이어: 면이 들쭉날쭉하면 레벨링뿐 아니라 베드 오염, Z 오프셋, 재료 상태도 같이 의심해야 합니다.
    2. 코너와 벽면: 고스팅, 링잉, 미세 파형은 진동 억제와 속도 프로파일 문제가 숨어 있을 때가 많습니다.
    3. 오버행과 브리징: 처짐이 심하면 냉각량, 최소 레이어 시간, 출력 순서가 핵심입니다.
    4. 치수 정확도: 끼워맞춤이 빡빡하거나 헐거우면 유량, 수축, 벽 두께 보정 문제를 먼저 봅니다.

    중요한 건 임의의 숫자를 외워 맞추는 게 아닙니다. 같은 모델, 같은 필라멘트, 같은 방향으로 반복 출력해 변화만 비교해야 합니다. 벤치류, 브리징 테스트, 치수 블록 같은 고정 테스트를 하나 정해두면 장비가 아니라 세팅이 바뀐 결과를 읽기 쉬워집니다.

    # G-code 안에서 속도/온도/유량 관련 명령만 추려보기
    grep -E '^(G0|G1|M104|M109|M140|M190|M220|M221)' test_print.gcode | head -n 80
    
    # 층수 힌트나 이동량을 대략 확인해 비교 포인트 잡기
    grep -c '^;LAYER:' test_print.gcode
    grep -c '^G1' test_print.gcode

    이런 식으로 G-code를 조금만 들여다봐도, “이상하게 느리다”, “팬이 너무 늦게 돈다”, “온도 대기가 길다” 같은 감각적인 불만을 더 구체적인 점검 항목으로 바꿀 수 있습니다. 이 단계가 생각보다 중요합니다. 프린터가 좋아도 문제를 읽는 방식이 없으면 같은 시행착오를 계속 돌게 되거든요.

    고속 3D 프린터 비교 후 출력 품질을 검증하는 테스트 결과 이미지

    출력 품질을 항목별로 읽기 시작하면, 장비 교체가 필요한 순간과 프로파일 수정으로 끝날 순간이 분명하게 갈립니다.

    자주 묻는 질문: 3D 프린터 추천을 부탁받으면 저는 이렇게 답합니다

    P1S가 더 초보자 친화적인가요?

    대체로는 그렇다고 답할 만합니다. 특히 “장비를 배우는 시간”보다 “결과물을 빨리 얻는 시간”이 중요한 분에게 그렇습니다. 출력 반복이 잦고 다색 계획도 있다면 더 그렇고요. 처음부터 장비와 너무 오래 씨름하고 싶지 않다면 P1S 쪽이 더 편하게 느껴질 가능성이 큽니다.

    K1 Max는 어떤 사람에게 더 맞나요?

    크기 제약 때문에 작업 설계 자체가 자주 바뀌는 사람에게 더 잘 맞습니다. 큰 파츠를 자주 나눠야 하고, 접합과 후처리가 반복 스트레스라면 K1 Max가 훨씬 설득력 있습니다. 장비 셋업과 세부 조정에 어느 정도 손이 가도 괜찮다면 만족도가 높아질 수 있습니다.

    고속 3D 프린터 비교에서 가장 후회 없는 기준 하나만 꼽자면요?

    최근에 실제로 뽑은 모델의 최대 외곽 크기입니다. 그다음이 실패했을 때 다시 만지는 시간이죠. 속도 숫자는 마지막 판단 요소에 가깝습니다.

    마지막 권고: 이런 경우엔 P1S, 저런 경우엔 K1 Max

    Bambu Lab P1S는 이런 분께 더 잘 맞습니다.

    • 반복 수정과 재출력이 잦아서 워크플로우 효율이 중요합니다.
    • 장비보다 출력물과 작업 속도를 우선합니다.
    • AMS를 포함한 다색 출력이 실제 작업 계획 안에 있습니다.
    • 대부분의 출력물이 중형 범위에 머물고, 대형 일체형 파츠 비중이 낮습니다.

    Creality K1 Max는 이런 분께 더 맞습니다.

    • 큰 출력물이 자주 필요해서 분할과 접합이 진짜 비용으로 느껴집니다.
    • 헬멧, 패널, 커버, 긴 하우징처럼 일체형이 유리한 파츠를 자주 만듭니다.
    • 장비 세팅과 튜닝 개입을 어느 정도 감수할 수 있습니다.
    • 구매 이유의 중심이 속도보다 공간에 있습니다.

    정리하면 반복 작업과 운영 효율이 우선이면 P1S, 대형 파츠와 분할 회피가 우선이면 K1 Max 쪽이 후회가 적습니다. 작업물 크기가 자주 제약이 되지 않는다면 P1S가 더 많은 사람에게 맞고, 크기 제약이 설계 자체를 자꾸 바꾸는 단계라면 K1 Max가 더 직접적인 해답이 됩니다.

    고속 3D 프린터 비교의 최종 선택 기준을 보여주는 요약 인포그래픽

    구매 직전에는 스펙표보다 최근 출력 기록을 다시 보세요. 무엇을 얼마나 자주, 어떤 이유로 다시 뽑았는지 적어보면 답이 꽤 선명해집니다.

  • [보안] Trivy 도입 1년 회고: DevSecOps 워크플로우 변화와 개선점 분석

    [보안] Trivy 도입 1년 회고: DevSecOps 워크플로우 변화와 개선점 분석

    본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.

    Trivy 도입 회고를 1년치로 다시 적어보면, 제가 얻은 가장 큰 변화는 탐지율보다도 의사결정 방식의 변화였습니다. 전에는 취약점 공지가 뜨면 운영팀이 먼저 불안해했고, 개발팀은 “우리 코드 문제인지 베이스 이미지 문제인지”부터 감으로 추정했었죠. Trivy를 붙인 뒤에는 최소한 그 대화가 구조화됐습니다. 어떤 단계에서 막을지, 무엇을 예외로 둘지, 어떤 결과는 즉시 수정이고 어떤 결과는 추적 대상으로 둘지 기준선이 생겼거든요.

    중요한 건 여기입니다. Trivy는 보안을 완성해주는 도구가 아니라, 컨테이너와 IaC 영역에서 팀의 판단을 표준화해주는 도구에 가깝습니다. 이걸 잘못 기대하면 “스캔은 도는데 사고는 왜 나지?”라는 반응이 나오고, 반대로 위치를 정확히 잡으면 릴리스 직전의 불필요한 공황을 꽤 줄여주더라고요. 저도 홈랩과 업무 환경에서 비슷한 패턴으로 굴려보면서, Trivy를 도입한 팀과 그렇지 않은 팀의 차이는 기능 수보다 운영 규칙의 유무에서 갈린다고 느꼈습니다.

    이번 글은 단순 사용기가 아니라, 1년 동안 실제로 남은 습관과 버린 습관을 같이 적는 회고입니다. 특히 CI에서 보안 스캔이 형식화되기 쉬운 팀, 결과는 쌓이는데 누구도 우선순위를 정하지 못하는 팀이라면 바로 적용 가능한 형태로 정리해봤습니다.

    Trivy 도입 회고 기반 DevSecOps 워크플로우 개요 이미지

    Trivy 도입 회고와 DevSecOps 워크플로우 변화를 한눈에 보여주는 개요 이미지입니다.

    1. 왜 Trivy를 넣었는지: 제가 원한 건 “더 많은 경고”가 아니라 “더 빠른 판별”이었습니다

    제가 처음 Trivy를 넣은 이유는 단순했습니다. 컨테이너 이미지를 배포하는 속도는 빨라졌는데, 취약점 확인은 여전히 사람 손을 많이 탔기 때문입니다. 패키지 목록을 보고 CVE를 대조하는 식의 확인은 운영 주기가 짧아질수록 바로 무너지더라고요. 그때 필요한 건 화려한 플랫폼이 아니라, 이미지와 설정을 같은 문법으로 반복 점검할 수 있는 스캐너였습니다.

    Trivy가 실무에서 먹히는 이유는 범위가 넓어서가 아니라 도입 순서를 잘게 쪼갤 수 있어서입니다. 이미지 스캔만 먼저 넣고, 이후에 IaC와 secret 탐지로 넓혀도 워크플로우가 크게 흔들리지 않습니다. 반면 SAST나 DAST는 언어, 프레임워크, 런타임, 테스트 환경에 더 강하게 묶이는 경우가 많아서 첫 진입 비용이 큽니다.

    다만 여기서 선을 분명히 그어야 합니다. 애플리케이션 로직 취약점, 예를 들면 인증 우회나 권한 검증 실수 같은 건 Trivy가 대신 찾아주지 않습니다. 제 경험상 Trivy는 아래 상황에서 특히 강하고, 아래 상황에서는 과도한 기대를 버리는 편이 맞았습니다.

    상황 Trivy를 우선 쓸 때 다른 도구를 같이 봐야 할 때 제 판단
    컨테이너 배포 파이프라인 베이스 이미지, OS 패키지, 언어 패키지 점검 런타임 행위 분석이 필요할 때 Trivy를 기본 게이트로 둡니다
    Kubernetes / Terraform 운영 매니페스트, Terraform, Dockerfile 오설정 점검 조직 정책 강제와 예외 승인 체계가 별도일 때 Trivy config 또는 fs 스캔을 먼저 붙입니다
    애플리케이션 코드 보안 의존성 수준의 위험 신호 확인 인증, 권한, 비즈니스 로직 취약점 검토 SAST, 코드리뷰, 위협 모델링이 필요합니다
    비밀값 관리 저장소 내 하드코딩 secret 탐지 이미 유출된 키 회전, 감사 추적 탐지 후 운영 절차가 더 중요합니다

    한마디로 말하면, Trivy는 “전체 보안 솔루션”이 아니라 배포 경로에 가까운 위험을 빠르게 분류하는 도구로 볼 때 가장 실용적이었습니다.

    2. Trivy를 1년 써보니 바뀐 DevSecOps 워크플로우

    도입 전에는 릴리스 직전에 한 번 몰아서 확인하는 습관이 있었습니다. 이 방식의 문제는 취약점 자체보다 발견 시점이 늦다는 데 있습니다. 이미지 하나에서 HIGH나 CRITICAL이 나와도, 그게 애플리케이션 의존성인지 베이스 이미지 레이어인지 구분하는 데 시간이 들고, 그다음에는 재빌드와 회귀 테스트가 줄줄이 따라옵니다. 보안 이슈가 일정 이슈로 번지는 전형적인 패턴이죠.

    Trivy를 붙인 뒤에는 스캔 위치를 늘린 게 아니라, 질문이 다른 세 지점으로 나눴습니다.

    1. 개발 중: 지금 만든 변경이 새 위험을 들여왔는가
    2. CI 빌드 단계: 이 커밋은 배포 금지선에 걸리는가
    3. 배포 전 또는 정기 점검: 허용한 예외가 아직도 타당한가

    이 세 지점을 구분하고 나니 팀 대화가 훨씬 현실적으로 바뀌었습니다. 예전에는 “보안팀이 막았다”는 식으로 받아들였는데, 지금은 “이건 신규 유입이라 지금 고쳐야 한다”, “이건 기존 부채라 추적하되 이번 배포는 진행한다”처럼 이야기할 수 있게 됐습니다.

    특히 제가 강하게 느낀 건, 모든 HIGH/CRITICAL을 일괄 차단하는 정책은 생각보다 오래 못 간다는 점입니다. 신규 서비스라면 가능하지만, 오래된 베이스 이미지와 레거시 패키지가 섞인 환경에서는 첫날부터 파이프라인이 전부 빨갛게 변합니다. 그러면 팀이 배우는 게 아니라 우회 방법을 먼저 찾습니다. 그래서 저는 보통 이렇게 권했습니다.

    • 신규 서비스: HIGH, CRITICAL을 바로 게이트로 겁니다
    • 기존 서비스: 우선 JSON 리포트를 남기고 신규 유입분만 차단합니다
    • IaC 점검: 즉시 차단보다 반복되는 오설정 패턴을 템플릿에서 제거합니다

    이 접근이 좋은 이유는 단순합니다. 보안 게이트는 강도보다 지속 가능성이 먼저이기 때문입니다.

    3. 실전 구현: 1년 뒤에도 남은 기본 스캔 패턴

    처음엔 옵션을 과하게 붙였는데, 시간이 지나고 보니 오래 살아남는 건 단순한 패턴이었습니다. 다만 단순하다고 해서 대충 쓰면 안 됩니다. Trivy는 같은 명령처럼 보여도 어디서 어떤 플래그를 붙이느냐에 따라 해석이 꽤 달라집니다.

    3-1. 로컬 이미지 점검: “지금 만든 이미지가 배포 금지선에 걸리는지”만 빠르게 확인

    # 사람이 읽기 좋은 표 형태로 확인
    trivy image --severity HIGH,CRITICAL --ignore-unfixed --format table my-app:local
    
    # 보안 게이트 용도: 보안 이슈가 있으면 종료 코드 1
    trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 my-app:local
    
    # 결과를 남겨서 이전 스캔과 비교
    trivy image --severity HIGH,CRITICAL --format json --output trivy-image-report.json my-app:local

    여기서 제가 실제로 중요하게 본 건 세 가지입니다. --severity는 우선순위 절삭용이고, --exit-code 1은 자동화 제어용이며, --format json --output은 운영 대화용입니다. 스캔 결과를 눈으로만 보고 끝내면 회고가 안 남습니다. 반대로 JSON을 남기면 “원래 있던 위험인지”, “이번 커밋에서 새로 생긴 위험인지”를 구분하기가 쉬워집니다.

    또 하나, --ignore-unfixed는 편의 옵션처럼 보이지만 팀 피로도를 크게 좌우합니다. 패치가 아직 없는 항목까지 동일한 톤으로 경고하면 개발자는 금방 무감각해지기 마련입니다. 제가 운영에서 얻은 결론은 이렇습니다. 수정 가능한 위험과 당장 수정 불가능한 위험을 같은 줄에 세우지 마세요. 이 둘은 우선순위가 다릅니다.

    3-2. 저장소와 IaC 점검: 여기서 많이 놓치는 건 “기본값의 함정”입니다

    Trivy를 처음 붙일 때 많은 팀이 놓치는 포인트가 하나 있습니다. image, fs, repo 계열 스캔에서 misconfiguration 탐지는 기본 활성화가 아닐 수 있다는 점입니다. 즉, 취약점 스캔이 성공했다고 해서 Kubernetes 매니페스트나 Terraform 설정까지 본 게 아닐 수도 있더라고요. 저는 이 부분에서 한 번 크게 헷갈렸고, 이후에는 의도적으로 스캐너 종류를 명시했습니다.

    # 파일시스템 기준으로 취약점, 오설정, 비밀값을 같이 확인
    trivy fs --scanners vuln,misconfig,secret --severity HIGH,CRITICAL .
    
    # 설정 자체를 집중적으로 볼 때
    trivy config .
    
    # JSON 결과를 남겨 후처리
    trivy fs --scanners misconfig,secret --format json --output trivy-fs-report.json .

    제 경험상 운영 사고는 애플리케이션 코드보다 YAML과 Terraform에서 먼저 터지는 경우가 많았습니다. 예를 들어 보안 컨텍스트 누락, 과도한 capability 허용, 퍼블릭 노출 보안 그룹, 암호화 누락 같은 항목은 코드리뷰에서 지나가도 설정 스캔에서는 반복적으로 잡힙니다. 그래서 Kubernetes나 Terraform 비중이 큰 팀이라면 이미지 스캔보다 trivy config .가 체감 효용이 더 클 수도 있습니다.

    Trivy 도입 회고를 위한 CI 이미지 스캔과 설정 스캔 구성도

    빌드, 스캔, 배포 승인 단계로 이어지는 Trivy 기반 CI 흐름을 설명하는 이미지입니다.

    3-3. CI 파이프라인 예시: 속도와 신뢰도를 같이 잡으려면 캐시 전략이 먼저입니다

    초기에는 매 파이프라인마다 DB를 처음부터 내려받아서 체감 속도가 꽤 나빴습니다. 나중에 알게 된 건, 느린 원인이 Trivy 자체의 스캔 엔진보다도 DB cold start와 캐시 운영 방식에 있는 경우가 많다는 점입니다. 저는 이후에 “업데이트 단계”와 “실제 스캔 단계”를 분리하는 식으로 바꿨습니다.

    stages:
      - build
      - security
    
    variables:
      TRIVY_CACHE_DIR: .trivycache
    
    trivy_db_warmup:
      stage: security
      image: aquasec/trivy:latest
      script:
        - trivy image --download-db-only --cache-dir "$TRIVY_CACHE_DIR"
      cache:
        key: trivy-cache
        paths:
          - .trivycache
    
    trivy_scan:
      stage: security
      image: aquasec/trivy:latest
      script:
        - trivy image --cache-dir "$TRIVY_CACHE_DIR" --skip-db-update --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 --format json --output trivy-report.json "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
        - trivy config --severity HIGH,CRITICAL .
      cache:
        key: trivy-cache
        paths:
          - .trivycache
      artifacts:
        when: always
        paths:
          - trivy-report.json

    여기서 핵심은 세 가지입니다. 첫째, --download-db-only로 DB 준비를 분리합니다. 둘째, 실제 스캔에서는 --skip-db-update로 매번 네트워크 병목을 만들지 않습니다. 셋째, 캐시 디렉터리를 명시해 러너별 편차를 줄입니다. 이 패턴은 파이프라인 속도를 위해서도 중요하지만, 더 본질적으로는 팀이 우회하지 않게 만드는 장치입니다.

    주의할 점도 있습니다. 로컬 파일 시스템 캐시는 편하지만, 같은 캐시를 여러 프로세스가 동시에 잡는 환경에서는 잠금 문제가 생길 수 있습니다. 현업에서는 이걸 Trivy 오류로 오해하기 쉬운데, 실제 원인은 병렬 러너와 공유 캐시 구조인 경우가 많습니다. 그래서 병렬 잡이 많은 CI라면 캐시 공유 방식을 먼저 점검하는 편이 좋았습니다.

    3-4. 예외는 파일이 아니라 계약입니다

    예외 처리도 초반에 대충 시작하면 나중에 제일 큰 부채가 됩니다. 제가 권하는 방식은 단순합니다. 예외를 남길 때 ID, 만료일, 이유를 같이 기록하세요. 특히 YAML 기반 ignore 파일은 이 문맥을 남기기에 좋았습니다.

    vulnerabilities:
      - id: CVE-2023-29491
        expired_at: 2026-12-31
        statement: Upstream fix is not available yet; tracked in internal patch queue
    
    misconfigurations:
      - id: AVD-DS-0002
        paths:
          - "docs/Dockerfile"
        statement: Documentation image intentionally runs as root for example output

    제가 이 방식을 선호한 이유는, 예외를 기술 부채로 보이게 만들기 때문입니다. 그냥 .trivyignore에 ID만 추가해두면 몇 달 뒤에는 아무도 왜 허용했는지 기억하지 못합니다. 반면 만료일과 설명이 남아 있으면, 분기 점검 때 예외를 다시 심사할 수 있습니다.

    4. 어떤 기준으로 운영했는지: 무조건 차단보다 “신규 유입 차단”이 먼저였습니다

    1년 돌리고 나서 가장 확신하게 된 건, 보안 스캔의 성패는 탐지 정확도보다 운영 규칙의 해상도에 달려 있다는 점입니다. 리포트만 잘 뽑아서는 아무 일도 안 일어납니다. 누가 무엇을 보고 언제 막고 언제 예외로 넘길지 정해져야 비로소 도구가 시스템이 됩니다.

    운영 단계 스캔 대상 차단 기준 추천 방식 쓰지 말아야 할 방식
    초기 도입 컨테이너 이미지 경고만, 차단 없음 리포트 해석 루틴부터 정착 첫날부터 전체 서비스 일괄 차단
    안정화 단계 이미지 + IaC CRITICAL 또는 신규 HIGH 신규 서비스 우선 적용 기존 부채와 신규 유입을 같은 기준으로 처리
    정착 이후 이미지 + IaC + secret 정책 위반, 신규 HIGH/CRITICAL, 만료된 예외 예외 만료 관리와 추세 분석 병행 예외를 영구 허용처럼 방치

    제가 현장에서 자주 내린 판단은 아래와 같습니다.

    • exit code 1이 발생했다: 무조건 “보안팀 이슈”가 아니라, 우선 신규 유입인지 기존 누적인지부터 구분합니다
    • HIGH/CRITICAL이 많다: 애플리케이션 패키지보다 베이스 이미지 교체 가능성을 먼저 봅니다
    • misconfig 경고가 반복된다: 개별 서비스 수정 대신 템플릿과 헬름 차트 기본값을 손봅니다
    • secret 탐지가 나왔다: 파일 삭제로 끝내지 말고 키 회전, 배포 환경, Git 히스토리 노출까지 확인합니다

    이 기준이 실무적으로 유용했던 이유는 숫자에 덜 매달리게 해주기 때문입니다. 결과 개수는 서비스 성격에 따라 달라집니다. 반면 신규 유입, 반복 패턴, 만료된 예외는 팀의 운영 건강도를 훨씬 잘 보여줍니다.

    5. 실제로 겪은 문제들: 속도, 노이즈, 예외 관리, 그리고 가장 흔한 오해

    여기부터가 회고의 핵심입니다. 도입 자체보다 오래 가는 실패 모드를 알아야 팀이 덜 지칩니다.

    5-1. DB 업데이트 때문에 느려지는 문제

    표면적으로는 “Trivy가 느리다”로 보이지만, 근본 원인은 대개 매 잡마다 DB를 새로 받는 구조입니다. 특히 ephemeral runner를 쓰면 이 비용이 더 자주 드러납니다. 제 기준에서는 매 커밋 전체 스캔보다, DB 준비를 분리하고 실제 게이트는 핵심 브랜치나 배포 직전 단계에 두는 쪽이 훨씬 오래 갔습니다.

    5-2. 오설정을 본 줄 알았는데 사실 안 본 문제

    이건 실무에서 꽤 위험합니다. 취약점 스캔 결과가 잘 나오니까 팀은 “IaC도 같이 봤겠지”라고 생각합니다. 그런데 fs나 image 계열에서 misconfig 스캐너를 명시하지 않으면 기대한 범위를 다 보지 못할 수도 있더라고요. 근본 원인은 도구 성능이 아니라 스캔 범위에 대한 잘못된 가정입니다.

    5-3. 수정 불가능한 항목이 너무 많이 보이는 문제

    --ignore-unfixed 없이 모든 항목을 그대로 내보내면 개발자 입장에서는 행동 기준이 흐려집니다. “지금 고칠 수 없는 항목”과 “지금 당장 베이스 이미지 업데이트로 줄일 수 있는 항목”을 분리하지 않으면 리포트는 금방 소음이 됩니다. 저는 그 뒤로 “행동 가능한 결과 우선” 원칙을 고정했습니다.

    5-4. 예외 처리 파일이 영구 창고가 되는 문제

    예외는 필요합니다. 문제는 예외 자체가 아니라 재검토 주기가 없는 예외입니다. 파일 하나에 ID가 계속 쌓이기 시작하면, 그 순간부터 스캔 결과의 신뢰도는 빠르게 떨어집니다. 제 경험상 예외 항목은 추가보다 제거가 더 어렵기 때문에, 처음부터 만료일과 이유를 강제하는 편이 낫습니다.

    # 현재 결과 저장
    trivy image --severity HIGH,CRITICAL --format json --output current-scan.json my-app:latest
    
    # 새로 유입된 취약점 식별에 필요한 최소 필드만 추출
    jq '.Results[]?.Vulnerabilities[]? | {VulnerabilityID, PkgName, InstalledVersion, FixedVersion, Severity}' current-scan.json

    이 출력에서 제가 보는 건 단순 개수가 아닙니다. PkgName과 InstalledVersion, FixedVersion 조합을 보면 “패키지 업그레이드로 해결 가능한지”, “베이스 이미지 교체가 먼저인지” 판단이 빨라집니다. 리포트는 많아도, 실제로 손대야 하는 지점은 보통 몇 군데로 수렴합니다.

    Trivy 도입 회고 결과 검증과 예외 관리 대시보드 이미지

    스캔 결과, 심각도 분류, 예외 검토 흐름을 보여주는 결과 화면 이미지입니다.

    6. 재현 가능한 시나리오 하나: 코드보다 베이스 이미지가 먼저인 경우

    실제 현장에서 자주 본 장면을 하나 말씀드리겠습니다. 애플리케이션 코드는 크게 바뀌지 않았는데, 어느 날 이미지 스캔에서 OS 패키지 쪽 HIGH/CRITICAL이 갑자기 눈에 띄게 늘어납니다. 이때 개발팀이 바로 애플리케이션 의존성을 뒤지기 시작하면 시간이 꽤 낭비됩니다. 제가 먼저 확인하는 순서는 거의 항상 같습니다.

    1. 취약점이 애플리케이션 패키지인지 OS 패키지인지 구분합니다
    2. 현재 베이스 이미지 태그가 오래됐는지 확인합니다
    3. 가능하면 공식 베이스 이미지를 최신 태그 계열로 교체해 재빌드합니다
    4. 재스캔 후에도 남는 항목만 애플리케이션 레벨에서 봅니다

    예를 들어 JSON 결과에서 동일한 OS 계열 패키지가 여러 항목에 반복 등장하면, 그때는 코드보다 베이스 레이어를 먼저 의심하는 편이 맞았습니다. 반대로 언어 패키지 매니저 쪽 결과가 중심이면 애플리케이션 의존성 정리가 먼저일 때가 많았습니다. 이 판단을 빨리 내리면 보안 대응이 코드 수리전이 아니라 레이어 선택 문제로 바뀝니다.

    다만 베이스 이미지 교체가 만능은 아닙니다. 베이스를 올리면 libc나 openssl 같은 런타임 동작 차이로 회귀가 날 수 있습니다. 그래서 저는 Trivy 결과를 보고 베이스 이미지를 바꿀 때는, 보안 수정과 기능 검증을 분리하지 않고 같은 변경 세트로 다루는 편이 낫다고 봅니다. 취약점 수치만 줄이고 서비스가 깨지면 운영 관점에서는 실패니까요.

    7. 1년 운영 결과: 좋아진 점과 아직 아쉬운 점

    좋아진 점은 분명했습니다. 첫째, 보안 이슈가 배포 막판에야 드러나는 빈도가 줄었습니다. 둘째, 컨테이너 보안 검토가 특정 담당자의 기억이나 감에 덜 의존하게 됐습니다. 셋째, IaC 오설정이 템플릿 수준에서 반복되는 패턴으로 보이기 시작했습니다. 이건 단순히 경고를 더 본다는 의미가 아니라, 조직이 같은 실수를 덜 반복한다는 쪽에 가깝습니다.

    반면 한계도 뚜렷했습니다.

    • Trivy만으로 애플리케이션 보안이 완성되지는 않습니다
    • 예외 관리가 느슨해지면 스캔 결과는 빠르게 형식화됩니다
    • 캐시와 업데이트 전략을 안 잡으면 CI 병목이 바로 눈에 띕니다
    • 팀 성숙도보다 앞선 강한 차단 정책은 우회를 부릅니다

    그래서 지금의 제 결론은 꽤 명확합니다. Trivy는 만능 도구가 아니라 컨테이너와 설정 검증의 기본 레일로 두는 게 가장 잘 맞습니다. 이 위치를 벗어나면 기대가 과해지고, 이 위치를 정확히 잡으면 투자 대비 효용이 좋습니다.

    Trivy 도입 회고의 도입 전후 비교와 개선 포인트 인포그래픽

    도입 전 수동 점검과 도입 후 자동화된 DevSecOps 워크플로우를 비교하는 요약 이미지입니다.

    8. 마무리: 이런 팀이라면 저는 이렇게 적용하라고 권합니다

    제 추천은 상황별로 꽤 분명합니다. 이제 막 시작하는 팀이라면 이미지 스캔부터 붙이세요. 처음부터 모든 경고를 막지 말고, HIGH/CRITICAL 결과를 읽고 분류하는 루틴부터 만드시는 편이 낫습니다. 이미 CI가 있는 팀이라면 --exit-code 1과 심각도 기준을 먼저 명시하세요. 배포 차단 기준이 불명확하면 스캔은 돌아도 운영은 바뀌지 않습니다.

    Kubernetes나 Terraform 비중이 큰 팀이라면 trivy config . 또는 trivy fs --scanners vuln,misconfig,secret .를 같이 두는 편이 맞습니다. 실무에서 더 자주 사고를 내는 건 코드보다 설정인 경우가 많기 때문입니다. 반대로 애플리케이션 인증, 권한, 비즈니스 로직 취약점까지 Trivy 하나로 해결하려고 하시면 방향이 틀어집니다. 그건 다른 검토 체계가 필요합니다.

    장기 운영 팀이라면 선택이 더 단순합니다. 신규 유입 위험은 차단하고, 기존 부채는 추적하되, 예외는 만료시켜 다시 심사하세요. 이 세 가지만 지켜도 Trivy는 충분히 값어치를 합니다. 결국 Trivy 도입 회고는 도구 평가라기보다 운영 습관 평가에 가까웠습니다. 저에게 남은 교훈도 하나였습니다. 스캐너는 많이 도는 것보다, 팀이 어떤 기준으로 멈추고 어떤 기준으로 진행할지를 분명히 할 때 비로소 도움이 됩니다.

    마지막으로 다시 적어둡니다. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. Trivy를 도입하실 때는 기능 목록보다 운영 규칙부터 설계해보세요. 그 순서가 맞아야 1년 뒤에도 파이프라인에 남습니다.

  • [보안] Kubernetes 보안 체크리스트: 컨테이너 이미지 스캔 자동화

    [보안] Kubernetes 보안 체크리스트: 컨테이너 이미지 스캔 자동화

    본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.

    Kubernetes 보안 체크리스트: 컨테이너 이미지 스캔 자동화

    Kubernetes 보안 체크리스트를 운영하다 보면 병목이 꽤 자주 이미지 스캔에서 드러납니다. 배포 파이프라인은 분 단위로 빨라졌는데 보안 판정이 여전히 사람 손에 걸려 있으면, 속도와 통제가 같이 무너지더라고요. 저도 홈랩이든 사내 테스트 클러스터든 처음에는 릴리스 직전 일회성 스캔으로 시작했는데, 운영으로 넘어가면 금방 한계가 보였습니다. 이미지 변경 속도, 베이스 이미지 교체 주기, 레지스트리 정책, 클러스터 반입 기준이 따로 놀면 자동화는 이름만 자동화가 되거든요.

    실무에서는 스캐너를 한 번 붙이는 것으로 끝나지 않습니다. 빌드 시점, 레지스트리 저장 시점, 클러스터 반입 시점, 운영 중 재평가 시점을 나눠서 봐야 합니다. 이유는 단순합니다. 배포 당시엔 통과한 이미지라도, 며칠 뒤 취약점 데이터베이스가 갱신되면 같은 다이제스트가 다른 판정을 받을 수 있습니다. 이 차이를 놓치면 팀은 바로 “어제는 괜찮았는데 왜 오늘 막히지?”라는 혼란에 빠집니다.

    CI, 레지스트리, Kubernetes 클러스터, 보고 체계를 한눈에 보여주는 전체 구성도입니다.

    Kubernetes 보안 체크리스트에서 이미지 스캔이 중요한 이유

    현장에서 필요한 건 CVE 숫자 나열이 아닙니다. 무엇을 기준으로 배포를 막을지, 무엇은 추적만 하고 무엇은 즉시 조치할지, 그 판정을 누가 재현할 수 있는지가 핵심입니다. 실제 스캔 결과를 보면 문제는 대체로 세 부류로 갈립니다. 베이스 이미지 교체로 끝나는 항목, 애플리케이션 의존성 업데이트가 필요한 항목, 이미지가 아니라 배포 매니페스트나 실행 권한 설정을 손봐야 하는 항목입니다. 같은 High라도 대응 경로가 완전히 다릅니다.

    그래서 체크리스트를 만들 때 제가 먼저 적는 건 도구 이름이 아니라 다음 네 가지입니다. 첫째, 어떤 시점에 검사할지. 둘째, 어떤 대상을 검사할지. 셋째, 어떤 심각도와 조건에서 실패로 볼지. 넷째, 예외를 누구 승인으로 얼마나 오래 둘지. 이 네 줄이 빠지면 결과는 늘 많고, 책임은 늘 흐려집니다.

    Kubernetes 보안 체크리스트로 정리한 컨테이너 이미지 스캔 자동화 체크리스트

    1. 이미지 태그보다 다이제스트를 기준으로 기록: repo:tag만 저장하지 말고 repo@sha256:...를 남겨야 스캔 결과와 배포 대상을 정확히 연결할 수 있습니다.
    2. 빌드 직후 스캔: CI에서 이미지 생성 직후 취약점과 시크릿 혼입 여부를 확인합니다.
    3. 실패 기준을 코드로 고정: CRITICAL 즉시 차단, HIGH는 팀 정책에 따라 차단 또는 예외 관리 등으로 명시합니다.
    4. 데이터베이스 갱신 시점 기록: 로컬, CI, 클러스터의 스캔 결과가 다른 흔한 원인입니다.
    5. 레지스트리 인증 분리: 빌드용 자격 증명과 스캔용 읽기 전용 자격 증명을 분리해 두면 사고 범위를 줄이기 좋습니다.
    6. 클러스터 내 재스캔: 이미 떠 있는 워크로드를 주기적으로 다시 평가해야 운영 중 신규 공개 취약점을 따라잡을 수 있습니다.
    7. 예외 처리 만료일 강제: 예외는 허가가 아니라 부채입니다. 사유, 승인자, 만료일이 없으면 결국 영구 면제가 됩니다.
    8. 결과 저장 위치 통일: CI 로그만 믿지 말고 리포트 시스템이나 Kubernetes 리소스로 남겨 반복 조회 가능하게 해야 합니다.
    9. 로컬과 CI 판정 일치: 같은 옵션, 같은 대상, 가능하면 같은 DB 갱신 흐름을 써야 팀이 결과를 신뢰합니다.
    10. 최소 이미지 우선: Distroless나 slim 계열 검토는 보안 때문이기도 하지만, 스캔 노이즈와 패치 범위를 줄이는 데도 효과가 큽니다.
    11. Admission 단계 연계 여부 결정: 규정이 엄격하면 배포 허가 단계에서 차단하고, 초기 도입기라면 관찰 모드부터 들어가는 편이 덜 부서집니다.
    12. 성능 예산 확보: 대형 이미지 재스캔은 노드 자원과 레지스트리 호출을 먹습니다. 운영 클러스터에서 무작정 스캔 빈도를 높이면 다른 워크로드가 피해를 볼 수 있습니다.

    도구 선택 전에 먼저 정할 기준

    Trivy, Grype처럼 널리 쓰이는 스캐너는 출발점으로 충분히 좋습니다. 저는 빠르게 붙일 때는 Trivy를 자주 쓰는 편입니다. 로컬 점검, CI, Kubernetes 연계까지 흐름을 만들기 편해서 이거 진짜 실무에서 손이 덜 가더라고요. 다만 도구보다 먼저 정해야 할 것이 있습니다. 결과 해석 단위가 이미지인지, 레포지토리인지, 실제 클러스터 워크로드인지부터 정해야 합니다. 이 기준이 흔들리면 스캐너를 바꿔도 운영은 그대로 혼란스럽습니다.

    선택 포인트 A를 택할 때 B를 택할 때 제가 실제로 권하는 기준
    스캔 시점 CI만 스캔: 배포 속도와 단순성이 우선일 때 CI + 클러스터 재스캔: 운영 중 노출 추적이 중요할 때 운영 클러스터가 있으면 병행이 맞습니다. CI만으로는 신규 공개 취약점을 놓치기 쉽습니다.
    식별 방식 태그 기준: 초기에 단순하게 시작할 때 다이제스트 기준: 결과 재현성과 감사 추적이 필요할 때 실서비스는 다이제스트 기준으로 가는 편이 안전합니다. 태그는 너무 쉽게 바뀝니다.
    실패 정책 Critical만 차단: 도입 초기에 반발을 줄여야 할 때 Critical + High 차단: 규정과 배포 품질 기준이 이미 성숙했을 때 처음부터 전부 막기보다 Critical 차단, High는 만료일 있는 예외 관리가 현실적입니다.
    스캔 대상 OS 패키지 위주: 베이스 이미지 관리가 주 이슈일 때 OS + 애플리케이션 의존성 + 시크릿: 공급망 전체를 봐야 할 때 실무에서는 최소한 취약점과 시크릿은 같이 보는 편이 낫습니다.
    운영 방식 관찰 모드: 현재 상태 파악이 먼저일 때 차단 모드: 배포 기준을 강제해야 할 때 새로 도입하는 팀은 1~2주 정도 관찰 모드로 데이터부터 모으는 편이 덜 아픕니다.

    실전 구현 1: Kubernetes 보안 체크리스트 기준으로 로컬과 CI 맞추기

    가장 먼저 할 일은 개발자 로컬과 CI가 가능한 한 같은 판정을 내도록 맞추는 것입니다. 로컬에선 통과했는데 CI에서만 실패하면 팀은 금방 스캐너보다 파이프라인을 더 싫어하게 됩니다. 그래서 이 단계에서는 “옵션을 많이 주는 것”보다 “같은 옵션을 고정하는 것”이 더 중요합니다. 이 기준만 맞춰도 운영 피로도가 꽤 줄어듭니다.

    1. 이미지 빌드 후 Trivy로 즉시 검사

    아래 예시는 로컬에서 이미지 빌드 직후 취약점과 시크릿을 같이 보는 가장 단순한 흐름입니다. 팀 공통 기준을 잡을 때는 이런 명령부터 통일하는 게 훨씬 편합니다.

    docker build -t myapp:scan-test .
    trivy image \
      --severity HIGH,CRITICAL \
      --ignore-unfixed \
      --scanners vuln,secret \
      --format table \
      myapp:scan-test

    여기서 중요한 건 옵션 의미보다 운영상의 함정입니다. --ignore-unfixed는 노이즈를 줄이는 데 유용하지만, “아직 수정본이 없으니 위험이 없다”는 뜻은 아닙니다. 패치가 없어도 우회책이나 완화 조치가 필요한 경우가 있습니다. 그래서 저는 이 옵션을 쓰더라도 예외 문서에는 “미조치”보다 “공급사 수정 대기”처럼 상태를 분리해 적는 편입니다.

    --scanners vuln,secret 조합도 실무 효율이 좋습니다. 이미지 레이어에 테스트 토큰, 오래된 인증서, 샘플 키가 남아 있는 경우가 의외로 자주 나옵니다. 이건 CVE보다 발견 즉시 우선순위를 높게 잡아야 하는 유형입니다. 공격 난도가 낮고 노출 즉시 영향이 커질 수 있어서 그렇습니다.

    2. CI에서 실패 기준을 강제하고 결과를 아티팩트로 남기기

    CI에서는 사람 눈으로만 보는 표 형식과, 후처리용 JSON 결과를 같이 남겨 두는 편이 좋습니다. 나중에 추세 비교나 예외 검토할 때 이 차이가 꽤 크게 느껴집니다.

    IMAGE_REF="registry.example.com/team/myapp:${GIT_COMMIT}"
    
    trivy image \
      --severity CRITICAL,HIGH \
      --exit-code 1 \
      --ignore-unfixed \
      --scanners vuln,secret \
      --format json \
      --output trivy-report.json \
      "$IMAGE_REF"
    
    trivy image \
      --severity CRITICAL,HIGH \
      --ignore-unfixed \
      --scanners vuln,secret \
      --format table \
      "$IMAGE_REF"

    --exit-code 1이 없으면 자동화는 반쪽입니다. 로그에 경고가 많아도 배포가 계속되면 운영팀은 곧 “보안 경고는 참고용”으로 받아들이게 됩니다. 그 순간부터 정책은 문서에만 남습니다. 그리고 JSON 결과를 별도 파일로 남기는 이유도 분명합니다. 사람이 읽기 쉬운 표와 기계가 다시 처리하기 쉬운 형식을 분리해야 나중에 재검증이 편해집니다.

    초기에 자주 보는 실패 모드는 “로컬은 어제 DB로 검사했고, CI는 오늘 DB로 검사했다”는 불일치입니다. 겉으로는 같은 이미지, 같은 명령처럼 보여도 결과가 달라집니다. 그래서 운영 문서에는 최소한 이미지 다이제스트, 스캔 시각, 스캐너 버전, DB 갱신 시각 네 가지를 같이 남겨 두는 게 좋습니다.

    컨테이너 이미지 스캔이 포함된 Kubernetes 보안 체크리스트 CI 흐름

    빌드, 스캔, 실패 기준 적용, 배포 차단으로 이어지는 파이프라인 흐름 예시입니다.

    실전 구현 2: Kubernetes 클러스터 안에서 재스캔 자동화

    CI 스캔은 배포 직전의 사진 한 장에 가깝습니다. 운영은 동영상에 더 가깝고요. 이미지 자체는 그대로인데 취약점 데이터베이스가 갱신되거나, 오래된 워크로드가 남아 있거나, 수동 롤백으로 이전 태그가 다시 떠 있을 수 있습니다. 그래서 운영 클러스터에서는 주기 재평가가 꼭 필요합니다.

    1. Helm으로 Trivy Operator 설치

    Trivy Operator는 Kubernetes 안에서 보안 리포트를 CRD 형태로 남겨 주기 때문에, 클러스터 관점 추적이 필요한 팀에는 꽤 잘 맞습니다. 별도 콘솔 없이도 kubectl로 상태를 확인할 수 있다는 점도 편합니다.

    helm repo add aqua https://aquasecurity.github.io/helm-charts/
    helm repo update
    helm upgrade --install trivy-operator aqua/trivy-operator \
      --namespace trivy-system \
      --create-namespace

    이 방식의 장점은 결과가 Kubernetes 리소스로 남는다는 점입니다. CI 로그는 흩어지기 쉽지만 리소스는 조회 경로가 고정됩니다. 운영팀과 플랫폼팀이 같은 화면을 본다는 점도 생각보다 큽니다. 인수인계할 때 특히 편하더라고요.

    2. 스캔 결과 조회

    설치 후에는 먼저 실제로 어떤 리포트가 생성되는지 확인하는 게 좋습니다. 환경과 활성화된 기능에 따라 보이는 리포트 종류가 조금씩 다를 수 있습니다.

    kubectl get vulnerabilityreports -A
    kubectl describe vulnerabilityreport -n default <report-name>

    결과를 읽을 때는 숫자보다 맥락을 먼저 봅니다. 저는 보통 세 가지를 바로 확인합니다. 첫째, 어떤 워크로드의 어떤 컨테이너인지. 둘째, 문제가 베이스 이미지 계층인지 애플리케이션 의존성인지. 셋째, 지금 당장 수정 가능한지입니다. 같은 High라도 베이스 이미지 교체로 끝나는 항목과 코드 호환성 검토가 필요한 라이브러리 문제는 대응 일정이 다릅니다.

    3. 실제 배포 중인 이미지 목록부터 확인

    보고서가 아니라 실제 실행 대상을 먼저 보는 습관이 중요합니다. 선언상 최신 이미지가 적혀 있어도, 운영 클러스터에는 예전 이미지가 남아 있는 경우가 꽤 있습니다.

    kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{range .spec.containers[*]}{.image}{" "}{end}{"\n"}{end}'

    이 명령은 단순해 보여도 중요합니다. Git 저장소 기준으로는 최신 태그를 본다고 해도, 실제 클러스터에선 예전 태그나 수동 롤백된 이미지가 남아 있을 수 있습니다. 스캔 체계가 배포 선언만 보고 실제 실행 대상을 안 보면, 보고서는 깨끗한데 운영은 더러운 상태가 됩니다.

    4. 사설 레지스트리 인증 문제가 있는지 먼저 분리 진단

    리포트가 안 생길 때는 스캐너 자체를 의심하기 전에 인증 흐름부터 확인하는 편이 빠릅니다. 여기서 시간 많이 쓰는 팀이 정말 많습니다.

    kubectl get events -A --sort-by=.lastTimestamp
    kubectl get secrets -n trivy-system
    kubectl describe serviceaccount -n trivy-system trivy-operator

    클러스터 내 스캐너가 리포트를 못 만드는 가장 흔한 이유는 인증 연결 누락입니다. 이미지 풀은 애플리케이션 서비스 계정이 잘하는데, 스캐너는 다른 서비스 계정이라 접근을 못 하는 경우가 잦습니다. 표면 증상은 “리포트가 안 생긴다”인데 근본 원인은 권한 경계가 다른 데 있습니다. 이건 설정 몇 줄 더 넣는다고 해결되지 않고, 누가 어느 레지스트리를 어떤 자격 증명으로 읽는지 모델을 분리해서 봐야 합니다.

    재현 가능한 시나리오로 보는 운영 포인트

    이런 시나리오는 실제 운영에서 자주 나옵니다. 내부 API 서비스가 registry.example.com/team/api:2026-09-01로 배포됐고, 배포 당시 CI 스캔은 통과했습니다. 며칠 뒤 베이스 이미지에 대한 신규 취약점 정보가 스캐너 DB에 반영됩니다. 코드도, 이미지 다이제스트도 바뀌지 않았는데 클러스터 리포트에는 새로운 Critical이 생길 수 있습니다. CI만 보는 팀은 “왜 갑자기 숫자가 늘었지?”에서 멈추고, 클러스터 재스캔이 있는 팀은 “지금 떠 있는 워크로드 중 어느 네임스페이스가 영향권인지”까지 바로 확인합니다.

    이 지점에서 중요한 것은 억울함을 줄이는 기록입니다. 결과가 바뀌었는데 이미지가 안 바뀌었을 때 스캐너 오작동으로 몰아가는 경우를 여러 번 봤습니다. 실제로는 취약점 DB 갱신이나 메타데이터 해석 차이인 경우가 많았습니다. 그래서 운영 기준에는 반드시 스캔 시각, DB 갱신 시각, 대상 다이제스트를 함께 남겨 두는 게 좋습니다.

    실제로 많이 막히는 문제와 해결법

    Private Registry 인증이 안 붙는 경우

    증상은 단순합니다. 리포트가 생성되지 않거나 이벤트에 인증 실패가 남습니다. 그런데 근본 원인은 보통 “앱 배포에 쓰는 이미지 풀 자격 증명”과 “스캐너가 읽는 자격 증명”을 같은 것으로 착각한 데 있습니다. 해결은 권한을 더 크게 주는 것이 아니라, 스캐너 전용 읽기 권한이 어디에 연결돼 있는지 확인하는 것입니다.

    취약점이 너무 많이 떠서 운영이 멈추는 경우

    초기 도입기에는 거의 반드시 겪습니다. 이때 모든 심각도를 한 번에 차단하면 실제로는 보안 수준이 올라가는 게 아니라 파이프라인이 멈춥니다. 저는 보통 Critical 즉시 차단, High는 예외 문서와 만료일 부여, Medium 이하는 추세 관찰로 시작합니다. 운영이 감당할 수 있는 속도로 올라가야 자동화도 습관으로 남습니다.

    태그만 보고 이미지를 추적하는 경우

    latest나 재사용되는 릴리스 태그는 감사 추적에 불리합니다. 스캔 결과가 어느 바이너리 집합을 가리키는지 불명확해집니다. 실제 배포 검증이나 사고 대응까지 생각하면 다이제스트 기준 저장이 맞습니다. 태그는 사람이 보기 좋고, 다이제스트는 시스템이 책임지기 좋습니다.

    운영 판단 없이 숫자만 보는 경우

    스캔 수치는 우선순위의 단서일 뿐, 우선순위 그 자체는 아닙니다. 외부에 노출된 서비스의 런타임 라이브러리 문제와 내부 배치 컨테이너의 개발 도구 패키지 문제는 같은 High여도 대응 순서가 달라집니다. 저는 아래 네 항목을 같이 봅니다.

    • 노출 면적: Ingress 뒤 서비스인지, 내부 잡인지 구분합니다.
    • 권한 수준: root 실행 여부, 추가 capability, privileged 여부를 확인합니다.
    • 수정 가능성: 베이스 이미지 교체로 끝나는지, 코드 수정과 검증이 필요한지 나눕니다.
    • 예외 만료: 지금 못 고치는 항목은 다시 볼 날짜가 없으면 영구 미해결이 됩니다.

    스캔이 클러스터 자원을 갉아먹는 경우

    운영 규모가 커지면 이것도 무시하기 어렵습니다. 큰 이미지가 많고 네임스페이스가 많으면 스캔 자체가 CPU, 메모리, 레지스트리 호출량을 소모합니다. 증상은 오퍼레이터가 느려지거나 리포트 생성이 밀리는 식으로 나타납니다. 재스캔은 많이 돌린다고 무조건 좋은 게 아니라, 업데이트 빈도, 클러스터 자원, 조치 가능한 인력에 맞춰야 합니다.

    클러스터 보안 관점의 Kubernetes 보안 체크리스트 리포트 구성도

    오퍼레이터가 워크로드를 스캔하고 리포트를 남기는 클러스터 내부 흐름 예시입니다.

    검증: 자동화가 제대로 작동했는지 확인하는 방법

    자동화 검증은 명령 성공 여부만 보면 부족합니다. 실패해야 할 때 진짜 실패하는지, 운영 중 판정 변화가 반영되는지, 팀원이 바뀌어도 같은 경로로 확인 가능한지가 핵심입니다. 제가 점검할 때는 다음 순서로 봅니다.

    1. 테스트용 이미지를 하나 빌드하고 로컬 결과를 저장합니다.
    2. 같은 이미지를 CI에서 같은 심각도 기준으로 스캔해 동일하게 실패 또는 통과하는지 확인합니다.
    3. 가능하면 배포 시점의 이미지 다이제스트를 기록해 로컬 결과와 CI 결과를 같은 대상으로 묶습니다.
    4. 클러스터에 배포한 뒤 vulnerabilityreports 리소스가 생성되는지 확인합니다.
    5. 사설 레지스트리 이미지는 인증 실패 없이 조회되는지 이벤트와 서비스 계정 기준으로 검증합니다.
    6. 예외 처리한 항목이 문서, CI 정책, 클러스터 결과에서 같은 의미로 반영되는지 비교합니다.

    환경에 따라 추가 리포트가 생성될 수 있으니, 아래처럼 어떤 CRD가 실제로 보이는지 함께 확인하면 좋습니다. 다만 활성화된 기능과 Trivy Operator 설정에 따라 결과는 달라질 수 있습니다.

    kubectl get vulnerabilityreports -A
    kubectl get configauditreports -A
    kubectl get clustercompliancereports -A

    중요한 것은 결과를 반복 조회 가능한 형태로 남기는 것입니다. 사람이 한 번 보고 지나가는 로그는 운영 자산이 아닙니다. 조회 경로가 고정되고 같은 명령으로 다시 확인할 수 있어야 팀 지식이 됩니다. 그리고 Pod Security Standards, RBAC, NetworkPolicy 관련 글도 함께 보면 Kubernetes 보안 체크리스트를 더 입체적으로 잡는 데 도움이 됩니다.

    Kubernetes 보안 체크리스트 결과 검증용 이미지 스캔 대시보드

    배포된 워크로드별 취약점 현황과 확인 포인트를 보여주는 결과 화면 예시입니다.

    FAQ: 현장에서 자주 받는 질문

    Q. 스캔 결과가 바뀌었는데 이미지는 안 바뀌었습니다.

    A. 충분히 가능한 일입니다. 가장 흔한 원인은 취약점 데이터베이스 갱신입니다. 그래서 스캔 시각, DB 갱신 시각, 이미지 다이제스트를 같이 기록해야 나중에 원인 추적이 쉬워집니다.

    Q. 모든 High를 바로 차단해야 하나요?

    A. 팀 상태에 따라 다릅니다. 초기 도입기라면 Critical 우선 차단, High는 예외와 만료일 관리가 현실적입니다. 이미 배포 품질 게이트가 정착된 조직이라면 High까지 차단해도 됩니다. 중요한 건 기준을 문서가 아니라 파이프라인에 넣는 것입니다.

    Q. Kubernetes 보안 체크리스트에서 이미지 스캔만 하면 충분한가요?

    A. 부족합니다. 이미지 스캔은 공급망과 배포 전 검증의 한 축일 뿐입니다. Pod Security Standards, RBAC, NetworkPolicy, Secret 관리, 감사 로그까지 함께 봐야 운영 리스크가 줄어듭니다. 다만 시작점으로는 이미지 스캔이 체감 효과가 빠른 편입니다.

    운영 기준은 이렇게 잡으시면 됩니다

    제가 오래 운영을 보면서 내린 기준은 단순합니다. 팀이 아직 수동 검토에 기대고 있다면 Trivy CLI로 CI 차단부터 붙이세요. 이 단계에서는 로컬과 CI 판정 일치, 다이제스트 기록, 예외 만료일 관리가 핵심입니다. 운영 클러스터가 여러 개이거나 롤백이 잦다면 클러스터 재스캔도 같이 가져가세요. 배포 당시엔 멀쩡했던 이미지가 나중에 문제로 바뀌는 순간을 잡아내려면 이 경로가 필요합니다.

    반대로 아직 팀이 결과 해석도 못 따라가는데 Admission 단계에서 전부 막는 것은 추천하지 않습니다. 그건 통제가 아니라 마찰이 됩니다. 이럴 땐 관찰 모드로 현재 상태 파악, 그다음 Critical 차단, 마지막으로 High까지 확대 순서가 덜 부서집니다. 규정이 엄격하고 변경 이력 감사가 중요하다면, 그때 배포 허가 단계 연계까지 가면 됩니다.

    제 권고를 한 줄로 줄이면 이렇습니다. 작게 시작하는 팀은 CI 스캔 + 실패 코드 적용, 운영 가시성이 필요한 팀은 클러스터 재스캔 추가, 강제 통제가 필요한 조직은 Admission 정책 연계. 이 순서가 실무에서 가장 덜 아프고, 결과가 남고, 다음 담당자도 이해하기 쉽습니다.

    본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.

    CI 스캔, 클러스터 재스캔, 예외 관리, 정책 연계를 한 장으로 정리한 요약 이미지입니다.

  • [Linux] AppArmor 프로파일 작성 및 디버깅: 리눅스 보안 실전 트러블슈팅

    [Linux] AppArmor 프로파일 작성 및 디버깅: 리눅스 보안 실전 트러블슈팅

    AppArmor 프로파일 작성 및 디버깅: 리눅스 보안 실전 트러블슈팅

    AppArmor 프로파일 작성은 문법 싸움보다 관찰과 축소의 싸움에 더 가깝습니다. 서비스가 멀쩡히 돌다가 배포 직후 파일 접근이 막히고, 로그에는 애매한 <code>DENIED만 남는 경우가 생각보다 자주 나오거든요. 저도 처음엔 규칙 몇 줄만 더하면 끝날 줄 알았는데, 실제 운영에선 /tmp, /run, 동적 라이브러리, 인증서 경로, systemd 런타임 디렉터리처럼 눈에 잘 안 보이는 지점에서 계속 걸리더라고요. 그래서 AppArmor 프로파일 작성은 “무엇을 허용할까”보다 먼저 “프로세스가 실제로 어디를 밟는가”를 확인하는 절차가 더 중요합니다.

    이번 글은 단순 사용법이 아니라, 실무에서 제가 기준으로 삼는 판단 순서를 중심으로 정리했습니다. 어떤 로그를 먼저 믿어야 하는지, 규칙을 어느 단위로 끊어야 나중에 안 무너지는지, AppArmor로 막는 게 맞는 문제와 구조를 먼저 손봐야 하는 문제를 어떻게 구분하는지까지 한 번에 다룹니다. 광고성 정보만 잔뜩 모은 글이 아니라, 읽고 바로 써먹을 수 있게 명령어와 프로파일 예시도 복붙 가능한 수준으로 넣었습니다.

    AppArmor 프로파일 작성 개요와 리눅스 AppArmor 적용 흐름 이미지

    AppArmor가 프로세스와 파일 경로를 기준으로 접근을 제어하는 흐름을 보여주는 개요 이미지입니다.

    AppArmor 프로파일 작성 전에 먼저 이해해야 할 것

    AppArmor는 경로 기반 MAC(Mandatory Access Control)입니다. 같은 리눅스 보안 계층이라도 SELinux처럼 라벨을 설계하는 방식과는 결이 조금 다릅니다. AppArmor 쪽이 초반 진입은 쉬운 편이지만, 반대로 경로가 지저분한 애플리케이션에서는 정책도 금방 지저분해집니다. 그래서 저는 AppArmor를 잘 쓰는 조건을 이렇게 봅니다.

    • 데이터 경로가 분리돼 있다. 예: /var/lib/myapp, /etc/myapp, /run/myapp
    • 외부 프로세스 실행이 적다.
    • 임시 파일, 소켓, PID 파일 위치를 예측할 수 있다.
    • systemd 유닛이 비교적 단순하다.

    반대로 애플리케이션이 실행 중 여기저기 흩어진 경로를 건드리고, 셸을 여러 번 호출하고, 플러그인이나 후처리 스크립트를 동적으로 실행한다면 프로파일 작성보다 애플리케이션의 파일 구조를 먼저 정리하는 편이 비용 대비 효과가 큽니다. 이걸 무시하고 규칙만 늘리면 결국 /var/** rw, 같은 과한 허용으로 흘러가고, 그 시점부터는 보호 가치가 급격히 떨어집니다.

    Enforce와 Complain을 나누는 기준

    • enforce: 정책 위반을 차단합니다. 운영 보호용입니다.
    • complain: 일반 허용 규칙 밖 접근을 차단하지 않고 로그만 남깁니다. 다만 프로파일에 명시한 deny 규칙은 complain 모드에서도 계속 적용됩니다.

    여기서 많이 하는 실수가 “테스트 서버니까 바로 enforce”입니다. 저는 오히려 반대로 갑니다. 테스트 환경일수록 동작 패턴이 덜 안정적이라 예외 경로가 많고, 그래서 처음엔 complain이 낫습니다. enforce는 “정상 경로와 실패 경로를 둘 다 재현했고, 새 deny 로그가 더 이상 의미 있게 안 나온다”는 조건이 붙을 때 올립니다. 운영에서 장애 한 번 나면 AppArmor 자체에 대한 조직 신뢰가 확 꺾이거든요.

    리눅스 AppArmor 운영 전에 알아둘 파일과 도구

    리눅스 AppArmor를 만질 때는 도구를 많이 아는 것보다 언제 무엇을 쓰는지가 더 중요합니다. 아래 정도만 먼저 손에 익혀도 실무 대응 속도가 꽤 빨라집니다.

    • /etc/apparmor.d/: 프로파일 저장 위치
    • aa-status: 로드된 프로파일, enforce/complain 상태 확인
    • aa-complain, aa-enforce: 프로파일 모드 전환
    • apparmor_parser: 문법 검사와 프로파일 재로드
    • journalctl -k, dmesg, /var/log/audit/audit.log: 커널 또는 audit 로그 확인
    • aa-logprof: 로그 기반 규칙 제안
    • systemctl cat: systemd 유닛 정의 확인

    현장에서 바로 보는 기본 점검은 보통 이 정도면 충분합니다.

    sudo systemctl status apparmor
    sudo aa-status
    sudo ls /etc/apparmor.d/
    sudo journalctl -k -g apparmor -b
    

    제가 중요하게 보는 건 숫자보다 맥락입니다. aa-status에서 특정 프로파일이 로드됐는지, 어떤 모드인지 먼저 확인하고, journalctl -k -g apparmor -b로는 현재 부팅에서 실제 deny가 있었는지를 봅니다. auditd를 쓰는 서버라면 로그가 /var/log/audit/audit.log 쪽에 남을 수 있으니 그 점도 같이 확인해두면 덜 헤맵니다.

    AppArmor 프로파일 작성 실전: 작은 서비스부터 잠그는 게 맞습니다

    예시 시나리오를 하나 고정해보겠습니다. /usr/local/bin/report-sync라는 내부 동기화 스크립트가 있고, 설정은 /etc/report-sync/config.yml, 데이터는 /var/lib/report-sync/, 실행 중 락 파일은 /run/report-sync/에 남긴다고 가정하겠습니다. 외부 API 호출은 필요하지만 셸 실행은 필요 없다고 두겠습니다. 이 정도로 경로를 정리해두면 AppArmor 프로파일 작성 난도가 확 내려갑니다.

    1. 프로세스가 밟아야 하는 경로를 먼저 적습니다.
    2. 초안은 짧게 씁니다. 처음부터 다 맞히려 하지 않습니다.
    3. complain 모드에서 정상 시나리오와 실패 시나리오를 둘 다 돌립니다.
    4. deny 로그를 읽고 규칙을 추가하되, 상위 디렉터리를 통째로 열지 않습니다.
    5. 새 deny가 사라진 뒤 enforce로 올립니다.

    1. 프로파일 초안 작성

    sudo tee /etc/apparmor.d/usr.local.bin.report-sync > /dev/null <<'EOF'
    #include <tunables/global>
    
    /usr/local/bin/report-sync {
      #include <abstractions/base>
      #include <abstractions/nameservice>
    
      /usr/local/bin/report-sync r,
      /etc/report-sync/config.yml r,
    
      /var/lib/report-sync/ r,
      /var/lib/report-sync/** rwk,
    
      /run/report-sync/ rw,
      /run/report-sync/** rwk,
    
      network inet tcp,
      network inet6 tcp,
    
      deny /bin/sh x,
      deny /usr/bin/bash x,
      deny /usr/bin/dash x,
    }
    EOF
    

    여기서 몇 가지를 짚고 넘어가야 합니다. r은 읽기, w는 쓰기, k는 파일 잠금(lock), x는 실행입니다. 락 파일이나 PID 파일을 다루는 프로그램은 w만으로 부족할 때가 꽤 있습니다. 실제로 flock 같은 잠금 동작이 들어가면 k가 빠져서 계속 막히는 사례를 자주 봤습니다. 또 디렉터리 자체 권한과 하위 파일 권한은 분리해서 봐야 합니다. /var/lib/report-sync/ r,와 /var/lib/report-sync/** rwk,를 나눠 적는 이유도 그 지점 때문입니다.

    네트워크도 무조건 한 줄로 끝나진 않습니다. 단순 TCP 클라이언트라면 위 예시 정도로 시작할 수 있지만, 유닉스 도메인 소켓이나 raw 소켓을 쓰는 프로그램은 다른 규칙이 필요할 수 있습니다. 그래서 초안은 어디까지나 학습용 가설로 받아들이는 게 맞습니다.

    AppArmor 프로파일 작성 예시와 파일 경로 매핑 이미지

    프로파일 파일, 바이너리 경로, 허용된 데이터 디렉터리의 관계를 보여주는 구성 이미지입니다.

    2. 문법 검사와 complain 전환

    sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.report-sync
    sudo aa-complain /usr/local/bin/report-sync
    sudo aa-status | grep report-sync
    

    이 단계에서는 두 부류의 실패를 분리해서 봐야 합니다. 파서 오류는 프로파일 문법 문제고, 실행 중 deny는 권한 설계 문제입니다. 둘을 섞어 보면 디버깅 시간이 괜히 길어집니다. 파서가 깨질 때는 보통 중괄호 위치, 규칙 끝의 쉼표 누락, include 경로, 권한 토큰 오타 같은 기본 문법이 원인입니다.

    3. 실제 실행과 로그 수집

    sudo /usr/local/bin/report-sync
    sudo journalctl -k -g apparmor --since "10 minutes ago"
    sudo dmesg | grep -i apparmor
    

    로그를 볼 때는 한 줄 전체를 멍하니 읽기보다 핵심 필드만 먼저 잡는 편이 훨씬 빠릅니다.

    • operation=: 무엇을 하려 했는가. 예: open, exec, connect
    • profile=: 어떤 프로파일에 걸렸는가
    • name=: 실제 대상 경로는 무엇인가
    • requested_mask=: 프로세스가 원한 권한은 무엇인가
    • denied_mask=: 실제 막힌 권한은 무엇인가

    예를 들어 operation="open", name="/run/report-sync/lock", denied_mask="k"가 찍히면 이건 단순 쓰기 누락이 아니라 잠금 권한이 빠진 겁니다. 또 operation="exec"가 보이면 파일 접근 문제로만 보면 안 됩니다. 애플리케이션이 내부에서 다른 바이너리를 호출하고 있다는 뜻이라, 그 시점부터는 실행 전이인 ix, px, Px 같은 규칙을 검토해야 합니다.

    트러블슈팅: 실제 운영에서 자주 막히는 지점과 근본 원인

    AppArmor 디버깅은 규칙 추가 게임이 아닙니다. 왜 그 접근이 필요한지 이해하지 못한 채 허용만 늘리면, 나중에 문제를 재현하기가 더 어려워집니다. 아래는 제가 반복해서 본 실패 패턴입니다.

    문제 1. 설정 파일은 열었는데, 여전히 특정 파일 접근이 실패하는 경우

    겉으로는 /etc/myapp/config.yml만 읽는 것처럼 보여도 실제론 CA 인증서, locale 파일, 동적 모듈, 캐시 디렉터리, 소켓 파일을 추가로 건드리는 경우가 많습니다. 특히 HTTPS 통신을 하는 CLI는 /etc/ssl/ 계열 경로를 간접적으로 읽을 수 있습니다. 이럴 때 상위 디렉터리를 크게 열어버리면 빠르긴 해도, 나중에 왜 허용됐는지 설명이 안 됩니다.

    sudo aa-logprof
    

    aa-logprof는 유용하지만, 저는 제안된 규칙을 그대로 다 받지 않습니다. 기준은 단순합니다. 예측 가능한 고정 경로인가, 실패 원인이 해당 경로 하나로 닫히는가, 와일드카드를 쓰지 않아도 되는가를 먼저 봅니다. 예를 들어 설정 파일 1개 때문에 /etc/** r,를 받아들이면 그 순간 관리가 무너집니다.

    문제 2. 수동 실행은 되는데 systemd 서비스로만 실패하는 경우

    이건 정말 흔합니다. 원인은 보통 AppArmor 자체보다 실행 컨텍스트 차이입니다. systemd는 사용자, 작업 디렉터리, 환경 변수, RuntimeDirectory, StateDirectory, 소켓 활성화 여부가 전부 다를 수 있습니다. 수동으로는 안 밟던 /run/... 경로가 서비스 모드에선 갑자기 생기는 이유도 여기 있습니다.

    sudo systemctl cat report-sync.service
    sudo systemctl show report-sync.service -p User -p Group -p RuntimeDirectory -p StateDirectory -p WorkingDirectory
    sudo journalctl -u report-sync.service -b
    

    제가 보는 포인트는 세 가지입니다. 누가 실행하는가, 런타임 파일이 어디에 생기는가, ExecStartPre/ExecStartPost에서 별도 바이너리를 호출하는가입니다. 특히 전처리 스크립트가 있으면 메인 바이너리만 프로파일링해서는 잘 안 끝나더라고요.

    문제 3. deny 로그가 없는데 서비스가 죽는 경우

    이럴 땐 AppArmor만 붙들고 있으면 시간 낭비입니다. deny가 없으면 실제 차단은 없었다는 뜻일 가능성이 높습니다. 애플리케이션 자체 예외, 파일 소유권 문제, 잘못된 경로, systemd 타임아웃, 의존 서비스 미기동 같은 다른 층위를 바로 봐야 합니다.

    상황 우선 확인할 것 근본 원인 후보 제가 권하는 대응
    커널 또는 audit 로그에 AppArmor deny 존재 journalctl -k -g apparmor, /var/log/audit/audit.log 프로파일이 실제 접근을 차단 경로와 권한을 최소 범위로 추가
    deny 없음, 앱 로그에 파일 오류만 존재 애플리케이션 로그와 파일 소유권 경로 오타, 파일 미생성, UNIX 권한 문제 AppArmor보다 앱 설정과 POSIX 권한부터 확인
    수동 실행 성공, systemd 실패 systemctl show, /run, 실행 계정 실행 컨텍스트 차이 런타임 디렉터리와 보조 프로세스 경로를 반영
    규칙이 과도하게 많아짐 프로파일 범위와 파일 구조 애플리케이션 경로 설계가 산만함 정책 추가보다 경로 재구성이나 서비스 분리 검토
    operation="exec" deny 반복 호출되는 하위 바이너리 목록 외부 명령 실행 설계 무분별한 x 허용 대신 실행 전이 정책 검토

    이 표에서 핵심은 “어느 계층부터 볼 것인가”입니다. deny가 있으면 AppArmor부터, deny가 없으면 다른 원인부터 보는 식으로 순서를 고정해두면 추측이 크게 줄어듭니다.

    리눅스 AppArmor deny 로그 분석과 트러블슈팅 이미지

    커널 로그에서 operation, profile, name, denied_mask를 읽는 포인트를 강조한 이미지입니다.

    AppArmor 프로파일 작성 규칙을 다듬는 기준: 파일이 아니라 동작으로 자르세요

    제가 AppArmor 프로파일 작성에서 가장 경계하는 건 “일단 넓게 열고 나중에 줄이자”는 접근입니다. 실제로는 나중에 거의 안 줄어듭니다. 운영에 들어간 허용 규칙은 잘 안 걷히거든요. 그래서 저는 파일 기준보다 동작 기준으로 규칙을 끊습니다.

    • 설정 읽기: /etc/myapp/*.conf r,
    • 상태 저장: /var/lib/myapp/** rwk,
    • 로그 쓰기: /var/log/myapp/** w,
    • 런타임 파일: /run/myapp/** rwk,
    • DNS 조회 필요: #include <abstractions/nameservice>
    • 외부 프로세스 실행 필요: 대상 바이너리별로 별도 검토

    여기서 실행 권한은 특히 조심해야 합니다. x 하나로 끝나는 문제가 아니고, ix, px, Px처럼 실행 시 현재 프로파일을 상속할지, 별도 프로파일로 전환할지에 따라 보안 성질이 달라집니다. 저는 내부 배치가 tar, gzip, rsync 같은 외부 명령을 꼭 호출해야 할 때도 무작정 허용하지 않고, “정말 이 호출이 애플리케이션 내부에 있어야 하나”를 먼저 봅니다. 이거 한번 습관 붙여두면 정책이 훨씬 덜 지저분해집니다.

    언제 AppArmor를 쓰고, 언제 구조를 먼저 손봐야 하는가

    이 질문이 실무에선 더 중요합니다. 모든 서비스에 AppArmor를 똑같이 적용하는 건 좋은 운영이라고 보기 어렵습니다.

    상황 AppArmor 우선 구조 정리 우선
    내부 배치 스크립트, 작은 데몬 권장 보통 불필요
    데이터 경로가 명확한 API 서비스 권장 필요 시 병행
    여러 외부 명령을 연쇄 호출하는 모놀리식 앱 신중 우선 검토
    임시 파일과 소켓 위치가 제각각인 레거시 앱 신중 우선 검토
    서드파티 바이너리 보호 효과 큼 수정 불가라면 AppArmor 쪽이 현실적

    제가 실제로 추천하는 방향은 분명합니다. 작고 경로가 단순한 서비스는 AppArmor를 바로 적용하시고, 경로와 실행 흐름이 복잡한 서비스는 규칙을 늘리기 전에 파일 구조와 런타임 경로를 정리하시는 편이 낫습니다. AppArmor는 엉킨 구조를 마법처럼 정리해주는 도구라기보다, 정리된 구조를 더 안전하게 잠가주는 도구에 가깝습니다.

    검증: complain에서 enforce로 넘기기 전에 꼭 보는 체크리스트

    서비스가 한 번 성공했다고 바로 enforce로 올리면, 야간 배치나 장애 복구 루틴에서 다시 터질 수 있습니다. 그래서 저는 최소한 아래 단계를 거칩니다.

    1. 정상 시나리오를 여러 번 실행합니다.
    2. 재시도, 파일 없음, 로그 회전 직후처럼 실패 시나리오도 일부러 만듭니다.
    3. journalctl -k -g apparmor와 필요 시 audit 로그에 새 deny가 없는지 확인합니다.
    4. 프로파일을 다시 읽고, 목적이 모호한 와일드카드를 줄입니다.
    5. 그 다음에 enforce로 전환합니다.
    sudo aa-enforce /usr/local/bin/report-sync
    sudo aa-status | grep report-sync
    sudo journalctl -k -g apparmor --since "5 minutes ago"
    

    이때 기준은 단순합니다. 기능 성공 + 새 deny 없음 + 규칙 목적이 설명 가능함. 이 세 가지가 맞아야 합니다. deny가 계속 찍히는데 기능이 우연히 통과한 상태라면 아직 끝난 게 아닙니다. 선택 경로나 예외 처리 루틴에서 나중에 터질 가능성이 남아 있습니다.

    AppArmor 프로파일 작성에서 complain 모드와 enforce 모드 비교 이미지

    학습 단계인 complain 모드와 차단 단계인 enforce 모드의 차이를 한눈에 비교하는 이미지입니다.

    제가 많이 헤맸던 함정 하나: /tmp를 열어도 안 풀리는 이유

    예전에 백업 후처리 스크립트를 묶을 때 로그에는 /tmp/backup.lock만 보였습니다. 그래서 /tmp/** rwk,를 추가했는데도 실패가 계속됐습니다. 원인은 다른 데 있었습니다. 서비스가 systemd의 notify 메커니즘을 통해 상태를 알리고 있었고, 실제로는 /run/systemd/notify 계열 소켓 접근이 필요했던 겁니다. 겉으로 드러난 첫 번째 경로만 보고 달려들면 이런 식으로 시간을 잃습니다. 이거 한번 겪고 나면 로그 한 줄만 믿으면 안 된다는 걸 몸으로 배우게 됩니다.

    이 경험 이후로는 deny 로그를 한 줄만 보지 않습니다. 서비스가 어떤 타입으로 실행되는지, PID 파일과 소켓을 어디에 남기는지, 보조 바이너리를 호출하는지를 같이 봅니다. AppArmor 디버깅은 결국 프로세스의 발자국을 복원하는 작업입니다. 그 발자국이 정리되면 규칙은 오히려 짧아집니다.

    자주 묻는 질문

    프로파일은 수동으로 쓰는 게 좋을까요, 도구를 써야 할까요?

    초안은 수동으로 짧게 잡고, 실행 후엔 aa-logprof로 보완하는 방식을 권합니다. 처음부터 도구 제안을 전부 수락하면 과도한 허용이 들어가기 쉽습니다.

    requested_mask와 denied_mask는 어떻게 읽어야 하나요?

    requested_mask는 프로세스가 시도한 권한이고, denied_mask는 그중 실제 막힌 권한입니다. 읽기만 필요한 줄 알았는데 쓰기나 잠금까지 요청한다면, 허용을 늘리기 전에 애플리케이션 동작 자체를 다시 보는 게 맞습니다.

    홈랩과 운영 서버는 접근 방식을 달리해야 할까요?

    그렇습니다. 홈랩은 complain으로 동작 패턴을 학습하기 좋고, 운영은 이미 검증된 프로파일만 enforce로 올리는 편이 안정적입니다. 저는 홈랩에서 deny 패턴을 먼저 모아두고 운영에선 그 결과만 가져가는 방식을 선호합니다.

    실무에서 바로 적용할 추천안

    AppArmor 프로파일 작성이 필요한 상황이라면, 먼저 서비스의 설정 경로, 상태 저장 경로, 런타임 경로를 분리해두세요. 그다음 짧은 프로파일로 시작해서 complain에서 로그를 수집하고, deny를 보고 최소 범위로만 열어주면 됩니다. 이 순서를 지키면 정책이 쓸데없이 커지는 걸 꽤 잘 막을 수 있습니다.

    판단 기준도 분명하게 가져가시면 됩니다. 내부 스크립트, 배치, 작은 데몬, 서드파티 바이너리는 AppArmor 적용 효과가 큽니다. 반면 외부 명령 실행이 많고 경로가 산만한 레거시 서비스는 규칙을 늘리기 전에 구조를 먼저 정리하세요. 관련해서 SELinux 비교 글이나 systemd 보안 옵션 정리 글도 내부 링크로 함께 묶어두면 독자 체류 시간이 꽤 좋아집니다.

  • [Linux] Linux namespace 활용 사례: 격리 환경 구축과 보안 강화 실전

    [Linux] Linux namespace 활용 사례: 격리 환경 구축과 보안 강화 실전

    Linux namespace 활용 사례: 격리 환경 구축과 보안 강화 실전

    리눅스 서버를 오래 만지다 보면, 운영 호스트는 그대로 둔 채 어떤 프로세스가 실제로 무엇을 건드리는지 빨리 확인해야 하는 순간이 꼭 옵니다. 패키지를 하나 설치했더니 예상 못 한 소켓을 열고, 에이전트를 하나 올렸더니 부팅 직후 파일을 여기저기 만들고, 테스트 스크립트가 외부망으로 나가 버리는 식이죠. 저도 홈랩이든 실무든 비슷했고요. 그때 가장 손에 익혀둘 가치가 있었던 게 Linux namespace 활용이었습니다. 흔히 컨테이너의 재료 정도로만 설명되지만, 직접 unshare, ip netns, nsenter를 만져보면 이건 단순 개념 설명으로 끝낼 기술이 아니더라고요.

    중요한 건 기대치를 정확히 잡는 겁니다. namespace는 마법 상자가 아닙니다. 커널은 공유하고, CPU·메모리 같은 자원 제한도 기본으로 걸어주지 않습니다. 대신 프로세스가 보는 시야를 분리합니다. 이 차이를 이해하면 “언제 namespace를 직접 쓰고, 언제 컨테이너 런타임이나 VM으로 넘어가야 하는지” 판단이 훨씬 빨라집니다. 오늘은 정의 위주보다, Linux namespace 활용이 실제로 어디에 먹히고 어디서 한계가 드러나는지에 초점을 맞춰 보겠습니다.

    Linux namespace 활용에서 격리 범위를 보여주는 아키텍처 다이어그램

    호스트와 각 namespace가 PID, Network, Mount, UTS 관점에서 어떻게 분리되는지 한눈에 보여주는 개요 이미지입니다.

    1. 왜 Linux namespace 활용이 실무에서 바로 먹히는가

    제가 현장에서 namespace를 꺼내는 순간은 대체로 두 가지입니다. 첫째, 운영 호스트에 패키지나 설정 찌꺼기를 남기고 싶지 않을 때입니다. 둘째, 테스트 대상 프로세스가 어디까지 보이고 어디로 나가는지 통제하고 싶을 때죠. 이건 Dockerfile을 만들 정도로 반복적인 작업은 아닌데, 그렇다고 그냥 호스트에서 돌리기엔 찜찜한 상황에서 특히 유용합니다.

    • 서비스 시작 스크립트가 어떤 파일과 소켓을 만드는지 확인할 때
    • 외부 통신을 막은 채 애플리케이션 기동 여부만 검증할 때
    • 방화벽 정책이나 라우팅 변경 전, 별도 네트워크 공간에서 먼저 재현할 때
    • 문제 프로세스를 호스트 전체와 분리해 관찰할 때

    여기서 VM과 컨테이너, namespace를 같은 선상에서만 비교하면 판단이 흐려집니다. 제가 체감한 기준은 이렇습니다. VM은 커널까지 분리하는 대신 무겁고, 컨테이너는 운영 편의성이 좋지만 준비물이 생기고, namespace 직접 사용은 가장 가볍지만 조립 책임이 사용자에게 있습니다. 즉, namespace는 “기능이 약한 컨테이너”라기보다 “필요한 격리 축만 직접 뽑아 쓰는 커널 도구”에 가깝습니다.

    실무 품질을 좌우하는 포인트도 분명합니다. namespace는 CPU나 메모리 폭주를 막아주지 않습니다. 파일 시스템 쓰기 범위도 자동으로 최소화해 주지 않고요. 그래서 저는 보통 이렇게 선을 그어 설명합니다. namespace는 분리, cgroup은 제한, seccomp는 호출 축소, LSM(AppArmor/SELinux)은 정책 강제. 이걸 구분하지 않고 “격리됐다”라고만 말하면 운영 단계에서 사고 납니다.

    2. Linux namespace 활용 전, 무엇이 분리되는지 먼저 보자

    리눅스 네임스페이스는 종류를 외우는 것보다, 각 종류가 장애 분석과 보안에 어떤 영향을 주는지 아는 편이 더 중요합니다. 아래 표는 제가 실제로 판단할 때 쓰는 관점으로 정리한 겁니다.

    Namespace 종류 분리 대상 실무에서 바로 체감되는 효과 자주 놓치는 포인트 주요 도구
    PID 프로세스 ID 공간 테스트 프로세스를 별도 PID 1처럼 실행 가능 /proc를 새로 준비하지 않으면 ps 결과가 헷갈릴 수 있음 unshare --pid --fork, nsenter --pid
    NET 인터페이스, 라우팅, 포트, 방화벽 외부 송신 차단, veth 기반 통신 테스트, 정책 검증 lo를 올리지 않으면 내부 앱이 예상 밖으로 깨질 수 있음 ip netns, ip link, ip route
    MNT 마운트 포인트와 파일시스템 시야 임시 루트, bind mount, 읽기 전용 뷰 구성 전파 속성(shared/private) 이해 없이 건드리면 의도치 않게 호스트에 반영될 수 있음 unshare --mount, mount, pivot_root
    UTS 호스트명, NIS 도메인명 로그와 프롬프트에서 실험 환경 식별이 쉬워짐 보안 경계 자체는 약하고 주로 식별성 개선용 unshare --uts, hostname
    USER UID/GID 매핑과 일부 권한 범위 비특권 사용자 기반 실험에 유리 배포판 정책, 커널 설정, /etc/subuid//etc/subgid 상태에 따라 동작 차이가 큼 unshare --user --map-root-user
    IPC System V IPC, POSIX 메시지 큐 다른 프로세스와 IPC 충돌 방지 눈에 바로 안 보여서 문제 원인을 놓치기 쉬움 unshare --ipc

    저는 처음부터 전부 묶지 않습니다. 대부분 NET + MNT + PID로 시작하고, 로그 식별이 필요하면 UTS, 비특권 실험이 필요하면 USER를 추가합니다. 이유가 있습니다. namespace는 많이 넣을수록 안전해지는 면도 있지만, 동시에 원인 분리가 어려워집니다. 실패했을 때 어느 층에서 막혔는지 빨리 찾는 편이 실무에선 더 중요하거든요.

    3. 실전 시나리오: 격리된 테스트 환경을 직접 만들어보겠습니다

    이번에는 재현 가능한 시나리오로 가보겠습니다. 상황은 이렇다고 치죠. 새 에이전트를 올리기 전에, 이 프로세스가 외부망으로 나가지 못하게 막은 상태에서 정상 기동하는지 확인하고 싶습니다. 또한 호스트의 프로세스 목록과 파일시스템을 그대로 노출하고 싶지 않습니다. 이럴 때 저는 보통 1단계는 네트워크 분리, 2단계는 PID와 마운트 분리 순으로 갑니다.

    1. 호스트와 분리된 네트워크 namespace를 만듭니다.
    2. veth 페어를 붙여 통신 경로를 의도적으로 만듭니다.
    3. namespace 내부에 서비스 하나를 띄워 실제로 어디에 바인딩되는지 확인합니다.
    4. 그다음 PID, UTS, Mount namespace를 붙여 프로세스와 파일시스템 시야까지 좁힙니다.

    이 순서를 추천하는 이유는 단순합니다. 네트워크 단절, 바인딩 실패, /proc 관찰 오류, 마운트 전파 실수는 각기 보는 포인트가 다릅니다. 한 번에 다 엮으면 증상은 하나인데 원인은 넷 다 될 수 있거든요.

    3-1. 네트워크 namespace 생성

    먼저 가장 자주 쓰는 형태부터 보겠습니다. 아래 예시는 호스트 쪽 veth-host와 namespace 내부 veth-lab를 연결하고, 기본 라우트까지 넣는 최소 구성입니다. 외부 인터넷이 꼭 필요 없다면 default route는 일부러 빼도 괜찮습니다.

    sudo ip netns add labns
    sudo ip link add veth-host type veth peer name veth-lab
    sudo ip link set veth-lab netns labns
    
    sudo ip addr add 10.200.1.1/24 dev veth-host
    sudo ip link set veth-host up
    
    sudo ip netns exec labns ip addr add 10.200.1.2/24 dev veth-lab
    sudo ip netns exec labns ip link set lo up
    sudo ip netns exec labns ip link set veth-lab up
    sudo ip netns exec labns ip route add default via 10.200.1.1
    
    sudo ip netns exec labns ip addr
    sudo ip netns exec labns ip route

    이 단계에서 제일 많이 놓치는 건 두 가지입니다. 하나는 lo를 올리지 않는 것, 다른 하나는 “기본 라우트가 있어야만 내부 테스트가 된다”고 오해하는 겁니다. 사실 동일 서브넷 내 호스트와 namespace 간 통신만 볼 거라면 default route가 없어도 됩니다. 반대로 외부로 내보낼 생각이 없다면 일부러 default route를 넣지 않는 편이 더 안전합니다. 이거 진짜 편하더라고요. 시작부터 길을 다 열어 두면, 통신이 되는 이유가 라우팅인지 NAT인지 애플리케이션 재시도 때문인지 해석이 흐려집니다.

    또 하나, ip netns add는 named network namespace를 /var/run/netns/ 관례 아래에 연결해 관리합니다. 배포판에 따라 실제 경로가 /run/netns/로 보일 수도 있는데, 보통 둘은 같은 런타임 경로 계열로 이해하면 됩니다. 그래서 정리 순서도 중요합니다. 네임스페이스 내부 프로세스가 남아 있으면 삭제가 깔끔하게 안 될 수 있습니다.

    sudo ip netns pids labns
    sudo ip netns exec labns pkill -f http.server || true
    sudo ip netns del labns
    sudo ip link del veth-host || true

    여기서 ip netns pids를 먼저 보는 이유가 있습니다. veth 삭제가 실패하거나 namespace 삭제가 남는 경우, 대개 내부에 살아 있는 프로세스가 원인입니다. 명령어는 간단한데 cleanup을 무시하면 실습이 반복될수록 환경이 꼬입니다.

    3-2. namespace 내부에서 서비스 실행

    이제 namespace 내부에 실제 서비스를 올려 보겠습니다. 예시는 단순하지만, 판단 기준은 실무 그대로 가져갈 수 있습니다. 핵심은 “서비스가 떴는가”가 아니라 어디에 바인딩됐고, 어느 네임스페이스 관점에서 보여야 정상인가를 확인하는 겁니다.

    sudo mkdir -p /srv/labns/www
    printf '%s\n' 'hello from lab namespace' | sudo tee /srv/labns/www/index.html > /dev/null
    
    sudo ip netns exec labns bash -lc 'cd /srv/labns/www && python3 -m http.server 8080 --bind 10.200.1.2'
    
    # 다른 터미널에서 확인
    curl http://10.200.1.2:8080/
    sudo ip netns exec labns ss -lntp
    sudo ip netns exec labns curl -I http://10.200.1.2:8080/

    제가 이 단계에서 항상 같이 보는 건 ss -lntp와 바인딩 주소입니다. 127.0.0.1에만 바인딩된 서비스는 namespace 외부에서 접근되지 않아도 정상일 수 있습니다. 반대로 0.0.0.0에 걸렸다면 namespace 내부에선 모든 인터페이스를 받는다는 뜻이라, 추후 포워딩을 붙이면 생각보다 넓게 열릴 수 있습니다. 즉, 서비스 가시성 문제를 네트워크 문제로 오진하지 않는 것이 중요합니다.

    호스트에서 안 열리는데 namespace 내부에서는 잘 뜨는 경우도 자주 봅니다. 이때는 실패가 아니라, 오히려 격리가 의도대로 작동한 신호일 수 있습니다. 확인 순서는 보통 이렇습니다.

    • ip netns exec labns ss -lntp로 프로세스가 어떤 IP와 포트에 bind 했는지 본다
    • curl을 namespace 내부에서 먼저 날린다
    • 그다음 호스트에서 같은 IP로 접근해 경로가 있는지 확인한다
    • 필요하면 NAT나 포워딩을 추가하되, 마지막 단계로 미룬다
    리눅스 네임스페이스 기반 격리 환경 구축 네트워크 구성도

    호스트의 veth와 namespace 내부 인터페이스가 어떻게 연결되고, 테스트 서비스가 어느 IP에 바인딩되는지 설명하는 구성도입니다.

    3-3. 외부 송신까지 통제하려면 어디를 더 봐야 하나

    네트워크 namespace를 만든 뒤 많은 분들이 “이제 외부 인터넷도 자동으로 막히겠지”라고 생각하시는데, 그건 아닙니다. 경로와 NAT를 어떻게 주느냐에 따라 얼마든지 나갈 수 있습니다. 그래서 보안 검증 목적이면 처음에는 default route를 아예 주지 않거나, 주더라도 포워딩과 NAT를 넣지 않은 상태에서 출발하는 편이 낫습니다.

    외부 통신이 필요한 테스트라면 호스트에 포워딩과 NAT를 붙일 수는 있습니다. 다만 이건 편의성보다 해석 난도가 올라갑니다. 정책 검증이 목적이라면 “닫힌 상태에서 하나씩 여는 방식”이 원인 파악에 훨씬 유리합니다. 최소 예시는 아래처럼 구성합니다. 최근 배포판은 nftables를 기본으로 쓰는 경우도 많으니, 환경에 따라 iptables 명령이 호환 계층인지도 같이 확인하는 편이 좋습니다.

    # 호스트에서 IPv4 forwarding 활성화
    sudo sysctl -w net.ipv4.ip_forward=1
    
    # 예시: labns 대역을 외부 인터페이스 eth0로 NAT
    sudo iptables -t nat -A POSTROUTING -s 10.200.1.0/24 -o eth0 -j MASQUERADE
    sudo iptables -A FORWARD -i eth0 -o veth-host -m state --state RELATED,ESTABLISHED -j ACCEPT
    sudo iptables -A FORWARD -i veth-host -o eth0 -j ACCEPT

    다만 저는 일회성 분석 용도라면 여기까지는 잘 안 갑니다. NAT가 들어오면 이제 문제 원인이 애플리케이션인지, 라우팅인지, 포워딩 정책인지, 외부 DNS인지 한 층 더 늘어나기 때문입니다. 단순 기동 확인이라면 닫힌 내부망에서 끝내는 쪽이 훨씬 깔끔했습니다.

    3-4. PID, UTS, Mount까지 붙여 더 독립적인 실행 환경 만들기

    네트워크 분리만으로는 부족할 때가 많습니다. 프로세스 목록을 호스트와 분리하고, hostname을 바꾸고, 마운트 시야까지 따로 가져가면 디버깅 노이즈가 크게 줄어듭니다. 제가 자주 쓰는 형태는 아래와 같습니다.

    sudo unshare --fork --pid --mount --uts --ipc bash -lc '
      mount --make-rprivate /
      mount -t proc proc /proc
      hostname lab-host
      echo "[inside namespace] hostname: $(hostname)"
      echo "[inside namespace] process list:"
      ps -ef
      sleep 300
    '

    여기서 핵심은 두 줄입니다. mount --make-rprivate /와 mount -t proc proc /proc입니다. 첫 번째는 마운트 이벤트 전파를 끊어 의도치 않은 공유를 줄이는 역할을 하고, 두 번째는 현재 PID namespace 관점의 /proc을 다시 보여주기 위한 겁니다. 참고로 최근 unshare는 새 mount namespace에서 기본적으로 마운트를 private 쪽으로 돌리는 동작을 하기도 하지만, 명시적으로 적어두면 실습 문서나 운영 절차에서 덜 헷갈립니다.

    특히 PID namespace는 PID 1의 성격 때문에 생각보다 함정이 있습니다. 격리 환경 안에서 PID 1은 일반 프로세스보다 시그널 처리와 좀비 수거에 민감합니다. 실험처럼 짧게 돌릴 땐 괜찮지만, 조금이라도 오래 유지할 프로세스라면 tini 같은 init 래퍼나 별도 supervisor 역할을 고려하는 편이 안전합니다. 이 부분은 컨테이너에서도 똑같이 반복되죠.

    비특권 실험이 필요하면 USER namespace도 검토할 수 있습니다.

    unshare --user --map-root-user --mount --pid --fork bash -lc '
      id
      mount -t proc proc /proc
      ps -ef
      cat /proc/self/uid_map
      cat /proc/self/gid_map
    '

    다만 이건 “되면 좋고 아니면 말고” 식으로 접근하면 안 됩니다. 실제 서버에서는 /proc/sys/user/max_user_namespaces 값, 배포판 보안 정책, 그리고 일부 배포판에서만 제공되는 /proc/sys/kernel/unprivileged_userns_clone 같은 설정 때문에 막히는 경우가 적지 않습니다. 저는 이 단계에서 시간을 오래 쓰기보다, 운영 정책이 금지한 기능이라면 굳이 namespace 직접 조합에 집착하지 않는다는 쪽입니다. 홈랩과 운영 서버가 다르게 반응하는 대표적인 영역이 바로 여기거든요.

    4. Linux namespace 활용과 보안 강화, 어디까지 믿어도 되나

    Linux namespace 활용의 가장 큰 장점은 사고 범위를 줄이는 데 있습니다. 테스트 프로세스가 호스트 전체 프로세스 목록을 못 보고, 네트워크 인터페이스도 제한되고, 마운트 시야까지 좁아지면 “한 번 잘못 돌린 실험이 운영 전체를 어지럽히는 일”이 크게 줄어듭니다. 실수 방지 효과가 꽤 큽니다. 하지만 여기까지입니다. namespace만으로 완전한 샌드박스가 됐다고 보면 위험합니다.

    이유는 단순합니다. 커널은 여전히 공유합니다. 커널 취약점이 있거나 capability를 넓게 줬거나, 마운트와 디바이스 접근을 느슨하게 두면 격리 강도는 금방 낮아집니다. 제가 실무에서 namespace를 단독 보안 장치로 보지 않는 이유도 여기에 있습니다. 보통 아래 조합으로 봅니다.

    • namespace: 프로세스가 보는 세계 분리
    • cgroup: CPU, 메모리, pids 폭주 제한
    • seccomp: 허용 syscall 범위 축소
    • AppArmor 또는 SELinux: 파일·소켓·능력 사용 제약
    • capability drop: 필요 없는 권한 제거

    실무 판단을 더 직접적으로 말하면 이렇습니다. namespace는 “격리의 뼈대”이고, seccomp와 capability 조정이 “탈출 비용을 올리는 장치”이며, cgroup은 “망가질 때 피해 규모를 제한하는 장치”입니다. 하나만 넣고 안심하는 구조가 제일 위험했습니다.

    systemd를 쓰는 환경이라면, 별도 컨테이너 런타임 없이도 서비스 단에서 격리 옵션을 꽤 보강할 수 있습니다. 예를 들면 아래 같은 방향입니다.

    [Service]
    ExecStart=/usr/local/bin/my-agent
    NoNewPrivileges=yes
    PrivateTmp=yes
    ProtectSystem=strict
    ProtectHome=yes
    PrivateDevices=yes
    RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
    CapabilityBoundingSet=
    SystemCallFilter=@system-service

    이건 namespace의 대체재라기보다 보완재에 가깝습니다. 특히 “운영 서비스인데 컨테이너까지는 안 가고 싶다”는 구간에서 생각보다 강력합니다. 저도 네트워크 namespace로 송신 범위를 줄이고, systemd 옵션으로 파일시스템과 권한을 더 잠그는 조합을 자주 씁니다. 관련 옵션을 더 파고들고 싶다면 systemd 보안 설정 정리 글도 함께 보시면 흐름이 더 잘 잡힙니다.

    5. 트러블슈팅: 제가 실제로 자주 겪었던 문제들

    명령어보다 중요한 게 해석 기준입니다. namespace는 증상이 비슷해도 원인이 전혀 다른 경우가 많습니다. 아래는 제가 가장 자주 봤던 실패 패턴과, 그때 실제로 다시 확인하는 순서입니다.

    5-1. ping은 되는데 애플리케이션 통신은 안 되는 경우

    이건 라우팅보다 바인딩 주소 문제인 경우가 많았습니다. 127.0.0.1에만 바인딩된 서비스는 namespace 내부에서는 잘 떠 보이는데, 호스트나 다른 namespace에서는 닿지 않습니다. 반대로 0.0.0.0에 걸렸는데도 안 붙는다면 방화벽이나 경로를 보게 되죠. 저는 이럴 때 순서를 고정해 둡니다.

    • ss -lntp로 listening 주소를 먼저 본다
    • curl을 서비스가 실행 중인 namespace 내부에서 먼저 친다
    • 설정 파일의 listen, bind, host 값을 본다
    • 그다음에야 라우팅과 필터링 규칙을 본다

    이 순서를 추천하는 이유는, 네트워크 문제처럼 보이지만 실제론 프로세스 설정 문제인 경우가 정말 많기 때문입니다. bind 주소 확인 없이 iptables부터 보는 건 대개 시간 낭비였습니다.

    5-2. PID namespace에서 ps 결과가 이상한 경우

    거의 항상 /proc 문제였습니다. 새 PID namespace에 들어갔는데 현재 namespace에 맞는 proc 파일시스템을 보지 않으면, 프로세스 목록이 호스트 기준처럼 보여 혼란이 생깁니다.

    mount -t proc proc /proc
    ps -ef
    cat /proc/1/status
    readlink /proc/self/ns/pid

    /proc/1/status를 보면 현재 namespace 관점의 PID 1이 누구인지 알 수 있고, readlink /proc/self/ns/pid는 실제로 다른 namespace inode를 보고 있는지 확인하는 데 도움이 됩니다. 여기서 “ps가 이상하다”는 증상 자체가 문제의 본질이 아니라 관찰 대상이 잘못됐다는 신호인 경우가 많습니다.

    5-3. 사용자 namespace가 서버마다 다르게 동작하는 경우

    이건 커널 기능 차이보다 운영 정책 차이가 더 크게 체감됐습니다. 특히 비특권 사용자로 unshare --user --map-root-user를 시도할 때 서버마다 결과가 달라집니다.

    cat /proc/sys/user/max_user_namespaces
    [ -f /proc/sys/kernel/unprivileged_userns_clone ] && cat /proc/sys/kernel/unprivileged_userns_clone
    id
    uname -r
    • max_user_namespaces가 낮거나 0이면 생성이 막힐 수 있습니다.
    • unprivileged_userns_clone는 일부 배포판에서만 노출되는 설정이라, 파일이 없다고 이상한 건 아닙니다.
    • 보안 기준이 엄격한 서버는 정책상 의도적으로 비활성화한 경우가 많습니다.

    이럴 때 억지로 우회하는 건 보통 좋은 설계가 아닙니다. 제가 권하는 건 두 가지 중 하나입니다. 운영에서 반드시 필요하면 관리자 정책으로 정식 허용을 받거나, 아니면 컨테이너 런타임이나 별도 테스트 노드로 경로를 바꾸는 것. 운영 정책과 싸우는 형태는 오래 못 갑니다.

    5-4. 마운트는 분리한 줄 알았는데 호스트에 영향이 남는 경우

    이건 mount namespace를 처음 만질 때 특히 많이 겪습니다. 원인은 대개 마운트 전파 속성입니다. namespace를 나눴다고 해서 모든 마운트 이벤트가 자동으로 고립되는 건 아닙니다. 그래서 저는 mount 작업 전에 mount --make-rprivate /를 거의 습관처럼 넣습니다. 이 한 줄을 빼먹으면 bind mount나 임시 마운트가 예상 밖으로 공유돼 버릴 수 있습니다.

    증상은 보통 이렇습니다. 실험 환경 안에서만 붙인 줄 알았던 마운트가 호스트에서도 보이거나, 반대로 호스트 변경이 내부에 그대로 따라 들어옵니다. 이 경우에는 namespace 개념 자체보다 전파 모델을 다시 보는 편이 빠릅니다. 실무에서 mount namespace가 네트워크 namespace보다 까다롭게 느껴지는 이유가 여기 있습니다.

    Linux namespace 활용 점검과 트러블슈팅을 표현한 터미널 스타일 이미지

    ip netns exec, ss -lntp, mount -t proc proc /proc 같은 핵심 점검 포인트를 한 장으로 정리한 이미지입니다.

    6. 검증과 결과 확인: 무엇을 보고 성공이라 판단하나

    명령이 에러 없이 끝났다고 성공은 아닙니다. namespace는 “만들어졌다”와 “의도대로 격리됐다” 사이에 차이가 큽니다. 저는 아래 순서대로 확인합니다.

    1. 인터페이스가 의도한 namespace에 들어갔는지 본다
    2. 프로세스가 내부에서 어떤 PID와 hostname으로 보이는지 본다
    3. 서비스 포트가 기대한 주소에 bind 되었는지 확인한다
    4. 호스트와 namespace 사이 통신 범위를 점검한다
    5. 원치 않은 외부 송신 경로가 없는지 마지막에 확인한다
    ip netns list
    sudo ip netns exec labns ip addr
    sudo ip netns exec labns ip route
    sudo ip netns exec labns ps -ef
    sudo ip netns exec labns ss -lntp
    sudo ip netns exec labns curl -I http://10.200.1.2:8080/
    
    # 호스트에서 접근 확인
    curl -I http://10.200.1.2:8080/
    
    # namespace 내부에서 호스트 방향 확인
    sudo ip netns exec labns ping -c 2 10.200.1.1
    
    # 실제 네임스페이스 분리 여부 확인
    sudo ip netns exec labns readlink /proc/self/ns/net
    readlink /proc/self/ns/net

    결과 해석 기준도 미리 정해 두면 좋습니다.

    • ip netns list에 보이면 생성은 된 겁니다. 하지만 그걸로 끝은 아닙니다.
    • ip addr에 기대한 IP가 없으면 링크 이동이나 주소 할당 단계부터 다시 봐야 합니다.
    • ss -lntp에 프로세스가 없으면 서비스 시작 실패입니다. 이때는 로그보다 실행 명령과 bind 주소를 먼저 봅니다.
    • 호스트에서는 안 보이는데 내부에서는 보이면, 실패가 아니라 격리 성공일 수 있습니다.
    • 외부 통신이 되면 안 되는 시나리오인데 HTTP나 DNS가 나가면 default route, 포워딩, NAT부터 다시 봐야 합니다.

    제가 특히 강조하는 건 마지막 항목입니다. 격리 환경 검증에서 자주 빠지는 게 “열려 있지 않아야 할 경로 확인”입니다. 되는 것만 보면 절반만 본 겁니다. 막혀야 할 것이 실제로 막히는지 확인해야 보안 관점의 검증이 됩니다.

    7. 운영에 붙일 때의 현실적인 추천 조합

    실제로 써보면 모든 namespace를 한 번에 다 쓰는 방식은 종종 과합니다. 목적에 따라 조합을 나누는 편이 유지보수성이 더 좋았습니다. 아래 표는 제가 선택할 때 쓰는 기준입니다.

    상황 추천 조합 이유 굳이 피할 상황
    간단한 네트워크 테스트 NET 설정이 단순하고 효과가 바로 보입니다. 장기 운영 서비스처럼 재현성과 배포 표준화가 중요한 경우
    프로세스 독립 실행 검증 PID + UTS + MNT 프로세스, hostname, 파일시스템 시야를 함께 분리하기 좋습니다. 마운트 전파를 이해하지 못한 상태에서 급히 운영에 넣는 경우
    보안 민감한 임시 실행 USER + NET + MNT + cgroup + seccomp 권한, 시야, 자원, syscall 범위를 함께 줄일 수 있습니다. USER namespace 정책이 금지된 운영 서버
    반복 배포되는 서비스 직접 namespace보다 컨테이너 런타임 검토 이미지 관리, 재시작 정책, 로깅, 표준화 측면에서 유리합니다. 일회성 포렌식 재현이나 빠른 실험처럼 준비 비용이 아까운 경우

    여기서 제 추천은 꽤 명확합니다. 한 번성 검증이면 직접 namespace, 반복 운영이면 컨테이너 런타임이 대체로 맞습니다. 전자는 스패너처럼 빠르게 꺼내 쓰는 도구이고, 후자는 운영 체계에 가깝습니다. 둘 중 무엇이 더 우월하냐의 문제가 아니라, 조립 책임을 누가 지느냐의 차이로 보는 편이 현실적입니다.

    비용 관점에서도 비슷합니다. namespace 직접 조합은 초기 준비가 가볍지만 사람이 더 똑똑해야 합니다. 반면 컨테이너 런타임은 준비물이 늘어나지만 반복성이 좋아집니다. 그래서 팀 단위로 넘어가면 후자가 이기고, 개인 디버깅이나 빠른 재현 실험에선 전자가 여전히 강합니다. 관련해서 컨테이너와 VM 비교 글까지 같이 연결하면 내부 링크 흐름도 자연스럽게 만들 수 있습니다.

    Linux namespace 활용과 컨테이너 선택 기준을 비교한 인포그래픽

    일회성 테스트, 반복 운영, 보안 요구사항에 따라 어떤 선택이 적합한지 비교한 요약 이미지입니다.

    8. 자주 받는 질문과 마지막 권고

    Q1. namespace만 쓰면 컨테이너를 대체할 수 있나요?

    부분적으로는 가능합니다. 특히 빠른 재현, 네트워크 격리 실험, 프로세스 시야 분리 정도는 namespace만으로도 충분히 됩니다. 다만 이미지 관리, 배포 파이프라인, 재시작 정책, 로그 수집 표준화까지 생각하면 컨테이너 런타임이 훨씬 편한 구간이 분명히 있습니다. 저는 보통 “문제 재현과 분석 단계는 namespace”, “반복 서비스화 단계는 컨테이너”로 나눠 봅니다.

    Q2. 보안 강화 목적이라면 무엇부터 붙여야 하나요?

    제가 권하는 시작점은 NET namespace로 통신 범위를 먼저 줄이고, 그다음 MNT와 PID를 분리하는 방식입니다. 여기에 capability 최소화, seccomp, cgroup 제한을 얹는 쪽이 사고를 줄이기 좋았습니다. 처음부터 다 넣으면 강해 보이긴 하지만, 실패 시 원인 분리가 너무 어려워집니다.

    Q3. 홈랩에서도 해볼 가치가 있나요?

    충분합니다. 오히려 홈랩이 제일 좋습니다. 실패해도 운영 영향이 없고, namespace 특유의 함정인 bind 주소, loopback, /proc 준비, mount propagation을 몸으로 익히기 좋거든요. 저도 홈랩에서 먼저 손에 익힌 뒤에야 운영 서버에서 판단 속도가 빨라졌습니다.

    마지막으로 추천을 딱 잘라 말씀드리면 이렇습니다. 단발성 테스트, 포렌식성 재현, 서비스 기동 검증이 목적이면 Linux namespace 활용을 바로 써보는 편이 맞습니다. 반대로 팀 단위 반복 운영, 배포 자동화, 표준 이미지 관리가 목적이면 직접 namespace를 조립하기보다 컨테이너 런타임으로 넘어가는 쪽이 낫습니다. 보안 관점에서도 마찬가지입니다. namespace는 아주 좋은 출발점이지만 종착점은 아닙니다. 네트워크 분리만 믿지 말고, 권한과 syscall, 자원 제한까지 묶어야 실전에서 덜 흔들립니다.

  • [Linux] cgroup v2 vs v1: 리눅스 컨테이너 리소스 제어 차이 분석

    [Linux] cgroup v2 vs v1: 리눅스 컨테이너 리소스 제어 차이 분석

    cgroup v2 vs v1: 리눅스 컨테이너 리소스 제어 차이 분석

    컨테이너 운영에서 cgroup v2 vs v1은 단순한 버전 비교가 아닙니다. 실제 운영에선 “리소스 제한을 걸었다”와 “커널이 의도한 방식으로 그 제한을 집행한다” 사이의 간극이 자주 보이거든요. CPU는 남는데 응답 시간이 흔들리거나, 메모리 제한은 넣었는데 기대한 시점이 아니라 갑자기 OOM으로 터지거나, 블록 I/O가 한쪽에 쏠리면서 옆 워크로드 지연이 커지는 식입니다. 이런 문제를 따라가 보면 결국 cgroup 계층 구조, 컨트롤러 위임 방식, 런타임이 커널과 만나는 지점에서 갈리더라고요.

    현장에서는 아직도 v1 흔적이 남아 있습니다. 다만 신규 배포판, systemd 중심 서버, 최근 컨테이너 런타임 조합이라면 cgroup v2를 기준으로 보는 편이 훨씬 덜 헷갈립니다. 운영 문서나 장애 대응 런북을 정리할 때도 먼저 묻는 건 하나예요. “이 서버는 v1 습관으로 읽어야 하나, 아니면 v2 규칙으로 읽어야 하나?” 이걸 초반에 잘못 잡으면 뒤에 보는 메트릭과 파일 경로 해석이 전부 어긋납니다.

    cgroup v1과 v2 계층 구조 비교 다이어그램

    cgroup v1은 컨트롤러별로 계층이 갈라지고, cgroup v2는 단일 트리에서 CPU·메모리·I/O 정책을 함께 다루는 모습을 보여주는 개요 이미지입니다.

    1. 왜 아직도 cgroup v1과 v2를 따로 봐야 하나요

    커널 기능만 놓고 보면 v1도 충분히 강력했습니다. 문제는 운영 복잡도였죠. v1은 <code>cpu, memory, blkio 같은 컨트롤러가 각자 별도 계층을 가질 수 있어서, 같은 프로세스라도 CPU는 A 경로, 메모리는 B 경로, I/O는 C 경로에 속하는 일이 자연스러웠습니다. 설계 자유도는 높았지만 장애가 나면 운영자가 세 개의 지도를 동시에 펼쳐야 했습니다. 이 구조는 대규모 운영보다는 기능이 먼저 확장된 커널 인터페이스에 더 가깝다고 보는 편이 맞습니다.

    v2는 반대로 갔습니다. 유연성을 조금 덜어내는 대신 정책 일관성, 계층 예측 가능성, systemd와의 결합 안정성을 얻었습니다. 실무 체감도 꽤 분명합니다. v1에서는 “어디를 봐야 하지?”가 먼저였고, v2에서는 “이 값이 왜 이렇게 집행됐지?”가 먼저예요. 전자는 길을 잃기 쉽고, 후자는 원인 분석이 비교적 선형적입니다.

    제가 기준으로 삼는 차이는 세 가지입니다.

    • 관찰 경로 수: v1은 컨트롤러별로 흩어지고, v2는 한 트리에서 따라가기 쉽습니다.
    • 정책 위임 방식: v2는 부모가 자식에게 무엇을 넘겼는지 cgroup.subtree_control로 드러납니다.
    • 운영 도구 친화성: systemd, 최신 런타임, 배포판 기본값과 맞물릴 때 v2가 덜 삐걱거립니다.

    2. cgroup v2 vs v1 핵심 개념: 파일명 차이보다 더 큰 변화

    cgroup v2 vs v1을 파일명 변경 정도로만 기억하면 절반만 이해한 셈입니다. 많이들 memory.limit_in_bytes가 memory.max로 바뀌었다는 식으로 외우는데, 진짜 변화는 “리소스 제한을 어떤 계층 규칙 위에 올릴 것인가”에 있습니다. 이 차이를 이해해야 마이그레이션할 때 덜 흔들립니다.

    v1이 남긴 운영적 특징

    • 컨트롤러마다 별도 마운트와 별도 경로가 가능해서 추적 포인트가 많습니다.
    • 오래된 자동화 스크립트와 호환성이 좋지만, 경로 가정이 강하게 박혀 있는 경우가 많습니다.
    • 성능 이슈가 났을 때 CPU, 메모리, I/O를 같은 구조로 읽기 어렵습니다.

    v2가 바꾼 운영적 특징

    • /sys/fs/cgroup 단일 트리에서 정책과 통계를 같이 읽습니다.
    • cgroup.controllers와 cgroup.subtree_control로 하위 위임 여부가 명시됩니다.
    • cpu.max, memory.max, io.max처럼 이름 규칙이 정돈돼서 자동화 작성이 수월합니다.
    • memory.high, memory.low, memory.min처럼 “강한 상한”과 “보호·완충 구간”을 나눠 설계하기 좋습니다.
    항목 cgroup v1 cgroup v2 실무 해석
    계층 구조 컨트롤러별 분리 가능 통합 계층(unified hierarchy) v2가 장애 분석 경로를 줄입니다.
    CPU 제한 cpu.cfs_quota_us, cpu.cfs_period_us cpu.max v2는 쿼터와 주기를 한 파일에서 읽습니다.
    CPU 가중치 cpu.shares cpu.weight 절대 제한보다 경쟁 상황에서의 우선순위 설계에 중요합니다.
    메모리 상한 memory.limit_in_bytes memory.max 상한만 보면 부족하고, v2에선 memory.high도 같이 봐야 합니다.
    I/O 제어 blkio.* io.* 장치 단위 제한과 관찰 포인트가 v2에서 더 일관적입니다.
    위임 모델 복잡하고 일관성 낮음 명시적 위임 하위 cgroup이 왜 제어 파일을 못 쓰는지 원인 파악이 쉽습니다.
    운영 난이도 도구와 경로 의존성이 큼 상대적으로 단순 신규 표준화는 v2 쪽이 유리합니다.

    실무에서 특히 큰 차이는 v2의 메모리 압박 제어 모델입니다. v1 시절에는 상한(limit) 위주로 사고하는 팀이 많았는데, 상한만으로 운영하면 평소엔 멀쩡하다가 피크 순간에 갑자기 세게 잘리는 일이 생깁니다. v2는 memory.high 같은 완충 구간을 둘 수 있어서, “죽이기 전에 먼저 늦추는” 설계를 하기가 좋습니다. 이거 트래픽 피크가 있는 서비스에선 체감 차이가 꽤 큽니다.

    3. cgroup v2 vs v1 확인: 내 서버가 어떤 계층인지 빠르게 판별하기

    이 단계는 생략하면 안 됩니다. 장애 대응에서도 가장 먼저 보는 지점이에요. 블로그 글이나 사내 문서는 v2 기준인데, 실제 호스트는 hybrid 또는 v1 잔재가 섞여 있으면 설명이 전부 어긋나기 때문입니다.

    1. 파일시스템 타입을 확인합니다.
    2. 마운트 구조를 확인합니다.
    3. 현재 프로세스가 속한 cgroup 경로를 확인합니다.
    4. 대표 제어 파일 이름을 확인합니다.
    # 1) cgroup 파일시스템 타입 확인
    stat -fc %T /sys/fs/cgroup
    
    # 2) 마운트 구조 확인
    mount | grep cgroup
    
    # 3) 현재 셸의 cgroup 소속 확인
    cat /proc/self/cgroup
    
    # 4) v2 대표 파일 확인
    ls /sys/fs/cgroup | grep -E 'cgroup.controllers|cgroup.subtree_control|cpu.max|memory.max|io.max'

    판별 기준은 이렇습니다.

    • stat -fc %T /sys/fs/cgroup 결과가 cgroup2fs면 v2입니다.
    • mount 결과에서 cgroup on /sys/fs/cgroup/cpu처럼 컨트롤러별 마운트가 여럿 보이면 v1일 가능성이 큽니다.
    • /proc/self/cgroup이 단일 계층 형태면 v2, 여러 컨트롤러 라인이 나뉘면 v1 또는 hybrid일 가능성이 큽니다.

    systemd 환경에서는 system.slice, user.slice, kubepods.slice 같은 경로가 보일 수 있습니다. 이건 이상 징후가 아니라 커널 계층을 서비스 관리 단위와 맞춘 흔적입니다. 오히려 경로가 이렇게 구조적으로 보이면 추적이 쉬워집니다.

    다만 여기서 자주 놓치는 함정이 있습니다. 호스트는 v2인데, 오래된 문서나 툴링이 v1 파일명을 기준으로 체크를 돌리는 경우입니다. 그러면 모니터링은 “파일 없음”만 찍고 끝납니다. 그래서 단순 버전 확인에서 멈추지 말고, 실제 자동화가 찾는 파일명까지 같이 검증하는 편이 안전합니다.

    /sys/fs/cgroup 아래 파일 구조와 컨트롤러 예시

    실제 서버에서 확인하는 cgroup 파일 구조 예시와 cgroup.controllers, cpu.max, memory.max 같은 핵심 파일 위치를 보여주는 이미지입니다.

    4. 실전 구현: cgroup v2에서 CPU와 메모리 제한 걸기

    리눅스에서 cgroup을 만지는 방법은 크게 둘입니다. systemd에 맡기거나, 파일시스템을 직접 다루거나입니다. 운영 서버에서는 전자를 먼저 권합니다. 이유는 단순해요. 프로세스 배치, 권한, 정리(clean-up), 서비스 수명주기를 systemd가 같이 책임져 주기 때문입니다. 반대로 원리 이해나 실험에는 후자가 빠릅니다.

    방법 A. systemd-run으로 transient unit 실행

    # 메모리 상한 512M, 메모리 압박 임계값 384M, CPU 쿼터 50%
    sudo systemd-run --unit=cg-demo --scope \
      -p MemoryHigh=384M \
      -p MemoryMax=512M \
      -p CPUQuota=50% \
      -p CPUQuotaPeriodSec=100ms \
      bash -c 'stress-ng --vm 1 --vm-bytes 700M --cpu 2 --timeout 60s'
    
    # systemd 속성 및 매핑 결과 확인
    systemctl status cg-demo.scope
    systemctl show cg-demo.scope \
      -p ControlGroup \
      -p MemoryCurrent \
      -p MemoryHigh \
      -p MemoryMax \
      -p CPUQuotaPerSecUSec \
      -p CPUQuotaPeriodUSec

    여기서 중요한 포인트는 MemoryMax만 보지 않는 겁니다. 서비스가 순간 메모리 피크가 있는 구조라면 먼저 MemoryHigh를 잡고 관찰하는 편이 낫습니다. 상한을 너무 낮게 바로 조이면 OOM으로 가는 길이 짧아지거든요. 반대로 MemoryHigh는 압박 신호를 먼저 주기 때문에, 애플리케이션이 캐시를 줄이거나 GC가 반응할 시간을 벌 수 있습니다.

    CPU도 비슷합니다. CPUQuota=50%는 직관적이지만, 워크로드가 짧은 버스트를 자주 내는 유형이면 평균 사용률은 괜찮아 보여도 nr_throttled가 빠르게 올라갈 수 있습니다. 이럴 때는 쿼터를 높일지, 아예 CPUWeight 중심으로 바꿀지 판단해야 합니다. 절대 제한은 비용 통제엔 좋지만, 지연 민감한 서비스에는 부작용이 더 크게 느껴질 수 있습니다.

    방법 B. cgroup v2 파일을 직접 다뤄보기

    직접 실험할 때는 위임 규칙을 먼저 확인해야 합니다. v2는 파일이 보인다고 다 쓸 수 있는 구조가 아닙니다. 부모 cgroup이 자식에게 컨트롤러를 위임하지 않으면 자식 쪽에 기대한 제어 파일이 나타나지 않거나, 설정이 먹지 않습니다.

    # 루트에서 사용 가능한 컨트롤러 확인
    cat /sys/fs/cgroup/cgroup.controllers
    
    # 하위 cgroup에 cpu, memory 컨트롤러 위임
    echo '+cpu +memory' | sudo tee /sys/fs/cgroup/cgroup.subtree_control
    
    # 테스트용 cgroup 생성
    sudo mkdir /sys/fs/cgroup/lab-demo
    
    # 메모리 압박 임계값과 절대 상한 설정
    echo 402653184 | sudo tee /sys/fs/cgroup/lab-demo/memory.high
    echo 536870912 | sudo tee /sys/fs/cgroup/lab-demo/memory.max
    
    # CPU 최대치 설정: 100ms 주기 중 50ms 사용
    echo '50000 100000' | sudo tee /sys/fs/cgroup/lab-demo/cpu.max
    
    # CPU 상대 우선순위 설정(기본 100, 범위 1~10000)
    echo 200 | sudo tee /sys/fs/cgroup/lab-demo/cpu.weight
    
    # 프로세스를 해당 cgroup에서 실행
    sudo bash -c 'echo $$ > /sys/fs/cgroup/lab-demo/cgroup.procs; exec stress-ng --vm 1 --vm-bytes 700M --cpu 2 --timeout 60s'

    이 방식은 이해에는 좋지만 운영에는 조심해야 합니다. 특히 systemd가 관리하는 호스트에서는 임의 디렉터리를 직접 다루는 방식보다 unit이나 scope로 묶는 편이 덜 위험합니다. 마지막 줄처럼 현재 셸을 직접 옮기는 패턴은 학습용으로는 괜찮아도 실서버에서는 실수 여지가 있습니다. 실제 서버에선 별도 서비스, 별도 scope, 별도 사용자 세션 단위로 분리하는 편이 재현성과 정리 측면에서 훨씬 편하더라고요.

    5. 컨테이너 리소스 제어에서 무엇을 읽어야 하나

    설정보다 더 중요한 건 관찰입니다. 제한을 넣는 건 10초면 끝나지만, 그 제한이 실제로 어떤 방식으로 시스템을 압박하는지 읽는 건 완전히 다른 일입니다. 기본적으로 볼 파일은 memory.current, memory.events, cpu.stat, io.stat이고, 필요하면 memory.pressure와 cpu.pressure까지 같이 봅니다.

    # 메모리 사용량과 이벤트
    cat /sys/fs/cgroup/lab-demo/memory.current
    cat /sys/fs/cgroup/lab-demo/memory.events
    
    # CPU 스로틀링 통계
    cat /sys/fs/cgroup/lab-demo/cpu.stat
    
    # I/O 통계
    cat /sys/fs/cgroup/lab-demo/io.stat
    
    # PSI(Pressure Stall Information) 확인
    cat /sys/fs/cgroup/lab-demo/memory.pressure
    cat /sys/fs/cgroup/lab-demo/cpu.pressure

    읽는 기준은 숫자 자체보다 상관관계입니다.

    • memory.events의 high가 늘면 메모리 압박이 반복되고 있다는 뜻입니다. 바로 장애는 아니어도 응답 시간 악화의 전조일 수 있습니다.
    • memory.events의 max가 늘면 상한에 계속 부딪히고 있다는 뜻입니다. 이 상태가 길어지면 결국 OOM 또는 강한 reclaim으로 이어질 수 있습니다.
    • oom, oom_kill 증가와 애플리케이션 비정상 종료 시점이 맞물리면 원인 범위가 꽤 좁혀집니다.
    • cpu.stat의 nr_throttled, throttled_usec가 빠르게 증가하면 CPU 제한이 실제 처리 지연으로 번질 가능성이 큽니다.
    • memory.pressure가 높고 memory.current는 상한 아래라면, 단순 상한 초과보다 reclaim 비용이 문제일 수 있습니다.
    • io.stat만 보고 끝내면 안 됩니다. I/O는 애플리케이션 레이턴시, 파일시스템 flush, 스토리지 백엔드 상태와 같이 봐야 의미가 생깁니다.

    여기서 많이 생기는 오해가 하나 있습니다. CPU throttling이 보인다고 무조건 나쁜 건 아닙니다. 배치 잡, 백그라운드 인덱싱, 테스트 워커처럼 원래 느려져도 되는 작업이라면 쿼터가 잘 먹고 있다는 뜻일 수 있습니다. 반대로 API 서버나 짧은 요청을 많이 받는 프론트 계층은 같은 throttling이 체감 지연으로 바로 번집니다. 결국 같은 지표라도 워크로드 성격에 따라 해석이 달라집니다.

    systemd-run으로 생성한 cgroup과 메트릭 확인 흐름

    systemd transient unit으로 프로세스를 띄우고 memory.events, cpu.stat, PSI를 확인하는 검증 흐름을 보여주는 구성도입니다.

    6. v1에서 v2로 넘어갈 때 자주 만나는 함정과 근본 원인

    이 부분이 진짜 운영 포인트입니다. 표면적으로는 “파일명이 바뀌었네” 정도로 보이지만, 실제 사고 원인은 더 아래층에 있습니다.

    1) 예전 파일명을 계속 찾는 문제

    현상은 간단합니다. memory.limit_in_bytes가 없고, 스크립트는 실패합니다. 근본 원인은 더 분명합니다. 자동화가 커널 인터페이스를 추상화하지 않고 파일명을 직접 하드코딩했기 때문입니다. 가능하면 내부 도구에 “v1/v2 판별 후 매핑” 레이어를 두는 편이 좋습니다. 운영 자동화는 언젠가 커널 세부 구현과 어긋나거든요.

    2) cgroup.subtree_control를 빼먹는 문제

    현상은 “파일은 있는데 자식에서 못 쓴다”, “생성한 cgroup에 기대한 제어 파일이 없다”입니다. 원인은 v2가 부모의 명시적 위임을 요구하기 때문입니다. v1 습관대로 디렉터리만 만들면 될 거라고 보면 거의 여기서 막힙니다.

    3) no internal process 규칙을 무시하는 문제

    v2는 자원 분배를 담당하는 부모 cgroup과 실제 워크로드가 섞이는 걸 경계합니다. 현상은 구조가 이상하게 꼬이거나, 하위 설계가 불편해지는 것이죠. 원인은 계층을 “정책 노드”와 “실행 노드”로 분리하지 않고 한군데에 몰아넣었기 때문입니다. systemd가 슬라이스와 스코프로 나눠 주는 이유도 여기와 맞닿아 있습니다.

    4) 런타임 드라이버 불일치

    Docker, containerd, kubelet, systemd 조합에서 꽤 자주 만나는 문제입니다. 제한은 건 것 같은데 예상한 경로가 아니라 다른 계층에 정책이 생기거나, 관찰 위치가 어긋납니다. 근본 원인은 cgroup driver와 서비스 관리자 계층이 서로 다른 모델을 가정하기 때문입니다. 이럴 때는 설정 파일 하나만 보지 말고, 실제 서비스 프로세스의 /proc/<pid>/cgroup과 systemd의 ControlGroup를 같이 봐야 합니다.

    5) 메모리 상한만 믿고 압박 제어를 안 하는 문제

    현상은 피크 순간 응답 시간이 급격히 나빠지거나, 갑작스러운 OOM으로 이어지는 것입니다. 원인은 메모리를 “최대치 하나”로만 관리했기 때문입니다. 서비스 성격에 따라 memory.high를 먼저 두고, 정말 넘기면 안 되는 한계만 memory.max로 닫는 구성이 더 안정적으로 먹히는 경우가 많습니다. 비용 통제와 안정성 사이에 완충 구간을 두는 셈이죠.

    7. 재현 가능한 트러블슈팅 시나리오 하나

    홈랩이나 테스트 서버에서 자주 재현하는 건 “제한은 정상인데, 체감 성능이 왜 이렇게 나쁘지?” 유형입니다. 이런 문제는 상한만 보는 습관으로는 잘 안 잡힙니다. 압박 이벤트와 스로틀링을 같이 봐야 감이 옵니다.

    1. MemoryHigh와 MemoryMax를 분리해서 가진 transient unit을 만듭니다.
    2. 그 안에서 메모리를 빠르게 쓰는 프로세스를 실행합니다.
    3. memory.current, memory.events, memory.pressure를 같이 봅니다.
    4. 동시에 journalctl과 애플리케이션 로그 시점을 맞춰 봅니다.
    sudo systemd-run --unit=mem-lab --scope \
      -p MemoryHigh=192M \
      -p MemoryMax=256M \
      bash -c 'stress-ng --vm 1 --vm-bytes 400M --timeout 30s'
    
    CG=$(systemctl show mem-lab.scope -p ControlGroup --value)
    
    cat /sys/fs/cgroup${CG}/memory.current
    cat /sys/fs/cgroup${CG}/memory.events
    cat /sys/fs/cgroup${CG}/memory.pressure
    journalctl -u mem-lab.scope --no-pager

    이 시나리오에서 볼 건 세 가지입니다. 첫째, high 이벤트가 먼저 늘어나는지. 둘째, 그다음에 max나 oom이 붙는지. 셋째, 같은 시점에 애플리케이션 레이턴시나 종료 로그가 어떻게 움직이는지입니다. 여기서 얻는 교훈은 꽤 분명합니다. 메모리 문제는 사용량 숫자 하나로 판단하면 늦습니다. 압박 이벤트가 먼저 오고, 그다음 증상이 사용자에게 보이는 경우가 많습니다.

    비슷한 방식으로 CPU도 재현할 수 있습니다. 쿼터를 낮춘 뒤 cpu.stat에서 nr_throttled가 늘어나는 속도와 애플리케이션 p95 지연을 같이 보면, “제한이 먹는다”와 “서비스가 느려진다”가 언제 연결되는지 보입니다. 실제 튜닝은 그 시점을 찾는 데서 출발하더라고요.

    8. cgroup v2 vs v1 선택 기준: 언제 v2로 가고, 언제 점진 전환할까

    실무 기준으로 말하면 신규 구축은 v2 우선입니다. 다만 “무조건 최신이 좋다”가 아니라, 어떤 운영 비용을 줄일 수 있느냐로 봐야 합니다. v2는 특히 systemd 중심 서버, 표준화된 컨테이너 운영, 내부 자동화 정비를 같이 할 수 있는 팀에 잘 맞습니다. 반대로 오래된 스크립트와 모니터링이 v1 경로에 깊게 묶여 있다면, 이행 비용을 먼저 계산해야 합니다.

    상황 추천 이유 판단 포인트
    신규 리눅스 서버 구축 cgroup v2 우선 통합 계층, systemd 친화성, 운영 단순화 자동화와 모니터링을 처음부터 v2 기준으로 맞출 수 있는지
    systemd 중심 운영 v2 강력 추천 unit 속성과 cgroup 파일 매핑이 자연스럽습니다. systemctl show와 커널 파일을 함께 읽는 절차를 표준화할 수 있는지
    레거시 스크립트가 v1 파일명에 깊게 의존 점진 전환 한 번에 바꾸면 장애 탐지 공백이 생길 수 있습니다. 파일명 매핑 레이어와 모니터링 수정이 끝났는지
    CPU 버스트가 중요한 지연 민감 서비스 v2 + 쿼터 신중 적용 절대 제한이 지연을 키울 수 있습니다. CPUQuota보다 CPUWeight가 더 맞는지
    메모리 피크가 잦은 서비스 v2 + memory.high 활용 갑작스러운 OOM보다 완충형 압박 제어에 유리합니다. 상한 하나가 아니라 압박 임계값과 함께 설계했는지
    장애 분석 중 파일이 안 보여 혼란스러운 상태 먼저 버전 확인 v1/v2 혼동이 원인인 경우가 많습니다. 실제 PID 경로와 서비스 매니저 경로가 일치하는지

    정리하면 선택 기준은 비교적 단순합니다.

    • 이럴 땐 A: 신규 서버, systemd 기본, 컨테이너 런타임 표준화가 가능하면 cgroup v2로 가는 편이 맞습니다.
    • 이럴 땐 B: 레거시 감시 스크립트와 운영 도구가 v1 경로에 박혀 있으면, 즉시 전환보다 매핑 정리 후 점진 이행이 안전합니다.
    • 이럴 땐 C: 비용 통제 목적의 배치성 워크로드는 CPUQuota/MemoryMax를 비교적 적극적으로 써도 됩니다.
    • 이럴 땐 D: 지연 민감 서비스는 CPUWeight + 완만한 메모리 압박 제어를 먼저 검토하고, 절대 상한은 마지막에 닫는 편이 낫습니다.
    cgroup v1과 v2 선택 기준 요약 인포그래픽

    신규 구축, 레거시 유지, systemd 운영, 지연 민감 서비스 등 상황별로 cgroup v1과 v2 선택 기준을 정리한 요약 이미지입니다.

    9. 저는 이렇게 권합니다

    cgroup v2 vs v1을 공부하다 보면 파일명과 옵션 암기에 빠지기 쉽습니다. 그런데 운영 관점에서는 그보다 어떤 워크로드에 어떤 제어 방식을 쓰느냐가 더 중요합니다. 새로 시작하는 환경이라면 cgroup v2를 기본값으로 잡는 편이 좋습니다. 그리고 메모리는 상한 하나만 보지 말고 memory.high와 이벤트 카운터까지 같이 보고, CPU는 무조건 쿼터부터 닫기보다 서비스 성격에 따라 CPUWeight와 병행해서 판단해 보세요.

    반대로 v1 기반 자동화가 이미 깊게 들어가 있다면, 무리하게 한 번에 바꾸는 건 추천하지 않습니다. 이 경우엔 순서가 있습니다. 먼저 버전 판별 로직을 넣고, 그다음 파일명 매핑을 정리하고, 마지막으로 모니터링과 런북을 v2 기준으로 갈아타는 편이 안전합니다. 마이그레이션 실패는 커널 기능 부족보다 운영 습관이 바뀌지 않은 상태에서 인터페이스만 바꾼 경우에 더 자주 나오더라고요.

    한 줄로 정리하면 이렇습니다. 레거시 제약이 없다면 v2, 레거시가 강하면 점진 전환입니다. 그리고 어떤 경우든 설정값 하나만 보지 말고 memory.events, cpu.stat, PSI, 서비스 로그를 같이 읽어야 실제 원인이 보입니다. 관련 글이 있다면 systemd resource-control 가이드나 컨테이너 메모리 튜닝 체크리스트도 이어서 읽어 두세요. 같이 보면 운영 감각이 훨씬 빨리 붙습니다.

  • [OpenStack] OpenStack Ceph 마이그레이션 결정 기준과 절차

    [OpenStack] OpenStack Ceph 마이그레이션 결정 기준과 절차

    OpenStack Ceph 마이그레이션 결정 기준과 절차

    OpenStack Ceph 마이그레이션을 검토할 때 저는 늘 같은 질문부터 던집니다. "지금 바꾸면 뭐가 좋아지고, 대신 무엇을 새로 운영해야 하나"라는 질문이죠. 핵심은 단순한 저장소 교체가 아니라 운영 모델의 변경입니다. Cinder 볼륨만 옮길지, Glance 이미지 저장소까지 묶을지, Nova의 에페메럴 디스크까지 Ceph RBD로 정리할지에 따라 난이도와 장애 반경이 완전히 달라지거든요. 실제 운영에서는 성능 수치보다 스케줄링 일관성, Ceph 재배치 부담, 인증 권한, 롤백 가능성이 더 자주 발목을 잡습니다.

    이 글은 OpenStack과 Ceph의 일반적인 운영 패턴을 기준으로 정리했습니다. 특정 벤더 확장 기능이나 과장된 수치 없이, 현장에서 일정 잡을 때 실제로 보는 판단 기준과 절차만 남겼습니다. 한 줄로 줄이면 이렇습니다. 기존 백엔드를 덮어쓰지 말고, 새 백엔드를 병행 등록한 뒤 신규 유입과 기존 자산 이동을 분리해서 진행하는 편이 안전합니다. 관련해서 볼륨 타입 설계나 Ceph 풀 설계 글도 함께 보시면 운영 그림이 훨씬 빨리 잡힙니다.

    OpenStack Ceph 마이그레이션 전체 아키텍처 다이어그램

    OpenStack 서비스 계층과 Ceph 풀(pool) 구조, 데이터 이동 흐름을 한눈에 보여주는 개요 이미지입니다.

    1. OpenStack Ceph 마이그레이션을 고민하는 대표적인 이유

    운영 현장에서는 보통 "Ceph가 좋아 보여서"가 아니라 아래 같은 이유로 움직입니다.

    • 풀 구조가 성장 패턴을 못 따라갈 때: 같은 풀에 서로 다른 I/O 성격의 볼륨이 섞이면 운영 판단이 흐려집니다.
    • 볼륨 타입 정책을 분리해야 할 때: 테넌트별, 워크로드별, 복제 정책별로 다른 백엔드가 필요해집니다.
    • 증설과 장애 대응의 표준화가 필요할 때: 하드웨어별 절차보다 Ceph 기준으로 운영 일관성을 맞추고 싶을 때가 많습니다.
    • 기존 백엔드가 OpenStack 스케줄러와 잘 안 맞을 때: 사용자는 타입을 골랐는데 실제 배치가 정책대로 안 되는 경우가 반복됩니다.

    제가 실무에서 특히 중요하게 보는 건 "왜 옮기느냐"보다 어디까지 옮기느냐입니다. Cinder만 옮기면 블록 스토리지 정책 문제로 한정되지만, Glance까지 함께 들어가면 이미지 캐시와 부팅 경로가 바뀝니다. Nova 에페메럴까지 손대는 순간부터는 스토리지 변경이라기보다 컴퓨트 설계 변경에 가깝습니다.

    2. OpenStack Ceph 마이그레이션 결정 기준은 성능보다 운영 책임입니다

    Ceph 도입 여부를 성능 하나로만 판단하면 나중에 꼬이기 쉽습니다. 더 정확한 질문은 이겁니다. "이 팀이 Ceph의 재배치, 권한, 풀 정책, 장애 메시지를 자기 책임으로 읽고 운영할 준비가 됐는가"예요. 이 기준이 생각보다 훨씬 현실적입니다.

    판단 항목 기존 유지가 더 나은 경우 Ceph 전환이 더 나은 경우 실무 체크 포인트
    운영 복잡도 현재 절차가 단순하고 장애 대응 경로가 고정돼 있음 확장, 복제, 장애 도메인 관리를 한 체계로 묶고 싶음 클라우드 팀이 Ceph 경고 메시지와 재배치 상태를 직접 해석할 수 있는지
    볼륨 타입 정책 사실상 단일 티어로도 충분함 테넌트/업무군별로 다른 풀과 정책이 필요함 volume_backend_name, extra_specs, 타입 명명 규칙을 먼저 설계했는지
    장애 허용 구조 외부 스토리지가 이미 안정적으로 이중화됨 CRUSH 규칙과 failure domain을 직접 설계할 가치가 있음 복제 단위가 host인지 rack인지, 기존 가용영역 설계와 충돌하지 않는지
    마이그레이션 방식 짧은 점검창도 확보하기 어려움 신규/기존을 분리하고 몇 주에 걸쳐 점진 전환 가능 붙어 있는(in-use) 볼륨의 정책 변경 허용 여부와 드라이버 지원 범위
    운영 비용 전담 인력이 없고 스토리지 운영을 단순화해야 함 학습 비용을 들여도 장기적으로 통합 운영 이득이 큼 모니터링, 알림, 장애 복구 문서가 Ceph 기준으로 이미 준비됐는지

    짧게 정리하면 이렇습니다. 볼륨 타입 분리와 점진 전환이 필요하면 Ceph 쪽이 유리하고, 운영 역량이 아직 약하면 기존 백엔드를 남겨둔 병행 운영이 맞습니다. 반대로 다운타임 제로만 바라보면서 한 번에 덮어쓰는 방식은 실무에서 꽤 위험하더라고요.

    3. OpenStack 스토리지 교체 범위를 자르면 난이도가 확 줄어듭니다

    OpenStack 스토리지 교체는 기술 문제라기보다 범위 관리 문제인 경우가 많습니다. 보통 아래 셋으로 끊습니다.

    1. Cinder 볼륨만 전환: 가장 현실적이고, 실패해도 영향 반경을 제한하기 쉽습니다.
    2. Glance 이미지까지 전환: 이미지 업로드, 캐시, 부팅 경로를 함께 봐야 합니다.
    3. Nova 에페메럴 디스크까지 전환: 성능과 일관성 이득은 크지만 컴퓨트 노드 영향이 급격히 커집니다.

    제가 추천하는 순서는 거의 같습니다. Cinder -> Glance -> Nova예요. 이유는 단순합니다. Cinder는 볼륨 타입이라는 절연층이 있지만, Nova 루트/에페메럴까지 한 번에 건드리면 인스턴스 부팅 실패가 곧바로 서비스 장애로 번지기 쉽습니다.

    실무에서 자주 보는 장면도 있습니다. 기존에는 외부 SAN을 쓰다가 "이번에 Ceph도 붙였으니 볼륨도 이미지도 같이 넘기자"고 범위를 넓히는 경우죠. 이때 Cinder 신규 볼륨은 정상 생성되는데, 부팅 지연이나 이미지 캐시 미스 때문에 운영팀이 Glance 문제를 Ceph 문제로 오해하는 일이 꽤 많습니다. 그래서 저는 첫 전환 주기에는 신규 볼륨 경로와 이미지 경로를 일부러 분리합니다. 원인 분리가 정말 편해집니다.

    4. OpenStack Ceph 마이그레이션 사전 점검

    이 단계는 체크리스트라기보다 진입 금지선에 가깝습니다. 상태가 좋지 않으면 일정부터 미루는 편이 맞습니다. 마이그레이션 자체보다, 진행 중 나타난 문제의 원인이 Ceph인지 OpenStack인지 분리하기 어려워지기 때문입니다.

    ceph -s
    ceph health detail
    ceph osd stat
    ceph osd tree
    ceph osd df tree
    ceph osd pool ls detail
    rados df
    openstack volume service list
    openstack volume type list --long
    openstack volume list --status in-use --long

    여기서는 보통 이렇게 읽습니다.

    • ceph -s: 클러스터 전체 상태와 recovery/backfill 진행 여부를 먼저 봅니다. 단순 HEALTH_WARN보다 경고 원인이 재배치인지 용량 임계치인지가 더 중요합니다.
    • ceph health detail: degraded, undersized, inactive, nearfull 계열 메시지가 보이면 이동 작업을 미룹니다.
    • ceph osd df tree: 특정 OSD만 유독 가득 찼다면 마이그레이션 중 backfill이 겹치면서 체감 성능이 흔들릴 가능성이 큽니다.
    • ceph osd pool ls detail: 새 풀의 복제 정책, CRUSH rule, autoscale 상태를 봅니다. 풀 이름만 맞고 규칙이 다르면 기대한 장애 도메인이 깨질 수 있습니다.
    • openstack volume service list: 대상 cinder-volume 서비스가 up이고 enabled인지 확인합니다.
    • openstack volume type list --long: 현재 타입과 백엔드 이름 연결이 살아 있는지 확인합니다.
    • openstack volume list --status in-use --long: 붙어 있는 볼륨 비중이 높다면 온라인 정책 변경이 가능한지부터 따져야 합니다.

    실제 실패 모드도 비슷합니다. 새 타입은 잘 만들었는데 Ceph 쪽에서 이미 backfill이 길게 돌고 있던 상황이죠. OpenStack API는 성공으로 보이는데 사용자는 I/O 지연을 바로 체감합니다. OpenStack의 성공 응답이 Ceph 내부 재배치 비용까지 끝났다는 뜻은 아니라는 점, 이 부분은 꼭 분리해서 봐야 합니다.

    OpenStack Ceph 마이그레이션 사전 점검과 볼륨 타입 매핑 이미지

    Ceph 클러스터 헬스 체크, 풀 상태, Cinder 볼륨 타입과 백엔드 이름 매핑 관계를 설명하는 이미지입니다.

    5. 백엔드 설계는 pool보다 volume type을 먼저 잡아야 합니다

    운영에서 더 오래 가는 설계는 "풀 이름 중심"이 아니라 볼륨 타입 중심입니다. 사용자는 타입을 고르고, Cinder 스케줄러는 타입의 extra_specs를 보고 백엔드를 고르거든요. 그래서 이름 규칙이 중요합니다.

    • 백엔드 섹션 이름: 운영자가 구분하기 쉬운 내부 이름
    • volume_backend_name: 타입과 연결되는 스케줄링 식별자
    • Ceph 풀 이름: 실제 데이터가 놓이는 물리적 대상

    많이 헷갈리는 지점이 하나 있습니다. [rbd-new] 같은 설정 섹션 이름과 volume_backend_name=rbd-new는 같은 개념이 아닙니다. 우연히 같게 맞출 수는 있지만, 스케줄러가 직접 참조하는 값은 섹션명이 아니라 백엔드 이름입니다. 여기 오타가 나면 타입은 멀쩡해 보여도 No valid host 오류가 나기 쉽습니다.

    5-1. OpenStack Ceph 마이그레이션용 cinder.conf 예시

    [DEFAULT]
    enabled_backends = rbd-old,rbd-new
    
    [backend_defaults]
    rbd_ceph_conf = /etc/ceph/ceph.conf
    rbd_user = cinder
    rbd_secret_uuid = 11111111-2222-3333-4444-555555555555
    volume_driver = cinder.volume.drivers.rbd.RBDDriver
    
    [rbd-old]
    volume_backend_name = rbd-old
    rbd_pool = volumes_old
    
    [rbd-new]
    volume_backend_name = rbd-new
    rbd_pool = volumes_new

    현재 Cinder 샘플 설정에서도 공통 백엔드 옵션은 [backend_defaults] 섹션을 사용합니다. 여기서 중요한 건 세 가지예요. enabled_backends에 백엔드 섹션이 모두 들어가 있는지, 각 섹션의 volume_backend_name이 타입의 extra_specs와 정확히 일치하는지, 공통 설정을 쓴다면 [backend_defaults]와 개별 섹션의 덮어쓰기 관계를 이해하고 있는지입니다.

    기존 단일 백엔드 서비스에 다중 백엔드를 붙이는 경우, Cinder 서비스 호스트명이 host에서 host@backend 형태로 바뀌는 환경도 있습니다. 이런 경우 기존 볼륨의 host 메타데이터를 갱신하지 않으면 운영이 꼬일 수 있어서, 사전에 cinder-manage volume update_host 대상 여부를 꼭 확인하는 편이 좋습니다.

    5-2. Ceph 풀과 인증 준비

    새 풀만 만들고 끝내면 안 됩니다. RBD로 쓸 풀은 초기화와 권한까지 맞아야 합니다. 저는 신규 백엔드를 붙이기 전에 아래 항목을 먼저 확인합니다.

    ceph osd pool ls detail
    rbd pool init volumes_new
    ceph auth get-or-create client.cinder \
      mon 'profile rbd' \
      osd 'profile rbd pool=volumes_new, profile rbd pool=vms, profile rbd-read-only pool=images' \
      mgr 'profile rbd pool=volumes_new, profile rbd pool=vms'
    ceph auth get-or-create client.glance \
      mon 'profile rbd' \
      osd 'profile rbd pool=images' \
      mgr 'profile rbd pool=images'

    근본 원인은 늘 비슷합니다. 풀은 만들었는데 rbd pool init이 빠졌거나, client.cinder가 새 풀에 대한 cap을 갖고 있지 않거나, Glance와 Cinder 권한을 뭉뚱그려 잡아두는 식이죠. 이런 상태에선 볼륨 생성은 되는데 attach에서 실패하거나, 이미지 기반 볼륨 생성만 유독 실패하는 식으로 증상이 갈라집니다.

    5-3. 새 볼륨 타입 생성

    openstack volume type create ceph-rbd-new
    openstack volume type set --property volume_backend_name=rbd-new ceph-rbd-new
    openstack volume type show ceph-rbd-new
    openstack volume type list --long

    운영상 의미는 명확합니다. 신규 생성 경로를 먼저 새 백엔드로 돌리고, 기존 자산 이동은 그다음에 따로 한다는 거예요. 이 분리가 되면 실패했을 때 되돌리기도 쉬워집니다.

    6. OpenStack Ceph 마이그레이션 절차: 신규 유입과 기존 자산을 분리하세요

    실무 절차는 의외로 단순합니다. 대신 순서를 어기면 비용이 바로 커집니다.

    1. 새 백엔드를 등록하고 타입을 만든다: 아직 기존 타입은 그대로 둡니다.
    2. 신규 볼륨만 새 타입으로 생성하게 유도한다: 새 데이터 유입을 먼저 분리합니다.
    3. 테스트 프로젝트에서 생성, attach, detach, 삭제까지 한 사이클을 검증한다: 생성만 확인하면 부족합니다.
    4. 저위험 볼륨부터 정책 변경 또는 마이그레이션을 시작한다: 백업성 볼륨, 비핵심 업무부터 갑니다.
    5. Ceph 재배치 상태를 보며 배치를 늘린다: API 성공률이 아니라 클러스터 안정성을 기준으로 속도를 조절합니다.
    6. 기존 타입의 신규 생성을 막는다: 잔여 볼륨만 남기고 정리 단계로 들어갑니다.

    CLI는 환경에 따라 조금씩 다르게 씁니다. 현재 openstackclient에서는 볼륨 타입 변경을 openstack volume set --type로 수행할 수 있고, 일부 운영 환경은 여전히 cinder retype를 사용합니다. 핵심은 명령 이름보다 정책 변경이 실제 데이터 이동을 유발하는지를 테스트 볼륨으로 먼저 확인하는 겁니다.

    # 현재 openstackclient 계열에서 많이 쓰는 방식
    openstack volume set \
      --type ceph-rbd-new \
      --migration-policy on-demand \
      8f1f2f0d-1111-2222-3333-444444444444
    openstack volume show 8f1f2f0d-1111-2222-3333-444444444444
    
    # 환경에 따라 여전히 사용하는 cinder client 방식
    cinder retype \
      --migration-policy on-demand \
      8f1f2f0d-1111-2222-3333-444444444444 \
      ceph-rbd-new

    관리자가 목적지를 더 강하게 통제해야 한다면 백엔드 호스트를 직접 지정하는 방식도 씁니다.

    openstack volume migrate \
      --host host01@rbd-new#volumes_new \
      8f1f2f0d-1111-2222-3333-444444444444
    openstack volume show 8f1f2f0d-1111-2222-3333-444444444444 -f yaml

    저는 보통 이렇게 고릅니다. 볼륨 타입 정책까지 함께 정리하려면 retype 계열이 낫고, 특정 백엔드 호스트나 풀을 명시적으로 통제해야 하면 migrate가 더 직접적입니다. 다만 in-use 볼륨의 재타입은 마이그레이션이 동반되면 보통 관리자 권한이 필요하고, 암호화된 in-use 볼륨은 재타입 마이그레이션이 지원되지 않는 경우가 있습니다. 이런 케이스는 무리해서 실시간 전환하지 말고 점검창을 잡는 편이 더 안전합니다.

    OpenStack Ceph 마이그레이션 절차와 데이터 이동 흐름 이미지

    신규 생성 분리, 테스트 볼륨 이동, 운영 볼륨 확장 전환 순서를 보여주는 절차형 이미지입니다.

    7. OpenStack Ceph 마이그레이션에서 자주 부딪히는 실패 모드

    증상은 비슷해 보여도 원인은 다릅니다. 저는 아래 네 가지부터 먼저 자릅니다.

    7-1. 타입은 맞는데 스케줄링이 안 됩니다

    대부분은 volume_backend_name 불일치, 백엔드 서비스 비활성화, 드라이버 초기화 실패 셋 중 하나입니다.

    grep -E 'enabled_backends|volume_backend_name|rbd_pool|rbd_user' /etc/cinder/cinder.conf
    openstack volume service list
    journalctl -u openstack-cinder-scheduler -n 200 --no-pager
    journalctl -u openstack-cinder-volume -n 200 --no-pager

    배포판에 따라 systemd 유닛명은 다를 수 있으니 서비스 이름은 현 환경 기준으로 바꿔서 보시면 됩니다. 근본 원인은 흔히 두 가지예요. 첫째, 타입은 rbd-new를 보는데 실제 백엔드는 rbd_new처럼 다른 이름으로 등록된 경우. 둘째, 드라이버가 Ceph 인증 실패로 초기화되지 않았는데 운영자가 프로세스만 살아 있다고 착각하는 경우입니다.

    7-2. Ceph 인증 문제로 생성 또는 attach가 막힙니다

    이 경우는 생성 단계와 attach 단계의 증상이 달라서 더 헷갈립니다. 새 풀에 대한 cap이 없거나, Nova와 Cinder, Glance가 참조하는 사용자명과 시크릿, 키링 경로가 어긋나 있는 경우가 많습니다. 특히 Cinder가 볼륨을 만들 수 있는 권한과 Nova/libvirt가 그 볼륨을 실제로 매핑할 수 있는 권한은 완전히 같은 문제가 아닐 수 있습니다.

    7-3. 이동은 됐는데 체감 성능이 흔들립니다

    이건 마이그레이션 명령의 성공 여부보다 Ceph 내부 상태를 먼저 봐야 합니다. recovery, backfill, nearfull 경고가 길게 이어지면 배치를 줄이거나 윈도우를 다시 잡는 편이 맞습니다. 현장에서 자주 보는 패턴은 이렇습니다. 낮 시간대에 작은 볼륨 몇 개는 멀쩡해서 속도를 올렸는데, 저녁 배치에서 대용량 볼륨과 Ceph 재배치가 겹치며 지연이 튀는 거죠. 이거 진짜 자주 나옵니다.

    7-4. 소스 풀에 고아 이미지가 남습니다

    실패한 이동, 중단된 작업, 관리자의 수동 정리 이력 때문에 원본 풀에 RBD 이미지가 남을 수 있습니다. 그렇다고 바로 지우면 안 됩니다. 먼저 OpenStack DB가 가리키는 위치와 실제 풀 위치가 일치하는지 확인해야 합니다. 저는 최소 하루 이상 운영 검증을 둔 뒤 정리합니다.

    8. 검증은 보이는지보다 원하는 위치에서 실제로 쓰는지 봐야 합니다

    마이그레이션 후 검증은 세 겹으로 합니다. OpenStack 상태, Ceph 실체, 워크로드 체감입니다.

    1. 볼륨 메타데이터 확인: 타입, 상태, migration 관련 필드 확인
    2. 대상 풀의 실제 RBD 이미지 확인: 새 풀에 이미지가 생겼는지 확인
    3. attach 후 읽기/쓰기 확인: 실제 인스턴스에서 I/O 검증
    4. 소스 풀 잔존물 확인: 즉시 삭제하지 말고 정리 후보만 분리
    openstack volume show 8f1f2f0d-1111-2222-3333-444444444444
    rbd ls volumes_new
    rbd info volumes_new/volume-8f1f2f0d-1111-2222-3333-444444444444
    rbd ls volumes_old

    저는 운영 확인을 여기서 끝내지 않습니다. 인스턴스에 붙여 파일 생성, 재마운트, 재부팅 후 재연결까지 확인합니다. 볼륨이 새 풀에 존재하는 것과, 서비스가 그 볼륨을 문제없이 소비하는 것은 다른 검증이거든요. 여기까지 봐야 마음이 놓입니다.

    OpenStack Ceph 마이그레이션 완료 검증 대시보드 이미지

    볼륨 상태 확인, 대상 풀의 RBD 이미지 존재 여부, 운영 검증 체크리스트를 보여주는 결과 이미지입니다.

    9. 언제 Ceph 스토리지 전환을 고르고, 언제 병행 운영을 고를까

    현장 추천은 꽤 분명합니다.

    • 다운타임이 거의 없고 운영 안정성이 최우선이다: 기존 백엔드를 유지한 병행 운영 후, 새 타입으로 신규를 먼저 분리하고 기존은 점진적으로 옮기세요.
    • 목표가 스토리지 통합 운영과 볼륨 정책 세분화다: Ceph 백엔드를 붙이되, 처음 범위는 Cinder로 제한하세요.
    • 팀이 Ceph 장애 메시지와 재배치를 아직 익숙하게 다루지 못한다: 바로 전면 전환하지 말고 신규 볼륨만 새 백엔드로 보내며 운영 감각부터 쌓는 편이 낫습니다.
    • 이미 Ceph를 쓰고 있고 풀 구조만 재정비하고 싶다: 풀 직접 교체보다 새 백엔드와 새 타입을 따로 만들고 정책 전환으로 정리하는 편이 안전합니다.
    • Nova 에페메럴까지 한 번에 바꾸고 싶다: 말리고 싶습니다. Cinder 경로가 안정화된 뒤 별도 프로젝트로 다루는 편이 훨씬 덜 아픕니다.

    여러 번 겪어보면 결국 덜 아픈 방법은 비슷합니다. 새 백엔드를 먼저 붙이고, 신규 생성 경로를 갈라놓고, 기존 볼륨은 업무 중요도와 Ceph 상태를 보면서 천천히 옮기는 방식이죠. 빠른 전환보다 되돌릴 수 있는 전환이 훨씬 값집니다.

    FAQ

    Q. retype와 migrate 중 무엇을 먼저 고려하면 될까요?

    볼륨 타입 정책 자체를 바꾸는 게 목적이면 retype 계열이 더 자연스럽습니다. 특정 목적지 호스트와 풀을 명시적으로 통제해야 하면 migrate가 더 직접적입니다. 다만 둘 다 실제 데이터 복사 여부는 현재 배치와 드라이버 동작에 좌우되니, 테스트 볼륨 검증부터 해보는 게 맞습니다.

    Q. 기존 백엔드는 언제 제거하는 게 좋을까요?

    신규 생성이 완전히 차단되고, 잔여 볼륨과 소스 풀의 정리 후보가 분리되고, 운영 검증 기간을 지난 뒤가 적절합니다. 저는 최소 한 템포 두고 소스 풀을 정리합니다.

    Q. Ceph가 항상 정답인가요?

    아닙니다. Ceph의 장점은 기능 자체보다 운영 일관성에 있습니다. 팀이 그 일관성을 감당할 준비가 안 돼 있다면, Ceph 도입은 기술 선택이 아니라 운영 부채가 될 수도 있습니다.

    OpenStack Ceph 마이그레이션 결정 기준 요약 이미지

    어떤 상황에서 병행 운영, 점진 전환, 즉시 전환 중 무엇을 고를지 요약한 마무리 인포그래픽입니다.

  • [OpenStack] OpenStack 컨트롤러 노드 장애 진단 및 복구 전략

    [OpenStack] OpenStack 컨트롤러 노드 장애 진단 및 복구 전략

    OpenStack 컨트롤러 노드 장애 진단 및 복구 전략

    OpenStack 컨트롤러 노드 장애는 겉으로 보이는 증상보다 실제 원인이 한 단계 뒤에 숨어 있는 경우가 많습니다. Horizon이 멈췄다고 해서 Horizon만의 문제는 아니고, API 500 에러가 난다고 해서 무조건 Keystone이나 Nova API부터 재시작할 일도 아니더라고요. 제가 현장에서 가장 자주 봤던 패턴은 VIP는 살아 있는데 백엔드가 죽어 있는 경우, API 프로세스는 active인데 MQ나 DB 때문에 요청이 끝까지 처리되지 않는 경우, 그리고 클러스터는 떠 있으나 쿼럼이 깨져 상태 변경이나 쓰기가 막히는 경우였습니다. 그래서 OpenStack 컨트롤러 노드 장애는 서비스 이름보다 의존성의 방향으로 봐야 훨씬 빨리 풀립니다.

    운영 중이면 마음이 급해서 전체 재시작부터 누르기 쉽습니다. 그런데 그 한 번이 원인 추적 단서를 지워버리고, Galera나 RabbitMQ처럼 합류 상태가 중요한 구성요소는 오히려 더 꼬이게 만들 때가 많거든요. 제 경험상 복구 속도를 가장 크게 좌우하는 건 기술 스택 지식의 양보다, VIP → API 엔드포인트 → 인증 → MQ → DB → 스케줄링 순서로 범위를 줄이는 습관이었습니다. 이번 글은 그 순서를 기준으로, 실제 운영자가 바로 복붙해 확인할 수 있는 명령과 함께 OpenStack 복구 판단 기준을 정리한 버전입니다.

    컨트롤러 노드의 API, 메시지 큐, 데이터베이스, 가상 IP 흐름을 한눈에 보여주는 개요 이미지입니다.

    왜 OpenStack 컨트롤러 노드 장애가 까다로운가

    컨트롤러 노드는 단일 프로세스 집합이 아니라, 서로 실패를 전염시키는 제어면 묶음에 가깝습니다. 그래서 증상과 원인이 자주 어긋납니다. 예를 들어 사용자는 “로그인이 안 된다”고 말하지만 실제론 VIP가 다른 노드로 넘어가지 못한 경우가 있고, 운영자는 “Nova API가 죽었다”고 판단했는데 실제론 RabbitMQ 인증 불일치 때문에 scheduler와 conductor가 요청을 소비하지 못하는 경우도 있습니다. 이 차이를 빨리 읽어내는 게 OpenStack 디버깅의 핵심이더라고요.

    • API 계층: Apache HTTPD 또는 uWSGI 앞단은 떠 있어도 뒤쪽 WSGI 애플리케이션이 DB 세션 고갈, TLS 오류, 엔드포인트 오설정으로 실패할 수 있습니다.
    • 메시지 계층: RabbitMQ는 프로세스가 active여도 vhost 권한, DNS, Erlang cookie, 네트워크 분리 문제로 실질적인 메시지 전달이 끊길 수 있습니다.
    • 데이터 계층: Galera는 mysqld 프로세스가 떠 있다는 사실보다 Primary 쿼럼과 Synced 상태가 중요합니다. 여기서 어긋나면 조회 일부는 가능해 보여도 쓰기나 상태 변경이 막힐 수 있습니다.
    • 가상 IP 계층: Keepalived, HAProxy, Pacemaker는 각각 정상처럼 보여도 헬스체크 기준, 리소스 제약, fail count 누적으로 트래픽이 없는 노드로 붙어버릴 수 있습니다.

    이 구조에서 흔한 실수는 로그를 서비스별로 따로 읽는 겁니다. 실제로는 요청의 흐름으로 묶어 읽어야 합니다. 토큰 발급 실패면 Keystone 로그만 보는 게 아니라, Keystone이 의존하는 MariaDB 연결 오류와 HAProxy 백엔드 상태를 같이 봐야 합니다. 반대로 인스턴스 생성 실패라면 Nova API 로그만 보는 순간 반쯤 틀립니다. scheduler, conductor, RabbitMQ 연결, compute 서비스 heartbeat까지 같이 봐야 맞습니다.

    OpenStack 컨트롤러 노드 장애 복구 전에 정할 원칙

    OpenStack 복구는 “무엇을 고칠까”보다 “무엇을 아직 건드리지 말까”를 먼저 정하는 편이 안전합니다. 저는 장애가 나면 아래 다섯 가지부터 메모합니다. 이 습관이 생각보다 진짜 편하더라고요.

    1. 영향 범위 먼저 고정: 로그인만 실패하는지, 새 VM 생성만 안 되는지, 기존 VM 네트워크까지 영향이 있는지 적어둡니다. 복구 중 증상이 바뀌면 그 변화도 기록합니다.
    2. 단일 장애점 후보를 분리: VIP 문제, 인증 문제, MQ 문제, DB 쿼럼 문제를 한 바구니에 넣지 않습니다. 동시에 여러 계층을 건드리면 원인과 결과가 섞입니다.
    3. 재시작은 마지막 수단: 상태 확인 없이 systemctl restart를 반복하면, 장애 전 최초 오류 시점과 후속 오류가 뒤섞여 로그 해석이 더 어려워집니다.
    4. 클러스터는 프로세스가 아니라 합의 상태를 본다: Galera는 Primary, RabbitMQ는 노드 합류와 큐 소비 여부, Pacemaker는 리소스 위치와 fail count가 핵심입니다.
    5. 복구 완료 기준을 기능으로 잡는다: 프로세스 active는 통과선이 아니라 시작선입니다. 최소한 토큰 발급, 서비스 목록 조회, 테스트 생성 요청까지 확인해야 진짜 정상입니다.

    특히 HA 환경에서는 “한 대는 살아 있다”가 별 의미가 없을 때가 많습니다. VIP가 다른 노드에 남아 있거나, HAProxy가 헬스체크 기준 때문에 백엔드를 모두 제외한 상태라면 사용자는 전체 장애로 봅니다. 그래서 저는 컨트롤러 장애에서 먼저 리더를 찾는 게 아니라 현재 트래픽이 어디로 흘러야 하는지부터 확인합니다.

    실전 1단계: OpenStack 컨트롤러 노드 장애 범위 빠르게 좁히기

    처음 5분은 세부 원인보다 장애의 모양을 분류하는 시간입니다. Horizon만 안 열리는지, CLI도 안 되는지, 토큰은 발급되는데 특정 리소스 조회만 실패하는지 확인하면 절반은 끝납니다. 제가 자주 쓰는 1차 점검 묶음은 아래입니다.

    source admin-openrc
    
    openstack token issue
    openstack endpoint list
    openstack service list
    openstack compute service list
    openstack network agent list
    openstack hypervisor list

    여기서 해석 기준이 중요합니다.

    • openstack token issue 실패: Keystone 자체, Keystone DB 연결, Keystone 뒤단 VIP/TLS 문제를 먼저 의심합니다.
    • token issue는 성공하지만 compute service list 실패: Nova API 또는 Nova DB/MQ 경로를 좁혀봅니다.
    • CLI 전체가 타임아웃: VIP, HAProxy, 방화벽, DNS, 인증서 체인부터 봅니다.
    • 조회는 되는데 생성만 실패: 대체로 MQ, scheduler, conductor, compute heartbeat 쪽이 더 유력합니다.

    그다음엔 각 컨트롤러에서 서비스 상태를 넓게 훑되, “실패한 유닛 목록”과 “최근 30분 로그”를 같이 봅니다. active만 보면 놓치는 경우가 많아서요.

    sudo systemctl --failed
    sudo systemctl status apache2 httpd haproxy keepalived mariadb rabbitmq-server
    sudo journalctl -u haproxy -u keepalived -u mariadb -u rabbitmq-server --since "30 minutes ago"
    sudo journalctl -u openstack-keystone -u openstack-nova-api -u openstack-nova-scheduler -u openstack-nova-conductor -u neutron-server -u httpd --since "30 minutes ago"

    배포판별 차이도 미리 염두에 두는 게 좋습니다. Debian/Ubuntu 계열은 apache2, RHEL 계열은 httpd를 많이 씁니다. 패키지 방식에 따라 Keystone 서비스명이 다를 수 있고, WSGI 배치라면 개별 API 데몬보다 웹서버 로그가 먼저 단서를 줄 때도 있습니다. 이런 차이를 모르고 “서비스가 없다”고 오판하는 경우를 꽤 봤습니다.

    systemctl 상태와 OpenStack CLI 점검 명령이 터미널에 표시된 운영자 화면

    초기 장애 범위를 줄이기 위해 systemd 상태와 OpenStack CLI를 함께 점검하는 장면을 보여주는 이미지입니다.

    실전 2단계: VIP, HAProxy, Keepalived 또는 Pacemaker 확인

    컨트롤러가 2대 이상이면 API 장애처럼 보이는 이슈의 상당수가 사실은 진짜 API 장애가 아닙니다. VIP가 안 붙었거나, HAProxy가 헬스체크 실패로 백엔드를 전부 제외했거나, Pacemaker가 fail count 때문에 리소스 이동을 멈춘 경우가 더 실무적입니다. 제 판단 기준은 간단합니다. 토큰 발급도 안 되고 CLI가 아예 timeout이면 가장 먼저 L4/L7 진입점부터 본다입니다.

    Keepalived/HAProxy 조합을 쓰는 경우

    ip addr show
    sudo systemctl status keepalived haproxy
    sudo journalctl -u keepalived -u haproxy --since "20 minutes ago"
    echo "show stat" | sudo socat stdio /run/haproxy/admin.sock
    curl -sk https://VIP:5000/v3/
    curl -sk https://VIP:8774/

    여기서 보는 포인트는 세 가지입니다. 첫째, ip addr show로 VIP가 현재 노드에 실제로 붙었는지 확인합니다. 둘째, HAProxy admin socket의 show stat에서 프론트엔드는 열려 있지만 백엔드가 모두 DOWN인지 봅니다. 셋째, 단순 TCP 연결이 아니라 실제 API 경로에 curl을 때려 HTTP 응답 코드를 확인합니다. 401이 오면 인증 전 단계까지는 도달한 것이고, 503이면 백엔드 헬스체크나 라우팅 쪽을 더 의심해야 합니다.

    실무적으로는 HAProxy 헬스체크 정의가 너무 공격적일 때도 문제가 됩니다. 예를 들어 백엔드 프로세스는 살아 있는데 TLS 체인 오류나 리다이렉트 때문에 헬스체크가 실패하면, 사용자는 전체 장애처럼 보게 됩니다. 이럴 땐 애플리케이션을 고치기 전에 헬스체크가 무엇을 기준으로 down 판정을 내리는지부터 봐야 합니다.

    Pacemaker/Corosync 조합을 쓰는 경우

    sudo pcs status
    sudo pcs resource status
    sudo crm_mon -1
    sudo pcs resource failcount show
    sudo journalctl -u pacemaker -u corosync --since "30 minutes ago"

    Pacemaker 환경은 서비스 자체보다 정책이 장애처럼 보일 때가 많습니다. 리소스가 stop 상태인지, 특정 노드에 묶여 있는지, migration threshold를 넘어 fail count가 누적돼 더 이상 움직이지 않는지 확인해야 합니다. 제 경험상 이 구간은 서비스 지식보다 Pacemaker 제약 해석 능력이 복구 시간을 더 크게 좌우합니다. 그래서 운영 인원이 적거나 교대 근무가 잦은 팀이라면, 기능적으로 더 유연하더라도 복잡한 리소스 제약은 줄이는 편이 낫습니다.

    실전 3단계: MariaDB/Galera와 RabbitMQ를 분리해서 본다

    API 500 에러, 요청 지연, 일부 기능 실패는 DB와 MQ가 비슷한 얼굴로 나타납니다. 그래서 둘을 한꺼번에 “백엔드 문제”라고 묶으면 복구가 느려집니다. 제 방식은 이렇습니다. 인증과 조회 중심이면 DB를 먼저, 생성과 비동기 후속 작업이면 MQ를 먼저 봅니다. 물론 둘 다 봐야 하지만, 시작점은 달라야 합니다.

    MariaDB/Galera 확인 포인트

    sudo mysql -e "SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size';"
    sudo mysql -e "SHOW GLOBAL STATUS LIKE 'wsrep_cluster_status';"
    sudo mysql -e "SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';"
    sudo mysql -e "SHOW GLOBAL STATUS LIKE 'wsrep_ready';"
    sudo mysql -e "SHOW VARIABLES LIKE 'read_only';"
    sudo mysql -e "SHOW DATABASES;"

    판단 기준은 숫자보다 상태 문자열입니다.

    • wsrep_cluster_status=Primary가 아니면 일단 정상 쿼럼이 아닙니다. 프로세스가 살아 있어도 트랜잭션 경로는 신뢰하기 어렵습니다.
    • wsrep_local_state_comment=Synced가 아니면 노드가 합류 중이거나 분리된 상태일 수 있습니다.
    • wsrep_ready=ON이 아니면 노드가 애플리케이션 질의를 정상 처리하지 못할 가능성이 큽니다.
    • read_only=ON이거나 애플리케이션 쓰기만 실패하면, 클러스터 분할 또는 보호 모드 전환을 의심해야 합니다.
    • SHOW DATABASES;는 단순 접속 확인용일 뿐이고, 실제 장애 판단은 wsrep 상태와 쓰기 가능 여부로 해야 합니다.

    Galera에서 특히 위험한 순간은 “한 노드는 살아 보이는데 어느 노드가 마지막 정상 상태였는지 확신이 없을 때”입니다. 이때 성급하게 부트스트랩하면 데이터 최신성이 더 중요한 노드를 버리고 오래된 상태를 기준으로 클러스터를 다시 세울 수 있습니다. 그래서 강제 복구는 서비스 복구 명령이 아니라 데이터 기준점 선정 작업으로 봐야 합니다. 이 구분이 없으면 장애는 끝났는데 데이터 정합성 이슈가 남습니다.

    RabbitMQ 확인 포인트

    sudo rabbitmqctl cluster_status
    sudo rabbitmqctl list_queues name messages consumers durable
    sudo rabbitmqctl list_connections pid peer_host peer_port state user vhost
    sudo rabbitmqctl list_users
    sudo rabbitmqctl list_permissions --vhost /
    sudo journalctl -u rabbitmq-server --since "30 minutes ago"

    RabbitMQ는 “프로세스가 떠 있다”보다 “OpenStack 서비스가 올바른 계정과 vhost로 실제 연결 중인가”가 핵심입니다. 제가 많이 본 실패 모드는 아래와 같습니다.

    • 비밀번호 변경 후 부분 반영: 일부 컨트롤러만 새 transport_url을 쓰고 나머지는 예전 값이라, 특정 서비스만 간헐적으로 실패합니다.
    • 권한 불일치: 사용자는 존재하지만 vhost permission이 맞지 않아 연결은 되는데 소비가 안 되거나 채널 생성이 실패합니다.
    • DNS/호스트명 문제: RabbitMQ 노드 간 이름 해석이 어긋나 클러스터 합류가 불안정합니다.
    • Erlang cookie 불일치: 노드 프로세스는 살아 있으나 클러스터 멤버십이 정상 구성되지 않습니다.

    list_queues에서 메시지가 계속 쌓이고 소비자가 0에 가깝다면, 그 큐를 읽어야 할 서비스가 죽었거나 인증 문제로 붙지 못하는 경우가 유력합니다. 반대로 연결 수는 많은데 처리량이 안 나오는 상황은 DB 지연, API worker 부족, backend timeout까지 함께 봐야 합니다. 즉 RabbitMQ가 범인인지, RabbitMQ가 병목을 드러내는 거울인지를 구분해야 합니다.

    Galera 상태와 RabbitMQ 클러스터 상태를 나란히 비교하는 운영 대시보드 스타일 이미지

    데이터베이스 쿼럼과 메시지 큐 상태를 함께 확인해야 하는 이유를 보여주는 비교 이미지입니다.

    재현 가능한 실제 시나리오: API는 살아 보이는데 인스턴스 생성이 실패할 때

    이 패턴은 문서보다 현장에서 더 자주 나옵니다. Horizon 로그인은 되고 openstack token issue도 성공합니다. openstack server list도 됩니다. 그런데 openstack server create만 실패하거나, 요청이 accepted로 보였다가 일정 시간이 지나 ERROR로 떨어집니다. 이럴 때 저는 Nova API를 바로 재시작하지 않고, 요청이 메시지 큐를 타고 scheduler와 conductor로 이어지는 경로를 따라갑니다.

    1. 사용자가 VM 생성 요청을 보냅니다.
    2. Keystone 인증은 통과합니다.
    3. Nova API는 요청을 받아 DB와 MQ에 후속 작업을 남깁니다.
    4. scheduler 또는 conductor가 MQ 연결 문제, 권한 문제, 이름 해석 문제로 작업을 소비하지 못합니다.
    5. 사용자 입장에서는 “API는 사는데 생성만 안 되는 이상한 장애”로 보입니다.

    이 시나리오에서 제가 먼저 보는 명령은 아래입니다.

    source admin-openrc
    openstack server create --flavor m1.small --image TEST_IMAGE --network TEST_NET diag-test-vm
    openstack server event list diag-test-vm
    sudo journalctl -u openstack-nova-api -u openstack-nova-scheduler -u openstack-nova-conductor --since "15 minutes ago"
    sudo grep -R "transport_url\|rabbit" /etc/nova/
    sudo rabbitmqctl list_users
    sudo rabbitmqctl list_permissions --vhost /

    여기서 핵심은 실패 메시지 자체보다 어느 단계에서 멈췄는지입니다. API가 요청을 접수했는데 scheduler 로그가 비어 있으면 MQ 전달 경로를 의심하고, scheduler는 받았는데 conductor가 후속 작업을 못 하면 DB 또는 내부 RPC를 더 봅니다. transport_url은 특히 꼼꼼히 대조해야 합니다. 사용자명, 비밀번호, 호스트, 포트, vhost 중 하나만 틀려도 장애가 “간헐적”으로 보일 수 있습니다. 여러 컨트롤러 노드에서 설정 파일 버전이 미묘하게 다른 경우가 실제론 꽤 많습니다.

    다만 환경에 따라 openstack server event list 조회가 정책이나 권한 설정의 영향을 받을 수 있으니, 이벤트 조회가 막혀 있으면 Nova API와 scheduler 로그를 우선 기준으로 잡는 편이 안전합니다.

    선택 기준: 어떤 컨트롤러 노드 HA 구성이 복구에 유리한가

    컨트롤러 노드 HA는 기술적으로 더 많은 기능을 가진 구성이 항상 더 낫지는 않습니다. 제가 운영 관점에서 따지는 기준은 세 가지입니다. 새벽에 누가 복구하나, 리소스 이동 정책을 팀이 실제로 이해하나, 장애 원인을 10분 안에 좁힐 수 있나입니다. 그 기준으로 보면 선택은 꽤 선명해집니다.

    구성 방식 복구에 유리한 점 위험한 실패 모드 이럴 때 추천
    HAProxy + Keepalived 구조가 단순해 VIP와 백엔드 상태를 빠르게 추적하기 쉽습니다. 애플리케이션 리소스 자체의 정교한 제약 제어는 약합니다. 헬스체크 정의가 부정확하면 정상 서비스도 제외될 수 있습니다. 소규모 팀, 홈랩, 운영 인수인계가 잦은 환경
    Pacemaker + Corosync + HAProxy 리소스 이동 정책, 우선순위, 장애 전환 조건을 세밀하게 통제할 수 있습니다. fail count, constraint, stickiness 해석이 어렵습니다. 서비스는 정상인데 정책 때문에 복구가 지연될 수 있습니다. 엄격한 HA 정책이 필요하고, 클러스터 운영 숙련도가 팀 내에 확보된 환경
    단일 컨트롤러 + 정기 백업 원인 추적이 가장 단순하고 구성 관리 비용이 낮습니다. 컨트롤러 장애 시 제어면 중단을 감수해야 합니다. 복구 시간은 백업/복원 절차 숙련도에 좌우됩니다. 학습용, 비핵심 서비스, 짧은 중단을 허용할 수 있는 환경

    제 추천은 명확합니다. 운영 인원이 적고 장애 대응 절차가 아직 정교하지 않다면 단순한 HA 구성이 더 낫습니다. 반대로 리소스 제어 정책이 서비스 품질에 직접 연결되고, 팀이 Pacemaker 상태 해석에 익숙하다면 그때 복잡한 구성이 제값을 합니다. 기능이 아니라 복구 가능성을 기준으로 고르셔야 합니다.

    ⚠️ OpenStack 컨트롤러 노드 장애에서 자주 만나는 트러블슈팅 포인트

    • 시간 동기화 문제: 토큰 유효성 오류, TLS 검증 문제, 클러스터 노드 간 이상 동작이 전부 시간 어긋남에서 시작될 수 있습니다. timedatectl, chronyc sources -v, chronyc tracking을 같이 보세요.
    • 이름 해석 문제: VIP, 컨트롤러 FQDN, RabbitMQ 노드명 중 하나라도 서로 다르게 풀리면 MQ 클러스터, API endpoint, 인증서 검증이 동시에 흔들립니다.
    • TLS 인증서 만료 또는 CN/SAN 불일치: 프로세스는 모두 active인데 HTTPS 호출만 실패하는 전형적인 패턴입니다. 단순 ping보다 실제 핸드셰이크 확인이 낫습니다.
    • 부분 재시작의 함정: DB/MQ가 불안정한 상태에서 API만 재시작하면 재시도 로그만 늘고, 최초 오류 원인이 뒤로 묻힙니다.
    • 쿼럼 없는 강제 복구: Galera 부트스트랩은 마지막 정상 노드 기준이 불명확하면 데이터 정합성 리스크가 생깁니다.

    이 항목들은 사소해 보여도 현장에선 매우 자주 원인입니다. 특히 이름 해석과 시간 동기화는 “인프라는 멀쩡해 보이는데 왜 OpenStack만 이상하지?” 같은 상황을 만들어서 더 헷갈립니다. 그래서 저는 컨트롤러 장애 초기에 아래 묶음을 자주 함께 확인합니다.

    timedatectl
    chronyc sources -v
    chronyc tracking
    getent hosts controller-1 controller-2 controller-3
    getent hosts VIP_FQDN
    hostname -f
    openssl s_client -connect VIP:5000 -servername VIP_FQDN </dev/null

    여기서 인증서 출력은 길지만, 현장에선 세 가지만 봐도 충분할 때가 많습니다. 인증서 만료 여부, 요청한 서버 이름과 SAN 일치 여부, 체인 검증이 중간에서 끊기는지입니다. 애플리케이션 로그에서 SSL 오류만 보고 끝내면, 실제론 LB 앞단 인증서 배치 문제를 놓칠 수 있습니다.

    검증: 복구 후 무엇을 확인해야 하나

    복구는 서비스 프로세스가 올라온 시점이 아니라, 운영자가 다시 제어권을 회복한 시점에 끝납니다. 그래서 저는 확인 순서를 일부러 기능 기준으로 짭니다. 인증, 조회, 생성, HA 상태 재검증 순서입니다.

    1. openstack token issue 성공 여부
    2. openstack service list와 주요 에이전트 조회
    3. 네트워크, 이미지, 플레이버 목록 조회
    4. 테스트 포트 또는 테스트 인스턴스 생성 요청
    5. 기존 인스턴스의 콘솔, 볼륨, 네트워크 연결 상태 점검
    6. VIP, HAProxy 백엔드, Galera, RabbitMQ 클러스터 상태 재확인
    source admin-openrc
    
    openstack token issue
    openstack network list
    openstack image list
    openstack flavor list
    openstack server list
    openstack hypervisor list
    openstack compute service list
    openstack network agent list

    판단은 이렇게 하시면 됩니다. 인증은 되는데 특정 목록만 실패하면 그 서비스 API 또는 그 뒤단 백엔드가 아직 덜 복구된 겁니다. 조회는 되는데 생성이 안 되면 MQ, scheduler, compute 연동을 더 봐야 합니다. 그리고 정말 중요한 부분이 하나 더 있습니다. 복구 직후 5분만 보는 건 부족합니다. 큐 적체가 늦게 풀리거나, Galera가 재합류 후 상태가 흔들리는 경우가 있어서 최소한 짧은 관찰 구간은 두는 편이 안전합니다.

    복구 완료 후 OpenStack 서비스 목록과 정상 상태 체크리스트를 보여주는 검증 화면

    복구 후 단순 프로세스 상태가 아니라 실제 OpenStack 기능이 정상인지 검증하는 장면을 표현한 이미지입니다.

    정리 겸 FAQ: 현장에서 바로 결정하는 기준

    Q. OpenStack 컨트롤러 노드 장애가 났을 때 가장 먼저 뭘 봐야 하나요?

    A. 사용자 영향 범위와 VIP 진입점입니다. CLI와 Horizon이 같이 죽었다면 네트워크, VIP, HAProxy부터 보는 게 빠릅니다. 반대로 로그인은 되는데 특정 기능만 실패하면, 그 API 뒤의 MQ나 DB 의존성을 먼저 좁히는 편이 맞습니다.

    Q. 모든 서비스를 재시작하면 빨리 해결되지 않나요?

    A. 대체로 아닙니다. 특히 Galera와 RabbitMQ는 “살아났다”보다 “정상 합류했다”가 중요합니다. 합류 상태 확인 없이 재시작을 반복하면 원인도 흐려지고 복구 범위도 넓어질 수 있습니다.

    Q. 어떤 구조를 추천하나요?

    A. 결론은 분명합니다. 운영 난이도를 낮춰야 하는 환경이면 HAProxy + Keepalived가 더 낫고, 정교한 리소스 제어와 명확한 운영 절차가 이미 있는 팀이면 Pacemaker 기반 HA가 맞습니다. 홈랩이나 비핵심 서비스라면 단일 컨트롤러와 검증된 백업 절차가 오히려 더 현실적일 수 있습니다.

    현장에서 제일 빨리 통하는 한 문장을 남기면 이겁니다. OpenStack 컨트롤러 노드 장애는 서비스 이름 순서가 아니라 의존성 순서로 봐야 빨리 끝납니다. CLI 전체가 죽었으면 VIP와 LB부터, 인증만 안 되면 Keystone과 DB부터, 생성만 안 되면 Nova와 RabbitMQ부터 보세요. 그리고 운영 인원이 적다면 복잡한 HA보다 설명 가능한 HA가 더 안전합니다. 밤 2시에 문서 없이도 복구할 수 있는 구조가 결국 가장 좋은 구조더라고요.

    비슷한 운영 글을 계속 쓰신다면 Keystone 장애, RabbitMQ 권한 오류, Galera 쿼럼 복구 절차를 각각 내부 링크로 분리해 두는 것도 추천드립니다. 이런 묶음 글이 검색 유입도 잘 받고, 나중에 팀 온보딩할 때도 꽤 유용합니다.

    단순 구성과 고급 HA 구성의 선택 기준을 요약한 인포그래픽

    운영 규모에 따라 어떤 복구 전략과 HA 구성을 선택하면 좋은지 요약한 마무리 이미지입니다.