Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
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
Tags
more
Archives
Today
Total
관리 메뉴

Kwon's Study Blog !

[Docker] 복잡한 어플을 실제로 배포해보기 (개발 환경 부분) 본문

Docker

[Docker] 복잡한 어플을 실제로 배포해보기 (개발 환경 부분)

순샤인 2022. 4. 4. 17:32
현재 글은 
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
을 수강하며 정리한 내용입니다. 
문제시 비공개로 처리 하도록 하겠습니다.
 

따라하며 배우는 도커와 CI환경 - 인프런 | 강의

이 강의를 통해 도커에 대해서 배울 수 있으며, CI 환경을 구성할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

저번 글에선 도커를 이용해서 리액트앱을 만들었습니다.

 

하지만 실제로 애플리케이션을 만들 때는 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에 저장이 되므로

컨테이너를 삭제해도 변화된 데이터가 삭제가 되지 않습니다.