Docker를 이용한 서비스 컨테이너화 작업 - 4 (docker-compose.yml)

최민길(Gale)·2023년 5월 24일
1

Docker & Container

목록 보기
4/4

안녕하세요 오늘은 저번 시간에 만든 Dockerfile을 바탕으로 docker-compose.yml 파일을 통해 여러 컨테이너를 한번에 관리하는 방법에 대해 알아보겠습니다.

제가 서비스에 띄울 컨테이너는 서비스 기능을 제공하는 api 컨테이너, 알림, 메일 발송 등을 담당하는 utils 컨테이너, 그리고 redis 컨테이너 이렇게 3개로 구성됩니다. 여기에 https 설정을 위한 certbot 컨테이너도 사용할 예정입니다.

docker-compose.yml 파일을 만들기 전에 먼저 certbot ssl 인증 방식에 대해 알아보아야 합니다. SSL 인증서 발급 방식에는 크게 4가지가 존재합니다.

  1. Webroot 방식
  2. 웹 서버에 직접 발급받는 방식
  3. Standalone 방식
  4. DNS 방식

2번 방식의 경우는 기존 서버 구축 시 사용한 방법이고, 4번 방식의 경우는 저번 포스팅에서 설명드렸습니다. 각 방식은 한 가지 문제가 있습니다. 우선 2번 방식의 경우 컨테이너를 이용해서 발급받기 위해서는 nginx 관련 파일이 존재하는 경로를 certbot 컨테이너와 라우팅해서 처리를 진행하는데 마운팅해야 하는 경로가 너무 많아지고 복잡해진다는 단점이 있습니다. 4번 방식의 경우 인증서 발급은 무리가 없으나 자동화하여 시스템을 배포할 경우 배포할 때마다 DNS 서버에 들어가 그때 그때 토큰을 TXT 레코드에 등록해야 한다는 단점 때문에 자동화하기 어렵습니다.

그렇다면 1번과 3번 방식은 어떨까요? 우선 3번 Standalone 방식은 Certbot에서 간이 웹 서버를 돌려 인증을 처리하는 방식인데, 이 과정에서 80포트 및 443 포트를 사용하기 때문에 서버에서 HTTP 및 HTTPS 프로토콜을 사용할 경우 서버를 중지 후 인증 완료 후 다시 실행시켜야 한다는 단점이 있습니다. 하지만 1번 Webroot 방식의 경우 Let's Encrypt에서 제공하는 경로를 통해 인증하는 방식으로 서비스 종료를 하지 않아도 된다는 장점이 있습니다. 따라서 저는 서비스 종료가 발생하지 않으며 시스템을 자동화하여 배포하는 데에 장점이 있는 1번 Webroot 방식으로 진행했습니다.

Webroot 방식으로 진행하기 위해선 우선 Dockerfile을 수정해야 합니다. 좀 더 정확히 말하자면 https 설정을 진행할 웹 서버가 존재하는 Dockerfile을 수정하면 됩니다. Webroot 방식으로 진행할 경우 서버 내의 {webroot-path}/.well-known/acme-challenge 내부에 파일을 생성합니다. 따라서 해당 경로를 nginx 설정 파일에 추가해주면 됩니다. https 설정이 진행되기 전에 파일을 생성하므로 80 포트 안에 추가해주시면 됩니다. 또한 https 설정이 완료되었을 때 발급받는 키들과 nginx ssl 설정 파일이 존재하는 경로도 같이 설정해줍니다.

