본문 바로가기

DevOps & Infra/Docker

[Infrastructure/Docker] 튜토리얼 살펴보기#005. 파일 시스템의 이해

파일 시스템의 이해

튜토리얼의 Node.js 애플리케이션은 컨테이너를 닫고, 다시 시작할 때마다 기존에 추가했던 할 일(Todo) 목록이 초기화되고 있습니다. 컨테이너가 매 번 초기화되는 이유가 무엇일까요? 우선, 컨테이너의 동작 방식에 대해서 이해해야 합니다.

컨테이너는 파일 시스템을 기반으로 동작합니다. 컨테이너가 실행될 때, 이미지로부터 읽어 온 여러 개의 레이어를 사용하여 파일 시스템을 형성합니다. 각 컨테이너에는 파일을 생성하고, 수정하고, 제거할 수 있는 고유한 공간-스크래치 공간(Scatch space)이 존재합니다. 따라서 같은 이미지를 사용하는 컨테이너 간에는 파일의 변경 사항을 공유하지 않습니다.

파일 시스템 테스트

두 개의 컨테이너가 고유의 스크래치 공간을 사용하는 것을 테스트해보겠습니다. 다음 명령문을 사용하여 우분투 컨테이너를 시작하고, /data.txt 이름의 파일을 생성합니다.텍스트 파일에는 1~10000 사이의 난수를 기록합니다.

$ docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"

도커 레지스트리에서 이미지를 Pull하고, 컨테이너가 실행되었다면 /data.txt 파일을 확인 할 수 있습니다. 도커 데스크탑을 열어 실행 컨테이너의 CLI에 입장합니다.

컨테이너의 셸이 실행되면, 다음 명령문을 입력하여 파일의 내용을 읽어옵니다. 파일에 기록된 난수를 확인 할 수 있습니다.

$ cat /data.txt
6232

도커 데스크탑을 사용하지 않더라도,  다음 명령문을 사용하여 동일한 작업을 원격으로 요청 할 수 있습니다.

$ docker exec [컨테이너 ID] [셸 명령문]

컨테이너 ID는 docker ps 명령문을 사용하여 확인 할 수 있습니다.

$ docker ps
CONTAINER ID   IMAGE                     COMMAND                  CREATED         STATUS         PORTS                    NAMES
083cc001c511   ubuntu                    "bash -c 'shuf -i 1-…"   7 minutes ago   Up 7 minutes                            hungry_lalande
6404edb7da2c   namepgb/getting-started   "docker-entrypoint.s…"   7 hours ago     Up 7 hours     0.0.0.0:3000->3000/tcp   objective_pare
2878508bb6d4   docker/getting-started    "/docker-entrypoint.…"   4 days ago      Up 4 days      0.0.0.0:80->80/tcp       jovial_banach
$ docker exec 083cc001c511 cat /data.txt
6232

이제 또 다른 우분투 컨테이너를 실행합니다. 이전에 실행한 우분투 컨테이너와 동일한 이미지를 사용하도록 합니다. ubuntu는 도커 레지스트리에서 Pull 받은 이미지의 이름입니다. -it 플래그는 컨테이너를 실행하면서 이어지는 셸 명령을 컨테이너에 실행 요청합니다.

$ docker run -it ubuntu ls /

명령문의 실행 결과로, 새로 실행된 컨테이너의 루트 디렉토리에 /data.txt 파일이 존재하지 않는 것을 확인 할 수 있습니다.

$ docker run -it ubuntu ls /
bin   dev  home  lib32  libx32  mnt  proc  run   srv  tmp  var
boot  etc  lib   lib64  media   opt  root  sbin  sys  usr

앞서 설명한 것처럼 두 개의 컨테이너가 서로 다른 고유의 스크래치 공간을 갖고있기 때문입니다. 파일 시스템이 완전히 분리되어 있기 때문에, 컨테이너는 서로의 파일을 공유하지 않습니다.

이제 테스트가 끝난 우분투 컨테이너를 다음 명령문을 사용하여 삭제합니다.

docker rm -f [컨테이너 ID]

컨테이너 ID는 docker ps 명령문을 사용하여 확인합니다.

$ docker ps
CONTAINER ID   IMAGE                     COMMAND                  CREATED          STATUS          PORTS                    NAMES
083cc001c511   ubuntu                    "bash -c 'shuf -i 1-…"   15 minutes ago   Up 15 minutes                            hungry_lalande
6404edb7da2c   namepgb/getting-started   "docker-entrypoint.s…"   7 hours ago      Up 7 hours      0.0.0.0:3000->3000/tcp   objective_pare
2878508bb6d4   docker/getting-started    "/docker-entrypoint.…"   4 days ago       Up 4 days       0.0.0.0:80->80/tcp       jovial_banach
$ docker rm -f 083cc001c511
083cc001c511

컨테이너 볼륨(Container volume)

