스프링 AWS + Docker 배포를 해보자! - 1

Jun·2025년 5월 31일
post-thumbnail

배포는 왜 해야 할까?

우리가 만든 서비스들은 로컬호스트(localhost) 즉 본인의 컴퓨터에서만 실행시켜볼 수 있어요
만약 우리가 만든 서비스를 다른 사람들과 공유하거나 외부에서도 접속할 수 있게 하려면, 반드시 배포(Deploy)가 필요해요

AWS

저희는 AWS를 사용해서 배포를 진행해 보겠습니다. AWS(Amazon Web Services) 란 아마존에서 제공하는 클라우드 기반의 플랫폼으로 쉽게 말해 24시간 실행되는 원격의 컴퓨터를 빌리는 개념으로 생각하면 돼요

Docker


여러분 그런 경험 있지 않으신가요?

"내 컴퓨터에서는 이거 실행되는데…?"

개발자라면 한 번쯤 겪어봤을 법한 이 말. 환경 차이 때문에 코드가 어디선 안 돌아가는 황당한 상황, 도커(Docker)가 바로 이런 문제를 해결해줍니다.

도커는 애플리케이션을 컨테이너라는 독립된 환경에 패키징해서 실행할 수 있게 해주는 가상화 플랫폼이에요. 쉽게 말해, 개발자가 만든 코드와 실행 환경을 하나의 상자(Container)에 담아서, 어디서든 동일하게 실행되도록 보장해주는 기술입니다.

이제는 내 컴퓨터에서 잘 되던 프로그램이 운영 서버에서 오류를 일으킬까 봐 걱정할 필요가 없어요.

Docker 이미지와 Docker 컨테이너

도커 엔진에서 사용하는 기본 단위는 이미지와 컨테이너이며 도커 엔진의 핵심이에요.
도커 이미지와 컨테이너는 1:N 관계입니다.

Docker File -> Docker Image: Docker File은 도커 이미지를 만들때 사용하는 파일. docker build 명령어를 실행시키면 도커 이미지를 만들 수 있습니다.

Docker Image -> Docker Container: Docker Image를 docker run 명령어를 실행시키면 Docker Container를 만들 수 있습니다.

Docker 이미지

도커 이미지(Docker Image)는 컨테이너를 생성할 때 필요한 요소이며, 가상 머신을 생성할 때 사용하는 iso 파일과 비슷한 개념이에요. (ISO 파일은 쉽게 말해서 운영체제 설치용 USB를 만들 때 사용하는 설치 파일)

이미지는 컨테이너를 생성하고 실행할 때 읽기 전용으로 사용되며 여러 계층으로 된 바이너리 파일로 존재합니다.

도커에서 사용하는 이미지의 이름은 기본적으로 아래의 형태로 구성됩니다.

[저장소 이름]/[이미지 이름]:[태그]
  • 저장소 이름: 이미지가 저장된 장소. 저장소 이름이 명시되지 않은 이미지는 도커 허브의 공식 이미지를 뜻함.

  • 이미지 이름: 해당 이미지가 어떤 역할을 하는지 나타내며 필수로 설정해야함. ex) ubuntu:latest -> 우분투 컨테이너를 생성하기 위한 이미지라는 것을 나타냄.

  • 태그: 이미지의 버전을 나타냄. 태그를 생략하면 도커 엔진은 latest로 인식함.

Dokcer 컨테이너

도커 컨테이너(Docker Container)는 도커 이미지로 생성할 수 있으며, 컨테이너를 생성하면 해당 이미지의 목적에 맞는 파일이 들어 있는, 호스트와 다른 컨테이너로부터 격리된 시스템 자원 및 네트워크를 사용할 수 있는 독립된 공간(프로세스)이 생성됩니다.

대부분의 도커 컨테이너는 생성될 때 사용된 도커 이미지의 종류에 따라 알맞은 설정과 파일을 가지고 있기 때문에 도커 이미지의 목적에 맞도록 사용되는 것이 일반적입니다.

ex) 웹 서버 도커 이미지로부터 여러개의 도커 컨테이너를 생성하면 생성된 컨테이너의 개수만큼 웹 서버가 생성되고, 이 컨테이너들은 외부에 웹 서비스를 제공하는 데에 사용됩니다.

컨테이너는 이미지를 읽기 전용으로 사용하되 이미지에서 변경된 사항만 컨테이너 계층에 저장하므로 컨테이너에서 무엇을 하든지 원래 이미지는 영향을 받지 않습니다. 또한 생성된 각 컨테이너는 각기 독립된 파일시스템을 제공받으며 호스트와 분리돼 있으므로 특정 컨테이너에서 어떤 어플리케이션을 설치하거나 삭제해도 다른 컨테이너와 호스트는 변화가 없습니다.

실습


AWS Region 설정하기

우측 상단에 AWS의 리전이 서울로 설정 되어있는지 확인합니다. 서울이 아니라면 아시아 태평양 서울로 변경하여 줍니다.

검색창에 'EC2'를 입력하고 EC2 대시보드를 선택하고 인스턴스 시작하기 버튼을 클릭합니다.

인스턴스에 적당한 이름을 설정하여 줍니다.

다음으로 AMI를 설정하여 줍니다. 여기서는 프리티어가 지원되는 Amazon Linux의 Amazon Linux 2023 kernel-6.1 AMI를 선택합니다.

참고로 아마존 리눅스가 클라우드 환경과 Dokcer 컨테이너에 최적화되어 있다고 합니다.

인스턴스 유형은 프리티어로 표기된 t2.micro 선택해줍니다. 다른 유형 선택 시 비용이 나가니 주의 바랍니다!

인스턴스로 접근하기 위해서는 비밀 키가 필요합니다. 인스턴스는 지정된 pem 키(비밀키)와 매칭되는 공개키를 가지고 있어서 해당 pem 키 외에는 접근을 허용하지 않습니다. 새 키 페어 생성을 클릭하여 줍니다.

다음과 같이 키페어에 적당한 이름을 설정하고, RSA 유형의 .pem 키 파일을 생성하고 다운로드 받아줍니다. 한 번 다운받은 키 파일은 두 번 다시 다운받을 수 없기 때문에 보관 위치를 잘 기억합니다.

다음 네트워크 설정에서는 추후에 다시 설정할 것이기 때문에 지금은 별다른 설정 없이 넘어갑니다.

다음 단계는 스토리지 구성입니다. 스토리지는 여러분이 흔히 하드디스크라고 부르는 서버의 디스크를 이야기하며 서버의 용량을 설정하는 단계입니다. 기본값 설정은 8GB이지만 30GB까지 프리티어로 가능하므로 변경하여 줍니다.

요약이 아래 화면과 같다면 인스턴스 시작 버튼을 눌러줍니다.

인스턴스 탭으로 이동해 보면 정상적으로 인스턴스가 생성된 것을 확인할 수 있습니다.

탄력적 IP 할당

인스턴스 서버를 중지하고 재시작 시 IP주소가 변경됩니다. 변경된다면 DNS레코드, nginx.conf등 여러 정보들을 바뀐 IP주소로 재변경해야 되므로 불편을 초래할 수 있습니다. 이를 방지하기 위해 탄력적 IP주소를 활용하여 IP 주소를 고정시켜 봅시다.

먼저 네트워크 및 보안 -> 탄력적 IP 주소 할당을 통해 새로운 탄력적 IP를 할당하여 줍니다.

변경 없이 할당해줍니다.

탄력적 IP -> 작업 -> 탄력적 IP 주소 연결 버튼을 클릭하여 줍니다.

인스턴스를 클릭하면 자동으로 인스턴스 ID가 뜹니다. 선택 시 프라이빗 IP 주소도 자동으로 입력됩니다. 이후 연결 버튼을 클릭하면 우리의 인스턴스에 탄력적 IP가 할당됩니다.

주의할 점이 있습니다!!! 방금 생성한 탄력적 IP는 생성하고 EC2 서버에 연결하지 않으면 비용이 발생합니다. 즉, 생성한 탄력적 IP는 무조건 EC2에 바로 연결해야 하며 더는 사용할 인스턴스가 없을 때도 아래와 같이 탄력적 IP를 삭제(릴리스)해야 합니다.

보안그룹 설정

보안 그룹은 방화벽을 이야기합니다. 서버로 8080포트 이외의 요청은 허용하지 않는다 는 역할을 하는 방화벽이 AWS에서는 보안 그룹으로 사용됩니다.

✅ 인바운드 규칙(inbound) : 외부에서 EC2나 RDS 등의 내부로 접근할때 사용되는 방화벽 규칙
✅ 아웃바운드 규칙(outbound) : EC2나 RDS 등의 내부에서 외부로 접근할때 사용되는 방화벽 규칙

현재 자동으로 만들어진 보안 그룹으로 설정되어 있는데, 보안 그룹을 새로 만들어서 적용해보겠습니다.

저희는 EC2에 접속해서 서버를 띄우는 것이 목적이기 때문에 인바운드 규칙만 설정해 보겠습니다. 좌측 네트워크 및 보안 그룹 탭에서 보안 그룹을 선택해 줍니다.

