無知

갈 길이 먼 공부 일기

기술 공부/쿠버네티스

쿠버네티스 (7) | 컨피그맵, 시크릿

moozii 2022. 2. 23. 19:18
앞으로의 스터디 내용은 <Kubernetes in Action>을 기반으로 진행합니다.
자세한 내용은, 해당 책을 확인해주세요! 
http://www.yes24.com/Product/Goods/89607047 

 

애플리케이션은, 빌드된 애플리케이션 자체에 포함해서는 안되지만 필요한 설정 데이터들이 있다. 배포된 인스턴스별로 다른 설정들도 있고, 외부 시스템에 접근하기 위해 필요한 자격증명 관련 데이터도 있다. 이런 설정 옵션을 전달하는 방식에 대해 공부해 볼 예정이다.

 

The configMap and Volume interact to provide configuration for containers.&nbsp;https://www.docker.com/blog/designing-your-first-application-kubernetes-configuration-part4/

컨테이너화된 애플리케이션 설정

일반적인 컨테이너화 애플리케이션은 다음과 같이 설정 옵션을 넘겨줄 수 있다.

  1. 명령줄 인수로 애플리케이션 설정 넘겨주기
  2. 환경변수 사용하기
  3. 볼륨을 통한 마운트

상대적으로 환경 변수의 사용이 더 보편적인데, 그 이유는 도커 컨테이너 내부 설정 파일을 사용하기에 까다롭기 때문이다.

설정 파일을 이미지 안에 넣거나 파일을 가진 볼륨을 마운트해야 하는데,

이미지 내에 빌드하기에는 비밀을 유지해야 할 정보들이 이미지에 접근하는 모든 사람들에게 공개되고,

볼륨을 사용해서 마운트하기에는 컨테이너를 시작하기 전마다 파일이 볼륨에 있는지 확인해야 하는 것이 번거롭다.

 

이를 위해 사용하는 쿠버네티스 리소스가 컨피그맵이다.

설정 데이터를 쿠버네티스 리소스에 저장하고, 이를 깃 저장소 혹은 다른 파일 기반 스토리지에 저장하는 방법이다.

민감한 정보를 포함한 설정 옵션의 경우에는 시크릿 오브젝트를 활용한다.

(예를 들어 자격 증명, 개인 암호화 키 등 보안 관련 데이터가 있다)

 

 

1. 명령줄 인자를 컨테이너에 전달하기

 

쿠버네티스는 파드 컨테이너 정의에 지정된 실행 명령 대신 다른 실행파일을 실행시키거나, 다른 명령줄 인자를 사용해 실행할 수 있다. 

 

도커 명령어와 인자 정의 

  • 도커 명령의 구성
    • 명령어와 인자, 2개 부분으로 구성
  • ENTRYPOINT, CMD
    • ENTRYPOINT : 컨테이너 시작 시 호출하는 명령어 정의. 
    • CMD : ENTRYPOINT 전달되는 인자 정의.
      • 명령어 지정도 가능하지만 기본 인자 정의 시에만 활용하는 것을 권고.
      • 만약 CMD로 도커 파일에서 설정한 인자를 재정의하고 싶다면 도커 이미지 실행 시 arguments를 추가해
        $ docker run <image> <arguments> 와 같이 입력
  • shell과 exec 형식의 차이점
    • shell : 내부에서 정의된 명령을 쉘로 호출. 쉘 내부에서 실행하므로 메인 프로세스가 쉘 프로세스임. 불필요한 쉘 프로세스가 하나 더 추가되므로 아래의 방법을 권고.
      • ENTRYPOINT node app.js
    • exec : 컨테이너 내부에서 node 프로세스를 직접 실행.
      • ENTRYPOINT ["node", "app.js"]

 

실습을 아래와 같이 진행했다. 

fortune 스크립트 및 이미지에서 반복하는 주기를 변경할 수 있도록 수정하기 위해서, INTERVAL 변수를 추가하고, 첫번째 명령줄 인자의 값으로 초기화되도록 설정한다.

$ cat fortune-args/fortuneloop.sh 
#!/bin/bash
trap "exit" SIGINT

INTERVAL=$1
echo Configured to generate new fortune every $INTERVAL seconds

mkdir -p /var/htdocs

while :
do
  echo $(date) Writing fortune to /var/htdocs/index.html
  /usr/games/fortune > /var/htdocs/index.html
  sleep $INTERVAL
done

그리고 그에 맞게 도커 파일도 함께 수정한다.

$ cat fortune-args/Dockerfile 
FROM ubuntu:latest

RUN apt-get update ; apt-get -y install fortune
ADD fortuneloop.sh /bin/fortuneloop.sh

ENTRYPOINT ["/bin/fortuneloop.sh"]
CMD ["10"]

해당 이미지를 빌드해 도커 허브에 푸시하자. 

단, 도커 이미지 태그를 latest가 아닌 args로 지정해 구분하도록 하자. 

$ cd fortune-args 
$ docker build -t docker.io/joon0615/fortune:args .
(중략)

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

$ docker push docker.io/joon0615/fortune:args
The push refers to repository [docker.io/joon0615/fortune]
(중략)
args: digest: sha256:c19926f3a7e7cd4f8d633f1974fc3bf926bfe08b602963041f0f53d8ac1399a9 size: 948

$ docker run -t docker.io/joon0615/fortune:args
Configured to generate new fortune every 10 seconds
Mon Feb 21 01:55:23 UTC 2022 Writing fortune to /var/htdocs/index.html
Mon Feb 21 01:55:34 UTC 2022 Writing fortune to /var/htdocs/index.html
Mon Feb 21 01:55:43 UTC 2022 Writing fortune to /var/htdocs/index.html
Mon Feb 21 01:55:53 UTC 2022 Writing fortune to /var/htdocs/index.html
^C 

$ docker run -it docker.io/joon0615/fortune:args 15
Configured to generate new fortune every 15 seconds
Mon Feb 21 01:56:22 UTC 2022 Writing fortune to /var/htdocs/index.html
Mon Feb 21 01:56:37 UTC 2022 Writing fortune to /var/htdocs/index.html
Mon Feb 21 01:56:52 UTC 2022 Writing fortune to /var/htdocs/index.html
Mon Feb 21 01:57:07 UTC 2022 Writing fortune to /var/htdocs/index.html
Mon Feb 21 01:57:22 UTC 2022 Writing fortune to /var/htdocs/index.html
^C

위와 같이 도커 컨테이너를 실행시킬 수 있다. 

또한 인자를 추가적으로 입력해 주기를 바꾸는 실습도 진행해보았다.

Ctrl+C를 입력하면 스크립트를 중지할 수 있음도 확인했다.

$ cat fortune-pod-args.yaml
apiVersion: v1
kind: Pod
metadata:
  name: fortune2s
spec:
  containers:
  - image: joon0615/fortune:args
    args: ["2"]
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    emptyDir: {}

이번에는 컨테이너 이미지를 args 태그 이미지로 변경했고, args 값을 추가해 2초마다 메시지를 생성하도록 지정값을 변경해주었다. 

args값을 하나지만 배열 형태로 작성했는데, 배열 표기법은 여러 개의 인자를 가진 경우에도 유용하다고 한다. 

(문자열 값을 따로 따옴표로 묶을 필요는 없지만 숫자는 묶어야 한다)

 

 

2. 환경변수 설정으로 옵션 전달하기

 

파드의 각 컨테이너를 위한 환경변수 리스트 지정이 가능하다. 파드 수준으로도 환경변수를 설정할 수 있다면 좋겠지만 쿠버네티스 인 액션 책 집필 당시에는 이러한 옵션이 없다고 밝혔다. 

