Kubernetes를 배워보자 11일차 - ConfigMaps and Secrets 2

0

kubernetes

목록 보기
11/13

ConfigMaps and Secrets

kubernetes는 각 pod의 container마다 custom 환경변수를 설정할 수 있도록 해준다.

Note: container를 실행할 때 ENTRYPOINT등의 설정을 kubernetes에서 command, args라는 keyword로 오버라이드가 가능하다. 마찬가지로 container의 환경변수 역시도 kubernetes pod 생성 시 오버라이드 할 수 있다.

다음의 그림과 같이 pod내부의 각 container마다 다른 환경변수를 설정해줄 수 있다.

다음으로 이전에 만들었던 fourtune script를 환경변수에 따라 동작을 달리 하도록 변경해보도록 하자.

  • fourtuneloop.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

위의 script는 INTERVAL환경변수를 추가한 것이다. 이 환경변수 값만큼 fortune script는 기다렸다가 fortune명령어를 수행한다. 위의 script는 luksa/fortune:env라는 이미지로 dockerhub에 올려져 있다.

다음으로 환경변수를 설정하는 방법들에 대해서 알아보자.

1. container 정의에 환경 변수 설정하기

luksa/fortune:env image를 사용해서 pod를 만들어보도록 하자. 이 때 environment를 다음과 같이 직접 설정할 수 있다.

kind: Pod
spec: 
  containers:
  - image: luksa/fortune:env
    env:
    - name: INTERVAL
      value: "30"
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs

env를 통해서 원하는 환경변수를 key-value형식으로 넘겨주면 된다. 주의할 것은 해당 환경변수는 pod 레벨이 아니라 container 레벨이라는 것이다. 따라서 같은 환경변수를 pod에서 서로 다른 각기의 container마다 다를 값으로 구성할 수 있다.

추가적으로 환경변수 값을 다른 환경변수 값을 이용해 쓸 수 있다. $(VAR)을 통해서 다른 환경변수 값을 레퍼런스 할 수 있다는 것이다. 아래의 예시를 보자.

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

SECOND_VARFIRST_VAR의 값을 사용하기 때문에, SECOND_VAR의 값은 foobar이 되는 것이다.

그러나 이렇게 환경변수를 하드코딩하는 것은 좋은 방법이 아니다. 왜냐하면 환경변수를 pod description에 하드코딩하게 되면 pod와 환경변수 간의 강한 의존성이 생기기 때문에, 각 환경변수에 따른 pod 정의를 만들 수 밖에 없다. 가령, production환경의 환경변수와 test환경의 환경변수를 달리 해야하는데, 이 때 환경변수를 pod description에 하드코딩 한다면, 환경변수만 다르고 나머지는 동일한 두개의 pod description을 만들어야 하는 것이다.

kubernetes는 이를 위해서 pod와 환경변수 간의 decoupling으로 configmap을 만들었다.

2. ConfigMap 소개

kubernetes는 configuration 옵션으로 pod와 분리된 kuberentes object인 ConfigMap을 제시한다. 이는 key-value로 된 환경변수 리스트로 literal하게 만들 수도 있고, file로도 만들 수 있다.

application 코드는 configMap의 존재를 알 필요가 없다. kubernetes상에서 configMap에 있는 환경변수 key-value를 지정된 container로 주입해주기 때문이다. application은 오직 환경변수에 접근만 하면 된다.

위의 그림과 같이 pod는 그저 configMap volume을 통해서 configMap을 가져오고 이를 환경변수로 로딩해주는 것이다.

또는 configMap을 통해서 config file을 특정 지점에 전달해주는 것 또한 가능하다. 이를 통해서 application은 지정된 file path만을 보고있다가 configMap에 의해서 지정된 file path에 config file이 생기면 이를 읽고 configuration을 설정하는 것이 가능하다.

configMap을 사용하는 가장 큰 강점 중 하나는 configMap 이름은 동일하지만 가지고 있는 value는 다르게 할 수 있어, pod는 configMap의 이름만 가지고 application 설정이 가능하다는 것이다. 즉, configMap이름이 app-config인데 development환경과 production환경에서 다르게 할 수 있다. 때문에 configMap만 다르게 하고 pod description은 변경하지 않을 수 있다.

다음으로 ConfigMap을 만들어보도록 하자.

3. ConfigMap 생성

configMap의 entry들은 libral하게 만들 수도 있고, 내장된 file을 이용해 이래르 읽도록 하여 구성할 수도 있다.

