Notice
Recent Posts
Recent Comments
Link
«   2025/12   »
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 31
Archives
Today
Total
관리 메뉴

근묵자흑

Kubernetes Pattern: Self Awareness 본문

k8s/kubernetes-pattern

Kubernetes Pattern: Self Awareness

Luuuuu 2025. 11. 8. 19:59

 

쿠버네티스 환경에서 애플리케이션을 실행하다 보면 이런 고민을 하게 됩니다:

  • "내 Pod의 이름이 뭐지? 로그에 어떻게 남기지?"
  • "이 Pod에 할당된 메모리가 얼마야? JVM 힙을 얼마로 설정해야 하지?"
  • "같은 StatefulSet의 다른 Pod들을 어떻게 찾지?"

이런 질문들의 답을 얻기 위해 많은 개발자들이 Kubernetes API 서버를 직접 호출하는 코드를 작성합니다. 하지만 더 간단한 방법이 있습니다. 바로 Self Awareness 패턴입니다.

이 글에서는 실제 minikube 환경에서 테스트한 5가지 실습 예제를 통해 Self Awareness 패턴을 이해하고, 테스트를 통해 어떻게 적용한가에 대해 정리했습니다.

Self Awareness 패턴이란?

Self Awareness 패턴은 Kubernetes Downward API를 사용하여 Pod와 컨테이너의 메타데이터를 애플리케이션에 주입하는 방법입니다.

왜 중요한가?

기존 방식의 문제점:

# [BAD] API 서버를 직접 호출하는 방식
from kubernetes import client, config

config.load_incluster_config()
v1 = client.CoreV1Api()
pod = v1.read_namespaced_pod(pod_name, namespace)
print(f"내 Pod 이름: {pod.metadata.name}")

문제점:

  • Kubernetes 클라이언트 라이브러리 의존성 추가
  • RBAC 권한 설정 필요
  • API 서버에 추가 부하
  • 네트워크 지연 발생
  • 1000개 Pod가 초당 1번씩 호출하면 = 초당 1000 API 요청!

Downward API 방식:

env:
- name: POD_NAME
  valueFrom:
    fieldRef:
      fieldPath: metadata.name
# [GOOD] 환경변수로 즉시 접근
import os
pod_name = os.getenv('POD_NAME')
print(f"내 Pod 이름: {pod_name}")

장점:

  • 추가 라이브러리 불필요
  • RBAC 권한 불필요
  • API 서버 부하 제로
  • 지연 시간 없음
  • 모든 프로그래밍 언어에서 동일하게 사용

실습 환경

  • minikube v1.37.0

실습 1: 환경변수를 통한 메타데이터 주입

가장 기본적이고 많이 사용되는 방식입니다.

YAML 구성

apiVersion: v1
kind: Pod
metadata:
  name: env-downward-demo
  labels:
    app: myapp
    version: "1.0"
spec:
  containers:
  - name: app
    image: busybox:1.36
    command: ["sh", "-c", "echo Pod: $POD_NAME, IP: $POD_IP; sleep 3600"]
    resources:
      limits:
        memory: "256Mi"
        cpu: "200m"
    env:
    # Pod 메타데이터
    - name: POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name

    - name: POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP

    - name: NODE_NAME
      valueFrom:
        fieldRef:
          fieldPath: spec.nodeName

    # 레이블 값
    - name: APP_VERSION
      valueFrom:
        fieldRef:
          fieldPath: metadata.labels['version']

    # 리소스 제한 (MB 단위로 변환)
    - name: MEMORY_LIMIT_MB
      valueFrom:
        resourceFieldRef:
          resource: limits.memory
          divisor: "1Mi"

    # CPU 제한 (milliCPU 단위)
    - name: CPU_LIMIT_MILLICORES
      valueFrom:
        resourceFieldRef:
          resource: limits.cpu
          divisor: "1m"

테스트 실행

kubectl apply -f 01-env-variables.yaml
kubectl logs env-downward-demo

실행 결과

=== Self Awareness Pattern - Environment Variables ===

Pod Information:
  Pod Name: env-downward-demo
  Namespace: default
  Pod IP: 10.244.0.7
  Node Name: minikube
  Service Account: default

