Docker

Docker-기본

DongsunSin 2022. 9. 6. 20:38

Docker


도커(Docker)는 2013년 등장한 컨테이너 기반 가상화 도구입니다. 도커를 사용하면 컨테이너를 쉽게 관리할 수 있으며, 이미지를 만들어 외부 서버에 배포하는 것도 가능합니다. 가상 머신 자체는 완전한 컴퓨터라 항상 게스트 OS를 설치해야 합니다. 그래서 이미지 안에 OS가 포함되기 때문에 이미지 용량이 커집니다. 하지만, Docker에서는 반가상화보다 더 경량화된 방식을 택합니다. Docker 이미지에 서버 운영을 위한 프로그램과 라이브러리만 격리해서 설치할 수 있고, OS 자원(시스템 콜)은 호스트와 공유합니다. 이렇게 되면서 이미지 용량이 크게 줄어들었습니다.

Docker 이미지는 베이스 이미지에서 바뀐 부분만 이미지로 생성하고, 실행할 때는 베이스 이미지와 바뀐 부분을 합쳐서 실행해 용량이 큰 이미지가 중복되어 생성하는 것을 방지합니다.

운영체제로 보면 이미지는 실행 파일, 컨테이너는 프로세스, Docker는 실행 파일 또는 스크립트를 위한 실행 환경으로 보면 됩니다. 

도커를 실행한다는 것은 user space의 아래 두 프로그램을 실행한다는 것을 의미합니다. 

  • Docker daemon: 정상적으로 설치됐다면 이 프로그램은 항상 running 합니다.
  • Docker CLI: 사용자와 상호작용하는 도커 프로그램

도커가 빌드하는 컨테이너는 8가지 측면에서 격리됩니다.

  • PID namespace: 프로세스 식별자 및 기능
  • UTS namespace: 호스트와 도메인 이름
  • MNT namespace: 파일 시스템 액세스 및 구조
  • IPC namespace: 공유 메모리를 통한 통신 처리
  • NET namespace: 네트워크 액세스 및 구조
  • USR namespace: user이름, 식별자
  • chroot(): root 파일 시스템의 경로를 컨트롤
  • croups: 리소스 보호

리눅스 네임스페이스와 cgroups는 런타임에 컨테이너를 관리합니다. 도커는 다른 기술을 사용해 파일형 컨테이너를 shipping container와 같은 역할을 하는 컨테이너로 제공합니다.

각 컨테이너마다 PID namespace를 생성하는 것은 Docker의 가장 중요한 특징입니다. 같은 PID namespace를 갖게되면 각 컨테이너는 어느 호스트 머신에 어떤 프로세스가 실행되고 있는지 식별할 수 있어야 합니다. 게다가 최악으로는 한 컨테이너가 다른 컨테이너를 컨트롤할 수 있게될 수도 있습니다. 

도커는 환경에 구애받지 않는 시스템을 만들기 위한 3가지 특징을 가집니다:

  • Read-only file systems
  • Environment variable injection
  • Volumes

Read-only file systems: 이 시스템은 2개의 장점을 가집니다. 먼저, 컨테이너가 가진 파일의 변경사항으로부터 민감해지지 않을 것이라는 확신을 가질 수 있습니다. 둘째, 공격자가 컨테이너의 파일을 손상시킬 수 없을 것이라는 더 큰 확신을 가질 수 있습니다.

Environment variable injection: 사용자는 환경변수로 컨테이너 생성 시간에 인풋 또는 추가적인 configuration을 컨테이너의 프로세스에 제공할 수 있습니다.

Docker container status

  • Running 
  • Paused
  • Restarting
  • Exited (also used if the container has never been started)

Docker Image layers

layer란 적어도 다른 하나의 이미지와 관련된 이미지입니다. 이미지는 parent/child 관계를 유지합니다. 이미지는 다른 이미지와 relationship을 가질 수 있고, 다른 레포지토리의 다른 소유자의 이미지도 포함됩니다. 

Container file system abstraction and isolation

컨테이너 안에서 실행중인 프로그램들은 이미지 레이어에 대해 아무것도 모릅니다. 컨테이너 안에서, 파일 시스템이 컨테이너 안에서 실행하거나 이미지로 실행되는 것처럼 보이지 않게 실행됩니다. 컨테이너 관점에서는 이미지에 의해 제공된 파일의 특별한 복사본을 갖고 있다고 여겨집니다. 이것은 union 파일 시스템으로 불리는 것을 가능하게 합니다. 

Union 파일 시스템은 효과적인 파일 시스템 격리를 위해 필요한 중요한 도구 집합의 일부입니다. 다른 도구는 MNT 네임스페이스와 chroot 시스템 콜입니다. 파일 시스템은 레이어의 사용을 추상화하는 호스트 파일 시스템에 마운트 지점을 생성하는 데 사용됩니다. 생성된 레이어는 도커 이미지 레이어로 번들화됩니다. 마찬가지로 도커 이미지가 설치되면 해당 레이어는 시스템에 대해 선택한 특정 파일 시스템 공급자가 사용할 수 있도록 적절히 구성됩니다.

