Kwon's Study Blog !
[Docker] 복잡한 어플을 실제로 배포해보기 (개발 환경 부분) 본문
현재 글은
https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4-ci/dashboard
을 수강하며 정리한 내용입니다.
문제시 비공개로 처리 하도록 하겠습니다.
저번 글에선 도커를 이용해서 리액트앱을 만들었습니다.
하지만 실제로 애플리케이션을 만들 때는 Front End 부분만을 이용하는 것이 아닌 Back End 서버도 필요하고 DB등등 많은 것이 필요하기 때문에 이번 글에선 좀 더 많은 컨테이너를 사용해서 좀 더 실무에서 사용할 수 있는 애플리케이션을 만들어보겠습니다.
애플리케이션의 대략적인 구성도입니다.
이번 글에선 클라이언트에서 아무글이나 입력하면 리액트를 통해 노드로 전달된 이후에 Mysql DB에 저장한 후 저장된 것을 화면에 보여주는 앱을 구현하겠습니다.
그리고 컨테이너를 재시작해도 DB에 저장된 데이터를 남아있게 해주겠습니다.
Multi Container 앱을 위한 전체적인 설계
굉장히 많은 방법으로 설계를 할 수 있지만 우선 두 가지 방법의 설계를 생각해보겠습니다.
- Nginx로 클라이언트에서 오는 요청을 백엔드 서버와 프런트 서버로 나눠주는 구조
장점 :
1. Request를 보낼 때 URL 부분을 host 이름이 바뀌어도 변경시켜주지 않아도 됩니다.
2. 포트가 바뀌어도 변경을 안해 주어도 됩니다.
단점 :
Nginx 설정, 전체 설계가 다소 복잡합니다.
axios.get('/api/values')
- Nginx는 프런트 서버로만 사용하여 클라이언트에서 정적 파일을 요구할 때 제공해주는 구조
장점 :
설계가 다소 간단하여 구현하는게 더 쉽습니다.
단점 :
host name 이나 port변경이 있을 때 Request URL도 변경해줘야 합니다.
axios.get('http://localhost:5000/api/values')
->
axios.get('http://ksoon1985.com:5000/api/values')
전자의 경우의 설계가 더 복잡하지만 장점이 명확하므로 이 부분으로 실습을 해 나가겠습니다.
애플리케이션의 전체적인 구성도입니다.
이전 글인 [간단한 애플을 실제로 배포해보기] 에선
Travis CI 에서 테스트가 성공하면 테스트 소스를 AWS에 전달했지만
이번 글에선
Travis CI에서 테스트가 성공하면 도커 파일을 이미지로 만들고(빌드) -> 도커 허브로 전달해서
도커 허브에서 AWS로 전달을 하도록 하겠습니다.
Node JS 구성하기
docker-fullstack-app 폴더를 만든 후 하위에 backend 폴더를 만들고 터미널을 킨 후 아래 명령어를 입력합니다.
npm init
하면 package.json 파일이 생깁니다.
그 후 dependency 와 script를 수정합니다.
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start":"node server.js",
"dev":"nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"express":"4.16.3",
"mysql":"2.16.0",
"nodemon":"1.18.3",
"body-parser":"1.19.0"
},
"author": "",
"license": "ISC"
}
backend 폴더아래에 server.js 를 만듭니다.
앱을 시작할 때 node server.js로 시작하니 server.js파일이 시작점이 됩니다.
// 필요한 모듈들을 가져오기
const express = require("express");
const bodyParser = require("body-parser");
// Express 서버를 생성
const app = express();
// json 형태로 오는 요청의 본문을 해석해줄수있게 등록
app.use(bodyParser.json());
// 5000번 포트로 서버가 시작되면
app.listen(5000,()=>{
console.log("애플리케이션이 5000번 포트에서 시작 됐습니다.");
})
mysql을 연결하기 위한 db.js 파일을 작성합니다.
// mysql 모듈을 불러옴
var mysql = require("mysql");
/*
Host, user name, password, db name 을 명시해서
Pool을 생성해줌
그리고 다른 곳에서 사용할 수 있게 export함.
*/
var pool = mysql.createPool({
connectionLimit : 10,
host: "mysql",
user: "root",
password: "ksoon1985",
database: "myapp",
});
exports.pool = pool;
mysql 을 연동하여 API를 만든 server.js
// 필요한 모듈들을 가져오기
const express = require("express");
const bodyParser = require("body-parser");
// export된 pool을 server.js에서 불러오기
const db = require("./db");
// Express 서버를 생성
const app = express();
// json 형태로 오는 요청의 본문을 해석해줄수있게 등록
app.use(bodyParser.json());
// 테이블 생성하기
db.pool.query(`CREATE TABLE lists(
id INTEGER AUTO_INCREMENT,
value TEXT,
PRIMARY KEY (id)
)`,(err,results,fields)=>{
console.log('results', results)
})
// 5000번 포트로 서버가 시작되면
app.listen(5000,()=>{
console.log("애플리케이션이 5000번 포트에서 시작 됐습니다.");
})
// API
// DB에 lists 테이블에 있는 모든 데이터를 프론트 서버에 보내줌
app.get('/api/values', function(req,res,next){
db.pool.query('SELECT * FROM lists;',
(err,results,fields) => {
if(err)
return res.status(500).send(err)
else
return res.json(results)
})
})
// 클라이언트에서 입력한 값을 DB lists 테이블에 넣어주기
app.post('/api/value',function(req,res,next){
// db에 값 넣어주기
// body parser 를 이용해서 client에서 온 body를 이용가능
db.pool.query(`INSERT INTO lists (value) VALUES("${req.body.value}");`,
(err,results,fields) =>{
if(err)
return res.status(500).send(err)
else
return res.json({success:true, value: req.body.value})
// client에 return
})
})
예시)
위의 노란색 상자 부분이
API - app.get() 부분에서, db에서 얻어온 데이터들을 front에서 노출합니다.
아래의 노랜색 상자 부분이
API - app.post() 부분에서, front에서 입력한 데이터를 db에 저장합니다.
React JS 구성하기
터미널에서 backend의 상위 폴더인 docker-fullstack-app으로 이동 후
npx create-react-app frontend
명령어를 입력하면 docker-fullstack-app의 하위에 frontend 폴더를 생성 후 기본 react-app을 생성해 줍니다.
터미널을 frontend로 와서 아래 명령어를 입력하면
npm run start
리액트 앱이 잘 실행되는 것을 확인할 수 있습니다.
그리고 UI를 변경하기 위해 App.js 파일을 변경해 보겠습니다.
// useState : useState 를 사용하기 위해 react 라이브러리에서 가져옴
// useEffect : useState를 사용하기 위해 react 라이브러리에서 가져옴
import React,{useState, useEffect} from 'react'
import axios from 'axios';
import logo from './logo.svg';
import './App.css';
function App() {
// db에 저장된 값을 가져와 화면에 보여주기 전 이 State에 넣어둠
const [lists,setLists] = useState([])
// input 박스에 입력한 값이 이 State에 들어감
const [value,setValue] = useState("")
useEffect(()=>{
// 여기서 db에 있는 값을 가져옴.
axios.get(`/api/values`)
.then(response => {
console.log('response',response.data)
setLists(response.data)
})
},[])
// input box에 입력하면 onChangeEvent가 발생할 때 마다 value 값 셋팅
const changeHandler = (event) =>{
setValue(event.currentTarget.value)
}
// input box에 입력한 값이 db에 저장되고 그 후에 화면에 표출도 시켜준다.
const submitHandler = (event) => {
event.preventDefault();
axios.post(`/api/value`,
{value:value})
.then(response=>{
if(response.data.success){
console.log('response.data',response.data)
setLists([...lists,response.data]);
setValue("");
}else{
alert("값을 db에 넣는데 실패했습니다.");
}
})
}
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<div className="container">
{lists && lists.map((list,index)=>(
<li key={index}>{list.value}</li>
))}
<br />
<form className="example" onSubmit={submitHandler}>
<input
type="text"
placeholder="입력해 주세요 ..."
onChange={changeHandler}
// input의 value를 State의 value 로 컨트롤
value={value}
/>
<button type="submit">확인</button>
</form>
</div>
</header>
</div>
);
}
export default App;
css 변경을 위해 App.css 파일을 변경하겠습니다.
.container{
width: 375px;
}
form.example input {
padding: 10px;
font-size: 17px;
border: 1px solid gray;
float: left;
width: 74%;
background: #f1f1f1;
}
form.example button{
float: left;
width: 20%;
padding: 10px;
background: #2196F3;
color: white;
font-size: 17px;
border: 1px solid grey;
border-left: none;
cursor: pointer;
}
form.example button:hover{
background-color: #0b7dda;
}
form.example::after{
content:"";
clear: both;
display: table;
}
React App을 위한 docker file 만들기
Dockerfile (운영 환경) 작성
FROM node:alpine as builder
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx
EXPOSE 3000
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/build /usr/share/nginx/html
Dockerfile.dev (개발 환경) 작성
## 베이스 이미지를 도커 허브에서 가져옴.
FROM node:alpine
## 해당 애플의 소스코드들이 들어감.
WORKDIR /app
## package.json 파일 WORKDIR ./에(/app) 복사
COPY package.json ./
## package.json 종속들 install
RUN npm install
## frontend 로컬파일 (./ 에 있는) WORKDIR ./(/app) 에 복사
COPY ./ ./
## 이 컨테이너가 실행될 때 같이 실행 할 명령어 지정
CMD ["npm","run","start"]
default.conf (frontend 폴더 하위, nginx 설정 파일)
## nginx 서버 설정
server{
## nginx 서버 번호
listen 3000;
## / 로 들어오면
location / {
## html 파일이 위치할 루트 설정
root /usr/share/nginx/html;
## 사이트의 index 페이지로 할 파일명 설정
index index.html index.htm;
## React Router를 사용해서 페이지간 이동을 할 때 이 부분이 필요
try_files $uri $uri/ /index.html;
}
}
React는 Single Page Application입니다.
그러기에 index.html 하나의 정적 파일만 가지고 있어서 만약 {URL}/home 이렇게 접속을 하려고 할 때도 index.html 파일에 접근을 해서 라우팅을 시켜야 하는데 nginx에서는 자동으로 이걸 알 수가 없습니다.
그러기에 /home 에 접속하려고 할 때 /home에 매칭 되는 것이 없을 때에 대안책으로 index.html을 제공하여서 /home 으로 라우팅을 시킬 수 있게 임의로 설정을 해주는 것입니다.
Node App을 위한 docker file 만들기
Dockerfile (운영 환경) 작성
FROM node:alpine
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
CMD ["npm","run","start"]
Dockerfile.dev (개발 환경) 작성
FROM node:alpine
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
CMD ["npm","run","dev"]
Mysql을 위한 docker file 만들기
Mysql을 위한 docker file을 만들기 전에
Mysql DB 구성을 개발 환경과 운영환경에서 각각 보시면
DB작업은 데이터들을 보관하고 이용하는 부분이기에 조금의 실수라도 안 좋은 결과들을 얻을 수 있습니다.
그러기에 실제 중요한 데이터들을 다루는 운영환경에서는 더욱 안정적인 AWS RDS를 이용하여 DB를 구성해보는 것이 실제로 실무에서 더 보편적으로 쓰이는 방법이기에 이렇게 개발환경과 운영환경을 두 방법으로 나누었습니다.
원래 DB를 사용하려면 먼저 DB설치 파일을 다운로드하고 설치하여 Node App에 연결해줘야 합니다.
하지만 도커를 이용하면 그럴 필요가 없고 ...
docker-fullstack-app root directory에 mysql이란 폴더를 만들고 그 안에 Dockerfile을 생성합니다.
- Dockerfile (개발 환경)
## 도커 허브에서 mysql 베이스 이미지를 가져옵니다.
FROM mysql:5.7
## my.cnf 설정을 덮어씌웁니다.
## ADD : COPY와 같은 기능을하지만
## 원격리소스를 다운로드하고 TAR파일도 추출 가능
ADD ./my.cnf /etc/mysql/conf.d/my.cnf
그리고 Mysql 을 시작할 때 DB와 table이 필요한데 그것들을 만들 장소를 만듭니다.
mysql 하위에 sqls폴더를 생성 후 initialize.sql 파일을 생성합니다.
- initialize.sql
DROP DATABASE IF EXISTS myapp;
CREATE DATABASE myapp;
USE myapp;
CREATE TABLE lists(
id INTEGER AUTO_INCREMENT,
value TEXT,
PRIMARY KEY (id)
);
그리고 backend/server.js 파일에서 테이블생성하는 부분을 주석처리합니다.
// 테이블 생성하기
/*
db.pool.query(`CREATE TABLE lists(
id INTEGER AUTO_INCREMENT,
value TEXT,
PRIMARY KEY (id)
)`,(err,results,fields)=>{
console.log('results', results)
})*/
마지막으로 한글도 저장할 수 있게 설정을 해줘야 합니다.
mysql폴더에 my.cnf 파일을 생성합니다.
- my.cnf
[mysqld]
character-set-server=utf8
[mysql]
default-character-set=utf8
[client]
default-character-set=utf8
Nginx를 위한 docker file 만들기
현재 만들고 있는 애플리케이션의 전체 설계를 다시 보자면
그리고 현재 Nginx가 쓰이는 곳은 두 군데이며 서로 다른 이유로 쓰이고 있습니다.
하나는 Proxy를 이유로 , 다른 하나는 Static 파일을 제공해주는 역할을 하고 있습니다.
Nginx가 요청을 나눠서 보내는 기준은
location이 / 로 시작하는지 (-> React.js 로), /api 로 시작하는지 (-> Node.js 로) 따라서 나눠줍니다.
이제 docker_fullstack_app 폴더 하위로 nginx 폴더를 만들어줍니다.
그 하위에 default.conf (nginx 설정 파일), Dockerfile, Dockerfile.dev 파일을 만들어줍니다.
default.conf
# 3000번 포트에서 frontend가 돌아간다는 것을 명시
upstream frontend{
server frontend:3000;
}
# 5000번 포트에서 backend가 돌아간다는 것을 명시
upstream backend{
server backend:5000;
}
server{
# Nginx 서버 포트를 80번으로 열어줍니다.
listen:80;
# location에서 우선순위가 있는데 /가 가장 낮습니다. /api 찾고 -> /
location / {
# 요청을 http://frontend 로 보내라
proxy_pass http://frontend
}
# /api 로 들어오는 요청
location /api {
# 요청을 http://backend 로 보내라
proxy_pass http://backend
}
# 일단 이부분이 없으면 에러 (개발 환경에서만 )
# 에러처리를 위한 부분
location /sockjs-node{
proxy_pass http://frontend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
Dockerfile , Dockerfile.dev
FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf
Docker Compose 파일 작성하기
각각의 컨테이너를 위한 도커파일들을 작성했습니다.
하지만 컨테이너들은 서로 나눠져 있어 아무런 조치 없이는 서로 통신할 수 없습니다.
그래서 컨테이너들을 서비스안에서 서로 연결시켜 주기 위해 Docker Compose를 작성하겠습니다.
docker-compose.yml
version: '3'
services:
frontend:
build:
dockerfile: Dockerfile.dev
context: ./frontend
volumes:
- /app/node_modules
- ./frontend:/app
stdin_open: true # 리액트 앱을 종료할 때 나오는 버그를 잡아줌
nginx:
restart: always
build:
dockerfile: Dockerfile
context: ./nginx
ports:
- "3000:80"
backend:
build:
dockerfile: Dockerfile.dev
context: ./backend
container_name: app_backend
volumes:
- /app/node_modules
- ./backend:/app
mysql:
build: ./mysql
restart: unless-stopped #개발자가 임의로 멈출 때 빼고
container_name: app_mysql
ports:
- "3306:3306"
volumes:
- ./mysql/mysql_data:/var/lib/mysql
- ./mysql/sqls/:/docker-entrypoint-initdb.d/
environment:
MYSQL_ROOT_PASSWORD : password
MYSQL_DATABASE: myapp
※restart
모든 준비가 끝났다면
docker-compose up을 해보겠습니다.
실행이 잘 되는것을 볼 수 있습니다.
오류
기존 mysql 폴더안엔 Dockerfile, my.cnf, sqls/initalize.sql 파일이 있습니다.
그리고 docker-compose up으로 mysql 이미지를 만들고 나면 -> mysql_data라는 폴더가 생기는 것을 볼 수 있습니다.
실행중에 node server에서 -> mysql lists 테이블에 접근을 하는 부분에
500 err (Host ip is not allowed to connect to this Mysql)가 발생을 했었습니다.
한참 삽질을 하던중에 docker exec -it 컨테이너이름 bash 로 실행중인 mysql 컨테이너로 들어가서
mysql -u root -p mysql 하고 비밀번호를 입력하면
mysql에 접속을 할 수 있습니다.
오류가 생길 당시엔 databases에서 myapp이 없었습니다.
즉, 이부분으로 myapp이라는 database를 생성하는 과정에서 오류가 있었음을 알게됐습니다.
이런 경우엔 VS Code로 돌아와 mysql_data(mysql 이미지 생성 시 생기는 폴더) 라는 폴더를 삭제 후
sqls/initalize.sql , Dockerfile, my.cnf 파일등에 오타나 오류가 있었는지 확인을 한 후
다시 docker-compose up을 해주어 mysql_data 폴더를 다시 생성시켜야
위의 그림처럼 myapp database가 잘 생성이 되고
그 안에 lists table도 잘 있는지 확인할 수 있습니다.
Volume을 이용한 데이터 베이스 데이터 유지하기
docker-compose.yml 파일에서 mysql 부분을 보시면
volumes 로 설정한 부분이 있습니다.
현재까지 volume을 사용한 용도는 리액트나 노드 쪽에서 코드를 업데이트할 때 바로 그 코드가 애플리케이션에 적용이 될 수 있게 해주기 위해서 사용했었습니다.
DB에 저장된 자료를 컨테이너를 지우더라도 자료가 지워지지 않을 수 있게 해주기 위한 volume입니다.
원래는 컨테이너를 지우면 컨테이너에 저장된 데이터들도 지워지게 됩니다.
이렇게 컨테이너를 삭제할 때 컨테이너안에 저장된 데이터까지 삭제가 된다면 영속성이 필요한 데이터들은 어떻게 처리를 할까요 ?
그게 바로 volume을 설정해주는 것 입니다.
Volume 은 무엇일까?
볼륨은 도커 컨테이너에 의해 성성되고 사용되는 지속적인 데이터를 위한 선호 매커니즘 입니다.
Volume을 이용한 데이터 베이스 영속성 구조
위 사진 처럼 데이터가 컨테이너에 저장되는 것이 아닌 호스트 파일 시스템에 저장되고
그중에서도 도커에 의해서만 통제가 되는 도커Area에 저장이 되므로
컨테이너를 삭제해도 변화된 데이터가 삭제가 되지 않습니다.
'Docker' 카테고리의 다른 글
[Docker] 복잡한 어플을 실제로 배포해보기 (테스트 & 배포) (0) | 2022.04.08 |
---|---|
[Docker] 간단한 어플을 실제로 배포해보기(테스트 & 배포) (0) | 2022.03.30 |
[Docker] 간단한 어플을 실제로 배포해보기(개발 환경 부분) (0) | 2022.03.29 |
[Docker] Docker Compose (0) | 2022.03.26 |
[Docker] 도커를 이용한 간단한 Node.js 어플 만들기 (0) | 2022.03.25 |