컨테이너 속  데이터가 사라지는 이유

모든 컨테이너는 독립된 파일 시스템을 갖는다.  

같은 이미지에서 실행한 여러 개의 컨테이너는 처음에는 디스크의 내용이 모두 같지만,

그 중 한 컨테이너에서 애플리케이션이 파일을 수정해도 다른 컨테이너나 이미지는 영향을 받지않는다.

 

같은 이미지로부터 두개의 컨테이너 실행 - 컨테이너 속 파일에 무작위 숫자를 쓰는 기능

docker container run --name rn1 diamol/ch06-random-number
docker container run --name rn2 diamol/ch06-random-number

 

컨테이너에 생성된 파일을 로컬 컴퓨터로 복사

container cp rn1:/random/number.txt number1.txt
container cp rn2:/random/number.txt number2.txt

 

내용 확인

cat number1.txt
cat number2.txt

 

두 컨테이너 속에 있는 파일의 내용이 다름을 확인

 

 

위의 코드들을 실행했을때 일어나는 상황을 나타낸 그림

이미지 레이어(읽기 전용)는 모든 컨테이너가 공유 하지만 기록 가능 레이어는 컨테이너마다 다르다.

기록 가능 레이어는 컨테이너와 같은 생애주기를 가진다. ( 컨테이너 생성할때 생성되고 컨테이너를 삭제하면 같이 삭제된다.)

 

기록 가능 레이어를 새 파일을 만드는 데만 사용하는 것은 아니다.

기존 이미지 레이어에 있는 파일을 수정할 수 있음.

→ 기록 중 복사 (copy-on-write)라는 방법을 사용해 읽기 전용 레이어의 파일을 수정가능

방식 = 도커가 이 파일을 쓰기 가능 레이어로 복사해 온 다음 쓰기 가능 레이어에서 파일을 수정

 

<컨테이너를 실행해 파일의 내용을 출력 - 그다음 파일의 내용을 수정 - 컨테이너를 재시작해 변경된 파일 내용 확인>

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

파일의 내용이 변경된 상태

 

f1 을 삭제하면 변경된 내용도 사라진다.

만약에 컨테이너로 데이터베이스를 실행해 사용하고 있는데, 해당 컨테이너를 지운다면? 컨테이너에 있는 모든 데이터는 사라진다.

 

컨테이너 속 데이터가 지워지는 이유 요약

  1. 컨테이너 삭제 : 해당 컨테이너의 파일 시스템도 삭제되기 때문 데이터 손실 
  2. 컨테이너 재생성  : 동일한 이름으로 새 컨테이너를 생성하면 기존 컨테이너의 데이터는 유지되지 않음
  3. 새로운 이미지를 빌드 후 컨테이너 생성 : 이미지는 컨테이너의 상태를 포함하지 않기 때문에 데이터 손실

이를  막기 위해서 도커 볼륨(Docker volumn)과 마운트(mount)를 추가하여 데이터를 따로 저장할 수 있다.

 

도커 볼륨을 사용하는 컨테이너 실행하기

컨테이너에서 볼륨을 사용하는 방법

  1. 수동으로 직접 보륨을 생성해 컨테이너에 연결하는 방법
  2. Dockerfile 스크립트에서 VOLUME 인스트럭션을 사용하는 방법
    • VOLUME 인스트럭션 문법 : VOLUME <target-directory>

볼륨이 사용된  멀티 스테이지 빌드 Dockerfile 스크립트 예시

FROM diamol/dotnet-aspnet
WORKDIR /app
ENTRYPOINT ["dotnet", "ToDoList.dll"]

VOLUME /data
COPY --from=builder /out/ .
  • VOLUME /data = 특정 경로를 컨테이너의 볼륨으로 설정하는 명령어
    • 컨테이너를 실행할 때 /data 를 볼륨으로 사용하겠다는 선언, 컨테이너가 종료되어도 데이터 유지

 

<Todo-list 애플리케이션 이미지로 컨테이너를 실행해 컨테이너와 연결된 볼륨을 살펴 보기>

  1.  Dockerfile 스크립트의 정의에 따라 볼륨을 생성해 컨테이너에 연결

docker container run --name todo1 -d -p 8010:80 diamol/ch06-todo-list
  • -p 8010:80   = -p <호스트 포트>:<컨테이너 포트>

  2.  컨테이너에 마운트된 'todo1' 의 상세정보 출력

docker container inspect --format '{{.Mounts}}' todo1
  • inspect = Docker 컨테이너(또는 이미지, 네트워크 등)의 상세 정보를 JSON 형식으로 출력
  • --format =JSON 데이터를 원하는 형식으로 필터링하거나 변환하여 출력 (템플릿 표현식({{}})을 사용하여 특정 필드 값을 출력)
    • {{.Mounts}}: 컨테이너의 마운트 정보만 출력
  • todo1 =  정보를 출력하려는 대상 컨테이너의 이름

출력된 mount 항목의 정보

  3.  생성된 볼륨 확인

docker volume ls

 

http://localhost:8010/ 에 접속하여 to-do 애플리케이션 확인

 

<같은 이미지로 생성한  컨테이너속 애플리케이션의 데이터 비교 : --volumes-from 옵션 차이>

- 사전에 todo1 에 들어가서 데이터를 생성-

1.  새로운 컨테이너에 todo2 애플리케이션 실행 

docker container run --name todo2 -d diamol/ch06-todo-list

 

 

2. 새로운 컨테이너에 todo3 애플리케이션 실행 (todo1 과 데이터 공유, --volumes-from)

docker container run -d --name todo3 --volumes-from todo1 diamol/ch06-todo-listdocker container run -d --name todo3 --volumes-from todo1 diamol/ch06-todo-list

 

 

3. 데이터 디렉터리 비교

  • todo1 & todo3 는 데이터를 공유하므로 data 디렉터리의 내용이 같다.
  • todo2 는 새롭게 생성되어 data  디렉터리가 비어 있다.

이 방식은 컨테이너 간 파일 공유보다는 업데이트 간 상태를 보존하기 위한 용도가 적합

 

< 기존의 데이터를 보존하고 업데이터 후  보존된 데이터로 앱 실행 >

  • 볼륨을 사용하면 컨테이너를 교체 시에도 데이터를 보존할 수 있다.

