Multisite e oggetti Cloudflare R2

Utilizzo di Cloudflare R2 su un solo sito in un’installazione multisite di Discourse

Se gestisci un’installazione multisite di Discourse e desideri che solo uno dei tuoi siti utilizzi Cloudflare R2 (per caricamenti, backup e, facoltativamente, asset statici), la guida standard Configurazione di un provider di object storage compatibile con S3 per i caricamenti non copre del tutto le peculiarità dell’ambiente multisite. Questo post illustra cosa accade effettivamente, cosa rimane a livello di singolo sito rispetto a ciò che è a livello di cluster, e alcuni punti critici che ho incontrato lungo il percorso e che hanno richiesto tempo reale per essere risolti.

Il concetto fondamentale da comprendere: GlobalSetting vs SiteSetting

Tutto in questa configurazione si riduce a una distinzione:

  • Le variabili di ambiente di app.yml (DISCOURSE_*) diventano GlobalSetting — lette una sola volta all’avvio del contenitore, dall’ambiente di processo, e condivise da ogni sito nel cluster. RAILS_DB non ha alcun effetto su di esse.
  • I campi dell’interfaccia di amministrazione sono normali SiteSetting — memorizzati per singolo sito nel rispettivo database, effettivamente limitati a quel singolo sito.

Se esiste una GlobalSetting per una determinata voce, essa sovrascrive silenziosamente e nasconde il campo corrispondente SiteSetting nell’interfaccia di amministrazione. Questo significa: qualsiasi cosa inserisci in app.yml si applica a ogni sito, senza eccezioni e senza workaround tramite RAILS_DB.

Parte 1 — Caricamenti e backup (effettivamente per singolo sito, semplice)

Questa parte funziona esattamente come ci si aspetterebbe. enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket e i campi delle credenziali sono tutti normali impostazioni di sito. Configurali solo tramite Amministrazione → Impostazioni → cerca “S3”, accedendo al sito specifico che desideri su R2, e lascia app.yml intatto. Gli altri siti nel cluster continueranno a memorizzare localmente.

Valori di esempio per R2:

Enable S3 uploads = true
Enable direct S3 uploads = true
S3 access key ID / secret access key = <il tuo token R2>
S3 region = auto
S3 upload bucket = <nome del bucket>
S3 endpoint = https://<account-id>.r2.cloudflarestorage.com
S3 CDN URL = https://uploads.yourdomain.com
S3 use ACLs = false   (R2 utilizza autorizzazioni a livello di bucket, non ACL di oggetto)
S3 backup bucket = <nome del bucket di backup>
Backup location = S3

Imposta la policy CORS del tuo bucket direttamente nella dashboard di Cloudflare (R2 non ha bisogno del task rake CORS di Discourse):

