헬스 체크를 지원하는 도커 이미지 빌드하기

도커는 컨테이너를 시작할 때마다 애플리케이션의 기본적인 상태를 확인한다.

  • 컨테이너를 실행하면 내부에서 실행되는 프로세스(앱 실행파일 | 닷넷 런타임 | 셸 스크립트 같은 특정한 프로세스)가 있는데 도커가 확인하는 것은 이 프로세스의 실행 사태다.
  • 만약 이 프로세스가 종료되면 컨테이너도 종료상태가 된다.

만약 웹 애플케이션이  '500 Internal Server Error' 응답 받고 중지되어도 컨테이너는 정상이라고 판단한다.

 

웹 앱이 중지되어도 컨테이너가 정상적으로 작동하는 예시

500 상태코드를 받은 웹애플리케이션 예시
컨테이너가 정상적으로 작동하고 있는 예시

컨테이너의 진입점 프로세스가 실행 중 상태이므로 도커는 애플리케이션도 정상상이라고 판단하여 STATUS 가 Up 인것을 확인할 수 있다.

 

Dokcerfile 에서 HEALTHCHECK 인스트럭션을 이용하면 인스트럭션에 정의된 정보를 이용해 동작 중인 애플리케이션의 상태가 정상인지 확인할 수 있다.

  • HEALTHCHECK 동작방식
    • 인스트럭션에 도커 컨테이너안에서 실행하는 명령을 지정
    • 이 명령이 반환하는 상태코드를 보고 애플리케이션의 상태를 파악
    • 도커는 일정간격으로 이 명령을 실행
      • 상태 코드가 연속으로 일정 횟수 이상 실패로 나오면 해당 컨테이너를 이상 상태로 간주

 

Dockerfile 스크립트의 HEALTHCHECK 인스트럭션 예시

FROM diamol/dotnet-aspnet

ENTRYPOINT ["dotnet", "/app/Numbers.Api.dll"]
HEALTHCHECK CMD curl --fail http://localhost/health

WORKDIR /app
COPY --from=builder /out/ .
  • ENTRYPOINT 에서 "dotnet" 명령을 실행 → 앱 상태를 확인하기 위해 모니터링하는 프로세스도 "dotnet"
  • HEALTHCHECK CMD curl
    • --fail  = 정상이면 0 이외에는 다른 숫자 반환하도록 하는 옵션
    • http://localhost/health = 버그가 발동했는지 확인하기 위한 또 다른 API 엔트포인트 (500:버그, 200:정상)

< HEALTHCHECK가 적용된 도커파일 사용해보기>

diamol/ch08/exercises/numbers 폴더로 가서 이미지 빌드

docker image build -t diamol/ch08-numbers-api:v2 -f ./numbers-api/Dockerfile.v2 .

 

실행시킨 앱의 건강상태 확인

docker run -d -p 8081:80 diamol/ch08-numbers-api:v2

docker container ls

STATUS : healthy

버그 생성

curl http://localhost:8081/rng
curl http://localhost:8081/rng
curl http://localhost:8081/rng
curl http://localhost:8081/rng

 

컨테이너 건강상태 체크 (90초 정도 시간이 지나고 나서 해야 상태가 변함, 3번 건강상태를 체크해야 하기 때문)

docker container ls

에러가 발생하여 건강상태가 unhealthy 로 변함

  • 3 번 연속 건상상태를 확인한 결과 실패하였기 때문에 컨테이너 상태가 이상(unhealthy)로 출력
  • 컨테이너 상태가 이상이여도 종료되지는 않음

최근 컨테이너의 상태를 출력하여 더 자세히 확인해 보자

docker container inspect $(docker container ls --last 1 --format '{{.ID}}')

건강상태는 unhealthy 인데 앱은 runnging 상태인 것을 알 수 있다.

 

왜 이상 상태에 있는 컨테이너를 재시작하거나 다른 컨테이너로 교체하지 않은 것일까?

→ 도커가 컨테이너 재시작/교체 작업을 안전하게 처리할 수 없기 때문

 

클러스터는 항상  컨테이너를 추가로 실행할 여력이 있기 때문에 이상상태를 보이는 컨테이너는 두고, 대체컨테이너를 실행해 앱을 중단 시간없이 상태를 회복가능

디펜던시 체크가 적용된 켄테이너 실행하기

