Common features

Named clients

This feature is new and was initially designed to allow overriding credentials per named client. Feel free to open an issue to propose enhancements.

You can inject named clients with different configurations. To do this, annotate your injection point with @AmazonClient.

import io.quarkiverse.amazon.common.AmazonClient;

public class DynamoDbEnhancedClientTest {

    @Inject
    @AmazonClient("custom")
    DynamoDbClient clientNamedCustom;

Named clients inherit the configuration of the unamed client but you can override them.

quarkus.dynamodb.custom.aws.credentials.type=static
quarkus.dynamodb.custom.aws.credentials.static-provider.access-key-id=xxx
quarkus.dynamodb.custom.aws.credentials.static-provider.secret-access-key=yyy

Synthetic bean injection

Synthetic beans require manual registration of the client usage. This is because synthetic beans are only scanned for dependencies after standard beans. Due to this, when the bean BeanRegistrationPhaseBuildItem is consumed, and injection points are scanned, injection points the synthetic beans are not found.

To create a synthetic bean, use to following pattern. This example uses the async ECR client, however, the same principle applies to all clients. Refer to the generated test cases for an example of each client.

Create the processor class

If developing an extension, the processor class is placed into the deployment module. General practice is to place it in the <package name>.deployment package.

import io.quarkiverse.amazon.common.deployment.RequireAmazonClientInjectionBuildItem;
import io.quarkiverse.amazon.common.runtime.ClientUtil;
import io.quarkiverse.amazon.ecr.EcrTestRecorder;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import jakarta.inject.Singleton;
import org.jboss.jandex.ClassType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.ParameterizedType;
import software.amazon.awssdk.services.ecr.EcrAsyncClient;

public class ExampleProcessor {
    @Record(ExecutionTime.STATIC_INIT) (1)
    @BuildStep
    public void registerExampleBean(
            EcrTestRecorder recorder,
            BuildProducer<SyntheticBeanBuildItem> syntheticBeanProducer,
            BuildProducer<RequireAmazonClientInjectionBuildItem> requireAmazonClientInjectionProducer) {

        var clientClassName = DotName.createSimple(EcrAsyncClient.class); (2)
        syntheticBeanProducer.produce(
                SyntheticBeanBuildItem.configure(SyntheticBean.class) (3)
                        .scope(Singleton.class) (4)
                        .addInjectionPoint(ParameterizedType.create(
                                DotNames.INSTANCE,
                                ClassType.create(clientClassName)
                        ))
                        .createWith(recorder.createBean()) (5)
                        .done()
        );

        requireAmazonClientInjectionProducer.produce( (6)
                new RequireAmazonClientInjectionBuildItem(clientClassName, ClientUtil.DEFAULT_CLIENT_NAME) (7)
        );
    }
}
1 Can also be RUNTIME_INIT if required.
2 The client class required
3 The Synthetic bean you being created
4 Or a different scope that is required (ApplicationScoped etc)
5 The recorder function. If using SyntheticBeanBuildItem#createWith(Function), the bytecode produced by the function will be recorded, allowing for injection. This will not work with SyntheticBeanBuildItem#runtimeValue(RuntimeValue). However, if using SyntheticBeanBuildItem#runtimeValue(RuntimeValue), this approach can still be used to force the client inclusion and then use dynamic bean lookups to obtain the actual client.
6 Force the inclusion of the client into the build. Without this line, the client bean will be unresolved.
7 If a different @Named annotation is required, set the name at this point.

As a side note, if creating a processor in the test sources, do not rely on the processor being found in the META-INF/quarkus-build-steps.list from the QuarkusExtensionTest. The Quarkus bootstrap does not examine the test (generated) sources for this file. Do not adjust the classpath to include the file, it will create a race-condition where one test loads the processor list for a different test. Always manually create the META-INF/quarkus-build-steps.list manually using ShrinkWrap#addAsResource.

Create the recorder class

If developing an extension, the processor class is placed into the runtime module. General practice is to place it in the <package name> package.

import io.quarkus.arc.SyntheticCreationalContext;
import io.quarkus.runtime.annotations.Recorder;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.util.TypeLiteral;
import java.util.function.Function;
import software.amazon.awssdk.services.ecr.EcrAsyncClient;

@Recorder
public class EcrTestRecorder {
    public EcrTestRecorder() {}

    public Function<SyntheticCreationalContext<SyntheticBean>, SyntheticBean> createBean() {
        return context -> { (1)
            var ref = context.getInjectedReference(new TypeLiteral<Instance<EcrAsyncClient>>() {}); (2)
            return new SyntheticBean(ref);
        };
    }
}
1 The Function return. The result of this call, i.e. the Function lambda is recorded at build time and replayed at runtime to create the bean. The lambda itself is not evaluated at build time.
2 Get a reference to the injected bean, which is then passed to the constructor. Do not use the actual bean, or resolve it through Instance#get() at this point as the client bean may not have been created.

Create the bean class

If developing an extension, the processor class is placed into the runtime module. General practice is to place it in the <package name>.runtime package.

import jakarta.enterprise.inject.Instance;
import software.amazon.awssdk.services.ecr.EcrAsyncClient;

public class SyntheticBean {
    private Instance<EcrAsyncClient> client;

    public SyntheticBean(Instance<EcrAsyncClient> bean) { (1)
        this.bean = bean;
    }

    public void invoke() {
        this.bean.get(); (2)
    }
}
1 The bean instance is injected at this point. Since this is during startup, the actual bean may not be ready for use yet.
2 Invoke actions on the bean.

The synthetic bean class can be injected into any other class using the standard @Inject SyntheticBean bean approach. Be sure to only call methods invoking the bean after full startup of the CDI container.

Overriding the client configuration

You can override the client configuration by adding a custom producer that will further configure the client builder built by the extension.

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClientBuilder;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder;

@ApplicationScoped
public class DynamodbProducer {

    private static final DynamoDBModifyResponse EXECUTION_INTERCEPTOR = new io.quarkiverse.it.amazon.dynamodb.DynamoDBModifyResponse();

    @Produces
    @ApplicationScoped
    public DynamoDbClient createDynamoDbClient(DynamoDbClientBuilder builder) {
        builder.overrideConfiguration(
                c -> c.addExecutionInterceptor(EXECUTION_INTERCEPTOR));

        return builder.build();
    }

    @Produces
    @ApplicationScoped
    public DynamoDbAsyncClient createDynamoDbClient(DynamoDbAsyncClientBuilder builder) {
        builder.overrideConfiguration(
                c -> c.addExecutionInterceptor(EXECUTION_INTERCEPTOR));

        return builder.build();
    }
}

Custom AWS credentials provider

When configuring credentials type for a given client, you may not find the supported methods fit your context. In such case, you can implement and provide your own AwsCredentialsProvider as a bean.

As an example, we will implement a provider for a S3 client application hosted in EKS.

The provider named eks-iam can be implemented with a @Produces method:

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;

@Produces
@ApplicationScoped
@Named("eks-iam")
public AwsCredentialsProvider getAwsCredentialProvider() {

    String tokenFile = System.getenv("AWS_WEB_IDENTITY_TOKEN_FILE");
    String roleArn = System.getenv("AWS_ROLE_ARN");

    return (tokenFile != null && roleArn != null)
        ? WebIdentityTokenFileCredentialsProvider.builder()
            .webIdentityTokenFile(Paths.get(tokenFile))
            .roleArn(roleArn)
            .build()
        : DefaultCredentialsProvider.create();
}

Configure the default S3 client to use the custom provider for authentication:

quarkus.s3.aws.credentials.type=custom
quarkus.s3.aws.credentials.custom-provider.name=eks-iam

As this provider use WebIdentityTokenFileCredentialsProvider, add the following dependency to the application pom.xml:

<dependency>
    <groupId>io.quarkiverse.amazonservices</groupId>
    <artifactId>quarkus-amazon-sts</artifactId>
</dependency>

Docker image build with AWS CRT

Since AWS CRT version 0.31.0, native library must be embedded in the docker image. This is not done by default. To do this, you need to :

  1. edit .dockerignore to add:

    !target/*.so
    !target/ *.properties
  2. edit Dockerfile.native to add a line that copies *.so files and *.properties:

    # Shared objects to be dynamically loaded at runtime as needed,
    COPY --chown=1001:root target/*.properties target/*.so /work/