본문 바로가기

개발이야기/Docker

Docker를 사용해 Django 서버 SSL(HTTPS) 적용하기

728x90

이 글에서는 Let's EncryptNginx를 사용해 docker/django 환경에서 SSL인증서를 적용해 HTTPS를 사용하는 방법을 다뤄보겠다.

SSL?

SSL은 Secure Socket Layer로 인터넷에서 서버와 클라이언트 사이에서 전송되는 데이터를 암호화 하고 보호하기 위한 보안 표준 기술이다.
다른 사람이 전송되는 데이터를 보거나 탈취하는 것을 방지한다.

SSL의 핵심은 암호화다.
전송되는 데이터를 암호화 하기 위해 인증서가 필요한데, 이 글에서는 인증서를 발급받고 Django 서버에 적용하는 방법에 대해 알아보겠다.

Let's Encrypt

내가 운영하는 서버에 HTTPS를 사용하려면 CA(인증 기관)에서 SSL 인증서를 가져와야 한다.
이를 발급받기 위해서는 인증기관를 통해 비용을 지불하고 가져와야하는 경우가 많다.
하지만 Let's Encrypt를 이용하게되면 별도 비용없이 SSL 인증서 발급이 가능하다.

https://letsencrypt.org/ko/getting-started/
위 사이트에서는 인증서 발급, 설치, 갱신의 자동화를 위해 Certbot 사용을 권장하고 있다.
추가로 적용할 도메인도 필요하다.

Docker(docker compose)

Django 서버는 Docker compose를 사용해 구성한다.
기본적으로 DB를 제외한 Web, Nginx, Certbot 컨테이너를 생성하고, Web 컨테이너의 경우 Dockerfile을 작성한다.
먼저 Django 서버를 구성하기 위한 Dockerfile 이다.

# Dockerfile
FROM python:3.11-slim-bullseye

RUN apt-get update && \
    apt-get install python3-dev default-libmysqlclient-dev build-essential -y

RUN mkdir /django_server
WORKDIR /django_server

RUN pip install --upgrade pip
RUN pip install poetry

COPY poetry.lock /django_server/poetry.lock
COPY pyproject.toml /django_server/pyproject.toml
RUN poetry config virtualenvs.create false
RUN poetry install --no-interaction

ADD . /django_server

CMD [ "sh", "entrypoint.sh" ]

Poetry를 사용해 Python 프로젝트를 구성했다.

아래는 django 서버를 구동하기 위한 shell script이다.
Migrate를하고 gunicorn을 사용해 서버를 구동한다.
Port는 4000번을 사용했다.

# entrypoint.sh
#!/bin/bash
python manage.py makemigrations
python manage.py migrate

python manage.py collectstatic --noinput

echo ":::::::Run gunicorn"
gunicorn -b 0:4000 -w 2 django_server.wsgi:application

이제 docker-compose.yml 파일이다.

version: '3'
services:
    web:
        build:
            context: .
        environment: 
            - DEBUG=False
        volumes:
            - .:/django_server
        expose:
            - "4000"
    nginx:
        image: nginx:1.23.1
        ports:
            - "80:80"
            - "443:443"
        volumes:
            - .:/django_server
            - ./config/nginx/:/etc/nginx/conf.d/
            - ./certbot/conf:/etc/letsencrypt
            - ./certbot/www:/var/www/certbot
        depends_on:
            - web
        command : "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
    certbot:
        image: certbot/certbot
        container_name: certbot_service
        volumes : 
        - ./certbot/conf:/etc/letsencrypt
        - ./certbot/www:/var/www/certbot
        entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

web 컨테이너는 작성했던 Dockerfile을 사용하고 4000번 포트를 Expose한다.
nginx 컨테이너는 official 이미지를 사용하고 80, 443포트를 열어두었다.
추가로 config 및 certbot을 위한 volume을 연결한다.

certbot 컨테이너 또한 official 이미지를 사용한다.
entrypoint에는 인증서 갱신을 위한 Script를 적용했다.
12시간 마다 인증서가 자동 갱신된다.

Nginx config

Django 서버 연동 및 HTTPS 적용을 위해 nginx.conf 파일 작성이 필요하다.
아래와 같이 nginx config 폴더로 volume을 연결해 전달한다.
- ./config/nginx/:/etc/nginx/conf.d/
config파일 위치는 ./config/nginx/ 에 위치 한다.

# ./config/nginx/nginx.conf
upstream web { web upsteeam 선언
    ip_hash;
    server web:4000; # web 컨테이너의 4000번 포트 연결
}

server {
    listen 80;
    listen 443 ssl; # SSL

    server_name taeju.kim; # 도메인
    ssl_certificate /etc/letsencrypt/live/mock.domain-for-test.com/fullchain.pem; # SSL
    ssl_certificate_key /etc/letsencrypt/live/mock.domain-for-test.com/privkey.pem; # SSL

    location / {
        proxy_pass http://web/;
    }

    location /static/ {
        alias /django_server/static/; # django static file 서빙
    }

    location /.well-known/acme-challenge/ { # 도메인 소유권 인증을 위한 경로
        root /var/www/certbot;
    }

}

include /etc/letsencrypt/options-ssl-nginx.conf; # SSL
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # SSL

여기까지 certbot을 제외한 설정은 모두 마무리 되었다.
일단, # SSL 이라고 주석처리한 부분을 잘 기억 해두자.

Certbot

이제 모든 준비가 완료되었으니 인증서 발급 및 설치를 진행해보자.
우선 Certbot을 통해 SSL을 가져오는 Script가 필요하다.
아래와 같이 get_ssl.sh 파일을 만들어 보자

#!/bin/bash

if ! [ -x "$(command -v docker-compose)" ]; then
  echo 'Error: docker-compose is not installed.' >&2
  exit 1
fi

domains="taeju.kim"  # 도메인
rsa_key_size=4096
data_path="./certbot"  # certbot 위치 (docker-compose.yml파일에서 정의한 certbot의 root 경로)
email="iam@taeju.kim" # Adding a valid address is strongly recommended
staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits

if [ -d "$data_path" ]; then
  read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
  if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
    exit
  fi
fi

if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
  echo "### Downloading recommended TLS parameters ..."
  mkdir -p "$data_path/conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
  echo
fi

echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
docker-compose run --rm --entrypoint "\
  openssl req -x509 -nodes -newkey rsa:1024 -days 1\
    -keyout '$path/privkey.pem' \
    -out '$path/fullchain.pem' \
    -subj '/CN=localhost'" certbot
echo

echo "### Starting nginx ..."
docker-compose up --force-recreate -d nginx
echo

echo "### Deleting dummy certificate for $domains ..."
docker-compose run --rm --entrypoint "\
  rm -Rf /etc/letsencrypt/live/$domains && \
  rm -Rf /etc/letsencrypt/archive/$domains && \
  rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo

echo "### Requesting Let's Encrypt certificate for $domains ..."
#Join $domains to -d args
domain_args=""
for domain in "${domains[@]}"; do
  domain_args="$domain_args -d $domain"
done

# Select appropriate email arg
case "$email" in
  "") email_arg="--register-unsafely-without-email" ;;
  *) email_arg="--email $email" ;;
esac

# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi

docker-compose run --rm --entrypoint "\
  certbot certonly --webroot -w /var/www/certbot \
    $staging_arg \
    $email_arg \
    $domain_args \
    --rsa-key-size $rsa_key_size \
    --agree-tos \
    --force-renewal" certbot
echo

echo "### Reloading nginx ..."
docker-compose exec nginx nginx -s reload

get_ssl.sh 파일까지 작성 완료되었다면 이제 인증서 발급을 시작해보자.

우선 아래와 같은 조건이 충족되었는지 확인해보자.

  • domain이 적용된 서버 (리눅스 권장)
  • docker 및 docker-compose 설치
  • Dockerfile로 구동이 가능한 Django 프로젝트
  • ./config/nginx.conf
  • ./get_ssl.sh

모두 준비가 되었다면 ./config/nginx.conf 파일의 # SSL로 주석을 달아두었던 부분을 모두 주석 처리해보자.
인증서 발급 및 설치를 위해 SSL과 관련된 nginx 설정을 잠시 Disable 해두는 것이다.

이제 sh ./get_ssl.sh를 사용해 get_ssl.sh 파일을 실행 해보자.
새로운 인증서 발급이 진행되면 Certbot image pulling 및 설정이 시작될 것이다.

$ ./get_ssl.sh
Existing data found for taeju.kim. Continue and replace existing certificate? (y/N) y
### Downloading recommended TLS parameters ...

### Creating dummy certificate for taeju.kim ...
Creating django_server_certbot_run ... done
Generating a RSA private key
...............................+++++
...........................+++++
writing new private key to '/etc/letsencrypt/live/taeju.kim/privkey.pem'
-----

### Starting nginx ...
Recreating django_server_web_1 ... done
Recreating django_server_nginx_1 ... done

### Deleting dummy certificate for mock.domain-for-test.com ...
Creating django_server_certbot_run ... done

### Requesting Let's Encrypt certificate for taeju.kim ...
Creating django_server_certbot_run ... done
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for taeju.kim

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/taeju.kim/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/taeju.kim/privkey.pem
This certificate expires on 2023-05-11.
These files will be updated when the certificate renews.

NEXT STEPS:
- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

### Reloading nginx ...
2023/02/10 01:17:49 [notice] 14#14: signal process started

let's encrypt에서 도메인의 소유권을 확인하기 위해 /.well-known/acme-challenge/ 의 경로를 접근하게 된다.
만약 get_ssl.sh의 실행히 실패하게 되면 위의 경로를 확인해보자.

문제 없이 모두 종료되었다면 모든 컨테이너를 종료하고 nginx.conf의 SSL관련 설정을 다시 Enable 한다.
이제 docker-compose up -d 를 입력해 컨테이너를 모두 실행하자

그럼 HTTPS로 연결이 가능한것을 확인할 수 있다.