[도커 튜토리얼] 5. MySQL을 활용한 multi-container app 구동

Jisu·2023년 12월 1일
0

Docker

목록 보기
5/6
post-thumbnail

배경

이전 시간에는 컨테이너 앱의 데이터 영구 저장 방법인 Volume, Bind mount 방식에 대해서 알아보았다.

일반적으로 하나의 앱을 구성하기 위해 프론트 파일과 db를 따로 운영하게 된다. 이런 경우, UI 및 비즈니스 로직을 담고 있는 소스 코드 파일을 패키징한 컨테이너 1개와 database 컨테이너 1개로 운영하게 된다.

이번 시간에는 만약 database를 별도 컨테이너로 운영하게 될 경우 어떻게 연동하는지에 대해서 알아보자!

컨테이너와 컨테이너의 통신

각각의 컨테이너 독립적인 address를 가지고 운영된다. 따라서 database를 별도 컨테이너로 운영하는 경우에 어플리케이션과 연결하여 네트워크를 만들어줘야한다.


네트워크 생성 및 Mysql 이미지 빌드

아래 코드를 통해서 docker 앱에 todo-app이라는 이름으로 네트워크를 생성한다.

docker network create todo-app

방금 생성한 네트워크 내에서 mysql을 빌드한다

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:8.0
  • todo-app이라는 네트워크 내에서 이미지 빌드

— network-alias를 통해 mysql 컨테이너의 DNS를 mysql로 할당. 이를 통해 todo-app은 mysql 컨테이너 IP가 아닌 mysql 이라는 DNS로 컨테이너를 연결할 수 있음

  • -v : todo-mysql-data라는 볼륨을 호스트에 생성한다음 컨테이너의 /var/lib/mysql 경로에 마운트. MySQL 데이터베이스 파일이 호스트 시스템에 연결됨
  • -e: MYSQL_ROOT_PASSWORD=secret`: MySQL 루트 사용자의 비밀번호를 secret으로 설정
  • -e MYSQL_DATABASE=todos`: MySQL에 todos라는 데이터베이스를 생성

아마 mysql을 사용해본 사람이라면 패스워드나 db설정 같은 것은 친숙할 것이다.

docker ps로 확인하면 mysql과 node 이미지 컨테이너가 구동 중인것을 볼 수 있다

업로드중..

이 아이디를 사용해서 실행중인 mysql 컨테이너에 접속하여 mysql을 실행시킨다

docker exec -it {container IP} mysql -u root -p

실행하면 비밀번호를 치라고 나오는데 아까 mysql 컨테이너 이미지를 실행할 때 secret으로 비번설정을 했다. 그러니까 secret을 치면 된다!

업로드중..


Network DNS

자 이제 mysql 컨테이너에 들어가서 mysql에 접속도 가능해졌다. todo-app 컨테이너가 이 mysql 컨테이너에 연동하기 위해 어떤 방법을 쓰는지 알아보자.

먼저 nicolaka라는 컨테이너를 todo-app 네트워크에 실행시켜보자.

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

Network의 DNS

접속 후에 아래 명령어를 치면 여러 정보들을 보여준다.

dig mysql

업로드중..

Answer section을 보면 mysql에 대응되는 IP주소가 172.19.0.2로 나와있다.

하지만 network alias 명령어를 통해서 mysql 이름으로 DNS를 설정했고 IP를 사용하지 않아도 컨테이너를 찾을 수 있는 것이다!

이것이 중요한 이유는 mysql같은 database는 환경 설정에서 host IP를 설정하기 때문이다. 물론 실행한 뒤에 컨테이너 IP를 찾아서 넣어줘도 되지만 번거롭기 때문에 네트워크 내에서 DNS로 찾을 수 있도록 미리 설정해 준 것이다.


MySQL 연결하고 앱 구동하기

to-do app을 실행할 때 MySQL의 환경 변수를 같이 세팅해준다.

docker run -dp 127.0.0.1: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:18-alpine \
  sh -c "yarn install && yarn run dev"
  • host는 mysql이다. 같은 네트워크라서 mysql 이름으로도 IP주소를 찾아 접속할 수 있다
  • user는 root 유저, 비번은 secret으로 설정한다.
  • 이전에 빌드한 이미지로 사용하고 sh를 통해 컨테이너가 실행되고 실행할 cmd를 적어준다

