연세대 인공지능학회 YAI

[MLOps] Docker Basics / Images & Containers / Managing Images & Containers 본문

MLops

[MLOps] Docker Basics / Images & Containers / Managing Images & Containers

_YAI_ 2022. 8. 13. 14:11

Docker Basics / Images & Containers / Managing Images & Containers

*YAI 8기 김상현님이 MLOps팀에서 작성한 글입니다.

 


Docker Basics

What is Docker?

Docker

Docker컨테이너(container)를 만들고 이를 관리하는 기술입니다. 이때, 컨테이너란 표준화된 소프트웨어의 단위(a standardized unit of software)를 의미합니다. 예를 들어서 NodeJS 어플리케이션 컨테이너라면 어플리케이션 코드뿐만 아니라 의존성(dependencies)을 포함합니다. 컨테이너는 모두 동일하기 때문에, 같은 행동(behavior)을 할 것이라고 기대할 수 있습니다.

Container

컨테이너는 규격화된 모양을 갖고 있으며, 생성된 이후에는 자체적으로 보관 및 격리된다는 특징을 갖습니다. 따라서 특정 컨테이너의 콘텐츠는 다른 컨테이너의 콘텐츠와 섞이지 않습니다. 따라서 소프트웨어의 경우에는 필요한 코드뿐만 아니라 의존성을 한 번에 보관하여, 도커가 실행되는 모든 곳에서 이를 가져올 수 있도록 합니다. 따라서 어디든 누구든 상관없이 동일한 환경에서 어플리케이션을 실행할 수 있도록 하는 것입니다.

그리고 이러한 컨테이너에 대한 지원은 최신 운영체제라면 가능하며, 특히 Docker는 일반적으로 리눅스 환경에서 많이 사용됩니다. 따라서 Docker는 컨테이너의 생성 및 관리를 쉽게 해주는 도구입니다. 실제로 컨테이너는 Docker 없이도 만들 수 있지만, Docker는 업계의 사실상 표준이 되었습니다.

Why Containers?

Standardized Application Packages

왜 표준화된 어플리케이션 패키지가 필요한 지에 대해서 살펴보면 아래와 같습니다.

  • Different Development & Production Environments
  • 로컬 개발 환경과 배포 환경이 다른 경우에 이를 맞춰줄 수 있습니다. 만약 특정 어플리케이션이 특정 버전 이상의 의존성을 요구할 때, 개발 환경에서는 동작했던 코드가 배포 환경에서는 동작하지 않을 수 있습니다. 이를 나중에 발견하는 것은 상당히 힘든 일인데, 동일한 도커 환경에서 개발 및 배포가 이뤄지며 이를 해결할 수 있습니다.
  • Different Development Environments within a Team/Company
  • 마찬가지로 협업 관계에 있는 팀 혹은 사람들끼리 다른 개발 환경일 가능성이 농후합니다. 따라서 복잡한 종속성이 존재하는 프로젝트의 경우에는 항상 동작할 것이라는 보장을 할 수 없습니다. 소프트웨어 개발에서 가장 중요한 것 중 하나는 “재현성” (reproducibility)입니다.
  • Clashing Tools/Versions Between Different Projects
  • 개인이 작업하더라도, 복수 개의 프로젝트를 작업하는 경우에는 둘 사이에 버전, 종속성 등의 차이가 존재할 수 있습니다. 특정 버전을 삭제하고 재설치하는 수고로움 대신에 도커 환경에서 특정 버전으로 고정시켜서, 단순히 다른 컨테이너를 실행시키는 것만으로도 둘 이상의 환경을 오고갈 수 있습니다.

Virtual Machines

재현가능한 환경적인 문제는 가상머신(Virtual Machine, VM)으로도 해결이 가능합니다. 그런데 왜 컨테이너를 사용하는 것일까요?

가상머신은 컴퓨터에 원래 설치되어 있는 운영체제(Host OS)에 추가적인 VM 소프트웨어를 설치하여, 가상 운영체제(Virtual OS, Guest OS)와 통신을 합니다. 이를 통해서 캡슐화 (encapsulated) 및 분리된 (seperated) 환경을 제공한다는 것은 장점입니다.

그러나 가상 운영체제 → VM 엔진→ 호스트 OS로 가는 명령어의 체계는 매우 큰 오버헤드(Overhead)를 야기합니다. 특히 복수 개의 가상머신을 동시에 가동할 경우에는 모두 독립적으로 동작하고 이에 필요한 하드디스크 공간 및 메모리 공간도 상당합니다. 따라서 속도 및 성능이 저하됩니다. 또한, 매번 가상머신을 설치할 때마다 설정(config)을 해줘야하며, 이를 배포하는 것도 어렵다는 점이 큰 단점입니다.

