AWS CloudFront ile S3 Üzerinde Statik Web Sitesi Dağıtımı

Statik web sitelerini barındırmanın en maliyet etkin ve ölçeklenebilir yolu sorulduğunda, aklıma her zaman S3 ve CloudFront ikilisi gelir. Yıllarca farklı müşterilerin altyapılarını yönetirken bu kombinasyonun ne kadar güçlü olduğunu bizzat gördüm. Bir e-ticaret firmasının ürün katalogu sayfası olsun, bir SaaS şirketinin marketing sitesi olsun ya da bir startup’ın React uygulaması olsun; S3 üzerinde barınan ve CloudFront ile dağıtılan bir statik site, hem düşük maliyet hem de yüksek performans açısından rakipsiz bir çözüm sunuyor.

Bu yazıda sıfırdan başlayarak production’a hazır bir CloudFront dağıtımı kuracağız. Sadece “tıkla tıkla bitti” değil, gerçek dünya senaryolarında karşılaşacağınız sorunları, güvenlik konfigürasyonlarını ve optimizasyon ipuçlarını da ele alacağız.

Neden S3 + CloudFront?

Önce neden bu ikilinin mantıklı olduğunu anlayalım. S3 bucket’ınız tek bir region’da yaşar. Türkiye’deki bir kullanıcı eu-west-1’deki bucket’ınıza her istek attığında İrlanda’ya gidip gelir. Bu gecikmeyi hisseder. CloudFront ise AWS’nin global edge network’ünü kullanarak içeriğinizi dünya genelinde 400’den fazla lokasyona dağıtır. Kullanıcı İstanbul’daysa en yakın edge location’dan içerik alır.

Maliyet açısından da bakıldığında, S3 static website hosting zaten çok ucuz. CloudFront eklediğinizde origin’e giden istek sayısı dramatik şekilde düşer çünkü içerik edge’de cache’lenir. Sonuç olarak hem S3 transfer maliyetleriniz düşer hem de kullanıcılarınız çok daha hızlı sayfa yükleme süreleri yaşar.

Güvenlik açısından da önemli bir avantaj var: S3 bucket’ınızı tamamen private tutabilir, sadece CloudFront’un erişmesine izin verebilirsiniz. Bu sayede bucket’a doğrudan erişim engellenir.

Gereksinimler ve Hazırlık

Bu rehberde şunlara ihtiyacınız olacak:

  • AWS hesabı ve yeterli IAM izinleri
  • AWS CLI kurulu ve konfigüre edilmiş
  • Bir domain adı (opsiyonel ama önerilir)
  • ACM’de SSL sertifikası (domain kullanacaksanız)

AWS CLI kurulumunu ve konfigürasyonunu atlayarak direkt konuya girelim. Kullandığım komutlar AWS CLI v2 ile uyumludur.

S3 Bucket Oluşturma ve Konfigürasyon

İlk adım S3 bucket’ı oluşturmak. Önemli bir nokta: bucket adınızın domain adınızla aynı olması artık zorunlu değil, ama organizasyon açısından mantıklı. Ben örneklerde myapp-static-site kullanacağım.

# Bucket oluştur
aws s3api create-bucket 
  --bucket myapp-static-site 
  --region eu-west-1 
  --create-bucket-configuration LocationConstraint=eu-west-1

# Bucket versiyonlamayı aktif et (iyi pratik)
aws s3api put-bucket-versioning 
  --bucket myapp-static-site 
  --versioning-configuration Status=Enabled

# Public access'i tamamen engelle (CloudFront OAC kullanacağız)
aws s3api put-public-access-block 
  --bucket myapp-static-site 
  --public-access-block-configuration 
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Burada kritik nokta: Public access’i engelledik. Eskiden S3 static website hosting için bucket’ı public yapmanız gerekiyordu. Artık CloudFront Origin Access Control (OAC) kullanarak bucket’ı private tutabiliyoruz. Bu çok daha güvenli bir yaklaşım.

Şimdi statik site dosyalarını upload edelim. Gerçek projelerinizde bu adım CI/CD pipeline’ınızda otomatikleştirilmiş olur:

# Örnek statik site dosyaları oluştur
mkdir -p my-static-site
cat > my-static-site/index.html << 'EOF'
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <title>Merhaba CloudFront</title>
</head>
<body>
    <h1>CloudFront ile S3 Statik Site</h1>
    <p>Bu sayfa CloudFront üzerinden servis edilmektedir.</p>
