ConfigMap 과 Secret 으로 Kubernetes 애플리케이션 구성

KubernetesBeginner
지금 연습하기

소개

컨테이너 이미지는 여러 환경에서 재사용할 수 있어야 합니다. Kubernetes 는 민감하지 않은 설정을 ConfigMap 에, 인증 정보를 Secret 에 저장한 다음 두 객체 중 하나를 환경 변수나 파일로 주입합니다.

이 실습에서는 두 가지 방식을 모두 사용해 하나의 애플리케이션을 구성하고, 결과를 안전하게 확인한 뒤 구성을 변경하여 환경 변수와 마운트된 볼륨이 서로 다르게 갱신되는 이유를 살펴봅니다.

목표는 두 개의 API 객체를 만드는 데 그치지 않습니다. Kubernetes 저장소의 각 값이 실행 중인 컨테이너에 전달되는 과정을 따라가고, 값을 변경한 뒤 전달 방식에 따라 갱신 동작이 어떻게 달라지는지 직접 확인합니다.

애플리케이션 구성 생성

이 단계에서는 네임스페이스를 만들고 리터럴 값으로 ConfigMap 을 생성합니다. ConfigMap 은 data 아래에 문자열 키를 저장하며, 값을 암호화하지는 않습니다.

먼저 실습 작업 공간으로 이동합니다. 이 네임스페이스를 사용하면 이 예제에 필요한 모든 객체를 한곳에서 관리할 수 있으며, 다른 실습에서 이름이 비슷한 구성이 실수로 선택되는 것도 방지할 수 있습니다.

cd /home/labex/project/application-config
kubectl create namespace practice-config
kubectl create configmap app-settings -n practice-config --from-literal=APP_MODE=production --from-literal=LOG_LEVEL=info
kubectl get configmap app-settings -n practice-config -o yaml

각 --from-literal=KEY=VALUE는 하나의 항목을 추가합니다. YAML 출력에서는 저장된 키를 확인할 수 있으며, 민감하지 않은 구성에 적합합니다.

data: 아래에서 APP_MODE와 LOG_LEVEL을 확인합니다. Kubernetes 는 원하는 값을 저장하지만 아직 이를 사용하는 대상은 없습니다. ConfigMap 만 생성한다고 해서 Pod 가 변경되지는 않습니다.

출력의 관련 부분은 다음처럼 단순한 일반 텍스트로 표시됩니다.

data:
  APP_MODE: production
  LOG_LEVEL: info
kind: ConfigMap

uid, resourceVersion, creationTimestamp와 같은 필드는 Kubernetes 가 생성하므로 실행할 때마다 달라집니다. 이 단계에서는 사용자가 제공한 구성인 data만 확인하면 됩니다.

Secret 을 안전하게 생성

이 단계에서는 값을 YAML 파일에 넣지 않고 Secret 을 생성합니다. API 에서 Secret 데이터는 base64 로 인코딩될 뿐이며, 객체 종류가 Secret 이라는 이유만으로 암호화되는 것은 아닙니다. 따라서 RBAC 와 저장 데이터 암호화도 여전히 중요합니다.

generic은 전문화된 TLS Secret 이나 이미지 풀 Secret 이 아니라 일반적인 키 - 값 Secret 이라는 뜻입니다. 비밀번호를 따옴표로 감싸면 나중에 값에 특수 문자가 포함되더라도 셸이 이를 해석하지 않습니다.

kubectl create secret generic app-credentials -n practice-config --from-literal=username=appuser --from-literal=password='practice-pass'
kubectl get secret app-credentials -n practice-config
kubectl get secret app-credentials -n practice-config -o jsonpath='{.data.username}' | base64 --decode; echo

마지막 파이프라인은 예시를 위해 사용자 이름만 디코딩합니다. 일반적인 운영 출력에서는 비밀번호를 출력하지 않도록 주의해야 합니다.

파이프 문자는 인코딩된 JSONPath 결과를 base64 --decode로 전달합니다. 세미콜론 뒤의 echo는 디코딩된 값 다음에 줄바꿈을 추가하여 다음 셸 프롬프트가 읽기 쉽게 표시되도록 합니다. appuser가 표시되면 인코딩 방식이 작동한다는 뜻이지만, 동시에 base64 를 보안 경계로 간주해서는 안 된다는 점도 보여 줍니다.