다음은 command를 통해서 literal하게 환경변수를 구성하는 방법이다.

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

다음의 configMap은 sleep-interval=25라는 환경변수 entry를 만든다.

아래와 같이 한번에 여러 개의 환경변수도 만들 수 있다.

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

이제 우리가 만든 fortune-config configMap에 대해서 알아보도록 하자.

kubectl get configmaps fortune-config -o yaml

apiVersion: v1
data:
  sleep-interval: "25"
kind: ConfigMap
metadata:
  creationTimestamp: "2023-12-03T11:12:27Z"
  name: fortune-config
  namespace: default
  resourceVersion: "21269"
  uid: c7095569-6dbb-46a1-b3fb-2ca878fd21dc

data부분이 환경변수 값 구성 부분이다. 별로 특별할 것이 없음을 알 수 있다.

이전에 했듯이 yaml파일이 있다면 create -f 옵션으로도 호출이 가능하다.

kubectl create -f fortune-config.yaml

다음으로 환경변수로 file을 지정할 수도 있는데, kubernetes에서 파일을 읽은 뒤 파일 이름을 key로 하여 읽어 들인 file내용을 value로 쌍을 맞춘다. 이 경우 --from-file을 사용하고 --from-literal과 같이 사용할 수 있다.

다음과 같이 사용할 수 있다.

kubectl create configmap my-config
--from-file=foo.json
--from-file=bar=foobar.conf
--from-file=config-opts/
--from-literal=some=thing
  1. foo.json을 읽어들여서 foo.json이 key이고 그 내용이 value가 된다. 즉, key를 따로 지정하지 않으면 file이름이 key가 된다.
  2. foobar.conf을 읽어들여서 bar이 key가 되고 value는 foobar.conf가 된다.
  3. config-opts/ directory를 읽어들여서 내부의 file들을 모두 읽는다. 이 경우는 file마다 key를 지정할 수 없어 file이름이 key가 되고 value는 파일의 내용이 된다.
  4. some=thing은 file을 읽어들인 것이 아니라 literal하게 환경변수를 설정한 것이다.

이를 도식화하면 다음과 같다.

이제 ConfigMap을 pod에 적용시켜보도록 하자.

4. Pod에 ConfigMap설정

먼저 pod에 ConfigMap의 환경변수를 적용시키는 방법 중 하나는 valueFrom을 사용하는 것이다.

apiVersion: v1
kind: Pod
metadata:
  name: fortune-env-from-configmap
spec:
  containers:
  - image: luksa/fortune:env
    env:
    - name: INTERVAL
      valueFrom:
        configMapKeyRef:
          name: fortune-config
          key: sleep-interval
...

먼저, containersluksa/fortune:env에서 env를 설정하도록 한다. envINTERVAL이고 이 값은 valueFrom을 통해서 외부로 가져오는데, 외부는 configMapKeyRef으로 우리가 만든 configMap을 지정할 수 있다. fortune-config configMap을 지정하고 key는 sleep-interval로 지정하도록 한다. 이렇게 하면 INTERVAL의 값을 sleep-interval의 값으로 채울 수 있는 것이다.

다음과 같이 정리할 수 있다.

pod가 시작될 때 container가 referencing하고 있는 configMap이 없다면 어떻게 될까? 이 경우 pod내 다른 container들은 문제없이 구동되고, 존재하지 않는 configMap을 지정하고 있는 container는 생성에 실패한다. 이 후에 해당 configMap이 만들어지게 되면 해당 container 또한 fail상태에서 running으로 변경된다.

만약 원한다면 configMap-KeyRef.optional: true라는 설정을 통해서 configMap이 없어도 pod의 container를 실행시킬 수 있다.

위의 예제는 configMap에 있는 하나의 entry를 container에 넣어주는 방법이었다. 이제 configMap에 있는 모든 entry들을 한번에 넣어주는 방법을 확인해보도록 하자.

ConfigMap이 3개의 key인 FOO, BAR, FOO-BAR로 이루어져 있다고 하자. 이 ConfigMap의 모든 entry들을 pod에 expose할 수 있는데 envFrom attribute를 사용하여 가능하다.

spec:
  containers:
  - image: some-image
    envFrom:
    - prefix: CONFIG_
      configMapRef:
        name: my-config-map

