Setting up k3s on Hetzner Cloud

Ellie Huxtable

Hetzner Cloud에 k3s 구축하기

최근에 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를 몇 번 직접 운영해 본 적이 있기 때문입니다. 다만 kubeadm으로 했었지 k3s로는 아니었습니다.

서버

첫 번째 서버입니다! 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

참고하세요:

  1. 네트워크 인터페이스가 올바른지 확인하세요
  2. 토큰을 안전하게 생성하세요
  3. cloud-provider=external이 여전히 필요한지 확인하세요. Kubernetes v1.29 이상에서는 필요하지 않을 수도 있습니다.
  4. “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 status
Encryption 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에서 가져왔습니다):

  1. node.kubernetes.io/instance-type 라벨에 서버 타입을 추가하고, 외부 IPv4 및 IPv6 주소를 설정하며, Hetzner Cloud에서 삭제된 노드를 Kubernetes에서도 삭제합니다.
  2. 노드에 topology.kubernetes.io/regiontopology.kubernetes.io/zone 라벨을 설정하여 Kubernetes가 서버의 장애 도메인을 인식하도록 합니다.
  3. 파드 트래픽에 Hetzner Cloud 프라이빗 네트워크를 사용할 수 있습니다.
  4. Kubernetes Service와 함께 Hetzner Cloud 로드 밸런서를 사용할 수 있습니다

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 NAME
helm install hccm hcloud/hcloud-cloud-controller-manager -n kube-system --set networking.enabled=true --set networking.clusterCIDR=10.42.0.0/16

clusterCIDR 설정을 꼭 확인하세요. 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 nodes
NAME       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+k3s1

Traefik으로 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도 설정하겠지만, 클러스터를 최대한 스테이트리스하게 유지하려고 합니다. 게다가 엣지에서 SSL을 종료하는 것이 편하고 좋기도 합니다.

다음 단계

이제 Cloudflare로 DNS를 연결해 스테이트리스 서비스를 배포할 수 있는 단계까지 왔습니다!

지금도 꽤 잘 동작하고 있지만, 앞으로 하고 싶은 것들이 많이 있습니다

  1. Longhorn으로 스토리지 설정하기, 지금은 스토리지가 없습니다
  2. 모니터링 설정
  3. 자동화된 에이전트 설정을 위한 cloud-init
  4. 간편한 VPN 접속
  5. 추가 보안 강화

원문은 Ellie Huxtable님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.