Profile picture

[Docker Swarm] 실전 연습: 블로그 앱 배포하기 - Feat. Traefik, NFS

JaehyoJJAng2024년 06월 21일

개요

  • 도커 스웜(Docker Swarm)을 사용하여 포토 형식의 커뮤니티 서비스 배포

image


이번 게시글을 통해 기존에 도커 컴포즈로 배포했을 때와 어떤 차이점이 있는지,

컨테이너간 파일 시스템(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 및 서비스가 정상 로드되는지 확인해보자.
image


트러블슈팅

[nginx] failed (Permission denied)

stack으로 서비스 배포 후 컨테이너들은 정상 동작하였으나

서비스 접속 시 정적 파일이 제대로 제공되지 않는 문제가 발생하였다.


문제를 파악하기 위해 먼저 서비스 로그를 살펴봐야 했다.

# 서비스 ID 획득 위해 아래 명령어 실행
docker service ls

# nginx 서비스 로그 조회
docker service logs 3kzvztexw8zb

image
경로에 권한이 없어 접근하지 못하는 것으로 판단이 든다.


먼저 배포된 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;

Loading script...