自宅鯖を冗長化したはなし

Red Hat

1台しか無い状態でいずれかの原因でサーバが逝くと外付けHDDとAzure上にあるデータから復旧しないとならず、これはなかなか骨が折れる作業になる。なのでもうちょっと簡単にして復旧時間を短縮したい。今回の自宅鯖の更新ではこのあたりを考慮していくつかの対策を実施した。ポイントはプライマリが故障しても

  • 可能な限り最新のデータがある
    (daily, hourlyのバックアップでは欠落するデータが出る)
  • 一部のサービスは継続できる

ことに加えて、自宅鯖なんで

  • 出来るだけ安いこと

も重要。業務ならHAクラスタを組んでしまうなりコンテナに移行してKubernetesにしちゃえば良いけれど、自宅鯖だとオーバーキルになってしまうのでそこまではやりたくないw

またコンテナを導入した初期構築の負荷の低減というメリットと、コンテナによるトラブルや継続的なメンテナンス負荷のデメリットを勘案して、今回も自宅鯖には導入しないことにした。たくさんサーバがある、サービスがあるならコンテナが勝るのは言うまでも無いけれど、自宅鯖ぐらいだとメリットはほぼないですw

ということで詳細はともかくどういう構成になっているのか備忘録として書いておく。というのも、今回の故障で、旧鯖の構成をドキュメント化しておらず全てファイルから復旧したらやはりメタな情報が必要だったことと、RHEL 7から8への移行も兼ねたので作業量が増えたのが見直しのきっかけになった。

自宅鯖(s)のハードウェア

今回の更新ではサーバを2台にした。旧サーバは3.5インチHDDを4枚使ってRAID 10としていたけれど必要無くなったのでケース、SSD、NICを新調して小型化した。見ての通り「サーバ」とは言っているけれどパーツはPC用、それもゲーム用だったりする。

  • (新)Ryzen 5600Gのサーバ
  • (旧)Core i7 8700のサーバ

Ryzen 5600G

  • CPU: Ryzen 5600G
  • M/B: ASUS TUF GAMING B550M-PLUS
  • Memory: 64GB (DDR4 PC4-25600 ECC、ただしECCはまだDisable)
  • Main SSD (NVMe): ADATA XPG GAMMIX S70 BLADE 2TB
  • Data SSD1 (NVMe): Intel P3608 3.2TB
  • Data SSD2 (SATA): Samsung 870 QVO 8TB
  • NIC: TP-Link 10Gbps NIC TX401

Core i7 8700

  • CPU: Core i7 8700
  • M/B: Gigabyte Z370M D3H
  • Memory: 32GB (DDR4 PC4-17000)
  • Main SSD (NVMe): ADATA XPG GAMMIX S70 BLADE 1TB
  • NIC: TP-Link 10Gbps NIC TX401

サービス

自宅鯖で提供しているサービスはRed Hat Enterprise Linux 8に同梱されるRPMパッケージをメインに構成しており、代表的なものは

  • ウェブサーバ
  • ネームサーバ
  • メールサーバ
  • DHCPサーバ
  • データベースサーバ

といったところで、これだけ並べるとそれほど特殊には見えないのだけれどウェブサーバはかなり特殊な構成とも言える。

ウェブサーバ

全てApacheのVirtual Hostになっており、

  • 通常のDocument RootによるVirtual Host
  • php-fpmによるownCloud
  • php74-php-fpmによるWordpress
  • mod_proxyによるcockpit
  • mod_proxyによるPlex Media Server

が組み合わされVirtual Hostの総数は12となっている。

php-fpmが2つ動いているのはphpのバージョンが原因。ownCloudは7.2をサポートしているが、Wordpressは7.4以降でないとサポートしていない。php74-php-fpmに一本化しても良いけれど、ownCloudとWordpressではphp-fpmの設定を変えたいこともあり両方動かすこととした。php74-php-fpmはremiレポから持ってきているので勿論EPEL8レポもenableしてある。

ネームサーバ

bind (named-chroot)を使った外向き・内向きのViewを持ったサーバをIPv4/v6で構築してある。RHEL8ではchrootはmount namespaceになっているのでファイルの置き場所がちょっと変わったことに伴い、aclや広告ネットワークへのアクセスを落とすzoneのファイルなどは、named.confからincludeするようにした。

メールサーバ

