[Kubernetes in Action] ConfigMaps and Secrets

2021. 10. 30. 22:30DevOps/Docker & Kubernetes

Kubernetes in Action 2nd Edition을 정리한 글입니다.

 

 

 

Configuring Containerized Applications

애플리케이션을 개발하다 보면, 컨테이너화된 애플리케이션 안에 환경변수등을 주입해주어야 할 때가 있습니다. 데이터베이스에 접근하기 위한 암호이거나, 특정 Third-party Library를 사용하기 위한 private key와 같은 예시가 있는데, 이러한 정보들을 컨테이너화된 애플리케이션에 넣어주는 방법은 일반적으로는 크게 다음 3가지가 있습니다.

 

 

  • Passing Command-line arguments to containers
  • Setting Environment variables for a container
  • Mouting Configuration files into containers throught a special type of volume

 

 

위 3가지 방법들에 대해 하나씩 살펴본 후에, 보안상의 관점(security perspective)에서 노출에 민감한 정보들(secret key등)을 제대로 다루기 위해 쿠버네티스가 제공하는 타입인 "Secret"에 대해서 살펴보도록 하겠습니다.

 

 

Passing Command-line arguments to containers

쿠버네티스는 이미지에 정의된 default command를 overriding하는 방법을 제공합니다. yaml파일에 추가적인 설정을 통해 override하는 방식과, command-line arguments에 arguments를 추가하여 컨테이너를 실행시키는 방식이 있습니다.

 

 

Defining the command and arguments in Docker

Understanding ENTRYPOINT and CMD

ENTRYPOINT와 CMD는 도커의 해당 컨테이너가 수행하게 될 실행 명령을 정의하는 선언문을 의미합니다. 즉, 컨테이너 실행의 최종 단계로서 일반적으로 Dockerfile의 가장 마지막 부분에 선언됩니다.

 

 

ENTRYPOINT와 CMD는 비슷한 역할을 하지만 "컨테이너 시작시 실행 명령에 대한 Default 여부"에서 차이가 있습니다. ENTRYPOINT를 사용해서 컨테이너 명령을 정의한 경우에 해당 컨테이너가 수행될 때 반드시 ENTRYPOINT에서 지정한 명령을 수행하도록 합니다. 하지만 CMD를 사용해서 컨테이너 명령을 정의한 경우에는 해당 컨테이너가 수행될 때 인자값을 주게 되면 Dockerfile에 정의된 인자값 "대신" 변경되어 수행되게 됩니다.

 

 

Understanding the difference between the shell and exec forms

