Hetzner Cloud에 k3s 구축하기
원문은 Ellie Huxtable님이 에 게재했습니다. 이 블로그 구독하기
최근에 Atuin을 위해 HA k3s 클러스터를 구축했습니다!
HA etcd를 사용하고 있는데, 그러려면 홀수 개의 “server” 노드를 실행해야 하고 당연히 한 개보다 많아야 합니다. 그래서 최소 개수는 3개가 됩니다.
모든 서버는 프라이빗 네트워크 안의 자체 서브넷에 구성되어 있으며, ARM 인스턴스입니다. 방화벽은 거의 모든 인바운드와 대부분의 아웃바운드를 차단하도록 설정되어 있습니다.
솔직히 이렇게 쉬웠다는 점에 놀랐습니다. 제가 모르는 사이에 뭔가를 잘못했을 수도 있지만, 몇 년 전 kubeadm을 사용했을 때의 경험은 전혀 이렇게 좋지 않았습니다. k3s 만세!
출처
여러 자료를 많이 읽었습니다.
시작하기에 괜찮았던 가이드는 이겁니다: https://community.hetzner.com/tutorials/k3s-glusterfs-loadbalancer
하지만 k3s 문서가 정말 잘 되어 있어서 대부분 그걸 읽었습니다: https://docs.k3s.io/
자동화된 Hetzner k3s 설정 툴(아마도 매우 훌륭한) 중 하나는 사용하지 않았습니다. 무슨 일이 일어나고 있는지 제대로 이해하고 싶었고, 과거에 베어메탈에서 Kubernetes를 몇 번 운영해 본 적이 있습니다. 다만 k3s가 아니라 kubeadm으로요.
서버
첫 번째 서버입니다! 참고로 k3s에서는 컨트롤 플레인을 실행하는 노드를 “server”, 다른 노드를 “agent”라고 부릅니다. 기본적으로 마스터 노드에서도 일반 워크로드를 스케줄링할 수 있게 되어 있는데, 제 사용 사례에는 아마 괜찮을 겁니다.
특별한 Hetzner용 컨트롤러를 설치할 예정이라 클라우드 컨트롤러는 비활성화했고, Longhorn을 사용할 예정이라 로컬 스토리지도 비활성화했습니다. 꼭 좋은 토큰을 선택하세요!
그 외에는 Hetzner에서 프라이빗 네트워킹을 활성화했고 클러스터가 이를 사용하길 원하기 때문에, flannel이 프라이빗 네트워크 인터페이스를 바라보도록 설정했습니다.
curl -sfL https://get.k3s.io | sh -s - server \
--cluster-init \
--disable-cloud-controller \
--disable local-storage \
--node-name="$(hostname -f)" \
--flannel-iface=enp7s0 \
--kubelet-arg="cloud-provider=external" \
--secrets-encryption \
--disable=traefik \
--token=CHANGE ME이후 머신들에서는 매우 비슷한 명령어를 실행합니다
curl -sfL https://get.k3s.io | sh -s - server \
--server SERVER ADDRESS \
--disable-cloud-controller \
--disable local-storage \
--node-name="$(hostname -f)" \
--flannel-iface=enp7s0 \
--kubelet-arg="cloud-provider=external" \
--secrets-encryption \
--disable=traefik \
--token=CHANGE ME참고하세요:
- 네트워크 인터페이스가 올바른지 확인하세요
- 토큰을 안전하게 생성하세요
- cloud-provider=external이 여전히 필요한지 확인하세요. Kubernetes v1.29+에서는 그렇지 않을 수도 있습니다.
- “cluster init”은 첫 번째 서버 설정에만 필요합니다. 그 이후에는 이미 클러스터가 있으므로 더 이상 init이 필요 없습니다!
처음에는 Hetzner Cloud Controller 설정까지 다 마친 뒤에야 문서에서 각 노드에
--kubelet-arg="cloud-provider=external"플래그를 전달해야 한다고 적힌 걸 봤습니다.대부분의 플래그는 그냥 인스톨러를 다시 실행하면 설정이 조정되고 노드가 재시작됩니다. 하지만 이 플래그만큼은 놓치면 클러스터를 다시 설정해야 합니다. HCCM은 처음부터 올바르게 설정된 노드에만 레이블을 붙이며, 레이블이 없는 노드는 LB와 제대로 동작하지 않습니다.
이에 대한 배경을 조금 설명하자면, Kubernetes에는 k8s를 클라우드 프로바이더와 잘 연동해 주는 여러 CCM(cloud controller manager)이 있습니다. 외부 CCM을 설치하려면 현재 앞서 언급한 플래그를 설정해야 합니다. 다만 이 플래그는 꽤 오래전부터 deprecated 상태입니다. 원래는 v1.24에서 제거될 예정이었지만, 아직 그렇게 되지 않았습니다.
제 이해로는 현재 kubelet에 일부 CCM이 번들로 포함되어 있어서, 이 플래그를 사용하면 번들에 포함되지 않은 CCM을 사용할 수 있게 됩니다. 향후 계획은 CCM을 더 이상 번들로 제공하지 않는 것이며, 그렇게 되면 이 인자는 불필요해집니다(그래서 deprecated된 것입니다)
더 자세히 알고 싶다면 이슈를 참고하세요: https://github.com/kubernetes/kubernetes/issues/110018
링크된 PR에 따르면 이 변경은 v1.29에 포함될 수도 있습니다. 따라서 v1.29+를 실행 중이라면 cloud provider 플래그가 필요하지 않을 수도 있습니다!
이제 다음을 실행할 수 있습니다
kubectl get nodes설정한 머신 중 아무 곳에서나 실행하면 다음과 같은 결과가 나옵니다:
NAME STATUS ROLES AGE VERSION
server-1 Ready control-plane,etcd,master 4m4s v1.27.6+k3s1
server-2 Ready control-plane,etcd,master 47s v1.27.6+k3s1
server-3 Ready control-plane,etcd,master 19s v1.27.6+k3s1그리고 시크릿 암호화 상태도 다음 명령어로 확인할 수 있습니다
k3s secrets-encrypt statusEncryption Status: Enabled
Current Rotation Stage: start
Server Encryption Hashes: All hashes match
Active Key Type Name
------ -------- ----
* AES-CBC aescbckey좋네요!
접속
추가 설정을 하기 전에 노트북에서 kubectl 접속을 설정하고 싶었습니다. 노드 자체에서 직접 명령어를 실행하는 건 영 내키지 않았습니다.
설정이 완전히 끝나면 접속용으로 Tailscale(또는 innernet)을 설정할 예정이지만, 지금은 그냥 SSH 포트 포워딩을 사용하겠습니다. kubeconfig는 다음 명령어로 가져올 수 있습니다
cat /etc/rancher/k3s/k3s.yaml노드 중 하나에서 실행하세요.
간단히
ssh -L 6443:localhost:6443 root@a server ip실행하면 로컬 기기에서 kubectl을 사용할 수 있습니다. 다만 조금 더 견고한 방법을 꼭 설정하세요 😊
Hetzner Cloud Controller Manager
빨리 세 번 말해보세요. 어쨌든 hccm은 우리 클러스터를 Hetzner Cloud API와 연동해 주며, 덕분에 다음을 할 수 있습니다(README에서 가져왔습니다):
- 서버 타입을
node.kubernetes.io/instance-type레이블에 추가하고, 외부 IPv4 및 IPv6 주소를 설정하며, Hetzner Cloud에서 삭제된 노드를 Kubernetes에서도 삭제합니다. topology.kubernetes.io/region및topology.kubernetes.io/zone레이블을 노드에 설정하여 Kubernetes가 서버의 failure domain을 인식하게 합니다.- Pod 트래픽에 Hetzner Cloud Private Network를 사용할 수 있게 합니다.
- Kubernetes Service와 함께 Hetzner Cloud Load Balancer를 사용할 수 있게 합니다.
Hetzner 블로그 포스트에서는 그냥 매니페스트를 적용하라고 권장했지만, 하지만 hccm 문서에서는 Helm 차트를 권장합니다. 저는 프라이빗 네트워킹을 활성화한 상태로 설정했습니다(이걸 퍼블릭 네트워크에서 실행할 이유가 있을까요, 잘 모르겠네요, 아마 하지 마세요?)
helm repo add hcloud https://charts.hetzner.cloud
helm repo update hcloud그 다음 Hetzner Cloud API 토큰과 네트워크 이름이 담긴 k8s 시크릿을 설정해야 합니다(이것이 제가 시크릿이 저장 시 암호화되도록 하고 싶었던 이유 중 하나입니다)
kubectl -n kube-system create secret generic hcloud --from-literal=token=SOME SECRET --from-literal=network=NETWORK NAMEhelm install hccm hcloud/hcloud-cloud-controller-manager -n kube-system --set networking.enabled=true --set networking.clusterCIDR=10.42.0.0/16clusterCIDR 설정에 유의하세요. k3s 기본값을 변경하지 않았다면 10.42.0.0/16을 사용하면 됩니다.
kubectl logs -n kube-system deployment/hcloud-cloud-controller-manager이제 다음과 같은 출력이 보여야 하며,
kubectl describe node agent-1다음과 같은 추가 정보 어노테이션이 표시되어야 합니다:
node.kubernetes.io/instance-type=cax21
topology.kubernetes.io/region=fsn1
topology.kubernetes.io/zone=fsn1-dc14에이전트
k3s의 기본 동작은 실제로 모든 디플로이먼트를 노드에 스케줄링하므로, (이 동작이 괜찮다면) 이런 에이전트가 많이 필요하지 않을 수도 있습니다
설정은 서버와 꽤 비슷합니다! 그냥 설정이 조금 더 적을 뿐입니다. 이전에 사용한 토큰도 필요합니다
curl -sfL https://get.k3s.io | sh -s - agent \
--server SERVER ADDRESS \
--node-name="$(hostname -f)" \
--flannel-iface=enp7s0 \
--kubelet-arg="cloud-provider=external" \
--token=CHANGE ME원하는 만큼 이런 에이전트를 설정할 수 있습니다
kubectl get nodesNAME STATUS ROLES AGE VERSION
agent-1 Ready <none> 9s v1.27.6+k3s1
server-1 Ready control-plane,etcd,master 100m v1.27.6+k3s1
server-2 Ready control-plane,etcd,master 96m v1.27.6+k3s1
server-3 Ready control-plane,etcd,master 96m v1.27.6+k3s1Traefik으로 Ingress 설정하기
앞서 노드를 설정할 때 Traefik을 비활성화했습니다. 그런데 지금 다시 설정한다고요?
기본적으로 k3s의 Traefik은 자체 로드 밸런서를 사용합니다. 사실 이 자체로는 아무 문제가 없지만, 저는 hccm이 제 LB를 관리하도록 하고 싶었습니다. 이렇게 하면 클러스터가 모든 타깃을 자동으로 관리하는 제대로 된 클라우드 LB를 갖게 됩니다.
동작하게 만드는 데 좀 애를 먹었습니다(아래에 자세히 설명한 설정 오류를 몇 가지 저질렀습니다)
helm repo add traefik https://traefik.github.io/charts
helm repo update그 다음 traefik.values.yaml을 만들었습니다. 전체를 여기에 붙이지는 않겠습니다 - 일반적으로 기본값을 가져와 파일로 저장한 뒤 필요에 맞게 수정합니다. 여러분도 원하는 대로 설정하는 게 좋을 겁니다!
다만 서비스에 있는 이 어노테이션들은 정말 중요하다고 말씀드리고 싶습니다:
service:
enabled: true
## -- Single service is using `MixedProtocolLBService` feature gate.
## -- When set to false, it will create two Service, one for TCP and one for UDP.
single: true
type: LoadBalancer
# -- Additional annotations applied to both TCP and UDP services (e.g. for cloud provider specific config)
annotations:
load-balancer.hetzner.cloud/location: fsn1
load-balancer.hetzner.cloud/name: lb
load-balancer.hetzner.cloud/use-private-ip: "true"먼저 LB의 위치를 설정합니다. 이름을 지정하고, 그 다음 프라이빗 IP를 사용하도록 지정합니다. 처음에는 이 옵션이 없었는데, 그래서 로드 밸런서가 동작하지 않았습니다! 모든 타깃이 unhealthy 상태였습니다.
기본적으로 hccm은 LB에 퍼블릭 IP 주소만 추가하고 있었습니다. 방화벽이 이를 차단하고 있었기 때문에(노드로의 퍼블릭 인그레스가 없음) 아무 것도 제대로 라우팅되지 않았습니다. 이 설정을 바꾸니 모든 게 잘 됐습니다 😇
저는 Cloudflare를 사용해 SSL을 종료(terminate)하고 있습니다. 언젠가는 cert-manager를 설정하겠지만, 클러스터를 가능한 한 stateless하게 유지하려고 합니다. 게다가 엣지에서 SSL을 종료하는 것이 편하고 좋기도 합니다.
다음 단계
이제 Cloudflare로 DNS를 연결해 stateless 서비스를 배포할 수 있는 단계까지 왔습니다!
아직 꽤 잘 동작하고는 있지만, 다음에 하고 싶은 것들이 많이 있습니다
- Longhorn으로 스토리지 설정, 지금은 스토리지가 전혀 없습니다
- 모니터링 설정
- 자동화된 에이전트 설정을 위한 cloud-init
- 간편한 VPN 접속
- 추가 보안 강화
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기