본 글은 도커 교과서를 읽고 정리한 내용입니다.
이번 장에선 도커 볼륨과 마운트에 대해 배우고 컨테이너 파일 시스템이 어떻게 동작하는지 공부할 예정.
컨테이너 속 데이터가 사라지는 이유
도커 컨테이너에도 단일 드라이브로 된 파일 시스템이 존재한다.
파일 시스템의 내용은 이미지 속 파일에서 생성된다.
Dockerfile 스크립트에 COPY 인스트럭션으로 파일을 이미지로 복사하면 이 이미지로 실행한 컨테이너의 경우 같은 경로에 복사한 파일이 생성된다.
도커 이미지가 여러 개의 레이어 형태로 저장되는 거서럼 컨테이너 디스크 역시 이 이미지 레이어를 순서대로 합쳐 만든 가상 파일 시스템이다.
모든 컨테이너는 독립된 파일 시스템을 갖는다.
즉, 같은 이미지에서 실행한 여러 개의 컨테이너는 서로 영향을 미치지 않음.
이를 확인해보기 위해 다음 명령어를 실행해보고 출력을 확인해보자.
docker container run --name rn1 diamol/ch06-random-number
docker container run --name rn2 diamol/ch06-random-number
그 후
docker container cp rn1:/random/number.txt number1.txt
docker container cp rn2:/random/number.txt number2.txt
위 명령어를 통해 컨테이너 파일 시스템에 존재하는 txt 파일을 로컬 컴퓨터로 복사해 파일 내용을 확인해보자.
두 파일 내용이 다른 것을 보니 컨테이너의 파일 시스템이 독립적이란 것을 알 수 있다!
컨테이너의 파일시스템은 단일 디스크이다.
이 디스크는 도커가 여러 출처로부터 합쳐 만들고 컨테이너에 전달한 가상 파일 시스템이다.
이 출처는 기본적으로 이미지 레이어와 컨테이너의 기록 가능(read/write) 레이어로 구성되는데, 이미지 레이어는 모든 컨테이너가 공유하지만 기록 가능 레이어는 컨테이너마다 다르다.
이미지 레이어는 이미지를 내려받는 순간부터 삭제할 때까지 로컬 컴퓨터의 이미지 레이어에 존재하지만, 컨테이너의 쓰기 가능 레이어는 컨테이너를 실행할 때 생성되며 컨테이너를 삭제할 때 함께 삭제된다.(컨테이너를 종료한다고 컨테이너가 삭제되는 것은 아님.)
기록 가능 레이어를 통해 기존 이미지 레이어에 있는 파일을 수정할 수 있다.
이미지 레이어는 읽기 전용이라고 했는데 어떻게 수정이 가능할까?
도커는 기록 중 복사(copy-on-write)라는 방법을 사용해 읽기 전용 레이어를 수정한다.
컨테이너에서 이미지 레이어에 포함된 파일을 수정하려면, 먼저 도커가 이 파일을 쓰기 가능 레이어로 복사해 온 다음 쓰기 가능 레이어에서 파일을 수정한다.
컨테이너를 실행해 이미지 레이어에 포함된 파일의 내용을 출력해보자.
docker container run --name f1 diamol/ch06-file-display
파일의 내용을 수정한 뒤 컨테이너를 재시작해 변경된 파일 내용을 확인해보자
echo "change file content" > url.txt
docker container cp url.txt f1:/input.txt
다음 명령어를 통해 파일 내용이 바뀌었는지 확인해보자.
docker container start --attach f1
우리가 덮어쓴 파일 내용이 출력되는 것을 볼 수 있다
이렇게 수정된 파일은 해당 컨테이너의 기록 가능 레이어에만 존재한다.(이미지를 공유하는 다른 컨테이너나 이미지는 영향을 받지 않음)
만약 같은 이미지를 통해 새로운 컨테이너를 만들면 최초의 내용을 담은 파일 시스템을 가지며 , f1 컨테이너가 삭제되면 수정된 파일도 사라진다.
같은 이미지로 새로운 컨테이너를 실행해보고 내용을 확인해보자.
docker container run --name f2 diamol/ch06-file-display
최초의 내용이 존재한다.
처음 실행했던 컨테이너(f1)을 삭제하고 수정된 데이터가 사라진 것을 확인해보자.
docker container rm -f f1
docker container cp f1:/input.txt .
컨테이너 파일 시스템은 컨테이너와 같은 라이프사이클을 가진다.
컨테이너가 삭제되면 파일 시스템도 같이 삭제된다.
그런데 만약 컨테이너로 데이터베이스를 실행해 사용했다고 생각해보자.
데이터베이스 버전을 업데이트해서 다시 빌드했더니 모든 데이터가 사라지게 된다면 재앙이 될 것이다.
도커는 이런 경우를 방지하기 위해 도커 볼륨(Docker volume)과 마운트(Mount)라는 요소를 추가했다.
도커 볼륨과 마운트는 컨테이너와 별개의 라이프사이클을 가진다.
즉, 컨테이너가 대체돼도 삭제되면 안되는 데이터를 저장할 수 있다.
도커 볼륨을 사용하는 컨테이너 실행하기
도커 볼륨이란
- 도커에서 스토리지를 다루는 단위
- 컨테이너와 독립적으로 존재하며 별도의 생애주기를 갖는다.
- 컨테이너에 연결할 수 있다.
- 영속성이 필요한 stateful 애플리케이션을 컨테이너로 사용하려면 볼륨을 사용해야 한다.
- 컨테이너를 위한 USB 메모리라 생각하면 편함!
- 볼륨을 생성해 애플리케이션 컨테이너에 연결하면 컨테이너 파일 시스템의 한 디렉터리가 된다.
- 애플리케이션을 업데이트 하더라도 새로운 컨테이너에 다시 볼륨을 연결하면 데이터가 그대로 유지된다.
컨테이너에서 볼륨을 사용하는 방법은 두 가지.
- 수동으로 직접 볼륨을 생성해 컨테이너에 연결하기
- Dockerfile 스크립트에서 VOLUME 인스트럭션을 사용하기
VOLUME 인스트럭션을 사용해 만든 이미지로 컨테이너를 실행하면 자동으로 볼륨을 생성한다.
VOLUME <target-directory>
위와 같은 형식으로 사용!
아래 스크립트를 보며 볼륨에 대해 알아보자.
FROM diamol/dotnet-aspnet
WORKDIR /app
ENTRYPOINT ["dotnet", "ToDoList.dll"]
VOLUME /data
COPY --from=builder /out/ .
이 이미지로부터 컨테이너를 실행하면 자동으로 볼륨을 생성해 컨테이너를 연결해준다.
실행된 컨테이너엔 /data 디렉토리가 생성되는데 이 디렉토리는 다른 디렉토리와 똑같이 사용할 수 있지만 이 디렉토리 안의 내용은 볼륨에 영구 저장된다.
실제인지 확인해보자!
docker container run --name todo1 -d -p 8010:80 diamol/ch06-todo-list
docker container inspect --format '{{.Mounts}}' todo1
docker volume ls
볼륨은 도커에서 이미지나 컨테이너와 동급인 요소이다.
docker volume 명령어를 사용해 볼륨을 만들고 목록을 확인하고 삭제할 수 있다.
http://localhost:8010 으로 들어가보자.
이 애플리케이션은 데이터를 /data 디렉토리에 저장하는데, 웹페이지에서 to-do를 하나 추가하면 이 데이터는 도커 볼륨에 저장된다.
도커 이미지에서 볼륨을 정의하면 컨테이너를 생성할 때마다 새로운 볼륨을 만드는데, 컨테이너가 같은 볼륨을 공유하게 할 수 있다.
volumes-from 플래그를 적용하면 다른 컨테이너의 볼륨을 연결할 수 있다.
다음 예제는 같은 데이터를 공유하는 to-do 애플리케이션 컨테이너 두 개를 만드는 과정이다.
# 이 컨테이너를 실행하면 볼륨을 생성한다.
docker container run --name todo2 -d diamol/ch06-todo-list
# 리눅스/맥 환경의 경우
docker container exec todo2 ls /data
# 이 컨테이너는 todo1과 볼륨을 공유한다.
docker container run -d -name todo3 --volumes-from todo1 diamol/ch06-todo-list
# 리눅스/맥 환경의 경우
docker container exec todo3 ls /data
todo2 컨테이너는 새로운 볼륨을 생성해 연결하기 때문에 /data 디렉토리가 비어있다.
todo3 컨테이너는 todo1 컨테이너의 볼륨을 공유하기에 /data 디렉토리에 애플리케이션에서 추가된 데이터가 보인다.
애플리케이션 컨테이너는 종종 자신만이 접근할 수 있는 파일이 필요하다.
이런 파일을 다른 컨테이너가 동시에 접근하게 허용하면 애플리케이션이 비정상적으로 동작할 수 있다.
볼륨은 컨테이너 간 파일 공유 보단 업데이트 간 상태를 보존하기 위한 용도로 사용해야 하며, 이미지에서 정의하는 것보다는 명시적으로 관리하는 편이 더 낫다.
볼륨에 이름을 붙여 생성하고 업데이트 시 다른 컨테이너로 옮겨 연결하면 된다.
볼륨을 생성하고 데이터를 집어넣은 뒤 다른 애플리케이션에 연결해보자!
먼저 다음 명령어로 복사 대상 경로를 환경변수로 지정하자.
target='/data' # 리눅스, 맥 컨테이너
데이터를 저장할 볼륨을 생성해보자.
docker volume create todo-list
생성한 볼륨을 연결해 애플리케이션을 실행하자.
docker container run -d -p 8011:80 -v todo-list:$target --name todo-v1 diamol/ch06-todo-list
http://localhost:8011 에 들어가 데이터를 몇 건 추가해보자.
todo-v1 애플리케이션을 삭제하자.
docker container rm -f todo-v1
같은 볼륨을 사용해 todo-v2 애플리케이션을 실행하자
docker container run -d -p 8011:80 -v todo-list:$target --name todo-v2 diamol/ch06-todo-list:v2
v1에서 저장했던 데이터들이 그대로 남아있는 것을 볼 수 있다.
Dockerfile 스크립트의 VOLUME 인스트럭션과 docker container 명령의 --volume 플래그는 별개의 기능이다.
VOLUME 인스트럭션을 사용해 빌드된 이미지로 docker container run 명령에서 볼륨을 지정하지 않으면 항상 새로운 볼륨을 생성한다.
이렇게 생성 된 볼륨은 무작위 식별자를 가지므로, 컨테이너를 삭제한 후 같은 볼륨을 사용하려면 이 식별자를 미리 기억해야한다.
--volume 플래그는 이미지에 볼륨이 정의돼 있든 말든 상관없이 지정된 볼륨을 컨테이너에 마운트한다.
이미지에 볼륨이 지정돼 있더라도 이 정의가 무시되기에 새로운 볼륨이 생성되지 않는다.
이미지를 만드는 입장에선 안전장치로 VOLUME 인스트럭션을 이미지 정의에 포함시키는 것이 좋다.
이런 경우 사용자가 볼륨을 지정하지 않더라도 데이터가 유실 될 위험이 적다.(사용자 입장에선 별도로 볼륨을 만들어 붙이는 것이 좋음)
파일 시스템 마운트를 사용하는 컨테이너 실행하기
볼륨의 장점은 컨테이너와 스토리지의 생애주기를 분리하며 도커를 사용하는 방식 그대로 스토리지를 다룰 수 있다는 점이다.
호스트의 스토리지를 컨테이너에 직접적으로 연결하려면 바인드 마운트(bind mount)를 사용하면 된다.
바인드 마운트(bind mount)는 호스트 컴퓨터의 파일 시스템 디렉토리를 컨테이너 파일 시스템의 디렉토리로 만든다.
덕분에 도커를 사용할 때 컨테이너가 호스트 컴퓨터의 파일에 직접 접근할 수 있고 그 반대도 가능하다.
바인드 마운트를 사용하면 호스트 컴퓨터의 파일 시스템을 명시적으로 지정해 컨테이너 데이터로 쓸 수 있다.
호스트 컴퓨터의 로컬 디렉토리를 컨테이너에 바인드 마운트로 연결해보자.
먼저 파일 시스템 경로를 환경변수로 지정하자.(아래는 맥 / 리눅스 방식)
source="$(pwd)/databases" && target='/data'
mkdir ./databases
이후 호스트 컴퓨터에 생성한 디렉토리를 컨테이너에 바인드 마운트로 연결해 실행해보자.
docekr container run --mount type=bind,source=$source,target=$target -d -p 8012:80 diamol/ch06-todo-list
아래 명령으로 to-do 애플리케이션에 HTTP 요청을 보내 애플리케이션을 실행하자.
curl http://localhost:8012
databases 디렉토리에 db 파일이 생성됐는지 확인해보자.
ls ./databases
바인드 마운트는 양방향으로 동작한다.
컨테이너에서 만든 파일을 호스트 컴퓨터에서 수정할 수 있고, 호스트에서 만든 파일도 컨테이너에서 수정할 수 있다.
보안상 문제로 바인드 마운트를 사용하면 호스트 컴퓨터 파일에 접근하기 위해 권한 상승이 필요하다. 그래서 Dockefile 스크립트에 USER 인스트럭션을 사용해 컨테이너에 관리자 권한을 부여한다.(root 권한)
호스트 컴퓨터의 디렉토리를 읽기 전용으로 컨테이너에 연결할 수 있다.
이 방법은 호스트 컴퓨터에 작성한 설정을 컨테이너에 적용하기 위해 자주 쓰인다!
호스트 컴퓨터가 접근할 수 있는 스토리지라면 무엇이든 바인드 마운트를 통해 컨테이너에 연결할 수 있다.
분산 스토리지를 컨테이너에 연결해 유상태 애플리케이션을 사용하게 하면 신뢰성을 크게 향상시킬 수 있지만, 한계도 존재한다.
파일 시스템 마운트의 한계점
바인드 마운트와 볼륨을 잘 사용하려면 각 한계점을 알아야 한다.
이를 위해 2가지 예시를 볼 예정.
- 컨테이너 마운트 대상 디렉토리가 이미 존재하고 이미지 레이어에 이 디렉토리의 파일이 포함돼 있다면?
- 이미 존재하는 대상에 마운트하면 마운트의 원본 디렉토리가 기존 디렉토리를 완전히 대체함.
- 이미지에 포함돼 있던 원래 파일은 사용할 수 없게 됨.
- 호스트 컴퓨터의 파일 하나를 컨테이너에 이미 존재하는 디렉토리로 마운트하면 어떻게 될까?
- 디렉토리의 파일이 합쳐져 이미지에서 온 파일과 호스트에서 마운트된 파일이 모두 나타난다.
컨테이너의 파일 시스템은 어떻게 만들어지는가?
모든 컨테이너는 도커가 다양한 출처로부터 모아 만든 단일 가상 디스크로 구성된 파일 시스템을 갖는다.
-> 이러한 파일 시스템을 유니언 파일 시스템(Union File System)이라 한다.
컨테이너는 유니언 파일 시스템을 통해 물리적 위치가 서로 다른 파일과 디렉토리에 마치 단일 디스크를 사용하듯 접근할 수 있다.