Loading
2022. 5. 4. 23:52 - lazykuna

리눅스 커널 바로 위에 executable 얹기

이 글은 Ravelin Tech Blog에서 가져온 글입니다. 원문: Go in a scratch VM

보통 바이너리를 경량화된 이미지로 배포하라고 하면 아마 multi-stage docker 빌드를 하게 됩니다. 극한으로 더 경량화하려면, 정적 라이브러리들을 몽땅 때려박아 컴파일 한 후 scratch 이미지를 이용해서 빌드해 볼 수 있습니다. 하지만 이보다 더 경량화된 방법을 할 수 있을까요? 커널이 바로 우리가 지정해준 executable을 실행한다면 현실적으로 가장 경량화된 방법이 될 텐데, 이 과정에서 부팅프로세스 등 재미있는 내용들을 많이 다루고 있어 별도로 정리해 둡니다.

리눅스의 부팅 과정

아주 간단하게 보면 위 그림과 같습니다. Bootloader이 Kernel을 읽어들이고, 그 위에서 initramfs이 일종의 램디스크 개념으로 루트 파일시스템을 준비합니다 (최근의 커널에 적용된 개념입니다). 그 이후, 본격적으로 /sbin/init 이 실행되면서, 이를 기반으로 fork를 하며 필요한 각종 프로그램 및 서비스가 시작되게 됩니다. (근래에는 systemd 방식으로 바뀌었다고 합니다.)

아무튼, Docker에서 쓰이는 scratch 이미지와 같이 극한으로 경량화된 이미지는 그러한 복잡한 구조가 필요하지 않으므로 initd 기반을 사용하고 있는데, 굳이 initd를 실행해서 간접적으로 Go 바이너리를 실행시키지 않고, 바로 Go 바이너리만으로 대체해 보자는 게 여기에서의 취지입니다.

  • 물론, 뒤에서 이야기 되겠지만 이 방식은 드라이버나 각종 환경(이미지 등)의 변화에 대응하기 어려운 점이 있어 권장되는 방법은 아닙니다! 그냥 부팅 세부 과정 등에 대해서 알아볼 수 있는 것을 의의삼는걸로...

추가: Containerd의 구조

흔히 사용되는 컨테이너에서 사용하는 구조는 위와 같이 containerd에서 runC를 수행하고, 이것이 실질적으로 컨테이너의 프로그램을 구동하는 역할을 하게 됩니다. 결국 runC가 내부적으로 initd를 거쳐 컨테이너 프로세스를 실행하기 때문에, initd 부터는 동일 과정을 거치게 됩니다

프로그램 직접 만들어 넣기

간단한 Hello World와 같은 경우는 외부 드라이버에 의존하는 것이 거의 없습니다. 커널의 기본 기능(stdout, stdin)을 사용하는 것이 전부이기 때문에, 바로 가능할 것으로 보입니다. 실제로 이 경우, 연관 static library를 몽땅 때려넣고, init 대신 replace 하면 작동합니다!

그렇다면 네트워크에 접근하면 어떻게 될까요? 디바이스가 없다는 오류가 납니다. 무엇인가 initd가 해주는 일이 있는 것으로 보입니다.

scratch 이미지의 initd를 보면 실제로 아래와 같은 파일들을 볼 수 있습니다. 해당 프로그램들이 해주는 일을 프로그램에서 직접 해 주도록 하면 (드라이버 로드), 실제로 해당 기능도 문제없이 작동하는 것을 확인할 수 있습니다.

자세한 내용은 원본 포스트(Ravelin tech blog — Go in a scratch VM)를 참고해 주세요.

참고