docker-compose 가 파놓은 함정

텐저린티·2024년 2월 1일
0
post-thumbnail

🎯 사건의 발단

Docker Compose 를 활용해서 서비스 아키텍처를 구성하는 과정에서 발생한 문제다.

결론부터 말하자면,

docker compose 의 depends_on 속성 만으로는 컨테이너 간 의존성을 만족할 수 없다.

🔍 톺아보기

Docker Compose 란?

도커 공식문서를 살펴보면,

여기서 주목할 점은 크게 세 가지다.

  • 다중 컨테이너 애플리케이션 정의/실행
  • 간소/효율적 개발/배포 환경
  • 모든 환경에서 작동

정리하자면, 애플리케이션 별로 Docker Container 를 생성하고 서로 통신할 수 있도록 통합 환경을 만들 수 있다는 거다.

컨테이너가 한 개이던, 두 개던, 백 개이던,
서비스를 이루는 모든 컨테이너를 도커 이미지로 빌드하고, 컨테이너로 만들어 실행하는 일련의 과정이
docker compose up 이라는 명령어 한 번으로 끝난다.

그래서 왜 씀?

이 효자템을 활용해서 내가 얻고자 하는 것은 두 가지다.

  • 도커 생태계를 이해하기 위해
  • 클라우드 서비스 비용을 아끼기 위해

도커 생태계 이해?

내 소중한 맥북에 로컬 DB 를 깔기 싫어서 도커에 MySQL 컨테이너를 띄워서 사용하려는 목적으로 처음 도커를 썼다.

하지만, 주변의 소문과 풍문으로 들었을때 도커가 가진 능력의 반의 반도 사용하지 못하는 것 같았다.
한 번 써보고 싶었다.
이미지가 뭔지, 컨테이너가 뭔지, 왜 쓰는 건지 직접 써보면 감이 올 거라고 생각했다.
무엇보다 MSA 로 가는 첫걸음이자 지름길이라는 인식이 있는 것 같아서 맛을 보고 싶었다.

써본 결론은 MSA 로 가는 지름길도 맞고, 첫걸음으로 좋은 방법인 것도 맞았다.

클라우드 서비스 비용?

결국엔 개발한 서비스를 네이버나 AWS 에 올려야 한다.
지난번 플젝은 공짜로 클라우드 서비스를 이용했지만, 원래라면 달에 10만원은 우습게 깨질 정도로 써야 할 것이 많았다.
EC2 두 개, S3, RDS, 로드밸런서, Cloud Front..
다 써보고 왜 쓰는지 왜 아마존이 갓마존인지 알 수 있었다.

하지만!
돈을 아낄 줄 아는 개발자.
비용 절감을 고려하는 개발자!

개발한 서비스에 사용량이 많지 않을것 같으니 구태여 인프라를 여럿둬서 비용, 관리로 애먹고 싶지 않았다.
나는 도커 컨테이너, 구체적으로 말하면, 도커 컴포즈로 만들어진 인프라 자체를 EC2 에 때려넣어서 최대한 비용을 아껴보고자 했다.

🏗️ 구조

도커 공식 아키텍처 예시

그림을 보면 알 수 있듯이,
원래라면 AWS 나 NCP 생태계를 활용하면서 구축하는 인프라를 도커 컨테이너 안에 담는다는 내용이다.
따라서 외부에서 인프라에 접근하는 것은 443 포트 (https) 로 연결하는 것만 열어두면 된다.
나머지는 도커 컴포즈 네트워크 내에서 컨테이너끼리 통신하는 방식으로 동작한다.
내가 만들려고 하는 구조가 바로 이거다.

이 구조를 만들기 위한 docker-compose.yml 파일이다.
우리는 이 파일을 기반으로 도커 컨테이너를 생성/실행하고 인프라를 구축한다.

services:
  frontend:
    image: example/webapp
    ports:
      - "443:8043"
    networks:
      - front-tier
      - back-tier
    configs:
      - httpd-config
    secrets:
      - server-certificate

  backend:
    image: example/database
    volumes:
      - db-data:/etc/data
    networks:
      - back-tier

volumes:
  db-data:
    driver: flocker
    driver_opts:
      size: "10GiB"

configs:
  httpd-config:
    external: true

secrets:
  server-certificate:
    external: true

