Docker & K8S Study 2일차 (1)— 네트워크 개념으로 간단한 멀티 서비스를 구축하자

valley
12 min readJul 30, 2023

--

TMI

나에게 인풋과 아웃풋의 조화는 굉장히 중요하다.

강의 & 책 & 문서를 읽고 나면 꼭 직접 해봐야 기억에 남고, 어떤 부분에서 이해가 안됐고, 어려웠는지 알 수 있다.

이번 파트에서는 기존에 스터디 했던 내용을 복습하는 차원이라, 개념적인 부분을 추가적으로 더 다루지는 않는다.

** 이 방법이 best practice 라는게 아니라, “지금까지 배웠던걸로만" 가지고 만드는 방법이다. **

서비스의 구성도를 그리면, 아래와 같다.

예제의 기술 스텍은 아래와 같다.

  • DB는 Mongo
  • Backend Server는 Node
  • Frontend는 React

서비스를 구축해보자.

소스코드 파일은 여기서(Github Repo) 확인할 수 있다.

먼저 몽고 기본 베이스 이미지를 도커 허브에서 다운받아서 컨테이너를 실행시킨다.

docker run --name mongodb --rm -d -p 27017:27017 mongo

해당 명령어를 통해 mongodb 컨테이너가 현재 잘 실행되고 있는지 확인할 수 있다.

docker ps

우선 사용중이지 않은 이미지를 아래 명령어로 한번 제거해주고,

docker image prune -a

백엔드 서버쪽도 도커라이징 해보자.

backend 폴더로 들어가서 Dockerfile을 만들어 준다.

cd backend
FROM node

WORKDIR /app

COPY package.json .

RUN yarn install

COPY . .

EXPOSE 80

CMD ["node", "app.js"]

해당 Dockerfile을 기반으로 goals-node 이름으로 이미지를 만들어준다.

*터미널 실행경로가 현재 backend 이고 Dockerfile의 경로가 backend (같다)여서 .(현재경로 dot) 을 해준 것이다.

docker build -t goals-node .

~ 영차 영차 이미지 생성중 ~

이미지가 잘만들어졌나?

docker images

잘만들어졌다.

이미지가 잘 만들어졌다면, 해당 이미지를 기반으로 container 를 실행시킬 수 있다.

docker run --name goals-backend --rm goals-node

그리고 잘실행되지 않는다.

app.js 를 확인해보자.

mongoose.connect(
'mongodb://localhost:27017/course-goals',
{
useNewUrlParser: true,
useUnifiedTopology: true,
},
(err) => {
if (err) {
console.error('FAILED TO CONNECT TO MONGODB');
console.error(err);
} else {
console.log('CONNECTED TO MONGODB');
app.listen(80);
}
}
);
localhost:27017

이전 포스팅에서 설명했던 대로 localhost 를 하면 현재 컨테이너 안에 IP를 가르키기 때문에, 접근 할 수 없다.

localhost -> host.docker.internal를 사용해보자.

'mongodb://host.docker.internal:27017/course-goals',

코드를 바꾸었으니 다시 빌드를 해준다.

docker build -t goals-node 

그리고 다시 컨테이너를 실행시켜주면?

docker run --name goals-backend --rm goals-node

성공!

자 이제 프론트랑 연결이 잘되었는지 확인 해볼까?

로컬에서 프론트 프로젝트를 실행 시켜준다.

분명 80 포트로 서버를 열었는데 해당 api call이 실패 되고 있다.

도커 컨테이너를 실행할 때 외부 포트를 열어주어야 한다.

로컬 80 port 로 요청 -> 컨테이너 80 port 에서 받을 수 있게 말이다.

컨테이너를 중지하고 다시 -p 옵션을 사용해 포트를 열어준다.

 docker run --name goals-backend -p 80:80 --rm goals-node

확인해보면, 요청이 잘 가고, 응답을 잘 받아오는 것을 확인할 수 있다.

이번엔 리액트도 도커라이징 해보자!

frontend 경로로 이동해서 Dockfile을 만들고, 이미지를 생성한다.

FROM node

WORKDIR /app

COPY package.json .

RUN yarn install

COPY . .

EXPOSE 3000

CMD ["yarn", "start"]
docker build -t goals-react .

~ 영차 영차 이미지 생성중 ~

잘만들어졌다.

docker images

로컬호스트 3000를 열면 컨테이너 안의 3000에 닿을 수 있도록 포트를 열어주면서 컨테이너를 실행시킨다. 이름은 goals-frontend 이고 컨테이너가 중지되면 자동으로 컨테이너를 제거하는 옵션을 준다.

docker run --name goals-frontend --rm -p 3000:3000 goals-react

보면 위 과정이랑 거의 비슷하다. 이제 지겹지 않은가?

좋은 현상이다.

로컬 3000 포트로 접속하면 우리가 컨테이너 안에 띄운 리액트 앱으로 접속할 수 있다.

우선 여기까지 하면 잘 되지만, 현재는 로컬을 통해 모든 컨테이너가 통신하고 있음을 알 수 있다.

이렇게 하면 매번 포트를 명시하고 정해주어야 하기 때문에 이전에 배웠던 한 네트워크 안에 각각의 컨테이너를 넣어주면 좋을 것 같다.

우선 다 종료 시켜 주자.

docker stop goals-frontend goals-backend mongodb

goals-net 이라는 이름의 네트워크를 하나 만들어주자.

docker network create goals-net 

몽고디비가 해당 네트워크를 사용할 수 있도록 명령어를 입력 해준다.

docker run --name mongodb --rm --network goals-net mongo

