[DR] DR 아키텍처, 왜 이렇게 설계했나.. (DR 테스트 글 후속편)

지난 글(서울 리전 장애 유발로 도쿄 리전 활성화 하기)에서는 실제로 서울 리전에 장애를 일으켜보고 도쿄로 전환되는 과정을 지켜본 페일오버 테스트 결과를 정리했다. 이번 글은 그 결과물이 "무엇으로 이루어져 있는가"가 아니라 "왜 하필 이런 구성이었는가"를 다룬다. 돌아보면 이 DR 구성의 서비스 하나하나는 따로따로 고른 게 아니었다. 앞 단계에서 생긴 문제를 풀다 보니 다음 서비스가 자연스럽게 필요해지는 흐름이었다. 서울이 통째로 죽을 수 있다는 문제가 도쿄에 사본을 두자는 결론으로 이어지고, 사본이 생기니 트래픽을 어떻게 그쪽으로 보낼지가 문제가 되고, 자동 전환을 만들다 보니 딱 한 곳(DB 승격)만 사람 손을 거치게 남겨야 했고, 그 수동 판단이 실제 테스트에서 발목을 잡는 걸 보고서야 "판단을 도와주는 자동화"가 필요해졌다. 이 글은 그 흐름을 5단계로 나눠 살펴본다. 1단계 — "다른 리전에 사본이 있어야 한다" 문제: 서울(apnortheast2) 리전이 AZ 장애 확산이나 네트워크 단절 같은 이유로 통째로 마비될 수 있다. 단일 리전 구조에서는 이 순간 서비스가 전면 중단된다. 결론: 물리적으로 격리된 다른 리전에 실행 가능한 사본을 둬야 한다 — 도쿄(apnortheast1)리전을 선택했다. 여기서부터 "서울에 있는 구성요소 하나하나를 도쿄에 어떻게 복제할까"라는 질문이 이어진다. 그리고 데이터의 성격(상태 데이터인가, 언제든 다시 만들 수 있는 파생 데이터인가)에 따라 복제 방식이 갈린다는 게 이 단계의 핵심 판단이다. 서울 구성요소 복제가 필요한 이유 선택된 서비스 왜 이 서비스인가 앱 실행 환경 서울과 동일한 방식으로 돌아가야 배포 파이프라인을 이원화하지 않는다 EKS 서울도 EKS이므로 같은 Helm 차트·같은 매니페스트 구조를 그대로 재사용 가능 트래픽 진입점 사용자 요청을 받아 파드로 연결 ALB HTTPS 종료 + K8s Target Group과 자연스럽게 연동 데이터베이스 DB는 "상태"라서 단순 스냅샷이 아니라 계속 최신을 따라가야 하고, 필요 시 독립 DB로 전환 가능해야 함 RDS CrossRegion Read Replica 지속적 복제 + 승격(promote) 한 번으로 독립 primary 전환이 가능한 구조가 요구에 정확히 부합 이미지·정적 파일 마찬가지로 상태 데이터 S3 리전 간 복제(CRR) S3가 리전 간 복제를 자체 기능으로 이미 제공 — 별도 구축 불필요 컨테이너 이미지 도쿄에서도 이미지를 pull 할 수 있어야 파드가 뜸 ECR 리전 간 복제 위와 동일한 논리 — 레지스트리 자체 복제 기능 활용 캐시 캐시는 상태가 아니라 언제든 재생성 가능한 파생 데이터 ElastiCache — 의도적으로 복제 안 함 상시 복제 비용을 낼 가치가 없다는 비용 대비 효익 판단 이 단계의 원칙을 한 줄로 줄이면 이렇다. 상태를 가진 것은 전부 복제하고, 언제든 다시 만들 수 있는 것(캐시)은 복제하지 않는다. 이 구분이 이후 모든 복제 전략의 비용 판단 기준이 된다. 2단계 — "사본은 있는데, 사용자를 어떻게 그리로 보내나" 문제: 도쿄에 동일한 인프라가 있어도, 사용자 요청이 자동으로 그쪽으로 흐르지 않으면 무용지물이다. 결론: DNS 레벨에서 "서울 응답 없음 → 도쿄로" 자동 전환이 필요하다. 구성요소 역할 Route 53 Failover Routing 도메인의 Primary는 서울, Secondary는 도쿄로 설정. 헬스체크 실패 시 자동으로 응답을 도쿄 ALB로 전환 Route 53 헬스체크 서울 ALB가 살아있는지 주기적으로 확인 CloudWatch 알람 + 알림 헬스체크 실패를 사람에게도 알림 — 자동 전환은 일어나지만, 그 사실을 사람이 인지해야 다음 판단(DB 승격 등)을 시작할 수 있음 왜 이 구간만 "완전 자동화"로 설계됐는가? 3단계에서 다룰 DB 승격과 달리, 이 구간은 유일하게 사람 개입 없이 전부 자동으로 설계된 부분이다. 이유는 명확하다: DNS 응답을 바꾸는 행위는 되돌리기 쉽고(가역적), 데이터를 전혀 건드리지 않는다. 잘못 전환돼도 다시 정상으로 돌아가면 그만이라 리스크가 낮다 — 그래서 자동화의 비용 대비 이득이 확실한 구간이다. 3단계 — DB 승격만 수동으로 남긴 이유, 그리고 그것이 낳은 문제 초기 설계는 DB 승격(promote)을 명시적으로 "수동, 운영자 판단 후 실행" 으로 못박았다. 인프라와 트래픽은 자동화하면서 이 한 지점만 수동으로 남긴 이유는, 2단계와 정반대의 리스크 구조 때문이다. Route53 트래픽 전환 (자동) DB 승격 (수동으로 시작) 되돌릴 수 있는가 가역적 — DNS 응답만 바뀜 비가역 — 한 번 승격하면 되돌릴 수 없음 잘못되면 일시적 오탐, 다시 정상 응답하면 자동 복구 splitbrain — 서울·도쿄가 동시에 쓰기 가능한 상태가 되어 데이터 정합성이 깨짐 설계 원칙 "빠른 게 최우선" "틀리지 않는 게 최우선" 그런데 "수동"이 새로운 문제를 낳았다 "수동"이라는 말은 곧 내가 항상 판단할 준비가 되어 있어야 한다는 뜻이었다. 이건 막연한 걱정이 아니라 실제 페일오버 테스트를 해보면서 몸으로 느낀 문제였다. 트래픽이 도쿄로 넘어간 시점과 내가 실제로 DB 승격 명령을 누른 시점 사이에 약 2분의 공백이 있었는데, 그 사이에 들어온 쓰기 요청은 도쿄 RDS가 아직 read replica라 실패했을 수 있겠다 싶었다. "판단은 도와주되, 거기 걸리는 시간은 줄이고 싶다" — 이 생각이 다음 단계에서 다룰 승인 자동화 워크플로를 만들게 된 직접적인 계기였다. 테스트가 끝나자마자 바로 이 워크플로를 만들기 시작했다. 4단계 — "판단은 돕되, 방아쇠는 사람이 당긴다" 3단계의 요구를 그대로 풀어보면 이렇다. "사람이 판단할 재료(위험도 해석)는 자동으로 마련해주되, 최종 실행(승격)의 방아쇠는 여전히 사람이 당기게 한다." 이 요구 하나에서 아래 서비스 선택이 전부 파생된다. ① 서버리스(Lambda) — 왜 상시 서버가 아닌가.. 이 파이프라인은 평소엔 1분에 한 번 상태를 확인하는 것 외엔 거의 동작하지 않는다. 장애라는 이벤트가 터질 때만 깨어나면 되는 워크로드라, 상시 서버를 띄워두면 대부분의 시간을 낭비하게 된다. 기능을 진단·탐지·차단·승격·전환·검증·검수 등 여러 개의 작은 단위로 쪼갠 이유도 있다 — 각 단위에 필요한 최소 권한만 부여하기 위해서다. 예를 들어 "차단" 역할은 DB 접근 제어 권한만, "승격" 역할은 DB 승격 권한만 갖는다 — 하나가 뚫려도 피해가 그 역할의 권한 범위 안으로 제한된다. ② 워크플로 오케스트레이션(Step Functions) — 왜 함수끼리 직접 호출하지 않았나.. 문제 1 — 긴 대기: 사람 승인을 최대 15분까지 기다려야 하는데, 서버리스 함수는 실행 시간 제한이 있고 대기 상태를 함수 자신이 붙들고 있으면 그 시간만큼 과금된다. 해결: 워크플로가 토큰을 발급하고 실행을 "일시정지" 상태로 두면, 외부 이벤트(Slack 버튼 클릭)가 토큰을 반납할 때까지 과금 없이 대기할 수 있는 패턴을 활용했다. 문제 2 — 회고 데이터 확보: "복구까지 몇 분 걸렸나"를 측정하려면 각 단계 소요시간을 어딘가에 기록해야 한다. 해결: 워크플로 오케스트레이션 서비스는 상태 전환마다 자동으로 타임스탬프를 실행 기록에 남긴다. 별도 계측 코드 없이 이 기록을 읽기만 하면 되므로, 사후 회고 보고서가 거의 공짜로 구현될 수 있었다. ③ 별도 상태 저장소(DynamoDB) — 왜 RDS가 아니라 별도 저장소인가.. 문제: "지금 진짜 주인(primary)이 어디냐"를 기록할 저장소가 필요하다. 그런데 이걸 RDS(서울 혹은 도쿄)에 기록하면, 장애의 당사자가 될 수도 있는 그 DB에, 장애 상태를 기록하는 시스템이 의존하는 순환 논리가 생긴다. 해결: RDS와 완전히 독립된, 별도로 가용성이 높은 저장소가 필요했다. 선택된 서비스는 서버리스·고가용성이면서 조건부 쓰기(동시에 같은 값을 쓰려는 시도 중 하나만 성공시키는 방식)를 기본 지원한다. 이게 "동시에 두 곳에서 같은 승격을 시도하지 못하게 막는" 잠금 장치를 구현하는 데 정확히 필요한 기능이었다. 부가적으로, Slack 버튼에는 담을 수 있는 글자 수 제한이 있어서 워크플로의 긴 대기 토큰을 직접 넣을 수 없었다. 그래서 짧은 임시 키로 바꿔 이 저장소에 보관하고, 버튼 클릭 시 그 키로 원래 토큰을 조회하는 우회 구조를 썼다. ④ 생성형 AI(Bedrock) — 왜 AI를 끼워 넣었나, 그리고 왜 판단까지는 맡기지 않았나.. 문제: 복제 지연(초)이나 처리 대기량(byte) 같은 원시 수치는 새벽에 깨어난 당직자가 즉시 위험도를 판단하기 어렵다. 해결 — 그러나 역할은 제한적으로: 생성형 AI가 숫자를 사람이 읽을 수 있는 문장(위험도·예상 손실·권고 절차)으로 번역해주는 "설명 계층"으로만 쓰였다. 다만 "AI가 실수하면 시스템이 잘못 움직인다" 는 반대 리스크가 있으므로, 실제 상태 분기(승인 이후 무엇을 할지)는 AI가 아니라 정해진 규칙의 코드가 전담하도록 역할을 분리했다. 검수(사후 점검) 단계에서는 도구를 스스로 골라 쓰는 자율 루프 방식도 썼는데, 검수는 미리 정해둔 체크리스트만으로 다 커버할 수 없는 "이상 탐지형" 조사이기 때문이다. 상황에 따라 "서울을 다시 찔러볼지, DB 상태를 볼지"가 달라져야 하므로, 고정된 절차보다 필요에 따라 확인 방법을 스스로 고르는 방식이 이 요구에 맞았다. ⑤ Slack — 왜 새 UI를 안 만들었나.. 조직이 이미 상시 사용하는 협업 도구를 승인 인터페이스로 재사용했다. 새로운 대시보드나 앱을 따로 만들 필요 없이, 담당자가 휴대폰 알림만으로 즉시 반응(승인/거부)할 수 있다는 실용적인 이유다. API Gateway — 설계 의도가 아니라 제약으로 인한 대체 처음에는 Slack에서 호출할 공개 HTTPS 엔드포인트 하나만 있으면 충분하다고 보고 더 가벼운 방식으로 구현했다. 그런데 조직의 보안 정책이 인증 없는 공개 엔드포인트를 막고 있었고, 그 바람에 Slack에서 오는 요청이 막혀 승인 버튼이 동작하지 않는 문제가 있었다. API 게이트웨이로 앞단을 교체해 해결했는데, 뒤에 붙는 함수가 받는 요청 형식이 동일했던 덕분에 함수 코드는 한 줄도 바꾸지 않고 앞단 배선만 바꿔서 해결할 수 있었다. "공개 HTTPS 엔드포인트가 필요하다"는 원래 설계 의도는 그대로였고, 구현 수단만 조직 제약에 맞춰 바뀐 사례다. 5단계 — "실제 테스트 진행" 실제로 장애를 주입해보는 실험 코드 리뷰나 유닛 테스트로는 "리전 네트워크가 실제로 끊기면 무슨 일이 벌어지는지" 검증할 수 없다. Route53 헬스체크가 정말 반응하는지, 이상 탐지가 정말 작동하는지는 실제로 끊어봐야 안다. 그래서 서울 프라이빗 서브넷의 네트워크를 인위적으로 격리하는 장애 주입 실험(AWS FIS)을 진행했다. 이 실험에 앞서 관측 도구(CloudWatch Container Insights)부터 먼저 갖춰뒀다. 이유는 명확하다 — "지금 도쿄 클러스터가 버티고 있는지 눈으로 볼 수 없으면, 장애를 인위로 만드는 실험 자체가 의미가 없다." 관측 인프라가 먼저 갖춰져야 검증 도구를 쓸 자격이 생기는 순서다. 실제로 걸려 넘어졌던 함정 하나 Route 53은 특정 리전에 속하지 않는 글로벌 서비스다. 그런데 헬스체크를 어느 리전에서 만들었든 그 결과 지표는 항상 미국 동부(버지니아) 리전의 CloudWatch에만 게시된다 — AWS 플랫폼 자체의 고정된 동작이라 설계로 바꿀 수 있는 부분이 아니다. 이 함정에 나도 실제로 걸려 넘어진 적이 있다. 지난 테스트 때 알람을 서울 리전에 만들었다가 지표가 없다는 이유로 Slack 알림이 안 온 적이 있었고, 그 뒤로는 헬스체크 지표를 항상 미국 동부 리전 기준으로 고정해서 확인하도록 코드를 고쳤다. 이 경험을 겪으면서, 내가 설계를 "처음부터 완벽하게 해서 그대로 구현"한 게 아니라 "설계 → 구현 → 실전에서 실패 → 원인 파악 → 교훈을 반영해 재발 방지"를 반복하면서 다듬어왔다는 걸 새삼 느꼈다. 종합 — 하나의 흐름으로 지금까지의 다섯 단계를 하나로 이으면 다음과 같은 단일한 이야기가 된다. 서울이 통째로 죽을 수 있다 → 도쿄에 사본을 둔다(앱·DB·이미지·컨테이너 이미지 복제) → 트래픽은 자동으로 보낸다(DNS 전환, 가역적이라 안전) → 단, DB 승격은 사람이 결정(비가역·정합성 리스크) → 수동 결정이 느려지는 게 실측으로 드러남(공백 발생, RPO 목표 미달) → 판단을 돕는 자동화를 만들되 방아쇠는 사람이 당기게 → 이 자동화가 쓸 독립 저장소를 따로 둔다(순환 의존 방지) → 전체가 진짜 작동하는지 실제로 장애를 주입해본다 핵심 설계 철학 3가지 1. 되돌릴 수 있는지에 따라 자동화 수준을 달리한다 — 되돌리기 쉬운 것(DNS 전환)은 완전 자동, 되돌릴 수 없는 것(DB 승격)은 사람 승인을 반드시 거친다. 2. 판단을 돕는 것과 판단을 대신하는 것을 분리한다 — AI는 설명·조사·권고까지만 하고, 실제 상태 분기는 항상 정해진 규칙의 코드가 담당한다. 3. 설계는 한 번에 완성되지 않는다 — 리전 함정 사례처럼, 실전 테스트에서 발견된 실패를 다음 버전 설계에 그대로 반영하는 과정을 계속 반복했다.

Sep 18, 2026AWS
[DR] DR 아키텍처, 왜 이렇게 설계했나.. (DR 테스트 글 후속편)

[DR] DR 테스트 후기 — 서울 리전 장애 유발로 도쿄 리전 활성화 하기

서비스를 서울(apnortheast2) 단일 리전으로만 운영하고 있으면, 그 리전 전체에 장애가 났을 때(AZ 장애 확산, 네트워크 단절 등) 서비스가 통째로 멈춰버린다. 이걸 대비하기 위해 도쿄(apnortheast1)에 웜 스탠바이(Warm Standby) 인프라를 미리 구축해뒀는데, "구축해뒀다"는 것과 "실제로 장애가 나도 넘어간다"는 것 사이에는 꽤 큰 간극이 있다. 그래서 실제로 서울 리전에 장애를 일으켜보고, 트래픽이 도쿄로 넘어가서 서비스가 계속되는지 눈으로 확인하는 페일오버 테스트를 진행했다. 이 글은 그 테스트 과정과 결과를 정리한 기록이다. 테스트 개요 서울 리전에 장애가 났다고 "가정"하고, 트래픽이 도쿄 리전으로 넘어가 실제로 서비스가 돌아가는지 확인하는 테스트다. 오후 2시 10분에 시작해서 2시 30분에 종료했다. 장애는 AWS FIS(Fault Injection Service)로 서울 프라이빗 서브넷의 네트워크를 인위적으로 끊어서 만들었고, 그 뒤 Route53 헬스체크가 장애를 감지해 도쿄로 전환하는 전 과정을 Grafana(AMG) 대시보드로 지켜봤다. 확인하려던 질문은 여섯 가지였다. 서울 장애를 FIS로 유도할 수 있는가? Route53이 도쿄로 전환되는가? 도쿄 EKS의 애플리케이션이 정상 응답하는가? 도쿄 RDS read replica를 승격(promote)하면 쓰기가 되는가? RTO와 RPO를 어떤 기준으로 잴 것인가? DR 중 도쿄에 쌓인 데이터가 서울에 자동으로 반영되는가? 목표 기준선은 RTO 15분, RPO 60초로 잡았다. RTO는 장애가 시작된 시점부터 서비스가 다시 정상으로 돌아오기까지 걸린 시간이고, RPO는 장애 시점에 유실될 수 있는 데이터의 시간 폭이다. RPO가 60초라는 건 최악의 경우 마지막 60초어치 데이터까지는 잃을 수 있다고 본다는 뜻이다. 왜 웜 스탠바이인가? DR 전략은 크게 복구 속도와 평시 운영 비용의 트레이드오프다. 껐다가 필요할 때 다시 세우는 백업·복원은 저렴하지만 복구가 시간 단위로 걸리고, 반대로 두 리전을 항상 동시에 운영하는 액티브액티브는 거의 즉시 전환되지만 비용이 두 배로 들고 양방향 동기화까지 신경 써야 한다. Farmily는 24시간 서비스라 시간 단위 복구는 받아들이기 어려웠지만, 그렇다고 액티브액티브를 감당할 트래픽 규모도 아니었다. 그래서 축소된 규모로 도쿄를 항상 띄워 두고 분 단위로 복구하는 웜 스탠바이를 선택했다. 대상 리소스 서울이 평상시 트래픽을 받는 active 리전이고, 도쿄가 standby다. 서울에는 prodeks 클러스터, prodalb, prodrds가 있고, 도쿄에는 dreks 클러스터, dralb, 그리고 서울 prodrds의 crossregion read replica인 drrds가 있다. 사용자 트래픽의 진입점은 api.farmily.info 도메인이고, Route53 failover 라우팅으로 PRIMARY는 서울 ALB, SECONDARY는 도쿄 ALB를 가리킨다. 헬스체크는 api.farmily.info의 /api/v1/health 경로를 443 포트로 10초마다 찌르고, 3번 연속 실패하면 장애로 판정한다. 복구 절차 자동화 이번 테스트에는 장애 감지부터 결과 검증까지 이어지는 복구 절차 자동화를 함께 도입해서 돌렸다. EventBridge가 1분마다 서울 상태를 확인하다 이상을 감지하면(DynamoDB로 중복 실행은 막는다) Step Functions가 상태 진단 → (참고용) AI 위험도 분석 → 사람 승인 대기 → 승인 후 DB 승격·트래픽 전환 → 결과 검증까지 이어가는 흐름으로 움직인다. AI는 어디까지나 판단을 돕는 조언자일 뿐이고, 실제 전환은 사람이 Slack에서 승인 버튼을 눌러야 넘어간다. 테스트에 앞서 — 도쿄를 "서비스 가능한" 상태로 인프라(VPC, EKS, RDS Replica 등) 자체는 이미 구축돼 있었지만, 페일오버 테스트에 앞서 도쿄 dreks를 단순 인프라에서 실제로 앱이 도는 환경까지 끌어올려야 했다. 오토스케일링과 운영 애드온(clusterautoscaler, metricsserver 등)을 배치하고, IAM 정책과 Helm values, 네임스페이스 템플릿 같은 앱 배포 전제조건을 준비한 뒤, farmilyapp을 도쿄 dreks에 Helm으로 실제 배포했다. 배포 자체는 성공했지만 앱 파드가 곧바로 Ready 상태가 되지 않았다. CrashLoopBackOff였다. 원인은 앱 코드가 아니라 DR 시크릿의 DB 접속 정보 두 가지였다. 첫 번째는 DB URL 누락이었다. DR 시크릿에는 DBHOST, DBPORT, DBNAME, DBUSER, DBPASSWORD는 있었지만, Spring Boot가 요구하는 jdbc:postgresql:// 형식의 DBURL 키가 빠져 있었다. 'url' must start with "jdbc" 오류로 datasource 생성이 실패한 거였고, jdbc 형식의 URL을 추가해서 해결했다. 두 번째는 DB 사용자명이 어긋난 문제였다. FATAL: password authentication failed for user "farmily" 에러가 떴는데, 처음엔 비밀번호 문제로 보였지만 실제로는 DR RDS의 마스터 계정이 farmily가 아니라 farmilyadmin이었다. 계정명을 실제 RDS 마스터 계정에 맞춰 고치고 나서야 해결됐다. 두 가지를 고친 뒤 시크릿을 강제로 재동기화하고 앱을 재시작하자 정상으로 돌아왔다. DB 커넥션 풀이 drrds에 붙었고, 마이그레이션이 정상적으로 검증됐으며, Deployment가 2/2 Available로 안정됐다. 여기서 얻은 교훈은 세 가지다. 환경변수 키를 여러 군데에 중복으로 넣으면 어디서 병합된 결과인지 추적하기 어려우니 한쪽에만 둘 것, DB 계정명은 반드시 RDS 실제 master username과 맞출 것, 시크릿을 갱신해도 이미 떠 있는 파드의 환경변수는 자동으로 바뀌지 않으니 수정 뒤에는 앱을 재시작할 것. 이렇게 도쿄 앱을 2/2로 띄워 둔 상태가 이어지는 페일오버 테스트의 출발점이 됐다. 측정 결과 요약 지표 측정값 목표 판정 읽기/헬스 RTO 약 5분 29초 RTO 15분 충족 전체 서비스 RTO (쓰기 성공) 약 9분 14초 RTO 15분 충족 보수적 RTO (백업 완료) 약 13분 5초 RTO 15분 충족 RPO (승격 직전 복제 지연) 약 126초(2.10분) RPO 60초 미달 읽기/헬스 RTO는 Route53이 도쿄로 넘어가고 도쿄 ALB가 200을 처음 돌려준 시점까지의 시간이다. 쓰기까지 포함한 전체 서비스 RTO는 도쿄 RDS를 승격해 쓰기가 처음 성공한 시점 기준이며, 백업 완료까지 보수적으로 잡으면 약 13분 5초였다. RPO는 대시보드 카드로 2.10분(126초)이 찍혔다. 목표와 견주면 RTO는 9분 14초로 15분 안에 들어와 충족했지만, RPO는 126초로 60초 목표를 두 배 넘겼다. 복구 속도는 기준을 만족했지만 복제 지연이 목표보다 컸다는 게 이번 테스트의 가장 중요한 결과다. 장애 전환 타임라인 (KST) 시각 이벤트 14:11:55 FIS 네트워크 장애 시작 — 모든 RTO 기준점(T0) 14:12:00 Route53 헬스체크 첫 실패 14:1214:15 헬스체크 0 / 부분 실패 구간 14:16:00 헬스체크 다시 1로 표시 (서울 복구 아님 — 도쿄가 200 응답한 것) 14:17:24 api.farmily.info 도쿄 ALB로 200 응답 — 읽기/헬스 RTO 종료 (5분 29초) 14:19:20 RDS promotereadreplica 호출 (전환 후 약 2분 공백) 14:20:29 도쿄 drrds 독립 primary 승격 완료 (승격 약 1분 9초) 14:21:09 farmdiaries write 성공 — 전체 서비스 RTO 종료 (9분 14초) 14:25:00 RDS 백업 완료 — 보수적 RTO (13분 5초) 14:26:56 FIS 실험 종료 단계별 분석 ① 장애 감지 — 서울 리전 FIS로 서울 네트워크를 끊자 api.farmily.info 글로벌 헬스체크 상태값이 1에서 0으로 떨어졌다. 같은 시각 서울 ALB의 정상 타겟 수가 2에서 0으로 내려갔고 ALB 5xx가 1.4천 건 가까이 치솟았다. 세 패널이 같은 시점을 가리키는 걸 봐서 장애 주입은 의도대로 먹혔다. 한 가지 눈여겨볼 부분은 FIS의 scope를 vpc가 아니라 all로 줘야 한다는 점이다. 첫 시도에서 scope를 vpc로 했더니 VPC 내부의 ALB에서 Pod, RDS로 가는 통신이 살아있어서 헬스체크가 계속 성공했고 failover가 일어나지 않았다. ② 트래픽 전환 — 서울에서 도쿄로 서울 ALB 요청 수는 14시 11분께 3.5천까지 잠깐 튀었다가 곧바로 0으로 빠졌다. 같은 시간 도쿄 ALB 요청 수가 올라오기 시작해 100에서 130 사이를 오갔고 도쿄 ALB 정상 타겟 수는 2로 안정적으로 유지됐다. 트래픽이 서울에서 도쿄로 옮겨간 게 그래프에서 분명하게 보인다. 페일오버 타임라인 패널에서도 헬스체크가 약 14시 12분부터 16분까지 빨간 구간(장애)으로 표시됐다가 다시 초록으로 돌아왔다. ③ 데이터 복제 — RPO 도쿄 drrds의 복제 지연을 RPO 관점에서 본 패널이다. 60초 임계선을 그어 뒀는데, 실제 지연은 톱니 모양으로 출렁이며 최고 4분 17초까지 올라갔다. 승격 직전 기록한 값이 약 126초였고 이게 이번 테스트의 RPO가 됐다 — 60초 목표를 넘긴 셈이다. RDS 연결 수 패널을 보면 서울 prodrds 연결은 장애 중 잠깐 30까지 줄었다가 회복했고 도쿄 drrds 연결은 60 근처를 유지하다 14시 20분께 8까지 떨어진 구간이 있다. 승격 과정에서 연결이 끊기는 순간으로 보인다. 여기서 짚을 공백이 하나 있다. 트래픽은 14시 17분에 도쿄로 넘어갔지만 promote 호출은 14시 19분 20초에야 들어갔다. 2분 남짓한 이 사이에 도착한 쓰기는 도쿄 RDS가 아직 read replica라 readonly transaction 오류로 실패했을 수 있다. 다음 테스트에서는 도쿄 전환을 확인하는 즉시 ReplicaLag를 기록하고 promote를 실행해 이 틈을 좁혀야 한다. ④ 도쿄 리전 수용 도쿄가 트래픽을 받아낼 준비가 돼 있었는지를 본다. dreks 노드 수는 1로, 실행 중인 파드 수는 약 16으로 테스트 내내 일정했다. 도쿄 쪽 컴퓨트 용량이 흔들림 없이 떠 있었다. ⑤ 페일오버 후 서비스 상태 도쿄로 넘어간 뒤 서비스 품질을 본 구간이다. 도쿄 ALB 응답시간 p99는 14시 20분께 4초까지 튀는 등 몇 차례 스파이크가 있었다. 도쿄 ALB 5xx 비율은 14시 15분께 0.78%까지 올랐다가 5% 경계선 아래에서 가라앉았다. 노드 포화도는 서울 prodeks 기준 메모리가 50%에서 70% 사이, CPU는 간헐적으로 튀는 정도였다. 앱 시그널 지연은 0.7 근처에서 평탄했고 장애(Fault) 신호는 잡히지 않았다. 응답시간이 잠깐 튄 건 승격 직후 커넥션이 다시 맺히는 구간과 겹치는 것으로 보인다. 5xx가 1% 미만에 머문 만큼 도쿄가 트래픽을 받아내는 데는 무리가 없었다. ⑥ 복구·페일백 API 헬스체크는 14시 12분부터 15분까지 0으로 떨어졌다가 다시 1로 회복했다. 도쿄 RDS 복제 지연 소진 추이는 앞서 본 것과 같은 톱니 곡선을 그리며 14시 18분 무렵 데이터가 끊긴다. 옆에는 페일백 체크리스트가 함께 정리돼 있는데, 서울 Route53 헬스체크가 계속 UP인지 확인하고, 서울 RDS를 재기동한 뒤 도쿄에서 서울로 복제를 다시 세우고, ReplicaLag가 0에 가깝게 소진되면 행/체크섬으로 데이터 정합성을 검증한 다음, Route53 우선순위를 서울로 되돌리고, 도쿄 컴퓨트를 원상태 비율로 줄이는 순서다. 승격된 도쿄가 받은 쓰기를 서울로 역동기화하는 작업이 페일백의 핵심이다. 이번 테스트에서 도쿄에만 쓰인 데이터는 farmdiaries 한 건이었다. 같은 시간대 서울 RDS에는 이 행이 없었는데, promote로 복제가 끊긴 뒤 당연한 결과다. 한 건이라 행 단위 수동 이관으로 충분하다. 다만 실제 장애에서 도쿄에 쓰기가 많이 쌓였다면 행을 하나씩 옮기는 대신 도쿄를 새 기준(source of truth)으로 두고 서울을 도쿄에 맞춰 재구성하는 편이 낫다. 상시 운영 대시보드에서 함께 본 것들 페일오버 테스트와 같은 시간대를 상시 운영 대시보드(AMP)로도 떼어 봤다. FIS가 서울 네트워크를 끊었을 때 그 여파가 다른 컴포넌트에는 어떻게 나타났는지 함께 확인할 수 있었다. 클러스터 헬스 — 스크랩 타겟 24개가 모두 정상으로 잡혔고 노드는 3개였다. 노드 CPU는 한 노드만 14시 16분께 85%까지 올라갔고 나머지는 낮게 깔렸다(argocd가 올라간 노드의 동기화 작업으로 보인다). 노드 메모리는 클러스터 전체가 50% 안팎에서 평탄했다. 결국 테스트가 prod 클러스터의 기본 체력에는 부담을 주지 않았고, 한 노드의 CPU 스파이크도 메모리까지 번지지 않고 끝났다. Istio 메시 — farmilyapp 요청률이 12.5 req/s까지 올랐다가 14시 16분 이후 0으로 빠졌다. 서울 경로가 끊기면서 prod로 들어오던 트래픽이 사라진 구간이다. 응답시간 p99는 한때 1분 가까이 치솟았는데, 막힌 경로에서 타임아웃과 재시도가 쌓이며 생긴 꼬리 지연으로 보인다. 반면 5xx 에러율은 0.18%까지만 잠깐 올랐다가 0으로 떨어졌다. 메시가 장애를 그대로 드러내면서도 에러 폭증 없이 버텨냈다. KEDA — 스케일러 활성·메트릭 값·에러율이 모두 0 근처에서 평탄했다. 부하 테스트가 아니라 장애 전환 테스트라 큐 깊이 같은 스케일 트리거가 움직일 일이 없었다. 에러가 0인 걸 보면 KEDA는 발동만 안 했을 뿐 정상 대기 상태였다. Karpenter — 파드 상태는 Running 약 50개로 안정적이었고 NodePool CPU는 4코어 근처였다. 노드 생성·종료율과 프로비저닝 지연 p90 패널은 데이터가 없었다. 테스트 내내 새 노드를 띄울 일이 없어 이벤트 자체가 안 생긴 정상 상태다. 잠깐 보인 Pending은 롤아웃으로 파드가 갈려 나가는 순간이다. 이번 부하는 기존 용량으로 다 받아냈고 오토프로비저닝까지 갈 필요는 없었다. Argo Rollouts — 14시 13분께 farmilyapp 롤아웃이 Completed에서 Progressing으로 넘어갔다. desired가 2에서 4로 뛰고 available이 잠깐 0으로 떨어졌다가 2로 돌아왔다. 새 리비전이 뜨는 동안의 surge와 교체 과정이다. 시점이 장애 전환과 겹치는 걸 보면, FIS로 prod 파드가 흔들린 구간과 맞물려 available이 0까지 내려간 듯하다. 그래도 롤아웃은 멈추지 않고 끝까지 진행됐다. 핵심 교훈 이번 테스트로 분명해진 것들을 정리하면 이렇다. 헬스체크 하나로 서비스 전체 상태를 대표할 수 없다. /api/v1/health가 성공해도 내부 기능은 실패할 수 있다. 게다가 헬스체크 대상이 failover 도메인인 api.farmily.info 자체라서 도쿄가 200을 주기 시작하면 헬스체크가 다시 Healthy로 돌아온다. 실제로 서울이 여전히 장애인데도 14시 16분에 회복으로 표시됐다. Healthy가 곧 "서울 정상"을 뜻하지 않는다는 얘기다. 서울의 실제 생사를 보려면 서울 전용 엔드포인트가 따로 있어야 한다. RDS 승격은 앱 설정을 자동으로 바꿔주지 않는다. 사용자가 어느 앱과 DB를 쓰는지는 Route53이 정한다. 승격 이후에는 서울과 도쿄가 split 상태가 되어 자동으로 동기화되지 않는다. 무엇보다 쓰기 가능 RTO와 Route53 전환 RTO는 다른 값이라 반드시 나눠서 기록해야 한다. Route53이 도쿄로 넘어가도 도쿄 RDS가 아직 replica면 쓰기는 실패하기 때문이다. 한 줄로 줄이면 이렇다. DR failover 자체는 동작한다. 다만 쓰기까지 포함한 서비스 복구는 Route53 전환만으로 끝나지 않는다. RDS promote 타이밍과 데이터 복구 전략까지 runbook에 들어가야 진짜 복구다. 모니터링 체크리스트 다음 DR 테스트에서 무엇을 봐야 하는지 미리 추려 뒀다. 필수 세 가지는 최소한으로 챙길 지표고 나머지는 여유가 되면 함께 본다. 필수 지표 (최소 3가지) 지표 확인 내용 기준 1 Route53 헬스체크 서울 ALB가 Unhealthy로 전환되는지 30초 내 실패 감지 2 RDS Replica Lag 장애 시점 복제 지연 (RPO 측정) 60초 이내 3 트래픽 전환 서울 ALB 요청 수 0으로 감소 / 도쿄 증가 시작 전환 확인 추가 지표 (권장) 지표 확인 내용 기준 4 ALB Healthy Hosts 서울 Healthy 타겟 0 / 도쿄 유지 전환 확인 5 RDS Connections 서울 연결 감소 / 도쿄 증가 전환 확인 6 EKS 파드 개수 도쿄 파드 1개→증가 Auto Scaling 동작 7 API 응답 시간 도쿄 cold cache 응답 시간 증가 일시적 허용 8 에러율 5xx 에러 비율 5% 이내 시간대별 체크 (T = 장애 시작) 시점 기대 동작 T+0 장애 시작 — 서울 ALB 타겟 죽음 T+30초 헬스체크 실패 → Slack 알림 T+1분 도쿄로 트래픽 전환 T+2분 도쿄 파드 증가 시작 T+5분 RDS Lag 기록 (RPO) T+15분 도쿄 RDS 쓰기 가능 → 서비스 완전 복구 이번 테스트 실측은 트래픽 전환 확인이 T+5분 29초, 쓰기 복구가 T+9분 14초로 아래 기대치보다는 느렸지만 15분 목표 안에는 들어왔다. 헬스체크가 도쿄 200 응답에 다시 Healthy로 돌아오는 함정과 Slack 알림이 특정 리전에서만 뜨는 문제는 핵심 교훈·후속 조치 항목과 함께 보면 된다. 후속 조치 다음 테스트 전까지 손볼 것들이 있다. 도쿄 RDS에만 남은 데이터를 처리해야 하는데, 테스트 데이터면 폐기하고 보존이 필요하면 행 단위로 서울에 옮긴다. 도쿄 RDS는 서울 prodrds의 read replica로 다시 만들어 둬야 같은 방식으로 재테스트가 된다. 헬스체크는 서울 전용, 도쿄 전용, 글로벌로 나누고, CloudWatch 알람은 헬스체크 지표가 조회되는 리전에 맞춰 만들어야 한다. 지난 테스트에서 알람을 엉뚱한 리전에 만들었다가 INSUFFICIENTDATA가 떠 알림이 가지 않았다. 마지막으로 도쿄 앱이 지금 서울 ECR 이미지를 끌어다 쓰고 있으니 도쿄 ECR로 crossregion 복제를 걸고 이미지 경로를 도쿄 ECR로 바꿔야 진짜 리전 격리가 된다.

