홈랩에 서비스를 하나씩 올리다 보니 어느 순간부터 이런 일이 생겼습니다. “그 앱을 띄워 두긴 했는데… 주소가 뭐였더라?” Kubernetes에 있는 것도 있고, Proxmox의 별도 VM이나 Docker로 실행하는 것도 있어서 기억만으로 관리하기가 어려워졌습니다.
Homarr를 정리하다가 필요한 기능을 하나씩 적어 봤습니다. 직접 추가한 링크와 Kubernetes의 Ingress를 함께 보여 주고, NodePort는 접속할 노드만 선택하면 주소를 만들어 주면 좋겠더군요. 같은 서비스에 도메인이 여러 개 있어도 버튼은 하나였으면 했습니다.
그래서 KubeShelf를 만들었습니다. Kubernetes에서 발견한 서비스와 수동 링크를 카드로 모아 두고, 제가 쓰는 GitOps 흐름에 맞춰 설정을 관리하는 대시보드입니다. 이 글은 10월 9일에 구현·배포한 0.1.12까지의 기능을 기준으로 정리했습니다.
Kubernetes가 알고 있는 주소를 카드로 모으기
처음 요구사항은 복잡하지 않았습니다. 클러스터에 이미 들어 있는 접속 정보를 다시 수동으로 입력하는 일을 줄이고 싶었습니다. Go 서버가 Kubernetes API에서 Ingress, Service, Pod, EndpointSlice와 노드 정보를 읽고, React 화면에 서비스 카드를 보여 주도록 구성했습니다.
| 구분 | 주소를 가져오는 방법 |
|---|---|
| Ingress | host·path와 연결된 Service·포트로 접속 주소 생성 |
| NodePort | 노드 또는 등록한 도메인과 Service의 NodePort 조합 |
| 직접 등록 | Kubernetes 밖의 VM·Docker 서비스나 외부 링크 입력 |
자동 발견한 카드도 표시 이름, 아이콘, 설명과 주소를 수정할 수 있습니다. 새 서비스는 발견함에 NEW로 표시하고, 확인한 서비스에 주소나 라우팅 정보가 바뀌면 CHANGED로 다시 검토할 수 있게 했습니다. Pod가 재시작됐다는 이유만으로 매번 새 서비스가 되지는 않습니다.
![[Homelab] KubeShelf 개발기: Kubernetes 서비스를 자동으로 모으는 대시보드 [Homelab] KubeShelf 개발기: Kubernetes 서비스를 자동으로 모으는 대시보드](https://cola16.app/wp-content/uploads/2026/10/kubeshelf-demo-dashboard-20261009-1088x1024.png)
서비스가 없는 네임스페이스는 목록에서 자동으로 빠집니다. 나중에 Ingress나 NodePort가 생기면 다시 나타나며, 기존에 지정한 공개 범위와 확인 상태는 유지합니다. 이 앱에서 자동 발견하는 대상은 Ingress와 NodePort이므로, ClusterIP만 있는 내부 서비스까지 모두 접속 카드로 만들지는 않습니다.
도메인이 여러 개여도 서비스 카드는 하나
Nextcloud처럼 같은 서비스로 들어가는 도메인이 여러 개 있으면, 주소마다 카드를 만드는 것만으로도 화면이 금방 복잡해집니다. 그래서 같은 네임스페이스의 같은 Service와 포트를 가리키는 Ingress 주소는 한 카드로 모았습니다. 카드 안에서 접속 주소를 선택하는 방식입니다.
Nextcloud 카드 ├─ files.example.com └─ cloud.example.com NodePort 섹션의 접속 대상: worker-01 또는 node.example.com ├─ 관리 도구 → 선택한 주소:30080 └─ 다른 앱 → 선택한 주소:30443
백엔드 Service는 다르지만 같은 앱으로 묶고 싶은 경우에는 관리자가 직접 그룹을 지정할 수 있습니다. 여러 주소를 한 카드에 모아도 각 주소에 붙어 있던 공개 범위는 유지하도록 했습니다.
NodePort에는 추가 조건이 있었습니다. externalTrafficPolicy: Local인 서비스는 준비된 로컬 백엔드가 없는 노드로 접속하면 트래픽을 전달하지 못할 수 있습니다. KubeShelf는 Ready 상태의 Pod 기반 Endpoint가 있는 노드만 해당 서비스의 접속 후보로 사용합니다. 선택한 공통 노드가 맞지 않으면 마지막으로 사용한 적합한 대상을 유지하고, 가능한 대상 자체가 없으면 접속 불가로 표시합니다. Kubernetes의 트래픽 정책 설명에 이 조건이 정리돼 있습니다.
직접 등록한 도메인은 브라우저가 DNS로 해석합니다. KubeShelf가 DNS를 바꾸거나 해당 도메인의 목적지를 임의로 추측하지는 않습니다. Local 서비스에 사용할 도메인은 어느 노드에 연결되는지도 지정해야 준비된 백엔드가 있는지 판단할 수 있습니다. Redis처럼 웹 서비스가 아닌 포트는 브라우저 열기 대신 주소 복사를 제공합니다.
상태 표시는 Running보다 Ready를 기준으로
카드마다 별도의 HTTP 헬스체크를 돌리는 기능은 만들지 않았습니다. Kubernetes가 이미 가지고 있는 Pod Ready와 EndpointSlice 정보를 활용했습니다. 컨테이너가 Running이어도 요청을 받을 준비가 안 된 경우가 있으므로, 실행 여부보다 준비 상태를 보자는 선택이었습니다.
| 표시할 상태 | 판단 기준 |
|---|---|
| 준비됨 | 연결된 Pod와 Endpoint의 준비 상태 확인 |
| 일부 준비됨 | 준비된 백엔드와 준비되지 않은 백엔드가 함께 있음 |
| 준비된 Pod 없음 | Pod 기반 서비스에 준비된 백엔드가 없음 |
| 상태 미확인 | 수동 링크·외부 백엔드 또는 클러스터 정보 확인 불가 |
EndpointSlice는 Service 뒤의 Endpoint와 준비 상태를 제공하며, Pod 기반 Endpoint의 상태는 Pod Ready와 연결됩니다. EndpointSlice의 상태 조건을 참고했습니다. 다만 카드가 초록색이라는 것은 Kubernetes에서 백엔드가 준비됐다는 의미입니다. 브라우저에서 방화벽을 통과할 수 있는지, CDN이나 앱의 로그인까지 정상인지 보장하는 표시는 아닙니다.
자동 발견한 서비스의 공개 범위 정하기
자동 발견은 편하지만, 새로 생긴 관리 도구까지 모든 방문자에게 보여 주고 싶지는 않았습니다. 그래서 아직 정책을 정하지 않은 네임스페이스와 새 수동 링크는 관리자에게만 표시하도록 했습니다. 관리자가 목록을 확인한 뒤 공개 범위를 정하는 순서입니다.
네임스페이스에 기본 정책을 지정하고, 필요한 서비스나 주소에는 예외를 줄 수 있습니다. 누구나 보기, 지정한 그룹·사용자에게 표시, 관리자만 보기로 나눴습니다. 사용자는 authentik의 OIDC로 로그인하고, 그룹 정보는 authentik 디렉터리에서 확인합니다. 관리자 식별에는 바뀔 수 있는 표시 이름 대신 안정적인 사용자 식별자를 사용했습니다.
특히 Ingress와 NodePort의 기본 권한은 따로 관리합니다. 웹 주소를 공개한 네임스페이스라고 해서 노드 주소와 관리용 포트까지 공개되는 것은 원하지 않았습니다. NodePort 정책이 없으면 관리자 전용으로 처리합니다.
허용되지 않은 카드·주소·노드 정보는 서버에서 걸러낸 뒤 API로 전달합니다. 화면에서 카드만 감추고 원본 목록은 그대로 내려주는 방식으로 처리하지 않았습니다. 또한 “숨기기”는 화면 정리 기능이고, 공개 범위 설정은 권한 기능이라 각각 따로 두었습니다.
이 권한은 KubeShelf에서 볼 수 있는 목록과 주소를 제어합니다. 해당 주소로 직접 접속하는 것까지 KubeShelf가 막아 주는 구조는 아니므로, 목적지 앱의 인증과 네트워크 접근 정책은 그대로 필요합니다.
화면에서 저장해도 설정 원본은 Git에
설정 저장은 평소 쓰는 GitOps 운영 방식에 맞췄습니다. 표시 이름이나 공개 범위를 화면에서 바꾸면, 앱이 비공개 Gitea 저장소의 ConfigMap 매니페스트를 수정해 즉시 commit·push합니다. 그 뒤 Fleet이 적용하고, Pod에 마운트된 설정 파일이 갱신되면 앱이 다시 읽습니다.
화면에서 설정 변경 → 검증 → Git commit·push → Fleet이 ConfigMap 적용 → Pod의 마운트 파일 갱신 → KubeShelf가 새 revision을 읽음 → 적용 완료 표시
여기서 Git에 저장된 상태와 실행 중인 설정에 적용된 상태를 구분했습니다. push가 성공했어도 Fleet과 kubelet의 파일 갱신이 끝나기 전에는 “설정 적용 중”으로 표시합니다. 실제 파일의 revision을 읽어야 적용 완료가 됩니다. 공유 설정과 공개 범위 판단도 이 적용본을 기준으로 합니다.
설정 저장 중에는 중복 저장을 막고, 실패하면 편집 내용을 유지해 재시도할 수 있게 했습니다. 같은 파일을 다른 곳에서 수정한 경우에는 충돌을 알리고, force push로 덮어쓰지 않도록 했습니다.
Fleet의 Git 확인 주기는 15초로 설정했습니다. 하지만 15초 안에 전체 적용이 끝난다는 뜻은 아닙니다. Kubernetes의 ConfigMap 갱신 문서에서 설명하듯, 볼륨에 투영되는 시점에는 kubelet의 동기화와 캐시 전파 시간도 영향을 줍니다. subPath 마운트는 이런 갱신을 받지 못하므로 디렉터리 전체를 마운트했습니다.
첫 운영 검증에서는 Git push부터 앱이 새 설정을 읽기까지 약 27초가 걸렸고, Pod 재기동은 없었습니다. 이 값은 당시 한 번의 측정 결과이며 모든 저장의 고정 시간은 아닙니다.
즐겨찾기와 기본 주소도 계정별로 보존
서비스 목록을 만들고 나니 다음에는 “지금 필요한 것만 보고 싶다”는 요구가 생겼습니다. 관리자는 일상·관리·모니터링 모음으로 시작하고, 한 서비스를 여러 모음에 넣거나 처음 열릴 모음을 정할 수 있게 했습니다. 일반 사용자는 하나의 즐겨찾기 목록을 사용합니다.
로그인한 사용자의 즐겨찾기와 기본 접속 주소는 계정별 Git 파일과 ConfigMap으로 저장합니다. 휴대폰과 PC가 같은 계정을 쓰면 같은 설정을 이어 쓸 수 있고, 사용자마다 선택한 도메인이나 NodePort 대상도 따로 보존합니다. 선택을 바꿀 때마다 저장하지 않고, 하단 저장 버튼으로 변경을 모아서 push하도록 했습니다.
기존 브라우저에 있던 즐겨찾기 15개를 이 구조로 옮겼고, 재시작 후에도 유지되는 것을 확인했습니다. 이전이 끝난 뒤에는 일회성 가져오기 코드를 제거했습니다. 비로그인 사용자의 선택은 그 브라우저에만 보관합니다.
설정 원본이 Git에 있으므로 KubeShelf 자체를 위한 DB나 PV는 필요하지 않습니다. Git 작업 디렉터리는 emptyDir를 사용합니다. 0.1.12의 배포는 단일 replica와 Recreate 방식이며, 로그인 세션은 메모리에 보관하므로 재시작 후에는 다시 로그인해야 합니다. 설정 데이터는 Git에 남습니다.
공개 코드와 운영 설정을 나눠 배포
앱 코드와 재사용 가능한 Helm 차트는 GitHub의 KubeShelf 저장소에 공개했습니다. 컨테이너는 GHCR로 배포하고, Helm 저장소는 GitHub Pages에서 제공합니다. 집의 AMD64 노드와 OCI의 ARM64 노드를 함께 쓰기 위해 두 아키텍처의 이미지를 빌드하도록 구성했습니다.
| 위치 | 관리하는 내용 |
|---|---|
| 공개 GitHub | Go·React 소스, 테스트, 컨테이너 빌드, 범용 Helm 차트 |
| GHCR·GitHub Pages | 컨테이너 이미지와 버전별 차트 패키지 |
| 비공개 배포 저장소 | 실제 values, 인증 정보, Fleet 등록과 권한 |
| 비공개 런타임 저장소 | 공유 설정과 사용자별 ConfigMap 매니페스트 |
실제 도메인, 사용자 식별자와 인증 정보는 비공개 운영 설정에 두었습니다. 설정을 쓸 수 있는 앱이 일반 배포 매니페스트까지 마음대로 바꿀 필요도 없으므로, 런타임 저장소의 배포 권한은 설정 ConfigMap 범위로 제한했습니다. Kubernetes 서비스 발견용 API 권한도 읽기 전용입니다.
Helm으로 설치할 때는 공개 차트에 자신의 values를 결합하면 됩니다. 아래는 0.1.12 설치 형식입니다. 운영용 values 외에 런타임 ConfigMap, Git 접근 정보와 OIDC·디렉터리 인증 설정도 준비해야 합니다. 구체적인 준비 항목은 0.1.12 운영 안내에 정리했습니다.
helm repo add kubeshelf https://colah16.github.io/KubeShelf
helm repo update
helm upgrade --install kubeshelf kubeshelf/kubeshelf \
--version 0.1.12 \
--namespace public-services --create-namespace \
--values private-values.yaml첫 배포를 마치고
첫 버전은 OCI 노드에서 실행했고, 브라우저·권한·저장 흐름을 검증했습니다. 이후 공개 범위 설정을 Ingress와 NodePort로 분리하고, 모바일 화면과 권한 상속 표시, 숨김·복원, 계정별 즐겨찾기를 다듬어 10월 9일 저녁에 0.1.12 배포까지 마쳤습니다.
익명 사용자에게 NodePort와 노드 주소가 노출되지 않는지, 저장이 실패했을 때 선택값이 남는지, 즐겨찾기가 재시작 후에도 보존되는지도 확인했습니다. Git과 실제 앱의 설정이 이어지는 과정까지 검증한 뒤에야 평소 쓰는 서비스 입구로 사용할 수 있었습니다.
처음에는 “링크 버튼 몇 개 있으면 되겠지”에서 시작했습니다. 그런데 직접 써 보니 같은 앱의 여러 도메인, NodePort의 접속 노드, 사용자별 표시 범위와 즐겨찾기까지 연결돼 있더군요. KubeShelf에는 그렇게 실제로 필요했던 기능들을 하나씩 담았습니다. 이제 서비스를 늘려 놓고 주소를 다시 찾는 일이 조금 줄었습니다. ㅎㅎ
작업 기준일: 2026년 10월 9일. 0.1.12 배포까지의 구현·검증 기록을 바탕으로 정리했습니다. 본문 이미지는 예시 데이터로 구성한 초기 데모 화면입니다.

![[Homelab] KubeShelf 개발기: Kubernetes 서비스를 자동으로 모으는 대시보드 [Homelab] KubeShelf 개발기: Kubernetes 서비스를 자동으로 모으는 대시보드](https://cola16.app/wp-content/uploads/2026/10/kubeshelf-demo-dashboard-20261009.png)
답글 남기기