Resource Information:
  Memory Limit: 256 MB
  CPU Limit: 200 milliCPU
  Memory Request: 128 MB
  CPU Request: 100 milliCPU

Labels:
  App Version: 1.0

실무 활용: 구조화된 로깅

import logging
import json
import os

class K8sLogger:
    def __init__(self):
        self.pod_name = os.getenv('POD_NAME', 'unknown')
        self.namespace = os.getenv('POD_NAMESPACE', 'unknown')
        self.node = os.getenv('NODE_NAME', 'unknown')

    def log(self, level, message, **kwargs):
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'level': level,
            'message': message,
            'pod': self.pod_name,
            'namespace': self.namespace,
            'node': self.node,
            **kwargs
        }
        print(json.dumps(log_entry))

logger = K8sLogger()
logger.log('INFO', 'Request processed', request_id='abc-123', duration_ms=45)

로그 출력:

{
  "timestamp": "2025-11-08T10:30:45.123Z",
  "level": "INFO",
  "message": "Request processed",
  "pod": "api-server-5d8c7f6b9-xk2jp",
  "namespace": "production",
  "node": "node-3",
  "request_id": "abc-123",
  "duration_ms": 45
}

이제 ELK나 Loki에서 Pod별, 노드별로 로그를 쉽게 필터링할 수 있습니다

실습 2: 볼륨을 통한 메타데이터 주입

레이블과 어노테이션이 많거나, 런타임 중 변경이 필요한 경우 볼륨을 사용합니다.

YAML 구성

apiVersion: v1
kind: Pod
metadata:
  name: volume-downward-demo
  labels:
    app: myapp
    version: "2.0"
    environment: production
  annotations:
    team: "platform-team"
    build-id: "build-456"
spec:
  containers:
  - name: app
    image: busybox:1.36
    command: ["sh", "-c"]
    args:
    - |
      echo "=== All Labels ==="
      cat /etc/podinfo/labels
      echo "=== All Annotations ==="
      cat /etc/podinfo/annotations
      sleep 3600

    volumeMounts:
    - name: podinfo
      mountPath: /etc/podinfo
      readOnly: true

  volumes:
  - name: podinfo
    downwardAPI:
      items:
      # 모든 레이블을 하나의 파일로
      - path: "labels"
        fieldRef:
          fieldPath: metadata.labels

      # 모든 어노테이션을 하나의 파일로
      - path: "annotations"
        fieldRef:
          fieldPath: metadata.annotations

      # 개별 필드도 가능
      - path: "pod-name"
        fieldRef:
          fieldPath: metadata.name

테스트 실행

kubectl apply -f 02-volume-metadata.yaml
kubectl logs volume-downward-demo

실행 결과

=== All Labels ===
app="myapp"
environment="production"
version="2.0"

=== All Annotations ===
build-id="build-456"
team="platform-team"

⚠️ 볼륨과 환경변수의 차이

필드 환경변수 볼륨
metadata.name O O
metadata.labels O (개별) O (전체)
status.podIP O X
spec.nodeName O X

Point: podIPnodeName반드시 환경변수로 접근해야 합니다!

실습 3: 동적 레이블/어노테이션 업데이트

볼륨의 진가를 발휘하는 순간입니다. 실행 중인 Pod의 레이블/어노테이션 변경이 감지됩니다!

YAML 구성

apiVersion: v1
kind: Pod
metadata:
  name: dynamic-update-demo
  labels:
    stage: alpha
spec:
  containers:
  - name: watcher
    image: busybox:1.36
    command: ["sh", "-c"]
    args:
    - |
      LABELS_FILE="/etc/podinfo/labels"
      LAST_MD5=""

      while true; do
        CURRENT_MD5=$(md5sum $LABELS_FILE | cut -d' ' -f1)

        if [ "$CURRENT_MD5" != "$LAST_MD5" ]; then
          echo "[$(date)] LABELS CHANGED!"
          cat $LABELS_FILE
          LAST_MD5=$CURRENT_MD5
        fi

        sleep 5
      done

    volumeMounts:
    - name: podinfo
      mountPath: /etc/podinfo

  volumes:
  - name: podinfo
    downwardAPI:
      items:
      - path: "labels"
        fieldRef:
          fieldPath: metadata.labels

