Modello di app Web moderno per Java

Servizio app di Azure
Frontdoor di Azure
Cache di Azure per Redis

Questo articolo illustra come implementare il modello di app Web moderna. Il modello Di app Web moderna definisce come modernizzare le app Web nel cloud e introdurre un'architettura orientata ai servizi. Il modello di app Web moderna fornisce un'architettura prescrittiva, codice e linee guida di configurazione che si allineano ai principi di Azure Well-Architected Framework e si basa sul modello Reliable Web App.

Perché usare il modello di app Web moderna?

Il modello di app Web moderna consente di ottimizzare le aree a richiesta elevata dell'applicazione Web. Offre indicazioni dettagliate per separare queste aree, abilitando la scalabilità indipendente per l'ottimizzazione dei costi. Questo approccio consente di allocare risorse dedicate a componenti critici, migliorando le prestazioni complessive. Il disaccoppiamento dei servizi separabili può migliorare l'affidabilità impedendo rallentamenti in una parte dell'app di influire sugli altri e abilitare il controllo delle versioni dei singoli componenti dell'app in modo indipendente.

Come implementare il modello di app Web moderna

Questo articolo contiene informazioni sull'architettura, il codice e la configurazione per implementare il modello di app Web moderna. Usare i collegamenti seguenti per passare alle indicazioni necessarie:

  • Indicazioni sull'architettura: informazioni su come modularizzare i componenti delle app Web e selezionare le soluzioni PaaS (Platform as a Service) appropriate.
  • Linee guida sul codice: implementare quattro modelli di progettazione per ottimizzare i componenti disaccoppiati: Strangler Fig, Livellamento del carico basato su coda, Consumer concorrenti e Modelli di monitoraggio degli endpoint di integrità.
  • Linee guida per la configurazione: configurare l'autenticazione, l'autorizzazione, la scalabilità automatica e la containerizzazione per i componenti disaccoppiati.

Suggerimento

Logo di GitHubÈ disponibile un'implementazione di riferimento (app di esempio) del modello di app Web moderna. Rappresenta lo stato finale dell'implementazione dell'app Web moderna. Si tratta di un'app Web di livello di produzione che include tutti gli aggiornamenti di codice, architettura e configurazione descritti in questo articolo. Distribuire e usare l'implementazione di riferimento per guidare l'implementazione del modello di app Web moderna.

Linee guida per l'architettura

Il modello di app Web moderna si basa sul modello Reliable Web App. Richiede alcuni componenti dell'architettura aggiuntivi da implementare. È necessaria una coda di messaggi, una piattaforma contenitore, un servizio di archiviazione e un registro contenitori (vedere la figura 1).

Diagramma che mostra l'architettura di base del modello di app Web moderna.Figura 1. Elementi architettonici essenziali del modello di app Web moderna.

Per un obiettivo di livello di servizio (SLO) superiore, è possibile aggiungere una seconda area all'architettura dell'app Web. Configurare il servizio di bilanciamento del carico per instradare il traffico alla seconda area per supportare una configurazione attiva-attiva o attiva-passiva a seconda delle esigenze aziendali. Le due aree richiedono gli stessi servizi ad eccezione di un'area con una rete virtuale hub che si connette. Adottare una topologia di rete hub-spoke per centralizzare e condividere risorse, ad esempio un firewall di rete. Accedere al repository di contenitori tramite la rete virtuale hub. Se si dispone di macchine virtuali, aggiungere un bastion host alla rete virtuale hub per gestirli in modo sicuro (vedere la figura 2).

Diagramma che mostra l'architettura del modello di app Web moderna con la seconda area e la topologia di rete hub-spoke.Figura 2. Architettura del modello di app Web moderna con seconda area e topologia di rete hub-spoke.

Architettura disaccoppiamento

Per implementare il modello di app Web moderna, è necessario separare l'architettura dell'app Web esistente. La separazione dell'architettura comporta la suddivisione di un'applicazione monolitica in servizi più piccoli e indipendenti, ognuno responsabile di una funzionalità o di una funzionalità specifica. Questo processo comporta la valutazione dell'app Web corrente, la modifica dell'architettura e infine l'estrazione del codice dell'app Web in una piattaforma contenitore. L'obiettivo è identificare ed estrarre sistematicamente i servizi dell'applicazione che traggono vantaggio dalla separazione. Per separare l'architettura, seguire queste indicazioni:

  • Identificare i limiti del servizio Applicare principi di progettazione basati su dominio per identificare i contesti delimitati all'interno dell'applicazione monolitica. Ogni contesto delimitato rappresenta un limite logico e può essere un candidato per un servizio separato. I servizi che rappresentano funzioni aziendali distinte e hanno meno dipendenze sono buoni candidati per la disaccoppiamento.

  • Valutare i vantaggi del servizio. Concentrarsi sui servizi che traggono vantaggio dalla scalabilità indipendente. Ad esempio, una dipendenza esterna come un provider di servizi di posta elettronica in un'applicazione LINEB potrebbe richiedere un maggiore isolamento da un errore. Prendere in considerazione i servizi che subiscono aggiornamenti o modifiche frequenti. La disaccoppiamento di questi servizi consente la distribuzione indipendente e riduce il rischio di influire su altre parti dell'applicazione.

  • Valutare la fattibilità tecnica. Esaminare l'architettura corrente per identificare vincoli tecnici e dipendenze che potrebbero influire sul processo di disaccoppiamento. Pianificare la modalità di gestione e condivisione dei dati tra i servizi. I servizi disaccoppiati devono gestire i propri dati e ridurre al minimo l'accesso diretto al database attraverso i limiti del servizio.

  • Distribuire i servizi di Azure. Selezionare e distribuire i servizi di Azure necessari per supportare il servizio app Web da estrarre. Usare la sezione Selezionare la sezione dei servizi di Azure appropriata per indicazioni.

  • Separare il servizio app Web. Definire interfacce e API chiare per i servizi di app Web appena estratti per interagire con altre parti del sistema. Progettare una strategia di gestione dei dati che consente a ogni servizio di gestire i propri dati garantendo al tempo stesso coerenza e integrità. Per strategie di implementazione specifiche e modelli di progettazione da usare durante questo processo di estrazione, vedere la sezione Linee guida sul codice.

  • Usare l'archiviazione indipendente per i servizi disaccoppiati. Ogni servizio disaccoppiato deve avere archivi dati propri per semplificare il controllo delle versioni e la distribuzione. Ad esempio, l'implementazione di riferimento separa il servizio di posta elettronica dall'app Web ed elimina la necessità del servizio di accedere al database. Il servizio comunica invece lo stato di recapito della posta elettronica all'app Web tramite un messaggio di bus di servizio di Azure e l'app Web salva una nota nel relativo database.

  • Implementare pipeline di distribuzione separate per ogni servizio disaccoppiato. Le pipeline di distribuzione separate consentono l'aggiornamento di ogni servizio al proprio ritmo. Se team o organizzazioni diverse all'interno dell'azienda sono proprietari di servizi diversi, la presenza di pipeline di distribuzione separate offre a ogni team il controllo sulle proprie distribuzioni. Usare strumenti di integrazione continua e recapito continuo (CI/CD) come Jenkins, GitHub Actions o Azure Pipelines per configurare queste pipeline.

  • Rivedere i controlli di sicurezza. Assicurarsi che i controlli di sicurezza vengano aggiornati per tenere conto della nuova architettura, incluse le regole del firewall e i controlli di accesso.

Selezionare i servizi di Azure corretti

Per ogni servizio di Azure nell'architettura, vedere la guida pertinente al servizio di Azure in Well-Architected Framework. Per il modello di app Web moderna, è necessario un sistema di messaggistica per supportare la messaggistica asincrona, una piattaforma applicativa che supporta la containerizzazione e un repository di immagini del contenitore.

  • Scegliere una coda di messaggi. Una coda di messaggi è una parte importante delle architetture orientate ai servizi. Separa i mittenti e i ricevitori dei messaggi per abilitare la messaggistica asincrona. Usare le indicazioni sulla scelta di un servizio di messaggistica di Azure per scegliere un sistema di messaggistica di Azure che supporti le esigenze di progettazione. Azure include tre servizi di messaggistica: Griglia di eventi di Azure, Hub eventi di Azure e bus di servizio. Iniziare con bus di servizio come scelta predefinita e usare le altre due opzioni se bus di servizio non soddisfa le proprie esigenze.

    Service Caso d'uso
    Bus di servizio Scegliere bus di servizio per il recapito affidabile, ordinato ed eventualmente transazionale di messaggi di alto valore nelle applicazioni aziendali.
    Griglia di eventi Scegliere Griglia di eventi quando è necessario gestire in modo efficiente un numero elevato di eventi discreti. Griglia di eventi è scalabile per le applicazioni guidate dagli eventi in cui è necessario instradare molti eventi di piccole dimensioni, ad esempio le modifiche allo stato delle risorse, ai sottoscrittori in un modello di pubblicazione-sottoscrizione a bassa latenza.
    Hub eventi di Scegliere Hub eventi per l'inserimento di dati con velocità effettiva elevata elevata, ad esempio dati di telemetria, log o analisi in tempo reale. Hub eventi è ottimizzato per scenari di streaming in cui i dati in blocco devono essere inseriti ed elaborati continuamente.
  • Implementare un servizio contenitore. Per le parti dell'applicazione da inserire in contenitori, è necessaria una piattaforma applicativa che supporta i contenitori. Usare il materiale sussidiario Scegliere un servizio Azure Container per prendere decisioni. Azure include tre servizi contenitore principali: App azure Container, servizio Azure Kubernetes (AKS) e servizio app Azure. Iniziare con App contenitore come scelta predefinita e usare le altre due opzioni se App contenitore non soddisfa le proprie esigenze.

    Service Caso d'uso
    App contenitore Scegliere App contenitore se è necessaria una piattaforma serverless che ridimensiona e gestisce automaticamente i contenitori nelle applicazioni guidate dagli eventi.
    servizio Azure Kubernetes Scegliere il servizio Azure Kubernetes se è necessario un controllo dettagliato sulle configurazioni di Kubernetes e sulle funzionalità avanzate per la scalabilità, la rete e la sicurezza.
    App Web per contenitore Scegliere App Web per contenitori in servizio app per l'esperienza PaaS più semplice.
  • Implementare un repository di contenitori. Quando si usa un servizio di calcolo basato su contenitori, è necessario disporre di un repository per archiviare le immagini del contenitore. È possibile usare un registro contenitori pubblico, ad esempio Docker Hub o un registro gestito, ad esempio Registro Azure Container. Usare il materiale sussidiario Introduzione ai registri contenitori in Azure per prendere decisioni.

