Docker

Learn Docker In A Month Of Lunches - 07. Running Multi-Container Apps With Docker Compose

깡구_ 2023. 4. 3. 23:19

title: Learn Docker In A Month Of Lunches - 07. Running Multi-Container Apps With Docker Compose
date: 2023-04-03
tags:

  • Docker

Introduction

Learn Docker In A Month Of Lunches

GDSC에서 기존에 진행하던 HTTP 스터디 종료 이후, Docker 스터디를 이어서 진행하게 되었다.
실습 내용을 위주로 작성한다. 서적에는 각 내용에 대한 더욱 자세한 설명이 포함되어 있으니 포스팅에 적지 않은 자세한 내용은 서적을 보거나 검색 등을 추천한다.

Purpose

여러 Container 설정을 편하게 관리할 수 있는 Docker Compose에 대해 알아본다.

Anatomy Of Docker Compose

Dockerfile을 이용하면 하나의 Image를 구성한다.
Container 실행 시 옵션을 명시하여 다양한 Container를 연결할 수 있으나, 이 경우 Container가 많아질수록 귀찮은 작업이 늘어나며 옵션을 까먹을 경우 프로그램이 죽을 수 있다는 단점이 있다.
Docker Compose를 이용하면 이를 쉽게 해결할 수 있다.

Docker Compose 파일에는 각 Container가 어떠한 설정으로 실행되어야 하는지 작성한다.
Docker Compose를 이용하여 이를 실행하면 구성에 필요한 Container, Volume, Network 등을 모두 구성해 준다.

todo-web Docker Compose Yaml
todo-web Architecture

Docker Compose 파일은 YAML로 작성한다. JSON으로 변환하기 쉬우며 사람이 읽기도 쉬운 포맷이다.
해당 파일의 최상의 Level에는 아래와 같은 Key가 존재한다.

  • version - Docker Compose의 Version을 명시한다. 필수적인 Key로, 특정 Version을 명시해야만 한다.
  • service - 각 Container를 의미한다. Docker Compose에서는 Service 개념을 하나의 단위로 이용하는데, 동일한 Service를 여러 Container에서 사용할 수 있기 때문이다.
  • network - Container를 연결할 Network를 의미한다.

위 Docker Compose 파일은 아래의 명령어와 동일하다.

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

network에서 external을 명시하였는데, 이는 nat Network가 이미 존재할 것이며 이를 이용하겠다는 의미이다.
nat가 존재하지 않는다면 Docker Compose 실행 시 에러가 발생한다.

docker network create nat
cd ~/diamol/ch07/exercises/todo-list
docker compose up

Docker Compose todo-web

별도의 백그라운드 실행 옵션이 없었기에 위와 같이 현재 Terminal에서 todo-web를 실행한다.
docker compose up 명령 실행 시 현재 디렉토리에서 docker-compose.yml 파일을 찾아 실행한다.

각 Container별로 Log를 조회하면 여전히 불편하다. Docker Compose에서는 이를 개선한 편의 기능도 존재한다.
실행한 docker-compose.yml 파일의 디렉토리에서 docker compose logs를 입력하면 해당 파일에서 정의한 Service들의 Log를 모아서 출력한다.
각 Container에 대한 Log를 조회하고 싶다면 docker container logs 혹은 docker compose logs CONTAINER 처럼 이용한다.

Docker Compose Log

Container를 조회할 때는 디렉토리의 영향이 없었으나, Docker Compose는 docker-compose.yml을 기준으로 각 Log가 쌓인다.
따라서 해당 디렉토리와 하위 디렉토리에서만 Log 조회가 가능하다.

Docker Compose With Multi Container

Chapter04에서 NASA API를 이용하여 사진을 가져오는 프로그램을 실행하였다.
당시에는 여러 Container를 일일이 실행하여야 했으나, Docker Compose로 구성하면 명령어 한 줄로 실행할 수 있다.

Multi Container Yaml

accesslog의 경우 별도의 Port를 할당하지 않았다.
iotd의 경우 Container의 80번 Port를 할당하였으며, 호스트의 특정 Port를 명시하지 않았기에 임의의 Port가 할당된다.
image-gallery는 호스트의 8010번 Port와 Container의 80번 Port를 맵핑하였다.
또한 depends-on을 이용하여 다른 두 Container에 대한 의존성이 있음을 명시하였다.
이를 만족하기 위해 Docker Compose 실행 시 두 Container가 먼저 실행된다.

cd ~/diamol/ch07/exercises/image-of-the-day
docker compose up -d

