근묵자흑
Kubernetes Pattern: Self Awareness 본문
쿠버네티스 환경에서 애플리케이션을 실행하다 보면 이런 고민을 하게 됩니다:
- "내 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: podIP와 nodeName은 반드시 환경변수로 접근해야 합니다!
실습 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: 볼륨 파일이 업데이트되지 않음
원인:
- 환경변수로 설정됨 (환경변수는 업데이트 안 됨)
- kubelet sync 주기 (60-120초 소요)
- 지원하지 않는 필드 (
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]
요약
- 간단함: API 클라이언트 라이브러리 불필요
- 빠름: API 서버 호출 없이 로컬 접근
- 확장성: 수천 개의 Pod도 API 서버 부하 없음
- 유연함: 정적(env)과 동적(volume) 접근 모두 지원
- 범용성: 모든 프로그래밍 언어에서 동일하게 사용
언제 사용해야 하는가?
O 적합한 경우:
- 로그/메트릭에 Pod 정보 포함
- 리소스 제한에 따른 애플리케이션 튜닝
- Feature flag 동적 변경
- 서비스 디스커버리
- 멀티 컨테이너 간 정보 공유
X 부적절한 경우:
- 다른 Pod의 정보가 필요할 때 → API 서버 호출 또는 Service 사용
- 클러스터 전역 정보가 필요할 때 → API 서버 호출
- 복잡한 오케스트레이션 → Operator 패턴 고려
참고 자료
- Kubernetes 공식 문서 - Downward API
- Kubernetes Patterns 2nd Edition
'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 |