Я откладывал некоторые неприятные обновления зависимостей больше года и, наконец, добрался до них на прошлой неделе, после нескольких фальстартов типа npm
. В конце концов, я сдался и перестроился с нуля, используя инструмент gatsby new
, а затем портировал свои настройки. После того, как я доработал некоторые активы, я заметил, что изменения, похоже, не были применены на forcepush.tech. Это было не очень загадочно, поскольку с тех пор я установил кэш Cloudflare перед своим сайтом.
В качестве краткой справки, мой стек сайтов выглядит следующим образом:
- Сайт Gatsby в репозитории GitHub…
- развертывается на Netlify при push в
main
… - кэшируется на Cloudflare edge с примерно 2-часовым TTL (который, похоже, не настраивается на бесплатном уровне).
Я нашел кнопку на приборной панели Cloudflare, чтобы очистить кэш сайта вручную, поэтому я воспользовался ею, и мои изменения стали видимыми. Я также обратил внимание на подсказку API, которая позволяет автоматизировать эту процедуру каждый раз, когда мой сайт собирается на Netlify. Конечная точка API довольно проста: просто
POST https://api.cloudflare.com/client/v4/zones/:identifier/purge_cache
с заголовками "Authorization: Bearer TOKEN"
, "Content-Type"
, и никаких данных в запросе не требуется. Оставалась только одна проблема — как автоматически запустить этот режим.
Цели проектирования
- Кэшировать последнее развертывание сразу после его завершения
- Автоматически очищать и восстанавливать кэш.
- Не очищать без необходимости, поскольку я предполагаю, что восстановление и распространение неизмененного кэша на границу Cloudflare требует больших вычислительных затрат.
- Не платите ни за что
Опции
В конфигурации развертывания Netlify у меня было несколько вариантов запуска конечной точки Cloudflare: я мог либо добавить curl
к моей команде сборки, либо использовать веб-крюк после развертывания, который отправляет POST на произвольный URL.
Подход с curl
показался мне немного мудрёным, поскольку он должен быть &&
связан с и без того длинной командой rm -rf public/jidicula-resume && npm run build
, и мне не понравилась идея использования однострочного текстового поля для многокомандного скрипта. Это также не совсем очистка кэша после развертывания, поскольку эта конфигурация команды сборки запускается в начале развертывания, а не в конце.
Попадание в конечную точку очистки до начала развертывания открывает два варианта отказа, которые сводят на нет преимущества автоматизации очистки:
- кэш очищается до развертывания, а развертывание впоследствии не удается: кэш перестраивается с использованием последнего успешного развертывания -> перестройка кэша без изменений
- кэш очищается перед развертыванием, и развертывание проходит успешно, но медленно: кэш перестраивается до завершения развертывания, поэтому он все еще содержит устаревшее содержимое последнего развертывания -> перестройка кэша без изменений
Эти причины оставили меня с подходом POST-хука после развертывания… К сожалению, Netlify не позволяет использовать пользовательские заголовки в своем POST-хуке и может аутентифицироваться только через JWS, поэтому он не может соответствовать спецификации API Cloudflare purge_cache
.
Чтобы решить эту проблему несоответствия POST, я поискал и нашел запись в блоге Брайана Ли об использовании бессерверной облачной функции в качестве промежуточного программного обеспечения: после получения POST на конечную точку триггера отправить POST-запрос в Cloudflare на конечную точку cache-purge. Конечно, я решил сделать это на Go, а не на Python: он занимает еще меньше памяти, имеет лучшую производительность без какой-либо настройки, а быстрая компиляция позволит быстрее собрать функцию.
Реализация
Вот моя реализация на Go:
package purger
import (
"fmt "
"io"
"log"
"net/http"
"strings"
"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)
func init() {
functions.HTTP("PurgeCache", purgeCache)
}
// httpError logs the error and returns an HTTP error message and code.
func httpError(w http.ResponseWriter, err error, msg string, errorCode int) {
errorMsg := fmt.Sprintf("%s: %v", msg, err)
log.Printf("%s", errorMsg)
http.Error(w, errorMsg, errorCode)
}
func purgeCache(w http.ResponseWriter, r *http.Request) {
log.Printf("Received %s from %v", r.Method, r.RemoteAddr)
if r.Method == "POST" {
body, err := io.ReadAll(r.Body)
if err != nil {
httpError(w, err, "error reading POST body", http.StatusInternalServerError)
return
}
log.Printf("Request body: %s", body)
}
// Send POST request to Cloudflare
client := &http.Client{}
data := `{"purge_everything":true}`
req, err := http.NewRequest("POST",
"https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache",
strings.NewReader(data))
if err != nil {
httpError(w, err, "error creating new Request", http.StatusInternalServerError)
return
}
req.Header.Add("Authorization", "Bearer CLOUDFLARE-API-TOKEN")
req.Header.Add("Content-Type", "application/json")
cloudflareResp, err := client.Do(req)
if err != nil {
httpError(w, err, "error sending POST request", http.StatusInternalServerError)
return
}
defer cloudflareResp.Body.Close()
// Pass cloudflare response to caller
cloudflareRespBody, err := io.ReadAll(cloudflareResp.Body)
if err != nil {
httpError(w, err, "error reading Cloudflare response", http.StatusInternalServerError)
return
}
if cloudflareResp.StatusCode != http.StatusOK {
msg := fmt.Sprintf("error non-200 status: %s", cloudflareRespBody)
httpError(w, nil, msg, http.StatusInternalServerError)
return
}
log.Printf("Cloudflare response: %s", cloudflareRespBody)
_, err = w.Write(cloudflareRespBody)
if err != nil {
httpError(w, err, "error sending response to client", http.StatusInternalServerError)
return
}
}
- Здесь требуется некоторый набор шаблонов Google Cloud Functions:
init()
и вызовfunctions.HTTP
, который регистрирует вызываемую функцию. - Вызываемая функция, похоже, требует получения
http.ResponseWriter
и*http.Request
в своих параметрах (я возился, пытаясь понять, можно ли их опустить, поскольку документация по облачным функциям второго поколения не совсем полная).- Как обычно в Go, я использую
*http.Client
иhttp.NewRequest()
для добавления пользовательских заголовков в HTTP-запрос — шаги заключаются в созданииRequest
с помощьюNewRequest
и передаче егоclient
для отправки.
- Как обычно в Go, я использую
- Я использую все возможные встроенные коды ошибок для обработки различных видов сбоев и информирования вызывающей стороны о том, что что-то пошло не так. Также для удобства я разложил обычные вызовы
log.Printf()
&http.Error()
в функциюhttpError()
. - Независимо от ответа Cloudflare, функция пересылает его обратно вызывающей стороне.
- И в качестве заключительной полировки я регистрирую, откуда пришел запрос, и его тело, если это POST (который должен приходить только от Netlify). Облачные функции Google могут быть запущены по HTTP с помощью любого из запросов
POST
,PUT
,GET
,DELETE
илиOPTIONS
.
Для конфигурации Google Cloud Function я использовал Cloud Functions второго поколения, так как он использует Artifact Registry для хранения образа функции, а Artifact Registry имеет бесплатный уровень (Cloud Functions первого поколения используют Container Registry, который стоит денег). Дополнительные конфигурации:
- Выделенная память: 128 Мб (наименьший возможный вариант)
- Таймаут: 60 секунд (по умолчанию)
- Автомасштабирование: От 0 экземпляров минимум до 1 экземпляра максимум (для очистки кэша нужен только 1 запущенный экземпляр).
- Регион: us-east4 (это в Северной Вирджинии, там же, где находится основной регион AWS us-east — Netlify размещается на AWS, так что, надеюсь, это уменьшает некоторую задержку).
Обратная сторона
Основным недостатком этого подхода является то, что он полагается на безопасность через неизвестность — конечная точка триггера должна быть открыта для Netlify, чтобы она могла к ней подключиться. В худшем случае конечная точка может быть завалена вредоносными запросами, но конечный результат, вероятно, будет нормальным — я ограничил функцию до 1 вызова за раз, так что это может действовать как дроссель. Если вредоносные запросы станут проблемой, я добавлю в преамбулу функции раннюю проверку происхождения или содержимого запроса, или я даже могу разработать какую-нибудь проверку, по которой хук Netlify сможет аутентифицироваться с помощью JWS.
Резюме
В целом, я вполне доволен этим решением по очистке кэша — оно отвечает всем моим целям и работает быстро:
- ✅ Кэширование последнего развертывания сразу после его завершения
- ✅ Очистка и восстановление кэша автоматически
- ✅ Не очищайте без необходимости, потому что я предполагаю, что восстановление и распространение неизмененного кэша на границу Cloudflare требует больших вычислительных затрат.
- ✅ Не платите ни за что.
Если у вас есть вопросы или комментарии, напишите мне по адресу johanan+blog@forcepush.tech, найдите меня в Twitter @jidiculous или оставьте комментарий ниже.
Вы нашли этот пост полезным? Купите мне напиток или станьте моим спонсором здесь!