env대신에 envFrom을 사용하며, configMapRef를 통해서 ConfigMap을 레퍼런싱 할 수 있다. 이때 my-config-map에 존재하는 환경변수들은 CONFIG_라는 접두사를 갖게 된다. 즉 CONFIG_FOO, CONFIG_BAR과 같이 container에 환경변수로 제공되는 것이다. 참고로 prefix는 optional이다.

만약 다음과 같이 ConfigMap을 만들고 some-image에게 환경변수를 넘겨주었다면 아마 CONFIG_FOO, CONFIG_BAR는 제공되지만 CONFIG_FOO-BAR는 제공되지 않을 것이다. 이는 환경변수의 format이 아닌 -를 사용하였기 때문이다. 즉, kubernetes상에서는 환경변수의 format에 어긋나는 경우 이를 환경변수로 import하지 않는다는 것이다.

ConfigMap의 환경변수를 이용해서 pod의 arguments로 넘겨줄 수 있다. 가령, ConfigMap에 있는 sleep-interval값을 읽어서, luksa/fortune:args container에서 사용하는 INTERVAL arguments에 맵핑시킬 수 있다.

다음과 같이 yaml파일을 구성할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: fortune-args-from-configmap
spec:
  containers:
  - image: luksa/fortune:args
    env:
    - name: INTERVAL
      valueFrom:
        configMapKeyRef:
          name: fortune-config
          key: sleep-interval
    args: ["$(INTERVAL)"]

luksa/fortune:args는 어떠한 환경변수도 사용하지 않는다. 다만 argument로 입력을 받는데 이 입력이 INTERVAL인 것이다. 그리고 이 값은 configMapKeyRef를 통해서 fortune-config ConfigMap의 sleep-interval값을 사용한다.

즉, args로 전달되는 값은 fortune-config ConfigMap의 sleep-interval이다. 그리고 이 값을 통해서 luksa/fortune:args를 실행할 때 맨 처음에 사용하는 것이다.

5. ConfigMap volume을 사용하여 file로 ConfigMap entry들을 노출시키기

ConfigMap을 이용해서 환경변수 entry들을 하나하나 설정하는 방법은 정말 적은 환경변수들을 이용할 때만 사용하고 보통은 file전체를 전달하여 configuration을 설정할 수 있도록 한다. 이때 사용되는 특별한 volume이 바로 configMap volume이다. configMap volume은 파일로 ConfigMap의 각 entry를 노출시켜 container에서 동작 중인 process가 file을 읽음으로서 entry value를 얻도록 한다.

즉, config file을 container에 심어서 container에서 구종중인 process가 이를 읽게하는 방법이다.

예제로 nginx web-server의 configuration파일을 전달하여 client에게 응답을 전송할 때, gzip으로 응답을 압축하도록 하자. 다음의 nginx configuration파일이 필요하다.

  • 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;
  }
}

gzip on부분이 plain text와 XML파일들을 압축할 수 있도록 하는 부분이다.

일단 이전에 만들었던 ConfigMap을 삭제하도록 하자.

kubectl delete configmaps fortune-config 
configmap "fortune-config" deleted

다음으로 ConfigMap을 만들되 위의 my-nginx-config.conf파일을 포함하고, fortune에서 사용할 sleep-interval환경변수 entry도 포함하도록 하자.

다음과 같이 configmap-files라는 directory를 만들어놓고 my-nginx-config.confsleep-interval 파일을 각각 만들면 된다.

ls -al configamp-files/
...
-rw-rw-r-- 1 gyu gyu  221 12월  4 21:36 my-nginx-config.conf
-rw-rw-r-- 1 gyu gyu    2 12월  4 21:37 sleep-interval

그리고 configMap을 만들 때 해당 directory를 통째로 configMap에 넘겨주면 된다. kubernetes api에서 해당 directory의 파일들을 읽어 configMap에 처리하게 된다. key는 file이름이고 file내용이 값이다.

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

다음으로 배포된 configMap이 어떤 모습을 가지는 지 확인하도록 하자.

kubectl get configmaps 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:
...

data로 두 부분인 my-nginx-config.confsleep-interval이 있는 것을 알 수 있다. my-nginx-config.conf는 config파일이 통째로 들어가 있고, sleep-interval은 단일 value값만 들어간 것으로 알 수 있다.

이제, volume을 사용해서 configMap파일을 pod의 container에 적재하도록 하자. 우리는 이전에 volume을 사용하는 방법을 배웠다. 따라서 특별한 것은 없고 configMap의 파일을 volume에 어떻게 적재하는 지만 알아내면 volume을 통해서 pod에 configMap 파일을 적재하는 것이 가능하다.

