이전 포스팅들에서 언급했듯 저는 https://uxpirates.xyz 이 프로젝트를 풀스택으로 개발하여 서비스 중입니다.
물론 배포한 후 실제 서비스를 하고 있긴 하지만 계속해서 기능을 추가하고, QA를 통해 개선해나가고 있던 프로젝트였습니다. 정말 평상시와 똑같이 퇴근하고 여유롭게 작업하던 중에, 갑자기….. "이 사이트는 안전하지 않음"이라는 메시지가 나타나면서 사이트에 제대로 접속이 되지 않는 문제가 발생했습니다.
불과 몇 분 전까지 계속 개발하고 있던 url이었기에 정말 너무 당황해서, 서버가 꺼진 것인지 ec2 콘솔부터 급하게 접속해보았습니다. 하지만 서버는 docker compose를 통해 너무 멀쩡하게 잘 돌아가고 있었고, 그렇다면 도메인 문제일 수도 있겠다는 생각에 바로 기획자 분께 연락드려 도메인 기간이 만료되었는지를 여쭤보았습니다. (제가 혼자 배포한 서비스를 3개월 이상 지속하고 있던 것이 처음이라 이런 경우가 완전 처음이었습니다……)
그런데 도메인도 멀쩡히 살아 있었고, 당황한 마음을 잠시 멈추고 확인해보니 http로 자동으로 리다리렉트되면서 인증서에 대한 오류가 발생하는 것을 보고 ‘SSL 인증서가 문제겠구나!’ 하는 생각이 들었습니다.
이 프로젝트는 Let's Encrypt를 사용하여 무료로 인증서를 발급받았고, 이를 도커 컨테이너 환경에서 운영하고 있는 상태였습니다.
Let's Encrypt 인증서는 기본적으로 90일마다 갱신이 필요합니다. 하지만 제가 갱신을 자동화하지 않았기 때문에 처음 배포하고 3개월이 지난 지금…. 인증서가 만료된 것이었습니다……….
그래서 이번 기회에 Let's Encrypt 인증서를 갱신하고, 자동 갱신까지 설정해보았고, 그 과정을 정리해 포스팅하려 합니다..
우선 현재 인증서가 만료되었는지, 그리고 인증서 갱신이 가능한지 확인하기 위해 Certbot의 갱신 명령어를 실행하였습니다.
sudo docker run --rm -it \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /var/lib/letsencrypt:/var/lib/letsencrypt \
--network host certbot/certbot renew
하지만 아래 오류가 발생하였습니다.
Failed to renew certificate uxpirates.xyz with error:
The requested nginx plugin does not appear to be installed
이 오류는 Nginx 플러그인을 사용하여 인증서를 갱신하려 했지만, 도커 컨테이너 내에 해당 플러그인이 설치되지 않았기 때문에 발생한 문제였습니다.
(전 현재 docker compose와 nginx를 이용하여 서버를 배포해둔 상태입니다.)
그래서 Standalone 모드로 인증서를 갱신하도록 변경하였습니다.
sudo docker run --rm -it \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /var/lib/letsencrypt:/var/lib/letsencrypt \
--network host certbot/certbot renew --standalone
하지만.. 바로 해결되지 않았고,,,, 이번에는 아래 오류가 발생하였습니다.
Could not bind TCP port 80 because it is already in use by another process on this system.
문제 원인
해결 방법
우선 포트 80을 점유하고 있는 프로세스를 확인하였습니다.
sudo lsof -i :80
출력 결과:
nginx 1234 root 4u IPv4 123456 TCP *:http (LISTEN)
즉, Nginx가 포트 80을 점유하고 있었습니다.
따라서 Nginx를 일시적으로 중지한 후 인증서를 갱신하기로 하였습니다.
사실 nginx를 중지하면 배포되어 있는 서비스가 잠시 중단되기에, 너무 떨리기도 하고 괜찮을지 걱정이 되었지만…
지금 당장 https로 접속이 되지 않는 것이 가장 큰 문제였기에 ‘할 수 있는 건 빨리 다 해 보자’ 하는 마음으로 nginx 컨테이너를 잠시 멈추었습니다.
sudo docker-compose stop nginx
(일반적인 서버 환경이라면 sudo systemctl stop nginx
명령어를 사용하면 됩니다. 저는 docker compose를 사용하였기에 이렇게 멈추었습니다.)
그리고나서 Certbot 갱신을 다시 시도해주었습니다.
sudo docker run --rm -it \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /var/lib/letsencrypt:/var/lib/letsencrypt \
--network host certbot/certbot renew --standalone
다행히 이번에는 성공적으로 인증서가 갱신되었습니다.
그래서 우선 빨리 서비스를 다시 시작하기 위해 Nginx를 다시 실행해주었습니다.
sudo docker compose start nginx
(일반적인 서버 환경이라면 sudo systemctl start nginx
)
이제 급한 불은 껐으니(?), 세 달 뒤에 또 이런 일이 발생하지 않도록 자동 갱신을 설정해주려고 하였습니다.
사실 아직 자동 갱신이 제대로 되는지는 (자동 갱신 설정 후 3개월이 더 지나지 않았기 때문에) 확신하지 못하지만, 아마 제가 6월 말에도 인증서 관련 포스팅을 올리지 않는다면 큰 문제가 없는 것이 아닐까~ 싶습니다…ㅎㅎ,,,
어쨌든 3개월마다 이 과정을 반복하는 것은 비효율적이기 때문에 자동 갱신을 설정해주었는데,
먼저 테스트 모드로 자동 갱신이 정상적으로 동작하는지 확인하였습니다.
(가장 뒤에 --dry-run
명령어가 붙는데, 이 명령어를 빼면 실제로 갱신을 시키는 것이고, 붙이면 실제 갱신 없이 테스트하는 것이라고 합니다.)
sudo docker run --rm -it \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /var/lib/letsencrypt:/var/lib/letsencrypt \
--network host certbot/certbot renew --dry-run
성공적으로 갱신되는 것을 확인했고, (성공하면 Congratulations라고 메시지가 뜹니다)
이제 이 명령어를 자동으로 실행할 수 있도록 설정해주면 됩니다.
아래 명령어로 자동 갱신용 renew_cert.sh
파일을 만들어 주었습니다.
sudo nano /home/ubuntu/renew_cert.sh
(vim
, touch
등 편한 것으로 만들어도 무관합니다.)
그리고 이 파일에 내용을 작성해주는데, 아래처럼 nginx 중지 → 인증서 갱신 → nginx 재시작의 과정을 거칠 수 있게 해주면 됩니다.
#!/bin/bash
echo "Starting SSL certificate renewal process..."
# Nginx 중지
sudo docker compose stop nginx
# 인증서 갱신
sudo docker run --rm -it \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /var/lib/letsencrypt:/var/lib/letsencrypt \
--network host certbot/certbot renew
# Nginx 다시 시작
sudo docker compose start nginx
echo "SSL certificate renewal completed!"
모두 작성했다면 저장 후 나가기를 해주면 됩니다. (명령어 : Ctrl + X
→ Y
→ Enter
)
이제 스크립트 실행이 가능하도록 권한을 추가해주어야 합니다.
(바로 크론탭 설정하면 권한이 없다는 오류가 발생합니다.)
sudo chmod +x /home/ubuntu/renew_cert.sh
이제 아래 명령어로 직접 실행해볼 수도 있는데,
sudo /home/ubuntu/renew_cert.s
정상적으로 실행되면 자동 갱신을 설정할 준비가 된 것입니다.
이제 crontab
을 사용해서 3개월마다 자동 갱신되도록 설정해줍니다.
sudo crontab -e
(처음 실행하면 편집기를 고르라고 하는데, 저는 그냥 nano
를 선택했습니다.)
그리고 맨 아래 줄에 아래 내용을 추가해줍니다.
0 3 1 * * /home/ubuntu/renew_cert.sh >> /var/log/renew_cert.log 2>&1
이 설정은 매월 1일 새벽 3시에 자동으로 갱신하겠다는 의미이고, (아까 만들어둔 실행 파일을 통해)
(>> /var/log/renew_cert.log
는 로그를 저장하는 거라서 나중에 로그 확인이 가능합니다.)
저장하고 나가기 (Ctrl + X
→ Y
→ Enter
)로 종료해주면 됩니다.
크론이 정상적으로 등록되었는지 확인하려면,
crontab -l
이렇게 실행해서 방금 추가했던 renew_cert.sh
가 있는지 확인해주면 됩니다.
이제 매일 새벽 3시에 자동으로 인증서를 갱신하고, Nginx를 재시작하도록 설정했고,
3개월 후에… 아마 자동 갱신이……. 될 것이라 기대합니다 ^^
진짜 내가 주체가 되어 실제 배포를 하고 서비스를 한다는 건.... 훨씬 많은 문제가.... 다가오게 된다는 뜻.............^^
+(아래부터의 내용은 2025년 6월, 그러니까 3개월 후에 추가로 작성된 내용입니다..)
인증서를 발급하고 나서 딱 3개월쯤 지났을 때, 갑자기 https 접속이 되지 않았습니다. 분명 ssl 인증서가 잘 갱신된 줄 알았는데, 갱신 시점이 하루쯤 지나서 혹시나 하여 확인해보니
http로는 접속이 되는데, https는 브라우저에서 “이 연결은 안전하지 않음”이라는 메시지만 뜨는 것을 확인했습니다.....
처음엔 서버가 죽은 줄 알고 EC2 대시보드를 뒤졌고, nginx 설정이 잘못됐나 싶어 로그도 확인해봤지만 모두 이상이 없었습니다.
(심지어 그 와중에 EC2 대시보드에 아무 인스턴스가 없어서 서버 자체가 날라간 줄 알고 식겁했는데, 다시 보니 ec2 지역을 잘못 설정해두고 찾고 있었더군요 ^_^ 이악물)
그제서야 인증서 유효 기간을 확인해보니 이미 만료된 상태였습니다.
참고 명령어:
echo | openssl s_client -connect uxpirates.xyz:443 | openssl x509 -noout -dates
위에서 작성했었지만 이미 인증서 자동 갱신을 위한 스크립트도 만들어 두었고, /home/ubuntu/renew_cert.sh
에 아래처럼 설정해뒀습니다.
#!/bin/bash
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
set -e
sudo docker compose stop nginx
sudo docker run --rm \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /var/lib/letsencrypt:/var/lib/letsencrypt \
--network host certbot/certbot renew
sudo docker compose start nginx
그리고 이 스크립트를 매월 1일 새벽 3시에 실행되도록 root 크론탭에 추가해두었습니다.
sudo crontab -l
# 0 3 1 * * /home/ubuntu/renew_cert.sh >> /var/log/renew_cert.log 2>&1
하지만 문제는...
그 스크립트가 실제로 실행되었지만, 갱신에는 실패하고도 그 사실을 알지 못했다는 점입니다.
로그를 확인해본 결과, 자동 갱신이 실행은 되었지만 다음과 같은 이유로 조용히 실패했었습니다.
문제 1: 인증서 갱신 중 포트 80 충돌
Certbot은 인증서를 갱신할 때 도메인 검증을 위해 포트 80을 사용하려고 시도합니다.
하지만 이 시점에 이미 Nginx가 포트 80을 점유하고 있었기 때문에 다음과 같은 오류가 발생했습니다.
Could not bind TCP port 80 because it is already in use by another process on this system.
즉, 자동으로 실행된 스크립트에서도 nginx를 중지하려 했지만,
root 권한에서 환경 변수나 PATH 문제로 docker compose
가 실행되지 않았거나,
혹은 중지 전에 인증 절차가 시작되면서 충돌이 났던 것입니다.
문제 2: 스크립트가 실패해도 오류를 감지하지 못함
자동 갱신 스크립트에는 echo
출력은 있었지만,
성공 여부를 체크하거나 로그를 모니터링하지 않았기 때문에
인증서 갱신 실패가 조용히 묻혀버렸습니다.
root 경로
와 set -e
추가실패 시 바로 중단되도록 설정:
#!/bin/bash
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
set -e
이 설정 덕분에, docker compose
나 certbot
명령이 실패할 경우 스크립트 전체가 중단되며, 이후 과정을 실행하지 않아 문제를 감지할 수 있게 됩니다.
처음엔 스크립트를 매월 1일에 한 번만 실행되도록 설정해뒀는데,
0 3 1 * * /home/ubuntu/renew_cert.sh >> /var/log/renew_cert.log 2>&1
이 설정은 90일짜리 인증서를 30일마다 한 번만 체크하기 때문에,
실수 한 번이면 인증서가 만료될 수 있는 위험한 간격이었습니다.
→ 그래서 현재는 매일 새벽 3시에 실행되도록 크론탭을 수정하였습니다:
0 3 * * * /home/ubuntu/renew_cert.sh >> /var/log/renew_cert.log 2>&1
이렇게 하면 certbot이 알아서 갱신 대상인지 판단해주고, 필요할 때만 인증서를 갱신합니다.
부하도 거의 없고, 안전성이 훨씬 높아졌습니다.
추가적으로, /var/log/renew_cert.log
파일을 주기적으로 확인하거나,
갱신 실패 시 이메일이나 슬랙으로 알림을 받는 기능도 고려 중입니다.
처음엔 분명히 자동화했다고 생각했지만,
“실제로 실행되었는지, 실패하지는 않았는지, 갱신이 완료되었는지”
그 이후를 확인하지 않으면 진짜 자동화라고 할 수 없다는 것을 이번 경험을 통해 확실히 배웠습니다.
이제는 인증서가 만료되지 않고 자동으로 갱신되길 바라며...
다음 갱신 시점인 9월 말에 아무 일도 일어나지 않기를 🙏