볼륨을 생성(todo-list)

docker volume create todo-list

 

  버전 1 의 to-do 애플리케이션에서 볼륨을 사용 

docker container run -d -p 8011:80 -v todo-list:/data --name todo-v1 diamol/ch06-todo-list

 

→ 애플리케이션에서 UI를 통해 데이터 추가

 

→ 버전 1 삭제 후 애플리케이션을 버전 2로 업데이트 

docker container rm -f todo-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 에서 볼륨을 지정하지 않으면 항상 새로운 볼륨이 무작위로 만들어진 식별자로 생성

파일 시스템 마운트를 사용하는 컨테이너 실행하기

볼륨의 장점은 컨테이너와 스토리지의 생애주기를 분리하면서도 도커를 사용하는 방식 그대로 스토리지를 다룰 수 있는 점이다.

 

컨테이너에 좀 더 직접적으로 호스트의 스토리지를 연결할 수 있는 수단 = 바인드 마운트(bind mount)

 

바인드 마운트

- 호스트 컴퓨터 파일 시스템의 디렉터리를 컨테이너 파일 시스템의 디렉터리로 만든다.

- 도커를 사용할 때 컨테이너가 호스트 컴퓨터의 파일에 직접 접근 가능, 그 반대도 가능(양방향)

    - 호스트 컴퓨터에 대한 공격을 방지하기 위해 최소 권한을 가진 계정으로 실행

    - 호스트 컴퓨터 파일에 접근하기 위해 권한 상승 필요, Dockerfile 에 USER 인스트럭션을 이용하여 권한을 부여

 

<호스트 컴퓨터의 로컬 디렉터리를 컨테이너에 바인드 마운트로 연결 해보기>

현재 작업 디렉터리에 database 폴더 생성

mkdir database

 

바인드 마운트를 적용하여 컨테이너를 실행

docker container run --mount type=bind,source=$(pwd)/database,target=/data -d -p 8012:80 diamol/ch06-todo-list
  • $(pwd)는 현재 작업 디렉터리의 절대 경로를 삽curl 명령으로 컨테이너에 요청 보내서 애플리케이션 실행 
  • /data 디렉토리에 생성된 모든 데이터는 호스트의 $(pwd)/database 디렉토리에 저장
  • target=/data로 지정된 컨테이너 내부 경로는 컨테이너가 이 디렉터리를 사용하여 파일을 읽고 쓰는 위치
    • 가상경로로 /data 라고 했지만 실제로 데이터는 $(pwd)/database 에 저장
curl http://localhost:8012| Out-Null
  • Out-Null : 명령의 출력을 제거 하기 위한 옵션
  • 애플리케이션이 시작되면서 컨테이너 /data 에 데이터 베이스 파일이 생성되고 호스트의 database 에 저장

database 폴더 확인

ls ./database

컨테이너에서 생성한 todo-list.db 파일 확인

 

 

<바인드 마운트를 통해 애플리케이션이 호스트의 설정 파일을 사용해보기>

  • 제공된 to-do 애플리케이션은 /app/config 경로가 존재할 경우 이 디렉터리에서 추가 설정 파일을 로드한다. 호스트 컴퓨터의 디렉터리를 이 경로에 연결하도록 바인드 마운트를 적용한 컨테이너를 실행해 애플리케이션이 호스트 컴퓨터에 있는 설정 파일을 사용하도록 하자. ch06/exercises/todo-list(제공된 파일) 로 이동하여 다음 명령어 들을 입력하면 된다.

제공 파일 위치로 이동 (ch06/exercises/todo-list)

 

바인드 마운트된 호스트 컴퓨터의 디렉터리에 있는 설정파일(/app/config)을 컨테이너 속 애플리케이션이 사용하도록 명령

docker container run --name todo-configured -d -p 8013:80 --mount type=bind,source=$(pwd)/config,target=/app/config,readonly diamol/ch06-todo-list

- 주의. 긴 명령어 사이(type,source,target)에 공백으로 인해 아래의 오류가 생길수 있다.

                "invalid argument "type=bind," for "--mount" flag: invalid field '' must be a key=value pair"

 

애플리케이션 작동 여부 확인

curl http://localhost:8013 | Out-Null

 -Out-Null은 PowerShell에서 사용하는 출력 무시 명령, zsh에서는 사용할 수 없음

 

 

컨테이너 로그 확인

docker container logs todo-configured

  - 입력하면 많은 양의 debug 레벨 로그가 출력됨

 

 

호스트  컴퓨터가 접근할 수 있는 스토리지라면 무엇이든 바인드 마운트를 통해 컨테이너에 연결할 수 있다.

네트워크 드라이브가 리눅스나 윈도에 연결 돼 있다면 분산 스토리지를 컨테이너에 연결해 유상태 애플리케이션에서 사용하게 하면 신뢰성을 크게 개선할 수도 있지만 한계도 존재한다.

파일 시스템 마운트의 한계점

시나리오 1 : 컨테이너의 마운트 대상 디렉터리가 이미 존재하고 이미지 레이어에 이 디렉터리의 파일이 포함돼 있다면??

                      "컨테이너의 마운트 대상 디렉터리 = 컨테이너 안에 이미 존재하는 기존 디렉터리(예: /app)",
                      "이미지 레이어에 이 디렉터리의 파일이 포함 = 이미지에 이 디렉터리(/app)와 그 안의 파일들이 이미 저장되어 있다

  1. 원래 있던 파일과 마운트 된 파일에 모두 접근 가능하다.
  2. 이미 존재하는 대상 디렉터리에 마운트하면 마운트 원본 디렉터리가 기존 디렉터리를 완전히 덮어씌운다. 즉, 이미지에 포함돼 있던 원래 파일은 사용할 수 없다.

<정답은 2번>

(이유) 컨테이너 내부에 이미 존재하는 디렉터리(기존 디렉터리)가 있다고 해도, 마운트를 실행하면 해당 디렉터리는 마운트 원본 디렉터리로 완전히 덮어씌워지기 때문에 기존 디렉터리 안의 파일은 보이지 않고 사용할 수 없게 됩니다.

기존 디렉터리
     컨테이너 내부에 원래부터 존재하는 디렉터리로, 파일과 데이터(물건들)가 들어 있는 공간(상자)
         (예) 컨테이너 이미지에 포함된 디렉터리와 그 안의 파일들.

