볼륨이란, 호스트에 미리 준비된 사용 가능한 디렉토리를 일컫는다. 쿠버네티스에서는 볼륨 리소스를 생성/삭제할 수 없다. 파드에서 볼륨을 사용하기 위해 매니페스트에 볼륨을 지정하기만 할 수 있다. 볼륨은 플러그인 형태로 제공되는데, ConfigMap 과 Secret 도 볼륨 플러그인의 종류이다. 대표적인 볼륨 플러그인은 아래와 같다.

  • emptyDir
  • hostPath
  • downwardAPI
  • projected
  • nfs
  • iscsi
  • cephfs

1. emptyDir

노드에서 파드로 할당되는 호스트의 임시 디스크 영역으로, 파드가 종료되면 같이 삭제된다. 직접 마운트할 호스트의 영역을 지정할 수는 없다. 아래 매니페스트와 같이 emptyDir 볼륨을 마운트할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-emptydir
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      volumeMounts:
        - name: cache-volume
          mountPath: /cache
  volumes:
    - name: cache-volume
      emptyDir: {}

아래 커맨드로 컨테이너의 /cache 마운트 경로에 할당된 디스크를 확인해보자

$ kubectl exec -it sample-emptydir -- df -h | grep cache
/dev/mapper/ubuntu--vg-ubuntu--lv   11G  6.6G  3.1G  69% /cache

emptyDir.sizeLimit 항목을 사용하여 해당 볼륨의 사이즈 제한을 걸 수 있다. 아래 매니페스트처럼 지정한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-emptydir-limit
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      volumeMounts:
        - name: cache-volume
          mountPath: /cache
  volumes:
    - name: cache-volume
      emptyDir:
        sizeLimit: 128Mi

아래 커맨드와 같이 pod 를 --watch 옵션으로 감시하며, 컨테이너 내부에서 볼륨 마운트 된 경로에 150MB 용량의 더미 파일을 생성한다.

$ kubectl get pods --watch
NAME                                READY   STATUS    RESTARTS      AGE
sample-emptydir-limit               1/1     Running   0             2m57s

# 별도의 세션에서 실행
$ kubectl exec -it sample-emptydir-limit -- dd if=/dev/zero of=/cache/dummy bs=1M count=150
150+0 records in
150+0 records out
157286400 bytes (157 MB, 150 MiB) copied, 0.803752 s, 196 MB/s

# watch 중인 파드 상태가 아래처럼 바뀌었음을 확인
NAME                                READY   STATUS      RESTARTS      AGE
sample-emptydir-limit               0/1     Completed   0             5m45s

$ kubectl describe pods sample-emptydir-limit
...
Events:
  Type     Reason     Age    From               Message
  ----     ------     ----   ----               -------
  ...
  Warning  Evicted    5m46s  kubelet            Usage of EmptyDir volume "cache-volume" exceeds the limit "128Mi".
  Normal   Killing    5m46s  kubelet            Stopping container nginx-container

용량 초과로 인해 파드가 종료된 것을 알 수 있다.

 

emptyDir 은 호스트 노드의 디스크 뿐 아니라 RAM 영역인 tmpfs 영역에서 사용할 수도 있다. 아래처럼 emptyDir.medium 항목에 Memory 를 설정한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-emptydir-memory
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      volumeMounts:
        - name: cache-volume
          mountPath: /cache
  volumes:
    - name: cache-volume
      emptyDir:
        medium: Memory
        sizeLimit: 128Mi

다시 아래 커맨드로 확인해보면 노드의 tmpfs 영역이 볼륨으로 마운트된 것을 볼 수 있다.

$ kubectl exec -it sample-emptydir-memory -- df -h | grep cache
tmpfs                              128M     0  128M   0% /cache

하지만, Memory 영역을 마운트한 경우에는 sizeLimit 을 초과한 용량을 사용하려고 하면 한도까지만 write 가 되고, 파드가 종료되지는 않는다는 차이가 있다.(공식문서에는 해당 스펙이 기재되어 있지 않지만 실제로 이렇게 동작함. kublet 설정에 따라 다를 수 있음.)

$ kubectl exec -it sample-emptydir-memory -- dd if=/dev/zero of=/cache/dummy bs=1M count=150

$ kubectl get pods
NAME                                READY   STATUS      RESTARTS      AGE
sample-emptydir-memory              1/1     Running     0             16m

# 128M 까지만 기록됨
$ kubectl exec -it sample-emptydir-memory -- ls -lh /cache/dummy
-rw-r--r-- 1 root root 128M Jan 27 14:54 /cache/dummy

파드에는 컨테이너별로 사용할 수 있는 메모리 제한을 걸 수 있는데, 이는 볼륨의 emptyDir.sizeLimit 에 지정한 메모리 제한보다 더 우선 적용되는 항목이기 때문에, 아래 매니페스트로 파드를 생성한 후 70MB의 더미 파일을 만들면 OOM에 의해 파드가 종료된다. 파드의 컨테이너 메모리 제한은 spec.containers[].resources.limits.memory 에 지정할 수 있다.(실제로 해보면 나의 경우에는 파드가 종료되지 않는다...)

apiVersion: v1
kind: Pod
metadata:
  name: sample-emptydir-memory-with-memory-limits
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      resources:
        limits:
          memory: 64Mi
      volumeMounts:
        - mountPath: /cache
          name: cache-volume
  volumes:
    - name: cache-volume
      emptyDir:
        medium: Memory
        sizeLimit: 128Mi

 

2. hostPath

hostPath 는 emptyDir 과 다르게 호스트 노드의 지정한 영역을 마운트하는 방식이다. 파드가 종료되어도 호스트의 해당 영역은 그대로 유지된다. type 을 지정해야 하는데, 지정 가능한 값은 아래와 같다.

type 설명
DirectoryOrCreate 지정한 경로에 디렉토리가 없으면 0755 권한으로 빈 디렉토리를 생성
Directory 지정한 경로에 디렉토리가 없으면 파드 생성에 실패
FileOrCreate 지정한 경로에 파일이 없으면 0644 권한으로 빈 파일을 생성
File 지정한 경로에 파일이 없으면 파드 생성에 실패
Socket 지정한 경로에 UNIX 소켓 파일이 필요(Docker/CRI 소켓 연결시 사용)
CharDevice 지정한 경로에 문자 디바이스가 필요
BlockDevice 지정한 경로 블록 디바이스가 필요

하지만, hostPath 는 파드가 노드의 파일 시스템에 직접 접근하므로 보안상 위험하여 권장되지 않는다.

매니페스트 예시는 아래와 같다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-hostpath
spec:
  containers:
    - image: nginx:1.27
      name: nginx-container
      volumeMounts:
        - name: hostpath-sample
          mountPath: /srv
  volumes:
    - name: hostpath-sample
      hostPath:
        path: /etc
        type: DirectoryOrCreate
# 호스트 노드의 OS 정보 확인
$ kubectl exec -it sample-hostpath -- cat /srv/os-release | grep "PRETTY_NAME"
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"

 

3. downwardAPI

