이번에는 프론트 부분만을 사용하는게 아니라 백엔드 서버 + DB 까지 포함시켜서 만들어본다.
설명
- 아무 글이나 입력하면 리액트를 통해서 노드로 전달된 이후에 MySQL 데이터베이스에 저장한 후 그 저장된 것을 화면에 보여주는 앱을 만들어본다.
- 컨테이너를 재시작해도 DB 에 저장된 데이터는 남아있게 된다.
Multi Container 앱을 위한 설계 방식
- Nginx 로 클라이언트에서 오는 요청을 백엔드 서버와 프론트 서버로 나눠주는 구조
- Nginx 는 프론트 서버로만 사용하여 클라이언트에서 정적 파일을 요구할 때 제공해주는 구조
1번 방식
- 클라이언트(브라우저) 의 요청이 Nginx(프록시) 로 가고 여기에서 라우팅 역할을 하게 된다.
- 프론트 엔드 서버와 백엔드 서버를 가기 위해서는 Nginx 를 거친다.
- 장점은 Request 를 보낼 떄 URL 부분에 HOST 이름이나 포트가 바뀌어도 변경을 하지 않아도 된다.
- 즉 axios.get("/api/values") 처럼 api 경로만 넣어준다.
- 단점으로는 Nginx 설정이나 전체 설계가 복잡하다.
2번 방식
- 클라이언트(브라우저) 의 요청이 오면 요청된 PORT 에 따라서 프론트 서버나 백엔드 서버로 가게 된다.
- nginx 는 정적 파일을 보여줄 때만 사용된다.
- 장점으로는 설계가 다소 간단하고 구현하는데 쉽다.
- 단점으로는 HOST 이름이나 포트 변경이 있을 때 Request URL 도 변경되어야 한다.
- axios.get("http://localhost:5000/api/values") 로 하다가 만약 포트 번호가 바뀌게 된다면 수정을 해줘야한다.
전체적인 구현 순서
- 애플리케이션 소스 코드 작성
- 도커 파일 작성 (개발 환경 + 운영 환경 총 2개)
- 도커 컴포즈 파일 작성 (frontend , backend , nginx , mysql)
- github 에 push (메인 브랜치로 병합)
- Travis CI (테스트 소스 실행 후 성공하면 각 도커 파일을 이용해 이미지를 생성하고 도커 허브로 전달)
- 전에는 도커 허브로 전달하지 않았지만 이번엔 도커 허브로 전달해본다.
- 도커 허브
- Travis CI 에서 빌드된 이미지를 보관하고 AWS ElasticBeanStalk 에서 가져가려고 할 때 전달해준다.
- AWS ElasticBeanStalk 에 배포
Node JS 구성
- 폴더안에 backend , frontend 폴더 생성
- backend 에서 npm init
구현 순서
- package.json 파일안에 스크립트와 사용할 모듈들 명시
- express , mysql , nodemon , body-parser(요청 본문 해석)
- server.js 구현
- mysql 연결을 위한 ds.js 구현
- host , usrename , password 명시하고 pool 생성
- 이렇게 만든 pool 을 다른 부분에서도 사용할 수 있게 export
- export 된 pool 을 server.js 에서 불러오기
- 2가지 API 설계
- 데이터 노출 (GET)
- 데이터 입력 (POST)
React JS 구성
- npx create-react-app frontend 로 frontend 파일에 리액트 설치
구현 순서
- UI 를 위한 코드 작성 (데이터 입력할 수 있는 Form)
- 데이터의 흐름을 위한 State 생성
- usetState 을 사용하기 위해 react 라이브러리에서 가져오기
- lists : 데이터베이스에 저장된 값을 가져와서 화면에 보여주기 전 이 State 에 넣어둔다.
- value : Input 박스에 입력한 값이 이 state 에 들어간다.
- 데이터 베이스에서 데이터를 가져오기 위해 필요한 userEffect 사용
- useEffect 을 사용하기 위해 react 라이브러리에서 가져오기
- useEffect 는 데이터베이스에 있는 값을 가져오기위해 사용된다.
- 이벤트 핸들러 처리
- changeHandler : input 박스에 입력할 때 onChange Event 가 발생하는데 value State 을 변화 시켜준다.
- submitHandler : 값을 input 박스에 입력하고 확인 버튼을 누르면 입력한 값이 데이터 베이스에 저장되고 그 후에 화면에 표출도 시켜준다.
리액트 앱을 위한 도커 파일 만들기
- frontend 파일안에 Dockerfile , Dockerfile.dev 생성
# Dockerfile.dev (개발 환경)
FROM node:alpine
WORKDIR /app
RUN npm install
COPY ./ ./
CMD [ "npm" , "run" , "start" ]
----------------------------------------------------------------------------------------
# Dockerfile (운영 환경)
FROM node:alpine as builder
WORKIDR /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
- FROM nginx 전까지는 Nginx 가 제공을 해줄 build 파일들을 생성하는 단계
- 프론트 부분에서 정적 파일들을 가져다주는 nginx 에 대한 부분
- COPY 부분은 밑에 나올 default.conf 에서 해준 설정을 nginx 컨테이너 안에 있는 설정이 되게 복사를 해준다.
정적 파일을 제공해주기 위한 Nginx 를 위해서 frontend 안에 nginx 파일을 생성하고 default.conf 파일을 생성한다.
# default.conf
server {
listen 3000;
location / {
root /usr/share/nginx/html; # HTML 파일이 위치할 루트 설정
index index.html index.htm; # 사이트의 index 페이지로 할 파일명 설정
try_files $uri $uri/ /index.html #React Router 를 사용해서 페이지간 이동할 떄 사용
}
}
개발환경을 위한 Dockerfile.dev 는 이해하기 어렵지 않다.
운영환경을 위한 Dockerfile 을 생각해보면 Nginx 가 2가지의 역할을 한다.
- 첫 번째는 라우팅 하는 역할 , 두 번째는 정적 파일을 제공하는 역할
- 이러한 역할들을 하기 위해서는 nginx 에게 Build 파일을 제공해줘야 한다.
- 그게 바로 as builder 부분이다. (COPY --from=builder...)
- 뿐만 아니라 default.conf 에서 설정 한것도 nginx 가 알고 있어야 하기 때문에 그 부분도 COPY 를 해줘야한다.
- default.conf 에는 정적 파일들이 어디에 위치해있고, 라우팅을 할 때 필요한 부분들이 정의되어 있다.
그래서 nginx 의 역할을 하기 위해 필요한 정보들을 COPY 할 수 있도록 Dockerfile 을 만든다고 생각하면 된다.
노드 앱을 위한 도커 파일 만들기
- 개발환경을 위한 Dockerfile.dev 작성
FROM node:alpine
WORKDIR /app
COPy ./package.json ./
RUN npm install
COPY . .
CMD ["npm" , "run" , "dev"]
- CMD 에서 start 가 아닌 dev 인 이유는 코드가 변경될 때 바로 반영을 시켜주게 해 주는 nodeman 이라는 모듈을 사용하고 싶기 떄문에 dev 로 한다.
- 운영환경을 위한 Dockerfile 작성
FROM node:alpine
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
CMD ["npm" , "run" , "start"]
리액트는 nginx 부분까지 생각해야 했기 때문에 조금 길었지만 노드 앱을 위한 도커 파일은 어렵지 않게 작성할 수 있다.
DB
- Mysql 데이터베이스를 위해 도커 파일을 만들어야 한다.
- 개발 환경과 운영 환경 각각에서 DB 구성을 어떻게 해야할지 알아보자.
개발 환경 ----------> 도커 환경
운영 환경 ----------> AWS RDS 서비스 이용
굳이 이렇게 나누는 이유?
- DB 작업은 중요한 데이터들을 보관하고 이용하는 부분이기 때문에 조금의 실수로도 안 좋은 결과를 얻을 수 있다.
- 실제 중요한 데이터들을 다루는 운영환경에서는 더욱 안정적인 AWS RDS 를 이용하여 DB 를 구성해 보는 것이 실제로 실무에서 더 보편적으로 쓰이는 방법이기에 이렇게 개발 환경과 운영환경을 나눈다.
개발환경에서는 ElasticBeanStalk 안에 MySQL 까지 모두 들어간다.
운영환경에서는 ElasticBeanStalk 안에 MySQL 이 들어가지 않고 그 밖에서 AWS RDS 로 연결된다.
MySQL 을 위한 도커 파일 만들기
- 원래 데이터베이스를 사용하려면 먼저 데이터베이스 설치 파일을 다운로드하고 그걸 이용해서 데이터베이스를 설치하고 Node 앱에 연결을 해줘야하지만, 지금은 설치를 하지 않았다.
- 그래서 도커 이미지를 이용해서 설치를 해야한다.
- mysql 폴더 만들고 Dockerfile 작성
FROM mysql:5.7 # 베이스 이미지를 도커 허브에서 가져온다.
ADD ./my.cnf /etc/mysql/conf.d/my.cnf # 이 부분은 2,3번 가보면 알 수 있다.
- mysql 을 시작할 때 Database 와 Table 이 필요한데 만들 장소 생성
- mysql 에서 sqls 폴더 만들고 initalize.sql 파일 생성
- 여기에 CREATE DATABSE , USE , CREATE TABLE 와 같은 쿼리문 작성
- 한글도 저장할 수 있도록 설정 추가
- 현재는 어떠한 글을 데이터베이스에 넣어줄 때 한글이 깨지기 때문에 오류가 발생한다.
- 그래서 mysql 폴더안에 my.conf 라는 파일을 생성하고 아래와 같이 작성한다.
# my.conf
[mysqld]
character-set-server=utf8
[mysql]
default-character-set=utf8
[client]
default-character-set=utf8
- 한글이 깨지는 현상을 막기 위해 utf8 로 인코딩할 수 있게 설정
- 실제 mysql 설정을 해주는 my.conf 파일로 덮어 씌우기 위해서 Dockerfile 에 ADD 가 들어가는 것이다.
- 즉 이렇게 생성한 my.conf 를 실제 mysql 설정에도 적용시키는 것
실제로 mysql status 로 확인을 해보면 server , db , client characterset 이 원래는 latine1 이지만 utf8 로 바뀐다.
또한 Dockerfile 과 Dockerfile.dev 가 특별하게 다르게 해 줄 이유가 없다면 같은 내용이 들어간다.
NGINX 를 위한 도커 파일 만들기
- 현재 Nginx 가 쓰이는 곳은 2가지이며 서로 다른 이유로 쓰인다.
- 첫 번째는 Proxy , 두 번째는 Static 파일 제공
- 클라이언트에 요청을 보낼 때 정적 파일을 원할 때는 Nginx 의 설정에 따라 자동적으로 React JS 로 보내주고 API 요청일 경우에는 Node JS 로 보내준다.
- 요청을 나눠서 보내는 기준은 location 이 / 로 시작하는지 , /api 로 시작하는지로 판단한다.
- / 로 시작하면 프론트서버에게 , /api 로 시작하면 백엔드 서버에게 보낸다.
이러한 Proxy 기능을 위해 Nginx 설정을 해야한다.
1. nginx 폴더 만들고 default.conf , Dockerfile 생성
- 위에서 만들었던 frontend 폴더 안에 nginx 와는 다른 역할을 하는 nginx 이다.
- frontend 안에 있는 nginx 는 정적 파일 제공
- 이번에 만드는 nginx 는 Proxy
- default.conf 파일에 프록시 기능 작성
# 3000번 포트에서 frontend 가 돌아가고 있다는 것을 명시
upstream frontend {
server frontend:3000;
}
# 5000번 포트에서 backend 가 돌아가고 있다는 것을 명시
upstream backend {
server backend:5000;
}
server {
# nginx 서버 포트 80 번으로 열기
listen 80;
# location 에는 우선순위가 있는데, 그냥 / 이렇게만 되는 것은 우선순위가 가장 낮다.
# /api 로 시작하는 것을 먼저 찾고 그게 없다면 / 이렇게 시작되는 것 이기 떄문에
# 그 요청을 http://frontend 에게 보낸다.
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";
}
}
- 바로 뒤에 docker-compose 파일을 작성할 때 services: 부분에 frontend: , backend: 로 만들 예정이기 때문에 http://frontend , http://backend 로 설정해도 된다.
- 도커 파일을 이용하기 떄문에 이처럼 가능한 것이지 원래는 도메인 주소나 IP 주소를 넣어줘야한다.
- Nginx 를 위한 Dockerfile 작성
FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf
- 위에서 작성한 conf 파일을 컨테이너에서 실행될 Nginx 에도 적용될 수 있게 COPY 한다.
Docker Compose 파일 작성
- 각각의 컨테이너를 위한 도커파일을 작성했지만 컨테이너들을 따로 작동시킨다면 서로 통신을 할 수 없다.
- 그래서 컨테이너들을 서로 연결시켜주기 위해서 Docker Compose 를 작성해보자.
- backend , frontend , mysql , nginx 컨테이너 연결
version: '3'
services:
frontend:
build:
dockerfile: Dockerfile.dev
context: ./frontend
volumes:
- /app/node_modules
- ./frontend:/app
stdin_open: true
nginx:
restart: always
build:
dockerfile: Dokerfile.dev
context: ./nginx
ports:
- "3000:80"
backend:
build:
dockerfile: Dockerfile.dev
context: ./backend
container_name: app_backend
volumes:
- /app/node_moudles
- ./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: jiwon
MYSQL_DATABASE: myqpp
재시작 정책
- no : 어떠한 상황에서도 재시작 하지 않는다.
- always : 항상 재시작 한다.
- on-failure : 에러 코드와 함께 컨테이너가 멈췄을 때만 재시작
- unless-stopped : 개발자가 임의로 멈추려고 할 때 뺴고는 항상 재시작
- docker-compose up 실행
Volume 을 이용한 데이터베이스 데이터 유지
volumes:
- ./mysql/mysql_data:/var/lib/mysql
- ./mysql/sqls/:/docker-entrypoint-initdb.d/
- 해당 volumes 는 무엇 때문에 사용하는 걸까?
- 현재까지는 volume 을 사용하는 용도는 리액트나 노드쪽에서 코드를 업데이트할 때 바로 그 고크다 애플리케이션에 적용이 될 수 있게 해주기 위해서 사용했다.
- 이번에는 데이터베이스의 저장된 자료를 컨테이너를 지우더라도 자료가 지워지지 않을 수 있게 해주기 위한 volume 이라고 생각하면 된다.
원래는 컨테이너를 지우면 컨테이너에 저장된 데이터들이 지워진다.
1. 도커 이미지로 컨테이너를 생성한다.
2. 이때 도커 이미지는 읽기 전용이 된다.
3. 데이터가 추가될 때는 컨테이너 안에 데이터가 저장된다.
4. 컨테이너 삭제할 때 컨테이너 안에 저장된 데이터도 함께 삭제된다.
그렇기 때문에 컨테이너를 삭제하면 컨테이너 안에 저장된 데이터까지 삭제가 된다면 영속성이 필요한 데이터들은 어떻게 처리해야할까?
이를 위해서 volumes: 를 설정해줘야 한다.
- 볼륨이란 도커 컨테이너에 의해 생성되고 사용되는 지속적인 데이터를 위한 선호 메커니즘
- 볼륨을 이용하면 데이터 영속성을 보장할 수 있다.
- 도커 컨테이너 안에 데이터는 호스트 파일 시스템(DockerArea) 와 연결이 되어 있다.
-> DockerArea 의 위치는 /var/lib/docker/volumes/
- 컨테이너에서 변화가 일어난 데이터가 컨테이너 안에 저장되는것이 아니라 호스트 파일 시스템에 저장되고 그중에서도 도커에 의해서만 통제가 되는 DockerArea 에 저장이 되므로 컨테이너를 삭제해도 변환된 데이터는 사라지지 않는다.
참고자료