2020-10-29

Posted by karais89 on October 29, 2020

인프런에서 결제한 도커 강의 학습 및 내용 정리 진행. 이전에 도커에 대해 몇개의 강의 및 책을 학습한 적은 있어 대략적인 방식만 알고 있는 상태에서, 실제 개발에 적용 가능한지 확인을 위해 학습 중.

섹션 8. 복잡한 어플을 실제로 배포해보기(개발 환경 부분)

섹션 설명

실무에선 벡엔드도 이용하고 데이터베이스등 많은 것들을 이용한다.

좀 더 복잡한 구조와 컨테이너를 이용해 실무에 맞는 어플리케이션을 만들 예정.

개발환경에서 리액트 & 노드 & 데이터베이스 앱 개발

멀티 컨테이너 애플리케이션

  • NGINX
  • React
  • Node
  • MySQL

기능은 심플하나 설계는 복잡

브라우저에 글을 입력시 리액트를 통해 노드로 전달되고 mysql 데이터베이스에 입력값 저장후 출력하는 간단한 앱. 컨테이너를 재시작해도 DB에 저장된것은 남아 있는 앱

2가지 설계가 있음

  1. Nginx의 Proxy을 이용한 설계 (url 주소를 이용해 프론트, 백엔드 구분)
  2. Nginx는 정적파일을 제공만 해주는 설계 (클라이언트가 포트를 보고 프론트, 백엔드 구분)

1번이 더 복잡하므로 1번으로 구현을 할 예정

흐름

  1. 전체 소스 코드 작성
  2. Dockerfile 작성
  3. Docker-compose 작성
  4. 깃헙에 push
  5. Travis CI
  6. Docker Hub
  7. AWS ElasticBeanStalk

node.js 구성하기

  1. backend 폴더 생성
  2. package.json 만들기 (npm init)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
     {
       "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"
       },
       "author": "",
       "license": "ISC",
       "dependencies": {
         "express": "^4.17.1",
         "mysql": "^2.18.1",
         "nodemon": "^2.0.4",
         "body-parser": "^1.19.0",
         "axios": "^0.20.0"
       }
     }
    
    • scripts에 추가
    • dependencies 추가
  3. server.js 기본 구조 작성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
     // 필요한 모듈 가져오기
     const express = require("express");
     const bodyParser = require("body-parser");
    
     // express 서버 생성
     const app = express();
    
     // json 형태로 오는 요청의 본문을 해석해줄수있게 등록
     app.use(bodyParser.json());
    
     app.listen(5000, () => {
         console.log("애플리케이션이 5000번 포트에서 시작되었습니다.")
     })
    
  4. mysql을 node.js에 연결

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     const mysql = require("mysql");
     const pool = mysql.createPool({
         connectionLimit: 10,
         host: 'mysql',
         user: 'root',
         password: '1234',
         database: 'myapp'
     });
     exports.pool = pool;
    
  5. server.js pool 사용

    1
    
     const db = require('./db');
    
  6. api 작성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     // DB list 테이블에 있는 모든 데이터를 프론트 서버에 보내주기
     app.get('/api/values', (res, req) => {
         // DB에서 모든 정보 가져오기
         db.pool.query('SELECT * FROM lists;',
         (err, results, fileds) => {
             if (err) return res.status(500).send(err)
             else res.json(results)
         });
     });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     // 클라이언트에서 입력한 값을 데이터베이스 list 테이블에 넣어주기
     app.post('/api/value', (res, req, next) => {
         // DB에 값 넣어주기
         db.pool.query(`INSERT INTO lists (value) VALUES("${req.body.value}")`,
         (err, results, fileds) => {
             if (err) return res.status(500).send(err)
             else res.json({ success: true, value: req.body.value })
         });
     });
    
    1
    2
    3
    4
    5
    6
    7
    8
    
     // 테이블 생성
     db.pool.query(`CREATE TABLE lists(
         id INTEGER AUTO INCREMENT
         value TEXT,
         PRIMARY KEY (id)
     )`, (err, results, fileds) => {
         console.log('results:', results)
     });
    

React JS 구성하기

  1. create-react-app으로 앱 생성 (npx create-react-app frontend)
  2. app.js로 들어가 input 박스와 button 추가
  3. npm run start로 리액트 앱 실행
  4. app.js 코드 추가

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
     import logo from './logo.svg';
     import './App.css';
    
     function App() {
       return (
         <div className="App">
           <header className="App-header">
             <img src={logo} className="App-logo" alt="logo" />
           <div className="container">
             <form className="example" onSubmit>
               <input
                 type="text"
                 placeholder="입력해주세요..."
               />
               <button type="submit">확인</button>
             </form>
           </div>
           </header>
         </div>
       );
     }
    
     export default App;
    
  5. css 수정
  6. state 생성

최종 파일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import Recat, {useState, useEffect} from 'react';
import logo from './logo.svg';
import './App.css';
import axios from 'axios';