</body>
</html>
EOF

cat > my-static-site/error.html << 'EOF'
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <title>Sayfa Bulunamadı</title>
</head>
<body>
    <h1>404 - Sayfa Bulunamadı</h1>
</body>
</html>
EOF

# Dosyaları S3'e yükle
aws s3 sync my-static-site/ s3://myapp-static-site/ 
  --delete 
  --cache-control "public, max-age=86400"

--cache-control parametresini dikkat edin. HTML dosyaları için daha kısa, statik asset’ler (CSS, JS, görsel) için daha uzun cache süresi ayarlamak performans açısından önemli. Daha granüler kontrol için:

# HTML dosyaları için kısa cache (içerik değişebilir)
aws s3 sync my-static-site/ s3://myapp-static-site/ 
  --exclude "*" 
  --include "*.html" 
  --cache-control "public, max-age=300, must-revalidate"

# CSS ve JS için uzun cache (hash'li dosya isimleri kullanıyorsanız)
aws s3 sync my-static-site/ s3://myapp-static-site/ 
  --exclude "*" 
  --include "*.css" 
  --include "*.js" 
  --cache-control "public, max-age=31536000, immutable"

# Görseller için orta vadeli cache
aws s3 sync my-static-site/ s3://myapp-static-site/ 
  --exclude "*" 
  --include "*.jpg" 
  --include "*.png" 
  --include "*.webp" 
  --cache-control "public, max-age=604800"

CloudFront Origin Access Control (OAC) Kurulumu

OAC, bucket’ınıza sadece CloudFront’un erişmesine izin veren mekanizmadır. Bu eski OAI (Origin Access Identity) sisteminin yerini aldı ve çok daha esnek.

# OAC oluştur
aws cloudfront create-origin-access-control 
  --origin-access-control-config '{
    "Name": "myapp-oac",
    "Description": "OAC for myapp static site",
    "SigningProtocol": "sigv4",
    "SigningBehavior": "always",
    "OriginAccessControlOriginType": "s3"
  }'

Bu komutun çıktısında Id değerini not alın, CloudFront distribution oluştururken kullanacaksınız.

CloudFront Distribution Oluşturma

Şimdi ana işe gelelim. CloudFront distribution’ı JSON konfigürasyon dosyası ile oluşturmak yönetimi kolaylaştırır:

# distribution-config.json dosyası oluştur
cat > distribution-config.json << 'EOF'
{
  "CallerReference": "myapp-static-site-2024",
  "Comment": "MyApp Static Site Distribution",
  "DefaultRootObject": "index.html",
  "Origins": {
    "Quantity": 1,
    "Items": [
      {
        "Id": "myapp-s3-origin",
        "DomainName": "myapp-static-site.s3.eu-west-1.amazonaws.com",
        "S3OriginConfig": {
          "OriginAccessIdentity": ""
        },
        "OriginAccessControlId": "YOUR_OAC_ID_HERE"
      }
    ]
  },
  "DefaultCacheBehavior": {
    "TargetOriginId": "myapp-s3-origin",
    "ViewerProtocolPolicy": "redirect-to-https",
    "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
    "Compress": true,
    "AllowedMethods": {
      "Quantity": 2,
      "Items": ["GET", "HEAD"]
    }
  },
  "CustomErrorResponses": {
    "Quantity": 2,
    "Items": [
      {
        "ErrorCode": 403,
        "ResponsePagePath": "/error.html",
        "ResponseCode": "404",
        "ErrorCachingMinTTL": 300
      },
      {
        "ErrorCode": 404,
        "ResponsePagePath": "/error.html",
        "ResponseCode": "404",
        "ErrorCachingMinTTL": 300
      }
    ]
  },
  "PriceClass": "PriceClass_100",
  "Enabled": true,
  "HttpVersion": "http2and3"
}
EOF

# Distribution'ı oluştur
aws cloudfront create-distribution 
  --distribution-config file://distribution-config.json

Birkaç önemli nokta:

  • ViewerProtocolPolicy: redirect-to-https: HTTP’den HTTPS’e otomatik yönlendirme
  • CachePolicyId: Kullandığım ID AWS’nin “CachingOptimized” managed policy’si
  • PriceClass_100: Sadece Kuzey Amerika ve Avrupa edge location’larını kullanır, maliyet düşer. Türkiye için bu yeterli çünkü Frankfurt ve Paris edge’leri var
  • HttpVersion: http2and3: HTTP/3 desteği performance açısından önemli
  • Compress: true: Otomatik gzip/brotli sıkıştırma

