Kubernetes Practice 10

ConfigMap/Secret과 Pod 설정 주입 메커니즘

환경 변수, 파일 마운트, key/value가 한꺼번에 나오면 ConfigMap이 단순 메모장인지 Pod 설정 주입 장치인지 흐릿하다.

근거 · Kubernetes 교안 p107-p131, yaml/cm/*.yaml

ConfigMap/Secret과 Pod 설정 주입 메커니즘 대표 이미지

1장: ConfigMap은 단순한 메모장이 아니다

“ConfigMap과 Secret은 컨테이너 이미지와 설정 값을 분리할 수 있습니다.”

솔라는 방금 읽은 이 문장을 조용히 되뇌었다. 분리. 그건 알겠다. 개발할 때 쓰는 설정 값들을 애플리케이션 코드나 컨테이너 이미지 안에 박아두지 않고, 밖으로 빼내서 따로 관리한다는 뜻이겠지. 그런데 그 ‘밖’이라는 게 대체 어디며, 분리된 설정 값들은 대체 ‘어떻게’ 다시 컨테이너 안으로 들어와서 일을 하는 걸까?

솔라의 머릿속에서 ConfigMap은 그저 설정 값을 적어두는 깔끔한 메모장 정도의 이미지였다. 데이터베이스 주소, API 키, 로그 레벨 같은 것들을 KEY=VALUE 형식으로 차곡차곡 저장해두는 곳. 하지만 Pod가 실행될 때, 그 안에 있는 컨테이너가 이 메모장을 어떻게 보고 필요한 값을 꺼내 쓰는 것인지, 그 연결고리가 보이지 않았다. 마치 서로 다른 방에 있는 메모장과 사람 같았다. 사람이 메모장의 존재를 안다고 해서 저절로 그 내용이 머릿속에 입력되는 건 아니니까.

“언니, 이거 좀 이상해.”

노트북 화면을 들여다보던 솔라가 등을 돌리며 루나에게 말했다.

“ConfigMap이 그냥 설정 값을 저장하는 곳이라면, Pod랑은 아무 상관없는 거 아니야? Pod를 만들 때 이 ConfigMap을 쓰라고 어떻게 알려주지? 그냥 이름만 같으면 알아서 연결되는 건 아닐 테고…”

솔라의 생각은 타당했다. ConfigMap은 그저 키-값 저장소일 뿐 Pod와 직접적인 연결 고리가 없다는 생각. 그건 많은 사람이 처음 Kubernetes를 접할 때 하는 자연스러운 오해였다.

루나는 솔라의 화면을 잠시 들여다보더니, 아무 말 없이 자신의 노트북을 열었다. 그리고는 짧은 명령어 한 줄을 터미널에 입력할 준비를 했다.

“솔라, 우리 딱 한 가지만 해보자. 지금부터 Pod는 잠시 잊어. Pod도, 컨테이너도 없는 텅 빈 공간에 그 ‘메모장’부터 하나 만들어보는 거야.”

솔라는 루나의 말에 고개를 갸웃했지만, 이내 자기 노트북의 터미널 창을 활짝 열었다.

“메모장을… 만든다고?”

“응. my-config라는 이름의 ConfigMap을 만들 거야. LOG_LEVELinfo로, k8sstart라는 값을 갖도록.”

루나가 말한 내용을 담은 명령어는 다음과 같았다.

kubectl create configmap my-config --from-literal=LOG_LEVEL=info --from-literal=k8s=start

솔라는 명령어를 그대로 따라 입력하고 엔터를 쳤다.

configmap/my-config created

created. 생성되었다는 메시지가 떴다. 하지만 솔라의 표정은 여전히 물음표투성이였다. “그래서… 이게 다야? 어딘가에 my-config라는 메모장이 만들어졌다는 건데, 이걸로 뭘 알 수 있지?”

“이제 그 메모장이 어떻게 생겼는지, 어디에 있는지 확인해봐야지. 사람을 찾을 때 인적 사항을 조회하듯이 말이야.”

루나는 describe 명령어를 사용해보라고 힌트를 주었다.

솔라는 곧장 다음 명령어를 입력했다.

kubectl describe configmap my-config

터미널 화면에 my-config의 ‘인적 사항’이 주르륵 출력되었다.

Name:         my-config
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
LOG_LEVEL:
----
info
k8s:
----
start

Events:  <none>

솔라는 출력을 가만히 들여다보았다. Name, Namespace, 그리고 Data 섹션. Data 아래에는 방금 자신이 입력했던 LOG_LEVEL=infok8s=start가 보기 좋게 정리되어 있었다.

“아…”

솔라의 입에서 나지막한 탄성이 터져 나왔다.

“그냥 파일이나 메모장이 아니구나. 이건… 쿠버네티스가 정식으로 관리하는 객체(Object)였어. 이름(my-config)도 있고, 소속(default 네임스페이스)도 있고. 우리가 Pod를 만들 때 kind: Pod라고 쓰는 것처럼, 이것도 kind: ConfigMap인 독립적인 자원인 거네.”

“맞아.”

루나가 처음으로 입을 열었다.

“중요한 건 바로 그거야. ConfigMap은 Pod에 종속된 무언가가 아니야. Pod와는 별개로, 클러스터 안에 존재하는 독립적인 객체지. 언제든 누군가 이름으로 불러주기만을 기다리면서.”

솔라는 다시 화면을 보았다. Name: my-config. 이제 이 이름이 다르게 보였다. 단순한 이름표가 아니라, Kubernetes 클러스터 안에서 유일하게 식별될 수 있는 주소처럼 느껴졌다. Pod는 이 주소를 통해 ConfigMap을 찾아올 수 있을 것이다.

그렇다면 이제 남은 질문은 하나다.

“좋아. ConfigMap이 독립적인 객체라는 건 알겠어. 그럼 이제 Pod가 이 my-config라는 이름의 객체를 어떻게 참조해서, 저 infostart 값을 자기 것처럼 쓸 수 있는 걸까? 방법이 여러 가지 있을 것 같은데…”

솔라의 머릿속에 있던 ‘메모장’의 이미지가 ‘참조 가능한 주소를 가진 객체’로 바뀌는 순간이었다. 이제야 비로소, 분리된 설정이 어떻게 다시 컨테이너와 연결될 수 있는지, 그 구체적인 ‘주입’ 메커니즘을 파고들 준비가 된 것이다.

2장: ConfigMap 전체를 환경 변수로 받아오는 방법

솔라는 화면 한쪽에 지난번에 확인했던 my-config ConfigMap의 내용을 띄워놓고, 다른 한쪽에는 새로운 Pod YAML 파일을 열었다. Name: my-config. 이제 이 객체를 Pod 안으로 불러들일 차례였다. 방법이 여러 가지 있을 거라는 루나의 암시에, 솔라는 가장 직관적인 방법부터 떠올렸다. 컨테이너를 정의할 때 환경 변수를 직접 설정하는 env 필드를 사용하는 것이다.

“컨테이너에 환경 변수를 넣으려면… env 필드를 쓰고, namevalue를 지정하면 됐지.”

솔라는 키보드를 두드리기 시작했다. 하지만 이내 손을 멈췄다.

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: busybox
    command: ["sleep", "3600"]
    env:
    - name: LOG_LEVEL
      value: "info" # <-- 이 부분을 ConfigMap에서 가져와야 하는데...
    - name: k8s
      value: "start" # <-- 이것도...

“이렇게 값을 직접 복사해 넣으면… ConfigMap을 쓰는 의미가 없잖아. 설정 값을 분리한 게 아니라 그냥 한번 더 베껴 쓴 꼴인데.”

솔라의 미간이 좁아졌다. 만약 ConfigMap에 키가 열 개, 스무 개가 있다면 이걸 전부 손으로 YAML 파일에 옮겨 적어야 한단 말인가? 그건 너무 비효율적이었다. 분명히 각 키를 하나씩 참조하는 방법이 있을 것이고, 어쩌면 모든 키를 한 번에 가져오는 방법도 있을지 모른다고 생각했다. 하지만 지금 당장 머릿속에 떠오르는 건 각 값을 일일이 지정해주는, 수고스러운 그림뿐이었다.

그때, 솔라의 혼잣말을 들은 루나가 화면을 들여다보며 말했다.

“그 ConfigMap의 모든 데이터를 컨테이너의 환경 변수로 쓰고 싶다면, 그렇게 하나씩 짐을 옮길 필요는 없어. 이사할 때 쓰는 큰 상자 같은 방법이 있거든.”

루나는 솔라가 작성하던 env 필드를 지우고, 그 자리에 다른 코드를 채워 넣었다. env가 아닌 envFrom이라는, 조금 낯선 필드였다.

apiVersion: v1
kind: Pod
metadata:
  name: all-env-from-configmap-pod
spec:
  containers:
  - name: my-container
    image: busybox
    command: ["sleep", "3600"]
    envFrom:
    - configMapRef:
        name: my-config

솔라는 코드를 유심히 살폈다. “어? env가 아니라 envFrom이네. 그리고 그 안에 configMapRefmy-config 이름을 바로 지정했어.”

“맞아. envFrom은 말 그대로 ‘환경 변수를 가져올 출처’를 지정하는 거야. configMapRef는 그 출처가 ConfigMap이라는 뜻이고. 이렇게 하면 쿠버네티스가 my-config ConfigMap 안에 있는 모든 키-값 쌍을 찾아서, 컨테이너의 환경 변수로 자동으로 만들어줘.”

솔라의 눈이 반짝였다. 일일이 값을 나열해야 할 것이라는 자신의 예상이 보기 좋게 빗나갔다. 솔라는 파일 이름을 all-env-from-configmap.yaml로 저장하고 바로 클러스터에 적용했다.

kubectl apply -f all-env-from-configmap.yaml

pod/all-env-from-configmap-pod created 메시지가 뜨자마자, 솔라는 컨테이너 내부 환경을 확인하기 위해 exec 명령어를 입력했다. 정말로 ConfigMap의 모든 데이터가 환경 변수로 들어갔을까?

kubectl exec -it all-env-from-configmap-pod -- /bin/sh

컨테이너의 셸 프롬프트가 뜨자, 솔라는 지체 없이 env 명령어를 쳐서 모든 환경 변수를 출력했다.

/ # env
... (기본 환경 변수들) ...
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
LOG_LEVEL=info
KUBERNETES_SERVICE_HOST=10.96.0.1
HOSTNAME=all-env-from-configmap-pod
SHLVL=1
HOME=/root
k8s=start
... (더 많은 변수들) ...
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/

“와!”

솔라는 출력된 목록 중간에서 익숙한 이름들을 발견하고 외쳤다.

“정말이네! LOG_LEVEL=infok8s=start가 그대로 들어왔어! 내가 한 거라곤 ConfigMap 이름(my-config)을 알려준 것뿐인데.”

ConfigMap의 키(LOG_LEVEL, k8s)가 그대로 환경 변수의 이름이 되고, 값(info, start)이 환경 변수의 값이 되어 주입된 것을 두 눈으로 확인한 순간이었다. ConfigMap이 단순한 데이터 저장소를 넘어, Pod와 유연하게 결합하는 ‘주입 장치’라는 사실이 명확해졌다. envFrom은 마치 설정 값들을 담은 상자를 통째로 컨테이너 환경에 쏟아붓는 것과 같았다.

솔라는 만족스러운 표정으로 컨테이너 셸을 빠져나왔다. 이제 ConfigMap의 내용을 한 번에 환경 변수로 가져오는 방법은 확실히 알게 되었다. 그런데 문득 새로운 의문이 고개를 들었다.

“언니, 이건 정말 편한데… 만약에 ConfigMap에 키가 100개 있는데, 나는 딱 하나만 필요하다면 어떡하지? 아니면 LOG_LEVEL이라는 키를 APP_LOG_LEVEL이라는 다른 이름의 환경 변수로 쓰고 싶을 수도 있잖아. 이 방법은 그런 세밀한 조작은 안 되는 건가?“

3장: ConfigMap에서 원하는 환경 변수만 골라 담는 방법

솔라는 방금 전 성공의 흔적이 남아있는 터미널 화면과 Pod YAML 파일을 번갈아 보았다. envFrom. ConfigMap의 모든 내용을 한 번에 컨테이너 안으로 쏟아붓는, 시원하고 강력한 방법이었다. 하지만 솔라의 손가락은 이미 all-env-from-configmap.yaml 파일의 복사본을 만들고 있었다. 파일 이름은 selective-env-from-configmap.yaml. 마음은 벌써 다음 질문으로 넘어가 있었다.

‘전부 다 가져오는 건 알겠어. 그럼 딱 하나만, 그것도 다른 이름으로 가져오고 싶을 땐 어떻게 하지?’

솔라는 새로 만든 YAML 파일을 열고 envFrom 섹션을 뚫어지라 쳐다봤다. 여기에 필터 같은 옵션을 추가할 수 있을까? includeKeys: [LOG_LEVEL] 같은 식으로? 아니면 excludeKeys? 하지만 Kubernetes 명세에서 그런 필드를 본 기억이 없었다. 그녀는 잠시 고민하다 envFrom 블록을 지우고, 대신 처음 시도했던 env 필드를 다시 적기 시작했다.

    env:
    - name: APP_LOG_LEVEL
      value: ??? # my-config의 LOG_LEVEL 값을 여기에...

다시 원점이었다. value 필드에 값을 직접 넣는 순간 ConfigMap과의 동적인 연결은 끊어진다. 솔라는 키보드에서 손을 떼고 의자 등받이에 몸을 기댔다.

“이건… Pod를 만들기 전에 어떤 스크립트를 실행해서 my-config에서 LOG_LEVEL 값을 읽어온 다음, 그 값으로 이 YAML 파일의 ??? 부분을 채워 넣고 kubectl apply를 실행하라는 뜻인가? 너무 복잡한데. 그런 외부 도구가 꼭 필요할까?”

“모든 짐을 옮길 때 큰 상자를 쓰는 게 편하지만, 책상 위 연필 한 자루를 옮길 땐 손으로 집는 게 낫지.”

솔라의 혼잣말을 듣고 있던 루나가 조용히 말했다.

“이삿짐 상자(envFrom)를 버리고 다시 손(env)을 쓰려는 건 좋은 방향이야. 다만 연필을 어떻게 집어야 할지 방법을 모를 뿐이지.”

루나는 솔라가 작성하던 YAML 파일로 시선을 옮겼다. 그녀는 value 필드를 지우는 대신, 그 아래에 새로운 필드를 추가했다.

“컨테이너에 필요한 환경 변수의 이름은 APP_LOG_LEVEL이 맞아. 하지만 그 ‘값’이 고정된 문자열이 아니라, 다른 곳에서 ‘가져와야 할 값’이라는 걸 알려줘야 해. 그럴 땐 value 대신 valueFrom을 쓰는 거야.”

루나의 설명에 따라 솔라의 YAML 파일이 수정되었다.

apiVersion: v1
kind: Pod
metadata:
  name: selective-env-pod
spec:
  containers:
  - name: my-container
    image: busybox
    command: ["sleep", "3600"]
    env:
    - name: APP_LOG_LEVEL # 컨테이너가 사용할 최종 환경 변수 이름
      valueFrom:
        configMapKeyRef:
          name: my-config    # 값을 가져올 ConfigMap의 이름
          key: LOG_LEVEL      # ConfigMap 내부에서 가져올 키

솔라는 새로운 구조를 유심히 뜯어보았다. env 필드는 그대로 사용하지만, 그 안의 내용이 완전히 달랐다. “valueFrom… ‘~로부터 값을 가져온다’는 뜻이네. 그리고 그 출처가 configMapKeyRef, 즉 ConfigMap의 특정 키라는 걸 명시하는 거구나. name으로 ConfigMap을 지정하고, key로 그 안의 키를 지정하고… 그럼 LOG_LEVEL의 값인 infoAPP_LOG_LEVEL이라는 새 이름의 환경 변수 값으로 들어가는 거네!”

마치 퍼즐 조각이 맞춰지는 느낌이었다. 복잡한 스크립트나 외부 도구가 필요할 것이라는 예상은 완전히 빗나갔다. 필요한 모든 도구는 이미 Kubernetes의 Pod 명세 안에 준비되어 있었다.

솔라는 신이 나서 k8s 키도 APP_K8S_NAME이라는 이름으로 추가했다.

# selective-env-from-configmap.yaml
apiVersion: v1
kind: Pod
metadata:
  name: selective-env-pod
spec:
  containers:
  - name: my-container
    image: busybox
    command: ["sleep", "3600"]
    env:
    - name: APP_LOG_LEVEL
      valueFrom:
        configMapKeyRef:
          name: my-config
          key: LOG_LEVEL
    - name: APP_K8S_NAME
      valueFrom:
        configMapKeyRef:
          name: my-config
          key: k8s

“좋아, 이제 이 Pod를 실행하면 APP_LOG_LEVELAPP_K8S_NAME이라는 환경 변수 두 개만 생기겠지? 원래 이름인 LOG_LEVEL이나 k8s는 보이지 않을 거고.”

솔라는 확신에 찬 목소리로 말하며 파일을 저장하고 kubectl apply 명령을 실행했다. Pod가 성공적으로 생성되자, 그녀는 망설임 없이 컨테이너 내부로 들어갔다.

kubectl exec -it selective-env-pod -- /bin/sh

그리고 바로 env 명령어를 입력했다.

/ # env | grep APP
APP_LOG_LEVEL=info
APP_K8S_NAME=start

“됐다!”

솔라의 외침에 힘이 실렸다. 터미널은 그녀의 예상을 정확하게 증명해주었다. my-config ConfigMap에서 LOG_LEVELk8s 키의 값을 가져와, APP_LOG_LEVELAPP_K8S_NAME이라는 완전히 새로운 이름의 환경 변수로 주입하는 데 성공했다. 거대한 덩어리를 통째로 옮기는 envFrom과는 다른, 정교하고 섬세한 제어였다.

솔라는 Pod Spec의 valueFromconfigMapKeyRef 조합이 마치 원하는 재료만 정확히 골라 장바구니에 담는 것 같다고 생각했다. 심지어 장바구니에 담을 때 이름표를 새로 붙일 수도 있다니, 대단히 유연한 방식이었다.

컨테이너 셸을 빠져나온 솔라는 만족스럽게 의자에 기댔다. 하지만 그 만족감은 이내 또 다른 호기심으로 바뀌었다.

“언니, 이제 환경 변수는 완벽하게 이해했어. 통째로 넣는 법, 골라서 넣는 법, 이름 바꿔서 넣는 법까지. 그런데… 만약 설정 값이 ‘LOG_LEVEL=info’ 같은 짧은 문자열이 아니라, 수백 줄짜리 JSON 설정 파일이나 인증서 파일이라면 어떡하지? 그걸 통째로 환경 변수에 넣는 건 좀… 이상한데. 파일은 파일 형태로 컨테이너 안에서 쓸 수 있으면 제일 좋을 것 같아.”

4장: ConfigMap을 파일 시스템에 통째로 마운트하는 방법

솔라는 새로 연 YAML 파일의 빈 화면을 멍하니 바라보고 있었다. 머릿속은 온통 방금 전 루나에게 던졌던 질문으로 가득 차 있었다. 수백 줄짜리 JSON 설정 파일, 혹은 PEM 형식의 인증서. 이런 것들을 환경 변수에 통째로 넣는 것은 어딘가 부자연스럽고 관리하기도 까다로워 보였다. 파일은 파일 그대로 컨테이너 안에서 경로를 통해 접근하는 것이 가장 깔끔한 방법일 것이다.

“파일은… 파일로…”

솔라는 spec.containers 아래에 커서를 옮겨놓고 잠시 고민에 빠졌다. Pod에 파일을 전달하는 방법. 어렴풋이 볼륨(Volume)이라는 개념이 떠올랐다. 로컬 디렉터리나 클라우드 스토리지를 Pod에 연결하는 것처럼, ConfigMap 자체를 하나의 볼륨처럼 다룰 수는 없을까?

솔라는 나름의 가설을 세우고 코드를 작성하기 시작했다. 아마도 volumeMounts 같은 필드를 써서, 컨테이너 안의 특정 파일 경로를 지정하고, 그 대상이 my-config ConfigMap이라고 알려주면 될 것 같았다.

# 솔라's initial guess
...
    volumeMounts:
    - name: my-config-file
      mountPath: /etc/config/app.conf # 이 파일 안에 ConfigMap 내용이 전부 들어갈 거야!
...

“언니, 이렇게 하면 되려나? mountPath/etc/config/app.conf라고 파일 경로까지 다 지정해 주면, 쿠버네티스가 my-config에 있는 LOG_LEVEL=infok8s=start 내용을 전부 긁어모아서 app.conf라는 파일 하나로 만들어주지 않을까?”

마치 여러 개의 메모를 한 장의 종이에 옮겨 적는 그림을 상상하며 솔라가 말했다. 그녀의 예상은 ConfigMap의 모든 키-값이 하나의 큰 설정 파일로 통합될 것이라는 기대에 바탕을 두고 있었다.

“거의 다 왔어. 그런데 딱 한 가지, 쿠버네티스가 생각하는 방식과 솔라 네가 생각하는 방식이 다른 점이 있어.”

루나는 솔라의 화면을 가리키며 말했다.

“쿠버네티스에게 볼륨은 ‘저장 공간’ 그 자체를 의미하고, 볼륨 마운트는 그 저장 공간을 컨테이너의 특정 ‘위치(디렉터리)’에 연결하는 작업이야. 즉, mountPath는 보통 파일이 아니라 디렉터리를 가리키지. 그리고 ConfigMap을 볼륨으로 사용하면, 쿠버네티스는 그 안에 담긴 각 항목을 별개의 파일로 취급해.”

루나는 솔라의 YAML 파일에 두 개의 섹션을 추가하고 수정하도록 안내했다. 하나는 Pod 명세 최상단 레벨의 volumes였고, 다른 하나는 컨테이너 명세 아래의 volumeMounts였다.

# volume-mount-configmap.yaml
apiVersion: v1
kind: Pod
metadata:
  name: volume-mount-pod
spec:
  # 1. Pod 수준에서 "config-volume"이라는 이름의 볼륨을 정의한다.
  #    이 볼륨의 소스는 "my-config"라는 이름의 ConfigMap이다.
  volumes:
  - name: config-volume
    configMap:
      name: my-config
  containers:
  - name: my-container
    image: busybox
    command: ["sleep", "3600"]
    # 2. 컨테이너 내부에서 위에서 정의한 "config-volume"을
    #    "/etc/config" 디렉터리에 마운트한다.
    volumeMounts:
    - name: config-volume
      mountPath: /etc/config

솔라는 완성된 코드를 보며 구조를 파악했다. “아, 분리되어 있구나. Pod 전체에서 쓸 volumes를 먼저 정의하고, 그 다음에 컨테이너가 그 볼륨을 가져다가 volumeMounts로 특정 경로에 연결하는 거네. name: config-volume이 그 둘을 이어주는 연결고리고.”

“정확해. 이제 이 Pod를 실행시키고, 컨테이너 안 /etc/config 디렉터리에 정말로 app.conf라는 파일 하나만 있는지 확인해 봐.”

솔라는 루나의 말에 고개를 끄덕이며 파일을 저장하고 클러스터에 적용했다.

kubectl apply -f volume-mount-configmap.yaml

Pod가 성공적으로 생성되자, 솔라는 곧장 컨테이너 내부로 접속했다. 그녀의 머릿속에는 여전히 /etc/config/app.conf 파일의 내용물을 확인하는 상상이 그려져 있었다.

kubectl exec -it volume-mount-pod -- /bin/sh

프롬프트가 뜨자마자 솔라는 mountPath로 지정했던 /etc/config 디렉터리로 이동해 파일 목록을 출력했다.

/ # cd /etc/config
/etc/config # ls -l
total 0
lrwxrwxrwx    1 root     root            17 May 10 08:30 LOG_LEVEL -> ..data/LOG_LEVEL
lrwxrwxrwx    1 root     root            11 May 10 08:30 k8s -> ..data/k8s

“어?”

솔라의 눈이 동그래졌다. 예상했던 app.conf 파일은 어디에도 없었다. 대신 LOG_LEVELk8s라는, ConfigMap의 키와 똑같은 이름의 파일(정확히는 심볼릭 링크) 두 개가 덩그러니 놓여 있었다.

“파일이… 하나가 아니네? 키 이름이 그대로 파일 이름이 됐어!”

솔라는 믿기지 않는다는 표정으로 각 파일을 cat 명령으로 열어보았다.

/etc/config # cat LOG_LEVEL
info
/etc/config # cat k8s
start

파일의 내용물은 정확히 ConfigMap의 값과 일치했다. 그제야 솔라는 모든 것을 이해했다. ConfigMap을 볼륨으로 마운트하면, 키는 파일명이 되고 값은 그 파일의 내용이 되어 지정된 디렉터리 아래에 펼쳐진다는 사실을. 하나의 큰 뭉치가 아니라, 잘 정리된 개별 파일들의 모음이었던 것이다. 환경 변수 주입 방식과 닮은 듯하면서도 전혀 다른 작동 방식이었다.

솔라의 머릿속에 있던 ‘하나의 큰 설정 파일’이라는 이미지가 ‘키-파일 매핑 디렉터리’라는 명확한 구조로 바뀌었다. 이제 수백 줄짜리 JSON 파일도 문제없었다. database.json이라는 키로 ConfigMap에 저장해두면, 컨테이너 안에서 /etc/config/database.json이라는 파일로 곧장 읽을 수 있을 테니까.

“와, 이거 정말 편리하다. 애플리케이션에서는 그냥 정해진 경로의 파일을 읽기만 하면 되네.”

만족스럽게 컨테이너 셸을 빠져나온 솔라에게 또 다른 질문이 떠올랐다.

“언니, 이것도 envFrom처럼 ConfigMap의 모든 내용을 통째로 가져오는 방식이잖아. 만약 my-config에 있는 키 100개 중에 딱 k8s 하나만 파일로 만들고 싶으면 어떡해? 그리고 파일 이름도 k8s가 아니라 app_name.txt처럼 내가 원하는 이름으로 지정하고 싶은데… 그런 방법도 있을까?“

5장: ConfigMap에서 원하는 파일만 골라 원하는 이름으로 마운트하는 방법

솔라의 화면에는 이전 장에서 사용했던 volume-mount-configmap.yaml 파일이 열려 있었다. volumesvolumeMounts를 이용해 ConfigMap의 모든 키-값 쌍을 디렉터리에 파일로 펼쳐놓는, 깔끔하고 강력한 방법이었다. 하지만 솔라의 마음은 이미 다음 단계의 문제에 가 있었다.

그녀는 volumeMounts 섹션 아래를 뚫어지라 쳐다보며 중얼거렸다. “만약 my-config에 키가 100개 있는데, 나는 딱 k8s 키의 값만 파일로 만들고 싶으면 어떡하지?” 그녀는 곧장 야믈 파일을 복사해 selective-volume-configmap.yaml이라는 새 파일을 만들고, 기존 volumeMounts 섹션을 이리저리 수정하기 시작했다. subPath? 아니면 mountPath에 파일 이름까지 적어버릴까? 하지만 지난번의 경험으로 그건 디렉터리를 지정하는 필드라는 걸 알고 있었다. 파일명을 제어하는 건 생각보다 더 복잡한 일일지도 모른다. 어쩌면 이 기능을 위해서는 아예 k8s 키 하나만 담긴 새로운 ConfigMap을 만들어야 하는 게 아닐까? 하지만 그건 설정을 분리하는 의미를 퇴색시키는 것 같았다.

“선택적으로 파일을 마운트하는 게 가능하더라도, 파일명까지 내 마음대로 바꾸는 건 어려울 것 같아.”

솔라가 혼잣말처럼 내뱉은 말에, 루나가 조용히 다가왔다.

“지난번에 환경 변수를 다룰 때를 생각해 봐. 이삿짐 상자(envFrom)로 모든 걸 옮기는 방법도 있었지만, 연필 한 자루(valueFrom)만 골라서 옮기는 방법도 있었지.”

루나는 솔라의 volumeMounts 섹션을 가리켰다.

“지금 volumeMountsconfig-volume이라는 저장 공간 전체를 /etc/config에 연결하고 있어. 만약 그 저장 공간에서 ‘특정 아이템’만 골라 담을 수 있는 옵션이 있다면 어떨까?”

루나의 힌트에 솔라의 눈이 반짝였다. ‘아이템’. 그녀는 volumeMounts 아래에 items라는 새로운 필드를 추가했다. 자동완성 기능이 keypath라는 하위 필드를 보여주었다.

“이거다!”

솔라는 직감적으로 필드의 의미를 깨닫고 코드를 채워 넣기 시작했다. key에는 ConfigMap에서 가져오고 싶은 키의 이름을, path에는 컨테이너 안에서 보였으면 하는 파일의 이름을 적었다.

# selective-volume-configmap.yaml
apiVersion: v1
kind: Pod
metadata:
  name: selective-volume-pod
spec:
  volumes:
  - name: config-volume
    configMap:
      name: my-config
  containers:
  - name: my-container
    image: busybox
    command: ["sleep", "3600"]
    volumeMounts:
    - name: config-volume # 볼륨 소스를 지정
      mountPath: /etc/config # 마운트할 디렉터리
      # 이 볼륨에서 특정 아이템만 마운트한다
      items:
      - key: k8s # ConfigMap의 'k8s' 키를
        path: app_name.txt # 'app_name.txt' 라는 파일명으로 마운트

“와… 구조가 valueFrom이랑 비슷하면서도 다르네. volumeMounts 아래에 items를 써서, 어떤 키(key)를 어떤 경로(path)로 만들지 정하는 거구나.”

솔라는 감탄하며 말했다. pathmountPath에 대한 상대 경로다. 즉, /etc/config 디렉터리 안에 app_name.txt라는 이름으로 k8s 키의 내용이 담긴 파일이 생성될 것이다. 복잡한 스크립트나 별도의 ConfigMap은 필요 없었다.

솔라는 확신을 가지고 YAML 파일을 클러스터에 적용했다.

kubectl apply -f selective-volume-configmap.yaml

Pod가 생성되자마자, 그녀는 컨테이너 내부로 들어가 파일 시스템을 확인했다.

kubectl exec -it selective-volume-pod -- /bin/sh

프롬프트가 뜨자, 솔라는 곧장 /etc/config 디렉터리의 내용을 확인했다.

/ # ls -l /etc/config
total 4
-rw-r--r--    1 root     root             5 May 10 09:00 app_name.txt

“됐어! app_name.txt 파일 하나만 정확히 있어!”

지난번처럼 LOG_LEVEL 파일은 보이지 않았다. items를 사용하면 지정한 항목만 마운트되고, 나머지는 무시된다는 사실을 확인한 순간이었다. 그녀는 파일 내용까지 확인했다.

/ # cat /etc/config/app_name.txt
start

예상대로 my-configk8s 키 값이 정확히 들어있었다. 파일명을 제어하는 것이 어려울 것이라는 예상은 완벽히 빗나갔다. 쿠버네티스는 이미 필요한 모든 도구를 준비해두고 있었다.

그때 루나가 마지막 질문을 던졌다. “아주 잘했어. 그럼 마지막 퀴즈. 만약 데이터베이스 암호처럼 민감한 정보를 담고 있는 my-secret이라는 Secret 객체가 있다고 해봐. 그 안의 password라는 키 값을 /etc/secrets/db-pass라는 파일로 마운트하려면, 저 YAML 파일에서 딱 두 군데만 바꾸면 되는데… 어딜까?”

솔라는 잠시 YAML 파일을 응시했다. 그리고는 자신 있게 대답했다.

“알 것 같아! spec.volumes 섹션에서 configMap:secret:으로 바꾸고, 그 아래 namemy-secret으로 바꾸면 돼. volumeMountsitems 부분은 그대로 두면 되고!”

그 순간, 솔라의 머릿속에 흩어져 있던 모든 조각들이 하나의 그림으로 맞춰졌다. ConfigMap이든 Secret이든, 그것들은 단지 데이터를 담는 소스일 뿐이었다. 중요한 것은 Pod Spec에서 envFrom, valueFrom, volumes, volumeMounts, items 같은 ‘주입 메커니즘’을 어떻게 조합하여 사용하느냐였다.

솔라는 노트북 화면에 있던 모든 터미널과 YAML 파일을 닫고, 깨끗한 메모장을 열었다.

“언니, 이제 알겠어. 컨테이너 이미지를 설정으로부터 분리한다는 건, 그냥 ConfigMap을 쓰는 걸 넘어서는 문제였어.”

그녀는 키보드를 두드리며 방금 배운 내용을 정리하기 시작했다. 만약 새로운 애플리케이션을 배포해야 한다면, 이제 그녀는 이렇게 설계할 것이다.

  1. 로그 레벨 (string): 간단한 값이니 ConfigMap에 저장하고 env.valueFrom.configMapKeyRef를 써서 LOG_LEVEL 환경 변수로 주입한다.
  2. DB 암호 (sensitive string): 민감 정보는 무조건 Secret에 저장. env.valueFrom.secretKeyRef를 써서 DB_PASSWORD 환경 변수로 주입한다. 애플리케이션 코드에서는 환경 변수를 읽는 게 가장 간편하니까.
  3. 복잡한 JSON 설정 파일 (multiline text): 이건 환경 변수로 넣기엔 너무 크고 복잡해. ConfigMap에 settings.json이라는 키로 통째로 저장하고, volumesvolumeMountsitems와 함께 사용해서 컨테이너 안의 /app/config/settings.json 경로에 파일로 정확히 마운트한다.

메모장에 적힌 자신만의 설계도를 보며 솔라는 미소 지었다. 더 이상 ConfigMap은 단순한 ‘메모장’이 아니었다. 그것은 컨테이너의 내부 환경을 자솔라재로 조립할 수 있게 해주는 정교한 ‘부품 카탈로그’이자 ‘주입 장치’였다. 이제 솔라는 어떤 설정 값이 주어지더라도, 이미지를 재빌드하지 않고도 가장 우아하고 효율적인 방법으로 Pod에 설정을 주입할 수 있는 자신감을 얻었다.