Profile picture

기존에 구축한 가상 서버를 도커 기반으로 구축하는 방법

JaehyoJJAng2024년 12월 25일

개요

현대 IT 인프라 환겨에서는 일관성, 효율성, 그리고 관리 편의성을 높이기 위해 다양한 배포 방식이 사용됩니다.

전통적으로는 물리 서버나 가상화 소프트웨어(VMware 등)을 통해 각 서버에 OS를 설치하고,

필요한 패키지 및 서비스를 설치하는 방식(Native 설치)이 주로 사용되어 왔죠.


제가 이전에 가상 인프라를 구축할 때도 가상화 소프트웨어인 VMware를 사용하여

서버들을 생성하고 서버에 할당할 리소스들을 직접 프로그램을 통해 제어했었죠!
가상 인프라 네트워크 망 구축 및 서버 구축


물리 서버를 두거나 하이퍼바이저를 사용하여 OS를 직접 설치 하는 방법의 경우에는 다음과 같은 이점이 있었어요.

직접적인 하드웨어 접근 및 성능 최적화

  • 물리 서버나 전용 가상 머신은 CPU, 메모리, 스토리지 등 하드웨어 자원에 직접 접근이 가능해 성능 최적화에 유리

운영체제 전체 제어

  • 운영체제의 커널부터 시스템 라이브러리까지 직접 관리할 수 있어 특정 환경에 맞춰 최적의 설정 적용하기 용이함.

그 외에도 많은 이점들이 있는데요, 근데 그만큼 단점 또한 존재했었죠.


복잡한 설치 및 구성

  • 각 서버마다 OS 설치, 패키지 업데이트, 서비스 구성 등의 작업이 수작업 또는 스크립트를 통해 이루어져야 함. 그로 인해 관리 복잡성을 증가 시킴

환경 불일치 문제

  • 동일한 서비스를 여러 서버에 설치할 때 OS 버전, 패키지 버전, 설정 등이 다를 경우 환경 일관성을 유지하기 어려움.


그래서 저는 이러한 Native 방식을 버리고

트렌드에 맞춰서 도커 기반으로 서버를 다시 구축해보려고 합니다.


도커야 워낙 유명해서 서버쪽 다루시는 분들은 사용을 안해보신 분들이 거의 없을거라고 생각합니다.


도커는 전통적인 배포 방식에 비해 엄청난 이점을 가지고 있는데요


일관된 배포 환경

:도커 이미지는 빌드 시점에 정의되므로, 동일한 이미지를 여러 환경(개발, 테스트, 운영)에 걸쳐 사용할 수 있죠.

서버쪽에 유명한 밈이 있는데, 혹시 아시는 분들이 있으려나요?
image
도커는 이러한 "Works on my machine" 문제를 줄여줍니다.


경량화된 빠른 시작

:또한 컨테이너는 운영체제의 커널을 공유하기 때문에 가상 머신보다 훨씬 가볍고 빠르게 실행됩니다.

이는 당연히 리소스 활용 효율성과 확장성 측면에서 매우 유리하겠죠.


버전 관리 및 롤백 용이

:도커 이미지 자체에 버전 태깅을 할 수 있으므로, 새로운 버전 배포 시 문제가 발생하면 바로 이전 버전으로 롤백하면 돼요!


그럼 이제부터 이전에 구축한 서버 설정들을 도커로 전환해보겠습니다!


컨테이너 구성도

구성도는 네트워크 구축와 비슷하게 구성할 것입니다.


근데 이제 깔끔하게 웹 서버, DB 서버, NFS 서버로만 컨테이너를 생성해줄거고

다음과 같이 표로 정리하겠습니다.

컨테이너 역할 서비스 DMZ IP Local IP
cent1 WEB NGINX 172.18.1.91 10.18.1.91
cent2 DATABASE MariaDB 11.7 172.18.1.92 10.18.1.92
cent3 NFS Storage NFS 172.18.1.93 10.18.1.93

각 컨테이너는 실습 편의를 위해 DMZ 영역 IP와 내부망 IP를 모두 갖고 있다는 설정입니다.

  • DMZ IP(172.18.1.0/24): 웹 브라우저 접속 등 외부 서비스용
  • Local IP(10.18.1.0/24): 컨테이너 간 SSH, DB 접속, NFS 마운트 등에 사용함.


배포 코드 작성

이제부터 Dockerfile, docker-compose.yaml, 그리고 컨테이너 init에 필요한 쉘 스크립트(init.sh) 및 SSH 설정을 비롯해 그 외 각 서버에 필요한 설정 파일들을 작성해보도록 하겠습니다!


폴더 생성

설정 파일들을 모아놓을 디렉토리를 생성해주도록 하겠습니다.

해당 디렉토리는 도커 컴포즈에서 volume 속성을 통해 컨테이너와 매핑시켜줄겁니다.

mkdir -p ~/vws-docker/SRC

SSH 설정

컨테이너간 접근을 허용하기 위해서 SSH 설정 파일을 미리 작성해주도록 하겠습니다.


먼저 ~/vws-docker/SRC 디렉토리에 ssh라는 디렉토리를 새로 생성해줍시다.

mkdir ~/vws-docker/SRC/ssh
cd ~/vws-docker/SRC/ssh

호스트에서 ssh-keygen -t rsa -b 4096 명령을 실행해서 rsa 키를 ~/vws-docker/SRC/ssh에 생성해주도록 할게요.

ssh-keygen -t rsa -b 4096 -f ~/vws-docker/SRC/ssh/id_rsa

그러면 다음과 같이 id_rsa, id_rsa.pub 파일 두 개가 생길겁니다.

여기서 id_rsa.pub의 내용을 authorized_keys 라는 파일에 복사해줄겁니다.

cat id_rsa.pub | tee authorized_keys

그리고 마지막으로 config 파일도 작성해줄게요.

Host cent1
HostKeyAlgorithms +ssh-dss
HostName 172.18.1.91
User root
IdentityFile /root/.ssh/id_rsa
Port 22
ServerAliveInterval 30

Host cent2
HostKeyAlgorithms +ssh-dss
HostName 172.18.1.92
User root
IdentityFile /root/.ssh/id_rsa
Port 22
ServerAliveInterval 30

Host cent3
HostKeyAlgorithms +ssh-dss
HostName 172.18.1.93
User root
IdentityFile /root/.ssh/id_rsa
Port 22
ServerAliveInterval 30

모두 작성했다면 id_rsa.pub 파일은 삭제해도 좋습니다!


이렇게해서 각 컨테이너에 공통으로 들어갈 설정 파일들은 모두 작성이 끝났습니다!


이제 각 서버별로 도커 파일 및 서버에 필요한 설정 파일들을 마저 작성하고,

init.sh 스크립트 작성 및 도커 컴포즈를 작성한 후, 테스트까지해서 마무리해보겠습니다.


웹 서버

nginx.conf

웹 서버 컨테이너를 구동하기 위해 필요한 nginx.conf를 먼저 작성해주도록 하겠습니다!

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/VWS.error.log warn;
pid        /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/VWS.access.log  main;

    sendfile        on;
    tcp_nopush      on;
    keepalive_timeout 65;

    server {
        listen       80;
        server_name  vws.tmpcompany.com;

        root   /usr/share/nginx/html/www;
        index  index.html index.htm;

        location / {
            try_files $uri $uri/ =404;
        }

        error_page 404 /404.html;
            location = /404.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
}

Dockerfile.cent1

그리고 웹 서버용 도커파일도 작성해주도록 하겠습니다.

FROM rockylinux:8

ENV container docker

RUN dnf -y install epel-release && \
    dnf makecache && \
    dnf -y install \
        dnf-utils nano vim git net-tools tar binutils \
        psmisc wget sysstat dialog stress nginx \
        procps-ng iproute util-linux tree which man less bash-completion \
        openssh-server openssh-clients nfs-utils && \
    dnf clean all

VOLUME [ "/sys/fs/cgroup" ]
STOPSIGNAL SIGRTMIN+3

COPY SHELL/init.sh ./init.sh

RUN chmod +x ./init.sh

CMD ["./init.sh"]

이미지 빌드의 경우 도커 컴포즈를 사용해서 배포할 것이기에 따로 진행해주지 않겠습니다!


DB 서버

데이터베이스 서버의 경우에는 따로 필요한 설정 파일이 없으므로,

바로 Dockerfile을 작성해보도록 하겠습니다.

Dockerfile.cent2

FROM rockylinux:8

ENV container docker

RUN dnf -y install epel-release && \
    dnf makecache && \
    curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash && \
    dnf -y install \
      dnf-utils nano vim git net-tools tar binutils \
      psmisc wget sysstat dialog \
      boost-program-options stress \
      procps-ng iproute util-linux tree which man less bash-completion \
      openssh-server openssh-clients nfs-utils \
      MariaDB-server MariaDB-client MariaDB-backup && \
    dnf clean all

VOLUME [ "/sys/fs/cgroup" ]
STOPSIGNAL SIGRTMIN+3

COPY SHELL/init.sh /init.sh
RUN chmod +x /init.sh

CMD ["/init.sh"]

NFS 서버

exports

NFS 서버에서는 exports nfs 설정 파일을 작성해주도록 하겠습니다.

/nfs	10.18.1.0/24(rw,sync,no_all_squash,no_root_squash)

내부망 IP(10.18.1.0/24)만 접속할 수 있도록 설정해줬습니다.


Dockerfile.cent3

FROM rockylinux:8

ENV container docker

RUN dnf -y install epel-release && \
    dnf makecache && \
    dnf -y install \
      dnf-utils nano vim git net-tools tar binutils \
      psmisc wget sysstat dialog \
      stress \
      procps-ng iproute util-linux tree which man less bash-completion \
      openssh-server openssh-clients nfs-utils && \
    dnf clean all

VOLUME [ "/sys/fs/cgroup" ]
STOPSIGNAL SIGRTMIN+3

COPY SHELL/init.sh /init.sh
RUN chmod +x /init.sh

CMD ["/init.sh"]

init.sh 스크립트 작성

이제 init.sh 스크립트를 작성해보도록 하겠습니다.


해당 스크립트는 각 컨테이너가 생성될 때 실행될 스크립트라고 보시면 됩니다.

초기 설정을 잡아주는 용도라고 보시면 될 것 같아요!


바로 코드 작성해볼게요!

#!/bin/bash

set -euo pipefail

# 초기 변수 할당
HOST=$(hostname)
WORK="/labdata"
SSH="$WORK/ssh"
NGINX_CONF="/etc/nginx/conf.d/default.conf"
MDB_CONF="/etc/my.cnf.d/server.cnf"
NFS_EXPORTS="/etc/exports"

# /etc/hosts 등록
echo "hosts에 각 서버를 등록합니다."
cat << EOF > /etc/hosts
127.0.0.1   localhost
::1         localhost
10.18.1.91  cent1
10.18.1.92  cent2
10.18.1.93  cent3
EOF

# fstab 등록 (컨테이너 실행 시 NFS 자동 마운트)
echo "fstab에 nfs 정보 등록 중 .."
echo "10.18.1.91:/nfs   /mnt    nfs     nfsvers=3,tcp,nolock,noauto  0  0" >> /etc/fstab

# .bashrc 환경 설정
{
  echo "HISTTIMEFORMAT='## %Y-%m-%d %T ## '"
  echo "alias ls='ls --color=tty'"
  echo "alias vi='vim'"
  echo "alias ssh='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '"
} >> /root/.bashrc


