쿠버네티스를 이루는 컨테이너 도우미, 도커
이전 포스팅에서 쿠버네티스는 컨테이너를 오케스트레이션하며, 오케스트레이션하는 기본 단위는 파드라고 배웠습니다.
그리고 파드는 컨테이너로 이루어져 있다는 것도 배웠습니다.
이번 포스팅에선 파드를 이루는 컨테이너를 알아보고 컨테이너를 다루는 도구인 도커의 개념과 사용법을 배워봅시다!
쿠버네티스를 말할 때 도커를 자주 이야기하곤 합니다.
시중의 책도 대부분 도커를 다루고 나서 쿠버네티스를 다룹니다.
쿠버네티스를 이루는 기본 오브젝트가 파드고, 파드는 컨테이너로 이루어져 있으며, 컨테이너를 만들고 관리하는 도구가 도커이기 때문입니다.
그래서 도커를 배우고 나서 쿠버네티스를 배우는 것이 흐름에 더 맞습니다.
그런데 최근엔 도커를 몰라도 쿠버네티스를 배우고 사용하는 것이 가능해졌습니다.
이는 여러 공급사에서 만들어 둔 컨테이너 이미지로 쿠버네티스에 컨테이너 인프라 서비스를 만들 수 있다는 뜻입니다.
컨테이너 관리 기술이 발전했고 여러 애플리케이션이 이미 도커 이미지로 배포되고 있어서 배포된 이미지를 사용하면 도커를 몰라도 쿠버네티스의 환경을 만들고 사용할 수 있습니다.
하지만 쿠버네티스를 이루고 있는 기술 자체는 컨테이너를 벗어날 수 없습니다.
따라서 트러블 슈팅을 제대로 하려면 컨테이너를 잘 알아야합니다.
상황에 따라서는 직접 만든 소스 코드를 빌드해 컨테이너로 만들고 이를 쿠버네티스에서 사용할 수도 있습니다.
또한 CI/CD, 모니터링도 모두 컨테이너로 관리됩니다.
따라서 컨테이너와 이를 다루는 도커를 자세히 알아야 컨테이너 인프라 환경을 깊게 이해할 수 있습니다!
쿠버네티스 시스템을 기술적으로 정의해 봅시다.
파드들은 워커 노드라는 노드 단위로 관리하며,
워커 노드와 마스터 노드가 모여 쿠버네티스 클러스터가 됩니다.
그리고 파드는 1개 이상의 컨테이너로 이루어져 있습니다.
파드는 쿠버네티스로부터 IP를 받아 컨테이너가 외부와 통신할 수 있는 경로를 제공합니다.(이미 여러번 서비스를 통해 실습해봤죠?)
그리고 컨테이너들이 정상적으로 작동하는지 확인하고 네트워크나 저장 공간을 서로 공유하게 합니다.
파드가 이러한 환경을 만들기 때문에 컨테이너들은 마치 하나의 호스트에 존재하는 것처럼 작동할 수 있습니다.(파드는 1개 이상의 컨테이너라고 했죠?)
장리하면, 컨테이너를 돌보는 것이 파드고, 파드를 돌보는 것이 쿠버네티스 워커 노드이며, 워커 노드를 돌보는 것이 쿠버네티스 마스터 노드입니다.
그런데 쿠버네티스 마스터 역시 파드(컨테이너)로 이루어져 있습니다.
이 구조를 이루는 가장 기본인 컨테이너는 하나의 운영 체제 안에서 커널을 공유하며 개별적인 실행 환경을 제공하는 격리된 공간입니다.
여기서 개별적인 실행 환경이란 CPU, 네트워크, 메모리와 같은 시스템 자원을 독자적으로 사용하도록 할당된 환경을 말합니다.
개별적인 실행 환경에서는 실행되는 프로세스를 구분하는 ID도 컨테이너 안에 격리돼 관리됩니다.
그래서 각 컨테이너 내부에서 실행되는 애플리케이션들은 서로 영향을 미치지 않고 독립적으로 작동할 수 있습니다.
예를들어 워커 노드1의 HOST OS(Linux Kernel)에서 시스템 자원(CPU, 네트워크, 메모리, PID, 파일시스템) 등을 각 컨테이너로 독립적으로 할당하는 것과 같습니다.
각 컨테이너가 독립적으로 작동하기 때문에 여러 컨테이너를 효과적으로 다룰 방법이 필요해졌습니다.
오래전부터 유닉스나 리눅스는 하나의 호스트 운영 체제 안에서 자원을 분리해 할당하고, 실행되는 프로세스를 격리해서 관리하는 방법을 제공했습니다.
하지만 파일 시스템을 설정하고 자원과 공간을 관리하는 등의 복잡한 과정을 직접 수행해야 해서 일부 전문가만 할 수 있다는 단점이 있었습니다.
이런 복잡한 과정을 쉽게 만들어 주는 도구로 등장한 것이 도커이빈다.
도커는 컨테이너를 사용하는 방법을 명령어로 정리한 것이라고 보면 됩니다.
도커는 사용하면 사용자가 따로 신경쓰지 않아도 컨테이너를 생성할 때 개별적인 실행 환경을 분리하고 자원을 할당합니다!
컨테이너 관리 도구는 다음과 같이 도커 외에도 여러 가지가 있습니다.
컨테이너디(Containerd)
크라이오(CRI-O)
카타 컨테이너(Kata Containers)
도커
구분 | 컨테이너디 | 크라이오 | 카타 컨테이너 | 도커 |
---|---|---|---|---|
명령어 도구 | 별도 지원 | 타 도구 사용 | 자체 지원 | 자체 지원 |
내부 구조 | 단순 | 매우 단순 | 복잡 | 복잡 |
확장성 | 좋음 | 좋지 못함 | 좋지 못함 | 매우 좋음 |
컨테이너 관리 | 좋음 | 좋음 | 좋음 | 매우 좋음 |
이미지 관리 | 좋음 | 좋음 | 좋음 | 매우 좋음 |
보안성 | 좋은 | 좋음 | 매우 좋음 | 좋음 |
자원 사용량 | 매우 좋음 | 매우 좋음 | 좋지 못함 | 좋음 |
정보량 | 적음 | 거의 없음 | 거의 없음 | 거의 없음 |
특징을 살펴보고, 현재 쿠버네티스 인프라를 구성하는 데 가장 적합한 도구인 도커에 대해 더 자세히 살펴보도록 합시다.
도커는 앞서 베이그런트를 실행할 때 자동으로 설치되었습니다. 여기서는 도커로 컨테이너를 다루는 기본 명령들을 실습해 보겠습니다.
도커 이미지를 내려받아 컨테이너로 실행하고 도커 이미지와 컨테이너를 삭제하는 방법까지 배웁니다.
배울 내용을 간단히 정리하면 이미지 찾기 -> 실행하기 -> 디렉터리와 연결하기 -> 삭제하기
입니다.
먼저 컨테이너 이미지와 컨테이너의 관게를 정리해 봅시다.
컨테이너 이미지는 베이그런트 이미지와 유사합니다.
베이그런트 이미지는 이미지 자체로는 사용할 수 없고 베이그런트를 실행할 때 추가해야만 사용할 수 있습니다.
이와 마찬가지로 컨테이너 이미지도 그대로는 사용할 수 없고 도커와 같은 CRI로 불러들여야 컨테이너가 실제로 작동합니다.
이는 실행 파일과 실행된 파일 관계로 볼 수 잇습니다.
따라서 컨테이너를 삭제할 때는 내려받은 이미지와 이미 실행된 컨테이너를 모두 삭제해야만 디스크의 용량을 온전히 확보할 수 있습니다.(이미지도 용량을 차지하고, 이미지로 컨테이너를 실행했을 때도 별도의 공간을 또 사용합니다)
먼저 컨테이너를 만들 이미지가 있어야 합니다. 이미지를 검색해서 내려받고 구조를 살펴보겠습니다.
이미지는 레지스트리(registry) 라고 하는 저장소에 모여 있습니다. 레지스트리는 도커 허브처럼 공개된 유명 레지스트리(저장소, DB라고 생각하시면 됩니다)일 수도 있고, 내부에 구축한 레지스트리일 수도 있습니다.
이미지는 레지스트리 웹 사이트에서 직접 검색해도 되고, Terminus(본인이 사용하는 ssh client) 명령 창에서 쿠버네티스 마스터 노드에 접속해 검색할 수도 있습니다. 이때 별도의 레지스트리를 지정하지 않으면 기본으로 도커 허브에서 이미지를 찾습니다.
docker search <검색어>
를 입력하면 특정한 이름(검색어)을 포함하는 이미지가 있는지 찾습니다.
이미지는 애플리케이션, 미들웨어 등 고유한 목적에 맞게 패키지돼 있습니다.
예를 들어 docker search nginx
명령어로 현재 사용할 수 있는 nginx 이미지를 찾아보겠습니다.
STARS로 가장 많이 사용되는 이미지가 정렬되어 있어서 편하군요!
표시되는 각 열의 의미는 다음과 같습니다.
docker search
로 찾은 이미지는 docker pull
로 내려받을 수 있습니다. 앞에서 찾은 nginx 이미지를 내려받아 자세히 살펴봅시다!
docekr pull nginx
이미지를 내려받을 때 사용하는 태그, 레이어, 이미지의 고유 식별 값 등을 볼 수 있습니다.
태그(tag) : Using default tag
와 함께 뒤에 따라오는 태그 이름을 통해 이미지를 내려받을 때 사용한 태그를 알 수 있습니다. 아무런 조건을 주지 않고 이미지 이름만으로 pull을 수행하면 기본으로 latest 태그가 적용됩니다. latest는 가장 최신 이미지를 의미합니다. 따라서 내려받는 버전이 다를 수도 있습니다.
레이어(layer) : a330b6cecb98, e0ad2c0621bc, 9e56c3e0e6b7, 09f31c94adc6, 32b26e9cdb83, 20ab512bbb07는 pull을 수행해 내려받은 레이어입니다. 하나의 이미지는 여러 개의 레이어로 이루어져 있어서 레이어마다 Pull complete 메시지가 발생합니다.
다이제스트(digest) : 이미지의 고유 식별자(sha256:853b221d3341add7aaadf5f81dd088ea943ab9c918766e295321294b035f3f3e)로 이미지에 포함된 내용과 이미지의 생성 환경을 식별할 수 있습니다. 식별자는 해시(hash) 함수로 생성되며 이미지가 동일한지 검증하는 데 사용합니다. 이름이나 태그는 이미지를 생성할 때 임의로 지정하므로 이름이나 태그가 같다고 해서 같은 이미지라고 할 수 없습니다. 그러나 다이제스트는 고유한 값이므로 다이제스트가 같은 이미지는 이름이나 태그가 다르더라도 같은 이미지입니다.
상태(Status) : 이미지를 내려받은 레지스트리, 이미지, 태그 등의 상태 정보를 확인할 수 있습니다. 형식은 '레지스트리 이름/이미지 이름:태그'입니다. 여기서 내려받은 이미지는 docker.io 레지스트리에서 왔으며, 이미지 이름은 nginx이고, 태그는 앞서 설명한 것처럼 기본 태그인 latest입니다.
이미지의 태그와 레이어 구조는 컨테이너를 이해하는 데 매우 중요한 부분이므로 좀 더 자세히 살펴봅시다!
태그는 이름이 동일한 이미지에 추가하는 식별자입니다. 이름이 동일해도 도커 이미지의 버전이나 플랫폼(CPU 종류나 기본 베이스를 이루는 운영 체제 등)이 다를 수 있기 떄문에 이를 구분하는 데 사용합니다.
이미지를 내려받거나 이미지를 기반으로 컨테이너를 구동할 때는 이미지 이름만 사용하고 태그를 명시하지 않으면 latest 태그를 기본으로 사용합니다. 이미지 태그와 관련된 정보는 해당 이미지의 도커 허브 메뉴 중 Tags 탭에서 확인할 수 있습니다.
docker pull nginx
로 내려받는 latest는 docker pull nginx:1.21.3
으로 내려받는 이미지와 동일합니다. 만약 안정화 버전(stable, 1.18.0)을 사용하고 싶으면 docker pull nginx:stable
명령을 사용합니다.앞에서 컨테이너 이미지는 실행 파일이라고 했는데, 사실 이미지는 애플리케이션과 각종 파일을 담고 있다는 점에서 ZIP 같은 압축 파일에 더 가깝습니다.
그런데 앞축 파일은 압축한 파일의 개수에 따라 전체 용량이 증가합니다. 하지만 이미지는 같은 내용일 경우 여러 이미지에 동일한 레이어를 공유하므로 전체 용량이 감소합니다.
ZIP 같은 압축파일은 내부에 동일한 파일이 포함된 압축 파일과 이미지를 같은 파일이지만 각각 독립적으로 저장하고 공간을 점유합니다. 그에 반해 이미지는 내용이 같은 레이어들을 공유하기 때문에 전체 공간에서 봤을 때 상대적으로 용량을 적게 차지합니다.
두 개의 nginx 이미지(latest와 stable)를 비교해 차이점을 좀 더 살펴봅시다!
docker pull nginx:stable
명령으로 stable 이미지를 내려받습니다.a330b6cecb98
레이어는 Already exists라고 이미 존재하는 레이어라는 사실을 확인할 수 있습니다. docker images <이미지 이름>
명령을 실행해 내려받은 이미지를 조회합니다.docker images nginx
docker history nginx:stable
명령을 실행해 확인합니다. 생성 과정에서 단계별로 용량을 얼마나 차지하는지 자세한 이력이 나오므로 이미지가 차지하는 실제 용량을 확인할 수 있습니다.docker history nginx:stable
ADD file:4ff85d9f6aa2467...
와 69.3 MB
를 기억해 두세요.docker history nginx:latest
명령으로 latest 이미지의 생성 과정과 용량을 확인해봅시다.docker history nginx:latest
ADD file:4ff85d9f6aa2467...
와 69.3 MB
부분이 nginx:stable과 같습니다. 두 이미지가 레이어를 공유하고 있음을 알 수 있습니다!두 이미지가 각각 133MB, 133MB이지만 실제로는 69.3MB에 해당하는 레이어를 두 이미지가 공유하고 있습니다. 따라서 실제로 두 이미지 크기를 합하면 266MB가 아닌 196.7MB가 됩니다.
이처럼 도커로 작성된 컨테이너는 레이어를 재사용하기 때문에 여러 이미지를 내려받더라도 디스크 용량을 효율적으로 사용할 수 있습니다.
참고로 docker history
에서 나오는 내용들은 도커 컨테이너 이미지 자체를 만드는 명령을 보여주는 것입니다.
이러한 내용이 담긴 도커파일(Dockerfile)은 잠시 후에 살펴보겠습니다.
컨테이너의 이미지 구조를 살펴봤으니 이제 컨테이너를 생성하겠습니다.
먼저 내려받은 이미지를 기반으로 새로운 컨테이너를 실행해 어떻게 구성돼 있는지 살펴봅시다.
docker run -d --restart always nginx
docker run
으로 컨테이너를 생성하면 결과값으로 fe07... 과 같은 16진수 문자열이 나옵니다. 이런 문자열은 컨테이너를 식별할 수 있는 고유한 ID입니다. ID는 실행할 때마다 다르게 표시되므로 여러분이 실행한 결과는 이와 다를 수 있습니다. 고유한 ID가 생성된다는 것을 기억하고 진행을 위해 ID를 기록해둡니다.(fe0703fa797efe2c9feaefbfb6bbcf771b62b105c2a5f7564b2cd0d768b49806)docker run [옵션] <사용할 이미지 이름>[:태그 | @다이제스트]
이고, 태그와 다이제스트는 생략할 수 있습니다. 여기서 사용된 옵션은 다음 두 가지 입니다.-d(--detach)
: 컨테이너를 백그라운드에서 구동한다는 의미입니다. 옵션을 생략하면 컨테이너 내부에서 실행되는 애플리케이션의 상태가 화면에 계속 표시됩니다. 이 상태에서 빠져나오려고 Ctrl+C
를 누르면 애플리케이션뿐만 아니라 컨테이너도 함께 중단됩니다. 따라서 계속 작동해야 하는 서버나 DB같은 프로그램은 -d 옵션을 붙여 백그라운드에서 작동하게 합니다.--restart always
: 컨테이너의 재시작과 관련된 정책을 의미하는 옵션입니다. 프로그램에 예상하지 못한 오류가 발생하거나 리눅스 시스템에서 도커 서비스가 중지되는 경우에 컨테이너도 작동이 중지됩니다. 이때 중지된 컨테이너를 즉시 재시작하거나 리눅스 시스템에서 도커 서비스가 작동할 때 컨테이너를 자동으로 시작하도록 설정할 수 있습니다. 앞으로는 가상 머신을 중지한 후 다시 실행해도 자동으로 컨테이너가 기존 상태를 이어 갈 수 있게 --restart always
옵션을 사용하겠습니다.옵션에 따라 달라지는 컨테이너 시작 방법
컨테이너가 비정삭적으로 종료되거나 리눅스 시스템에서 도커 서비스가 시작될 때--restart
옵션 값에 따라 일어나는 일을 다음과 같습니다.
값 | 컨테이너 비정상 종료 시 | 도커 서비스 시작 시 |
---|---|---|
no(기본값) | 컨테이너를 재시작하지 않음 | 컨테이너를 시작하지 않음 |
on-failure | 컨테이너를 재시작함 | 컨테이너를 시작함 |
always | 컨테이너를 재시작함 | 컨테이너를 시작함 |
unless-stopped | 컨테이너를 재시작함 | 사용자가 직접 정지하지 않은 컨테이너만 시작함 |
docker ps
명령으로 생성한 컨테이너 상태를 확인합니다. 여기서 ps는 프로세서 상태(process status)를 의미합니다. 도커의 많은 명령어는 리눅스의 기본 명령어에서 가져왔습니다.
docker ps
조회 결과에서 각 열의 의미는 다음과 같습니다.
CONTAINER ID : 컨테이너를 식별하기 위한 고유 ID입니다. ID에는 docker run을 실행한 결과가 일부 표시되는데, 이를 통해 이미지나 컨테이너를 식별할 수 있습니다. (fe0703fa797e)
IMAGE : 컨테이너를 만드는 데 사용한 이미지로, 여기서는 nginx를 사용했습니다. (nginx)
COMMAND : 컨테이너가 생성될 때 내부에서 작동할 프로그램을 실행하는 명령어입니다. 여기서는 /docker-entrypoint.sh이며 해당 셸이 nginx 이미지로 컨테이너가 생성될 때 nginx 프로그램을 호출해서 서비스를 할 수 있도록 해줍니다. ("/docker-entrypoin...")
CREATED : 컨테이너가 생성된 시각을 표시합니다. (14 minutes ago)
STATUS : 컨테이너가 작동을 시작한 시각을 표시합니다. CREATED와 달리 컨테이너를 중지했다가 다시 시작할 경우 초기화 됩니다. (Up 14 minutes)
PORTS : 컨테이너가 사용하는 포트와 프로토콜을 표시합니다. 80/tcp는 컨테이너 내부에서 80번 포트와 TCP 프로토콜을 사용한다는 뜻입니다. 현재는 해당 포트로 컨테이너에 접속할 수 없습니다. 이 내용은 나중에 설명하겠습니다. (80/tcp)
NAMES : 컨테이너 이름을 표시합니다. 이름은 docker run
에 --name <이름>
옵션으로 직접 지정할 수도 있지만, 지정하지 않으면 컨테이너가 시작될 때 도커가 임의로 부여한 값이 나타나빈다. 여기서는 keen_rosalind
라는 이름이 임의로 부여됐습니다.
docker ps -f id=fe07
명령으로 컨테이너를 지정해 검색합니다(앞에서 기록해둔 16진수 ID를 넣어야합니다)
docker ps -f id=fe07
docker ps
에 -f(--filter) <필터링 대상>
옵션을 주면 검색 결과를 필터링할 수 있습니다.key(대상)=value(값)
형식으로 입력합니다. 이떄 value와 정확하게 일치하지 않더라도 value에 해당하는 문자열을 포함하는 경우는 필터링 됩니다. 생성된 nginx 컨테이너는 마스터 노드 내부에 존재하므로 curl 127.0.0.1
명령으로 컨테이너가 제공하는 nginx 웹 페이지 정보를 가지고 오게 합니다.
curl 127.0.0.1
docker ps
에서 설명한 80번 포트로 컨테이너에 접속할 수 없다는 문제가 왜 발생하는지 알아봅시다.앞에서 컨테이너의 PORTS 열에 표시되는 80/tcp는 컨테이너 내부에서 TCP 프로토콜의 80번 포트를 사용한다는 의미라고 했습니다.
하지만 curl로 전달한 요청은 로컬호스트(127.0.0.1)의 80번 포트로 전달만 될 뿐 컨테이너까지는 도달하지 못합니다.(호스트 네트워크의 80번 포트까지는 전달됐는데, 도커 네트워크의 80번 네트워크로는 전달되지 않습니다.)
즉, 호스트에 도달한 후 컨테이너로 도달하기 위한 추가 경로 설정이 돼 있지 않은 것입니다.
현재 상태로는 가상 머신 호스트에서 컨테이너의 80번 포트에 접속할 수 업습니다. 이를 좀 더 쉽게 이해하려면 마스터 노드의 주소인 192.168.1.10으로 요청하고 동일하게 아무것도 표시되지 않는 것을 확인해 보면 됩니다.
즉, 마스터 노드의 입장에서는 웹 브라우저를 통한 접속은 기본 포트인 80번에서 처리하려고 하나 이에 대해 응답해 줄 주체가 없는 것입니다.
따라서 응답을 컨테이너에서 처리해주기를 원한다면 80번으로 들어온 것을 컨테이너에서 바로 받아줄 수 있는 포트로 연결해 주는 설정이 추가로 필요합니다. 추가 설정하는 방법은 바로 다음 실습에서 확인해봅시다.
작동 중인 컨테이너의 설정을 변경할 수 있나요?
컨테이너는 변경 불가능한 인프라(immutable infastructure)를 지향합니다.
변경 불가능한 인프라는 초기에 인프라를 구성하면 임의로 디렉토리 연결이나 포트 노출과 같은 설정을 변경할 수 없습니다.
따라서 컨테이너에 적용된 설정을 변경하려면 새로운 컨테이너를 생성해야 합니다.
이러한 특성 덕분에 컨테이너로 배포된 인프라는 배포된 상태를 유지한다는 장점이 있습니다.
컨테이너 외부에서도 컨테이너 내부에 접속할 수 있게 새로운 컨테이너를 구동해보겠습니다!
-p 8080:80
옵션을 추가해 새로운 컨테이너(nginx-exposed)를 실행합니다.docker run -d -p 8080:80 --name nginx-exposed --restart always nginx
-p(--publish)
는 외부에서 호스트로 보낸 요청을 컨테이너 내부로 전달하는 옵션으로, -p <요청 받을 호스트 포트>:<연결할 컨테이너 포트>
형식으로 사용합니다.(호스트 네트워크가 8080으로 요청을 받고 도커 네트워크의 80번 포트로 전달)-f name=nginx-exposed
옵션을 추가해 필터링합니다.docker ps -f name=nginx-exposed
0.0.0.0:8080->80/tcp
: 0.0.0.0의 8080번 포트로 들어오는 요청을 컨테이너 내부의 80번 포트로 전달한다는 의미입니다. 0.0.0.0은 존재하는 모든 네트워크 어댑터를 의미합니다. m-k8s 호스트는 자기 자신을 나타내는 127.0.0.1과 외부에 노출된 192.168.1.10 등의 IP를 가지고 있는데, 요청이 호스트에 할당된 어떤 IP의 8080번 포트로 들어오더라도 컨테이너 내부의 80번 포트로 전달됩니다.nginx-exposed
: 현재 작동 중인 컨테이너의 이름입니다. 이전에는 컨테이너 이름을 지정하지 않아 NAMES에 도커가 임의로 부여한 이름이 표시됐지만, 이번에는 --name
옵션으로 지정한 이름이 표시됩니다.192.168.1.10:8080
을 입력해 가상 머신을 호스팅하는 노트북에서 컨테이너로 접근할 수 있는지 확인합니다.192.168.1.10:8080
kubectl get nodes -o wide
192.168.1.101:8080
현재 nginx 내부에는 따로 작성한 파일이 없기 때문에 기본 페이지를 보여줍니다.
따라서 사용자가 원하는 페이지를 출력하기 위해서는 웹 페이지와 관련된 화면을 별도로 작성해야 합니다.
컨테이너 내부에서 웹 페이지 파일을 변경할 수 있지만 이런 경우, 컨테이너를 다시 생성하게 되면 매번 웹 페이지 파일을 전송해야 합니다.
그러므로 영속적으로 웹 페이지 파일을 사용하기 위해서는 특정 디렉토리와 컨테이너 내부의 디렉토리를 연결하는 것이 효과적인 사용법입니다!
도커는 컨테이너 내부에서 컨테이너 외부의 파일을 사용할 수 있는 방법으로 크게 4가지를 제공합니다.
docker cp
- docker cp <호스트 경로> <컨테이너 이름>:<컨테이너 내부 경로>
형식으로 호스트에 위치한 파일을 구동 중인 컨테이너 내부에 복사
Dockerfile ADD
- 이미지는 Dockerfile을 기반으로 만들어지는데, 이때 Dockerfile에 ADD라는 구문으로 컨테이너 내부로 복사할 파일을 지정하면 이미지를 빌드할 때 지정한 파일이 이미지 내부로 복사됌
바인드 마운트
- 호스트의 파일 시스템과 컨테이너 내부를 연결해 어느 한쪽에서 작업한 내용이 양쪽에 동시에 반영되는 방법
볼륨
- 호스트의 파일 시스템과 컨테이너 내부를 연결하는 것은 바인드 마운트와 동이랗지만, 호스트의 특정 디렉터리가 아닌 도커가 관리하는 볼륨을 컨테이너와 연결
사용할 수 있는 방법이 총 4가지가 있지만, 현재 웹 페이지를 연결하는 것처럼 오랫동안 고정된 내용을 각 사용자마다 다르게 취하는 경우에는 바인드 마운트 또는 볼륨이 효과적인 방법입니다.
컨테이너 내부에서 외부 파일을 사용하는 방법을 정리하면 다음과 같습니다.
구분 | docker cp | Dockerfile ADD | 바인드 마운트 | 볼륨 |
---|---|---|---|---|
컨테이너 적용 | 구동 중 복사 | 이미지 생성 시 복사 | 구동 시 디렉터리 연결 | 구동 시 도커의 볼륨 연결 |
파일 보관 위치 | 컨테이너 내부 | 컨테이너 내부 | 호스트(디렉토리) | 호스트(도커 볼륨) |
주 활용 용도 | 임시 파일 | 컨테이너 생성 시 필요한 파일 | 보존이 필요한 파일 | 보존이 필요한 파일 |
관리 편의성 | 좋지 못함 | 좋음 | 좋음 | 매우 좋음 |
파일 보존성 | 좋지 못함 | 좋음 | 매우 좋음 | 매우 좋음 |
4가지 방법 중 nginx 웹 페이지를 사용자가 변경하기에 가장 용이한 바인드 마운트와 볼륨을 실습을 통해 확인해 보겠습니다!
호스트와 컨테이너를 연결하려면 연결 대상이 되는 컨테이너 내부의 디렉토리 구조를 먼저 알아야 합니다.
현재 정상적으로 노출된 nginx 컨테이너의 구조를 살펴보면 처음 접속할 때 노출되는 페이지는 /usr/share/nginx/html/index.html입니다. 따라서 우리가 수정해야 하는 파일이 index.html이며, 이러한 경로 설정은 /etc/nginx/nginx.conf에 존재합니다.
컨테이너 내부에 연결할 /root/html/ 디텍토리를 호스트에 생성합니다.
mkdir -p /root/html
docker run
명령으로 nginx-bind-mounts라는 이름의 컨테이너를 구동하고, 컨테이너의 /usr/share/nginx/html/ 디렉토리와 호스트의 /root/html/ 디텍터리를 연결합니다.
-v(--volume)
는 호스트 디렉토리와 컨테이너 디렉토리를 연결하는 옵션이로, -v <호스트 디렉토리 경로>:[컨테이너 디텍토리 경로]
형식으로 사용합니다.docker run -d -p 8081:80 -v /root/html:/usr/share/nginx/html --restart always --name nginx-bind-mounts nginx
nginx-bind-mounts 컨테이너를 조회해 STATUS가 정상(Up n minutes)인지 확인합니다.
docker ps -f name=nginx-bind-mounts
컨테이너가 정상적으로 구동했음을 확인하면 컨테이너 내부와 연결된 /root/html/ 디렉토리를 확인합니다. 이 디렉토리는 사용자가 호스트에 생성한 빈 비렉토리인데, 조회하면 여전히 비어있습니다.
ls /root/html
웹 브라우저에서 192.168.1.10:8081에 연결해 nginx-bind-mounts 컨테이너에서 실행되는 nginx에 접속되는지 확인합니다. 이 때 nginx의 기본 화면이 아닌 오류 화면이 표시됩니다.
cp 명령어로 호스트 디렉터리와 컨테이너 디렉터리를 연결할 때 사용할 index.html을 /root/html/에 복사합니다. 저자가 미리 작성해 둔 파일을 사용하겠습니다. 그리고 제대로 저장되었는지 확인합니다.
cp ~/_Book_k8sInfra/ch4/4.2.3/index-BindMount.html /root/html/index.html
ls /root/html
브라우저에서 192.168.1.10:8081로 다시 접속해 index.html이 정상적으로 표시되는지 확인합니다.
컨테이너 내부 확인하기
호스트 디렉터리와 컨테이너 디렉터리가 연결된 경우에 컨테이너 내부를 보려면 어떻게 할까요?
kubectl과 동일하게 docker에도 컨테이너에 접속하거나 내부에 명령을 전달하고 결괏값을 반환하는 exec라는 명령어가 있습니다.
docker exec <컨테이너 ID | 이름> <명령어>
형식으로 실행하면 컨테이너에서 명령을 실행하고 결과를 보여 줍니다. 결과를 보면 바인드 마운트를 통해 호스트와 ㅇ녀결된 index.html이 확인됩니다.
docker exec nginx-bind-mounts ls /usr/share/nginx/html
docker의 많은 기능과 명령이 kubectl과 동일하거나 비슷합니다!
볼륨(volume)은 도커가 직접 관리하며 컨테이너에 제공하는 호스트의 공간입니다.
앞서 배운 바인드 마운트와는 어떤 차이가 있는지 실습으로 확인해봅시다!
볼륨은 바인드 마운트와 다르게 덮어쓰기가 아니고 동기화됩니다(호스트 디렉토리가 비어있을 경우에)!
볼륨을 생성합니다. 이때 nginx-volume은 생성할 볼륨의 이름입니다.
docker volume create nginx-volume
생성된 볼륨을 조회합니다. 볼륨에 적용된 드라이버 종류와 실제 호스트와 연결된 디렉토리, 볼륨 이름 등을 조회할 수 있습니다.
docker volume inspect nginx-volume
볼륨으로 생성된 디렉터리를 확인합니다.
ls /var/lib/docker/volumes/nginx-volume/_data
컨테이너에 연결할 볼륨을 호스트에 생성했으니 호스트와 컨테이너의 디렉토리를 연결할 컨테이너를 구동합니다.
-v [볼륨 이름]:[컨테이너 디렉토리]
입니다.docker run -d -v nginx-volume:/usr/share/nginx/html -p 8082:80 --restart always --name nginx-volume nginx
볼륨 디렉토리의 내용을 다시 확인합니다.
ls /var/lib/docker/volumes/nginx-volume/_data
웹 브라우저로 192.168.1.10:8082를 연결해 nginx-volume 컨테이너의 nginx에 접속합니다.
nginx-volume에 cp 명령어로, 바꿀 파일을 볼륨 디렉터리로 복사해 볼륨에서 변경한 내용이 컨테이너 디렉터리에 동기화 되는지 테스트합니다.
cp ~/_Book_k8sInfra/ch4/4.2.3/index_Volume.html /var/lib/docker/volumes/nginx-volume/_data/index.html
웹 브라우저에서 다시 192.168.1.10:8082에 접속하면 바뀐 index.html이 표시됩니다(동기화 되었습니다!).
이처럼 볼륨을 사용하면 컨테이너에 존재하는 파일을 그대로 보존할 수도 있고, 필요할 때 변경해서 사용할 수도 있습니다.
또한, 사용 중인 볼륨을 docker volume ls
명령으로 조회할 수 있고, docker volume rm
명령으로 삭제할 수도 있어서 바인드 마운트보다 관리하기가 더 쉽습니다!
볼륨 경로는 고정돼 있나요?
실습에서 볼륨은 /var/lib/docker/volumes/ 디렉터리 안에 생성됐습니다.
리눅스의 시스템 디렉터리 정의에 따르면 var 디렉터리는 시스템이 사용하는 파일 중 로그, 캐시, 상태 정보 등을 저장합니다.
docker volume create
를 실행할 때 기본으로 설정된 /var/lib/docker/volumes 디렉터리를 그대로 사용하면 도커의 볼륨을 사용하는 데이터가 늘어날수록 시스템의 다른 기능이 사용해야 하는(로그, 캐시 등) 용량까지 차지하는 문제가 생길 수 잇습니다.
리눅스 시스템을 운영할 때 큰 용량의 데이터를 저장하는 경로는 별도로 분리해 충분한 용량을 할당하는 경우가 많습니다.
따라서 대량의 데이터를 저장하고 연결하려는 목적이면 기본 디렉토리가 아닌 충분한 용량이 확보된 디렉토리 경로를 설정해야 할 필요가 있습니다.
도커 버전에 따라서 경로를 분리하는 기능의 지원 여부가 다른데, 예제에서 사용힌 버전은 1.13.1입니다.
docker-ce 17.05 버전부터는 도커의 데몬 설정에서 볼륨이 저장되는 경로를 지정하는--data-root
옵션이나 호스트 경로와 컨테이너 경로를 명확하게 연결하는--mount
옵션을 제공합니다.
사용이 끝나고 더 이상 사용하지 않을 컨테이너라면 공간을 확보히기 위해 삭제하는 것이 좋습니다.
그래서 이번에는 컨테이너를 안전하게 삭제하는 방법을 알아봅시다!
컨테이너나 이미지를 삭제하기 전에 먼저 컨테이너를 정지해야 합니다!
삭제할 때 말고도 동일한 호스트의 포트를 사용하는 컨테이너를 배포하거나 작동 중인 컨테이너 사용 자체를 종료할 때도 먼저 컨테이너를 정지해야 합니다.
docker ps -f ancestor=nginx
명령으로 nginx 이미지를 기반으로 생성된 컨테이너를 조회합니다.
ancestor
키는 컨테이너를 생성하는 데 사용한 이미지를 기준으로 필터링합니다.docker ps -f ancestor=nginx
컨테이너를 정지하는 명령은 docker stop <컨테이너 이름 | ID>
입니다. 4번째 행에 표시된 컨테이너인 keen_rosalind
를 정지해보겠습니다. 컨테이너가 정지되면 입력한 컨테이너 이름이 결과로 표시됩니다.
docker stop keen_rosalind
3번째 행에 표시된 nginx-exposed 컨테이너를 컨테이너 ID로 정지해 보겠습니다. 해당 컨테이너의 ID는 cccc6fa9409b
이므로 docker stop cccc
으로 정지합니다. 컨테이너 정상적으로 정지되면 입력한 컨테이너 이름이 결과로 표시됩니다.
docker stop cccc
컨테이너를 하나씩 정지하면 번거로울 수 있으니 nginx 이미지를 사용하는 모든 컨테이너를 한꺼번에 정지해 보겠습니다. 컨테이너 조회 명령에 -q(--quite)
옵션을 추가해 컨테이너 ID만 출력합니다.
docker ps -q -f ancestor=nginx
앞서 사용한 명령을 docker stop
에서 인자로 사용하도록 $()안에 넣습니다. 실행하면 nginx를 이미지로 사용하는 모든 컨테이너가 정지됩니다.
docker stop $(docker ps -q -f ancestor=nginx)
모든 컨테이너가 정지됐는지 다시 조회해봅시다!
docker ps -q -f ancestor=nginx
컨테이너가 모두 정지됐으나 삭제된 것은 아닙니다. 정지된 컨테이너를 포함해 모든 컨테이너 목록을 조회합니다.
docker ps
에 -a(--all)
옵션을 주면 정지된 컨테이너를 포함해 nginx 이미지를 기반으로 생성한 컨테이너가 모두 조회됩니다. docker start <컨테이너 이름 | ID>
를 실행합니다.docker ps -a -f ancestor=nginx
정지한 컨테이너가 더 이상 필요 없으면 삭제해 사용 중인 컨테이너 목록을 관리하고, 사용하지 않는 컨테이너 이미지를 삭제해 저장 공간을 확보합니다.
docker rm <컨테이너 이름 | ID>
명령으로 삭제합니다. 이 명령은 한꺼번에 컨테이너를 중지할 때 사용했던 방법을 그대로 사용합니다. 기존 옵션과 조합해 현재 정지된 모든 컨테이너를 삭제해보겠습니다.docker rm $(docker ps -aq -f ancestor=nginx)
정지하지 않은 컨테이너 삭제 시 발생하는 오류와 해결책
컨테이너를 정지하지 않았거나 컨테이너의 자동 재시작 옵션 때문에 컨테이너가 구동 중일 때 삭제를 진행하면 'You cannot remove a running container' 라는 오류가 발생합니다. 이런 경우에는docker stop
명령으로 컨테이너를 다시 정지한 후 삭제하거나,docker rm
에-f(--force)
옵션을 붙여 실행 중인 컨테이너를 강제로 삭제합니다. 하지만 강제 삭제는 의도하지 않은 삭제가 일어날 수 있으니 주의해야 합니다.
컨테이너가 정상적으로 삭제됐는지 docker ps -a -f ancestor=nginx
명령으로 확인합니다. 모든 컨테이너가 삭제되면 조회되는 컨테이너가 없는게 정상입니다.
docker ps -a -f ancestor=nginx
컨테이너는 모두 삭제했지만, 내려받은 이미지는 아직도 남아서 공간을 차지하고 있습니다. 용량 확보를 위해 더 이상 필요없는 이미지를 삭제하겠습니다.
nginx:latest
이미지와 nginx:stable
이미지를 하나씩 삭제할 수도 있지만, docker rmi $(docker images -q nginx)
명령으로 이미지를 한번에 삭제해보겠습니다.rmi
는 이미지를 지운다는 의미입니다.(rm + i)docker rmi $(docker images -q nginx)
이로써 도커로 컨테이너를 다루는 방법을 모두 살펴봤습니다. 이제 다음 포스팅에선 직접 컨테이너 이미지를 만들어 쿠버네티스에서 사용하는 방법을 알아봅시다!
본 게시물은 "컨테이너 인프라 환경 구축을 위한 쿠버네티스/도커 - 조훈,심근우,문성주 지음(2021)" 기반으로 작성되었습니다.