networks:
  # The presence of these objects is sufficient to define them
  front-tier: {}
  back-tier: {}

내가 설계한 아키텍처

나는 이런식으로 아키텍처를 구성할 생각이다.

🧳 준비물

  • Docker Desktop
  • docker-compose.yaml
  • 각 도커 컨테이너

Docker Desktop

맥 용, 윈도우 용 따로 있다.
별도로 Docker 를 설치하고 CLI 로만 모든 작업을 해도 되지만,
Docker Desktop 앱을 설치하면 자동으로 Docker 도 설치되고, GUI로 편리하게 사용이 가능하다.

대충 이렇게 생겼다.
앞서 로컬DB 설치하기 싫어서 만들어둔 MySQL 컨테이너와 이번 플젝 준비하면서 구성한 아키텍처가 보인다.

happyparking 이라고 보이는 저거를 만드는게 삽질의 원인이다.

Docker-compose.yml

결과적으로 문제를 해결한 yml 설정이다.

services:  
  # DB Container  
  db:  
    container_name: db 
    image: postgres:${version} # PostgreSQL 이미지
    environment:  
      - 'POSTGRES_USER=${username}'  
      - 'POSTGRES_PASSWORD=${password}'  
      - 'POSTGRES_DB={schema_name}'  
    ports:  
      - "5432:5432"  # 도커 기본 포트
    healthcheck:  # 컨테이너 헬스 체크 (이게 성공해야 server 서비스가 실행됨)
      test: [ "CMD-SHELL", "pg_isready -h db -p 5432" ]  
  
  # Server Container  
  server:  
    container_name: server  
    depends_on:  # 내 삽질의 주인공
      db:  # db 서비스에 의존성이 있음
        condition: service_healthy  # db 서비스의 헬스체크가 성공한 경우에 해당 서비스 실행
    build:  
      context: ./happyparking-server  # 이러한 경로에 있는
      dockerfile: Dockerfile  # 도커파일을 빌드
    environment:  # spring boot profile 설정
      - SPRING_PROFILES_ACTIVE=default,auth,docs,monitor,prod  
    ports:  # spring boot 기본 포트
      - "8080:8080"  
    healthcheck:  # prometheus 를 위한 헬스 체크
      test: [ "CMD-SHELL", "curl -f http://localhost:9292/actuator/health || exit 1" ]  
      interval: 10s  
      timeout: 5s  
      retries: 3  # 너무 많아도 의미 없음

  # Prometheus Container  
  prometheus:  
    container_name: prometheus  
    image: prom/prometheus:latest  # 프로메테우스 이미지
    ports:  # 프로메테우스 기본 포트
      - "9090:9090"  
    volumes:  # 커스텀한 프로메테우스 yml 설정을 사용 (기본 yml 대체)
      - ./happyparking-monitor/prometheus.yml:/etc/prometheus/prometheus.yml  
    depends_on:  # server 에 의존성
      server:  
        condition: service_healthy  
  
  # Grafana container  
  grafana:  
    container_name: grafana  
    image: grafana/grafana:latest  # 그라파나 이미지
    ports:  # 기본 포트
      - "3000:3000"  
    volumes:  
      - ./grafana-data:/var/lib/grafana  
    depends_on:  # 헬스체크를 하지 않아도 프로메테우스는 빠르게 실행됨
      - prometheus

📺 진행과정

문제를 작게 보아야 눈에 보이는 법이다.
프로메테우스, 그라파나 이런건 잊어버리고, DB랑 Server 서비스만 보자.

문제는 무엇?

컨테이너 간의 의존성 문제다.
Spring Boot 컨테이너 (이후 Server 서비스) 는 PostgreSQL 컨테이너(이후 DB 서비스) 에 의존성을 갖고 있다.

쉽게 말하면,
DB 서비스는 혼자서도 잘 실행되는데, Server 서비스는 DB 서비스가 없으면 실행되지 않는다는 것.

docker compose up 명령어로 실행해보면,
Spring Boot 실행 중에 Connection Refused 니,
jdbc connection pool 이 없다느니,
서버로 패킷을 보냈는데 답장이 없다느니

별별 소리를 다 한다.

원인은 무엇?

docker-compose 의 depends_on 속성 때문이다.

공식문서를 들고 왔다.
정확히 원인을 이야기해준다.
영어 울렁증만 없었어도 빨리 해결했을텐데.

