ML/MLOps

기반 다지기 - Docker 편

minturtle 2024. 12. 23. 09:50
반응형

도커란?

  • 도커 공식 홈페이지에서 정의하는 도커는 아래와 같습니다.

Docker는 애플리케이션을 개발, 배포, 실행하기 위한 개방형 플랫폼입니다. Docker를 사용하면 애플리케이션과 인프라를 분리하여 소프트웨어를 빠르게 제공할 수 있습니다. Docker를 통해 애플리케이션을 관리하는 것과 동일한 방식으로 인프라를 관리할 수 있습니다. Docker의 코드 배포, 테스트, 배포 방법론을 활용하면 코드 작성과 프로덕션 환경에서 실행하는 사이의 지연을 크게 줄일 수 있습니다.

  • 즉 도커는 개발, 배포, 실행할 때 필요한 어플리케이션을 인프라에 종속적이지 않게 사용할 수 있다는 것을 의미합니다.
  • 하나의 개발팀이 React, Python API 서버, PostgresSQL DB를 사용한다고 가정해볼 때, 도커가 없다면 팀의 모든 개발자가 동일한 버전을 가지고 있는지 검증하여 개발을 진행하여야 합니다.
  • 하지만 도커는 컨테이너라는 개념을 도입해 React, Python, DB 등의 어플리케이션을 완전히 격리된 환경에서 실행할 수 있어, 서버나 개발 환경에 필요한 버전을 맞추는 것이 가능합니다.

 

Container vs Virtual Machine

https://kubernetes.io/ko/docs/concepts/overview/

  • 도커를 처음 공부할 때 흔하게 볼 수 있는 것은 VM과 Container의 차이점입니다.
  • VM은 자체 커널, 하드웨어 드라이버, 프로그램 및 응용 프로그램이 있는 전체 운영 체제를 의미합니다. 위 그림에서 볼 수 있듯, VM내에서는 GUEST OS가 존재하게 되는데, 이 GUEST OS는 User영역, Kernel 영역, 가상화된 HW 영역을 모두 포함하기 때문에 굉장히 무겁습니다.
    • 위 그림에서 Hypervisor는 단일 물리적 머신에서 여러 가상 머신을 실행하는 데 사용할 수 있는 소프트웨어로, 필요에 따라 CPU 및 메모리와 같은 컴퓨터 자원을 개별 가상 머신에 할당하는 역할을 합니다. 즉, 방금 언급한 가상화된 HW가 바로 Hypervisor에 의해 할당된 자원이라고 이해하면 됩니다.
  • 근데 과연 특정 앱을 분리된 환경에서 실행하기 위해서 전체 OS를 매번 다운로드 받아 실행시켜야하는게 꼭 필요할까요? 이러한 생각에서 컨테이너 개념이 등장하게 되었습니다.
  • Container 환경에서는 Kernel 영역이 Container Runtime에 의해 공유되며, 이 Container Runtime이 HostOS와 직접 연동되어 사용됩니다. 따라서 개발자는 Application 프로그램과 그에 필요한 라이브러리만 Image에 담아서 이를 실행시키면 되는 것입니다.
  • 정리하자면 VM은 Hypervisor에 의해 하드웨어가 가상화 된 것이고, Container는 Container Runtime에 의해 OS의 커널을 공유하여 사용하는 것입니다.

그런데 왜 다른 OS의 이미지를 사용할 수 있을까?

  • 여기까지의 설명을 이해했다면 한가지의 의문점이 들게 됩니다. 왜 Windows나 Ubuntu같은 환경에서도 CentOS 같이 다른 운영체제 기반의 이미지도 사용이 가능할까요?
  • 결론적으로 이것이 가능한 이유는 docker container runtime이 Host의 리눅스 커널을 공유하여 사용하고 있기 때문인데요, 이 때문에 리눅스 기반의 다른 OS 기반의 이미지를 문제없이 구동할 수 있고, 윈도우에서 도커를 사용하기 위해선 WSL을 필요로 하게 됩니다.

Docker의 namespace, cgroups

  • 또 한가지의 궁금한 점이 생길 수 있는데요, 그것은 컨테이너 끼리 커널을 공유함에도 불구하고, 어째서 컨테이너들은 서로를 인식하지 못하는가?입니다.
  • 이를 가능하게 하는 것이 Linux 커널의 namespace 기술을 활용하는 것인데요, Namespace는 시스템 리소스를 가상화하여 프로세스에게 독립적인 공간을 제공하는 경량 가상화 기술입니다.
  • namespace는 PID namespace, network namespace, Mount namespace, UTS namespace 등이 존재하며, Docker는 컨테이너를 생성할 때 위의 namespace들을 조합하여 사용합니다.
  • namespace가 커널영역을 분리해준다면, cgroups는 네트워크, CPU, Memory, I/O 영역과 같이 HW 자원과 연관된 부분을 분리하는 리눅스의 기술입니다.