리눅스 커널은 MNT 네임스페이스를 제공합니다. 도커가 컨테이너를 생성하면 새 컨테이너는 고유한 MNT 네임스페이스를 갖고 컨테이너에 대한 새 마운트 지점이 이미지에 생성됩니다. 

마지막으로, chroot는 이미지 파일 시스템의 루트를 컨테이너 컨텍스트의 루트로 만드는 데 사용됩니다. 이렇게 하면 컨테이너 내부에서 실행중인 모든 항목이 호스트 파일 시스템의 다른 부분을 참조할 수 없습니다. 

Benefits로는 먼저, 공통 레이어를 한 번만 설치하면 되는 것입니다. 반대로 vm 기술은 컴퓨터에 중복된 가상 머신이 있는 횟수만큼 동일한 파일을 저장합니다. 둘째, 레이어는 의존성을 관리하고 우려를 분리하기 위한 거친 도구를 제공합니다. 이는 software 작성자에게 유용하며 사용자관점에서는 사용중인 이미지와 레이어를 검사하여 실행중인 소프트웨어를 신속하게 식별하는 데 도움이 됩니다. 마지막으로, 일부 기본 이미지 위에 사소한 변경 사항을 레이어화할 수 있는 경우 software specialization을 쉽게 만들 수 있습니다. 전문화된 이미지를 제공하면 최소한의 커스텀화로 필요한 것을 정확히 얻을 수 있습니다.

Volume

볼륨은 컨테이너 디렉토리 트리의 마운트 지점으로, 호스트 디렉토리 트리의 일부가 마운트되어있습니다. 볼륨이 없으면 컨테이너 사용자는 이미지 마운트를 제공하는 union 파일 시스템으로만 작업할 수 있습니다.

볼륨은 단일 컨테이너에 독립적인 범위 또는 라이프사이클이 있는 데이터를 분할하고 공유하기 위한 도구입니다. 따라서 볼륨은 파일을 공유하거나 쓰는 컨테이너형 시스템 설계의 중요한 부분이 됩니다. 이미지는 프로그램과 같은 상대적으로 정적인 파일을 포장하고 배포하는데 적합하며, 볼륨은 동적 데이터 또는 specialization이 포함됩니다. 이러한 구별을 통해 이미지를 재사용할 수 있고, 데이터를 쉽게 공유할 수 있습니다. 이러한 상대적으로 정적 및 동적인 파일 공간의 분리로 다형성 및 합성 툴과 같은 고급 패턴을 구현할 수 있습니다.

보다 근본적으로, 볼륨은 애플리케이션과 호스트의 관심사를 분리할 수 있습니다. 이미지가 호스트에 생성된 컨테이너와 호스트에 로드됩니다. 도커는 실행 중인 호스트에 대해 알지 못하면서 컨테이너에 사용할 수 있는 파일에 대해서만 주장할 수 있습니다. 즉, Docker만으로는 마운트된 네트워크 스토리지나 하드 드라이브와 같은 호스트별 기능을 활용할 수 없습니다.그러나 호스트에 대한 지식이 있는 사용자는 볼륨을 사용해 컨테이너의 디렉토리를 해당 호스트의 적절한 스토리지에 매핑할 수 있습니다.

위 그림은 volume과 bind mount의 차이를 보여줍니다. 볼륨은 호스트의 도커 영역안에서 관리됩니다. 바인드 마운트의 경우 외부(host)에서 컨테이너 안쪽으로 내용을 추가할 수 있지만 볼륨의 경우 지원하지 않습니다. 또한 해당 볼륨을 사용하는 컨테이너에서만 식별할 수 있습니다. volume을 사용하면 컨테이너간에 볼륨을 안전하게 공유할 수 있습니다. 또한, 볼륨드라이버를 사용하면 볼륨의 내용을 암호화하거나 다른 기능을 추가 할 수 있습니다.

 

 

<sharing volume>

  • Host-dependent sharing: 호스트 파일 시스템의 알려진 단일 경로의 bind mount volume을 갖을 때 호스트-디펜던트 공유를 사용한다고 말합니다. 컨테이너 밖에서는 작성한 디렉토리 내용을 나열하거나 새 파일을 보고 변경 내용을 볼 수 있습니다. 호스트쪽 디렉토리에 의존이 생기고 만약 이 디렉토리의 데이터를 잘못 손대면 애플리케이션에 부정적 영향을 미칠 수 있습니다.
  • Generalized sharing and the volumes-from flag: --volumes-from 플래그와 볼륨을 공유하는 것은 휴대용 애플리케이션 아키텍처를 구축하는 데 중요한 도구이지만 몇 가지 제한 사항이 있습니다. 도커 managed 볼륨을 사용하면 호스트 시스템의 데이터 및 파일 시스템 구조에서 컨테이너가 분리됩니다. Docker가 관리되는 볼륨에 대해 생성한 파일 및 디렉토리는 여전히 설명 및 유지관리해야 합니다. Docker가 이러한 파일에서 어떻게 작동하는지 이해하려면 managed volume lifecycle을 이해해야 합니다.

