퍼시스턴드(스)란?- 데이터의 영속성
컨테이너는 무상태 애플리케이션에게는 최적의 실행 환경이다.
사용량이 증가하더라도 클러스터에 실행 중인 컨테이너의 수를 늘리기만 하면, 모든 요청이 똑같이 처리된다.
또 롤링 업데이트를 통해 서비스 중단 없이 점진적으로 업데이트를 배포할 수도 있다.
그러나 어플리케이션에 상태가 전혀 없을 수는 없다. 퍼시스턴시나 성능 향상을 위해 디스크를 사용하는 컴포넌트가 있어야하고, 이 컴포넌트 역시 컨테이너에서 실행된다.
스토리지 관리되면 복잡한 상황이 많다.
도커 컨테이너에도 단일 드라이브로 된 파일 시스템이 있다. 이 파일 시스템의 내용은 이미지속 파일로 부터 만들어 진다.
Dockerfile 스크립트에서 COPY 인스트럭션을 사용해 파일을 이미지로 복사하면, 이 이미지로 실행한 컨테이너에도 같은 경로에 복사된 파일이 있다.
컨테이너의 디스크 는 이미지 레이어를 순서대로 합쳐 만든 가상의 파일 시스템이다.
모든 컨테이너는 독립된 파일 시스템을 갖는다.
같은 이미지에서 실행한 여러 개의 컨테이너는 처음에는 디스크의 내용이 모두 같지만, 그중 한 컨테이너에서 애플리케이션이 파일을 수정해도 다른 컨테이너나 이미지는 영향을 받지 않는다.
//컨테이너 두개를 실행하는데 이 이미지에 담긴 애플리케이션은 컨테이너 속 파일에 무작위 숫자를 쓰는 기능을 한다.
docker container run --name rn1 diamol/ch06-random-number
docker container run --name rn2 diamol/ch06-random-number
컨테이너를 실행하면 텍스트 파일에 무작위 숫자를 쓰는 스키립트가 실행된다. 그리고 컨테이너를 종료하면 Exited 상태가 된다. 이 두 컨테이너는 같은 이미지로 실행됐으나 파일시스템의 내용이 서로 다르다.
컨테이너를 종료해도 파일 시스템은 삭제되지 않는다. 그러므로 컨테이너의 파일과 디텍터리는 그대로 남아있다.
// docker container cp 명령을 사용해 두 컨테이너에서 무작위 숫자가 쓰인 텍스트 파일을 로컬 컴퓨터로 복사해 온 다음, 파일의 내용을 확인한다.
// 두 컨테이너로 부터 /random/number.txt 파일을 로컬 컴퓨터로 복사한다. 이 파일은 컨테이너에서 생성한 것으로 서로 내용이 다르다.
docker container cp rn1:/random/number.txt number1.txt
docker container cp rn2:/random/number.txt number2.txt
// 두 컨테이너에서 복사해 온 파일의 내용을 확인한다 실제로 파일의 내용이 서로 다르다.
cat number1.txt
cat number2.txt
기록 가능 레이어를 새 파일을 만드는 데만 사용하는 것은 아니다. 기존 이미지 레이어에 있는 파일을 수정할 수도 있다.
도커는 기록 중 복사 라는 방법을 사용해 읽기 전용 레이어의 파일을 수정할 수 있다.
컨테이너에서 이미지 레이어에 포함된 파일을 수정하려면!
먼저 도커가 이 파일을 쓰기 가능 레이어로 복사해 온 다음 쓰기 가능 레이어에서 파일을 수정한다.
컨테이너를 실행해 파일의 내용을 출력한다. 그다음 파일의 내용을 수정하고 컨테이너를 재시작해 변경된 파일 내용을 확인한다.
// 컨테이너가 실행되면 input.txt 파일의 내용을 출력한다. 지금 출력된 내용은 이미지에 포함된 input.txt 파일의 내용이다.
docker container run --name f1 diamol/ch06-file-display
// 이 명령을 실행하면 로컬 컴퓨터에 다른 내용이 들어 있는 새로운 파일이 생성된다. 그리고 컨테이너에서 이 파일이 원래 있던 input.txt 파일을 덮어씌운것 처럼 사용된다.
echo "https://eltonstoneman.com" > url.txt
docker container cp url.txt f1:/input.txt
// 컨테이너가 첫 번째 실행을 마치고 종료됐다. 이 컨테이너를 다시 시작하면 똑같은 명령이 실행되지만 파일의 내용이 변경된 상태다.
docker container start --attach f1
컨테이너 속 파일을 수정하면 컨테이너의 동작에 영향을 미친다.
그러나 이미지를 공유하는 다른 컨테이너나 이미지는 영향을 받지 않는다. 수정된 파일은 해당 컨테이너의 기록 가능 레이어에만 존재하기 때문이다. 새로운 컨테이너는 이미지로부터 받은 최초의 내용을 담은 파일시스템을 가지며 f1 컨테이너가 삭제되면 수정된 파일도 삭제된다.
새 컨테이너를 실행해 해당 파일의 내용이 그대로인지 확인해 보자. 그리고 처음 실행했던 컨테이너를 삭제하고 수정된 데이터가 사라진 것을 확인하라.
// 같은 이미지로 새 컨테이너를 실행하면 원래 파일 내용이 출력된다. f1 컨테이너의 파일을 수정해도 이미지에는 영향을 미치지 않았다.
docker container run --name f2 diamol/ch06-file-display
//처음 만든 컨테이너를 삭제하면 이 컨테이너의 기록 가능 레이어도 수정된 데이터와 함께 삭제된다. 컨테이너의 파일 시스템은 일시적인 것이다.
docker container rm -f f1
docker container cp f1:/input.txt .
컨테이너 파일 시스템은 컨테이너와 같은 생애주기를 갖는다. 컨테이너가 삭제되면 이 컨테이너의 기록 가능 레이어와 여기서 수정된 데이터도 함께 삭제된다.
실무에서는 새 이미지를 빌드하고 오래된 컨테이너를 삭제한 다음 새 이미지에서 실행한 컨테이너로 대체하는 방법으로 애플리케이션을 업데이트한다. 이 과정에서 기존 컨테이너에 있는 수정된 데이터는 모두 손실된다. 새 컨테이너는 이미지에서 받은 파일만 갖고 있기 때문이다. => 도커 볼륨과 마운트! 이들 요소는 컨테이너와는 별개의 생애주기를 갖는다. 그러므로 컨테이너가 대체돼도 지속돼야 할 데이터를 저장할 수 있다.
컨테이너에서 볼륨을 사용하는 방법은 두가지다.
1. 수동으로 직접 볼륨을 생성해 컨테이너에 연결한다.
2. Dockerfile 스크립트에서 VOLUME 인스트럭션을 사용하는 방법이다. 이 인스트럭션을 사용해 만든 이미지로 컨테이너를 실행하면 자동으로 불륨을 생성한다.
VOLUM 인스트럭션의 문법은 VOLUME target-directory 형식이다.
아래 예제는 diamol/ch06-todo-list 이미지를 빌드하기 위한 멀티 스테이지 빌드 Dockerfile 스크립트의 일부다. 이 애플리케이션은 볼륨을 사용하는 유상태 어플리케이션이다.
// 볼륨이 사용된 멀티 스테이지 빌드 Dockerfile 스크립트의 일부
FROM diamol/dotnet-aspnet
WORKDIR /app
ENTRYPOIN ["dotnet", "ToDoList.dll"]
VOLUME /data
COPY --from=builder /out/ .
이 이미지로 부터 컨테이너를 실행하면 자동으로 볼륨을 생성해 컨테이너에 연결해준다.
실행된 컨테이너에는 /data 디렉터리가 있는데(윈도 컨테이너라면 C:\data). 이 디렉터리는 다른 디렉터리와 똑같이 사용할 수 있는데 이 데렉터리의 내용은 볼륨에 영구적으로 저장된다.
// todo-list 애플리케이션 이미지로 컨테이너를 실행해 컨테이너와 연결된 볼륨을 살펴보자.
// container run 명령에는 볼륨이 언급되지 않았지만, Dockerfile 스크립트의 정의에 따라 볼륨을 생성해 컨테이너에 연결한다.
docker container run --name todo1 -d -p 8010:80 diamol/ch06-todo-list
// 컨테이너 정보를 확인하면 'mounts' 항목에서 볼륨 정보를 볼 수 있다. 불륨 ID와 해당 볼륨의 호스트 컴퓨터상 결로, 볼륨이 연결된 컨테이너 파일 시스템의 경로 등을 확인할 수 있다.
docker container inspect --format '{{.Mounts}}' todo1
// 볼륨은 도커에서 이미지나 컨테이너와 동급인 요소다. docker volume 명령을 사용해 볼륨을 만들고 목록을 확인하고 삭제할 수 있다. 이 볼륨은 새로 만든 컨테이너에 연결하기 위해 만들어진 것이다.
docker volume ls
웹브라우저에서 https://localhost:8018에 접근하면 to-do 애플리케이션을 볼 수 있다. 이 애플리케이션은 데이터를 /data 디렉터리에 저장하는데, 웹 페이지에서 할일을 하나 추가하면 이 데이터는 도커 볼륨에 저장된다.
// to-do 애플리케이션의 두 번째 컨테이너를 실행하고 data 디렉터리의 내용을 확인해보자. 그 다음에는 이 컨테이너와 데이터를 공유하는 첫 번째 컨테이너의 해당 디렉터리와 내용을 비교하자.
// 이 컨테이너는 새로운 볼륨과 함께 실행된다.
docker container run --name todo2 -d diamol/ch06-todo-list
// 볼륨에는 아무것도 없으므로 /data 디렉터리가 비어있다.
docker container exec todo2 ls /data
//이 컨테이너는 todo1 컨테이너의 볼륨을 공유한다. /data 디렉터리의 내용도 같다.
docker container run -d --name t3 --volumes-from todo1 diamol/ch06-todo-list
// todo1 컨테이너와 볼륨을 공유하므로 /data 디렉터리에 애플리케이션에서 추가된 데이터가 보인다.
docker container exec t3 ls /data
컨테이너간 볼륨 공유는 모든일을 해결해주지는 못한다. 애플리케이션 컨테이너는 종종 자신만이 접근 할 수 있는 파일을 필요로 한다.
이러한 파일을 다른 컨테이너가 동시에 접근하게 허용하면 어플리케이션이 비 정상적으로 동작할 수도 있다.
볼륨은 컨테이너 간 파일 공유보다는 업데이트 간 상태를 보존하기 위한 용도로 사용해야한다.
이미지에 정의하는 것보다는 명시적으로 관리하는 편이 더 낫다.
볼륨에 이름을 붙여 생성하고 업데이트 시 다른 컨테이너로 옮겨 연결하면 된다.
볼륨을 생성하고 버전 1의 to-do 애플리케이션에서 볼륨을 사용하라. 그다음 애플리케이션에서 UI를 통해 데이터를 추가하고, 애플리케이션을 버전 2로 업데이트 해 보자. 운영체제에 따라 팡리 경로가 달라지므로 환경 변수로 먼저 정의해 본문의 코드를 쉽게 붙여 넣을 수 있다.
# 복사 대상 결로를 환경 변수로 정의한다.
target='/data'
# 데이터를 저장할 볼륨도 생성한다
// 새로운 볼륨을 이름을 붙여 만든다. 지금은 그냥 빈 스토리지다.
docker volume create todo-list
# 볼륨을 연결해 v1 애플리케이션을 실행한다.
// -v 플래그를 사용해 새 컨테이너를 실행한다. 이 플래그는 컨테이너의 파일 시스템 경로 /data에 지정한 볼륨을 마운트 하라는 의미다.
docker container run -d -p 8011:80 -v todo-list:/data --name todo-v1 diamol/ch06-todo-list
# http://localhost:8011 페이지에서 애플리케이션에 데이터를 몇 건 추가한다.
# v1 애플리케이션이 실행 중인 컨테이너를 삭제한다
// -f 플래그를 붙이면 컨테이너가 실행되고 있더라도 컨테이너를 삭제할 수 있다. 이 과정에서 컨테이너의 기록가능 레이어가 함께 삭제 되지만 볼륨은 삭제되지 않는다.
docker container rm -f todo-v1
# 그 다음에는 같은 볼륨을 사용하도록 v2 애플리케이션 컨테이너를 실행한다.
// 업데이트된 이미지로 새로운 컨테이너를 실행하고, 이 컨테이너에 기존 볼륨을 같은 경로로 연결한다.
그러면 v2 애플리케이션은 v1에서 생성했던 데이터를 그대로 유지한다.
docker container run -d -p 8011:80 -v todo-list:/data --name todo-v2 diamol/ch06-todo-list:v2
볼륨은 컨테이너보다 먼저 생성돼 자신과 연결됐던 컨테이너가 삭제된 뒤에도 그대로 남아있다. 이로써 볼륨이 컨테이너와 별개의 생애주기를 가졌다는 것을 알 수 있다. 볼륨을 사용하면 컨테이너를 교체해 애플리케이션을 업데이트 하더라도 데이터를 보존할 수 있다.
Dockerfile 스크립트의 VOLUME 인스트럭션과 docker container 명령의 --volume 플래그는 별개기능이다.
VOLUME 인스트럭션을 사용해 빌드된 이미지로 docker container run 명령에서 볼륨을 지정하지 않으면 항상 새로운 볼륨을 함께 생성한다.
이 볼륨은 무작위로 만들어진 식별자를 가지므로, 컨테이너를 삭제한 후 볼륨을 재사용하려면 이 식별자를 미리 기억해야한다.
--volume 플래그는 이미지에 볼륨이 정의돼 있든 말든 지정된 볼륨을 컨테이너에 마운트 한다. 이미지에 볼륨이 정의돼 있더라도 이 정의가 무시되므로 새로운 볼륨이 생성되지 않는다.
지금 우리가 to-do 애플리케이션을 업데이트한 과정이 바로 이 경우다.
이미지에 볼륨이 정의돼 있지 않아도 똑같은 결과를 얻는다. 이미지를 만드는 입장에서 안전장치 삼아 VOLUME 인스트럭션 이미지 정의 에 포함해 두는게 좋다. 그러면 사용자가 볼륨을 지정하지 않더라도 데이터를 유실할 일이없다.
이미지의 기본 볼륨 설정에 의존하지 않고 별도로 이름을 붙여 만든 볼륨을 사용하는 것이 좋다.
// 호스트 컴퓨터의 로컬 디렉터리를 컨테이너에 바인드 마운트로 연결해 보라. 파일 시스템 경로는 호스트 운영체제의 방식을 따라야한다.
// 본문의 명령을 그대로 복사해 사용할 수 있도록 경로를 환경 변수로 설정한다.
source="$(pwd)/databases" && target='/data'
// 바인드 마운트를 적용해 컨테이너를 실행. 호스트 컴퓨터의 data 디렉터리를 컨테이너 안에서 사용한다.
mkdir ./databases
docker container run --mount type=bind,source=$source,target=$target -d -p 8012:80 diamol/ch06-todo-list
// to-do 애플리케이션에 HTTP 요청을 보낸다. 이 요청을 받아 애플리케이션이 시작되며 데이터베이스 파일이 생성된다.
// 컨테이너 포트 80번을 호스트의 8012번 포트로 공개한다. curl 명령으로 컨테이너에 HTTP 요청을 보내면 애플리케이션이 시작되면서 data 에 데이터 베이스 파일이 생성된다.
curl http://localhost:8012
// 호스트 컴퓨터에 연결된 데렉터리에 데이터 베이스 파일이 생성된것을 확인하는 명령어 이다.
ls ./databases
이 파일이 컨테이너에서 생성한 파일이다.
호스트 컴퓨터에서 이 파일을 직접 사용하거나 다른 파일을 추가할 수 있다. 새로 만든 파일도 컨테이너에서 사용할 수 있다.
// to-do 애플리케이션은 /app/config 경로가 존재할 경우 이 디렉터리에서 추가 설정파일을 로드한다. 호스트 컴퓨터의 디렉터리를 이 경로에 연결하도록 바인드 마운트를 적용한 컨테이너를 실행해 애플리케이션이 호스트 컴퓨터에 있는 설정 파일을 사용하도록 하라. 이 책 소스 코드의 압축을 푼 디렉터리까지 이동한 다음, 아래 명령을 입력한다.
source="$(pwd)/config" && 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 http://localhost:8013
호스트 컴퓨터에 위치한 설정 파일에는 좀 더 상세한 내용까지 로그를 출력하도록 설정돼 있다.
컨테이너를 실행하면 호스트 컨테이너의 디렉터리가 연결되고 애플리케이션이 설정파일 디렉터리를 발견해 안에 있는 로그 설정을 읽어들인다. 많은 양의 debug 레벨 로그가 출력된 것으로 보아 설정이 변경됐음을 알 수 있다.
호스트 컴퓨터가 접근할 수 있는 스터리지라면 무엇이든 바인드 마은트를 통해 컨테이너에 연결할 수 있다.
바인드 마운트와 볼륨을 효율적으로 활용하려면 각 요소의 핵심 사용 시나리오와 한계점을 이해 해야한다.
컨테이너의 마운트 대상 디렉터리가 이미 존재하고 이미지 레이어에 이 데렉터리의 파일이 포함돼 있다면 어떻게 될까?
이미 존재하는 대상 디렉터리에 마운트하면 마운트의 원본 디렉터리가 기존 디렉터리를 완전히 대체한다. 그래서 이미지에 포함돼 있던 원래 파일을 사용할 수 없다.
디렉터리의 목록을 출력하는 간단한 컨테이너를 실행해 직접 확인해보자.
// 마운트가 없는 컨테이너를 실행해 이미지에서 받은 파일 목록을 확인하라. 그 다음 마운트를 지정해 컨테이너를 다시 실행하고 마운트 원본 디렉터리의 파일 목록이 출력되는지 확인하라.
cd ./ch06/exercises/bind-mount
source="$(pwd)/new" && target='/init'
//컨테이너를 실행하면 /init 디렉터리의 파일 목록을 출력한다. 이 컨테이너는 바인드 마운트가 마운트되지 않았으므로 이미지에 포함된 기존 파일의 목록이 출력된다.
docker container run diamol/ch06-bind-mount
처음 실행한 컨테이너는 두 개의 파일명 abc.txt와 def.txt를 출력했다.
이들 파일은 이미지 레이어에서 컨테이너로 전달된 파일이다.
// /init를 대상으로 바인드 마운트를 마운트해 컨테이너를 실행하면 원래 디렉터리의 내용은 숨겨지고 바인드 마운트의 원본 디렉터리가 이를 대체한다.
docker container run --mount type=bind,source=$source,target=$target diamol/ch06-bind-mount
두번째 컨테이너는 이미지 레이어에서 받은 파일이 마운트된 파일로 대체 됐으므로 파일 목록이 123.txt, 456.txt로 바뀌었다.
두 번째 시나리오는 이와는 조금 다르다.
호스트 컴퓨터의 파일 하나를 컨테이너에 이미 존재하는 디렉터리로 마운트 하면 어떻게 될까?
이번에는 디렉터리의 파일이 합쳐져 이미지에서 온 파일과 호스트에서 마운트된 파일이 모두 나타난다.(단, 윈도 컨테이너는 이 기능을 제공하지 않아 동작이 달라짐)
컨테이너 파일 시스템은 윈도 컨테이너와 리눅스 컨테이너의 동작이 일치하지 않는다. (동일하게 동작하는 경우가 없는것은 아님)
단일 파일 마운트의 한계점은 이보다 더 명확하다. 윈도와 리눅스 컴퓨터를 모두 갖고 있거나 리눅스 컨테이너와 윈도 컨테이너를 모두 지원하는 윈도 버전 도커 데스크톱에서 확인할 수 있다.
리눅스 컨테이너에는 단일 파일 마운트 기능을 사용할 수 있다. 대상 디렉터리가 컨테이너에 이미 존재한다면 마운트되는 디렉터리와 내용이 합쳐진다.
세 번째 시나리오는 다른 시나리오에 비해 드물다.
분산파일 시스템을 사용하면 네트워크상의 모든 컴퓨터에서 데이터에 접근할 수 있지만, 대개 분산 파일 시스템의 메커니즘은 윈도 파일 공유에 쓰이는 SMB, 파일스 , AWS, s3 등 로컬 컴퓨터의 운영체제의 파일시스템과 다른 경우가 많다. 이런한 분산 파일 스토리지를 컨테이너에 마운트하면 일반적인 파일 시스템의 일부처럼 보이지만 지원하지 않는 동작이 있을 수 있다.
컨테이너에 분산 스토리지를 마운트할 계획이라면, 이런 위험과 함께 분산 스토리지의 성능이 로컬 스토리지와 큰 차이가 있다는 것도 고려해야한다.
모든 컨테이너는 도커가 다양한 출처로부터 모아 만든 단일 가상 디스크로 구성된 파일시스템을 갖는다. 이 파일 시스템을 유니언 파일 시스템이라고한다. 도커를 설치하면 우리가 사용하는 운영체제에 맞춰 최선의 구현을 선택해 주기 때문에 상세한 구현은 신경쓸 필요가 없다.
컨테이너는 유니언 파일 시스템을 통해 물리적 위치가 서로 다른 파일과 디렉터리에 마치 단일 디스크를 사용하듯 접근할 수 있다.
컨테이너이세 실행되는 애플리케이션의 입장에서는 단일 디스크만을 볼 수 있지만 컨테이너나 이미지를 생성해 사용하는 사용자는 여러 출처를 합쳐 이 디스크를 구성 할 수 있다.
여러 개의 이미지 레이어, 역시 하나 이상의 볼륨 마운트와 바인드 마운트를 컨테이너에 연결 할 수 있다.
그러나 기록 가능 레이어는 하나밖에 가질 수 없다.
컨테이너 스토리지를 구성할 때 고려해야할 일반론
기록 가능 레이어: 비용이 비싼 계산이나 네트워크를 통해 저장해야 하는 데이터의 캐싱 등 단기 저장에 적합하다. 각 컨테이너마다 독립적인 기록 가능 레이어를 갖지만, 컨테이너가 삭제되면 여기 저장된 데이터는 유실된다.
로컬 바인드 마운트: 호스트 컴퓨터와 컨테이너 간 데이터를 공유하기 위해 사용한다. 개발자의 로컬 컴퓨터에서 컨테이너로 소스 코드를 전달하기 위해 사용하면 로컬 컴퓨터에서 수정한 내용이 이미지 빌드 없이도 즉시 컨테이너로 전달 될 수 있다.
분산 바인드 마운트: 네트워크 스토리지와 컨테이너 간에 데이터를 공유하기 위해 사용한다.
가용성이 높지만 로컬 디스크와 비교해 지원하지 않는 파일 시스템 기능이 있거나 성능 면에서 차이가 있을 수 있다.
읽기 전용으로 설정 파일을 전달하거나 공유 캐시로 활용할 수 있으며 읽기 쓰기 가능으로 데이터를 저장해 동일 네트워크 상의 모든 컨테이너나 컴퓨터와 데이터를 공유하는 데 적합하다.
볼륨 마운트: 컨테이너와 도커 객체인 볼륨 간에 데이터를 공유하기 위해 사용된다. 볼륨 마운트를 사용하면 애플리케이션이 볼륨에 데이터를 영구적으로 저장한다. 컨테이너를 교체하는 방식으로 애플리케이션을 업데이트해도, 이전 버전 컨테이너의 데이터를 그대로 유지할 수 있다.
이미지 레이어: 이미지 레이어는 컨테이너의 초기 파일 시스템을 구성한다. 레이어는 적층 구조를 갖는데, 후속 레이어와 이전 레이어의 내용이 서로 충돌하는 경우 후속 레이어의 내용이 적용된다. 레이어는 읽기 전용이며 여러 컨테이너가 공유한다.