어제 docker에 대한 간략한 글을 써봤다.
간단히 설명하고 'docker는 멀티 서비스다!'하고 끝냈다.
그러니 이번엔 써보자
준비합시다
서치는 어렵지 않으니 잘 설치하고
docker version
docker info
둘다 잘 나오면 OK다.
멀티 서비스를 써보자
먼저 폴더부터 간단하게 만들자 설명하기 쉽게 윈도우 기준 powershell 기반으로 설명을 진행하겠다.
cd 원하는경로
mkdir multi-demo\nginx,multi-demo\api -Force
cd multi-demo
웹서버 준비
생성한 multi-demo 경로에 보면 api 라는 폴더가 있는데 여기에 아주 간단한 API 파일을 만들자
그냥 붙여넣으면 된다.
api\app.js
const express = require("express"); // 대충 백엔드 동작하는 애임
const { createClient } = require("redis"); // 대충 데이터베이스 연결하려고 가져옴
const app = express();
const port = process.env.PORT || 8080; // 기본 웹 포트 8080에 오픈
const redis = createClient({ url: `redis://redis:6379` }); //기본 포트 6379
redis.on("error", console.error);
(async () => { try { await redis.connect(); } catch(e) { console.error(e); } })();
app.get("/health", (_,res)=>res.json({ok:true})); // 웹 포트 열고 /health로 접근하면 서버 살아있는지 응답해주겠다는 것
app.get("/api/ping", async (_, res) => { // /api/ping 호출되면 Redis에 저장된 숫자 값 1씩 증가시기고 메시지랑 같이 반환하겠다는 것
const n = await redis.incr("ping");
res.json({ msg: "pong", count: n });
});
app.listen(port, () => console.log("API on :" + port)); // 이거 쓴 순간부터 우리 서버 영업합니다.
그 다음은 DockerFile을 만들어보자
어제 설명 안하고 Docker compose를 설명한거 같지만, 간단히 도커에서 단일 컨테이너 만드는 설계도 같은거다.
이후에 Docker Compose 기반으로 다중 컨테이너 애플리케이션을 정의할 때 api 단일 컨테이너를 생성하는 방식을 정의하는 용도로 쓸 생각이다.
api\Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY app.js .
RUN npm init -y && npm install express redis
EXPOSE 8080
CMD ["node","app.js"]
dockerfile에 주석 달면 별로 안 예뻐서 별도로 설명을 진행하겠다.
FROM 이미지명
기반 이미지를 뭘 쓸지를 물어보는거
node:20-alpine은 Node.js 버전 20을 쓰겠다는 거고 대충 백엔드 프레임워크다. -alpine은 가볍고 작은 Linux 버전인데 컨테이너 이미지 작게 하려고 쓰는거다.즉 아까
app.js에서 서버 돌린다고 express 가져다 썻는데 그거 동작하기 위한 언어 프레임워크가 부착된 작은 리눅스 버전 설치하겠다는 뜻이다.
WORKDIR /app
컨테이너 내부 어디서 작업하세요? /app 에서요
우리가 만든 app 폴더랑 다른거다. 말 그대로 docker 동작시 내부 격리된 공간이 따로 있는데 거기서 app 폴더 쓰겠다는 거다.
COPY app.js .
우리가 만든 app.js 파일을 컨테이너안으로 복사하는거다
아까
WORKDIR /app로 작업 폴더 지정했으니 거기로 복사된다.
RUN npm init -y && npm install express redis
- 프로젝트 공간 init하고 필요한 라이브러리 설치하겠다는 거다. 굳이 알 필요는 없다.
EXPOSE 8080
우리 컨테이너 8080 써요! 하고 광고하는거다.
실제 동작과는 별개라서
docker run으로 컨테이너 동작시-p 8080:8080으로 포트 연결해줘야 한다.
CMD ["node","app.js"]
컨테이너 시작시 기본으로 실행 될 명령어 지정하는거다.
시작하면 CMD 명령으로
node app.js실행하는데, 우리 백엔드 프레임워크인 Node.js 기반으로 웹 서버 시작하는 명령어다.
nginx 준비
웹 서버 준비가 됬다면 다음은 nginx를 준비할꺼다.
nginx가 뭐냐고 물어보신다면 웹 서버다.
아니 방금 웹 서버 준비 했다면서 또 웹 서버 준비해요?
사실 아까 준비한건 웹 서버가 아니다.
복잡해질까봐 그냥 웹 서버라고 했는데 정확히는 웹 애플리케이션 서버(WAS)다.
뭐가 다른거에요?
약간 전문적인곳 가면 대부분 1층이나 처음 맞이하는게 안내 데스크다.
손님 처음 맞이하는 이곳이 바로 WEB 서버며 그 뒤에는 여러분의 목적에 맞추어 전문적인 서비스를 제공해줄 곳이 바로 WAS 서버다.
WEB 서버는 그냥 줘도 문제 없는 정적 콘텐츠 빠르게 주거나 목적에 맞춰 어디로 가면 될지 알려주고
WAS는 복잡한 비즈니스 로직을 실행하고, 계산하고, 데이터 처리하는게 주다.
병원 생각하면 그냥 손소독제 쓰고 싶은 분들이나 놀러오신 노인 분들은 안내데스크 선에서 컷하고
손님은 목적에 알맞게 외과-1진료실, 내과-2진료실 배분해서 부하 안가게 처리하는 용도다.
여러분들의 복잡한 몸뚱아리 가지고 이리저리 처리하는건 뒤에 의사분들이 처리할 것이다.
한번 진행해보자
아까 폴더 만들때 api 말고 nginx폴더도 같이 만들었다 이번엔 거기에 nginx.conf 파일을 만들자
nginx\nginx.conf
# 동작 방식 정의인데 기본 설정으로 가겠다고 비워두기
events {}
http {
server {
listen 80; # 80 포트로 오면 처리, 필요하면 바꾸기
location / { root /usr/share/nginx/html; index index.html; } # 요청된 파일을 어디서 찾을지 설정(index.html는 이후에 만들꺼임)
location /api/ { proxy_pass http://api:8080; } # /api/로 오는 요청 어디로 쏴줄까 정의하는 곳
location /health { return 200 "ok\n"; } # 건강체크
}
}
nginx에서 사용할 기본 제공 html 파일을 만들자
nginx\index.html
<!doctype html>
<html><body>
<h1>Multi Service Demo</h1>
<button id="btn">/api/ping</button>
<pre id="out"></pre>
<script>
document.getElementById('btn').onclick = async ()=>{
const r = await fetch('/api/ping');
document.getElementById('out').textContent = JSON.stringify(await r.json(), null, 2);
};
</script>
</body></html>
별건 없고 버튼 누르면 /api/ping으로 통신 한번 보내보겠다는 거다.
통신 돌아오면 알아서 보일거다.
드디어 docker
대충 서비스 준비 됬으면 이제 멀티 서비스를 운용할 docker-compose 파일을 작성해보자
docker-compose.yml
name: multi-demo # 간단한 이름
networks:
appnet: {} # appnet 이라는 이름의 가상 네트워크를 생성한다. 이 네트워크에 속한 컨테이너들은 서로 서비스 이름으로 통신 가능하다.
volumes:
web_static: {} # web_static 이라는 이름의 볼륨을 생성한다. 해당 볼륨을 통해 저장된 데이터는 컨테이너가 종료되거나 삭제되도 유지된다.
services: # 서비스 정의 영역으로 redis(데이터베이스), api(WAS), web(WEB) 각각의 서비스를 정의한다.
redis:
image: redis:7-alpine # redis는 DB고, alpine은 아까 말한대로 경량화 된 리눅스 OS 환경
networks: [appnet]
api:
build: ./api # ./api 폴더에 있는 Dockerfile을 사용해 이미지를 직접 빌드한다는 뜻이다.
environment:
PORT: "8080" # 컨테이너 내부 환경 변수 정의하는건데 아까 app.js에서 process.env.PORT || 8080 여기서 환경 변수 끌어다 쓸 꺼다.
depends_on: [redis] # api 서비스 시작되기 전에 redis 서비스 먼저 시작되야한다고 순서 강제하는 거다.
networks: [appnet]
healthcheck: # 컨테이너 실행하고 잘 동작하는지 체크하는 용도로 정의하는 거
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"] # http://localhost:8080/health 잘 호출되고 리턴 200 돌아오면 ok
interval: 5s
timeout: 3s
retries: 10
web:
image: nginx:alpine
depends_on:
api: # api 서비스에 의존해서 WAS 먼저 잘 도는지 확인
condition: service_healthy # api에 healthcheck 정의해놨는데 그거 성공했으면 web 서비스 시작하겠다는 설정
ports:
- "8088:80" # 사용자 컴퓨터(호스트) 8088번 포트로 들어온 요청을 80포트로 전달, 즉 우리는 http://localhost:8088로 접근 가능
volumes: # 우리가 만든 설정 파일들과 정적 호스팅하기 위한 볼륨을 연결하는 용
- web_static:/usr/share/nginx/html
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/index.html:/usr/share/nginx/html/index.html:ro
networks: [appnet]
명령을 보면 공통적으로 appnet이라는 가상 네트워크에 속해서 각자 서비스 명으로 통신을 진행한다.
외부(우리 PC) 호스트와 연결되는 포트는 web에서 8088:80 으로 연결되는 경로뿐인데, 어차피 나머지 서비스는 자기들끼리 잘 통신하면 되니깐 굳이 연결할 필요가 없다.
혹시라도 docker-compose 파일 위치를 물어보신다면 우리가 만든 multi-demo 폴더에다가 두고 돌리면 된다.
진짜로 구동해보자.
위와 같이 설정이 다 됬다면 이제 powershell에서 다음 명령을 구동하면 된다.
docker compose up -d --build # 우리가 열심히 만든 서비스 빌드해줘
docker compose ps # 잘 돌고 있니 체크
curl http://localhost:8088/health # 서비스 헬스체크 호출해보자!
curl http://localhost:8088/api/ping # ping 메시지 돌아오는지 체크
응답이 잘 돌아오면
http://localhost:8088/으로도 한번 접근해서 버튼 한번 눌러보자
메시지에 'pong' 잘 돌아오고 count도 올라가면
WEB - WAS - DB 까지의 Docker 기반의 멀티 서비스 데모가 완료된 것이다.
좀더 복잡한 개념을 담으면 좋겠지만 기술을 글로 써낸다는 것은 제법 시간이 걸리는 일이기에
나중에 기회가 된다면 좀더 복잡한 개념을 다뤄보고자 한다.