컨테이너로 분산 애플리케이션 실행하기-01

jjunhwan.kim·2023년 6월 23일
0

도커

목록 보기
3/6
post-thumbnail

개요

안녕하세요. "도커 교과서" 라는 책을 읽고 공부한 내용을 정리해 보겠습니다. 이 포스트는 7장의 내용을 정리하였습니다.

도커 컴포즈 파일의 구조

지금까지 봤던 Dockerfile 스크립트는 하나의 애플리케이션을 패키징하기 위한 스크립트입니다. 웹 프론트엔드, 백엔드 API, 데이터베이스를 갖춘 애플리케이션을 패키징하려면 각 컴포넌트에 하나의 Dockerfile 스크립트가 필요하므로 총 세 개의 Dockerfile 스크립트가 필요합니다.

그렇다면 이 세개의 컨테이너는 누가 실행해야 할까요? 직접 순서대로 도커 명령어를 이용해 실행할 수 있습니다. 하지만 수동으로 실행시키는 것은 실수와 오류가 생길 수 있습니다.

이런 방법 대신 도커 컴포즈 파일에 애플리케이션의 구조를 정의할 수 있습니다. 도커 컴포즈 파일은 모든 컴포넌트가 실행 중일 때 어떤 상태여야 하는지 기술하는 파일입니다. 또한 docker run 명령으로 컨테이너를 실행할 때 지정하는 모든 옵션을 한데 모아 놓은 파일입니다.

도커 컴포즈 파일을 작성하고 도커 컴포즈 도구를 사용해 애플리케이션을 실행합니다. 그러면 도커 컴포즈 도구가 컨테이너, 네트워크, 볼륨 등 필요한 모든 도커 객체를 만들도록 도커 API에 명령을 내립니다.

아래 스크립트는 이전 포스트에서 사용했던 스프링 어플리케이션 도커 이미지를 도커 컴포즈로 실행하도록 도커 컴포즈 파일로 작성한 것입니다.

version: '3.7'

services:
  backend:
    build:
      context: ./backend
    image: spring-backend:1.0.0 
    container_name: my-web-backend
    ports:
      - "8080:8080"
    networks:
      - app-net
      
networks:
  app-net:
  • 도커 컴포즈 파일은 YAML 문법으로 기술됩니다. 따라서 들여쓰기가 중요합니다.
  • version은 도커 컴포즈 파일 형식의 버전을 나타냅니다. 버전에 따라 문법이 다르므로 버전을 지정합니다.
  • services는 애플리케이션을 구성하는 모든 컴포넌트를 열거하는 부분입니다. 도커 컴포즈에서는 실제 컨테이너 대신 서비스 개념을 단위로 삼습니다. 하나의 서비스를 같은 이미지로 여러 컨테이너에서 실행할 수 있기 때문입니다.
  • networks는 서비스 컨테이너가 연결될 모든 도커 네트워크를 열거하는 부분입니다.
  • 위의 backend 서비스는 backend 디렉토리의 Dockerfile 스크립트를 spring-backend:1.0.0 이미지 이름으로 빌드합니다. 해당 이미지로부터 단일 컨테이너로 실행되며 컨테이너 이름은 my-web-backend 이고 호스트 컴퓨터에 8080 포트를 공개합니다. 그리고 app-net 이라는 이름의 도커 네트워크에 연결됩니다.
  • 아래 명령어를 실행한 것과 같은 상태가 됩니다.
    docker network create app-net
    docker run -d -p 8080:8080 --name my-web-backend --network app-net spring-backend:1.0.0

서비스 이름은 컨테이너의 이름으로도 쓰이고(컨테이너의 이름과 서비스 이름이 완전히 일치하지는 않습니다) 도커 네트워크상에서 다른 컨테이너들이 해당 컨테이너를 식별하기 위한 DNS 네임으로도 쓰입니다.

https://github.com/nefertirii/docker-example/tree/40a1deb1058d3d3ec17f794896037fd5d587b9be 프로젝트를 다운받아 도커 컴포즈 파일이 있는 경로로 이동하여 docker compose up 명령어를 통해 애플리케이션을 실행시킬 수 있습니다.

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

위에서 설명했던 프로젝트에 프론트엔드와 백엔드를 업데이트 한 분산 애플리케이션을 만들었습니다. 이 애플리케이션은 리액트로 구현된 웹 프론트엔드, 자바 스프링으로 구현된 웹 백엔드, MySQL 데이터베이스로 구현되어있습니다.

