Docker + EC2 로 배포하기

1

오늘도 레벨업

목록 보기
1/4

개발 환경

backend : nestJS
frontend: react

Directory 구성

Backend
	-dockerfile.dev
    -dockerfile
frontend
	-dockerfile.dev
    -dockerfile
    -ngnix.conf
docker-compose.yml
.dockerignore

Dockerfile 작성

도커 파일은 총 2개씩 4개의 파일을 만들었는데 실제로 운영서버만 배포를 진행하였고 개발서버 도커파일은 로컬에서만 확인을 했다.

도커파일을 작성하는 요령은 이 게시글을 참고했다.

위 링크에서는 DB(postgre)를 포함한 상태지만 mongo-atlas를 사용하는 경우에는 별도로 db 정보를 입력할 필요 없이 url을 환경변수로만 넣어주면 되었다.

개발

backend


//Dockerfile.dev


FROM node:12-alpine // node를 도커허브에서 받아온다

RUN mkdir -p /svr/app //파일이 저장될 저장소 만들기 

WORKDIR /svr/app // 만든 저장소를  workdir로 지정

RUN npm i -g @nestjs/cli --silent  //nest cli 설치 

COPY package.json .  
//  필요한 dependency의 목록을 복사한다(node module을 복사하는 것이 아닌 목록을 가져옮)

COPY package-lock.json . 

RUN npm install --silent 

// dependency 설치 (--silent: 로그 없이 설치) 

COPY . .
// 현재 디렉토리 (dockerfile이 있는 곳)에 있는 파일을 만든 저장소에 복사

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

// nest를 디버깅 모드로 서버 시작 ( nest start --watch)

이슈

Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error

이 에러같은 경우 bcrypt의 특성에 따라 발생하는 에러이다. bcrypt같은 경우 python 기반으로 작동한다. dockerfile에서의 Node:12-alpine 같은 경우 필요한 최소의 기능만 담아 용량을 줄였기 때문에 python이 없고 그에 따라서 bcrypt를 인식하지 못하는 문제가 발생한다. 이 경우 가장 빠르고 편한 방법은 bcrypt 대신에 bcryptjs를 사용하는 것이다. 이 라이브러리 같은 경우 bcrypt가 js 환경에서 더 잘 작동하도록 만들어졌다.

다른 방법은 python을 별도의 명령어로 docker file에 추가해주는 방식(참고)이 있다.

bcryptjs-npm

ReferenceError: TextEncoder is not defined Node.js with mongoose

이 에러 같은 경우 docker로 받아온 node의 버전이 너무 오래되어서 호환이 안되기 때문에 발생한 것이다

mongoose v6 이상은 nodee 12 이상을 요구하기 때문에 From node:10 -alpine으로 별도의 예제를 따라하다 문제가 발생했다.

node버전을 올려서 받아주면 문제가 해결된다.

스택오버플로우 - 참고

frontend


//Dockerfile.dev


FROM node:12-slim  

// alpine보다는 python이 추가된 가벼운 형태의 node (https://jadehan.tistory.com/58)

WORKDIR /usr/src/app

COPY . .

RUN npm install --silent

운영

운영 서버에 사용되는 dockerfile의 경우 build의 과정을 거쳐야 한다. nest, react모두 js기반이기 때문에 web브라우저가 이해하고 동작하기 위해서는 static한 파일이 되는 build를 거쳐야 하는 것이다.

추가로 frontend같은 경우 webserver를 통해서 html, css, js 파일들이 웹에 그려지기 때문에 ngnix, apache같은 웹서버 설정도 필요하다.

backend


FROM node:12-alpine


WORKDIR /app

COPY . .

RUN npm install --silent

RUN npm run build // 빌드 진행 

EXPOSE 5000 //포트 설정 

CMD ["node", "dist/main"]  //      "start:prod": "node dist/main",

frontend

FROM node:12-slim as build
//build라는 이름의 temporary image (multistage pattern)

WORKDIR /app

COPY package.json .

RUN npm install --silent

COPY . .

RUN npm run build

FROM nginx:alpine  // nginx를 도커허브로 부터 받아온다.

COPY nginx.conf /etc/nginx/conf.d/default.conf 
// 같은 디렉토리에 있는 nginx.conf 파일 복사 

COPY --from=build /app/build /usr/share/nginx/html  
//임시로 만든 build 이미지를 가져와서 그 파일을 nginx/html에 복붙!

EXPOSE 80  //port 개방 

CMD ["nginx", "-g", "daemon off;"]
//nginx.conf

server {
    listen 80;
    
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri /index.html;
    }
}

Docker compose

사용한 docker compose 코드는 아래와 같다.


version: "3" //version 지정 - multi-container같은 경우 3 추천

services:
  frontend:
    container_name: app_frontend  // container 이름 지정 
    # image: ECR uri 지정   
    // 만약 aws ECR을 이용해서 build파일을 가져올 경우- cloud
    build: 
      context: ./frontend  // 어디서 파일을 읽어올지 (디렉토리 지정)
      dockerfile: Dockerfile  // 어떤 dockerfile을 읽을지
    restart: always  // 문제 발생시 어떻게 할지 - 항상 다시 시작
    networks:
      - backend   // 지정한 network로 container간 통신
    ports: 
      - "3000:80"  
      // port지정 - nginx가 80을 base로 사용하지만 3000으로 사용
    volumes:
      - "./frontend/src:/usr/src/app/src"
      //file 변경에 따라서 추적 

  backend:
    container_name: app_backend
    # image: ECR uri 지정 
    build:
      context: ./backend
      dockerfile: Dockerfile
    restart: always
    networks:
      - backend
    ports:
      - "5000:5000"
    volumes:
      - ./backend:/svr/app
      - /svr/app/node_modules

    environment:
      MONGO_URI: 환경변수로 mongo_atlas uri 넘겨줌 
      SECRETKEY:  jwtsecret
      SWAGGER_USER: -
      SWAGGER_PASSWORD: -
      AUTHOR_EMAIL: - 

networks:  // container간 통신 방식 
  backend:
    driver: bridge

image vs build

작성한 코드 파일을 dockerfile의 디렉토리에서 복사한 내용으로 빌드를 할 수도 있고 aws 서비스 중에 하나인 ECR(elastic container resistry)를 이용해서 cloud로 이용할 수 있다.
ECR docker image

이 프로젝트 같은 경우 두가지 방식으로 진행을 해보았는데 아무래도 git clone을 받아서 진행을 할 것이고 매번 docker hub에 푸시를 하고 다시 ECR을 최신화하는 과정이 자동화가 되지 않으면 여간 성가신 일이 아닐 것 같아서 그냥 build로 진행을 했다.

EC2 배포

가장 기본적인 형태인 EC2로 배포를 진행하기로 했다.

튜토리얼-Docker Compose MEAN stack on AWS EC2을 기반으로 진행을 하였다.

EC2 인스턴스 생성

인스턴스를 생성하여 진행을 할 때 주의해야 할 부분은 보안그룹을 설정이다. 이 곳에서 portip를 개방해주어야지 docker yml에서 지정한 port로 접속이 가능하다.

편집을 눌러서 추가해주면 된다.

프로젝트의 경우에는 frontend 포트 3000, backend 포트 5000, nginx 포트 80, 기본 ssh 트래픽 80 을 지정해주었다.
그리고 접속을 어느 ip에서든 할 수 있도록 0.0.0.0으로 지정해주었다.

Acces key-pair 발급

EC2의 접속을 하기 위해서는 access key를 발급받아야 한다.
공식문서에 과정이 잘 나와있어 따라만 하면 되서 큰 문제는 없다. ( aws cli 설치 필요)

다운 받은 .pem 파일의 디렉토리 위치를 기억해두어야 한다.

EC2 접속 + 도커 설치

인스턴스를 생성하고 나면 해당 인스턴스의 퍼블릭 DNS주소를 확인할 수 있다. 이 주소를 기반으로 내가 만든 인스턴스의 shell로 접속이 가능하다.

accesss-key(pem 파일)이 위치한 디렉토리에서
ssh -i '파일이름.pem' '내가지정한OS'@'내인스턴스의 퍼블릭DNS주소'
을 입력하면 ubuntu shell의 접속할 수 있다.

그 다음 sudo apt update를 입력하여 OS를 최신으로 업그레이드

이후 sudo apt install docker.io ,sudo apt install docker-compose 를 입력하여 도커를 OS에 설치

sudo usermod -a G Docker ubuntu 로 도커에 sudo 권한 부여

sudo usermode

github clone

이제 세팅은 끝이 났고 만든 코드를 깃헙에서 clone 받아 docker만 돌려주면 끝이난다.

git clone 레포지토리 링크
cd 해당 폴더이름
docker-compose --build up

도커 파일이 있는 곳에서 build를 진행하고 docker-compose up을 하면 서버가 돌아가게 된다. 코드 변화가 없는 상태에서 서버를 다시 킬때는 --build명령어를 제외하면 된다.

Nohup

No hang up

