LabHub
Get started
배우기 러닝패스 코스

Building clusters with Kubespray and Terraform

Terraform builds the inventory, calls Kubespray, then deploys on top

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

같은 VM 안에서 OpenTofu 가 kubespray 인벤토리를 템플릿으로 만들고, kubespray 실행을 감싸 클러스터를 세우고, 세운 클러스터에 kubernetes·helm 프로바이더로 네임스페이스·RBAC·애플리케이션을 선언합니다. 밖에서 바뀐 것을 plan 으로 잡아 되돌립니다.

왜 중요한가

현장의 클러스터 수명 주기는 보통 세 층입니다 — 노드를 만드는 층(클라우드·가상화), 노드 위에 쿠버네티스를 까는 층(kubespray), 클러스터 위에 팀의 공간과 권한과 앱을 까는 층. Terraform 은 첫 층과 셋째 층에 강하고, 둘째 층은 Ansible 에 맡기는 것이 일반적입니다. 이 실습은 그 경계를 한 VM 안에서 이어 붙여 봅니다: 무엇을 Terraform 의 state 로 두고, 무엇을 kubespray 의 멱등성에 맡기고, 선언과 실제가 어긋났을 때 누가 그것을 알아채는가. 노드를 만드는 첫 층은 이 VM 에서 하지 않습니다 — 이 플랫폼의 가상화 API 를 실습에서 부르게 하면 격리가 깨지기 때문입니다. 설치를 포함해 약 20분을 기다립니다.

단계

  1. /root/ks/tf/cluster/main.tfhashicorp/local 프로바이더로 세 가지 local_file 을 선언하세요 — inventory(/root/ks/inventory/lab/inventory.ini/root/ks/tf/cluster/inventory.tftpl 템플릿과 nodes 변수로), host_vars(노드마다 /root/ks/inventory/lab/host_vars/<노드>.ymlansible_connection), version(/root/ks/inventory/lab/group_vars/k8s_cluster/zz-terraform.ymlkube_version: <변수>). kube_version 변수의 기본값은 1.35.8, nodes 의 기본값은 node1 한 대(컨트롤 플레인·워커 겸, local 연결)입니다. tofu validate 가 통과해야 합니다.
  2. /root/ks/tf/cluster 에서 tofu apply 로 세 파일을 만드세요. 만들어진 /root/ks/inventory/lab/inventory.ini 이 state 의 local_file.inventory 내용과 같아야 하고, ansible-inventory 로 읽었을 때 node1 이 kube_control_plane·etcd·kube_node 에 있으며 kube_version 이 1.35.8 으로 풀려야 합니다.
  3. /root/ks/tf/cluster/main.tfterraform_data "kubespray" 를 더해, 인벤토리·판·host_vars 파일 내용이 바뀔 때만 다시 만들어지게(triggers_replace) 하고, 만들어질 때 local-exec/opt/ks/kubespray 에서 ansible-playbook -i /root/ks/inventory/lab/inventory.ini cluster.yml 을 돌려 출력을 /root/ks/logs/tf-cluster.log 에 남기게 하세요(HOME=/root 를 넘깁니다). tofu apply 로 설치까지 끝나면(약 7분) state 에 terraform_data.kubespray 가 있고, 로그의 PLAY RECAP 이 failed=0, node1 이 Ready 여야 합니다.
  4. /root/ks/tf/cluster 에서 tofu plan -detailed-exitcode 를 그대로 한 번, -var kube_version=1.36.4 를 붙여 한 번 돌려 보세요(적용하지 않습니다). /root/ks/tf/plan.jsonsteady_exit(첫 plan 의 종료 코드), bump_exit(두 번째의 종료 코드), bump_replaces(두 번째 plan 이 바꾸거나 다시 만들겠다는 리소스 주소의 정렬 배열)를 적으세요.
  5. /root/ks/tf/apps/main.tfhashicorp/kubernetes(3.2.1)·hashicorp/helm(3.3.0) 프로바이더를 /root/.kube/config 로 설정하고, 네임스페이스 team-a(라벨 owner=platform), 서비스어카운트 deployer, deployments 를 만들고 고칠 수 있고 pods·services 는 읽기만 하는 Role deployer 와 그 RoleBinding, 그리고 로컬 차트 /opt/ks/charts/helloteam-a 에 레플리카 2 로 까는 helm_release "hello" 를 선언해 tofu apply 하세요. deployer 는 team-a 에서 deployments 를 만들 수 있고 노드는 지울 수 없어야 하며, hello 의 파드 두 개가 Ready 여야 합니다.
  6. kubectl label ns team-a owner=someone-else --overwrite 로 네임스페이스 라벨을 밖에서 바꾼 뒤, /root/ks/tf/apps 에서 tofu plan -detailed-exitcode 로 차이를 잡아 출력을 /root/ks/tf/drift-plan.txt 에 남기세요. /root/ks/tf/drift.jsonexit_code(그 plan 의 종료 코드), drifted(바꾸겠다고 나온 리소스 주소의 정렬 배열)를 적습니다. 아직 적용하지 않습니다.
  7. /root/ks/tf/apps 에서 tofu apply 로 차이를 되돌리세요. 끝나면 team-a 의 owner 라벨이 platform 이고, tofu plan -detailed-exitcode 가 0 이어야 합니다. 그리고 /root/ks/tf/cluster 의 plan 도 0 이어야 합니다(클러스터 쪽 선언도 그대로).