Sep 17, 2026AWS
[DR] DR 테스트 후기 — 서울 리전 장애 유발로 도쿄 리전 활성화 하기

[Kubernetes] HAproxy를 통한 마스터 노드 삼중화(HA) 구성

쿠버네티스 클러스터를 운영할 때 마스터 노드(컨트롤 플레인)가 1대뿐이라면, 그 노드가 죽는 순간 클러스터 전체를 제어할 수 없게 된다. 이런 단일 장애점(SPOF)을 없애기 위해 마스터 노드를 여러 대로 구성하고, 그 앞단에 로드밸런서를 두는 것이 일반적인 고가용성(HA) 구성이다. 이번 글에서는 HAproxy를 이용해서 마스터 노드 3대를 삼중화하는 과정을 정리해본다. 고가용성(HA) 구성의 원리 마스터 노드를 여러 대로 구성한다고 해서 모든 노드가 동시에 요청을 처리하는 건 아니다. 보통 3개의 마스터 노드가 있다면 그중 한 개만 활성화(Active) 상태이고, 나머지는 Standby 상태로 대기한다. 그러다가 Active 상태였던 노드에 문제가 생기면, 대기 중이던 다른 노드 하나가 새로운 Active로 전환되면서 서비스를 계속 유지하는 방식이다. 노드 개수는 왜 홀수여야 할까? 새로운 Active 마스터 노드를 선출하려면 마스터 노드의 개수를 반드시 홀수 개(3, 5, 7, 9...) 로 구성해야 한다. 이는 네트워크 분할(Network Partition) 같은 상황에서 클러스터가 과반수 이상의 동의를 얻어 의사결정 권한을 명확히 하기 위함이다. 짝수 개로 구성하면 표가 정확히 반으로 갈리는 상황(Splitbrain)이 발생할 수 있어서, 어느 쪽이 정상 다수인지 판단할 수 없게 된다. 그럼 여기서 말하는 "의사결정"이란 정확히 뭘까? 조금 더 구체적으로 들어가보자. 쿠버네티스의 모든 상태 정보 — 어떤 파드가 떠 있는지, 어떤 노드가 살아있는지, 디플로이먼트 설정이 뭔지 — 는 각 마스터 노드 안에 있는 etcd라는 저장소에 기록된다. kubectl apply로 뭔가를 배포하든, 노드가 하트비트를 보내든, 컨트롤러가 파드 개수를 조정하든, 결국은 전부 "etcd에 이 상태 변경을 기록해달라"는 요청으로 이어진다. 마스터가 3대면 etcd도 3벌이 각자 떠서 서로 동기화하고 있는데, 한 대에만 기록하고 끝내버리면 나머지 두 대는 그 사실을 모르는 채로 남게 된다. 그래서 etcd는 Raft라는 분산 합의 알고리즘을 사용해서, 상태 변경 요청이 들어올 때마다 "이 변경을 클러스터의 공식 기록으로 확정해도 되는가"를 매번 투표로 결정한다. 리더 역할을 맡은 etcd가 나머지 멤버들에게 "이거 기록해도 될까?"라고 묻고, 과반수가 "나도 기록했다"고 응답해야 그 변경이 확정(commit)된다. 과반수 동의를 못 받으면 그 요청은 보류되고, 클러스터 입장에서는 새로운 배포도, 파드 상태 갱신도 반영되지 않는 상태가 된다. 이 투표를 진행할 리더가 누구인지를 정하는 것도 같은 방식의 의사결정이다. 리더가 죽으면 남은 멤버들끼리 다시 투표해서 새 리더를 뽑는데, 이 재선출 과정 동안 잠깐 요청이 안 먹히는 시간이 생긴다. 뒤에서 소개할 장애 테스트에서 kubectl 명령이 한순간 타임아웃 나는 것도 바로 이 재선출 과정 때문이다. 쿼럼(quorum) 계산 — 몇 대까지 죽어도 괜찮을까? 마스터가 3대일 때 과반수(쿼럼)는 2표다. 즉 3대 중 2대만 살아있어도 투표는 성립하고, 클러스터는 정상적으로 의사결정을 계속할 수 있다. 반대로 3대 중 2대가 죽어서 1대만 남으면, 1표로는 과반수(2표)에 못 미치기 때문에 그때는 진짜로 쿼럼이 깨지고 클러스터가 쓰기 작업을 처리하지 못하는 상태에 빠진다. 정리하면 3대 구성에서는 딱 1대까지만 장애를 허용할 수 있는 셈이다. 그렇다면 "짝수인 4대로 구성하면 더 안전하지 않을까?" 하는 생각이 들 수 있는데, 계산해보면 그렇지 않다. 4대 구성의 과반수는 3표이므로, 4대 중 2대가 죽으면 역시 쿼럼이 깨진다. 결국 4대든 3대든 버틸 수 있는 장애 대수는 똑같이 1대뿐이고, 4대는 서버 한 대를 더 쓰면서도 내결함성은 그대로인 비효율적인 구성이 된다. 같은 수준의 내결함성을 얻는 데 필요한 최소 노드 수를 구하면 자연스럽게 홀수(3, 5, 7...)가 나오기 때문에, 마스터 노드는 관례적으로 홀수 개로 구성하는 것이다. 실습 아키텍처 이번 실습에서는 아래와 같이 구성한다. 마스터 노드 3개 (m1, m2, m3) — 각각 etcd를 포함한 컨트롤 플레인 역할 HAproxy 서버 1개 — 마스터 노드 앞단에서 로드밸런싱 + 리버스 프록시 역할 kubectl 명령은 HAproxy 서버의 가상 IP(VIP)를 향해 요청을 보내고, HAproxy가 이 요청을 뒤쪽의 m1 / m2 / m3 중 살아있는 노드로 분산해주는 구조다. 1. HAproxy 서버 설정 먼저 HAproxy 역할을 할 서버에 HAproxy를 설치한다. bash apt update y apt install y haproxy 설정 파일을 연다. bash vi /etc/haproxy/haproxy.cfg 기존 내용은 전부 지우고, 아래 내용으로 채워준다. frontend kubernetesmasterlb bind 0.0.0.0:6443 option tcplog mode tcp defaultbackend kubernetesmasternodes backend kubernetesmasternodes mode tcp balance roundrobin option tcpcheck option tcplog server k8smaster1 211.183.3.101:6443 check server k8smaster2 211.183.3.102:6443 check server k8smaster3 211.183.3.103:6443 check frontend: 클라이언트(kubectl 등)의 요청을 6443 포트로 받는다. 쿠버네티스 API 서버의 기본 포트가 6443이기 때문이다. backend: 실제 요청을 전달할 대상, 즉 마스터 노드 3대의 IP와 포트를 나열한다. balance roundrobin으로 요청을 순서대로 분산하고, option tcpcheck로 각 서버가 살아있는지 주기적으로 확인한다. 마스터 노드 3대를 위 IP들을 갖도록 구성해두면 된다. 삼중화가 잘 구성되어 있다면, 특정 Active 마스터 노드에 문제가 생기더라도 Standby 상태였던 마스터 노드가 자동으로 Active로 선출되어 서비스가 유지된다. 설정을 저장했으면 HAproxy를 재시작하고 부팅 시 자동 실행되도록 등록한다. bash systemctl restart haproxy systemctl enable haproxy 참고: 이 시점에는 아직 m1 m3 마스터 노드들이 준비되지 않았기 때문에, HAproxy 입장에서는 백엔드 서버들이 응답하지 않아 에러 로그가 뜰 수 있다. 마스터 노드들을 구성한 뒤 다시 확인하면 된다. 2. 첫 번째 마스터 노드(m1) 초기화 이제 m1 서버에서 클러스터를 초기화한다. 이때 핵심은 controlplaneendpoint에 HAproxy 서버의 IP를 지정해주는 것이다. 이렇게 해야 클러스터의 모든 구성원이 HAproxy를 거쳐서 API 서버에 접근하게 된다. bash kubeadm init podnetworkcidr=10.244.0.0/16 uploadcerts kubernetesversion=v1.30.3 \ ignorepreflighterrors=all \ controlplaneendpoint=211.183.3.50 초기화가 끝나면 kubectl을 사용할 수 있도록 kubeconfig를 설정해준다. bash mkdir p $HOME/.kube sudo cp i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id u):$(id g) $HOME/.kube/config 3. m2, m3를 컨트롤 플레인으로 조인 kubeadm init이 끝나면 콘솔에 조인용 명령어가 출력된다. 이 중 컨트롤 플레인 노드용 조인 명령을 복사해서 m2, m3 서버에 각각 붙여넣어 실행하면 된다. (워커 노드용 조인 명령과는 다른 명령이므로 헷갈리지 않도록 주의한다.) m2, m3에 컨트롤 플레인 조인 명령을 복붙해주면 마스터 노드 삼중화 구성이 완료된다. 4. HAproxy 서버에서 kubectl 사용하기 HAproxy 서버에서도 클러스터를 직접 관리할 수 있으면 편하다. 이를 위해서는 두 가지가 필요하다. 1. kubectl 명령이 설치되어 있어야 한다. 2. 해당 클러스터의 kubeconfig 파일(m1의 /etc/kubernetes/admin.conf에 존재)을 가져와야 한다. kubectl 설치 bash aptget install y apttransporthttps cacertificates curl mkdir p /etc/apt/keyrings curl fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key sudo gpg dearmor o /etc/apt/keyrings/kubernetesaptkeyring.gpg echo 'deb [signedby=/etc/apt/keyrings/kubernetesaptkeyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' sudo tee /etc/apt/sources.list.d/kubernetes.list aptget update aptget install y kubectl aptmark hold kubectl kubeconfig 가져오기 m1에서 admin.conf 내용을 출력해서 복사한다. bash 에서 cat /etc/kubernetes/admin.conf 내용 복사 haproxy 서버에서 kube 설정 디렉터리를 만들고, 복사한 내용을 붙여넣는다. bash 에서 mkdir /.kube vi /.kube/config 파일 생성 후 m1에서 복사한 내용을 붙여넣기 이제 HAproxy 서버에서 kubectl get nodes를 실행하면 m1, m2, m3 세 노드가 모두 정상적으로 조회되는 것을 확인할 수 있다. 5. 장애 상황 테스트 — Active 노드 다운 삼중화가 실제로 잘 동작하는지 확인해보자. m1 서버(현재 Active 역할)를 강제로 중지시킨 뒤, HAproxy 서버에서 다시 kubectl 명령을 날려본다. 바로 직후에는 아래처럼 명령이 먹히지 않는 순간이 발생한다. root@haproxy: kubectl get nodes Unable to connect to the server: net/http: TLS handshake timeout HAproxy가 죽은 노드를 헬스체크로 감지하고 백엔드에서 제외하기까지는 약간의 시간이 걸리기 때문에 나타나는 현상이다. 조금 뒤 watch 명령으로 다시 확인해보면, m1이 NotReady 상태로 바뀌고 나머지 m2, m3는 여전히 Ready 상태로 정상 동작하고 있는 것을 확인할 수 있다. bash watch kubectl get nodes 즉, Active였던 m1이 죽어도 나머지 마스터 노드(m2, m3)가 살아있는 한 클러스터 제어 자체는 계속 정상적으로 이루어진다는 걸 실습으로 확인할 수 있었다. 결국 HAproxy를 이용한 마스터 노드 삼중화의 핵심은 "요청을 받는 창구(HAproxy)는 하나로 통일하되, 실제로 그 요청을 처리하는 마스터는 여러 대를 두어 하나가 죽어도 나머지가 대신 응답하게 만드는 것"이다. 단일 장애점을 없애고 안정적으로 클러스터를 운영하려면 꼭 필요한 구성이라는 걸 다시 한번 체감했다.

Sep 08, 2026kubernetes
[Kubernetes] HAproxy를 통한 마스터 노드 삼중화(HA) 구성

[Kubernetes] HPA(HorizontalPodAutoscaler) 개념 정리 및 실습

쿠버네티스로 서비스를 운영하다 보면 트래픽이 갑자기 몰리는 순간이 반드시 온다. 이럴 때마다 사람이 직접 파드 개수를 늘렸다 줄였다 하는 건 사실상 불가능하다. 이번 글에서는 이 문제를 자동으로 해결해주는 쿠버네티스 리소스인 HPA(Horizontal Pod Autoscaler) 를 개념부터 실습까지 정리해본다. HPA란? HPA는 CPU, 메모리 사용량 혹은 사용자 정의 메트릭(Custom Metrics)의 부하에 따라 파드의 개수를 자동으로 늘리거나 줄여주는(Scaling) 쿠버네티스 리소스다. 트래픽이 갑자기 늘어나면 파드를 추가로 띄워서 대응하고 (Scaleout) 트래픽이 줄어들면 불필요한 파드를 줄여서 자원 낭비를 막는다 (Scalein) 즉, 사람이 개입하지 않아도 부하에 맞춰 자원을 효율적으로 관리해주는 것이 HPA의 목적이다. Vertical Scaling vs Horizontal Scaling 오토스케일링은 크게 두 가지 방향으로 나눌 수 있다. Vertical (Scale up/down): 파드 개수는 그대로 두고, 파드 하나가 쓸 수 있는 자원(CPU, 메모리)을 늘리는 방식 Horizontal (Scale out/in): 파드 자체의 개수를 늘리거나 줄이는 방식 HPA는 이름 그대로 후자, Horizontal 방식이다. Vertical 방식은 파드를 재시작해야 하는 등 비활성화 구간이 생기기 쉬운 반면, Horizontal 방식은 파드를 추가/제거하는 방식이라 무중단으로 대응하기 좋다는 장점이 있다. 다만 DB처럼 상태를 갖는(stateful) 워크로드는 여러 개로 늘려도 유의미하지 않은 경우가 많아서, 상태 없는(stateless) 서비스에 주로 적합하다. 실습 환경 준비 먼저 실습용 디렉터리를 하나 만들어준다. bash mkdir hpa cd hpa 1. 리소스를 제한하는 파드 생성 HPA가 CPU 사용률을 기준으로 스케일링 여부를 판단하려면, 먼저 파드에 CPU/메모리 자원을 얼마나 할당할지(requests)와 최대 얼마까지 쓸 수 있는지(limits)를 정의해줘야 한다. bash vi res.yml yaml spec: containers: name: nginx image: nginx:latest resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m" requests: 컨테이너 생성 시 최소한 보장받는 자원. 스케줄러가 파드를 배치할 노드를 고를 때 이 값을 기준으로 판단한다. limits: 컨테이너가 최대로 사용할 수 있는 자원. 이 값을 넘기면 제한이 걸린다. 이렇게 파드에 자원을 할당해두면, 파드에 부여된 리소스(CPU, RAM) 사용률이 특정 임계값 이상이거나 이하일 때 파드 개수를 자동으로 늘리거나 줄일 수 있게 된다. 다만 이걸 하려면 대상 파드의 자원 사용량을 실시간으로 모니터링해줄 메트릭 서버가 반드시 필요하다. 2. 메트릭 서버(Metrics Server) 설치 HPA는 파드의 CPU/메모리 사용량을 직접 측정하지 않는다. 클러스터 내에 별도로 떠 있는 메트릭 서버로부터 이 값을 가져와서 판단한다. 그래서 HPA를 쓰기 전에 메트릭 서버부터 설치해야 한다. 메트릭서버 매니페스트는 아래 깃허브에서 받을 수 있다. https://github.com/kubernetessigs/metricsserver bash curl Ls https://github.com/kubernetessigs/metricsserver/releases/latest/download/components.yaml o metric.yml 다운로드한 매니페스트를 열어서 몇 가지를 수정해줘야 한다. bash vi metric.yml 수정이 필요한 부분은 containers.args 쪽이다. yaml spec: containers: args: kubeletinsecuretls 추가 certdir=/tmp secureport=10250 kubeletpreferredaddresstypes=InternalIP 수정 kubeletusenodestatusport metricresolution=15s kubeletinsecuretls 옵션을 추가해서 kubelet과의 통신 시 TLS 인증서 검증을 생략하도록 한다. (사설/테스트 환경에서 인증서 문제로 메트릭 서버가 뜨지 않는 걸 방지하기 위함) kubeletpreferredaddresstypes 값에서 ,ExternalIP,Hostname을 삭제하고 InternalIP만 남겨서, 노드의 내부 IP로만 kubelet에 접근하도록 해준다. 수정이 끝났으면 클러스터에 적용한다. bash kubectl apply f metric.yml 3. 테스트용 Deployment / Service 배포 HPA로 스케일링을 테스트해볼 대상이 필요하다. nginx 기반의 간단한 Deployment와 Service를 하나 만들어준다. bash vi dep.yml yaml apiVersion: v1 kind: Service metadata: name: mysvc spec: selector: app: myweb ports: port: 80 targetPort: 80 apiVersion: apps/v1 kind: Deployment metadata: name: mydep spec: replicas: 3 selector: matchLabels: app: myweb template: metadata: labels: app: myweb spec: containers: image: 61.254.18.30:5000/ipnginx name: mywebcon resources: requests: cpu: "10m" limits: cpu: "20m" CPU 사용률을 기준으로 스케일링을 확인할 것이기 때문에, 일부러 requests.cpu, limits.cpu 값을 아주 작게(10m, 20m) 잡았다. 이렇게 해야 약간의 부하만 줘도 사용률(%)이 쉽게 임계치를 넘어서 스케일링 동작을 눈으로 빠르게 확인할 수 있다. bash kubectl apply f dep.yml 배포 후 kubectl top pod로 각 파드의 실제 자원 사용량을 확인할 수 있다. 4. HPA 생성하기 — 명령어로 바로 생성 가장 간단하게는 kubectl autoscale 명령어로 바로 HPA를 만들 수 있다. bash kubectl autoscale deploy mydep min=1 max=5 cpupercent=50 CPU 사용량 임계치를 50%로 설정해서, 최소 1개 최대 5개 사이에서 파드 개수를 자동으로 조절하겠다는 의미다. 5. HPA를 매니페스트로 작성하기 위에서 명령어 한 줄로 만든 것과 동일한 설정을 매니페스트(YAML) 파일로도 작성할 수 있다. 실제 운영 환경에서는 이렇게 선언적으로 관리하는 걸 선호한다. bash vi as.yml yaml apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler metadata: name: hpanginx spec: maxReplicas: 5 minReplicas: 1 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: mydep 오토스케일링 대상 Deployment 이름 명시 targetCPUUtilizationPercentage: 50 scaleTargetRef에 스케일링 대상이 되는 Deployment를 지정해주고, targetCPUUtilizationPercentage로 임계치를, minReplicas/maxReplicas로 파드 개수의 하한선과 상한선을 정해주는 구조다. 6. 부하를 줘서 스케일 아웃 확인하기 이제 실제로 트래픽을 흉내 내서 HPA가 정말 동작하는지 확인해볼 차례다. 아래 명령어로 0.1초 간격으로 서비스에 계속 curl 요청을 날려서 부하를 준다. bash i=1; while true; do sleep 0.001; echo $((i++)) curl s ; done 여기서 IP는 kubectl get svc로 확인한 대상 서비스(예: mysvc)의 ClusterIP를 넣어주면 된다. 7. 결과 확인 부하를 주는 동안 다른 터미널에서 HPA와 파드 상태를 실시간으로 지켜보면 스케일 아웃이 일어나는 걸 확인할 수 있다. bash kubectl get hpa 현재 오토스케일링 설정이 어떻게 되어 있는지, 그리고 CPU 사용률이 임계치 대비 몇 %인지 확인할 수 있다. bash kubectl top pod watch 명령어와 함께 사용하면 파드 개수가 늘어나는 과정을 실시간으로 관찰할 수 있다. bash watch kubectl get hpa watch kubectl top pod 부하를 주기 시작하면 CPU 사용률이 임계치(50%)를 넘으면서, 처음 3개였던 파드가 최대 5개까지 늘어나는 것을 볼 수 있다. 8. 부하가 사라지면 다시 Scalein 반대로 부하 명령을 멈추면 어떻게 될까? CPU 사용률이 떨어지면서 HPA가 자동으로 파드 개수를 줄이기 시작한다. [이미지 삽입: 부하 종료 후 REPLICAS가 줄어드는 kubectl get hpa / kubectl top pod 캡처] 흥미로운 점은, 처음 Deployment 매니페스트에서 지정했던 replicas: 3과는 상관없이 HPA가 설정한 minReplicas 값인 1개까지 파드 수가 줄어든다는 것이다. HPA가 붙는 순간, 파드 개수의 기준은 Deployment의 replicas가 아니라 HPA의 minReplicas / maxReplicas가 된다는 걸 실습을 통해 확인할 수 있었다. 마무리 이번 실습으로 정리한 흐름을 요약하면 다음과 같다. 1. 파드에 requests/limits로 CPU·메모리 자원을 할당한다. 2. 메트릭 서버를 설치해서 파드의 실시간 자원 사용량을 수집할 수 있게 한다. 3. 스케일링 대상이 될 Deployment/Service를 배포한다. 4. kubectl autoscale 명령 또는 HPA 매니페스트로 오토스케일링 정책(최소/최대 개수, CPU 임계치)을 설정한다. 5. 부하를 줘서 Scaleout을, 부하를 멈춰서 Scalein을 눈으로 확인한다. HPA는 결국 "메트릭 서버가 수집한 지표"와 "내가 설정한 임계치"를 비교해서 파드 개수를 알아서 조절해주는 도구다. Deployment의 replicas는 초기값일 뿐, HPA가 붙는 순간부터는 HPA의 설정값이 우선한다는 점만 기억해두면 실무에서 헷갈릴 일이 없을 것 같다.

Sep 02, 2026kubernetes
[Kubernetes] HPA(HorizontalPodAutoscaler) 개념 정리 및 실습

동일한 부하 스크립트를 온프레미스와 AWS 환경에 각각 테스트 해 보았다.

