Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Archives
Today
Total
관리 메뉴

근묵자흑

Kubernetes Pattern: Controller 본문

k8s/kubernetes-pattern

Kubernetes Pattern: Controller

Luuuuu 2026. 1. 31. 20:06

 

Kubernetes는 선언적(Declarative) 리소스 관리를 기반으로 동작합니다. 사용자가 원하는 상태를 정의하면 시스템이 해당 상태를 유지합니다. 이 동작을 담당하는 것이 바로 Controller입니다.

이 글에서는 Controller 패턴의 개념과 구현 방법을 실제 테스트 결과와 함께 정리하고, 최신 동향도 확인해 보겠습니다.

 

Controller 개요

문제

Kubernetes는 범용 오케스트레이션 플랫폼이므로 모든 애플리케이션 요구사항을 기본 기능으로 충족하지 못합니다. 플랫폼을 수정하지 않고 기능을 확장할 방법이 필요합니다.

해결책

Kubernetes의 이벤트 기반 아키텍처를 활용하여 커스텀 Controller를 작성합니다. Controller는 리소스 상태 변경을 감시하고, 현재 상태를 원하는 상태에 맞추는 조정(Reconciliation) 작업을 수행합니다.

 

Controller 동작 원리

Controller는 Observe-Analyze-Act 사이클을 반복 실행합니다.

flowchart LR
    A[Observe] --> B[Analyze]
    B --> C[Act]
    C --> A

    A -.- A1[리소스 상태 감시]
    B -.- B1[현재 상태와 목표 상태 비교]
    C -.- C1[조정 작업 수행]

상태 조정 흐름

sequenceDiagram
    participant User
    participant API as API Server
    participant Ctrl as Controller
    participant Pod

    User->>API: ConfigMap 수정
    API->>Ctrl: MODIFIED 이벤트 전송
    Ctrl->>Ctrl: Annotation 확인
    Ctrl->>API: Pod 삭제 요청
    API->>Pod: Pod 종료
    Note over API,Pod: ReplicaSet이 새 Pod 생성
    API-->>Ctrl: 삭제 완료 응답

내장 Controller와 커스텀 Controller

Kubernetes에는 ReplicaSet, DaemonSet, StatefulSet, Deployment, Service 등 표준 리소스를 관리하는 내장 Controller 컬렉션이 있습니다. 이 Controller들은 컨트롤 플레인 노드에 배포되는 Controller Manager의 일부로 실행됩니다.

중요한 점은 이 Controller들이 서로를 인식하지 못한다는 것입니다. 각자 무한 조정 루프에서 독립적으로 실행되며, 자신이 담당하는 리소스의 실제 상태와 원하는 상태를 모니터링하고 그에 따라 행동합니다.

이러한 기본 Controller 외에도 Kubernetes의 이벤트 기반 아키텍처는 다른 커스텀 Controller를 네이티브하게 연결할 수 있게 해줍니다. 커스텀 Controller도 내부 Controller와 동일한 방식으로 상태 변경 이벤트에 반응하여 추가 기능을 제공할 수 있습니다.

Controller와 Operator의 구분

복잡성과 진화 관점에서 능동적 조정 컴포넌트를 두 그룹으로 분류할 수 있습니다.

Controller는 표준 Kubernetes 리소스를 모니터링하고 조작하는 단순한 조정 프로세스입니다. 주로 플랫폼 동작을 강화하고 새로운 플랫폼 기능을 추가하는 데 사용됩니다.

Operator는 CustomResourceDefinition(CRD)과 상호작용하는 정교한 조정 프로세스입니다. 일반적으로 복잡한 애플리케이션 도메인 로직을 캡슐화하고 전체 애플리케이션 라이프사이클을 관리합니다.

이 글에서는 단순한 Controller에 집중하고, Operator 패턴은 Chapter 28에서 다룹니다.


구현 예제: ConfigMap Watcher Controller

ConfigMap 변경 시 연관된 Pod를 재시작하는 Controller를 구현합니다. ConfigMap을 환경변수로 사용하는 Pod는 ConfigMap 변경만으로는 새 값을 반영하지 못하므로, Pod 재시작이 필요합니다.