server {
	listen 443 ssl;
	listen [::]:443 ssl;
	server_tokens off;

	root /var/api;

	index index.php index.html index.htm index.nginx-debian.html;

	server_name SERVER_NAME;

	location / {
		try_files $uri $uri/ /index.php?$query_string;
	}

	location ~ \.php$ {
		include snippets/fastcgi-php.conf;
		fastcgi_pass unix:/run/php/php8.0-fpm.sock;
	}
	ssl_certificate /etc/letsencrypt/live/SERVER_NAME/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/SERVER_NAME/privkey.pem;
	include /etc/letsencrypt/options-ssl-nginx.conf;
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

server {
	listen 80;
	listen [::]:80;
	server_name SERVER_NAME;
	server_tokens off;

	#Redirect http to https
	location / {
		return 301 https://$host$request_uri;

		# Prevent Caching of 301 Redirect Request
		expires epoch;
	}

	#Certbot Webroot setting
	location ^~ /.well-known/acme-challenge/ {
		default_type "text/plain";
		root /var/www/certbot;
	}
}

Dockerfile 수정이 완료되었으면 docker-compose.yml 파일을 생성합니다. 현재 프로젝트의 경우 api 컨테이너에서 utils 컨테이너와 redis 컨테이너를 참조해야 하므로 도커 네트워크 설정을 같이 진행합니다. 아래 보시는 것처럼 docker-compose.yml 내부에 네트워크를 생성할 수 있습니다. 이 경우 docker-compose.yml 파일을 삭제하면 같이 없어지게 되고, 내부의 컨테이너끼리 할당한 서브넷으로 통신이 가능합니다. 보시는 것처럼 ipam: config: -subnet: {서브넷 마스크} 형식으로 컨테이너에 특정 서브넷 마스크를 고정하여 할당할 수 있습니다.

...
networks:
  toda:
    driver: bridge
    ipam:
      config:
        - subnet: 172.57.0.0/16
        ...

Certbot 컨테이너는 저번 포스팅에서 진행한 것처럼 letsencrypt 인증서가 발급되는 경로를 마운팅해줍니다. 여기서 주의할 점은 Webroot 방식의 경우 {webroot-path} 내부의 .well-known/acme-challenge에 접근해야 하기 때문에 이 경로 역시 같이 마운팅해줍니다. 또한 인증서 갱신을 자동적으로 진행하기 위해 entrypoint에 갱신 명령을 추가합니다.

services:
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt
      - /var/lib/letsencrypt:/var/lib/letsencrypt
      - {webroot-path}:{webroot-path}
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
    ...

이어서 api, utils, redis 컨테이너를 실행시키는 부분을 작성합니다. build: 부분에 패키지 경로와 Dockerfile 생성 경로를 통해 따로 Dockerfile을 빌드하여 이미지로 만드는 작업을 진행할 필요 없이 한번에 빌드까지 진행해줍니다. 하지만 이미 이미지가 존재할 경우 따로 빌드를 진행하지 않으니 새롭게 빌드하기 위해선 기존 이미지를 제거해주어야 합니다. api 서버의 경우 https 인증을 위해 Certbot 컨테이너와 같은 볼륨을 마운트해줍니다.

# 172.57.0.2 할당
  api:
    networks: 
      toda:
        ipv4_address: 172.57.0.2
    build:
      context: ${PWD}/api
      dockerfile: ${PWD}/api/Dockerfile
      args:
        - SERVER_NAME=${SERVER_NAME}
        - DB_NAME=${DB_NAME}
        - DB_HOST=${DB_HOST}
        - DB_USER=${DB_USER}
        - DB_PW=${DB_PW}
        - FCM_ALARM=172.57.0.3
        - REDIS_HOST=172.57.0.4
    image: toda:php
    container_name: toda_php
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt
      - /var/lib/letsencrypt:/var/lib/letsencrypt
      - /var/www/certbot:/var/www/certbot
    restart: always
    ports:
      - 80:80
      - 443:443

# 172.57.0.3 할당
  utils:
    networks: 
      toda:
        ipv4_address: 172.57.0.3
    build:
      context: ${PWD}/utils
      dockerfile: ${PWD}/utils/Dockerfile
      args:
        - DB_NAME=${DB_NAME}
        - DB_HOST=${DB_HOST}
        - DB_USER=${DB_USER}
        - DB_PW=${DB_PW}
        - MAIL_PW=${MAIL_PW}
        - FCM_TOKEN=${FCM_TOKEN}
    image: toda:utils
    container_name: toda_utils
    restart: always

# 172.57.0.4 할당
  redis:
    networks:
      toda:
        ipv4_address: 172.57.0.4
    image: redis:7.0.11
    container_name: redis

docker-compose.yml 파일이 생성이 완료되었다고 해서 자동 배포가 진행되지 않습니다. SSL 인증을 위한 별도 쉘 스크립트를 실행시켜주어야 하는데 https://github.com/wmnnd/nginx-certbot 패키지 코드를 수정해서 서비스에 맞게 커스텀을 진행하였습니다. 저는 nginx 이미지를 사용하지 않고 자체적으로 nginx 패키지를 설치하여 사용해 해당 컨테이너에서 nginx를 재부팅하는 방식으로 수정하였고, Ubuntu 20.04 버전에서 nginx 설정 파일 위치 등이 다르기 때문에 경로를 재조정하였습니다.

여기서 주의할 점은 기존에 docker-compose.yml 파일을 실행하여 생성한 네트워크가 존재하는 상태에서 deploy.sh와 docker-compose.yml 경로를 바꿔 다시 실행하면 로 생성되는 네트워크가 기존 네트워크와 ip가 겹치게 되어 충돌하게 되어 Pool overlaps with other one on this address space 에러가 발생합니다. 따라서 docker-compose.yml 파일 위치를 변경할 경우 꼭 네트워크를 삭제 후 진행해주어야 하며 쉘 스크립트 작성 시 역슬래시'\'로 커맨드를 연장할 경우 사이에 주석을 넣으면 해당 명령이 정상적으로 실행되지 않으니 이런 사소한 부분도 조심하시면 되겠습니다.

#!/bin/bash
# Ubuntu 20.04, NGINX 1.18 ver 기준
# 원본 : https://github.com/wmnnd/nginx-certbot

# docker-compose 설치 확인
if ! [ -x "$(command -v docker-compose)" ]; then 		# -x $(명령어) : 명령어를 실행 가능하면 참, 그렇지 않으면 거짓 / command -v : 경로 반환

  echo 'Error: docker-compose is not installed.' >&2 		# >&2 : 모든 출력을 강제로 표준 에러로 출력
								# > : 프로그램의 출력을 표준 출력에서 지정한 출력으로 변경
								# & : 표준 입출력 숫자를 인식하게 해주는 설정값. 이게 없을 경우 뒤의 표준 입출력 값을 파일로 인식
								# 2 : 표준 에러
  exit 1
fi


# 참고 : 기존에 생성한 네트워크가 존재하는 상태에서 deploy.sh와 docker-compose.yml 경로를 바꿔 실행하면 Pool overlaps with other one on this address space 에러 발생

# 변수 설정
domains=$SERVER_NAME
rsa_key_size=$RSA_KEY_SIZE
data_path="/etc/letsencrypt"
email="cmg4739@gmail.com" 					# Adding a valid address is strongly recommended
staging=1							# 스테이징 환경 : 운영 환경과 거의 동일한 환경을 만들어놓고 운영 환경으로 이전하기 전 검증하는 환경

# 이미 ssl 인증서가 존재할 경우 다시 발급받을 것인지 확인
if [ -d "$data_path" ]; then # -d file : file이 디렉터리이면 참
								# read -p "command" val: command를 띄운 후 사용자 입력을 받아 val에 저장 
  read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
  if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
    exit
  fi
fi

# options-ssl-nginx.conf, ssl-dhparams.pem 없을 경우 설치
if [ ! -e "$data_path/options-ssl-nginx.conf" ] || [ ! -e "$data_path/ssl-dhparams.pem" ]; then	# -e file : file이 존재하면 참
  echo "### Downloading recommended TLS parameters ..."
  mkdir -p "$data_path"						# mkdir -p : 존재하지 않는 중간의 디렉토리 자동 생성
# curl -s : 에러가 발생해도 출력하지 않음
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/options-ssl-nginx.conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/ssl-dhparams.pem"
  echo
fi

# Certbot 실행시켜 발급받은 키 가져오기
# 주의 : \로 연결한 커맨드 사이에 주석이 존재할 경우 에러 발생
echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/live/$domains"
docker-compose run --rm --entrypoint "\
  openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
    -keyout '$path/privkey.pem' \
    -out '$path/fullchain.pem' \
    -subj '/CN=localhost'" certbot
echo

# Dockerfile의 ENTRYPOINT보다 docker-compose.yml의 entrypoint가 우선 순위가 더 높음
# openssl : SSL, TLS 관련 기능들을 제공하는 오픈 소스 라이브러리
# req : 인증서 서명 요청서 생성하고 처리
# X.509 : 공개 키 기반(PKI)의 ITU-T 표준
# -nodes : 개인 키가 생성될 때 암호화되지 않음
# -newkey arg : 새로운 인증 요청서와 개인 키 생성 / arg : 요청서와 개인 키의 타입
# -days n : -x509 옵션 사용 시 인증 가능 기간 설정
# -keyout filename : 생성된 개인 키를 filename 경로로 생성
# -out filename : 생성된 파일을 filename 경로로 생성
# -subj arg : 입력 요청의 Subject를 지정된 데이터로 대체하고 수정된 요청을 출력
# Subject : 인증서 필수 항목으로 소유자의 정보를 나타냄
# CN : Common Name, SSL 인증서로 보호되는 서버 이름

# 컨테이너 실행
echo "### Starting toda ..."
docker-compose up --force-recreate -d api utils redis		# --force-recreate : 모든 컨테이너를 중지하고 다시 생성
echo

# Certbot 컨테이너의 발급한 키 전부 삭제
echo "### Deleting dummy certificate for $domains ..."
docker-compose run --rm --entrypoint "\
  rm -Rf /etc/letsencrypt/live/$domains && \			# rm -Rf : 최상위 디렉토리 밑에 있는 모든 파일과 디렉토리를 삭제하고 자기 자신까지 삭제
  rm -Rf /etc/letsencrypt/archive/$domains && \
  rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo

# 인증 진행할 도메인이 여러 개일 경우 전부 인증하는 옵션 설정
# -d domain : domain 주소 인증 시 필요 옵션
echo "### Requesting Let's Encrypt certificate for $domains ..."
domain_args=""
for domain in "${domains[@]}"; do				# ${} : parameter substitution, 감싼 부분에 변수 대입
								# array[@] : array 배열의 모든 원소를 가져옴
  domain_args="$domain_args -d $domain"
done

# 유효한 이메일인지 체크
# Select appropriate email arg
case "$email" in
  "") email_arg="--register-unsafely-without-email" ;;		# {String}) : String 조건일 경우 실행
  *) email_arg="--email $email" ;;				# *) : default, 위의 조건 제외 나머지
