본문 바로가기

Issue

[Docker] 도커 컴포즈를 통해 도커를 관리하다 생긴 문제들 - (2) (Docker Compose)

이번 방학에 팀으로 프로젝트를 하나 진행하고 있었다. 게임제작과 관련된 부분인데 해당 프로젝트에서 서버 쪽을 담당하게 되었다.
기간은 올해 말까지로 예상중이고, 해당 프로젝트에서 방학에는 인증서버 제작과 Docker를 사용해 보는 것이 목표였다.

저번 포스팅에선 Docker Network를 이용해 컨테이너간의 통신에 관련한 문제를 해결하는 포스팅을 작성하였다.
이번 포스팅에는 Docker Compose를 작성하여 ScyllaDB, redis, 인증서버를 한 번에 컨테이너화 시켜 관리하는 과정과 그 과정에서 생긴 사소한 이슈를 해결하는 과정을 작성했다.

 

1) 여러 컨테이너를 한번에 관리해 보자

컨테이너는 현재 3개 존재한다. [scylla, redis, 인증서버]
하지만, 이후에 게임에 필요한 서버들과 각종 기능을 위해 여러 컨테이너가 더 추가될 것 같았고. 해당 컨테이너를 관리하기 위해 여러 컨테이너들을 동시에 정의하고 실행하게 구성해 두어 배포 및 개발환경의 테스트 배포를  편리하게 하고 싶었다. 이후 실제 서비스를 배포하게 된다면 쿠버네티스(Kubernetes)를 이용해 배포를 진행하게 될 것 같지만, 현재 단계에서는 먼저 도커 컴포즈를 통해 진행하였다.

 

2) 기존 컨테이너의 설정 Docker Compose로 작성해 보자

컨테이너들의 설정을 담아 도커 컴포즈를 통해 한번에 관리하기 위해선 docker-compose.yaml 파일을 통해 정의해야 한다.

version: '3'

services:
  auth-server:
    build:
      context: .
      dockerfile: Dockerfile 	# 인증서버는 Dockerfile로 정의해 관리 
    ports:
      - "8000:8000"
    networks:
      - keycap-network	# (2)

  scylla:
    image: scylladb/scylla:latest
    ports:
      - "9042:9042"
    volumes:
      - ./scylla/scylla-data:/var/lib/scylla	# (1)
    networks:
      - keycap-network	# (2)
    container_name: scylla

  redis:
    image: redis:latest
    ports:
      - "6379:6379"
    networks:
      - keycap-network	# (2)
    container_name: redis

networks:
  keycap-network:	# (2)
    external:
      name: keycap-network

volumes:
  scylla-data:	# (1)

기본적인 docker-compose.yaml 파일에 대한 작성을 완료해 두었다. 저번 포스팅에서 했던 설정들을 모두 유지하며 작성했다.
(1) DB의 저장된 정보들을 컨테이너를 삭제하고 다시 생성해도 유지하기 위해 scylla의 volumes을 로컬에 존재하는 디렉토리에 마운트 해두었고, 도커에 별도의 volume을 두어 관리할 수 있게 volumes에 대한 설정값도 추가해 두었다.

(2) Docker Network를 통해 컨테이너 간 통신이 가능하도록 networks에 대한 설정값을 추가해 두었다.

 

 

3) 컨테이너간 통신의 문제

컨테이너간 통신을 위해 docker-compose.yaml에 networks 설정을 추가해 두고 keycap-network라는 네트워크명으로 3개의 컨테이너를 연결해 두었다. 하지만, 또다시 scylla와 연동되지 않는 문제가 발생했고 원인을 파악하기 위해 디버깅해 보았다.

 

3 - 1) 도커 컴포즈에서 도커 네트워크 설정이 적용되지 않는가?

