AWS EC2 ve VPC Kurulumu: Terraform ile Adım Adım Rehber

Altyapıyı elle kurmak bir noktadan sonra sürdürülemez hale gelir. Bir EC2 instance’ı ayağa kaldırmak için AWS konsoluna girip tıklamak ilk seferinde hızlı gelebilir, ama aynı ortamı staging, production ve DR için tekrar etmek zorunda kaldığında işler karmaşıklaşır. Terraform tam bu noktada devreye giriyor: altyapını kod olarak tanımlıyorsun, versiyon kontrolüne alıyorsun ve tekrarlanabilir deployment’lar yapıyorsun. Bu yazıda gerçek dünya senaryolarıyla AWS üzerinde VPC ve EC2 kurulumunu Terraform ile nasıl yapacağını adım adım anlatacağım.

Ön Hazırlık: Ortamını Hazırla

Başlamadan önce birkaç şeyi yerine getirmen gerekiyor. Terraform CLI kurulu olmalı, AWS CLI yapılandırılmış olmalı ve tabii ki bir AWS hesabın bulunmalı.

Terraform kurulumu için:

# Linux üzerinde Terraform kurulumu
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform -y

# Kurulumu doğrula
terraform version

AWS CLI tarafında ise credentials’larını yapılandırman gerekiyor. Burada IAM kullanıcısı yerine IAM role kullanmayı öneririm, özellikle CI/CD pipeline’larında çalışıyorsan. Ama lokal geliştirme için şimdilik kullanıcı bazlı devam edelim:

aws configure
# AWS Access Key ID: [buraya kendi key'ini gir]
# AWS Secret Access Key: [buraya kendi secret'ını gir]
# Default region name: eu-west-1
# Default output format: json

# Bağlantıyı test et
aws sts get-caller-identity

Proje dizin yapısını da baştan düzgün kuralım. Küçük projelerde her şeyi tek dosyaya yazabilirsin, ama büyüdükçe pişman olursun. Modüler bir yapı ile başlamak mantıklı:

mkdir -p terraform-aws-demo/{modules/vpc,modules/ec2,environments/prod,environments/staging}
cd terraform-aws-demo
touch main.tf variables.tf outputs.tf providers.tf

Provider ve Backend Yapılandırması

providers.tf dosyası ile başlayalım. Provider versiyonlarını sabitlemek önemli, yoksa bir gün terraform init çalıştırdığında breaking change ile karşılabilirsin:

# providers.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  # Remote state için S3 backend - production'da mutlaka kullan
  backend "s3" {
    bucket         = "sirket-terraform-state"
    key            = "prod/vpc-ec2/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "Terraform"
      Project     = var.project_name
    }
  }
}

S3 backend kullanıyorsan DynamoDB tablosunu da önceden oluşturman gerekiyor. Bu tabloyu state locking için kullanıyoruz, yani iki kişi aynı anda terraform apply çalıştırırsa birisi beklemek zorunda kalıyor:

# State bucket oluştur
aws s3api create-bucket 
  --bucket sirket-terraform-state 
  --region eu-west-1 
  --create-bucket-configuration LocationConstraint=eu-west-1

# Bucket versioning aç
aws s3api put-bucket-versioning 
  --bucket sirket-terraform-state 
  --versioning-configuration Status=Enabled

# DynamoDB lock tablosu oluştur
aws dynamodb create-table 
  --table-name terraform-state-lock 
  --attribute-definitions AttributeName=LockID,AttributeType=S 
  --key-schema AttributeName=LockID,KeyType=HASH 
  --billing-mode PAY_PER_REQUEST 
  --region eu-west-1

VPC Modülü: Ağ Altyapısını Kur

VPC, tüm AWS altyapısının temeli. Bunu doğru tasarlamadan EC2’ya geçme. Gerçek dünyada çoğu zaman şu senaryo karşıma çıkıyor: geliştirici EC2 instance’ını default VPC’ye atıyor, sonra production’da “neden public subnet’te neden?” sorusu çıkıyor ortaya. Baştan doğru yapalım.

