티스토리 뷰

반응형

회사에서 한 장비에 수십 개의 도커 컨테이너를 동시에 운용할 상황이 생겼다.

컨테이너 각각에서 작업이 이루어지고 ssh를 통해 접속해서 확인할 수 있도록 구성하였는데,

컨테이너가 많아지자 모든 컨테이너로의 ssh 접속이 불가해졌다는 연락을 받았다.

ssh -vv로 로그를 확인하니 접속 시 인증 단계는 정상적으로 넘어가나, 셸 할당이 실패하는 상황이었다.

 

결론적으로는 ssh 접속 시의 사용자에 대한 프로세스 제한을 늘려주어 임시로 해소하였고,

각 컨테이너에서의 사용자 UID를 구분해 주어 근본적인 문제를 해결하였다.

 

컨테이너에서의 ulimit -a로 프로세스 제한을 확인했을 때는 분명 unlimited로 표시되었고,

서버 자원은 충분하며 각 컨테이너에서의 프로세스/스레드는 수백 개 수준이었는데도 프로세스 생성이 불가하여 원인을 찾는데 시간이 꽤 걸렸다. 문제를 해결하며 덕분에 컨테이너에서의 사용자 UID를 구분해야 함을 알게 되었다.

 


 

다른 컨테이너에서 같은 UID의 사용자 중 하나로 스레드 초과 실행하기

 

상황 재현을 위해 외부에서 ssh로 접속 가능한 컨테이너 두 개를 띄우고, 한 쪽에서만 스레드를 많이 띄워보자.

 

아래와 같이 `start.sh`와 Dockerfile을 만들고 컨테이너를 띄운다.

 

#!/bin/bash

# start.sh

/usr/sbin/sshd

tail -f /dev/null

 

SSH 데몬을 직접 실행시킨다.

 

# Dockerfile
FROM centos:7

COPY start.sh /start.sh
RUN chmod +x /start.sh

RUN yum -y install openssh-server && \
      yum clean all

RUN ssh-keygen -A

RUN useradd appuser && \
      echo "appuser:1234" | chpasswd

ENTRYPOINT ["/start.sh"]

 

간단하게 appuser라는 사용자를 생성한다.

 

docker build -t test .
docker run -d -p 10001:22 --name cont1 test
docker run -d -p 10002:22 --name cont2 test

ssh appuser@localhost -p10001 # 1234
# [appuser@server ~]$

 

위의 도커파일을 통해 cont1, cont2라는 컨테이너를 생성하였고,

정상적으로 ssh 접속도 가능함을 확인 가능하다.

 

 

이제 두 번째 컨테이너에 아래와 같이 더미 스레드를 생성하는 스크립트를 만들고 실행하자

(리눅스에서 프로세스와 스레드는 동일하게 취급된다)

 

#!/bin/bash

count="$1"

process_count=0

for ((i = 0; i < count; i++)); do
    sleep 3600 &

    process_count=$((process_count + 1))
done

 

# docker exec로 접속하여 실행
docker exec -u appuser -it cont2 /bin/bash
[appuser@server /]$ vi limit.sh
[appuser@server /]$ ./limit.sh 5000 # 5000개의 스레드 생성
[appuser@server /]$ ps aux | wc -l
# >> 5007

 

 

더미 스레드는 두 번째 컨테이너에 생성했지만

두 번째 컨테이너 뿐만 아니라 첫 번째 컨테이너에서도 ssh 접속이 셸 할당 단계에서 불가능해진다

 

ssh -vv appuser@localhost -p10001 # 프로세스가 몇 없는 1번 컨테이너로 접속

# >> ...
# >> debug2: PTY allocation request accepted on channel 0
# >> debug2: channel_input_status_confirm: type 100 id 0
# >> shell request failed on channel 0

 


 

문제 진단 및 해소

 

이는 Dockerfile에서 생성한 사용자의 UID가 동일한 것이 원인이다.

두 컨테이너 모두 `appuser` 일반 사용자로 접속해 UID를 확인해 보자

 

[appuser@server /]$ id
# >> uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)

 

별도의 설정을 하지 않는다면 같은 UID를 가진 사용자가 만들어질 수 있다.

 

 

SSH 접속 과정에서 PAM(Pluggable Authentication Modules) 인증을 거치는데 이 과정에서 각 사용자가 생성 가능한 프로세스/스레드 수를 검사하게 된다.

 

제한 관련 설정은 리눅스 배포판마다 위치가 조금씩 다른데, centos7의 경우 /etc/security/limits.d/ 의 20-proc.conf에 존재한다.

redhat 계열의 linux는 이름이 다르지만 대부분 이 폴더에 존재하며, 보통 /etc/security에서의 설정들이 영향을 미친다.

 

# 20-nproc.conf
# Default limit for number of user's processes to prevent
# accidental fork bombs.
# See rhbz #432903 for reasoning.