먼저 Nginx의 config파일은 /etc/nginx/nginx.conf에 default로 저장된다. 단, 이 부분은 default configuration파일이므로 /etc/nginx/conf.d/에 config파일을 넣도록 하자. 해당 디렉토리에 넣으면 nginx에서 config file을 읽어서 모두 적용시킨다.

이제 우리가 원하는 모양은 다음과 같다.

이제 pod를 만들어서 volume을 적재해 configMap의 config파일을 넣어주도록 하자.

apiVersion: v1
kind: Pod
metadata:
  name: fortune-configmap-volume
spec:
  containers:
  - image: luksa/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
    configMap:
      name: fortune-config

다음은 두 개의 container로 이루어져 있는데 luksa/fortune:envvalueFrom을 통해서 INTERVAL값을 설정한다. 다음으로 nginx:alpinevolumeMounts를 통해서 파일을 마운팅하는데 nameconfig이고 mountPath/etc/nginx/conf.d라는 것을 알 수 있다.

config는 아래의 volumes부분의 config와 일치하고 volume으로 configMap을 링킹하고 있는 것을 알 수 있다. configfortune-config confgMap을 레퍼런스하기 때문에 이 안에 있는 모든 config file들을 가져와 /etc/nginx/conf.d 디렉터리에 로딩한다.

그렇다면 nginx configuration이 정말 잘되었는 지 확인해보도록 하자. 먼저 nginx가 동작중인 pod port를 외부에 forwarding시켜준 뒤에 요청을 보내보도록 하자. 우리가 만든 configuration이 적용되었다면 응답으로 압축된 응답이 전달될 것이다.

kubectl port-forward fortune-configmap-volume 8080:80 &
[1] 55685
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

다른 shell을 열어 요청을 보내보도록 하자.

curl -H "Accept-Encoding: gzip" -I localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Mon, 04 Dec 2023 13:18:42 GMT
Content-Type: text/html
Last-Modified: Mon, 04 Dec 2023 13:18:22 GMT
Connection: keep-alive
ETag: W/"656dd19e-34"
Content-Encoding: gzip

Content-Encoding: gzip에서 볼 수 있듯이 응답이 gzip으로 압축된 것을 볼 수 있다.

성공할 확인하였지만, 실제로 /etc/nginx/conf.d디렉터리에 configuration파일이 적재되어있는 지 확인해보도록 하자.

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

sleep-intervalnginx에서 사용하지 않지만 fortune-config configMap을 volume으로 마운팅할 때 같이 온 것을 알 수 있다. 필요에 따라 configMap을 여러 개를 두어서 필요한 부분에만 넣어줄 수 있지만, 사실 하나의 pod에 쓰이는 환경변수들이기 때문에 굳이 번잡하게 나눌 필요는 없다.

다행스럽게도 configMap volume에서 ConfigMap의 entry를 뽑아올 수 있는 로직이 존재한다. volume의 items attribute를 사용하면 된다.

...
  volumes:
  - name: config
    configMap:
      name: fortune-config
      items:
      - key: my-nginx-config.conf
        path: gzip.conf

itemsconfigMapfortune-config에서 일부 entry를 가져오도록 하는데, items아래에 list로 적어주면 된다. keyConfigMap에서 entry의 key이고, 해당 key의 값을 gzip.conf파일에 저장하여 container에 제공한다는 것이다.

따라서, 위의 yaml파일이 설정되면 /etc/nginx/conf.d 디렉터리에는 gzip.conf만 있게되고 내용을 살피면 my-nginx-config.conf와 동일하다.

한가지 조심해야할 것은 volume을 특정 directory에 마운트 할 때 기존의 file들이 모두 삭제된다는 것이다. 즉, 위의 경우는 /etc/nginx/conf.d directory에 config file들을 적재하므로 /etc/nginx/conf.d안에 있던 기존의 모든 file들이 삭제된다.

위 경우는 문제없지만, 만약 /etc/var과 같이 linux 시스템 파일이 적재되는 파일 시스템 directory에 마운팅시키게 되면 모든 파일들이 삭제(숨겨짐)되므로 side effect가 생길 수 있음을 알아두도록 하자.

물론 기존 디렉터리에 있던 모든 파일들을 삭제하지 않고, 단일 file이나 디렉터리를 마운팅하는 방법도 있다.

