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.
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:
- Installare la CLI di LocalStack per interagire tramite linea di comando.
- Installare LocalStack Desktop per utilizzare lo strumento tramite un’interfaccia grafica.
- Usare l’estensione di LocalStack per Docker Desktop per un’integrazione fluida.
- Usare Docker Compose o Docker per avviare LocalStack come container.
- 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:
- Java System Properties – aws.accessKeyId e aws.secretAccessKey.
- Variabili d’ambiente – AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY.
- Token di identità Web – recuperati da proprietà di sistema o variabili d’ambiente.
- File dei profili delle credenziali – situato nella posizione predefinita (~/.aws/credentials).
- Credenziali del container Amazon ECS – utilizzando la variabile d’ambiente AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.
- 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:
- Una coda SQS chiamata
book-event-queue
. - Una tabella DynamoDB chiamata
person
con una chiave di partizioneid
. - Una variabile nel Parameter Store chiamata
config/localstack/env-value
con valorelocal
.
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
eAWS_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).
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:
- Cloud Native Spring in Action: https://amzn.to/3xZFg1S
- Pro Spring 6: An In-Depth Guide to the Spring Framework: https://amzn.to/4g8VPff
- Pro Spring Boot 3: An Authoritative Guide With Best Practices: https://amzn.to/4hxdjDp
- Docker: Sviluppare e rilasciare software tramite container: https://amzn.to/3AZEGDI
- Kubernetes. Guida per gestire e orchestrare i container: https://amzn.to/3EdA94v