본문 바로가기

DevOps & Infra/Docker

[Infrastructure/Docker] 튜토리얼 살펴보기#006. 다중 컨테이너 앱 구축

다중 컨테이너 앱 구축

지금까지 우리는 하나의 컨테이너에서 동작하는 애플리케이션을 통해 작업했습니다. 하지만 대부분의 실서비스 되는 애플리케이션은 여러 개의 서비스가 결합되어 운용됩니다. 이번 섹션에서는 애플리케이션 스택에 MySQL 데이터베이스를 추가해보도록 하겠습니다.

이처럼 두 가지 이상의 서비스를 결합해야 하는 경우, 각 서비스를 서로 다른 컨테이너에서 운용하는 것이 적합합니다. 컨테이너를 분리하는 이유는 다음과 같습니다.

  • 데이터베이스의 확장은 보수적이지만, API와 프론트앤드를 확장할 일은 제법 잦은 업무입니다. 각 서비스에 대한 확장 시나리오가 서로 다르므로, 서비스에 따라서 컨테이너를 분리하면 골치아픈 일을 줄일 수 있습니다.
  • 별도의 컨테이너를 운용하면, 각 서비스 별 업데이트 버전을 별도로 관리할 수 있습니다.
  • 각 서비스 별로 머신을 분리하고 싶을 경우, 컨테이너가 분리되어 있다면 각 머신에서 서로 다른 컨테이너를 시작하기만 하면 됩니다. 일반적으로 서비스 별로 요구하는 머신의 퍼포먼스 등의 차이가 발생하므로, 컨테이너를 분리하는 것이 합리적입니다.
  • 컨테이너가 여러 개의 프로세스를 실행하려면 프로세스 관리자(Process manager)가 필요합니다. 다중 프로세스를 운용하는 컨테이너의 실행 과정이 더 번거롭습니다.

따라서 튜토리얼의 Todo 애플리케이션과 MySQL은 다음 그림과 같은 구조가 되도록 작업합니다. 하나의 박스는 개별 컨테이너를 의미합니다.

컨테이너 간의 네트워킹

기본적으로 컨테이너는 호스트 시스템에서 독립되서 실행됩니다. 당연한 얘기지만, 도커의 컨테이너가 추구하는 이상적인 구조이기 때문입니다. 컨테이너가 독립되었다는 것은, 다시 말해 고립되었다는 의미와 같습니다. 즉, 우리는 호스트 시스템에서 여러 개의 컨테이너를 실행하고 있다는 것을 알고 있으나, 각 컨테이너는 다른 컨테이너의 존재를 알지 못합니다. 그렇다면 컨테이너 간의 통신은 어떻게 해야 할까요?

컨테이너 간의 통신은 네트워크를 통해 해결합니다. 각 컨테이너가 일종의 가상 머신이라는 가정하에, 서로 간에 IP 주소를 사용하여 통신 할 수 있습니다. 단순히 동일한 네트워크 망에만 연결되어 있으면 됩니다.

컨테이너를 네트워크에 연결하는 방법은 두 가지가 있습니다.

  • 컨테이너를 시작하면서 네트워크를 할당
  • 기존 컨테이너에 네트워크를 할당

MySQL 컨테이너 실행

MySQL 컨테이너의 실행에 앞서, 도커 API를 사용하여 네트워크를 만들어 보겠습니다. 

$ docker network create [네트워크 이름]

네트워크 이름은 사용자가 임의로 지정합니다. 네트워크 이름은 나중에 컨테이너에 네트워크를 할당할 때 사용됩니다.

$ docker network create todo-app
53e3833329d26a85863e8a98733027ebc130a728d344282760bdab5ae3f1b1d0

이제 MySQL 컨테이너를 실행하고 네트워크를 할당합니다. 그리고 MySQL이 데이터베이스를 초기화하기 위한 몇 가지 환경 변수를 정의합니다(MySQL Docker Hub listening에서 환경 변수-Envrionment Variables 섹션을 참고합니다).

$ docker run -d \
    --network todo-app --network-alias mysql \
    -v todo-mysql-data:/var/lib/mysql \
    -e MYSQL_ROOT_PASSWORD=secret \
    -e MYSQL_DATABASE=todos \
    mysql:5.7

PowerShell 사용자라면, 다음 명령문을 사용합니다.