테스트 실행

# 터미널 1: Pod 생성 및 로그 모니터링
kubectl apply -f 03-dynamic-update.yaml
kubectl logs -f dynamic-update-demo

# 터미널 2: 레이블 변경
kubectl label pod dynamic-update-demo stage=beta --overwrite
kubectl label pod dynamic-update-demo feature=new-api

실행 결과

Initial state:
stage="alpha"

[2025-11-08 10:34:38] LABELS CHANGED!
stage="beta"

[2025-11-08 10:35:22] LABELS CHANGED!
feature="new-api"
stage="beta"

업데이트 감지 시간: 15-60초 (kubelet sync 주기)

실무 활용: 설정 동적 리로드

package main

import (
    "crypto/md5"
    "io/ioutil"
    "time"
)

func watchConfig(filePath string, callback func(string)) {
    lastHash := ""

    for {
        data, _ := ioutil.ReadFile(filePath)
        currentHash := fmt.Sprintf("%x", md5.Sum(data))

        if currentHash != lastHash {
            log.Println("Configuration changed!")
            callback(string(data))
            lastHash = currentHash
        }

        time.Sleep(10 * time.Second)
    }
}

func main() {
    go watchConfig("/etc/podinfo/labels", func(content string) {
        // 레이블 변경 시 설정 리로드
        reloadConfiguration(content)
    })

    // 메인 애플리케이션 실행
    startServer()
}

사용 시나리오:

  • Feature flag 토글 (feature-x=enabled)
  • 로그 레벨 변경 (log-level=debug)
  • 카나리 배포 진행률 (canary-weight=20)

실습 4: 리소스 인식 애플리케이션

같은 컨테이너 이미지로 다양한 리소스 환경에 배포할 때 자동으로 튜닝됩니다.

YAML 구성

apiVersion: v1
kind: Pod
metadata:
  name: resource-aware-app
spec:
  containers:
  - name: app
    image: busybox:1.36
    command: ["sh", "-c"]
    args:
    - |
      # CPU 기반 워커 스레드 계산
      if [ $CPU_LIMIT_MILLICORES -ge 1000 ]; then
        WORKERS=$(($CPU_LIMIT_MILLICORES / 1000))
      else
        WORKERS=1
      fi

      # 메모리 기반 캐시 크기 (25% 사용)
      CACHE_SIZE_MB=$(($MEMORY_LIMIT_MB * 25 / 100))

      # 메모리 기반 커넥션 풀
      CONN_POOL_SIZE=$(($MEMORY_LIMIT_MB / 32))

      echo "Workers: $WORKERS"
      echo "Cache: ${CACHE_SIZE_MB}MB"
      echo "Connections: $CONN_POOL_SIZE"

      # 실제 애플리케이션 시작
      ./app --workers=$WORKERS --cache-size=$CACHE_SIZE_MB

    resources:
      limits:
        memory: "512Mi"
        cpu: "500m"

    env:
    - name: MEMORY_LIMIT_MB
      valueFrom:
        resourceFieldRef:
          resource: limits.memory
          divisor: "1Mi"
    - name: CPU_LIMIT_MILLICORES
      valueFrom:
        resourceFieldRef:
          resource: limits.cpu
          divisor: "1m"

테스트 결과

작은 설정 (512MB, 500m CPU):

Workers: 1
Cache: 128MB
Connections: 16

큰 설정 (2GB, 2000m CPU):

Workers: 2
Cache: 512MB
Connections: 64

실무 활용: Java 애플리케이션 힙 자동 조정

apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: openjdk:17
        resources:
          limits:
            memory: "2Gi"
            cpu: "1000m"
        env:
        - name: MEMORY_LIMIT_MB
          valueFrom:
            resourceFieldRef:
              resource: limits.memory
              divisor: "1Mi"
        - name: CPU_LIMIT_MILLICORES
          valueFrom:
            resourceFieldRef:
              resource: limits.cpu
              divisor: "1m"
        command:
        - sh
        - -c
        - |
          # 메모리의 75%를 힙으로 할당
          HEAP_SIZE=$(($MEMORY_LIMIT_MB * 75 / 100))

          # CPU에 따라 GC 스레드 조정
          GC_THREADS=$(($CPU_LIMIT_MILLICORES / 1000))

          java -Xmx${HEAP_SIZE}m \
               -Xms${HEAP_SIZE}m \
               -XX:ParallelGCThreads=${GC_THREADS} \
               -jar application.jar