Docker Compose가 실행되며 각 Image를 가져온 후 Container를 실행한다.
Cache를 이용하게 되면 빠르게 실행이 되어 확인이 어려울 수 있으나, 의존성의 순서에 맞게 Container가 실행된다.
-d == --detach 옵션을 이용하여 백그라운드로 실행하였다.

현재 API를 처리하는 Container는 Stateless이기에 여러 Container를 실행시켜 Scale Out이 가능하다.

docker compose up -d --scale iotd=3
curl http://localhost:8010
curl http://localhost:8010
curl http://localhost:8010
curl http://localhost:8010
curl http://localhost:8010
curl http://localhost:8010
docker compose logs -n=1 iotd

Multi Container Log

http://localhost:8010에 접근할 때, 여러 iotd가 나눠서 처리한다.
위 사진에서 2, 3번째 iotd가 API를 처리한 이력을 확인할 수 있다.
--scale 옵션을 이용하면 특정 Container를 늘려 Scale Out이 가능하다.

Docker Compose로 편하게 관리하였으나, 사실 내부적으로 Docker API를 이용한다.

docker compose stop
docker compose start
docker container ls

Docker Compose Stop Start

Docker Compose를 이용하여 띄운 Container들을 전부 중지한 후, 다시 시작하여 조회하는 예제이다.
일반적으로 Docker Compose 시작은 의존성에 맞게 진행된다. 의존하는 것이 적은 Container부터 시작된다.

Docker Compose에는 많은 기능이 존재하지만, 결국 최종적으로 Docker API를 통해 명령을 보낸다.
Docker 자체에서는 여러 프로그램을 관리한다는 사실을 모른다는 뜻이기도 하다.
따라서 yaml 파일을 통해 Docker Compose를 관리해야 한다.

또한 yaml 파일의 편집이 아닌, Docker를 직접적으로 변경할 수도 있다.
다만 이 경우, Docker Compose를 이용한 관리에서 벗어나기에 예상치 못한 문제가 발생할 수 있다.
이전에 실행한 image-of-the-day 또한 기존 Docker Compose가 존재하던 중 Scale Out을 하였다.
이는 직접 변경하였기에, Docker Compose를 다시 실행한다면 기존의 설정대로 구성된다.

docker compose down
docker compose up -d
docker container ls

Configure With Yaml

docker compose down 명령어로 인하여 Container를 삭제한다.
yaml 파일에서 external로 명시하지 않은 Network나 Volume 또한 함께 삭제된다.
다시 Docker Compose로 실행하면 yaml의 세팅을 따라 1개의 iotd Container만 실행된다.

Docker Compose를 이용하면 쉽게 관리할 수 있으나, Client Side Tool이라는 것을 명심하여야 한다.
Docker는 이에 대해 알지 못하기에 yaml 파일을 잘 관리하여야 적절한 프로그램 구성이 가능하다.

How Docker Plugin Container Together

각 Container는 Virtual IP를 할당받는다. Container가 재시작되면 IP는 고정되지 않고 바뀐다.
Docker는 내부적으로 DNS를 구성하여 이러한 변동 IP 문제를 해결한다.
Container의 이름을 통해 Virtual IP를 알아낼 수 있으며, 이로 인하여 매번 IP를 확인할 필요 없이 Container의 이름만으로 쉽게 사용할 수 있는 것이다.
DNS에 저장되는 Domain명은 Container의 이름과 동일하다면 바로 이를 넘겨주며, 다를 경우 DNS가 추가적인 작업을 통해 IP를 넘겨준다.
image-gallery를 통해 이를 확인한다. Request 대상 Container가 하나라면 하나의 IP가, 여러 개라면 여러 IP를 Response로 받게 된다.

docker compose up -d --scale iotd=3
docker container exec -it image-of-the-day-image-gallery-1 sh
nslookup accesslog
exit

nslookup

nslookup 명령을 이용하여 DNS에 accesslog의 IP를 조회하였다.
accesslog를 찾을 수 없다는 에러가 발생하였으나, 바로 accesslog의 IP를 알려주었다.
해당 에러는 nslookup의 버그로 추정된다.

이후 accesslog를 삭제하여 실행하면 다른 IP를 할당받게 된다. 아래 예제를 통해 확인한다.

docker container rm -f image-of-the-day-accesslog-1
docker compose up -d --scale iotd=3
docker container exec -it image-of-the-day-image-gallery-1 sh
nslookup accesslog
nslookup iotd
exit

Same IP

예제에서는 accesslog만 삭제가 되었으며, 다른 Container의 변경 사항이 없었다.
기존 Container는 유지하되, 존재하지 않는 accesslog만 실행하게 된다.
따라서 기존 IP를 그대로 부여받았다. 다른 Container가 먼저 실행된다면 다른 IP를 부여받게 될 것이다.

