MidПрактика2 min

S3 и CloudFront

Детальная настройка S3 static hosting с CloudFront CDN, bucket policies и OAC

Полная настройка S3 для CRC

Создание и конфигурация бакета

# Создаём бакет
aws s3 mb s3://ivan-petrov-resume --region us-east-1

# Включаем статический хостинг
aws s3 website s3://ivan-petrov-resume \
  --index-document index.html \
  --error-document error.html

# Блокируем публичный доступ (CloudFront будет единственным путём)
aws s3api put-public-access-block \
  --bucket ivan-petrov-resume \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Загрузка файлов с правильными Content-Type

# Загрузка с установкой Content-Type и Cache-Control
aws s3 sync ./frontend/ s3://ivan-petrov-resume/ \
  --delete \
  --exclude "*" \
  --include "*.html" \
  --content-type "text/html; charset=utf-8" \
  --cache-control "max-age=3600"

aws s3 sync ./frontend/ s3://ivan-petrov-resume/ \
  --exclude "*" \
  --include "*.css" \
  --content-type "text/css" \
  --cache-control "max-age=86400"

aws s3 sync ./frontend/ s3://ivan-petrov-resume/ \
  --exclude "*" \
  --include "*.js" \
  --content-type "application/javascript" \
  --cache-control "max-age=86400"

CloudFront Distribution

Полная конфигурация через CLI

# Шаг 1: Создаём OAC
OAC_ID=$(aws cloudfront create-origin-access-control \
  --origin-access-control-config '{
    "Name": "crc-s3-oac",
    "SigningProtocol": "sigv4",
    "SigningBehavior": "always",
    "OriginAccessControlOriginType": "s3"
  }' --query 'OriginAccessControl.Id' --output text)

# Шаг 2: Запрашиваем SSL-сертификат (ОБЯЗАТЕЛЬНО us-east-1)
CERT_ARN=$(aws acm request-certificate \
  --domain-name resume.ivanpetrov.com \
  --validation-method DNS \
  --region us-east-1 \
  --query 'CertificateArn' --output text)

echo "Добавьте CNAME-запись для валидации сертификата"
aws acm describe-certificate \
  --certificate-arn $CERT_ARN \
  --region us-east-1 \
  --query 'Certificate.DomainValidationOptions[0].ResourceRecord'

# Шаг 3: Создаём CloudFront distribution
aws cloudfront create-distribution --distribution-config "{
  \"CallerReference\": \"crc-$(date +%s)\",
  \"Comment\": \"CRC Frontend\",
  \"DefaultRootObject\": \"index.html\",
  \"Enabled\": true,
  \"HttpVersion\": \"http2and3\",
  \"PriceClass\": \"PriceClass_100\",
  \"Origins\": {
    \"Quantity\": 1,
    \"Items\": [{
      \"Id\": \"S3-crc\",
      \"DomainName\": \"ivan-petrov-resume.s3.us-east-1.amazonaws.com\",
      \"S3OriginConfig\": {\"OriginAccessIdentity\": \"\"},
      \"OriginAccessControlId\": \"$OAC_ID\"
    }]
  },
  \"DefaultCacheBehavior\": {
    \"TargetOriginId\": \"S3-crc\",
    \"ViewerProtocolPolicy\": \"redirect-to-https\",
    \"Compress\": true,
    \"AllowedMethods\": {\"Quantity\": 2, \"Items\": [\"GET\", \"HEAD\"]},
    \"CachedMethods\": {\"Quantity\": 2, \"Items\": [\"GET\", \"HEAD\"]},
    \"CachePolicyId\": \"658327ea-f89d-4fab-a63d-7e88639e58f6\",
    \"ResponseHeadersPolicyId\": \"\"
  },
  \"ViewerCertificate\": {
    \"ACMCertificateArn\": \"$CERT_ARN\",
    \"SSLSupportMethod\": \"sni-only\",
    \"MinimumProtocolVersion\": \"TLSv1.2_2021\"
  },
  \"Aliases\": {\"Quantity\": 1, \"Items\": [\"resume.ivanpetrov.com\"]},
  \"CustomErrorResponses\": {
    \"Quantity\": 1,
    \"Items\": [{
      \"ErrorCode\": 403,
      \"ResponsePagePath\": \"/index.html\",
      \"ResponseCode\": \"200\",
      \"ErrorCachingMinTTL\": 300
    }]
  }
}"

Bucket Policy для OAC

# Даём CloudFront доступ к S3
DIST_ARN="arn:aws:cloudfront::123456789012:distribution/EDXXXXXXX"

aws s3api put-bucket-policy --bucket ivan-petrov-resume --policy "{
  \"Version\": \"2012-10-17\",
  \"Statement\": [{
    \"Sid\": \"AllowCloudFrontOAC\",
    \"Effect\": \"Allow\",
    \"Principal\": {\"Service\": \"cloudfront.amazonaws.com\"},
    \"Action\": \"s3:GetObject\",
    \"Resource\": \"arn:aws:s3:::ivan-petrov-resume/*\",
    \"Condition\": {
      \"StringEquals\": {
        \"AWS:SourceArn\": \"$DIST_ARN\"
      }
    }
  }]
}"

Route53 DNS

# Создаём A-запись (Alias) на CloudFront
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "resume.ivanpetrov.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d1234567890.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    }]
  }'

Типичные проблемы

Проблема: 403 при обращении к /path (SPA routing)

Решение: Custom Error Response в CloudFront: 403 → /index.html с кодом 200.

Проблема: Сертификат не валидируется

Решение: Убедитесь, что CNAME-запись для валидации добавлена в DNS. Проверьте: aws acm describe-certificate --certificate-arn $CERT_ARN --region us-east-1.

Проблема: Старый контент после деплоя

Решение: Инвалидация CloudFront + проверьте Cache-Control заголовки.

aws cloudfront create-invalidation \
  --distribution-id EDXXXXXXX \
  --paths "/*"

Проверь себя

Что означает PriceClass_100 в CloudFront?

Зачем в CloudFront настраивается Custom Error Response 403 → /index.html?

Какой HTTP-версию рекомендуется включить в CloudFront для максимальной производительности?

Почему для Cache-Control HTML файлов установлен max-age=3600, а не 86400?

Почему для CloudFront рекомендуется OAC вместо публичного S3?