confgMap volume으로 myconfig.conf file을 가지고 있다고 하자. 그리고 이것을 container/etc 디렉터리에 someconfig.conf로 추가하자고 하고 싶다고 하자. 단, myconfig.conf file을 /etc 디렉터리에 someconfig.conf로 저장할 때, 기존의 /etc 파일들이 삭제(숨김처리)되지 않도록 하기 위해서 subPath라는 것을 사용할 수 있다.

  spec:
    containers:
    - image: some/image
      volumeMounts:
      - name: myvolume
        mountPath: /etc/someconfig.conf
        subPath: myconfig.conf

mountPath/etc/someconfig.conf라고 파일로 configuration을 저장하도록 하되 해당 파일로 subPathconfigMapmyconfig.conf entry를 마운팅하도록 한다. 이렇게 subPath를 사용하면 기존의 /etc아래의 기존 파일들을 삭제하지 않고 파일을 추가할 수 있다.

한 가지 명심할 것은 subPath를 사용하여 개개의 file을 설정하는 것은 file들을 업데이트하는 것과 관련해서 큰 결점을 가진다. 이에 대해서는 추후에 알아보도록 하자.

추가한 파일의 permission도 조정할 수 있는데, 기본적으로 configMap volume에서의 file들은 모두 644(rw-r-r--)로 설정되어 있다. 원하는 경우, 이를 수정할 수 있다.

  volumes:
  - name: config
    configMap:
      name: fortune-config
      defaultMode: "6600"

이렇게 만들면 fortune-config에서의 모든 파일들은 permission이 6600이 된다.

5.1 app 재시작 없이 app의 config를 업데이트하기

환경변수 또는 command-line arguments를 configuration source로 사용하는 것의 한가지 결점은 process가 진행되는 동안에, 이 값들을 변경할 수 없다는 것이다. 오직 ConfigMap을 업데이트한 다음에 pod를 재생성하는 방법 밖에 없다.

ConfigMap이 업데이트될 때, ConfigMap을 레퍼런싱하는 모든 volume 안의 파일들은 업데이트된다. 파일들이 변경된 것을 감지하는 것은 온전히 process의 구현에 따라 달라지지만, kubernetes는 file이 변경된 뒤에 container에게 signal을 전달하는 것을 지원한다.

먼저, kubectl edit 명령어로 nginx의 compression옵션을 off시키도록 하자.

kubectl edit configmap fortune-config

editor가 나오게 되고 gzip ongzip off로 수정하면 된다. 다음으로 kubectl exec명령어로 파일 변경이 적용되었는 지 확인해보도록 하자.

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 process는 처음에만 해당 configuration파일을 읽을 뿐, 이를 watch하고 있지 않기 때문에 반영이 안된다는 것이다.

따라서, nginx명령어로 nginx process에게 config reload signal을 보내도록 하자.

kubectl exec fortune-configmap-volume -c web-server -- nginx -s reload

다음과 같이 pod를 재시작하지 않고도 signal을 주어서 새로운 config가 반영된 process를 실행시킬 수 있다.

한 가지 조심해야할 것이 있는데, 위에서 말했듯이 ConfigMap의 모든 파일들을 로딩하는 것이 아니라, subPath로 일부의 file만 로딩하는 것은 config file을 수정하게 될 때, 수정된 내용이 반영되지 않는다는 문제가 있다.

왜냐하면 모든 ConfigMap파일들은 symbolic links를 통해서 atomically하게 업데이트하기 때문이다.

kubectl exec -it fortune-configmap-volume -c web-server -- ls -lA /etc/nginx/conf.d
total 4
drwxr-xr-x    2 root     root          4096 Dec  4 14:36 ..2023_12_04_14_36_38.888836059
lrwxrwxrwx    1 root     root            31 Dec  4 14:36 ..data -> ..2023_12_04_14_36_38.888836059
lrwxrwxrwx    1 root     root            27 Dec  4 13:09 my-nginx-config.conf -> ..data/my-nginx-config.conf
lrwxrwxrwx    1 root     root            21 Dec  4 13:09 sleep-interval -> ..data/sleep-interval

configMap의 file들이 마운팅된 /etc/nginx/conf.d디렉터리를 확인하면 my-nginx-config.confsleep-interval..data/로 symbolic link되어있고 data..2023_12_04_14_36_38.888836059에 link되어있는 것을 볼 수 있다.

