PHP
AWS
S3
Laravel
CloudFront

S3にCloudFrontを通すことで月20万ぐらい節約した話

More than 1 year has passed since last update.

開発しているサイトで画像や動画などの静的ファイルをS3に置き、HTMLのimgやvideoタグでS3URLを指定し読み込むということをやっていたんですが、この方法では予想よりもかなりお金がかかったためS3との間にCloudFrontを通したところ料金が激安になったという話です。

最初からCloudFront使っとけって話なんですが、インフラの経験が足りずに一月ぐらい出遅れたという失敗談でもあります。


AWSの料金でDataTransferが急増。タグ付けをすることでどのサービスが原因かを特定する

AWSの利用料金がかなり上がったことは日時でSlackに通知されるようになっているため気づくことができました。

しかし、料金カテゴリにはプロダクト名やサービス名ではなくDataTransferとしか出ていなかったため、どのサービスが原因か特定する必要がありました。

DataTransferということなのでデータ通信に負荷がかかりすぎていることはなんとなく予想できたのですが、とりあえずS3など主要なサービスにタグ付をすることにしました。

S3でのタグ付はバケットに入って、プロパティ > 詳細設定のTagsからすることができます。ひとまずキーにはName、値にはサービス名を設定しました。

これによりAWSのコストエクスプローラーでタグごとの利用料金を計測することができます。

タグはS3のバケットごとに割り振るため、利用料金急騰はどのプロダクトで使用しているS3バケットかまで特定することができました。

S3では置いているだけでもお金がかかりますが、リクエストに対するレスポンスもデータ量に応じてお金がかかるため、動画なんか置いてたらちょっとでも人が来るようになったらヤバイってことですね。


S3の間にCloudFrontをはさむことでS3へのリクエストを減らす

S3からの大量データ送信がまずいわけなのでそれを減らすためにキャッシュサーバであるCloudFrontを導入します。

ユーザーはCloudFrontにデータをリクエストしますが、CloudFrontからデータを送信するコストはS3から送信するコストよりもかなり安く済むので、DataTransferの料金は安くなる上にエッジサーバーから送信され、HTTP2も使えるので速くもなります。

ただし、CloudFront自体の料金もあり、そこのコストによってはS3だけで十分という場合もあるかもしれません。

CloudFrontのディストリビューションを作ること自体はすごく簡単で、基本的にデリバリーメソッドをwebで、OriginDomainNameにS3のホストを入れるだけ。


CloudFrontのキャッシュには気をつける必要がある

CloudFrontを導入したことで安くなり速くなりいいことづくめだったが、キャッシュまわりで少し新たに手を加える必要があった。

というのも、S3に上げた画像や動画は、CloudFront側で自動で検知して更新を反映してくれるわけではないので、画像などを更新したはずがユーザーはCloudFrontの方しか見ないので、いつまでもキャッシュされた古いデータを見てしまうということが起こり得るのだ。

CloudFrontのキャッシュ時間はS3オリジンのCache-ControlとCloudFrontのMIN/MAX/Default TTLから決定されるようだ。

このあたりについては下記URLが参考になった。

【新機能】Amazon CloudFrontに「Maximum TTL / Default TTL」が設定できるようになりました! | Developers.IO

上記の設定も参考にしたが、CloudFrontにはInvalidationというキャッシュを更新する機能が備わっており、今回はそれで問題の解消を図ることにした。

CloudFrontのディストリビューションに入って、Invalidationsタブからパスを指定してInvalidateボタンを押すことでそのパスのオブジェクトのキャッシュが更新される。

全てのオブジェクトを更新したければ/*で更新すればよい。

ただ、頻繁に全てのオブジェクトを更新していてはCloudFrontの効果が薄れるし、何より手動で毎回やるのはめんどくさい。

AWSには当然ながらこういった機能をプログラム側から操作できるAPIも備えており、今回のプロダクトはLaravelだったので、aws-sdk-phpから特定のパスでinvalidationをすることにした。

protected static function cloudFrontInvalidation($paths)

{
$client = \AWS::createClient('CloudFront');

$client->createInvalidation([
'DistributionId' => config('filesystems.cloud_front_distribution_id'),
'InvalidationBatch' => [
'Paths' => [
'Quantity' => count($paths),
'Items' => $paths,
],
'CallerReference' => time(),
],
]);
}

このような特定のパスでinvalidationできるメソッドを作成し、S3へのアップロードを行う処理のところに組み込むことで、キャッシュについての問題も解消することができた。

なお、invalidation自体にも料金はかかるが、月に1000invalidationはまでは無料で、それ以後も安い料金だったのでプログラムに組み込むことでその辺りの心配は少なかった。

また、1000invalidationというのは更新されるオブジェクトの数ではなく、あくまでパスベースなのでうまくパスを指定すれば大量オブジェクトのキャッシュ更新を少ないinvalidationで実現することもできる。


参考サイト

【新機能】Amazon CloudFrontに「Maximum TTL / Default TTL」が設定できるようになりました! | Developers.IO