티스토리 뷰

728x90
반응형

서론

Legacy 환경에서 각 환경별(개발, 테스트, 운영)로 설정을 구분하기 위해 우리는 Property를 활용해 왔다. Property를 적용하기 위해 application.properties나 application.yml과 같은 설정 파일 기반으로 적용하거나 @Configuration 어노테이션을 사용하여 각 환경 별 구분된 환경 정보를 가져갈 수 있었다. 이러한 방식은 모두 애플리케이션 레벨에서 환경 설정을 관리하는 방법이라 할 수 있다.

최근 애플리케이션은 Cloud Native하게 개발하기 위해 개발방식도 변화되어 가고 있다. 특히 Spring Boot 기반의 가볍고 빠른 개발을 지원하는 Runtime Framework의 활용도가 높아지고 있으며, 이는 Cloud Platform에 빠르게 이식되어 민첩하게 배포 될 수 있도록 개발해야 한다. 이를 CNA라고 부른다.
이때 환경 설정을 애플리케이션 레벨에서 관리하게 될 경우 소스코드와의 Dependency가 많아지고, 환경 설정의 변경이 발생할때 마다 매번 소스코드 체크아웃부터 시작한다면, 이는 CNA의 신속한 반영을 저해하는 요소가 될 수 있다. 따라서 Kubernetes와 같은 클라우드 환경에 적용하기 위해서는 애플리케이션 레벨이 아닌 오브젝트 레벨에서 환경 설정을 관리할 수 있도록 구성해야 한다.

이를 Kubernetes는 configMap과 secret이라는 오브젝트를 통해 제공하고 있다.

configMap은 다른 오브젝트가 사용할 구성 대체로 Pod 내 컨테이너가 사용할 구성을 저장할 수 있는 Kubernetes의 오브젝트이다. spec 이 있는 대부분의 쿠버네티스 오브젝트와 달리, 컨피그맵에는 data 및 binaryData 필드가 있다. 이러한 필드는 key - value 값으로 구성된다. data 필드와 binaryData 는 모두 선택 사항으로 data 필드는 UTF-8 바이트 시퀀스를 포함하도록 설계되었으며 binaryData 필드는 바이너리 데이터를 포함하도록 설계되었다.
configMap은 많은 양의 데이터를 보유하도록 설계되지 않았다. configMap에 저장된 데이터는 1MiB를 초과할 수 없다. 이 제한보다 큰 설정을 저장해야 하는 경우, 볼륨을 마운트하는 것을 고려하거나 별도의 데이터베이스 또는 파일 서비스를 사용할 수 있다.
configMap은 별도의 보안 또는 암호화를 제공하지 않는다. 저장되어 있는 data를 kubernetes api 또는 yaml 파일을 통해 손쉽게 값을 확인할 수 있어 Credential 정보를 저장하기에는 부적합하며, 일반적인 정보를 저장하는데 적합하다. 예를 들어 로그 레벨이나 dev, prod와 같은 환경 구분 등을 설정하는데 활용된다.

따라서 저장하려는 데이터가 Credential한 정보 즉, ID/Passwd 등 인 경우, configMap 대신 시크릿(Secret) 또는 3rd Party 툴을 사용하여 데이터를 비공개로 유지하는 것이 바람직하다.


ConfigMap 적용

configMap을 사용하여 Pod에서 참조할 수 있도록 구성하는 방법으로는 크게 네가지 방법을 제시할 수 있다.

1) 컨테이너 내에서 CLI 또는 ARGS로 참조
2) 컨테이너의 환경 변수 
3) 애플리케이션이 참조할 수 있도록 읽기 전용 볼륨에 파일을 추가하여 참조
4) 쿠버네티스 API를 사용하여 컨피그맵을 읽는 파드 내에서 실행할 코드 작성

이러한 방법들은 소비되는 데이터를 모델링하는 방식에 따라 다르게 쓰인다. 처음 세 가지 방법의 경우, kubelet이 Pod내 컨테이너를 시작할 때 configMap의 데이터를 사용한다. 네 번째 방법은 configMap과 데이터를 읽기 위해 코드를 작성해야 한다는 것을 의미한다.

먼저 간단히 샘플을 작성하여 테스트 해보도록 하자. 샘플은 Spring Boot 환경 기반 application.properties에 env를 적용하고 configMap으로 변경하여 반영하는 방법이다.

a. 소스코드 작성

application.properties

echo.log.level=Debug

HelloController.java

package com.sonnara.k8s;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
	@Value("${echo.log.level}")
	String echoLogLevel;

	@RequestMapping(value = "/hello", method = RequestMethod.GET)
	public String hello() {
		return "Hi MSA World!" + " " + echoLogLevel;

	}
}

위와 같이 적용하고 호출하면, 아래와 같이 화면에 출력되는 것을 확인할 수 있다.

이를 configMap에 적용하기 위해 다음과 같이 application.properties를 수정한다.

echo.log.level=${LOG_LEVEL}

b. configMap.yaml 작성

이제 LOG_LEVEL이라는 환경 변수를 configMap으로부터 받아 HelloController.java에 매핑 시켜주면 완성이다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: loglevel-configmap
data:
  loglevel: DEBUG

위는 loglevel이라는 key와 INFO라는 value를 갖는 loglevel-configmap이라는 이름의 ConfigMap을 정의한 yaml 파일이다. 이를 deployment.yaml과 매핑하여 Container 내부 env로 전달한다.