아키텍처

flowchart TB
    subgraph "Controller Pod"
        proxy[kubeapi-proxy<br/>Ambassador Container]
        watcher[config-watcher<br/>Controller Script]
        proxy <--> watcher
    end

    subgraph "API Server"
        api[Kubernetes API]
        watch[Watch API<br/>watch=true]
    end

    subgraph "Application"
        cm[ConfigMap<br/>webapp-config]
        deploy[Deployment<br/>webapp]
        pod[Pod<br/>app=webapp]
        deploy --> pod
        cm -.->|환경변수| pod
    end

    watcher -->|1. Watch ConfigMaps| watch
    watch -->|2. MODIFIED 이벤트| watcher
    watcher -->|3. DELETE Pod| api
    api -->|4. Pod 삭제| pod

Controller Script

Controller는 Shell Script로 구현됩니다. Kubernetes API의 Watch 기능을 사용하여 ConfigMap 변경을 실시간으로 수신합니다.

#!/bin/bash

namespace=${WATCH_NAMESPACE:-default}
base=http://localhost:8001
ns=namespaces/$namespace

start_event_loop() {
  echo "::: Starting to wait for events"

  # watch=true: Hanging GET으로 이벤트 스트림 수신
  curl -N -s $base/api/v1/${ns}/configmaps?watch=true | while read -r event
  do
    event=$(echo "$event" | tr '\r\n' ' ')

    local type=$(echo "$event" | jq -r .type)
    local config_map=$(echo "$event" | jq -r .object.metadata.name)
    local annotations=$(echo "$event" | jq -r '.object.metadata.annotations')

    if [ "$annotations" != "null" ]; then
      local pod_selector=$(echo $annotations | \
        jq -r 'to_entries | .[] | select(.key == "k8spatterns.com/podDeleteSelector") | .value | @uri')
    fi

    echo "::: $type -- $config_map -- $pod_selector"

    if [ $type = "MODIFIED" ] && [ -n "$pod_selector" ]; then
      delete_pods_with_selector "$pod_selector"
    fi
  done
}

delete_pods_with_selector() {
  local selector=${1}
  echo "::::: Deleting pods with $selector"

  local pods=$(curl -s $base/api/v1/${ns}/pods?labelSelector=$selector | \
               jq -r .items[].metadata.name)

  for pod in $pods; do
    exit_code=$(curl -s -X DELETE -o /dev/null -w "%{http_code}" $base/api/v1/${ns}/pods/$pod)
    if [ $exit_code -eq 200 ]; then
      echo "::::: Deleted pod $pod"
    else
      echo "::::: Error deleting pod $pod: $exit_code"
    fi
  done
}

start_event_loop

주요 구현 포인트는 다음과 같습니다.

항목 설명
watch=true API Server와 지속적인 연결을 유지하며 이벤트를 수신합니다
localhost:8001 Ambassador 컨테이너(kubeapi-proxy)를 통해 API에 접근합니다
WATCH_NAMESPACE Downward API로 현재 네임스페이스를 주입합니다
podDeleteSelector ConfigMap Annotation에서 Pod 선택자를 추출합니다

Controller Deployment

apiVersion: v1
kind: ServiceAccount
metadata:
  name: config-watcher-controller
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: config-watcher-controller
subjects:
- kind: ServiceAccount
  name: config-watcher-controller
roleRef:
  name: edit
  kind: ClusterRole
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: config-watcher-controller
data:
  config-watcher-controller.sh: |
    # Controller Script 내용
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: config-watcher-controller
spec:
  replicas: 1  # Singleton 패턴
  selector:
    matchLabels:
      app: config-watcher-controller
  template:
    metadata:
      labels:
        app: config-watcher-controller
    spec:
      serviceAccountName: config-watcher-controller
      containers:
      # Ambassador 패턴: API Server 프록시
      - name: kubeapi-proxy
        image: k8spatterns/kubeapi-proxy
      # Controller 메인 컨테이너
      - name: config-watcher
        image: k8spatterns/curl-jq
        env:
        - name: WATCH_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace  # Downward API
        command: ["sh", "/watcher/config-watcher-controller.sh"]
        volumeMounts:
        - mountPath: "/watcher"
          name: config-watcher-controller
      volumes:
      - name: config-watcher-controller
        configMap:
          name: config-watcher-controller

