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

jjunhwan.kim·2023년 6월 15일
0

도커

목록 보기
2/6
post-thumbnail

개요

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

레지스트리, 리포지터리, 이미지 태그

도커 컨테이너와 이미지-01 포스트에서 이미지에서 컨테이너를 실행시 로컬 컴퓨터에 이미지가 없으면 도커가 자동으로 이미지를 내려받는 것을 보았습니다.

도커 이미지들은 도커 레지스트리라고 불리는 서버에 저장됩니다. 도커 허브(https://hub.docker.com)는 도커 레지스트리 중에서 가장 유명한 레지스트리입니다. 또한 도커 허브는 도커 엔진에 기본으로 설정된 레지스트리입니다. 따라서 도커 엔진이 로컬 컴퓨터에 없는 이미지를 내려받을 때 가장 먼저 찾아보는 곳이 도커 허브입니다.

도커 이미지에는 이름이 부여됩니다. 이미지의 전체 이름은(이미지 참조라고도 부릅니다) 네 개의 요소로 구성됩니다. 레지스트리 도메인, 계정, 리포지터리, 이미지 태그로 구성됩니다.

아래와 같은 형식입니다.

{이미지 레지스트리 도메인}/{계정 이름}/{이미지 리포지터리 이름}:{이미지 태그}

아래는 젠킨스 이미지의 이미지 참조입니다.

docker.io/jenkins/jenkins:lts-jdk11
  • 이미지 레지스트리 도메인: 이미지가 저장된 레지스트리의 도메인으로 기본 값은 도커 허브입니다.
  • 계정 이름: 이미지 작성자의 계정 이름입니다. 개인 혹은 단체 이름입니다.
  • 이미지 리포지터리 이름: 일반적으로 애플리케이션의 이름입니다. 하나의 리포지터리는 여러 버전의 이미지를 담을 수 있습니다.
  • 이미지 태그: 애플리케이션의 버전을 나타냅니다. 기본 값은 latest입니다.

로컬 컴퓨터에서만 사용하면 이미지 이름을 마음대로 지어도 문제가 없지만, 레지스트리에 이미지를 업로드 하려면 이미지 이름의 구성 요소(도메인, 계정, 리포지터리, 태그) 형식을 만족해야합니다.

레지스트리와 태그는 따로 지정하지 않으면 기본 값을 사용합니다. 레지스트리의 기본 값은 도커 허브, 태그의 기본 값은 latest를 사용합니다.

이미지 태그는 항상 부여해야합니다. 태그는 같은 애플리케이션의 서로 다른 버전을 구별하기 위해 쓰입니다. 이미지를 빌드할 때 태그를 따로 지정하지 않으면 기본적으로 latest 태그가 부여됩니다. 명시적으로 태그를 부여하여 레지스트리에 이미지를 푸시하는 게 좋습니다.

도커 허브에 직접 빌드한 이미지 푸시하기

먼저 도커 허브에 이미지를 푸시하려면 도커 허브 계정이 필요합니다. https://www.docker.com 에서 계정을 생성합니다.

레지스트리에 이미지를 푸시하려면 두 가지 절차가 필요합니다.

첫 번째는 도커 CLI에서 도커 허브에 로그인을 해야합니다. 터미널에서 아래 명령어를 입력하여 로그인합니다.

docker login

두 번째는 이미지에 푸시 권한을 가진 계정명을 포함하는 이미지 참조를 붙여야 합니다.

먼저 이미지를 빌드해 보겠습니다. 이전 포스트에서 사용했던 예제 프로젝트(https://github.com/nefertirii/docker-example/tree/2819ddc4a6ee5564a89583947fb3934c330274e4)를 사용하여 빌드하겠습니다.

docker build -t spring-backend:1.0.0 .

spring-backend:1.0.0 이미지를 빌드하였습니다. 이미지 참조를 부여해보겠습니다.

docker tag spring-backend:1.0.0 jjunhwan/spring-backend:1.0.0

docker images 명령어로 확인하면 아래와 같이 출력됩니다.

jjunhwan/spring-backend   1.0.0     780721b9c59c   39 seconds ago   540MB
spring-backend            1.0.0     780721b9c59c   39 seconds ago   540MB

docker push 명령어로 이미지를 레지스트리에 푸시합니다.

docker push jjunhwan/spring-backend:1.0.0

The push refers to repository [docker.io/jjunhwan/spring-backend]
c0af6e82b2f7: Pushed
31a4c635113f: Pushed
074f0db39a93: Mounted from library/amazoncorretto
ee2f70e82444: Mounted from library/amazoncorretto
1.0.0: digest: sha256:bc0f8bbb34876637d15abfd361ab63e4e57842755791d9abba701f2c1dd6a8d6 size: 1161

도커 허브에 접속하면 푸시된 이미지를 확인할 수 있습니다.

레지스트리도 도커 엔진과 같은 방식으로 이미지 레이어를 다루기 때문에 Dockerfile 스크립트 최적화가 중요합니다. 레지스트리의 캐시상에 일치하는 레이어가 없을 경우에만 실제로 업로드가 이루어집니다.

컨테이너 속 데이터가 사라지는 이유

도커 컨테이너에도 단일 드라이브로 된 파일 시스템이 있습니다. 파일 시스템의 내용은 이미지 속 파일로부터 만들어집니다. Dockerfile 스크립트에서 COPY 인스트럭션을 사용해 파일을 이미지로 복사하면, 이 이미지를 실행한 컨테이너에도 복사된 파일이 있습니다.

모든 컨테이너는 독립된 파일 시스템을 갖습니다. 같은 이미지에서 실행한 여러 개의 컨테이너는 한 컨테이너에서 파일을 수정해도 다른 컨테이너에서 영향을 받지 않습니다.

docker cp 명령어로 컨테이너와 로컬 컴퓨터 간에 파일을 복사할 수 있습니다.

먼저 우분투 컨테이너를 시작합니다.

docker run -it -d --name ubuntu bash

컨테이너에 접속합니다.

docker exec -it ubuntu bash

테스트 할 파일을 만듭니다.

cd /tmp
echo test > test.txt
exit

컨테이너 속 /tmp/test.txt 파일을 로컬 컴퓨터 현재 경로에 복사합니다.

docker cp {컨테이너이름}:{컨테이너 내부 파일경로} {로컬 컴퓨터 파일 경로}
docker cp ubuntu:/tmp/test.txt .

반대로 로컬 컴퓨터의 파일을 컨테이너 복사합니다.

docker cp {로컬 컴퓨터 파일 경로} {컨테이너이름}:{컨테이너 내부 파일경로}
docker cp test.txt ubuntu:/tmp

컨테이너의 파일 시스템은 단일 디스크인데, 이 디스크는 도커가 이미지 레이어와 기록 가능 레이어를 합쳐 만든 가상 파일 시스템 입니다.

아래 그림을 보면 이미지의 레이어는 모든 컨테이너가 공유합니다. 이미지 레이어는 읽기 전용이고, 각 컨테이너가 따로 갖는 기록 가능 레이어는 컨테이너와 같은 생애주기를 갖습니다.

이미지 레이어는 이미지를 다운받을 때 로컬 컴퓨터에 생성되고, 컨테이너의 기록 가능 레이어는 컨테이너를 실행 할 때 생성됩니다.

기록 가능 레이어는 새 파일을 만드는 것 뿐만 아니라 기존 이미지 레이어에 있는 파일을 수정 할 때에도 사용됩니다. 이미지 레이어는 읽기 전용이므로, 이미지 레이어에 포함된 파일을 수정하려 하면 도커는 해당 파일을 기록 가능 레이어에 복사해 온 다음 해당 레이어에서 파일을 수정합니다. 이를 기록 중 복사(copy-on-write)라고 합니다.

컨테이너 파일 시스템은 컨테이너와 같은 생애주기를 갖습니다. 컨테이너가 삭제되면 컨테이너의 기록 가능 레이어와 해당 레이어에서 수정된 데이터도 함께 삭제됩니다. 도커를 사용하면 애플리케이션을 업데이트 할 때 기존 컨테이너를 삭제하고 새 이미지를 빌드하고 새 이미지로부터 새로운 컨테이너를 시작하게 됩니다. 이 때 기존 컨테이너에 있는 데이터는 모두 사라지게 됩니다.

도커 볼륨과 마운트를 사용하면 컨테이너가 대체되도 지속되어야 할 데이터를 저장할 수 있습니다.

도커 볼륨을 사용하는 컨테이너 실행하기

볼륨은 컨테이너와 독립적으로 존재하며 별도의 생애주기를 갖는 스토리지 단위입니다. 볼륨을 생성해 컨테이너에 마운트하면 컨테이너 파일 시스템의 한 디렉터리가 됩니다. 나중에 애플리케이션을 업데이트하더라도 새로운 컨테이너에 볼륨을 마운트하면 데이터가 그대로 유지됩니다.

컨테이너에서 볼륨을 사용하는 방법은 두 가지가 있습니다. 첫 번째는 수동으로 볼륨을 생성하여 컨테이너에 마운트하는 방법입니다. 두 번째는 Dockerfile 스크립트에서 VOLUME 인스트럭션을 사용하는 방법입니다.

먼저 두 번째 방법부터 보겠습니다.

아래는 이전 포스트에서 작성했던 Dockerfile 스크립트에 VOLUME 인스트럭션을 추가했습니다.

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"]

# 볼륨 추가
VOLUME /data
COPY --from=build /src/build/libs/*.jar app.jar

도커 이미지에서 볼륨을 정의하면 컨테이너를 생성할 때마다 새로운 볼륨을 만듭니다. 위의 스크립트를 이미지로 빌드하고, 아래와 같이 두 개의 컨테이너를 실행합니다.

docker run --name backend1 -d -p 8080:8080 spring-backend:1.0

docker run --name backend2 -d -p 8081:8080 spring-backend:1.0

볼륨을 조회해 봅니다.

docker volume ls

두 개의 볼륨이 생성된 것을 확인할 수 있습니다.

DRIVER    VOLUME NAME
local     586c094a299bef813b61a84145ce75f75056447d7c842e9d10fe51a8d5b32e77
local     cbd60228790c238117d979328a522238708c933e2fc04e64b8565abed16b4cb1

각 컨테이너의 볼륨 정보를 자세히 보고싶다면 docker inspect 명령어를 사용합니다. Mounts 필드를 보면 볼륨 정보를 볼 수 있습니다.

docker inspect backend1
[
    {
    	...

        "Mounts": [
            {
                "Type": "volume",
                "Name": "586c094a299bef813b61a84145ce75f75056447d7c842e9d10fe51a8d5b32e77",
                "Source": "/var/lib/docker/volumes/586c094a299bef813b61a84145ce75f75056447d7c842e9d10fe51a8d5b32e77/_data",
                "Destination": "/data",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
        
        ...
    }
]

아래와 같이 docker run 으로 컨테이너 실행시 volumes-from 옵션을 사용하여 다른 컨테이너의 볼륨을 마운트하여 사용할 수도 있습니다. 이 때에는 backend1 컨테이너와 backend3 컨테이너가 동일한 볼륨을 공유합니다.

docker run --name backend3 -d -p 8082:8080 --volumes-from backend1 spring-backend:1.0

볼륨은 이미지에서 정의하는 것 보다는 명시적으로 관리하는 편이 더 낫습니다. 볼륨에 이름을 붙여서 생성하고 업데이트 시 다른 컨테이너로 마운트하면 됩니다.

첫 번째 방법인 수동으로 볼륨을 명시적으로 생성해 보겠습니다.

docker volume create backend-data

컨테이너 내부 경로에 볼륨 컨테이너를 마운트합니다. docker run 명령어의 -v 옵션의 값으로 {볼륨 이름}:{컨테이너 내부 경로} 를 전달하면 됩니다.

docker run --name backend4 -d -p 8083:8080 -v backend-data:/data spring-backend:1.0

backend4 컨테이너에 접속하여 마운트 된 /data 경로에 파일을 생성해 보겠습니다.

docker exec -it backend4 bash
cd /data
echo test > test.txt
exit

새로운 컨테이너 backend5를 실행합니다. 역시 동일한 backend-data 볼륨 컨테이너를 /data 경로에 마운트합니다.

docker run --name backend5 -d -p 8084:8080 -v backend-data:/data spring-backend:1.0

backend5 컨테이너는 backend4 컨테이너와 동일한 볼륨 컨테이너를 사용하므로 /data 경로에 test.txt 파일이 존재하는지 확인해보면 파일이 존재하는 것을 볼 수 있습니다.

docker exec -it backend5 bash
cd /data
ls

Dockerfile 스크립트의 VOLUME 인스트럭션과 docker run 명령어의 -v, --volume 옵션은 별개 기능입니다.

  • Dockerfile 스크립트에 VOLUME 인스트럭션을 사용해 빌드된 이미지를 docker run 명령으로 실행시키면 항상 새로운 볼륨을 생성합니다.
  • docker run 명령으로 컨테이너를 실행시킬 때 -v, --volume 옵션을 전달하면 이미지에 볼륨이 정의되어 있더라도 이 정의가 무시되고 지정된 볼륨을 컨테이너에 마운트합니다.
  • 위의 예제에서 backend4 와 backend5 컨테이너에 backend-data 볼륨 컨테이너가 마운트 된 것을 보면 알 수 있습니다. Dockerfile에 VOLUME 인스트럭션이 정의되어 있었지만 -v 옵션을 사용하였기 때문에 새로운 볼륨이 생성되지 않고 직접 생성한 backend-data 볼륨이 컨테이너에 마운트 된 것입니다.

파일 시스템 마운트를 사용하는 컨테이너 실행하기

호스트 컴퓨터의 스토리지를 컨테이너에 좀 더 직접적으로 연결할 수 있는 수단이 있습니다. 바로 바인드 마운트입니다. 바인드 마운트는 호스트 컴퓨터 파일 시스템의 디렉터리를 컨테이너 파일 시스템의 디렉터리로 만듭니다. 컨테이너가 호스트 컴퓨터의 파일에 직접 접근할 수 있고 반대로 호스트 컴퓨터에서 컨테이너의 디렉터리에도 접근할 수 있습니다.

바인드 바운트는 docker run 명령어의 -v 옵션의 값으로 {호스트 컴퓨터 경로}:{컨테이너 경로} 를 전달하면 됩니다.

아래와 같이 호스트 컴퓨터의 ~/Desktop/data 디렉터리를 컨테이너의 /data 디렉터리에 마운트합니다.

docker run --name backend6 -d -p 8085:8080 -v ~/Desktop/data:/data spring-backend:1.0

바인드 마운트는 양방향으로 동작합니다. 컨테이너에서 만든 파일을 호스트 컴퓨터에서 수정할 수도 있고, 반대로 호스트에서 만든 파일도 컨테이너에서 수정할 수 있습니다.

컨테이너에서 호스트 컴퓨터의 파일에 접근하기 위해 컨테이너의 계정 권한 상승이 필요할 수 있습니다. Dockerfile 스크립트에서 USER 인스트럭션을 사용하여 관리자 권한을 부여합니다.

호스트 컴퓨터가 접근할 수 있는 스토리지라면 무엇이든 바인드 마운트를 통해 컨테이너에 연결할 수 있습니다. 예를 들어 네트워크 드라이브 같은 경우에도 바인드 마운트로 컨테이너에 연결할 수 있습니다.

파일 시스템 마운트의 한계점

컨테이너의 마운트 대상 디렉터리가 이미 존재하고 이미지 레이어에 이 디렉터리의 파일이 포함되어 있다면 어떻게 될까요? 이미 존재하는 대상 디렉터리에 마운트하면 이미지에 포함되어 있던 원래 파일은 사용할 수 없습니다.

아래와 같이 이미지에 /data/test.txt 파일을 포함시켜 보겠습니다.

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"]

# /data 디렉터리 생성 후 test.txt 파일 생성
RUN mkdir /data && echo test > /data/test.txt
COPY --from=build /src/build/libs/*.jar app.jar

이미지를 빌드하고 컨테이너로 실행시켜 보겠습니다.

docker build -t spring-backend:1.0 .
docker run --name backend1 -d -p 8080:8080 spring-backend:1.0

컨테이너에 접속하여 /data/test.txt 파일이 존재하는 것을 확인합니다.

docker exec -it backend1 bash

cd /data
ls

이번에는 동일한 이미지를 바인드 마운트하여 컨테이너로 실행해 봅니다.

docker run --name backend2 -d -p 8081:8080 -v ~/Desktop/data:/data spring-backend:1.0

컨테이너에 접속하여 /data 경로를 확인하면 test.txt 파일이 없는 것을 볼 수 있습니다.

docker exec -it backend2 bash

cd /data
ls

즉 바인드 마운트를 하여 컨테이너를 실행하면 원래 디렉터리의 내용은 숨겨지고 바인드 마운트 한 디렉터리가 이를 대체합니다.

이번에는 호스트 컴퓨터의 파일 하나를 컨테이너에 이미 존재하는 디렉터리에 마운트하면 어떻게 될까요? 이는 조금 다르게 동작합니다.
호스트 컴퓨터의 ~/Desktop/data 디렉터리에 test2.txt를 만들고 이를 동일하게 컨테이너의 data 디렉터리에 마운트 해 보겠습니다.
아래의 커맨드를 보면 -v ~/Desktop/data/test2.txt:/data/test2.txt 부분에서 호스트 컴퓨터의 test2.txt 파일 하나를 컨테이너의 data 디렉터리에 test2.txt 라는 하나의 파일로 마운트 하였습니다.

docker run --name backend3 -d -p 8082:8080 -v ~/Desktop/data/test2.txt:/data/test2.txt spring-backend:1.0

컨테이너에 접속하여 /data 경로를 확인하면 이번에는 이미지에 포함된 ₩test.txt 파일과 바인드 마운트 한 test2.txt 파일이 둘 다 존재하는 것을 확인할 수 있습니다.

docker exec -it backend3 bash

cd /data
ls

단, 이 기능은 윈도우 컨테이너에서는 제공하지 않아 동작이 다르다고 합니다.

분산 파일 시스템을 컨테이너에 바인드 마운트하면 어떻게 될까요? 분산 파일 시스템을 사용하면 네트워크상의 모든 컴퓨터에서 데이터에 접근할 수 있습니다. 분산 파일 시스템의 매커니즘은 SMB, 애저 파일스, AWS S3 등 다양한데 이런 분산 파일 스토리지를 컨테이너에 마운트하면 일반적인 파일 시스템의 일부처럼 보이지만 지원하지 않는 동작이 있을 수 있습니다.

따라서 바인드 마운트의 원본 스토리지가 컨테이너에서 사용하는 모든 파일 시스템 기능을 제공하지 않을 수 있다는 것을 고려해야 합니다. 하지만 이 사실은 애플리케이션을 실행해 보지 않고서는 미리 파악하기 어렵습니다. 또한 분산 스토리지의 성능이 로컬 스토리지와 큰 차이가 있다는 것도 생각해야 합니다. 분산 스토리지는 파일 입출력이 네트워크를 거쳐야 하기 때문입니다.

컨테이너의 파일 시스템은 어떻게 만들어지는가

모든 컨테이너는 도커가 다양한 출처로부터 모아 만든 단일 가상 디스크로 구성된 파일 시스템을 갖습니다. 이 파일 시스템을 유니언 파일 시스템이라고 합니다. 이 유니언 파일 시스템은 운영체제마다 다른 방식으로 구현되어 있습니다.

컨테이너는 유니언 파일 시스템을 통해 물리적 위치가 서로 다른 파일과 디렉터리에 마치 단일 디스크를 사용하듯 접근할 수 있습니다.

컨테이너는 위의 그림 처림 이미지 레이어, 볼륨 마운트, 바인드 마운트, 기록가능 레이어 등 다양한 출처가 합쳐져 단일 디스크가 구성됩니다. 볼륨 마운트와 바인드 마운트는 하나 이상이 가능하지만 기록 가능 레이어는 하나만 가질 수 있습니다.

다음은 컨테이너 스토리지를 구성할 때 고려해야 할 일반론입니다.

  • 기록 가능 레이어: 비용이 비싼 계산이나 네트워크를 통해 저장해야 하는 데이터의 캐싱 등 단기 저장에 적합합니다. 컨테이너가 삭제되면 저장된 데이터는 삭제됩니다.

  • 로컬 바인드 마운트: 호스트 컴퓨터와 컨테이너 간 데이터를 공유하기 위해 사용합니다. 호스트 컴퓨터에서 마운트 디렉터리의 파일 내용을 수정하면 즉시 컨테이너로 변경된 내용이 전달될 수 있습니다.

  • 분산 바인드 마운트: 네트워크 스토리지와 컨테이너 간에 데이터를 공유하기 위해 사용합니다. 가용성이 높지만 로컬 디스크와 비교해 지원하지 않는 파일 시스템 기능이 있거나 성능 차이가 있을 수 있습니다. 읽기 전용으로 설정 파일을 전달하거나 공유 캐시로 활용할 수 있습니다. 읽기 쓰기 가능으로 데이터를 저장해 동일 네트워크상의 모든 컨테이너나 컴퓨터와 데이터를 공유하는데 적합합니다.

  • 볼륨 마운트: 컨테이너와 도커 객체인 볼륨 간에 데이터를 공유하기 위해 사용합니다. 볼륨 마운트를 사용하면 애플리케이션이 볼륨에 데이터를 영구적으로 저장할 수 있습니다. 컨테이너를 교체하는 방식으로 애플리케이션을 업데이트해도, 이전 버전 컨테이너의 데이터를 그대로 유지할 수 있습니다.

  • 이미지 레이어: 이미지 레이어는 컨테이너의 초기 파일 시스템을 구성합니다. 레이어는 적층 구조를 갖는데 후속 레이어와 이전 레이어가 충돌하는 경우 후속 레이어의 내용이 적용됩니다. 레이어는 읽기 전용이고 여러 컨테이너가 공유합니다.

0개의 댓글