A Look at Read-Only Volumes

이번엔 read-only volume에 대해 살펴보겠습니다. 지난시간에 보았듯 다음과 같은 볼륨을 사용한다고 해봅시다.

-v /${container_path}/node_modules -v ${host_machine_path}:${container_path}

여기서 바인드 마운트를 생각해보면, 우리가 호스트 머신의 파일을 변경하면 자동으로 컨테이너에도 반영됩니다. 그런데 생각해보면 컨테이너는 호스트 머신의 상태를 읽기만 할 뿐 쓰거나 변경할 권한이 있어서는 안됩니다. 오직 개발자가 host machine의 파일 시스템을 변경할 수 있도록 해야합니다. 컨테이너나 running application은 파일을 변경해선 안되죠.

volume은 기본적으로 r/w이 가능합니다. 따라서 위의 예시에서 컨테이너는 호스트 머신의 파일 시스템을 변경할 수 있는 것이죠. 따라서 우리의 바인드 마운트를 read-only volume으로 만들어줄 필요가 있습니다. 이는 다음과 같은 옵션을 사용하면 가능합니다.

-v /${container_path}/node_modules -v ${host_machine_path}:${container_path}:ro

바인트 마운트에 :ro 옵션이 추가된 것을 볼 수 있습니다. 이제 컨테이너는 호스트 머신의 변경사항만 반영할 뿐, 컨테이너에서 호스트 머신의 상태를 변경할 수 없습니다.

그러나 앱 전체를 바인드 마운트 한 상태에서 앱 내부의 특정 폴더는 write 권한이 필요한 경우가 있을 수 있습니다. 어플리케이션의 유저인터렉션의 결과를 파일 형태로 저장하는 등의 기능이 필요한 경우 그럴 수 있죠.

이럴 경우 특정 폴더에 다음과 같이 specific sub-volume을 지정해주면, /app/node_modules의 예제처럼 다른 볼륨을 사용할 수 있도록 해줍니다. 다음과 같은 형태로 적용할 수 있습니다.

-v /${container_path}/node_modules -v ${host_machine_path}:${container_path}:ro -v /{container_path}/{right_need_folder}

이제 전체 컨테이너는 호스트머신의 read 권한만 갖는 바인드 마운트를 사용하지만, 컨테이너의 {right_need_folder} 만큼은 r/w 권한을 갖는 특정한 볼륨을 사용합니다.

따라서 해당 폴더는 컨테이너에서 r/w 전부 가능하도록 만들 수 있는 것이죠. 위 예에선anonymous volume을 사용하였기에 컨테이너가 죽고 다시 실행되면 해당 볼륨의 내용은 사라지게 될겁니다. 만약 컨테이너를 재실행해도 계속 데이터를 남기고 싶다면 해당 볼륨을 named-volume으로 설정해주면 됩니다.

Managing Docker Volumes

지금까지 볼륨과 바인드마운트에 대한 전반적인 내용을 살펴봤습니다. 여기서 volume은 docker에 의해 관리된다고 배웠었죠. 우리는 도커 커멘드를 통해 볼륨을 관리할 수도 있습니다.

docker volume --help 해당 명령어를 통해 우리가 사용할 수 있는 명령어와 옵션을 살펴볼 수 있습니다.

docker volume ls 를 사용하면 현재 active한 볼륨을 살펴볼 수 있습니다. 위 예시를 따라했다면 우리가 만든 볼륨의 정보를 볼 수 있을 것입니다. 여기서 bind-mount는 보이지 않습니다. 왜냐하면 bind-mount는 도커에 의해 관리되는 것이 아니기 때문이죠.

docker volume create --help 를 사용하면 명령어를 통해 볼륨을 생성할 수 있는 명령어들을 볼 수 있습니다.

예를 들어 docker volume create feedback-files 명령어를 실행하면 feedback-files 볼륨이 생성된 것을 확인할 수 있습니다. 이 볼륨을 컨테이너 생성시 활용할 수 있습니다.

물론, -v 옵션을 통해 볼륨을 생성하는 경우 볼륨을 자동으로 생성해주기에 명시적으로 명령어를 통해 볼륨을 생성할 필요는 사실 없습니다.

docker volume inspect {volume} 을 통해 volume에 대한 정보를 확인할 수 있습니다. 생성된 날짜, 이름, Mountpoint, options 등의 정보를 볼 수 있습니다.