Web Application

Controller가 감시할 대상 애플리케이션입니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: webapp-config
  annotations:
    # Controller가 사용할 Pod 선택자
    k8spatterns.com/podDeleteSelector: "app=webapp"
data:
  message: "Welcome to Kubernetes Patterns !"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
spec:
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp  # 위 선택자와 일치
    spec:
      containers:
      - name: app
        image: k8spatterns/mini-http-server
        ports:
        - containerPort: 8080
        env:
        - name: MESSAGE
          valueFrom:
            configMapKeyRef:
              name: webapp-config
              key: message
---
apiVersion: v1
kind: Service
metadata:
  name: webapp
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 8080
  selector:
    app: webapp

테스트 결과

minikube 환경에서 테스트를 수행했습니다.

테스트 환경

Kubernetes: minikube
Namespace: controller-demo

테스트 과정

  1. Controller와 Web App 배포
  2. 초기 Pod 확인: webapp-f4ddf5b47-7mnl2
  3. ConfigMap 수정
  4. Controller 동작 확인
  5. 새 Pod 확인: webapp-f4ddf5b47-6skwj

Controller 로그

::: Starting to wait for events
::: ADDED -- config-watcher-controller --
::: ADDED -- kube-root-ca.crt --
::: ADDED -- webapp-config -- app%3Dwebapp
::: MODIFIED -- webapp-config -- app%3Dwebapp
::::: Deleting pods with app%3Dwebapp
::::: Deleted pod webapp-f4ddf5b47-7mnl2

Controller가 시작되면 먼저 기존 ConfigMap들에 대한 ADDED 이벤트를 수신합니다. 그 후 ConfigMap이 수정되면 MODIFIED 이벤트를 받아 해당하는 Pod를 삭제합니다.

동작 확인

flowchart LR
    subgraph Before
        CM1[ConfigMap<br/>message: Welcome...]
        Pod1[Pod<br/>webapp-...-7mnl2]
    end

    subgraph "ConfigMap 수정"
        Patch[kubectl patch]
    end

    subgraph After
        CM2[ConfigMap<br/>message: Hello...]
        Pod2[Pod<br/>webapp-...-6skwj]
    end

    CM1 --> Patch
    Patch --> CM2
    Pod1 -.->|삭제| Patch
    Patch -.->|생성| Pod2
단계 내용
수정 전 Pod 이름 webapp-f4ddf5b47-7mnl2
수정 전 MESSAGE Welcome to Kubernetes Patterns !
수정 후 Pod 이름 webapp-f4ddf5b47-6skwj
수정 후 MESSAGE Hello from Controller Pattern Demo - 19:48:56

ConfigMap이 수정되자 Controller가 기존 Pod를 삭제했고, Deployment의 ReplicaSet이 새 Pod를 생성했습니다. 새 Pod는 수정된 ConfigMap 값을 환경변수로 가져와 정상적으로 새로운 메시지를 반환합니다.


적용된 패턴

이 Controller 구현에는 여러 Kubernetes 패턴이 적용되어 있습니다.

flowchart TB
    Controller[ConfigMap Watcher Controller]

    Controller --> P1[Singleton Service<br/>replicas: 1]
    Controller --> P2[Ambassador<br/>kubeapi-proxy]
    Controller --> P3[Self Awareness<br/>Downward API]
    Controller --> P4[Configuration Resource<br/>ConfigMap + Annotation]

    P1 -.- D1[동시성 문제 방지]
    P2 -.- D2[API Server 접근 단순화]
    P3 -.- D3[네임스페이스 자동 인식]
    P4 -.- D4[Controller 설정 저장]

Singleton Service 패턴

