본문으로 건너뛰기
AIDevOps
  • Learn
  • Learning Paths
  • Practice
  • Open Source
  • Books
  • Engineering

    AI DevOpsAI 서비스 개발·운영 전체 지도LLMOpsLLM 배포·평가·관측실전 프로젝트AI Agent 프로젝트 실습

    Knowledge

    Docs기술 문서 모음Blog엔지니어링 아티클Plogger개발 기록 피드

    Validate

    Certification3단계 역량 인증 · 준비 중
AI Models
LlamaMistralGemmaDeepSeekQwen
☁️ 클라우드
클라우드 입문 & 로드맵AWSGCPAzureNCPCloudflare
🤖 AI 실전 개발
AI 실전 입문 & 로드맵Hugging FaceLangChainLlamaIndexLLMOps|LangGraphMCPMulti-AgentAgent Evaluation
🧠 AI Core
AI 입문 & 로드맵ML FundamentalsLLM Fundamentals|Python AIC++|PyTorchTensorFlowJAX
🧠 AI Agent 개발
금융 AI AgentLLM API 서버주식 투자 AgentAIOps AI Agent교육 AI Agent코딩 AI Agent
🌱 Spring Cloud
Spring 입문 & 로드맵Spring Cloud GatewaySpring BootJava|Spring AISpring SecuritySpring BatchSpring JPA
🐳 DevOps
DevOps 입문 & 로드맵LinuxDockerCI/CD|Kubernetes 기본K8s 심화/실무PrometheusGrafana
🧱 인프라
인프라 입문 & 로드맵NginxRedis
🎨 Frontend
Frontend 입문 & 로드맵JavaScriptTypeScript|ReactNext.js|VueNuxt
📱 Mobile
Mobile 입문 & 로드맵KotlinAndroidFlutter
⚙️ Backend
Backend 입문 & 로드맵Python 기본FastAPIDjangoFlask|CGoGinNode.js
💾 Database
DB 입문 & 로드맵공통 SQLOracleMySQLPostgreSQL|MongoDB벡터 DB
🧪 검증
k6JMeternGrinder
AIDevOps

Engineering AI. From Code to Production.
AI와 AI Agent를 개발하고 운영하기 위한 엔지니어링 학습 플랫폼

Learn

  • 전체 가이드
  • Learning Paths
  • Practice
  • Books

Resources

  • AI DevOps
  • LLMOps
  • 실전 프로젝트
  • Docs
  • Blog
  • Plogger
  • Open Source
  • Certification (준비 중)

Start Here

  • AI Core 로드맵
  • AI 실전 개발 로드맵
  • Spring Cloud 로드맵
  • DevOps 로드맵
  • 인프라 로드맵

 

  • 클라우드 로드맵
  • Frontend 로드맵
  • Mobile 로드맵
  • Backend 로드맵
  • Database 로드맵
© 2026 AI DevOps Korea. All rights reserved.
이용약관개인정보처리방침Sitemaptestforge.kr
  1. Home
  2. Learn
  3. 클라우드
  4. AWS
AWS 프로덕션 웹서비스 구축 가이드 — ALB부터 오토스케일링까지

☁️ AWS 완전 가이드

Visitors

실전 웹서비스를 AWS에 처음부터 끝까지 구축합니다. Multi-AZ VPC 네트워크, ALB 로드밸런싱과 HTTPS 종료, ACM·Route 53 도메인 연결, ECS Fargate 오토스케일링, RDS, S3+CloudFront, IAM·Secrets 보안, CloudWatch 모니터링, CI/CD, Terraform 프로젝트 구조화까지 — 프로덕션 수준 아키텍처를 순서대로 정리했습니다.

  • Intermediate · 중급
  • 업데이트 2026.09.19
  • 약 30분 읽기
  • 17개 섹션
  • 예제 코드 18개

포함된 Learning Path

이 가이드는 아래 경로의 한 단계입니다. 앞뒤 순서와 함께 학습해보세요.

  • Cloud Native Engineer →
  • Production AI Engineer →
ALB 로드밸런싱 & HTTPS 종료ECS Fargate 오토스케일링RDS·Secrets 보안 설계Terraform 통합 IaC

목차