modules/vpc/main.tf:

# modules/vpc/main.tf
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

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

# Public Subnet'ler - Load Balancer ve NAT Gateway burada olacak
resource "aws_subnet" "public" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = var.availability_zones[count.index]

  map_public_ip_on_launch = true

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

# Private Subnet'ler - EC2 instance'ları burada olacak
resource "aws_subnet" "private" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
  availability_zone = var.availability_zones[count.index]

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

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-${var.environment}-igw"
  }
}

# Elastic IP for NAT Gateway
resource "aws_eip" "nat" {
  count  = var.enable_nat_gateway ? length(var.availability_zones) : 0
  domain = "vpc"

  tags = {
    Name = "${var.project_name}-${var.environment}-nat-eip-${count.index + 1}"
  }
}

# NAT Gateway - private subnet'lerin internete çıkması için
resource "aws_nat_gateway" "main" {
  count         = var.enable_nat_gateway ? length(var.availability_zones) : 0
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "${var.project_name}-${var.environment}-nat-${count.index + 1}"
  }

  depends_on = [aws_internet_gateway.main]
}

# Public Route Table
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
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-public-rt"
  }
}

# Private Route Table - her AZ için ayrı
resource "aws_route_table" "private" {
  count  = var.enable_nat_gateway ? length(var.availability_zones) : 1
  vpc_id = aws_vpc.main.id

  dynamic "route" {
    for_each = var.enable_nat_gateway ? [1] : []
    content {
      cidr_block     = "0.0.0.0/0"
      nat_gateway_id = aws_nat_gateway.main[count.index].id
    }
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-private-rt-${count.index + 1}"
  }
}

# Route Table Association'lar
resource "aws_route_table_association" "public" {
  count          = length(var.availability_zones)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  count          = length(var.availability_zones)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[var.enable_nat_gateway ? count.index : 0].id
}

VPC modülünün değişkenlerini tanımlayalım:

# modules/vpc/variables.tf
variable "project_name" {
  description = "Proje adı, tüm resource isimlerinde kullanılır"
  type        = string
}

variable "environment" {
  description = "Ortam adı: prod, staging, dev"
  type        = string
}

variable "vpc_cidr" {
  description = "VPC CIDR bloğu"
  type        = string
  default     = "10.0.0.0/16"
}

variable "availability_zones" {
  description = "Kullanılacak AZ listesi"
  type        = list(string)
}

variable "enable_nat_gateway" {
  description = "NAT Gateway oluşturulsun mu? Dev ortamda false yapabilirsin, maliyet azalır"
  type        = bool
  default     = true
}

# modules/vpc/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

EC2 Modülü: Instance ve Security Group

EC2 modülünde dikkat etmem gereken birkaç konu var: Security Group’ları minimal tutmak, instance’a IAM role atamak ve user_data ile başlangıç yapılandırmasını otomatikleştirmek.

modules/ec2/main.tf:

# modules/ec2/main.tf

# En güncel Amazon Linux 2023 AMI'ı otomatik bul
data "aws_ami" "amazon_linux_2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Security Group
resource "aws_security_group" "ec2" {
  name_prefix = "${var.project_name}-${var.environment}-ec2-"
  vpc_id      = var.vpc_id
  description = "EC2 instance security group"

  # SSH erişimi - sadece belirli IP'lerden
  ingress {
    description = "SSH from bastion or VPN"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = var.allowed_ssh_cidrs
  }

  # Uygulama portu - sadece VPC içinden
  ingress {
    description = "App port from VPC"
    from_port   = var.app_port
    to_port     = var.app_port
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }

  # Outbound tamamen açık
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-ec2-sg"
  }
}

# IAM Role - EC2 için
resource "aws_iam_role" "ec2" {
  name = "${var.project_name}-${var.environment}-ec2-role"

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

# SSM için managed policy ekle - SSH yerine SSM Session Manager kullanabilirsin
resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.ec2.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ec2" {
  name = "${var.project_name}-${var.environment}-ec2-profile"
  role = aws_iam_role.ec2.name
}

