Setting up k3s on Hetzner Cloud

Ellie Huxtable

Hetzner Cloudでk3sをセットアップする

最近、Atuin用にHA構成のk3sクラスターをセットアップしました!

HA etcdを使っているので、「server」ノードは奇数台、かつ当然ながら複数台必要です。最小構成は3台になります。

serverはすべてプライベートネットワーク内の専用サブネットに配置しており、ARMインスタンスです。ファイアウォールは、ほぼすべての受信通信と大半の送信通信を許可しない設定にしています。

正直、こんなに簡単だったことに驚いています。気付いていないところで何か間違えているかもしれませんが、数年前にkubeadmを使ったときの体験は、これほど良いものではありませんでした。k3s最高!

参考資料

いろいろ読みました。

このガイドはよい出発点でした:https://community.hetzner.com/tutorials/k3s-glusterfs-loadbalancer

ただ、k3sのドキュメントは本当によくできていて、主にそちらを読みました:https://docs.k3s.io/

(おそらくとても良い)Hetzner向けk3s自動セットアップツールは使いませんでした。何が起きているのかをきちんと理解したかったのと、以前にKubernetesをベアメタルで何度か動かしたことがあったからです。使ったのはk3sではなく、kubeadmでしたが。

Server

最初のserverです! k3sではコントロールプレーンを実行するノードを「server」、それ以外のノードを「agent」と呼びます。デフォルトではmasterノードにも通常のワークロードをスケジュールできます。私の用途なら、おそらくこれで問題ありません。

専用のHetzner用コントローラーをインストールするためcloud controllerを無効化し、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」が必要なのは最初のserverのセットアップだけです。その後はすでにクラスターがあるので、initはもう不要です!

最初はHetzner Cloud Controllerのセットアップまで進めてから、ドキュメントに、各ノードへ--kubelet-arg="cloud-provider=external"フラグを渡す必要があると書かれているのを見つけました。

たいていのフラグはインストーラーを再実行すれば、設定を調整してノードを再起動してくれます。しかし特にこのフラグを付け忘れた場合は、クラスターを作り直す必要があります。HCCMがラベル付けするのは最初から正しくセットアップされたノードだけで、ラベルのないノードではLBが正しく動作しません。

少し背景を説明します。Kubernetesには多数のCCM(cloud controller manager)があり、基本的にはk8sをクラウドプロバイダーと適切に連携させるものです。外部CCMをインストールするには、現時点では先述のフラグを設定する必要があります。ただし、これはかなり前から非推奨です。当初はv1.24で削除される予定でしたが、まだ実現していません。

私の理解では、現在のkubeletには一部のCCMがバンドルされており、このフラグを指定するとバンドルされていないCCMを使えるようになります。将来的にはCCMがバンドルされなくなる予定なので、この引数は不要になります(そのため非推奨です)。

詳しく知りたい場合はこちらのIssueです: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

また、次のコマンドでSecret暗号化の状態も確認できます。

k3s secrets-encrypt status
Encryption Status: Enabled
Current Rotation Stage: start
Server Encryption Hashes: All hashes match

Active  Key Type  Name
------  --------  ----
 *      AES-CBC   aescbckey

いい感じです!

アクセス

これ以上セットアップを進める前に、ノートPCから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

これを素早く3回言ってみてください。さて、hccmはクラスターをHetzner Cloud APIと統合します。つまり、(READMEから拝借すると)次のことができます:

  1. server typeをnode.kubernetes.io/instance-typeラベルに追加し、外部IPv4およびIPv6アドレスを設定し、Hetzner Cloudから削除されたノードをKubernetesから削除します。
  2. ノードにtopology.kubernetes.io/regionおよびtopology.kubernetes.io/zoneラベルを設定することで、serverの障害ドメインをKubernetesに認識させます。
  3. PodのトラフィックにHetzner Cloud Private Networksを使えるようにします。
  4. 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 Secretを設定する必要があります(これもSecretが保存時に暗号化されていることを確認したかった理由の一つです)。

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

Agent

k3sのデフォルト動作では、実際にはすべてのDeploymentが全ノードにスケジュールされるため、この動作でよければagentはそれほど多く必要ないかもしれません。

セットアップはserverとかなり似ています! ただし設定は少なめです。先ほどのトークンも必要です。

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は独自のロードバランサーを使います。それ自体はまったく問題ありませんが、LBをhccmに管理させたいと思いました。これにより、適切なクラウドLBが得られ、すべてのターゲットがクラスターによって自動管理されます。

動作させるには少し試行錯誤が必要でした(後述する設定ミスをしていました)。

helm repo add traefik https://traefik.github.io/charts
helm repo update

次にtraefik.values.yamlを作成しました。ここには全体を貼りません。通常はデフォルト値を取得してファイルに保存し、必要に応じて編集します。自分が望む形で設定するのも、おそらくよい考えです!

ただし、Serviceでは次のアノテーションが非常に重要です:

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はパブリックIPアドレスだけをLBに追加していました。ファイアウォールがこれをブロックしていたため(ノードへのパブリックな直接受信通信は許可していません)、何も正しくルーティングされていませんでした。この変更後は、すべてうまくいきました 😇

SSLの終端にはCloudflareも使っています。いずれcert-managerを設定するかもしれませんが、クラスターはできるだけステートレスに保ちたいです。それに、エッジでSSLを終端するのは手軽でよいです。

次のステップ

これで、ステートレスなサービスをデプロイし、CloudflareでDNSをそれらに向けられる段階になりました!

今でもかなりうまく動いていますが、次にやりたいことがいくつかあります。

  1. 現時点ではストレージがないため、Longhornでストレージをセットアップする
  2. モニタリングをセットアップする
  3. agentを自動セットアップするためのCloud init
  4. 手軽なVPNアクセス
  5. 追加のセキュリティ強化

原文は Ellie Huxtable により に公開されました。

この記事は「gpt-5.6-terra」を使用して翻訳されました。