마운트 원본 디렉터리
      호스트 시스템에 존재하며, 컨테이너 내부의 특정 디렉터리와 연결되기 위해 사용하는 디렉터리
     새로운 상자 역할을 하며, 호스트(또는 외부) 시스템에서 가져온 디렉터리

▶ 기존 상자 위에 새로운 상자를 얹으면, 기존 상자 안에 무엇이 들어 있는지 확인하거나 사용할 수 없다.

 

예시:

  1. 컨테이너에 /app이라는 디렉터리가 존재하며, 그 안에 file1.txt와 file2.txt라는 파일이 포함되어 있다고 가정
    (이 디렉터리와 파일들은 컨테이너 이미지에 포함된 상태)
  2. 이제, 호스트 시스템의 /host/app 디렉터리를 컨테이너의 /app 디렉터리에 마운트한다고 가정
  3. 마운트 작업을 실행하면:
    • 컨테이너의 /app 디렉터리는 더 이상 기존의 file1.txt, file2.txt 파일을 보여주지 않고, 대신 호스트의 /host/app 디렉터리 내용을 표시 함 (이미지에 포함된 /app 디렉터리의 파일들은 가려져서 접근 불가)

참고:  이 동작은 기존 디렉터리의 파일을 삭제하는 것이 아니라, 단순히 마운트 원본 디렉터리의 내용으로 덮어씌워 보여지는 것

 

< 마운트가 없는 컨테이너를 실행해 이미지에서 받은 파일 목록을 확인하라. 그다음 마운트를 지정해 컨테이너를 다시 실행하고 마운트 원본 디렉터리의 ㅍ차일 목록이 출력되는지 확인하라.>

 

바인드 마운트가 마운트되어 있지 않은 컨테이너 실행

docker container run diamol/ch06-bind-mount

 - abc.txt 와 def.txt 출력

/init 디렉터리에 있는 파일 목록 출력 (abc.txt, def.txt)

 

/init 를 대상으로 바인드 마운트를 마운트해 컨테이너 실행

docker container run --mount type=bind,source=$(pwd)/new,target=/init diamol/ch06-bind-mount

 - 기존 디렉터리의 내용은 숨겨지고 바인트 마운트의 원본 디렉터리가 이를 대체

123.txt, 456.txt 를 출력

 

시나리오 2. 호스트 컴퓨터의 파일 하나를 컨테이너에 이미 존재하는 디렉터리로 마운트 하면 어떻게 될까?

    ▶ 디렉터리의 파일이 합쳐져 이미지에서 온 파일과 호스트에서 마운트된 파일이 모두 나타남(윈도 컨테이너는 이 기능 제공 안함)

 

컨테이터 파일 시스템은 윈도 컨테이너와 리눅스 컨테이너의 동작이 일치하지 않는 몇 안 되는 영역 중 하나

 

< 단일 파일 마운트는 리눅스 컨테이너와 윈도 컨테이너에서 서로 다르게 동작한다.>

 

리눅스 버전

docker container run --mount type=bind,source="$(pwd)/new/123.txt",target=/init/123.txt diamol/ch06-bind-mount

이동 시킨 파일과 같이 출력

 

윈도우 버전(target 부분에서 주소가 다름)

docker container run --mount type=bind,source="$(pwd)/new/123.txt",target=C:\init\123.txt diamol/ch06-bind-mount

 

 

 

위의 구체적인 예를 나타낸 그림 : 애저 파일스 스토리를 컨테이너 스토리지로 사용해 Postgress 데이터베이스를 실행한 경우

그림 6-14 분산 파일 시스템을 마운트하면 일반적인 파일 시스템의 기능 중에서 지원하지 않는 기능이 있을 수 있다.

애저 파일스는 읽기 및 쓰기 기능은 똑같이 제공하지만 지원하지 않는 기능이 존재. 위의 그림에서는 애저 파일스에서 지원하지 않는 파일 링크 생성을 시도하다가 실패해서 앱 오류를 일으킨 것을 나타내었다.

 

컨테이너의 파일 시스템은 어떻게 만들어지는가?

유니언 파일 시스템(union file system)?

모든 컨테이너는 도커가 다양한 출처로부터 모아 만든 단일 가상 디스크로 구성된 파일 시스템을 가지는데 이걸 유니언 파일 시스템이라고 한다.

 

컨테이너는 유니언 파일 시스템을 통해 물리적 위치가 서로 다른 파일과 디렉터리에 마치 단일 디스크를 사용하듯 접근할 수 있다.

컨테이너에서 실행되는 애플리케이션의 입장에서는 단일 디스크만을 볼수 있지만, 컨테이너나 이미지를 생성해 사용하는 사용자는 여러 출처를 합쳐 이 디스크를 구성할 수 있다.

  • 기록가능 레이어(Writeable layer) :
    • 비용이 비싼 계산이나 네트워크를 통해 저장해야 하는 데이터의 캐싱등 단기 저장에 적합,
    • 컨테이너가 삭제되면 저장된 기록도 삭제됨
  • 로컬 바인드 마운트(Bind mount:local)
    • 호스트 컴퓨터와 컨테이너 간 데이터를 공유하기 위해 사용
    • 로컬 컴퓨터에서 수정한 내용을 이미지 빌드 없이 즉시 컨테이너로 이동 가능
  • 분산 바인드 마운트(Bind mount: distrubuted)
    • 네트워크 스토리지와 컨테이너 간에 데이터를 공유하기 위해 사용
    • 읽기 전용으로 설정 파일을 전달하거나 공유 캐시로 활용 가능
    • 읽기 쓰기 가능으로 데이터를 저장해 동일 네트워크상의 모든 컨테이너나 컴퓨터와 데이터를 공유하는데 적합
  • 볼륨 마운트(Volume mount)
    • 컨테이너와 도커 객체인 볼륨 간에 데이터를 공유하기 위해 사용
    • 애플리케이션이 볼륨에 데이터를 영구적으로 저장가능 (컨테이너를 교체하는 방식으로 앱을 업데이트해도 데이터 유지)

목차

레지스트리, 리포지터리, 이미지 태그 다루기

도커 이미지 참조의 구조