여러 컨테이너에 나뉘어 실행되는 분산 애플리케이션은 이상이 생긴 컨테이너를 교체할 때 처음 앱을 실행할 때처럼 컨테이너 간 의존관계를 고려하지 않기때문에 문제를 격는다.

 

도커가 동작하는 서버가 한 대뿐이라면 웹컨테이너를 실행하기전에 API 컨테이너가 실행되도록 가능하다.

하지만, 클러스터 환경의 컨테이너 플랫폼이라면 컨테이너의 순서까지 통제할수가 없어서 API 가 사용 가능한 상태가 되기 전에 웹 앱이 실행되는 일이 있을 수도 있다.

 

(예시) 컨테이너 상태는 정상인데 핵심 의존 관계를 만족하지 않아 앱이 정상적으로 동작하니 않는 상황

방식 : 실행 중인 모든 컨테이너를 제거해 동작 중인API 컨테이너가 없게한 후 앱 컨테이너를 실행한 다음 웹 브라우저에서 앱에 접근한다. 

docker container rm -f $(docker container ls -aq)
docker container run -d -p 8082:80 diamol/ch08-numbers-web
docker container ls

 

컨테이너 실행 상태 : 컨테이너 실행 중, 앱도 이상이 없음
서버 프로세스가 실행 중이고 컨테이너도 정상이지만 API를 사용할 수 없는 상태여서 웹 앱도 제대로 동작하지 않음

 

이런 경우를 방지하기 위해

의존 관계를 만족하는지 점검하는 디펜던시 체크 기능도 도커 이미지에 추가할 수 있다.

 

디펜던시 체크

  • 앱 실행전에 필요한 요구사항을 체크
  • 명시된 모든 요구사항이  확인되면 디펜던시 체크가 성공하고 앱이 실행
  • 인스트럭션으로 구현 된것은 아니고 앱 실행 명령에 로직을 추가하는 방법으로 구현

디펜던시 체크가 구현된 예시 (CMD 인스트럭션에 구현)

FROM diamol/dotnet-aspnet

ENV RngApi:Url=http://numbers-api/rng

CMD curl --fail http://numbers-api/rng && \
    dotnet Numbers.Web.dll

   먼저 API가 사용가능한 상태라면 curl 명령이 성공하고 이어지는 닷넷 코어 앱 실행 명령을 실행

 

디펜던시 체크가 설정된 컨테이너 실행

docker container run -d -p 8083:80 diamol/ch08-numbers-web:v2

 

도커 컨테이너 상태 확인 ▶ 실행 중인 API 가 없어 v2 는 종료된 것을 확인 가능

 

애플리케이션 체크를 위한 커스텀 유틸리티 만들기

curl은 웹 애플리케이션이나 API 를 테스트하는데 매우 유용한 도구다.

  • 실무에서 개발하는 앱을 테스트하는 목적으로는 사용하지 않는다.
  • 이유 = 사용하지 않을 도구를 추가하면 
    • 이미지 크기증가
    • 외부 공격에 노출될 여지 증가

▶  앱 체크에는 앱과 같은 언어로 구현된 별도의 커스텀 유틸리티를 사용

 

앱과 같은 언어로 구현된 커스텀 유틸리티의 장점

  • 이미지에 추가적인 소프트웨어를 포함 시킬 필요가 없다.
  • 재시도 횟수나 분기등 셸 스크립트로는 표현하기 까다로운 복잡한 체크 로직을 적용할 수 있다. 특히 리눅스와 윈도 양쪽에서 사용할 크로스 플랫폼 이미지라면 더욱 유용
  • 앱과 같은 설정을 사용해 대상 URL을 여러 곳에 반복 정의하거나 수정에서 누락시키는 일을 방지할 수 있다.
  •  컨테이너 실행 전에 확인이 필요한 모든 사항을 검증할 수 있다. (앱과 같은 라이브러리 환경에서 데이터베이스 접속이나 인증서 파일의 존재 유무등)
  • 다양한 상황에서 동작이 가능하다

 

닷넷 코어로 구현한 간단한 HTTP 테스트 유틸리티를 사용해 API 이미지의 헬스 체크와 웹 이미지의 디펜던시 체크 앱 빌드 과정

 마지막 단계 코드

