호스트에 mysql 과 redis-cli 같은 명령줄 도구를 몇 개 깔려고 했는데 docker exec 로 컨테이너에 들어가 명령을 치는 것이 번거로웠기 때문이다. 깔기 전에 위험 평가를 했고 그 평가가 실제로 필요했다.
Table of contents
Open Table of contents
배경 — 겹치는 데이터 디렉터리
컨테이너 몇 개가 호스트의 /var/lib/mysql 과 /var/lib/redis 를 -v 로 직접 마운트해서 데이터 저장에 쓰고 있었다. 호스트에 같은 패키지를 깔면 그 패키지도 관례상 같은 경로를 데이터 디렉터리로 잡는다.
배포판이 달라도 이런 경로는 대체로 같기 때문에 겹칠 가능성이 높았다. 겹치는 경로를 두 주체가 서로 모른 채로 쓰는 것이 이 상황의 위험이었다.
상황 — 세 곳이 같은 식별자를 쓰는 상태
패키지를 깔면 mysql 서비스 사용자가 만들어지고 그 사용자가 데이터 디렉터리를 소유하게 된다. ls -ln /var/lib/mysql 과 getent passwd 로 지금 상태를 보니 디렉터리 소유자 번호를 호스트의 다른 시스템 데몬이 이미 점유하고 있었다.
docker exec 로 본 컨테이너 안 mysqld 프로세스의 id -u 도 같은 번호였고 디렉터리는 그 번호에 700 으로 권한이 좁게 걸려 있었다. 세 곳이 같은 식별자에 물려 있는 상태였다.
원인 — 설치가 소유권을 바꾸는 경로
useradd(8) 매뉴얼은 --system 계정의 번호를 SYS_UID_MIN 과 SYS_UID_MAX 사이에서 고르되 이미 쓰인 값은 피한다고 설명하므로 그 번호가 차 있으면 다른 번호를 받게 된다. 설치 스크립트가 데이터 디렉터리를 새 사용자 소유로 chown 하면 컨테이너 프로세스가 자기 데이터에 접근을 잃는다.
권한이 좁게 걸려 있어서 그룹이나 기타 사용자로는 읽지도 못하므로 컨테이너가 다음 쓰기 시점에 죽는다. 설치 직후가 아니라 나중에 죽는다는 점이 이 사고를 찾기 어렵게 만든다.
검증 — 파일 상태와 프로세스 응답
실제로 apt install 로 깔고 나서 다시 보니 소유권이 그대로였고 서비스 사용자도 안 만들어졌으며 systemctl status 로 본 서비스는 비활성 상태였다. 디렉터리가 이미 있고 번호가 차 있어서 설치 스크립트가 넘어간 것으로 보인다.
그렇다고 안 건드렸으니 괜찮다로 끝내지 않고 컨테이너에 SELECT 1 과 PING 을 직접 보내 응답을 확인했다. 파일 상태가 안 바뀐 것과 프로세스가 정상인 것은 다른 사실이라 둘 다 봐야 했다.
결론 — 클라이언트만 두는 원칙
이번에는 괜찮았지만 다른 패키지였으면 소유권 변경이 돌았을 수도 있다. 두 가지 방법이 있었는데 서버 패키지째 깔고 서비스만 비활성으로 두는 것과 mysql-client 처럼 클라이언트 패키지만 두는 것이다. 뒤쪽을 골랐다. 이유는 서버 패키지의 설치 스크립트가 데이터 디렉터리를 만지는 경로를 아예 밟지 않아서다. 대신 호스트에서 서버 쪽 도구를 쓰고 싶을 때마다 docker exec 로 돌아가야 하는 것을 감수했다.
서버가 뜨면 같은 데이터 디렉터리를 두 프로세스가 쓰게 되어 확실히 깨진다. 패키지가 없는 도구는 실행 파일을 직접 풀거나 인터페이스를 직접 호출하는 것으로 대체했는데 목적이 도구 설치가 아니라 호스트에서 명령을 치는 것이었기 때문이다.
정리
- 마운트된 경로를 호스트 패키지와 공유하면 설치가 소유권을 바꿀 수 있다
- 주요 데이터 디렉터리는 배포판이 달라도 같은 경로라 잘 겹친다
- 설치 전에
getent passwd로 사용자 번호 점유를ls -ln으로 디렉터리 소유권을 실측한다 - 컨테이너 프로세스의 사용자 번호도 함께 확인한다
- 소유권이 바뀌면 컨테이너가 다음 쓰기 시점에 죽는다
- 사후 확인은 파일 상태와 프로세스 응답을 둘 다 본다
- 호스트에는 클라이언트만 두고 서버는 기동하지 않는다
- 목적은 도구 설치가 아니라 명령을 칠 수 있는 상태다