Linee guida per il codice

Per separare ed estrarre correttamente un servizio indipendente, è necessario aggiornare il codice dell'app Web con i modelli di progettazione seguenti: il modello Strangler Fig, il modello di livellamento del carico basato su coda, il modello Consumer concorrenti, il modello Monitoraggio endpoint integrità e il modello Di ripetizione dei tentativi.

Diagramma che mostra il ruolo dei modelli di progettazione nell'architettura del modello di app Web moderna.Figura 3. Ruolo dei modelli di progettazione.

  1. Modello Fig strangler: il modello Strangler Fig esegue la migrazione incrementale delle funzionalità da un'applicazione monolitica al servizio disaccoppiato. Implementare questo modello nell'app Web principale per eseguire gradualmente la migrazione delle funzionalità ai servizi indipendenti indirizzando il traffico in base agli endpoint.

  2. Modello di livellamento del carico basato su coda: il modello di livellamento del carico basato su coda gestisce il flusso di messaggi tra il producer e il consumer usando una coda come buffer. Implementare questo modello nella parte producer del servizio disaccoppiato per gestire il flusso dei messaggi in modo asincrono usando una coda.

  3. Modello Consumer concorrenti: il modello Consumer concorrenti consente a più istanze del servizio disaccoppiato di leggere in modo indipendente dalla stessa coda di messaggi e competere per elaborare i messaggi. Implementare questo modello nel servizio disaccoppiato per distribuire le attività tra più istanze.

  4. Modello di monitoraggio degli endpoint di integrità: il modello di monitoraggio degli endpoint di integrità espone gli endpoint per il monitoraggio dello stato e dell'integrità di diverse parti dell'app Web. (4a) Implementare questo modello nell'app Web principale. (4b) Implementarlo anche nel servizio disaccoppiato per tenere traccia dell'integrità degli endpoint.

  5. Modello di ripetizione dei tentativi: il modello di ripetizione dei tentativi gestisce gli errori temporanei ritentando le operazioni che potrebbero non riuscire in modo intermittente. (5a) Implementare questo modello in tutte le chiamate in uscita ad altri servizi di Azure nell'app Web principale, ad esempio le chiamate alla coda di messaggi e agli endpoint privati. (5b) Implementare anche questo modello nel servizio disaccoppiato per gestire gli errori temporanei nelle chiamate agli endpoint privati.

Ogni modello di progettazione offre vantaggi che si allineano a uno o più pilastri di Well-Architected Framework (vedere la tabella seguente).

Schema progettuale Percorso di implementazione Affidabilità (RE) Sicurezza (SE) Ottimizzazione costi (CO) Eccellenza operativa (OE) Efficienza delle prestazioni (PE) Supporto dei principi del framework ben progettato
Modello Fig strangler App Web principale RE:08
CO:07
CO:08
OE:06
OE:11
Schema di livellamento del carico basato sulle code Produttore di un servizio disaccoppiato RE:06
RE:07
CO:12
PE:05
Modello consumer concorrenti Servizio disaccoppiato RE:05
RE:07
CO:05
CO:07
PE:05
PE:07
Modello di monitoraggio degli endpoint di integrità App Web principale e servizio disaccoppiato RE:07
RE:10
OE:07
PE:05
Modello di ripetizione dei tentativi App Web principale e servizio disaccoppiato RE:07

Implementare il modello Strangler Fig

Usare il modello Strangler fig per eseguire gradualmente la migrazione delle funzionalità dalla codebase monolitica ai nuovi servizi indipendenti. Estrarre nuovi servizi dalla codebase monolitica esistente e modernizzare lentamente le parti critiche dell'app Web. Per implementare il modello Strangler Fig, seguire queste indicazioni:

  • Configurare un livello di routing. Nella codebase dell'app Web monolitica implementare un livello di routing che indirizza il traffico in base agli endpoint. Usare la logica di routing personalizzata in base alle esigenze per gestire regole business specifiche per indirizzare il traffico. Ad esempio, se si dispone di un /users endpoint nell'app monolitica e si è spostata tale funzionalità nel servizio disaccoppiato, il livello di routing indirizza tutte le richieste al /users nuovo servizio.

  • Gestire l'implementazione delle funzionalità. Implementare flag di funzionalità e implementazione a fasi per implementare gradualmente i servizi disaccoppiati. Il routing delle app monolitiche esistente deve controllare il numero di richieste ricevute dai servizi disaccoppiati. Iniziare con una piccola percentuale di richieste e aumentare l'utilizzo nel tempo man mano che si ottiene fiducia nella stabilità e nelle prestazioni.

    Ad esempio, l'implementazione di riferimento estrae la funzionalità di recapito della posta elettronica in un servizio autonomo, che può essere gradualmente introdotta per gestire una parte più ampia delle richieste per inviare messaggi di posta elettronica contenenti guide al supporto di Contoso. Poiché il nuovo servizio dimostra l'affidabilità e le prestazioni, alla fine può assumere l'intero set di responsabilità di posta elettronica dal monolith, completando la transizione.

  • Usare un servizio di facciata (se necessario). Un servizio di facciata è utile quando una singola richiesta deve interagire con più servizi o quando si desidera nascondere la complessità del sistema sottostante dal client. Tuttavia, se il servizio disaccoppiato non dispone di API pubbliche, potrebbe non essere necessario un servizio di facciata.

    Nella codebase dell'app Web monolitica implementare un servizio di facciata per instradare le richieste al back-end appropriato (monolith o microservizio). Nel nuovo servizio disaccoppiato assicurarsi che il nuovo servizio possa gestire le richieste in modo indipendente quando si accede tramite la facciata.

Implementare il modello di livellamento del carico basato su coda

Implementare il modello di livellamento del carico basato su coda nella parte producer del servizio disaccoppiato per gestire in modo asincrono le attività che non necessitano di risposte immediate. Questo modello migliora la velocità di risposta e la scalabilità complessive del sistema usando una coda per gestire la distribuzione del carico di lavoro. Consente al servizio disaccoppiato di elaborare le richieste a una velocità coerente. Per implementare questo modello in modo efficace, attenersi alle indicazioni seguenti:

  • Usare l'accodamento messaggi non bloccante. Verificare che il processo che invia messaggi alla coda non blocchi altri processi durante l'attesa che il servizio disaccoppiato gestisca i messaggi nella coda. Se il processo richiede il risultato dell'operazione di disaccoppiata del servizio, implementare un modo alternativo per gestire la situazione durante l'attesa del completamento dell'operazione in coda. Ad esempio, in Spring Boot è possibile usare la StreamBridge classe per pubblicare in modo asincrono i messaggi nella coda senza bloccare il thread chiamante (vedere il codice di esempio):

    private final StreamBridge streamBridge;
    
    public SupportGuideQueueSender(StreamBridge streamBridge) {
        this.streamBridge = streamBridge;
    }
    
    // Asynchronously publish a message without blocking the calling thread
    @Override
    public void send(String to, String guideUrl, Long requestId) {
        EmailRequest emailRequest = EmailRequest.newBuilder()
                .setRequestId(requestId)
                .setEmailAddress(to)
                .setUrlToManual(guideUrl)
                .build();
    
        log.info("EmailRequest: {}", emailRequest);
    
        var message = emailRequest.toByteArray();
        streamBridge.send(EMAIL_REQUEST_QUEUE, message);
    
        log.info("Message sent to the queue");
    }
    

    Questo esempio Java usa StreamBridge per inviare messaggi in modo asincrono. Questo approccio garantisce che l'applicazione principale rimanga reattiva e possa gestire contemporaneamente altre attività, mentre il servizio disaccoppiato elabora le richieste in coda a una velocità gestibile.

  • Implementare la ripetizione e la rimozione dei messaggi. Implementare un meccanismo per ritentare l'elaborazione dei messaggi in coda che non possono essere elaborati correttamente. Se gli errori vengono mantenuti, questi messaggi devono essere rimossi dalla coda. Ad esempio, bus di servizio include funzionalità predefinite per la coda di tentativi e messaggi non recapitabili.

  • Configurare l'elaborazione dei messaggi idempotenti. La logica che elabora i messaggi dalla coda deve essere idempotente per gestire i casi in cui un messaggio potrebbe essere elaborato più volte. In Spring Boot è possibile usare @StreamListener o @KafkaListener con un identificatore di messaggio univoco per impedire l'elaborazione duplicata. In alternativa, è possibile organizzare il processo aziendale per operare in un approccio funzionale con Spring Cloud Stream, in cui il consume metodo viene definito in modo da produrre lo stesso risultato quando viene eseguito ripetutamente. Leggere Spring Cloud Stream con bus di servizio per un elenco più dettagliato delle impostazioni che gestiscono il comportamento di utilizzo dei messaggi.

  • Gestire le modifiche apportate all'esperienza. L'elaborazione asincrona può portare a attività che non vengono completate immediatamente. Gli utenti devono essere informati quando l'attività è ancora in fase di elaborazione per impostare le aspettative corrette ed evitare confusione. Usare segnali visivi o messaggi per indicare che un'attività è in corso. Consentire agli utenti di ricevere notifiche al termine dell'attività, ad esempio un messaggio di posta elettronica o una notifica push.

Implementare il modello Consumer concorrenti

Implementare il modello Consumer concorrenti nel servizio disaccoppiato per gestire le attività in ingresso dalla coda di messaggi. Questo modello comporta la distribuzione di attività tra più istanze di servizi disaccoppiati. Questi servizi elaborano i messaggi dalla coda, migliorando il bilanciamento del carico e aumentando la capacità del sistema di gestire le richieste simultanee. Il modello Consumer concorrenti è efficace quando:

  • La sequenza di elaborazione dei messaggi non è fondamentale.
  • La coda rimane inalterata dai messaggi in formato non valido.
  • L'operazione di elaborazione è idempotente, ovvero può essere applicata più volte senza modificare il risultato oltre l'applicazione iniziale.

Per implementare il modello Consumer concorrenti, seguire queste raccomandazioni:

  • Gestire messaggi simultanei. Quando si ricevono messaggi da una coda, assicurarsi che il sistema venga ridimensionato in modo prevedibile configurando la concorrenza in modo che corrisponda alla progettazione del sistema. I risultati del test di carico consentono di decidere il numero appropriato di messaggi simultanei da gestire ed è possibile iniziare da uno per misurare le prestazioni del sistema.

  • Disabilitare il prelettura. Disabilitare il prelettura dei messaggi in modo che i consumer recuperino solo i messaggi quando sono pronti.

  • Usare modalità di elaborazione dei messaggi affidabili. Usare una modalità di elaborazione affidabile, ad esempio PeekLock (o il relativo equivalente), che ritenta automaticamente i messaggi che non superano l'elaborazione. Questa modalità migliora l'affidabilità rispetto ai metodi di eliminazione-first. Se un ruolo di lavoro non riesce a gestire un messaggio, un altro deve essere in grado di elaborarlo senza errori, anche se il messaggio viene elaborato più volte.

  • Implementare la gestione degli errori. Instradare messaggi non elaborati o non elaborabili a una coda separata di messaggi non recapitabili. Questa progettazione impedisce l'elaborazione ripetitiva. Ad esempio, è possibile intercettare le eccezioni durante l'elaborazione dei messaggi e spostare il messaggio problematico nella coda separata. Per bus di servizio, i messaggi vengono spostati nella coda dei messaggi non recapitabili dopo un numero specificato di tentativi di recapito o in caso di rifiuto esplicito da parte dell'applicazione.

  • Gestire i messaggi non in ordine. Progettare i consumer per elaborare i messaggi che arrivano fuori sequenza. Più consumer paralleli significa che potrebbero elaborare messaggi non in ordine.

  • Scala in base alla lunghezza della coda. I servizi che utilizzano messaggi da una coda devono essere ridimensionati automaticamente in base alla lunghezza della coda. La scalabilità automatica basata su scalabilità consente un'elaborazione efficiente dei picchi di messaggi in ingresso.

  • Usare la coda message-reply. Se il sistema richiede notifiche per l'elaborazione post-messaggio, configurare una coda di risposta o risposta dedicata. Questa configurazione divide la messaggistica operativa dai processi di notifica.

  • Usare i servizi senza stato. Prendere in considerazione l'uso di servizi senza stato per elaborare le richieste da una coda. Consente un facile ridimensionamento e un utilizzo efficiente delle risorse.

  • Configurare la registrazione. Integrare la registrazione e la gestione delle eccezioni specifiche all'interno del flusso di lavoro di elaborazione dei messaggi. Concentrarsi sull'acquisizione degli errori di serializzazione e sull'indirizzamento di questi messaggi problematici a un meccanismo di messaggi non recapitabili. Questi log forniscono informazioni utili per la risoluzione dei problemi.

Ad esempio, l'implementazione di riferimento usa il modello Consumer concorrenti in un servizio senza stato in esecuzione in App contenitore per elaborare le richieste di recapito tramite posta elettronica da una coda di bus di servizio.

Il processore registra i dettagli di elaborazione dei messaggi, agevolando la risoluzione dei problemi e il monitoraggio. Acquisisce gli errori di deserializzazione e fornisce informazioni dettagliate necessarie durante il debug del processo. Il servizio viene ridimensionato a livello di contenitore, consentendo una gestione efficiente dei picchi di messaggi in base alla lunghezza della coda (vedere il codice seguente).

@Configuration
public class EmailProcessor {

    private static final Logger log = LoggerFactory.getLogger(EmailProcessor.class);

    @Bean
    Function<byte[], byte[]> consume() {
        return message -> {

            log.info("New message received");

            try {
                EmailRequest emailRequest = EmailRequest.parseFrom(message);
                log.info("EmailRequest: {}", emailRequest);

                EmailResponse emailResponse = EmailResponse.newBuilder()
                        .setEmailAddress(emailRequest.getEmailAddress())
                        .setUrlToManual(emailRequest.getUrlToManual())
                        .setRequestId(emailRequest.getRequestId())
                        .setMessage("Email sent to " + emailRequest.getEmailAddress() + " with URL to manual " + emailRequest.getUrlToManual())
                        .setStatus(Status.SUCCESS)
                        .build();

                return emailResponse.toByteArray();

            } catch (InvalidProtocolBufferException e) {
                throw new RuntimeException("Error parsing email request message", e);
            }
        };
    }
}

Implementare il modello di monitoraggio degli endpoint di integrità

Implementare il modello di monitoraggio degli endpoint di integrità nel codice dell'app principale e nel codice del servizio disaccoppiato per tenere traccia dell'integrità degli endpoint dell'applicazione. Gli agenti di orchestrazione come il servizio Azure Kubernetes o le app contenitore possono eseguire il polling di questi endpoint per verificare l'integrità del servizio e riavviare le istanze non integre. Spring Boot offre il supporto predefinito per i controlli di integrità con Spring Boot Esponi gli endpoint di controllo dell'integrità per le dipendenze chiave, ad esempio database, broker di messaggi e sistemi di archiviazione. Per implementare il modello di monitoraggio degli endpoint di integrità, attenersi alle indicazioni seguenti:

  • Implementare i controlli di integrità. Usare Spring Boot Per implementare endpoint di controllo dell'integrità. Spring Boot Agent espone un endpoint /actuator/health che include indicatori di integrità predefiniti e controlli personalizzati per varie dipendenze. Per abilitare l'endpoint di integrità, aggiungere la spring-boot-starter-actuator dipendenza nel pom.xml file o build.gradle .

    <!-- Add Spring Boot Actuator dependency -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    

    Configurare l'endpoint di integrità in application.properties come illustrato nell'implementazione di riferimento: txt management.endpoints.web.exposure.include=metrics,health,info,retry,retryevents

  • Convalidare le dipendenze. Spring Boot Agent include indicatori di integrità per varie dipendenze, ad esempio database, broker di messaggi (RabbitMQ o Kafka) e servizi di archiviazione. Per convalidare la disponibilità dei servizi di Azure, ad esempio Archiviazione BLOB di Azure o bus di servizio, usare plug-in della community come le integrazioni di Azure Spring Apps o Micrometer, che forniscono indicatori di integrità per questi servizi. Se sono necessari controlli personalizzati, è possibile implementarli creando un bean personalizzato HealthIndicator .

    import org.springframework.boot.actuate.health.Health;
    import org.springframework.boot.actuate.health.HealthIndicator;
    import org.springframework.stereotype.Component;
    
    @Component
    public class CustomAzureServiceBusHealthIndicator implements HealthIndicator {
        @Override
        public Health health() {
            // Implement your health check logic here (e.g., ping Service Bus)
            boolean isServiceBusHealthy = checkServiceBusHealth();
            return isServiceBusHealthy ? Health.up().build() : Health.down().build();
        }
    
        private boolean checkServiceBusHealth() {
            // Implement health check logic (pinging or connecting to the service)
            return true; // Placeholder, implement actual logic
        }
    }
    
  • Configurare le risorse di Azure. Configurare la risorsa di Azure per usare gli URL di controllo integrità dell'app per confermare la disponibilità e la conformità. Ad esempio, è possibile usare Terraform per usare gli URL di controllo integrità per confermare la disponibilità e l'idoneità delle app distribuite in App contenitore. Per altre informazioni, vedere Probe di integrità in App contenitore.

Implementare il modello di ripetizione dei tentativi

Il modello Di ripetizione dei tentativi consente alle applicazioni di eseguire il ripristino da errori temporanei. Il modello Di ripetizione dei tentativi è fondamentale per il modello Reliable Web App, quindi l'app Web deve usare già il modello Di ripetizione dei tentativi. Applicare il modello Di ripetizione dei tentativi alle richieste ai sistemi di messaggistica e alle richieste rilasciate dai servizi disaccoppiati estratti dall'app Web. Per implementare il modello Di ripetizione dei tentativi, seguire queste indicazioni:

  • Configurare le opzioni di ripetizione dei tentativi. Quando si esegue l'integrazione con una coda di messaggi, assicurarsi di configurare il client responsabile delle interazioni con la coda con le impostazioni di ripetizione appropriate. Specificare i parametri, ad esempio il numero massimo di tentativi, il ritardo tra i tentativi e il ritardo massimo.

  • Usare il backoff esponenziale. Implementare la strategia di backoff esponenziale per i tentativi. Ciò significa aumentare il tempo tra ogni tentativo in modo esponenziale, che consente di ridurre il carico nel sistema durante periodi di alti tassi di errore.

  • Usare la funzionalità di ripetizione dei tentativi dell'SDK. Per i servizi con SDK specializzati, ad esempio bus di servizio o archiviazione BLOB, usare i meccanismi di ripetizione dei tentativi predefiniti. I meccanismi di ripetizione dei tentativi predefiniti sono ottimizzati per i casi d'uso tipici del servizio e possono gestire i nuovi tentativi in modo più efficace con meno configurazione necessaria nella propria parte.

  • Adottare librerie di resilienza standard per i client HTTP. Per i client HTTP, è possibile usare Resilience4* insieme a RestTemplate o WebClient di Spring per gestire i tentativi nelle comunicazioni HTTP. RestTemplate di Spring può essere sottoposto a wrapping con la logica di ripetizione dei tentativi di Resilience4j per gestire in modo efficace gli errori HTTP temporanei.

  • Gestire il blocco dei messaggi. Per i sistemi basati su messaggi, implementare strategie di gestione dei messaggi che supportano i tentativi senza perdita di dati, ad esempio usando le modalità "peek-lock" dove disponibili. Assicurarsi che i messaggi non riusciti vengano ritentati in modo efficace e spostati in una coda di messaggi non recapitabili dopo errori ripetuti.

Linee guida per la configurazione

Le sezioni seguenti forniscono indicazioni sull'implementazione degli aggiornamenti della configurazione. Ogni sezione è allineata a uno o più pilastri del framework ben progettato.

Impostazione Affidabilità (RE) Sicurezza (SE) Ottimizzazione costi (CO) Eccellenza operativa (OE) Efficienza delle prestazioni (PE) Supporto dei principi del framework ben progettato
Configurare l'autenticazione e l'autorizzazione SE:05
OE:10
Implementare la scalabilità automatica indipendente RE:06
CO:12
PE:05
Distribuire un servizio in contenitori CO:13
PE:09
PE:03

Configurare l'autenticazione e l'autorizzazione

Per configurare l'autenticazione e l'autorizzazione in tutti i nuovi servizi di Azure (identità del carico di lavoro) aggiunti all'app Web, seguire queste indicazioni:

  • Usare le identità gestite per ogni nuovo servizio. Ogni servizio indipendente deve avere una propria identità e usare le identità gestite per l'autenticazione da servizio a servizio. Le identità gestite eliminano la necessità di gestire le credenziali nel codice e ridurre il rischio di perdita di credenziali. Consente di evitare di inserire informazioni riservate come stringa di connessione nei file di codice o di configurazione.

  • Concedere privilegi minimi a ogni nuovo servizio. Assegnare solo le autorizzazioni necessarie a ogni nuova identità del servizio. Ad esempio, se un'identità deve eseguire solo il push in un registro contenitori, non concedere autorizzazioni pull. Esaminare queste autorizzazioni regolarmente e modificarle in base alle esigenze. Usare identità diverse per ruoli diversi, ad esempio la distribuzione e l'applicazione. Questo limita il potenziale danno se un'identità viene compromessa.

  • Adottare l'infrastruttura come codice (IaC). Usare strumenti IaC simili o Bicep come Terraform per definire e gestire le risorse cloud. IaC garantisce un'applicazione coerente delle configurazioni di sicurezza nelle distribuzioni e consente di controllare la versione della configurazione dell'infrastruttura.

Per configurare l'autenticazione e l'autorizzazione per gli utenti (identità utente), seguire queste indicazioni:

  • Concedere privilegi minimi agli utenti. Proprio come con i servizi, assicurarsi che agli utenti vengano concesse solo le autorizzazioni necessarie per eseguire le attività. Esaminare e modificare regolarmente queste autorizzazioni.

  • Eseguire controlli di sicurezza regolari. Esaminare e controllare regolarmente la configurazione della sicurezza. Cercare eventuali configurazioni errate o autorizzazioni non necessarie e correggerle immediatamente.

L'implementazione di riferimento usa IaC per assegnare identità gestite a servizi aggiunti e ruoli specifici a ogni identità. Definisce i ruoli e l'accesso alle autorizzazioni per la distribuzione definendo i ruoli per il push e il pull del Registro Container (vedere il codice seguente).

resource "azurerm_role_assignment" "container_app_acr_pull" {
  principal_id         = var.aca_identity_principal_id
  role_definition_name = "AcrPull"
  scope                = azurerm_container_registry.acr.id
}

resource "azurerm_user_assigned_identity" "container_registry_user_assigned_identity" {
  name                = "ContainerRegistryUserAssignedIdentity"
  resource_group_name = var.resource_group
  location            = var.location
}

resource "azurerm_role_assignment" "container_registry_user_assigned_identity_acr_pull" {
  scope                = azurerm_container_registry.acr.id
  role_definition_name = "AcrPull"
  principal_id         = azurerm_user_assigned_identity.container_registry_user_assigned_identity.principal_id
}


# For demo purposes, allow current user access to the container registry
# Note: when running as a service principal, this is also needed
resource "azurerm_role_assignment" "acr_contributor_user_role_assignement" {
  scope                = azurerm_container_registry.acr.id
  role_definition_name = "Contributor"
  principal_id         = data.azuread_client_config.current.object_id
}

Configurare la scalabilità automatica indipendente

Il modello di app Web moderna inizia a suddividere l'architettura monolitica e introduce il disaccoppiamento dei servizi. Quando si separa un'architettura di app Web, è possibile ridimensionare i servizi disaccoppiati in modo indipendente. Il ridimensionamento dei servizi di Azure per supportare un servizio app Web indipendente, anziché un'intera app Web, ottimizza i costi di ridimensionamento in base alle esigenze. Per ridimensionare automaticamente i contenitori, seguire queste indicazioni:

  • Usare i servizi senza stato. Verificare che i servizi siano senza stato. Se l'app Web contiene lo stato della sessione in-process, esternalizzarla a una cache distribuita come Redis o a un database come SQL Server.

  • Configurare le regole di scalabilità automatica. Usare le configurazioni di scalabilità automatica che forniscono il controllo più conveniente sui servizi. Per i servizi in contenitori, il ridimensionamento basato su eventi, ad esempio Kubernetes Event-Driven Autoscaler (KEDA) offre spesso un controllo granulare, consentendo di ridimensionare in base alle metriche degli eventi. Le app contenitore e il servizio Azure Kubernetes supportano KEDA. Per i servizi che non supportano KEDA, ad esempio servizio app, usare le funzionalità di scalabilità automatica fornite dalla piattaforma stessa. Queste funzionalità includono spesso il ridimensionamento in base alle regole basate sulle metriche o sul traffico HTTP.

  • Configurare le repliche minime. Per evitare un avvio a freddo, configurare le impostazioni di scalabilità automatica per mantenere almeno una replica. Un avvio a freddo si verifica quando si inizializza un servizio da uno stato arrestato, che spesso crea una risposta ritardata. Se ridurre al minimo i costi è una priorità ed è possibile tollerare ritardi di avvio a freddo, impostare il numero minimo di repliche su 0 durante la configurazione della scalabilità automatica.

  • Configurare un periodo di raffreddamento. Applicare un periodo di raffreddamento appropriato per introdurre un ritardo tra gli eventi di ridimensionamento. L'obiettivo è prevenire attività di ridimensionamento eccessive attivate da picchi di carico temporanei.

  • Configurare il ridimensionamento basato su coda. Se l'applicazione usa una coda di messaggi come bus di servizio, configurare le impostazioni di scalabilità automatica in base alla lunghezza della coda con i messaggi di richiesta. Il scaler mira a mantenere una replica del servizio per ogni messaggio N nella coda (arrotondato per errotondare).

Ad esempio, l'implementazione di riferimento usa il scaler KEDA bus di servizio per ridimensionare automaticamente l'app contenitore in base alla lunghezza della coda bus di servizio. La regola di ridimensionamento, denominata service-bus-queue-length-rule, regola il numero di repliche del servizio in base al numero di messaggi nella coda di bus di servizio specificata. Il messageCount parametro è impostato su 10, il che significa che il scaler aggiunge una replica per ogni 10 messaggi nella coda. Le repliche massime (max_replicas) sono impostate su 10 e le repliche minime sono implicitamente 0 a meno che non venga sottoposto a override, che consente al servizio di ridurre fino a zero quando non sono presenti messaggi nella coda. Il stringa di connessione per la coda di bus di servizio viene archiviato in modo sicuro come segreto in Azure, denominato azure-servicebus-connection-string, che viene usato per autenticare il scaler nella bus di servizio.

    max_replicas = 10
    min_replicas = 1

    custom_scale_rule {
      name             = "service-bus-queue-length-rule"
      custom_rule_type = "azure-servicebus"
      metadata = {
        messageCount = 10
        namespace    = var.servicebus_namespace
        queueName    = var.email_request_queue_name
      }
      authentication {
        secret_name       = "azure-servicebus-connection-string"
        trigger_parameter = "connection"
      }
    }

Distribuire un servizio in contenitori

La containerizzazione significa che tutte le dipendenze per il funzionamento dell'app vengono incapsulate in un'immagine leggera che può essere distribuita in modo affidabile in un'ampia gamma di host. Per inserire in contenitori la distribuzione, seguire queste indicazioni:

  • Identificare i limiti del dominio. Per iniziare, identificare i limiti del dominio all'interno dell'applicazione monolitica. Ciò consente di determinare quali parti dell'applicazione è possibile estrarre in servizi separati.

  • Creare immagini Docker. Quando si creano immagini Docker per i servizi Java, usare immagini di base OpenJDK ufficiali. Queste immagini contengono solo il set minimo di pacchetti necessari per l'esecuzione di Java, riducendo al minimo le dimensioni del pacchetto e l'area di attacco.

  • Usare Dockerfile a più fasi. Usare un Dockerfile a più fasi per separare gli asset in fase di compilazione dall'immagine del contenitore di runtime. Consente di mantenere le immagini di produzione piccole e sicure. È anche possibile usare un server di compilazione preconfigurato e copiare il file JAR nell'immagine del contenitore.

  • Eseguire come utente nonroot. Eseguire i contenitori Java come utente nonroot (tramite nome utente o UID, $APP_UID) per allinearsi al principio dei privilegi minimi. Limita gli effetti potenziali di un contenitore compromesso.

  • Ascolto sulla porta 8080. Quando si esegue come utente nonroot, configurare l'applicazione per l'ascolto sulla porta 8080. Si tratta di una convenzione comune per gli utenti nonroot.

  • Incapsulare le dipendenze. Assicurarsi che tutte le dipendenze per la funzione dell'app siano incapsulate nell'immagine del contenitore Docker. L'incapsulamento consente di distribuire l'app in modo affidabile in un'ampia gamma di host.

  • Scegliere le immagini di base appropriate. L'immagine di base scelta dipende dall'ambiente di distribuzione. Se si esegue la distribuzione in App contenitore, ad esempio, è necessario usare immagini Docker Linux.

L'implementazione di riferimento illustra un processo di compilazione Docker per la containerizzazione di un'applicazione Java. Questo Dockerfile usa una compilazione a fase singola con l'immagine di base OpenJDK (mcr.microsoft.com/openjdk/jdk:17-ubuntu), che fornisce l'ambiente di runtime Java necessario.

Il Dockerfile include i passaggi seguenti:

  1. Dichiarazione di volume: viene definito un volume temporaneo (/tmp), consentendo l'archiviazione file temporanea separata dal file system principale del contenitore.
  2. Copia degli artefatti: il file JAR dell'applicazione (email-processor.jar) viene copiato nel contenitore, insieme all'agente di Application Insights (applicationinsights-agent.jar) per il monitoraggio.
  3. Impostazione del punto di ingresso: il contenitore è configurato per eseguire l'applicazione con l'agente di Application Insights abilitato, usando java -javaagent per monitorare l'applicazione durante il runtime.

Questo Dockerfile mantiene l'immagine snella includendo solo le dipendenze di runtime, adatte per ambienti di distribuzione come App contenitore, che supportano contenitori basati su Linux.

# Use OpenJDK 17 base image on Ubuntu as the foundation
FROM mcr.microsoft.com/openjdk/jdk:17-ubuntu

# Define a volume to allow temporary files to be stored separately from the container's main file system
VOLUME /tmp

# Copy the packaged JAR file into the container
COPY target/email-processor.jar app.jar

# Copy the Application Insights agent for monitoring
COPY target/agent/applicationinsights-agent.jar applicationinsights-agent.jar

# Set the entry point to run the application with the Application Insights agent
ENTRYPOINT ["java", "-javaagent:applicationinsights-agent.jar", "-jar", "/app.jar"]

Distribuire l'implementazione di riferimento

Distribuire l'implementazione di riferimento del modello di app Web moderno per Java. Nel repository sono disponibili istruzioni sia per lo sviluppo che per la distribuzione di produzione. Dopo la distribuzione, è possibile simulare e osservare i modelli di progettazione.

Diagramma che mostra l'architettura dell'implementazione di riferimento.Figura 3. Architettura dell'implementazione di riferimento. Scaricare un file di Visio di questa architettura.