# EC2 Instance
resource "aws_instance" "main" {
  count = var.instance_count

  ami                    = data.aws_ami.amazon_linux_2023.id
  instance_type          = var.instance_type
  subnet_id              = var.private_subnet_ids[count.index % length(var.private_subnet_ids)]
  vpc_security_group_ids = [aws_security_group.ec2.id]
  iam_instance_profile   = aws_iam_instance_profile.ec2.name
  key_name               = var.key_name

  root_block_device {
    volume_type           = "gp3"
    volume_size           = var.root_volume_size
    encrypted             = true
    delete_on_termination = true
  }

  user_data = base64encode(templatefile("${path.module}/templates/userdata.sh", {
    environment  = var.environment
    project_name = var.project_name
  }))

  # Bu instance'ı yanlışlıkla silmekten koru
  disable_api_termination = var.environment == "prod" ? true : false

  tags = {
    Name = "${var.project_name}-${var.environment}-ec2-${count.index + 1}"
  }
}

User data template’ini de hazırlayalım:

# modules/ec2/templates/userdata.sh
#!/bin/bash
set -e

# Sistem güncellemelerini yap
dnf update -y

# SSM Agent'ı etkinleştir
systemctl enable amazon-ssm-agent
systemctl start amazon-ssm-agent

# CloudWatch Agent kurulumu
dnf install -y amazon-cloudwatch-agent

# Temel araçları kur
dnf install -y 
  htop 
  vim 
  git 
  jq 
  curl 
  wget 
  unzip

# Hostname'i düzgün ayarla
hostnamectl set-hostname ${project_name}-${environment}-$(curl -s http://169.254.169.254/latest/meta-data/instance-id)

# CloudWatch log grubu için agent config
cat > /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json << 'EOF'
{
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/messages",
            "log_group_name": "/${project_name}/${environment}/system",
            "log_stream_name": "{instance_id}"
          }
        ]
      }
    }
  }
}
EOF

/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl 
  -a fetch-config 
  -m ec2 
  -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json 
  -s

echo "Userdata tamamlandi: $(date)" >> /var/log/userdata.log

Ana Yapılandırma: Her Şeyi Bir Araya Getir

Şimdi her şeyi main.tf dosyasında birleştirelim. Bu dosya ortam bazlı değişkenleri alıp modüllere dağıtacak:

# main.tf - Kök modül
locals {
  project_name = "webapp"
  environment  = var.environment
  aws_region   = var.aws_region

  availability_zones = [
    "${var.aws_region}a",
    "${var.aws_region}b",
    "${var.aws_region}c"
  ]
}

module "vpc" {
  source = "./modules/vpc"

  project_name       = local.project_name
  environment        = local.environment
  vpc_cidr           = var.vpc_cidr
  availability_zones = local.availability_zones
  enable_nat_gateway = var.enable_nat_gateway
}

module "ec2" {
  source = "./modules/ec2"

  project_name       = local.project_name
  environment        = local.environment
  vpc_id             = module.vpc.vpc_id
  vpc_cidr           = var.vpc_cidr
  private_subnet_ids = module.vpc.private_subnet_ids
  instance_type      = var.instance_type
  instance_count     = var.instance_count
  root_volume_size   = var.root_volume_size
  key_name           = var.key_name
  allowed_ssh_cidrs  = var.allowed_ssh_cidrs
  app_port           = var.app_port
}

# outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = module.vpc.vpc_id
}

output "private_subnet_ids" {
  description = "Private subnet ID'leri"
  value       = module.vpc.private_subnet_ids
}

output "ec2_instance_ids" {
  description = "EC2 instance ID'leri"
  value       = module.ec2.instance_ids
}

variables.tf dosyasını da hazırlayalım ve terraform.tfvars ile ortam bazlı değerleri verelim:

# terraform.tfvars - production değerleri
aws_region         = "eu-west-1"
environment        = "prod"
vpc_cidr           = "10.0.0.0/16"
enable_nat_gateway = true
instance_type      = "t3.medium"
instance_count     = 2
root_volume_size   = 50
key_name           = "prod-keypair"
allowed_ssh_cidrs  = ["10.10.0.0/24"]  # Sadece VPN IP bloğu
app_port           = 8080

