Kubernetes ConfigMap, Secret 깊게 이해하기
Kubernetes ConfigMap과 Secret의 설계 철학과 내부 동작 원리를 OS 프로세스 모델부터 kubelet 메커니즘까지 깊게 이해한다.
ConfigMap과 Secret이란?
ConfigMap과 Secret은 컨테이너화된 애플리케이션의 설정을 이미지와 분리하기 위해 탄생한 쿠버네티스 API 오브젝트로 이는 12-Factor App 원칙 중 하나인 Config(주요 설정은 환경변수로 저장하거나 저장소에 커밋되지 않는 파일에 저장한다.)철학을 쿠버네티스 환경에서 구현하는 핵심적인 방법이다.
ConfigMap을 알아보자
ConfigMap은 민감하지 않은 설정 데이터를 Key-Value 형태로 저장하는데 사용하는 API 오브젝트이다. Pod는 볼륨, ENV, 커맨드 라인 등으로 ConfigMap을 활용할 수 있다.
예를 들어 로깅 레벨 지정, 외부 서비스 엔드 포인트, 기능 플래그 등이 해당되며, ConfigMap을 사용하면 컨테이너 이미지에서 환경별 구성을 분리하여, 애플리케이션을 쉽게 이식할 수 있다.
추가로 ConfigMap은 많은 양의 데이터를 보유하도록 설계되지 않았다. 컨피그맵에 저장된 데이터는 1MiB를 초과할 수 없으며 이 제한보다 큰 설정을 저장해야 하는 경우, 볼륨 마운트하는 것을 고려하거나 별도의 데이터베이스 또는 파일 서비스를 사용할 수 있다.
Secret을 알아보자
Secret은 민감한 데이터를 저장하기 위해 특별히 설계되었다. API Key, 데이터베이스 패스워드, TLS 인증서, OAuth 토근 등이 여기에 해당된다. 따라서 Secret을 사용하면 사용자의 기밀 데이터를 애플리케이션에 넣을 필요가 없다.
주의사항
- Secret은 기본적으로 ETCD에 암호화되지 않고 평문으로 저장된다.
- 그렇기 때문에 API Access 권한이 있는 모든 사용자 또는 etcd에 접근할 수 있는 모든 사용자는 시크릿을 조회하거나 수정할 수 있다.
- 또한 네임스페이스에서 파드 생성 권한이 있는 사람은 누구나 해당 접근을 사용하여 해당 네임스페이스의 모든 시크릿을 읽을 수 있다.
권장되는 사용법
- Secret에 대해 Encryption at Rest를 활성화한다.
- Secret에 대한 최소한의 접근 권한을 지니도록 RBAC 규칙을 활성화 또는 구성한다.
- 특정 컨테이너에서만 시크릿에 접근하도록 한다.
- 외부 시크릿 저장소 제공 서비스를 사용하는 것을 고려한다.
스펙 정리
- type
Opaque:가장 일반적인 타입으로 정해진 형식 없는 임의의 데이터를 저장한다.kubernetes.io/service-account-token:ServiceAccount의 자격증명을 저장하며, 자동으로 생성되고 파괴된다.kubernetes.io/dockerconfigjson:Private Docker Registry 에 접근하기 위한 인증 정보를.docker/config.json형태로 저장한다. data 필드에 .dockerconfigjson 이라는 Key가 반드시 필요하다.kubernetes.io/basic-auth:기본 인증을 위한 자격증명을 저장한다. data 필드에 username과 password 키가 필요하다.kubernetes.io/ssh-auth:SSH 키 페어를 저장한다. data 필드에 ssh-privatekey, ssh-publickey 키가 필요하다.kubernetes.io/tls:TLS 인증서와 개인키를 저장한다. Ingress TLS Offloading 등에 사용된다. data 필드에 tls.crt, tls.key 키가 반드시 필요하다.
- data
- Secret의 실제 데이터를 key-value 형태로 저장하는 필드이다. value는 반드시 base64로 인코딩된 문자열이어야 하며, key 이름은 영문, 숫자,
-,_,.문자만 포함할 수 있다.
- Secret의 실제 데이터를 key-value 형태로 저장하는 필드이다. value는 반드시 base64로 인코딩된 문자열이어야 하며, key 이름은 영문, 숫자,
- stringdata
- data 필드의 편의성을 위한 필드이다. 여기에 key-value를 일반 문자열로 입력하면 api server가 이를 자동으로 base64 인코딩하여 data 필드에 반영한다. 이 필드는 쓰기 전용 이며,
kubectl get secret -o yaml등으로 조회하면stringData필드는 보이지 않고 data 필드에 인코딩된 값만 나타난다.
- data 필드의 편의성을 위한 필드이다. 여기에 key-value를 일반 문자열로 입력하면 api server가 이를 자동으로 base64 인코딩하여 data 필드에 반영한다. 이 필드는 쓰기 전용 이며,
왜 Secret과 Config를 분리하였을까?
Config나 Secret은 Pod 입장에서 주입받는 방법이나 활용 방법이 크게 다르지 않다. 그렇다면 쿠버네티스는 왜 두 Workload를 분리하여 운영할까?
1) 최소 권한:
- RBAC을 통해 Secret 같은 민감 데이터는 접근 제어가 가능해진다. 만약, ConfigMap에 모든 데이터가 다 담긴다면 노출하고 싶지 않은 데이터를 방지할 방법이 복잡해진다.
- 또, 그렇다고 ConfigMap 자체에 접근제한을 걸면 ConfigMap을 공유하거나 이용해야 하는 경우 복잡성이 높아진다. 따라서 ConfigMap에는 민감하지 않는 데이터를 담아 불필요한 접근제어를 제거하고 Secret에만 접근 제어를 진행하여 명확하게 권한 기반으로 제어할 수 있게된다.
2) 확장성 및 생태계 통합:
- Secret을 별도의 API Kind로 정의한 덕분에 HashiCorp Vault나 AWS Secrets Manager 같은 외부 시스템은 Secret 객체만을 명확한 대상으로 삼는 컨트롤러(예: External Secrets Operator)를 통해 쿠버네티스와 매끄럽게 연동될 수 있다.
- 결과적으로, 이는 단순히 Secret을 외부에 저장하는 것을 넘어, 동적 Secret 생성, 자동 순환, 중앙화된 감사 등 쿠버네티스 네이티브 기능만으로는 어려운 고급 보안 관리 기능을 위임하여 클러스터에 확장할 수 있게 만들어준다.
ENV와 Secret의 주입 방식 이해
ConfigMap과 Secret을 Pod의 Container에 전달하는 방법은 크게 2가지 방법이 있으며 각 사용의 장단이 뚜렷하다.
1) 환경 변수로 주입
env 또는 envFrom 필드를 사용해 ConfigMap 이나 Secret의 key-value를 컨테이너 환경 변수로 직접 매핑할 수 있다.
장점
- 사용이 가장 간편하고, 대부분의 애플리케이션의 추가적인 코드 수정 없이 환경 변수로부터 설정을 읽을 수 있다.
단점
- Pod이 시작된 후에는 ConfigMap 이나 Secret의 변경 사항이 반영되지 않는다. 업데이트를 적용하려면 파드를 재시작해야 한다.
- 환경 변수는 /proc/[pid]/environ 파일을 통해 Pod 내 다른 프로세스나 권한 있는 사용자에게 노출될 수 있어 보안적으로 덜 안전하다.
예시
# 예시: 특정 key를 선택하여 환경 변수로 주입
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: redis
env:
- name: MY_ENV_VARIABLE
valueFrom:
configMapKeyRef:
name: my-configmap # 참조할 ConfigMap 이름
key: my-key # 사용할 key
2) 볼륨 마운트로 주입
ConfigMap이나 Secret을 정의하고, 이를
volumeMounts를 통해 컨테이너 내부 특정 경로에 파일 형태로 마운트한다. 각 key는 파일명이 되고, value는 해당 파일의 내용이된다.
장점
- Hot Reload가 가능하다. ConfigMap 이나 Secret이 업데이트되면, 잠시 후(kubelet의 동기화 주기에 맞게) Pod 재시작 없이도 컨테이너 내의 파일 내용이 자동으로 동기화된다.
단점
- 애플리케이션이 파일 시스템에서 설정을 읽도록 구현되어 있어야 한다.따라서 properties, conf, toml등의 파일을 볼륨 마운트로 Pod에 주입하여 사용하는 방법에 적합하다.
예시
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test-config
data:
APP_NAME: "MyTestApp"
DATABASE_HOST: "db.example.com"
DATABASE_PORT: "5432"
LOG_LEVEL: "INFO"
---
apiVersion: v1
kind: Pod
metadata:
name: test-pod-volume-only
spec:
containers:
- name: main-container
image: busybox
command: ["sh", "-c", "sleep 3600"]
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: test-config
restartPolicy: Never
그렇다면, 왜 두 가지 방식을 지원할까?
결론부터 말하면, 이는 '예측 가능한 안정성(Immutability)'과 '운영상의 유연성(Dynamic Update)' 이라는, 서로 다른 두 설계 목표를 모두 지원하기 위함이다.
1) env/envFrom 방식: 불변성을 통한 안정성 확보
이 방식으로 주입된 환경 변수는 컨테이너 시작 시 메모리에 주입되어 절대 변하지 않는 불변(Immutable) 값이 된다. 이는 "설정이 바뀌면, Pod를 새로 배포한다"는 불변 인프라(Immutable Infrastructure) 철학을 강제하여, 런타임 중 설정 변경으로 인한 예측 불가능한 오류를 원천적으로 차단한다.
하지만 이 '불변성'이라는 특징 때문에, Pod 재시작 없이는 설정을 변경할 수 없어 동적인 Hot Reload는 지원되지 않는다.
2) volumes/volumeMounts 방식: 동적 업데이트를 통한 유연성 확보
반면 이 방식은 ConfigMap/Secret을 파일 시스템의 리소스로 마운트한다. Kubelet이 원본 리소스의 변경을 감지하면 마운트된 파일을 원자적으로(atomically) 교체해주므로, 애플리케이션이 이를 감지해 Pod 재시작 없이 Hot Reload하는 것이 가능하다.
이는 로깅 레벨 변경이나 기능 플래그 전환처럼, 서비스 중단 없이 즉각적인 설정 변경이 필요한 운영상의 요구를 충족시키기 위한 강력한 유연성을 제공한다.
ENV 주입 과정 알아보기
환경 변수가 일단 주입된 후에 불변한 이유는 쿠버네티스의 정책은 아니고 OS의 프로세스 모델의 구조 때문이다. 컨테이너도 OS 관점에서 프로세스이기에 동일한 원리를 따른다. 보통 새로운 프로세스가 생성될 때, fork()와 execve() 라는 두 개의 System Call을 통해 이루어진다.
fork()를 실행하면 부모 프로세스는 자신과 똑같은 복제본인 자식 프로세스를 만들고 이 때 자식은 부모의 메모리, 파일 디스크립터, 그리고 환경 변수까지 복사해 물려 받는다.execve()자식 프로세스는 이 시스템 콜을 통해 자기 자신을 새로운 프로세스로 교체하는데, 이 함수를 호출 할 때, 새로운 프로세스에 전달할 초기 환경 변수 목록을 인자로 넘겨준다.
이 과정은 컨테이너가 시작될 때도 동일하게 일어난다.
kubelet(부모 프로세스) 역할:kubelet은 컨테이너 런타임에게 Pod Spec에 정의된 환경 변수 목록을 전달한다.컨테이너 런타임:이 정보를 받아 컨테이너 내부에서 실행될 메인 프로세스를 execve()로 실행하면서 전달받은 환경 변수들로 초기화된다.
Kubelet의 동작 원리 - Deep Dive
Kubelet은 ConfigMap/Secret을 컨테이너에 전달하는 실질적인 역할을 하는데, 그 과정이 어떻게 되는지 조금 더 자세히 알아보자.
감시:각 노드의 kubelet은 Control Plane의 API Server를 지속적으로 Watch 하고 있다. 이 때 자신에게 할당된 (spec.nodeName이 자신의 노드 이름인 경우) Pod가 있는지 체크한다.Pod 인지 및 명세 확인:스케줄러에 의해 특정 Pod이 노드에 할당되면, kubelet은 해당 Pod의 명세를 API Server로 요청하여 읽어들인다. 이 때 어떤 ConfigMap이나 Secret을 필요로 하는지 파악한다.데이터 요청(Fetch):kubelet은 Pod 명세에 따라 필요한 ConfigMap과 Secret 객체의 내용을 API 서버에 명시적으로 요청하여 가져온다. 이 통신은 TLS로 암호화 되며, kubelet은 ServiceAccount나 인증서를 통해 API 서버로부터 인증/인가를 받아야한다.주입 메커니즘 실행
주입 메커니즘 실행
A. 환경 변수로 주입인 경우
- kubelet은 가져온 ENV(key-value 데이터)를 메모리에 유지한다.
- CRI에 컨테이너 생성 요청을 할 때 이 ENV 데이터를 컨테이너 설정의 환경 변수 부분에 포함하여 전달한다.
- 컨테이너 런타임은 이 정보를 받아 컨테이너의 첫 번째 프로세스를 실행할 때 환경 변수로 설정해준다.
B. 볼륨 마운트로 주입인 경우
- 호스트에 디렉토리 생성: kubelet은 호스트 노드의 특정 경로(일반적으론
/var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~configmap/<volume-name>) 에 디렉토리를 생성한다. - 데이터 기록: 가져온 데이터의 각 key를 파일이름으로, 각 value를 파일 내용으로 하여 해당 디렉토리에 파일을 생성한다.
- 심볼릭 링크 생성: kubelet은
..data(가칭)라는 이름의 심볼릭 링크를 만들어, 방금 생성한 타임스탬프 디렉토리를 가리키도록 한다. - 컨테이너 마운트: Kubelet이 컨테이너 런타임에 컨테이너 생성을 요청할 때, "호스트의 이 경로를 컨테이너 내부의 저 경로(mountPath)로 마운트하라"고 지시하고 컨테이너 런타임이 이 지시에 따라
..data디렉토리를 컨테이너 바인드 마운트한다.
왜 ENV는 불변하고 volumeMount 방식은 가능한가?
환경 변수는 execve() 함수에 의해 최초 초기화될 때 컨테이너나 프로세스에 각인되며 kubelet에 의해 컨테이너가 시작되면 격리되어 변경할 방법이 없다. 프로세스도 마찬가지로 실행되면 다른 프로세스로부터 격리된다. 따라서 환경 변수 값을 수정할 수 있는 방법을 OS 레벨에서 제공하지 않기에 변경할 방법이 없다.
만약, 이게 가능하다면 프로세스가 다른 프로세스의 PATH 환경 변수를 마음대로 바꾸거나, DB_PASSWORD 를 바꾸는 등의 끔찍한 보안 및 안정성 문제를 일으킬 수 있다. 따라서 프로세스 격리 원칙을 지키기 위해 ENV 값들은 불변하다.
볼륨 마운트 방식이 가능한 이유 - Hot reload 원리
반면, 볼륨 마운트를 통한 업데이트가 가능한 이유는 파일 시스템을 마운트 하는 방식이 처음부터 여러 프로세스 간 데이터 공유를 위해 설계된 표준적인 메커니즘이기 때문인데 조금더 자세히 알아보자
이유: 심볼릭 링크
- 위에서 말한 것처럼 컨테이너에 마운트되는 것은 심볼릭 링크로 원본 디렉토리를 바라보고 있다.
- 따라서 kubelet은 이를 이용해 심볼릭 링크를 원자적으로 업데이트 하는 방식을 사용하기 때문이다.
과정:
- 컨테이너에는
..data심볼릭 링크가 마운트 되어있다. (위에서 설명) - ConfigMap/Secret이 업데이트되면,
kubelet은 새로운 타임스탬프 디렉토리(2025.v2)를 만들고 새 데이터를 쓴 뒤,..data심볼릭 링크가 가리키는 대상을 새 디렉토리로 교체한다.
주입받은 환경 변수는 프로세스 메모리 중 어떤 영역에 존재할까?
결론:
환경 변수는 프로세스의 가상 메모리 중 Stack 세그먼트 위인 가장 높은 주소 영역에 위치한다.
컨테이너의 본질은 프로세스이다. 따라서 컨테이너의 환경 변수는 프로세스와 동일한 메모리 구조로 맵핑된다. 환경 변수는 프로세스의 가상 메모리 중 Stack 세그먼트 위인 가장 높은 주소 영역에 위치하며, 프로세스 생성 시 호출되는 execve()System Call 결과 다음과 같은 메모리 구조를 가지게된다.
+------------------------+ <-- Highest Memory Address
| Kernel Space | (프로세스에서 직접 접근 불가)
+------------------------+
| |
| Environment Variables |
| Command-line Arguments |
+------------------------+
| Stack (스택) | (아래 방향으로 자람)
| ↓ |
+------------------------+
| |
| (Unmapped Memory) |
| |
+------------------------+
| ↑ |
| Heap (힙) | (위 방향으로 자람)
+------------------------+
| BSS Segment |
+------------------------+
| Data Segment |
+------------------------+
| Text Segment (Code) |
+------------------------+ <-- Lowest Memory Address
| (Reserved) |
+------------------------+
그렇다면 왜 Stack 위일까?
불변의 값을 나열하는 것이면
Text Segment,Data Segment,BSS Segment사이나Heap아래여도 될 것 같은데 왜 Stack 위일까?
초기화 과정과 메모리 영역의 특성을 파악해보자
프로세스 메모리 영역 구성은 위에서 본 것처럼 Text, Data, BSS, Heap, Unmapped Memory, Stack, ENV 이렇게 구성되어 있다. 각, 영역의 특성을 이해하면 Stack 위에 위치하는 것이 합리적이다 라는 것을 알 수 있다.
우선, Text, Data, BSS 영역에 위치하지 않는 이유는 다음과 같다.
프로세스 생성 과정이 복잡해지고 생성 지연을 최소화 하기 위함
-
실행 파일과의 직접적인 매핑: 커널의 로더(loader)가 execve를 처리할 때, 디스크에 있는 실행 파일의 Text Segment와 Data Segment를 가상 메모리 공간으로 거의 그대로
mmap()을 통해 매핑한다. 즉, 파일에서의 연속된 바이트들이 메모리에서도 연속된 공간에 매핑되는데 이는 매우 빠르고 효율적인 작업이다. -
가변 크기 데이터의 문제: 만약 Text와 Data 세그먼트 사이에 가변 크기인 환경 변수 블록을 삽입한다고 가정해보자. 이때 커널이 거쳐야 하는 과정은 다음과 같다.
- Text 세그먼트를 메모리에 매핑한다.
- 이번에 실행되는 프로세스의 환경 변수 크기(envp의 총 길이)를 계산한다.
- 계산된 크기만큼의 빈 공간을 확보한다.
- 그 빈 공간 다음에 Data 세그먼트를 매핑한다.
이 과정은 실행 파일의 내용을 한 번에 쭉 매핑하는 것에 비해 훨씬 복잡하고 비효율적이다. 또한, Text와 Data 사이의 거리가 실행 시마다 달라져 디버깅과 프로파일링을 매우 어렵게 만든다.
-
메모리 권한 설정의 복잡성: Text 세그먼트는 보통 읽기/실행(r-x) 권한을, Data와 BSS는 읽기/쓰기(rw-) 권한을 가진다. 세그먼트 사이에 다른 데이터를 끼워 넣는 것은 페이지 단위로 권한을 관리하는 MMU의 설정을 복잡하게 만들고 추가적인 오버헤드를 유발하게 된다.
만약, Heap 아래 (즉, BSS와 Heap 사이)에 위치한다면?
기존 동작 원리
Text, Data, BSS 세그먼트 들은 컴파일 시점에 결정되면 링크 과정을 통해 메모리에 맵핑되며 정적 데이터 영역이 끝나는 지점의 주소를 _end 또는 __bss_end_ 와 같은 심볼로 파일 안에 기록해둔다.
또, 메모리 동적 할당을 담당하는 libc 의 malloc 구현을 보면 커널에게 물어볼 필요 없이 실행 파일 안에 기록된 _end 심볼의 주소를 읽어, 정적 데이터의 끝점을 알게되고 힙의 시작점을 고정할 수 있다.
그런데 BSS와 Heap 사이에 ENV (동적인 사이즈)가 배치된다면 libc의 malloc 초기화 과정에 힙의 시작점을 알기 위해 반드시 커널과 소통해야 한다. (libc는 유저 스페이스의 라이브러리이고, ENV 초기화는 execve 라는 커널 영역의 책임이다.)
- 어떻게? 커널이 execve의 결과로 새로운 힙 시작 주소를 프로세스에게 넘겨주거나, libc가 "내 힙은 어디서 시작합니까?"라고 묻는 새로운 시스템 콜을 호출해야 한다.
이 방식은 커널과 libc 사이에 의존성을 만들고 커널은 유저 스페이스 라이브러리인 malloc의 구현 방식을 고려해야 하며, libc는 커널이 제공하는 새로운 인터페이스에 의존해야 한다. 이렇게 되면 전체적인 시스템의 복잡도가 증가하고 유연성이 떨어진다.
왜 스택은 괜찮은가?
- 스택(Stack):
- 관리 주체: CPU(하드웨어) 와 커널
- 관리 방식: ESP/RSP 라는 스택 포인터(Stack Pointer) CPU 레지스터를 통해 관리됩니다.
- 초기화: execve 시점에 커널이 환경 변수, 인자 등을 모두 스택의 최상단에 쌓은 후, 그 최종 주소를 RSP 레지스터에 딱 한번 설정해 줍니다.
- 이후 동작: 그 후부터는 프로그램의 call, ret, push, pop 같은 기계어 명령어에 의해 RSP 레지스터 값이 하드웨어 레벨에서 자동으로 증가하거나 감소합니다. 유저스페이스 라이브러리가 스택의 시작점을 알 필요가 없습니다. CPU가 알아서 다 해줍니다.
- 힙(Heap):
- 관리 주체: libc (소프트웨어)
- 관리 방식: malloc, free 함수 내부의 복잡한 알고리즘(free list, bins 등)을 통해 관리됩니다. 힙에는 CPU 레지스터 같은 하드웨어 지원이 없습니다. 순수 소프트웨어의 영역입니다.
- 초기화: libc는 자신의 소프트웨어 자료구조를 초기화하기 위해 "힙이 어디서부터 시작되는가" 라는 기준점 정보가 반드시 필요합니다. 이 정보를 가장 간단하고 독립적으로 얻는 방법이 바로 컴파일 타임에 약속된 _end 심볼을 사용하는 것입니다.
요약하자면, 스택은 커널이 하드웨어(CPU 레지스터)에 초기값을 설정해주면 그만이지만, 힙은 소프트웨어(libc)가 자신의 관리 체계를 시작하기 위해 고정된 기준점이 필요하다.
왜 Secret은 Base64로 인코딩 할까?
Secret 데이터 base64로 인코딩하는 것은 보안적인 측면에서 불안 요소이다. 그럼 왜 base64로 인코딩 하는 형태로 설계되었을까?
이 고민의 핵심 base64로 인코딩 하는 것이 보안과는 관련이 없다는 점을 파악하는 것이다. 결론은 Secret의 data 필드는 임의의 바이너리 데이터를 담을 수 있게 설계되었기 때문에 base64 기반의 인코딩을 지원한다.
YAML이나 JSON 형식의 매니페스트 파일은 기본적으로 Text 기반인데 줄바꿈이나 null 문자(\0) 또는 기타 제어 문자가 포함된 바이너리 데이터(예: id_rsa 개인키, .p12 인증서 파일 등)를 그대로 넣으면 파싱 오류가 발생하거나 데이터 변형이 초래될 수 있다.
이런 문제를 해결하고자 base64를 활용한다. base64는 모든 바이너리 데이터를 ASCII로 변환하고 이렇게 변환된 문자열은 YAML/JSON 파일에 아무런 문제 없이 포함될 수 있다.
왜 기본적으로 암호화하지 않았을까?
유연성과 확장성을 중시하기 때문
쿠버네티스는 핵심 설계 원칙 중 하나로 확장 가능성을 내세우고 있다. 만약, 쿠버네티스가 특정 암호화 방식을 내장하여 강제했다면 사용자들은 환경에 맞는 최적의 KMS(Key Management Solution)를 도입하는데 큰 제약을 겪게될 것이다.
대신 쿠버네티스는 Encryption at Rest 라는 플러그형 메커니즘을 제공한다. 사용자는 자신의 환경에 맞는 KMS Provider를 선택하여 API Server와 연동할 수 있고 이를 통해 쿠버네티스는 특정 기술에 종속되지 않으면서도, 강력한 보안을 지원할 수 있다.
결론
Kubernetes의 ConfigMap과 Secret은 단순히 설정과 민감 정보를 컨테이너에 전달하는 수단을 넘어, 클라우드 네이티브 환경의 핵심 설계 철학을 담고 있는 중요한 API 오브젝트이다. 이 글을 통해 우리는 표면적인 사용법에서 시작하여, 두 리소스가 분리된 이유, 두 가지 주입 방식이 제공하는 상반된 가치, 그리고 이 모든 것이 운영체제의 프로세스 모델과 메모리 구조에 어떻게 뿌리내리고 있는지 깊이 이야기해보았다.
이 과정에서 알게된 것들은 다음과 같다. ConfigMap과 Secret의 본질
- ConfigMap과 Secret은 12-Factor App의 설정 외부화 원칙을 구현하는 Kubernetes API 오브젝트이다.
- 두 리소스의 분리는 RBAC을 통한 접근 제어 차별화와 보안 정책의 세분화를 위함이다.
- Secret의 Base64 인코딩은 암호화가 아닌 바이너리 데이터 호환성을 위한 인코딩 방식이다.
두 가지 주입 방식의 근본적 차이
- 환경 변수 방식:
execve()시스템 콜에 의해 프로세스 시작 시점에 고정되어 불변성(Immutability)을 보장한다. - 볼륨 마운트 방식: Kubelet의 symlink 교체를 통해 동적 업데이트(Hot Reload)가 가능하며 유연성을 제공한다.
- 이 두 방식은 '안정성 vs 유연성'이라는 상반된 설계 목표를 모두 지원하기 위한 의도적인 선택이다.
프로세스 메모리와 환경 변수의 관계
- 환경 변수는 프로세스 메모리의 Stack 세그먼트 위 가장 높은 주소 영역에 위치한다.
- execve() 호출 시 커널이 환경 변수를 메모리에 복사하며, 이후 프로세스 생명주기 동안 변경되지 않는다.
- Text, Data, BSS 세그먼트는 컴파일/링크 타임에 크기가 결정되므로, 런타임에 결정되는 환경 변수와는 별도 영역에 배치된다.
Kubelet의 동작 메커니즘
- Kubelet은 API 서버를 지속적으로 Watch하여 ConfigMap/Secret 변경을 감지한다.
- 볼륨 마운트 시 새로운 타임스탬프 디렉터리를 생성하고 symlink를 원자적으로 교체하여 Hot Reload를 구현한다.
- 이 방식은 파일 시스템 수준에서 일관성을 보장하며, 애플리케이션이 부분적으로 업데이트된 설정을 읽는 것을 방지한다.
보안과 확장성 설계
- Secret은 기본적으로 ETCD에 암호화되지 않고 평문으로 저장되지만, Encryption at Rest를 통해 강화할 수 있고 권장된다.
- External Secrets Operator와 같은 확장 도구를 통해 HashiCorp Vault, AWS Secrets Manager 등과 통합 가능하다.
- 이는 Kubernetes가 특정 보안 솔루션을 강요하지 않고 확장 가능한 플랫폼을 지향한다는 설계 철학을 보여준다.
운영 환경에서의 고려사항
- 환경 변수는 프로세스 격리를 위해 불변하지만, 이로 인해 동적 설정 변경이 불가능하다.
- 볼륨 마운트는 Hot Reload가 가능하지만, 애플리케이션이 파일 변경을 감지하고 처리하는 로직이 필요하다.
- 두 방식 모두 장단점이 있으므로, 애플리케이션의 특성과 운영 요구사항에 따라 적절한 방식을 선택해야 한다.