replicas: 1로 설정하여 단일 인스턴스만 실행합니다. 여러 Controller가 동시에 같은 리소스를 조작하면 충돌이 발생할 수 있습니다.

Ambassador 패턴

kubeapi-proxy 컨테이너가 localhost:8001을 API Server로 프록시합니다. Controller 스크립트는 인증 처리 없이 localhost로 API에 접근할 수 있습니다.

Self Awareness 패턴

Downward API를 사용하여 metadata.namespace를 WATCH_NAMESPACE 환경변수로 주입합니다. Controller가 배포된 네임스페이스를 자동으로 인식합니다.

Configuration Resource 패턴

Controller 설정은 Annotation을 사용합니다. k8spatterns.com/podDeleteSelector Annotation으로 재시작할 Pod의 Label 선택자를 지정합니다.


Controller 데이터 저장 위치

Controller가 사용하는 데이터는 저장 위치에 따라 특성이 다릅니다.

저장 위치 용도 특징
Labels 리소스 식별 및 선택 인덱싱됨, 셀렉터 쿼리 가능
Annotations 비식별 메타데이터 인덱싱 안됨, 구문 제약 적음
ConfigMap 복잡한 설정 데이터 CRD 대안, 클러스터 권한 불필요

이 예제에서는 Annotation을 사용합니다. Pod 선택자는 식별 목적이 아닌 Controller 동작을 위한 설정이므로 Annotation이 적합합니다.

 


실무에서 사용되는 Controller

cert-manager

TLS 인증서 관리를 자동화하는 Controller입니다. 세 개의 핵심 컴포넌트로 구성됩니다.

  1. Controller: Certificate → CertificateRequest → Order → Challenge 흐름 관리
  2. Webhook: Admission 검증 및 변환
  3. CA Injector: ValidatingWebhookConfiguration 등에 CA 주입

동일 클러스터에서 Let's Encrypt, Vault, HashiCorp, 자체 CA를 혼용할 수 있으며, Issuer(네임스페이스)와 ClusterIssuer(클러스터 전역)로 범위를 구분합니다.

Argo CD

GitOps CD를 위한 Controller입니다. Application Controller가 Git 저장소의 원하는 상태와 클러스터의 실제 상태를 비교하여 Sync 상태를 관리합니다. Sync Waves와 Hooks 패턴으로 PreSync → Sync → PostSync 단계의 복잡한 배포 오케스트레이션이 가능합니다.

Flux

GitOps Toolkit 아키텍처로 5개의 전문화된 Controller로 구성됩니다.

  1. Source Controller: Git, OCI, Helm, S3 소스 통합
  2. Kustomize Controller: Kustomization 적용
  3. Helm Controller: HelmRelease 선언적 관리
  4. Notification Controller: 알림 및 웹훅 수신
  5. Image Automation: 컨테이너 이미지 태그 자동 업데이트

dependsOn 필드로 Controller 간 의존성을 선언하여 순차적 조정을 보장합니다.


정리

Controller는 Kubernetes의 선언적 관리를 가능하게 하는 핵심 컴포넌트입니다.

  • Controller는 Observe-Analyze-Act 사이클을 반복하며 상태를 조정합니다
  • Watch API로 리소스 변경을 실시간으로 수신합니다
  • Annotation은 Controller 설정 저장에 적합합니다
  • Singleton 패턴으로 동시성 문제를 방지합니다
  • 프로덕션 환경에서는 controller-runtime 등 프레임워크를 사용합니다
  • 모든 Reconciliation은 멱등하게 설계해야 합니다
  • GenerationChangedPredicate로 불필요한 이벤트를 필터링합니다
  • Server-Side Apply를 기본 적용 방식으로 채택합니다
  • ObservedGeneration과 Conditions로 상태를 명확히 표현합니다
  • 외부 리소스 관리 시 Finalizer를 반드시 구현합니다
  • 대규모 환경에서는 Sharding과 Watch 최적화를 고려합니다

다음 장에서는 CRD와 결합한 Operator 패턴을 다룹니다.


참고