https://kubernetes.io/ko/docs/tasks/inject-data-application/environment-variable-expose-pod-information/

 

환경 변수로 컨테이너에 파드 정보 노출하기

본 페이지는 파드에서 실행 중인 컨테이너에게 파드가 환경 변수를 사용해서 자신의 정보를 노출하는 방법에 대해 설명한다. 환경 변수는 파드 필드와 컨테이너 필드를 노출할 수 있다. 시작하

kubernetes.io

관련 문서를 찾아봐도, 변수의 값을 어디서 받아오는가가 파드 수준인가 컨테이너 수준인가의 차이일뿐 환경변수 리스트 지정은 컨테이너 단위인듯 보이니 이 글을 작성하는 시점에도 유사한 것 같다. 

 

fortune-env/fortuneloop.sh 쉘 스크립트에 $INTERVAL이라는 변수로 메시지 작성 주기를 지정해두었는데, 

본래 있던 변수 초기화 부분을 삭제했다.

$ cd fortune-env 

$ cat fortuneloop.sh 
#!/bin/bash
trap "exit" SIGINT
echo Configured to generate new fortune every $INTERVAL seconds
mkdir -p /var/htdocs
while :
do
  echo $(date) Writing fortune to /var/htdocs/index.html
  /usr/games/fortune > /var/htdocs/index.html
  sleep $INTERVAL
done

$ docker build -t docker.io/joon0615/fortune:env . 
[+] Building 2.5s (9/9) FINISHED                                                
(중략)
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

$ docker push docker.io/joon0615/fortune:env
The push refers to repository [docker.io/joon0615/fortune]
754b1d7271e7: Pushed 
825dea5881e8: Layer already exists 
36ffdceb4c77: Layer already exists 
env: digest: sha256:064bee09d80943e997a8c33d688d284d8db35846237d56b9bfe6ce8daca66b8d size: 948

 

환경변수를 컨테이너 정의에 포함해 스크립트에 전달하고자 환경변수 목록에 변수를 추가하자. 

파드 레벨이 아닌 컨테이너 정의 내에 포함되는 내용이다. 

$ cat fortune-pod-env.yaml
apiVersion: v1
kind: Pod
metadata:
  name: fortune-env
spec:
  containers:
  - image: joon0615/fortune:env
    env:
    - name: INTERVAL
      value: "30"
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    emptyDir: {}

각 컨테이너를 설정할 때, 쿠버네티스는 자동으로 동일 네임스페이스 내 각 서비스에 환경변수를 노출한다.

자동 주입식 설정이 기본임을 유의하자. 

 

또한, 아래와 같이 다른 환경변숫값을 참조하여 변수값을 설정할 수 있다.

env:
- name: FIRST_VAR
  value: "foo"
- name: SECOND_VAR
  value: "$(FIRST_VAR)bar"

 

 

3. 컨피그맵으로 설정 분리하기

 

이렇게 파드 정의에 하드코딩된 값을 불러오면 효율적이지만, 프로덕션과 개발을 분리하기 위해서는 서로 분리된 파드 정의가 필요함을 뜻하기도 한다. 여러 환경에서 동일한 파드 정의를 재사용하려면 파드 정의와 설정을 분리해야 한다는 의미이다. 이를 위해 컨피그맵 리소스를 사용하여, value 필드 대신 valueFrom으로 환경변숫값의 원본 출처로 사용할 수 있다. 환경에 따라 달라지기도 하고, 자주 변경되는 설정 옵션을 애플리케이션 소스 코드와 별도로 유지하기 위해 파드 정의 밖으로 이동시키는 것이다.

 

컨피그맵

키-값 쌍으로 구성된 설정 파일을 저장해, 설정옵션을 분리하는 별도 오브젝트가 컨피그맵이다. 

애플리케이션은 컨피그맵을 알지 못해도, 맵의 내용은 컨테이너 환경변수나 볼륨 파일 형태로 전달되며, 

$(ENV_VAR) 구문을 사용해 명령줄에도 참조해서 명령줄 인자로도 전달 가능하다. 

 

애플리케이션은 쿠버네티스 REST API 엔드포인트를 통해 컨피그맵 내용을 읽을 수 있지만, 되도록 애플리케이션은 쿠버네티스와 무관하도록 유지되어야 한다.

 

컨피그맵이란 별도의 독립적인 오브젝트에 설정을 포함시키는 것이므로, 각기 다른 환경에서도 컨피그맵은 동일한 이름으로 여러 매니페스트를 유지할 수 있고, 동일한 파드 정의로 생성된 여러 파드들이 서로 다른 컨피그맵을 참조해 서로 다른 설정을 사용할 수 있다. 말을 풀어서 해보자면, 같은 파드 매니페스트로 하나는 개발 환경에 파드를, 하나는 프로덕션 환경에 파드를 설정했다고 해보자. 그리고 파드는 컨피그맵의 값을 참조하게 된다. 그런데 이때 개발 환경의 컨피그맵과 프로덕션 환경의 컨피그맵은 서로 다른 매니페스트로 생성된 컨피그맵일 수 있다. 그래서 개발 네임스페이스 내 컨피그맵은 개발에 사용할 값을, 프로덕션 네임스페이스 내 컨피그맵은 프로덕션에 사용할 값을, 서로 다른 값을 지니게 된다는 것이다. 그래도 이름만 동일하다면, 각기 다른 값을 지닌 컨피그맵이라 할지라도 각각의 파드는 문제 없이 참조가 가능하다. 

 

컨피그맵 생성하기

 

실습 안내에 따르면

kubectl create -f configmap.yaml로도 생성 가능하지만,
kubectl create configmap 명령어를 통해 컨피그맵을 생성할 수 있다고 한다.

 

sleep-interval=25라는 단일 항목을 지닌 컨피그맵을 생성해보았다.

$ kubectl create configmap fortune-config --from-literal=sleep-interval=25
configmap/fortune-config created

$ kubectl get configmap
NAME               DATA   AGE
fortune-config     1      22s
kube-root-ca.crt   1      27d

$ kubectl describe configmap fortune-config
Name:         fortune-config
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
sleep-interval:
----
25

BinaryData
====

Events:  <none>

컨피그맵은 일반적으로 위와 같은 단일 항목 구성보다는, 여러 항목을 포함하는 형식을 취한다. 

이를 위해서는 여러개의 --from-literal 인자를 추가해야 한다. 

$ kubectl create configmap myconfigmap --from-literal=foo=bar --from-literal=bar=baz --from-literal=one=two
configmap/myconfigmap created

$ kubectl describe configmap myconfigmap   
Name:         myconfigmap
Namespace:    default
Labels:       <none>
Annotations:  <none>
Data
====
bar:
----
baz
foo:
----
bar
one:
----
two
BinaryData
====
Events:  <none>

 

kubectl get configmap {configmap_name} -o yaml을 통해 YAML 정의를 출력할 수 있다.

$ kubectl get configmap myconfigmap -o yaml
apiVersion: v1
data:
  bar: baz
  foo: bar
  one: two
kind: ConfigMap
metadata:
  creationTimestamp: "2022-02-22T06:29:57Z"
  name: myconfigmap
  namespace: default
  resourceVersion: "104380"
  uid: 90276d43-a227-40d9-9f53-97fbe10c9d30
$ kubectl get configmap fortune-config -o yaml
apiVersion: v1
data:
  sleep-interval: "25"