https://github.com/nefertirii/docker-example/tree/7771f7026e62daea5142bb9e955e3e13c9d16288

세 개의 컨테이너를 차례로 실행시켜 애플리케이션을 시작하고 모든 컨테이너가 동일한 도커 가상 네트워크에 약속된 이름으로 접속되어 서로 통신합니다. 이러한 일을 도커 컴포즈가 대신해줍니다.

아래의 컴포즈 스크립트를 보면 services 필드에 서로 다른 유형의 서비스가 정의되어 있습니다. 프론트엔드 서비스는 80 포트를 공개하였고 backend 서비스에 의존합니다. depends_on 항목을 백엔드 서비스가 실행된 이후에 프론트엔드 서비스가 실행됩니다.

version: '3.7'

services:

  frontend:
    build:
      context: ./frontend
    image: react-frontend:1.0.0 
    ports:
      - "80:80"
    networks:
      - app-net
    depends_on:
      - backend

  backend:
    build:
      context: ./backend
    image: spring-backend:1.0.0 
    ports:
      - "8080"
    networks:
      - app-net
    depends_on:
      - db

  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: root1234
      MYSQL_DATABASE: board
    ports:
      - "3306:3306"
    networks:
      - app-net
      
networks:
  app-net:

위의 프로젝트를 다운받아 도커 컴포즈 파일이 있는 프로젝트 루트 디렉터리에서 다음 명령어를 실행합니다.

docker compose up -d

애플리케이션 구동이 끝나면 http://localhost/posts 에 접속합니다. 간단한 게시판 페이지가 출력됩니다.

컴포즈 파일을 사용해 여러 개의 컨테이너로 구성된 애플리케이션을 마치 한 덩어리처럼 다룰 수 있습니다. 백엔드 서비스는 상태가 없으므로 컨테이너를 늘리는 방법으로 스케일 아웃할 수 있습니다. 프론트엔드 컨테이너가 백엔드에 데이터를 요청하면 도커가 여러 개의 백엔드 컨테이너에 이 요청을 고르게 분배해 줍니다.

docker compose up -d --scale backend=3

게시판 페이지에서 몇 번 새로고침 한 이후에 아래 명령을 통해 백엔드 컨테이너의 로그를 확인해봅니다.

docker compose logs --tail=3 backend

명령 실행 후 아래와 같이 출력됩니다. 게시판 목록 페이지에서 새로고침을 하면 프론트엔드에서 백엔드에 게시글 목록 API를 호출하고 이 요청을 세 개의 컨테이너가 고르게 나눠서 처리합니다. --tail=3 파라미터는 각 백엔드 컨테이너의 마지막 로그 3개를 출력하라는 옵션입니다.

docker compose logs --tail=3 backend
project-backend-2  | 2023-06-23T15:23:32.759Z DEBUG 1 --- [nio-8080-exec-5] org.hibernate.SQL                        : select p1_0.id,p1_0.content,p1_0.title from post p1_0
project-backend-2  | 2023-06-23T15:23:33.167Z DEBUG 1 --- [nio-8080-exec-8] org.hibernate.SQL                        : select p1_0.id,p1_0.content,p1_0.title from post p1_0
project-backend-2  | 2023-06-23T15:23:33.575Z DEBUG 1 --- [nio-8080-exec-7] org.hibernate.SQL                        : select p1_0.id,p1_0.content,p1_0.title from post p1_0
project-backend-3  | 2023-06-23T15:23:32.618Z DEBUG 1 --- [nio-8080-exec-8] org.hibernate.SQL                        : select p1_0.id,p1_0.content,p1_0.title from post p1_0
project-backend-3  | 2023-06-23T15:23:33.034Z DEBUG 1 --- [io-8080-exec-10] org.hibernate.SQL                        : select p1_0.id,p1_0.content,p1_0.title from post p1_0
project-backend-3  | 2023-06-23T15:23:33.435Z DEBUG 1 --- [nio-8080-exec-9] org.hibernate.SQL                        : select p1_0.id,p1_0.content,p1_0.title from post p1_0
project-backend-1  | 2023-06-23T15:23:32.883Z DEBUG 1 --- [io-8080-exec-10] org.hibernate.SQL                        : select p1_0.id,p1_0.content,p1_0.title from post p1_0
project-backend-1  | 2023-06-23T15:23:33.303Z DEBUG 1 --- [nio-8080-exec-3] org.hibernate.SQL                        : select p1_0.id,p1_0.content,p1_0.title from post p1_0
project-backend-1  | 2023-06-23T15:23:33.707Z DEBUG 1 --- [nio-8080-exec-4] org.hibernate.SQL                        : select p1_0.id,p1_0.content,p1_0.title from post p1_0

도커 컴포즈를 통해 전체 애플리케이션을 관리할 수 있습니다. 컨테이너를 중지하거나 시작할 수 있습니다. 이러한 작업은 일반 도커 명령어를 통해서도 할 수 있습니다. 도커 컴포즈는 별도의 명령어가 있지만 내부적으로는 도커 API를 사용하므로 일반 도커 명령어와 똑같이 관리할 수 있습니다.

아래 명령어로 애플리케이션의 모든 컨테이너를 중지합니다.

docker compose stop

아래 명령어로 중지됬던 기존 컨테이너가 재시작됩니다.

docker compose start

아래 명령어로 컨테이너 목록을 확인합니다.

docker compose ps

도커 컴포즈는 클라이언트 측에서 동작하는 도구입니다. 도커 컴포즈 명령을 실행하면 컴포즈 파일의 내용에 따라 도커 API로 요청을 보냅니다. 도커 엔진 자체는 컨테이너를 실행할 뿐, 여러 컨테이너가 하나의 애플리케이션으로 동작하는지 모릅니다. 이를 아는 것은 YAML 형식의 컴포즈 파일을 읽어 애플리케이션의 구조를 이해한 컴포즈 뿐입니다. 따라서 컴포즈를 사용해 애플리케이션을 관리하려면 컴포즈 파일을 작성하고 이 파일을 읽을 수 있게 해야합니다.

컴포즈 파일을 수정하거나 도커 명령어로 직접 애플리케이션을 수정하면, 애플리케이션이 컴포즈 파일에 기술된 구조와 불일치 될 수 있습니다. 이 상태에서 도커 컴포즈로 애플리케이션을 관리하려고 하면 비정상적인 동작을 보일 수 있습니다.

앞에서 컴포즈 파일 수정 없이 백엔드 서비스 컨테이너를 세 개로 스케일링했던 경우가 이에 해당합니다. 이 상태에서 컴포즈로 애플리케이션을 재시작하면 백엔드 서비스는 다시 한 개의 컨테이너만으로 동작합니다.

아래 명령어로 애플리케이션을 중지하고 컨테이너를 모두 제거합니다.

docker compose down

애플리케이션을 다시 시작합니다. 실행 중인 컨테이너가 없으므로 컴포즈 파일에 정의된 내용대로 모든 서비스를 다시 생성합니다.

docker compose up -d

컴포즈 파일에는 스케일 아웃에 대한 정의가 없으므로 컨테이너 목록을 조회하면 백엔드 컨테이너는 하나로 돌아갑니다.

docker ps

따라서 도커 컴포즈로 애플리케이션을 배포하면 컴포즈 파일을 통해 리소스를 관리해야합니다.

도커 컨테이너 간의 통신

분산 애플리케이션의 구성 요소인 컨테이너는 서로 어떻게 통신할까요?

컨테이너는 도커 엔진으로부터 가상 IP 주소를 부여받고 모두 같은 도커 네트워크로 연결되어 이 가상 IP 주소를 통해 서로 통신할 수 있습니다. 그러나 애플리케이션의 컨테이너가 교체되면 IP 주소도 변경됩니다. IP 주소가 변경돼도 문제가 없도록 도커에서 DNS를 이용해 서비스 디스커버리 기능을 제공합니다.

DNS는 IP 주소를 도메인과 연결하는 기능을 제공하는 시스템입니다. 도커에도 DNS 서비스가 내장되어 있습니다. 컨테이너에서 실행중인 애플리케이션이 다른 구성 요소에 접근하기 위해 DNS 서비스를 사용합니다. 컨테이너 이름을 도메인으로 설정하여 조회하면 해당 컨테이너의 IP 주소를 찾아줍니다. 이런 방법으로 도커 네트워크 상에 있는 다른 컨테이너의 정보를 사용할 수 있습니다. 만약 도메인이 가리키는 대상이 컨테이너가 아니면 도커 엔진이 동작하는 호스트 컴퓨터에 요청을 보내 호스트 컴퓨터가 속한 네트워크나 인터넷의 IP 주소를 조회합니다.