참고

인벤토리를 템플릿으로 만든다

/root/ks/tf/cluster/main.tfhashicorp/local 프로바이더로 세 가지 local_file 을 선언하세요 — inventory(/root/ks/inventory/lab/inventory.ini/root/ks/tf/cluster/inventory.tftpl 템플릿과 nodes 변수로), host_vars(노드마다 /root/ks/inventory/lab/host_vars/<노드>.ymlansible_connection), version(/root/ks/inventory/lab/group_vars/k8s_cluster/zz-terraform.ymlkube_version: <변수>). kube_version 변수의 기본값은 1.35.8, nodes 의 기본값은 node1 한 대(컨트롤 플레인·워커 겸, local 연결)입니다. tofu validate 가 통과해야 합니다.

templatefile() 은 %{ for } … %{ endfor } 지시문으로 반복하고, ~ 는 줄바꿈을 먹습니다. 노드의 역할을 변수(맵)에 두면 노드를 늘릴 때 맵에 한 줄을 더하는 것으로 인벤토리·host_vars 가 함께 바뀝니다. 조리법이 k8s-cluster.yml 에 적어 두던 판 줄은 지워 두었습니다 — 판의 주인을 Terraform 하나로 두기 위해서입니다. OpenTofu 는 terraform 이라는 이름으로도 부를 수 있습니다.

파일만 먼저 apply

/root/ks/tf/cluster 에서 tofu apply 로 세 파일을 만드세요. 만들어진 /root/ks/inventory/lab/inventory.ini 이 state 의 local_file.inventory 내용과 같아야 하고, ansible-inventory 로 읽었을 때 node1 이 kube_control_plane·etcd·kube_node 에 있으며 kube_version 이 1.35.8 으로 풀려야 합니다.

state 에는 Terraform 이 만든 파일의 내용이 그대로 들어 있습니다. 누가 inventory.ini 를 손으로 고치면 다음 plan 이 그것을 되돌리겠다고 나옵니다 — 이 파일의 주인이 이제 Terraform 이라는 뜻입니다. tofu state show local_file.inventory 로 내용을 볼 수 있습니다.

terraform_data 로 kubespray 를 부른다

/root/ks/tf/cluster/main.tfterraform_data "kubespray" 를 더해, 인벤토리·판·host_vars 파일 내용이 바뀔 때만 다시 만들어지게(triggers_replace) 하고, 만들어질 때 local-exec/opt/ks/kubespray 에서 ansible-playbook -i /root/ks/inventory/lab/inventory.ini cluster.yml 을 돌려 출력을 /root/ks/logs/tf-cluster.log 에 남기게 하세요(HOME=/root 를 넘깁니다). tofu apply 로 설치까지 끝나면(약 7분) state 에 terraform_data.kubespray 가 있고, 로그의 PLAY RECAP 이 failed=0, node1 이 Ready 여야 합니다.

local-exec 는 Terraform 을 돌리는 곳(이 VM)에서 명령을 실행합니다. 명령이 실패하면 리소스가 tainted 로 남아 다음 apply 가 다시 부릅니다. kubespray 의 kube 모듈은 HOME 이 비면 kubeconfig 를 못 찾으므로 environment 로 넘깁니다. apply 가 7분 동안 터미널을 붙잡으니 systemd-run 이나 tmux 로 띄우는 편이 안전합니다.

