이 글은 구글의 컨테이너 빌드에 대한 권장사항을 기반으로 하고 있고, 그 권장사항들에 대해서 정리합니다.
이전에 도커파일에서 다뤘던 CMD와 ENTRYPOINT 와 깊은 연관이 있으니 그 부분과 함께 공부하면 좋습니다.
이 글은 컨테이너를 실행과 동시에 꺼지는 상황을 해결하는데 해결책을 제시할 수 있습니다.
그 중 중요한 부분인 PID1 에 대한 내용도 함께 다뤄보겠습니다.
컨테이너 빌드에 대한 권장사항
컨테이너 작업을 시작할 때 사용자가 자주 범하는 실수는 컨테이너를 여러 가지 작업을 동시에 실행할 수 있는 가상 머신(VM)으로 처리하는 것입니다. 컨테이너는 이러한 방식으로 작동할 수 있지만 그렇게 하면 컨테이너 모델의 장점이 상당 부분 줄어듭니다.
예를 들어 기본 Apache/MySQL/PHP 스택을 선택해 단일 컨테이너에서 모든 구성요소를 실행하려고 할 수 있습니다.
하지만 PHP-FPM을 실행하는 경우 Apache용 컨테이너 하나, MySQL용 컨테이너 하나, PHP용 하나 등과 같이 2개나 3개의 컨테이너를 사용하는 것이 가장 좋습니다.
이전 글에서 몇 번 언급한 적이 있는데, 컨테이너는 한번에 하나의 앱만을 패키징 하는 것이 좋습니다.
오늘은 그 이유에 대해서 먼저 이야기 해보겠습니다.
따라서 초기 설계의도를 고려해서, 각 컨테이너에는 하나의 앱만을 포함하여야만 합니다.
컨테이너가 시작될 때, 앱이 시작되어야만 하고, 앱이 중단되면, 컨테이너도 중단되어야만 합니다.
이것은 컨테이너를 사용하는데 강력한 권고사항입니다.
다음 그림에서 왼쪽 그림이 컨테이너에서 권장사항을 지키는 프로세스의 모습입니다.
그에 반해 오른쪽의 안 좋은 예시로는 부모 프로세스가 두 개여서, 두 개의 앱을 실행하고 있습니다.
왜 하나의 컨테이너에는 하나의 앱, 즉 하나의 부모 프로세스 만을 가지는 것이 좋다고 할까요?
이것을 더 쉽게 위의 그림을 보면서 예시를 들어 설명해보겠습니다.
컨테이너에서 실행 중인 프로세스 중 하나에서 갑자기 오류가 발생해서 응답이 없어졌습니다. 그런데 오류가 난 프로세스가 컨테이너에서 상당히 중요한 역할을 하는 프로세스여서, 더이상 컨테이너는 제 역할을 하지 못합니다.
이런 상황이라면 당연하게도, 컨테이너가 오류가 난 상태를 확인해서 컨테이너의 오류를 처리를 해야만 합니다.
주의사항 : 단, 프로세스 관리 시스템인 supervisor를 사용하거나, bash 스크립트를 컨테이너 진입점으로 해서 백그라운드로 앱을 실행하는 방법을 통해 조치할 수 있지만, 이 방법을 권장되지 않습니다.
이것을 보게 되면 왜 inception 서브젝트에서 위와 같이 명시했는지 볼 수가 있습니다.
왜 이 멘트가 있는지 알 수가 있는 부분이었습니다.
그런데 왜 여러개의 어플리케이션을 실행하면, 컨테이너가 오류가 났다는 사실을 감지하지 못하는 것일까요?
이것에 대한 이해를 하기 위해서는 유닉스의 프로세스 구조에 대해서 알아야만 합니다.
유닉스의 프로세스 구조 (트리구조, 데몬프로세스, PID1)
트리 구조로 유지되는 프로세스들은 부모 프로세스가 죽으면 모두 종료됩니다.
그렇기 때문에 서비스로 유지해야 하는 프로세스들을 별도의 루트를 만들어 동작하는 방식을 사용합니다.
이때 사용하는 루트 프로세스를 데몬 프로세스라고 합니다. (여기서 루트 프로세스와 PID1은 같은 맥락으로 사용합니다.)
일반적인 유닉스 구조는 PID 1로 데몬 프로세스를 실행합니다.
하지만, 도커 컨테이너 내부에서는 따로 직접적으로 설정해서 데몬 프로세스 가 설정되어있지 않다면, 기본적으로 PID 1로써 실행하지 않습니다.
도커 컨테이너는 PID1로써 컨테이너를 실행할 때 처음 사용하는 명령으로 설정합니다.
즉 도커파일의 ENTRYPOINT 또는 CMD를 이용해서 설정해줄 수 있습니다.
이전에 도커파일 게시글에서 ENTRYPOINT 파트에서 설명 했듯, 어플리케이션이 컨테이너인 경우 CMD보다는 ENTRYPOINT 로 PID1번을 설정해주는 것을 더 권장했습니다.
그렇기 때문에 컨테이너를 제대로 관리하기 위해서는 PID1번에 데몬 프로세스 또는 그에 맞는 기능을 하는 프로세스를 지정해주어야만 합니다.
그렇다면 어떻게 컨테이너를 설정하는 것이 더 잘 관리할 수 있는 방법일까요?
컨테이너 관리 권장사항
1. 리눅스 signal 처리
첫째로, 리눅스 signal을 제대로 처리 해야 합니다. 리눅스에서 시그널은 컨테이너 내부의 프로세스 수명 주기를 제어하는 주요 방법입니다.
앱의 수명 주기를 앱이 포함된 컨테이너와 동일하게 연결하려면 앱이 Linux 신호를 올바르게 처리하도록 해야만 합니다.
리눅스 시그널 중 가장 중요하게 처리해야하는 시그널은 프로세스를 종료하는 SIGTERM입니다.
그리고 앱은 프로세스를 비정상적으로 종료하는 데 사용되는 SIGKILL 신호 또는 Ctrl+C를 입력할 때 전송되는, SIGINT 신호를 수신하도록 할 수 있습니다.
기본적으로 도커에서 시그널로 PID1과 통신을 하기 때문에, 만약 PID1 프로그램을 설정해야한다면, 시그널을 제대로 처리하는 프로세스인지 확인할 필요가 있습니다.
[문제 1] 리눅스 커널의 시그널 처리 방법
Linux 커널이 신호를 처리하는 방법은 PID 1을 가진 프로세스와 그렇지 않은 프로세스에서 차이가 있습니다. 신호 핸들러가 PID1을 가진 프로세스에 자동으로 등록되지 않으므로 SIGTERM 또는 SIGINT 같은 신호는 기본적으로 아무런 영향을 미치지 않습니다.
따라서 PID1을 가진 프로세스를 종료하려면 기본적으로, 단계적 종료를 방지하는 SIGKILL을 사용하여 프로세스를 강제 종료해야 합니다.
다만, 앱에 따라 SIGKILL을 사용하면 모니터링 시스템에 사용자 표시 오류, 쓰기 중단(데이터 저장용), 원치 않는 알림이 발생할 수 있는 문제가 있습니다.
2. PID의 개념과 활용
둘째로, PID1을 활용해야 합니다.
계속해서 PID1에 대해서 설명했지만 아주 중요한 개념이기 때문에 PID의 개념에 대해서 더 자세히 설명하고, 문제점을 해결하는 방법에 대해서 설명하도록 하겠습니다.
프로세스 식별자(PID)는 Linux 커널이 각 프로세스에 제공하는 고유한 식별자입니다.
컨테이너에는 호스트 시스템의 PID에 매핑되는 고유한 PID 세트가 있습니다.
일반적으로 정상적인 운영체제의 경우 이 프로세스는 init 시스템(예: systemd 또는 SysV)입니다.
[문제 2] 기본 init 시스템의 고아 프로세스 관리
systemd와 같은 기본 init 시스템은 분리된 좀비 프로세스를 제거(회수)하는 데에도 사용됩니다.
상위 프로세스가 사라져 분리된 프로세스 즉 ,고아 프로세스는 PID 1이 있는 프로세스를 다시 부모 프로세스로 가지게 됩니다. PID 1은 프로세스가 사라질 때 다시 자원을 회수할 책임이 있습니다. 정상적인 init 시스템은 그렇게 작동합니다.
따라서 컨테이너에서도 PID 1을 갖고 있는 프로세스가 이러한 책임을 다해야만합니다. 이러한 제거를 제대로 처리하지 못하면 메모리나 다른 리소스가 부족해질 수 있습니다.
문제 해결 솔루션
솔루션 1: PID 1으로 실행하고 시그널 핸들러로 등록
이 방법은 첫번째 문제인 리눅스 커널의 신호 처리 문제만을 해결할 수 있습니다.
다만, 앱이 제어를 해서 하위 프로세스를 생성하면 두 번째 문제를 방지할 수 있습니다.
이 솔루션을 구현하는 가장 쉬운 방법은 Dockerfile에서 CMD 또는 ENTRYPOINT 안내를 사용하여 프로세스를 실행하는 것입니다.
예를 들어 다음 Dockerfile에서 nginx는 실행할 수 있는 최초이자 유일한 프로세스입니다.
FROM debian:11
RUN apt-get update && \
apt-get install -y nginx
EXPOSE 80
CMD [ "nginx", "-g", "daemon off;" ]
다만, 때로는 프로세스가 제대로 실행될 수 있도록 컨테이너에서 환경을 준비해야 할 수 있습니다. 이 경우 컨테이너를 시작할 때 셸 스크립트를 실행하는 것이 가장 좋습니다. 이 셸 스크립트는 환경을 준비하고 기본 프로세스를 실행하는 작업을 담당합니다.
하지만 이 방법을 사용하는 경우 셸 스크립트는 프로세스가 아닌 PID 1을 가지므로 기본 exec 명령어를 사용하여 셸 스크립트에서 프로세스를 실행해야 합니다. exec 명령어로 스크립트를 원하는 프로그램으로 바꿉니다. 그 다음 프로세스에서 PID 1을 상속합니다
솔루션 2: 특수한 init 시스템 사용
기본적인 Linux 환경에서와 마찬가지로 init 시스템을 사용하여 이러한 문제를 처리할 수도 있습니다.
특수한 init 시스템을 사용하는 경우 init 프로세스는 PID 1을 가지며 다음과 같은 역할을 수행합니다.
올바른 신호 핸들러를 등록합니다.
앱에서 신호가 작동하는지 확인합니다.
최종 모든 좀비 프로세스를 수거(reap)합니다.
그외의 솔루션으로 쿠버네티스의 프로세스 네임스페이스 공유 사용 설정 등의 방법이 있습니다.
또는 dumb-init 을 사용하는 방법도 존재합니다.
컨테이너 실행에 대한 권장사항
컨테이너를 관리하는 로깅, 모니터링
컨테이너의 로그를 관리하는 것은 어플리케이션 관리의 필수적입니다. 따라서 권장되는 사항으로는 컨테이너의 로그를 다른 파일시스템에 저장하는 것을 권장합니다.
다만 저희 개발 단계에서는 의미가 있다고 생각했으나 현재 과제와는 맞지 않다고 생각해 굳이 설정을 해주지는 않았습니다.
모니터링
로깅과 마찬가지로 모니터링은 애플리케이션 관리의 필수적인 부분입니다.
컨테이너화된 애플리케이션 모니터링은 많은 측면에서 컨테이너화되지 않은 애플리케이션 모니터링에 적용되는 것과 동일한 원칙에 따릅니다.
그러나 컨테이너화된 인프라는 일반적으로 매우 동적이고 컨테이너가 자주 생성 또는 삭제되므로 매번 모니터링 시스템을 다시 구성하기는 어렵습니다.
모니터링은 크게 블랙박스 모니터링과 화이트박스 모니터링, 두 가지로 분류됩니다.
블랙박스 모니터링은 최종 사용자처럼 외부에서 애플리케이션을 살펴보는 것을 나타냅니다. 블랙박스 모니터링은 제공하고자 하는 최종 서비스가 사용 가능하고 작동하는 경우 유용합니다. 블랙박스 모니터링은 인프라 외부에서 작동하므로 기존 인프라와 컨테이너화된 인프라를 구분하지 않습니다.
화이트박스 모니터링은 일종의 높은 권한 액세스를 사용하여 애플리케이션을 살펴보고 최종 사용자가 볼 수 없는 애플리케이션 행동에 대한 측정항목을 수집하는 것을 나타냅니다. 화이트박스 모니터링은 인프라의 가장 깊은 레이어를 살펴봐야 하므로 기존 인프라와 컨테이너화된 인프라의 차이가 큽니다. 주로 쿠버네티스에서 많이 사용됩니다.
보안문제 권장사항
컨테이너에게 권한을 너무 많이 주지 않는 것을 권장합니다.
너무 높은 권한을 가진 컨테이너는 호스트 머신의 모든 디바이스에 엑세스 할 수가 있게 되는데 이것은 보안적으로 상당히 위험한 행동입니다.
컨테이너 내에서 루트로 프로세스를 실행하지 않는 것을 추천합니다. 왜냐하면 도커와 리눅스 커널 자체에서 취약점이 있어서 컨테이너 밖으로 나가서 로컬으로 접속하는 사고가 발생할 수 있기 때문입니다.
하지만 루트로 실행하지 않는 것은 현실적으로 어렵습니다. 왜냐하면 유명 소프트웨어 패키의 상당수가 루트로 프로세스를 실행하기 때문입니다.
이미지 버전 권장사항
FROM debian:latest 보다는 FROM debian:11.6 이렇게 특정 버전을 정확하게 지정해주는 것이 두가지 버전이 나오지 않는 방법입니다.