아무리 ec2에 서버를 배포했더라도 지금 내 컴퓨터가 아닌 아마존 컴터를 빌려쓰는 개념의 차이만 있다. 그래서 aws 터미널을 끄면 로컬환경에서와 같이 서버가 종료되게 된다. 터미널을 종료하더라도 백그라운드에서 계속하여 돌아가도록 리눅스의 nohup 과 같이 명령어를 입력한다.
나의 프로젝트 경우에는 nohup docker-compose up으로 도커파일로 서버를 띄우는 명령어를 백그라운드로 실행했다.

서버를 종료하려면 해당하는 processpsid를 찾은다음 kill을 해주면 된다.
나의 경우에는 docker ps를 통해서 확인이 가능하다.

새로 터미널을 키고 바로 docker ps를 입력했는데 잘 돌아가고 있는 상태

nohup 참고

배포과정에서 발생한 이슈

aws 프리티어 초과

똑같은 accesskey로 진행을 했는데 갑자기 진행이 안되길래 console을 확인해보니 위와 같이(검사 1/2 - 인스턴스 연결성 검사 실패) 나왔다. 이 경우 검색을 해보니 프리티어에 할당된 cpu, 메모리를 초과해서 사용한 것이 원인이라고 하였다.

그래서 인스턴스 타입을 변경을 하고 다시 서버를 돌려보니 잘 돌아가게 되었다. 단점은 타입을 변경하게 되면 public DNS주소도 달라지고 권한같은 경우(github)도 초기화 되는 것 같았다. 권한 문제가 발생하여 폴더를 지우고 다시 클론을 받아 진행했다.

awss-인스턴스 타입 변경

Axios url 중복 문제

로컬에서 문제가 없었는데 배포를 하고 나니 axios가 url을 인식하는 과정에서 오류가 발생했다. http//가 추가로 url에 들어갔는데 이를 해결하기 위해 axios의 baseUrl을 별도로 지정을 해주어 요청 엔드포인트를 정확히 지정해주었다.

axios.defaults.headers.common["Access-Control-Allow-Origin"] = `${awsDNS}:3000`;

axios.defaults.baseURL = `${awsDNS}:5000/api`;
ReactDOM.render(
	...
  document.getElementById("root")
);

Cors issue

배포를 하고 나니 로컬 환경에서 일어나지 않았던 CORS이슈가 발생했다. CORS의 정의대로 same origin이 아닌 cross-origin에서 온 요청을 보안을 위해서 막게된 것이다. 로컬에서야 localhost로 같은 origin이지만 배포를 하면 다르게 인식이 되었다.

특히 이 부분은 nest쪽에서 handle을 했어야 했는데 대부분의 인터넷 해결방안은 모든 origin에 대해서 허용을 하는 방식으로 진행이 되었다. 이게 근본적인 해결 방안은 아니라고 생각을 해서 직접 지정해주기로 했다.

방법이 두가지가 있었는데 하나는 앱 전체적으로 허용 origin을 지정,각 api요청에 Allow-cross-origin 헤더 추가 방식이있었다.

앱 전체적으로 허용 origin을 지정

  const app = await NestFactory.create<NestExpressApplication>(AppModule);
	...
  SwaggerModule.setup('docs', app, document);
  app.enableCors({
    origin: ['특정 url'],
    credentials: true,
  });

하지만 이렇게 지정을 했을때도 cors문제는 해결이 되지 않았다. (정확한 원인을 찾지는 못함)

앱 전체적으로 허용 origin을 지정

  const app = await NestFactory.create<NestExpressApplication>(AppModule);
	...
  SwaggerModule.setup('docs', app, document);
  app.enableCors({
    origin: ['특정 url'],
    credentials: true,
  });

Allow-origin-control header추가

axios.defaults.headers.common["Access-Control-Allow-Origin"] = `${awsDNS}:3000`;

ReactDOM.render(
  <React.StrictMode>
    <AuthContextProvider>
      <GoalContextProvider>
        <App />
      </GoalContextProvider>
    </AuthContextProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

axios.default.headers.commmon을 통해서 axios의 모든 요청에 대해서 header를 한번에 추가할 수 있다.

api 요청에 별도의 명시를 하지 않았지만 header에 추가되어서 전달되는 것을 확인가능하다.

추가사항

처음 도커 기반으로 배포를 진행해본 것이다 보닌 심플한 방법으로 접근을 했는데 추가적으로 githupaction이나 zenkins로 테스트코드까지 챙기고 CI/CD도 구축해봐야겠다.

profile
기록을 통해 한 걸음씩 성장ing!

0개의 댓글