FROM diamol/dotnet-aspnet
ENTRYPOINT ["dotnet", "Numbers.Api.dll"]
HEALTHCHECK CMD ["dotnet", "Utilities.HttpCheck.dll", "-u", "http://localhost/health"]

WOKDIR /app
COPY --from=http-check-builder /out/ .
COPY --from=builder /out/ .

HEALTHCHECK 에서 curl 대신 닷넷 코어로 구현된 테스트 유틸리티를 사용

 

<실습> 모든 컨테이너 삭제후 무작위 숫자 API를 v3 버전의 컨테이너로 실행한다. 이번에는 헬스 체크 간격을 조금 줄인다. 컨테이너의 상태가 정상인지 확인하고 API를 몇번 호출해 상태가 이상으로 바뀌는지 확인하라.

# 기존 컨테이너를 모두 삭제
docker container rm -f $(docker container ls -aq)
      -a  = 모든 컨테이너(중지된 컨테이너 포함)를 조회합니다.
      -q = 컨테이너 ID만 출력
      -aq = 모든 컨테이너의 ID를 출력 
      $(...) = 명령어 치환으로  ( )에서 얻은 결과를 가져온다. 

# API를 v3 버전의 이미지로 실행한다.
docker container run -d -p 8080:80 --health-interval 5s diamol/ch08-numbers-api:v3

# 5초 정도 기다린 후 컨테이너 목록을 확인한다.
docker container ls

# API 를 네번 호출 - 세번은 성공, 마지막은 실패
curl http://localhost:8080/rng

# 앱에 버그가 발생했다. 15초 기다린 후 상태가 이상으로 바뀌는지 확인한다.
docker container ls

 

커맨드 실행 출력 내용

4번 째 요청 부터 버그가 발생 → HTTP 테스트 유틸리리티가 세 번 연속 실패를 반환 → unhealthy 상태로 변환

 

<실습> 웹 앱 버전 v3를 실행하라. API 가 없으므로 컨테이너가 바로 종료된다.

docker container run -d -p 8081:80 diamol/ch08-numbers-web:v3
docker container ls --all

 

diamol/ch08-numbers-web:v3 : 웹 앱 컨테이너의 디펜던스 체크가 실패해 컨테이너가 종료

diamol/ch08-numbers-api:v3   : api 컨테이너가 실행 중이지만 컨테이너 이름이 "number-api" 로 지정되지 않아서 웹 앱이 API                                                                컨테이너를 발견하지 못한다.

 

도커 컴포즈에 헬스 체크와 디펜던시 체크 정의하기

디펜던시 체크에 실패했을 때 실행하던 컨테이너를 종료해야 하는 이유

▶  단일 서버에서 앱을 실행 중이라면 이상이 생긴 컨테이너를 새 컨테이너로 교체하면 더 심각한 장애를 일으킬 수 있기 때문

 하지만 종료된 컨테이너를 재시작하거나 이미지에 정의 되지 않은 헬스 체크를 추가할 수는 있다.

 

도커 컴포즈 파일에서 헬스 체크 옵션 설정하기

number-api:
	image: diamol/ch08-numbers-api:v3
    ports:
    	- "8087:80"
    healthCheck:
    	interval: 5s
        timeout: 1s
        retries: 2
        start_period: 5s
    networks:
    	- app-net

 

이미지에 헬스 체크가 정의 되어 있지 않은 경우 컴포즈 파일에서 정의하는 법

numbers-web:
    image: diamol/ch08-numbers-web:v3
    restart: on-failure
    environment:
      - RngApi__Url=http://numbers-api/rng
    ports:
      - "8088:80"
    healthcheck:
      test: ["CMD", "dotnet", "Utilities.HttpCheck.dll", "-t", "150"]
      interval: 5s
      timeout: 1s
      retries: 2
      start_period: 10s
    networks:
      - app-net

 

<실습> 지금 있는 컨테이너를 모두 삭제하고 도커 컴포즈를 이용해 무작위 숫자 애플리케이션을 실행하라. 애플리케이션이 제대로 실행됐는지 알아보기 위해 실행 후 컨테이너 목록을 확인하라

 

# 컴포즈 파일이 있는 디렉터리로 이동
 
# 현재 컨테이너를 모두 삭제
docker container rm -f $(docker container ls -aq)
 
# 애플리케이션 실행
docker-compose ls
 
# 5초를 기다린 다음 컨테이너 목록을 확인
docker container ls
 