혹시 도커 네트워크 옵션 자체가 적용이 되지 않는지에 대한 의문이 들었고, 코드상 scylla 컨테이너와 통신이 되지 않으면 에러를 출력하며 프로그램이 종료되도록 해두었기 때문에, redis는 정상적으로 동작하는지 확인해보아야 했다.

	// ScyllaDB 클라이언트 설정
	cluster := gocql.NewCluster("scylla:9042")
	cluster.Keyspace = "keycap_auth"
	cluster.ProtoVersion = 4
	session, err := cluster.CreateSession()
	if err != nil {
		log.Fatal(err)	// ScyllaDB 와 통신하지 못하면 프로그램이 종료된다
	}
	defer session.Close()

	// Redis 클라이언트 설정
	rdb := redis.NewClient(&redis.Options{
		Addr: "redis:6379", // Redis 서버 주소
	})
	_, err = rdb.Ping(context.Background()).Result()
	if err != nil {
		log.Fatal(err)
	}

따라서 ScyllaDB 부분을 지우고, Redis와의 통신을 먼저 점검해 보기로 했다.

 

----> 이 부분에서 약간의 시간을 잡아먹었는데, 먼저 나는 Goland라는 IDE를 사용했다.
docker-compose.yaml 파일을 자동적으로 run을 돌릴 수 있는 버튼이 존재해 그것을 이용해 docker-compose를 실행시켰는데
도커 컴포즈 파일을 실행시키기 위해선
          - docker-compose build
          - docker-compose up
이런 명령어를 통해 제어할 수 있었다. 당연하게 build이후 up 시켜 컨테이너를 만들줄 알았지만 아마도 run 만 실행하는 것 같았다.
분명 코드를 수정하고 IDE를 통해 도커 컴포즈를 실행시켰는데, 계속해서 scylla와 통신할 수 없다는 에러를 출력해 약간의 시간을 잡아먹게 되었고
이후에는 IDE를 사용하지 않고 그냥 터미널에서 위의 명령어를 통해 직접 제어하는 방식으로 변경하였다.

 

이후 다시 Redis와의 통신을 점검해 보기로 했을 땐, 정상적으로 redis와 연동되어 실행되는 것을 확인할 수 있었다. 그렇다면 docker-compose.yaml 파일에서 도커 네트워크는 정상적으로 설정되는 것을 확인하였고, 다시 Scylla의 config를 수정해야 하는가?를 고민했다.
하지만, 기존 도커 네트워크 설정으로 정상적으로 동작하는 것을 확인했으니 도커 컴포즈의 설정에 문제가 있다고 생각하여 scylla의 config는 확인하지 않고 도커 컴포즈의 옵션값을 찾아보기로 했다.

3 - 2) 도커 컴포즈에서 통신 관련 옵션들을 찾아보자

먼저 도커 네트워크에서는 특별한 문제를 찾아볼 수 없었으니, scylla의 통신 관련 옵션들을 찾아보았고, 이를 도커 컴포즈에서 제어할 수 있는 방법을 다시 찾아보기로 했다. 그러다 ScyllaDB의 Config에서 확인했던 LISTEN_ADDRESS 환경변수에 대해 생각하게 되었고, 도커 컴포즈에서 환경변수를 제어할 수 있다는 정보를 알게 되었다.
기존에 연결에 성공했던 Redis 서비스의 경우, 기본적으로 모든 네트워크 인터페이스에서 수신 대기하도록 설정되어 있어 별도의 환경 변수 설정이 필요하지 않았지만, ScyllaDB 서버는 다른 서비스가 서버에 접근할 수 있게 하기 위해 LISTEN_ADDRESS 환경변수를 설정해 주어 특정 호스트명에서 수신 대기하도록 설정해주어야 했다.

 

기존 docker-compose.yaml 파일의 scylla 설정값에 environment를 이용해 환경변수로 LISTEN_ADDRESS를 scylla로 설정해 주어 외부 컨테이너에서 scylla에 연결할 수 있도록 해주었다.

  scylla:
    image: scylladb/scylla:latest
    ports:
      - "9042:9042"
    volumes:
      - ./scylla/scylla-data:/var/lib/scylla
    networks:
      - keycap-network
    environment:
      - LISTEN_ADDRESS=scylla # 외부접속 허용 ip를 scylla로 설정
    container_name: scylla

 