온프레미스(AsIs) vs AWS(ToBe) 같은 k6 스크립트, 같은 부하를 온프레미스 단일 VM과 AWS ECS Fargate에 그대로 가해서, "정말 클라우드가 버티는가"를 숫자로 확인한 기록. 결론부터: 온프레미스는 평상시 부하(VU 100)에서조차 이미 마비 상태였고, 클라우드는 42만 건이 넘는 요청을 에러 0%로 처리했다. 들어가며 부하테스트를 하면 보통 "얼마나 버티는지"를 확인한다고 생각한다. 그런데 이번 테스트의 결론은 조금 달랐다. 버티고 말고를 따질 필요도 없이, 온프레미스는 평상시 트래픽 수준에서조차 이미 죽어있었다. 같은 k6 시나리오(Baseline·Stress·Spike)를 온프레미스 단일 VM과 AWS ECS Fargate에 똑같이 쐈다. 온프는 세 시나리오 전부 p95가 44초 근처로 수렴하며 마비됐고, 클라우드는 태스크를 1 → 4개로 자동 확장하며 요청을 전부 받아냈다. 그리고 이 과정에서 클라우드 쪽에 잠복해 있던 버그 2건(Redis 역직렬화, RDS ReplicaLag)까지 실제로 찾아내서 고쳤다. 이 글의 한 문장 요약은 이거다 — 느려서 무너진 게 아니라, 캐시도 없고 수평 확장도 안 되는 구조라서 무너졌다. 한눈에 보는 결론 같은 부하·같은 스크립트(k6loadv3.js)를 온프레미스 단일 VM과 AWS ECS Fargate에 그대로 가했다. 결과는 정반대였다. 시나리오 온프레미스 (AsIs) AWS 클라우드 (ToBe) Baseline (100 RPS) p95 43.99s p95 31ms (워밍업 후) Stress (→2000 RPS) p95 45.02s, 처리량 60 → 33 RPS 역행, 유실 93% p95 2.2s, 에러 0%, 태스크 1 → 4 자동 확장, 427,421건 무장애 Spike (→2500 RPS) DB CPU 100% 2회 재포화, 첫 타임아웃 3건 단일 태스크 큐잉 흡수, 에러 0%, 종료 후 1분 내 복구 공통 병목 DB CPU 포화 + HikariCP pending 169 (App CPU 1320% 유휴) 병목 없음 — RDS CPU 한 자릿수, 태스크 수평 확장 온프레미스 단일 VM은 평시 부하조차 p95 44초로 사실상 응답 불가였고, 부하를 올릴수록 처리량이 오히려 꺾이는 구조적 천장(DB CPU 포화 + 커넥션 풀 고갈)을 보였다. 동일 부하에서 AWS는 에러 0%로 흡수하며 태스크를 1 → 4로 자동 확장했다. 이 격차가 VMware → AWS 마이그레이션의 정량적 당위성이다. 1. 목적과 테스트 설계 마이그레이션의 당위성을 "느낌"이 아니라 AsIs/ToBe 수치 대비로 증명하는 것이 목적이다. 멘토 조언("부하 테스트는 AsIs/ToBe 대비가 정석, 기준은 동시 접속자 수")에 따라 ① 온프레미스 현 구성의 한계점과 병목 특정 ② AWS 구성의 탄력성(오토스케일 실작동·무장애 흡수) ③ 장애·복구 거동을 MTTR·SLO·BCP·RCA 관점으로 해석한 운영 설계 근거 확보를 목표로 잡았다. 시나리오 3종 (공통 스크립트) 시나리오 목적 부하 패턴 baseline 평상시 기준선 100 RPS 일정 (RATEMULT=0.5) stress 한계점 탐색 100 → 2000 RPS 계단식 11단계 spike 순간 폭증 내성 순간 최대 2500 RPS 급증 → 급감 공통 파라미터는 RATEMULT=0.5, MAXHANDLE=100(반복 조회 농가 100명 = 캐시 효과 측정 범위). 엔드포인트는 공개 프로필 60% + health 40%(+ 일부 calendar), 부하 기준은 동시 접속자(VU)로 현실적 동시 농부 100200명을 가정했다. 환경 비교 항목 온프레미스 (AsIs) AWS 클라우드 (ToBe) 컴퓨팅 VMware VM, App 2vCPU/2GB 고정 1대 ECS Fargate, 오토스케일 1 → N (CPU 70%, 3분 평가) DB PostgreSQL VM 2vCPU/2GB RDS (연결 한도 225, MultiAZ) 캐시 없음 ElastiCache Redis 입구 / 부하생성 WAS 직접(192.168.x) / 노트북 k6 + Tailscale ALB(HTTPS) / 배스천 EC2(t3.small) 측정 한계 (정직한 기록): 두 환경 모두 k6에서 "Insufficient VUs, reached 2000" 경고가 떴다. 부하 생성기(클라이언트) 자체가 상한이었다는 뜻이며, droppediterations의 일부는 클라이언트 도착률 한계다. 그러나 서버 거동 차이는 명확하다. 온프는 달성 처리량이 수십 RPS로 붕괴하며 p95 44초(서버 포화)인 반면, 클라우드는 같은 조건에서 427,421건을 에러 0%로 처리하고 태스크를 확장했다. 비교는 유효하며, 클라우드의 진짜 한계는 클라이언트에 막혀 미측정인 만큼 더 높다. 2. AsIs — 온프레미스 (단일 VM) Baseline — 평시 부하에서 이미 "마비" 100 RPS 평상시 부하인데도 p95 43.99초(기준 0.8초의 약 55배), 달성 처리량은 목표의 60%(max 60.5 / mean 35.3 RPS)에 그쳤다. 에러는 0%지만 응답이 44초 걸리는 것은 사실상 장애다. 온프 Baseline · 자원포화 — DB CPU max 72.1%로 위험선(70%) 돌파, App(WAS) CPU는 20.4%로 유휴. 병목은 명확히 DB 레이어. 온프 Baseline · 커넥션풀 — HikariCP pending 169(풀 30 전부 점유·idle 0). 응답 지연의 직접 원인. JVM Heap 32.6%로 앱은 여유. 총 9,515건 처리 중 dropped 5,087건(약 35% 유실). Stress — DB CPU 100% 천장에 고정 100 → 2000 RPS로 올리자 DB CPU가 100% 완전 포화(mean 75.8%)됐다. 흡수할 추가 용량이 없어 달성 처리량이 오히려 60→33 RPS로 역행하고, 목표 부하의 93%(480,047건)가 유실됐다. p95 45.02초. HikariCP pending 169는 baseline과 동일하게 고정됐다. 온프 Stress · 자원포화 — DB CPU가 100%에 평탄하게 붙어 정체. WAS CPU는 13.2%. "더 줘도 DB가 먼저 천장을 친다"의 가장 명확한 증거. 온프 Stress · 부하 — 달성 요청률 max 32.9 / mean 12.2 RPS. baseline(60.5)보다 낮다. 서버 포화로 처리량이 역행하는 구간. Spike — 폭증 흡수 불가 + 첫 타임아웃 순간 폭증(목표 2500 RPS)에서 DB CPU가 "두 개의 산" 모양으로 두 차례 100% 재포화됐다(mean 66.2%). 폭증을 흡수할 버퍼가 없어 처음으로 타임아웃 실패 3건(60초 초과)이 발생했다. p95 44.88초, max 1분 0초. 커넥션풀은 세 시나리오 모두 동일하게 pending 169에서 막혔다. 온프 Spike · 자원포화 — DB CPU "두 개의 산": 1차 상승 → 중간 저점(36%) → 2차 100% 재포화. 충격 후 회복 전에 재포화. 온프 Spike · 지연·에러 — 폭증 구간(15:4915:50)에 4xx 401 스파이크, k6 로그에 request timeout 3건. 온프에서 처음 발생한 실질 실패. 종합 — "죽지는 않지만 마비" 10분 이상 고부하에도 전 인스턴스는 UP(Healthy 4/0)을 유지했다. 문제는 "죽음"이 아니라 "마비"다 — 살아는 있으나 p95 44초로 응답 불가다. 일관된 천장은 ① p95 4445초 ② DB CPU 72 → 100% 포화 vs App CPU 1320% 유휴 ③ HikariCP pending 169 고정 ④ 유실 35% → 93% → 91% ⑤ 부하를 올려도 처리량 역행으로 나타났다. 왜 이렇게 무너졌나 — 인과관계 5단계 App CPU는 노는데 p95 44초인 현상을 인과로 풀면 이렇다. 1. 근본 원인 = 캐시 부재 + 단일 DB 2vCPU. Redis가 없어 모든 읽기가 DB로 직행한다. 프로필·캘린더 조회가 매번 복수 테이블 JOIN으로 떨어지고, DB는 2vCPU뿐이라 연산이 곧 천장이 된다. 2. App CPU가 노는데 마비인 이유 — 톰캣/HikariCP 스레드가 DB 응답을 블로킹 대기(I/O wait)로 멈춰 있다. "일하는 중"이 아니라 "기다리는 중"이라 CPU를 거의 안 쓴다. 그래서 App을 증설해도 소용없다. 3. pending 169의 이유 — 풀 30개가 전부 느린 쿼리에 묶이면 새 요청이 대기 큐에 쌓인다. 동시 처리량 = 도착률 × 응답시간(Little's law)인데 응답시간(44초)이 폭발해 풀 처리율을 초과하며 169까지 적체됐다. 풀을 키워도 근본(느린 DB)이 그대로면 pending 위치만 바뀐다. 4. 처리량 역행의 이유 — DB CPU가 100%에 닿은 뒤엔 동시 쿼리들이 같은 자원(CPU·락·버퍼)을 두고 경쟁해 개별 쿼리가 더 느려진다. 전형적 혼잡 붕괴(congestion collapse)다. 5. 세 시나리오 p95가 모두 4445초인 이유 — 부하 방식과 무관하게 같은 평형점(풀 포화 + DB 100%)에 수렴하기 때문. 한계가 부하 패턴이 아니라 구조(용량)로 정해진다는 뜻이다. 요약하면 온프의 문제는 "느려서"가 아니라 구조가 한 곳(단일 DB)에서 직렬화되기 때문이다. 캐시가 그 직렬 구간을 우회시키고 수평 확장이 용량을 늘리는데, 온프엔 그 두 레이어가 없다. 3. ToBe — AWS 클라우드 (ECS Fargate) Baseline & RCA — Redis 역직렬화 버그 발견·수정 초기 baseline에서 응답은 빨랐으나(p95 65ms) apperrors가 59.67%에 달했다. 관측 → 로그 → 코드를 추적해 원인을 규명했다. 증상은 "첫 요청 200 → 두 번째부터 500 → TTL(10분) 만료 후 다시 200"이라는 간헐 장애·자가복구 패턴이었다. 원인은 RedisConfig의 GenericJackson2JsonRedisSerializer + DefaultTyping.NONFINAL 조합이 final 타입(Java record)에는 타입 정보를 붙이지 않는 비대칭이었다. record DTO 캐시가 쓰기는 {본문}, 읽기는 [타입, {본문}](WRAPPERARRAY)을 기대해, 캐시 HIT마다 역직렬화 예외 → 500이 발생했다. nonfinal 타입(crops)이 멀쩡했던 점이 이 가설을 뒷받침했다. 조치는 단순 버그 수정에 그치지 않고 재발 방지·장애 내성까지 설계했다. 직렬화 정상화 — DefaultTyping.NONFINAL → EVERYTHING으로 record/final 루트에도 타입 정보를 부여해 쓰기·읽기 라운드트립을 복원. 캐시 키 버전화 — CacheNames.VERSION(v2) 도입으로 직렬화 포맷이 또 바뀌어도 옛 키가 자동 격리돼 롤링 배포 중 포맷 혼재를 차단. 캐시 장애 내성 — CacheErrorHandler로 역직렬화 오류를 캐시 미스로 강등 + 깨진 키 evict → DB 폴백, Redis 일시 장애에도 500 대신 서비스가 지속되도록 설계. 변경 파일은 RedisConfig.java·CacheNames.java 2개이며, 코드 수정 후 에러는 59.67% → 0%로 떨어졌다. 클라우드 Baseline(버그) — ALB Target 5xx(앱 500) 8.94K 폭증. 캐시 히트 시점마다 500. 캐시가 독이 된 상태. 클라우드 Baseline(버그) — 현재 캐시 히트율 0%(빨강). 역직렬화 실패로 캐시가 무력화됨. 클라우드 Baseline(수정 후) — ALB 5xx 전부 0, 응답은 콜드스타트 첫 1점(p99 7.9s)만 튀고 14:21부터 평탄(p50 7.56ms). 버그 해소 입증. 클라우드 Baseline(수정 후) 요약 — 에러율 0%, Running 태스크 1(100 RPS는 CPU 13%로 오토스케일 트리거 미달 — 설계대로), RDS 연결 32. 이 과정에서 부하 시험이 운영 전 잠복 버그를 드러냈고, 관측 → 로그 기반 원인 규명 → 코드 수정 → 재현 시험으로 검증 루프를 완성했다. Stress — 오토스케일 1 → 4, 2000 RPS 에러 0% 흡수 CPU 평균 70%를 3분 연속 돌파하자 CloudWatch 알람이 발동해 desired 태스크가 1 → 2(14:53)→3(14:55) → 4(14:57)로 단계 증설됐다. 전 구간 apperrors 0% / httpreqfailed 0%(0/427,421건), ALB 5xx 0이었다. 클라우드 Stress · 가용성·오토스케일 — ECS 태스크 Running/Desired 1 → 2 → 3 → 4 단계 증설. 탄력 확장의 직접 증거. 클라우드 Stress · 자원포화 — ECS CPU avg 99.8% / max 100%(mean 70.1%)로 70·80% 선 돌파 → 스케일아웃 트리거. RDS CPU는 낮음. 클라우드 Stress · 요약 — 에러율 0%, Running 태스크 4, RDS 연결 122/225(한도 내 여유). RequestCount는 peak 66.6K/분(mean 20.4K)으로 온프와 자릿수가 다른 처리량. 지연 SLO 관점에서는 p95 2.18s로 "p95 클라우드 Spike · 자원포화 — ECS CPU 15:36 29% → 15:37 avg 84.9%/max 99.9% → 15:38 20% 급락(고점 약 1분). 짧아서 오토스케일 평가창(3분) 미충족. 클라우드 Spike · 지연·에러 — 응답시간 15:36 피크(p99 9.52s) 후 15:38 0으로 감쇠, ALB 5xx 전부 0. 에러 없이 지연으로 흡수 후 즉시 회복. 클라우드 Spike · 캐시 — 현재 캐시 히트율 100%(초록). Redis가 DB를 보호. RDS CPU 피크도 35%에 그침. 에러율은 평탄 0%(max 0.0346%). 교훈: 짧은 스파이크는 반응형 오토스케일(3분 지연)로 못 막는다. 기존 용량이 큐잉으로 버티되 지연이 오르므로, 최소 태스크 버퍼 + 요청 큐/타임아웃 설계가 필요하다(개선 백로그). 4. AsIs vs ToBe 직접 비교 시나리오 / 지표 온프레미스 (AsIs) AWS 클라우드 (ToBe) Baseline p95 43.99s 31ms (워밍업 후) Stress p95 / 에러 45.02s / 0% (마비) 2.18s / 0% Stress 처리량 거동 60 → 33 RPS 역행 정상 흡수, 427,421건 무장애 Stress 확장 불가 (단일 VM 고정) 태스크 1 → 4 자동 확장 Stress 유실 93% (480,047건) 0% 실패 (도착률은 클라이언트 한계) Spike 거동 DB CPU 100% 2회 재포화, 타임아웃 3건 단일 태스크 큐잉, 에러 0% 흡수 Spike 복구(MTTR) 자력 회복 느림·재포화 종료 후 1분 내 복귀 병목 자원 DB CPU 100% + HikariCP pending 169 없음 (RDS CPU 한 자릿수, 연결 122/225) 5. 멘토링 지표로 본 해석 (MTTR·SLO·BCP·RCA) MTTR(평균 복구 시간): 클라우드 spike는 충격 종료 후 1분 내 p50 ms 복귀로 빠른 자력 회복을 보였다. 온프는 회복 전 재포화로 MTTR이 길고 불안정하다. "120초"는 일반값일 뿐, Farmily 기준 자체 복구 목표(SLO)를 정해 알람·헬스체크 기준으로 삼는다. SLO: 무결성 SLO(에러율2000 RPS) 재측정 인프라 관측 SLO 공식화, 알람 기준(5xx·DB CPU·HikariCP·ReplicaLag) 정비 모니터링 결론 부하 시험은 마이그레이션의 당위성을 숫자로 확정했다. 온프레미스 단일 VM은 평상시 부하에서 이미 p95 44초로 마비했고, 그 원인(DB CPU 포화 + HikariCP 풀 고갈, 캐시·수평확장 부재)은 튜닝이 아니라 구조의 문제였다. 동일 부하에서 AWS는 에러 0%로 흡수하며 태스크를 자동 확장했고, 시험 과정에서 잠복 버그(Redis 역직렬화)와 ReplicaLag 이상까지 사전에 제거했다. 남은 과제(지연 SLO 튜닝·스파이크 버퍼)는 클라우드 위에서 푸는 운영 최적화 영역이며, 온프에서는 애초에 도달할 수 없는 단계다. App CPU가 20%로 놀고 있다고 "여유 있다"는 뜻은 아니다. 기다리는 중인지, 일하는 중인지부터 구분해야 한다. 온프레미스가 무너진 건 느려서가 아니라, 캐시도 없고 수평으로 확장할 수도 없는 구조였기 때문이다. 그 구조를 그대로 두고 서버 스펙만 올렸다면, 아마 같은 자리에서 또 무너졌을 것이다. 그림 출처: Grafana 대시보드 스크린샷 (온프 = Prometheus exporters, 클라우드 = CloudWatch 데이터소스)

Aug 18, 2026AWS
동일한 부하 스크립트를 온프레미스와 AWS 환경에 각각 테스트 해 보았다.

AWS DMS로 온프레미스 PostgreSQL을 RDS로, pgvector 타입까지 무손실 마이그레이션하기

들어가며.. 온프레미스 PostgreSQL을 AWS RDS로 옮겨야 했다. 그냥 옮기는 거라면 어렵지 않았겠지만, Farmily DB에는 AI 콘텐츠 생성용 RAG 테이블에 pgvector 확장의 커스텀 타입 vector(1024) 컬럼이 있었다. 일반적인 DMS 방식은 표준 SQL 타입만 변환하기 때문에 이런 확장 타입을 그대로 옮기지 못한다. 그래서 AWS DMS의 동종(Homogeneous) 마이그레이션으로 Full Load + CDC 방식을 선택했고, 그 과정에서 서로 다른 원인으로 7번 막혔다. 이 글은 마이그레이션 과정에서 실제로 부딪힌 문제와 거기서 얻은 인사이트, 그리고 최종 검증 결과를 정리한다. 1. 왜 동종(Homogeneous) 마이그레이션인가 일반(이종) DMS의 타입 매핑 엔진은 표준 SQL 타입만 변환한다. vector(1024) 같은 확장 커스텀 타입은 매핑 대상에 없어 그대로 보존되지 않는다. 동종 마이그레이션은 다르다. 내부적으로 PostgreSQL 네이티브 도구(pgdump/pgrestore + 논리 복제)를 사용하기 때문에, 확장과 커스텀 타입을 원본 그대로 옮길 수 있다. 소스와 타깃이 같은 엔진(PostgreSQL)일 때만 쓸 수 있는 방식이라 이번 케이스에 정확히 맞았다. 온프레미스 PostgreSQL 15 (pgvector 0.8.2) ↓ DMS 동종 마이그레이션 (Full Load + CDC) AWS RDS PostgreSQL 18 (pgvector 0.8.1) 2. 왜 Tailscale 릴레이가 필요했나 온프레미스 DB는 VMware HostOnly 네트워크(192.168.30.0/24)에 있어 AWS에서 직접 접근할 수 없었다. DB를 인터넷에 직접 노출하는 방식은 보안상 제외했다. 대신 Tailscale(WireGuard 오버레이 VPN)로 온프레미스와 AWS를 같은 사설 오버레이 네트워크로 묶고, VPC 안의 릴레이 EC2가 socat으로 5432 포트를 온프레미스 DB로 포워딩하는 구조를 만들었다. DMS는 릴레이 EC2의 사설 IP로 접속하면 온프레미스 DB에 도달한다. [온프레미스 dbserver] [AWS prodvpc] PostgreSQL 15 릴레이 EC2(farmilybastion) RDS PostgreSQL 18 192.168.30.10 ──Tailscale──► 10.1.1.64 (socat :5432) ──► prodrds (VMnet HostOnly) WireGuard Tailscale IP 100.106.105.53 (DMS/RDS가 접속) 이 "릴레이 경유" 구조가 나중에 다룰 7번째 이슈(CDC subscription)의 원인이 된다. 3. Full Load + CDC — 무중단 전환의 기반 Full Load로 기존 데이터를 통째로 복사하고, 이후 발생하는 변경(INSERT/UPDATE/DELETE)을 CDC(Change Data Capture)로 실시간 반영한다. 두 단계를 함께 쓰면 데이터를 옮기는 동안에도 소스를 계속 운영할 수 있어, 무중단 전환(cutover)의 기반이 된다. DMS는 Start 시 연결 → 논리 복제 검사 → 스키마 복원 → Full Load → CDC 순서로 진행한다. 이 다섯 단계 각각에서 서로 다른 원인으로 총 7번 차단됐다. 4. 7개의 차단 — 트러블슈팅 여정 에러 메시지가 가리키는 단계를 단서로 원인을 좁혀가며 하나씩 해소했다. 요약하면 이렇다. 단계 증상 원인 유형 조치 1 타깃 연결 password authentication failed for user "admin" 시크릿 username 오타 admin → farmilyadmin 2 타깃 연결 no pghba.conf entry ... no encryption SSL 모드 불일치 none → require 3 논리 복제 검사 Logical replication is not enabled for TARGET RDS 기본값 미설정 파라미터그룹 rds.logicalreplication=1 + 재부팅 4 소스 연결 password authentication failed for user "dmsuser" DB 비번 ↔ 시크릿 불일치 비번 변경 후 시크릿 동기화 5 CDC publication must be superuser to create FOR ALL TABLES publication 권한 부족 소스 접속 계정을 슈퍼유저(postgres)로 전환 6 스키마 복원 role "farmily" does not exist 소유권 롤 부재 타깃에 동일 이름 롤 생성(이름표용) 7 CDC subscription could not connect to the publisher ... Connection timed out RDS→릴레이 경로 미허용 릴레이 SG에 5432 ← RDS SG 인바운드 추가 에러 메시지는 어느 계층이 막혔는지를 알려준다 일곱 번 막히면서 알게 된 건, 에러 메시지의 종류가 곧 "어느 계층에서 막혔는지"를 알려준다는 점이다. password authentication failed → 비밀번호 단계까지 도달했다는 뜻. 즉 네트워크는 이미 뚫려 있고, 자격 증명만 틀렸다. no pghba.conf entry ... no encryption → 매칭되는 접속 규칙이 없다는 뜻. SSL 모드나 대역이 안 맞는다. Connection timed out → 네트워크 자체가 안 뚫렸다는 뜻. 보안 그룹이나 라우팅을 봐야 한다. 이 구분 덕분에 매번 처음부터 전체를 의심하지 않고, 에러 메시지 하나로 확인할 범위를 곧바로 좁힐 수 있었다. 가장 비자명했던 문제 — CDC는 양방향을 요구한다 7개 중 가장 오래 걸린 건 5와 7이었고, 둘은 같은 뿌리를 공유한다. 처음 세운 가정은 "DMS만 릴레이에 붙으면 된다"는 단방향 모델이었다. 하지만 동종 마이그레이션의 CDC는 그렇게 동작하지 않았다. publication(5): 소스에서 변경을 내보내려면 FOR ALL TABLES publication을 생성해야 하는데, 이건 슈퍼유저 권한이 필요하다. 처음 만든 dmsuser는 SELECT + REPLICATION 권한만 있는 일반 계정이라 거부됐다. subscription(7): 타깃(RDS)이 publication을 구독하려면, RDS가 직접 릴레이 EC2에 접속해야 한다. 지금까지 열어둔 경로는 "DMS → 릴레이"뿐이었고, "RDS → 릴레이" 경로는 열려 있지 않았다. DMS → 릴레이(10.1.1.64) ✅ (열려 있었음) RDS → 릴레이(10.1.1.64) ❌ → ✅ (추가로 열어야 했음) 정리하면, publication은 소스 측 슈퍼유저 권한을, subscription은 타깃→소스 직접 연결을 요구한다. "DMS가 릴레이에 붙는 것"과 "RDS가 릴레이에 붙는 것"은 별개의 경로이고, CDC를 쓰려면 둘 다 필요하다는 걸 두 번의 차단을 겪고 나서야 이해했다. 5 Whys — 7을 파고들면 단계 질문 → 답 Why 1 왜 막혔나 → RDS가 릴레이(10.1.1.64:5432)에 직접 접속을 시도했으나 timeout Why 2 왜 timeout이 났나 → 릴레이 SG가 RDS SG로부터의 5432 인바운드를 허용하지 않았음 Why 3 왜 막지 못했나 → 네트워크 경로를 "DMS → 릴레이" 단방향으로만 설계함 Why 4 왜 사전에 못 찾았나 → 연결 검증을 psql(베스천 경로)로만 했고, RDS→릴레이 경로는 검증 항목에 없었음 Why 5 (근본) 동종 마이그레이션 CDC의 데이터 흐름(타깃이 소스에 직접 subscribe)에 대한 이해 부족 시크릿이 실제 접속 주체를 결정한다 또 하나 명확히 알게 된 것: DB 쪽에서 비밀번호를 바꾸고 pghba.conf를 열어도, Secrets Manager의 username이 여전히 예전 계정을 가리키면 DMS는 계속 그 계정으로 접속을 시도한다. 계정을 전환하는 실제 스위치는 DB 설정이 아니라 시크릿이었다. 5. 최종 성과 및 검증 7개 이슈를 모두 해소한 뒤 재시작해서, 아래 결과로 완주했다. 지표 결과 로드된 테이블 24개 (오류 0) 경과 시간 약 16분 31초 CDC 지연 시간 0ms 정합성 (소스=타깃 행 수) 전 테이블 일치 vector 타입 보존 vector(1024) 보존 (확장 0.8.2 → 0.8.1) 데이터 정합성 — 소스와 타깃에서 동일한 행 수 비교 쿼리를 실행해 확인했다. users 501,015건, notificationsettings 501,010건을 포함해 검증한 6개 테이블 전부 소스=타깃으로 일치했다. 총 약 540만 행(약 1GB) 규모였다. 타입·스키마 보존 — 이번 마이그레이션의 핵심 목적이었던 부분이다. 타깃 RDS에서 \d recipeembeddings를 확인한 결과, embedding 컬럼이 vector(1024)로 보존됐고 PRIMARY KEY·UNIQUE 제약·IDENTITY 시퀀스·인덱스 4개·text[] 배열 타입까지 전부 그대로 옮겨졌다. pgvector 확장 버전이 소스(0.8.2)와 타깃(0.8.1)이 달랐음에도 타입과 차원이 정확히 일치함을 확인했다. recipeembeddings.embedding 값은 현재 전부 NULL이다. 이번 검증의 대상은 "벡터 데이터 이전"이 아니라 vector(1024) 타입(스키마)·차원·인덱스·제약의 보존이라는 점을 분명히 해둔다. 6. 범위 — 여기까지 했고, 여기부터는 안 했다 이번 작업은 DMS Full Load + CDC로 데이터를 이전하고 정합성·타입 보존을 검증하는 단계까지다. 애플리케이션(WAS)의 DB 연결을 RDS로 바꿔 실제 트래픽을 넘기는 cutover(앱 전환)는 포함하지 않았다. 검증이 끝난 뒤에는 환경을 정리했다 — DMS 삭제, 그리고 CDC가 생성한 replication slot을 온프레미스에서 수동 삭제했다(DMS를 지워도 slot은 자동으로 없어지지 않는다. 방치하면 WAL이 계속 쌓여 디스크를 위협한다). 그래서 지금 시점에는 소스↔타깃 실시간 동기화가 끊긴 상태이고, 실제로 cutover를 하려면 CDC 동기화를 다시 구성해야 한다. "검증"과 "앱 전환"은 다른 단계라는 걸 분명히 해두고 싶었다. 7. 얻은 교훈 잘된 점 에러 메시지의 종류로 막힌 계층을 빠르게 좁힌 것, 그리고 이슈를 하나 해결할 때마다 바로 psql로 재검증한 뒤 다음 단계로 넘어간 것. 덕분에 "고친 줄 알았는데 사실 안 고쳐진" 상태로 다음 단계에 진입하는 일이 없었다. 개선할 점 사전 점검 체크리스트가 있었다면 1·3·6은 애초에 마주치지 않았을 문제였다. 그리고 CDC를 양방향 구조로 이해하지 못한 채 네트워크·권한을 단방향으로만 설계한 게 5·7의 공통 원인이었다 — 이건 사전 지식으로 막을 수 있는 문제였다. 다른 마이그레이션에도 적용할 수 있는 인사이트 동종 마이그레이션 CDC는 publication(소스, 슈퍼유저)과 subscription(타깃→소스 직접 연결)이라는 양방향 권한·네트워크를 요구한다. 릴레이 경유 구조라면 양쪽 방향 모두 SG를 열어야 한다. RDS는 논리 복제가 기본적으로 꺼져 있다. Full Load + CDC를 쓰려면 소스뿐 아니라 타깃에도 rds.logicalreplication=1(Static, 재부팅 필요)을 설정해야 한다. 실제 접속 계정을 바꾸는 스위치는 DB 설정이 아니라 Secrets Manager의 값이다. 메이저 버전 차이(PG15→18)나 확장 버전 차이(pgvector 0.8.2→0.8.1)가 있어도, 타입 수준 검증을 직접 하면 무손실 이전을 확인할 수 있다. 마치며 일반 DMS로는 vector 커스텀 타입을 지킬 수 없었기 때문에 동종 마이그레이션을 선택했고, 그 선택이 맞았다는 걸 타입 검증으로 확인했다. 하지만 그 과정이 매끄럽지는 않았다 — 인증, SSL, 논리 복제, 권한, 스키마, 네트워크까지 다섯 단계 전부에서 한 번씩 막혔다. 가장 크게 배운 건 기술적인 해결책 하나가 아니라, CDC라는 메커니즘 자체를 양방향으로 이해해야 한다는 것이었다. "소스에서 타깃으로 데이터가 흐른다"는 그림만 그리고 설계하면, 실제로는 타깃이 소스에 다시 연결하러 오는 순간 막힌다. 이번 7번의 차단 중 절반 이상이 결국 이 오해에서 비롯됐다.

Aug 11, 2026AWS
AWS DMS로 온프레미스 PostgreSQL을 RDS로, pgvector 타입까지 무손실 마이그레이션하기

AWS FinOps Agent로 비용 분석 자동화 파이프라인 구축하기

들어가며 AWS를 사용하는 프로젝트에서 비용 관리는 필수적이지만, 매일 콘솔에 접속해서 비용을 확인하고 분석하는 것은 비효율적이다. Farmily 프로젝트를 진행하며 AWS FinOps Agent를 활용해 비용 데이터 수집부터 분석, 리포트 생성, Slack 전송까지 전 과정을 자동화했다. 이 글에서는 FinOps 자동화 파이프라인을 어떻게 구축했는지, 그리고 실제로 어떤 인사이트를 얻을 수 있었는지 공유한다. 1. FinOps 자동화 파이프라인 아키텍처 전체 자동화 흐름은 다음과 같다: AWS 비용 데이터 → FinOps Agent 분석 → HTML 리포트 생성 → Slack 전송 이 흐름을 매일, 매월, 그리고 임계값 초과 시 자동으로 실행하도록 구성했다. 1.1 핵심 구성 요소 FinOps Agent AWS 비용 데이터를 조회하고 분석해서 리포트를 만들어 Slack으로 보내는 AI 에이전트다. 사람이 콘솔에 직접 들어가 확인하던 작업을 완전히 자동화했다. Automations (트리거) 에이전트가 언제 실행될지를 정의하는 자동화 트리거다. 시간 기반(CRON) 2개와 이벤트 기반(COSTANOMALY) 1개, 총 3종으로 구성했다. Memory 리포트 작성 기준을 저장해둔 파일이다. farmilyreportstandards에 다음 항목들이 저장된다: 리포트 11개 섹션 목록 데이터 수집 순서 형식 규칙 (한국어, HTML, 전송 방식) 리포트 (HTML Artifact) 에이전트가 생성하는 결과물이다. AWS Cloudscape 스타일의 인터랙티브 HTML 파일로, Slack에 파일로 첨부되어 전달된다. 2. 자동화 트리거 전략 세 가지 트리거를 통해 상황에 맞는 비용 리포트를 받을 수 있도록 설계했다. 이름 트리거 방식 실행 시점 Farmily Monthly Cost Report CRON 매월 1일 오전 9시 (KST) Daily AWS Cost Report Slack CRON 매일 오전 9시 (KST) Farmily Cost Threshold Slack Alert COSTANOMALY MTD 비용이 $50 단위 초과 시 즉시 2.1 시간 기반 트리거 (CRON) 정해진 시간에 무조건 실행되는 방식이다. EventBridge 스케줄러를 기반으로 하며, UTC 기준이기 때문에 KST(UTC+9)로 변환해서 설정했다. 매일 오전 9시: cron(0 0 ? ) 매월 1일 오전 9시: cron(0 0 1 ? ) 2.2 이벤트 기반 트리거 (COSTANOMALY) 시간과 무관하게 AWS 비용 이상 감지 이벤트가 발생하면 즉시 실행된다. $50 단위 임계값 초과를 감지해 트리거하며, "언제"가 아니라 "무슨 일이 생겼을 때" 동작한다. 예를 들어, 이번 달 누적 비용(MTD)이 $150에서 $200을 넘어가는 순간 자동으로 경고 리포트가 발송된다. 3. 에이전트 실행 흐름 자동화가 트리거되면 다음 단계를 거쳐 리포트가 생성된다. Step 1: 에이전트 실행 트리거가 발생하면 FinOps Agent가 깨어나 Memory에서 리포트 기준(farmilyreportstandards)을 로드한다. Step 2: AWS Cost Explorer API 호출 다음 순서로 비용 데이터를 순차 조회한다: 서비스별 비용 (SERVICE 차원) 리전별 비용 (REGION 차원) USAGETYPE 상세 비용 비용 예측 (Forecast) Step 3: 분석 및 리포트 생성 수집한 데이터를 바탕으로 리포트를 생성한다: 월간: 11개 섹션으로 구성된 종합 HTML 리포트 일간: 핵심 지표 요약 리포트 임계값 초과: 경고 메시지 및 원인 분석 Step 4: Slack 전송 생성된 HTML 파일을 첨부하고 한국어 요약 메시지와 함께 Slack 채널로 전송한다. 4. Memory를 활용한 일관성 유지 자동화 실행 시 에이전트는 이전 대화를 기억하지 못한다. 그래서 "어떤 형식으로 만들어라"는 기준을 Memory 파일에 저장해두면, 매번 실행될 때마다 그 기준을 읽고 동일한 품질의 리포트를 만들 수 있다. 대화에서 기준 확정 → Memory 저장 → 프롬프트에 "Memory 준수" 명시 → 실행 시마다 동일 기준 적용 이를 통해 사람이 직접 지시하지 않아도 자동화가 일관된 품질의 리포트를 생성할 수 있다. 5. 11개 섹션으로 구성된 종합 분석 프롬프트 월간 리포트는 Cloud FinOps 프레임워크를 기반으로 11개 섹션으로 구성된다. 각 섹션은 비용 최적화를 위한 구체적인 인사이트를 제공한다. 섹션 1: 현재 비용 현황 분석 전체 비용 파악 총 누적 비용: 프로젝트 시작부터 현재까지 총 지출 금액 월별 비용 추이: 최근 6개월 월별 지출 그래프 (증가/감소 추세) 일별 평균 비용: 최근 30일 기준 하루 평균 지출 예상 월간 비용: 현재 추세로 이번 달 예상 총액 서비스별 비용 분석 비용 순서대로 모든 AWS 서비스를 나열하고 각 서비스의 총 비용, 전체 비용 대비 비율, 주요 과금 요소, 비용 증가/감소 추세를 분석한다. 특히 EKS, RDS, ALB/NLB, VPC, Lambda, S3, CloudWatch, Route53, Bedrock, ECR 등 주요 서비스를 상세하게 분석한다. 리전별 비용 분석 서울 리전 (apnortheast2): Production 환경 비용 도쿄 리전 (apnortheast1): DR 환경 비용 기타 리전: useast1 (Route53 Health Check 등) 섹션 2: FinOps Big Five 분석 Rightsizing (적정 크기 조정) 컴퓨팅 리소스의 실제 사용률과 프로비저닝을 비교 분석한다. EKS Worker Nodes의 CPU/Memory 평균 사용률, RDS 인스턴스의 CPU/Memory/Connection 사용률, Lambda 함수의 메모리 설정 대비 실제 사용량을 확인한다. 과다 프로비저닝된 리소스는 다운사이징, 부족한 리소스는 업사이징을 제안한다. Scheduling (스케줄링) 상시 운영 중인 리소스를 식별하고 절감 시나리오를 제시한다. DR 환경 Warm Standby를 24/7 운영할지 주간만 운영할지, 개발/테스트 환경을 야간/주말에 중지할 수 있는지, 배치 작업을 스팟 인스턴스로 전환할 수 있는지 검토한다. Pricing Model (요금 모델) 현재 사용 중인 요금 모델을 분석하고 최적화 기회를 제시한다. OnDemand, Reserved Instances, Savings Plans, Spot Instances의 비율을 확인하고, RI/SP 전환 시 절감 효과를 1년/3년 약정으로 비교한다. Storage Tier (스토리지 계층화) S3 스토리지를 분석한다. 버킷별 용량과 접근 빈도를 확인하고, Lifecycle Policy 적용 현황을 점검한다. Standard에서 IA, 그리고 Glacier로 전환할 수 있는 기회를 찾는다. EBS/RDS 스토리지의 경우 gp3와 gp2의 비용을 비교하고, IOPS 프로비저닝 대비 실제 사용량을 확인한다. Monitoring (모니터링 최적화) CloudWatch 비용을 분석한다. 커스텀 지표 개수, 로그 보관 기간, 대시보드/알람 개수를 확인하고 최적 Retention을 제안한다. Container Insights의 필요 여부도 비용 대비 효용성 관점에서 재평가한다. 섹션 3: EKS/Container 비용 심층 분석 Kubernetes 환경의 비용을 Podlevel, Nodelevel, Data Transfer 세 가지 관점에서 분석한다. Podlevel 비용 Namespace별, Deployment별로 비용을 추정한다. farmilyapi, farmilyappai 등의 CPU/Memory Request, Limit, 실제 사용량을 비교하고 Resource Request/Limit 조정을 제안한다. Vertical Pod Autoscaler(VPA) 도입도 검토한다. Nodelevel 비용 서울 Production 환경에서는 Karpenter를 사용하는데, 노드 효율성을 평가하고 인스턴스 타입 믹스(t3, c6i, m6i 등)를 분석한다. Spot과 OnDemand의 비율도 확인한다. 도쿄 DR 환경의 경우 현재 Node Group 1노드(t3.medium)를 24/7 운영 중인데, Fargate Warm Standby로 전환하면 사용 시에만 과금되는 장점이 있다. Data Transfer 비용 인터넷 송신(Transfer Out) 비용과 리전 간 전송 비용을 분석한다. ALB에서 사용자로 가는 트래픽, ECR Pull 트래픽, RDS CrossRegion Replication(서울→도쿄) 등을 확인한다. CloudFront 도입이나 VPC Endpoint를 통한 AWS 서비스 통신으로 Transfer 비용을 절감할 수 있는지 검토한다. 섹션 4: DR 비용 최적화 Farmily는 서울도쿄 간 Warm Standby 전략을 사용 중이다. 현재 DR 전략의 비용을 평가하고 대안을 제시한다. 현재 DR 전략 평가 도쿄 EKS 노드, 도쿄 RDS Replica, 기타 리소스(ALB, 네트워크)의 월 비용을 합산하여 총 DR 비용을 산출한다. 현재 RTO 15분, RPO 1분 목표를 달성하기 위해 Warm Standby가 필요한데, 만약 RTO를 4시간으로 늘릴 수 있다면 Pilot Light로 전환하여 약 70%를 절감할 수 있다. DR 계층별 비용 비교 전략 RTO 월 비용 절감액 비고 Backup & Restore 24시간 $50 $XXX 스냅샷만 Pilot Light 14시간 $200 $XXX RDS만 대기 Warm Standby (현재) 15분 $XXX 1노드 상시 운영 ActiveActive 섹션 5: AI/ML 비용 분석 Bedrock 사용 현황 모델별(Claude, Titan 등) 사용량을 토큰 수로 파악하고, API 호출 빈도를 시간당/일간으로 확인한다. 평균 응답 토큰 수를 Input/Output 비율로 분석한다. 최적화 기회 캐싱 전략으로 반복 질문을 캐싱하거나, Prompt 최적화로 토큰 수를 줄이거나, 저렴한 모델로 전환 가능한 작업을 식별한다. Lambda 함수 비용 farmilyselectphoto, farmilycardrenderer 등의 호출 수와 실행 시간을 분석하고, 메모리 설정 대비 실제 사용량을 확인한다. Provisioned Concurrency의 필요 여부와 메모리 크기 조정을 성능 대비 비용 관점에서 검토한다. 섹션 6: 낭비 요소 탐지 유휴 리소스 Detached 상태의 EBS 볼륨, 미할당 Elastic IP(시간당 과금됨), 오래된 AMI/스냅샷, 빈 S3 버킷, 사용하지 않는 Load Balancer 등을 식별한다. 비정상 패턴 탐지 특정 날짜에 비용이 2배 이상 증가한 구간의 원인을 분석하고, 매주 특정 시간대의 과도한 Lambda 호출 같은 주기적 낭비를 식별한다. 예상보다 높은 Transfer Out은 공격이나 오류 가능성을 의미할 수 있다. 섹션 7: 실행 가능한 권장 사항 각 권장 사항마다 우선순위(High/Medium/Low), 예상 절감액(월간 $), 구현 난이도(쉬움/보통/어려움), 리스크(서비스 영향도), 실행 단계(구체적 작업 목록)를 제공한다. 예시: 권장 사항 1: RDS 인스턴스 다운사이징 우선순위: High 예상 절감: 월 $120 (연 $1,440) 난이도: 쉬움 (10분 작업) 리스크: 낮음 (CPU 사용률 15% → 30%로 증가하지만 여유 충분) 실행 단계: 1. 스냅샷 생성 (백업) 2. db.t3.medium → db.t3.small 변경 3. 1주일 모니터링 4. 성능 이상 없으면 확정 섹션 8: FinOps KPI 제시 비용 효율성 Unit Cost: 사용자 1명당 월 비용 Transaction Cost: API 요청 1만 건당 비용 Cost per Feature: 주요 기능별 비용 배분 최적화 진행도 절감 실현율: (절감 실행액 / 식별된 총 절감 기회) × 100 RI/SP 커버리지: (약정 적용 시간 / 전체 사용 시간) × 100 낭비율: (유휴 리소스 비용 / 총 비용) × 100 목표 설정 낭비율 5% 이하 RI 커버리지 70% 이상 월 비용 증가율 섹션 9: Unit Economics & 비즈니스 가치 비용 대비 가치 CAC(Customer Acquisition Cost), LTV(Lifetime Value), LTV/CAC 비율(3 이상 유지 목표)을 계산한다. 기능별 ROI 각 주요 기능의 비용 대비 비즈니스 기여도를 평가한다. AI 추천 기능은 Bedrock 비용 대비 사용자 참여도 증가로, 실시간 알림은 Lambda + SNS 비용 대비 이탈률 감소로, DR 환경은 Warm Standby 비용 대비 서비스 신뢰도 가치로 측정한다. ROI가 1 미만인 기능은 축소/제거를 검토하고, ROI가 3 이상인 기능은 투자를 확대한다. 섹션 10: 보고서 형식 Executive Summary (경영진용) 총 비용 현황을 1문장으로 요약하고, Top 3 비용 서비스를 각 1줄로 정리한다. 즉시 실행 가능한 절감 방안 Top 3를 총 절감액과 함께 제시한다. Detailed Analysis (실무진용) 서비스별 상세 분석을 표와 그래프로 제공하고, FinOps Big Five 각 항목별 권장 사항을 나열한다. 단계별 실행 계획을 30일/60일/90일로 구분하여 제안한다. Appendix 전체 리소스 목록, 비용 산출 근거, 참고 문서 링크를 첨부한다. 섹션 11: FinOps 운영 프로세스 분석 후 지속적인 운영을 위한 사이클을 제안한다. Inform (알림) 매주 비용 이상 패턴 알림 예산 대비 진행률 Optimize (최적화) 매월 권장 사항 실행 RI/SP 갱신 검토 Operate (운영) 분기별 전체 FinOps 전략 재평가 목표 KPI 조정 6. 리포트 형식을 HTML로 선택한 이유 여러 형식을 비교한 결과, HTML을 선택했다. 형식 특징 텍스트 메시지 간단하지만 표나 차트 표현 불가 PDF 정적이라 인터랙션 없음 HTML 테이블 정렬, 탭 전환, 차트 hover 등 인터랙티브 가능 Slack에 HTML 파일로 첨부하면 팀원들이 다운로드해서 브라우저로 열어볼 수 있다. 데이터를 직접 탐색하면서 필요한 정보를 찾을 수 있다는 점이 큰 장점이다. 7. 실제 리포트 결과 2026년 6월 월간 리포트를 통해 얻은 주요 인사이트다. 비용 현황 6월 총 비용과 전월 대비 증가율 확인 주요 비용 서비스: EKS, RDS, Bedrock 순 즉시 실행 가능한 최적화 기회 1. RDS 다운사이징을 통한 월 절감 2. DR 환경 Pilot Light 전환 검토 3. CloudWatch 로그 보관 기간 조정 FinOps KPI 낭비율, RI 커버리지, 사용자 1명당 월 비용 등의 지표를 대시보드로 확인 8. 임시 리포트 요청 방법 자동화 외에 임시로 리포트가 필요할 때는 에이전트에게 직접 요청하면 된다. "이번 달 리포트 만들어줘" 에이전트가 Memory에서 기준을 읽어 자동화와 동일한 형식으로 만들어준다: "이번 달 리포트 만들어줘" → Memory 로드 → 데이터 수집 → HTML 생성 → Slack 전송 9. 얻은 교훈 성공 요인 Memory를 활용한 일관성 유지 자동화마다 동일한 품질의 리포트를 생성할 수 있었다. 프롬프트 버전 관리도 용이했다. 3가지 트리거 전략 정기 리포트(월간/일간)와 이상 감지를 조합해서 완벽한 커버리지를 확보했다. 비용 급증 시 즉시 대응할 수 있었다. HTML 인터랙티브 리포트 팀원들이 직접 탐색하면서 인사이트를 발견할 수 있었다. 표 정렬, 차트 hover 등의 UX가 큰 도움이 되었다. 마치며 AWS FinOps Agent를 활용한 비용 분석 자동화로 다음과 같은 효과를 얻었다. 먼저 시간을 크게 절감했다. 매일 30분씩 소요되던 비용 확인 작업을 완전히 자동화했다. 인사이트도 향상되었다. 11개 섹션의 체계적 분석을 통해 놓치고 있던 최적화 기회를 발견할 수 있었다. 비용 임계값 초과 시 실시간 알림을 받아 빠르게 대응할 수 있게 되었다. 특히 Cloud FinOps 프레임워크 기반의 체계적인 11개 섹션 분석을 통해 단순 비용 확인을 넘어 실행 가능한 최적화 방안을 도출할 수 있었다. Rightsizing, Scheduling, Pricing Model, Storage Tier, Monitoring 등 FinOps Big Five를 기반으로 한 분석은 놓치기 쉬운 절감 기회를 발견하는 데 큰 도움이 되었다.

Jul 22, 2026AWS
AWS FinOps Agent로 비용 분석 자동화 파이프라인 구축하기

인프라와 데이터 보호 (feat. AWS Technical Essentials)

Amazon VPC 구성요소 ELB는 수신되는 애플리케이션 트래픽을 Amazon EC2 인스턴스, 컨테이너, IP 주소, AWS Lambda 함수 등 여러 대상에 자동으로 분산한다. ELB는 단일 가용 영역 또는 다중 가용 영역에서 다양한 애플리케이션 트래픽 로드를 처리할 수 있다. ELB는 VPC와 연동되어 통합 인증서 관리, 사용자 인증 및 보안 소켓 계 (SSL)/전송 계층 보안(TLS) 복호화를 비롯한 강력한 보안 기능을 제공한다. 이 두 서비스의 결합으로 TLS 설정을 중앙에서 관리하고 애플리케이션에서 CPU 집약적인 워크로드를 오프로드할 수 있는 유연성이 제공된다. AWS ELB(Elastic Load Balancing) 네 가지 로드 밸런서 유형 지원 내결함성 개선 인스턴스의 상태 확인 수행 하나 또는 여러 가용 영역에서 작동 시간에 따른 트래픽 변화에 맞게 크기 조정 가능 ELB는 수신되는 애플리케이션 트래픽을 EC2 인스턴스, 컨테이너, IP 주소 등 여러 대상으로 자동 분산시킨다. 단일 가용 영역 또는 여러 가용 영역에서 애플리케이션 트래픽의 다양한 로드를 처리할 수 있다. 이렇게 하면 애플리케이션의 내결함성이 향상된다. 애플리케이션에 대한 요청의 전체적인 흐름을 방해하지 않고 필요에 따라 로드 밸런서에서 인스턴스를 추가 및 제거할 수 있다. ELB는 시간이 흐르면서 애플리케이션에 대한 트래픽이 변화함에 따라 로드 밸런싱 디바이스를 확장하거나 축소하며, 대다수의 워크로드에 맞춰 자동으로 확장할 수 있다. 로드 밸런서가 비정상 대상을 탐지하면, 해당 대상으로의 트래픽 라우팅을 중단한 후 대상이 정상인 것이 다시 탐지될 때 해당 대상으로의 트래픽 라우팅을 재개한다. VPC 보안 기능 VPC 보안 기능으로 서브넷 라우팅, 네트워크 ACL(NACL), 보안 그룹(Security group)을 제공한다. 여기서 퀴즈, NACL과 Securyti group 중 IP를 막는데 즉각적이고, 효율적인 방식은 뭘까? NACL은 deny(거부) 룰을 추가할 수 있지만 Security group은 오직 allow(허용) 정책만 추가할 수 있다. 위 그림에서 살펴봤듯이 크기(범위)는 VPC NACL(서브넷) 보안 그룹(인스턴스) 순이다. 서브넷 및 라우팅 테이블 처음에는 기본 라우팅 테이블에 단일 경로 즉, VPC 내에서 통신할 수 있게 해주는 로컬 경로만 포함되어 있다. 이 로컬 경로는 VPC 서브넷용이다. 이 슬라이드의 예제에서 VPC 네트워크는 10.0.0.0/16이므로 '로컬' 트래픽에 대한 각 라우팅 테이블의 네트워크 Classless InterDomain Routing(CIDR) 항목이다. 인스턴스가 VPC 서브넷 내에 있으므로 VPC에서 인스턴스를 시작할 때마다 로컬 경로가 해당 인스턴스를 자동으로 처리한다. 기본 라우팅 테이블은 다른 라우팅 테이블과 명시적으로 연결되지 않은 모든 서브넷의 라우팅을 제어한다. 기본 경로 테이블에서 경로를 추가, 제거, 수정할 수는 있지만 기본 경로 테이블을 삭제할 수는 없다. VPC를 보호하는 한 가지 방법은 기본 라우팅 테이블을 원래 기본 상태(VPC용 로컬 경로만 있는) 그대로 두고, 새로 생성하는 각 서브넷을 사용자가 생성하는 사용자 정의 라우팅 테이블과 명시적으로 연결하는 것이다. 이를 통해 각 서브넷의 아웃바운드 트래픽이 라우팅되는 방법을 명시적으로 제어할 수 있다. 프라이빗 서브넷 및 인터넷 인터넷에서 리소스를 완전히 격리하는 경우에는 단점이 있다. EC2 인스턴스의 소프트웨어 업데이트 다운로드를 허용하려는 용도로만 인터넷 액세스를 사용하려는 경우를 예로 들 수 있다. 이 경우 또는 프라이빗 서브넷에 있는 리소스가 인터넷에 액세스하도록 선택적으로 허용하려는 경우에는 Network Address Translation(NAT) 디바이스를 사용할 수 있다. 그럼 프라이빗 서브넷은 인터넷에 액세스할 수 있지만, 인터넷에서 인스턴스와의 연결을 시작할 수 없다. NAT 게이트웨이가 NAT 인스턴스보다 우수한 가용성 및 대역폭을 제공하므로 NAT 게이트웨이를 사용하는 것이 좋다. 네트워크 ACL (NACL) VPC 보안을 강화하도록 필터링 계층을 추가하려면 네트워크 ACL을 구성할 수 있다. 네트워크 액세스 제어 목록(ACL)은 서브넷 수준에서 트래픽을 제어하기 위한 방화벽 역할을 하는 VPC의 선택적 보안 계층이다. VPC에는 수정 가능한 기본 네트워크 ACL이 제공된다. 이 ACL은 기본적으로 모든 인바운드, 아웃바운드 IP 트래픽을 허용한다. 트래픽을 제어하려면 특정 ip 주소나 네트워크, 프로토콜 및 포트에 대한 허용 및 거부 규칙을 추가해야 한다. 모든 규칙은 순서대로 처리되므로 규칙 구성 순서가 중요하다. 규칙 번호의 범위는 132766이다. 10, 20, 30과 같이 일정하게 증가하는 규칙 번호를 지정하는 것이 좋다. 번호가 가장 낮은 규칙이 먼저 평가된다. 네트워크 ACL(NACL)은 stateless이므로 인바운드 트래픽과 아웃바운드 트래픽을 모두 처리해야 한다. stateless : 상태 비저장 (과거 기억 x) 보안 그룹 보안 그룹은 인스턴스에 대한 인바운드 및 아웃바운드 트래픽을 제어하는 가상 방화벽 역할을 한다. VPC에서 인스턴스를 시작할 경우 인스턴스의 보안 그룹을 지정해야 한다. 보안 그룹을 생성할 때 인바운드 트래픽을 제어하는 규칙 세트 하나와 아웃바운드 트래픽을 제어하는 별도의 규칙 세트를 인스턴스에 추가한다. but, 보안 그룹은 stateful이므로 아웃바운드 측의 반환 트래픽은 자동으로 허용된다. 허용 규칙만 사용 가능하므로 명시적으로 혀용하지 않는 모든 항목은 거부된다. 심층 방어를 사용하는 데이터 흐름 엣지 보안 심층 방어 모델과 AWS WellArchitected Framework 보안 원칙의 설계 방침을 활용하여 AWS 워크로드를 보호하는 것 외에도 엣지에도 특정 보호 기능을 적용할 수 있다. 엣지에서의 보호란 VPC, AWS 엣지 서비스 그리고 디바이스 또는 애플리케이션 내의 오리진 인프라를 보호하는 것이다. 엣지 보호에서는 보통 콘텐츠 전송 보호(예: 웹 애플리케이션), AWS 환경에서 실행되는 애플리케이션과 네트워크에 대한 보호, 분산 서비스 거부(DDoS) 공격 완화 등을 수행한다. AWS WAF 웹 애플리케이션을 대상으로 한 악성 웹 요청을 감지 및 차단 웹 트래픽 필터링 기능 제공 실시간 지표 제공 애플리케이션 계층 보호 기능 제공 통합 대상: Amazon CloudFront Amazon API Gateway Application Load Balancers Amazon Cognito User Pools AWS AppSync AWS WAF는 웹 애플리케이션을 대상으로 한 악성 웹 요청을 탐지 및 차단하는 웹 애플리케이션 방화벽이다. AWS WAF는 커스텀 또는 서트 파티 제공하고 IP 주소, 지리적 위치 등에 대한 세부 정보를 포함하는 원시 요청을 캡처한다. AWS WAF는 실시간 지표를 제공하고 IP 주소, 지리적 위치 등에 대한 세부 정보를 포함하는 원시 요청을 캡처한다. AWS WAF는 Amazon CloudFront, Amazon API Gateway 및 Application Load Balancer와 긴밀하게 통합된다. 쉽게 말해, 개발자가 블랙리스트 명단을 직접 만들어서 적용하거나 전문 보안 업체에서 만들어둔 전문가용 블랙리스트를 구독해서 사용한다는 것이다. 또한 사용자가 웹사이트에 접속할 때 보내는 가공되지 않은 실제 데이터 덩어리를 IP 주소, 지리적 위치, HTTP 헤더/바디 등 정보를 낱낱이 뒤져본다는 것이다. AWS WAF의 작동 방식 웹 ACL을 생성하고 규칙을 추가하여 보호 전략을 정의할 수 있다. 규칙은 웹 요청을 검사하기 위한 기준, 그리고 이 기준과 일치하는 요청을 처리하는 방법을 정의한다. 웹 ACL을 생성한 후에는 하나 이상의 AWS 리소스와 연결할 수 있다. AWS WAF 웹 ACL을 사용하여 보호할 수 있는 리소스 유형은 Amazon CloudFront 배포, Amazon API Gateway 애플리케이션 프로그래밍 인터페이스(API) 및 Application Load Balancer이다. 웹 ACL과 개별 규칙을 사용하면, 웹 요청을 차단할지 아니면 허용할지를 지정할 수 있다. AWS WAF는 웹 ACL에 나열되는 순서대로 규칙을 평가한다. 그런데 트래픽이 구성된 어떤 규칙의 기준도 충족하지 않으면 어떻게 될까? 규칙을 설정할 때는 ACL의 기본 작업을 설정한다. 이 작업은 구성된 어떤 규칙과도 일치하지 않는 요청을 허용할지 아니면 차단할지를 나타낸다. 다른 규칙이 일치하지 않으면 해당 트래픽에는 기본 작업이 적용된다. 또한 AWS WAF에는 웹 사이트가 어떻게 보호되고 있는지 확인할 수 있는 2가지 방법이 있다. Amazon CloudWatch에서 제공하는 1분 지표와 AWS WAF API 또는 관리 콘솔에서 제공하는 샘플링된 웹 요청을 확인하면 된다. 이를 통해 차단, 허용 또는 수가 계산된 요청 및 지정된 요청과 일치한 규칙을 확인할 수 있다. 예를 들어 IP 주소 조건으로 인해 차단된 웹 요청 등을 확인할 수 있다. AWS WAF 규칙 및 규칙 그룹 여기에 표시된 웹 ACL에는 차단(거부 목록) 또는 허용(허용 목록)할 IP 주소를 수동으로 삽입할 수 있게 해주는 사용자 정의 규칙 2개가 포함되어 있다. 또한 두 개의 규칙으로 구성된 관리형 규칙 그룹도 포함되어 있다. HTTP flood 규칙은 특정 IP 주소에서 발생하는 다량의 요청으로 구성된 공격(예: 웹 계층 DDoS 공격 또는 브루트 포스 로그인 시도)을 차단한다. SQL 명령어 삽입 규칙은 요청의 Uniform Resource Identifier(URI), 쿼리 문자열 또는 본문에서 일반적인 SQL 명령어 주입 패턴을 차단하도록 설계되었다. 규칙 그룹과 웹 ACL 모두 규칙을 포함하는데 이 규칙은 두 위치에서 동일한 방식으로 정의되지만 다음과 같은 차이가 있다. 각 웹 ACL에 규칙 그룹 참조 스테이트먼트를 추가하여 여러 웹 ACL에서 단일 규칙 그룹을 재사용할 수 있다. 웹 ACL은 재사용할 수 없다. 규칙 그룹에는 기본 동작이 없다. 웹 ACL에서는 포함하는 각 규칙 또는 규칙 그룹에 대해 기본 작업을 설정한다. 규칙 그룹 또는 웹 ACL 내의 개별 규칙에는 정의된 작업이 있다. 규칙 그룹을 AWS 리소스와 직접 연결하지는 않는다. 규칙 그룹을 사용하여 리소스를 보호하려면 웹 ACL에서 규칙 그룹을 사용해야 한다. DDoS 완화 분산 서비스 거부(DDoS) 공격은 매우 흔히 발생하는 공격 유형이지만 대상 AWS 서비스 조합을 사용해 심층 방어 전략을 구현하면 차단할 수 있다. AWS 서비스에는 기본적인 DDoS 방지 기능이 표준 기능으로 포함되어 있다. CloudFront, Application Load Balancer, Network Load Balancer, Global Acclerator, 탄력적 IP 주소 또는 Amazon Route 53는 네트워크 및 전송 계층 공격을 대상으로 하는 기본 DDoS 방지 기능이 제공된다. AWS 글로벌 인프라는 DDoS 복원력을 고려하여 설계되어 있다. AWS 글로벌 인프라에는 초과 트래픽을 자동으로 탐지하고 필터링할 수 있는 DDoS 완화 시스템이 포함되어 있다. AWS 글로벌 인프라에서 제공하는 확장성이 뛰어난 다양한 인터넷 연결에 액세스 하면 가용성에 미치는 영향은 최소화하면서 DDoS 공격을 흡수하고 장애를 격리하는 능력을 크게 높일 수 있다. AWS 엣지 로케이션은 웹 애플리케이션에 이러한 이점을 제공하는 추가 네트워크 인프라 계층을 제공한다. AWS Shield 상시 작동하는 네트워크 흐름 모니터링 및 AWS 서비스로 들어오는 트래픽 검사 흔히 발생하는 DDoS 공격으로부터 보호 AWS WAF와 통합하여 즉각적인 규칙 업데이트 및 앱 취약성 방지 기능 제공 AWS Shield는 DDoS 공격을 방지할 수 있도록 구축된 관리형 서비스이다. AWS Shield는 애플리케이션 가동 중지 및 지연 시간을 최소화하는 상시 탐지 및 자동 인라인 완화 기능을 제공한다. 또한 AWS에서 실행되는 웹 애플리케이션을 가장 일반적이고 빈번히 발생하는 인프라 계층 공격(예: UDP flood)과 상태 고갈 공격(예: TCP SYN flood)으로부터 자동으로 보호한다. Shield와 AWS WAF를 함께 사용하면 HTTP POST, GET flood와 같은 애플리케이션 계층 공격을 방어할 수 있으므로 DDoS 보호를 강화할 수 있다. 추가 인프라 완화 서비스 컴퓨팅 리소스 보호 Amazon Machine Image(AMI)는 EC2 인스턴스를 시작하는 데 필요한 정보를 제공한다. 또한 소프트웨어와 함께 인스턴스를 부팅하는 데 필요한 모든 정보(예: 운영체제와 소프트웨어→Linux, Apache, 웹 사이트)가 포함되어 있다. Amazon Inspector 자동화된 리소스 검색 기능 제공 EC2 인스턴스 및 Amazon Elastic Container Registry(Amazon ECR) 및 AWS Lambda 워크로드에서 자동화된 연속 보안 평가 실행 광범위하게 배포된 AWS System Manager 에이전트(SSM 에이전트)를 스캔에 사용 AWS Organizations, AWS Security Hub 및 AWS EventBridge와 통합 보안 표준 및 모범 사례 적용 지원 Amazon Inspector를 사용하면 리소스를 배포되기 전과 프로덕션 환경에서 실행되는 동안 모두 보안 취약성 및 보안 모범 사례와의 차이점을 파악할 수 있다. 애플리케이션에 대한 표준 및 모범 사례를 정의하고 이러한 표준을 준수하는지 검증하여 보안 문제가 프로덕션 애플리케이션에 영향을 끼치기 전에 해당 문제를 미리 관리할 수 있다. Amazon Inspector는 널리 배포된 AWS Systems Manager 에이전트(SSM 에이전트)를 사용하여 실행 중인 모든 EC2 인스턴스, Amazon Elastic 컨테이너 레지스트리(Amazon ECR) 리포지토리, AWS Lambda 함수를 자동으로 검색하고 취약점을 검사한다. Amazon Inspector를 사용하는 샘플 워크플로 추가 컴퓨팅 보호 서비스 AWS Systems Managers는 AWS 인프라에 대한 가시성과 제어를 제공한다. 여기에는 관리 작업을 자동화할 수 있는 기능이 포함되어 있다. 이러한 작업에는 시스템 인벤토리 수집, 운영 체제 패치 적용, 최신 바이러스 백신 정의 유지 관리, 대규모로 운영 체제 및 애플리케이션 구성 등이 있다.

Jul 16, 2026Study
인프라와 데이터 보호 (feat. AWS Technical Essentials)

Multi-AZ RDS failover 테스트 검증기 (feat.AWS FIS)

MultiAZ RDS failover 테스트 검증기 (feat.AWS FIS) AWS FIS로 운영 DB에 강제 failover를 일으켜, "MultiAZ가 정말 장애를 견디는가"를 숫자로 확인한 기록. 결론부터: DB는 15초 만에 살아났는데, 앱은 15분 동안 못 붙었다. 들어가며 RDS를 MultiAZ로 켜두면 한 가용영역(AZ)이 죽어도 다른 AZ가 받아준다고 한다. 콘솔에서 체크박스 하나 켜는 걸로 "장애 대응 완료"라고 믿기 쉽다. 그런데 정말 그런가? 켜 두는 것과 실제로 견디는 것은 다른 이야기다. 그래서 직접 깨뜨려보기로 했다. AWS FIS(Fault Injection Simulator)로 운영 중인 RDS Primary를 강제로 재부팅시켜 failover를 일으키고, 그 과정을 Grafana로 초 단위로 관측했다. 두 번에 걸쳐 진행했는데, 1차에서 예상 못 한 게 터졌고 2차에서 그 원인을 끝까지 파고들었다. 이 글의 한 문장 요약은 이거다 — DB 가용성과 서비스 가용성은 같은 숫자가 아니다. 0. 먼저 개념부터 본문에 계속 나오는 용어 다섯 개만 잡고 가자. FIS (Fault Injection Simulator) — AWS의 장애 주입 도구. "이 자원에 이런 장애를 일으켜라"를 템플릿으로 정의해 일부러 장애를 내고 시스템 반응을 관측한다. 카오스 엔지니어링의 실행 도구다. MultiAZ Failover — RDS를 두 AZ에 Primary·Standby로 이중화한 구성. Primary가 죽으면 다른 AZ의 Standby가 새 Primary로 승격된다. 평소 동기 복제라 데이터 손실이 없다. RTO (Recovery Time Objective) — 장애부터 복구까지 허용하는 목표 시간. 어느 계층을 기준으로 재느냐(DB냐 앱이냐)에 따라 값이 완전히 달라진다 — 이 글의 핵심. HikariCP (커넥션 풀) — Spring Boot 기본 DB 커넥션 풀. 요청마다 연결을 새로 만들지 않고 미리 만든 연결을 재사용한다. failover로 Primary가 바뀌면 풀에 있던 연결은 옛 Primary를 바라보는 죽은 연결이 된다. 이걸 얼마나 빨리 버리고 새로 만드냐가 앱 복구 속도를 가른다. warm pool — 방금 새 연결로 통째로 갈려서 전부 "신선한" 상태의 풀. 반대는 오래 묵은 연결로 찬 cold pool. (뒤에서 함정으로 다시 등장한다.) 1. 무엇을 테스트했나 구성 내용 대상 DB prodrds · PostgreSQL 18.3 · db.t3.small · MultiAZ = true 앱 계층 ECS · Spring Boot · HikariCP 트래픽 ALB → Target Group → ECS 안전장치 Stop Condition = CloudWatch 5xx 알람 관측 Grafana(AMG) + RDS Recent events 합격 기준은 세 가지로 잡았다. 1. DB RTO Primary(2c)가 재부팅되면 Standby(2a)가 새 Primary로 승격된다. 문제는 오른쪽 — DB는 2a로 옮겨갔는데 앱의 커넥션 풀은 여전히 옛 Primary(2c)를 붙들고 있다. 2. 1차: 일단은 평온했다, 5xx가 터지기 전까진 FIS 액션 aws:rds:rebootdbinstances에 forceFailover=true를 줘서 Primary를 강제로 재부팅했다. "Primary가 갑자기 죽는" 상황을 인위적으로 만든 것이다. 시작 직전 대시보드는 모든 게 정상이었다. DB 커넥션 10개 안정, 5xx 0건, p99 10.7ms. 그림 1. 1차 — 시작 전 정상 기준선. DB 커넥션 10 · 정상 타깃 1 · 앱 5xx 0 · p99 10.7ms · DB CPU 3.99%. 이후 그림과 비교할 'before' 상태. 그림 2. 1차 — 장애 직전 상세. 요청 수·활성 커넥션·ECS 태스크 running 1 · Failover 5xx 누적 0. 아직 모든 지표 정상. 그런데 failover를 건 직후, 5xx가 폭증하기 시작했다. 그림 3. 1차 — 장애 정점. 요청 1.5K 스파이크, Failover 5xx 누적 280건(대부분 앱 target 5xx). DB와 ECS는 멀쩡히 살아있는데(태스크 running 1) 앱이 DB에 못 붙어 5xx가 터졌다. 이 한 장이 'DB 가용 ≠ 서비스 가용'을 그대로 보여준다. 이상했다. DB CPU도, 메모리도, ECS 태스크도 전부 건강했다. 그런데 앱은 5xx를 쏟아냈다. 전환 구간을 들여다보니 DB 커넥션이 10에서 5로 빠지고 p99가 396ms까지 치솟았다. 그림 4. 1차 — 전환 중. DB 커넥션 10→5 감소, 읽기/쓰기 지연 스파이크와 함께 p99가 396ms(p50/p99 패널은 2초까지)로 상승. 앱이 옛 Primary의 죽은 커넥션을 붙들고 새 Primary로 못 옮겨가는 구간. 그리고 한참 뒤에야 앱이 회복됐다. 그림 5. 1차 — 종료 후 회복. DB 커넥션 0→10 복귀, p99가 30초(커넥션 대기 천장)를 찍었다가 5.02ms로 정상화. 5xx 막대는 16:0316:04에 잡힌다. DB는 진작(15:50) 복구됐는데 앱 재연결은 16:05. 체감 RTO ≈ 15분의 끝. 정리하면 1차의 숫자는 이렇게 갈렸다. 지표 값 DB RTO (인프라) 약 1분 — 왜 1차가 실패했나? failover 직후 RDS가 Standby를 다시 동기화(재동기화)하는 시간을 기다리지 않고 곧바로 다시 실행했기 때문이다. 이게 2차에서 검증할 가설이 됐다. 3. 2차: 이번엔 기다렸다, 그리고 회복을 관측했다 2차는 단 하나만 바꿨다 — failover 후 RDS 동기화가 완료될 때까지 기다렸다가 다음 회차를 실행했다. 설정은 한 줄도 안 건드렸다. 그렇게 18:45 / 18:56 / 19:16, 세 번 깨끗하게 때렸다. 구분 1차 2차 실행 방식 동기화 완료 전 곧바로 재실행 동기화 완료까지 대기 후 실행 실행 횟수 2회 3회 결과 실패 — 전환 도중 앱 오류 폭증 정상 — 매 회차 깨끗하게 전환·관측 매 회차 깨끗하게 전환됐다. DB 연결 수 그래프를 보면 failover 시점에 커넥션이 잠깐 푹 꺼졌다가 다시 차오르는 게 그대로 보인다. 그림 6. 2차 1회차(18:45) — p99 2.21ms · DB CPU 5.02% · 정상 타깃 1. DB 연결 30 → failover 시점 일시 감소 → 30 복귀. 동기화 완료를 기다린 뒤 실행하니 깨끗이 전환됐다. 그림 7. 2차 2회차(18:56) — p99 2.39ms · DB CPU 5.41%. 같은 패턴이지만 앱 커넥션 풀 회복이 약 2분으로 빨라졌다. 그림 8. 2차 3회차(19:16) — p99 2.62ms. 읽기/쓰기 지연 스파이크가 짧게 여러 번 났다 회복. 요청 700 스파이크도 잠깐 보인다 — 요청 스레드가 죽은 커넥션을 즉시 일괄 폐기해 앱 회복이 약 20초까지 빨라졌다. DB 자체 복구는 RDS 이벤트 로그로 실측했다. 그림 9. 2차 — RDS Recent events. 18:45·18:56·19:16 세 번 모두 MultiAZ failover started → DB instance restarted → failover completed. DB 자체 복구 15초·전체 3045초의 실측 근거. (위 6/11 23:03·23:07은 정기 백업.) [2차 — RDS MultiAZ failover events] 1회차 18:45:02 started → 18:45:18 restarted → 18:45:31 completed (AZ 2c→2a) 2회차 18:56:17 started → 18:56:32 restarted → 18:57:01 completed (AZ 2a→2c) 3회차 19:16:27 started → 19:16:42 restarted → 19:17:01 completed (AZ 2c→2a) 3회 측정을 표로 모으면 이렇다. 지표 1회차 2회차 3회차 평균 DB 자체 복구 16초 15초 15초 15.3초 전체 완료 30초 45초 35초 36.7초 앱 커넥션 풀 복구 5분 2분 20초 — ECS 태스크 교체 없음 없음 없음 — 여기서 눈에 띄는 줄이 앱 커넥션 풀 복구: 5분 → 2분 → 20초다. 설정은 동일했는데 왜 점점 빨라졌을까? 4. 핵심 분석 — DB는 15초, 앱은 15분, 그 사이 14분의 사각 같은 failover 한 번을 두 계층이 전혀 다른 시간으로 겪었다. 그림으로 보면 이렇다. 시간축 0 15초 15분 │ │ │ DB 계층 ──────● (복구 15초) │ 앱 계층 ──────○━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● failover ↑ DB는 살아있으나 앱이 못 붙는 구간 ≈ 14분 ↑ 앱 복구 '가용성'을 DB 한 층으로만 재면 저 가운데 14분이 통째로 가려진다. 사용자가 실제로 겪는 시간은 가장 느린 계층, 즉 앱이 결정한다. 왜 앱이 늦게 붙었나 — HikariCP의 죽은 커넥션 failover로 Primary가 바뀌면, 풀 안의 연결들은 전부 옛 Primary를 향하는 죽은 연결이 된다. 문제는 이 죽은 연결을 언제 감지해서 새 연결로 갈아끼우느냐다. 1회차 (5분) — 죽은 커넥션 감지 후, housekeeper 스레드가 10초에 1개씩 검증·폐기했다. 풀 20개를 다 갈아끼우는 데 5분. 로그엔 'Thread starvation or clock leap detected'가 동반됐다. 2회차 (2분) — 같은 패턴이지만, 직전에 한 번 갈린 풀이라 수명 짧은 연결이 섞여 더 빨리 교체됐다. 3회차 (20초) — 요청 스레드가 즉시 감지해 0.5초 안에 풀 전체를 일괄 폐기했다(housekeeper 10초 대기 없이). 단, 직후 새 연결 생성이 몰리며 'request timed out after 30000ms'가 짧게 났다 정상화됐다. 회차가 빨라진 건 '개선'이 아니다 — warm pool 함정 여기가 제일 중요하다. 5분 → 20초는 내가 뭘 고쳐서가 아니다. 한 번 failover를 겪으면 풀이 통째로 새 연결로 갈린다. 그 직후 회차는 "방금 만들어 수명이 거의 0인" 신선한 연결만 남은 상태(warm pool)가 되고, 그 덕에 우연히 빨라진 것뿐이다. 문제는 실제 운영 failover는 며칠몇 주 멀쩡히 돌던 cold pool에서 딱 한 번 터진다는 것. 즉 현실은 늘 '1회차(5분)' 상황이지 2·3회차(빠름)가 아니다. 첫 회차가 5분이나 걸린 직접 원인은 maxLifetime 기본값이 30분이라 housekeeper가 죽은 연결을 천천히 교체했기 때문이다. 참고로 "warm pool"은 EC2 Auto Scaling의 Warm Pools와는 완전히 다른 개념이다. 여기선 "커넥션 풀이 한 번 갈려서 신선해진 상태"를 가리킨다. 5. 판정과 개선 과제 AWS 기대치 대비로 보면 인프라는 전부 통과, 앱만 숙제로 남았다. 지표 결과 AWS 기대치 판정 :: DB 자체 복구 1516초 30초 이내 양호 전체 Failover 완료 3045초 60초 이내 양호 데이터 손실 없음 (동기복제) 무손실 양호 ECS 태스크 생존 전 회차 교체 없음 — 양호 앱 에러 구간 최대 5분 · 체감 15분(1차) — 개선 필요 개선 1순위는 HikariCP 튜닝이다. 조치 기대 효과 :: 1 maxlifetime = 60000 (60초) 커넥션 수명 단축 → failover 시 빠른 교체 2 connectiontimeout = 5000 (5초) 새 연결 실패 시 빠른 에러 반환 3 keepalivetime = 30000 (30초) 주기적 alive 체크 → 죽은 연결 조기 발견 4 validationtimeout = 3000 (3초) validate 빠른 실패 처리 5 ECS min 태스크 2개 이상 1개가 죽어도 다른 태스크가 커버 그 외에 1차에서 도출한 개선들: Deep health check — ALB 헬스체크가 DB 연결까지 점검하게. Healthy=1 맹점을 없애 죽은 경로로 트래픽 보내는 걸 차단. 알람 기준 보강 — Healthy 수가 아니라 5xx·DB 활성 커넥션 수 기준으로. FIS 템플릿에 aws:fis:wait 추가 — reboot는 13초 fireandexit라 정작 장애 구간엔 실험이 끝나 Stop Condition이 무력화된다. wait로 실험 수명을 복구 구간까지 늘려야 한다. 마치며 이 실험의 목적은 "MultiAZ가 장애를 견디는가"를 추측이 아니라 숫자로 확인하는 것이었고, 결과는 두 계층으로 뚜렷이 갈렸다. 검증된 것 — MultiAZ failover 메커니즘 자체는 정상이다. DB 복구 1516초, 데이터 무손실, AZ 자동 전환, ECS 태스크 생존. 인프라 회복력은 합격. 발견한 것 — 서비스 RTO를 좌우하는 건 DB가 아니라 앱이었다. HikariCP가 죽은 연결을 늦게 교체해 1차 체감 복구가 15분, 5xx 280건. 그 와중에 ALB 헬스체크는 DB 단절을 못 봤다. 운영 교훈 — failover 직후 RDS 재동기화가 끝나기 전에 재실행하면 안 된다(1차 실패 원인). 그리고 풀 회복이 빨라 보인 건 warm pool 효과일 뿐, 실제 운영은 늘 '5분' 상황이라 튜닝이 전제다. 가장 크게 남은 한 줄은 이거다. 서비스 가용성은 가장 빠른 계층이 아니라 가장 느린 계층이 결정한다. DB가 15초 만에 살아나도 앱이 14분을 못 붙으면, 사용자에게 그 서비스는 14분간 죽어 있던 것이다. 'MultiAZ를 켜 두는 것'과 '장애를 실제로 견디는 것'의 차이가 바로 여기에 있다. 이게 카오스 엔지니어링을 직접 해보기 전엔 안 보이던 사각지대였다. 체크박스를 켜는 것과, 깨뜨려서 숫자를 보는 것은 정말로 달랐다.

Jun 18, 2026AWS
Multi-AZ RDS failover 테스트 검증기 (feat.AWS FIS)

Terraform로 다루는 클라우드 인프라

Terraform로 다루는 클라우드 인프라 코드로 인프라를 선언하고, 한 번의 명령으로 만들고 지우는 법 — 그리고 프로젝트에 적용된 내용 목차 1. Terraform이란 무엇인가 2. AWS CLI와 무엇이 다른가 3. 장점과 단점 4. 기본 문법 한눈에 5. 꼭 쓰는 명령어 6. State 관리 — Remote Backend 7. 팀 협업 방식 8. 우리 프로젝트 구조 9. 실제 정의된 AWS 리소스 10. 실제 코드 예시 11. 전체 아키텍처 12. [주의사항] 1. Terraform이란 무엇인가 HashiCorp가 만든 Infrastructure as Code(IaC) 도구다. 한 줄로 풀면, 클라우드 인프라를 코드로 적어두고 명령어 한 번으로 만들고·고치고·지우는 도구다. 콘솔에서 마우스로 클릭하던 작업을 텍스트 파일(.tf)로 옮긴다고 보면 된다. 코드 작성 → plan(미리보기) → apply(실제 반영) → 인프라 완성 먼저 잡고 갈 세 가지 개념 선언형 (Declarative) — "어떻게 만들어라"가 아니라 "최종 상태가 이래야 한다"고 적는다. 그러면 Terraform이 현재 상태와 비교해서 달라진 부분만 알아서 반영한다. State 파일 — 지금 인프라가 어떤 상태인지 추적하는 JSON 파일이다(terraform.tfstate). Terraform이 "내가 뭘 만들어놨는지" 기억하는 장부라고 보면 된다. Provider — AWS·GCP·Azure 같은 클라우드 API에 연결해주는 플러그인이다. "AWS를 쓰겠다"고 선언하면 해당 provider가 실제 API 호출을 담당한다. 2. AWS CLI와 무엇이 다른가 둘 다 AWS를 다루지만 접근 방식이 정반대다. CLI는 명령형(하나씩 시킨다), Terraform은 선언형(원하는 결과를 적는다). 항목 AWS CLI Terraform 방식 명령형 — 하나씩 실행 선언형 — 최종 상태 정의 예시 aws ec2 runinstances … resource "awsinstance" "web" {…} 상태 추적 ✗ 없음 · 직접 확인 ✓ state 파일로 자동 추적 변경 미리보기 ✗ 불가 ✓ terraform plan 롤백 수동으로 원복 코드 되돌리고 apply 재현성 스크립트 따로 작성 코드 자체가 곧 문서 의존성 순서를 직접 관리 의존성 그래프 자동 생성 삭제 리소스 하나씩 terraform destroy 한 번 언제 뭘 쓰나 AWS CLI — 빠른 조회, 디버깅, 한 번 쓰고 마는 작업. Terraform — 인프라를 짓고 관리할 때, 환경을 복제할 때, 여럿이 협업할 때. 3. 장점과 단점 좋기만 한 도구는 없다. 도입 전에 트레이드오프를 알고 가는 게 중요하다. 장점 인프라를 코드로 버전 관리 — Git으로 변경 이력 추적 plan으로 적용 전 변경사항 확인 → 실수 방지 같은 코드로 dev/prod 환경 복제 모듈화로 재사용 — VPC 모듈 한 번 만들면 dev·prod 공유 멀티 클라우드 — AWS·GCP·Azure 동일 문법 의존성 자동 해결 — 서브넷 전에 VPC부터 생성 단점 State 파일 관리 필요 — 분실·충돌 시 위험 콘솔에서 수동 변경하면 drift 발생(코드와 실제가 어긋남) 학습 곡선 — HCL 문법, 모듈 구조 이해 필요 일부 리소스는 삭제 후 재생성 → downtime 가능 민감정보(비밀번호) 관리에 주의 필요 drift(드리프트)란 코드에 적힌 상태와 실제 클라우드 상태가 달라진 것을 말한다. 누군가 콘솔에서 손으로 바꾸면 생긴다. 7장 협업 규칙이 이걸 막기 위한 것이다. 4. 기본 문법 한눈에 Terraform은 HCL(HashiCorp Configuration Language)이라는 문법을 쓴다. 자주 쓰는 7가지 블록만 알면 대부분 읽힌다. 41. Provider — 어떤 클라우드를 쓸지 선언 hcl terraform { requiredproviders { aws = { source = "hashicorp/aws" version = " 5.0" } } } provider "aws" { region = "apnortheast2" 서울 리전 } 42. Resource — 실제로 만들 자원 hcl resource "awsinstance" "web" { ami = "ami0c55b159cbfafe1f0" instancetype = "t3.micro" tags = { Name = "mywebserver" } } 구조는 항상 resource "리소스타입" "이름" { 설정값 } 형태다. 43. Variable — 값을 바깥에서 주입 hcl 변수 선언 (variables.tf) variable "dbpassword" { type = string sensitive = true plan 출력 시 마스킹 } 변수 사용 resource "awsdbinstance" "main" { password = var.dbpassword } 실제 값은 terraform.tfvars에 적는다 — dbpassword = "mysecretpassword" 44. Output — 결과값 꺼내기 hcl output "albdnsname" { value = awslb.main.dnsname } apply 후 값을 출력하고, 다른 모듈에서 참조할 수도 있다. 45. Module — 재사용 가능한 코드 묶음 hcl module "vpc" { source = "../../modules/vpc" 모듈 경로 env = "dev" region = "apnortheast2" vpccidr = "10.0.0.0/16" } 한 번 정의해두고 dev/prod에서 다른 값만 넣어 재사용한다. 46. Data Source — 기존 리소스 읽어오기 hcl data "awsroute53zone" "main" { name = "farmily.info" } 사용: data.awsroute53zone.main.zoneid Terraform이 만들지 않은 기존 리소스 정보를 가져올 때 쓴다. 47. locals — 반복 표현식 정리 hcl locals { appenvironment = [ { name = "S3BUCKET", value = module.s3.bucketid }, { name = "S3REGION", value = var.region }, ] } 5. 꼭 쓰는 명령어 명령어 설명 terraform init 프로바이더 다운로드, 백엔드 연결 (최초 1회) terraform plan 변경사항 미리보기 (실제 반영 X) terraform apply 실제 인프라에 반영 terraform destroy 모든 리소스 삭제 (주의!) terraform state list 관리 중인 리소스 목록 terraform state show 특정 리소스 상세 정보 terraform fmt 코드 포맷팅 terraform validate 문법 검증 코드 수정 → plan → 변경 확인 → apply → 완료 6. State 관리 — Remote Backend State 파일을 각자 노트북에 두면 팀원끼리 충돌한다. 그래서 S3에 두고 공유한다. 동시에 두 명이 apply하지 못하도록 DynamoDB로 잠금(Lock)을 건다. hcl backend.tf terraform { backend "s3" { bucket = "farmilyterraformstate" key = "prod/terraform.tfstate" region = "apnortheast2" dynamodbtable = "terraformlocks" 동시 수정 방지 (Lock) encrypt = true } } 구성 요소 역할 S3 버킷 State 파일 저장소 DynamoDB 테이블 동시에 apply 못 하게 Lock encrypt 저장 시 암호화 7. 팀 협업 방식 브랜치 전략 main (운영 반영 코드) └── featxxx (작업 브랜치) → PR 생성 → 리뷰 → main 머지 → apply 협업 규칙 1. 콘솔에서 직접 수정 금지 — 코드와 실제가 어긋남(drift) 2. main에 직접 커밋 금지 — PR로만 머지 3. plan 결과를 PR에 공유 — 뭐가 바뀌는지 팀원이 확인 4. tfvars는 gitignore — 비밀번호 등 민감정보 보호 5. 모듈 수정은 dev 먼저 테스트 — prod 반영 전 검증 .gitignore 설정 gitignore .terraform/ .tfstate .tfstate.backup .tfplan .terraform.lock.hcl .tfvars !.tfvars.example .DSStore 8. 우리 프로젝트 구조 Desktop/VPC/ ├── environments/ │ ├── dev/ 개발 환경 │ │ ├── main.tf 모듈 호출 (dev 설정) │ │ ├── variables.tf 변수 선언 │ │ ├── backend.tf S3 state 설정 │ │ └── terraform.tfvars 변수 값 (gitignore) │ └── prod/ 운영 환경 │ ├── main.tf │ ├── variables.tf │ ├── backend.tf │ └── terraform.tfvars └── modules/ 재사용 모듈 ├── vpc/ ├── ecs/ ├── rds/ ├── elasticache/ ├── s3/ ├── cloudfront/ ├── cloudwatch/ ├── sg/ ├── acm/ ├── route53/ ├── ecsscheduler/ └── waf/ 환경 분리 방식 같은 모듈을 dev/prod에서 다른 파라미터로 호출하는 게 핵심이다. hcl dev: NAT 1개, RDS 소규모, Replica 없음 module "vpc" { source = "../../modules/vpc" enablemultinat = false } prod: NAT 2개, RDS MultiAZ, Replica 있음 module "vpc" { source = "../../modules/vpc" enablemultinat = true } 9. 실제 정의된 AWS 리소스 Prod 환경 — 84개 리소스 서비스 리소스 설명 VPC VPC, Subnet×4, IGW, NAT×2, Route Table×3, S3 Endpoint 네트워크 기반 ECS Fargate Cluster, Task Definition, Service, ALB, Listener(HTTP+HTTPS), Target Group, Auto Scaling 백엔드 컨테이너 RDS PostgreSQL(MultiAZ) + Read Replica, Subnet Group, Monitoring Role 데이터베이스 ElastiCache Redis Replication Group(노드 2개), Subnet Group 캐시 S3 s3bucket, prodfrontendweb, templates 저장소 3개 CloudFront 프론트엔드 CDN + 이미지 CDN(OAC) 정적 배포 Route53 api → ALB, 루트 → CF, www → CF DNS ACM ALB용(서울) + CloudFront용(버지니아) SSL 인증서 WAF Web ACL + Rate Limit + AWS Managed Rules L7 보안 CloudWatch ECS CPU/Mem 알람, RDS CPU 알람, SNS Topic 모니터링 Security Group ALB, ECS, RDS, Redis, Lambda, Noti Lambda, AgentCore (7개) 접근 제어 IAM ECS Execution/Task Role, S3 정책, Secrets Manager 정책 권한 Dev 환경 — 65개 리소스 (Prod 대비 차이) 서비스 Prod과의 차이 VPC NAT 1개 (비용 절감) ECS Scheduler로 평일 09–18시만 운영 RDS SingleAZ, Replica 없음 ElastiCache 노드 1개 S3 1개만 (devs3bucket) CloudFront / WAF 없음 Route53 api.dev.farmily.info → ALB 10. 실제 코드 예시 VPC 모듈 — modules/vpc/main.tf hcl resource "awsvpc" "main" { cidrblock = var.vpccidr enablednssupport = true enablednshostnames = true tags = { Name = "${var.env}vpc" } } resource "awssubnet" "publica" { vpcid = awsvpc.main.id cidrblock = var.publicsubnetacidr availabilityzone = "${var.region}a" mappubliciponlaunch = true tags = { Name = "${var.env}publicsubneta" } } 모듈 호출 — environments/prod/main.tf hcl module "ecs" { source = "../../modules/ecs" env = "prod" region = var.region vpcid = module.vpc.vpcid VPC 모듈의 출력값 참조 privatesubnetids = [module.vpc.privatesubnetaid, module.vpc.privatesubnetcid] containerimage = var.containerimage desiredcount = 1 maxcount = 8 taskcpu = "512" taskmemory = "1024" enablehttps = true albcertificatearn = module.acmalb.certificatearn } 민감정보 관리 — Secrets Manager 연동 비밀번호를 코드에 적지 않고, Secrets Manager에서 ECS 컨테이너로 직접 주입한다. hcl data "awssecretsmanagersecret" "app" { name = "farmily/prod/app" } locals { appsecrets = [for k in ["DBPASSWORD", "JWTSECRET", "KAKAOCLIENTSECRET"] : { name = k valueFrom = "${data.awssecretsmanagersecret.app.arn}:${k}::" }] } 11. 전체 아키텍처 요청이 들어와서 데이터까지 닿는 전체 흐름이다. 위에서 아래로 읽으면 된다. mermaid flowchart TD Internet([Internet]) R53["Route 53farmily.info · api.farmily.info"] CF["CloudFront정적 웹 CDN"] ALB["ALBHTTPS · L7"] WAF[["WAF"]] S3F["S3frontend 정적 호스팅"] ECS["ECS FargateSpring Boot 컨테이너"] RDS[("RDSPostgreSQL")] REDIS[("ElastiCacheRedis")] S3I["S3이미지"] Internet R53 R53 CF R53 ALB WAF .보호. ALB CF S3F ALB ECS ECS RDS ECS REDIS ECS S3I Route 53(DNS)가 트래픽을 둘로 나눠 — 정적 웹은 CloudFront·S3로, API는 WAF를 거친 ALB·ECS로 보낸다. ECS는 다시 RDS·Redis·S3 세 데이터 계층과 통신한다. 12. 주의사항 실수하기 쉬운 지점 prod에서 terraform destroy 절대 금지 — 모든 리소스가 삭제된다. apply 전 항상 plan 확인 — 뭐가 바뀌는지 반드시 눈으로 체크. 콘솔 수정 후엔 terraform import — 코드에 반영해야 drift를 막는다. tfvars는 절대 git에 올리지 않기 — 비밀번호 노출 사고로 이어진다. 모듈 수정은 영향 범위 확인 — 모듈 하나 바꾸면 dev·prod 둘 다 영향. CI/CD가 바꾸는 값은 lifecycle 블록으로 무시하게 둔다. hcl lifecycle { ignorechanges = [taskdefinition] CI/CD가 이미지 태그 바꾸는 건 무시 }

Jun 17, 2026Terraform
Terraform로 다루는 클라우드 인프라

AWS Summit SEOUL 2026 후기

AWS Summit SEOUL 2026을 다녀왔다. 요즘 관심을 가지고 있던 주제가 마이그레이션, 운영, 그리고 AWS의 생성형 AI 서비스인 Bedrock이었던 만큼, 이번 Summit에서는 해당 내용을 다루는 세션들을 위주로 듣고 오자는 목표를 가지고 참여했다. 이번에 들었던 세션은 다음과 같다. Amazon Bedrock Knowledge Base와 S3 Vectors로 스트리밍 AI 챗봇 구축하기 VMware 종속을 넘어 자율로, 삼성SDS와 함께하는 AX 전략 2천만 디바이스 요청을 처리하는 API Gateway 마이그레이션 생존기 (LG U+) Amazon Bedrock Knowledge Base와 S3 Vectors로 스트리밍 AI 챗봇 구축하기.. 가장 인상 깊었던 세션은 첫 번째 세션이었다. 현재 진행 중인 팀 프로젝트와도 방향성이 맞닿아 있었기 때문에 개인적으로 가장 많은 인사이트를 얻을 수 있었던 시간이었다. 세션에서는 Bedrock Knowledge Base와 S3 Vectors를 활용해 도서 기반 AI 챗봇을 구축한 사례를 소개했다. 단순히 “챗봇을 만들었다” 수준의 발표가 아니라, 실제 서비스 관점에서 어떤 고민과 선택이 있었는지를 상세하게 다뤘다는 점이 특히 좋았다. 발표에서는 다음과 같은 내용들을 단계별로 설명했다. 소비자 요구사항 분석 Bedrock Knowledge Base의 동작 방식 문장 단위 분할(Chunking) 벡터 변환(Embedding) 기본 청킹(Default Chunking)에서 시맨틱 청킹(Semantic Chunking)으로 개선한 과정 특히 RAG(RetrievalAugmented Generation)를 실제로 운영 환경에 적용하면서 발생했던 문제들과 그 해결 과정이 굉장히 인상 깊었다. PoC 단계에서 비용을 고려한 모델 선택 전략, 토큰 사용량 최적화, Guardrail을 통한 제약 설정, 데이터 트래킹 등 단순 기능 구현을 넘어 “서비스 운영” 관점에서 어떤 요소들을 고려해야 하는지까지 다루며 발표가 진행되었다. 무엇보다 좋았던 점은 발표자분이 직접 겪었던 트러블슈팅 경험들을 솔직하게 공유해주셨다는 부분이었다. Bedrock을 처음 접하거나 실제 프로젝트에 적용해보는 과정에서 충분히 마주칠 수 있는 문제들이 많았고, 현재 진행 중인 프로젝트에서도 충분히 참고할 수 있을 만한 좋은 레퍼런스가 될 것 같다는 생각이 들었다. VMware 종속을 넘어 자율로, 삼성SDS와 함께하는 AX 전략.. 이어서 들었던 삼성SDS 세션 역시 굉장히 흥미로웠다. 세션에서는 Nutanix와 AWS 서비스를 활용해 VMware 환경에서 어떻게 전환(Migration)을 진행했는지 실제 사례 중심으로 설명했다. 단순히 “클라우드로 옮긴다” 수준이 아니라, 왜 전환을 결정했는지, 어떤 구조로 운영했는지, 운영 과정에서 어떤 문제를 겪었는지까지 실제 현업 관점에서 다뤘다는 점이 인상적이었다. 2천만 디바이스 요청을 처리하는 API Gateway 마이그레이션 생존기 (LG U+).. 마지막으로 들었던 LG U+ 세션도 굉장히 재미있게 들었다. 대규모 트래픽 환경에서 API Gateway를 어떻게 마이그레이션했고, 실제 운영 과정에서 어떤 문제를 해결했는지를 중심으로 발표가 진행되었다. 특히 기억에 남았던 부분은 다양한 모델 테스트를 통해 최적의 RAG 패턴을 구축해 나가는 과정이었다. 단순히 성능만 보는 것이 아니라 비용 효율성과 운영 안정성까지 고려하며 최적의 구조를 찾아가는 과정이 인상 깊었다. 또한 “최저 비용으로 운영 혁신을 달성한다”는 관점에서 접근한 내용들이 실제 서비스 운영을 고민하는 입장에서 굉장히 현실적으로 다가왔다. 이번 AWS Summit SEOUL 2026은 단순히 새로운 서비스를 소개하는 행사가 아니라, 실제 기업들이 어떤 방식으로 AWS 서비스를 운영 환경에 적용하고 있는지 생생하게 들을 수 있었던 자리였다. 특히 최근 관심을 가지고 공부하고 있던 분야인: 마이그레이션 운영 자동화 Bedrock RAG 생성형 AI 서비스 아키텍처 등과 관련된 실제 사례들을 직접 들을 수 있어서 개인적으로 굉장히 의미 있는 시간이었다. 기술 자체도 중요하지만, 결국 실제 서비스에서는 비용, 운영 안정성, 장애 대응, 트래픽, 데이터 관리까지 모두 함께 고려해야 한다는 점을 다시 한번 느낄 수 있었던 경험이었다. 현재 진행 중인 프로젝트에서도 이번 Summit에서 들었던 내용들을 많이 참고하게 될 것 같다.

May 20, 2026컨퍼런스
AWS Summit SEOUL 2026 후기

SAA 공부하며 끄적이는 개념들,,

S3 Presigned URL 업로드(대규모 사용자 파일 업로드 적합) 웹 서버/EC2가 파일을 직접 받지 않게 하고 애플리케이션은 업로드 권한이 있는 Presigned URL만 발급 Storage Gateway File Gateway 온프레미스에서 S3를 로컬 파일 서버처럼 사용하게 해주는 서비스 S3 Glacier Deep Archive 이 스토리지 클래스는 규정 준수 요구 사항을 충족하기 위해 데이터세트를 수년 동안 보존하도록 설계되었으며, 백업이나 재해 복구 또는 검색하는데 최대 72시간까지 기다려도 되는 자주 액세스하지 않는 데이터에도 사용할 수 있다. S3 Glacier Instant Retrieve 의료 의미지, 뉴스 미디어, 유전체학 등 즉각적 접근이 필요한 아카이브 데이터에 적합하다. S3 Glacier Flexible Retrieve 즉각적 접근은 필요없지만 백업, 재해복구 등 대용량 데이터 세트를 몇분 12시간 내 검색 무료 제공 S3 Intelligent Tiering 액세스 빈도, 불규칙한 사용 패턴 모를 때 사용 가능, 다양한 액세스 패턴에 적합 S3 Standard 자주 접근하는 범용 스토리지 S3 StandardInfrequent Access(S3 StandardIA) 가끔 Access 하지만 가끔할 때 즉각적 접근 필요 시 적합 S3 One ZoneInfrequent Access 하나의 지역에만 사용 가능한 S3 클래스 단일 AWS 가용 영역에 데이터를 저장하기 때문에 스토리지 클래스에 저장된 데이터는 가용성 영역이 파괴되는 경우 손실 AWS Transfer Family SFTP, FTPS, FTP 및 AS2 프로토콜을 사용하여 Amazon S3, Amazon EFS로 파일을 전송할 수 있게 하는 서비스 AWS FSx 클라우드에서 다양한 파일 시스템을 제공하는 완전 관리형 서비스 사용자는 고성능 파일 시스템을 쉽게 설정, 관리 및 운영할 수 있으며 윈도우 파일서버, Lustre, NetApp ONTAP, OpenZFS와 같은 다양한 파일 시스템 옵션을 선택할 수 있다. 온프레미스 환경과 클라우드 환경을 연결하여 하이브리드 클라우드 스토리지 솔루션 AWS FSx for NetApp OnTAP NetApp의 인기있는 OnTAP 파일 시스템에 구축된 매우 안정적이고 확장 가능하며 성능이 뛰어나고 기능이 풍부한 파일 스토리지를 제공하는 완전관리형 서비스 데이터 센터에 클라우드의 간소성과 유연성을 제공하고 데이터 센터의 모든 엔터프라이즈 기능을 퍼블릭 클라우드에 제공 AWS EC2 배치 그룹 종류 클러스터 배치 그룹 단일 가용 영역 내에 있는 인스턴스의 논리적 그룹(인스턴스는 단일 랙에 격리되지 않음) 파티션 배치 그룹 애플리케이션에 대한 상관 관계가 있는 하드웨어 장애 가능성을 줄이는 데 도움 파티션 배치 그룹을 사용하는 경우 Amazon EC2는 각 그룹을 파티션이라고 하는 논리 세그먼트로 나눈다. 각 랙은 자체 네트워크 및 전원이 있다. 배치 그룹 내 두 파티션이 동일한 랙을 공유하지 않으므로 애플리케이션 내 하드웨어 장애의 영향을 격리 분산형 배치 그룹 각각 고유한 하드웨어에 배치된 인스턴스 그룹 인스턴스가 동일한 장비를 공유할 때 장애가 동시에 발생할 수 있는 위험이 줄어듦 Spot 인스턴스 AWS의 남는 EC2 자원을 매우 싸게 빌려주는 방식(최대 90% 할인) 대신 AWS가 필요하면 갑자기 회수할 수 있음(보통 2분 전 알림 제공) Batch Job 작업에 적합 실패하면 다른 인스턴스가 다시 처리(중단 허용 가능) EC2 Spot Fleet 여러 Spot Intance 요청을 관리하고 지정된 목표 용량을 충족시키기 위해 여러 인스턴스 유형과 가용 영역을 활용하여 비용을 최적화하는 서비스 스팟 인스턴스 풀에서 사용 가능한 인스턴스를 검색하여 목표 용량을 자동으로 채우고 필요에 따라 인스턴스 유형과 가용 영역을 변경하여 비용을 절감하고 애플리케이션의 가용성을 높일 수 있음 DR Options Backup & Restore ($) 느림(시간 단위, Hours) 단순 데이터 백업 Pilot Light ($$) 중간(십분 단위, 약 10분 90분), DB만 동기화 Warm Standby($$$) 빠름(분 단위, 약 5분 15분), DB 동기화 + APP은 낮은 스펙으로 대기 MultiSite($$$$) 아주 빠름(거의 실시간), DB 동기화 + 같은 스펙의 APP 구성 Network AWS Global Accelerator 트래픽 관리를 간소화하고 성능을 최대 60% 개선하는 네트워킹 서비스 AWS Shield와 통합되어 DDoS 공격으로부터 애플리케이션을 보호, 애플리케이션 상태, 사용자 위치 및 고객이 구성하는 정책의 변경에 즉각적으로 대응하여 항상 성능에 기반한 최적의 엔드포인트로 사용자 트래픽을 라우팅 고정/정적 IP 주소 또는 빠른 지역 장애 조치에 필요한 HTTP 사용 사례에 적합 전 세계 사용자가 AWS 애플리케이션에 더 빠르고 안정적으로 접속하도록 AWS 글로벌 네트워크로 우회시켜주는 서비스 ALB, NLB, EC2, Elastic IP 앞에는 붙이나 S3에 직접 접근은 하지 않는다. 항목 CloudFront Global Accelerator 역할 CDN 네트워크 가속 캐싱 O X 정적 콘텐츠 강함 약함 TCP/UDP 최적화 일부 매우 강함 게임/실시간 앱 보통 강함 CloudFront 파일을 가까운 곳에 캐시 Global Accelerator 네트워크 경로 자체를 최적화 VPC Endpoint Endpoint 유형 중 하나 VPC Endpoint를 사용하면 VPC 내부 또는 외부에 있는 AWS 서비스들과 통신할 때 인터넷 통신이 되지 않더라도 Private한 통신 환경을 통해 서비스에 접근 NLB 데이터를 전송하는 지연 시간을 최소화하고 다른 AWS 영역으로 신속하게 페일오버해야 하는 요구 사항을 충족 TCP/UDP 트래픽을 처리하며 네트워크 계층에서의 로드 밸런싱 ALB 주로 HTTP/HTTPS 트래픽을 처리하는 데 사용되며 애플리케이션 계층에서의 로드 밸런싱을 제공 오토 스케일링 그룹을 함께 사용하여 애플리케이션의 트래픽 변화에 따라 자동으로 서버 인스턴스 수를 조정 AWS VPC Lattice 다양한 AWS 서비스와 리소스 간의 연결, 보안, 모니터링을 중앙 집중식으로 관리할 수 있도록 지원하는 완전 관리형 애플리케이션 네트워킹 서비스 VPC Lattice를 사용하면 복잡한 네트워크 구성 없이도 서비스 간 통신을 간소화하고 보안 정책을 일관되게 적용하며 트래픽 흐름을 효율적으로 관리 AWS Elastic Fabric Adapter(EFA) Amzon EC2 인스턴스에 연결하여 고성능 컴퓨팅(HPC) 및 기계 학습(ML) 워크로드를 가속화하는 고급 네트워크 인터페이스 EFA는 노드 간 통신을 향상시켜 클라우드에서 대규모 애플리케이션을 실행할 수 있도록 지원하며, 특히 HPC 및 ML 작업에서 성능 향상을 제공한다 AWS Transit Gateway 여러 VPC와 온프레미스를 중앙 허브로 연결하는 서비스 VPC Peering 단점 해결 암호화 제공x AWS PrivateLink PrivateLink는 AWS 내의 VPC와 서비스를 연결하는 데 사용되지만 온프레미스 데이터 센터를 AWS에 연결하도록 설계되지 않았다. DB & Data AWS RDS 데이터 베이스 인프라 및 업데이트들을 AWS 측에서 관리해주고 데이터베이스의 설치, 운영 그리고 관리 등의 서비스들을 지원하는 AWS 관계형 데이터베이스 RDS Proxy 데이터베이스의 연결 관리를 위한 메모리 및 CPU 오버헤드를 줄이며 성능 개선, 예기치 않은 트래픽 급증 처리 Amazon RDS 유휴 암호화 데이터베이스 인스턴스, 자동 백업, 읽기 전용 복제본, 스냅샷 등 저장된 데이터를 보호 AWS는 기본 스토리지와 백업 데이터를 AES256 암호화 알고리즘을 사용하여 암호화 Amazon RDS Custom 기본 OS 및 DB 환경에 액세스할 필요가 있는 레거시, 사용자 지정, 패키지 애플리케이션을 위한 관리형 데이터베이스 서비스 AWS SCT(Schema Conversion Tool) 소스 데이터베이스의 데이터베이스 스키마를 대상 Amazon RDS 인스턴스와 호환되는 형식으로 자동으로 변환하는 프로젝트 기반 사용자 인터페이스를 제공 Amazon Aurora MySQL 및 PostgreSQL과 호환되는 완전 관리형 관계형 데이터베이스 엔진 (읽기 전용)자동으로 3개의 AZ에 6개의 복제본 생성( DynamoDB 서버리스 기반 KeyValue NoSQL 데이터베이스 용량에 맞게 테이블을 자동으로 조정하므로 별로 관리하지 않아도 성능 유지, 주문형 백업 기능 제공(보관을 위해서 특정 시점 백업 및 복구, 단 35일로 제한) 온디맨드 용량 모드(주문형 모드) 이전에 관찰된 트래픽 수준까지 상승/하락 시 워크로드 즉시 수용 가능. 읽기/쓰기 용량 모드 있음 트래픽에 따라 자동 확장하고 앱에서 읽기/쓰기 요청에 대해서만 비용 지불, 용량 계획 없이 요청 당 지불가격 제공 변동성이 큰 트래픽 프로비저닝 모드 애플리케이션에서 필요한 초당 읽기/쓰기 횟수 미리 지정 AutoScaling 사용하여 변경에 따라 용량 자동 조정 가능. 예측 가능한 트래픽 GSI(Global Secondary Index) 기본 테이블과 다른 파티션 키와 정렬 키를 사용하여 데이터를 조회할 수 있는 인덱스 GSI를 사용하면 기본 테이블의 파티션 키와 정렬된 키와는 다른 기준으로 데이터를 검색할 수 있어 다양한 쿼리 패턴을 지원 빠르지만 자유로운 검색에 약한 DynamoDB의 보완 DynamoDB와 DAX(DynamoDB Accelerator)가 결합되면 한 단계 업그레이드하여 읽기 중심의 워크로드에서 초당 수백만 개의 요청에도 마이크로초의 응답 시간을 지원 + 인메모리 캐시 DynamoDB TTL(Time to Live) 항목별 타임스탬프를 정의하여 더 이상 필요하지 않은 시기를 결졍 지정된 타임스탬프의 날짜 및 시간 직후 DynamoDB는 쓰기 처리량을 소모하지 않고 테이블에서 항목을 삭제 DynamoDB Global Tables MultiRegion, MultiActive 구조 모든 리전에서 읽기, 쓰기 가능 여러 리전에서 데이터를 실시간으로 복제하며 사용자 요청에 빠르게 대응할 수 있음 낮은 지연 시간 보장 AWS PITR(PointinTime Recovery) 특정시점복구 AWS 서비스에서 제공하는 백업 및 복구 기능으로 데이터베이스를 특정 시점으로 복원 Amazon Athena 표준 SQL을 사용해 S3에 저장된 데이터를 간편하게 분석할 수 있는 대화식 쿼리 서비스 Amazon Athena Federated Query 데이터 분석가가 서로 다른 데이터 소스에 저장된 데이터를 단일 SQL 쿼리를 통해 분석할 수 있게 해주는 기능 S3, RDS, DynamoDB 등 다양한 데이터 소스를 마치 하나의 데이터 웨어하우스처럼 쿼리할 수 있도록 함 AWS Kinesis 실시간 스트리밍 데이터를 수집, 분석, 처리하는 서비스 Kinesis Data Streams 수집 & 전송 대규모의 애플리케이션의 로그, IoT 디바이스의 데이터 등 스트리밍 데이터를 실시간으로 수집하는 서비스 수집한 데이터는 다른 AWS 서비스 또는 애플리케이션에서 사용되어 분석처리 기본 보존 시간은 24시간이며 최대 8760시간(365일)까지 연장 Kinesis Data Firehose 실시간 스트리밍 데이터를 안정적으로 받은 후 필요한 경우 변환하여 다른 AWS의 서비스로 전달하는 서비스 Lambda 함수를 호출하여 데이터 변환 후 전송 가능 Kinesis Data Analytics SQL/Apache Flink로 데이터 스트림 분석 Kinesis Video Streams 비디오 스트림 수집, 저장 및 처리 Amazon MSK(Managed Streams for Apache Kafka) Apache Kafka를 사용하여 스트리밍 데이터를 처리하는 애플리케이션을 빌드하고 실행할 수 있는 완전관리형 서비스 Amazon Redshift 클라우드에서 완전히 관리되는 페타바이트급 데이터 웨어하우스 서비스, 지정시간동안 앱 비활성화일 때 분석 서버, 클라이언트 측 암호화 지원 AWS Glue 사용자가 여러 소스의 데이터를 쉽게 검색, 준비, 이동, 통합할 수 있도록 하는 서버리스 데이터 통합 서비스(JDBC 커넥터를 통해 특정 데이터베이스 연결을 지원) Athena에서 쿼리 가능한 Parquet 파일을 쓸 수 있음 AWS Glue DataBrew 코딩 없이 데이터를 정리하고 정규화하여 분석 및 기계 학습(ML)에 적합하게 준비할 수 있도록 도와주는 시각적 데이터 준비 도구 데이터 분석 및 ML 전에 데이터를 쉽게 처리하고 정제할 수 있도록 지원 DataBrew를 사용하면 데이터 준비 시간을 최대 80%까지 단축 AWS Lake Formation S3의 기본 데이터가 있는 데이터베이스, 테이블 및 열과 같은 데이터 카탈로그 리소스에 대한 액세스 권한을 부여하거나 취소 가능 서비스 Amazon AppFlow Salesforce, SAP, Zendesk, Slack 및 ServiceNow 같은 SaaS와 S3/Amazon Redshift 사이 데이터를 안전하게 전송할 수 있는 완전 관리형 통합 서비스 Amazon ElastiCache 더 느린 디스크 기반 데이터베이스에 전적으로 의존하기 보다는 신속한 관리형 인 메모리 시스템에서 정보를 검색할 수 있는 기능을 지원함으로써 웹 애플리케이션의 성능을 향상 + 분산세션 데이터 관리 인터넷 규모의 실시간 애플리케이션을 지원 도입을 위해선 아키텍쳐 변경이 필요 Amazon Neptune AWS에서 제공하는 완전 관리형 그래프 데이터베이스 서비스 고도로 연결된 데이터셋을 효율적으로 저장하고 쿼리할 수 있도록 설계되었으며 주로 소셜 네트워킹, 추천 엔진, 지식 그래프, 생명과학 연구 등의 분야에서 활용 AWS Neptune Streams Amazon Neptune 그래프 데이터베이스의 변경 사항을 실시간으로 캡처하고 처리할 수 있는 기능. 이를 통해 데이터베이스의 변경 사항을 다른 시스템으로 복제하거나 변경 내용을 기반으로 새로운 로직을 트리거하는 등 다양한 작업을 수행 AWS Quantum Ledger Database(Amazon QLDB) 투명하고 변경 불가능하며 암호화 방식으로 검증 가능한 트랜잭션 로그를 제공하는 완전 관리형 원장 데이터베이스 AWS Lustre 고성능 병렬 파일 시스템인 Lustre를 기반으로 하는 Amazon FSx for Lustre 서비스 대규모 컴퓨팅 워크로드에서 높은 처리량과 낮은 지연 시간을 제공하며 AWS 클라우드에서 사용하기 쉽도록 완전 관리형으로 제공. 특히 머신 러닝, 고성능 컴퓨팅(HPC), 미디어 처리 등 데이터 집약적인 작업에 적합 Security AWS Secrets Manager 자격증명 저장 및 관리 서비스 AWS Console이나 AWS CLI(Bastion 호스트처럼)를 통해 EC2 인스턴스에 안전하게 로그인할 수 있는 기능 DB 비밀번호, API Key, 인증 정보 같은 Secret 관리 전용 서비스 AWS Secrets Manager Auto Rotate(자동회전) 보안 암호의 만료 시점을 자동으로 관리하고 주기적으로 암호를 교체하여 보안을 강화하는 기능 Amazon Cognito 웹 및 모바일 앱을 위한 자격 증명 플랫폼 OAuth 2.0 액세스 토큰 및 AWS 보안 인증을 위한 사용자 디렉터리, 인증 서버, 인증 서비스 Amazon GuardDuty 계정보호 서비스 AWS 계정 및 워크로드에서 악의적 활동 모니터링하고 상세 보안 결과 제공 위협 탐지 서비스 Amazon Inspector 취약점 스캔 서비스 Amazon Shield advance 정교한 대규모 DDoS 공격에 대한 보호 및 완화 실시간 가까운 가시성 EC2, ELB, CloudFront, Global Accelerator, Route 53 요금 보호 제공 Amazon Macle 민감한 데이터를 검색, 모니터링 및 보호하는데 도움이 되는 관리형 데이터 보안 서비스 Amazon Firewall Manager 여러 AWS 계정과 리전에서 방화벽 규칙을 중앙에서 관리할 수 있도록 하는 보안 서비스 AWS WAF용 관리형 규칙과 통합되므로 사전에 구성된 WAF 규칙을 애플리케이션에 손쉽게 배포 AWS KMS(Key Management Service) 클라우드 환경에서 데이터를 보호하기 위한 AWS의 암호화 키 관리 서비스 KMS는 암호화 키를 안전하게 생성, 저장, 관리하고 이를 사용하여 AWS 서비스와 애플리케이션에서 데이터를 암호화하거나 복호화할 수 있도록 지원 고객 관리형 키(CMK) 사용자가 생성, 소유, 관리하는 키로 키, 정책, 키 사용 권한 등을 사용자가 직접 설정 AWS 관리형 키 AWS가 관리하는 키로 사용자는 키 정책을 설정할 수 없지만 키 사용 권한을 제어 S3 버킷에 저장된 중요한 데이터를 암호화할 때 사용자는 AWS 관리형 키 대신 CMK를 사용하여 데이터에 대한 통제력을 높일 수 있다. AWS Security Token Service(STS) AWS 리소스에 접근하기 위한 임시 보안 자격 증명을 발급하는 서비스 기존 IAM 사용자나 역할에 대한 접근 권한을 일시적으로 부여하여 특정 작업이나 리소스에 대한 액세스를 제어 SAML 호환성 없이도 기존 LDAP 디렉터리 서비스를 사용하여 AWS Management Console에 사용자를 인증 Monitoring Amazon QuickSight PostgreSQL용 Amazon S3 / RDS 비롯한 다양한 데이터 소스에서 대화형 대시보드 및 보고서 생성하는 데이터 시각화 서비스 AWS CloudTrail 사용자가 활동 및 API 사용 추적하여 감사, 보안 모니터링 및 운영 문제 해결 AWS Config 리소스 구성 사항 변경 추적 규칙 생성으로 EBS 볼륨의 암호화 여부 확인 후 암호화되지 않은 볼륨에 플래그 지정 가능 Amazon Workload Discovery AWS 클라우드 워크로드를 시각화하여 아키텍처 다이어그램으로 자동 생성해주는 솔루션 AWS 환경에서 실행되는 애플리케이션 및 서비스들의 구조를 자동으로 파악하고 시각적으로 보여주는 도구 AWS 비용 및 사용 보고서 쿼리, 리소스 검색, 아키텍처 다이어그램 저장 및 내보내기 등을 수행 AWS XRay 애플리케이션이 처리하는 요청에 대한 데이터를 수집하고 해당 데이터를 보고, 필터링하고, 인사이트를 얻어 문제와 최적화 기회를 식별하는 데 사용할 수 있는 도구를 제공하는 서비스 애플리케이션의 동작에 관한 데이터를 수집/분석하는 서비스 XRay는 특히 트러블슈팅의 목적으로 이용 AWS Step Functions 실시간 진단 및 대시보드를 제공하고 Amazon CloudWatch 및 AWS CloudTrail과 통합되며 전체 상태, 실패한 단계, 입력 및 출력을 비롯한 모든 실행 관련 정보를 기록. 서버리스 서비스 시각적 워크플로우를 사용하여 분산 애플리케이션 및 마이크로 서비스의 구성 요소를 조정하여 애플리케이션을 쉽게 구축할 수 있게 해주는 완전 관리형 서비스 여러 AWS Lambda 함수를 반응형 서버리스 애플리케이션에 결합하고 EC2 인스턴스, 컨테이너 또는 온프레미스 서버에서 실행되는 데이터 및 서비스를 오케스트레이션 가능 AWS Resource Groups Tag Editor 태그를 기반으로 리소스를 관리하고 구성하도록 특별히 설계 AWS Trusted Advisor AWS 환경의 비용, 보안 및 성능을 최적화 AWS Systems Manager 클라우드 및 온프레미스 리소스의 통합 관리를 지원하며 자동화와 효율성을 통해 운영 복잡성을 줄이고 보안을 강화하는데 필수적인 도구 회사는 실행 중인 애플리케이션을 중단하지 않고 IAM 역할이나 사용자를 수동으로 생성하거나 수정하지 않고도 EC2 인스턴스를 패치 기본 호스트 구성 관리 Systems Manager가 EC2 인스턴스를 관리형 인스턴스로 자동 관리할 수 있게 해주는 AWS Systems Manager의 기능 Service Snowball Edge (마이그레이션 장비) 기존 저장소의 대역폭이 충분하지 않을 때 대용량 데이터 세트를 클라우드로 이전하는데 도움(80TB(실사용 72TB)까지 한번에 이동 가능하며 Snowball Edge는 100TB(실사용 83TB)까지 가능) aws가 고용량 저장 장비를 사용자에게 보내고 사용자는 장비에 데이터를 복사해 다시 aws에게 보낸다. aws는 장비에 저장된 데이터를 S3, EBS, Glacier 등으로 저장한다. Amazon Snowcone AWS Snow Family에 속하는 소형, 휴대용 엣지 컴퓨팅 및 데이터 전송 장치 Snowcone은 데이터 수집, 처리, 저장 및 전송을 위해 설계되었으며 특히 네트워크 연결이 제한적이거나 신뢰할 수 없는 환경에서 유용 AWS DataSync 데이터 전송 프로세스의 관리와 고성능 및 보안 데이터 전송에 필요한 인프라를 모두 자동화 endtoend 보안을 제공하여 데이터가 안전하고 온전하며 사용할 준비가 된 상태로 도착하도록 보장 암호화, 데이터 무결성 확인 및 대역폭 최적화도 처리 온프레미스 ↔ AWS 사이에서 데이터를 빠르고 자동으로 옮겨주는 데이터 전송 서비스 전송 가능 대표 대상 S3, EFS, FSx 지원 프로토콜 NFS, SMB, 객체 스토리지 Amazon SQS(Simple Queue Service) 작업 요청 솔루션을 분리하여 대기열에 따라서 인스턴스 수를 확장 및 리소스 추가제공 갑작스러운 작업이 몰려도 추후 처리가능하도록 보관기능 제공 손실위험이 있는 데이터를 잠시 보관하는 용도 Dead Letter Queue 등 다양한 옵션으로 메시지 처리가 실패했을 경우 해당 메시지를 보관했다가 다시 처리할 수 있게끔 하는 기능을 제공 Amazon SNS(Simple Notification Service) 클라우드에서 손쉽게 알림을 설정, 운영 및 전송할 수 있도록 하는 웹 서비스 Amazon SES(Simple Email Service) 기업이 자체 이메일 주소와 도메인을 사용하여 이메일을 보내고 받을 수 있도록 하는 비용 효율적이고 확장 가능한 이메일 서비스 AWS Fargate EC2 인스턴스의 서비스나 클러스터를 관리할 필요없이 컨테이너 실행을 위해 ECS에 사용가능한 기술 컨테이너를 실행하는 데 필요한 컴퓨팅 리소스를 자동으로 할당하고 관리 AWS Fargate Spot Fargate를 이용해 컨테이너를 실행할 때, AW의 유휴 용량을 활용하여 Fargate 정가보다 최대 70% 저렴한 비용으로 컨테이너를 실행할 수 있는 기능 Amazon VPC CNI(Container Network Interface) 플러그인 EKS 클러스터에서 파드에 대한 네트워킹을 담당하는 플러그인. 이 플러그인은 각 노드에 VPC IP 주소를 할당하고 파드들이 서로 통신할 수 있도록 네트워크를 구성하는 역할 AWS App2Container(A2C) 기존 온프레미스 또는 가상 머신에서 실행되는 Java 및 .NET 애플리케이션을 컨테이너화된 애플리케이션으로 전환하여 현대화할 수 있도록 지원하는 명령줄 도구. 이 도구를 사용하면 코드 변경 없이 애플리케이션을 컨테이너 이미지로 변환하여 ECS 또는 EKS와 같은 컨테이너 오케스트레이션 서비스에서 실행 AWS Lightsail 저렴하고 예측 가능한 가격으로 컨테이너와 같은 클라우드 리소스의 간단한 관리를 제공하는 사용하기 쉬운 가상 프라이빗 서버 클랄우드의 문턱을 낮춘 상품, 개인 개발자나 IT 인프라 관리자가 없는 소규모 기업체에 적합한 서비스 AWS Database Migration Service Oracle에서 Oracle로의 동종 마이그레이션은 물론 Oracle 또는 Microsoft SQL Server에서 Amazons Aurora로의 서로 다른 데이터베이스 플랫폼 간의 이기종 마이그레이션을 지원 짧은 지연시간으로 데이터를 지속적 복제 가능 AWS Control Tower Best Practice에 기반한 보안 멀티 계정 환경(Landing Zone이라고 부른다)의 설정을 자동화하는 매니지드 서비스 AWS 환경에 대한 지속적인 가시성을 제공하기 위해 프로비저닝 된 OU 및 계정 수, 활성화 된 가드레일 수 등을 시각화한 대시보드를 제공하여 이를 통해 해당 가드레일에 대해 OU 및 계정의 상태를 확인할 수 있도록 제공 Guardrail 클라우드 리소스 설정 시 보안 규칙에 어긋나지 않도록 클라우드 리소스를 유지시킬 수 있는 기능을 제공 특정 설정 정책 배포 차단 Detective Controls(탐지 제어) AWS Config 규칙을 사용하여 조직의 AWS 계정 및 리소스에서 규정 준수 위반 사항을 지속적으로 모니터링하는 기능 AWS Account Factory AWS Control Tower의 핵심 기능 중 하나로, 새로운 AWS 계정을 손쉽게 만들고 관리할 수 있는 자동화된 프로세스를 제공 AWS Elastic Beanstalk 웹 애플리케이션 및 서비스의 배포 및 조정을 위한 서비스 코드를 업로드 하면 Elastic Beanstalk이 용량 프로비저닝, 로드 밸런싱, 자동 조정부터 시작하여 애플리케이션 상태 모니터링에 이르기까지 배포를 자동으로 처리 AWS Amplify 풀스택 애플리케이션 개발을 지원하는 개발 플랫폼 프론트엔드와 백엔드를 쉽게 설정, 개발, 배포를 통합적으로 관리하는 데 유용 Elastic Beanstalk은 백엔드를 쉽게 구성해주지만 Amplify는 프론트엔드, 백엔드, CI/CD를 쉽게 올인원으로 만들도록 도와준다. AWS CloudFormation AWS 리소스를 모델링하고 설정하여 리소스 관리 시간을 줄이로 AWS에서 실행되는 애플리케이션에 더 많은 시간을 사용하도록 해주는 서비스(IaC) AWS Pinpoint 모바일 앱 및 웹 애플리케이션의 마케팅 전략을 강화하고 사용자 경험을 최적화하는 데 도움이 되는 강력한 마케팅 서비스 고객이 특정 키워드가 포함된 메시지를 보낼 때 자동 응답을 생성 약속 확인, 주문 업데이트, 일회용 암호와 같은 다이렉트 메시지를 전송가능 AWS Lambda 서버리스 컴퓨팅 서비스로 서버를 프로비저닝할 필요없이 특정 이벤트에 대한 응답으로 코드를 실행 호출 수와 함수 실행 시간에 따라 요금을 부과 Lambda@Edge를 생성하면 들어오는 요청의 UserAgent 헤더를 검사하고 사용 중인 장치 유형을 확인. 이 정보 기반으로 응답을 사용자 지정하고 적절한 버전의 콘텐츠를 사용자에게 보낼 수 있음 AWS Lambda SnapStart Lambda 함수의 콜드 스타트 성능을 개선하기 위해 제공되는 기능 함수를 처음 실행하거나 유휴 상태에서 재개할 때 발생하는 지연 시간을 줄여준다. SnapStart는 함수의 초기화 단계를 스냅샷으로 찍어 저장하고 이후 호출 시 이 스냅샷을 사용하여 빠르게 함수를 시작한다. AWS Batch 사용자가 AWS에서 배치 작업을 실행할 수 있게 해주는 완전관리형 서비스 다른 언어로 작성된 다양한 유형의 작업을 처리하고 EC2 인스턴스에서 실행 AWS Organizations 여러 AWS 계정을 중앙에서 관리하고 통합할 수 있는 서비스 비용을 절감하며 보안 및 규정 준수를 강화할 수 있는 강력한 도구 Amazon ElastiCache for Redis AWS에서 제공하는 완전 관리형 인메모리 캐싱 서비스로 Redis 데이터베이스를 클라우드에서 쉽게 설정, 관리 및 확장할 수 있도록 지원 고성능과 낮은 지연 시간을 제공하며 애플리케이션의 데이터베이스 레이어를 가속화하고 세션 저장소, 게임 리더보드 등 다양한 용도로 사용 AWS EMR(Elastic MapReduce) 대규모 데이터 처리 및 분석을 위해 제공하는 클라우드 기반 서비스 Apacahe Hadoop, Apache Spark 등 오픈 소스 빅데이터 프레임워크를 사용하여 데이터를 처리하고 분석할 수 있도록 지원 유연하고 확장 가능한 분산 처리 환경을 제공 병렬 데이터 처리 런타임 역할 클러스터에서 실행되는 애플리케이션이 AWS 리소스에 액세스할 수 있도록 권한을 부여하는데 사용 보안 구성에 따라 각 클러스터마다 고유한 식별자를 생성하여 클러스터 내에서 실행되는 애플리케이션이 필요한 리소스에 안전하게 액세스 API Gateway 서버리스 애플리케이션, REST API, WebSocket API를 만들고 관리할 수 있도록 해주는 완전 관리형 서비스 API 개발자가 애플리케이션 백엔드에 안전하게 액세스하고, API를 쉽게 게시, 유지 관리, 모니터링 및 보안할 수 있도록 지원 AWS 엣지 최적화 엔드포인트 클라이언트가 지리적으로 분산되어 있을 때 API 요청의 지연 시간을 줄이기 위해 사용 이 엔드포인트 유형은 AWS 관리형 CloudFront 배포를 사용하여 요청을 가장 가까운 CloudFront 엣지 로케이션으로 라우팅하여 응답 속도를 높여준다 리전 API 엔드포인트 동일 리전의 클라이언트를 위한 서비스 EC2 인스턴스를 실행하는 클라이언트가 동일 리전에서 API를 호출하거나 AP가 수요가 큰 소수의 클라이언트에게 서비스를 제공하기 위한 것이라면 리전 API는 연결 오버헤드를 줄인다 프라이빗 API 엔드포인트 인터페이스 VPC 종단점을 사용해서 Amazon VPC에서만 액세스할 수 있는 API 엔드포인트 AWS AutoScaling Warm Pool 애플리케이션의 확장 속도를 높이고 비용을 절감하기 위해 사전 초기화된 EC2 인스턴스 풀을 유지하는 기능 웜 풀을 사용하면 새 인스턴스를 시작하는 대신 미리 준비된 인스턴스를 사용하여 애플리케이션 수요 증가에 빠르게 대응 AWS Outposts AWS 인프라와 서비스를 고객의 온프레미스 환경, 즉 데이터 센터나 엣지 로케이션에 확장하여 제공하는 완전 관리형 서비스 즉, AWS가 완전한 rack을 만들어 보내주면 고객 사이트에 설치해서 고객이 사용. 그러나 물리적 관리는 AWS 지원 휴지통 보존 규칙 아카이브 스토리지 티어의 아카이빙된 스냅샷에도 적용된다 보존 규칙과 일치하는 아카이빙된 스냅샷을 삭제하면 보존 규칙에 정의된 기간 동안 해당 스냅샷이 휴지통에 보존된다 아카이빙된 스냅샷에 대한 요금으로 청구 Amazon OpenSearch Service AWS 클라우드에서 OpenSearch 클러스터를 쉽고 안전하게 배포, 확장 및 운영할 수 있도록 지원하는 완전 관리형 서비스 OpenSearch는 데이터 검색, 분석, 모니터링 등 다양한 용도로 사용되는 오픈 소스 검색 및 분석 엔진 Amazon MGN(Application Migration Sevice) 온프레미스 또는 다른 클라우드 환경에서 AWS로 서버를 마이그레이션하는데 사용되는 서비스 컷오버 인스턴스(cutover instance) 소스 서버를 AWS 환경으로 완전히 전환하기 위한 최종 단계를 의미 컷오버 인스턴스는 테스트 단계를 거친 후 실제 운영 환경으로 사용될 인스턴스를 의미하며 이 단계에서 소스 서버의 데이터와 애플리케이션이 AWS로 마이그레이션 AWS ParallelCluster AWS에서 고성능 컴퓨핑(HPC) 클러스터를 생성하고 관리할 수 있는 서비스 Boto3 파이썬으로 AWS 서비스를 제어하고 관리하기 위한 공식 SDK 즉, 파이썬 코드를 사용하여 S3, EC2, Lambda 등 다양한 AWS 서비스를 프로그래밍 방식으로 조작하고 관리할 수 있게 해주는 도구 AI/ML Amazon SageMaker 완전 관리형 인프라, 도구 및 워크플로를 사용하여 모든 사용 사례에 대해 기계 학습(ML) 모델을 구축, 훈련 및 배포하는 완전 관리형 서비스

May 20, 2026AWS
SAA 공부하며 끄적이는 개념들,,

서버가 죽지 않도록... ELB와 Auto-Scailing 이해하기 (feat. AWS)

먼저 ELB와 AutoScailing에 대해 알아보자. ELB (Elastic Load Balancing) ELB는 들어오는 애플리케이션 트래픽을 Amazon EC2 인스턴스, 컨테이너, IP 주소 등 여러 대상에 자동으로 분산시킨다. 단일 가용 영역 또는 여러 가용 영역에서 다양한 대상에 걸쳐 애플리케이션 트래픽의 부하를 분산하여 애플리케이션의 결함 허용 능력을 높인다. 그 중 ALB(Application Load Balancer)의 특징은 OSI 모델의 7계층(애플리케이션 계층)에서 작동하며 HTTP/HTTPS 프로토콜의 헤더 내용을 기반으로 고급 라우팅 결정을 내릴 수 있다. NLB(Network Load Balancer)는 OSI 모델의 4계층에서 작동하며 초당 수백만 개의 요청을 처리할 수 있는 성능을 제공한다. 또한 가용 여역당 하나의 고정 IP를 가질 수 있다는 큰 특징이 있다. 쉽게 얘기해보면 웹서비스를 운영한다면 ALB를, 게임 서버나 실시간 스트리밍이라면 속도가 생명인 NLB를 사용한다고 볼 수도 있겠다. AutoScailing Amazon EC2 AutoScailing은 애플리케이션의 부하를 처리할 수 있는 정확한 수의 EC2 인스턴스를 유지할 수 있게 도와준다. 지정된 조건에 따라 EC2 인스턴스를 자동으로 시작하거나 종료한다. 이를 통해 수요가 급증할 때는 성능을 유지하고 수요가 적을 때는 비용을 절감한다. 가장 큰 목적은 고가용성(HighAvailability)이다. scale out = 서버의 갯수가 늘어남, scale in = 서버의 갯수가 줄어듬. scale up = 서버의 리소스가 증가, scale down = 서버의 리소스가 감소. AutoScailing의 구성 순서는 아래와 같다. 1. ELB를 생성하면서 빈 타겟그룹을 생성한다. 2. 빈 타겟그룹으로 로드밸런서를 생성한다. 3. AutoScailing 그룹을 만든 후, 위에서 만든 빈 타켓그룹에 넣어준다. 간단한 실습을 통해 개념을 익히고 감각을 익혀보자. Web 서버를 ubuntu 24.04 기반으로 생성하여 AutoScailing을 구성해보자. EC2 인스턴스 개수는 최소 1대 최대 3대 가용 영역 최소 2개 이상(a, c)으로 하는 AutoScailing 가능한 web 서버들을 프라이빗 서브넷에 위치. stress 패키지가 재부팅 시에도 자동으로 부하가 걸리게끔. 외부(노트북의 브라우저)에서 웹페이지가 보이도록. ALB의 Listen Port : 88 Web 서버(인스턴스)를 생성할 때 퍼블릭으로 둔 후 필요한 패키지를 설치하고 인스턴스를 이미지 화 하여 프라이빗 서브넷에 인스턴스를 생성하는 방향으로 진행하였다. 먼저 퍼블릭 서브넷에 인스턴스를 하나 만들어서 nginx, stress(cpu 부하용) 패키지를 설치한 후 이미지화 하여 시작 템플릿을 만든다. bash apt update y && apt install y nginx echo webtem /var/www/html/index.html systemctl enable now nginx stress 패키지 설치 후 cpu에 부하 걸어주기. bash apt install y stress stress c 2 t 1200 t3.micro의 vcpus 갯수가 2개 이상이므로 c 2, t초 20분 동안 2 core cpu에 100% 부하를 준다. bash tee /etc/systemd/system/stress.service bash systemctl daemonreload systemctl enable stress 시스템 반영 후 재부팅 시에도 stress를 주도록 설정한다. 여기까지 했다면 이제 이미지(AMI)를 생성하고 시작 템플릿을 생성해준다. 1\. 로드 밸런서 생성 Internetfacing을 선택하고 2개의 가용 영역을 생성해주는데 퍼블릭 서브넷으로 설정을 해준다. 보안 그룹은 22번과 80번 포트가 열린 보안그룹을 선택한다. 여기서 대상 그룹을 생성해줘야 한다. 대상 그룹 생성 탭에 들어가 빈 타겟 그룹을 생성해준 뒤 돌아와서 생성한 대상 그룹을 선택해 준다. 2\. AutoScailing 그룹 생성 다음으로 AutoScailing 그룹을 생성해줘야 한다. 여기서도 2개의 가용 영역을 생성해주는데 서브넷은 프라이빗 서브넷에 위치시킨다. (EC2 인스턴스가 프라이빗 서브넷에 생성되어야 하므로) 기존의 로드 밸런서에 연결하는 설정을 해주고 위에서 만들었던 로드 밸런서 대상 그룹을 선택해준다. 지금부터가 중요하다. "ELB 상태 확인 켜기"를 체크해주고,  원하는 용량, 최소 용량, 최대 용량을 정의한 뒤 정책 이름을 설정해준다. 목표 CPU 사용률은 70%로 설정한다. 평균 CPU 사용률이 70%를 넘어서면 AutoScailing이 인스턴스를 늘려야겠다라고 판단해 새로운 인스턴스를 생성한다. 여기서 새로운 인스턴스는 기존에 생성된 인스턴스 스펙과 동일하게 생성된다. (아까 만든 시작 템플릿을 바탕으로) 추가로 모니터링 섹션에서 "그룹 지표 수집 활성화"를 체크해주면 CloudWatch에서 지표를 볼 수 있다. 최종적으로 88번 포트로 접속하면 잘 뜨는 것을 확인할 수 있다. 평균 CPU 사용률이 70%가 넘어가면 인스턴스가 최대 3개까지 늘어나도록 설정했다. 필요시 사용률이 줄어들면 인스턴스가 줄어들도록 scale in 정책을 만들 수 있다. 이 부분도 평균 CPU 사용률을 정의해주면 해당 값 이하로 떨어질 경우 AutoScailing이 인스턴스 갯수를 줄인다. CloudWatch 콘솔에 접속해 지표를 보면 CPU 사용률을 확인할 수 있다. (미처 CloudWatch에서 지표 확인 화면을 캡처하지 못해 비슷한 상황인 다른 사용자의 CloudWatch 이미지를 가져왔다.) 경보 알람이 발생하면 AutoScailing이 발생하고 인스턴스가 늘어나고 줄어드는 동작을 진행한다.  AutoScailing 흐름을 보면 이런 흐름이다. 지금까지 구성한 실습을 보면 이러하다. ALB가 퍼블릭 서브넷에 위치하여 외부 손닙을 맞이하는 입구 역할을 하고 EC2는 프라이빗 서브넷에 숨서어 실제 서비스를 제공한다. 외부 사용자는 ALB(88포트)로 들어오고, ALB는 내부망을 통해 EC2(80포트)로 연결을 토스한다. 인스턴스는 공인 IP가 없어도 ALB 덕분에 안전하게 서비스할 수 있다. 시작 템플릿은 서버를 만들 때 필요한 붕어빵 틀이라고 생각하면 편하다 AutoScailing 그룹을 생성하고 scale out(확장), scale in(축소) 정책을 생성해 CPU 부하에 따라서 시작 템플릿을 활용해 서버를 자동으로 생성하고 삭제한다. 이 과정에서 궁금증이 생겼다. "그럼 CPU 사용률이 떨어져 인스턴스가 삭제될 때 해당 인스턴스에 남아있는 사용자는 안 튕기나?" 삭제 결정이 내려진 서버에는 더 이상 새로운 사용자를 보내지 않는다.(배수 상태, Draining) 이미 접속 중인 손님이 볼일을 마칠 때까지 설정된 시간(기본 300초)동안 기다려 준다. 시간이 다 되면 사용자가 남아있더라도 연결을 끊고 서버를 종료한다. (이때 브라우저는 자동으로 다른 살아있는 서버에 재접속을 시도한다.) 그래서 사용자 입장에서는 잠깐의 딜레이가 발생하고 다른 큰 이슈 없이 사용할 수 있게된다. 좋은 인프라륵 구축하는 것도 중요하지만 이 속에서 "사용자가 장애를 느끼지 않게 하는 것"이 가장 큰 핵심인 것 같다. ALB와 AutoScailing의 조합은 단순한 자동화를 넘어 서비스의 연속성을 보장하는 강력한 도구인 것 같다.

May 12, 2026AWS
서버가 죽지 않도록... ELB와 Auto-Scailing 이해하기 (feat. AWS)

[Docker] Docker Swarm 정리

Docker를 사용하다 보면 단일 서버만으로는 한계가 생긴다. 트래픽이 몰리거나 서버가 죽었을 때 대응이 안 되기 때문이다. 이런 문제를 해결하기 위해 나온 게 바로 Docker Swarm이다. Docker vs Docker Compose vs Docker Swarm Docker — 단일 호스트, 단일 컨테이너 Docker Compose — 단일 호스트, 여러 종류의 컨테이너 Docker Swarm — 다수의 호스트(manager + workers)에 여러 종류의 컨테이너 여러 개의 호스트 묶음을 "클러스터" 라고 한다. docker swarm은 도커에서 만든 컨테이너 오케스트레이션 툴이다. 오케스트레이션이란? 오케스트레이션은 다수의 호스트로 구성된 클러스터에서 다수의 컨테이너를 관리하는 기술이다. (스케일링, 헬스체크, 로드밸런서(트래픽제어)) 혼자서 악기 하나를 연주하는 건 쉽지만, 수십 명의 연주자가 합주를 하려면 지휘자가 필요하다. docker swarm은 그 지휘 역할을 하는 것이다. 클러스터링 : 여러 대의 물리 서버를 하나의 가상 리소스로 통합한다. 스케일링 : 명령 한 줄로 컨테이너 개수를 수십 개로 늘리거나 줄인다. 상태 관리(Selfhealing) : 어떤 서버가 죽어서 컨테이너가 꺼지면 살아있는 다른 서버에 자동으로 그 컨테이너를 다시 살려낸다. 로드밸런싱(트래픽제어) : 들어오는 요청을 여러 서버에 떠 있는 컨테이너들에게 골고루 분산한다. docker swarm 클러스터 구성 실습에서 사용한 클러스터 구성은 아래와 같다. manager : 211.183.3.100 worker1 : 211.183.3.110 worker2 : 211.183.3.120 manager node 구성 manager 노드는 control plane 역할을 한다. manager에서 아래 명령으로 swarm을 초기화한다. bash docker swarm init 초기화 후 출력되는 join 토큰을 파일로 저장해두자. bash vi token manager 노드와 worker1,2 노드가 연결이 되었다. docker service 다수의 호스트에 한 가지 종류의 컨테이너를 한 개 이상 생성한다. docker run으로 컨테이너를 생성하는 것과 비슷한 개념이다. bash docker service create replicas 2 p 5858:80 name myweb oolralra/ipnginx replicas : 복제본, 컨테이너를 두 개 띄우겠다는 의미. ipnginx : 자신(컨테이너)의 IP를 출력하는 이미지. 로드밸런싱이 잘 되고 있다. bash docker service ps myweb 따로 컨테이너 배치 제한을 두지 않았기 때문에 현재 총 3개의 노드 중 아무 곳에 배치되었을 것이다. bash docker network inspect ingress docker swarm을 구성하는 순간, 오버레이 네트워크도 함께 구성된다. 오버레이 네트워크에 의해 다른 호스트에 있는 컨테이너들끼리도 서로 통신이 가능하다. 만든 web1과 web2 컨테이너를 하나로 묶어주는, 눈에는 보이지 않는 접속 포인트가 생긴다. (LB와 유사) docker stack deploy docker compose와 비슷하다. 컴포즈 파일이 필요하다. 기존의 컴포즈 파일과 거의 문법이 흡사하지만 조금 다른 부분이 있다. 스택이란 여러 종류의 서비스의 묶음이라고 생각하면 된다. bash mkdir /swarm cd /swarm vi dockercompose.yml yml services: ipnginx: image: oolralra/ipnginx deploy: replicas: 2 placement: constraints: [node.role != manager] 노드의 역할이 manager인 경우는 배치하지 않겠다. 워커노드에만 컨테이너를 배치하겠다. bash docker stack deploy c dockercompose.yml ipstack bash docker stack rm ipstack 스택 삭제 명령어. visualizer — 컨테이너 시각화 각 호스트의 컨테이너를 시각화하는 컨테이너 이미지 = visualizer bash vi visual.yml yml services: visual: image: 61.254.18.30:5000/visualizer ports: '5656:8080' volumes: /var/run/docker.sock:/var/run/docker.sock deploy: placement: constraints: [node.role == manager] bash docker stack deploy c visual.yml vis insecure 설정이 안 되어있으면 해주자. bash vi /etc/docker/daemon.json json { "insecureregistries": ["211.183.3.100:5000","61.254.18.30:5000"] } bash systemctl restart docker 211.183.3.100:5656으로 접속. 사설 저장소에서 이미지 가져오기 61.254.18.30:5000/ipnginx:latest 이미지로 컴포즈 파일을 수정해서 replicas 4개로 5959 포트로 publish 해보자. 저장소 수정 후 insecure 설정도 바꿔줘야 한다. 사설 저장소에 이미지가 존재함에도 불구하고 컨테이너가 띄워지지 않는다. 사설 저장소 insecure 설정이 manager 노드에만 되어있는데, 실질적으로 컨테이너 이미지를 땡겨오는 주체는 worker1과 worker2이기 때문이다. worker1과 worker2에도 insecure 설정을 해주면 된다. bash vi /etc/docker/daemon.json json { "insecureregistries": ["211.183.3.100:5000","61.254.18.30:5000"] } bash docker stack deploy c dockercompose.yml pstack ECR 퍼블릭 갤러리 이미지로 WordPress + MySQL 스택 배포 DB는 manager에 한 개만 띄우고, wordpress는 worker 노드에 2개를 띄워서 12345 포트로 접속 가능하게 해보자. bash vi wordpress.yml yml services: mywp: image: public.ecr.aws/docker/library/wordpress:php8.1apache ports: '12345:80' deploy: replicas: 2 placement: constraints: [node.role == worker] environment: WORDPRESSDBHOST: dbdb WORDPRESSDBUSER: wpuser WORDPRESSDBPASSWORD: '1234' WORDPRESSDBNAME: wpdb dependson: dbdb dbdb: image: public.ecr.aws/docker/library/mysql:8 deploy: placement: constraints: [node.role == manager] environment: MYSQLROOTPASSWORD=1234 MYSQLUSER=wpuser MYSQLPASSWORD=1234 MYSQLDATABASE=wpdb 잘 접속된다.

May 12, 2026Docker
[Docker] Docker Swarm 정리

[Kubernetes] Pod, 리소스, Ingress 정리

Pod란? 한 개 이상의 컨테이너로 구성된 쿠버네티스의 기본 단위 같은 Pod면 IP가 같다 같은 Pod 내 컨테이너들은 포트로 구분한다 Pod도 결국 컨테이너로 구성되어 있으며, 이 컨테이너를 실행하기 위해 docker, containerd, CRIO 같은 CRI(컨테이너 런타임)가 필요하다. 1\. kubeapiserver 내/외부의 모든 요청을 주고받는 서버. contoller나 scheduler,proxy의 주시대상. 호텔지배인. 2\. kubescheduler 생성될 리소스들을 어떤 노드에 '배치'할지 결정(스케쥴링) 호텔 로비 직원 3\. kubecontroller 다양한 리소스에 대한 여러가지 컨트롤러들이 존재한다. 원하는 상태(Desired state)에 현재 상태(current state)가 수렴하도록 지속적으로 모니터링. 문제가 생기면 고치거나 리소스를 재생성. ex) 하우스키퍼 4\. etcd 클러스터 및 모든 리소스에 대한 정보를 key:value 형태로 저장하는 일종의 데이터베이스. ex) 장부 모든 노드에 존재하는 컴포넌트 1\. kubelet 노드 관리자. 실질적으로 각 노드에 존재하는 리소스 관리 2\. kubeproxy 노드 안과 밖을 넘나드는 수직트래픽을 관리. 매니페스트 (yml 파일) 매니페스트란 내가 원하는 상태를 적어둔 명세서다. bash vi testpod.yml yml apiVersion: v1 kind: Pod metadata: name: test spec: containers: image: public.ecr.aws/docker/library/httpd:latest name: testcon bash kubectl apply f testpod.yml kubectl apply f는 docker compose up, docker stack deploy와 비슷하다. 내가 원하는 상태(Desired State)가 미리 정의된 매니페스트 파일을 구성해놓고 apply해서 반영한다. 매니페스트 수정 후 다시 apply하면 바로 변경사항을 반영시킬 수 있다. bash kubectl delete f testpod.yml 앞으로는 명령어로 직접 리소스를 생성하거나 지우지 말고 매니페스트 파일을 f 옵션을 통해 apply하거나 delete하도록 하자. label 중요 bash vi labels.yml yml apiVersion: v1 kind: Pod metadata: name: testlabelpod labels: app: myweb spec: containers: image: public.ecr.aws/docker/library/httpd:alpine name: testlabelcon label은 여러 개를 쓸 수 있다. bash kubectl apply f labels.yml 생성. bash kubectl describe pod testlabelpod 조회. label은 리소스를 컨트롤(원하는 상태, 현재 상태)하고 찾아가기 위한 용도다. 쿠버네티스의 다양한 리소스들 1\. pod 한 개 이상의 컨테이너로 구성된 쿠버네티스의 기본 배포 단위. 2\. replicaset Pod의 복제본 수를 유지해주는 리소스. 지정한 수만큼 Pod가 항상 실행되도록 보장한다. 3\. Deployment ReplicaSet을 관리하며 롤링 업데이트, 롤백 등을 지원하는 리소스. 실무에서 가장 많이 사용한다. 4\. namespace 클러스터 내에서 리소스를 논리적으로 분리하는 단위. 팀이나 프로젝트별로 격리된 환경을 만들 수 있다. 5\. Service (너무 중요!) 작은 로드밸런서라고도 할 수 있다. Pod에 접근하기 위한 고정된 엔드포인트를 제공하는 리소스이다. Pod는 재생성될 때마다 IP가 바뀌기 때문에 Service를 통해 안정적으로 접근한다. 서비스라는 리소스를 생성 시 하나의 접속지점이 생성된다. 이 서비스를 통해 모든 노드에 존재하는 pod에 트래픽을 인가할 수 있다. 인가하는 기준은 labels 를 통해 해당 pod를 특정하면 된다. 서비스는 다양한 종류(type)이 존재한다. 51. ClusterIP 타입 (svc의 default 타입) 클러스터 내부에서만 유효한 IP 내부 테스트 용도, 외부로 배포를 안하는 경우. 내부에 존재하는 서비스들끼리만 통신할 때. bash vi dep.yml yml apiVersion: apps/v1 kind: Deployment metadata: name: mydep spec: replicas: 3 selector: matchLabels: app: myweb template: metadata: labels: app: myweb spec: containers: image: public.ecr.aws/docker/library/httpd:alpine name: mywebcon bash kubectl apply f dep.yml svc를 연결할 deployment 생성. bash vi depsvc.yml 서비스 매니페스트 정의. yml apiVersion: v1 kind: Service metadata: name: svcmyweb spec: selector: app: myweb ports: port: 80 targetPort: 80 bash kubectl apply f depsvc.yml svc 생성. bash kubectl describe svc svcmyweb 자세한 정보 확인. Endpoint에 뜨는 pod들은 건강한 pod만 뜬다. 52. NodePort 타입 노드의 포트 서비스를 제공받는 사용자 입장에서 내부로 진입하여 pod에 접근하려면 노드 포트로 진입해야 한다. bash mkdir svc cd svc vi svcnodeport.yml yml apiVersion: v1 kind: Service metadata: name: svcdep spec: selector: app: mydep type: NodePort ports: nodePort: 30001 port: 80 targetPort: 80 apiVersion: apps/v1 kind: Deployment metadata: name: mydep spec: replicas: 1 selector: matchLabels: app: mydep template: metadata: labels: app: mydep spec: containers: image: public.ecr.aws/docker/library/httpd:alpine name: depcon bash kubectl apply f svcnodeport.yml bash kubectl describe svc svcdep 3개의 포트(NodePort, Port, TargetPort)가 각각 어떤 대상인지 구분할 수 있어야 한다. Port 서비스 포트 TargetPort pod NodePort Node 외부에서 노드로 통신만 된다면 노드 포트를 통해 pod로 접속 가능하다. 어떤 노드로 들어가는지는 중요하지 않고, NodePort로 접근하면 동일한 공간(오버레이 네트워크가 구성된 podnetwork)으로 들어간다는 사실을 인지하자. pod가 어떤 노드에 존재하는지는 신경 쓸 필요도 없고 중요하지도 않다. pod가 worker1에 띄워져 있어도 worker2의 노드 포트를 통해 접근 가능하다. 53. LoadBalancer 타입 클러스터 관리자의 도움이 필요한 서비스 타입. 쿠버네티스 외부 네트워크 대역의 IP를 자동으로 할당하고 관리해 주는 사설 로드밸런서 관리자가 MetalLB다. EKS 같은 쿠버네티스 클러스터의 경우 클라우드 서비스 제공자(AWS)가 LB를 제공해줄 수 있지만, 온프레미스에 구성한 클러스터는 그렇지 않다. 따라서 svc를 LoadBalancer 타입으로 만들었을 때 누군가는 LB를 생성하면서 노드 대역대의 IP를 할당해줘야 한다. 그 기능을 활성화하기 위해 MetalLB가 필요하다. bash vi configmetal.yml LB가 생성됐을 때 뿌려줄 IP 범위 설정. yml apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: firstpool namespace: metallbsystem spec: addresses: 211.183.3.200211.183.3.240 안겹치게 수정. LB가 부여받을 IP 범위 apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: example namespace: metallbsystem bash kubectl apply f configmetal.yml vi lbtom.yml yml apiVersion: v1 kind: Service metadata: name: svctom spec: selector: app: tom type: LoadBalancer ports: port: 80 targetPort: 8080 apiVersion: apps/v1 kind: Deployment metadata: name: deptom spec: replicas: 1 selector: matchLabels: app: tom template: metadata: labels: app: tom spec: containers: image: public.ecr.aws/docker/library/tomcat:10.1.40jre11 name: tomcon svc의 타입으로 LoadBalancer 타입을 지정해준다. nodePort는 삭제한다. bash kubectl apply f lbtom.yml 생성된 LB는 svc의 포트를 따라간다. Ingress (제일 중요!) path 기반 라우팅 (/board로 가면 board 앱으로, /login으로 가면 login 앱으로) 일반적인 svc는 path 기반 라우팅이 불가능하다. (LoadBalancer, NodePort, ClusterIP) 서비스는 한 종류의 라벨만 품을 수 있기 때문에 이런 한계가 발생한다. 합칠 수 없다 = 같은 주소가 될 수 없다. 여러 개의 컨테이너에 각 기능들을 구현하면 path로 라우팅이 가능해야 한다. 일반적인 svc는 path 기반 라우팅이 불가능하기 때문에 Ingress라는 리소스가 필요하다. Ingress를 구성하면 path 기반 라우팅이 가능하다. ex) www.naver.com/board 로 오면 svcboard라는 svc로 보내줘. ex) www.naver.com/login 으로 오면 svclogin이라는 svc로 보내줘. → 하나의 접속지점(www.naver.com)을 통해 여러 개의 svc를 구성할 수 있다. Ingress를 구성하기 위해서는 Ingress Controller가 필요하다. (AWS EKS에서는 Ingress Controller를 LoadBalancer Controller라고 부른다.) Ingress Controller 설치 bash vi deploy.yaml 필요한 부분 수정. (type 을 NodePort에서 LoadBalancer로 수정한다.) bash kubectl apply f deploy.yaml kubectl get svc n ingressnginx Ingress 매니페스트 작성 bash cp ../svc/ip.yml . vi ip.yml yml apiVersion: v1 kind: Service metadata: name: svcipnginx spec: selector: app: myipnginx ports: port: 80 targetPort: 80 apiVersion: apps/v1 kind: Deployment metadata: name: ipdep spec: replicas: 2 selector: matchLabels: app: myipnginx template: metadata: labels: app: myipnginx spec: containers: image: public.ecr.aws/docker/library/httpd:alpine name: ipcon bash kubectl apply f ip.yml kubectl describe svc svcipnginx 서비스가 정상인지 확인. bash vi ingip.yml yml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingip annotations: nginx.ingress.kubernetes.io/rewritetarget: / spec: ingressClassName: "nginx" rules: host: rapa.com http: paths: path: /httpd pathType: Prefix backend: service: name: svcipnginx port: number: 80 annotation : 추가적인 정보. labels와 비슷하지만 주로 부가적인 기능 명시. ingressClassName : ingresscontroller의 종류 중에 nginx 방식을 사용. rewritetarget: / → 비록 /httpd라는 경로로 들어왔더라도 실제 pod에서는 /라는 경로로 바꿔주는 기능. host : 영문주소. IP는 안됨. DNS 기능이 필요하다. 인증서가 있다면 https 통신도 가능하다. path : 한 종류의 앱. pathType: Prefix → /httpd로 접근하는 애들 전부. 서비스의 이름을 마치 주소처럼 사용하고 있다. bash kubectl apply f ingip.yml ingress를 describe 했을 때 endpoint들(pod들)이 잘 떠있는 것만 봐도 ingresssvcpod가 잘 연결되어 있는 걸 어느정도 확인할 수 있다. 원래는 DNS를 통해서 rapa.com에 해당하는 IP를 매핑시켜줘야 하지만, 미니 DNS인 /etc/hosts 파일을 사용하자. bash vi /etc/hosts Ingress 트래픽 흐름 쿠버네티스에서 외부 사용자가 도메인 이름(rapa.com)을 치고 들어와서 실제 앱(pod)까지 도달하는 ingress 트래픽 흐름. rapa.com → ingresscontroller의 svc ExternalIP(DNS) → ingress(path 기반 라우팅) → svc → pod