Deployment: Plan, Apply ve Destroy

Her şey hazır olduğunda deployment adımları şunlar:

# Terraform'u başlat, provider'ları indir
terraform init

# Değişkeni dışarıdan geçirerek plan oluştur
terraform plan -var-file="environments/prod/terraform.tfvars" -out=tfplan

# Plan dosyasını gözden geçir
terraform show tfplan

# Uygula
terraform apply tfplan

# Sadece belirli bir modülü güncelle (örneğin EC2 güncelliyorsun VPC'ye dokunmadan)
terraform apply -target=module.ec2 -var-file="environments/prod/terraform.tfvars"

# State'i kontrol et
terraform state list
terraform state show module.ec2.aws_instance.main[0]

Yaygın Hatalar ve Çözümleri

State lock hatası: Birisi terraform apply sırasında CTRL+C yaparsa lock takılı kalabilir. DynamoDB’deki lock kaydını manuel silmek gerekiyor:

# Lock ID'yi bul
terraform force-unlock LOCK_ID

# Ya da DynamoDB'den manuel sil
aws dynamodb delete-item 
  --table-name terraform-state-lock 
  --key '{"LockID": {"S": "sirket-terraform-state/prod/vpc-ec2/terraform.tfstate"}}' 
  --region eu-west-1

AMI değişimi sonrası instance destroy/recreate: data.aws_ami her zaman en güncel AMI’ı getiriyor. Production’da bu tehlikeli olabilir. AMI ID’sini variable olarak sabitleyip controlled bir şekilde güncellemek daha güvenli:

# Mevcut AMI'ı bul ve tfvars'a kaydet
aws ec2 describe-images 
  --owners amazon 
  --filters "Name=name,Values=al2023-ami-*-x86_64" 
  --query 'sort_by(Images, &CreationDate)[-1].ImageId' 
  --output text 
  --region eu-west-1

Drift detection: Birisi konsol üzerinden değişiklik yapmışsa Terraform state ile gerçek durum farklılaşır. Bunu tespit etmek için:

# Refresh ile mevcut durumu kontrol et
terraform plan -refresh-only -var-file="environments/prod/terraform.tfvars"

Güvenlik Kontrolleri

Production’a almadan önce birkaç güvenlik kontrolü yapmak iyi pratik. tfsec veya checkov araçlarını pipeline’a dahil edebilirsin:

# tfsec kurulumu ve çalıştırma
curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash
tfsec . --minimum-severity HIGH

# checkov ile tarama
pip install checkov
checkov -d . --framework terraform

# terraform validate ile sözdizimi kontrolü
terraform validate

# terraform fmt ile kod formatını düzelt
terraform fmt -recursive

Sonuç

Terraform ile AWS VPC ve EC2 kurulumu başta karmaşık gelebilir, ama bir kez doğru şablonu oluşturduktan sonra yeni ortamlar açmak dakikalar alıyor. Burada anlattığım yapı birkaç temel prensip üzerine kuruyor: modüler yapı ile tekrar kullanılabilirlik, remote state ile ekip çalışması, security group minimalizmi ile güvenlik ve tagging standardizasyonu ile maliyet takibi.

Sonraki adım olarak bu yapıyı bir CI/CD pipeline’ına entegre etmeni öneririm. GitLab CI veya GitHub Actions ile pull request açıldığında otomatik terraform plan çalıştırıp output’u PR’a yorum olarak bırakabilirsin. Böylece ekipteki herkes ne değişeceğini görmeden merge olmaz. Bu konuyu da ilerleyen yazılarda ele alacağım.

Bir de şunu söyleyeyim: Terraform’u öğrenmenin en iyi yolu production benzeri bir ortamda denemek. AWS Free Tier hesabı açıp buradaki kodu çalıştır, hata al, debug et. Belgeler ve terraform console komutu en iyi arkadaşın olacak bu süreçte.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir