:wq!

GhostをCloudFront+Nginxでキャッシュする

ども、takiponeです。

本ブログはオープンソースのブログエンジンGhostを利用しています。最近、Ghost バージョン1系へのアップグレードにあたりCDNとしてAmazon CloudFront、リバースプロキシとしてNginxでの構成に移行したのでご紹介します。


図で示すと以下になります。

GhostとNginxは、ScalewayパリリージョンのC1インスタンスでDockerで稼働させています。

CloudFrontを使う理由

日本からパリだとネットワーク的に遠いので、CloudFrontを噛ませて日本にあるエッジサーバーからコンテンツを配信するのが第一の目的です。ついでにCloudFrontによるフルHTTPS、HTTP/2、IPv6対応もお任せしています。無償でTLS証明書が取得できるACM(AWS Certificate Manager)が嬉しいですね。

CloudFlareにも同様の機能があり以前使っていた時期もあったのですが、TLS証明書が関係のないドメインとSANsでごっちゃにされるのが嫌だったのでCloudFrontに乗り換えた経緯があります。

なお、キャッシュ期間は/ghost(Ghost-Admin)は全ヘッダを転送&キャッシュなし、デフォルトのビヘイビアはGhostのレスポンスにMax-Age:60を付けて1分にしていますがもっと長くしても良いかなと思っています。

Nginxを使う理由

GhostをフルHTTPSでホストするためにはコンフィグのurlhttps://スキーマをセットする必要があるのですが、Ghostでのプロトコルの判断ロジックがExpressに依存していてX-Forwarded-Protoヘッダにhttpsをセットする必要があります。CloudFrontはオリジンへのX-Forwarded-Protoヘッダ転送をサポートせず、独自のCloudFront-Forwarded-Protoが代替のヘッダになっています。

以前はNginxを経由せずCloudFrontから直接Ghostを参照するために、Expressに以下のパッチを当てて対応していました。

diff -u -r old/node_modules/express/lib/request.js new/node_modules/express/lib/request.js
--- old/node_modules/express/lib/request.js	2016-02-07 13:04:41.000000000 +0900
+++ new/node_modules/express/lib/request.js	2016-02-07 13:08:58.000000000 +0900
@@ -315,7 +315,7 @@

   // Note: X-Forwarded-Proto is normally only ever a
   //       single value, but this is to be safe.
-  var header = this.get('X-Forwarded-Proto') || proto
+  var header = this.get('X-Forwarded-Proto') || this.get('CloudFront-Forwarded-Proto') || proto
   var index = header.indexOf(',')

   return index !== -1

Ghost 1系にアップデートするタイミングでこのパッチがエラーになってしまったので、Nginxでプロキシしてヘッダを置換することにしました。以下のイメージです。

利用しているnginx.confは以下の通りです。

nginx.conf

user  nginx;
worker_processes  1;

error_log  /dev/stderr warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log /dev/stdout main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    gzip  on;
    server_tokens off;

    server {
        listen 80;
        server_name _;
        location / {
          proxy_pass http://ghost:2368;
        }
        proxy_redirect          off;
        proxy_set_header        Host              $host;
        proxy_set_header        X-Real-IP         $remote_addr;
        proxy_set_header        X-Forwarded-Proto $http_cloudfront_forwarded_proto;
        client_max_body_size    10m;
        client_body_buffer_size 128k;
        proxy_connect_timeout   90;
        proxy_send_timeout      90;
        proxy_read_timeout      90;
        proxy_buffers           32 4k;
    }
}

Docker周り

nginx.confproxy_passディレクティブで指定しているホスト名ghostは、DockerのUser-defined bridgesのDNS機能でGhostコンテナのIPアドレスに名前解決できることを想定しています。docker-compose.yamlでUser-defined bridge internalを定義してNginxとGhostの両コンテナが接続しています。

docker-compose.yaml

version: '2'
services:
  nginx:
    image: "arm32v7/nginx"
    ports:
      - "80:80"
    volumes:
      - "/opt/ghost/nginx.conf:/etc/nginx/nginx.conf"
    networks:
      - internal
  ghost:
    image: "arm32v7/ghost:1.19.2"
    container_name: "ghost"
    volumes:
      - "/opt/ghost/content/:/var/lib/ghost/content/"
      - "/opt/ghost/config.production.json:/var/lib/ghost/config.production.json"
    networks:
      - internal
networks:
  internal:

なお、ghostコンテナのイメージ名は年明けに修正された日本語IME対応のPRを適用するために、バージョン1.22のGhostコンテナを手元でビルドして利用しているため実際と異なるものです(tamurashingoさんに圧倒的感謝!)。Docker Hubのarm32v7/ghostイメージのビルドが最近Failしているのが気がかりです。。。

コスト

ScalewayのC1サーバーが2.99€/月、AWSはRoute 53の$0.5/月とCloudFrontの従量がだいたい$0.5/月くらいで概ね月額500円です。C1サーバーは2年半前から利用していますが、とても安定していて満足しています。

まとめ

Ghostをホストする例として、CDNのCloudFrontとヘッダ置換のためにリバースプロキシとしてNginxを組み合わせる例をご紹介しました。