Skip to content
V1 V2

AWS S3 client

The s3-client-aws integration is independent of the experimental Kora S3 client. Add either artifact on its own, or use both when you need generated object operations and the full AWS SDK API.

AWS SDK client

The s3-client-aws artifact is a thin integration around the AWS SDK for Java v2. It publishes the SDK's own software.amazon.awssdk.services.s3.S3Client as a graph component, configures it from the Kora configuration and routes its HTTP traffic through Kora's HttpClient so that timeouts and telemetry stay consistent with the rest of the application.

It contains no @S3 annotations and no Kora S3 models — working with this artifact means working with the AWS SDK API. Use it for everything the declarative contract does not cover: bucket administration, object copying, presigned URLs, ACL and policy management, batch deletion.

Dependency

Dependency build.gradle:

implementation "io.koraframework:s3-client-aws"

Module:

@KoraApp
public interface Application extends AwsS3ClientModule { }

Dependency build.gradle.kts:

implementation("io.koraframework:s3-client-aws")

Module:

@KoraApp
interface Application : AwsS3ClientModule

Note that the group is io.koraframework, not io.koraframework.experimental — this artifact is not an experimental module. Requires any HTTP client module to be added: the SDK's own apache-client and netty-nio-client transports are excluded, and Kora supplies an SdkHttpClient implementation backed by the graph's HttpClient.

Configuration

Configuration is read from the s3client.aws path. Basic parameters:

s3client.aws {
    url = "http://localhost:9000" //(1)!
    credentials {
        accessKey = "someKey" //(2)!
        secretKey = "someSecret" //(3)!
    }
    region = "aws-global" //(4)!
}
  1. URL of the S3 storage (required, no default)
  2. S3 access key (required, no default)
  3. S3 secret key (required, no default)
  4. S3 storage region (default: aws-global)
s3client:
  aws:
    url: "http://localhost:9000" #(1)!
    credentials:
      accessKey: "someKey" #(2)!
      secretKey: "someSecret" #(3)!
    region: "aws-global" #(4)!
  1. URL of the S3 storage (required, no default)
  2. S3 access key (required, no default)
  3. S3 secret key (required, no default)
  4. S3 storage region (default: aws-global)
Full Configuration

Complete configuration described in the AwsS3Config class (example values or default values are specified):

s3client.aws {
    url = "http://localhost:9000" //(1)!
    credentials {
        accessKey = "someKey" //(2)!
        secretKey = "someSecret" //(3)!
    }
    region = "aws-global" //(4)!
    addressStyle = "PATH" //(5)!
    requestTimeout = "45s" //(6)!
    checksumCalculationRequest = "WHEN_REQUIRED" //(7)!
    checksumValidationResponse = "WHEN_REQUIRED" //(8)!
    chunkedEncodingEnabled = true //(9)!
    telemetry {
        logging {
            enabled = false //(10)!
        }
        metrics {
            enabled = false //(11)!
            slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(12)!
            tags = { // (13)!
                "key1" = "value1"
                "key2" = "value2"
            }
        }
        tracing {
            enabled = true //(14)!
            attributes = { // (15)!
                "key1" = "value1"
                "key2" = "value2"
            }
        }
    }
}
  1. URL of the S3 storage, passed to the SDK as an endpoint override (required, no default)
  2. S3 access key (required, no default)
  3. S3 secret key (required, no default)
  4. S3 storage region (default: aws-global)
  5. Object access style, can have values PATH or VIRTUAL_HOSTED (default: PATH)
  6. Maximum operation execution time, applied to the underlying HTTP request (default: 45s)
  7. When request checksums are calculated, can have values WHEN_SUPPORTED or WHEN_REQUIRED (default: WHEN_REQUIRED)
  8. When response checksums are validated, can have values WHEN_SUPPORTED or WHEN_REQUIRED (default: WHEN_REQUIRED)
  9. Whether to use chunked encoding when signing object data during upload (default: true)
  10. Enables module logging (default: false)
  11. Enables module metrics (default: false)
  12. Configures SLO for metrics (default: io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO)
  13. Configures metric tags (default: {})
  14. Enables module tracing (default: true)
  15. Configures tracing attributes (default: {})
s3client:
  aws:
    url: "http://localhost:9000" #(1)!
    credentials:
      accessKey: "someKey" #(2)!
      secretKey: "someSecret" #(3)!
    region: "aws-global" #(4)!
    addressStyle: "PATH" #(5)!
    requestTimeout: "45s" #(6)!
    checksumCalculationRequest: "WHEN_REQUIRED" #(7)!
    checksumValidationResponse: "WHEN_REQUIRED" #(8)!
    chunkedEncodingEnabled: true #(9)!
    telemetry:
      logging:
        enabled: false #(10)!
      metrics:
        enabled: false #(11)!
        slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(12)!
        tags: #(13)!
          key1: value1
          key2: value2
      tracing:
        enabled: true #(14)!
        attributes: #(15)!
          key1: value1
          key2: value2
  1. URL of the S3 storage, passed to the SDK as an endpoint override (required, no default)
  2. S3 access key (required, no default)
  3. S3 secret key (required, no default)
  4. S3 storage region (default: aws-global)
  5. Object access style, can have values PATH or VIRTUAL_HOSTED (default: PATH)
  6. Maximum operation execution time, applied to the underlying HTTP request (default: 45s)
  7. When request checksums are calculated, can have values WHEN_SUPPORTED or WHEN_REQUIRED (default: WHEN_REQUIRED)
  8. When response checksums are validated, can have values WHEN_SUPPORTED or WHEN_REQUIRED (default: WHEN_REQUIRED)
  9. Whether to use chunked encoding when signing object data during upload (default: true)
  10. Enables module logging (default: false)
  11. Enables module metrics (default: false)
  12. Configures SLO for metrics (default: io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO)
  13. Configures metric tags (default: {})
  14. Enables module tracing (default: true)
  15. Configures tracing attributes (default: {})

The bucket name is not part of AwsS3Config: the SDK takes it on every request, so declare it in your own @ConfigSource interface.

Usage

The SDK client is injected directly, without a tag:

@Component
public final class AwsS3Service {

    private final S3Client s3Client; //(1)!
    private final String bucket;

    public AwsS3Service(S3Client s3Client, S3Config config) {
        this.s3Client = s3Client;
        this.bucket = config.bucket();
    }

    public PutObjectResponse putObject(String key, byte[] value) {
        return s3Client.putObject(r -> r.bucket(bucket).key(key), RequestBody.fromBytes(value));
    }

    public ResponseInputStream<GetObjectResponse> getObject(String key) {
        return s3Client.getObject(r -> r.bucket(bucket).key(key));
    }

    public ListObjectsV2Response listObjects(String prefix) {
        return s3Client.listObjectsV2(r -> r.bucket(bucket).prefix(prefix).maxKeys(50));
    }

    public DeleteObjectsResponse deleteObjects(List<String> keys) { //(2)!
        var identifiers = keys.stream()
                .map(key -> ObjectIdentifier.builder().key(key).build())
                .toList();

        return s3Client.deleteObjects(r -> r.bucket(bucket).delete(d -> d.objects(identifiers)));
    }
}
  1. software.amazon.awssdk.services.s3.S3Client, the AWS SDK client
  2. Batch deletion, which the declarative client does not generate
@Component
class AwsS3Service(
    private val s3Client: S3Client, //(1)!
    config: S3Config
) {

    private val bucket: String = config.bucket()

    fun putObject(key: String, value: ByteArray): PutObjectResponse =
        s3Client.putObject({ it.bucket(bucket).key(key) }, RequestBody.fromBytes(value))

    fun getObject(key: String): ResponseInputStream<GetObjectResponse> =
        s3Client.getObject { it.bucket(bucket).key(key) }

    fun listObjects(prefix: String): ListObjectsV2Response =
        s3Client.listObjectsV2 { it.bucket(bucket).prefix(prefix).maxKeys(50) }

    fun deleteObjects(keys: List<String>): DeleteObjectsResponse { //(2)!
        val identifiers = keys.map { ObjectIdentifier.builder().key(it).build() }
        return s3Client.deleteObjects { r -> r.bucket(bucket).delete { d -> d.objects(identifiers) } }
    }
}
  1. software.amazon.awssdk.services.s3.S3Client, the AWS SDK client
  2. Batch deletion, which the declarative client does not generate

Failures are reported by the SDK's own exception hierarchy — NoSuchKeyException, NoSuchBucketException, S3Exception — not by the Kora S3 exceptions.

Custom SDK behaviour is added with ExecutionInterceptor components: every software.amazon.awssdk.core.interceptor.ExecutionInterceptor published under @Tag(Tag.Factory.class) is registered on the client alongside Kora's own telemetry interceptor.

Using both artifacts

Both artifacts can live in one application: they occupy different packages and different configuration sections, so there is nothing to reconcile.

implementation "io.koraframework:s3-client-aws"
implementation "io.koraframework.experimental:s3-client-kora"
@KoraApp
public interface Application extends AwsS3ClientModule, KoraS3ClientModule { }
implementation("io.koraframework:s3-client-aws")
implementation("io.koraframework.experimental:s3-client-kora")
@KoraApp
interface Application : AwsS3ClientModule, KoraS3ClientModule
s3client.aws { //(1)!
    url = ${S3_URL}
    credentials {
        accessKey = ${S3_ACCESS_KEY}
        secretKey = ${S3_SECRET_KEY}
    }
}

s3client.uploads { //(2)!
    endpoint = ${S3_URL}
    bucket = "uploads"
    credentials {
        accessKey = ${S3_ACCESS_KEY}
        secretKey = ${S3_SECRET_KEY}
    }
}
  1. Fixed path of the AWS SDK client
  2. Path declared in @S3.Client("s3client.uploads") of the declarative client
s3client:
  aws: #(1)!
    url: ${S3_URL}
    credentials:
      accessKey: ${S3_ACCESS_KEY}
      secretKey: ${S3_SECRET_KEY}

  uploads: #(2)!
    endpoint: ${S3_URL}
    bucket: "uploads"
    credentials:
      accessKey: ${S3_ACCESS_KEY}
      secretKey: ${S3_SECRET_KEY}
  1. Fixed path of the AWS SDK client
  2. Path declared in @S3.Client("s3client.uploads") of the declarative client

Bucket administration

Creating a bucket or checking that it exists is not part of the @S3 contract, so it goes through the AWS SDK client. The bucket name from @S3.Bucket ends up in a generated class rather than in a component, so the initializer reads the same configuration path itself:

@ConfigSource("s3client.uploads")
public interface S3UploadsConfig {

    String bucket();
}
@Root //(1)!
@Component
public final class S3BucketInitializer implements Lifecycle {

    private final S3Client s3Client; //(2)!
    private final S3UploadsConfig config;

    public S3BucketInitializer(S3Client s3Client, S3UploadsConfig config) {
        this.s3Client = s3Client;
        this.config = config;
    }

    @Override
    public void init() {
        var bucket = this.config.bucket();
        try {
            this.s3Client.headBucket(HeadBucketRequest.builder().bucket(bucket).build());
        } catch (NoSuchBucketException e) {
            this.s3Client.createBucket(CreateBucketRequest.builder().bucket(bucket).build());
        }
    }

    @Override
    public void release() {}
}
  1. Required: nothing depends on this component, so without @Root it is dropped from the graph
  2. software.amazon.awssdk.services.s3.S3Client
@ConfigSource("s3client.uploads")
interface S3UploadsConfig {

    fun bucket(): String
}
@Root //(1)!
@Component
class S3BucketInitializer(
    private val s3Client: S3Client, //(2)!
    private val config: S3UploadsConfig
) : Lifecycle {

    override fun init() {
        val bucket = config.bucket()
        try {
            s3Client.headBucket(HeadBucketRequest.builder().bucket(bucket).build())
        } catch (e: NoSuchBucketException) {
            s3Client.createBucket(CreateBucketRequest.builder().bucket(bucket).build())
        }
    }

    override fun release() {}
}
  1. Required: nothing depends on this component, so without @Root it is dropped from the graph
  2. software.amazon.awssdk.services.s3.S3Client

A Lifecycle component nobody depends on is dropped

Kora builds only the part of the graph that is reachable. A component that just prepares external state and is not a dependency of anything is removed together with everything it pulled in, which surfaces as a build error such as interface software.amazon.awssdk.services.s3.S3Client wasn't found in graph. Mark it @Root to keep it.