All Articles ↓
4 mesi fa

CUBA: Prepararsi alla Produzione

"Funzionava sulla mia macchina locale!"
Al giorno d'oggi sembra un meme, ma il problema "ambiente di sviluppo vs ambiente di produzione" esiste ancora. Come sviluppatori, dovreste sempre tenere a mente che prima o poi la vostra applicazione inizierà a lavorare nell'ambiente di produzione. In questo articolo, parleremo di alcune peculiarità specifiche di CUBA che vi aiuteranno ad evitare problemi quando la vostra applicazione andrà in produzione.

Linee guida per lo Sviluppo

Prediligere i Servizi

Quasi ogni applicazione CUBA implementa alcuni algoritmi di logica di business. La raccomandazione migliore in questo caso è quella di implementare tutta la logica di business nei servizi CUBA. Tutte le altre classi: controller di schermate, application listeners, ecc. dovrebbero delegare l'esecuzione della logica di business ai servizi. Questo approccio presenta i seguenti vantaggi:

  1. Ci sarà una sola implementazione della logica di business in un unico luogo
  2. Potete chiamare questa logica di business da diversi contesti ed esporla come servizio REST.

Ricordate che la logica di business include condizioni, cicli, ecc. Ciò significa che le invocazioni di servizio dovrebbero idealmente consistere di una sola riga. Per esempio, supponiamo di avere il seguente codice in un controller di schermata:

Item item = itemService.findItem(itemDate);
if (item.isOld()) {
   itemService.doPlanA(item);
} else {
   itemService.doPlanB(item);
}

Se vedete del codice come questo, considerate di spostarlo dal controller all’ itemService come metodo separato processOldItem(Date date) perché ha tutta l'aria di essere una parte della logica di business della vostra applicazione.

Poiché le schermate e le API possono essere sviluppati da team diversi, mantenere la logica di business in un unico posto vi aiuterà ad evitare incongruenze nel comportamento dell'applicazione in produzione.

Essere Stateless

Quando si sviluppa un'applicazione web, ricordate che sarà utilizzata da più utenti. Nella pratica, ciò significa che un certo codice può essere eseguito da più thread contemporaneamente. Quasi tutti i componenti dell'applicazione: servizi, beans così come i gestori di eventi possono potenzialmente essere eseguiti in multithreading. La prassi migliore in questo caso è quella di mantenere i componenti privi di stato. Ciò significa che non si dovrebbero introdurre membri di classe variabili condivisi. Utilizzate variabili locali e tenete le informazioni specifiche della sessione nello store dell'applicazione che non è condiviso tra più utenti. Ad esempio, è possibile mantenere una piccola quantità di dati serializzabili nella sessione utente.

Se avete bisogno di condividere alcuni dati, utilizzate il database o un archivio in-memory dedicato e condiviso come Redis.

Usare i Log

A volte qualcosa va storto in produzione. E quando succede, è abbastanza difficile capire esattamente cosa abbia causato il problema, perché non è possibile eseguire il debug dell'applicazione installata in produzione. Per semplificare il vostro lavoro, dei vostri colleghi e del team di supporto, e per aiutarvi a capire il problema ed essere in grado di riprodurlo, dovreste sempre aggiungere il logging all'applicazione.

Inoltre, il logging svolge un ruolo di monitoraggio passivo. Dopo il riavvio, l'aggiornamento o la riconfigurazione dell'applicazione, un amministratore di solito guarda i log per assicurarsi che tutto sia stato avviato con successo.

E il logging può aiutare a risolvere problemi che possono verificarsi non nell'applicazione, ma nei servizi con cui l'applicazione è integrata. Ad esempio, per capire perché un gateway di pagamento rifiuti alcune transazioni, potrebbe essere necessario registrare tutti i dati e poi utilizzarli durante i colloqui con il team di supporto del gateway.