esac

# 스테이징 옵션 설정
# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi

# Certbot Webroot 방식으로 인증서 생성
docker-compose run --rm --entrypoint "\
  certbot certonly --webroot --webroot-path=/var/www/certbot \
    $staging_arg \
    $email_arg \
    $domain_args \
    --rsa-key-size $rsa_key_size \
    --agree-tos \
    --no-eff-email \
    --force-renewal" certbot
echo

# 웹 서버 재부팅
echo "### Reloading toda:php ..."
docker-compose exec api service nginx restart

이렇게 생성한 sh 파일을 chmod +x 명령을 통해 실행 권한을 부여한 후 환경 변수로 빼놓은 민감한 정보를을 함께 실행해줍니다. 명령을 실행하면 Dockerfile 이미지 빌드부터 SSL 인증서 발급까지 자동으로 진행됩니다. 여기서 주의할 점은 진행할 도메인 주소에 현재 서버의 IP 주소를 꼭 등록해주어야 하며, 테스트 진행 시에는 쉘 스크립트 파일의 staging 옵션을 활성화해서 SSL 인증서 발급 요청이 거부되는 것을 방지합니다. SSL 인증서 발급 시 특정 횟수 이상으로 인증이 실패할 경우 특정 시간 동안 해당 도메인 주소로 인증서를 발급할 수 없어 시간을 절약하고 싶으시다면 테스트 시 staging 옵션을 활성화해주시면 되겠습니다.

ENV="env" bash -c './{위에서 생성한 쉘 스크립트 파일}'

추후 빌드 시간을 줄이기 위한 dockerfile 최적화 및 멀티스테이지 빌드 방식에 대해서 공부할 예정입니다. 또한 쿠버네티스를 이용하여 컨테이너를 관리하는 법과 CD 시스템을 구축하여 push 시 자동 배포하는 시스템도 구축할 예정이니 다음 게시글도 기대해주세요. 감사합니다!

profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글