daybreak6 / docker

도커와CI환경 스터디 공간
0 stars 0 forks source link

스터디 8주차 인증하기 #8

Open LangVE opened 2 years ago

LangVE commented 2 years ago

목표

LangVE commented 2 years ago

8장 읽었습니다.

kaiwind7 commented 2 years ago

08. 다중 컨테이너를 활용한 애플리케이션의 개발 환경 구축

8.1 다중 컨테이너를 활용해 만들 애플리케이션의 구조

멀티 컨테이너 애플리케이션을 위한 전체적인 설계

  1. 클라이언트에서 엔진엑스로 오는 요청을 백엔드 서버와 프런트엔드 서버로 나누는 구조
  2. 엔진엑스는 프런트엔드 서버로만 사용해 클라이언트에서 정적 파일을 요구할 때 이를 제공하는 형식으로만 사용하는 구조

엔진엑스의 프록시(Proxy) 기능을 이용한 설계

첫번째 설계 방식은 엔진엑스를 두 가지 역할로 이용 엔진엑스에 있는 프록시 기능과 정적 파일을 제공하는 기능 클라이언트에서 요청을 보낼 때 요청이 프런트엔드 서버를 위한 것이면 프런트엔드 서버 쪽으로 보내고 백엔드 서버를 위한 요청이라면 백엔드 서버로 보낸다. 정적 파일을 제공하는 기능은 프런트엔드 서버에 많은 정적 파일이 있는데, 클라이언트에서 이러한 파일을 요청하면 전달.

장점

요청을 보낼 때 호스트의 이름이 바뀌더라도 경로 부분을 변경하지 않아도 되는 점 포트 자체를 넣지 않아도 되므로 요청을 보내는 경로의 포트가 바뀌어도 포트를 바꿔주지 않아도 된다는 점

단점

엔진엑스의 설정하는 부분이 다소 복잡하며 전체적인 설계도 다소 복잡

엔진엑스는 정적 파일만 제공하는 설계

장점

설계가 다소 간단해서 구현하기가 더 쉽다

단점

호스트 이름이나 포트 변경이 있을 떄 모든 요청의 경로를 변경해야 한다는 점

애플리케이션의 구현 순서

  1. 애플리케이션의 소스 코드 작성
  2. 도커 파일을 작성
  3. 도커 컴포즈 파일을 작성
  4. 깃허브 원격 저장소에 소스 코드를 푸시
  5. Travis CI에서 테스트 및 빌드를 진행
  6. 도커 허브에서 빌드된 도커 이미지를 보관
  7. AWS 일래스틱 빈스톡을 이용해 애플리케이션을 배포

8.2 Node,js로 애플리케이션의 백엔드 서버 구현하기

  1. 폴더생성 /docker-multi-app

    backend

  2. backend 폴더 아래에 package.json 파일을 생성 npm init 명령어를 이용 or 직접 package.json 파일을 만든 다음 필요한 소스 코드를 작성

  3. package.json 파일에 스크립트와 사용할 모듈 명시

    {
       "name": "backend",
       "version": "1.0.0",
       "description": "",
       "main": "server.js",
       "scripts": {
           "test": "",
           "start": "nodeserver.js",
           "dev": "nodemonserver.js"
       },
       "dependencies": {
           "express": "4.16.3",
           "mysql": "2.16.0",
           "nodemon": "1.18.3",
           "body-parser": "1.19.0"
       },
       "author": "",
       "license": "ISC"
    }
  4. 시작점이 되는 server.js 파일 생성

  5. server.js

    // 핑요한 모듈 가져오기
    const express = require("express");
    
    // 익스프레스 서버 생성
    const app = express();
    
    // JSON 형태로 전달되는 요청의 본문을 해석할 수 있게 등록
    app.use(express.json());
    
    app.listen(5000, () => {
       console.log('애플리케이션이 5000번 포트에서 시작됐습니다.');
    });
  6. Node,js 애플리케이션과 Node,js 애플리케이션을 연결하기 위한 db.js 파일을 생성

  7. 생성한 db.js 파일에 호스트, 유저 이름, 비밀번호, 데이터베이스 이름을 명시해 pool을 생성. 그리고 생성한 pool을 다른 파일에서도 사용할 수 있게 익스포트.

    const mysql = require("mysql");
    const pool = mysql.createPool({
       connectionLimit: 10,
       host: 'mysql',
       user: 'root',
       password: 'password',
       database: 'myapp',
       port: 3306
    });
    exports.pool = pool;
  8. 익스포트한 pool을 server.js 에서 불러온다. db.js 파일 안에 pool이 들어 있기 때문에 db.js 파일을 가져오면 pool을 가져오게 된다.

    ...
    const db = require(./db)
    ...
  9. 애플리케이션에서 필요한 두 가지 API 구현

    ...
    
    // DB의 lists 테이블에 있는 모든 데이터를 프런트엔드 서버로 보내주기
    app.get('/api/values', function (req, res) {
       // DB에서 모든 정보 가져오기
       db.pool.query('SELECT * FROM lists;', 
                    (err, results, fileds) => {
           if(err)
               return res.status(500).send(err);
           else
               // 프런트엔드에 DB에서 가져온 정보를 json 형식으로 보냅니다.
               return res,json(results);
       })
    })
    
    // 클라이언트에서 입력한 값을 DB의 lists 테이블에 넣어주기
    app.post('/api/value', function (req, res, next) {
       // 데이터베이스에 값 넣어주기
       db.pool.query('INSERT INTO lists (value) VALUES ("${req,body.value}")', (err, results, fileds) => {
           if(err)
               return res.status(500).send(err);
           else 
               return res.json.({success: true, value: req.body.value});
       })
    })

