Usare un bucket di S3 come repository Maven privato

Un problema tipico nella fase in cui si cerca di spacchettare un monolite in un ecosistema di microservizi specializzati è la gestione delle parti di codice comuni: nel mio caso, ad esempio, mi sono ritrovato ad avere cose come la configurazione di Datadog (sempre solo cuori per Datadog ❤️), o quella dell’SMTP per inviare le mail, copiate e incollate in una decina di progetti diversi.

Non credo ci sia bisogno che stia a dilungarmi sul motivo per cui questo è il male: penso basti dire che alla seconda volta che ho dovuto perdere un pomeriggio per riportare una modifica su ogni progetto ho deciso di trovare una soluzione più pratica.

Iniziamo col dire che non è un problema solo mio e che la mia situazione non è necessariamente la migliore per tutti i casi: diverse organizzazioni, più grandi e più piccole della mia, hanno affrontato la cosa in modi diversi, dal monorepo di Google all’installazione (e manutenzione) in-house di applicazioni non banali come Artifactory o Nexus.

La mia soluzione, invece, è molto più semplice e terra terra, ma fa assolutamente al caso mio: ok, voglio estrarre tutte le classi comuni a tutte le applicazioni in una libreria da includere come dipendenza, ma dove la metto?

Una opzione che avevo considerato era dipendere direttamente da un repository Bitbucket, ma l’ho scartata perché mi pareva complicato (col senno di poi, non lo è) da gestire dentro un build.gradle e per dipendere un po’ meno da Bitbucket, che ha incident un po’ troppo spesso per i miei gusti.

La scelta definitiva, quindi, come avrete intuito dal titolo, è stata usare un bucket S3 come repository, e devo dire che è stato sorprendentemente facile.

La parte legata strettamente a S3 è piuttosto straightforward: si tratta, fondamentalmente, di creare un bucket nuovo, ovviamente privato, e due coppie di chiavi, una con permessi di lettura che sarà condivisa tra tutti gli sviluppatori e tutte le applicazioni dell’ecosistema, e una con permessi di scrittura che sarà usata solo dalla (dalle) libreria comune in questione per pubblicarsi sul repository.

Poi, siccome non ci piace tenere le credenziali salvate nei repo, dai build.gradle le recupereremo così:

  allprojects {
    awsAccessKeyId = System.getenv("aws_access_key_id") ?: findProperty("aws_access_key_id") ?: ""
    awsSecretAccessKey = System.getenv("aws_secret_access_key") ?: findProperty("aws_secret_access_key") ?: ""
  }

Questo, oltre a essere meglio che avere le credenziali hardcoded nel build.gradle, ha anche il vantaggio che permette di mettere la coppia chiave-secret nel gradle.properties globale e dimenticarsene forever and ever, senza dover copiarle in ogni progetto che è un po’ il razionale di tutto ciò.

Fatto questo, dobbiamo dire a gradle che esiste il nostro repository, e gradle, fortunatamente, supporta proprio i bucket S3, tra le altre cose, per cui sarà sufficiente questo:

  repositories {
        mavenCentral()

        maven {
            url = "s3://<il nome del vostro bucket>"
            credentials(AwsCredentials) {
                accessKey = awsAccessKeyId
                secretKey = awsSecretAccessKey
            }
        }
    }

Ovviamente mavenCentral lo teniamo come prioritario, ma in seconda battuta diciamo a gradle che le cose che non trova lì, come la nostra libreria comune, le cerca su quest’altro nuovo repository.

A questo punto possiamo includere la nostra nuova libreria come dipendenza e iniziare a spostarci quello che vogliamo, comprese anche delle altre dipendenze: nel mio caso, tutti i progetti dipendono da io.micrometer:micrometer-registry-datadog e non volevo dover copiare la dipendenza in ognuno, per cui ho tagliato la riga e l’ho incollata nel build.gradle della mia libreria.

Facile? Sì. Funziona? No.

Non funziona perché lo scope che si usa di solito per le dipendenze in Gradle, cioè compile (o implementation se volete essere compliant con Gradle 7 e non prendervi un warning), non rende la dipendenza transitiva, cioè non fa sì che venga esportata anche come dipendenza per i progetti che da questa dipendono.

Lo scope giusto, e questo è un “gotcha” che mi ha richiesto un paio d’ore di mal di testa, è api: se specifico la dipendenza così, tutte le applicazioni che dipendono dalla mia libreria “vedono” anche io.micrometer:micrometer-registry-datadog.

“Sì ok”, mi direte ora, “ma come la pubblichi la libreria su S3?”

Anche questa è una cosa sorprendentemente facile: c’è un plugin di Gradle endorsed, che si chiama per l’appunto maven-publish, che fa esattamente questa cosa, aggiungendo ai comandi possibili un pratico ./gradlew publish che fa esattamente quello che vi aspettereste.

Per agevolare cicli di sviluppo veloci, inoltre, ha anche un fantastico ./gradlew publishToMavenLocal che, a patto che abbiate messo mavenLocal() tra i repositories in cui cercare le dipendenze, vi risparmia il giro “pubblica su S3 – scarica da S3” a ogni minima modifica della libreria, utile quando siete nella fase di transizione in cui la libreria cambia di frequente.

E a proposito di cose che cambiano di frequente, pensate forse funzioni tutto così facile? Ovviamente no, perché Gradle, che è furbo, non aggiorna tutte le dipendenze a ogni build, neanche se sono marcate come SNAPSHOT, ma le tiene in cache per un po’, dove “un po’” è per fortuna sufficientemente configurabile.

Come si fa? Si fa così: innanzitutto la dipendenza va di chiarata come changing, cioè

api("it.tuaazienda:tualibreria:1.0-SNAPSHOT") { changing = true }

E poi bisogna dire a Gradle come gestire le dipendenze changing, per semplicità sempre dentro allprojects:

allprojects {
    configurations.all {
        resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
    }
}
Fatto questo, a ogni build (o a ogni reimport di build.gradle, se usate IntelliJ) avrete l’ultima versione della libreria pubblicata su S3, e ovviamente anche i sorgenti, con la possibilità quindi di mettere breakpoints all’interno del codice della libreria.

Et voila, la prossima volta che devo cambiare l’indirizzo da cui mandiamo le mail transazionali potrò cambiarlo in un posto solo anziché ottantasette.