Summary
-
hostPath利用時のPod移動不可問題を解決するため、rsyncとlsyncdを組み合わせたディレクトリ同期を導入した - これにより、片ノード障害時でもPodが別ノードへ移動し、継続稼働が可能になった
- 本構成は単一Pod稼働を前提とし、複数Pod同時稼働時のデータ整合性問題に注意が必要である
本記事はUbuntu 24.04 および 26.04環境で実施した内容である。
やらないこと
- Ceph等の分散ストレージの導入
- 完全なリアルタイムでのデータ整合性の確保
- CSIドライバーによるストレージ抽象化
本記事における課題
Kubernetes環境において、永続ストレージの選択は重要な課題である。
- 分散ストレージ(例: Ceph, GlusterFS)は高可用性を提供するが、構築と運用に多くのリソース(CPU, メモリ, ストレージI/O)を必要とする。今回の環境では導入が困難であると判断した。
- NFSは比較的容易に導入可能だが、ネットワーク越しでのI/O性能が低く、一部のアプリケーションで動作が著しく遅延した。特にデータベースや頻繁なファイル操作を伴うアプリケーションで顕著であった。
- 各KubernetesノードにはRAID1構成のHDDがあり、十分なI/O性能を持つ。このローカルストレージを最大限活用したい。
-
hostPathボリュームはノードローカルストレージへの高速アクセスを提供する。しかし、Podが別のノードに再スケジュールされた際、データが存在しないため継続稼働が不可能となる課題があった。
よって、高速なノードローカルストレージの性能を活かしつつ、ノード障害時にPodが移動可能となる高可用性を実現する必要があった。
やったこと
本課題を解決するため、Kubernetesノードローカルストレージ(RAID1 HDD)をhostPathで使用し、そのデータをlsyncdとrsyncを用いて別ノードへ同期する構成を導入した。これにより、ローカルストレージの性能とノード障害時のデータ可用性を両立した。
基本アーキテクチャ
-
ノードローカルストレージの利用: 各Kubernetesノードは
/{base-path}/配下にRAID1構成のHDDをマウントする。PodはhostPathでこの領域を利用する。 -
lsyncdとrsyncによる同期: 各ノード上の特定ディレクトリ(例:
/{base-path}/app-data/)の変更をlsyncdが監視し、変更を検知次第rsyncを用いてもう一方のノードへ自動で同期する。 - 高可用性の実現: 片方のノードに障害が発生した場合でも、Podは別の健全なノードに再スケジュールされ、同期済みのデータにアクセスして処理を継続できる。
-
Podの制約: この構成では、データ整合性を確保するため、対象となるPodは常に1つのノードでのみ稼働するように制御する。具体的には、StatefulSetの
replicas: 1を設定する。
lsyncdの選定理由
lsyncdは、指定されたディレクトリの変更イベントを監視し、イベント発生時にrsyncなどの同期ツールを呼び出す。これにより、リアルタイムに近い形で効率的な差分同期を実現できるため、このユースケースに最適であった。
SSH鍵認証の設定
lsyncdがrsync経由でリモートノードへSSH接続するため、各ノードでrootユーザーのSSH鍵認証を設定する必要がある。セキュリティを考慮し、パスワード認証は無効化する。
-
SSH鍵ペアの生成: 各ノードのrootユーザーで鍵ペアを生成する。既に存在するならばスキップ可能である。
sudo ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N "" -
公開鍵の交換: 各ノードのroot公開鍵(
/root/.ssh/id_ed25519.pub)を、相手ノードのrootユーザーの~/.ssh/authorized_keysに追加する。# 同期元ノードから同期先ノードへ公開鍵をコピー ssh-copy-id -i /root/.ssh/id_ed25519.pub root@<リモートノードのIPアドレス>これを相互に行い、各ノードからrootユーザーでパスワードなしにSSH接続できることを確認する。
-
SSHD設定の変更:
PermitRootLogin prohibit-passwordを設定し、rootユーザーのパスワード認証を禁止する。これにより鍵認証のみが許可される。# /etc/ssh/sshd_config を編集 sudo sed -i 's/^#PermitRootLogin prohibit-password/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config sudo sed -i 's/^PermitRootLogin yes/#PermitRootLogin yes/' /etc/ssh/sshd_config sudo sed -i 's/^PermitRootLogin without-password/#PermitRootLogin without-password/' /etc/ssh/sshd_config sudo systemctl restart sshd設定変更後、
systemctl restart sshdでSSHDサービスを再起動する。
lsyncdのインストール
今回使用したOSはUbuntu 24.04/26.04である。以下のコマンドでlsyncdをインストールする。
sudo apt update
sudo apt install -y lsyncd
インストール後、systemctl status lsyncdでサービスが起動していることを確認する。
lsyncd設定例
/etc/lsyncd/lsyncd.conf.luaは以下の通りに設定した。設定では、複数アプリケーションのデータディレクトリをそれぞれ同期対象としている。
注釈: <ベースパス>、<アプリケーション名>、<リモートノードのIPアドレス>は環境に合わせて適切に置き換える必要がある。
settings{
logfile = "/var/log/lsyncd.log",
statusFile = "/tmp/lsyncd.stat",
statusInterval = 1,
insist = 1,
}
sync{
default.rsync,
source = "/<ベースパス>/app-data-1/",
target = "root@<リモートノードのIPアドレス>:/<ベースパス>/app-data-1/",
delete = "running",
init = false,
exclude = {"**.log", "**.log.*"},
rsync = {
archive = true,
_extra = {"--exclude=*.log", "--exclude=*.log.*"},
rsh = "/usr/bin/ssh -i /root/.ssh/id_ed25519 -o StrictHostKeyChecking=no"
}
}
sync{
default.rsync,
source = "/<ベースパス>/app-data-2/",
target = "root@<リモートノードのIPアドレス>:/<ベースパス>/app-data-2/",
delete = "running",
init = false,
rsync = {
archive = true,
rsh = "/usr/bin/ssh -i /root/.ssh/id_ed25519 -o StrictHostKeyChecking=no"
}
}
-
settings: ロギングとステータスファイルのパス、ステータス更新間隔、エラー時の再試行(insist = 1)を設定する。 -
syncブロック: 各同期対象ディレクトリごとに定義する。-
source: 同期元のローカルパス。 -
target: 同期先のリモートパス。root@<リモートノードのIPアドレス>形式でSSH接続先を指定する。 -
delete = "running": 同期元でファイルが削除された場合、同期先でも削除する設定。runningはlsyncdが起動中に削除されたファイルを追跡し、同期先でも削除することを意味する。 -
init = false: 重要な設定である。lsyncdサービス起動時に初回同期を実行しない。これにより、サービス起動時の大規模なI/O負荷や、既に同期されているデータに対する不要な処理を避ける。事前にrsync -av <source> root@<target-ip>:<target>コマンドを手動で実行し、初期同期を完了させておくことが推奨される。 -
exclude: 同期対象から除外するファイルを指定する。特にアプリケーションが頻繁に書き込むログファイルなどを除外することで、同期のオーバーヘッドを減らし、同期の無限ループを防ぐ。
-
-
rsync.rsh:rsyncがSSH接続に使用するコマンドを指定する。秘密鍵のパスと、StrictHostKeyChecking=noを設定することで、初回接続時のKnown Hosts警告を回避し、自動化を容易にする。ただし、StrictHostKeyChecking=noはセキュリティリスクを伴うため、本番環境ではより厳格なSSHホストキー管理を検討すべきである。 -
rsync._extra: 同期対象から除外するファイルを指定する。特にアプリケーションが頻繁に書き込むログファイルなどを除外することで、同期のオーバーヘッドを減らし、同期の無限ループを防ぐ。
一部のアプリケーションでは、_extraオプションでログファイルを除外しないと、既存のログローテーション機構と競合し、ファイルが大量に生成される事象が発生した。excludeと合わせて_extraも用いることで、rsyncコマンドラインレベルでの確実な除外が期待できる。
Kubernetes StatefulSet設定例
Podが単一ノードで稼働し、hostPathを利用するStatefulSetの例を示す。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-app-statefulset
labels:
app: my-app
spec:
replicas: 1 # Podは常に1つのみ稼働させる
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node-01
- node-02
volumes:
- name: app-data-volume
hostPath:
path: /<ベースパス>/app-data-1/ # lsyncdで同期しているローカルパス
type: DirectoryOrCreate
containers:
- name: my-app-container
image: my-app-image:latest
ports:
- containerPort: 80
volumeMounts:
- name: app-data-volume
mountPath: /data/app-data # コンテナ内のマウントパス
-
replicas: 1: Podが1つしか起動しないことを保証する。これにより、複数のPodが同時に同じデータに書き込むことを防ぎ、データ整合性問題を回避する。 -
hostPath:/data/app-data-1/をPod内の/data/app-dataにマウントする。これにより、コンテナはノードのローカルストレージに高速にアクセスできる。 -
affinity: Podがデプロイされるノードを、lsyncdをセットアップしたノードに限定する。
この構成は、あくまで片方のノードが書き込みを行い、もう片方がその変更を追随する形式である。両方のノードで同時に同じデータに対して書き込みが発生した場合、データの整合性が破綻する可能性が非常に高い。 このため、今回のユースケースではPodが常に単一のノードでしか稼働しないように厳しく制御している。
まとめ・所感
本記事で紹介したrsyncとlsyncdを組み合わせたノードローカルストレージ同期構成は、リソースが限られた環境でKubernetesの永続ストレージ問題に対処する有効な手段である。
分散ストレージの導入コストやNFSの性能課題を回避し、既存のノードローカルストレージの性能を最大限に活用しつつ、ノード障害時の高可用性も確保できた。
ただし、この方法はデータ整合性の保証が限定的であり、単一PodによるReadWriteOnce相当の利用に限定される。複数Podによる同時書き込みが必要な場合には、CephFSやRookのような分散ファイルシステムや、CSIドライバーを活用したより高度なストレージソリューションを検討すべきである。
