해당 포스팅은 쿠버네티스 어나더 클래스 (지상편) - Sprint 1, 2 강의 내용을 기반으로 작성했습니다.
Configmap 기본 설명
Configmap과 Secret은 Pod 내부에 다양한 설정값을 주입하기 위한 object이다.
Configmap을 사용해 컨테이너 이미지에서 configuration을 분리하여 App을 사용할 수 있다.
예를 들어 개발, 운영 단계에서 달라지는 설정값들을 모두 이미지 빌드 단계에서 설정하지 않고,
각 환경에 맞는 Configmap을 Pod에 주입함으로써 편리하게 App을 실행할 수 있다.
Configmap을 정의하는 일반적인 yaml파일 형식은 다음과 같다.
apiVersion: v1
kind: ConfigMap
metadata:
name: special-config
namespace: default
data:
SPECIAL_KEY: "very"
SPECIAL_LEVEL: "charm"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: game-config
namespace: default
data:
game.properties: |
enemies=aliens
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30
ui.json: |-
{
"color": {
good=purple,
bad=yellow
}
}
configmap의 data 정의의 가장 기본적인 형태는 special-config 처럼 key:value 형식으로 data를 정의하는 것이다. 또 다른 형태는 game-config처럼 data를 game.properties, ui.json과 같이 파일 형태로 정의할 수 있다.
다음으로 정의한 configmap의 데이터를 pod에 주입시키기 위해서 다양한 방법이 존재하는데,
아래 Deployment 정의를 통해 configmap의 data를 연결하는 다양한 예시를 살펴보자.
apiVersion: apps/v1
kind: Deployment
metadata:
name: game
spec:
template:
spec:
containers:
- name: game-container
image: registry.k8s.io/busybox
envFrom:
- configMapRef:
name: special-config
env:
- name: SPECIAL_LEVEL_KEY
valueFrom:
configMapKeyRef:
name: special-config
key: SPECIAL_LEVEL
- name: SPECIAL_KEY
value: "NEW_KEY"
volumeMounts:
- name: config
mountPath: "/app/config"
readOnly: true
volume:
- name: config
configMap:
name: game-config
metadata나 다른 속성값들은 제외하고 configmap과 관련된 속성들만 남겼다.
위 예시에는 컨테이너에 configmap data를 주입하는 3가지 방법이 나타나있다.
- configMapRef: envFrom 속성에서 configMapRef를 사용해 name과 일치하는 configmap의 모든 data를 환경변수로 주입한다.
- configMapKeyRef: env 속성에서 valueFrom.configMapKeyRef를 사용해 configmap에서 key와 일치하는 개별 데이터를 환경변수로 주입한다.
- configmap volume: volume에서 configMap을 연결하면 name이 일치하는 volumeMounts와 매칭이 된다. mountPath 경로 아래에 configmap의 key와 일치하는 파일들이 생성된다. (game.properties, ui.json)
눈썰미가 좋은 사람이라면, 위 예제에서 한 가지 이상하다고 느낄 것이다..
바로 envFrom.configMapRef에서 special-config를 연결해 SPECIAL_KEY, SPECIAL_LEVEL 환경 변수를 정의했는데, env 필드에 동일한 이름이지만 다른 value를 가지는 SPECIAL_KEY가 새롭게 정의된 것이다.
이렇게 디플로이먼트 yaml파일에서 환경변수를 설정할 때 환경변수의 이름이 중복되는 경우 env에서 정의된 값이 envFrom 에서 정의된 값보다 우선순위를 가진다. 따라서 SPECIAL_KEY는 "very"가 아닌 "NEW_KEY"를 value로 가진다.
이 방법을 통해 환경 변수의 값을 디플로이먼트의 파드 정의에서 간단히 수정할 수 있어 편리하고, 값을 바꿔가며 문제가 발생한 위치를 특정할 수 있다.
Secret 기본 설명
Secret은 configmap과 사용방법이 거의 동일하지만 패스워드, 토큰, key와 같은 중요한 데이터를 보관할 때 사용한다. 시크릿 또한 configmap과 마찬가지로 환경변수로 등록되거나, 컨테이너에 마운트된 볼륨의 파일처럼 파드에 등록할 수 있다. 아래는 그 예시이다.
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
stringData:
username: admin
password: mypassword
data:
USER_NAME: YWRtaW4=
PASSWORD: MWYyZDFlMmU2N2Rm
---
apiVersion: v1
kind: Pod
metadata:
name: secret-test-pod
spec:
containers:
- name: test-container
image: registry.k8s.io/busybox
command: [ "/bin/sh", "-c", "env" ]
envFrom:
- secretRef:
name: mysecret
volumeMounts:
- name: foo
mountPath: "/etc/foo"
env:
- name: SECRET_USERNAME
valueFrom:
secretKeyRef:
name: mysecret
key: username
optional: false
volumes:
- name: foo
secret:
secretName: mysecret
defaultMode: 0400
restartPolicy: Never
먼저 secret 정의를 살펴보면 configmap과 거의 동일하지만 다른 점이 몇가지 존재한다.
우선 type이 존재하고 데이터를 정의하는 부분이 data, stringData 필드 두개로 존재한다는 부분이다. 시크릿은 여러 종류의 기밀 데이터를 용이하게 처리하기 위해 여러가지 타입이 있는데, 기본값으로 Opaque를 가지며 이 타입은 configmap과 동일한 역할로 사용자 정의 데이터를 처리한다. 다른 타입은 추후 포스팅에서 다루도록 하겠다.
데이터 정의부는 stringData 필드나 data 필드를 사용해서 정의할 수 있는데 두 차이점은 다음과 같다.
시크릿은 기본적으로 평문 문자열을 base64 인코딩/디코딩한 값을 사용하여 보안을 지킨다.
따라서 우리가 값을 지정해줄 때 평문 문자열은 stringData 필드에, base64로 인코딩된 값은 data 필드에 정의하면 된다. 두 속성값을 모두 사용할 필요는 없으며 둘 중 하나만 정의해서 사용해도 무방하다. (위 예시는 그저 한번에 설명하기 위함)
다음으로 secret을 pod에 연결하는 방식은 configmap과 거의 동일하다.
다만 configMapRef, configMapKeyRef, volume.configMap 속성을 secretRef, secretKeyRef, volume.secret 처럼 configMap -> secret으로 치환하여 사용하면 된다.
Configmap, Secret 작동 예제
Configmap의 data를 보면 key:value 형식의 값들이 존재한다.
App 동작시에는 크게 3가지 유형의 환경변수가 사용된다
- 인프라 환경에 따른 값: spring_profiles_active는 App이 어느 환경에서 돌아가는지 App이 기동되는 시점에 알려주기 위한 환경변수이다.
- App의 기능을 제어하기 위한 값: application_role은 App의 역할을 지정한다. 마이크로 서비스 아키텍처를 반영한다.
- 외부 환경을 App으로 주입시키기 위한 값: postgresql_filepath는 Secret 데이터로 연결할 파일의 경로이다. 해당 경로는 Pod의 mountPath에서 정하고, App에서 기동할 때 환경변수에서 DB의 정보를 확인하고 접속할 수 있다. 따라서 mountPath의 경로를 변경하고 싶을 때 앱을 다시 빌드하지 않고 Configmap data만 바꾸면 된다.
Configmap으로 위와 같은 3가지 유형의 환경변수를 정의하여 값을 주입했을 때
실제 Spring App의 작동 예시는 다음과 같다.
- Configmap의 data가 컨테이너 내부의 환경변수로 주입된다.
- spring 컨테이너 이미지는 java jar파일 실행 명령어를 수행하는데, 이 때 환경변수 값들이 매칭되어 들어간다.
만약 spring_profiles_active 값이 없으면 null로 들어가게 된다. - spring의 경우 각 환경별로 값들을 세팅할 수 있는 파일이 있어 spring이 dev라는 값을 보고 application-dev.yaml 파일을 기반으로 App 초기화를 수행한다.
Secret의 stringData를 보면 데이터베이스 정보가 있는데 postgre-info.yaml이라는 파일형식으로 값을 설정한다.
해당 형식으로 오브젝트를 저장하면 stringData는 쓰기 전용 속성이고, 실제 데이터는 configmap과 동일하게 data 속성으로 저장이 되는데, key는 그대로지만 value값이 base64 인코딩되어 저장된다.
Pod 설정시 volumeMounts에 mountPath를 설정하면 컨테이너 내부에 해당 Path가 생성되고,
그 다음 volume이 매칭되어 secret과 연결이 되고 컨테이너 안에 파일이 만들어 진다.
이 때 value 값이 다시 디코딩 되기 때문에 컨테이너 내부에서는 내가 입력 형태의 값이 보인다.
App이 기동할 때 postgresql-info.yaml 파일을 읽어 DB 정보를 얻어올 수 있다.
이러한 과정을 봤을 때 secret은 그 이름과 달리 그렇게 보안적인 이점을 얻어올 수 없다.
configmap에서 비밀번호를 저장하고 사용해도 무방하다.
데이터 암호화 부분은 configmap, secret과는 별개로 생각해야 한다.
다만 secret을 volume으로 연결하지 않고 envFrom으로 환경변수에 값을 주입할 수 있는데,
이는 Pod에 들어가서 누구나 env 명령으로 중요한 데이터를 노출시킬 수 있어 이러한 방법은 지양하는 것이 좋다.
영역 파괴의 주범 ConfigMap
쿠버네티스 환경을 보면 각 인프라 환경마다 Pod가 만들어진다.
이 때 각 Pod는 모두 동일한 container image를 다운받아 생성되고, 환경에 따른 설정값을 configmap으로 관리한다.
이를 이미지 안에 java jar 명령어에 환경변수 값을 읽어 실행하도록 설정한다.
개발 환경을 보면 스프링으로 개발하고 GitHub으로 소스를 커밋하면 Jenkins에서 파이프라인이 실행된다.
파이프라인 내 로직에서는 소스 빌드 이후 컨테이너를 빌드하여 컨테이너 이미지가 dockerhub에 업로드되고
dev 환경에 Pod를 즉시 배포한다. qa와 prod 환경은 필요할 때 배포 버튼을 누르도록 구성했다.
만약 이를 쿠버네티스가 아닌 일반적인 VM 환경에서 수행했을 때의 경우를 살펴보자.
스프링으로 개발하고 소스 코드를 빌드하는 단계까지는 동일하다.
인프라 담당자는 환경별로 서버를 세팅해놓고 openjdk도 설치를 해놓는다. 이 때 필요한 환경 변수 값을 관리해야 한다.
배포는 JAR package 파일을 VM 환경에 복사해놓고 실행 명령어를 직접 날린다.
이 때 데브옵스 담당자가 이 App에 환경이나 목적에 맞는 변수를 빌드 시에 직접 넣는다.
Jenkins에도 환경변수 관리 기능이 있기 때문에 이를 이용해 실행 스크립트를 구성한다.
또한 개발자는 환경마다 Properties 파일을 관리해야 한다.
각 파트마다 관리해야 했던 변수 값들을 쿠버네티스에서는 configmap을 통해 편리하고 일관성있게 관리할 수 있다.
다만 큰 프로젝트의 경우 각 영역마다 담당자가 있고, 각 담당자들이 모두 쿠버네티스를 다룰 줄 아는 것이 아니다.
또한 모든 인프라가 다 쿠버네티스 위에서 돌아가는 것도 아니다.
따라서 configmap에 데이터를 넣기 위해 각 영역의 담당자들과 논의를 해야한다.
이러한 과정에서 사람마다 본인의 영역이 침범됐다고 느낄 수 있다고 한다(영역파괴의 주범).
configmap 또한 결국 관리를 원활하게 하기 위한 object이기 때문에 기능적인 부분만 생각하는 것이 아니라
배포와 운영 단계까지 고려하여 잘 설계해야 하는 것이 중요한 포인트이다.
이름 때문에 기대가 너무 컸던 Secret
Secret은 이름만 보면 중요한 데이터를 보안적으로 관리하는 목적으로 사용될 것 같지만
동작방식을 보면 실상 엄청 그렇지 않아 보이고, configmap만 사용해도 충분해 보인다.
그렇다면 실제로 Secret을 어떻게 사용하는지에 대한 케이스를 살펴볼 필요성이 있다.
Secret에는 configmap과 다르게 type이라는 속성이 존재한다.
기본값은 "opaque"이며 이 값을 사용하면 configmap과 동일한 기능을 수행한다.
하지만 다른 type을 사용할 경우 configmap과 기능이 전혀 달라지게 된다.
먼저 type에 docker-registry을 주면 data로 docker-username, docker-password, docker-email key를 꼭 포함해야 한다.
이는 private docker registry를 사용할 때 해당 registry에서 이미지를 가져오기 위해 접근을 허용하는 secret이다.
이 타입의 secret을 pod에 연결할 때는 imagePullSecrets 속성에 연결해주어야 한다.
결국 쿠버네티스에서는 사용자 편의에 따라 커스텀하게 관리할 수 있는 요소를 제공해 주는데,
그걸 사용할 때 Secret이 key와 value를 설정하는 템플릿 역할을 해주는 것이다.
따라서 각각의 타입에는 정해진 key가 존재한다.
예시로 또 다른 type은 tls가 있는데, pod마다 다른 보안 인증서를 심을 때 사용한다.
하지만 이러한 type들 중에 데이터 암호화를 제공해 주는 것은 없다.
따라서 중요 데이터에 대한 암호화는
- Secret에 대한 오브젝트 생성을 파이프라인에 태워서 만들지 않고 Cluster에서 직접 생성하고 관리.
쿠버네티스 관리 권한을 잘 설정하면 아무나 Secret을 조회하거나 Pod에 접근할 수 없다.
하지만 오브젝트를 yaml 파일로 배포하려면 형상관리 서버나 배포 서버를 거쳐야 하므로
모든 오브젝트를 쿠버네티스 내에서만 사용할 수 없다. 따라서 비밀번호를 사용하는 Secret만 클러스터 상에서 직접 관리하는 방식을 사용할 수 있다. - 특정 key에 대해 자체 암호화를 통해 문자를 암호화하여 Secret으로 관리한다. 그러면 파이프라인 상에서 중간에 누가 문자를 보더라도 실제 비밀번호를 알 수는 없다. 다만 App 내부에 이 암호를 복호화하는 로직을 포함하고 있어야 한다. 이러한 방식을 사용하면 secret이 아니라 configmap을 사용해도 무방하다.
- Vault 같은 보안 관련 서드파티를 사용하여 관리한다. 관리자에 한해 아이디와 패스워드로 Vault에 접속하고 App에서 쓸 비밀번호를 입력한다. 그러면 pod가 기동하면서 vault에 비밀번호를 요청하는데, 관리자가 지정한 pod에만 비밀번호를 주도록 설정할 수 있다.
2, 3번 방식은 클러스터 내에 누가 접근해도 App의 중요 데이터가 노출되는 일을 막을 수 있다는 장점이 있다.
'인프라' 카테고리의 다른 글
[Kubernetes] 쿠버네티스 Component 동작으로 이해하기 (0) | 2024.05.03 |
---|---|
[Kubernetes] 쿠버네티스 기능 이해하기 - PVC, PV / Deployment / HPA / Service (0) | 2024.05.02 |
[Kubernetes] 쿠버네티스 기능 이해하기 - Probe (0) | 2024.04.29 |
[Kubernetes] 쿠버네티스 Object 이해하기 (0) | 2024.04.26 |
[Kubernetes] 쿠버네티스가 실무에서 편한 이유 (0) | 2024.04.25 |