4) 아직도 scylla에 연결할 수 없다?

scylla에 환경변수 설정을 추가해 두며, 더 이상 문제는 생기지 않을 것 같았다.
실제로도 인증서버와 scylla, redis 모두 연동되었고 도커 컴포즈를 통해 컨테이너 3개를 동시에 정의하고 실행하게 될 수 있었다.

다만, 약간의 문제가 생겼는데 docker-compose up을 통해 빌드된 컨테이너들을 실행시키게 되면, scylla 컨테이너가 완전히 켜지기 전, 인증서버가 실행되며 scylla와 연결할 수 없어 한번 종료되고, 이후 scylla 컨테이너가 완전히 실행되었을 때, 수동으로 인증서버 컨테이너를 다시 실행시켜 줘야만 정상적으로 실행할 수 있었다.

아마 scylla의 volume설정과 로컬에 volume을 마운트해둔 탓에 초기에 실행되는 시간이 늘어난 것 같다.

 

큰 문제는 아니었지만 그래도 도커 컴포즈를 통해 동시에 여러 컨테이너를 정의하고 실행시키는 데 있어서 수동으로 scylla 컨테이너가 완전히 켜지는 것을 확인한 이후에 수동으로 인증서버를 켜둔다는 것은 조금 어색했고 이를 수정할 방법을 찾게 되었다.

 

4 - 1) 도커 컴포즈의 depens_on 설정

docker-compose.yaml 파일의 설정중 depens_on이라는 옵션값이 있었다.

services:
  auth-server:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    networks:
      - keycap-network
    depends_on:
      - scylla	# scylla가 시작할때까지 기다림
      - redis	# redis가 시작할때까지 기다림

depens_on을 통해 해당 컨테이너명을 가진 컨테이너가 시작될 때까지 기다린 이후에 정의된 컨테이너를 시작할 수 있는 옵션값이었고, 이를 추가하여 불편함을 개선하려 했다. 하지만, depends_on은 서비스가 시작된 것만을 보장하고, 서비스 내의 애플리케이션이 완전히 준비되었는지 여부는 보장하지 않는 단순히 시작 순서를 정의하는 옵션이었다.


따라서 불편함은 개선되지 않았고 동일하게 scylla가 완전히 준비되기 이전에 연결 요청을 보내어 강제로 인증서버가 종료되었다.

 

4 - 2) wait-for-it.sh 스크립트

wait-for-it.sh 스크립트를 컨테이너에 적용할 수 있는 방법을 알게 되었다. wait-for-it은 자체적으로 해당 주소의 서비스가 수신 대기 상태가 될 때까지 기다리고, 이를 통해 서비스가 완전히 준비되었을 때 컨테이너를 실행시켜 다른 서비스가 해당 주소에 존재하는 서비스와 통신을 시작할 수 있게 해주는 스크립트이다.

 

이를 인증서버를 정의해둔 Dockerfile에서 불러오고 사용하게 정의할 수 있었다. docker-compose.yaml에서는 Dockerfile로 빌드된 인증 서버의 실행 시작 시간을 wait-for-it.sh 스크립트를 통해 제어할 수 있게 되었다.

...
# Copy wait-for-it script to the container
COPY ./wait-for-it-master/wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh

# Compile the Go app
RUN go build -o server

# Specify the command to run the Go app
CMD ["/wait-for-it.sh", "scylla:9042", "--", "./server"] # 스크립트 적용

# Expose port 8000 to the host
EXPOSE 8000

 

4 - 3) wait-for-it.sh 스크립트의 옵션 추가

실제로 스크립트는 적용되어 인증서버 컨테이너의 시작은 늦춰졌지만 여전히 scylla와 통신할 수 없다는 에러와 함께 종료되는 것을 확인할 수 있었다.

 

scylla 컨테이너가 시작되는 와중에 wait-for-it.sh 스크립트에서 timeout이 발생하였고 연결실패로 인증서버가 종료되었다.