Containers

여전히 컨테이너를 사용하더라도 Host OS는 존재합니다. 이 위에 VM 엔진 대신에 OS에서 제공하거나 Docker와 같은 툴이 제공하는 컨테이너 지원 레이어가 추가됩니다. 이 위에 Docker 엔진이 실행되며, 그 위에서 컨테이너가 실행되게 됩니다.

가상머신과 가장 큰 차이점은 컨테이너에 운영체제 구성 요소의 일부가 포함되더라도, 이는 가상머신에 필요로 양에 비해서는 매우 작고 가볍다(lightweight)는 것입니다. 따라서 OS 및 하드디스크 용량에 주는 부담이 적으면서도 독립적이고 동일한 환경에서 동작하도록 보장할 수 있습니다.

또한, 컨테이너를 구성하는 configuration 파일을 만들어서, 이를 다른 사람들과 공유해 다른 사람들이 동일한 컨테이너를 다시 만들 수 있도록 할 수 있다는 것입니다. 혹은 컨테이너를 이미지(image)라고 불리는 것에 빌드(build)할 수도 있습니다. 이 컨테이너 혹은 이미지를 다른 사람들과 공유하여 동일한 환경에서 코드가 실행될 수 있도록 보장할 수 있습니다.

Docker Setup

Installation

도커 설치는 운영체제 환경에 따라서 차이가 존재합니다. 따라서 docker 공식 홈페이지에 접속하여, 이를 읽어보면서 설치를 따라하는 것을 추천합니다.

특이한 점은 macOS나 Windows의 경우에는 Docker Desktop 혹은 Docker Toolbox라는 GUI 기반의 응용프로그램을 설치해야 하지만, Linux의 경우에는 별도의 GUI가 존재하지 않는다는 점입니다. 대신 Linux에서는 Docker Engine를 설치하게 됩니다. 그 이유는 Linux는 운영체제 자체에서 Docker가 사용하는 기술을 지원하고 있기 때문입니다.


Images & Containers

이미지(Image)와 컨테이너(Container)의 차이를 살펴볼 필요가 있습니다. 컨테이너는 앞서 애플리케이션 코드, 실행하기 위한 환경 등을 비롯해 무엇이든 포함하는 작은 패키지라고 할 수 있습니다. 즉, 이를 “소프트웨어의 단위" (unit of software)를 실행하는 것이라고 생각하면 좋습니다.

한편, 도커를 이용해서 작업할 때는 이미지(image)라는 디졸버(dissolver) 개념 역시 필요합니다. 이미지는 컨테이너의 블루프린트(blueprint) 혹은 템플릿(templete)으로 사용될 것이기 때문입니다. 이미지는 코드와 더불어 코드를 실행하는데 필요한 도구들을 포함합니다. 그런 다음 이를 바탕으로 복수 개의 컨테이너가 만들어지고 이것들이 실행되어 코드가 동작하게 됩니다. 즉, 이미지는 모든 설정 명령과 모든 코드가 포함된 공유 가능한 패키지입니다. 반면, 컨테이너는 “실행되는" (running) 어플리케이션이 됩니다.

Finding/Creating Images

컨테이너를 만들기 위해서는 이미지가 필요합니다. 이때, 기존에 존재하고 사전에 구축된 이미지를 이용할수도, 이미지를 직접 만들수도 있습니다.

Find Images