0 / 19
  1. 가이드 사용법
  2. 구조 다이어그램
  3. 전체 아키텍처 한눈에 보기
  4. VPC 네트워크 설계 (Multi-AZ)
  5. ALB 로드밸런서 구성
  6. ACM 인증서 & Route 53 도메인
  7. ECS Fargate 배포 & ALB 연동
  8. 오토스케일링
  9. RDS 데이터베이스 구성
  10. S3 정적 자산 & CloudFront CDN
  11. IAM 역할 & 보안 정책
  12. Secrets Manager & 환경변수
  13. CloudWatch 모니터링 & 알람
  14. CI/CD (ECR + GitHub Actions)
  15. Terraform 프로젝트 구조화
  16. 프로덕션 체크리스트
  17. AWS 설계
  18. 운영 기준
  19. 검증 전략
목차 19개 섹션
  1. 가이드 사용법
  2. 구조 다이어그램
  3. 전체 아키텍처 한눈에 보기
  4. VPC 네트워크 설계 (Multi-AZ)
  5. ALB 로드밸런서 구성
  6. ACM 인증서 & Route 53 도메인
  7. ECS Fargate 배포 & ALB 연동
  8. 오토스케일링
  9. RDS 데이터베이스 구성
  10. S3 정적 자산 & CloudFront CDN
  11. IAM 역할 & 보안 정책
  12. Secrets Manager & 환경변수
  13. CloudWatch 모니터링 & 알람
  14. CI/CD (ECR + GitHub Actions)
  15. Terraform 프로젝트 구조화
  16. 프로덕션 체크리스트
  17. AWS 설계
  18. 운영 기준
  19. 검증 전략

가이드 사용법

읽는 방향

AWS를 실무 흐름으로 이해하기

실전 웹서비스를 AWS에 처음부터 끝까지 구축합니다. Multi-AZ VPC 네트워크, ALB 로드밸런싱과 HTTPS 종료, ACM·Route 53 도메인 연결, ECS Fargate 오토스케일링, RDS, S3+CloudFront, IAM·Secrets 보안, CloudWatch 모니터링, CI/CD, Terraform 프로젝트 구조화까지 — 프로덕션 수준 아키텍처를 순서대로 정리했습니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.

핵심 관점

인프라 / 운영

설치 명령을 외우기보다 트래픽, 런타임, 관측, 장애 대응이 어떤 순서로 이어지는지 파악합니다.

ALB 로드밸런싱 & HTTPS 종료ECS Fargate 오토스케일링RDS·Secrets 보안 설계Terraform 통합 IaC

구조 다이어그램

글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 AWS를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.

학습 흐름

다이어그램 렌더링 중…

아키텍처 관점

다이어그램 렌더링 중…

전체 아키텍처 한눈에 보기

AWS를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.

실전 웹서비스는 컨테이너 하나만 띄운다고 끝나지 않습니다. 도메인을 연결하고, HTTPS를 종료하고, 트래픽을 여러 인스턴스로 분산하고, DB를 안전하게 격리하고, 장애를 감지할 수 있어야 비로소 "프로덕션"이라 부를 수 있습니다. 이 가이드는 아래 아키텍처를 구성 요소 하나씩, 실제로 만들어가는 순서로 이어집니다.
다이어그램 렌더링 중…
구성 요소역할배치 위치
Route 53DNS — 도메인을 ALB에 Alias로 연결-
ACMTLS 인증서 무료 발급 및 자동 갱신-
ALBL7 로드밸런싱, HTTPS 종료, 경로/호스트 기반 라우팅Public Subnet (Multi-AZ)
ECS Fargate서버리스 컨테이너 실행Private Subnet (Multi-AZ)
RDS관리형 관계형 DB, Multi-AZ 자동 페일오버Private Subnet
S3 + CloudFront정적 자산 저장 + 글로벌 CDN 배포-
Secrets ManagerDB 비밀번호·API 키 보안 관리 및 런타임 주입-
CloudWatch로그 수집, 메트릭, 알람-

Tip

이 순서(네트워크 → ALB → 도메인/인증서 → 컴퓨트 → 데이터 → 보안 → 모니터링 → 배포 자동화)는 실제로 인프라를 처음부터 구축할 때 의존관계가 꼬이지 않는 순서이기도 합니다 — 뒤 섹션의 리소스는 앞 섹션에서 만든 리소스를 참조합니다.

VPC 네트워크 설계 (Multi-AZ)

여기서는 VPC 네트워크 설계 (Multi-AZ)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

프로덕션 서비스는 반드시 최소 2개의 가용 영역(AZ)에 걸쳐 배치해야 합니다. 하나의 AZ에 장애가 발생해도 나머지 AZ가 트래픽을 계속 받을 수 있어야 하기 때문입니다. ALB와 NAT Gateway는 퍼블릭 서브넷에, ECS Task와 RDS는 인터넷에서 직접 접근할 수 없는 프라이빗 서브넷에 배치합니다.
network.tfHCL
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = { Name = "prod-vpc" }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}

resource "aws_subnet" "public" {
  for_each = { a = { cidr = "10.0.1.0/24", az = "ap-northeast-2a" },
               b = { cidr = "10.0.2.0/24", az = "ap-northeast-2c" } }
  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value.cidr
  availability_zone       = each.value.az
  map_public_ip_on_launch = true
  tags = { Name = "public-${each.key}" }
}

resource "aws_subnet" "private" {
  for_each = { a = { cidr = "10.0.11.0/24", az = "ap-northeast-2a" },
               b = { cidr = "10.0.12.0/24", az = "ap-northeast-2c" } }
  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az
  tags = { Name = "private-${each.key}" }
}

resource "aws_eip" "nat" {
  domain = "vpc"
}

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public["a"].id
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }
}
서브넷CIDR 예시배치 리소스아웃바운드 경로
Public Subnet A (AZ-a)10.0.1.0/24ALB, NAT GatewayInternet Gateway
Public Subnet B (AZ-b)10.0.2.0/24ALBInternet Gateway
Private Subnet A (AZ-a)10.0.11.0/24ECS Task, RDS(Primary)NAT Gateway
Private Subnet B (AZ-b)10.0.12.0/24ECS Task, RDS(Standby)NAT Gateway

Tip

  • 보안 그룹(Security Group)은 상태 저장(stateful)이라 인바운드를 허용하면 응답 트래픽은 자동으로 허용됩니다. 반면 NACL은 무상태(stateless)라 인/아웃 규칙을 각각 명시해야 합니다 — 대부분의 서비스는 SG만으로 충분하고, NACL은 서브넷 단위의 추가 방어선이 필요할 때만 사용하세요.
  • NAT Gateway는 AZ마다 별도로 만들면 가용성은 올라가지만 요금도 그만큼 늘어납니다. 비용을 아끼려면 위 예시처럼 NAT Gateway 1개를 두고 두 프라이빗 서브넷이 함께 쓰게 할 수 있지만, 그 AZ가 죽으면 아웃바운드 인터넷 접근이 전체적으로 끊기는 트레이드오프가 있습니다.

ALB 로드밸런서 구성

여기서는 ALB 로드밸런서 구성을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

ALB(Application Load Balancer)는 OSI 7계층(애플리케이션 레이어)에서 동작하며, HTTP/HTTPS 요청의 경로·호스트 헤더를 보고 트래픽을 분산합니다. 반드시 서로 다른 AZ의 퍼블릭 서브넷 최소 2개에 걸쳐 생성해야 합니다.
alb.tfHCL
resource "aws_security_group" "alb" {
  name   = "alb-sg"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_lb" "main" {
  name               = "my-app-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = [for s in aws_subnet.public : s.id]
}

resource "aws_lb_target_group" "app" {
  name        = "my-app-tg"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  target_type = "ip"   # Fargate awsvpc 네트워크 모드는 반드시 ip

  health_check {
    path                = "/health"
    healthy_threshold   = 2
    unhealthy_threshold = 3
    interval            = 30
    timeout             = 5
    matcher             = "200"
  }
}
listeners.tfHCL
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.cert.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

# 80번 포트는 항상 443으로 리다이렉트만 수행
resource "aws_lb_listener" "http_redirect" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# 경로 기반 라우팅 예시: /api/* 는 별도 API Target Group으로
resource "aws_lb_listener_rule" "api" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 10

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.api.arn
  }
  condition {
    path_pattern { values = ["/api/*"] }
  }
}
구성 요소설명
ListenerALB가 요청을 수신 대기하는 포트/프로토콜 (80, 443)
Target Group실제 트래픽을 받을 대상의 묶음 (ECS Task IP, EC2, Lambda)
Health Check대상의 정상 여부를 주기적으로 확인하고, 비정상이면 트래픽에서 제외
Listener Rule경로(path)·호스트 헤더 조건에 따라 다른 Target Group으로 라우팅

