AWS と比べるとマイナーな Google Cloud Platform (GCP) を使って静的な Web コンテンツを無料で公開するというお話です。
Cloud Storage (AWS の S3 の位置づけのサービス) にファイルを置くだけで簡単に Web で公開できます。設定だけちゃんとやっておけば、ファイルを置くだけで Cloud Storage 自体が Web サーバの役割をはたしてくれます。更に、無料枠で公開しておけばちょっとしたサイトでありましたら、無料で公開できます。
また、特に何もしなくとも(追加料金無しで) CDN が利用可能となっているのがよいですね。(逆にこれがあるのでコンテンツはキャッシュされますので注意が必要です。これについては後半部分のCache-Control でふれます。)
一般公開で閲覧可能なオブジェクトはデフォルトで Cloud Storage ネットワークにキャッシュされるので、特に設定しなくても、Cloud Storage は基本的に Content Delivery Network(CDN)のように機能します。
ここでは、以下の4つの作業(+ 1)を行い、storage-test.designrecipe.jp
の FQDN のサイトを Google Cloud Platform の Stroage にアップロードした静的コンテンツを使って公開するということを行います。
- ドメインの転送
- バケットの作成
- コンテンツのアップ
- インデックスページとエラーページを設定する
- 【Appendix】Cache-Control (キャッシュコントール)
基本的な作業内容は以下をベースにしています。
作業を行うにあたっての前提
上記の作業を行うにあたって、前提として以下の作業が既に完了していることとします。
- GCP のアカウントは設定されおり、利用するプロジェクトも用意されている。
gcloud
、gsutil
といったコマンドが使える状態となっている。(Web のコンソールを使っても当然同じことができますが、ここではコマンドを使います。)- 何かしらのドメインサービスを利用しており、DNS サーバはドメインを管理しているサービスのものを既に使っている
1. ドメインの転送
利用したいドメインを c.storage.googleapis.com
に CNAME しておく必要があります。
CNAME する要件は GCP のものですが、CNAME の設定自体は一般的な DNS の設定となります。
ここでは、http://storage-test.designrecipe.jp
でサイトを公開することにしますので、DNS に以下のような設定を行います。(設定方法は利用しているサービスで異なるかもしれませんので、各種サービスに倣ってください。)
cname storage-test c.storage.googleapis.com.
これで、http://storage-test.designrecipe.jp
へのアクセスは DNS で http://c.storage.googleapis.com
に向き先を変えられ、http://c.storage.googleapis.com
をホストするサーバで HTTP Header の Host の値である storage-test.designrecipe.jp
の値をみて該当ホストに振り分ける、という処理になっているはずです。(正確には、http://c.storage.googleapis.com
も更に CNAME されていますので、「最終的に名前解決されたサーバで、」となります。)
DNS 設定後の情報は以下のようになっています。
$ dig storage-test.designrecipe.jp ...(snip)... ;; ANSWER SECTION: storage-test.designrecipe.jp. 120 IN CNAME c.storage.googleapis.com. c.storage.googleapis.com. 1096 IN CNAME storage.l.googleusercontent.com. storage.l.googleusercontent.com. 238 IN A 172.217.27.80 ...(snip)...
2. バケットの作成
Cloud Storage はオブジェクト(テストファイルや画像ファイルなどのこと)ストレージです。そのオブジェクトを格納するための入れ物として、バケットというものをまず用意する必要があります。
単にファイルのストレージとして利用する場合であれば、任意のバケットの名前を付ければいいのですが、今回 Web で公開し、かつ、FQDN (ホスト名) でアクセスできるようにします。そのためには、FQDN にどのバケットが関連するのか GCP 側で知る必要がありますので、バケット名には、DNS に設定した CNAME に一致する名前をつける必要があります。
今回、storage-test.designrecipe.jp
を使っておりますので、この名前をバケット名に使います。
$ gsutil mb \ -p project123 \ -l us-central1 \ -c regional \ gs://storage-test.designrecipe.jp
上記のコマンドですが、指定しているオプションは以下の通りです。
-p
はプロジェクトの名称を適切に設定します。-l
はバケットを作成するロケーションです。基本的には好きなところを選べばいいのですが、無料枠を使いたい場合には、「北バージニア [us-east4] を除く米国リージョン」を指定します。-c
はストレージクラス(ストレージタイプ)のことで、Multi-Regionalがベターだと思うのですが、無料枠を使いたい場合には、「Regional」を指定する必要があります。- CNAME したホスト名(FQDN)
storage-test.designrecipe.jp
を最後にgs://storage-test.designrecipe.jp
と指定します。
ちなにに、以下は2019/05現在の Cloud Storage の無料枠の定義です。
5 GB の Regional Storage(1 か月あたり、北バージニア [us-east4] を除く米国リージョンのみ)
5,000 回のクラス A オペレーション(1 か月あたり)
50,000 回のクラス B オペレーション(1 か月あたり)
1 GB の北米から全リージョン宛ての下りネットワーク(1 か月あたり、中国とオーストラリアを除く)
さて、ここで注意点があります。バケットの名称に FQDN を使用する場合には、使用されたドメインの所有確認が行われている必要があります。所有確認が行われていない場合、以下のようなエラーをみることになりますので、事前に設定を行っておきます。
$ gsutil mb -p project123 -l us-central1 -c regional gs://storage-test.designrecipe.jp Creating gs://storage-test.designrecipe.jp/... AccessDeniedException: 403 The bucket you tried to create requires domain ownership verification. Please see https://cloud.google.com/storage/docs/naming?hl=en#verification for more details.
3. コンテンツのアップ
Web コンテンツをアップします。
まずは、index.html
と 404.html
を用意します。
$ echo "Hello, GCP Storage." > index.html $ echo "not found page." > 404.html
先程作成したバケットにコンテンツをアップします。
$ gsutil cp \ -a public-read \ ./index.html \ ./404.html \ gs://storage-test.designrecipe.jp Copying file://./index.html [Content-Type=text/html]... Copying file://./404.html [Content-Type=text/html]... - [2 files][ 36.0 B/ 36.0 B] Operation completed over 2 objects/36.0 B.
cp の -a
オプションを使って public-read
を与えています。
-a
オプションは、canned_acl
と言われるもので、バケット、もしくは、オブジェクトに ACL をセットする簡単な方法です。
CANNED ACLS The simplest way to set an ACL on a bucket or object is using a "canned ACL". The available canned ACLs are:
値の public-read
はその名の通り、どのユーザにも公開する権限です。以下は、gsutil help acls
の出力からの抜粋ですが、必要なことがよく纏められて書いてありますので、そのまま貼り付けておきます。
(NOTE
で触れられていますが、クドいですが、Cache-Control は気にしておく必要があります。後半部分に記載します。)
public-read Gives all users (whether logged in or anonymous) READ permission. When you apply this to an object, anyone on the Internet can read the object without authenticating. NOTE: By default, publicly readable objects are served with a Cache-Control header allowing such objects to be cached for 3600 seconds. If you need to ensure that updates become visible immediately, you should set a Cache-Control header of "Cache-Control:private, max-age=0, no-transform" on such objects. For help doing this, see 'gsutil help setmeta'. NOTE: Setting a bucket ACL to public-read will remove all OWNER and WRITE permissions from everyone except the project owner group. Setting an object ACL to public-read will remove all OWNER and WRITE permissions from everyone except the object owner. For this reason, we recommend using the "acl ch" command to make these changes; see "gsutil help acl ch" for details.
Web のコンソールでも確認してみます。
問題ないようですので、実際にhttp://storage-test.designrecipe.jp/index.html
でアクセスしてみます。
OK です。
ちなみに、この状態でファイル(index.html
)を指定せずに、http://storage-test.designrecipe.jp/
でアクセスするとどうなるでしょうか。
Access Denied になっています。インデックスのページを設定します。
4. インデックスページとエラーページを設定する
gsutil
で web
サブコマンドを使います。
$ gsutil web set -m index.html -e 404.html gs://storage-test.designrecipe.jp Setting website configuration on gs://storage-test.designrecipe.jp/...
-m
は、main_page_suffix
ですので、メインページの指定です。-e
は error_page
ですので、エラー時(404)のページを指定します。
先程と同様に http://storage-test.designrecipe.jp/
でアクセスします。
OK です。今回は Access Denied ではなく、ちゃんと index に指定したページが表示されました。
適当な存在しないページの URI (http://storage-test.designrecipe.jp/abc.html
) でアクセスします。
こちらも OK です。ちゃんと 404 のページが表示されています。
Web サーバを準備することなしにあっという間に Web コンテンツを公開することができました。スバラシイですね。
5. Cache-Control (キャッシュコントロール)
さて、先ほどから後ほどとしていた Cache-Control
についてです。
help の記載にもありましたように、特に指定を行わずにコンテンツをあげた場合には、3600 秒、つまり1時間は同じ URI でアクセスを行った場合、仮にサーバ側のコンテンツを変更したとしても、キャッシュされた変更前のコンテンツが利用者には表示されます。
キャッシュはやった方がお得(キャッシュが効いている間はレスポンスに本文が含まれない、または、利用者の近い場所のサーバからコンテンツが配信されるなで描画が速くなります)ですが、ブラウザ、または、CDN などの中間サーバに一度キャッシュが保存された場合には、その賞味期限が切れるまで特に何もしなければコンテンツは昔のものがそのまま表示されてしまうので、コンテンツの更新時の運用はちょっと考える必要があります。
ここでは、単純にキャッシュしたくなければ、"Cache-Control:private, max-age=0, no-transform"
を指定するのも手ですが、public
のまま、max-age
を短くして試してみます。
このキャッシュが何で制御されているかといいますと、HTTP header の中の情報で制御されています。キャッシュはなかなか奥が深く、しっかり書くとこの記事で全ては書けなくなってしまうので、ここでは Cache-Control
だけを扱います。Cache-Control
を適切に記載することでキャッシュポリシーの規定を行えます。
この情報(正確にはこの情報とそれに付随してGCPが付加しているその他のheader)を見て、ブラウザ、間に入っている CDN、プロキシなどはキャッシュのコントロールを行います。一度アクセスを行った後は一定期間 Web へのアクセスを行わずローカルのファイルを参照したり、初めてのアクセスであってもオリジンとなるサーバまでコンテンツを取りに行かず、CDN などに残されているキャッシュで折り返したりします。通常、public
がついていれば、CDN などでキャッシュを行うことを意図しており、private
の場合は、ブラウザでのキャッシュ(逆に CDN などはキャッシュしない)を指定することになっています。
デフォルトで設定される該当の Cache-Control
の値は以下の値です。
$ curl -v -s http://storage-test.designrecipe.jp 2>&1 | grep -e Cache-Control -e Expires -e Last < Cache-Control: public, max-age=3600 < Expires: Mon, 13 May 2019 23:18:16 GMT < Last-Modified: Thu, 14 Feb 2019 11:15:24 GMT
これを変更するためには、HTTP Header の情報を cp
コマンドの前に追加します。
$ gsutil -h "Cache-Control:public,max-age=300" \ cp -a public-read test.html \ gs://storage-test.designrecipe.jp
上記では、max-age
300秒(5分)としていますので、リクエストの時刻を起点にレスポンスを300秒キャッシュに保存し、再利用することをできることを示しています。
$ curl -v -s http://storage-test.designrecipe.jp/test.html 2>&1 | grep -e Cache-Control -e Expires -e Last < Cache-Control: public,max-age=300 < Expires: Mon, 13 May 2019 22:52:26 GMT < Last-Modified: Mon, 13 May 2019 22:41:29 GMT
ちゃんと反映されています。
さて、今回時間をベースにキャッシュをコントロールしますが、ここで重要な HTTP header は、Last-Modified
Response header と、If-Modified-Since
Request header です。
Last-Modified
はサーバにあるコンテンツの最終更新日時の時間です。一度そのコンテンツを取得していれば、同じコンテンツのリクエストの場合(同じURI)には、前回取得したLast-Modified
のコンテンツの最終更新日時の値をIf-Modified-Since
Request header に入れてリクエストします。サーバ側では、そのIf-Modified-Since
の日時とサーバにあるコンテンツの最終更新日時を比較し、最終更新日時が更新されていれば、Response code 200 を返しつつ、実体の新しいコンテンツを返します。最終更新日時が更新されていない場合は、Response code 304 (Not Modified) を返し、実体は返しません。(本文は返しません。)
例をみてみます。以下では、
- Response code は 200 (実体が返っています)
- Response の
Last-Modified
header は "Mon, 13 May 2019 13:55:31 GMT"- サーバにあるコンテンツの最終更新日時です。
- Response の
Expires
は、"Mon, 13 May 2019 14:00:49 GMT"- 実はこれ、リクエストのあった日時に
Cache-Control
で指定したmax-age
の長さが加算されたものになっています(リクエストの日時が起点)。GCP 側でCache-Control
の値を見て勝手に付与しているようです。この日時までは仮にコンテンツに更新があっても新しいコンテンツは返されず、キャッシュの情報が参照されます。
(間違ってコンテンツをアップロードした時などに、すぐにコンテンツをアップロードし直しても反映されない場合などはこの設定が効いている可能性があります。)
- 実はこれ、リクエストのあった日時に
- Request の
If-Modified-Since
header は "Mon, 13 May 2019 13:39:01 GMT"
もう一度リクエストを行ってみると、
- Response code は 304 (Not Modified)
If-Modified-Since
の日時とサーバのコンテンツの日時を比較し、変更がないので、変更がないよ、というレスポンスだけを返し、実体は返しません。
- Request の
If-Modified-Since
header は "Mon, 13 May 2019 13:39:01 GMT" のまま- コンテンツは変わっていないので、先に受け取った
Last-Modified
の値のままです。
- コンテンツは変わっていないので、先に受け取った
サーバでコンテンツの更新が無い限り、304 を返し、コンテンツの実体は返しません。
CDN で管理されるキャッシュはその CDN の仕様によって若干変わるところはあると思いますが、GCP の場合は、
Cache-Control
でpublic
を指定した場合、max-age
に与えられた時間は初回のリクエストを起点にキャッシュされる。- その時間を過ぎた後は、
If-Modified-Since
の値をチェックし、コンテンツに変更がなければ 304 を返却。変更があれば、200 で実体を返す。
という一般的な動作になっているようです。
このことを理解した上でコンテンツを運用しないとすぐに更新したいコンテンツなどがあった場合には、意図したコンテンツを利用者に見せられない状態が発生することになります。
キャッシュに関しては以下のページに非常によく纏まっています。