# 웹 애플리케이션 로그도 확인
docker container logs numbers-numbers-web-1

 

도커 컴포즈 파일에 depends_on 설정을 사용해 직접 디펜던시 체크를 하도록 하지 않는 이유

▶ 도커 컴포즈가 디펜던시 체크를 할 수 있는 범위가 단일 서버로 제한되기 떄문

헬스 체크와 디펜던시 체크로 복원력있는 애플리케이션을 만들 수 있는 이유

물리 서버가 한 대뿐인 환경이라면 도커 컴포즈에 웹 컨테이너보다 API 컨테이너를 먼저 실행시키라고 지시할 수 있다.

 

10 대의 물리 서버와 20개의 API 컨테이너와 50 여개 웹앱을 실행해야 한다면 어떻게 이 애플리케이션의 시작 절차를 설계해야할까?

---> API 컨테이너를 먼저 실행시킨 후 웹앱을 실행하도록 설계를 한 경우, 마지막 1 개의 API 가 실행이 늦어져 5분이나 걸리면 웹앱은 그 동안 하나도  실행되지 않아서 애플리케이션이 동작 중이라고 할 수가 없다.

  ▶ 여기서 문제는 API 컨테이너가 부족하더라도 웹 앱 컨테이너를 실행하는데는 문제가 없다.

 

!! 디펜던시 체크와 헬스 체크를 도입하면 보이는 효과

  • 플랫폼이 실행 순서를 보장하게 할 필요가 없다.
  • 가능한 빨리 컨테이너를 실행하면 된다.
  • 일부 컨테이너가 의존관계를 만족하지 못한 상태라면 재실행되거나 다른 컨테이너로 교체된다.

운영 환경의 클러스터에서 이러나느 컨테이너의 생애주기

*애플리케이션의 자기수복 :  일시적인 오류를 플래폼이 해소해 주는 것 

      앱에 메모리 누수를 일으키는 까다로운 버그가 있더라도 플랫폼에서 해당 컨테이너를 메모리를 잃지 않은 새 컨테이너로 대체 - 버그를 수정하지 않았지만 애플리케이션은 계속 동작할 수 있다. 

 

헬스 체크와 디펜던시 체크에 주의 할 점

  • 헬스 체크는
    • 주기적으로 실행되므로 시스템에 부하를 주는 내용이어서는 안 된다.
    • 자원을 너무 많이 소모하지 않으면서 앱이 실질적으로 동작 중인지 검증할 수 있는 핵심적인 부분을 테스트해야 한다.
  • 디펜던시 체크는
    • 앱 시작 시에 한 번만 실행되므로 테스트 대상이 빠짐없이 정확하도록 주의해야 한다.
    • 누락된 의존 관계가 있으면 이 문제를 플랫폼이 해결하지 못하면 앱에도 문제가 발생한다.

연습문제 : 메모리 누수 대처

조건

  • 애플리케이션 시작 시 충분한 메모리가 있는지 확인하고, 메모리가 부족한 경우 컨테이너를 종료한다
  • 애플리케이션 실행 중 4 초 간격으로 최대치를 초과해 메모리를 사용하는지 확인한다. 최대치를 초과했다면 해당 컨테이너의 상태를 이상으로 판정해야 한다.
  • 테스트 로직은 memory-check.js 스크립트에 이미 작성돼 있다. Dockerfile 스크립트에서 테스트 스크립트를 그대로 사용하면 된다.
  • 테스트 스크립트와 Dockerfile 슼크립트는 ch08/lab 디렉터리에 있다.

Dokerfile 스크립트

FROM diamol/node

ENV MAX_ALLOCATION_MB=4096 \
    LOOP_ALLOCATION_MB=512 \
    LOOP_INTERVAL_MS=2000

CMD ["node", "memory-hog.js"]

WORKDIR /app
COPY src/ .

 

 

Dockerfile solution

더보기

FROM diamol/node

ENV MAX_ALLOCATION_MB=4096 \
    LOOP_ALLOCATION_MB=512 \
    LOOP_INTERVAL_MS=2000

CMD node memory-hog.js && \

          node memory-check.js

 

HEALTHCHECK --interval=5S \

  CMD node memory-check.js

WORKDIR /app
COPY src/ .

 

문제 풀면서 틀린 부분

 