postfix、postgrey、dovecotによるsmtps / imapsサーバ。postfixの認証はdovecotで。メールの振り分けはprocmail、スパム対策はS25Rに加えてspamassassinなので、.procmailrc.spamassasin/etc/skel/に入れてuseraddしたらデフォルトの設定が入るようにしてある。

DHCPサーバ

DNS同様IPv4/v6デュアルスタックなのでdhcpdとdhcpd6をenableしてある。

データベースサーバ

RHELに同梱されるのはmariadbでownCloudとWordpressのバックエンドになっている。Redisもキャッシュとして動かしてある。

その他

Plex Media Server、APCのUPSを監視するためのapcupsdなどが動いている。

自宅鯖(s)の同期

非対称クラスタみたいなものでプライマリでの変更をスタンバイに自動的に反映する必要がある。Shared NothingなのでSPOFが存在しない反面、データを同期するには工夫が必要になる。ファイルベースで同期出来るもの、サービスが連携しないと同期が難しいものがあるので、それらを分けて構築した。

mariadb

ある意味で簡単なのはmariadb。binlogでmaster/slave(昨今はよろしくないワーディングとされているが、用語としてこれらを用いざるを得ないし、実際my.cnfに設定する用語)を構築すれば完了。特に難しいことはない。

PostgreSQL

スパム送信元のIPアドレスからのアクセスをfirewalldで制限するため、IPアドレスをPostgreSQLのテーブルに格納するようにした。つまり、firewall-cmdで管理するにはIPアドレスが多くなりすぎて、IPアドレスの追加に数分もかかるようになってしまったので、直接/etc/firewalld/zones/drop.xmlを生成し、firewall-cmd --reloadするようにした。PostgreSQLの同期はlogical replicationを設定する、つまりプライマリがパブリッシャ、セカンダリがサブスクライバになるようにすれば良い。

dovecot

10分毎にimapsyncを走らせている。自宅鯖のメール流量だとこのぐらいで十分。Maildirは後述のlsyncdの対象にしてあるが念の為。

ファイルベースの同期

他のサービスは

  • 設定ファイルの同期
  • 提供するデータの同期

が必要だけれど、いずれもファイルの同期をした上でサービスをreloadすれば済むものがほとんどなので、lsyncdによる同期と必要に応じてlsyncdからbashスクリプトをキックして何らかの操作を自動的に行う。当然のことだけれどホスト固有のファイル、例えば/etc/sysconfig/network-scripts/などは同期してはまずいので、/etc/全体ではなくサービスの設定ファイルが含まれるディレクトリをlsyncdで監視している。lsyncdのサンプルは/usr/share/doc/lsyncd/examples/にある。man lsyncdも参考になる。

rsync/rsyncsshで同期する場合、lsyncd.confinsist=trueを入れておかないと、ちょっとrsyncが接続出来なかっただけでlsyncdがstopしてしまうので注意。SELinuxのラベルも同期する必要があるので、xattrs=trueも必要。

RPMパッケージの同期

DNFにはトランザクションをフックする仕組みがYumの頃からあり、python3-dnf-plugin-post-transaction-actionsパッケージをインストールすると、/etc/dnf/plugins/post-transaction-actions.d/ディレクトリが用意される。ここに*.actionというファイルを置いて、

*:any:echo '${repoid}' >>/tmp/post-trans-actions-repos

のように書けばDNFのトランザクションのタイミングで何かしらの処理をキックできる。詳細はman dnf-post-transaction-actionsを参照。

バックアップ

RPMパッケージのリスト

意外と忘れがちなのが、どのRPMパッケージがインストールされていたかというメタデータのバックアップ。rpmdbに記録されているけれどrpmdbが破損することもあるし、そもそも当該ホストが起動すら出来ない場合はrpmdbをレスキューするのも難しくなるので、事前に日次バッチでリストを出力してバックアップしておく。

設定ファイルのパッチ

上記のようにファイルを同期しているものの、RHELのバージョンがあがるとそもそも設定ファイルなども変わってしまい、バックアップしてある設定ファイルをそのまま使えることは期待できない。最近はOSSプロジェクトも成熟して変化が少なくなったので、旧バージョンの設定ファイルを新バージョンに読ませるとsyntax errorが出ることは減ったとは言え、事前に備えておくのは急遽対処するよりずっとコストが低い。

旧サーバではetckeeperを使っていたが長年運用していると.gitディレクトリが肥大化してバックアップ時に邪魔になり無視できなくなった。