8.3 React,js 로 애플리케이션의 프런트엔드 구현하기

  1. create-react-app으로 리액트 애플리케이션 생성

    npx create-react-app frontend
  2. 명령어 실행하면 리액트 애플리케이션 생성

  3. App.js 파일에 UI를 위한 소스 코드를 작성

    import React from 'react';
    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">
                           <input type="text" placeholder="입력해주세요..." />
                           <button type="submit">확인</button>
                    </form>
                   </div>
            </header>
        </div>
       };
    }
    
    export default App;
  4. UI에 CSS를 적용하기 위해 App.css 파일에 다음과 같이 CSS 코드를 추가

    ...
    
    @keyframes App-logo-spin {
       from {
           transform: rotate(0deg);
       }
       tp {
           transform: rotate(360deg);
       }
    }
    
    .container {
       width: 375px;
    }
    
    form.example input {
       padding: 10px;
       font-size: 17px;
       border: 1px solid grey;
       float: left;
       width: 74%;
       background: #f1f1f1;
    }
    
    form.example button {
       float: left;
       widh: 20%;
       padding: 10px;
       background: #2196F3;
       color: white;
       font-size: 17px;
       border: 1px solid grey;
       border-left: none;
       cursor: pointer;
    }
    
    form.example button:hover {
       background: #ob7dda;
    }
    
    form.example::after {
       content: "";
       clear: both;
       display: table;
    }
  5. 데이터의 흐름을 위한 스테이트를 생성

    import React, {useState} from 'react';
    import logo from './logo.svg';
    import './App.css';
    
    // 백엔드 서버와 비동기 통신을 하기 위해 axios 라이브러리를 가져온다
    import axios from 'axios';
    
    function App() {
    
       //DB에 저장된 값을 가져와서 화면에 보여주기 전에 이 lists state에 넣어줌
       const [lists, setLists] = useState([]);
    
       // Input 박스에 입력한 값이 이 value State에 들어감
       const [value, setValue] = useState("");
    
       ... 생략 ...
    

    리액트에서는 State를 이용해 데이터의 흐름을 처리. 그리고 State를 이용하려면 react 라이브러리에서 useState 훅을 불러와야 한다. 이 useState 훅을 이용해 lists와 value State를 생성. 그리고 나서 lists State를 배열 형식으로 입력하면 입력한 글들이 lists에 하나씩 들어감. 그리고 value State는 문자열 형식으로 현재 입력하는 문자열이 들어감.

  6. App.js에서는 axios라는 브라우저와 Node.js를 위한 비동기 통신 라이브러리를 가져옴 그러기 위해서는 package.json 파일에 axios 라이브러리를 명시

    ...
    "dependencies": {
       "react": "^16.13.1",
       "react-dom": "^16.13.1",
       "react-scripts": "3.4.1",
       "axios": "0.19.2"
    }
    ...
  7. 데이터베이스에서 데이터를 가져오는 데 필요한 useEffect추가

    // useEffect를 사용하기 위해 react 라이브러리에서 가져온다.
    import React, {useState, useEffect} from 'react';
    import logo from './logo,svg';
    import './App.css';
    import axios from 'axios';
    
    function App() {
       //DB에 저장된 값을 가져와서 화면에 보여주기 전에 이 lists State에 넣어줍니다.
       const [lists, setLists]  = useState([]);
    
       //Input 박스에 입력한 값이 이 value State에 들어갑니다.
       const[value, setValue] = useState("");
    
       useEffect(() => {
           // 여기에 데이터베이스에 있는 값을 가져옵니다.
       }, [])
    }
  8. 애플리케이션의 기능 구현

    import React, {useState} from 'react';
    import logo from './logo.svg';
    import './App.css';
    
    // 백엔드 서버와 비동기 통신을 하기 위해 axios 라이브러리를 가져온다
    import axios from 'axios';
    
    function App() {
    
       //DB에 저장된 값을 가져와서 화면에 보여주기 전에 이 lists state에 넣어줌
       const [lists, setLists] = useState([]);
    
       // Input 박스에 입력한 값이 이 value State에 들어감
       const [value, setValue] = useState("");
    
       useEffect(() => {
           // 여기에 데이터베이스에 있는 값을 가져옵니다.
       axios.get('/api/values')
        .then(response => {
            console.log('response', response);
            setLists(response.data);
        })
       }, []);
    
       // Input 박스에 값을 입력(onChange 이벤트가 발생) 할 대마다 
       // Value State를 변경합니다.
       const chageHandler = (event) => {
           setValue(event.currentTarget.value);
       };
    
       // Input 박스에 값을 입력하고 확인 버튼을 누르면
       // 입력한 값이 데이터베이스에 저장되고
       // 화면에 값을 보여줍니다.
       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>
                       ))}
                       <br/>
                       안녕하세요
    
                       // 확인 버튼을 누르면 onSubmit 이벤트가 발생하고
                       // submitHandler 함수를 호출합니다.
                       <form className="example" onSubmit={submitHandler}>
                           <input type="text" placeholder="입력해주세요..." 
                           // 값을 입력할 때마다 onChange 이벤트가 발생하고
                           // changeHandler 함수를 호출합니다.
                           onChange={changeHandler}
                           // Input 박수의 value를 state의 value로 컨트롤합니다.
                           value={value}
                           />
                           <button type="submit">확인</button>
                    </form>
                   </div>
            </header>
        </div>
       };
    
       export default App;
    
    }
    

