続 カッコの付け方

AWSを始めとしたクラウドコンピューティング全般と、唯一神emacsにおける()の付け方についてだらだら書きます

CloudFront ワイルドカードキャッシュクリアの不具合(修正済)

CloudFront(以下CFと略)を始め、CDNを利用するにあたり、まず注意することは、キャッシュを削除する手段を確立することです。CFはinvalidationをURL毎に発行することにより、このキャッシュクリアを実現していましたが、昨年、ワイルドカード (e.g /image/*) で一気にURLを指定し、クリアすることができるようになりました、が、一部の環境において不具合があり、それをAWSサポートに連絡、修正してもらいました。今はすでに治っているので問題ありませんが、CFの理解のために記述しておきます。

不具合ってどこよ?

https://blog.cloudpack.jp/2014/11/06/etcetera-to-update-header-on-cloudfront/

この問題を 勝手に古いヘッダのキャッシュ問題 と呼称します。
発生のメカニズムは

  1. CFにキャッシュされる、このとき、レスポンスヘッダもキャッシュされる
  2. TTLが切れて、オリジンにリクエストが飛ぶ
  3. オリジンのヘッダだけが変わっていて、コンテンツに変化がない場合、オリジンは304を返す
  4. CFは304を受けると、1. でキャッシュしたデータをつかいまわす

これに対して invalidationをかければOK!なんですが、ワイルドカードでinvalidationを掛けた場合、上手くクリアできません。

ヘッダだけが変わるってどういう状況?

CORSです。CORSの設定を忘れて、CFにキャッシュされたあとに、オリジンにCORS向けヘッダを指定したらこの状況になります。

CORSってなんや?

長くなるので適当に AJAXなどで、動的に別のURLの画像やコンテンツを取得し、使う場合に発動するブラウザ側のセキュリティーチェックと考えてください。
「えっ、Yahooとかでも外部サイトの広告とか出てるやん」と思うでしょうが、あくまでも動的にやる場合です、HTMLにベタでimageタグ書く場合はCORS関係ないです。ちょー適当にいうとWeb2.0です。知らないと生きていけない知識ですので、はじめて知った人はググってください。

今回はGETのことだけ考えていますが、POSTの場合でもCORSはあります。プリフライトリクエストでググってください。こちらはちょっとむずかしいですので、私のボケ防止にいつか書く。

CFのキャッシュの一生

CFは特定のURLリクエストを受けると、下記の動作をとります。

  • CFにキャッシュが無い場合は、オリジンに取りに行く Miss from cloudfront
  • CFにキャッシュがあり、且つTTLが切れていない場合は、キャッシュから返す Hit from cloudfront
  • CFにキャッシュがあるが、TTLが切れている場合は、再度オリジンに取りに行く RefreshHit from cloudfront

上の2つはだいたいわかると思いますが、最後の RefreshHit from cloudfront の時に 古いヘッダのキャッシュ問題は発生します。

TTLが切れたらキャッシュは破棄されるのではないのか?

破棄されません。破棄されるとおもいますよねーふつう。原理的にこれがあるので、古いヘッダのキャッシュ問題が発生するのです。

ということは 古いヘッダのキャッシュ問題は放っておいたらずっと解消しない?

確率としては極めて低いですが、解消する可能性はあります。
CFは RefreshHit from cloudfront 状態で実はキャッシュは捨てていないのですが、キャッシュ対象のURLに一定時間以上、アクセスが発生しない場合は、完全にキャッシュ削除されるタイミングがあります。しかしこのタイミングは完全にAWS様の機嫌次第です。何分とか明言されておりません。またTTLが1日とかの場合は挙動が変わるかもしれません(試してないです)

検証

状況を整理しておきます。

  • 古いヘッダのキャッシュ問題そのものは修正されていない。個人的にはTTL切れ = キャッシュ破棄としてもらうほうがいいので、改善要望は出した。
  • 古いヘッダのキャッシュ問題のW/Aは、invalidationすること。
  • しかし 個別URLで invalidation した場合は綺麗に Miss from cloudfrontになるが、ワイルドカードでinvalidation した場合は、RefreshHit from cloudfrontになる(キャッシュ破棄が発生しない)。

今回の問題は、ワイルドカード invaliだと古いヘッダのキャッシュ問題のW/Aが効かない(かった)、ということです。しかもこの問題はTTLが十分に長い設定(e.g 1day)などの場合は発生しません。私の検証では、Hit from cloudfront状態つまり、TTLが切れていない状態で、ワイルドカード invaliを掛けた場合は、なぜかMiss from cloudfrontになりますつまりW/Aが効いて、キャッシュが破棄される。TTLを1分とか短い設定にしていると、必然的にRefreshHit from cloudfrontになりやすく、この状態でのW/A(ワイルドカードでinvaliをかける)が効かないということがわかっています(た)。

手順概要

  1. S3にダミーコンテンツをぶち込む
  2. S3にCORSの設定をしない
  3. CFをS3オリジンで作る、TTLは30秒とか短くする
  4. CFにキャッシュを吸わせる = 古いヘッダのキャッシュ を作る
  5. S3にCORSの設定をする
  6. CFに問い合わせて古いヘッダのキャッシュ問題の発生を確認
  7. ワイルドカードで invaliかける
  8. CFに問い合わせてMiss from cloudfrontを確認 (不具合修正の確認)

検証スクリプト

S3にダミーコンテンツをぶち込むやつ

#!/bin/env ruby

require 'aws-sdk'
BACKET_NAME = 'YOUR Bucket Name'

def put_3k_files (d, ver)
  s3 = Aws::S3::Client.new(region: 'YOUR Region')
  for num in 0..1000 do
    key = d + sprintf("/%04d.txt",num)
    s3.put_object(bucket: BACKET_NAME, key: key, body: 'this is ' + key + 'ver ' + ver)
  end
end

put_3k_files 'js', '1'
put_3k_files 'js/001', '1'
put_3k_files 'js/001/a', '1'
put_3k_files 'js/001/b', '1'
put_3k_files 'sound/prd', '1'
put_3k_files 'asset', '1'

S3を突っつくやつ

#!/bin/env ruby

require 'net/http'
require 'uri'
require 'resolv'

def get_url(ac_url)
  url = URI.parse(ac_url)
  req = Net::HTTP::Get.new(url.path)
  req.add_field 'Origin', 'http:example.com'
  req.add_field 'Host', 'CF FQDN'
  res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req)}

  ret_arr = [7]
  ret_arr[0] = ac_url
  ret_arr[1] = res.code
  ret_arr[2] = "None"
  res.each do |n, v|
    if n == 'access-control-allow-origin'
      ret_arr[2] = v
    end
    if n == 'content-length'
      ret_arr[3] = v
    end
    if n == 'x-cache'
      ret_arr[4] = v
    end
    if n == 'etag'
      ret_arr[5] = v
    end
    if n == 'age'
      ret_arr[6] = v
    end
  end
  return ret_arr.join("\t")
end

rslv = Resolv::DNS.new()
ip_list = rslv.getaddresses('CF FQDN')

dirs = ['js','js/001','js/001/a','js/001/b','sound/prd','asset']
#dirs = ['js']

for num in 0..1000 do
  key = ARGV[0] + sprintf("/%04d.txt",num)
  full_url = 'http://' + 'CF FQDN' + "/" +  key
  print get_url(full_url) + "\n"

#    ip_list.each do |ip|
#      full_url = 'http://' + ip.to_s + "/" +  key
#      print get_url(full_url) + "\n"
#    end
end

コードは散らかっているが、引数に js とか asset とか指定すると、その配下の1ファイルを突っつきます。出力の3カラム目に *がきていれば CORS用のヘッダが来ています。コメントアウトしてますが、DNSラウンドロビンで返してくるIP全てに対しても打つ事ができます。(あたりまえですが、めちゃ時間かかります)興味ある人は試してみてください。

検証結果

問題は確かに解消していました。

まとめ

ワイルドカードによるinvalidationの問題は解消されましたが、基本的にinvalidation自体が悪手です。さらに、前述してますが、TTLが切れたらキャッシュも削除される という認識が根本的に間違っています(いいか悪いかを別にして、CFにおいては)。さらにさらに、TTLさえ短ければキャッシュ残りは心配しなくていいという考えも間違っています、レスポンスヘッダとコンテンツをCFはごちゃまぜでキャッシュしています。
よって、古典に習い

  • 先頭URLをバージョンとかにして、バージョンごとにURLを使い捨てる
  • QueryString等でごまかす

を使うのがベストです。別URLはCFは別キャッシュだと判断するので、オリジンが同じであろうと関係なく、URLが違えば別キャッシュとして扱う と覚えておきましょう。ちなみにリクエストヘッダもCFが同じキャッシュとみなすか否かに含まれているようです(試してみてください)。少し話がそれましたが、invalidationはおまけぐらいに心構えるのが良いです。

.. あと、この手の検証をやる人は、ブラウザ側のキャッシュを強制無効にしてやるのがいいです。