운영환경에서는 도커 스웜이나 쿠버네티스 같은 컨테이너 플랫폼상에서 애플리케이션을 실행하게 될텐데, 플랫폼은 애플리케이션이 스스로 이상에서 회복할 수 있도록 하는 기능을 제공한다.
플랫폼이 컨테이너에서 실행중인 애플리케이션 상태가 정상인지 확인할 수 있는 정보를 이미지에 함께 패키징할 수 있다.
이런 방법으로 애플리케이션이 정상적으로 동작하지 않으면 플랫폼이 비정상 컨테이너를 삭제하고 새 컨테이너로 대체한다.
도커는 애플리케이션의 상태가 실제로 정상인지 확인할 수 있는 정보를 도커 이미지에 직접 넣을 수 있는 기능을 제공한다.
Dockerfile 스크립트에 상태 확인을 위한 로직을 추가하면 된다.
헬스 체크 로직이 없는 상태에서 생길 수 있는 문제를 먼저 체험해보자.
#API 컨테이너를 실행한다.
docker container run -d -p 8080:80 diamol/ch08-numbers-api
#API를 세 번 호출한다 - 각 호출마다 무작위 숫자가 반환된다.
curl http://localhost:8080/rng
curl http://localhost:8080/rng
curl http://localhost:8080/rng
# 네 번째 부터 API 호출이 실패한다.
curl http://localhost:8080/rng
#컨테이너의 상태를 확인한다.
docker container ls
Dockerfile에서 HEALTHCHECK 인스트럭션
FROM diamol/dotnet-sdk AS builder
WORKDIR /src
COPY src/Numbers.Api/Numbers.Api.csproj .
RUN dotnet restore
COPY src/Numbers.Api/ .
RUN dotnet publish -c Release -o /out Numbers.Api.csproj
# app image
FROM diamol/dotnet-aspnet
ENTRYPOINT ["dotnet", "/app/Numbers.Api.dll"]
HEALTHCHECK CMD curl --fail http://localhost/health
WORKDIR /app
COPY --from=builder /out/ .%
HEALTHCHECK 에서 사용된 명령은 curl 명령으로, 컨테이너 내부에서 실행된다. URL /health는 버그가 발동했는지 확인하기 위한 또 다른 API 엔드포인트다. 만약 버그가 발동했다면 500 'Internal Server Error'가 반환되고 정상이라면 200 'OK' 가 응답으로 반환된다.
헬스체크 시에 엔드포인트 /health로 HTTP 요청을 보내는데, 이 엔드포인트의 응답은 애플리케이션 상태의 정상 여부다. --fail 옵션을 붙이면 curl이 전달받은 상태 코드를 도커에 전달한다.
요청이 성공하면 curl이 0을 반환하고 실패하면 0이외의 숫자를 반환한다. 도커는 0을 헬스 체크 정상, 0이외의 값을 비정상으로 간주한다.
Dockerfile 스크립트의 파일명이 다르고 파일이 위치한 경로도 다를 경우 이미지 빌드
일반적으로 Dockerfile 스크립트의 파일명은 Dockerfile이고, 도커는 이 이름을 가진 파일을 찾아 빌드를 시도한다. 그러나 이번에는 Dockerfile 스크립트의 파일명이 다르고 파일이 위차한 경로도 다르다. 이 상태에서 이미지를 빌드하려면 build 부명령에 Dockerfile 스크립트의 정확한 위치와 파일명을 지정해야한다.
# 예제 코드의 최상위 디렉터리로 이동하면 하위 폴더와 Dockerfile 스크립트 파일이 있다.
cd ./ch08/exercises/numbers
# -f 옵션을 붙여 Dockerfile 스크립트 파일의 경로를 지정한다.
docker image build -t diamol/ch08-numbers-api:v2 -f ./numbers-api/Dockerfile.v2 .
이 이미지 빌드가 끝나면 헬스 체크 기능을 갖춘 애플리케이션을 실행할 수 있다. 여기에 더해 헬스 체크 간격과 애플리케이션의 상태를 이상으로 간주하는 누적 실패 횟수도 설정할 수 있다. 기본 값은 30초 간격으로 연속 3회 이상 실패하면 애플리케이션이 이상 상태로 간주된다.
# 버전 v2 이미지로 API 컨테이너를 실행하라
docker container run -d -p 8081:80 diamol/ch08-numbers-api:v2
# 30초 정도 기다린 다음 컨테이너 목록을 확인한다.
docker container ls
# API를 네 번 호출한다. 처음 세번은 무작위 숫자를 반환하고 네 번째는 실패한다.
curl http://localhost:8081/rng
curl http://localhost:8081/rng
curl http://localhost:8081/rng
curl http://localhost:8081/rng
# 애플리케이션이 이상 상태에 빠졌다. 90초를 기다려 도커가 이상 상태를 감지하는지 확인한다.
docker container ls
세번 연속 헬스 체크 결과가 실패했기 때문에 컨테이너의 상태가 이상(unhealthy)으로 나온다. 하지만 컨테이너는 여전히 실행중이다. 이상이 발생한 컨테이너라고 해서 도커가 종료시키지는 않기 때문이다.
# 가장 최근에 컨테이너의 상태 출력
docker container inspect $(docker container ls --last 1 --format '{{.ID}}')
🤔 💭 애플리케이션이 이상 상태임에도 컨테이너의 상태는 여전히 실행 중이라고 나온다. 왜 이상 상태에 있는 컨테이너를 재시작하고나 다른 컨테이너로 교체하지 않은 것일까?
도커 엔진은 단일 서버에서 동작하는데, 이상이 생긴 컨테이너를 도커가 중지하고 재시작할 수는 있지만 그 시간 동안에 애플리케이션이 동작하지 않는다. (데이터 유실)
도커 입장에서는 이상 상태를 보이는 컨테이너를 교체하는 작업을 직접 수행했을 때 상황을 더 악화시키지 않을 것이라는 보장이 없으므로, 이상 상태 발생을 통보만 할 뿐 컨테이너는 그대로 둔다.
도커가 동작하는 여러 대의 서버로 구성되고 도커 스웜이나 쿠버네티스가 관리하는 클러스트 환경에서는 헬스 체크 기능이 더욱 유용하다. 클러스터는 컨테이너를 추가로 실행할 여력이 항상 있기 때문에 이상 상태를 보이는 컨테이너를 그대로 두고 대체 컨테이너를 실행해 애플리케이션의 중단 시간 없이 상태를 회복할 수 있다.
여러 컨테이너에 나뉘어 실행되는 분산 애플리케이션은 다른 문제를 겪을 수 있다. 이상이 생긴 컨테이너를 교체할 때는 처음 애플리케이션을 실행할 때 처럼 컨테이너 간 의존 관계를 고려하지 않기 때문이다.
우리가 예제로 삼았던 무작위 숫자 API도 웹페이지가 딸려 있었다. 이 웹 애플리케이션은 API와는 별도의 컨테이너에서 실행돼 API를 호출해 생성한 무작위 숫자를 제공한다.
도커가 동작하는 서버가 한 대뿐이라면 웹 컨테이너를 실행하기 전에 API 컨테이너가 샐행되도록 보장할 수 있지만
클러스터 환경의 컨테이너 플랫폼이라면 컨테이너의 실행 순서까지 통제할 수 없다.
그래서 API가 사용 가능한 상태가 되기 전에 웹 애플리케이션이 실행되는 일이 있을 수 있다.
# 실행 중인 모든 컨테이너를 제거해 동작 중인 API 컨테이너가 없게 한다.
docker container rm -f $(docker container ls -aq)
# 웹 애플리케이션 컨테이너를 실행한 다음 웹 브라우저에서 애플리케이션에 접근한다.
# 웹 애플리케이션은 API를 사용해 무작위 숫자를 생성한다. 그러나 웹 애플리케이션이 실행될 때 API가 사용 가능한 상태인지 확인하지 않는다.
docker container run -d -p 8082:80 diamol/ch08-numbers-web
# 컨테이너가 실행중이고 애플리케이션 상태가 정상임에도 제대로 동작하지 않는다.
docker container ls
애플리케이션 중에는 실행 시 필요한 의존 관계를 미리 확인하는 로직을 포함한 것도 있지만 대부분의 애플리케이션은 이런 로직이 없다.
의존 관계를 만족하는지 점검하는 디펜던시 체크 기능도 도커 이미지에 추가할 수 있다. 디펜던시 체크는 애플리케이션 실행 전에 필요한 요구 사항을 확인하는 기능이다.
모든 요구 사항이 확인되면 디펜던시 체크가 성공하고 애플리케이션이 실행된다. 반대로 만족하지 못하는 요구 사항이 잆다면 디펜던시 체크가 실패해 애플리케이션이 실행되지 않는다.
애플리케이션 실행 명령에 로직을 추가하는 방법으로 구현된다.
FROM diamol/dotnet-aspnet
ENV RngApi:Url=http://numbers-api/rng
CMD curl --fail http://numbers-api/rng && \
dotnet Numbers.Web.dll
WORKDIR /app
COPY --from=builder /out/ .
docker container run -d -p 8084:80 diamol/ch08-numbers-web:v2
docker container ls --all
헬스체크와 디펜던시 체크를 갖췄다면 이제 컨테이너 플랫폼 환경에 적합한 애플리케이션이라고 할 수 있다.
실제 애플리케이션 체크에는 애플리케이션과 같은 언어로 구현된 별도의 커스텀 유틸리티를 사용하는 것이 낫다.
(보안 정책상의 이유로 이미지에 curl을 포함시킬 수 없기 때문이다.)
애플리케이션과 같은 언어로 구현된 커스텀 유틸리티의 장점
닷넷 코어로 구현한 간단한 HTTP 테스트 유틸리티를 사용해 API 이미지의 헬스 체크와 웹 이미지의 디펜던시 체크에 사용한다. 이들 애플리케이션에는 애플리케이션 빌드와 유틸리티 빌드로 나뉜 멀티 스테이지 빌드가 적용돼 있다.
애플리케이션과 유틸리티를 나눠 빌드 및 패키징 하는 멀티 스테이지 빌드 과정
Dockerfile.v3파일의 빌드 단계중 마지막 단계에서 HEALTHCHECK curl 대신 닷넷 코어로 구현된 테스트 유틸리티를 사용한다.
# app image
FROM diamol/dotnet-aspnet
ENTRYPOINT ["dotnet", "Numbers.Api.dll"]
HEALTHCHECK CMD ["dotnet", "Utilities.HttpCheck.dll", "-u", "http://localhost/health"]
WORKDIR /app
COPY --from=http-check-builder /out/ .
COPY --from=builder /out/ .
# 기존 컨테이너를 모두 삭제한다.
docker container rm- f $(docker container ls -aq)
# API를 v3 버전의 이미지로 실행한다
docker container run -d -p 8080:80 --health-interval 5s diamol/ch08-numbers-api:v3
# 5초 정도 기다린 후 컨테이너 목록을 확인한다
docker container ls
# API를 네 번 호출한다 - 처음 세 번은 성공하고, 마지막 한 번은 실패한다.
curl https://localhost:8080/rng
curl https://localhost:8080/rng
curl https://localhost:8080/rng
curl https://localhost:8080/rng
# 애플리케이션에 버그가 발생했다. 15c초 기다린 후 상태가 이상으로 바뀌는지 확인한다
docker container ls
웹 애플리케이션의 Dockerfile.v3 파일을 보면, API 사용 가능 여부를 확인하는 디펜던시 체크에도 같은 유틸리티를 사용했다.
-t 옵션은 유틸리티가 요청에 대한 응답을 기다릴 제한 시간을 설정한 것
-c 옵션은 애플리케이션과 같은 설정 파일을 읽어 그 설정대로 대상 URL을 지정한 것
# app image
FROM diamol/dotnet-aspnet
ENV RngApi:Url=http://numbers-api/rng
CMD dotnet Utilities.HttpCheck.dll -c RngApi:Url -t 900 && \
dotnet Numbers.Web.dll
WORKDIR /app
COPY --from=http-check-builder /out/ .
COPY --from=builder /out/ .
이제 동작은 똑같이 유지 하면서 애플리케이션 이미지에서 curl을 제거할 수 있게 됐다.
# 웹 애플리케이션 이미지 버전 v3은 컨테이너 실행 시 HTTP 테스트 유틸리티를 사용해 디펜던시 체크를 수행한다.
docker container run -d -p 8081:80 diamol/ch08-numbers-web:v3
# 웹 애플리케이션 컨테이너의 데펜던스 체크가 실패해 컨테이너가 종료 됐다.(diamol/ch08-numbers-web:v3)
# API 컨테이너가 실행 중이지만 컨테이너 이름이 numbers-api로 지정되지 않았다. 웹 애플리케이션이 API 컨테이너를 발견하지 못한다. (만약 지정했더라도 네 번째 호출부터 버그를 일으키는 것은 변함 없다.)
docker container ls --all
커스텀 테스트 유틸리티를 따로 마련하는 또다른 장점
도커 컴포즈는 애플리케이션의 상태에 이상이 생겼을 때 어느 정도 복원할 수 있는 기능이 있다.
그러나 도커 컴포즈도 이상이 생긴 컨테이너를 새 컨테이너로 대체하지 않는다.
단일 서버에서 애플리케이션을 실행중이라면 더 심각한 장애를 일으킬 수 있다.
종료된 컨테이너를 재시작하거나 이미지에 정의되지 않은 헬스 체크를 추가할 수는 있다.
이 스크립트에서는 컨테이너 버전을 v3을 지정하고 있는데, 이 이미지에는 헬스 체크가 적용됐고 커스텀 테스트 유틸리티가 들어갔다.
version: "3.7"
services:
numbers-api:
image: diamol/ch08-numbers-api:v3
ports:
- "8087:80"
healthcheck:
interval: 5s
timeout: 1s
retries: 2
start_period: 5s
networks:
- app-net
도커 컴포즈 파일에서는 헬스 체크의 옵션을 상세하게 설정할 수 있다.
헬스 체크를 실시하는 데도 CPU와 메모리 자원이 필요하므로 운영 환경에서는 좀 더 간격을 길게 잡는 것이 낫다.
이미지에 헬스 체크가 정의되지 않았다면 컴포즈 파일에서 정의하는 방법도 있다. 서비스에 헬스 체크를 추가했다. 설정값은 API 서비스와 같은 값을 적용했다. test 필드가 헬스 체크를 위해 실행하는 명령이다.
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
networks:
app-net:
external:
name: nat
전체 스크립트
version: "3.7"
services:
numbers-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
networks:
app-net:
external:
name: nat
# 컴포즈 파일이 있는 디렉터리로 이동
cd ./ch08/exercises/numbers
# 현재 컨테이너를 모두 삭제
docker container rm -f $(docker container ls -aq)
# 애플리케이션 실행
// 앞서 모든 컨테이너를 삭제했으므로, 도커 컴포즈는 API와 웹 애플리케이션을 구성할 컨테이너를 모두 새로 생성한다. 그러나 이들 서비스 간에 의존 관계를 저으이하지 않았기 때문에 모든 컨테이너가 동시에 생성된다.
docker-compose up -d
# 5초를 기다린 다음 컨테이너 목록을 확인
docker container ls
# 웹 애플리케이션 로그도 확인
docker container logs number-numbers-web-1
적어도 세 번까지는 버튼을 클릭해 무작위 숫자를 출력할 수 있다. 네 번째 호출부터는 API에 버그가 발생해 오류를 출력한다.
왜 도커 컴포즈 파일에 depends_on 설정을 사용해 직접 디펜던시 체크를 하도록 하지 않았을까?
그 이유는 도커 컴포즈가 디펜던시 체크를 할 수 있는 범위가 단일 서버로 제한되기 때문이다. 이에 비하면 운영 환경에서 애플리케이션을 실제 시작할 때 일어나는 상황은 이보다 훨씬 예측하기 어렵다.
여러 개의 요소를 구성된 분산 시스템으로 동작하는 애플리케이션은 유연성과 기민성 면에서 뛰어나다. 그러나 반대로 급분로 관리가 그만큼 어려워진다.
구성 요소 간의 복잡한 의존 관계를 각 구성 요소를 시작하는 시작하는 순서에 반영해 모델링하는것은 좋은방법이 아니다.
물리 서버가 한 대 뿐인 환경이라면 도커 컴포즈에 웹 컨테이너보다 API 컨테이너를 먼저 실행시키라고 지시할 수 있다. 그러면 이 순서대로 컨테이너가 실행된다. 저자의 운영 환경에서는 10여 대의 서버에서 쿠버네티스를 운영하는데, 이 클러스터에서 20여 개의 API 컨테이너와 50여개의 웹 애플리케이션 컨테이너를 실행한다.
이 애플리케이션의 시작 절차를 설계한다면 어떻게 해야 할까? 웹 애플리케이션 컨테이너 50개보다 API 컨테이너 20개를 먼저 실행해야 할까?
20개의 API 컨테이너 중 19개는 무사히 실행됐는데 마지막 한 개가 실행이 늦어져 5분이나 걸렸다면 어떻게 될까?
웹 애플리케이션 컨테이너가 하나도 실행되지 않았으므로 애플리케이션이 동작중이라 할 수없다.
하지만 API 컨테이너가 하나 부족하더라도 50개의 웹 애플리케이션 컨테이너를 실행하는 데는 문제가 없다.
디펜던시 체크와 헬스 체크를 도입하면 처음부터 플랫폼이 실행 순서를 보장하게 할 필요가 없다.
가능한 한 빨리 컨테이너를 실행하면 된다. 일부 컨테이너가 의존 관계를 만족하지 못한 상태라면 재실행되거나 다른 컨테이너로 교체될 것이다.
이런 방법이면 대규모 애플리케이션의 경우 완전 동작 상태가 되는 데 몇 분 정도가 걸린다. 하지만 그 동안에도 애플리케이션이 동작하며 요청을 처리할 수 있다.
애플리케이션의 자기수복이란 일시적인 오류를 플랫폼이 해소해 주는 것이다. 애플리케이션에 메모리 누수를 일으키는 까다로운 버그가 있더라도 플랫폼에서 해당 컨테이너를 메모리를 잃지 않은 새 컨테이너로 대체하면 된다. 버그를 수정한 것은 아니지만 애플리케이션은 계속 동작할 수 있다.