*          soft    nproc     4096
root       soft    nproc     unlimited

 

root 이외의 사용자(*)는 프로세스 수 제한이 4096개로 기본 설정되어 있다.

root 계정으로만 설정 가능한 hard 옵션에서 지정한 범위 이하로 일반 사용자도 자신의 최대 프로세스 수를 soft옵션으로 제한 가능하다.

 

기본 수치는 리눅스 배포판마다 다르며 aws와 같은 서비스를 이용하는 경우 인스턴스 사양에 따라 적절히 조절된 채로 제공되니 확인이 필요하다.

 

 

기본 사용자의 상한이 4096이었으므로 스레드를 5000개 이상 실행해 버린 2번 컨테이너에는 ssh 접속 시 새로운 셸, 즉 새로운 프로세스가 appuser라는 일반 사용자 이름으로 할당되는 시점에 제한에 막혀 접속 불가 처리된 것이다.

 

 

 

같은 UID의 일반 사용자를 통해 1번 컨테이너에 접속이 불가능한 이유는 뭘까?

컨테이너는 다르나 UID가 같으면 사용자에 대한 프로세스/스레드 수가 검사될 때 이 수가 합산되기 때문이다.

 

 

컨테이너를 사용하면 호스트의 커널을 공유하게 되며 UID, GID 풀은 전역으로 단 하나만 공유, 관리된다.

 

`appuser` 사용자로 컨테이너 1에 ssh 접속을 시도하면 아래의 과정을 거쳐 실패하는 듯하다.

1. 호스트에서 ssh 요청으로 인해 새로운 셸 프로세스를 생성할 때 컨테이너 내의 사용자 `appuser`의 UID 1000으로 프로세스 수를 검색,

2. 글로벌하게 UID 1000으로 생성된 프로세스가 5000개 이상임이 확인,

3. 컨테이너 1의 `20-proc.conf`를 참조, root 이외 사용자는 4096개의 프로세스만 생성 가능하므로 셸 프로세스 생성 실패

 

즉, 단 하나의 프로세스를 같은 UID의 일반 사용자로 실행하는 컨테이너를 4096개 생성하면,

모든 컨테이너에서 해당 사용자로의 프로세스 생성이 불가능해지는 것이다.

 

 

 

만약 2번 컨테이너에만 `appuser`의 프로세스 제한을 해제하면 어떻게 될까?

 

# container2 20-nproc.conf

# Default limit for number of user's processes to prevent
# accidental fork bombs.
# See rhbz #432903 for reasoning.

*          soft    nproc     4096
root       soft    nproc     unlimited
appuser    soft    nproc     unlimited # appuser 사용자에 대한 process soft limit 해제

 

# 2번 컨테이너에 일반 사용자로 접속 시도 (성공)
ssh appuser@localhost -p10002
# >> Last login: ...
[appuser@server ~]$ ps aux | wc -l
# >> 5009

# 1번 컨테이너에 일반 사용자로 접속 시도 (실패)
ssh appuser@localhost -p10001
# >> shell request failed on channel 0

# 1번 컨테이너에 루트 사용자로 접속 시도 (성공)
ssh root@localhost -p10001
[root@server ~]$ ps aux | wc -l
# >> 7

 

5000개 이상의 프로세스를 실행 중인 2번 컨테이너에 `appuser`로는 접속이 가능해지고

단 7개의 프로세스를 실행 중인 1번 컨테이너에는 억울하게도 여전히 `appuser`로 접근이 불가하다.

 

root 사용자의 경우 프로세스 수의 제한이 없으므로 1번 컨테이너에도 접속이 가능하다.

 

일반 사용자들의 UID 자체를 다르게 가져가거나, 컨테이너마다 이 제한을 매번 해제해주면 문제가 해결됨을 알 수 있다.

 

참고로, docker exec로 /bin/bash에 진입해 스크립트를 돌리는 상황에서는 PAM 인증을 거치지 않아 일반 사용자로도 제한 이상의 프로세스를 생성할 수 있다.

 

 


 

예시는 SSH로의 접속을 들었으나, 같은 UID의 일반 사용자로 새로운 프로세스를 생성하는 모든 작업에선 컨테이너가 많아진다면 문제가 발생할 수 있다.

 

동일한 커널을 공유하는 컨테이너 기술에서는 같은 UID를 가지는 사용자를 활용하는 것은 위험하다.

컨테이너들에서 일반 사용자를 추가할 때 동적으로 다른 UID를 가지도록 관리해야 안전하다.

 

 

 

Reference

https://access.redhat.com/solutions/30316

https://medium.com/@mccode/understanding-how-uid-and-gid-work-in-docker-containers-c37a01d01cf

https://techblog.woowahan.com/2569/

 

 

 

반응형