1. AWS RDS를 사용하는 이유
RDS란?
AWS RDS(Relational Database Service)는 AWS가 관리형으로 제공하는 데이터베이스 서비스다. MySQL, PostgreSQL, MariaDB, Oracle, SQL Server 등을 지원하며, DB 운영에 필요한 인프라를 AWS가 대신 처리해준다.
EC2 직접 설치 vs RDS 비교
항목 EC2에 직접 설치 RDS로 관리
| 운영체제 패치 | 직접 관리 | AWS 자동 처리 |
| DB 엔진 업그레이드 | 직접 수행 | 콘솔에서 클릭 한 번 |
| 백업 | 직접 스크립트 작성 | 자동 백업 + 스냅샷 |
| HA(고가용성) | 직접 레플리카 구성 | Multi-AZ 옵션 체크 |
| 모니터링 | 직접 CloudWatch 에이전트 설치 | 기본 연동 |
| 스토리지 확장 | 인스턴스 스펙 변경 필요 | Auto Scaling 지원 |
| 비용 | 인스턴스 비용만 | 관리형 비용 추가 (더 비쌈) |
RDS의 주요 이점
✅ 자동 백업 & 스냅샷
RDS는 보존 기간(1~35일) 내에서 자동 백업을 수행하고, 특정 시점으로 복구(Point-in-Time Recovery)가 가능하다. EC2에 직접 설치한 경우 이를 구현하려면 cron + pg_dump 같은 스크립트를 직접 짜고 관리해야 한다.
✅ Multi-AZ 고가용성
Multi-AZ 옵션을 켜면 AWS가 자동으로 다른 가용 영역(AZ)에 스탠바이 인스턴스를 생성하고, Primary 장애 시 자동으로 Failover한다. EC2 직접 구성이라면 레플리케이션 설정, Failover 스크립트, DNS 전환까지 전부 수동이다.
✅ 패치 및 엔진 업그레이드 자동화
OS 보안 패치나 DB 마이너 버전 업그레이드를 AWS가 유지보수 윈도우에 맞춰 처리해준다. EC2라면 업그레이드할 때 직접 SSH 접속 후 작업해야 하고, 잘못되면 직접 롤백해야 한다.
✅ 스토리지 자동 확장 (Storage Auto Scaling)
RDS는 스토리지가 임계치에 도달하면 자동으로 용량을 늘려준다. EC2는 EBS 볼륨 확장 → 파일시스템 리사이즈 작업을 직접 해야 한다.
✅ I AM & VPC 보안 통합
IAM 인증, VPC 서브넷 그룹, 보안 그룹을 통해 네트워크 수준에서 접근을 제어할 수 있다. EC2라면 이 설정도 직접 구성해야 한다.
RDS가 적합하지 않은 상황
관리형 서비스라서 편하지만, 모든 상황에서 최선은 아니다.
❌ 비용이 민감한 소규모 프로젝트
RDS는 EC2에 직접 설치하는 것보다 비용이 더 높다. 사이드 프로젝트나 개발 환경이라면 EC2 + Docker로 DB를 띄우는 게 훨씬 저렴하다. (실무에서도 규모가 작을 경우, 종종 EC2로 배포하는 것 같습니다)
❌ OS 수준의 커스터마이징이 필요한 경우
RDS는 OS에 직접 접근할 수 없다. 특정 DB 설정 파일을 직접 수정하거나 OS 레벨 튜닝이 필요하다면 EC2 직접 설치가 맞다. (이때, OS 레벨 튜닝은 커널 파라미터 최적화, 디스크 마운트 설정, 보안 에이전트 등이 있다고 합니다)
❌ PostgreSQL 확장 기능 (Extension) 제약
RDS에서는 일부 PostgreSQL Extension이 제한된다. 예를 들어 pg_cron은 Aurora PostgreSQL에서는 지원되지만 일반 RDS에서는 제한적이다. 커스텀 Extension을 사용해야 한다면 EC2 직접 설치 또는 RDS Custom을 검토해야 한다.
- pg_cron : DB 내부의 스케줄링(특정 시간에 쿼리문 실행하고 싶을 때)
❌ 레이턴시 극단적으로 최적화가 필요한 경우
같은 EC2 인스턴스 내에 DB를 띄우면 네트워크 레이턴시를 최소화할 수 있다. RDS는 별도 엔드포인트를 통해 통신하므로 미세한 레이턴시 차이가 존재한다.
결국 RDS와 EC2 사이의 선택은 '인프라 관리 비용'과 '자유도' 사이의 트레이드오프이다.
2. GitHub Actions 트리거(Trigger) 종류와 CI/CD 시나리오
GitHub Actions 트리거란?
GitHub Actions 워크플로우는 특정 이벤트가 발생했을 때 실행된다. 이 이벤트를 트리거라고 하며, on: 키워드로 정의한다.
on:
push:
branches:
- main
주요 트리거 유형
1. push
가장 기본적인 트리거. 특정 브랜치나 태그에 push가 발생하면 실행된다.
on:
push:
branches:
- main
- develop
tags:
- 'v*.*.*'
적합한 시나리오:
- main 또는 develop에 push될 때 자동으로 빌드 & 테스트 실행
- v* 태그가 push되면 자동으로 프로덕션 배포 트리거
2. pull_request
PR이 생성되거나, 수정되거나, 동기화될 때 실행된다. CI의 핵심 트리거다.
on:
pull_request:
branches:
- main
- develop
types:
- opened
- synchronize
- reopened
types 옵션:
타입 설명
| opened | PR 최초 생성 시 |
| synchronize | PR에 새 커밋 push 시 |
| reopened | 닫혔다가 다시 열릴 때 |
| closed | PR이 닫힐 때 (머지 포함) |
적합한 시나리오:
- PR이 올라올 때마다 자동으로 테스트 & 빌드 수행
- PR 코드 품질 검사 (Lint, 코드 커버리지 등)
- 브랜치 보호 규칙과 함께 사용해 테스트 통과 전 머지 차단
3. workflow_dispatch
사람이 GitHub 콘솔 또는 API를 통해 수동으로 워크플로우를 실행하는 트리거. 입력값(input)을 받을 수도 있다.
on:
workflow_dispatch:
inputs:
environment:
description: '배포 환경'
required: true
default: 'staging'
type: choice
options:
- staging
- production
적합한 시나리오:
- 수동 배포 (긴급 핫픽스 배포, 특정 환경에 선택적 배포)
- 정기적이지 않은 데이터 마이그레이션 작업
- 개발팀이 직접 트리거 시점을 제어해야 할 때
- Github Actions UI, Github CLI를 통해 실행을 할 수 있음
# GitHub CLI(gh)를 사용하는 경우
gh workflow run deploy.yml -f environment=production
왜 "일정 부분"만 정의할까?
- 위험한 작업의 보호 : DB 마이그레이션이나 전체 재배포처럼 실수로 진행되면 안되는 작업은 수동으로 실행한다!
- 유연한 대응 : 특정 브랜치를 급하게 테스트 서버에 올려봐야 할 때
4. schedule
cron 문법으로 정기적으로 워크플로우를 실행한다.
on:
schedule:
- cron: '0 2 * * *' # 매일 새벽 2시 (UTC 기준)
적합한 시나리오:
- 매일 새벽 정기 빌드 & 테스트 (Nightly Build)
- 주기적인 보안 취약점 스캔 (Dependabot 대체)
- 배치 작업 결과 슬랙 알림
5. workflow_call
다른 워크플로우에서 재사용 가능하도록 현재 워크플로우를 호출 가능하게 만드는 트리거.
# reusable-deploy.yml
on:
workflow_call:
inputs:
environment:
required: true
type: string
# 다른 워크플로우에서 호출
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
적합한 시나리오:
- 여러 프로젝트에서 공통 배포 로직을 재사용할 때
- 모노 레포 환경에서 서비스별로 공통 CI 파이프라인을 공유할 때 (모노 레포는 하나의 저장소 안에 폴더로 구분하는 구조를 말함)
6. release
GitHub Release가 생성, 수정, 발행될 때 실행된다.
on:
release:
types:
- published
적합한 시나리오:
- GitHub Release를 발행하면 자동으로 프로덕션 배포
- 릴리즈 노트와 함께 Docker 이미지 빌드 & ECR 푸시
7. issue_comment / pull_request_review
이슈 댓글이나 PR 리뷰가 달릴 때 실행된다.
on:
issue_comment:
types:
- created
적합한 시나리오:
- PR에 / deploy staging 같은 댓글을 달면 자동 배포 트리거 (ChatOps 패턴)
- 리뷰 Approve 시 자동으로 머지 또는 배포 파이프라인 진행
트리거 조합 예시 — 실제 CI/CD 파이프라인
on:
push:
branches:
- develop # develop push → 스테이징 배포
pull_request:
branches:
- main # main PR → 테스트 & 빌드 검증
release:
types:
- published # Release 발행 → 프로덕션 배포
workflow_dispatch: # 수동 실행 옵션 유지
이런 식으로 트리거를 조합하면 브랜치 전략과 CI/CD 파이프라인을 자연스럽게 연결할 수 있다.
정리
트리거 주요 용도
| push | 빌드/테스트 자동화, 태그 기반 배포 |
| pull_request | PR 단위 코드 검증 |
| workflow_dispatch | 수동 배포, 긴급 대응 |
| schedule | 정기 배치, Nightly Build |
| workflow_call | 워크플로우 재사용 (공통 파이프라인) |
| release | 릴리즈 기반 프로덕션 배포 |
| issue_comment | ChatOps 패턴, 댓글 기반 트리거 |
3. Terraform 기본 구조와 IaC
Terraform이란?
HashiCorp에서 만든 IaC(Infrastructure as Code) 도구다. HCL(HashiCorp Configuration Language)로 인프라를 코드로 선언하고, AWS/GCP/Azure 등 다양한 클라우드에 적용할 수 있다.
기본 구조
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# State를 S3에 저장, DynamoDB로 Lock
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = "ap-northeast-2"
default_tags {
tags = {
Environment = "prod"
ManagedBy = "Terraform"
Project = "my-service"
}
}
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "main-vpc"
}
}
Terraform 워크플로우
terraform init # 초기화 (Provider 다운로드, Backend 연결)
terraform plan # 변경사항 미리보기 (무엇이 생성/수정/삭제되는지)
terraform apply # 실제 적용
terraform destroy # 모든 리소스 삭제 (주의!)
실무에서는 terraform plan 결과를 PR에 자동으로 코멘트로 달아 리뷰하는 방식이 일반적이다. Atlantis, Terraform Cloud, GitHub Actions + tfcmt 조합을 많이 사용한다.
State 관리의 중요성
Terraform은 terraform.tfstate 파일에 현재 인프라 상태를 기록한다.
- 로컬 저장 금지: 팀 협업 불가, 분실 시 치명적. .gitignore 처리 필수
- S3 + DynamoDB 조합 권장: S3에 State 저장, DynamoDB로 동시 실행 방지(Lock)
- Terraform Cloud / Spacelift: State 관리 + 협업 UI 제공하는 SaaS
4. CI — Continuous Integration
목표
모든 커밋/PR에서 품질 게이트를 자동 검증하여 깨진 코드가 main 브랜치에 들어가지 않게 한다. 프로덕션에서 버그를 발견하는 비용 vs PR 단계에서 발견하는 비용 차이는 10~100배다.
주요 체크 항목
✅ 코드 스타일 / 포맷팅
언어 도구
| Java | Spotless, Checkstyle |
| TypeScript | Prettier |
| Go | gofmt (언어 내장) |
| Kotlin | ktlint |
스타일은 도구로 자동화해야 한다. PR 리뷰에서 들여쓰기를 지적하는 건 낭비다.
✅ 정적 분석 (Lint)
버그 가능성, 안티패턴, 보안 취약점을 코드 실행 없이 탐지한다.
언어 도구
| Java | SpotBugs, PMD, Error Prone |
| TypeScript | ESLint |
| Go | golangci-lint |
| Python | Pylint, Ruff |
SpotBugs는 NullPointerException 가능성, 동기화 누락, 리소스 누수 등을 정적으로 잡아준다.
✅ 단위 테스트
언어 도구
| Java | JUnit 5, Mockito, AssertJ |
| TypeScript | Jest, Vitest |
| Go | testing 표준 라이브러리 |
✅ 테스트 커버리지
언어 도구
| Java | JaCoCo |
| TypeScript | Istanbul / nyc |
| Go | go test -cover |
커버리지 자체가 목적은 아니다. 경험적으로 라인 커버리지 70~80% + 핵심 로직 분기(Branch) 커버리지 확인이 적절하다. 전체 커버리지 70% 고정보다 변경된 라인의 커버리지 80% 이상 요구가 유지보수에 더 좋다.
✅ 통합 테스트 (Testcontainers)
실제 DB, Redis, Kafka를 띄우고 하는 테스트. Mock과 달리 실제 제품 동작을 검증한다.
@Testcontainers
class UserRepositoryIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Test
void shouldSaveAndFindUser() {
User saved = repository.save(new User("alice"));
assertThat(repository.findById(saved.getId())).isPresent();
}
}
✅ 보안 스캔
유형 설명 도구
| SAST | 코드 자체 정적 분석 | SonarQube, Snyk Code, Semgrep |
| SCA | 의존성 라이브러리 CVE 탐지 | Dependabot, Snyk, OWASP Dependency-Check |
| 컨테이너 스캔 | 이미지 내 OS 패키지 취약점 | Trivy, Grype, AWS ECR Scanning |
| Secret 스캔 | 하드코딩된 API 키 탐지 | gitleaks, trufflehog |
Log4Shell(2021) 이후 SCA는 필수로 자리잡았다.
GitHub Actions CI 예시
name: CI
on:
pull_request:
push:
branches: [main]
env:
IMAGE_NAME: 1234567890.dkr.ecr.ap-northeast-2.amazonaws.com/my-app
jobs:
build-test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: gradle
- name: Check format
run: ./gradlew spotlessCheck
- name: Static analysis
run: ./gradlew check -x test
- name: Unit tests + coverage
run: ./gradlew test jacocoTestReport
- name: Coverage gate
run: ./gradlew jacocoTestCoverageVerification
- name: Integration tests
run: ./gradlew integrationTest
- name: SAST (SonarQube)
run: ./gradlew sonar
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Build image
if: github.event_name == 'push'
run: docker build -t $IMAGE_NAME:${{ github.sha }} .
- name: Scan image (Trivy)
if: github.event_name == 'push'
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE_NAME }}:${{ github.sha }}
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Push image
if: github.event_name == 'push'
run: |
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $IMAGE_NAME
docker push $IMAGE_NAME:${{ github.sha }}
docker tag $IMAGE_NAME:${{ github.sha }} $IMAGE_NAME:latest
docker push $IMAGE_NAME:latest
5. CD — Continuous Delivery / Deployment
Delivery vs Deployment 차이
구분 설명
| Continuous Delivery | 언제든 배포 가능한 상태 유지. 실제 배포는 수동 승인 |
| Continuous Deployment | 모든 변경이 자동으로 프로덕션까지 반영 |
대부분은 dev는 Continuous Deployment, prod는 Continuous Delivery 조합을 사용한다.
환경 분리
환경 목적 데이터 배포 방식
| local | 개발자 개인 환경 | 더미 데이터 | 수동 |
| dev | 통합 테스트, 데모 | 더미 또는 익명화 복제본 | 자동 (main 머지 시) |
| stage | 프로덕션 리허설, QA | 프로덕션 익명화 복제본 | 자동 (태그 또는 수동 트리거) |
| prod | 실제 사용자 | 실제 데이터 | 수동 승인 후 자동 |
배포 파이프라인 흐름 (쿠버네티스 환경일 때)
단일 EC2 일 때는, Github Actions 사용 권장!
[개발자 로컬]
│ git push
▼
[PR open]
├─→ CI (lint, test, scan, coverage)
│
│ PR merge to main
▼
[CI main] → 이미지 빌드 + push (태그: <git-sha>)
│
▼
[CD dev] (자동) ← ArgoCD가 새 이미지 태그를 감지하여 dev에 배포
│
│ 검증 완료, 태그 생성 (v1.2.3)
▼
[CD staging] (자동 또는 수동) ← 태그 기반 배포
│
│ 수동 승인
▼
[CD prod] (Blue-Green / Canary)
dev에 merge하고, main으로 배포를 하는 줄 알았는데 완전히 잘못 알았다... staging은 사용자에게 나가기 직전 마지막으로 확인하는 곳이다. dev는 가짜 데이터를 쓰지만, staging은 실제 운영 데이터와 거의 똑같은 복제본을 연결해서 테스트를 한다. 이때 QA를 통해 배포 최종 승인을 내린다!!
'Weekly Paper' 카테고리의 다른 글
| Weekly Paper #14 (0) | 2026.06.09 |
|---|---|
| Weekly Paper #13 (0) | 2026.06.09 |
| Weekly Paper #10 (0) | 2026.04.01 |
| Weekly Paper #9 (0) | 2026.03.23 |
| Weekly Paper #7 (0) | 2026.02.27 |