May 12, 2026kubernetes
[Kubernetes] Pod, 리소스, Ingress 정리

[Docker] Compose 파일 작성법과 주요 옵션 총정리

Docker compose 도커(docker run)의 경우에는 단일 컨테이너를 실행한다. 도커 컴포즈는 여러 종류의 컨테이너를 한번에 띄울 수 있다. ex) 도커로 3tier를 구성한다면, docker run 을 3번 해서 webwasdb 컨테이너를 각각 구성해야 하고 —link도 구성해줘야 한다. 하지만 docker compose의 경우엔 3종류의 컨테이너를 컴포즈 파일 하나에 서 동시에 구성이 가능하다. 컨테이너를 띄우는 목적은 뭘까? 서비스를 제공하고 싶기 때문이다. 컨테이너 안에 앱, 애플리케이션이 들어있다. 도커 컴포즈에서는 이런 한 종류의 컨테이너를 service라고 지칭한다. mkdir /com cd /com vi dockercompose.yml dockercompose.yml 이라는 파일명 = 디폴트값 한 개의 서비스로 구성된 도커 컴포즈 파일 편집 services: 내가 띄울 다양한 종류의 컨테이너들 webserver: 서비스의 이름. 내가 원하는대로 정하면 됨. image: nginx ports: '8787:80' publish 기능 networks: webnet 아직 만들진 않았다. 밑에서 만들예정. 놓여질 네트워크 networks: webnet: webnet이라는 이름의 네트워크 생성. 샘플 코드. docker compose up 잘 동작한다. docker compose down 도커 컴포즈 삭제 docker compose up d docker run 과 비슷하게 d (detach) 백그라운드로 동작할 수 있다. 볼륨 구성 호스트의 현재 디렉토리에 vtest라는 디렉토리를 구성한 후 간단한 인덱스 파일을 생성하여 컨테이너에 볼륨 구성을 통해 웹루트디렉토리에 넣어주고 싶다. mkdir ./vtest 경로 생성. echo composevolumetest vtest/index.html 인덱스 파일 생성. vi dockercompose.yml services: 내가 띄울 다양한 종류의 컨테이너들 webserver: 서비스의 이름. 내가 원하는대로 정하면 됨. image: nginx ports: '8787:80' publish 기능 networks: webnet 아직 만들진 않았다. 밑에서 만들예정. 놓여질 네트워크 volumes: ./vtest:/usr/share/nginx/html 호스트의 ./vtest를 컨테이너의 /usr/share/nginx/html에 마운트 networks: webnet: webnet이라는 이름의 네트워크 생성. docker compose up d 마운트가 잘 되었다. dockercompose.yml 옵션들에 대해 알아보자. networks: 네트워크 정의 및 선택 ports: \p, publish 옵션 volumes: \v, 호스트와 컨테이너 마운트 command: CMD environment: \e, 환경변수 dependson: 의존성 설정(컨테이너 띄울 순서)도 가능. ex) node.js 앱(todo, weather, chat)의 경우 몽고디비가 연동이 안되면 앱 자체가 죽어버린다. node.js 서비스를 정의하면서 몽고디비를 depends\on 으로 걸어줘야 한다. /com의 ubun 이라는 경로에서 위 명령어를 활용하여 ubuntu:latest를 동작시키는 compose 파일을 만들어 보자. 네트워크 : ubun\net mkdir ubun cd ubun/ vi dockercompose.yml services: ubun: image: ubuntu:latest networks: ubunnet command: "sleep infinity" networks: ubunnet: docker compose up d docker compose down environment: ENVTEST=test ubun 서비스에 환경변수 추가. docker compose up d docker exec it ubunubun1 bash 환경변수가 잘 들어있다. 도커 컴포즈에서는 —link 를 굳이 명시하지 않아도 서비스의 이름으로 찾아갈 수 있다. wordpress 구성하기! mysql:8, wordpress:latest를 베이스 이미지로 하여, Dockerfile은 쓰지않고, dockercompose.yml 파일을 구성해보자. publish 포트는 wordpress는 1234로 하고, mysql는 따로 publish 하지 않는다. mkdir wp cd wp vi dockercompose.yml services: wp: image: wordpress:latest ports: '1234:80' environment: WORDPRESSDBHOST=dbdb WORDPRESSDBNAME=wpdb WORDPRESSDBUSER=wpuser WORDPRESSDBPASSWORD=1234 networks: wpnet dependson: dbdb dbdb: image: mysql:8 environment: MYSQLROOTPASSWORD: '1234' MYSQLUSER: 'wpuser' MYSQLPASSWORD: '1234' MYSQLDATABASE: 'wpdb' networks: wpnet networks: wpnet: 성공! 기존에 존재하는 네트워크를 사용하고 싶을 때 docker network create mynet driver=bridge 네트워크 생성. vi dockercompose.yml 172.19.0.0 대역에 생성됐다. 빌드한 이미지로 서비스 구성 mkdir build cd build/ vi Dockerfile FROM httpd:latest 도커 파일 생성. vi dockercompose.yml services: myweb: build: . dockercompose.yml 파일이 있는 경로(.)의 Dockerfile을 빌드 도커 컴포즈 파일 생성. docker compose up d echo buildtest index.html index.html 파일 생성. vi Dockerfile 위에서 생성한 index.html 파일을 넣어보자. FROM httpd:latest WORKDIR /usr/local/apache2/htdocs COPY index.html index.html docker compose up d curl localhost:5959 기본 페이지가 뜨고 dockercompose up d 를 했지만 이미지가 빌드되지 않았다. 이미 빌드를 한 적이 있어서 빌드된 이미지가 존재하면 다시 빌드하지 않는다. docker compose down 컨테이너 종료. docker compose up d build —build 옵션을 명시해야 계속 빌드한다. 다시 curl을 찍어보면, 수정한 Dockerfile 대로 다시 빌드가 되었다. docker compose down 컨테이너 종료. 이번엔 빌드를 하 되, 도커파일을 지정해줘보자. vi dockercompose.yml services: myweb: build: dockercompose.yml 파일이 있는 경로(.)의 Dockerfile을 빌드 context: . 디렉토리 지정 dockerfile: Dockerfile 도커파일의 이름 지정. image: myimg:1 빌드해서 나온 이미지의 이름. 단독으로 쓰일때와는 의미가 좀 다르다. ports: '5959:80' context: 도커가 빌드를 시작할 때 참고할 작업 폴더의 위치. context: . 으로 되어있으면 도커는 현재 폴더에 있는 Dockerfile, 소스코드, package.json 등을 모두 빌드 컨텍스트 라는 임시 공간에 담는다. 위 코드 중 context, build 부분 해석 → Dockerfile 란 이름의 도커파일로 빌드를 하는데, 지정한 디렉토리(현재 디렉토리)의 모든 내용을 빌드해라. docker compose scaling —scaling = 로 서비스를 구성하는 컨테이너의 수를 변경할 수 있다. dockercompose.yml 파일에 포트 범위를 명시해준다. 하나의 접속 지점을 통해 로드밸런싱이 됐으면 좋겠지만 docker compose 에서는 그게 불가능하다. 서비스를 구성하는 컨테이너마다 호스트의 포트를 1:1 로 맵핑시켜야 한다. 왜? 도커 컴포즈에서 scale 옵션을 통해 컨테이너 개수를 늘리는 것은 동일한 서비스를 여러 개 복제하여 실행하는 기능에 불과하다. 이 방식은 단순히 물리적인 숫자를 늘려줄 뿐, 외부에서 들어오는 트래픽을 각 컨테이너의 상태나 부하 정도에 맞춰 지능적으로 분산해주는 전문적인 로드밸런싱 기능을 포함하지 않는다. 따라서 여러 개의 컨테이너를 띄우더라도 특정 컨테이너에만 요청이 몰리거나, 장애가 발생한 컨테이너로 접속이 시도되는 등의 한계가 존재한다. 이를 해결하기 위해서는 Nginx나 HAProxy와 같은 별도의 로드밸런서 서비스를 도커 컴포즈 설정에 추가하여 앞단에 배치해야 한다. 전문 로드밸런서는 들어오는 모든 요청을 단일 지점에서 받아 각 컨테이너로 골고루 전달하고, 컨테이너의 생존 여부를 실시간으로 확인하여 정상적인 곳으로만 트래픽을 보내는 가이드 역할을 수행한다. 결과적으로 효율적인 트래픽 분산과 안정적인 서비스 유지를 위해서는 단순한 개수 확장보다는 로드밸런서를 통한 체계적인 관리가 필수적이다.

