Block Image

Negli ultimi anni, ho avuto modo di lavorare intensivamente con il cloud AWS, sviluppando applicazioni Spring Boot che vengono distribuite su AWS ECS tramite AWS CloudFormation. Questi applicativi sfruttano una vasta gamma di servizi AWS, tra cui SQS, DynamoDB, Parameter Store, e molti altri.

Un aspetto fondamentale nello sviluppo di applicazioni moderne è garantire che possano essere eseguite e testate in modo affidabile anche in locale. Tuttavia, quando si integrano servizi cloud, ottenere un ambiente locale che replichi fedelmente l’infrastruttura AWS rappresenta una sfida significativa.

Per gli sviluppatori che utilizzano AWS, uno strumento essenziale è LocalStack. LocalStack è un emulatore di servizi AWS che può essere eseguito in un container, sia sul proprio laptop che in un ambiente CI. Questo strumento consente di eseguire e testare applicazioni AWS, incluse funzioni Lambda, interamente in locale, senza la necessità di connettersi al cloud reale. LocalStack supporta un’ampia gamma di servizi AWS, tra cui Lambda, S3, DynamoDB, Kinesis, SQS, SNS, e molti altri.

In questo articolo, esploreremo come configurare un’applicazione Spring Boot per funzionare in locale utilizzando LocalStack. L’applicazione interagirà con servizi come SQS, DynamoDB e Parameter Store tramite Spring Cloud AWS, mostrando come replicare in modo efficace un ambiente AWS completamente locale per sviluppi e test più efficienti.

Prerequisiti

  • Java 21 o superiori.
  • Installazione di Podman o Docker nella macchina.

L'applicazione Spring Boot

L'applicazione Spring Boot utilizza Java 21, Spring Boot 3 e Spring Cloud AWS 3.3.0 ed ha le seguenti dipendenze Maven:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.awspring.cloud</groupId>
            <artifactId>spring-cloud-aws-dependencies</artifactId>
            <version>${spring-cloud-aws.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>


<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>io.awspring.cloud</groupId>
        <artifactId>spring-cloud-aws-starter-sqs</artifactId>
    </dependency>
    <dependency>
        <groupId>io.awspring.cloud</groupId>
        <artifactId>spring-cloud-aws-starter-dynamodb</artifactId>
    </dependency>
    <dependency>
        <groupId>io.awspring.cloud</groupId>
        <artifactId>spring-cloud-aws-starter-parameter-store</artifactId>
    </dependency>

    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Rilevanti sono le dipendenze di Spring Cloud AWS per SQS, DynamoDB e Parameter Store, siccome la nostra applicazione utilizzerà questi strumenti di AWS.

Integrazione di Spring Cloud AWS con SQS

L’applicazione agisce sia come producer che come consumer per SQS. I messaggi scambiati rappresentano un libro, con il seguente modello Java:

public record Book(String isbn, String name) {}
Producer con Spring Cloud AWS SQS

Per la parte di producer, Spring Cloud AWS SQS, in combinazione con Spring Boot, fornisce un’istanza preconfigurata di SqsTemplate. Questa classe può essere facilmente iniettata nei bean e consente di inviare messaggi a una coda SQS. Internamente, SqsTemplate utilizza un’istanza di SqsAsyncClient, un client fornito dall’SDK AWS, che gestisce l’interazione con l’API SQS.

Ecco un esempio di classe producer:

@Component
public class BookProducer {

    private final SqsTemplate sqsTemplate;

    public BookProducer(SqsTemplate sqsTemplate) {
        this.sqsTemplate = sqsTemplate;
    }

    @EventListener(ApplicationReadyEvent.class)
    public void produce() {
        var book = new Book("isbn", "Name");
        sqsTemplate.send(to -> to.queue("book-event-queue")
                .payload(book)
                .header("eventId", UUID.randomUUID().toString())
                .delaySeconds(2)
        );


    }
}

In questo esempio, al momento dell’avvio dell’applicazione, viene inviato automaticamente un messaggio alla coda book-event-queue. La fluent API di SqsTemplate consente di:

  • Specificare la coda di destinazione tramite il metodo queue.
  • Definire il payload del messaggio.
  • Aggiungere header personalizzati (es. eventId).
  • Impostare un ritardo opzionale per la consegna del messaggio (in secondi) tramite delaySeconds.

Questa flessibilità rende l’invio di messaggi estremamente semplice e configurabile.

Consumer con Spring Cloud AWS SQS

Per quanto riguarda la parte di consumer, è possibile utilizzare i metodi di SqsTemplate per ricevere messaggi. Tuttavia, il modo più intuitivo e standardizzato con Spring Cloud AWS SQS è tramite l’annotazione @SqsListener. Questa annotazione permette di configurare una classe bean come listener per una specifica coda.

Ecco un esempio di consumer:

@Component
public class BookConsumer {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @SqsListener("book-event-queue")
    public void listen(@Payload Book payload,@Headers Map<String, Object> headers) {
        log.info("Book event received, headers: {}, payload: {}", headers, payload);
    }
}

In questo caso:

  • Il consumer resta in ascolto sulla coda book-event-queue.
  • Il payload del messaggio viene automaticamente deserializzato in un oggetto Book grazie al supporto di Spring Cloud AWS SQS.
  • Gli header del messaggio e il payload deserializzato vengono loggati per scopi dimostrativi.
Nota: È importante notare che Amazon SQS supporta solo payload di tipo String. Tuttavia, Spring Cloud AWS semplifica questa limitazione trasformando automaticamente i messaggi JSON in oggetti Java e viceversa. Questo processo di serializzazione e deserializzazione consente di lavorare direttamente con oggetti Java, senza dover gestire manualmente il parsing del JSON.

Integrazione di Spring Cloud AWS con DynamoDB

Lo starter di Spring Cloud AWS DynamoDB fornisce un’implementazione preconfigurata per interagire con DynamoDB. Durante l’avvio dell’applicazione, viene registrato un bean di tipo DynamoDbOperations, che offre un livello di astrazione di alto livello per lavorare con DynamoDB. L’implementazione predefinita di questa interfaccia è DynamoDbTemplate, la quale si basa su DynamoDbEnhancedClient, una classe avanzata fornita dall’SDK AWS.

Nella nostra applicazione, leggiamo e scriviamo dati da una tabella DynamoDB chiamata person. La classe di dominio di riferimento è:

public abstract class PersonEntity {

    private UUID id;
    private String name;
    private String lastName;

    // getters, setters, etc...
    
}

Per mappare questa classe alla tabella person, utilizziamo la seguente entità:

@DynamoDbBean
public class Person extends PersonEntity {

    @DynamoDbPartitionKey
    @Override
    public UUID getId() {
        return super.getId();
    }
}

Grazie all’annotazione @DynamoDbBean, la classe Person viene riconosciuta come un’entità mappata alla tabella DynamoDB person. Il nome della tabella viene risolto automaticamente utilizzando il bean DynamoDbTableNameResolver. L’implementazione predefinita di questo resolver trasforma il nome della classe annotata in snake_case e permette l’uso di prefissi o suffissi per personalizzare ulteriormente il nome della tabella.

Operazioni CRUD con DynamoDbTemplate

L’applicazione utilizza un’istanza preconfigurata di DynamoDbTemplate, registrata automaticamente da Spring Cloud AWS, per eseguire operazioni CRUD in modo semplice. Di seguito, un esempio pratico che scrive un record nella tabella person e lo legge subito dopo l’avvio dell’applicazione:

@Component
public class DbInitializer {

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

    private final DynamoDbTemplate dynamoDbTemplate;

    public DbInitializer(DynamoDbTemplate dynamoDbTemplate) {
        this.dynamoDbTemplate = dynamoDbTemplate;
    }

    @EventListener(ApplicationReadyEvent.class)
    public void initialize() {
        Person entity = new Person();
        UUID id = UUID.randomUUID();
        entity.setId(id);
        entity.setName("John Doe");
        entity.setLastName("Smith");

        dynamoDbTemplate.save(entity);

        Person entityFromDb = dynamoDbTemplate.load(
                Key.builder().partitionValue(id.toString()).build(),
                Person.class);

        log.info("Found Person from DynamoDb: {}", entityFromDb);
    }
}

Integrazione di Spring Cloud AWS con Parameter Store

Per caricare le proprietà da AWS Parameter Store nella nostra applicazione Spring, è necessario configurare il file application.properties aggiungendo la seguente proprietà:

spring.config.import=aws-parameterstore:<path-parameter-store>

Ad esempio, supponendo di avere un path nel Parameter Store chiamato /config/localstack, la proprietà corretta sarà:

spring.config.import=aws-parameterstore:/config/localstack/?prefix=localstack.

In questa configurazione, il suffisso "/?prefix=localstack.", indica che le proprietà del Parameter Store presenti sotto il path /config/localstack verranno mappate nelle proprietà di Spring col prefisso "localstack.".

La seguente classe viene utilizzata per mappare le proprietà configurate nel Parameter Store con le variabili della nostra applicazione Spring:

@Component
@ConfigurationProperties(prefix = "localstack")
public class LocalstackProperties {

    private String envValue;

    // getter, setter
}

In questo caso, la variabile envValue sarà automaticamente popolata con il valore associato al path /config/localstack/env-value nel Parameter Store. Questo approccio consente di centralizzare la configurazione delle proprietà esterne e di mantenerle facilmente gestibili e riutilizzabili all’interno dell’applicazione.

Di seguito una classe che allo startup dell'applicazione recupera il valore della variabile envValue dal Parameter Store e lo logga:

@Component
public class PrintConfigs {

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

    private final LocalstackProperties localstackProperties;

    public PrintConfigs(LocalstackProperties localstackProperties) {
        this.localstackProperties = localstackProperties;
    }


    @EventListener(ApplicationReadyEvent.class)
    public void printConfigs() {
        log.info("Printing env value from parameter-store: {}", localstackProperties.getEnvValue());
    }
}

Configurazione di LocalStack

LocalStack offre diversi modi per essere eseguito e configurato, a seconda delle esigenze e dell’ambiente. Ecco alcune opzioni per iniziare:

  1. Installare la CLI di LocalStack per interagire tramite linea di comando.
  2. Installare LocalStack Desktop per utilizzare lo strumento tramite un’interfaccia grafica.
  3. Usare l’estensione di LocalStack per Docker Desktop per un’integrazione fluida.
  4. Usare Docker Compose o Docker per avviare LocalStack come container.
  5. Deployare LocalStack su Kubernetes utilizzando Helm per un setup scalabile e containerizzato.

In questo tutorial, utilizzeremo Docker Compose per eseguire LocalStack in locale. Inoltre, sfrutteremo la gerarchia di lettura delle proprietà di application.properties di Spring Boot per caricare configurazioni specifiche per l’esecuzione locale.

Configurare Spring Boot per LocalStack

Per configurare Spring Boot con LocalStack, crea una sottocartella chiamata config nella root del progetto. All’interno, aggiungi un file application.properties con il seguente contenuto:

spring.cloud.aws.credentials.access-key=test
spring.cloud.aws.credentials.secret-key=test
spring.cloud.aws.region.static=us-east-1
spring.cloud.aws.endpoint=http://localhost:4566

Le properties scritte nel file config/application.properties hanno precedenza su quelle del file resources/application.properties. Inoltre, questo file sarà letto solo quando l'applicazione sarà avviata in locale.

Questa configurazione elimina la necessità di creare un profilo AWS sulla nostra macchina, poiché le credenziali e l’endpoint locale sono definiti esplicitamente. Durante l’esecuzione, Spring Cloud AWS utilizzerà queste proprietà per interagire con LocalStack.

Spring Cloud AWS auto-configura un bean di tipo DefaultCredentialsProvider, che determina le credenziali da utilizzare seguendo questo ordine di precedenza:

  1. Java System Properties – aws.accessKeyId e aws.secretAccessKey.
  2. Variabili d’ambiente – AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY.
  3. Token di identità Web – recuperati da proprietà di sistema o variabili d’ambiente.
  4. File dei profili delle credenziali – situato nella posizione predefinita (~/.aws/credentials).
  5. Credenziali del container Amazon ECS – utilizzando la variabile d’ambiente AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.
  6. Credenziali del profilo dell’istanza EC2 – tramite il servizio di metadati di Amazon EC2.

Puoi sovrascrivere questo comportamento impostando esplicitamente le proprietà spring.cloud.aws.credentials.access-key e spring.cloud.aws.credentials.secret-key, come mostrato nella configurazione.

Inizializzare le Risorse AWS in LocalStack

Nella stessa cartella config, crea uno script chiamato init-aws.sh per inizializzare automaticamente le risorse AWS necessarie al primo avvio del container LocalStack:

#!/bin/bash

echo "Create SQS queue"
aws sqs create-queue --queue-name book-event-queue --endpoint-url http://localhost:4566

echo "Create DynamoDB table"
aws dynamodb create-table \
    --table-name person \
    --attribute-definitions \
        AttributeName=id,AttributeType=S \
    --key-schema \
        AttributeName=id,KeyType=HASH \
    --provisioned-throughput \
        ReadCapacityUnits=5,WriteCapacityUnits=5 \
    --endpoint-url http://localhost:4566

echo "Create Parameter Store"
aws ssm put-parameter \
    --name "config/localstack/env-value" \
    --value "local" \
    --type String \

Questo script crea:

  1. Una coda SQS chiamata book-event-queue.
  2. Una tabella DynamoDB chiamata person con una chiave di partizione id.
  3. Una variabile nel Parameter Store chiamata config/localstack/env-value con valore local.

Configurare Docker Compose

Nella root del progetto, crea un file compose.yaml con la seguente configurazione:

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-4.0.3}"
    image: localstack/localstack:4.0.3
    ports:
      - "127.0.0.1:4566:4566"            # LocalStack Gateway
      - "127.0.0.1:4510-4559:4510-4559"  # external services port range
    environment:
      # LocalStack configuration: https://docs.localstack.cloud/references/configuration/
      - DEBUG=${DEBUG:-0}
      - AWS_ACCESS_KEY_ID=test
      - AWS_SECRET_ACCESS_KEY=test
      - AWS_DEFAULT_REGION=us-east-1
    volumes:
#      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "./config/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh"  # ready hook
      - "/var/run/docker.sock:/var/run/docker.sock"

Analizziamo il file:

  • Le variabili AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY corrispondono a quelle definite in application.properties.
  • Lo script init-aws.sh viene montato come ready hook, garantendo l’inizializzazione delle risorse AWS all’avvio di LocalStack.
  • La riga commentata ${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack può essere usata per persistere le risorse AWS oltre il ciclo di vita del container.

Avviare LocalStack

Dalla root del progetto, avvia LocalStack con il seguente comando:

Con Podman:

podman compose up -d

Con Docker:

docker compose up -d

Controlla i log del container per assicurarti che le risorse siano state inizializzate correttamente:

Podman:

podman compose logs -f localstack

Docker:

docker compose logs -f localstack

Vedrai i seguenti log:

Create SQS queue
INFO --- [et.reactor-0] localstack.request.aws     : AWS sqs.CreateQueue => 200
{
    "QueueUrl": "http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/book-event-queue"
}
Create DynamoDB table
INFO --- [et.reactor-0] localstack.utils.bootstrap : Execution of "require" took 1320.33ms
INFO --- [et.reactor-0] localstack.request.aws     : AWS dynamodb.CreateTable => 200
{
    "TableDescription": {
        "AttributeDefinitions": [
            {
                "AttributeName": "id",
                "AttributeType": "S"
            }
        ],
        "TableName": "person",
        "KeySchema": [
            {
                "AttributeName": "id",
                "KeyType": "HASH"
            }
        ],
        "TableStatus": "ACTIVE",
        "CreationDateTime": 1737820295.128,
        "ProvisionedThroughput": {
            "ReadCapacityUnits": 5,
            "WriteCapacityUnits": 5
        },
        "TableSizeBytes": 0,
        "ItemCount": 0,
        "TableArn": "arn:aws:dynamodb:us-east-1:000000000000:table/person",
        "TableId": "4cdb8ba5-56e2-4b99-9ca4-da2d46a33e8b",
        "DeletionProtectionEnabled": false
    }
}
Create Parameter Store
INFO --- [et.reactor-1] localstack.request.aws     : AWS ssm.PutParameter => 200
{
    "Version": 1
}
Ready.

Bene, abbiamo creato le risorse di SQS, DynamoDB e Parameter Store di cui necessità la nostra applicazione.

Monitorare le risorse di LocalStack su LocalStack Cloud

Puoi monitorare le risorse AWS create con Localstack andando su LocalStack cloud (puoi loggarti con l'account GitHub).

Block Image

LocalStack Cloud rileverà automaticamente una istanza di LocalStack attiva nella tua macchina, permettendoti di visualizzare le risorse create come code SQS e tabelle DynamoDB, oltre a poter effettuare operazioni di scrittura su questi strumenti.

Avviare l’Applicazione

Sempre dalla root del progetto, avvia l’applicazione Spring Boot con:

./mvnw clean spring-boot:run

Controlla i log dell’applicazione:

DbInitializer: Found Person from DynamoDb:
 PersonEntity{id=d7cc6d96-c5ac-480d-9a0a-c0f47c7f5abe, name='John Doe', lastName='Smith'}
 
PrintConfigs: Printing env value from parameter-store: local

BookConsumer: Book event received, headers: 
{eventId=27a6d678-9d92-4abd-b069-ca8e03224887, ...}, 
payload: Book[isbn=isbn, name=Name]

Come possiamo vedere dai log, la nostra applicazione ha interagito correttamente con risorse AWS create localmente grazie a LocalStack.

Conclusioni

In questo tutorial, abbiamo esplorato come utilizzare LocalStack per creare un ambiente locale in grado di emulare i servizi AWS, come SQS, DynamoDB e Parameter Store, e integrarlo con un’applicazione Spring Boot. Abbiamo configurato LocalStack tramite Docker Compose, inizializzato le risorse AWS necessarie utilizzando uno script dedicato e sfruttato le capacità di Spring Cloud AWS per interagire con queste risorse in modo semplice e trasparente.

Grazie a questa configurazione, abbiamo dimostrato come sia possibile replicare un ambiente AWS completamente in locale, consentendo di testare e sviluppare applicazioni in modo rapido ed efficace, senza dipendere dal cloud reale. Questo approccio non solo semplifica lo sviluppo, ma offre anche un modo economico e controllato per garantire la qualità del codice in ambienti simili a quelli di produzione.

Trovate il codice completo sul mio repo di GitHub al seguente link: GitHub.

Altri articoli su Spring: Spring.
Articoli su Docker: Docker.

Libri consigliati su Spring, Docker e Kubernetes:

Additional Resources