영어로 배우는 Cluster API: KubeVirt 위에 클러스터 찍어 내기
Your first workload cluster, and its first bug
목표
관리 클러스터 위에 워크로드 클러스터 capi-demo(컨트롤 플레인 1·워커 1)를 선언으로 세웁니다. 그 과정에서 실제로 나는 비호환 하나를 로그에서 찾아 고치고,
Calico 를 올려 노드를 Ready 로 만든 뒤, 그 실패를 영어 이슈로 씁니다.
왜 중요한가
Cluster API 의 약속은 "선언하면 맞춰 준다" 이지만, 선언이 거부되면 컨트롤러는 같은 오류를 로그에 반복해서 남길 뿐 화면은 조용합니다. Machine 이 안 생길 때 어느 층(MachineDeployment, MachineSet, Machine, 인프라, 부트스트랩)에서 멈췄는지 내려가며 보는 법이 Cluster API 운영의 대부분입니다. 그리고 그렇게 찾은 것을 남이 재현할 수 있게 영어로 적는 것이 오픈소스 기여의 첫걸음입니다. 이 실습의 버그는 지어낸 것이 아니라 이 코스를 만들며 실제로 만난 것입니다.
🏓 티티: 오늘의 표현은 "should have at least 1 properties" 예요. 영어로는 어색한 복수형이지만, 스키마 검증기가 찍는 문장 그대로 검색해야 같은 이슈를 찾을 수 있어요.
이 실습은 이미지 받기(실측 1분에서 4분 반)와 클러스터 생성·CNI(약 3분)를 기다립니다. 세션이 60분이니 필요하면 연장하세요. VM 이 끝나면 클러스터도 사라집니다.
단계
- 워크로드 클러스터의 노드가 될 디스크 이미지
quay.io/capk/ubuntu-2404-container-disk:v1.34.1(압축 약 2GiB)를 관리 클러스터의 containerd(k3s ctr -n k8s.io)에 미리 받으세요. 실측 60초에서 4분 반까지 걸렸으니(quay.io 속도에 따라 다릅니다)systemd-run으로 띄워 두고 다음 단계로 넘어가도 됩니다. 이미지 목록에 그 이름이 보이면 통과입니다. - 1모듈과 같은 값(이름
capi-demo, 네임스페이스default, 쿠버네티스v1.34.1, 컨트롤 플레인 1대·워커 1대, 노드 이미지quay.io/capk/ubuntu-2404-container-disk:v1.34.1,CRI_PATH=/var/run/containerd/containerd.sock)으로 매니페스트를 만들어/root/capi/m2/capi-demo.yaml에 저장하고, 두 KubevirtMachineTemplate 의memory.guest를 컨트롤 플레인2Gi, 워커1Gi로 바꾸세요. 아직 적용하지 않습니다. /root/capi/m2/capi-demo.yaml을 관리 클러스터에 적용하고, 컨트롤 플레인 Machine 이Running이 되는 것을 지켜보세요.kubectl get machines -w로 phase 가 어떤 순서로 바뀌는지 눈으로 확인합니다(실측 약 70초).- 컨트롤 플레인은 떴는데 워커 Machine 은 몇 분이 지나도 생기지 않습니다. 어느 컨트롤러가 무엇을 거부하는지 찾아 그 오류 줄을
/root/capi/m2/worker-error.txt에 붙이세요. 파일에는 오류를 낸 컨트롤러의 종류(controllerKind), 거부된 필드의 경로, 검증 메시지가 그대로 들어 있어야 합니다. - 워커가 생기도록 고치세요. 고칠 대상은 Machine 이 아니라 MachineSet 이 복제하는 원본, 워커용
KubeadmConfigTemplate(capi-demo-md-0)입니다.joinConfiguration.nodeRegistration이 비어 있지 않게 컨트롤 플레인 쪽과 같은criSocket: /var/run/containerd/containerd.sock을 적으면 됩니다. 워커 Machine 이Running이 되면 통과입니다(실측 약 1분). - 워크로드 클러스터의 kubeconfig 를
/root/capi/m2/capi-demo.kubeconfig에 받고, 그 클러스터에 Calicov3.32.2의manifests/calico.yaml을 파드 대역10.243.0.0/16로 맞춰 설치하세요. 노드 둘이 모두Ready가 되면 통과입니다(실측 약 70초). - 4단계의 실패를 업스트림 저장소에 올린다고 생각하고 영어 이슈 초안을
/root/capi/m2/issue.md에 쓰세요. 문법이 아니라 정보를 채점합니다. 반드시 들어갈 것: 첫 줄은#로 시작하는 제목(100자 이하, 어느 오브젝트의 문제인지KubeadmConfigTemplate이나nodeRegistration을 넣어서),What happened,What did you expect to happen,How to reproduce,Environment네 소제목, 재현 명령clusterctl generate cluster, 오류 문구at least 1 properties, 판 셋v1.14.3·v0.11.2·v1.34.1. 영어로(라틴 문자 90% 이상), 60단어 이상.
참고
- 관리 클러스터에는 CAPI v1.14.3 과 CAPK v0.11.2 이 이미 init 되어 있습니다(
kubectl get providers -A). 작업 자리는/root/capi/m2입니다. - 관리 클러스터의 kubectl 은 기본 kubeconfig 를, 워크로드 클러스터는
--kubeconfig /root/capi/m2/capi-demo.kubeconfig를 씁니다. 두 클러스터를 헷갈리면 엉뚱한 곳에 Calico 를 깔게 됩니다. - 한눈에 보기:
clusterctl describe cluster capi-demo --show-conditions all이 Cluster 부터 Machine 까지의 조건을 나무 모양으로 보여 줍니다. - 흔한 실수: 워커가 안 생긴다고 Machine 이나 KubeadmConfig 를 직접 고치는 것. 그 둘은 MachineSet 이 템플릿에서 복제한 결과라서, 원본 템플릿을 고쳐야 다음 복제가 바뀝니다.
- 문서: CAPK quick start · Machine phases · Calico manifest install
Pull the node image first
워크로드 클러스터의 노드가 될 디스크 이미지 quay.io/capk/ubuntu-2404-container-disk:v1.34.1(압축 약 2GiB)를 관리 클러스터의 containerd(k3s ctr -n k8s.io)에 미리 받으세요. 실측 60초에서 4분 반까지 걸렸으니(quay.io 속도에 따라 다릅니다) systemd-run 으로 띄워 두고 다음 단계로 넘어가도 됩니다. 이미지 목록에 그 이름이 보이면 통과입니다.
KubeVirt 의 containerDisk 는 VM 디스크를 OCI 이미지로 싣는 방식이라, 보통의 컨테이너 이미지처럼 containerd 가 받습니다. 미리 받아 두면 Machine 이 Provisioning 에 머무는 시간에서 다운로드가 빠져, 무엇이 느린지 나눠 볼 수 있습니다. systemd-run --unit=capk-pull k3s ctr -n k8s.io images pull <이미지> 처럼 띄우면 터미널이 끊겨도 계속 받습니다.
Generate and right-size the manifest
1모듈과 같은 값(이름 capi-demo, 네임스페이스 default, 쿠버네티스 v1.34.1, 컨트롤 플레인 1대·워커 1대, 노드 이미지 quay.io/capk/ubuntu-2404-container-disk:v1.34.1, CRI_PATH=/var/run/containerd/containerd.sock)으로 매니페스트를 만들어 /root/capi/m2/capi-demo.yaml 에 저장하고, 두 KubevirtMachineTemplate 의 memory.guest 를 컨트롤 플레인 2Gi, 워커 1Gi 로 바꾸세요. 아직 적용하지 않습니다.
템플릿 기본값은 노드마다 4Gi 입니다. virt-launcher 파드는 guest 메모리에 오버헤드를 더해 요청하므로(실측 4Gi → 4372Mi) 8 GiB VM 에는 그런 노드가 둘 들어가지 않습니다. kubeadm 사전 검사는 컨트롤 플레인에 1700MB 이상을 요구하니 컨트롤 플레인은 2Gi 로 둡니다. clusterctl 이 다시 써 낸 YAML 에서는 값에 따옴표가 없을 수 있으니 sed 보다는 편집기나 YAML 을 읽는 도구(python3 의 yaml 모듈)로 바꾸는 편이 안전합니다.
Apply and watch the control plane
/root/capi/m2/capi-demo.yaml 을 관리 클러스터에 적용하고, 컨트롤 플레인 Machine 이 Running 이 되는 것을 지켜보세요. kubectl get machines -w 로 phase 가 어떤 순서로 바뀌는지 눈으로 확인합니다(실측 약 70초).
Machine 의 phase 는 Pending → Provisioning → Provisioned → Running 순으로 바뀝니다. Provisioning 은 인프라(여기서는 KubeVirt VM)를 만드는 중, Provisioned 는 VM 은 있는데 아직 노드로 등록되지 않은 상태, Running 은 노드와 연결된 상태입니다. 적용할 때 ... is deprecated; use ... v1beta2 ... 경고가 나오는데, 템플릿이 옛 API 판으로 적혀 있어서 나는 경고이고 적용은 됩니다.
Where is my worker?
컨트롤 플레인은 떴는데 워커 Machine 은 몇 분이 지나도 생기지 않습니다. 어느 컨트롤러가 무엇을 거부하는지 찾아 그 오류 줄을 /root/capi/m2/worker-error.txt 에 붙이세요. 파일에는 오류를 낸 컨트롤러의 종류(controllerKind), 거부된 필드의 경로, 검증 메시지가 그대로 들어 있어야 합니다.
MachineDeployment → MachineSet → Machine 순으로 내려가 보세요. 위 오브젝트는 있는데 Machine 이 없다면, MachineSet 이 Machine 을 만들다 실패한 것입니다. core 컨트롤러(capi-system 네임스페이스)의 로그에서 Reconciler error 를 찾아보세요. 영어 오류 메시지는 '무엇이(field) 왜(should ...) 틀렸는지' 를 한 줄에 담습니다.
Fix the template, not the Machine
워커가 생기도록 고치세요. 고칠 대상은 Machine 이 아니라 MachineSet 이 복제하는 원본, 워커용 KubeadmConfigTemplate(capi-demo-md-0)입니다. joinConfiguration.nodeRegistration 이 비어 있지 않게 컨트롤 플레인 쪽과 같은 criSocket: /var/run/containerd/containerd.sock 을 적으면 됩니다. 워커 Machine 이 Running 이 되면 통과입니다(실측 약 1분).
템플릿에는 kubeletExtraArgs: {} 한 줄뿐이었습니다. 이것이 새 API 판(v1beta2)으로 바뀌면서 빈 객체 nodeRegistration: {} 가 되고, v1beta2 의 KubeadmConfig 스키마는 그 자리에 속성이 하나 이상 있어야 한다고 거부합니다. kubectl edit 이나 kubectl patch --type merge 로 템플릿을 고치면 MachineSet 이 다음 reconcile 에서 고친 템플릿으로 다시 시도합니다.
Bring the nodes to Ready
워크로드 클러스터의 kubeconfig 를 /root/capi/m2/capi-demo.kubeconfig 에 받고, 그 클러스터에 Calico v3.32.2 의 manifests/calico.yaml 을 파드 대역 10.243.0.0/16 로 맞춰 설치하세요. 노드 둘이 모두 Ready 가 되면 통과입니다(실측 약 70초).
Machine 이 Running 이어도 노드는 NotReady 입니다. 조건 메시지에 cni plugin not initialized 가 보이는데, 파드 네트워크(CNI)를 아직 아무도 깔지 않았다는 뜻입니다. Cluster API 는 CNI 를 깔아 주지 않습니다. clusterctl get kubeconfig 로 kubeconfig 를 받고, calico.yaml 에서 주석 처리된 CALICO_IPV4POOL_CIDR 를 풀어 클러스터 선언의 파드 대역과 맞춥니다. kubectl 을 부를 때 --kubeconfig 로 어느 클러스터에 거는지 늘 확인하세요.
File the bug in English
4단계의 실패를 업스트림 저장소에 올린다고 생각하고 영어 이슈 초안을 /root/capi/m2/issue.md 에 쓰세요. 문법이 아니라 정보를 채점합니다. 반드시 들어갈 것: 첫 줄은 # 로 시작하는 제목(100자 이하, 어느 오브젝트의 문제인지 KubeadmConfigTemplate 이나 nodeRegistration 을 넣어서), What happened, What did you expect to happen, How to reproduce, Environment 네 소제목, 재현 명령 clusterctl generate cluster, 오류 문구 at least 1 properties, 판 셋 v1.14.3·v0.11.2·v1.34.1. 영어로(라틴 문자 90% 이상), 60단어 이상.
메인테이너가 이슈에서 가장 먼저 찾는 것은 '무엇이 일어났고 무엇을 기대했는가' 와 '내 자리에서 똑같이 만들 수 있는가' 입니다. 제목에는 증상과 대상을 넣고(예: 무엇이 무엇 때문에 거부된다), 'It doesn't work' 같은 말은 피합니다. 재현 단계는 번호를 붙여 명령 그대로 적고, 판은 '최신' 이 아니라 숫자로 적습니다. 블로그 「오늘의 영어 표현」의 「Expected vs Actual」 편(/blog/english-daily-02-expected-vs-actual)이 같은 틀을 다룹니다.