docker.io/diamol/golang:latest
  • docker.io : 이미지가 저장된 레지스트리의 도메인. 기본값은 도커 허브
  • diamol : 이미지 작성자의 계정 이름. 개인 혹은 단체의 이름에 해당
  • golang : 이미지 레포지토리 이름. 일반적으로 애플리케이션의 이름에 해당. 하나의 레포지토리는 여러 버전의 이미지를 담을 수 있다.
  • latest : 이미지 태그. 애플리케이션의 버전 혹은 변종을 나타낸다. 기본값은 latest

참고로 규모가 큰 회사는 사내 네트워크나 전용 클라우드 환경에 자사의 도커 레지스트리를 별도로 꾸리는 경우가 많음

만약 해당 도메인이 r.sixeyed.com 이고 위와 같은 파일을 레지스트리에 푸쉬한다면 아래와 같이 변경하면 된다.

r.sixeyed.com/diamol/golang:latest

 - 앞의 도메인 주소만 바꾸어 주면 된다.

도커 허브에 직접 빌드한 이미지 푸쉬하기

필요한 것

- 도커 허브 계정

 

도커 허브 계정 이름(가입할 때 기입한 이메일)을 환경 변수로 정의하기

# window 환경
dockerId="도커허브계정이름"

# 리눅스 또는 macOS
export dockerId="도커허브계정이름"

 

도커 허브에 로그인

docker login --username $dockerId

 

기존의 이미지에 새로운 이미지 참조 부여

 

# docker image tag (image ls 에 있는 이름) (자신의 도커허브 계정 이름)/access-log/(version)
docker image tag image-gallery jeoy/image-gallery:v1

docker image ls

두 이미지의 아이디가 같다.

 

 

이미지를 레지스트리에 푸시

docker image push jeoy/image-gallery:v1

 

아래의 그림에 출력 내용을 나타낸 그림을 보면 Pushed 된 것들은 이미지 레이어다.

레지스트리 역시 도커 엔진과 같은 방식으로 이미지 레이어를 다루면 그만큼 Dockerfile 스크립트의 최적화가 더욱 중요해진다.

이유는 레지스트리에서도 캐시상에 레이어 해시와 일치하는 레이어가 없을 경우에만 실제로 업로드가 이루어지는데 이는 도커 엔진의 레이어 캐시와 완전 같은 방식이다.

→ 최적화 된 Dockerfile 스크립트는 빌드 시간, 디스크 요량, 그리고 네트워크 대역폭까지 영향을 미치는 중요한 요소다.

 

도커 허브에 새로 푸쉬된 이지미에 대한 도커 허브 웹페이지 URL을 출력하는 명령

echo "https://hub.docker.com/r/jeoy/image-gallery/tags"

 

출력 내용 

https://hub.docker.com/r/jeoy/image-gallery/tags

 

접속 하면 보여주는 이미지 정보

 

레지스트이에 이미지를 푸시하고 확인 하는 방법은 위의 내용이 전부다.

이제 모든 사람들이 이 애플리케이션을 검색하고 내려받고 실행할 수 있다.

나만의 도커 레지스트리 운영하기

로컬 네트워크에 전용 레지스트리가 있으면 좋은점

  • 인터넷 회선 사용량을 줄여 전송시간 절약
  • 주로 사용하는 공개 레지스터리가 다운됐을때 신속하게 전환가능
  • 중요한 데이터를 외부 클라우드 서비스나 제3자에게 의존하지 않고, 자체적으로 관리가능

책에서 패키징한 이미지를 사용해 컨테이너 형태로 도커 레지스트리 실행 -> 전용 레지스트리 생성

docker container run -d -p 5000:5000 --restart always diamol/registry
  • --restart : 도커를 재시작했을 때 해당 컨테이너(diamol/registry)도 자동으로 재시작
  • localhost:5000 을 사용해 이미지에 태그를 부여하면 새로운 레지스트리에 이미지 푸쉬 가능

도메인 네임을 별명으로 붙이기

#윈도우
Add-Content -Value "127.0.0.1 registry.local" -Path /windows/system32/drivers/etc/hots

#리눅스 macOS
echo $'\n127.0.0.1 registry.local'| sudo tee -a /etc/hosts

 

registry.local 을 이용한 ping test

 

이제 이미지 참조에 도메인 네임registry.local :5000 을 사용 가능

image-gallery 이미지에 새로 만든 레지스트리 도메인 네임을 추가해 이미지를 참조를 부여

docker image tag image-gallery registry.local:5000/gallery/ui:v1
docker image tag image-of-the-day registry.local:5000/gallery/api:v1
docker image tag access-logs registry.local:5000/gallery/logs:v1

 

이제 HTTP 를 사용하여 이미지 푸시하고 내려받는 로컬 컴퓨터의 레지스트리에 HTTPS를 적용해야 한다.

 (도커 데스크탑) 오른쪽 위에 있는 톱니바퀴(⚙️) 를 클릭 -> Docker Engine 에서 

     "insecure-registries": ["registry.local:5000"]  를 추가 그 후 도커 를 restart

 

태그를 부여한 이미지를 푸시

docker image push registry.local:5000/gallery/ui:v1

 

에러 발생...

이미지 태그를 효율적으로 사용하기

이미지 태그를 붙이는 방법 [major].[miner].[patch] 형태를 따라하기

  • patch 만 바뀐 버전 : 변경 내용이 버그 수정뿐만이고, 기능은 지난 버전과 같다.
  • minor 자리가 바뀜 : 추가된 기능은 있지만, 기존 기느은 모두 유지
  • majot 자리가 바뀜 : 완전히 다른 기능을 가진다는 정보 유추 가능

공식 이미지에서 골든 이미지로 전환하기

도커허브는 검증된 퍼블리셔(verified publisher) 와 공식 이미지(official image) 제도를 통해 멀웨어가 배포되는 것을 방지한다.

 

골든 이미지는 공식이미지를 기반 이미지로 삼아 인증서나 환경 설정값 등 자신이 필요한 설정을 추가 한 것이다.

 

닷넷 코어 애플리케이션을 위한 골든 이미지를 빌드할 수 있는 스크립트를 이용하여 이미지 빌드하기

cd ch05/exercises/dotnet-sdk
docker image builid -t golden/dotentcore-sdk:3.0 .

cd ../aspnet-runtime
docker image build -t golden/aspnet-core:3.0

 