방금 테스트에서 우리는 각 컨테이너가 이미지로부터 실행될 때 새로 정의되는 것을 확인 할 수 있었습니다. 컨테이너에서 파일을 생성하고, 수정하고, 삭제할 수 있지만 이러한 모든 변경사항은 컨테이너가 제거됨으로써 휘발됩니다. 이때 볼륨(Volume)을 사용하면 컨테이너의 파일 시스템을 보존 할 수 있습니다.

볼륨은 컨테이너의 특정 파일 경로를 호스트 시스템에 연결하는 기능입니다. 컨테이너의 디렉토리가 마운트(Mount)되면, 호스트 시스템에서도 변경 사항을 그대로 확인 할 수 있습니다. 컨테이너가 재시작되더라도, 호스트 시스템의 디렉토리를 다시 마운트해서 파일 시스템을 보존하는 방식입니다.

튜토리얼 앱에 마운트

튜토리얼 앱의 Todo 목록은 SQLite 데이터베이스를 사용하여 /etc/todos/todo.db에 저장됩니다. 데이터베이스가 단일 파일로 구성되기 때문에, 해당 파일을 유지하고 다음 컨테이너 시작 시에 사용할 수 있다면 컨테이너가 중단된 지점부터 재사용 할 수 있습니다. 이를 위해 볼륨을 생성하고 디렉토리에 첨부-마운팅(Mounting)해야 합니다.

이제 마운팅을 위해 named volume을 사용합니다. named volume은 단순한 구조의 데이터 버킷이라고 생각하면 됩니다. 도커는 호스트 시스템에서 디스크의 물리적인 위치를 기억하고, 사용자는 볼륨의 이름(name)을 기억하기만 하면 됩니다. 볼륨을 사용할 때마다 도커가 데이터의 제공이 올바른지 확인해줄 것입니다.

볼륨을 생성하는 명령문은 다음과 같습니다.

$ docker volume create [볼륨 이름]

우리는 볼륨에 todo-db라는 이름을 붙이도록 하겠습니다.

$ docker volume create todo-db
todo-db

볼륨이 생성되면 도커 데스크탑의 Volumes 탭에서 확인 할 수 있습니다.

기존에 튜토리얼 앱을 실행하고 있던 컨테이너를 제거합니다.

$ docker rm -f [컨테이너 ID]

컨테이너 ID는 docker ps 명령문을 사용하여 확인 할 수 있습니다.

$ docker ps
CONTAINER ID   IMAGE                     COMMAND                  CREATED       STATUS       PORTS                    NAMES
6404edb7da2c   namepgb/getting-started   "docker-entrypoint.s…"   7 hours ago   Up 7 hours   0.0.0.0:3000->3000/tcp   objective_pare
2878508bb6d4   docker/getting-started    "/docker-entrypoint.…"   4 days ago    Up 4 days    0.0.0.0:80->80/tcp       jovial_banach
$ docker rm -f 6404edb7da2c
6404edb7da2c

튜토리얼 앱을 다시 실행하는데, 이번에는 컨테이너의 시작 명령문에서 -v 플래그로 볼륨 마운트를 지정합니다. 우분투 이미지에서 SQLite가 /etc/todos/todo.db 경로에 데이터베이스를 저장하므로, 이전에 생성한 named volume을 사용하여 /etc/todos 디렉토리를 마운트하고, 마운트 경로에 있는 모든 파일을 캡처합니다.

도커 컨테이너 실행 명령문을 입력하며, -v 플래그를 사용하여 [볼륨 이름]:[마운트 경로]를 지정합니다.

$ docker run -dp 3000:3000 -v todo-db:/etc/todos namepgb/getting-started
68cde9f0c49cdbf4dbb99b092efc98018462424ff984b474587b415aceb1164d

다시 도커 데스크탑을 실행하여 Volumes 항목을 확인합니다. 우리가 이전에 작성한 todo-db 볼륨이 이제 IN USE 태그가 붙어있는 것을 확인 할 수 있습니다.

이제 웹 페이지를 열고 튜토리얼 앱(http://127.0.0.1:3000/​)에 Todo 목록을 추가해볼까요? 변경된 목록은, 컨테이너를 삭제하고 재시작하더라도(동일한 볼륨을 지정하고 있다는 가정하에) 목록이 유지되는 것을 확인 할 수 있습니다.

그렇다면 볼륨은 실제 호스트 시스템 어디에 저장될까요? 이같은 질문에, 도커는 볼륨을 검사할 수 있는 명령문을 제공합니다.

$ docker volume inspect [볼륨 이름]

 

볼륨 정보는 JSON 포멧으로 출력됩니다. 이중에서 Mountpoint가 궁금한 볼륨의 실제 저장 경로입니다.

$ docker volume inspect todo-db
[
    {
        "CreatedAt": "2022-02-26T18:57:22Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

하지만 아쉽게도 아직은 Mountpoint에 직접 접근할 수 없습니다. 도커 데스크탑이 실행되는동안, 도커 명령문은 실제로는 호스트 시스템 내의 VM에서 실행되기 때문입니다. 따라서 Mountpoint에 직접 접근하기 위해서는 먼저 VM 내부에 들어 갈 수 있어야 합니다.