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.

A grans trets

Dividiré l'article en les segúents parts:

  1. Per què voldria jo un Object Storage?

  2. Abans de començar, què és CORS i CSP?

  3. Crear un Spaces Object Storage a DigitalOcean

  4. Afegir suport pel nostre Spaces Object Storage a la nostra instància Mastodon

  5. Moure les dades actuals al nostre Spaces Object Storage

  6. Afegir permís per a un CDN extern al reverse proxy

  7. Reiniciar el servei

  8. Redirigir el tràfic local cap al CDN

  9. Cloenda

  10. 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.

1. Per què voldria jo un Object Storage?

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.

2. Abans de començar, què és CORS i CSP?

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.

3. Crear un Spaces Object Storage a DigitalOcean

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.

3.1. Crear un nou 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.

  1. Sel.leccionem Spaces al menú lateral dins de Manage.

  2. Clickem al botó Create Spaces Bucket. Se'ns obrirà un formulari molt simple per introduir la info bàsica

  3. 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.

  4. Marquem la casella per Content Delivery Network (CDN).

  5. 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.

  6. Escullim a quin projecte s'adjunta aquest bucket, en el cas que tinguem més d'un projecte amb DigitalOcean.

  7. Clickem al botó Create a Spaces Bucket.

3.2. Asignar un joc de claus al 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.

  1. Sel.leccionem API al menú lateral fora de Manage.

  2. Sel.leccionem Space Keys al menú superior de la pàgina.

  3. 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.

  4. 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.

  5. 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!!

3.3. Configurar el nou bucket

Ara apliquem algunes configuracions al bucket, per assegurar-nos que el seu funcionament i com apareix al món és tal i com necessitem.

  1. Sel.leccionem Spaces al menú lateral dins de Manage.

  2. Sel.leccionem el nostre bucket media. S'obrirà una pàgina mstrant el contingut del bucket (buit a hores d'ara).

  3. Clickem a la pestanya Settings. Apareixeran les opcions del bucket.

3.3.1. File Listing

  1. Ens assegurem que File Listing és Restricted.

3.3.2. CDN

  1. Per a CDN (Content Delivery Network), clickem al botó de la dreta Change > Edit CDN Settings. S'obrirà un quadre allà mateix.

  2. Marquem Enable CDN

  3. 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.

  4. 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

  5. Sel.lecciona Create a new subdomain i introdueix el nom del subdomini a crear, cdn en el meu cas.

  6. Introdueix un nom per al certificat. Només s'accepten lletres, números, barres i punts.

  7. 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.

3.3.3. CORS

Aquesta configuració és obligatòria per CDNs en dominis diferents al domini de l'aplicació, tal com he explicat més amunt.

  1. 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.

  2. Introduir la URL (incloent el protocol https) del host que farà ús del CDN, per exemple https://el-meu-mastodon.com

  3. Marqueu quina és l'acció que volem permetre. Jo les he marcat totes, ja que serà el Mastodon el qui farà totes les accions.

  4. Clickeu el botó Save CORS Configuration. Una nova línia apareixerà en el quadre de la secció CORS.

4. Afegir suport pel nostre Spaces Object Storage a la nostra instància Mastodon

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.

  1. SSH a la nostra instància Mastodon:

    ssh xavi@corellia
  2. Mou-te al directori de la instància:

    cd ~/mastodon
  3. Edita arxiu de variables d'entorn:

    nano .env.production
  4. 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
  5. 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!

Descripció dels paràmetres

  • 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!

5. Moure les dades actuals al nostre Spaces Object Storage

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.

5.1. Instal.lar 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.

  1. SSH a la nostra instància Mastodon:

    ssh xavi@corellia
  2. Instala s3cmd:

    sudo apt-get install s3cmd

5.2. Configurar 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.

  1. 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:

  1. Creem un arxiu de test:

    echo "test" > tmp.txt
  2. 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 🥳

5.3. Pujar els arxius al bucket

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.

  1. SSH a la nostra instància Mastodon:

    ssh xavi@corellia
  2. Mou-te al directori de la instància

    cd ~/mastodon
  3. 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...

6. Afegir permís per a un CDN extern al reverse proxy

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:

  1. SSH al host del reverse proxy:

    ssh xavi@dagobah
  2. Editar l'arxiu de configuració del virtual host de la instància mastodon:

    sudo nano /etc/apache2/sites-available/012-mastodon.conf
  3. 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 *"
  4. Guarda (ctrl + o) i surt (ctrl + x)

  5. Reiniciar l'Apache:

    sudo service apache2 restart

Descripció dels paràmetres

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"

7. Reiniciar el servei

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:

  1. SSH a la nostra instància Mastodon:

    ssh xavi@corellia
  2. Mou-te al directori de la instància

    cd ~/mastodon
  3. Reinicia el servei

    docker-compose up -d

7.1. Nota per instàncies amb prou moviment

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:")

8. Redirigir el tràfic local cap al CDN

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.

  1. SSH al host del reverse proxy:

    ssh xavi@dagobah
  2. Editar l'arxiu de configuració del virtual host de la instància mastodon:

    sudo nano /etc/apache2/sites-available/012-mastodon.conf
  3. 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]
  4. Guarda (ctrl + o) i surt (ctrl + x)

  5. Reiniciar l'Apache:

    sudo service apache2 restart

9. Cloenda

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!

10. Referències

Previous Post Next Post