위에서 사용 된 Dockerfile 스크립트

FROM mcr.microsoft.com/dotnet/core/sdk:3.0.100

LABEL framework="dotnet"
LABEL version="3.0"
LABEL description=".NET Core 3.0 SDK"
LABEL owner="golden-images@sixeyed.com"

WORKDIR src
COPY global.json .

 다른 멀티 스테이지 빌드 스크립트와 같은 구조지만

 - 기반 이미지는 우리가 만든 이미지

 - LABEL을 이용하여 이미지의 메타테이터 정의 및 일반적인 설정을 추가

연습문제: 도커 레지스트리 API v2 명세가 담긴 문서를 조사하기

 

 

 

목차

Dockerfile이 있는데 빌드 서버가 필요할까?

대부분의 프로그래밍 언어는 프로젝트를 빌드하기 위해 다양한 도구가 필요함

- 신규로 참여한 개발자는 이 도구를 설치하는데 시간을 허비 할 수 있다.  ( 빌드 서버와 버전이 달라지는 것만으로 빌드가 실패 할 수 있음)

 

이런 경우에 빌드 툴 체인을 한 번에 패키딩해서 공유할 수 있다면 편하다 → Dockerfile 스크립트

  1. 개발에 필요한 모든 도구를 배포하는 Dockerfile 스크립트 작성하여 모두 이미지로 만든다.

  2. 애플리케이션 패키징을 위한 Dockerfile 스크립트에서 이 이미지를 사용해 소스 코드를 컴파일함으써 애플리케이션을 패키징

 

위의 워크플로(멀티 스테이지 빌드)를 적용한 Dockerfile 스크립트

FROM diamol/base AS build-stage
RUN echo 'Building...' > /build.txt

FROM diamol/base AS test-stage
COPY --from=build-stage /build.txt /build.txt
RUN echo 'Building...' >> /build.txt

FROM diamol/base
COPY --from=test-stage /build.txt /build.txt
CMD  cat /build.txt

  - 모든 stage 는 FROM 으로 시작하는데 빌드 단계에 필요에 의해 AS 를 이용하여 이름을 붙일 수 있다.

 

실행과정을 표현한 그림 -  각 빌드 단계는 서로 격리돼 있다.

 

현재 폴더에 있는 Dockerfile 실행하여 multi-stage 이미지 빌드

docker image build -t multi-stage .

 

Terminal 출력 결과
Docker 에서 확인한 Log

  • build-stage = 빌드 도구가 설치된 기반 이미지를 사용하여 빌드
  • test-stage =  빌드한 바이너리를 복사해서 단위 테스를 수행
  • 마지막으로 build-stage 에서 빌드하고 test-stage에서 테스트까지 성공적으로 마친 바이너리를 이 이미지에 복사해서 넣는다.

이 과정을 표현한 그림

애플리케이션 빌드 실전예제: Node.js 소스 코드

컨테이너화된 Node.js 애플리케이션을 실행하려면

- Node.js 런타임 과 소스 코드가 애플리케이션 이미지에 포함되어야 함.

(ch04/exercises/access-log 에 있는 파일)

npm 을 사용해 Node.js 애플리케이션을 빌드하는 Dokcerfile 스크립트

FROM diamol/node AS builder

WORKDIR /src
COPY src/package.json .
RUN npm install

# app
FROM diamol/node

EXPOSE 80
CMD ["node", "server.js"]

WORKDIR /app
COPY --from=builder /src/node_modules/ /app/node_modules/
COPY src/ .

 

1. Dockerfile 실행하여 이미지 빌드

docker image build -t access-log .

 

2. 빌드 한 access-log 이미지로 컨테이너로 실행, 이 컨테이너를 nat 네트워크에 연결하며 80번 포트를 공개하라.

docker container run --name accesslog -d -p 801:80 --network nat access-log
  •  -d (Detached mode)  = 백그라운드에서 실행
  • -p  = 포트 맵핑
  • 이때 "docker: Error response from daemon: network nat not found." 라는 문구가 뜰 수 있다.
docker network ls

    이용해서 네트워크를 확인하여 nat 의 존재 여부를 한 번더 확인 후 없으면

 

  nat 생성 후  다시  2번 에서 실행된 명령어 실행

docker network create -d bridge nat

 

성공적으로 이미지가 컨테이너에 담기면

http://localhost:801/stats 에 접속하여 결과를 확인

 

멀티 스테이지 Dockerfile 스크립트 이해하기

컨테이너 안에서 애플리케이션을 빌드하는 것이 유용한 이유

  1. 표준화 = 어떤 운영체제를 사용하든 모든 과정은 도커 컨테이너 내부에서 이루어 진다.
    • 컨테이너가 빌드하기 위한 모든 도구들의 정확한 버전을 보유하고 있으므로 빌드 실패를 크게 줄인다.
  2. 성능향상 = 멀티 스테이지 빌드의 각 단계는 자신만의 캐시를 따로 갖는다.
    • 처음에 Dockerfile 스크립트를 세심하게 최적화해서 작성한다면  이후로 캐시 재사용을 통해 90% 이상의 빌드 단계에서 시간을 절약할 수 있다. - 이유 : 빌드 중에 각 인트스럭션에 해당하는 레이어 캐시를 보유(3장에서 자세한 내용참고)
  3. 빌드 과정의 세밀한 조정하며 이미지를 가능한 한 작게 유지 가능
    • 예 ) curl 을 이용하여 인터넷을 통해 필요한 파일을 다운 받는다.  이 과정을 빌드 초기 단계에 모아 놓는다면 최종 이미제는 curl 을 포함시키지 않아도 된다. → 이미지 크기 줄여서 애플리케이션 시작 시간을 단축 가능

연습: 멀티 스테이지 빌드와 Dockerfile 스크립트 최적화

Dockerfile 스트크립트

FROM diamol/golang 

WORKDIR web
COPY index.html .
COPY main.go .

RUN go build -o /web/server
RUN chmod +x /web/server

CMD ["/web/server"]
ENV USER=sixeyed
EXPOSE 80

 