효과:

  • DEV 환경 (512MB): 384MB 힙
  • STG 환경 (2GB): 1536MB 힙
  • PRD 환경 (4GB): 3072MB 힙
  • 동일한 이미지로 모든 환경 대응!

실습 5: 멀티 컨테이너 Pod의 메타데이터 공유

Init 컨테이너와 사이드카가 메인 컨테이너의 리소스 정보를 알아야 할 때 사용합니다.

YAML 구성

apiVersion: v1
kind: Pod
metadata:
  name: multi-container-aware
spec:
  # Init 컨테이너: 메인 앱의 CPU에 따라 nginx 설정 생성
  initContainers:
  - name: config-generator
    image: busybox:1.36
    command: ["sh", "-c"]
    args:
    - |
      WORKERS=$(($CPU_LIMIT / 1000))
      if [ $WORKERS -lt 1 ]; then WORKERS=1; fi

      cat > /config/nginx.conf <<EOF
      user nginx;
      worker_processes ${WORKERS};

      http {
          server {
              listen 80;
              location /info {
                  return 200 '{"workers":${WORKERS}}';
              }
          }
      }
      EOF

      echo "Generated nginx config with ${WORKERS} workers"

    env:
    - name: CPU_LIMIT
      valueFrom:
        resourceFieldRef:
          containerName: main-app  # 메인 앱의 CPU 참조!
          resource: limits.cpu
          divisor: "1m"

    volumeMounts:
    - name: nginx-config
      mountPath: /config

  containers:
  # 메인 애플리케이션
  - name: main-app
    image: nginx:1.25-alpine
    resources:
      limits:
        memory: "1Gi"
        cpu: "1000m"
    volumeMounts:
    - name: nginx-config
      mountPath: /etc/nginx/nginx.conf
      subPath: nginx.conf

  # 모니터링 사이드카
  - name: monitor
    image: busybox:1.36
    command: ["sh", "-c"]
    args:
    - |
      echo "Monitoring main-app"
      echo "Main Memory: $(cat /etc/resources/main-mem-limit)MB"
      echo "Main CPU: $(cat /etc/resources/main-cpu-limit)m"

      while true; do
        echo "[$(date '+%H:%M:%S')] Health check"
        sleep 10
      done

    volumeMounts:
    - name: resources
      mountPath: /etc/resources

  volumes:
  - name: nginx-config
    emptyDir: {}

  - name: resources
    downwardAPI:
      items:
      - path: "main-mem-limit"
        resourceFieldRef:
          containerName: main-app
          resource: limits.memory
          divisor: "1Mi"
      - path: "main-cpu-limit"
        resourceFieldRef:
          containerName: main-app
          resource: limits.cpu
          divisor: "1m"

테스트 결과

Init 컨테이너:

Generated nginx config with 1 workers

Monitor 사이드카:

Monitoring main-app
Main Memory: 1024MB
Main CPU: 1000m
[10:35:45] Health check

Downward API  가이드

Pod 레벨 메타데이터 (fieldRef)

필드 환경변수 볼륨 예시 값
metadata.name O O my-app-5d8c7
metadata.namespace O O production
metadata.uid O O abc-123-def
metadata.labels O (개별) O (전체) app="myapp"
metadata.annotations O (개별) O (전체) version="1.0"
spec.nodeName O X node-1
spec.serviceAccountName O X default
status.podIP O X 10.244.0.5
status.hostIP O X 192.168.1.10

컨테이너 레벨 리소스 (resourceFieldRef)

필드 환경변수 볼륨 divisor 예시
requests.cpu O O 1m (milliCPU)
limits.cpu O O 1m
requests.memory O O 1Mi, 1Gi
limits.memory O O 1Mi, 1Gi
requests.ephemeral-storage O O 1Gi
limits.ephemeral-storage O O 1Gi

Divisor 사용법

