어플 배포(개발 환경 부분)

Min·2021년 1월 18일
3

Docker

목록 보기
4/5
post-thumbnail

장점
1. Request를 보낼때 URL 부분을 host 이름이 바뀌어도
변경시켜주지 않아도 된다.
2. 포트가 바뀌어도 변경을 안해주어도 된다.
axios.get{'/api/values}


1. 전체 소스코드

1) 프론트엔드

$ npx create-react-app frontend

// package.json

"dependencies": {
    "axios": "0.19.2"
}
// src/App.js

import React, { useState, useEffect } from "react";
import logo from "./logo.svg";
import "./App.css";
import axios from "axios";

function App() {
  useEffect(() => {
    axios.get("/api/hi").then((response) => {
      console.log("response", response);
    });
  }, []);

  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>)}
          <br />
          안녕하세요.
          <form className="example" onSubmit={submitHandler}>
            <input
              type="text"
              placeholder="입력해주세요..."
              onChange={changeHandler}
              value={value}
            />
            <button type="submit">확인.</button>
          </form>
        </div>
      </header>
    </div>
  );
}

export default App;

2) 백엔드

$ npm init -v

// package.json
{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "express": "4.16.3",
    "mysql": "2.16.0",
    "nodemon": "1.18.3",
    "body-parser": "1.19.0"
  }
}
// backend/server.js

//필요한 모듈들을 가져오기
const express = require("express");
const bodyParser = require("body-parser");

const db = require("./db");

//Express 서버를 생성
const app = express();

// json 형대토 오는 요청의 본문을 해석해줄수있게 등록
app.use(bodyParser.json());

//DB lists 테이블에 있는 모든 데이터를 프론트 서베에 보내주기
app.get("/api/hi", function (req, res) {
  //데이테베이스에서 모든 정보 가져오기
  res.status(200).send("good");
});

//DB lists 테이블에 있는 모든 데이터를 프론트 서베에 보내주기
app.get("/api/values", function (req, res) {
  //데이테베이스에서 모든 정보 가져오기
  db.pool.query("SELECT * FROM lists;", (err, results, fileds) => {
    if (err) return res.status(500).send(err);
    else return res.json(results);
  });
});

// 클라이언트에서 입력한 값을 데이터베이스 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 });
    }
  );
});

app.listen(5000, () => {
  console.log("애플리케이션이 5000번 포트에서 시작되었습니다.");
});
// .db

const mysql = require("mysql");
const pool = mysql.createPool({
  connectionLimit: 10,
  host: "mysql",
  user: "root",
  password: "johnahn",
  database: "myapp",
});
exports.pool = pool;

2. Dockerfile

1) 개발환경 : Dockerfile.dev


프론트엔드

// Dockerfile.dev

FROM node:alpine

WORKDIR /app

COPY package.json ./

RUN npm install

COPY ./ ./

CMD [ "npm", "run", "start" ]

백엔드


FROM node:alpine

WORKDIR /app

COPY ./package.json ./

RUN npm install

COPY . .

CMD ["npm", "run", "dev"]

2) 운영환경 : Dockerfile


컨테이너 안에있는 nginx파일경로 - deafult.conf 주소

  • 위에 있는 생성한 경로파일을 오른쪽에 넣어준다.

프론트엔드

// 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

FROM node:alpine

WORKDIR /app

COPY ./package.json ./

RUN npm install

COPY . .

CMD ["npm", "run", "start"]

3) nginx(serving)

// frontend/nginx/deafult.conf

server {
    listen 3000;

    location / {
        # HTML 파일이 위치할 루트 설정
        root /usr/share/nginx/html;
        
        # 사이트의 index 페이지로 할 파일명 설정 
        index index.html index.htm;

        # React Router를 사용해서 페이지간 이동을 할때 필요
        try_files $uri  $uri/ /index.html;

    }
}
# 리액트는 SPA 이므로 index.html 하나의 정적파일만 가지고 있다.
# 만약 {URL}/home 이렇게 접속하려고 할 때도
# index.html 파일에 접근을해서 라우팅을 시켜야 하는데
# nginx에서는 자동으로 이걸 알수가 없다.
# 그래서 /home에 접속하려고 할 때 /home에 매칭되는 것이 없을 때 대안으로
# index.html을 제공하여서 /home으로 라우팅될 수 있게 임시 설정

4) mysql


원래 DB를 사용하려면 먼저 DB 설치파일을 다운받고
그걸 이용해서 DB를 설치한 후 노드앱에 연결해야 되는데
설치는 안하고 노드앱만 연결시킨 상태

하단그림의 Mysql이 없는 상태
도커 이미지를 이용해 설치해준다

// Dockerfile

FROM mysql:5.7

# my.cnf 설정을 실제 mysql 설정을 해주는 my.cnf파일로 덮어준다.
ADD ./my.cnf /etc/mysql/conf.d/my.cnf
// my.cnf

// DB에 넣어 줄때 한글이 깨지는 오류발생 때문에
[mysqld]
character-set-server=utf8

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8
// sqls/initialze.sql 
// DB와 Table 만들 장소

DROP DATABASE IF EXISTS myapp;

CREATE DATABASE myapp;
USE myapp;

CREATE TABLE lists (
    id INTEGER AUTO_INCREMENT,
    value TEXT,
    PRIMARY KEY (id)
);

5) Nginx(proxy)

현재 Nginx가 쓰이는 곳은 둑군데이며 서로 다른 이유로 쓰이고 있다.
하나는 Proxy(ngix(proxy))를 이유로
다른 하나 Static 파일을 제공(ngix(serving))해주는 역할.

// default.conf
# 3000번 포트에서 frontend가 돌아가고 있다는 것을 명시
upstream frontend {
    server frontend:3000;
}

# 5000번 포트에서 backend가 돌아가고 있다는 것을 명시
upstream backend {
    server backend:5000;
}

server {
  	# Nginx 서버 포트 80번으로 열어준다.
    listen 80;

  	# location에는 우선 순위가 있는데 '/'은 우선 순위가 가장 낮다.
    # 여기서는 /api로 시작하는 것을 먼저 찾고
    # 그게 없다면 '/'로 시작되는 것이므로
    # 그 요청을 http://fronted로 보내면 된다. 
    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";
    }

}
// Dockerfile

FROM nginx
COPY ./default.conf  /etc/nginx/conf.d/default.conf

3. Docker-compose

컨테이너들이 서로 연결시켜주는 역할

도커환경의 MySQL부분 정리

DB를 운영 환경에서는 도커를 이용하는게 아닌
AWS RDS를 이용하므로
MySQL을 이용한 부분을 먼저 정리

mysql이 이제는 도커 안에서 돌아가는게 아닌 AWS에서 돌아가고 있는 것을
우리의 어플리케이션에 연결만 해줄것이니
mysql을 어플리케이션에 연결해주는 부분 빼고 다 지워준다.

// docker-compose.yml

version: "3"

services:
  frontend:
    # 개발환경을 위한 Dockerfile이 어디있는지 알려준다.
    build:
      dockerfile: Dockerfile.dev
      context: ./frontend

    # 코드를 수정 후 다시 이미지를 build하는 것 없이
    # 수정된 코드가 반영 될 수 있게 volume을 이용
    volumes:
      - /app/node_modules
      - ./frontend:/app

    # 리액트 앱을 종료할때 나오는 버그를 잡아줌
    stdin_open: true

  nginx:
    # 재시작 정책
    # no : 어떠한 상황에서도 재시작을 하지 않는다.
    # always : 항상 재시작
    # on-faiulre : on-faiulre 에러코드와 함께 컨테이너가 멈추었을때만 재시작
    # unless-stopped : 개발자가 임의로 멈추려고 할때 빼고는 항상 재시작
    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"
    # 1. DB에 저장된 자료를 컨테이너를 지우더라도 자료자 지워지지 않을 수 있게
    # 2. 리액트나 노드쪽에서 코드를 업데이트할 때 바로 그 코드가 어플리케이션에 적용될 수 있게
    volumes:
      - ./mysql/mysql_data:/var/lib/mysql
      - ./mysql/sqls/:/docker-entrypoint-initdb.d/
    # Mysql의 Root 계정 비밀번호와 DB의 이름을 지정
    environment:
      MYSQL_ROOT_PASSWORD: johnahn
      MYSQL_DATABASE: myapp

$ docker-compose up --build

4. 깃헙에 Push

5. Travis CI

깃헙의 소스를 Travis CI에서 가져와줘야 한다.

빌드된 이미지를 도커허브로 보내는 장점

도커허브에 빌드된 이미지를 보내진 이미지를 AWS에서 가져가므로
EB 안에서 다시 이미지를 빌드 하지 않아도 된다.
모든 과정 중에 한번의 빌드만 이루어 진다.

도커허브 회원가입

도커허브 링크

Travis CI의 Setting에서 레파지토리 활성화.
cf) Travis CI org
Travis CI com으로 이전될 예정.
깃헙 소스가 변경될 때마다 소스를 가져와
테스트하고 배포하라고 알려줌

.travis.yml 파일 작성

// .travis.yml 

# 언어(플랫폼을 선택)
language: generic

# 관리자 권한 갖기
sudo: required

# 도커 환경 구성
services:
  - docker

# 스크립트를 실행할 수 있는 환경
before_install:
  - docker build -t devpark0714/react-test-app -f ./frontend/Dockerfile.dev ./frontend

# 실행할 스크립트(테스트)
script:
  - docker run -e CI=true devpark0714/react-test-app npm test

# 테스트 성공 후 할일
after_success:
  # 각각의 이미지를 빌드하기
  - docker build -t devpark0714/docker-frontend ./frontend
  - docker build -t devpark0714/docker-backend ./backend
  - docker build -t devpark0714/docker-nginx ./nginx

  # 도커 허브에 로그인
  - echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_ID" --password-stdin

  # 빌드된 이미지를 도커 허브에 푸쉬
  - docker push devpark0714/docker-frontend
  - docker push devpark0714/docker-backend
  - docker push devpark0714/docker-nginx

도커 허브에 로그인 값 넣어주기

echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_ID" --password-stdin

Travis CI의 Setting에서 Environment Variables

깃헙에 푸쉬로 이미지가 빌드되어 도커 허브에 올라가는지 확인

Dockerrun.aws.json

노드, Mysql, NginX 등을 위한 dockerfile이 여럿 있다.
그러기에 EB가 어떤 파일을 먼저 실행하고 어떻게 행동을 취해야 하는지
자동으로 프로세스를 해나갈 수 없기 때문에
임의로 설정해주는 파일이 Dockerrun.aws.json 이다.

AWS 정의
Docker 컨테이너 세트를 EB 애플리케이션으로 배포하는 방법을 설명하는
EB 고유의 JSON 파일.
멀티 컨테이너 Docker 환경에 사용.

EB가 어떤 파일을 먼저 실행하고 어떻게 행동을 취해야 하는지
자동으로 프로세스를 해나갈 수 없기 때문에
Task에 어떻게 컨테이너를 실행할지 정의해준다. 작업정의(Task Definition)

작업 정의를 등록할 때는 Container Definition을 명시해줘야한다.
Dockerrun.aws.json안에다 이 Container Definition을 넣어준다.

Container Definition은 도커 데몬으로 전해진다.

// Dockerrun.aws.json

{
  	// Dockerrun 버전 2로 지정
    "AWSEBDockerrunVersion": 2,
    // 이 안에서 컨테이너들을 정의
    "containerDefinitions": [
      	// 하나의 컨테이너
        {
          	// 컨테이너 이름
            "name": "frontend",
          	// Docker 컨테이너를 구축할 온라인 Docker 리포지토리의 Docker 이미지 이름
            "image": "devpark0714/docker-frontend",
          	// 이 이름을 이용해서 도커 컴포즈를 이용해 생성된 다른 컨테이너에서 접근 가능 
            "hostname": "frontend",
          	// 컨테이너가 실패할 경우 작업을 중지해야 하면 true.
          	// 필수적이지 않은 컨테이너는 인스턴스의 나머지 컨테이너에 영향을 미치지 않고
          	// 종료되거나 충돌할 수 있다.
            "essential": false,
          	// 컨테이너용으로 예약할 컨테이너 인스턴스에 있는 메머리의 양.
          	// 컨테이너 정의에서 memory 또는 memoryReservation 파라미터 중 하나
          	// 또는 모두에 0이 아닌 정수를 지정하면 된다.
            "memory": 128
        },
      	// 하나의 컨테이너
        {
            "name": "backend",
            "image": "devpark0714/docker-backend",
            "hostname": "backend",
            "essential": false,
            "memory": 128
        },
        {
            "name": "nginx",
            "image": "devpark0714/docker-nginx",
            "hostname": "nginx",
            "essential": true,
          	// 컨테이너에 있는 네트워크 지점을 호스트에 있는 지점에 매핑
            "portMappings": [
                {
                    "hostPort": 80,
                    "containerPort": 80
                }
            ],
          	// 연결할 컨테이너의 목록. 연결될 컨테이너는 서로 검색하고 안전하게 통신 가능.
            "links": ["frontend", "backend"],
            "memory": 128
        }
    ]
}

6. Docker Hub

7. AWS ElasticBeanStalk

Java, .NET, PHP, Node.js, Python, Ruby, Go 및 Docker를 사용하여 개발된 웹 애플리케이션 및 서비스를 Apache, Nginx, Passenger 및 IIS와 같은 친숙한 서버에서 손쉽게 배포하고 확장할 수 있는 서비스(환경)

다중 컨테이너 앱을 위한 EB 환경 생성

VPC(virtual private cloud)와 Security Group

현재까지 운영환경에서의 DB를 위한 것을 하무것도 해주지 않았기 때문에
AWS의 RDS를 이용해 MYSQL을 어플레키이션과 연결해야 한다.
이 때문에 VPC와 Security Group을 설정해 주어야 한다.

VPC

VPC를 사용하면 AWS 클라우드에서 논리적으로 격리된 공간을 프로비저닝하여
고객이 정의하는 가상 네트워크에서 AWS 리소스를 시작할 수 있다.

AWS에서 만든 EC2 인스턴스나 EB 인스턴스 혹은 RDS DB를 만들었다면
이러한 인스턴스들을 나의 아이디에서만 접근이 가능하게
논리적으로 격리된 네트워크에서 생성이 되게 해준다.
그래서 다른 아이디로는 접근 하는 것은 물론 보는 것도 불가능하게 된다.

EB 인스턴스나 RDS를 생성하면 자동적으로 기본 VPC가 할당되며
할당이 될 때는 지역(region)별로 다르게 할당이 된다.

Security Group

Inbound : 외부에서 EC2 인스턴스나 EB 인스턴스로 요청을 보내는 트래픽(HTTP, HTTPS, SSH 등)
Goutbound :EC2인스턴스나 EB 인스턴스 등에서 외부로 나가는 트래픽.
ex) 파일을 다운로드 하거나 inbound로 들어온 트래픽을 처리하여 응답하는 경우

Security Group이 Inbound와 Outbound를 통제해서
트래픽을 열고 닫을 수 있다.

VPC와 Security Group을 이용해서 EB 인스턴스와 RDS가 서로 소통할 수 있는 방법은
VPC안에 있는 AWS 서비스 간에서 트래픽을 모두 허용할 수 있게 Security Group을 허용해준다.

DB를 위한 환경변수 생성

// docker-compose.yml

  backend:
    environment:
      MYSQL_HOST: mysql
      MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: johnahn777
      MYSQL_DATABASE: myapp
      MYSQL_PORT: 3306
// backend/db.js

const mysql = require("mysql");

const pool = mysql.createPool({
  connectionLimit: 10,
  host: process.env.MYSQL_HOST,
  user: process.env.MYSQL_USER,
  password: process.env.MYSQL_ROOT_PASSWORD,
  database: process.env.MYSQL_DATABASE,
  port: process.env.MYSQL_PORT,
});
exports.pool = pool;

MySQL을 위한 AWS RDS 생성

AWS RDS

docker-compose.yml 환경변수의

마스터 사용자 이름 : MYSQL_USER
마스터 암호 : MYSQL_PASSWORD
초기 데이터베이스 이름 : MYSQL_DATBASE

Security Group

EB 인스턴스와 RDS가 서로 요청을 보낼 수 있게
Security Group 생성

AWS VPC

VPC - 보안 - 보안그룹 - 보안그룹 생성

인바운드/아웃바운드 규칙 설정

포트 범위 : 3306
소스 : 잠시 전에 만든 보안그룹 선택(DockerSecurityGroup)

profile
slowly but surely

0개의 댓글