iotd를 조회하였을 때, 모든 IP를 Response 받았다.
이 덕분에 간단한 Load Balancing이 가능하며, 상세한 동작은 프로그램이 결정한다.
여러 IP 중 첫 번째 IP만 사용할 수도 있다. DNS 조회 시 시스템은 순서를 계속 바꾸기에 이것만으로도 간단한 Round Robin이 가능하다.

#Configuration In Docker Compose
Chapter06에서 사용하였던 todo 프로그램 또한 다양한 방식으로 구성 가능하다.
단일 Container 및 Volume을 이용할 수도 있고, 별도의 DB를 Container로 띄워 이용할 수도 있다.
아래 Docker Compose 파일은 Postgres를 DB로 이용하도록 구성하였다.

Postgres Yaml

 

  • environment - 환경 변수에 대한 설정이다. Database:Provider 값을 Postgres로 설정한다.
  • secrets - Container 내부에 저장할 Secret 값에 대한 설정이다. /app/config/secrets.json 파일에 postgres-connection 이라는 값을 저장한다.

Secret 값은 보통 Kubernetes나 Docker Swarm과 같은 Container Orchestration을 통해 설정한다.
Cluster DB에 암호화되어 저장되기에 DB 비밀번호나 인증서, API Key 등 노출되어서는 안 되는 정보 관리에 적합하다.
Docker만 이용할 경우 Cluster DB가 없기에 파일을 이용하여 Secret 값을 저장한다.

Secret Yaml

이전에 보았던 Yaml 파일의 하단에 위 내용이 추가적으로 존재한다.
이를 통해 postgres-connection이라는 값을 ./config/secrets.json에서 가져오라고 명시한다.
이전 Chapter에서는 Mount를 이용하였으나, Secret을 이용하였기에 추후 Cluster DB를 이용하도록 확장하더라도 쉽게 변경할 수 있다.

이러한 방식으로 Docker Compose 파일을 이용하여 다양한 환경의 프로그램 구성이 가능하다.
프로덕션과 개발 환경을 다르게 구성할 수도 있고, 일부 기능만 이용할 수도 있다.

cd ~/diamol/ch07/exercises/todo-list-postgres
docker compose up -d
docker compose ps

Container 내부 혹은 Volume, Mount 등을 통해 호스트에 저장하던 방식과 다르게 별도의 DB Container를 이용하였다.
http://localhost:8030에 접속하면 이전과 동일하게 프로그램 사용이 가능하나, 데이터가 저장되는 위치가 다르다.
5433 Port로 DB에 접근이 가능하다. todo DB이며 postgres 유저로, 비밀번호는 없다.

Postgres Data

각 프로그램과 설정을 분리하여 관리할 수 있다는 것이 Docker의 핵심 중 하나이다.
Build한 Image는 다양한 설정을 통해 개발 환경에서 테스트하며, 프로덕션에 Image를 안정적으로 배포할 수 있다.
Docker Compose 파일만 수정하여 설정을 쉽게 변경할 수 있으며 확장에도 용이하다.

#Limitation Of Docker Compose
Docker Compose를 이용하면 다양한 복잡한 설정도 간편하게 관리할 수 있다.
과거에는 Word 문서 등을 통해 관리하였으나, 이에 비하면 훨씬 직관적이고 유지보수도 용이하다.
다양한 장점으로 인하여 Docker Compose가 만능이라고 생각할 수 있으나 여전히 한계가 존재한다.

Docker Compose를 이용하여 각 Container의 연관 관계를 비롯한 다양한 설정을 할 수 있다.
또한 실제 명령은 Docker API를 통해 이루어지기에 기존 Container를 그대로 이용하기도 한다.
docker compose up을 통해 Container들을 띄우는 것이 끝이다.
이후 각 Container를 관리하지 않으며, 프로그램이 죽는다면 수동으로 Docker Compose를 실행하여야 한다.

Docker Compose LifeCycle

위 그림이 Docker Compose를 사용하기 적절한 LifeCycle이다.
개발, CI, 테스트 등의 환경에서는 Docker Compose를 이용하여도 된다. 프로덕션이 아니기에 프로그램이 죽어도 괜찮다.
다만 프로덕션에서는 각 Container를 관리하여야 하기에, Kubernetes나 Docker Swarm과 같은 Container Orchestration을 이용하여야 한다.
물론 프로덕션 환경에서도 Docker Compose 포맷을 이용하여 각 프로그램 설정을 한다.