위에서 사용한 예제 프로젝트의 프론트엔드 서비스의 Nginx 설정파일을 보면 backend:8080 처럼 백엔드 서버의 IP 주소 대신 컴포즈 파일의 서비스 이름을 도메인 이름으로 사용합니다.

upstream backend {
  server backend:8080;
}

server {
  listen 80;

  location / {
    root /app;
    index index.html index.htm;
    try_files $uri $uri/ /index.html;
  }

  location /api {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_pass http://backend;
  }

또한 백엔드의 application.yml 설정 파일을 보면 jdbc:mysql://db:3306/board 처럼 데이터베이스 IP 주소 대신 db 라는 컴포즈 파일의 서비스 이름을 도메인 이름으로 사용합니다.

spring:
  h2:
    console:
      enabled: true
      path: /h2-console

  datasource:
    url: jdbc:mysql://db:3306/board?serverTimezone=UTC&characterEncoding=UTF-8
    username: root
    password: root1234
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    database-platform: org.hibernate.dialect.MySQL57Dialect
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        # show_sql: false
        # format_sql: true
        # use_sql_comments: true
        default_batch_fetch_size: 100

logging:
  level:
    org:
      hibernate:
        SQL: DEBUG
        type:
          descriptor:
            sql:
              BasicBinder: TRACE

위의 프로젝트의 백엔드 컨테이너 수를 3개로 늘려 실행해 보겠습니다.

docker compose up -d --scale backend=3

그리고 프론트엔드 컨테이너에 접속합니다.

docker exec -it project-frontend-1 sh

아래 nslookup 명령어를 통해 도메인으로 backend 서비스 이름을 지정하여 그 결과를 조회해봅니다.

nslookup backend

조회된 결과를 보면 아래와 같이 172.18.0.3, 172.18.0.4, 172.18.0.5 세 개의 IP 주소가 출력됩니다. 도커 네트워크에 연결된 모든 컨테이너는 172로 시작하는 IP 주소를 부여 받습니다. 따라서 애플리케이션에서 서비스 이름을 도메인 삼아 접근하면 도커 DNS가 컨테이너의 IP 주소를 찾아주는 것을 알 수 있습니다.

Server:		127.0.0.11
Address:	127.0.0.11:53

Non-authoritative answer:
Name:	backend
Address: 172.18.0.5
Name:	backend
Address: 172.18.0.3
Name:	backend
Address: 172.18.0.4

Non-authoritative answer:

도커의 DNS 시스템은 조회 결과 순서를 매번 변화시킵니다. 따라서 nslookup 명령을 실행 시킬 때마다 결과 순서가 다릅니다. 이를 활용해 컨테이너 간의 트래픽을 고르게 분산시킬 수 있습니다.

도커 컴포즈의 한계점

도커 컴포즈는 복잡한 분산 애플리케이션의 설정을 짧고 명료판 포맷의 파일로 나타낼 수 있게 합니다. YAML 형식의 컴포즈 파일은 애플리케이션 배포 방법을 설명하는 문서 파일보다 낫습니다.

배포 과정이 변경되면 기존에는 배포 과정을 설명하는 문서도 변경하고 배포 과정도 테스트를 해야했습니다. 하지만 컴포즈 파일은 그 자체가 배포 과정이므로 배포 방법이 변경되면 컴포즈 파일만 수정하면 됩니다.

docker compose up 명령을 실행하기만 하면 내가 정의한 상태대로 애플리케이션을 실행할 수 있습니다.

하지만 도커 컴포즈가 할 수 있는 일은 여기까지입니다. 도커 컴포즈는 도커 스웜이나 쿠버네티스 같은 완전한 컨테이너 플랫폼이 아닙니다. 도커 컴포즈에는 애플리케이션이 지속적으로 정의된 상태를 유지하도록 하는 기능이 없습니다. 일부 컨테이너가 오류를 일으키거나 강제로 종료되더라도 docker compose up 명령을 다시 실행하지 않는 한 애플리케이션의 상태를 원래대로 되돌릴 수 없습니다.

0개의 댓글