홈 서버의 변화들

2021-10-13, Ubuntu부터 가상화까지 제가 홈 서버를 운영하면서 변화한 점을 다루었습니다.

*이 글의 헤더 이미지는 [email protected]에서 제공되었습니다.

Story 목록으로 돌아가기

저는 생각보다 꽤 오래 홈 서버를 운영해왔습니다. 가장 큰 이유로는 온라인으로 결제할 수 있는 수단이 없었다는 것입니다. 저는 주로 이제 더 이상 사용하지 않는 노트북들로 저전력의 서버를 만들어왔습니다. 그리고 지금 Ubuntu만으로도 힘들어 하던 정말 작은 Intel B815에서 꽤 만족하는 Intel i7 8550U까지 걸어오게 되었습니다.

Historical flow

2018 Mid~: Ubuntu Server 16

저의 첫 서버는 Intel B815에 2GB RAM을 가진 서버였습니다. 당시 할 줄 아는것도 많지 않았지만 노트북을 바꾸게 되어서 바로 이전에 개발하며 사용하던 노트북을 서버의 용도로 쓸 수 있다는 것을 알았을 때 바로 Ubuntu Server를 설치했습니다.

그 때를 돌아보면 apt install php...를 제일 손에 익히고 살지 않았나 싶기도 하고 VestaCP와 같은 웹 패널에 익숙하던 시기였습니다.

Slide 8

2020 Mid~: Alpine Linux

Ubuntu Server 20 LTS가 출시하고 나서 저는 Python을 포함한 많은 디펜던시의 버전이 업그레이드된다는 소식을 듣고 즉시 업그레이드를 결정했습니다. 하지만 새로운 버전의 Ubuntu Server는 사용하기에는 너무나도 불안정했습니다. 그리고 거의 수개월을 기다렸지만 그 이후에도 서버에 커널 패닉 이슈 등이 생기는 등 더 이상 서버가 Ubuntu Server 20으로 장거리를 뛸 수가 없다는 것을 알고 다른 운영체제를 고민했습니다. 그 결과로 성능을 높게 평가해 Alpine Linux가 선정되었습니다.

초기에 Alpine을 사용하는 것은 그 자체로 고통이었습니다. 이전에 nano가 기본적으로 설치되지 않은 서버를 쓰는 것도 고통이었지만 sudo도 없는 기분은 또 처음이었습니다. 하지만 이런 아무것도 없는 Alpine으로 인해 조금이나마 쉘 환경에 익숙해지게 되었습니다.

2021 Early~: Docker

2021년에는 Docker를 만났습니다. 비록 지금은 표준에서 멀어지고 유료화를 감행하여 Kubernetes로 돌아서게 되었지만 2종 가상화인 컨테이너라는 개념을 처음으로 만들어준 소프트웨어였습니다. Docker를 도입한 이후 저의 개발은 기존과 다르게 컨테이너 등에서의 실행을 어느정도 고려하게 되었습니다. 그리고 Docker 자체의 기능은 그렇게 풍부하지 않지만 Health-check라는 개념을 사용해볼 수 있는 기회였습니다.

명령어를 손에 익히는 것은 어려웠지만 그렇게 큰 문제는 아니었습니다. Docker를 사용하고 나서 저는 서버를 재설치하는 것에 대해 다시 생각해보게 되었습니다.

2021 Mid~: Libvirt

M1 맥북을 구매하며 서버를 i7 8550U로 업그레이드하였습니다.

서버를 재설치하는 것은 저에게 굉장히 귀찮은 일 중 하나였고 결국 서버를 재설치하는 것을 피하기 위해서 Libvirt를 Alpine Linux에 설치하여 사용했습니다. 또 VM마다 IP가 할당될 수 있도록 메인 인터페이스를 네트워크 브릿지로 만들고 DHCP 신호를 전달하도록 iptables를 수정했던 기억이 있습니다. 당시에는 Arch Linux의 Wiki 또한 굉장히 많이 참고했는데 둘의 구조가 묘하게 비슷한 까닭에 저는 수월하게 문제들을 해결할 수 있었습니다.