그림으로 표현하면 Mongo container 는 goals-net 이라는 이름의 네트워크 에 들어온 것이다.

backend 경로에 있는 app.js 의 커넥팅 url을 아래 처럼 바꾸어 준다.

이제 커넥팅의 몽고디비 경로가 로컬에서 컨테이너명으로 바뀐 것이다.

"mongodb://mongodb:27017/course-goals"

다시 빌드하고,

docker build -t goals-node .

node server도 같은 네트워크에 넣어준다

docker run --name goals-backend --rm --network goals-net goals-node

그럼 프론트엔드도 동일하게 로컬 포트를 요청하려는 컨테이너명으로 바꾸어 준다.

localhost -> goals-node

코드가 바뀌었으니 다시 빌드하고,

 docker build -t goals-react .   

네트워크에 추가 해서 컨테이너를 실행시켜 준다.

대신 프론트는 로컬에서 접속을 해야하기 때문에 로컬 포트는 열어둔다.

docker run --name goals-frontend --rm --network goals-net -p 3000:3000 goals-react

3000 포트에 접속 했더니 서버랑 연결이 되지 않는다는 에러가 뜬다.

뭐가 문제일지 잠시 생각해보자.

배웠던 대로라면 localhost -> goals-node 로 경로를 바꾸는게 맞았는데, 에러가 뜬다. 당연히 이렇게 하면 될줄 알았는데 말이다.

다시 생각해보자.

노드 -> 몽고디비와의 통신에서는 노드 런타임에 의해 실행된다.

하지만, 현재 상황에서 React → Node 요청을 실행하는 주최가 브라우저 이다. 브라우저 입장에서는 goals-node 를 알아 들을 수 없다.

그래서 서버 포트를 열고 로컬 브라우저에서 돌리는 React가 접근 할 수 있게 만들자. 80 포트를 열고,

docker run --name goals-backend --rm -d -p 80:80 --network goals-net goals-node

프론트 코드를 다시 goals-node -> localhost 로 변경한 뒤,

이미지를 다시 빌드하고

 docker build -t goals-react .

3000 포트를 열어 다시 실행한다.

docker run --name goals-frontend -p 3000:3000 --rm goals-react

통신이 잘 이루어지는 것을 확인할 수 있다.

목표를 입력하고 Add Goal 버튼을 누르면 디비에 데이터가 추가 되어 표시 된다.

하지만, mongodb 컨테이너를 중지하면, 내가 추가했던 데이터가 날라간다.

이걸 위해 우리는 이전에 배웠던 이름있는 볼륨을 사용하여 해결할 수 있다.

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

이젠 컨테이너를 중지해도 데이터가 유지된다.

그리고 또하나 해결해야 하는게 코드를 수정할 때마다 매번 다시 이미지를 빌드해주고 있는데, 실제 개발 환경에서 이렇게 수정과 빌드를 반복한다면 도커컨테이너에 띄워서 개발할 수가 없다.

이것도 이전 포스팅에 올려두었던 바인드마운트을 활용하면 해결할 수 있다.

하지만 이렇게만 하면 컨테이너가 수정된 버전을 바라보고 있긴 하나, 노드서버의 경우 코드가 변경 되면 항상 서버를 껐다가 다시 켜줘야 한다. 이건 여간 번거로운일이 아니다. nodemon 을 깔아서 코드 변경 시 서버를 자동으로 다시 돌려주는 패키지의 도움을 받는 것이 좋다.

yarn add nodemon -D
docker run --name goals-backend-v [전체경로]:/app --rm -v /app/node_modules -p 80:80 --network goals-net goals-node

그리고 Docker file의 CMD 도 [“node”, “app.js”] -> [“yarn”, “start”] 로 바꾸어준다.

리액트 프로젝트 또한 바인딩 마운트를 활용하여, 코드가 수정되면 화면에 바로 반영되게끔 할 수 있다.

docker run -v [전체경로]/frontend/src:/app/src --name goals-frontend --rm -p 3000:3000 goals-react

마지막으로 리액트 도커파일에 수정해야할 부분이 있다.

FROM node

WORKDIR /app

COPY package.json .

RUN yarn install

COPY . .

EXPOSE 3000

CMD ["yarn", "start"]

여기서 COPY 에 주목해보자 리액트는 매번 종속성을 다운로드 한 후에 모든 파일을 카피한다.

이 말은 node_modules 까지 카피한다는 말이다.

보통 github에는 node_modules를 올리지 않는다는 걸 알 것이다.

굉장히 큰 폴더이기 때문이다. 그래서 종속성 트리관리인 lock 파일만 올리는게 대부분이다.

이때 도커 빌드시에 node_modules를 무시하는 방법은 없을까?

바로 .dockerignore 이 있다.

.dockerignore 파일을 만들고 안에 무시해도 되는 파일들을 정의해주면 된다.

node_modules
.git
Dockerfile

이렇게 최적화를 진행할 수 있다.

어떤가?

이렇게 각자 컨테이너를 띄우고 디비 <-> 서버 <-> 프론트 의 통신방법을 각각 달리하고.. 각 컨테이너 마다 네트워크 설정을 하고 데이터 보존을 위해 각 컨테이너 에 데이터를 유지하기 위해 볼륨을 추가하고..

여기서 몇개의 컨테이너가 더 추가 된다고 가정해보자.

얼마나 머리가 아프고 복잡해질까?

이번 포스팅에서는 “아 다중 컨테이너 관리 하려니 머리아파지네" 소리가 나왔다면 성공적인 연습이였다고 생각이 든다.

— — — — — —

다음 포스팅 때는 드디어!

docker-compose를 활용해서 다중 컨테이너를 다뤄보자!

--

--