Neden bulut?
Kanat Havayolları'nın veri ekibi her gece uçuş, rezervasyon ve gecikme dosyalarını işliyor. İlk yıl tek bir sunucu yetti. Sonra veri büyüdü, sunucunun diski doldu, bir gece disk bozuldu ve iki günlük veri kayboldu; yeni sunucu almak haftalar sürdü. Bulut (cloud) bu sorunları kiralık, ölçeklenebilir ve yönetilen hizmetlerle çözer: depolamayı terabayt terabayt değil gigabayt-ay olarak ödersin, sunucuyu dakikalar içinde açıp kapatırsın, yedekleme ve donanım arızası sağlayıcının sorunudur.
Bu derste bir veri ya da backend mühendisinin bulutta ilk gün ihtiyaç duyduğu temelleri öğreneceksin: hizmet modelleri, bölgeler, nesne depolama (S3) ve boto3, data lake klasör düzeni, IAM ve en az yetki ilkesi, imzalı URL, serverless fonksiyonlar, yönetilen veritabanları ve maliyet. Uygulamayı Docker ile paketlemeyi docker-deploy dersinde gördün; bu ders o paketin nereye ve nasıl gideceğini anlatır.
pip install boto3 moto. Gerçek hesapta denerken ücretsiz katman (free tier) sınırlarını ve bütçe alarmını ayarlamayı unutma. Yeşil kutular ve alıştırmalar boto3'ün arayüzünü taklit eden bellek içi bir SahteS3 sınıfı ve saf Python ile çalışır.Hizmet modelleri, bölgeler ve sağlayıcılar
- IaaS (altyapı hizmeti): sanal makine, disk, ağ kiralarsın; işletim sistemi ve yazılım senin sorumluluğundur. Örnek: EC2.
- PaaS (platform hizmeti): kodunu ya da konteynerini verirsin, sunucuyu sağlayıcı yönetir. Örnek: Cloud Run, App Service, yönetilen veritabanı.
- SaaS (yazılım hizmeti): hazır uygulamayı kullanırsın. Örnek: Gmail, Salesforce.
Bölge (region) coğrafi bir veri merkezi grubudur (ör. Frankfurt); kullanılabilirlik alanı (availability zone, AZ) bölge içindeki birbirinden bağımsız elektrik ve ağa sahip veri merkezidir. Kritik sistemler en az iki AZ'ye yayılır; bir AZ çökerse diğeri çalışmaya devam eder. Bölge seçimi gecikme (kullanıcıya yakınlık), fiyat ve yasal veri yerleşimi kurallarına göre yapılır.
| Hizmet | AWS | Google Cloud | Azure |
|---|---|---|---|
| Nesne depolama | S3 | Cloud Storage (GCS) | Blob Storage |
| Sanal makine | EC2 | Compute Engine | Virtual Machines |
| Serverless fonksiyon | Lambda | Cloud Run functions | Azure Functions |
| Konteyner çalıştırma | ECS / Fargate | Cloud Run | Container Apps |
| Yönetilen PostgreSQL | RDS / Aurora | Cloud SQL | Azure Database for PostgreSQL |
| Veri ambarı | Redshift | BigQuery | Synapse / Fabric |
| Kimlik ve yetki | IAM | Cloud IAM | Entra ID + RBAC |
Nesne depolama: bucket, key, prefix
S3 bir dosya sistemi değildir; anahtar-değer deposudur. Bucket (kova) küresel olarak benzersiz adlı bir kaptır; içindeki her nesne (object) bir anahtar (key) ile saklanır: ham/kanat/ucuslar/yil=2024/ay=09/gun=01/part-000.csv. Konsolda "klasör" gibi görünen şey anahtarın /'a kadarki önekidir (prefix); gerçek klasör yoktur. Nesneler değiştirilmez, üstüne yazılır; "dosyanın sonuna ekle" yoktur.
import boto3
s3 = boto3.client("s3", region_name="eu-central-1") # kimlik bilgisi ortamdan/rolden gelir
s3.upload_file("ucuslar.csv", "kanat-veri", "ham/kanat/ucuslar/yil=2024/ay=09/gun=01/part-000.csv")
nesne = s3.get_object(Bucket="kanat-veri", Key="ham/kanat/ucuslar/yil=2024/ay=09/gun=01/part-000.csv")
metin = nesne["Body"].read().decode("utf-8")
# Listeleme en fazla 1000 anahtar döndürür: sayfalayıcı (paginator) kullan
sayfalayici = s3.get_paginator("list_objects_v2")
for sayfa in sayfalayici.paginate(Bucket="kanat-veri", Prefix="ham/kanat/ucuslar/yil=2024/ay=09/"):
for nesne in sayfa.get("Contents", []):
print(nesne["Key"], nesne["Size"])
Aynı arayüzü bellek içinde taklit eden bir sınıfla tarayıcıda deneyelim. Alıştırmalarda bu SahteS3 sınıfını kullanacaksın; metot ve parametre adları boto3 ile aynıdır, böylece kodun gerçek istemciye neredeyse değişmeden taşınır.
class NoSuchKey(KeyError):
pass
class SahteS3:
"""boto3 S3 istemcisinin bellek içi, sadeleştirilmiş taklidi."""
def __init__(self):
self.kovalar = {}
def create_bucket(self, Bucket):
self.kovalar.setdefault(Bucket, {})
def put_object(self, Bucket, Key, Body):
if isinstance(Body, str):
Body = Body.encode("utf-8")
self.kovalar[Bucket][Key] = Body
return {"ETag": str(hash(Body))}
def get_object(self, Bucket, Key):
try:
return {"Body": self.kovalar[Bucket][Key], "ContentLength": len(self.kovalar[Bucket][Key])}
except KeyError:
raise NoSuchKey(Key) from None
def delete_object(self, Bucket, Key):
self.kovalar[Bucket].pop(Key, None)
def list_objects_v2(self, Bucket, Prefix="", MaxKeys=1000, ContinuationToken=None):
anahtarlar = sorted(k for k in self.kovalar[Bucket] if k.startswith(Prefix))
if ContinuationToken:
anahtarlar = [k for k in anahtarlar if k > ContinuationToken]
sayfa = anahtarlar[:MaxKeys]
yanit = {"KeyCount": len(sayfa), "IsTruncated": len(anahtarlar) > MaxKeys}
if sayfa: # gerçek S3 gibi: boş sonuçta Contents yok
yanit["Contents"] = [{"Key": k, "Size": len(self.kovalar[Bucket][k])} for k in sayfa]
if yanit["IsTruncated"]:
yanit["NextContinuationToken"] = sayfa[-1]
return yanit
s3 = SahteS3()
s3.create_bucket(Bucket="kanat-veri")
for gun in ["01", "02", "03"]:
s3.put_object(Bucket="kanat-veri", Key=f"ham/kanat/ucuslar/yil=2024/ay=09/gun={gun}/part-000.csv",
Body="flight_id,status\n1,landed\n")
s3.put_object(Bucket="kanat-veri", Key="ham/kanat/gecikmeler/yil=2024/ay=09/gun=01/part-000.csv", Body="x")
yanit = s3.list_objects_v2(Bucket="kanat-veri", Prefix="ham/kanat/ucuslar/", MaxKeys=2)
print([n["Key"].split("/")[-2] for n in yanit["Contents"]], yanit["IsTruncated"])
print(s3.get_object(Bucket="kanat-veri", Key="ham/kanat/ucuslar/yil=2024/ay=09/gun=01/part-000.csv")["Body"].decode())
Data lake yerleşimi ve bölümleme
Data lake (veri gölü), farklı kaynaklardan gelen ham ve işlenmiş verinin nesne depolamada düzenli bir yol şemasıyla tutulmasıdır. Yaygın katman düzeni (madalyon mimarisi olarak da bilinir):
| Katman | İçerik | Kural |
|---|---|---|
ham/ (bronze) | Kaynaktan geldiği gibi | Asla değiştirilmez; yeniden işleme kaynağı |
temiz/ (silver) | Tür dönüşümü, tekilleştirme, şema doğrulama | Genellikle Parquet |
mart/ (gold) | İş için özetlenmiş tablolar | Panolar ve analistler okur |
Her katmanda veri tarihe göre bölümlenir (partitioning). Hive biçimi (yil=2024/ay=09/gun=01) sorgu motorlarının (Athena, BigQuery, Spark, DuckDB) yalnızca ilgili klasörleri okumasını sağlar (partition pruning): "Eylül verisi" sorgusu diğer ayları hiç açmaz. Ay ve gün iki haneli yazılmalıdır, yoksa metin sıralaması bozulur (ay=10 < ay=9).
from datetime import date
def lake_yolu(katman, kaynak, tablo, gun, parca=0):
if katman not in {"ham", "temiz", "mart"}:
raise ValueError(f"bilinmeyen katman: {katman}")
return f"{katman}/{kaynak}/{tablo}/yil={gun.year}/ay={gun.month:02d}/gun={gun.day:02d}/part-{parca:03d}.csv"
print(lake_yolu("ham", "kanat", "ucuslar", date(2024, 9, 1)))
print(sorted(["ay=9", "ay=10", "ay=11"]), sorted(["ay=09", "ay=10", "ay=11"]))
IAM ve en az yetki
IAM (Identity and Access Management) "kim, hangi kaynakta, hangi işlemi yapabilir?" sorusunu yönetir. Yetki JSON politikalarıyla verilir. Değerlendirme kuralı basittir: varsayılan olarak her şey yasaktır; bir Allow izin verir; açık bir Deny her zaman kazanır.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "HamKatmaniOku",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::kanat-veri/ham/kanat/*"
},
{
"Sid": "MartKatmaninaYaz",
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::kanat-veri/mart/*"
}
]
}
En az yetki ilkesi (least privilege): her kimlik yalnızca işini yapmak için gereken eylemlere, yalnızca gereken kaynaklarda sahip olmalı. "Action": "s3:*" ve "Resource": "*" görmek kod incelemesinde (code review) kırmızı bayraktır.
| Erişim anahtarı (access key) | Rol (role) | |
|---|---|---|
| Nedir? | Kalıcı kullanıcı adı/parola çifti | Geçici kimlik bilgisi veren kimlik |
| Süre | Silinene kadar geçerli | Dakikalar/saatler sonra kendiliğinden düşer |
| Risk | Koda, git'e, loga sızabilir | Sızsa bile kısa ömürlü |
| Kullanım | Mümkünse hiç; zorunluysa kasada (secrets manager) | EC2, Lambda, konteyner, CI (OIDC) için standart |
boto3 kimlik bilgisini sırayla ortam değişkenlerinden, ~/.aws profilinden ve çalıştığı makinenin rolünden arar. Koda asla anahtar yazma; yazdıysan anahtarı hemen iptal et (git geçmişinden silmek yetmez).
İmzalı URL (presigned URL)
Bir müşteriye özel bir faturayı indirtmek için kovayı herkese açmak yerine süreli, imzalı bir bağlantı üretirsin: s3.generate_presigned_url("get_object", Params={...}, ExpiresIn=900). URL; nesne anahtarı, son kullanma zamanı ve gizli anahtarla üretilmiş bir HMAC imzası içerir. Sunucu imzayı yeniden hesaplar, eşleşmiyorsa ya da süre geçtiyse reddeder. Mantığı standart kütüphaneyle:
import hashlib
import hmac
GIZLI = b"sunucuda-saklanan-gizli-anahtar"
def imzala(anahtar, bitis):
mesaj = f"{anahtar}|{bitis}".encode()
return hmac.new(GIZLI, mesaj, hashlib.sha256).hexdigest()[:16]
bitis = 1_735_700_000 + 900 # simdi + 15 dakika (epoch saniye)
imza = imzala("fatura/2024/12/F-1001.pdf", bitis)
print(f"https://kanat-veri.s3.amazonaws.com/fatura/2024/12/F-1001.pdf?expires={bitis}&sig={imza}")
print(hmac.compare_digest(imza, imzala("fatura/2024/12/F-1001.pdf", bitis)),
hmac.compare_digest(imza, imzala("fatura/2024/12/F-1002.pdf", bitis)))
Serverless fonksiyon ve olay tetikleyici
Serverless (sunucusuz) fonksiyonda yalnız bir fonksiyon yazarsın; sağlayıcı onu bir olay geldiğinde çalıştırır, milisaniye başına ücret alır, boşta maliyet sıfırdır. Tipik veri kullanımı: ham/'a yeni dosya düşünce S3 bir olay üretir, Lambda dosyayı okuyup doğrular ve temiz/'e yazar. Sınırları: çalışma süresi sınırı (AWS Lambda'da 15 dakika), soğuk başlangıç (cold start) gecikmesi, sınırlı bellek/disk. Uzun işler için konteyner (Fargate, Cloud Run) ya da toplu iş (batch) hizmetleri seçilir.
import json, urllib.parse, boto3
s3 = boto3.client("s3")
def handler(event, context):
for kayit in event["Records"]:
kova = kayit["s3"]["bucket"]["name"]
anahtar = urllib.parse.unquote_plus(kayit["s3"]["object"]["key"]) # olaydaki anahtar URL kodludur!
govde = s3.get_object(Bucket=kova, Key=anahtar)["Body"].read()
# ... doğrula, dönüştür ...
s3.put_object(Bucket=kova, Key=anahtar.replace("ham/", "temiz/", 1), Body=govde)
return {"statusCode": 200, "body": json.dumps({"islenen": len(event["Records"])})}
Yönetilen veritabanı ve ambar
Kendi sunucunda PostgreSQL kurmak yerine yönetilen hizmet (RDS, Cloud SQL) yedekleme, yama, çoğaltma ve AZ'ler arası yedekliliği üstlenir; sen şema ve sorguyla ilgilenirsin (postgresql-python dersi). Analitik için BigQuery (sunucusuz, taranan bayt başına ücret), Snowflake (depolama ve hesaplama ayrı ölçeklenir) ve Redshift gibi ambarlar kullanılır; dbt modelleri (dbt-elt) bu ambarlarda çalışır. Ortak tasarım ilkesi: depolama ve hesaplamanın ayrılması. Veri ucuz nesne depolamada durur, sorgu anında hesaplama kiralanır.
Maliyet tuzakları
Bulut faturası birkaç kalemden oluşur: depolama (GB-ay), istek sayısı (her PUT/GET/LIST), dışarı çıkan trafik (egress: buluttan internete ya da başka bölgeye veri aktarımı) ve hesaplama süresi. En sık sürprizler:
- Çıkış trafiği: veriyi buluta yüklemek genellikle ücretsiz, dışarı çekmek ücretlidir. Analizi verinin bulunduğu bölgede yap.
- Küçük dosyalar: saatte binlerce 5 KB'lık dosya hem istek ücretini patlatır hem de sorgu motorlarını yavaşlatır. Dosyaları yüzlerce MB'lık parçalarda birleştir (compaction).
- Unutulan kaynaklar: test için açılıp kapatılmayan makineler, ekli kalmış diskler, eski anlık görüntüler. Etiketleme (tag) ve bütçe alarmı şart.
- Yaşam döngüsü yok: 3 yıllık ham log'u en pahalı depolama sınıfında tutmak. Eski veriyi soğuk sınıfa taşıyan ya da silen yaşam döngüsü (lifecycle) kuralı yaz.
# Temsili birim fiyatlar (gerçek fiyatlar bölgeye ve zamana göre değişir; sağlayıcının fiyat sayfasına bak)
FIYAT = {"depolama_gb_ay": 0.023, "put_1000": 0.005, "get_1000": 0.0004, "cikis_gb": 0.09}
def aylik(gb, put, get, cikis_gb):
return round(gb * FIYAT["depolama_gb_ay"] + put / 1000 * FIYAT["put_1000"]
+ get / 1000 * FIYAT["get_1000"] + cikis_gb * FIYAT["cikis_gb"], 2)
# Aynı 500 GB veri: saatte 3600 küçük dosya mı, saatte 1 büyük dosya mı?
print("küçük dosyalar:", aylik(500, 3600 * 24 * 30, 3600 * 24 * 30 * 5, 50), "$")
print("birleştirilmiş:", aylik(500, 24 * 30, 24 * 30 * 5, 50), "$")
Altyapıyı kod olarak yönetmek
Konsolda tıklayarak kurulan altyapı tekrarlanamaz ve denetlenemez. IaC (Infrastructure as Code) araçlarında (Terraform, Pulumi, AWS CDK) kovalar, roller ve politikalar dosyada tanımlanır, kod incelemesinden geçer ve plan ile değişiklik önceden görülür.
# main.tf
resource "aws_s3_bucket" "veri" {
bucket = "kanat-veri"
tags = { ekip = "veri", ortam = "prod" }
}
resource "aws_s3_bucket_lifecycle_configuration" "ham" {
bucket = aws_s3_bucket.veri.id
rule {
id = "ham-soguk"
status = "Enabled"
filter { prefix = "ham/" }
transition { days = 90 storage_class = "GLACIER" }
}
}
Test: sahte bulut
Birim testlerinde gerçek buluta gitmek yavaş, pahalı ve kırılgandır. moto kütüphanesi boto3 çağrılarını bellek içinde karşılar; alıştırmalardaki SahteS3 ile aynı fikir. Entegrasyon testleri için LocalStack gibi yerel emülatörler de vardır.
import boto3
from moto import mock_aws
@mock_aws
def test_rapor_mart_katmanina_yazilir():
s3 = boto3.client("s3", region_name="eu-central-1")
s3.create_bucket(Bucket="kanat-veri", CreateBucketConfiguration={"LocationConstraint": "eu-central-1"})
rapor_uret(s3, "kanat-veri", "2024-09-01") # test edilen fonksiyon
anahtarlar = [o["Key"] for o in s3.list_objects_v2(Bucket="kanat-veri", Prefix="mart/")["Contents"]]
assert anahtarlar == ["mart/kanat/gunluk/yil=2024/ay=09/gun=01/rapor.json"]
s3:* yetkili bir uygulama rolü. Kıdemli ekipler üç kuralı otomatikleştirir: hesap düzeyinde "herkese açık erişimi engelle" ayarı, uzun ömürlü anahtar yerine rol ve OIDC, ve her kaynağa sahibini ve maliyet merkezini gösteren etiket. Faturayı her ay ekip bazında incelemek de mühendisliğin parçasıdır (FinOps).Azure ve BigQuery karşılıkları
Türkiye'deki ilanlarda Azure ve BigQuery sık geçer. Kavramlar aynı, adlar farklıdır. İstemciler ağ ve hesap ister; tarayıcıda çalışmaz.
| Kavram | AWS | Azure | Google Cloud |
|---|---|---|---|
| Nesne depolama | S3 | Blob Storage | Cloud Storage (GCS) |
| Veri ambarı | Redshift | Synapse, Fabric | BigQuery |
| Veri taşıma ve zamanlama | Glue, Step Functions | Data Factory | Dataflow, Composer |
| Python kütüphanesi | boto3 | azure-storage-blob | google-cloud-storage, google-cloud-bigquery |
Tablo biçimleri
Data lake'te düz Parquet dosyalarının üstüne Apache Iceberg, Delta Lake ve Hudi gibi açık tablo biçimleri ACID işlem, şema evrimi, zamanda yolculuk (eski sürümü sorgulama) ve satır düzeyinde güncelleme ekler. "Lakehouse" mimarisi bunların üstüne kurulur.
Tutarlılık ve idempotent yazma
S3 bugün yazdığını hemen okuyabildiğin güçlü tutarlılık sunar; ama olay tetikleyicileri en az bir kez (at-least-once) teslim edilir. Aynı olay iki kez gelebilir; Lambda'n aynı girdiyle iki kez çalışınca aynı sonucu üretmeli (deterministik çıktı anahtarı, üstüne yazma).
Ağ ve şifreleme
VPC (sanal özel ağ), alt ağlar ve güvenlik grupları veritabanını internetten ayırır; VPC endpoint ile S3 trafiği internete çıkmadan gider (hem güvenlik hem çıkış maliyeti). Kovalarda varsayılan şifreleme açık olmalı; hassas veride anahtarı kendin yönettiğin KMS anahtarı kullan.
Gözlemlenebilirlik
CloudWatch, Cloud Logging gibi hizmetler logları ve metrikleri toplar; S3 erişim logları ve CloudTrail "kim, ne zaman, neyi okudu?" sorusunu yanıtlar. Denetim izi KVKK açısından da gereklidir.