May 12, 2026Docker
[Docker] Compose 파일 작성법과 주요 옵션 총정리

ECS vs Docker Swarm 컨테이너 오케스트레이션 비교 프로젝트

ECS vs Docker Swarm 컨테이너 오케스트레이션 비교 프로젝트 "성능 비교에서 운영 철학 비교로 — 실패가 아닌 발견의 과정" 프로젝트 개요 항목 내용 프로젝트 유형 인프라 비교 실험 기간 2026년 4월 목표 동일 EC2 환경에서 Docker Swarm과 Amazon ECS의 성능·운영 특성 비교 핵심 기술 Docker Swarm, Amazon ECS(EC2 Launch Type), AWS ALB, Prometheus, Grafana, k6 인프라 EC2 t3.small × 2대, ALB, VPC(퍼블릭 서브넷 단일 구성) 초기 목표 CPU 부하 상황에서의 오토스케일링 반응 속도 비교 컨테이너 강제 종료 후 복구 시간(Recovery Time) 측정 동일 애플리케이션 · 동일 부하 시나리오 기반의 공정한 비교 시스템 구성 공통 환경 EC2 t3.small × 2대 (각 2GB RAM) ├── 애플리케이션 컨테이너 (Spring Boot /env, /stress, /health) ├── cAdvisor (컨테이너 메트릭 수집) └── nodeexporter (시스템 메트릭 수집) 모니터링: Prometheus + Grafana 부하 생성: k6 (rampingarrivalrate 방식) 로그 수집: Python autoscaletest.py (2초 간격 컨테이너 수 추적) Docker Swarm 구성 Manager Node (클러스터 제어 + ingress 담당) Worker Node 1 ─┐ Worker Node 2 ─┘ → 앱 컨테이너 실행 (2 replicas) 네트워크: bridge / overlay 스케줄링: 로컬 Besteffort 방식 Amazon ECS 구성 ECS Cluster ├── EC2 인스턴스 1 (ECS Agent + 앱 Task + cAdvisor) └── EC2 인스턴스 2 (ECS Agent + 앱 Task + cAdvisor) Task 스펙: 0.25 vCPU / 0.5GB → 0.5 vCPU / 0.5GB (진행과정에서 변경) 네트워크: bridge → awsvpc 전환 ALB + Target Group(IP 타입) + Health Check(/health) 오토스케일링: CloudWatch CPU 70% 초과 시 Task 증가 비교 공정성 가설 및 검증 가설 — "Swarm 3대 vs ECS 2대가 공정한 비교다" 두 플랫폼을 같은 인스턴스 수(예: 둘 다 2대)로 맞추는 것이 얼핏 공정해 보이지만, 실제 애플리케이션을 처리하는 데이터 플레인 자원을 기준으로 보면 다르다. Docker Swarm은 Manager 노드가 클러스터 제어(오케스트레이션) 및 ingress 트래픽을 담당하므로, 앱 컨테이너는 Worker 2대에만 배치된다. Amazon ECS는 ECS의 Control Plane이 AWS 관리 영역에 존재하므로, 별도의 Manager 인스턴스가 불필요하다. EC2 2대 모두 앱 Task를 실행하는 워커 역할을 한다. 따라서 공정한 비교 기준은 "앱을 실제로 처리하는 인스턴스 수" 이며, 이 관점에서: Swarm: Manager 1대 + Worker 2대 (총 3대) ↔ ECS: EC2 2대 (총 2대) 양쪽 모두 앱 처리 인스턴스는 2대로 동일하다. 하지만 이 가설이 성립하려면 한 가지를 검증해야 했다. "Swarm Manager가 앱 컨테이너를 실행하지 않고, 실제 처리 부하는 Worker 2대가 전담하는가?" 검증 — 노드별 리소스 사용 현황 측정 Grafana + nodeexporter로 유휴 상태에서 각 노드의 리소스 사용량을 측정했다. 노드 역할 CPU Busy Sys Load RAM 사용량 비고 :::::: Swarm Manager 3.1% 12.5% 25.3% CPU/Load 가장 높음 → 제어 부담 Swarm Worker 1 2.5% 28.5% 33.1% Sys Load 매우 높음 → 실제 연산 수행 Swarm Worker 2 2.5% 4.5% 34.0% 메모리 점유 높음 → 앱 컨테이너 실행 ECSClu 1 2.0% 1.0% 34.9% 안정적 유휴 상태 ECSClu 2 2.1% 2.5% 34.9% ECSClu 1과 거의 동일 패턴 검증 결과 — 가설 입증 측정 결과는 가설을 명확히 지지한다. Manager: RAM 25.3%로 Worker(3334%)보다 유의미하게 낮고, Sys Load도 낮다. 앱 컨테이너를 실행하지 않고 클러스터 제어 부담만 지고 있음이 확인됐다. Worker 1·2: RAM 3334% 점유 및 높은 Sys Load → 실제 앱 컨테이너들이 Worker 노드에 배치되어 자원을 점유하고 있음. ECSClu 1·2: RAM 34.9%로 Swarm Worker와 유사한 수준 → 동일한 앱 처리 부담을 지고 있음. 결론: "Swarm 3대(Manager + Worker 2) vs ECS 2대"는 공정한 비교다. 앱을 실제로 처리하는 데이터 플레인은 양쪽 모두 2대이며, ECS는 관리형 서비스이므로 Manager 인스턴스 비용 없이 동일한 처리 용량을 확보할 수 있다. 실험 1 — 컨테이너 장애 복구 테스트 테스트 방법 runrecoverytest.py 스크립트를 각 노드에 대해 실행해 측정을 자동화했다. SSH로 원격 접속하여 docker kill로 컨테이너를 강제 종료하고, Prometheus API를 5초 간격으로 폴링해 컨테이너 수가 다시 EXPECTEDREPLICAS(1) 이상으로 복구되는 시점을 감지한다. [측정 흐름] kill 명령 실행 (killepoch 기록) → 5초 간격 Prometheus 폴링 (containerstarttimeseconds 기반) → 새 컨테이너의 starttime이 killepoch 이후임을 확인 → 복구 시각 기록 (recoveryseconds = recoveredepoch killepoch) → 결과를 recoverysummary.csv / recoveryraw.csv 에 저장 Prometheus 쿼리 구조 Swarm: containerlabelcomdockerswarmservicename="myapp" 으로 서비스 단위 집계 ECS: name="ecstoy." 정규식으로 Task 컨테이너 집계 복구 판단: max(containerstarttimeseconds{...}) 값이 kill 시점 이후로 갱신되었는지 확인 Docker Swarm Test ※ 그라파나 대시보드의 시간축은 UTC 기준으로 표시되며, 실제 테스트 실행 시각(KST, UTC+9)과 9시간 차이가 있습니다. ECS Test ※ 그라파나 대시보드의 시간축은 UTC 기준으로 표시되며, 실제 테스트 실행 시각(KST, UTC+9)과 9시간 차이가 있습니다. 전체 측정 결과 (recoverysummary.csv) 총 8회 테스트 수행 (Swarm 4회, ECS 4회). 테스트 ID 플랫폼 kill 시각 복구 시각 복구 시간(초) 결과 :: swarmworker120260406 Swarm 14:32:27 14:32:46 19.08초 ✅ success swarmworker220260406 Swarm 15:34:42 15:35:01 19.19초 ✅ success ecs120260407 ECS 02:42:49 — timeout ❌ fail ecs220260407 ECS 03:19:07 03:30:25 677.63초 (약 11분) ✅ success swarmworker120260411 Swarm 13:50:25 13:50:46 21.20초 ✅ success swarmworker220260411 Swarm 13:54:44 13:55:05 21.21초 ✅ success ecs120260411 ECS 13:56:26 14:13:54 1047.99초 (약 17분) ✅ success ecs220260411 ECS 14:17:25 14:40:39 1394.02초 (약 23분) ✅ success 플랫폼별 요약 구분 측정 횟수 평균 복구 시간 범위 성공률 ::::: Docker Swarm 4회 약 20초 19.08 21.21초 4/4 (100%) Amazon ECS 4회 약 1,040초 (약 17분) 677초 1,394초 (1회 timeout 제외) 3/4 (75%) Swarm은 4회 모두 1921초로 일관된 복구를 보인 반면, ECS는 677초1,394초로 회차마다 편차가 크고, 1회는 180초 timeout 내에 복구 자체를 실패했다. ECS timeout 케이스(ecs120260407)는 스크립트 제한(180초) 때문에 실패로 기록됐지만, 이후 테스트에서 같은 노드가 1,047초 만에 복구된 것으로 보아 실제 복구는 이뤄졌으나 측정 범위를 벗어난 것으로 판단된다. 원인 분석 (ECS 복구 지연) ECS의 복구 지연은 단순한 성능 문제가 아니라 구조적 설계 차이에서 비롯된다. ① ALB Health Check 대기 (가장 큰 원인) 체크 주기: 기본 30초 정상 임계 횟수: 기본 5회 최소 소요: 30초 × 5회 = 150초(2분 30초) ② Deregistration Delay (Connection Draining) 기존 컨테이너 등록 해제 대기: 기본 300초(5분) 이론 합계: 5분 + 2분 30초 ≈ 7분 30초 실측값(677초1,394초)은 이 이론치보다도 훨씬 큰데, 이는 테스트 당시 EC2 자원 여유 부족으로 새 Task 생성 자체가 지연됐기 때문으로 추정된다. ③ ECS Agent 폴링 주기 EC2 내 ECS Agent는 실시간 Push가 아닌 주기적 Poll 방식으로 AWS Control Plane과 통신 Swarm 대비 수초수십초 추가 지연 발생 ④ ECS 회차별 편차의 원인 실험 당시 EC2 t3.small(2GB)에 ECS Agent, cAdvisor, nodeexporter가 상주 중이었고, 새 Task 생성 시 메모리 여유가 부족한 경우 배치 자체가 지연됐다. 이는 ECS의 Resource reservation 기반 스케줄링 특성상, 자원이 확보되지 않으면 배치를 아예 거부하는 구조에서 비롯된다. 최적화 가능 방향 설정 항목 기본값 최적화 값 효과 Deregistration Delay 300초 30초 이하 대기 시간 대폭 단축 Health Check Interval 30초 10초 확인 주기 단축 Healthy Threshold 5회 2회 통과 시간 20초로 단축 EC2 메모리 여유 확보 Task 0.25vCPU/0.5GB 인스턴스 업그레이드 or Task 스펙 조정 Task 배치 지연 해소 실험 2 — CPU 부하 오토스케일링 테스트 부하 시나리오 (k6 stresstest.js) 시나리오: rampingarrivalrate 워밍업(30→2분) → CPU 상승(60→3분) → 70% 근접(80→3분) → 유지(80, 5분) → 추가 확장(100, 3분) → 유지(100, 5분) 오토스케일링 트리거: CPU Utilization 70% 초과 Docker Swarm 오토스케일링 결과 이벤트 시각 내용 ScaleOut 10:56:15 2 → 4 컨테이너 증가 Stabilization 10:56:15 이후 count=4로 안정 유지 ScaleIn 11:00:15 4 → 2 컨테이너 감소 SelfHealing 실험 중 1→0→1 컨테이너 재생성 확인 Amazon ECS 오토스케일링 결과 및 이슈 ECS에서는 의도한 ScaleOut 대신 SelfHealing(Task 교체) 현상이 반복됐다. 증상 /stress 요청으로 CPU 100% → /health 지연 → ALB Health Check 실패 ECS가 Unhealthy Task를 종료 후 새 Task 생성 (Desired Count 유지) container=error 로그 반복 확인 원인 부하가 한쪽 컨테이너에 집중 → 전체 평균 CPU가 낮아 오토스케일링 미충족 bridge 모드 고정 hostPort(80) → 동일 인스턴스에 복수 Task 배치 불가 조치 및 awsvpc 전환 후 네트워크 모드: bridge → awsvpc 전환 Target Group 타입: instance → IP 타입 재생성 부하 방식: constantarrivalrate → rampingarrivalrate로 변경 전환 후 2→3 증가는 확인됐으나 Task 1개가 종료되는 현상 재발. 실험 시간 제약으로 완전한 안정화 달성 전 종료. 주요 트러블슈팅 기록 T1. Bridge 모드 고정 hostPort Scaleout 실패 내용 증상 Desired Count 34로 변경해도 Running Task는 2개에서 증가 안 함 원인 hostPort=80 고정 시 EC2 한 대에 동일 포트 컨테이너 2개 배치 불가 조치 hostPort=0(동적 포트) 변경 → 이후 awsvpc + IP Target Group으로 전환 T2. Target Group 설정 오류 → Health Check 실패 → SelfHealing 오해 발생 흐름 bridge 모드에서 hostPort 고정 → Scaleout 시도 시 포트 충돌로 신규 Task 배치 실패 → awsvpc 전환 결정 → 기존 instance 타입 Target Group을 ip 타입으로 변경 시도 → AWS 정책상 불가(immutable) → Target Group 신규 생성했으나 포트 설정 미스 → ALB가 Task를 Unhealthy로 판정 → ECS가 Unhealthy Task 종료 후 새 Task 생성 반복 → Task가 죽었다 살아나는 걸 보고 Scaleout으로 오해 → 실제로는 SelfHealing(장애 교체) 이었음 내용 증상 Scaleout을 기대했으나 기존 Task가 종료되고 새 Task가 생성되는 패턴 반복 근본 원인 instance 타입 Target Group → awsvpc 전환 후 ip 타입으로 재생성 필요. AWS ALB Target Type은 생성 후 변경 불가(immutable)이므로 신규 생성했으나 포트 설정 불일치로 Health Check 계속 실패 조치 Target Group ip 타입으로 재생성 (port 8080, /health) → ALB Security Group에서 8080 인바운드 허용 확인 → /stress?timeout=10 → timeout=1로 부하 완화 T3. Grafana 컨테이너 수 과다 표시 내용 증상 ECS Running Task 2개인데 Grafana에서 45개로 표시 원인 awsvpc 모드에서 Task마다 internalecspause 컨테이너 자동 생성 조치 PromQL에 containerlabelcomamazonawsecscontainername="toysb" 필터 추가 비교 분석 핵심 지표 비교 항목 Docker Swarm Amazon ECS 설계 목적 경량 오케스트레이션 클라우드 운영 플랫폼 복구 시간 약 21초 약 7분 (기본 설정) 스케일링 반응 즉시 (로컬 스케줄러) 수십초수분 (CloudWatch 기반) 스케줄링 방식 Besteffort (공격적) Resource reservation (보수적) 오버커밋 가능 거의 불가 네트워크 오버헤드 낮음 (bridge/overlay) 높음 (ENI, ALB, Target Group) 운영 복잡도 낮음 높음 안정성 상대적으로 낮음 높음 모니터링 통합 직접 구성 필요 CloudWatch 기본 연동 왜 ECS가 느린가 (구조적 이해) [Docker Swarm 복구 흐름] 컨테이너 종료 감지 (Docker Engine 내장) → 즉시 재스케줄링 (로컬 의사결정) → 컨테이너 Running → 즉시 트래픽 수신 ≈ 수초20초 [Amazon ECS 복구 흐름] Docker Engine 장애 감지 → ECS Agent 전달 → AWS Control Plane 보고 (네트워크 왕복) → 새 Task 생성 → ENI 할당 → ALB Target Group 등록 → Health Check 대기 (30초 × 5회 = 150초) → Deregistration Delay 대기 (300초) ≈ 7분+ ECS의 느림은 "안전한 트래픽 전환을 보장하는 설계"의 결과다. 💡 프로젝트를 통해 얻은 핵심 인사이트 1. 동일 환경 비교의 어려움 — ECS는 불리한 조건에서 측정됐다 ECS를 Swarm 수준에 맞추기 위해 세팅 초기에 Task 리소스를 0.25 vCPU/0.5GB로 제한하자 컨테이너 불안정, 메모리 부족, GC 문제가 발생했다. "비교를 공정하게 만들려다 ECS의 장점을 모두 제거하는" 역설을 경험했다. 이번 실험에서 측정된 ECS의 느린 복구 시간(677초1,394초)은 ECS 자체의 한계가 아니라 실험 조건의 한계다. [이번 실험의 ECS 조건 — 불리한 환경] EC2 t3.small (2GB) 위에 ECS Agent + cAdvisor + nodeexporter 상주 → Task 리소스를 0.25vCPU/0.5GB로 억지로 제한 → 메모리 여유 부족으로 새 Task 배치 자체가 지연 → Swarm 비교를 위해 ECS 본래 설계 방식을 벗어난 구성 [ECS 본래 용도로 사용했다면] Fargate 사용 → EC2 관리 불필요, AWS가 미리 준비한 자원 풀에서 즉시 컨테이너 주입 → Task 배치 지연 없음 → Health Check 설정 튜닝 (Interval 10초, Threshold 2회) 시 복구 시간 20초대도 가능 즉, "ECS가 느리다"가 아니라 "EC2 위에 억지로 올린 ECS가 느렸다" 는 것이 정확한 표현이다. Fargate로 ECS 본래 용도대로 운영했다면 복구 속도 자체는 Swarm과 크게 다르지 않을 수 있다. 2. 비교 관점의 전환 Before After 단순 성능 비교 (속도) 운영 전략 비교 (적합한 환경의 차이) "ECS가 느리다" "EC2 위에 억지로 올린 ECS가 느렸다" "Swarm이 더 좋다" "온프레미스엔 Swarm, 클라우드엔 ECS(Fargate)" 3. 실무 관점에서의 선택 기준 ECS가 Swarm보다 낫다거나, 기업이 Swarm을 버리고 ECS를 선택한다는 식의 결론은 정확하지 않다. 실제로는 인프라 환경(온프레미스 vs 클라우드 온디맨드) 에 따라 각각이 적합한 상황이 다르다. 상황 적합한 선택 이유 온프레미스 / 자체 서버 보유 Docker Swarm 클라우드 벤더 종속 없이 직접 소유한 서버에서 경량 오케스트레이션 운영 가능. AWS API 호출 비용·지연 없음. AWS 클라우드 온디맨드 Amazon ECS ALB·CloudWatch·IAM·VPC 등 AWS 에코시스템과 완전 통합. 인프라를 직접 소유하지 않고 사용한 만큼만 과금. 클라우드지만 벤더 독립이 중요 Docker Swarm 멀티클라우드 또는 이식성이 중요한 환경에서는 Swarm이 유리. 서버리스 지향, 인프라 관리 최소화 ECS (Fargate) EC2 자체를 관리할 필요 없이 컨테이너 단위로 과금·운영 가능. 이 프로젝트를 통해 깨달은 핵심은, "어느 쪽이 더 좋은가"가 아니라 "우리 인프라 환경이 온프레미스냐 클라우드냐에 따라 자연스럽게 선택이 갈린다" 는 점이다. 온프레미스 서버를 보유한 조직이 굳이 ECS를 쓸 이유가 없고, AWS 위에서 운영하는 조직이 굳이 Swarm을 직접 관리할 이유도 없다. 4. 트러블슈팅 역량 단순 기능 구현을 넘어, 네트워크 모드 선택이 확장성에 미치는 영향, ALB Health Check가 복구 시간을 결정하는 메커니즘, ECS Agent 폴링 방식의 한계 등 인프라 내부 동작 원리를 실험을 통해 직접 검증했다. 기술 스택 구분 기술 컨테이너 오케스트레이션 Docker Swarm, Amazon ECS (EC2 Launch Type) 애플리케이션 Spring Boot (Java) 인프라 AWS EC2 (t3.small), ALB, VPC, Target Group 모니터링 Prometheus, Grafana, cAdvisor, nodeexporter 부하 테스트 k6 (rampingarrivalrate), Python (autoscaletest.py) 네트워크 bridge, overlay, awsvpc, ENI 회고 및 결론 본 프로젝트는 "완벽한 비교 실험"보다 더 가치 있는 것을 남겼다. 단순히 수치를 비교하는 과정에서, 두 시스템이 서로 다른 문제를 풀기 위해 설계됐다는 점을 발견했다. ECS를 Swarm처럼 다운그레이드해서 비교하는 것 자체가 잘못된 접근이었음을 실험 도중 깨달았고, 그 깨달음 자체가 이 프로젝트의 핵심 성과다. Docker Swarm은 빠르고 단순한 오케스트레이터, Amazon ECS는 느리지만 안정적인 클라우드 운영 플랫폼이다.