kind: ConfigMap
metadata:
  creationTimestamp: "2022-02-22T00:51:58Z"
  name: fortune-config
  namespace: default
  resourceVersion: "90168"
  uid: 5c06241a-c7c2-4834-b44e-b9d82489c907
  
$ cat fortune-config.yaml 
apiVersion: v1
kind: ConfigMap
metadata:
  name: fortune-config
data:
  sleep-interval: "25"

위에 2가지는 다음과 같은 차이를 보인다. 

첫번째로 YAML 정의를 get 해온 컨피그맵의 경우에는 우리가 --from-literal을 통해 인자를 추가한 컨피그맵으로, 쿠버네티스가 자동으로 생성해주었다고 볼 수 있다. 

두번째 yaml 파일은 기존 쿠버네티스 인 액션 도서의 깃헙에 저장된 yaml 파일로, 데이터와 이름이 모두 동일하지만, 기타 메타데이터는 없는 모습이다. 그 부분은 yaml 파일에 굳이 미리 지정할 필요가 없기 때문에 따로 없다. 

2번째 파일과 같은 yaml 파일을 통해 미리 정의한 컨피그맵은 다음과 같이 쿠버네티스 API에 게시할 수 있다. 

$ kubectl create -f fortune-config.yaml

 

설정 파일로 컨피그맵 생성하기

 

컨피그맵에 전체 설정 파일도 한번에 저장하는 것이 가능하다. 

$ kubectl create configmap my-config --from-file=config-file.conf

다음의 명령어를 실행한 디렉토리 내 config-file.conf와 같은 이름의 설정파일을 찾아 해당 파일의 내용을 컨피그맵의 config-file.conf 키 값으로 저장한다. 즉, 명령에 --from-file을 통해 파일을 디스크에서 읽어 개별 항목으로 저장하는 것이다. 

 

디렉토리 내 파일 전체로 컨피그맵 생성하기

 

특정 파일이 아니라, 디렉터리에 있는 모든 파일을 가져올 수도 있다.

$ kubectl create configmap my-config --from-file=/path/to/dir

디렉토리 내 각 파일을 개별 항목으로 작성하는데, 이때 디렉토리 내 파일의 파일명이 컨피그맵 키로 사용 가능한 파일만 추가된다.

 

옵션 결합하기

$ kubectl create configmap my-config 
  --from-file=foo.json 			// key=foo.json, value=foo.json contents
  --from-file=bar=foobar.conf 		// key=bar, value=foobar.conf contents
  --from-file=config-opts/		// total directory under config-opts
  --from-literal=some=thing		// key=some, value=thing

 

컨피그맵 항목을 환경변수로 컨테이너에 전달하기

$ cat fortune-pod-env-configmap.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: fortune-env-from-configmap
spec:
  containers:
  - image: joon0615/fortune:env
    env:
    - name: INTERVAL
      valueFrom: // 고정값이 아닌 컨피그맵의 키 안에서 값을 가져와 초기화함
        configMapKeyRef:
          name: fortune-config // 참조하는 컨피그맵 이름
          key: sleep-interval // 컨피그맵에서 해당 키 아래에 저장된 값으로 변수 설정
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    emptyDir: {}

파드 정의 내에 컨테이너의 환경변수 상에 INTERVAL이라는 변수를 선언하고, 그 값을 컨피그맵의 값을 참조하도록 하여 생성하도록 한다. 

그렇게 값을 받아오면, 해당 변수는 해당 컨테이너 이미지를 통해 빌드된 컨테이너 html-generator가 참조하여 fortuneloop.sh 프로세스를 수행한다. 

 

존재하지 않는 컨피그맵이 파드 정의에 기록되어 있다면

컨테이너의 시작이 실패한다. 하지만 파드 내 존재하지 않는 컨피그맵을 포함하지 않는 다른 컨테이너들은 정상적으로 시작된다.  만약 누락된 컨피그맵을 늦게나마 생성해주면, 사용자가 굳이 파드 전체를 다시 만들지 않아도, 과거에 실패한 컨테이너가 알아서 시작된다. 컨피그맵 참조 자체를 옵션으로 가져가도록 파드 정의 상에 configMapKeyRef.optional: true로 지정한다면 컨피그맵이 존재하지 않아도 컨테이너가 시작된다. 

 

컨피그맵의 모든 항목을 한 번에 환경변수로 전달

컨피그맵의 모든 항목을 환경변수로 한번에 노출하는 방법이 있다. 앞에서는 각 항목을 일일이 환경변수로 생성하는 것은 번거롭기도 하고 오류가 발생하기 쉬운 수동적인 입력 방법이기 때문에 한번에 자동적으로 환경변수로 전달하는 다음과 같은 방법을 선택하는 것이 좋다. 

env 속성 대신 envFrom 속성을 사용하면 한번에 환경변수로 모두 노출할 수 있다.

spec:
  containers:
  - image: some-image
    envFrom:
    - prefix: CONFIG_ 		// 모든 환경변수는 CONFIG_ 라는 접두사를 가짐 
      configMapRef:
        name: my-config-map // 참조하는 컨피그맵 이름

환경변수 앞에 붙을 접두사를 별도로 지정할 수 있고, 그에 따라 위 예시에는 CONFIG_FOO, CONFIG_BAR과 같은 환경변수를 가진다.

참조한 컨피그맵에 담긴 항목 중 올바른 환경변수 이름 형식을 지키지 않은 경우, (예를 들어 dash -를 포함한 경우) 쿠버네티스가 임의로 키로 변환하지 않고 건너뛴다. (다만 건너뛰었다고 이벤트로 기록되어 추후에 확인 가능하다)

 

컨피그맵 항목을 명령줄 인자로 전달

컨피그맵 항목을 환경변수로 초기화하고 인자로 참조하도록 지정하는 방식으로 명령줄 인자 전달이 가능하다.

컨피그맵의 항목을 컨테이너 내 환경변수로 전달하면, 그 환경변수가 쉘스크립트 내 변수로 전달되는 방식이다. 

즉, 컨테이너 정의에 args: ["$ENVVARIABLENAME"]을 통해 명령줄 인자로 전달하는 것이다.

$ cat fortune-pod-args-configmap.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: fortune-args-from-configmap
spec:
  containers:
  - image: joon0615/fortune:args	// 	해당 이미지에서 첫번째 인자에서 간격을 가져옴
    env:
    - name: INTERVAL				// 컨피그맵에서 환경 변수를 정의
      valueFrom: 
        configMapKeyRef:
          name: fortune-config
          key: sleep-interval
    args: ["$(INTERVAL)"] 			// 첫번째 인자로 앞에서 설정한 환경변수를 지정
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    emptyDir: {}

 

컨피그맵 볼륨을 통한 컨피그맵 항목 파일 노출

환경변수나 명령줄 인자를 통한 설정 옵션 전달은 변숫값이 짧을 때 사용하는 방식으로, 전달해야 하는 양이 많을 때는 일반적으로 컨피그맵 볼륨을 활용한다. 

 

파드의 컨테이너 안에서 실행되는 웹 서버 환경설정용 설정 파일을 사용한다고 하자. 

해당 NginX 웹서버가, 클라이언트로 응답을 압축해서 보내려 할 때, 압축을 사용하려면 아래와 같이 gzip 압축을 활성화해야 한다.

$ cat configmap-files/my-nginx-config.conf 
server {
    listen              80;
    server_name         www.kubia-example.com;

    gzip on;
    gzip_types text/plain application/xml;
    // 일반 텍스트와 xml 파일에 대해 gzip 압축 활성화

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

}
$ kubectl delete configmap fortune-config 
configmap "fortune-config" deleted

$ cd configmap-files 

