Deploy: Github Actions를 이용한 AWS Lightsail 배포 자동화 구성 정리
🎯핵심 목표
1. AWS Lightsail 환경에서 웹 애플리케이션을 배포 가능한 형태로 구성하는 방법을 정리한다.
2. GitHub Actions를 이용해 코드 푸시부터 서버 반영까지 자동으로 배포되는 파이프라인을 구축하는 과정을 기록한다.
3. 실제로 설정하면서 마주쳤던 오류와 트러블슈팅 과정을 정리해, 다시 설정할 때 참고할 수 있는 가이드를 만든다.
4. EC2 대비 Lightsail + GitHub Actions 조합을 사용해본 소감과 활용 팁을 공유한다.
1. AWS Lightsail
AWS Lightsail은 가상 서버(VPS)를 쉽고 빠르게 구축하고 관리할 수 있도록 만든 AWS의 입문용 클라우드 서비스입니다. 클라우드에 대한 전문 지식이 없는 사용자도 웹사이트나 애플리케이션을 운영하는 데 필요한 핵심 기능(가상머신, 스토리지, 네트워크, 로드밸런서, DNS 관리 등)을 포함한 '묶음'을 저렴한 월정액으로 쉽게 이용할 수 있습니다.

AWS LightSail 특징
Lightsail의 가장 큰 특징은 간단하고 무료입니다. AWS EC2에 비해 훨씬 간단하게 인스턴스 생성할 수 있으며, 워드프레스, Node.js 등 미리 구성된 플랫폼을 선택해 바로 사용할 수 있습니다. 뿐만 아니라 스토리지, 로드 밸런서, DNS 관리, 고정 IP 등 서비스를 구성하는 데 필요한 주요 기능이 하나의 서비스에 통합되어 있습니다. 학습 목적, 개인 혹은 소규모 프로젝트 기준으로 사용하는 데 적절하다고 볼 수 있습니다.
명확한 장점만큼 단점도 매우 명확합니다. 보안 그룹과 규칙 설정이 매우 미흡하고, 일부 네트워크 설정은 Lightsail의 자동 설정을 따라야 합니다. 그래서 AWS를 깊게 공부하거나 다양하고 구체적인 설정 환경을 원한다면 Lightsail은 부적절합니다. 그래서 트래픽이나 리소스가 급격히 증가할 것으로 예상되는 애플리케이션에는 적합하지 않을 수 있으며, 서비스 사용에 대한 여러 제한 사항이 있습니다. 사전 구성된 묶음 상품을 사용하기 때문에 세부적인 설정을 원하는 경우 제약이 있을 수 있습니다.
AWS LightSail 구성 요소

1. 인스턴스 (Instance)
AWS Lightsail에서 제공하는 가상 서버로, 리눅스(Ubuntu, Debian, CentOS 등)나 Windows 기반 이미지를 선택해 생성합니다. 웹 서버, 백엔드 애플리케이션, DB 서버 등으로 활용할 수 있고, EC2보다 단순한 설정으로 빠르게 띄우는 데 초점이 맞춰져 있습니다.
2. 플랜 (Plan)
CPU(vCPU), 메모리(RAM), SSD 스토리지 용량, 월 데이터 전송량(트래픽) 조합으로 미리 정해진 플랜을 고르는 방식입니다.
- 월 $3.5 정도의 저렴한 플랜부터 시작
- 비용이 정액제에 가까워서, EC2처럼 요금 계산이 복잡하지 않고 예측 가능한 비용 구조를 제공합니다.
개인 프로젝트나 소규모 서비스에 적합하다는 장점이 있습니다.
3. 앱/OS (Blueprint: App + OS 또는 OS Only)
인스턴스 생성 시 선택하는 이미지입니다. 워드프레스, LAMP 스택, Node.js, Nginx, Django, Joomla 등 미리 구성된 애플리케이션 번들 또는 Ubuntu, Debian 같은 순수 OS(Operating System Only) 바로 웹 서비스를 띄우고 싶다면 번들을, 직접 환경을 구성하고 싶다면 OS Only를 선택하면 됩니다.
4. 리전 및 가용 영역 (Region & Availability Zone) Lightsail 인스턴스를 어느 리전에 둘지 선택합니다.
- 예: 서울(ap-northeast-2) 리전 선택
- 지연 시간(latency)과 사용자 위치, 그리고 다른 AWS 리소스와의 연계를 고려해서 정합니다.
EC2처럼 세밀한 AZ 관리보다는, “어느 리전에 둘 것인가”에 더 초점이 맞춰져 있다고 보면 됩니다.
5. 네트워킹/IP
Lightsail는 다음과 같은 네트워크 관련 기능을 제공합니다.
- 고정 IP(Static IP): 인스턴스를 재부팅해도 IP가 바뀌지 않도록 고정 IP를 할당
- DNS 관리: Lightsail 내에서 도메인과 레코드를 관리할 수 있는 간단한 DNS 기능 제공
- 로드 밸런서(Lightsail Load Balancer): 여러 인스턴스 앞단에 로드 밸런서를 두어 트래픽 분산 및 헬스 체크 가능
- 방화벽 규칙: 포트(HTTP 80, HTTPS 443, SSH 22 등)를 인바운드 규칙으로 열어주는 간단한 네트워크 보안 설정
6. 스토리지
Lightsail는 SSD 기반 블록 스토리지를 제공합니다.
- 인스턴스에 추가 디스크(Attached Disk)를 붙여서 확장 가능
- 스냅샷(Snapshot) 기능으로 인스턴스나 디스크의 시점을 백업해 둘 수 있고, 이를 기반으로 새 인스턴스를 생성할 수도 있습니다. 백업/롤백 포인트를 만들 때 유용합니다.
7. 관리 & 모니터링
- 모니터링: CPU 사용량, 네트워크, 디스크 사용량 등의 기본 메트릭을 Lightsail 콘솔에서 그래프로 확인 가능
- 알람(Alerts): 특정 임계값(예: CPU 80% 이상 지속) 기준으로 알람을 설정할 수 있음
- SSH 접속: 브라우저에서 바로 SSH 접속(Web-based SSH)도 지원해서, 키 파일 없이 빠르게 접속할 수 있음
AWS LightSail 인스턴스 생성
1. 인스턴스 생성 버튼 클릭과 함께 시작합니다.

2. 인스턴스 가용 영역은 서울(ap-norteast-2)으로 선택합니다.

3. 원하는 인스턴스 이미지를 선택합니다.
저는 Ubuntu 22.04를 선택했습니다.

4. 선택 사항에서 SSH 키를 생성합니다.
현재 생성 중인 인스턴스에 팀원 혹은 로컬 터미널로 SSH 연결하기 위해 필요한 키를 생성해야 합니다.
시작 스크립트, 자동 스냅샷은 설정하지 않은 상태입니다.



5. 인스턴스 플랜을 선택합니다.
90일 무료 사용으로 듀얼 스택에서 월별 $12 플랜으로 선택합니다.

6. 인스턴스 이름을 지정하고 생성합니다.
인스턴스 식별을 위한 이름을 생성합니다. 새 태그를 작하지 않고 생성했습니다.

AWS LightSail 인스턴스 관리
스냅샷, 스토리지 구성을 하지 않은 상태로 인스턴스를 생성했습니다.
따라서 일부 설명은 생략하고, 유용한 내용을 다루도록 할게요.
1. 생성된 인스턴스를 클릭합니다.

2. 인스턴스에 연결합니다.
SSH를 사용하여 연결을 클릭하면 생성된 인스턴스에 접근할 수 있습니다.
여기에서 원하는 모든 작업을 수행할 수 있습니다.


3. 지표 확인하기
90일 무료 플랜의 경우 CPU 버스트를 지원합니다. CPU 버스트 데이터를 모두 소진하더라도, 스프링 부트 애플리케이션과 도커 이미지로 실행된 레디스 운영이 가능합니다. CPU 용량, 네트워크 트래픽을 살펴볼 때 상당히 도움되는 지표를 제공하고 있습니다.

4. 네트워킹 확인하기
규칙 추가를 통해 애플리케이션 포트 30000을 사용하도록 설정합니다.
듀얼 스택으로 생성된 인스턴스는 IPv6를 지원하지 않기 때문에 IPv4 방화벽에서만 규칙을 작성하면 됩니다.
⭐ 추가 정보
AWS EC2와 동일하게, Lightsail 인스턴스를 만들면 퍼블릭 및 프라이빗 IPv4 주소가 할당됩니다. 퍼블릭 IP 주소는 인터넷에 액세스할 수 있지만, 프라이빗 IP 주소는 동일한 AWS 리전에 있는 Lightsail 계정의 리소스에만 액세스할 수 있습니다.
가장 아래로 내려보면 로드 밸런싱, 배포 설정도 있습니다. 배포할 애플리케이션은 모놀리식 구조의 프로젝트이므로 넘어갑니다.


5. 도메인 확인하기
HTTPS를 사용하기 위해 반드시 도메인이 필요합니다. 1년 단위로 15$ 가격에 구매 가능합니다.
HTTPS 설정은 당장 다루지 않을게요.
⭐ 추가 정보
도메인 가격이 부담되는 경우에는 25.12.07 기준으로 가비아(Gabia)에서 500원에 판매하는 도메인을 고려해볼 수 있어요.

6. 인스턴스 기록 확인하기
생성된 인스턴스 이력을 간단하게 살펴볼 수 있습니다.

AWS LightSail 데이터베이스 생성
1. 데이터베이스 생성하기
MySQL 8.0.44 선택합니다.
로그인 자격 증명과 마스터 데이터베이스 이름은 모두 기본값 그대로 사용해도 되고, 원하는 값으로 수정 가능합니다.


2. 플랜 선택하기
무료 플랜을 선택하고 생성합니다.

AWS LightSail 데이터베이스 관리
1. 데이터베이스 정보 확인하기
데이터베이스에 접속 가능한 엔드포인트와 사용자 이름 및 암호를 확인합니다. 최초 생성 시 암호 변경을 합니다.
⭐ 추가 정보
나머지는 항목들은 인스턴스와 유사하므로 생략합니다.

AWS LightSail 버킷
1. 버킷 생성하기
버킷 위치 서울로 선택하고, 가장 저렴한 플랜을 선택 후 생성합니다.


2. 버킷 객체하기
버킷에 파일을 업로드하면 객체에서 확인할 수 있습니다.

3. 버킷 권한 설정하기
권한은 개별 객체를 퍼블릭 및 읽기 전용으로 설정 가능으로 하고, 액세스 키를 생성합니다.
생성된 액세스 키의 보안 액세스 키는 최소 생성 시 한 번만 보이므로 키 관리에 주의합니다.

2. Github Actions
GitHub Actions는 코드 저장소(repository)에서 CI/CD(지속적 통합/지속적 배포) 파이프라인을 자동화하는 플랫폼입니다. 리포지토리에서 발생하는 이벤트(예: 코드 푸시, 풀 리퀘스트)에 따라 빌드, 테스트, 배포 등 복잡한 워크플로를 자동으로 실행할 수 있습니다.
주요 구성 요소
- 워크플로(Workflow): 자동화할 전체 프로세스로, `github/workflows` 폴더에 있는 YAML(.yml) 파일에 정의됩니다.
- 이벤트(Event): 워크플로를 시작하는 특정 활동입니다. 예를 들어, push 또는 pull_request 가 있습니다.
- 작업(Job): 러너라는 가상 머신 또는 컨테이너에서 실행되는 하나의 처리 단위입니다. 워크플로 내에서 여러 작업이 순차적 또는 병렬로 실행될 수 있습니다.
- 단계(Step): 작업 내에서 실행되는 가장 작은 단위로, 셸 명령을 실행하거나 액션을 사용합니다.
- 액션(Action): 재사용 가능한 코드 단위로, 특정 작업을 쉽게 수행하도록 도와줍니다. GitHub Marketplace에서 다양한 액션을 찾아 사용할 수 있습니다.
Github Actions 공식 문서에서 자세한 내용을 살펴볼 수 있습니다.
GitHub Actions 이해 - GitHub 문서
GitHub Actions는 빌드, 테스트 및 배포 파이프라인을 자동화할 수 있는 CI/CD(연속 통합 및 지속적인 업데이트) 플랫폼입니다. 리포지토리에 대한 모든 끌어오기 요청을 빌드 및 테스트하거나 병합된
docs.github.com
CI/CD 단어 이해
CI/CD는 앱 개발 단계에 자동화를 통합하는 앱 제공 방식으로, 지속적 통합/지속적 제공 또는 배포를 뜻합니다. 애자일 개발 방식에서 발전한 CI/CD는 운영 원칙을 포함하는 포괄적인 용어이며, Devops 팀 혹은 관련 업무 담당자가 코드 변경을 앱에 쉽고 빠르게 구현하도록 지원합니다.
파이프라인(pipeline) 이란?
“여러 작업을 정해진 순서대로 자동으로 흘러가게 해 둔 과정”을 개발 쪽에서는 보통 파이프라인(pipeline) 이라고 부릅니다.조금 더 풀어 말하면, "빌드하고 → 테스트하고 → 배포하는 흐름" 또는 "코드를 분석하고 → 패키징하고 → 이미지 만들고 → 서버에 올리는 흐름" 이렇게 “연속된 작업 단계들”을 하나의 라인(line) 으로 묶어 놓은 것을 파이프라인이라고 생각하시면 돼요.
CI/CD의 배경
CI/CD는 긴 개발 주기와 수작업 프로세스로 인해 개발 팀과 운영 팀 간에 지연, 오류, 불만이 종종 발생하는 기존 소프트웨어 개발과 관련된 많은 과제에 직접 대응합니다. CI/CD 접근 방식은 코드 변경에서부터 프로덕션 배포에 이르기까지 전체 소프트웨어 제공 파이프라인을 자동화하고 간소화하여 공동 작업 문화를 촉진하고 효율성과 안정성을 높이는 것을 목표로 합니다. 이를 위해 CI/CD는 지속적 통합(CI), 지속적 제공/지속적 배포(모두 CD로 지칭), 지속적 테스트(CT)에 의존합니다.
CI(Continuous Integration, 지속적 통합)이란?
“코드가 바뀔 때마다 자동으로 빌드·테스트해서, 항상 잘 돌아가는 코드 상태를 유지하는 것”
지속적 통합은 CI/CD의 첫 번째 요소이며, 일반적으로 하루에 여러 번 코드 변경을 공유 버전 관리 리포지토리에 자동으로 자주 통합하는 데 중점을 둡니다. 각 통합은 자동화된 빌드 및 테스트 프로세스를 트리거하여 개발 주기 초기에 문제를 감지합니다. CI는 서로 다른 팀 구성원의 코드 변경으로 인한 충돌 없이 코드 품질이 유지되도록 보장하여 개발자가 코드베이스의 안정성을 방해하지 않고 동시에 작업할 수 있도록 합니다.
CD(Continuous Delivery / Deployment, 지속적 제공 / 지속적 배포)이란?
“CI를 통과한 코드를 자동으로 배포 가능한 상태까지 가져가거나, 실제 서버에까지 계속 배포해버리는 것”
지속적 제공은 자동화된 테스트를 통과한 후 코드 변경 사항을 다양한 환경(예: 스테이징 또는 테스트)에 자동으로 제공하여 CI 프로세스를 확장합니다. 지속적 제공은 변경 사항을 프로덕션에 자동으로 푸시하지 않습니다. 대신, 변경 사항이 사용자에게 배포되기 전에 일종의 수동 개입에 의존하는 통제된 릴리스 프로세스를 제공합니다. 이렇게 하면 소프트웨어 업데이트가 항상 배포 가능한 상태로 유지되므로, 언제든지 릴리스할 수 있습니다. 지속적 제공을 통해 조직은 배포 관련 문제의 위험을 줄이는 동시에 고품질의 소프트웨어의 빠른 릴리스를 촉진합니다.
지속적 배포란?
지속적 배포는 수동 개입 없이 자동화된 테스트를 통과하는 즉시 코드 변경 사항을 프로덕션에 자동으로 배포하여 자동화 수준을 더욱 높입니다. 이 접근 방식은 신속하고 빈번한 릴리스를 목표로 하는 조직에 적합합니다. 자동화된 거버넌스와 자동화된 테스트 및 신뢰할 수 있는 인프라에 대한 높은 수준의 확신이 필요하지만 지속적인 배포를 통해 코드 변경과 최종 사용자의 가용성 사이의 리드 타임을 크게 줄일 수 있습니다.
지속적 테스트란?
지속적 테스트는 CI/CD의 마지막 측면으로, 개발 프로세스 전반에 걸쳐 자동화된 테스트가 일관되게 실행되도록 보장합니다. 여기에는 코드 품질과 운용성을 검증하기 위한 단위, 통합, 기능 및 성능 테스트가 포함됩니다. CT는 개발자에게 실시간 피드백을 제공하여, 개발자가 문제를 즉시 식별하여 해결하도록 돕고 애플리케이션의 발전에 따라 신뢰성과 안정성을 유지관리하도록 지원합니다. 오늘날 대부분의 CI/CD 파이프라인에는 보안 검사와 함께 테스트 프로세스가 포함됩니다.
종합하자면, CI/CD는 개발자가 높은 수준의 자동화와 모니터링을 활용하여 앱 개발을 개선할 수 있는 시스템을 생성합니다. CI는 “코드 품질과 통합”에 초점, CD는 “배포 자동화”에 초점이 있어요.
CI/CD 전체 흐름 이해

코드가 한 번 올라갔을 때 실제로 일어나는 일들
Spring Boot나 MSA 프로젝트를 예로 들면, 보통 이런 흐름이 만들어집니다.
- 개발자가 feature/group-api 같은 브랜치에서 기능을 개발합니다.
- 작업을 마치고 원격 저장소에 push를 하거나, Pull Request(PR) 를 생성합니다.
- 이 순간, GitHub Actions / GitLab CI / Jenkins 같은 CI 도구가 이벤트를 감지합니다.
- 설정해 둔 스크립트에 따라 빌드와 테스트가 자동으로 실행됩니다.
- 빌드나 테스트가 실패하면 PR에 ❌ 가 표시되고, 성공하면 ✅ 가 표시됩니다.
- CI를 통과한 코드만 main 브랜치에 병합하도록 팀 규칙을 세우면, main은 항상 “빌드 가능 + 테스트 통과” 상태를 유지하게 됩니다.
- 여기까지가 CI의 영역이고, 이후에는 이 결과물을 가지고 CD 단계가 이어집니다.
CD 단계에서는 이렇게 처리될 수 있습니다.
- CI에서 통과된 결과물(JAR, Docker 이미지 등)을 스테이징 혹은 운영 환경에 자동으로 배포 준비하거나,
조건이 맞으면 실제 운영 서버까지 자동 배포까지 이어집니다.
결국 한 줄로 요약하면, 코드 푸시 → CI(빌드/테스트) → CD(배포 준비 또는 자동 배포) 라는 흐름으로 이해할 수 있습니다.
CI(지속적 통합)는 무엇을 해결해 주는가
CI의 핵심 목적은 “문제를 최대한 빨리, 작은 단위에서 발견하는 것”입니다. 여기서 말하는 문제는 빌드 실패, 테스트 실패, 의존성 오류, 형식 오류 등 코드 품질 전반을 포함합니다. CI를 적용하지 않았을 때 생기는 문제들이 있습니다.예전 방식처럼 각자 몇 주씩 브랜치에서 개발만 하다가, 나중에 한 번에 main에 합치려 하면 다음과 같은 일이 자주 생깁니다.
- 서로 다른 브랜치에서 변경한 코드가 한꺼번에 섞이면서 충돌(Conflict)이 크게 발생
- 어느 시점부터 빌드가 깨졌는지 파악하기 어려움
- 테스트를 돌려 보니, 실패하는 테스트가 너무 많아서 원인 추적이 지옥이 됨
이른바 “통합 지옥(Merge Hell)”이라고 부르는 상황입니다. 이때 CI를 도입하면 통합 방식이 완전히 바뀝니다. 코드를 조금씩, 자주(main 기준으로 짧은 주기) 통합합니다. 통합될 때마다 자동으로 빌드와 테스트가 돌기 때문에, 문제가 생기면 “어느 PR, 어느 커밋에서 깨졌는지”를 아주 쉽게 알 수 있습니다. 결과적으로 main 브랜치는 항상 배포 가능한 상태에 최대한 가깝게 유지됩니다. CI 파이프라인에서는 보통 다음과 같은 작업들이 자동으로 실행됩니다.
- 프로젝트 빌드 (예: ./gradlew clean build)
- 단위 테스트 / 통합 테스트 실행
- 코드 스타일 및 정적 분석(선택)
- 간단한 품질 기준 체크(예: 테스트 통과 여부, 특정 커버리지 기준 등)
이 중 빌드 + 테스트만 있어도 충분히 “CI”라고 부를 수 있으며, 처음에는 이 정도부터 시작해도 매우 큰 효과를 얻을 수 있습니다.
CD(지속적 제공 / 배포)는 어디서부터 시작되는가
CI가 끝나면, 이제 “이 검증된 코드를 실제 서비스 환경으로 어떻게 가져갈 것인가”가 남습니다. 이 부분을 담당하는 개념이 CD(Continuous Delivery / Deployment) 입니다. 사실 CD는 지속적 제공과 지속적 배포 두 가지 단계로 나눌 수 있습니다.
1. Continuous Delivery(지속적 제공)
지속적 제공은 간단히 말해, “언제든지 배포 버튼만 누르면 바로 배포할 수 있는 상태까지 자동으로 만들어 두는 것”입니다. 여기까지는 주로 다음과 같은 작업들이 자동화됩니다.
- CI가 끝난 아티팩트(JAR, Docker 이미지)를 빌드
- 레지스트리(e.g. Docker Registry)에 이미지 푸시
- 스테이징 환경에 자동 배포
- 배포 스크립트, Helm 차트, ECS/EKS 설정 등을 미리 준비해 두는 것
운영 배포는 보통 사람이 마지막 승인 또는 버튼 클릭으로 진행합니다. 즉, “배포 직전까지는 자동, 실제 배포 시작은 사람이 결정”하는 형태입니다.
2. Continuous Deployment(지속적 배포)
지속적 배포는 여기서 한 발 더 나아갑니다. “조건을 만족하면, 운영 환경에 배포하는 것까지도 자동으로 진행하는 것”입니다. 예를 들어, 다음과 같은 흐름이 가능합니다.
- main 브랜치에 PR이 머지됨
- CI(빌드/테스트)가 모두 통과
- 품질 기준을 만족했는지 체크
- 모두 만족하면 Kubernetes/ECS/서버 등에 자동으로 배포 실행
- Blue-Green 배포나 Canary 배포 전략과 연계해 점진적으로 트래픽을 전환
이 단계는 당연히 테스트, 모니터링, 롤백 전략이 어느 정도 갖춰져 있어야 안전하게 도입할 수 있습니다.
정리하자면 다음과 같습니다.
- Continuous Delivery → 배포 직전 상태까지 자동화, 최종 배포 시작은 사람이 버튼/승인
- Continuous Deployment → 조건 충족 시 운영 배포까지 완전히 자동화
둘 다 “CD”라고 부르기 때문에 혼동되지만, “어디까지 자동으로 할 것이냐”의 범위가 다른 개념이라고 보면 이해하기 쉽습니다. CI/CD 편의성을 위해 다양한 벤더가 제공하는 관리형 CI/CD 툴이 있습니다. 주요 퍼블릭 클라우드 공급업체는 모두 GitLab, CircleCI, Travis CI, Atlassian Bamboo, Red Hat OpenShift 등과 함께 CI/CD 솔루션을 제공합니다.
지금까지 내용을 한 문단으로 다시 정리해 보면 다음과 같습니다.
CI(Continuous Integration) 는 코드가 변경될 때마다 자동으로 빌드와 테스트를 수행해서 항상 통합 가능한 깨끗한 코드 상태를 유지하는 과정이고, CD(Continuous Delivery / Deployment) 는 이렇게 검증된 코드를 배포 가능한 상태 또는 실제 운영 환경까지 빠르고 일관되게 전달하는 자동화 과정입니다. Delivery는 “배포 직전까지 자동”, Deployment는 “운영 배포까지 자동”이라는 차이를 가지고 있습니다.
⭐ 추가 정보
카카오엔터프라이즈가 GitHub Actions를 사용하는 이유 - tech.kakao.com
- 카카오엔터프라이즈 aaron, greta가 함께 작성하였습니다. GitHu...
tech.kakao.com
Github Actions 이해하기
Github Actions를 어렵게 생각하기보다, “내가 정해 둔 로직을 대신 실행해 주는 일종의 컴퓨터”라고 이해하면 훨씬 편합니다.
실제로, 이어서 설명하게 될 워크플로우라는 Github Actions 스크립트를 실제 컴퓨터가 실행합니다.

CI/CD 구성 방식은 팀마다, 프로젝트마다 조금씩 다르지만, 크게 보면 대부분 아래와 비슷한 흐름을 갖습니다.
- 코드 작성 후 Commit
먼저 로컬 환경에서 코드를 수정하고, 변경 내용을 Git에 커밋합니다. 이 단계까지는 기존 개발 과정과 완전히 동일합니다. - Github에 Push
커밋한 내용을 원격 저장소(Github)에 push합니다.
이 순간이 CI/CD 파이프라인이 시작될 수 있는 트리거 이벤트가 됩니다. - Push를 감지해서 Github Actions 워크플로우 실행
Github는 “특정 브랜치로 push가 발생하면 Github Actions를 실행하라”는 설정을 보고, 해당 이벤트를 감지하는 즉시 작성해 둔 Github Actions 워크플로우를 실행합니다. 워크플로우 안에서는 일반적으로 다음과 같은 단계들이 순서대로 진행됩니다.- 빌드(Build): 프로젝트를 실제로 컴파일하고, 애플리케이션을 실행 가능한 상태로 만듭니다.
- 테스트(Test): 단위 테스트, 통합 테스트 등을 자동으로 실행하여 코드에 문제가 없는지 검증합니다.
- 배포(Deploy): 테스트까지 통과했다면, 배포 스크립트를 통해 서버나 클라우드 환경으로 새 코드를 올립니다.
- 서버에서 최신 코드로 재실행 배포가 완료되면, 서버는 배포된 최신 코드 기준으로 애플리케이션을 재실행하게 됩니다.
예를 들어 Docker를 사용한다면,- 새로운 이미지로 컨테이너를 다시 띄우거나
- 기존 컨테이너를 내려고 새 버전으로 교체하는 식으로 애플리케이션이 최신 상태로 동작하도록 만들어 줍니다.
AWS EC2 설정하기
1. 인스턴스 생성 및 보안그룹 포트 규칙 설정하기
생성된 인스턴스의 보안그룹에서 애플리케이션 포트를 개방하도록 규칙을 생성합니다.

2. JDK 설치하기
sudo apt update # apt 업데이트
sudo apt install openjdk-21-jdk -y # 자바 21 설치
java -version # 설치 확인
3. git clone으로 프로젝트 다운로드하기
git clone {리포지토리 클론 주소 입력}
만약 Private Repository를 clone할 경우에는 Github의 계정과 비밀번호를 입력해야만 clone 할 수 있습니다. 비밀번호는 실제 Github 계정의 비밀번호가 아니라 Github 토큰을 입력해야 합니다. 토큰을 발급받는 방법은 아래 링크를 참고하시면 됩니다.
github 토큰 발급하기
위와 같이 private으로 된 repository를 다운받으려고 하면 github의 아이디와 비밀번호를 입력하라고 한다. 그런데 이걸 맨날 private에서 public으로 바꿔서 하기엔 너무 귀찮다. 실제로도 그렇게 어렵지
velog.io
4. clone 받은 프로젝트 실행하기
cd {프로젝트경로}
./gradlew clean build # 테스트 제외: ./gradlew clean build -x test
cd build/libs
nohup java -jar {스냅샷파일명}.jar &
sudo lsof -i:30000 # 30000 포트에서 Spring Boot가 실행되는지 여부 확인
5. (추가정보) merge 이후 gut pull하기
clone 이후에 해당 리포지토리에 최신 코드들이 반영되는 경우에는 git pull을 통해 최신 코드를 다운로드합니다.
그리고 다시 빌드 후 재배포하면 됩니다.
cd {프로젝트경로}
git pull origin main
sudo lsof -i:8080 # 30000 포트에 Spring Boot가 실행되고 있는지 확인
sudo fuser -k -n tcp 8080 # 30000 포트에 실행되고 있는 프로세스 종료
./gradlew clean build # 테스트 제외: ./gradlew clean build -x test
cd build/libs
nohup java -jar {스냅샷파일명}.jar &
6. (추가정보) github 계정과 비밀번호 등록하기
private repository의 경우에는 배포를 할 때마다 Github 계정과 비밀번호를 일일이 치는 과정이 포함되어 있으면 배포를 자동화할 수가 없습니다. 따라서 최초 한 번만 작성하고 그 이후에는 Github 계정과 비밀번호를 입력하지 않아도 됩니다.
git config --global credential.helper store
git pull origin main
# Github 계정 및 비밀번호 입력
git pull origin main # 더 이상 비밀번호를 안 묻는 걸 확인할 수 있다.
해당 방식은 ~/.git-credentials에 로그인 정보를 저장해둠으로써 github 계정과 비밀번호를 따로 묻지 않는 방식입니다.
인스턴스에 접근할 수 있는 모든 사용자가 내 Github 정보를 볼 수 있다는 점이 단점입니다.
Git, Pull/Push할 때 id password 묻지 않게 하기
배경
blog.pinedance.click
7. (추가정보) Docker 설치하기
##############################################
# 0. (선택) 기존에 깔려 있을 수 있는 Docker / Compose 지우기
##############################################
sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc || true
##############################################
# 1. 기본 패키지 업데이트 + 필수 패키지 설치
##############################################
sudo apt update
sudo apt install -y ca-certificates curl
##############################################
# 2. Docker 공식 GPG key & repo 등록
# (Ubuntu 22.04 = jammy 기준)
##############################################
# keyrings 디렉토리 생성
sudo install -m 0755 -d /etc/apt/keyrings
# Docker GPG key 다운로드
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
# key 파일 권한 조정
sudo chmod a+r /etc/apt/keyrings/docker.asc
##############################################
# 3. docker.sources 정리 및 재생성 (Ubuntu 22.04 = jammy)
##############################################
# 3-1) 기존 docker.sources 삭제 (없어도 에러 무시)
sudo rm /etc/apt/sources.list.d/docker.sources 2>/dev/null || true
# 3-2) 올바른 내용으로 다시 생성
# Ubuntu 22.04 → Suites: jammy
printf 'Types: deb\nURIs: https://download.docker.com/linux/ubuntu\nSuites: jammy\nComponents: stable\nSigned-By: /etc/apt/keyrings/docker.asc\n' \
| sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null
##############################################
# 4. 패키지 목록 업데이트
##############################################
sudo apt update
##############################################
# 5. Docker 엔진 + Compose 플러그인 설치
##############################################
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
##############################################
# 6. Docker 데몬 자동 시작 설정 + 현재 바로 시작
##############################################
sudo systemctl enable --now docker
##############################################
# 7. docker 그룹에 사용자 추가 (ubuntu 사용자 기준)
##############################################
# docker 그룹 없으면 생성 (있으면 에러 무시)
sudo groupadd docker 2>/dev/null || true
# ubuntu 사용자를 docker 그룹에 추가
sudo usermod -aG docker ubuntu
##############################################
# 8. 로그아웃 후 다시 로그인 필요 (중요)
##############################################
# 아래 명령은 바로 실행하지 말고,
# SSH 세션을 끊고 다시 접속한 다음 실행하세요.
##############################################
# docker 그룹에 잘 들어갔는지 확인
groups
# Docker 버전 확인
docker -v
# Docker Compose 버전 확인
docker compose version
# 테스트 컨테이너 실행 (처음 한 번은 이미지 받아서 좀 걸릴 수 있음)
docker run hello-world
위 스크립트는 Ubuntu 22.04 (jammy) 기준으로 그대로 사용 가능합니다. 중간에 usermod -aG docker ubuntu 이후에는 반드시 SSH 로그아웃 후 재접속해야 sudo 없이 docker ps, docker run` 등이 정상 동작합니다. 프로젝트에 작성한 도커 컴포즈 파일을 실행하면 정상적으로 인스턴스에서 도커 이미지를 실행하실 수 있습니다.
appleboy로 코드 배포를 자동화하기
GitHub Actions는 CI/CD 자동화를 위한 플랫폼이고, appleboy/ssh-action은 GitHub Actions에서 SSH 연결 및 원격 명령 실행을 돕는 특정 마켓플레이스 액션(Action)입니다. 둘은 '전체 플랫폼'과 '그 플랫폼 내의 도구' 관계이며, 대립되는 개념이 아니라 함께 사용되어 배포 자동화(특히 SSH를 이용한)를 구현합니다. 즉, GitHub Actions 생태계 안에서 appleboy/ssh-action을 활용하여 배포 작업을 수행하는 것입니다.
결론적으로, appleboy/ssh-action은 GitHub Actions라는 큰 프레임워크 안에서 'SSH를 이용한 배포'라는 특정 기능을 구현하기 위해 사용하는 유용한 도구(액션)입니다. appleboy github repository에서 더 많은 내용을 살펴볼 수 있습니다.
GitHub - appleboy/ssh-action: GitHub Actions for executing remote ssh commands.
GitHub Actions for executing remote ssh commands. Contribute to appleboy/ssh-action development by creating an account on GitHub.
github.com
appleboy 핵심 문법 정리
- name: SSH로 원격 서버에 접속해서 명령 실행
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }} # 접속할 서버 주소 (IP 또는 도메인)
username: ${{ secrets.SERVER_USER }} # SSH 사용자 이름 (예: ubuntu)
key: ${{ secrets.SERVER_SSH_KEY }} # 개인키(PEM 파일 내용 전체)
port: 22 # (선택) SSH 포트, 기본값 22
script_stop: true # script 내 명령 하나라도 실패하면 전체 job 실패 처리
script: | # 서버에서 실행할 실제 쉘 스크립트
echo "서버에 접속되었습니다."
whoami
pwd
- host: SSH 접속할 서버 주소
- username: SSH 접속 계정명
- key: SSH 개인키(PEM 파일 내용) – GitHub Secrets에 저장해두고 불러오는 게 일반적
- port: SSH 포트(기본 22, 바꾸면 여기서 지정)
- script_stop: true이면, script 안에서 한 명령이라도 실패하면 액션 전체를 실패로 처리
- script: SSH로 접속한 뒤 원격 서버에서 실행할 쉘 명령들을 여러 줄로 작성
코드 배포 과정을 자동화하기
실제 운영중인 프로젝트의 워크플로우 파일 내용을 주석으로 정리한 코드입니다. 스프링 부트 프로젝트의 루트 경로를 기준으로 .github/workflows/workflows.yml 파일로 아래 코드를 작성하면 됩니다. workflows.yml 대신 deploy.yml 처럼 다른 파일명으로 작성해도 워크플로우 코드는 실행 가능합니다.
name: Deploy with Lightsail # GitHub Actions 워크플로우 이름 (UI에서 표시되는 이름)
on: # 이 워크플로우를 언제 실행할지 트리거 설정
push: # push 이벤트가 발생했을 때
branches: # 그 중에서도 특정 브랜치에 push 되었을 때만
- main # main 브랜치로 push될 때 워크플로우 실행
jobs: # 실제로 실행할 작업(job)들 정의
deploy: # 'deploy'라는 이름의 job 정의
runs-on: ubuntu-latest # GitHub Actions가 사용할 실행 환경 (호스트 OS) - 최신 Ubuntu
steps: # job 안에서 순차적으로 실행할 step 목록
- name: SSH로 Lightsail에 접속합니다. # 이 step의 이름 (로그에 표시됨)
uses: appleboy/ssh-action@v1.0.3 # appleboy/ssh-action 액션을 사용해서 SSH 접속 후 명령 실행
with: # appleboy/ssh-action에 넘겨줄 인자들
host: ${{ secrets.EC2_HOST }} # SSH 접속할 호스트 (Lightsail/EC2의 IP 또는 도메인, Secrets에서 가져옴)
username: ${{ secrets.EC2_USERNAME }} # SSH 접속 계정명 (예: ubuntu), Secrets에서 가져옴
key: ${{ secrets.EC2_PRIVATE_KEY }} # SSH 개인키(PEM 파일 내용), Secrets에 저장한 값을 사용
script_stop: true # script 안에서 하나라도 실패하면 이 step 전체를 실패로 처리
script: | # 여기부터는 원격 서버에서 실행할 쉘 스크립트 (멀티라인)
set -euo pipefail # 쉘 옵션 설정
# -e : 명령 실패 시 스크립트 즉시 종료
# -u : 선언되지 않은 변수 사용 시 에러
# -o pipefail : 파이프라인 명령에서 하나라도 실패하면 실패로 간주
APP_DIR="/home/ubuntu/WeGo_BackEnd" # 애플리케이션이 위치한 디렉터리 경로 변수
BRANCH="main" # 배포에 사용할 Git 브랜치 (main)
APP_PORT="8080" # 애플리케이션이 실행될 포트 번호
cd "$APP_DIR" # 애플리케이션 디렉터리로 이동
echo "[INFO] Git 업데이트..." # 로그 출력: Git 업데이트 시작
git fetch --all --prune # 원격 브랜치/태그 정보 최신화 + 필요 없는 참조 정리
git checkout "$BRANCH" # 배포 대상 브랜치로 체크아웃 (main 브랜치)
git reset --hard "origin/$BRANCH" # 원격(origin) main 브랜치 상태로 로컬 브랜치를 강제로 맞춤 (수정사항 초기화)
echo "[INFO] 기존 앱 종료 (포트 $APP_PORT)..." # 로그 출력: 기존 앱 종료 시도
sudo fuser -k -n tcp "$APP_PORT" || true # 해당 포트를 사용 중인 프로세스를 강제 종료
# 프로세스가 없어서 실패해도 전체 스크립트는 계속 진행하도록 '|| true'
echo "[INFO] Gradle 빌드..." # 로그 출력: Gradle 빌드 시작
chmod +x gradlew # gradlew 실행 권한 부여 (혹시 권한 없을 경우 대비)
./gradlew clean build -x test # Gradle로 클린 빌드 실행, 테스트(-x test)는 생략
echo "[INFO] JAR 찾는 중..." # 로그 출력: 빌드된 JAR 파일 경로 확인 안내
JAR="build/libs/wegobackend-0.0.1-SNAPSHOT.jar" # 빌드 결과 JAR 파일 경로를 변수로 지정
echo "[INFO] 사용 JAR: $JAR" # 어떤 JAR를 사용할지 로그로 출력
echo "[INFO] 애플리케이션 시작..." # 로그 출력: 애플리케이션 실행 시작 안내
nohup java -jar "$JAR" --server.port="$APP_PORT" > ./output.log 2>&1 &
# nohup으로 백그라운드 실행
# - JAR 실행
# - server.port를 APP_PORT로 지정 (8080)
# - 표준 출력 & 에러를 output.log 파일로 리다이렉트
# - & 로 백그라운드 실행
echo "[INFO] 앱이 포트 $APP_PORT 에서 시작되었습니다." # 최종 로그: 앱 시작 완료 안내
이 워크플로우는 전체적으로 이렇게 동작합니다.
- main 브랜치에 코드를 push하면
- GitHub Actions에서 Deploy with Lightsail 워크플로우 실행
- deploy job이 ubuntu-latest 환경에서 실행
- appleboy/ssh-action으로 Lightsail(또는 EC2) 서버에 SSH 접속
- 서버에서 순서대로:
- 배포 디렉터리로 이동 (APP_DIR)
- Git 브랜치 main 기준으로 코드 최신화 (fetch, reset --hard)
- 기존 앱이 쓰던 포트(8080)를 점유한 프로세스 종료
- Gradle 빌드(clean build -x test)
- 빌드된 JAR 파일 선택
- nohup java -jar ... 로 백그라운드 실행
- 최종적으로 서버의 8080 포트에서 새 버전이 뜨는 구조
워크플로우 코드 작성을 모두 마친 상태에서 main 브랜치에 코드를 push 하기 전에 repository에서 선행해야 할 작업이 있습니다. 해당 repository settings에서 Secrets and variables Actions 탭에서 Repository secrets의 New repository secret으로 인스턴스 정보를 작성해야만 합니다.

Repository secrets 작성했다면 워크플로우 코드를 포함해서 Push 합니다. 그리고 Actions 탭에서 All workflows를 살펴보면 정상적으로 동작이 잘 되는지 여부를 확인할 수 있습니다. 그리고 인스턴스에서 수행중인 서버로 접속해서 정상적으로 동작되는지 확인하면 됩니다.

3.실제 설정 과정에서 마주쳤던 오류와 트러블슈팅 가이드
CI/CD 관련 글들을 읽다 보면 마치 모든 것이 한 번에 매끄럽게 구성되는 것처럼 보입니다. 하지만 실제로 직접 구축해 보면, 사소한 설정 하나 때문에 몇 시간씩 발이 묶이기 일쑤입니다. 이번에 Lightsail + GitHub Actions 조합으로 Spring Boot 애플리케이션을 배포하면서도 비슷한 경험을 했고, 특히 “리소스가 작은 서버에서 빌드를 직접 돌리는 구조” 가 생각보다 큰 병목이 될 수 있다는 걸 실제 수치로 확인하게 되었습니다. 이 섹션에서는 제가 겪었던 문제와, 다시 같은 실수를 반복하지 않기 위해 정리해 둔 트러블슈팅 과정을 공유합니다.
Lightsail 2GB 플랜에서 빌드가 끝나지 않던 문제
당시 사용했던 Lightsail 인스턴스 스펙은 메모리 2GB,vCPU 2개 플랜으로 선택했습니다.(매 월 $15) 이 2GB 인스턴스 하나에 다음 구성 요소를 모두 올려서 사용했습니다.
- Spring Boot 애플리케이션 (JAR 실행)
- Docker Compose로 띄운 MySQL 컨테이너
- Docker Compose로 띄운 Redis 컨테이너
GitHub Actions에서는 appleboy/ssh-action을 사용해 Lightsail로 SSH 접속 후, 서버에서 직접 아래 스크립트를 실행하는 구조였습니다.
cd /home/ubuntu/WeGo_BackEnd
git fetch --all --prune
git reset --hard origin/main
sudo fuser -k -n tcp 8080 || true
chmod +x gradlew
./gradlew clean build -x test
nohup java -jar build/libs/wegobackend-0.0.1-SNAPSHOT.jar --server.port=8080 > output.log 2>&1 &
이론만 보면 아주 단순한 플로우입니다. 문제는 ./gradlew clean build -x test 단계에서 서버가 급격히 버벅이기 시작했다는 점이었습니다. 빌드가 시작되면 CPU 사용률은 대략 평균 40% 내외에서 움직이는데, 메모리는 2GB에 거의 근접(약 1.8~1.9GB) 하면서 스왑까지 사용하기 직전 상태가 됩니다. 이 순간부터 SSH 접속이 자주 끊기거나 입력 딜레이가 수 초 단위로 발생하고, htop 화면 갱신조차 버벅거릴 만큼 전체 시스템 응답성이 떨어졌습니다. GitHub Actions 쪽에서는 build 스텝이 10분 이상 진행되지 않는 것처럼 보이는 상황도 여러 번 발생했습니다.
처음에는 “GitHub Actions Runner 문제가 아닐까?”, “네트워크 지연인가?”를 의심했지만, 실제로는 Lightsail 인스턴스 자체가 리소스 부족 상태였습니다. SSH로 겨우 접속이 될 때 아래 명령어들로 상태를 확인했습니다.
# CPU/메모리 전반
htop
top
mpstat 1 1
# 메모리 / 스왑 사용량
free -h
# Docker 컨테이너별 리소스 사용량
docker stats
예를 들어, Spring Boot 애플리케이션만 최소 라이브러리 구성으로 띄운 상태(도커 미실행)에서 mpstat, free -h 결과는 다음과 비슷했습니다.
# CPU는 한가하지만, 메모리는 이미 1.2Gi 정도 사용 중인 상태
Linux 6.14.0-1014-aws (ip-172-26-14-78) _x86_64 (2 CPU)
Average: CPU %usr %sys %iowait %idle
all 0.00 0.00 0.00 100.00
total used free shared buff/cache available
Mem: 1.9Gi 1.2Gi 215Mi 2.9Mi 671Mi 683Mi
Swap: 0B 0B 0B
이 상태에서 Gradle 빌드를 시작하면, 추가로 필요한 메모리가 한꺼번에 치솟으면서 2GB라는 하드 리밋을 그냥 박아버리는 상황이 반복되었습니다. 정리하자면, “Spring Boot 앱 + MySQL + Redis가 항상 떠 있는 2GB 서버에서, 빌드까지 같은 머신에 몰아넣은 것이 병목의 근본 원인” 이라는 결론에 도달하게 되었습니다.
리소스 병목에 대한 구조적 해결 방향
이 문제는 단순히 “스펙이 낮아서 그렇다” 수준이 아니라, “CI/CD 설계를 서버 리소스 구조와 함께 어떻게 가져갈 것인가” 라는 고민으로 이어졌습니다. 전체적으로 두 가지 축에서 해결 방향을 고민했습니다.
- 빌드와 실행의 책임을 분리하기
- 서버에서 빌드를 돌릴 수밖에 없을 때, 상태를 가진 컴포넌트(DB)를 밖으로 빼기
각각을 조금 더 구체적으로 정리해보겠습니다.
빌드는 GitHub Actions가, 서버는 실행만 담당하도록 역할 분리
가장 이상적인 구조는, “빌드(컴파일/패키징)는 CI 서버에서, 애플리케이션 실행은 Lightsail에서” 로 역할을 분리하는 것입니다. 기존 구조는 다음과 같았습니다.
- GitHub Actions → SSH로 Lightsail 접속
- Lightsail에서 `./gradlew clean build -x test` 실행
- 같은 서버에서 `java -jar`로 애플리케이션 실행
즉, 가장 무거운 작업인 빌드까지 2GB Lightsail 인스턴스가 떠안고 있는 구조였습니다. 이를 다음과 같이 분리하는 것이 더 바람직합니다.
1. GitHub Actions Runner(ubuntu-latest)에서 빌드 수행
- name: Gradle Build
run: |
chmod +x gradlew
./gradlew clean build -x test
2. 빌드 결과 JAR를 서버로 전송하거나 아티팩트로 관리
- name: Upload JAR to Lightsail
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: "build/libs/wegobackend-0.0.1-SNAPSHOT.jar"
target: "/home/ubuntu/WeGo_BackEnd"
3. Lightsail에서는 빌드 없이 전달받은 JAR만 실행
sudo fuser -k -n tcp 8080 || true
nohup java -jar /home/ubuntu/WeGo_BackEnd/wegobackend-0.0.1-SNAPSHOT.jar \
--server.port=8080 > output.log 2>&1 &
이렇게 되면 Gradle 빌드에 필요한 CPU·메모리는 GitHub Actions의 Runner가 사용하게 되고, Lightsail 인스턴스는 Spring Boot 실행 + Redis + 기타 경량 작업에만 집중할 수 있습니다. 개인 프로젝트나 소규모 서비스에서 가장 권장되는 패턴이 이 구조입니다. 다만, 상황상 빌드를 반드시 서버에서 돌려야 하는 경우도 있기 때문에, 그때 어떤 선택을 했는지와 한계를 함께 정리해보겠습니다.
서버에서 빌드를 계속 돌려야 한다면: MySQL을 RDS로 분리하기
현실적으로 “당장은 CI 서버에서 빌드를 다 처리하는 구조로 바꾸기 어렵다”는 전제를 두고, 빌드는 여전히 Lightsail에서 수행하되, 리소스를 최대한 나눠 가지는 방향을 고민했습니다. 그 과정에서 가장 효과적이었던 해결책이 바로 MySQL을 RDS로 분리하는 것이었습니다. 초기 상태는 Lightsail 2GB 인스턴스 한 대에 Spring Boot 애플리케이션, Docker Compose 기반 MySQL, Docker Compose 기반 Redis를 모두 올려서 사용했습니다. 이 상태에서 ./gradlew clean build -x test를 실행하면 메모리는 거의 100%까지 치솟고, 빌드 시 SSH 지연/끊김, GitHub Actions의 build 스텝 장시간 정체되었습니다. 이때 MySQL을 RDS 인스턴스로 분리한 뒤 다시 모니터링 했을 때, 수치는 확연히 달라졌습니다.
- 평상시(애플리케이션 + Redis만 실행) → 메모리 사용량 약 30% 내외, CPU는 거의 idle (1~5% 미만)
- 빌드 시(Gradle 빌드 + 애플리케이션 + Redis) → 메모리 사용량 50~60% 수준, 빌드는 끝까지 정상 진행(SSH 끊김 현상 거의 사라짐)
즉, MySQL이 차지하던 메모리와 디스크 I/O 부담을 RDS로 완전히 넘기니, 같은 2GB 플랜에서도 “애플리케이션 + Redis + 빌드” 정도만 감당하면 되는 구조로 바뀌었고 체감상 CI/CD 파이프라인이 훨씬 안정적으로 동작하게 되었습니다. 여기서 얻은 인프라적인 이점을 정리해 보면 다음과 같습니다.
4. MySQL을 RDS로 분리하면서 얻은 인프라적 이점
1. 애플리케이션 서버를 더 ‘stateless’에 가깝게 만들기
2. MySQL이 로컬 Docker 컨테이너로 묶여 있으면, 인스턴스를 갈아탈 때마다 DB 볼륨, 데이터 백업/복원까지 함께 고민해야 합니다. RDS로 분리하면 Lightsail 인스턴스는 거의 순수 애플리케이션 실행 서버에 가까워지고, “DB는 중앙의 RDS가 제공하고, 필요하면 앱 서버만 여러 대 띄운다”는 그림을 그리기 쉬워집니다.
3. 앱 서버 리소스와 DB 리소스를 독립적으로 스케일링 가능
- DB가 병목 → RDS 인스턴스 클래스만 올리면 됨
- 애플리케이션이 병목 → Lightsail 플랜만 상향 조정
- 같은 리소스를 쓰더라도, 어디에 얼마를 배분할지 선택할 수 있다는 점에서 효율이 훨씬 좋아집니다.
4. 기존은 애플리케이션이 느려도, DB가 부담스러워도 인스턴스 스펙 하나만 올릴 수 있습니다.
5. 분리 이후에는 백업/모니터링/장애 대응을 RDS에 위임합니다.
RDS는 자동 백업, 스냅샷, CloudWatch 모니터링, 파라미터 그룹 관리 등을 기본 제공하기 때문에, 로컬 Docker MySQL에서 직접 스냅샷·덤프·볼륨 관리하던 부담을 크게 줄여줍니다. 향후 Multi-AZ 구성을 적용하면 가용성(HA) 측면에서도 구조가 훨씬 견고해집니다.
6. 디스크 I/O와 메모리 부담이 분리되면서 전체 성능 안정화
- 로컬 Docker MySQL은 애플리케이션과 같은 인스턴스의 CPU/메모리/디스크 I/O를 공유합니다.
- Gradle 빌드처럼 디스크 접근이 많은 작업과 DB I/O가 겹치면 서로 자원을 두고 싸우게 됩니다.
- DB를 RDS로 분리하면, Lightsail은 코드 실행 + 빌드 + Redis 위주이고, RDS는 DB I/O 전담 으로 역할이 나뉘면서 빌드 타임과 애플리케이션 응답 속도가 모두 더 안정적인 범위에서 움직이게 됩니다.
Spring Boot에서 RDS로 전환 시 고려할 기본 포인트
Spring Boot 코드를 기준으로 보면, 로컬 MySQL이냐 RDS냐의 차이는 결국 “URL과 자격 증명”입니다.
구성 자체는 크게 어렵지 않습니다. application-prod.yml 예시는 다음과 같이 가져갈 수 있습니다.
spring:
datasource:
url: jdbc:mysql://${RDS_ENDPOINT}:${RDS_PORT}/wego
?useSSL=false
&serverTimezone=Asia/Seoul
&allowPublicKeyRetrieval=true
&rewriteBatchedStatements=true
&cachePrepStmts=true
&prepStmtCacheSize=250
&prepStmtCacheSqlLimit=2048
&useServerPrepStmts=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${RDS_USER}
password: ${RDS_PASSWORD}
MySQL: useSSL의 대안 sslMode
질문의 배경MySQL 데이터베이스 URL을 보면 늘 useSSL이 눈에 띄는데, “SSL = HTTPS처럼 CA 인증서를 두고 통신하는 거 아냐?”라는 의문이 먼저 떠올랐습니다. 정확히 어떤 인증·암호화 매커니즘으로
limdaeil.tistory.com
추가로 고려해야 할 포인트는 다음과 같습니다.
- 보안 그룹
- RDS 보안 그룹에서 Lightsail의 공인 IP만 허용하도록 설정
- 3306 포트를 퍼블릭에 열어두지 않도록 주의
- CI/CD 연동
- GitHub Secrets 또는 .env에 DB 접속 정보 저장
- --spring.profiles.active=prod 형태로 로컬/운영 프로파일 분리 가능
이 정도만 준비하면, 로컬 Docker MySQL에서 RDS로 옮기는 작업은 생각보다 큰 공사 없이도 진행할 수 있습니다.
핵심 정리: “빌드는 그대로라도, DB만 분리해도 체감이 달라진다”
핵심만 다시 정리하면, 이번 케이스에서는 빌드는 여전히 Lightsail(2GB)에서 수행하고 있습니다. 그럼에도 불구하고,
- 메모리 사용량
- 평시: 25~35% 수준
- 빌드 시: 50~60% 선에서 안정
- 빌드 안정성
- GitHub Actions `build` 스텝이 멈춰 있는 현상 거의 사라짐
- SSH 지연/끊김 현상도 크게 감소
- 인프라 구조
- 애플리케이션 서버와 DB 서버의 역할이 분리되어, 이후 스케일업/스케일아웃 전략을 세우기 훨씬 수월한 구조로 변경
이라는 변화를 체감할 수 있었습니다.
리소스 병목을 해결하는 방향은 꼭 “서버 스펙을 키운다”가 아니라, “상태를 가진 컴포넌트(DB)를 밖으로 분리해서 책임을 나눈다”는 선택도 충분히 유효하다는 것을 몸으로 느낀 사례였습니다.
서버에서 빌드를 반드시 돌려야 할 때 시도했던 최소한의 방어선과 한계
RDS 도입 이전에는, 어떻게든 2GB 한 대에서 모든 것을 버텨보려고 여러 가지 미세 조정을 시도했습니다. 대표적으로 다음과 같은 전략들입니다.
1. 빌드 시점에 불필요한 컨테이너 일시 중지
docker compose stop mysql redis
./gradlew clean build -x test
docker compose start mysql redis
빌드 중에는 DB와 캐시가 필요 없다는 점을 활용해서, 빌드 타임에 MySQL·Redis를 잠시 내려 그만큼의 메모리와 CPU를 빌드 프로세스에 돌려보려는 시도였습니다. 이론상 타당한 접근이고, 실제로 메모리 피크를 약간 낮추는 효과는 있었습니다.
2. Gradle 워커 수 및 데몬 설정 조정
gradle.properties를 통해, 빌드시 동시에 떠오르는 워커 수를 줄이고 데몬을 끄는 방식으로 시도했습니다.
org.gradle.workers.max=1
org.gradle.daemon=false
또는 커맨드라인에서 직접 다음과 같이 실행하기도 했습니다.
./gradlew clean build -x test --no-daemon
이는 빌드 속도를 조금 포기하는 대신, 동시 실행되는 스레드/프로세스 수를 줄여 메모리 피크를 낮추려는 시도였습니다.
3. bootJar 기반 빌드 시도 및 클래스패스 문제
전체 clean build 대신, 빌드 비용을 줄이기 위해 ./gradlew bootJar만 실행하는 전략도 실험했습니다.의도는 "테스트 및 기타 서브 모듈 빌드는 생략하고,실행 가능한 JAR만 빠르게 만들자"였지만, 실제로는 클래스패스 관련 문제가 발생했습니다.
- 일부 의존 라이브러리가 JAR에 올바르게 포함되지 않거나,
- 실행 시점에 ClassNotFoundException, NoClassDefFoundError와 같은 의존성 로딩 오류가 발생
현재 구조와 플러그인 설정이 있는 상황에서, 프로젝트에 맞는 전체 빌드 구성을 고려하지 않고 bootJar만 단독으로 가져가다 보니, 생성된 JAR의 클래스패스가 기대와 다르게 구성되는 부작용을 겪었습니다. 결국 운영 안정성을 위해 다시 ./gradlew clean build로 회귀하게 되었습니다. 이러한 방어선들을 차례로 적용해 보면서 느낀 점은: “빌드 피크를 조금 줄이는 데에는 분명 도움이 됐지만, 결국 Spring Boot + MySQL + Redis + 빌드를 2GB 한 대에 전부 몰아넣는 구조 자체가 한계에 가까웠다.” 는 것이었습니다. 컨테이너를 잠시 내렸다 올리거나, Gradle 설정을 조정하는 식의 미세 튜닝보다는, 아키텍처 레벨에서 리소스 구조와 책임을 분리하는 쪽이 훨씬 안정적이고 장기적인 해법이라는 점을 확인하게 되었습니다.
“그냥 4GB로 올리면 되지 않을까?”에 대한 고민과, 왜 적절하지 않았는지
중간에 한 번쯤은 이런 생각이 스쳐 지나갔습니다. “그냥 Lightsail 2GB → 4GB 플랜으로 올리면 다 해결되는 거 아닌가?” 실제로 4GB 플랜으로 올리면, 단일 인스턴스 위에서, Spring Boot 애플리케이션, Docker Compose 기반 MySQL, Redis, Gradle 빌드를 동시에 돌릴 수 있을 가능성이 높아집니다. 하지만, 비용과 구조를 함께 놓고 보면 이 선택이 항상 최선이라고 보긴 어렵습니다.
비용 관점: 4GB 한 대 vs 2GB + RDS 2GB
단순화해서 보면, 선택지는 대략 이렇게 나뉩니다.
- 선택 A → Lightsail 4GB 인스턴스 한 대 → 애플리케이션 + MySQL + Redis + 빌드를 모두 여기에 올리는 “올인원 서버”
- 선택 B → Lightsail 2GB 인스턴스 한 대 + RDS 2GB급 인스턴스 한 대 → 애플리케이션 + Redis + (빌드) = Lightsail → MySQL = RDS
실제 요금 구조를 보면, 위 두 조합의 총비용은 크게 차이가 나지 않거나, 경우에 따라 오히려 비슷한 수준이 됩니다. 즉, “같은 돈을 쓴다”고 가정했을 때, 어디에 어떤 구조로 리소스를 배분할지의 문제입니다.
리소스 활용 관점: 4GB 한 대에 몰아넣는 구조의 문제
4GB 인스턴스 하나에 모든 컴포넌트를 몰아넣으면 다음과 같은 문제들이 남습니다.
- 리소스 경쟁
- 애플리케이션, DB, Redis, 빌드가 여전히 같은 CPU·메모리·디스크 I/O를 두고 경쟁합니다.
- 배치 작업, 대량 쿼리, 빌드 같은 일시적인 부하가 들어오면 한쪽이 튀는 순간 다른 컴포넌트 전체에 영향을 줍니다.
- 장애 전파 범위
- OS 문제, 디스크 이슈, 인스턴스 장애가 나면 애플리케이션 + DB + 캐시가 동시에 영향을 받는 구조입니다.
- 장애 대응 및 복구 전략이 DB까지 함께 얽혀 있어 훨씬 복잡해집니다.
- 스케일링 유연성 부족
- 애플리케이션 부하만 늘어나도 인스턴스 전체 스펙을 올려야 하고,
- DB 부하가 늘어나도 마찬가지입니다.
- 반대로 2GB + RDS 2GB 구조에서는:
- DB 병목 → RDS 스펙만 조정
- 앱 서버 병목 → Lightsail 플랜만 조정 이 가능해 스케일링 단위를 역할별로 나눌 수 있습니다.
아키텍처 관점: “얼마나 크게?”보다 “어디에 무엇을?”이 더 중요하다 CI/CD와 인프라 설계를 함께 고민해 보면, 이번 경험에서 가장 크게 남은 문장은 이겁니다. “리소스를 얼마나 크게 쓰느냐보다, 무엇을 어느 레이어에 맡길지 먼저 설계하는 것이 더 중요하다.” 4GB 한 대에 모두 몰아넣는 선택은 단기적으로는 빨리 해결된 것처럼 보이는 선택일 수 있습니다. 하지만 장기적으로 보면,
- 리소스 경쟁
- 장애 전파 범위
- 백업/복구 전략
- 스케일링 유연성
네 가지 측면에서 약점을 계속 안고 가는 구조가 됩니다. 반대로, 2GB Lightsail + 2GB RDS 조합은 애플리케이션 서버와 DB의 책임이 명확히 분리되고, 이후 EC2, ALB, Auto Scaling, ElastiCache 같은 컴포넌트를 도입할 때도 자연스럽게 확장 가능한 형태입니다. 동일하거나 비슷한 비용을 쓰더라도 구조적으로 훨씬 건강한 인프라를 구성할 수 있습니다.
정리하자면, 이번 경험을 통해 내린 결론은 다음과 같습니다. “CI/CD 설계에서 빌드와 실행, 그리고 DB까지 모두 한 서버에 몰아 넣는 것은 단기적인 편의일 수는 있지만, 리소스·아키텍처 관점에서는 좋지 않다. 같은 비용이라면, 인스턴스를 키우기보다 역할을 분리하는 방향이 훨씬 안정적이다.” 이 관점을 머릿속에 기본값으로 깔아두면, 나중에 서비스가 성장했을 때 “초기 설계 때문에 발목 잡힌다”는 느낌을 훨씬 덜 경험하게 될 것 같습니다.