ちょっと話題の記事

できた!S3 オリジンへの直接アクセス制限と、インデックスドキュメント機能を共存させる方法

CloudFrontをS3 オリジンで利用するとき、「CloudFrontをバイパスしたアクセスを制限」「インデックスドキュメントを返す」という要望は少なくないのではないでしょうか?出来そうで出来なかった共存を Lambda@Edge で解決!
2018.04.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ちょっと伝わりにくいタイトルですが、やりたいことは以下の2つです。

  • CloudFront の S3 オリジンには直接アクセスさせない(CloudFront をバイパスした S3 へのアクセスをブロック)
  • オブジェクト指定のないアクセス(末尾"/"の URL アクセス)にはインデックスドキュメントを返す(サブディレクトリも含む)

画にするとこういうことです↓

「CloudFront をバイパスさせない」という点でまず考えるのは、オリジンアクセスアイデンティティでかと思います。そして、「オブジェクト指定のないアクセスにインデックスドキュメントを返す」という点で思いつくのは、Default Root Object ですね。最初に考えたときは、この2つの機能で実現できるだろうと思ってたのですが、そうは問屋が卸してくれなかったです。。

なぜなら、CloudFront の Default Root Object 設定は、ディストリビューションのルート URL のみが対象であり、サブディレクトリには効きません。つまり、以下のような動作になるということです。

  • 例:Default Root Object に index.html を定義した場合
  • http://exsample.com/ → index.html を返す
  • http://exsample.com/subdir/ → index.html は返りません

Default Root Object では対応できないため、次に考えるのは S3 静的ホスティングのインデックスドキュメント設定ですね。これならばサブディレクトリに対しても、インデックスドキュメントを返すことができるのですが、その場合、S3 オリジンではなく、カスタムオリジンとして S3 のウェブサイトエンドポイントに変更する必要があるため、オリジンアクセスアイデンティティを使用した CloudFront のバイパス制限を実装することが出来なくなります。

ではどうしましょう、、。S3 バケットポリシーで、CloudFront に割り当てられる可能性のある IP アドレス範囲を片っ端からアクセス許可すれば可能かもしれませんが、どれだけの IP アドレスを定義する必要があるでしょうか。また、それらの IP アドレス範囲の不定期な変更をチェックし、変更があった場合は S3 バケットポリシーに反映するような仕組みが必要になるでしょう。現実的にはあまり採用したくない方法ですね。

そこでご紹介するのが、Lambda@Edgeです。

Lambda@Edge の出番ですよ!

Lambda@Edge は 2016年の Re:Invent で発表された、「CloudFront のエッジロケーションで Lambda を実行する」サービスになります。

【速報】AWS Lambda@Edgeが発表されました! #reinvent

CloudFront ディストリビューションに Lambda 関数に関連付けると、CloudFront エッジロケーションでリクエストとレスポンスが傍受することができ、次の CloudFront イベントの発生時に Lambda 関数を実行できます。

  • CloudFront がビューワーからリクエストを受信したとき (ビューワーリクエスト)
  • CloudFront がリクエストをオリジンに転送する前 (オリジンリクエスト)
  • CloudFront がオリジンからレスポンスを受信したとき (オリジンレスポンス)
  • CloudFront がビューワーにレスポンスを返す前 (ビューワーレスポンス)

つまり、どゆこと?

今回のケースならば、オリジンリクエスト をトリガーに Lambda@Edge 関数を実行させて、オブジェクト指定のない URL (末尾が "/" で終わっている)アクセスを、index.html を付与した URL パスに書き換えて S3 オリジンにリクエストするようにしてしままえば良い、ということです。 画にするとこんな感じです↓

この構成であれば、そもそも Default Root Object の指定は必要ありません。そして、S3 静的ウェブサイトホスティングを有効にする必要もないため、オリジンアクセスアイデンティティ が利用できますので、CloudFront をバイパスしたアクセスを制限することも可能です!

早速やってみる!

S3 バケットとオブジェクトの準備

S3 バケットの直下と、subdir/index.html を準備しておきます。なお、静的ウェブサイトホスティングは設定しません。

  • s3://cm-rootobject-test/index.html → root index! と表示。
  • s3://cm-rootobject-test/subdir/index.html → subdir index! と表示。

CloudFront を作成

CloudFront ディストリビューションを作成します。 CloudFront コンソールを開き、Create Distribution を選択します。Select a delivery method for your content では WebGet Started を選択します。

  • Origin Domain Name: 先ほど作成した S3 バケットを選択
  • Restrict Bucket Access: Yes を選択
  • Origin Access Identity: Create a new identity を選択
  • Grant Read Permissions on Bucket: Yes, Update Bucket Policy を選択

  • Object Caching: Customize を選択。
  • Minimum TTL: 0
  • Maximum TTL: 0
  • Default TTL: 0

今回はテストを容易にするために、キャッシュさせない設定としましたが、実際に利用されるならば、適切にキャッシュを設定したほうが良いでしょう。(じゃないと、毎回 Lambda@Edge 関数が起動しますので、、)その他は、デフォルトを受け入れてます。もちろん Default Root Object の定義もしておりません。

Lambda@Edge がないときぃ〜

まずは、Lambda@Edge 関数を作成する前の状態を確認してみましょう。先ほど作成した CloudFront の Domain Name に対して //subdir/ でアクセスしてみます。

$ curl -I http://dz00r977zpll6.cloudfront.net/
HTTP/1.1 403 Forbidden
Content-Type: application/xml
Connection: keep-alive
x-amz-bucket-region: ap-northeast-1
Date: Tue, 24 Apr 2018 14:43:47 GMT
Server: AmazonS3
X-Cache: Error from cloudfront
Via: 1.1 39dd8394fd9e6a08c6913ca5845d75f1.cloudfront.net (CloudFront)
X-Amz-Cf-Id: 0_r8ooshXGuZ80pSWmlJXucPoL6Pz_Nrm6PpOKZP6vLsesyzgZ1ZiQ==

$ curl -I http://dz00r977zpll6.cloudfront.net/subdir/
HTTP/1.1 403 Forbidden
Content-Type: application/xml
Connection: keep-alive
Date: Tue, 24 Apr 2018 14:44:04 GMT
Server: AmazonS3
X-Cache: Error from cloudfront
Via: 1.1 39dd8394fd9e6a08c6913ca5845d75f1.cloudfront.net (CloudFront)
X-Amz-Cf-Id: ZnXZ0P3hX4QIISWN75M6ro26_iBfjZsVu_Ca-h5_bFLSPylKvG4VXg==

今回は Default Root Object も、静的ウェイブサイトホスティングも使ってないので index.html は返ってこずに、403 Forbiddenになっていることが確認できました。

Lambda@Edge 用のロール作成

Lambda@Edge 関数に割り当てるロールを作成しておきます。このあたりの設定は、マニュアルに記載されているので、そのまま書いておきます。通常の Lambda 関数と違って、CloudFront ディストリビューションへの関連付け用のアクセス許可が必要となります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}

また、サービスプリンシパル lambda.amazonaws.comedgelambda.amazonaws.com が AssumeRole できる必要があるので、IAM ロール設定の[信頼関係]タブを開いて、[信頼関係の編集]を開き、以下のように設定します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
            "lambda.amazonaws.com",
            "edgelambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Lambda@Edge 関数を作成

それでは Lambda コンソールを開いて作成しましょう。

まずリージョンの指定ですが、2018年4月時点で Lambda@Edge 関数 の作成は、バージニアリージョンでしか出来ないようですので、us-east-1(米国東部:バージニア北部)を選択してください。ランタイムについても同時点での対応は Node.js 6.10 のみになります。次に、事前に作成しておいた Lambda@Edge 用の IAM ロールを指定します(このタイミングで作成しても問題ありません)。指定できましたら[関数の作成]をクリックします。

関数が作成できましたら、以下のコードをコピーして貼り付けましょう。なお、コードの編集はバージョンが $LATEST でなければ出来ませんので、編集できない場合はバージョンを確認してください。

index.handller

'use strict';
exports.handler = (event, context, callback) => {
    
    // Extract the request from the CloudFront event that is sent to Lambda@Edge 
    var request = event.Records[0].cf.request;

    // Extract the URI from the request
    var olduri = request.uri;

    // Match any '/' that occurs at the end of a URI. Replace it with a default index
    var newuri = olduri.replace(/\/$/, '\/index.html');
    
    // Log the URI as received by CloudFront and the new URI to be used to fetch from origin
    console.log("Old URI: " + olduri);
    console.log("New URI: " + newuri);
    
    // Replace the received URI with the URI that includes the index page
    request.uri = newuri;
    
    // Return to CloudFront
    return callback(null, request);

};

その他はデフォルトを受け入れて[保存]します。

トリガーの設定

次に関数のトリガーを設定します。今度はバージョンが $LATEST だと設定が出来ませんので、[アクション]のプルダウンメニューから[新しいバージョンを発行]を選択します。

バージョンの説明は任意で入力し[発行]をクリックします。

バージョンが変わったことを確認し、左ペインから[CloudFront]を選択します。

画面を下にスクロールし、[トリガーの設定]を行います。ディストリビューションでは事前に作成した CloudFront ディストリビューションを指定し、[CloudFront イベント]は、オリジンリクエストを選択します。さいごに、[トリガーとレプリケート]にチェックをいれて[追加]をクリックしましょう。

CloudFront ディストリビューションの Status を確認すると、In Progress になり、グローバルの ClouFront へのデプロイが完了するまで少し待ちます。Deployed に変われば完了です。

アクセスしてみる。Lambda@Edge があるときぃ〜

それでは先ほどと同じ方法でアクセスしてみましょう!

$ curl -I http://dz00r977zpll6.cloudfront.net/
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 46
Connection: keep-alive
Date: Wed, 25 Apr 2018 15:05:53 GMT
Last-Modified: Wed, 18 Apr 2018 14:42:50 GMT
ETag: "cef53ea838dd5ef398d854e599152324"
Accept-Ranges: bytes
Server: AmazonS3
X-Cache: Miss from cloudfront
Via: 1.1 45fd5b8af901a90c70a650138c35291c.cloudfront.net (CloudFront)
X-Amz-Cf-Id: gLoeNcXrk9n5BxTaWLMPR1ysaB696A8-R3lDvitFN_-A3o1boJu8lA==

$ curl -I http://dz00r977zpll6.cloudfront.net/subdir/
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 48
Connection: keep-alive
Date: Wed, 25 Apr 2018 15:06:20 GMT
Last-Modified: Wed, 18 Apr 2018 14:43:41 GMT
ETag: "ea456df8da39d2bb8f075c99e36ca90c"
Accept-Ranges: bytes
Server: AmazonS3
X-Cache: Miss from cloudfront
Via: 1.1 821834a43f39b878528a4af98ff4016c.cloudfront.net (CloudFront)
X-Amz-Cf-Id: auBn-T3pAXrUbGZrUptStW48582a3Q_DuaVvEoVXcZWRmiGytJLMbw==

HTTP ステータスは 200 が返ってくるようになりましたね。きちんとサブディレクトリの index.html なのか、内容も確認してみましょう。

$ curl -v http://dz00r977zpll6.cloudfront.net/subdir/
*   Trying 54.230.124.199...
* TCP_NODELAY set
* Connected to dz00r977zpll6.cloudfront.net (54.230.124.199) port 80 (#0)
> GET /subdir/ HTTP/1.1
> Host: dz00r977zpll6.cloudfront.net
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html
< Content-Length: 48
< Connection: keep-alive
< Date: Wed, 25 Apr 2018 15:08:42 GMT
< Last-Modified: Wed, 18 Apr 2018 14:43:41 GMT
< ETag: "ea456df8da39d2bb8f075c99e36ca90c"
< Accept-Ranges: bytes
< Server: AmazonS3
< X-Cache: Miss from cloudfront
< Via: 1.1 d5145f1eca1e6acd0298623b7a7eeb34.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: _Q93RMRRl5q0VD7LkpsOf9kcMMHLS-JNCiyVnL_hKj1_mG7mY-DmQA==
<
* Connection #0 to host dz00r977zpll6.cloudfront.net left intact
<html><body><h1>subdir index!</h1></body></html>

ちゃんと subdir index! が表示されているので、サブディレクトリの index.html が返ってますね!

$ curl -I http://dev.classmethod.jp/wp-content/uploads/2019/01/httpstls.oguri_.classmethod.infoindex.html-2019-01-25-06-37-20.png
HTTP/1.1 404 Not Found
x-amz-error-code: NoSuchWebsiteConfiguration
x-amz-error-message: The specified bucket does not have a website configuration
x-amz-error-detail-BucketName: cm-rootobject-test
x-amz-request-id: 54249ADB18C18A2F
x-amz-id-2: tYFAEMWMca8oRgTv1AfaVbzRdF0qrN3RRSPPJS1TN7vAWtLbnTiyCLOcnDfZ2Yd3L7x+FTFBU58=
Transfer-Encoding: chunked
Date: Thu, 26 Apr 2018 15:42:10 GMT
Server: AmazonS3

もちろん、CloudFront をバイパスしたアクセスは出来ません。これで期待したとおりに動いてることが確認できましたね!

まとめ

Lambda@Edge 関数を使い、URL パスを書き換えることでインデックスドキュメントを返す方法をご紹介しました。静的ウェブサイトホスティングではなく、S3 オリジンのまま利用することで、CloudFront をバイパスしたアクセス制限はオリジンアクセスアイデンティティにまるっとおまかせできるので、比較的、簡単な方法ではないかと思います! また、今回 Lambda@Edge 関数を初めて使いましたが、通常の Lambda 関数とは違って少しクセがあるようですね。そのあたりは、以下、大阪オフィス 西村の記事をあわせて読んでいただければ良いかと思います!

以上!大阪オフィスの丸毛(@marumo1981)でした!

Amazon CloudFrontとAWS Lambda@EdgeでSPAのBasic認証をやってみる

AWS Lambda@Edgeのログはどこ?AWS Lambda@Edgeのログ出力先について

参考