configmap-files $ ls
my-nginx-config.conf	sleep-interval

configmap-files $ cat sleep-interval 
25

$ cd ../

$ kubectl create configmap fortune-config --from-file=configmap-files/
configmap/fortune-config created

$ kubectl get configmap fortune-config -o yaml
apiVersion: v1
data:
  my-nginx-config.conf: |
    server {
        listen              80;
        server_name         www.kubia-example.com;

        gzip on;
        gzip_types text/plain application/xml;

        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }

    }
  sleep-interval: |
    25
kind: ConfigMap
metadata:
  creationTimestamp: "2022-02-22T14:34:55Z"
  name: fortune-config
  namespace: default
  resourceVersion: "119811"
  uid: 06102a09-b9ed-412b-a412-f6e01f960018

그렇게 생성한 컨피그맵을, 파드의 컨테이너들이 사용하도록 해보자. 

컨피그맵 항목을 볼륨 속의 파일로 전달하기 위해서는, 컨테이너의 파일시스템과 연동된 볼륨을 만들어 컨피그맵과 이어주어야 한다.

컨피그맵 항목에서 생성된 파일로 볼륨을 초가화하면 되는 것이다.

$ cat fortune-pod-configmap-volume.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: fortune-configmap-volume
spec:
  containers:
  - image: joon0615/fortune:env
    env:
    - name: INTERVAL
      valueFrom:
        configMapKeyRef:
          name: fortune-config
          key: sleep-interval
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    - name: config
      mountPath: /etc/nginx/conf.d 		// 컨피그맵 볼륨을 마운트하는 위치
      readOnly: true
    - name: config
      mountPath: /tmp/whole-fortune-config-volume
      readOnly: true
    ports:
      - containerPort: 80
        name: http
        protocol: TCP
  volumes:
  - name: html
    emptyDir: {}
  - name: config		// fortune-config 컨피그맵을 참조하는 볼륨
    configMap:
      name: fortune-config

curl 명령을 이용해 서버 응답을 확인하고, 디렉토리 내 파일들을 확인해보자.

** 서버 응답을 확인해보는 과정에서 본래는 ETag 아래에 Content-Encoding: gzip 이라는 줄이 보여진다는데 확인이 어렵다.

$ kubectl create -f fortune-pod-configmap-volume.yaml 
pod/fortune-configmap-volume created

$ kubectl port-forward fortune-configmap-volume 8080:80 &
[1] 58736
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
Handling connection for 8080

------------------------------------
$ curl -H "Accept-Encoding: gzip" -I localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.21.6
Date: Tue, 22 Feb 2022 15:16:39 GMT
Content-Type: text/html
Content-Length: 85
Last-Modified: Tue, 22 Feb 2022 15:16:15 GMT
Connection: keep-alive
ETag: "6214fe3f-55"
Accept-Ranges: bytes

$ kubectl exec fortune-configmap-volume -c web-server ls /etc/nginx/conf.d
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
my-nginx-config.conf
sleep-interval

$ kubectl exec fortune-configmap-volume -c web-server -- ls /etc/nginx/conf.d
my-nginx-config.conf
sleep-interval

여러 컨피그맵을 동일한 파드의 컨테이너들을 구성하는 데에 사용하면 안된다. 

동일한 파드의 컨테이너들은 컨테이너가 서로 밀접하게 관계되어 하나의 유닛으로 설정되고, 하나의 컨피그맵을 사용하는 것이 바람직하다. 여러 컨피그맵을 각 컨테이너에 나누어 사용하게 할 수는 있지만 바람직하지는 않다.

 

디렉토리를 마운트할 때 기존 파일을 숨긴다고?

볼륨을 디렉토리에 마운트하면, 우리가 마운트하는 위치(예를 들어 예제에서는 "- name: config \n mountPath: /etc/nginx/conf.d" 디렉토리)의 디렉토리 안에 있는, 컨테이너 이미지 자체의 저장 파일들은 숨겨졌다는 것이다. 리눅스 상에서 비어 있지 않은 디렉토리 위에 파일 시스템을 마운트하면 이런 식으로 기존 파일을 숨김 처리한다고 한다. 우리가 마운트한 파일시스템 내 파일만 포함하고, 기존 파일은 마운트 중에는 접근이 불가능해지는 것이다. 

이를 염두에 두어야 하는 이유는, 중요한 파일을 가지고 있는 디렉토리에 무턱대고 마운트를 했다가 컨테이너 손상이 일어날 가능성이 있기 때문이다. 예를 들어, /etc의 경우는 컨테이너의 중요 파일을 주로 포함하는 디렉토리인데, 이 디렉토리 전체에 바로 볼륨을 마운트하면, 컨테이너 작동에 필요한 다른 주요 원본 파일들이 숨김처리되면서 손상이 발생하는 것이다. 즉, /etc 디렉토리 상에 파일을 추가해야 하면 단순한 마운트 방법 외의 새로운 방법이 필요하다. 

 

기존 파일을 안숨기고 개별 항목을 파일로 마운트하면 손상을 막는다

앞의 간편한 방법의 단점을 살펴보았으니, 컨피그맵 항목들을 개별 파일로 기존 디렉토리 안에 추가해서, 기존 파일의 숨김 처리는 일어나지 않도록 해보자. 전체 볼륨을 마운트하지 않고, 즉 volumeMount를 사용하지 않고, subPath 속성으로 파일 혹은 디렉토리 1개를 볼륨에 마운트하는 방식이다. 

 

컨피그맵 상에 정의된 항목들은 각각 컨피그맵 볼륨 상에 올라가면, 컨테이너 상의 파일시스템 중 각기 다른 개별 파일에 파일시스템 내 새로운 이름의 파일로 마운트되는 것이다. 

spec:
  containers:
  - image: some/image
    volumeMounts:
    - name: myVolume
      mountPath: /etc/someconfig.conf 	// 디렉토리가 아닌 특정 파일에 마운트
      subPath: myconfig.conf 		// 전체 볼륨을 마운트하는 대신 myconfig.conf 항목만 마운트

subPath 속성은 모든 볼륨 종류를 마운트할 수 있지만 전체가 아닌 일부, 개별 파일을 마운트하는 방식에 사용된다. 

이런 개별적인 파일 단위 마운트는 파일 업데이트 측면에서의 불편함이 존재한다. 

 

컨피그맵 볼륨 내 파일 권한 설정

컨피그맵 볼륨 내 모든 파일의 권한은 644, -rw-r--r--로 설정된다. (책의 오타에 유의하자)

첫 글자는 일반 파일인지, 디렉토리인지를 의미합니다.
'-'는 '파일'임을 암시하는데, '디렉토리(폴더)'의 경우에는 'd'로 표시됩니다.
예 ) 파일 : -r-x-xrwx, 디렉토리 : drwxrwxrwx
그렇다면 뒤에 rwx 는 무엇을 의미할까요?
r : read(읽기) w : write(쓰기) x : execute(실행)
그런데 왜 3개나 표시가 되어 있을까요? 이는, 각 각의 사용자들의 권한을 표시하기 위입니다.
앞에 디렉토리/파일 인디케이터를 제외한, 나머지 9개를 3개씩 나눠보면..
rwx / rwx / rwx
첫번째 rwx 는 소유자를 의미, 두번째 rwx 는 그룹권한을 의미, 세번째 rwx 는 다른 사용자의 권한을 의미합니다.
출처) Linux 권한(permission) 정리
https://itseminar.tistory.com/11 
더 알아보기: https://hack-cracker.tistory.com/32 

