DevOps/Docker & Kubernetes

[Docker] Persistent Storage : Docker volume

나죽못고나강뿐 2023. 7. 14. 19:29
📕 목차

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)

 

https://subscription.packtpub.com/book/application-development/9781788992329/1/ch01lvl1sec05/understanding-docker-images-and-layers

  • 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을 수정할 수 있다.
        1. 디스크 공간 절약 : CoW 전략으로 여러 Container가 동일한 Image Layer 공유 가능하며, 중복 데이터를 줄이고 디스크 공간 절약
        2. 빠른 Container 생성 : 새로운 Conatiner 생성 시, 기존 Image Layer를 복사하지 않아도 된다.
        3. 변경 사항 격리 : 각 Container는 자체 writeable 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를 관리할 수 있다.
  • 사용 방법
    1. 수동으로 직접 volume을 생성해 Container에 연결
    2. 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에 저장된다.

  • 두 번째 명령어 결과로 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 차이점 정리

(좌) volume / (우) bind-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로부터 값을 읽어오도록 바뀌어서 빈 페이지가 출력된다!