해결해야 할 문제

  • 지금 있는 Dockerfile 스크립트로 이미지를 빌드한다. 이어서 Dockerfile 스크립트를 최적화한 다음 새로운 이미지를 빌드하라.
  • 현재 이미지는 리눅스 환경에서 약 800MB, 윈도 환경에서 약 5.2GB 크기다. 최적화 된 이미지의 크기는 리눅스 환경에서 약 15MB, 윈도 환경에서 약 260MB가 되도록하라.
  • 현재 Dockerfile 스크립트에 포함된 HTML 파일의 내용을 수정하면 7 단계의 빌드 단계를 재수행한다.
  • Dockerfile 스크립트를 최적화해서 HTML 파일을 수정하더라도 재수행하는 빌드 단계가 한 단계가 되도록 하라.

[힌트] 주어진 Dockerfile 스트크립트와 같은 애플리케이션을 실행하는 Dockerfile

더보기
FROM diamol/golang AS builder

COPY main.go .
RUN go build -o /server

# app
FROM diamol/base
ENV IMAGE_API_URL="http://iotd/image" \
    ACCESS_API_URL="http://accesslog/access-log"

CMD ["/web/server"]

WORKDIR /web
COPY index.html .
COPY --from=builder /server .
RUN chmod +x server

 

최적화 후

FROM diamol/golang AS builder

COPY main.go .
RUN go build -o /server
RUN chmod +x /server

# 프로덕션 이미지
FROM diamol/base

EXPOSE 80
CMD ["/web/server"]
ENV USER="sixeyed"

WORKDIR web
COPY --from=builder /server .
COPY index.html .

 

설명

      1. 멀티 스테이지 이용하여 이미지의 크기 감소 
        • builder 스테이지에서 빌드된 바이너리를 프로덕션 스테이지로 복사
          • builder 스테이지에서 필요한 도구와 라이브러리를 설치
            • diamol/golang 이미지를 기반으로 하고, main.go 파일을 복사한 후 /server로 실행 파일을 빌드
          • 프로덕션 스테이지에서 index.html/server만 포함되므로 더 경량화된 이미지 생성가능
      2. builder 스테이지에서 권한을 부여하여 중복되는 작업 제거 
        • RUN chmod +x /server를 통해 실행 권한을 부여 (빌드과정에서 부여하는 것이 적합)
          • 이유 = 빌드 과정에서 파일의 실행 권한을 한 번 설정하면, 이후 단계에서 반복적으로 설정할 필요가 없다. 즉, 빌드 과정에서 chmod +x를 실행하면, 해당 명령이 한 번만 실행되고 이후 단계에서는 캐시를 재사용
      3. 멀티 스테이지 이용하여 중복될수 있는 문제 해결
        1. builder 스테이지에서 권한을 부여하여 중복되는 작업 제거
          • RUN chmod +x /server를 통해 실행 권한을 부여 (빌드과정에서 부여하는 것이 적합)
            • 유 = 빌드 과정에서 파일의 실행 권한을 한 번 설정하면, 이후 단계에서 반복적으로 설정할 필요가 없다. 즉, 빌드 과정에서 chmod +x를 실행하면, 해당 명령이 한 번만 실행되고 이후 단계에서는 캐시를 재사용
        2. ENVWORKDIR를 프로덕션 스테이지에 배치하여 중복 작업 방지
          • 프로덕션 단계에만 필요한 설정(ENV, WORKDIR)은 빌더 단계에서 실행할 필요가 없으므로, 프로덕션 스테이지에 배치하여 불필요한 중복 작업을 제거
        3. 불필요한 이미지 레이어 생성 방지
          • Docker는 각 명령어(RUN, COPY, ENV, WORKDIR 등)를 실행할 때마다 새로운 이미지 레이어를 생성
          • builder프로덕션 스테이지 구별이 없으므로 불필요한 레이어 생성 → 이미지 전체에 영향
        •  

목차

 

도커 허브에 공유된 이미지 사용하기

레지스트리(registry)

  • 이미지를 제공하는 저장소
  • docer image pull (이미지 이름) 을 입력하면 필요한 이미지 중 로컬 컴퓨터에 없는 이미지를 도커 허브(레지스트리)에서 다운 받는다.
docker image pull diamol/ch03-web-ping

 

이미지 레이어?

 

위 그림에서 보면  docker image pull diamol/ch03-web-ping  를 실행해서 

하나의 이미지를 다운 받는데 여러 건의 파일을 다운받는 것을 볼 수 있다.

이미지는 여러 개의 작은 파일로 구성돼있다. 이 각각의 파일을 이미지 레이어라고 부른다.

 

내려받은 이미지로 컨테이너를 실행하고 실행된 애플리케이션의 기능을 확인

docker container run -d --name web-ping diamol/ch03-web-ping

애플리케이션 기능 확인 명령어 실행 결과

이 때 환경 변수에 있는 블로그 주소 blog.sixeyed.com 에 요청을 보내는 로그 를 확인 가능 

이 환경변수는 호스트 운영체제의 것을 가져오는 것이 아니라 도커가 기본값을 컨테이너에 적용하고 이 값을 애플리케이션에서 이용하는 방식이다.

 

- 환경 변수 변경하여 새로운 컨테이너 실행

docker rm -f web-ping
docker container run --env TARGET=google.com diamol/ch03-web-ping
  • --env TARGET=google.com : 애플리케이션에서 사용할 환경 변수 값을 지정 (구글에 요청을 보내도록 변경)

 

포인트!

- 도커 이미지는 설정값의 기본값을 포함해 패키징 된다. 컨테이너를 실행할 때 이 설정값을 바꿀 수 있게 구성해야한다.(환경변수 이용)

 

이미지에 포함된 기본 값 외의 다양한 설정값이 반영된 컨테이너 그림

 

호스크 컴퓨터는 고유한 환경 변수가 있다.

하지만,  컨테이너의  환경 변수는 도커가 부여한 환경 변수만 가진다. 

위의 그림에 보이듯이 같은 애플리케이션을 실행하고 있지만 주어진 환경 변수에 의해 동작이 달라지고 있다.

 

Dockerfile 작성하기

Dockerfile?

- 애플리케이션을 패키징하기 위한 간단한 스크립트.

- 일련의 인스트럭션으로 구성

- 어떠한 애플리케이션이라고 패키징 가능

예시

FROM diamlo/node

ENV TARGET="blog.sixeyed.com"
ENV METHOD="HEAD"
ENV INTERVAL="3000"

WORKDIR /web-ping
COPY app.js