$ docker run -d `
    --network todo-app --network-alias mysql `
    -v todo-mysql-data:/var/lib/mysql `
    -e MYSQL_ROOT_PASSWORD=secret `
    -e MYSQL_DATABASE=todos `
    mysql:5.7

명령문을 하나씩 살펴보겠습니다.

  • -d
    컨테이너를 데몬으로 실행합니다.
  • --network todo-app --network-alias mysql
    컨테이너가 실행되면서 todo-app 이름의 네트워크를 할당(--network todo-app)합니다. --network-alias 플래그는 할당 네트워크의 별칭을(mysql) 지정합니다. 이 내용과 관련해서는 이어서 보충 설명하도록 하겠습니다.
  • -v todo-mysql-data:/var/lib/mysql
    todo-mysql-data 볼륨을 사용하여 MySQL 데이터를 저장하는 /var/lib/mysql 경로에 마운트합니다. 하지만 우리는 todo-mysql-data 볼륨을 생성한적이 없습니다. 그렇다면 도커는 이 플래그를 어떻게 처리할까요?
    도커는 아직 생성된적 없는 볼륨을 지정할 경우 자동으로 볼륨을 생성합니다.
  • -e MYSQL_ROOT_PASSWORD=secret
    MySQL 환경 변수를 지정합니다. 이 환경 변수는 필수로 지정해야 하며, MySQL root 슈퍼 계정에 대한 암호를 지정합니다. 예시에서는 암호를 secret으로 지정하고 있습니다.
  • -e MYSQL_DATABASE=todos
    MySQL 환경 변수를 지정합니다. 이 환경 변수는 선택적이며, 이미지 생성 단계에서 같이 생성하려는 데이터베이스의 이름을 지정합니다. 예시에서는 todos 이름의 데이터베이스를 생성하고 있습니다.
  • mysql:5.7
    MySQL 5.7버전의 이미지를 사용합니다.

명령문이 제대로 실행되었다면, 도커 컨테이너에서 mysql:5.7 이미지를 갖는 컨테이너가 실행 중인 것을 확인 할 수 있습니다(또는 docker ps 명령문으로 확인합니다).

아직 한 가지 더 확인 할 것이 남아있습니다. 컨테이너가 데이터베이스를 제대로 실행하고 있는지 확인하기 위해서 데이터베이스에 직접 연결해보도록 하겠습니다.

$ docker exec -it [컨테이너 ID] mysql -p

컨테이너 ID는 docker ps 명령문을 사용하여 확인합니다. 이때 참조하는 컨테이너 ID는 MySQL 이미지를 실행중인 컨테이너입니다. 명령문을 입력하고, 앞서 MySQL 컨테이너 실행 명령문에 MySQL 환경 변수 플래그로 지정한 root 슈퍼 계정 비밀번호를 기입합니다. 다음과 같이 MySQL에 연결되었다면 정상입니다.

$ docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                    NAMES
e467f0bef7c3   mysql:5.7                "docker-entrypoint.s…"   11 minutes ago   Up 11 minutes   3306/tcp, 33060/tcp      upbeat_bhaskara
2546cd6b1552   node:12-alpine           "docker-entrypoint.s…"   35 hours ago     Up 35 hours     0.0.0.0:3000->3000/tcp   bold_liskov
2878508bb6d4   docker/getting-started   "/docker-entrypoint.…"   5 days ago       Up 5 days       0.0.0.0:80->80/tcp       jovial_banach
$ docker exec -it e467f0bef7c3 mysql -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.37 MySQL Community Server (GPL)

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

MySQL 환경 변수 플래그로 지정한 데이터베이스가 정상적으로 생성되었는지도 확인합니다. 다음과 같이 todos 데이터베이스가 항목에 노출되면 정상입니다.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| todos              |
+--------------------+
5 rows in set (0.00 sec)

우리가 터미널에서 실행중인 프로세스는 MySQL 클라이언트입니다. 필요한 내용을 모두 확인하였으니, MySQL 클라이언트를 종료하기 위해서 exit를 입력합니다.

mysql> exit
Bye

MySQL 컨테이너의 IP주소 확인

이번에는 MySQL 컨테이너를 우리의 Todo 애플리케이션에 연결해보도록 하겠습니다. 어떻게 가능할까요? 앞서 컨테이너가 서로 동일한 네트워크 망에 존재하면, 각자의 IP 주소를 사용하여 네트워킹 할 수 있다고 하였습니다.

결과적으로 MySQL 컨테이너의 IP 주소를 알아야 모든 것이 가능해집니다. 그래야 Todo 애플리케이션이 MySQL 컨테이너에 연결 할 수 있으니까요.

IP 주소를 확인하기 위해서 nicolaka/netshoot 컨테이너를 실행합니다. nicolaka/netshoot는 여러 네트워킹 문제를 해결하거나 디버그하는데 도움이 되는 이미지입니다. 이때 실행 컨테이너가 우리가 생성한 todo-app 네트워크에 연결되도록 합니다.

$ docker run -it network todo-app nicolaka/netshoot

컨테이너를 데몬으로 실행하지 않았기 때문에, 컨테이너가 실행하는 앱을 터미널에서 제어합니다. 이제 nicolaka/netshoot 컨테이너를 사용하여 mysql의 IP주소를 확인할 수 있습니다. mysql은 MySQL 컨테이너 실행에 연결된 todo-app 네트워크의 별칭입니다. nicolaka/netshoot에서 DNS 툴인 dig 명령문을 사용해봅시다.

Welcome to Netshoot! (github.com/nicolaka/netshoot)

$ dig mysql

; <<>> DiG 9.16.22 <<>> mysql
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15112
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;mysql.                         IN      A

;; ANSWER SECTION:
mysql.                  600     IN      A       172.18.0.2

;; Query time: 4 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Mon Feb 28 07:20:49 UTC 2022
;; MSG SIZE  rcvd: 44

실행 결과에서 'ANSWER SECTION'을 볼까요? 172.18.0.2가 MySQL 컨테이너에 연결하기 위한 IP 주소입니다. 이제 IP 주소를 확인하였으니 Todo 애플리케이션에 연결할 수 있게 되었습니다.

이번 섹션을 마무리하기 전에 짚고 넘어가야할 내용이 있습니다. 사실 우리가 dig 명령문을 사용할때 지정한 mysql은 일반적으로 유효한 호스트 이름이 아닙니다. 하지만 도커는 네트워크 별칭이 있는 컨테이너의 IP 주소를 확인할 수 있도록 합니다. 우리가 MySQL 컨테이너를 실행할 때 사용한 플래그를 확인해볼까요?

--network todo-app --network-alias mysql

MySQL 컨테이너를 실행하면서 todo-app 네트워크를 할당하고, 네트워크에 mysql이라는 별칭을 부여했습니다. 이제부터 mysql은 호스트 이름처럼 사용 될 수 있습니다. 다시말해서, MySQL 컨테이너와 통신하고자 한다면 단순히 mysql 호스트를 사용 할 수 있다는 것을 의미합니다(네이버 서버의 IP를 알지 못하더라도, 웹 브라우저 URL에 "www.naver.com"를 입력하기만 해도 되는 것처럼 말이죠).

Todo 애플리케이션과 연결

이제 MySQL 컨테이너와 Todo 애플리케이션을 서로 연결하는 일만 남았습니다. MySQL 연결에 필요한 설정을 위해 몇가지 환경 변수를 지원하고 있습니다.

  • MYSQL_HOST
    연결하려는 MySQL 서버의 호스트 이름(또는 IP 주소)를 지정합니다.
  • MYSQL_USER
    연결에 사용되는 사용자 이름입니다.
  • MYSQL_PASSWORD
    연결에 사용되는 사용자 비밀번호입니다.
  • MYSQL_DB
    연결에 사용되는 데이터베이스 이름입니다.

사실 환경 변수를 사용하여 연결 설정을 지정하는 것은 프로덕션 단계에서는 적합하지 않습니다. 이 방법은 개발 목적에서만 권장하며, 보다 안전한 메커니즘을 통해 연결 설정을 하는 것이 좋습니다.

이전에 실행 중인 Todo 컨테이너를 종료하고, 다음 명령문을 사용하여 Todo 컨테이너를 다시 시작합니다.

$ docker run -dp 3000:3000 \
  -w /app -v "$(pwd):/app" \
  --network todo-app \
  -e MYSQL_HOST=mysql \
  -e MYSQL_USER=root \
  -e MYSQL_PASSWORD=secret \
  -e MYSQL_DB=todos \
  node:12-alpine \
  sh -c "yarn install && yarn run dev"

컨테이너에 대한 로그를 보면, MySQL 서버에 연결되었음을 확인 할 수 있습니다.

$ docker logs b37127467c5b
yarn install v1.22.17
[1/4] Resolving packages...
warning Resolution field "ansi-regex@5.0.1" is incompatible with requested version "ansi-regex@^2.0.0"
warning Resolution field "ansi-regex@5.0.1" is incompatible with requested version "ansi-regex@^3.0.0"
success Already up-to-date.
Done in 0.86s.
yarn run v1.22.17
$ nodemon src/index.js
[nodemon] 2.0.13
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
Waiting for mysql:3306.
Connected!
Connected to mysql db at host mysql
Listening on port 3000

웹 브라우저를 열어 Todo 앱에 접속하여 항목을 추가해볼까요?

MySQL 서버에 접속하여 항목이 제대로 추가되었는지 확인합니다. 기존 앱은 SQLite 데이터베이스를 사용하였으나, 이제는 MySQL 데이터베이스에 연동되어 동작하는 것을 볼 수 있습니다.

$ docker exec -it [컨테이너 ID] mysql -p todos

mysql> select * from todo_items;
+--------------------------------------+-------------------------------------+-----------+
| id                                   | name                                | completed |
+--------------------------------------+-------------------------------------+-----------+
| 29cf9ac9-4127-42d7-a093-5bd3bc153b7e | this will be insert in mysql server |         0 |
+--------------------------------------+-------------------------------------+-----------+
1 row in set (0.00 sec)