[Docker] Persistent Storage : Docker volume
📕 목차
1. Container 속 데이터 유실
2. Docker volume을 사용하는 Container
3. File system mount를 사용하는 Container
4. File system mount 한계점
5. Container의 File system이 만들어지는 원리
6. 연습 문제
1. Container 속 데이터 유실
📌 Container 단일 디스크
- Docker Container에도 단일 드라이브로 된 file system이 있다.
- Image Layer를 순서대로 합쳐 만든 가상 파일 시스템이다.
- 모든 Container는 독립된 file system을 갖는다.
1️⃣ 같은 Image로 두 개의 Container 생성해서 실행해보기
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
cat .\number1.txt
cat .\number2.txt
- 두 Container는 모두 같은 경로에 파일을 생성했지만 내용이 다르다. (file system 독립)
- 즉, 같은 데이터베이스 엔진 Image로 실행된 두 Container는 서로 전혀 다른 데이터를 담을 수도 있다.
📌 기록 가능 레이어(Writeable Layer)
- Container의 file system은 Docker가 여러 출처로부터 합쳐서 만들어 Container에 전달한 단일 디스크(linux는 /dev/sda1, window는 C:/), 즉 가상 file system이다. (Image Layer + Writeable Layer)
- Image Layer
- 모든 Container가 공유
- 읽기 전용
- Image를 삭제할 때까지 Local PC의 Image Layer에 존재
- Writeable Layer
- Container 개별 관리
- Container와 같은 생명 주기 (종료 X, 삭제 시 소멸)
- 기록 중 복사(copy-on-write) 방식으로 읽기 전용인 Image Layer의 file을 수정할 수 있다.
- 디스크 공간 절약 : CoW 전략으로 여러 Container가 동일한 Image Layer 공유 가능하며, 중복 데이터를 줄이고 디스크 공간 절약
- 빠른 Container 생성 : 새로운 Conatiner 생성 시, 기존 Image Layer를 복사하지 않아도 된다.
- 변경 사항 격리 : 각 Container는 자체 writeable Layer를 가지므로, 변경사항이 외부에 영향을 주지 않는다.
- Image Layer
2️⃣ 컨테이너가 생성한 파일을 수정하고 재시작해보기
docker container run --name f1 diamol/ch06-file-display
echo "http://eltonstoneman.com" > url.txt
docker container cp url.txt f1:/input.txt
docker container start --attach f1
- 기존 출력값이 없어지고 덮어씌운 정보가 출력된다.
3️⃣ 새로운 컨테이너에서 수정된 데이터 삭제 확인
docker container run --name f2 diamol/ch06-file-display
docker container rm -f f1
docker container cp f1:/input.txt .
위 실행 결과를 토대로 2가지 사실을 알 수 있다.
- 동일한 Image로 Container를 실행하면 원래 파일 내용이 출력된다. 즉, f1 Container와 f2 Container는 독립적이다.
- 처음 만든 Container를 삭제하면 해당 Container의 기록 가능 Layer도 수정된 데이터와 함께 영구 삭제된다.
📌 Volume의 필요성
- 실무에서는 new Image를 build하고 old Conatiner를 삭제한 다음, new Image에서 실행한 Container로 대체하는 방법으로 애플리케이션을 업데이트한다.
- 이 과정에서 기존 Container에서 수정된 데이터는 모두 손실된다.
- Docker는 이런 상황을 위해 도커 볼륨(Docker volume)과 마운트(Mount)를 추가했다.
- 이 둘은 Container와 별개의 Life cycle을 갖는다.
2. Docker volume을 사용하는 Container
📌 Docker volume
- Docker에서 storage를 다루는 단위 (Container를 위한 USB)
- volume은 Container와 독립적이며, 별도의 생애 주기를 갖지만 연결할 수 있다.
- 즉, Stateless Container에서 Statefull Container를 관리할 수 있다.
- 사용 방법
- 수동으로 직접 volume을 생성해 Container에 연결
- Dockerfile에서 VOLUME Instruction 사용
- VOLUME <target-directory>
1️⃣ volume이 사용된 Mult-stage build Dockerfile
FROM diamol/dotnet-sdk AS builder
WORKDIR /src
COPY src/ToDoList.csproj .
RUN dotnet restore
COPY src/ .
RUN dotnet publish -c Release -o /out ToDoList.csproj
# app image
FROM diamol/dotnet-aspnet
WORKDIR /app
ENTRYPOINT ["dotnet", "ToDoList.dll"]
# set in the base image - `/data` for Linux, `C:\data` for Windows
VOLUME $SQLITE_DATA_DIRECTORY
# set in the base image - `root` for Linux, `ContainerAdministrator` for Windows
USER $SQLITE_USER
COPY --from=builder /out/ .
docker container run --name todo1 -d -p 8010:80 diamol/ch06-todo-list
docker container inspect --format '{{.Mounts}}' todo1
docker volume ls
- 두 번째 명령어 결과로 volume id, 호스트 컴퓨터상 경로, 연결된 container file system 경로 등을 확인할 수 있다.
- volumn은 Docker에서 Image, Container와 동급인 요소다.
2️⃣ 같은 Image로 생성한 Container에서 내용 확인하기
docker container run --name todo2 -d diamol/ch06-todo-list
docker container exec todo2 ls /data
docker container run -d --name t3 --volumes-from todo1 diamol/ch06-todo-list
docker container exec t3 ls /data
- todo2는 별도의 volume을 생성하기 때문에 /data 디렉토리가 비어있다.
- t3의 경우엔 todo1과 데이터베이스를 공유(--volumes-from)하므로 데이터가 들어가 있다.
- 하지만 file에 대해 여러 Container의 동시 접근을 허용하는 것은 위험하다.
💡 volume은 Container 간 파일 공유보다는 State 보존을 위한 목적으로 사용하라.
3️⃣ 버전 1의 to-do 애플리케이션 업데이트하기
$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
docker container rm -f todo-v1
docker container run -d -p 8011:80 -v todo-list:$target --name todo-v2 diamol/ch06-todo-list:v2
- -v(--volume) : Container의 file system 경로 /data에 지정한 volume을 mount (Image에 정의된 volume 무시)
- Volume은 Container보다 먼저 생성되어 자신과 연결됐었던 Container가 삭제된 뒤에도 그대로 남아있다.
- rm -f : 실행 중인 Container의 writeable Layer까지 삭제하지만 volume은 삭제되지 않는다.
- 즉, Volume은 Container와 별도의 Life-cycle을 가지며, Application을 업데이트 하더라도 데이터를 보존할 수 있다.
- 작성자는 안전장치로 Dockerfile에 VOLUME Instruction을 마련하고, 사용자도 volume 설정에 의존하지 않고 별도의 이름을 붙여 만든 volume을 사용하는 것이 좋다.
3. File system mount를 사용하는 Container
📌 바인드 마운트(bind mount)
- volume보다도 Host의 storage를 Conatiner에 직접적으로 연결한다.
- Host PC file system의 디렉토리를 Container file system의 디렉토리로 만든다.
- Container와 Host가 서로 간의 파일에 직접 접근이 가능해진다. (양방향)
- Host PC에서 접근 가능한 파일 시스템이면 무엇이든 Container에서도 사용 가능하다.
- SSD 디스크, 고사용성 디스크 어레이, 네트워크 상의 분산 storage
1️⃣ Host PC의 local directory를 Container에 bind mount로 연결하기
$source="$(pwd)/databases".ToLower(); $target="/data"
mkdir databases
docker container run --mount type=bind,source=$source,target=$target -d -p 8012:80 diamol/ch06-todo-list
curl.exe http://localhost:8012
ls .\databases\
해당 애플리케이션은 HTTP request를 받으면 데이터베이스 파일을 생성하고 시작한다.
- Container가 생성한 파일을 Host PC에서 직접 사용하거나, 다른 파일을 추가해서 Container에서 사용할 수도 있다.
📌 권한
- bind mount는 양방향으로 작동한다.
- Host PC에 접근하기 위해, Dockerfile에서 USER Instruction을 사용해 관리자 권한(root)을 부여한다.
- 대개 Container의 Host PC 공격을 막기 위해 최소 권한을 가진 계정으로 실행된다.
- Container가 Host PC의 file에 쓰기 작업을 할 필요가 없다면, Host PC 디렉터리를 읽기 전용으로 Container에 연결할 수 있다.
- Host PC에서 작성한 설정을 Container에 적용할 때 자주 쓰인다.
2️⃣ Container가 읽기 전용 디렉터리에서 설정값 바꾸게 하기
$source="$(pwd)/config".ToLower(); $target="/app/config"
docker container run --name todo-configured -d -p 8013:80 --mount type=bind,source=$source,target=$target,readonly diamol/ch06-todo-list
curl.exe http://localhost:8013
docker container logs todo-configured
이전 to-do 애플리케이션은 로그 출력을 최소한으로 줄이도록 설정되어 있었지만, /app/config 경로가 존재할 경우 해당 디렉터리에서 추가 설정 파일을 로드한다.
Host PC에 위치한 설정 파일에는 좀 더 상세한 내용까지 로그를 출력하도록 설정되어 있고, Container가 해당 설정 파일을 읽어 로그 설정을 수정한다.
✒️ Docker volume과 mount 차이점 정리
Docker Volume은 Docker에 의해 만들어지고 관리되는 객체다.
주로 Host의 /var/lib/docker/volumes/ 에 저장되며, Container에 mount하지 않아도 미리 생성이 가능하다.
보통 DB같은 데이터들을 저장할 때 많이 사용한다.
• docker volume create (볼륨명)
• docker volume inspect (볼룸명)
• docker run -v (볼륨명):(공유할 Container 경로) (Image 경로)
• docker container inspect --format '{{.Mounts}}' (Container 이름)
• docker volume ls
Docker volume을 사용하지 않고 Host 디렉토리에 직접 access한다.
Container가 지워지더라도 Host에 파일이 남아 데이터를 보존할 수 있다.
보안에 취약하다는 단점이 있다.
docker container run --mount type=bind,source=$source,target=$target (Container 이름)
4. File system mount 한계점
📌 Scenario 1
🤔 Container의 mount 대상 디렉토리가 이미 존재하고 Image Layer에 해당 디렉토리의 file이 포함돼 있는 경우
- 이미 존재하는 대상 디렉토리에 mount하면, 기존 디렉토리를 완전히 대체한다.
- 따라서, Image에 포함되어 있던 원래 file은 사용할 수 없다.
$source="$(pwd)/new".ToLower(); $target="/init"
docker container run diamol/ch06-bind-mount
docker container run --mount type=bind,source=$source,target=$target,readonly diamol/ch06-bind-mount
Image Layer에서 받은 file이 mount file로 대체되어 결과가 바뀌었다.
📌 Scenario 2
🤔 Host PC의 file 하나를 Container에 이미 존재하는 디렉터리로 mount하는 경우
- 디렉터리 file이 합쳐져 Image에서 온 file과 Host에서 mount된 file이 모두 나타난다.
- 단, window Container에서는 해당 기능을 제공하지 않아 동작이 달라진다.
- Linux에서는 file를 mount 할 수 있지만, Linux는 디렉토리만을 mount 할 수 있다.
- Container file system은 Window Container와 Linux Container의 동작이 일치하지 않는 몇 안 되는 영역 중 하나다.
- 이번 챕터에서 경로 문자열을 OS 별로 다르게 환경 변수로 정의하는 것도 같은 이유다.
📌 Scenario 3. 매우 드물고 복잡한 경우
🤔 분산 file system을 Container에 bind-mount 하는 경우
- 매우 드물지만, 한 번 발생하면 회피할 도리가 없는 케이스다.
- 분산 file system 메커니즘은 Local PC OS의 file system과 다른 경우가 많아, 지원을 하지 않는 동작이 있을 수 있다.
- bind-mount의 original storage가 Container의 모든 file system을 제공하지 않을 수 있다.
- 이 사실은 애플리케이션을 실행해 보기 전까지는 미리 파악할 수도 없다.
- Container에 분산 storage를 mount 할 거라면, 성능 측면에서도 Local storage와 큰 차이가 있다는 것을 고려하라.
- 디스크를 많이 사용하는 Application이 모든 file 입출력을 위해 Network를 거쳐야 한다면 Application이 멈출 수도 있다.
✒️ 분산 파일 시스템(Distributed File System)
Client가 자신의 컴퓨터에 있는 것처럼 Server에 저장된 데이터에 access하고 처리할 수 있는 Client-Server 기반의 Application이다.
Client가 Server의 file에 접근하면 Server는 Client에게 file의 copy를 전송하고, 해당 copy는 데이터가 처리되는 동안 Client의 컴퓨터에 cache된 후 Server로 반환된다.
Hadoop의 경우 DFS는 하나의 file을 3개의 file로 만들어 3군데 저장한다. (이렇게 하면 어떠한 일이 있더라도 데이터가 유실되지 않는다는 것이 수학적으로 증명되었기 때문이다.)
5. Container의 File system이 만들어지는 원리
📌 유니언 파일 시스템(Union File System)
- Docker가 다양한 출처로부터 모아 만든 단일 가상 디스크로 구성된 file system (모든 Container가 갖는다.)
- OS마다 다른 방식으로 구현되어 있다.
- Docker가 알아서 Host OS에 맞게 최선의 구현을 선택하므로, 상세한 구현은 무시해도 된다.
- Container는 Union File System으로 물리적 위치가 서로 다른 file과 directory에 단일 디스크처럼 접근 가능하다.
- 기록 가능 Layer는 하나밖에 가질 수 없으나, 여러 개의 Image Layer, 하나 이상의 volume mount와 bind mount를 Container에 연결할 수 있다.
📌 Container Storage 구성할 때 고려할 점
- writeable Layer
- 단기 저장에 적합하다.
- 비용이 비싼 계산, Network를 통해 저장해야 하는 데이터 캐싱 등
- Container 삭제 시 유실되도 무관한 데이터 위주
- 로컬 bind-mount
- Host PC와 Container 간 데이터 공유
- 개발자 PC의 소스 코드를 전달할 때, Local PC에서 Image build 없이도 수정 사항이 Container로 전달된다.
- 분산 bind-mount
- Network Storage와 Container 간에 데이터 공유
- 가용성은 높지만 리스크가 크다.
- 읽기 전용으로 설정 파일 전달, 공유 캐시로 활용, 읽기 쓰기 가능으로 데이터를 저장해 동일 Network 상의 모든 Container나 PC와 데이터를 공유하는데 적합하다.
- volume mount
- Container와 Volume(Docker 객체) 간 데이터 공유
- Application이 volume에 데이터를 영구 저장한다.
- Container를 교체 하는 방식으로 Application을 업데이트해도, 이전 버전 Container 데이터 유지가 가능하다.
- Image Layer
- Container 초기 file system 구성
- 후속 Layer와 이전 Layer 충돌 시, 후속 Layer가 데이터를 덮어 쓴다.
- Layer는 읽기 전용이며 여러 Container가 공유한다.
6. 연습 문제
- to-do Application을 Container로 실행하되 미리 등록된 할 일이 없는 상태로 Application이 시작되도록 Storage를 설정하라.
- docker rm -f $(docker ps -aq)로 모든 컨테이너 삭제
- diamol/ch06-lab Image로 Container 실행 후 현재 등록된 할 일 확인 (이때 mount를 추가해 Container 실행)
대충 Container 삭제하고 설정 파일을 열어보니 이렇게 되어 있다.
{
"ConnectionStrings": {
"ToDoDb": "Filename=/new-data/todo-list.db"
}
}
todo-list.db에 접근하려면 new-data에 한 번 더 접근해야 할 것 같다.
그리고 원래는 db를 안 뒤지는데, 설정값 바꿔주면 해당 volume 경로로 db를 뒤지는 것 같으니 Container 실행 시 설정값을 줘야 한다.
docker container run -d -p 8000:80 diamol/ch06-lab
일단 기존 거 확인하라니까 한 번 확인해주고 시작하자.
docker volume create ch06-lab
$source="$(pwd)/solution".ToLower(); $target="/app/config"
volume 적당히 만들어주고, solution 디렉토리에 있는 설정 파일을 넘겨줘야 하니 해당 디렉토리를 source로 잡았다.
target의 경우엔 /app/config로 잡았다.
docker container run -d -p 8001:80 --mount type=bind,source=$source,target=$target,readonly --volume ch06-lab:/new-data diamol/ch06-lab
ch06-lab이라는 volume에서 /new-data 디렉토리를 Container에 연결하고, 당연히 읽기 전용으로 했다.
설정값이 db로부터 값을 읽어오도록 바뀌어서 빈 페이지가 출력된다!