Kubernetes in Action : Chapter7. ConfigMap, Secret

    들어가기 전

    이 글은 쿠버네티스 인 액션 7장을 공부하며 작성한 글입니다. 


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

    일반적으로 컨테이너화 된 어플리케이션에 설정값을 전달할 때는 다음 방법을 사용한다. 

    • 커맨드 라인 인자로 전달.
    • 컨테이너를 위한 사용자 정의 환경변수 지정
    • 설정 파일을 전달해서 어플리케이션에서 사용.

    설정값이 많지 않다면 커맨드 라인으로 충분히 전달할 수 있다. 하지만 설정값이 많아지게 되면 결국 설정 파일을 만들어서 넘기게 된다. 또한 환경 변수를 이용한 것도 방법이 될 수 있다. 

    주로 도커에서는 환경 변수를 사용한다. 왜냐하면 이미지 내부에 설정 파일을 재정의하기 까다롭다. 설령 이미지 내부의 설정 파일을 재정의하고 이미지를 빌드하면, 이 이미지는 '그 설정' 전용 이미지가 되어버린다는 문제가 있기 때문이다. 그렇지만 환경변수는 언제든지 탈취될 수 있는 정보들이다. 누군가 컨테이너에 접근할 수 있는 경우가 있기 때문이다. 

    쿠버네티스에서는 '설정 파일'을 좀 더 유연하게 전달할 수 있도록 ConfigMap, Secret 오브젝트를 제공한다. 이 장에서는 ConfigMap / Secret을 이용한 어플리케이션 설정에 대해 공부할 것이다. 


    7.2 컨테이너에 명령줄 인자 전달

    쿠버네티스는 특정 명령어를 이용해서 컨테이너가 실행될 때, 커맨드라인 인자를 전달하는 기능을 제공한다. 이 절에서는 도커 / 쿠버네티스를 이용해 각각 커맨드 라인을 전달하는 방법을 알아본다. 

     


     

    7.2.1 도커에서 명령어와 인자 정의

    도커에서는 ENTRYPOINT, CMD를 지원한다. 이 녀석들은 다음 기능을 담당한다.

    • ENTRYPOINT : 컨테이너가 시작될 때 호출될 명령어를 정의함.
    • CMD : ENTRYPOINT에 전달된 인자를 정의함. 

    따라서 올바른 방식은 ENTRYPOINT를 항상 '명령어'를 실행하도록 정의하고, 추가 인자 설정이 필요한 경우에만 CMD를 사용하는 것이다. 

     

    도커에서 Shell  / Exec 형식의 차이점 → Exec을 사용하자. 

    도커의 엔트리 포인트를 정의할 때는 Shell / Exec 방식이 존재한다. 각 방식은 결과 자체는 동일하지만, 하나의 프로세스가 더 생기기 때문에 exec 형식을 사용하는 것이 좋다.

    # Shell 형식
    ENTRYPOINT node app.js 
    
    # Exec 형식
    ENTRYPOINT ["node", "app.js"]

    Shell 형식을 사용하면 컨테이너가 시작될 때, Shell을 하나 띄운 후 Shell을 통해 node 어플리케이션을 실행한다. Shell 형식으로 실행한 후 컨테이너의 프로세스 목록을 보면 아래와 같다. Shell 방식으로 실행하면 메인 프로세스가 Shell이 된다. 불필요한 프로세스가 생기기 때문에 Docker 이미지를 만들 때, exec 방식으로 사용해야 한다. 

    # Shell 방식으로 실행한 결과
    PID TTY   STAT TIME COMMAND
     1   ?    Ss   ...  /bin/sh -c node app.js # 불필요한 쉘 띄워짐. 
     7   ?    Sl   ... node app.js
     
     PID TTY   STAT TIME COMMAND
     1   ?    Sl   ... node app.js

     

     

    exec 형식으로 ENTRYPOINT + CMD 사용

    도커 컨테이너에서 exec 형식으로 ENTRYPOINT + CMD를 정상적으로 사용하는 예제를 살펴보고자 한다. 

    • 쉘 스크립트를 작성한다.
    • 쉘 스크립트는 인자 1개를 받아서 INTERVAL에 저장하고, 해당 값만큼 Sleep 한다. 
    #!/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

    도커 파일을 작성한다. 

    • ENTRYPOINT로는 실행할 프로세스를 정의한다. 
    • CMD는 전달할 인자를 정의한다. 
    FROM ubuntu:latest
    RUN apt-get update -y; apt-get -y install fortune
    ADD fortuneloop.sh /bin/fortuneloop.sh
    
    # 실행할 프로세스 정의
    ENTRYPOINT ["/bin/fortuneloop.sh"]
    
    # 실행할 때 사용할 기본인자
    CMD ["10"]

    만약 해당 이미지를 실행할 때, 아래와 같이 인자를 주게 되면 CMD에 있는 값은 오버라이딩 된다.

    $ docker run ojt90902/fortune:args 20
    >>>
    # 원래는 10이 나와야 하지만(이미지), 20이 나옴. 
    Configured to generate new fortune every 20 seconds

    7.2.2 쿠버네티스에서 명령과 인자 재정의

    쿠버네티스에서 컨테이너의 스펙을 정의할 때, ENTRYPOINT와 CMD를 모두 재정의 할 수 있다. 대부분은 CMD 부분만 재정의하고 ENTRYPOINT를 재정의하는 경우는 거의 없다. 각 경우를 비교하면 다음과 같다. 

    Docker k8s 설명
    ENTRYPOINT cmmand 컨테이너 안에서 실행되는 실행 파일
    CMD args 실행파일에 전달되는 인자 

    앞서서 정의했던 컨테이너에 CMD를 재정의한 Pod는 다음과 같다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: c7-1
    spec:
      containers:
        - name: c7-1
          image: ojt90902/fortune:args
          imagePullPolicy: Always
          
          # 인자 재정의. 배열이므로 아래와 같이 전달 가능. 
          args: 
            - "50"
            - "100"

    7.3 컨테이너의 환경변수 설정 + 7.3.1 Pod에 환경변수 지정

    이미지에서 먼저 환경변수를 참조하도록 쉘 스크립트를 작성한다. 아래 쉘 스크립트는 환경변수 $INTERVAL을 참조한다.

    #!/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

    그리고 파드를 사용할 때, args를 제거해주고 아래와 같이 ENV를 선언해준다. 이렇게 선언해두면 Pod의 컨테이너가 만들어질 때, 컨테이너에는 INTERVAL이라는 환경변수가 생기게 된다. 

    apiVersion: v1
    kind: Pod
    metadata:
      name: c7-5
    spec:
      containers:
        - name: c7-5
          image: ojt90902/fortune:env
          imagePullPolicy: Always
          # 환경변수 설정
          env:
            - name: INTERVAL
              value: "50"

    7.3.2 변수값에서 다른 환경변수 참조

    쿠버네티스의 설정 파일에서는 "$(VAR)" 구문을 이용해서 이미 정의된 변수를 참조해서 코드 반복을 줄일 수 있다. 따라서 필요한 경우 유용하게 사용할 수 있을 것이다. 

    apiVersion: v1
    kind: Pod
    metadata:
      name: c7-5
    spec:
      containers:
        - name: c7-5
          image: ojt90902/fortune:env
          imagePullPolicy: Always
          env:
            - name: INTERVAL
              value: "50"
            # 이미 정의된 변수 참조.
            - name: HELLO
              value: "$(INTERVAL)-HELLO"

    7.3.3 하드코딩된 환경변수의 단점 → valueFrom을 쓰자. 

    파드를 정의할 때 환경변수를 하드코딩하면 사용하기에는 편리하다. 하지만 설정이 파드 정의에 포함되기 때문에 다음 문제점이 발생한다.

    • 프로덕션 / 개발 환경에 필요한 파드를 각각 정의해야한다. 

    이런 이유때문에 파드 정의와 환경변수 값은 분리되는 것이 좋다. 쿠버네티스는 이 기능을 ConfigMap과 valueFrom 명령어를 이용해 지원해준다. 


    7.4 컨피그맵으로 설정 분리

    우리는 일반적으로 어플리케이션과 어플리케이션의 설정을 분리한다. 이것은 쿠버네티스에도 좋은 선택 사항이 된다. 

    • 어플리케이션 코드 = Pod
    • 설정값 = ConfigMap 

    어플리케이션 코드와 설정값을 쿠버네티스에 대입해서 생각해보면, 파드로부터 설정값을 ConfigMap을 이용해서 분리하는 것이 더욱 유연한 확장을 가능하게 한다. 


    7.4.1 컨피그맵 소개

    쿠버네티스는 ConfigMap 오브젝트를 제공하고, 네임스페이스 단위의 오브젝트다. ConfigMap은 다음 특성을 가진다.

    • ConfigMap은 Key - Value 쌍 값을 가지는 쿠버네티스 오브젝트다. 
    • ConfigMap의 Value는 짧은 문자열부터, 전체 설정 파일까지 가능하다. 
    • ConfigMap의 내용은 컨테이너의 환경 변수 / 볼륨 파일로 전달됨.

    하나의 파드 파일을 정의하고, ConfigMap을 3개를 생성하면 하나의 파드 정의 파일로 3개의 Context를 유지할 수 있다는 장점이 있다. 


    7.4.2 컨피그 맵 생성

    컨피그맵은 다음 방법으로 생성할 수 있다.

    • 명령어로 생성
      • 단순 문자열로 생성.  → from-literal
      • 파일로 생성. → from-file 
      • 디렉토리로 생성. → from-file
      • 다양한 옵션 결합 (위의 세 가지를 조합해서 가능)
    • Yaml 파일로 선언 

     

    명령어로 생성

    명령어로 생성하는 방법은 다음과 같다. 

    # ConfigMap 생성하기
    $ kubectl create configmap chapter7-foo \
    --from-literal=key1=value1 --from-file=foo.json --from-file=hello/

    생성된 컨피그맵은 아래와 같다. 

    # 생성된 ConfigMap 살펴보기
    $ kubectl get configmap chapter7-foo -o yaml
    apiVersion: v1
    data:
      foo.json: |
        {
          "foo": "bar",
          "bar": "foo"
        }
      hello.conf: |
        A=1
        B=2
        C=3
      key1: value1
    kind: ConfigMap
    metadata:
      creationTimestamp: "2023-05-26T12:13:04Z"
      name: chapter7-foo
      namespace: default
      resourceVersion: "406102"
      uid: 09b5976a-d0bf-418a-9b43-49a63d4766a5

    각 컨피그맵은 다음과 같이 생성된다.

    • 문자열로 생성 → 각각이 key / value로 전달된다. 
    • 파일로 생성 → 파일 이름은 Key / 파일 내용이 Value 
    • 디렉토리 → 각 파일 이름이 각 Key / 각 파일 내용이 Value 

    위의 정의 파일에서 "|" 파이프라인 문자열을 볼 수 있다. 파이프라인 문자열은 해당 정의 파일이 문자열 한 줄이 아니라, 여러줄의 문자열이 온다는 것을 알려주는 것이다. 


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

    컨피그맵 항목을 컨테이너 환경변수로 사용하기 위해서는 다음 두 가지 키워드를 사용해야 한다.

    • valueFrom 사용 → value를 다른 곳에서 불러오겠다는 의미 .
    • configMapKeyRef 사용 → ConfigMap 객체를 사용하겠다는 의미. 

    configMapKeyRef를 사용할 때 ConfigMap의 이름과 가지고 있는 Key를 적절히 정의하면, 해당 명령어를 사용한 컨테이너의 환경변수로 전달된다. 

      ...
        - name: c7-9
          image: ojt90902/fortune:env
          imagePullPolicy: Always
          env:
            - name: INTERVAL
              valueFrom:
                configMapKeyRef:
                  name: fortune-config
                  key: sleep-interval

    존재하지 않는 컨피그맵을 참조할 때, 파드는?

    존재하지 않는 컨피그맵을 만약 파드의 컨테이너가 참조하고 있다면 어떻게 될까? 이 때 쿠버네티스는 다음과 같이 ContainerCreatingConfigError를 발생시킨다.

    NAME        READY   STATUS                       RESTARTS       AGE
    c7-9        0/1     CreateContainerConfigError   0              6s

    정확하게는 다음과 같이 동작한다. 아래 동작의 결과로 STATUS는 CreateContainerConfigError가 된다. 

    • 파드는 스케쥴링 된다. 
    • 컨피그맵을 참조하지 않는 파드의 컨테이너는 정상 생성된다.
    • 컨피그맵을 참조하지만, 컨피그맵이 없는 컨테이너는 생성되지 않는다. 

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

    쿠버네티스는 환경변수를 넣을 수 있도록 env / envFrom을 지원한다. 각각의 명령어는 다음 차이를 가진다

    • env : 환경변수 하나만 설정
    • envFrom : ConfigMap 등이 가지고 있는 전체 환경변수 설정. 이 때, 각 환경변수의 이름에 Prefix를 붙일 수도 있다. 

    예를 들면 아래와 같이 작성할 수 있다. 

    ...
          envFrom:
            - prefix: KUBE_CONFIG_
              configMapRef:
                name: fortune-config

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

    컨피그맵 항목은 환경변수 / 볼륨에만 직접적으로 전달할 수 있다. 하지만 쿠버네티스에 선언된 변수 참조를 통해서 명령줄에 전달할 수도 있다. 

    apiVersion: v1
    kind: Pod
    metadata:
      name: c7-11
    spec:
      containers:
        - name: c7-11
          image: ojt90902/fortune:args
          imagePullPolicy: Always
          env:
            - name: INTERVAL
              valueFrom:
                configMapKeyRef:
                  name: fortune-config
                  key: sleep-interval
          # 선언된 인자를 참조 변수로 전달함. 
          args:
            - "$(INTERVAL)"

    7.4.6 컨피그맵 볼륨을 사용해 컨피그맵 항목을 파일로 노출

    ConfigMap에는 단순한 문자열 뿐만 아니라, 파일 전체를 ConfigMap의 Value에 저장할 수 있다. 일반적으로 설정값이 많아지면 설정 파일(Config File)을 전달한다. ConfigMap을 통해서 설정 파일을 전달할 수도 있다. 이 때, 다음과 같은 방식으로 전달한다.

    • ConfigMap을 ConfigMap Volume으로 참조한다.
    • ConfigMap Volume을 특정 경로에 마운트 시켜서 참조하도록 한다. 

    아래에서 순차적으로 살펴보자.


    컨피그맵 생성

    아래 설정 파일을 생성한 후, 명령어를 이용해 컨피그맵을 생성한다. 아래 설정 파일은 nginx 컨테이너에서 사용할 설정 파일이다.

    # ./config-files/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;
        }
    }
    
    
    # configMap 생성
    $ kubectl create cm fortune-config --from-file=config-files/

    볼륨 안에 있는 컨피그맵 사용

    ConfigMap을 Volume으로 등록한 후, 컨테이너의 파일 시스템에 마운트해서 설정값을 사용할 수 있다. 아래와 같이 작성한다.

    • ConfigMap 타입의 Volume을 등록.
    • 등록된 Volume을 VolumeMount를 통해 특정 컨테이너에 등록 
    apiVersion: v1
    kind: Pod
    metadata:
      name: c7-14
    spec:
      containers:
        - name: c7-14-nginx
          image: nginx:alpine
          
          # 볼륨 마운트 해서 사용. 
          volumeMounts:
            - mountPath: /etc/nginx/conf.d
              name: config
              readOnly: true
      
      # ConfigMap 볼륨 등록
      volumes:
        - name: config
          configMap:
            name: fortune-config

    참고하는 과정은 다음과 같다.


    ConfigMap을 볼륨으로 참고할 때, 단순 문자열은 어떻게 마운트?

    ConfigMap을 볼륨으로 참고할 때, 단순 문자열은 어떤 형태로 Volume에 마운트 될까? 답은 간단하다.

    • ConfigMap은 Key / Value로 값이 저장된다
    • Key는 파일 이름, Value는 파일 내용이 된다. 

    이렇게 등록된다. 실제로 컨피그맵을 생성하고, 볼륨 마운트 된 곳에 가서 파일명과 파일 내용을 확인해보면 위의 방식을 따라서 생성된 것을 알 수 있다. 

    # 아래 명령어로 컨피그맵 생성
    $ kubectl create cm fortune-config --from-literal=key1=value1 --from-file=my-nginx-config.conf
    
    # 마운트 된 볼륨 확인
    $ etc/nginx/conf.d # ls
    >>> 
    key1                  my-nginx-config.conf
    
    
    $ cat etc/nginx/conf.d key1
    >>>>
    value1

    볼륨에 특정 파일만 노출

    ConfigMap을 볼륨으로 등록하면, ConfigMap의 모든 Key / Value 쌍이 파일로 노출된다. 만약 컨피그맵이 여러 설정 파일을 가졌고, 각 설정 파일은 각 컨테이너에서만 필요한 경우라면 ConfigMap이 가진 모든 파일을 노출할 필요가 없다. 쿠버네티스는 Volume을 등록할 때, item이라는 키워드를 이용해서 이 기능을 구현한다. 다음과 같이 동작한다. 

    • Volume에서 ConfigMap을 설정한 후, Name을 설정 → ConfigMap을 인식함. 
    • items를 이용해 Key를 설정. → ConfigMap이 가진 Key를 알고 있기 때문에 해당 파일만 마운트함. 
      containers:
        - name: c7-14-nginx
          image: nginx:alpine
          volumeMounts:
            - mountPath: /etc/nginx/conf.d
              name: config
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: fortune-config
            # 특정 파일만 노출. 
            items:
              - path: gzip.conf
                key: my-nginx-config.conf

    위와 같이 작성한다면, 이 Pod에 한정에서는 ConfigMap 타입의 볼륨에는 gzip.conf라는 파일만 존재한다. 그리고 /etc/nginx/conf.d 경로 아래에 gzip.conf가 등록되게 된다. 


    디펙토리 안에 다른 파일을 숨기지 않고 개별 컨피그맵 항목을 파일로 마운트

    ConfigMap 볼륨을 마운트 하게 되면, 해당 경로에 원래 존재하던 컨테이너의 파일들은 모두 사라지고 ConfigMap 볼륨의 파일만 나타나게 된다. 이것은 꽤 불합리 할 수 있다. 기존에 존재하던 설정 파일은 당연히 필요하고, 새로운 설정 파일을 하나 추가해야하는 경우라면 이 방법은 사용할 수 없다. 

    이런 경우를 해결하기 위해 쿠버네티스는 볼륨을 마운트 할 때, subpath라는 키워드를 이용해서 도와준다. 아래와 같이 사용할 수 있다. 

    • volumes: 여기서 노출할 파일을 설정한다. path에서 gzip.conf라는 이름의 파일이 ConfigMap의 Volume으로 노출된다.
    • mountPath : 마운트 할 파일을 경로와 함께 표시한다. 
    • subpath : ConfigMap Volume을 통해서 마운트할 값을 선택한다. 

    이렇게 작성하면 subpath에 표시된 파일이 mountPath에 표시된 파일에 마운트 되게 된다. 이렇게 되면, 컨테이너의 원래 디렉토리 내부의 파일은 그대로 유지된다. 그리고 필요로 하는 설정 파일만 마운트 할 수 있다는 장점이 존재한다. 하지만 이 방법은 파일 업데이트와 관련해 상대적으로 큰 결함을 가진다. 

    ...
          volumeMounts:
            - mountPath: /etc/nginx/conf.d/default.conf1
              name: config
              subPath: gzip.conf
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: fortune-config
            items:
              - path: gzip.conf
                key: my-nginx-config.conf

    7.4.7 어플리케이션을 재시작하지 않고 어플리케이션 설정 업데이트

    환경변수 / 커맨드 라인으로 설정값을 전달하는 것의 단점은 프로세스가 실행되고 있는 동안 설정값을 업데이트 할 수 없다는 것이다. ConfigMap 오브젝트는 Edit 기능을 통해서 수정이 가능하다. ConfigMap을 볼륨으로 참조하고 있는 경우, 변경사항이 볼륨에도 반영된다. 만약 어플리케이션이 설정 재로딩 기능을 제공한다면, 어플리케이션을 재시작 할 필요 없이 설정값을 업데이트 할 수 있다. 

    즉, ConfigMap을 볼륨으로 마운트하면 재시작 없이 어플리케이션 설정을 변경할 수 있다는 것이다.


    파일이 한꺼번에 업데이트 되는 방법 이해

    다음 상황을 고려해보자.

    • ConfigMap에 여러 파일이 존재하고 있다. 여기서는 5개라고 하자.
    • 5개의 파일의 내용을 수정하고, ConfigMap Edit를 끝냈다. 

    그러면 5개의 파일은 각각 업데이트 되는 것일까? 한번에 업데이트 되는 것일까? 쿠버네티스는 파일을 한꺼번에 업데이트한다. 따라서 순차적으로 파일이 업데이트 될 때의 문제는 고려하지 않아도 된다. 한꺼번에 업데이트 하는 방법은 다음과 같다.

    • 필요한 파일을 새로 생성한다. 
    • 일괄적으로 심볼릭 링크를 만들어 볼륨에 마운트 한다. 
    1130561 drwxrwxrwx    3 root     root          4096 May 26 23:25 .
    1130716 drwxr-xr-x    3 root     root          4096 May 24 22:44 ..
    1130575 drwxr-xr-x    2 root     root          4096 May 26 23:25 ..2023_05_26_23_25_13.2563150777
    1048957 lrwxrwxrwx    1 root     root            32 May 26 23:25 ..data -> ..2023_05_26_23_25_13.2563150777
    1048964 lrwxrwxrwx    1 root     root             8 May 26 23:25 a -> ..data/a
    1048967 lrwxrwxrwx    1 root     root             8 May 26 23:25 b -> ..data/b
    1048968 lrwxrwxrwx    1 root     root             8 May 26 23:25 c -> ..data/c

    파일만 마운트 했을 때, 업데이트는 되지 않음. 

    전체 볼륨을 마운트 하는 것이 아니라 subPath를 이용해서 볼륨의 특정한 파일만 마운트한 경우라면, 이 파일은 업데이트 되지 않는다. 이 때의 대안은 다음과 같다.

    • 전체 볼륨을 컨테이너의 다른 디렉토리에 마운트
    • 필요한 파일만 심볼릭 링크를 만들어서 사용

    컨피그맵 업데이트의 결과 이해하기

    컨테이너의 가장 중요한 기능은 불변성이다. 따라서 특정 컨테이너는 어플리케이션이 실행되고 있는 중에 설정값을 다시 로딩하는 기능을 지원하지 않을 수도 있다. 만약 이런 컨테이너를 대상으로 동적으로 ConfigMap을 업데이트 하면 각 컨테이너가 서로 다른 설정값을 가지고 실행되고 있는 최악의 경우가 발생한다. 

    • A 파드 → 이전 설정값으로 동작 
    • B 파드 → 변경된 설정값으로 동작 

    따라서 어플리케이션이 설정을 동적으로 로딩하는 기능을 제공하지 않는다면 ConfigMap을 업데이트 하지 않는 것이 좋다. ConfigMap + 어플리케이션을 함께 다시 재배포 하는 것이 좋다. 

    설정을 동적으로 로딩하는 어플리케이션이라면 ConfigMap을 수정해서 핫로딩을 진행해도 괜찮다. 하지만, ConfigMap의 볼륨 파일이 업데이트 되는데 시간이 걸리기 때문에 해당 시간동안은 각 파드가 서로 다른 Config 값을 가지고 동작할 수 있음을 인지해야한다. 


    7.5 시크릿으로 민감한 데이터를 컨테이너에 전달

    ConfigMap을 이용해서 데이터를 전달할 때, 이 값은 상대적으로 덜 중요한 값이었다. 그렇지만 보안상의 이유로 중요한 데이터가 있을 수 있는데, 이런 녀석들은 Secret을 이용해서 전달하면 된다. 


    7.5.1 시크릿 소개

    시크릿은 사용상 ConfigMap과 유사한 방법으로 사용할 수 있다.

    • 환경변수로 전달.
    • 볼륨으로 마운트

    또한 시크릿은 이런 특징을 가진다.

    • 시크릿은 해당 시크릿을 마운트 하려고 하는 파드가 있는 노드에만 개별 시크릿을 배포함.
    • 시크릿은 노드의 메모리에서만 저장됨. 보안상의 문제인데, 메모리는 손쉽게 휘발되는 반면 물리 저장소에 기록되는 경우 삭제해도 데이터가 남을 수 있기 때문임. 
    • 마스터 노드의 etcd에는 시크릿이 암호화 되지 않은 형식으로 저장됨. 따라서 마스터 노드의 보호가 필요함. 

    이런 특성을 가지고 있기 때문에 ConfigMap, Secret을 사용하는 형태는 다음과 같이 이해할 수 있다. 

    • 민감하지 않고, 일반 설정 데이터는 컨피그맵을 사용한다.
    • 본질적으로 민감한 데이터는 시크릿을 사용해 보관한다. 

    7.5.2 기본 토큰 시크릿 소개

    책에 있던 쿠버네티스 버전 (1.17)에는 기본 토큰 시크릿 (default-token)이 존재했었던 것 같다. 하지만 최근의 쿠버네티스 버전(1.27) 에서는 기본 토큰 시크릿이 없는 것 같다. 


    7.5.3 시크릿 생성 + 타입

    시크릿을 생성하는 방법은 ConfigMap을 생성하는 방법과 동일하다. 아래와 같은 방식으로 생성할 수 있고, 생성된 데이터는 자동으로 Base64로 인코딩 되어서 Secret에 저장된다.

    $ kubectl create secret generic hello-secret \
    --from-literal=key1=value1 \
    --from-file=secret-files/

    또한 Secret은 세 가지 타입이 존재한다. 아래에서 볼 수 있듯이, 일반적으로 사용하는 secret은 generic 타입으로 선언하면 된다. 나머지는 Docker, tls 통신에서 사용할 secret이다. 

      docker-registry   Create a secret for use with a Docker registry
      generic           Create a secret from a local file, directory, or literal value
      tls               Create a TLS secret

    7.5.4 컨피그맵과 시크릿 비교

    앞서 이야기 한 것처럼 시크릿은 생성되고 나면, 값이 Base64로 인코딩 되어있다. 따라서 해당 Secret의 값을 보는 것만으로는 실제 어떤 값을 가지고 있었는지 알 수 없다. 하지만 파드에 환경변수 / 볼륨으로 전달될 때는 자동으로 디코딩 되어서 전달되므로, 사용하는 쪽에서는 ConfigMap / Secret을 구별하지 않고도 사용할 수 있다. 

    apiVersion: v1
    data:
      key1: dmFsdWUx
      key2: dmFsdWUy
    kind: Secret
    metadata:
      creationTimestamp: "2023-05-26T23:45:04Z"
      name: hello-secret
      namespace: default
      resourceVersion: "472354"
      uid: 9123fcd6-b0da-4f3c-98c0-d5a5031cadc9

    stringData 필드 소개

    stringData는 Secret을 yaml 파일로 선언할 때만 사용할 수 있는 필드다. 이 필드가 선언되어 있는 yaml 파일로 Secret을 생성하면, 자동으로 해당 값은 Base64로 인코딩 되어서 저장된다. 아래의 yaml 파일과 생성된 파일을 살펴보면 바로 이해할 수 있다.

    # 선언형 파일
    apiVersion: v1
    kind: Secret
    metadata:
      name: string-data-secret
    # 인코딩 하지 않은 값을 선언만함. 
    stringData:
      hello: no-encoded

    생성된 파일을 살펴보면 다음과 같다. 

    apiVersion: v1
    # 데이터가 인코딩 되어서 들어가 있음.
    data:
      hello: bm8tZW5jb2RlZA==
    kind: Secret
    metadata:
      annotations:
        kubectl.kubernetes.io/last-applied-configuration: |
          {"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"name":"string-data-secret","namespace":"default"},"stringData":{"hello":"no-encoded"}}
      creationTimestamp: "2023-05-26T23:48:37Z"
      name: string-data-secret
      namespace: default
      resourceVersion: "472922"
      uid: 8358fb14-63d3-4655-a86b-b32431750d94
    type: Opaque

    파드에서 시크릿 항목 읽기

    secret 볼륨 / 환경변수를 통해 파드에 노출하면, 시크릿 항목의 값이 실제 형식으로 디코딩 되어서 전달된다. 따라서 시크릿을 사용하는 쪽에서는 이 부분에 대해서 고민하지 않고 사용할 수 있다. 


    7.5.5 파드에서 시크릿 사용

    Nginx 컨테이너에서 아래 설정 파일을 사용하는 경우를 고려해보자. 이 때, https 통신을 지원하고 인증서를 다음과 certs/ 폴더 아래에서 참조해야 한다고 가정해보자. 그리고 파드는 인증서를 Secret을 통해서 공급할 것이다. 

    # my-nging-config.conf
    server {
        
        listen              80;
        listen              443 ssl;
        server_name         www.kubia-example.com;
        # 아래 인증서는 secret으로 넣을 예정
        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;
        }    
    }

    파드가 참조할 때는 다음과 같이 참조할 수 있다. 필요한 부분만 남기고 나머지는 삭제하면 대략 이런 느낌이다. 파드에서 Secret을 볼륨으로 마운트해서 쓰는 것도 일반 ConfigMap을 쓰는 것과 동일하다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: c7-24-nginx
    spec:
      containers:
      	...
            # Secret으로 볼륨 마운트
            - name: certs
              mountPath: /etc/nginx/certs
              readOnly: true
        ...
      volumes:
        ...
        - name: certs
          secret:
            secretName: fortune-https

    시크릿 볼륨을 메모리에 저장하는 이유

    Secret 볼륨은 시크릿 파일을 저장하는데 인메모리 파일 시스템(tmpfs)를 사용한다. 컨테이너에 마운트 된 볼륨을 조회하면 이를 확인할 수 있다.. tmpfs를 사용하는 이유는 민감한 데이터를 노출시킬 수도 있는 디스크에 저장하지 않기 위해서다. 

    $ kubectl exec fortune-https -- mount | grep certs
    >>>>
    tmpfs on /etc/nginx/certs type tmpfs (ro, realtime)

    환경변수로 시크릿 항목 노출

    Secret 역시 ConfigMap처럼 컨테이너에 환경변수로 전달해 줄 수 있다. 하지만 환경변수를 통해 Secret을 전달한다면, 한 가지 단점이 존재한다. 이 단점 때문에 Secret은 환경변수로 전달되는 것이 기피된다. 

    • 어플리케이션은 일반적으로 오류 보고서에 환경변수를 기록하거나 시작하면 로그에 환경변수를 남겨 의도치 않게 시크릿을 노출할 수 있음. 

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

    공개 Docker Hub에서 이미지를 가져오는 경우라면, 일반적인 방법으로 파드에서 사용할 이미지를 가져올 수 있다. 하지만 비공개 Repository에서 이미지를 가져온다면 자격증명이 필요하다. 이 때 Secret을 이용해서 처리할 수 있다.

     

    도커 레지스트리 인증을 위한 Secret 생성

    Secret은 docker-registry 타입이 존재하는데, 이 타입으로 생성하면 된다. 

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

    이렇게 생성된 Secret을 describe로 살펴보면 dockfconfigjson 파일이 존재하는 것을 알 수 있다. 파드가 이미지를 가져올 때, 이 파일을 이용해서 자격을 증명하고 이미지를 pull하는 것이다. 

    Name:         hello-hub-secret
    Namespace:    default
    
    Type:  kubernetes.io/dockerconfigjson
    
    Data
    ====
    .dockerconfigjson:  161 bytes

     

    파드에서 시크릿 사용하기

    파드에서 시크릿을 사용할 때는 다음과 같이 사용해주면 된다

    • imagePullSecrets 항목을 추가하고, 해당 항목에 도커 허브의 자격 증명이 있는 Secret을 추가하면 됨. 
    apiVersion: v1
    kind: Pod
    metadata:
      name: c7-11
    spec:
      containers:
        - name: c7-11
          image: ojt90902/fortune:args
      # 자격증명이 필요한 허브에서 가져오는 경우, docker-registry 타입의 secret을 사용해야 함.
      imagePullSecrets:
        - name: hello-hub-secret

    정리

    • Docker에서 ENTRYPOINT는 실행할 어플리케이션 설정, CMD는 ENTRYPOINT에 전달한 커맨드 라인 변수를 의미함. CMD는 언제든지 오버라이딩 될 수 있다. 
    • Docker에서 ENTRYPOINT는 Shell / Exec 방식으로 작성 가능함. Exec 방식으로 작성하는 것이 좋음. (불필요한 Shell이 안뜸)
    • Pod에 환경변수를 하드코딩하면, 설정과 파드 정의가 강하게 결합한다. 이것은 각 개발환경마다 새로운 파드 정의를 해야하는 불편함을 가져온다. 해결을 위해 ConfigMap + valueFrom을 이용한다. 
    • Pod에서 환경변수를 사용할 때는 env에 value, valueFrom을 사용할 수 있음.
      • value : 직접 값을 넣을 때 사용. 
      • valueFrom : ConfigMap, Secret을 참조해서 넣을 때 사용. 
    • Pod에서 ConfigMap이 가진 환경변수 전체를 전달한다면 envFrom을 사용.
    • 쿠버네티스에서 선언된 변수는 "$(VAR)"를 이용해서 전달할 수 있음. "$(VAR)-1"같이 다른 문자열을 붙일 수도 있음. 
    • 존재하지 않는 ConfigMap을 참조하는 파드 → 문제가 되는 컨테이너만 생성되지 않음. 나머지 컨테이너는 정상 생성됨. 
    • ConfigMap을 볼륨으로 선언해서 마운트 할 수 있음.
      • Key를 이름으로 가지는 파일이 생성됨. 파일의 내용은 Value임.
      • 볼륨을 마운트하면, 해당 디렉토리에 원래 있던 파일은 모두 삭제되고 볼륨에 있는 내용만 쓸 수 있음.
      • 볼륨을 선언할 때, ConfigMap이 가지고 있는 특정한 파일만 노출시킬 수 있음. items를 이용하면 됨.
    • ConfigMap 볼륨을 디렉토리에 마운트하면, 디렉토리에 기존에 존재하던 파일은 모두 삭제됨.
      • subPath(볼륨에서 가져올 파일 이름) + mountPath (컨테이너에 마운트 할 파일 이름)을 선택하면, 디렉토리의 다른 파일은 유지하면서 특정 파일만 마운트 할 수 있음.
    • ConfigMap의 파일 업데이트
      • ConfigMap 전체 볼륨을 마운트 한 경우, ConfigMap의 값이 수정되었을 때 이것을 사용하는 볼륨의 파일도 모두 업데이트 됨.
      • 파일 전체를 만들고, 한번에 심볼릭 링크를 추가하기 때문에 동시성 문제 고려 할 필요 없음. 
      • ConfigMap의 일부 파일만 마운트 한 경우, 파일은 업데이트 되지 않음.
    • Secret 관련
      • Secret은 해당 Secret이 필요로 하는 파드가 있는 노드에 생성되어, 메모리에 저장됨 (tmpfs)
      • Secret은 마스터노드 etcd에는 암호화 되어 있지 않은 채로 저장됨. 
      • Secret은 Base64로 인코딩 됨. 사용할 때는 자동으로 디코딩 되어서 사용함. 
      • Secret은 선언할 때, stringData 필드를 지원함. 여기에 선언해서 Secret을 만들면 자동으로 base64로 인코딩 됨.
      • Secret은 환경변수로 전달하는 것을 추천하지 않음. 어플리케이션 에러는 환경변수를 기록하는 경우가 있는데, 따라서 의도치 않게 중요한 값이 노출될 수 있기 때문임. 

    댓글

    Designed by JB FACTORY