별도로 파일 권한 설정을 변경하고자 하면 다음과 같이 볼륨 정의를 변경해야 한다.

$ cat fortune-pod-configmap-volume-defaultMode.yaml
apiVersion: v1
kind: Pod
metadata:
  name: fortune-configmap-volume
spec:
  containers:
  - image: joon0615/fortune:env
    env:
    - name: INTERVAL
      valueFrom:
        configMapKeyRef:
          name: fortune-config
          key: sleep-interval
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    - name: config
      mountPath: /etc/nginx/conf.d
      readOnly: true
    - name: config
      mountPath: /tmp/whole-fortune-config-volume
      readOnly: true
  volumes:
  - name: html
    emptyDir: {}
  - name: config
    configMap:
      name: fortune-config
      defaultMode: 0660 	// 644에서 600으로 변경 -rw-rw----

파일 소유자와 그룹만 파일을 읽고 쓸 수 있도록 만들었다. 

 

애플리케이션 재시작 없이 설정만 업데이트하기

환경 변수 혹은 명령줄 인자를 통한 설정 전달의 단점은, 프로세스 실행 중의 업데이트가 불가능하다는 것이다.

컨피그맵을 사용해 볼륨으로 설정을 노출하면 그와 달리 파드와 분리되기 때문에 파드를 다시 만들거나 컨테이너를 재시작할 필요 없이 업데이트가 가능하다. 컨피그맵을 업데이트하면, 그를 참조하는 모든 볼륨들의 파일들이 업데이트된다. 그 뒤에는 프로세스가 자동적으로 볼륨 내 파일들의 변경을 감지해 새로이 로드한다. 쿠버네티스는 파일 업데이트 후 컨테이너에 신호를 보내는 것을 지원하기도 한다. (컨피그맵 업데이트 -> 볼륨 내 파일 업데이트 사이의 지연 시간은 생각보다 길 수 있다. 최대 1분 정도로 보면 된다.)

 

아래와 같이 컨피그맵을 수정한 후 컨테이너 내의 파일 내용을 출력하는 명령어를 실행시키면, 다음과 같이 변경됨을 확인할 수 있다.

$ kubectl edit configmap fortune-config 

  my-nginx-config.conf: |
    server {
        listen              80;
        server_name         www.kubia-example.com;

        gzip on; 	// gzip off로 변경후 :wq로 저장
        gzip_types text/plain application/xml;

        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }

    }
  sleep-interval: |
    25
kind: ConfigMap
metadata:
  creationTimestamp: "2022-02-22T14:34:55Z"
  name: fortune-config
  namespace: default
  resourceVersion: "119811"
  uid: 06102a09-b9ed-412b-a412-f6e01f960018
$ kubectl edit configmap fortune-config 
configmap/fortune-config edited

$ kubectl exec fortune-configmap-volume -c web-server -- cat /etc/nginx/conf.d/my-nginx-config.conf
server {
    listen              80;
    server_name         www.kubia-example.com;

    gzip off;
    gzip_types text/plain application/xml;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

}

 

설정을 다시 로드하기 위해 Nginx에 신호 전달하기

$ kubectl exec fortune-configmap-volume -c web-server -- nginx -s reload
2022/02/23 06:53:59 [notice] 36#36: signal process started

$ kubectl port-forward fortune-configmap-volume 8080:80 &
[1] 29755
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
Handling connection for 8080
Handling connection for 8080

--------------------------------------
$ curl localhost:8080
You will hear good news from one you thought unfriendly to you.

$ curl -H "Accept-Encoding: gzip" -I localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.21.6
Date: Wed, 23 Feb 2022 06:55:09 GMT
Content-Type: text/html
Content-Length: 135
Last-Modified: Wed, 23 Feb 2022 06:54:58 GMT
Connection: keep-alive
ETag: "6215da42-87"
Accept-Ranges: bytes

앞선 실습에서도 인코딩 여부를 확인하지 못했기 때문에 설정 변경 영향을 확인하는 본 실습의 결과에서도 인코딩 헤더가 없는 것이 놀랍지는 않다... 왜 안떴는지 의문..

 

파일을 한번에 업데이트하는 방법

컨피그맵 볼륨 내 모든 파일을 쿠버네티스가 변경을 감지해 업데이트하는 메커니즘에 대해 알아보자.

쿠버네티스는 심볼릭 링크를 통해 이 작업을 수행한다. 

$ kubectl exec -it fortune-configmap-volume -c web-server -- ls -lA /etc/nginx/conf.d
total 4
drwxr-xr-x    2 root     root          4096 Feb 23 06:53 ..2022_02_23_06_53_32.3254026991
lrwxrwxrwx    1 root     root            32 Feb 23 06:53 ..data -> ..2022_02_23_06_53_32.3254026991
lrwxrwxrwx    1 root     root            27 Feb 23 06:36 my-nginx-config.conf -> ..data/my-nginx-config.conf
lrwxrwxrwx    1 root     root            21 Feb 23 06:36 sleep-interval -> ..data/sleep-interval

마운트된 컨피그맵 볼륨 내 파일은, ..data/ 디렉토리 파일들을 가리키는 심볼릭 링크임을 확인할 수 있다. 

컨피그맵이 업데이트되면, 쿠버네티스는 이와 같은 새 디렉토리를 생성해 모든 파일을 해당 디렉토리에 작성하고, ..data 심볼릭링크가 새 디렉토리를 가리키도록 해 모든 파일을 한번에 효과적으로 변경한다. 

심볼릭 링크(symbolic link) 란? - 링크를 연결하여 원본 파일을 직접 사용하는 것과 같은 효과를 내는 링크이다. 윈도우의 바로가기와 비슷한 개념 - 특정 폴더에 링크를 걸어 NAS, library 원본 파일을 사용하기 위해 심볼릭 링크를 사용한다.
출처: https://qjadud22.tistory.com/22

 

그러면 이미 존재하는 디렉토리에 파일만 마운트하는 방식은 왜 업데이트가 되지 않을까

그 이유는, 개별 파일을 추가하고 원본 컨피그맵을 업데이트할 때 파일을 업데이트시키려면, 전체 볼륨을 다른 디렉토리에 마운트한 다음, 해당 파일을 가리키는 심볼릭 링크를 생성해야 하는데, 컨테이너 이미지에서 심볼릭 링크를 만들거나, 컨테이너를 시작할 때 심볼릭 링크를 만들 수 있기 때문이다. 즉 신규 심볼릭 링크 생성 측면에서 파일 단위로는 진행하기 어렵다는 것이다. 

 

중도 컨피그맵 수정은 불변성을 해친다.
다시 읽기를 지원하는 애플리케이션에 한해 수정을 통한 비동기 업데이트는 가능하다.

컨테이너의 핵심은 불변성이다. Immutability, 불변성이란 동일한 이미지에서 생성된 여러 컨테이너 간에 차이가 없다는 것이다. 

그러면 컨피그맵 수정은 그 불변성을 해친다고 볼 수 있다. 애플리케이션이 설정을 다시 읽는 기능을 지원하지 않는 경우에는, 인스턴스들의 설정이 서로 다른 상황이 발생할 수 있다. 컨피그맵 변경 이후 생성된 파드와 그렇지 않은 기존 파드 간의 설정 차이가 있을 수 있으므로, 파드가 사용하는 동안 기존 컨피그맵을 함부로 수정하는 것은 바람직하지 않다. 애플리케이션이 다시 읽기, Reloading을 지원하는 경우에 컨피그맵 수정은 가능하지만, 그 업데이트 과정도 전체 인스턴스에 걸쳐 동기화되어 일시에 진행되는 것이 아니고, 시간차가 발생한다는 것에 유의해야 한다. 

 