c. deployment.yaml 작성

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-configmap-deployment
  labels:
    app: k8s-configmap
spec:
  replicas: 1
  selector:
    matchLabels:
      app: k8s-configmap
  template:
    metadata:
      labels:
        app: k8s-configmap
    spec:
      containers:
      - name: k8s-configmap
        image: 192.168.123.141:5000/middleware/springboot/configmap:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        env:
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef: 
              name: loglevel-configmap
              key: loglevel

configMap이 매핑되어 HelloController의 return echoLogLevel까지 전달되는 과정은 다음과 같이 도식화 해 볼 수 있다.위와 같이 spec.template.spec.container.env를 활용하여 환경변수를 적용한다.

작성이 완료되면 각각 apply를 수행하고 테스트를 진행해보자.

위와 같이 configMap에 적용한 key(loglevel)를 기반으로 value 값인 INFO가 함께 출력되는 것을 확인할 수 있다.

d. config 수정 반영

properties 파일이나 @Configuration 어노테이션을 적용할 경우 config를 변경하면, 소스코드 빌드/패키징 부터 전체 플로우를 모두 진행한 후 배포를 진행해야 했다. 위와 같이 configMap을 사용하면 아래와 같이 쉽게 적용이 가능하다.

[root@ip-192-168-114-198 ~]# kubectl edit configmap loglevel-configmap
# 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:
  loglevel: TRACE
kind: ConfigMap
metadata:
  creationTimestamp: "2021-01-05T00:59:13Z"
  name: loglevel-configmap
  namespace: default
  resourceVersion: "221016"
  selfLink: /api/v1/namespaces/default/configmaps/loglevel-configmap
  uid: 3b9c36a7-910e-49a1-a3a0-6d05b7a0628b
configmap/loglevel-configmap edited
[root@ip-192-168-114-198 ~]#

위와 같이 kubectl edit (loglevel: TRACE)로 변경한 후 

[root@ip-192-168-114-198 ~]# kubectl rollout restart deployment/k8s-configmap-deployment
deployment.apps/k8s-configmap-deployment restarted
[root@ip-192-168-114-198 ~]# kubectl get pods
NAME                                        READY   STATUS        RESTARTS   AGE
k8s-configmap-deployment-76d87bc586-m7z28   0/1     Terminating   0          19m
k8s-configmap-deployment-7d4b6bcc94-t4js7   1/1     Running       0          6s
[root@ip-192-168-114-198 ~]#

kubectl rollout으로 deployment를 restart를 하면

위와 같이 정상적으로 TRACE로 변경된것을 알 수 있다.

configMap만 수정한 후에는 변경된 데이터가 반영되지는 않는다. 앞서 적용 방법에 대해 이야기했듯이 configMap은 kubelet이 pod 내 container를 기동하는 시점에 configmap을 적용하기 때문이다. 다만, 별도로 application level에서 수정하지 않고도 environment가 적용되는 것을 확인할 수 있다.


Secret 적용

a. secret.yaml 작성

apiVersion: v1
kind: Secret
metadata:
  name: loglevel-secret
data:
  loglevel: U0UtSU5GTw==

Secret도 ConfigMap과 동일한 형태로 yaml파일이 작성된다. 다만 plain type의 ConfigMap과 다르게 Base64 형태로 value를 구성한다. configmap의 경우 loglevel: SE-INFO라고 구성한다면, secret은 loglevel: U0UtSU5GTw==으로 구성한다.

b. deployment.yaml 작성

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-configmap-deployment
  labels:
    app: k8s-configmap
spec:
  replicas: 1
  selector:
    matchLabels:
      app: k8s-configmap
  template:
    metadata:
      labels:
        app: k8s-configmap
    spec:
      containers:
      - name: k8s-configmap
        image: 192.168.123.141:5000/middleware/springboot/configmap:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        env:
        - name: CONFIGMAP_LOG_LEVEL
          valueFrom:
            configMapKeyRef: 
              name: loglevel-configmap
              key: loglevel
        - name: SECRET_LOG_LEVEL
          valueFrom:
            secretKeyRef: 
              name: loglevel-secret
              key: loglevel

configmap과 마찬가지로 deployment.yaml에 정의하며, configMapKeyRef 대신 secretKeyRef에 name과 key를 정의한다.

c. 소스코드 작성

<application.properties>
echo.secret.log.level=${SECRET_LOG_LEVEL}

<HelloController.java>
@Value("${echo.secret.log.level}")
String echoSecretLogLevel;

위와 같이 ConfigMap과 Secret을 함께 정의한 후 배포 및 호출을 진행해 보자.

위와 같이 ConfigMap은 Plain 형태 그대로 표출되고, Secret은 Base64로 정의된 Value의 Decode 값이 Return 되는 것을 확인할 수 있다.

이와 같이 Log Level, Database Info, Spring Profile 등 간단한 설정 정보를 Properties 파일로 관리할 수도 있지만, configMap과 secret으로도 손쉽게 적용이 가능하며, 민첩하게 반영이 가능하다.


결론

Cloud Native Application을 개발하기 위해서는 경량화 된 Runtime Framework를 사용하여 민첩하게 배포하는 것이 무엇보다 중요하다. 단순한 환경 설정을 적용하기 위해 전체 CI/CD 파이프라인을 태우면서 배포해야 하는 것은 민첩성에 부합한다. 코드 레벨을 벗어나 Kubernetes Object 레벨에서 설정을 적용하기를 원한다면, configMap과 secret을 사용해 보자.

728x90
반응형