Network exposure

도커는 가상 네트워크를 실행중인 모든 컨테이너를 컴퓨터가 연결된 네트워크에 연결하기 위해 생성합니다. 이 네트워크를 bridge라고 합니다. 

The local Docker network topology

가상 네트워크는 도커가 설치된 시스템의 local이며 참여하는 컨테이너와 호스트가 연결된 넓은 네트워크 사이의 경로로 구성됩니다. 

컨테이너에는 자체적인 개인 loopback 인터페이스와 별도의 Ethernet 인터페이스가 호스트 네임스페이스의 다른 가상 인터페이스에 연결되어있습니다. 이러한 두 개의 링크된 인터페이스는 호스트의 네트워크 스택과 각 컨테이너에 대해 생성된 스택 사이의 링크를 형성합니다. 각 컨테이너에는 고유한 개인 IP 주소가 할당됩니다. 연결은 docker0이라는 도커 bridge 인터페이스를 통해 라우팅됩니다. 컨테이너마다 생성된 각 virtual interface는 docker0에 연결되어 있고 함께 네트워크를 형성합니다. 이 bridge 인터페이스는 호스트가 연결된 네트워크에 연결되어 있습니다. 

도커는 커널 네임스페이스를 사용해 전용 virtual interface를 만들지만 네임스페이스 자체는 네트워크 격리를 제공하지 않습니다. 네트워크 노출 또는 격리는 호스트의 방화벽 규칙에 의해 제공됩니다. 

Four network container archetypes

모든 도커 컨테이너는 4가지 원형 중 하나를 따릅니다. 컨테이너가 다른 로컬 컨테이너 및 호스트의 네트워크와 상호작용하는 방식을 정의합니다: Closed containers, Bridged containers, Joined containers, Open containers

  • Closed containers

네트워크 트래픽을 허용하지 않는 폐쇄형 컨테이너 입니다. 이러한 컨테이너에서 실행되는 프로세스는 Loopback 인터페이스에만 액세스할 수 있습니다. 네트워크나 인터넷에 액세스하는 프로그램은 사용할 수 없습니다. 사용 예로는 볼륨 컨테이너, 백업 작업, 오프라인 배치 처리 또는 진단도구 등이 있습니다. 또한, 임의의 비밀번호를 생성하기 위한 프로그램을 실행시 네트워크 액세스 없이 컨테이너 안에서 실행되어 그 번호의 도난을 방지해야할 때 closed container를 사용합니다. 도커는 각 컨테이너에 대해 전용 Loopback 인터페이스를 생성함으로써 컨테이너 내부에서 실행되는 프로그램이 네트워크를 통해 통신할 수 있도록 하지만 해당 통신이 컨테이너를 벗어나지 않게 합니다. 

docker run 커맨드에 --net 플래그에 none을 표시해 closed container를 생성하도록 지시할 수 있습니다.

  • Bridged containers

브릿지된 컨테이너는 default 컨테이너입니다. 이 컨테이너는 private loopback 인터페이스와 네트워크 브릿지를 통해 호스트의 나머지 부분에 연결되는 또 다른 전용 인터페이스가 있습니다. docker0에 연결된 모든 인터페이스는 동일한 가상 서브넷의 일부입니다. 즉, docker0 인터페이스를 통해 서로 통신할 수 있습니다. 

Reaching out

bridged 컨테이너를 선택하는 이유는 네트워크 액세스가 필요한 프로세스 때문입니다. docker run --net bridge로 설정할 수 있습니다. 인터넷에 액세스해야 하는 소프트웨어나 개인 네트워크의 다른 컴퓨터가 있는 경우 bridged container를 사용할 수 있습니다.

Custom name resolution