CMD ["node","/web-ping/app.js"]
  • 인스트럭션 설명
    • FROM : 시작 지점을 지정
    • ENV : 환경 변수 값을 지정
      • INTERVAL 3000 = 3 초 마다 요청 전송
    • WORKDIR : 컨테이너 이미지 파일 시스템에 디렉터리를 만들고, 해당 디렉터리를 작업 디렉터리로 지정
    • COPY : 로컬 파일 시스템의 파일 혹은 디렉터리를 컨테이너 이미지로 복사 [원본경로] [복사경로]
      • 로컬 파일 시스템에 있는 app.js 파일을 이미지의 작업 디렉터리로 복사
    • CMD : 도커가 이미지로 부터 실행할 때 실행할 명령을 지정
      • node 로 애플리케이션을 시작하도록 app.js 를 지정

컨테이너 이미지 빌드하기

이미지를 빌드하기 위해 Dockerfile 스크립트, 이미지의 이름, 패키징에 필요한 파일의 경로를 지정

docker image build --tag web-ping (파일의 경로)
  • --tag의 인자값(web-ping) 은 이미지의 이름
  • 다음 인자는 파일의 경로 (도커에서는 이 디렉터리를 컨텍스트 라고함)
    • . 을 찍으면 현재 작업 디렉토리를 의미

빌드 후 이미지 확인 ( w 로 시작하는 모든 이미지 목록 출력)

docker image ls w*

 

이렇게 빌드된 이미지는 도커 허브에서 내려받은 이미지와 같은 똑같은 방식으로 사용할 수 있다.

도커 이미지와 이미지 레이어 이해하기

  • 도커 이미지는 이미지 레이어가 모인 논리적 대상이다.
  • 레이어는 도커 엔진의 캐시에 물리적으로 저장된 파일이다.

만약에 Nods.js 애플리케이션이 실행되는 컨텡너를 여러 개 실행한다면 이들 컨테이너는 모두 Nods.js 런타임이 들어 있는 이미지 레이어를 공유한다. 아래 그림에 해당 상황을 표현했다.

 

이 경우 이미지의 용량을 확인하는 방법

docker image ls

  - 출력되는 화면에서 SIZE 확인 ( 논리적인 용량)

  • REPOSITORY 에 있는 3 이미지는 모두 Node.js 기반 레이어를 공유
  • SIZE 에 있는 수치가 실제로 디스크 용량을 차지하는 크기 같지만, 이 수치는 이미지의 논리적 용량으로 공유된 레이어의 용량이 반영되지 않은 수치 ( 약 151 MB )
docker system df

- 출력되는 화면에서 Type Images 의 SIZE 확인 ( 이미지 캐시에 실제 사용된 디스크 용량)

  • 80.79 MB 가 이미지 레이어를 저장하는데  실제 사용된 디스크 용량이다.
  • 약 70MB 는 이미지 끼리 레이어를 공유하여 디스크 공간 절약

이미지 레이어 캐시를 이용한 Dockerfile 스크립트 최적화

 

도커는 캐시에 일치하는 레이어가 있는지 확인하기 위해 해시값을 이용한다.

 

해시

  • 입력값이 같은지 확인할 수 있는 일종의 디지털 지문
  • 해시값은 Dockerfile 스크립트의 인스트럭션과 인스트럭션에 의해 복사되는 파일의 내용으로 부터 계산
  • 기존 이미지 레이어에 해시값이 일치하는 것이 없다면 캐시 미스가 발생하고 해당 인스트럭션이 실행

한번 인스트럭션이 실행되면 그 다음에 오는 인스트럭션은 수정된 것이 없더라도 모두 실행

 ( 예시) Dockerfile 예시 (step 1 ~ step 7)

FROM diamlo/node

ENV TARGET="blog.sixeyed.com"
ENV METHOD="HEAD"
ENV INTERVAL="3000"

WORKDIR /web-ping
COPY app.js

CMD ["node","/web-ping/app.js"]

만약

step 1 ~ 5 ( FROM ... ~ WORKDIR/ web-ping)  변경이 없음  → 캐시에 저장된 것을 재사용

step 6      (COPY app.js)                                     변경(app.js 수정) → 다시 실행

step 7      (CMD).                                                  변경 없음 → step 4의 캐시를 재사용하지 못하여 다시 실행

 

Dockerfile 스크립트의 인스트럭션은 작성 시 숙지하면 좋은 사항들

  • 잘 수정하지 않는 인스트럭션을 앞에 배치
  • 자주 수정되는 인스트럭션이 뒤에 배치
  • 이유는 캐시에 저장된 이미지 레이어를 되도록 많이 재사용하기 위함

예시 Dockerfile 최적화 

FROM diamlo/node

CMD ["node","/web-ping/app.js"]

ENV TARGET="blog.sixeyed.com"\
    METHOD="HEAD" \
    INTERVAL="3000"

WORKDIR /web-ping
COPY app.js

  - 자주 수정되는 COPY 를 제일 뒤로 배치

 - ENV 는 하나의 인스트럭션으로 여러 개의 환경 변수를 정의 

연습문제 : Dockerfile 스크립트 없이 도커 이미지 빌드

이미지 위치 : diamol/ch03-lab
이미지 속 파일 : diamol/ch03.txt
문제 : 이미지 속 파일 뒤에 나의 이름을 추가한 다음 수정된 파일을 포함하는 새로운 이미지를 빌드
조건: Dockerfile 스크립트 사용 불가

 

ch03.txt 에 My name 입력 후 종료

  • -it ( --interactive --tty) : 이미지 안에서 터미널 켜기
  • --name : 컨테이너 이름 짓기 ( ch03ex)
  • diamol/ch03-lab : 이미지 이름

ch03ex 컨테이너에 있는 변경 사항을 커밋 하여 새로운 이미지 ex03 생성

  • docker commit (컨테이너 이름) (새로운 이미지 이름) : 컨테이너 이름에 있는 변경 사항을 저장하여 새로운 이미지 생성

생성된 이미지를 실행해서 변경사항 확인

 

목차

1번 구조 : 기본적인 NestJS 구조

참조

구조 특징

modules 폴더를 중심으로 모듈 단위로 코드를 관리합니다.
각 모듈은 컨트롤러, 서비스, 엔티티, 인터페이스 등의 파일을 함께 포함합니다.