Tip

  • ECS Task SG는 ALB SG로부터 들어오는 트래픽만 컨테이너 포트로 허용하도록 체인처럼 구성하세요. 이렇게 하면 ALB를 거치지 않은 직접 접근이 원천 차단되고, "ALB → Task"라는 트래픽 흐름이 보안 그룹 규칙만 봐도 명확해집니다.
  • 헬스체크 경로(/health)는 인증 없이 200을 반환해야 합니다. 인증 미들웨어에 걸려 401/403이 나오면 Target Group 전체가 Unhealthy로 판정되어 서비스 전체가 죽은 것처럼 보이는 사고로 이어지기 쉽습니다.
  • ALB는 기본적으로 크로스존 로드밸런싱(cross-zone load balancing)이 활성화되어 있어, 특정 AZ에 Task가 몰려도 모든 AZ의 대상에 고르게 분산됩니다.

ACM 인증서 & Route 53 도메인

여기서는 ACM 인증서 & Route 53 도메인을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

ACM(AWS Certificate Manager)으로 무료 TLS 인증서를 발급받아 위에서 만든 HTTPS 리스너에 연결하고, Route 53 Alias 레코드로 도메인을 ALB에 직접 연결합니다. Alias 레코드는 CNAME과 달리 루트 도메인(example.com)에도 사용할 수 있고 조회 비용이 없습니다.
acm-route53.tfHCL
resource "aws_acm_certificate" "cert" {
  domain_name               = "example.com"
  subject_alternative_names = ["*.example.com"]
  validation_method         = "DNS"

  lifecycle { create_before_destroy = true }
}

resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
      name  = dvo.resource_record_name
      type  = dvo.resource_record_type
      value = dvo.resource_record_value
    }
  }
  zone_id = aws_route53_zone.main.zone_id
  name    = each.value.name
  type    = each.value.type
  records = [each.value.value]
  ttl     = 60
}

resource "aws_acm_certificate_validation" "cert" {
  certificate_arn         = aws_acm_certificate.cert.arn
  validation_record_fqdns = [for r in aws_route53_record.cert_validation : r.fqdn]
}

resource "aws_route53_record" "alb_alias" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "example.com"
  type    = "A"

  alias {
    name                   = aws_lb.main.dns_name
    zone_id                = aws_lb.main.zone_id
    evaluate_target_health = true
  }
}

Tip

  • ALB에서 쓰는 인증서는 ALB와 같은 리전이면 되지만, CloudFront에서 쓸 인증서는 반드시 us-east-1(버지니아 북부) 리전에서 발급해야 합니다 — 리전을 헷갈려서 CloudFront에 인증서를 연결하지 못하는 경우가 흔합니다.
  • DNS 검증 방식을 쓰면 검증 레코드가 남아있는 한 인증서가 자동으로 갱신됩니다. 이메일 검증 방식은 만료 전 수동 승인이 필요하니 특별한 이유가 없다면 DNS 검증을 사용하세요.

ECS Fargate 배포 & ALB 연동

여기서는 ECS Fargate 배포 & ALB 연동을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

Task Definition에 컨테이너 이미지·CPU/메모리·로그 설정을 지정하고, ECS Service의 load_balancer 블록으로 앞서 만든 Target Group에 연결합니다. 컨테이너가 쓰는 역할이 두 가지(Execution Role, Task Role)로 나뉘는데, 자세한 차이는 뒤의 IAM 섹션에서 다룹니다.
task-definition.jsonJSON
{
  "family": "my-app-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest",
      "portMappings": [{ "containerPort": 8080 }],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-app",
          "awslogs-region": "ap-northeast-2",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
}
ecs-service.tfHCL
resource "aws_ecs_cluster" "main" {
  name = "my-app-cluster"
}

resource "aws_security_group" "ecs_tasks" {
  name   = "ecs-tasks-sg"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]   # ALB에서만 허용
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_ecs_service" "app" {
  name            = "my-app-service"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = [for s in aws_subnet.private : s.id]
    security_groups  = [aws_security_group.ecs_tasks.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.app.arn
    container_name   = "app"
    container_port   = 8080
  }

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  depends_on = [aws_lb_listener.https]
}

Tip

deployment_circuit_breaker를 켜두면 새 배포가 계속 Unhealthy로 실패할 때 ECS가 자동으로 이전 버전으로 롤백합니다. desired_count는 항상 2 이상으로 유지해 배포 도중에도 무중단을 보장하세요.

오토스케일링

여기서는 오토스케일링을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