DNS는 IP주소에 호스트 이름을 매핑하기 위한 프로토콜 입니다. 아웃바운드 통신을 변경하는 가장 기본적인 방법 중 하나는 IP 주소의 이름을 만드는 것입니다. 도커는 새 컨테이너에 대한 DNS configuration을 커스터마이징하는 --hostname 플래그를 제공합니다. 컨테이너 내부의 DNS 재정의 시스템에 항목을 추가합니다. 이 항목은 제공된 호스트 이름을 컨테이너의 bridge IP 주소에 매핑합니다: docker run --hostname 호스트네임 

 컨테이너의 DNS configuration을 커스터마이징하는 두 번째 옵션은 사용할 하나 이상의 DNS 서버를 지정하는 기능입니다. --dns-search=[] 옵션을 사용하면 기본 호스트 이름 접미사 같은 DNS 검색 도메인을 지정할 수 있습니다. docker run 명령의 --add-host = [] 플래그를 사용하면 IP 주소 및 호스트 이름 쌍에 대한 사용자 지정 매핑을 제공 할 수 있습니다. 다른 옵션과 달리 이 플래그는 데몬 시작시 기본값으로 설정할 수 없습니다. name-to-IP 주소 맵은 프로그램이 특정 네트워크 주소에서 자신을 분리하는 데 사용할 수있는 간단한 인터페이스를 제공합니다. DNS가 아웃 바운드 트래픽 동작을 변경하는 가장 좋은 도구인 경우 방화벽 및 네트워크 토폴로지가 인바운드 트래픽을 제어하는 가장 좋은 도구입니다.

Opening inbound communication

Bridged containers는 기본적으로 호스트 네트워크에서 액세스 할 수 없습니다. 컨테이너는 호스트의 방화벽 시스템으로 보호됩니다. 기본 네트워크 토폴로지는 호스트의 외부 인터페이스에서 컨테이너 인터페이스로의 경로를 제공하지 않습니다. 즉, 호스트 외부에서 컨테이너로 이동할 수있는 방법이 없습니다. 

docker run 명령어는 호스트의 네트워크 스택에 있는 포트와 새 컨테이너의 인터페이스 사이에 매핑을 생성하는 데 사용할 수있는 플래그, -p = [] 또는 --publish = []를 제공합니다. 매핑 형식은 다음 네 가지 형식을 가질 수 있습니다:

  • <containerPort> 이 양식은 컨테이너 포트를 모든 호스트 인터페이스의 동적 포트에 바인딩합니다: docker run -p 3333 ...
  • <hostPort>:<containerPort> 이 양식은 지정된 컨테이너 포트를 각 호스트 인터페이스의 지정된 포트에 바인딩합니다: docker run -p 3333:3333
  • <ip>::<containerPort> 이 양식은 지정된 IP 주소를 사용하여 컨테이너 포트를 인터페이스의 동적 포트에 바인딩합니다: docker run -p 192.168.0.32::2222 ...
  • <ip>:<hostPort>:<containerPort> 이 양식은 지정된 IP 주소를 사용하여 컨테이너 포트를 인터페이스의 지정된 포트에 바인딩합니다: docker run -p 192.168.0.32:1111:1111 ...

각 명령 조각은 호스트 인터페이스의 포트에서 컨테이너 인터페이스의 특정 포트로의 경로를 만듭니다. docker run 명령은 컨테이너가 노출해야하는 포트 번호를 가지는 다른 플래그 --expose를 제공합니다. 이 플래그는 각 포트에 대해 한 번씩 여러 번 설정할 수 있습니다.

이 도구를 사용하면 인바운드 트래픽을 호스트에서 실행되는 올바른 브리지 컨테이너로 라우팅하는 것을 관리 할 수 있습니다. 또 다른 미묘한 유형의 커뮤니케이션이 있습니다. 컨테이너 간 커뮤니케이션입니다.

Inter-container communication

모든 로컬 브리지 컨테이너는 동일한 브리지 네트워크에 있으며 기본적으로 서로 통신할 수 있습니다. 아래 그림은 동일한 호스트에있는 5 개의 컨테이너 간의 네트워크 관계를 보여줍니다.

docker in action 도서 참고

소프트웨어에는 기본 암호 또는 비활성화된 암호화와 같은 보안 수준이 낮은 기능이 함께 제공되는 것이 일반적입니다. 순진한 사용자는 네트워크 토폴로지 또는 일부 로컬 방화벽이 컨테이너를 개방형 액세스로부터 보호 할 것이라고 기대할 수 있습니다. 어느 정도는 사실이지만 기본적으로 모든 컨테이너는 다른 로컬 컨테이너에서 완전히 액세스 할 수 있습니다.

최악의 경우 컨테이너 간 통신을 활성화하면 컨테이너 내의 손상된 프로그램이 다른 로컬 컨테이너를 공격 할 수 있습니다. 그래서 Docker 데몬을 시작할 때 컨테이너 간의 네트워크 연결을 허용하지 않도록 구성할 수 있습니다. 이는 다중 테넌트 환경에서 모범 사례입니다. Docker 데몬을 시작할 때 docker -d --icc=false ...로  수행할 수 있습니다. 

 

  • Joined containers

덜 격리된 네트워크 컨테이너 원형을 joined container라고 합니다. 이러한 컨테이너는 공통 네트워크 스택을 공유합니다. 이러한 방식은 joined 컨테이너간에 격리가 존재하지 않습니다. 이는 제어 및 보안 감소를 의미합니다. 보안성이 가장 낮은 원형은 아니지만 감옥의 벽이 허물어진 최초의 원형입니다.

