Railsで assets:precompile
したときに吐き出されるapplication.js/application.cssなどの解決方法についての検討です。
webpackerで吐き出されるファイルたちも範疇に入っています。
前提として、 assets:precomile
をするとdigestが付与されたファイルたちをpublicに出力します。
public └── assets ├── application-6788f0a7ffd28bf5fe4cc7c1c16c6c7fc009a2183e45f5bb230ee409843071f3.js ├── application-6788f0a7ffd28bf5fe4cc7c1c16c6c7fc009a2183e45f5bb230ee409843071f3.js.gz ├── application-d5effe4f291031ebf6419a81688641cbb134accdaece09f682f7e2c96a09d881.css └── application-d5effe4f291031ebf6419a81688641cbb134accdaece09f682f7e2c96a09d881.css.gz
このファイルたちをimageの中に含めていたのですが、運用してみて以下のようなことに気づきました。
- RollingUpdateなので同時に複数バージョンのpuma podが起動している瞬間が存在する
- これ自体は想定していた
- 古いpodにアクセスされた後にブラウザからのassetsリクエストを新しいpodで受けるとassetsが取れない
- デプロイ時に挙動が変なケースがある、ということから気づいた
上記のような問題より、assetsを複数バージョンのimageで解決する方法について検討しました。
調べたり思いついたりしたのは以下の案です。
- GCS bucketにassetsを配置する
- firebase hostingにassetsを配置する
- S3 bucketにassetsを配置する
- ingress nginx controllerでassetsをキャッシュする
最終的にingress nginx controllerでassetsをキャッシュする方式を採用しました。
他の案がなぜ没になったか、なぜ採用したのかも書いておきます。
GCS bucketにassetsを配置する
GCSにassetsを置いて静的サイトとして公開する場合だとGCSのみではhttpsリクエストを受けられません*1。
前段にLBを挟んでSSLはそこでterminateしてhttpリクエストをGCS bucketに流す必要があります。
LBを前段に挟むと費用がかさんでしまうので静的サイトとして公開するのは没になりました。
他の手段として、全てのオブジェクトを全てのユーザに公開するという手法もありました。
この場合だとGCSのAPIを経由してhttpsでアクセスを受けることができます。
リダイレクトさせるためには以下の設定をnginxに記載し、期待通りの動作をすることは確認できました。
rewrite ^/assets(.*)$ https://storage.googleapis.com/assets-bucket/assets$1 permanent;
この方式にしてGCSにassetsを置く方法でも良いなとは思ったのですが、GCSに置くあたりのintegrationで悩みました*2。
assets_sync gemを利用するか、自前で書くか、GitHub Actionsの中で解決するようにするか、あたりです。
最終的にはingress nginx controllerでのキャッシュが一番要件にあっていてシンプルだった、というのが見送った理由です。
firebase hostingにassetsを配置する
GCSで静的サイトを公開した場合にhttpsリクエストを受けられないと知った後に調査しました。
こちらが没になったのは以下のような理由です。
- 各ドメインごとにfirebase hostingのprojectを作る必要がある
- 管理が煩雑になるのでNG
- firebase hostingにassetsを配置するのが手間がかかりそうだった
- firebaseはterraformで管理していなかったので手動オペレーションが増えることが微妙
S3 bucketにassetsを配置する
GCSと違ってbucketを静的サイトとして公開してもhttps通信が可能です。
ただ、複数のcloudを管理するのは個人趣味程度だと煩雑になりそう、というのが見送った経緯です。
AWSでフルに構成していたらこちらを採用したかもしれません。
ただ自分の要件だとingress nginx controllerキャッシュが(ry
ingress nginx controllerでassetsをキャッシュする
自分の構成の場合、全てのドメインは単独のingress nginx controller podを通してアクセスが来ている構成になっていました。
ingress nginx controllerから各サービスのnginxにreverse proxyするような構成です。
そして、各サービスのnginx podはサービスが更新されるとimageが変わる、アプリとのsidecarになっています。
そのため各サービス側のnginxでassetsをキャッシュするのは要件にあいませんでした。
ingress nginx controllerでキャッシュができると解決するなーと思いながら調べていたらできたのでそちらを採用しました。
具体的には以下のような修正を入れました。
stable/nginx-ingress chartを利用していたのでvalues.yamlに以下の部分を追加しました。
この設定を書いておくとingress nginx controller pod内の /tmp/nginx-cache
に指定されたパスのレスポンスがキャッシュされます。
controller: config: http-snippet: | proxy_cache_path /tmp/nginx-cache levels=1:2 keys_zone=static-cache:2m max_size=100m inactive=1d use_temp_path=off; proxy_cache_key "$scheme://$proxy_host$request_uri$is_args$args"; proxy_cache_lock on; proxy_cache_use_stale updating;
次に各サービスのingressを以下のように変更しました。
before
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: before-ing spec: tls: - secretName: example-tls hosts: - example.com rules: - host: example.com http: paths: - backend: serviceName: app servicePort: 80
after
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: after-ing spec: tls: - secretName: example-tls hosts: - example.com rules: - host: example.com http: paths: - backend: serviceName: app servicePort: 80 --- apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: after-ing-static annotations: nginx.ingress.kubernetes.io/proxy-buffering: "on" nginx.ingress.kubernetes.io/configuration-snippet: | proxy_cache static-cache; proxy_cache_valid 200 301 302 1d; proxy_cache_valid 404 10m; proxy_cache_use_stale error timeout updating http_404 http_500 http_502 http_503 http_504; proxy_cache_bypass $http_x_purge; add_header X-Cache-Status $upstream_cache_status; add_header Cache-Control "public, max-age=86400, s-maxage=86400"; spec: tls: - secretName: example-tls hosts: - example.com rules: - host: example.com http: paths: - path: /assets backend: serviceName: app servicePort: 80
ざっくりいうとassets用のingressを用意してassetsの場合はキャッシュするようにしました。
この対策を行うと、アプリに来る前にingress-nginx-controller内のキャッシュがあればそのまま返してくれるようになります。
そのため、railsでは特に問題ない部分ですがassetsにはuniqなdigestが付与されている必要があります。
キャッシュの期間はとりあえず1日程度にしているのですが、デプロイ時のRolling Updateの瞬間だけカバーされていれば良いので、実際はもっと短くて良いと思います。
1hぐらいで十分なのではないでしょうか。
assets/packsのようなファイルに関してはデプロイ前に誰かが参照しているであろう、という前提の元ですがこの対策を入れると複数バージョンを跨いだ場合でもassetsの状態を気にしなくて良くなりました。
独自ツールを作ったり新規bucketのterraform設定を書いたりもしなくて良いので、工数的には一番安く問題を解決できると感じ、この構成を採用しました。
複数のサービスを運用していると、サービスの方にこのような問題解決の方法を仕込みに行くのが手間なのでk8sレイヤで解決してアプリに改修は不要、という構成だったのも魅力のポイントです。
ただingress-nginx-controller podが複数いたりするとcacheの共有をしておく必要が出てきたりと考えることが増えてしまいます。
お金に余裕がある場合ならGCSの前段にLBを挟むとCloud CDNとかも併用できるようになるのでそちらを採用するのが良いと思います。