가운데 명령은 두 값 중 어느 것도 노출하지 않고 메타데이터만 표시합니다.

NAME              TYPE     DATA   AGE
app-credentials   Opaque   2      ...
appuser

DATA=2는 Secret 에 두 개의 키가 있다는 뜻이며, 바이트 수를 의미하지 않습니다. Opaque는 일반적인 키 - 값 Secret 에 사용되는 API 유형입니다.

Pod 에 구성 주입

이 단계에서는 ConfigMap 값을 환경 변수로 사용하고 Secret 을 읽기 전용 파일로 마운트합니다.

Pod 를 생성하기 전에 매니페스트에서 두 가지 전달 경로를 구분해 봅니다.

  • envFrom은 ConfigMap 의 모든 키를 컨테이너 환경 변수로 변환합니다.
  • volumes는 Secret 을 Pod 에 연결하고, volumeMounts는 해당 볼륨이 이 컨테이너 내부의 어느 경로에 나타날지 지정합니다.

Pod 의 반복문은 실제 애플리케이션 작업을 수행하지 않습니다. 두 전달 경로를 확인할 수 있도록 컨테이너가 계속 실행되게 할 뿐입니다.

cd /home/labex/project/application-config
cat <<'EOF' > configured-app.yaml
apiVersion: v1
kind: Pod
metadata:
  name: configured-app
  namespace: practice-config
spec:
  containers:
    - name: app
      image: busybox:1.36
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "while true; do sleep 30; done"]
      envFrom:
        - configMapRef:
            name: app-settings
      volumeMounts:
        - name: credentials
          mountPath: /etc/app-credentials
          readOnly: true
  volumes:
    - name: credentials
      secret:
        secretName: app-credentials
EOF

매니페스트를 적용하고 Pod 가 준비 상태가 될 때까지 기다립니다.

kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s

이제 컨테이너 내부에서 각 전달 경로를 확인합니다. printenv는 프로세스 환경 변수를 읽고, ls는 Secret 볼륨이 투영한 파일을 표시합니다.

kubectl exec configured-app -n practice-config -- printenv APP_MODE LOG_LEVEL
kubectl exec configured-app -n practice-config -- ls -l /etc/app-credentials

envFrom은 ConfigMap 의 모든 키를 가져옵니다. Secret 볼륨은 키마다 하나의 파일을 생성하며, 컨테이너 내부에서는 읽기 전용으로 표시됩니다.

환경 변수 출력은 production 다음에 info가 표시되는 것이 정상입니다. 디렉터리에는 username과 password라는 이름의 파일이 있어야 하며, 파일 이름은 Secret 키에 대응합니다.

실제 컨테이너에서 목록은 다음과 비슷하게 표시됩니다.

production
info
total 0
lrwxrwxrwx ... password -> ..data/password
lrwxrwxrwx ... username -> ..data/username

이 항목들은 Kubernetes 가 관리하는 심볼릭 링크입니다. 이러한 간접 참조를 사용하면 애플리케이션이 파일을 읽는 동안 파일을 직접 덮어쓰지 않고도 kubelet 이 마운트된 데이터를 새 버전으로 전환할 수 있습니다. 애플리케이션은 여전히 익숙한 경로인 /etc/app-credentials/username과 /etc/app-credentials/password를 열어 사용할 수 있습니다.

마운트된 Secret 데이터 업데이트

이 단계에서는 Secret 을 업데이트하고 마운트된 파일을 확인합니다. kubectl create ... --dry-run=client -o yaml | kubectl apply -f -는 원하는 YAML 을 로컬에서 생성한 뒤 apply로 직접 전달합니다. 여기서 -는 표준 입력을 의미합니다.

이 방식은 명령형으로 생성한 객체를 삭제하지 않고 업데이트할 때 유용합니다. 왼쪽 명령은 완전한 Secret 매니페스트를 출력하고, 오른쪽 명령은 해당 매니페스트를 API 서버로 전송합니다.

kubectl create secret generic app-credentials -n practice-config --from-literal=username=serviceuser --from-literal=password='rotated-pass' --dry-run=client -o yaml | kubectl apply -f -

