Managed Kubernetesサービス開発者の自宅k8sクラスタ全容
2年以上自宅で Kubernetes クラスタを構築・運用をしています。
Kubernetes クラスタはクラスタを構成する要素も様々な選択肢があり、その上で動かすアプリケーションの選択肢も非常に多くなっています。
そのためとりあえずクラスタを構築しようと思い立っても実際にクラスタとして動かすまで多くの要素について調べる必要がありなかなか大変です。
また、クラスタの全体像を説明する実例というものも少なく、一体何を用意したらクラスタとして稼働できるのか・どういうところに問題が出るのかということを理解できるまでに時間がかかります。
そこでこの記事では自分が構築・運用しているクラスタについて全体像から詳細までを説明します。
なお自分は普段業務でマネージド Kubernetes のサービスを開発しているため、自宅のクラスタは実験的な要素をもたせている部分もあります。
なお Kubernetes 上に何らかのアプリケーションをデプロイできるという前提知識があると仮定し Kubernetes それ自体については説明をしません。
ハードウェア
ハードウェアは安価な小さいコンピュータを組み合わせるようにしています。
ストレージをクラスタ外に出したいと思うこともありますが、これも今はやっていません。
ストレージを別に持っていないのは実験的な要素の一つです。
ハードウェアを用意する予算があるのであれば NAS を用意し、iSCSI を通して PersistentVolume として利用するのが良いと思います。
ストレージをクラスタ外に出す方が安定して稼働させることができると思います。
コントロールプレーン
コントロールプレーンとなるノードは必要最低限の Pod が稼働すればよく、むしろより安価に台数を用意したいので Raspberry Pi 4 を利用しています。
メモリは 4GB の方がおすすめです。
現在のメモリ量を見る限り 4GB 未満では結構苦しい運用になってしまうのではないかなと思います。
Raspberry Pi を複数台利用するとなると、1つで3台をスタックできるケースを利用したくなりますがこれはあえて利用していません。
3台を1つのケースでスタックしてしまうと1台ずつメンテナンスなどをすることが難しくなってしまうからです。
見栄えはスタックできるケースの方が良くはなりますが、それは分散システムの良さを捨てることになるので、そうするのであればわざわざ複数台構成にせず1台構成で良いと思います。
(コントロールプレーンに限らずワーカーノードでも同様です。基本的におうち Kubernetes は1台ずつ止めてメンテナンスができるようになっています)
ワーカー
様々なアプリケーションを稼働させるため多少の CPU パワーとメモリ、そして x86 アーキテクチャのマシンを利用します。
現在利用しているのは
- Intel NUC 7世代 x2
- Intel NUC 8世代
- Ryzen のミニPC
の4台です。
CPU は違いますが、それぞれメモリは 16GB、ディスクは 500GB 程度の構成です。
メモリはだいたいコアあたり 4GB の計算で、ディスクは 100GB をホスト OS に、それ以外をストレージの領域として利用しています。
ワーカーは3台ではなく4台構成になっていますが、4台になっていた方がマシンを止めてメンテナンスなどをしやすいのでおすすめです。
小さい PC なので稼働しているとホコリが溜まってしまい冷却効率が落ちます。
ホコリを掃除する際には1台ずつ止めて分解する必要がありますが、この際でも3台構成を維持できていた方が可用性が高くメンテナンスがやりやすくなります。
UniFi Dream Machine Pro
クラスタ上位のルータには UniFi Dream Machine Pro (以降UDMP)を利用しています。
UDMP 導入前は UniFi Security Gateway を利用していました。
UDMP は日本のストアから買っても5万円以下で買えますし、搭載されている UnifiOS は podman で任意のコンテナを動かすことができるので大変便利です。
(そもそも UniFi Network Controller など UDMP にインストールできる公式なソフトウェアもコンテナとなっており podman で動作しています)
UDMP のポートだと少し足りない(クラスタだけで7台ある上にそれ以外に有線で接続しているマシンもあるので)ので同じく UniFi 製品の16ポートスイッチも利用しています。
このスイッチは安価なものなので L2 スイッチですが PoE+ をサポートしているのでアクセスポイントは PoE で電源が供給されています。(アクセスポイントも同様に UniFi の WiFi6 対応のもの)
OS
各マシンの OS はすべて Ubuntu 20.04 で統一しています。これは自分が10年以上 Ubuntu を使っていて一番慣れているだけです。
おそらく他のディストリビューションでもそんなに違いはありません。
ハードウェアまとめ
現在クラスタを構成しているマシンは
マシン | Role |
---|---|
Raspberry Pi 4 4GB | Control plane |
Raspberry Pi 4 4GB | Control plane |
Raspberry Pi 4 4GB | Control plane |
Intel NUC | Worker |
Intel NUC | Worker |
Intel NUC | Worker |
Ryzen MiniPC | Worker |
となっています。
構成図
クラスタを構成するものを図にすると以下のようになります。
この中の一部コンポーネントは次節以降で登場します。
Kubernetes基盤
クラスタの基盤部分についてどのような構成になっているのか説明します。
kubeadm
クラスタの構築自体については kubeadm を利用しています。
kubeadm を利用したクラスタの構築方法については公式のドキュメントが参考になります。
Static Pod と systemd
kubeadm でデプロイすると kube-apiserver 等は Static Pod で、kubelet は systemd 以下で動作するようになります。
kube-apiserver は意外にメモリを食ってしまうのでそのまま動作させているとメモリ不足になることがあります。(ありました)
なので kube-apiserver と etcd のマニフェストには resources.limits
を設けています。
コントロールプレーンの冗長化
コントロールプレーンのマシンは3台あるので冗長化して1台が壊れても API サーバーにアクセスできるようにします。
これには haproxy と keepalived を利用しています。
この2つはコントロールプレーンのマシンでそれぞれ稼働しており、Static Pod として kubelet がスーパーバイザーになっています。
kubectl の認証
kube-apiserver がどのように認証するかというのも選ぶことができます。
クライアント証明書認証だったり、事前に apiserver と共有したトークンだったりと複数の認証方法が用意されており、またそれらを複数有効にすることができます。
特に悩むのは kubectl の認証をどうするかということだと思いますが、おうち Kubernetes ではクライアント証明書認証を使っています。
コントロールプレーンのマシン上に CA の秘密鍵があるので、各クライアント側で CSR を作成しそれをコントロールプレーンのマシンでコピー、コントロールプレーンのマシン側でそれに署名し証明書をクライアント側へコピーしてきます。
クライアント側の秘密鍵はクライアントの外には出ません。
またクライアントごとに別の証明書を用意しています。つまりクライアントごとに別の秘密鍵を作成しており、それぞれ CA が署名しています。
クライアント側の証明書のセットアップはシェルスクリプトを書いて対応しています。
CNI
Kubernetes のネットワーク部はユーザーが好きなものを選べるようになっています。
おうち Kubernetes では Calico を使っています。
自宅のネットワークでクラスタを構築する場合、大体どの CNI を選んでも動作すると思います。
それぞれの CNI には少しずつ特色があるので好きなものを選べばよいのですが、選べないという場合は Calico や Cillium といった有名で利用者が多そうなものを選んでおけばとりあえずは良いと思います。
おうち Kubernetes であれば最悪、CNI の入れ替えのためにネットワーク断を伴うような作業もできるでしょうし後から変えるということもできなくもないです。
ただし CNI を入れ替える作業は結構大変だと思います。
SDS
Pod で永続化ストレージが使いたくなることもあるので Software Defined Storage として Rook / Ceph を動かしています。
Ceph クラスタ用にホストマシンのパーティションを分けてあり、各マシンで 350GB 程度が割り当てられています。
Ceph クラスタを安定的に運用するのは結構大変なのでそういうことをしたくなければ NAS をネットワーク内に配置し iSCSI 経由で使う方が楽だと思います。
MetalLB
いわゆる Service Load Balancer がないとクラスタ外からクラスタ内へのリーチが面倒なので MetalLB を利用しています。
Node Port を使うという方法もありますがクラスタ構築当初、上位のルータをロードバランサー的に振る舞わせることができなく、冗長性を確保したかったので Node port は利用していません。
今は MetalLB の speaker から BGP でアドレスを広報し、UDMP で分散させることもできるようになっています。
UDMP のファームウェアが 1.9 以下の場合は Linux カーネルが古く UDMP で分散させると MetalLB が機能しませんでした。これは古い Linux カーネルの場合、ECMP が per-packet で分散してしまうためです。MetalLB はノード間でパケットをカプセル化して転送するような機能は有していないため同じストリームのパケットは同じノードに到達する必要があります。
UDMP はファームウェアが 1.10 になった際にカーネルがアップデートされ、per-flow ECMP になったので MetalLB から BGP で広報しても動作するようになりました。
なお手を入れていない UDMP で bgpd は動いてませんので自らの手で設定し動かす必要があります。
自作ゼロトラストプロキシ
クラスタ内のサービスへインターネットからリーチできた方が便利ですが、セキュリティ面が気になります。
そこでゼロトラストプロキシを自作して使っています。
このゼロトラストプロキシは Ingress controller としても動作しますし、他にも GitHub の Webhook を管理する機能などもあります。
認証局は Google に任せており自分の Google アカウントでログインすることでクラスタ内のサービスに認証付きでアクセスすることができます。
HTTP だけではなく独自のコマンドの stdin / stdout を通して任意のプロトコルを通すこともできます。
ssh_config に次のような設定を書いておくと、ゼロトラストプロキシの認証を行った上で ssh 接続することができます。
Host *.ssh.x.f110.dev
ProxyCommand heim-tunnel proxy %h
通信が https でカプセル化されるというメリットもあり、例えば街の WiFi などで 22 番が塞がれていたとしても 443 番が塞がれていることはまずないので ssh 接続することができます。
更にこの自作ゼロトラストプロキシのデプロイは operator も実装してそれにより行われています。
自作ゼロトラストプロキシは別リポジトリとして開発しているので誰でも利用することはできます。(ただしドキュメントはほぼないので利用は簡単ではないかもしれません)
ネットワーク超え
自宅のネットワークは UDMP であるため、クラスタとその他のクライアントで vlan を使ってセグメントを分けてあります。
更にクラスタのセグメントからその他のクライアントのセグメントへの疎通は最小限にしてあり(SSH は直接通れるようにしてあるとか)基本的にクラスタ側からクライアント側へは接続できません。
しかしその他のクライアントのセグメントにも Linux マシンがあり(これは通常利用しているマシン)このマシンにインターネット越しに SSH したいこともあります。
これも自作ゼロトラストプロキシで解決できるようにしています。
接続先のマシン(クライアントセグメントの Linux)でエージェントプログラムを起動しておくことで常にプロキシに対してコネクションを張るようになっています。
このコネクションはクライアントセグメント → クラスタセグメントになります。
このコネクションを利用して、インターネットからの通信を通るようにしています。
接続先からプロキシの間だけ、逆向きに張られたコネクションを利用しコネクションができるようになっています。
この仕組みは自宅内のネットワークだけではなくインターネット越しでも当然機能するので例えばオフィスにあるマシンでエージェントを起動しておけばインターネットから SSH 接続するといったことも可能になります。
cert-manager
自作ゼロトラストプロキシにはサーバー証明書が必要なため、それの発行をしています。
Let’s encrypt の証明書で期限内に更新もして Secret を更新してくれるのでほぼ手がかかりません。
手がかからないようにするには自作プロキシの方で Secret の更新をちゃんと監視し更新されたら証明書を読み直す必要はあるのですが…
(意外にマウントした ConfigMap や Secret の更新をちゃんと監視できないという例は聞きますし、自作プロキシの検証のためにも自動で証明書を更新し読み直しをしています。もし証明書の読み直し部分にバグがあればここで気がつくことができます)
ArgoCD
マニフェストの適用を手動でやっているといずれ適用忘れが発生するので push 型の GitOps をするようにしています。
GitOps のツールとして ArgoCD を利用しており、基本的には適用したいマニフェストはリポジトリにコミットして push するようになっています。
ただ、自分の権限は cluster-admin になっており直接マニフェストを適用することも可能ではあります。
ArgoCD を通してしかマニフェストを適用できないというほど厳密に管理すると、その ArgoCD が同一クラスタで動いている都合上、何も操作できなくなるという可能性があります。
なので自分の権限でもマニフェストは適用できるようにしておき、修復できるようにしています。
実際、ArgoCD でマニフェストを適用できなくなるといった状態には結構陥るので手動でマニフェストを適用することはあります。
Loki
クラスタでは多くの Pod が動いているのでログの収集もしておきたいところです。
多くの場合 kubectl logs
でログを直接見るのですが、過去のログを見たり検索したくなることはあるのでそのために Grafana Loki でログを集約しています。
各ノードからログを集めてくるのは promtail を使っています。
promtail の代わりに fluent-bit を利用していたこともあったのですが、総合的に promtail の方が楽に運用できると考えて promtail に落ち着きました。
fluent-bit はバッファー等の設定が結構煩雑なのと油断すると簡単に溢れたりしてしまい運用に手がかかります。
promtail もたまに CPU を大量に使うときがあったりするのですが、fluent-bit ほど運用に手がかかるという感じはしませんでした。
自作コントローラ
前述の自作ゼロトラストプロキシはプロキシ本体だけではなく、Kubernetes 上にデプロイするためのコントローラも自作してそれによってデプロイされています。
自作コントローラはこれ以外にもクラスタの操作などで自動化できると思ったものをいくつか作っています。
例えばクラスタ上で Grafana が動いていますがこれには事前にユーザーアカウントを作っておく必要があります。
そのユーザーを作成するだけのコントローラがあります。
Kubernetes 上に GrafanaUser
というオブジェクトを作るとユーザーを管理してくれます。
それ以外にも Harbor のプロジェクトも Kubernetes のオブジェクトで管理しています。
気軽にコントローラを追加して動かすことができるようにしておくことでなるべく自動化をしていくようにしています。
バックアップ
おうち Kubernetes は一つのクラスタなので万が一に備えて Kubernetes のデータは外部にバックアップをしています。
Kubernetes のバックアップは Etcd のバックアップを取ることで、そういうツールもありますがバックアッププログラムを自作しています。
バックアップを取りオブジェクトストレージに保存するだけであれば大して複雑でもなくコード量も大したことがないので自分で書いてしまった方がトータルでの運用コストを下げられるだろうという判断です。
バックアッププログラムは Kubernetes の CronJob で定期的に動かし、バックアップデータは Google Cloud Storage に保存しています。
バックアップデータは一定期間保存し、ある程度古いものは消していますがこれは GCS の機能で実現されています。
クラスタ上で動いているもの
ここまではクラスタとして機能させるために必要であったり、クラスタの運用に必要なサービスとして動かしているものを紹介しました。
ここからはクラスタ上で動いているが、必ずしも運用のために必要ではないが自分がクラスタ上で動かしているものの一部を紹介します。
minio
クラスタ内にオブジェクトストレージもあると便利に使えます。
SDS があるので必要であれば PersistentVolume をマウントすればいいのですが、オブジェクトストレージがあれば複数 Pod から共通のデータを参照したりもできるので便利に使えます。
おうち Kubernetes ではここにいくつもデータを置いています。(NAS としては利用していません)
自作CI
自分のリポジトリ用に CI ツールを自作しています。
仕事などで Jenkins や drone などを使ったこともありますがどの CI ツールもどうもしっくり来なくて満足できませんでした。
それは CI でやりたいことを上から書き下していくスタイルなため、タスクの前後関係がはっきりしなかったりとか分岐が分かりづらかったりして、書いた本人が書いた直後は理解できるがいずれ誰も理解できなくなるからです。
そこでこれを解決するために自作 CI ツールを作って動かしています。
自分のリポジトリは Bazel でビルドするようにしているので Bazel で CI タスクも定義するようになっています。
したがって自分のローカルでも概ね同じように CI タスクを動かすことができますし、依存関係も Bazel を通して覗くことができます。
Prometheus
クラスタの監視には Prometheus を利用しています。
ただ Prometheus 自体がクラスタ内で動いているため、クラスタとして全く動作しなくなるレベルのものは検知することができないことが多いです。
ただ、あるノードの死活監視とかは行えるので気がついたらノードが死んでいたといった事態は避けられます。
Grafana
Prometheus が収集したデータの可視化ダッシュボードとして Grafana を利用しています。
これは定番の構成だと思います。
Grafana にはあまり大事なデータは入れておらず(ダッシュボードのデータくらい?)かなりお手軽にデプロイしているので Pod が再作成される際などは一時的に疎通しなくなります。
ユーザーの認証などは前段のプロキシで行っているので Grafana に求めているのは Prometheus のデータを可視化することと、Loki のフロントエンドになることだけです。
Hashicorp Vault
クラスタ構築当初は Secret にクレデンシャルの類をそのまま入れていたのですが、一つのクレデンシャルを複数の Namespace にコピーするといったことが起きるようになってきたので最近 Hashicorp Vault を導入しました。
基本的にクレデンシャルは Vault に保存しておき、ArgoCD でそれを Secret に同期しています。これには argocd-vault-plugin を利用しています。
また一部では直接 Vault へ問い合わせている箇所もあります。(自作コントローラなど)
Vault 自体は GCP の Cloud Key Management で Seal しています。
NATS
おうち Kubernetes 内の自作しているサービスにおいてコンポーネントを分離したマイクロサービス的な設計にすることがあり、サービス間の通信のために NATS をデプロイしています。
今のところ NATS を利用しているソフトウェアは一つですが、こういった分散システムを構築するためのコンポーネントは事前に用意しておくと後々便利に使えるのでそれを使いたいソフトウェアが1つある時点で構築・運用しました。
利用量としてはほぼ無いに等しく(1日1つイベントが飛ぶ程度。それを Subscribe しているプロセスが3つ)性能についての良し悪しは全くわかりませんが、NATS があることでコンポーネントを分離することができ、不要な処理が減らせる・リソースの共有をしなくても済むなどの大きなメリットがあります。
Harbor
クラスタ内のコンテナレジストリとして Harbor をデプロイしています。
クラスタ外のコンテナレジストリ(Docker Hub や Quay、GitHub Container Registryなど)だとインターネットと通信しなければいけず、また場合によっては保存・転送にコストがかかるのでそういったことを考えなくても済むコンテナレジストリをクラスタ内に用意しています。
ただ、このコンテナレジストリに保存されているコンテナはコンテナレジストリ自体が起動しないと取得できないため、クラスタの動作に直接的に影響があるものはここには保存していません。
クラスタ外の要素
Kubernetes クラスタの構築・運用を完全にクラスタ内に閉じることはできません。
ここではクラスタ外で利用しているものについて説明します。
ただ、なるべくクラスタ内に閉じれるようにしているため、要素としてはそれほど多くはありません。
マニフェストリポジトリ
上の方で ArgoCD で GitOps をしていることは書きましたが、そのためのリポジトリは GitHub に置いています。
このリポジトリはソフトウェアが入っておらず、GitOps や設定ファイルの置き場となっています。
また当初は Secret を直接利用していたこともあり、git-crypt で特定のファイルのみ暗号化しています。
リポジトリ自体もプライベートにはなっていますが、万が一ヒストリごと流出してしまってもクレデンシャルは流出しないようになっています。
GCP
Cloud Storage
クラスタ内にもオブジェクトストレージは用意していますが、バックアップといった失いたくないデータはクラスタ外に出しており、その保存先は Cloud Storage になっています。
バックアップデータだけの利用なのとファイル自体も圧縮されているためコストとしては毎月10円もかかっていません。
これくらいのコストであれば信頼性も高いことからお金を払ってしまったほうが色々楽になります。
Container Registry
クラスタの動作に必須なコンテナなどは Container Registry に保存しています。
クラスタ上にデプロイされているコンテナすべてを Tier1 / Tier2 と2つのグループに分けており、Tier1 はそれらのコンテナが動作しなかった場合、クラスタ内の他のサービスが動作しないなどの重要なコンテナです。
Tier1 のコンテナはすべて自分の Container Registry へコピーしています。
おうち Kubernetes にはサードパーティが作成したコンテナもそのままデプロイしているものが多くあり、サードパーティによってはコンテナレジストリとして DockerHub を利用しているものもあります。
DockerHub はレート制限があるなど結構不便なのと、個人的にサービスの先行きが心配でもあるので利用しているコンテナはすべて自分の Container Registry にコピーしています。
クラスタ内の構成
ここまで紹介してきたコンポーネントの関係を図にすると下のようになります。
それぞれの関係を線でつないでしまうとごちゃごちゃしてしまうので描いていません。
クラスタのアップデート
無事にクラスタの構築が終わり、稼働しだすと数カ月後に Kubernetes のアップデート作業がやってきます。
自分は Managed Kubernetes サービスの開発者であり、業務で Kubernetes のリリースにはよく触れるため特に仕掛けを用意しなくても新しいリリースがあったことに気がつくことができます。
そうではない場合、メジャーリリース(Kubernetes vX.Y.Z の Y が変わる時)には気がつけるようになっていると良いと思います。
アップデート作業については基本的に kubeadm のガイド の通りに行います。
ただ、手順を追加している部分があります。
ノードの drain 後に **ノード自体を再起動しています。**再起動後 apt upgrade
でシステム全体を更新した後に kubelet を更新しています。
システム全体を更新した際にカーネルの更新があれば uncordon する前にもう一度再起動をしています。
1回目の再起動はゾンビプロセスなどをなくし、一度クリーンな状態にすることが目的です。
本来はゾンビプロセスなどがなく、再起動をせずにシステム全体を更新できる方が良いのですが数ヶ月動かしているとゾンビプロセスの1つや2つはできており、また PersistentVolume をマウントしているコンテナがあるとディスク IO で詰まることもよくあります。
そのため一旦再起動した方が結果的に早く確実な作業になります。
おうち Kubernetes という箱庭
マネージド Kubernetes の開発時にはおうち Kubernetes という箱庭は役に立ちます。仕事で導入する前に色々試すことができるので、業務で似たようなものを実装する際は経験値がある状態から行うこともできますしあえて事前に試していた方法とは違うものを実装してみて良し悪しを判断することもあります。
しかし箱庭のメンテナンスは意外にコストが掛かります。自宅のクラスタなので見て見ぬ振りをすることも簡単でしょう。実際、意図していなくてもソフトウェアのアップデートが滞っていたとか動かなくなっていたということはよく起こります。
そういった状態を発見するたびに自動化するための仕組みを作りそれ以降なるべく起きないように心がけていくことが重要です。
最初は色々ないので様々なものを作らないといけないかもしれません。
しかし少しずつ着実に自動化していけば手作業は確実に減っていきます。
Kubernetes を動かすという運用ではありますが、ソフトウェアの開発のように一歩ずつ改善していくのが良いのではないでしょうか。
決して銀の弾丸はありません。また、最初からハイクオリティなものを目指すのも難しいですし、いきなりハイクオリティなものを作るのは至難の業です。
Discussion