日次で設定ファイルのパッチを作成してバックアップするようにした。現行バージョンならこのパッチをそのまま適用すれば良いし、メジャーバージョンアップならこのパッチを見ながら設定ファイルを更新すれば作業としてはかなり楽になる。

#!/bin/bash

pkgs=$(rpm -qa | sort | grep -v gpg-pubkey)

for pkg in $pkgs; do
	confs=$(rpm -V $pkg | grep ' c ' | awk '{print $3}')
	if [ -n "$confs" ]; then
		dname=$(rpm -q --qf '%{NAME}' $pkg)
		mkdir /var/log/rpmstore/$dname
		for conf in $confs; do
			confdirname=$(dirname $conf)
			conffname=$(basename $conf)
			mkdir -p /var/log/rpmstore/$dname$confdirname
			diff -up /opt/rpmstore$conf $conf > /var/log/rpmstore/$dname$confdirname/$conffname.patch
		done
	fi
	unset confs
done

例えば、/etc/httpd/conf/httpd.confなら以下のようになる。

# cat /var/log/rpmstore/httpd/etc/httpd/conf/httpd.conf.patch 
--- /opt/rpmstore/etc/httpd/conf/httpd.conf	2021-10-16 21:33:10.282608490 +0900
+++ /etc/httpd/conf/httpd.conf	2021-10-21 20:50:25.020176790 +0900
@@ -44,6 +44,10 @@ ServerRoot "/etc/httpd"
 #Listen 12.34.56.78:80
 Listen 80
 
+RewriteEngine on
+RewriteCond %{HTTPS} off
+RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
+
 #
 # Dynamic Shared Object (DSO) Support
 #
@@ -95,7 +99,7 @@ ServerAdmin root@localhost
 #
 # If your host doesn't have a registered DNS name, enter its IP address here.
 #
-#ServerName www.example.com:80
+ServerName www.rio.st:80
 
 #
 # Deny access to the entirety of your server's filesystem. You must

RPMパッケージに含まれないファイル

RPMパッケージに含まれるファイルは上述のスクリプトでパッチが作成されるけれど、例えば/etc/httpd/conf.d/にvimで作成したファイルや、/etc/yum.repos.d/にwgetで持ってきたレポファイルなどは対象とならない。rpm -qf /path/to/fileすればRPMパッケージに含まれないファイルが見つけられるので、/etc/以下についてはこれらのファイルのリストをバックアップする。

#!/bin/bash

LANG=C

filter='/etc/dnf/modules.d/'
filter=$filter'\|/etc/aliases.db'
filter=$filter'\|/etc/pki/'
filter=$filter'\|/etc/rhsm/syspurpose/'
filter=$filter'\|/etc/grub.d/'
filter=$filter'\|/etc/.pwd.lock'
filter=$filter'\|/etc/ssh/'
filter=$filter'\|/etc/nvme/'
filter=$filter'\|/etc/nsswitch.conf.bak'
filter=$filter'\|/etc/cockpit/'
filter=$filter'\|/etc/iscsi/'
filter=$filter'\|/etc/insights-client/'
filter=$filter'\|/etc/.updated'
filter=$filter'\|/etc/postfix/.*.db'
filter=$filter'\|/etc/subuid-'
filter=$filter'\|/etc/subgid-'
filter=$filter'\|/etc/group-'
filter=$filter'\|/etc/gshadow-'
filter=$filter'\|/etc/passwd-'
filter=$filter'\|/etc/shadow-'

for f in `find /etc/ -type f`; do rpm -qf $f; done | grep 'is not owned by any package'| grep -v $filter | awk '{print $2}'

LANG=ja_JP.UTF-8

バックアップ先

バックアップ先は2箇所用意した。

ConoHA VPS

NS/MXレコードにもしているCentOS。こちらは単にrsyncで同期すれば良い。もちろんrsyncだけではなくnamed-chroot、postfixも動いている。

Azure Storage Account

azcopyコマンドをRHELに入れAzure ADでアプリを登録しクレデンシャルをRHELに持たせてやることで、高速に(高帯域で)バックアップすることが可能。

まとめ

自宅に複数台のサーバがあるという状態はずいぶんと久しぶりのことだし、そういう構成を取っている・取ろうとする人が多いとも思えないから、あまりこのエントリーの需要があるとも思わないけれど、自宅鯖+VPSで冗長化するといったことも考えられるので何らかの役に立てば。