Docker Compose이 프로덕션에 적합하지 않다는 의미가 아니다.
기존에 VM을 사용하던 조직이 Docker로 전환을 할 때, Docker Compose를 이용하여 시작하는 것은 괜찮다.
HA, DR, Load Balancing 등은 불가능하겠지만 이는 VM도 동일하다.
각 프로그램을 유기적으로 연결하고 VM에서의 문제를 해결할 수 있다.
이후 서비스를 확장하며 문제가 발생할 때, Container Orchestration을 도입하여도 괜찮다.

Lab

todo 프로그램을 테스트 환경에 적합하도록 Docker Compose 파일을 수정한다.
호스트나 Docker Engine이 재시작될 경우 Container도 재시작되어야 한다.
DB Container를 사용하지 않고 Mount를 이용하여 데이터를 저장하여야 한다.
테스트를 위해 호스트의 80번 Port를 이용하여야 한다.
https://docs.docker.com/compose/compose-file/에서 추가적인 Docker Compose에 대한 정보를 확인할 수 있다.

우선 Mount를 이용할 것이기에 이후 데이터 삭제를 위해 lab 디렉토리에서 진행한다.
또한 현재 디렉토리의 Docker Compose 파일을 기반으로 Lab을 진행하기에 해당 파일을 복사한다.

cp docker-compose.yml ~/diamol/ch07/lab
cd ~/diamol/ch07/lab

본 디렉토리에는 정답 파일이 존재하지만, 주목적은 postgres-connection.json을 이용하는 것이다.
Postgres DB에 대한 연결 정보가 저장되어 있으며, 이를 직접 작성하려면 많은 자료를 찾아보아야 한다.
본 실습의 목적은 해당 json파일을 설정하는 것이 아니기에 이를 그대로 이용한다.

version: "3.7"

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

이미 구성된 정보부터 수정한다.
테스트 환경에서 80번 Port를 이용하도록 명시되어 있기에 80:80 으로 수정한다.
또한 기존의 postgres-connection.json을 이용하므로 secrets에서 해당 파일을 바라보도록 수정한다.

남은 재시작, Mount를 해결하여야 한다.
재시작의 경우 공식 문서를 따라 한다.
alwaysunless-stopped 옵션 중 unless-stopped를 사용한다.
always는 정상적인 Container 종료에도 재시작이 되는데, 이는 의도적으로 Container 종료를 의미한다.
Container를 의도적으로 종료하는 상황은 거의 없을 것 같으나, 테스트 환경에서 이를 테스트할 수도 있을 것 같기에 unless-stopped를 이용한다.

Mount의 경우, 공식 문서에서는 Volume으로만 나와 있기에 정보를 찾기가 어려웠다.
결론부터 말하자면 volume: 처럼 이용하는 것이 맞다.
Volume 관련 문서에서는 상세한 사용법이 나와 있지 않으나, Legacy Version - Long Syntax에서는 상세 옵션들을 알려준다.
각 옵션에서 사용 가능한 것들은 Bind Mounts 혹은 Volumes에서 확인할 수 있다.
type의 경우 Mount이기에 bind로 명시하면 된다.
sourcetarget--mount 옵션에서 사용하던 것과 동일하다. 각각 호스트의 디렉토리, 프로그램의 디렉토리를 작성하면 된다.
target의 경우, todo-db를 따로 실행하여 내부를 확인하면 /var/lib/postgresql/data에 DB 데이터가 저장되는 것을 확인할 수 있다. 이를 이용한다.

추가로 찾은 내용들을 Docker Compose 파일에 추가해주면 된다.

version: "3.7"

services:
  todo-db:
    image: diamol/postgres:11.5
    ports:
      - "5433:5432"
    networks:
      - app-net
    restart: unless-stopped
    volumes:
      - type: bind
        source: lab-data
        target: /var/lib/postgresql/data

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

networks:
  app-net:

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

이제 lab-data 디렉토리를 생성한 후, Docker Compose를 실행한 후, http://localhost:80에서 데이터를 추가한다.

mkdir lab-data
docker compose up -d

Wrong Owner

lab-data 디렉토리의 소유자가 UID 70번으로 변경되었다. 이는 Container의 UID로 추정되는데, 이유는 정확히 모르겠다.
Mount중에는 권한 부여가 불가능하다. root 계정으로 결과를 확인하면 정상적으로 Mount가 이루어진 것을 확인할 수 있다.

su -
cd /home/USER/diamol/ch07/lab
cd lab-data
ll

Lab Result

Conclusion

여러 Container 설정을 편하게 관리할 수 있는 Docker Compose에 대해 알아보았다.
서적에서 다룬 것보다 훨씬 많은 기능이 제공되니, 공식 문서나 다양한 포스팅 등을 통해 적절한 Docker Compose 파일을 구성하는 것이 좋다.