[
  {
    "AllowedOrigins": ["https://your-site.tld"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

Parte 2 — Migrazione dei caricamenti locali esistenti

rake uploads:migrate_to_s3 legge la configurazione S3 solo dalle variabili di ambiente — non ha alcun fallback alle impostazioni di sito, indipendentemente da ciò che è configurato nell’interfaccia di amministrazione. Questa è una lacuna reale nel task, non un errore di configurazione. Passa le credenziali inline per un’esecuzione una tantum invece di modificare app.yml:

./launcher enter app

RAILS_DB=default \
DISCOURSE_S3_REGION=auto \
DISCOURSE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com \
DISCOURSE_S3_BUCKET=<nome del bucket> \
DISCOURSE_S3_ACCESS_KEY_ID=<chiave> \
DISCOURSE_S3_SECRET_ACCESS_KEY=<segreto> \
rake uploads:migrate_to_s3

Queste variabili di ambiente esistono solo per quel processo shell — nulla viene persistito una volta che esci.

Errore di checksum con versioni più recenti di AWS SDK

Se riscontri:

Aws::S3::Errors::InvalidRequest: You can only specify one non-default checksum at a time.

questa è un’incapacità nota tra le versioni recenti di aws-sdk-core (che impostano per impostazione predefinita l’invio di un checksum CRC32) e R2. Correggi aggiungendo due ulteriori variabili di ambiente allo stesso comando:

export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required

Record “non migrati” residui dopo un’esecuzione per lo più riuscita

Se il task termina con un messaggio come 1 of 1291 uploads are not migrated, non allarmarti — tutto il resto è già stato migrato e gli URL del DB sono già stati riscritti. Trova il ritardatario in rails c:

base_url = File.join(SiteSetting.Upload.s3_base_url, "original/")
Upload.by_users.where("url NOT LIKE '#{base_url}%'").pluck(:id, :url, :original_filename)

Nel mio caso si trattava di un record Upload errato (un zip di log di backup) che utilizzava l’URL in stile endpoint grezzo di R2 invece del formato URL CDN che il controllo si aspetta — un falso positivo, non un fallimento reale. Correggi l’URL o elimina il record se non si tratta di contenuto significativo.

Parte 3 — Asset statici (JS/CSS) — la parte che è effettivamente a livello di cluster

È qui che l’obiettivo di “solo per un sito” incontra un muro invalicabile. Gli asset compilati sono condivisi su tutto il cluster multisite — esiste un unico bundle JS/CSS compilato, non uno per sito. Se gli asset vengono serviti da R2 o localmente è deciso una sola volta, all’avvio di Rails, tramite GlobalSetting.use_s3? — non esiste un override per singolo sito per questo.

Se desideri che gli asset siano scaricati su R2, devi inserire i dettagli di connessione (non la flag di abilitazione del caricamento) in app.yml:

env:
  DISCOURSE_USE_S3: true
  DISCOURSE_S3_REGION: auto
  DISCOURSE_S3_ENDPOINT: https://<account-id>.r2.cloudflarestorage.com
  DISCOURSE_S3_ACCESS_KEY_ID: "xxx"
  DISCOURSE_S3_SECRET_ACCESS_KEY: "xxx"
  DISCOURSE_S3_BUCKET: <nome del bucket>
  DISCOURSE_S3_CDN_URL: https://uploads.yourdomain.com
  AWS_REQUEST_CHECKSUM_CALCULATION: when_required
  AWS_RESPONSE_CHECKSUM_VALIDATION: when_required

hooks:
  after_assets_precompile:
    - exec:
        cd: $home
        cmd:
          - sudo -E -H -u discourse bundle exec rake s3:upload_assets
          - sudo -E -H -u discourse bundle exec rake s3:expire_missing_assets

Note:

  • Non impostare DISCOURSE_CDN_URL. Solo DISCOURSE_S3_CDN_URL. Impostare entrambi, con il tuo dominio principale proxyato tramite Cloudflare, causa loop di reindirizzamento secondo l’avvertimento stesso della guida S3 principale.
  • Usa bundle exec rake, non bundle rake (errore di battitura facile) — e usa sudo -E -H -u discourse (il -H imposta correttamente HOME per l’utente discourse; senza di esso Bundler ricade in una directory temporanea a ogni esecuzione).
  • Lo scaricamento degli asset interessa entrambi i siti. I tag <script>/<link> del tuo secondo sito inizieranno anche a risolvere verso l’URL CDN di R2, poiché è lo stesso bundle compilato. Assicurati che AllowedOrigins della CORS del tuo bucket includa il dominio di ogni sito.
  • Questo non forza i caricamenti effettivi del tuo secondo sito su S3 — enable_s3_uploads rimane un’impostazione genuinamente per singolo sito, indipendente dalla GlobalSetting di servizio degli asset. Verifica con SiteSetting.Upload.enable_s3_uploads in rails c per il DB di quel sito dopo la ricompilazione.

USE_DB_S3_CONFIG — cosa fa effettivamente (e cosa no)

Vedrai USE_DB_S3_CONFIG=true menzionato in alcune configurazioni della community (ad es. il chart di Bitnami) come modo per far sì che s3:upload_assets legga le credenziali dalle impostazioni di sito invece che dalle variabili di ambiente. Funziona per il task di caricamento stesso — ma non inverte GlobalSetting.use_s3?, che è la flag che controlla effettivamente se gli URL degli asset vengono riscritti verso il CDN al momento del rendering. Quindi puoi spingere con successo i file su R2 con USE_DB_S3_CONFIG e vedere comunque il tuo sito servire gli asset localmente, perché il controllo del rendering della pagina non vede mai “S3 è abilitato”. Se desideri che gli asset vengano effettivamente serviti da R2, hai bisogno del vero DISCOURSE_USE_S3: true + variabili di ambiente di connessione in app.yml, non solo del workaround di configurazione DB.

Parte 4 — Cosa non sarà comunque su R2, e perché

Anche con l’hook funzionante, s3:upload_assets carica solo ciò che si trova in Rails.application.assets.load_path — il manifesto Sprockets di Rails. Tre categorie vengono generate fuori da quella pipeline e non appaiono mai in questo elenco, quindi rimangono su disco locale indipendentemente da tutto:

  • CSS dei temi — compilati dinamicamente per tema/scheme di colori da Stylesheet::Manager di Discourse, non tramite Sprockets.
  • theme-javascripts — JS compilato per tema da ThemeJavascriptCompiler.
  • extra-locale / file JS di localizzazione — generati da JsLocaleHelper.

Questo non è un problema di configurazione — questi non sono mai stati asset Sprockets in primo luogo, quindi non esiste una variabile di ambiente che li includa. Nella pratica questo significa: bundle JS core Ember/vendor → scaricato con successo su R2; CSS/JS dei temi e localizzazioni → rimangono locali, serviti direttamente dall’app. Questo è uno stato normale e funzionante, non uno rotto.

Riepilogo: cosa mettere effettivamente dove

Cosa Dove Ambito
enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket Interfaccia di amministrazione, per sito Per singolo sito
Credenziali + DISCOURSE_S3_REGION/ENDPOINT/BUCKET/CDN_URL + DISCOURSE_USE_S3 app.yml, solo se desideri lo scaricamento CDN degli asset A livello di cluster (inevitabile)
Hook after_assets_precompile app.yml A livello di cluster
AWS_REQUEST_CHECKSUM_CALCULATION / AWS_RESPONSE_CHECKSUM_VALIDATION app.yml A livello di cluster (flag di comportamento SDK innocuo)
CORS sul bucket Dashboard Cloudflare Deve includere il dominio di ogni sito se gli asset sono condivisi

Se non hai bisogno dello scaricamento CDN degli asset, salta completamente la Parte 3 — puoi eseguire una configurazione R2 completamente funzionante, genuinamente per singolo sito (solo caricamenti + backup) senza mai toccare app.yml.

1 Mi Piace