將WordPress 使用的CDN 從CloudFlare 切換到AWS CloudFront

CloudFlare 這間公司是個大善人,多年前剛起步的時候我不由得感嘆我靠!為什麼會有人做這種事情,在Twitter 上給CEO 發消息感激他的貢獻,當時他還回我了。

後來順著internet https 的流行,率先推出不要錢的SSL/TLS offloading 服務,引起一大波網站從其他CDN 遷移,因為其他的CDN 光是證書就要收上一大筆錢。

再後來他的WAF 規則對阻止wordpress 的spam comments 效果很好,所以就一直用他。

但,他從中國訪問的效果一向不佳,早年和百度合作的時候,曾經有一段時間很順暢但非常短暫,他的香港節點並不直接提供給中國用戶使用,中國用戶直接訪問會被遞送到美國的節點,如果網站啟用了CloudFlare 的收費服務Argo Smart Routing,中國用戶才會被遞送到香港節點。是不是聽起來和AWS Global Accelerator很像?

之所以想要換成AWS CloudFront ,是因為經過這些年行業廝殺,也有了0$ 的方案,主要特徵如下:

3個Free Plan (官方網站沒說)
5GB S3 存儲用於靜態文件。
每月1M 請求量。
每月100GB 數據傳輸量。
5 條cache behavior rules (官方網站上說的是5條WAF rules,沒有講cache behavior rules)。

好了看這張圖吧,吧啦吧啦一大堆,有用的沒幾個:

簡單來說,每天的流量小於3.3G,請求量小於33333,就可以考慮使用0$ 方案的CloudFront。

之所以想要切換是因為CloudFront 雖然也會時不時因為別的網站連帶被GFW 屏蔽掉,但比起來,CloudFlare 看起來是無差別在中國境內被限速,有時候在中國境內,CloudFront 還是蠻快的,畢竟浙江那麼多做海外電商的中小企業,每天都在抱怨錄入一個商品清單都要花兩個小時。

首先為我的源站更換TLS 證書,因為CloudFront 只接受使用合法證書的源站,不接受自簽發證書。CloudFlare 雖然是支持無法驗證的自簽發證書,但是他也支持使用client/server 證書雙向驗證這種更安全的機制確保源站不會被直接訪問。

免費的合法證書,騰訊雲以曾經提供過一段時間,不知道現在還有沒有,不過現在流行的是letsencrypt 可以免費續期,90天效期的合法數字證書。

安裝certbot和nginx plugin:

dnf install certbot python3-certbot-nginx

進行證書簽發,此時需要確保站點是可以訪問的,因為letsencrypt 會通過訪問站點完成驗證,建議先將網站域名直接解析至IP地址。

certbot --nginx -d bbken.org -d www.bbken.org
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Renewing an existing certificate for bbken.org and www.bbken.org
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/bbken.org/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/bbken.org/privkey.pem
This certificate expires on 2026-02-27.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
Deploying certificate
Successfully deployed certificate for bbken.org to /etc/nginx/vhosts/bbken.org.conf
Successfully deployed certificate for www.bbken.org to /etc/nginx/vhosts/bbken.org.conf
Your existing certificate has been successfully renewed, and the new certificate has been installed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

雖然他說已經setup a scheduled task 但是有時候並沒有,如果驗證後沒有,那麼需要自己創建一個定時器:

創建定時器:

sudo tee /etc/systemd/system/certbot-renew.timer << 'EOF'
[Unit]
Description=Certbot Renewal Timer
[Timer]
OnCalendar=daily
RandomizedDelaySec=12h
Persistent=true
[Install]
WantedBy=timers.target
EOF

創建服務:

sudo tee /etc/systemd/system/certbot-renew.service << 'EOF'
[Unit]
Description=Certbot Renewal
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
EOF

啟用服務:

sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renew.timer

驗證服務:

sudo systemctl status certbot-renew.timer
sudo systemctl list-timers | grep certbot

到這裡,證書就會每天檢查,一旦需要更新就會自動更新,那就等90天後再看看吧,反正現在看也看不出什麼來。

源站配置好之後,接下來需要配置CloudFront,添加一個新的distribution,

Create distribution ,輸入一個名字,例如CF-bbken.org,隨便輸入什麼貓貓狗狗都可以。

選擇源站類型,靜態網站也可以使用Amazon S3,比如做圖片網站,相冊,使用hugo 做blog,純靜態html 發布,這裡我使用Other,然後在下方輸入源站域名,因為源站不支持IP地址,所以你必須使用一個二級域名指向IP地址,例如source.bbken.org。

頁面拉到下方,選擇Customize origin settings,創建一個custom header,因為,失去了CloudFlare 的client/server 雙向數字證書驗證保護源站,要如何來保護CloudFront 後面的源站呢?

通過設置源站的 custom header,CloudFront 在向源站發送的每一個請求都會附加這個header,源站將通過這個header 識別來自於CloudFront 的請求並拒絕其他請求,從而保護源站不受到攻擊,這裡我使用如下的配置:

header name = X-Origin-Verify
value = 5Pb9Qsf2XbZK23C

與此同時,在nginx 的host 配置文件中,加入下面的配置,該配置將拒絕所有不帶有這個header 的請求從而保護源站:

###bbken.org at 443####
        server {
        server_name bbken.org www.bbken.org;
        listen 443 ssl;
        listen [::]:443 ssl;
        # Block all requests without CloudFront header
        if ($http_x_origin_verify != "5Pb9Qsf2XbZK23C") {
        return 403;
            }

同時,為了保護wordpress 的login page 和xmlrpc api,可以加入下面的配置,例如我可以只允許我的IP地址31.13.87.36 訪問wp-login.php,並限制只有來自於日本的IP地址可以訪問到xmlrpc api:

location = /wp-login.php {
    allow 31.13.87.36;        # Your IP address
    deny all;
        }
    include fastcgi-ssl.conf;
    fastcgi_pass unix:/home/www/php.sock;
    }
location = /xmlrpc.php {
        if ($http_cloudfront_viewer_country != "JP") {
            return 403;
        }

但用戶的請求中並不會帶有country code 呢,這個country code 從何而來?這就是下一步的cache settings 中需要修改的部分:

將cache policy 設置為CachingOptimozed,將Origin request policy 設置為AllViewerAndCloudFrontHeaders-2022-06。

這裡配置的是default 路徑下所有文件的cache policy,在源站請求策略中,需要為源站送去所有來自於用戶的headers 並且加上CloudFront 加上額外的headers, 其中就包括了根據Maxmind GeoIP database 分析用戶IP地址所得出的 country code。

你可以仔細閱讀官方用戶指南中,CloudFront 加上了哪些headers。

也可以寫一個頁面來獲取他,例如headers.php

<?php
header('Content-Type: text/plain');
print_r(getallheaders());
?>

他長這個樣子,比如我在日本,訪問的時候甚至可以精確到城市Osaka,當然,Maxmind 的GeoIP city 免費資料庫出錯的機率有時候還是很大的,我想以AWS 很摳的風格來說,應該不會去買收費的資料庫。

Array
(
    [Cloudfront-Viewer-Latitude] => 34.84230
    [Cloudfront-Viewer-Time-Zone] => Asia/Tokyo
    [Cloudfront-Viewer-Postal-Code] => 562-0021
    [Cloudfront-Viewer-City] => Osaka
    [Cloudfront-Viewer-Country-Region-Name] => Osaka
    [Cloudfront-Viewer-Country-Region] => 27
    [Cloudfront-Viewer-Country-Name] => Japan
    [Cloudfront-Viewer-Country] => JP
    [Cloudfront-Viewer-Http-Version] => 3.0
    [Priority] => u=0, i
    [Sec-Fetch-Dest] => document
    [Sec-Fetch-User] => ?1
    [Sec-Fetch-Mode] => navigate
    [Sec-Fetch-Site] => none
    [Upgrade-Insecure-Requests] => 1
    [Sec-Ch-Ua-Platform] => "macOS"
    [Sec-Ch-Ua-Mobile] => ?0
    [Sec-Ch-Ua] => "Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"
    [X-Amz-Cf-Id] => Sc-drS9O65XlBKlqmXBgQy5o8GKUtQ4pRw8NiqKxWEBvkIk7Bh4VKg==
    [Cloudfront-Viewer-Tls] => TLSv1.3:TLS_AES_128_GCM_SHA256:fullHandshake
    [Cloudfront-Viewer-Address] => 152.69.197.33:49333
    [Accept-Encoding] => br,gzip
    [Cloudfront-Viewer-Longitude] => 135.50400
    [Cloudfront-Viewer-Asn] => 31898
    [Connection] => keep-alive
    [Via] => 3.0 0cf2f9f29d4ea64bbc1cf639883c7e5a.cloudfront.net (CloudFront)
    [User-Agent] => Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
    [X-Forwarded-For] => 152.69.197.33
    [Cloudfront-Forwarded-Proto] => https
    [Accept] => text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
    [Accept-Language] => en-US,en;q=0.9
    [Cookie] => _ga=GA1.1.1737721359.1763112581; _ga_PBL0EPJRYT=GS2.1.s1764820194$o3$g1$t1764820240$j14$l0$h0; cf_clearance=AeyyOOy1qnnUVxucgyJEPLGjgOaxN7w3CJX0f7Kkty8-1764388065-1.2.1.1-84shCFFoEVI.YrH7rr6BuaQRU0UILV7DKkL8GyWY8O5cDTyCJ0LL9RleTZVQS11eUsBX.npUGs3UkUgDRUkBHsC3JndxqSC.W9TNlLYSOncByE5Mj88kppK55MB4.CwjTcDq6QoB7ULnu7I4hrFFiD6PQnRMY6TfBFZL_Sm2dyMcuXX3GF2Yvlr8wBqbsUWVLx_v8OuzDFViHOUXS9PqG4BsG1.R5ASPCPqbyr_bgXI
    [Cloudfront-Is-Android-Viewer] => false
    [Cloudfront-Is-Ios-Viewer] => false
    [Cloudfront-Is-Desktop-Viewer] => true
    [Cloudfront-Is-Smarttv-Viewer] => false
    [Cloudfront-Is-Tablet-Viewer] => false
    [Cloudfront-Is-Mobile-Viewer] => false
    [X-Origin-Verify] => 5Pb9Qsf2XbZK23C
    [Host] => bbken.org
    [Content-Length] => 
    [Content-Type] => 
)

預覽一下,堅決的創建他。

接下來進入Cache Behaviors 設置,添加幾個新的cache policy,網路上有很多關於wordpress 的cache policy 設置,他們使用傳統的legacy settings, 都已經老舊而顯得不合時宜,這裡有兩種方案提供給你,注意喔,不能超過5條喔:

第一種:預設緩存所有文件和路徑,然後將必要的不需要cache 的路徑單獨拿出來,請注意先後順序,有優先級。

優點:靜態化很強,如果使用了wp-supercache 進行靜態化的話,效果就很好,因為所有blog posts 都是pure html 進行緩存。
缺點:comments 的部分可能還需要加入到 Managed-CachingDisabled ,需要安裝plugin來自動刷新緩存,否則你新增一篇blog 但是首頁不會更新。

/wp-login.php       
Managed-CachingDisabled 
Managed-AllViewerAndCloudFrontHeaders-2022-06

/xmlrpc.php         
Managed-CachingDisabled 
Managed-AllViewerAndCloudFrontHeaders-2022-06

/wp-admin/*         
Managed-CachingDisabled 
Managed-AllViewerAndCloudFrontHeaders-2022-06

Default(*)         
Managed-CachingOptimized 
Managed-AllViewerAndCloudFrontHeaders-2022-06

第二種:預設不緩存所有文件和路徑,然後將需要緩存的路徑拿出來,請注意先後順序,有優先級。

優點:只緩存必要的部分,這兩個路徑是官方建議以及wp-supercache 建議,以及目前網路上大多數wordpress 和cloudfront 結合建議緩存的路徑。
缺點:對於靜態化的wordpress 無法做到所有的blog 頁面緩存。

/wp-includes/*      
Managed-CachingOptimized 
Managed-AllViewerAndCloudFrontHeaders-2022-06

/wp-content/*       
Managed-CachingOptimized 
Managed-AllViewerAndCloudFrontHeaders-2022-06

Default(*)         
Managed-CachingDisabled 
Managed-AllViewerAndCloudFrontHeaders-2022-06

配置好Behaviors 之後,回到Distribution 的首頁,我還沒有添加domain,沒有添加之前,CloudFront 上面就不能識別你的domain name,即使你將域名解析過來也是不行的,點擊Add domain。

輸入domain name,這裡我輸入兩個,你也可以輸入多個,甚至可以將不同的domains 放在同一個distrbution:

這個時候,會提示沒有TLS 證書,點擊Create certificate,

AWS certificate manager 會自動為你生成兩個需要在domain resolver 那邊需要添加的CNAME,將他們添加進去。

添加之後這個頁面會自動刷新驗證,DNS 解析生效時間不一,有的長有的短,但是現在大多數DNS 服務商都已經縮短為300秒,哪像我剛上網那時候都一天兩天的,不要錢的證書創建成功後,點擊Next。

預覽一下,沒問題,點擊Add domains,

這樣就完全結束了!等待右上角的Deploying 完成,就可以訪問啦!

好了回過頭來說wordpress 的cloudfront plugin,主要就是刷新緩存,安裝C3 Cloudfront Cache Controller,安裝完成後,首先需要創建一個IAM 用戶,為他賦予CloudFront 讀取和刷新緩存的權限,並分配一個AK/SK,當然,如果你的server 在EC2 ,你也可以使用instance profile ,這樣AK/SK 可以留空即可。輸入Distribution ID 和AK/SK,Save Changes。

輸入post id 來刷新,或是選擇Flush All Cache。

輸入post id 刷新,將會刷新post,分類以及首頁。

在更新一篇blog 後,他會自動刷新。

是不是很簡單。

注意到CloudFront 已經啟用HTTP/3 了我的源站還在HTTP/1.1。

更新一下nginx好了。

全局配置文件nginx.conf 中 http 段落加入:

http {
	http2 on;
	http3 on;

vhosts.conf 中server 段落加入:

###bbken.org at 443####
        server {
        server_name bbken.org www.bbken.org;
        listen 443 ssl ;
	listen 443 quic reuseport;
	listen [::]:443 ssl;
	listen [::]:443 quic reuseport;  # IPv6
	add_header Alt-Svc 'h3=":443"; ma=86400';

重啟一下nginx -s reload。

檢查一下udp 監聽:

ss -ulnp | grep :443
UNCONN 0      0                              0.0.0.0:443       0.0.0.0:*    users:(("nginx",pid=281315,fd=28),("nginx",pid=281314,fd=28),("nginx",pid=240301,fd=28))
UNCONN 0      0                              0.0.0.0:443       0.0.0.0:*    users:(("nginx",pid=281315,fd=26),("nginx",pid=281314,fd=26),("nginx",pid=240301,fd=26))
UNCONN 0      0                                 [::]:443          [::]:*    users:(("nginx",pid=281315,fd=27),("nginx",pid=281314,fd=27),("nginx",pid=240301,fd=27))
UNCONN 0      0                                 [::]:443          [::]:*    users:(("nginx",pid=281315,fd=29),("nginx",pid=281314,fd=29),("nginx",pid=240301,fd=29))

測試一下header:

HTTP/2 200 
server: nginx/1.28.0
date: Fri, 05 Dec 2025 08:03:12 GMT
content-type: text/html; charset=UTF-8
vary: Accept-Encoding
vary: Accept-Encoding, Cookie
cache-control: max-age=3, must-revalidate

因為curl 不支持HTTP/3 下次再測。