이전 시간에는 컨테이너 앱의 데이터 영구 저장 방법인 Volume, Bind mount 방식에 대해서 알아보았다.
일반적으로 하나의 앱을 구성하기 위해 프론트 파일과 db를 따로 운영하게 된다. 이런 경우, UI 및 비즈니스 로직을 담고 있는 소스 코드 파일을 패키징한 컨테이너 1개와 database 컨테이너 1개로 운영하게 된다.
이번 시간에는 만약 database를 별도 컨테이너로 운영하게 될 경우 어떻게 연동하는지에 대해서 알아보자!
각각의 컨테이너 독립적인 address를 가지고 운영된다. 따라서 database를 별도 컨테이너로 운영하는 경우에 어플리케이션과 연결하여 네트워크를 만들어줘야한다.
아래 코드를 통해서 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
— network-alias를 통해 mysql 컨테이너의 DNS를 mysql로 할당. 이를 통해 todo-app은 mysql 컨테이너 IP가 아닌 mysql 이라는 DNS로 컨테이너를 연결할 수 있음
todo-mysql-data
라는 볼륨을 호스트에 생성한다음 컨테이너의 /var/lib/mysql
경로에 마운트. MySQL 데이터베이스 파일이 호스트 시스템에 연결됨secret
으로 설정todos
라는 데이터베이스를 생성아마 mysql을 사용해본 사람이라면 패스워드나 db설정 같은 것은 친숙할 것이다.
docker ps로 확인하면 mysql과 node 이미지 컨테이너가 구동 중인것을 볼 수 있다
이 아이디를 사용해서 실행중인 mysql 컨테이너에 접속하여 mysql을 실행시킨다
docker exec -it {container IP} mysql -u root -p
실행하면 비밀번호를 치라고 나오는데 아까 mysql 컨테이너 이미지를 실행할 때 secret으로 비번설정을 했다. 그러니까 secret을 치면 된다!
자 이제 mysql 컨테이너에 들어가서 mysql에 접속도 가능해졌다. todo-app 컨테이너가 이 mysql 컨테이너에 연동하기 위해 어떤 방법을 쓰는지 알아보자.
먼저 nicolaka라는 컨테이너를 todo-app 네트워크에 실행시켜보자.
docker run -it --network todo-app nicolaka/netshoot
접속 후에 아래 명령어를 치면 여러 정보들을 보여준다.
dig mysql
Answer section을 보면 mysql에 대응되는 IP주소가 172.19.0.2로 나와있다.
하지만 network alias 명령어를 통해서 mysql 이름으로 DNS를 설정했고 IP를 사용하지 않아도 컨테이너를 찾을 수 있는 것이다!
이것이 중요한 이유는 mysql같은 database는 환경 설정에서 host IP를 설정하기 때문이다. 물론 실행한 뒤에 컨테이너 IP를 찾아서 넣어줘도 되지만 번거롭기 때문에 네트워크 내에서 DNS로 찾을 수 있도록 미리 설정해 준 것이다.
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"
실행 후 docker ps로 id를 찾고 docker logs로 로그를 확인한다.
docker logs {container IP}
mysql이 연결된 것을 볼 수 있다!
현재 데이터베이스에는 데이터가 없다. 앱을 실행하고 데이터를 넣어보자
이후 mysql에 접속하여 명령어를 사용하여 살펴보면 내부 db에 todo_items라는 테이블 내에 데이터가 들어가는 것을 볼 수 있다.
ex.
show databases;
show tables;
select * from {table name}
** 참고로 mysql에는 한글 패치가 안되어있어서 한글로 데이터를 넣을 경우 ???로 뜬다
웹 개발 코드는 친숙하지 않지만 내부 구조를 살펴보는 것도 도움이 될 것 같다.
// 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],
);