S3 Bucket Policy’yi Güncelleme

OAC oluşturulduktan sonra S3 bucket’ına CloudFront’un erişebilmesi için bucket policy güncellenmeli:

# Bucket policy'yi güncelle
# YOUR_DISTRIBUTION_ARN ve YOUR_ACCOUNT_ID değerlerini kendinize göre düzenleyin
cat > bucket-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipal",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::myapp-static-site/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
        }
      }
    }
  ]
}
EOF

aws s3api put-bucket-policy 
  --bucket myapp-static-site 
  --policy file://bucket-policy.json

Bu policy sayesinde bucket’a sadece belirttiğiniz CloudFront distribution erişebilir. Başka kimse, başka distribution da dahil olmak üzere.

Custom Domain ve SSL Sertifikası

Gerçek bir production senaryosunda CloudFront’un verdiği d1234abcd.cloudfront.net domain’ini değil, kendi domain’inizi kullanmak istersiniz. Bunun için ACM’de SSL sertifikası oluşturmanız gerekiyor. Önemli: CloudFront için ACM sertifikası mutlaka us-east-1 region’ında oluşturulmalı.

# us-east-1'de sertifika talep et
aws acm request-certificate 
  --domain-name "example.com" 
  --subject-alternative-names "www.example.com" 
  --validation-method DNS 
  --region us-east-1

Sertifika doğrulaması için Route53’te CNAME kaydı eklemeniz gerekiyor. ACM bu CNAME değerlerini size verir. Doğrulama tamamlandıktan sonra distribution’ı güncelleyin:

# Mevcut distribution config'i çek
aws cloudfront get-distribution-config 
  --id YOUR_DISTRIBUTION_ID 
  --output json > current-config.json

# ETag değerini not al, güncelleme için gerekli
# Sonra distribution'ı güncelle
aws cloudfront update-distribution 
  --id YOUR_DISTRIBUTION_ID 
  --if-match YOUR_ETAG 
  --distribution-config '{
    "Aliases": {
      "Quantity": 2,
      "Items": ["example.com", "www.example.com"]
    },
    "ViewerCertificate": {
      "ACMCertificateArn": "arn:aws:acm:us-east-1:YOUR_ACCOUNT:certificate/YOUR_CERT_ID",
      "SSLSupportMethod": "sni-only",
      "MinimumProtocolVersion": "TLSv1.2_2021"
    }
  }'

MinimumProtocolVersion: TLSv1.2_2021 ile eski ve güvensiz TLS versiyonlarını engelleyebilirsiniz. Modern web güvenliği açısından bu önemli.

Cache Invalidation ve Deployment

Statik sitenizi güncelledikten sonra CloudFront cache’ini temizlemeniz gerekebilir. Özellikle index.html gibi hash’siz dosyalar için bu kritik:

# Tüm cache'i temizle (dikkatli kullanın, maliyeti var)
aws cloudfront create-invalidation 
  --distribution-id YOUR_DISTRIBUTION_ID 
  --paths "/*"

# Sadece belirli dosyaları invalidate et (daha ekonomik)
aws cloudfront create-invalidation 
  --distribution-id YOUR_DISTRIBUTION_ID 
  --paths "/index.html" "/app.js" "/style.css"

# Invalidation durumunu kontrol et
aws cloudfront get-invalidation 
  --distribution-id YOUR_DISTRIBUTION_ID 
  --id YOUR_INVALIDATION_ID

İlk 1000 invalidation path’i aylık ücretsiz. Sonrası ücretli. Bu yüzden her deployment’ta /* yapmak yerine sadece değişen dosyaları invalidate etmek daha ekonomik. CI/CD pipeline’ınızda değişen dosyaların listesini tutup sadece onları invalidate etmek iyi bir pratik.

CloudFront Functions ile Edge Logic

Bir React SPA (Single Page Application) çalıştırıyorsanız, doğrudan /hakkimizda gibi bir URL’e girildiğinde S3’ten 403/404 alırsınız çünkü o path’te fiziksel bir dosya yok. Custom error responses kısmen çözüm sağlar ama daha temiz yaklaşım CloudFront Functions kullanmak:

# CloudFront Function oluştur
cat > spa-routing-function.js << 'EOF'
function handler(event) {
    var request = event.request;
    var uri = request.uri;
    
    // Dosya uzantısı varsa dokunma (CSS, JS, görseller vs.)
    if (uri.includes('.')) {
        return request;
    }
    
    // Trailing slash varsa index.html ekle
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
        return request;
    }
    
    // Diğer tüm path'leri index.html'e yönlendir (SPA routing)
    request.uri = '/index.html';
    return request;
}
EOF

# Function'ı AWS'e yükle
aws cloudfront create-function 
  --name spa-routing 
  --function-config '{"Comment": "SPA Routing", "Runtime": "cloudfront-js-2.0"}' 
  --function-code fileb://spa-routing-function.js

# Function'ı publish et
aws cloudfront publish-function 
  --name spa-routing 
  --if-match YOUR_FUNCTION_ETAG

Bu function’ı distribution’ınızdaki default cache behavior’a viewer-request event’ine bağladığınızda React Router, Vue Router gibi client-side routing sistemleri sorunsuz çalışır.

Monitoring ve Alerting

Production ortamında dağıtımınızı izlemek şart. CloudFront metrics’i CloudWatch’a gönderir:

# CloudFront için gerçek zamanlı metrikleri aktif et (ücretli özellik)
aws cloudfront create-monitoring-subscription 
  --distribution-id YOUR_DISTRIBUTION_ID 
  --monitoring-subscription '{
    "RealtimeMetricsSubscriptionConfig": {
      "RealtimeMetricsSubscriptionStatus": "Enabled"
    }
  }'

# 5xx error rate için alarm oluştur
aws cloudwatch put-metric-alarm 
  --alarm-name "CloudFront-5xx-Errors" 
  --alarm-description "CloudFront 5xx error rate yüksek" 
  --metric-name "5xxErrorRate" 
  --namespace "AWS/CloudFront" 
  --dimensions Name=DistributionId,Value=YOUR_DISTRIBUTION_ID 
  --period 300 
  --evaluation-periods 2 
  --threshold 5 
  --comparison-operator GreaterThanThreshold 
  --statistic Average 
  --alarm-actions "arn:aws:sns:YOUR_REGION:YOUR_ACCOUNT:your-alert-topic"

Cache hit ratio’sunu da izleyin. Düşük cache hit ratio, origin’e çok fazla istek gittiği anlamına gelir. Genellikle %80’in üzerinde olmasını hedefleyin.

Güvenlik Hardening

Security Headers

CloudFront Response Headers Policy ile güvenlik header’larını merkezi olarak yönetebilirsiniz:

aws cloudfront create-response-headers-policy 
  --response-headers-policy-config '{
    "Name": "myapp-security-headers",
    "Comment": "Security headers for myapp",
    "SecurityHeadersConfig": {
      "XSSProtection": {
        "Override": true,
        "Protection": true,
        "ModeBlock": true
      },
      "FrameOptions": {
        "Override": true,
        "FrameOption": "DENY"
      },
      "ReferrerPolicy": {
        "Override": true,
        "ReferrerPolicy": "strict-origin-when-cross-origin"
      },
      "ContentTypeOptions": {
        "Override": true
      },
      "StrictTransportSecurity": {
        "Override": true,
        "IncludeSubdomains": true,
        "Preload": true,
        "AccessControlMaxAgeSec": 31536000
      }
    }
  }'

WAF Entegrasyonu

Eğer botlardan, DDoS’tan ya da kötü niyetli trafikten korunmak istiyorsanız CloudFront’a AWS WAF ekleyebilirsiniz. Statik siteler için temel koruma yeterli olur:

# WAF Web ACL oluştur (us-east-1'de)
aws wafv2 create-web-acl 
  --name myapp-waf 
  --scope CLOUDFRONT 
  --region us-east-1 
  --default-action Allow={} 
  --rules '[
    {
      "Name": "RateLimitRule",
      "Priority": 1,
      "Statement": {
        "RateBasedStatement": {
          "Limit": 2000,
          "AggregateKeyType": "IP"
        }
      },
      "Action": {"Block": {}},
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "RateLimitRule"
      }
    }
  ]' 
  --visibility-config '{
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "myapp-waf"
  }'

IP başına 2000 istek/5 dakika limiti çoğu statik site için yeterli olur. Gerçek kullanıcılar bu limite takılmaz.

Gerçek Dünya Senaryoları ve Sorun Giderme

Senaryo 1: Deployment Sonrası Eski İçerik Görünüyor

En sık karşılaşılan sorun. CloudFront cache’i TTL süresi dolmadan yeni içeriği servis etmez. Çözüm:

  • Kritik dosyalar için invalidation çalıştırın
  • CSS ve JS dosyalarına hash ekleyin (webpack/vite bunu otomatik yapar), bu dosyalar her build’de farklı isme sahip olur ve invalidation gerekmez
  • Sadece index.html ve benzeri değişmeyen isimdeki dosyaları invalidate edin

Senaryo 2: SPA’da Sayfa Yenileme 403 Hatası

CloudFront, /hakkimizda path’ini S3’te arar ve bulamayınca 403 döner. İki çözüm yolu var:

  • Custom error responses ile 403/404’ü index.html‘e yönlendirin
  • Yukarıda anlattığım CloudFront Function ile edge’de routing yapın. İkinci yöntem daha temiz çünkü 200 status code döner.

Senaryo 3: CORS Hataları

Farklı bir domain’den API çağrıları yapıyorsanız S3’te CORS ayarlamanız gerekebilir:

aws s3api put-bucket-cors 
  --bucket myapp-static-site 
  --cors-configuration '{
    "CORSRules": [
      {
        "AllowedHeaders": ["*"],
        "AllowedMethods": ["GET", "HEAD"],
        "AllowedOrigins": ["https://example.com", "https://www.example.com"],
        "MaxAgeSeconds": 3600
      }
    ]
  }'

Senaryo 4: Maliyet Optimizasyonu

Aylık CloudFront faturanız beklenenden yüksekse kontrol edilecekler:

  • Cache hit ratio düşük mü? Cache policy’yi gözden geçirin
  • PriceClass doğru mu? Türkiye’de kullanıcılarınız varsa PriceClass_200 (Avrupa dahil) yeterli
  • Gereksiz invalidation var mı? Her deployment’ta /* yapmayın

CI/CD Pipeline Entegrasyonu

Gerçek projelerinizde deployment’ı otomatikleştirmek istersiniz. GitHub Actions ile basit bir örnek:

# .github/workflows/deploy.yml
name: Deploy to S3 and CloudFront

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build
        run: npm ci && npm run build
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-west-1
      
      - name: Deploy to S3
        run: |
          # HTML dosyaları kısa cache
          aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }} 
            --delete 
            --exclude "*" 
            --include "*.html" 
            --cache-control "public, max-age=300"
          
          # Asset'ler uzun cache
          aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }} 
            --exclude "*.html" 
            --cache-control "public, max-age=31536000, immutable"
      
      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation 
            --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} 
            --paths "/index.html" "/404.html"

Bu pipeline build aşamasında projeyi derler, S3’e farklı cache policy’lerle yükler ve sadece değişmesi gereken dosyaları invalidate eder. Hash’li asset dosyaları otomatik olarak yeni isimlere sahip olduğu için onları invalidate etmemiz gerekmez.

Sonuç

S3 ve CloudFront kombinasyonu, statik web sitesi barındırma konusunda gerçekten düşünülmüş bir çözüm. Doğru yapılandırıldığında ayda birkaç dolar ile milyonlarca isteği karşılayabilen, dünya genelinde hızlı erişim sağlayan bir altyapıya kavuşuyorsunuz.

Özetlemek gerekirse kritik noktalara dikkat edin:

  • Bucket’ı private tutun, sadece OAC ile CloudFront erişsin
  • Cache policy’lerinizi akıllıca yapılandırın, HTML’e kısa, hash’li asset’lere uzun TTL
  • Security header’larını ihmal etmeyin, Response Headers Policy ile kolayca ekleyebilirsiniz
  • SPA kullanıyorsanız CloudFront Function ile edge routing yapın
  • CI/CD pipeline’ınızda sadece gerekli dosyaları invalidate ederek maliyet optimizasyonu yapın
  • Cache hit ratio’su ve 5xx error rate‘i CloudWatch ile izleyin

Bu altyapıyı bir kez doğru kurduğunuzda, sunucu bakımıyla hiç uğraşmadan, otomatik ölçeklenen ve global erişime sahip bir web sitesine sahip olursunuz. Serverless altyapının güzelliği de tam olarak bu.

Bir yanıt yazın

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