하지만 여전히 Alpine의 서브 릴리즈를 업그레이드할 때마다 그 다음 부팅이 정상적으로 되지 않는 문제가 하나 있었습니다. 저는 추천으로 Void Linux 혹은 Arch나 Debian으로 넘어가는 것을 시도했지만 여전히 제 취향은 아니었습니다.

2021 Late~: LXD + K3s

마지막으로 현재는 LXD를 통한 컨테이너 관리에 들어갔습니다. VM은 너무 무겁고 Docker는 컨테이너를 VM처럼 쓸 수 있도록 저를 내버려두지 않았는데 LXD는 이를 동시에 만족하는 컨테이너 기술이였습니다. 마이그레이션 중에 Alpine Linux에서 LXD를 실행할 경우 본래 Ubuntu를 위해 개발된 소프트웨어라는 점에서 호환성 문제를 피해가는 것이 쉽지가 않았습니다. 특히나 Systemd와 OpenRC의 차이점으로 인해서 많은 부분을 수정하고 나서 과연 이것이 충분히 효율적인가를 생각했을 때 아니었기에 바로 Debian을 호스트로 차용하고 Alpine Linux를 게스트로 사용하는 것으로 변경했습니다. Debian으로 변경 후에 서버 온도가 몇 도 증가한 것 그리고 Systemd를 사용하는 것이 마음에 들지는 않았지만 호환성을 생각했을 때 충분하다고 생각합니다.

Alpine Linux에서 얻은 경험으로 저는 Debian에서도 내부 브릿지를 쉽게 설정하고 외부 IP를 컨테이너에 할당했습니다. 그리고 ZFS 스토리지 풀을 사용하고 Docker 프로젝트를 천천히 Kubernetes로 이전을 하며 조금 더 다양화된 서버 경험을 만들어나가고 있습니다.

Configuration

제가 처음 달성하고자 하는 목표는 물리적 재설치가 필요없는 서버를 만들어나가는 것입니다. 여기에서는 그동안 사용했던 VM 기반의 가상화(Libvirt)가 아닌 LXD를 통한 컨테이너 방식의 2종 가상화를 사용합니다.

1종 가상화가 아닌 이유는 홈 서버이기 때문입니다. 집에서 돌아가기 때문에 더 조용하고 온도도 낮으며 동시에 가벼워야 합니다. 모든 것이 성능의 문제입니다.

LXD: (Host) Virtualization Layer

호스트 운영체제로는 데비안 혹은 우분투를 사용합니다. LXD의 첫 타겟은 Systemd의 우분투였으며 높은 안정성을 성취하기 위해서는 데비안 계열의 미니멀한 이미지를 사용해야 합니다.

Snapcraft 문서를 참조하여 Snap을 설치하고 LXD도 곧바로 설치합니다.

글은 언제나 오래될 수 있으므로 가급적 공식 문서를 한 번 확인해주세요.

whoami # -> root

# Install required packages
apt update && apt install -y ca-certificates curl wget bridge linux-headers-<arch>
apt install -y zfsutils-linux

# Install snap
apt install snapd && snap install core

# Install LXD and initialize
snap install lxd && lxd init

# Enable service
systemctl enable lxd

LXD를 초기 설정할 때에는 반드시 dir가 아닌 zfs 혹은 btrfs를 스토리지 백엔드로 사용해야 더 빠른 성능을 얻고 컨테이너 별 quota 또한 설정이 가능합니다. 브릿지와 같은 경우에는 내부와 외부를 모두 사용할 것이므로 기본 설정으로 설정을 완료해주세요.

스토리지 백엔드의 기능 비교는 여기에서 더 자세히 확인할 수 있습니다.

Bridge: (Host) L2 Networking Layer

이 작업은 업스트림 네트워크 제공자에서 DHCP를 제공하는 경우 한 포트를 사용하며 하위 컨테이너나 네트워크 인터페이스에 외부 IP를 할당하기 위해 진행됩니다.

# Enable tun module
modprobe tun
echo tun | sudo tee -a /etc/modules

