Docker를 이용한 서비스 컨테이너화 작업 - 3 (컨테이너 HTTPS 설정)

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

Docker & Container

목록 보기
3/4

안녕하세요 오늘은 컨테이너에 https 설정을 진행하고 이에 맞게 Dockerfile을 수정 및 컨테이너를 실행하는 방법에 대해 포스팅해보겠습니다.

https에 대해 간단히 알아보자면, https는 http에 보안 취약점을 해결하기 위해 탄생한 프로토콜입니다. http와 기본 사용법은 동일하지만 통신 간에 모든 내용이 암호화되어 전송된다는 차이점이 있습니다. SSL 또는 TLS 프로토콜을 이용해 데이터를 암호화하며 서버와 클라이언트는 발급받은 키를 이용해 복호화하여 데이터를 통신합니다.

암호화 방식은 공개 키 방식과 대칭 키 방식을 함께 사용합니다. 공개 키 방식의 경우 하나의 키를 공개 키로 공개하고, 서버 자신은 공개 키와 개인 키를 가지고 있어 개인 키는 공개되지 않습니다. 대칭 키 방식의 경우 암호화와 복호화 시 같은 암호 키를 사용하는 방식입니다. 클라이언트가 서버에 접속 시 서버는 인증서를 클라이언트에 보내고 클라이언트에서 공개 키로 이를 해독합니다. 해독이 완료되면 대칭 키를 생성하여 서버로 암호화하여 전송하고, 서버에서는 개인 키로 암호화된 데이터를 해독하는 방식으로 데이터가 전송됩니다.

https://hub.docker.com/r/certbot/certbot
이번에 인증서를 발급받을 letsencrypt는 무료로 인증서를 발급해주는 기관으로 3달 단위로 갱신하여 사용 가능합니다. 제가 이전에 https 설정을 진행한 방식은 웹 서버에 직접 인증서를 발급받는 방법이었습니다. 이번 실습에서는 DNS 방식으로 인증서를 발급받아 보겠습니다.

인증서를 발급받아 https 설정을 진행하는 프로세스는 다음과 같습니다.
1. Docker Hub에 있는 Certbot 이미지를 이용하여 인증서를 발급받는다. 이 때 인증서가 저장되는 디렉토리를 마운팅하여 본 서버에 인증서 파일이 존재하게 된다.
2. Dockerfile 내부의 웹 서버 설정에 https 포트 허용 및 인증서 관련 정보를 추가한다.
3. 발급받은 인증서 디렉토리를 마운팅하여 기존의 Dockerfile로 생성한 이미지를 실행한다

우선 첫 번째 단계를 진행해보겠습니다. 아래의 명령어를 실행하면 Certbot 컨테이너가 생성됩니다. 이 때 인증서를 서버에 발급받는 것이 목표이기 때문에 컨테이너는 삭제되는 옵션을 추가하고 약관 동의, 메일 수신 등 불필요한 내용은 옵션으로 처리했습니다. 여기서 주의할 점은 저는 우분투 20.04 버전으로 진행해서 인증서 발급 경로가 아래와 같은데 OS 종류 및 버전에 따라 인증서 발급 디렉토리가 달라질 수 있습니다. 인증서가 발급되는 경로로 -v 옵션을 이용해 마운팅해주시면 되겠습니다.

docker run -it --rm --name certbot \
  -v '/etc/letsencrypt:/etc/letsencrypt' \
  -v '/var/lib/letsencrypt:/var/lib/letsencrypt' \
  certbot/certbot certonly -d '{도메인 주소}' --email {자신의 이메일} --agree-tos --no-eff-email --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory

명령어를 실행하면 하나의 화면이 뜨면서 _acme-challenge.{도메인 주소}의 주소로 TXT 레코드에 하단의 토큰을 등록하라고 뜹니다. 이 때 사용 중이신 dns 서비스에 들어가셔서 TXT 레코드로 변경 후 호스트를 _acme-challenge.{도메인 주소}로 설정하신 후 토큰을 추가해주시면 되겠습니다. 여기서 주의하실 점은 호스트 추가 시 풀 도메인을 추가하시면 안되고 (ex domain.com일 경우 _acme-challenge.domain.com이 아닌 _acme-challenge.domain까지만 호스트로 등록합니다) 반드시 등록 이후에 커널에서 엔터를 눌러 진행해야 합니다. 등록 이전에 엔터를 눌러 진행할 경우 정상적으로 발급되지 않습니다.

발급이 되었다면 마운팅한 경로로 들어가보시면 키 값이 정상적으로 존재하는 것을 확인하실 수 있습니다. 이를 사용하기 위해 Dockerfile에 들어가 웹 서버에 관련 정보를 추가해주시면 됩니다. 웹 서버에 수정할 부분은 다음과 같습니다.

  1. https 포트 허용 (443)
  2. 발급받은 인증서의 공개 키와 개인 키 설정

저의 경우 nginx로 진행했습니다. 수정한 결과는 다음과 같습니다.

기존에는 아래 부분이 주석 처리되어 있음. 주석 해제
...
	listen 443 ssl default_server;
	listen [::]:443 ssl default_server;
...

이 코드는 없기 때문에 새롭게 추가
	ssl_certificate /etc/letsencrypt/live/{도메인 주소}/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/{도메인 주소}/privkey.pem;

이전 포스팅에서 Dockerfile을 다른 리포지토리에 저장하여 이를 실행시키는 방식으로 구현했기 때문에 Dockerfile 내부에서 git clone을 진행했습니다. 하지만 추후 CI/CD 프로세스를 적용시키기 위해 번거로운 과정이 발생하기 때문에 기존 코드와 같은 리포지토리를 사용하게 되었습니다. 따라서 몇 가지 변화가 있었습니다.

  1. 기존 ENV로 사용하던 환경 변수를 ARG로 사용하였다
    --> 이미지 빌드 시에만 값을 사용하고 이후에는 값이 필요가 없어 컨테이너 실행 시에서 접근 가능한 ENV 대신 ARG 방식으로 진행했습니다.

  2. git clone 대신 COPY를 이용하여 파일을 웹 서버 루트 경로로 이동시켰습니다. 이런 방식으로 인해 코드 수정 시 CI/CD 프로세스를 이용하여 빠르게 빌드 및 컨테이너 실행이 가능합니다.

  3. sed -i 를 조금 더 유연하게 사용하였습니다. 특정 행의 문자를 수정 및 추가하는 방식을 사용하여 코드 가독성을 향상시켰습니다.

  4. 기존 방식은 git clone을 이용하기 때문에 캐싱을 방지하는 --no-cache 옵션을 사용하여 느린 빌드가 진행되었는데 COPY를 이용하여 파일을 복사하는 방식을 사용하여 캐싱을 통한 빌드가 가능해져 기존 약 180초 빌드 시간이 3초 수준으로 감소하였습니다. (반복 빌드 시 기준)

Dockerfile 전문은 다음과 같습니다.

# 도커의 경우 명령어마다 레이어가 만들어지기 때문에 명령어 수를 줄이는게 중요
# && \ 로 연장한 명령 사이에 주석 들어가 있을 경우 empty continuation line warning 발생

# 이미지 세팅
# FROM을 여러 개 실행하면 각각의 FROM 이전에 실행된 명령은 초기화
FROM ubuntu:20.04
MAINTAINER Gale <cmg4739@gmail.com>

# 환경 변수 세팅
# 이미지 빌드 시에만 사용하므로 ENV 대신 ARG 사용
# docker build -t toda:test --build-arg arg=arg .
ARG 	JWT_SECRET_KEY \
	SERVER_NAME \
	DB_NAME \
	DB_HOST \
	DB_USER \
	DB_PW \
	FCM_ALARM_SERVER \
	REDIS_HOST

#1. 기본 세팅 & 구성요소 설치
WORKDIR /var
RUN mkdir api && \
apt-get update && \
apt install software-properties-common -y && \
add-apt-repository ppa:ondrej/php -y && \
apt-get install nginx -y && \
apt-get install \
	php8.0-common \
	php8.0-cli \
	php8.0-fpm \
	php8.0-cgi \
	php8.0-curl \
	php8.0-mysql \
	php8.0-opcache \
	php8.0-mbstring \
	php8.0-redis -y

#2. 패키지 COPY
COPY . /var/api

#3. env 수정 & 패키지 권한 부여
# sed -i 's(구문자)(바꿀 단어)(구분자)(대체단어)(구분자)g' 파일명
# 특정 행의 문자열 치환 시 sed -i '{line_num}s(구분자)(바꿀단어)(구분자)(대체단어)(구분자)g' 파일명
# 특정 행에 문자열 추가 시 sed -i '{line_num} i\(문자열)' 파일명
# 특정 행 아래에 문자열 추가 시 sed -i '{line_num} a\(문자열)' 파일명

# 로그 폴더 생성 위해 쓰기 권한까지 부여
# 참고 : WORKDIR에서 ARG 참조 시 ${env} 형식으로 참조
WORKDIR /var/api
RUN	sed -i 's%JWT_SECRET_KEY_SAMPLE%'$JWT_SECRET_KEY'%g' env.php && \
	sed -i 's%SERVER_URL_SAMPLE%https://'$SERVER_NAME'%g' env.php && \
	sed -i 's%DB_NAME_SAMPLE%'$DB_NAME'%g' env.php && \
	sed -i 's%DB_HOST_SAMPLE%'$DB_HOST'%g' env.php && \
	sed -i 's%DB_USER_SAMPLE%'$DB_USER'%g' env.php && \
	sed -i 's%DB_PW_SAMPLE%'$DB_PW'%g' env.php && \
	sed -i 's%FCM_ALARM_SERVER_SAMPLE%'$FCM_ALARM_SERVER'%g' env.php && \
	sed -i 's%REDIS_HOST_SAMPLE%'$REDIS_HOST'%g' env.php && \
	chmod -R 777 .

#4. nginx 수정
WORKDIR /etc/nginx/sites-available
RUN     sed -i '27s%# %%g' default && \
	sed -i '28s%# %%g' default && \
	sed -i 's%root /var/www/html;%root /var/api;%g' default && \
	sed -i 's%server_name _;%server_name '$SERVER_NAME';%g' default && \
	sed -i 's%index index.html%index index.php index.html%g' default && \
	sed -i 's%try_files $uri $uri/ =404;%try_files $uri $uri/ /index.php?$query_string;%g' default && \
	sed -i '56s%#%%g' default && \
	sed -i '57s%#%%g' default && \
	sed -i 's%#	fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;%	fastcgi_pass unix:/run/php/php8.0-fpm.sock;%g' default && \
	sed -i '63s%#%%g' default && \
	sed -i '64 i\	ssl_certificate /etc/letsencrypt/live/'$SERVER_NAME'/fullchain.pem;' default && \
	sed -i '64 a\	ssl_certificate_key /etc/letsencrypt/live/'$SERVER_NAME'/privkey.pem;' default

#5. php 수정
WORKDIR /etc/php/8.0/fpm
RUN	sed -i 's%;date.timezone =%date.timezone = Asia/Seoul%g' php.ini && \
	sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/g' php.ini && \
	sed -i 's/session.cookie_httponly =/session.cookie_httponly = 1/g' php.ini && \
	sed -i 's/;session.cookie_secure =/session.cookie_secure = 1/g' php.ini && \
	sed -i 's/memory_limit = 128M/memory_limit = 256M/g' php.ini && \
	sed -i 's/post_max_size = 8M/post_max_size = 56M/g' php.ini && \
	sed -i 's/upload_max_filesize = 2M/upload_max_filesize = 1024M/g' php.ini && \
	sed -i 's/max_file_uploads = 20/max_file_uploads = 50/g' php.ini && \
	sed -i 's/;opcache.memory_consumption=128/opcache.memory_consumption=128/g' php.ini && \
	sed -i 's/;opcache.interned_strings_buffer=8/opcache.interned_strings_buffer=8/g' php.ini && \
	sed -i 's/;opcache.max_accelerated_files=10000/opcache.max_accelerated_files=50000/g' php.ini && \
	sed -i 's/;opcache.revalidate_freq=2/opcache.revalidate_freq=60/g' php.ini && \
	sed -i 's/;opcache.enable_cli=0/opcache.enable_cli=1/g' php.ini && \
	sed -i 's/;opcache.enable=1/opcache.enable=1 opcache.jit=tracing opcache.jit_buffer_size=100M/g' php.ini

#6. nginx & php 실행
# 내부에 설치한 모듈은 설정 파일을 직접 실행시켜야 정상적으로 동작
# CMD, ENTRYPOINT의 경우 Dockerfile 내에서 단 한번만 실행
# nginx 서버를 foreground로 돌리지 않으면 컨테이너를 background로 실행해도 컨테이너 안의 서버가 실행이 안된 상태이기 때문에 daemon off로 foreground로 계속 실행 중인 상황으로 만들기
ENTRYPOINT service php8.0-fpm start && nginx -g "daemon off;"

수정한 Dockerfile로 이미지를 생성한 후 컨테이너를 실행합니다. 이 때 -v 옵션을 이용하여 인증서 경로를 마운팅하여 실행될 컨테이너에서도 인증서 키에 접근 가능하도록 합니다. 여기서 주의할 점은 https를 사용하기 때문에 포트 허용 시 443 포트를 허용해야 합니다.

컨테이너가 정상적으로 띄워져도 https 접속이 되지 않고 dns_probe_finished_nxdomain 오류가 발생하면서 연결이 되지 않는 경우가 있습니다. 이 경우는 위에서 TXT 레코드 등록"만" 진행해서 발생하는 오류입니다. TXT 레코드 등록 뿐만 아니라 A 레코드로 도메인 주소를 호스트로 서버의 IP 주소를 등록해야 정상적으로 동작됩니다.

다음 시간에는 docker-compose를 이용해서 여러 컨테이너들을 한번에 관리하는 작업을 진행하고 발급받은 인증서를 자동으로 갱신하는 작업도 같이 진행해보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다!

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

0개의 댓글