# milliCPU로 변환 (500m → 500)
- name: CPU_LIMIT
  valueFrom:
    resourceFieldRef:
      resource: limits.cpu
      divisor: "1m"

# MB로 변환 (512Mi → 512)
- name: MEMORY_LIMIT_MB
  valueFrom:
    resourceFieldRef:
      resource: limits.memory
      divisor: "1Mi"

# GB로 변환 (2Gi → 2)
- name: MEMORY_LIMIT_GB
  valueFrom:
    resourceFieldRef:
      resource: limits.memory
      divisor: "1Gi"

실무에선 어떻게 활용할 것인가?

1. 환경변수 vs 볼륨 선택 기준

# [GOOD] 환경변수 사용: 시작 시 필요한 정보
env:
- name: POD_NAME
  valueFrom:
    fieldRef:
      fieldPath: metadata.name
- name: MEMORY_LIMIT_MB
  valueFrom:
    resourceFieldRef:
      resource: limits.memory
      divisor: "1Mi"

# [GOOD] 볼륨 사용: 런타임 업데이트가 필요한 정보
volumes:
- name: podinfo
  downwardAPI:
    items:
    - path: "labels"
      fieldRef:
        fieldPath: metadata.labels

선택 가이드:

사용 케이스 권장 방법
애플리케이션 시작 설정 환경변수
로깅/모니터링 식별자 환경변수
리소스 기반 튜닝 환경변수
Feature flag 볼륨 (동적 업데이트)
설정 리로드 볼륨 (동적 업데이트)
많은 메타데이터 (10개 이상) 볼륨

2. 네이밍 컨벤션

env:
# Kubernetes 메타데이터는 K8S_ 접두사
- name: K8S_POD_NAME
  valueFrom:
    fieldRef:
      fieldPath: metadata.name

- name: K8S_NAMESPACE
  valueFrom:
    fieldRef:
      fieldPath: metadata.namespace

# 리소스 정보는 RESOURCE_ 접두사
- name: RESOURCE_MEMORY_LIMIT_MB
  valueFrom:
    resourceFieldRef:
      resource: limits.memory
      divisor: "1Mi"

- name: RESOURCE_CPU_LIMIT_MILLICORES
  valueFrom:
    resourceFieldRef:
      resource: limits.cpu
      divisor: "1m"

# 애플리케이션 설정은 APP_ 접두사
- name: APP_VERSION
  value: "1.0.0"

- name: APP_ENV
  value: "production"

3. 에러 처리

import os

def get_pod_metadata():
    """Pod 메타데이터를 안전하게 가져오기"""
    return {
        'pod_name': os.getenv('K8S_POD_NAME', 'unknown-pod'),
        'namespace': os.getenv('K8S_NAMESPACE', 'default'),
        'pod_ip': os.getenv('K8S_POD_IP', '0.0.0.0'),
        'node': os.getenv('K8S_NODE_NAME', 'unknown-node'),
    }

def get_resource_limits():
    """리소스 제한을 안전하게 가져오기"""
    try:
        memory_mb = int(os.getenv('RESOURCE_MEMORY_LIMIT_MB', '0'))
        cpu_millicores = int(os.getenv('RESOURCE_CPU_LIMIT_MILLICORES', '0'))

        if memory_mb == 0 or cpu_millicores == 0:
            raise ValueError("Resource limits not set")

        return {
            'memory_mb': memory_mb,
            'cpu_millicores': cpu_millicores,
        }
    except (ValueError, TypeError) as e:
        # 기본값 사용
        return {
            'memory_mb': 512,
            'cpu_millicores': 500,
        }

# 사용
metadata = get_pod_metadata()
resources = get_resource_limits()