kuberntes에서는 ConfigMap이 업데이트되면 업데이트 날짜를 기준으로 새로운 디렉터리를 만들고 이를 data에 link시킨다. 따라서 모든 file들이 atomic하게 한 번에 업데이트되는 것이다.

문제는, subPath를 통해서 ConfigMap의 개별 파일들을 마운트하게 되는 경우, 파일과 ConfigMap사이의 symbolic link가 떨어져 나가기 때문에 ConfigMap이 수정되어서 새로운 디렉터리를 만들어도 개별 파일들에게는 영향을 미치지 않는 것이다.

- name: config
  mountPath: /etc/nginx/conf.d/my-nginx-config.conf
  subPath: my-nginx-config.conf
  readOnly: true

실제로 위와 같이 subPathConfigMap의 개별 file을 할당해놓으면, 개별 file에 symbolic link가 없는 것을 확인할 수 있다.

kubectl exec -it fortune-configmap-volume -c web-server -- ls -lA /etc/nginx/conf.d
total 8
-rw-r--r--    1 root     root          1093 Dec  4 15:06 default.conf
-rw-r--r--    1 root     root           222 Dec  4 15:06 my-nginx-config.conf

따라서 subPath로 개별 config파일들을 마운트하는 것보다, configMap전체를 마운트하는 것이 더 좋은 선택지이다.

자동으로 config file이 변경되는 것을 감지하는 것이 application에 달려있다라고 했는데, 이를 도와주는 library들이 있다. 대표적으로 golang의 viper가 있는데 viper는 환경변수나 파일의 값이 변경되면 이를 확인하여 새로운 값으로 업데이트해준다.

이렇게 application쪽에서 감지하지 않으면, 실제로 실행중인 process에 변경된 configuration을 적용할 방법이 딱히 존재하지 않는다.

6. Secret

일반적으로 configuration data는 보안에 민감한 data가 아니기 때문에 따로 관리할 필요는 없었지만, 일부 data는 보안에 민감한 정보를 담고 있기 때문에 외부에 노출되기 꺼려지는 것들이 있다. 가령 credential이나 private encrytion key들로 이러한 data는 secure하게 보관되어야 한다.

SecretConfigMap과 별반 다를바 없는데, 똑같이 key-value map으로 이루어져 있고 ConfigMap처럼 각 entry들이 환경변수로 container에 주입되거나, volume을 통해 file로 container에 노출될 수 있다.

단 몇가지 차이점이 있는데,
1. SecretSecret에 접근해야하는 pod가 있는 node에만 배포된다.
2. 또한, node자체에서 Secret은 memory에만 저장되지 disk로 저장되지 않는다. 따라서, Secret삭제 후에 disk에 값이 남아있지 않는다.

원래는 master node 자체(etcd)에서 Secret은 암호화되지 않은 형식으로 저장되었다. 이는 master node가 Secret에 저장된 민감한 data를 안전하게 보관하기 위해서 보안화되어야 한다는 의미였다. 그러나 kubernetes version 1.7이후로 etcd는 Secret을 암호화된 형식으로 저장하여, 시스템을 더욱 견고하게 하였다.

Secret을 사용할 지 ConfigMap을 사용할 지에 대해서는 다음의 경우에서 선택하면 된다.
1. 보안에 민감하지 않은 plain한 configuration data를 저장할 때는 ConfigMap을 사용할 것
2. 보안에 민감하고, key아래에 보관되어야 하는 data를 저장할 때는 Secret을 사용할 것, 만약 보안에 민감한 data와 민감하지 않는 data가 함께 있는 경우 Secret에 저장하도록 한다.

이제 Secret을 만들어보도록 하자. 우리가 이전에 만든 ortune-configmap-volume pod의 nginx에 https 설정을 추가하기 위해서, certificate와 private key가 필요하다.

먼저 certificate와 private key를 만들어보도록 하자.

openssl genrsa -out https.key 2048
openssl req -new -x509 -key https.key -out https.cert -days 3650 -subj "/CN=www.kubia-example.com"

위의 명령어를 치면, https.certhttps.key가 만들어진다.

다음으로 Secret이 어떻게 동작하는 지 확인하기 위해서 foo라는 파일을 만들고 bar이라는 데이터를 넣도록 하자.

이제 kubectl create secret명령어를 통해서 다음 3개의 file들을 가지는 Secret을 만들도록 하자.

kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert --from-file=foo 

secret/fortune-https created

generic는 secret의 종류인데, https를 만들기위함이니까 tls로 만들 수도 있다. 단 이 경우 entry name이 달라지므로 generic으로 만든 것이다.

