3.2 매니페스트 작성
오브젝트를 파일로 적어 둔 것이 매니페스트다. 형식은 YAML이 사실상 표준이지만 API server가 실제로 받는 것은 JSON이고, YAML은 전송 전에 JSON으로 변환된다. 이 변환이 매니페스트 작성 사고의 상당 부분을 설명한다. YAML은 사람이 쓰기 편하도록 따옴표와 괄호를 생략하게 해 주는데, 그 생략이 타입 추론과 만나면 의도와 다른 값이 만들어진다.
YAML이 놓는 함정
들여쓰기부터. YAML은 들여쓰기로 구조를 표현하고 탭을 허용하지 않는다. 공백 2칸이 관례다. 들여쓰기가 한 칸 어긋나면 필드가 다른 부모 아래로 이동하는데, 이동한 위치가 스키마상 유효하지 않으면 에러로 드러나지만, 우연히 유효한 위치면 조용히 다른 의미가 된다. 에러가 나는 쪽이 오히려 다행인 구조다.
다음은 타입 추론이다. 따옴표 없는 스칼라를 YAML 파서는 생긴 모양대로 해석한다.
env:
- name: MYSQL_PORT
value: 3306 # 문자열이 아니라 숫자 3306
- name: DEBUG
value: no # 문자열 "no"가 아니라 boolean false
- name: APP_VERSION
value: 1.10 # 문자열 "1.10"이 아니라 숫자 1.1env의 value는 문자열이어야 하므로 셋 다 “cannot unmarshal number(bool) into … of type string” 같은 에러로 거부된다. 에러가 나서 다행이지만, 메시지가 타입 얘기만 해서 원인이 따옴표 누락이라는 점을 바로 알아채기 어렵다. YAML을 받는 도구가 Kubernetes만도 아니어서, 스키마 검증이 느슨한 도구(CI 설정, 템플릿 값 파일)에서는 잘못된 타입이 에러 없이 통과하기도 한다. 숫자나 boolean처럼 보이는 문자열은 항상 따옴표로 감싼다.
value: "3306"멀티라인 문자열은 두 기호를 구분한다. |는 개행을 보존하고 >는 개행을 공백으로 접는다.
data:
init.sql: |
CREATE TABLE t (id int);
CREATE INDEX ON t (id);
note: >
여러 줄로 적었지만
한 줄로 접힌다.스크립트, 설정 파일, 인증서처럼 개행이 의미를 갖는 내용에 >를 쓰면 내용이 망가진다. 이런 값은 |로 적는다.
필수 필드와 kubectl explain
매니페스트의 최소 골격은 apiVersion, kind, metadata(그 안의 name)이고, 대부분의 오브젝트가 여기에 spec을 더한다. 이 네 필드 중 하나라도 빠지면 API server가 거부한다.
spec 안쪽의 필드가 기억나지 않을 때는 검색 대신 kubectl explain을 실행한다. explain은 접속한 클러스터가 제공하는 OpenAPI 스키마를 읽으므로, 웹 문서와 달리 그 클러스터 버전에 정확히 맞는 답이 나온다.
$ kubectl explain deployment.spec.strategy
KIND: Deployment
VERSION: apps/v1
FIELD: strategy <DeploymentStrategy>
DESCRIPTION:
The deployment strategy to use to replace existing pods with new ones.점 표기로 원하는 깊이까지 내려가고, --recursive를 붙이면 하위 필드 전체가 트리로 나온다. 어떤 필드가 필수인지, 타입이 무엇인지도 함께 표시되므로 새 종류의 오브젝트를 처음 쓸 때 출발점으로 삼기 좋다.
kubectl apply와 3-way merge
매니페스트를 반영하는 명령은 셋이다. create는 오브젝트가 없을 때만 성공하고, replace는 기존 오브젝트를 통째로 갈아치우며, apply는 “이 파일이 원하는 상태다"라고 반복 선언한다. 있으면 차이만 반영하고 없으면 만드니까, 선언형 워크플로에서 쓰는 것은 apply다.
apply가 차이를 계산할 때 참조하는 것은 세 버전이다.
- 직전에 apply했던 내용. 오브젝트의
kubectl.kubernetes.io/last-applied-configurationannotation에 JSON으로 저장되어 있다. - 클러스터에 있는 현재 라이브 오브젝트.
- 지금 제출하는 새 매니페스트.
이 셋을 비교하는 것이 3-way merge다. 새 매니페스트에 추가된 필드는 반영하고, 매니페스트에 없지만 다른 주체(controller, admission, autoscaler)가 채운 필드는 그대로 두고, 직전 apply에는 있었는데 새 매니페스트에서 사라진 필드는 삭제한다. 마지막 동작이 last-applied 사본이 존재하는 이유다. 라이브 오브젝트와 새 파일만 비교해서는 “원래 없던 필드"와 “지웠던 필드"를 구분할 방법이 없다.
kubectl edit나 kubectl scale 같은 명령형 명령으로 함께 고치면 세 버전의 관계가 어긋난다. 다음 apply가 그 변경을 되돌리기도 하고 남겨두기도 해서 결과 예측이 어려워진다. 한 오브젝트의 관리 방식은 하나로 통일한다.server-side apply와 field manager
위의 merge 계산은 kubectl 프로세스 안에서 일어난다. 그래서 client-side apply라 부른다. server-side apply(SSA)는 같은 계산을 API server로 옮긴 방식으로, 1.22에서 GA가 됐다.
kubectl apply --server-side --field-manager=deploy-pipeline -f web.yamlSSA에서는 last-applied annotation 대신 서버가 오브젝트의 metadata.managedFields에 필드 단위 소유자를 기록한다. 이 소유자를 field manager라 부른다. 필드마다 임자가 명시되므로 여러 주체가 한 오브젝트를 나눠 관리하는 상황이 명확해진다. 배포 파이프라인이 이미지 필드를 소유하고 HPA가 replicas를 소유하면, 파이프라인의 apply가 HPA의 스케일 결정을 덮어쓰지 않는다.
다른 manager가 소유한 필드를 바꾸려 하면 서버가 409 Conflict로 거부한다. 소유권을 넘겨받을 의도가 맞을 때만 --force-conflicts로 강행한다. 거부가 아니라 강행이 기본이 되면 SSA를 쓰는 이유가 사라지므로, conflict 에러를 만나면 어느 도구와 겹쳤는지부터 확인하는 편이 맞다.
정리
매니페스트 사고의 두 축은 YAML 타입 추론과 들여쓰기이고, 스키마 확인은 kubectl explain으로 클러스터 안에서 끝난다. apply는 last-applied 사본을 낀 3-way merge로 필드 삭제까지 추적하며, server-side apply는 같은 문제를 field manager라는 필드 단위 소유권으로 다시 정리한 방식이다.