시크릿을 통한 보안 데이터 전달

시크릿이란

시크릿이라는 쿠버네티스 오브젝트는, 키-값 쌍을 가진 맵으로 컨피그맵과 유사하게 환경변수나 볼륨파일을 통해 시크릿 항목 노출이 가능하다. 시크릿에 접근해야 하는 파드가 실행하고 있는 노드에만 개별 시크릿을 배포해 보안을 유지하며, 노드 자체적으로 시크릿을 메모리에만 저장하고 물리 저장소에는 남지 않도록 한다.

 

보안 상 유의사항

  1. 마스터 노드의 저장소인 etcd에는 암호화되지 않은 시크릿이 저장되므로, 시크릿의 보안을 위해서는 마스터 노드 보호가 필수적이다. (단 1.7 업데이트 이후 쿠버네티스는 시크릿을 암호화하여 저장소에 저장하므로 보안이 강화되었다)
  2. 권한 없는 사용자가 API 서버를 이용하지 못하게 막아야 시크릿 보안 유지가 가능하다. 파드 생성이 가능한 사용자 누구나 시크릿 마운트나 시크릿 접근이 가능하다. 

시크릿 VS 컨피그맵

  • 컨피그맵
    • 민감하지 않은 일반 설정 데이터
  • 시크릿
    • 본질적으로 민감한 데이터.
    • 일반 설정 데이터와 혼용되었더라도 민감한 데이터가 포함된 경우.

 

기본 토큰 시크릿

모든 파드에는 secret 볼륨이 자동으로 연결된다. 시크릿은 리소스이기 때문에 kubectl get secrets로도 조회 가능하다. 

$ kubectl get secrets 
NAME                  TYPE                                  DATA   AGE
default-token-zgkg9   kubernetes.io/service-account-token   3      28d
tls-secret            kubernetes.io/tls                     2      6d19h

$ kubectl describe secrets 
Name:         default-token-zgkg9
Namespace:    default
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: default
              kubernetes.io/service-account.uid: 3d8e32a1-5774-48a9-9ace-cbbc145253a5

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1111 bytes
namespace:  7 bytes
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6Il9nMFVJUjlpMWJBYk9fOGt1dG5PYmZLUEFMaGRnUEo0eFZ0VTBpYlZrbW8ifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4temdrZzkiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjNkOGUzMmExLTU3NzQtNDhhOS05YWNlLWNiYmMxNDUyNTNhNSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.PKgpQ8Ymg4g2YfxDcI8tzXhvcWjdWsyusQE5_TmSQCKh-TWifm2LZ8cRZnY-0pxmdkxhpOBtzNmWZZEUXbeUMHUhHcuho4sjJqUrOM1itAZXEv9JGr7OswFh92ozdHiGtLJb9Nl8lV4TbJopq9OyIDl0f9_rw1GcSWeKUkyHETCAe4HvfekmcmjVIy33hPyCPeADsuCN8yumL_33aFDq0HYpOYosY2iG5hZbIYNbVKfoT1iXkdObPtsz23WUctiehy8anngAkT6_0citMEsE2Neimq1pooGcD5PhrSZD9z9fd59C_SUedgNZw3ss-sD3veDTZvgSN_lJ54q-FdNQNA


Name:         tls-secret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  kubernetes.io/tls

Data
====
tls.crt:  997 bytes
tls.key:  1679 bytes

$ kubectl describe pods
Name:         fortune-configmap-volume
Namespace:    default
Priority:     0
Node:         minikube/192.168.59.100
Start Time:   Wed, 23 Feb 2022 15:36:11 +0900
Labels:       <none>
Annotations:  <none>
Status:       Running
IP:           172.17.0.6
IPs:
  IP:  172.17.0.6
