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)
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/16 → local (VPC içi)
0.0.0.0/0 → IGW (internete çık)
Private subnet route table:
10.0.0.0/16 → local (VPC içi)
0.0.0.0/0 → NAT (NAT üzerinden dışa çık)
DB subnet route table:
10.0.0.0/16 → local (sadece VPC içi — internet yok)
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) │
└─────────────────────────────────────┘
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"
}
}
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
}
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]
}
}
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
}
}
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
}
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
}
Doğrulama Testleri
Test 1 — ALB üzerinden erişim:
curl http://<alb-dns-name>
# Hello from private subnet!
Test 2 — EC2 private IP, public IP yok:
terraform output app_private_ip
# 10.0.3.111 — public IP yok
Test 3 — RDS dışarıdan erişilemiyor:
nc -zv <rds-address> 5432 -w 5
# Connection timed out
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
}
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
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"
}
Güvenlik olaylarında "kim nereye bağlandı" sorusunun cevabı burada.
RDS multi-AZ:
resource "aws_db_instance" "main" {
multi_az = true # otomatik failover
}
Öğ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ı:
- Terraform + Terragrunt + Ansible: A Hands-On Learning Journey
- Kubernetes Probe'larını Kasıtlı Bozarak Öğrendim
- Container İçine Giremiyorum — Ve Bu İyi Bir Şey: Distroless Image'lar
- Prometheus ve Grafana'yı Derinlemesine Anlamak
Top comments (0)