ENTRYPOINT와 CMD를 사용해 args를 설정할 수 있는 것을 살펴보았습니다. 두 명령어는 모두 2가지 format으로 사용이 가능한데, 이 사용방식에 따라서 프로세스가 작동하는 방식이 조금 다릅니다. 

 

  • shell form (e.g ENTRYPOINT node app.js)
  • exec form (e.g ENTRYPOINT ["node", "app.js"]

 

form의 이름에서 알 수 있듯, 전자인 shell form은 shell process안에서 동작하고, 후자인 exec form은 shell process를 만들지 않고 바로 실행됩니다. 

 

FROM node:14

ADD app.js /app.js

CMD node app.js

 

 

각각의 Dockerfile을 만들어서 이미지를 빌드하고 컨테이너를 동작시킨 후에 ps x 커맨드로 현재 컨테이너에서 동작하고 있는 프로세스들의 목록을 확인해보면 shell form으로 실행시킨 컨테이너는 1번 프로세스 (메인 프로세스)가 shell 프로세스이고 exec form으로 실행시킨 컨테이너는 1번 프로세스가 node app.js의 애플리케이션 실행 프로세스인 것을 확인할 수 있습니다. 대부분의 경우 shell process가 불필요하기 때문에 도커에서는 exec form으로 컨테이너를 실행하도록 권장하고 있습니다.

 

 

 

FROM node:14

ADD app.js /app.js

CMD ["node", "app.js"]

 

 

 

Overriding the command and arguments in Kubernetes

위와 같이 컨테이너를 실행시킬 때 Command-line Arguments로 args를 넣어주는 방법도 있지만, 쿠버네티스에서는 파드를 생성할 때 yaml파일에 정의하는 컨테이너 이미지에 "command"와 "args" 를 추가함으로써 설정할 수 있습니다. [공식문서

 

apiVersion: v1
kind: Pod
metadata:
  name: command-demo
  labels:
    purpose: demonstrate-command
spec:
  containers:
  - name: command-demo-container
    image: debian
    command: ["printenv"]
    args: ["HOSTNAME", "KUBERNETES_PORT"]
  restartPolicy: OnFailure

 

 

Setting environment variables for a container

쿠버네티스에서 파드를 생성할 때, yaml파일에 명시된 컨테이너의 descriptor에 'env'를 추가함으로써 컨테이너 별로 환경변수를 주입해줄 수 있습니다. 앞서 언급했던 "command", "args" 방식은 특정 커맨드와 해당 커맨드가 실행될 때에 주입될 변수들을 넣어주는 방식이라면 "env"는 단지 환경변수의 목록들을 넣어주고, 컨테이너 내부에서 해당 변수들을 사용하는 방식으로 동작합니다. (Nodejs에서 process.env.SOME_VARIABLES를 참조해서 사용하는 것과 동일합니다.)

 

 

 

kind: Pod
spec:
  containers:
  - image: some-image
    env:
    - name: FOO
      value: "foo"
    - name: BAR
      value: "bar"
    name: image-name

 

env는 여러개를 생성할 수 있으며, 각 컨테이너 별로 유지되어야 합니다. 하지만 Pod의 Descriptor에 하드코딩되어 있기 때문에 Development, Production 컨테이너의 설정을 별도로 관리해야 한다는 단점이 있습니다. 이 때문에 클린 아키텍쳐의 관점에서 pod descriptor와 configuration(환경 설정 및 관련된 변수들)을 decouple(분리)해야 할 필요성이 생겼고, 쿠버네티스는 "ConfigMap"이라는 리소스를 통해 이를 지원합니다.

 

 

 

Decoupling configuration with a ConfigMap

ConfigMap의 등장배경, 즉 풀고자 하는 "문제"는 환경(production, development)마다 다른 config option들(API Endpoints, private key, DB config 등등)을 Application의 Source Code로부터 분리시켜 관리하는 것입니다. 간단한 예를 들어 말하자면 pod descriptor에 env 변수들을 하나하나 주입하는 것을 막고, 외부로부터 "참조"해서 사용하자는 것입니다.

 

 

Introducing ConfigMaps

ConfigMap은 이름 그대로 단순히 key / value pair를 저장하는 Map입니다. 이전에 Volumes를 다룬 포스팅에서도 간단히 살펴봤듯, 쿠버네티스에서는 쿠버네티스 리소스와 관련된 specification을 애플리케이션으로부터 격리시키는 것을 지향하고 있습니다. 애플리케이션을 작성하는 입장에서 쿠버네티스의 존재를 모른 상태로 개발을 할 수 있도록 한다는 의미입니다. 이러한 원칙이 ConfigMap에도 적용되어 있기 때문에 애플리케이션은 ConfigMap을 직접 참조하거나 ConfigMap이 존재한다는 사실 자체를 알 필요가 없이 ConfigMap의 key/value를 사용할 수 있습니다. (물론 ConfigMap을 직접 참조할 수도 있지만 권장하지는 않습니다). 즉 애플리케이션은 이전에 사용하던 것과 같이 process.env.SOME_VALUE 와 같은 식으로(Nodejs의 경우) 변수를 사용하여 개발할 수 있다는 것입니다.

 

 

 

 

ConfigMap안의 contents들은 환경변수(environment variables)나 volume안의 파일 형태로 컨테이너에게 전달 됩니다. (위의 그림 참조) 또는 ConfigMap 컨텐츠 전체를 command-line argument를 통해 넘겨줄 수도 있습니다.

 

 

중요한 것은 이런 식으로 ConfigMap을 별도의 standalone 객체로 유지함으로써 각각의 환경(production, development)에 동일한 이름(e.g config)의 ConfigMap을 두고, pod descriptor에는 ConfigMap의 이름을 참조하도록 구성함으로써 서로 다른 환경에서도 동일한 pod descriptor를 사용할 수 있다는 것입니다. 

 

 

 

Creating a ConfigMap

ConfigMap은 kubectl create 커맨드로 쉽게 생성할 수 있습니다. yaml 파일을 통해 생성할 수도 있고, --from-literal 옵션을 주어서 cli를 통해 생성할 수도 있습니다.

 

 

Using the kubectl create configmap command

 

 

 

 

Creating a ConfigMap entry from the contents of a file

kubectl create configmap 커맨드는 디스크(파일 시스템)로부터 파일을 읽어와서 ConfigMap의 개별 엔트리로 넣을 수 있는 기능을 제공합니다. 예를 들어서 다음과 같은 json file이 있다고 할 때 다음의 커맨드를 통해 해당 파일의 컨텐츠를 통째로 value에 넣을 수 있습니다.

 

 

 

 

다음과 같이 --from-file=${variable name}=temp.json 으로 configmap에 들어가는 파일의 key값을 명시적으로 설정해줄 수도 있습니다. 이외에도 kubectl create configmap ${config_name} --from-file=/path/to/dir 명령어를 통해 디렉토리 안에 있는 모든 파일들을 key / value 쌍으로 저장할 수 있습니다. (key 값은 파일의 이름이 됩니다)

 

 

 

ConfigMap을 생성할 때 위에서 살펴보았던 이 옵션들을 다음과 같이 다양하게 조합해서 사용할 수도 있습니다.

 

kubectl create configmap my-config
--from-file=foo.json
--from-file=bar=foobar.conf
--from-file=config-opts/
--from-literal=some=thing

 

 

 

 

 

Passing a ConfigMap entry to a container as an environment variable

ConfigMap에 key / value를 정의한 후에는 pod의 컨테이너 안에 이 값을 주입해주어야 합니다. 가장 간단하게는 descriptor에 environment variable을 설정해주는 방법이 있습니다. 생성했던 'yeoul-config'라는 ConfigMap을 참조해서 그 안의 'sleep-interval' key를 가지는 value를 가져와서 INTERVAL이라는 환경변수에 주입해주는 방식입니다. 

 

 

만약 컨테이너가 시작되는 시점에 컨테이너가 참조하고 있는 ConfigMap이 없다면, 해당 컨테이너는 시작하지 못하게 됩니다. (fail to start), 하지만 이후 ConfigMap이 생성되면 명시적으로 restart할 필요 없이 해당 컨테이너가 자동으로 재시작되도록 스케쥴됩니다.

 

apiVersion: v1
kind: Pod
metadata:
  name: env-from-configmap
spec:
  containers:
  - image: some-image
    env:
    - name: INTERVAL
      valueFrom:
        configMapKeyRef:
          name: yeoul-config
          key: sleep-interval

 

 

 

Passing all entries of a ConfigMap as environment variables

ConfigMap의 엔트리의 개수가 크다면, 이 모든 값들을 일일이 yaml에 기록하는 것은 귀찮은 일이 될 수 있습니다. 때문에 쿠버네티스에서는  ConfigMap의 모든 엔트리를 expose(노출)하는 방법을 제공합니다. Optional한 prefix(아래의 예제에서는 CONFIG_)를 추가하면 실제로 사용하는 환경변수의 Prefix를 설정할 수 있습니다. 아래의 예제를 예로 들면 configMap에 "FOO", "BAR"이라는 2개의 key가 있다고 할 때 컨테이너에서는 CONFIG_FOO, CONFIG_BAR로 접근해서 사용할 수 있습니다.

 

 

만약 key 이름이 valid한 format이 아니라면 (dash를 포함하거나 특수문자를 넣거나 등등) 쿠버네티스는 해당 키를 추가하지 않고 스킵합니다. 예를 들어서 "FOO-BAR"라는 키가 있다면 "CONFIG_FOO-BAR"라는 키 이름은 valid하지 않기 때문에 해당 키는 환경변수에 추가되지 않게 되고 해당 이름으로 접근할 수 없습니다.

 

apiVersion: v1
kind: Pod
metadata:
  name: env-from-configmap-2
spec:
  containers:
    - image: some-image
      envFrom:
        - prefix: CONFIG_
          configMapRef:
            name: my-config-map

 

 

혹은 다음과 같이 ConfigMap으로부터 환경변수를 꺼내서 컨테이너의 argument로 넣어줄 수도 있습니다

apiVersion: v1
kind: Pod
metadata:
  name: env-from-configmap
spec:
  containers:
    - image: some-image
      env:
        - name: INTERVAL
          valueFrom:
            configMapKeyRef:
              name: yeoul-config
              key: sleep-interval
      args: ["${INTERVAL}"]

 

 

Updating an app's config without having to restart the app

애플리케이션에서 사용하는 config를 주입할 때 env variable을 사용하거나 command-line arguments를 사용하면 config 변경 시에 컨테이너나 파드를 재시작해야 합니다.(본문에서는 애플리케이션을 재시작한다고 나와있습니다.) 하지만 ConfigMap을 사용하면 애플리케이션을 재시작할 필요 없이 config update가 가능합니다.

 

 

다음의 명령어를 사용하여 쿠버네티스 ConfigMap을 수정할 수 있으며, ConfigMap을 수정하게 되면 해당 ConfigMap을 Reference하고 있는 모든 볼륨의 파일이 업데이트 됩니다. 이때, 파일을 여러개 업데이트했더라도, 이는 Atomic하게 일어나기 때문에 업데이트 사항은 한번에 반영되며 변경 이후에는 reload 명령어를 통해 애플리케이션에서 config를 reload해주어야 합니다. (이는 컨테이너나 파드를 재시작하는 것과는 다릅니다.)

kubectl edit configmap ${configmapName}

 

 

Understanding how the files are udpated atomically

ConfigMap의 파일들의 수정은 atomic하게 일어나는데, 이는 내부적으로 Symbolic Link를 사용하기 때문에 가능합니다. 실제로 마운트된 configMap Volume을 확인해보면 sleep-interval, my-nginx-config.conf와 같은 config 파일들이 ..data 디렉토리안의 내용들을 심볼릭 링크로 참조하고 있는 것을 알 수 있고, 이 ..data directory는 특정 디렉토리를 심볼릭 링크로 참조하고 있는 것을 알 수 있습니다. 즉, 파일 여러개가 변경되면 이 변경을 반영한 디렉토리를 새로 생성하고, 심볼릭 링크가 참조하는 디렉토리를 새로 바꾸는 식으로 동작하기 때문에 파일 여러개의 변경이 Atomic하게 Detect될 수 있는 것입니다.

 

 

이는 볼륨을 Immutable하게 관리하여 변경사항이 생기면 새로운 볼륨을 생성하고 참조만 바꿔주게 함으로써 변경 사항의 감지를 용이하게 한 것이라고 생각할 수 있습니다. 다만 해당 configMap을 참조하고 있는 애플리케이션은 명시적으로  reload를 통해 변경사항을 새로 업데이트해주어야 합니다. 다시말해서 ConfigMap이 업데이트 되었더라도 이를 참조하는 애플리케이션이 자동으로 이를 반영하지는 않는다는 의미입니다.

 

 

Using Secrets to pass sensitive data to containers

애플리케이션에는 private key, credential등의 민감한(sensitive)정보가 포함되기 마련입니다. 이러한 정보를 관리하기 위해서 쿠버네티스는 ConfigMap과 비슷하지만 약간 다른 방식의 리소스인 "Secret"을 지원합니다. Secret은 ConfigMap과 마찬가지로 key / value pair를 저장하는 자료구조이지만, 보안을 위해 항상 메모리 위에 저장되며 phsyical storage에는 별도로 저장되지 않습니다. 또한 해당 Secret를 사용하는 파드가 있는 노드에만 분배됩니다. Secret이 메모리 위에 저장되기 때문에 너무 많은 Secret를 사용하게 되면 OutofMemory에러가 발생할 수 있으며, 이와 같은 이유로 non-sensitive한 config들은 ConfigMap을 사용하는 것을 권장합니다. 

 

 

Introducing the default token secret

기본적으로 모든 파드는 secret 볼륨을 가지고 있습니다. secret은 쿠버네티스 리소스이므로 kubectl get command, kubectl describe command를 사용해서 secret의 정보 및 유무를 확인할 수 있습니다. 

 

 

 

파드에 기본적으로 마운트되어 있는 Secret은 token, ca.crt, namespace의 3가지 엔트리를 포함하며, 이는 파드 내에서 쿠버네티스 API서버와 안전하게 통신할 수 있는 기반을 제공합니다.(자격증명을 위함) Secret을 포함하는 파드의 구조를 도식화 해보면 다음과 같습니다. 위에서 설명했듯, 파드 내에 컨테이너가 있고 (일반적으로 한개의 파드 안에는 한개의 컨테이너) 해당 파드에 Default로 존재하는 Secret Volume이 마운트되어 있습니다. 이 Secret은 ca.crt, namespace, token이 포함되어 있으며, 이 3가지 엔트리를 통해 쿠버네티스 API 서버와 파드가 안전하게 통신할 수 있습니다. (주의할 점은 ca.crt, namespace, token은 Secret이 기본적으로 가지고 있어야 하는 사항이 아니라, 파드와 쿠버네티스 API Server가 안전하게 통신하기 위한 "자격 증명"을 통한 Secret의 데이터입니다. 즉 하나의 usecase입니다)

 

 

 

Comparing ConfigMaps and Secrets

ConfigMap과 Secret은 생김새도 비슷하고, 사용방법도 거의 동일하지만 몇가지 부분에서 차이점이 있습니다. 가장 큰 차이는 Data를 plain text로 저장하는 ConfigMap과는 다르게  Secret은 Data를 Base64 encoded format으로 저장한다는 점입니다. Secret의 경우 SSL 인증서와 같은 바이너리 파일을 저장해야 하는 경우가 있는데, 이를 Plain Text로 저장할 수 없기 때문입니다. 따라서 Secret은 모든 데이터를 Base64 인코딩을 한 상태로 저장하고, 꺼내 쓸 때 이를 디코드 해서 사용하는 암호화 - 복호화 절차가 추가됩니다.

 

 

 

 

Using the Secret in Pod

Secret을 사용하는 것은 ConfigMap을 사용하는 방법과 거의 비슷합니다. 아래의 예시에서 "certs"라는 이름으로 볼륨을 매핑하고, Secret을 해당 볼륨에 마운트해주면 됩니다. 애플리케이션에서는 mountPath를 통해서 시크릿에 접근할 수 있습니다.

 

 

 

 

 

Reference

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

 

시크릿(Secret)

시크릿은 암호, 토큰 또는 키와 같은 소량의 중요한 데이터를 포함하는 오브젝트이다. 이를 사용하지 않으면 중요한 정보가 파드 명세나 컨테이너 이미지에 포함될 수 있다. 시크릿을 사용한다

kubernetes.io

 

반응형