Docker는 특정 컨테이너에 대해 생성된 인터페이스의 액세스를 다른 새 컨테이너에 제공하여 이러한 유형의 컨테이너를 빌드합니다. 이러한 방식으로 인터페이스는 managed volume 처럼 공유됩니다. --net 플래그의 컨테이너 값을 사용하면 새 컨테이너가 결합되어야하는 컨테이너를 지정할 수 있습니다. 이러한 방식으로 결합된 컨테이너는 다른 형태의 격리를 유지합니다. 그들은 다른 파일 시스템, 다른 메모리 등을 유지합니다. 그러나 똑같은 네트워크 구성 요소를 가질 것입니다.

서로 다른 두 데이터에 액세스 할 수있는 두 개의 서로 다른 프로그램이 통신해야하지만, 다른 데이터에 대한 직접 액세스를 공유해서는 안되는 경우 이 패턴을 사용할 수 있습니다. 한 프로세스가 다른 보호된 채널을 통해 다른 프로세스를 모니터링 해야하는 상황을 고려하십시오. 컨테이너 간 통신에는 방화벽 규칙이 적용됩니다. 한 프로세스가 노출되지 않은 포트에서 다른 프로세스와 통신해야하는 경우 가장 좋은 방법은 컨테이너를 join하는 것입니다.

  • Open containers

Open containers는 위험합니다. 네트워크 컨테이너가 없으며 호스트 네트워크에 대한 전체 액세스 권한이 있습니다. 여기에는 중요한 호스트 서비스에 대한 액세스가 포함됩니다. 개방형 컨테이너는 격리를 제공하지 않으며 다른 옵션이없는 경우에만 고려해야합니다.

이 유형의 컨테이너는 docker run 명령에서 --net 옵션의 값으로 host를 지정할 때 생성됩니다.

이 구성을 사용하면 프로세스가 1024보다 낮은 번호의 보호된 네트워크 포트에 바인딩 할 수 있습니다.

Dockerfile


  • FROM

FROM은 어떠 이미지를 기반으로 이미지를 생성할지 설정합니다. FROM은 반드시 설정해야 합니다.

이미지 이름과 태그를 함께 설정할 수도 있습니다. 

  • RUN

RUN은 FROM에서 설정한 이미지 위에서 스크립트 혹은 명령을 실행합니다. 여기서 RUN으로 실행한 결과가 새 이미지로 생성되고, 실행 내역은 이미지의 히스토리에 기록됩니다. 

셸 없이 실행은 RUN ["<실행 파일>", "<매개 변수1>", "<매개 변수2>"] 형식, 

셸로 실행은 RUN <명령> 형식입니다. 

RUN으로 실행한 결과는 캐시되며 다음 빌드 때 재사용합니다. 이를 원치 않으려면 docker build 명령에서 --no-cache 옵션을 사용하세요.

  • CMD
CMD는 컨테이너가 시작되었을 때 스크립트 혹은 명령어를 실행합니다. 즉 docker run 명령으로 컨테이너를 생성하거나 docker start 명령으로 정지된 컨테이너를 시작할 때 실행됩니다. CMD는 Dockerfile에서 한 번만 사용할 수 있습니다.
 
CMD ["<실행 파일>", "<매개 변수1>", "<매개 변수2>"] 형식으로 사용합니다.
  • ENTRYPOINT를 사용하였을 때
CMD ["<매개 변수1>", "<매개 변수2>"] 형식입니다. ENTRYPOINT에 설정한 명령에 매개 변수를 전달하여 실행합니다. Dockerfile에 ENTRYPOINT가 있으면 CMD는 ENTRYPOINT에 매개 변수만 전달하는 역할을 합니다. 그래서 CMD 독자적으로 파일을 실행할 수 없게 됩니다.
  • ENTRYPOINT
ENTRYPOINT는 컨테이너가 시작되었을 때 스크립트 혹은 명령을 실행합니다. CMD와 실행 조건은 동일합니다. ENTRYPOINT ["<실행 파일>", "<매개 변수1>", "<매개 변수2>"] 형식입니다. 셸을 사용하지 않는 방식입니다. CMD와 ENTRYPOINT는 컨테이너가 생성될 때 명령어가 실행되는 것은 동일하지만, docker run 명령에서 다릅니다. docker run <이미지> <실행할 파일> 형식인데, docker run 명령에서 실행할 파일을 설정하면 CMD는 무시됩니다. ENTRYPOINT는 무시되지 않고, 실행할 파일 설정 자체를 매개 변수로 받아서 처리합니다.
  • EXPOSE
EXPOSE는 호스트와 연결할 포트 번호를 설정합니다.
EXPOSE <포트 번호> 형식
EXPOSE 하나로  포트 번호를 두 개 이상 동시에 설정할 수 있습니다. EXPOSE는 호스트와 연결만 할 뿐 외부에 노출되지 않습니다. 
포트를 외부에 노출하려면 docker run 명령의 -p, -P 옵션을 사용해야 합니다.
  • ENV

 ENV는 환경 변수를 설정합니다. ENV로 설정한 환경 변수는 RUN, CMD, ENTRYPOINT에 적용됩니다.

  • ADD