CMD ["node", "memory-hog.js", "memory-check.js"] 

  • 형태: JSON 배열 방식
  • 실행: node memory-hog.js memory-check.js 로 실행
  • 동작: node 명령어에 두 파일이 인자로 전달
  • 결과: node는 첫 번째 파일(memory-hog.js)만 실행하고, 두 번째 인자(memory-check.js)는 실행되지 않고 단순히 인자로 전달
  • 의도: 만약 memory-hog.js가 다른 스크립트를 받아서 실행하도록 설계되었다면 정상 동작하지만, 그렇지 않다면 memory-check.js는 무시됨.

 

 

도커 컴포즈 파일의 구조

도커 컴포즈 파일?

  • 모든 컴포넌트가 실행 중일 때 애플리케이션이 어떤 상태여야 하는지를 기술하는 파일
  • docker container run 명령으로 컨테이너를 실행할때 지정하는 모든 옵션을 한데 모아 놓은 단순한 형식의 파일

* 컨테이너 : 실제로 실행되고 있는 도커 인스턴스. 예를 들어, 웹 서버 (예: Nginx)가 실행되고 있는 컨테이너, 애플리케이션 서버(“Node.js 앱)이 실행되고 있는 컨테이너. 즉 웹 서버, 애플리케이션 서버, 데이터베이스 각각이 하나의 컨테이너로 실행되고 있는 것. 

 

* 서비스 : 각 컨테이너는 하나의 역할을 담당하지만, 도커 컴포즈에서는 같은 역할을 하는 컨테이너들을 "서비스"라는 논리적인 단위로 묶어서 관리

 

도커 컴포즈 파일의 전체 스크립트.( to-do 애플리케이션을 실행하는 내용)

version: '3.7'

services:
  
  todo-web:
    image: diamol/ch06-todo-list
    ports:
      - "8020:80"
    networks:
      - app-net

networks:
  app-net:
    external:
      name: nat

 - YAML 문법으로 기술 , YAML 문법은 들여쓰기를 통해 구조를 정의하므로 들여쓰기가 중요

 

도커 컴포즈 파일의 구조

  • version
    • 파일에 사용된 도커 컴포즈 파일 형식의 버전을 나타낸다 : 버전마다 문법과 표현 가능 요소에 많은 변화가 있었으므로 버전 지정 필수
      • 버전을 포함하여 실행하면 warnning messge 가 발생하지만 무시해도 된다.
        • the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
  • networks
    • 서비스 컨테이너가 연결될 모든 도커 네트워크를 열거하는 부분
  • services
    • 애플리케이션을 구성하느 모든 컴포넌트를 열거하는 부분
    • 도커 컴포즈에서는 실제 컨테이너 대신 서비스 개념을 단위로 삼는다. 하나의 서비스를 같은 이미지로 여러 컨테이너에서 실행할 수 있기 때문이다.
      • 컨테이너 대신 서비스 개념을 단위로 삼는 이유
        • 도커 컴포즈는 단순히 하나의 컨테이너만 다루는 게 아니라, 여러 컨테이너를 조합해서 큰 애플리케이션을 구성하도록 설계되어 있기 때문
        • 하나의 서비스에 같은 역할(웹서버 | 데이터베이스 | 애플리케이션 서버)을 하는 여러 컨테이너를 하나의 서비스로 묶어서 관리가 가능하기 때문
      • 같은 이미지를 여러 컨테이너에서 실행한다는 뜻
        • 서비스의 설정에서 replicas(복제본) 옵션을 사용하면 같은 이미지를 사용하는 컨테이너 여러 개를 실행 가능
          • 예시
services:
  web:
    image: my-web-app:latest
    deploy:
      replicas: 3

 

왜 여러 컨테이너를 묶는 걸까?  → 확장성(Scalability) 때문

 

트래픽이 많은 웹 서버를 운영해야 한다고 가정

  • 하나의 컨테이너만 실행하면 그 컨테이너가 모든 요청을 처리해야 하므로 과부하가 생길 수 있다.
  • 이를 해결하기 위해 같은 이미지를 사용하는 여러 컨테이너를 실행해서 트래픽을 분산 처리해야 한다.

▶ 이때, 도커 컴포즈에서 같은 역할을 하는 여러 컨테이너를 하나의 "서비스"로 묶어서 관리하면 쉽고 효율적

 

 