tun 모듈을 로드한 뒤에 /etc/network/interfaces에 새 브릿지를 설정합니다. 여기에서는 eth0이 물리 네트워크 인터페이스인 것으로 가정합니다.

auto lo
iface lo inet loopback

auto br0
iface br0 inet dhcp
  bridge_ports eth0
  bridge_stp 0

그 다음 네트워크 서비스를 재시작하고 업스트림 네트워크 제공자에서 적절한 IP 주소를 서버가 할당받는지 확인해주세요.

service networking restart
ip a

Permission: (Host)

마지막으로는 적절한 권한을 LXD 컨테이너를 실행할 사용자에게 할당합니다.

adduser <username> lxd

Container: (Host)

이제 새로운 컨테이너를 생성합니다. LXD는 이미지를 Docker나 기존 apt처럼 외부 저장소에서 받아오곤 하는데 이는 lxc remote list로 확인할 수 있습니다.

cgroup v2 지원에 관한 경고는 LXD가 아닌 Snap에서 표시되는 메세지이기 때문에 무시해도 무방합니다.

[email protected]:~$ lxc remote list
WARNING: cgroup v2 is not fully supported yet, proceeding with partial confinement
+-----------------+------------------------------------------+---------------+-------------+--------+--------+--------+
|      NAME       |                   URL                    |   PROTOCOL    |  AUTH TYPE  | PUBLIC | STATIC | GLOBAL |
+-----------------+------------------------------------------+---------------+-------------+--------+--------+--------+
| images          | https://images.linuxcontainers.org       | simplestreams | none        | YES    | NO     | NO     |
+-----------------+------------------------------------------+---------------+-------------+--------+--------+--------+
| local (current) | unix://                                  | lxd           | file access | NO     | YES    | NO     |
+-----------------+------------------------------------------+---------------+-------------+--------+--------+--------+
| ubuntu          | https://cloud-images.ubuntu.com/releases | simplestreams | none        | YES    | YES    | NO     |
+-----------------+------------------------------------------+---------------+-------------+--------+--------+--------+
| ubuntu-daily    | https://cloud-images.ubuntu.com/daily    | simplestreams | none        | YES    | YES    | NO     |
+-----------------+------------------------------------------+---------------+-------------+--------+--------+--------+

주소를 확인한 뒤, 필요한 이미지가 있는지 접속하여 확인합니다. 그런 다음 새 컨테이너를 생성합니다.

lxc launch <remote_name>:<image_name>/<image_version> <container_name> [options]

Limitations

컨테이너를 생성하는 것은 사실 그렇게 어렵지 않습니다. 하지만 여러 명이 한 서버를 사용해야 한다면 말이 달라집니다. 컨테이너 별로 자원 관리를 하여 호스트 서버가 위험한 상태에 도달하지 않게 만들어야 합니다.

lxc config 명령을 통해 제한을 조절할 수 있습니다.

lxc config set <container_name> <key> <value>

CPU

Core limitation

  • key: limits.cpu
  • value: 0 (cores)

CPU pinning, 컨테이너가 CPU의 특정 코어만 사용하도록 하는 것입니다.

  • key: limits.cpu
  • value: 0-1 (cores)

CPU time

  • key: limits.cpu.allowance
  • value: 50ms/100ms

Memory

Memory limitation

  • key: limits.memory
  • value: 2GB (단위의 기본값은 MBytes입니다)

Special

Nested virtualization, Docker 등을 컨테이너 내부에서 사용할 경우 필요합니다.

  • key: security.nesting
  • value: true

Limitations: Device

컨테이너에 연결될 기기는 처음 컨테이너를 시작할 때가 아니면 기본 프로파일에서 할당되기 때문에 기본 기기를 덮어씌워야 합니다.

Disk

가장 먼저 기본 프로파일에서 할당된 디스크 대신 컨테이너에 개별로 할당될 디스크를 만들어야 합니다.

lxc config device add <container_name> root disk pool=default path=/

그리고 quota를 설정할 수 있습니다.

lxc config device set <container_name> root size 32GB

Network