ADD는 파일을 이미지에 추가합니다. ADD <복사할 파일 경로> <이미지에서 파일이 위치할 경로> 형식입니다.

  • COPY

COPY는 파일을 이미지에 추가합니다. ADD와 달리 COPY는 압축 파일을 추가할 때 압축을 해제하지 않고, 파일 URL도 사용할 수 있습니다.

  • VOLUME

VOLUME은 디렉토리의 내용을 컨테이너에 저장하지 않고 호스트에 저장하도록 설정합니다. 

VOLUME <컨테이너 디렉터리> 또는 VOLUME ["컨테이너 디렉터리 1", "컨테이너 디렉터리2"] 형식입니다.

단, VOLUME으로는 호스트의 특정 디렉토리와 연결할 수는 없습니다. → 그러려면 docker run 명령에서 -v 옵션을 사용하세요.

( -v <호스트 디렉터리>:<컨테이너 디렉터리>)

  • USER

USER는 명령을 실행할 사용자 계정을 설정합니다. RUN, CMD, ENTRYPOINT에 적용됩니다.

 USER <계정 사용자명> 형식입니다.

  • WORKDIR

WORKDIR은 RUN, CMD, ENTRYPOINT의 명령이 실행될 디렉토리를 설정합니다.

WORKDIR <경로> 형식입니다.

WORKDIR 뒤에 오는 모든 RUN, CMD, ENTRYPOINT에 적용되며, 중간에 다른 디렉토리를 설정하여 실행 디렉토리를 바꿀 수 있습니다. WORKDIR은 절대 경로 대신 상대 경로도 사용할 수 있습니다. 상대 경로를 사용하면 먼저 설정한 WORKDIR의 경로를 기준으로 디렉토리를 변경합니다. 최초 기준은 /입니다.

  • ONBUILD

ONBUILD는 생성한 이미지를 기반으로 다른 이미지가 생성될 때 명령을 실행(trigger)합니다. 최초에 ONBUILD를 사용한 상태에서는 아무 명령도 실행하지 않습니다. 다음 번에 이미지가 FROM으로 사용될 때 실행할 명령을 예약하는 기능입니다.

ONBUILD <Dockerfile 명령> <Dockerfile 명령의 매개 변수> 형식입니다. FROM, MAINTAINER, ONBUILD를 제외한 모든 Dockerfile 명령을 사용할 수 있습니다.

docker-compose


순서에 맞춰 데이터베이스 컨테이너를 실행하고, 앱 컨테이너를 실행하고... 도커가 별로 편하진 않네요?

  • 장황한 옵션
  • 앱 컨테이너와 데이터베이스 컨테이너의 실행 순서

도커 컴포즈를 사용하면 컨테이너 실행에 필요한 옵션을 docker-compose.yml에 적어둘 수 있고, 컨테이너 간 의존성, 실행 순서를 관리할 수 있습니다.

version 

docker-compose.yml 파일의 첫 줄은 파일 규격 버전을 적습니다. "3"은 3으로 시작하는 최신 버전을 의미합니다. (참고: compose 파일의 버전과 호환성을 안내한 공식 문서)

services 

이 항목 밑에 실행하려는 컨테이너들을 정의합니다. 컴포즈에서는 컨테이너 대신 서비스라는 개념을 사용합니다!

services:
db:
volumes:
- ./docker/data:/var/lib/postgresql/data

volumes

docker compose에서는 volumes에 상대 경로를 지정할 수 있습니다. 

ex)

docker run으로 앱 컨테이너를 실행할 때 -v 옵션을 사용해 프로젝트 루트 디렉토리를 컨테이너 안의 /app 디렉토리와 연결했던 부분과 같습니다.

environment

docker run 명령어의 -e 옵션에 적었던 내용들입니다. (환경변수)

build

db 서비스와 달리 앱 서비스(여기에서는 diango)에서는 특정 이미지 대신 build 옵션을 추가합니다.

context는 docker build 명령을 실행할 디렉토리 경로입니다.

dockerfile에는 '개발용' 도커 이미지를 빌드하는데 사용할 Dockerfile을 지정하면 됩니다. Dockerfile-dev 파일에서는 (운영용 Dockerfile과 달리) 소스코드를 컨테이너에 넣지 않습니다.

ports

docker run 명령어의 -p 옵션에 해당합니다. 

command

docker run으로 앱 컨테이너를 실행할 때 가장 마지막에 적었던 명령어 부분입니다. 

ex) 

command:
         - python manage.py runserver 0:8000

