Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AWS로 이해하는 클라우드 컴퓨팅 기초

온프레미스에서 출발해 AWS 위에서
실전 마이크로서비스를 운영할 수 있는 수준까지
초보자의 호흡으로 차근차근 쌓아 올리는 책.


이 책의 척추 — 우리가 만들어 갈 구조

이 책의 모든 장은 아래 구조의 어느 한 점에 속한다.

사용자
  ↓ DNS (Route 53)
CloudFront
  ↓
API Gateway
  ↓
ALB
  ↓
ECS (Fargate) — 마이크로서비스들
  ↓
RDS · DynamoDB · ElastiCache

각 장은 시작에서 “이 장이 척추의 어디에 해당하는가” 를 짚고 들어간다.


매 장의 골격

  1. 이 장에서 말하고자 하는 것
  2. 개념 본문 — 단계별 설명
  3. 우리 서비스에서는 어디에 쓰이나 — 척추 그림에 점 찍기
  4. 직접 확인해보기 — CLI / 콘솔
  5. 코드로는 이렇게 생겼다 — Terraform 스니펫
  6. 이렇게 쓰면 망한다 — 안티패턴
  7. 한 줄로 정리
  8. 이 장의 핵심 정리

목차

1부. 클라우드란 무엇인가

  • 1장. 온프레미스와 클라우드의 이해
  • 2장. 클라우드는 어떻게 서버를 제공하는가
  • 3장. 클라우드 서비스 모델의 이해
  • 4장. 클라우드 책임 공유 모델의 이해
  • 5장. 클라우드의 과금 구조 이해하기
  • 6장. 확장성과 가용성의 이해
  • 7장. 리전과 가용 영역의 이해

2부. 서버와 네트워크의 기초

  • 8장. 서버 아키텍처의 기초 이해
  • 9장. 상태와 무상태의 이해
  • 10장. 서버와 네트워크의 연결 이해
  • 11장. 트래픽과 접근 제어의 이해

3부. EC2 — 첫 서버 띄우기

  • 12장. EC2란 무엇인가
  • 13장. 인스턴스 타입 이해하기
  • 14장. EC2 스토리지 구성 이해하기
  • 15장. AMI와 이미지 기반 배포
  • 16장. EC2 접속과 권한 — 키 페어 · Session Manager · IAM Role
  • 17장. EBS 스냅샷과 백업 전략
  • 18장. EC2 요금 모델 — On-Demand · Spot · RI · Savings Plans

4부. VPC — 네트워크를 직접 설계하기

  • 19장. 서버는 어디에 연결되는가
  • 20장. IP 주소 범위와 CIDR 이해하기
  • 21장. 서브넷 (Subnet)
  • 22장. 퍼블릭 서브넷과 프라이빗 서브넷
  • 23장. 인터넷 게이트웨이 (Internet Gateway)
  • 24장. 라우팅 테이블 (Route Table)
  • 25장. NAT Gateway
  • 26장. 보안 그룹 (Security Group)
  • 27장. NACL (Network ACL)
  • 28장. 운영 환경을 위한 VPC 디자인

5부. 도메인과 인증서 — 사용자가 들어오는 길

  • 29장. 도메인이 서버까지 도달하기까지 — DNS의 이해
  • 30장. Route 53 — AWS의 DNS
  • 31장. HTTPS는 어디에서 끝나는가 — TLS 종료의 이해
  • 32장. ACM — 인증서를 어디에 다는가

6부. 트래픽 분산과 자동 확장

  • 33장. Elastic Load Balancer 지형 — ALB · NLB · GWLB
  • 34장. ALB의 라우팅 — 리스너 · 규칙 · 타깃 그룹
  • 35장. 경로 · 호스트 기반 라우팅으로 서비스 쪼개기
  • 36장. NLB는 언제 쓰는가
  • 37장. Auto Scaling Group과 스케일링 정책

7부. 엣지 계층 — CDN과 보안

  • 38장. CloudFront — 사용자에 가까운 곳에서 응답하기
  • 39장. CloudFront의 Origin · 캐시 · OAC
  • 40장. WAF — 엣지에서 걸러내는 보안

8부. 컨테이너로 옮겨 가기

  • 41장. 왜 컨테이너인가 — VM과의 차이
  • 42장. 도커 기초 — 이미지 · 컨테이너 · 레이어
  • 43장. ECR — 컨테이너 이미지 저장소
  • 44장. ECS의 구조 — Cluster · Task · Service
  • 45장. Task Definition 깊게 보기
  • 46장. EC2 vs Fargate — 어느 쪽에서 돌릴까
  • 47장. ECS Service와 ALB 연결하기
  • 48장. ECS Service Discovery — Cloud Map
  • 49장. ECS 배포 전략 — Rolling · Blue-Green
  • 50장. EKS는 언제 고려하는가 — 개념 한 줄 이해
  • 51장. 통합 흐름 — 우리 서비스를 ECS 위로 올려보기

9부. API 진입점

  • 52장. API Gateway — REST API vs HTTP API
  • 53장. API Gateway의 인증 — IAM · Cognito · Lambda Authorizer
  • 54장. CloudFront → API Gateway → ALB 의 역할 분담

10부. 스토리지

  • 55장. 블록 · 파일 · 객체 스토리지의 차이
  • 56장. S3 — 객체 스토리지의 기본
  • 57장. S3 스토리지 클래스와 수명 주기 — 비용 설계 관점
  • 58장. 사전 서명 URL과 권한 모델
  • 59장. CloudFront + S3로 정적 콘텐츠 서비스하기
  • 60장. EFS · FSx — 공유 파일 시스템

11부. 데이터 계층 — 서비스별로 나누고 운영하기

  • 61장. AWS 데이터베이스 지형 한눈에 보기
  • 62장. RDS — 관리형 관계형 DB의 구조
  • 63장. Multi-AZ — 장애 대비의 기본
  • 64장. Read Replica — 읽기를 어떻게 늘릴까
  • 65장. Aurora — 클라우드 네이티브 RDS
  • 66장. DynamoDB의 사고방식 — Key-Value · 파티션 키
  • 67장. DynamoDB 인덱스 — LSI · GSI
  • 68장. DynamoDB의 용량 모델과 비용
  • 69장. ElastiCache (Redis) — 캐시 계층의 이해
  • 70장. Database per Service — 서비스별 DB 분리 패턴
  • 71장. 백업 · 복구 · 재해 복구 전략
  • 72장. 통합 흐름 — 데이터 계층을 우리 서비스에 끼우기

12부. 서비스 간 통신

  • 73장. 동기 vs 비동기 — 무엇을 어디에 쓰는가
  • 74장. SQS — 큐로 결합도 낮추기
  • 75장. SNS — 팬아웃 패턴
  • 76장. EventBridge — 이벤트 기반 아키텍처
  • 77장. Saga 패턴 맛보기 — 분산 트랜잭션의 현실

13부. 권한 · 시크릿 · 암호화

  • 78장. IAM 기본 — User · Group · Role · Policy
  • 79장. IAM Role 활용 — EC2 · ECS Task Role · Execution Role
  • 80장. 최소 권한 설계
  • 81장. KMS — 데이터 암호화
  • 82장. Secrets Manager와 Parameter Store

14부. 관측성 — MSA의 절반

  • 83장. CloudWatch 메트릭과 알람
  • 84장. CloudWatch Logs와 Logs Insights
  • 85장. 컨테이너 로깅 — FireLens · awslogs 드라이버
  • 86장. X-Ray — 분산 트레이싱
  • 87장. CloudTrail — 변경 추적

15부. 여러 VPC와 외부 연결

  • 88장. VPC Endpoint — VPC 안에서 외부 서비스 접근
  • 89장. VPC Peering
  • 90장. Transit Gateway
  • 91장. AWS Site-to-Site VPN
  • 92장. AWS Direct Connect

16부. 자동화와 배포

  • 93장. IaC — CloudFormation · Terraform 개요
  • 94장. 이미지 빌드 자동화 — CodeBuild · GitHub Actions
  • 95장. 배포 자동화 — CodePipeline · CodeDeploy
  • 96장. 배포 전략 — Blue-Green · Canary

17부. 실전 종합 — 풀 스택 한 바퀴

  • 97장. 도메인 설계 — 3개 서비스로 쪼개기
  • 98장. CF → APIGW → ALB → ECS → RDS/DynamoDB 통합 흐름
  • 99장. 운영 체크리스트 — 보안 · 비용 · 관측성
  • 100장. 다음 단계 — 기초 이후 어디로 갈 것인가

1장. 온프레미스와 클라우드의 이해

1. 우리가 만든 프로그램은 어디에서 실행될까?

우리가 웹 서비스를 개발했다고 가정해보자.

  • 로그인 기능을 만들고
  • 게시판을 만들고
  • 데이터를 저장하도록 구현했다

그런데 이 프로그램은 어디에서 실행될까?

내 노트북에서만 실행된다면 다른 사람은 사용할 수 없다.
따라서 24시간 켜져 있고, 인터넷에 연결된 컴퓨터가 필요하다.

이 컴퓨터를 우리는 서버(Server) 라고 부른다.


2. 서버란 무엇인가?

서버는 특별한 장비가 아니다.

다른 사람의 요청을 받아 처리해주는 컴퓨터

가 서버다.

  • 웹 요청을 처리하면 웹 서버
  • 데이터를 저장하면 데이터베이스 서버
  • 파일을 저장하면 스토리지 서버

결국 서버도 컴퓨터다.
다만 꺼지면 안 되고, 많은 요청을 동시에 처리해야 하며,
안정적으로 운영되어야 한다는 점이 다를 뿐이다.


3. 서버는 어디에 두어야 할까?

이제 중요한 선택이 등장한다.

서버를 직접 준비할 것인가,
아니면 누군가에게 빌릴 것인가?

여기서 두 가지 운영 방식이 나온다.

  1. 직접 서버를 구매해서 운영하는 방식 → 온프레미스
  2. 인터넷을 통해 서버를 빌려 쓰는 방식 → 클라우드

4. 온프레미스란 무엇인가?

온프레미스는 회사가 직접 서버를 구매하고,
직접 설치하고, 직접 운영하는 방식이다.

온프레미스 운영 과정 예시

  • 서버 장비 구매
  • 데이터센터(IDC)에 설치
  • 네트워크 연결
  • 운영체제 설치
  • 보안 설정
  • 장애 대응

모든 책임이 내부에 있다.

장점

  • 완전한 통제권
  • 보안 및 네트워크를 세밀하게 설계 가능
  • 장기적으로는 비용 예측이 비교적 안정적

단점

  • 초기 비용이 큼
  • 서버가 부족하면 새로 구매해야 함
  • 확장에 시간이 오래 걸림

5. 클라우드란 무엇인가?

클라우드는 인터넷을 통해 서버를 임대하여 사용하는 방식이다.

대표적인 클라우드 서비스 제공자는 다음과 같다.

  • Amazon Web Services
  • Google Cloud Platform
  • Microsoft Azure

이 문서에서는 AWS를 기준으로 설명한다.

클라우드에서는 이렇게 한다

  • 웹 콘솔에서 서버를 생성한다
  • 몇 분 안에 사용 가능하다
  • 필요 없으면 삭제한다
  • 사용한 만큼 비용을 지불한다

즉, 서버를 소유하지 않고 사용한다.


6. 온프레미스와 클라우드의 차이

비용 구조

구분온프레미스클라우드
비용 방식장비 선구매사용량 기반 과금
초기 비용높음낮음
장기 비용자원 활용률에 따라 유리할 수 있음설계와 사용량에 따라 크게 변동

확장 방식

구분온프레미스클라우드
확장 방법장비 추가 구매즉시 서버 생성
확장 속도느림빠름
탄력성제한적자동 확장 가능

운영 책임

구분온프레미스클라우드
하드웨어내부 책임클라우드 사업자
OS / 애플리케이션내부 책임대부분 내부
물리 보안내부 책임사업자 책임

7. 갑자기 사용자가 10배 늘어난다면?

온프레미스

  • 서버 용량이 부족하다
  • 새 장비를 주문한다
  • 배송과 설치를 기다린다
  • 설정을 완료한다

시간이 오래 걸린다.

클라우드

  • 서버를 여러 대 추가 생성한다
  • 자동으로 트래픽을 분산한다

몇 분 안에 확장이 가능하다.

이 차이가 클라우드의 가장 큰 특징이다.


8. 클라우드는 무조건 더 좋은가?

그렇지는 않다.

  • 사용량이 많아지면 비용이 커질 수 있다
  • 설계를 잘못하면 불필요한 자원이 계속 과금된다
  • 자동화와 운영 이해가 필요하다

클라우드는 단순한 서버 임대가 아니라
운영 방식의 변화다.


9. 정리

이 장에서 기억해야 할 핵심은 다음과 같다.

  1. 서버는 24시간 동작하는 컴퓨터다.
  2. 온프레미스는 서버를 직접 소유하고 운영하는 방식이다.
  3. 클라우드는 서버를 빌려 사용하는 방식이다.
  4. 두 모델의 차이는 위치가 아니라 운영 구조에 있다.

2장. 클라우드는 어떻게 서버를 제공하는가

1. 서버를 몇 분 만에 만드는 것이 가능한 이유

1장에서 우리는 클라우드가 서버를 임대하는 방식이라고 배웠다.

그렇다면 이런 질문이 생긴다.

  • 버튼을 누르면 왜 몇 분 만에 서버가 만들어질까?
  • 실제 물리 서버를 새로 설치하는 것일까?

답은 아니다.

클라우드는 대부분 가상화 기술을 이용해
하나의 물리 서버를 여러 개로 나누어 제공한다.


2. 물리 서버와 가상 서버

물리 서버

  • 실제 하드웨어 장비
  • CPU, 메모리, 디스크, 네트워크 카드가 물리적으로 존재
  • 데이터센터에 설치되어 있음

가상 서버

  • 물리 서버 위에 소프트웨어적으로 만들어진 서버
  • 독립된 운영체제를 실행
  • 다른 가상 서버와 격리되어 동작

사용자는 가상 서버를 사용하지만,
실제로는 물리 서버의 자원을 나누어 사용하는 구조다.


3. 가상화란 무엇인가

가상화(Virtualization)는

하나의 물리 자원을 여러 개의 논리적 자원으로 나누는 기술

이다.

예를 들어:

  • CPU 32코어
  • 메모리 256GB

를 가진 물리 서버가 있다면,

이를 나누어

  • 4코어 / 16GB 서버 여러 개
  • 8코어 / 32GB 서버 여러 개

처럼 만들 수 있다.

이렇게 만들어진 각각의 서버는
독립된 컴퓨터처럼 동작한다.


4. 하이퍼바이저의 역할

이 작업을 수행하는 소프트웨어를 하이퍼바이저라고 한다.

하이퍼바이저는 다음을 담당한다.

  • 물리 CPU를 여러 개의 가상 CPU로 분할
  • 메모리를 나누어 각 가상 서버에 할당
  • 디스크를 논리적으로 분리
  • 네트워크를 가상으로 구성

즉, 물리 자원을 관리하고 나누어주는 관리자 역할을 한다.


5. 왜 이것이 클라우드를 가능하게 하는가

클라우드 사업자는 대규모 데이터센터에
수많은 물리 서버를 보유하고 있다.

사용자가 “새 서버를 만들어 달라”고 요청하면,

  • 이미 준비된 물리 서버 위에
  • 하나의 가상 서버를 생성하고
  • 운영체제를 올린 뒤
  • 네트워크를 연결해준다

이 과정이 자동화되어 있기 때문에
몇 분 안에 서버가 생성될 수 있다.

물리 장비를 새로 설치하는 것이 아니라,
이미 존재하는 자원을 분할하여 제공하는 것이다.


6. 가상화의 장점

가상화는 클라우드에 다음과 같은 장점을 제공한다.

1) 빠른 생성

물리 설치 없이 즉시 서버 제공 가능

2) 높은 자원 활용률

하나의 물리 서버를 여러 사용자가 공유

3) 격리

한 가상 서버의 장애가 다른 서버에 직접 영향을 주지 않음

4) 유연한 확장

필요할 때 서버를 추가 생성 가능


7. 가상 서버는 완전히 독립적인가

가상 서버는 논리적으로 독립적이지만
물리 자원을 공유한다는 사실은 변하지 않는다.

따라서 다음과 같은 상황이 발생할 수 있다.

  • 같은 물리 서버의 다른 가상 서버가 과도한 자원을 사용
  • 네트워크 공유로 인한 지연
  • 디스크 I/O 경쟁

이 문제를 줄이기 위해
클라우드 사업자는 자원 격리 기술을 지속적으로 발전시키고 있다.


8. 가상 머신과 컨테이너

가상화에는 두 가지 주요 방식이 있다.

가상 머신(VM)

  • 독립된 운영체제를 포함
  • 격리 수준이 높음
  • 상대적으로 무거움

컨테이너(Container)

  • 운영체제를 공유
  • 가볍고 빠름
  • 마이크로서비스 구조에 적합

현재 대부분의 클라우드 서비스는
가상 머신 기반 구조 위에서 시작되었으며,
최근에는 컨테이너 기술도 널리 활용되고 있다.


9. 클라우드 서비스마다 부르는 이름

각 클라우드 서비스는
이 가상 서버를 서로 다른 이름으로 부른다.

  • Amazon Web Services → EC2
  • Google Cloud Platform → Compute Engine
  • Microsoft Azure → Virtual Machine

이름은 다르지만,
기본 원리는 동일하다.


10. 정리

이 장에서 기억해야 할 핵심은 다음과 같다.

  1. 클라우드는 물리 서버를 직접 빌려주는 것이 아니다.
  2. 가상화 기술을 이용해 물리 자원을 나누어 제공한다.
  3. 하이퍼바이저가 자원을 분할하고 관리한다.
  4. 이 구조 덕분에 빠른 생성과 확장이 가능하다.

3장. 클라우드 서비스 모델의 이해

1. 같은 서비스를 다른 방식으로 운영한다면

우리가 간단한 웹 서비스를 만든다고 가정해보자.

  • 사용자는 회원가입을 한다.
  • 로그인 후 게시글을 작성한다.
  • 게시글은 데이터베이스에 저장된다.

이 서비스를 운영하려면 다음이 필요하다.

  • 서버
  • 운영체제
  • 웹 서버 프로그램
  • 데이터베이스
  • 네트워크 설정
  • 보안 설정
  • 백업 및 모니터링

이 중에서 누가 어디까지 관리하느냐에 따라
클라우드 서비스 모델이 나뉜다.


2. IaaS란 무엇인가

IaaS는 Infrastructure as a Service의 약자다.
인프라를 서비스 형태로 제공한다는 의미다.

예를 들어 Amazon Web Services 에서는
가상 서버 서비스를 EC2라고 부른다.

게시판 서비스를 IaaS로 운영하면 다음과 같다.

우리가 직접 해야 하는 일

  • 가상 서버 생성
  • 운영체제 설치 및 설정
  • 웹 서버 설치
  • 데이터베이스 설치
  • 보안 패치 적용
  • 백업 구성
  • 확장 설계

클라우드는 서버, 네트워크, 스토리지를 제공하지만
그 위의 대부분은 우리가 책임진다.

IaaS는 온프레미스와 가장 유사한 모델이다.
차이는 물리 장비를 직접 소유하지 않는다는 점이다.


3. PaaS란 무엇인가

PaaS는 Platform as a Service의 약자다.
실행 환경을 서비스로 제공한다는 의미다.

같은 게시판 서비스를 PaaS로 운영하면
우리가 관리해야 할 영역이 줄어든다.

우리가 하는 일

  • 애플리케이션 코드 작성
  • 기능 개발
  • 데이터 구조 설계

클라우드가 대신하는 일

  • 운영체제 관리
  • 런타임 환경 관리
  • 자동 확장
  • 일부 보안 패치
  • 관리형 데이터베이스 운영

즉, 우리는 인프라보다
애플리케이션 개발에 집중하게 된다.

IaaS보다 추상화 수준이 한 단계 높다.


4. SaaS란 무엇인가

SaaS는 Software as a Service다.
완성된 소프트웨어를 서비스 형태로 제공하는 모델이다.

지금까지는 게시판을 “직접 만든다”는 전제였다.
하지만 SaaS에서는 게시판을 직접 개발하지 않는다.

이미 만들어진 서비스를 사용한다.

우리가 하는 일

  • 계정 생성
  • 사용자 관리
  • 기능 설정
  • 요금 결제

우리가 하지 않는 일

  • 서버 관리
  • 운영체제 관리
  • 보안 패치
  • 확장 설계
  • 백업 구성
  • 인프라 모니터링

이 모든 것은 서비스 제공자가 담당한다.

예를 들어
Microsoft 의 Microsoft 365,
Google 의 Google Workspace 등이 이에 해당한다.

SaaS는 개발이나 인프라 운영이 아닌
“서비스 사용”에 가깝다.


5. 한눈에 비교하면

같은 게시판 서비스를 기준으로 정리하면 다음과 같다.

모델우리가 관리하는 범위
IaaS운영체제 이상 대부분
PaaS애플리케이션 중심
SaaS거의 없음

오른쪽으로 갈수록
직접 관리해야 할 영역은 줄어들고
추상화 수준은 높아진다.


6. 왜 이렇게 나뉘어 있는가

클라우드는 단순히 서버를 빌려주는 산업이 아니다.
운영 부담을 얼마나 줄여줄 것인가에 따라 발전해왔다.

  • 인프라만 제공 → IaaS
  • 실행 환경까지 제공 → PaaS
  • 완성된 소프트웨어 제공 → SaaS

즉, 관리 범위를 단계적으로 줄여가는 구조다.


7. 어떤 모델을 선택해야 할까

정답은 없다.
서비스의 목적과 조직의 역량에 따라 달라진다.

  • 세밀한 제어가 필요하면 IaaS
  • 개발 속도가 중요하면 PaaS
  • 직접 개발할 필요가 없으면 SaaS

실제 환경에서는 이 모델들을 혼합해 사용한다.

예를 들어:

  • 핵심 서비스는 IaaS
  • 데이터베이스는 관리형(PaaS)
  • 협업 도구는 SaaS

이처럼 상황에 맞게 조합하는 것이 일반적이다.


8. 정리

이 장에서 기억해야 할 핵심은 다음과 같다.

  1. 같은 서비스라도 운영 방식은 달라질 수 있다.
  2. 차이는 “누가 어디까지 관리하느냐”이다.
  3. IaaS는 인프라 중심 모델이다.
  4. PaaS는 실행 환경 중심 모델이다.
  5. SaaS는 완성된 소프트웨어 모델이다.

4장. 클라우드 책임 공유 모델의 이해

1. 클라우드를 쓰면 모든 책임이 사라질까

클라우드를 처음 접하면 이런 생각을 할 수 있다.

서버를 빌렸으니, 보안과 장애도 클라우드가 다 책임지는 것 아닐까?

하지만 그렇지 않다.

클라우드는 모든 책임을 대신해주는 서비스가 아니다.
클라우드 사업자와 사용자 사이에 책임이 나뉘어 있다.

이것을 책임 공유 모델(Shared Responsibility Model) 이라고 한다.


2. 책임 공유 모델이란 무엇인가

책임 공유 모델은 다음을 의미한다.

클라우드는 인프라의 일부를 책임지고,
사용자는 그 위에서 동작하는 것에 대해 책임진다.

예를 들어 Amazon Web Services 는
자사 공식 문서에서 이 모델을 명확히 설명하고 있다.

핵심은 다음과 같다.

  • 클라우드는 클라우드 자체의 보안(Security of the Cloud) 을 책임진다.
  • 사용자는 클라우드 위에서 사용하는 것(Security in the Cloud) 을 책임진다.

3. 물리 인프라에 대한 책임

클라우드 사업자가 책임지는 영역은 다음과 같다.

  • 데이터센터 건물 보안
  • 물리 서버 장비
  • 네트워크 장비
  • 전력 및 냉각 시스템
  • 하이퍼바이저 계층

즉, 우리가 직접 접근할 수 없는 물리 영역은
클라우드가 책임진다.

온프레미스라면 우리가 직접 관리해야 할 부분이다.


4. 운영체제와 애플리케이션은 누가 책임질까

여기서부터는 서비스 모델에 따라 달라진다.

IaaS의 경우

  • 물리 서버 → 클라우드 책임
  • 가상 서버 → 클라우드 책임
  • 운영체제 → 사용자 책임
  • 애플리케이션 → 사용자 책임
  • 데이터 → 사용자 책임

즉, OS부터 위는 우리가 관리한다.

PaaS의 경우

  • 운영체제 → 클라우드 책임
  • 런타임 환경 → 클라우드 책임
  • 애플리케이션 코드 → 사용자 책임
  • 데이터 → 사용자 책임

관리 범위가 줄어든다.

SaaS의 경우

  • 인프라 → 클라우드 책임
  • 플랫폼 → 클라우드 책임
  • 애플리케이션 운영 → 클라우드 책임
  • 데이터 입력 및 접근 권한 관리 → 사용자 책임

SaaS에서도 데이터 보호와 접근 제어는
사용자의 책임이다.


5. 실제 사고 사례를 생각해보자

예를 들어 이런 상황을 가정해보자.

  • 서버에 보안 패치를 적용하지 않았다.
  • 관리자 비밀번호를 단순하게 설정했다.
  • 데이터베이스를 외부에 공개했다.

이 경우 책임은 누구에게 있을까?

클라우드가 아니라
사용자에게 있다.

클라우드는 “환경”을 제공하지만,
그 위의 설정 실수까지 대신 책임지지는 않는다.


6. 책임 공유 모델이 중요한 이유

클라우드 전환을 고려할 때
가장 흔한 오해 중 하나는 다음이다.

클라우드로 가면 보안이 자동으로 해결된다.

하지만 실제로는

  • 잘못된 권한 설정
  • 과도하게 열린 네트워크
  • 암호화 미적용
  • 백업 미구성

등은 여전히 사용자의 책임이다.

따라서 클라우드는 보안을 없애는 기술이 아니라,
보안의 경계를 바꾸는 모델이다.


7. 책임 공유 모델을 한눈에 정리하면

영역IaaSPaaSSaaS
데이터센터클라우드클라우드클라우드
물리 서버클라우드클라우드클라우드
운영체제사용자클라우드클라우드
애플리케이션사용자사용자클라우드
데이터사용자사용자사용자

오른쪽으로 갈수록
사용자의 관리 범위는 줄어든다.
하지만 데이터 책임은 항상 사용자에게 있다.


8. 정리

이 장에서 기억해야 할 핵심은 다음과 같다.

  1. 클라우드는 모든 책임을 대신해주지 않는다.
  2. 물리 인프라는 클라우드가 책임진다.
  3. 운영체제와 애플리케이션은 모델에 따라 달라진다.
  4. 데이터 보호 책임은 항상 사용자에게 있다.
  5. 보안은 사라지는 것이 아니라 경계가 이동하는 것이다.

5장. 클라우드의 과금 구조 이해하기

1. 왜 클라우드 비용은 체감이 다를까

온프레미스에서는 비용 구조가 비교적 단순하다.

  • 서버 구매 비용
  • 랙 비용
  • 회선 비용
  • 유지보수 비용

대부분 고정 비용이다.

하지만 클라우드는 다르다.

자원을 “소유”하지 않고 “사용”하기 때문에
비용이 사용량에 따라 계속 변한다.

이 차이 때문에 클라우드 비용은
예측이 어렵게 느껴질 수 있다.


2. 클라우드 과금의 기본 원리

클라우드 과금의 핵심은 단순하다.

사용한 만큼 지불한다.

대표적인 클라우드 사업자인
Amazon Web Services 역시
대부분의 서비스가 사용량 기반 과금이다.

과금 단위는 다음과 같이 나뉜다.

  • 서버 사용 시간
  • CPU / 메모리 크기
  • 스토리지 용량
  • 네트워크 트래픽
  • 요청 횟수

즉, 자원을 여러 단위로 나누어 계산한다.


3. 서버 과금은 어떻게 계산될까

가상 서버(IaaS)의 경우 보통 다음 요소로 계산된다.

  • 인스턴스 유형 (CPU, 메모리 크기)
  • 실행 시간 (시간 또는 초 단위)
  • 추가 스토리지
  • 네트워크 아웃바운드 트래픽

예를 들어,

  • 24시간 계속 실행
  • 고사양 인스턴스
  • 트래픽이 많음

이면 비용이 크게 증가한다.

반대로,

  • 필요할 때만 실행
  • 자동 종료
  • 소형 인스턴스 사용

이면 비용을 줄일 수 있다.


4. 스토리지와 네트워크 비용

초보자가 가장 자주 놓치는 부분이
네트워크 비용이다.

스토리지 과금

  • 저장 용량 기준
  • I/O 요청 횟수 기준
  • 백업 및 스냅샷 용량 기준

네트워크 과금

  • 클라우드 내부 트래픽
  • 리전 간 트래픽
  • 인터넷으로 나가는 트래픽

특히 인터넷으로 나가는 트래픽(아웃바운드)은
비용이 빠르게 증가할 수 있다.

스트리밍, 이미지 서비스, 대용량 다운로드 서비스는
이 부분이 매우 중요하다.


5. 고정 비용과 변동 비용의 차이

온프레미스는 고정 비용 중심이다.

  • 서버를 구매하면 비용은 이미 발생
  • 사용률이 높을수록 효율이 좋아짐

클라우드는 변동 비용 중심이다.

  • 사용량이 적으면 비용도 적음
  • 사용량이 많으면 비용도 증가

즉, 클라우드는 유연성을 제공하는 대신
비용이 예측하기 어려울 수 있다.


6. 언제 클라우드가 비싸게 느껴질까

다음과 같은 환경에서는
클라우드가 오히려 더 비싸게 느껴질 수 있다.

  • 24시간 고부하가 지속되는 서비스
  • 자원을 거의 100% 사용 중인 환경
  • 이미 감가상각이 끝난 장비 운영 중인 경우

이 경우 온프레미스가 비용 효율이 더 높을 수 있다.


7. 언제 클라우드가 유리할까

다음과 같은 경우에는 클라우드가 유리하다.

  • 트래픽 변동이 큰 서비스
  • 특정 시간대만 고부하
  • 빠른 실험과 확장이 필요한 환경
  • 초기 투자 비용을 줄이고 싶은 경우

클라우드는 “항상 싸다”가 아니라
“필요한 만큼만 쓴다”는 점이 핵심이다.


8. 비용을 이해하는 사고 방식

클라우드를 사용할 때는
다음 질문을 항상 해야 한다.

  • 이 서버는 24시간 필요할까?
  • 자동으로 줄일 수 있는 구조인가?
  • 트래픽을 줄일 방법은 없는가?
  • 관리형 서비스가 오히려 효율적인가?

클라우드에서는
아키텍처 설계가 곧 비용 설계다.


9. 정리

이 장에서 기억해야 할 핵심은 다음과 같다.

  1. 클라우드는 사용량 기반 과금이다.
  2. 서버, 스토리지, 네트워크가 주요 비용 요소다.
  3. 네트워크 아웃바운드는 특히 주의해야 한다.
  4. 고정 부하 서비스는 온프레미스가 유리할 수 있다.
  5. 비용은 설계에 따라 크게 달라진다.

6장. 확장성과 가용성의 이해

1. 서버는 반드시 멈추지 않아야 할까

온프레미스 환경에서는 이런 생각이 자연스럽다.

  • 서버는 절대 죽으면 안 된다.
  • 가장 좋은 장비를 써야 한다.
  • 장애는 최대한 막아야 한다.

하지만 현실에서는 어떤 시스템도
영원히 멈추지 않을 수는 없다.

  • 하드웨어 고장
  • 네트워크 장애
  • 갑작스러운 트래픽 폭증
  • 소프트웨어 오류

클라우드는 이 현실을 전제로 설계된다.

서버는 죽을 수 있다.
중요한 것은 “죽지 않게 만드는 것”이 아니라
“죽어도 서비스가 유지되게 만드는 것”이다.


2. 확장성이란 무엇인가

확장성(Scalability)은
부하가 증가해도 서비스를 유지할 수 있는 능력이다.

예를 들어:

  • 사용자가 100명일 때는 서버 1대로 충분
  • 사용자가 10,000명으로 늘어나면 서버가 더 필요

이때 어떻게 대응할 것인가가 확장성의 핵심이다.


3. 두 가지 확장 방식

수직 확장

  • 기존 서버의 성능을 높인다.
  • CPU, 메모리를 더 큰 사양으로 변경한다.

한계가 명확하다.
장비 교체가 필요하고, 물리적 제한이 존재한다.

수평 확장

  • 서버 수를 늘린다.
  • 여러 대가 동시에 요청을 처리한다.

이 방식은 이론상 확장 한계가 낮다.
클라우드는 이 수평 확장에 매우 유리하다.

대표적인 클라우드 서비스 제공자인
Amazon Web Services 는
자동 확장 기능을 제공한다.

부하가 증가하면 서버를 자동으로 늘리고,
부하가 줄어들면 자동으로 줄인다.


4. 가용성이란 무엇인가

가용성(Availability)은
서비스가 얼마나 지속적으로 이용 가능한지를 의미한다.

예를 들어:

  • 99% 가용성
  • 99.9% 가용성
  • 99.99% 가용성

숫자가 높을수록 장애 허용 시간이 줄어든다.

하지만 가용성은 단순히
“서버가 켜져 있는가”의 문제가 아니다.

하나의 서버가 꺼져도
전체 서비스는 계속 동작하는가?

이 질문이 핵심이다.


5. 단일 장애 지점

단일 장애 지점(Single Point of Failure)은
한 지점의 장애가 전체 서비스 중단으로 이어지는 구조다.

예:

  • 서버 1대만 운영
  • 데이터베이스 1대만 운영
  • 로드 밸런서 없음

이 구조에서는 작은 장애도
전체 중단으로 이어진다.

클라우드는 이러한 단일 장애 지점을 제거하기 쉽게 만든다.


6. 다중 영역 구성

클라우드 환경에서는
서버를 서로 다른 물리적 영역에 배치할 수 있다.

Amazon Web Services 는
이를 리전과 가용 영역이라는 구조로 제공한다.

  • 가용 영역은 서로 물리적으로 분리된 데이터센터
  • 전력과 네트워크가 독립적
  • 동시에 장애가 발생할 확률이 낮음

여러 영역에 서버를 배치하면
하나의 데이터센터가 장애가 나도
서비스는 유지된다.


7. 클라우드의 설계 철학

온프레미스 사고:

  • 강력한 한 대의 서버
  • 장애를 막는 구조

클라우드 사고:

  • 여러 대의 비교적 작은 서버
  • 장애를 전제로 한 구조
  • 자동 확장
  • 자동 복구

이 차이가 가장 중요하다.

클라우드는 완벽한 서버를 제공하는 것이 아니라
유연하게 복구 가능한 환경을 제공한다.


8. 확장성과 가용성은 비용과 연결된다

  • 서버를 여러 대 운영하면 비용 증가
  • 다중 영역 구성하면 비용 증가
  • 데이터 복제하면 비용 증가

따라서 다음 질문이 중요하다.

  • 우리 서비스는 어느 수준의 가용성이 필요한가?
  • 장애 허용 범위는 어디까지인가?
  • 비용 대비 적절한 설계는 무엇인가?

클라우드에서는
아키텍처 설계가 곧 비용 설계다.


9. 이 장의 핵심 정리

  1. 서버는 언제든지 장애가 날 수 있다.
  2. 클라우드는 장애를 전제로 설계한다.
  3. 수평 확장은 클라우드의 핵심 강점이다.
  4. 단일 장애 지점을 제거하는 것이 중요하다.
  5. 가용성 수준은 설계와 비용의 균형이다.

7장. 리전과 가용 영역의 이해

1. 서버를 여러 대 두면 충분할까

6장에서 우리는 단일 장애 지점을 제거해야 한다고 배웠다.

그렇다면 서버를 2대 이상 두면 충분할까?

만약 두 서버가 같은 건물,
같은 전력 설비, 같은 네트워크 장비를 사용한다면
건물 단위의 사고가 발생했을 때 둘 다 동시에 멈출 수 있다.

  • 정전
  • 화재
  • 네트워크 장비 고장
  • 자연 재해

따라서 고가용성을 위해서는
서버를 단순히 여러 대 두는 것이 아니라
물리적으로 분리된 위치에 배치해야 한다.

이 구조를 가능하게 하는 개념이
리전과 가용 영역이다.


2. 리전이란 무엇인가

리전(Region)은
클라우드 사업자가 특정 지리적 위치에 구축한
하나의 독립된 인프라 단위다.

예를 들어
Amazon Web Services 는
서울, 도쿄, 프랑크푸르트, 버지니아 등
전 세계 여러 리전을 운영하고 있다.

각 리전은 다음과 같은 특징을 가진다.

  • 독립된 전력 인프라
  • 독립된 네트워크 인프라
  • 독립된 데이터센터 그룹
  • 다른 리전과 물리적으로 분리

하나의 리전이 장애가 나더라도
다른 리전에는 영향을 주지 않도록 설계되어 있다.

리전은 재해 복구 단위라고 이해하면 쉽다.


3. 가용 영역이란 무엇인가

이제 중요한 개념이다.

가용 영역(Availability Zone, AZ)은
하나의 리전 안에 존재하는
서로 다른 물리적 데이터센터다.

여기서 중요한 점은 다음이다.

가용 영역은 데이터센터 내부의 구역이 아니다.
같은 건물 안의 다른 방이 아니다.

서로 완전히 분리된 독립 데이터센터다.


4. 가용 영역에 대한 흔한 오해

많은 사람들이 처음에 이렇게 생각한다.

  • 하나의 데이터센터 건물 안에 여러 구역이 있는 것인가?
  • 서버실을 나눈 개념인가?

아니다.

가용 영역은 다음과 같은 구조다.

예를 들어 서울 리전에 3개의 가용 영역이 있다면:

  • 서울 내 서로 다른 위치의 3개 독립 데이터센터
  • 전력 설비가 서로 다름
  • 네트워크 인입 경로가 다름
  • 동시에 장애가 날 확률이 매우 낮음

즉,

서울 강남의 데이터센터 하나
서울 마포의 데이터센터 하나
서울 성수의 데이터센터 하나

이런 개념에 가깝다.

(정확한 위치는 보안상 공개되지 않는다.)


5. 서울과 부산은 같은 리전일까

이론적으로는 하나의 리전에
서울과 부산이 포함될 수도 있다.

하지만 일반적으로는 다음 원칙이 있다.

  • 같은 리전 안의 가용 영역은
    지연 시간이 매우 낮아야 한다.
  • 수 밀리초(ms) 수준의 빠른 통신이 가능해야 한다.

서울-부산처럼 거리가 먼 경우에는
보통 다른 리전으로 구분된다.

왜냐하면:

  • 리전 간은 재해 복구 단위
  • 가용 영역은 고가용성 단위
  • 지연 시간 차이가 크기 때문

즉,

가용 영역은 “같은 도시권 내의 분리된 데이터센터”
정도로 이해하는 것이 가장 정확하다.


6. 왜 가용 영역이 중요한가

고가용성의 핵심은
단일 장애 지점을 제거하는 것이다.

예를 들어:

  • 서버 1대 → 장애 시 서비스 중단
  • 서버 2대를 서로 다른 가용 영역에 배치 → 한 영역 장애 시에도 서비스 유지

이를 다중 AZ 구성이라고 한다.

이 구조에서는:

  • 한 데이터센터가 정전되어도
  • 네트워크가 끊겨도
  • 특정 건물에 사고가 발생해도

서비스는 계속 동작할 수 있다.


7. 리전과 가용 영역의 차이 정리

구분리전가용 영역
단위지리적 지역독립된 데이터센터
목적재해 복구고가용성
거리도시/국가 단위같은 도시권 내
지연 시간비교적 있음매우 낮음
장애 범위대규모부분적

리전은 “대규모 재해 대비”
가용 영역은 “일반적인 장애 대비”

라고 이해하면 쉽다.


8. 설계 철학과의 연결

6장에서 배운 내용을 다시 생각해보자.

  • 서버는 죽을 수 있다.
  • 데이터센터도 장애가 날 수 있다.
  • 하나의 도시도 재해를 겪을 수 있다.

클라우드는 이를 전제로
리전과 가용 영역이라는 구조를 제공한다.

즉,

클라우드는 물리적 분리를 기본 설계로 제공한다.

이 점이 온프레미스와의 중요한 차이 중 하나다.


9. 이 장의 핵심 정리

  1. 서버 여러 대만으로는 충분하지 않다.
  2. 물리적 분리가 고가용성의 핵심이다.
  3. 리전은 지리적 단위다.
  4. 가용 영역은 서로 다른 물리적 데이터센터다.
  5. 가용 영역은 같은 건물 안의 구역이 아니다.
  6. 다중 AZ 구성은 기본 설계 전략이다.

8장. 서버 아키텍처의 기초 이해

이 장에서 말하고자 하는 것

클라우드는 새로운 개념처럼 보이지만,
결국 그 위에서 동작하는 것은 “서버”다.

클라우드를 이해하려면
먼저 서버가 무엇이고,
웹 서비스가 어떻게 구성되는지 이해해야 한다.

이 장의 목적은 다음 세 가지다.

  1. 서버의 기본 개념 이해
  2. 웹 서비스의 구성 요소 이해
  3. 클라우드 위에 올라가는 구조를 머릿속에 그릴 수 있게 만들기

1. 서버란 무엇인가

서버(Server)는 특별한 장비가 아니다.

다른 컴퓨터의 요청을 받아 처리하는 컴퓨터

이다.

웹 브라우저로 어떤 사이트에 접속하면
브라우저는 서버에 요청(Request)을 보내고,
서버는 처리 결과를 응답(Response)으로 반환한다.

이 구조를 클라이언트-서버 구조라고 한다.

  • 클라이언트 → 요청을 보내는 쪽
  • 서버 → 요청을 처리하는 쪽

2. 웹 서비스는 어떻게 동작하는가

게시판 서비스를 예로 들어보자.

  1. 사용자가 브라우저에서 글 작성 버튼 클릭
  2. 브라우저가 서버에 HTTP 요청 전송
  3. 서버가 요청을 처리
  4. 데이터베이스에 저장
  5. 결과를 다시 사용자에게 응답

이 과정은 하나의 서버가 아니라
여러 역할의 서버가 나누어 수행한다.


3. 서버의 역할 분리

서비스가 커질수록 서버의 역할을 나누게 된다.

1) 웹 서버

  • HTTP 요청을 받아 처리
  • 정적 파일 제공 (이미지, CSS, JS)
  • 애플리케이션 서버로 요청 전달

대표적인 웹 서버 소프트웨어:

  • Nginx
  • Apache

2) 애플리케이션 서버

  • 실제 비즈니스 로직 처리
  • 로그인 검증
  • 게시글 저장 처리
  • API 응답 생성

예:

  • Node.js
  • Java (Spring)
  • PHP
  • Python (Django)

3) 데이터베이스 서버

  • 데이터 저장
  • 조회, 수정, 삭제 수행

대표적인 DB:

  • MySQL
  • PostgreSQL
  • MariaDB

4) 캐시 서버

  • 자주 사용하는 데이터를 메모리에 저장
  • 응답 속도 향상

예:

  • Redis
  • Memcached

왜 역할을 나누는가

  • 성능 향상
  • 부하 분산
  • 보안 분리
  • 확장성 확보
  • 장애 영향 최소화

4. 서버 운영체제(OS)

서버도 운영체제가 필요하다.

대표적인 서버 OS는 다음과 같다.

1) Linux 계열

  • Ubuntu
  • Rocky Linux
  • Debian
  • CentOS (구버전)

특징:

  • 오픈소스
  • 라이선스 비용 없음
  • 서버에 최적화
  • 클라우드에서 가장 많이 사용

2) Windows Server

  • Microsoft 환경에 적합
  • .NET 기반 서비스와 궁합이 좋음
  • 라이선스 비용 존재

클라우드에서 Linux가 많은 이유

  • 비용 절감
  • 자동화 도구와의 호환성
  • 컨테이너 생태계
  • 안정성

5. 서버는 항상 한 대로 운영할까 (Tier 구조)

초기에는 한 대로 시작할 수 있다.
하지만 확장성과 안정성을 위해 구조를 분리한다.


1) 단일 서버 구조 (All-in-One)

  • 웹 + 앱 + DB 모두 한 서버
flowchart LR
    U["사용자/브라우저"] --> S["단일 서버 - Web + App + DB"]

장점:

  • 간단
  • 초기 구축 빠름

단점:

  • 장애 시 전체 중단
  • 확장 어려움

2) 2-Tier 구조

  • 애플리케이션 서버와 DB 서버 분리
flowchart LR
    U["사용자/브라우저"] --> A["App 서버 - Web + App"]
    A --> DB["DB 서버"]

장점:

  • DB 보호 가능
  • 앱 서버 확장 가능

3) 3-Tier 구조

  • 웹 서버 / 애플리케이션 서버 / DB 서버 분리
flowchart LR
    U["사용자/브라우저"] --> W["Web 서버"]
    W --> A["App 서버"]
    A --> DB["DB 서버"]

장점:

  • 역할 명확
  • 확장 유연
  • 보안 분리 가능

실무 확장 예시

flowchart LR
    U["사용자"] --> LB["로드밸런서"]
    LB --> W["Web 서버"]
    W --> A1["App 서버 1"]
    W --> A2["App 서버 2"]
    A1 --> DB["DB 서버"]
    A2 --> DB

이 구조가 클라우드에서 가장 흔한 형태다.


6. 클라우드와 서버의 연결

클라우드는 서버 구조를 바꾸는 것이 아니다.

  • 웹 서버
  • 앱 서버
  • DB 서버
  • 캐시 서버

이 구성은 그대로 유지된다.

차이점은:

  • 물리 장비를 직접 소유하지 않음
  • 서버를 빠르게 생성 가능
  • 자동 확장 가능
  • 관리형 DB 사용 가능

즉,

클라우드는 기존 서버 아키텍처를
더 유연하게 운영할 수 있게 만든 환경이다.


7. 이 장의 핵심 정리

  1. 서버는 요청을 처리하는 컴퓨터다.
  2. 웹 서비스는 여러 역할의 서버로 구성된다.
  3. Tier 구조는 확장성과 보안을 위한 설계 방식이다.
  4. Linux는 클라우드에서 가장 많이 사용되는 서버 OS다.
  5. 클라우드는 서버 구조 위에서 동작한다.

9장. 상태와 무상태의 이해

1. 상태란 무엇인가

상태(State)란
어떤 시스템이 이전 요청의 정보를 기억하고 있는 것을 의미한다.

예를 들어:

  • 로그인 후 세션 정보가 서버 메모리에 저장됨
  • 장바구니 정보가 특정 서버에 저장됨
  • 사용자의 임시 데이터가 서버 내부에 보관됨

이 경우, 서버는 이전 요청의 정보를 “기억”하고 있다.

이를 상태가 있는 서버라고 한다.


2. 상태가 있는 서버의 문제점

상태가 있는 서버는 처음에는 단순해 보인다.
하지만 확장성과 연결되면 문제가 발생한다.

1) 로드밸런싱 문제

사용자가 로그인 후
서버 A에 세션이 저장되었다고 가정하자.

이후 요청이 서버 B로 전달되면
B는 로그인 정보를 알지 못한다.

즉, 특정 사용자가 특정 서버에 묶이게 된다.

이를 세션 종속성(Session Affinity) 문제라고 한다.

2) 서버 장애 시 데이터 손실

서버 A가 장애로 종료되면
그 서버 메모리에 있던 세션 정보도 사라진다.

사용자는 다시 로그인해야 할 수 있다.

3) 수평 확장의 제약

클라우드에서는 서버를 여러 대로 늘리는 것이 일반적이다.

하지만 상태가 서버 내부에 있다면
서버를 자유롭게 늘리고 줄이기 어렵다.

Auto Scaling이 제대로 동작하기 힘들다.


3. 무상태란 무엇인가

무상태(Stateless)는
각 요청이 독립적으로 처리되는 구조를 의미한다.

즉,

  • 어떤 서버가 요청을 받아도 동일한 결과를 낼 수 있어야 한다.
  • 이전 요청 정보에 의존하지 않는다.

서버는 단순히 요청을 처리하는 역할만 한다.


4. 왜 클라우드에서는 무상태가 중요한가

클라우드는 다음을 전제로 설계된다.

  • 서버는 언제든지 종료될 수 있다.
  • Auto Scaling으로 서버 수가 변한다.
  • 다중 AZ 환경에서 서버가 분산된다.

이 환경에서 상태를 서버 내부에 저장하면
구조가 복잡해진다.

따라서 클라우드에서는
무상태 설계를 기본 전략으로 사용한다.


5. 무상태 설계는 어떻게 구현하는가

무상태가 되기 위해서는 상태를 서버 외부로 분리해야 한다.

1) 세션을 데이터베이스에 저장

  • 로그인 정보
  • 사용자 설정 정보

모든 서버가 같은 DB를 참조한다.

2) 캐시 서버 사용

Redis 같은 외부 캐시에 세션 저장.

장점:

  • 빠른 속도
  • 서버 간 공유 가능

3) JWT 방식 사용

로그인 정보를 토큰 형태로 사용자에게 전달하고
서버는 토큰을 검증만 수행.

서버가 세션을 저장하지 않아도 된다.


6. 상태와 무상태 비교

구분상태 있음무상태
세션 저장 위치서버 내부외부 저장소
확장성제한적매우 유리
장애 대응취약유리
로드밸런싱복잡단순

7. 현실적인 접근

완전한 무상태가 항상 가능한 것은 아니다.

예를 들어:

  • 파일 업로드 중 임시 저장
  • 대용량 처리 작업
  • 트랜잭션 처리

이 경우에도
핵심 원칙은 같다.

서버 내부가 아니라 외부 저장소에 상태를 둔다.


8. 설계 철학으로서의 무상태

온프레미스 사고:

  • 서버는 오래 살아있다.
  • 메모리에 저장해도 괜찮다.

클라우드 사고:

  • 서버는 언제든지 교체될 수 있다.
  • 서버는 일시적인 존재다.
  • 상태는 외부에 둔다.

이 사고 전환이
클라우드 네이티브 설계의 핵심이다.


9. 이 장의 핵심 정리

  1. 상태는 이전 요청 정보를 기억하는 것이다.
  2. 상태가 서버 내부에 있으면 확장이 어렵다.
  3. 무상태는 요청이 독립적인 구조다.
  4. 클라우드에서는 무상태 설계를 권장한다.
  5. 상태는 외부 저장소에 분리하는 것이 원칙이다.

10장. 서버와 네트워크의 연결 이해

이 장에서 말하고자 하는 것

우리는 서버가 무엇인지 배웠다.
그렇다면 이제 질문이 생긴다.

사용자의 요청은 어떻게 그 서버까지 도달하는가?

이 장은
서버와 사용자를 연결하는 네트워크의 기본 원리를 이해하는 장이다.


1. 우리는 어떻게 서버에 접속하는가

사용자가 브라우저를 열고
어떤 웹사이트에 접속한다고 가정해보자.

우리는 보통 이렇게 입력한다.

www.example.com

엔터를 누르면 화면이 나타난다.

하지만 실제로는 다음과 같은 과정이 발생한다.

  1. 내 컴퓨터가 서버의 위치를 알아낸다.
  2. 그 위치로 데이터를 보낸다.
  3. 서버가 응답을 다시 보낸다.

이 과정을 가능하게 하는 것이 네트워크다.


2. 네트워크란 무엇인가

네트워크는

컴퓨터와 컴퓨터를 연결하는 구조

다.

인터넷은 전 세계의 수많은 네트워크가 연결된 거대한 구조다.

우리가 웹사이트에 접속한다는 것은
내 컴퓨터가 인터넷을 통해
어딘가에 있는 서버와 통신한다는 의미다.


3. 서버의 위치는 어떻게 알까 — IP 주소

서버와 통신하려면
서버의 “위치”를 알아야 한다.

이 위치 정보가 IP 주소다.

IP 주소는

네트워크 안에서 장치를 구분하는 주소

다.

예:

  • 203.0.113.10
  • 142.250.206.78

택배를 보내려면 주소가 필요하듯,
데이터를 보내려면 IP 주소가 필요하다.


4. 그런데 우리는 왜 IP를 입력하지 않을까 — DNS

우리는 숫자(IP) 대신 도메인을 입력한다.

www.google.com

DNS(Domain Name System)는

사람이 기억하기 쉬운 도메인 이름을
컴퓨터가 통신 가능한 IP 주소로 변환해주는 시스템

이다.

DNS 흐름 다이어그램

sequenceDiagram
    participant B as 브라우저
    participant DNS as DNS 서버
    participant S as 웹 서버

    B->>DNS: www.example.com 의 IP는?
    DNS-->>B: 203.0.113.10

    B->>S: 203.0.113.10:443 으로 요청
    S-->>B: HTML/데이터 응답

요약하면:

도메인 입력 → DNS 조회 → IP 확인 → 서버 연결


5. 모든 IP가 인터넷에 공개되어 있을까

IP에는 두 종류가 있다.

이걸 이해하기 위해 전화번호로 비유해보자.

  • 공인 IP → 일반 전화번호. 누구든 그 번호로 전화를 걸 수 있다.
  • 사설 IP → 회사 내선번호. 회사 안에서만 통하고, 외부에서는 그 번호로 전화할 수 없다.

1) 공인 IP

인터넷 전체에서 유일한 주소다.

외부 어디서든 이 IP로 직접 접근할 수 있기 때문에
웹 서버처럼 외부에 공개해야 하는 서버에 사용한다.

예: 1.2.3.4


2) 사설 IP

내부 네트워크 안에서만 쓰이는 주소다.

외부에서는 이 IP로 직접 접근할 수 없기 때문에
DB 서버처럼 외부에 노출되면 안 되는 시스템에 사용한다.

예: 192.168.0.10


그러면 같은 IP를 공인으로도 쓰고 사설로도 쓰는 건가?

아니다.

사설 IP로 쓸 수 있는 범위는 처음부터 정해져 있다.
아래 범위에 해당하면 무조건 사설 IP다.

  • 10.0.0.0 ~ 10.255.255.255
  • 172.16.0.0 ~ 172.31.255.255
  • 192.168.0.0 ~ 192.168.255.255

집에서 공유기에 연결했을 때
192.168.0.x 같은 IP를 본 적이 있을 거다.

그게 바로 사설 IP다. 이 범위의 IP는 인터넷에서 직접 사용할 수 없다.

즉,

공인과 사설은 내가 고르는 게 아니라
IP 주소 자체가 어느 범위에 속하느냐로 이미 정해져 있는 것이다.


6. 사설 IP 서버가 외부와 통신해야 하는 경우 — NAT

앞에서 사설 IP는 외부에서 직접 접근할 수 없다고 했다.

그러면 이런 의문이 든다.

사설 IP를 쓰는 서버는 인터넷이 아예 안 되는 건가?

결론부터 말하면, 그렇지 않다.


사설 IP 서버도 외부와 통신이 필요한 순간이 있다

생각보다 흔한 상황이다.

  • 결제/문자/인증 같은 외부 API를 호출해야 할 때
  • 서버의 OS나 패키지를 업데이트해야 할 때
  • 도커 이미지 같은 것을 외부에서 내려받아야 할 때

이런 경우, 사설 IP 그대로는 인터넷에 요청을 보낼 수 없다.

그래서 등장하는 것이 NAT(Network Address Translation) 다.


NAT는 뭘 하는 건가

쉽게 말하면,

내부 사설 IP를 외부 공인 IP로 바꿔치기해서
인터넷과 통신할 수 있게 해주는 기술이다.

회사 내선번호로는 외부에 전화를 걸 수 없지만,
회사 대표번호를 통해 나가면 외부 통화가 가능한 것과 같다.

예를 들어:

  • 내부 서버 IP: 10.0.0.5 (사설)
  • 외부로 나갈 때: 1.2.3.4 (공인)로 변환되어 통신

그래서 NAT가 주는 이점은?

내부 서버는 필요할 때 인터넷을 사용할 수 있으면서도,
외부에서 직접 들어오는 접근은 기본적으로 차단된다.

즉, 나가는 건 되지만 들어오는 건 안 되는 구조다.

이게 바로 사설 IP + NAT 조합이
보안 측면에서 많이 쓰이는 이유다.


7. IP만 알면 서버에 접속할 수 있을까?

서버의 IP를 알면 그 서버까지는 찾아갈 수 있다.

하지만 한 가지 문제가 있다.


서버 안에는 여러 서비스가 동시에 돌아갈 수 있다

예를 들어 하나의 서버에서 이런 것들이 동시에 실행되고 있다고 해보자.

  • 웹사이트를 보여주는 서비스
  • 관리자가 원격으로 접속하는 서비스
  • 데이터를 저장하고 꺼내는 데이터베이스

IP는 하나인데, 서비스는 여러 개다.

그러면 자연스럽게 이런 의문이 든다.

같은 IP로 들어왔는데,
이 요청이 웹사이트를 보려는 건지,
원격 접속을 하려는 건지,
데이터베이스에 접근하려는 건지
서버는 어떻게 구분할까?


그래서 포트(Port)가 존재한다

앞에서 IP를 택배 주소에 비유했다.

택배 주소로 건물까지는 찾을 수 있지만,
그 건물 안에 카페도 있고 사무실도 있고 병원도 있다면
몇 호로 가야 하는지도 알아야 한다.

IP = 건물까지의 주소
포트 = 건물 안의 호수

서버도 마찬가지다.
같은 IP라도 포트 번호에 따라 다른 서비스로 연결된다.

대표적인 포트 번호:

포트 번호서비스쉽게 말하면
80HTTP일반 웹사이트
443HTTPS보안 웹사이트
22SSH원격 접속
3306MySQL데이터베이스

그래서 실제로 브라우저가 웹사이트에 접속할 때는
IP만 쓰는 게 아니라 포트까지 포함해서 요청을 보낸다.

예: 1.2.3.4:443 → 이 서버의 보안 웹사이트 서비스로 연결해줘

다만 브라우저가 443 같은 기본 포트는 자동으로 붙여주기 때문에
우리가 직접 입력할 일은 거의 없다.


8. 전체 흐름 다시 정리

웹사이트 접속 과정은 다음과 같다.

  1. 도메인 입력
  2. DNS가 IP 반환
  3. 브라우저가 IP의 특정 포트로 요청 전송
  4. 서버가 응답 반환

이 모든 과정은 네트워크 규칙 위에서 동작한다.


9. 이 장의 핵심 정리

  1. 서버와 사용자는 네트워크로 연결된다.
  2. 서버의 위치는 IP 주소로 식별된다.
  3. 우리는 도메인을 입력하지만 실제 통신은 IP로 이루어진다.
  4. 공인 IP와 사설 IP는 이미 정해진 주소 대역이다.
  5. NAT는 내부 IP를 외부 IP로 변환한다.
  6. 포트는 하나의 서버 안에서 서비스를 구분하는 번호다.

다음 장에서는 이 개념을 바탕으로 클라우드 안에서 나만의 네트워크를 만드는 방법, VPC와 서브넷 구조를 살펴본다.

11장. 트래픽과 접근 제어의 이해

이 장에서 말하고자 하는 것

10장에서 우리는 다음을 배웠다.

  • 도메인은 DNS를 통해 IP로 변환된다.
  • 서버는 IP와 포트로 식별된다.
  • 사설 IP와 공인 IP는 용도가 다르다.
  • 포트는 서버 안의 서비스를 구분한다.

그렇다면 이제 질문이 생긴다.

서버는 아무나 언제든지 접근할 수 있을까?

답은 아니다.


1. 서버는 기본적으로 열려 있지 않다

보안을 생각해보자.

  • 데이터베이스가 인터넷에 그대로 공개되어 있다면?
  • 관리자 접속 포트가 모든 사람에게 열려 있다면?

매우 위험하다.

그래서 서버는 기본적으로
모든 접근을 허용하는 것이 아니라 차단하는 것이 원칙이다.

필요한 것만 열어준다.

이것이 접근 제어(Access Control)다.


2. 트래픽이란 무엇인가

네트워크에서 오가는 데이터를 트래픽(Traffic)이라고 한다.

서버 기준으로 보면
트래픽은 두 방향이 있다.

  • 들어오는 트래픽
  • 나가는 트래픽

이를 인바운드와 아웃바운드라고 부른다.


3. 인바운드와 아웃바운드

인바운드(Inbound)

외부 → 서버로 들어오는 트래픽

예:

  • 사용자가 웹사이트 접속
  • 개발자가 원격 접속

아웃바운드(Outbound)

서버 → 외부로 나가는 트래픽

예:

  • 외부 API 호출
  • 패키지 다운로드
  • 운영체제 업데이트

4. 포트를 연다는 의미

10장에서 포트를 배웠다.

  • 80 → HTTP
  • 443 → HTTPS
  • 22 → SSH
  • 3306 → MySQL

이제 중요한 질문이 있다.

“포트를 연다”는 것은 무엇을 의미할까?

그 의미는 다음과 같다.

특정 포트로 들어오는 인바운드 트래픽을 허용한다는 것

예를 들어:

  • 443번 포트 인바운드 허용 → 웹 접속 가능
  • 22번 포트 특정 IP만 허용 → 관리자만 접속 가능
  • 3306번 포트 외부 차단 → 데이터베이스 보호

포트와 접근 제어는 항상 함께 작동한다.


5. 접근 제어의 실제 예시

웹 서버의 경우

  • 인바운드 443 허용 (모든 사용자)
  • 인바운드 22는 관리자 IP만 허용
  • 아웃바운드는 외부 API 호출 가능

데이터베이스 서버의 경우

  • 인바운드 외부 전면 차단
  • 내부 서버 IP만 허용
  • 아웃바운드는 필요 시 허용

이렇게 하면
외부에서 DB에 직접 접근하는 위험을 막을 수 있다.


6. 왜 기본은 차단인가

보안의 기본 원칙은 다음과 같다.

필요한 것만 허용하고, 나머지는 차단한다.

이를 최소 권한 원칙(Principle of Least Privilege)이라고 한다.

인터넷에 연결된 서버는
항상 외부의 접근 시도를 받을 수 있다.

따라서 “기본 허용”은 매우 위험하다.


7. 이 장의 핵심 정리

  1. 서버는 기본적으로 열려 있지 않다.
  2. 트래픽은 인바운드와 아웃바운드로 나뉜다.
  3. 포트를 연다는 것은 해당 서비스 접근을 허용한다는 의미다.
  4. 접근 제어는 보안을 위한 기본 장치다.
  5. 최소 권한 원칙은 서버 설계의 기본이다.

12장. EC2란 무엇인가

이 장에서 말하고자 하는 것

우리는 이제 알고 있다.

  • 서버란 무엇인가
  • 네트워크가 어떻게 연결되는가
  • 접근은 어떻게 통제되는가
  • 클라우드 서버는 가상 서버라는 것

그렇다면 이제 질문이 생긴다.

AWS에서는 이 가상 서버를 무엇이라고 부를까?

그 이름이 바로 EC2다.


1. EC2의 의미

EC2는 Elastic Compute Cloud의 약자다.

이 이름에는 세 가지 의미가 담겨 있다.

  • Elastic → 필요에 따라 늘어나고 줄어드는
  • Compute → 연산 자원 (CPU, 메모리)
  • Cloud → 클라우드 환경

즉,

EC2는 클라우드에서 제공하는 탄력적인 가상 서버다.


2. EC2는 무엇을 제공하는가

EC2는 우리가 직접 서버를 구매하지 않고도
다음 자원을 사용할 수 있게 해준다.

  • CPU
  • 메모리
  • 네트워크
  • 디스크

우리는 필요한 사양을 선택하고
몇 분 안에 서버를 생성할 수 있다.


3. EC2는 물리 서버인가?

아니다.

EC2는 실제 장비가 아니라
AWS의 물리 서버 위에 만들어진 가상 서버다.

사용자 입장에서는
일반 서버와 동일하게 동작한다.

  • 운영체제를 설치할 수 있고
  • 프로그램을 실행할 수 있으며
  • 웹 서비스를 운영할 수 있다.

하지만 실제 장비 관리(전원, 교체, 하드웨어 장애 등)는
AWS가 담당한다.


4. EC2에서 “인스턴스”란 무엇인가

EC2에서는 서버를 인스턴스(Instance) 라고 부른다.

이 말은

하나의 실행 중인 가상 서버

를 의미한다.

즉,

  • EC2 서비스 = 서버를 제공하는 시스템
  • EC2 인스턴스 = 우리가 만든 실제 가상 서버

라고 이해하면 된다.


5. 인스턴스 타입이란 무엇인가

EC2를 만들 때는 사양을 선택해야 한다.

예를 들어:

  • 작은 서버
  • 메모리가 많은 서버
  • CPU가 많은 서버

이 사양 묶음을 인스턴스 타입이라고 한다.

쉽게 말해:

어떤 크기의 서버를 빌릴 것인가

를 선택하는 단계다.

작게 시작할 수도 있고,
더 큰 사양으로 변경할 수도 있다.


6. EC2는 어떻게 생성되는가

EC2를 생성하는 과정은 다음과 같다.

  1. 운영체제 선택
  2. 서버 사양 선택
  3. 네트워크 설정
  4. 접근 규칙 설정
  5. 서버 생성

지금은 이 흐름만 이해하면 충분하다.

각 단계에서 등장하는 개념은
다음 장에서 하나씩 살펴본다.


7. 온프레미스 서버와 EC2의 차이

구분온프레미스EC2
서버 확보 방식장비 구매클릭으로 생성
준비 시간수일 ~ 수주몇 분
확장장비 추가 필요즉시 생성 가능
하드웨어 관리직접 관리AWS가 관리

즉,

EC2는 서버를 “소유”하는 것이 아니라
필요할 때 “사용”하는 방식이다.


8. 왜 EC2가 중요한가

클라우드 환경에서 대부분의 서비스는 이 EC2 위에서 시작된다.

  • 웹 서버
  • API 서버
  • 배치 서버
  • 테스트 서버

모두 EC2 위에서 동작할 수 있다.

EC2를 이해하면
클라우드 인프라의 중심을 이해한 것이다.


9. 이 장의 핵심 정리

  1. EC2는 AWS가 제공하는 가상 서버 서비스다.
  2. EC2 인스턴스는 하나의 실행 중인 가상 서버다.
  3. 우리는 서버 사양(인스턴스 타입)을 선택해 생성한다.
  4. 서버 생성과 관리는 소프트웨어적으로 이루어진다.
  5. 물리 장비 관리는 AWS가 담당한다.

13장. 인스턴스 타입 이해하기

이 장에서 말하고자 하는 것

EC2는 가상 서버다.
그렇다면 이제 중요한 질문이 생긴다.

어떤 서버를 선택해야 할까?

무조건 가장 큰 서버를 쓰면 될까?

그렇지 않다.
클라우드는 사용한 만큼 비용이 발생한다.

따라서 인스턴스 선택은
성능과 비용을 동시에 고려하는 결정이다.


1. 인스턴스 타입은 실제로 어떻게 생겼을까?

AWS 콘솔에서 인스턴스 타입은 보통 이렇게 보인다.

t3.micro
m6i.large
c7g.xlarge
r6g.2xlarge
p4d.24xlarge
i4i.large

처음 보면 복잡해 보이지만 구조를 알면 어렵지 않다.

형식은 다음과 같다.

[패밀리][세대][옵션].[크기]

예:

m6i.large

2. 이름을 읽는 법

1️⃣ 패밀리 (m, t, c, r, p, i 등)

서버의 “성격”을 의미한다.

  • T → 버스트형
  • M → 균형형
  • C → CPU 중심
  • R → 메모리 중심
  • I → 스토리지 중심
  • P → GPU

2️⃣ 세대 숫자

숫자가 높을수록 최신 세대다.

일반적으로:

  • 성능 개선
  • 전력 효율 개선
  • 내부 하드웨어 개선

이 이루어진다.

3️⃣ 옵션 문자 (i, g 등)

  • i → Intel 기반
  • g → ARM(Graviton) 기반

즉, CPU 아키텍처를 나타낸다.

4️⃣ 크기 (size)

  • nano
  • micro
  • small
  • medium
  • large
  • xlarge
  • 2xlarge …

클수록 vCPU와 메모리가 증가한다.


3. 인스턴스를 구성하는 핵심 요소

1️⃣ vCPU

가상 CPU 개수.

  • 동시 처리 능력과 관련
  • 요청이 많을수록 중요

2️⃣ 메모리 (RAM)

  • 캐시
  • 세션
  • DB 처리
  • 대용량 데이터 분석

3️⃣ 스토리지

  • 네트워크 기반 디스크(EBS)
  • 로컬 고속 디스크(Instance Store)

I/O 성능은 DB, 로그 분석 등에 큰 영향을 준다.

4️⃣ 네트워크 성능

  • 대역폭
  • 패킷 처리 능력

트래픽이 많은 서비스에서는 매우 중요하다.


4. 같은 vCPU라도 성능은 다를 수 있다

많은 초보자가 이렇게 생각한다.

vCPU 2개면 다 같은 성능 아닌가?

그렇지 않다.

이유 1️⃣ CPU 아키텍처 차이

예:

  • m6i → Intel 기반
  • m6g → ARM 기반

같은 2 vCPU라도 내부 CPU 구조가 다르다.

ARM은 전력 효율이 좋고 비용 대비 성능이 뛰어난 경우가 많다.
Intel/AMD는 기존 소프트웨어 호환성이 넓다.

이유 2️⃣ CPU 세대 차이

예:

  • m5.large (이전 세대)
  • m6i.large (신형 세대)

같은 2 vCPU라도:

  • 클럭 속도
  • IPC(한 클럭당 처리 명령 수)
  • 캐시 구조

가 다르다.

최신 세대는
같은 vCPU 수라도 더 많은 작업을 처리할 수 있다.

이유 3️⃣ vCPU는 물리 코어가 아닐 수 있다

vCPU는 실제 물리 코어와 1:1이 아닐 수 있다.

많은 경우:

  • 1 물리 코어 = 2 vCPU (하이퍼스레딩)

즉,

vCPU 숫자는 성능의 절대 기준이 아니다.

확인 팁


5. 메모리도 세대에 따라 차이가 있다

물리 서버에서는:

  • DDR4
  • DDR5

같은 메모리 세대 차이가 존재한다.

AWS에서는 메모리 규격을 직접 선택하지는 않지만,
세대가 올라갈수록:

  • 메모리 대역폭 향상
  • 지연 시간 감소

같은 개선이 반영된다.

즉,

최신 세대 인스턴스는 CPU뿐 아니라 메모리 성능도 함께 개선되는 경우가 많다.


6. 주요 패밀리 요약

T 계열 (Burstable)

  • 저렴
  • 순간 성능 상승 가능
  • 개발/소규모 서비스

M 계열 (General Purpose)

  • CPU/메모리 균형
  • 일반 웹 서비스

C 계열 (Compute Optimized)

  • CPU 중심
  • 연산 작업, 고트래픽 API

R 계열 (Memory Optimized)

  • 메모리 중심
  • 데이터베이스, 캐시 서버

I 계열 (Storage Optimized)

  • 고성능 디스크
  • 로그 분석, 고성능 DB

P 계열 (GPU)

  • 머신러닝, AI
  • 비용 매우 높음

7. 무조건 큰 서버가 답일까?

클라우드는 시간 단위 과금이다.

과도한 사양 선택은
매월 불필요한 비용을 발생시킨다.

따라서 전략은 다음과 같다.

  1. 작은 사양으로 시작
  2. 모니터링
  3. 부족하면 확장
  4. 과하면 축소

8. 서비스 특성에 따라 선택하기

서비스 유형추천 방향
일반 웹 서버M 또는 T
고트래픽 APIC
데이터베이스R
로그 분석I
머신러닝P

9. 이 장의 핵심 정리

  1. 인스턴스 타입은 이름 구조를 이해하면 읽을 수 있다.
  2. 패밀리는 서버의 성격을 의미한다.
  3. 같은 vCPU라도 CPU 아키텍처와 세대에 따라 성능이 다를 수 있다.
  4. 최신 세대 인스턴스는 성능과 효율이 개선되는 경우가 많다.
  5. 무조건 큰 사양이 정답은 아니다.
  6. 서비스 특성과 비용을 함께 고려해야 한다.

14장. EC2 스토리지 구성 이해하기

이 장에서 말하고자 하는 것

EC2 인스턴스를 생성할 때 우리는 다음을 선택한다.

  • 인스턴스 타입 (CPU, 메모리)
  • 스토리지

많은 경우 CPU와 메모리에 집중하지만,
실제 운영 환경에서는 디스크 성능이 병목이 되는 경우가 많다.

스토리지를 이해하는 것은
안정적인 서비스 운영을 위한 중요한 요소다.


1. 서버에서 스토리지는 어떤 역할을 하는가

서버의 디스크에는 다음이 저장된다.

  • 운영체제
  • 애플리케이션 코드
  • 로그 파일
  • 업로드 파일
  • 데이터베이스 데이터

즉, 서버의 모든 데이터는 결국 디스크에 기록된다.

CPU가 빠르더라도
디스크에서 데이터를 읽고 쓰는 속도가 느리면
전체 응답 속도가 느려질 수 있다.


2. 디스크 성능을 판단하는 두 가지 기준

스토리지 성능은 주로 두 가지 수치로 표현된다.

① IOPS

IOPS는

초당 처리 가능한 읽기/쓰기 작업 횟수

를 의미한다.

예:

  • IOPS 3,000 → 1초에 3,000번 입출력 작업 가능
  • IOPS 16,000 → 더 많은 요청을 처리 가능

작은 데이터를 자주 읽고 쓰는 환경에서 중요하다.

예:

  • 사용자 정보 조회
  • 게시글 조회
  • 주문 처리

② Throughput

Throughput은

초당 전송 가능한 데이터 양 (MiB/s)

을 의미한다.

예:

  • 250 MiB/s
  • 1,000 MiB/s

큰 파일을 다루는 환경에서 중요하다.

예:

  • 대용량 파일 업로드
  • 백업 작업
  • 로그 파일 처리

IOPS와 Throughput의 차이

  • IOPS → 작업 “횟수”
  • Throughput → 데이터 “양”

서비스 특성에 따라
어떤 수치가 더 중요한지 달라진다.


3. EC2에서 사용하는 스토리지 종류

EC2에서 주로 사용하는 스토리지는 두 가지다.

① EBS (Elastic Block Store)

  • 네트워크 기반 블록 스토리지
  • 인스턴스와 분리 가능
  • 스냅샷 지원
  • 가장 일반적으로 사용됨

대부분의 운영 서버는 EBS를 사용한다.

② Instance Store

  • 물리 서버에 직접 연결된 로컬 디스크
  • 매우 빠른 I/O 성능
  • 인스턴스 종료 시 데이터 삭제

임시 데이터나 캐시 용도로 사용된다.


4. EBS 볼륨 타입별 성능 비교

주요 EBS 타입의 공식 최대 성능은 다음과 같다.

EBS 볼륨 타입IOPS (최대)Throughput (최대)특징
gp3최대 16,000최대 1,000 MiB/s범용 SSD, 기본 선택
io2최대 256,000최대 4,000 MiB/s고성능 SSD
st1최대 500최대 500 MiB/sHDD, 대용량 순차 작업
sc1최대 250최대 250 MiB/s저렴한 HDD, 장기 보관

공식 성능 수치는 AWS 문서에서 확인 가능합니다: EBS 볼륨 성능 비교 표 (AWS)


5. 각 볼륨 타입의 특징

gp3 (범용 SSD)

  • 기본 IOPS 3,000
  • IOPS와 Throughput을 독립적으로 조정 가능
  • 비용 대비 성능 우수

일반적인 웹 서비스나 API 서버에 적합하다.

io2 (Provisioned IOPS SSD)

  • 매우 높은 IOPS 지원
  • 지연 시간이 낮음
  • 비용이 높음

대규모 데이터베이스나
고성능이 필수인 환경에 적합하다.

st1

  • HDD 기반
  • 순차 읽기/쓰기에 적합
  • IOPS는 낮음

대용량 로그 처리나 분석용 저장소에 적합하다.

sc1

  • 가장 저렴한 HDD
  • 접근 빈도가 낮은 데이터에 적합

백업이나 장기 보관용 데이터에 사용된다.


6. 스토리지 때문에 발생하는 성능 차이

다음과 같은 상황이 있을 수 있다.

  • CPU 사용률은 낮다.
  • 메모리도 충분하다.
  • 그런데 서비스 응답이 느리다.

이 경우 디스크 성능이 부족할 가능성이 있다.

특히 데이터베이스는
디스크 성능에 직접적인 영향을 받는다.


7. 운영체제 디스크와 데이터 디스크 분리

EC2 인스턴스를 생성하면
운영체제가 설치되는 기본 디스크가 생성된다.

이 디스크에는:

  • 운영체제
  • 애플리케이션
  • 데이터

를 모두 저장할 수 있다.

그러나 운영 환경에서는 보통 분리한다.

  • 디스크 1 → 운영체제 전용
  • 디스크 2 → 데이터 전용

이렇게 분리하면:

  • 운영체제 문제 발생 시 데이터 보호 가능
  • 데이터 디스크만 확장 가능
  • 백업 및 복구 전략 수립이 용이

8. 스토리지와 인스턴스의 관계

EBS는 네트워크를 통해 인스턴스와 연결된다.

따라서 인스턴스 타입에 따라:

  • 최대 EBS 대역폭
  • 처리 가능한 I/O 한계

가 존재한다.

즉,

스토리지 성능은 디스크 타입뿐 아니라
인스턴스 사양과도 연결되어 있다.

서버 설계는 항상 전체 균형을 고려해야 한다.


9. 스토리지 선택 전략

  1. 일반적인 서비스는 gp3로 시작한다.
  2. 실제 모니터링을 통해 디스크 사용량과 I/O를 확인한다.
  3. 필요 시 IOPS를 조정한다.
  4. 고성능이 필요한 경우에만 io2를 고려한다.

클라우드는
초기에 과도한 사양을 선택할 필요가 없다.


10. 이 장의 핵심 정리

  1. 디스크는 서버 성능에 큰 영향을 준다.
  2. IOPS는 작업 횟수, Throughput은 데이터 전송량이다.
  3. gp3는 대부분의 환경에서 적합하다.
  4. io2는 고성능이 필요한 경우에 사용한다.
  5. 운영체제와 데이터 디스크를 분리하면 관리가 용이하다.
  6. 스토리지 성능은 인스턴스 사양과도 연결되어 있다.

15장. AMI와 이미지 기반 배포

이 장에서 말하고자 하는 것

EC2를 한 대 띄울 때 우리는 다음을 선택했다.

  • 운영체제
  • 인스턴스 타입
  • 스토리지

그런데 운영체제는 어디서 오는가?
미리 준비된 디스크 이미지에서 온다.

AMI (Amazon Machine Image)

다.

AMI는 EC2의 “디스크 + 설정 묶음” 이다.
같은 AMI로 띄우면 어디서든 같은 모양의 서버가 만들어진다.

이 장은 AMI의 구조와 운영 활용을 본다.


1. AMI는 무엇을 담고 있는가

AMI
 ├─ 운영체제 (Amazon Linux · Ubuntu · Windows 등)
 ├─ 미리 설치된 소프트웨어
 ├─ 디스크 스냅샷 (루트 볼륨 + 추가 볼륨)
 ├─ 부팅 설정
 └─ 권한 정보 (누가 이 AMI를 쓸 수 있는지)

EC2를 생성할 때

"AMI ami-xxxx → 인스턴스 타입 t3.small → ..."

이 AMI가 곧 새 EC2의 출발점이다.


2. AMI의 종류

1. AWS 제공 AMI

Amazon Linux 2023, Ubuntu LTS, Windows Server 등 OS 벤더 표준 이미지.

처음 띄울 때 거의 항상 여기서 시작

2. 마켓플레이스 AMI

서드파티가 만들어 판매하는 이미지 (예: Bitnami WordPress).

3. 커뮤니티 AMI

누구나 공개한 이미지.

운영에는 절대 쓰지 않는다 — 출처 불명, 보안 위험

4. 본인의 커스텀 AMI

직접 만들어 등록한 이미지.

운영의 자리는 보통 여기


3. 왜 커스텀 AMI를 만드는가

EC2를 띄울 때마다 OS · 패치 · 에이전트 · 애플리케이션을 처음부터 깔면

  • 시작이 느리다 (수 분 ~ 십수 분)
  • 매번 결과가 미묘하게 다를 수 있다
  • 실패 가능 지점이 많다

미리 다 준비된 AMI를 만들어 두면

  • 인스턴스 시작이 빠르다 (수십 초)
  • 동일한 상태로 매번 시작
  • Auto Scaling이 신규 인스턴스를 즉시 합류시킬 수 있다

이 사고방식이 Immutable Infrastructure (불변 인프라) 다.


4. AMI를 만드는 두 가지 방법

1. 실행 중인 EC2에서 스냅샷

EC2를 만들고 설정한 뒤 콘솔에서 “Create Image” 클릭.

  • 빠르고 단순
  • 사람이 손으로 한 작업이 그대로 박힘
  • 재현성이 낮다

2. 자동화된 빌드 (Packer)

HashiCorp Packer로 코드(JSON/HCL)로 정의한 AMI 빌드.

1. 임시 EC2 띄움
2. 정의된 프로비저닝 스크립트 실행 (Ansible · Shell)
3. 스냅샷 만들고 AMI로 등록
4. 임시 EC2 정리
  • 재현 가능
  • 코드 리뷰 가능
  • CI에서 자동 빌드

운영의 자리는 거의 항상 Packer


5. 컨테이너 시대의 AMI

EC2 직접 운영에서는 AMI가 핵심이지만,
이 책의 척추(ECS Fargate) 에서는 AMI를 거의 안 만진다.

EC2 시대:  AMI → EC2 → 애플리케이션 배포
컨테이너:  Dockerfile → 이미지 → 컨테이너

대신

  • ECS on EC2 — ECS 최적화 AMI를 쓴다 (AWS가 관리)
  • 일부 데이터 / GPU 워크로드는 여전히 직접 AMI

“AMI = EC2판 컨테이너 이미지” 로 이해하면 사고방식이 자연스럽다


6. AMI 운영의 규칙

□ 베이스 OS는 AWS 공식 또는 사내 검증된 것만
□ 태그로 버전 · 빌드 시각 · 커밋 해시 박기
□ AMI 이름에 환경 · 용도 명시 (web-prod-2026-01-15-sha-abc)
□ 오래된 AMI 정기 정리 (수명 주기)
□ 리전마다 AMI ID가 다름 — 멀티 리전이면 각 리전에 복사
□ KMS 암호화 (운영 AMI는 거의 항상)

7. 우리 서비스에서

이 책의 척추는 ECS Fargate라 AMI 직접 운영은 거의 없다.

대신 다음 경우에 만난다.

- ECS on EC2 (Fargate가 아닌 클러스터)
  → ECS-Optimized AMI (AWS 제공) 사용
- 데이터/GPU 워크로드 (예: ML 학습 노드)
  → 커스텀 AMI (Packer로)
- Bastion · 운영용 워크스테이션
  → 사내 표준 AMI

8. 직접 확인해보기 — CLI

최신 Amazon Linux 2023 AMI 찾기

aws ec2 describe-images \
  --owners amazon \
  --filters "Name=name,Values=al2023-ami-2023.*-x86_64" \
            "Name=state,Values=available" \
  --query 'reverse(sort_by(Images, &CreationDate))[:1].[ImageId,Name]' \
  --output table

본인 AMI 목록

aws ec2 describe-images --owners self

EC2에서 AMI 만들기

aws ec2 create-image \
  --instance-id i-xxx \
  --name "web-prod-2026-01-15" \
  --description "build sha-abc123" \
  --no-reboot

--no-reboot 는 운영 중인 인스턴스 재부팅 없이 만들지만,
파일 시스템 일관성이 살짝 약해질 수 있다.


9. 코드로는 이렇게 생겼다 — Packer + Terraform

Packer로 AMI 빌드

source "amazon-ebs" "web" {
  region        = "ap-northeast-2"
  source_ami_filter {
    filters = {
      name                = "al2023-ami-2023.*-x86_64"
      virtualization-type = "hvm"
      root-device-type    = "ebs"
    }
    owners      = ["amazon"]
    most_recent = true
  }
  instance_type = "t3.small"
  ssh_username  = "ec2-user"
  ami_name      = "web-{{ timestamp }}"
  encrypt_boot  = true
  kms_key_id    = "alias/prod-storage"
  tags = {
    Name        = "web"
    BuildSha    = "${var.git_sha}"
    Environment = "prod"
  }
}

build {
  sources = ["source.amazon-ebs.web"]
  provisioner "shell" {
    inline = [
      "sudo dnf update -y",
      "sudo dnf install -y nginx",
      "sudo systemctl enable nginx",
    ]
  }
}

Terraform에서 AMI 사용

data "aws_ami" "web" {
  most_recent = true
  owners      = ["self"]
  filter {
    name   = "name"
    values = ["web-*"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.web.id
  instance_type = "t3.small"
  subnet_id     = aws_subnet.private_a.id
  ...
}

새 AMI가 빌드되면 다음 Terraform Apply에 자동으로 반영된다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 사람이 콘솔에서 EC2를 띄우고 손으로 설정

재현 불가능. 다음 사람이 같은 환경을 못 만든다.

Packer + 프로비저닝 스크립트로 코드화

안티패턴 2. AMI 이름이 “web-final-final-v2”

어느 게 진짜인지 모른다.

타임스탬프 · 커밋 해시로 자동 이름

안티패턴 3. 옛 AMI를 안 정리한다

수십 개의 옛 AMI + 스냅샷이 누적 → 청구서

AWS Backup · 수명 주기 매니저로 자동 정리

안티패턴 4. 커뮤니티 AMI를 운영에 쓴다

검증되지 않은 코드가 들어 있을 수 있다.


11. 한 줄로 정리

AMI는 EC2의 디스크 이미지이며,
Packer로 코드화해 만들면 컨테이너 이미지와 비슷한 운영이 가능하다


12. 이 장의 핵심 정리

  1. AMI는 EC2를 띄울 때의 OS + 디스크 + 설정 묶음이다.
  2. AWS 공식 AMI에서 시작하고, 커스텀이 필요하면 Packer로 코드화.
  3. Immutable Infrastructure — 인스턴스를 바꾸지 말고 새 AMI로 띄운다.
  4. ECS Fargate에서는 AMI를 거의 만지지 않는다.
  5. 이름 · 태그 · 정리 · 암호화가 운영의 기본이다.
  6. AMI는 리전마다 별도 — 멀티 리전은 복사 필요.

16장. EC2 접속과 권한 — 키 페어 · Session Manager · IAM Role

이 장에서 말하고자 하는 것

EC2를 띄웠다면 다음 질문이 생긴다.

“이 서버에 어떻게 접속해서 들어가지?” “이 서버 안의 코드는 어떻게 AWS API를 호출하지?”

이 둘은 다른 문제다.

  • 사람 → 서버 접속 — SSH · Session Manager
  • 서버 코드 → AWS API — IAM Role (Instance Profile)

이 장은 이 두 가지를 각각 정리한다.


1. 전통 방식 — SSH + 키 페어

키 페어 생성

EC2를 만들 때 키 페어를 선택한다.

공개 키 → AWS가 EC2 안에 자동 설치
비공개 키(.pem) → 사용자가 보관

접속

ssh -i my-key.pem ec2-user@<public-ip>
  • ec2-user (Amazon Linux), ubuntu (Ubuntu) 등 OS별 기본 사용자
  • 22번 포트가 열려 있어야 함

키 페어 운영의 어려움

  • .pem 파일을 누가 가졌는지 추적이 어렵다
  • 키 회전이 번거롭다
  • 22 포트를 인터넷에 노출 → 공격 표면
  • 누가 언제 접속했는지 감사 로그가 약하다

운영 환경에서 SSH 키는 점점 안 쓰는 방향이다


2. 더 나은 방식 — AWS Systems Manager Session Manager

[운영자] → AWS 콘솔 / CLI → IAM 검증 → Session Manager → EC2
  • SSH 키 불필요
  • 22 포트 열 필요 없음 (그래서 인터넷에서도 차단 가능)
  • IAM 권한으로 누가 접속할 수 있는지 통제
  • CloudTrail에 모든 세션 기록 (선택적으로 명령까지 기록)
  • 프라이빗 서브넷의 EC2도 직접 접속 가능 (VPC Endpoint 필요)

운영 환경의 기본은 Session Manager 다


3. Session Manager의 동작

1. EC2에 SSM Agent 설치 (Amazon Linux 2023 · Ubuntu 최근 버전은 기본 설치)
2. EC2에 IAM Role 부여 → AmazonSSMManagedInstanceCore
3. EC2가 시작 시 SSM 서비스에 연결 (아웃바운드 443)
4. 운영자가 콘솔/CLI에서 세션 시작

22 포트도, 키 페어도, 인터넷 노출도 필요 없다.


4. EC2가 AWS API를 호출하려면 — IAM Role + Instance Profile

EC2 안의 코드가 S3를 읽거나 DynamoDB에 쓰려면 권한이 필요하다.

[EC2]
  ↑ 받음
[Instance Profile]
  ↑ 담음
[IAM Role]
  ├─ Trust Policy: ec2.amazonaws.com이 받을 수 있음
  └─ Permission Policy: S3 GetObject 등

EC2 안에서

aws s3 ls

를 그냥 호출하면 자동으로 임시 자격증명이 사용된다.

EC2 안에 액세스 키를 절대 박지 않는다
Instance Profile + Role이 그 자리를 대신한다


5. Instance Metadata Service (IMDS)

EC2 안에서 자기 정보를 조회하는 API.

http://169.254.169.254/latest/meta-data/

여기서

  • 자기 인스턴스 ID
  • 자기 IAM Role의 임시 자격증명
  • 자기 보안 그룹 ID
  • 자기 사용자 데이터

등을 받을 수 있다.

IMDSv1 vs IMDSv2

IMDSv1은 보안 취약점이 있어 SSRF 공격으로 악용될 수 있었다.

신규 인스턴스는 무조건 IMDSv2 only

HttpTokens = required
HttpPutResponseHopLimit = 1

6. Bastion vs Session Manager

옛날 패턴 — 외부 노출된 작은 EC2 (Bastion) 를 거쳐 내부 EC2로 SSH.

운영자 → Bastion (퍼블릭) → 내부 EC2

문제:

  • Bastion 자체가 공격 표면
  • 키 관리 · 패치 운영 부담
  • 감사 로그 약함

Session Manager를 쓰면 Bastion 자체가 불필요해진다
정말 SSH가 필요한 환경에서만 Bastion 운영


7. 우리 서비스에서

이 책의 척추는 ECS Fargate라 사람이 컨테이너에 접속할 일이 거의 없다.

사람 → 운영용 일회성 컨테이너 (ECS Exec)
                            ↑ SSM 동일 메커니즘
  • ECS Exec — 컨테이너 안에 셸 접속하는 ECS 기능
  • 권한 모델은 SSM과 같다 (IAM)

EC2 직접 운영이 필요한 경우 (예: 일부 워커, GPU 노드):

[운영자] → Session Manager → EC2 (프라이빗 서브넷)
                  ↑
            VPC Endpoint (com.amazonaws.region.ssm)

8. 직접 확인해보기 — CLI

Session Manager 접속

aws ssm start-session --target i-1234567890abcdef0

브라우저 없이 터미널에서 바로 셸이 열린다.

EC2 안에서 자기 자격증명 보기 (IMDSv2)

TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

ECS Exec — 컨테이너 셸 접속

aws ecs execute-command \
  --cluster msa \
  --task <task-id> \
  --container app \
  --interactive \
  --command "/bin/sh"

9. 코드로는 이렇게 생겼다 — Terraform

# IAM Role (Session Manager + 애플리케이션 권한)
resource "aws_iam_role" "web" {
  name = "web-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.web.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "web" {
  name = "web-instance-profile"
  role = aws_iam_role.web.name
}

resource "aws_instance" "web" {
  ami                  = data.aws_ami.web.id
  instance_type        = "t3.small"
  subnet_id            = aws_subnet.private_a.id   # 프라이빗
  iam_instance_profile = aws_iam_instance_profile.web.name

  # IMDSv2 강제
  metadata_options {
    http_tokens                 = "required"
    http_put_response_hop_limit = 1
  }

  # SSH 안 열어도 됨 (Session Manager 사용)
  vpc_security_group_ids = [aws_security_group.web.id]
}

AmazonSSMManagedInstanceCore 한 줄이 Session Manager의 핵심.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 22번 포트를 0.0.0.0/0 에 연다

“잠깐 디버깅” 으로 연 포트가 영영 열려 있다.

Session Manager로 가서 22를 닫는다

안티패턴 2. .pem 키를 Slack/이메일로 공유

누가 가졌는지 추적 안 됨, 퇴사자 회수 어려움.

안티패턴 3. EC2 안에 AWS 액세스 키를 박는다

가장 흔한 보안 사고. 코드 저장소에 박히기까지 한다.

Instance Profile + Role 외 다른 길은 없다

안티패턴 4. IMDSv1을 그대로 두는 옛 인스턴스

신규는 IMDSv2 only. 옛 인스턴스도 점진 전환.


11. 한 줄로 정리

사람 접속은 Session Manager, 코드의 AWS 호출은 Instance Profile + Role.
22 포트와 키 페어는 점점 자리를 비운다.


12. 이 장의 핵심 정리

  1. 사람 → EC2 접속은 Session Manager가 표준이다.
  2. EC2 안 코드의 AWS 호출은 Instance Profile + IAM Role 로.
  3. EC2 안에 액세스 키 박지 않는다.
  4. IMDSv2 only가 신규 인스턴스의 기본값.
  5. Bastion은 거의 불필요 — Session Manager + 프라이빗 서브넷.
  6. ECS Exec은 같은 메커니즘으로 컨테이너 셸을 연다.

17장. EBS 스냅샷과 백업 전략

이 장에서 말하고자 하는 것

EC2의 디스크(EBS) 에는 다음이 들어 있다.

  • 운영체제
  • 애플리케이션
  • 로그
  • 임시 데이터

서버가 죽거나 사람이 실수로 파일을 지웠을 때
디스크 단위로 시점 복구가 가능해야 한다.

이걸 가능하게 하는 도구가

EBS 스냅샷 (Snapshot)

이다.

이 장은 스냅샷의 동작과 운영 전략을 정리한다.


1. 스냅샷이란

스냅샷은 EBS 볼륨의 시점 백업 이다.

EBS 볼륨 (100GB, 70GB 사용 중)
   ↓ 스냅샷 만들기
[Snapshot in S3]
  ├─ 사용 중인 블록만 저장 (70GB 분량)
  └─ KMS로 자동 암호화 (볼륨이 암호화돼 있으면)
  • S3 위에 저장 (사용자가 직접 볼 수는 없음)
  • 같은 볼륨의 다음 스냅샷은 변경된 블록만 저장 (증분)
  • 매우 효율적

2. 어떻게 동작하는가

첫 스냅샷:   사용 중인 모든 블록 백업 (70GB)
다음 스냅샷: 그 사이 변경된 블록만 (10GB)
다음 스냅샷: 또 변경된 블록만 (5GB)

각 스냅샷은 독립적인 시점 복구가 가능하다.
중간 스냅샷을 삭제해도 다른 스냅샷은 그대로 살아 있다.


3. 스냅샷에서 볼륨 복원

Snapshot → 새 EBS 볼륨 (다른 AZ도 가능)
  • 새 AZ에 볼륨을 만들 때 자주 쓴다 (EBS는 AZ 종속이라)
  • 새 EC2에 그 볼륨을 attach
  • 또는 그 스냅샷으로 새 AMI 등록

4. 무엇을 백업해야 하나

운영체제 디스크: AMI에 박혀 있으면 백업 덜 중요
데이터 디스크:   매우 중요 (사용자 업로드 · DB · 로그)
임시 디스크:     백업 안 함

운영체제와 데이터 디스크를 분리해 두면 백업이 단순해진다.

디스크 1: OS (변하지 않음, AMI로 복구 가능)
디스크 2: 데이터 (자주 스냅샷)

5. Application-Consistent vs Crash-Consistent

스냅샷은 디스크 단위 백업이라 애플리케이션 일관성이 보장되지 않는다.

DB가 쓰는 중에 스냅샷 시작
  ↓
스냅샷에는 "쓰다 만" 상태가 박힘
  ↓
복원 시 손상 가능

해결:

  • DB는 RDS/Aurora 의 백업 사용 (앱 일관성 보장)
  • 직접 운영하는 DB는 스냅샷 전에 flush/lock
  • 파일 시스템은 sync + fsfreeze 같은 절차

EC2 위에 직접 DB를 띄우지 않는 게 가장 단순한 해답


6. AWS Backup — 자동화

스냅샷을 사람이 손으로 만들지 않는다.

AWS Backup 으로 정책 기반 자동화.

Backup Plan
  ├─ 일정: 매일 새벽 3시
  ├─ 대상: 태그 "Backup=daily" 가 붙은 모든 EBS
  ├─ 보관: 30일
  └─ 교차 리전 복사 (선택)
  • 사람의 손이 닿지 않는다
  • 보관 정책 일관됨
  • Vault Lock으로 악의적 삭제 방지 가능

운영 EBS는 거의 항상 AWS Backup 으로


7. 빠른 복원 — EBS Fast Snapshot Restore

스냅샷에서 큰 볼륨을 복원하면 처음 접근하는 블록이 느리다.

긴급 복구 시나리오에서는 FSR (Fast Snapshot Restore) 를 켜 둔다.

FSR 활성화된 스냅샷 → 복원 즉시 풀 성능
  • 비용이 든다
  • DR · 자주 복원하는 시나리오에서만

8. 우리 서비스에서

이 책의 척추는 ECS Fargate라 EBS 직접 운영은 적다.

주요 데이터:
  RDS · DynamoDB → AWS Backup 으로 별도 다룸 (71장)
  S3 → 버전 관리 + 수명 주기

EC2가 있다면 (예: ML 노드):
  운영체제 → AMI
  데이터 디스크 → AWS Backup daily, retention 30일

데이터를 거의 외부 저장소에 두므로 EBS 백업 부담이 적다.


9. 직접 확인해보기 — CLI

스냅샷 만들기

aws ec2 create-snapshot \
  --volume-id vol-xxxx \
  --description "manual before upgrade" \
  --tag-specifications 'ResourceType=snapshot,Tags=[{Key=Name,Value=pre-upgrade}]'

스냅샷 목록

aws ec2 describe-snapshots --owner-ids self

스냅샷에서 새 볼륨 만들기

aws ec2 create-volume \
  --availability-zone ap-northeast-2c \
  --snapshot-id snap-xxxx \
  --volume-type gp3

AWS Backup으로 점검

aws backup list-backup-plans
aws backup list-backup-jobs --by-state COMPLETED

10. 코드로는 이렇게 생겼다 — Terraform (AWS Backup)

resource "aws_backup_vault" "main" {
  name        = "main-vault"
  kms_key_arn = aws_kms_key.backup.arn
}

resource "aws_backup_plan" "daily_ebs" {
  name = "daily-ebs"

  rule {
    rule_name         = "daily-3am"
    target_vault_name = aws_backup_vault.main.name
    schedule          = "cron(0 18 * * ? *)"  # UTC 18 = KST 03

    lifecycle {
      delete_after = 30
    }

    copy_action {
      destination_vault_arn = aws_backup_vault.dr.arn  # DR 리전
      lifecycle { delete_after = 90 }
    }
  }
}

# 태그로 백업 대상 자동 선택
resource "aws_backup_selection" "tagged" {
  name         = "tagged-ebs"
  plan_id      = aws_backup_plan.daily_ebs.id
  iam_role_arn = aws_iam_role.backup.arn

  selection_tag {
    type  = "STRINGEQUALS"
    key   = "Backup"
    value = "daily"
  }
}

EBS 볼륨에 Backup=daily 태그만 붙이면 자동 포함.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 스냅샷을 수동으로 가끔 만든다

사람이 잊는다. 매일이 아닌 백업은 효과가 약하다.

안티패턴 2. EC2 위에 DB를 운영하면서 스냅샷만 믿는다

앱 일관성이 깨질 수 있다.

DB는 RDS/Aurora 또는 명시적 quiesce 절차

안티패턴 3. 스냅샷을 같은 리전 · 같은 계정에만 둔다

리전 장애 · 계정 침해 시 모두 사라진다.

Cross-Region 복사 + Vault Lock

안티패턴 4. 복원을 한 번도 안 해본다

“백업은 됐는데 복원이 안 되더라” 가 가장 흔한 사고.

분기마다 실제 복원 시뮬레이션


12. 한 줄로 정리

EBS 스냅샷은 디스크의 시점 백업이며,
AWS Backup + 교차 리전 + 복원 시뮬레이션이 운영의 토대다


13. 이 장의 핵심 정리

  1. EBS 스냅샷은 사용 중인 블록만 저장하는 증분 백업이다.
  2. OS 디스크와 데이터 디스크를 분리하면 백업이 단순해진다.
  3. DB는 EC2 EBS가 아니라 관리형 DB (RDS · Aurora) 가 답이다.
  4. AWS Backup으로 정책 기반 자동화한다.
  5. Cross-Region 복사와 Vault Lock으로 폭발 반경을 좁힌다.
  6. 복원은 분기마다 시뮬레이션 — 안 해본 백업은 백업이 아니다.

18장. EC2 요금 모델 — On-Demand · Spot · RI · Savings Plans

이 장에서 말하고자 하는 것

EC2를 띄울 때 똑같이 t3.small을 띄워도
어떤 요금 모델로 구매하느냐 에 따라 비용이 크게 달라진다.

네 가지 주요 모델:

  • On-Demand
  • Spot
  • Reserved Instance (RI)
  • Savings Plans

같은 사양에 최대 90% 까지 비용 차이가 나기도 한다.

이 장은 각자의 자리와 운영적 선택을 정리한다.


1. On-Demand — 기본값

시간당 (또는 초당) 사용한 만큼 청구
약정 없음 / 자유로움
가장 비쌈 (기준 가격)
  • 시작 / 실험에 가장 자연스러움
  • 트래픽 예측 어려운 초기 단계
  • 단기적이거나 들쭉날쭉한 워크로드

운영 초기는 거의 항상 On-Demand로 시작


2. Spot — 가장 싸지만 잠깐 끊김

AWS의 남는 용량을 경매처럼 빌려 쓴다.

On-Demand 대비 약 70~90% 저렴
AWS가 필요할 때 회수 (2분 전 알림)

Spot이 어울리는 경우

  • 상태 비저장 워커
  • 배치 / ML 학습
  • CI 빌드 노드
  • Auto Scaling 그룹의 일부

Spot이 어울리지 않는 경우

  • 데이터베이스
  • 상태 보존 중요한 단독 인스턴스
  • 회수가 비즈니스에 즉시 영향 주는 워크로드

회수돼도 자동 복구되는 워크로드만 Spot


3. Reserved Instance (RI) — 약정 할인

1년 또는 3년 약정 → 약 30~70% 할인
선납 / 부분 선납 / 미선납 옵션
인스턴스 타입 · 리전 · OS 지정 필요

특징:

  • 약정 기간 동안 그만큼의 컴퓨트를 늘 쓰는 게 전제
  • 인스턴스 타입이 바뀌면 옛 RI가 무용지물이 되기도 함
  • “어떤 사양이 얼마나 필요한가” 가 명확할 때 유리

RI는 점점 자리를 Savings Plans 에 내주고 있다


4. Savings Plans — 더 유연한 약정

RI의 진화형. 같은 1/3년 약정이지만 더 유연하다.

Compute Savings Plans

  • “시간당 $X 만큼은 무조건 쓴다” 약정
  • EC2 · Fargate · Lambda 모두 적용
  • 인스턴스 패밀리 · 리전 자유롭게 바꿔도 할인 유지
  • 가장 유연

EC2 Instance Savings Plans

  • 특정 인스턴스 패밀리에 더 큰 할인
  • 유연성은 RI 수준

새 약정은 거의 항상 Compute Savings Plans 가 정답


5. 네 모델을 한 표로

모델약정할인폭회수 위험유연성
On-Demand없음기준없음매우 높음
Spot없음70~90%있음 (2분 알림)높음
RI1/3년30~70%없음낮음
Compute Savings Plans1/3년30~70%없음높음

6. 어떻게 조합할까 — 운영 전략

대부분의 운영은 혼합 전략 을 쓴다.

기본 안정 운영 (24시간 늘 떠 있어야 하는 부분)
  → Compute Savings Plans

피크 시간 추가 / 스케일 아웃
  → On-Demand

상태 비저장 워커 / 배치
  → Spot

특정 인스턴스 패밀리 큰 사용
  → EC2 Instance Savings Plans

Fargate에도 똑같이 적용

Fargate         : Compute Savings Plans 할인 가능
Fargate Spot    : 70% 수준 할인 (Spot 동일 회수 가능)

우리 척추(ECS Fargate)에서는 Fargate + Fargate Spot 혼합이 표준


7. 데이터 전송 비용 — 의외의 폭탄

EC2 컴퓨트만 보다가 청구서가 늘어나는 가장 큰 이유는 보통 데이터 송신 이다.

인터넷으로 나가는 트래픽: GB당 과금
NAT Gateway 통과:        시간 + GB당
Cross-AZ:                작지만 누적
Cross-Region:            크다

대응:

  • CloudFront로 흡수 (CloudFront → 사용자는 더 쌈)
  • VPC Endpoint로 NAT 우회 (88장)
  • 같은 AZ로 묶기 (가능한 곳만 — 가용성과 균형)

8. 비용 가시화 — Cost Explorer · CUR

운영의 시작은 “보는 것” 이다.

Cost Explorer     : 그래프로 비용 추세
Cost and Usage Report (CUR) : 자세한 데이터를 S3로 → Athena
Budgets            : 예산 임계 알람

매주 한 번 Cost Explorer 보는 습관


9. 우리 서비스에서

ECS Fargate (web · api · workers)
  → Compute Savings Plans 70% 약정
  → 워커 일부는 Fargate Spot

RDS · ElastiCache
  → RI 또는 Aurora I/O Optimized 옵션

S3 / DynamoDB / Lambda
  → Compute Savings Plans 가 일부 적용 (Lambda)
  → 자체 수명 주기 · TTL · On-Demand 로 시작

비용 가드:
  Budgets 알람 (월 임계 80% · 100%)
  Cost Anomaly Detection
  태그로 서비스별 비용 추적

10. 직접 확인해보기 — CLI · 콘솔

Cost Explorer로 한 달 비용

콘솔: Billing & Cost Management → Cost Explorer

예산 알람 만들기

aws budgets create-budget \
  --account-id <acct> \
  --budget '{
    "BudgetName":"monthly-200",
    "BudgetLimit":{"Amount":"200","Unit":"USD"},
    "TimeUnit":"MONTHLY",
    "BudgetType":"COST"
  }' \
  --notifications-with-subscribers ...

Savings Plans 추천 보기

aws ce get-savings-plans-purchase-recommendation \
  --savings-plans-type COMPUTE_SP \
  --term-in-years ONE_YEAR \
  --payment-option NO_UPFRONT

추천 결과는 참고용 — 약정 전에는 사용 패턴을 본인이 한 번 더 확인한다


11. 코드로는 이렇게 생겼다 — Terraform (Budget · Anomaly)

resource "aws_budgets_budget" "monthly" {
  name              = "monthly-prod"
  budget_type       = "COST"
  limit_amount      = "500"
  limit_unit        = "USD"
  time_unit         = "MONTHLY"

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = ["ops@example.com"]
  }
}

resource "aws_ce_anomaly_monitor" "service" {
  name              = "service-anomaly"
  monitor_type      = "DIMENSIONAL"
  monitor_dimension = "SERVICE"
}

resource "aws_ce_anomaly_subscription" "alerts" {
  name             = "anomaly-alerts"
  monitor_arn_list = [aws_ce_anomaly_monitor.service.arn]
  frequency        = "DAILY"

  subscriber {
    type    = "EMAIL"
    address = "ops@example.com"
  }
}

예산 + 이상 감지 두 가지가 비용 통제의 출발선.


12. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 처음부터 큰 RI 약정

사용 패턴이 안 잡힌 상태에서 3년 약정 → 안 쓰는 RI 잔뜩.

On-Demand로 3~6개월 돌리고 데이터로 약정

안티패턴 2. 상태 있는 서비스에 Spot

회수 시 데이터 손실 또는 서비스 중단.

안티패턴 3. NAT Gateway 비용을 못 본다

조용히 매달 늘어난다.

S3 · ECR 등은 VPC Endpoint로 우회

안티패턴 4. 태그 없이 운영

어느 서비스가 얼마를 쓰는지 모름 → 최적화 불가능.

Environment · Service · Team 태그는 사실상 필수


13. 한 줄로 정리

같은 사양도 어떻게 사느냐에 따라 비용이 90%까지 달라진다.
안정 부하는 Savings Plans, 상태 비저장은 Spot, 변동은 On-Demand — 혼합이 정답이다.


14. 이 장의 핵심 정리

  1. On-Demand · Spot · RI · Savings Plans 네 모델이 있다.
  2. 새 약정은 Compute Savings Plans 가 거의 항상 정답이다.
  3. Spot은 상태 비저장 워크로드에만 — Fargate Spot도 마찬가지.
  4. 데이터 전송 · NAT Gateway 가 의외의 비용 폭탄.
  5. Budgets · Cost Anomaly Detection · 태그로 가시화.
  6. 약정은 데이터로 — 사용 패턴 잡힌 후에 결정한다.

19장. 서버는 어디에 연결되는가

이 장에서 말하고자 하는 것

앞 장에서 우리는 EC2 인스턴스를 생성했다.

이제 서버는 준비되었다.

그렇다면 다음 질문이 생긴다.

이 서버는 인터넷 어디에 존재하는 걸까?

서버는 단순히
“클라우드 어딘가에 떠 있는 컴퓨터” 가 아니다.

모든 서버는 반드시
어떤 네트워크 안에 연결되어 있어야 한다.

이 장에서는

  • 서버가 네트워크 안에서 어떻게 배치되는지
  • AWS에서 이 네트워크가 어떻게 구성되는지

를 이해한다.


1. 서버는 반드시 네트워크 안에 있어야 한다

서버는 혼자 존재할 수 없다.

왜냐하면 서버의 목적은
다른 시스템과 통신하는 것이기 때문이다.

예를 들어 웹 서비스는 보통 다음과 같이 동작한다.

flowchart LR
User[사용자] --> Server[웹 서버]
Server --> DB[데이터베이스]
  1. 사용자가 서버에 요청을 보낸다
  2. 서버는 데이터베이스에 데이터를 요청한다
  3. 결과를 사용자에게 전달한다

이 모든 과정은
네트워크 연결이 있어야만 가능하다.

즉,

모든 서버는 반드시 어떤 네트워크 안에 존재한다.


2. 온프레미스에서는 어떻게 네트워크를 만들까

먼저 우리가 익숙한 온프레미스 환경을 생각해보자.

보통 회사 내부 네트워크는 다음과 같은 구조를 가진다.

flowchart TB
Internet[인터넷]
Router[라우터 / 방화벽]
Switch[스위치]
Server1[웹 서버]
Server2[애플리케이션 서버]
Server3[DB 서버]

Internet --> Router
Router --> Switch
Switch --> Server1
Switch --> Server2
Switch --> Server3

이 구조의 특징은 다음과 같다.

  • 회사 내부에 하나의 네트워크 공간이 존재한다
  • 서버들은 이 네트워크 안에 배치된다
  • 일부 서버만 외부 인터넷과 연결된다

즉 서버는 항상

특정 네트워크 공간 안에 배치된다.


3. 클라우드에서도 같은 문제가 발생한다

이제 클라우드 환경을 생각해보자.

AWS에는 수많은 회사들이 서버를 운영하고 있다.

만약 AWS가 모든 서버를
같은 네트워크에 연결한다면 어떤 일이 발생할까?

  • 다른 회사 서버와 충돌
  • IP 주소 충돌
  • 보안 문제

이 문제를 해결하기 위해 AWS는

고객마다 독립된 네트워크 공간을 제공한다.

이 네트워크가 바로 VPC다.


4. VPC란 무엇인가

VPC는 Virtual Private Cloud의 약자다.

간단히 말하면

클라우드 안에서 고객이 사용하는
독립된 네트워크 공간

이다.

개념적으로 보면 다음과 같다.

flowchart TB
Internet[인터넷]

subgraph VPC
Server1[EC2 서버]
Server2[EC2 서버]
Server3[EC2 서버]
end

Internet --> VPC

VPC 안에는 여러 서버를 배치할 수 있고
이 서버들은 서로 통신할 수 있다.

그리고 중요한 점이 하나 있다.

EC2 인스턴스는 반드시 하나의 VPC 안에서 생성된다.

즉 AWS에서 서버를 만들면
항상 어떤 네트워크 공간(VPC) 안에 위치하게 된다.


5. VPC 안의 서버는 IP 주소를 가진다

네트워크에서는
각 장비를 구별하기 위해 IP 주소를 사용한다.

예를 들어 다음과 같은 주소가 있다.

10.0.1.10
10.0.2.15
10.0.3.20

VPC 안에서 생성된 서버는
이처럼 하나의 IP 주소를 할당받는다.

이 IP 주소를 이용해
서버들은 서로 통신한다.

예를 들어

flowchart LR
Web[웹 서버]
App[애플리케이션 서버]
DB[DB 서버]

Web --> App
App --> DB
  • 웹 서버 → 애플리케이션 서버
  • 애플리케이션 서버 → 데이터베이스

이 모든 통신은
IP 주소를 기반으로 이루어진다.


6. 모든 서버가 인터넷에 노출될 필요는 없다

대부분의 서비스는
모든 서버가 인터넷에 연결될 필요가 없다.

예를 들어 다음 구조를 생각해보자.

flowchart TB
Internet[인터넷]

subgraph VPC
Web[웹 서버]
App[애플리케이션 서버]
DB[DB 서버]
end

Internet --> Web
Web --> App
App --> DB

이 구조에서

  • 웹 서버 → 외부에서 접근 가능
  • 애플리케이션 서버 → 내부 통신만 수행
  • DB 서버 → 외부 접근 차단

이 방식은 대부분의 서비스에서 사용하는 구조다.

이렇게 하면

  • 보안 강화
  • 네트워크 분리
  • 내부 시스템 보호

가 가능하다.


7. VPC는 사용할 IP 주소 범위를 가진다

VPC는 하나의 네트워크 공간이기 때문에
사용할 수 있는 IP 주소 범위가 필요하다.

그래서 VPC를 생성할 때
다음과 같은 범위를 지정한다.

10.0.0.0/16

이 범위 안에서
각 서버에 IP 주소가 할당된다.

이 표기법을 CIDR이라고 부른다.

CIDR은 간단히 말하면

네트워크에서 사용할 IP 주소 범위를 표현하는 방법

이다.

이 개념은 다음 장에서
조금 더 자세히 살펴본다.


8. 이 장의 핵심 정리

  1. 모든 서버는 네트워크 안에서 동작한다.
  2. 온프레미스에서는 회사 내부 네트워크를 사용한다.
  3. AWS에서는 이 네트워크 역할을 VPC가 담당한다.
  4. EC2는 반드시 VPC 안에서 생성된다.
  5. VPC 안의 서버는 IP 주소를 가진다.
  6. 일부 서버만 인터넷과 연결된다.
  7. VPC는 사용할 IP 주소 범위(CIDR) 를 가진다.

20장. IP 주소 범위와 CIDR 이해하기

이 장에서 말하고자 하는 것

앞 장에서 우리는 VPC를 만들 때
다음과 같은 값을 지정하는 것을 보았다.

10.0.0.0/16

이 숫자는 단순한 설정값이 아니다.

이 값은

이 네트워크에서 어떤 IP 주소들을 사용할 수 있는지

를 정의한다.

이 장에서는

  • 네트워크에서 IP 주소 범위가 왜 필요한지
  • CIDR 표기법이 무엇인지

를 이해한다.


1. 네트워크에서는 IP 주소가 필요하다

네트워크에 연결된 모든 장비는
자신을 식별할 수 있는 주소를 가져야 한다.

이 주소가 바로 IP 주소다.

예를 들어 다음과 같다.

10.0.1.10
10.0.1.20
10.0.2.15

이 IP 주소 덕분에

  • 어떤 서버에 요청을 보내는지
  • 어떤 서버가 응답을 보내는지

를 정확하게 구분할 수 있다.


2. 하지만 IP를 무작정 사용할 수는 없다

IP 주소는 마음대로 사용할 수 있는 것이 아니다.

네트워크에는

사용할 수 있는 IP 주소 범위

가 존재한다.

예를 들어 회사 내부 네트워크가 다음 범위를 사용한다고 가정해보자.

10.0.0.0 ~ 10.0.255.255

이 범위 안에서만
서버나 장비에 IP 주소를 할당할 수 있다.

즉 네트워크는 항상

IP 주소 범위를 기준으로 구성된다


3. AWS에서도 IP 주소 범위를 먼저 정한다

AWS에서도 동일하다.

VPC를 만들 때
다음과 같은 값을 설정한다.

10.0.0.0/16

이 값은

이 VPC가 사용할 IP 주소 범위

를 의미한다.

이 범위 안에서

  • EC2
  • 데이터베이스
  • 로드밸런서

같은 리소스들이
각자의 IP 주소를 할당받는다.


4. CIDR 표기법이란 무엇인가

여기서 등장하는 것이 바로 CIDR 표기법이다.

CIDR은

네트워크에서 사용할 IP 주소 범위를 표현하는 방식

이다.

예를 들어 다음과 같은 표기가 있다.

10.0.0.0/16

이 표기는 두 부분으로 구성된다.

10.0.0.0 → 네트워크 시작 주소
/16      → 네트워크 크기

10.0.0.0/16

은 다음 범위를 의미한다.

10.0.0.0 ~ 10.0.255.255

이 범위 안에서 IP 주소를 사용할 수 있다.


5. CIDR 뒤의 숫자는 무엇을 의미할까

CIDR 뒤의 숫자는
네트워크 크기를 의미한다.

예를 들어 다음과 같은 CIDR이 있다.

10.0.0.0/16
10.0.0.0/24

이 둘은 네트워크 크기가 다르다.

CIDR사용할 수 있는 IP 범위
10.0.0.0/1610.0.0.0 ~ 10.0.255.255
10.0.0.0/2410.0.0.0 ~ 10.0.0.255

/16 → 매우 큰 네트워크
/24 → 더 작은 네트워크

이렇게 이해할 수 있다.


6. 왜 네트워크를 여러 개로 나눌까

하나의 큰 네트워크만 사용해도
서버를 운영할 수 있다.

하지만 실제 서비스에서는
네트워크를 여러 개로 나누는 경우가 많다.

예를 들어 다음과 같은 구조다.

10.0.1.x → 웹 서버
10.0.2.x → 애플리케이션 서버
10.0.3.x → 데이터베이스

이렇게 나누면

  • 보안 관리가 쉬워지고
  • 네트워크 구조가 명확해지고
  • 서비스 구조를 분리할 수 있다.

이처럼 큰 네트워크를
작은 네트워크로 나누는 것을

서브넷 (Subnet)

이라고 한다.


8. 이 장의 핵심 정리

  1. 네트워크에서는 모든 장비가 IP 주소를 가진다.
  2. 네트워크는 항상 IP 주소 범위를 기준으로 구성된다.
  3. AWS에서도 VPC를 만들 때 IP 범위를 지정한다.
  4. 이 범위를 표현하는 방식이 CIDR이다.
  5. CIDR 뒤의 숫자는 네트워크 크기를 의미한다.
  6. 큰 네트워크는 여러 개의 서브넷으로 나눌 수 있다.

21장. 서브넷 (Subnet)

이 장에서 말하고자 하는 것

앞 장에서 우리는 VPC를 만들 때
다음과 같은 IP 주소 범위를 지정하는 것을 보았다.

10.0.0.0/16

이 범위는 하나의 큰 네트워크 공간이다.

하지만 실제 서비스에서는
이 네트워크를 그대로 사용하는 경우는 거의 없다.

보통 이 큰 네트워크를
여러 개의 작은 네트워크로 나누어 사용한다.

이 장에서는

네트워크를 왜 나누는지
그리고 그 구조가 무엇인지

를 이해한다.


1. 왜 네트워크를 나눌까

웹 서비스는 보통 여러 종류의 서버로 구성된다.

flowchart LR
    User[사용자] --> Web[웹 서버]
    Web --> App[애플리케이션 서버]
    App --> DB[데이터베이스]

예를 들어 다음과 같은 서버들이 있다.

  • 웹 서버
  • 애플리케이션 서버
  • 데이터베이스 서버

만약 이 모든 서버가
같은 네트워크에 존재한다면

10.0.0.0/16

이 네트워크 안에 모든 서버가 섞이게 된다.

이렇게 되면

  • 서버 역할을 구분하기 어렵고
  • 네트워크 정책을 나누기 어렵고
  • 관리가 복잡해진다.

그래서 보통 서버 역할에 따라
네트워크를 여러 개로 나누어 사용한다.


2. 이렇게 나눈 네트워크를 서브넷이라고 한다

앞에서 살펴본 것처럼
하나의 네트워크를 여러 개로 나누면
구조를 훨씬 쉽게 관리할 수 있다.

이처럼

하나의 큰 네트워크를
여러 개의 작은 네트워크로 나누는 것을

서브넷 (Subnet) 이라고 한다.

여기서 중요한 점은 다음이다.

서브넷은 서버 한 대를 위한 공간이 아니다
여러 서버를 배치하기 위한 네트워크 공간이다

예를 들어

  • 웹 서버들이 모여 있는 네트워크
  • 애플리케이션 서버들이 모여 있는 네트워크
  • 데이터베이스 서버들이 있는 네트워크

이 각각이 하나의 서브넷이 될 수 있다.


3. 서브넷 구조 예시

예를 들어 다음과 같은 VPC가 있다고 하자.

10.0.0.0/16

이 네트워크를 다음처럼 나눌 수 있다.

10.0.1.0/24
10.0.2.0/24
10.0.3.0/24

각 네트워크는 서로 다른 서버 그룹을 담을 수 있다.

flowchart LR
    subgraph Region["Region"]
        subgraph VPC["VPC  10.0.0.0/16"]
            subgraph WebSubnet["Web Subnet  10.0.1.0/24"]
                Web1["웹서버"]
                Web2["웹서버"]
            end
            subgraph AppSubnet["App Subnet  10.0.2.0/24"]
                App1["애플리케이션 서버"]
                App2["애플리케이션 서버"]
            end
            subgraph DBSubnet["DB Subnet  10.0.3.0/24"]
                DB1[("DB 서버")]
                DB2[("DB 서버")]
            end
        end
    end

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px
    style WebSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:4
    style AppSubnet fill:#e3f2fd,stroke:#64b5f6,stroke-width:1px,stroke-dasharray:4
    style DBSubnet fill:#fce4ec,stroke:#e91e63,stroke-width:1px,stroke-dasharray:4
    style Web1 fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style Web2 fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style App1 fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
    style App2 fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
    style DB1 fill:#fce4ec,stroke:#c62828,stroke-width:1px
    style DB2 fill:#fce4ec,stroke:#c62828,stroke-width:1px

이 구조에서 중요한 점은 다음이다.

  • 서브넷 안에는 여러 서버가 존재할 수 있다
  • 서버 역할에 따라 네트워크를 나눌 수 있다

4. AWS에서 서브넷과 AZ의 관계

AWS에서는 서브넷을 생성할 때
어느 AZ에 속할지 함께 선택한다.

서브넷 생성
→ AZ 선택

이 함께 이루어진다.

예를 들어 다음과 같은 구조가 가능하다.

flowchart LR
    subgraph AWSRegion["AWS Region"]

        subgraph AZa["Availability Zone A"]
            subgraph WebSubnet["Web Subnet"]
                Web1["웹 서버"]
                Web2["웹 서버"]
            end
        end

        subgraph AZb["Availability Zone B"]
            subgraph AppSubnet["App Subnet"]
                App1["애플리케이션 서버"]
                App2["애플리케이션 서버"]
            end
        end

        subgraph AZc["Availability Zone C"]
            subgraph DBSubnet["DB Subnet"]
                DB1[("DB 서버")]
            end
        end

    end

    style AWSRegion fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style AZa fill:#e6f4ea,stroke:#2e7d32,stroke-width:1px
    style AZb fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
    style AZc fill:#fce4ec,stroke:#c62828,stroke-width:1px
    style WebSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:4
    style AppSubnet fill:#e8f0fd,stroke:#64b5f6,stroke-width:1px,stroke-dasharray:4
    style DBSubnet fill:#fdecea,stroke:#e91e63,stroke-width:1px,stroke-dasharray:4
    style Web1 fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style Web2 fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style App1 fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
    style App2 fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
    style DB1 fill:#fce4ec,stroke:#c62828,stroke-width:1px

이처럼 서로 다른 AZ에
서브넷을 배치하면

  • 장애 대응
  • 서비스 안정성 향상

과 같은 효과를 얻을 수 있다.


5. 실무에서 많이 사용하는 서브넷 구조

실제 AWS 환경에서는
다음과 같은 구조가 많이 사용된다.

10.0.0.0/16  ← VPC

이 네트워크를 다음과 같이 나눈다.

10.0.1.0/24  ← Web Subnet
10.0.2.0/24  ← App Subnet
10.0.3.0/24  ← DB Subnet

즉 대부분의 환경에서는

VPC는 크게 만들고 (/16)
서브넷을 /24 단위로 나누어 사용한다.

이렇게 하면

  • 네트워크 확장성이 좋아지고
  • 구조를 이해하기 쉬워지고
  • 관리가 쉬워진다.

6. 이 장의 핵심 정리

  1. VPC는 하나의 큰 네트워크 공간이다.
  2. 큰 네트워크는 여러 개의 작은 네트워크로 나눌 수 있다.
  3. 이 작은 네트워크를 서브넷(Subnet) 이라고 한다.
  4. 서브넷 안에는 여러 서버가 배치될 수 있다.
  5. AWS에서는 서브넷 생성 시 AZ를 선택한다.
  6. 보통 /16 VPC → /24 서브넷 구조를 많이 사용한다.

22장. 퍼블릭 서브넷과 프라이빗 서브넷

이 장에서 말하고자 하는 것

앞 장에서 우리는
VPC 안의 네트워크를 서브넷으로 나누는 방법을 살펴보았다.

예를 들어 다음과 같은 구조였다.

10.0.0.0/16  ← VPC
10.0.1.0/24
10.0.2.0/24
10.0.3.0/24

이렇게 네트워크를 나누면
서버 역할에 따라 구조를 분리할 수 있다.

하지만 여기서 한 가지 질문이 생긴다.

모든 서브넷이 인터넷과 연결되어야 할까?

실제 서비스에서는
모든 서버가 인터넷에 노출될 필요는 없다.

그래서 AWS에서는 서브넷을 보통 두 가지로 나눈다.

  • 퍼블릭 서브넷 (Public Subnet)
  • 프라이빗 서브넷 (Private Subnet)

1. 모든 서버가 인터넷에 연결될 필요는 없다

웹 서비스를 다시 생각해보자.

flowchart LR
    User[사용자] --> Web[웹 서버]
    Web --> App[애플리케이션 서버]
    App --> DB[데이터베이스]

이 구조에서

  • 사용자는 웹 서버에만 접근한다
  • 애플리케이션 서버는 웹 서버와만 통신한다
  • 데이터베이스는 외부에서 접근할 필요가 없다

인터넷 ↔ 웹 서버
웹 서버 ↔ 애플리케이션 서버
애플리케이션 서버 ↔ 데이터베이스

이런 구조가 된다.

그래서 서버의 역할에 따라
인터넷 접근 여부를 나누게 된다.


2. 퍼블릭 서브넷 (Public Subnet)

퍼블릭 서브넷은
인터넷과 직접 연결된 서브넷이다.

즉 외부 사용자가 접근할 수 있는 서버가 위치한다.

예를 들어

  • 웹 서버
  • 로드밸런서
  • Bastion 서버

같은 서버들이 여기에 위치한다.

예시 구조는 다음과 같다.

flowchart LR
    Internet["Internet"]

    subgraph Region["Region"]
        subgraph VPC["VPC"]
            subgraph PublicSubnet["Public Subnet"]
                Web1["웹 서버"]
                Web2["웹 서버"]
            end
        end
    end

    Internet --> Web1
    Internet --> Web2

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px
    style PublicSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:4
    style Internet fill:#fff,stroke:#888,stroke-width:1px
    style Web1 fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style Web2 fill:#fff8e1,stroke:#f9a825,stroke-width:1px

퍼블릭 서브넷의 특징은 다음과 같다.

  • 인터넷에서 접근 가능
  • 외부 트래픽이 들어오는 서버 위치
  • 보통 웹 계층이 위치

3. 프라이빗 서브넷 (Private Subnet)

프라이빗 서브넷은
인터넷에서 직접 접근할 수 없는 서브넷이다.

외부에서는 접근할 수 없고
VPC 내부 서버만 접근할 수 있다.

예를 들어 다음 서버들이 위치한다.

  • 애플리케이션 서버
  • 데이터베이스
  • 내부 서비스 서버

예시 구조는 다음과 같다.

flowchart LR
    Internet["Internet"]

    subgraph Region["Region"]
        subgraph VPC["VPC"]
            subgraph PublicSubnet["Public Subnet"]
                Web["웹 서버"]
            end

            subgraph PrivateSubnet["Private Subnet"]
                App["애플리케이션 서버"]
                DB["데이터베이스"]
            end
        end
    end

    Internet -->|"인바운드 트래픽"| Web
    Web -->|"내부 호출"| App
    App -->|"쿼리"| DB

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px
    style PublicSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:4
    style PrivateSubnet fill:#e3f2fd,stroke:#64b5f6,stroke-width:1px,stroke-dasharray:4
    style Internet fill:#fff,stroke:#888,stroke-width:1px
    style Web fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style App fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
    style DB fill:#fce4ec,stroke:#c62828,stroke-width:1px

이 구조에서는

  • 사용자는 웹 서버에만 접근
  • 내부 서버는 외부에서 직접 접근 불가

이렇게 되어
보안이 훨씬 강해진다.


4. 왜 퍼블릭과 프라이빗을 나눌까

이렇게 네트워크를 나누면
다음과 같은 장점이 있다.

보안 강화

데이터베이스 같은 서버는
외부에서 직접 접근할 필요가 없다.

그래서 프라이빗 서브넷에 배치한다.

네트워크 구조 명확화

서버 역할이 명확하게 나뉜다.

Public Subnet  → 웹 서버
Private Subnet → 애플리케이션 서버
Private Subnet → 데이터베이스

공격 범위 축소

외부에서 접근 가능한 서버가
최소화된다.


5. 실제 서비스에서의 구조

실제 AWS 환경에서는
다음과 같은 구조가 매우 일반적이다.

flowchart LR
    Internet["Internet"]

    subgraph Region["Region"]
        subgraph VPC["VPC  10.0.0.0/16"]

            subgraph PublicSubnet["Public Subnet"]
                Web1["웹 서버"]
                Web2["웹 서버"]
            end

            subgraph PrivateSubnet["Private Subnet"]
                App1["애플리케이션 서버"]
                App2["애플리케이션 서버"]
                DB[("데이터베이스")]
            end

        end
    end

    Internet -->|"인바운드 트래픽"| Web1
    Internet -->|"인바운드 트래픽"| Web2
    Web1 -->|"내부 호출"| App1
    Web2 -->|"내부 호출"| App2
    App1 -->|"쿼리"| DB
    App2 -->|"쿼리"| DB

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px
    style PublicSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:4
    style PrivateSubnet fill:#e3f2fd,stroke:#64b5f6,stroke-width:1px,stroke-dasharray:4
    style Internet fill:#fff,stroke:#888,stroke-width:1px
    style Web1 fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style Web2 fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style App1 fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
    style App2 fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
    style DB fill:#fce4ec,stroke:#c62828,stroke-width:1px

이 구조는 대부분의 웹 서비스에서
기본적으로 사용되는 아키텍처다.


6. 이 장의 핵심 정리

  1. 모든 서브넷이 인터넷과 연결될 필요는 없다.
  2. 인터넷과 연결된 서브넷을 퍼블릭 서브넷이라고 한다.
  3. 인터넷에서 직접 접근할 수 없는 서브넷을 프라이빗 서브넷이라고 한다.
  4. 보통 웹 서버는 퍼블릭 서브넷에 배치한다.
  5. 애플리케이션 서버와 데이터베이스는 프라이빗 서브넷에 배치한다.

23장. 인터넷 게이트웨이 (Internet Gateway)

이 장에서 말하고자 하는 것

앞 장에서 우리는 서브넷을 다음과 같이 나누는 구조를 살펴보았다.

  • 퍼블릭 서브넷 (Public Subnet)
  • 프라이빗 서브넷 (Private Subnet)

퍼블릭 서브넷에는
외부 사용자가 접근해야 하는 서버가 위치한다.

예를 들어

  • 웹 서버
  • 로드밸런서

같은 서버들이다.

그렇다면 여기서 한 가지 질문이 생긴다.

퍼블릭 서브넷에 있는 서버는
어떻게 인터넷과 연결될까?

이 연결을 담당하는 것이

인터넷 게이트웨이 (Internet Gateway)

이다.


1. VPC는 기본적으로 인터넷과 연결되어 있지 않다

AWS에서 VPC를 생성하면
그 네트워크는 기본적으로 인터넷과 연결되어 있지 않다.

즉 다음과 같은 상태다.

flowchart LR
    Internet["Internet"]

    subgraph Region["Region"]
        subgraph VPC["VPC"]
            Server["EC2 서버"]
        end
    end

    Internet x--x|"연결 없음"| VPC

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px
    style Internet fill:#fff,stroke:#888,stroke-width:1px
    style Server fill:#fff8e1,stroke:#f9a825,stroke-width:1px

이 상태에서는

  • 인터넷에서 서버에 접근할 수 없고
  • 서버도 인터넷으로 나갈 수 없다.

즉 VPC는 기본적으로
외부와 분리된 네트워크다.


2. 인터넷 게이트웨이란 무엇인가

VPC를 하나의 회사 내부 네트워크라고 생각해보자.

회사 내부 네트워크도
외부 인터넷과 통신하려면
출입구가 필요하다.

AWS에서 이 역할을 하는 것이

인터넷 게이트웨이 (Internet Gateway)

이다.

인터넷 게이트웨이는

VPC와 인터넷을 연결하는 장치

라고 이해하면 된다.


3. 퍼블릭 서브넷과 인터넷 게이트웨이

퍼블릭 서브넷에는
인터넷과 통신해야 하는 서버가 위치한다.

예를 들어 웹 서버가 여기에 배치된다.

인터넷에서 들어오는 트래픽은
먼저 인터넷 게이트웨이를 통과한 뒤
VPC 내부 서버로 전달된다.

구조를 보면 다음과 같다.

flowchart LR
    Internet["Internet"]

    subgraph Region["Region"]
        subgraph VPC["VPC"]
            IGW["Internet\nGateway"]

            subgraph PublicSubnet["Public Subnet"]
                Web1["웹 서버"]
                Web2["웹 서버"]
            end
        end
    end

    Internet <-->|"트래픽"| IGW
    IGW --> Web1
    IGW --> Web2

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px
    style PublicSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:4
    style Internet fill:#fff,stroke:#888,stroke-width:1px
    style IGW fill:#f3e5f5,stroke:#9c27b0,stroke-width:1px
    style Web1 fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style Web2 fill:#fff8e1,stroke:#f9a825,stroke-width:1px

이 구조에서 네트워크 흐름은 다음과 같다.

Internet
↓
Internet Gateway
↓
Public Subnet
↓
Web Server

즉 퍼블릭 서브넷의 서버들은
인터넷 게이트웨이를 통해
인터넷과 통신할 수 있다.


4. 하지만 이것만으로는 충분하지 않다

인터넷 게이트웨이를 연결했다고 해서
모든 서버가 자동으로 인터넷과 통신하는 것은 아니다.

왜냐하면 네트워크에는

트래픽이 어디로 이동할지 결정하는 규칙

이 필요하기 때문이다.

이 역할을 하는 것이

라우팅 테이블 (Route Table)

이다.


6. 이 장의 핵심 정리

  1. VPC는 기본적으로 인터넷과 연결되어 있지 않다.
  2. 인터넷과 통신하려면 인터넷 게이트웨이가 필요하다.
  3. 인터넷 게이트웨이는 VPC와 인터넷을 연결하는 장치다.
  4. 퍼블릭 서브넷의 서버는 인터넷 게이트웨이를 통해 인터넷과 통신한다.
  5. 실제 네트워크 경로는 라우팅 테이블을 통해 결정된다.

24장. 라우팅 테이블 (Route Table)

이 장에서 말하고자 하는 것

앞 장에서 우리는
인터넷 게이트웨이 (Internet Gateway) 를 통해
VPC가 인터넷과 연결되는 구조를 살펴보았다.

하지만 여기서 한 가지 질문이 생긴다.

인터넷에서 들어온 트래픽은
어느 서버로 전달될까?

또는

서버가 인터넷으로 요청을 보내면
어디로 나가야 할까?

이처럼 네트워크에서는 항상
트래픽을 어디로 보낼지 결정하는 규칙이 필요하다.

이 역할을 하는 것이

라우팅 테이블 (Route Table)

이다.


1. 라우팅이란 무엇인가

네트워크에서 라우팅(Routing)

트래픽을 어디로 보낼지 결정하는 과정

을 의미한다.

예를 들어 서버가 외부 주소로 요청을 보내면

8.8.8.8

이 트래픽을

인터넷으로 보낼지
내부 네트워크로 보낼지

결정해야 한다.

이러한 규칙을 모아 놓은 것이
라우팅 테이블이다.


2. 라우팅 테이블의 구조

라우팅 테이블은 보통 다음과 같은 형태로 구성된다.

DestinationTarget
10.0.0.0/16local
0.0.0.0/0Internet Gateway

각 항목의 의미는 다음과 같다.

Destination

→ 목적지 네트워크

Target

→ 트래픽을 보낼 위치

즉 라우팅 테이블은

특정 주소로 가는 트래픽을 어디로 보낼지 정의한 규칙 목록

이다.


3. 라우팅 규칙의 두 가지 유형

라우팅 규칙은 크게 두 가지 역할을 한다.

① 내부 통신

다음 규칙을 보자.

DestinationTarget
10.0.0.0/16local

이 규칙의 의미는 다음과 같다.

VPC 내부 네트워크로 가는 트래픽은 VPC 내부로 전달한다

그래서

Web → App
App → DB

같은 내부 통신이 가능하다.


② 인터넷 통신

다음 규칙을 보자.

DestinationTarget
0.0.0.0/0Internet Gateway

이 규칙의 의미는 다음과 같다.

인터넷으로 가는 모든 트래픽은
Internet Gateway로 보낸다

그래서 서버가 외부 주소로 요청을 보내면
인터넷 게이트웨이를 통해 인터넷으로 나가게 된다.


4. 여러 규칙이 일치할 때

하나의 IP 주소가
여러 라우팅 규칙과 동시에 일치할 수도 있다.

예를 들어 다음 라우팅 테이블이 있다고 하자.

DestinationTarget
10.0.0.0/16local
0.0.0.0/0Internet Gateway

그리고 목적지가

10.0.0.20

이라면

10.0.0.0/16
0.0.0.0/0

두 규칙 모두와 일치한다.

이 경우 라우팅 테이블은

더 구체적인 네트워크 규칙을 선택한다

예를 들어

10.0.0.0/16

0.0.0.0/0

보다 더 좁은 범위의 네트워크다.

그래서 목적지가

10.0.0.20

이면

10.0.0.0/16 → local

규칙이 선택된다.

이 방식을 네트워크에서는

Longest Prefix Match

라고 부른다.


5. 서브넷과 라우팅 테이블

AWS에서는

서브넷이 라우팅 테이블을 사용한다

즉 어떤 서브넷이 어떤 라우팅 테이블을 사용하는지에 따라

인터넷 연결 여부

가 결정된다.

예를 들어 라우팅 테이블에 다음 규칙이 있으면

DestinationTarget
0.0.0.0/0Internet Gateway

그 서브넷은

인터넷과 연결된 서브넷

이 된다.

즉 이것이

퍼블릭 서브넷이다.


6. 프라이빗 서브넷

반대로 어떤 서브넷의 라우팅 테이블에

0.0.0.0/0 → Internet Gateway

규칙이 없다면

그 서브넷은

인터넷에서 직접 접근할 수 없는 네트워크

가 된다.

이것이 프라이빗 서브넷이다.


7. 이 장의 핵심 정리

  1. 라우팅은 트래픽을 어디로 보낼지 결정하는 과정이다.
  2. 라우팅 규칙을 모아 놓은 것이 라우팅 테이블이다.
  3. 라우팅 테이블은 Destination → Target 구조로 이루어진다.
  4. 하나의 주소가 여러 규칙과 일치하면 더 구체적인 규칙이 선택된다.
  5. 서브넷이 어떤 라우팅 테이블을 사용하는지에 따라 퍼블릭 / 프라이빗 서브넷이 결정된다.

25장. NAT Gateway

이 장에서 말하고자 하는 것

앞 장에서 우리는
라우팅 테이블(Route Table) 을 통해
트래픽이 어디로 이동하는지 결정된다는 것을 살펴보았다.

또한 다음과 같은 사실도 알게 되었다.

  • 퍼블릭 서브넷 → 인터넷 게이트웨이로 연결 가능
  • 프라이빗 서브넷 → 인터넷 게이트웨이로 직접 연결되지 않음

여기서 한 가지 질문이 생긴다.

프라이빗 서브넷의 서버는
인터넷으로 나갈 수 없을까?

예를 들어 서버가 다음 작업을 해야 할 수도 있다.

  • 패키지 다운로드
  • 외부 API 호출
  • OS 업데이트

이처럼 인터넷으로 나가는 통신이 필요한 경우가 있다.

이 문제를 해결하는 것이

NAT Gateway

이다.


1. 프라이빗 서브넷의 문제

프라이빗 서브넷의 서버는
인터넷에서 직접 접근할 수 없다.

예를 들어 다음과 같은 구조가 있다고 하자.

flowchart LR
    Internet["Internet"]

    subgraph Region["Region"]
        subgraph VPC["VPC"]
            subgraph PublicSubnet["Public Subnet"]
                Web["웹 서버"]
            end

            subgraph PrivateSubnet["Private Subnet"]
                App["애플리케이션 서버"]
                DB[("데이터베이스")]
            end
        end
    end

    Internet -->|"인바운드 트래픽"| Web
    Web -->|"내부 호출"| App
    App -->|"쿼리"| DB

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px
    style PublicSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:4
    style PrivateSubnet fill:#e3f2fd,stroke:#64b5f6,stroke-width:1px,stroke-dasharray:4
    style Internet fill:#fff,stroke:#888,stroke-width:1px
    style Web fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style App fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style DB fill:#fce4ec,stroke:#e91e63,stroke-width:1px

이 구조에서

App → Internet

같은 요청은
인터넷 게이트웨이를 직접 사용할 수 없다.


2. NAT란 무엇인가

NAT는

Network Address Translation

의 약자다.

간단히 말하면

내부 IP 주소를
외부 IP 주소로 변환하는 기술

이다.

예를 들어 다음과 같은 상황을 생각해보자.

App Server
10.0.2.10

이 서버가 인터넷으로 요청을 보내면
외부에서는 이 주소를 알 수 없다.

그래서 NAT는

10.0.2.10
→ 공인 IP

으로 변환하여
인터넷과 통신하게 만든다.


3. NAT Gateway의 역할

AWS에서는 이 NAT 기능을

NAT Gateway

가 수행한다.

NAT Gateway는 보통
퍼블릭 서브넷에 위치한다.

그리고 구조는 다음과 같다.

flowchart RL
    Internet["Internet"]

    subgraph Region["Region"]
        subgraph VPC["VPC"]
            IGW["Internet\nGateway"]

            subgraph PublicSubnet["Public Subnet"]
                NAT["NAT\nGateway"]
            end

            subgraph PrivateSubnet["Private Subnet"]
                App["애플리케이션 서버"]
            end
        end
    end

    App -->|"아웃바운드 요청"| NAT
    NAT -->|"외부 요청"| IGW
    IGW <-->|"트래픽"| Internet

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px
    style PublicSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:4
    style PrivateSubnet fill:#e3f2fd,stroke:#64b5f6,stroke-width:1px,stroke-dasharray:4
    style Internet fill:#fff,stroke:#888,stroke-width:1px
    style IGW fill:#f3e5f5,stroke:#9c27b0,stroke-width:1px
    style NAT fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style App fill:#fff8e1,stroke:#f9a825,stroke-width:1px

이 구조에서

Private Subnet → Internet

통신이 가능해진다.


4. 중요한 특징

NAT Gateway는 다음 특징을 가진다.

내부 → 외부 가능

프라이빗 서브넷의 서버는
인터넷으로 요청을 보낼 수 있다.

App → Internet

외부 → 내부 불가능

인터넷에서는
프라이빗 서브넷의 서버에
직접 접근할 수 없다.

Internet → App

은 불가능하다.


5. 라우팅 테이블과 NAT Gateway

프라이빗 서브넷의 라우팅 테이블에는
보통 다음 규칙이 추가된다.

DestinationTarget
10.0.0.0/16local
0.0.0.0/0NAT Gateway

이 규칙의 의미는 다음과 같다.

인터넷으로 가는 트래픽은
NAT Gateway로 보낸다

그래서 프라이빗 서브넷의 서버는

App → NAT Gateway → Internet

경로로 인터넷과 통신하게 된다.


6. 전체 네트워크 구조 정리

지금까지 살펴본 AWS 네트워크 구조를
전체적으로 보면 다음과 같다.

flowchart LR
    Internet["Internet"]

    subgraph Region["Region"]
        subgraph VPC["VPC"]
            IGW["Internet\nGateway"]

            subgraph PublicSubnet["Public Subnet"]
                Web["EC2\n(Web Server)"]
                NAT["NAT\nGateway"]
            end

            subgraph PrivateSubnet["Private Subnet"]
                App["애플리케이션 서버"]
                DB[("데이터베이스")]
            end
        end
    end

    Internet <--> IGW
    IGW <--> Web
    IGW <--> NAT
    NAT <--> App
    Web --> App
    App --> DB

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px
    style PublicSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:4
    style PrivateSubnet fill:#e3f2fd,stroke:#64b5f6,stroke-width:1px,stroke-dasharray:4
    style Internet fill:#fff,stroke:#888,stroke-width:1px
    style IGW fill:#f3e5f5,stroke:#9c27b0,stroke-width:1px
    style Web fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style NAT fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style App fill:#fff8e1,stroke:#f9a825,stroke-width:1px
    style DB fill:#fce4ec,stroke:#e91e63,stroke-width:1px

이 구조에서

  • 외부 사용자는 웹 서버에 접근
  • 내부 서버는 인터넷으로 요청 가능
  • 데이터베이스는 외부에서 접근 불가

가 된다.


7. 이 장의 핵심 정리

  1. 프라이빗 서브넷의 서버는 인터넷 게이트웨이를 직접 사용할 수 없다.
  2. NAT는 내부 IP를 외부 IP로 변환하는 기술이다.
  3. AWS에서는 NAT Gateway가 이 역할을 수행한다.
  4. NAT Gateway는 보통 퍼블릭 서브넷에 위치한다.
  5. 프라이빗 서브넷은 0.0.0.0/0 → NAT Gateway 라우팅을 사용한다.
  6. NAT Gateway를 통해 프라이빗 서브넷 → 인터넷 통신이 가능해진다.

26장. 보안 그룹 (Security Group)

이 장에서 말하고자 하는 것

앞 장에서 우리는
서버들이 서로 통신할 수 있는 구조를 만들었다.

이제 서버는 연결되어 있다.

하지만 여기서 중요한 문제가 하나 있다.

이 서버에 누가 접근할 수 있는가?

이걸 제어하는 것이

보안 그룹(Security Group)

이다.


1. EC2를 만들면 반드시 설정해야 하는 것

AWS에서 EC2를 생성할 때
반드시 설정해야 하는 것이 있다.

보안 그룹

서버는 생성되는 순간부터
외부 접근을 제어해야 하기 때문이다.


2. 보안 그룹은 무엇을 기준으로 동작할까

보안 그룹은 다음 3가지 기준으로 동작한다.

IP 또는 보안그룹 ID
포트
프로토콜

예시

IP: 0.0.0.0/0
포트: 80, 443
프로토콜: TCP

👉 의미

모든 IP에서 웹 서버 접근 허용

💡 참고 (중요)

보안 그룹은 IP뿐만 아니라

다른 보안 그룹을 허용 대상으로 설정할 수 있다

허용 대상: sg-web

👉 의미

웹 서버만 접근 가능

보통은 IP로 설정하기보다

보안 그룹 ID로 설정하는 것이 더 안전하다

이유:

  • IP는 변경될 수 있음
  • 보안 그룹은 변경되지 않음

3. 기본 동작 방식

보안 그룹은 기본적으로

❌ 아무도 접근 못함 (기본 차단)

상태에서 시작한다.

그리고

✅ 필요한 것만 열어준다

이걸

화이트리스트 방식

이라고 한다.


4. 인바운드 / 아웃바운드

보안 그룹은 두 가지 방향을 가진다.

인바운드 (Inbound)

외부 → 서버로 들어오는 트래픽

아웃바운드 (Outbound)

서버 → 외부로 나가는 트래픽


5. 실제 보안 그룹 설정

이제 우리가 만든 구조에
보안 그룹을 적용해보자.

flowchart LR
    Internet["Internet"]
 
    subgraph Region["Region"]
        subgraph VPC["VPC"]
            IGW["Internet<br/>Gateway"]
 
            subgraph PublicSubnet["Public Subnet"]
                subgraph sgWeb["🔒 sg-web"]
                    Web["Web Server"]
                end
            end
 
            subgraph PrivateSubnet["Private Subnet"]
                subgraph sgApp["🔒 sg-app"]
                    App["App Server"]
                end
                subgraph sgDb["🔒 sg-db"]
                    DB[("Database")]
                end
            end
        end
    end
 
    Internet <-->|"HTTP / HTTPS"| IGW
    IGW <--> sgWeb
    sgWeb -->|"8080 허용"| sgApp
    sgApp -->|"3306 허용"| sgDb
 
    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px,color:#1a2a5e
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px,color:#1b5e20
    style PublicSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:5 4,color:#2e7d32
    style PrivateSubnet fill:#e3f2fd,stroke:#64b5f6,stroke-width:1px,stroke-dasharray:5 4,color:#0d47a1
    style sgWeb fill:#fff3e0,stroke:#e65100,stroke-width:2px,stroke-dasharray:6 3,color:#bf360c
    style sgApp fill:#ede7f6,stroke:#4527a0,stroke-width:2px,stroke-dasharray:6 3,color:#311b92
    style sgDb fill:#fce4ec,stroke:#880e4f,stroke-width:2px,stroke-dasharray:6 3,color:#880e4f
    style Internet fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px,color:#424242
    style IGW fill:#f3e5f5,stroke:#9c27b0,stroke-width:1px,color:#4a148c
    style Web fill:#fffde7,stroke:#f9a825,stroke-width:1px,color:#795548
    style App fill:#e8f5e9,stroke:#43a047,stroke-width:1px,color:#1b5e20
    style DB fill:#fce4ec,stroke:#e91e63,stroke-width:1px,color:#880e4f

① 웹 서버 (sg-web)

웹 서버는 외부 사용자들이 접근해야 한다.

그래서 다음을 허용한다.

허용 대상: 0.0.0.0/0 (모든 IP)
포트: 80 (HTTP), 443 (HTTPS)
프로토콜: TCP

👉 의미

누구든지 웹 서버에 접속 가능

② 앱 서버 (sg-app)

앱 서버는 외부에서 접근하면 안 된다.

그래서 이렇게 설정한다.

허용 대상: sg-web (웹 서버)
포트: 8080
프로토콜: TCP

👉 의미

웹 서버만 앱 서버에 접근 가능

③ DB 서버 (sg-db)

DB는 가장 중요한 데이터가 있기 때문에
더 강하게 제한해야 한다.

허용 대상: sg-app (앱 서버)
포트: 3306 (MySQL)
프로토콜: TCP

👉 의미

앱 서버만 DB 접근 가능


6. 흐름으로 이해하기

이 설정을 적용하면

가능한 흐름

사용자 → 웹 서버 (80, 443)
웹 서버 → 앱 서버 (8080)
앱 서버 → DB (3306)

불가능한 흐름

인터넷 → 앱 서버 ❌
인터넷 → DB ❌
웹 서버 → DB ❌

7. 이 장의 핵심 정리

  1. 보안 그룹은 EC2 생성 시 반드시 설정해야 한다.
  2. 보안 그룹은 IP(또는 보안그룹), 포트, 프로토콜 기준으로 동작한다.
  3. 기본은 차단이며 허용만 가능하다.
  4. IP 대신 보안 그룹을 허용 대상으로 설정할 수 있다.
  5. 보안 그룹을 사용하면 더 안전하고 관리가 쉽다.

💡 추가로 알아두기

지금까지는 EC2 기준으로 설명했지만

네트워크 통신이 필요한 AWS 리소스는
대부분 보안 그룹을 동일하게 사용한다

예를 들어

  • RDS (데이터베이스)
  • ElastiCache (Redis)
  • Load Balancer
  • 일부 컨테이너 서비스 (ECS 등)

👉 이런 리소스들도 모두

누가(IP 또는 보안그룹)
어느 포트로 접근할 수 있는지

를 보안 그룹으로 제어한다.

보안 그룹은 EC2만의 기능이 아니라
AWS 네트워크 접근 제어의 기본 개념이다

27장. NACL (Network ACL)

이 장에서 말하고자 하는 것

앞 장에서 우리는 보안 그룹을 통해
서버 단위로 접근을 제어하는 방법을 배웠다.

이제 각 서버는
누가, 어떤 포트로 접근할 수 있는지 제어할 수 있다.

그런데 한 가지 더 생각해볼 수 있다.

서버 단위가 아니라
네트워크 자체를 한 번에 제어할 수는 없을까?

예를 들어 특정 IP나 대역을
서버마다 막는 것이 아니라
아예 네트워크 입구에서 막고 싶은 경우다.

이럴 때 사용하는 것이

NACL (Network ACL)

이다.


1. NACL이란 무엇인가

NACL은

서브넷 단위에서 동작하는 접근 제어 기능

이다.

보안 그룹이 서버 앞에서 동작한다면
NACL은

서브넷 입구에서 먼저 필터링

한다고 보면 된다.


2. 흐름으로 이해하기

트래픽이 서버까지 도달하는 과정은 다음과 같다.

Internet → NACL → 보안 그룹 → 서버

  1. NACL에서 먼저 검사
  2. 보안 그룹에서 한 번 더 검사

이렇게 두 단계로 제어된다.


3. 동작 방식

NACL은 보안 그룹과 다르게
규칙을 처리하는 방식이 있다.

✔ 번호 순서대로 검사

NACL은

낮은 번호부터 순서대로 규칙을 확인한다

예시

번호설정
100모든 트래픽 차단
20080 포트 허용

이 경우 결과는 다음과 같다.

100번에서 이미 차단됨 → 80도 접근 불가

👉 핵심

먼저 매칭되는 규칙이 적용된다


4. 보안 그룹과의 차이

보안 그룹과 NACL은 비슷해 보이지만
동작 방식이 다르다.

구분보안 그룹NACL
적용 대상EC2 (서버)Subnet
규칙 방식허용만 가능허용 + 차단
상태StatefulStateless

여기서 특히 중요한 것은 두 가지다.

허용 + 차단

보안 그룹은 허용만 설정하면 되지만 NACL은

허용과 차단을 모두 직접 설정할 수 있다


Stateless

보안 그룹은 응답 트래픽이 자동 허용되지만 NACL은

인바운드와 아웃바운드를 각각 따로 설정해야 한다


5. 어떻게 사용하는가

NACL은 보통
서브넷 단위에서 전체를 제어할 때 사용한다.

예를 들어

  • 특정 IP 대역 차단
  • 외부 공격 차단

이런 경우

네트워크 입구에서 한 번에 막는다


6. 구조로 이해하기

flowchart LR
    Internet["Internet"]

    subgraph Region["Region"]
        subgraph VPC["VPC"]
            IGW["Internet<br/>Gateway"]

            subgraph PublicNACL["🛡️ NACL (Public)"]
                subgraph PublicSubnet["Public Subnet"]
                    subgraph sgWeb["🔒 sg-web"]
                        Web["Web Server"]
                    end
                end
            end

            subgraph PrivateNACL["🛡️ NACL (Private)"]
                subgraph PrivateSubnet["Private Subnet"]
                    subgraph sgApp["🔒 sg-app"]
                        App["App Server"]
                    end
                    subgraph sgDb["🔒 sg-db"]
                        DB[("Database")]
                    end
                end
            end
        end
    end

    Internet <-->|"HTTP / HTTPS"| IGW
    IGW <--> sgWeb
    sgWeb -->|"8080 허용"| sgApp
    sgApp -->|"3306 허용"| sgDb

    style Region fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2px,color:#1a2a5e
    style VPC fill:#e6f4ea,stroke:#2e7d32,stroke-width:2px,color:#1b5e20
    style PublicNACL fill:#fff8e1,stroke:#c62828,stroke-width:2px,stroke-dasharray:8 4,color:#b71c1c
    style PrivateNACL fill:#fff8e1,stroke:#c62828,stroke-width:2px,stroke-dasharray:8 4,color:#b71c1c
    style PublicSubnet fill:#e8f5e9,stroke:#66bb6a,stroke-width:1px,stroke-dasharray:5 4,color:#2e7d32
    style PrivateSubnet fill:#e3f2fd,stroke:#64b5f6,stroke-width:1px,stroke-dasharray:5 4,color:#0d47a1
    style sgWeb fill:#fff3e0,stroke:#e65100,stroke-width:2px,stroke-dasharray:6 3,color:#bf360c
    style sgApp fill:#ede7f6,stroke:#4527a0,stroke-width:2px,stroke-dasharray:6 3,color:#311b92
    style sgDb fill:#fce4ec,stroke:#880e4f,stroke-width:2px,stroke-dasharray:6 3,color:#880e4f
    style Internet fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px,color:#424242
    style IGW fill:#f3e5f5,stroke:#9c27b0,stroke-width:1px,color:#4a148c
    style Web fill:#fffde7,stroke:#f9a825,stroke-width:1px,color:#795548
    style App fill:#e8f5e9,stroke:#43a047,stroke-width:1px,color:#1b5e20
    style DB fill:#fce4ec,stroke:#e91e63,stroke-width:1px,color:#880e4f

7. 정리하면서 이해하기

보안 그룹 → 서버 접근 제어
NACL → 네트워크 접근 제어

  • 보안 그룹은 “이 서버에 접근 가능?”
  • NACL은 “이 네트워크에 들어올 수 있음?”

8. 이 장의 핵심 정리

  1. NACL은 서브넷 단위에서 동작하는 접근 제어 기능이다.
  2. 트래픽은 NACL → 보안 그룹 순서로 검사된다.
  3. NACL은 규칙을 번호 순서대로 검사한다.
  4. 허용과 차단을 모두 설정할 수 있다.
  5. Stateless 구조라서 인바운드/아웃바운드를 각각 설정해야 한다.

28장. 운영 환경을 위한 VPC 디자인

이 장에서 말하고자 하는 것

지금까지 우리는 VPC, 서브넷, 라우팅, NAT Gateway, 보안 그룹, NACL을 각각 배웠다.

이제는 이 요소들을 단순히 이해하는 것을 넘어서

운영 환경에서 어떻게 설계해야 하는지

를 이해해야 한다.

운영 환경에서는

  • 장애가 발생해도 서비스가 유지되어야 하고
  • 보안이 유지되어야 하며
  • 확장에도 대응할 수 있어야 한다

1. 운영 환경을 머리로 그려보기

사용자가 서비스를 이용하는 흐름을 생각해보자.

사용자 → 웹 서버 → 애플리케이션 서버 → 데이터베이스

이 구조는 단순하지만
운영 환경에서는 그대로 사용할 수 없다.

여기에는 몇 가지 문제가 있다.


2. 가용성 문제

서버가 하나만 있으면

서버 장애 → 서비스 중단

이 된다.

그래서 서버를 여러 개로 늘리고
서로 다른 위치에 배치한다.

AWS에서는 이를 위해

가용 영역(AZ)

을 제공한다.

운영 환경에서는

최소 2개 이상의 AZ를 사용한다

이렇게 하면

한 AZ 장애 → 다른 AZ가 서비스 유지

가 가능해진다.


3. 네트워크 구조 설계

운영 환경에서는 네트워크를 다음과 같이 나눈다.

퍼블릭 서브넷 → 외부와 통신
프라이빗 서브넷 → 내부 전용

그리고 이 구조를

각 AZ마다 동일하게 구성한다

AZ-A → Public + Private
AZ-B → Public + Private

형태로 만든다.

이렇게 해야

어느 AZ가 장애가 나도 동일한 구조로 서비스 유지가 가능하다


4. 보안 설계

모든 서버를 외부에 열어두면 위험하다.

그래서 역할에 따라 접근을 제한한다.

웹 서버 → 외부 접근 허용
앱 서버 → 웹 서버만 접근 가능
DB → 앱 서버만 접근 가능

또한 인터넷 통신이 필요 없는 자원은

프라이빗 서브넷에 배치한다

이렇게 하면

외부에서 직접 접근할 수 없는 구조가 된다


5. NAT Gateway의 역할

프라이빗 서버는 외부에서 접근하면 안 되지만
외부로 나가는 통신은 필요하다.

그래서 사용하는 것이

NAT Gateway

이다.

이 구조는 다음과 같이 동작한다.

프라이빗 → 인터넷 가능
인터넷 → 프라이빗 불가

6. 트래픽 처리 구조

사용자가 특정 서버에 직접 접근하면
서버 장애나 과부하에 취약하다.

그래서 중간에

Load Balancer

를 둔다.

사용자 → Load Balancer → 여러 웹 서버

이 구조는

  • 트래픽 분산
  • 장애 자동 대응

을 가능하게 한다.


7. IP 대역 설계

운영 환경에서는 VPC를 만들 때

충분히 큰 IP 대역을 확보해야 한다

예:

10.0.0.0/16

이유는 다음과 같다.

  • AZ 추가 가능성
  • 서브넷 분리
  • 서비스 확장

처음부터 여유 있는 설계를 해야 한다


8. Custom VPC를 사용하는 이유

기본(Default) VPC를 사용할 수도 있지만
운영 환경에서는

Custom VPC를 사용하는 것이 일반적이다

이유는 다음과 같다.

  • 원하는 구조로 설계 가능
  • IP 대역 충돌 방지
  • 확장 고려 가능

9. 전체 설계 관점 정리

운영 환경 설계는 다음 기준으로 이루어진다.

1. 최소 2개 이상의 AZ 사용 (가용성)
2. 퍼블릭 / 프라이빗 분리 (보안)
3. NAT Gateway 활용 (내부 통신)
4. Load Balancer 사용 (트래픽 분산)
5. 충분한 IP 대역 확보 (확장성)
6. Custom VPC 사용 (유연성)

10. 이 장의 핵심 정리

  1. 운영 환경에서는 가용성, 보안, 확장성을 함께 고려해야 한다.
  2. 최소 2개 이상의 AZ로 구조를 구성해야 한다.
  3. 퍼블릭과 프라이빗 서브넷을 나누고 동일한 구조를 반복한다.
  4. NAT Gateway와 Load Balancer를 통해 안정적인 구조를 만든다.
  5. VPC는 충분한 IP 대역으로 설계해야 한다.
  6. 운영 환경에서는 Custom VPC 사용이 일반적이다.

29장. 도메인이 서버까지 도달하기까지 — DNS의 이해

이 장에서 말하고자 하는 것

지금까지 우리는 서버를 어디에 두는지,
어떤 네트워크에 연결하는지를 다뤘다.

하지만 정작 사용자가 우리 서비스를 사용할 때는
서버의 IP 주소를 입력하지 않는다.

https://example.com

이렇게 도메인을 입력한다.

그러면 어떻게 이 이름이
실제 서버까지 도달할 수 있을까?

이 질문에 답하는 시스템이

DNS (Domain Name System)

다.

DNS는 우리가 만들 MSA에서 가장 바깥쪽,
즉 “서비스의 입구” 에 해당한다.


1. 사용자는 IP 주소를 외우지 않는다

서버는 결국 IP 주소로 식별된다.

3.36.124.10

하지만 사람은 숫자를 외우기 어렵다.
또 서버 IP는 바뀔 수도 있다.

그래서 사람이 외울 수 있는 이름을 쓴다.

example.com
shop.example.com
api.example.com

이 이름을 다시 IP로 바꿔주는 역할이
DNS의 핵심이다.


2. DNS는 전화번호부와 같다

DNS의 동작을 한 줄로 요약하면 이렇다.

이름을 받아 주소를 돌려준다

example.com      → 3.36.124.10
api.example.com  → 13.124.55.21

전화번호부에서 사람 이름을 보고
번호를 찾는 것과 같은 구조다.


3. DNS는 한 곳에 있지 않다

여기서 많은 사람이 오해한다.

DNS는 어딘가에 있는 하나의 서버가 아니다.

DNS는 여러 단계로 나뉜 계층 구조다.

루트(.)
 └─ TLD (.com / .net / .kr)
     └─ 권한 있는 네임서버 (example.com)
         └─ 레코드 (api.example.com = 13.124.55.21)

이 구조 덕분에 전 세계 도메인을
한 군데에서 관리하지 않고도 답을 찾을 수 있다.


4. 우리 컴퓨터는 어떻게 답을 받는가

사용자가 브라우저에 도메인을 입력하면
다음 흐름이 일어난다.

1. 브라우저 캐시 확인
2. OS 캐시 확인
3. 리졸버(통신사 DNS 등)에 질문
4. 리졸버가 루트 → TLD → 권한 네임서버 순서로 추적
5. 최종 IP를 받아 사용자에게 전달

여기서 중요한 개념이

리졸버 (Resolver)

다.

리졸버는 사용자를 대신해서
DNS 계층을 따라 답을 찾아주는 중간자다.

대부분의 답은 캐시에 들어 있어서
실제 모든 단계를 매번 거치지는 않는다.


5. TTL — 캐시는 얼마나 유지되는가

DNS 응답에는 TTL (Time To Live) 이 함께 붙는다.

TTL은 이 답을 얼마나 캐시에 보관할지를 의미한다.

TTL 60   → 60초 동안 캐시
TTL 3600 → 1시간 동안 캐시

TTL이 길면

  • 응답이 빠르다
  • 변경 반영이 느리다

TTL이 짧으면

  • 변경이 빨리 반영된다
  • 매번 조회가 늘어난다

운영 환경에서는 이 균형이 중요하다.


6. DNS 레코드의 종류

DNS는 IP만 알려주는 게 아니다.

레코드의미
A도메인 → IPv4 주소
AAAA도메인 → IPv6 주소
CNAME도메인 → 다른 도메인 (별칭)
MX메일 서버
TXT임의의 텍스트 (SPF, 인증 등)
NS이 도메인을 책임지는 네임서버

AWS 환경에서는 여기에
ALIAS 라는 AWS 전용 레코드가 추가된다.

ALIAS는 CNAME과 비슷하지만
CloudFront, ALB, S3 같은 AWS 리소스를
직접 가리킬 수 있다는 점이 다르다.

이건 30장에서 자세히 다룬다.


7. 도메인 등록자와 DNS 호스팅은 다르다

많은 사람이 헷갈리는 부분이다.

도메인을 사는 곳과
DNS를 운영하는 곳은 같을 수도 다를 수도 있다.

도메인 등록자 (Registrar)
  → 도메인 이름의 소유권을 관리한다
  → 예: 가비아, GoDaddy, Route 53 Domains

DNS 호스팅 (DNS Hosting)
  → 그 도메인의 레코드를 실제로 응답한다
  → 예: Route 53, Cloudflare DNS

운영에서는

도메인은 가비아에서 사고
DNS는 Route 53에서 운영한다

같은 구성이 흔하다.

이걸 가능하게 하는 게
NS (네임서버) 레코드다.

등록자에 “이 도메인의 DNS는 Route 53이 책임진다” 라고
지정해 두면 그 뒤로는 Route 53이 응답한다.


8. 우리 MSA에서 DNS는 어디에 있나

이 책에서 만들어 갈 척추 그림을 다시 보자.

사용자 → CloudFront → API Gateway → ALB → ECS → DB

사용자가 가장 먼저 닿는 지점은
실제로는 CloudFront가 아니다.

DNS다.

사용자
  ↓ "api.example.com 어디야?"
DNS (Route 53)
  ↓ "이 CloudFront 주소로 가"
CloudFront
  ↓ ...

즉 DNS는

우리 MSA의 가장 첫 번째 점

이다.

여기서 어디로 보낼지 잘못 결정하면
뒤에 무엇을 잘 만들어도 닿지 않는다.


9. 직접 확인해보기 — CLI로 DNS 들여다보기

DNS 응답은 명령줄에서 바로 볼 수 있다.

dig 명령

dig example.com

응답에 다음이 들어 있다.

;; ANSWER SECTION:
example.com.   300   IN   A   93.184.216.34
  • 300 → TTL
  • A → 레코드 타입
  • 93.184.216.34 → 실제 IP

특정 레코드만 조회

dig api.example.com A
dig example.com NS
dig example.com TXT

어떤 리졸버를 거쳤는지 확인

dig @8.8.8.8 example.com

이렇게 하면 Google 공용 DNS를 직접 사용해 조회한다.

이 명령들을 외울 필요는 없다.

다만 도메인이 동작하지 않을 때
“DNS 단계에서 답이 잘못 오는지” 를
가장 먼저 확인할 수 있다는 점이 중요하다.


10. 미리 보는 Route 53 — 코드로는 이렇게 생겼다

30장에서 본격적으로 다룰 Route 53을
미리 한 번만 들여다보자.

Terraform으로 표현하면 이렇다.

resource "aws_route53_zone" "main" {
  name = "example.com"
}

resource "aws_route53_record" "api" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "api.example.com"
  type    = "A"
  ttl     = 60
  records = ["13.124.55.21"]
}

지금은 이해하지 않아도 된다.

다만 우리가 콘솔에서 클릭하든
이렇게 코드로 작성하든
결국 만들어지는 결과는 같다는 것만 기억하자.


11. 이렇게 쓰면 망한다 — DNS 안티패턴

안티패턴 1. TTL을 너무 길게 둔다

TTL = 86400 (하루)

장애가 났을 때 IP를 바꿔도
하루 동안 옛 IP로 가는 사용자가 생긴다.

운영 서비스의 핵심 레코드는
60초 ~ 300초 정도로 시작하는 게 일반적이다.

안티패턴 2. 도메인 최상위에 CNAME을 단다

example.com → CNAME → my-loadbalancer-xxx.elb.amazonaws.com

DNS 표준상 도메인 최상위(apex)는 CNAME을 가질 수 없다.

AWS에서는 이 문제를 해결하기 위해
ALIAS 레코드를 제공한다.

안티패턴 3. DNS 제공자를 여러 곳에 섞어 쓴다

NS 일부는 가비아 / 일부는 Route 53

이러면 사용자에 따라 다른 답을 받아서
“누구는 되는데 누구는 안 되는” 장애가 생긴다.

한 도메인은 한 DNS에서만 응답해야 한다.

안티패턴 4. DNS만으로 로드 밸런싱을 하려고 한다

api.example.com → IP A
api.example.com → IP B

DNS 라운드 로빈으로는

  • 헬스 체크가 불완전하고
  • 캐시 때문에 트래픽이 고르게 분산되지 않으며
  • 장애 서버를 빠르게 빼낼 수 없다

이건 ALB의 일이지 DNS의 일이 아니다.


12. 한 줄로 정리

DNS는 사용자가 가장 먼저 닿는 시스템이며
도메인을 IP로 바꿔 우리 인프라의 입구로 안내하는 역할을 한다


13. 이 장의 핵심 정리

  1. 사용자는 IP가 아니라 도메인으로 서비스를 찾는다.
  2. DNS는 이름을 IP로 바꿔주는 계층 구조의 시스템이다.
  3. 리졸버와 캐시 덕분에 모든 조회가 매번 전 세계를 도는 것은 아니다.
  4. TTL은 응답을 얼마나 캐시에 둘지를 정한다.
  5. 도메인 등록자와 DNS 호스팅은 다른 개념이다.
  6. AWS에서는 DNS 운영을 위해 Route 53을 사용하며, 다음 장에서 자세히 다룬다.
  7. DNS는 우리 MSA의 가장 첫 번째 점이다.

30장. Route 53 — AWS의 DNS

이 장에서 말하고자 하는 것

앞 장에서 우리는 DNS의 개념을 배웠다.

  • 이름을 IP로 바꿔준다
  • 계층 구조로 동작한다
  • TTL과 캐시가 있다

이제 AWS에서 그 DNS를 직접 운영하는 도구를 본다.

Route 53

이다.

Route 53은 단순한 DNS 호스팅을 넘어
헬스 체크 · 트래픽 라우팅 · 도메인 등록까지
한 곳에서 다루도록 만들어졌다.


1. Route 53은 무엇인가

Route 53은 AWS의 관리형 DNS 서비스다.

  • 권한 있는 네임서버 역할을 한다
  • 가용성이 매우 높게 설계되어 있다
  • 도메인 등록도 제공한다 (선택)
  • AWS 리소스를 직접 가리키는 ALIAS 레코드를 제공한다

다른 DNS 서비스와 가장 큰 차이는

AWS 안의 리소스와 강하게 연동된다

는 점이다.


2. Hosted Zone — 도메인 단위 관리 구획

Route 53에서 도메인 하나를 관리하려면
먼저 Hosted Zone 을 만든다.

example.com → Hosted Zone 1
shop.com    → Hosted Zone 2

Hosted Zone 안에 그 도메인의 모든 레코드가 들어간다.

example.com (Hosted Zone)
 ├─ A      → 1.2.3.4
 ├─ MX     → mail.example.com
 ├─ NS     → ns-xxx.awsdns-xx.com
 └─ api    → 13.124.55.21

Hosted Zone을 만들면 AWS가
자동으로 NS 레코드 4개를 발급한다.

이 4개를 도메인 등록자에 등록하면
그 시점부터 Route 53이 그 도메인의 응답을 책임진다.


3. 레코드 만들기

기본 레코드 추가는 단순하다.

이름:   api.example.com
타입:   A
값:     13.124.55.21
TTL:    60

이 정보가 곧 DNS 응답이 된다.

dig api.example.com
# → 13.124.55.21

4. ALIAS — AWS 리소스를 직접 가리키는 특별 레코드

운영에서 가장 많이 쓰는 게 ALIAS 레코드다.

CloudFront, ALB, S3, API Gateway 같은 AWS 리소스는
실제 IP 주소가 자주 바뀐다.

A 레코드에 IP를 박아두면 그 IP가 사라질 때 장애가 난다.

대신 ALIAS를 쓰면

api.example.com (A · ALIAS) → my-alb-1234.elb.amazonaws.com

Route 53이 ALB의 현재 IP를 매번 자동으로 응답한다.

ALIAS는 CNAME처럼 보이지만 A 레코드처럼 동작한다

또 두 가지가 더 좋다.

  • 도메인 최상위(apex)에도 쓸 수 있다 (CNAME은 불가)
  • ALIAS 자체에 추가 요금이 없다

운영에서는 사실상

CloudFront / ALB / API Gateway는 ALIAS로 가리킨다

가 기본이다.


5. 라우팅 정책 — 같은 이름에 여러 답을 주기

Route 53은 단순한 “이름 → IP” 만 하지 않는다.

같은 이름에 대해 여러 라우팅 정책 을 선택할 수 있다.

정책동작
Simple그냥 하나의 답
Weighted비율에 따라 분산 (예: 90% / 10%)
Latency가장 가까운 리전으로 보냄
Geolocation사용자의 지역에 따라 다른 답
Failover정상 리소스로 자동 전환
Multi-Value Answer여러 IP를 헬스 체크와 함께 응답

운영에서 가장 많이 쓰는 두 가지는

  • Failover — 운영/백업 구조
  • Latency — 글로벌 서비스에서 지역 라우팅

이다.


6. 헬스 체크 — 죽은 곳으로 보내지 않기

Failover 라우팅을 쓰려면 헬스 체크 가 필요하다.

[헬스 체크] 30초마다 https://api.example.com/health 확인
  ↓
정상 → Primary 응답
이상 → Secondary 응답

헬스 체크는 Route 53이 전 세계 여러 지점에서
실제 요청을 보내 확인한다.


7. 우리 서비스에서 Route 53은 어디에 있나

척추 그림에서 가장 첫 점이다.

[사용자]
    ↓ DNS 질의
[Route 53]   ← 우리가 만들 곳
    ↓ ALIAS 응답
[CloudFront]
    ↓
[API Gateway]
    ↓
[ALB]
    ↓
[ECS]

Route 53은 사용자가 우리 인프라로 들어오는
가장 첫 관문이다.

여기를 잘못 설정하면 뒤쪽이 멀쩡해도
사용자는 도달하지 못한다.


8. 직접 확인해보기 — CLI

Hosted Zone 만들기

aws route53 create-hosted-zone \
  --name example.com \
  --caller-reference $(date +%s)

레코드 추가

aws route53 change-resource-record-sets \
  --hosted-zone-id Z123ABC \
  --change-batch file://record.json

Hosted Zone에 등록된 레코드 보기

aws route53 list-resource-record-sets \
  --hosted-zone-id Z123ABC

9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_route53_zone" "main" {
  name = "example.com"
}

# ALB를 가리키는 ALIAS 레코드
resource "aws_route53_record" "api" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "api.example.com"
  type    = "A"

  alias {
    name                   = aws_lb.main.dns_name
    zone_id                = aws_lb.main.zone_id
    evaluate_target_health = true
  }
}

ALIAS 레코드는 일반 A 레코드와 달리
records 가 아닌 alias 블록을 쓴다는 점만 기억하면 된다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. ALB / CloudFront 주소를 IP로 박는다

api.example.com  A  13.250.55.21  ← ALB의 현재 IP

ALB는 IP가 바뀐다. 며칠 ~ 몇 주 뒤 장애 난다.

AWS 리소스를 가리킬 때는 반드시 ALIAS를 쓴다

안티패턴 2. 도메인을 산 곳의 DNS를 그대로 쓴다

도메인은 가비아
DNS도 가비아 (기본값)

가비아 DNS에는 ALIAS가 없다.
헬스 체크도, Failover도 못 쓴다.

운영 서비스라면 NS를 Route 53으로 위임한다

안티패턴 3. 핵심 레코드 TTL을 86400(하루)으로 둔다

장애 대응이 하루 늦는다.
운영 트래픽이 받는 레코드는 60~300으로 시작한다.

안티패턴 4. Failover만 설정하고 헬스 체크는 안 만든다

이러면 전환이 일어나지 않는다.
Route 53은 헬스 체크 결과로만 Primary/Secondary를 바꾼다.


11. 한 줄로 정리

Route 53은 AWS 안의 리소스와 단단히 묶이는 관리형 DNS이며
우리 인프라의 첫 관문이다


12. 이 장의 핵심 정리

  1. Route 53은 AWS의 관리형 DNS 서비스다.
  2. 도메인 단위로 Hosted Zone을 만든다.
  3. ALIAS는 AWS 리소스를 가리킬 때 항상 우선한다.
  4. 라우팅 정책으로 같은 이름에 다양한 동작을 줄 수 있다.
  5. Failover에는 반드시 헬스 체크가 함께 있어야 한다.
  6. 핵심 레코드의 TTL은 짧게 시작해 운영 데이터로 조정한다.

31장. HTTPS는 어디에서 끝나는가 — TLS 종료의 이해

이 장에서 말하고자 하는 것

우리는 사용자가 도메인을 입력해
DNS를 거쳐 서버에 도달하는 흐름까지 왔다.

이제 다음 질문이 남는다.

https://api.example.com 의 그 "s" 는
어디에서 시작하고 어디에서 끝나는가?

이 장은 그 답을 다룬다.

핵심 키워드는

TLS 종료 (TLS Termination)

이다.


1. HTTPS는 무엇을 하는가

HTTPS는 HTTP에 보안을 더한 프로토콜이다.

  • 통신 내용을 암호화 해서 도청을 막고
  • 서버가 진짜 그 서버임을 검증 한다

이 두 가지를 동시에 하는 표준이 TLS다.

HTTP + TLS = HTTPS


2. TLS 핸드셰이크 — 매우 간단히

사용자가 서버에 처음 HTTPS로 연결하면
다음 흐름이 일어난다.

1. 사용자: 안녕, 너 누구야?
2. 서버: 내 인증서 받아 (공개키 포함)
3. 사용자: (인증서를 신뢰할 수 있는지 검증)
4. 둘이 한 번만 쓸 키를 만든다
5. 이 키로 이후 모든 데이터를 암호화

핵심은 4번이다.

진짜 데이터는 “한 번만 쓰는 키” 로 빠르게 암호화된다
인증서는 그 키를 안전하게 만들기 위한 도구다


3. 인증서가 하는 일

인증서는 서버의 신분증이다.

여기에는 다음이 들어 있다.

  • 도메인 이름 (예: api.example.com)
  • 공개키
  • 발급 기관(CA)의 서명
  • 유효 기간

브라우저는 미리 신뢰 가능한 CA 목록을 갖고 있다.

서명을 그 목록의 CA 키로 검증해 통과하면
“이 서버는 진짜다” 라고 판단한다.


4. TLS는 어디에서 끝낼 수 있는가

HTTPS 통신은 어딘가에서 한 번은
풀린(decrypted) 상태 가 되어야 한다.

서버가 요청을 읽으려면 평문이 필요하기 때문이다.

이 푸는 지점이

TLS 종료 지점

이다.

AWS에서는 세 군데 중에서 고를 수 있다.

사용자
  ↓ HTTPS
[① CloudFront]
  ↓ HTTPS or HTTP
[② ALB]
  ↓ HTTPS or HTTP
[③ EC2 / ECS]

각 지점에 인증서를 둘 수 있다.


5. 옵션 ① 엣지에서 종료 — CloudFront에서

사용자 ─HTTPS→ CloudFront ─HTTP→ ALB ─HTTP→ ECS

CloudFront 단계에서 푼다.

  • 사용자와 가까운 곳에서 핸드셰이크해 응답이 빠르다
  • 내부 구간은 HTTP라 처리가 가볍다

내부망이 AWS 안에 있고 신뢰 가능하다는 전제다.


6. 옵션 ② LB까지 HTTPS — ALB에서 종료

사용자 ─HTTPS→ CloudFront ─HTTPS→ ALB ─HTTP→ ECS

CloudFront와 ALB 사이도 HTTPS로 묶는다.

  • 내부 트래픽이 외부 네트워크를 일부 지나갈 때 안전하다
  • 인증서가 두 곳에 필요하다 (CloudFront · ALB)

운영에서 가장 일반적으로 사용하는 방식이다.


7. 옵션 ③ End-to-End — 서버까지 HTTPS

사용자 ─HTTPS→ CloudFront ─HTTPS→ ALB ─HTTPS→ ECS

서버까지 전 구간 암호화한다.

  • 가장 보안이 강하다
  • 인증서 관리가 가장 복잡하다
  • 처리 비용이 가장 크다

규제 산업(금융 · 의료)이나
민감 데이터를 다루는 서비스에서 쓴다.


8. 어디에서 종료해야 할까

판단 기준은 단순하다.

대부분 서비스   → ②번 (ALB에서 종료)
민감 데이터    → ③번 (End-to-End)
정적 콘텐츠만   → ①번 (CloudFront에서 종료)

핵심은

사용자 ~ 우리 인프라 입구 구간은 무조건 HTTPS

이고

내부 구간을 어디까지 HTTPS로 묶을지는 데이터 민감도로 결정

한다는 점이다.


9. 우리 서비스에서

이 책에서 만들 구조는 옵션 ② 다.

사용자 ─HTTPS→ CloudFront ─HTTPS→ ALB ─HTTP(내부 VPC)→ ECS

이유:

  • 보편적이다
  • 인증서 관리가 두 곳으로 제한된다
  • 내부 VPC 통신은 보안 그룹으로 통제 가능하다

10. 직접 확인해보기 — CLI

서버의 인증서 보기

openssl s_client -connect api.example.com:443 -servername api.example.com < /dev/null

응답에 인증서 정보가 들어 있다.

subject=CN = api.example.com
issuer =CN = Amazon, OU = Server CA 1B, O = Amazon, C = US

인증서의 유효 기간

echo | openssl s_client -connect api.example.com:443 2>/dev/null \
  | openssl x509 -noout -dates
notBefore=Mar  1 00:00:00 2025 GMT
notAfter =Mar  1 23:59:59 2026 GMT

이 두 명령은 운영 중에
“내 인증서가 살아 있나” 를 가장 빠르게 확인하는 방법이다.


11. 코드로는 이렇게 생겼다 — Terraform

ALB에 HTTPS 리스너를 다는 모양이다.

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.main.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

certificate_arn 이 가리키는 인증서가
다음 장 ACM에서 만들 인증서다.


12. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. HTTP 그대로 운영한다

http://api.example.com → 그대로 노출

브라우저가 경고하고, 검색 순위도 떨어진다.

운영 서비스는 무조건 HTTPS

안티패턴 2. HTTP 요청을 HTTPS로 안 보낸다

http://... 으로 온 요청을 그대로 받으면
사용자 일부는 평문으로 통신한다.

ALB 또는 CloudFront 에서

80 → 443으로 redirect

을 반드시 켠다.

안티패턴 3. 자체 서명 인증서로 운영한다

self-signed certificate

브라우저가 막는다. 사용자에게 “위험” 경고가 뜬다.

운영은 반드시 CA에서 발급한 인증서를 쓴다.
AWS에서는 ACM이 무료로 제공한다.

안티패턴 4. 인증서 만료를 모니터링하지 않는다

만료 하루 전에 알아채면 늦다.

ACM은 자동 갱신을 지원하지만
DNS 검증이 깨지면 갱신이 실패할 수 있다.

만료 30일 전부터 알람을 건다


13. 한 줄로 정리

HTTPS는 어딘가 한 곳에서 풀려야 하고,
그 지점을 어디에 둘지가 곧 보안 설계의 한 축이다


14. 이 장의 핵심 정리

  1. HTTPS는 HTTP + TLS이고, 암호화와 서버 검증을 함께 한다.
  2. TLS 종료 지점은 CloudFront · ALB · 서버 중 한 곳이다.
  3. 일반 서비스는 ALB에서 종료하는 ②번이 가장 흔하다.
  4. 민감 데이터는 End-to-End로 서버까지 HTTPS를 유지한다.
  5. HTTP는 443으로 리다이렉트하고, 자체 서명 인증서는 운영에 쓰지 않는다.
  6. 인증서 만료는 자동 갱신과 함께 알람으로 감시한다.

32장. ACM — 인증서를 어디에 다는가

이 장에서 말하고자 하는 것

앞 장에서 우리는 HTTPS와 TLS 종료를 다뤘다.

그 모든 곳에 필요한 것이

인증서

다.

이 인증서를 AWS에서 발급 · 갱신 · 배포하는 서비스가

AWS Certificate Manager (ACM)

이다.

이 장에서는 ACM을 어떻게 쓰고
어디에 붙일 수 있는지를 본다.


1. ACM이 하는 일

ACM은 세 가지 역할을 한다.

  1. 인증서를 무료로 발급한다 (퍼블릭 인증서)
  2. 자동으로 갱신한다 (DNS 검증을 유지하면)
  3. AWS 리소스에 직접 붙일 수 있게 한다

요약하면

사람이 만료를 신경 쓰지 않고
HTTPS를 운영할 수 있게 해주는 서비스

다.


2. 인증서 발급 흐름

콘솔에서 발급은 단순하다.

1. 도메인 입력 (예: api.example.com)
2. 검증 방식 선택 (DNS 권장)
3. ACM이 검증용 CNAME을 알려준다
4. 그 CNAME을 Route 53에 추가
5. 검증 완료 → 인증서 발급

DNS 검증을 한 번 걸어두면
ACM이 자동 갱신 시점에 다시 그 CNAME을 확인해
사람이 손대지 않고 갱신이 끝난다.


3. DNS 검증 vs 이메일 검증

ACM은 두 가지 검증 방식을 제공한다.

방식동작갱신
DNS지정된 CNAME을 도메인에 추가자동
Email도메인 소유자 이메일로 확인 링크수동

운영에서는 사실상 DNS 검증 한 가지 만 쓴다.


4. 와일드카드 인증서

여러 서브도메인을 하나의 인증서로 묶을 수 있다.

*.example.com

이러면 다음이 다 같은 인증서로 처리된다.

api.example.com
shop.example.com
admin.example.com

다만 와일드카드는 한 단계만 커버한다.

*.example.com           → 가능
*.api.example.com       → 별도 발급 필요
*.*.example.com         → 불가

운영에서는 보통

example.com 과 *.example.com 두 개를 함께 발급

해 두는 게 편하다.


5. 리전 제약 — CloudFront는 us-east-1

여기서 많은 사람이 한 번 막힌다.

ACM 인증서는 발급한 리전 안에서만 쓸 수 있다.

서울(ap-northeast-2) 발급 → 서울의 ALB · APIGW 에 사용 가능

그런데 CloudFront는 글로벌 서비스라서 us-east-1 인증서만 받는다.

CloudFront 에 붙일 인증서  → us-east-1 에서 발급
ALB / APIGW 에 붙일 인증서 → 그 리소스의 리전에서 발급

운영에서는 사실상

같은 도메인 인증서를 us-east-1과 서울 양쪽에 발급해 둔다


6. 어디에 붙일 수 있는가

ACM 인증서는 다음 리소스에 직접 붙는다.

  • CloudFront
  • ALB / NLB (TLS 리스너)
  • API Gateway
  • App Runner / ECS Service (간접)

EC2에 직접 붙일 수는 없다.

EC2에서 HTTPS를 종료하려면 ACM이 아니라
인증서를 직접 가져와 서버에 설치해야 한다

그래서 운영에서는 보통

EC2/ECS 앞에 ALB를 두고, ACM 인증서는 ALB에 붙인다


7. 우리 서비스에서

척추 그림에서 ACM이 닿는 지점은 두 곳이다.

[사용자]
   ↓ HTTPS (us-east-1 ACM)
[CloudFront]
   ↓ HTTPS (서울 ACM)
[ALB]
   ↓ HTTP (내부 VPC)
[ECS]

같은 도메인 api.example.com 에 대해
인증서를 us-east-1과 서울 양쪽에 발급해 둔다.


8. 직접 확인해보기 — CLI

인증서 발급 요청

aws acm request-certificate \
  --domain-name "api.example.com" \
  --validation-method DNS \
  --region ap-northeast-2

응답에 CertificateArn 이 들어 있다.

검증용 CNAME 보기

aws acm describe-certificate \
  --certificate-arn <arn> \
  --region ap-northeast-2

응답의 DomainValidationOptions.ResourceRecord
Route 53에 추가할 CNAME 정보가 들어 있다.

인증서 만료일 확인

aws acm describe-certificate \
  --certificate-arn <arn> \
  --query 'Certificate.NotAfter'

9. 코드로는 이렇게 생겼다 — Terraform

DNS 검증까지 자동화한 모양이다.

resource "aws_acm_certificate" "main" {
  domain_name       = "api.example.com"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

# 검증용 CNAME을 Route 53에 자동 추가
resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.main.domain_validation_options :
    dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  zone_id = aws_route53_zone.main.zone_id
  name    = each.value.name
  type    = each.value.type
  ttl     = 60
  records = [each.value.record]
}

# 검증 완료까지 기다림
resource "aws_acm_certificate_validation" "main" {
  certificate_arn         = aws_acm_certificate.main.arn
  validation_record_fqdns = [for r in aws_route53_record.cert_validation : r.fqdn]
}

이 세 리소스가 묶여서 한 단위처럼 동작한다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. CloudFront 인증서를 서울에서 발급한다

us-east-1이 아닌 곳에서 발급 → CloudFront 에 못 붙는다

콘솔 상단의 리전을 us-east-1으로 바꾸고 발급한다.

안티패턴 2. 검증 CNAME을 삭제한다

발급된 뒤에는 안 보이지만
검증 CNAME은 그대로 두어야 한다.

자동 갱신할 때 ACM이 그 CNAME을 다시 확인한다
삭제하면 갱신 실패 → 만료 → HTTPS 중단

안티패턴 3. EC2에 직접 HTTPS를 박는다

EC2 위에 nginx → 인증서 직접 설치

작동은 하지만

  • 갱신을 수동으로 해야 한다
  • 서버마다 같은 인증서를 배포해야 한다
  • 인증서 키 관리가 어렵다

가능한 한 ALB에 ACM을 붙이는 구조로 간다

안티패턴 4. Email 검증으로 발급한다

자동 갱신이 안 된다.

1년 뒤 사람이 잊는다 → 만료 → 장애

운영에서는 무조건 DNS 검증이다.


11. 한 줄로 정리

ACM은 인증서 발급부터 자동 갱신까지 책임지는 AWS의 인증서 관리 서비스다


12. 이 장의 핵심 정리

  1. ACM은 무료로 퍼블릭 인증서를 발급한다.
  2. DNS 검증을 쓰면 자동 갱신이 가능하다.
  3. CloudFront에 붙일 인증서는 us-east-1에서 발급한다.
  4. 같은 도메인이라도 리전마다 따로 발급해 둔다.
  5. ACM은 EC2에 직접 못 붙이므로 ALB를 함께 둔다.
  6. 검증 CNAME은 발급 후에도 그대로 유지한다.

33장. Elastic Load Balancer 지형 — ALB · NLB · GWLB

이 장에서 말하고자 하는 것

지금까지 우리는 사용자가 도메인을 거쳐
HTTPS로 들어오는 길까지 그렸다.

사용자 → DNS → CloudFront → ?

이제 그 트래픽을 실제 서버로 분산하는 도구를 본다.

Elastic Load Balancer (ELB)

ELB는 한 가지가 아니라 세 종류로 나뉜다.

이 장은 그 셋의 차이와 선택 기준을 정리한다.


1. Load Balancer가 왜 필요한가

서버를 한 대만 띄우면

사용자 → 서버 1대

세 가지 문제가 생긴다.

  • 서버가 죽으면 서비스가 죽는다
  • 트래픽이 늘면 한 대로 부족하다
  • 새 버전을 배포할 때 끊긴다

서버를 여러 대로 늘리면 이 문제는 풀리지만
사용자가 어떤 서버로 가야 할지 결정할 수 없다.

이 결정을 대신해주는 게 Load Balancer다.

사용자 → Load Balancer → [서버1 · 서버2 · 서버3]

Load Balancer는

  • 죽은 서버는 빼고
  • 트래픽을 고루 분산하고
  • 새 서버를 매끄럽게 합류시킨다

2. ELB의 세 종류

AWS의 Elastic Load Balancing은 세 가지로 나뉜다.

종류풀네임작동 계층핵심 용도
ALBApplication Load BalancerL7 (HTTP/HTTPS)웹 트래픽 · API
NLBNetwork Load BalancerL4 (TCP/UDP)초저지연 · 비-HTTP
GWLBGateway Load BalancerL3 (IP)보안 어플라이언스 체인

L7 / L4 / L3 의 차이가 핵심이다.


3. L4와 L7의 차이 — 누가 트래픽을 어디까지 보는가

네트워크는 계층으로 나뉜다.

L7  애플리케이션  HTTP / HTTPS의 내용
L6  표현
L5  세션
L4  전송         TCP / UDP의 포트
L3  네트워크     IP 주소
L2  데이터링크
L1  물리
  • L4 LB(NLB)는 IP와 포트만 본다 → 빠르다
  • L7 LB(ALB)는 HTTP 헤더 · 경로 · 쿠키까지 본다 → 똑똑하다

이 차이가 두 LB의 능력 차이를 만든다.


4. ALB — HTTP를 똑똑하게 분배

ALB는 HTTP/HTTPS 트래픽을 다룬다.

다음을 보고 라우팅을 결정할 수 있다.

  • 경로 (/api, /admin)
  • 호스트 (api.example.com vs admin.example.com)
  • HTTP 헤더
  • 쿼리 스트링
  • HTTP 메서드 (GET / POST)

그래서

같은 ALB 뒤에 여러 서비스를 붙여
경로별로 다른 서비스로 보낼 수 있다

이게 MSA 진입점으로 ALB가 가장 흔히 쓰이는 이유다.


5. NLB — 빠르고 단순하게

NLB는 TCP / UDP 계층에서 동작한다.

  • HTTP 헤더를 보지 않는다
  • 그래서 빠르다 (지연 100µs 수준)
  • 고정 IP를 가질 수 있다
  • 초당 수백만 요청 처리

용도:

  • 게임 서버
  • WebSocket / gRPC 일부
  • TLS만 패스스루로 넘기고 싶을 때
  • 고정 IP가 필요한 환경 (방화벽 화이트리스트 등)

6. GWLB — 보안 장비를 가운데 끼우기

GWLB는 좀 다르다.

방화벽 · IDS · DPI 같은
보안 어플라이언스를 트래픽 경로에 끼우기 위한 LB다.

요청 → GWLB → [보안 장비들] → 원래 목적지

일반 서비스 운영에서는 거의 만질 일이 없다.

이름만 기억하고 넘어가도 충분하다.


7. 어떤 걸 골라야 하는가

선택 기준은 단순하다.

HTTP/HTTPS 서비스          → ALB
TCP/UDP · 초저지연 · 고정 IP → NLB
보안 장비 체인              → GWLB

이 책에서 만들 마이크로서비스 구조는 모두 HTTP 기반이므로

ALB 가 기본 선택지

다.

NLB는 36장에서 따로 다룬다.


8. ALB와 NLB의 짧은 비교

항목ALBNLB
계층L7L4
프로토콜HTTP / HTTPS / gRPCTCP / UDP / TLS
경로 기반 라우팅가능불가
고정 IP불가가능
지연보통매우 낮음
가격처리 단위처리 단위

9. 우리 서비스에서

척추 그림에서 ELB가 들어가는 위치다.

[사용자]
   ↓
[CloudFront]
   ↓
[API Gateway]
   ↓
[ALB]   ← 여기, 이번 단원의 주제
   ↓
[ECS Task 1 · 2 · 3 ...]

ALB가 사용자가 만든 트래픽을
실제 컨테이너로 보내는 다리 역할을 한다.


10. 직접 확인해보기 — CLI

ALB 목록 보기

aws elbv2 describe-load-balancers

특정 ALB 상세

aws elbv2 describe-load-balancers \
  --names my-alb

헬스 상태 확인

aws elbv2 describe-target-health \
  --target-group-arn <tg-arn>

11. 코드로는 이렇게 생겼다 — Terraform

ALB 한 대를 만드는 가장 작은 코드다.

resource "aws_lb" "main" {
  name               = "msa-alb"
  internal           = false
  load_balancer_type = "application"
  subnets            = [aws_subnet.public_a.id, aws_subnet.public_b.id]
  security_groups    = [aws_security_group.alb.id]
}

load_balancer_type"application" (ALB) · "network" (NLB) · "gateway" (GWLB) 중 하나가 들어간다.

ALB는 최소 2개의 AZ 에 걸쳐 만든다.


12. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 모든 트래픽에 NLB를 쓴다

NLB는 빠르지만 멍청하다.
경로 라우팅 · 헤더 검사 · 호스트 라우팅 같은 일을 못 한다.

HTTP라면 ALB가 거의 항상 정답

안티패턴 2. ALB를 한 AZ에만 둔다

ALB는 AZ마다 노드가 만들어진다.
한 AZ에만 두면 그 AZ가 죽으면 LB 자체가 죽는다.

최소 두 개 이상의 AZ에 걸친다

안티패턴 3. LB 없이 EC2를 도메인으로 직접 가리킨다

api.example.com → EC2 한 대의 IP
  • EC2 IP가 바뀌면 끊긴다
  • 한 대만 죽어도 서비스가 죽는다

운영 서비스에 LB는 사실상 필수

안티패턴 4. ALB의 인증서를 EC2/ECS 안에도 또 박는다

ALB에서 종료할 거면 EC2는 HTTP로 받게 둔다.
양쪽에 똑같은 인증서를 박으면 운영만 복잡해진다.


13. 한 줄로 정리

ELB는 ALB · NLB · GWLB 세 가지이며,
HTTP 기반 MSA에서는 ALB가 가장 자주 사용된다


14. 이 장의 핵심 정리

  1. Load Balancer는 트래픽 분산과 장애 격리를 동시에 해준다.
  2. AWS의 ELB는 ALB · NLB · GWLB 세 가지로 나뉜다.
  3. ALB는 L7에서 동작해 경로 · 호스트 라우팅이 가능하다.
  4. NLB는 L4에서 동작해 빠르고 고정 IP를 제공한다.
  5. GWLB는 보안 장비 체인을 위한 특수 LB다.
  6. ALB는 최소 두 AZ에 걸쳐 만든다.

34장. ALB의 라우팅 — 리스너 · 규칙 · 타깃 그룹

이 장에서 말하고자 하는 것

앞 장에서 우리는 ELB 세 종류를 보고
HTTP 서비스에서는 ALB가 기본 선택임을 정리했다.

이제 ALB 안으로 들어간다.

ALB가 트래픽을 어떻게 받아
어디로 보내는지를 결정하는 세 부품이 있다.

리스너 (Listener)
   ↓
규칙 (Rule)
   ↓
타깃 그룹 (Target Group)
   ↓
실제 서버

이 셋의 관계를 이해하면 ALB의 모든 동작이 한눈에 들어온다.


1. 큰 그림 먼저

[ALB]
 ├─ Listener (443 HTTPS)
 │   ├─ Rule 1: path = /api/*       → TG-api
 │   ├─ Rule 2: host = admin.…      → TG-admin
 │   └─ default                     → TG-web
 │
 ├─ Target Group "TG-api"   ─→ [api-task-1, api-task-2]
 ├─ Target Group "TG-admin" ─→ [admin-task-1, admin-task-2]
 └─ Target Group "TG-web"   ─→ [web-task-1, web-task-2]

이 한 장에 모든 게 들어 있다.


2. 리스너 — 어떤 포트로 받을 것인가

리스너는

어떤 포트 · 어떤 프로토콜로 트래픽을 받는다

의 선언이다.

흔히 두 개를 만든다.

Listener 80  (HTTP)  → 443으로 리다이렉트
Listener 443 (HTTPS) → 실제 라우팅 규칙

HTTPS 리스너에는 ACM 인증서가 붙는다.


3. 규칙 — 어떤 조건에 어디로 보낼 것인가

리스너 안에 여러 규칙(Rule) 을 둘 수 있다.

규칙은 다음 정보로 매칭한다.

  • 경로 (/api/*)
  • 호스트 (admin.example.com)
  • HTTP 헤더
  • HTTP 메서드 (GET/POST)
  • 쿼리 스트링
  • 소스 IP
규칙 1: 경로가 /api/* 이면     → TG-api
규칙 2: 호스트가 admin.… 이면  → TG-admin
기본:   그 외 전부             → TG-web

규칙은 우선순위 순으로 평가되고
가장 먼저 매칭된 규칙이 적용된다.


4. 타깃 그룹 — 실제 서버들의 모임

타깃 그룹(Target Group)은

같은 역할을 하는 서버들의 묶음

이다.

TG-api
   ├─ task 1 (10.0.1.10:8080)
   ├─ task 2 (10.0.2.11:8080)
   └─ task 3 (10.0.3.12:8080)

타깃은 다음 세 종류가 될 수 있다.

  • EC2 인스턴스
  • IP 주소 (ECS Fargate가 여기 해당)
  • Lambda 함수

ECS / Fargate 환경에서는 IP 타입 타깃 그룹 을 쓴다.


5. 헬스 체크 — 누가 살아 있는가

타깃 그룹마다 헬스 체크 규칙이 있다.

경로:      /health
프로토콜:   HTTP
간격:      30초
정상 응답:  200 OK
연속 정상:  2번 → 살아 있음
연속 실패:  2번 → 죽음으로 표시

ALB는 살아 있는 타깃에만 트래픽을 보낸다.

새 컨테이너가 떠도 헬스 체크를 통과해야 트래픽이 들어간다
그래서 /health 엔드포인트는 모든 서비스에 필수다


6. 분배 알고리즘

ALB가 여러 타깃 사이에 트래픽을 어떻게 나누는가.

기본은 Round Robin 이다.

요청 1 → task 1
요청 2 → task 2
요청 3 → task 3
요청 4 → task 1
...

특정 사용자를 같은 서버에 계속 보내고 싶다면
Sticky Session(고정 세션) 을 켠다.

사용자 A (쿠키) → 항상 task 2

다만 가능한 한 무상태로 만들고 sticky를 켜지 않는 게
확장과 장애 대응에 유리하다.


7. 우리 서비스에서

척추 그림에서 ALB 안쪽 모양은 이렇다.

[ALB]
 ├─ Listener 443
 │   ├─ /api/orders/*    → TG-orders
 │   ├─ /api/users/*     → TG-users
 │   ├─ /api/payments/*  → TG-payments
 │   └─ default           → TG-web
 │
 ├─ TG-orders   → ECS "orders"   (2 task)
 ├─ TG-users    → ECS "users"    (2 task)
 ├─ TG-payments → ECS "payments" (2 task)
 └─ TG-web      → ECS "web"      (2 task)

ALB 한 대 안에서
경로만으로 4개의 마이크로서비스를 나눠 보낸다.

이게 다음 장에서 본격적으로 다룰 주제다.


8. 직접 확인해보기 — CLI

리스너 보기

aws elbv2 describe-listeners \
  --load-balancer-arn <alb-arn>

리스너 안의 규칙 보기

aws elbv2 describe-rules \
  --listener-arn <listener-arn>

타깃 헬스 상태 확인

aws elbv2 describe-target-health \
  --target-group-arn <tg-arn>

이 명령은 운영 중 가장 자주 쓴다.

“트래픽이 왜 우리 서비스로 안 와요?” → 80%는 헬스 체크 실패


9. 코드로는 이렇게 생겼다 — Terraform

타깃 그룹 · 리스너 · 규칙을 한 묶음으로 만든 모양이다.

# 타깃 그룹
resource "aws_lb_target_group" "api" {
  name        = "tg-api"
  port        = 8080
  protocol    = "HTTP"
  target_type = "ip"            # Fargate 를 위해 IP 타입
  vpc_id      = aws_vpc.main.id

  health_check {
    path                = "/health"
    interval            = 30
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }
}

# 443 리스너
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  certificate_arn   = aws_acm_certificate.main.arn
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.web.arn
  }
}

# /api/* 경로 규칙
resource "aws_lb_listener_rule" "api" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 100

  condition {
    path_pattern {
      values = ["/api/*"]
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.api.arn
  }
}

세 리소스가 한 단위로 동작한다는 점만 기억하면 된다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. /health 엔드포인트를 인증 뒤에 둔다

GET /health → 401 Unauthorized

ALB는 401을 실패로 본다 → 타깃이 영원히 죽음 상태.

/health 는 인증 없이 200을 돌려준다

안티패턴 2. 헬스 체크 경로를 무겁게 만든다

/health → DB 쿼리 5개 → 외부 API 호출

헬스 체크 자체가 시스템을 무겁게 만든다.

/health 는 빠른 응답만 한다
깊은 검사는 별도 /ready 등으로 분리

안티패턴 3. 모든 서비스를 하나의 타깃 그룹에 몰아넣는다

TG-everything → orders · users · payments · web 모두
  • 한 서비스 장애가 헬스 체크에 섞인다
  • 스케일링이 같이 묶인다
  • 배포가 분리되지 않는다

서비스별로 타깃 그룹을 분리한다

안티패턴 4. Sticky Session을 기본으로 켠다

확장하기 어렵고 장애 대응이 늦어진다.

가능한 한 무상태로 만들고 sticky는 마지막 수단


11. 한 줄로 정리

ALB는 리스너 · 규칙 · 타깃 그룹의 세 부품으로 트래픽을 받아 분배한다


12. 이 장의 핵심 정리

  1. ALB는 리스너 · 규칙 · 타깃 그룹의 3단 구조다.
  2. 리스너는 어떤 포트로 받을지를 정한다.
  3. 규칙은 경로 · 호스트 등의 조건으로 어디로 보낼지 결정한다.
  4. 타깃 그룹은 같은 역할의 서버들을 묶고, 헬스 체크로 살아 있는 곳에만 보낸다.
  5. ECS / Fargate에서는 IP 타입 타깃 그룹을 쓴다.
  6. /health 는 인증 없이, 가볍게, 항상 빠르게 응답해야 한다.

35장. 경로 · 호스트 기반 라우팅으로 서비스 쪼개기

이 장에서 말하고자 하는 것

앞 장에서 우리는 ALB의 부품 — 리스너 · 규칙 · 타깃 그룹 — 을 봤다.

이 장은 그 부품을 가지고

하나의 ALB로 여러 마이크로서비스를 어떻게 나눠 받는가

를 본다.

마이크로서비스 구조에서 ALB가 가장 빛나는 지점이다.


1. 출발 — 모놀리스에서 MSA로

작은 서비스에는 보통 ALB 한 대 + 서버 한 묶음이면 충분하다.

사용자 → ALB → web 서버 (모놀리스)

서비스가 커지면 기능별로 나누고 싶어진다.

주문, 사용자, 결제, 알림 ...

각각을 다른 서비스로 띄우면

사용자 → ALB → orders 서비스
                users 서비스
                payments 서비스

이때 ALB가 어떻게 분기할지 결정해야 한다.


2. 두 가지 분기 방법

ALB가 서비스를 나누는 방법은 크게 두 가지다.

  • 경로 기반(Path-based)
  • 호스트 기반(Host-based)

둘은 함께 써도 된다.


3. 경로 기반 라우팅

같은 도메인 안에서 경로로 서비스를 나눈다.

api.example.com/orders/*   → orders 서비스
api.example.com/users/*    → users 서비스
api.example.com/payments/* → payments 서비스
api.example.com/*           → web (기본)

특징:

  • 클라이언트는 도메인 하나만 알면 된다
  • 인증서도 한 장이면 된다
  • 프론트엔드와 같은 도메인이라 CORS가 단순해진다

작은 ~ 중간 규모 MSA에 적합하다.


4. 호스트 기반 라우팅

서브도메인으로 나눈다.

orders.example.com   → orders 서비스
users.example.com    → users 서비스
payments.example.com → payments 서비스
admin.example.com    → admin 서비스

특징:

  • 서비스 경계가 명확하다
  • DNS 와 인증서 관리 부담이 늘어난다 (ACM 와일드카드로 완화 가능)
  • 보안 경계 분리에 유리하다

내부망과 외부망이 섞인 환경이나
관리자/사용자 영역 분리에 자주 쓴다.


5. 둘을 함께 쓰는 패턴

운영에서는 보통 섞어 쓴다.

api.example.com/orders/*    → orders
api.example.com/users/*     → users
api.example.com/payments/*  → payments

admin.example.com/*         → admin

외부 사용자용 API는 경로로 나누고
관리자나 별도 도메인이 필요한 영역은 호스트로 분리


6. 규칙은 우선순위대로

ALB의 규칙은 우선순위(priority)대로 평가된다.

priority 10 : path = /api/orders/*    → TG-orders
priority 20 : path = /api/users/*     → TG-users
priority 30 : path = /api/payments/*  → TG-payments
default     : *                       → TG-web

priority가 작을수록 먼저 평가된다.

가장 구체적인 규칙을 앞에 둔다.

/api/orders/special   ← 먼저
/api/orders/*         ← 그다음
/api/*                ← 그 다음
/*                    ← 마지막

7. 서비스마다 타깃 그룹은 따로

이게 가장 중요한 원칙이다.

orders   → TG-orders   → orders 서비스
users    → TG-users    → users 서비스
payments → TG-payments → payments 서비스

한 ALB · 여러 타깃 그룹 · 각 타깃 그룹은 한 서비스

이렇게 분리해야

  • 서비스별 헬스 체크가 독립된다
  • 서비스별 스케일링이 분리된다
  • 서비스별 배포가 분리된다
  • 한 서비스 장애가 다른 서비스에 안 번진다

8. 우리 서비스에서

척추에 점을 찍어보자.

[사용자]
    ↓ DNS
[CloudFront]
    ↓
[API Gateway]
    ↓
[ALB]   ← 여기서 경로로 4개로 나뉜다
 ├─ /api/orders/*   → ECS "orders"
 ├─ /api/users/*    → ECS "users"
 ├─ /api/payments/* → ECS "payments"
 └─ /*              → ECS "web"

이 ALB가 사실상

사용자가 만든 요청을 마이크로서비스로 흩어 보내는 분배기

역할을 한다.


9. 직접 확인해보기 — CLI

규칙 추가

aws elbv2 create-rule \
  --listener-arn <listener-arn> \
  --priority 10 \
  --conditions Field=path-pattern,Values='/api/orders/*' \
  --actions Type=forward,TargetGroupArn=<tg-orders-arn>

규칙 우선순위 변경

aws elbv2 set-rule-priorities \
  --rule-priorities RuleArn=<rule-arn>,Priority=5

어떤 규칙에 매칭됐는지 확인

ALB 액세스 로그를 S3에 켜두면
요청별로 어떤 타깃 그룹에 갔는지 다 남는다.

target_group_arn = arn:aws:elasticloadbalancing:.../TG-orders/...

10. 코드로는 이렇게 생겼다 — Terraform

서비스 3개를 한 ALB로 분기하는 모양이다.

# 공통 ALB와 리스너는 앞 장에 있다고 가정

locals {
  services = {
    orders   = { path = "/api/orders/*",   port = 8081 }
    users    = { path = "/api/users/*",    port = 8082 }
    payments = { path = "/api/payments/*", port = 8083 }
  }
}

resource "aws_lb_target_group" "svc" {
  for_each = local.services

  name        = "tg-${each.key}"
  port        = each.value.port
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = aws_vpc.main.id

  health_check {
    path = "/health"
  }
}

resource "aws_lb_listener_rule" "svc" {
  for_each = local.services

  listener_arn = aws_lb_listener.https.arn
  priority     = 100 + index(keys(local.services), each.key)

  condition {
    path_pattern {
      values = [each.value.path]
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.svc[each.key].arn
  }
}

서비스를 추가할 때 services 맵에 한 줄만 더하면 된다.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 정규식 같은 복잡한 매칭을 ALB에 떠넘긴다

ALB의 path_pattern은 *, ? 정도만 지원한다.
복잡한 라우팅을 ALB에 넣으면 빠르게 한계가 온다.

복잡한 매칭은 애플리케이션 또는 API Gateway로

안티패턴 2. 같은 path를 여러 priority에 중복으로 둔다

priority 10 : /api/*    → TG-monolith
priority 20 : /api/v2/* → TG-v2

10이 먼저 잡아서 20은 영원히 실행되지 않는다.

더 구체적인 규칙을 작은 priority에 둔다

안티패턴 3. 서비스를 늘릴 때마다 ALB를 새로 만든다

ALB 자체가 비용이 든다.
한 ALB에 규칙 100개까지는 무리 없이 들어간다.

한 ALB로 모으되, 너무 많아지면 도메인 단위로 나눈다

안티패턴 4. 모든 서비스를 한 타깃 그룹에 묶고 path만 다르게 둔다

타깃 그룹은 “같은 역할의 서버 묶음” 이다.
역할이 다른 서비스를 같은 TG에 두면

  • 헬스 체크가 의미를 잃고
  • 스케일링이 묶이고
  • 장애가 서로 번진다

12. 한 줄로 정리

하나의 ALB에서 경로와 호스트로 트래픽을 나눠
각 마이크로서비스의 타깃 그룹으로 보내는 것이 MSA의 진입 구조다


13. 이 장의 핵심 정리

  1. 경로 기반과 호스트 기반은 ALB에서 서비스를 나누는 두 축이다.
  2. 두 방식은 함께 써도 된다.
  3. 서비스마다 타깃 그룹을 반드시 따로 둔다.
  4. priority는 가장 구체적인 규칙을 앞에 둔다.
  5. 한 ALB로 수십 개 서비스까지는 충분히 운영된다.
  6. 복잡한 라우팅은 ALB가 아니라 API Gateway / 애플리케이션 단계로 옮긴다.

36장. NLB는 언제 쓰는가

이 장에서 말하고자 하는 것

ALB가 대부분의 HTTP 서비스를 책임진다.

그런데 어떤 상황에서는 ALB로 부족하다.

  • HTTP가 아닌 프로토콜이 필요할 때
  • 초저지연이 중요할 때
  • 고정 IP가 필요할 때

이때 등장하는 것이

NLB (Network Load Balancer)

다.

ALB의 대체가 아니라 다른 자리에 쓰는 LB 다.


1. NLB가 다른 점

NLB는 L4(TCP/UDP)에서 동작한다.

  • HTTP 헤더를 보지 않는다
  • 경로 · 호스트 라우팅을 못 한다
  • 그 대신 처리 지연이 매우 짧다 (100µs 수준)
  • 초당 수백만 연결을 처리한다
  • AZ마다 고정 IP를 가질 수 있다

2. NLB가 어울리는 상황

상황 1. 비-HTTP 프로토콜

  • WebSocket 일부
  • MQTT, AMQP
  • 게임용 TCP/UDP 트래픽
  • 일부 데이터베이스 프록시

상황 2. 고정 IP가 필요한 환경

방화벽 화이트리스트나
파트너사가 IP를 등록해야 하는 환경에서는
ALB의 가변 IP가 곤란하다.

NLB는 AZ마다 Elastic IP를 붙일 수 있다.

NLB AZ-A IP : 3.36.55.10
NLB AZ-B IP : 3.36.55.11

이 두 IP를 파트너에게 주면 끝난다.

상황 3. 초저지연이 중요한 서비스

거래소 · 실시간 시세 · 광고 입찰처럼
밀리초가 의미 있는 환경에서는 NLB를 쓴다.

상황 4. TLS 패스스루가 필요한 경우

암호화된 트래픽을 LB에서 안 풀고
그대로 뒤로 넘기고 싶을 때 NLB를 쓴다.


3. NLB가 어울리지 않는 상황

  • 일반적인 HTTP / HTTPS API → ALB
  • 경로 기반 라우팅이 필요한 경우 → ALB
  • 인증서를 LB에서 종료해 내부는 HTTP로 받고 싶은 경우 → ALB

“L7 라우팅이 필요하면 ALB” 가 기준선이다


4. ALB 앞에 NLB를 두는 패턴

가끔 NLB를 ALB 앞에 두는 구조가 나온다.

사용자 → NLB(고정 IP) → ALB(L7 라우팅) → ECS

이유:

  • 외부에는 고정 IP를 노출하고
  • 내부 라우팅은 ALB의 L7 능력을 그대로 쓰고 싶을 때

특수 요건이 없으면 굳이 따라할 필요는 없다.


5. 우리 서비스에서

이 책에서 만드는 MSA의 기본 구조에는 NLB가 없다.

CloudFront → API Gateway → ALB → ECS → DB

이걸로 충분하다.

NLB는 “특수 요건이 생기면 이름을 떠올리는 도구” 로 알아두면 된다.


6. 직접 확인해보기 — CLI

# NLB 만들기
aws elbv2 create-load-balancer \
  --name my-nlb \
  --type network \
  --subnets subnet-xxx subnet-yyy

ALB와 같은 elbv2 API를 쓰지만 --type network 가 다르다.


7. 코드로는 이렇게 생겼다 — Terraform

resource "aws_eip" "nlb_a" { domain = "vpc" }
resource "aws_eip" "nlb_b" { domain = "vpc" }

resource "aws_lb" "nlb" {
  name               = "my-nlb"
  internal           = false
  load_balancer_type = "network"

  subnet_mapping {
    subnet_id     = aws_subnet.public_a.id
    allocation_id = aws_eip.nlb_a.id    # 고정 IP
  }

  subnet_mapping {
    subnet_id     = aws_subnet.public_b.id
    allocation_id = aws_eip.nlb_b.id
  }
}

subnet_mapping 으로 AZ마다 Elastic IP를 지정해
고정 IP를 만들 수 있다.


8. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. HTTP 서비스에 NLB를 기본으로 쓴다

경로 라우팅 · 호스트 라우팅 · 헤더 검사가 다 막힌다.
결국 애플리케이션에서 분기 코드를 짜게 된다.

HTTP라면 거의 항상 ALB

안티패턴 2. NLB에 보안 그룹이 없다고 가정한다

NLB에도 2023년부터 보안 그룹을 붙일 수 있다.
다만 ALB와 동작이 미묘하게 다르다.

NLB 뒷단 보안 그룹 규칙은 꼭 다시 점검한다

안티패턴 3. NLB로 TLS 패스스루를 한 뒤 뒷단에서 또 패스스루 한다

NLB(TLS 패스) → EC2(TLS 패스) → 어디서 종료할지 모름

TLS는 어딘가 한 곳에서 반드시 종료된다
패스스루의 끝을 명확히 설계해야 한다


9. 한 줄로 정리

NLB는 L4의 빠르고 단순한 LB이며,
HTTP가 아니거나 고정 IP가 필요할 때 등장한다


10. 이 장의 핵심 정리

  1. NLB는 L4에서 동작해 ALB와 다른 자리에 쓴다.
  2. HTTP / HTTPS API에는 거의 항상 ALB가 맞다.
  3. NLB는 고정 IP · 초저지연 · TLS 패스스루 · 비-HTTP 프로토콜이 필요할 때 고려한다.
  4. 우리 MSA 구조의 기본형에는 NLB가 없다.
  5. NLB는 도구 이름으로 기억해 두고, 요건이 생기면 그때 깊이 들여다본다.

37장. Auto Scaling Group과 스케일링 정책

이 장에서 말하고자 하는 것

이제 우리는 트래픽을 ALB가 받아
여러 서버로 분산하는 모양까지 그렸다.

그런데 다음 질문이 남는다.

서버를 미리 몇 대 띄워둘까?
트래픽이 늘면 누가 더 만들어줄까?

이 질문에 답하는 도구가

Auto Scaling Group (ASG)

이다.

ASG는 EC2 인스턴스를 자동으로 늘리고 줄인다.
컨테이너 환경의 ECS Service Auto Scaling도
같은 사고방식 위에 있다.


1. 왜 자동 스케일링이 필요한가

미리 큰 서버 한 대를 띄우면

평소: 5% 사용 → 자원 낭비
피크: 100%   → 더 큰 게 필요

미리 여러 대를 띄우면

평소에도 여러 대가 켜져 있음 → 비용

해결은 단순하다.

트래픽에 맞춰 자동으로 서버 수를 바꾼다

이게 클라우드의 핵심 강점 중 하나다.


2. ASG의 기본 구성

ASG는 세 가지 숫자로 운영된다.

최소 (Minimum)  : 항상 이만큼은 떠 있어야 한다
원하는 (Desired): 지금 이만큼 떠 있어야 한다
최대 (Maximum) : 이 이상으로는 안 늘어난다
Min 2  Desired 4  Max 10
  • 평소: 4대 운영
  • 트래픽 폭증: 최대 10대까지 자동 증가
  • 트래픽 줄면: 2대까지 자동 축소

3. ASG는 무엇을 보고 결정하는가

ASG는 CloudWatch 지표 를 본다.

  • CPU 사용률
  • 네트워크 입출력
  • ALB로 받는 요청 수
  • 커스텀 지표

이 지표가 임계를 넘으면 서버를 늘리거나 줄인다.


4. 스케일링 정책의 세 가지 방식

방식 1. Target Tracking — 가장 단순하고 권장

CPU 사용률 평균 50%를 유지해라

ASG가 알아서 늘리고 줄인다.
운영의 90%는 이걸로 해결된다.

방식 2. Step Scaling — 단계별로

CPU 70% 넘으면 +1
CPU 90% 넘으면 +3
CPU 30% 미만이면 -1

세밀한 제어가 필요할 때.

방식 3. Scheduled — 시간 기반

매일 오전 9시에 desired = 10
매일 오후 11시에 desired = 2

트래픽 패턴이 시간에 명확히 묶일 때 (예: 출근 시간).


5. 헬스 체크와의 협력

ASG는 ALB의 헬스 체크와 연동된다.

1. ASG가 새 EC2를 띄운다
2. ALB의 타깃 그룹에 자동 등록된다
3. 헬스 체크 통과 → 트래픽 받기 시작
4. 만약 실패 → ASG가 그 EC2를 죽이고 새로 띄운다

ASG는 단순히 수만 맞추는 게 아니라
죽은 인스턴스를 자동 교체하는 자가 치유 메커니즘이기도 하다


6. ECS에서의 자동 스케일링

ECS에서는 두 단계의 스케일링이 있다.

1. 서비스 안의 태스크 수 자동 조정 (Service Auto Scaling)
2. (EC2 기반일 때만) 클러스터의 EC2 인스턴스 수 자동 조정 (ASG)

Fargate를 쓰면 2번이 사라진다. 태스크 수만 조정하면 끝난다.

이 책의 척추 구조는 Fargate를 기본으로 가정하므로
ASG보다는 Service Auto Scaling을 더 자주 만지게 된다.

다만 사고방식은 같다.


7. 우리 서비스에서

척추 그림에서 자동 스케일링이 동작하는 위치다.

[ALB]
   ↓
[ECS Service: orders]   ← Service Auto Scaling
   ├─ task 1
   ├─ task 2
   └─ (트래픽 늘면 자동으로 task 3, 4 …)
  • CPU > 60% → 태스크 +1
  • 한 시간간 CPU < 30% → 태스크 -1
  • 최소 2개, 최대 10개

이 규칙 하나로

“낮에는 자동으로 커지고 밤에는 자동으로 줄어드는 서비스”

가 된다.


8. 직접 확인해보기 — CLI

ASG 보기

aws autoscaling describe-auto-scaling-groups

원하는 인스턴스 수 변경

aws autoscaling set-desired-capacity \
  --auto-scaling-group-name my-asg \
  --desired-capacity 5

ECS Service Auto Scaling 등록

aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/cluster-name/service-name \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 \
  --max-capacity 10

9. 코드로는 이렇게 생겼다 — Terraform

ECS Service Auto Scaling을 Target Tracking으로 거는 모양이다.

resource "aws_appautoscaling_target" "ecs" {
  service_namespace  = "ecs"
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.orders.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  min_capacity       = 2
  max_capacity       = 10
}

resource "aws_appautoscaling_policy" "ecs_cpu" {
  name               = "cpu-target-50"
  service_namespace  = "ecs"
  resource_id        = aws_appautoscaling_target.ecs.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
  policy_type        = "TargetTrackingScaling"

  target_tracking_scaling_policy_configuration {
    target_value = 50.0

    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
  }
}

이걸 띄우면

“CPU 평균 50% 부근을 유지해라”

라는 규칙이 적용된다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 최소를 1로 둔다

Min 1

한 대가 죽는 순간 서비스가 0대가 된다.
운영 서비스는 최소 2대로 시작한다.

안티패턴 2. 최대를 너무 크게 둔다

Max 100

비용 사고가 난다.
악성 트래픽 한 번에 수십 ~ 수백 인스턴스가 떠서 청구서가 폭발한다.

Max는 “정상 트래픽 + 여유분” 정도로 제한한다
그 위로는 사람이 의식적으로 늘려야 한다

안티패턴 3. 헬스 체크 실패를 무시한 채 스케일링한다

ASG가 자꾸 인스턴스를 새로 띄우는데도
헬스 체크가 계속 실패하면 새 인스턴스도 계속 죽는다.

“왜 ASG가 끝없이 인스턴스를 만들고 죽이나” 면
스케일링 문제가 아니라 헬스 체크 문제다

안티패턴 4. Sticky Session을 켜고 자동 스케일링도 켠다

신규 인스턴스가 들어와도 기존 사용자는 계속 옛 인스턴스로 간다.
새 인스턴스는 한가하고 옛 인스턴스만 죽도록 바쁘다.

자동 스케일링과 sticky는 본질적으로 안 어울린다
한쪽을 선택하면 다른 쪽을 양보한다


11. 한 줄로 정리

Auto Scaling은 트래픽에 맞춰 서버 수를 자동으로 조정하면서
죽은 인스턴스까지 자동 교체하는 자가 치유 메커니즘이다


12. 이 장의 핵심 정리

  1. ASG는 Min · Desired · Max 세 숫자로 인스턴스 수를 관리한다.
  2. Target Tracking이 가장 단순하고 권장되는 정책이다.
  3. ALB 헬스 체크와 ASG는 함께 동작해 자가 치유를 이룬다.
  4. ECS에서는 Service Auto Scaling이 비슷한 역할을 한다.
  5. Fargate에서는 클러스터 ASG 자체가 사라지고 태스크 수만 조정한다.
  6. Max는 비용 사고를 막기 위해 의식적으로 제한한다.

38장. CloudFront — 사용자에 가까운 곳에서 응답하기

이 장에서 말하고자 하는 것

지금까지 우리는 사용자가 도메인을 거쳐
ALB까지 도달하는 길을 만들었다.

그런데 한국에서 만든 서비스를 미국 사용자가 쓴다면

미국 사용자 → 서울 ALB → 응답

응답이 너무 느리다. 매번 태평양을 왕복한다.

이걸 해결하는 도구가

Content Delivery Network (CDN)

이고 AWS의 CDN이

CloudFront

다.

CloudFront는 단순히 빠르게 만들어주는 것 이상으로
보안과 비용에도 영향을 주는 핵심 인프라다.


1. CDN이 하는 일

CDN의 아이디어는 단순하다.

자주 쓰이는 응답을 사용자 가까이에 미리 가져다 둔다

서울 origin → 1번만 가져온다
       ↓
[도쿄 엣지] [LA 엣지] [런던 엣지] [상파울루 엣지]
       ↓               ↓
   일본 사용자        미국 사용자

같은 응답이라면 매번 origin까지 갈 필요가 없다.

엣지에 캐시된 응답이 직접 나간다.

이게 두 가지를 동시에 해결한다.

  • 사용자 응답이 빠르다 (가까운 곳에서 응답)
  • origin 부하가 줄어든다 (요청이 흡수됨)

2. CloudFront의 엣지 네트워크

AWS는 전 세계에 두 단계의 캐시 계층을 둔다.

[엣지 로케이션 (가장 가까운 곳)]
         ↑
[리전 엣지 캐시 (좀 더 큰 캐시)]
         ↑
[Origin (서울 ALB 등)]
  • 엣지 로케이션 — 사용자에 가장 가까움. 수백 곳
  • 리전 엣지 캐시 — 엣지 로케이션의 백업 캐시 역할

엣지에서 캐시 미스가 나면
바로 origin까지 가는 게 아니라
리전 엣지 캐시에서 한 번 더 확인한다.

같은 응답에 대해 origin 부담을 최소화하는 구조다


3. 요청은 어떻게 처리되는가

사용자가 https://example.com/image.png 를 요청하면

1. DNS → CloudFront의 가까운 엣지 응답
2. 엣지에서 image.png 가 캐시에 있나?
   - 있다 → 즉시 응답 (Cache Hit)
   - 없다 → 리전 엣지 캐시 확인
       - 있다 → 가져와 응답
       - 없다 → origin 까지 가서 가져오고 캐시
3. 다음 요청은 캐시에서 바로 응답

캐시 적중률(Cache Hit Ratio)이 높을수록
origin이 한가해지고 사용자가 빨라진다.


4. Origin — 어디에서 가져오는가

CloudFront 뒤에 둘 수 있는 origin은 여러 가지다.

  • S3 — 정적 콘텐츠 (이미지, JS, CSS)
  • ALB — 동적 서비스
  • API Gateway — API 서비스
  • Custom HTTP — 외부 서버 등

한 CloudFront 배포 안에 여러 origin을 두고
경로별로 다른 origin으로 보낼 수 있다.

/static/*  → S3
/api/*     → ALB
/*         → S3 (SPA 호스팅)

이 부분은 다음 39장에서 자세히 다룬다.


5. 무엇을 캐시하는가

기본적으로 CloudFront는

  • GET / HEAD 응답을 캐시한다
  • 응답의 Cache-Control, Expires 헤더와
    CloudFront 자체의 TTL 설정을 함께 본다

POST · PUT · DELETE 같은 변경 요청은 캐시되지 않는다.

정적 자원 → 길게 캐시
동적 API → 짧거나 캐시 안 함


6. CloudFront가 보안에도 영향을 주는 이유

CloudFront를 앞에 두면 두 가지 보안 이점이 생긴다.

1. ALB · S3 를 노출하지 않을 수 있다

사용자 → CloudFront → (비공개) ALB / S3

S3에는 OAC, ALB에는 보안 그룹 + 헤더 검증으로
CloudFront만 통과시키는 구조를 만들 수 있다.

2. AWS Shield 표준 보호가 자동 적용된다

DDoS 공격이 origin까지 닿지 않고
CloudFront 단계에서 흡수된다.

WAF를 붙이면 한 단계 더 막을 수 있다. (40장에서 다룬다)


7. 우리 서비스에서

척추 그림에서 CloudFront 위치다.

[사용자]
   ↓ HTTPS (us-east-1 ACM)
[CloudFront]   ← 여기
   ├─ /static/*  → S3
   └─ /*         → ALB → ECS
  • 정적 자원은 S3에서 직접 받아 캐시
  • 동적 API는 ALB로 패스 (짧은 TTL 또는 캐시 비활성)
  • HTTPS는 여기서 한 번 풀린다
  • AWS Shield 기본 보호

8. 직접 확인해보기 — CLI

배포 목록 보기

aws cloudfront list-distributions

캐시 무효화 (Invalidation)

aws cloudfront create-invalidation \
  --distribution-id E1234ABCD \
  --paths "/static/*"

배포 후에 캐시를 강제로 비울 때 가장 자주 쓴다.

응답에서 캐시 적중 여부 확인

CloudFront는 응답 헤더에 다음을 넣어준다.

x-cache: Hit from cloudfront
x-cache: Miss from cloudfront

curl -I 로 헤더를 보면 캐시 동작을 바로 알 수 있다.


9. 코드로는 이렇게 생겼다 — Terraform

ALB를 origin으로 한 가장 작은 배포다.

resource "aws_cloudfront_distribution" "main" {
  enabled         = true
  is_ipv6_enabled = true
  aliases         = ["api.example.com"]

  origin {
    domain_name = aws_lb.main.dns_name
    origin_id   = "alb-origin"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  default_cache_behavior {
    target_origin_id       = "alb-origin"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
    cached_methods         = ["GET", "HEAD"]

    cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"   # CachingDisabled (동적 API용)
  }

  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate.us_east.arn
    ssl_support_method  = "sni-only"
  }

  restrictions {
    geo_restriction { restriction_type = "none" }
  }
}

viewer_certificate.acm_certificate_arn
반드시 us-east-1 에서 발급한 인증서를 가리켜야 한다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 동적 API 응답을 길게 캐시한다

/api/me  → 24시간 캐시

다른 사용자가 내 정보로 응답을 받을 수 있다.

인증된 응답은 캐시하지 않거나 Cache Key에 사용자 단위 필드를 넣는다

안티패턴 2. 캐시 무효화에 의존한다

매 배포마다 /* invalidation을 거는 방식.

  • invalidation은 비용이 든다 (월 무료 한도 초과 시)
  • 즉시 반영되지 않는다 (수 분 ~ 수십 분)

파일명에 해시(app.a3f2c.js)를 박아 새 파일로 배포한다
그러면 invalidation 없이 신버전이 즉시 적용된다

안티패턴 3. CloudFront 없이 S3를 정적 호스팅으로 공개한다

S3 static website hosting → 그대로 노출
  • HTTPS가 약하다
  • DDoS 보호가 없다
  • 캐시가 없어 속도가 느리다

정적 콘텐츠는 S3 + CloudFront 조합이 표준

안티패턴 4. Cache Key에 쓸데없는 헤더를 다 넣는다

User-Agent, Accept-Language, Cookie 같은 걸
Cache Key에 다 넣으면 캐시가 사실상 동작하지 않는다.

Cache Key는 응답을 정말로 바꾸는 헤더만 포함한다


11. 한 줄로 정리

CloudFront는 응답을 사용자 가까이에 캐시해
속도 · 비용 · 보안을 동시에 개선하는 엣지 계층이다


12. 이 장의 핵심 정리

  1. CloudFront는 AWS의 CDN으로 엣지 로케이션에 응답을 캐시한다.
  2. 엣지 → 리전 엣지 캐시 → origin 의 2단 캐시 구조를 가진다.
  3. origin에 S3 · ALB · API Gateway 등을 둘 수 있고, 경로별로 다르게 보낼 수 있다.
  4. 캐시 키 · TTL · Cache-Control 설정이 캐시 적중률을 결정한다.
  5. 인증된 동적 응답은 캐시하지 않도록 정책을 분리한다.
  6. CloudFront에 붙일 인증서는 us-east-1에서 발급한다.

39장. CloudFront의 Origin · 캐시 · OAC

이 장에서 말하고자 하는 것

앞 장에서 우리는 CloudFront가 무엇이고
왜 쓰는지를 봤다.

이 장은 CloudFront를 실제로 운영할 때 가장 자주 다루는 세 가지를 본다.

  • Origin — 어디서 데이터를 가져올 것인가
  • Cache Behavior — 어떤 경로를 어떻게 다룰 것인가
  • OAC (Origin Access Control) — S3 같은 origin을 어떻게 보호할 것인가

1. 한 배포 안에서 여러 Origin 다루기

CloudFront 배포는 origin을 여러 개 가질 수 있다.

배포 example.com
 ├─ origin "s3-static"   → S3 (정적 파일)
 ├─ origin "api-alb"     → ALB (동적 API)
 └─ origin "img-bucket"  → S3 (이미지)

각 origin은 단순한 “어디서 가져올 것인가” 의 선언이다.

어떤 경로를 어떤 origin으로 보낼지는
다음에 나오는 Cache Behavior가 결정한다.


2. Cache Behavior — 경로별 동작

Cache Behavior는

어떤 path가 어떤 origin으로 가고
어떻게 캐시할지

를 결정한다.

/static/*  →  s3-static     캐시: 길게
/img/*     →  img-bucket    캐시: 길게
/api/*     →  api-alb       캐시: 없음
default    →  s3-static     캐시: 중간

규칙은 ALB처럼 우선순위 순으로 평가되며
가장 먼저 매칭된 behavior가 적용된다.


3. Cache Key — 무엇이 다르면 다른 캐시인가

CloudFront는 같은 응답이라도

“어떤 요청이 같은 요청인가”

를 정해야 캐시할 수 있다.

이 기준이 Cache Key 다.

기본적으로 다음이 키에 들어간다.

  • URL 경로
  • 쿼리 스트링 (선택적으로)
  • 헤더 (선택적으로)
  • 쿠키 (선택적으로)
GET /products?id=1  → 캐시 키 A
GET /products?id=2  → 캐시 키 B (다른 응답으로 분리)

키에 들어가는 항목이 많을수록
캐시가 잘게 쪼개져 적중률이 떨어진다.

응답을 진짜로 바꾸는 항목만 키에 포함한다


4. TTL — 얼마나 캐시할 것인가

TTL은 세 가지가 함께 작용한다.

Min TTL    : 최소 캐시 시간
Default TTL: 응답에 Cache-Control 이 없을 때
Max TTL    : Cache-Control 이 있어도 이 이상 안 캐시

origin이 응답에 다음을 넣으면 그게 우선이다.

Cache-Control: public, max-age=3600

이러면 1시간 캐시.

운영에서는 origin의 응답 헤더로 TTL을 통제하는 게 깔끔하다


5. 캐시 무효화 (Invalidation)

배포 후 즉시 새 파일을 보여주고 싶을 때 쓴다.

aws cloudfront create-invalidation \
  --distribution-id E123ABCD \
  --paths "/index.html" "/static/*"

다만 운영에서는 invalidation에 의존하는 대신

app.js  → app.a3f2c.js
style.css → style.b91e8.css

처럼 파일명에 해시를 박는 방식이 표준이다.

새 배포는 새 파일이라 캐시가 무관해진다


6. OAC — S3를 안전하게 노출하기

S3를 정적 파일 origin으로 쓸 때 가장 흔한 실수는

S3 버킷을 public 으로 노출

이다.

이러면 CloudFront 없이도 S3에 직접 접근할 수 있다.

대신 쓰는 방식이

OAC (Origin Access Control)

이다.

S3는 비공개 (Public Access Block ON)
오직 "이 CloudFront 배포" 만 읽을 수 있게 권한 부여

이렇게 하면

사용자 → CloudFront → S3   (OK)
사용자 → S3 직접 접근       (차단)

운영에서는 사실상 OAC가 표준이다.

과거 OAI(Origin Access Identity)는 OAC로 대체됐다
신규 구성은 무조건 OAC


7. 동적 origin도 보호하기

ALB에 직접 누가 접근하는 걸 막고 싶다면 두 가지를 함께 쓴다.

  1. CloudFront가 origin에 보낼 때 비밀 헤더를 추가
    x-origin-secret: <긴 무작위 문자열>
    
  2. ALB는 그 헤더가 없는 요청은 403

이러면

사용자 → CloudFront → ALB (헤더 있음, OK)
사용자 → ALB 직접 접근 (헤더 없음, 차단)

CloudFront-ALB 사이도 보안을 줄 수 있는 구조


8. 우리 서비스에서

척추 그림의 CloudFront 안쪽이다.

[CloudFront]
 ├─ /static/*  →  s3-static  (OAC, 24h 캐시)
 ├─ /img/*     →  s3-img     (OAC, 7d 캐시)
 ├─ /api/*     →  alb        (캐시 없음, 시크릿 헤더)
 └─ /*         →  s3-static  (SPA index.html)

이 단순한 구조 하나로

  • 정적 자원은 CDN에서 흡수
  • 동적 API는 그대로 패스
  • 백엔드는 모두 비공개
  • 보안과 속도가 함께 잡힌다

9. 직접 확인해보기 — CLI

배포 설정 보기

aws cloudfront get-distribution \
  --id E123ABCD

Cache Hit Ratio 확인

CloudFront 콘솔의 Reports 또는 CloudWatch 지표
CacheHitRate 로 확인한다.

aws cloudwatch get-metric-statistics \
  --namespace AWS/CloudFront \
  --metric-name CacheHitRate \
  --dimensions Name=DistributionId,Value=E123ABCD Name=Region,Value=Global \
  --start-time 2026-01-01T00:00:00Z \
  --end-time   2026-01-02T00:00:00Z \
  --period 3600 \
  --statistics Average

운영에서 가장 자주 보는 지표 중 하나다.


10. 코드로는 이렇게 생겼다 — Terraform

S3 + OAC + 두 개의 behavior가 있는 배포다.

resource "aws_cloudfront_origin_access_control" "s3" {
  name                              = "s3-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "main" {
  enabled = true
  aliases = ["example.com"]

  origin {
    domain_name              = aws_s3_bucket.static.bucket_regional_domain_name
    origin_id                = "s3-static"
    origin_access_control_id = aws_cloudfront_origin_access_control.s3.id
  }

  origin {
    domain_name = aws_lb.main.dns_name
    origin_id   = "alb"
    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  default_cache_behavior {
    target_origin_id       = "s3-static"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    cache_policy_id        = "658327ea-f89d-4fab-a63d-7e88639e58f6"  # CachingOptimized
  }

  ordered_cache_behavior {
    path_pattern           = "/api/*"
    target_origin_id       = "alb"
    viewer_protocol_policy = "https-only"
    allowed_methods        = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
    cached_methods         = ["GET", "HEAD"]
    cache_policy_id        = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"  # CachingDisabled
  }

  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate.us_east.arn
    ssl_support_method  = "sni-only"
  }

  restrictions {
    geo_restriction { restriction_type = "none" }
  }
}

/api/* 만 캐시 비활성 정책을 쓰고
나머지는 기본 캐시 정책을 쓰는 모양이다.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. S3 버킷을 public 으로 두고 CloudFront만 앞에 둔다

S3 버킷은 비공개로 두고 OAC로 권한을 준다.

public 버킷은 절대 운영에 두지 않는다

안티패턴 2. 모든 요청을 같은 Cache Behavior로 처리한다

정적과 동적이 같은 캐시 정책을 쓰면
한쪽이 망가진다.

/api/* 와 /static/* 은 거의 항상 다른 behavior가 필요하다

안티패턴 3. Cache Key에 Authorization 헤더를 넣지 않고 인증 API를 캐시한다

GET /api/me → 다른 사용자의 응답이 캐시된다
  • 인증 API는 캐시하지 않거나
  • Authorization 헤더를 Cache Key에 포함

안티패턴 4. ALB가 인터넷에 그대로 노출돼 있다

CloudFront를 앞에 두고도 ALB가 누구나 접근 가능하면
CDN을 우회한 공격이 가능하다.

CloudFront 시크릿 헤더 + ALB 규칙으로 우회를 차단한다


12. 한 줄로 정리

Origin은 어디서 가져올지, Cache Behavior는 어떻게 다룰지,
OAC는 누구만 가져갈 수 있는지를 정한다


13. 이 장의 핵심 정리

  1. CloudFront 배포는 여러 origin을 가질 수 있고 경로별로 다른 origin으로 보낼 수 있다.
  2. Cache Behavior가 어떤 경로를 어떻게 캐시할지 결정한다.
  3. Cache Key는 응답을 진짜로 바꾸는 항목만 포함해야 적중률이 높다.
  4. 배포 자원은 invalidation 대신 파일명 해싱으로 갱신하는 게 표준이다.
  5. S3 origin은 비공개 + OAC 조합이 기본이다.
  6. ALB origin은 시크릿 헤더로 CloudFront만 통과시키도록 보호할 수 있다.

40장. WAF — 엣지에서 걸러내는 보안

이 장에서 말하고자 하는 것

CloudFront까지 깔면 사용자가 들어오는 길은 정리된다.

그런데 그 길에는 다음이 섞여 들어온다.

  • SQL 인젝션 시도
  • XSS 시도
  • 크롤러
  • 무차별 로그인 시도

이걸 애플리케이션 직전에서 미리 걸러내는 도구가

AWS WAF (Web Application Firewall)

다.

CloudFront · ALB · API Gateway 같은 엣지/입구 단계에
WAF를 끼워 둔다.


1. WAF가 막는 것

WAF는 HTTP 요청을 검사해 다음을 막는다.

  • OWASP Top 10에 가까운 공격 (SQLi, XSS, 경로 탐색 등)
  • 알려진 악성 IP/봇
  • 비정상적으로 잦은 요청 (Brute force)
  • 지정한 국가/IP 차단
  • 사용자 정의 패턴

2. WAF가 못 막는 것

WAF가 만능은 아니다.

  • 비즈니스 로직 취약점 (권한 우회, 잘못된 상태 전이)
  • 정상으로 보이는 정교한 봇
  • 인증 후 발생하는 내부 권한 문제

WAF는 첫 번째 그물이지 마지막 방어선이 아니다
애플리케이션 단계의 검증을 대체하지 않는다


3. WAF의 구성 요소

Web ACL

WAF의 최상위 단위.
하나의 보호 묶음이다.

Web ACL
 ├─ Rule 1
 ├─ Rule 2
 └─ Default action (allow / block)

Rule

요청이 어떤 패턴이면 어떻게 할지의 정의.

  • 매칭 조건: 헤더 / 쿼리 / 바디 / IP 등
  • 동작: ALLOW / BLOCK / COUNT / CAPTCHA

Rule Group

여러 룰을 묶은 단위.
직접 만들 수도 있고 AWS가 만들어 제공하는 것도 있다.


4. AWS Managed Rules — 가장 먼저 켜야 할 것

AWS가 제공하는 룰 묶음이다.

룰 그룹내용
Core rule setOWASP 기반 일반 보호
Known bad inputs알려진 악성 입력
SQL databaseSQL 인젝션
Linux / Unix명령 주입
Bot Control봇 탐지
Anonymous IP listVPN / Tor / 호스팅 IP

WAF를 처음 켤 때는 우선 Core + Known bad inputs + SQL 부터


5. Rate-based Rule — 폭증 차단

같은 IP에서 짧은 시간에 너무 많이 들어오는 요청을 막는다.

5분 동안 같은 IP가 2,000회 초과 → BLOCK

로그인 페이지, 비밀번호 재설정, 인증 코드 검증처럼
악용되기 쉬운 엔드포인트에 따로 더 엄격한 임계를 건다.


6. Count 모드 — 일단 보기만 하기

새 룰을 켤 때 곧장 BLOCK 으로 두면
정상 트래픽까지 막아 사고가 난다.

WAF는 룰을 COUNT 모드로 둘 수 있다.

COUNT → 매칭은 잡지만 차단은 안 함

로그를 며칠 보고 오탐이 없는 걸 확인한 뒤
BLOCK으로 전환하는 게 운영의 표준 흐름이다.


7. WAF는 어디에 다는가

WAF는 다음 리소스에 붙는다.

  • CloudFront
  • ALB
  • API Gateway
  • Cognito User Pool
  • AppSync
사용자 → [WAF + CloudFront] → ALB → ECS

CloudFront에 다는 게 가장 흔하다.
공격 트래픽이 origin까지 도달하기 전에 막는다.

가장 바깥에 한 번, 필요하면 ALB에도 추가로


8. 우리 서비스에서

척추 그림에서 WAF 위치다.

[사용자]
   ↓
[WAF + CloudFront]   ← 여기, 첫 그물
   ├─ /api/* → ALB
   │           ↓
   │       [(선택) WAF + ALB]   ← 두 번째 그물
   │           ↓
   │       [ECS]
   └─ /static/* → S3
  • 모든 트래픽은 CloudFront WAF 통과
  • /api/* 만 별도 ALB WAF로 한 번 더 (필요 시)
  • CloudFront WAF는 us-east-1 글로벌 범위

9. 직접 확인해보기 — CLI

Web ACL 만들기

aws wafv2 create-web-acl \
  --name my-acl \
  --scope CLOUDFRONT \
  --default-action Allow={} \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=my-acl \
  --region us-east-1

--scope 가 중요하다.

CLOUDFRONT → CloudFront에 붙일 WAF (반드시 us-east-1)
REGIONAL   → ALB · APIGW 등에 붙일 WAF (해당 리전)

Web ACL을 리소스에 붙이기

aws wafv2 associate-web-acl \
  --web-acl-arn <acl-arn> \
  --resource-arn <alb-arn>

차단된 요청 로그

WAF 로그를 CloudWatch Logs · S3 · Firehose 로 보낼 수 있다.

aws wafv2 put-logging-configuration ...

운영에서는 로그 없이 WAF를 켜지 않는다.
오탐이 났을 때 추적이 안 된다.


10. 코드로는 이렇게 생겼다 — Terraform

CloudFront에 붙일 WAF 한 벌의 모양이다.

resource "aws_wafv2_web_acl" "main" {
  provider = aws.us_east_1   # CloudFront WAF는 us-east-1

  name  = "main-acl"
  scope = "CLOUDFRONT"

  default_action {
    allow {}
  }

  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 1

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        vendor_name = "AWS"
        name        = "AWSManagedRulesCommonRuleSet"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "common"
      sampled_requests_enabled   = true
    }
  }

  rule {
    name     = "RateLimit"
    priority = 10

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 2000
        aggregate_key_type = "IP"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "rate-limit"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "main-acl"
    sampled_requests_enabled   = true
  }
}

이 정도가 처음 켤 때의 적정 출발선이다.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 룰을 추가하자마자 BLOCK 으로 둔다

정상 트래픽이 같이 잡혀 장애가 난다.

새 룰은 COUNT 로 며칠 → 로그 확인 → BLOCK 전환

안티패턴 2. WAF 로그를 안 켠다

차단됐는데 왜 차단됐는지 추적이 안 된다.

WAF는 로그 없이 운영하지 않는다

안티패턴 3. WAF에만 의존하고 애플리케이션 검증을 안 한다

WAF는 그물이지 잠금장치가 아니다.

입력 검증 · 인증 · 권한 검사 모두 애플리케이션에 있어야 한다

안티패턴 4. CloudFront WAF를 서울 리전에서 만들려고 한다

scope = CLOUDFRONT 인데 region = ap-northeast-2 → 실패

CloudFront WAF는 반드시 us-east-1


12. 한 줄로 정리

WAF는 엣지에서 악성 트래픽을 거르는 첫 번째 그물이며,
애플리케이션 검증을 대체하지 않는 추가 계층이다


13. 이 장의 핵심 정리

  1. WAF는 HTTP 레벨 공격을 엣지에서 거르는 보안 계층이다.
  2. Web ACL · Rule · Rule Group의 3단 구조다.
  3. AWS Managed Rules로 시작해 환경에 맞게 보강한다.
  4. Rate-based Rule로 폭증 트래픽과 무차별 시도를 막는다.
  5. 새 룰은 COUNT 모드로 검증 후 BLOCK으로 옮긴다.
  6. WAF는 첫 그물이지 마지막 방어선이 아니다.
  7. CloudFront WAF는 us-east-1, ALB / APIGW WAF는 해당 리전.

41장. 왜 컨테이너인가 — VM과의 차이

이 장에서 말하고자 하는 것

지금까지 우리는 EC2 위에 애플리케이션을 띄우는 그림을 그렸다.

그런데 운영해보면 EC2만으로 부족한 지점이 보인다.

  • 새 서버에 환경을 똑같이 깔기 어렵다
  • 개발과 운영 환경이 미묘하게 다르다
  • 배포가 느리다
  • 한 서버에 여러 서비스를 띄우면 충돌이 잘 난다

이걸 한 번에 풀어주는 게

컨테이너 (Container)

이고 표준 도구가

Docker

다.


1. VM과 컨테이너의 차이

VM

[하드웨어] → [Hypervisor] → [Guest OS] → [App]

OS 자체를 통째로 만든다. 무겁고 부팅이 느리다.

컨테이너

[하드웨어] → [Host OS] → [Container = 프로세스 격리] → [App]

OS는 공유하고 프로세스만 격리한다.
수십 MB, 초 단위 부팅.


2. 컨테이너가 풀어주는 4가지

환경 재현성

"내 컴퓨터에서는 잘 됐는데..."

이미지에 OS · 런타임 · 라이브러리 · 코드를 다 담는다.
어디서든 동일하게 실행된다.

빠른 배포

docker run my-app:v2

EC2 새로 띄우는 것보다 압도적으로 빠르다.

격리

한 머신에 여러 컨테이너를 띄워도 런타임 충돌이 없다.

표준 인터페이스

빌드 → 이미지 → 실행이 표준이라
ECS · EKS · K8s 어디서든 같은 이미지가 돈다.


3. 우리 서비스에서

척추 그림이 이 단원에서 바뀐다.

이전:
  ALB → EC2 (애플리케이션 직접 설치)

지금부터:
  ALB → ECS (Fargate) → 컨테이너 안의 애플리케이션

같은 트래픽 분배 구조에서 “서버” 가 “컨테이너” 로 바뀐 것이다.


4. 직접 확인해보기 — CLI

docker run -p 8080:8080 nginx

8080 포트로 nginx가 즉시 뜬다. 이게 컨테이너의 첫 모습이다.


5. 코드로는 이렇게 생겼다 — Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]

이 파일이 곧 컨테이너 이미지의 설계도다. 다음 장에서 자세히 다룬다.


6. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 컨테이너 안에 데이터를 영구 저장한다

컨테이너 죽으면 데이터도 사라진다.

영구 데이터는 RDS · S3 · EFS 같은 외부 저장소로

안티패턴 2. 한 컨테이너에 여러 프로세스를 띄운다

  • 디버깅 어렵다
  • 한쪽이 죽어도 컨테이너는 살아 있는 척한다
  • 스케일링이 어색하다

한 컨테이너 = 한 프로세스

안티패턴 3. 이미지를 latest로 받는다

배포마다 다른 이미지가 떨어진다. 롤백 불가.

항상 명시적 버전 태그 (커밋 해시 · 시맨틱 버전)

안티패턴 4. 운영체제를 통째로 베이스로 잡는다

FROM ubuntu:22.04 (70MB+) 대신
FROM node:20-alpine (50MB) 또는 distroless 베이스를 쓴다.


7. 한 줄로 정리

컨테이너는 환경을 패키징해 어디서든 같게 돌리는 표준이며,
MSA가 자연스럽게 위에 올라타는 토대다


8. 이 장의 핵심 정리

  1. VM은 OS 통째, 컨테이너는 프로세스 격리.
  2. 컨테이너는 환경 재현성, 빠른 배포, 격리, 표준 인터페이스를 동시에 해결한다.
  3. 컨테이너는 일회용으로 다뤄야 한다. 영구 데이터는 외부 저장소에.
  4. 한 컨테이너에 한 프로세스만 띄운다.
  5. 이미지 태그는 항상 명시적으로, latest 의존을 피한다.

42장. 도커 기초 — 이미지 · 컨테이너 · 레이어

이 장에서 말하고자 하는 것

앞 장에서 우리는 컨테이너가 무엇이고 왜 쓰는지를 봤다.

이제 그 컨테이너를 실제로 만드는 도구 Docker 의 핵심 세 개념을 잡는다.

  • 이미지(Image)
  • 컨테이너(Container)
  • 레이어(Layer)

1. 이미지와 컨테이너 — 다른 것이다

이미지   : 실행되지 않은 설계도 (붕어빵 틀)
컨테이너 : 그 설계도로 실행된 인스턴스 (붕어빵)
my-app:v1   ← 이미지 1개
  ↓ docker run
컨테이너 1, 2, 3 ...   ← 같은 이미지로 컨테이너 여러 개

이미지는 변하지 않는다. 컨테이너는 살아 있는 프로세스다.


2. Dockerfile — 이미지의 설계도

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]

각 줄이 이미지에 한 단계씩 쌓아 올라간다.


3. 레이어 — 이미지의 속살

이미지는 단일 파일이 아니라 여러 레이어 의 누적이다.

[Layer A: 베이스 OS]
[Layer B: 의존성]
[Layer C: 코드]

Dockerfile의 각 명령이 한 레이어를 만든다.

이 구조 덕분에

같은 베이스 + 같은 의존성이면 캐시를 재사용한다

코드만 바꾸면 그 레이어만 다시 만들어진다.


4. 빌드 캐시를 살리는 순서

# 좋은 순서
COPY package.json package-lock.json ./
RUN npm ci
COPY . .            ← 코드 변경 시에도 npm ci 캐시 살아남
# 나쁜 순서
COPY . .
RUN npm ci          ← 코드가 조금만 바뀌어도 의존성 다시 설치

자주 안 바뀌는 명령을 위에, 자주 바뀌는 명령을 아래에


5. Multi-stage Build — 가벼운 이미지

# 빌드 단계
FROM node:20-alpine AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build

# 실행 단계
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

빌드 도구는 빌드 단계에만, 최종 이미지에는 실행에 필요한 것만.

이미지가 작아지면 push · pull · 배포가 다 빨라진다


6. 우리 서비스에서 컨테이너의 표준 모양

  • alpine 또는 distroless 베이스
  • multi-stage build
  • 빌드 캐시를 살리는 COPY 순서
  • CMD 하나 (한 프로세스)
  • 비루트 사용자
  • /health 엔드포인트 또는 HEALTHCHECK

7. 직접 확인해보기 — CLI

docker build -t orders:v1 .
docker run -p 8080:8080 orders:v1
docker ps
docker logs <container-id>
docker exec -it <container-id> sh
docker history orders:v1

docker history 는 이미지가 어떻게 만들어졌는지 한눈에 본다.


8. 코드로는 이렇게 생겼다 — 운영용 Dockerfile

FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app

RUN addgroup -S app && adduser -S app -G app
USER app

COPY --from=build --chown=app:app /app/dist ./dist
COPY --from=build --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/package.json ./

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:8080/health || exit 1
CMD ["node", "dist/server.js"]

9. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 모든 걸 한 줄에 우겨넣는다

RUN apt-get update && apt-get install -y nodejs && git clone ... && npm ci && npm run build

레이어 캐시를 활용 못 한다.

안티패턴 2. 루트로 실행한다

기본 도커는 root로 실행. 보안 사고가 호스트까지 영향 줄 수 있다.

USER app 으로 비루트 전환

안티패턴 3. 비밀 키 · 토큰을 이미지에 박는다

ENV DB_PASSWORD=mypassword123

이미지에 남으면 어디서든 읽을 수 있다.

비밀은 Secrets Manager · Parameter Store · 환경 변수로 주입

안티패턴 4. OS 도구를 가득 깔아둔다

vim, curl, ssh 가 다 들어가면 공격 표면이 넓어진다.

실행에 필요한 것만 남기고 distroless 고려


10. 한 줄로 정리

Dockerfile은 이미지의 설계도이며,
레이어 캐시와 multi-stage 빌드를 살리는 게 곧 운영 품질이다


11. 이 장의 핵심 정리

  1. 이미지는 설계도, 컨테이너는 실행 인스턴스다.
  2. 이미지는 레이어의 누적이며, COPY/RUN 순서가 캐시를 결정한다.
  3. Multi-stage build로 빌드 도구를 최종 이미지에서 분리한다.
  4. 비루트 사용자, 작은 베이스, 비밀 제거가 보안의 기본이다.
  5. /health 엔드포인트와 HEALTHCHECK는 모든 운영 이미지에 있어야 한다.

43장. ECR — 컨테이너 이미지 저장소

이 장에서 말하고자 하는 것

이미지를 빌드했다면 다음 문제가 생긴다.

“이 이미지를 ECS / EKS / EC2 가 어떻게 받아가지?”

이때 등장하는 게

Amazon ECR (Elastic Container Registry)

다.

ECR은 AWS가 운영하는 컨테이너 이미지 저장소다.


1. ECR이 하는 일

  • 이미지를 안전하게 저장 (프라이빗 기본)
  • IAM으로 접근 통제
  • 취약점 자동 스캔
  • ECS · EKS · Lambda 와 매끄럽게 연동

Docker Hub와 비슷한 자리지만
AWS 내부에 두니 권한 · 속도 · 비용이 좋다.


2. 리포지토리 = 한 종류의 이미지

orders 리포지토리
  ├─ orders:v1
  ├─ orders:v2
  └─ orders:sha-a3f2c1

서비스마다 리포지토리 하나가 일반적이다.


3. 이미지 태그 — 운영 규칙

좋은 태그

  • 시맨틱 버전: v1.2.3
  • 커밋 해시: sha-a3f2c1
  • 빌드 번호: build-145

피해야 할 태그

  • latest — 내용이 자꾸 바뀐다
  • prod — 어디서 만들어졌는지 모른다

태그는 불변(immutable) 으로 — 한 번 푸시한 태그는 덮어쓰지 않는다

ECR은 imageTagMutability = IMMUTABLE 로 설정할 수 있다.


4. 인증과 푸시

aws ecr get-login-password --region ap-northeast-2 \
  | docker login --username AWS \
                 --password-stdin <acct>.dkr.ecr.ap-northeast-2.amazonaws.com

docker tag orders:v1 <acct>.dkr.ecr.ap-northeast-2.amazonaws.com/orders:v1
docker push <acct>.dkr.ecr.ap-northeast-2.amazonaws.com/orders:v1

ECS가 그 이미지를 받을 때는
Task Execution Role 에 ECR pull 권한이 필요하다.


5. 이미지 스캔

  • Basic Scan — 알려진 CVE 매칭 (무료)
  • Enhanced Scan — Inspector 기반 깊은 스캔 (유료)
스캔 결과: CRITICAL 1, HIGH 3, MEDIUM 12

CRITICAL 등급 이상은 알람 또는 배포 차단으로 연결한다.

운영 이미지는 무조건 스캔 결과를 본다


6. 수명 주기 정책

오래된 이미지를 자동 정리한다.

규칙 1: 태그 없는 이미지 7일 후 삭제
규칙 2: 최근 30개만 유지

ECR 비용의 절반은 “안 쓰는 옛 이미지” 다.


7. 우리 서비스에서

[개발자 또는 CI]
   ↓ docker push
[ECR: orders / users / payments]
   ↑ docker pull (Task Execution Role)
[ECS Service Task]
  • 서비스마다 리포지토리 하나
  • 태그는 커밋 해시 (불변)
  • CRITICAL 취약점은 배포 전 차단
  • 수명 주기로 자동 정리

8. 직접 확인해보기 — CLI

aws ecr create-repository \
  --repository-name orders \
  --image-scanning-configuration scanOnPush=true \
  --image-tag-mutability IMMUTABLE

aws ecr list-images --repository-name orders

aws ecr describe-image-scan-findings \
  --repository-name orders \
  --image-id imageTag=v1

9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_ecr_repository" "orders" {
  name                 = "orders"
  image_tag_mutability = "IMMUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }

  encryption_configuration {
    encryption_type = "AES256"
  }
}

resource "aws_ecr_lifecycle_policy" "orders" {
  repository = aws_ecr_repository.orders.name

  policy = jsonencode({
    rules = [
      {
        rulePriority = 1
        description  = "untagged 7일 후 삭제"
        selection = {
          tagStatus   = "untagged"
          countType   = "sinceImagePushed"
          countUnit   = "days"
          countNumber = 7
        }
        action = { type = "expire" }
      },
      {
        rulePriority = 2
        description  = "최근 30개만 유지"
        selection = {
          tagStatus   = "any"
          countType   = "imageCountMoreThan"
          countNumber = 30
        }
        action = { type = "expire" }
      }
    ]
  })
}

10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. latest 태그로 운영한다

롤백 불가능.

안티패턴 2. 수명 주기를 안 건다

청구서가 매달 늘어난다.

안티패턴 3. 스캔 결과를 안 본다

CRITICAL CVE가 운영 이미지에 있는데 모른다.

안티패턴 4. 권한 없는 사용자에게 push 권한을 준다

이미지가 곧 운영 코드다. push 권한은 CI/일부 인력으로 제한.


11. 한 줄로 정리

ECR은 AWS 안의 컨테이너 이미지 저장소이며,
불변 태그 · 스캔 · 수명 주기 세 가지가 운영의 기본이다


12. 이 장의 핵심 정리

  1. ECR은 AWS의 관리형 컨테이너 이미지 저장소다.
  2. 서비스마다 리포지토리 하나가 표준이다.
  3. 태그는 불변으로 운영해야 롤백이 가능하다.
  4. 스캔 결과를 알람으로 받고, CRITICAL은 배포에서 차단한다.
  5. 수명 주기 정책으로 오래된 이미지를 자동 정리한다.
  6. ECS의 Task Execution Role이 이미지를 pull 한다.

44장. ECS의 구조 — Cluster · Task · Service

이 장에서 말하고자 하는 것

도커 이미지를 ECR에 올렸으니
이제 그 이미지를 실제로 돌리는 도구가 필요하다.

Amazon ECS (Elastic Container Service)

다.

이 장은 ECS가 무엇이고 어떤 구성 요소로 동작하는지 본다.


1. ECS는 무엇을 해주는가

ECS는 컨테이너 오케스트레이션 서비스다.

다음을 자동으로 한다.

  • 컨테이너 어디서 돌릴지 결정
  • 죽으면 다시 띄우기
  • 트래픽 분배(ALB 연동)
  • 로그/메트릭 수집
  • 배포 (Rolling / Blue-Green)

사람이 EC2에 SSH로 들어가 docker run 하지 않게 해준다.


2. ECS의 4개 핵심 개념

Task Definition  : 컨테이너 1대의 "설계도"
Task             : 실제로 돌고 있는 컨테이너 묶음
Service          : Task가 항상 N개 살아 있도록 관리하는 컨트롤러
Cluster          : 위 모든 것이 들어 있는 논리적 공간
[Cluster: msa]
 ├─ Service "orders"
 │   ├─ Task 1   (Task Definition: orders:7)
 │   └─ Task 2
 ├─ Service "users"
 │   └─ ...

3. Task Definition — 컨테이너 설계도

어떤 이미지를 / 어떤 자원으로 / 어떤 환경 변수로 돌릴지의 정의.

  • 이미지 URL
  • CPU · 메모리
  • 환경 변수
  • 포트
  • IAM Role
  • 로그 설정

자세한 건 45장.


4. Task — 살아 있는 컨테이너 묶음

Task는 하나 이상의 컨테이너 다.

Task A
 ├─ app 컨테이너
 └─ log-forwarder 컨테이너 (사이드카)

대부분 Task = 컨테이너 1개.
필요할 때만 사이드카를 추가한다.

Task는 일회용이다. 죽으면 끝, Service가 다시 띄운다.


5. Service — Task를 N개 유지하는 컨트롤러

desiredCount = 2
  ├─ Task 1 죽음 → Service가 새 Task 띄움
  └─ Task 2 정상

Service는 또한

  • ALB Target Group에 Task IP 자동 등록
  • 헬스 체크 실패 시 교체
  • 새 버전 배포 (Rolling)

까지 한다. 운영의 대부분은 Service 단위.


6. Cluster — 논리적 그룹

환경별로 나누는 게 일반적이다.

prod 클러스터   → 운영
stage 클러스터  → 스테이징
dev 클러스터    → 개발

7. Capacity Provider — 컨테이너를 어디서 돌릴까

  • Fargate — 서버리스, 노드 관리 없음
  • EC2 — 본인이 만든 EC2 클러스터

이 선택을 추상화한 게 Capacity Provider다. 자세한 비교는 46장.


8. 우리 서비스에서

[Cluster: msa]
 ├─ Service "orders"   (Fargate, desired=2)
 ├─ Service "users"    (Fargate, desired=2)
 ├─ Service "payments" (Fargate, desired=2)
 └─ Service "web"      (Fargate, desired=2)

9. 직접 확인해보기 — CLI

aws ecs create-cluster --cluster-name msa

aws ecs register-task-definition \
  --cli-input-json file://task-def.json

aws ecs create-service \
  --cluster msa \
  --service-name orders \
  --task-definition orders:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[...],securityGroups=[...]}"

aws ecs list-tasks --cluster msa --service-name orders

10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_ecs_cluster" "main" {
  name = "msa"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

resource "aws_ecs_task_definition" "orders" {
  family                   = "orders"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "512"
  memory                   = "1024"
  execution_role_arn       = aws_iam_role.task_execution.arn

  container_definitions = jsonencode([{
    name      = "app"
    image     = "${aws_ecr_repository.orders.repository_url}:v1"
    essential = true
    portMappings = [{ containerPort = 8080 }]
  }])
}

resource "aws_ecs_service" "orders" {
  name            = "orders"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.orders.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = [aws_subnet.private_a.id, aws_subnet.private_b.id]
    security_groups = [aws_security_group.task.id]
  }
}

11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. Container Insights를 안 켠다

ECS의 메트릭이 제대로 안 모인다.

안티패턴 2. 한 클러스터에 prod와 dev를 섞는다

권한 · 네트워크 · 비용이 다 섞인다.

안티패턴 3. Task에 외부 IP를 직접 노출한다

assign_public_ip = true 는 보안 위험. Task는 프라이빗 서브넷.

안티패턴 4. Service 없이 RunTask로만 띄운다

RunTask는 일회성. 운영 서비스는 무조건 Service.


12. 한 줄로 정리

ECS는 Cluster 안에서 Service가 Task Definition으로 Task를 N개 살아 있게 유지하는 구조다


13. 이 장의 핵심 정리

  1. ECS의 네 축은 Cluster · Service · Task Definition · Task 다.
  2. Task Definition은 설계도, Task는 실행 인스턴스다.
  3. Service가 Task 수와 배포를 책임진다.
  4. Capacity Provider는 Fargate/EC2 선택을 추상화한다.
  5. 환경별로 Cluster를 나누고 Container Insights를 켠다.
  6. 운영 서비스에 RunTask 단독은 쓰지 않는다.

45장. Task Definition 깊게 보기

이 장에서 말하고자 하는 것

운영에서 가장 자주 만지는 게 Task Definition이다.

새 버전 배포 = 새 Task Definition 리비전 등록

이 장은 Task Definition의 주요 필드를 정리한다.


1. 주요 필드

family                : 이름 (orders, users 등)
revision              : 자동 증가 번호 (1, 2, 3 …)
networkMode           : awsvpc (Fargate는 항상 이거)
cpu / memory          : Task 전체 자원
executionRoleArn      : Task를 띄울 때 필요한 권한
taskRoleArn           : Task 안의 코드가 AWS API 호출할 때 권한
containerDefinitions  : 컨테이너 한 대 이상의 정의

2. Family와 Revision

orders:1, orders:2, orders:3 ...

새 버전 배포:

이미지 push → 새 revision 등록 → Service가 그 revision으로 업데이트

orders:2 가 망가지면 Service에 orders:1 을 다시 지정해 롤백.

모든 revision은 보존된다 — 롤백 자산이다


3. CPU와 Memory

Fargate는 묶음으로 고른다.

0.25 vCPU  →  512 MB / 1 GB / 2 GB
0.5  vCPU  →  1 ~ 4 GB
1    vCPU  →  2 ~ 8 GB
2    vCPU  →  4 ~ 16 GB

묶음 밖의 조합은 불가.

너무 작게 잡으면 OOM, 너무 크게 잡으면 비용


4. Container Definition

container "app"
  ├─ image          : ECR URL
  ├─ portMappings
  ├─ environment / secrets
  ├─ logConfiguration
  ├─ healthCheck
  ├─ essential      : true/false
  └─ dependsOn      : 사이드카 순서

essential

essential = true   → 이 컨테이너 죽으면 Task 전체 죽음
essential = false  → 죽어도 Task는 살아 있음

메인 앱은 true.


5. 환경 변수와 시크릿

"environment": [
  { "name": "PORT", "value": "8080" }
]

"secrets": [
  { "name": "DB_PASSWORD",
    "valueFrom": "arn:aws:secretsmanager:...:secret:db/password" }
]

비밀은 Task Definition 안에 평문으로 두지 않는다


6. 로그 설정

"logConfiguration": {
  "logDriver": "awslogs",
  "options": {
    "awslogs-group": "/ecs/orders",
    "awslogs-region": "ap-northeast-2",
    "awslogs-stream-prefix": "app"
  }
}

7. Execution Role vs Task Role

가장 헷갈리는 두 권한.

Execution Role
  → "Task를 띄우기 위해" 필요
  → ECR pull, CloudWatch Logs 쓰기, Secrets Manager 읽기
  → ECS 에이전트가 사용

Task Role
  → "Task 안 코드" 가 AWS API 호출할 때 권한
  → S3 읽기, DynamoDB 접근 등
  → 애플리케이션이 사용

이 둘을 헷갈리면

  • “Task가 시작 안 됨” → Execution Role 문제
  • “Task는 떴는데 S3 권한 거부됨” → Task Role 문제

8. 우리 서비스에서

orders 의 Task Definition은 대략:

family: orders
cpu: 512 / memory: 1024
executionRoleArn: ecs-task-execution-role
taskRoleArn: orders-task-role (DynamoDB 읽기 권한)

container "app"
  image: <ecr>/orders:v1
  portMapping: 8080
  environment: PORT=8080
  secrets: DB_PASSWORD ← Secrets Manager
  logConfig: awslogs → /ecs/orders

같은 패턴을 users · payments 로 복제.


9. 직접 확인해보기 — CLI

aws ecs register-task-definition \
  --cli-input-json file://task-def.json

aws ecs describe-task-definition \
  --task-definition orders

aws ecs describe-task-definition \
  --task-definition orders:5

10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_ecs_task_definition" "orders" {
  family                   = "orders"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "512"
  memory                   = "1024"
  execution_role_arn       = aws_iam_role.task_execution.arn
  task_role_arn            = aws_iam_role.orders_task.arn

  container_definitions = jsonencode([{
    name      = "app"
    image     = "${aws_ecr_repository.orders.repository_url}:v1"
    essential = true

    portMappings = [
      { containerPort = 8080, protocol = "tcp" }
    ]

    environment = [
      { name = "PORT", value = "8080" }
    ]

    secrets = [
      { name      = "DB_PASSWORD",
        valueFrom = aws_secretsmanager_secret.db_password.arn }
    ]

    logConfiguration = {
      logDriver = "awslogs"
      options = {
        awslogs-group         = "/ecs/orders"
        awslogs-region        = "ap-northeast-2"
        awslogs-stream-prefix = "app"
      }
    }
  }])
}

11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 비밀을 평문 환경 변수에 둔다

secrets 로 Secrets Manager에서 가져온다.

안티패턴 2. Execution Role과 Task Role을 혼동한다

같은 Role을 두 자리에 박으면 권한이 과해진다.

두 Role은 별도로, 각자의 최소 권한만

안티패턴 3. revision을 안 정리한다

수십 개의 revision이 쌓여 검색이 어려워진다. 주기적으로 deregister.

안티패턴 4. CPU/메모리를 한 번 잡고 안 본다

실측 후 조정한다.


12. 한 줄로 정리

Task Definition은 컨테이너의 설계도이며,
family + revision 단위로 모든 배포와 롤백이 일어난다


13. 이 장의 핵심 정리

  1. family + revision 이 Task Definition의 식별자다.
  2. CPU/메모리는 Fargate 허용 묶음 안에서만.
  3. Execution Role은 Task 시작용, Task Role은 Task 내부 코드용.
  4. 비밀은 secrets 로 Secrets Manager에서 주입한다.
  5. revision은 모두 보존된다 — 곧 롤백 자산.
  6. 실측 기반으로 CPU/메모리를 주기적으로 조정한다.

46장. EC2 vs Fargate — 어느 쪽에서 돌릴까

이 장에서 말하고자 하는 것

ECS는 컨테이너를 두 종류의 인프라 위에서 돌릴 수 있다.

  • EC2 — 본인이 만든 EC2 인스턴스 위
  • Fargate — 서버리스. 노드 관리 없음

이 선택이 운영의 무게를 크게 바꾼다.


1. 가장 큰 차이 — 노드를 누가 관리하는가

EC2 (자체 노드)

[ECS Cluster]
  ├─ EC2 instance 1   ← 본인이 띄움
  ├─ EC2 instance 2
  └─ EC2 instance 3
       ↑ Task들이 흩어져 돌아감
  • OS 패치
  • 보안 업데이트
  • 디스크 관리
  • 노드 ASG 운영

→ 모두 본인 책임

Fargate

[ECS Cluster]
  ├─ Task 1 (어떤 호스트에 있는지 모름)
  ├─ Task 2
  └─ Task 3
  • AWS가 호스트 관리
  • 사용자는 Task만 본다

2. 비용 — 어느 쪽이 싼가

상황마다 다르다.

Fargate가 유리

  • 트래픽이 들쭉날쭉
  • 컨테이너 수가 적거나 중간
  • 운영 인력이 적다
  • 한가한 노드를 계속 띄우고 싶지 않다

EC2가 유리

  • 일정한 대규모 트래픽
  • 노드 활용률을 90% 이상 끌어올릴 수 있다
  • Spot 인스턴스를 적극 활용한다
  • GPU 등 Fargate가 지원하지 않는 자원이 필요하다

작은 ~ 중간 규모는 거의 항상 운영 비용까지 합쳐 Fargate가 유리


3. 기능 비교 표

항목EC2Fargate
노드 관리본인AWS
Spot 사용자유Fargate Spot 일부 가능
GPU가능불가
Privileged 컨테이너가능불가
데몬셋 패턴가능불가
시작 속도빠름약간 느림
자원 활용률본인 책임Task 단위 과금이라 자동

4. 보안

Fargate는 호스트가 격리돼 있어
커널 공유로 인한 위험이 낮다.

EC2 기반 ECS는 한 EC2에 여러 Task가 같은 커널을 공유한다.

규제 산업 · 멀티 테넌트 환경에서 Fargate를 선호하는 이유 중 하나다.


5. 우리 서비스에서

이 책의 척추 구조는 Fargate 기본.

이유:

  • 운영 부담 최소
  • 학습 곡선 낮음
  • 작은 ~ 중간 규모 MSA에 비용도 합리적

GPU나 데몬셋이 필요해지면 일부 서비스만 EC2로 옮긴다.


6. 같은 클러스터에서 섞어 쓰기

[Cluster: msa]
 ├─ Service "orders"   (Fargate)
 ├─ Service "users"    (Fargate)
 └─ Service "ml-infer" (EC2 GPU)

서비스마다 Capacity Provider를 다르게 지정한다.


7. 직접 확인해보기 — CLI

# Fargate
aws ecs create-service \
  --cluster msa --service-name orders \
  --task-definition orders:1 --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[...],securityGroups=[...]}"

# EC2
aws ecs create-service \
  --cluster msa --service-name ml-infer \
  --task-definition ml:1 --desired-count 1 \
  --launch-type EC2

8. 코드로는 이렇게 생겼다 — Terraform (Fargate + Spot 혼합)

resource "aws_ecs_service" "orders" {
  name            = "orders"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.orders.arn
  desired_count   = 2

  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    weight            = 1
  }

  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    weight            = 4
  }

  network_configuration {
    subnets         = [aws_subnet.private_a.id, aws_subnet.private_b.id]
    security_groups = [aws_security_group.task.id]
  }
}

위 예시는 Fargate Spot을 80% 섞은 구성 이다.

  • 80% Fargate Spot — 싸지만 잠깐 끊김 가능
  • 20% Fargate — 안정

상태 비저장 서비스라면 큰 절감이 된다.


9. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. “비싸 보여서” Fargate를 안 쓴다

운영 인력 시간까지 합치면 보통 손해. 실측 청구서로 판단한다.

안티패턴 2. 상태 있는 서비스에 Fargate Spot 비율을 높게 잡는다

Spot은 언제든 회수될 수 있다.

안티패턴 3. EC2 기반 ECS에서 노드 활용률을 안 본다

노드가 늘 한가하면 비용 낭비.

안티패턴 4. Privileged · GPU 같은 요건을 Fargate에 띄우려 한다

지원되지 않는다. 시작부터 EC2 · EKS 노드그룹으로.


10. 한 줄로 정리

Fargate는 노드 관리를 없애고, EC2는 더 자유로운 제어와 비용 최적화를 제공한다


11. 이 장의 핵심 정리

  1. EC2와 Fargate는 “노드를 누가 관리하는가” 가 가장 큰 차이다.
  2. 작은 ~ 중간 규모는 운영 비용까지 합쳐 Fargate가 유리하다.
  3. GPU · Privileged · 데몬셋 같은 요건은 EC2가 필요하다.
  4. 같은 Cluster에 두 종류를 섞어 쓸 수 있다.
  5. Fargate Spot은 상태 비저장 서비스에서 큰 절감 효과를 낸다.

47장. ECS Service와 ALB 연결하기

이 장에서 말하고자 하는 것

이제 우리는

  • ECS의 구조 (Cluster / Task / Service)
  • Task Definition 으로 컨테이너 정의
  • Fargate를 쓸지 EC2를 쓸지

까지 다뤘다.

그런데 띄운 컨테이너에 트래픽이 들어오려면 ALB와 묶어야 한다.

이 장은 그 묶음을 본다.


1. 큰 그림

[ALB]
  └─ Target Group (IP 타입)
       ↑ 자동 등록
[ECS Service]
  ├─ Task 1 (10.0.1.10)
  ├─ Task 2 (10.0.2.11)
  └─ Task 3 (...)

핵심은

ECS Service가 새 Task를 띄울 때마다
그 Task의 IP를 자동으로 Target Group에 등록한다

는 점이다. 사람이 ALB에 IP를 박지 않는다.


2. Service의 loadBalancers 설정

ECS Service에 ALB Target Group을 묶는다.

Service "orders"
  ↳ TG ARN: tg-orders
  ↳ Container name: app
  ↳ Container port: 8080

ECS는 이걸 보고

  • Task가 뜨면 → TG에 IP 등록
  • Task가 죽으면 → TG에서 제거

3. Health Check 흐름

1. Service가 Task 띄움
2. 컨테이너 시작
3. ALB가 /health 요청 → 200
4. 연속 2번 성공 → Healthy
5. 트래픽 들어옴
6. 연속 2번 실패 → Unhealthy → TG에서 빠짐
7. Service가 새 Task 띄움

Service가 원하는 desiredCount를 유지하면서
죽은 Task는 자동 교체한다


4. Health Check Grace Period

Task가 막 떴는데 ALB가 곧장 체크하면
애플리케이션이 아직 부팅 중일 수 있다.

healthCheckGracePeriodSeconds: 60

이 시간 동안은 헬스 체크 실패를 무시한다.

부팅 느린 애플리케이션은 이 값을 충분히 둔다
안 그러면 ECS가 끝없이 Task를 죽이고 만들기를 반복한다


5. 우리 서비스에서

[ALB]
 ├─ TG-orders   → ECS Service "orders"   (2 task)
 ├─ TG-users    → ECS Service "users"    (2 task)
 ├─ TG-payments → ECS Service "payments" (2 task)

한 TG = 한 ECS Service = 한 마이크로서비스


6. 직접 확인해보기 — CLI

Service 만들기 (TG 연결)

aws ecs create-service \
  --cluster my-cluster \
  --service-name orders \
  --task-definition orders:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --load-balancers "targetGroupArn=<tg-arn>,containerName=app,containerPort=8080" \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-x,subnet-y],securityGroups=[sg-x]}" \
  --health-check-grace-period-seconds 60

TG에 자동 등록된 Task 보기

aws elbv2 describe-target-health \
  --target-group-arn <tg-arn>

응답의 Id 가 Task의 ENI IP다.


7. 코드로는 이렇게 생겼다 — Terraform

resource "aws_ecs_service" "orders" {
  name            = "orders"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.orders.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = [aws_subnet.private_a.id, aws_subnet.private_b.id]
    security_groups = [aws_security_group.task.id]
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.orders.arn
    container_name   = "app"
    container_port   = 8080
  }

  health_check_grace_period_seconds = 60

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }
}

deployment_circuit_breaker 는 배포가 실패하면
자동으로 이전 버전으로 돌아간다. 운영에 거의 항상 켠다.


8. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. Service에 TG 없이 띄우고 외부에 직접 노출한다

Task의 IP는 자주 바뀐다. 도메인으로 가리킬 수 없다.

ECS Task는 항상 ALB / NLB의 TG 뒤에

안티패턴 2. Grace Period가 0이다

부팅 느린 앱이 끝없이 죽고 뜬다.

60~120초로 시작해서 줄여본다

안티패턴 3. 한 TG에 여러 Service를 묶는다

스케일링 · 배포 · 헬스 체크가 다 섞인다.

안티패턴 4. Circuit Breaker를 안 켠다

배포 실패해도 새 Task가 계속 뜨려다 죽는 무한 루프.


9. 한 줄로 정리

ECS Service는 Task를 ALB Target Group에 자동 등록하며,
헬스 체크 결과로 트래픽과 자가 치유를 함께 다룬다


10. 이 장의 핵심 정리

  1. ECS Service의 load_balancer 설정이 ALB TG와 묶는다.
  2. Task가 뜨면 IP가 자동 등록, 죽으면 제거된다.
  3. Grace Period로 부팅 중 헬스 체크를 무시할 수 있다.
  4. 한 Service = 한 TG = 한 마이크로서비스가 표준이다.
  5. deployment_circuit_breaker + rollback은 배포 안전망이다.

48장. ECS Service Discovery — Cloud Map

이 장에서 말하고자 하는 것

ALB는 외부 트래픽을 분배해준다.

그런데 컨테이너끼리 서로 부를 때는 어떻게 할까?

orders 컨테이너 → users 컨테이너 호출

이때 매번 새 Task의 IP를 알아내기는 어렵다.

이 문제를 푸는 게

AWS Cloud Map

이고 ECS에서 이걸 활용하는 기능이

ECS Service Discovery

다.


1. 무엇을 해결하는가

ECS Task는 IP가 자주 바뀐다.

users-task-1 : 10.0.1.10
users-task-2 : 10.0.2.11
... 죽고 살고 ...
users-task-3 : 10.0.1.45

대신 Service에 이름을 붙이면

users.local

이 이름이 자동으로 현재 살아 있는 Task의 IP들을 가리킨다.

다른 컨테이너는 http://users.local:8080 으로 부르기만 하면 된다


2. 동작 구조

[ECS Service "users"]
  ├─ task 1 (10.0.1.10)
  └─ task 2 (10.0.2.11)
       ↓ 자동 등록
[Cloud Map: users.local]
  ├─ A 10.0.1.10
  └─ A 10.0.2.11
       ↓ DNS 질의
[orders 컨테이너] → users.local 으로 요청

Task가 뜨고 죽을 때마다 Cloud Map의 DNS가 자동으로 갱신된다


3. ALB와 Service Discovery는 자리가 다르다

ALB              → 외부 사용자 → 서비스
Service Discovery → 서비스 ↔ 서비스 (내부)
  • ALB: L7 라우팅, 인증서, 헬스 체크 정밀
  • Service Discovery: 단순 DNS, 내부 식별

내부 호출에 ALB를 또 두면

  • 비용이 든다
  • 지연이 늘어난다
  • 트래픽이 굳이 외부 경로를 흉내 낸다

내부 호출은 Service Discovery로.


4. Service Connect — 더 단단한 옵션

ECS는 Service Connect 라는 진화된 방식도 제공한다.

  • DNS 기반 + 사이드카 프록시
  • 자동 재시도 · 타임아웃 · 메트릭
  • HTTP/2 · gRPC 지원

새로 시작하는 프로젝트라면 Service Connect 부터 검토해도 좋다.


5. 우리 서비스에서

[ALB]   ← 외부 트래픽
   ↓
[ECS Service: web]
   ↓ http://users.local
[ECS Service: users]
   ↓ http://payments.local
[ECS Service: payments]
  • 외부 → 내부: ALB
  • 내부 → 내부: Service Discovery (또는 Service Connect)

6. 직접 확인해보기 — CLI

aws servicediscovery create-private-dns-namespace \
  --name local \
  --vpc <vpc-id>

aws servicediscovery create-service \
  --name users \
  --dns-config "NamespaceId=<ns-id>,DnsRecords=[{Type=A,TTL=10}]"

dig users.local

VPC 안에서만 응답한다.


7. 코드로는 이렇게 생겼다 — Terraform

resource "aws_service_discovery_private_dns_namespace" "main" {
  name = "local"
  vpc  = aws_vpc.main.id
}

resource "aws_service_discovery_service" "users" {
  name = "users"

  dns_config {
    namespace_id = aws_service_discovery_private_dns_namespace.main.id

    dns_records {
      type = "A"
      ttl  = 10
    }
  }
}

resource "aws_ecs_service" "users" {
  name            = "users"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.users.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = [aws_subnet.private_a.id, aws_subnet.private_b.id]
    security_groups = [aws_security_group.task.id]
  }

  service_registries {
    registry_arn = aws_service_discovery_service.users.arn
  }
}

service_registries 한 줄이
ECS Service와 Cloud Map을 묶는다.


8. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 내부 호출에도 ALB를 둔다

비용 · 지연 · 복잡도 모두 늘어난다.

안티패턴 2. DNS TTL을 길게 둔다

Task가 죽었는데 클라이언트가 옛 IP로 계속 보낸다.

내부 Discovery DNS의 TTL은 짧게 (10초 권장)

안티패턴 3. 애플리케이션에서 DNS를 한 번만 해석한다

일부 언어/라이브러리는 부팅 시 DNS만 보고 캐싱한다.
이러면 TTL이 짧아도 의미가 없다.

매 호출마다 새로 해석하거나, 짧은 캐시를 명시적으로 켠다

안티패턴 4. Service Discovery를 외부 트래픽에 쓴다

.local 은 VPC 내부에서만 응답한다.


9. 한 줄로 정리

Service Discovery는 자주 바뀌는 컨테이너 IP를
이름 하나로 안정적으로 부를 수 있게 만든다


10. 이 장의 핵심 정리

  1. ECS Task는 IP가 자주 바뀌므로 내부 호출에 이름이 필요하다.
  2. Cloud Map은 Task를 자동으로 DNS에 등록해준다.
  3. ALB는 외부 트래픽, Service Discovery는 내부 호출이다.
  4. DNS TTL은 짧게, 클라이언트 캐시도 점검한다.
  5. 새 프로젝트는 Service Connect 도 검토할 가치가 있다.

49장. ECS 배포 전략 — Rolling · Blue-Green

이 장에서 말하고자 하는 것

새 버전 컨테이너를 띄우는 순간은 운영의 가장 위험한 순간이다.

  • 새 버전이 죽어 있다면?
  • 사용자 트래픽이 어떻게 영향을 받는가?
  • 문제가 보이면 얼마나 빨리 되돌릴 수 있는가?

이 질문에 답하는 게 배포 전략 이다.

ECS는 두 가지 표준 전략을 제공한다.

  • Rolling Update
  • Blue-Green

1. Rolling Update — 기본 전략

ECS의 기본 배포 방식.

v1 v1 v1 v1   ← 시작
v1 v1 v1 v2   ← Task 한 개씩 교체
v1 v1 v2 v2
v1 v2 v2 v2
v2 v2 v2 v2   ← 완료
  • 단순하다
  • 추가 인프라 거의 안 든다
  • v1과 v2가 동시에 트래픽을 받는 구간이 존재

이 구간이 문제가 될 수 있다.

  • DB 스키마 호환성 필요
  • API 호환성 필요

2. Rolling 의 두 비율 — Max % · Min %

minimumHealthyPercent : 배포 중 최소 몇 % 살아 있어야 하는가
maximumPercent        : 배포 중 최대 몇 %까지 띄울 수 있는가
desiredCount = 4
min 50% / max 200%
  → 최소 2개 살아 있고
  → 최대 8개까지 동시 허용
  • min 100% / max 200% → 신버전 4개 띄운 뒤 구버전 4개 빼기 (끊김 없음)
  • min 50% / max 100% → 자원 절약 (잠깐 적게 운영)

3. Blue-Green — 안전한 전략

ECS + CodeDeploy 조합으로 쓴다.

[ALB]
 ├─ TG-blue (v1)   ← 현재 100%
 └─ TG-green (v2)  ← 새 버전 대기

배포 시:
  1. Green TG에 v2 띄움
  2. 헬스 체크 통과
  3. Listener forward를 Green으로 전환
  4. 문제 없으면 Blue 정리
  5. 문제 있으면 즉시 Blue로 되돌림
  • 트래픽 전환이 한순간
  • 롤백이 빠르다
  • 구버전·신버전 혼재 구간 없음
  • 자원이 두 배 필요한 순간이 있음

4. Canary — 일부만 천천히 바꾸기

Blue-Green 위에서 트래픽 비율을 단계적으로 옮긴다.

0%  →  10%  →  50%  →  100%

각 단계 사이에 시간을 두고 지표 (에러율 · 지연) 가 정상인지 본다.


5. 어떤 걸 골라야 하는가

일반 마이크로서비스 → Rolling
중요 비즈니스 흐름   → Blue-Green
민감한 변화        → Canary

처음부터 Blue-Green 안 가도 된다.
Rolling + Circuit Breaker + 빠른 롤백이면 운영 90% 커버.


6. Deployment Circuit Breaker

ECS 배포가 실패하면 자동으로 멈추는 안전망이다.

새 Task가 계속 헬스 체크 실패 → 배포 중단
rollback = true → 이전 버전으로 되돌림

운영에서는 사실상 기본값.


7. 우리 서비스에서

배포 시:
[CI/CD]
   ↓ 새 이미지 푸시
[ECR]
   ↓
[ECS Service]  ← Rolling 또는 Blue-Green
   ├─ task v1 ──┐
   ├─ task v1   │
   └─ task v2 ←┘
  • 기본: Rolling + Circuit Breaker
  • 결제 · 인증: Blue-Green

8. 직접 확인해보기 — CLI

# 새 Task Definition으로 업데이트 (Rolling)
aws ecs update-service \
  --cluster my-cluster \
  --service orders \
  --task-definition orders:7

# 배포 상태
aws ecs describe-services \
  --cluster my-cluster \
  --services orders \
  --query 'services[0].deployments'

# 롤백
aws ecs update-service \
  --cluster my-cluster \
  --service orders \
  --task-definition orders:6

9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_ecs_service" "orders" {
  name            = "orders"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.orders.arn
  desired_count   = 4
  launch_type     = "FARGATE"

  deployment_minimum_healthy_percent = 100
  deployment_maximum_percent         = 200

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  network_configuration {
    subnets         = [aws_subnet.private_a.id, aws_subnet.private_b.id]
    security_groups = [aws_security_group.task.id]
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.orders.arn
    container_name   = "app"
    container_port   = 8080
  }
}

Blue-Green이 필요하면 deployment_controller.type = "CODE_DEPLOY" 로 바꾸고 CodeDeploy를 함께 구성한다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 호환되지 않는 DB 스키마 변경을 Rolling으로 배포

v1·v2가 동시에 살아 있을 때 한쪽이 깨진다.

스키마는 두 단계로 — 추가 → 코드 반영 → 제거

안티패턴 2. Circuit Breaker 없이 배포

새 Task가 끝없이 죽고 다시 떠서 비용 폭증.

안티패턴 3. 배포만 보고 트래픽 영향은 안 본다

새 버전이 5xx 100% 인데도 ECS는 Task 살아 있다고 본다.

배포 직후 ALB 5xx · 지연 · 에러율을 함께 본다

안티패턴 4. 롤백 절차를 모른다

배포 전에 “이전 리비전” 을 메모해 두는 습관.


11. 한 줄로 정리

Rolling은 단순하고 빠르고, Blue-Green은 안전하다.
둘 다 Circuit Breaker와 명확한 롤백 절차가 받쳐야 운영이 된다.


12. 이 장의 핵심 정리

  1. Rolling은 ECS 기본 전략이며 min/max %로 속도를 조절한다.
  2. Blue-Green은 트래픽을 한순간에 전환하고 즉시 롤백할 수 있다.
  3. Canary는 Blue-Green 위에서 트래픽 비율을 단계적으로 옮긴다.
  4. DB 스키마 변경은 두 단계로 나눠 호환성을 유지한다.
  5. Circuit Breaker + rollback은 사실상 기본값이다.
  6. 롤백은 이전 Task Definition 리비전 지정 한 줄.

50장. EKS는 언제 고려하는가 — 개념 한 줄 이해

이 장에서 말하고자 하는 것

지금까지 ECS를 중심으로 컨테이너를 다뤘다.

AWS의 컨테이너 오케스트레이션은 ECS만 있는 게 아니다.

EKS (Elastic Kubernetes Service)

도 있다.

이 장은 EKS가 무엇이고 언제 고를지를 초보자 시점에서 간단히 정리한다.


1. EKS는 무엇인가

EKS는 AWS가 관리해주는 Kubernetes 클러스터다.

EKS = "AWS가 운영해주는 Kubernetes"

Kubernetes는 오픈소스의 컨테이너 오케스트레이션 표준이다.
EKS는 그 컨트롤 플레인을 AWS가 책임지고 운영한다.


2. ECS와 EKS의 차이

항목ECSEKS
만든 곳AWS 자체오픈소스 Kubernetes
학습 곡선낮음높음
AWS 의존성강함약함 (이식성 높음)
멀티 클라우드어려움쉬움
생태계AWS 중심광대한 K8s 생태계
인력 시장작음
비용Task 단위컨트롤 플레인 시간당 + 노드

3. ECS가 더 어울리는 경우

  • AWS 안에서만 동작하는 서비스
  • 운영 인력이 적다
  • Kubernetes 학습 비용이 부담된다
  • 빠르게 시작해야 한다

이 책의 척추 구조도 ECS 기준이다.


4. EKS가 더 어울리는 경우

  • 멀티 클라우드를 고려한다
  • Helm · Argo · Istio 같은 K8s 표준 도구를 이미 쓴다
  • K8s 경험자가 채용 시장에서 풍부하다
  • 사내 표준이 K8s 기반이다

5. Fargate는 ECS와 EKS 양쪽에서 쓸 수 있다

  • ECS on Fargate
  • EKS on Fargate

“노드 관리에서 해방되는 것” 은 ECS / EKS 선택과 별개의 축이다


6. 우리 서비스에서

이 책의 구조는 ECS on Fargate 다.

이유:

  • 초보자 학습 곡선이 가장 낮다
  • AWS 다른 서비스와의 연동이 자연스럽다
  • 운영 인력이 적어도 무리 없이 굴러간다

EKS는 회사 사정에 따라 나중에 고려할 수 있는 옵션으로 둔다.


7. EKS의 기본 용어 한 번만 보기

검토할 때 마주칠 핵심 용어다.

Pod         : 하나 이상의 컨테이너 묶음 (ECS의 Task와 유사)
Deployment  : Pod의 원하는 수와 버전 관리 (ECS의 Service와 유사)
Service     : 내부 트래픽 노출 (Service Discovery와 유사)
Ingress     : 외부 트래픽 진입점 (ALB와 비슷한 자리)
Namespace   : 논리적 분리 (ECS Cluster와 비슷한 자리)

ECS의 사고방식이 익으면 EKS로 옮기는 것도 어렵지 않다.


8. 직접 확인해보기 — CLI

# 클러스터 만들기 (eksctl 사용이 일반적)
eksctl create cluster \
  --name my-cluster \
  --region ap-northeast-2 \
  --fargate

# kubectl 로 노드 보기
kubectl get nodes

kubectl 은 Kubernetes의 표준 CLI다.
EKS 운영의 90%는 이 명령으로 한다.


9. 코드로는 이렇게 생겼다 — Terraform

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "my-cluster"
  cluster_version = "1.30"

  vpc_id     = aws_vpc.main.id
  subnet_ids = [
    aws_subnet.private_a.id,
    aws_subnet.private_b.id,
  ]

  fargate_profiles = {
    default = {
      selectors = [
        { namespace = "default" }
      ]
    }
  }
}

ECS와 비교하면 부속이 더 많다.
처음 켤 때 학습 비용이 크다는 점이 코드에서도 보인다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. “K8s가 멋지니까” EKS 부터 시작한다

운영 인력 부족한데 EKS를 깔면 운영 자체가 부담.

ECS로 시작 → 필요해지면 EKS

안티패턴 2. EKS 컨트롤 플레인 비용을 무시한다

EKS는 클러스터당 시간 단위 비용이 있다.
개발용 클러스터를 여럿 띄우면 청구서가 쌓인다.

안티패턴 3. K8s YAML을 IaC 없이 직접 apply 한다

손으로 적용한 리소스는 추적이 안 된다.

Helm / Argo CD / Terraform 같은 도구와 함께 쓴다


11. 한 줄로 정리

EKS는 K8s를 AWS가 대신 운영해주는 옵션이며,
이식성과 K8s 생태계가 필요할 때 ECS의 대안으로 등장한다


12. 이 장의 핵심 정리

  1. EKS는 AWS의 관리형 Kubernetes다.
  2. AWS 종속 + 단순함 → ECS, 이식성 + 표준 → EKS.
  3. Fargate는 ECS · EKS 양쪽에서 사용할 수 있다.
  4. 이 책은 ECS on Fargate를 기본으로 한다.
  5. EKS는 인력 · 표준 · 멀티 클라우드 요건이 생길 때 고려한다.

51장. 통합 흐름 — 우리 서비스를 ECS 위로 올려보기

이 장에서 말하고자 하는 것

Part 8에서 우리는 컨테이너의 모든 부품을 다뤘다.

  • 41장. 왜 컨테이너인가
  • 42장. 도커 기초
  • 43장. ECR
  • 44장. ECS 구조
  • 45장. Task Definition
  • 46장. EC2 vs Fargate
  • 47장. Service와 ALB 연결
  • 48장. Service Discovery
  • 49장. 배포 전략
  • 50장. EKS

이번 장은 이 부품들이 한 묶음으로 어떻게 굴러가는지 정리한다.

작은 마이크로서비스 한 개를 처음부터 끝까지 띄워본다


1. 우리가 띄울 서비스

매우 단순한 orders 서비스다.

GET /health         → 200
GET /api/orders     → 주문 목록
POST /api/orders    → 주문 생성

이 서비스를 컨테이너로 패키징해 ALB 뒤에 ECS Fargate로 띄운다.


2. 전체 구성도

[사용자]
  ↓ HTTPS
[CloudFront]
  ↓
[ALB]   ── /api/orders/* ──→ TG-orders
  ↓
[ECS Service "orders"]
  ├─ Task 1 (Fargate)
  └─ Task 2 (Fargate)
  ↓ 내부 호출
[RDS]   ← (다음 단원에서 다룸)

이번 장에서는 ALB ~ ECS 까지 닫는다. DB는 다음 단원.


3. 단계 — 무엇을 어떤 순서로 만드는가

1. Dockerfile 작성 → 이미지 빌드
2. ECR 리포지토리 생성
3. 이미지 푸시 (orders:v1)
4. Task Definition 등록 (이미지 URL · CPU · 메모리)
5. ALB · Target Group 준비
6. ECS Cluster · Service 생성
   - desiredCount = 2
   - Target Group 연결
7. ALB Listener Rule에 /api/orders/* → TG-orders 추가
8. (있다면) Service Discovery 등록
9. ALB DNS로 호출 테스트 → 헬스 체크 통과 확인

4. 핵심 4개 파일

.
├─ Dockerfile             ← 컨테이너 이미지 정의
├─ task-definition.json   ← (또는 Terraform 리소스로 대체)
├─ buildspec / workflow   ← 빌드/푸시 자동화
└─ terraform/             ← 인프라 정의

다음 단원(자동화)에서 한 번에 묶는 흐름을 다룬다.


5. 우리 서비스에서

[ALB]
 └─ /api/orders/*  →  TG-orders
                          ↑
                  [ECS Service "orders"]
                    ├─ task v1
                    └─ task v1

이걸 그대로 users · payments 로 복제하면 마이크로서비스가 늘어 간다.

[ALB]
 ├─ /api/orders/*   → ECS "orders"
 ├─ /api/users/*    → ECS "users"
 └─ /api/payments/* → ECS "payments"

이게 1~51장의 결과물이다.


6. 직접 확인해보기 — CLI 한 바퀴

# 1. ECR 로그인 & 이미지 푸시
aws ecr get-login-password --region ap-northeast-2 \
  | docker login --username AWS --password-stdin <acct>.dkr.ecr.ap-northeast-2.amazonaws.com

docker build -t orders:v1 .
docker tag orders:v1 <acct>.dkr.ecr.ap-northeast-2.amazonaws.com/orders:v1
docker push <acct>.dkr.ecr.ap-northeast-2.amazonaws.com/orders:v1

# 2. Task Definition 등록
aws ecs register-task-definition --cli-input-json file://task-def.json

# 3. Service 만들기
aws ecs create-service \
  --cluster my-cluster \
  --service-name orders \
  --task-definition orders:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --load-balancers "targetGroupArn=<tg-arn>,containerName=app,containerPort=8080" \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-x,subnet-y],securityGroups=[sg-x]}"

# 4. 호출 테스트
curl https://api.example.com/api/orders

7. 코드로는 이렇게 생겼다 — Terraform 한 묶음

resource "aws_ecr_repository" "orders" {
  name = "orders"
}

resource "aws_ecs_cluster" "main" {
  name = "msa"
}

resource "aws_ecs_task_definition" "orders" {
  family                   = "orders"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "512"
  memory                   = "1024"
  execution_role_arn       = aws_iam_role.task_execution.arn
  task_role_arn            = aws_iam_role.task.arn

  container_definitions = jsonencode([{
    name      = "app"
    image     = "${aws_ecr_repository.orders.repository_url}:v1"
    essential = true
    portMappings = [{ containerPort = 8080 }]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        awslogs-group         = "/ecs/orders"
        awslogs-region        = "ap-northeast-2"
        awslogs-stream-prefix = "app"
      }
    }
  }])
}

resource "aws_ecs_service" "orders" {
  name            = "orders"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.orders.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  network_configuration {
    subnets         = [aws_subnet.private_a.id, aws_subnet.private_b.id]
    security_groups = [aws_security_group.task.id]
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.orders.arn
    container_name   = "app"
    container_port   = 8080
  }
}

이 한 묶음이 Part 8 전체 + 33~37장 + 19~28장 (VPC) 의 결과물이다.


8. 이렇게 쓰면 망한다 — 통합 단계의 흔한 함정

함정 1. Task의 보안 그룹이 ALB 보안 그룹과 안 맞다

ALB → Task 8080 포트가 막혀 있으면 헬스 체크 영원히 실패.

Task SG는 ALB SG에서 오는 8080을 허용해야 한다

함정 2. 서브넷이 외부 인터넷 접근이 안 된다

ECR · CloudWatch · Secrets Manager 호출이 막힌다.

프라이빗 서브넷이면 NAT Gateway 또는 VPC Endpoint 필요

함정 3. Execution Role 권한 부족

ECR pull · CloudWatch 쓰기 권한이 없으면 Task 시작 실패.

AmazonECSTaskExecutionRolePolicy 부터 붙인다

함정 4. ALB Listener Rule을 까먹는다

Service · TG는 만들었는데 ALB 규칙이 없으면 트래픽이 안 들어온다.


9. 한 줄로 정리

컨테이너 한 개를 띄우는 일은 ECR · TaskDef · Service · ALB · VPC · IAM 이 한 묶음으로 맞물려야 한다


10. 이 장의 핵심 정리

  1. Part 8 부품들이 모이면 ALB → ECS → 컨테이너 흐름이 완성된다.
  2. 새 서비스를 추가하는 패턴은 “ECR 푸시 → TaskDef → Service → ALB 규칙” 의 반복이다.
  3. 실패의 80%는 보안 그룹 · 네트워크 · IAM에서 온다.
  4. 같은 패턴을 복제해 여러 마이크로서비스로 확장한다.
  5. 다음 단원은 이 서비스에 데이터 계층을 붙인다.

52장. API Gateway — REST API vs HTTP API

이 장에서 말하고자 하는 것

ALB까지 깔면 외부 트래픽이 ECS로 들어오는 길은 만들어진다.

그런데 운영하다 보면 다음이 필요해진다.

  • 사용자 인증 (JWT · OAuth · API Key)
  • 사용량 제한 (Rate limit · Quota)
  • 요청/응답 변환
  • 버전 관리
  • 서드파티 API와의 결합

이걸 ALB로 다 풀면 애플리케이션이 무거워진다.

이 문제를 한 단계 위에서 다루는 도구가

Amazon API Gateway

다.


1. API Gateway 위치

[사용자]
   ↓
[CloudFront]
   ↓
[API Gateway]   ← 여기, 인증·제한·변환
   ↓
[ALB]
   ↓
[ECS]

CloudFront와 ALB 사이에서 API 진입점 역할을 한다.


2. REST API vs HTTP API

항목REST APIHTTP API
출시오래됨신형
가격비쌈1/3 ~ 2/3
기능풍부 (사용량 계획, API Key, Mock)핵심만
지연작음
권장레거시새 프로젝트

새로 시작한다면 HTTP API 가 표준
사용량 계획 · 복잡한 변환이 정말 필요할 때만 REST API


3. API Gateway가 해주는 것

인증

  • JWT (Cognito · 외부 IdP)
  • Lambda Authorizer (커스텀 검증)
  • IAM 서명 (내부 호출)

제한

  • Rate limit (초당 N건)
  • Burst (순간 허용량)

변환

  • 요청 헤더/바디 변환
  • 응답 변환

통합 대상

  • HTTP (ALB · 외부 URL)
  • Lambda
  • AWS 서비스 직접 (DynamoDB · SQS 등)
  • VPC Link (프라이빗 ALB / NLB)

API Gateway는 기본적으로 인터넷 쪽이다.

[API Gateway]   ← 인터넷
   ↓ VPC Link
[Private ALB]   ← VPC 안
   ↓
[ECS]

ALB를 인터넷에 노출하지 않고도 API Gateway로 받아 내부로 전달할 수 있다.


5. ALB가 있는데 API Gateway도 필요한가

  • ALB는 L7 라우팅 + 헬스 체크 + 트래픽 분배가 강함
  • API Gateway는 인증 · 제한 · 변환이 강함
순수 내부용 ALB만으로 충분 → API Gateway 없음
공개 API · 외부 파트너 · 인증 필요 → API Gateway

대부분의 공개 SaaS는 둘 다 쓴다.


6. 우리 서비스에서

[CloudFront]
 ├─ /static/* → S3
 └─ /api/*
     ↓
   [API Gateway]
     ├─ JWT 인증
     ├─ Rate limit
     └─ VPC Link
          ↓
   [Private ALB]
     ├─ /api/orders/*   → ECS "orders"
     ├─ /api/users/*    → ECS "users"
     └─ /api/payments/* → ECS "payments"

7. 직접 확인해보기 — CLI

aws apigatewayv2 create-api \
  --name orders-api \
  --protocol-type HTTP

aws apigatewayv2 create-vpc-link \
  --name to-alb \
  --subnet-ids subnet-x subnet-y \
  --security-group-ids sg-z

aws apigatewayv2 create-route \
  --api-id <api-id> \
  --route-key "GET /api/orders" \
  --target "integrations/<integration-id>"

8. 코드로는 이렇게 생겼다 — Terraform

resource "aws_apigatewayv2_api" "main" {
  name          = "msa-api"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_vpc_link" "alb" {
  name               = "to-alb"
  security_group_ids = [aws_security_group.apigw.id]
  subnet_ids         = [aws_subnet.private_a.id, aws_subnet.private_b.id]
}

resource "aws_apigatewayv2_integration" "alb" {
  api_id             = aws_apigatewayv2_api.main.id
  integration_type   = "HTTP_PROXY"
  integration_uri    = aws_lb_listener.https.arn
  connection_type    = "VPC_LINK"
  connection_id      = aws_apigatewayv2_vpc_link.alb.id
  integration_method = "ANY"
}

resource "aws_apigatewayv2_route" "proxy" {
  api_id    = aws_apigatewayv2_api.main.id
  route_key = "ANY /api/{proxy+}"
  target    = "integrations/${aws_apigatewayv2_integration.alb.id}"
}

resource "aws_apigatewayv2_stage" "prod" {
  api_id      = aws_apigatewayv2_api.main.id
  name        = "$default"
  auto_deploy = true
}

이 모양이 “공개 → APIGW → VPC Link → 프라이빗 ALB” 의 기본형이다.


9. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 새 프로젝트에 REST API를 그냥 쓴다

비용과 지연만 늘어난다. 신규는 HTTP API.

안티패턴 2. ALB가 인터넷에 노출돼 있는데 API Gateway도 둔다

API Gateway 우회로가 열린다. ALB는 프라이빗으로 옮기고 VPC Link로 받는다.

안티패턴 3. API Gateway 안에 비즈니스 로직을 박는다

복잡한 변환을 VTL에 박으면 유지보수가 어렵다.

비즈니스 로직은 애플리케이션, API Gateway는 얇은 진입점

안티패턴 4. Rate limit을 안 건다

악의적 트래픽 한 번에 다운스트림이 무너진다.


10. 한 줄로 정리

API Gateway는 인증 · 제한 · 변환을 다루는 공개 API의 진입점이며,
VPC Link로 프라이빗 ALB를 안전하게 노출한다


11. 이 장의 핵심 정리

  1. API Gateway는 ALB 앞에서 인증 · 제한 · 변환을 책임진다.
  2. 새 프로젝트는 HTTP API가 기본 선택지다.
  3. VPC Link로 프라이빗 ALB를 노출할 수 있다.
  4. ALB는 내부, API Gateway는 외부 — 역할을 분리한다.
  5. Rate limit은 거의 항상 켠다.

53장. API Gateway의 인증 — IAM · Cognito · Lambda Authorizer

이 장에서 말하고자 하는 것

API Gateway의 진가는 인증이다.

ALB에 인증을 박는 것보다 API Gateway에서 처리하면

  • 애플리케이션이 가벼워지고
  • 인증 정책을 한 곳에서 관리할 수 있고
  • 미인증 요청이 ECS까지 도달하지 않는다

이 장은 API Gateway가 제공하는 세 가지 인증 방식을 본다.


1. 세 가지 방식

방식누가 검증하나용도
IAMAWS SigV4 서명내부 호출 / 머신 간
Cognito / JWT AuthorizerJWT 토큰사용자 인증
Lambda Authorizer커스텀 Lambda자유 로직

2. IAM 인증 — 내부 호출

요청에 AWS SigV4 서명 부착
  ↓
API Gateway가 IAM 정책으로 검증
  ↓
허용 → ECS / 거부 → 403

용도:

  • 서비스 → 서비스
  • Lambda → API
  • CI → API

사용자에게는 거의 쓰지 않는다 (서명 만들기 부담).


3. JWT Authorizer — 사용자 인증의 표준

사용자 → API에 Bearer 토큰
  ↓
API Gateway가 JWT 서명 · 만료 · scope 검증
  ↓
검증 통과 → 백엔드로 패스 (사용자 정보를 헤더에 주입)
검증 실패 → 401

토큰 발급자는 보통 둘 중 하나다.

  • Amazon Cognito
  • 외부 IdP — Auth0, Okta, Firebase, Google

4. Cognito User Pool

Cognito는 두 가지로 나뉜다.

  • User Pool — 회원가입 / 로그인 / 비밀번호 / MFA
  • Identity Pool — AWS 임시 자격증명 발급

대부분의 경우 User Pool만 쓴다.

회원가입 / 로그인 → User Pool
  ↓ JWT 토큰
사용자 → API Gateway → 백엔드

5. Lambda Authorizer — 자유 로직

JWT 표준 검증으로 부족할 때 쓴다.

  • 토큰을 외부 IdP에 매번 introspection
  • 사용자별 권한을 DB에서 조회
  • 회사 내부 토큰 포맷

Lambda 함수가

입력: 요청의 토큰
출력: ALLOW / DENY + 컨텍스트

를 돌려준다.

단순 JWT는 JWT Authorizer로
복잡한 로직은 Lambda Authorizer로


6. 인증 캐시

Lambda Authorizer는 매 요청마다 부르면 비싸다.

응답 캐시 TTL: 300초

같은 토큰이면 5분간 결과 재사용.

토큰 만료까지 남은 시간 > 캐시 TTL
안 그러면 만료된 토큰이 캐시 동안 통과한다


7. 우리 서비스에서

[사용자]
  ↓ Bearer <JWT>
[API Gateway]
  ├─ JWT Authorizer (Cognito User Pool)
  │    ↓ ALLOW
  └─ VPC Link
        ↓
   [Private ALB]
  • 사용자 인증: Cognito + JWT Authorizer
  • 내부 머신 호출: IAM
  • 특수 검증: Lambda Authorizer

8. 직접 확인해보기 — CLI

aws apigatewayv2 create-authorizer \
  --api-id <api-id> \
  --authorizer-type JWT \
  --identity-source '$request.header.Authorization' \
  --jwt-configuration Audience=<client-id>,Issuer=https://cognito-idp.ap-northeast-2.amazonaws.com/<userpool-id> \
  --name cognito-jwt

aws apigatewayv2 update-route \
  --api-id <api-id> \
  --route-id <route-id> \
  --authorization-type JWT \
  --authorizer-id <authorizer-id>

9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_cognito_user_pool" "main" {
  name = "msa-users"
}

resource "aws_cognito_user_pool_client" "web" {
  name            = "web"
  user_pool_id    = aws_cognito_user_pool.main.id
  generate_secret = false
}

resource "aws_apigatewayv2_authorizer" "jwt" {
  api_id           = aws_apigatewayv2_api.main.id
  authorizer_type  = "JWT"
  identity_sources = ["$request.header.Authorization"]
  name             = "cognito"

  jwt_configuration {
    audience = [aws_cognito_user_pool_client.web.id]
    issuer   = "https://cognito-idp.ap-northeast-2.amazonaws.com/${aws_cognito_user_pool.main.id}"
  }
}

resource "aws_apigatewayv2_route" "orders" {
  api_id             = aws_apigatewayv2_api.main.id
  route_key          = "ANY /api/orders/{proxy+}"
  target             = "integrations/${aws_apigatewayv2_integration.alb.id}"
  authorization_type = "JWT"
  authorizer_id      = aws_apigatewayv2_authorizer.jwt.id
}

이 한 묶음이 “토큰이 있어야만 들어올 수 있는 API” 다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. ALB · 백엔드에서 같은 인증을 또 한다

이중 검증으로 운영이 어려워진다.

인증은 한 곳에서, 그 뒤로는 신뢰

안티패턴 2. Bearer 만 보고 통과시킨다

서명 · 만료 · issuer · audience 모두 검증해야 한다.
JWT Authorizer는 자동 처리한다.

안티패턴 3. Lambda Authorizer 캐시 TTL을 길게 둔다

만료된 토큰이 캐시 동안 통과될 수 있다.

안티패턴 4. 비공개 엔드포인트에 Authorizer를 안 붙인다

모든 route에 명시적인 authorization_type을 설정한다.


11. 한 줄로 정리

API Gateway의 인증은 IAM · JWT · Lambda Authorizer 셋이며,
JWT + Cognito 조합이 일반 사용자 인증의 표준이다


12. 이 장의 핵심 정리

  1. API Gateway는 IAM · JWT · Lambda Authorizer 세 가지 인증을 제공한다.
  2. 사용자 인증의 표준은 JWT + Cognito 또는 외부 IdP다.
  3. Lambda Authorizer는 자유로운 로직이 필요할 때 쓴다.
  4. 인증은 한 곳에서, 백엔드는 신뢰한다.
  5. 캐시 TTL은 토큰 만료보다 짧게 둔다.

54장. CloudFront → API Gateway → ALB 의 역할 분담

이 장에서 말하고자 하는 것

지금까지 우리는 외부 트래픽의 세 계층을 모두 만났다.

  • CloudFront — 엣지, CDN, DDoS, TLS 종료
  • API Gateway — 인증, Rate limit, 변환
  • ALB — 서비스 라우팅, 헬스 체크, 트래픽 분배

이 셋이 한 줄에 늘어선다.

그럼 왜 셋 다 필요한가? 무엇을 누가 책임지나?


1. 각 계층이 잘하는 칸이 다르다

책임CloudFrontAPI GatewayALB
정적 콘텐츠 캐시
DDoS / WAF 흡수✅ (Shield)
TLS 종료 (외부)
Rate limit
JWT 인증
경로 기반 서비스 분배
헬스 체크 + 자가 치유
컨테이너 IP 자동 등록

2. 흐름 한 번 더

[사용자]
   ↓ HTTPS, DNS
[CloudFront]   ← 캐시, DDoS, 엣지 TLS
   ↓
[API Gateway]  ← 인증, Rate limit, 변환
   ↓ VPC Link
[Private ALB]  ← 경로 라우팅, 헬스 체크
   ↓
[ECS Service]  ← 자가 치유, 스케일링
   ↓
[Container]    ← 비즈니스 로직

각 단계가 자기 영역에서만 일한다.


3. 책임 분리의 효과

1. 한 계층이 죽어도 다른 게 일부 흡수

  • CloudFront 캐시가 origin 다운을 일정 시간 가린다
  • API Gateway가 Rate limit으로 폭증을 흡수해 ALB가 안 죽는다
  • ALB 헬스 체크가 죽은 Task를 즉시 분리

2. 보안 깊이 (Defense in Depth)

  • CloudFront WAF — 1차 그물
  • API Gateway 인증 — 2차 검증
  • VPC Link · 보안 그룹 — 3차 격리

3. 비용 최적화

  • 정적 응답은 CloudFront에서 흡수
  • 미인증 요청은 API Gateway에서 차단

4. 흔히 묻는 결정들

Q. 작은 서비스도 셋 다 깔아야 하나?

  • MVP / 내부 도구 → ALB 만으로 충분
  • 공개 API · SaaS → CloudFront + API Gateway + ALB
  • 정적 페이지 + 약간의 API → CloudFront + API Gateway (Lambda 가능)

Q. CloudFront 없이 API Gateway 만 두면?

  • DDoS 흡수 약함
  • TLS 종료가 사용자에서 멀다
  • 정적 자원 캐시 없음

Q. API Gateway 없이 CloudFront → ALB 만 두면?

  • 인증을 애플리케이션이 다 한다
  • Rate limit이 빈약
  • 토큰 검증이 ECS까지 도달 — 부하

5. 한 도메인에서 정적과 동적 동시에

example.com
 ├─ /static/* → S3
 ├─ /api/*    → API Gateway → ALB → ECS
 └─ /*        → S3 (SPA index.html)

CloudFront 한 배포 안에서 두 origin을 가진다.

  • CORS 문제 없음
  • 사용자가 보기에 한 도메인

6. 우리 서비스의 완성형

[Route 53] api.example.com
   ↓ ALIAS
[CloudFront] (us-east-1 ACM, WAF)
 ├─ /static/* → S3 (OAC)
 └─ /api/*    → APIGW
      ↓ JWT Authorizer
      ↓ VPC Link
[Private ALB] (서울 ACM)
 ├─ /api/orders/*   → ECS "orders"
 ├─ /api/users/*    → ECS "users"
 └─ /api/payments/* → ECS "payments"
   ↓
[ECS Service · Task · Container]
   ↓
[RDS · DynamoDB · ElastiCache]

이 그림이 1~54장의 결과물이다.


7. 직접 확인해보기 — 흐름 디버깅

문제가 어디서 났는지 모를 때

# 1. CloudFront 응답 헤더
curl -I https://api.example.com/api/orders
# → x-cache, x-amz-cf-id 확인

# 2. API Gateway 도달 여부
aws logs tail /aws/apigw/main --follow

# 3. ALB 액세스 로그
aws s3 cp s3://alb-logs/... - | head

# 4. ECS Task 로그
aws logs tail /ecs/orders --follow

“어느 계층까지 도달했나” 가 디버깅의 시작이다


8. 코드로는 이렇게 생겼다 — Terraform (origin 부분만)

# CloudFront: ALB가 아니라 API Gateway를 origin으로
origin {
  domain_name = replace(aws_apigatewayv2_api.main.api_endpoint, "https://", "")
  origin_id   = "apigw"
  custom_origin_config {
    origin_protocol_policy = "https-only"
    http_port              = 80
    https_port             = 443
    origin_ssl_protocols   = ["TLSv1.2"]
  }
}

그 아래는 38·52장 코드 그대로.


9. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. ALB가 인터넷에 노출돼 있다

우회로가 열려서 보안 효과가 의미를 잃는다.

진짜 origin은 무조건 비공개

안티패턴 2. 모든 계층에서 인증을 또 한다

운영이 못 따라간다.

인증은 API Gateway 한 곳에서, 그 뒤로는 신뢰

안티패턴 3. 같은 도메인을 두 계층에서 동시에 다룬다

DNS 매핑이 꼬인다.

한 도메인 → 한 진입점 (CloudFront)

안티패턴 4. 각 계층의 로그를 안 모은다

장애 추적이 안 된다.


10. 한 줄로 정리

CloudFront는 엣지에서, API Gateway는 인증/제한에서, ALB는 서비스 분배에서
각자 잘하는 일만 한다 — 그래서 셋을 함께 둔다


11. 이 장의 핵심 정리

  1. CloudFront · API Gateway · ALB는 책임이 겹치지 않는 세 계층이다.
  2. 보안 깊이와 비용 최적화가 동시에 이뤄진다.
  3. 작은 서비스는 ALB만으로 시작 가능, 공개 SaaS는 셋 다.
  4. ALB는 반드시 비공개.
  5. 인증은 API Gateway 한 곳에서.
  6. 각 계층의 로그를 모두 켜야 장애 추적이 가능하다.

55장. 블록 · 파일 · 객체 스토리지의 차이

이 장에서 말하고자 하는 것

ECS 위에 컨테이너를 띄우고 ALB로 받는 흐름까지 만들었다.

다음 질문이 자연스럽게 따라온다.

“데이터를 어디에 저장할까?”

AWS의 저장소는 크게 세 종류로 나뉜다.

  • 블록 스토리지
  • 파일 스토리지
  • 객체 스토리지

이 셋의 차이를 모르고 들어가면 잘못된 자리에 데이터를 둔다.


1. 블록 스토리지 — 디스크처럼

물리 디스크처럼 동작하는 저장소

  • 한 번에 한 인스턴스가 마운트
  • OS가 파일 시스템(ext4 등) 을 만든다
  • 빠르다 — DB에 적합
  • 같은 AZ 안에서만 마운트 가능

AWS에서는 EBS 다.


2. 파일 스토리지 — 공유 폴더처럼

여러 서버가 동시에 마운트하는 공유 폴더

  • NFS · SMB 프로토콜
  • 여러 인스턴스 동시 읽기/쓰기
  • AZ 사이에서도 접근 가능
  • 블록보다 느리다

AWS에서는 EFS · FSx 다.


3. 객체 스토리지 — HTTP로 접근

파일을 HTTP API로 저장 · 조회하는 저장소

  • 마운트하지 않는다
  • 폴더 구조가 가짜다 (Prefix)
  • 사실상 무제한 용량
  • 매우 저렴
  • 동시에 N 사람이 GET 가능

AWS에서는 S3 다.


4. 한 표로

항목블록 (EBS)파일 (EFS)객체 (S3)
접근한 인스턴스 마운트여러 인스턴스 마운트HTTP API
적합DB · OS 디스크공유 파일 · 미디어정적 자원 · 백업 · 로그
용량정해진 크기거의 무제한거의 무제한
속도매우 빠름중간객체 단위 응답
비용보통비쌈매우 저렴
AZ같은 AZ만멀티 AZ리전

5. 어떻게 고르는가

DB · 인덱스 · 부팅 디스크 → EBS
여러 서버가 공유하는 파일 → EFS
사용자 업로드 · 이미지 · 로그 · 백업 → S3

의심스러우면 S3 부터 본다


6. 컨테이너 시대의 변화

EC2 시대에는 디스크가 자연스러웠다.

컨테이너 시대에는

  • 컨테이너는 일회용 → EBS 의존도 줄어듦
  • 데이터는 외부 저장소로 → S3 · RDS · DynamoDB 가 표준
  • 공유 파일이 필요하면 EFS

컨테이너 안에 영구 데이터를 두지 않는다


7. 우리 서비스에서

[ECS 컨테이너]
   ├─ 사용자 업로드 → S3
   ├─ 정적 자원   → S3 (CloudFront로 캐시)
   ├─ 로그        → CloudWatch / S3
   ├─ DB          → RDS / DynamoDB
   └─ 공유 파일   → EFS (필요할 때만)

이 책의 척추는 S3 + RDS + DynamoDB 가 중심이다.


8. 직접 확인해보기 — CLI

# 블록 (EBS)
aws ec2 describe-volumes

# 파일 (EFS)
aws efs describe-file-systems

# 객체 (S3)
aws s3 ls
aws s3 cp ./file.txt s3://my-bucket/file.txt

각자의 인터페이스가 다른 게 그대로 느껴진다.


9. 코드로는 이렇게 생겼다 — Terraform

# 블록
resource "aws_ebs_volume" "data" {
  availability_zone = "ap-northeast-2a"
  size              = 100
  type              = "gp3"
}

# 파일
resource "aws_efs_file_system" "shared" {
  performance_mode = "generalPurpose"
}

# 객체
resource "aws_s3_bucket" "uploads" {
  bucket = "msa-uploads"
}

세 종류가 얼마나 다른 자리에 사는지 코드에서도 보인다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 사용자 업로드를 EBS에 저장한다

EBS는 한 인스턴스에만 붙는다. 스케일링하면 다른 인스턴스에서 그 파일을 못 본다.

업로드는 S3

안티패턴 2. 로그를 컨테이너 안에 쌓는다

컨테이너 죽으면 사라진다.

로그는 CloudWatch / S3

안티패턴 3. EFS를 데이터베이스에 쓴다

NFS는 DB가 요구하는 fsync 동작과 충돌이 난다.

안티패턴 4. S3에 폴더가 있다고 믿는다

a/b/c.txt 는 사실 한 객체 키다. “폴더 이동” 같은 연산이 없다.


11. 한 줄로 정리

블록은 디스크, 파일은 공유 폴더, 객체는 HTTP — 자리가 다른 세 종류다


12. 이 장의 핵심 정리

  1. 블록(EBS) · 파일(EFS) · 객체(S3) 는 접근 방식 자체가 다르다.
  2. DB와 OS는 블록, 공유 파일은 파일, 나머지는 거의 다 객체다.
  3. 컨테이너 시대에는 영구 데이터를 외부 저장소로 옮긴다.
  4. 의심스러우면 S3가 보통 답이다.
  5. EFS는 DB나 대량 트랜잭션에는 어울리지 않는다.

56장. S3 — 객체 스토리지의 기본

이 장에서 말하고자 하는 것

앞 장에서 객체 스토리지의 개념을 봤다.

이제 그 대표 주자

Amazon S3 (Simple Storage Service)

를 본다.

S3는 AWS에서 가장 오래된 서비스이자 가장 많이 쓰이는 서비스다.


1. 기본 단위 — 버킷과 객체

버킷 "msa-uploads"
 ├─ users/123/profile.png
 ├─ orders/2025/01/15/order-123.json
 └─ static/app.a3f2c.js
  • 버킷(Bucket) : 글로벌 고유한 이름의 컨테이너
  • 객체(Object) : 키(경로) + 값(데이터) + 메타데이터

폴더는 사실 가짜다 — users/123/profile.png 는 한 객체의 키 문자열.


2. 버킷 이름은 글로벌

S3 버킷 이름은 전 세계에서 유일 해야 한다.

이미 누가 쓰는 이름 → 만들 수 없다

운영에서는 회사/조직 식별자 + 환경 + 용도 패턴을 쓴다.

acme-prod-uploads
acme-prod-logs
acme-dev-static

3. S3는 사실상 무제한

  • 객체 크기 최대 5TB
  • 버킷당 객체 수 제한 없음
  • 처리량 자동 확장

용량 걱정 없이 쓴다는 게 S3의 첫 번째 장점


4. 강한 일관성

2020년부터 S3는 read-after-write consistency 가 기본이다.

PUT 직후 GET → 방금 쓴 내용을 반드시 받는다

옛날에는 결국 일관성(eventual)이었지만 지금은 아니다.


5. 권한 — Public이 아니다, 항상

S3 버킷은 기본적으로 비공개다.

Public Access Block ON  ← 항상 켠다

권한은 두 방식으로 준다.

  • IAM 정책 — 누가(주체) 무엇을 할 수 있는가
  • 버킷 정책 — 이 버킷에 누가 접근할 수 있는가

6. 사전 서명 URL — 임시 권한

직접 노출하지 않고 객체에 일시 접근을 주는 방식이다.

서버 → 사용자에게 "5분 동안 유효한 URL" 전달
사용자 → 그 URL로 직접 S3 GET/PUT

다음 58장에서 자세히 본다.


7. CloudFront와 함께 — 정적 호스팅 표준

S3 (비공개, OAC)
  ↑
CloudFront (HTTPS, 캐시)
  ↑
사용자

이게 정적 자원의 표준 모양이다 (39장).


8. 우리 서비스에서

[S3 버킷들]
 ├─ static          → SPA 빌드 결과 (CloudFront 노출)
 ├─ uploads         → 사용자 업로드 (사전 서명 URL)
 ├─ ecs-logs        → ECS 로그 백업
 └─ data-archive    → 분석용 백업

용도별로 버킷을 분리한다.

  • 권한이 다르다
  • 수명 주기가 다르다
  • 비용 분석이 쉬워진다

9. 직접 확인해보기 — CLI

aws s3 mb s3://msa-uploads

aws s3 cp ./profile.png s3://msa-uploads/users/123/profile.png

aws s3 cp s3://msa-uploads/users/123/profile.png ./

aws s3 ls s3://msa-uploads/users/123/

aws s3 sync ./build s3://msa-static/

s3 sync 는 정적 배포에 가장 자주 쓴다.


10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_s3_bucket" "uploads" {
  bucket = "msa-prod-uploads"
}

resource "aws_s3_bucket_public_access_block" "uploads" {
  bucket = aws_s3_bucket.uploads.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "uploads" {
  bucket = aws_s3_bucket.uploads.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "uploads" {
  bucket = aws_s3_bucket.uploads.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

네 가지 (생성 + Public 차단 + 버전 + 암호화) 가 운영 출발선이다.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. Public Access Block을 끈다

대부분의 S3 노출 사고가 여기서 시작된다.

항상 켠다. 공개가 필요하면 CloudFront + OAC로

안티패턴 2. 같은 버킷에 모든 걸 섞는다

권한 · 수명 · 비용이 다 섞인다.

용도별 버킷 분리

안티패턴 3. versioning을 안 켠다

실수로 덮어쓰면 복구가 안 된다.

안티패턴 4. 사용자 업로드에 원본 파일명을 그대로 쓴다

충돌 · 인젝션 위험.

UUID / hash 로 키를 생성한다


12. 한 줄로 정리

S3는 글로벌 단위의 버킷 안에 객체를 HTTP API로 다루는 무제한 저장소다


13. 이 장의 핵심 정리

  1. S3의 단위는 버킷(글로벌 고유) + 객체(키 + 값) 다.
  2. 폴더는 가짜다. 키 문자열일 뿐.
  3. 2020년 이후 강한 일관성을 보장한다.
  4. Public Access Block은 항상 켜고, 공개는 CloudFront + OAC로.
  5. 운영 출발선: 버킷 + Public 차단 + 버전 + 암호화.
  6. 용도별 버킷을 분리한다.

57장. S3 스토리지 클래스와 수명 주기 — 비용 설계 관점

이 장에서 말하고자 하는 것

S3는 싸다. 그런데 한 가지 가격으로 다 같지는 않다.

S3는 객체마다 스토리지 클래스(Storage Class) 를 가질 수 있다.

자주 읽는 객체와 거의 안 읽는 객체에 같은 비용을 내면 손해다.


1. 주요 스토리지 클래스

클래스용도가격
Standard자주 읽음기본
Intelligent-Tiering접근 패턴 변함Standard 비슷 + 모니터링 비용
Standard-IA가끔 읽음Standard의 ~50%
One Zone-IA가끔 읽음, AZ 하나더 쌈 (가용성 낮음)
Glacier Instant Retrieval거의 안 읽음, 즉시 꺼냄매우 쌈
Glacier Flexible / Deep Archive장기 보관가장 쌈 (꺼낼 때 시간)

2. 어떤 객체에 어떤 클래스?

자주 읽는 사용자 데이터 → Standard
정적 자원 (CloudFront 캐시) → Standard 또는 Intelligent-Tiering
오래된 로그 / 백업 → Glacier
법적 보관용 / 5년 이상 → Deep Archive

3. Intelligent-Tiering — 패턴 모를 때

Intelligent-Tiering
  ↓ 30일간 접근 없음
자동으로 IA로 이동
  ↓ 90일간 접근 없음
자동으로 Archive Instant로 이동

AWS가 알아서 옮긴다.

운영에서 “기본값으로 켤만한” 클래스다


4. 수명 주기 정책 (Lifecycle Policy)

객체가 만들어진 뒤 시간이 지남에 따라 자동으로 옮기거나 삭제하는 규칙이다.

uploads/* 객체
  30일 후 → Standard-IA
  90일 후 → Glacier
  365일 후 → 삭제

운영 비용 최적화의 절반은 이 규칙 한 번 거는 일이다.


5. 버전 관리와 수명 주기

versioning이 켜져 있으면 옛 버전도 보관된다.

현재 버전: Standard
옛 버전:   30일 후 → IA
          90일 후 → Glacier
          365일 후 → 삭제

versioning을 켜고 수명 주기를 안 걸면 청구서가 끝없이 늘어난다.


6. 요청 비용도 무시할 수 없다

S3는 저장 비용 뿐 아니라 요청 비용 도 든다.

PUT · LIST: 비쌈
GET: 쌈

수많은 작은 객체에 LIST를 자주 호출하면 저장 비용보다 요청 비용이 더 클 수 있다.

작은 객체보다는 적절히 묶고, 자주 호출되는 정적 자원은 CloudFront로 흡수


7. 우리 서비스에서

uploads/ (사용자 업로드)
  ├─ 0~90일: Standard
  ├─ 90~365일: Standard-IA
  └─ 365일+ : Glacier

logs/ (ECS 로그 백업)
  ├─ 0~7일: Standard
  ├─ 7~30일: IA
  ├─ 30일+ : Glacier
  └─ 365일+ : 삭제

static/ (CloudFront 캐시)
  └─ Intelligent-Tiering

8. 직접 확인해보기 — CLI

aws s3api head-object \
  --bucket msa-uploads \
  --key users/123/profile.png \
  --query StorageClass

aws s3 cp s3://msa-uploads/key s3://msa-uploads/key \
  --storage-class STANDARD_IA \
  --metadata-directive REPLACE

9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_s3_bucket_lifecycle_configuration" "uploads" {
  bucket = aws_s3_bucket.uploads.id

  rule {
    id     = "tier-uploads"
    status = "Enabled"

    filter { prefix = "uploads/" }

    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }

    transition {
      days          = 90
      storage_class = "GLACIER"
    }

    expiration {
      days = 365
    }
  }

  rule {
    id     = "old-versions"
    status = "Enabled"

    filter {}

    noncurrent_version_transition {
      noncurrent_days = 30
      storage_class   = "STANDARD_IA"
    }

    noncurrent_version_expiration {
      noncurrent_days = 365
    }
  }
}

이 한 묶음이 비용 절감 효과의 대부분을 만든다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 모든 객체를 Standard로만 둔다

매달 비용 줄 기회를 놓친다.

안티패턴 2. Glacier에 둔 객체를 자주 꺼낸다

Glacier는 꺼낼 때 비용이 든다. 자주 꺼내면 Standard보다 비싸진다.

안티패턴 3. versioning은 켰는데 옛 버전 수명 주기는 안 건다

옛 버전이 영원히 쌓인다.

안티패턴 4. 작은 객체 수백만 개를 IA / Glacier로 보낸다

IA · Glacier는 최소 과금 크기(128KB) 가 있다. 작은 객체는 손해.


11. 한 줄로 정리

스토리지 클래스 + 수명 주기는 S3의 비용 절반을 결정하는 두 손잡이다


12. 이 장의 핵심 정리

  1. S3는 객체마다 클래스를 가진다 — 자주 vs 가끔 vs 거의 안 읽음.
  2. Intelligent-Tiering은 패턴 모를 때의 안전한 기본값이다.
  3. 수명 주기 정책으로 자동 이동 / 삭제를 건다.
  4. versioning과 옛 버전 수명 주기는 항상 함께 설계한다.
  5. 작은 객체에 IA/Glacier는 오히려 비싸질 수 있다.
  6. 요청 비용도 무시할 수 없다 — CloudFront로 흡수한다.

58장. 사전 서명 URL과 권한 모델

이 장에서 말하고자 하는 것

사용자가 큰 파일(이미지 · 동영상) 을 업로드한다.

① 사용자 → 우리 서버 → S3
② 사용자 → S3 직접

①은 서버 트래픽과 컴퓨팅을 잡아먹는다.
②는 가장 효율적이지만 “어떻게 임시로 권한을 줄 것인가” 가 문제다.

이걸 푸는 방식이

사전 서명 URL (Presigned URL)

이다.


1. 사전 서명 URL이란

서버가 S3에 대한 임시 권한이 담긴 URL을 만들어준다.

https://msa-uploads.s3.amazonaws.com/users/123/profile.png?
  X-Amz-Algorithm=...
  &X-Amz-Credential=...
  &X-Amz-Expires=300
  &X-Amz-Signature=...
  • 5분간만 유효
  • 이 객체에 대해 PUT (또는 GET) 만 허용
  • URL을 가진 사람은 그 동안만 직접 S3 접근 가능

2. 흐름

1. 사용자: "프로필 사진 올릴 거야"
2. 서버: 사전 서명 URL 생성 → 사용자에게 전달
3. 사용자 → 그 URL로 S3에 직접 PUT
4. 서버: (선택) S3 이벤트로 후처리

서버는 데이터 자체를 들고 다니지 않는다.

  • 트래픽 절감
  • 서버 부하 절감
  • 보안: 임시 권한이라 노출 위험 적음

3. PUT 용 / GET 용

GET 용: 비공개 객체를 잠시 다운로드 가능하게
PUT 용: 임시 업로드 슬롯

GET용은 “private 영상 일시 재생” 같은 용도에 쓴다.


4. POST Policy — 더 정교한 업로드 제어

PUT 사전 서명 URL은 한 객체에 대한 단순 권한이다.

업로드 시 제약 (크기 · Content-Type · 접두사) 이 필요하면 POST Policy 를 쓴다.

허용 조건:
  - 키가 uploads/user-{id}/ 로 시작
  - Content-Type은 image/*
  - 크기 5MB 이하

위반하는 업로드는 S3가 자체적으로 거부한다.


5. S3 권한 모델 — 세 층

S3 접근은 세 층의 검사를 모두 통과해야 한다.

1. IAM 정책 (호출자가 무엇을 할 수 있는가)
2. 버킷 정책 (이 버킷이 누구에게 무엇을 허용)
3. 객체 ACL (개별 객체 권한 — 거의 안 쓴다)

그리고 Public Access Block 으로 전체 노출이 한 번 더 막힌다.

기본은 모든 게 막혀 있고, 세 층 중 하나라도 거절하면 통과 안 된다.


6. CloudFront OAC와 사전 서명 URL의 차이

항목OAC (CloudFront)사전 서명 URL
대상모든 사용자특정 사용자
캐시가능거의 불가
만료없음짧음 (분 단위)
용도정적 공개 자원비공개 / 일시 접근

정적 공개 → CloudFront + OAC
사용자별 비공개 → 사전 서명 URL


7. 우리 서비스에서

[사용자] (프로필 사진 업로드)
   ↓ POST /api/uploads/presign
[API Gateway → ECS "uploads"]
   ↓ Task Role로 S3 PutObject 권한
   ↓ presigned URL 생성
[사용자] (URL 받음)
   ↓ PUT 그 URL (S3 직접)
[S3 버킷 uploads]
   ↓ S3 이벤트
[Lambda 또는 SQS] (썸네일 생성 등)

uploads 서비스는 데이터를 만지지 않는다. “URL 발급기” 역할만.


8. 직접 확인해보기 — CLI

aws s3 presign s3://msa-uploads/users/123/profile.png --expires-in 300

--expires-in 단위는 초.

curl --upload-file ./file.png "<presigned-url>"

로 PUT 하면 파일이 올라간다.


9. 코드로는 이렇게 생겼다 — Node.js

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: "ap-northeast-2" });

async function presign(key) {
  const command = new PutObjectCommand({
    Bucket: "msa-uploads",
    Key: key,
    ContentType: "image/png",
  });
  return getSignedUrl(s3, command, { expiresIn: 300 });  // 5분
}

ECS Task Role이 s3:PutObject 권한을 가지면 끝.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 만료 시간을 길게 둔다

URL이 새어 나가면 그 시간 동안 누구나 업로드 가능.

분 단위로 짧게 — 5~15분 정도

안티패턴 2. 큰 파일을 서버를 통해 받는다

API Gateway는 페이로드 크기 제한이 있다. ECS도 큰 파일은 메모리 부담.

큰 파일은 사전 서명 URL로 사용자 → S3 직접

안티패턴 3. POST Policy 제약을 안 건다

악성 사용자가 거대한 파일을 올려 비용 폭탄을 만들 수 있다.

안티패턴 4. 사전 서명 URL을 CloudFront에 노출한다

캐시되면 만료된 URL이 계속 응답된다.


11. 한 줄로 정리

사전 서명 URL은 사용자 ↔ S3 직접 통신에 짧은 임시 권한을 주는 방식이며,
큰 파일 업로드 · 비공개 자원 접근의 표준 도구다


12. 이 장의 핵심 정리

  1. 사전 서명 URL은 임시 S3 접근 권한이 담긴 URL이다.
  2. 사용자 → S3 직접 통신으로 서버 부담을 없앤다.
  3. PUT · GET 둘 다 가능하다.
  4. 제약이 필요하면 POST Policy.
  5. 만료는 짧게 — 보통 5~15분.
  6. CloudFront OAC와는 용도가 다르다 — 공개 vs 일시 비공개.

59장. CloudFront + S3로 정적 콘텐츠 서비스하기

이 장에서 말하고자 하는 것

CloudFront와 S3를 묶어
정적 자원 서비스의 표준 구성 을 만든다.

  • SPA (React/Vue/Next) 빌드 결과
  • 이미지 · CSS · JS
  • 다운로드 파일
  • 공개 미디어

부품은 38·39·56장에서 다 봤다.


1. 표준 구성도

[사용자]
   ↓ HTTPS, DNS (Route 53 ALIAS)
[CloudFront] (us-east-1 ACM, WAF)
   ↓ OAC
[S3 버킷] (비공개)
  • 사용자 응답 빠름 (엣지 캐시)
  • S3 노출 없음 (OAC)
  • HTTPS 자동 (CloudFront + ACM)

2. SPA 호스팅

SPA의 특징:

  • / 만 진짜 파일 — index.html
  • /login, /orders/123 같은 경로도 같은 index.html 을 받아야 함 (브라우저 라우터)

CloudFront에서 한 줄로 해결한다.

오류 응답 매핑:
  S3에서 404 NoSuchKey → CloudFront가 / 의 index.html 200 응답

3. 캐시 무효화 없이 새 버전 배포

invalidation에 의존하지 않는다. 대신 파일명 해시 패턴.

빌드 결과:
  index.html
  static/app.a3f2c1.js
  static/style.b91e8.css
  • index.html 은 캐시 짧게 (1분)
  • static/*.js, *.css 는 캐시 길게 (1년) — 새 빌드는 새 파일명

4. 도메인 한 개에 정적 + API

example.com
 ├─ /api/*    → API Gateway → ALB → ECS
 └─ /*        → S3 (SPA)

CloudFront 한 배포 안에서 두 origin을 가진다.

  • CORS 문제 없음
  • 사용자가 보기에 한 도메인

5. 우리 서비스에서

[Route 53] example.com → CloudFront
[CloudFront]
 ├─ default → S3 "static"
 │           - Cache 1년
 │           - 404 → / 의 index.html (SPA)
 ├─ /api/*  → API Gateway → ALB → ECS
 └─ /img/*  → S3 "img"
             - Cache 7일

6. 배포 흐름 — CI/CD에서

# 1. 빌드
npm run build

# 2. 정적 자원 (긴 캐시)
aws s3 sync ./dist s3://msa-static/ \
  --cache-control "public,max-age=31536000,immutable" \
  --exclude index.html

# 3. index.html (짧은 캐시)
aws s3 cp ./dist/index.html s3://msa-static/index.html \
  --cache-control "public,max-age=60"

# 4. (필요 시) invalidation
aws cloudfront create-invalidation \
  --distribution-id E123ABCD \
  --paths "/index.html"

invalidation은 index.html 한 줄로 끝. 정적 자원은 파일명이 바뀌어 자동 새 캐시.


7. 직접 확인해보기 — CLI

curl -I https://example.com/static/app.a3f2c1.js
# Cache-Control: public,max-age=31536000,immutable
# x-cache: Hit from cloudfront

curl -I https://example.com/
# Cache-Control: public,max-age=60

8. 코드로는 이렇게 생겼다 — Terraform (요약)

resource "aws_s3_bucket" "static" {
  bucket = "msa-prod-static"
}

resource "aws_cloudfront_origin_access_control" "s3" {
  name                              = "s3-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "main" {
  enabled = true
  aliases = ["example.com"]

  origin {
    domain_name              = aws_s3_bucket.static.bucket_regional_domain_name
    origin_id                = "s3"
    origin_access_control_id = aws_cloudfront_origin_access_control.s3.id
  }

  default_cache_behavior {
    target_origin_id       = "s3"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    cache_policy_id        = "658327ea-f89d-4fab-a63d-7e88639e58f6"  # CachingOptimized
  }

  custom_error_response {
    error_code         = 404
    response_code      = 200
    response_page_path = "/index.html"
  }

  custom_error_response {
    error_code         = 403
    response_code      = 200
    response_page_path = "/index.html"
  }

  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate.us_east.arn
    ssl_support_method  = "sni-only"
  }

  restrictions {
    geo_restriction { restriction_type = "none" }
  }
}

이게 정적 SPA 호스팅의 최소 운영 구성이다.


9. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. S3 정적 호스팅 엔드포인트를 그대로 쓴다

HTTPS · DDoS · 캐시 다 약하다.

항상 CloudFront 앞에

안티패턴 2. SPA 404 매핑을 안 한다

/orders/123 으로 직접 들어오면 흰 화면.

안티패턴 3. 모든 파일을 같은 캐시로 둔다

index.html 도 1년 캐시 → 새 배포가 안 보임.

index.html은 짧게, 해시 파일은 길게

안티패턴 4. 새 빌드마다 /* 전체 invalidation

비용 + 지연. 파일명 해시로 대체.


10. 한 줄로 정리

CloudFront + S3 (OAC) + 파일명 해시 + SPA 404 매핑이
정적 자원 서비스의 사실상 표준이다


11. 이 장의 핵심 정리

  1. CloudFront + S3 (OAC) 가 정적 콘텐츠의 표준 모양이다.
  2. SPA는 404 → index.html 200 매핑이 필요하다.
  3. 파일명 해시 + 차등 캐시로 invalidation 부담을 없앤다.
  4. 정적 + API를 한 도메인에 두면 CORS 문제도 사라진다.
  5. S3 정적 웹사이트 엔드포인트는 사실상 쓸 일이 없다.

60장. EFS · FSx — 공유 파일 시스템

이 장에서 말하고자 하는 것

S3는 객체 스토리지지, “공유 파일 시스템” 이 아니다.

여러 서버가 같은 폴더를 마운트해서 동시에 읽고 쓸 때 필요한 도구가

EFS · FSx

다.

  • 워드프레스 같은 PHP 앱
  • 머신러닝 데이터셋
  • 빌드 캐시 공유
  • 일부 미디어 처리 파이프라인

1. EFS는 무엇인가

EFS(Elastic File System)는

여러 EC2 / 컨테이너가 동시에 마운트하는 NFS

  • NFSv4 프로토콜
  • 멀티 AZ
  • 자동 확장 (용량 신경 안 써도 됨)
  • 사용한 만큼 과금

POSIX 파일 시스템처럼 동작한다.


2. FSx — 더 특수한 영역

  • FSx for Lustre — 고성능 HPC, 머신러닝
  • FSx for Windows — SMB (윈도우 공유)
  • FSx for NetApp ONTAP / OpenZFS — 엔터프라이즈 NAS 호환

일반 리눅스 서비스의 공유 파일은 EFS
윈도우 / 고성능 / NAS 호환은 FSx


3. EFS의 성능 모드와 처리량

성능 모드:
  General Purpose : 대부분 (지연 짧음)
  Max I/O         : 동시 접근 많음 (지연 약간 큼)

처리량 모드:
  Bursting        : 보통 워크로드
  Provisioned     : 일정한 고처리량
  Elastic         : 자동 (최근 권장)

처음에는 General Purpose + Elastic 으로 시작.


4. EFS의 스토리지 클래스

S3처럼 EFS도 클래스가 있다.

EFS Standard
EFS Standard-IA       (30일 미접근 자동 이동)
EFS One Zone
EFS One Zone-IA

수명 주기를 켜두면 거의 안 만지는 파일이 자동으로 IA로.


5. ECS / Fargate에서 EFS 쓰기

Fargate는 EFS를 직접 마운트할 수 있다.

Task Definition
  └─ volumes
      └─ efsVolumeConfiguration
           - fileSystemId: fs-xxxx
           - rootDirectory: /shared
  └─ containerDefinitions[].mountPoints
      └─ /app/data

컨테이너 안에서 /app/data 가 EFS의 /shared 가 된다.

다만 컨테이너 시대에 EFS는 자주 쓰는 도구는 아니다
대부분 S3 + RDS · DynamoDB 로 풀린다


6. 언제 쓰지 말아야 하는가

  • 데이터베이스 (EBS 또는 관리형 DB)
  • 매우 작은 IO를 매우 자주 (NFS 오버헤드 누적)
  • “그냥 파일 저장” — 이건 S3

EFS는 “공유” 가 핵심이다. 공유가 필요 없다면 EFS 쓸 이유가 없다


7. 우리 서비스에서

이 책의 척추 구조는 EFS를 기본으로 두지 않는다.

  • 사용자 업로드 → S3
  • 정적 자원 → S3
  • DB → RDS · DynamoDB
  • 로그 → CloudWatch / S3

EFS는 “공유 파일이 진짜 필요할 때” 이름을 떠올리는 도구


8. 직접 확인해보기 — CLI

aws efs create-file-system \
  --performance-mode generalPurpose \
  --throughput-mode elastic

aws efs create-mount-target \
  --file-system-id fs-xxxx \
  --subnet-id subnet-xxxx \
  --security-groups sg-xxxx

# EC2에서 마운트
sudo mount -t nfs4 -o nfsvers=4.1 \
  fs-xxxx.efs.ap-northeast-2.amazonaws.com:/ \
  /mnt/efs

9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_efs_file_system" "shared" {
  performance_mode = "generalPurpose"
  throughput_mode  = "elastic"

  lifecycle_policy {
    transition_to_ia = "AFTER_30_DAYS"
  }
}

resource "aws_efs_mount_target" "a" {
  file_system_id  = aws_efs_file_system.shared.id
  subnet_id       = aws_subnet.private_a.id
  security_groups = [aws_security_group.efs.id]
}

resource "aws_efs_mount_target" "b" {
  file_system_id  = aws_efs_file_system.shared.id
  subnet_id       = aws_subnet.private_b.id
  security_groups = [aws_security_group.efs.id]
}

각 AZ마다 Mount Target을 둬야 한다는 게 핵심.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. EFS를 DB 저장소로 쓴다

NFS의 fsync 동작이 DB와 충돌. 데이터 손상 가능.

안티패턴 2. Mount Target을 한 AZ만 둔다

다른 AZ의 컨테이너가 못 붙는다.

안티패턴 3. 보안 그룹을 잘못 잡는다

EFS SG는 NFS 포트(2049) 를 클라이언트 SG에서 허용해야 한다.

안티패턴 4. 작은 파일을 초고빈도로 읽는다

NFS 오버헤드 누적. 캐시 / 로컬 디스크가 맞다.


11. 한 줄로 정리

EFS는 여러 컴퓨트가 동시에 마운트하는 NFS 공유이며,
S3나 DB가 답이 안 될 때만 등장하는 도구다


12. 이 장의 핵심 정리

  1. EFS는 NFS 기반의 공유 파일 시스템이다.
  2. FSx는 Lustre · Windows · NetApp 같은 특수 옵션이다.
  3. 컨테이너 시대에는 대부분 S3로 풀린다.
  4. DB · 초고빈도 작은 IO에는 어울리지 않는다.
  5. Mount Target은 사용하는 모든 AZ에 둬야 한다.

61장. AWS 데이터베이스 지형 한눈에 보기

이 장에서 말하고자 하는 것

지금까지 우리는 ECS 위에 컨테이너를 띄우고
S3로 정적 자원을 다루는 흐름까지 만들었다.

이제 데이터 계층에 진입한다.

AWS의 데이터베이스는 단 한 종류가 아니다.

  • 관계형 DB (RDS / Aurora)
  • NoSQL (DynamoDB)
  • 인-메모리 캐시 (ElastiCache)
  • 검색 (OpenSearch)
  • 시계열 (Timestream)
  • 그래프 (Neptune)
  • 분석 (Redshift)

이 장은 그 지형을 한눈에 보고
어떤 워크로드에 무엇이 어울리는지 를 정리한다.


1. 가장 자주 쓰는 4가지

이 책의 척추에서는 다음 4개가 중심이다.

종류서비스핵심 용도
관계형RDS · Aurora정형 데이터, 트랜잭션, 조인
NoSQL (Key-Value)DynamoDB대규모 단순 조회, 무제한 확장
인-메모리ElastiCache (Redis)캐시, 세션, 실시간
검색OpenSearch전문 검색, 로그 분석

나머지 (Neptune, Timestream, Redshift 등) 는 특수 워크로드용이다.


2. 관계형이 어울리는 경우

정형 스키마 + 트랜잭션 + 조인이 자주 필요
  • 주문 · 결제 · 회계
  • 회원 정보
  • 재고 / 송장

데이터 사이의 관계가 명확하고, “한 번에 여러 테이블을 일관성 있게 바꾼다” 가 필요하면 관계형


3. DynamoDB가 어울리는 경우

"이 키로 이 값" 형태의 단순 조회가 대부분
초당 수만 ~ 수십만 건
응답 시간 한 자릿수 ms
  • 세션 / 토큰 저장소
  • 게임 점수 / 리더보드
  • 이벤트 로그 / 시계열에 가까운 데이터
  • 카탈로그 / 추천 결과

조인이 적고 무한 확장이 필요하면 DynamoDB


4. ElastiCache (Redis) 가 어울리는 경우

초저지연 (마이크로초 수준)
일시적 데이터 OK
  • 캐시 계층 (DB 앞단)
  • 세션 저장
  • Rate limit 카운터
  • 실시간 랭킹

“잠깐 빠르게” 가 필요하면 Redis


5. 어떻게 고르는가 — 의사결정 트리

조인 · 트랜잭션 자주 필요?
  └─ Yes → 관계형 (RDS / Aurora)
  └─ No
      └─ 키로 단순 조회만?
          └─ Yes → DynamoDB
          └─ No
              └─ 전문 검색?
                  └─ Yes → OpenSearch
              └─ 시계열?
                  └─ Yes → Timestream
              └─ ...

캐시 / 일시 데이터?
  └─ Yes → ElastiCache

6. 서비스별 DB 분리 — MSA의 핵심 원칙

orders   → orders 전용 DB
users    → users 전용 DB
payments → payments 전용 DB

여러 서비스가 같은 DB를 공유하면

  • 스키마 변경이 서로 깨진다
  • 한 서비스 부하가 다른 서비스에 번진다
  • 배포 속도가 묶인다

Database per Service — 70장에서 자세히 다룬다


7. 우리 서비스에서

[ECS Service: orders]    → RDS (PostgreSQL) — 트랜잭션·조인
[ECS Service: users]     → RDS (PostgreSQL)
[ECS Service: payments]  → RDS (PostgreSQL)
[ECS Service: catalog]   → DynamoDB — 무제한 조회
[ECS Service: sessions]  → ElastiCache (Redis)

이렇게 한 서비스의 DB가 다른 서비스의 부담이 되지 않는다.


8. 직접 확인해보기 — CLI

# RDS
aws rds describe-db-instances

# DynamoDB
aws dynamodb list-tables

# ElastiCache
aws elasticache describe-cache-clusters

각자의 인터페이스가 다르다 — 데이터 모델이 다르기 때문이다.


9. 코드로는 이렇게 생겼다 — Terraform (개요)

# 관계형
resource "aws_db_instance" "orders" {
  identifier     = "orders"
  engine         = "postgres"
  engine_version = "16"
  instance_class = "db.t4g.small"
}

# NoSQL
resource "aws_dynamodb_table" "catalog" {
  name         = "catalog"
  hash_key     = "id"
  billing_mode = "PAY_PER_REQUEST"

  attribute {
    name = "id"
    type = "S"
  }
}

# 캐시
resource "aws_elasticache_cluster" "sessions" {
  cluster_id      = "sessions"
  engine          = "redis"
  node_type       = "cache.t4g.small"
  num_cache_nodes = 1
}

각자 다음 장들에서 자세히 본다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. “RDS 하나로 다 한다”

서비스가 늘면 한 RDS가 병목이 된다.

서비스별로 DB를 분리한다

안티패턴 2. DynamoDB에 SQL식 사고방식을 그대로 적용

조인 · 임의 쿼리 · 복잡한 필터를 기대한다.
DynamoDB는 다른 사고방식이 필요하다 (66~68장).

안티패턴 3. 캐시를 영구 저장소처럼 쓴다

Redis 노드가 죽으면 데이터가 사라질 수 있다.

캐시는 캐시일 뿐 — 원본은 DB

안티패턴 4. 모든 워크로드에 같은 DB를 강요한다

“우리는 PostgreSQL만 쓴다” 같은 정책은 빠르게 한계에 부딪힌다.

워크로드에 맞는 DB를 고르는 게 운영 비용을 낮춘다


11. 한 줄로 정리

AWS 데이터베이스 지형은 한 종류가 아니며,
워크로드에 맞는 DB를 골라 서비스별로 분리하는 게 MSA의 토대다


12. 이 장의 핵심 정리

  1. 자주 쓰는 4가지: RDS · DynamoDB · ElastiCache · OpenSearch.
  2. 조인·트랜잭션 → 관계형 / 단순 조회·무제한 확장 → DynamoDB.
  3. 초저지연·일시 데이터 → ElastiCache.
  4. MSA에서는 Database per Service 가 원칙이다.
  5. 워크로드마다 다른 DB를 쓰는 게 운영적으로 자연스럽다.

62장. RDS — 관리형 관계형 DB의 구조

이 장에서 말하고자 하는 것

관계형 DB는 직접 설치해 운영할 수도 있다.

하지만 다음 일들이 따라온다.

  • OS · DB 엔진 패치
  • 백업 스크립트
  • 장애 복구
  • 디스크 확장
  • 모니터링

이 부담을 AWS가 가져가는 게

Amazon RDS (Relational Database Service)

다.

이 장은 RDS의 구조와 핵심 옵션을 본다.


1. RDS가 해주는 일

  • 엔진 설치 · 패치
  • 자동 백업 · 스냅샷
  • 장애 발생 시 자동 복구
  • Multi-AZ 동기 복제
  • Read Replica
  • 모니터링 (CloudWatch / Performance Insights)

사용자는 “엔진 + 사양 + 설정” 만 선언한다.


2. 지원하는 엔진

PostgreSQL
MySQL
MariaDB
Oracle
SQL Server

그리고 별도로

Aurora (MySQL 호환 · PostgreSQL 호환)

가 있다. Aurora는 65장에서 따로 다룬다.

새 프로젝트라면 PostgreSQL이 사실상 기본 선택지
(확장성 · 기능 · 커뮤니티 측면에서)


3. RDS의 핵심 구성 요소

DB Instance
  ├─ 엔진 · 버전
  ├─ Instance Class (db.m6g.large 등)
  ├─ Storage (gp3, io2, …)
  ├─ Multi-AZ 여부
  ├─ Subnet Group (어떤 서브넷에 둘 것인가)
  ├─ Parameter Group (DB 설정)
  ├─ Option Group (엔진별 부가 기능)
  └─ Security Group

4. Subnet Group — DB를 어디에 둘 것인가

RDS는 VPC 안의 프라이빗 서브넷 에 둔다.

[VPC]
 ├─ Public Subnet (ALB)
 └─ Private Subnet (ECS, RDS)

DB Subnet Group은 “DB가 살 수 있는 서브넷 묶음” 이다.
Multi-AZ를 쓰려면 여러 AZ의 서브넷 이 들어 있어야 한다.


5. 보안 그룹 — 누가 DB에 들어올 수 있는가

DB 보안 그룹은 보통

인바운드: ECS Task의 SG에서 오는 5432(PG) 또는 3306(MySQL)
아웃바운드: (필요 시) 외부 호출

DB는 절대 인터넷에 직접 노출하지 않는다
publicly_accessible = false


6. Parameter Group — DB 설정 묶음

max_connections, shared_buffers, work_mem 같은 엔진 파라미터를 묶어 관리.

  • 기본 그룹은 수정 불가
  • 운영 시에는 본인 그룹을 만들어 적용
  • 일부 파라미터는 재시작이 필요

7. 백업 — 두 가지가 함께 돈다

자동 백업

  • 매일 한 번
  • backup_retention_period 일수만큼 보관 (최대 35일)
  • 그 기간 안 어떤 시점으로도 복구 가능 (Point-in-time recovery)

수동 스냅샷

  • 사람이 명시적으로 만든다
  • 삭제할 때까지 보관

운영 DB는 자동 백업을 켜고, 큰 변경 직전에는 수동 스냅샷도 따로 찍는다


8. 우리 서비스에서

[VPC]
 ├─ Public Subnet → ALB
 └─ Private Subnet
     ├─ ECS Service "orders"
     └─ RDS PostgreSQL
         - Multi-AZ
         - 자동 백업 7일
         - 보안 그룹: orders Task SG만 허용

서비스마다 자기 DB를 가진다.


9. 직접 확인해보기 — CLI

aws rds create-db-instance \
  --db-instance-identifier orders \
  --engine postgres \
  --engine-version 16 \
  --db-instance-class db.t4g.small \
  --allocated-storage 50 \
  --storage-type gp3 \
  --master-username appadmin \
  --manage-master-user-password \
  --db-subnet-group-name private-subnets \
  --vpc-security-group-ids sg-xxx \
  --multi-az \
  --backup-retention-period 7

aws rds describe-db-instances --db-instance-identifier orders

10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_db_subnet_group" "main" {
  name       = "private-db"
  subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
}

resource "aws_security_group" "db" {
  name   = "db"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.task.id]
  }
}

resource "aws_db_instance" "orders" {
  identifier              = "orders"
  engine                  = "postgres"
  engine_version          = "16"
  instance_class          = "db.t4g.small"
  allocated_storage       = 50
  storage_type            = "gp3"

  username                = "appadmin"
  manage_master_user_password = true

  db_subnet_group_name    = aws_db_subnet_group.main.name
  vpc_security_group_ids  = [aws_security_group.db.id]

  multi_az                = true
  backup_retention_period = 7
  deletion_protection     = true
  skip_final_snapshot     = false

  publicly_accessible     = false
}

11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. publicly_accessible = true

DB가 인터넷에 노출된다. 거의 항상 사고로 이어진다.

무조건 false. 접근은 Session Manager 또는 Bastion으로

안티패턴 2. deletion_protection 안 켠다

실수로 운영 DB가 한 번에 사라질 수 있다.

안티패턴 3. master 비밀번호를 평문으로 박는다

Terraform state · 저장소에 평문이 남는다.

manage_master_user_password = true → AWS Secrets Manager로 자동 관리

안티패턴 4. 백업 보관 0일

복구 자체가 불가능. 운영은 최소 7일.


12. 한 줄로 정리

RDS는 관리형 관계형 DB이며, 사양 + 네트워크 + 백업 + Multi-AZ 가 운영의 4축이다


13. 이 장의 핵심 정리

  1. RDS는 엔진 설치 · 패치 · 백업 · 복구를 AWS가 가져간다.
  2. PostgreSQL이 새 프로젝트의 기본 선택지로 무난하다.
  3. RDS는 프라이빗 서브넷, publicly_accessible = false.
  4. Multi-AZ는 다음 장에서, Read Replica는 그다음 장에서 다룬다.
  5. 자동 백업 + deletion_protection + 비밀번호 Secrets Manager 관리가 출발선.

63장. Multi-AZ — 장애 대비의 기본

이 장에서 말하고자 하는 것

DB가 한 대만 있으면

그 DB가 죽음 → 서비스 전체 중단

이걸 해결하는 첫 단계가

Multi-AZ 배포

다.

이 장은 RDS Multi-AZ가 어떻게 동작하고
운영에서 무엇을 보장하는지를 본다.


1. Multi-AZ는 무엇인가

같은 DB를 다른 AZ에 한 대 더 띄워두고 동기 복제 한다.

[AZ-A: Primary (쓰기/읽기)]
       ↓ 동기 복제
[AZ-B: Standby (대기)]

평소에는 Primary만 트래픽을 받는다.
Primary가 죽으면 Standby가 자동으로 Primary가 된다.


2. 자동 페일오버

Primary 장애 감지
  ↓
DNS 레코드가 Standby를 가리키도록 자동 전환
  ↓
애플리케이션은 같은 엔드포인트로 계속 호출

페일오버는 보통 60 ~ 120초 안에 끝난다.

애플리케이션이 자동 재연결을 잘 처리하면 사용자는 잠깐의 끊김만 경험한다


3. 읽기 분산이 아니다

여기서 많이 헷갈리는 부분.

Multi-AZ Standby는 트래픽을 안 받는다

  • 읽기 요청도 Standby로 가지 않는다
  • 단지 장애 대비용으로만 동기 복제된다

읽기를 늘리고 싶다면 Read Replica 가 필요하다 (64장).


4. 비용

Multi-AZ는 사실상 DB를 두 대 띄우는 것과 같다.

컴퓨트 비용: 약 2배
스토리지 비용: 약 2배

운영 서비스에서는 무조건 켠다는 게 일반적이지만
개발 / 테스트 환경에서는 끄는 경우가 많다.


5. Multi-AZ vs Multi-AZ Cluster

RDS는 두 가지 방식의 Multi-AZ를 제공한다.

항목Multi-AZ (기본)Multi-AZ Cluster
노드 수2 (Primary + Standby)3 (Writer + 2 Reader)
Standby로 읽기
페일오버 속도60~120초더 빠름 (수십 초)
지원 엔진모든 RDS 엔진MySQL · PostgreSQL 일부

기본 Multi-AZ 로 시작 → 읽기 부하가 명백히 늘면 Read Replica 또는 Cluster 검토


6. 우리 서비스에서

[ECS "orders"]
   ↓
[RDS PostgreSQL]
 ├─ AZ-A  Primary   (active)
 └─ AZ-B  Standby   (동기 복제)
  • 평소: AZ-A 가 모든 트래픽
  • AZ-A 장애 시: 60~120초 안에 AZ-B 가 Primary

운영 DB는 무조건 Multi-AZ. 비용보다 가용성이 거의 항상 더 비싸다


7. 직접 확인해보기 — CLI

신규 DB에 Multi-AZ

aws rds create-db-instance \
  --db-instance-identifier orders \
  --engine postgres \
  --multi-az \
  ...

기존 DB에 Multi-AZ 켜기

aws rds modify-db-instance \
  --db-instance-identifier orders \
  --multi-az \
  --apply-immediately

이 변경은 다운타임 없이 켜진다.

페일오버 강제 (테스트)

aws rds reboot-db-instance \
  --db-instance-identifier orders \
  --force-failover

운영 환경에서 한 번은 실제로 시켜봐야 한다
페일오버가 정말 잘 동작하는지 / 애플리케이션이 잘 재연결하는지 확인


8. 코드로는 이렇게 생겼다 — Terraform

resource "aws_db_instance" "orders" {
  identifier     = "orders"
  engine         = "postgres"
  engine_version = "16"
  instance_class = "db.t4g.small"

  multi_az                = true   # ← 이 한 줄
  backup_retention_period = 7

  db_subnet_group_name    = aws_db_subnet_group.main.name
  vpc_security_group_ids  = [aws_security_group.db.id]

  publicly_accessible     = false
  deletion_protection     = true
}

multi_az = true 한 줄이 두 AZ에 동기 복제를 켠다.

DB Subnet Group이 두 AZ의 서브넷을 갖고 있어야 한다.


9. 애플리케이션 측 고려사항

페일오버가 일어나면 기존 연결은 끊어진다.

  • 커넥션 풀이 자동 재연결을 지원해야 한다
  • 쓰기 직후 트랜잭션은 실패할 수 있다 → 재시도 로직 필요
  • 읽기 전용 모드를 일부 구간에 두는 게 도움이 될 수 있다

운영 DB에 대한 클라이언트는 항상 “끊어질 수 있다” 를 전제로 설계


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 운영 DB에 Multi-AZ 안 켠다

한 AZ 장애로 서비스 전체가 멈춘다.

안티패턴 2. Standby로 읽기를 보낸다고 착각한다

Standby는 트래픽을 안 받는다. 읽기 분산은 Read Replica.

안티패턴 3. 페일오버를 한 번도 시험해보지 않는다

실제 장애 시 애플리케이션이 재연결을 못 해 더 큰 사고가 난다.

정기적으로 force-failover를 시뮬레이션한다

안티패턴 4. DB Subnet Group이 한 AZ만 가진다

Multi-AZ를 켜고 싶어도 안 켜진다.


11. 한 줄로 정리

Multi-AZ는 같은 DB를 다른 AZ에 동기 복제해 두는 장애 대비 구성이며,
운영 DB의 사실상 필수 옵션이다


12. 이 장의 핵심 정리

  1. Multi-AZ는 같은 DB를 다른 AZ에 동기 복제한 형태다.
  2. Primary 장애 시 60~120초 안에 자동 페일오버한다.
  3. Standby는 트래픽을 안 받는다 — 읽기 분산 아님.
  4. 비용은 약 2배지만 가용성이 거의 항상 더 비싸다.
  5. 페일오버는 운영에 들어가기 전 반드시 한 번 시험해본다.

64장. Read Replica — 읽기를 어떻게 늘릴까

이 장에서 말하고자 하는 것

Multi-AZ는 장애 대비를 해준다.
하지만 읽기 트래픽이 늘어나는 문제는 풀어주지 않는다.

사용자 늘어남
  → 읽기 쿼리 폭증
  → Primary DB CPU 100%

이때 등장하는 게

Read Replica

다.


1. Read Replica의 구조

[Primary]   ← 쓰기
   ↓ 비동기 복제
[Replica 1]   ← 읽기
[Replica 2]   ← 읽기
[Replica 3]   ← 읽기
  • 쓰기는 Primary로
  • 읽기는 Primary 또는 Replica로 분산

복제는 비동기 다.
Multi-AZ Standby가 동기인 것과 결정적으로 다르다.


2. Replication Lag — 약간의 지연

비동기 복제이므로 Replica는

항상 Primary보다 약간 뒤처질 수 있다

지연은 보통 수십 ms ~ 수 초 정도.

1. 사용자가 주문을 만든다 (Primary에 INSERT)
2. 곧장 "내 주문 목록" 요청 (Replica에서 SELECT)
3. Replica에 아직 반영 안 됨 → 새 주문이 안 보임

이 한 줄짜리 시나리오가 운영에서 자주 만난다.


3. 어떻게 다루는가

1. 강한 일관성이 필요한 읽기는 Primary로

주문 직후 사용자에게 보여줄 데이터 → Primary
대시보드 / 통계 / 검색 → Replica

2. 같은 트랜잭션 안에서는 같은 노드

쓰기 직후 같은 트랜잭션의 후속 읽기는 Primary로.

3. “내 데이터” 는 Primary로

자기가 방금 만든 데이터를 즉시 봐야 하는 화면은 Primary.


4. Read Replica 활용 패턴

패턴용도
대시보드 / 분석 쿼리무거운 SELECT 를 Replica로 격리
검색 / 통계약간 오래된 데이터로도 충분
Read-Heavy API다수의 Replica 로 수평 확장
다른 리전 ReplicaDR · 글로벌 응답성

5. Cross-Region Read Replica

다른 리전에도 Replica를 둘 수 있다.

[ap-northeast-2: Primary]
       ↓ 리전 간 복제
[us-east-1: Replica]

용도:

  • 글로벌 사용자에게 가까운 곳에서 읽기 응답
  • DR (장애 복구) — 리전 전체 장애에 대비

6. Replica 승격 (Promotion)

Primary에 문제가 생기면 Read Replica를 승격(promote) 해 새 Primary로 만들 수 있다.

aws rds promote-read-replica \
  --db-instance-identifier orders-replica-1

승격 후에는 더 이상 Replica가 아니다 (독립적인 DB).


7. 우리 서비스에서

[ECS "orders"]
   ↓ 쓰기·강한 읽기
[RDS Primary  (Multi-AZ)]
   ↓ 비동기 복제
[Replica 1]  ← 무거운 SELECT
[Replica 2]  ← 대시보드
  • Primary: 트랜잭션 · 강한 일관성 읽기
  • Replicas: 대시보드 · 통계 · 무거운 SELECT

읽기 쿼리가 한가하면 Replica는 0~1개로 시작.


8. 직접 확인해보기 — CLI

Read Replica 만들기

aws rds create-db-instance-read-replica \
  --db-instance-identifier orders-replica-1 \
  --source-db-instance-identifier orders \
  --db-instance-class db.t4g.small

Replication Lag 확인

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name ReplicaLag \
  --dimensions Name=DBInstanceIdentifier,Value=orders-replica-1 \
  --start-time 2026-01-01T00:00:00Z \
  --end-time   2026-01-02T00:00:00Z \
  --period 60 \
  --statistics Average

ReplicaLag 알람은 거의 항상 켠다


9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_db_instance" "orders" {
  identifier     = "orders"
  engine         = "postgres"
  instance_class = "db.t4g.small"
  multi_az       = true
  # ... 62장 코드와 동일
}

resource "aws_db_instance" "orders_ro" {
  identifier             = "orders-ro-1"
  replicate_source_db    = aws_db_instance.orders.identifier
  instance_class         = "db.t4g.small"
  publicly_accessible    = false
  vpc_security_group_ids = [aws_security_group.db.id]
}

Replica는 별도 엔드포인트를 가진다.


10. 애플리케이션에서 두 엔드포인트 다루기

DATABASE_URL=postgres://...primary.../app
DATABASE_RO_URL=postgres://...replica.../app

코드에서

if (isReadOnly && !needsStrongConsistency) → DATABASE_RO_URL
else → DATABASE_URL

ORM에 따라 read/write 분리를 자동화해주는 기능이 있다.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 강한 일관성이 필요한 읽기를 Replica로 보낸다

“방금 만든 데이터가 안 보이는” 버그가 난다.

안티패턴 2. ReplicaLag 알람을 안 건다

복제가 끊겼는데도 모른다 → Replica에서 옛 데이터만 응답.

안티패턴 3. Replica 한 대로 모든 읽기를 받는다

Replica가 죽으면 즉시 부하가 Primary로 몰린다.

운영 부하가 크다면 Replica도 2대 이상

안티패턴 4. Replica를 영원한 DR 백업으로만 둔다

DR이 목적이라면 Cross-Region Replica + 정기 페일오버 시험이 필요하다.
승격 절차를 한 번도 안 해봤다면 실전에서 실패한다.


12. 한 줄로 정리

Read Replica는 비동기 복제 기반으로 읽기 부하를 분산하는 도구이며,
강한 일관성이 필요한 읽기와 그렇지 않은 읽기를 구분하는 게 핵심이다


13. 이 장의 핵심 정리

  1. Read Replica는 비동기 복제 기반의 읽기 전용 DB다.
  2. Multi-AZ Standby와는 다르다 — Standby는 트래픽을 안 받는다.
  3. Replication Lag 때문에 강한 일관성 읽기는 Primary로 보낸다.
  4. Cross-Region Replica로 글로벌 응답성과 DR을 동시에 다룰 수 있다.
  5. ReplicaLag 알람은 거의 항상 켠다.
  6. 승격 절차는 사전에 시뮬레이션해 둔다.

65장. Aurora — 클라우드 네이티브 RDS

이 장에서 말하고자 하는 것

RDS는 기존 관계형 DB 엔진을 그대로 관리해주는 서비스다.

AWS는 한 발 더 나아가
“클라우드 환경에 맞춰 새로 설계한” 관계형 DB를 만들었다.

Amazon Aurora

다.

Aurora는 MySQL · PostgreSQL 과 호환 되지만
내부 구조는 완전히 다르다.


1. Aurora가 다른 점

스토리지가 분리되어 있다

일반 RDS:
  [DB 인스턴스] = 컴퓨트 + 스토리지 한 묶음

Aurora:
  [Writer · Reader 인스턴스]
       ↓ 공유
  [Aurora Storage Layer (10GB 단위로 6 AZ에 분산 복제)]
  • 스토리지가 자동 확장 (10GB → 최대 128TB)
  • 6중 복제 (3 AZ × 2)
  • 한 노드가 죽어도 스토리지는 살아 있음

Replica가 같은 스토리지를 본다

Writer ──┐
         ├─ 같은 스토리지를 공유
Reader 1 ┤
Reader 2 ┘
  • Replica 복제 지연이 매우 작다 (보통 < 10ms)
  • Replica 추가가 빠르다 (스토리지를 새로 만들지 않으므로)

2. RDS vs Aurora 핵심 비교

항목RDSAurora
호환 엔진PG · MySQL · Oracle 등PG · MySQL 호환
스토리지인스턴스에 붙음분리된 클러스터 스토리지
복제 지연수 초까지보통 < 10ms
Replica 추가 속도느림빠름
최대 스토리지64 TB (엔진별)128 TB
비용일반적으로 더 쌈약간 더 비쌈
페일오버60~120초30초 내외

3. Aurora Serverless v2

Aurora는 서버리스 옵션을 제공한다.

워크로드에 따라 ACU (Aurora Capacity Unit) 가 자동 확장
0.5 ACU ~ 128 ACU 범위
  • 트래픽 없으면 작게, 폭증하면 자동 확장
  • 개발/스테이징 · 들쭉날쭉한 워크로드에 적합

“들쭉날쭉” + “관계형” 이면 Aurora Serverless v2 가 답일 수 있다


4. Aurora Global Database

여러 리전에 클러스터를 두고 통합 관리할 수 있다.

[ap-northeast-2: Primary 리전]
       ↓ 빠른 복제 (보통 < 1초)
[us-east-1: Secondary 리전 (읽기)]
  • 리전 간 복제 지연이 매우 작다
  • 리전 전체 장애 시 다른 리전을 승격 (DR)

5. 어떤 걸 고를까 — RDS vs Aurora

간단하고 비용을 최대한 낮추고 싶다 → RDS
복제 지연을 짧게 두고 싶다       → Aurora
Replica를 빠르게 늘리고 싶다     → Aurora
들쭉날쭉한 워크로드             → Aurora Serverless v2
글로벌 / DR이 중요              → Aurora Global Database
Oracle · SQL Server 호환 필요   → RDS (Aurora 미지원)

6. 우리 서비스에서

이 책의 척추 구조에서는 두 선택지가 다 자연스럽다.

초기 단계: RDS PostgreSQL Multi-AZ
중대형 단계: Aurora PostgreSQL 호환으로 마이그레이션
글로벌 단계: Aurora Global Database

시작은 RDS, 자라면 Aurora — 두 단계 진화가 흔하다


7. 직접 확인해보기 — CLI

Aurora 클러스터 만들기

aws rds create-db-cluster \
  --db-cluster-identifier orders-aurora \
  --engine aurora-postgresql \
  --engine-version 16 \
  --master-username appadmin \
  --manage-master-user-password \
  --db-subnet-group-name private-subnets \
  --vpc-security-group-ids sg-xxx

Writer · Reader 인스턴스 추가

aws rds create-db-instance \
  --db-instance-identifier orders-aurora-writer \
  --db-cluster-identifier orders-aurora \
  --engine aurora-postgresql \
  --db-instance-class db.r6g.large

aws rds create-db-instance \
  --db-instance-identifier orders-aurora-reader-1 \
  --db-cluster-identifier orders-aurora \
  --engine aurora-postgresql \
  --db-instance-class db.r6g.large

8. 코드로는 이렇게 생겼다 — Terraform

resource "aws_rds_cluster" "orders" {
  cluster_identifier      = "orders-aurora"
  engine                  = "aurora-postgresql"
  engine_version          = "16"

  master_username             = "appadmin"
  manage_master_user_password = true

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.db.id]

  backup_retention_period = 7
  deletion_protection     = true
}

resource "aws_rds_cluster_instance" "writer" {
  identifier         = "orders-aurora-writer"
  cluster_identifier = aws_rds_cluster.orders.id
  engine             = aws_rds_cluster.orders.engine
  instance_class     = "db.r6g.large"
  publicly_accessible = false
}

resource "aws_rds_cluster_instance" "reader" {
  identifier         = "orders-aurora-reader-1"
  cluster_identifier = aws_rds_cluster.orders.id
  engine             = aws_rds_cluster.orders.engine
  instance_class     = "db.r6g.large"
  publicly_accessible = false
}

Aurora 클러스터의 두 엔드포인트:

  • cluster_endpoint — Writer
  • reader_endpoint — Reader 로드 밸런싱

9. 두 엔드포인트의 운영

Writer 엔드포인트  → 쓰기 + 강한 일관성 읽기
Reader 엔드포인트  → 읽기 (여러 Reader에 분산)

Writer 페일오버가 일어나면 cluster_endpoint가 자동으로 새 Writer를 가리킨다.

애플리케이션이 cluster_endpoint를 쓰면 페일오버에 따로 신경 쓸 게 거의 없다


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. Aurora에 RDS와 똑같은 사고로 접근

Aurora의 빠른 Replica · Serverless · Global 같은 강점을 안 쓰면 그냥 더 비싼 RDS가 된다.

안티패턴 2. Reader 엔드포인트로 강한 일관성 읽기를 보낸다

복제 지연이 작아도 0은 아니다.

방금 만든 데이터를 즉시 보는 화면은 Writer로

안티패턴 3. Aurora Serverless v1 을 새로 시작한다

v1은 사실상 deprecated. 신규는 무조건 v2.

안티패턴 4. Aurora 클러스터를 한 AZ에만 둔다

서브넷 그룹이 한 AZ만 가지면 Aurora의 6중 복제 의미가 줄어든다.


11. 한 줄로 정리

Aurora는 스토리지가 분리된 클라우드 네이티브 관계형 DB이며,
복제 지연 · 빠른 Replica · 서버리스 · 글로벌이 RDS와 결정적으로 다른 점이다


12. 이 장의 핵심 정리

  1. Aurora는 RDS와 호환되지만 내부 구조가 다른 DB다.
  2. 스토리지가 분리돼 있어 Replica 추가가 빠르고 복제 지연이 작다.
  3. Aurora Serverless v2는 들쭉날쭉한 워크로드에 적합하다.
  4. Aurora Global Database로 리전 간 복제·DR을 다룰 수 있다.
  5. 시작은 RDS, 자라면 Aurora 가 흔한 진화 경로다.
  6. cluster_endpoint / reader_endpoint 두 엔드포인트 운영이 표준이다.

66장. DynamoDB의 사고방식 — Key-Value · 파티션 키

이 장에서 말하고자 하는 것

지금까지 본 RDS · Aurora는 관계형 DB다.

테이블에 행 · 열 · 관계 · 트랜잭션이 있다

DynamoDB는 다른 세계다.

키로 단순히 값을 꺼낸다
그 대신 무제한에 가까운 확장과 한 자릿수 ms 응답을 보장한다

이 장은 DynamoDB의 사고방식을 잡는다.
구문 이전에 모델이 다르다.


1. DynamoDB는 무엇인가

  • AWS의 완전 관리형 NoSQL DB
  • 키-값 · 문서 모델
  • 서버를 관리할 필요 없음
  • 처리량을 자동으로 확장 가능
  • 단일 자릿수 밀리초 응답 (보통)
  • 멀티 AZ 자동 복제

2. 가장 큰 사고방식 차이

관계형:    "이 조건을 만족하는 행을 다 줘"
DynamoDB: "이 키를 가진 항목을 줘"

DynamoDB는 임의 검색을 잘 못 한다.
그 대신 키 기반 조회를 압도적으로 잘 한다.

DynamoDB 모델링의 70%는 “어떤 키로 무엇을 꺼낼지” 를 미리 정하는 일이다


3. Partition Key — 데이터를 어디에 둘지 결정

DynamoDB는 데이터를 여러 파티션 에 흩어서 저장한다.

파티션 키 "user_id"
   ↓ 해시
어떤 파티션에 저장될지 결정

같은 파티션 키 값을 가진 항목은 같은 파티션에 모인다.

user_id = "u-1"  →  Partition A
user_id = "u-2"  →  Partition B
user_id = "u-3"  →  Partition A   (해시가 같은 곳)

4. Sort Key — 같은 파티션 안에서 정렬

파티션 키 하나만 쓰면 한 파티션에 항목 하나밖에 못 둔다.

대부분의 테이블은 파티션 키 + 정렬 키 의 복합 키를 쓴다.

Primary Key (user_id, created_at)

user_id = "u-1", created_at = "2026-01-01" → 항목 A
user_id = "u-1", created_at = "2026-01-02" → 항목 B
user_id = "u-1", created_at = "2026-01-03" → 항목 C

이렇게 두면

“u-1 의 최근 10개 항목” 같은 쿼리가 가능해진다


5. 두 가지 조회 방식

GetItem — 키 하나로 한 항목

GetItem (user_id="u-1", created_at="2026-01-02") → 항목 B

가장 빠른 조회. O(1).

Query — 같은 파티션 안의 여러 항목

Query (user_id="u-1", created_at BETWEEN "2026-01-01" AND "2026-01-31")
→ user_id=u-1 의 1월 항목들

파티션 키는 반드시 정확히 지정해야 한다.

Scan — 모든 파티션 훑기

Scan (모든 항목 검사)

거의 항상 안 쓴다. 비싸고 느리다.

운영에서 Scan은 “데이터 마이그레이션 같은 일회성” 에만


6. Hot Partition — 가장 흔한 문제

파티션 키 선택이 잘못되면 한 파티션에 부하가 몰린다.

파티션 키 = "country"
  ↓
"KR" 파티션에 모든 한국 트래픽이 몰림 → 처리량 한계

파티션 키는 카디널리티가 높은(고유값이 많은) 필드 가 좋다
user_id · order_id · device_id 같은 것


7. 우리 서비스에서

[ECS Service "catalog"]
   ↓
[DynamoDB Table "products"]
  Primary Key: (product_id)
  
[ECS Service "sessions"]
   ↓
[DynamoDB Table "sessions"]
  Primary Key: (session_id)
  TTL on expires_at
  • 단순 키 조회 · 무제한 확장 · 일정한 응답 → DynamoDB가 자연스럽다

8. 직접 확인해보기 — CLI

테이블 만들기

aws dynamodb create-table \
  --table-name orders \
  --attribute-definitions \
      AttributeName=user_id,AttributeType=S \
      AttributeName=created_at,AttributeType=S \
  --key-schema \
      AttributeName=user_id,KeyType=HASH \
      AttributeName=created_at,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST

항목 넣기

aws dynamodb put-item \
  --table-name orders \
  --item '{
    "user_id":    {"S": "u-1"},
    "created_at": {"S": "2026-01-01"},
    "total":      {"N": "12000"}
  }'

조회

aws dynamodb query \
  --table-name orders \
  --key-condition-expression "user_id = :uid" \
  --expression-attribute-values '{":uid": {"S": "u-1"}}'

9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_dynamodb_table" "orders" {
  name         = "orders"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "user_id"
  range_key    = "created_at"

  attribute {
    name = "user_id"
    type = "S"
  }

  attribute {
    name = "created_at"
    type = "S"
  }

  point_in_time_recovery {
    enabled = true
  }

  server_side_encryption {
    enabled = true
  }
}

PAY_PER_REQUEST 는 사용량 기반 과금. 시작에 좋다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. SQL 식 사고로 모델링한다

“정규화” 하면 조회마다 N번 Get을 부르게 된다.

DynamoDB는 읽는 모양에 맞춰 데이터를 미리 배치 한다
“쓰기에서 일을 해서 읽기를 단순하게”

안티패턴 2. 카디널리티 낮은 필드를 파티션 키로

hot partition 발생. 한 파티션 부하가 한계에 닿는다.

안티패턴 3. Scan을 일상적으로 쓴다

비용 · 지연 다 폭발한다.

Scan은 일회성 마이그레이션이나 분석용으로만

안티패턴 4. Point-in-time recovery를 안 켠다

실수로 데이터를 지웠을 때 복구가 안 된다.

운영 테이블은 무조건 PITR 켜기


11. 한 줄로 정리

DynamoDB는 키로 값을 꺼내는 모델이며,
“어떻게 읽을지” 를 먼저 정하고 그에 맞춰 데이터를 배치하는 사고방식이다


12. 이 장의 핵심 정리

  1. DynamoDB는 키-값/문서 모델의 NoSQL이다.
  2. Partition Key는 데이터를 어디에 둘지, Sort Key는 그 안의 순서를 결정한다.
  3. GetItem · Query 는 빠르고, Scan 은 거의 안 쓴다.
  4. 카디널리티 높은 필드를 파티션 키로 잡아 hot partition을 피한다.
  5. “읽는 모양에 맞춰 데이터를 미리 배치한다” 가 모델링의 핵심이다.
  6. 운영 테이블은 PITR + 암호화가 출발선이다.

67장. DynamoDB 인덱스 — LSI · GSI

이 장에서 말하고자 하는 것

앞 장에서 우리는 DynamoDB가
“파티션 키 + 정렬 키” 로 데이터를 꺼낸다는 걸 봤다.

그런데 운영을 하다 보면 같은 데이터에 대해
다른 키로 조회하고 싶을 때 가 생긴다.

orders 테이블
  Primary Key: (user_id, created_at)

질문 1: "u-1 사용자의 주문 목록" → Primary Key로 가능
질문 2: "status='paid' 인 주문 목록" → Primary Key로 불가능
질문 3: "상품 id로 주문 검색" → 불가능

이걸 푸는 도구가

인덱스 (Index)

다.

DynamoDB는 두 종류의 인덱스를 제공한다.

  • LSI (Local Secondary Index)
  • GSI (Global Secondary Index)

1. 큰 그림

테이블의 Primary Key      = "주된 조회 경로"
인덱스(LSI / GSI)         = "추가 조회 경로"

인덱스는 원본 테이블의 읽기 전용 사본 처럼 동작한다.

DynamoDB가 자동으로 동기화해준다.


2. LSI — Local Secondary Index

같은 파티션 키를 공유하면서
다른 정렬 키 로 한 번 더 보고 싶을 때.

원본:    (user_id, created_at)
LSI:     (user_id, total)
"u-1의 주문을 금액 큰 순으로" → LSI로 Query

특징:

  • 파티션 키는 원본과 같다
  • 테이블 만들 때만 생성 가능 (나중에 추가 불가)
  • 같은 파티션 키 안에서 최대 10GB
  • 사용 빈도가 점점 줄어들고 있다 (GSI가 더 유연)

3. GSI — Global Secondary Index

완전히 다른 파티션 키 + 정렬 키 로 한 번 더 본다.

원본:  (user_id, created_at)
GSI:   (status, created_at)
"status='paid' 인 주문을 최신순으로" → GSI로 Query

특징:

  • 파티션 키도 정렬 키도 자유롭게 선택
  • 언제든 추가 · 삭제 가능
  • 별도의 처리량 / 비용
  • 비동기 복제 (약간의 지연 가능)

운영에서 추가 조회 경로가 필요하면 거의 항상 GSI


4. 인덱스에 무엇을 담을지

GSI를 만들 때 프로젝션(projection) 을 고른다.

프로젝션담는 데이터비용
KEYS_ONLY키 + 인덱스 키만가장 쌈
INCLUDE키 + 지정 속성보통
ALL모든 속성가장 비쌈, 가장 편함

매번 원본 테이블을 다시 GetItem 해야 한다면 ALL을 검토 — IO 절감


5. 인덱스 비용 모델

GSI는

  • 별도의 쓰기 용량 (원본에 쓸 때 인덱스에도 쓰기)
  • 별도의 읽기 용량
  • 별도의 스토리지

를 가진다.

인덱스 한 개 추가 = 쓰기 처리량이 ~2배가 될 수 있음

설계 단계에서 인덱스 수를 줄이는 게 중요하다.


6. “Single Table Design” 의 핵심

DynamoDB 고급 패턴 중 하나.

한 테이블 안에 여러 종류의 엔티티를 담고
GSI로 다양한 접근 경로를 만든다

PK = "USER#u-1"     SK = "PROFILE"      → 사용자 프로필
PK = "USER#u-1"     SK = "ORDER#o-1"    → 주문
PK = "USER#u-1"     SK = "ORDER#o-2"    → 주문
PK = "ORDER#o-1"    SK = "META"         → 주문 메타

이렇게 키를 설계해 “한 번에 사용자 + 모든 주문” 같은 조회를 단일 Query로 끝낸다.

초보자에게 권장하기는 어려운 패턴이지만,
DynamoDB가 진가를 발휘하는 구간이다.


7. 우리 서비스에서

[DynamoDB "orders"]
  PK: user_id
  SK: created_at

GSI: "by-status"
  PK: status         (paid, shipped, cancelled)
  SK: created_at

GSI: "by-product"
  PK: product_id
  SK: created_at
  • 기본 조회: 사용자별 주문
  • “결제 완료 주문 큐” → GSI by-status
  • “이 상품 누가 샀나” → GSI by-product

8. 직접 확인해보기 — CLI

GSI 추가

aws dynamodb update-table \
  --table-name orders \
  --attribute-definitions AttributeName=status,AttributeType=S AttributeName=created_at,AttributeType=S \
  --global-secondary-index-updates \
    '[{"Create":{"IndexName":"by-status","KeySchema":[{"AttributeName":"status","KeyType":"HASH"},{"AttributeName":"created_at","KeyType":"RANGE"}],"Projection":{"ProjectionType":"ALL"}}}]'

GSI로 Query

aws dynamodb query \
  --table-name orders \
  --index-name by-status \
  --key-condition-expression "#s = :s" \
  --expression-attribute-names '{"#s": "status"}' \
  --expression-attribute-values '{":s": {"S": "paid"}}'

9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_dynamodb_table" "orders" {
  name         = "orders"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "user_id"
  range_key    = "created_at"

  attribute {
    name = "user_id"
    type = "S"
  }
  attribute {
    name = "created_at"
    type = "S"
  }
  attribute {
    name = "status"
    type = "S"
  }
  attribute {
    name = "product_id"
    type = "S"
  }

  global_secondary_index {
    name            = "by-status"
    hash_key        = "status"
    range_key       = "created_at"
    projection_type = "ALL"
  }

  global_secondary_index {
    name            = "by-product"
    hash_key        = "product_id"
    range_key       = "created_at"
    projection_type = "ALL"
  }
}

10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 모든 컬럼마다 GSI를 만든다

쓰기 비용이 곱절로 늘어난다.

정말 필요한 조회 경로만 인덱스로

안티패턴 2. GSI에 카디널리티 낮은 키 (예: gender)

hot partition으로 이어진다.

안티패턴 3. GSI의 일관성에 강한 기대를 한다

GSI는 비동기 복제다 — 약간 지연될 수 있다.

강한 일관성이 필요한 조회는 원본 테이블 Query

안티패턴 4. LSI를 새로 시작하는 테이블에 우선적으로 쓴다

LSI는 제약이 많다 (생성 후 추가 불가, 10GB 한도).
GSI 가 거의 항상 더 유연하다.


11. 한 줄로 정리

GSI는 DynamoDB에서 추가 조회 경로를 만드는 도구이며,
비용·지연을 의식하면서 “정말 필요한 인덱스만” 두는 게 핵심이다


12. 이 장의 핵심 정리

  1. LSI는 같은 파티션 키 + 다른 정렬 키.
  2. GSI는 완전히 다른 키 조합 — 더 유연하다.
  3. 인덱스는 원본의 읽기 전용 사본처럼 동작한다.
  4. 인덱스 하나가 쓰기 처리량을 ~2배로 늘릴 수 있다.
  5. GSI는 비동기 복제다 — 강한 일관성이 필요하면 원본을 쓴다.
  6. Single Table Design은 강력하지만 학습 곡선이 있다.

68장. DynamoDB의 용량 모델과 비용

이 장에서 말하고자 하는 것

DynamoDB는 “초당 얼마나 처리할 수 있는지” 를 두 가지 모델로 다룬다.

  • On-Demand (PAY_PER_REQUEST)
  • Provisioned

이 선택이 비용과 운영 성격을 결정한다.


1. On-Demand — 가장 단순

요청한 만큼 자동 처리
사용한 만큼 청구
  • 트래픽을 미리 예측할 필요 없다
  • 폭증 트래픽에도 자동 대응
  • 단위 비용은 Provisioned 보다 비쌈

새 프로젝트는 거의 항상 On-Demand로 시작한다.


2. Provisioned — 미리 정해두기

RCU (Read Capacity Unit)  : 초당 읽기 단위
WCU (Write Capacity Unit) : 초당 쓰기 단위

미리 RCU · WCU를 잡아두고 그 안에서 처리한다.

  • 단위 비용이 싸다
  • 정해진 한도를 넘으면 throttle (요청 거부)
  • Auto Scaling으로 동적 조정 가능

트래픽이 일정하거나 충분히 예측되는 운영 환경에서만 권장


3. RCU · WCU 의 의미

대략적인 단위.

1 RCU = 초당 4KB 이하 항목 1번 강한 일관성 읽기
       또는 초당 4KB 이하 항목 2번 결과적 일관성 읽기
1 WCU = 초당 1KB 이하 항목 1번 쓰기

큰 항목은 더 많은 단위를 잡아먹는다.

항목을 작게 유지하면 비용이 직접적으로 줄어든다


4. Throttle — 한도를 넘었을 때

Provisioned에서 RCU/WCU를 넘으면 ProvisionedThroughputExceededException 으로 거부된다.

이 시점에 일부 요청이 실패한다

클라이언트는 지수 백오프 (Exponential Backoff) 로 재시도.

운영 알람: throttle 발생 시 즉시 알람


5. Hot Partition과 처리량

DynamoDB는 처리량을 파티션마다 나눠 가진다.

전체 1,000 WCU
파티션 10개 → 한 파티션당 100 WCU

한 파티션 키에 트래픽이 몰리면 그 100 WCU를 넘는 순간 throttle.

카디널리티 높은 파티션 키가 결정적으로 중요한 이유


6. 비용을 가르는 항목

DynamoDB 청구서의 주요 항목:

  • RCU/WCU (또는 On-Demand 요청 수)
  • 스토리지
  • 백업 (PITR + 스냅샷)
  • GSI · LSI (별도 청구)
  • DynamoDB Streams (이벤트 스트림 켤 때)
  • Global Table (멀티 리전 복제)

7. 비용 절감 패턴

1. TTL (Time To Live)

만료 시각이 있는 데이터는 자동 삭제.

세션 토큰: 1시간 후 삭제
캐시 항목: 24시간 후 삭제
aws dynamodb update-time-to-live \
  --table-name sessions \
  --time-to-live-specification "Enabled=true,AttributeName=expires_at"

TTL은 무료다. 안 쓸 이유가 거의 없다

2. 항목 크기 줄이기

긴 필드명을 짧게.

"customer_email" → "ce"

서비스 코드에 매핑 레이어를 두고 저장 형식만 짧게 한다.

3. Eventual Consistency 활용

가능한 곳은 결과적 일관성으로 → RCU 절반.

4. On-Demand → Provisioned 전환

트래픽이 안정되면 Provisioned + Auto Scaling 으로 옮긴다.


8. 우리 서비스에서

[Table "sessions"]
  - On-Demand
  - TTL on expires_at (1시간 후 삭제)

[Table "products"]
  - On-Demand 시작
  - 트래픽 안정되면 Provisioned + Auto Scaling

[Table "events"]
  - On-Demand
  - TTL on expires_at (90일 후 삭제)

세션 · 이벤트 같은 짧게 사는 데이터에 TTL을 거는 게 큰 절감.


9. 직접 확인해보기 — CLI

Provisioned 로 변경

aws dynamodb update-table \
  --table-name orders \
  --billing-mode PROVISIONED \
  --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=10

Auto Scaling 등록

aws application-autoscaling register-scalable-target \
  --service-namespace dynamodb \
  --resource-id "table/orders" \
  --scalable-dimension dynamodb:table:WriteCapacityUnits \
  --min-capacity 5 \
  --max-capacity 100

Throttle 메트릭

aws cloudwatch get-metric-statistics \
  --namespace AWS/DynamoDB \
  --metric-name ThrottledRequests \
  --dimensions Name=TableName,Value=orders \
  --start-time 2026-01-01T00:00:00Z \
  --end-time   2026-01-02T00:00:00Z \
  --period 60 \
  --statistics Sum

10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_dynamodb_table" "sessions" {
  name         = "sessions"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "session_id"

  attribute {
    name = "session_id"
    type = "S"
  }

  ttl {
    enabled        = true
    attribute_name = "expires_at"
  }

  point_in_time_recovery {
    enabled = true
  }
}

ttl 한 블록이 큰 비용 절감을 만든다.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 처음부터 Provisioned로 시작한다

트래픽 예측이 어렵다. throttle로 사고난다.

시작은 On-Demand → 안정 후 Provisioned

안티패턴 2. TTL을 안 켠다

세션 · 캐시 · 임시 데이터가 영원히 쌓인다.

안티패턴 3. 항목 하나에 거대한 페이로드를 박는다

1 WCU = 1KB. 100KB 항목 = 100 WCU.

큰 데이터는 S3로 → DynamoDB에는 메타만

안티패턴 4. throttle 알람 없이 운영

요청이 거부되는데도 모른다.


12. 한 줄로 정리

DynamoDB 비용은 처리량 모델 + 항목 크기 + 인덱스 + TTL 로 결정되며,
시작은 On-Demand + TTL 이 거의 항상 정답이다


13. 이 장의 핵심 정리

  1. 처리량 모델은 On-Demand · Provisioned 두 가지다.
  2. 새 프로젝트는 On-Demand로 시작한다.
  3. RCU/WCU 단위로 비용이 매겨진다 — 항목 크기가 직접적인 영향.
  4. Hot Partition은 처리량 한도를 결정적으로 끌어내린다.
  5. TTL · 항목 크기 줄이기 · Eventual Consistency 가 비용 절감의 세 축.
  6. throttle 알람은 운영 기본값.

69장. ElastiCache (Redis) — 캐시 계층의 이해

이 장에서 말하고자 하는 것

데이터베이스로 부족한 게 하나 더 있다.

“수십 ms 가 아니라 한 자릿수 ms로 응답해야 한다”

DB는 디스크 기반이라 일정 한계가 있다.
이때 등장하는 게

인-메모리 데이터 저장소

이고 AWS의 관리형 옵션이

Amazon ElastiCache (Redis · Valkey · Memcached)

다.

대부분의 운영에서 Redis (또는 Valkey) 를 쓴다.


1. 캐시가 풀어주는 문제

같은 데이터를 자꾸 읽을 때

사용자가 매번 "내 프로필" 요청
  ↓
매번 DB Query → 부하 누적

캐시를 끼우면

요청 → 캐시 확인
  └─ 있음 (HIT) → 즉시 응답
  └─ 없음 (MISS) → DB Query → 캐시에 저장 → 응답

같은 데이터는 그 뒤로 캐시에서 바로 응답된다.


2. ElastiCache가 잘하는 다른 일들

  • 세션 저장 (서버 무상태 유지)
  • Rate limit 카운터 (분 단위 요청 수)
  • 실시간 랭킹 / 리더보드
  • Pub/Sub 메시징
  • 분산 락

Redis는 단순 키-값을 넘는 다양한 자료구조를 지원한다.

String · Hash · List · Set · Sorted Set · Stream · ...

3. 클러스터 vs 단일 노드

단일 노드 (Standalone)

  • Primary 1 + Replica N
  • 메모리 한도 = 한 노드의 메모리
  • 단순하다

Cluster Mode Enabled

  • 데이터를 여러 샤드에 분산
  • 메모리 한도 = N × 노드 메모리
  • 클라이언트가 cluster를 인식해야 함
처음에는 단일 노드 + Replica 1~2개로 시작  
데이터가 한 노드 메모리 한도에 가까워지면 Cluster Mode 검토

4. 휘발성 — 영구 저장소가 아니다

Redis는 메모리 기반이다.

  • 노드 재시작 → 데이터 사라짐 가능
  • AOF · 스냅샷 옵션이 있지만 RDB · DynamoDB 수준의 보존을 기대하면 안 된다

캐시는 캐시일 뿐 — 원본은 DB

캐시가 텅 비어도 (Cold start) 시스템이 살아남아야 한다.


5. 캐시 패턴

Cache-Aside (가장 흔함)

1. 캐시 확인
2. MISS → DB 조회 → 캐시에 저장 → 반환
3. HIT → 캐시에서 바로 반환

Write-Through

쓸 때 DB + 캐시 동시에

Write-Behind

캐시에만 즉시 쓰고 DB는 비동기로
(데이터 손실 위험)

운영의 80%는 Cache-Aside로 시작.


6. TTL — 데이터 유효 기간

캐시 항목마다 만료 시간을 둔다.

SET user:u-1 "..." EX 300   ← 5분 후 자동 삭제
  • 너무 길게 → 옛 데이터 응답
  • 너무 짧게 → DB로 자꾸 떨어짐

대부분 1분 ~ 1시간 범위에서 결정.


7. 보안 · 네트워크

  • VPC 안에 둔다 (퍼블릭 노출 금지)
  • 인증 (Redis AUTH 또는 IAM Auth)
  • 전송 암호화 (TLS)
  • 저장 암호화 (KMS)
Security Group:
  인바운드: ECS Task SG에서 오는 6379

8. 우리 서비스에서

[ECS Service "orders"]
   ↓ 캐시 먼저 보고
[ElastiCache Redis]
   ↓ MISS
[RDS PostgreSQL]
  • 사용자 프로필 조회 → 캐시
  • 카탈로그 목록 → 캐시
  • 세션 토큰 → 캐시
  • Rate limit 카운터 → 캐시

DB 부하의 절반 이상이 캐시로 흡수된다.


9. 직접 확인해보기 — CLI

클러스터 만들기 (단일 노드 + Replica 1)

aws elasticache create-replication-group \
  --replication-group-id orders-cache \
  --replication-group-description "orders cache" \
  --engine redis \
  --cache-node-type cache.t4g.small \
  --num-cache-clusters 2 \
  --automatic-failover-enabled \
  --transit-encryption-enabled \
  --at-rest-encryption-enabled \
  --cache-subnet-group-name private-cache \
  --security-group-ids sg-cache-xxx

redis-cli 접속

redis-cli -h master.orders-cache.xxx.cache.amazonaws.com --tls
> SET user:u-1 "json" EX 300
> GET user:u-1

10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_elasticache_subnet_group" "main" {
  name       = "private-cache"
  subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
}

resource "aws_elasticache_replication_group" "orders" {
  replication_group_id       = "orders-cache"
  description                = "orders cache"
  engine                     = "redis"
  node_type                  = "cache.t4g.small"
  num_cache_clusters         = 2
  automatic_failover_enabled = true

  subnet_group_name          = aws_elasticache_subnet_group.main.name
  security_group_ids         = [aws_security_group.cache.id]

  transit_encryption_enabled = true
  at_rest_encryption_enabled = true
}

11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 캐시를 영구 저장소처럼 쓴다

노드 재시작에 데이터가 사라질 수 있다.

항상 원본은 DB

안티패턴 2. TTL 없이 캐시한다

오래된 데이터가 영원히 응답된다.

안티패턴 3. 캐시가 죽었을 때 시스템도 같이 죽는다

Redis 장애가 곧 서비스 장애로 전이된다.

캐시 실패 시 DB로 우회하는 경로를 코드로 보장

안티패턴 4. 캐시를 보안 없이 운영

TLS · AUTH 안 켜고 VPC 안이라 안전하다고 가정.

전송 · 저장 암호화는 거의 항상 켠다


12. 한 줄로 정리

ElastiCache는 DB 앞단에 두는 인-메모리 캐시이며,
캐시는 캐시일 뿐 — 원본은 항상 DB라는 원칙 위에 운영한다


13. 이 장의 핵심 정리

  1. Redis는 한 자릿수 ms 응답의 인-메모리 저장소다.
  2. 캐시 · 세션 · Rate limit · 랭킹 · Pub/Sub에 다양하게 쓴다.
  3. 휘발성이라 영구 저장소처럼 쓰지 않는다.
  4. Cache-Aside가 가장 흔한 패턴이다.
  5. TTL · 보안 · 캐시 실패 경로 설계가 운영의 출발선이다.

70장. Database per Service — 서비스별 DB 분리 패턴

이 장에서 말하고자 하는 것

지금까지 우리는 RDS · DynamoDB · ElastiCache 를 보았다.

마이크로서비스 구조에서 마지막 결정이 남아 있다.

“DB를 서비스마다 따로 두는가, 한 DB를 공유하는가?”

이 결정이 MSA의 성패를 가른다.

이 장은 Database per Service 원칙을 정리한다.


1. 한 DB 공유의 문제

[orders 서비스]
[users 서비스]      → 하나의 RDS
[payments 서비스]

처음에는 단순해 보인다.
하지만 곧 다음 문제들이 나타난다.

1. 스키마 변경의 위험

users 서비스의 스키마 변경이 orders 서비스를 깨뜨릴 수 있다.

2. 한 서비스의 부하가 다른 서비스를 마비

payments 의 무거운 쿼리가 orders 응답을 느리게 만든다.

3. 배포 속도가 묶임

같은 DB 마이그레이션을 여러 팀이 함께 조정해야 한다.

4. 기술 선택이 묶임

orders 는 관계형이 자연스러운데
catalog 는 DynamoDB가 맞다 — 한 DB로는 풀 수 없다.


2. Database per Service 원칙

[orders 서비스]   → RDS PostgreSQL (orders 전용)
[users 서비스]    → RDS PostgreSQL (users 전용)
[payments 서비스] → RDS PostgreSQL (payments 전용)
[catalog 서비스]  → DynamoDB
[sessions 서비스] → ElastiCache

각 서비스가 자기 DB를 소유한다.

  • 다른 서비스는 그 DB에 직접 접근 못 한다
  • 데이터는 오직 그 서비스의 API를 통해서만 본다

3. 데이터를 어떻게 공유하는가

DB가 분리되면 자연스럽게 질문이 생긴다.

“주문 화면에 사용자 이름이 필요한데, users DB에 못 들어가면 어떻게?”

세 가지 패턴이 있다.

1. 동기 API 호출

orders → GET users-service/users/u-1 → 사용자 정보
  • 단순함
  • 다른 서비스가 죽으면 영향받음

2. 이벤트 + 로컬 복제

users 서비스 → "사용자 정보 변경" 이벤트 → SNS/EventBridge
orders 서비스가 받아서 자기 DB에 일부 정보 보관
  • 다른 서비스의 가용성에 덜 의존
  • 약간의 데이터 지연 가능

3. 공유 읽기 모델 (CQRS · BFF)

여러 서비스의 데이터를 합쳐서 보여주는 별도 읽기 전용 서비스를 둔다.

대규모 환경에서 쓴다.


4. Saga — 분산 트랜잭션의 현실

한 DB의 트랜잭션 (BEGIN ~ COMMIT) 은
여러 서비스에 걸친 작업을 한 번에 묶을 수 없다.

"주문 생성" 작업:
  1. orders.orders 테이블에 주문 INSERT
  2. payments.payments 테이블에 결제 시도
  3. inventory.products 재고 차감

이 세 단계를 한 트랜잭션에 못 묶는다.

해결 패턴이 Saga 다 — 77장에서 다룬다.


5. 운영적 분리 — DB만 나누는 게 끝이 아니다

진짜 분리는 다음을 다 포함한다.

  • DB 인스턴스/클러스터 분리
  • 백업 · 복구 책임 분리
  • 모니터링 · 알람 분리
  • IAM 정책 분리 (Task Role)
  • 스키마 마이그레이션 도구 (Flyway · Liquibase 등) 별도 운영

“한 RDS에 스키마만 다르게” 는 진정한 Database per Service가 아니다


6. 우리 서비스에서

[ECS "orders"]
   ↓ orders-task-role
[RDS PostgreSQL "orders-db"]   (Multi-AZ)

[ECS "users"]
   ↓ users-task-role
[RDS PostgreSQL "users-db"]    (Multi-AZ)

[ECS "payments"]
   ↓ payments-task-role
[RDS PostgreSQL "payments-db"] (Multi-AZ)

[ECS "catalog"]
   ↓ catalog-task-role
[DynamoDB "products"]

[ECS "sessions"]
   ↓ sessions-task-role
[ElastiCache "sessions-cache"]

각 서비스 Task Role은 자기 DB에만 접근할 수 있다.


7. 어떻게 시작할까

처음부터 모든 서비스에 DB를 따로 둘 필요는 없다.

Phase 1: 모놀리스 (한 DB)
Phase 2: 서비스 경계가 보이기 시작
Phase 3: 분리한 서비스만 별도 DB로
Phase 4: 새 서비스는 처음부터 자기 DB

“DB 분리” 가 곧 서비스 분리의 진짜 완성이다


8. 직접 확인해보기 — CLI

서비스별 IAM Role + DB 접근 정책

aws iam create-role --role-name orders-task-role ...
aws iam put-role-policy \
  --role-name orders-task-role \
  --policy-name orders-db-only \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["rds-db:connect"],
      "Resource": "arn:aws:rds-db:ap-northeast-2:...:dbuser:orders-db/orders-app"
    }]
  }'

다른 서비스의 Task Role로는 orders-db에 못 들어간다


9. 코드로는 이렇게 생겼다 — Terraform (스케치)

module "orders" {
  source = "./modules/microservice"

  name           = "orders"
  db_engine      = "postgres"
  db_size        = "db.t4g.small"
  task_role_arn  = aws_iam_role.orders_task.arn
}

module "users" {
  source = "./modules/microservice"

  name           = "users"
  db_engine      = "postgres"
  db_size        = "db.t4g.small"
  task_role_arn  = aws_iam_role.users_task.arn
}

module "catalog" {
  source = "./modules/microservice"

  name          = "catalog"
  db_engine     = "dynamodb"
  task_role_arn = aws_iam_role.catalog_task.arn
}

같은 모듈을 서비스마다 인스턴스화 — 운영의 표준 모양이다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 다른 서비스의 DB에 직접 SELECT

“잠깐 한 줄만” 이 결국 강한 결합이 된다.

다른 서비스의 데이터는 그 서비스의 API로만

안티패턴 2. 한 RDS에 스키마만 다르게

인스턴스가 한 대인 이상 부하 · 백업 · 마이그레이션이 묶인다.

안티패턴 3. “공통 DB” 를 두고 모두 거기 쓰자

공통 DB는 모든 서비스의 SPOF가 된다.

안티패턴 4. 분산 트랜잭션을 강요한다

서비스 사이에 ACID 트랜잭션을 묶으려 하면 끝없는 복잡도가 된다.

Saga 패턴을 받아들이고 최종 일관성을 설계한다


11. 한 줄로 정리

Database per Service는 각 서비스가 자기 DB를 소유하는 원칙이며,
MSA의 진짜 경계는 데이터 분리에서 완성된다


12. 이 장의 핵심 정리

  1. 한 DB 공유는 스키마 충돌 · 부하 전염 · 배포 묶임을 만든다.
  2. 각 서비스가 자기 DB를 소유한다 — 다른 서비스는 API로만 접근.
  3. 데이터 공유는 동기 호출 · 이벤트 복제 · 별도 읽기 모델 중에 고른다.
  4. 분산 트랜잭션 대신 Saga와 최종 일관성을 받아들인다.
  5. 운영적 분리 (백업 · 알람 · IAM) 까지 함께 가야 진짜 분리다.
  6. 처음부터 완벽히 분리할 필요는 없다 — 서비스 경계가 보이는 곳부터.

71장. 백업 · 복구 · 재해 복구 전략

이 장에서 말하고자 하는 것

운영 데이터는 다음 시나리오에 대비해야 한다.

  • 사람이 실수로 지웠다
  • 애플리케이션 버그로 잘못 썼다
  • DB 인스턴스가 망가졌다
  • 한 AZ 전체가 죽었다
  • 한 리전 전체가 죽었다
  • 랜섬웨어 / 악의적 삭제

각 시나리오마다 필요한 도구가 다르다.

이 장은 AWS의 백업 · 복구 옵션을 한 줄로 잡는다.


1. 두 가지 핵심 지표 — RTO와 RPO

RTO (Recovery Time Objective) : 얼마나 빨리 복구할 수 있는가
RPO (Recovery Point Objective): 얼마나 옛 데이터까지 잃어도 되는가
RTO 1시간 / RPO 5분
  → 1시간 안에 복구
  → 최근 5분 이내 데이터 손실 허용

이 두 숫자가 백업 · DR 설계의 출발점이다.

더 짧을수록 비용이 폭증한다


2. AWS의 백업 도구

도구무엇을 백업특징
RDS 자동 백업RDS · Aurora매일 + 트랜잭션 로그 → PITR
RDS 수동 스냅샷RDS · Aurora사람이 명시적으로, 영구 보관
DynamoDB PITRDynamoDB35일간 어느 시점이든 복구
DynamoDB 백업DynamoDB사람이 명시적으로
S3 버전 관리S3객체 변경/삭제 이력
EBS 스냅샷EBS디스크 시점 백업
AWS Backup위 전부 + EFS + 더통합 백업 관리

통합 운영에는 AWS Backup 이 핵심이다


3. Point-in-Time Recovery (PITR)

가장 강력한 회복 도구.

RDS · Aurora · DynamoDB:
  지난 N일(보통 35일) 동안의 어느 시점으로도 복구 가능

예:

2026-03-15 14:23:11 직전 상태로 복구

사람이 실수로 데이터를 지운 시각 직전으로 정확히 돌아갈 수 있다.

운영 DB의 PITR은 사실상 필수


4. 시나리오별 도구

시나리오 1. 사람이 실수로 한 행 삭제 (30분 전)

→ PITR로 30분 전 시점 복구

시나리오 2. 한 테이블이 망가짐

→ 백업/스냅샷에서 그 테이블만 복원

시나리오 3. DB 인스턴스 자체 장애

→ Multi-AZ Standby로 자동 페일오버

시나리오 4. AZ 전체 장애

→ Multi-AZ 가 처리

시나리오 5. 리전 전체 장애

→ Cross-Region Replica / Backup → 다른 리전에서 복구

시나리오 6. 악의적 광범위 삭제

→ 별도 AWS 계정에 백업 복제 (vault lock) 필요


5. AWS Backup — 한 곳에서 관리

Backup Plan
  ├─ 일정: 매일 새벽 3시
  ├─ 보관 기간: 30일
  ├─ 대상: RDS · DynamoDB · EFS · EBS
  └─ 다른 계정/리전으로 복사 (선택)
  • 각 서비스의 백업 설정을 따로 관리하지 않아도 된다
  • 보관 기간 · 암호화 · 복제 정책을 일관되게 적용

5개 이상의 다양한 자원을 백업한다면 AWS Backup으로 통합


6. Vault Lock — 백업 자체를 지키기

악의적 사용자가 운영자 권한을 탈취해 백업까지 지우는 시나리오가 있다.

AWS Backup은 Vault Lock 을 제공한다.

Vault Lock 활성화 → 보관 기간 내 백업은 누구도 못 지움

규제 산업 · 중요한 데이터에는 거의 필수.


7. Cross-Region DR

리전 전체 장애를 대비하려면 다른 리전에 백업 또는 복제 필요.

RDS / Aurora    → Cross-Region Read Replica 또는 백업 복사
DynamoDB        → Global Tables
S3              → Cross-Region Replication
AWS Backup      → Copy to another region

리전 DR은 비용이 큰 결정 — 정말 RTO/RPO 요구가 강할 때만


8. 우리 서비스에서

[AWS Backup Plan: daily-prod]
  대상: 모든 운영 RDS · DynamoDB · EFS
  일정: 매일 새벽 3시
  보관: 30일
  교차 리전 복사: us-east-1 (선택)

[RDS]
  자동 백업 7일, PITR 가능, Multi-AZ

[DynamoDB]
  PITR ON

[S3 운영 버킷]
  버전 관리 ON, 만료 후 옛 버전 정리

대부분의 실수 / 장애가 이 구성으로 회복 가능하다.


9. 직접 확인해보기 — CLI

RDS PITR로 복원

aws rds restore-db-instance-to-point-in-time \
  --source-db-instance-identifier orders \
  --target-db-instance-identifier orders-restored \
  --restore-time 2026-03-15T14:23:11Z

DynamoDB PITR 켜기

aws dynamodb update-continuous-backups \
  --table-name orders \
  --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true

AWS Backup Plan 보기

aws backup list-backup-plans
aws backup list-backup-jobs --by-state COMPLETED

10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_backup_vault" "main" {
  name = "main-vault"
}

resource "aws_backup_plan" "daily" {
  name = "daily"

  rule {
    rule_name         = "daily-3am"
    target_vault_name = aws_backup_vault.main.name
    schedule          = "cron(0 18 * * ? *)"   # UTC 18 = KST 03

    lifecycle {
      delete_after = 30
    }

    copy_action {
      destination_vault_arn = aws_backup_vault.dr.arn   # us-east-1
    }
  }
}

resource "aws_backup_selection" "prod" {
  name         = "prod-resources"
  plan_id      = aws_backup_plan.daily.id
  iam_role_arn = aws_iam_role.backup.arn

  resources = [
    aws_db_instance.orders.arn,
    aws_dynamodb_table.products.arn,
  ]
}

11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 백업만 하고 한 번도 복구를 시도해보지 않는다

“백업 되는 줄 알았던” 데이터가 실제로는 복구 불가능한 경우가 많다.

분기마다 복구를 시뮬레이션 — 실제 다른 환경에서 복원해본다

안티패턴 2. 백업도 같은 계정 · 같은 리전에만 둔다

악의적 계정 탈취에 한 방에 다 사라진다.

별도 백업 계정 또는 별도 리전 복사

안티패턴 3. PITR 기간을 1일로 둔다

사람이 실수를 알아채는 데도 며칠 걸린다.

최소 7일, 운영은 보통 30일

안티패턴 4. RTO/RPO를 정의하지 않은 채 백업한다

실제 사고가 났을 때 “얼마나 빨리?” 가 의사결정의 기준이 없다.


12. 한 줄로 정리

백업 · 복구는 도구 묶음이 아니라 RTO/RPO 기반의 설계이며,
정기적인 복구 시뮬레이션이 진짜 안전망이다


13. 이 장의 핵심 정리

  1. RTO/RPO 가 백업 · DR 설계의 출발점이다.
  2. PITR은 사람의 실수에 가장 강력한 도구다.
  3. Multi-AZ · Cross-Region Replica · AWS Backup이 각자의 자리에 있다.
  4. AWS Backup으로 여러 자원을 통합 관리한다.
  5. Vault Lock · 별도 계정 백업으로 악의적 삭제에 대비한다.
  6. 복구 시뮬레이션 없는 백업은 백업이 아니다.

72장. 통합 흐름 — 데이터 계층을 우리 서비스에 끼우기

이 장에서 말하고자 하는 것

Part 11에서 우리는 데이터 계층의 모든 부품을 다뤘다.

  • 61장. AWS DB 지형
  • 62장. RDS
  • 63장. Multi-AZ
  • 64장. Read Replica
  • 65장. Aurora
  • 66장. DynamoDB 사고방식
  • 67장. DynamoDB 인덱스
  • 68장. DynamoDB 용량 모델
  • 69장. ElastiCache
  • 70장. Database per Service
  • 71장. 백업 · 복구 · DR

이 장은 그 부품들을 우리 MSA 척추에 끼워 한 그림으로 정리한다.


1. 전체 그림

[사용자]
  ↓
[CloudFront]
  ↓
[API Gateway]
  ↓ VPC Link
[Private ALB]
  ├─ /api/orders/*   → ECS "orders"   → RDS  orders-db (Multi-AZ)
  ├─ /api/users/*    → ECS "users"    → RDS  users-db  (Multi-AZ)
  ├─ /api/payments/* → ECS "payments" → RDS  payments-db (Multi-AZ)
  ├─ /api/catalog/*  → ECS "catalog"  → DynamoDB products
  └─ /api/sessions/* → ECS "sessions" → ElastiCache sessions-cache

(공통)
  ↓ AWS Backup
  ├─ 매일 백업
  └─ 다른 리전 복사

각 서비스가 자기 DB를 갖는 게 핵심이다.


2. 서비스마다 어떤 DB를 골랐나

서비스DB이유
ordersRDS PostgreSQL트랜잭션 · 조인 · 정형 데이터
usersRDS PostgreSQL트랜잭션 · 인증 일관성
paymentsRDS PostgreSQL + Multi-AZ강한 일관성 · 감사 로그
catalogDynamoDB단순 조회 · 무한 확장
sessionsElastiCache Redis초저지연 · TTL

61장의 의사결정 트리가 그대로 적용됐다.


3. 가용성 설계의 층

서비스 단위:
  ECS Service desiredCount ≥ 2
  ALB Multi-AZ
  Auto Scaling

DB 단위:
  RDS Multi-AZ
  Aurora cluster (필요 시)
  DynamoDB는 기본 멀티 AZ

백업 단위:
  AWS Backup daily
  Cross-Region 복사 (선택)
  Vault Lock (선택)

세 층이 합쳐져야 진짜 가용성이 만들어진다.


4. 읽기 부하가 늘어날 때

Phase 1: Primary 한 대로 시작
Phase 2: 트래픽 늘면 Read Replica 추가
Phase 3: 더 늘면 Aurora 전환 (빠른 Replica)
Phase 4: 글로벌이면 Aurora Global Database

캐시 계층은 Phase 1부터 끼우는 게 흔하다.

“DB가 느려서 못 살겠다” 보다는 “캐시를 일단 끼우자” 가 먼저


5. 쓰기 부하가 늘어날 때

Phase 1: 인스턴스 사양 업
Phase 2: 핫 데이터를 DynamoDB로 분리
Phase 3: 이벤트 기반 분리 (다음 단원 73~77장)
Phase 4: 샤딩 (수동 분할)

관계형 DB의 쓰기 확장은 수평 확장이 어렵다.
일정 규모를 넘으면 데이터 모델을 재검토한다.


6. 서비스 간 데이터 공유

ordersusers 정보가 필요할 때 — 70장에서 본 세 패턴 중 하나.

1. 동기 API (단순)
2. 이벤트 + 로컬 복제 (느슨한 결합)
3. CQRS / BFF (대규모)

이벤트 기반은 다음 단원(메시징)에서 본격적으로 다룬다.


7. 운영 점검 체크리스트

각 DB마다 다음을 확인한다.

□ Multi-AZ / Cluster 켜졌는가
□ 자동 백업 (PITR) 켜졌는가
□ 보관 기간이 충분한가 (최소 7일, 권장 30일)
□ deletion_protection 켜졌는가
□ publicly_accessible = false 인가
□ 보안 그룹이 ECS Task SG 만 허용하는가
□ 비밀번호가 Secrets Manager에 있는가
□ ReplicaLag · CPU · Connections 알람이 있는가
□ AWS Backup Plan에 포함됐는가
□ 복구를 한 번이라도 시뮬레이션했는가

8. 우리 서비스의 DB Terraform 묶음 (요약)

# orders 서비스 모듈
module "orders_db" {
  source                  = "./modules/rds-postgres"
  identifier              = "orders"
  instance_class          = "db.t4g.small"
  multi_az                = true
  backup_retention_period = 7
  deletion_protection     = true
  subnet_group_name       = aws_db_subnet_group.private.name
  allowed_sg_id           = aws_security_group.orders_task.id
}

# 같은 모듈을 users · payments 에도 적용
module "users_db"    { source = "./modules/rds-postgres" ... }
module "payments_db" { source = "./modules/rds-postgres" ... }

# DynamoDB
resource "aws_dynamodb_table" "products" {
  name         = "products"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "product_id"

  attribute {
    name = "product_id"
    type = "S"
  }

  point_in_time_recovery {
    enabled = true
  }
}

# ElastiCache
resource "aws_elasticache_replication_group" "sessions" {
  replication_group_id       = "sessions-cache"
  ...
}

# AWS Backup
resource "aws_backup_plan" "daily" {
  ...
  resources = [
    module.orders_db.arn,
    module.users_db.arn,
    module.payments_db.arn,
    aws_dynamodb_table.products.arn,
  ]
}

같은 모듈을 인스턴스화 — 새 서비스를 추가할 때 한 묶음으로 복제.


9. 이렇게 쓰면 망한다 — 통합 단계의 흔한 함정

함정 1. DB 보안 그룹이 ECS Task SG에서 안 열려 있다

연결이 끝없이 timeout. “DB는 살아 있는데 안 보임” 증상.

함정 2. 비밀번호를 평문 환경 변수로 박는다

배포 자동화 단계에서 비밀번호가 로그에 남는다.

항상 Secrets Manager → ECS Task Definition의 secrets 로 주입

함정 3. 캐시가 죽으면 서비스가 같이 죽는다

캐시 실패 시 DB로 우회하는 경로를 안 만들어 둠.

함정 4. 모니터링 알람이 없다

DB 부하가 100% 인데 며칠 후에야 안다.

CPU · Connections · ReplicaLag · Throttle 은 거의 항상 켠다


10. 한 줄로 정리

데이터 계층은 “어떤 DB를 어디에 쓸지” + “어떻게 보호할지” 의 두 결정으로 완성되며,
서비스별 분리와 백업 · 복구 시뮬레이션이 운영의 진짜 안전망이다


11. 이 장의 핵심 정리

  1. Part 11 부품들이 모이면 MSA 데이터 계층이 완성된다.
  2. 서비스마다 자기 DB를 가진다 — RDS · DynamoDB · ElastiCache 를 워크로드별로.
  3. Multi-AZ · Replica · Cluster 는 가용성, AWS Backup · PITR · Vault Lock은 회복성을 담당한다.
  4. 캐시는 거의 항상 끼우는 게 운영 비용을 가장 빠르게 낮춘다.
  5. 다음 단원은 서비스 간 통신을 다룬다 — 데이터를 어떻게 흘릴지.

73장. 동기 vs 비동기 — 무엇을 어디에 쓰는가

이 장에서 말하고자 하는 것

마이크로서비스가 늘어나면 서비스끼리 부르는 일이 잦아진다.

orders 서비스 → users 서비스 (사용자 정보 조회)
orders 서비스 → payments 서비스 (결제 처리)
orders 서비스 → notification 서비스 (주문 알림)

이때 호출 방식이 두 가지로 나뉜다.

  • 동기 (Synchronous) — 답을 기다린다
  • 비동기 (Asynchronous) — 메시지만 던지고 다음으로 간다

이 선택이 시스템의 결합도와 안정성을 결정한다.


1. 동기 호출

orders → "u-1 사용자 정보 줘" → users
        ← (응답 기다림)
        ← 사용자 정보
  • 단순하다
  • 실시간 응답이 필요할 때 자연스럽다
  • 호출 대상이 죽어 있으면 나도 영향받는다

REST · gRPC · GraphQL 호출이 모두 동기다.


2. 비동기 호출

orders → "주문 생성됨" → 메시지 큐
                          ↓ (시간차)
                       notification 서비스가 꺼내서 처리
  • orders는 응답을 기다리지 않는다
  • notification 서비스가 잠깐 죽어도 orders는 영향 없음
  • 처리에 시간차가 있다 (보통 ms~수 초)

3. 어떤 걸 골라야 하는가

동기가 어울리는 경우

  • 사용자에게 즉시 응답해야 함
  • 결과를 기다려야 다음 단계 진행이 가능
  • 강한 일관성이 필요
"내 프로필 보여줘"  → 동기
"이 주문 결제됐어?" → 동기

비동기가 어울리는 경우

  • 보내고 다음으로 가도 됨
  • 받는 쪽이 느려도 보내는 쪽은 빨라야 함
  • 여러 서비스에 같은 사건을 알리고 싶음
"주문 생성됨" → 알림 / 분석 / 추천 등 여러 곳으로
"이미지 업로드됨" → 썸네일 생성 / 분석 / 검열

4. 비동기가 풀어주는 결합도

같은 일을 동기로 묶으면 이렇게 된다.

orders → users
       → payments
       → notification
       → analytics
       → recommendation
  • 어느 한 서비스가 느려지면 orders 응답이 느려진다
  • 어느 한 서비스가 죽으면 orders 도 실패
  • 새 서비스를 끼우려면 orders 코드 수정 필요

비동기로 풀면

orders → 메시지 발행 (한 곳)
            ↓
       여러 서비스가 각자 받음
  • orders는 단 한 번만 보낸다
  • 새 서비스를 끼우려면 그 서비스가 구독하면 끝
  • 한 서비스 장애가 다른 서비스에 안 번진다

5. AWS의 비동기 도구들

도구모델핵심 용도
SQS큐 (1:1)작업 큐, 워커 패턴
SNS토픽 (1:N)팬아웃, 알림
EventBridge이벤트 버스 (라우팅)이벤트 기반 아키텍처, 서드파티 통합
Kinesis스트림대량 실시간 처리
MSK / MQKafka / RabbitMQ복잡한 라우팅, 외부 호환

각자 다음 장들에서 자세히 본다.


6. 동기와 비동기는 한 시스템에서 같이 산다

실제 운영은 둘을 섞는다.

사용자 → orders (동기)
         ↓
       "주문 생성됨" 이벤트 발행 (비동기)
         ↓
       notification · analytics · recommendation 받음

외부와의 접점은 동기, 내부 부수 효과는 비동기

이게 가장 흔한 패턴이다.


7. 우리 서비스에서

[사용자] → [API Gateway] → [ALB] → [orders]     ← 동기 (응답 필요)
                                       ↓
                                   "OrderCreated" 이벤트
                                       ↓ (비동기)
                                   ┌─ notification ─ 알림
                                   ├─ analytics    ─ 통계
                                   └─ inventory    ─ 재고 차감

orders는 한 번만 발행하고, 받는 쪽은 각자 처리한다.


8. 직접 확인해보기 — 의사 결정 가이드

새 호출이 필요해질 때 다음 질문을 던진다.

"호출자가 응답이 없으면 다음 단계를 못 가나?"
  └─ Yes → 동기
  └─ No  → 비동기

"여러 서비스가 같은 사건을 알아야 하나?"
  └─ Yes → 비동기 (SNS · EventBridge)

"보내는 쪽이 받는 쪽 가용성에 묶이면 곤란한가?"
  └─ Yes → 비동기 (SQS)

9. 코드로는 이렇게 생겼다 — 발행자 예 (Node.js)

import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";

const sns = new SNSClient({ region: "ap-northeast-2" });

async function publishOrderCreated(order) {
  await sns.send(new PublishCommand({
    TopicArn: process.env.ORDER_EVENTS_TOPIC,
    Message: JSON.stringify({
      type: "OrderCreated",
      orderId: order.id,
      userId: order.userId,
      total: order.total,
    }),
  }));
}

orders는 누가 받는지 모른다 — 토픽에 던지기만 하면 끝.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 모든 호출을 동기로 만든다

시스템이 결합도 폭탄이 된다. 한 서비스 장애가 모두에 번진다.

안티패턴 2. 모든 호출을 비동기로 만든다

사용자가 결과를 기다려야 하는 흐름이 어색해진다.
응답에 즉시 결과를 줘야 하는 곳에는 동기가 맞다.

안티패턴 3. 비동기에 정확한 응답 시간을 기대한다

비동기는 본질적으로 시간차가 있다.

“5초 안에 처리됨” 같은 보장은 비동기로 만들기 어렵다

안티패턴 4. 메시지를 받지 못해도 시스템이 알아채지 못한다

DLQ (Dead Letter Queue) · 알람 없이는 메시지 손실을 모른다.


11. 한 줄로 정리

동기는 답을 기다리고, 비동기는 메시지를 던진다.
외부 접점은 동기, 내부 부수 효과는 비동기 — 이 조합이 운영의 기본이다


12. 이 장의 핵심 정리

  1. 동기는 즉시 응답, 비동기는 시간차 처리다.
  2. 비동기는 결합도를 낮추고 장애 전염을 막는다.
  3. 한 시스템에서 둘은 함께 산다 — 자리에 맞춰 고른다.
  4. AWS는 SQS · SNS · EventBridge · Kinesis 등 비동기 도구를 풍부하게 제공한다.
  5. 비동기에는 시간차가 본질이다 — 즉각 응답을 기대하지 않는다.

74장. SQS — 큐로 결합도 낮추기

이 장에서 말하고자 하는 것

가장 단순한 비동기 도구가

Amazon SQS (Simple Queue Service)

다.

큐(Queue)는 우편함 같은 것이다.

  • 누가 메시지를 넣는다
  • 다른 누가 와서 꺼낸다
  • 보내는 쪽과 받는 쪽이 동시에 살아 있지 않아도 된다

1. SQS의 기본 모델

[Producer]  → [Queue: orders-jobs] → [Consumer]
                  ├─ msg 1
                  ├─ msg 2
                  └─ msg 3
  • Producer는 큐에 메시지를 넣는다 (SendMessage)
  • Consumer는 큐에서 메시지를 꺼낸다 (ReceiveMessage)
  • 처리가 끝나면 메시지를 삭제 (DeleteMessage)

2. Standard vs FIFO

항목StandardFIFO
순서 보장약함 (대체로)강함 (그룹 안에서)
중복 가능성있음 (At-least-once)없음 (Exactly-once)
처리량거의 무제한초당 ~300건 (배치로 ~3000)
비용약간 비쌈

대부분 Standard로 시작 — 순서/중복이 중요한 곳만 FIFO


3. Visibility Timeout — 처리 중인 메시지를 잠시 감춤

Consumer가 메시지를 꺼내면 그 메시지는 다른 Consumer가 못 보게 잠시 숨겨진다.

Consumer A가 메시지 꺼냄 → 30초 동안 다른 Consumer에 안 보임
  ↓
처리 성공 → DeleteMessage → 큐에서 사라짐
처리 실패 / 시간 초과 → 다시 나타남 → 다른 Consumer가 받을 수 있음

처리 시간보다 Visibility Timeout이 짧으면 같은 메시지가 두 번 처리된다.

처리 시간이 길면 Visibility Timeout도 길게 (또는 ChangeMessageVisibility로 연장)


4. At-least-once — “한 번 이상” 보장

Standard SQS의 핵심 특성.

한 메시지가 두 번 이상 전달될 수 있다

따라서 Consumer는 멱등하게(idempotent) 처리해야 한다.

"주문 12345 처리됨" 표시를 먼저 본다
  ↓ 이미 처리됐으면 skip
  ↓ 아니면 처리

SQS를 쓰는 모든 Consumer는 멱등성 설계가 기본이다


5. Dead Letter Queue (DLQ)

메시지가 N번 처리에 실패하면 별도 큐로 옮긴다.

[Queue] → 3번 실패 → [DLQ]

DLQ는

  • 끝없이 재시도되는 메시지를 격리
  • 사람이 살펴볼 수 있게 보관
  • 알람의 트리거

DLQ 없는 큐는 운영하지 않는다


6. Long Polling

ReceiveMessage 할 때 메시지가 없으면 빈 응답이 즉시 돌아온다 (Short Polling).
이러면 API 호출이 끝없이 발생해 비용 · 부하 모두 손해.

WaitTimeSeconds = 20

이러면 메시지가 도착할 때까지 (최대 20초) 기다린다.

Long Polling은 사실상 기본값 — 항상 켠다


7. 흔한 패턴 — 워커 큐

[Producer]
  ↓ SendMessage
[Queue]
  ↓
[Consumer Auto Scaling]
  ├─ Worker 1
  ├─ Worker 2
  └─ Worker N   (큐 깊이에 따라 자동 증감)

ECS Service Auto Scaling이 큐 길이를 보고 Worker 수를 조절한다.

큐에 메시지가 1000건 → Worker 늘어남
큐가 비면 → Worker 줄어듦

8. 우리 서비스에서

[orders 서비스]
   ↓ SendMessage "주문 생성됨"
[SQS: order-events]
   ↓
[notification worker]   → 알림 전송
[analytics worker]       → 통계 적재

각 Worker는 자기 처리만 책임진다.

order-events  ───→ notification-jobs    (DLQ: notification-dlq)
              ───→ analytics-jobs       (DLQ: analytics-dlq)

큐를 워커별로 분리하면 한쪽이 막혀도 다른 쪽이 영향 없다.


9. 직접 확인해보기 — CLI

# 큐 만들기
aws sqs create-queue --queue-name order-events

# 메시지 보내기
aws sqs send-message \
  --queue-url <queue-url> \
  --message-body '{"type":"OrderCreated","orderId":"o-1"}'

# 메시지 받기 (Long Polling)
aws sqs receive-message \
  --queue-url <queue-url> \
  --wait-time-seconds 20

# 메시지 삭제
aws sqs delete-message \
  --queue-url <queue-url> \
  --receipt-handle <handle>

10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_sqs_queue" "order_events_dlq" {
  name                      = "order-events-dlq"
  message_retention_seconds = 1209600   # 14일
}

resource "aws_sqs_queue" "order_events" {
  name                      = "order-events"
  visibility_timeout_seconds = 60
  message_retention_seconds  = 345600   # 4일
  receive_wait_time_seconds  = 20       # Long polling

  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.order_events_dlq.arn
    maxReceiveCount     = 3
  })
}

DLQ는 항상 함께 만든다.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. DLQ 없이 큐를 운영

실패 메시지가 영원히 재시도된다. 비용 폭증.

안티패턴 2. 멱등성 없이 Consumer를 만든다

같은 주문이 두 번 결제되는 버그가 난다.

안티패턴 3. Visibility Timeout이 처리 시간보다 짧다

같은 메시지를 여러 Worker가 동시에 처리한다.

안티패턴 4. Short Polling으로 끝없이 ReceiveMessage 호출

SQS API 비용이 메시지 처리 비용보다 커진다.

Long Polling은 항상 켠다


12. 한 줄로 정리

SQS는 가장 단순한 비동기 도구이며,
Visibility Timeout · 멱등성 · DLQ · Long Polling 네 가지가 운영의 토대다


13. 이 장의 핵심 정리

  1. SQS는 1:1 큐 — Producer가 넣고 Consumer가 꺼낸다.
  2. Standard는 at-least-once, FIFO는 순서/중복 제거가 강하다.
  3. Visibility Timeout은 처리 중 메시지를 다른 Consumer로부터 감춘다.
  4. Consumer는 항상 멱등하게 설계한다.
  5. DLQ는 사실상 필수, Long Polling도 항상 켠다.
  6. ECS Service Auto Scaling을 큐 깊이로 묶어 워커를 자동 조절한다.

75장. SNS — 팬아웃 패턴

이 장에서 말하고자 하는 것

SQS는 한 큐에 한 Consumer 그룹이 붙는 구조다.

한 메시지 → 한 그룹이 처리

그런데 같은 메시지를 여러 곳에 동시에 보내야 할 때가 있다.

"주문 생성됨"
  → 알림 서비스도 알아야 하고
  → 분석 서비스도 알아야 하고
  → 추천 서비스도 알아야 하고
  → 재고 서비스도 알아야 한다

이때 등장하는 게

Amazon SNS (Simple Notification Service)

다.


1. SNS의 기본 모델 — 1:N

[Publisher]
   ↓ Publish
[Topic: order-events]
   ↓ 동일 메시지를 모든 구독자에게 전달
   ├─ Subscriber 1 (SQS)
   ├─ Subscriber 2 (SQS)
   ├─ Subscriber 3 (Lambda)
   └─ Subscriber 4 (HTTPS endpoint)
  • 토픽에 한 번 발행하면 모든 구독자에게 복제 전달
  • Publisher는 누가 받는지 모른다
  • 구독자는 자유롭게 추가/삭제

2. Fan-out 패턴 — SNS + SQS 조합

가장 흔한 운영 패턴이다.

[Publisher]
   ↓
[SNS Topic]
   ├─ SQS: notification-jobs    → notification worker
   ├─ SQS: analytics-jobs        → analytics worker
   ├─ SQS: recommendation-jobs   → recommendation worker
   └─ SQS: inventory-jobs        → inventory worker
  • 발행자는 SNS에 한 번
  • 각 Worker는 자기 큐에서 자기 페이스로 처리
  • 한쪽이 막혀도 다른 큐에는 영향 없음
  • 새 구독자를 추가해도 발행자 코드는 변경 없음

SNS 직접 → Lambda / HTTPS 도 가능하지만 SNS → SQS → Worker가 안정적


3. 메시지 필터링 — 받고 싶은 것만 받기

SNS는 구독에 필터 정책 을 둘 수 있다.

구독자가 받고 싶은 것:
  { "type": ["OrderCreated", "OrderCancelled"] }

토픽에 흘러오는 메시지:
  - OrderCreated   → 통과
  - OrderShipped   → 차단
  - OrderCancelled → 통과

필터링은 SNS 단계에서 일어나므로 불필요한 트래픽이 SQS까지 가지 않는다.


4. Order는 보장하지 않는다 (기본)

SNS Standard 토픽은 순서 보장이 없다.

"OrderCreated" → "OrderPaid" 발행
  ↓
구독자는 "OrderPaid" 가 먼저 도착할 수도 있다

순서가 중요한 도메인은 SNS FIFO + SQS FIFO 조합으로 묶는다.


5. SNS와 EventBridge — 비슷한데 다르다

“둘 다 1:N 인데 뭐가 다른가?”

이 질문이 자주 나온다.

SNS         → 단순 팬아웃 (빠름, 저비용)
EventBridge → 풍부한 라우팅, 서드파티 통합, 스키마 레지스트리
  • 내부 단순 fan-out → SNS
  • 복잡한 라우팅 / 서드파티 SaaS · AWS 서비스 이벤트 통합 → EventBridge

다음 76장에서 자세히 본다.


6. 우리 서비스에서

[orders 서비스]
   ↓ Publish "OrderCreated"
[SNS Topic: order-events]
   ├─ SQS: notification-jobs    → ECS notification worker → 이메일/푸시
   ├─ SQS: analytics-jobs        → ECS analytics worker  → DynamoDB
   └─ SQS: inventory-jobs        → ECS inventory worker  → 재고 차감
  • orders 서비스 코드는 단 한 줄: sns.publish(...)
  • 새 도메인(예: loyalty) 이 생기면 그쪽 SQS를 토픽에 구독시키면 끝

7. 직접 확인해보기 — CLI

토픽 만들기

aws sns create-topic --name order-events

구독 (SQS)

aws sns subscribe \
  --topic-arn <topic-arn> \
  --protocol sqs \
  --notification-endpoint <sqs-queue-arn>

발행

aws sns publish \
  --topic-arn <topic-arn> \
  --message '{"type":"OrderCreated","orderId":"o-1"}' \
  --message-attributes '{
    "type": {"DataType":"String","StringValue":"OrderCreated"}
  }'

message-attributes 가 필터 정책의 기준이 된다.


8. 코드로는 이렇게 생겼다 — Terraform

resource "aws_sns_topic" "order_events" {
  name = "order-events"
}

resource "aws_sqs_queue" "notification_jobs" {
  name = "notification-jobs"
}

resource "aws_sns_topic_subscription" "notification" {
  topic_arn            = aws_sns_topic.order_events.arn
  protocol             = "sqs"
  endpoint             = aws_sqs_queue.notification_jobs.arn
  raw_message_delivery = true

  filter_policy = jsonencode({
    type = ["OrderCreated", "OrderCancelled"]
  })
}

# SNS가 SQS에 쓸 수 있도록 큐 정책
resource "aws_sqs_queue_policy" "notification" {
  queue_url = aws_sqs_queue.notification_jobs.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "sns.amazonaws.com" }
      Action    = "sqs:SendMessage"
      Resource  = aws_sqs_queue.notification_jobs.arn
      Condition = {
        ArnEquals = { "aws:SourceArn" = aws_sns_topic.order_events.arn }
      }
    }]
  })
}

SNS → SQS의 권한 설정이 자주 까먹는 부분이다.


9. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. SNS → 여러 Lambda 직접 구독

Lambda 가 한 번이라도 실패하면 메시지 손실 가능 (재시도는 있지만 한계).

안정성이 중요하면 SNS → SQS → Lambda

안티패턴 2. 한 큐에 여러 도메인 메시지를 섞는다

필터 없이 모든 메시지가 한 큐에 → 워커가 자기와 무관한 메시지까지 처리.

도메인별 큐를 분리하고 SNS 필터로 분기

안티패턴 3. 메시지 본문에 거대한 페이로드를 넣는다

SNS · SQS는 메시지 크기 제한이 있다 (기본 256KB).
큰 데이터는 S3에 두고 SNS에는 키만.

안티패턴 4. 발행자가 누가 받는지 신경 쓴다

SNS의 핵심은 발행자/구독자 분리.
발행자가 구독자 추가/제거를 알아야 한다면 추상화가 무너진 것.


10. 한 줄로 정리

SNS는 1:N 팬아웃 도구이며,
“SNS → 여러 SQS → 각자 워커” 가 가장 안정적인 패턴이다


11. 이 장의 핵심 정리

  1. SNS는 토픽 기반의 1:N 발행/구독 모델이다.
  2. 발행자는 구독자를 모른다 — 결합도가 결정적으로 낮아진다.
  3. SNS → SQS → Worker 가 가장 안정적인 운영 패턴이다.
  4. 필터 정책으로 구독자가 받고 싶은 것만 받는다.
  5. 순서가 중요하면 SNS FIFO + SQS FIFO.
  6. 단순 팬아웃은 SNS, 복잡한 라우팅은 EventBridge.

76장. EventBridge — 이벤트 기반 아키텍처

이 장에서 말하고자 하는 것

SNS가 단순 팬아웃이라면
EventBridge는 한 단계 더 풍부한 이벤트 버스 다.

Amazon EventBridge

EventBridge는 다음을 한 곳에서 다룬다.

  • 내 애플리케이션 이벤트
  • AWS 서비스 자체 이벤트 (예: EC2 상태 변경)
  • 서드파티 SaaS 이벤트 (Datadog, Zendesk, Shopify 등)

복잡한 라우팅과 스키마 관리를 더해서.


1. 기본 모델 — Event Bus + Rule + Target

[Publisher]
   ↓ PutEvents
[Event Bus]
   ↓
[Rule 1: type=OrderCreated]    → SQS · Lambda · ECS · Step Functions
[Rule 2: type=PaymentFailed]   → SNS · email
[Rule 3: source=aws.ec2]        → CloudWatch / Lambda
  • 이벤트는 Event Bus로 보낸다
  • Rule이 이벤트 패턴을 매칭해 Target으로 보낸다
  • 한 이벤트가 여러 Rule에 매칭될 수 있다

2. 이벤트 패턴 — JSON 매칭

{
  "source": ["myapp.orders"],
  "detail-type": ["OrderCreated"],
  "detail": {
    "amount": [{ "numeric": [">", 100000] }]
  }
}
  • source · detail-type · detail 의 값을 매칭
  • 숫자 · 접두사 · IP · anything-but 같은 풍부한 연산자 지원

SNS의 필터 정책보다 표현력이 훨씬 크다


3. 다양한 Target

EventBridge가 보낼 수 있는 곳이 매우 많다.

Lambda
SQS
SNS
ECS Task 실행 (RunTask)
Step Functions
Kinesis
EventBridge Pipes
다른 계정의 Event Bus
HTTPS API (API Destinations)

특히

다른 계정의 Event Bus 로 이벤트를 보낼 수 있다

가 멀티 계정 운영에서 강력하다.


4. AWS 서비스 이벤트 자동 수신

EventBridge에는 “default” 이벤트 버스가 있다.

EC2 인스턴스 상태 변경
S3 객체 생성
RDS 백업 완료
CodePipeline 단계 변경
Health Dashboard 알림
... 등 거의 모든 AWS 서비스의 이벤트

이 이벤트를 자동으로 받아서 처리할 수 있다.

"운영 RDS에 백업이 실패하면 → Slack 알림"
"S3에 파일이 업로드되면 → Lambda 처리"
"보안 그룹이 변경되면 → 알람"

5. EventBridge Pipes — 단순 연결

큐 → 변환 → 또 다른 큐 같은 직선 흐름을 코드 없이 구성.

[SQS] → [Filter] → [Enrichment(Lambda)] → [SNS]

ETL 같은 작은 연결을 Pipes 로 풀 수 있다.


6. Schema Registry

이벤트의 스키마를 등록해 관리할 수 있다.

  • 발행자/구독자가 같은 스키마를 보고 있는지 확인
  • 코드 생성기로 클라이언트 SDK 자동 생성
  • AWS 자체 이벤트 스키마도 자동 발견

큰 조직에서 이벤트 계약을 관리할 때 핵심 도구


7. SNS vs EventBridge — 다시

항목SNSEventBridge
라우팅토픽 + 필터 정책Rule + 풍부한 패턴
Target 다양성SQS · Lambda · HTTPS · email · SMS거의 모든 AWS 서비스 + 외부
AWS 서비스 이벤트
서드파티 SaaS
스키마 관리
성능매우 빠름빠름 (SNS보다 약간 늦음)
비용가장 쌈약간 비쌈

단순 fan-out → SNS
라우팅 · AWS/SaaS 통합 · 스키마 관리 → EventBridge


8. 우리 서비스에서

[orders 서비스]
   ↓ PutEvents
[EventBridge: msa-bus]
   ├─ Rule "high-value": detail.amount > 1,000,000
   │     → Step Functions (VIP 처리 워크플로우)
   ├─ Rule "all-orders":
   │     → SQS (analytics-jobs)
   │     → SQS (notification-jobs)
   └─ Rule "cancel":  detail-type = OrderCancelled
         → Lambda (환불 처리)

(추가로)
default 버스
  ├─ S3 객체 업로드 → Lambda (썸네일)
  └─ RDS 백업 실패 → SNS (Slack)

내부 도메인 이벤트는 msa-bus, AWS 시스템 이벤트는 default 버스.


9. 직접 확인해보기 — CLI

Event Bus + Rule + Target

aws events create-event-bus --name msa-bus

aws events put-rule \
  --event-bus-name msa-bus \
  --name high-value-orders \
  --event-pattern '{
    "source": ["myapp.orders"],
    "detail-type": ["OrderCreated"],
    "detail": { "amount": [{ "numeric": [">", 1000000] }] }
  }'

aws events put-targets \
  --event-bus-name msa-bus \
  --rule high-value-orders \
  --targets '[{"Id":"1","Arn":"<step-functions-arn>","RoleArn":"<role-arn>"}]'

이벤트 발행

aws events put-events --entries '[{
  "EventBusName": "msa-bus",
  "Source": "myapp.orders",
  "DetailType": "OrderCreated",
  "Detail": "{\"orderId\":\"o-1\",\"amount\":1500000}"
}]'

10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_cloudwatch_event_bus" "msa" {
  name = "msa-bus"
}

resource "aws_cloudwatch_event_rule" "high_value" {
  name           = "high-value-orders"
  event_bus_name = aws_cloudwatch_event_bus.msa.name

  event_pattern = jsonencode({
    source        = ["myapp.orders"]
    "detail-type" = ["OrderCreated"]
    detail = {
      amount = [{ numeric = [">", 1000000] }]
    }
  })
}

resource "aws_cloudwatch_event_target" "high_value_sqs" {
  event_bus_name = aws_cloudwatch_event_bus.msa.name
  rule           = aws_cloudwatch_event_rule.high_value.name
  arn            = aws_sqs_queue.vip_jobs.arn
}

11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 단순 fan-out 인데 EventBridge로 시작

SNS가 더 싸고 빠르다.

“라우팅” 이 진짜 필요할 때 EventBridge

안티패턴 2. 이벤트 스키마를 정의하지 않는다

Producer와 Consumer가 다른 모양을 가정 → 깨진다.

Schema Registry 또는 사내 문서로 계약을 명시

안티패턴 3. DLQ를 안 건다

EventBridge → SQS 같은 흐름도 실패 가능. DLQ로 격리한다.

안티패턴 4. 한 이벤트 버스에 모든 도메인을 섞는다

권한 · 모니터링이 다 섞인다.

도메인/팀 단위로 Event Bus를 분리하는 게 큰 조직에서 자연스럽다


12. 한 줄로 정리

EventBridge는 라우팅 · AWS 이벤트 · 서드파티 통합 · 스키마 관리를 더한 이벤트 버스이며,
단순 팬아웃을 넘어서는 순간 등장한다


13. 이 장의 핵심 정리

  1. EventBridge는 Event Bus + Rule + Target 의 3축이다.
  2. 이벤트 패턴 매칭이 SNS 필터보다 표현력이 크다.
  3. AWS 서비스 자체 이벤트와 서드파티 SaaS를 자연스럽게 받는다.
  4. Target은 Lambda · SQS · ECS · Step Functions · 다른 계정 등 매우 다양하다.
  5. 단순 팬아웃은 SNS, 라우팅·통합·스키마는 EventBridge.
  6. Schema Registry는 큰 조직에서 이벤트 계약을 관리하는 핵심 도구다.

77장. Saga 패턴 맛보기 — 분산 트랜잭션의 현실

이 장에서 말하고자 하는 것

서비스가 하나라면 한 트랜잭션으로 묶을 수 있다.

BEGIN;
INSERT INTO orders ...;
UPDATE inventory SET stock = stock - 1;
INSERT INTO payments ...;
COMMIT;

마이크로서비스에서는 이게 불가능하다.

orders 서비스 (DB1)
inventory 서비스 (DB2)
payments 서비스 (DB3)

세 DB에 걸친 트랜잭션을 한 번에 묶는 분산 트랜잭션은
이론적으로 가능하지만 실전에서는 거의 쓰지 않는다 (느림 · 복잡 · 한 곳 죽으면 전체 멈춤).

대신 쓰는 패턴이

Saga

다.


1. Saga의 아이디어

큰 트랜잭션을 작은 단계로 쪼개고
실패하면 보상(Compensation) 트랜잭션으로 되돌린다

Step 1: 주문 생성 (orders)
Step 2: 재고 차감 (inventory)
Step 3: 결제 (payments)

만약 Step 3에서 실패하면

Compensation 3: (결제는 못 했지만 추가 작업 없음)
Compensation 2: 재고 복원
Compensation 1: 주문 취소 처리

각 서비스는 자기 DB의 로컬 트랜잭션만 다룬다.

“최종 일관성(Eventual Consistency)” 을 받아들이는 게 핵심이다


2. 두 가지 Saga 스타일

Choreography — 안무

서비스끼리 이벤트를 주고받으며 스스로 다음 단계를 결정.

orders: 주문 생성 → "OrderCreated" 발행
  ↓
inventory: 받음 → 재고 차감 → "InventoryReserved" 발행
  ↓
payments: 받음 → 결제 → "PaymentSucceeded" 발행
  ↓
orders: 받음 → 주문 확정
  • 중앙 컨트롤러 없음
  • 단순한 흐름에 자연스러움
  • 흐름이 복잡해지면 추적이 어려움

Orchestration — 지휘자

별도의 컨트롤러가 각 단계를 호출.

[Saga Orchestrator (Step Functions 등)]
  ├─ orders.createOrder()
  ├─ inventory.reserveStock()
  ├─ payments.charge()
  └─ orders.confirmOrder()

실패 시 보상:
  ├─ payments.refund()
  └─ inventory.releaseStock()
  └─ orders.cancelOrder()
  • 흐름이 한 곳에 있어 가독성 좋음
  • 복잡한 흐름에 유리
  • Step Functions 같은 도구가 잘 어울림

3. AWS 도구 매핑

Saga 방식AWS 조합
ChoreographySNS / EventBridge + SQS + 각 서비스
OrchestrationStep Functions + Lambda / ECS

4. 멱등성과 보상의 현실

Saga의 핵심 어려움은

보상이 항상 깨끗하지 않다

는 점이다.

"결제 환불" 은 항상 가능한가?
"발송된 알림" 은 어떻게 되돌리나?
"외부에 보낸 신호" 는?
  • 모든 단계는 멱등하게 설계 (한 번 받아도 두 번 받아도 같은 결과)
  • “이미 처리됐는지” 를 알 수 있는 식별자 (saga_id, transaction_id) 가 필요
  • 비즈니스적으로 “되돌릴 수 없는” 행동은 가능한 한 뒤로 미룬다

5. Step Functions — Orchestration 도구

AWS의 워크플로우 엔진.

{
  "StartAt": "CreateOrder",
  "States": {
    "CreateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:...:createOrder",
      "Next": "ReserveStock",
      "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "Fail" }]
    },
    "ReserveStock": { ... },
    "Charge": { ... },
    "ConfirmOrder": { "End": true },
    "Fail": { "Type": "Fail" }
  }
}
  • JSON으로 흐름 정의
  • 각 단계의 재시도 / 타임아웃 / 보상 흐름을 선언적으로 표현
  • 시각화 · 추적이 강함

복잡한 Saga는 Step Functions가 거의 정답


6. Outbox Pattern — 이벤트 발행의 함정

Saga의 흔한 함정.

서비스가 DB에 쓴 직후 곧장 이벤트 발행
  ↓
DB는 커밋됐는데 이벤트 발행이 실패 (또는 그 반대)
  ↓ 일관성 깨짐

해결:

1. DB에 "이벤트 outbox" 테이블도 같이 INSERT (같은 트랜잭션)
2. 별도 프로세스가 outbox를 읽어 이벤트로 발행
3. 발행 후 outbox에 표시

이걸 Transactional Outbox 패턴이라 한다.
DynamoDB Streams · Debezium 등으로 풀기도 한다.


7. 우리 서비스에서

주문 흐름 (Choreography):
  [orders] 주문 생성 → EventBridge "OrderCreated"
     ↓ SQS
  [inventory] 재고 차감 → "InventoryReserved"
     ↓ SQS
  [payments] 결제 → "PaymentSucceeded"
     ↓ SQS
  [orders] 주문 확정 → "OrderConfirmed"

실패 시 보상:
  [payments] 실패 → "PaymentFailed"
     ↓ SQS
  [inventory] 재고 복원
  [orders] 주문 취소

복잡한 흐름이면 Step Functions로 옮긴다.


8. 직접 확인해보기 — Step Functions

aws stepfunctions create-state-machine \
  --name OrderSaga \
  --definition file://order-saga.json \
  --role-arn <role-arn>

aws stepfunctions start-execution \
  --state-machine-arn <sm-arn> \
  --input '{"orderId":"o-1","userId":"u-1","amount":15000}'

콘솔에서 시각화된 실행 흐름을 볼 수 있다 — 디버깅에 강력하다.


9. 코드로는 이렇게 생겼다 — Terraform (Step Functions)

resource "aws_sfn_state_machine" "order_saga" {
  name     = "OrderSaga"
  role_arn = aws_iam_role.sfn.arn

  definition = jsonencode({
    StartAt = "CreateOrder"
    States = {
      CreateOrder = {
        Type     = "Task"
        Resource = "arn:aws:states:::lambda:invoke"
        Parameters = {
          FunctionName = aws_lambda_function.create_order.arn
          "Payload.$"  = "$"
        }
        Next  = "ReserveStock"
        Retry = [{
          ErrorEquals     = ["States.ALL"]
          IntervalSeconds = 2
          MaxAttempts     = 3
          BackoffRate     = 2
        }]
        Catch = [{
          ErrorEquals = ["States.ALL"]
          Next        = "Fail"
        }]
      }
      ReserveStock  = { ... }
      Charge        = { ... }
      ConfirmOrder  = { Type = "Succeed" }
      Fail          = { Type = "Fail" }
    }
  })
}

10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 분산 트랜잭션을 강요한다

2PC(Two-Phase Commit) 류는 마이크로서비스에 거의 안 맞는다.

안티패턴 2. 보상이 어려운 단계를 앞에 둔다

환불 불가능한 결제를 먼저 하면 보상이 불가능.

되돌릴 수 있는 작업을 앞에, 되돌릴 수 없는 작업을 가장 뒤에

안티패턴 3. 멱등성이 없다

재시도가 두 번째 처리에 또 결제하는 사고를 만든다.

안티패턴 4. Outbox 없이 “DB 쓰고 이벤트 발행”

실패 시 일관성이 깨진다.

Transactional Outbox 또는 CDC(DB Streams)로 풀자


11. 한 줄로 정리

Saga는 분산 트랜잭션의 현실적 대안이며,
“되돌릴 수 있는 단계 + 멱등성 + 명시적 보상” 이 핵심이다


12. 이 장의 핵심 정리

  1. 마이크로서비스에서는 ACID 분산 트랜잭션 대신 Saga를 쓴다.
  2. Choreography(이벤트 안무) 와 Orchestration(컨트롤러) 두 스타일이 있다.
  3. 단순 흐름은 Choreography, 복잡한 흐름은 Step Functions로 Orchestration.
  4. 모든 단계는 멱등해야 한다.
  5. 되돌릴 수 없는 작업은 가장 뒤로 미룬다.
  6. Transactional Outbox로 DB-이벤트 일관성을 지킨다.

78장. IAM 기본 — User · Group · Role · Policy

이 장에서 말하고자 하는 것

지금까지 우리는 ECS · ALB · RDS · S3 같은 자원을 다뤘다.

그 모든 호출의 뒤에는 같은 질문이 깔려 있다.

“누가, 무엇을, 어디서, 할 수 있는가?”

이 질문에 답하는 시스템이

AWS IAM (Identity and Access Management)

이다.

이 장은 IAM의 4개 핵심 개념을 잡는다.

  • User
  • Group
  • Role
  • Policy

1. 가장 큰 그림

누가(주체)  →  무엇을(권한)  →  어떤 자원에  →  어떤 조건에서

User · Role · Group        Policy           Resource         Condition

IAM은 이 4개 축을 조립하는 시스템이다.


2. User — 사람 또는 머신의 신원

IAM User: 한 사용자
  ├─ 이름
  ├─ 비밀번호 / 액세스 키
  └─ MFA 디바이스
  • 사람 운영자
  • CI/CD 시스템 (가능한 한 피하고 Role로 대체)

운영에서는 User 수를 최소화하고
머신 작업은 거의 항상 Role로 풀어야 한다


3. Group — User의 묶음

Group "developers"
  ├─ User alice
  ├─ User bob
  └─ Policy: S3 읽기, ECS Describe …

같은 권한을 여러 User에게 주려고 만든다.

  • 권한은 Group에 붙인다 (User에 직접 안 붙임)
  • Group 자체에는 액세스 키가 없다

4. Role — 임시로 빌리는 신원

가장 중요한 개념.

Role: 누구든지 잠깐 빌려 쓸 수 있는 "신분증"
  ├─ Trust Policy: 누가 이 Role을 받을 수 있나
  └─ Permission Policy: 받은 사람이 무엇을 할 수 있나

EC2 · ECS · Lambda 같은 AWS 자원은 거의 Role을 받아서 AWS API를 호출한다.

[EC2 / ECS Task] → Role 받음 → S3 GetObject 가능
  • 키를 박지 않는다 (자동 회전되는 임시 자격증명)
  • 누가 받았는지 추적 가능
  • 외부 계정도 받을 수 있음 (Cross-Account)

5. Policy — 권한의 문서

JSON으로 표현되는 권한 정의.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject"],
    "Resource": "arn:aws:s3:::msa-uploads/*",
    "Condition": {
      "StringEquals": { "aws:RequestedRegion": "ap-northeast-2" }
    }
  }]
}

핵심 5요소:

  • Effect — Allow / Deny
  • Action — 어떤 호출을 (s3:GetObject)
  • Resource — 어떤 자원에 (arn:aws:s3:::bucket/*)
  • Condition — 어떤 조건일 때만
  • Principal — (Resource-based policy 일 때) 누가

6. 두 종류의 Policy

Identity-based Policy

User · Group · Role 에 붙는다.

"이 사용자/Role은 무엇을 할 수 있나"

Resource-based Policy

자원에 붙는다 (S3 버킷, KMS 키, Lambda 함수 등).

"이 자원에 누가 접근할 수 있나"

S3 버킷 정책 · SQS 큐 정책 · KMS 키 정책이 대표 예다.


7. 평가 순서 — 명시적 Deny 우선

1. 명시적 Deny → 즉시 차단
2. 명시적 Allow → 허용
3. 둘 다 없음 → 묵시적 Deny (차단)

기본은 모든 게 막혀 있고, Allow가 있어야 통과한다
그리고 Deny가 하나라도 있으면 무조건 막힌다

이게 IAM 보안의 기반이다.


8. AWS Managed vs Customer Managed Policy

AWS Managed

AWS가 만들어 제공하는 정책 (예: AmazonS3ReadOnlyAccess).

  • 시작에 편하다
  • 권한 범위가 넓어 운영에 직접 쓰기엔 과한 경우 많음

Customer Managed

본인이 만든 정책.

  • 최소 권한에 맞춰 좁게 작성
  • 운영 환경의 표준

AWS Managed는 출발점, Customer Managed가 운영의 자리다


9. 우리 서비스에서

[사람]
  ├─ IAM User (개발자) ← MFA 필수
  └─ Group "developers" → 콘솔 읽기 권한

[머신]
  ├─ ECS Task Role "orders-task"   → orders-db, SQS publish
  ├─ ECS Task Role "users-task"    → users-db
  ├─ ECS Task Role "payments-task" → payments-db, SQS publish
  ├─ Lambda Role "thumbnail"        → S3 read/write 특정 경로
  └─ GitHub Actions Role            → ECR push, ECS update (OIDC)
  • 사람에게는 최소한의 User
  • 머신은 모두 Role
  • 각 Role은 자기 일에만 권한

10. 직접 확인해보기 — CLI

User · Group · Role

aws iam create-user --user-name alice
aws iam create-group --group-name developers
aws iam add-user-to-group --user-name alice --group-name developers
aws iam create-role --role-name orders-task \
  --assume-role-policy-document file://trust-policy.json

Policy 붙이기

aws iam put-role-policy \
  --role-name orders-task \
  --policy-name orders-db-access \
  --policy-document file://policy.json

“내가 이 작업을 할 수 있나” 시뮬레이션

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123:role/orders-task \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::msa-uploads/users/123/x.png

운영 디버깅에 매우 자주 쓴다.


11. 코드로는 이렇게 생겼다 — Terraform

# 사람 그룹
resource "aws_iam_group" "devs" {
  name = "developers"
}

# Group에 정책 붙이기 (AWS Managed)
resource "aws_iam_group_policy_attachment" "devs_readonly" {
  group      = aws_iam_group.devs.name
  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

# ECS Task Role
data "aws_iam_policy_document" "ecs_trust" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "orders_task" {
  name               = "orders-task"
  assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
}

# 좁은 권한 (Customer Managed)
resource "aws_iam_role_policy" "orders_db" {
  role = aws_iam_role.orders_task.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = ["rds-db:connect"]
      Resource = "arn:aws:rds-db:ap-northeast-2:...:dbuser:orders-db/orders-app"
    }]
  })
}

12. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. EC2 / ECS에 IAM User 키를 박는다

키가 새어 나가면 회수가 어렵고 권한이 그대로 살아 있다.

머신은 Role을 받는다 — 키는 임시 자동 회전

안티패턴 2. 루트 계정으로 일상 작업

루트는 모든 권한을 가진다 — 사고 한 번에 끝.

루트는 MFA 켜고 잠가두고, 일상은 IAM User / Role

안티패턴 3. Action / Resource를 * 로 둔다

“잠깐 안 되네” 의 응급 처치가 영구 권한이 된다.

안티패턴 4. 명시적 Deny를 함부로 쓴다

Deny는 강력해서 의도치 않게 다른 권한까지 막을 수 있다.

가능한 한 Allow를 좁게 — Deny는 진짜 차단이 필요할 때만


13. 한 줄로 정리

IAM은 User · Group · Role · Policy 의 4축이며,
머신은 거의 항상 Role로 푸는 게 운영의 표준이다


14. 이 장의 핵심 정리

  1. IAM의 4축: User · Group · Role · Policy.
  2. Role은 임시 자격증명 — 머신 작업의 거의 모든 답.
  3. Policy는 Effect · Action · Resource · Condition 으로 이뤄진다.
  4. 평가 순서: Deny가 한 번이라도 있으면 무조건 차단.
  5. AWS Managed는 출발점, Customer Managed가 운영의 자리.
  6. 루트는 잠가두고, 일상 작업은 IAM User · Role.

79장. IAM Role 활용 — EC2 · ECS Task Role · Execution Role

이 장에서 말하고자 하는 것

앞 장에서 IAM의 4축을 봤다.

이 장은 실전에서 가장 자주 다루는

Role의 활용 패턴

을 정리한다.

특히 ECS 환경에서 헷갈리는 두 Role을 명확히 한다.

  • Execution Role
  • Task Role

1. Trust Policy vs Permission Policy

Role은 두 종류의 정책을 가진다.

Trust Policy    : 누가 이 Role을 받을 수 있나
Permission Policy: 받은 사람이 무엇을 할 수 있나

EC2 인스턴스에 붙이는 Role의 Trust Policy:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ec2.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}

이러면 “EC2 서비스가 이 Role을 받을 수 있다” 가 된다.


2. EC2 Instance Profile

EC2가 Role을 직접 받을 수는 없다.
중간에 Instance Profile 을 거친다.

[Role]
  ↓ 담는다
[Instance Profile]
  ↓ EC2에 붙는다
[EC2 instance]
  ↓ 임시 자격증명 자동 발급
[Application]
  ↓ AWS SDK 호출 → S3 / DynamoDB / …

콘솔에서 Role을 만들면 Instance Profile이 자동으로 만들어진다.
Terraform에서는 명시적으로 만들어야 한다.


3. ECS의 두 Role — Execution vs Task

ECS에서는 두 종류의 Role 이 있다.

[ECS Task Definition]
  ├─ executionRoleArn   ← Execution Role
  └─ taskRoleArn        ← Task Role

Execution Role

Task를 띄우기 위해 ECS 에이전트가 쓰는 권한

  • ECR 이미지 pull
  • CloudWatch Logs에 로그 쓰기
  • Secrets Manager에서 비밀 가져오기 (secrets 필드)

Task Role

Task 안의 컨테이너가 AWS API를 호출할 때 쓰는 권한

  • S3 GetObject
  • DynamoDB Query
  • SQS SendMessage

4. 두 Role을 헷갈리면 일어나는 증상

Task가 시작 안 됨        → Execution Role 문제
  - "이미지 못 가져와요"
  - "로그 그룹에 못 써요"
  - "Secret 못 읽어요"

Task는 떴는데 API 거부됨  → Task Role 문제
  - "S3 GetObject denied"
  - "DynamoDB Query unauthorized"

이 두 증상이 운영에서 가장 자주 마주친다.


5. Lambda Role

Lambda 함수도 자기 Role을 가진다.

[Lambda]
  ├─ Execution Role (Lambda가 받음)
  │   ├─ CloudWatch Logs 쓰기
  │   └─ 함수가 호출할 AWS API 권한
  └─ 자동 발급되는 임시 자격증명

ECS와 달리 Execution Role / Task Role 구분이 없다 — Lambda는 한 Role.


6. Cross-Account Role — 다른 계정에 권한 빌려주기

[A 계정의 Role: ReadOnlyRoleForB]
  Trust Policy: B 계정만 받을 수 있음

[B 계정 사용자] → AssumeRole → 임시 자격증명 → A 계정 자원 접근
  • 멀티 계정 환경에서 표준
  • AWS Organizations 환경에 자주 등장
  • 외부 서드파티(DataDog · 빌링 도구 등) 에 권한 줄 때도 사용

7. OIDC Federation — 외부 신원으로 Role 받기

GitHub Actions · GitLab · Kubernetes ServiceAccount 같은 외부 신원이
Role을 받을 수 있다.

[GitHub Actions Workflow]
  ↓ OIDC 토큰 발급
[AWS IAM OIDC Provider]
  ↓ AssumeRoleWithWebIdentity
[Role: GitHubDeployRole]
  ↓ 임시 자격증명
[배포 작업]
  • CI에 액세스 키를 박지 않는다
  • 짧은 임시 자격증명만 사용
  • 어느 워크플로우인지 추적 가능

새 CI/CD는 거의 항상 OIDC로 풀어야 한다


8. 우리 서비스에서

Execution Role (공용)
  ├─ ecr:GetAuthorizationToken
  ├─ ecr:BatchGetImage
  ├─ logs:CreateLogStream / PutLogEvents
  └─ secretsmanager:GetSecretValue (특정 시크릿만)

Task Role (서비스마다 별도)
  orders-task:
    ├─ rds-db:connect (orders-db 만)
    └─ sns:Publish (order-events 토픽만)
  users-task:
    └─ rds-db:connect (users-db 만)
  catalog-task:
    └─ dynamodb:Query / GetItem (products 테이블만)

GitHub Actions Role (OIDC)
  ├─ ecr push
  ├─ ecs update-service
  └─ cloudfront create-invalidation

각 Role은 자기 일에 필요한 권한만 가진다.


9. 직접 확인해보기 — CLI

Role 만들기 (Trust Policy)

aws iam create-role \
  --role-name orders-task \
  --assume-role-policy-document '{
    "Version":"2012-10-17",
    "Statement":[{
      "Effect":"Allow",
      "Principal":{"Service":"ecs-tasks.amazonaws.com"},
      "Action":"sts:AssumeRole"
    }]
  }'

EC2 Instance Profile

aws iam create-instance-profile --instance-profile-name web
aws iam add-role-to-instance-profile \
  --instance-profile-name web \
  --role-name web-role

AssumeRole로 임시 자격증명 받기 (Cross-Account)

aws sts assume-role \
  --role-arn arn:aws:iam::123456789:role/ReadOnly \
  --role-session-name my-session

10. 코드로는 이렇게 생겼다 — Terraform (ECS Task Roles)

# Execution Role (모든 ECS Task가 공용으로 받음)
resource "aws_iam_role" "task_execution" {
  name = "ecs-task-execution"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ecs-tasks.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "task_execution_basic" {
  role       = aws_iam_role.task_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# orders 서비스 Task Role
resource "aws_iam_role" "orders_task" {
  name = "orders-task"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ecs-tasks.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "orders_task_perms" {
  role = aws_iam_role.orders_task.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = "sns:Publish"
        Resource = aws_sns_topic.order_events.arn
      }
    ]
  })
}

task_role_arnexecution_role_arn 을 명확히 분리한다.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. Execution Role과 Task Role을 합친다

보안이 약해진다 (불필요한 권한이 컨테이너 안에 그대로 보임).

안티패턴 2. 한 Task Role을 모든 서비스에 공용으로 쓴다

한 서비스의 권한이 모든 서비스에 흘러간다.

서비스마다 별도 Task Role, 각자 최소 권한

안티패턴 3. CI에 IAM User 키를 박는다

키 유출이 곧 운영 사고로 이어진다.

OIDC + AssumeRole로 키 없이 풀어낸다

안티패턴 4. AssumeRole 권한을 너무 넓게 준다

“누구나 어떤 Role이든 받아갈 수 있다” 같은 Trust Policy는 사고난다.

Trust Policy도 최소 범위로


12. 한 줄로 정리

Role은 머신/외부 신원에 임시 자격증명을 발급하는 도구이며,
ECS의 Execution / Task Role 분리와 CI의 OIDC 활용이 운영의 토대다


13. 이 장의 핵심 정리

  1. Role은 Trust Policy(누가 받을 수 있나) + Permission Policy(무엇을 할 수 있나) 다.
  2. EC2는 Instance Profile을 거쳐 Role을 받는다.
  3. ECS는 Execution Role(Task 띄움용) + Task Role(컨테이너 코드용) 두 가지가 있다.
  4. Lambda는 Execution Role 하나만 가진다.
  5. Cross-Account · OIDC Federation으로 외부 신원에도 Role을 줄 수 있다.
  6. CI/CD는 OIDC로 키 없이 운영하는 게 표준이다.

80장. 최소 권한 설계

이 장에서 말하고자 하는 것

IAM Policy는 자유롭게 쓸 수 있다.

Effect: Allow
Action: "*"
Resource: "*"

이 두 줄이면 모든 게 동작한다.

하지만 운영 보안의 절반은 이 유혹을 이겨내는 일이다.

Principle of Least Privilege (최소 권한 원칙)

이 장은 그 설계 방법을 정리한다.


1. 최소 권한이란 무엇인가

어떤 주체가 자기 일을 하는 데 꼭 필요한 권한만 가진다

  • 더 많은 권한은 사고를 키운다
  • 더 적은 권한은 정상 작업을 막는다

균형이 어렵지만 방향은 명확하다.

기본은 “다 막혀 있고, 필요한 것만 연다”


2. 권한이 새어 나가는 흔한 자리

1. * Action / Resource

{ "Action": "s3:*", "Resource": "*" }

가장 빈번한 사고 원인.

2. 와이드한 Managed Policy

AdministratorAccess, PowerUserAccess 같은 광범위 정책을 컨테이너에 박는다.

3. “잠깐만” 으로 추가한 권한이 영구화된다

디버깅용으로 풀어준 권한이 그대로 운영에 남는다.

4. 모든 환경에 같은 Role 사용

dev / stage / prod 가 같은 Role을 공유하면 한쪽 사고가 다른 쪽에 번진다.


3. 좁히는 방법 — Action

먼저 어떤 Action 이 필요한지 구체적으로 적는다.

"Action": [
  "s3:GetObject",
  "s3:PutObject"
]

s3:* 대신 실제 호출하는 API 만.


4. 좁히는 방법 — Resource

자원도 구체적으로.

"Resource": [
  "arn:aws:s3:::msa-uploads/*"
]

* 대신 버킷 이름 · 키 prefix 까지.

DynamoDB:

"Resource": "arn:aws:dynamodb:ap-northeast-2:123:table/orders"

5. 좁히는 방법 — Condition

특정 조건일 때만 허용.

"Condition": {
  "StringEquals": { "aws:RequestedRegion": "ap-northeast-2" },
  "Bool":         { "aws:SecureTransport": "true" }
}
  • 특정 리전에서만
  • HTTPS 호출만
  • 특정 VPC Endpoint 통해서만
  • 특정 시간대에만

6. 환경별 분리

prod / stage / dev 의 IAM은 분리한다

방법:

  • 별도 AWS 계정 — 가장 깔끔 (Organizations 사용)
  • 같은 계정 안에서 Role · 정책 이름 · 자원 prefix 로 분리
prod-orders-task vs dev-orders-task
arn:aws:s3:::msa-prod-uploads vs msa-dev-uploads

7. IAM Access Analyzer — 자동 점검

AWS가 제공하는 분석 도구.

  • 외부에 노출된 자원 자동 발견
  • 사용되지 않는 권한 식별
  • Policy 작성 시 권장 사항 제시
aws accessanalyzer list-findings

정기적으로 검토 — 안 쓰는 권한은 제거


8. 권한 사용 로그 — CloudTrail

CloudTrail은 모든 AWS API 호출 로그를 남긴다.

"이 Role 이 지난 90일 동안 실제로 어떤 Action을 호출했나"

IAM 콘솔에 “Access Advisor” 가 이걸 요약해서 보여준다.

“이 Role이 실제로 쓴 Action 만 남기고 나머지 정리”

운영 정리 작업의 표준.


9. SCP — 조직 차원의 한도

AWS Organizations 환경에서는 Service Control Policy 로 조직 전체에 한도를 건다.

"우리 조직 안의 어떤 계정도 us-east-1 외에는 못 쓴다"
"루트 계정의 일부 권한은 무조건 차단"
"특정 서비스는 사용 금지"

SCP는 Allow 이전에 작동하는 천장이다 — 개별 계정의 IAM이 더 넓어도 SCP가 막는다.


10. 우리 서비스에서

[orders-task Role]
  Allow:
    rds-db:connect   → orders-db / orders-app
    sns:Publish      → order-events 토픽
    secretsmanager:GetSecretValue → orders/db-password
  Condition:
    aws:RequestedRegion = ap-northeast-2
    aws:SecureTransport = true
  Deny:
    iam:* (절대 IAM 변경 못 함)

각 서비스의 Task Role이 이 모양으로 좁아진다.


11. 직접 확인해보기 — CLI

정책 시뮬레이션

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123:role/orders-task \
  --action-names s3:DeleteObject \
  --resource-arns arn:aws:s3:::msa-uploads/x

“이 Role이 이 작업을 할 수 있나” 를 사전 검증.

Access Advisor (콘솔)

IAM → Role → Access Advisor → 마지막 사용 시각

90일 이상 안 쓴 권한은 정리 후보.


12. 코드로는 이렇게 생겼다 — Terraform

data "aws_iam_policy_document" "orders_task" {
  statement {
    actions   = ["rds-db:connect"]
    resources = [
      "arn:aws:rds-db:ap-northeast-2:${data.aws_caller_identity.current.account_id}:dbuser:${aws_db_instance.orders.resource_id}/orders-app"
    ]
  }

  statement {
    actions   = ["sns:Publish"]
    resources = [aws_sns_topic.order_events.arn]
  }

  statement {
    actions   = ["secretsmanager:GetSecretValue"]
    resources = [aws_secretsmanager_secret.orders_db.arn]
  }

  statement {
    sid       = "RegionLock"
    effect    = "Deny"
    actions   = ["*"]
    resources = ["*"]
    condition {
      test     = "StringNotEquals"
      variable = "aws:RequestedRegion"
      values   = ["ap-northeast-2"]
    }
  }
}

resource "aws_iam_role_policy" "orders_task" {
  role   = aws_iam_role.orders_task.id
  policy = data.aws_iam_policy_document.orders_task.json
}

좁은 Allow + 명확한 Deny 가 한 묶음.


13. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. *:* 정책 그대로 운영

한 번도 좁히지 않은 정책이 사고의 첫 단추가 된다.

안티패턴 2. AdministratorAccess 를 컨테이너에

운영 컨테이너에 IAM · 빌링 권한까지 들어가면 침해 시 회복 불가능.

안티패턴 3. 디버깅용 권한이 영구화

임시로 풀어준 권한은 일정에 잡고 회수한다.

안티패턴 4. 정책을 한 번 만들고 다시 안 본다

서비스가 진화하면 권한도 달라진다 — 분기마다 정리.


14. 한 줄로 정리

최소 권한은 한 번에 완성되지 않는다 —
좁게 시작 → 실제 사용 로그로 다듬기 → 정기 회수의 반복이다


15. 이 장의 핵심 정리

  1. 기본은 “다 막혀 있고 필요한 것만 연다”.
  2. Action · Resource · Condition 세 축을 모두 좁힌다.
  3. 환경(prod/stage/dev) 은 IAM도 분리한다.
  4. Access Analyzer · Access Advisor · CloudTrail 로 실제 사용을 검증한다.
  5. SCP는 조직 차원의 천장이다.
  6. 최소 권한은 분기마다 정리하는 운영 작업이다.

81장. KMS — 데이터 암호화

이 장에서 말하고자 하는 것

데이터 보호의 두 축이 있다.

  • 전송 중 암호화 (in transit) — TLS
  • 저장 중 암호화 (at rest) — 디스크에 쓸 때 암호화

저장 중 암호화를 다루는 AWS 핵심 도구가

AWS KMS (Key Management Service)

다.

S3 · RDS · EBS · DynamoDB · Secrets Manager · CloudWatch Logs 등
거의 모든 데이터 저장 서비스가 KMS와 연동된다.


1. KMS의 기본

KMS는 암호화 키를 안전하게 만들고 관리 해주는 서비스다.

[KMS Key]
  ├─ 실제 키 값은 KMS 밖으로 절대 나가지 않음
  └─ 호출자는 키 ID로 "암호화 해줘 / 복호화 해줘" 만 요청

키 값을 사람이 직접 다루지 않는 게 핵심이다.


2. 두 종류의 키

AWS Managed Key

AWS가 자동으로 만들고 회전한다 (예: aws/s3, aws/rds).

  • 시작에 편하다
  • 키 정책을 본인이 수정할 수 없다

Customer Managed Key (CMK)

본인이 만들고 관리한다.

  • 키 정책 · 회전 주기 · 접근 권한을 직접 통제
  • 키 삭제 (대기 기간 후) 가능
  • 운영 환경의 표준

시작은 AWS Managed, 운영 자원은 CMK로 옮긴다


3. Envelope Encryption — 큰 데이터를 효율적으로

KMS API는 작은 데이터만 직접 암호화한다 (~4KB).

큰 데이터는 Envelope Encryption 패턴.

1. KMS에서 Data Key 발급
2. Data Key로 큰 데이터를 암호화 (애플리케이션 측에서)
3. Data Key 자체는 KMS Key로 암호화해 저장

대부분의 AWS 서비스 (S3 · EBS · RDS …) 가 내부적으로 이걸 자동으로 한다.


4. 키 회전

CMK는 자동 회전 옵션을 켤 수 있다.

365일마다 새 키 값으로 회전
옛 키 값은 그대로 보관 (옛 데이터 복호화 가능)

호출자는 키 ID만 가지고 있으면 변화를 느끼지 못한다.

운영 CMK는 자동 회전을 켜는 게 표준


5. 키 정책 (Key Policy)

KMS Key는 자체적인 Key Policy 를 가진다 (Resource-based).

"이 키를 누가 쓸 수 있나"
"이 키를 누가 관리할 수 있나" (관리자 / 사용자 분리)

운영에서는 보통

관리자 그룹 → 키 정책 변경, 삭제
서비스 Role → 사용만 (Encrypt / Decrypt)

로 분리한다.


6. 어디에 쓰이는가

서비스KMS 활용
S3SSE-KMS — 객체별 자동 암호화
EBS볼륨 암호화
RDS / Aurora스토리지 암호화 + 백업 암호화
DynamoDB테이블 데이터 + 백업 암호화
Secrets Manager시크릿 값 암호화
CloudWatch Logs로그 그룹 암호화
Lambda환경 변수 암호화
SQS · SNS메시지 암호화 (SSE)

거의 모든 데이터 저장소가 KMS 위에 산다.


7. KMS는 비싸진다 — 호출 횟수

KMS는 호출당 과금이다.

$0.03 / 10,000건 (대략)

작은 비용 같지만 초당 수천 번 호출하면 누적된다.

해법:

  • Envelope Encryption (이미 대부분 자동)
  • DEK 캐싱 (애플리케이션 측에서 짧게 캐시)
  • S3 Bucket Keys (S3 SSE-KMS의 호출 수를 크게 줄임)

8. 우리 서비스에서

[CMK들]
  ├─ kms/prod-db        → RDS · DynamoDB 암호화
  ├─ kms/prod-storage   → S3 버킷 암호화
  └─ kms/prod-secrets   → Secrets Manager 시크릿

각 키는 자동 회전 ON, 키 정책은 서비스별로 사용 권한 분리
  • 사람 관리자: 키 정책 관리
  • ECS Task Role: 자기 서비스가 쓰는 키만 Encrypt/Decrypt 허용

9. 직접 확인해보기 — CLI

# CMK 만들기
aws kms create-key \
  --description "prod-storage" \
  --key-spec SYMMETRIC_DEFAULT

# 자동 회전 켜기
aws kms enable-key-rotation --key-id <key-id>

# 별칭(alias) 만들기 (사람이 기억하기 쉽게)
aws kms create-alias \
  --alias-name alias/prod-storage \
  --target-key-id <key-id>

# S3 버킷 암호화 적용
aws s3api put-bucket-encryption \
  --bucket msa-prod-uploads \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "alias/prod-storage"
      },
      "BucketKeyEnabled": true
    }]
  }'

BucketKeyEnabled: true 가 비용 절감의 핵심.


10. 코드로는 이렇게 생겼다 — Terraform

resource "aws_kms_key" "storage" {
  description             = "prod-storage"
  enable_key_rotation     = true
  deletion_window_in_days = 30

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "EnableRootAdmin"
        Effect    = "Allow"
        Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
        Action    = "kms:*"
        Resource  = "*"
      },
      {
        Sid       = "AllowOrdersTask"
        Effect    = "Allow"
        Principal = { AWS = aws_iam_role.orders_task.arn }
        Action    = ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"]
        Resource  = "*"
      }
    ]
  })
}

resource "aws_kms_alias" "storage" {
  name          = "alias/prod-storage"
  target_key_id = aws_kms_key.storage.id
}

resource "aws_s3_bucket_server_side_encryption_configuration" "uploads" {
  bucket = aws_s3_bucket.uploads.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_alias.storage.arn
    }
    bucket_key_enabled = true
  }
}

11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 모든 자원에 같은 키 하나

폭발 반경이 커진다.

도메인/환경/용도별로 키 분리

안티패턴 2. Bucket Keys를 안 켜고 S3에 SSE-KMS

S3 GET 마다 KMS 호출이 발생해 비용이 폭증한다.

안티패턴 3. 키 정책을 너무 넓게

누구나 Decrypt 할 수 있으면 KMS의 의미가 줄어든다.

사용 권한은 서비스 Role 단위로 좁게

안티패턴 4. 자동 회전을 안 켠다

운영 키는 자동 회전을 켜는 게 기본.


12. 한 줄로 정리

KMS는 키를 직접 다루지 않고 안전하게 쓸 수 있게 해주는 도구이며,
저장 중 암호화 + 자동 회전 + 좁은 키 정책 세 가지가 운영의 토대다


13. 이 장의 핵심 정리

  1. KMS는 키 값을 노출하지 않고 암호화를 제공하는 서비스다.
  2. 운영 자원은 Customer Managed Key (CMK) 로 옮긴다.
  3. Envelope Encryption으로 큰 데이터도 효율적으로 처리한다.
  4. 키 정책으로 관리자 / 사용자 권한을 분리한다.
  5. S3는 SSE-KMS + Bucket Keys 조합이 표준.
  6. 자동 회전은 거의 항상 켠다.

82장. Secrets Manager와 Parameter Store

이 장에서 말하고자 하는 것

애플리케이션은 다음 같은 비밀을 다룬다.

  • DB 비밀번호
  • API 키
  • OAuth 클라이언트 시크릿
  • 인증서 / 키 파일

이걸 어디에 두고, 어떻게 가져올까?

AWS의 두 가지 옵션:

  • AWS Secrets Manager
  • AWS Systems Manager Parameter Store

이름이 비슷한데 자리가 다르다.


1. 둘의 차이

항목Secrets ManagerParameter Store
본질비밀 전용설정/비밀 둘 다
자동 회전✅ (Lambda로 구현)
가격비밀당 월 비용 + 호출당표준 무료 + 호출
크기64KB표준 4KB / 고급 8KB
버전 관리자동자동
RDS 통합✅ (자동 회전 내장)부분

비밀은 Secrets Manager, 단순 설정은 Parameter Store
가벼운 운영에서는 Parameter Store SecureString 으로도 충분


2. Secrets Manager — 비밀 전용

secret "orders/db-password"
  ├─ 현재 버전: AWSCURRENT
  ├─ 옛 버전: AWSPREVIOUS
  └─ Rotation Lambda: 30일마다 DB 비밀번호 갱신
  • RDS · Aurora 비밀번호를 자동 회전
  • KMS로 자동 암호화
  • JSON 객체 통째로 저장 가능 ({ "username":"...", "password":"..." })

3. Parameter Store — 설정 + 가벼운 비밀

/orders/prod/api-url     → "https://internal/orders"
/orders/prod/db-password → SecureString (KMS 암호화)
/orders/prod/feature-x   → "enabled"
  • 계층적 이름 (/...)
  • 일반 String · 안전한 SecureString · StringList
  • 무료 (표준 등급)

비밀은 SecureString을 쓰고 KMS 키를 지정한다.


4. ECS Task와의 통합

가장 흔한 사용법.

Task Definition의 container.secrets:
  - DB_PASSWORD → arn:aws:secretsmanager:...:secret:orders/db-password
  - API_KEY     → arn:aws:ssm:...:parameter/orders/prod/api-key

ECS가 Task 시작 시점에 가져와 환경 변수로 주입 한다.

컨테이너 안에서:
  process.env.DB_PASSWORD ← 그냥 평범한 환경 변수처럼 쓰면 됨

애플리케이션 코드는 비밀 가져오는 로직을 가지지 않아도 된다
Execution Role에 GetSecretValue 권한만 있으면 끝


5. 비밀이 코드/저장소에 남지 않게 하기

1. Git에 비밀 커밋 금지

.env 같은 파일에 박지 않는다. git-secrets 같은 도구로 자동 검사.

2. Terraform state에도 평문 비밀 X

manage_master_user_password = true 같은 옵션을 써서 Secrets Manager가 알아서 만들도록.

3. 로그에 비밀이 출력되지 않게

실수로 console.log(config) 했을 때 비밀이 CloudWatch에 들어가지 않도록.


6. 자동 회전 — Secrets Manager의 진가

30일마다 자동으로 새 비밀번호 생성
  ↓
1. Secrets Manager가 새 비밀번호 발급
2. DB에 새 비밀번호 설정
3. Secrets Manager의 AWSCURRENT 업데이트
4. 애플리케이션은 다음 GetSecretValue 호출에서 새 비밀번호를 받음
  • RDS · Aurora는 내장 회전 기능 제공
  • DocumentDB · Redshift도 일부 지원
  • 일반 비밀은 직접 Lambda 작성

사람이 비밀번호를 손으로 바꾸는 일이 사라진다


7. 우리 서비스에서

Secrets Manager:
  orders/db-password   ← Multi-AZ RDS, 자동 회전 30일
  users/db-password    ← 같음
  payments/db-password ← 같음
  third-party/stripe-key
  third-party/twilio-token

Parameter Store:
  /orders/prod/api-url
  /orders/prod/feature-flags
  /orders/prod/log-level

비밀은 Secrets Manager, 단순 설정은 Parameter Store.


8. 직접 확인해보기 — CLI

Secrets Manager

aws secretsmanager create-secret \
  --name orders/db-password \
  --secret-string '{"username":"app","password":"<auto>"}'

aws secretsmanager get-secret-value --secret-id orders/db-password

# 회전 설정
aws secretsmanager rotate-secret \
  --secret-id orders/db-password \
  --rotation-lambda-arn <lambda-arn> \
  --rotation-rules AutomaticallyAfterDays=30

Parameter Store

aws ssm put-parameter \
  --name /orders/prod/api-url \
  --value "https://internal/orders" \
  --type String

aws ssm put-parameter \
  --name /orders/prod/api-key \
  --value "secret123" \
  --type SecureString \
  --key-id alias/prod-secrets

aws ssm get-parameter \
  --name /orders/prod/api-key \
  --with-decryption

9. 코드로는 이렇게 생겼다 — Terraform

# Secrets Manager
resource "aws_secretsmanager_secret" "orders_db" {
  name        = "orders/db-password"
  description = "orders RDS master password"
  kms_key_id  = aws_kms_key.secrets.arn
}

# RDS가 자동으로 비밀번호를 만들고 회전하도록 두는 게 더 깔끔
resource "aws_db_instance" "orders" {
  ...
  manage_master_user_password = true
  master_user_secret_kms_key_id = aws_kms_key.secrets.id
}

# Parameter Store
resource "aws_ssm_parameter" "api_url" {
  name  = "/orders/prod/api-url"
  type  = "String"
  value = "https://internal/orders"
}

resource "aws_ssm_parameter" "feature_flag" {
  name  = "/orders/prod/feature-x"
  type  = "String"
  value = "enabled"
}

ECS Task Definition의 secrets:

secrets = [
  {
    name      = "DB_PASSWORD"
    valueFrom = aws_secretsmanager_secret.orders_db.arn
  },
  {
    name      = "API_URL"
    valueFrom = aws_ssm_parameter.api_url.arn
  }
]

이렇게 두면 컨테이너 안에서는 그냥 환경 변수.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 비밀을 평문 환경 변수로 박는다

Task Definition · 배포 로그 · CloudFormation에 평문이 남는다.

항상 secrets 로 가져온다

안티패턴 2. .env 파일을 Git에 커밋

가장 흔한 사고. 회수도 어렵다.

안티패턴 3. 비밀 회전을 한 번도 안 한다

유출된 비밀이 영원히 살아 있다.

안티패턴 4. 모든 비밀을 한 시크릿에 합친다

한 비밀의 회수 / 회전이 다른 비밀에 영향을 준다.

비밀 단위는 좁게 — 서비스 · 용도 별로


11. 한 줄로 정리

비밀은 Secrets Manager로 자동 회전 · 자동 주입,
단순 설정은 Parameter Store — 코드/저장소에 평문 비밀을 남기지 않는다


12. 이 장의 핵심 정리

  1. 비밀 전용은 Secrets Manager, 일반 설정은 Parameter Store.
  2. RDS는 manage_master_user_password 로 Secrets Manager 자동 관리가 가능하다.
  3. ECS의 secrets 필드로 컨테이너에 자동 주입 — 코드는 평범한 환경 변수처럼 쓴다.
  4. 자동 회전 (Secrets Manager) 으로 비밀의 수명을 통제한다.
  5. 비밀은 좁게 — 서비스 · 용도 단위 분리.
  6. Git · 로그 · Terraform state에 평문 비밀이 남지 않게 한다.

83장. CloudWatch 메트릭과 알람

이 장에서 말하고자 하는 것

마이크로서비스 환경의 운영 난이도 절반은

“지금 무슨 일이 일어나고 있는지 보이지 않는다”

에서 온다.

이걸 풀어주는 게 관측성(Observability) 이다.

AWS의 출발점은

Amazon CloudWatch

이고, 그 중에서도 가장 기본이

메트릭(Metrics) + 알람(Alarms)

이다.


1. 메트릭 — 시간에 따라 변하는 숫자

CPUUtilization  : 65.3 (2026-01-01 10:00:00)
RequestCount    : 1240 (2026-01-01 10:00:00)
Latency         : 184  (2026-01-01 10:00:00)

AWS 서비스 대부분이 자기 메트릭을 자동으로 CloudWatch에 보낸다.

EC2          → CPU · 네트워크 · 디스크
ALB          → RequestCount · 5xx · TargetResponseTime
ECS          → CPU · Memory · TaskCount
RDS          → CPU · Connections · ReplicaLag
DynamoDB     → ConsumedCapacity · ThrottledRequests
SQS          → ApproximateNumberOfMessages
CloudFront   → Requests · CacheHitRate

운영자는 이 숫자들을 본다.


2. Namespace · Dimension · Metric

Namespace  : "AWS/ECS"
Metric Name: "CPUUtilization"
Dimensions : ServiceName=orders, ClusterName=msa
Statistic  : Average / Sum / Max / Min / p95 / p99
Period     : 1분 / 5분 / …

이 5개 축이 메트릭의 좌표다.

같은 이름의 메트릭도 Dimension 이 다르면 별도 시계열


3. 통계와 백분위 (p95 · p99)

평균은 진실을 가린다.

평균 응답 시간: 80ms
하지만 p99: 1.2초   ← 1%의 사용자는 끔찍한 경험

운영에서 자주 보는 건

  • p50 — 중앙값
  • p95 — 상위 5%의 경계
  • p99 — 상위 1%의 경계

평균과 p99 를 함께 본다


4. 알람 (Alarm)

조건: 지표가 임계를 넘으면 알린다
ALB 5xx > 10건 / 5분  → 알람 발생
        ↓
SNS → Slack · 이메일 · 호출

알람의 상태:

  • OK — 정상
  • ALARM — 조건 충족
  • INSUFFICIENT_DATA — 데이터 부족

알람이 시끄러우면 사람이 무시한다.
“정말 행동이 필요할 때만” 울리도록 만든다.


5. Composite Alarm — 여러 조건의 조합

"ALB 5xx 가 늘고 AND ECS CPU 가 높을 때만" → 진짜 운영 문제

단일 메트릭 알람은 노이즈가 많다.
Composite Alarm으로 조합해 정밀도를 높인다.


6. Custom Metric — 우리 애플리케이션 지표

AWS 메트릭만으로 부족할 때, 코드에서 직접 메트릭을 보낸다.

OrdersCreated     (도메인 지표)
PaymentLatency
QueueLag

방법:

  • PutMetricData API 호출
  • CloudWatch Agent 의 StatsD 인터페이스
  • CloudWatch Embedded Metric Format (EMF) — 로그 한 줄에 메트릭을 포함

EMF가 컨테이너 환경에 가장 자연스럽다 (로그만 출력하면 자동 메트릭화)


7. Dashboard

여러 메트릭을 한 화면에서 본다.

[서비스 orders 대시보드]
  ├─ RequestCount
  ├─ p95 / p99 Latency
  ├─ 5xx Rate
  ├─ ECS CPU / Memory
  └─ RDS Connections · ReplicaLag

서비스마다 하나씩.


8. 어떤 메트릭에 알람을 거나 — 출발점

사용자 관점 (가장 중요)

  • 응답 시간 p95 / p99
  • 에러율 (4xx / 5xx)

시스템 관점

  • ECS CPU · Memory
  • RDS CPU · Connections · ReplicaLag
  • SQS Queue depth · ApproximateAgeOfOldestMessage

비용 관점

  • DynamoDB ThrottledRequests
  • NAT Gateway BytesOut
  • Lambda Errors / Throttles

SLO(서비스 수준 목표) 기반으로 정한다
“p95 가 200ms 이하” 같은 약속에서 알람이 나온다


9. 우리 서비스에서

서비스마다 (orders / users / payments):
  ├─ ALB 5xx > 1%        → Slack
  ├─ ALB p99 > 1s         → Slack
  ├─ ECS CPU > 80%        → Slack
  ├─ Task 가용 < min      → Page (호출)

DB:
  ├─ RDS CPU > 80%
  ├─ RDS Connections > 80% of max
  └─ ReplicaLag > 30s

큐:
  ├─ DLQ 메시지 > 0       → Page
  └─ ApproximateAge > 10분

알람의 등급을 나눈다 — 모두 호출하지 않는다.


10. 직접 확인해보기 — CLI

메트릭 조회

aws cloudwatch get-metric-statistics \
  --namespace AWS/ECS \
  --metric-name CPUUtilization \
  --dimensions Name=ServiceName,Value=orders Name=ClusterName,Value=msa \
  --start-time 2026-01-01T00:00:00Z \
  --end-time   2026-01-02T00:00:00Z \
  --period 60 \
  --statistics Average

알람 만들기

aws cloudwatch put-metric-alarm \
  --alarm-name orders-cpu-high \
  --metric-name CPUUtilization \
  --namespace AWS/ECS \
  --statistic Average \
  --dimensions Name=ServiceName,Value=orders Name=ClusterName,Value=msa \
  --period 60 \
  --threshold 80 \
  --comparison-operator GreaterThanThreshold \
  --evaluation-periods 5 \
  --alarm-actions arn:aws:sns:...:ops-alerts

커스텀 메트릭 (PutMetricData)

aws cloudwatch put-metric-data \
  --namespace MyApp \
  --metric-name OrdersCreated \
  --value 1 \
  --dimensions Service=orders,Env=prod

11. 코드로는 이렇게 생겼다 — Terraform

resource "aws_cloudwatch_metric_alarm" "orders_5xx" {
  alarm_name          = "orders-alb-5xx"
  namespace           = "AWS/ApplicationELB"
  metric_name         = "HTTPCode_Target_5XX_Count"
  statistic           = "Sum"
  period              = 60
  evaluation_periods  = 5
  threshold           = 10
  comparison_operator = "GreaterThanThreshold"

  dimensions = {
    LoadBalancer = aws_lb.main.arn_suffix
    TargetGroup  = aws_lb_target_group.orders.arn_suffix
  }

  alarm_actions = [aws_sns_topic.ops_alerts.arn]
  ok_actions    = [aws_sns_topic.ops_alerts.arn]

  treat_missing_data = "notBreaching"
}

treat_missing_data 설정이 운영에서 중요하다 — 데이터가 없을 때의 기본 동작.


12. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 평균만 본다

p95 · p99 가 보이지 않으면 사용자 경험을 알 수 없다.

안티패턴 2. 알람을 너무 많이 만든다

모두가 알람을 무시하는 “알람 피로(alert fatigue)” 가 온다.

정말 행동이 필요할 때만 울리도록

안티패턴 3. 알람을 임곗값만으로 거고 사람 행동 매뉴얼이 없다

“왜 울렸지?” 만 반복.

알람에 Runbook 링크를 함께 둔다

안티패턴 4. CPU 만 본다

CPU 가 한가한데도 사용자 응답이 느린 경우가 많다 (DB · 외부 호출 · 네트워크).

사용자 메트릭 (5xx · p95) 부터 본다


13. 한 줄로 정리

CloudWatch 메트릭과 알람은 운영의 눈이며,
“사용자 메트릭 + p95/p99 + 의미 있는 알람” 이 핵심이다


14. 이 장의 핵심 정리

  1. CloudWatch는 AWS 서비스 메트릭을 자동으로 모은다.
  2. Namespace · Dimension · Metric · Statistic · Period 가 좌표다.
  3. p95 / p99 는 평균이 가리는 진실을 보여준다.
  4. Custom Metric은 EMF가 컨테이너에 가장 자연스럽다.
  5. Composite Alarm으로 노이즈를 줄인다.
  6. 알람은 SLO 기반으로, 등급을 나눠 — 모두 호출하지 않는다.

84장. CloudWatch Logs와 Logs Insights

이 장에서 말하고자 하는 것

메트릭이 “숫자” 라면 로그는 “이야기” 다.

  • 무슨 요청이 들어왔는지
  • 어디서 에러가 났는지
  • 어떤 사용자가 무엇을 했는지

이걸 수집하고 검색하는 도구가

Amazon CloudWatch Logs

이고, 그 안에서 빠르게 질의하는 도구가

CloudWatch Logs Insights

다.


1. 기본 단위 — Log Group · Log Stream · Event

Log Group "/ecs/orders"
 ├─ Log Stream  app/task-1
 │   ├─ event: "request received ..."
 │   ├─ event: "DB query ..."
 │   └─ ...
 ├─ Log Stream  app/task-2
 └─ ...
  • Log Group — 서비스 · 환경 단위 묶음
  • Log Stream — 보통 한 컨테이너 / 인스턴스 단위
  • Event — 로그 한 줄

2. 로그 보관 기간 — 비용의 가장 큰 부분

기본은 영원히 보관이다.

한 달 100 GB → 영원히 → 매달 비용이 누적

거의 항상

운영 로그: 30일 ~ 90일
감사 로그: 365일+ (S3로 아카이브)
디버그 로그: 7일

Log Group 만들 때 retention 을 같이 정한다


3. 구조화 로그 (Structured Logging)

평문 로그는 검색이 어렵다.

"POST /api/orders 200 in 184ms for user u-1"

JSON 한 줄로 바꾸면

{ "method":"POST", "path":"/api/orders", "status":200,
  "duration_ms":184, "user_id":"u-1" }

CloudWatch Logs Insights는 JSON 필드를 자동으로 인식한다.

운영 로그는 무조건 JSON. 처음부터 그렇게 만든다


4. CloudWatch Logs Insights — 빠른 질의 언어

SQL 비슷한 별도의 쿼리 언어.

fields @timestamp, status, duration_ms, user_id
| filter status >= 500
| stats count() by bin(1m)
fields @timestamp, path, duration_ms
| filter duration_ms > 1000
| sort duration_ms desc
| limit 20

운영 상황에서 가장 자주 쓰는 도구다.
“방금 5xx 가 늘었는데 어떤 경로?” 같은 질문에 분 단위로 답한다.


5. Metric Filter — 로그에서 메트릭 만들기

로그를 보면서 메트릭을 자동 생성할 수 있다.

Filter Pattern: { $.status >= 500 }
  ↓ 매칭될 때마다
Metric: MyApp/ServerErrors +1

이 메트릭에 알람을 걸면 “로그 기반 알람” 이 만들어진다.

EMF를 쓰면 Metric Filter 없이도 메트릭이 자동 생성됨


6. Subscription Filter — 다른 곳으로 흘려보내기

로그를 다른 시스템으로 실시간 전송.

CloudWatch Logs → Kinesis Data Firehose → S3 / OpenSearch
                → Lambda (실시간 처리)
                → 다른 계정의 Logs

조직 차원에서 로그를 한 데이터 레이크로 모을 때 사용.


7. 어떤 로그를 보내야 하는가

애플리케이션 로그

  • HTTP 요청 한 줄 (path · status · duration · user_id · trace_id)
  • 비즈니스 이벤트 (주문 생성 · 결제 성공)
  • 에러 (stack trace · 컨텍스트)

AWS 서비스 로그

  • ALB 액세스 로그 (S3 → Athena 또는 직접 CloudWatch)
  • CloudFront 액세스 로그
  • VPC Flow Logs
  • API Gateway 로그
  • WAF 로그

시스템 로그

  • OS / 컨테이너 syslog (필요 시)

8. 비밀이 로그에 들어가지 않게

로그는 사고가 자주 나는 자리다.

실수로 console.log(config) → DB 비밀번호가 CloudWatch에

방어:

  • 로깅 라이브러리에 마스킹 설정 (password, token 같은 키)
  • PII (이름 · 이메일 · 신용카드 마지막 4자리) 도 정책에 따라 마스킹
  • 비밀이 들어간 로그 그룹은 삭제 + 사용자 통보 가 표준

9. 우리 서비스에서

Log Group:
  /ecs/orders     (retention 30d, JSON, EMF)
  /ecs/users      (retention 30d)
  /ecs/payments   (retention 90d, 감사 강화)
  /aws/apigw/main (retention 30d)
  /aws/lambda/thumbnail (retention 14d)

ALB · CloudFront 액세스 로그: S3 → Athena

장기 보관(법적):
  Kinesis Firehose → S3 (Glacier)
  • 모두 JSON 구조화 로그
  • 비밀번호 · 카드 번호는 마스킹
  • 핵심 필드: timestamp · level · trace_id · user_id · request_id

10. 직접 확인해보기 — Logs Insights 쿼리 예

최근 5xx 분포

fields @timestamp, status, path
| filter status >= 500
| stats count() by status, path
| sort count() desc

느린 요청 Top 20

fields @timestamp, path, duration_ms
| filter duration_ms > 1000
| sort duration_ms desc
| limit 20

특정 사용자의 모든 요청

fields @timestamp, path, status
| filter user_id = "u-1"
| sort @timestamp desc

trace_id로 한 요청의 전체 흐름

fields @timestamp, @message
| filter trace_id = "abc123"
| sort @timestamp asc

11. 코드로는 이렇게 생겼다 — Terraform

resource "aws_cloudwatch_log_group" "orders" {
  name              = "/ecs/orders"
  retention_in_days = 30
  kms_key_id        = aws_kms_key.logs.arn
}

resource "aws_cloudwatch_log_metric_filter" "orders_5xx" {
  name           = "orders-5xx"
  log_group_name = aws_cloudwatch_log_group.orders.name
  pattern        = "{ $.status >= 500 }"

  metric_transformation {
    name      = "ServerErrors"
    namespace = "MyApp/Orders"
    value     = "1"
    unit      = "Count"
  }
}

이 한 묶음이 로그 → 메트릭 → 알람 흐름의 시작.


12. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. retention을 안 정한다

청구서가 매달 늘어난다.

안티패턴 2. 비구조화 로그

“문자열 매칭” 으로 검색하니 느리고 부정확.

처음부터 JSON

안티패턴 3. 비밀이 로그에 들어간다

로깅 라이브러리 단계에서 마스킹 — 코드에서 매번 처리하지 않는다.

안티패턴 4. 모든 로그를 한 그룹에

권한 · 보관 기간 · 알람이 다 섞인다.

서비스 · 환경별 분리


13. 한 줄로 정리

CloudWatch Logs는 운영의 이야기를 모으는 곳이며,
JSON 구조화 + 적절한 retention + Logs Insights 가 핵심이다


14. 이 장의 핵심 정리

  1. Log Group · Stream · Event 가 기본 단위.
  2. retention은 거의 항상 명시한다 — 비용 통제.
  3. JSON 구조화 로그가 운영의 기본.
  4. Logs Insights로 분 단위 질의가 가능하다.
  5. Metric Filter / EMF로 로그를 메트릭으로 만든다.
  6. 비밀과 PII가 로그에 흘러가지 않게 마스킹은 필수.

85장. 컨테이너 로깅 — FireLens · awslogs 드라이버

이 장에서 말하고자 하는 것

컨테이너의 표준 로그 방식은

표준 출력(stdout) / 표준 에러(stderr) 에 쓴다

다.

이 출력을 어떻게 CloudWatch (또는 다른 백엔드) 로 보낼지가
컨테이너 로깅 드라이버 의 일이다.

ECS에서는 두 가지가 흔하다.

  • awslogs 드라이버
  • FireLens (Fluent Bit / Fluentd 사이드카)

1. awslogs 드라이버 — 가장 간단

ECS Task Definition에 다음을 추가하면 끝.

"logConfiguration": {
  "logDriver": "awslogs",
  "options": {
    "awslogs-group":         "/ecs/orders",
    "awslogs-region":        "ap-northeast-2",
    "awslogs-stream-prefix": "app"
  }
}

ECS 에이전트가 컨테이너의 stdout/stderr를 CloudWatch Logs로 직접 보낸다.

  • 설정이 단순
  • 컨테이너 이미지에 추가 사이드카 필요 없음
  • 보낼 곳을 CloudWatch 외에 못 정함

단순한 서비스는 거의 항상 awslogs로 충분


2. FireLens — 더 유연한 로깅 사이드카

ECS Task에 Fluent Bit (또는 Fluentd) 사이드카 를 띄워
로그를 다양한 백엔드로 보낸다.

[app container] → stdout
                    ↓
              [Fluent Bit 사이드카]
                    ↓ 라우팅
              ├─ CloudWatch Logs
              ├─ Kinesis Firehose → S3
              ├─ Datadog
              ├─ OpenSearch
              └─ Splunk
  • 다중 백엔드
  • 필터링 · 파싱 · 변환 가능
  • 운영 로그와 분석용 로그를 다른 곳으로 분리 가능

3. 언제 FireLens가 필요한가

1. 로그를 여러 백엔드로

  • 운영용: CloudWatch
  • 장기 보관: S3
  • 분석: OpenSearch / Datadog

2. 사전 처리가 필요

  • 민감 정보 마스킹
  • 필드 추가/삭제
  • 다른 포맷으로 변환

3. 비용 절감

  • 디버그 로그는 S3 (Glacier) 로 직행
  • 운영 로그만 CloudWatch (비싸지만 검색 빠름)

4. 멀티 클라우드 표준

  • Fluent Bit은 AWS · GCP · Azure 어디서나 동일

4. 사이드카 패턴

Task 안에 두 개의 컨테이너 가 함께 돈다.

Task
 ├─ app          (essential = true)
 └─ log_router   (essential = false, Fluent Bit)
  • app 죽으면 Task 죽음
  • log_router 죽어도 app은 살아 있음 (essential = false)

5. 로그 형식 — JSON 한 줄

어떤 드라이버를 쓰든 가장 중요한 건 JSON 한 줄로 출력 하는 것이다.

{"ts":"2026-01-01T10:00:00Z","level":"info","trace_id":"abc","msg":"order created","order_id":"o-1","user_id":"u-1"}
  • CloudWatch Logs Insights가 자동 인식
  • Fluent Bit · OpenSearch 모두 자연스럽게 처리

Logger 라이브러리에서 JSON 출력을 기본으로 설정


6. EMF — 로그 한 줄로 메트릭까지

CloudWatch Embedded Metric Format 을 쓰면
로그 출력만으로 메트릭이 자동 생성된다.

{
  "_aws": {
    "CloudWatchMetrics": [{
      "Namespace": "MyApp/Orders",
      "Dimensions": [["Service"]],
      "Metrics": [{ "Name": "OrdersCreated", "Unit": "Count" }]
    }]
  },
  "Service": "orders",
  "OrdersCreated": 1
}
  • PutMetricData 호출 안 함 (비용 절감)
  • 로그가 곧 메트릭의 원본
  • 라이브러리 (aws-embedded-metrics) 사용이 일반적

7. 우리 서비스에서

단순 서비스 (대다수)

ECS Task → awslogs → /ecs/<service-name>

핵심 서비스 (payments · audit)

ECS Task → FireLens (Fluent Bit 사이드카)
  ├─ CloudWatch Logs (운영 검색)
  └─ Kinesis Firehose → S3 (Glacier, 장기 감사)

용도가 단순하면 awslogs, 복잡한 라우팅이 필요하면 FireLens.


8. 직접 확인해보기 — Task Definition (FireLens 예)

{
  "family": "orders",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "<ecr>/orders:v1",
      "essential": true,
      "logConfiguration": {
        "logDriver": "awsfirelens"
      }
    },
    {
      "name": "log_router",
      "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable",
      "essential": false,
      "firelensConfiguration": {
        "type": "fluentbit",
        "options": { "enable-ecs-log-metadata": "true" }
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group":  "/ecs/firelens",
          "awslogs-region": "ap-northeast-2",
          "awslogs-stream-prefix": "router"
        }
      }
    }
  ]
}

app은 awsfirelens 드라이버로 보내고, 라우터의 출력만 awslogs.


9. 코드로는 이렇게 생겼다 — Terraform (FireLens Task)

resource "aws_ecs_task_definition" "orders" {
  family                   = "orders"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "512"
  memory                   = "1024"
  execution_role_arn       = aws_iam_role.task_execution.arn
  task_role_arn            = aws_iam_role.orders_task.arn

  container_definitions = jsonencode([
    {
      name      = "app"
      image     = "${aws_ecr_repository.orders.repository_url}:v1"
      essential = true
      portMappings = [{ containerPort = 8080 }]
      logConfiguration = { logDriver = "awsfirelens" }
    },
    {
      name      = "log_router"
      image     = "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable"
      essential = false
      firelensConfiguration = {
        type    = "fluentbit"
        options = { enable-ecs-log-metadata = "true" }
      }
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = "/ecs/firelens"
          awslogs-region        = "ap-northeast-2"
          awslogs-stream-prefix = "router"
        }
      }
    }
  ])
}

Fluent Bit의 라우팅 규칙은 별도 ConfigMap (또는 S3) 파일로 둔다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 컨테이너 안에 파일로 로그를 쓴다

컨테이너 죽으면 사라진다.

항상 stdout / stderr

안티패턴 2. 한 줄 로그가 여러 줄에 걸친다 (stack trace 등)

파서가 깨진다.

멀티라인은 한 JSON 문자열로 묶거나 라우터에서 자동 합치기 설정

안티패턴 3. FireLens를 모든 서비스에 적용

단순한 서비스에 사이드카는 오버헤드.

단순함이 답인 곳은 그대로 awslogs

안티패턴 4. 로깅 사이드카에 충분한 자원을 안 준다

Fluent Bit이 메모리 부족으로 죽으면 로그 손실.


11. 한 줄로 정리

컨테이너 로그는 stdout으로, awslogs (단순) 또는 FireLens (다중/변환) 로 보낸다
JSON 한 줄과 trace_id 가 핵심이다


12. 이 장의 핵심 정리

  1. 컨테이너 로그는 stdout/stderr 가 표준.
  2. ECS는 awslogs 드라이버로 CloudWatch에 바로 보낼 수 있다.
  3. FireLens는 다중 백엔드 · 변환 · 비용 최적화에 강하다.
  4. 로그는 JSON 한 줄로, trace_id · user_id 같은 필드를 포함.
  5. EMF로 로그가 곧 메트릭이 되는 패턴이 가능하다.
  6. 단순 서비스는 awslogs, 복잡한 라우팅은 FireLens.

86장. X-Ray — 분산 트레이싱

이 장에서 말하고자 하는 것

마이크로서비스에서 한 사용자 요청이 통과하는 경로는 길다.

사용자 → CloudFront → API Gateway → ALB
    → orders → users (호출)
            → payments (호출)
                → DB · 외부 결제 API

운영 중 “이 한 요청은 어디서 1.5초가 걸렸나?” 가 자주 묻는 질문이다.

이 답을 주는 도구가

분산 트레이싱(Distributed Tracing)

이고, AWS의 옵션이

AWS X-Ray

다.


1. Trace · Segment · Subsegment

Trace (한 요청의 전체 이야기)
 ├─ Segment: API Gateway
 ├─ Segment: orders 서비스
 │   ├─ Subsegment: DB query
 │   └─ Subsegment: users 서비스 호출
 │       └─ Segment: users 서비스
 │           └─ Subsegment: DB query
 └─ Segment: payments 서비스
     └─ Subsegment: 외부 결제 API

각 구간이 얼마나 걸렸는지 시각적으로 펼쳐진다.


2. trace_id — 한 요청을 따라가는 끈

요청이 시작될 때 발급되는 ID.

Trace ID: 1-abcd1234-...

모든 다운스트림 호출에 이 ID가 헤더로 전달된다.

X-Amzn-Trace-Id: Root=1-abcd1234-...

각 서비스의 로그에도 trace_id를 같이 기록한다.

로그 + 트레이스 + 메트릭이 trace_id 로 한 줄에 묶인다
이게 운영 관측성의 핵심 연결고리


3. AWS X-Ray SDK · OpenTelemetry

X-Ray SDK

AWS 전용. 비교적 단순.

OpenTelemetry (OTel) + AWS Distro (ADOT)

업계 표준. AWS · Datadog · Honeycomb 등 어디든 보낼 수 있다.

새 프로젝트는 거의 항상 OpenTelemetry 부터
AWS 종속을 줄이고 다른 백엔드로 옮기기 쉬움


4. 자동 계측 vs 수동 계측

자동 계측

프레임워크 / 라이브러리가 알아서 트레이스를 만든다.

  • HTTP 요청
  • DB 쿼리
  • AWS SDK 호출
  • HTTP 클라이언트

수동 계측

비즈니스 로직 안에서 명시적으로 구간을 만든다.

tracer.startActiveSpan("calculate-shipping", (span) => {
  span.setAttribute("order.weight", weight);
  ...
  span.end();
});

자동 계측을 먼저, 비즈니스적으로 의미 있는 구간만 수동 추가


5. ECS · API Gateway · ALB와의 통합

  • API Gateway — 한 줄로 X-Ray 활성화
  • ALB — 자동 trace_id 전파 헤더
  • ECS / Fargate — X-Ray 데몬 사이드카 또는 OTel Collector
  • Lambda — Tracing 옵션을 켜면 자동

활성화는 어렵지 않다 — 코드는 SDK 한 줄


6. Service Map

X-Ray가 자동으로 그려주는 서비스 의존성 그래프.

[client] → [API Gateway] → [orders]
                            ├─ → [users]
                            ├─ → [payments] → [외부 API]
                            └─ → [RDS]
  • 평균 지연 · 에러율을 노드에 표시
  • 어느 노드가 느린지 / 에러를 만드는지 한눈에

7. 우리 서비스에서

모든 서비스가 OpenTelemetry로 계측
  ↓ OTLP
[ADOT Collector 사이드카]
  ↓
[X-Ray] (운영)
[추가로 Datadog/Honeycomb 도 옵션]

각 서비스 로그에 trace_id 필수 필드
  • p95 가 튀는 시점 → X-Ray에서 trace 한 건 클릭 → 어느 구간에서 느렸나 즉시 파악

8. 직접 확인해보기

ECS Task에 X-Ray 사이드카 추가

{
  "name": "xray-daemon",
  "image": "public.ecr.aws/xray/aws-xray-daemon:latest",
  "essential": false,
  "portMappings": [{ "containerPort": 2000, "protocol": "udp" }]
}

애플리케이션 코드 (Node.js, X-Ray SDK)

const AWSXRay = require("aws-xray-sdk");
const express = require("express");

const app = express();
app.use(AWSXRay.express.openSegment("orders"));
app.use(routes);
app.use(AWSXRay.express.closeSegment());

OpenTelemetry 권장 패턴

import { NodeSDK } from "@opentelemetry/sdk-node";
import { AwsXrayIdGenerator } from "@opentelemetry/id-generator-aws-xray";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";

const sdk = new NodeSDK({
  idGenerator: new AwsXrayIdGenerator(),
  traceExporter: new OTLPTraceExporter({ url: "http://localhost:4317" }),
});
sdk.start();

9. 코드로는 이렇게 생겼다 — Terraform (API Gateway X-Ray)

resource "aws_apigatewayv2_stage" "prod" {
  api_id      = aws_apigatewayv2_api.main.id
  name        = "$default"
  auto_deploy = true

  default_route_settings {
    detailed_metrics_enabled = true
    logging_level            = "INFO"
    throttling_burst_limit   = 5000
    throttling_rate_limit    = 10000
  }
}

# X-Ray 활성화는 통합 단계에서 설정
# Terraform 에서는 IAM 권한과 ADOT Collector 사이드카 추가가 핵심

10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. trace_id 를 로그에 안 넣는다

트레이스와 로그가 분리돼 운영에서 서로를 못 찾는다.

모든 로그 한 줄에 trace_id 필수

안티패턴 2. 샘플링을 안 한다

모든 요청을 트레이싱하면 비용 폭증. X-Ray 기본은 1초당 1건 + 5%.

운영 부하에 맞춰 샘플링 조정

안티패턴 3. 외부 호출은 안 추적한다

가장 느린 구간이 외부 결제 API · S3 같은 곳에 있는데 보이지 않는다.

AWS SDK · HTTP 클라이언트 자동 계측 켠다

안티패턴 4. 트레이스가 있는데 안 본다

운영 회의나 사고 보고에서 추측만 한다.

사용자 영향 큰 사고는 항상 트레이스로 시작


11. 한 줄로 정리

X-Ray는 한 요청의 여행을 시각화하는 도구이며,
trace_id 가 로그 · 메트릭 · 트레이스를 한 줄로 묶는 끈이다


12. 이 장의 핵심 정리

  1. 분산 트레이싱은 한 요청의 다중 서비스 흐름을 따라간다.
  2. Trace ID가 운영 관측성의 핵심 연결고리다.
  3. OpenTelemetry + AWS Distro (ADOT) 가 새 프로젝트의 표준이다.
  4. 자동 계측을 먼저, 의미 있는 구간만 수동 추가.
  5. Service Map으로 의존성과 지연을 한눈에 본다.
  6. 모든 로그 한 줄에 trace_id 를 넣는 게 가장 큰 차이를 만든다.

87장. CloudTrail — 변경 추적

이 장에서 말하고자 하는 것

지금까지 본 관측성 도구는 사용자 트래픽 의 이야기를 다뤘다.

  • 메트릭 — 시스템 숫자
  • 로그 — 애플리케이션 이야기
  • 트레이스 — 요청 흐름

또 하나가 남아 있다.

“AWS 환경 안에서 누가 무엇을 했는가”

이걸 기록하는 도구가

AWS CloudTrail

이다.


1. CloudTrail이 기록하는 것

AWS API 호출을 모두 기록한다.

이벤트 예:
  - 누가     : User / Role / Service
  - 언제     : 2026-01-01T10:23:45Z
  - 어디서   : 13.124.55.21 (소스 IP)
  - 무엇을   : RunInstances · CreateBucket · UpdateService …
  - 어떤 자원에 : i-..., bucket=...
  - 결과     : 성공 / 실패 / Access Denied
  • 콘솔 클릭도 결국 API 호출이므로 모두 기록된다
  • IAM Role을 받은 워크로드의 호출도 기록된다

2. 두 종류의 이벤트

Management Events

인프라/계정 변경.

  • EC2 시작/정지
  • IAM Role 만들기
  • S3 버킷 생성
  • VPC 변경

기본으로 켜져 있다 (90일까지 콘솔 검색 가능).

Data Events

자원 안의 데이터 작업.

  • S3 객체 PUT / GET
  • Lambda Invoke
  • DynamoDB GetItem · PutItem

기본으로는 안 켜진다 (양이 많고 비용 큼).
필요한 경우만 켠다.


3. Trail — 영구 보관과 분석

CloudTrail의 기본 보관은 90일이다.

장기 보관 / 분석을 위해 Trail 을 만든다.

Trail "all-management"
  ├─ S3 버킷에 자동 적재
  ├─ CloudWatch Logs로도 흘려보냄 (선택)
  └─ Multi-region 켜기
  • S3에는 영구 보관 (수년)
  • Athena 로 SQL 질의 가능
  • 보안 분석 도구 (GuardDuty 등) 의 입력

4. 왜 필요한가

1. 사고 추적

“어제 운영 RDS가 왜 삭제됐지?”
→ CloudTrail로 누가 언제 DeleteDBInstance 호출했는지 추적.

2. 감사 / 컴플라이언스

ISO · SOC2 · PCI 같은 표준은 변경 이력 보관을 요구한다.

3. 보안 사고 대응

계정이 탈취되었다면 어떤 권한이 어떤 행위를 했는지 봐야 한다.

4. 권한 사용 분석

“이 Role이 실제로 어떤 Action 만 호출했나” 를 보고 정책을 좁힌다 (80장).


5. CloudTrail Lake — SQL로 질의

CloudTrail 이벤트를 직접 SQL로 검색하는 관리형 데이터 스토어.

SELECT eventTime, userIdentity.arn, eventName, requestParameters
FROM <event-data-store>
WHERE eventName = 'DeleteDBInstance'
  AND eventTime > '2026-01-01'

S3에 적재된 로그를 Athena로 질의하는 것보다 즉시성이 좋다.


6. 보안 관련 통합

CloudTrail은 단독으로는 큰 가치가 없을 수 있다.
다음 서비스와 함께 가치를 발휘한다.

  • GuardDuty — 비정상 패턴 자동 탐지 (사용자 비활성, 외부 국가에서 호출 등)
  • Security Hub — 여러 보안 신호 통합
  • AWS Config — 자원의 상태 변경 추적 (compliance 평가)

“CloudTrail은 일기, GuardDuty는 일기를 보고 사고를 찾는 형사”


7. 우리 서비스에서

[CloudTrail]
  ├─ Trail "prod-all"
  │   ├─ Management Events: ON
  │   ├─ Multi-region: ON
  │   ├─ Log file validation: ON
  │   └─ S3 bucket: msa-prod-cloudtrail (Object Lock + KMS)
  ├─ Data Events on critical buckets only
  └─ CloudWatch Logs → 일정 패턴에 알람

[GuardDuty]: 켬
[Security Hub]: 켬
[AWS Config]: 핵심 자원 추적 ON

운영 사고가 났을 때 “누가 무엇을 했나” 를 시간 단위로 추적 가능.


8. 직접 확인해보기 — CLI

최근 이벤트 조회 (콘솔에서 보던 그것)

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteDBInstance \
  --max-results 10

Trail 만들기

aws cloudtrail create-trail \
  --name prod-all \
  --s3-bucket-name msa-prod-cloudtrail \
  --is-multi-region-trail \
  --enable-log-file-validation \
  --kms-key-id alias/cloudtrail

aws cloudtrail start-logging --name prod-all

Trail 상태

aws cloudtrail get-trail-status --name prod-all

9. 코드로는 이렇게 생겼다 — Terraform

resource "aws_s3_bucket" "cloudtrail" {
  bucket = "msa-prod-cloudtrail"
}

resource "aws_s3_bucket_versioning" "cloudtrail" {
  bucket = aws_s3_bucket.cloudtrail.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_object_lock_configuration" "cloudtrail" {
  bucket = aws_s3_bucket.cloudtrail.id
  rule {
    default_retention {
      mode = "GOVERNANCE"
      days = 365
    }
  }
}

resource "aws_cloudtrail" "main" {
  name                          = "prod-all"
  s3_bucket_name                = aws_s3_bucket.cloudtrail.id
  is_multi_region_trail         = true
  enable_log_file_validation    = true
  include_global_service_events = true
  kms_key_id                    = aws_kms_key.cloudtrail.arn

  event_selector {
    read_write_type           = "All"
    include_management_events = true

    # 핵심 버킷의 Data Events만 켬
    data_resource {
      type   = "AWS::S3::Object"
      values = ["${aws_s3_bucket.uploads.arn}/"]
    }
  }
}

Object Lock + 별도 계정으로 복제 → 악의적 삭제로부터 보호.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. CloudTrail을 안 켠다

사고가 나도 누가 했는지 모른다.

모든 운영 계정에 켠다 (Organizations 전체에 켜는 게 표준)

안티패턴 2. Trail S3 버킷이 누구나 만질 수 있다

악의적 사용자가 흔적을 지울 수 있다.

Object Lock + 별도 보안 계정 복제

안티패턴 3. Data Events 를 무차별 켠다

S3 GET 마다 이벤트가 발생 → 비용 폭증.

핵심 자원에만

안티패턴 4. 로그만 모으고 알람이 없다

누가 IAM Role을 만들거나 보안 그룹을 풀어도 모른다.

핵심 이벤트는 CloudWatch Alarm 또는 EventBridge Rule로 알람


11. 한 줄로 정리

CloudTrail은 AWS 안의 모든 행위를 기록하는 일기이며,
Multi-region · Object Lock · 알람 세 가지가 운영의 출발선이다


12. 이 장의 핵심 정리

  1. CloudTrail은 AWS API 호출의 누가·언제·무엇·어디를 기록한다.
  2. Management Events는 기본 ON, Data Events는 필요한 자원만.
  3. Trail로 S3에 영구 보관 + KMS · Object Lock으로 보호한다.
  4. GuardDuty · Security Hub · Config 와 함께 가치가 커진다.
  5. 핵심 이벤트 (IAM 변경 · SG 변경 · 삭제 작업) 는 알람으로 연결한다.
  6. CloudTrail이 없으면 사고 대응이 불가능에 가깝다.

88장. VPC Endpoint — VPC 안에서 외부 서비스 접근

이 장에서 말하고자 하는 것

VPC 안에서 운영하다 보면 다음과 같은 호출이 자주 일어난다.

ECS Task → S3 GetObject
ECS Task → DynamoDB Query
ECS Task → Secrets Manager
ECS Task → ECR pull image

이 호출들은 AWS 서비스 자체 를 향한다.
그런데 기본적으로 이 호출은 인터넷을 거쳐서 나간다.

ECS (프라이빗 서브넷) → NAT Gateway → 인터넷 → S3

이 구조의 문제:

  • NAT Gateway 데이터 처리 비용이 든다
  • 트래픽이 굳이 인터넷을 거친다 (성능 · 보안)
  • NAT Gateway 가용성에 묶인다

이걸 풀어주는 게

VPC Endpoint

다.


1. VPC Endpoint가 하는 일

VPC 안에서 AWS 서비스로 가는 사설 길 을 연다

ECS (프라이빗 서브넷) → [VPC Endpoint] → S3
                       (인터넷 안 거침)
  • NAT Gateway 비용 없음
  • 트래픽이 AWS 백본 안에서만 흐름
  • IAM과 결합한 추가 보안 통제 가능

2. 두 종류의 Endpoint

Gateway Endpoint

  • 라우팅 테이블에 항목을 추가하는 방식
  • S3, DynamoDB 만 지원
  • 무료
  • 서브넷에 ENI를 만드는 방식
  • 거의 모든 AWS 서비스 지원 (Secrets Manager, ECR, KMS, SQS, SNS, …)
  • 시간당 + 데이터 처리 비용 있음

S3 · DynamoDB → Gateway (무료)
그 외 → Interface


3. Gateway Endpoint 동작

라우팅 테이블에 추가:
  prefix list "com.amazonaws.region.s3"  →  vpce-xxxx

ECS Task가 S3 호출
  ↓ 라우팅 테이블 매칭
  ↓ 인터넷이 아닌 VPC Endpoint로
  ↓ S3
  • 별도 IP 변경 필요 없음 (도메인 그대로)
  • 같은 리전의 S3만 가능

4. Interface Endpoint 동작

서브넷에 ENI 생성 (사설 IP)
  ↓
"secretsmanager.ap-northeast-2.amazonaws.com" 도메인이
이 ENI를 가리키도록 사설 DNS 자동 설정

ECS Task → 도메인 호출 → 사설 IP → ENI → 서비스
  • 사설 DNS가 자동으로 작동 (enable_dns_hostnames = true 등)
  • AZ 별로 ENI를 둬야 가용성 보장

5. 보안 — Endpoint Policy

Endpoint에도 정책을 붙일 수 있다.

"이 VPC Endpoint를 통해서는 우리 버킷만 접근 가능"

S3의 경우 강력한 보안 패턴:

Bucket Policy:
  "특정 VPC Endpoint를 통해서만 접근 허용"

이러면 외부 인터넷이나 다른 VPC 에서는 그 버킷에 접근 불가.


같은 Interface Endpoint 메커니즘으로 내 서비스를 다른 VPC에 노출 할 수도 있다.

[Provider VPC]
  NLB → PrivateLink Service
                ↑
[Consumer VPC]
  Interface Endpoint → 내부망 통신
  • SaaS · 사내 서비스 공유에 자주 쓰는 패턴
  • 인터넷 노출 없이 안전한 다중 VPC 연결

7. 우리 서비스에서

Gateway Endpoint:
  ├─ S3        (NAT 비용 절감 큼)
  └─ DynamoDB  (NAT 비용 절감)

Interface Endpoint:
  ├─ ECR + ECR DKR (Task 시작 빠름, NAT 절감)
  ├─ Secrets Manager
  ├─ Logs (CloudWatch Logs)
  ├─ SSM
  ├─ KMS
  └─ STS

ECS Fargate 운영에서는 ECR · Logs · SSM Interface Endpoint 가 NAT 비용을 가장 크게 줄인다


8. NAT vs VPC Endpoint — 비용 비교

NAT Gateway: 시간당 + 처리 GB당 ($0.045/GB 수준)
S3 Gateway Endpoint: 무료
Interface Endpoint: 시간당 + 처리 GB당 (NAT의 약 60~70%)

S3 트래픽이 많다면 Gateway Endpoint 만 켜도 월 수십만 원 이상 절감되는 일이 흔하다.


9. 직접 확인해보기 — CLI

Gateway Endpoint (S3)

aws ec2 create-vpc-endpoint \
  --vpc-id <vpc-id> \
  --service-name com.amazonaws.ap-northeast-2.s3 \
  --vpc-endpoint-type Gateway \
  --route-table-ids <rtb-id-1> <rtb-id-2>

Interface Endpoint (Secrets Manager)

aws ec2 create-vpc-endpoint \
  --vpc-id <vpc-id> \
  --service-name com.amazonaws.ap-northeast-2.secretsmanager \
  --vpc-endpoint-type Interface \
  --subnet-ids <subnet-a> <subnet-b> \
  --security-group-ids <sg-endpoint>

동작 확인

# Task 안에서
dig secretsmanager.ap-northeast-2.amazonaws.com
# → 사설 IP (10.0.x.x) 가 응답되어야 함

10. 코드로는 이렇게 생겼다 — Terraform

# Gateway Endpoint
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.ap-northeast-2.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [aws_route_table.private.id]
}

resource "aws_vpc_endpoint" "dynamodb" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.ap-northeast-2.dynamodb"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [aws_route_table.private.id]
}

# Interface Endpoint (보안 그룹은 443 인바운드 허용)
resource "aws_security_group" "endpoints" {
  name   = "vpc-endpoints"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.task.id]
  }
}

locals {
  interface_endpoints = [
    "ecr.api", "ecr.dkr", "secretsmanager",
    "logs", "ssm", "kms", "sts"
  ]
}

resource "aws_vpc_endpoint" "interface" {
  for_each            = toset(local.interface_endpoints)
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.ap-northeast-2.${each.value}"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.private_a.id, aws_subnet.private_b.id]
  security_group_ids  = [aws_security_group.endpoints.id]
  private_dns_enabled = true
}

private_dns_enabled = true 가 핵심 — 도메인이 자동으로 사설 IP를 가리킨다.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. NAT Gateway만 두고 모든 트래픽을 인터넷으로

S3 · ECR 트래픽이 누적되면 NAT 비용이 폭증한다.

가장 먼저 S3 Gateway Endpoint를 켠다

안티패턴 2. Interface Endpoint를 한 AZ에만 둔다

그 AZ가 죽으면 그 서비스 호출이 전부 실패.

사용하는 AZ 모두에 ENI

안티패턴 3. Endpoint 보안 그룹을 잘못 잡는다

Task SG에서 오는 443이 막혀 있으면 호출이 안 된다.

안티패턴 4. Bucket Policy로 Endpoint 제한을 걸고 다른 길도 열어둔다

한 길은 막고 다른 길로는 풀려 있으면 무의미.

S3 버킷 정책에서 “오직 이 VPC Endpoint” 만 허용


12. 한 줄로 정리

VPC Endpoint는 VPC 안에서 AWS 서비스로 가는 사설 길이며,
NAT 비용 절감과 보안 강화를 동시에 푼다


13. 이 장의 핵심 정리

  1. VPC Endpoint는 Gateway (S3 · DynamoDB) 와 Interface (그 외) 두 종류다.
  2. Gateway는 무료, Interface는 시간/GB 비용이 있지만 NAT보다 싸다.
  3. ECS Fargate는 ECR · Logs · SSM · Secrets Manager Interface Endpoint가 큰 차이를 만든다.
  4. Bucket Policy + Endpoint Policy로 S3 접근을 VPC 안으로 한정할 수 있다.
  5. Interface Endpoint는 사용하는 모든 AZ에 ENI를 둔다.
  6. NAT만 두지 않는다 — 가장 먼저 S3 Gateway Endpoint를 켠다.

89장. VPC Peering

이 장에서 말하고자 하는 것

VPC는 기본적으로 서로 격리된 네트워크다.

즉, 아무 설정도 하지 않으면

VPC A (10.0.0.0/16)
VPC B (192.168.0.0/16)

→ 서로 통신 불가

하지만 실제 환경에서는

  • 서비스 간 통신
  • 계정 간 연결
  • 네트워크 분리 후 연동

이 필요하다.

그래서 가장 먼저 사용하는 방식이

VPC Peering

이다.


1. VPC Peering이란 무엇인가

VPC Peering은

두 개의 VPC를 직접 연결하는 기능

이다.

이 연결은 인터넷을 거치지 않고 AWS 내부 네트워크를 통해 통신한다.


2. 구조로 이해하기

flowchart LR
    VPC1["VPC A<br />10.0.0.0/16"]
    PCX["Peering Connection<br />pcx-123"]
    VPC2["VPC B<br />192.168.0.0/16"]

    VPC1 <--> PCX <--> VPC2

이 구조는 단순하다.

  • VPC A와 VPC B 사이에
  • Peering이라는 “전용 통로”가 하나 생긴다

3. 왜 사용하는가

인터넷을 통해 연결할 수도 있지만 그 방식은 문제가 많다.

VPC → Internet → VPC

이 경우

  • 외부 네트워크를 거침
  • 보안 취약
  • 지연 발생

반면 VPC Peering은

VPC → AWS 내부망 → VPC

이기 때문에

  • 빠르고
  • 안전하고
  • 안정적이다

4. 중요한 개념: 라우팅

여기서 가장 중요한 포인트가 있다.

Peering을 만들었다고 바로 통신되는 것이 아니다

왜냐하면

네트워크는 “어디로 보내야 할지” 알아야 하기 때문이다


라우팅 테이블 설정

VPC A

Destination        Target
10.0.0.0/16        local
192.168.0.0/16     pcx-123

VPC B

Destination        Target
192.168.0.0/16     local
10.0.0.0/16        pcx-123

의미

192.168.0.0/16 → Peering으로 보내라

상대 VPC의 IP 대역을 Peering 연결로 전달

👉 핵심

Peering은 “통로”, 라우팅은 “방향 지정”


5. 동작 흐름

실제로 트래픽이 이동하는 흐름은 다음과 같다.

VPC A (10.0.0.10)
→ 라우팅 테이블 확인
→ 192.168.0.0/16 발견
→ pcx-123으로 전달
→ VPC B로 도착

라우팅 테이블이 없으면 Peering이 있어도 통신 불가


6. 반드시 알아야 할 제약

VPC Peering은 간단하지만 중요한 제한이 있다.

1) CIDR 대역이 겹치면 안 된다

VPC A: 10.0.0.0/16
VPC B: 10.0.0.0/16 ❌

이 경우 AWS는 어디로 보내야 할지 구분할 수 없다.

2) 전이적 라우팅 불가

flowchart LR
    VPC1["10.0.0.0/16"]
    VPC2["192.168.0.0/16"]
    VPC3["172.31.0.0/16"]

    VPC1 <--> VPC2
    VPC2 <--> VPC3

이 구조에서

VPC1 → VPC3 ❌

이유

Peering은 직접 연결된 VPC끼리만 통신 가능

3) 연결 수 증가 문제

VPC가 많아지면

n(n-1)/2

만큼 연결 필요

예:

VPC 4개 → 6개 연결

👉 관리 복잡


7. 언제 사용하는가

VPC Peering은 다음 상황에서 적합하다.

VPC 2~3개 정도
단순한 구조
직접 통신 필요

예:

  • 서비스 A ↔ 서비스 B
  • 개발 ↔ 운영 환경
  • 계정 간 간단 연결

8. 한 줄로 정리

VPC Peering은 두 VPC를 직접 연결하는 가장 간단한 방식이지만
규모가 커지면 한계가 있다


9. 이 장의 핵심 정리

  1. VPC는 기본적으로 서로 통신할 수 없다
  2. VPC Peering은 두 VPC를 직접 연결하는 기능이다
  3. 인터넷을 거치지 않고 AWS 내부망으로 통신한다
  4. 반드시 라우팅 테이블 설정이 필요하다
  5. CIDR이 겹치면 사용할 수 없다
  6. 전이적 라우팅이 불가능하다
  7. 소규모 VPC 연결에 적합하다

90장. Transit Gateway

이 장에서 말하고자 하는 것

앞 장에서 우리는 VPC Peering을 통해
두 개의 VPC를 연결하는 방법을 배웠다.

Peering은 단순하고 빠르지만
VPC가 많아지면 문제가 발생한다.

  • 연결 수가 급격히 증가하고
  • 서로 직접 연결되지 않은 VPC는 통신할 수 없다

이 문제를 해결하기 위해 등장한 것이

Transit Gateway (TGW)

이다.


1. Transit Gateway란 무엇인가

Transit Gateway는

여러 네트워크를 하나의 중앙에서 연결하는 서비스

다.

쉽게 말하면

AWS에서 제공하는 중앙 라우터

라고 보면 된다.


2. 왜 필요한가

VPC Peering 구조를 다시 생각해보자.

flowchart LR
    VPC1["VPC A<br>10.0.0.0/16"]
    VPC2["VPC B<br>192.168.0.0/16"]
    VPC3["VPC C<br>172.31.0.0/16"]

    VPC1 <--> VPC2
    VPC2 <--> VPC3

이 구조에서 많은 사람들이 이렇게 생각한다.

VPC A → VPC C 가능할 것 같지만 ❌

이유는 단순하다.

중간 VPC를 통해 전달할 수 없기 때문이다

또한 VPC가 많아질수록 연결 수는 계속 증가한다.

n(n-1)/2

즉, Peering은 규모가 커질수록 관리가 어려워진다.


3. Transit Gateway 구조

이 문제를 해결하기 위해 구조를 바꾼다.

flowchart TB
    TGW["Transit Gateway"]

    VPC1["VPC A<br>10.0.0.0/16"]
    VPC2["VPC B<br>192.168.0.0/16"]
    VPC3["VPC C<br>172.31.0.0/16"]

    VPC1 --> TGW
    VPC2 --> TGW
    VPC3 --> TGW

이 구조의 핵심은

VPC → TGW → VPC

이다.

각 VPC가 서로 직접 연결되는 것이 아니라
모두 TGW라는 중앙 지점을 통해 연결된다.


4. 어떻게 동작하는가

Transit Gateway를 이해할 때 가장 중요한 것은 라우팅이다.

각 VPC는 목적지를 TGW로 보내고
TGW가 최종 목적지를 결정한다.

VPC A 라우팅 테이블

Destination        Target
192.168.0.0/16     tgw-123
172.31.0.0/16      tgw-123

Transit Gateway 라우팅

Destination        Target
10.0.0.0/16        VPC A
192.168.0.0/16     VPC B
172.31.0.0/16      VPC C

흐름

VPC A → TGW → VPC C

즉,

VPC는 TGW로 보내고, TGW가 목적지를 찾아준다


5. Peering과의 차이

구조 차이가 핵심이다.

Peering → VPC ↔ VPC 직접 연결
TGW → VPC → TGW → VPC

Peering은 단순한 연결이고
TGW는 실제로 라우팅을 수행하는 장비다.


6. 어떤 장점이 있는가

Transit Gateway를 사용하면 구조가 크게 바뀐다.

먼저, 전이적 라우팅이 가능해진다.

VPC A → TGW → VPC C 가능

그리고 연결 구조가 단순해진다.

VPC 10개 → TGW 하나만 연결

또한 모든 라우팅을 중앙에서 관리할 수 있다.

결과적으로 네트워크가 커질수록
오히려 관리가 쉬워진다.


7. 언제 사용하는가

Transit Gateway는 다음과 같은 상황에서 사용한다.

VPC가 여러 개
네트워크가 복잡함
중앙 관리가 필요함

예를 들어

  • 서비스별 VPC 분리
  • 계정 단위 분리
  • 대규모 시스템

8. 한 줄로 정리

Transit Gateway는 여러 VPC를 하나로 묶는 중앙 라우터다


9. 이 장의 핵심 정리

  1. VPC Peering은 규모가 커지면 한계가 있다
  2. Transit Gateway는 중앙 허브 역할을 한다
  3. 모든 VPC는 TGW를 통해 연결된다
  4. VPC는 목적지를 TGW로 보내고 TGW가 라우팅한다
  5. 전이적 라우팅이 가능하다
  6. 대규모 네트워크에 적합하다

91장. AWS Site-to-Site VPN

이 장에서 말하고자 하는 것

지금까지 우리는 AWS 내부에서 VPC 간 연결을 다뤘다.
하지만 실제 환경에서는 AWS만 사용하는 경우는 드물다.

회사 내부 데이터센터(IDC)나 기존 시스템과
AWS를 함께 사용하는 경우가 많다.

그래서 이런 요구가 생긴다.

회사 내부망 ↔ AWS VPC 연결

이걸 해결하는 가장 기본적인 방법이

AWS Site-to-Site VPN

이다.


1. VPN이란 무엇인가

VPN은

인터넷 위에 암호화된 통신 경로를 만들어
두 네트워크를 안전하게 연결하는 방식

이다.

즉, 물리적으로는 인터넷을 사용하지만
논리적으로는 내부망처럼 동작한다.


2. 구조로 이해하기

%%{init: {'flowchart': {'subGraphTitleMargin': {'top': 10, 'bottom': 20}}}}%%
flowchart LR
    subgraph OnPrem["On-prem Data Center<br/>(172.16.0.0/16)"]
        CGW["Customer Gateway<br/>(고객 라우터)"]
    end

    subgraph VPC["VPC (10.0.0.0/16)"]
        VGW["Virtual Private Gateway<br/>VGW-123"]
        RT["라우팅 테이블"]
    end

    CGW <==>|"IPSec 터널 (암호화)"| VGW

이 구조는 다음과 같이 이해하면 된다.

회사 네트워크에서 라우터를 통해 인터넷으로 나가고
그 트래픽이 AWS의 VPN 장비(VGW)에 도착한 뒤
VPC 내부로 전달된다.


3. 구성 요소

VPN 연결에서 중요한 구성은 두 가지다.

Customer Gateway는 회사 쪽 장비다.
실제 VPN 터널을 생성하는 라우터나 방화벽이 여기에 해당한다.

Virtual Private Gateway는 AWS 쪽 장비다.
VPC에 붙어 있는 VPN의 진입 지점이다.

즉, 이 둘이 연결되어 하나의 터널을 만든다.

회사 장비 ↔ AWS 장비

4. 어떻게 동작하는가

VPN 연결은 단순 연결이 아니라
암호화된 터널을 생성한다.

CGW → Internet → VGW

이 구간 전체가 IPSec으로 보호된다.

그래서 외부 인터넷을 지나가더라도
데이터는 안전하게 보호된다.

결과적으로

AWS를 회사 내부망처럼 사용할 수 있게 된다


5. 라우팅 구조

VPN에서도 핵심은 라우팅이다.

회사와 AWS 양쪽 모두
상대 네트워크를 VPN으로 보내도록 설정해야 한다.

VPC 쪽 라우팅은 다음과 같다.

Destination        Target
172.16.0.0/16      vgw-123

이 의미는

회사 네트워크로 가는 트래픽은 VPN으로 보내라

회사 쪽 라우팅은 다음과 같다.

Destination        Target
10.0.0.0/16        VPN 터널

이 의미는

AWS 네트워크는 VPN으로 보내라

그래서 실제 흐름은 이렇게 된다.

회사 서버 → 라우팅 확인 → VPN 터널 → AWS VPC

6. 왜 VPN을 사용하는가

단순히 인터넷으로 통신할 수도 있지만
VPN을 사용하는 이유는 명확하다.

인터넷 방식은 공용 IP 기반이라 외부에 노출되고
보안 측면에서 취약하다.

반면 VPN은 사설 IP 기반으로 통신하고
암호화가 적용되기 때문에 훨씬 안전하다.

인터넷 → 공개된 통신
VPN → 내부망처럼 동작


7. 중요한 특징

AWS VPN은 기본적으로 두 개의 터널을 제공한다.

터널 2개 (Active / Standby)

하나의 터널에 문제가 생기면
자동으로 다른 터널을 사용한다.

그래서 기본적인 고가용성이 확보된다.

하지만 VPN은 인터넷 기반이기 때문에

  • 지연 시간이 일정하지 않을 수 있고
  • 대역폭에 한계가 있다

8. 언제 사용하는가

VPN은 다음과 같은 상황에서 적합하다.

빠르게 연결이 필요할 때
비용을 최소화해야 할 때
온프레미스와 기본 연결이 필요할 때

예를 들어

  • AWS 초기 도입
  • 개발 환경 연결
  • 백업 네트워크

9. 한 줄로 정리

Site-to-Site VPN은 인터넷 위에 암호화된 터널을 만들어
온프레미스와 AWS를 연결하는 방식이다


10. 이 장의 핵심 정리

  1. 온프레미스와 AWS를 연결해야 하는 상황이 존재한다
  2. VPN은 인터넷 위에 암호화된 터널을 만드는 방식이다
  3. Customer Gateway와 Virtual Private Gateway가 연결된다
  4. 라우팅을 통해 트래픽이 VPN으로 전달된다
  5. 사설 네트워크처럼 안전하게 통신할 수 있다
  6. 기본적으로 이중 터널로 고가용성을 제공한다
  7. 인터넷 기반이기 때문에 성능 제한이 존재한다

92장. AWS Direct Connect

이 장에서 말하고자 하는 것

앞 장에서 우리는 Site-to-Site VPN을 통해
온프레미스와 AWS를 연결하는 방법을 배웠다.

VPN은 빠르게 구축할 수 있고 편리하지만
인터넷을 기반으로 하기 때문에 성능과 안정성에 한계가 있다.

그래서 실제 운영 환경에서는 이런 요구가 생긴다.

인터넷을 거치지 않고 AWS와 안정적으로 연결하고 싶다

이 요구를 해결하는 방법이

AWS Direct Connect

이다.


1. Direct Connect란 무엇인가

Direct Connect는

AWS와 고객 네트워크를 전용 회선으로 연결하는 서비스

다.

VPN이 인터넷 위에 암호화된 터널을 만드는 방식이라면
Direct Connect는

AWS까지 이어지는 전용 네트워크 경로를 구성하는 방식

이다.

즉, 통신 경로 자체를 바꾼다.


2. 왜 Direct Connect가 필요한가

VPN에서는 트래픽이 반드시 인터넷을 지나간다.

회사 → 인터넷 → AWS

이 구조에서는

  • 네트워크 품질이 인터넷 상태에 영향을 받고
  • 지연 시간이 일정하지 않으며
  • 대역폭에도 한계가 생긴다

Direct Connect는 이 구조를 다음과 같이 바꾼다.

회사 → 전용 회선 → AWS 네트워크

이 차이로 인해

더 안정적이고, 더 예측 가능한 네트워크를 만들 수 있다


3. 구조로 이해하기

flowchart LR
    subgraph CustomerDC["🏢 &nbsp;고객 데이터 센터&nbsp;"]
        direction TB
        PC["💻 💻 💻<br/>사용자 PC"]
        CR(["🔐 Customer Router"])
        PC --- CR
    end

    subgraph DXLocation["🏢 &nbsp;Direct Connect 로케이션 — Equinix DA1&nbsp;"]
        direction TB
        subgraph PCage["&nbsp;고객 / 파트너 케이지&nbsp;"]
            PR(["🔐 Customer or<br/>Partner Router"])
        end
        subgraph ACage["&nbsp;AWS 케이지&nbsp;"]
            DXE(["🔐 AWS Direct Connect<br/>Endpoint"])
        end
    end

    subgraph AWSCloud["☁️ &nbsp;AWS Cloud&nbsp;"]
        direction TB
        subgraph Region["&nbsp;서울 리전 (ap-northeast-2)&nbsp;"]
            direction TB
            subgraph VPCBox["&nbsp;VPC&nbsp;"]
                direction LR
                VGW["🔒<br/>VGW"]
                EC2["🖥️<br/>EC2"]
            end
            S3["🪣<br/>Amazon S3"]
            DDB["⚡<br/>DynamoDB"]
        end
    end

    CR ==>|"전용 회선"| PR
    PR ==> DXE
    DXE ==>|"🟢 Private VIF"| VGW
    DXE -->|"🔵 Public VIF"| S3
    DXE -->|"🔵 Public VIF"| DDB

    classDef customerStyle fill:#1a3a52,stroke:#4a9eff,stroke-width:2px,color:#fff
    classDef dxStyle fill:#2a1f4a,stroke:#a78bfa,stroke-width:2px,color:#fff
    classDef awsStyle fill:#1a3d2e,stroke:#00d084,stroke-width:2px,color:#fff
    classDef regionStyle fill:#0f2e1f,stroke:#4a9eff,stroke-width:1.5px,color:#fff,stroke-dasharray: 4 3
    classDef vpcStyle fill:#0a1f14,stroke:#00d084,stroke-width:1.5px,color:#fff
    classDef cageStyle fill:#1a1533,stroke:#a78bfa,stroke-width:1px,color:#fff,stroke-dasharray: 5 3
    classDef node fill:#2d4a6b,stroke:#4a9eff,stroke-width:1.5px,color:#fff
    classDef awsNode fill:#1e4a3a,stroke:#00d084,stroke-width:1.5px,color:#fff
    classDef router fill:#3d2a5c,stroke:#a78bfa,stroke-width:2px,color:#fff

    class CustomerDC customerStyle
    class DXLocation dxStyle
    class AWSCloud awsStyle
    class Region regionStyle
    class VPCBox vpcStyle
    class PCage,ACage cageStyle
    class PC node
    class CR,PR,DXE router
    class VGW,EC2,S3,DDB awsNode

    linkStyle 0 stroke:#4a9eff,stroke-width:2px
    linkStyle 1 stroke:#a78bfa,stroke-width:2.5px
    linkStyle 2 stroke:#a78bfa,stroke-width:2.5px
    linkStyle 3 stroke:#00d084,stroke-width:2.5px
    linkStyle 4 stroke:#4a9eff,stroke-width:2px,stroke-dasharray: 5 5
    linkStyle 5 stroke:#4a9eff,stroke-width:2px,stroke-dasharray: 5 5

4. 구조의 핵심 이해

이 구조에서 가장 중요한 것은
Direct Connect Location이다.

많이 헷갈리는 부분이 바로 여기다.

왜 AWS에 직접 연결하지 않고 중간 지점을 거칠까?

AWS는 리전 단위는 공개하지만
실제 내부 데이터센터의 물리 위치나
고객이 직접 연결할 수 있는 지점은 공개하지 않는다.

또한 모든 고객이 임의로 AWS 내부에 직접 연결하는 구조는
보안과 운영 측면에서 관리가 어렵다.

그래서 AWS는

공식적으로 연결 가능한 접속 지점(DX Location)

을 제공한다.

이걸 흐름으로 보면

회사 → DX Location → AWS 내부망 → VPC

이 된다.

DX Location은 AWS 네트워크로 들어가는 공식 입구다


5. 어떻게 동작하는가

전체 흐름을 보면 단순하다.

회사 → 라우터 → 전용선 → DX Location → AWS → VPC

라우팅 방식은 VPN과 동일하다.

차이는 “경로”뿐이다.

VPC 라우팅

172.16.0.0/16 → VGW 또는 TGW

온프레미스 라우팅

10.0.0.0/16 → Direct Connect

트래픽을 어디로 보낼지는 라우팅이 결정하고
Direct Connect는 그 경로를 전용선으로 제공한다


6. Virtual Interface (VIF)

Direct Connect에서는 하나의 물리 회선을
여러 네트워크로 나누어 사용할 수 있다.

이 개념이 VIF다.

예를 들어

Private VIF → VPC 연결
Public VIF → AWS 퍼블릭 서비스
Transit VIF → Transit Gateway 연결

하나의 물리 연결 위에 여러 논리 네트워크를 구성할 수 있다


7. 언제 사용하는가

Direct Connect는 단순 연결보다는
다음과 같은 요구가 있을 때 사용한다.

대용량 데이터 전송
지연 시간 민감 서비스
안정적인 네트워크 필요

예를 들어

  • 금융 시스템
  • 대규모 데이터 처리
  • 하이브리드 클라우드 환경

8. VPN과 함께 사용하는 이유

Direct Connect는 안정적이지만
물리 회선이기 때문에 장애 가능성이 존재한다.

그래서 실무에서는

기본 경로 → Direct Connect
백업 경로 → VPN

구조를 많이 사용한다.

성능은 Direct Connect, 장애 대비는 VPN


9. 한 줄로 정리

Direct Connect는 AWS까지 이어지는 전용 네트워크 경로를 만드는 서비스다


10. 이 장의 핵심 정리

  1. VPN은 인터넷 기반이라 성능 한계가 있다
  2. Direct Connect는 전용 회선으로 AWS와 연결한다
  3. Direct Connect Location을 통해 AWS 네트워크에 접속한다
  4. 라우팅 방식은 VPN과 동일하다
  5. VIF를 통해 하나의 회선을 여러 용도로 사용할 수 있다
  6. 고성능과 안정성이 필요한 환경에서 사용된다
  7. VPN과 함께 사용해 안정성을 확보한다

93장. IaC — CloudFormation · Terraform 개요

이 장에서 말하고자 하는 것

지금까지 우리는 콘솔과 CLI로 자원을 만들었다.

운영을 진지하게 하려면 다음 질문이 생긴다.

  • 같은 환경을 dev/stage/prod 에 똑같이 만들 수 있나?
  • 누가 무엇을 바꿨는지 코드처럼 리뷰할 수 있나?
  • 어제와 오늘의 인프라 차이를 볼 수 있나?

이 모든 답이

Infrastructure as Code (IaC)

다.

AWS 환경의 두 가지 주류:

  • AWS CloudFormation — AWS 자체
  • Terraform — HashiCorp · 멀티 클라우드

1. CloudFormation — AWS 네이티브

  • YAML / JSON으로 자원 선언
  • AWS가 관리하는 Stack 단위로 배포 / 롤백
  • AWS 서비스 변경에 가장 빠르게 따라옴
  • AWS만 다룬다
Resources:
  Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: msa-prod-uploads
  • CDK (TypeScript · Python · Go 등 코드) 도 결국 CloudFormation으로 변환된다

2. Terraform — 멀티 클라우드 표준

  • HCL (HashiCorp Configuration Language)
  • AWS · GCP · Azure · GitHub · Datadog 등 거의 모든 시스템 지원
  • Plan / Apply 워크플로우가 명확
  • State 파일로 현재 상태 추적
resource "aws_s3_bucket" "uploads" {
  bucket = "msa-prod-uploads"
}
  • 사실상 IaC의 업계 표준
  • AWS 외 서비스도 같은 도구로 관리

새 프로젝트는 Terraform이 사실상 기본 — 멀티 클라우드 / 외부 서비스 통합이 자연스럽다


3. 두 도구 비교

항목CloudFormationTerraform
범위AWS 전용멀티 클라우드
언어YAML/JSON (+ CDK 코드)HCL
StateAWS가 관리본인이 관리 (S3 + DynamoDB)
새 AWS 서비스 지원가장 빠름약간 늦음 (provider 업데이트)
도입 비용낮음약간 높음
생태계작음매우 큼

4. IaC가 풀어주는 문제들

1. 환경 복제

dev / stage / prod 를 같은 코드로 만든다.

2. 변경 리뷰

Terraform Plan / CloudFormation ChangeSet 으로 “무엇이 바뀌는지” 사전에 본다.
Pull Request로 리뷰 가능.

3. 롤백

이전 커밋으로 돌아가면 인프라도 함께 돌아간다.

4. 문서화

코드가 곧 운영 문서다 — “왜 이 보안 그룹이 있나” 가 커밋 메시지에 있다.

5. 자동화의 토대

CI/CD가 인프라까지 함께 다룰 수 있다.


5. State — Terraform의 핵심 개념

terraform.tfstate
  ├─ 어떤 자원이 만들어졌나
  ├─ 자원의 현재 속성값
  └─ 자원 간 의존 관계

State 파일은 민감한 정보를 담을 수 있다 (RDS 비밀번호 등).

S3 (암호화) + DynamoDB (잠금) 조합으로 원격 State 운영이 표준

terraform {
  backend "s3" {
    bucket         = "msa-tfstate"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "msa-tfstate-lock"
  }
}

6. 모듈화 — 같은 패턴 반복

orders · users · payments 같은 마이크로서비스가 모두 비슷한 모양이라면 모듈 로 묶는다.

module "orders" {
  source = "./modules/microservice"
  name   = "orders"
  port   = 8080
}

module "users" {
  source = "./modules/microservice"
  name   = "users"
  port   = 8081
}

새 서비스를 추가할 때 모듈 호출 한 블록만 추가하면 끝.


7. Workflow — Plan · Apply

1. 코드 작성 / 수정
2. terraform plan   → 무엇이 바뀔지 미리 본다
3. PR로 리뷰
4. 머지 후 terraform apply (CI에서 자동)
  • Plan 결과를 PR에 자동 코멘트 (Atlantis · Terraform Cloud · GitHub Actions)
  • Apply는 사람의 명시적 승인 후 (또는 자동, 신중히)

8. 우리 서비스에서

terraform/
  ├─ providers.tf       (aws, version)
  ├─ backend.tf         (remote state)
  ├─ network/           (VPC · 서브넷 · 라우팅 · Endpoint)
  ├─ shared/            (ALB · CloudFront · WAF)
  ├─ services/
  │   ├─ orders/        (ECS Service · Task Def · RDS)
  │   ├─ users/
  │   └─ payments/
  └─ environments/
      ├─ dev.tfvars
      ├─ stage.tfvars
      └─ prod.tfvars
  • 같은 코드 + 환경별 변수
  • 환경별 별도 State (서로 독립)

9. 직접 확인해보기 — CLI

# 초기화 (provider 다운로드 · backend 연결)
terraform init

# 무엇이 바뀔지 미리 보기
terraform plan -var-file=environments/prod.tfvars

# 적용
terraform apply -var-file=environments/prod.tfvars

# 자원 삭제 (위험!)
terraform destroy -var-file=environments/prod.tfvars

plan 의 출력을 읽는 능력이 IaC 운영의 핵심 스킬이다.


10. 코드로는 이렇게 생겼다 — Terraform 모듈 호출

module "vpc" {
  source = "./modules/vpc"
  cidr   = "10.0.0.0/16"
  azs    = ["ap-northeast-2a", "ap-northeast-2c"]
}

module "orders_service" {
  source = "./modules/microservice"

  name           = "orders"
  vpc_id         = module.vpc.id
  subnets        = module.vpc.private_subnets
  alb_listener   = module.shared.alb_listener
  container_image = "${var.ecr_url}/orders:${var.image_tag}"

  db_engine      = "postgres"
  db_instance_class = "db.t4g.small"
}

같은 모듈을 여러 서비스에 인스턴스화 — 운영의 표준.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 콘솔로 만든 자원과 IaC가 섞인다

“실제 운영 자원” 이 어디서 만들어졌는지 추적이 안 된다.

한 자원은 한 방식으로만 관리

안티패턴 2. State 파일을 Git에 커밋

민감한 정보(비밀번호 · ARN)가 평문으로 저장소에 남는다.

원격 State (S3 + KMS) 가 표준

안티패턴 3. Apply 권한을 누구나 가진다

실수 한 번에 운영 자원이 사라진다.

CI/CD를 통한 Apply, 사람은 PR 리뷰만

안티패턴 4. 한 State 파일에 모든 환경

prod와 dev가 같은 파일에 있으면 잘못된 변경이 prod에 영향.

환경마다 별도 State


12. 한 줄로 정리

IaC는 인프라를 코드로 다루는 패러다임이며,
새 프로젝트는 Terraform이 사실상 기본 — Plan · 모듈 · 원격 State 가 운영의 토대다


13. 이 장의 핵심 정리

  1. IaC는 환경 복제 · 변경 리뷰 · 롤백 · 자동화의 토대다.
  2. CloudFormation은 AWS 네이티브, Terraform은 멀티 클라우드 표준이다.
  3. Terraform은 State + Plan/Apply 워크플로우가 핵심이다.
  4. 원격 State (S3 + DynamoDB) 가 사실상 표준.
  5. 모듈로 같은 패턴을 반복한다.
  6. Apply 권한은 CI/CD로 한정, 사람은 PR 리뷰.

94장. 이미지 빌드 자동화 — CodeBuild · GitHub Actions

이 장에서 말하고자 하는 것

ECR에 이미지를 올리려면 다음을 매번 한다.

1. 코드 변경
2. docker build
3. docker tag
4. docker push
5. ECS Task Definition 갱신
6. ECS Service update

사람이 매번 손으로 하면 실수가 나고, 시간도 든다.

이걸 자동화하는 게 CI (Continuous Integration) 다.

AWS의 두 가지 주류:

  • AWS CodeBuild — AWS 자체 빌드 서비스
  • GitHub Actions — GitHub 호스팅 (사실상의 표준)

이 장은 둘의 자리를 잡고, 운영 패턴을 정리한다.


1. 빌드 자동화의 목적

코드가 머지되면 자동으로 이미지가 만들어져 ECR에 올라간다

git push
  ↓
[CI: 빌드 · 테스트]
  ↓
docker build → 태그 = sha-{commit}
  ↓
docker push to ECR
  ↓
(다음 장: 배포 트리거)
  • 사람의 손이 닿지 않는다 → 재현성
  • 어떤 코드로 어떤 이미지가 만들어졌는지 추적 가능
  • 테스트 실패 시 배포가 자동으로 막힌다

2. CodeBuild vs GitHub Actions

항목CodeBuildGitHub Actions
위치AWS 내GitHub 호스팅 (또는 self-hosted)
인증IAM Role 자연스러움OIDC로 Role 받기
시작 속도약간 느림빠름
가격빌드 시간 기준무료 한도 + 분당 과금
코드와 거리별도 console코드 옆 (PR 통합)
도입 비용약간 큼매우 낮음

코드가 GitHub에 있다면 거의 항상 GitHub Actions가 자연스럽다
CodeBuild는 AWS CodePipeline · CodeDeploy와 한 묶음으로 갈 때 유리


3. OIDC — CI에서 키 없이 Role 받기

가장 중요한 보안 패턴.

[GitHub Actions]
  ↓ OIDC 토큰 (워크플로우당 발급)
[AWS IAM OIDC Provider]
  ↓ AssumeRoleWithWebIdentity
[Role: GitHubBuildRole]
  ↓ 임시 자격증명 (15분)
[ECR push, ECS update]
  • GitHub Secrets에 AWS 키를 박지 않는다
  • 어느 워크플로우 · 어느 브랜치인지 IAM Condition으로 제한
"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
  },
  "StringLike": {
    "token.actions.githubusercontent.com:sub": "repo:my-org/orders:ref:refs/heads/main"
  }
}

4. 표준 빌드 단계

1. 소스 체크아웃
2. 의존성 캐시 복원
3. 테스트 실행
4. (실패 시) 빌드 중단
5. 이미지 빌드 (multi-stage, 빌드 캐시 활용)
6. 이미지 스캔
7. 이미지 태그 = git commit sha
8. ECR 로그인 (OIDC Role)
9. push
10. ECS update-service 또는 다음 단계로

각 단계가 명시적으로 보여야 디버깅이 쉽다.


5. 빌드 캐시 — 속도의 절반

캐시 없이 매번 처음부터 빌드하면 5분 이상 걸린다.

GitHub Actions:
  - actions/setup-node + cache
  - docker/build-push-action + cache

CodeBuild:
  - S3 캐시
  - Local 캐시 (DOCKER_LAYER)
  • 의존성 라이브러리
  • 도커 레이어
  • 컴파일 결과물

캐시가 잘 돌아가면 빌드 시간이 1/3 이하로 줄어든다.


6. 이미지 스캔과 배포 차단

푸시 후 ECR이 자동 스캔 (43장).

CRITICAL CVE 발견 → 배포 워크플로우 실패

ECR 스캔 결과를 받아 다음 단계 진행 여부를 결정한다.


7. 우리 서비스에서

.github/workflows/orders.yml
  on: push to main (paths: services/orders/**)

steps:
  1. checkout
  2. node setup + cache
  3. test (jest)
  4. docker buildx build (cache from GHCR)
  5. OIDC AssumeRole → AWS
  6. ECR push (orders:sha-abc123)
  7. ECS update-service --force-new-deployment

각 서비스마다 동일 패턴의 workflow.

서비스가 늘어도 같은 템플릿 복사만 하면 끝


8. 직접 확인해보기 — GitHub Actions 워크플로우 (예)

name: orders-ci

on:
  push:
    branches: [main]
    paths: ['services/orders/**']

permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123:role/GitHubBuildRole
          aws-region: ap-northeast-2

      - uses: aws-actions/amazon-ecr-login@v2

      - name: Build & Push
        uses: docker/build-push-action@v5
        with:
          context: services/orders
          push: true
          tags: |
            ${{ secrets.ECR_REGISTRY }}/orders:sha-${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy
        run: |
          aws ecs update-service \
            --cluster msa \
            --service orders \
            --force-new-deployment

9. CodeBuild 예 (buildspec.yml)

version: 0.2

phases:
  pre_build:
    commands:
      - aws ecr get-login-password --region $AWS_REGION \
          | docker login --username AWS --password-stdin $ECR_REGISTRY
  build:
    commands:
      - docker build -t orders:$CODEBUILD_RESOLVED_SOURCE_VERSION services/orders
      - docker tag orders:$CODEBUILD_RESOLVED_SOURCE_VERSION $ECR_REGISTRY/orders:sha-$CODEBUILD_RESOLVED_SOURCE_VERSION
      - docker push $ECR_REGISTRY/orders:sha-$CODEBUILD_RESOLVED_SOURCE_VERSION
  post_build:
    commands:
      - aws ecs update-service --cluster msa --service orders --force-new-deployment

CodePipeline의 한 Stage로 자연스럽게 묶인다.


10. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. CI에 AWS 키를 박는다

유출 시 사고가 크다. OIDC로 푼다.

안티패턴 2. latest 태그로 푸시

어떤 커밋이 어떤 이미지인지 추적 불가.

태그는 sha-{commit} 또는 시맨틱 버전

안티패턴 3. 테스트 없이 빌드만 한다

운영에 깨진 이미지가 올라간다.

안티패턴 4. 빌드 시간이 30분

캐시를 안 살리고 매번 처음부터 빌드한다.

캐시 적용은 거의 항상 큰 효과


11. 한 줄로 정리

이미지 빌드 자동화의 핵심은 OIDC + 캐시 + 명시 태그이며,
사람이 손대지 않는 자동화가 운영의 토대다


12. 이 장의 핵심 정리

  1. CI 자동화는 재현성 · 추적성 · 빠른 피드백을 만든다.
  2. CodeBuild는 AWS 네이티브, GitHub Actions는 사실상 표준.
  3. CI 인증은 OIDC + IAM Role 이 거의 항상 정답.
  4. 이미지 태그는 commit sha 또는 시맨틱 버전 — latest 금지.
  5. 빌드 캐시가 시간의 절반을 결정한다.
  6. 이미지 스캔 실패는 배포를 자동 중단시킨다.

95장. 배포 자동화 — CodePipeline · CodeDeploy

이 장에서 말하고자 하는 것

빌드된 이미지를 ECS Service로 올리고
인프라 변경을 적용하는 일까지가 배포(CD) 다.

  • 새 이미지가 ECR에 올라오면 자동으로 stage로 → prod로
  • 인프라 변경이 머지되면 자동으로 적용
  • 사람이 손대지 않고 검증된 흐름

AWS의 두 가지 핵심 도구:

  • AWS CodePipeline — 단계(Stage) 를 묶는 파이프라인
  • AWS CodeDeploy — 배포 전략 실행 (Rolling · Blue-Green · Canary)

그리고 GitHub Actions / Argo CD 같은 외부 도구도 자연스럽게 쓰인다.


1. CI vs CD vs IaC 배포

CI (94장)  : 코드 → 이미지
CD (이 장) : 이미지 → ECS Service
IaC 배포   : Terraform → 인프라 변경

운영 자동화는 셋이 함께 굴러간다.


2. CodePipeline — 단계의 묶음

Source        → S3 / GitHub / CodeCommit
Build         → CodeBuild
Deploy (Stage) → CodeDeploy / ECS Service / S3 동기화
Approval      → 사람이 클릭 (선택)
Deploy (Prod) → CodeDeploy ...

각 Stage 사이에 Approval 을 끼우면 “stage 검증 후 사람이 prod 클릭” 흐름.


3. CodeDeploy — 배포 전략의 실행

ECS와 묶여 Blue-Green 배포를 제공.

새 Task Definition 등록
  ↓
Green TG에 새 버전 띄움
  ↓
헬스 체크 통과
  ↓
ALB Listener 전환 (즉시 또는 Canary 비율 조정)
  ↓
일정 시간 모니터링
  ↓
문제 없음 → Blue 정리
문제 발견 → 자동 롤백

49장에서 본 Blue-Green / Canary 가 여기서 실행된다.


4. ECS Rolling vs CodeDeploy Blue-Green

항목ECS Rolling (기본)CodeDeploy Blue-Green
추가 인프라없음TG 2개 + Listener Rule
전환 속도점진한순간 (또는 Canary)
롤백새 버전 다시 띄움Listener 되돌리기 (빠름)
두 버전 공존있음거의 없음
설정 복잡도낮음중간

단순 서비스: ECS Rolling
중요 서비스: CodeDeploy Blue-Green


5. GitHub Actions 직접 배포 — 가벼운 패턴

복잡한 CodePipeline 없이 GitHub Actions가 직접 ECS를 업데이트하는 패턴도 흔하다.

push → build → ecr push → aws ecs update-service
  • 작은 ~ 중간 규모에 적합
  • 외부 시스템 (Slack 알림 · 승인) 통합이 자유로움

6. Argo CD / Flux — GitOps

Kubernetes(EKS) 환경에서 자주 쓰는 패턴.

Git에 매니페스트 (k8s YAML)
  ↓ Argo CD 가 주기적으로 비교
실제 클러스터와 차이가 있으면 자동 동기화
  • Git이 단일 진실
  • 변경 추적 · 롤백이 git 명령
  • ECS에서는 GitOps 비슷한 도구가 적지만 Terraform + GitHub Actions 조합이 비슷한 효과

7. IaC 배포 — Terraform 자동화

PR 생성
  ↓ CI: terraform plan → PR에 코멘트
사람이 plan 결과 리뷰
  ↓ 머지
CI: terraform apply (prod 환경)
  • Apply 권한은 CI Role에만
  • 사람 직접 apply 금지
  • PR을 통한 변경만 운영에 반영

8. 우리 서비스에서

애플리케이션 배포:
  GitHub Actions (CI)
    ├─ build → ECR
    ├─ stage 자동 배포 (ECS Rolling)
    ├─ E2E 테스트
    ├─ Slack 알림 + 사람 승인
    └─ prod 배포 (ECS Rolling 또는 Blue-Green)

인프라 배포 (Terraform):
  PR → plan in CI → 리뷰 → 머지 → CI apply

데이터 마이그레이션:
  별도 Job으로 분리 (Flyway / Liquibase)
  애플리케이션 배포 직전에 자동 실행
  • 핵심 서비스만 Blue-Green
  • 나머지는 Rolling + 빠른 롤백

9. 직접 확인해보기 — CLI

ECS Rolling 배포

aws ecs update-service \
  --cluster msa \
  --service orders \
  --force-new-deployment

Task Definition 만 갱신 (이미지 태그만 바꿀 때)

aws ecs register-task-definition --cli-input-json file://task-def.json
aws ecs update-service \
  --cluster msa \
  --service orders \
  --task-definition orders:42

CodeDeploy Blue-Green 배포 트리거

aws deploy create-deployment \
  --application-name orders-app \
  --deployment-group-name orders-dg \
  --revision '{"revisionType":"AppSpecContent","appSpecContent":{"content":"..."}}'

10. 코드로는 이렇게 생겼다 — GitHub Actions (Rolling 배포)

name: orders-deploy

on:
  push:
    branches: [main]
    paths: ['services/orders/**']

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123:role/GitHubDeployRole
          aws-region: ap-northeast-2

      - uses: aws-actions/amazon-ecr-login@v2

      - name: Build & Push
        run: |
          docker build -t $ECR/orders:sha-${{ github.sha }} services/orders
          docker push $ECR/orders:sha-${{ github.sha }}

      - name: Render task def
        id: td
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-def.json
          container-name: app
          image: ${{ env.ECR }}/orders:sha-${{ github.sha }}

      - name: Deploy
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.td.outputs.task-definition }}
          service: orders
          cluster: msa
          wait-for-service-stability: true

wait-for-service-stability: true 가 배포 완료까지 기다리게 한다 — 실패 시 워크플로우도 실패.


11. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. prod 배포에 승인 단계가 없다

의도하지 않은 변경이 그대로 흘러간다.

적어도 stage → prod 사이에 사람의 명시적 승인

안티패턴 2. 롤백 절차를 모른다

“이전 Task Definition 리비전 지정” 한 줄을 외워둔다.

안티패턴 3. DB 마이그레이션과 코드 배포가 같은 트랜잭션

실패 시 한쪽만 롤백되어 정합성이 깨진다.

마이그레이션은 두 단계 (호환 추가 → 코드 반영 → 옛 컬럼 제거)

안티패턴 4. 배포 직후 메트릭을 안 본다

새 버전이 5xx 100% 인데 모른다.

자동 롤백 + 사람 모니터링 둘 다


12. 한 줄로 정리

배포 자동화는 빌드 → 검증 → 배포 → 모니터링 의 흐름이며,
사람의 승인 · 자동 롤백 · 추적 가능성이 운영의 토대다


13. 이 장의 핵심 정리

  1. CI는 빌드, CD는 배포, IaC apply는 인프라 변경 — 셋이 함께 굴러간다.
  2. CodePipeline + CodeDeploy 가 AWS 네이티브 풀스택.
  3. GitHub Actions 직접 배포도 작은~중간 규모에 충분하다.
  4. ECS Rolling이 기본, 중요 서비스는 CodeDeploy Blue-Green.
  5. 인프라는 PR 기반 — Plan 리뷰 후 머지 시 자동 Apply.
  6. DB 마이그레이션은 코드 배포와 단계를 나눠 호환성을 지킨다.

96장. 배포 전략 — Blue-Green · Canary

이 장에서 말하고자 하는 것

49장에서 ECS의 배포 전략을 다뤘다.

이 장은 그 위에서
전체 시스템 차원의 배포 전략 을 정리한다.

  • 새 버전이 깨져도 사용자 영향을 최소화하기
  • 점진적으로 트래픽을 옮기기
  • 자동으로 문제를 감지하고 되돌리기

1. 세 가지 표준 전략

전략핵심추가 인프라사용자 영향
RollingTask 한 개씩 교체없음잠시 v1 · v2 공존
Blue-Green두 환경, 트래픽 한순간 전환두 배없음
CanaryBlue-Green + 트래픽 단계적 이동두 배일부 사용자만

각각 49장에서 본 ECS 단의 동작이 시스템 전체로 확장된다.


2. Blue-Green 의 진가

[Listener]
 └─ TG-blue (v1, 100%)

신버전 배포:
  TG-green 에 v2 띄움 → 헬스 통과 → Listener forward 전환
  ↓
[Listener]
 └─ TG-green (v2, 100%)
  • 전환이 한순간
  • 롤백도 한순간 (forward 되돌림)
  • DB 스키마 호환 부담이 줄어듦

3. Canary — 안전한 점진 전환

Step 1: 5%  → green
        ↓ 5분 관찰 (자동 메트릭)
Step 2: 25% → green
        ↓ 10분 관찰
Step 3: 50% → green
Step 4: 100% → green

각 단계에서 메트릭 (5xx · 지연 · 비즈니스 지표) 이 임계를 넘으면 자동으로 롤백.

정말 위험한 변경(결제 로직 · DB 호환성) 에 어울린다


4. Feature Flag — 코드는 배포, 기능은 별도 토글

배포 전략과 함께 자주 등장한다.

새 기능 코드를 배포
  ↓ 기본은 OFF
  ↓ 일부 사용자에게만 ON (회사 내부, 베타 그룹)
  ↓ 점진 확대 → 모든 사용자
  • “배포 ≠ 출시” 가 가능해진다
  • 기능을 즉시 끌 수 있다
  • AWS AppConfig · LaunchDarkly · 자체 구현 등

코드 롤백보다 Feature Flag OFF 가 더 빠른 비상 정지


5. CloudFront / API Gateway 가중치

CloudFront · API Gateway · Route 53 에서도 가중치 라우팅을 줄 수 있다.

api.example.com
  ├─ 90% → 기존 ALB (v1)
  └─ 10% → 새 ALB (v2)

전체 스택을 두 개 유지하는 큰 변경(예: 클라우드 마이그레이션) 에서도 같은 사고방식.


6. 자동 롤백 트리거

배포 후 어떤 신호로 롤백할까.

  • ALB 5xx 비율 > 1%
  • 응답 지연 p99 > 임계
  • CloudWatch Alarm 발동
  • 비즈니스 지표 (주문 수 · 결제 성공률) 급감

“신호” 가 명확해야 자동 롤백이 동작한다 — 모호한 임계는 잡음만 만든다


7. 인프라 변경의 배포 전략

코드뿐 아니라 인프라 변경에도 전략이 필요하다.

안전한 인프라 변경

  • 추가 (생성) → 거의 항상 안전
  • 변경 (수정) → 다운타임 가능 — 신중
  • 삭제 (제거) → 가장 위험 — 충분한 검토

Terraform lifecycle.create_before_destroy = true 가 핵심:

lifecycle {
  create_before_destroy = true
}

새 자원을 먼저 만들고, 트래픽 전환 후, 옛 자원 삭제.


8. DB 스키마 변경의 단계

배포 전략의 가장 큰 함정.

v1: column a 사용
v2: column a 삭제, b 사용

만약 v1 · v2 가 동시에 살아 있는 시간에 b 컬럼을 추가/삭제하면 한쪽이 깨진다

해결 — 항상 세 단계로 분리:

Phase 1: b 컬럼 추가 (DB만 변경, v1 그대로 동작)
Phase 2: 새 코드 v2 배포 (b를 함께 쓰면서 a도 유지)
Phase 3: a 컬럼 제거 (다음 배포에서)

이러면 어떤 시점에도 두 버전이 호환된다.


9. 우리 서비스에서

서비스별 전략:
  orders / users   → ECS Rolling + Circuit Breaker
  payments         → CodeDeploy Blue-Green
  catalog          → Rolling + Feature Flag

자동 롤백:
  ALB 5xx > 1% for 2 minutes
  ALB p99 > 1.5s for 5 minutes

DB 변경:
  3-Phase migration (추가 → 코드 → 삭제)
  Flyway / Liquibase

10. 직접 확인해보기 — CLI

Canary 비율 변경 (CodeDeploy)

aws deploy create-deployment-config \
  --deployment-config-name CanaryEcs10Then50 \
  --compute-platform ECS \
  --traffic-routing-config '{
    "type": "TimeBasedCanary",
    "timeBasedCanary": {
      "canaryPercentage": 10,
      "canaryInterval": 5
    }
  }'

자동 롤백 알람 연결

CodeDeploy Deployment Group에 CloudWatch Alarm을 묶어두면
배포 중 알람이 울리면 자동 롤백.


11. 코드로는 이렇게 생겼다 — Terraform (CodeDeploy + Alarms)

resource "aws_codedeploy_app" "payments" {
  compute_platform = "ECS"
  name             = "payments"
}

resource "aws_codedeploy_deployment_group" "payments" {
  app_name              = aws_codedeploy_app.payments.name
  deployment_group_name = "payments-dg"
  service_role_arn      = aws_iam_role.codedeploy.arn

  deployment_style {
    deployment_type   = "BLUE_GREEN"
    deployment_option = "WITH_TRAFFIC_CONTROL"
  }

  deployment_config_name = "CodeDeployDefault.ECSCanary10Percent5Minutes"

  ecs_service {
    cluster_name = aws_ecs_cluster.main.name
    service_name = aws_ecs_service.payments.name
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [aws_lb_listener.https.arn]
      }
      target_group { name = aws_lb_target_group.payments_blue.name }
      target_group { name = aws_lb_target_group.payments_green.name }
    }
  }

  alarm_configuration {
    enabled = true
    alarms  = [aws_cloudwatch_metric_alarm.payments_5xx.alarm_name]
  }

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
  }
}

이 구성이 핵심 서비스 배포의 표준 모양이다.


12. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 모든 서비스에 Blue-Green

오버엔지니어링 — 단순 서비스는 Rolling이면 충분.

안티패턴 2. 자동 롤백 신호가 모호하다

“CPU 50% 이상” 같은 임계는 일상이라 도움이 안 된다.

사용자 영향 지표 (5xx · 지연) 가 신호여야 한다

안티패턴 3. DB 변경을 한 번에 한다

v1·v2 공존 시점에 깨진다.

항상 3-Phase

안티패턴 4. 배포가 끝나면 잊는다

배포 후 30분 ~ 1시간은 평소보다 더 주의 깊게 봐야 한다.


13. 한 줄로 정리

배포 전략은 “위험을 작게 쪼개고 자동으로 되돌릴 수 있게 만드는” 설계이며,
Rolling · Blue-Green · Canary + Feature Flag + 3-Phase DB 가 합쳐져 안전망이 된다


14. 이 장의 핵심 정리

  1. Rolling은 단순, Blue-Green은 안전, Canary는 점진.
  2. Feature Flag로 “배포 ≠ 출시” 를 분리한다.
  3. 자동 롤백은 사용자 영향 지표가 신호여야 한다.
  4. 인프라는 create_before_destroy 로 안전하게 옮긴다.
  5. DB 변경은 3-Phase 가 사실상 표준이다.
  6. 배포 후 모니터링까지가 한 묶음이다.

97장. 도메인 설계 — 3개 서비스로 쪼개기

이 장에서 말하고자 하는 것

지금까지 우리는 AWS의 부품을 하나씩 살펴봤다.

이제 마지막 단원은

“한 서비스를 어떻게 마이크로서비스 3개로 쪼개는가”

를 구체적으로 다룬다.

이 장은 도메인 분리 의 기준과 결과를 정리한다.


1. 시작 — 모놀리스의 하루

작은 쇼핑 서비스를 가정하자.

사용자
  ├─ 회원가입 / 로그인
  ├─ 상품 둘러보기
  ├─ 장바구니
  ├─ 주문
  ├─ 결제
  └─ 주문 내역 보기

처음에는 한 코드베이스 · 한 DB로 충분하다.

[모놀리스]
  └─ RDS (users + products + orders + payments)

문제는 자라면서 시작된다.

  • 결제 모듈 변경이 회원가입 배포와 묶임
  • 한 팀이 다른 영역을 모르고 건드림
  • DB 한 곳에 부하 누적

2. 분리의 기준 — 도메인 경계

마구잡이로 쪼개면 더 큰 혼란이 된다.

“독립적으로 변할 수 있는 단위” 로 나눈다

판단 기준:

  • 데이터 소유권 — 누가 이 데이터의 진실을 책임지나
  • 변경 주기 — 함께 자주 바뀌나, 따로 바뀌나
  • 팀 / 책임 — 누가 운영하나
  • 외부 의존성 — 외부 API · 정책에 묶이나

이 책에서는 다음 세 도메인으로 나눈다.

1. users      (회원 · 인증)
2. orders     (주문 · 장바구니)
3. payments   (결제)

각각이 위 기준에서 독립적이다.


3. 각 서비스의 역할

users

  • 회원가입 / 로그인 / 프로필
  • JWT 발급 (Cognito 와 함께)
  • DB: 사용자 테이블

orders

  • 장바구니 · 주문 생성 · 주문 조회
  • “사용자 정보” 는 API로 또는 이벤트로 받아 일부만 저장
  • DB: 주문 / 장바구니 테이블

payments

  • 결제 시도 · 환불 · 결제 내역
  • 외부 PG와 통신
  • DB: 결제 트랜잭션 (감사 강화)

“주문 데이터를 누가 책임지나” 같은 질문에 한 서비스만 답해야 한다


4. 데이터를 어떻게 공유할까

70장에서 본 세 가지 패턴.

orders 가 "사용자 이름" 이 필요할 때:

① 동기 호출:  orders → users API
② 이벤트 + 로컬 복제:
   users 가 "UserCreated/Updated" 발행
   orders 가 받아서 자기 DB에 (id, name) 만 저장
③ 별도 BFF / CQRS

이 책의 척추 구조는 주로 ②를 권장한다.

orders DB 안에:
  user_id_name_cache (id, name) — 화면용
실제 진실은 users 서비스

5. 서비스 간 호출

orders → users (사용자 확인)              : 동기
orders → payments (결제 시도)             : 동기 (응답 필요)
orders → SNS "OrderCreated"                : 비동기
   ├─ notification 워커 → 알림
   ├─ analytics 워커  → 통계
   └─ inventory 워커  → 재고 차감
  • 사용자에게 응답이 필요한 흐름 → 동기
  • 부수 효과 → 비동기

6. 같은 패턴, 다른 인스턴스

각 서비스가 같은 인프라 패턴을 반복한다.

서비스 = {
  ECR 리포지토리
  ECS Task Definition + Service
  ALB Target Group + Listener Rule
  IAM Task Role (좁은 권한)
  RDS 또는 DynamoDB
  Secrets Manager 항목
  CloudWatch Log Group
  CloudWatch Alarm 묶음
}

Terraform 모듈 하나로 추상화하면 새 서비스 추가가 단 몇 줄.

module "users"    { source = "./modules/microservice"  name = "users"   ... }
module "orders"   { source = "./modules/microservice"  name = "orders"  ... }
module "payments" { source = "./modules/microservice"  name = "payments" ... }

7. 경계의 비용 — 분리는 공짜가 아니다

마이크로서비스는 다음 비용이 든다.

  • 네트워크 호출 지연
  • 분산 트랜잭션의 어려움 (Saga · 멱등성 · Outbox)
  • 운영 도구의 추가 (관측성 · IAM · 배포)
  • DB 데이터 중복 (의도적)
  • 개발 환경 복잡도

작게 시작해서 경계가 보이는 곳부터 쪼개는 게 가장 안전하다
처음부터 5개 · 10개 서비스로 쪼개는 건 거의 항상 후회로 이어진다


8. 이 책에서 3개로 끝낸 이유

  • 도메인 경계가 명확
  • 한 권에서 다룰 수 있는 폭
  • 한 사람 / 작은 팀이 운영 가능

진짜 서비스가 자라면 다음으로 자연스럽게 분리되는 후보들:

notification, search, recommendation, inventory, audit, ...

같은 패턴을 그대로 복제하면 된다.


9. 우리 서비스의 최종 도식

[사용자]
  ↓ DNS (Route 53)
[CloudFront] (WAF, us-east-1 ACM, OAC)
 ├─ /static/* → S3 static (CloudFront + OAC)
 └─ /api/*    → API Gateway (JWT Authorizer · Rate limit)
                  ↓ VPC Link
              [Private ALB] (서울 ACM)
                 ├─ /api/users/*    → ECS "users"    → RDS users-db
                 ├─ /api/orders/*   → ECS "orders"   → RDS orders-db
                 └─ /api/payments/* → ECS "payments" → RDS payments-db

부수 효과:
  ECS "orders" → SNS "order-events"
                  ├─ SQS notification → ECS notification-worker
                  ├─ SQS analytics    → ECS analytics-worker
                  └─ SQS inventory    → ECS inventory-worker

이 그림이 1~96장의 결과물이고
다음 장(98) 이 그 흐름을 한 번 더 완성형으로 그린다.


10. 직접 점검 — 도메인 경계가 잘 잡혔는지

다음 질문에 명확히 답하면 분리가 잘된 것이다.

  • 이 서비스의 데이터 진실은 누구인가
  • 다른 서비스가 이 데이터를 어떻게 받는가 (API / 이벤트 / 복제)
  • 이 서비스 혼자 배포할 수 있는가
  • 이 서비스 장애가 다른 서비스에 어디까지 영향을 주는가
  • 한 명이 이 서비스를 처음부터 끝까지 운영할 수 있는가

11. 코드로는 이렇게 생겼다 — 서비스 모듈 (스케치)

module "users" {
  source = "./modules/microservice"

  name              = "users"
  container_image   = "${var.ecr_url}/users:${var.image_tag}"
  container_port    = 8080
  cpu               = 512
  memory            = 1024
  desired_count     = 2
  alb_listener_arn  = module.shared.alb_listener_arn
  path_pattern      = "/api/users/*"

  db = {
    engine         = "postgres"
    instance_class = "db.t4g.small"
    multi_az       = true
  }

  task_role_extra_policies = [
    aws_iam_policy.users_sns_publish.arn,
  ]
}
  • 모듈은 70~80줄의 Terraform
  • 인스턴스마다 위처럼 10여 줄

12. 이렇게 쓰면 망한다 — 안티패턴

안티패턴 1. 기능 단위가 아닌 기술 계층으로 나눈다

“API 서비스 / 비즈니스 서비스 / DB 서비스” 식 분리는 결합도가 폭증한다.

도메인(사람이 알아보는 비즈니스 단위) 으로 나눈다

안티패턴 2. 작은 단위로 너무 잘게 쪼갠다

“마이크로” 가 아니라 “나노” 서비스 — 호출 지옥.

한 서비스가 한 화면의 한 영역 전체를 책임질 수 있게

안티패턴 3. 분리해 놓고 같은 DB를 공유

이름만 분리, 실질은 모놀리스.

Database per Service 가 진짜 분리

안티패턴 4. 처음부터 모든 부수 효과를 동기로 묶는다

한 서비스 장애가 모든 서비스로 번진다.


13. 한 줄로 정리

도메인 분리는 “독립적으로 변할 수 있는 단위” 로 나누는 일이며,
데이터 소유 · 변경 주기 · 책임 · 외부 의존이 그 기준이다


14. 이 장의 핵심 정리

  1. 도메인 경계가 명확한 곳부터 나눈다.
  2. 이 책은 users · orders · payments 의 3 서비스로 끝낸다.
  3. 같은 인프라 패턴을 Terraform 모듈로 추상화해 반복한다.
  4. 외부 응답은 동기, 부수 효과는 비동기.
  5. 분리는 비용이 든다 — 작게 시작하고 경계가 보일 때 쪼갠다.
  6. 다음 장은 이 세 서비스가 합쳐진 전체 흐름을 한 번 더 정리한다.

98장. CF → APIGW → ALB → ECS → RDS/DynamoDB 통합 흐름

이 장에서 말하고자 하는 것

이 책의 척추 그림을 책 처음부터 반복해 왔다.

사용자 → DNS → CloudFront → API Gateway → ALB → ECS → DB

이 장은 그 흐름을 한 요청의 일대기 로 따라가며
그동안 배운 모든 부품이 어떻게 한 줄에 늘어서는지 정리한다.


1. 한 요청이 통과하는 12 단계

사용자가 POST https://example.com/api/orders 를 호출한다.

 1. DNS 질의             → Route 53 ALIAS → CloudFront
 2. TLS 핸드셰이크       → CloudFront (us-east-1 ACM)
 3. WAF 검사             → 통과
 4. CloudFront 캐시 확인  → /api/* 는 캐시 비활성 → origin으로
 5. API Gateway 도달      → JWT Authorizer 검증 → ALLOW
 6. Rate limit 통과
 7. VPC Link → Private ALB
 8. ALB 규칙 매칭         → /api/orders/* → TG-orders
 9. ALB → ECS Task        → 보안 그룹 통과
10. 컨테이너 처리          → DB · 캐시 · 이벤트 발행
11. 응답 반환             → ALB → APIGW → CloudFront → 사용자
12. 부수 효과 (비동기)     → SNS → SQS → 워커들

이 12 단계의 부품들이 1~96장의 거의 모든 챕터에 대응한다.


2. 각 단계의 책임 다시 보기

단계도구책임
1Route 53이름 → IP
2CloudFront + ACMHTTPS 종료
3WAF악성 트래픽 차단
4CloudFront정적 캐시 흡수
5API Gateway + Cognito인증
6API GatewayRate limit
7VPC Link사설 경로
8ALB경로 기반 라우팅
9Security Group네트워크 통제
10ECS · RDS · ElastiCache비즈니스 로직
11(반대 방향)응답
12SNS · SQS · 워커비동기 부수 효과

각 단계가 자기 영역에서만 일한다.


3. 같은 흐름을 IaC로

이 12 단계 전체가 Terraform 으로 표현된다.

module "network"   { source = "./modules/vpc" ... }
module "edge"      { source = "./modules/cloudfront" ... }
module "api_entry" { source = "./modules/apigw" ... }
module "shared"    { source = "./modules/alb" ... }

module "users"     { source = "./modules/microservice"  name = "users" ... }
module "orders"    { source = "./modules/microservice"  name = "orders" ... }
module "payments"  { source = "./modules/microservice"  name = "payments" ... }

module "events"    { source = "./modules/event-bus"  ... }
module "workers" {
  source   = "./modules/worker-fleet"
  services = ["notification", "analytics", "inventory"]
}

module "observability" { source = "./modules/observability" ... }
module "backup"        { source = "./modules/backup" ... }
  • 모든 자원은 코드로
  • 환경(dev / stage / prod) 은 변수만 다르게

4. 보안 깊이 — 한 요청에 닿는 보안 계층

CloudFront WAF → API Gateway 인증 → VPC Link → Private ALB SG → Task SG → DB SG
       ↓             ↓                  ↓             ↓             ↓        ↓
   엣지 그물     첫 인증 필터        사설 경로       LB→Task     Task→DB   DB만 받음

이 모든 단계 + IAM · KMS · Secrets Manager 가 “Defense in Depth” 를 만든다.

한 계층이 뚫려도 다른 계층이 막는다


5. 관측성 — 한 요청을 시간별로 따라가기

사용자                    trace_id 발급
  ↓
CloudFront 로그            trace_id 함께 기록
  ↓
API Gateway 로그           trace_id, JWT의 user_id
  ↓
ALB 액세스 로그            trace_id (헤더로 전달)
  ↓
ECS 컨테이너 stdout         JSON 로그, trace_id, user_id, request_id
  ↓
X-Ray Segment              ECS · DB · 외부 호출 시간 분포
  ↓
CloudWatch Metric          5xx, p95, p99

trace_id 하나만 있으면 어느 시점에 어디서 무엇이 일어났는지 끝까지 추적 가능


6. 비용 — 한 요청에 따라붙는 청구서

요청 하나의 비용은 다음 항목의 작은 조각이 모인 합이다.

  • CloudFront 요청 수 + 데이터 전송
  • WAF 요청 평가
  • API Gateway 요청 수
  • ALB 처리 단위 (LCU)
  • ECS Fargate vCPU · 메모리 시간
  • RDS 시간 + I/O
  • DynamoDB 요청 단위
  • CloudWatch 로그 GB
  • X-Ray 트레이스 수
  • 데이터 송신 (CloudFront 외부, NAT, VPC Endpoint)

한 요청이 한 자릿수 원도 안 된다. 하지만 모이면 운영 비용의 95%


7. 장애가 났을 때 — 흐름을 거꾸로 추적

사용자: "주문이 안 돼요"
  ↓
CloudWatch Dashboard → ALB 5xx 가 늘었나?
  ↓ Yes
ALB → 어느 TG?
  ↓ TG-orders
ECS Service "orders" → 어떤 Task?
  ↓
CloudWatch Logs Insights → 5xx 응답의 trace_id 추출
  ↓
X-Ray → 그 trace의 어디서 지연/에러?
  ↓
RDS slow query? → CloudWatch RDS Performance Insights

문제가 어디서 났는지 분 단위로 좁힌다.


8. 우리 서비스의 한 요청 (실제 예)

POST https://example.com/api/orders
Authorization: Bearer <JWT>
{
  "items": [...],
  "user_id": "u-1"
}

→ Route 53 ALIAS → CloudFront (cache miss) → APIGW (JWT OK)
→ ALB /api/orders/* → ECS orders Task
  → users.local (Cloud Map) → user 정보 조회
  → orders-db INSERT
  → SNS "OrderCreated"
→ HTTP 201 응답

비동기:
  notification 워커: SQS → 이메일 발송
  analytics 워커:    SQS → DynamoDB 기록
  inventory 워커:    SQS → 재고 차감

이 한 줄 안에 1~96장의 거의 모든 개념이 들어 있다.


9. 직접 따라 만들어보기 — 권장 순서

처음 만든다면 이 순서가 가장 무리 없다.

1. VPC + 서브넷 (19~28장)
2. ALB + 도메인 + ACM (30~37장)
3. ECR + 도커 이미지 + ECS 1개 서비스 (41~51장)
4. CloudFront 끼우기 (38~40장)
5. API Gateway 끼우기 (52~54장)
6. RDS 붙이기 (62~63장)
7. ElastiCache · DynamoDB 필요할 때 (66~69장)
8. SQS · SNS 로 비동기 분리 (74~76장)
9. 관측성 · 백업 · CI/CD (83~96장)
10. 두 번째 · 세 번째 서비스로 확장 (97장)

한 번에 다 깔지 않는다 — 한 층씩 올리면서 동작을 확인


10. 코드로는 이렇게 생겼다 — 한 줄 호출의 IaC 흔적

api.example.com → orders 서비스 한 줄을 만드는 데
관여하는 Terraform 리소스가 (대략):

aws_route53_record         (api.example.com → CloudFront)
aws_acm_certificate         (us-east-1, 서울)
aws_cloudfront_distribution
aws_wafv2_web_acl
aws_apigatewayv2_api / authorizer / vpc_link / integration / route
aws_lb / listener / target_group / listener_rule
aws_security_group (alb, task, db)
aws_ecs_cluster / task_definition / service
aws_iam_role (task_execution, task_role)
aws_db_instance (orders-db)
aws_secretsmanager_secret
aws_cloudwatch_log_group / metric_alarm

작아 보이는 흐름 뒤에 이만큼이 받친다.


11. 이렇게 쓰면 망한다 — 통합 단계의 흔한 함정

함정 1. 한 번에 다 만들고 한 번에 동작시킨다

어디가 실패했는지 모른다.

한 층씩 올린다 — 그때마다 curl로 확인

함정 2. dev에서만 동작 확인

prod는 IAM · 네트워크 · KMS 가 더 좁다. 환경별 검증 필요.

함정 3. trace_id 를 안 흘린다

12 단계 어디에서 끊겼는지 추적이 안 된다.

함정 4. 알람 없이 운영

사용자가 알려야 알게 된다. 사용자 신뢰가 가장 비싼 비용.


12. 한 줄로 정리

한 요청은 12 단계를 통과하며, 각 단계는 자기 책임만 진다.
그 모든 단계가 trace_id 하나로 묶여 추적 가능할 때 운영이 가능하다.


13. 이 장의 핵심 정리

  1. CF → APIGW → ALB → ECS → DB 의 12 단계가 한 요청의 일대기다.
  2. 각 단계는 책임이 겹치지 않는다.
  3. Defense in Depth 가 보안 깊이를 만든다.
  4. trace_id 하나로 모든 단계가 묶여 추적 가능하다.
  5. 운영 비용의 대부분은 작은 항목들의 누적이다.
  6. 한 번에 만들지 않는다 — 한 층씩 올리면서 확인한다.

99장. 운영 체크리스트 — 보안 · 비용 · 관측성

이 장에서 말하고자 하는 것

지금까지 본 부품들을 운영에 올리기 직전 점검 목록이다.

세 가지 축으로 정리한다.

  • 보안
  • 비용
  • 관측성

각 항목은 책의 어느 장에서 다뤘는지도 함께 표시했다.


1. 보안 — 외부 노출

□ Route 53 ALIAS 로만 AWS 자원 가리키기                (30)
□ ACM 인증서: CloudFront는 us-east-1, ALB는 그 리전     (32)
□ 80 → 443 리다이렉트 켜져 있음                         (31)
□ CloudFront 앞단에 WAF (Managed Rules + Rate-based)    (40)
□ ALB 는 인터넷에 직접 노출되지 않음 (Private)           (52, 54)
□ S3 버킷 Public Access Block ON                       (56)
□ 공개 S3 자원은 CloudFront + OAC 로만                  (39)
□ HTTP 호출이 CloudFront 시크릿 헤더를 거쳐 ALB로       (39)

2. 보안 — 네트워크

□ Task는 프라이빗 서브넷                                (28, 51)
□ NAT Gateway + VPC Endpoint (S3·ECR·Logs·SSM·Secrets)  (88)
□ 보안 그룹: ALB ← 인터넷 443 / Task ← ALB / DB ← Task   (26, 47)
□ NACL은 기본 그대로 (SG가 주된 통제)                    (27)
□ Multi-AZ로 ALB · ECS · RDS 구성                       (24, 63)

3. 보안 — 권한 · 비밀

□ 루트 계정 MFA, 일상 사용 금지                          (78)
□ 사람 사용자에게 MFA 강제                              (78)
□ 머신은 항상 Role (User 키 사용 금지)                   (79)
□ ECS Execution Role / Task Role 분리                   (79)
□ 서비스마다 별도 Task Role                              (79, 80)
□ Task Role 권한: Action · Resource 모두 좁게            (80)
□ KMS Customer Managed Key 도입 + 자동 회전              (81)
□ S3 SSE-KMS + Bucket Keys                              (81)
□ 비밀은 Secrets Manager / Parameter Store (SecureString) (82)
□ Task Definition의 secrets 필드로 주입 (평문 환경 X)    (45, 82)
□ Git/로그에 비밀 안 들어가게                            (82, 84)
□ CI 인증은 OIDC (액세스 키 안 박음)                     (79, 94)

4. 보안 — 감사 · 탐지

□ CloudTrail Multi-Region · Log Validation · Object Lock (87)
□ 변경에 민감한 이벤트는 알람 (IAM 변경 · SG 변경 · 삭제) (87)
□ GuardDuty · Security Hub 활성화                       (87)
□ AWS Config 핵심 자원 추적                              (87)
□ WAF 로그 수집 + 알람                                   (40)

5. 비용 — 컴퓨트

□ Fargate 시작은 On-Demand, 안정 후 Spot 혼합            (46)
□ ECS Service Auto Scaling Target Tracking               (37)
□ Max를 너무 크게 잡지 않기 (비용 사고 방지)              (37)
□ ALB 한 대로 여러 서비스 (TG 분리)                       (33, 35)
□ Sticky Session은 꼭 필요할 때만                         (34, 37)

6. 비용 — 데이터

□ S3 수명 주기: Standard → IA → Glacier → 삭제           (57)
□ versioning + 옛 버전 수명 주기 함께                    (56, 57)
□ S3 SSE-KMS 는 Bucket Keys 켜기                         (81)
□ DynamoDB On-Demand 시작 → 안정 후 Provisioned + Auto   (68)
□ DynamoDB TTL 켜기 (세션·이벤트)                        (68)
□ RDS Multi-AZ는 켜되 dev는 끄기                          (63)
□ ECR 수명 주기 (오래된 이미지 정리)                      (43)
□ CloudWatch Logs retention 명시 (30~90일 표준)           (84)

7. 비용 — 네트워크

□ S3 · DynamoDB는 Gateway Endpoint로 NAT 우회             (88)
□ ECR · Logs · SSM · Secrets · KMS Interface Endpoint     (88)
□ CloudFront로 정적 트래픽 흡수                            (38, 59)
□ CloudFront Cache Hit Ratio 알람                          (38)
□ CloudFront WAF 호출 비용 점검                            (40)

8. 관측성 — 메트릭 · 알람

□ 서비스마다 대시보드: RequestCount · p95/p99 · 5xx · CPU (83)
□ DB: CPU · Connections · ReplicaLag · Throttle           (62~64, 68)
□ SQS: 메시지 깊이 · 오래된 메시지 · DLQ                  (74)
□ 알람 등급 분리 (Slack vs Page)                          (83)
□ 알람마다 Runbook 링크                                   (83)

9. 관측성 — 로그 · 트레이스

□ 모든 로그 JSON 한 줄                                    (84, 85)
□ 모든 로그에 trace_id · request_id · user_id            (86)
□ 비밀 · PII 마스킹 (로깅 라이브러리에서)                  (84)
□ FireLens 또는 awslogs (서비스 특성에 맞게)              (85)
□ 분산 트레이싱 (X-Ray 또는 OpenTelemetry ADOT)            (86)
□ ALB · CloudFront · WAF 액세스 로그 S3                    (84)

10. 복구 · DR

□ AWS Backup Plan 매일 (RDS·DynamoDB·EFS 등 통합)         (71)
□ RDS PITR 활성 (보관 7~30일)                              (62)
□ DynamoDB PITR 활성                                       (66)
□ Vault Lock 또는 별도 계정 백업 복제                      (71)
□ 정기 복구 시뮬레이션 (분기마다)                          (71)
□ 핵심 서비스 페일오버 시험                                (63)
□ Cross-Region 백업 (필요 시)                              (71)

11. CI/CD

□ 이미지 태그는 sha-{commit} · 불변                       (43, 94)
□ ECR 스캔 결과로 배포 차단 (CRITICAL)                     (43, 94)
□ CI 인증은 OIDC + IAM Role                                (94)
□ ECS Service deployment_circuit_breaker + rollback        (47)
□ 핵심 서비스는 Blue-Green (CodeDeploy)                    (49, 95, 96)
□ Terraform Plan 결과 PR 코멘트, 머지 시 Apply             (93, 95)
□ DB 스키마 변경은 3-Phase                                 (96)

12. 처음 환경 만들 때 — 추천 순서

1. AWS 계정 분리 (Organizations · prod/stage/dev)
2. Identity 베이스라인 (IAM 정책 · SCP · MFA · 루트 잠금)
3. CloudTrail · Config · GuardDuty 켜기
4. KMS 키 / Secrets Manager 도입
5. VPC + 서브넷 + Endpoint 만들기
6. ALB + ACM + Route 53
7. ECR · ECS Cluster + 첫 서비스
8. RDS / DynamoDB / ElastiCache
9. CloudFront + WAF + API Gateway
10. CI/CD (OIDC) + IaC (Terraform)
11. CloudWatch 대시보드 · 알람 · X-Ray
12. AWS Backup · 복구 시뮬레이션

13. SLO · SLI — 운영의 약속

운영을 더 단단히 하려면 측정 가능한 약속을 만든다.

SLI (지표)        SLO (목표)
응답 성공률      99.9% / 30일
p95 지연         500ms 이하
주문 처리 성공률 99.95%

알람과 배포 차단 모두 이 숫자 기반으로 설계한다.

“느낌” 으로 운영하지 않고 “약속한 숫자” 로 운영한다


14. 한 줄로 정리

운영은 한 번에 끝나지 않는다 — 분기마다 이 체크리스트를 다시 본다
보안 · 비용 · 관측성 세 축의 균형이 곧 운영 품질이다


15. 이 장의 핵심 정리

  1. 운영 점검은 보안 · 비용 · 관측성 세 축으로 본다.
  2. 각 항목은 책의 어느 장과 연결돼 있다 — 잊었다면 그 장으로 돌아간다.
  3. 처음 환경은 12단계로 차근차근 올린다.
  4. SLO 가 있어야 알람과 배포 차단이 “느낌” 이 아닌 약속이 된다.
  5. 체크리스트는 분기마다 재검토한다.
  6. 한 번도 안 일어난 시나리오는 일어난 적이 없는 게 아니라 아직 안 일어났을 뿐.

100장. 다음 단계 — 기초 이후 어디로 갈 것인가

마무리하며

여기까지 왔다.

  • 클라우드와 온프레미스의 차이에서 시작해
  • VPC · EC2 · ALB · CloudFront 같은 인프라의 부품을 익히고
  • 컨테이너와 ECS로 옮겨가
  • DB · 캐시 · 메시징으로 데이터와 통신을 다루고
  • IAM · KMS · Secrets Manager 로 보안을 깔고
  • CloudWatch · X-Ray · CloudTrail 로 관측성을 갖추고
  • IaC · CI/CD · 배포 전략으로 자동화하고
  • 마지막으로 마이크로서비스 한 벌을 그렸다.

이 책은 “AWS의 모든 것” 을 다루지 않았다.
대신

실전 마이크로서비스를 처음부터 끝까지 운영하기 위한 한 묶음

을 다뤘다.


1. 다음 단계 — 같은 책의 다른 깊이

각 단원에는 한 권의 책이 따로 있다.
지금부터는 필요에 따라 깊이 들어간다.

네트워크

  • Transit Gateway · 멀티 VPC 설계
  • Direct Connect · 글로벌 백본
  • Network Firewall · Resolver DNS

컨테이너

  • EKS 본격 운영 (Helm · Argo · Karpenter)
  • Service Mesh (App Mesh · Istio · Linkerd)
  • Multi-cluster 운영

데이터

  • DynamoDB 단일 테이블 설계 심화
  • Aurora Global Database · DSQL
  • OpenSearch · Athena · Glue · Redshift
  • Kinesis · Kafka (MSK)

보안

  • AWS Control Tower · 멀티 계정 거버넌스
  • IAM Identity Center (SSO)
  • Macie · Inspector · Detective
  • PCI · ISO · SOC2 준수 설계

관측성

  • OpenTelemetry 본격 도입
  • Datadog · Honeycomb · New Relic 같은 외부 도구
  • SLO 기반 알람 + Error Budget

자동화

  • CDK / Pulumi (코드로 IaC)
  • GitOps (Argo CD · Flux)
  • Policy as Code (OPA · Sentinel · Checkov)

비용

  • Cost Explorer · Cost and Usage Report 분석
  • Savings Plans · RI · Spot 혼합 설계
  • FinOps 도입

2. 서비스가 자랐을 때 — 진화의 길

Phase 1: 모놀리스 한 대
Phase 2: 도메인 3~5개 마이크로서비스
Phase 3: 비동기 메시징 본격 도입
Phase 4: 멀티 리전 · 글로벌
Phase 5: 데이터 플랫폼 (분석 · ML)
Phase 6: 멀티 클라우드 또는 하이브리드

이 책은 Phase 1 → 2 → 3 까지를 다뤘다.

각 다음 단계는

  • 거대한 학습 곡선
  • 새로운 도구
  • 새로운 운영 패턴

을 가져온다.
그 모든 출발선이 지금까지 본 척추다.


3. 사람과 조직의 변화

기술만큼 중요한 게 사람과 운영 방식이다.

한 사람 → 여러 명 → 여러 팀 → 여러 조직

이 변화에는 다음이 필요해진다.

  • 온콜 (On-call) 체계 — PagerDuty · Opsgenie
  • Runbook · Postmortem 문화
  • DevOps · SRE 분리 (필요 시)
  • Platform Team (내부 플랫폼을 만드는 팀)
  • 변경 관리 · 보안 검토 절차

기술은 사람이 운영한다.
조직이 자라지 않으면 시스템도 자라기 어렵다.


4. 어떤 실수를 가장 자주 하는가

기초 이후 운영에서 자주 보는 실수들.

1. 너무 일찍 마이크로서비스로 쪼갠다

3~5명 팀이 10개 마이크로서비스를 운영하려고 한다. 결국 모놀리스로 되돌아간다.

2. 보안을 나중으로 미룬다

“운영 안정되면 IAM 정리할게요” 가 영원히 미뤄진다. 첫 설계에 보안을 함께 넣는다.

3. 알람만 만들고 Runbook은 없다

울리는데 무엇을 해야 할지 모른다.

4. 복구를 한 번도 안 해본다

백업이 백업으로 동작하는지 모른다.

5. 비용을 보지 않는다

사용자가 늘면 청구서가 따라 늘어난다. 매주 보는 습관이 필요하다.

6. 기술 선택을 유행으로

“K8s가 멋지니까”, “Service Mesh가 멋지니까” — 운영 비용이 더 크다.


5. 학습을 이어가는 자료

AWS 공식

  • AWS Well-Architected Framework — 모든 설계의 기준선
  • AWS Solutions Architect 자격 — 폭을 다지는 데 좋음
  • AWS Blogs · re:Invent 영상

운영

  • The Twelve-Factor App — 클라우드 네이티브 앱의 기본
  • SRE Book (Google) — 신뢰성 운영의 표준
  • Building Microservices (Sam Newman)
  • Domain-Driven Design (Eric Evans) — 도메인 분리의 토대

데이터

  • Designing Data-Intensive Applications (Martin Kleppmann)
  • The DynamoDB Book (Alex DeBrie)

보안

  • AWS Security Hub · CIS Benchmark
  • OWASP Top 10

6. 잊지 말아야 할 원칙들

이 책 전체를 한 줄씩 추렸을 때 남는 원칙들이다.

1. 단순함이 가장 비싼 자원이다.
2. 자동화되지 않은 절차는 결국 깨진다.
3. 모든 운영 자원은 죽을 수 있다 — 죽어도 살아남게 설계한다.
4. 보안은 마지막 단계가 아니라 첫 단계다.
5. 관측되지 않는 시스템은 운영되지 않는 시스템이다.
6. trace_id 하나가 운영의 절반이다.
7. 한 번도 시험해본 적 없는 복구 절차는 작동하지 않는다.
8. 작은 결합이 큰 사고를 만든다 — 그래서 분리한다.
9. 모든 비용은 누군가의 청구서다 — 운영자가 그 누군가.
10. 진짜 마이크로서비스는 데이터 분리에서 완성된다.

7. 그래서, 이제 무엇을 할까

이 책을 덮고 가장 먼저 권하는 것:

작은 마이크로서비스 한 개를 처음부터 끝까지 띄워본다

  • Hello World API 1개
  • CloudFront + API Gateway + ALB + ECS Fargate + RDS
  • Terraform 으로 IaC
  • GitHub Actions 로 CI/CD
  • CloudWatch 대시보드 + 알람

규모는 작아도 된다.
한 사이클을 직접 돌려보는 것 이 1~99장의 모든 글보다 큰 학습이 된다.


8. 마지막으로

클라우드는 끝없는 분야다.

하지만 그 모든 흐름의 출발선은 같다.

사용자가 어떻게 우리 시스템에 닿는가 그 데이터는 어디에 있는가 누가 무엇을 할 수 있는가 그것이 잘 동작하는지 어떻게 보는가 잘못됐을 때 어떻게 되돌리는가

이 다섯 질문에 답할 수 있다면
어떤 새 도구가 나와도 자기 자리를 찾는다.

이 책이 그 출발점이 되기를.


책의 한 줄 요약

클라우드는 단순한 서버 임대가 아니다.
운영 가능한 시스템을 만드는 방식이며,
AWS는 그 방식을 부품으로 제공한다.
우리가 한 일은 그 부품들을 한 줄에 세우는 일이었다.