downwardAPI 는 파드 정보나 컨테이너 정보를 파일로 컨테이너 내에 배치하기 위한 플러그인이다. 파드의 정보는 fieldRef 로, 컨테이너의 정보는 resourceFieldRef 로 가져올 수 있다. 아래 매니페스트를 적용해보자.

apiVersion: v1
kind: Pod
metadata:
  name: sample-downward-api
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      volumeMounts:
        - name: downward-api-volume
          mountPath: /srv
  volumes:
    - name: downward-api-volume
      downwardAPI:
        items:
          - path: "podname"
            fieldRef:
              fieldPath: metadata.name
          - path: "cpu-request"
            resourceFieldRef:
              containerName: nginx-container
              resource: requests.cpu

아래 커맨드로 파드명과 컨테이너의 CPU 요구사항이 파일로 배치되었는지 확인한다.

$ kubectl exec -it sample-downward-api -- tail -n +1 /srv/podname /srv/cpu-request
==> /srv/podname <==
sample-downward-api
==> /srv/cpu-request <==
0

 

4. projected

projected 는 Secret, ConfigMap, downwardAPI, serviceAccountToken 를 하나의 볼륨 마운트 경로에 통합하는 플러그인이다. 여러가지 볼륨으로 관리되는 기밀/설정 정보를 컨테이너 내 하나의 디렉터리에 배치하고자 할 때 사용한다. 아래의 매니페스트 예시를 보자.

(sample-db-auth 시크릿과 sample-configmap 컨피그맵은 미리 생성되어 있어야 한다. 없다면 아래를 참고하여 생성하자.)

apiVersion: v1
kind: Pod
metadata:
  name: sample-projected
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      volumeMounts:
        - name: projected-volume
          mountPath: /srv
  volumes:
    - name: projected-volume
      projected:
        sources:
          - secret:
              name: sample-db-auth
              items:
                - key: username
                  path: secret/username.txt
          - configMap:
              name: sample-configmap
              items:
                - key: nginx.conf
                  path: configmap/nginx.conf
          - downwardAPI:
              items:
                - path: "podname"
                  fieldRef:
                    fieldPath: metadata.name
$ kubectl exec -it pod/sample-projected -- tail -n +1 /srv/secret/username.txt /srv/configmap/nginx.conf /srv/podname
==> /srv/secret/username.txt <==
root
==> /srv/configmap/nginx.conf <==
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

==> /srv/podname <==
sample-projected

sample-db-auth 시크릿의 username 값은 base64 디코딩되어 마운트되는 것을 알 수 있다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

컨피그맵이나 시크릿의 데이터를 파드에서 사용할 때 볼륨 마운트 방식으로 사용할 수 있다.

이때 마운트된 볼륨 내의 파일들에 대해서 permission 을 설정해 줄 수 있다. 컨피그맵과 시크릿 모두 기본 퍼미션은 0644(rw-r--r--)이다. 퍼미션은 매니페스트 파일로 작성할 때 8진수 퍼미션 값을 10진수로 변환하여 기입하여야 한다.

8진수(Octal Mode) 10진수 rwx 표기(Symbolic Mode)
0400 256 r--------
0600 384 rw-------
0644 420 rw-r--r--
0700 448 rwx------
0755 493 rwxr-xr-x
0777 511 rwxrwxrwx

 

1. ConfiMap 예시

아래의 매니페스트는 컨피그맵의 쉘 스크립트 파일을 0755 권한으로 마운트한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-scripts
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      command: ["/config/test.sh"]
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap
        items:
          - key: test.sh
            path: test.sh
            mode: 493 # 0755

아래 커맨드로 쉘 스크립트 실행을 확인해본다.

$ kubectl apply -f sample-configmap-scripts.yaml

# 권한 확인
$ kubectl exec -it sample-configmap-scripts -- ls -l /config/test.sh
lrwxrwxrwx 1 root root 14 Jan 17 08:01 /config/test.sh -> ..data/test.sh

# 실행됨 확인
$ kubectl logs sample-configmap-scripts
Hello, kubernetes

 

그런데, 권한 확인을 해보니 0755 권한은 rwxr-xr-x 이어야 하는데 rwxrwxrwx 로 출력된다. 쿠버네티스에서 Secret 이나 ConfigMap 을 볼륨으로 마운트하면 Atomic Reference Updates 라는 메커니즘에 의해 파일들이 복잡한 심볼릭 링크 구조를 가지기 때문이다. 심볼릭 링크가 걸린 경로인 "..data/test.sh" 는 실제 데이터 경로가 아니라 쿠버네티스가 데이터 업데이트시 원자성을 보장하기 위해 만든 중간 경로이다. 예를 들어 하나의 시크릿에 10개의 key-value 가 존재하고, 각각의 key-value 를 파일로 만들어서 파드에 볼륨 마운트 한다고 치자. 어느순간 시크릿이 수정되어 10개의 마운트된 파일들이 모두 수정되어야 하는데, 그 중 5개만 수정된 상태에서 애플리케이션에서 마운트된 경로에 접근했을 경우 5개의 파일은 이전버전의 시크릿을, 5개의 파일은 현재버전의 시크릿을 읽게 되어 일관성이 무너지게 된다. 따라서 10개의 파일을 모두 업데이트 한 후, 어느 시점에 한번에 마운트 링크를 변경시키는 방식을 사용하는 것이다. 실제로 마운트 경로의 파일을 모두 출력해보면 "..data" 라는 심볼릭 링크가 있고, 다시 타임스탬프 형식으로 이루어진 디렉토리를 참조하는 형태임을 알 수 있다. 복잡한 심볼릭 링크를 따라가서 실제 파일의 경로를 보려면 -L 옵션(dereference)을 사용한다.

# 심볼릭 링크가 가리키는 중간 심볼릭 링크와 실제 데이터가 저장된 디렉토리를 볼 수 있다.
$ kubectl exec -it sample-configmap-scripts -- ls -alF /config
total 12
drwxrwxrwx 3 root root 4096 Jan 17 08:01 ./
drwxr-xr-x 1 root root 4096 Jan 17 08:01 ../
drwxr-xr-x 2 root root 4096 Jan 17 08:01 ..2026_01_17_08_01_01.3453801296/
lrwxrwxrwx 1 root root   32 Jan 17 08:01 ..data -> ..2026_01_17_08_01_01.3453801296/
lrwxrwxrwx 1 root root   14 Jan 17 08:01 test.sh -> ..data/test.sh*

# -L 옵션으로 심볼릭 링크를 쭉 따라가서 원본 파일 정보를 출력할 수 있다.
$ kubectl exec -it sample-configmap-scripts -- ls -lL /config/test.sh
-rwxr-xr-x 1 root root 52 Jan 17 08:01 /config/test.sh

 

2. Secret 예시

아래의 매니페스트는 시크릿의 모든 key-value 를 0400 권한으로 마운트한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-secret-secure
spec:
  containers:
    - name: secret-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      secret:
        secretName: sample-db-auth
        defaultMode: 256

아래 커맨드로 권한을 확인해본다.

$ kubectl exec -it sample-secret-secure -- ls -l /config/..data/
total 8
-r-------- 1 root root 12 Jan 25 07:39 password
-r-------- 1 root root  4 Jan 25 07:39 username

 

3. 동적 업데이트