사용 이유와 원리

  • 모듈화: 기능별로 디렉토리를 분리하여 각 기능이 독립적으로 동작할 수 있도록 설계합니다.
    • 예: user 모듈은 사용자 관련 로직만을 포함.
  • NestJS의 표준 권장사항 준수: NestJS의 모듈 기반 아키텍처 철학에 따라 관리와 확장이 용이.
  • 개발과 유지보수의 편의성: 각 모듈이 독립적이므로 특정 기능 변경 시 다른 모듈에 미치는 영향이 적음.

폴더 구성

src/
  ├── modules/
  │     ├── user/
  │     │     ├── user.controller.ts
  │     │     ├── user.service.ts
  │     │     ├── user.entity.ts
  │     │     └── user.dto.ts
  │     ├── auth/
  │     │     ├── auth.controller.ts
  │     │     ├── auth.service.ts
  │     │     └── auth.module.ts
  ├── app.module.ts
  ├── main.ts

 

2번 구조 : 클린 아키텍처(Clean Architecture) 적용

참조

구조 특징

애플리케이션의 비즈니스 로직을 중심으로 설계합니다.
의존성 방향을 안쪽으로 향하게 설계하여 핵심 도메인이 외부 프레임워크에 의존하지 않도록 구성합니다.

  • 의존성 방향을 안쪽으로 향하게?
    • 시스템의 고수준 모듈저수준 모듈 에 의존하지 않도록 하고, 대신 추상화를 통해 두 모듈이 서로 독립적으로 동작할 수 있게 설계하자는 내용
      • 고수준 모듈: 비즈니스 로직을 처리하는 서비스 클래스
      • 저수준 모듈: 데이터베이스, 외부 API 등 실제 구현을 담당하는 클래스
      • 고수준 모듈은 저수준 모듈의 구현을 알 필요 없이, 인터페이스나 추상화된 계약에 의존하여 기능을 수행

사용 이유와 원리

  • 독립성 유지:
    • 비즈니스 로직(application)과 기술 세부사항(infrastructure)을 분리.
    • 도메인 계층(domain)은 외부 의존성과 무관하게 설계.
  • 테스트 용이성:
    • 비즈니스 로직이 외부 의존성 없이 독립적으로 작성되어 테스트가 쉬움.
  • 확장성과 유지보수성:
    • 프레임워크나 ORM 변경이 요구되더라도 핵심 로직에 미치는 영향 최소화.
  • 클린 아키텍처 원칙 준수:
    • Use Case와 Entity를 중심으로 설계하여 애플리케이션 안정성 증대.

폴더 구성

src/
  ├── application/
  │     ├── use-cases/
  │     │     ├── create-user.use-case.ts
  │     │     └── find-user.use-case.ts
  ├── domain/
  │     ├── entities/
  │     │     ├── user.entity.ts
  │     └── repositories/
  │           └── user.repository.ts
  ├── infrastructure/
  │     ├── controllers/
  │     │     └── user.controller.ts
  │     ├── services/
  │     │     └── user.service.ts
  │     ├── orm/
  │           └── user.orm-entity.ts
  ├── app.module.ts
  ├── main.ts

3번 구조 : 프로젝트 전체를 계층 중심으로 분리

참조

구조 특징

이 구조는 대규모 프로젝트를 위한 설계로, 모듈을 기능 중심 (1번 구조)이 아닌 프로젝트 전체를 계층 중심으로 분리합니다.
코드를 기능(Feature)이 아닌 역할과 책임(Layer)을 기준으로 분리.
주요 계층은 API, Service, Repository, Entity로 나누고, 공통 유틸리티는 별도로 관리합니다.

사용 이유와 원리

  • 대규모 프로젝트 적합성:
    • 계층별로 분리하여 복잡한 프로젝트의 역할을 명확히 관리.
  • 유지보수 용이성:
    • 계층 독립성으로 변경 시 다른 계층에 미치는 영향을 최소화.
  • 재사용성:
    • 공통 코드를 별도로 관리하여 코드 중복 제거.
  • 규모 확장성:
    • 계층 구조 확장을 통해 아키텍처 안정성 확보.

폴더 구성

src/
  ├── api/
  │     ├── user/
  │     │     ├── user.controller.ts
  │     │     └── user.dto.ts
  ├── services/
  │     ├── user/
  │     │     └── user.service.ts
  ├── repositories/
  │     ├── user/
  │     │     └── user.repository.ts
  ├── entities/
  │     ├── user.entity.ts
  ├── common/
  │     ├── utils/
  │     │     ├── logger.util.ts
  │     │     └── validation.util.ts
  │     └── interceptors/
  ├── app.module.ts
  ├── main.ts

1번, 2번, 3번 구조 비교 요약

구조 유형 구조 설계 방식 주요 특징 장점 적합한 프로젝트
1번  모듈 중심 설계 - 기능별 모듈(user, auth 등) 단위로 구성
- 모듈 내에 컨트롤러, 서비스, 엔티티 등이 함께 포함됨
- NestJS의 표준 구조
- 간결하고 직관적
- 모듈 단위로 관리가 쉬움
소규모~중규모 프로젝트
2번  클린 아키텍처 - 비즈니스 로직 중심 설계
- 의존성 방향이 안쪽으로 향함
- Application, Domain, Infrastructure로 분리
- 비즈니스 로직과 기술 세부사항 분리
- 유지보수와 테스트 용이
- 핵심 도메인이 외부 의존성 없음
중규모~대규모 프로젝트
3번  계층 중심 설계
(Layer-Oriented Design)
- 역할(계층) 중심으로 코드 분리
- 주요 계층: API, Service, Repository, Entity
- 공통 유틸리티 별도 관리
- 코드 책임 분리 명확
- 대규모 프로젝트에 적합
- 확장성과 재사용성 극대화
대규모 프로젝트 및 팀 협업

추가 설명

  • 1번 구조는 NestJS의 기본 구조로 빠른 프로토타이핑과 간단한 프로젝트에 적합.
  • 2번 구조는 클린 아키텍처를 적용하여 비즈니스 로직 중심으로 설계하며, 기술 스택의 변경에 유연.
  • 3번 구조는 계층 중심으로 프로젝트를 조직화해 대규모 팀 프로젝트에서 효율적입니다.

+ Recent posts