요약하자면,

depends_on 속성은 컨테이너가 '준비' 될 때까지 기다리는 게 아니라, '실행'될 때까지만 기다린다는 것.
의존성을 가지고 있는 RDBMS 가 있는 경우 문제가 발생할 수 있다.

해결책은 무엇?

각 서비스 별로 헬스체크와 헬스체크를 기반으로 한 depends_on 설정이다.
쉘모드에서 -h 옵션으로 상태체크를 해줌으로써 컨테이너 서비스가 제대로 설정됐는지, 제대로 실행됐는지, 제대로 동작하는지 확인할 수 있다.

이후에 의존성을 갖던 서비스를 실행하면 문제를 해결할 수 있다.

# 의존대상 서비스
healthcheck:  # 컨테이너 헬스 체크 (이게 성공해야 server 서비스가 실행됨)
      test: [ "CMD-SHELL", "pg_isready -h db -p 5432" ]  

# 의존주체 서비스
depends_on:  # 내 삽질의 주인공
      db:  # db 서비스에 의존성이 있음
        condition: service_healthy  # db 서비스의 헬스체크가 성공한 경우에 해당 서비스 실행

프로메테우스 - 그라파나는 괜찮은 이유?

DB 세팅에 꽤 오랜 시간이 걸려서 발생한 문제였다.
DB 메타데이터로 임시 DB 를 설정하고, 이를 실제 DB 로 변경하는 과정까지 걸리는 시간이 Spring Boot 실행이 되고도 10초 정도 더 걸렸다.
그래서 의존성에 문제가 발생한 것.

마찬가지로, 프로메테우스 실행속도가 빠른데, Spring Boot 가 그 속도를 따라가지 못해서 의존성 문제가 발생한다.

하지만 프로메테우스는 빠르게 실행되므로, 그라파나와 상태체크를 따로 하지 않아도 정상적으로 실행된다.

상태체크를 따로 해주면 명확히 의존관계를 확립할 수 있지만, 그러면 시간이 더 걸리니까 여기까지만 했다.

노파심에 하는 소리

Spring Boot DB datasource 설정

아마 이런식으로 대부분 로컬 개발 환경을 설정할 것이다.

spring:
	datasource:
		url: jdbc:{디비벤더}://localhost:{포트번호}/{스키마이름}

이렇게 해도 DB 컨테이너가 제대로 돌아가고 있다면 IDE 에서 실행하는데에는 아무런 문제가 없다.

하지만 Docker Compose 로 만들어진 컨테이너 환경이라면 이야기가 달라진다.

Docker Container 는 내부적으로 자신만의 호스트를 갖는다.
따라서 localhost 로는 해당 컨테이너 안에서만 사용 가능한 것이지, 다른 컨테이너는 공감할 수 없는 내용이다.

따라서 우린 docker-compose 로 설정한 네트워크 내에서 각각의 서비스를 호스트 이름으로 해서 설정을 해줘야 한다.

spring:
	datasource:
		url: jdbc:{디비벤더}://{DB서비스이름}:{포트번호}/{스키마이름}

Spring Boot DB hibernate dialect 설정

스프링 부트에서는 DB 방언을 따로 설정할 필요가 없다.
스프링 부트가 자동으로 필요한 설정을 제공하기 때문이다.
구태여 개발자가 방언을 설정해서 똑똑한 스프링 부트님의 심기를 건드릴 필요가 없다는 것.

하지만 도커 컨테이너를 사용하는 경우에는 말이 좀 다르다.
hibernate dialect 를 설정하지 않으면 도커 컨테이너가 기함하면서 실행을 거부한다.

🔑 결론

이 문제로 반나절 정도 고생하다가 결국 못고쳤는데,
그다음 날 다시 보니까 원인이 생각나서 고치게 됐다.

역시 문제가 막힐 때는 잠시 쉬었다가 다시 보는 유연함이 필요한 것 같다.

구글링해도 문제가 너무 다양해서 의존성 관련한 포스팅은 많지 않았던 것 같은데,
아님 내가 검색을 못하던가

암튼 이 글이 도움이 되었으면 좋겠다.

🔗 참조

Docker Docs
Docker Docs - Compose의 시작 및 종료 순서 제어

profile
개발하고 말테야

0개의 댓글