위의 컨피그맵 예시에서 사용한 sample-configmap 의 모든 키를 파드에 볼륨 마운트 한 후, 컨피그맵의 thread 값만 "16" -> "32" 로 변경한 후 파드 재시작 없이도 변경된 값이 마운트 경로에서도 변경되는지 확인해보자.

먼저 아래 매니페스트를 적용하여 sample-configmap 컨피그맵의 모든 key-value 를 마운트한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-multi-volume
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap
$ kubectl exec -it sample-configmap-multi-volume -- cat /config/thread
16

이 상태에서 sample-configmap 매니페스트의 thread 값을 32로 변경 후 apply 한다.

기본 업데이트 간격은 60초로 설정되어 있기 때문에 바로 변경되지 않더라도 최대 60초 후에는 변경된 것을 확인할 수 있다.

$ kubectl exec -it sample-configmap-multi-volume -- cat /config/thread
32

 

4. 변경 불가능한 컨피그맵/시크릿 만들기

아래처럼 매니페스트에 immutable: true 항목을 추가하면 컨피그맵이나 시크릿을 한번 생성하면 도중에 절대 변경 불가능하도록 만들 수 있다.

apiVersion: v1
kind: Secret
metadata:
  name: my-secure-config
type: Opaque
immutable: true  # <--- 이 항목을 추가합니다.
data:
  api-key: AAA

kubectl apply, patch, edit 같은 명령어는 물론 어떤 방법으로도 data 를 변경할 수 없다. 데이터를 변경하려면 리소스를 삭제 후 변경된 data 를 포함하여 재생성하여야만 한다. 또한, immutable 항목 자체도 한번 생성하면 변경 불가능하다. 아래와 같은 이점이 있다.

  • Kubelet 부하 감소: 쿠버네티스의 워커 노드(Kubelet)는 Secret이 변경되었는지 주기적으로 API Server를 호출하여 감시(Watch)한다. immutable: true로 설정된 리소스는 변경되지 않음을 보장받으므로, Kubelet이 감시를 중단하여 클러스터 전체의 부하를 줄일 수 있다.
블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

컨피그맵은 파드의 컨테이너에서 참조하여 사용하게 된다. 환경 변수로 전달하는 방법과 볼륨으로 마운트하여 사용하는 방법이 있다.

  • 환경 변수로 전달
    • 컨피그맵의 특정 키 전달: spec.containers[].env[].valueForm.configMapKeyRef.name/key
    • 컨피그맵 전체 키 전달: spec.containers[].envFrom[].configMapRef.name
  • 볼륨으로 마운트
    • 컨피그맵의 특정 키 마운트: spec.volumes[].configMap.items[].key/path
    • 컨피그맵 전체 키 마운트: spec.volumes[].configMap.name

1. 환경 변수로 특정 키 전달

아래 매니페스트와 같이 env[] 에 환경변수들을 이름과 값으로 하나씩 직접 지정한다. sample-configmap 이라는 컨피그맵에 connection.max 키로 담긴 값을 CONNECTION_MAX 라는 환경변수로 세팅하는 것이다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-single-env
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      env:
        - name: CONNECTION_MAX
          valueFrom:
            configMapKeyRef:
              name: sample-configmap
              key: connection.max
$ kubectl apply -f sample-configmap-single-env.yaml
$ kubectl exec -it sample-configmap-single-env -- env | grep CONNECTION_MAX
CONNECTION_MAX=100

 

2. 환경 변수로 전체 키 전달

직접 환경변수명과 불러올 컨피그맵 값을 지정하지 않고 컨피그맵 전체를 환경변수로 그대로 등록하는 방법이다. 간단하긴 하지만, 매니페스트만 봐서는 어떤 환경변수가 설정되는지 알 수 없다는 단점이 있다.

아래 매니페스트처럼 간단히 envFrom[] 에 가져올 컨피그맵들의 참조를 걸어주면 된다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-multi-env
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      envFrom:
        - configMapRef:
            name: sample-configmap
$ kubectl apply -f sample-configmap-multi-env.yaml
$ kubectl exec -it sample-configmap-multi-env -- env
...
connection.max=100
connection.min=10
nginx.conf=user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

sample.properties=property.1=value-1
property.2=value-2
property.3=value-3

test.sh=#!/bin/bash
echo "Hello, kubernetes"
sleep infinity

thread=16
...

그러나 위처럼 파드에 설정된 환경변수를 출력해보면 nginx.conf, sample.properties, test.sh 가 제대로 설정되지 못했음을 알 수 있다.(개행 처리가 의도대로 되지 않음) 개행된 값을 컨테이너로 주입하고자 할 때는 볼륨 마운트 방식으로 사용하여야 한다. 아래와 같은 주의사항을 참고한다.

  • 환경 변수명에 "." 또는 "-" 가 포함되지 않도록 한다.
  • 환경 변수 값이 개행을 포함하지 않도록 한다.(yaml 문법의 <key>: | 개행 스타일 포함)

 

3. 볼륨으로 특정 키 마운트

컨피그맵의 정보를 컨테이너의 볼륨으로 마운트 하는 방식으로, spec.containers[].volumeMounts[] 에 마운트 할 볼륨명과 마운트 경로를 지정하고, spec.volumes[] 에 생성할 볼륨명과 값을 가져올 컨피그맵 및 읽을 key, 그리고 마운트 경로에 생성할 파일명(path)을 지정한다. 직접 마운트 파일명을 지정한다는 것이 특징이다.

다음과 같은 매니페스트로 생성한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-single-volume
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap
        items:
          - key: nginx.conf
            path: nginx-sample.conf
$ kubectl apply -f sample-configmap-single-volume.yaml
$ kubectl exec -it sample-configmap-single-volume -- cat /config/nginx-sample.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

 

4. 볼륨으로 전체 키 마운트

컨피그맵의 전체 정보를 컨테이너의 볼륨으로 마운트하는 방식으로, spec.containers[].volumeMounts[] 에 마운트 할 볼륨명과 마운트 경로를 지정하고, spec.volumes[] 에 생성할 볼륨명과 가져올 컨피그맵을 지정한다. 마운트 경로에 생성되는 파일명은 컨피그맵의 key 와 동일하다. 매니페스트는 간단해지지만 매니페스트만으로는 컨피그맵의 어떤 key-value가 마운트되는지 알 수 없다는 단점이 있다.

다음과 같은 매니페스트로 생성한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-multi-volume
spec:
  containers:
    - name: configmap-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap
$ kubectl apply -f sample-configmap-multi-volume.yaml
$ kubectl exec -it sample-configmap-multi-volume -- ls /config
connection.max	connection.min	nginx.conf  sample.properties  test.sh	thread
$ kubectl exec -it sample-configmap-multi-volume -- paste /config/connection.max /config/nginx.conf /config/sample.properties
100	user nginx;	property.1=value-1
	worker_processes auto;	property.2=value-2
	error_log /var/log/nginx/error.log;	property.3=value-3
	pid /run/nginx.pid;

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

ConfigMap 은 key-value 들을 저장하는 리소스이다. value 에는 바이너리 데이터나 nginx.conf 같은 설정 내용도 저장할 수 있다. 하나의 ConfigMap에는 1MB 까지 저장 가능하다. 생성하는 방법은 Generic 타입의 Secret 과 거의 동일하며, 3가지 생성하는 방법을 설명한다.

 

1. 매니페스트에서 생성

일반적인 리소스 생성 방식과 동일하게 kubectl apply -f <매니페스트파일명> 또는 kubectl create -f <매니페스트파일명> --save-config 으로 생성하는 방식이다. 시크릿과는 달리 base64 인코딩되지 않는다. value 를 여러 행으로 저장하는 경우(nginx 설정파일 내용 등) YAML 문법대로 <key>: | 로 시작하여 여러 행에 정의한다. 숫자는 큰따옴표로 둘러쌓아야 한다. 아래 예시를 참고한다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-configmap
data:
  thread: "16"
  connection.max: "100"
  connection.min: "10"
  sample.properties: |
    property.1=value-1
    property.2=value-2
    property.3=value-3
  nginx.conf: |
    user nginx;
    worker_processes auto;
    error_log /var/log/nginx/error.log;
    pid /run/nginx.pid;
  test.sh: |
    #!/bin/bash
    echo "Hello, kubernetes"
    sleep infinity

 

아래 커맨드로 저장된 key-value 를 확인해본다.

$ kubectl get configmaps sample-configmap -o yaml
apiVersion: v1
data:
  connection.max: "100"
  connection.min: "10"
  nginx.conf: |
    user nginx;
    worker_processes auto;
    error_log /var/log/nginx/error.log;
    pid /run/nginx.pid;
  sample.properties: |
    property.1=value-1
    property.2=value-2
    property.3=value-3
  test.sh: |
    #!/bin/bash
    echo "Hello, kubernetes"
    sleep infinity
  thread: "16"
kind: ConfigMap
...

 

2. 리터럴 값을 전달하여 생성

kubectl 커맨드로 시크릿을 생성하되, --from-literal 옵션으로 직접 key-value 쌍을 전달하는 방식이다. 아래처럼 생성한다.

# 생성
$ kubectl create configmap --save-config web-config \
  --from-literal=connection.max=100 --from-literal=connection.min=10

# 확인
$ kubectl get cm web-config -o json | jq '.data'
{
  "connection.max": "100",
  "connection.min": "10"
}

 

3. 파일의 내용을 value 로 생성

kubectl 커맨드로 생성하되, --from-file 옵션으로 key-value 로 사용될 파일들을 지정하는 방식이다. 파일명이 key 가 되고, 파일 내용이 value 가 된다. 만약 파일명 대신 다른 key 를 사용하고자 한다면 --from-file=<사용할key>=<파일명> 형식으로 입력한다.

# 생성
$ kubectl create configmap --save-config sample-configmap-file \
> --from-file=./nginx.conf

# 확인
$ kubectl get configmaps sample-configmap-file -o json | jq '.data'
{
  "nginx.conf": "user  nginx;\nworker_processes  auto;\nerror_log  /var/log/nginx/error.log warn;\npid        /var/run/nginx.pid;\n"
}

# describe 확인
$ kubectl describe configmaps sample-configmap-file
Name:         sample-configmap-file
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
nginx.conf:
----
user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;



BinaryData
====

Events:  <none>

 

k8s 에 실제 등록하지 않고 매니페스트 파일로 출력하려면 --dry-run=client -o yaml 옵션을 주어 실행한다. UTF-8 이외의 데이터를 저장하면 자동으로 binaryData 필드로 base64 인코딩 되어 저장된다.

$ kubectl create configmap sample-configmap-binary-2 \
  --from-file=image.png \
  --from-literal=index.html="Hello, Kubernetes" \
  --dry-run=client -o yaml \
  > sample-configmap-binary-2.yaml
# sample-configmap-binary-2.yaml 매니페스트
apiVersion: v1
binaryData:
  image.png: iVBORw0KGgoAAAANSUhEUgAAAs...<생략>
data:
  index.html: Hello, Kubernetes
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: sample-configmap-binary-2

이제 이를 기반으로 ConfigMap 을 생성한 후, 아래 매니페스트로 ConfigMap 을 볼륨 마운트 한 파드를 생성한 후, curl 로 호출해보자

apiVersion: v1
kind: Pod
metadata:
  name: sample-configmap-binary-webserver
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /usr/share/nginx/html
  volumes:
    - name: config-volume
      configMap:
        name: sample-configmap-binary-2
# ConfigMap 생성
$ kubectl apply -f sample-configmap-binary-2.yaml

# Pod 생성
$ kubectl apply -f sample-configmap-binary-webserver.yaml

# 확인을 위한 포트 포워딩
$ kubectl port-forward sample-configmap-binary-webserver 8080:80

# 호출 확인
$ curl http://localhost:8080/index.html
Hello, Kubernetes

# 볼륨 마운트 경로에 파일 생성 확인
$ kubectl exec -it sample-configmap-binary-webserver -- ls /usr/share/nginx/html
image.png  index.html

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

Secret 을 파드에서 사용중인 상태라면, Secret 이 수정되었을 때 자동으로 파드에 수정된 값이 반영될까? 이는 파드에서 Secret 을 사용하는 방식에 따라 다르다.

방식 시크릿 수정시 반영 방법
환경 변수 파드 재시작
볼륨 마운트 최대 1분(기본값) 이내 자동 반영

환경 변수 방식과 볼륨 마운트 방식에서 Secret 수정을 반영하는 방법을 각각 알아본다.

 

1. 환경 변수 방식

기본적으로 Pod 는 불변 객체로 취급되기 때문에 운영중에 환경 변수를 동적으로 변경할 수 없다. 환경 변수는 파드를 기동할 때에만 설정된다. 따라서, Secret 을 변경 후 파드에 반영하기 위해서는 파드를 삭제 후 재시작 하여야만 한다. Deployment 로 관리하는 경우에는 아래 커맨드로 재시작하면 된다.

$ kubectl rollout restart deployment <디플로이먼트명>

 

 

2. 볼륨 마운트 방식

볼륨 마운트 방식에서는 Secret 변경 후 최대 1분 이내에 변경사항이 파드에 자동 반영된다. 아래 매니페스트로 username 과 password 가 각각 "admin", "adminpassword" 로 설정된 시크릿을 생성 후, 이를 파드에서 볼륨 마운트한 후, 시크릿 내용을 "root", "rootpassword" 로 변경 후 1분 후에 파드의 볼륨을 확인해보자

# 시크릿 매니페스트
apiVersion: v1
kind: Secret
metadata:
  name: sample-db-auth
type: Opaque
data:
  username: YWRtaW4= # admin
  password: YWRtaW5wYXNzd29yZA== # adminpassword
# 파드 매니페스트
apiVersion: v1
kind: Pod
metadata:
  name: sample-secret-multi-volume
spec:
  containers:
    - name: secret-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      secret:
        secretName: sample-db-auth
# 파드 내 볼륨 확인
$ kubectl exec -it sample-secret-multi-volume -- paste /config/password /config/username
adminpassword	admin
# 시크릿 매니페스트 변경
apiVersion: v1
kind: Secret
metadata:
  name: sample-db-auth
