개요
- 도커 스웜(Docker Swarm)을 사용하여 포토 형식의 커뮤니티 서비스 배포
이번 게시글을 통해 기존에 도커 컴포즈로 배포했을 때와 어떤 차이점이 있는지,
컨테이너간 파일 시스템(FS)을 공유를 어떻게 할건지에 대해 기록해보려고 한다.
도커 스웜에서 더 이상 하기 어려운 것
도커 컴포즈에서는 volumes
를 사용해 컨테이너 간 데이터를 호스트에서 간단하게 관리할 수 있었다.
도커 스웜에서도 그와 비슷한 방식으로 데이터 관리가 가능하다.
그러나 도커 스웜에서는 도커 컴포즈의 volumes
를 관리하는 방법에 약간의 차이가 있다.
그럼 바로 볼륨 관리 방법을 살펴보도록 하자.
1. 로컬 볼륨 (Local Volume)
- 기본적으로
docker volume create
를 사용하여 로컬 볼륨을 생성하고, 이를 서비스에 연결한다. - 그러나 로컬 볼륨은 특정 노드 에서만 사용이 가능하다.
- 스웜 클러스터에서 여러 노드가 있다고 가정할 때, 로컬 볼륨은 해당 노드에만 연결되므로 다른 노드에서 해당 데이터 접근이 불가능하기 때문이다.
2. 네트워크 파일 시스템 (NFS)와 같은 분산 볼륨
- 여러 노드에서 데이터를 공유해야 한다면 분산 파일 시스템 을 사용해야 한다. 예를 들어 NFS 서버를 사용하여 클러스터의 여러 노드에서 데이터를 공유할 수 있다.
볼륨 플러그인
- 스웜은 볼륨 플러그인을 사용해 여러 가지 외부 스토리지 솔루션을 연동할 수 있다. 예를 들어 GlusterFS, Ceph 등이 있다.
여기서는 NFS가 설치된 분산 볼륨의 대상이 되는 서버를 한 대 설치하고,
노드들과 해당 볼륨 서버간 데이터를 연동하는 작업을 최종적으로 해볼 것이다.
애플리케이션 배포하기
이제 본격적으로 커뮤니티 서비스를 도커 스웜으로 배포해보려고 한다.
컨테이너 간 데이터 공유는 NFS를 사용할 거고
traefik 또한 별도로 띄워줘야 한다.
차근차근 따라해보자.
NFS 패키지 설치
먼저 각 노드별로 nfs-common
을 설치해줘야 한다.
설치 방법은 OS별로 다를 수 있다.
# Debian
sudo apt-get install -y nfs-common
NFS 예제
도커 컴포즈의 volumes에서 NFS
를 어떻게 활용하는지 간단한 예제로 짚고 넘어가보자.
services:
db:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
- "db-data:/var/lib/mysql"
volumes:
db-data:
driver: local
driver_opts:
type: "nfs"
o: "addr=<NFS_서버_주소>,rw,vers=4"
device: ":<NFS의_공유폴더경로>"
사용 방법은 이처럼 매우 간단하다.
Traefik 도커 컴포즈 작성
이제 Traefik 서비스에 대한 도커 컴포즈를 작성하고 이를 stack
으로 배포해보겠다.
여기서 작성하는 코드는 이전에 작성한 스웜(Swarm) 클러스터 Traefik 도입기를 참고하여 만들었다.
docker-compose-traefik.yaml
services:
traefik:
image: traefik
ports:
- target: 80
published: 80
mode: host
protocol: tcp
- target: 443
published: 443
mode: host
protocol: tcp
- 9001:9001
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/log/traefik:/var/log/traefik
- traefik-certificates:/certificates
deploy:
mode: global
update_config:
order: stop-first
placement:
constraints:
- node.labels.traefik-public.traefik-certificates == true
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
- traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
- traefik.http.routers.traefik-public-http.rule=Host(`도메인명입력`)
- traefik.http.routers.traefik-public-http.entrypoints=traefik-ui
- traefik.http.routers.traefik-public-http.middlewares=https-redirect
- traefik.http.routers.traefik-public-https.rule=Host(`도메인명입력`)
- traefik.http.routers.traefik-public-https.entrypoints=traefik-ui
- traefik.http.routers.traefik-public-https.tls=true
- traefik.http.routers.traefik-public-https.service=api@internal
- traefik.http.routers.traefik-public-https.tls.certresolver=le
# - traefik.http.services.traefik-public.loadbalancer.server.port=80
- "traefik.http.middlewares.non-www-to-www.redirectregex.regex=^https?://(?:www\\.)?(.+)"
- "traefik.http.middlewares.non-www-to-www.redirectregex.permanent=true"
- "traefik.http.middlewares.non-www-to-www.redirectregex.replacement=https://www.$${1}"
command:
- --providers.swarm.network=traefik-public
# - --providers.swarm.constraints=Label(`traefik.constraint-label`, 'traefik-public')
- --providers.swarm.exposedbydefault=false
- --providers.swarm.endpoint=unix:///var/run/docker.sock
- --entrypoints.http.address=:80
- --entrypoints.https.address=:443
- --certificatesresolvers.le.acme.email=<이메일주소입력>
- --certificatesresolvers.le.acme.storage=/certificates/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
- --accesslog.bufferingsize=100
- --accesslog.filepath=/var/log/traefik/traefik-access.log
- --accesslog.fields.defaultmode=keep
- --accesslog.fields.names.ClientUsername=drop
- --accesslog.fields.headers.defaultmode=keep
- --accesslog.fields.headers.names.User-Agent=keep
- --accesslog.fields.headers.names.Authorization=drop
- --accesslog.fields.headers.names.Content-Type=keep
networks:
- "traefik-public"
volumes:
traefik-certificates: {}
networks:
traefik-public:
name: traefik-public
external: true
옵션 설명은 따로 하지 않겠다.
애플리케이션 도커 컴포즈 작성
이번에는 애플리케이션에 대한 도커 컴포즈 작성을 해보겠다.
docker-compose-app.yaml
services:
nginx:
image: yshrim12/pinterest-nginx:latest
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.nginx.rule=Host(`palworldgall.shop`)
- traefik.http.routers.nginx-https.rule=Host(`palworldgall.shop`)
- traefik.http.routers.nginx.entrypoints=http
- traefik.http.routers.nginx.middlewares=https-redirect
- traefik.http.routers.nginx-https.entrypoints=https
- traefik.http.routers.nginx-https.tls=true
- traefik.http.routers.nginx-https.tls.certresolver=le
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
- traefik.http.services.nginx-https.loadbalancer.server.port=80
volumes:
- "swarm_static_volume:/usr/src/app/staticfiles"
- "swarm_media_volume:/usr/src/app/media"
ports:
- "8989:80"
networks:
- "traefik-public"
web:
image: yshrim12/pinterest-app:latest
command: gunicorn pragmatic.wsgi:application --bind 0.0.0.0:8000 --reload
volumes:
- "swarm_static_volume:/usr/src/app/staticfiles"
- "swarm_media_volume:/usr/src/app/media"
environment:
DEBUG: 1
SECRET_KEY: django-insecure-sen@o8cq)drd9o+3&^w366*+_f(7z*0=13out2(c=jrd324lvr
DJANGO_ALLOWED_HOSTS: localhost 127.0.0.1 192.168.219.193 palworldgall.shop www.palworldgall.shop [::1]
SQL_ENGINE: django.db.backends.postgresql
SQL_DATABASE: pragmatic
SQL_USER: pragmatic_user
SQL_PASSWORD: pragmatic_pass
SQL_HOST: db
SQL_PORT: 5432
networks:
- "traefik-public"
db:
image: postgres:12.0-alpine
environment:
POSTGRES_USER: pragmatic_user
POSTGRES_PASSWORD: pragmatic_pass
POSTGRES_DB: pragmatic
volumes:
- "swarm_postgres_data:/var/lib/postgresql/data"
networks:
- "traefik-public"
volumes:
swarm_postgres_data:
driver: local
driver_opts:
type: "nfs"
o: "addr=192.168.219.179,rw,vers=4"
device: ":/nfs/share/db"
swarm_static_volume:
driver: local
driver_opts:
type: "nfs"
o: "addr=192.168.219.179,rw,vers=4"
device: ":/nfs/share/web/staticfiles"
swarm_media_volume:
driver: local
driver_opts:
type: "nfs"
o: "addr=192.168.219.179,rw,vers=4"
device: ":/nfs/share/web/media"
networks:
traefik-public:
name: traefik-public
external: true
나머지는 일반적으로 애플리케이션 배포할 때 작성하는 코드와 별반 다르지 않다.
nginx
서비스 보면 labels
가 붙어있는데 이는 스웜(Swarm) 클러스터 Traefik 도입기에 자세한 설명이 기록되어 있다.
stack 실행
지금까지 작성한 YAML을 stack
명령어로 실행해주면 된다.
docker stack deploy -c docker-compose-traefik.yaml
docker stack deploy -c docker-compose-app.yaml
접속 테스트
도메인명으로 접근 후 HTTPS 및 서비스가 정상 로드되는지 확인해보자.
트러블슈팅
[nginx] failed (Permission denied)
stack
으로 서비스 배포 후 컨테이너들은 정상 동작하였으나
서비스 접속 시 정적 파일이 제대로 제공되지 않는 문제가 발생하였다.
문제를 파악하기 위해 먼저 서비스 로그를 살펴봐야 했다.
# 서비스 ID 획득 위해 아래 명령어 실행
docker service ls
# nginx 서비스 로그 조회
docker service logs 3kzvztexw8zb
경로에 권한이 없어 접근하지 못하는 것으로 판단이 든다.
먼저 배포된 nginx의 컨테이너 내부에서 정적 파일이 공유된 경로 /usr/src/app/staticfiles
의 폴더 권한 및 파일들의 권한이 어떻게 되어있는지 파악해야 했다.
docker exec -it <nginx_container_id> ls -lh /usr/src/app/
drwxr-xr-x 1 1005 1005 4.0K Dec 10 13:54 admin
drwxr-xr-x 1 1005 1005 4.0K Dec 10 13:54 articleapp
-rw-r--r-- 1 1005 1005 2.8K Dec 10 13:54 base.css
drwxr-xr-x 1 1005 1005 4.0K Dec 10 13:54 fonts
drwxr-xr-x 1 1005 1005 4.0K Dec 10 13:54 hitcount
drwxr-xr-x 1 1005 1005 4.0K Dec 10 13:54 image
drwxr-xr-x 1 1005 1005 4.0K Dec 10 13:54 js
drwxr-xr-x 1 1005 1005 4.0K Dec 10 13:54 summernote
흠 .. 권한이 딱봐도 이상해보이지 않는가?
먼저 컨테이너로 마운트된 /usr/src/app/staticfiles
의 권한이 이상한 것이 파악되었기에,
nfs 서버로 이동하여 filestatic
계정의 홈 디렉토리에서 해당 폴더의 권한을 원하는 값으로 수정해주면 된다.
su filestatic
cd ~/
sudo chown -R 0:0 .
여기서는 0(=root)
값으로 변경해주었다.
하지만 그럼에도 Permission 에러 로그가 멈추지 않았다.
해결하기
- nginx에서 root로 설정한 디렉토리 경로의 권한 확인
- 해당 유저 그룹을
/etc/nginx/nginx.conf
와 일치시키기.
본인의 경우에는 해당 경로에 대한 권한 유저가 root
로 되어 있었고
/etc/nginx/nginx.conf
파일에서 user
를 다음과 같이 변경하여 해결하였다.
#user nginx;
user root;