SecretConfigMap의 차이를 알기위해서 kubectl get명령어를 두 kubernetes resource를 확인해보도록 하자.

kubectl get secrets fortune-https -o yaml
apiVersion: v1
data:
  foo: YmFyCg==
  https.cert: LS0tLS1CRUdJ...
  https.key: LS0tLS1CRU...
kind: Secret
metadata:
...

다음의 SecretConfigMap과 비교해보도록 하자.

kubectl get configmaps fortune-config -o yaml
apiVersion: v1
data:
  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;
      }
    }
  sleep-interval: "25"
kind: ConfigMap
...

두 resource의 차이는 Secret은 각 entry가 base64로 인코딩되어있다는 것이다. 반면 ConfigMap은 plain text이기 때문에, 외부에 노출되기 쉽다. Secret은 base64로 인코딩된 data를 pod에서 가져올 때 자동으로 디코딩해준다. 즉, 저장할 때는 인코딩, 가져올 때는 디코딩을 자동으로 해주는 것이다.

한가지 명심할 것은 Secret은 언제나 sensitive data에만 쓰이는 것은 아니다. base64인코딩과 디코딩을 자동으로해주기 때문에 binary data에도 쓰기에 안성맞춤이다.

그러나 모든 데이터가 binary data이지 않다. 이런 모든 string데이터들은 SecretstringData라는 field에 저장된다. 다음의 경우를 보자.

kind: Secret
apiVersion: v1
stringData:
  foo: plain text
data:
  https.cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCekNDQ...
  https.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcE...

stringData field는 write-only이다. 조심해야할 것은 read-only가 아니라는 것이다. 즉, 값을 쓰기만 하는 것이 가능하다는 것이다. stringData field는 -o yaml옵션으로 안보일텐데, 이는 write-only이기 때문이다. 대신 stringData에 쓰여진 모든 entry들은 data로 저장된다. 따라서 stringData에 적힌 모든 entry들이 base64인코딩되어 data에 적히는 것이다. 위의 경우는 foo에 적힌 bar이 base64로 인코딩되어서 data에 들어간다.

7. pod에서 secret 데이터 읽기

pod에서 Secret데이터를 읽을 때는 별도의 디코딩이 필요하지 않다. 자동으로 pod에 Secret 데이터 파일이나 환경변수가 디코딩되어 container에 들어가기 때문이다.

이제 nginx에서 secret 데이터를 사용해 https를 제공할 수 있도록 하자. 먼저 fortune-config에서 nginx관련된 configuration을 수정하기로 하자.

kubectl delete configmaps fortune-config

이전의 configmap을 지우고 다음과 같이 새로 만들도록 하자.

  • fortune-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: fortune-config
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:!MD5;

      location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
      }
    }
  sleep-interval: "25"

ssl설정을 완료한 다음에 배포해주도록 하자.

kubectl create -f fortune-config.yaml 
configmap/fortune-config created

nginx에서는 /etc/nginxcerts디렉터리에서 https.cert, https.key정보를 가지고 https를 구축한다. 따라서 Secret에 있는 cert정보와 private key를 /etc/nginx/certs 디렉터리에 가져다 놓아야한다.

다음으로 Secret을 volume에 마운팅하기 위해서 pod를 수정하도록 하자.

  • fortune-pod-configmap-volume.yaml
apiVersion: v1
kind: Pod
metadata:
  name: fortune-pod-configmap-and-secret
spec:
  containers:
  - image: luksa/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
    - name: certs
      mountPath: /etc/nginx/certs/
      readOnly: true
    ports:
      - containerPort: 80
      - containerPort: 443
  volumes:
  - name: html
    emptyDir: {}
  - name: config
    configMap:
      name: fortune-config
  - name: certs
    secret:
      secretName: fortune-https

달라진 부분은 web-server container의 volumeMounts.namecerts가 추가되었고 mountPath/etc/nginx/certs라는 것이다. 또한, https port를 위해서 ports.containerPort: 443을 열어주었다. 마지막으로 volumes.name으로 certs를 만들고 secret으로 fortune-https를 연결시켰다.

그림으로 표현하면 다음과 같다.

다시 Pod를 재생성한 다음에 nginx에서 certs디렉터리에 있는 cert와 private key를 읽고 실제로 https를 지원하고 있는 지 알아보도록 하자.

kubectl delete -f ./fortune-pod-configmap-volume.yaml 
pod "fortune-configmap-volume" deleted

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