type: Opaque
data:
  username: cm9vdA== # root
  password: cm9vdHBhc3N3b3Jk # rootpassword
# 변경된 시크릿 적용
$ kubectl apply -f sample-db-auth.yaml

# 파드에 반영되었는지 확인
$ kubectl exec -it sample-secret-multi-volume -- paste /config/username /config/password
root	rootpassword

 

3. 볼륨 갱신 메커니즘 설명

시크릿을 볼륨으로 파드에 마운트하였을 때는 자동 갱신 되는데, 이는 노드의 kubelet 이 Secret 리소스 수정을 감시하고 있기 때문이다. 감시하는 매커니즘은 아래 세가지 중 하나로 설정할 수 있다.

방식 설명
watch 이벤트 기반(실시간 감시)
ttl 캐시 만료 기반(기본값)
polling 주기적 변경여부 조회

이는 노드 kubelet 전역설정이며, 즉 해당 노드에서 동작하는 모든 파드에 일괄 적용되는 정책이다. 파드나 네임스페이스 단위에서 조정할 수는 없다. 

노드의 /var/lib/kubelet/config.yaml 파일에서 configMapAndSecretChangeDetectionStrategy 항목으로 설정하면 된다.

# watch 방식으로 변경
apiVersion: kubelet.config.k8s.io/v1beta1
...
configMapAndSecretChangeDetectionStrategy: watch
...
# ttl 방식의 캐시 유효시간 30초로 변경
apiVersion: kubelet.config.k8s.io/v1beta1
...
configMapAndSecretChangeDetectionStrategy: ttl
syncFrequency: 30s
...

설정파일 저장 후에는 kubelet 재시작이 필요하다.

 

4. 일반적으로 사용하는 방법

kubelet 의 ttl 시간을 변경하는 것은 보통 k8s 클러스터 관리자의 권한이다. 따라서 개발자의 경우에는 Secret 변경 후 rollout restart 를 하는 방식으로 운영하는게 일반적이다. 이렇게 적용하면 즉시 반영되며 예측 가능하게 된다.

kubectl apply -f <시크릿매니페스트파일명>
kubectl rollout restart deployment <디플로이먼트명>

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

시크릿은 파드의 컨테이너에서 참조하여 사용하게 된다. 환경 변수로 전달하는 방법과 볼륨으로 마운트하여 사용하는 방법이 있다.

  • 환경 변수로 전달
    • 시크릿의 특정 키 전달: spec.containers[].env[].valueForm.secretKeyRef.name/key
    • 시크릿 전체 키 전달: spec.containers[].envFrom[].secretRef.name
  • 볼륨으로 마운트
    • 시크릿의 특정 키 마운트: spec.volumes[].secret.items[].key/path
    • 시크릿 전체 키 마운트: spec.volumes[].secret.secretName

1. 환경변수로 특정 키 전달

아래 매니페스트와 같이 env[] 에 환경변수들을 이름과 값으로 하나씩 직접 지정한다. sample-db-auth 라는 시크릿에 username 키로 담긴 값을 DB_USERNAME 이라는 환경변수로 세팅하는 것이다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-secret-single-env
spec:
  containers:
    - name: secret-container
      image: nginx:1.27
      env:
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: sample-db-auth
              key: username
$ kubectl apply -f sample-secret-single-env.yaml
$ kubectl exec -it sample-secret-single-env -- env | grep DB_USER
DB_USERNAME=root

 

2. 환경변수로 전체 키 전달

직접 환경변수명과 불러올 시크릿 값을 지정하지 않고 시크릿 전체를 환경변수로 그대로 등록하는 방법이다. 간단하긴 하지만, 매니페스트만 봐서는 어떤 환경변수가 설정되는지 알 수 없다는 단점이 있다.

아래 매니페스트처럼 간단히 envFrom[] 에 가져올 시크릿들의 참조를 걸어주면 된다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-secret-multi-env
spec:
  containers:
    - name: secret-container
      image: nginx:1.27
      envFrom:
      - secretRef:
          name: sample-db-auth
$ kubectl apply -f sample-secret-multi-env.yaml
$ kubectl exec -it sample-secret-multi-env -- env
...
password=rootpassword
username=root
...

 

여러 시크릿들을 가져오는 경우 key 가 동일하여 충돌할 가능성이 있다. 이런 경우에는 prefix 를 붙여 충돌을 방지할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-secret-prefix-env
spec:
  containers:
    - name: secret-container
      image: nginx:1.27
      envFrom:
        - secretRef:
            name: sample-db-auth
          prefix: DB1_
        - secretRef:
            name: sample-db-auth
          prefix: DB2_
$ kubectl apply -f sample-secret-prefix-env.yaml
$ kubectl exec -it sample-secret-prefix-env -- env | egrep "^DB"
DB1_password=rootpassword
DB1_username=root
DB2_password=rootpassword
DB2_username=root

 

3. 볼륨으로 특정 키 마운트

시크릿의 정보를 컨테이너의 볼륨으로 마운트 하는 방식으로, spec.containers[].volumeMounts[] 에 마운트 할 볼륨명과 마운트 경로를 지정하고, spec.volumes[] 에 생성할 볼륨명과 값을 가져올 시크릿 및 읽을 key, 그리고 마운트 경로에 생성할 파일명(path)을 지정한다. 직접 마운트 파일명을 지정한다는 것이 특징이다.

다음과 같은 매니페스트로 생성한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-secret-single-volume
spec:
  containers:
    - name: secret-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      secret:
        secretName: sample-db-auth
        items:
          - key: username
            path: username.txt
$ kubectl apply -f sample-secret-single-volume.yaml
$ kubectl exec -it sample-secret-single-volume -- cat /config/username.txt
root

 

4. 볼륨으로 전체 키 마운트

시크릿의 전체 정보를 컨테이너의 볼륨으로 마운트하는 방식으로, spec.containers[].volumeMounts[] 에 마운트 할 볼륨명과 마운트 경로를 지정하고, spec.volumes[] 에 생성할 볼륨명과 가져올 시크릿을 지정한다. 마운트 경로에 생성되는 파일명은 시크릿의 key 와 동일하다. 매니페스트는 간단해지지만 매니페스트만으로는 시크릿의 어떤 key-value 가 마운트되는지 알 수 없다는 단점이 있다.

다음과 같은 매니페스트로 생성한다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-secret-multi-volume
spec:
  containers:
    - name: secret-container
      image: nginx:1.27
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      secret:
        secretName: sample-db-auth
$ kubectl apply -f sample-secret-multi-volume.yaml
$ kubectl exec -it sample-secret-multi-volume -- ls /config
password  username

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

Secret 이해에서 설명한 시크릿의 종류 중에 TLS, Docker 레지스트리, Basic, SSH 시크릿을 생성하는 방법을 알본다.

 

1. TLS

인그레스 등에서 사용하는 TLS 인증서를 시크릿으로 생성할 때 사용하는 시크릿 타입이다. 데이터 스키마로는 tls.crt 와 tls.key 를 가진다. 매니페스트에 직접 시크릿 값을 지정할 수도 있지만, 인증서 비밀키와 인증서 파일로 지정하는 것이 일반적이다.

샘플로 사용하기 위한 자체 서명 인증서를 아래 커맨드로 생성한다.