function App() {

  useEffect(() => {
    // 여기서 데이터 베이스에 있는 값을 가져온다.
    axios.get('/api/values')
    .then(response => {
      console.log('response', response);
      setLists(response.data)
    });
  }, [])

  const [lists, setLists] = useState([])
  const [value, setValue] = useState("")

  const changeHandler = (event) => {
    setValue(event.currentTarget.value)
  }
  
  const submitHandler = (event) => {
    event.preventDefault();

    axios.post(`/api/value`, {value: value})
    .then(response => {
      if (response.data.success) {
        console.log('response', response);
        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>
        })}

        <form className="example" onSubmit={submitHandler}>
          <input
            type="text"
            placeholder="입력해주세요..."
            onChange={changeHandler}
            value={value}
          />
          <button type="submit">확인</button>
        </form>
      </div>
      </header>
    </div>
  );
}

export default App;
1
2
# axios 에러 발생시 설치 시도.
npm install axios

리액트 앱을 위한 도커 파일 만들기

  1. frontend 폴더에 Dockerfile 생성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     FROM node:alpine
    
     WORKDIR /app
    
     COPY package.json ./
    
     RUN npm install
    
     COPY ./ ./
    
     CMD ["npm", "run", "start"]
    
    • 개발환경
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     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
    
    • 운영환경
    • default.conf 파일을 컨테이너 안에 있는 nginx 설정 파일 경로
  2. default.conf 파일 작성

1
2
3
4
5
6
7
8
9
10
11
server {
    listen 3000;

    location / {
        root /usr/share/nginx/html;

        index index.html index.htm;

        try_files $uri $uri/ /index.html;
    }
}
  • try_fiels 는 리액트를 위해 필요한 부분

노드 앱을 위한 도커 파일 만들기

  1. Dockerfile 생성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     FROM node:alpine
    
     WORKDIR /app
    
     COPY ./package.json ./
    
     RUN npm install
    
     COPY . .
    
     CMD ["npm", "run", "dev"]
    
    • 개발환경
    • npm run dev → nodemon 사용을 위해
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     FROM node:alpine
    
     WORKDIR /app
    
     COPY ./package.json ./
    
     RUN npm install
    
     COPY . .
    
     CMD ["npm", "run", "start"]
    
    • 운영환경

DB에 관해서

  • mysql 사용
  • 개발 환경 → 도커 환경 이용
  • 운영 환경 → AWS RDS 서비스 이용

나누는 이유?

DB 작업은 중요한 데이터들을 보관하고 이용하는 부분이기에 조금의 실수도 용납되지 않음.

실무에서는 AWS RDS를 이용하여 사용하는게 더 안정적이고 더 보편적이다.

MYSQL을 위한 도커 파일 만들기

  1. mysql 폴더 생성 후 Dockerfile 생성

    1
    
     FROM mysql:5.7
    
  2. 테이블을 만들 공간도 sqls 폴더를 생성. initialize.sql 파일도 만듬

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     DROP DATABASE IF EXISTS myapp;
    
     CREATE DATABASE myapp;
     USE myapp;
    
     CREATE TABLE lists (
         id INTEGER AUTO_INCREMENT,
         value TEXT,
         PRIMARY KEY (id)
     )
    
  3. 한글이 깨지는 부분이 있음. my.cnf 파일 생성후 설정

    1
    2
    3
    4
    5
    6
    7
    8
    
     [mysqld]
     character-set-server=utf8
    
     [mysql]
     default-character-set=utf8
    
     [client]
     default-character-set=utf8
    
    • 위 파일을 덮어 씌워줘야 한다.

최종 도커 파일

1
2
3
FROM mysql:5.7

ADD ./my.cnf /etc/mysql/conf.d/my.cnf

NGINX를 위한 도커 파일 만들기

클라이언트에서 request 보낼때 nginx를 이용해 프론트와 서버를 나누어서 보낼 수 있다.

api로 시작하면 서버 아니면 클라이언트로 보내는 형태.

  1. 정적파일 제공 (react)
  2. api 요청 (nodejs)

흐름

  1. nginx 폴더 생성 후 default.conf 파일 및 Dockerfile 생성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
     upstream frontend {
         server frontend:3000;
     }
    
     upstream backend {
         server backend:5000;
     }
    
     server {
         listen 80;
    
         location / {
             proxy_pass http://frontend;
         }
    
         location /api {
             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";
         }
     }
    
    1
    2
    
     FROM nginx
     COPY ./default.conf /etc/nginx/conf.d/default.conf
    

Docker Compose 파일 작성하기

각각의 도커 파일을 만듬.

각각의 도커 파일로 이미지 생성 하고, 만들 예정

컨테이너끼리 통신을 위해서는 docker-compose가 필요하다.

  1. docker-compose.yml 파일 생성
  2. 뼈대 만들기
  3. 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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: 1234
            MYSQL_DATABASE: myapp

Docker Volume을 이용한 데이터 베이스 데이터 유지하기

데비터베이스에 저장된 자료를 컨테이너를 지우더라도 자료가 지워지지 않도록 하기 위해 생성함.