도커 컴포즈 파일의 전체 스크립트 중 아래의 부분이 어떻게 구성이 되는지를  보여주는 그림

services:
  
  todo-web:
    image: diamol/ch06-todo-list
    ports:
      - "8020:80"
    networks:
      - app-net

애플리케이션이 어떤 리소스로 어떻게 구성되는지 나타낸 그림

① todo-web 이라는 서비스는  diamol/ch06-todo-list 이미지로부터 단일 커테이너로 실행

② 이 컨테이너는 호스트 컴퓨터의 80번 포트로 자신의 8020번 포트를 공개

③ app-net 이라는 이름의 도커 네트워크에 연결

 

최종적인 결과는 아래의 명령을 실행한 것과 같은 상태가 된다.

docker container run -p 8020:80 --name todo-web --network nat diamol/ch06-todo-list

 

네트워크 부분 내용 설명

networks:
  app-net:
    external:
      name: nat

    nat 이라는 이름의 외부 네트워크로 연결

    external =  nat 네트워크가 이미 존재하므로 새로 생성하지 말라는 뜻

 

 

도커 컴포즈 파일 실행 방법

docker network create nat

cd (ch07/exercises/todo-list 가 있는 경로)

docker-compose up

Container todo-list-todo-web-1  Cre...    ← 현재 있는 리소스와 애플리케이션을 구성하는 리소스를 비교해 더 필요한 요소를 생성

Attaching to todo-web-1 아래는 ←  to-do 애플리케이션 시동 로그

 

도커 컴포즈를 사용해 여러 컨테이너로 구성된 애플리케이션 실행하기

4장에서 오늘의 천문 사진을 보여주는 애플리케이션은 여러가지 언어를 이용하여 구현된 분산 애플리케이션이다.

  • 웹 프런트엔드 = 자바 (image-gallery)
  • 로그 수집 모듈 = Node.js (accesslog)
  • REST API  =  GO (iotd)

 

4 장에서는 분산 애플리케이션을 실행한 과정

1. 차례로 이들 컨테이너를 실행 시켜서 애플리케이션을 가동

2. 모든 컨테이너를 동일한 도커 가상 네트워크에 미리 약속된 이름으로 접속시켜 애플리케이션의 구성 요소가 서로 통신할 수 있도록 구성

 

4 장에서 실행한 분산 애플리케이션을 실행하기 위한 도커 컴포즈 스크립트

accesslog:
	image: diamol/ch04-access-log

iotd:
	image: diamol/ch04-image-of-the-day
    ports:
    	- "80"

image-gallery:
	image: diamol/ch04-image-gallery
    ports:
    	- "8010:80"
    depends_on:
    	- accesslog
        - iotd
  • accesslog = 설정값과 포트가 필요없으므로 이미지 이름만 기술
  • iotd = REST API 이므로 포트 번호설정
  • image-gallery = depends_on 을 실행하여 의존성을 선언.
    • image-gallery 서비스를 실행하기 전에, 이 의존성을 만족하기 위해 iotd, accesslog 를 먼저 실행하여 시도함.

 

위 애플리케이션의 아키텍쳐를 나타낸 그림

* 참고로 이 그림은 도커 컴포즈 스크립트를 다이어그램으로 변환해 주는 도구를 사용해 만든것 