Docker Hub(https://hub.docker.com) 에 접속하면 이미지들을 검색할 수 있습니다. 예를 들어, “node”라고 Docker Hub에서 검색할 경우 최상단에 Docker Official Images라는 딱지가 붙은 이미지를 발견할 수 있습니다. 이를 실행시키는 방법은 터미널에서 아래의 명령어를 수행하는 것입니다.

docker run node

이를 실행하면, 해당 이미지 node 를 기반으로 한 컨테이너를 생성하게 됩니다. 다시 말해, 컨테이너는 실제로 실행 중인 이미지 인스턴스이기 때문입니다. 이미지에는 환경 설정 코드가 포함되며, 노드 이미지에는 노드 설치 코드가 포함됩니다. 따라서 이 경우에는 노드 인터렉티브 쉘을 간단히 실행할 수 있습니다.

이 경우 별다른 일이 일어나지는 않은 것처럼 보입니다. 그런데 실제로는 이미지가 다운르드 되었고, 이를 기반으로 컨테이너를 만들어서 실행이 이뤄졌습니다. 그러나 위 이미지는 별다른 일을 수행하지 않았습니다. 이를 바탕으로 인터렉티브 쉘(shell)을 얻기 위해 명령어를 입력할 수 있습니다. 그러나 도커 자체는 분리된(isolated) 환경에서 동작하고 있으므로, 이 쉘이 우리 사용자에게 노출이 되지는 않습니다.

이를 확인하기 위해서는 아래의 명령어를 입력하여 도커가 생성한 모든(all) 컨테이너 및 프로세스를 표시하게 합니다. 만약 -a 플래그가 없다면 현재 실행 중인 프로세스만으르 보여줍니다.

docker ps -a

위 그림에서 5분 전 5 minutes agonode 이미지를 965918eea50e라는 컨테이너 ID로 생성한 것을 볼 수 있습니다. 그리고 해당 이미지는 종료가 되었으며 Exited (0) 5 minutes ago 자동으로 생성된 임의의 이름 happy_mirzakhani도 볼 수 있습니다. 그럼에도 불구하고 이렇게 실행된 인터렉티브 쉘(shell)은 자동으로 사용자에게 노출되지 않습니다.

docker run -it node

이를 입력하면 아래와 같이 node의 쉘을 확인할 수 있습니다.

 

따라서 -it 플래그를 추가하면 도커에게 컨테이너 내부에서 호스트 머신으로 대화형 세션(interactive session)을 노출하고 싶다는 것을 알리게 됩니다. -i는 interactive mode의 약자이고, -t는 pseudo-TTY의 약자로 리눅스 디바이스 드라이브 중 콘솔이나 터미널을 의미한다고 합니다.

Create Images

Dockerfile을 만들어서 이미지를 직접 만들 수도 있습니다. 아래와 같은 형태로 가능합니다.

FROM node

WORKDIR /app

COPY . ./

RUN npm install

EXPOSE 80

CMD ["node", "server.js"]

FROM

FROM은 이미지를 생성할 때 베이스가 되는 이미지를 의미합니다. 여기서는 node를 사용합니다.

COPY

COPY 명령어는 두 개의 인자를 받습니다. 처음 인자는 복사되어야 할 파일들(host system)이 있는 곳을 의미하고, 다음 인자는 복사될 경로(container system)를 의미합니다. 이때, .을 사용하면 Dockerfile이 있는 경로 및 하위 경로 전체를 복사하게 되는데, 이때 Dockerfile 자체는 제외됩니다. 일반적으로는 위에서처럼 /app과 같이 특정 경로를 지정해주는 것이 더 좋다고 합니다.

RUN

RUN은 명령어를 실행할 수 있도록 해줍니다. 그러나 여기서 문제점은 기본 working directory가 Docker file system의 root라는 것입니다. 위에서는 /app 폴더에 복사를 하고 있기 때문에, working directory를 이동시켜줘야 합니다.

WORKDIR

따라서 모든 명령어를 실행하기 이전에 WORKDIR 명령어를 수행하여 도커 컨테이너의 작업 디렉토리를 설정하게 됩니다. 위에서는 /app을 working directory로 지정합니다. 따라서 위의 COPY와 RUN도 절대 경로 /app에서 이뤄지게 됩니다.

CMD

여기서 명심해야 하는 것은 이미지가 컨테이너의 템플릿이어야 한다는 점입니다. 이미지를 실행하는 것이 아니라 이미지를 기반으로 컨테이너가 실행되게 됩니다. 즉, 컨테이너를 시작하는 경우에만 서버를 시작하고 싶은 것입니다. 따라서 RUN을 통해서 명령을 실행하는 것이 아닌 CMD를 통해서 명령을 실행합니다.

RUN과 달리 CMD는 이미지가 생성될 때 실행되는 코드가 아니라, 이미지를 기반으로 컨테이너가 시작될 때 실행된다는 차이점이 존재합니다. 또한, 문법적으로도 차이가 존재하는데, string으로 된 array를 전달해야 합니다. 만약 CMD를 지정해주지 않는다면, base image의 CMD가 실행되게 됩니다. 그러나 만약 base image가 없거나 CMD가 없을 경우에는 에러를 만나게 됩니다.

EXPOSE

다만 이러한 경우에는 어플리케이션이 실행되는 것을 볼 수 없습니다. 그 이유는 도커 컨테이너가 격리되어 있기 때문입니다. 따라서 내부적인 네트워크도 별도로 존재합니다. 그리고 해당 포트를 도커 컨테이너가 우리에게 노출하지 않습니다. 따라서 Dockerfile을 통해서 CMD를 실행하기 전에 이를 설정해줘야 합니다. 참고로 CMD는 대부분 가장 마지막에 수행해야 합니다. 그래서 EXPOSE 80을 통해 80번 포트를 열어주게 됩니다.

그러나 실제로 EXPOSE는 아무 일도 수행하지 않습니다. 문서화(documentation) 목적으로 작성해두는 것입니다. 이에 대해서는 뒤에서 더 다루겠습니다.

Run Images

이제는 docker run이 아니라, docker build를 이용해서 이미지를 빌드할 수 있습니다. 따라서 Docker에게 새로운 커스텀 이미지를 빌드하도록 지시합니다. 이때 Dockerfile을 찾을 수 있는 경로를 제시해야 합니다. 물론 커스텀 이름을 지정해줄 수도 있지만, 일단은 아래와 같은 방식으로 수행하도록 합니다.

docker build .

이후 생성된 이미지의 ID는 Successfully built 334837ba6abd 와 같은 형태로 볼 수 있습니다. 이 ID를 docker run 명령어를 통해서 실행할 수 있습니다.

docker run 334837ba6abd

그러나 이 경우에도 웹 브라우저를 통해 localhost에 방문하더라도, 서버를 볼 수는 없습니다. 비록 Dockerfile에서 포트를 노출했음에도 불구하고 말입니다. 다른 터미널을 열어서 docker ps 명령어를 실행하면 여전히 node가 실행 중인 것을 확인할 수 있습니다. 이를 종료하는 것은 docker stop 명령어입니다. 이 뒤에 자동으로 생성된 컨테이너의 이름을 입력하면 됩니다.

docker stop tender_moore

중요한 것은 포트를 열어주고 이를 호스트에서 받기 위해서는 docker run 뒤에 특별한 옵션을 추가해야 한다는 것입니다. 이미지 이름 앞에 -p 플래그를 추가하는데, 이는 publish를 의미합니다. 이를 통해 도커에게 어떤 로컬 포트가 있는지를 알려줄 수 있습니다. 문법은 host_port:docker_port 입니다.

docker run -p 3000:80 334837ba6abd

이경우 웹 브라우저에서 localhost:3000에 접속하는 경우 어플리케이션이 포트 3000번에 publish가 되었으므로, 정상적으로 프로젝트를 볼 수 있게 됩니다.

Read-only Images

Immutable Images

위에서 어떻게 base image를 불러와 이미지를 만들고, 이를 컨테이너로 만들어서 실행하는 방법까지 배울 수 있었습니다. 그런데 이때, node JS 어플리케이션 코드의 일부를 변경했다고 해보겠습니다. 그리고 다시 docker run -p 3000:80 <image_id> 를 이용해 이미지를 실행할 경우, 해당 변경사항이 반영이 되어 있을까요?

정답은 “코드 변경사항이 반영되어 있지 않다”입니다. 심지어 이전의 컨테이너를 docker start를 통해 재시작한 것이 아니라, 이미지에서 컨테이너를 새롭게 만든 것임에도 불구하고 말입니다. 왜냐하면 빌드된 이미지는 immutable, 즉 변경이 불가능하기 때문입니다. 위의 Dockerfile에서 COPY 명령어를 통해 image filesystem으로 코드의 복사를 명령했습니다. 이때, 기본적으로 복사한 시점에서 소스 코드의 스냅샷을 만듭니다. 즉, 일단 이미지가 빌드된 이후 소스 코드를 수정하더라도, 해당 스냅샷의 복사가 또 일어나지는 않습니다.

따라서 여기서 가장 중요한 점은 이미지는 기본적으로 잠겨있으며, 일단 빌드가 되면 끝이라는 점입니다. 이미지의 모든 것이 읽기 전용이며, 과거에 코드를 복사했기 때문에 이후의 변경 사항은 반영되지 않는 것입니다. 심지어 코드를 삭제하더라도 이미지는 영향을 받지 않습니다.

Rebuild Images

이미지를 다시 빌드하기 위해서는 마찬가지로 아래의 명령어를 실행하면 됩니다.

docker build .

이 과정에서 눈여겨봐야 할 점은 COPY 명령어 이전에는 전부 캐싱(cache)된 이미지를 사용하는데, 복사하는 코드가 달라진 COPY 부터는 캐시된 이미지를 사용하지 않는다는 점입니다. 따라서 Step이 1부터가 아니라 중간 숫자부터 시작하게 됩니다. 요약하자면, 다시 빌드를 수행할 때에는 변경된 부분의 명령과 그 이후의 모든 명령에 대해서만 다시 평가됩니다. Docker는 특정 명령어 행의 수행 결과가 이전과 동일한지 아닌지 여부를 인식하고, 달라지는 부분 이후만 자동적으로 다시 만들기 때문입니다. 이러한 구조를 레이어 기반 아키텍처(layer-based architecture)라고 합니다.

이러한 이미지의 레이어들 위헤 컨테이너가 만들어질 때는 컨테이너 레이어가 추가적으로 붙게 됩니다. 이는 CMD 명령어가 담당하게 됩니다. 이를 통해 코드를 실행 중인 어플리케이션인 이미지 위에 새로운 추가 레이어를 추가합니다. 이는 읽고 쓸 수 있는 레이어라는 특징이 있고, 최종 레이어라는 특징이 있습니다.

따라서 Docker의 동작을 최적화하기 위한 방법 중 하나는 npm install 혹은 pip install 과 같이 환경 설정에 필요한 파일들을 분리하여 복사하는 것입니다. 환경설정을 마친 후에 나머지 코드를 복사하고 이를 실행한다면, 코드가 수정되어 다시 빌드를 수행할 때 npm install 혹은 pip install 이 일어나지 않을 것입니다. 안타깝게도 Docker는 코드를 변경하더라도 패키지 설치 레이어에는 영향이 없다는 것을 알아차릴 수 있는 방법이 없습니다. 따라서 이는 이를 설계하는 사람이 최적화시켜야 하는 부분입니다.

FROM node

WORKDIR /app

COPY package.json /app

RUN npm install

COPY . /app

EXPOSE 80

CMD ["node", "server.js"]

Managing Images & Containers

Introduction

기본적인 동작 원리는 이해했으니, 이제는 이미지 및 컨테이너를 관리하는 방법을 배울 것입니다. 이미지와 관련된 명령어로는 아래와 같은 것들이 있습니다.

  • -t, docker tag: 이미지에 태그를 추가합니다.
  • docker images: 도커의 이미지들을 열거합니다.
  • docker image inspect: 도커의 이미지를 분석합니다.
  • docker rmi, docker prune: 도커 이미지를 삭제합니다.

컨테이너와 관련된 명령어로는 아래와 같은 것들이 있습니다.

  • --name: 이름을 붙일 수 있습니다.
  • --help: 추가적인 설정 항목들을 살펴봐야 합니다.
  • docker ps: 도커의 컨테이너들을 열거합니다.
  • docker rm: 도커 컨테이너를 지웁니다.

Managing Containers

Remove Containers

docker rm <container_name> 을 이용해서 컨테이너를 지울 수 있습니다. 다만, 현재 실행 중인 컨테이는 (당연히) 지울 수 없습니다. 또한, docker rm <name1> <name2> ... 와 같이 복수개를 띄어쓰기(space)로 구분된 채로 열거하여, 한 번에 삭제할 수도 있습니다. 추가적으로 docker container prune 을 이용해서 모든 멈춘(stopped) 컨테이너들을 한 번에 삭제할 수도 있습니다.

Remove Images

docker images 를 이용해서 이미지들을 열거할 수 있습니다. 이때, 이미지들의 사이즈 역시도 확인 가능합니다. 기본적으로는 base 이미지의 크기에 추가적인 종속성과 코드로 인해 커지게 됩니다. 뿐만 아니라 python 혹은 node 이미지의 경우에도 Linux 운영체제 이미지 위에 탑재가 됩니다. 대략적으로 node 이미지는 900MB 정도를 차지하는데, 이에는 node JS 뿐만 아니라 이를 실행하기 위한 툴과 운영체제 이미지가 추가된 크기입니다.

docker rmi 는 이미지를 제거하기 위한 명령어입니다. docker rmi <image_id> 와 같은 방식으로 이미지를 제거할 수 있습니다. 컨테이너를 삭제하는 것과 마찬가지로, 이미지를 삭제하기 위해서는 해당 이미지가 어떠한 컨테이너에서도 사용되어서는 안됩니다. 심지어 컨테이너가 멈췄더라도 해당 이미지를 삭제할 수는 없습니다. 다시 실행하기 위해서는 이미지가 필요하기 때문이죠.

현재 컨테이너에서 사용되지 않는 이미지를 모두 삭제하기 위해서는 docker image prune 을 사용할 수 있습니다.

Automatically Remove a Container

이미지를 실행하는 docker run 명령어에서 --help를 이용해 다양한 설정을 볼 수 있는데, 그 중에서 유용한 것은 --rm 플래그입니다. 이는 컨테이너가 종료될 경우 자동으로 해당 컨테이너를 지워줍니다. 이것이 유용한 이유는 일반적으로 도커가 웹 서버와 같은 형태에 사용되는데, 컨테이너를 중단시키는 경우는 서버의 코드가 변경된 경우말고는 거의 없기 때문입니다. 따라서 작동이 중지된 컨테이너를 자동적으로 삭제하게 하는 것은 유용할 때가 많습니다.

Inspect Images

이미지는 일반적으로 파일 크기가 매우 큽니다. 실행에 필요한 모든 파일들과 구성 요소들이 포함되어 있기 때문입니다. 그러나 컨테이너는 특정 이미지 위에 새로 추가된 레이어만 존재하기 때문에, 무겁지 않습니다. 따라서 여러 컨테이너는 이미지 내부의 코드를 공유합니다. 그렇기 때문에 이미지 내부의 이 코드도 잠겨 있습니다. 예를 들어, 새로운 파일을 만든다고 할지라도 읽기 전용인 이미지 레이어가 아니라, 새롭게 추가된 얇은 컨테이너 레이어 내부에 파일이 만들어집니다. 이것이 Docker의 이미지와 컨테이너가 작동하는 방식입니다.

이미지를 자세하게 살펴보기 위해서는 docker image inspect <image_id>를 입력하면 됩니다.

f567ba0fb6ad [
    {
        "Id": "sha256:f567ba0fb6ade67d2039c70e209720fca69a3896706c0a63c8c06350a578e77f",
        "RepoTags": [],
        "RepoDigests": [],
        "Parent": "sha256:a745ececef84785df7634cf94273cfc73b25f35d91cc58805dd8ef711c7e43f6",
        "Comment": "",
        "Created": "2022-05-27T11:10:04.207375069Z",
        "Container": "f9d5339587b55e184f583a0c1a36186182f4409e43891f2329c2c4771cb7b686",
        "ContainerConfig": {
            "Hostname": "f9d5339587b5",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "LANG=C.UTF-8",
                "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D",
                "PYTHON_VERSION=3.7.8",
                "PYTHON_PIP_VERSION=20.2.2",
                "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/5578af97f8b2b466f4cdbebe18a3ba2d48ad1434/get-pip.py",
                "PYTHON_GET_PIP_SHA256=d4d62a0850fe0c2e6325b2cc20d818c580563de5a2038f917e3cb0e25280b4d1"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"python\" \"main.py\"]"
            ],
            "Image": "sha256:a745ececef84785df7634cf94273cfc73b25f35d91cc58805dd8ef711c7e43f6",
            "Volumes": null,
            "WorkingDir": "/app",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "DockerVersion": "20.10.12",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "LANG=C.UTF-8",
                "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D",
                "PYTHON_VERSION=3.7.8",
                "PYTHON_PIP_VERSION=20.2.2",
                "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/5578af97f8b2b466f4cdbebe18a3ba2d48ad1434/get-pip.py",
                "PYTHON_GET_PIP_SHA256=d4d62a0850fe0c2e6325b2cc20d818c580563de5a2038f917e3cb0e25280b4d1"
            ],
            "Cmd": [
                "python",
                "main.py"
            ],
            "Image": "sha256:a745ececef84785df7634cf94273cfc73b25f35d91cc58805dd8ef711c7e43f6",
            "Volumes": null,
            "WorkingDir": "/app",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 875520001,
        "VirtualSize": 875520001,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/53a04aa51ff2bcf3a4c238a12b8d35d6d590344447b544c258d95184d37f810a/diff:/var/lib/docker/overlay2/b20d10097e50349396c45ad197e779d241206661cc5e5080817f55e33a5177e3/diff:/var/lib/docker/overlay2/defb217556e0f4cd8cc7dc1bc5c2465e881a322ced16f163f21c2faed98b5dba/diff:/var/lib/docker/overlay2/ff4e0ee28b6bb169115d492fc4e01561b2e187ba074017d2a6f96d414d6d072b/diff:/var/lib/docker/overlay2/6cb126ce2ad4aab1d409010b9f235d7a281cb42e8556ee94fd3d9897c24712c0/diff:/var/lib/docker/overlay2/857fc3896d20d2658c2b16a6b5c464b7f0e47ec0545e0368908770f021e1024c/diff:/var/lib/docker/overlay2/55edabc934be6a93022a97d1fb761257a7db4b20b5668c250dceabfaea0373bb/diff:/var/lib/docker/overlay2/9229bddfa8612b2e065da7851f462331cea246c87b48fa0efb6470d86897722d/diff:/var/lib/docker/overlay2/9f496e43b7b47fecf4bff610bce40243bd633c0886e302caecd26aa00fec9a7f/diff",
                "MergedDir": "/var/lib/docker/overlay2/3e8e6fc67a6f3090a95b7ed7fae6106291642c00a32b31894a77626a04165ef7/merged",
                "UpperDir": "/var/lib/docker/overlay2/3e8e6fc67a6f3090a95b7ed7fae6106291642c00a32b31894a77626a04165ef7/diff",
                "WorkDir": "/var/lib/docker/overlay2/3e8e6fc67a6f3090a95b7ed7fae6106291642c00a32b31894a77626a04165ef7/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:0ced13fcf9441aea6c4ee1defc1549772aa2df72017588a1e05bc11dd30b97b6",
                "sha256:b2765ac0333ae89829bb991a50d961bbb20069bac6eefce6fab8ef4d253ba24c",
                "sha256:7a9460d5321859e34344b2817f9e87b4c18bc9eb42dca91e0822d3511ea42a79",
                "sha256:e5df62d9b33ae0f2e75d7a92270f20fcac04986ac22dd0674c0420c171cc0d56",
                "sha256:e480b226ea6b55d0522602a1f40cdcf9b5c89fa64f5e7dac665ca699e22273d6",
                "sha256:f17789b093c5f7a0f68b385ee77e6b98ea27ee4c89adf780ecdfe4fa9329e413",
                "sha256:e83007e57a22233f7cf853439b8df0b013c4a9593326f198f1343c5db6e1900a",
                "sha256:fe1ecc1383cee375a4224131dc3af8ac19edca50ced37102b894aebcebb35bd5",
                "sha256:29f7ddb11d956395ab9ab3667668feab9cc7c3321c95f540c46e2d5f837a1806",
                "sha256:ee34ef4b066ca8f75288fb336e904238b97d1d6df708e9f8775a49af83212ed3"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

ENTRYPOINT, WorkingDir, ExposedPorts, Cmd 등과 같은 정보들이 포함되어 있습니다. 그리고 Os 항목에 linux를 기반으로 한다는 것을 볼 수 있습니다. 더 아래로 스크롤하면 Layers 항목에 이 이미지의 다른 레이어들을 볼 수 있습니다. 일반적으로 레이어의 수는 명령어의 수와 같거나 혹은 그보다 많습니다 (CMD 명령어 제외). 그 이유는 Dockerfile에 정의된 레이어에만 국한되지 않기 때문입니다. 이를 통해서 이미지가 어떠한 식으로 구성되어 있는 지를 대략적으로 살펴볼 수 있습니다.

Copying Files

컨테이너에서 로컬로, 혹은 로컬에서 컨테이너로 파일을 복사하기 위해서는 docker cp 명령어를 사용할 수 있습니다. 로컬에서 현재 작업 중인 폴더에 dummy라는 하위 폴더가 존재하고, 여기에 여러 파일들이 존재한다고 하겠습니다. 기본적인 문법은 docker cp <source> <target> 입니다. 따라서 로컬에서 도커 컨테이너로 파일을 복사하는 방법은 아래와 같습니다.

docker cp dummy/. boring_vaughan:/test

위 명령의 경우 boring_vaughan 이라는 아이디를 가진 컨테이너의 /test 경로에 파일들을 복사하게 됩니다. 만약 해당 폴더가 컨테이너 내부에 존재하지 않을 경우에는 이를 생성하게 됩니다.

이를 반대로 기록하는 경우에는 컨테이너에서 로컬로 파일이 복사됩니다.

docker cp boring_vaughan:/test dummy

물론 일반적으로는 이미지를 다시 빌드하여 컨테이너를 재시작하여 파일을 변경하게 됩니다. 파일을 복사하는 것은 좋은 해결책은 아닙니다. 추후에 이미지를 다시 빌드하지 않고도 코드를 변경할 수 있는 방법을 배울 예정입니다. 그럼에도 불구하고 컨테이너에 파일을 필수불가결하게 변경해야 하는 경우도 존재하는데, 예를 들어 웹 서버의 설정 파일(configuration file)의 경우입니다. 또한, 컨테이너 내부에서 로그 파일을 생성하는 경우에도 docker cp 명령어를 이용해서 복사할 수 있습니다.

Naming Images & Containers

Naming Containers

docker run 명령어에서 --name 플래그는 컨테이너에 이름을 붙일 수 있도록 합니다.

docker run --name <container_name> <image_name>

Tagging Images

마찬가지로 이미지에 대해서도 일종의 이름을 붙일 수 있는데, 이를 태그라고 합니다. 이미지 태그는 두 부분으로 구성되어 있습니다. 이름(name)에 해당되는 부분은 리포지토리(repository)라고 부릅니다. 그리고 콜론 : 뒤에 붙는 것은 태그(tag)라고 합니다. 이렇게 두 부분으로 나눈 것은 이미지의 일반적인 이름과 특정화된 버전을 구분해서 지정할 수 있습니다. 이를 통해서 더 작아진 매우 슬림한 구성을 사용할 수도 (더 가벼운 형태의 운영체제를 사용하는 것 등의 방법을 통해), 다른 버전을 사용할 수도 있게 됩니다.

docker build 명령어의 -t 혹은 --tag 플래그는 이를 담당하는데, name:tag 형식으로 이름을 붙여줄 수 있게 됩니다. 태그 부분에는 굳이 숫자일 필요가 없으며, 어떠한 문자라도 가능합니다.

docker build . -t sampleapp:1.0

Example

간단한 Python 파일을 실행하는 코드를 Docker를 이용해 만들어보겠습니다.

일단, 실행될 어플리케이션을 만듭니다. 아래의 코드를 입력하고 파일명은 main.py로 했습니다.

이후 Dockerfile을 만듭니다.

구조는 매우 단순합니다. Base image로는 python:3.7.8 을 사용했습니다. 이때, : 뒤에 붙은 3.7.8 은 태그(tag)로, 다양한 이미지들을 관리할 수 있는 기능이라고 볼 수 있습니다. 일반적으로는 저렇게 버전 정보를 붙입니다. 이후 COPY 명령어를 이용해서 코드를 복사하고, 이후 WORKDIR 을 이용해서 작업 폴더(working directory)를 변경해줍니다. 이후 CMD 명령어를 이용해 Python 파일을 실행합니다.

이후 docker run . 명령어를 입력하면 위에서 작성한 Dockerfile을 바탕으로 이미지를 생성하게 됩니다. 만약 중간에 필요한 레이어가 있다면 자동으로 다운로드를 수행합니다.

위에서 Successfully built 뒤에 빌드된 이미지의 id를 볼 수 있습니다. 이를 실행해줍니다. 그러면 아래와 같은 에러 메시지를 만나게 됩니다.

이유는 앞서 언급했듯이 terminal이 input을 받아들일 수 없기 때문입니다. 따라서 터미널을 에뮬레이트하여 도커 가상환경에 접근할 수 있도록 해야합니다. 이를 위해서는 -it 플래그를 달아주면 됩니다.

이 경우에는 정상적으로 터미널에서 사용자의 input을 받고, 결과값을 정상적으로 출력해주게 됩니다.

이후에 docker ps -a를 이용해서 실행된 이미지 및 컨테이너를 살펴볼 수 있습니다.

동일한 이미지 f567ba0fb6ad 를 이용해서 각기 다른 컨테이너 62e986764754689a6d6b663d 가 생성된 것을 확인할 수 있습니다. 그리고 첫번째 컨테이너는 Exited (1) 즉, 문제가 있이 종료되었다는 것을 확인할 수 있으며, 두번째 컨테이너는 Exited (0)으로 문제없이 종료된 것을 확인할 수 있습니다. 그리고 랜덤하게 부여된 이름 inspiring_moserpensive_sammet 역시 볼 수 있습니다.

이번에는 이 컨테이너를 다시 실행시켜보도록 하겠습니다. 이는 docker start pensive_sammet 명령어를 통해서 가능합니다. 그러나 터미널에서는 아무 일도 일어나지 않는데, 그 이유는 기본적으로 start 명령어는 detached 모드에서 실행되기 때문입니다. 실행되는 내용을 터미널에 연결시켜주기 위해서는 docker attach pensive_sammet 명령어를 이용해서 연결시켜야 합니다. 그런데 여전히 터미널에는 아무런 화면도 뜨지 않습니다. 그 이유는 이미 처음에 사용자의 input을 받을 때 메시지가 이미 출력되었기 때문입니다. 숫자 1개를 입력하면 바로 다음 숫자를 묻는 메시지가 출력되고, 이를 입력하면 랜덤한 숫자를 내뱉으며 프로그램이 종료됩니다.

또한, 참고적으로 start 명령어로 컨테이너가 재실행될 경우에는 기존에 실행했던 세팅을 기억하고 동일하게 실행됩니다. 따라서 -it 플래그를 별도로 붙여주지 않더라도, user input을 입력할 수 있는 것입니다. 마지막으로 attached 모드에서 재실행시키기 위해서는 docker start -a pensive_sammet 과 같이 -a 플래그를 달아주면 됩니다.

Comments