使用 CloudFront 分发私有 S3 内容

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 请求使用已配置操作身份进行认证。与不提供 AWS 凭据的匿名 HTTP 请求比较:

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 状态。这个明确指定的练习端点测试直接 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)

$(...) 将返回的 OAC ID 保存到 OAC_ID。--query 只选择 ID,供下一份配置引用。整个实验请保持此 Terminal 打开。

分配配置将 content-origin 连接到普通 S3 桶端点,而不是 S3 网站端点。TargetOriginId 选择源站,DefaultRootObject 将 / 映射为 index.html。旧式 ForwardedValues 配置不转发 Cookie 或查询字符串。此处全部 TTL 为零,防止源站权限测试复用已缓存的成功结果。allow-all 允许本实验的 HTTP 用户端请求;HTTPS 另行学习。

下一个 here-document 未加引号,因此 Shell 会把 $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 将此精确分配名称和练习端口连接到 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 和 OAC ID 指向不同资源:

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 禁用并删除所属资源,保留无关内容。接下来观察缓存复用,并使更新对象的缓存失效。