IP 주소로 접속하는 서비스를 계속 쓰다 보니 두 가지가 계속 불편했다. 하나는 인스턴스를 껐다 켤 때마다 IP가 바뀌는 것, 다른 하나는 브라우저 주소창에 뜨는 "안전하지 않음" 표시였다. 이 둘을 한 번에 정리하기로 했다.
Elastic IP부터
도메인을 연결하려면 대상 IP가 고정이어야 한다. 안 그러면 인스턴스 재시작할 때마다 DNS를 손으로 다시 고쳐야 하는데, 그건 말이 안 되는 일이라 도메인 사기 전에 Elastic IP부터 할당했다.
EC2 콘솔 → 네트워크 및 보안 → 탄력적 IP → 할당을 누르면 새 고정 IP를 하나 받는다. 그걸 인스턴스에 연결(Associate)하면 끝이다.
탄력적 IP는 실행 중인 인스턴스에 붙어있는 동안만 무료다. 할당만 해두고 아무 데도 안 붙이면 그때부터 시간당 소액이 나간다. 유휴 자원을 그대로 방치하지 못하게 만든 요금 정책 같아서, 할당하자마자 바로 인스턴스에 연결했다.
연결하고 나서 IP가 바뀌었으니 CORS 허용 목록 같은 걸 전부 새 IP로 갱신했다. 이게 마지막으로 하는 IP 갱신이었다. 이후로는 도메인 기준으로만 관리하면 되니까.
도메인은 가비아에서
원래는 Route53에서 바로 사고 싶었는데, 계정 결제 플랜 문제로 도메인 구매가 막혀있었다. EC2 프리티어랑은 별개의 제약이었다. 계정을 유료 플랜으로 올리면 되긴 하는데, 이번엔 그냥 다른 방법을 택했다. 이것저것 알아보다가 가비아에서 berich.co.kr을 구매했다.
Route53으로 옮기는 것도 생각해봤는데, 호스팅 영역 하나 만드는 데만 월 소액이 계속 나간다. A레코드 두 개(@, www) 등록하는 게 전부인 상황에서 굳이 그 비용을 낼 이유가 없어서 가비아 자체 DNS 관리 기능을 그냥 썼다.
| 타입 | 호스트 | 값 |
|---|---|---|
| A | @ | Elastic IP |
| A | www | Elastic IP |
@는 berich.co.kr 자체를, www는 www.berich.co.kr을 가리킨다. 둘 다 같은 서버(같은 IP)로 보내고, 어느 쪽으로 들어와도 같은 화면이 뜨게 했다. DNS를 어디서 관리하든 뒤에 나오는 HTTPS 인증서 발급 과정에는 영향이 없다. 인증서를 주는 쪽에서 확인하는 건 "이 도메인이 진짜 이 서버를 가리키는가" 하나뿐이라, Route53인지 가비아인지는 상관없었다.
HTTPS가 정확히 뭘 해주는지
붙이기 전에 이 부분을 좀 헷갈렸는데, 정리하면 두 가지다. 하나는 통신 암호화 - http://는 로그인 비밀번호 같은 데이터가 평문 그대로 오가서 같은 와이파이를 쓰는 다른 사람도 이론적으로 가로챌 수 있는데, https://는 그걸 막아준다. 다른 하나는 서버 신원 보증이며 브라우저가 접속한 곳이 진짜 berich.co.kr이 맞는지, 중간에 다른 서버로 가로채인 게 아닌지 확인해주는 역할이다.
Let's Encrypt로 인증서 받기
Let's Encrypt는 무료로 SSL 인증서를 내주는 비영리 인증기관이다. certbot이라는 프로그램이 서버에서 알아서 발급 절차를 처리해준다. 흐름은 대략 이렇다.
certbot이 "berich.co.kr 인증서 줘"라고 Let's Encrypt에 요청
-> Let's Encrypt가 80번 포트로 접속해서 도메인 소유권 확인
-> 검증 통과하면 EC2에 인증서 파일(fullchain.pem, privkey.pem) 전송
-> Nginx 설정에 이 파일 경로를 연결하면 HTTPS 활성화
여기서 좀 걸린 부분이 있었는데, 보통 나오는 가이드는 certbot --nginx처럼 Nginx가 서버에 직접 설치돼 있다는 걸 전제로 한다. 근데 우리 Nginx는 Docker 컨테이너 안에 있어서 이 방법을 그대로 못 썼다.
대신 인증서 자체는 EC2 호스트 쪽(/etc/letsencrypt)에 저장하고, docker-compose.prod.yml에서 그 경로를 읽기 전용으로 컨테이너에 마운트해서 컨테이너 안 Nginx가 참조하게 했다.
frontend:
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro # 인증서 읽기전용 마운트
ports:
- "80:80"
- "443:443"
발급받을 때도 컨테이너의 80번 포트를 잠깐 비워줘야 해서, --standalone 모드로 certbot이 그 포트를 임시로 점유하게 했다.
sudo certbot certonly --standalone \
-d berich.co.kr -d www.berich.co.kr \
--pre-hook "docker compose -f /home/ubuntu/docker-compose.prod.yml stop frontend" \
--post-hook "docker compose -f /home/ubuntu/docker-compose.prod.yml start frontend"
--pre-hook으로 발급 직전에 프론트엔드 컨테이너를 잠깐 내리고, --post-hook으로 발급 끝나면 다시 올리게 했다. 인증서는 90일마다 만료되는데, certbot이 설치할 때 시스템 타이머를 자동으로 등록해둬서 만료 전에 알아서 갱신을 시도한다. 이 hook은 갱신 설정 파일(/etc/letsencrypt/renewal/berich.co.kr.conf)에도 그대로 저장돼서, 자동 갱신 때도 같은 순서(컨테이너 내리고 - 발급 - 다시 올리고)를 반복한다. 실제로 갱신이 잘 되는지는 sudo certbot renew --dry-run으로 미리 시뮬레이션해볼 수 있다.
Nginx 설정 - 80은 리다이렉트, 443이 진짜
server {
listen 80;
server_name berich.co.kr www.berich.co.kr;
return 301 https://$host$request_uri; # https로 리다이렉트
}
server {
listen 443 ssl;
server_name berich.co.kr www.berich.co.kr;
ssl_certificate /etc/letsencrypt/live/berich.co.kr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/berich.co.kr/privkey.pem;
location / {
root /usr/share/nginx/html;
try_files $uri /index.html; # SPA 라우팅 처리
}
location /api/ {
proxy_pass http://backend:8080/api/; # 백엔드로 프록시
client_max_body_size 10m; # 업로드 용량 제한
}
}
80번으로 들어오면 무조건 443으로 리다이렉트시키고, 실제 응답은 443 서버 블록이 담당한다. 이 설정을 컨테이너에 반영하고 브라우저로 http://berich.co.kr을 쳐보니 자동으로 https://로 넘어가면서 주소창에 자물쇠가 떴다.
하나 걸렸던 것 - IP로 접속하면 경고가 뜬다
인증서는 도메인 전용이라 https://Elastic IP로 직접 접속하면 브라우저가 인증서 이름 불일치로 경고를 띄운다.
다음은 이 모든 배포 과정을 손으로 안 하고 git push 한 번으로 끝내는 이야기다. 지금까지는 코드 고칠 때마다 로컬에서 이미지 빌드하고, push하고, SSH 들어가서 pull하고 재기동하고 이 반복이 꽤 번거로웠다.
'AWS' 카테고리의 다른 글
| PWA로 홈 화면에 앱처럼 설치하기 (0) | 2026.08.30 |
|---|---|
| GitHub Actions로 배포 자동화하기 (0) | 2026.08.30 |
| 파일 저장소를 S3로 분리하기 (0) | 2026.08.30 |
| Docker로 EC2에 배포하기 (0) | 2026.08.30 |