상단의 보안 그룹 생성 버튼을 클릭하여 줍니다.

보안 그룹에 적당한 이름과 설명을 적고 하단의 인바운드 규칙 추가 버튼을 클릭합니다.

아래 화면과 같이 총 4개의 규칙을 추가합니다. 규칙에 대한 설명은 아래와 같습니다.

  • 8080: 스프링 부트 기반의 서버를 열어줄 것이기 때문에 사용자 지정으로 8080 포트를 개방하고 URL을 아는 누구나 접속할 수 있도록 Anywhere-IPV4로 설정합니다.
  • 22(SSH): 현재 사용 중인 컴퓨터에서 서버로 접속하고자 할 때 사용합니다. Anywhere-IPV4로 SSH 트래픽을 허용해야 집에 아니라 카페, 강의실과 같은 곳에서도 접속이 가능합니다.
  • 80(HTTP): HTTP 연결 시 사용됩니다.
  • 433(HTTPS): HTTPS 연결 시 사용합니다.

작성을 완료하였으면 보안 그룹 생성 버튼을 클릭합니다.

다시 EC2 인스턴스 탭으로 돌아와서 인스턴스 ID 위에서 우측 마우스를 클릭하고 보안 -> 보안그룹변경을 클릭합니다.

보안 그룹 선택에서 앞서 생성한 보안그룹을 선택하여 추가하고 기존의 보안 그룹은 제거해 줍니다. 마지막으로 저장 버튼을 클릭합니다.

이제 키 페어 파일(.pem)이 있는 디렉토리에서 ssh -i "키 페어 파일" ec2-user@ec2-탄력적 IP 주소.ap-northeast-2.compute.amazonaws.com으로 EC2 인스턴스 서버에 접속할 수 있습니다.

해당 명령은 인스턴스의 연결 탭에서도 확인할 수 있습니다.(왼쪽에 네모 클릭하면 자동 복사)


이제 우리가 만든 인스턴스에 연결해봅시다. 터미널 창으로 돌아와서 해당 명령어를 복사하여 입력해 줍니다.

만약 Are you sure you want to continue connecting (yes/no/[fingerprint])? 라는 질문이 나온다면 yes를 입력합니다.

다음과 같은 오류가 발생하면, other의 권한을 변경하여 줍니다. other와 group에게 읽기, 쓰기, 실행 그 어떤 권한도 부여하지 않습니다. (명령어: chmod 600 LikeLionServer.pem)


다시 접속을 시도해 보면 정상적으로 접속되는 것을 확인할 수 있습니다.

타임존 변경하기

EC2 서버의 기본 타임존은 UTC입니다. 이는 세계 표준 시간으로 한국의 시간대가 아닙니다. 즉 한국의 시간과는 9시간이 차이가 발생합니다. 서버의 타임존을 한국 시간(KST) 으로 변경합니다.

우선 현재 타임존이 어떻게 설정되어 있는지 확인합니다. (명령어: date)

다음 명령어를 차례로 수행합니다.

sudo rm /etc/localtime

sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

다시 한국 시간으로 타임존이 잘 변경되었는지 확인합니다. (명령어: date)

스왑 메모리 설정

운영체제 메모리의 데이터들 중에서 어느 순간 활발하게 읽거나 쓰여지는 부분은 많지 않고 대부분은 아주 가끔 읽거나 쓰여집니다. 따라서 메모리에서 활발하게 읽거나 쓰여지는 부분만 RAM 에 보관하고, 아주 가끔 읽거나 쓰여지는 부분은 디스크(HDD 나 SDD)에 보관하게 된다면 메모리를 더 효율적으로 사용할 수 있습니다. 디스크의 이러한 공간을 스왑 영역이라고 합니다.

현재 메모리의 용량과 사용량을 확인합니다. 약 1GB 정도의 용량을 가지고 있습니다. 이 정도로는 애플리케이션을 실행하다가 먹통이 될 수도 있습니다. (명령어: free -m)

스왑 파일을 사용하여 메모리를 할당해봅시다. 보통 메모리 크기의 2배만큼 할당하는 것을 권장합니다.

스왑 영역으로 사용할 파일을 만들어 줍니다. 다음 명령어를 순서대로 입력합니다.

  • sudo fallocate -l 2G /swapfile # 스왑 파일 생성
  • sudo chmod 600 /swapfile # 생성된 스왑 파일의 권한 설정
  • sudo mkswap /swapfile # 스왑 파일을 스왑 영역으로 설정합니다.
  • sudo swapon /swapfile # 설정된 스왑 파일을 활성화하여 스왑 메모리로 사용합니다.
  • echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 스왑 파일을 재부팅 시에도 자동으로 활성화되도록 /etc/fstab 파일에 추가합니다.

스왑 메모리가 정상적으로 반영되었는지 확인합니다. 스왑 메모리가 2GB 생성된 것을 확인할 수 있습니다. (명령어: free -m)

HTTPS 적용하기

https는 전송되는 데이터와 수신되는 데이터가 모두 암호화되어 데이터를 안전하게 보호할 수 있습니다. 또한 웹사이트가 인증되어 사용자에게 신뢰할 수 있도록 해줍니다.

DNS 레코드 설정

먼저 시작에 앞서 가비아에서 도메인을 구입하여 줍니다. 저는 가난한 학생이기에 이벤트 500원짜리를 구입하겠습니다.

가비아에서 도메인을 구입했다면 다시 AWS 검색창에서 Route53을 검색하여 접속합니다.

좌측 탭에서 호스팅 영역을 선택하고 호스팅 영역 생성 버튼을 클릭합니다.

다음과 같이 도메인 이름에 구매한 도메인을 적어주고 유형은 퍼블릭 호스팅 영역을 선택하여 줍니다. 구성을 마쳤으면 호스팅 영역 생성 버튼을 클릭합니다.

호스팅 영역 생성이 완료되면 레코드 생성 버튼을 클릭합니다.

다른 부분들은 모두 그대로 두고 본인 EC2 서버의 탄력적 IP 주소를 값 부분에 입력합니다. 입력을 마쳤으면 레코드 생성 버튼을 클릭합니다.

이로써 레코드 세트가 총 3개가 되었습니다.

이번에는 가비아에 로그인 후 접속하여 My 가비아 탭을 선택합니다. 그리고 순서대로 도메인 -> 구매한 도메인 관리 버튼을 클릭하여 도메인 관리 페이지로 접속합니다.

이제 이곳에서 네임서버를 등록해야 합니다. 다음과 같이 네임서버 설정 버튼을 클릭합니다.

이곳에 앞서 Route53의 호스팅 영역 NS 레코드의 값/트래픽 라우팅 대상에 해당하는 값들을 입력해 주어야 합니다. 다음과 같이 위에서부터 하나 하나 그대로 입력해 줍니다. 단, 마지막에 점(.)은 지우고 입력합니다.

다 입력했으면 소유자 인증을 마치고 적용 버튼을 눌러줍니다.

NGINX, JDK-21 설치

EC2 인스턴스에 접속하여 NGINX와 JDK-21을 설치해봅시다.

nginx 설치

sudo dnf update -y
sudo dnf install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx  

sudo dnf install -y java-21-amazon-corretto-devel jdk-21 설치

설치하면서 나오는 질문에는 y를 눌러 넘어갑니다.

java --version 명령으로 설치된 자바를 확인할 수 있습니다.

다음 명령어로 Nginx를 실행합니다.
sudo systemctl start nginx.service

Nginx가 정상 구동 중인지 확인하고 싶다면 다음 명령어를 입력합니다.
systemctl status nginx.service

Proxy Server 구성

포트번호가 80인 http에서 요청이 오면, Spring Boot 프로젝트에서 8080번 포트를 바라볼 수 있도록 proxy 설정을 해주겠습니다.

먼저 요청에 대한 로그와 에러를 기록할 수 있는 디렉토리를 생성해 주겠습니다.
sudo mkdir /var/log/nginx/proxy/
다음으로 proxy 관련 설정을 진행합니다. 먼저 다음 명령어로 proxy_params라는 이름의 파일을 생성합니다.
sudo vi /etc/nginx/proxy_params
그리고 다음 내용을 proxy_params에 복사하여 추가합니다.
vi 편집기에서는 i를 눌러서 편집 모드로 들어가고 내용을 입력 후 esc로 빠져나와 :wq로 저장하고 나옵니다.

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;

client_max_body_size 256M;
client_body_buffer_size 1m;

proxy_buffering on;
proxy_buffers 256 16k;
proxy_buffer_size 128k;
proxy_busy_buffers_size 256k;

proxy_temp_file_write_size 256k;
proxy_max_temp_file_size 1024m;

proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
proxy_intercept_errors on;