도커 컴포즈 파일 버전 3부터 links 항목을 사용하지 않더라도 한 네트워크 안에 있는 서비스끼리 서로 통신을 할 수 있기 때문에, 이 항목을 사용하지 않았습니다.(Links topic in Networking in Compose) 한 docker-compose.yml 안에 있는 서비스들은 별도로 지정하지 않으면 하나의 네트워크에 속합니다.(네트워크와 관련된 더 자세한 내용: Networking in Compose)

database volume 

지금까지는 데이터베이스의 실제 데이터를 ./docker/data에 저장하고 있었는데, 실수로 지워지거나 소스 코드 버전 관리 시스템에 들어가면 낭패입니다... 도커에 맡겨봅시다!

docker-compose.yml
version: '3'
 
# 변경 부분!
volumes:
  django_sample_db_dev: {}
 
services:
  db:
    image: postgres
    volumes:
      # 여기도!
      - django_sample_db_dev:/var/lib/postgresql/data
    environment:
      ...

django_sample_db_dev라는 이름으로 볼륨을 하나 만듭니다. 이 볼륨은 db 서비스에서 사용하려면, db 서비스 선언부 안에 volumes 항목을 넣고 -가상디스크_이름:컨테이너_속_디렉터리 처럼 지정합니다. 이후로는 모든 데이터베이스 데이터가 django_sample_db_dev 볼륨에 저장됩니다.

docker-compose 주요 명령어

docker-compose 명령어를 짧은 alias로 등록해두면 편리합니다. 

  • up -d

docker-compose.yml 파일 내용에 따라 이미지를 빌드하고 서비스를 실행합니다. 

  1. 서비스를 띄울 네트워크 설정
  2. 필요한 볼륨 생성(or 이미 존재하는 볼륨과 연결)
  3. 필요한 이미지 pull
  4. 필요한 이미지 build
  5. 서비스 의존성에 따라 서비스 실행

up 명령 옵션

-d: 서비스 실행 후 콘솔로 빠져나옵니다.

--force-recreate: 컨테이너를 지우고 새로 만듭니다.

--build: 서비스 시작 전 이미지를 새로 만듭니다.

 

  • ps

현재 환경에서 실행중인 각 서비스의 상태를 보여줍니다.

  • stop, start

서비스를 멈추거나, 멈춰 있는 서비스를 시작합니다.

  • down

서비스를 지웁니다. 컨테이너와 네트워크를 삭제하며, 옵션에 따라 볼륨도 지웁니다. ( --volume: 볼륨까지 삭제)

  • exec

실행 중인 컨테이너에서 명령어를 실행합니다. 자동화된 마이그레이션용 파일 생성이나 유닛 테스트, lint 등을 실행할 때 사용합니다.

(run은 새 컨테이너를 만들어서 명령어를 실행합니다. --rm옵션을 추가하지 않으면, 컨테이너가 종료된 후에도 삭제되지 않습니다.)

  • logs

서비스의 로그를 확인할 수 있습니다. logs 뒤에 서비스 이름을 적지 않으면 도커 컴포즈가 관리하는 모든 서비스의 로그를 함께 보여줍니다.

 

<꿀팁>

  • docker-compose.yml 파일을 수정했으면 stop → rm → up 하지말고 up명령만 실행해도 달라진 부분이 있다면 알아서 컨테이너를 재생성하고 서비스를 재시작해줍니다.
  • Dockerfile-dev 파일을 수정했을 땐 up명령에 --build 옵션을 넣으면 이미지를 새로 만들고 서비스를 재시작합니다.
  • Database 내용을 지우고 싶을 때 down 명령에 --volume 옵션을 추가해 서비스에서 사용하는 볼륨을 삭제합니다.(초기화)

컨테이너에서 사용하는 프로세스 격리 기능


컨테이너 구현에는 리눅스 namespace, 루트 디렉토리 격리, 컨트롤 그룹, capability, union mount 외에 다양한 리눅스 커널 기능들이 사용됩니다.

  • 리눅스 네임스페이스(Linux namespace)

리눅스 네임스페이스는 특정 프로세스의 리눅스 리소스 접근을 제어하기 위해 사용됩니다. 네임스페이스는 리소스 별로 IPC 네임스페이스, 마운트 네임스페이스, 네트워크 네임스페이스, 프로세스 ID 네임스페이스, 사용자 네임스페이스, UTS 네임스페이스, 컨트롤 그룹 네임스페이스 등으로 나뉩니다. 

시스템 상에서 실행되는 프로세스들은 기본적으로 init 프로세스의 네임스페이스를 공유하지만 시스템콜이나 unshare 명령어를 사용해 리소스 별로 네임스페이스를 분리하는 것이 가능합니다.

  • 도커에서 PID 네임스페이스가 어떻게 사용되고 있나요?

도커 컨테이너는 하드웨어 애뮬레이션 없이 리눅스 커널을 공유해서 바로 프로세스를 실행합니다. 

  • 컨테이너는 호스트 시스템의 커널을 사용합니다.
  • 컨테이너는 이미지에 따라 실행되는 환경(파일 시스템)이 달라집니다.