# SSH 설정
mkdir -p /root/.ssh
cp -rfp "$SSH"/* /root/.ssh
chmod 600 "$SSH/id_rsa" || true
chmod 644 "$SSH/authorized_keys" || true
chmod 644 "$SSH/config" || true

# login 메시지 설정
cat << 'EOF' >> /root/.bashrc

echo ""
echo "---------------------------------------------------------------------"
echo "이 서버는 학습용 가상회사인 Virtual Web Service Company의 서버입니다."
echo ""
echo "실습의 용이성을 위해 selinux와 iptables를 off 했습니다."
echo ""
echo "root 유저로 접속했습니다. 이대로 실습을 진행해 주세요."
echo "---------------------------------------------------------------------"
echo ""
EOF

# 서버 역할별 처리
case ${HOST} in
  cent1)
    echo "[cent1] 웹 서버 준비 중..."
  
    # nginx 전체 설정 덮어쓰기
    if [ -e ${WORK}/nginx.conf ]; then
      echo "[cent1] nginx.conf 전체 덮어쓰기..."
      cp -fvp ${WORK}/nginx.conf /etc/nginx/nginx.conf
    else
      echo "[cent1] nginx.conf 파일이 존재하지 않습니다."
    fi
  
    # 웹 소스 압축 해제
    if [ -e ${WORK}/web_src.tgz ]; then
      echo "[cent1] 웹 소스 복사 중..."
      mkdir -p /usr/share/nginx/html
      tar xzf ${WORK}/web_src.tgz -C /usr/share/nginx/html --strip-components=1
    else
      echo "[cent1] web_src.tgz 파일이 존재하지 않습니다."
    fi
  
    # nginx 시작
    echo "[cent1] nginx 실행"
    /usr/sbin/nginx
  
    ;;

  cent2)
    echo "[cent2] DB 서버 준비 중..."
    mysqld_safe --datadir=/var/lib/mysql &
    sleep 5
    if [ ! -d "${WORK}/test_db" ]; then
      git clone https://github.com/t2sc0m/test_db.git ${WORK}/test_db
    fi
    mysql -uroot < ${WORK}/test_db/employees.sql || true
    ;;

  cent3)
    echo "[cent3] NFS 서버 준비 중..."

    # /nfs 디렉터리 생성 + 권한 설정
    mkdir -p /nfs
    chmod 755 /nfs

    # /etc/exports 복사
    [ -e ${WORK}/exports ] && cp -fvp ${WORK}/exports ${NFS_EXPORTS}

    # NFS 관련 데몬 실행
    rpcbind
    exportfs -a
    /usr/sbin/rpc.nfsd
    /usr/sbin/rpc.mountd -F &
    ;;

  *)
    echo "⚠️ 알 수 없는 HOST: ${HOST}"
    ;;
esac

# 공통 SSH 설정
if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then
  echo "[${HOST}] SSH 호스트 키 생성 중..."
  ssh-keygen -A
fi

# SSH 데몬 실행
/usr/sbin/sshd

# 컨테이너 종료 방지
exec tail -f /dev/null

Docker Compose

이제 마지막으로 도커 컴포즈를 작성해보겠습니다!

services:
  cent1:
    build:
      context: .
      dockerfile: Dockerfile.cent1
    container_name: cent1
    hostname: cent1
    networks:
      dmz_net:
        ipv4_address: 172.18.1.91
      local_net:
        ipv4_address: 10.18.1.91
    ports:
      - "80:80"
    volumes:
      - ./SRC:/labdata
    privileged: true
    init: true
    tty: true
    stdin_open: true

  cent2:
    build:
      context: .
      dockerfile: Dockerfile.cent2
    container_name: cent2
    hostname: cent2
    networks:
      dmz_net:
        ipv4_address: 172.18.1.92
      local_net:
        ipv4_address: 10.18.1.92
    volumes:
      - ./SRC:/labdata
    privileged: true
    init: true
    tty: true
    stdin_open: true

  cent3:
    build:
      context: .
      dockerfile: Dockerfile.cent3
    container_name: cent3
    hostname: cent3
    networks:
      dmz_net:
        ipv4_address: 172.18.1.93
      local_net:
        ipv4_address: 10.18.1.93
    volumes:
      - ./SRC:/labdata
      - nfsdata:/nfs
    privileged: true
    init: true
    tty: true
    stdin_open: true

volumes:
  nfsdata:

networks:
  dmz_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.18.1.0/24
  local_net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.18.1.0/24

네트워크 설정과 ipam 옵션

– 여러 컨테이너가 서로 통신할 수 있도록 bridge 네트워크를 생성하고, 각 네트워크에 대해 별도의 서브넷을 지정합니다. – ipam 옵션을 통해 네트워크 내에서 사용할 IP 서브넷을 명시적으로 정의합니다. – 예를 들어, dmz_net172.18.1.0/24 서브넷, local_net10.18.1.0/24 서브넷으로 설정되어 있습니다.


컨테이너 관련 옵션

  • init: true – 컨테이너 내에서 좀비 프로세스(zombie process)를 처리하기 위한 간단한 init 프로세스(tini 등)를 실행합니다. – 컨테이너가 종료할 때 프로세스 정리 문제를 줄여줍니다.

테스트

image


Loading script...