애플리케이션이 다루는 설정값 중에는 SSH 키, 비밀번호, 인증 토큰처럼 외부에 노출되어서는 안 되는 값들이 있다. 컨피그맵도 문자열 값을 담아 환경 변수나 볼륨 파일로 파드에 전달할 수 있지만, 컨피그맵은 애초에 노출되어도 문제없는 일반 설정을 다루기 위한 오브젝트다.
쿠버네티스는 이런 민감 정보를 별도로 다루기 위해 시크릿(Secret)이라는 오브젝트를 둔다. 시크릿은 컨피그맵과 마찬가지로 네임스페이스에 속하고, 파드에 값을 전달하는 방식도 환경 변수와 볼륨 파일 두 가지로 동일하다.
다만 컨피그맵과 다른 부분은 시크릿의 값을 조회하면 base64로 인코딩되어 노출된다는 점이고, 사용 목적에 따라 여러 종류의 타입으로 나뉜다는 점이다.
컨피그맵과 마찬가지로 명령 한 줄로 시크릿을 만들 수 있다. 단순 문자열 값은 --from-literal 옵션으로 키-값을 지정한다.
kubectl create secret generic password \
--from-literal=password=1q2w3e4r
--from-file, --from-env-file 옵션도 컨피그맵과 동일하게 사용할 수 있다. 파일 하나를 통째로 값으로 담고 싶으면 --from-file을, key=value 형식의 줄이 여러 개 담긴 파일을 여러 키-값으로 풀어 넣고 싶으면 --from-env-file을 쓰면 된다.
여기서 쓴 generic은 시크릿의 종류를 뜻한다. 컨피그맵과 달리 시크릿은 사용 목적에 따라 몇 가지 타입으로 나뉘는데, generic으로 만든 시크릿은 특정 목적 없이 일반적으로 쓰는 Opaque 타입으로 자동 설정된다.
시크릿도 다른 리소스처럼 get으로 목록을 조회한다.
kubectl get secrets목록을 확인하면 생성한 password 외에 default-token-...으로 시작하는 시크릿도 확인할 수 있는데, 이는 ServiceAccount에 의해 네임스페이스마다 자동으로 생성되는 시크릿이다.
시크릿에 담긴 값은 describe나 -o yaml로 확인한다.
kubectl describe secret password
kubectl get secret password -o yaml
컨피그맵과 달리, 키-값 쌍에서 값이 base64로 인코딩되어 출력된다. base64는 암호화가 아니라 인코딩이므로 누구나 디코딩해 원래 값을 복원할 수 있다.
시크릿을 안전하게 만드는 것은 이 인코딩이 아니라 쿠버네티스의 RBAC와 같은 접근 제어이며, base64는 값에 개행이나 특수문자가 섞여 있어도 YAML 안에 안전하게 담기 위한 장치에 가깝다.
시크릿도 컨피그맵처럼 YAML로 정의하고 파드에서 참조하는 방식으로 관리하는 편이 낫다.
apiVersion: v1
kind: Secret
metadata:
name: password
type: Opaque
data:
password: ...
data 아래의 값은 base64로 인코딩된 값을 그대로 적어 넣는다. 이 시크릿을 파드에서 볼륨 파일로 마운트하려면, 컨피그맵과 마찬가지로 spec.volumes에 시크릿을 참조하는 볼륨을 정의하고 volumeMounts로 마운트 경로를 지정한다.
apiVersion: v1
kind: Pod
metadata:
name: secret-volume-pod
spec:
containers:
- name: my-container
image: busybox
args: ['tail', '-f', '/dev/null']
volumeMounts:
- name: secret-volume
mountPath: /etc/secret
volumes:
- name: secret-volume
secret:
secretName: password
여기서 중요한 점은, YAML에 base64로 인코딩한 값을 입력해 시크릿을 만들었더라도 파드가 이 값을 환경 변수나 볼륨 파일로 가져오면 디코딩된 원래 값을 사용하게 된다는 것이다. 위 예에서 /etc/secret/password 파일을 열어 보면 1q2w3e4r이 그대로 들어 있다.
디플로이먼트 등으로 파드를 생성할 때, YAML에 정의된 이미지가 노드 로컬에 없으면 쿠버네티스는 자동으로 이미지를 받아온다. 이미지가 프라이빗 레지스트리에 있다면 인증 절차가 필요한데, 쿠버네티스는 이때 docker login 명령어 대신 시크릿을 사용한다.
*도커 엔진은 docker login에 성공하면 인증 정보를 ~/.docker/config.json 파일에 저장해 뒀다가 이후 이미지를 받아올 때 자동으로 사용한다. 쿠버네티스는 이 인증 정보를 컨테이너 내부가 아니라 시크릿이라는 클러스터 오브젝트로 옮겨서 관리한다는 점이 다르다.
레지스트리 인증 시크릿을 만드는 방법은 두 가지다.
이미 docker login으로 로그인해 ~/.docker/configs.json이 만들어져 있다면, 그 파일을 그대로 시크릿의 데이터로 가져올 수 있다.
kubectl create secret generic registry-auth \
--from-file=.dockerconfigjson=/root/.docker/config.json \
--type=kubernetes.io/dockerconfigjson
docker-registry라는 전용 종류를 쓰면 사용자 이름과 비밀번호를 명령어에 직접 명시해 시크릿을 만들 수 있다.
kubectl create secret docker-registry registry-auth-by-cmd \
--docker-username=admin \
--docker-password=1q2w3e4r
--docker-server는 필수 옵션이 아니며, 지정하지 않으면 기본값인 도커 허브를 사용한다. 사설 레지스트리를 쓴다면 서버 주소를 함께 지정한다.
kubectl create secret docker-registry registry-auth-registry \
--docker-username=admin \
--docker-password=1q2w3e4r \
--docker-server=bluecool.registry.com
두 방법 중 어느 쪽을 쓰든, 생성된 시크릿은 kubernetes.io/dockerconfigjson이라는 동일한 타입으로 설정된다.
kubectl get secrets만들어진 시크릿은 파드 YAML의 imagePullSecrets에 이름을 지정해 사용한다.
spec:
containers:
- name: my-app
image: bluecool.registry.com/my-app:1.0
imagePullSecrets:
- name: registry-auth-registry
이미지를 받아오는 시점은 imagePullPolicy로 조절한다. Always는 파드가 생성될 때마다 레지스트리에서 이미지를 다시 확인해 받아오고, IfNotPresent는 노드에 같은 이미지가 없을 때만 받아온다. Never는 레지스트리 조회 없이 노드에 있는 이미지만 사용하며, 없으면 실패한다.
태그를 latest로 지정하면 기본값이 Always가 되고, 그 외의 태그를 명시하면 IfNotPresent가 기본값이 된다.
인증서와 개인 키 쌍을 다루는 kubernetes.io/tls 타입도 있다. 테스트용으로 자체 서명 인증서를 만들어 보자.
openssl req -new -newKey rsa:4096 -days 365 -nodes \
-x509 -subj "/CN=example.com" -keyout cert.key -out cert.crt
생성된 cert.crt, cert.key 파일로 TLS 시크릿을 만든다.
kubectl create secret tls my-tls-secret \
--cert cert.crt --key cert.key
cert.crt와 cert.key 파일의 내용은 각각 tls.crt, tls.key라는 키로 시크릿에 저장되며, 다른 시크릿과 마찬가지로 값은 base64로 인코딩된다.
kubectl get secrets my-tls-secret이렇게 명령어로 만든 시크릿을 YAML로 배포하려면, 인증서와 키 데이터를 YAML 파일에 함께 저장해 둬야 한다. 컨피그맵을 만들 때처럼 --dry-run=client -o yaml로 시크릿 정의를 뽑아내 파일로 저장해 두면 된다.
시크릿의 데이터가 많아질수록 base64로 인코딩된 값을 YAML 파일에 직접 저장해 두는 방식은 다루기 번거로워진다. 이럴 때는 kubectl에 내장된 kustomize 기능을 쓸 수 있다.
kustomize는 자주 쓰는 YAML 속성을 별도로 정의해 재사용하거나, 여러 YAML 파일을 하나로 묶는 등 다양한 용도로 쓰이는 기능이다.
시크릿은 kustomization.yaml에서 secretGenerator로 정의한다.
secretGenerator:
- name: password
literals:
- password=1q2w3e4r
컨피그맵을 kustomize로 생성하고 싶다면 configMapGenerator를 쓴다. 다만 컨피그맵은 시크릿과 달리 종류가 없으므로 type을 정의하지 않는다는 차이가 있다.
생성될 결과물은 적용 전에 미리 확인할 수 있다.
kubectl kustomize ./kustomization.yaml이 위치한 디렉토리에서는 apply -k 명령어로 정의된 리소스를 한 번에 배포한다.
kubectl apply -k ./
kubectl delete -k ./
kustomize로 생성된 컨피그맵이나 시크릿의 이름 뒤에는 내용을 해시한 값이 자동으로 붙는다. kubectl create configmap이나 kubectl create secret에 --append-hash 옵션을 줘도 같은 효과를 얻을 수 있다.
이름에 해시가 포함되면 내용이 바뀔 때마다 새 이름의 오브젝트가 생성되므로, 이를 참조하는 디플로이먼트의 파드 스펙도 함께 바뀌어 롤링 업데이트가 자연스럽게 트리거된다.
이미 배포된 시크릿의 값을 바꾸는 방법은 여러 가지다. kubectl edit으로 바로 수정하거나, YAML 파일을 고친 뒤 kubectl apply로 반영하거나, kubectl patch로 일부 필드만 바꿀 수도 있다.
문제는 이렇게 값을 바꿔도 파드에 항상 즉시 반영되지는 않는다는 점이다. 시크릿이나 컨피그맵을 환경 변수로 파드에 제공한 경우, 값을 바꿔도 이미 떠 있는 파드의 환경 변수에는 반영되지 않는다. 환경 변수는 컨테이너가 시작될 때 한 번 주입되고 끝이기 때문에, 새 값을 적용하려면 파드를 재시작해야 한다.
반면 볼륨 파일로 마운트한 경우에는 값이 바뀌면 마운트된 파일의 내용도 자동으로 갱신된다. 다만 파일 내용이 갱신됐다고 해서 파드 내부의 프로세스가 이를 자동으로 다시 읽어 들이는 것은 아니다.
프로세스가 갱신된 값을 실제로 반영하려면 별도의 로직이 필요하다. 예를 들어 파일 변경을 감지해 원래 프로세스에 리로드 시그널을 보내는 사이드카 컨테이너를 파드에 함께 두거나, 애플리케이션 코드 수준에서 쿠버네티스 API로 컨피그맵, 시크릿 변경 알림(Watch)을 받아 스스로 다시 읽어 들이게 만드는 방법이 있다.
참고로 앞서 살펴본 default-token-... 시크릿처럼 ServiceAccount와 연결된 시크릿은 조금 특별하게 동작한다. 이런 시크릿은 실수로 삭제하더라도 쿠버네티스가 감지해 자동으로 다시 만들어 준다.
ServiceAccount가 클러스터 API에 접근하기 위한 필수 인증 수단이므로, 이를 잃어버리지 않도록 쿠버네티스가 직접 보장해 주는 셈이다.
