Docker/Kubernetes 정리 2

윤석주·2023년 1월 24일
1

docker

목록 보기
3/10

개요

지난 포스팅에서 도커에 대한 대략적인 내용을 살펴봤습니다. 이번 포스팅에선 두 가지 개념에 중점을 맞춰 알아보겠습니다. (로컬 도커환경은 설치되어 있다고 가정하겠습니다)

  • Images & Containers
    • Using Pre-Built & Custom Images
    • Creating & Managing Containers

컨테이너는 지난 시간에 조금 살펴봤습니다. 어플리케이션과, 실행을 위한 환경을 패키징해놓은 개념이죠.

이미지는 컨테이너를 만들기 위한 Templates/Blueprints에 가깝습니다. 객체지향을 공부해보셨다면, 클래스와 객체의 관계와 비슷하게 이해해도 좋습니다.

  • Images
    • Templates/Blueprints for containers
    • Contains code + required tools/runtimes
  • Containers
    • The running "unit of software"
    • Multiple containers can be created based on one image

따라서 어플리케이션 코드와 환경을 포함한 이미지를 만들어놓고, 이 이미지를 이용해 똑같은 여러개의 독립된 컨테이너를 생성할 수 있습니다.

Image (App code, Environment) -> Container/Container/Container

이미지가 코드와 환경에 관한 패키지라면, 컨테이너는 이미지로 만든 실제로 동작하는 인스턴스입니다.

Using & Running Pre-built Images

도커 컨테이너를 사용하기 위해 이미지가 필요하다는 사실은 이해했을겁니다. 우리는 컨테이너를 생성하기 위해 이미지가 필요하죠. 이미지는 크게 두 가지 방식으로 얻을 수 있습니다.

  • Use an existing, pre-built Image

기본적으로 첫 번째 방법이 많이 사용되고 있으며, Docker Hub라는 소스를 통해 도커 이미지를 세계적으로 공유하고 사용할 수 있습니다.


도커허브에서 노드 이미지를 검색하면 공식적인 이미지를 볼 수 있습니다. 위 이미지는 공식 노드팀에 의해 제공되고 있습니다.

도커를 이용해 위 이미지를 활용해보겠습니다.


터미널에 docker run node 명령어를 입력하면, 도커는 "node"에 해당하는 이미지를 찾아 컨테이너를 생성하고 실행합니다. 위 예시의 경우 로컬에 해당 이미지가 없기 때문에 도커는 도커허브의 node이미지를 찾아 다운받고, 해당 이미지를 이용해 컨테이너를 생성하고 실행합니다.

어플리케이션 코드가 없기 때문에 실행된 컨테이너는 바로 종료됩니다. 따라서 우리는 컨테이너가 실행된건지 확인할 수 없죠.
하지만 docker ps -a 명령어를 사용하여 해당 컨테이너를 확인할 수 있습니다.
(도커가 만든 모든 process, container를 보여주는 명령어입니다.)

Images는 node이고, Status는 Exited로 종료된 것을 볼 수 있습니다.

컨테이너를 생성하고 실행했지만, 막상 눈에 띄는 확인을 할 수 없었습니다. 이제 명령어를 조금 바꿔서, 해당 컨테이너 안의 노드환경을 실제로 이용해보겠습니다.

docker run -it node

it는 컨테이너의 node환경과 interactive session을 열어줍니다.

-it 플래그를 이용하여 컨테이너를 바로 종료하지 않고 interactive session이 열린것을 확인할 수 있습니다. 이제 컨테이너 내부의 node 환경을 사용할 수 있습니다.

중요한 것은 우리가 사용한 컨테이너 내부에서 노드가 동작하고 있으며, -it 플래크를 이용해 우리에게 노출되었다는 점입니다. 즉, 여기서 사용하는 노드는 우리의 로컬 호스트에 있는 노드가 아닙니다. 컨테이너 내부에 있는 노드이죠.

위 이미지의 노드 버전을 잘 기억하기 바랍니다. 이후 Ctrl+c를 2번 눌러 종료하고, 로컬 호스트의 노드를 확인해봅시다.

node -v