timeout 에러가 발생한 것을 보니 wait-for-it 스크립트의 기본 timeout 시간이 15초인 것 같았고, 이런 timeout 시간 설정을 제어할 수 있는 방법을 찾아보았다.

방법은 매우 간단했다 기존 Dockerfile에서 정의한 스크립트 설정 커맨드에 "-t", "60"을 추가해 두면 됐다.

...
# Copy wait-for-it script to the container
COPY ./wait-for-it-master/wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh

# Compile the Go app
RUN go build -o server

# Specify the command to run the Go app
CMD ["/wait-for-it.sh", "scylla:9042", "-t", "60", "--", "./server"] # timeout 60초로 수정

# Expose port 8000 to the host
EXPOSE 8000

위와 같이 설정한 이후 docker-compose 파일을 다시 build 하여 실행했고 성공적으로 동시에 3개의 컨테이너의 빌드, 실행등의 제어를 처리할 수 있게 되었다.

wait-for-it 스크립트가 무려 53초나 기다려주며 도커 컴포즈 파일을 통해 동시에 3개의 컨테이너가 정상적으로 실행될 수 있게 되었다.

 

 

5) 마치며

이번에 도커를 다루며 생겼던 문제들을 해결하며 공부 순서와 개념 이해의 중요성에 대해서 생각하게 되었다.

 

이번 프로젝트는 거의 새롭게 다루는 기술들을 위주로 사용하였다. Go언어(Golang)로 작성된 서버, ScyllaDB라는 새로운 NoSQL 데이터베이스, JWT 인증방식의 Token 관리를 위해 사용되는 Redis와 해당 기술들을 컨테이너에 담아 분리하여 관리하는 도커(Docker), 그 컨테이너들을 동시에 제어할 수 있게 해주는 도커 컴포즈(Docker Compose)까지..

프로그래밍 언어나 기본적인 기술들은 기본적인 구글링과 오픈소스들을 찾아보며 단기간에 바로바로 프로젝트에 적용하며 코드를 작성하고 생기는 에러들을 쉽게 해결하며 결과물을 만들어낼 수 있었다.

그러나, 도커를 통해 해당 기술들을 관리하고 테스트 환경 배포를 위해 도커 컴포즈를 활용할 때는 GPT, 구글링 등등... 정보들이며 문제해결을 위해 정보들을 찾기 어려워 오래 걸렸다. 오히려 작성해야 되는 부분들은 압도적으로 양이 적었는데도 말이다.

 

문제해결을 위해선 먼저 어떤 것으로 인해 문제가 발생했을지에 대해 '의심'하는 것이 중요하다고 생각한다.
어떤 원리로 인해 어디서 에러가 발생했는지와 그 '의심'을 확실하게 파악하고 맞다/아니다 의 판단을 바로 내려 다른 '의심'을 하는 과정을 거치며 문제해결에 대한 실마리를 찾아야 한다고 생각한다.

이 과정에서 '개념'의 유무가 절대적인 부분을 차지한다고 생각한다.

그 기술에 대한 '개념'. 정확하지 않더라도 어느 정도의 큰 틀에서의 '흐름'정도는 이해하고 있어야 한다고 생각한다. '흐름'을 알아야 이 문제에 대한 원인들을 구분 지어 테스트해 볼 수 있고, 어떤 원인인지 맞다/아니다 의 판단을 정확하게 내릴 수 있다.

 

프로그래밍 언어쪽은 문법(세부적인 '개념') 쪽은 조금 문제였지만 프로그래밍을 계속 해왔다 보니 '흐름'정도는 이해하고 있었고, 구글링을 통해 쉽게 문제를 해결할 수 있었다. 하지만, 도커와 관련된 부분은 흐름에 대한 이해나 아무런 개념 없이 진행했다 보니 많은 시간을 투자하면서도 문제해결에 대한 실마리를 쉽게 찾지 못했던 것 같다.


앞으로는 공부방식을 조금 수정하여 진행해 보기로 했다. '개념' 혹은 '흐름'이라도 이해하고 진행하자!