CUBA utilizza un pacchetto collaudato della libreria slf4j come facade e implementazione di logback. Basta iniettare la funzione di logging nel codice della classe e si è pronti per usarlo.

@Inject
private Logger log;

Poi basta invocare questo servizio nel proprio codice:

log.info("Transaction for the customer {} has succeeded at {}", customer, transaction.getDate());

Ricordate che i messaggi di log dovrebbero essere comprensibili e contenere informazioni sufficienti per capire cosa è successo nell'applicazione. Potete trovare molti altri consigli sul logging nelle applicazioni Java nella serie di articoli "Clean code, clean logs". Inoltre, si consiglia di dare un'occhiata all'articolo "9 Logging Sins".

Inoltre, in CUBA abbiamo i log delle statistiche di performance, che ci consentono di verificare come l'applicazione consumi le risorse di un server. Potrebbe tornare utile se il servizio di assistenza clienti iniziasse a ricevere lamentele circa la lentezza dell'applicazione. Con questo log in mano, sarà possibile individuare più velocemente il collo di bottiglia.

Gestire le Eccezioni

Le eccezioni sono molto importanti perché forniscono informazioni preziose quando qualcosa va storto nella vostra applicazione. Pertanto, regola numero uno: mai ignorare le eccezioni. Usate il metodo log.error(), create un messaggio significativo, aggiungete il contesto e lo stack trace. Questo messaggio sarà l'unica informazione che utilizzerete per identificare ciò che è successo.

Se avete una code convention, aggiungete una sezione con le regole per la gestione degli errori.

Consideriamo un esempio: caricare l'immagine del profilo utente nell'applicazione. Questa immagine del profilo verrà salvata nel servizio di archiviazione file utilizzando le API di caricamento file di CUBA.

Questo è il modo in cui NON si deve gestire un'eccezione:

        try {
            fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); 
        } catch (Exception e) {}

Se si verifica un errore, nessuno lo saprà e gli utenti saranno sorpresi quando non vedranno la foto del loro profilo.

Questo approccio è leggermente migliore, ma tutt'altro che ideale:

        try {
            fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); 
        } catch (FileStorageException e) {
            log.error (e.getMessage)
        }

Ci sarà un messaggio di errore nei log e cattureremo solo particolari classi di eccezione. Ma non ci saranno informazioni sul contesto: qual era il nome del file, chi ha cercato di caricarlo. Inoltre, non ci sarà alcuna stack trace, quindi sarà abbastanza difficile stabilire dove si è verificata l'eccezione. E un'altra cosa - l'utente non sarà avvisato del problema.

Questo invece potrebbe essere un buon approccio:

        try {
            fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); 
        } catch (FileStorageException e) {
            throw new RuntimeException("Error saving file to FileStorage", e);
        }

Conosciamo l'errore, non perdiamo l'eccezione originale, aggiungiamo un messaggio significativo. Il metodo chiamante verrà notificato dell'eccezione. Potremmo aggiungere il nome dell'utente corrente e, probabilmente, il nome del file al messaggio per aggiungere un po' più di informazioni di contesto.

Nelle applicazioni CUBA, a causa della loro natura distribuita, si potrebbero avere diverse regole di gestione delle eccezioni per i moduli core e web. C'è una sezione apposita nella documentazione relativa alla gestione delle eccezioni. Si prega di leggerla prima di implementare la vostra policy.

Configurazione in base all’Ambiente

Quando si sviluppa un'applicazione, è opportuno isolare le parti del codice specifiche per ogni ambiente e quindi utilizzare l'attivazione delle funzioni e i profili per attivare o disattivare tali parti a seconda dell'ambiente.

Utilizzare le implementazioni di Servizio appropriate

Qualsiasi servizio in CUBA è composto da due parti: un'interfaccia (API di servizio) e la sua implementazione. A volte, l'implementazione può dipendere dall'ambiente di deploy. Come esempio, useremo il servizio di archiviazione file.

In CUBA, è possibile utilizzare un file storage per salvare i file che sono stati inviati all'applicazione, e poi utilizzarli nei vostri servizi. L'implementazione predefinita utilizza il file system locale sul server per conservare i file.

Ma quando si distribuisce l'applicazione sul server di produzione, questa implementazione potrebbe non funzionare bene per gli ambienti cloud o per il deployment in cluster.

Per abilitare implementazioni specifiche per l'ambiente, CUBA supporta profili di runtime che consentono di utilizzare un servizio specifico a seconda del parametro di avvio o della variabile d'ambiente.

In questo caso, se decidiamo di utilizzare l'implementazione di Amazon S3 per il salvataggio dei file in produzione, è possibile specificare il bean nel modo seguente:

<beans profile="prod">
   <bean name="cuba_FileStorage" class="com.haulmont.addon.cubaaws.s3.AmazonS3FileStorage"/>
</beans>

E l'implementazione in S3 sarà automaticamente abilitata quando si imposta la proprietà:

spring.profiles.active=prod

Quindi, quando sviluppate un'applicazione CUBA, cercate di identificare servizi specifici per l'ambiente e abilitate una corretta implementazione per ogni ambiente. Cercate di non scrivere codice come questo:

If (“prod”.equals(getEnvironment())) {
   executeMethodA();
} else {
   executeMethodB();
}

Provate a implementare un servizio myService che abbia un metodo executeMethod() e due implementazioni, quindi configuratelo usando i profili. A questo punto il vostro codice sarà simile a questo:

myService.executeMethod();

Che è più chiaro, più semplice e più facile da mantenere.

Esternalizzare le Impostazioni

Se possibile, estrarre le impostazioni dell'applicazione nei file delle proprietà. Se un parametro può cambiare in futuro (anche se la probabilità è bassa), esternalizzarlo sempre. Evitate di memorizzare URL di connessione, nomi di host, ecc. come semplici stringhe nel codice dell'applicazione e non copiateli semplicemente in giro. Il costo per mantenere aggiornato un valore hardcoded nel codice è molto più alto. L'indirizzo del server di posta, la dimensione delle anteprime delle foto dell'utente, il numero di tentativi se non c'è una connessione di rete - tutti questi sono esempi di proprietà che è necessario esternalizzare. Utilizzate le interfacce di configurazione e iniettatele nelle vostre classi per ottenere i valori di configurazione.

Utilizzate i profili di runtime per mantenere le proprietà specifiche dell'ambiente in file separati.

Ad esempio, pensate di utilizzare un gateway di pagamento nella vostra applicazione. Ovviamente, non dovreste utilizzare transazioni reali per testare le funzionalità durante lo sviluppo. Per questo motivo decidete di utilizzare un gateway stub nel vostro ambiente locale, testate l'API sul lato gateway nell'ambiente di pre-produzione e infine usate un gateway reale in produzione. E ovviamente gli indirizzi dei gateway sono diversi per i vari ambienti.

Non scrivete il vostro codice in questo modo:

If (“prod”.equals(getEnvironment())) {
      gatewayHost = “gateway.payments.com”;
} else if (“test”.equals(getEnvironment())) {
      gatewayHost = “testgw.payments.com”;
} else {
      gatewayHost = “localhost”;
}
connectToPaymentsGateway(gatewayHost);

Create invece tre file di proprietà: dev-app.properties, test-app.properties e prod-app.properties e definite tre diversi valori per la proprietà payment.gateway.host.name in questi file.

Dopodiché, definite un'interfaccia di configurazione:

@Source(type = SourceType.DATABASE)
public interface PaymentGwConfig extends Config {

    @Property("payment.gateway.host.name")
    String getPaymentGwHost();
}

Quindi iniettate l'interfaccia e utilizzatela nel vostro codice:

@Inject
PaymentGwConfig gwConfig;

//service code

connectToPaymentsGateway(gwConfig.getPaymentGwHost());

Questo codice è più semplice e non dipende dagli ambienti, tutte le impostazioni sono in file di proprietà e non occorre cercarle all'interno del codice se qualcosa dovesse cambiare.

Gestire i Timeout di Rete

Considerare sempre le invocazioni di servizio via rete come inaffidabili. Molte librerie per le richieste ai servizi web si basano sul modello di comunicazione sincrono. Ciò significa che quando si invoca un servizio web dal thread di esecuzione principale, l'applicazione si sospende fino a quando non si riceve la risposta.

Anche se si richiama un servizio web in un thread separato, c'è la possibilità che questo thread non riprenda mai l'esecuzione a causa di un timeout di rete.

Esistono due tipi di timeout:

  1. Timeout di connessione
  2. Timeout di lettura

Nell'applicazione, questi tipi di timeout devono essere gestiti separatamente. Usiamo lo stesso esempio del paragrafo precedente: un gateway di pagamento. In questo caso il timeout di lettura potrebbe essere notevolmente più lungo di quello di connessione. Le transazioni bancarie possono essere elaborate per un tempo piuttosto lungo, decine di secondi, fino a diversi minuti. Ma la connessione dovrebbe essere veloce, quindi conviene impostare il timeout di connessione a 10 secondi, per esempio.

I valori di timeout sono buoni candidati per essere spostati nei file di proprietà. E impostarli sempre per tutti i servizi che interagiscono in rete. Qui di seguito un esempio di definizione di un bean di servizio:

<bean id="paymentGwConfig" class="com.global.api.serviceConfigs.GatewayConfig">
    <property name="connectionTimeout" value="${xxx.connectionTimeoutMillis}"/>
    <property name="readTimeout" value="${xxx.readTimeoutMillis}"/>
</bean>

Nel vostro codice, dovreste includere una sezione speciale che si occuperà dei timeout.

Linee guida per il Database

Un database è il cuore di quasi tutte le applicazioni. E quando si tratta di installare e aggiornare la produzione, è molto importante non danneggiare il database. Oltre a questo, il carico di lavoro del database sulla workstation di uno sviluppatore è ovviamente diverso dal server di produzione. Questo è il motivo per cui si potrebbe voler applicare alcune pratiche descritte qui di seguito.

Generare Scripts specifici per l’Ambiente

In CUBA, generiamo script SQL sia per la creazione che per l'aggiornamento del database applicativo. E dopo la prima creazione del database sul server di produzione, non appena il modello cambia, il framework CUBA genera gli script di aggiornamento.

C'è una sezione apposita nella documentazione relativa all'aggiornamento del database in produzione, si prega di leggerla prima di andare in produzione per la prima volta.

Consiglio finale: effettuate sempre il backup del database prima di un aggiornamento. In questo modo risparmierete un sacco di tempo e di fastidi in caso di problemi.

Tenere conto della Multitenancy

Se il vostro progetto sarà un'applicazione multi-tenant, vi preghiamo di tenerne conto fin dall'inizio del progetto.

CUBA supporta il multitenancy tramite un add-on, introducendo alcune modifiche al modello di dati dell'applicazione e alla logica di interrogazione del database. Ad esempio, a tutte le entità specifiche per un Tenant viene aggiunta una colonna separata con il tenantId. A questo punto, tutte le query sono implicitamente modificate per utilizzare questa colonna. Ciò significa che si dovrebbe considerare questa colonna quando si scrivono query SQL native.

Si prega di notare che l'aggiunta di funzionalità multi-tenancy, ad un'applicazione esistente in produzione, potrebbe essere complicata a causa delle caratteristiche specifiche menzionate sopra. Per semplificare la migrazione, è consigliabile mantenere tutte le query personalizzate nello stesso livello applicativo, preferibilmente nei servizi o in un livello di accesso ai dati separato.

Considerazioni sulla Sicurezza

Quando si tratta di un'applicazione accessibile a più utenti, la sicurezza gioca un ruolo importante. Per evitare fughe di dati, accessi non autorizzati, ecc. è necessario considerare seriamente la sicurezza. Qui sotto potete trovare un paio di principi che vi aiuteranno a migliorare l'applicazione in termini di sicurezza.

Codice Sicuro

La sicurezza inizia con il codice in grado di prevenire i problemi. Qui potete trovare un ottimo riferimento, realizzato da Oracle, riguardo la scrittura di codice sicuro. Qui sotto potete trovare alcune (forse ovvie) raccomandazioni estratte da questa guida.

Linea guida 3-2 / INJECT-2: Evitare SQL dinamico

È noto che le istruzioni SQL create dinamicamente, compresi gli input non attendibili, sono soggette all'iniezione di comandi. In CUBA, potrebbe essere necessario eseguire istruzioni JPQL, quindi, evitate anche il JPQL dinamico. Se è necessario aggiungere parametri alle query, usate le classi e i metodi appropriati:

       try (Transaction tx = persistence.createTransaction()) {
            // get EntityManager for the current transaction
            EntityManager em = persistence.getEntityManager();
            // create and execute Query
            Query query = em.createQuery(
                    "select sum(o.amount) from sample_Order o where o.customer.id = :customerId");
            query.setParameter("customerId", customerId);
            result = (BigDecimal) query.getFirstResult();
            // commit transaction
            tx.commit();
        }

Linea guida 5-1 / INPUT-1: Convalidare gli input
Gli input provenienti da fonti non attendibili devono essere convalidati prima dell'uso. Gli input costruiti in modo malevolo possono causare problemi, sia che provengano dagli argomenti di un metodo che da fonti esterne. Alcuni esempi sono l'overflow di valori interi e gli attacchi di attraversamento directory inserendo sequenze "../" nei nomi di file. In CUBA, oltre ai controlli nel codice, è possibile utilizzare dei validatori nella GUI.

Questi sono solo alcuni esempi di linee guida per un codice sicuro. Leggete attentamente la guida, vi aiuterà a migliorare il vostro codice in molti modi.

Mantenere i Dati Personali al Sicuro

Alcune informazioni personali dovrebbero essere protette perché è un requisito di legge. In Europa abbiamo il GDPR, per le applicazioni mediche negli USA ci sono i requisiti HIPAA, ecc. Quindi prendeteli in considerazione quando progettate la vostra applicazione.

CUBA consente di impostare vari permessi e di limitare l'accesso ai dati utilizzando ruoli e gruppi di accesso. In questi ultimi, potete definire vari vincoli che vi permetteranno di impedire l'accesso non autorizzato ai dati personali.

Ma fornire l'accesso è solo una parte della sicurezza dei dati personali. Ci sono molti requisiti negli standard di protezione dei dati e nei requisiti specifici del settore. Vi preghiamo di dare un'occhiata a questi documenti prima di pianificare l'architettura dell'applicazione e il modello dei dati.

Alterare o Disabilitare gli Utenti e i Ruoli Predefiniti

Quando si crea un'applicazione utilizzando il framework CUBA, nel sistema vengono creati due utenti: admin e anonymous. Cambiate sempre le loro password di default nell'ambiente di produzione prima che l'applicazione venga messa a disposizione degli utenti. È possibile farlo manualmente o aggiungere uno statement SQL allo script di inizializzazione 30-....sql.

Utilizzare le raccomandazioni della documentazione CUBA che vi aiuteranno a configurare correttamente i ruoli in produzione.

Se avete una struttura organizzativa complessa, considerate la possibilità di creare amministratori locali per ogni filiale invece di diversi utenti "super-amministratori" a livello di organizzazione.

Esportare i Ruoli in Produzione

Prima del primo deploy, di solito è necessario copiare i ruoli e i gruppi di accesso dal server di sviluppo (o di staging) a quello di produzione. In CUBA, è possibile farlo utilizzando un'interfaccia utente amministrativa integrata, invece di farlo manualmente.

Per esportare ruoli e privilegi si può usare la schermata Amministrazione -> Ruoli. Dopo aver scaricato il file, è possibile caricarlo nella versione di produzione dell'applicazione.

text

Per i gruppi di accesso esiste un processo simile, ma per questo è necessario utilizzare la schermata Amministrazione -> Gruppi di accesso.

text

Configurare l’Applicazione

L'ambiente di produzione è solitamente diverso da quello di sviluppo, così come la configurazione dell'applicazione. Ciò significa che è necessario eseguire alcuni controlli aggiuntivi, per garantire che la vostra applicazione funzioni senza problemi quando si tratta di andare in produzione.

Configurare i Log

Assicurarsi di aver configurato correttamente il sottosistema di logging per la produzione: verificare che il livello di log sia impostato al livello desiderato (di solito è INFO) e che i log non vengano cancellati al riavvio dell'applicazione. Si può fare riferimento alla documentazione relativa per la corretta configurazione dei log, e per ulteriori approfondimenti sui logger.

Se si utilizza Docker, usate i volumi Docker per memorizzare i file di log al di fuori del contenitore.

Per la corretta analisi dei log, è possibile utilizzare uno strumento apposito per la raccolta, la memorizzazione e l'analisi dei log. Alcuni esempi sono ELK stack e Graylog. Si raccomanda di installare il software di logging su un server separato, per evitare impatti negativi sulle prestazioni dell'applicazione.

Esecuzione in Configurazione Cluster
Le applicazioni CUBA possono essere configurate per funzionare in cluster. Se si decide di utilizzare questa configurazione, è necessario prestare attenzione all'architettura dell'applicazione, altrimenti si potrebbe ottenere un comportamento inaspettato. Vorremmo richiamare la vostra attenzione sulle caratteristiche più utilizzate che è necessario impostare in modo specifico per l'ambiente di cluster:

Attività Pianificate
Se si desidera eseguire attività pianificate nella propria applicazione, come la generazione di report giornalieri o l'invio di email settimanali, è possibile utilizzare la corrispondente funzionalità integrata nel framework. Ma immaginate di essere un cliente che ha ricevuto tre e-mail di marketing identiche. Siete soddisfatti? Questo può accadere se la vostra attività viene eseguita su tre nodi del cluster. Per evitare questi inconvenienti, si consiglia di utilizzare il task scheduler CUBA che vi permette di creare attività singleton.

Cache Distribuita
Il caching è una funzione che può migliorare le prestazioni dell'applicazione. E a volte gli sviluppatori cercano di mettere in cache quasi tutto, perché la memoria è piuttosto a buon mercato oggigiorno. Ma quando l'applicazione viene distribuita su più server, la cache viene distribuita tra i server e dovrebbe essere sincronizzata. Il processo di sincronizzazione avviene su una connessione di rete relativamente lenta e questo può aumentare il tempo di risposta. Un consiglio: eseguire test di carico, e misurare le prestazioni prima di prendere una decisione sull'aggiunta di più cache, specialmente in un ambiente clusterizzato.

Conclusioni

La piattaforma CUBA semplifica lo sviluppo, e probabilmente finirete di sviluppare prima di quando vi aspettavate, e comincerete a pensare alla produzione. Ma il deployment non è un compito semplice, sia che si utilizzi CUBA o meno. Tuttavia, se iniziate a pianificare il processo di deploy sin dalle prime fasi di sviluppo, e seguite le semplici regole indicate in questo articolo, ci sono buone probabilità che il vostro percorso verso la produzione sia scorrevole, che richieda il minimo sforzo, e che non affrontiate problemi gravi.

Paolo Furini