Friday 1 March 2019

AWS, Spring, Localstack

Using the AWS Java client is very straightforward.  Unit testing is also quite simple by just mocking the AWS classes.  However, integration testing is more complicated.  Here is an example of using LocalStack, TestContainers and Spring to wire AWS objects to point to the LocalStack instance.

Localstack: An implementation of AWS which runs locally with natively or in a docker container
TestContainers: A java library that lets a docker container be run locally for testing

Here TestContainers is used to start the localstack docker image so that the AWS calls can be made against it.


Maven dependencies

Using the v2 dependencies for the AWS library requires bringing in the AWS bom (bill of materials) so that any dependency can just be declared and the bom takes care of getting the correct versions of each dependency.  In the example below the S3 and SQS dependencies are configured.

Dependency management & dependencies

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>bom</artifactId>
        <version>2.4.11</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>s3</artifactId>
    </dependency>
    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>sqs</artifactId>
    </dependency>

Test dependencies:

    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>testcontainers</artifactId>
      <version>1.10.6</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>localstack</artifactId>
      <version>1.10.6</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>cloud.localstack</groupId>
      <artifactId>localstack-utils</artifactId>
      <version>0.1.18</version>
      <scope>test</scope>
    </dependency>


AWS Configuration

The normal spring configuration for aws clients is very straight forward.  Here is an example of an S3Client and an SqsClient using the AWS v2 objects.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.sqs.SqsClient;

@Configuration
public class AwsConfiguration {

  @Bean
  public S3Client s3Client(){
    return S3Client.builder().region(Region.EU_WEST_1).build();
  }

  @Bean
  public SqsClient sqsClient(){
    return SqsClient.builder().region(Region.EU_WEST_1).build();
  }
}

These objects will use the default AwsCredentialProvider but this can be overridden here.


TestConfiguration

To create the test configuration we need to start Localstack using TestContainers.  This test configuration starts the Localstack and then uses it to configure the S3Client and SqsClient to point to localstack

import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3;
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SQS;

import java.net.URI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.testcontainers.containers.localstack.LocalStackContainer;
import org.testcontainers.containers.wait.strategy.DockerHealthcheckWaitStrategy;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain;
import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.CreateQueueRequest;

@TestConfiguration
public class AwsConfigurationTest {

  @Bean
  public LocalStackContainer localStackContainer() {
    LocalStackContainer localStackContainer = new LocalStackContainer().withServices(SQS, S3);
    localStackContainer.start();
    return localStackContainer;
  }

  @Bean
  public S3Client s3Client() {

    final S3Client client = S3Client.builder()
        .endpointOverride(URI.create(localStackContainer().getEndpointConfiguration(S3).getServiceEndpoint()))
        .build();

    client.createBucket(CreateBucketRequest.builder().bucket("test_bucket").build());

    return client;
  }

  @Bean
  public SqsClient sqsClient() {

    final SqsClient sqs = SqsClient.builder()
        .endpointOverride(URI.create(localStackContainer().getEndpointConfiguration(SQS).getServiceEndpoint()))
        .build();

    sqs.createQueue(CreateQueueRequest.builder().queueName("test_queue").build());

    return sqs;
  }
}




1 comment: