DEV Community

Cover image for AWS VPC Networking — Public Subnet, Private Subnet ve 3-Tier Mimari
Taha Yağız Güler
Taha Yağız Güler

Posted on

AWS VPC Networking — Public Subnet, Private Subnet ve 3-Tier Mimari

Temel Kavramlar

CIDR — IP Adresi Aralığı

10.0.0.0/16 → 65,536 IP adresi  (VPC için)
10.0.1.0/24 → 256 IP adresi     (subnet için)
Enter fullscreen mode Exit fullscreen mode

VPC CIDR'ı subnet'leri kapsamalı — 10.0.0.0/16 VPC'nin içinde 10.0.1.0/24 subnet açılabilir.

Route Table — Trafik Yönlendirme

Subnet'e gelen trafiğin nereye gideceğini söyler. Public ve private subnet'in tek teknik farkı burada:

Public subnet route table:
  10.0.0.0/16local   (VPC içi)
  0.0.0.0/0IGW     (internete çık)

Private subnet route table:
  10.0.0.0/16local   (VPC içi)
  0.0.0.0/0NAT     (NAT üzerinden dışa çık)

DB subnet route table:
  10.0.0.0/16local   (sadece VPC içiinternet yok)
Enter fullscreen mode Exit fullscreen mode

IGW vs NAT Gateway

Internet Gateway: İki yönlü kapı. Dışarıdan içeri, içeriden dışarı trafik geçer. Public subnet'teki kaynaklar public IP alır, doğrudan internete açılır.

NAT Gateway: Tek yönlü kapı. Sadece içeriden dışarıya. Private subnet'teki EC2 dnf update yapmak için NAT üzerinden çıkar ama dışarıdan bu EC2'ya doğrudan erişilemez.


Kurduğumuz Mimari

İnternet
    ↓
Internet Gateway
    ↓
┌─────────────────────────────────────┐
│ Public Subnet (AZ-a + AZ-b)         │
│ ALB + NAT Gateway                   │
│ Route: 0.0.0.0/0 → IGW              │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│ Private Subnet (AZ-a + AZ-b)        │
│ EC2 — Uygulama Sunucusu             │
│ Route: 0.0.0.0/0 → NAT             │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│ DB Subnet (AZ-a + AZ-b)             │
│ RDS — PostgreSQL                    │
│ Route: sadece local (internet yok)  │
└─────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Her katmanda 2 AZ — biri düşerse diğeri devam eder.


Terraform ile VPC Kurulumu

VPC ve Subnet'ler

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = { Name = "${var.environment}-vpc" }
}

# Public Subnet — 2 AZ
resource "aws_subnet" "public" {
  count             = length(var.azs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 1)
  availability_zone = var.azs[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.environment}-public-${var.azs[count.index]}"
    Tier = "public"
  }
}

# Private Subnet — 2 AZ
resource "aws_subnet" "private" {
  count             = length(var.azs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 3)
  availability_zone = var.azs[count.index]

  tags = {
    Name = "${var.environment}-private-${var.azs[count.index]}"
    Tier = "private"
  }
}

# DB Subnet — 2 AZ
resource "aws_subnet" "db" {
  count             = length(var.azs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 5)
  availability_zone = var.azs[count.index]

  tags = {
    Name = "${var.environment}-db-${var.azs[count.index]}"
    Tier = "db"
  }
}
Enter fullscreen mode Exit fullscreen mode

cidrsubnet(var.vpc_cidr, 8, count.index + 1) otomatik CIDR hesaplıyor:

  • public: 10.0.1.0/24, 10.0.2.0/24
  • private: 10.0.3.0/24, 10.0.4.0/24
  • db: 10.0.5.0/24, 10.0.6.0/24

IGW, NAT ve Route Table'lar

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

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

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public[0].id  # NAT public subnet'te olmalı
  depends_on    = [aws_internet_gateway.main]
}

# Public route table — IGW üzerinden internete
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
  }
}

# Private route table — NAT üzerinden dışa
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
  }
}

# DB route table — internet yok
resource "aws_route_table" "db" {
  vpc_id = aws_vpc.main.id
  # 0.0.0.0/0 route yok
}
Enter fullscreen mode Exit fullscreen mode

Security Group Tasarımı

En kritik nokta: IP aralığı değil, SG referansı kullan.

# ALB SG — internetten 80/443
resource "aws_security_group" "alb" {
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# App SG — sadece ALB SG'den trafik alır
resource "aws_security_group" "app" {
  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
    # IP değil SG referansı — ALB IP'si değişse bile kural geçerli
  }
}

# RDS SG — sadece App SG'den trafik alır
resource "aws_security_group" "rds" {
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
}
Enter fullscreen mode Exit fullscreen mode

Bu zincir şunu sağlıyor:

  • İnternet → ALB (port 80)
  • ALB → EC2 (port 8080)
  • EC2 → RDS (port 5432)
  • İnternet → EC2 ❌
  • İnternet → RDS ❌
  • EC2 → RDS doğrudan ❌ (sadece app-sg üzerinden)

ALB — Public Subnet

resource "aws_lb" "main" {
  name               = "${var.environment}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [var.alb_sg_id]
  subnets            = var.public_subnet_ids  # 2 AZ
}

resource "aws_lb_target_group" "app" {
  port     = 8080
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  health_check {
    path                = "/health"
    healthy_threshold   = 2
    unhealthy_threshold = 3
  }
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}
Enter fullscreen mode Exit fullscreen mode

EC2 — Private Subnet

resource "aws_instance" "app" {
  instance_type          = "t3.micro"
  subnet_id              = var.private_subnet_ids[0]
  vpc_security_group_ids = [var.app_sg_id]

  # Public IP yok — private subnet'te
  user_data = <<-USERDATA
    #!/bin/bash
    dnf install -y nginx
    cat > /etc/nginx/conf.d/app.conf << 'NGINX'
    server {
      listen 8080;
      location / {
        return 200 'Hello from private subnet!\n';
      }
      location /health {
        return 200 '{"status":"ok"}';
      }
    }
    NGINX
    systemctl restart nginx
  USERDATA
}

# EC2'yu ALB target group'a bağla
resource "aws_lb_target_group_attachment" "app" {
  target_group_arn = var.target_group_arn
  target_id        = aws_instance.app.id
  port             = 8080
}
Enter fullscreen mode Exit fullscreen mode

RDS — DB Subnet

resource "aws_db_subnet_group" "main" {
  subnet_ids = var.db_subnet_ids  # DB subnet'ler
}

resource "aws_db_instance" "main" {
  engine         = "postgres"
  engine_version = "15"
  instance_class = "db.t3.micro"

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [var.rds_sg_id]

  publicly_accessible = false  # internet'ten erişilemez
  skip_final_snapshot = true
}
Enter fullscreen mode Exit fullscreen mode

Doğrulama Testleri

Test 1 — ALB üzerinden erişim:

curl http://<alb-dns-name>
# Hello from private subnet!
Enter fullscreen mode Exit fullscreen mode

Test 2 — EC2 private IP, public IP yok:

terraform output app_private_ip
# 10.0.3.111 — public IP yok
Enter fullscreen mode Exit fullscreen mode

Test 3 — RDS dışarıdan erişilemiyor:

nc -zv <rds-address> 5432 -w 5
# Connection timed out
Enter fullscreen mode Exit fullscreen mode

Test 4 — Private EC2 NAT üzerinden dışa çıkabiliyor:

SSM Session Manager ile EC2'ya bağlanıp curl ifconfig.me yaptığında NAT Gateway'in Elastic IP'sini görüyorsun — EC2'nun kendi IP'si değil.


Production Best Practices

Her AZ'de ayrı NAT Gateway:

# Lab'da tek NAT — production'da her AZ için
resource "aws_nat_gateway" "main" {
  count         = length(var.azs)
  subnet_id     = aws_subnet.public[count.index].id
  allocation_id = aws_eip.nat[count.index].id
}
Enter fullscreen mode Exit fullscreen mode

Tek NAT Gateway varken o AZ düşerse tüm private subnet'lerin internet erişimi kesilir.

Bastion host yerine SSM:

Eski: Bastion host (public) → SSH → private EC2
Yeni: SSM Session Manager → private EC2
Enter fullscreen mode Exit fullscreen mode

SSM ile SSH portu kapalı, bastion host maliyeti yok, key yönetimi yok. IAM policy ile kimin hangi instance'a bağlanabileceği kontrol ediliyor.

VPC Flow Logs:

resource "aws_flow_log" "main" {
  vpc_id       = aws_vpc.main.id
  traffic_type = "ALL"
}
Enter fullscreen mode Exit fullscreen mode

Güvenlik olaylarında "kim nereye bağlandı" sorusunun cevabı burada.

RDS multi-AZ:

resource "aws_db_instance" "main" {
  multi_az = true  # otomatik failover
}
Enter fullscreen mode Exit fullscreen mode

Öğrendiklerim

Bu lab'dan önce "public subnet internete açık, private subnet kapalı" diyordum. Şimdi şunu söyleyebiliyorum:

Public ve private subnet'in tek teknik farkı route table'da — public'te 0.0.0.0/0 → IGW var, private'te 0.0.0.0/0 → NAT var, DB subnet'te hiç internet route'u yok.

IGW iki yönlü, NAT tek yönlü. Private EC2 güncelleme yapmak için NAT kullanır ama dışarıdan ona ulaşılamaz. RDS DB subnet'te çünkü oradan internet rotası yok — en kritik veri en izole katmanda.

Security group'larda IP aralığı değil SG referansı kullanmak önemli — ALB IP'si değişse bile app-sg kuralı bozulmaz.


Bu yazı, bir mülakattan aldığım geri bildirimi uygulamalı olarak çalışma serim.
Serinin diğer yazıları:

All code from this lab is available on GitHub. If you spot something that could be done better, I'd genuinely love to hear it in the comments.

Top comments (0)