docker volume rm {volume}을 통해 특정 볼륨을 삭제할 수 있습니다. 컨테이너를 중지하고 컨테이너에서 사용하던 볼륨을 삭제한 후 다시 컨테이너를 실행해보면, 해당 볼륨이 삭제되어 내부에 있던 데이터가 전부 없어진 것을 확인할 수 있습니다.

docker volume prune 명령어를 통해 사용되지 않는 볼륨을 전부 삭제할 수 있습니다. 컨테이너를 중지하고 해당 명령어를 실행하면, 사용되지 않는 모든 볼륨이 삭제된 것을 확이할 수 있습니다.

COPY vs Bind Mounts

지금까지 볼륨에 대한 전체적인 내용을 살펴봤습니다. 여기서 의문이 들 수 있는데요. 바로 바인드 마운트와 Dockerfile의 COPY입니다. 우리는 많은 경우 COPY 명령어를 이용해 컨테이너에 호스트 폴더의 내용을 복사합니다. 하지만 Bind Mounts를 사용한다면 COPY를 호스트 머신의 내용으로 덮어 씌우게 되겠죠. 따라서 COPY를 사용할 필요가 없어집니다.

그럼 Bind Mounts를 적용하면 COPY 명령어를 사용할 필요가 전혀 없을까요? 실제로 COPY 명령어를 지우고 이미지를 빌드하여 다시 컨테이너를 생성해보면 어플리케이션이 문제없이 실행되는 것을 확인할 수 있습니다.

하지만 바인드 마운트에 대해 기억해야할 부분이 있습니다. 바인드 마운트는 개발시점에 호스트 머신의 변경 사항을 즉각적으로 반영하기 위해 사용되는 경우가 많다는 것이죠. 개발이 완료되었고, 실제 어플리케이션을 서버에 올리는 경우에는 바인드 마운트를 사용하지 않을 것입니다. 왜냐하면 서버에 배포된 어플리케이션은 어플리케이션이 실행된 상태에서 업데이트되는 코드는 없기 때문입니다. 만약 어플리케이션이 떠 있는 상태에서 코드가 그때그때 변경된다면 오히려 문제가 발생하겠죠.

production에서는 개발이 완료된 코드에 대한 snap-shot이 필요합니다. 따라서 이를 위해 Dockerfile에는 COPY 명령어가 필요합니다. 바인드 마운트를 사용해도 물론 동작하겠지만, 우리는 특정 서버에 배포를 위해선 COPY 명령어를 사용할 확률이 높습니다.

dockerignore

COPY . . 명령어를 사용하면 모든 상태를 컨테이너에 복사합니다. 그러나 컨테이너에 복사해선 안되는 상태가 존재할 수 있습니다. 이럴 경우 .dockerignore 파일을 생성하여 복사를 방지하는 파일/폴더를 명시할 수 있습니다. .gitignore에 익숙한 개발자들은 당연히 이런 개념에 익숙하겠죠?

node_modules와 같은 폴더를 명시하면 COPY시 불필요한 복사를 방지할 수 있습니다. 이런 방법을 통해 이미지 생성시의 최적화를 진행할 수 있겠죠?

Environment Variables & .env files

볼륨을 넘어왔으니 이제 상대적으로 조금 가벼운 주제를 살펴보겠습니다. Docker는 built-time ARGuments와 runtime ENVironment variable을 지원합니다.

  • ARG
    • Available inside of Dockerfile, NOT accessible in CMD or any application code
    • Set on image build (docker build) via --build-arg
  • ENV
    • Available inside of Dockerfile & in application code
    • Set via ENV in Dockerfile or via --env on docker run

이 개념들은 더 유연한 이미지와 컨테이너를 만들도록 도와줍니다. 왜냐하면 모든 값들을 하드코딩하지 않고 다이나믹하게 적용할 수 있기 때문이죠.

조금은 모호하니 실제 예시를 살펴봅시다. 우리의 어플리케이션에 다음과 같은 코드가 있다고 해봅시다.

app.listen(80);

이 상태에서 우리가 개발하는 경우만 포트를 바꾸고 싶다는 니즈가 있다고 가정해볼까요? 이런 경우 사용할 수 있는 것이 environment variable, 환경변수 입니다.

