쿠버네티스는 기본적으로 수평 확장(ReplicaSet, Deployment)을 전제로 설계되었습니다.
그러나 아래와 같이 오직 하나의 인스턴스만 존재해야 하는 워크로드가 있습니다.
이러한 요구를 충족하기 위한 패턴이 바로 싱글톤 서비스(Singleton Service)입니다.
| 방식 | 특징 |
|---|---|
| Deployment + Replica=1 | 가장 단순하지만, 여러 노드에서 동시에 재스케줄될 가능성이 있음 |
| StatefulSet + PersistentVolume | 스토리지와 안정적인 이름을 보장하며 단일 복제본 유지 가능 |
| Leader Election | 여러 Pod가 실행되더라도 단 하나만 리더 역할 수행 |
| CronJob / Job | 주기적 실행이 필요할 때 단일 실행 보장 |
replicas: 1 설정싱글톤 패턴을 안전하게 운영하려면 Pod 레벨을 넘어 애플리케이션 자체에서 중복 실행을 방지해야 합니다.
이를 내부 잠금(Internal Lock)과 외부 잠금(External Lock)으로 나눌 수 있습니다.
synchronized(lockObject) {
// 크론 작업 단일 실행 보장
}
# Redis Redlock 기반 분산 락
SETNX job-lock <uuid> EX 30
단일 Pod 서비스는 노드 업그레이드나 유지보수 시 중단 위험이 큽니다.
이를 최소화하려면 PodDisruptionBudget(PDB)을 설정해 강제 중단을 방지해야 합니다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: singleton-pdb
spec:
minAvailable: 1
selector:
matchLabels:
app: singleton
minAvailable: 1 : 최소 한 개의 Pod는 항상 유지kubectl drain 등의 관리 작업 중 자동 중단을 방지minAvailable: 1이 강제되어, drain 작업이 거부될 수 있디.| 기능/변화 | 상태 | 싱글톤 서비스 영향 |
|---|---|---|
| Coordinated Leader Election | Beta | 리더 선출 알고리즘을 결정적·예측 가능하게 개선. 업그레이드나 멀티 버전 환경에서 리더 충돌을 줄여 안정성 강화 |
| Scheduler 비동기 API 호출 | Beta | 스케줄러 응답성이 향상되어 리더 장애 시 failover 시간을 단축 |
| ContainerRestartRules | Alpha | Pod 내 개별 컨테이너 재시작 정책 세밀 제어 → 사이드카 장애 격리로 메인 프로세스 안정성 확보 |
| Dynamic Resource Allocation(DRA) | Stable | GPU 등 특수 자원을 단일 Pod가 독점해야 할 때 자원 예약·할당 정확도 향상 |
| Ordered Namespace Deletion | Stable | 네임스페이스 삭제 시 리소스 정리 순서 보장 → Lease·Lock 제거 충돌 완화 |
renewDeadline, retryPeriod 등을 클러스터 규모·네트워크 지연에 맞게 조정minAvailable: 1은 노드 드레인을 거부 → maxUnavailable: 0 등의 대안을 함께 고려main.go
package main
import (
"context"
"fmt"
"os"
"time"
coordinationv1 "k8s.io/api/coordination/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
)
func main() {
// Kubernetes in-cluster config
config, err := rest.InClusterConfig()
if err != nil {
panic(err.Error())
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
}
id, _ := os.Hostname()
lock, err := resourcelock.New(
resourcelock.LeasesResourceLock,
"default",
"singleton-leader",
clientset.CoreV1(),
clientset.CoordinationV1(),
resourcelock.ResourceLockConfig{
Identity: id,
},
)
if err != nil {
panic(err)
}
// coordinationv1 타입을 직접 참조해 사용
// (단순히 변수에 할당하고 _ 로 무시해도 import는 유효 처리됨)
_ = coordinationv1.Lease{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
Lock: lock,
ReleaseOnCancel: true,
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
for {
fmt.Printf("%s is leader: running singleton task...\n", id)
// metav1 실제 사용 예
now := metav1.Now()
fmt.Printf("Current metav1 time: %s\n", now.String())
time.Sleep(10 * time.Second)
}
},
OnStoppedLeading: func() {
fmt.Printf("%s lost leadership\n", id)
},
},
})
}
이 코드는 coordination.k8s.io/v1의 Lease 객체를 사용해, 여러 Pod 중 하나만 리더로 선출되어 주기 작업을 수행합니다.
go 빌드
go mod init singleton
go mod tidy
빌드 작업
# -------- Build Stage --------
FROM golang:1.25 AS builder
WORKDIR /app
# CGO 비활성화 + 정적 빌드
ENV CGO_ENABLED=0 GOOS=linux GOARCH=arm64
COPY go.mod go.sum* ./
RUN go mod download || true
COPY . .
RUN go build -ldflags="-s -w" -o singleton .
# -------- Runtime Stage --------
# glibc 없는 경량 이미지
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/singleton /singleton
USER nonroot:nonroot
ENTRYPOINT ["/singleton"]
빌드 및 이미지 푸시
docker build -t gjrjr4545/singleton:latest .
docker push gjrjr4545/singleton:latest
rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: singleton-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: singleton-role
rules:
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "create", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: singleton-rb
subjects:
- kind: ServiceAccount
name: singleton-sa
roleRef:
apiGroup: rbac.authorization.k8s.io # ← 들여쓰기 바로 roleRef 밑으로
kind: Role
name: singleton-role
deploy.yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: singleton-app
spec:
replicas: 2 # 2개 이상 띄워도 리더는 1개만!
selector:
matchLabels:
app: singleton
template:
metadata:
labels:
app: singleton
spec:
serviceAccountName: singleton-sa
containers:
- name: singleton
image: <YOUR_REPO>/singleton:1.0
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 128Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: singleton-pdb
spec:
minAvailable: 1
selector:
matchLabels:
app: singletokubectl apply -f rbac.yaml
kubectl apply -f deploy.yaml
kubectl get lease singleton-leader -o yaml
kubectl logs -l app=singleton

kubectl delete pod -l app=singleton --force
→ 다른 Pod가 즉시 리더를 인계받고 작업을 계속하는지 확인합니다.

client-go 이용해서 리더선출 방식의 싱글턴을 구현하셨는데, 굿잡!