로컬환경마다 다르겠지만, 저의 경우 16버전의 노드가 깔려있는 것을 볼 수 있습니다. 컨테이너 내부의 노드 버전과 완전히 다른걸 알 수 있죠. 컨테이너는 로컬호스트와 완전히 분리된 독립적인 환경이라는 것을 알 수 있습니다.

우리는 노드를 호스트에 설치할 필요도 없이, 필요한 노드 버전의 이미지를 사용할 수 있는 것을 살펴봤습니다.

다시 docker ps -a 명령어를 통해 살펴보면 같은 이미지로 생성된 2개의 독립적인 컨테이너를 확인할 수 있습니다.

A NodeJS App

위에서 우리는 Pre-Built 이미지를 사용했습니다. 이번엔 base 이미지 위에 우리의 노드 코드를 동작하도록 하는 이미지를 직접 생성하여 사용해보겠습니다. 즉, Create your own, custom Image를 해보는 것이죠. 이를 위해 Dockerfile이라는 파일의 작성이 필요합니다.

우선 아주 간단한 노드 어플리케이션 코드를 준비했습니다. 80번 포트에서 동작하는 코드로, 매우 간단한 코드입니다. (어플리케이션에 대한 자세한 이해는 없어도 됩니다.)

우리는 이 코드를 노드 위에서 실행되도록 하는 이미지를 만들고, 그 이미지를 이용해 컨테이너를 생성하여 어플리케이션을 실행하려고 합니다.

package.json을 보면 express와 body-parser를 설치할 필요가 있습니다. 원래라면 로컬에 노드를 설치하고, npm install 명령어를 실행하고, node server.js를 이용해 어플리케이션 코드를 실행하겠죠.

우리는 위 과정을 도커 컨테이너에서 실행하려고 합니다. 따라서 Dockerfile을 작성하고, 이미지를 만들어 보겠습니다.

먼저 프로젝트의 루트에 Dockerfile 라는 이름으로 다음과 같이 작성해줍니다.

  • FROM node - 노드 이미지를 베이스 이미지로 사용하겠다는 얘기입니다.
  • WORKDIR /app - 이미지 내부의 filesystem에서 /app폴더를 작업폴더로 지정합니다.
  • COPY . /app - 두 개의 폴더를 확인할 수 있습니다. 처음 폴더는 Dockerfile이 존재하는 로컬 프로젝트의 폴더를 의미합니다. 두 번째 폴더는 이미지 내부의 파일시스템을 의미합니다. 따라서 위 명령어는 로컬폴더의 루트에 있는 모든 파일을 이미지의 /app 폴더 내부에 복사한다는 의미입니다.
  • RUN npm install - 이미지의 /app 폴더 내부에 의존성을 설치합니다.
  • EXPOSE 80 - 로컬에 노출할 포트를 명시합니다.
  • CMD [ "node", "server.js" ] - 컨테이너에서 실행할 명령어를 넣어줍니다. 이미지 생성시가 아닌, 컨테이너 실행시 실행되어야 하기 때문에 RUN이 아닌 CMD를 사용합니다.

이제 위 Dockerfile을 이용해서 이미지를 생성하고, 도커 파일을 실행해보겠습니다. 먼저 이미지 생성입니다. 터미널을 열고 다음과 같은 명령어를 실행합니다.

docker build .

docker build는 Dockerfile이 있는 위치를 받아, 해당 파일을 기반으로 이미지를 만드는 명령어입니다.

명령어를 실행하면 위와 같이 Dockerfile을 기반으로 이미지를 생성하는 것을 볼 수 있습니다.

이제 위에서 생성된 이미지 id를 잘 기억하여, docker run 명령어를 실행해줍니다. 위 예시에선 다음과 같습니다.
docker run -p 3000:80 eed32d5331cac852822783e7e487a9f7180fa59f132fd2c82970a237e7fb3b56

명령어에 -p 태그가 들어간 것을 볼 수 있습니다. 이는 로컬호스트의 포트와, 실행하는 컨테이너의 포트를 이어주는 역할을 한다고 볼 수 있습니다.

Dockerfile을 보면 어플리케이션은 80번 포트에서 동작한다고 하지만, 사실 이는 컨테이너의 포트를 의미합니다(컨테이너마다 독자적 포트를 갖는다고 생각하면 이해가 쉽습니다). 이를 로컬 호스트의 3000번 포트와 연결시켜주는 겁니다.

