Mock AWS con Localstack en Spring Boot

Mock AWS with localstack

Mock AWS with localstack


Es realmente importante comprobar y hacer test de nuestros servicios así como conexiones infraestructura etc, y verificar que todo funciona correctamente. Eso es perfecto, sin duda, pero cuando comienzas a tener interacciones con terceros a la hora de desplegar o tener que verificar tus servicios en AWS, se pueden complicar las tareas. Para esos casos en lo que utilizas AWS como infraestructura, LocalStack es una solución perfecta. En esta entrada vamos a un artículo sobre Mock AWS con Localstack en Spring Boot.

Cuando empezamos a desarrollar una aplicación que se va a conectar a AWS para hacer uso de diferentes servicios, es preferible poder trabajar únicamente en tu local sin necesidad de realizar ninguna conexión, cuando te encuentras en la fase de desarrollo. Es en esos casos, cuando Localstack nos va a facilitar el trabajo e integración de servicios con AWS. Sin necesidad de crear nada en AWS, simplemente levantando una imagen docker vas a poder a trabajar de una manera rápida y sencilla.

¿Qué es Localstack?

Localstack es una colección de servicios y/o productos compatibles con AWS que se pueden ejecutar en tu entorno local si necesidad de conectarse a AWS. Es un entorno para desarrollo y testing, obviamente no es para entornos productivos.

Además Localstack nos va a permitir realizar test integrados para servicios de AWS con la ayuda de Testcontainers, de manera que no necesitemos realizar ninguna conexión extra a nuestra instancia de AWS en nuestros test integrados.

En esta página de github podemos ver los componentes con los que cuenta LocalStack.

Hands on con LocalStack y Spring Boot

Para poder realizar nuestras pruebas con AWS y Spring Boot vamos a necesitar:

  • Docker
  • docker-compose
  • aws CLI
  • maven

A continuación vamos a crear una pequeña aplicación en Spring Boot en la que vamos a establecer una configuración con dynamodb y S3 de AWS, esta aplicación permitirá subir documentos a S3 y se conectará a una base de datos DynamoDB que tendrá una tabla (Car) . Estos dos servicios de AWS se encontrarn mockeados gracias a Localstack.

Todo el código de la aplicación se encuentra en nuestro github.

Spring boot con Localstack | Mock AWS con Localstack en Spring Boot
Spring boot con Localstack

Crear servicios de AWS con Localstack

Vamos a hacer uso de Localstack para simular servicios de AWS para ello haremos uso de docker:

docker run -t -i localstack/localstack

O podemos crear un docker-compose en donde definir los servicios implicados, como por ejemplo hacer uso de S3 y dynamodb:

version: '3.9'

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
    image: localstack/localstack
    ports:
      - "4566-4599:4566-4599"
      - "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
    environment:
      - SERVICES=s3,dynamodb
      - DEBUG=1
      - DATA_DIR=/tmp/localstack/data
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - localstack_data:/tmp/localstack/data
    networks:
      - localstack      
volumes:
    localstack_data:
networks:
  localstack: {}      

Una vez hemos configurado nuestro docker-compose vamos a ejecutarlo con el siguiente comando:

docker-compose up -d

Después de levantar nuestros servicios en local vamos a añadir una pequeña configuración en nuestro cliente de aws, para ello vamos a escribir en nuestro terminal:

aws configure 

y rellenar los campos que aparecen, al menos asegurarte de rellenar correctamente la región:

AWS Access Key ID [None]: anything
AWS Secret Access Key [None]: anything
Default region name [US East]: us-east-1
Default output format [None]: text

Conexión con Localstack

Vamos a probar y a verificar nuestra conexión con Localstack a través de AWS CLI. Por ejemplo vamos a ejecutar el siguiente comando para crear un bucket en nuestro S3:

aws s3api --endpoint-url=http://localhost:4566 create-bucket --bucket test

o el siguiente comando para visualizar los buckets en nuestro entorno local:

aws s3api --endpoint-url=http://localhost:4566 list-buckets

Una vez hemos establecido toda la configuración de Localstack es momento de comenzar con la configuración de Spring Boot.

Configurar Spring Boot con Localstack para hacer Mock de servicios de AWS

Vamos a añadir la configuración necesaria para poder establecer la conexión con DynamoDB y S3 y con Localstack para la parte de testing.

Dependencias Maven con Localstack DynamoDB y S3

		<dependency>
			<groupId>com.amazonaws</groupId>
			<artifactId>aws-java-sdk-dynamodb</artifactId>
			<version>${version}</version>
		</dependency>
		<dependency>
			<groupId>com.github.derjust</groupId>
			<artifactId>spring-data-dynamodb</artifactId>
			<version>${version}</version>
		</dependency>
		<dependency>
			<groupId>cloud.localstack</groupId>
			<artifactId>localstack-utils</artifactId>
			<version>${version}</version>
			<scope>test</scope>
		</dependency>

La dependencia de spring-data-dynamodb, nos permitirá trabajar con nuestra aplicación como si hicieramos uso de la dependencia de spring-data de spring boot. Es decir, vamos a poder hacer uso de CrudRepository o JPARepository mediante su interfaz, para poder trabajar con nuestra base de datos.

Configuración de Application.yaml con Localstack DynamoDB y S3

Vamos a establecer una configuración de conexión con DynamoDB y S3 como si de una configuración normal se tratará, la aplicación no es consciente que se trata de un Mock de AWS.

aws.dynamodb.endpoint=http://localhost:4566
aws.s3.endpoint=http://localhost:4566
aws.accesskey=anything
aws.secretkey=anything
aws.region=us-east-1

server.port=9080

Aunque el valor de aws.dynamodb.endpoint y aws.s3.endpoint, es el mismo lo hemos dividido en dos para la parte de testing de nuestra aplicación ya que haremos uso de containers de localstack en donde asignaremos la dirección que nos da el contenedor.

Creación del cliente DynamoDB

Para poder establecer la comunicación y conexión con DynamoDB de AWS, uno de los primeros pasos que tenemos que hacer dentro de nuestra aplicación de Spring Boot, para eso vamos a crear un Bean de tipo AmazonDynamo, en el que definimos los parámetros de conexión. Y además en esta misma clase activaremos e indicaremos haciendo uso de @EnableDynamoDBRepositories donde se encuentran nuestros repositorios de Base de Datos.

@Configuration
@EnableDynamoDBRepositories(basePackages = "com.refactorizando.sample.localstack.repository")
public class DynamoDBConfig {

    @Value("${aws.dynamodb.endpoint}")
    private String awsEndpoint;

    @Value("${aws.accesskey}")
    private String awsAccessKey;

    @Value("${aws.secretkey}")
    private String awsSecretKey;

    @Value("${aws.region}")
    private String awsRegion;

    @Bean
    public DynamoDBMapperConfig dynamoDBMapperConfig() {
        return DynamoDBMapperConfig.DEFAULT;
    }

    @Bean
    public AmazonDynamoDB amazonDynamoDB() {
        return AmazonDynamoDBClientBuilder
                .standard()
                .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(awsEndpoint, awsRegion))
                .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(awsAccessKey, awsSecretKey)))
                .build();
    }
}

Creación de la Entidad para DynamoDB

Para poder mappear los campos de una tabla de DynamoDB con un objeto de nuestra aplicación, necesitamos hacer uso de diferentes anotaciones:

  • DynamoDBTable
  • DynamoDBHashKey
  • DynamoDBAutoGeneratedKey
  • DynamoDBAttribute
@DynamoDBTable(tableName = "Car")
public class Car implements Serializable {

    @Id
    @DynamoDBHashKey(attributeName = "id")
    @DynamoDBAutoGeneratedKey
    @JsonIgnore
    private String id;

    @DynamoDBAttribute(attributeName = "color")
    private String color;

    @DynamoDBAttribute(attributeName = "model")
    private String model;

    @DynamoDBAttribute(attributeName = "brand")
    private String brand;

    @DynamoDBAttribute(attributeName = "available")
    private Boolean available;
}

Creación Repositorio para conexión con DynamoDB en Spring Boot

Vamos a crear la capa repository de nuestra aplicación que extenderá de CrudRepository de Spring Data, y se encargará de hacer las operaciones con DynamoDB.

En esta capa repository hay que añadir la anotación @EnableScan, esto es porque todos los métodos del repositorio se van a implementar como consultas, por lo que para eso tiene que existir un índice en otro caso las consultas fallaran.

@EnableScan
public interface CarRepository extends CrudRepository<Car, String> {

    List<Car> findByModel(String model);

    List<Car> findByColor (String color);
}

La definición es prácticamente identica a cualquier repositorio que se hace con Spring Data, indicando la entidad y el tipo de id que en este caso es String.

Gracias a que extendemos de CrudRepository, vamos a poder tener las operaciones por defecto que nos da CrudRepository, como save, findById etc…

Creación del cliente de S3 de AWS

Al igual que anteriormente hemos creado el cliente con DynamoDB vamos a crearlo con S3 de AWS. Para ello vamos a hacer uso de la clase AmazonS3ClientBUilder:

    @Value("${aws.s3.endpoint}")
    private String awsEndpoint;

    @Value("${aws.accesskey}")
    private String awsAccessKey;

    @Value("${aws.secretkey}")
    private String awsSecretKey;

    @Value("${aws.region}")
    private String awsRegion;

    
    @Bean
    public AmazonS3 s3() {

        return AmazonS3ClientBuilder
                .standard()
                .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(awsEndpoint, awsRegion))
                .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(awsAccessKey, awsSecretKey)))
                .build();

    }

Pasamo las credenciales y el endpoint para poder crear la conexión del cliente de S3 de AWS.

Operaciones con S3 de AWS en Spring Boot

Una vez hemos configurado el cliente vamos a ver como crear alguna operación con Spring Boot para realizar operaciones sobre S3.

Para crear un bucket vamos a hacer uso del cliente de amazon creado anteriormente (cliente creado para S3) y vamos a llamar al método crearBucket.

    private final AmazonS3 amazonS3;

    @Override
    public void createBucket(String bucketName) {

        amazonS3.createBucket(bucketName);
    }

Para crear el bucket desde nuestra aplicación vamos a tener que hacer un pequeño arreglo ya que el diseño de la API de S3 asume que el nombre del bucket es parte del nombre del dominio, por lo que el SDK codifica el nombre del bucket con el nombre del dominio de la URL utilizada. Parta evitar y solucionar este problema podemos añadir a nuestro /etc/hosts lo siguiente:

127.0.0.1       nombre-bucket.localhost

Podemos obtener todos los documentos en un bucket concreto hacemos uso del método listObjectsV2 de nuestro cliente de S3.

    @Override
    public List<String> getAllDocumentsFromBucket(String bucketName) {

        if(!amazonS3.doesBucketExistV2(bucketName)) {
            log.info("Bucket name is not available."
                    + " Try again with a different Bucket name.");
            throw new NoSuchElementException("Bucket name is not available");
        }

        return amazonS3.listObjectsV2(bucketName).getObjectSummaries().stream()
                .map(S3ObjectSummary::getKey)
                .collect(Collectors.toList());    
    } 

Y para acabar con ejemplos de operaciones sobre S3, vamos a subir un documento a nuestro bucket:

   @Override
    public void uploadDocument(MultipartFile file, String bucketName) throws IOException {

        String tempFileName = UUID.randomUUID() + file.getName();
        File tempFile = new File(System.getProperty("java.io.tmpdir") + "/" + tempFileName);
        file.transferTo(tempFile);
        amazonS3.putObject(bucketName, UUID.randomUUID() + file.getName(), tempFile);
        tempFile.deleteOnExit();
    }

Ahora nos faltaría un controlador que invoque a los ejemplos anteriores para poder hacer pruebas sobre nuestra aplicación, el código para ver el ejemplo completo lo puedes ver aquí.

Arrancando y probando la aplicación

Una vez hemos creado la aplicación y con nuestro LocalStack arrancado, vamos a probar algún endpoint. Si quieres puedes consultar el código entero en nuestro github.

Pero antes debemos ejecutar el siguiente comando en el que configuramos la clave primaria de nuestra entidad y la creación de la tabla:

aws dynamodb create-table --cli-input-json file://car_table.json --endpoint-url=http://localhost:4569

El json ejecutado es el siguiente:

{
    "TableName": "Car",
    "AttributeDefinitions": [
        {
            "AttributeName": "id",
            "AttributeType": "S"
        }
    ],
    "KeySchema": [
        {
            "AttributeName": "id",
            "KeyType": "HASH"
        }
    ],
    "ProvisionedThroughput": {
        "ReadCapacityUnits": 5,
        "WriteCapacityUnits": 5
    }
}

Testing con Localstack

Vamos a mostrar de manera resumida, como podemos hacer testing haciendo uso de testcontainers y localstack para poder levantar una imágen docker y realizar todo el proceso de testing de nuestra aplicación directamente en nuestra carpeta de test.

Por lo que vamos a añadir dos dependencias nuevas por un lado testcontainers y por otro lado la dependencia de testcontainers integrada con localstack:

		<dependency>
			<groupId>org.testcontainers</groupId>
			<artifactId>localstack</artifactId>
			<version>${container-version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.testcontainers</groupId>
			<artifactId>testcontainers</artifactId>
			<version>${container-version}</version>
			<scope>test</scope>
		</dependency>

Ahora vamos a hacer uso de containers, para levantar un contenedor de localstack en nuestros test integrados con Spring Boot y poder realizar test de nuestros servicios simulando una conexión a AWS.

Para ello primero vamos a definir los servicios que queremos simular haciendo uso de LocalStackContainer:

public static LocalStackContainer localStackContainer = new LocalStackContainer().withServices
            (LocalStackContainer.Service.S3, LocalStackContainer.Service.DYNAMODB);

A continuación habrá que arrancar el contenedor, e indicar el endpoint de conexión para ello hacemos:

    static {

        localStackContainer.start();

    }

    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {


        @Override
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {

            TestPropertyValues values = TestPropertyValues.of(
                    "aws.dynamodb.endpoint=" +
                            localStackContainer.getEndpointConfiguration(LocalStackContainer.Service.DYNAMODB)
                                    .getServiceEndpoint(),
                    "aws.s3.endpoint=" +
                            localStackContainer.getEndpointConfiguration(LocalStackContainer.Service.S3)
                                    .getServiceEndpoint());


            values.applyTo(configurableApplicationContext);
        }
    }
}

Anteriormente lo que hemos hecho ha sido sobreescribir las propiedades de nuestro application.yaml. Indicando que la propiedad aws.dynamodb.endpoint y aws.s3.endpoint , para que tome el valor de LocalStackContainer del servicio DYNAMODB y S3. También podemos hacer uso del application.yml de nuestra carpeta resources de test.

Una vez hemos configurado nuestro LocalStack para nuestros test integrados, es momento de crear algún test, vamos a ver una clase de test que hemos creado para verificar nuestro servicio CarService.

@RunWith(SpringRunner.class)
@SpringBootTest
public class CarServiceTest extends BaseIntegrationTest {

    @Autowired
    private CarService carService;

    @Autowired
    private AmazonDynamoDB amazonDynamoDB;


    @Autowired
    private AmazonS3 amazonS3;


    @Test
    public void verifyTableName() {

        assertThat(amazonDynamoDB.listTables().getTableNames().contains("car"));

    }

    @Test
    public void verifyS3Bucket() {
        Throwable throwable = catchThrowable(() -> carService.getAllDocumentsFromBuckets("aaaaa"));

        NoSuchElementException notFoundException = (NoSuchElementException) throwable;

        assertThat(notFoundException.getMessage()).isEqualTo("Bucket name is not available");

    }

    @Test
    public void createBucket() {

        carService.createBucket("aaaa");

        assertThat(amazonS3.listBuckets().contains("aaaa"));

    }
}

En la clase anterior vemos algún ejemplo de uso del cliente de dynamoDB y de S3 en los que gracias a LocalStack podemos simular las llamadas a AWS.

Conclusión

En esta entrada, Mock AWS con Localstack en Spring Boot, hemos visto como podemos simular las llamadas a AWS haciendo uso de LocalStack, y containers para la parte de test de nuestra aplicación. Localstack es una herramienta muy útil para las fases de desarrollo y testing de nuestra aplicación y poder aislarnos de las partes de infraestructura. La configuración y desarrollo de nuestra aplicación no va a variar en el desarrollo, tan solo la configuración, por lo que únicamente habrá que cambiar los parámetros o crear perfiles de conexión en nuestro desarrollo.

Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *