Доставляйте закрытое содержимое S3 через CloudFront

AWSBeginner
Практиковаться сейчас

Введение

Команда хочет предоставлять клиентам страницу релиза через CloudFront, сохраняя источник S3 закрытым. Загрузите подготовленную страницу, соедините распределение с управлением доступом к источнику и разрешите только этому распределению читать объекты. Проверьте работающий клиентский путь и запрещённый прямой доступ к источнику.

Сначала завершите AWS Foundations, операции с объектами S3 и изучение ресурсных политик IAM. Эта независимая VM предоставляет настроенный доступ AWS CLI, index.html и отдельный эталонный бакет. Наблюдайте за распределением и источником в AWS View сверху, выполняйте работу в Terminal снизу. Личный аккаунт AWS и публичный домен не нужны. Здесь используется один обычный источник S3; кэширование и HTTPS изучаются отдельно.

Связь с сертификацией

Сертификация Задача экзамена Практика
Solutions Architect – Associate (SAA-C03) Задача 1.1 Ограничить чтение S3 выбранным распределением CloudFront с помощью ресурсной политики.

Обзор лабораторной работы

Схема: клиент обращается к CloudFront, которому разрешено читать закрытый источник S3; анонимный прямой запрос отклоняется.

Подготовьте содержимое закрытого источника

На этом шаге создайте закрытый бакет S3 и загрузите подготовленную страницу.

Источник (origin) хранит содержимое, которое получает CloudFront. Viewer — клиент, запрашивающий содержимое у CloudFront. Доступ клиента и доступ к источнику — разные разрешения: страницу можно читать через распределение, сохраняя запрет анонимного прямого чтения S3.

Работайте в предоставленном каталоге проекта. Сохраните эталонный бакет labex-n02-reference без изменений:

cd /home/labex/project
cat index.html
aws s3api create-bucket \
  --bucket labex-n02-content

Установите владение объектами Bucket owner enforced. Владельцем остаётся владелец бакета, разрешения через ACL отключаются; OAC использует политику бакета. Включите все четыре защиты Block Public Access, предотвращающие публичные разрешения через ACL или политики:

aws s3api put-bucket-ownership-controls \
  --bucket labex-n02-content \
  --ownership-controls '{"Rules":[{"ObjectOwnership":"BucketOwnerEnforced"}]}'
aws s3api put-public-access-block \
  --bucket labex-n02-content \
  --public-access-block-configuration '{"BlockPublicAcls":true,"IgnorePublicAcls":true,"BlockPublicPolicy":true,"RestrictPublicBuckets":true}'

Загрузите предоставленную страницу. --content-type text/html описывает объект как HTML-документ:

aws s3api put-object \
  --bucket labex-n02-content \
  --key index.html \
  --body index.html \
  --content-type text/html
aws s3api head-object \
  --bucket labex-n02-content \
  --key index.html

Посмотрите размер объекта и ContentType. Запрос CLI аутентифицирован как настроенный оператор. Сравните его с анонимным HTTP-запросом без учётных данных AWS:

curl --noproxy '*' \
  --output /dev/null \
  --write-out 'Direct origin: HTTP %{http_code}\n' \
  http://127.0.0.1:5000/labex-n02-content/index.html

Ожидайте Direct origin: HTTP 403. --output /dev/null отбрасывает тело ошибки, --write-out выводит статус HTTP. Этот явно заданный учебный endpoint проверяет прямой доступ к объекту S3, не делая бакет публичным. Выполните проверку закрытого источника.

Пример после загрузки: объект источника размером 91 байт показан рядом с неизменённым эталоном; распределение ещё не создан.

Соедините распределение с источником

На этом шаге соедините распределение CloudFront с бакетом S3 и убедитесь, что соединение само по себе не предоставляет разрешение на источник.

Управление доступом к источнику (OAC) определяет, как CloudFront аутентифицирует запросы к источнику. Выберите S3, Signature Version 4 и подпись always. Запишите обычную конфигурацию CLI:

cat > oac.json <<'JSON'
{
  "Name": "labex-n02-oac",
  "Description": "Read the private release origin",
  "SigningProtocol": "sigv4",
  "SigningBehavior": "always",
  "OriginAccessControlOriginType": "s3"
}
JSON
OAC_ID=$(aws cloudfront create-origin-access-control \
  --origin-access-control-config file://oac.json \
  --query OriginAccessControl.Id \
  --output text)