네트워크 또한 디스크와 마찬가지로 기본 프로파일에서 할당된 장치 대신 컨테이너 별로 관리가 가능하도록 새로운 장치를 추가합니다. lxdbr0은 LXD가 생성하는 브릿지의 기본 이름입니다.

만약 외부 IP를 할당하려면 여기에서 parent 값을 위에서 생성한 br0으로 설정하면 됩니다. 그럼 컨테이너에 할당된 인터페이스가 외부 DHCP 서버와 직접 통신을 하며 외부 IP를 할당 받을 수 있습니다.

lxc config device add <container_name> eth0 nic name=eth0 nictype=bridged parent=lxdbr0

Ingress

lxc config device set <container_name> eth0 limits.ingress 200Mbit

Egress

lxc config device set <container_name> eth0 limits.egress 500Mbit

Id Mapping

마지막으로는 컨테이너와 호스트 사이의 폴더 혹은 파일을 컨테이너에 마운트해서 사용할 경우입니다. 그런데 기본적으로 unprivileged 상태의 컨테이너의 경우에는 운영체제의 UID/GID가 호스트와 완전히 다른 영역에 맵핑되어 호스트의 파일을 사용할 수 없게 됩니다.

[email protected]:~$ lxc launch images:ubuntu/focal test
WARNING: cgroup v2 is not fully supported yet, proceeding with partial confinement
Creating test
Starting test

[email protected]:~$ lxc config device add test home disk source=/mnt/sda/containers/ path=/home/ubuntu
WARNING: cgroup v2 is not fully supported yet, proceeding with partial confinement
Device home added to test

[email protected]:~$ lxc exec test bash
WARNING: cgroup v2 is not fully supported yet, proceeding with partial confinement

[email protected]:~# ls -al /home
total 6
drwxr-xr-x  3 root   root       3 Oct 15 07:46 .
drwxr-xr-x 17 root   root      23 Oct 15 07:52 ..
drwxr-xr-x  2 nobody nogroup 4096 Oct 12 07:41 ubuntu

성공적으로 디렉터리를 마운트했지만 위와 같이 nobodynogroup이 표시되며 실제로 사용할 수 없는 폴더가 됩니다. 이 때에는 컨테이너를 실행하는 사용자의 Id를 컨테이너에서 접근 가능하도록 만들어주고 LXD가 새로 설정된 Id를 다시 맵핑하도록 해야 합니다.

whoami // -> gohojeong

# Configure Id Map range
[email protected]:~$ printf "lxd:$(id -u):1\nroot:$(id -u):1\n" | sudo tee -a /etc/subuid
lxd:1001:1
root:1001:1

[email protected]:~$ printf "lxd:$(id -g):1\nroot:$(id -g):1\n" | sudo tee -a /etc/subgid
lxd:1001:1
root:1001:1

# Restart LXD
[email protected]:~$ sudo service lxd restart

# Set new Id Map
[email protected]:~$ printf "uid $(id -u) 1000\ngid $(id -g) 1000" | lxc config set test raw.idmap -

# Restart container, only restarting LXD will just save container's state
[email protected]:~$ lxc restart test

프로젝트 스토리를 읽어주셔서 정말 감사합니다.
다른 스토리도 읽어보시겠어요?

프로젝트 스토리는 개발을 진행하면서 새롭게 제시하는 아이디어와 개발하는 중 가치있었던 경험을 적어나가는 곳입니다.

Story 목록으로 돌아가기Typed.sh에서 더 많은 글 보기

번들링없는 프론트엔드로의 전환

2021-09-10, 번들링없는 프론트엔드는 HTTP/2 스펙과 함께 웹 브라우저의 캐싱 전략을 최대한으로 끌어올릴 수 있게 해줍니다.

대규모 채팅 플랫폼 작성

2021-10-07, 대규모 커뮤니케이션 플랫폼에서 사용될 아키텍쳐와 방식들을 직접 구상하고 작성해보았습니다.

Next.JS에서의 끊김없는 배포 환경 전환

2021-09-09, Next.JS 사용 환경에서 Static Export와 Server Side Rendering 사이의 차이를 극복하여 개발자 경험과 확장성을 최대화했습니다.