Запуск очистки кэша Cloudflare с помощью крючков Netlify после развертывания и функции Google Cloud в Go

Я откладывал некоторые неприятные обновления зависимостей больше года и, наконец, добрался до них на прошлой неделе, после нескольких фальстартов типа 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 для отправки.
  • Я использую все возможные встроенные коды ошибок для обработки различных видов сбоев и информирования вызывающей стороны о том, что что-то пошло не так. Также для удобства я разложил обычные вызовы 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 или оставьте комментарий ниже.

Вы нашли этот пост полезным? Купите мне напиток или станьте моим спонсором здесь!

Оцените статью
devanswers.ru
Добавить комментарий