다음 명령어를 입력하여 Nginx 설정 파일을 열어보겠습니다.
sudo vi /etc/nginx/nginx.conf
다음과 같이 아래로 내리다가 보면 server 블록을 찾을 수 있습니다. 해당 부분에서 다음 내용들을 수정할 것입니다.

  • server_name에 도메인 입력
  • reverse proxy 설정
  • CORS 설정

변경 및 추가할 부분은 다음과 같이 총 세 부분입니다.
첫 번째 부분은 등록한 도메인 명으로 요청이 들어올 때 처리하는 부분이고,

두 번째 부분은 접근 로그와 에러 로그를 기록하는 부분입니다.

마지막 세 번째 부분은 Spring Boot 어플리케이션과 실제 연결되는 부분으로, 먼저 해당 location 블록에서 요청을 처리하고, 하단의 CORS 관련 헤더를 추가한 이후에, Spring Boot가 실행 중인 8080 포트로 요청을 전달합니다.

server {
        listen       80;
        listen       [::]:80;
        server_name  {{등록한 도메인 입력}}
        root         /usr/share/nginx/html;

        access_log   /var/log/nginx/proxy/access.log;
        error_log    /var/log/nginx/proxy/error.log;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / { # location 블록
                include /etc/nginx/proxy_params;
                proxy_pass http://{{서버의 Public IP}}:8080;    # reverse proxy의 기능
                if ($http_x_forwarded_proto = 'http') {
                        return 301 https://$host$request_uri;
                }
                # CORS 설정
                if ($request_method = 'OPTIONS') {
                        add_header 'Access-Control-Allow-Origin' 'http://localhost:3000'; # 프론트엔드 주소
                        add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, OPTIONS, PUT, DELETE';
                        add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
                        add_header 'Access-Control-Allow-Credentials' 'true';
                        add_header 'Access-Control-Max-Age' 1728000;
                        add_header 'Content-Type' 'text/plain charset=UTF-8';
                        add_header 'Content-Length' 0;
                        return 204;
                }
                add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always; # 프론트엔드 주소
                add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, OPTIONS, PUT, DELETE' always;
                add_header 'Access-Control-Allow-Credentials' 'true' always;
                add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range' always;
        }
    }

위에서 주의할 점은 추후 프론트엔드와 통신할 때 CORS 설정 중 Access-Control-Allow-Origin 부분을 와일드 카드(모두 허용)를 뜻하는 *이 아닌 프론트엔드 주소를 입력해야 한다는 것입니다.

쿠키를 이용한 통신에서는 사이트 간 통신에서 와일드 카드가 허용되지 않기 때문입니다. 프론트엔드 배포가 완료되면 배포 주소로 변경합니다.

또 주의할 점은 Nginx에서 CORS 관련 헤더를 추가했으면 Spring Boot에서는 CORS 처리를 하면 안된다는 점입니다. CORS 처리는 헤더를 추가하는 과정이기 때문에 두 곳에서 같은 헤더를 추가하면 충돌이 발생하기 때문입니다.

Nginx 설정을 수정했기 때문에 먼저 Nginx 문법에 오류가 없는지 확인하는 명령어로 테스트를 진행합니다.
sudo nginx -t

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

위와 같은 문구가 나온다면 정상인 것입니다.
문제가 없다면 다음 명령어를 통해 설정을 재반영하여 재실행해 줍니다.
sudo systemctl restart nginx.service

Let's encrypt SSL 인증서 발급(HTTPS 적용)

certbot 설치

Let's encrypt 기관이 제공하는 certbot (certification robot) 데몬을 이용하면 인증서 발급, 인증서 설치, 인증서 갱신이 모두 자동으로 처리됩니다.

SSL 인증서는 90일마다 갱신되어야 하는데, 이 갱신 작업도 certbot에 의해 자동으로 처리됩니다.

아래와 같은 명령어로 cerbot을 설치합니다.

sudo dnf install -y certbot python3-certbot-nginx Certbot 및 Certbot Nginx 플러그인 설치

다음 명령어로 암호화된 https 프로토콜로만 도메인에 접근할 수 있도록 설정하여 줍니다.

sudo certbot --nginx -d junyeong.store 본인 도메인 입력

원한다면 갱신 알림과 보안 문제 등을 위한 이메일을 입력해 주고, 나머지 부분도 모두 yes를 입력합니다. 정상적으로 인증서가 발급되면 마지막 부분에 축하한다는 문구가 뜹니다.

Congratulations! You have successfully enabled https://junyeong.store

참고
Dockerdocs
배포의 정석
Docker를 활용해 AWS EC2에 Spring Boot 배포하기 (feat. Nginx)
AWS + Docker 배포

profile
꾸준하게

0개의 댓글