Containers:
  html-generator:
    (중략)
    Mounts:
      /var/htdocs from html (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-knjgd (ro)
  web-server:
  (중략)
    Mounts:
      /etc/nginx/conf.d from config (ro)
      /tmp/whole-fortune-config-volume from config (ro)
      /usr/share/nginx/html from html (ro)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-knjgd (ro)
(후략)

secret은 ca.crt, namespace, token 3가지의 항목을 지니고 있는데, 파드 안에서 쿠버네티스 API 서버와 통신할 때 필요한 요소이다. 

** 실습 상으로는 kubectl describe pods를 통해 마운트된 위치에서 본래는 Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-cfee9과 같이 secret이 드러나야 하지만, 드러나지 않았다. 

$ kubectl describe secrets kube-api-access-knjgd
Error from server (NotFound): secrets "kube-api-access-knjgd" not found

 

시크릿 볼륨이 마운트된 디렉터리에서 세개의 파일을 확인할 수 있기는 하다. 

$ kubectl exec fortune-configmap-volume -- ls /var/run/secrets/kubernetes.io/serviceaccount/
Defaulted container "html-generator" out of: html-generator, web-server
ca.crt
namespace
token

 

시크릿 생성하기

$ mkdir own_fortune-https
$ cd own_fortune-https 

// 개인 키를 만든다
own_fortune-https % openssl genrsa -out https.key 2048
Generating RSA private key, 2048 bit long modulus
...................+++
...........................................................................+++
e is 65537 (0x10001)

// 인증서를 만든다
own_fortune-https % openssl req -new -x509 -key https.key -out https.cert -days 3650 -subj /CN=www.kubia-example.com

// 시크릿 설명을 위해 foo라는 추가 더미 파일에 bar라는 문자열을 저장한다.
own_fortune-https % echo bar > foo

own_fortune-https % cd ../    

// 앞서 만든 키, 인증서, 더미 파일을 기반으로 시크릿을 생성한다
// generic 시크릿을 생성했다 (시크릿 유형은 아래 설명 참조)
$ kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert --from-file=foo 
secret/fortune-https created

generic 시크릿을 생성해보았다. 

시크릿 유형에는 크게 3가지가 있다.

1. 도커레지스트리용 docker-registry, 2. TLS 통신용 tls, 3. generic이다.

 

generic secret은 키 파일 내용을 가진 키 항목과, 인증서 파일 내용을 가진 인증서 항목이다.

앞선 생성방식과 같이 --from-file을 이용할 때, 개별 파일 지정 대신 전체 디렉토리 활용도 가능하다.

 

컨피그맵 VS 시크릿

% kubectl get secret fortune-https -o yaml
apiVersion: v1
data:
  foo: YmFyCg==
  https.cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN2RENDQWFRQ0NRREFaUzdac3VaZUFUQU5CZ2txaGtpRzl3MEJBUXNGQURBZ01SNHdIQVlEVlFRRERCVjMKZDNjdWEzVmlhV0V0WlhoaGJYQnNaUzVqYjIwd0hoY05Nakl3TWpJek1EZzFNakkwV2hjTk16SXdNakl4TURnMQpNakkwV2pBZ01SNHdIQVlEVlFRRERCVjNkM2N1YTNWaWFXRXRaWGhoYlhCc1pTNWpiMjB3Z2dFaU1BMEdDU3FHClNJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUMvTVE4WmE1cmZvQzluWldZaEpPWERxWG1EN2puUnA1b2QKR052cFgvQmU5eFVzSVpHL3A1UlJ4eE10YU9iL2dYRUQ0MlVON3VUUVJ1T2hzSVp0cGNyaHlLNENxWkZSek1pNQptSXpmSHpQajdUV0pBSEYyQnMvZVJZdHpBZ281TXNzUnFuL2dHemFmY2ZsL0NOMURoUlZralZWbXBVV2RFZzRMCk4vaDQxTWQ2d1ZMMXBNaWdRU2hobUlaUk0wSkNZVDJCcVkyNzQyVWlKYnoyNkNNQTgxdmkvQ3kzeFFyZlc0UmQKajJ1bGM2NDV3dzUzM2NWS3hQNnVkSmtackNSQVRUVUFYRUFwSCswdTQ3OGdQRDJYeUZiSGZXb2hXK0xiY2FFYwpzUXBRWnZvQ2tWQWVJc3JlNGtRVmpYd1VDdGJoUXRXRjJHZ0gzNk9ocnlzVnpmTFdCT3o3QWdNQkFBRXdEUVlKCktvWklodmNOQVFFTEJRQURnZ0VCQUNJK2VXaWx4YjgxWUF3MkJaV1kyMHJISHJxNVNiem9KSUphOElCdkpCTGwKM1dvejZvQXVnRmVzaXRHN25uY3RSK0trMWdLdzF3alIyNUEyMEVycWgyVkQ1c2lpTUp3R1NnSnRwUzBuMktRSgpTVk1jQnlSQUNHenNTZW8yVmkvQVlEWXRSdFg3Z1lyQ2VaaFZpSTBuU3dsQS9Ib2s3bmJITCsrZUpJdm1Zc3IzCks2Tmg3SlJBUnViaFQyWWZldmozSTIxWjV0N3gyOGs1ZG1kalIwdlBmK1VoeDBqU25Nbk5WMnVKS3FML2pNS1YKTlliQzFBSEFqKzk5eTdlUjNsU1ZPUzB0QVdZRHJtRWJHbiswcUFtUlFKK2M3dFRxNE9KbFZpRU4yTTZ3Y0lQQgovQ3VDa1BNSXBHWkd4b0d0ZGVrLzE5YUxkV3lBM2VJcmQxTDZiVDhYWUxjPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
  https.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdnpFUEdXdWEzNkF2WjJWbUlTVGx3Nmw1Zys0NTBhZWFIUmpiNlYvd1h2Y1ZMQ0dSCnY2ZVVVY2NUTFdqbS80RnhBK05sRGU3azBFYmpvYkNHYmFYSzRjaXVBcW1SVWN6SXVaaU0zeDh6NCswMWlRQngKZGdiUDNrV0xjd0lLT1RMTEVhcC80QnMybjNINWZ3amRRNFVWWkkxVlpxVkZuUklPQ3pmNGVOVEhlc0ZTOWFUSQpvRUVvWVppR1VUTkNRbUU5Z2FtTnUrTmxJaVc4OXVnakFQTmI0dndzdDhVSzMxdUVYWTlycFhPdU9jTU9kOTNGClNzVCtyblNaR2F3a1FFMDFBRnhBS1IvdEx1Ty9JRHc5bDhoV3gzMXFJVnZpMjNHaEhMRUtVR2I2QXBGUUhpTEsKM3VKRUZZMThGQXJXNFVMVmhkaG9COStqb2E4ckZjM3kxZ1RzK3dJREFRQUJBb0lCQVFDR0VodjVnbml2V29BRApiSnFxb1ZveDAvVXZhKzNGdEZjaEVsNmNEN2Zha0QrYm04cTk4QURWTzltWjNWY25VeGp5VmhKMks5RHVzTmROCmVpRTZZS21kblFGUlFxRnlFRDJ0MEdqd3ljdTBpSklqQ1ZtSEg0M0MyMWZIaFdXdjZJdDRUdXl2TmNZZkYyaVQKQ1o5SGl0cU9rWUdTb2xEbnJWS0YwWEQvSW83ajU5ak9OdGorZEhLc08yVC9tT0tIaktiTFBiNnlhZzBqNG41cgppVEFxdVh1WGp5dFJYeERkazAyaDNrWEtVaUM3VVNoMlc2VmFTRGEzanFxeDR3b3RzZEUzcGVPN1VKRm14bkw1CnBpNDhZTFFaKzdoeXJBV20yb3MrTDBjcU92cnJYZjVIYUtWQzFYaDRzSHExeEZXRWdBeDhyUFJwdjRhc2NFSHgKOWNJa08zZ0pBb0dCQU9tQWVUb1QxNzFuN21mUGJYSm8xbnNRVGlqZDFwdVNJTUR2cGtITm5sRkh4b0ZIL0dwNwptcURTWDcwOHNEczFFRDZLTmVKWXVOVHF0azlqR3lKUkplemgvOWxFRThNMFRLK053U29jOS9uSDBwbzNrZnMzCnAwNFd4MjB6d0JKL2tmTUhJYjVPMkdnTDZBWUZNUS9GV2NlWmxYcjdVdEVKYStLWGt5U2M2K0MzQW9HQkFOR2MKOTVtdW5KcVNIQ1pwVWJaaEhwMElUU2FacktUeFplbnJNaHNjYzJwS0MxaWxYK2EyYzNoTkNaMXFoNTM0SzlTMwpUZmhuTm5GUzFaQzZOMjlNd05rQXRCS2VYa2psL0dUUTI0WG5mN0grZFo0bWc4eWJjMGZDM3AyaCtVU080cUlqCmUzWURnUGJ5a0JHSno5ODViTVhRdHBXT1YyQ3NtYy9nSHRTUDdvbmRBb0dCQU9pVFU2d2FhWSs1dm9mZDNyalAKWFpxME1mV1lpSkxxS1AramRDa2s0aVU1WmFvTmhvaVZWdjRLck51ellDR0pDQVlTNmZycXZpY3RKYXZMSGhLLwpXeFFvUXdzb09McjlFOGprVzl0VGdWZGt4Z3RmZ0dNR0d2bTN6S21qbXhPUngxQ1c4UEE2WG1pOE96NEwyOE5HCm9kY2l6Zlh4OEpwUFZRc2NTSzUyTXFmZkFvR0FEK0liQ1BRb1BiWXdsK0NISnBDNVp5REg1OUxoT0NacW1JMFAKNE9vSS9OYmJnVDRXeEQ2ekJUeWhLK0owb0UyNzFJU0hUZmxVTU1ZY0ZMbG5sZkYwODN5UUtKRURoL2FWWjNaRQpEQ044azVvNmcxOWJ3VmRPSnZQbk5uNmRpc3BnOWZSR1dLenZTc2NhajVtbnZMeDNONDRYSjhIL2NQM2pNZE5ECndzdWRRYWtDZ1lFQW93aE9oWmRabnJhNjYydTFuQXRCZDRZRk5vaXlwRXloeC9RUGJ5bWlJT1lNd3ZSOVhmZ0YKcVpaWlYvSkVMdzc4VWtEYmlmMTRFUkgzSVpBRmV1R1pBQVpocGtNOFA0TXFsY0NDZDNsYnhDYzIrQjhFK2JmVgpQQ2k1VnUxUVpXTmd2eXFBV3dVdlZ3UXN4T01BOTRvWmtCTEZNMi9LbUpIK2JNWlVLcncvTitFPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
kind: Secret
metadata:
  creationTimestamp: "2022-02-23T08:54:20Z"
  name: fortune-https
  namespace: default
  resourceVersion: "129675"
  uid: 22d53c4c-f262-40a8-b90d-217e2583a2e3
type: Opaque

시크릿 항목 내용은 인코딩 문자열로 표시되어, 컨피그맵의 일반 텍스트 내용 표시와는 차별화된다. 

항목을 읽을 때마다 인코딩, 디코딩이 필요하다는 시크릿 YAML / JSON Manifest 특성이 번거로울 수 있다.

 

인코딩 문자열, 정확히는 Base64 인코딩을 사용하는 이유는, 일반 텍스트 외 바이너리 값도 담을 수 있기 때문이다. 

(참고로 민감하지 않은 데이터도 시크릿을 활용할 수는 있지만, 시크릿의 최대 크기가 1MB라는 점을 유의해 사용해야 한다)

 

바이너리 데이터가 필요없을 경우 시크릿 값을 stringData 필드로 설정할 수 있도록 쿠버네티스가 지원한다.

해당 필드로 만든 시크릿 데이터는 인코딩되지 않는다. stringData 필드는 쓰기 전용이지, 읽기 전용이 아니므로, 값을 설정할 때만 사용할 수 있다. 따라서 kubectl get -o yaml과 같은 명령어로 정의를 조회할 때에는 해당 필드는 표시되지 않고, 대신 해당 필드로 지정한 모든 항목은 다른 data 항목처럼 인코딩돼어 표시된다. 

 

시크릿 볼륨을 컨테이너에 노출하면, 시크릿 값이 바이너리/일반 데이터 종류 여부 무관히 실제 형식으로 디코딩되어 파일에 기록된다. 

환경변수로 노출해도 디코딩되어 기록된다. 즉 애플리케이션에서는 디코딩 없이 자유롭게 읽고 찾아 사용 가능하다.

 

파드에서 시크릿 사용하기

# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
  my-nginx-config.conf: |
    server {
      listen              80;
      listen              443 ssl;
      server_name         www.kubia-example.com;
      ssl_certificate     certs/https.cert;    
      ssl_certificate_key certs/https.key;
      ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;    
      ssl_ciphers         HIGH:!aNULL:!MDS;
      location / {
        root   /usr/share/nginx/html;       
        index  index.html index.htm;
       }
     }
  sleep-interval: |
    25
kind: ConfigMap
metadata:
  creationTimestamp: "2022-02-22T14:34:55Z"
  name: fortune-config
  namespace: default
<rs/6r/zsvmc9b9461frwzfnjv399sm0000gn/T/kubectl-edit-3291317077.yaml" 20L, 862B
// 컨피그맵 데이터를 수정해, 설정에서 서버가 인증서와 키 파일을 원하는 경로로 읽도록 지정
$ kubectl edit configmap fortune-config
configmap/fortune-config edited

$ vim fortune-pod-https.yaml 
$ cat fortune-pod-https.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: fortune-https
spec:
  containers:
  - image: joon0615/fortune:env
    name: html-generator
    env:
    - name: INTERVAL
      valueFrom: 
        configMapKeyRef:
          name: fortune-config
          key: sleep-interval
    volumeMounts:
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    - name: config
      mountPath: /etc/nginx/conf.d
      readOnly: true
    - name: certs  // Nginx 서버가 인증서와 키를 해당 경로에서 읽도록 설정해 시크릿 볼륨을 해당 위치에 마운트
      mountPath: /etc/nginx/certs/
      readOnly: true
    ports:
    - containerPort: 80
    - containerPort: 443
  volumes:
  - name: html
    emptyDir: {}
  - name: config
    configMap:
      name: fortune-config
      items:
      - key: my-nginx-config.conf
        path: https.conf
  - name: certs  // 시크릿 볼륨을 정의해 해당 시크릿을 참조하도록 한다
    secret:
      secretName: fortune-https

 

컨피그맵 수정 이후 파드 생성, 응답 확인을 수행하면 정상적으로 진행된다. 

$ kubectl create -f fortune-pod-https.yaml 
pod/fortune-https created

$ kubectl port-forward fortune-https 8443:443 &
[1] 40640
Forwarding from 127.0.0.1:8443 -> 443
Forwarding from [::1]:8443 -> 443
Handling connection for 8443
Handling connection for 8443

------------------------------

$ curl https://localhost:8443 -k
The human race is a race of cowards; and I am not only marching in that
procession but carrying a banner.
		-- Mark Twain

// -v 옵션은 상세 로깅 설정
$ curl https://localhost:8443 -k -v
*   Trying ::1:8443...
* Connected to localhost (::1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=www.kubia-example.com
*  start date: Feb 23 08:52:24 2022 GMT
*  expire date: Feb 21 08:52:24 2032 GMT
*  issuer: CN=www.kubia-example.com
*  SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET / HTTP/1.1
> Host: localhost:8443
> User-Agent: curl/7.77.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Date: Wed, 23 Feb 2022 09:43:46 GMT
< Content-Type: text/html
< Content-Length: 122
< Last-Modified: Wed, 23 Feb 2022 09:43:31 GMT
< Connection: keep-alive
< ETag: "621601c3-7a"
< Accept-Ranges: bytes
< 
The human race is a race of cowards; and I am not only marching in that
procession but carrying a banner.
		-- Mark Twain
* Connection #0 to host localhost left intact

 

인증서와 개인 키를 시크릿 볼륨에 마운트해 파드에 성공적으로 전달했다. 해당 과정에서 시크릿 볼륨은 시크릿 파일 저장을 위해 인메모리 파일 시스템, tmpfs를 사용하는데, 이는 볼륨 조회를 통해 확인 가능하다.

$ kubectl exec fortune-https -c web-server -- mount | grep certs
tmpfs on /etc/nginx/certs type tmpfs (ro,relatime,size=3935088k)

이 인메모리 파일 시스템에 저장하는 이유는 민감한 데이터를 물리 디스크에 저장하지 않기 위함이다.

 

시크릿 항목을 볼륨이 아닌 환경변수를 통해 노출할 수도 있다. 

과거에 configMapRef를 사용했듯이,

파드 정의 - 컨테이너 정의 내에 valueFrom: secretKeyRef를 사용해서 시크릿을 참조시키면 된다.

 

이렇게 컨테이너 정의에서 시크릿 키를 가진 시크릿의 이름, 키의 이름 등을 명세해 변수를 시크릿 항목에서 설정해, 쿠버네티스에서 시크릿을 환경변수로 노출할 수 있지만 최선의 방법은 아니다. 로그에 환경변수가 작성되면서 의도치 않은 시크릿 노출이 발생할 가능성이 있고, 자식 프로세스가 상위 프로세스의 환경변수 전체를 상속받는 과정에서 서드파티 바이너리를 실행하면 시크릿 데이터가 무용지물이 될 수 있다. 

되도록 안전을 위해서는 항상 시크릿 볼륨을 사용하자. 

 

 

이미지를 가져올 때 사용하는 시크릿

애플리케이션의 시크릿 활용 및 전달에 대해 그간 다뤘다면, 쿠버네티스에서 자격 증명을 전달하는 과정에서도 시크릿 전달 방법을 숙지해야 한다. 프라이빗 이미지 레지스트리 환경에서 그 레지스트리 내 이미지를 가져오려면 자격 증명을 알아야 한다는 것이다. 

 

도커 허브 프라이빗 저장소를 사용하는 파드 실행 조건

  1. 도커 레지스트리 자격증명 보유 시크릿 생성
    1. $kubectl create secret docker-registry mydockerhubsecret -- docker-username=myusername --docker-password=mypassword --docker-email=my.email@provider.com
  2. 파드 매니페스트 내 imagePullSecrets 필드에 해당 시크릿 참조
$ cat pod-with-private-image.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: private-pod
spec:
  imagePullSecrets:
  - name: mydockerhubsecret
  containers:
  - image: username/private:tag
    name: main

 

https://kubernetes.io/docs/concepts/configuration/

 

Configuration

Resources that Kubernetes provides for configuring Pods.

kubernetes.io