# 리소스 기반 튜닝
workers = max(1, resources['cpu_millicores'] // 1000)
cache_mb = resources['memory_mb'] // 4

4. 문서화

apiVersion: v1
kind: Pod
metadata:
  name: well-documented-app
  annotations:
    description: "Production API server"
    downward-api-usage: |
      Environment Variables:
      - POD_NAME: Used for structured logging
      - MEMORY_LIMIT_MB: Auto-tune JVM heap (75% of limit)
      - CPU_LIMIT: Auto-tune worker thread pool

      Volumes:
      - /etc/podinfo/labels: Dynamic feature flags
      - /etc/podinfo/annotations: Configuration updates
spec:
  containers:
  - name: app
    image: api-server:1.0
    env:
    - name: POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name
      # 로깅에서 Pod 식별에 사용

    - name: MEMORY_LIMIT_MB
      valueFrom:
        resourceFieldRef:
          resource: limits.memory
          divisor: "1Mi"
      # JVM 힙 크기 = MEMORY_LIMIT_MB * 0.75

트러블슈팅

문제 1: 환경변수가 비어있음

증상:

kubectl exec my-pod -- env | grep POD_NAME
POD_NAME=

원인: 잘못된 fieldPath

해결:

# [BAD] 잘못된 예
- name: POD_NAME
  valueFrom:
    fieldRef:
      fieldPath: metadata.pod.name  # 틀림!

# [GOOD] 올바른 예
- name: POD_NAME
  valueFrom:
    fieldRef:
      fieldPath: metadata.name  # 맞음!

문제 2: 볼륨 파일이 업데이트되지 않음

원인:

  1. 환경변수로 설정됨 (환경변수는 업데이트 안 됨)
  2. kubelet sync 주기 (60-120초 소요)
  3. 지원하지 않는 필드 (status.podIP, spec.nodeName)

해결:

# [GOOD] 볼륨으로 레이블/어노테이션 마운트
volumes:
- name: podinfo
  downwardAPI:
    items:
    - path: "labels"
      fieldRef:
        fieldPath: metadata.labels  # 업데이트 가능

# [BAD] podIP는 볼륨으로 불가능
# - path: "pod-ip"
#   fieldRef:
#     fieldPath: status.podIP  # 에러!

# [GOOD] podIP는 환경변수로
env:
- name: POD_IP
  valueFrom:
    fieldRef:
      fieldPath: status.podIP  # OK

문제 3: Pod 생성 실패

에러 메시지:

The Pod "my-pod" is invalid:
spec.volumes[0].downwardAPI.fieldRef.fieldPath: Unsupported value: "status.podIP":
supported values: "metadata.annotations", "metadata.labels", "metadata.name",
"metadata.namespace", "metadata.uid"

해결: 볼륨 지원 필드 확인

# 볼륨으로 사용 가능한 필드
volumes:
- name: podinfo
  downwardAPI:
    items:
    - path: "name"
      fieldRef:
        fieldPath: metadata.name        # [GOOD]
    - path: "namespace"
      fieldRef:
        fieldPath: metadata.namespace   # [GOOD]
    - path: "labels"
      fieldRef:
        fieldPath: metadata.labels      # [GOOD]
    - path: "annotations"
      fieldRef:
        fieldPath: metadata.annotations # [GOOD]

 

요약

  1. 간단함: API 클라이언트 라이브러리 불필요
  2. 빠름: API 서버 호출 없이 로컬 접근
  3. 확장성: 수천 개의 Pod도 API 서버 부하 없음
  4. 유연함: 정적(env)과 동적(volume) 접근 모두 지원
  5. 범용성: 모든 프로그래밍 언어에서 동일하게 사용

언제 사용해야 하는가?

O 적합한 경우:

  • 로그/메트릭에 Pod 정보 포함
  • 리소스 제한에 따른 애플리케이션 튜닝
  • Feature flag 동적 변경
  • 서비스 디스커버리
  • 멀티 컨테이너 간 정보 공유

X 부적절한 경우:

  • 다른 Pod의 정보가 필요할 때 → API 서버 호출 또는 Service 사용
  • 클러스터 전역 정보가 필요할 때 → API 서버 호출
  • 복잡한 오케스트레이션 → Operator 패턴 고려

 

  1.  

참고 자료

 

 

'k8s > kubernetes-pattern' 카테고리의 다른 글

Kubernetes Pattern: Sidecar  (4) 2025.11.22
Kubernetes Pattern: Init Conatiner  (2) 2025.11.15
Service Discovery 심화: Knative  (2) 2025.11.01
Kubernetes Pattern: Service Discovery  (4) 2025.10.25
Kubernetes Pattern: Stateless Service  (8) 2025.10.18