Docker의 이미지

  • 다음으로 알아 볼 것은 도커의 이미지에 대한 내용입니다. 도커 공식문서의 이미지에 대한 정의는 다음과 같습니다.

컨테이너 이미지는 컨테이너를 실행하기 위한 모든 파일, 바이너리, 라이브러리 및 구성을 포함하는 표준화된 패키지입니다.

  • 이미지에는 중요한 두가지의 원칙이 있다고 합니다.
    1. 이미지를 만든 후에는 수정할 수 없으며, 새 이미지를 만들거나 그 위에 변경사항을 추가할 수만 있다.
    2. 컨테이너 이미지는 레이어로 구성되며, 각 계층은 파일 추가, 제거, 수정하는 파일 시스템 변경 집합을 나타낸다.
  • 이 두가지 원칙을 통해 기존의 이미지를 확장할 수 있다고 합니다.
  • 이러한 기본 이미지는 우리가 처음부터 만들 필요는 없고, 잘 만들어진 BASE 이미지를 가지고 와서 사용할 수 있습니다.
    • 도커는 Docker Hub에서 이미지들을 공유하거나 다운받을 수 있으며, Docker hub에는 Docker 공식 이미지, Docker에서 검증한 상업용 게시자의 고품질 이미지, 일반 사용자가 업로드한 이미지를 다운받을 수 있습니다.
  • Docker HUB의 이미지를 Docker hub에서 또는 Docker Desktop에서 검색할 수 있으며, 여기서 Image의 세부정보를 조회해 이미지에 설치된 라이브러리나 패키지 등에 대한 정보를 확인할 수 있습니다.

  • 여기서 Docker hub는 이미지 레지스트리로, 이미지를 다른 사용자와 공유하는 것이 가능한 centralized location입니다. 이 외에도 Image Registry는 Google Container Registry, GitLab 컨테이너 레지스트리 등이 있습니다.

 

이미지 레이어

  • 앞서 언급했듯 각 레이어는 Dockerfile의 명령어에 해당하며, 이전 레이어에 대한 파일 시스템의 변경사항을 나타냅니다.
  • Dockerfile을 빌드할 때 각 명령에 대해 이전 빌드의 명령을 다시 사용할 수 있는지에 대한 여부를 확인하는데, 이를 통해 더 빠른 빌드를 수행할 수 있습니다.

  • 이 때 캐시가 무효화되는 경우가 있는데요, 그 경우는 다음과 같습니다.
    1. RUN 명령어의 명령을 변경하는 경우
    2. COPY하는 파일의 모든 변경사항(파일 내용, 권한 등)이 감지되었을 때
    3. 특정 레이어의 캐시가 무효화되면 그 뒤의 레이어도 모두 캐시 무효화
  • 즉 캐시를 적극적으로 활용하기 위해선 자주 변경되는 부분을 Dockerfile 아래에 두는 것이 유리합니다.

Docker Image 작성

  • 도커 이미지 작성의 기본은 여기에서 확인하실 수 있으며, 이번에는 많이 혼동하여 사용하는 ENTRYPOINT와 CMD의 차이에 대해서만 알아보도록 하겠습니다.
  • ENTRYPOINT와 CMD의 공통점으로는 컨테이너를 실행할 때 수행하는 명령어라는 점입니다.
  • 하지만 ENTRYPOINT는 컨테이너가 시작될 때 항상 실행되는 기본 명령어로, 실행되는 명령어를 변경할 수 없습니다.
  • 이와 달리 CMD는 인자를 통해 Dockerfile 에 지정된 CMD 값을 대신 하여 지정한 인자값으로 변경하여 실행할 수 있습니다.
ENTRYPOINT ["echo", "Hello"]
CMD ["World"]
  • 기본 실행 : Hello world, docker run image_name John 실행 시 Hello John 출력
  • 따라서 변경되지 않는 명령어는 ENTRYPOINT로, 변경 가능한 프로그램 arg등은 CMD로 지정하는게 적절합니다.

Multi-Stage Build

  • 기존 빌드에서는 모든 빌드 명령이 순서대로 실행되며 단일 빌드 컨테이너에서 종속성 다운로드, 코드 컴파일 및 애플리케이션 패키징이 모두 수행되기 때문에 이미지의 부피가 커지면서 불필요한 용량을 차지하게 됩니다.
  • 다단계 빌드는 Dockerfile에 각각 특정 목적을 가진 여러 단계를 도입한 기능으로, 빌드의 여러 부분을 여러 환경에서 동시에 실행할 수 있는 기능입니다.
  • 예를 들어, C와 같은 컴파일 언어에서 한 단계에서 컴파일하고 컴파일된 이진 파일을 최종 런타임 이미지에 복사하여 최종 이미지에는 컴파일러를 포함하지 않는 식으로 구축이 가능합니다.
  • 아래는 간단한 예시입니다.
FROM eclipse-temurin:21.0.2_13-jdk-jammy AS builder
WORKDIR /opt/app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY ./src ./src
RUN ./mvnw clean install

FROM eclipse-temurin:21.0.2_13-jre-jammy AS final
WORKDIR /opt/app
EXPOSE 8080
COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]

도커 컨테이너

  • 도커의 이미지를 실행하게 되면 Container를 생성하게 되는데요, Container는 기본적으로 아래의 실행 명령어를 사용합니다.
docker run -p nginx
  • 컨테이너의 포트와 HOST의 포트를 연결시키려면 -p HOST_PORT:CONTAINER_PORT 와 같이 지정할 수 있으며, 이때 HOST_PORT를 생략하면 HOST 내에서 사용가능한 랜덤한 포트를 사용합니다.
docker run -p 8000:80 nginx
docker run -p 80 nginx
  • 만약 Docker IMAGE에서 EXPOSE된 port에 모두 랜덤 포트를 매핑시키고 싶다면, -P 태그를 사용합니다.
docker run -P nginx
  • 환경변수를 지정하고 싶다면 -e 태그를 사용하거나 —env-file을 통해 환경변수 파일을 입력해 줄 수 있습니다.
docker run -e TEST=1 nginx
docker run --env-file .env nginx
  • 컨테이너는 기본적으로 HOST의 리소스(CPU, Memory)를 모두 사용할 수 있지만, 특정 컨테이너에 제한을 걸 수 있습니다. cpu는 —cpus, memory는 —memory 옵션을 부여할 수 있습니다.
docker run --memory="512m" --cpus="0.5" postgres

 

 

컨테이너 네트워크

  • 컨테이너 네트워킹은 컨테이너가 서로 다른 컨테이너나 다른 workload와 통신할 수 있는 능력을 말합니다.
  • 더 쉽게 설명하자면, 같은 호스트 내의 실행중인 컨테이너 간에 연결을 돕는 논리적 네트워크 개념입니다.
  • 이를 어떻게 가능하게 할까요? 먼저 도커 사용자가 도커를 설치하면 여러가지 Network Driver이 설치되며, 컨테이너마다 원하는 네트워크 드라이버를 지정할 수 있습니다.

  • 먼저 이를 이해하기 위해선 네트워크 인터페이스에 대한 정의가 필요합니다.
네트워크 인터페이스(Network Interface)란 컴퓨터나 네트워크 장비가 네트워크에 연결되어 통신하기 위한 물리적 또는 가상의 연결 포인트를 의미합니다.  이때 물리적 네트워크 인터페이스는 NIC나 이더넷 카드를 말하며, 가상 네트워크 인터페이스는 SW로 가상화된 네트워크 카드를 의미합니다.

참고 : https://ssoontory.tistory.com/399
  • 그 후 veth에 대한 이해도 필요합니다.
veth는 리눅스의 Virtual Ethernet Interface이며, 가상 네트워크 인터페이스입니다. veth는 는 일반적인 네트워크 인터페이스와는 달리 패킷을 전달받으면, 자신에게 연결된 다른 네트워크 인터페이스로 패킷을 보내주는 식으로 동작하기 때문에 항상 쌍으로 생성해줘야 합니다. 도커에서는 실행중인 컨테이너 수만큼 veth로 시작하는 인터페이스가 생성됩니다.
참고 : https://velog.io/@choidongkuen/서버-Docker-Network-에-대해
  • 도커는 내부적으로 컨네이너에 내부 IP를 순차적으로 할당하며, 내부 IP는 도커가 설치된 호스트만 쓸 수 있으므로 컨테이너 내부의 veth와 호스트의 물리 네트워크 인터페이스인 eth와 연결되며, 이때 컨테이너의 veth와 eth는 직접 연결되지 않고 Docker Bridge Network Driver에 의해 연결됩니다. 추가적으로, 같은 Bridge 내의 컨테이너끼리의 통신도 가능합니다.

https://haward.tistory.com/186

이제 아까 봤던 도커 네트워크 드라이버 이미지를 다시 보겠습니다.

1. Bridge Network

  • Docker의 기본 네트워크 드라이버입니다.
  • 단일 호스트 내에서 컨테이너 간 통신에 사용됩니다.
  • 각 컨테이너에 고유한 IP 주소를 할당합니다.
  • 포트 매핑을 통해 외부와의 통신이 가능합니다.

2. Host Network

  • 컨테이너가 호스트의 네트워크 스택을 직접 사용합니다.
  • 별도의 네트워크 격리 없이 호스트와 동일한 네트워크 인터페이스를 공유합니다.
  • 포트 매핑이 필요 없어 네트워크 성능이 우수합니다.

3. None Network

  • 컨테이너에 네트워크 기능을 제공하지 않습니다.
  • 완전히 격리된 네트워크 환경이 필요할 때 사용합니다.

4. Overlay Network

  • 여러 Docker 호스트 간의 분산 네트워크를 구성할 때 사용합니다.
  • Swarm 모드에서 서비스 간 통신에 주로 사용됩니다.

5. Macvlan Network

  • 컨테이너에 물리적 네트워크 인터페이스처럼 보이는 MAC 주소를 할당합니다.
  • 컨테이너가 물리적 네트워크에 직접 연결된 것처럼 동작하게 합니다.

Remote Drivers

  • 써드파티에서 제공하는 플러그인 형태의 네트워크 드라이버입니다.
  • 특수한 네트워킹 요구사항을 충족시키기 위해 사용될 수 있습니다.

Docker log

  • 다음으로 알아볼 내용은 도커 로그에 관한 내용입니다. Docker Container의 로그는 아래의 명령어로 볼 수 있는데요.
docker logs <container_name>
  • 이 때 특이한 점은 Host 시스템이 재부팅되어도 컨테이너의 로그는 날아가지 않는다는 건데요, 도커는 어떻게 로그를 저장하여 유지할까요?
  • 리눅스는 기본적으로 stdin, stdout, stderr 총 3개의 I/O 스트림을 사용해 콘솔에 상호작용할 수 있습니다. 이 중 docker logs는 stdout과 stderr를 표시합니다. 이 때 도커는 logging driver에 따라 로그를 다양한 방식으로 저장합니다.
  • Docker가 기본적으로 사용하는 logging driver는 json-file logging driver로, 각 OS마다 JSON 파일을 저장하는 경로는 아래와 같습니다.
    • Linux: /var/lib/docker/containers/<container_id>/<container_id>-json.log
    • Windows : C:\ProgramData\docker\containers\<container_id>\<container_id>-json.log
  • json-logging driver외에도 아래의 log driver이 존재합니다.

  • 컨테이너를 실행할 때, 아래의 예시와 같이 log driver를 지정할 수 있습니다.
docker run -it --log-driver none alpine ash
  • 또 로그는 blocking 방식으로 작성될 수 있는데, 이 때 버퍼 크기를 넘으면 로그가 유실될 수 있으므로 주의해야 합니다.
docker run -it --log-opt mode=non-blocking --log-opt max-buffer-size=4m alpine ping 127.0.0.1

Docker Volume

  • 마지막으로 Docker volume에 대해 알아보겠습니다. Docker volume은 host - container의 파일시스템간의 마운트를 통해 디렉터리를 공유하며, 이로 인해 Container이 삭제되어도 데이터가 유지되는 장점이 있습니다.
  • 볼륨을 사용하는 방법은 총 4가지가 존재합니다
    • Volume Mounts
    • Bind Mounts
    • tmpfs Mounts
    • Named pipes
  • Volume Mounts는 “docker volume create” 명령어로 생성된 볼륨을 컨테이너에 연결하는 것입니다. 이렇게 생성된 볼륨은 볼륨 컨테이너로 불리며, 볼륨 컨테이너 : 컨테이너 간에 1:N으로 연결해 여러 컨테이너가 하나의 디렉터리를 공유하는 것이 가능합니다. 또, 후술할 Bind Mounts에 비해 바인딩된 디렉터리가 여기저기 흩어져 있지 않고 중앙 관리가 가능하다는 점이 장점입니다.
  • docker volume create my-vol docker run -it -v my-vol:/container_dir img
  • Bind Mounts는 호스트의 특정 디렉터리 - 컨테이너의 특정 디렉터리를 마운트 하는 것으로, Volume Mounts와 유사하지만 Docker에서 관리되지 않으므로 볼륨 디렉터리를 중앙에서 관리할 수 없습니다.
  • docker run -it -v /host_dir:/container_dir img
  • tmpfs Mounts는 호스트의 디스크가 아닌 메모리에 저장됩니다.
  • docker run --tmpfs /data:noexec,size=1024,mode=1777
  • named pipe는 Windows 운영 체제에서 사용되는 특별한 종류의 파일 시스템 객체로, 이름을 가진 파이프를 통해 서로 다른 프로세스 간 단방향 통신을 수행할 수 있습니다.

참고

반응형