코드를 다음과 같이 수정해보겠습니다.

app.listen(process.env.PORT);

node의 경우 process.env.{환경변수명}을 통해 Global env에 접근할 수 있도록 기능을 제공합니다. 대부분의 개발언어와 도구에서 환경변수에 접근하는 기능을 제공하죠.

이제 환경변수 PORT의 값에 따라 어플리케이션 포트 번호가 변경될 수 있죠.

Dockerfile에 다음과 같은 코드를 추가해봅시다.

...
ENV PORT 80

EXPOSE $PORT
...

이제 우리의 코드는 PORT 변수의 값에 따라 유연하게 port 번호를 변경할 수 있는 코드가 되었습니다.

이 환경변수는 docker run 명령어 수행시 특정 값으로 세팅도 가능합니다. 다음과 같은 명령어를 사용하여 컨테이너 실행시 환경변수의 값을 세팅할 수 있습니다.

docker run ... --env PORT=8000 ...

위 명령어와 같이 실행해보면 어플리케이션이 8000번 포트에서 실행되는 것을 확인할 수 있습니다. 환경변수 덕분에 우리는 포트 번호를 수정하고 싶을때 이미지를 재생성할 필요가 없어졌습니다. 그저 환경변수 세팅만 바꿔주면 되는 것이죠.

--env-e로도 사용할 수 있습니다. 또한 multiple environment variable 설정도 가능합니다.

-e PORT=8000 -e KEY=VALUE

위처럼 -e를 여러번 명시하여 사용할 수 있습니다.

만약 환경변수가 많다면 명령어에서 세팅하지 않고 .env 파일을 생성하여 해당 파일에서 환경변수를 관리할 수 있습니다.

// .env
PORT=8000
KEY=VALUE
...

docker run ... --env-file ./.env ...

위와 같이 .env file을 생성하고 --env-file 옵션을 통해 해당 파일을 명시하여, 파일 내부의 변수들을 환경변수로 사용할 수 있습니다.

secure data 등 중요한 정보가 포함된 변수는 Dockerfile에 명시해선 안됩니다. 런타임시만 사용되도록 분리된 환경변수 파일을 생성하여 사용해야 합니다. 그렇지 않으면 "baked into the image"되어 docker history <image> 명령어를 통해 누구나 환경변수 내용을 확인할 수 있습니다.
credentials나 private key같은 중요한 정보는 분리된 환경변수 파일에 명시하고 image의 일부가 되지 않도록 해야 합니다. 또한 source control repository에 commit되지 않도록 주의해야합니다.

Build Argument(ARG)

이제 build-time argument를 살펴보겠습니다. 이미지 자체를 빌드할때 필요한 정보는 환경변수로 만들 필요가 없을 수 있습니다. Dockerfile에 환경변수로 PORT를 명시한 경우를 생각해보면 default value(80)을 하드코딩한 것을 알 수 있죠.

이런 경우 Build-time argument를 사용할 수 있습니다. Dockerfile에 다음과 같이 작성해보겠습니다.

...
ARG DEFAULT_PORT=80

ENV PORT $DEFAULT_PORT

EXPOSE $PORT
...

Argument는 application code에서 접근할 수 없습니다. 오직 Dockerfile에서만 접근 가능하죠. 이제 PORT 환경변수의 기본값을 Argument로 세팅할 수 있습니다.

이제 다음과 같은 명령어를 실행해보겠습니다.

docker build ... --build-arg DEFAULT_PORT=8000 ...

이제 이미지를 빌드할 때 Dockerfile을 변경하지 않고 다이나믹하게 PORT 값을 변경할 수 있습니다.

Summary

지금까지 배운 내용을 간략하게 정리해봅시다.

  • Containers can read/write data. Volumes can help with data storage, Bind Mounts can help with direct container interaction.
    • Containers can read/write data, but written data is lost if the container is removed
    • Volumes are folders on the host machine, managed by Docker, which are mounted into the Container
      • Named Volumes: survive container removal and can therefore be used to store persistent data
      • Anonymous Volumes: attached to a container - they can be used to save (temporary) data inside the container
      • Bind Mounts: folders on the host machine which are specified by the user and mounted into containers - like Named Volumes
  • Build ARGuments and Runtime ENVironment variables can be used to make images and containers more dynamic / configurable

출처

profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글