도커 컨테이너와 이미지-01

jjunhwan.kim·2023년 6월 9일
1

도커

목록 보기
1/6
post-thumbnail

개요

안녕하세요. "도커 교과서" 라는 책을 읽고 공부한 내용을 정리해 보겠습니다. 이 포스트는 1장 부터 4장 까지의 내용을 정리하였습니다.

  • 도커란?
    "컨테이너" 라는 단위로 애플리케이션을 실행하는 기능을 제공하는 플랫폼입니다. 명령 한 줄로 애플리케이션의 빌드와 실행을 할 수 있는 편리한 도구입니다.

  • 도커를 사용하면 어떤 장점이 있을까요?
    컨테이너에서 애플리케이션을 실행하면 애플리케이션을 실행하는 환경에 관계없이 동일한 환경에서 애플리케이션을 실행할 수 있습니다. 도커를 사용하지 않았을 때에는 윈도우 또는 리눅스같은 운영체제에 Node.js, JRE, JDK 등의 런타임을 설치하고, 애플리케이션 빌드 환경을 구축합니다. 그리고 애플리케이션을 빌드하고 실행합니다. 도커를 사용하면 이러한 과정을 어느 컴퓨터에서 실행하던지 동일하게 자동으로 수행할 수 있습니다.

도커 설치

설치가 완료되면 터미널에서 docker version 명령으로 도커 엔진이 잘 동작중인지 확인합니다.

Hello World 실행하기

도커에서 제공하는 hello-world 이미지를 컨테이너로 실행해볼까요? docker run 명령어를 통해 이미지를 컨테이너로 실행할 수 있습니다.
run 다음에 오는 문자열은 도커 이미지 이름입니다.

docker run hello-world

아래와 같은 결과가 나오면 정상입니다.

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (arm64v8)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
  • 위에서 실행한 docker run 명령어는 도커 이미지를 컨테이너로 실행하라는 명령어입니다.
  • 도커 이미지는 로컬에 존재하지 않으면 기본적으로 도커 허브에서 자동으로 다운로드 받습니다. 도커 허브는 도커 이미지 저장소입니다.
  • 하나의 이미지로 여러 개의 컨테이너를 생성할 수 있습니다. 도커 이미지에는 애플리케이션을 실행하는데 필요한 모든 내용들이 정의되어 있습니다.

컨테이너란?

컨테이너는 그럼 무엇일까요? 컨테이너는 애플리케이션과 애플리케이션을 실행할 컴퓨터가 들어있는 하나의 상자라고 볼 수 있습니다. 한 컴퓨터에서 이러한 상자들이 여러 개 실행될 수 있습니다. 이 상자들은 독립적인 환경을 갖고 있습니다. 따라서 가상머신을 생각해 볼 수 있지만 컨테이너는 가상머신과는 다릅니다.

출처: https://www.docker.com/resources/what-container

가상머신은 각각의 가상머신이 자신만의 운영체제를 가지고 있지만, 컨테이너는 호스트 컴퓨터의 운영체제를 공유합니다.

  • 가상 머신은 가상 머신마다 자신만의 운영체제를 가지므로, 많은 리소스를 사용합니다. 각각의 가상머신은 독립된 환경을 제공하지만 운영체제의 라이선스 비용과 각각의 운영체제 업데이트 설치 등의 부담이 있습니다.
  • 컨테이너는 호스트 컴퓨터의 운영체제를 공유하므로 필요한 리소스를 줄일 수 있습니다. 따라서 같은 호스트 컴퓨터에서 가상머신에 비해 더 많은 수의 애플리케이션을 실행할 수 있습니다. 또한 컨테이너는 가상머신과 같이 독립된 환경을 제공합니다.

컨테이너를 원격 컴퓨터처럼 사용하기

위에서 실행했던 hello-world 컨테이너는 메시지를 출력하고 종료되는 컨테이너 였습니다. 아래 명령어를 통해 종료된 컨테이너를 확인해 볼 수 있습니다.

docker ps -a

docker ps 명령어를 사용하면 컨테이너에 부여된 ID, 이름, 이미지 등을 확인할 수 있습니다.

컨테이너를 마치 원격 컴퓨터에 접속하듯 터미널을 통해 사용해 볼 수 있습니다. 우분투에 터미널로 접속하듯이 사용해보도록 하겠습니다.

docker run -it ubuntu

hostname
exit

아래의 도커 명령어를 사용하여 컨테이너에 대한 여러 정보를 얻을 수 있습니다.

컨테이너에서 실행 중인 프로세스 목록

docker top {CONTAINER_ID}

컨테이너에서 수집된 로그

docker logs {CONTAINER_ID}

컨테이너의 상세 정보

docker inspect {CONTAINER_ID}

컨테이너를 사용해 웹 사이트 호스팅하기

앞에서 실행한 hello-world 컨테이너는 실행 한 후 실행 중인 컨테이너 목록을 확인하는 docker ps 명령어로 조회해보면 나오지 않습니다. 컨테이너 내부의 애플리케이션이 종료되면 컨테이너의 상태도 종료가됩니다.

모든 컨테이너의 목록을 확인하는 docker ps -a 명령어로 조회해보면 hello-world 컨테이너가 출력되고 상태가 Exited로 되어있습니다. 컨테이너가 종료되도 컨테이너는 삭제되지 않습니다.