다음으로 port-forward를 해주도록 하자.

kubectl port-forward fortune-pod-configmap-and-secret 8443:443 &

curl https://localhost:8443 -k
You will be imprisoned for contributing your time and skill to a bank robbery.

성공적으로 응답을 받은 것을 확인할 수 있다.

이전에 Secret은 in-memory filesystem(tmpfs)를 사용하여 Secret file들을 저장한다고 하였다. 따라서 다음의 명령어를 통해서 실제 파일들이 저장되어있는 지 확인할 수 있다.

kubectl exec fortune-configmap-volume -c web-server -- mount | grep certs
tmpfs on /etc/nginx/certs type tmpfs (ro,relatime,size=16170152k,inode64)

tmpfs가 사용되기 때문에 secret에 저장된 민감한 정보들은 disk에 쓰이는 것이 아니라는 것을 알 수 있다.

configmap에서 특정 값을 환경변수로 노출하였듯이 secret에서도 가능하다. 다음은 fortune-https secret에 있던 foo의 값을 container에서 FOO_SECRET으로 접근할 수 있도록 해준다.

    env:
    - name: FOO_SECRET
      valueFrom:
        secretKeyRef:
          name: fortune-https
          key: foo

valueFromsecretKeyRef를 썼기 때문에 Secret으로부터 값을 얻을 수 있다는 것을 알 수 있다. 단, secret을 쓸 정도로 민감한 데이터를 환경변수로 노출시키지 말아야한다. 환경변수는 application이 구동되면서 의도치 않게 로깅될 수 있으며, 또 다른 환경변수 설정 방식에 의해 덮여쓰일 수도 있다. 또한, 환경변수들은 모두 자식 프로세스를 만들 때 부모 프로세스의 환경변수를 가져감으로 원치 않은 보안사고가 날 수 있다.

따라서 Secret은 volume으로 쓰는 것이 가장 좋다.

8. Image pull secrets

지금까지는 public image registry(docker hub, local registry)를 사용했지만, private registry를 사용하는 경우에는 certificate가 필요하다. 이는 kubernetes에서 private registry의 image를 pull하기 위해서는 인증된 certificate가 있는 경우에만 허용하도록 하기 때문이다.

이 certificate가 바로 Secret이 담고 있는 data인 것이고, private registry는 Secret에 인증 정보를 담도록 하여, 사용자만 만든 Secret이 private registry를 사용할 수 있는 지 없는 지 확인한다. 만약 인증에 성공한다면 kubernetes cluster에서 private registry를 통해서 image를 가져와 pod를 만들 수 있는 것이다.

1. 사용자 --> Secret생성 --> pod생성 --> kubernetes에서 private registry에 접근
2. private registry에 대한 인증 정보가 담긴 Secret 확인 --> 인증 성공 시 pod가 성공적으로 배포

http://hub.docker.com

도커 허브에서 private registry를 구축할 수 있다. 또는 harbor나 os local상에 local registry를 구축하여 private하게 사용할 수 있다.

private registry에서 image를 가져와 pod를 만들기 위해서는 다음의 두 가지를 설정해야한다.
1. private registry 인증 정보를 담은 Secret
2. pod manifest의 imagePullSecrets field안에서 Secret을 레퍼런싱

가령, docker-hub의 private registry를 만들었다면 다음과 같이 Secret의 만들면 된다.

kubectl create secret docker-registry mydockerhubsecret \
  --docker-username=myusername --docker-password=mypassword \
  --docker-email=my.email@provider.com

한 가지 주목해야할 것은 이전에 우리가 만든 secretgeneric type인 반면에 여기서는 docker-registry라는 type이 쓰였다. 이는 특수 type으로 private registry에서 image를 가져올 때 사용한다.

다음으로 kubernetes에서 private registry image를 가져오기 위해서, 어떤 Secret을 써야하는 지 알려주어야 한다. 이는 pod description의 spec.imagePullSecrets에 적어주면 된다.

apiVersion: v1
kind: Pod
metadata:
  name: private-pod
spec:
  imagePullSecrets:
  - name: mydockerhubsecret
  containers:
  - image: username/private:tag
    name: main

imagePullSecrets에 우리가 만든 docker-registry Secret인 mydockerhubsecret을 넣어주면 kubernetes에서 해당 pod를 만들 때 Secret을 읽어서 private registry에 인증을 완료한 뒤, image를 가져온다.

0개의 댓글