CloudFront でプライベートな S3 コンテンツを配信する

AWSBeginner
オンラインで実践に進む

はじめに

チームは S3 オリジンをプライベートに保ちながら、CloudFront 経由で公開用ページを顧客に読んでもらいたいと考えています。用意されたページをアップロードし、ディストリビューションとオリジンアクセスコントロールを接続して、そのディストリビューションだけにオブジェクトの読み取りを許可します。利用可能なビューワー経路と、拒否されるオリジンへの直接経路を試します。

先に 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 バケットを作成し、用意されたページをアップロードします。

オリジンは CloudFront が取得するコンテンツを保存します。ビューワーは 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 は別途学びます。

次のヒアドキュメントは引用符なしなので、シェルが $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 をサービスのプリンシパルとして指定し、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 で OAC を削除します。ディストリビューション 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 で所有リソースを無効化して削除し、独立したコンテンツを保持しました。次はキャッシュの再利用を観察し、更新したオブジェクトを無効化します。