Secret 을 처음에 kubectl create로 생성했으므로, 첫 업데이트에서는 성공 메시지 앞에 다음과 같은 경고가 표시될 수 있습니다.

Warning: resource secrets/app-credentials is missing the kubectl.kubernetes.io/last-applied-configuration annotation ...
secret/app-credentials configured

이 경고는 apply가 비교할 이전 선언적 스냅샷을 가지고 있지 않다는 뜻입니다. Kubernetes 는 이 업데이트 과정에서 해당 주석을 자동으로 추가하므로 configured가 표시되면 성공한 것입니다. Secret 을 삭제한 후 다시 만들 필요는 없습니다.

kubelet 은 투영된 Secret 볼륨의 내용을 비동기적으로 업데이트합니다. 새 사용자 이름이 나타날 때까지 반복해서 확인합니다.

current_username=""
for attempt in $(seq 1 24); do
  current_username=$(kubectl exec configured-app -n practice-config -- cat /etc/app-credentials/username)
  if [ "$current_username" = "serviceuser" ]; then
    break
  fi
  sleep 5
done
echo "$current_username"
test "$current_username" = "serviceuser"

볼륨 투영은 최종적 일관성을 따르므로 이 반복문은 최대 2 분 동안 재시도합니다. 테스트 환경에서는 API 객체가 즉시 변경되었지만 마운트된 파일이 갱신되는 데 1 분 넘게 걸렸습니다.

$(...)는 명령 치환입니다. 셸은 seq 1 24를 실행하고 그 숫자를 반복문의 값으로 사용합니다. 각 반복에서 현재 파일 내용을 current_username에 저장하고, 일치하는 값이 처음 나타나면 break로 반복을 중단합니다. sleep 5는 API 를 계속해서 조회하지 않도록 간격을 둡니다. 마지막 test는 값이 끝까지 변경되지 않으면 실패를 반환하므로, 오래된 데이터로 조용히 다음 작업을 진행할 수 없습니다. 성공적인 출력은 serviceuser입니다.

환경 변수 구성 갱신

이 단계에서는 LOG_LEVEL을 업데이트하고 실행 중인 Pod 와 비교합니다. 환경 변수는 컨테이너가 시작될 때 고정되므로 ConfigMap 을 변경해도 현재 프로세스의 환경 변수는 다시 작성되지 않습니다.

병합 패치는 data 아래의 항목 하나만 변경합니다. 실행 중인 프로세스에서 즉시 값을 읽어 보면 마운트된 볼륨과의 중요한 차이를 확인할 수 있습니다.

kubectl patch configmap app-settings -n practice-config --type=merge -p '{"data":{"LOG_LEVEL":"debug"}}'
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL

Pod 는 여전히 info를 출력합니다. 저장된 매니페스트로 Pod 를 다시 생성해야 새 값을 사용할 수 있습니다.

configmap/app-settings patched
info

이는 패치 실패가 아니라 예상된 결과입니다. ConfigMap 에는 debug가 저장되어 있지만, 이미 실행 중인 프로세스는 시작할 때 생성된 환경 변수를 계속 사용합니다.

cd /home/labex/project/application-config
kubectl delete pod configured-app -n practice-config --wait=true
kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL

새로 생성된 Pod 는 debug를 출력합니다. 환경 변수 기반 구성을 변경한 뒤 운영 환경의 워크로드에서 일반적으로 롤아웃을 실행하는 이유가 바로 이 차이입니다.

pod "configured-app" deleted
pod/configured-app created
pod/configured-app condition met
debug

매니페스트 자체는 여전히 이름으로 ConfigMap 을 참조하며, configured-app.yaml에 값이 복사된 것은 아닙니다. Pod 를 다시 생성하면 kubelet 이 해당 참조를 다시 확인하고 최신 데이터를 주입합니다.

요약

이미지에서 애플리케이션 설정과 인증 정보를 분리하고, ConfigMap 데이터를 환경 변수로 사용했으며, Secret 데이터를 파일로 마운트했습니다. 또한 투영된 Secret 을 교체하고, 환경 변수 기반 구성을 갱신하기 위해 Pod 를 다시 생성했습니다.