실행 후 docker ps로 id를 찾고 docker logs로 로그를 확인한다.

docker logs {container IP} 

업로드중..

mysql이 연결된 것을 볼 수 있다!

현재 데이터베이스에는 데이터가 없다. 앱을 실행하고 데이터를 넣어보자

업로드중..

이후 mysql에 접속하여 명령어를 사용하여 살펴보면 내부 db에 todo_items라는 테이블 내에 데이터가 들어가는 것을 볼 수 있다.

ex. 
show databases; 
show tables;
select * from {table name}

업로드중..

** 참고로 mysql에는 한글 패치가 안되어있어서 한글로 데이터를 넣을 경우 ???로 뜬다


리액트 참고

  • 사실 mysql만 연동한다고 되는 것이아니라 내부 코드로 mysql에 데이터를 삽입하는 과정 코드까지 다 들어가있어야 한다. 테스트 파일에는 다음과 같은 코드가 있었다.
  • 예를 들어 getPromise 함수를 통해 Promise 타입을 반환하고 있다.

웹 개발 코드는 친숙하지 않지만 내부 구조를 살펴보는 것도 도움이 될 것 같다.


// getItems (routing)
// db의 getItems 함수를 실행하도록 라우팅
const db = require('../persistence');

module.exports = async (req, res) => {
    const items = await db.getItems();
    res.send(items);
};

// mysql
// mysql 호스트, pwd 등 설정
// mysql에 쿼리하여 데이터를 얻고 보내주는 함수 선언
const waitPort = require('wait-port');
const fs = require('fs');
const mysql = require('mysql2');

const {
    MYSQL_HOST: HOST,
    MYSQL_HOST_FILE: HOST_FILE,
    MYSQL_USER: USER,
    MYSQL_USER_FILE: USER_FILE,
    MYSQL_PASSWORD: PASSWORD,
    MYSQL_PASSWORD_FILE: PASSWORD_FILE,
    MYSQL_DB: DB,
    MYSQL_DB_FILE: DB_FILE,
} = process.env;

let pool;

async function init() {
    const host = HOST_FILE ? fs.readFileSync(HOST_FILE) : HOST;
    const user = USER_FILE ? fs.readFileSync(USER_FILE) : USER;
    const password = PASSWORD_FILE ? fs.readFileSync(PASSWORD_FILE) : PASSWORD;
    const database = DB_FILE ? fs.readFileSync(DB_FILE) : DB;

    await waitPort({ 
        host, 
        port: 3306,
        timeout: 10000,
        waitForDns: true,
    });

    pool = mysql.createPool({
        connectionLimit: 5,
        host,
        user,
        password,
        database,
        charset: 'utf8mb4',
    });

    return new Promise((acc, rej) => {
        pool.query(
            'CREATE TABLE IF NOT EXISTS todo_items (id varchar(36), name varchar(255), completed boolean) DEFAULT CHARSET utf8mb4',
            err => {
                if (err) return rej(err);

                console.log(`Connected to mysql db at host ${HOST}`);
                acc();
            },
        );
    });
}

async function getItems() {
    return new Promise((acc, rej) => {
        pool.query('SELECT * FROM todo_items', (err, rows) => {
            if (err) return rej(err);
            acc(
                rows.map(item =>
                    Object.assign({}, item, {
                        completed: item.completed === 1,
                    }),
                ),
            );
        });
    });
}

// apps.js
// items로 라우팅하여 받아오는 데이터로 UI를 그리는 함수

function TodoListCard() {
    const [items, setItems] = React.useState(null);

    React.useEffect(() => {
        fetch('/items')
            .then(r => r.json())
            .then(setItems);
    }, []);

    const onNewItem = React.useCallback(
        newItem => {
            setItems([...items, newItem]);
        },
        [items],
    );

Reference

Docker Tutorial - Multi container apps

profile
비즈니스에 관심많은 DevOps Engineer 장지수입니다.

0개의 댓글