Application Auto Scaling으로 ECS 서비스의 실행 개수(desired count)를 트래픽에 따라 자동으로 늘리거나 줄입니다. CPU 사용률 기준과 ALB 요청 수 기준 중 서비스 특성에 맞는 지표를 선택하세요.
autoscaling.tfHCL
resource "aws_appautoscaling_target" "ecs" {
  max_capacity       = 10
  min_capacity       = 2
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

resource "aws_appautoscaling_policy" "request_count" {
  name               = "alb-request-count-tracking"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.ecs.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
  service_namespace  = aws_appautoscaling_target.ecs.service_namespace

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ALBRequestCountPerTarget"
      resource_label = "${aws_lb.main.arn_suffix}/${aws_lb_target_group.app.arn_suffix}"
    }
    target_value       = 1000
    scale_in_cooldown  = 300
    scale_out_cooldown = 60
  }
}

Tip

  • ALBRequestCountPerTarget(요청 수 기준)은 CPU 기준보다 트래픽 버스트에 더 빠르게 반응합니다 — I/O 대기가 많고 CPU를 많이 쓰지 않는 API 서버에 더 적합합니다.
  • scale-out cooldown은 짧게(예: 60초), scale-in cooldown은 길게(예: 300초)로 잡으세요. 반대로 하면 트래픽이 잠깐 튀었다가 바로 스케일 인 되는 플래핑(flapping) 현상이 생깁니다.

RDS 데이터베이스 구성

여기서는 RDS 데이터베이스 구성을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

RDS는 반드시 프라이빗 서브넷에만 배치하고, 보안 그룹으로 ECS Task 보안 그룹에서만 접근을 허용합니다. Multi-AZ를 활성화하면 Primary 장애 시 Standby로 자동 페일오버됩니다(평균 60~120초 소요).
rds.tfHCL
resource "aws_db_subnet_group" "main" {
  name       = "main-db-subnet-group"
  subnet_ids = [for s in aws_subnet.private : s.id]
}

resource "aws_security_group" "rds" {
  name   = "rds-sg"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.ecs_tasks.id]   # ECS Task에서만 접근
  }
}

resource "aws_db_instance" "main" {
  identifier                   = "my-app-db"
  engine                       = "postgres"
  engine_version               = "16.4"
  instance_class               = "db.t4g.medium"
  allocated_storage            = 20
  storage_encrypted            = true
  multi_az                     = true
  db_subnet_group_name         = aws_db_subnet_group.main.name
  vpc_security_group_ids       = [aws_security_group.rds.id]
  username                     = "appuser"
  manage_master_user_password  = true   # 비밀번호를 Secrets Manager에 자동 생성/저장
  backup_retention_period      = 7
  deletion_protection          = true
  skip_final_snapshot          = false
  final_snapshot_identifier    = "my-app-db-final-snapshot"
}

Tip

  • manage_master_user_password = true로 설정하면 RDS가 마스터 비밀번호를 자동 생성해 Secrets Manager에 저장합니다 — 애플리케이션 코드나 Terraform 변수에 비밀번호를 직접 적을 필요가 없어집니다.
  • 운영 환경에서는 deletion_protection = true와 skip_final_snapshot = false를 반드시 설정하세요. 이 두 값을 빠뜨려서 실수로 실행한 terraform destroy가 프로덕션 DB를 흔적도 없이 삭제하는 사고가 실제로 자주 일어납니다.

S3 정적 자산 & CloudFront CDN

여기서는 S3 정적 자산 & CloudFront CDN을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

이미지, JS, CSS 같은 정적 자산은 ECS 컨테이너가 직접 서빙하지 않고 S3에 저장한 뒤 CloudFront로 전 세계에 캐싱 배포하는 것이 훨씬 빠르고 저렴합니다.
s3-cloudfront.tfHCL
resource "aws_s3_bucket" "static" {
  bucket = "my-app-static-assets"
}

resource "aws_s3_bucket_public_access_block" "static" {
  bucket                  = aws_s3_bucket.static.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_cloudfront_origin_access_control" "oac" {
  name                              = "my-app-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "cdn" {
  enabled             = true
  default_root_object = "index.html"

  origin {
    domain_name              = aws_s3_bucket.static.bucket_regional_domain_name
    origin_id                = "s3-static"
    origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
  }

  default_cache_behavior {
    target_origin_id       = "s3-static"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }
  }

  restrictions {
    geo_restriction { restriction_type = "none" }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

Tip

  • S3 버킷은 퍼블릭 액세스를 완전히 차단하고, OAC(Origin Access Control)를 통해서만 CloudFront가 접근하도록 구성하는 것이 최신 권장 방식입니다 — 예전에 쓰던 OAI(Origin Access Identity)는 더 이상 권장되지 않습니다.
  • CloudFront 배포 변경 사항이 전 세계 엣지에 전파되는 데 5~15분 정도 걸릴 수 있습니다. 방금 배포했는데 반영이 안 됐다고 당황하기 전에 잠시 기다려보세요.

IAM 역할 & 보안 정책

여기서는 IAM 역할 & 보안 정책을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

ECS에서는 Execution Role과 Task Role을 혼동하기 쉽습니다. Execution Role은 ECS 에이전트가 ECR에서 이미지를 받아오고 로그를 CloudWatch로 보내기 위한 역할이고, Task Role은 컨테이너 안에서 실행 중인 애플리케이션 코드가 S3·Secrets Manager 같은 다른 AWS 서비스를 호출할 때 쓰는 역할입니다.
iam-execution-role.tfHCL
resource "aws_iam_role" "ecs_execution" {
  name = "ecsTaskExecutionRole"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ecs-tasks.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ecs_execution" {
  role       = aws_iam_role.ecs_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
iam-task-role-policy.jsonJSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::my-app-static-assets/*"
    }
  ]
}
역할사용 주체대표 권한
Execution RoleECS 에이전트 (인프라 레벨)ecr:GetDownloadUrlForLayer, logs:CreateLogStream, secretsmanager:GetSecretValue(주입용)
Task Role컨테이너 내부 애플리케이션 코드s3:GetObject, secretsmanager:GetSecretValue(코드에서 직접 호출) 등

Tip

  • 절대 Access Key를 코드나 환경변수 파일에 직접 넣지 마세요. Task Role을 쓰면 컨테이너가 임시 자격증명을 자동으로 받아오고, 자격증명은 자동으로 로테이션됩니다.
  • Resource를 "*"로 열어두는 정책은 최소 권한 원칙 위반입니다. 반드시 특정 ARN(버킷, 테이블 등)으로 범위를 좁히세요.

Secrets Manager & 환경변수 관리

여기서는 Secrets Manager & 환경변수 관리을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

DB 비밀번호, 외부 API 키 같은 민감 정보는 이미지나 환경변수 파일에 하드코딩하지 말고 Secrets Manager(또는 로테이션이 필요 없다면 Parameter Store SecureString)에 저장한 뒤, Task Definition의 secrets 필드로 런타임에 주입합니다.
secrets.tfHCL
resource "aws_secretsmanager_secret" "app_secrets" {
  name = "my-app/prod/secrets"
}

resource "aws_secretsmanager_secret_version" "app_secrets" {
  secret_id = aws_secretsmanager_secret.app_secrets.id
  secret_string = jsonencode({
    OPENAI_API_KEY = "sk-..."
  })
}
task-definition-secrets.jsonJSON
{
  "containerDefinitions": [{
    "name": "app",
    "environment": [
      { "name": "NODE_ENV", "value": "production" }
    ],
    "secrets": [
      {
        "name": "OPENAI_API_KEY",
        "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:my-app/prod/secrets:OPENAI_API_KEY::"
      }
    ]
  }]
}
비교Secrets ManagerParameter Store (SecureString)
비용시크릿당 월 과금기본(Standard) 티어는 무료
자동 로테이션Lambda 연동으로 지원미지원 (수동 갱신)
적합한 용도DB 비밀번호 등 주기적 로테이션이 필요한 값변경이 드문 API 키·설정값

Tip

민감하지 않은 값(NODE_ENV, 리전 등)은 environment 필드에, 민감한 값은 secrets 필드에 분리해서 넣으세요 — secrets 필드는 ECS 에이전트가 Execution Role 권한으로 런타임에만 주입하고, 태스크 정의 JSON 자체에는 실제 값이 남지 않습니다.

CloudWatch 모니터링 & 알람

여기서는 CloudWatch 모니터링 & 알람을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

CloudWatch로 로그를 모으고, 이상 징후는 알람으로 즉시 알림 받도록 구성합니다.
monitoring.tfHCL
resource "aws_cloudwatch_log_group" "app" {
  name              = "/ecs/my-app"
  retention_in_days = 30
}

resource "aws_sns_topic" "alerts" {
  name = "prod-alerts"
}

resource "aws_sns_topic_subscription" "email" {
  topic_arn = aws_sns_topic.alerts.arn
  protocol  = "email"
  endpoint  = "oncall@example.com"
}

resource "aws_cloudwatch_metric_alarm" "alb_5xx" {
  alarm_name          = "alb-target-5xx-spike"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods   = 2
  metric_name          = "HTTPCode_Target_5XX_Count"
  namespace            = "AWS/ApplicationELB"
  period               = 60
  statistic            = "Sum"
  threshold            = 10
  alarm_actions        = [aws_sns_topic.alerts.arn]
  dimensions = {
    LoadBalancer = aws_lb.main.arn_suffix
  }
}
지표의미왜 중요한가
ALB HTTPCode_Target_5XX_Count백엔드가 5xx를 반환한 횟수애플리케이션 장애의 가장 직접적인 신호
ALB UnHealthyHostCountUnhealthy로 판정된 대상 수헬스체크 실패 → 트래픽을 받을 대상이 줄어듦
ECS CPUUtilization / MemoryUtilization태스크 리소스 사용률스케일링 판단과 용량 계획의 기준
RDS DatabaseConnections / FreeStorageSpaceDB 연결 수, 남은 디스크 용량커넥션 풀 고갈, 디스크 풀로 인한 다운타임 예방

Tip

  • 로그 그룹에 retention_in_days를 지정하지 않으면 영구 보관되어 비용이 계속 쌓입니다 — 운영 로그는 30~90일 정도로 제한하는 것을 권장합니다.
  • "ALB 5xx 급증"과 "UnHealthyHostCount 발생" 이 두 알람만이라도 반드시 SNS로 즉시 알림을 받도록 설정하세요 — 대부분의 장애를 가장 먼저 감지할 수 있는 신호입니다.

CI/CD (ECR + GitHub Actions)

여기서는 CI/CD (ECR + GitHub Actions)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

컨테이너 이미지를 ECR에 푸시하고 ECS 서비스를 새 이미지로 갱신하는 흐름을 GitHub Actions로 자동화합니다. 이미지 태그를 커밋 SHA로 고정해두면 ECS가 정확히 어떤 빌드를 실행 중인지 항상 추적할 수 있고, 서비스 갱신 후 배포 상태가 안정될 때까지 기다리는 단계를 넣어야 실패한 배포를 자동으로 감지할 수 있습니다.
.github/workflows/deploy.ymlYAML
name: Deploy to ECS

on:
  push:
    branches: [main]

permissions:
  id-token: write   # OIDC로 임시 자격증명 발급
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deployer
          aws-region: ap-northeast-2

      - name: Login to Amazon ECR
        id: ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build, tag and push image
        env:
          REGISTRY: ${{ steps.ecr.outputs.registry }}
          REPOSITORY: my-app
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
          docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG

      - name: Force new ECS deployment
        run: |
          aws ecs update-service \
            --cluster my-app-cluster \
            --service my-app-service \
            --force-new-deployment

Tip

  • 장기 Access Key를 GitHub Secrets에 저장하는 대신 OIDC(OpenID Connect)로 GitHub Actions가 임시 자격증명을 발급받도록 구성하면, 키 노출·유출이라는 위험 자체가 사라집니다 — aws-actions/configure-aws-credentials의 role-to-assume 방식을 사용하세요.
  • 이미지 태그로 latest 대신 커밋 SHA(github.sha)를 쓰면 "지금 프로덕션에 어떤 커밋이 떠 있는지"가 명확해지고, 문제가 생겼을 때 이전 태그로 즉시 롤백할 수 있습니다.

Terraform 프로젝트 구조화

여기서는 Terraform 프로젝트 구조화을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

앞서 다룬 리소스(VPC, ALB, ECS, RDS, ...)를 실제 팀에서 운영할 때는 파일 하나에 다 몰아넣지 않고 모듈 단위로 쪼개고, 원격 상태(remote state)를 S3 + DynamoDB 락으로 관리해 여러 사람이 동시에 안전하게 작업할 수 있도록 합니다.
디렉토리 구조TEXT
infra/
├── modules/
│   ├── network/       # VPC, 서브넷, NAT, 라우팅 테이블
│   ├── alb/           # ALB, 타겟그룹, 리스너, ACM
│   ├── ecs-service/   # ECS 서비스 + 오토스케일링
│   └── rds/           # RDS 인스턴스
├── envs/
│   ├── staging/
│   │   ├── main.tf    # 모듈 호출 + 환경별 변수값
│   │   └── backend.tf
│   └── production/
│       ├── main.tf
│       └── backend.tf
envs/production/backend.tfHCL
terraform {
  backend "s3" {
    bucket         = "my-app-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Tip

  • state 파일에는 DB 비밀번호 같은 민감 정보가 평문으로 남을 수 있습니다 — state를 저장하는 S3 버킷은 반드시 서버측 암호화(SSE)를 켜고 퍼블릭 접근을 차단하세요.
  • staging/production처럼 환경별로 state 파일과 backend key를 분리해야, 실수로 스테이징에서 실행한 apply가 프로덕션에 반영되는 사고를 막을 수 있습니다.
  • CI 파이프라인 규칙으로 "PR에서는 plan만 실행하고 사람이 리뷰, main 브랜치 머지 후에만 apply"를 강제하면 예상치 못한 변경을 사전에 걸러낼 수 있습니다.

프로덕션 체크리스트

여기서는 프로덕션 체크리스트을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.

✅
배포 전 최종 점검

• 보안 그룹: ALB만 0.0.0.0/0에 노출하고, ECS·RDS는 내부 보안그룹 참조로만 접근 허용
• WAF: ALB에 AWS WAF를 연결해 SQL Injection·XSS·Rate Limiting 기본 규칙 적용
• ALB 액세스 로그: S3 저장을 활성화해 장애·공격 발생 시 사후 분석이 가능하도록 준비
• Multi-AZ: ALB·ECS·RDS 모두 최소 2개 AZ에 분산 배치
• 백업 & 삭제 방지: RDS 자동 백업 활성화, deletion_protection = true
• 비용 알람: AWS Budgets로 예상치 못한 과금을 조기에 감지
• 태그 전략: Environment/Service/Owner 태그를 전 리소스에 일관 적용 (비용 분석·자동화 스크립트에 필수)

연계 가이드: Docker 가이드 · Kubernetes 가이드 · Cloudflare 가이드
waf.tfHCL
resource "aws_wafv2_web_acl" "main" {
  name  = "alb-waf"
  scope = "REGIONAL"

  default_action { allow {} }

  rule {
    name     = "aws-managed-common"
    priority = 1
    override_action { none {} }
    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "aws-managed-common"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "alb-waf"
    sampled_requests_enabled   = true
  }
}

resource "aws_wafv2_web_acl_association" "alb" {
  resource_arn = aws_lb.main.arn
  web_acl_arn  = aws_wafv2_web_acl.main.arn
}

AWS 실무 설계

AWS 실무 설계은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.

AWS 설계는 비용 최적화(Cost Optimization), 다중 가용구역(Multi-AZ) 기반의 고가용성(High Availability), 그리고 IAM 최소 권한 정책 및 VPC 망 격리를 기본으로 합니다.
결정 지점확인 질문실무 기준
경계AWS 코드에서 바뀌기 쉬운 부분은 어디인가?입출력, 설정, 외부 연동, 핵심 규칙을 분리합니다.
상태상태가 어디서 생성되고 어디서 사라지는가?상태 소유자와 수명 주기를 코드로 드러냅니다.
장애실패했을 때 호출자는 무엇을 받는가?timeout, fallback, error contract를 먼저 정합니다.

AWS 운영 기준

이 섹션은 AWS 운영 기준을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.

CloudWatch 메트릭 관측, EC2 Auto Scaling, IAM Credential Rotation, 그리고 Cost Explorer를 통한 자원 사용량 모니터링을 지속해야 합니다.

Tip

  • Multi-AZ VPC
  • IAM Role usage (No AccessKeys)
  • CloudWatch Alarms
  • Terraform State Management

AWS 검증 전략

AWS 검증 전략은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.

Terraform Plan 사전 검증, AWS Config 정책 위반 자동 탐지, IAM Policy Simulator 검사, 그리고 보안 그룹(Security Group) 오설정 방지를 위한 회귀 테스트를 수행해야 합니다.
품질 축검증 방법완료 기준
정확성정상/실패 케이스를 자동화합니다.핵심 시나리오가 재현 가능하게 통과합니다.
회귀 방지버그 수정 시 동일 케이스를 테스트로 남깁니다.같은 장애가 다시 배포되지 않습니다.
운영성로그, 메트릭, 알림을 확인합니다.문제가 생겼을 때 원인 추적 경로가 있습니다.
← 이전 가이드클라우드 입문 & 로드맵다음 가이드 →GCP