DigitalOcean ofereix Spaces, un Object Storage compatible amb S3 d'Amazon a un preu fixe i més raonable. Intentant reduir la càrrega de la meva instància Mastodon que tinc a la Raspberry Pi 4, l'he configurat per que usi Spaces per als arxius media d'usuari i cache. De pas he hagut de sol.lucionar algun problema de CSP, però he quedat ben satisfet amb el resultat.
Dividiré l'article en les segúents parts:
Per què voldria jo un Object Storage?
Abans de començar, què és CORS i CSP?
Crear un Spaces Object Storage a DigitalOcean
Afegir suport pel nostre Spaces Object Storage a la nostra instància Mastodon
Moure les dades actuals al nostre Spaces Object Storage
Afegir permís per a un CDN extern al reverse proxy
Reiniciar el servei
Redirigir el tràfic local cap al CDN
Cloenda
Referències
A més, vull deixar clar que assumeixo el següent:
La instància Mastodon està instal.lada en una Raspberry Pi 4 sota Docker. Això és important per que algunes accions varien sensiblement (reiniciar la instància per llegir l'arxiu de configuració, no hi ha usuari mastodon
a la màquina, ...)
El host es diu corellia
i em conecto via hostname perque faig servir un servidor DNS local, tal com vaig explicar en aquest article. Molt probablement voldreu substituir el nom de host per la IP del vostre host.
L'usuari en el host és xavi
. Molt probablement voldreu canviar-ho per l'usuari que teniu al vostre host.
La instància Mastodon corre dins del directori /home/xavi/mastodon
Faig servir docker-compose
sempre que puc.
El reverse proxy el tinc corrent sobre Apache en una altra Raspberry Pi tal com vaig explicar en aquest article, amb hostname dagobah
, mateix usuari xavi
.
Servir una aplicació web des de casa té les seves desavantatges, com per exemple que part de l'ample de banda de la conexió a internet de casa es fa servir per a la web.
Servir una aplicació web des d'una Raspberry Pi també implica haver de servir les seves imatges, arxius de cache, ...
Imagina la mateixa web, amb les mateixes imatges, havent de ser servits una vegada i una altra a tots i cadascun dels clients que ho sol.liciten, per la mateixa petita conexió a internet i la mateixa petita màquina.
I si poguéssim redirigir tots els arxius d'imatges, vídeos i cache a un CDN extern, i deixar només el pes de l'aplicació real a la Raspberry?
Un Object Storage és un "disc a un servidor extern"" on es guarden "objectes" en base clau / valor, on la clau és el nom i el valor és el contingut. No se li diu "disc dur al núvol" perque no és un Filesystem, és una API a un servidor. El fet que nosaltres ho veiem amb directoris és una fantasia, tot el directori/directori/arxiu.jpg
és simplement un texte que serà la clau, i el contingut serà el valor, i és responsabilitat del client representar-ho com un disc... o no.
Això fa que tinguem proveidors de serveis d'Object Storage relativament assequibles, tots basant-se en el mateix principi, més o menys tots compatibles amb l'API de l'S3 d'Amazon, que ha esdevingut l'estàndar.
Total, amb una mínima inversió (ve a ser bastant barat) podem lliurar-nos de la càrrega de servir el media des de casa.
CORS són les sigles en anglès de Cross-Origin Resource Sharing, i és un mecanisme per permetre que una pàgina web diferent a la nostra faci ús de recursos que hi tenim.
CSP són les sigles en anglès de Content Security Policy, i és un standard introduït per prevenir atacs de cross-scripting, clickjacking i altres atacs d'injecció de codi a les pàgines web.
Per mirar d'explicar-ho fàcil, posem que tenim una instància Mastodon a el-meu-mastodon.com
. Per defecte servim tot el contingut estàtic (Imatges, Javascript, CSS, ...) des del mateix domini. Els arxius Javascript són codi que es podria manipular per fer coses lletges a l'ordinador de l'usuari. Si un cracker es posa enmig de la comunicació podria alterar-los, o podria modificar la pàgina web per a que els Javascripts a usar es servissin manipulats des d'un altre servidor. Aquest és un atac de cross-scripting, molt fàcil de fer en una extensió de navegador, per exemple, i són els més comuns.
El CSP és part d'un conjunt de polítiques implementades en els navegadors que s'asseguren que el contingut es serveix des d'on s'espera i de la forma que s'espera, obligant a definir el seu origen i bloquejant qualsevol altre.
El CORS ve a ser quelcom similar però des del punt de vista del contingut estàtic, controlant qui el demana i permetent o no servir-lo.
Ambdós estratègies són rellevants en aquest article ja que volem servir media des d'una altra màquina, un CDN. El més fàcil és fer que aquest CDN es munti com a subdomini de la pàgina, per exemple cdn.el-meu-mastodon.com
, però també podem muntar aquest CDN a un altre domini, per exemple cdn.coses-del-xavi.com
i autoritzar la nostra instància i i el CDN per que parlin entre ells. Aquesta segona opció ens permetria tenir tot un conjunt d'estàtics centralitzat en una sola màquina i usar-los a diferents instàncies, reduint el cost total.
En el meu cas particular, pagant un sol bucket i configurant-lo com a extern a dues aplicacions puc servir contingut a ambdues d'una sola tirada, reduint la factura 😉
Aquest article és util per les dues estratègies. Si configurem el CDN com a subdomini de l'aplicació principal podem estalviar-nos les seccions que expliquen la configuració de CSP i CORS. Si el configurem com a un domini extern hem de posar especial atenció a aquestes seccions més avall.
Parteixo de la base que ja tenim una conta a DigitalOcean. Un cop logats al panell de control, hem de fer 3 accions principalment: crear el bucket, assignar-li unes claus d'accés i configurar el now bucket.
Crearem el bucket que allotjarà els arxius a servir. No té més secret que crear-lo a una regió específica i adjuntar-lo a un projecte que tinguem a DigitalOcean.
Sel.leccionem Spaces al menú lateral dins de Manage.
Clickem al botó Create Spaces Bucket. Se'ns obrirà un formulari molt simple per introduir la info bàsica
Sel.leccionem una regió del datacenter des de la que es serviran els arxius. Recomano sel.leccionar la més propera a on es trobin els teus usuaris potencials. Per a Catalunya, jo he sel.leccionat Frankfurt, però Amsterdam és igualment vàlid.
Marquem la casella per Content Delivery Network (CDN).
Escullim un nom per al bucket. Idealment hauria de definir què hi conté, com ara media o statics. Es recomana usar només minúscules i evitar barres i punts. En aquest article li diré media
.
Escullim a quin projecte s'adjunta aquest bucket, en el cas que tinguem més d'un projecte amb DigitalOcean.
Clickem al botó Create a Spaces Bucket.
Això és necessari per a que l'aplicació pugui "logar-se" al Spaces i pugui accedir al contingut del bucket, com a mesura de seguretat.
Sel.leccionem API al menú lateral fora de Manage.
Sel.leccionem Space Keys al menú superior de la pàgina.
Clickem al botó Generate New Key. S'obrirà una nova fila a la taula inferior amb un espai per introduir el nom de la clau.
Introduim el nom de la clau. Com que les claus són gratis, aconsello crear una clau per aplicació que l'usarà, de forma que si vàries aplicacions usaran el mateix bucket, tinguem una clau diferent per aplicació, per poder anular-ne una individualment. En el meu cas, jo utilitzo el nom de domini de l'aplicació com a nom de la clau.
Clickem sobre la icona ☑️. Procedirà a generar la clau i en uns segons mostrarà la Key i la Secret. Copiem ambdós ARA MATEIX a un lloc segur, aquest és l'únic moment que veurem la Secret, quan sortim de la secció la perdrem!!
Ara apliquem algunes configuracions al bucket, per assegurar-nos que el seu funcionament i com apareix al món és tal i com necessitem.
Sel.leccionem Spaces al menú lateral dins de Manage.
Sel.leccionem el nostre bucket media
. S'obrirà una pàgina mstrant el contingut del bucket (buit a hores d'ara).
Clickem a la pestanya Settings. Apareixeran les opcions del bucket.
Per a CDN (Content Delivery Network), clickem al botó de la dreta Change > Edit CDN Settings. S'obrirà un quadre allà mateix.
Marquem Enable CDN
Clickem sobre l'enllaç Add a new subdomain certificate. S'obrirà un pop-up des d'on configurem el SSL del domini. Usarem Let's Encrypt.
Sel.lecciona el domini al que afegirem el CDN. Recomano usar el mateix que el de l'aplicació o bé sel.lecciones un de diferent havent de treballar les configuracions CORS i CSP com he explicat més amunt i ensenyo més avall. En l'exemple d'aquest article ho farem en un de diferent: coses-del-xavi.com
Sel.lecciona Create a new subdomain i introdueix el nom del subdomini a crear, cdn
en el meu cas.
Introdueix un nom per al certificat. Només s'accepten lletres, números, barres i punts.
Clickem sobre Save. Procedirà a generar el subdomini i a registrar un certificat per aquest.
Nota: Per a que aquests passos funcionin DigitalOcean ha de ser qui administri el domini per tal que pugui crear una entrada nova al DNS amb el subdomini.
Aquesta configuració és obligatòria per CDNs en dominis diferents al domini de l'aplicació, tal com he explicat més amunt.
Per a CORS Configurations, clickem el botó de la dreta Add. S'obrirà un pop-up on introduir un domini que farà ús del CDN.
Introduir la URL (incloent el protocol https
) del host que farà ús del CDN, per exemple https://el-meu-mastodon.com
Marqueu quina és l'acció que volem permetre. Jo les he marcat totes, ja que serà el Mastodon el qui farà totes les accions.
Clickeu el botó Save CORS Configuration. Una nova línia apareixerà en el quadre de la secció CORS.
Aquesta hauria de ser la part fàcil. Aquí editarem l'arxiu de variables d'entorn de la nostra instància Mastodon i hi introduirem els paràmetres per a conectar al nostre Spaces Object Storage de DigitalOcean.
Cal tenir present que no hi ha suport directe per a Spaces Object Storage de DigitalOcean. El que farem és tirar de la compatibilitat d'aquest amb el S3 Object Storage d'Amazon, que si està suportat a Mastodon.
SSH a la nostra instància Mastodon:
ssh xavi@corellia
Mou-te al directori de la instància:
cd ~/mastodon
Edita arxiu de variables d'entorn:
nano .env.production
Afegeix els següents paràmetres al final de l'arxiu:
# Media a Spaces de DigitalOcean
S3_ENABLED=true
S3_PROTOCOL=https
S3_BUCKET=media
S3_REGION=fra1
S3_HOSTNAME=cdn.coses-del-xavi.com
S3_ENDPOINT=https://fra1.digitaloceanspaces.com
S3_ALIAS_HOST=cdn.coses-del-xavi.com
AWS_ACCESS_KEY_ID=ABCDEFGHI12345
AWS_SECRET_ACCESS_KEY=abcdefghijk123456789lmnopqrst987654321vwxyz
Guarda (ctrl
+ o
) i surt (ctrl
+ x
)
ℹ️Nota: No reiniciem encara el servei, ja que la instància fa referència a tot un conjunt d'arxius que encara no estan pujats al Spaces!
S3_ENABLED
: Activar el suport S3
S3_PROTOCOL
: https
significa que es conecta encriptat, que és necessari en el nostre cas
S3_BUCKET
: Nom del bucket que hem creat, media
en el nostre cas.
S3_REGION
: Nom de la regió on hem creat el bucket, Frankfurt en el meu cas (fra1
).
S3_HOSTNAME
: Nom del CDN que serveix els arxius. Hem dit més amunt que serà un subdomini al domini extern de l'aplicació: cdn.coses-del-xavi.com
. Si hem muntat el CDN com a subdomini del domini de l'aplicació, el posarem aquí: cdn.el-meu-mastodon.com
.
S3_ENDPOINT
: URL de l'endpoint de DigitalOcean, sense referència al nostre bucket. Per a Frankfurt és https://fra1.digitaloceanspaces.com
, incloent el protocol!
S3_ALIAS_HOST
: Repetirem aquí el valor de S3_HOSTNAME
AWS_ACCESS_KEY
: La clau que hem configurat per a accedir al nostre Space
AWS_SECRET_ACCESS_KEY
: La Secret que ens ha retornat DigitalOcean al configurar la clau. Atenció, si no l'hem apuntat (com he dir més amunt) l'hauren de regenerar, no hi ha forma de recuperar una Secret!
Bé, ara toca moure el contingut actual de la nostra instància que tenim en local cap al nostre bucket de DigitalOcean. Aquest procés pot arribar a trigar una bona estona, depenent de la quantitat d'arxius que tinguem a hores d'ara.
A més, val a dir que molts d'ells poden ser arxius prou grans, així que és millor instal.lar una aplicació client a la nostra Raspberry Pi i configurar-la per que es conecti al bucket, i copiar els arxius via terminal.
s3cmd
L'aplicació client per interactuar amb el bucket està també dissenyada per S3 d'Amazon i tirarem de la compatibilitat dels Spaces de DigitalOcean amb S3.
SSH a la nostra instància Mastodon:
ssh xavi@corellia
Instala s3cmd
:
sudo apt-get install s3cmd
s3cmd
Un cop instal.lada l'aplicació hem de configurar-la. Aquesta ve amb un configurador, i ja va bé perque aquest generarà un arxiu de configuració per defecte a ~/.s3cfg
que és prou dens però que el configurador només ens pregunta els paràmetres imprescindibles.
Executar el configurador
s3cmd --configure
El configurador ens preguntarà:
Access Key
: La clau que hem configurat per a accedir al nostre Space, el mateix que AWS_ACCESS_KEY
més amunt.
Secret Key
: La Secret que ens ha retornat DigitalOcean al configurar la clau, el mateix que AWS_SECRET_ACCESS_KEY
més amunt.
Default Region [US]
: Nom de la regió on hem creat el bucket, el mateix que S3_REGION
més amunt.
S3 Endpoint [s3.amazonaws.com]
: Endpoint, sense protocol ni bucket: fra1.digitaloceanspaces.com
DNS-style bucket+hostname:port template for accessing a bucket [%(bucket)s.s3.amazonaws.com]
: Plantilla que usarà per muntar l'endpoint: %(bucket).fra1.digitaloceanspaces.com
Encryption password
: ho deixarem en blanc. Prem Enter.
Path to GPG program [/usr/bin/gpg]
: ho deixarem per defecte. Prem Enter.
Use HTTPS protocol [Yes]
: ho deixarem per defecte. Prem Enter.
HTTP Proxy server name
: ho deixarem en blanc. Prem Enter.
A continuació mostrarà un resum dels paràmetres i ens preguntarà si volem provar la conexió:
Test access with supplied credentials? [Y/n]
: Clar que si, Prem Enter.Si tot va bé hauria de dir:
Please wait, attempting to list all buckets...
Success. Your access key and secret key worked fine :-)
Llavors pregunta is volem guardar la configuració:
Save settings? [y/N]
: Si que volem, Prem Y
i Enter.Ara anem a provar que tot funciona:
Creem un arxiu de test:
echo "test" > tmp.txt
Pujem l'arxiu al bucket:
s3cmd --acl-public put tmp.txt s3://media/tmp.txt
Hauria de sortir un texte per pantalla com aquest:
upload: 'tmp.txt' -> 's3://media/tmp.txt' [1 of 1]
5 of 5 100% in 0s 58.42 B/s done
Public URL of the object is: http://fra1.digitaloceanspaces.com/media/tmp.txt
Si és així, tot funciona 🥳
Ara si, anem a pujar els arxius. Els arxius que ens interessen són els arxius que estan dins de public/system
, que són:
accounts
: Imatges d'avatar i de headers dels usuaris
cache
: Arxius generats per la instància per millorar el rendiment
media_attachments
: Arxius adjunts als posts
site_uploads
: Arxius que els usuaris pujen al servidor.
Com que tots ells viuen al mateix directori, podem copiar-los fàcilment amb una comanda.
SSH a la nostra instància Mastodon:
ssh xavi@corellia
Mou-te al directori de la instància
cd ~/mastodon
Copiar els arxius:
s3cmd --acl-public sync --add-header="Cache-Control:public, max-age=315576000, immutable" public/system/ s3://media
Després d'uns instants "penjat", on està recopilant les dades a copiar, començarà a mostrar per pantalla línia a línia els arxius que va pujant. Això pot trigar una bona estona...
Si hem configurat el CDN com a subdomini d'un domini diferent a l'aplicació, ens queda encara una cosa a fer: modificar l'entrada del reverse proxy per tal que afegeixi uns headers a les peticions per tal d'acceptar arxius que venen de servidors externs.
En el meu cas, el reverse proxy el tinc en una Raspberry Pi diferent, en un Apache on per cada aplicació tinc un virtual host diferent. Així doncs, en el meu cas haig de fer:
SSH al host del reverse proxy:
ssh xavi@dagobah
Editar l'arxiu de configuració del virtual host de la instància mastodon:
sudo nano /etc/apache2/sites-available/012-mastodon.conf
Afegir la següent línia dins del virtual host que escolta el port 443:
Header always set Content-Security-Policy "default-src 'self'; script-src *; style-src *; font-src *;img-src * data:; media-src *; connect-src *"
Guarda (ctrl
+ o
) i surt (ctrl
+ x
)
Reiniciar l'Apache:
sudo service apache2 restart
El que estem fent aquí és definir el Content-Security-Policy de forma que accepti els origens de dades conforme al que li estem passant. self
significa el propi domini, així que acceptarà només arxius que vinguin del mateix domini i rebutjarà tots els demés. *
significa que acceptarà arxius que vinguin de qualsevol domini. És una línia que funcionarà a tothom, però és la més insegura. El que ens cal és substituir tots els caràcters *
per l'origen de les dades expliícitament, com explico a continuació:
default-src 'self'
: Per defecte tots els origens han de ser el propi domini
script-src 'self'
: Els scripts (javascripts) han de venir del propi domini
style-src 'self'
: Els estils (CSS) han de venir del propi domini
font-src 'self'
: Les fonts han de venir del propi domini
img-src 'self' cdn.coses-del-xavi.com data:
: Les imatges de tipus data:
han de venir del CDN especificat. Noteu que després de data
hi han dos punts :
.
media-src 'self' cdn.coses-del-xavi.com
: El continguit de media ha de venir del CDN especificat.
connect-src 'self' cdn.coses-del-xavi.com
: El contingut que es carrega via AJAX o Websocket.
Així que la línia quedaria de la següent forma:
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self';img-src 'self' cdn.coses-del-xavi.com data:; media-src 'self' cdn.coses-del-xavi.com; connect-src 'self' cdn.coses-del-xavi.com"
Ara si, ho tenim tot copiat i configurat, és el moment de reiniciar el servei, per tal que es carregui l'arxiu de variables d'entorn i la instància Mastodon comenci a usar el nostre bucket a Spaces:
SSH a la nostra instància Mastodon:
ssh xavi@corellia
Mou-te al directori de la instància
cd ~/mastodon
Reinicia el servei
docker-compose up -d
Durant el temps en que s'han començat a pujar els arxius al bucket i el moment en que la instància s'ha reiniciat i per tant comença a fer servir el bucket, és molt possible que s'hagin creat nous arxius locals que no són presents al bucket, i que a la pàgina donaran error i no es mostraran.
Un cop la instància ja està funcionant, podriem tirar la següent comanda des del directori mastodon
a la nostra Raspberry per afegir els arxius que hi falten al bucket:
find -type f | cut -d"/" -f 2- | xargs -P 6 -I {} s3cmd --acl-public sync --add-header="Cache-Control:public, max-age=315576000, immutable" {} s3://media/$(echo {} | sed "s:public/system/\(.*\):\1:")
Un cop la instància ja està funcionant podriem dir que la feina des del costat servidor ja està acabada. Però, hem pensat en tots els clients que ja ens han visitat, cachejat, i que esperen rebre les imatges des de la ubicació anterior? Hauriem de redirigir-los al CDN i notificar-los que des d'ara es serviràn des d'allà.
Per això crearem una regla nova al reverse proxy per a que tot el contingut que es demani al directori public/system/
es retorni en un header 301 (redirecció permanent) cap al CDN, així els clients demanaran la propera vegada el contingut directament al CDN.
SSH al host del reverse proxy:
ssh xavi@dagobah
Editar l'arxiu de configuració del virtual host de la instància mastodon:
sudo nano /etc/apache2/sites-available/012-mastodon.conf
Busquem el bloc de paràmetres que comença amb la línia RewriteEngine On
dins del virtual host que escolta el port 443 (n'hem de tenir una segur, ja que ja estem redirigint el tràfic cap a la Raspberry que allotja la instància Mastodon), i afegim la següent línia al final del bloc:
RewriteRule ^/system(.*) https://cdn.coses-del-xavi.com$1 [L,R=301,NE]
Guarda (ctrl
+ o
) i surt (ctrl
+ x
)
Reiniciar l'Apache:
sudo service apache2 restart
En la meva experiència, des de que les imatges es serveixen des del CDN noto la instància més lleugera, més ràpida, i la càrrega de la Raspberri és també menor.
Trobo que Mastodon hauria de donar suport als Spaces de DigitalOcean directament, i no tirar de compatibilitat amb S3, ja que a l'hora de configurar el servei tot és ben confús.
Una de les coses que no m'han agradat és que no hi ha forma de definir un "directori" inicial al bucket, així que els directoris de que hem pujat de public/system/
han d'existir si o si a l'arrel del bucket. En canvi, Pixelfed té un paràmetre DO_SPACES_ROOT
que permet organitzar-ho tot dins d'un directori, de forma que el bucket es pot compartir més fàcilment amb altres aplicacions sense tenir sensació de desordre.
Espero que hagi quedat tot entès i clar. No dubtis en contactar-me per a qualsevol pregunta o aclaració.
Salut!