Section5 : Docker로 다중 컨테이너 애플리케이션 구축하기

tein·2022년 7월 8일
0

도커 & 쿠버네티스

목록 보기
7/14

이번 섹션에서는

  • 함께 작동하는 여러 서비스로 구성된 애플리케이션(프론트엔드 애플리케이션, 백엔드 웹 서버, 데이터 베이스 등)을 구축하는 방법
  • 여러 서비스를 하나의 큰 앱에 결합하는 방법
  • 도커를 사용하여 여러 컨테이너와 함께 작동하는 방법

에 대해 알아본다.


🎈Target 앱 & 설정

지난 섹션에서는 MongoDB 컨테이너를 제거했다가 다시 생성하면 데이터를 잃었었다.
이번 섹션에서는 그 부분을 개선시킨다. 액세스 또한 제한한다.

그리고 백엔드 API에서 로그 데이터를 유지하고자 한다.

그리고 소스 코드 변경사항을 즉시 반영되도록 한다.

🎈MongoDB 서비스 도커화 하기

  1. $] docer run --name mongodb --rm -d mongo

    이것이 mongodb 컨테이너를 시작하는 가장 기본적인 형태

  2. $] docker --name mongodb --rm -d -p 27017:27017 mongo

    데이터 베이스를 포함하는 컨테이너를 가동하여 1이 작동하려면 도커 컨테이너에서 로컬머신으로 MongoDB의 포트인 27017을 노출해야한다.
    따라서 로컬 호스트 머신의 동일한 포트에서 이 27017 포트를 노출하여 컨테이너에서 MongoDB가 실행되도록 할 수 있다.
    하지만 아직 도커화 되지 않은 로컬 실행 백엔드와 여전히 통신할 수 있다.

  • 먼저 $] docker container prune 실행하여 중지된 모든 컨테이너 제거한다.
    (이전에 실행시킨 MongoDB 컨테이너가 존재하지 않도록)
  • 노드 api가 실행되고 있는 다른 터미널로 돌아가서 ctrl + c로 중지시킨다.
  • 그리고 $] node app.js로 다시 시작하면 MongoDB에 성공적으로 연결된다.

🎈Node 앱 도커화 하기

  1. 노드 서버가 실행중인 터미널로 이동

  2. ctrl + c로 종료

  3. 프로젝트의 자체 이미지 빌드하는 자체 Dockerfile작성
    (MongoDB는 이 작업이 필요 없었음)

  4. Dockerfile

    FROM node
    WORKDIR	/app
    COPY package.json .
    RUN npm install
    COPY . .
    EXPOSE 80
    CMD ["node", "app.js"]
  5. $] docker image prune -a 실행하여 시스템에 있는 사용하지 않는 이미지 모두 삭제

  6. $] docker build -t goals-node . 로 이미지 빌드
    4의 Dockerfile을 기반으로 백엔드 이미지를 빌드한다.

  7. 이미지 빌드 다 되면 $] docker run --name goals-backend --rm goals-node
    실행한다.

  8. 하지만 MongoDB에 연결하지 못해 충돌이 발생한다.
    포트를 노출하는 컨테이너에서 실행중인 MongoDB를 가지고 있는데, 도커화된 백엔드 애플리케이션에서 로컬호스트에 접근하고 있다.
    현재 이 애플리케이션이 컨테이너 내부에 있으므로 동일한 컨테이너 내부 포트에서 서비스에 접근하려고 한다. (호스트 머신에 접근X)
    (해결은 네트워킹 모듈에서 다룸)

  9. 여기서 앞서 배웠던 host.docker.internal 도메인으로 대체한다.
    이는 도커에 의해 실제 로컬 호스트 머신 ip로 변환되는 특수 주소, 특수 식별자이다.

  10. 따라서 이를 대체하면 실행중인 MongoDB 컨테이너와 함께 코드가 작동할 것이다.

  11. 소스 바꿨으니 이미지 리빌드 하고 컨테이너 다시 실행한다.

  12. 새로운 문제가 있으니 React 프론트엔드 애플리케이션이 이 백엔드와 통신할 수 없다는 것.
    이는 백엔드가 컨테이너에서 실행되는 동안 노출되는 포트를 게시하지 않기 때문
    그래서 특정 포트에서 이 백엔드 애플리케이션과 통신하려는 프론트엔드가 실패함.

  13. 그래서 컨테이너 중지 하고
    $] docker run --name goals-backend --rm -d -p 80:80 goals-node
    로 내부 포트 80을 게시한다.

  14. 이렇게 하면 백엔드와 프론트가 통신할 수 있다.
    (프론트는 아직 도커화 하지 않음)

🎈React SPA를 컨테이너로 옮기기

  1. 자체 명령이 다시 필요하므로 frontend 폴더에서 npm start로 실행했던 프로세스 중지한다. (ctrl + c)

  2. 도커화 하려면 React 프로젝트 설정에 대한 몇가지 기본 지식이 필요하다.
    프론트엔드 설정은 결국 node에 의존하지만, 노드 애플리케이션이 아니다.
    하지만 node를 사용하여 React 애플리케이션을 제공하는 이 개발 서버를 가동한다.
    node는 React 코드를 최적화하고 브라우저가 이해할 수 있는 코드로 변환하는데에 사용된다.
    이는 frontend 폴더에 있는 Dockerfile이 베이스 이미지로 node를 기반으로 해야하는 이유이다.
    (노드 코드 직접 작성 X, 프로젝트 설정에 NodeJS가 필요 O)

  3. frontend Dockerfile

    FROM node
    
    WORKDIR /app
    
    COPY package.json .
    
    RUN npm install
    
    COPY  . .
    
    EXPOSE 3000
    
    CMD ["npm", "start"]
  4. 이 파일로 빌드 한다.
    $] docker build -t goals-react .

  5. 완료되고 나면 컨테이너를 실행할 수 있다.
    $] docker run --name goals-frontend --rm -d -p 3000:3000 goals-react
    하지만 이를 실행하면 goals-react 컨테이너가 더이상 실행되지 않는데, attached 모드로 확인해보면 개발 서버를 시작한 후 작동이 멈추는 것을 알 수 있다.

  6. 이는 React 프로젝트의 설정과 관련된 것으로, 'docker run' 명령에 '-it' 옵션을 추가하여 인터렉티브 모드로 실행해야하기 때문이다.
    컨테이너에 명령을 입력하여 상호작용하길 원한다고 컨테이너에게 알려야한다. React 프로젝트는 이렇게 하지 않으면 이것이 트리거가 되어 즉시 서버를 중지하는 방식으로 설정되어 있기 때문이다.

  7. 이제 프론트엔드도 자체 컨테이너에서 실행된다.

  8. 각 컨테이너 이름을 통해 자동으로 서로 통신할 수 있게 하면서 데이터도 지속되도록 해보자.
    실행되고 있던 백엔드, 프론트엔드, 데이터베이스 컨테이너를 모두 중지한다. (터미널 전환에 주의한다.)

🎈효율적인 컨테이너 간 통신을 위한 Docker 네트워크 추가하기

  1. 먼저 $] docker network ls` 사용하여 사용 가능한 네트워크 확인한다.

  2. 새 네트워크를 만들것이므로, $] docker network create goals-net이라고 명명하여 생성한다.

  3. 이 네트워크에 모든 컨테이너를 시작하여 컨테이너 모두를 네트워크에 넣을 수 있다.

  4. $] docker run --name mongodb --rm -d mongo
    여기에서 MongoDB 컨테이너를 다시 실행하는데, 이제 포트를 적을 필요가 없다. 따라서 포트 대신 --netword를 추가하여 goals-net 네트워크에서 컨테이너를 실행한다.

    $] docker run --name mongodb --rm -d --network goals-net mongo

  5. 백엔드도 실행하기 전 코드에서 DB 접속 URI를 수정한다.
    host.docker.internal -> mongodb
    이미지를 다시 빌드하고,
    $] docker build -t goals-node .
    $] docker run --name goals-backend --rm -d --network goals-net goals-node

  6. 이제 MongoDB 컨테이너와 동일한 네트워크에 연결된 백엔드 컨테이너가 시작된다.

  7. React도 코드를 수정해야한다.
    localhost로 접근한 모든 곳을 노드 애플리케이션 컨테이너의 이름으로 수정한다.

    localhost -> goals-backend
    $] docker build -t goals react 입력하여 이미지 리빌드 한다.

    $] docker run --name goals-frontend --netword goals-net --rm -p 3000:3000 -it goals-react
    실행하면 작동하지 않는데, 이유가 좀 까다롭다.
    프론트엔드 애플리케이션은 노드 런타임에 의해 컨테이너에서 직접적으로 실행된다.
    React코드는 컨테이너 내부에서 실행되지 않고, 항상 브라우저에서 실행된다.
    이 점이 백엔드 노드 코드와 비교되는 핵심적인 차이점이다.

  8. React코드가 컨테이너 내부에서 실행되지 않고 항상 브라우저에서 실행되고, 이는 goals-backend에 접근하는 코드가 도커에 의해 goals-backend로 변환될 수 있는 컨테이너에서 실행되지 않음을 의미한다.

  9. 그래서 React 애플리케이션 코드에서 localhost로 되돌린다. 이는 로컬 머신과 통신할 수 있는 주소이다.

  10. Reack 애플리케이션에는 도커 컨테이너 내부에서 실행되는 JavaScript 코드가 아니라 브라우저에서 실행되는 JavaScript 코드가 있기 때문이다.

  11. 브라우저에서 실행되는 JavaScrpit 코드이므로 도커가 아닌 브라우저가 이해할 수 있는 코드가 필요하다.

  12. 그래서 Reack 애플리케이션이 있는 컨테이너를 중지하고 이미지를 다시 리빌드 한다.
    여기에는 --network 옵션이 필요하지 않고 대신 포트를 직접 노출한다.

  13. 백엔드는 MongoDB와 계속 통신해야하므로 네트워크가 필요하고, 따라서 중지하거나 다시 시작하지 않아도 된다.

🎈볼륨으로 MongoDB에 데이터 지속성 추가하기

  1. 기존 방식은 MongoDB 중지 후 재실행 하면 데이터가 사라진다. (--rm 옵션을 넣어뒀기 때문)

  2. 따라서 데이터를 분리해야하는데, 데이터가 컨테이너 중지 및 제거 후에도 남을 수 있도록 하드 드라이브에 저장해야한다.

  3. 데이터가 남도록 하려면 -v 플래그로 볼륨을 추가한다.
    MongoDB가 데이터를 저장하는 내부적인 경로를 알아야하는데, 그 경로는 DockerHub의 MongoDB 컨테이너에 대한 문서(https://hub.docker.com/_/mongo)에서 찾을 수 있다.

    MongoDB가 컨테이너 내부에 데이터베이스를 저장하는 곳은 /data/db 폴더다.
    앞서 배운 것 처럼 명명된 볼륨을 사용한다.

    $] docker run --name mongodb -v data:/data/db --rm -d --network goals-net mongo

  4. 이제 컨테이너를 중지하고 재실행해도 데이터가 여전히 존재하는 것을 볼 수 있다.

  5. 다음으로는 핵심 요구 사항 중 하나인 보안과 데이터 베이스 액세스 방지를 알아보자.

    환경 변수를 이용하여 사용자 이름과 비밀번호를 보안 레이어에 추가할 수 있다.
    MONGO_INITDB_ROOT_USERNAME, MONGO_INITDB_ROOT_PASSWORD

    $] docker run --name mongodb -v data:/data/db --rm -d --network goals-net -e MONGO_INITDB_ROOT_USERNAME=max -e MONGO_INITDB_ROOT_PASSWORD=secret mongo

    이렇게 실행하면 데이터 베이스가 보호되어서 MongoDB와 통신하는 노드 애플리케이션이 사용자와 비밀번호 없이 액세스하려고 하므로 연결에 실패하는 것을 볼 수 있다.

  6. https://www.mongodb.com/docs/manual/reference/connection-string/
    여기서 MongoDB 연결에 필요한 URI를 찾을 수 있다.

    mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]

    호스트 주소 앞에 '사용자이름:비밀번호@' 형식으로 추가한다.

    mongodb://mongodb:27017/course-goals -> mongodb://max:secret@mongodb:27017/course-goals

  7. 이제 노드 이미지를 다시 빌드한 다음 컨테이너를 재시작하면 실패하는데,
    URI 문자열 끝에 authSource=admin 쿼리 매개변수를 추가해야하기 때문이다.

    mongodb://max:secret@mongodb:27017/course-goals
    ->
    mongodb://max:secret@mongodb:27017/course-goals?authSource=admin

  8. 이제 진짜 연결된다.

🎈NodeJS 컨테이너의 볼륨, 바인딩 마운트 및 폴리싱(Polishing)

  1. 이제 노드 백엔드에서 컨테이너에 작성하는 로그 파일 데이터가 유지되도록 하고, 실시간 소스 코드 업데이트를 이루어지게 해보자.

  2. 백엔드 컨테이너를 중지하고, 두 개의 볼륨을 추가한다.
    1) 로그 파일에 필요한 볼륨 : 명명된 볼륨으로 실행
    $] docker run --name goals-backend -v logs:/app/logs --rm -p 80:80 --network goals-net goals-node
    - app/logs : 컨테이너 내부에서 로그가 기록되는 곳
    2) -v 빌드로 생성한 바인트 마운드 :
    - 폴더에 대한 전체 경로가 필요하다.
    - app.js를 우클릭하고 전체 경로를 복사한 후 맨끝의 app.js만 지운다.
    $] docker run --name goals-backend -v /Users/docker-complete/backend:/app -v logs:/app/logs --rm -p 80:80 --network goals-net goals-node
    - 더 긴 컨테이너 내부 경로가 우선하고, 더 짧은 경우 내부 경로를 덮어쓴다. 따라서 ^ logs의 예시는 컨테이너에서 작성한 로그가 로컬 호스트 프로젝트 폴더의 로컬 logs 폴더로 덮어쓰여지지 않는다.
    3) node_modules에도 유사한 작업을 한다.
    - 로컬 호스트 머신에 node_modules가 없는 경우 컨테이너의 기존 node_modules를 덮어쓰지 않아야한다.
    - 컨테이너가 충돌하고 의존성을 찾을 수 없기 때문
    4) 익명 볼륨을 추가한다.
    $] docker run --name goals-backend -v /Users/docker-complete/backend:/app -v logs:/app/logs -v /app/node_modules -d --rm -p 80:80 --network goals-net goals-node

  3. 노드 프로젝트에 부가 종속성을 더해 실제로 코드가 변경되었을 때 서버가 자동으로 다시 시작되도록 한다.

  • 이를 위해 package-lock.json을 삭제한다.

  • packages.json에 추가한다.

    "devDependencies": {
      "nodemon":"^2.0.4"
      // nodemon은 JavaScript 파일이 변경되면 노드 서버를 재시작 해준다.
    }

    그리고 nodemon을 활용하기 위해 그 실행을 보장해야하는데, scripts 섹션에 start 스크립트를 추가한다.

    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "start": "nodemon app.js" // add
    },

    Dockerfile도 수정한다.

    FROM node
    WORKDIR	/app
    COPY package.json .
    RUN npm install
    COPY . .
    EXPOSE 80
    CMD ["npm", "start"]
  1. 이제 리빌드된 이미지를 기반으로 컨테이너를 재시작한다.

  2. MongoDB 접속 URI에 하드코딩한 사용자와 비밀번호를 Dockerfile에서 ENV 명령으로 환경변수로 추가한다.

    FROM node
    WORKDIR	/app
    COPY package.json .
    RUN npm install
    COPY . .
    EXPOSE 80
    
    # add
    ENV MONGODB_USERNAME=root 
    ENV MONGODB_PASSWORD=secret 
    
    CMD ["npm", "start"]

    그리고 코드도 수정한다.

    `(백틱) 안에 쓴다.
    `mongodb://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@mongodb:27017/course-goals?authSource=admin`
  3. .dockerignore 파일을 생성하여 node_modules, Dockerfile, .git 파일을 추가한다.
    .dockerignore 파일에 추가하면 기존 컨테이너에 복사되지 않는 파일/폴더 들이다.
    이렇게 하면 컨테이너 내부에 이미 설치한 모든 종속성을 불필요하게 다시 복사하지 않는다.

🎈(바인드 마운트로) React 컨테이너에 대한 라이브 소스 코드 업데이트하기

  • $] docker run으로 실행 할 때 -v 를 설정하여 컨테이너의 /app/src 폴더에 바인딩 한다.
  • 프론트엔드는 이미 변경사항을 적용하고 애플리케이션을 다시 빌드하도록 React로 구성되어 있으므로 nodemon은 필요하지 않다.
  • .dockerignore 파일을 생성하여 node_modules, Dockerfile, .git 파일을 추가한다.

  • 개발용이므로 배포시 문제가 생길 수 있다.
  • 이는 뒤의 다른 섹션에서 다룬다.

프론트랑 백 나눠서 서버 올리고 그걸 어떻게 연동시키고 디비는 어떻게 연결하는지 너무 궁금했는데 이번 강의를 통해 알게 되어서 너무 즐겁다 ^v^

profile
내 시행착오 모음집

0개의 댓글