이제 로컬호스트의 3000번 포트에 접근하면, 컨테이너에서 동작하는 어플리케이션을 확인할 수 있습니다.

Image Layers


위에서 다뤘던 Dockerfile입니다. 이 파일을 빌드하여 이미지를 생성할 수 있었죠. 이미지와 관련하여 또 다른 중요한 내용은 이미지가 layer base라는 것 입니다.

단어가 좀 어렵게 다가오죠? 쉽게 생각하면 단계별로 캐싱되어있다고 봐도 무방합니다. 만약 이미지를 빌드한 다음, Dockerfile을 조금 수정하여 다시 빌드한다고 해봅시다. 그렇게 되면 Dockerfile의 수정된 부분만 다시 실행됩니다.

빌드하는 경우를 살펴봅시다.


첫 번째 사진은 처음 빌드시, 두 번째 사진은 이후 다시 빌드시의 화면입니다. 빌드시간이 서로 다른점과, cached가 보이시나요? 두 번째 빌드의 경우 첫 번째 빌드시의 내용을 캐싱했기 때문입니다. Dockerfile과 코드의 내용이 동일하기 때문이죠.

도커는 도커파일의 모든 instruction 결과를 캐싱해놓습니다. 그리고 다시 빌드하는 경우 캐시된 결과를 사용합니다.

이를 layer based architecture 라고 부릅니다. Dockerfile의 모든 instruction은 layer를 나타냅니다. 그리고 이미지는 여러 layer 들로부터 생성되는 것이죠.

이미지는 read only 입니다. 이 말은 이미지가 build 되면 이미지 내부의 코드를 변경할 수 없다는 의미입니다. 따라서 다른 이미지가 필요하다면 이미지를 다시 빌드해야 하죠.

이미지는 layer를 이용해 생성하고, layer는 캐싱되어 있습니다. 그리고 이미지로 컨테이너를 생성하고 실행하면, 컨테이너는 새로운 layer를 생성합니다(container layer).

따라서 만약 어플리케이션 코드를 수정한 다음 이미지를 다시 실행한다면, 캐싱이 되지 않는 부분만 다시 실행되는 것을 볼 수 있습니다.

위 사진은 어플리케이션의 문구를 조금 수정하고 다시 빌드한 내용입니다. 빌드 내용을 보면 WORKDIR /app까지는 캐싱을 사용하고, COPY . /app 이후부터는 사용하지 않는 것을 볼 수 있습니다.

어플리케이션의 내용이 바뀌었기에 해당 instruction의 캐시를 사용할 수 없기 때문이죠. 그리고 도커는 캐시를 사용할 수 없는 layer 이후의 모든 layer를 캐시를 사용하지 않고 다시 실행합니다. 따라서 이후의 instruction들이 모두 실행된 것을 볼 수 있습니다.

우리의 도커파일을 보면, npm install의 경우 어플리케이션 코드가 조금 바뀌더라도 동일한 결과이지만, 도커는 이후 layer에 대한 자세한 분석을 하지 않기에 캐시를 사용하지 않고 다시 실행하는 것이죠.

물론 개발자는 package.json이 바뀌지 않으면 동일한 결과를 사용해도 괜찮은 것을 알고 있습니다. 하지만 도커는 layer를 자세히 분석하지 않아서 캐싱을 사용할 수 없었죠. 그렇기 때문에 다음과 같은 간단한 최적화를 통해 캐시를 사용하도록 유도할 수 있습니다.

package.json 파일을 먼저 카피하고, npm install을 실행하도록 변경했습니다. 이제 package.json이 변경되지 않는 이상 이미지 생성시 의존성 설치가 다시 실행되는 일은 없겠죠.

이러한 layer 캐싱은 이미지 빌드시의 시간을 줄여주는 역할을 합니다. 따라서 layer 구조를 이해하고 Dockerfile 최적화를 통해 필요한 이점을 잘 사용하는 것이 중요합니다.

출처

profile
웹 개발을 공부하고 있는 윤석주입니다.

1개의 댓글

comment-user-thumbnail
2023년 1월 28일

별로네요
제마음의 별로⭐️

답글 달기