$(...) сохраняет возвращённый ID OAC в OAC_ID. --query выбирает только ID для ссылки в следующей конфигурации. Сохраняйте этот Terminal открытым в течение лабораторной работы.

Конфигурация соединяет content-origin с обычным endpoint бакета S3, а не endpoint сайта S3. TargetOriginId выбирает источник; DefaultRootObject сопоставляет / с index.html. Устаревший блок ForwardedValues исключает пересылку cookie и строк запроса. Все TTL равны нулю, чтобы проверка разрешений не использовала успешный ответ из кэша. allow-all разрешает используемый здесь клиентский HTTP-запрос; HTTPS изучается отдельно.

Следующий here-document указан без кавычек, поэтому оболочка подставляет $OAC_ID в JSON-файл:

cat > distribution.json <<JSON
{
  "CallerReference": "labex-n02-release",
  "Comment": "labex-n02:private-content",
  "Enabled": true,
  "DefaultRootObject": "index.html",
  "Origins": {
    "Quantity": 1,
    "Items": [{
      "Id": "content-origin",
      "DomainName": "labex-n02-content.s3.amazonaws.com",
      "S3OriginConfig": {"OriginAccessIdentity": ""},
      "OriginAccessControlId": "$OAC_ID"
    }]
  },
  "DefaultCacheBehavior": {
    "TargetOriginId": "content-origin",
    "ViewerProtocolPolicy": "allow-all",
    "TrustedSigners": {"Enabled": false, "Quantity": 0},
    "ForwardedValues": {"QueryString": false, "Cookies": {"Forward": "none"}},
    "MinTTL": 0,
    "DefaultTTL": 0,
    "MaxTTL": 0
  }
}
JSON
DIST_ID=$(aws cloudfront create-distribution \
  --distribution-config file://distribution.json \
  --query Distribution.Id \
  --output text)
DIST_DOMAIN=$(aws cloudfront get-distribution \
  --id "$DIST_ID" \
  --query Distribution.DomainName \
  --output text)

Посмотрите подключённый источник:

aws cloudfront get-distribution-config \
  --id "$DIST_ID" \
  --query DistributionConfig.Origins

Домен S3 и OriginAccessControlId должны соответствовать вашему бакету и OAC. Перед тестом дождитесь развёртывания распределения в плоскости управления. Официальный waiter повторно читает статус и не создаёт клиентский трафик:

aws cloudfront wait distribution-deployed \
  --id "$DIST_ID"

Проверьте клиентский путь. --resolve направляет это точное имя распределения и учебный порт к подготовленному endpoint доставки в VM; он не меняет системный DNS и не регистрирует домен:

curl --noproxy '*' \
  --resolve "${DIST_DOMAIN}:8082:127.0.0.1" \
  --output /dev/null \
  --write-out 'Viewer before permission: HTTP %{http_code}\n' \
  "http://${DIST_DOMAIN}:8082/index.html"

Ожидайте HTTP 403. Создание распределения и выбор OAC описывают соединение; S3 всё ещё нужна политика, разрешающая этому распределению доступ. Не делайте бакет публичным для исправления результата. Выполните проверку соединения.

Пример до разрешения источника: подключённое распределение возвращает HTTP 403 на реальный клиентский запрос.

Разрешите доступ выбранному распределению

На этом шаге разрешите выбранному распределению CloudFront читать объекты, сохранив запрет анонимного прямого доступа к источнику.

ARN идентифицирует ресурс AWS и его аккаунт. Получите ARN распределения:

DIST_ARN=$(aws cloudfront get-distribution \
  --id "$DIST_ID" \
  --query Distribution.ARN \
  --output text)

Политика бакета указывает cloudfront.amazonaws.com как сервисного субъекта (principal), разрешает только s3:GetObject и ограничивает разрешение объектами вашего бакета. Условие AWS:SourceArn ограничивает запрос сервиса конкретным распределением. Это отличается от публичной политики с Principal: "*". Запишите политику с полученным ARN:

cat > bucket-policy.json <<JSON
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "cloudfront.amazonaws.com"},
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::labex-n02-content/*",
    "Condition": {"StringEquals": {"AWS:SourceArn": "$DIST_ARN"}}
  }]
}
JSON
aws s3api put-bucket-policy \
  --bucket labex-n02-content \
  --policy file://bucket-policy.json

Снова запросите объект. Теперь --fail завершает команду ошибкой при неуспешном HTTP-статусе, а --include показывает заголовки вместе с реальным документом:

curl --fail --include --noproxy '*' \
  --resolve "${DIST_DOMAIN}:8082:127.0.0.1" \
  "http://${DIST_DOMAIN}:8082/index.html"

Ожидайте HTTP 200, тип содержимого text/html и страницу с Release one. Вы получили реальные байты объекта через распределение, а не только успешный ответ конфигурации.

Сразу повторите анонимную прямую проверку источника:

curl --noproxy '*' \
  --output /dev/null \
  --write-out 'Direct origin after viewer success: HTTP %{http_code}\n' \
  http://127.0.0.1:5000/labex-n02-content/index.html

Она должна по-прежнему возвращать HTTP 403. Клиентский путь работает, а прямой источник остаётся закрытым. AWS View показывает подключённый источник и последний клиентский результат. Выполните проверку доступа.

Пример после разрешения: реальный клиентский запрос возвращает HTTP 200 через подключённое распределение.

Удалите только свои ресурсы доставки

На этом шаге отключите и удалите распределение перед удалением OAC и содержимого S3. Сохраните эталонный бакет без изменений.

CloudFront использует ETag как токен версии для изменения конфигурации. Получите текущую конфигурацию и её ETag, не угадывая токен:

aws cloudfront get-distribution-config \
  --id "$DIST_ID" \
  --query DistributionConfig \
  --output json > distribution-current.json
DIST_ETAG=$(aws cloudfront get-distribution-config \
  --id "$DIST_ID" \
  --query ETag \
  --output text)

В этой конфигурации Enabled распределения — единственное одноимённое свойство со значением true. Следующая стандартная замена sed создаёт отключённую копию, сохраняя настройки источника:

sed 's/"Enabled": true/"Enabled": false/' distribution-current.json > distribution-disabled.json
aws cloudfront update-distribution \
  --id "$DIST_ID" \
  --if-match "$DIST_ETAG" \
  --distribution-config file://distribution-disabled.json

Дождитесь развёртывания отключённой конфигурации. В AWS распространение изменений может занять время; упражнение не измеряет глобальную задержку развёртывания:

aws cloudfront wait distribution-deployed \
  --id "$DIST_ID"

Обновление меняет ETag. Перед удалением отключённого распределения прочитайте новейший токен:

DIST_ETAG=$(aws cloudfront get-distribution-config \
  --id "$DIST_ID" \
  --query ETag \
  --output text)
aws cloudfront delete-distribution \
  --id "$DIST_ID" \
  --if-match "$DIST_ETAG"

Удалите OAC с его собственным ETag. ID распределения и ID OAC относятся к разным ресурсам:

OAC_ETAG=$(aws cloudfront get-origin-access-control \
  --id "$OAC_ID" \
  --query ETag \
  --output text)
aws cloudfront delete-origin-access-control \
  --id "$OAC_ID" \
  --if-match "$OAC_ETAG"

Удалите только созданные вами объект и бакет:

aws s3api delete-object \
  --bucket labex-n02-content \
  --key index.html
aws s3api delete-bucket \
  --bucket labex-n02-content
aws s3api list-buckets \
  --query Buckets[].Name

Эталонный бакет должен остаться, а бакет содержимого — отсутствовать. AWS View должен показывать отсутствие распределения содержимого и только эталонный объект. Выполните проверку очистки. Неуспешный API-запрос не доказывает удаление ресурса.

Пример после очистки: распределение и бакет содержимого удалены, независимый эталонный объект сохранён.

Резюме

Вы соединили распределение с обычным закрытым источником S3, настроили всегда подписываемые запросы OAC и разрешили чтение только выбранному распределению. Реальные HTTP-тесты различили разрешённый клиентский доступ и запрещённый анонимный доступ к источнику. Затем вы использовали актуальные ETag для отключения и удаления своих ресурсов, сохранив независимое содержимое. Далее наблюдайте повторное использование кэша и инвалидируйте обновлённый объект.