$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout ~/tls.key -out ~/.tls.crt -subj "/CN=sample1.example.com"

아래 커맨드로 key 파일과 인증서 파일 경로를 지정하여 tls 타입 인증서를 생성한다.

$ kubectl create secret tls --save-config tls-sample \
> --key ~/tls.key --cert ~/.tls.crt

 

2. Docker 레지스트

도커 허브 같은 도커 레지스트리의 private 저장소를 사용중이라면, 이미지를 업로드/다운로드 하기 위해 인증이 필요하다. k8s 에서는 이러한 인증 정보를 저장하기 위해 docker-registry 타입의 시크릿을 사용한다. .dockerconfigjson 데이터 스키마를 가지는 시크릿이다.

매니페스트로도 생성할 수 있지만, 형식이 특수하기 때문에 일반적으로 kubectl 로 직접 생성한다.

$ kubectl create secret docker-registry --save-config sample-registry-auth \
  --docker-server=REGISTRY_SERVER \
  --docker-username=REGISTRY_USER \
  --docker-password=REGISTRY_USER_PASSWORD \
  --docker-email=REGISTRY_USER_EMAIL

 

값으로는 자신이 사용하는 저장소와 계정정보를 넣으면 된다. 아래 커맨드로 시크릿을 조회해보면 dockerconfig json 문자열 자체가 base64 인코딩되어있다. 따라서 매니페스트에 작성하는 것 보다는 kubectl 로 작성하거나, 매니페스트로 저장하고 생성하고 싶다면 --dry-run -o yaml 하여 리다이렉트한 파일을 만들어 apply 하는 것이 좋다.

$ kubectl get secrets sample-registry-auth -o json | jq '.data'
{
  ".dockerconfigjson": "eyJhdXRocyI6eyJSRUdJU1RSWV9TRVJWRVIiOnsidXNlcm5hbWUiOiJSRUdJU1RSWV9VU0VSIiwicGFzc3dvcmQiOiJSRUdJU1RSWV9VU0VSX1BBU1NXT1JEIiwiZW1haWwiOiJSRUdJU1RSWV9VU0VSX0VNQUlMIiwiYXV0aCI6IlVrVkhTVk5VVWxsZlZWTkZVanBTUlVkSlUxUlNXVjlWVTBWU1gxQkJVMU5YVDFKRSJ9fX0="
}

$ kubectl get secrets sample-registry-auth -o json | jq -r '.data[".dockerconfigjson"]' | base64 --decode
{"auths":{"REGISTRY_SERVER":{"username":"REGISTRY_USER","password":"REGISTRY_USER_PASSWORD","email":"REGISTRY_USER_EMAIL","auth":"UkVHSVNUUllfVVNFUjpSRUdJU1RSWV9VU0VSX1BBU1NXT1JE"}}}

 

docker-registry 시크릿을 실제 이미지를 다운받는 파드에서 사용하려면, 파드 매니페스트의 spec.imagePullSecrets[] 에 시크릿을 지정해주면 된다. 파드에는 여러 컨테이너가 존재할 수 있으므로(멀티 컨테이너 파드) imagePullSecrets 도 배열로 지정한다.(컨테이너 각각에 시크릿을 설정하는 방법은 없다.)

apiVersion: v1
kind: Pod
metadata:
  name: sample-pull-secret
spec:
  containers:
    - name: secret-image-container
      image: REGISTRY_NAME/secret-image:latest
  imagePullSecrets:
    - name: sample-registry-auth

멀티 컨테이너에서 각각의 이미지 저장소가 다르다면, 아래와 같은 매커니즘으로 시크릿을 탐색한다.

  1. kubelet이 이미지 pull
  2. imagePullSecrets에 등록된 Secret들을 순서대로 모두 시도
  3. 해당 레지스트리에 매칭되는 인증 정보가 있으면 성공

또 다른 방법은, Secret 1개에 여러 레지스트리 인증정보를 집어넣는 것이다. 아래처럼 지정된 형식의 json 구조에 인증정보를 저장해 두고, Secret 생성시 .dockerconfigjson 이라는 key 로 지정하여 생성하면 된다.

// dockerconfigjson 파일 내용
{
  "auths": {
    "registry-a.example.com": {
      "auth": "dXNlcjpwYXNz"
    },
    "registry-b.example.com": {
      "auth": "dXNlcjI6cGFzczI="
    }
  }
}

 

아래 커맨드로 Secret 을 생성한다.

$ kubectl create secret --save-config sample-multi-registry-secret \
  --type=kubernetes.io/dockerconfigjson \
  --from-file=.dockerconfigjson=./dockerconfigjson

 

3. Basic

id, password 기반의 인증인 경우에 적합한 시크립 타입이다. username, password 데이터 스키마를 지정해야 한다.

kubectl 로 생성시 --type kubernetes.io/basic-auth 와 --from-literal 로 인증정보를 지정하여 생성하거나, 아래와 같은 매니페스트로 생성한다.

apiVersion: v1
kind: Secret
metadata:
  name: sample-basic-auth
type: kubernetes.io/basic-auth
data:
  username: cm9vdA== # root
  password: cm9vdHBhc3N3b3Jk # rootpassword

 

4. SSH

비밀키로 인증하는 시스템인 경우 SSH 인증용 스키마를 가진 ssh-auth 타입 시크릿을 사용한다. ssh-privatekey 라는 데이터 스키마를 사용하는 타입이다. 비밀키 파일이 크기 때문에 일반적으로 kubectl 로 --from-file 옵션으로 비밀키 파일을 지정하여 생성한다. 마찬가지로 매니페스트 파일로 생성하여 apply 하려면 --dry-run -o yaml 리다이렉션 하여 사용한다.

아래 커맨드를 이용하여 샘플 키를 생성하고 이를 기반으로 SSH 시크릿을 생성한다.

$ ssh-keygen -t rsa -b 2048 -f sample-key -C "sample"

$ kubectl create secret generic --save-config sample-ssh-auth \
> --type kubernetes.io/ssh-auth \
> --from-file=ssh-privatekey=./sample-key
secret/sample-ssh-auth created

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

Secret 리소스의 타입 중에 Opaque 타입은 일반적인 스키마리스 시크릿을 위한 타입이다. 시크릿 리소스 하나에는 여러 key-value 값들이 저장될 수 있다. 하나의 시크릿에 저장 가능한 사이즈는 1MB 까지이다.

생성하는 4가지 방법을 설명한다.

 

1. 매니페스트에서 생성

일반적인 리소스 생성 방식과 동일하게 kubectl apply -f <매니페스트파일명> 또는 kubectl create -f <매니페스트파일명> --save-config 으로 생성하는 방식이다. 아래 매니페스트는 username, password 를 시크릿으로 생성한다.

apiVersion: v1
kind: Secret
metadata:
  name: sample-db-auth
type: Opaque
data:
  username: cm9vdA== # root
  password: cm9vdHBhc3N3b3Jk # rootpassword

하지만 시크릿 값이 base64 로 되어있어야 한다는 번거로움이 있다. 일반 텍스트로 작성하려면 아래와 같이 data 대신 stringData 항목을 설정한다.

apiVersion: v1
kind: Secret
metadata:
  name: sample-db-auth-nobase64
type: Opaque
stringData:
  username: root
  password: rootpassword

아래 커맨드로 생성 후 조회해보자

$ kubectl create -f sample-db-auth-nobase64.yaml --save-config

$ kubectl get secrets sample-db-auth-nobase64 -o json | jq '.data'
{
  "password": "cm9vdHBhc3N3b3Jk",
  "username": "cm9vdA=="
}

$ kubectl get secrets sample-db-auth-nobase64 -o json | jq -r '.data.username' | base64 --decode
root

 

2. 리터럴 값을 전달하여 생성

kubectl 커맨드로 시크릿을 생성하되, --from-literal 옵션으로 직접 시크릿 key-value 쌍을 전달하는 방식이다. 아래처럼 생성한다.

$ kubectl create secret generic --save-config sample-db-auth \
  --from-literal=username=root --from-literal=password=rootpassword

 

3. envfile 기반 생성

kubectl 커맨드로 시크릿을 생성하되, --from-env-file 옵션으로 시크릿 key-value 쌍들이 정의된 파일을 지정하는 방식이다. Docker 에서 --env-file 옵션을 사용하여 컨테이너를 기동하는 경우에는 그대로 시크릿 이관이 가능하다. 아래처럼 생성한다.

# env-secret.txt 파일의 내용
username=root
password=rootpassword

# 생성 커맨드
$ kubectl create secret generic --save-config sample-db-auth \
  --from-env-file=./env-secret.txt

 

4. 파일의 내용을 value 로 생성

kubectl 커맨드로 시크릿을 생성하되, --from-file 옵션으로 시크릿 key-value 로 사용될 파일들을 지정하는 방식이다. 파일명이 key 가 되고, 파일 내용이 value 가 되기 때문에 아래 내용에 주의한다.

  • 파일명에 확장자를 붙이지 않는다.
  • 파일 내용에 개행이 붙지 않게 한다.

만약 파일명 대신 다른 key 를 사용하고자 한다면 --from-file=<사용할key>=<파일명> 형식으로 입력한다. 파일 내용에 개행이 붙지 않게 하려면 printf 나 echo -n 옵션으로 출력하여 리다이렉션 하면 된다. 아래 커맨드처럼 입력하여 생성한다.

$ printf "root" > ./username
$ printf "rootpassword" > ./password
$ kubectl create secret generic --save-config sample-db-auth \
  --from-file=./username --from-file=./password

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

1. 사용 이유

예를 들어 데이터베이스를 쿠버네티스 파드에 컨테이너로 띄운다면, 데이터베이스 사용자 아이디와 패스워드 같은 기밀 정보가 컨테이너에 주입되어야 한다. 이를 위한 몇가지 방법을 생각해볼 수 있다.(Docker 기준)

  • 이미지를 정의하는 Dockerfile 에 직접 아이디와 패스워드를 기재하는 방법
  • 이미지를 정의하는 Dockerfile 에 ARG 지시자로 수신하고, ENV 지시자로 런타임 변수로 저장하도록 작성하고 이미지 빌드시 --build-arg 로 빌드 인수로 아이디와 패스워드를 전달하는 방법
  • 컨테이너 실행시에만 -e 로 인수로 아이디와 패스워드를 전달하는 방법
  • Pod나 Deployment의 매니페스트 파일에 직접 아이디와 패스워드를 기재하는 방법

그러나, 이미지 정의 파일에 기재하거나 이미지 빌드시 인수로 넘기는 방법은 이미지 자체에 기밀 정보가 포함되기 때문에 보안상 바람직하지 않다. 이미지 레지스트리 서버에 올라가기 때문이다. 더구나 기밀 정보 내용을 변경할 때 마다 이미지를 재빌드 하여야 한다는 번거로움이 있다.(빌드시 DOCKER_BUILDKIT 을 사용하여 secret 을 이미지에 저장되지 않도록 할 수 있기는 하다.)

Pod나 Deployment 매니페스트에 기밀 정보를 기재하는 것도 해당 매니페스트를 github 같은 소스 저장소에 올리기 때문에 보안상 바람직하지 않다. 또한 해당 기밀 정보를 여러 애플리케이션에서 필요로 할 경우 애플리케이션 마다 기밀 정보를 중복되게 기재해야 하기 때문에 관리가 어려워진다.

이러한 문제점들을 해결하기 위해서 Secret 리소스를 사용할 수 있다. 기밀 정보를 별도의 리소스에 단일 저장해두고 여러 파드에서 읽어들이는 것이다.

2. 시크릿의 데이터 처리 구조

시크릿 데이터는 마스터 노드의 etcd 라는 데이터베이스에 저장된다. etcd 는 분산 KVS(Key-Value Store)이다. 분산 KVS란 한대의 머신에서 모두 관리하지 않고 여러대의 머신에 분산되어 관리된다는 뜻이다. 즉, 마스터 노드가 여러대인 경우 여러대에 etcd 가 분산되어 저장된다는 것이다. 시크릿의 실제 저장 위치는 /registry/secrets/<namespace>/<name> 이다. 기본적으로 시크릿 데이터는 Base64 인코딩만 되어있고 암호화 되어있지는 않다.

이렇게 저장된 시크릿은 항상 각 노드에 전송되지 않고, 실제 시크릿을 사용하는 파드가 노드에 존재하는 경우에만 해당 워커노드의 kubelet 이 마스터 노드의 API Server를 호출하여 시크릿 데이터를 가져온다. 가져온 데이터는 민감한 기밀 데이터이기 때문에 워커 노드에 영구적으로 남지 않아야 한다. 따라서 하드디스크가 아닌 RAM 기반의 가상 파일 시스템에 저장하게 되는데, 이 영역을 tmpfs 라고 한다. 위치는 /var/lib/kubelet/pods/<pod-id>/volumes/kubernetes.io~secret/ 경로 아래에 생성된다. 노드가 재부팅되거나 접근중인 Pod 가 삭제되면 메모리에서 사라지는 영역이다.

시크릿은 데이터는 기본적으로는 암호화되어있지 않기 때문에, 시크릿 매니페스트같은 파일을 소스 형상관리 서버에 직접 업로드하면 안되고, 시크릿을 암호화하는 OSS, Vault 등의 솔루션을 사용하여야 한다.

3. 시크릿 리소스의 종류(타입)

종류 용도
Opaque 범용
kubernetes.io/tls TLS 인증서용
kubernetes.io/basic-auth 기본 인증용
kubernetes.io/dockerconfigjson 도커 레지스트리 인증 정보용
kubernetes.io/ssh-auth SSH 인증 정보용
kubernetes.io/service-account-token 서비스 어카운트 토큰용
bootstrap.kubernetes.io/token Bootstrap 토큰용

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

1. 정적 설정

spec.containers[].env[] 배열에 정적인 값들을 설정한다. 다음과 같은 매니페스트로 파드를 생성해본다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-env
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      env:
        - name: MAX_CONNECTION
          value: "100"

아래 커맨드를 이용해서 컨테이너 내부에 환경 변수가 설정된 것을 확인한다.

$ kubectl exec -it sample-env -- env | grep MAX_CONNECTION
MAX_CONNECTION=100

기본적으로 컨테이너의 timezone 은 UTC 로 설정되어 있다. 아래 커맨드를 실행해보자.

$ kubectl exec -it sample-env -- date
Sun Jan  4 07:10:26 UTC 2026

 

아래처럼 정적 설정을 통해서 타임존을 Asia/Seoul 로 변경해보자.

...
      env:
        - name: MAX_CONNECTION
          value: "100"
        - name: TZ
          value: Asia/Seoul
...

단, Standalone Pod 이므로, 매니페스트 변경 후 kubectl apply 로는 변경점 적용이 되지 않는다. k8s 는 파드를 불변 객체에 가깝게 취급하기 때문에 컨테이너 환경변수를 런타임에 직접 바꾸는 것이 허용되지 않는 것이다.(허용된 필드만 런타임에 수정 적용 가능함) 따라서 파드를 삭제한 후 다시 생성한다. Deployment 로 관리하는 파드라면 Deployment 변경을 apply 하면 된다.

$ kubectl delete -f sample-env.yaml

# 매니페스트의 spec.containers[].env 변경 후 파드 재생성
$ kubectl apply -f sample-env.yaml
$ kubectl exec -it sample-env -- cat date
Sun Jan  4 16:33:23 KST 2026

 

2. 파드 정보로 설정

파드 매니페스트에서 지정한 값 이외에, 파드가 기동된 후에야 알 수 있는 값들이 있다. 이러한 추가적인 파드 정보들을 환경변수로 사용하려면, valueFrom.fieldRef.fieldPath 항목을 사용할 수 있다. fieldPath 에 들어오는 값은 kubectl get pods <파드명> -o yaml 으로 조회한 경로를 사용하면 된다.(-o json | jq 또는 -o jsonpath 으로 path 를 선택하여 보면 편하다.)

아래 예시는 파드가 실제 띄워진 노드의 이름을 K8S_NODE 라는 컨테이너 환경변수로 지정한 것이다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-env-pod
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      env:
        - name: K8S_NODE
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName

아래 명령어를 실행하여 컨테이너 내부 환경변수를 확인한다.

$ kubectl exec -it sample-env-pod -- env | grep K8S_NODE
K8S_NODE=worker02

 

3. 컨테이너 정보로 설정

파드 정보가 아닌 파드 내의 컨테이너 정보를  환경변수로 설정하기 위해서는 valueFrom.resourceFieldRef 항목을 사용한다. 세팅 가능한 path 는 파드와 마찬가지로 kubectl get pods <파드명> -o yaml 에서 확인 가능하다.(아래 예제에서는 컨테이너에 resources 항목을 직접 지정하지 않았기 때문에 -o yaml 이나 describe 로는 조회되지는 않는다.)

아래 매니페스트를 이용하여 CPU_REQUEST 에 nginx-container 컨테이너의 CPU 리소스 최솟값을, CPU_LIMITS 에 CPU 리소스 최댓값을 환경변수로 설정해 보자.

apiVersion: v1
kind: Pod
metadata:
  name: sample-env-container
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      env:
        - name: CPU_REQUESTS
          valueFrom:
            resourceFieldRef:
              containerName: nginx-container
              resource: requests.cpu
        - name: CPU_LIMITS
          valueFrom:
            resourceFieldRef:
              containerName: nginx-container
              resource: limits.cpu

아래 커맨드를 이용해 컨테이너 내부 환경변수를 확인한다.

$ kubectl exec -it sample-env-container -- env | grep CPU
CPU_REQUESTS=0
CPU_LIMITS=4

 

4. 시크릿 리소스 참조

기밀정보를 환경변수로 사용하고자 할 때는 시크릿 리소스를 생성하여 파드에서는 이를 참조하게 한다. 참조 방식이기 때문에 관리가 편하다.

 

5. 컨피그맵 리소스 참조

단순 키-밸류 값이나 설정파일을 환경변수로 사용하고자 할 때는 ConfigMap 리소스를 생성하여 파드에서는 이를 참조하게 한다. 참조 방식이기 때문에 관리가 편하다.

 

5. 환경변수를 command, args 에서 사용시 유의사항

아래 매니페스트로 파드를 실행하면 "echo ${TESTENV} ${HOSTNAME}" 커맨드가 실행되어 "100 sample-env-fail" 같은 출력이 예상된다. 하지만 실제 실행해보면 "${TESTENV} ${HOSTNAME}" 가 문자열 그대로 출력됨을 알 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: sample-env-fail
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      command: ["echo"]
      args: ["${TESTENV}", "${HOSTNAME}"]
      env:
        - name: TESTENV
          value: "100"
$ kubectl logs sample-env-fail
${TESTENV} ${HOSTNAME}

쿠버네티스에서 파드 템플릿의 command 나 args 로 명령어를 실행할 때에는 일반적인 쉘 스크립트처럼 ${변수명} 이나 $변수명 으로 변수를 사용할 수 없다. 대신 $(변수명) 을 사용해야 한다. 또한, 파드 템플릿에 정의된 환경변수만 사용 가능하다. $(변수명) 형태로 수정된 매니페스트로 파드를 실행한 후 출력 결과를 확인해보자

$ kubectl logs sample-env-fail2
100 $(HOSTNAME)

$(TESTENV) 는 정상 출력되었지만, $(HOSTNAME) 은 문자열 그대로 출력되었다. 파드 템플릿에서 정의된 변수는 TESTENV 뿐이기 때문이다. 만약 HOSTNAME 처럼 OS 에서 참조 가능한 환경 변수를 사용하고 싶다면, 별도의 셸 스크립트로 만든 후 실행해야 한다. 아래 매니페스트로 파드를 실행해 확인해보자

apiVersion: v1
kind: Pod
metadata:
  name: sample-env-success
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      command: ["/bin/sh", "-c"]
      args:
        - |
          cat << 'EOF' > /endpoint.sh
          #!/bin/sh
          echo "Endpoint script started"
          echo ${TESTENV} ${HOSTNAME}
          EOF

          chmod +x /endpoint.sh
          exec /endpoint.sh
      env:
        - name: TESTENV
          value: "100"
$ kubectl logs sample-env-success
Endpoint script started
100 sample-env-success

정상적으로 ${TESTENV} 와 ${HOSTNAME} 이 모두 출력되는 것을 볼 수 있다.

시크릿 리소스를 참조하는 방식, 컨피그맵 리소스를 참조하는 방식, 파드 정보를 참조하는 방식(fieldRef), 컨테이너 정보를 참조하는 방식(resourceFieldRef) 모두 마찬가지로 $(변수명) 으로 사용해야 하며, 파드 템플릿에 정의한 환경변수만 사용 가능하다. 아래 매니페스트로 파드를 실행해 확인해보자

apiVersion: v1
kind: Pod
metadata:
  name: sample-env-fail3
  labels:
    app: sample-app
spec:
  containers:
    - name: nginx-container
      image: nginx:1.27
      command: ["echo"]
      args: ["$(K8S_NODE)", "${K8S_NODE}"]
      env:
        - name: K8S_NODE
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
$ kubectl logs sample-env-fail3
worker01 ${K8S_NODE}

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,