Web/Django-python

docker-compose 사용해서 장고+gunicorn+nginx 서버에 SSL 적용하기

참고 링크 : https://medium.com/@pentacent/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71

여러가지 방법들이 있으나,

지금 할 방법은 로컬 서버에 letsencrypt 설치 후, nginx 컨테이너에 해당 인증서를 볼륨으로 마운트시켜 사용하게 하는 방식.

이경우 certbot 컨테이너도 하나 더 추가해서 사용해야 한다.

해야하는 작업 순서 : docker-compose 수정, nginx 수정, nginx 컨테이너와 certbot 컨테이너 연결, ssl 인증서 발급 , 클라우드 서비스 포트 정책 수정


** 1. docker-compose 파일 수정**

  • nginx 포트에 443 포트 추가
  • certbot 컨테이너 추가
nginx:
    image: nginx
    container_name: nginx_service
    volumes:
      - ../volumes-nginx:/etc/nginx/conf.d
      - ../volumes/.static_root/:/static/
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - durumi
  certbot:
    image: certbot/certbot

2. nginx 설정파일 수정

  • 마운트되는 nginx.conf 설정파일에 ssl 관련 내용 추가. server를 하나 더 만들어서 ssl 내용으로 바꿔주자.
  • 이상태로 시작하면 ssl 인증 키 파일이 없어서 서버 시작 불가능하다.
 server {
    listen 80;
    server_name localhost;
    location / {
        return 301 https://$host$request_uri;
    } 
    location /static/ {
        alias /static/;
    }
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }    
}

server {
    location / {
        proxy_pass http://durumi/;
    }
    location /static/ {
        alias /static/;
    }
    listen 443 ssl;
    server_name localhost;    
}

3. nginx 컨테이너와 certbot 컨테이너 연결

  • lets encrypt는 유효한 URL에 대해 인증서를 할당하기 위해 사용자가 요청한 URL이 유효한지 검사하는 작업을 거친다. ("challenge"라 불리는 요청 작업)
  • 현 상태에서는 서버 시작이 불가하기 떄문에, 유효 URL 체크 작업이 힘듬. 따라서 이미지 마운트 기능을 통해 certbot 컨테이너에서 nginx컨테이너로 인증서 파일을 직접 넘겨주자.
  • 아래 작업은 certbot 컨테이너에서 인증서를 발급받아 로컬 서버에 마운트 된 디렉토리에 저장하고, nginx 컨테이너는 동일한 디렉토리를 마운트해 동일한 인증서 파일을 공유하는 작업이라고 생각하면 됨.
  • docker-compose 파일에서 certbot 부분 volumes 추가
certbot:
    image: certbot/certbot
    container_name: certbot_service
    volumes : 
      - ../volumes/certbot/conf:/etc/letsencrypt
      - ../volumes/certbot/www:/var/www/certbot
  • docker-compose 파일에서 nginx 부분 volumes 추가
- ../volumes/certbot/conf:/etc/letsencrypt
- ../volumes/certbot/www:/var/www/certbot
  • 동일 로컬 디렉토리 경로 volumes/certbot 을 지정해주는게 중요함.
  • 마지막으로, nginx 컨테이너로 넘겨준 파일을 이용해 유효한 챌린지가 되게 해주자. (nginx.conf파일) 아래 내용을 80번 포트 섹션에 추가해주면 된다.
location /.well-known/acme-challenge/ {
    root /var/www/certbot;
} 

4. HTTPS 인증서 연결

  • nginx 설정파일 수정하여 발급받은 ssl인증서를 사용할 수 있게 해줘야한다. nginx.conf 파일에서 443포트 설정한 server 부분에 해당 내용 추가해주자.
ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;
  • 현재 우리 서버는 도메인을 localhost 로 연결하고 있는데, example.org 자리에 실제 도메인 주소를 넣어줘야 하는듯.
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
  • 위 내용은 nginx.conf 파일 맨 끝에 그냥 추가해주면 될듯.

5. 인증서 발급을 위한 준비

  • 이제 실제 인증서를 발급받을 순서지만 그 전에, nginx컨테이너에서 바로 인증서를 추가할수는 없으므로

    1. 더미 증명서를 발급받은 다음
    2. nginx서버를 시작
    3. 더미를 삭제하고,
    4. 실제 증명서를 요청하는 작업

    이 필요하다. 원본 문서에서 제공한 자동화 스크립트를 활용하였다.

    적용시엔 domain , data_path, email을 적절하게 수정해서 사용해야함. docker-compose 파일과 동일 경로에 넣고 실행해주면 된다.

    #!/bin/bash
    
    if ! [ -x "$(command -v docker-compose)" ]; then
      echo 'Error: docker-compose is not installed.' >&2
      exit 1
    fi
    
    domains="expamle.org" 
    rsa_key_size=4096
    data_path="../volumes/certbot"
    email="email@gmail.com" # 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

6. 자동 인증서 갱신

  • 해도 되고 안해도 되는 부분이긴 한데, lets encrypt로 발급받은 인증서는 갱신기한이 존재한다. certbot에서 자동으로 갱신 작업을 하게끔 명령어를 설정해주자.
  • docker-compose 파일에서 certbot 부분에 해당 내용추가한다. 12시간마다 갱신 여부를 판별하게 하는 동작.
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
  • 동일하게 nginx 부분에 해당 내용 추가한다. nginx에서 6시간마다 설정을 다시 로드하게 하는 동작.
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

7. 최종 결과

nginx.conf

upstream durumi {
    ip_hash;
    server containger_name:port; # 서버의 컨테이너 명
}

server {
    listen 80;
    server_name localhost;
    location / {
        return 301 https://$host$request_uri;
    } 
    location /static/ {
        alias /static/;
    }
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }    
}
server {
    listen 443 ssl;
    server_name localhost;    
    location / {
        proxy_pass http://example.org/; 
    }
    location /static/ {
        alias /static/;
    }

    ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;
}

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

docker-compose 일부

nginx:
    image: nginx
    container_name: nginx_service
    volumes:
      - ../volumes-nginx:/etc/nginx/conf.d
      - ../volumes/.static_root/:/static/
      - ../volumes/certbot/conf:/etc/letsencrypt
      - ../volumes/certbot/www:/var/www/certbot
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - durumi
    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 : 
      - ../volumes/certbot/conf:/etc/letsencrypt
      - ../volumes/certbot/www:/var/www/certbot
    entrypoint : "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

8. 실행

세팅이 끝났으니 실제로 스크립트를 돌려보면, 해당 도메인에 대해 challenge를 실패했다는 문구가 나올것이다.

도메인에 연결되는 서버를 내려놨으니 당연한 것. nginx에서 ssl 관련 부분을 일단 제외하고, docker-compose up로 서버를 올려둔 다음에 해당 명령어 실행해봐야겠다.

뭐가 문젠지 알았다. 도메인과 연결되는 nginx 컨테이너는 켜져 있어야 URL에 대한 challenge가 되는것.

docker-compose up nginx 로 nginx만 켜둔 상태에서 위 스크립트를 실행하면 정상적으로 인증서 발급이 완료된다.

nginx realoading 과정에서 에러가 발생할수도 있지만 큰 문제는 아님.

마지막으로, 클라우드 서비스( 나의 경우는 네이버 클라우드) 에서 ACG 정책을 수정하여 443 포트로 접속이 가능하게 설정을 풀어줘야 한다. 안그러면 당연히 연결이 불가.

뿌듯