May 11, 2026오케스트레이션
ECS vs Docker Swarm 컨테이너 오케스트레이션 비교 프로젝트

On-Premise K8s × AWS 하이브리드 인프라 구축 및 GitOps CI/CD 자동화

OnPremise K8s × AWS 하이브리드 인프라 구축 및 GitOps CI/CD 자동화 프로젝트 기간: 2026.04.27 – 2026.04.30 기술 스택: Kubernetes · Helm · ArgoCD · GitHub Actions · Amazon ECR · AWS · Tailscale 역할: 온프레미스 K8s 클러스터 세팅 / 하이브리드 네트워크 설계 / 초기 매니페스트 작성 / Helm Chart 고도화 / CI/CD 파이프라인 구축 목차 1. 프로젝트 선정 이유 2. 프로젝트 개요 3. 전체 아키텍처 4. 하이브리드 네트워크 설계 (Tailscale) 5. 온프레미스 K8s 클러스터 구성 6. 초기 매니페스트 작성 7. Helm Chart 고도화 8. ECR 프라이빗 레지스트리 연동 트러블슈팅 9. Pod → RDS 접근 트러블슈팅 10. ArgoCD 기반 CI/CD 파이프라인 구축 11. 배포 검증 결과 12. 회고 및 개선 방향 1. 프로젝트 선정 이유 AWS Cloud School 교육 과정에서 네트워크, 리눅스, AWS, Docker, Kubernetes를 순차적으로 학습한 뒤, "배운 기술을 실제 운영 환경에 가깝게 통합해보자" 는 목표로 이 프로젝트를 기획했습니다. 단순히 로컬에서 K8s를 돌리는 것이 아니라, 다음과 같은 실무에서 마주할 수 있는 제약 조건을 의도적으로 설정했습니다: 제약 조건 설명 이중 NAT 환경 학원 공유기 → VMware NAT, 공인 IP 없음 포트 포워딩 불가 네트워크 관리 권한 없음 Private RDS 연동 필요 보안 관점에서 RDS 퍼블릭 노출은 안티패턴 멀티 환경 운영 Dev/Prod 분리 + 동일 Chart 재사용 이 제약을 해결하는 과정 자체가 클라우드/DevOps 엔지니어에게 요구되는 핵심 역량(네트워크 설계, IaC, GitOps, 트러블슈팅)을 증명할 수 있다고 판단했습니다. 2. 프로젝트 개요 단순히 애플리케이션을 컨테이너로 실행하는 수준을 넘어서, 실제 운영 환경에 가까운 배포 자동화 구조를 직접 설계하고 구현하는 것을 목표로 했습니다. 온프레미스 VMware 환경에 Kubernetes 클러스터를 직접 구성하고, 원시 YAML 매니페스트에서 출발해 Helm Chart로 고도화한 뒤 GitHub Actions → ECR → ArgoCD 로 이어지는 GitOps 기반 CI/CD 파이프라인까지 완성했습니다. 핵심 목표: 쿠버네티스 핵심 리소스(Deployment, Service, Ingress, Namespace)를 실제 클러스터에서 직접 경험 온프레미스 ↔ AWS VPC 하이브리드 네트워크 연결 (Tailscale Subnet Router) Prod / Dev 환경을 노드 단위로 분리하여 운영 안정성 확보 Helm Chart로 환경별 설정을 코드로 관리 GitOps 방식으로 배포 이력 추적 및 자동화 3. 전체 아키텍처 CI/CD 흐름: 개발자 코드 Push │ ▼ GitHub Actions (CI) │ Docker 이미지 빌드 │ Amazon ECR Push ▼ ArgoCD (CD / GitOps) │ Git 상태 감지 → Sync ▼ OnPremise K8s 클러스터 (VMware 211.183.3.0/24) ├── Master Node (211.183.3.200) ├── Prod: worker1 (211.183.3.210) + worker2 (211.183.3.220) └── Dev: devworker (211.183.3.230) 외부 접근 흐름: 사용자 → Route53(prod.dongkyu.cloud) → EC2 nginx 리버스 프록시 → Tailscale 터널 → K8s NGINX Ingress Controller (NodePort 31018) → Service → Pod 4. 하이브리드 네트워크 설계 (Tailscale) 41. 환경 제약과 네트워크 방안 비교 온프레미스 K8s 클러스터에서 AWS Private VPC의 리소스(RDS 등)에 접근해야 했지만, 학원 환경은 이중 NAT 구조로 공인 IP가 없었습니다. 방안 설명 채택 여부 이유 AWS SitetoSite VPN IPsec 터널 불가 고정 공인 IP + VPN 장비 필요 AWS Direct Connect 전용선 불가 물리적 전용선, 월 수십만 원 VPC Endpoint (PrivateLink) 프라이빗 접근 부적합 온프레미스→AWS 방향 해결 불가, NLB 비용 월 $25+ Public RDS + IP 허용 RDS 퍼블릭 노출 부적합 보안 안티패턴, 이중 NAT로 공인 IP 변동 Tailscale Subnet Router WireGuard 메시 VPN 채택 outbound HTTPS만으로 동작, 무료 42. Tailscale 채택 근거 이중 NAT에서도 동작: outbound HTTPS(443)만 사용하므로 방화벽/NAT 뒤에서도 연결 가능 공인 IP 불필요: 양쪽 모두 Tailscale coordination 서버에 outbound 연결만 하면 됨 Subnet Router 기능: EC2 한 대를 서브넷 라우터로 설정하면 VPC 전체 대역에 접근 가능 WireGuard 기반: 커널 레벨 동작, OpenVPN 대비 34배 빠른 성능 무료 티어: 개인 사용 시 100대 디바이스까지 무료 43. 구성 방법 EC2 — Subnet Router 설정: bash 1. IP 포워딩 활성화 sudo sysctl w net.ipv4.ipforward=1 echo 'net.ipv4.ipforward = 1' sudo tee a /etc/sysctl.conf 2. EC2 소스/대상 확인 비활성화 (AWS 콘솔) 3. VPC 대역 광고 sudo tailscale up advertiseroutes=10.0.0.0/16 acceptdns=false 4. Tailscale Admin Console에서 서브넷 라우팅 승인 k8s 마스터 노드 — 라우트 수락: bash sudo tailscale up acceptroutes 44. 외부 접근 구조 (nginx 리버스 프록시) nginx prod — prod.dongkyu.cloud server { listen 80; servername prod.dongkyu.cloud; location / { proxypass http://100.100.150.8:31018; proxysetheader Host $host; proxysetheader XRealIP $remoteaddr; proxysetheader XForwardedFor $proxyaddxforwardedfor; proxysetheader XForwardedProto $scheme; } } 100.100.150.8: 마스터 노드의 Tailscale IP 31018: K8s NGINX Ingress Controller의 NodePort prod/dev 모두 같은 NodePort로 보내고, Host 헤더를 그대로 전달하기 때문에 K8s Ingress가 Host 기반으로 dev/prod를 구분 45. Route53 도메인 설정 레코드 타입 값 용도 prod.dongkyu.cloud A EC2 Elastic IP 운영 환경 접속 dev.dongkyu.cloud A EC2 Elastic IP (동일) 개발 환경 접속 derp.dongkyu.cloud A EC2 Elastic IP (동일) 자체 DERP 서버 5. 온프레미스 K8s 클러스터 구성 51. 클러스터 노드 구성 역할 호스트명 IP 환경 Master toymaster 211.183.3.200/24 제어 플레인 Worker toyworker1 211.183.3.210/24 Prod Worker toyworker2 211.183.3.220/24 Prod Worker devtoyworker 211.183.3.230/24 Dev kubeadm v1.30.14, Ubuntu 24.04, Flannel CNI로 구성했으며 Pod CIDR은 10.244.0.0/16을 사용했습니다. 52. Prod / Dev 환경 분리 단순히 Namespace만 분리하면 Pod가 어느 노드에든 스케줄링될 수 있습니다. 노드 라벨과 nodeSelector를 조합해 Prod Pod는 Prod 노드에만, Dev Pod는 Dev 노드에만 배치되도록 강제했습니다. bash kubectl create namespace dev kubectl create namespace prod kubectl label node toyworker1 env=prod kubectl label node toyworker2 env=prod kubectl label node devtoyworker env=dev yaml Deployment spec 일부 spec: template: spec: nodeSelector: env: prod dev 환경은 env: dev 항목 Dev Prod Namespace dev prod Node 라벨 env=dev env=prod 도메인 dev.dongkyu.cloud prod.dongkyu.cloud 레플리카 1 2 6. 초기 매니페스트 작성 61. 애플리케이션 구조 Backend(Spring Boot, 8080 포트)와 Frontend(Nginx, 80 포트)를 각각 Deployment + Service로 정의하고, NGINX Ingress Controller로 외부 라우팅을 구성했습니다. yaml appdeploy.yml — backend Deployment (일부) apiVersion: apps/v1 kind: Deployment metadata: name: backenddep spec: replicas: 2 selector: matchLabels: app: backend template: spec: nodeSelector: env: prod imagePullSecrets: name: ecrsecret containers: name: backend image: 431538665162.dkr.ecr.apnortheast2.amazonaws.com/backend:latest ports: containerPort: 8080 62. Ingress 라우팅 설계 /api/(.) 경로는 backend로, 나머지 경로(/?(.))는 frontend로 분기합니다. yaml ingress.yml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: appingress annotations: nginx.ingress.kubernetes.io/rewritetarget: /$1 nginx.ingress.kubernetes.io/useregex: "true" spec: ingressClassName: nginx rules: host: prod.dongkyu.cloud http: paths: path: /api/(.) pathType: ImplementationSpecific backend: service: name: backend port: number: 8080 path: /?(.) pathType: ImplementationSpecific backend: service: name: frontend port: number: 80 7. Helm Chart 고도화 초기 매니페스트는 환경마다 중복 YAML을 작성해야 했습니다. Helm Chart로 템플릿화하여 하나의 Chart를 values 파일만 바꿔 dev/prod에 재사용하는 구조로 개선했습니다. GitHub: LDK511/aws13k8sproject 71. 디렉토리 구조 helm/ ├── backend/ │ ├── Chart.yaml │ ├── templates/ │ │ ├── deployment.yaml │ │ ├── service.yaml │ │ └── ingress.yaml │ ├── values.yaml 공통 기본값 │ ├── valuesdev.yaml Dev 오버라이드 │ └── valuesprod.yaml Prod 오버라이드 └── frontend/ ├── Chart.yaml ├── templates/ │ ├── deployment.yaml │ └── service.yaml ├── values.yaml ├── valuesdev.yaml └── valuesprod.yaml 72. Before vs After 비교 항목 초기 매니페스트 Helm Chart 고도화 후 환경별 파일 dev/prod 각각 별도 YAML values 파일만 교체 재사용성 없음 (복사·붙여넣기) 하나의 Chart 재사용 배포 이력 수동 관리 helm history로 추적 롤백 이전 파일 재적용 helm rollback 1커맨드 8. ECR 프라이빗 레지스트리 연동 트러블슈팅 문제 상황 ECR 프라이빗 저장소에서 이미지를 Pull할 때 ImagePullBackOff 오류가 지속적으로 발생했습니다. 터미널에서 docker login을 성공했음에도 동일한 에러가 반복되었습니다. 근본 원인 분석 원인 설명 인증 주체 분리 터미널의 docker login 정보를 Kubelet이 자동으로 공유하지 않음 저장 위치 차이 로그인 정보는 /.docker/config.json (유저 홈) → Kubelet은 참조 불가 노드 전파 안 됨 마스터 노드 인증은 워커 노드에 전파되지 않음 런타임 차이 최신 K8s는 containerd 사용 → Docker 로그인 정보와 미호환 해결 방법 bash ECR 인증 토큰으로 K8s Secret 생성 (유효시간 12시간) kubectl create secret dockerregistry ecrsecret \ dockerserver=431538665162.dkr.ecr.apnortheast2.amazonaws.com \ dockerusername=AWS \ dockerpassword=$(aws ecr getloginpassword region apnortheast2) yaml Deployment에 imagePullSecrets 명시 spec: template: spec: imagePullSecrets: name: ecrsecret 운영 개선 포인트: ECR 토큰은 12시간마다 만료됩니다. 실제 운영 환경에서는 CronJob을 통한 자동 갱신이 필요합니다. 9. Pod → RDS 접근 트러블슈팅 문제 상황 마스터 노드에서는 Tailscale을 통해 RDS(10.0.22.7)에 정상 접속되지만, Pod에서는 접근이 불가능한 문제가 발생했습니다. 원인 분석 Tailscale은 정책 라우팅(table 52)을 사용합니다. 호스트 프로세스는 table 52를 참조하지만, Pod 트래픽은 메인 라우팅 테이블을 참조하여 VPC 경로를 찾지 못했습니다. 호스트 프로세스 → ip rule → table 52 → tailscale0 → OK Pod 트래픽 → ip rule → main table → 경로 없음 → FAIL bash 확인: table 52에는 경로가 있음 ip route get 10.0.0.0 → 10.0.0.0 dev tailscale0 table 52 src 100.100.150.8 확인: 메인 테이블에는 없음 ip route grep tailscale → (아무것도 없음!) 해결 과정 단계 내용 결과 1차 시도 ip rule + iptables FORWARD/MASQUERADE 마스터 Pod만 성공, 워커 실패 2차 시도 FORWARD 체인 순서 수정 (A → I) 여전히 실패 근본 원인 Flannel CNI가 워커에서 이미 MASQUERADE를 수행하여 src IP가 변경됨 — 최종 해결 iptables 규칙에서 소스 제한 제거, 인터페이스 + 목적지 기준으로 매칭 성공 최종 해결 명령 bash 마스터 노드 ip rule add to 10.0.0.0/16 lookup 52 priority 5000 iptables I FORWARD d 10.0.0.0/16 o tailscale0 j ACCEPT iptables I FORWARD s 10.0.0.0/16 i tailscale0 m state state RELATED,ESTABLISHED j ACCEPT iptables t nat I POSTROUTING d 10.0.0.0/16 o tailscale0 j MASQUERADE 워커 노드 (각각) ip route add 10.0.0.0/16 via 211.183.3.200 영구화 (systemd 서비스) bash /etc/systemd/system/vpcroute.service 로 등록하여 재부팅 후에도 자동 적용 MASQUERADE가 필요한 이유 [MASQUERADE 없이] Pod(10.244.1.5) → RDS 도착 → RDS 응답: "10.244.1.5로 보내야지" → AWS: "10.244.1.5? 모르는 IP인데?" → DROP [MASQUERADE 있으면] Pod(10.244.1.5) → 마스터에서 src를 100.100.150.8로 변환 → RDS 도착 → RDS 응답: "100.100.150.8로 보내야지" → Tailscale 네트워크로 정상 라우팅 → 마스터 도착 → 마스터가 다시 dst를 10.244.1.5로 복원 → Pod 도착 10. ArgoCD 기반 CI/CD 파이프라인 구축 101. CI/CD 흐름 개발자 Push (main → Prod, develop → Dev) │ ▼ GitHub Actions ├── Docker 이미지 빌드 └── Amazon ECR Push (이미지 태그: commit SHA) │ ▼ ArgoCD (Git 저장소 감지) │ helm/backend/values.yaml의 image.tag 변경 감지 ▼ K8s 클러스터 자동 Sync ├── dev namespace ← develop 브랜치 └── prod namespace ← main 브랜치 102. Application CRD 구성 yaml argocd/backenddev.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: backenddev namespace: argocd spec: project: default source: repoURL: https://github.com/LDK511/aws13k8sproject.git path: helm/backend targetRevision: develop helm: valueFiles: valuesdev.yaml destination: server: https://kubernetes.default.svc namespace: dev syncPolicy: automated: selfHeal: true prune: true 총 4개의 Application CRD를 작성하여 backenddev, backendprod, frontenddev, frontendprod 모두 자동 배포가 가능하도록 구성했습니다. 103. 브랜치 전략 연계 브랜치 ArgoCD Target 배포 환경 네임스페이스 main main Prod prod develop develop Dev dev 11. 배포 검증 결과 Ingress 라우팅 검증 bash kubectl get ingress A NAMESPACE NAME CLASS HOSTS ADDRESS PORTS dev appingress nginx dev.dongkyu.cloud 211.183.3.230 80 prod appingress nginx prod.dongkyu.cloud 211.183.3.230 80 ArgoCD 동기화 상태 4개 Application(backenddev, backendprod, frontenddev, frontendprod) 모두 Healthy / Synced 상태로 정상 동작을 확인했습니다. RDS 연결 검증 bash root@toymaster: mysql h database1.cxey8usueno9.apnortheast2.rds.amazonaws.com u admin p Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 163 Server version: 8.4.8 Source distribution mysql → Tailscale Subnet Router를 통해 Private Subnet의 RDS에 정상 접근 확인 12. 회고 및 개선 방향 잘 된 점 5가지 네트워크 방안을 트레이드오프 분석하여 환경에 최적인 Tailscale Subnet Router를 선택하고 구현 Pod→RDS 정책 라우팅 문제를 3단계에 걸쳐 근본 원인까지 추적하고 해결 초기 단순 매니페스트에서 Helm Chart 고도화, CI/CD 연동까지 전체 배포 사이클을 한 번에 경험 nodeSelector를 통한 Prod/Dev 노드 분리로 리소스 격리 구현 ECR ImagePullBackOff 트러블슈팅 과정에서 K8s 인증 체계(Secret, Kubelet, containerd)에 대한 깊은 이해 획득 GitOps 방식으로 배포 이력 추적 및 selfHeal을 통한 클러스터 자동 복구 경험 개선할 점 / 향후 계획 개선 항목 이유 ECR Secret 자동 갱신 CronJob 12시간 토큰 만료 문제 해결 서울 자체 DERP 서버 구축 도쿄 DERP 대비 레이턴시 50%+ 개선 (3050ms → 515ms) TLS/HTTPS 적용 (certmanager) 현재 HTTP로만 서비스 중 HPA (Horizontal Pod Autoscaler) 트래픽 기반 자동 스케일링 Prometheus + Grafana 모니터링 강화 Pod/노드 메트릭 시각화 이 프로젝트의 핵심 가치는 "서비스를 만드는 것"이 아니라 "서비스가 어떻게 운영되는가"를 직접 설계하고 증명한 것입니다. 네트워크 방안 분석 → Tailscale 하이브리드 연결 → DNS → Ingress → Service → Pod로 이어지는 전체 흐름, ECR 인증 구조, Pod→RDS 정책 라우팅 트러블슈팅, Helm 기반 환경 분리, GitOps 배포를 경험하며 실무적인 관점에서 바라볼 수 있었습니다.

May 10, 2026kubernetes
On-Premise K8s × AWS 하이브리드 인프라 구축 및 GitOps CI/CD 자동화