( https://github.com/pmsipilot/docker-compose-viz )

 

 

<분리 모드 (detachede mode)로 애플리케이션을 실행하기>

(docker-compse 파일이 있는 폴더로 이동한 후)

docker-compose up --detach

 - 컨테이너는 백그라운드에서 실행

- 로그를 확인하려면 명령어를 별도로 실행      docker-compose logs <service_name>

 

실행 한 결과

http://localhost:8010/ 에 접속하면 4장에서 실행한 애플리케이션과 동일하게 작동

 

< 도커 컴포즈를 사용해 iotd 서비스의 컨테이너 수를 늘려보기>

이점 : 컴포즈 파일을 이용해여 여러 개의 컨테이너로 구성된 애플리케이션을 마치 한 덩어리 처럼 다룰 수 있음.

특히 API 서비스는 상태가 없으므로 컨테이너를 늘리는 방법으로 스케일 아웃할 수 있다. 즉, 웹 컨테이너가 API에 데이터를 요청하면 도커가 여러 개의 API 컨테이너에 이 요청을 고르게 분배

 

docker-compose up -d --scale iotd=3

명령 실행 후 출력된 결과

이제 http://localhost:8010/ 페이지를 여러 번 새로고침을 해보자

docker-compose logs --tail=1 iotd

   --tail=1 : 각 iotd 컨테이너의 제일 마지막 로그만 출력

출력 된 내용을 보면 새로고침 요청을 늘어난 3개의 컨테이너가 고르게 나눠 처리한 것을 볼수 있다.

 

docker-compose ls

 

도커 컴포즈를 이용하여 재시작

// 애플리케이션 중지, 컴포즈 리소스와 컨테이너 삭제
docker-compose down 

// 컴포즈 파일에 정의된 대로 리소소를 다시 생성, 애플리케이션 재시작
docker-compose up -d

docker-compose ls

 

앞에서 iot를 3개로 확장했던 것이 사라진 것을 running(3) 에서 알수 있다.

 

< 참고 > 도커 컴포즈는 YAML 파일에 정의된 애플리케이션 정의에 의존하는 클라이언트 측 도구임을 잊어서는 안된다.

 

도커 컨테이너간의 통신

애플리케이션 생애주기동안에 컨테이너가 교체되면 IP 주소도 변경된다.

IP 주소가 변경돼도 문제가 없도록 도커에서 DNS 를 이용해 서비스 디스커버리 기능을 제공한다.

 

<iot를 3개로 확장한 후 DNS 조회 : nslookup>

docker-compose up -d scale --iotd=3

docker container exec -it image-of-the-day-image-gallery-1 sh
/web # nslookup aceeslog

  accesslog 서비스를 DNS에 조회한 결과 해당 컨테이너의 IP 주소가 조회된 것을 볼 수 있다.

 

도커 네트워크에 연결된 모든 컨테이너는 이 네트워크의 범위에 포함되는 IP 주소를 부여 받는다. 

그리고 이 네트워크를 통해 컨테이너 간 통신이 가능하다.

▶ DNS 조회를 사용하면 컨테이너가 교체돼 IP 주소가 변경되더라도 항상 새로 만들어진 컨테이너에 접근할 수 있다.

 

< accesslog 컨테이너 삭제 후 애플리케이션 재실행할때 컴포즈의 행동 확인 > 

만약 도커 명령행에서 accesslog 컨테이너를 직접 삭제하고 도커 컴포즈로 애플리케이션을 재실행할 때

1. accesslog 컨테이너가 없으므로 새로운 accesslog 컨테이너를 생성

2. 애플리케이션 재실행

▶ DNS 조회하면 새로운 IP 주소가 할당된것을 알 수 있다.

 

accesslog 컨테이너 삭제

docker container rm -f image-of-the-day-accesslog-1

삭제 후 DNS 확인한 결과

docker-compose up -d --scale iotd=3

을 실행한 후 결과

accesslog 컨테이너가 없으므로 새롭게 accesslog 컨테이너를 생성하여 대응

 

 DNS 확인한 결과

DNS 조회 결과를 보면 accesslog 서비스에 새로 배정된 컨테이너가 기존 컨테이너의 IP를 그래도 유지했다.

이유) 기존 컨테이너가 삭제되면서 부여됐던 IP 주소도 재사용이 가능해졌기 때문

 

도커 컴포즈로 애플리케이션 설정값 지정하기

소규모 프로젝트라면 어떤 데이터 베이스를 사용하더라도 문제가 없다.

일정 규모 이상의 애플리케이션이라면 별도의 데이터베이스를 사용하는 것이 낫다.

이번에는 SQLite 데이터베이스를 사용하는 to-do 앱을  원격 컨테이너에서 동작하는 PostgreSQL 데이터베이스를 이용해보자.

 

SQLite → PostgreSQL

PostgreSQL을 사용하는 to-do 앱의 서비스를 정의한 컴포즈 파일

services:
  todo-db:
    image: diamol/postgres:11.5
    ports:
      - "5433:5432"
    networks:
      - app-net

  todo-web:
    image: diamol/ch06-todo-list
    ports:
      - "8030:80"
    environment:
      - Database:Provider=Postgres
    depends_on:
      - todo-db
    networks:
      - app-net
    secrets:
      - source: postgres-connection
        target: /app/config/secrets.json

networks:
  app-net:

secrets:
  postgres-connection:
    file: ./config/secrets.json
  • todo-db 에서 사용할 데이터벵스 PostgresSQL 설정
  • todo-wb
    • environment 에서 사용할 데이터베이스 지정
    • depends_on 에서 todo-db 연결 (의존성 연결)
    • secrets:
      • postgres-connection 에 설정된 비밀값을  secrets.json 에 기록
  • secrets
    • postgres-connection 이름으로 로컬에서 비밀값을 읽어오는 부분

위의 도커컴포즈 파일이 있는 폴더에서 실행했을 때 출력된 내용

docker-compose up -d

 

Tip. docker-compose 로 실행한 애플리케이션 만 보는 명령어

docker-compose ps

 

도커 컴포즈도 만능은 아니다.

docker-compose 파일을 실행하면 내가 설정한 상태로 애플리케이션을 실행할 수 있다.

하지만, 애플리케이션이 지속적으로 정의된 상태를 유지하도록 하는 기능이 없다.

→ 일부 컨테이너가 오류를 일으키거나 강제로 종료되면 docker-compose up 명령을 다시 실행시켜야 애플리케이션의 상태를 원대로 되돌아 간다.

 

도커 컴포즈를 사용하기 적합한 시기를 나타내는 그림

  • 자신의 컴퓨터에서 앱을 실행하고 e2e 테스트를 수행하는데 사용
  • 지속적 통합 프로세스 중 빌드 및 자동화된 테스트에 컴포즈를 사용해 앱을 실행
  • 단일 서버에서 컴포즈를 실행해 테스트를 진행하며 테스트 환경을 최소한으로 유지
  • 운영환경에는 컴포즈보다는 도커 스윔이나 쿠버네티스를 사용, 하지만 애플리케이션 정의에는 컴포즈 파일을 포맷을 사용

연습문제: 도커 컴포즈를 이용하여 애플리케이션을 좀 더 신뢰성 있게 실행하기

 

조건

  • 호스트 컴퓨터가 재부팅되거나 도커 엔진이 재시작되면 애플리케이션 컨테이너도 재시작되도록 하라.
  • 데이터베이스 컨테이너는 바인드 마운트에 파일을 저장해 애플리케이션을 재시작하더라도 데이터를 유지할 수 있도록 하라.
  • 테스트를 위해 웹 애플리케이션은 80 번 포트를 주시하도록 하라.

참고

https://docs.docker.com/reference/compose-file/

 

Compose file reference

Find the latest recommended version of the Docker Compose file format for defining multi-container applications.

docs.docker.com

 

정답?

더보기

version: "3.7"

services:
  todo-db:
    image: diamol/postgres:11.5
    restart: unless-stopped
    environment:
      - PGDATA=/var/lib/postgresql/data
    volumes:
      - type: bind
        source: /data/postgres
        target: /var/lib/postgresql/data
    networks:
      - app-net

  todo-web:
    image: diamol/ch06-todo-list
    restart: unless-stopped
    ports:
      - "8050:80"
    environment:
      - Database:Provider=Postgres
    depends_on:
      - todo-db
    secrets:
      - source: postgres-connection
        target: /app/config/secrets.json

secrets:
  postgres-connection:
    file: postgres-connection.json

networks:
  app-net:
    external:
      name: nat

설명

더보기

restart: unless-stopped

위 설정의 동작

* 컨테이너가 비정상적으로 종료될 경우 자동 재시작

* 사용자가 컨테이너를 수동으로 중지한 경우에는 재시작하지 않음

* 서버 재부팅 시 다시 시작

-> 비정상 종료되거나 서버 재부팅 시 다시 시작하지만, 수동으로 중지된 경우는 재시작하지 않음. 

 

 

 

   volumes:
      - type: bind
        source: /data/postgres
        target: /var/lib/postgresql/data

 

-> bind 를 이용하여 데이터를 보존

source : 호스트 컴퓨터에서 데이터를 저장하는 경로

target : 컨테이너 내부 경로, 

 

컨테이너의 애플리케이션은 target 디렉토리에 데이터를 저장하지만, 실제로 그 데이터는 호스트의 source 경로에 저장

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

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

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

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

 

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

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프로덕션 스테이지 구별이 없으므로 불필요한 레이어 생성 → 이미지 전체에 영향
        •  

+ Recent posts