8.4 리액트 애플리케이션을 위한 도커 파일 만들기

  1. frontend 폴더에 두 개의 도커 파일을 생성. Dockerfile.dev dockerfile

  2. 개발 환경을 위한 도커 파일을 작성

    # 베이스 이미지를 도커 허브에서 가져옵니다
    FROM node:alpine
    
    # 해당 어플리케이션의 소스 코드가 이 디렉터리로 들어갑니다
    WORKDIR /app
    
    # 소스 코드가 바뀔 때마다 종속성까지 다시 복사하지 않도록
    # 먼저 종속성 목록을 담고 있는 package.json을 복사합니다.
    COPY package.json ./
    
    # package.json에 명시된 종속성을 설치합니다.
    RUN npm install
    
    # 로컬에 있는 모든 소스 코드를 WORKDIR로 복사합니다.
    COPY ./ ./
    
    # 컨테이너가 시작되면 실행할 명령어를 명시합니다.
    CMD ["npm", "run", "start"]
  3. 운영 환경을 위한 도커 파일을 작성.

    FROM node:alpine as builder
    WORKDIR /app
    COPY package.json ./
    RUN npm install
    COPY ./ ./
    RUN npm run build
    
    # 엔진엑스를 기동하고 앞서 생성한 빌드 파일을 제공합니다.
    # default.conf에 해준 설정을 엔진엑스 컨테이너 안으로 복사합니다
    FROM nginx
    EXPOSE 3000
    COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
    COPY --from-builder /app/build /usr/share/nginx/html
  4. 엔진엑스 설정 파일을 작성

엔진엑스 설정 변경하기

  1. 엔진엑스 설정을 작성할 파일을 생성 frontend/nginx/default.conf

  2. default.conf 설정작성

    server {
       listen 3000;
    
       location / {
           # HTML 파일이 위치할 루트 경로를 성정
           root /usr/share/nginx/html;
           # index 페이지의 파일명을 설정
           index index.html index.htm;
           # 리액트 라우터를 사용해 페이지를 이동할 때
           # 이 부분이 필요합니다.
           try_files $uri $uri/ /index,html;
       }
    }

8.5 노드 애플리케이션을 위한 도커 파일 만들기

  1. backend 폴더에 두개의 도커 파일 생성. Dockerfile.dev Dockerfile
  2. 개발 환경을 위한 도커 파일 작성
  3. 운영 환경을 위한 도커 파일 작성

8.6 개발 환경과 운영 환경의 데이터 베이스 구성

개발 환경과 운영 환경의 데이터베이스를 나누는 이유는?

데이터베이스의 구조 자세히 살펴보기

8.7 MySQL을 위한 도커 파일 만들기

MySQL을 위한 도커 파일 작성

  1. mysql 폴더를 생성하고, 도커 파일 생성

  2. 도커 파일에 소스 코드를 작성

    # mysql 베이스 이미지를 도커 허브에서 가져옵니다.
    FROM mysql:5,7
  3. MySQL을 시작할 때 데이터베이스와 테이블이 필요하므로 이를 작성할 파일 생성 ../sqls/initialize.sql 파일 생성

  4. initialize.sql 파일에 데이터베이스와 테이블을 생성하는 코드 작성

    DROP DATABASE IF EXISTS myapp;
    
    CREATE DATABASE myapp;
    USE myapp;
    
    CREATE TABLE lists (
    id INTEGER AUTO INCREMENT,
       value TEXT,
       PRIMARY KEY (id)
    );
  5. 한글로 저장할 수 있게 설정 mysql 폴더 아래에 my.cnf 파일 생성 엔진엑스 설정은 default.conf 파일에 작성

  6. 한글이 깨지는 현상을 막기 위해 my.cnf 파일 인코딩 설정

    [mysqld]
    character-set-server=utf8
    
    [mysql]
    default-character-set=utf8
    
    [client]
    default-character-set=utf8
  7. 실제 컨테이너 안에서 MySQL 설정을 하는 my.cnf 파일을 앞서 작성한 파일로 덮어씀

    FROM mysql:5.7
    
    ADD ./my.cnf /etc/mysql/conf.d/my.cnf
  8. 설정이 잘 됐는지는 8.9절에서 도커 컴포즈를 작성한 후에 도커 컴포즈로 MySQL 컨테이너를 실행해 확인

8.8 엔진엑스를 위한 설정 파일과 도커 파일 만들기

프록시 기능을 위한 엔진엑스 설정

  1. nginx 폴더 생성하고, nginx 폴더에 설정 파일과 도커 파일을 생성. 설정파일 default.conf 도커 파일의 파일명 Dockerfile 지정

  2. default.conf 파일에 프록시 기능을 구현하기 위한 코드 작성

    # 3000번 포트에서 프런트엔드가 작동하고 있다는 것을 명시
    upstream frontend {
       server frontend: 3000;
    }
    
    # 5000번 포트에서 백엔드가 작동하고 있다는 것을 명시
    upstream backend {
       server backend: 5000;
    }
    
    server {
       listen 80;
    
       #'/' 경로로 시작하는 요청은 http://frontend로 보냅니다.
       location / {
           proxy_pass http://frontend;
       }
    
       # '/api' 경로로 시작하는 요청은 http://backend로 보냅니다.
       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";
       }
    }
  3. 엔진엑스를 위한 도커 파일을 작성

    # 엔진엑스 베이스 이미지를 가져옵니다
    FROM nginx
    
    # default.conf에 작성된 것을 컨테이너에서 실행될 nginx에도 
    # 적용될 수 있게 COPY 명령어로 복사
    COPY ./default.conf /etc/nginx/conf.d/default.conf

8.9 개발환경을 위한 도커 컴포즈 파일 작성하기

  1. 최상위 디렉토리에 도커 컴포즈 파일인 docker-compose-dev.yml 생성

  2. 서비스를 위한 기본 구조 작성

  3. frontend 서비스를 위한 설정 작성

  4. nginx 서비스를 위한 설정을 작성

  5. backend 서비스를 위한 설정을 작성

  6. MySQL 서비스를 위한 설정을 작성

  7. 모든 설정이 끝났다면 도커 컴포즈로 애플리케이션을 시작

  8. 8.7절 인코딩 설정 확인

    version: "3"
    service:
     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

8.10 볼륨을 이용한 데이터베이스의 데이터 유지하기

도커 볼륨을 이용한 데이터 영속성 구조