손호영
Production2026.07 – 현재1인 개발·운영

Noraneko Platform

개인 프로젝트의 빌드·배포·상태 확인을 한곳에서 관리하려고 만든 운영 플랫폼입니다. oshi@log와 RailNetwork-JP는 이 플랫폼에서 빌드·배포하고, 한국 RailNetwork는 기존 배포 경로를 유지한 채 상태만 확인합니다.

4
운영 호스트
5.4k
Go LOC·테스트 포함
23
경보 규칙
13
대시보드
2.6k줄
Ansible
23
플랫폼 컨테이너
Noraneko Platform 아키텍처 다이어그램
시스템 아키텍처 — 클릭하면 원본으로 열립니다

무엇을 했나

01

설계 결정

현재 규모에서 고른 배포 도구와 이유입니다.

Kubernetes를 쓰지 않은 이유

VM 4대와 소수의 서비스에는 Kubernetes 운영 비용이 더 크다고 판단했습니다. 필요한 기능은 반복 가능한 배포, 이전 digest로 롤백, 서비스 추가 시 관측 설정 생성이었습니다. JSON Schema로 검사한 서비스 YAML, platformctl, digest 고정, Portainer stack update로 현재 요구를 처리하고 있습니다.

직접 만든 배포 경로를 없앤 이유

초기에는 Ansible SSH 배포, Telegram 승인, Jenkins 입력을 함께 사용했습니다. 배포 진입점이 여러 개라 현재 배포 상태를 확인하기 어려웠고, 유지할 코드도 늘었습니다. 이 경로들을 제거하고 배포·롤백을 Portainer stack update로 통일했습니다. 등록 서비스 배포는 Jenkins가 요청하고 롤백은 Portainer 화면에서 수행합니다.

Jenkins를 고른 이유

셀프호스팅 환경에서 CI 설정과 자격 증명 범위를 코드로 관리하려고 Jenkins를 선택했습니다. JCasC와 Job DSL로 컨트롤러 설정과 잡을 Git에 두고, 컨트롤러는 executor 0으로 설정했습니다. 실제 빌드는 권한과 자원을 제한한 별도 컨테이너에서 실행합니다.

02

플랫폼 아키텍처

GitHub push가 이미지와 배포 요청으로 이어지는 흐름입니다.

CI는 digest 발행까지

GitHub push를 받은 Go 릴레이가 HMAC·저장소 허용 목록·중복 요청을 검사한 뒤 내부 Jenkins를 호출합니다. Jenkins는 제한된 컨테이너에서 테스트하고 Buildx 빌드와 Trivy 검사를 거쳐 GHCR 태그와 digest를 만듭니다. 실제 서버 반영은 Portainer에서 별도로 진행합니다.

호스트별 역할 분리

노라네코(arm64)는 Jenkins·Prometheus·Grafana·Loki·Tempo 등 17개 컨테이너로 CI와 중앙 관측을, 1GB급 맛챠(x86_64)는 공개 Gatus 상태 페이지와 Portainer Server만 맡습니다. Portainer Agent는 노라네코와 각 서비스 호스트에 깔리고, 등록 서비스는 Jenkins가 Portainer API로 release digest 반영을 요청하며 롤백·미등록 배포는 운영자가 GUI에서 합니다. CI가 무거워도 배포 관리 화면은 영향을 받지 않도록 호스트 단위로 책임을 갈랐습니다.

관리 화면과 내부 포트 접근 제한

공개 HTTP 요청은 Cloudflare 프록시 주소에서 온 경우만 Caddy가 받습니다. SSH·Portainer·관측 포트는 Tailscale ACL에 등록한 기기와 사용자만 접근할 수 있습니다. Portainer 화면은 Tailscale Serve 안에서만 열고, Agent 통신에는 공유 시크릿을 사용합니다.

03

공급망 보안과 배포 안전성

이미지와 자격 증명을 다루는 과정에 넣은 검사입니다.

digest 고정 릴리스와 VERSION 게이트

릴리스는 루트 VERSION 파일 변경이 있어야만 성립하고, CI는 SemVer 불변 태그와 함께 이미지 digest를 아티팩트로 발행합니다. Portainer stack에는 릴리스 버전과 직전 digest가 함께 기록되어, 배포 단위는 sha256 digest입니다. 태그가 나중에 다른 이미지로 옮겨져도 배포물은 바뀌지 않고, 롤백 대상도 digest로 남습니다.

빌드 실행 범위 제한

Jenkins 컨트롤러는 코드를 직접 실행하지 않습니다. 테스트와 빌드는 non-root 사용자와 메모리·CPU·PID 제한이 있는 컨테이너에서 실행합니다. Trivy 스캐너 이미지도 digest로 고정하고 취약점 DB는 캐시로 재사용합니다.

시크릿 관리

Ansible 시크릿은 ansible-vault로 암호화한 파일만 커밋합니다. GHCR 업로드 토큰과 다운로드 토큰을 나누고 필요한 권한만 줍니다. 빌드 로그·Telegram 메시지·Portainer 설명에는 운영 시크릿을 넣지 않습니다.

04

운영 자동화와 관측성

서비스 설정 생성과 상태 확인을 자동화한 부분입니다.

서비스 YAML 하나에서 설정 6종 생성

platformctl render는 서비스 YAML 하나에서 Prometheus 타깃, 경보, 내부·공개 Gatus, Ansible 변수 등 6종 설정을 만듭니다. 생성 파일은 generated/에 저장하고 CI에서 원본과 같은지 확인합니다. 새 호스트는 부트스트랩 스크립트와 서비스 템플릿으로 추가합니다.

중앙 관측과 읽기 전용 상태 봇

노라네코가 메트릭·로그·트레이스·경보를 중앙 수집하고, 서비스 호스트는 인증된 Prometheus remote write와 Loki push로 텔레메트리를 보냅니다. 맛챠의 공개 Gatus가 외부 관점에서 서비스 헬스를 감시하고, 한글 Telegram 봇은 고정 chat/user ID 검증 아래 상태 조회와 경보 수신만 합니다. 봇에는 변경 권한이 없습니다.

풀어낸 케이스

마주친 문제 · 고민하고 적용한 접근 · 실제 결과를 한 세트로 정리했습니다.

CASE 01HMAC + allowlist + dedup
문제
GitHub 웹훅을 받으려면 Jenkins를 공개 인터넷에 노출해야 했습니다. CI 서버는 노출하고 싶지 않았습니다.
고민 · 접근
Go로 얇은 릴레이를 직접 작성해 Cloudflare CIDR만 통과하는 Caddy 뒤에 두고, X-Hub-Signature-256을 상수시간 비교로 검증, 레포 allowlist는 ^…$ 앵커 강제 + 와일드카드 금지, delivery ID로 중복 제거 후 내부 Jenkins URL로만 전달하도록 했습니다.
결과
서명이 검증되지 않은 요청은 Jenkins에 닿을 수 없고, 공개 원본 IP 직접 접근은 Caddy와 OCI NSG가 이중으로 차단합니다.
CASE 02digest-pinned 배포
문제
태그 기반 배포는 레지스트리에서 태그가 다른 이미지로 옮겨지면 검증 없이 그대로 배포되는 공급망 리스크가 있습니다.
고민 · 접근
루트 VERSION 게이트를 통과한 빌드만 SemVer 불변 태그 + digest 아티팩트를 발행하고, Portainer stack이 릴리스 버전·직전 digest를 기록하며 sha256 digest 자체를 실행하도록 설계했습니다.
결과
커밋(digest 준비)과 배포(반영)가 분리되고, 태그 변조가 일어나도 배포물은 바뀌지 않습니다.
CASE 03배포·롤백 단일 경로
문제
Ansible SSH 배포·Telegram 승인·수동 스크립트가 함께 남아 있어, 현재 배포 상태를 확인할 경로가 여러 개였습니다.
고민 · 접근
애착이 있던 자작 배포 파이프라인을 걷어내고 Portainer stack update 단일 경로로 고정했습니다. 등록 서비스는 Jenkins가 Portainer API로 digest 반영을 요청하고, 롤백·미등록 배포는 운영자가 Tailscale Serve 전용 GUI에서 합니다. Jenkins의 구형 배포 잡·SSH 크리덴셜은 제거했습니다.
결과
배포·롤백의 진입점이 하나로 줄어 현재 상태 파악과 사고 대응이 단순해졌고, Jenkins가 쥐는 크리덴셜 표면도 축소됐습니다.
CASE 04매니페스트 1개 → 산출물 6종
문제
서비스를 추가할 때 Prometheus·Gatus·Alertmanager 설정을 각각 손으로 고치면 누락되거나 서로 다른 값이 들어가기 쉬웠습니다.
고민 · 접근
platformctl render가 서비스 YAML 하나에서 6종 설정을 만들고, 생성 결과를 generated/에 커밋하도록 했습니다. 새 호스트는 부트스트랩 스크립트와 템플릿으로 추가합니다.
결과
서비스 추가 때 반복하던 설정 편집이 줄었고, 생성된 파일은 Git에서 변경 이력을 확인할 수 있습니다.

주요 성과

기술 스택