컨테이너가 서로 다른 파일 시스템을 가질 수 있는 것은 이미지(파일의 집합)을 루트 파일 시스템으로 강제로 이식시켜 프로세스를 실행하기 때문입니다. 프로세스는 자신의 루트 디렉토리가 /가 아닌 그 아래의 특정 디렉토리인 것처럼 동작합니다. 

도커 컨테이너는 프로세스이지만, 프로세스라고 부르기 보다는 컨테이너라고 부르는 데는 이유가 있습니다. 컨테이너는 리눅스 커널에 포함된 프로세스 격리 기술들을 사용해 생성된 특별한 프로세스 입니다. 도커 컨테이너로 실행한 bash 셸의 PID는 1번입니다.

그런데, 실제 호스트 상에서 1번 프로세스는 systemd입니다. 이를 가능케 하는 것이 리눅스 네임스페이스 입니다. PID 네임스페이스가 분리되어 있기 때문에 도커 컨테이너의 프로세스는 1번이 됩니다. 

단순히 PID를 분리하면 PID가 아까와 같이 1이 아닌 일반 프로세스의 값이 됩니다. 둘다 맞는데, 이 이유는 둘이 같은 프로세스이기 때문입니다. 즉, 프로세스에게는 자신의 PID가 1로 보이지만, 호스트 입장에서 보면 PID가 2387 등 일반적인 프로세스라는 의미입니다.

사실 모든 프로세스는 PID 네임스페이스를 가지고 있습니다.

도커 컨테이너의 PID가 1번인 원리도 위와 같습니다. 컨테이너는 프로세스이며, 호스트 입장에서는 컨테이너도 하나의 프로세스입니다.

도커 컨테이너가 프로세스라는 걸 정확히 이해하면 도커 이미지를 ’단 하나의 타깃 프로세스를 실행하기 위한 파일들의 집합’으로 이해할 수 있게됩니다. docker exec 명령 또한 SSH 접속이나 가상머신에서 셸을 실행하는 것이 아닌, 컨테이너가 사용중인 네임스페이스나 파일 시스템에 접근하기 위한 특별한 명령어에 불과합니다. 

  • 루트 디렉토리 격리와 chroot

컨테이너는 호스트의 파일 시스템이 아닌 별도의 실행 환경을 가지고 있습니다. 루트 디렉토리 격리 기술로 chroot 같은 시스템 콜이 이용됩니다. 프로세스가 바라보는 루트 디렉토리를 파일 시스템 상의 특정한 디렉토리로 변경하는 것이 가능합니다. 

 

  • 컨트롤 그룹(cgroups)

cgroups는 프로세스에서 사용가능한 CPU, 메모리, 네트워크 대역폭, 디스크 I/O 등을 그룹 단위로 제어하는 리눅스 커널 기능입니다. 

컨트롤 그룹은 컨테이너에서만 사용되는 기능은 아니며, 리눅스 시스템에서 프로세스 관리를 위해 일반적으로 사용됩니다.

  • 리눅스 캐퍼빌리티(Linux capabilities)

Linux capabilities는 프로세스의 권한을 제어하는 기능입니다. 리눅스의 프로세스는 크게 루트 권한으로 실행되는 특권 프로세스와 일반 사용자가 실행하는 비특권 프로세스로 나뉩니다. 루트의 권한을 세분화해 프로세스의 권한을 제어합니다. 컨테이너 런타임에서도 일부 루트 권한이 필요한 경우 이 기능을 사용해 권한을 지정합니다.

 

  • 유니온 마운트(Union Mount)

유니온 마운트는 계층화된 파일 시스템은 구현합니다. 유니온 마운트를 통해 이미지 빌드 과정에서 캐시를 사용하거나, 이미지를 관리하는 데도 이점이 있습니다. 

복수의 파일시스템을 하나의 파일시스템으로 마운트해 두 파일 시스템에서 동일한 파일이 있다면 나중에 마운트된 파일 시스템의 파일을 오버레이합니다. 하위 파일 시스템에 대한 쓰기 작업은 CoW(Copy On Write) 전략에 따라 복사본을 성하여 수행해 원본 파일 시스템은 변하지 않습니다.

도커 컨테이너 레이어 구조는 컨테이너 레이어와 이미지 레이어로 분류합니다. 이미지 레이어는 Read-only layer, 컨테이너 레이어는 Writable Layer입니다. 따라서 컨테이너 레이어는 각 컨테이너마다 자신만의 상태를 갖고, 한 이미지로 여러 개의 컨테이너를 생성해도 문제가 되지 않습니다. 

참고 자료

http://pyrasis.com/docker.html
https://docs.docker.com/get-started/overview/

https://www.44bits.io/ko/post/is-docker-container-a-virtual-machine-or-a-process

도서-Docker in action