바뀐 것이 없으면 아무것도 안 한다 — 판을 바꾸면?

/root/ks/tf/cluster 에서 tofu plan -detailed-exitcode 를 그대로 한 번, -var kube_version=1.36.4 를 붙여 한 번 돌려 보세요(적용하지 않습니다). /root/ks/tf/plan.jsonsteady_exit(첫 plan 의 종료 코드), bump_exit(두 번째의 종료 코드), bump_replaces(두 번째 plan 이 바꾸거나 다시 만들겠다는 리소스 주소의 정렬 배열)를 적으세요.

-detailed-exitcode 는 바뀔 것이 없으면 0, 있으면 2 를 돌려줍니다. 판을 바꾸면 무엇이 다시 만들어지는지 보세요 — terraform_data 가 다시 만들어지면 cluster.yml 이 다시 불립니다. kubespray 의 업그레이드는 cluster.yml 이 아니라 upgrade-cluster.yml 입니다. Terraform 은 그 차이를 모릅니다. tofu plan -json 의 resource_changes 가 주소와 동작(update·replace)을 알려 줍니다.

세운 클러스터에 선언으로 배포한다

/root/ks/tf/apps/main.tfhashicorp/kubernetes(3.2.1)·hashicorp/helm(3.3.0) 프로바이더를 /root/.kube/config 로 설정하고, 네임스페이스 team-a(라벨 owner=platform), 서비스어카운트 deployer, deployments 를 만들고 고칠 수 있고 pods·services 는 읽기만 하는 Role deployer 와 그 RoleBinding, 그리고 로컬 차트 /opt/ks/charts/helloteam-a 에 레플리카 2 로 까는 helm_release "hello" 를 선언해 tofu apply 하세요. deployer 는 team-a 에서 deployments 를 만들 수 있고 노드는 지울 수 없어야 하며, hello 의 파드 두 개가 Ready 여야 합니다.

클러스터를 세우는 루트 모듈과 그 위에 배포하는 루트 모듈을 나누는 이유가 있습니다 — 한 모듈에서 클러스터를 만들고 같은 apply 에서 그 클러스터로 프로바이더를 설정하면, 첫 plan 때 아직 없는 kubeconfig 를 읽어야 합니다. helm 프로바이더 3.x 에서는 kubernetes 설정이 블록이 아니라 kubernetes = {{ ... }} 속성이고 set 도 목록 속성입니다. 권한은 kubectl auth can-i ... --as=system:serviceaccount:team-a:deployer 로 확인합니다.

누가 밖에서 바꿨다

kubectl label ns team-a owner=someone-else --overwrite 로 네임스페이스 라벨을 밖에서 바꾼 뒤, /root/ks/tf/apps 에서 tofu plan -detailed-exitcode 로 차이를 잡아 출력을 /root/ks/tf/drift-plan.txt 에 남기세요. /root/ks/tf/drift.jsonexit_code(그 plan 의 종료 코드), drifted(바꾸겠다고 나온 리소스 주소의 정렬 배열)를 적습니다. 아직 적용하지 않습니다.

plan 은 먼저 실제 상태를 읽어(refresh) state 와 비교하고, 그다음 선언과 비교합니다. 밖에서 바뀐 것이 선언과 다르면 선언대로 되돌리겠다는 계획이 나옵니다. 이것이 drift 감지이고, 정기적으로 plan 을 돌려 종료 코드 2 를 알림으로 거는 것이 흔한 운영 방식입니다.

선언으로 되돌린다

/root/ks/tf/apps 에서 tofu apply 로 차이를 되돌리세요. 끝나면 team-a 의 owner 라벨이 platform 이고, tofu plan -detailed-exitcode 가 0 이어야 합니다. 그리고 /root/ks/tf/cluster 의 plan 도 0 이어야 합니다(클러스터 쪽 선언도 그대로).

apply 는 plan 과 같은 비교를 한 번 더 하고 차이를 선언대로 맞춥니다. drift 를 되돌릴지, 아니면 밖에서 바꾼 것이 맞아 선언을 고칠지는 사람이 판단할 몫입니다 — 이번에는 선언이 맞다고 보고 되돌립니다.