오래 실행되는 서버와 상태를 가진 시스템에도 불변 인프라의 아이디어를 적용해, 부팅할 때마다 루트 파일시스템을 초기화하고 필요한 상태만 명시적으로 유지하는 방법을 설명합니다.
불변 시스템을 위한 가변 시스템
2020년 4월 13일 게시
나는 부팅할 때마다 내 시스템을 지운다.
시간이 지나면 시스템은 루트 파티션에 상태를 쌓아 올린다. 이 상태는 /etc와 /var 같은 여러 디렉터리에 존재하며, 서비스를 올리는 과정에서 문서화가 부족했거나 순서가 뒤바뀐 모든 단계를 나타낸다.
“맞아,
myapp-init를 실행해.”
이런 작고 사소한 “아, 이런” 단계들은 결국 사라져서 운영 절차서에 나타나지 않는다.
“그냥 ca-certificates를 내려받아서 … 그걸 고치려고 …”
이런 임시방편 하나하나는, 마침내 그 끔찍한 RHEL 7에서 RHEL 8로의 업그레이드를 하게 되는 3년 뒤에 역사를 반복하도록 당신을 운명짓는다.
“아,
touch /etc/ipsec.secrets를 해야 해, 안 그러면 l2tp 터널이 동작하지 않아.”
불변 인프라는 이렇게 잊히는 수많은 단계를 제거하는 데 놀라울 정도로 효과적인 방법이다. 매주 또는 매달 서버를 삭제하고 교체하는 고통을 기꺼이 받아들이면, 자동화와 운영 절차서를 계속 시험하고 실제로 사용하게 된다.
여기서 핵심은 시스템 상태를 정기적으로, 가리지 않고 제거하는 것이다. 서버 전체를 파괴해 버리면, 그 과정에서 해 두었던 작은 수정들을 잊어버릴 여지가 거의 없다.
이런 기법은 두 가지 요구 사항을 만족할 때 훌륭하게 동작한다.
불변 인프라가 동작하지 않는 경우는 많고, 더러운 비밀은 바로 그런 서버일수록 좋은 도구가 가장 절실하다는 점이다.
오래 실행되는 서버는 긴 장애를 만든다. 그 운영 절차서는 낡았고 불완전하다. 이런 서버는 자잘한 수정들을 계속 쌓아 올리며 결국 굳어 버리고 깨지기 쉬운 눈송이가 된다. 다만 그 눈송이의 팔이 구조를 지탱하고 있을 뿐이다.
이런 시스템에도 불변 인프라의 아이디어를 가져와 보자. 이 시스템이 경기장 전광판에 내장되어 있든, 데이터센터에 있든, 책상 아래에 있든, 우리는 할 수 있다, 상태를 통제된 상태로 유지할 수 있다.
오래 실행되는 서버에 불변 기법을 적용할 때 어려운 점은, 정확히 어디까지가 애플리케이션 상태이고 어디서부터 운영체제, 소프트웨어, 설정이 시작되는지 아는 것이다.
이것이 어려운 이유는 레거시 운영체제와 Filesystem Hierarchy Standard가 이런 관심사를 제대로 분리하지 못하기 때문이다. 예를 들어 /var/lib는 상태 정보를 위한 곳이지만, 그중 실제로 추적하고 싶은 것은 얼마나 되는가? /etc에 의도적으로 설정한 것은 무엇인가?
아마 그리 많지 않을 것이다.
당신이 신경 쓰지 않더라도, 이렇게 쌓이는 온갖 잡동사니는 수렁이다. 모든 것이 더 어려워진다. 운영 환경 복제, 변경 사항 테스트, 실수 되돌리기까지.
새 컴퓨터를 받는 순간은 깨끗함의 순간이다. 키캡에는 기름기가 없고, 화면은 완벽하며, 하드디스크는 새롭고 손대지 않은 상태다. 대략 한 시간 정도는 그렇다.
그 상태로 돌아가 보자.
NixOS는 두 개의 디렉터리만으로 부팅할 수 있다. /boot와 /nix다.
/nix에는 읽기 전용 시스템 설정이 들어 있으며, 이것은 configuration.nix로 지정되고 시스템 세대로 빌드되고 추적된다. 이것들은 절대 바뀌지 않는다. /nix 안에 파일이 한 번 만들어지고 나면, 설정 내용 자체를 바꾸는 유일한 방법은 원하는 내용을 담은 새 시스템 설정을 빌드하는 것이다.
드라이브에서 /nix 바깥에 생성되는 모든 설정이나 파일은 상태이자 잡동사니다. 우리는 /nix와 /boot 바깥의 모든 것을 잃어버려도 건강한 시스템을 가질 수 있다. 내 방식은 어떤 상태가 중요한지 명시적으로 선택하고, 고르고, 그것만 유지하는 것이다.
이것이 가능한 이유는 부팅 순서에 있다.
NixOS에서 부트로더는 표준 Linux 배포판과 같은 기본 단계를 따른다. 커널은 초기 램디스크와 함께 시작되고, 초기 램디스크는 시스템 디스크를 마운트한다.
그리고 여기서 유사점은 끝난다.
NixOS는 부트로더가 몇 가지 추가 정보를 넘기도록 설정한다. 구체적인 시스템 설정이 바로 그것이다. 이것이 NixOS의 부트로더 롤백의 비밀이며, 동시에 매 부팅 때마다 디스크를 지우는 열쇠이기도 하다. 이 매개변수의 이름은 systemConfig다.
매번 시작할 때마다 아주 초기의 부팅 단계는 시스템 설정이 무엇이어야 하는지 안다. 전체 시스템 설정이 읽기 전용 /nix/store에 저장되어 있고, systemConfig를 통해 전달된 디렉터리가 그 설정에 대한 참조를 가지고 있기 때문이다. 그러면 초기 부팅 과정은 /etc와 /run을 선택된 설정에 맞게 조작한다. 보통은 몇 개의 심볼릭 링크를 바꾸는 작업이 포함된다.
하지만 /etc가 아예 존재하지 않는다면, 초기 부팅은 /etc를 생성하고 평범한 다른 부팅처럼 계속 진행한다. 마찬가지로 /var, /dev, /home, 그리고 반드시 존재해야 하는 다른 핵심 디렉터리도 생성한다.
간단히 말해, 비어 있는 /는 NixOS에게 놀라운 일은 아니다. 사실 NixOS netboot, EC2, 그리고 설치 미디어는 모두 이런 방식으로 시작한다.
데이터 저장을 선택적으로 허용하기 전에, 먼저 기본적으로 데이터를 저장하지 않도록 해야 한다. 나는 원하지 않는 데이터를 쉽고 안전하게 지울 수 있으면서, 유지하고 싶은 데이터는 보존할 수 있는 방식으로 파일시스템을 구성해 이 작업을 한다.
내가 선호하는 방법은 ZFS 데이터셋을 사용하고, 마운트되기 전에 빈 스냅샷으로 롤백하는 것이다. 다른 파일시스템의 파티션을 사용해도 똑같이 잘 동작할 수 있다. 부팅 시 mkfs를 실행하거나 그와 비슷한 일을 하면 된다. RAM이 많다면 지우기 단계를 건너뛰고 /를 tmpfs로 만들어도 된다.
NixOS를 설치할 때 나는 디스크를 두 개의 파티션으로 나눈다. 하나는 부트 파티션용이고, 다른 하나는 ZFS 풀용이다. 그다음 몇 개의 데이터셋을 만들고 마운트한다.
내 루트 데이터셋:
# zfs create -p -o mountpoint=legacy rpool/local/root
마운트하기도 전에, 나는 완전히 빈 상태에서 스냅샷을 만든다:
# zfs snapshot rpool/local/root@blank
그리고 나서 마운트한다:
# mount -t zfs rpool/local/root /mnt
그다음 /boot용으로 만든 파티션을 마운트한다:
# mkdir /mnt/boot
# mount /dev/the-boot-partition /mnt/boot
/nix용 데이터셋을 만들고 마운트한다:
# zfs create -p -o mountpoint=legacy rpool/local/nix
# mkdir /mnt/nix
# mount -t zfs rpool/local/nix /mnt/nix
그리고 /home용 데이터셋도:
# zfs create -p -o mountpoint=legacy rpool/safe/home
# mkdir /mnt/home
# mount -t zfs rpool/safe/home /mnt/home
마지막으로, 부팅 사이에 명시적으로 유지하고 싶은 상태를 위한 데이터셋도 만든다:
# zfs create -p -o mountpoint=legacy rpool/safe/persist
# mkdir /mnt/persist
# mount -t zfs rpool/safe/persist /mnt/persist
참고: 내 시스템에서는
rpool/local아래의 데이터셋은 절대 백업하지 않고,rpool/safe아래의 데이터셋은 백업한다.
이제 매 부팅 때마다 루트 데이터셋을 안전하게 지우는 일은 아주 쉽다. 장치가 사용 가능해진 다음, 빈 스냅샷으로 롤백하면 된다:
{
boot.initrd.postDeviceCommands = lib.mkAfter ''
zfs rollback -r rpool/local/root@blank
'';
}
그다음 나는 평소처럼 설치를 마무리한다. 모든 것이 잘 되었다면, 다음 부팅은 빈 루트 파티션으로 시작하지만 그 외의 부분은 당신이 지정한 그대로 설정되어 있을 것이다.
이제 나는 아무 상태도 유지하지 않으므로, 무엇을 유지하고 싶은지 지정할 차례다. 여기서의 선택은 시스템의 역할에 따라 다르다. 노트북이 가지는 상태는 서버와 다르다.
여기 몇 가지 상태의 예와 내가 그것을 보존하는 방법이 있다. 이 예시들은 주로 재설정이나 심볼릭 링크를 사용하지만, ZFS 데이터셋과 마운트 포인트를 사용해도 잘 동작한다.
키를 위해 /persist 아래에 디렉터리를 만든다:
# mkdir -p /persist/etc/wireguard/
그리고 Nix의 wireguard 모듈을 사용해 그곳에 키를 생성한다:
{
networking.wireguard.interfaces.wg0 = {
generatePrivateKeyFile = true;
privateKeyFile = "/persist/etc/wireguard/wg0";
};
}
/etc 구조를 반영하도록 /persist 아래에 디렉터리를 만든다:
# mkdir -p /persist/etc/NetworkManager/system-connections
그리고 Nix의 etc 모듈을 사용해 심볼릭 링크를 설정한다:
{
etc."NetworkManager/system-connections" = {
source = "/persist/etc/NetworkManager/system-connections/";
};
}
/var 구조를 반영하도록 /persist 아래에 디렉터리를 만든다:
# mkdir -p /persist/var/lib/bluetooth
그다음 systemd의 tmpfiles.d 규칙을 사용해 /var/lib/bluetooth에서 내가 유지하는 디렉터리로 심볼릭 링크를 만든다:
{
systemd.tmpfiles.rules = [
"L /var/lib/bluetooth - - - - /persist/var/lib/bluetooth"
];
}
/etc 구조를 반영하도록 /persist 아래에 디렉터리를 만든다:
# mkdir -p /persist/etc/ssh
그리고 Nix의 openssh 모듈을 사용해 그 디렉터리에 키를 만들고 사용한다:
{
services.openssh = {
enable = true;
hostKeys = [
{
path = "/persist/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
{
path = "/persist/ssh/ssh_host_rsa_key";
type = "rsa";
bits = 4096;
}
];
};
}
/var 구조를 반영하도록 /persist 아래에 디렉터리를 만든다:
# mkdir -p /persist/var/lib/acme
그다음 systemd의 tmpfiles.d 규칙을 사용해 /var/lib/acme에서 내가 유지하는 디렉터리로 심볼릭 링크를 만든다:
{
systemd.tmpfiles.rules = [
"L /var/lib/acme - - - - /persist/var/lib/acme"
];
}
처음 몇 주 동안 나는 이 과정이 조금 무섭다고 느꼈다. 매번 재부팅할 때 중요한 데이터를 잃고 있는 걸까? 아니, 그렇지 않았다.
걱정되거나 다음 부팅 때 어떤 상태를 잃게 될지 알고 싶다면, 루트 파일시스템의 파일 목록을 보고 중요한 것이 빠졌는지 확인할 수 있다:
# tree -x /
├── bin
│ └── sh -> /nix/store/97zzcs494vn5k2yw-dash-0.5.10.2/bin/dash
├── boot
├── dev
├── etc
│ ├── asound.conf -> /etc/static/asound.conf
... snip ...
ZFS도 비슷한 답을 줄 수 있다:
# zfs diff rpool/local/root@blank
M /
+ /nix
+ /etc
+ /root
+ /var/lib/is-nix-channel-up-to-date
+ /etc/pki/fwupd
+ /etc/pki/fwupd-metadata
... snip ...
당신은 보존하려고 했던 새로운 상태를 마주칠 수도 있다. 나는 새로운 서비스를 추가할 때, 그것이 어떤 상태를 기록하는지 그리고 내가 그것을 신경 쓰는지 생각한다. 신경 쓴다면, 그 상태를 /persist로 보내는 방법을 찾는다.
이 기계들을 어느 정도 규칙적으로 재부팅하도록 주의하라. 그러면 시스템 상태가 올바르게 추적되고 있다는 사실을 계속 입증할 수 있고, 시스템도 민첩하게 유지된다.
이 기법은 하드웨어로 가득한 데이터센터 없이도, 그리고 중요한 상태를 실제로 지니는 시스템에서도, 매 부팅마다 내게 “새 컴퓨터 냄새”를 가져다주었다. 나는 이 전략을 크고 작은 다양한 시스템에 배포했다. 빌드 팜 서버, 데이터베이스 서버, 내 NAS와 홈 서버, raspberry pi 차고문 개폐기, 그리고 노트북까지.
NixOS는 정말 많은 방식으로 강력한 새로운 배포 모델을 가능하게 하며, 온갖 형태와 크기의 시스템을 올바르고 일관되게 관리할 수 있게 해 준다. 나는 이 일시적 루트 모델이 이런 유연성과 힘을 보여 주는 또 하나의 사례라고 생각한다. 나는 이 파티셔닝 방식이 참조 아키텍처가 되어, 우리를 이 끝없는 레거시 수렁에서 꺼내 주기를 바란다.