컨테이너를 통해 간단한 웹 사이트를 호스팅 해보겠습니다. nginx 웹서버 이미지를 이용해 보겠습니다. 도커 이미지는 도커 허브에 공개되어 있습니다. (https://hub.docker.com/_/nginx)

아래 명령어를 터미널에서 입력해봅시다.

docker run -d --name nginx -p 8080:80 nginx

브라우저에서 http://localhost:8080 으로 접속하면 "Welcome to nginx!" 화면이 뜰겁니다.

위 커맨드에서 -d 옵션, --name 옵션, -p 옵션을 사용했습니다.

  • -d, --detach: 컨테이너를 백그라운드에서 실행하며 컨테이너 ID를 출력합니다. -d 옵션 없이 실행하면 터미널에 로그가 출력되고 Control + C로 종료하면 컨테이너도 종료됩니다.
  • -p, --publish: 컨테이너의 포트를 호스트 컴퓨터에 공개합니다. 각각의 컨테이너는 하나의 컴퓨터처럼 IP 주소(물론 가상 IP 주소입니다.)를 가지고 컨테이너에서 실행되는 애플리케이션은 포트를 가질 수 있습니다. 컨테이너에서 실행되는 애플리케이션의 포트를 호스트 컴퓨터 포트에 공개해야 컨테이너 외부에서 접근할 수 있습니다. 여기서는 컨테이너 내부의 80 포트를 호스트 컴퓨터의 8080 포트에 공개합니다.
  • name: 컨테이너의 이름을 지정합니다.

컨테이너의 IP 주소와 포트

앞의 예제에서 컨테이너 실행시 컨테이너의 포트를 호스트 컴퓨터의 포트에 공개했습니다. 왜 이런 작업이 필요할까요?

  • 기본적으로 컨테이너는 외부 환경에 노출되지 않습니다.
  • 컨테이너는 고유의 IP 주소를 갖습니다. (보통 172로 시작하는 IP 주소입니다.) 하지만 이 IP주소는 도커 내부 가상 네트워크 주소이므로 컨테이너 외부에서 접근할 수 없습니다.
  • 따라서 컨테이너의 포트를 호스트 컴퓨터의 포트에 공개하여 외부에서 호스트 컴퓨터의 IP주소와 포트에 요청을 보내면 도커가 해당 포트로 들어온 요청을 컨테이너로 전달합니다.

도커가 컨테이너를 실행하는 원리

  • 도커의 컴포넌트 중 도커 엔진은 도커 이미지와 컨테이너를 관리합니다. 도커 엔진은 도커 이미지를 다운받고 캐시하고, 컨테이너를 생성하고 네트워크를 만드는 등 핵심 기능을 담당합니다.
  • 도커 API를 통해서 도커 엔진과 상호작용 할 수 있습니다. API는 표준 HTTP 기반 REST API이고 API를 통해서만 도커 엔진에 접근할 수 있습니다.
  • 도커 CLI는 도커 API 클라이언트 입니다. 도커 API에 요청을 전달하는 역할을 합니다. 터미널에서 docker 명령을 사용하면 도커 CLI가 도커 API를 호출합니다.
  • 도커 API는 명세가 공개되어 있습니다. 따라서 도커 CLI외의 다른 클라이언트를 사용할 수 있습니다. GUI 기반 클라이언트 도구들이 있습니다.
  • 도커 엔진은 containerd라는 컴포넌트를 통해 컨테이너를 관리합니다. containerd는 호스트 운영체제가 제공하는 기능을 통해 컨테이너를 만듭니다. containerd는 CNCF에서 관리하는 오픈소스 프로젝트입니다.

도커 허브에 공유된 이미지 사용하기

앞에서 사용했던 docker run 명령어는 필요한 이미지 중 로컬 컴퓨터에 없는 이미지가 있으면 이미지를 저장소에서 다운받습니다.
또는 docker pull 명령어를 통해 이미지를 명시적으로 받을 수도 있습니다.
도커는 저장소에서 이미지를 찾을 때 가장 먼저 도커 허브에서 이미지를 찾습니다.

docker pull ubuntu:20.04

도커 이미지 목록을 조회하여 이미지가 추가되었는지 확인합니다.

docker images

docker pull 명령어를 통해 이미지를 내려받는 과정을 보면 아래와 같이 하나의 이미지에 대해 여러 개의 파일을 다운받는 것을 볼 수 있습니다. 각각의 파일을 이미지 레이어라고 부릅니다. 도커 이미지는 물리적으로 여러 개의 작은 파일로 구성되어 있습니다.

docker pull jenkins/jenkins:lts
lts: Pulling from jenkins/jenkins
b04fae59f135: Pull complete
3120cb9083ba: Pull complete
3c0a0e518a2a: Pull complete
30e5f4a71251: Pull complete
59f917facca4: Pull complete
f6b1a644069b: Pull complete
a841abad8e1a: Pull complete
7cdc2776c2ce: Pull complete
f810d9f04fb9: Pull complete
e560a1287994: Pull complete
724c63a33c93: Pull complete
adb7543bd1cd: Pull complete
bc5fb2c193f0: Pull complete
Digest: sha256:90f7f78a7e114516216b6c0a06e00c597d6490597caec9d7bd7a95ce5c7dada0
Status: Downloaded newer image for jenkins/jenkins:lts
docker.io/jenkins/jenkins:lts

Dockerfile 작성하기

  • Dockerfile은 애플리케이션을 패키징하기 위한 스크립트입니다.
  • Dockerfile은 인스트럭션으로 구성되어 있는데, 인스트럭션을 실행한 결과로 도커 이미지가 만들어집니다.
  • 아래는 간단한 스프링 애플리케이션을 패키징하기 위한 Dockerfile 스크립트입니다.
  • 전체 프로젝트는 https://github.com/nefertirii/docker-example/tree/227aedc0b7570fd16f19c8893f63a3489cc297f2 에서 볼 수 있습니다.
FROM gradle:7.6.1 AS build

WORKDIR /src # 작업 디렉터리 /src 생성 및 지정
COPY . . # Dockerfile이 위치한 경로의 모든 파일을 컨테이너의 작업 디렉터리(/src)로 복사
RUN gradle bootJar

# 멀티 스테이지 빌드
FROM amazoncorretto:17

WORKDIR /app
COPY --from=build /src/build/libs/*.jar app.jar
EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

인스트럭션을 하나씩 알아보겠습니다.

  • FROM: 모든 이미지는 다른 이미지로부터 출발합니다. 베이스 이미지를 지정합니다. 이 스크립트에서는 애플리케이션을 빌드할 때 gradle 이미지를, 애플리케이션을 실행할 때 amazoncorretto(JDK) 이미지를 사용합니다. 이처럼 여러 개의 FROM을 사용하는 것을 멀티 스테이지 빌드라고 합니다. (https://docs.docker.com/build/building/multi-stage/)

  • WORKDIR: 컨테이너 이미지 파일 시스템에 디렉터리를 만들고, 해당 디렉터리를 작업 디렉터리로 지정하는 인스트럭션입니다.

  • COPY: 로컬 파일 시스템의 파일 혹은 디렉터리를 컨테이너 이미지 파일 시스템에 복사하는 인스트럭션입니다. COPY {원본경로} {복사경로} 형식으로 사용합니다. 이 스크립트에서는 Dockerfile이 위치한 경로의 모든 파일을 컨테이너의 작업 디렉터리로 복사했습니다.

  • RUN: 컨테이너에서 명령어를 실행합니다. 이 스크립트에서는 쉘에서 gradle bootJar 명령어를 실행하여 스프링 애플리케이션 jar 파일을 생성하였습니다.

  • EXPOSE: EXPOSE 명령어는 컨테이너가 실행될 때 지정된 포트에서 수신 대기함을 Docker에 알립니다. EXPOSE 명령은 실제로 포트를 게시하지 않습니다. 이미지를 빌드하는 사람과 컨테이너를 실행하는 사람 사이에서 게시할 포트에 대한 일종의 문서 역할을 합니다. 컨테이너를 실행할 때 실제로 포트를 공개하려면 docker run에서 -p 옵션을 사용하여 포트를 공개하거나 -P 옵션을 사용하여 노출된 모든 포트를 공개해야합니다.

  • CMD: 도커가 이미지로부터 컨테이너를 실행했을 때 실행할 명령을 지정하는 인스트럭션입니다. docker run 으로 실행시 사용자가 직접 실행할 명령어를 입력시 Dockerfile의 CMD는 실행되지 않습니다. docker run {이미지 이름} echo hello 실행시 Dockerfile의 CMD 대신 echo hello 가 실행됩니다.

  • ENTRYPOINT: CMD와 동일하게 컨테이너가 실행됬을 때 실행할 명령을 지정합니다. CMD와 다른 점으로 docker run 으로 실행시 명령어를 입력하면 ENTRYPOINT의 파라미터로 인식합니다. docker run {이미지 이름} echo hello 실행시 echo hello는 ENTRYPOINT의 파라미터로 전달됩니다.

  • 더 많은 인스트럭션은 https://docs.docker.com/engine/reference/builder 를 참고합니다.

컨테이너 이미지 빌드하기

이미지를 빌드하려면 Dockerfile 스크립트와 이미지 이름, 패키징에 필요한 파일의 경로가 필요합니다.

예제 프로젝트(https://github.com/nefertirii/docker-example/tree/227aedc0b7570fd16f19c8893f63a3489cc297f2)를 다운받아 backend 디렉터리로 이동합니다.

Dockerfile과 소스 코드가 준비되어있습니다.

터미널에서 Dockerfile이 있는 경로에서 docker build 명령어를 사용하여 Dockerfile로 이미지를 빌드합니다.

docker build -t spring-backend .

-t 옵션은 이미지의 이름을 지정하고, 그 다음 인자는 Dockerfile 및 이미지에 포함시킬 파일의 경로입니다. "." 은 현재 작업 디렉터리를 의미합니다.

docker images 명령어로 이미지가 빌드되었는지 확인합니다.

docker images
REPOSITORY       TAG       IMAGE ID       CREATED             SIZE
spring-backend   latest    ac5a108f9cd9   10 minutes ago      540MB

도커 이미지와 이미지 레이어 이해하기

도커 이미지에는 패키징에 포함시킨 모든 파일이 들어있습니다. 또한 여러 메타데이터 정보도 들어있습니다. 이 정보 중에는 이미지가 어떻게 빌드되었는지에 대한 간단한 이력도 포함됩니다. 이 정보를 이용하면 이미지를 구성하는 각 레이어가 무엇이고 어떤 명령으로 빌드되었는지 알 수 있습니다.

위에서 생성한 spring-backend 도커 이미지의 히스토리를 확인해보겠습니다.

docker history spring-backend:latest
IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
ac5a108f9cd9   3 hours ago   ENTRYPOINT ["java" "-jar" "app.jar"]            0B        buildkit.dockerfile.v0
<missing>      3 hours ago   EXPOSE map[8080/tcp:{}]                         0B        buildkit.dockerfile.v0
<missing>      3 hours ago   COPY /src/build/libs/*.jar app.jar # buildkit   44.8MB    buildkit.dockerfile.v0
<missing>      4 hours ago   WORKDIR /app                                    0B        buildkit.dockerfile.v0
<missing>      3 days ago    /bin/sh -c #(nop)  ENV JAVA_HOME=/usr/lib/jv…   0B
<missing>      3 days ago    /bin/sh -c #(nop)  ENV LANG=C.UTF-8             0B
<missing>      3 days ago    |1 version=17.0.7.7-1 /bin/sh -c set -eux   …   300MB
<missing>      3 days ago    /bin/sh -c #(nop)  ARG version=17.0.7.7-1       0B
<missing>      3 days ago    /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>      3 days ago    /bin/sh -c #(nop) COPY dir:dfd59a801cb05cf6d…   195MB
  • CREATED BY 항목은 해당 레이어를 구성한 Dockerfile 스크립트의 인스트럭션입니다
  • Dockerfile 인스트럭션과 이미지 레이어는 1:1 관계를 갖습니다.
  • 도커 이미지는 이미지 레이어가 모인 논리적 대상입니다. 레이어는 도커 엔진의 캐시에 물리적으로 저장된 파일입니다. 즉 이미지 레이어들은 여러 이미지와 컨테이너에서 공유됩니다.

위의 그림에서 amazoncorretto 이미지는 운영체제와 JDK를 포함합니다. 우리가 만든 spring-backend 이미지는 amazoncorretto 이미지를 기반으로 하므로 해당 이미지의 모든 레이어를 포함합니다.

그렇다면 spring-backend 이미지의 용량은 얼마나 될까요?
docker images 명령어를 입력해보면 아래와 같이 기반 이미지 용량 495MB에 app.jar 파일 용량 44.8MB을 더한 540MB으로 표시됩니다.

docker images
REPOSITORY               TAG       IMAGE ID       CREATED         SIZE
another-spring-backend   latest    826fbb6df779   3 seconds ago   540MB
spring-backend           latest    ac5a108f9cd9   4 hours ago     540MB
<none>                   <none>    0c1c30848360   4 hours ago     540MB
<none>                   <none>    71c2ce6dd573   5 hours ago     540MB
amazoncorretto           17        aaa5b1888a74   3 days ago      495MB
hello-world              latest    b038788ddb22   5 weeks ago     9.14kB

another-spring-backend 이미지도 마찬가지로 기반 이미지 용량 495MB에 app.jar 파일 용량 44.8MB을 더한 540MB으로 표시됩니다. 이렇게 보면 기반이미지 495MB, spring-backend 540MB, another-spring-backend 540MB 용량을 차지할 것 처럼 보입니다.

하지만 이는 이미지의 논리적 용량이지 실제로 차지하는 디스크 용량을 나타내는 것이 아닙니다. spring-backend 540MB, another-spring-backend 이미지는 기반 이미지인 amazoncorretto 이미지의 레이어를 공유하므로 실제로 디스크 용량을 훨씬 덜 차지합니다.

docker system df 명령어를 입력하면 실제 디스크 용량을 확인할 수 있습니다.

docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          6         1         674.1MB   629.3MB (93%)
Containers      1         0         0B        0B
Local Volumes   0         0         0B        0B
Build Cache     34        0         811.2MB   811.2MB

출력 결과를 보면 이미지들의 실제 용량은 674.1MB를 차지하는 것으로 나옵니다. 즉 spring-backend, another-spring-backend는 기반 이미지의 레이어를 공유한다는 것입니다.

이미지 레이어를 여러 이미지가 공유한다면, 공유되는 레이어는 수정할 수 없어야 합니다. 만약 이미지의 레이어를 수정할 수 있다면 수정한 내용이 레이어를 공유하는 다른 이미지에도 영향을 미치게 됩니다. 따라서 도커는 이미지 레이어를 읽기 전용으로 만들어 이런 문제를 방지합니다. 이미지를 빌드하면서 레이어가 만들어지면 레이어는 다른 이미지에서 재사용 될 수 있지만 레이어를 수정할 수는 없습니다.

이미지 레이어 캐시를 이용한 Dockerfile 스크립트 최적화

Dockerfile 스크립트의 인스트럭션은 각각 하나의 이미지 레이어와 1:1로 연결됩니다. 이미지 빌드시 인스트럭션의 결과가 이전에 캐시된 레이어에 있다면 캐시된 레이어를 재사용합니다.

아래는 spring-backend 이미지의 Dockerfile 스크립트입니다.

FROM gradle:7.6.1 AS build

WORKDIR /src # 작업 디렉터리 /src 생성 및 지정
COPY . . # Dockerfile이 위치한 경로의 모든 파일을 컨테이너의 작업 디렉터리(/src)로 복사
RUN gradle bootJar

# 멀티 스테이지 빌드
FROM amazoncorretto:17

WORKDIR /app
COPY --from=build /src/build/libs/*.jar app.jar
EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

해당 이미지를 빌드하면 아래와 같이 출력됩니다.

docker build -t spring:1 .
[+] Building 66.3s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                            0.0s
 => => transferring dockerfile: 251B                                                                                                                                            0.0s
 => [internal] load .dockerignore                                                                                                                                               0.0s
 => => transferring context: 2B                                                                                                                                                 0.0s
 => [internal] load metadata for docker.io/library/amazoncorretto:17                                                                                                            0.0s
 => [internal] load metadata for docker.io/library/gradle:7.6.1                                                                                                                 1.7s
 => [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226                                                    42.2s
 => => resolve docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226                                                           0.0s
 => => sha256:f76840a01c8dcffa48f41af385088d66b51ca6214ebb03f5ffb6c72b1e376fe6 1.79kB / 1.79kB                                                                                  0.0s
 => => sha256:498a0978395375469510ee1ec67911c5fc47525d29edceb6407fe24c94ba23e0 191.40MB / 191.40MB                                                                             35.9s
 => => sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 1.21kB / 1.21kB                                                                                  0.0s
 => => sha256:6c7698a779f6d8c45a39a6721fb5cce267d66ff8ab5181c55aa6d02c8ddacd01 28.39MB / 28.39MB                                                                                6.7s
 => => sha256:b3eb20d332dad785127ed3f3e6dcc4b0d15bd0c80eed668d36d238b6c154998a 18.46MB / 18.46MB                                                                               11.3s
 => => sha256:f3a54ae46b7b05d61cce6ffe7724ecec8fb77191aae25628e1fa7cddfa24c9d4 10.26kB / 10.26kB                                                                                0.0s
 => => sha256:611408d965705e52bf288c19198044f6d0623af144aa26e3c18f917ea296215e 175B / 175B                                                                                      7.0s
 => => extracting sha256:6c7698a779f6d8c45a39a6721fb5cce267d66ff8ab5181c55aa6d02c8ddacd01                                                                                       0.9s
 => => sha256:53b4b9e222f66a540b194cd9c75a811c4f3762826a015ca45f37e771f97b0b78 4.37kB / 4.37kB                                                                                  7.2s
 => => sha256:0a93e4039b0d410743a10a3bf95b8ef5f1bbb45e6caa43364a38c9240292a0ee 51.10MB / 51.10MB                                                                               18.3s
 => => sha256:cce21c45d77e8bc478d3c19b5af3f9bc8a137cf56fed46c308a81162248120e8 122.08MB / 122.08MB                                                                             29.1s
 => => extracting sha256:b3eb20d332dad785127ed3f3e6dcc4b0d15bd0c80eed668d36d238b6c154998a                                                                                       0.6s
 => => extracting sha256:498a0978395375469510ee1ec67911c5fc47525d29edceb6407fe24c94ba23e0                                                                                       2.8s
 => => extracting sha256:611408d965705e52bf288c19198044f6d0623af144aa26e3c18f917ea296215e                                                                                       0.0s
 => => extracting sha256:53b4b9e222f66a540b194cd9c75a811c4f3762826a015ca45f37e771f97b0b78                                                                                       0.0s
 => => extracting sha256:0a93e4039b0d410743a10a3bf95b8ef5f1bbb45e6caa43364a38c9240292a0ee                                                                                       1.5s
 => => extracting sha256:cce21c45d77e8bc478d3c19b5af3f9bc8a137cf56fed46c308a81162248120e8                                                                                       1.5s
 => [stage-1 1/3] FROM docker.io/library/amazoncorretto:17                                                                                                                      0.0s
 => [internal] load build context                                                                                                                                               1.2s
 => => transferring context: 45.20MB                                                                                                                                            1.2s
 => [build 2/4] WORKDIR /src                                                                                                                                                    0.1s
 => [build 3/4] COPY . .                                                                                                                                                        0.1s
 => [build 4/4] RUN gradle bootJar                                                                                                                                             20.4s
 => [stage-1 2/3] COPY --from=build /src/build/libs/*.jar app.jar                                                                                                               0.1s
 => [stage-1 3/3] WORKDIR /app                                                                                                                                                  0.0s
 => exporting to image                                                                                                                                                          0.1s
 => => exporting layers                                                                                                                                                         0.1s
 => => writing image sha256:82b380afaf34fb346b73e902d1308f265be6951cd9ec8895266dabccd63afb4f                                                                                    0.0s
 => => naming to docker.io/library/spring:1                                                                                                                                     0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

동일한 이미지를 이름만 바꿔 다시 빌드해봅시다. 아래와 같이 CACHED로 표시된 부분은 캐시된 이미지 레이어를 재사용한 것입니다. 따라서 빌드가 빠르게 수행됩니다.

docker build -t spring:2 .
[+] Building 0.8s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                            0.0s
 => => transferring dockerfile: 37B                                                                                                                                             0.0s
 => [internal] load .dockerignore                                                                                                                                               0.0s
 => => transferring context: 2B                                                                                                                                                 0.0s
 => [internal] load metadata for docker.io/library/amazoncorretto:17                                                                                                            0.0s
 => [internal] load metadata for docker.io/library/gradle:7.6.1                                                                                                                 0.7s
 => [stage-1 1/3] FROM docker.io/library/amazoncorretto:17                                                                                                                      0.0s
 => [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226                                                     0.0s
 => [internal] load build context                                                                                                                                               0.0s
 => => transferring context: 6.31kB                                                                                                                                             0.0s
 => CACHED [build 2/4] WORKDIR /src                                                                                                                                             0.0s
 => CACHED [build 3/4] COPY . .                                                                                                                                                 0.0s
 => CACHED [build 4/4] RUN gradle bootJar                                                                                                                                       0.0s
 => CACHED [stage-1 2/3] COPY --from=build /src/build/libs/*.jar app.jar                                                                                                        0.0s
 => CACHED [stage-1 3/3] WORKDIR /app                                                                                                                                           0.0s
 => exporting to image                                                                                                                                                          0.0s
 => => exporting layers                                                                                                                                                         0.0s
 => => writing image sha256:82b380afaf34fb346b73e902d1308f265be6951cd9ec8895266dabccd63afb4f                                                                                    0.0s
 => => naming to docker.io/library/spring:2                                                                                                                                     0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

이번에는 spring-backend 프로젝트의 소스 코드를 변경하고 이미지를 다시 빌드하면 어떻게 될까요?
소스코드가 변경되었기 때문에 새로운 이미지 레이어가 생깁니다. 도커의 이미지 레이어가 변경되면 변경된 레이어보다 위에 오는 레이어는 재사용할 수 없습니다.

소스코드를 수정한 뒤 다시 빌드해봅시다. 소스 코드가 변경되었으므로 app.jar 파일이 변경됩니다.
따라서 COPY --from=build /src/build/libs/*.jar app.jar 인스트럭션의 결과가 달라지므로 이전에 캐시된 레이어를 재사용하지 않습니다. 따라서 해당 인스트럭션 다음의 인스트럭션은 수정된 것이 없더라도 재실행됩니다.

따라서 아래 로그에서 COPY, WORKDIR 인스트럭션에 CACHED가 빠진 것을 볼 수 있습니다.

docker build -t spring:3 .
[+] Building 23.0s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                            0.0s
 => => transferring dockerfile: 37B                                                                                                                                             0.0s
 => [internal] load .dockerignore                                                                                                                                               0.0s
 => => transferring context: 2B                                                                                                                                                 0.0s
 => [internal] load metadata for docker.io/library/amazoncorretto:17                                                                                                            0.0s
 => [internal] load metadata for docker.io/library/gradle:7.6.1                                                                                                                 0.7s
 => CACHED [stage-1 1/3] FROM docker.io/library/amazoncorretto:17                                                                                                               0.0s
 => [internal] load build context                                                                                                                                               0.0s
 => => transferring context: 6.69kB                                                                                                                                             0.0s
 => [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226                                                     0.0s
 => CACHED [build 2/4] WORKDIR /src                                                                                                                                             0.0s
 => [build 3/4] COPY . .                                                                                                                                                        0.2s
 => [build 4/4] RUN gradle bootJar                                                                                                                                             21.5s
 => [stage-1 2/3] COPY --from=build /src/build/libs/*.jar app.jar                                                                                                               0.1s
 => [stage-1 3/3] WORKDIR /app                                                                                                                                                  0.0s
 => exporting to image                                                                                                                                                          0.1s
 => => exporting layers                                                                                                                                                         0.1s
 => => writing image sha256:aa750785e5efea06cb5cdf755190d1aaa1460a541112acb5e486e7def7f1102a                                                                                    0.0s
 => => naming to docker.io/library/spring:3                                                                                                                                     0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

이번에는 Dockerfile을 아래와 같이 수정해보겠습니다.

FROM gradle:7.6.1 AS build

WORKDIR /src
COPY . .
RUN gradle bootJar

FROM amazoncorretto:17

WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

COPY --from=build /src/build/libs/*.jar app.jar

캐시를 초기화하고 이미지를 다시 빌드해봅니다.

모든 이미지를 삭제합니다.

docker rmi -f $(docker images -a)

빌드 캐시를 삭제합니다.

docker builder prune
docker build -t spring:1 .
[+] Building 68.2s (14/14) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                            0.0s
 => => transferring dockerfile: 251B                                                                                                                                            0.0s
 => [internal] load .dockerignore                                                                                                                                               0.0s
 => => transferring context: 2B                                                                                                                                                 0.0s
 => [internal] load metadata for docker.io/library/amazoncorretto:17                                                                                                            0.0s
 => [internal] load metadata for docker.io/library/gradle:7.6.1                                                                                                                 2.3s
 => [auth] library/gradle:pull token for registry-1.docker.io                                                                                                                   0.0s
 => [stage-1 1/3] FROM docker.io/library/amazoncorretto:17                                                                                                                      0.0s
 => [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226                                                    42.4s
 => => resolve docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226                                                           0.0s
 => => sha256:b3eb20d332dad785127ed3f3e6dcc4b0d15bd0c80eed668d36d238b6c154998a 18.46MB / 18.46MB                                                                                5.4s
 => => sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 1.21kB / 1.21kB                                                                                  0.0s
 => => sha256:f76840a01c8dcffa48f41af385088d66b51ca6214ebb03f5ffb6c72b1e376fe6 1.79kB / 1.79kB                                                                                  0.0s
 => => sha256:f3a54ae46b7b05d61cce6ffe7724ecec8fb77191aae25628e1fa7cddfa24c9d4 10.26kB / 10.26kB                                                                                0.0s
 => => sha256:6c7698a779f6d8c45a39a6721fb5cce267d66ff8ab5181c55aa6d02c8ddacd01 28.39MB / 28.39MB                                                                               14.1s
 => => sha256:498a0978395375469510ee1ec67911c5fc47525d29edceb6407fe24c94ba23e0 191.40MB / 191.40MB                                                                             36.3s
 => => sha256:611408d965705e52bf288c19198044f6d0623af144aa26e3c18f917ea296215e 175B / 175B                                                                                      5.6s
 => => sha256:53b4b9e222f66a540b194cd9c75a811c4f3762826a015ca45f37e771f97b0b78 4.37kB / 4.37kB                                                                                  5.9s
 => => sha256:0a93e4039b0d410743a10a3bf95b8ef5f1bbb45e6caa43364a38c9240292a0ee 51.10MB / 51.10MB                                                                               13.6s
 => => sha256:cce21c45d77e8bc478d3c19b5af3f9bc8a137cf56fed46c308a81162248120e8 122.08MB / 122.08MB                                                                             35.7s
 => => extracting sha256:6c7698a779f6d8c45a39a6721fb5cce267d66ff8ab5181c55aa6d02c8ddacd01                                                                                       0.8s
 => => extracting sha256:b3eb20d332dad785127ed3f3e6dcc4b0d15bd0c80eed668d36d238b6c154998a                                                                                       0.6s
 => => extracting sha256:498a0978395375469510ee1ec67911c5fc47525d29edceb6407fe24c94ba23e0                                                                                       2.8s
 => => extracting sha256:611408d965705e52bf288c19198044f6d0623af144aa26e3c18f917ea296215e                                                                                       0.0s
 => => extracting sha256:53b4b9e222f66a540b194cd9c75a811c4f3762826a015ca45f37e771f97b0b78                                                                                       0.0s
 => => extracting sha256:0a93e4039b0d410743a10a3bf95b8ef5f1bbb45e6caa43364a38c9240292a0ee                                                                                       1.5s
 => => extracting sha256:cce21c45d77e8bc478d3c19b5af3f9bc8a137cf56fed46c308a81162248120e8                                                                                       1.4s
 => [internal] load build context                                                                                                                                               1.3s
 => => transferring context: 45.20MB                                                                                                                                            1.3s
 => [stage-1 2/3] WORKDIR /app                                                                                                                                                  0.1s
 => [build 2/4] WORKDIR /src                                                                                                                                                    0.1s
 => [build 3/4] COPY . .                                                                                                                                                        0.1s
 => [build 4/4] RUN gradle bootJar                                                                                                                                             21.5s
 => [stage-1 3/3] COPY --from=build /src/build/libs/*.jar app.jar                                                                                                               0.1s
 => exporting to image                                                                                                                                                          0.1s
 => => exporting layers                                                                                                                                                         0.1s
 => => writing image sha256:b36fe5a249d0d6ebbd861da30c5d50e82ef0109040f12f2855aee0c88c305bb9                                                                                    0.0s
 => => naming to docker.io/library/spring:1                                                                                                                                     0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

동일한 이미지로 다시 빌드해봅시다.

docker build -t spring:2 .
[+] Building 0.8s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                            0.0s
 => => transferring dockerfile: 37B                                                                                                                                             0.0s
 => [internal] load .dockerignore                                                                                                                                               0.0s
 => => transferring context: 2B                                                                                                                                                 0.0s
 => [internal] load metadata for docker.io/library/amazoncorretto:17                                                                                                            0.0s
 => [internal] load metadata for docker.io/library/gradle:7.6.1                                                                                                                 0.7s
 => [stage-1 1/3] FROM docker.io/library/amazoncorretto:17                                                                                                                      0.0s
 => [internal] load build context                                                                                                                                               0.0s
 => => transferring context: 6.31kB                                                                                                                                             0.0s
 => [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226                                                     0.0s
 => CACHED [stage-1 2/3] WORKDIR /app                                                                                                                                           0.0s
 => CACHED [build 2/4] WORKDIR /src                                                                                                                                             0.0s
 => CACHED [build 3/4] COPY . .                                                                                                                                                 0.0s
 => CACHED [build 4/4] RUN gradle bootJar                                                                                                                                       0.0s
 => CACHED [stage-1 3/3] COPY --from=build /src/build/libs/*.jar app.jar                                                                                                        0.0s
 => exporting to image                                                                                                                                                          0.0s
 => => exporting layers                                                                                                                                                         0.0s
 => => writing image sha256:b36fe5a249d0d6ebbd861da30c5d50e82ef0109040f12f2855aee0c88c305bb9                                                                                    0.0s
 => => naming to docker.io/library/spring:2                                                                                                                                     0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

이번에는 소스코드를 수정하고 빌드해봅시다. 이번에는 CACHED [stage-1 2/3] WORKDIR /app 부분을 보면 캐시된 이미지 레이어를 사용합니다.

docker build -t spring:3 .
[+] Building 21.9s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                            0.0s
 => => transferring dockerfile: 37B                                                                                                                                             0.0s
 => [internal] load .dockerignore                                                                                                                                               0.0s
 => => transferring context: 2B                                                                                                                                                 0.0s
 => [internal] load metadata for docker.io/library/amazoncorretto:17                                                                                                            0.0s
 => [internal] load metadata for docker.io/library/gradle:7.6.1                                                                                                                 0.7s
 => [internal] load build context                                                                                                                                               0.0s
 => => transferring context: 6.69kB                                                                                                                                             0.0s
 => [stage-1 1/3] FROM docker.io/library/amazoncorretto:17                                                                                                                      0.0s
 => [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226                                                     0.0s
 => CACHED [build 2/4] WORKDIR /src                                                                                                                                             0.0s
 => [build 3/4] COPY . .                                                                                                                                                        0.2s
 => [build 4/4] RUN gradle bootJar                                                                                                                                             20.4s
 => CACHED [stage-1 2/3] WORKDIR /app                                                                                                                                           0.0s
 => [stage-1 3/3] COPY --from=build /src/build/libs/*.jar app.jar                                                                                                               0.1s
 => exporting to image                                                                                                                                                          0.1s
 => => exporting layers                                                                                                                                                         0.1s
 => => writing image sha256:c5ee9f587a5bf16a10b66aac79d324ba5f51c894eab32b61a9c8e780f921fb6c                                                                                    0.0s
 => => naming to docker.io/library/spring:3                                                                                                                                     0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

따라서 Dockerfile 스크립트의 인스트럭션은 잘 수정하지 않는 인스트럭션이 앞으로 오고 자주 수정되는 인스트럭션이 뒤로 오도록 배치하는 것이 좋습니다. 이렇게 해야 캐시에 저장된 이미지 레이어를 되도록 많이 재사용할 수 있습니다.

최적화한 Dockerfile을 다시 보면, EXPOSE, ENTRYPOINT 인스트럭션은 COPY 인스트럭션보다 수정할 일이 적으므로 앞에 배치합니다.
COPY 인스트럭션은 소스 코드가 변경되면 app.jar가 변경되므로 복사할 파일의 내용이 변경됬기 때문에 재사용되지 않고 다시 실행됩니다.
이렇게하면 소스코드가 변경되어도 마지막 COPY 인스트럭션을 제외한 WORKDIR, EXPOSE, ENTRYPOINT 인스트럭션을 재사용 할 수 있습니다.

최종 수정한 예제 코드는 https://github.com/nefertirii/docker-example/tree/2819ddc4a6ee5564a89583947fb3934c330274e4 를 참고하시기 바랍니다.

멀티 스테이지 빌드

위의 spring-backend 이미지의 Dockerfile 스크립트를 다시 한 번 보겠습니다. 이 스크립트에는 빌드가 여러 단계로 나뉘는 멀티 스테이지 빌드가 적용되었습니다.

FROM gradle:7.6.1 AS build

WORKDIR /src
COPY . .
RUN gradle bootJar

# 멀티 스테이지 빌드
FROM amazoncorretto:17

WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

COPY --from=build /src/build/libs/*.jar app.jar

각 빌드 단계는 FROM 인스트럭션으로 시작됩니다. FROM 뒤에 AS를 이용해 이름을 붙일 수도 있습니다.

위의 스크립트는 크게 두 단계로 나뉩니다. 첫 번째 단계인 build 단계에서 소스 코드를 빌드하여 jar 파일을 생성합니다. 마지막 단계인 두 번째 단계에서 build 단계에서 만들어진 jar 파일을 복사합니다. 각 빌드 단계는 독립적으로 실행되지만, 앞 단계에서 만들어진 디렉터리나 파일을 복사할 수 있습니다. 최종 산출물은 마지막 단계의 내용물을 담은 도커이미지 입니다.

정리하면, 빌드 단계에서는 빌드에 필요한 빌드 도구가 설치된 기반 이미지를 사용하여 소스 코드를 빌드합니다. 그리고 실행 단계에서는 애플리케이션을 실행할 런타임이 들어있는 기반이미지를 사용하고, 앞에서 빌드한 애플리케이션을 복사합니다.

이렇게 멀티 스테이지 빌드를 사용하면 어떤 점이 좋을까요?

첫 번째로, 도커가 설치되어 있고 소스 코드와 Dockerfile만 있다면 애플리케이션을 실행할 수 있습니다. 빌드 도구와 자바 런타임을 설치할 필요가 없습니다. 깃허브 같은 코드 저장소에서 다운로드 받은 다음 도커 명령어만 사용하면 애플리케이션을 빌드하고 실행할 수 있습니다.

두 번째로, 최종적으로 생성되는 애플리케이션 이미지에 빌드 도구는 포함되지 않습니다. 멀티 스테이지 빌드에서 생성되는 이미지에는 마지막 단계의 내용물만 포함되므로 빌드 도구 없이 런타임과 애플리케이션만 포함됩니다. 따라서 빌드에 필요한 환경과 실행에 필요한 환경을 분리하여 애플리케이션 실행에 필요한 환경만 이미지에 담아 용량을 줄일 수 있습니다.

0개의 댓글