k8s의 Deployment, ReplicaSet
Controller-Runtime 사용한 k8s Deployment, ReplicaSet 작동 원리
by HOON
0
Last updated on Feb. 5, 2025, 8:10 a.m.
안녕하세요. 오늘은 쿠버네티스를 사용하면서 가장 먼저 궁금했던 Deployment와 ReplicaSet의 작동 원리에 대해서 한번 살펴보겠습니다.
우선 Deployment와 ReplicaSet에 대해 간략하게 설명드리자면, 아래는 k8s doc에 존재하는 deployment와 replicasset의 yml 파일 입니다.
각각의 document는 다음과 같습니다. 한번 보고 오시면 좋을 것 같아요.
Deployment doc
ReplicaSet doc
![]() |
![]() |
---|---|
Deployment YAML | ReplicaSet YAML |
언뜻 보기에도 굉장히 비슷해보이네요, 실제로 비슷합니다. 처음엔 Deployment와 ReplicaSet의 차이는 이름뿐이라고 생각했었어요.
그럼 각각 어떤 기능을 수행하는지 보겠습니다.
먼저, Kubernetes에서 Deployment는 애플리케이션을 선언적으로 관리하기 위한 상위 객체
입니다.
Deployment는 내부적으로 ReplicaSet을 생성하고 관리하며, ReplicaSet은 실제 Pod의 개수를 제어합니다.
-
Deployment Spec의 replicas 필드
이 값은 원하는 Pod 수를 나타냅니다.
예를 들어, replicas: 3이라면 Kubernetes는 현재 클러스터에 3개의 Pod가 실행되고 있는지 지속적으로 확인하고, 부족하면 새 Pod를 생성하며, 초과하면 Pod를 삭제합니다. -
ReplicaSet과 Reconciliation Loop
Kubernetes 컨트롤러 매니저 내에 존재하는 ReplicaSet 컨트롤러는 선언된 상태(예: replicas: 3)와 실제 상태(실행 중인 Pod 수)를 비교한 뒤, 차이가 있으면 필요한 작업(새로운 Pod 생성 또는 기존 Pod 삭제)을 수행하여 일관성을 유지합니다.
따라서 이런 모양이 되겠네요.
그렇다면 이 과정을 어떻게 자동으로 관리 해주는걸까요?? 🤷🏻♂️
답은 바로 Kubernetes
Controller-Runtime 에 있습니다!!
💡
Controller-Runtime 소개
쿠버네티스에서 컨트롤러 런타임이란 Kubernetes의 기본 컨트롤러 개발 패턴(Reconcileration loop)을 보다 쉽게 구현할 수 있도록 도와주는 역할을 합니다.
(여기서 Reconcileration이란 컨트롤러 핵심으로, 특정 리소스(Deployment, Custom Resource등)의 상태를 관찰하고, 요구치에 맞게 가동해주는 역할입니다.)
컨트롤러 런타임의 기능은 앞서 말한 Reconciler 말고도, 컴포넌트 초기화와 라이프사이클을 담당하는 Manager, 혹은 이벤트 기록
등 다양하니 한번 살펴보시면 좋을 것 같아요.
한마디로 controller-runtime을 사용하면 기본 제공되는 Kubernetes 컨트롤러와 동일한 패턴을 사용하여 커스텀 리소스를 관리하거나, 기존 리소스(Deployment 등)를 감시하여 추가 로직을 적용할 수 있겠네요!! -> 그렇담? 제가 원하는 커스텀 리소스를 생성 할 수 있겠네요! 그 내용은 다음 블로그 주제로 정해보겠습니다.
그럼! 이제 실제 코드를 볼까요??
Deployment의 Replica 수를 제어하는 커스텀 컨트롤러 예제
// controllers/deployment_controller.go
package controllers
import (
"context"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
ctrl "sigs.k8s.io/controller-runtime"
)
// DeploymentReconciler는 Deployment를 감시하는 커스텀 컨트롤러입니다.
type DeploymentReconciler struct {
client.Client
}
// Reconcile 함수는 Deployment 객체의 현재 상태와 원하는 상태를 비교하고, replica 수를 조정합니다.
func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := ctrllog.FromContext(ctx)
// Deployment 객체를 가져옴
var deploy appsv1.Deployment
if err := r.Get(ctx, req.NamespacedName, &deploy); err != nil {
log.Error(err, "Deployment 객체를 가져올 수 없음")
// 객체가 삭제된 경우 에러 무시
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 현재 replica 수 로깅
currentReplicas := int32(0)
if deploy.Spec.Replicas != nil {
currentReplicas = *deploy.Spec.Replicas
}
log.Info("Deployment Reconcile", "name", deploy.Name, "currentReplicas", currentReplicas)
// 예시 로직: replica 수가 3보다 작으면 3으로 업데이트
desiredReplicas := int32(3)
if currentReplicas < desiredReplicas {
deploy.Spec.Replicas = &desiredReplicas
if err := r.Update(ctx, &deploy); err != nil {
log.Error(err, "Deployment replica 업데이트 실패", "desiredReplicas", desiredReplicas)
return ctrl.Result{}, err
}
log.Info("Deployment replica 업데이트 완료", "name", deploy.Name, "newReplicas", desiredReplicas)
}
return ctrl.Result{}, nil
}
// SetupWithManager 함수는 매니저와 함께 컨트롤러를 설정합니다.
func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1.Deployment{}).
Complete(r)
}
실제로 저희가 주목해야 할 부분은 currentReplica를 관리하고, desireReplicas 변수가 사용되는 부분을 보면 될 것 같네요.
그럼 실제로 이 코드를 테스트 해볼까요? 테스트를 위해 GPT의 도움을 한번 받아볼게요!
// controllers/deployment_controller_test.go
package controllers_test
import (
"context"
"path/filepath"
"testing"
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" // PodTemplateSpec에 필요
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// 테스트에 필요한 controller-runtime 패키지들
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
ctrl "sigs.k8s.io/controller-runtime"
// BDD 스타일의 어서션 라이브러리 (선택사항: [Gomega](https://onsi.github.io/gomega/))
. "github.com/onsi/gomega"
// 본인이 작성한 컨트롤러 패키지 import (모듈 경로에 맞게 수정)
"example.com/my-controller/controllers"
)
// int32Ptr는 int32 값을 포인터로 반환하는 헬퍼 함수입니다.
func int32Ptr(i int32) *int32 {
return &i
}
func TestDeploymentReconciler(t *testing.T) {
g := NewGomegaWithT(t)
// envtest 환경 설정: CRD 경로 등이 필요하면 Options에 추가할 수 있습니다.
testEnv := &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("..", "config", "crd", "bases"),
},
}
cfg, err := testEnv.Start()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cfg).ToNot(BeNil())
// 클라이언트 생성
k8sClient, err := client.New(cfg, client.Options{})
g.Expect(err).ToNot(HaveOccurred())
// 테스트를 위한 Deployment 객체 생성 (초기 replica 수: 1)
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: int32Ptr(1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "test"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": "test"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "pause",
Image: "k8s.gcr.io/pause:3.2",
},
},
},
},
},
}
err = k8sClient.Create(context.Background(), deploy)
g.Expect(err).ToNot(HaveOccurred())
// Manager 생성 및 컨트롤러 등록
mgr, err := ctrl.NewManager(cfg, ctrl.Options{MetricsBindAddress: "0"})
g.Expect(err).ToNot(HaveOccurred())
reconciler := &controllers.DeploymentReconciler{Client: mgr.GetClient()}
err = reconciler.SetupWithManager(mgr)
g.Expect(err).ToNot(HaveOccurred())
// 별도의 고루틴에서 Manager 시작 (컨트롤러 루프 실행)
stopMgr := make(chan struct{})
defer close(stopMgr)
go func() {
err = mgr.Start(stopMgr)
g.Expect(err).ToNot(HaveOccurred())
}()
// Reconcile이 일어날 시간을 확보하기 위해 잠시 대기 (예: 5초)
time.Sleep(5 * time.Second)
// 업데이트된 Deployment 객체 가져오기
updatedDeploy := &appsv1.Deployment{}
err = k8sClient.Get(context.Background(), client.ObjectKey{
Name: "test-deployment",
Namespace: "default",
}, updatedDeploy)
g.Expect(err).ToNot(HaveOccurred())
// 기대하는 replica 수는 3
g.Expect(*updatedDeploy.Spec.Replicas).To(Equal(int32(3)))
}
controller-runtime은 envtest 패키지를 사용해서, 실제 Kubernetes API 서버와 etcd 테스트 환경을 구성할 수 있습니다.
이를 통해 로컬에서 컨트롤러의 동작을 검증할 수 있어요!
제가 테스트를 진행한 환경은 VirtualBox ubuntu22.04 릴리즈 버전입니다.
Kubernetes가 설치되어있고, kubebuilder는 설치되어있지 않았어요.
따라서 먼저 go 언어와 kubebuilder를 각각 설치해주세요. 설치방법은 따로 작성하지 않겠습니다.
(참고로 kubebuilder는 curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
해당 명령어로 설치했습니다.)
다음, setup-envtest
을 통해서 k8s 특정 바이너리(etcd, kube-apiserver, kubectl)을 다운로드해볼게요.
우선 종류가 뭐가 있는지 볼까요?
저는 적당한 1.24.2 버전으로 선택해보겠습니다. 어차피 실제 설치되어있는 k8s랑은 상관없으니깐요!
사진에 존재하는 명령어를 설명 드리겠습니다.
setup-envtest use 1.24.0 --bin-dir=$KUBEBUILDER_ASSETS #envtest에서 사용할 k8s 바이너리를 1.24.0에 맞게 다운로드하여 지정디렉터리에 저장합니다.
export KUBEBUILDER_ASSETS=$HOME/.kubebuilder/bin #envtest 바이너리 저장시 디렉터리가 없다면 디렉터리를 미리 생성합니다.
mkdir -p $KUBEBUILDER_ASSETS #환경변수 KUBEBUILDER를 설정합니다.
즉, 바이너리를 원하는 버전으로 설치하고, 환경변수 설정까지 하는 과정입니다.
적용이 잘 됐나 볼까요??
etcd, kube-apiserver, kubectl 까지 테스트를 위한 바이너리가 설치 완료됐습니다!
그럼 다음, 테스트 결과까지 보겠습니다! (이미지가 잘 안보여서, 이미지모듈이 수정되면 업데이트 하겠습니다..!)
k8s-master@k8s-master:~/k8s-deployment-controller$ go test -v ./controllers
=== RUN TestDeploymentReconciler
2025-02-04T12:21:55Z DEBUG controller-runtime.test-env starting control plane
2025-02-04T12:22:03Z DEBUG controller-runtime.test-env installing CRDs
2025-02-04T12:22:03Z DEBUG controller-runtime.test-env installing webhooks
2025-02-04T12:22:03Z INFO controller-runtime.metrics Starting metrics server
2025-02-04T12:22:03Z INFO controller-runtime.metrics Serving metrics server {"bindAddress": ":8080", "secure": false}
2025-02-04T12:22:03Z INFO Starting EventSource {"controller": "deployment", "controllerGroup": "apps", "controllerKind": "Deployment", "source": "kind source: *v1.Deployment"}
2025-02-04T12:22:03Z INFO Starting Controller {"controller": "deployment", "controllerGroup": "apps", "controllerKind": "Deployment"}
2025-02-04T12:22:03Z INFO Starting workers {"controller": "deployment", "controllerGroup": "apps", "controllerKind": "Deployment", "worker count": 1}
2025-02-04T12:22:04Z INFO Deployment Reconcile {"controller": "deployment", "controllerGroup": "apps", "controllerKind": "Deployment", "Deployment": {"name":"test-deployment","namespace":"default"}, "namespace": "default", "name": "test-deployment", "reconcileID": "ec0e5b7a-f4a2-4c85-865b-f947862852af", "name": "test-deployment", "currentReplicas": 1}
2025-02-04T12:22:04Z INFO Deployment replica 업데이트 완료 {"controller": "deployment", "controllerGroup": "apps", "controllerKind": "Deployment", "Deployment": {"name":"test-deployment","namespace":"default"}, "namespace": "default", "name": "test-deployment", "reconcileID": "ec0e5b7a-f4a2-4c85-865b-f947862852af", "name": "test-deployment", "newReplicas": 3}
2025-02-04T12:22:04Z INFO Deployment Reconcile {"controller": "deployment", "controllerGroup": "apps", "controllerKind": "Deployment", "Deployment": {"name":"test-deployment","namespace":"default"}, "namespace": "default", "name": "test-deployment", "reconcileID": "fd572d43-cad3-4675-a809-1f49659b7106", "name": "test-deployment", "currentReplicas": 3}
잘 보면, currentReplicas를 1로 시작했다가 3까지 증설하는 모습을 볼 수 있습니다.
이렇게 테스트는 성공적으로 끝났습니다! 👏🏻
마무리
이번 글에서는 Kubernetes Deployment가 어떻게 replica 수를 관리하는지와, controller-runtime을 활용하여 간단한 커스텀 컨트롤러를 구현하는 방법을 알아보았습니다. 실제 사용하면서 어떻게 관리가 되나 궁금했는데, 이렇게 직접 소스를 구경하면서 테스트 해보니 어느정도 감이 잡히네요.
그럼 다음 블로그 주제는 실제 제가 필요로하는 서비스에 대해서 nginx 혹은 busybox 같은 서비스처럼 구동 가능하도록 생성해보겠습니다.
감사합니다.
Leave a Comment: