Skip to content

Add ec2InstanceProfileName configuration #6208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-994f3a1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Adding ec2InstanceProfileName configuration to specify IMDS instance profile for retrieving credentials."
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static java.time.temporal.ChronoUnit.MINUTES;
import static software.amazon.awssdk.utils.ComparableUtils.maximum;
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
import static software.amazon.awssdk.utils.StringUtils.isBlank;
import static software.amazon.awssdk.utils.cache.CachedSupplier.StaleValueBehavior.ALLOW;

import java.net.URI;
Expand Down Expand Up @@ -103,6 +104,8 @@ private enum ApiVersion {
private final Supplier<ProfileFile> profileFile;

private final String profileName;

private final String ec2InstanceProfileName;

private final Duration staleTime;

Expand All @@ -118,6 +121,13 @@ private InstanceProfileCredentialsProvider(BuilderImpl builder) {
.orElseGet(() -> ProfileFileSupplier.fixedProfileFile(ProfileFile.defaultProfileFile()));
this.profileName = Optional.ofNullable(builder.profileName)
.orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow);
this.ec2InstanceProfileName = builder.ec2InstanceProfileName;

if (isBlank(ec2InstanceProfileName) && ec2InstanceProfileName != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to StringUtils.isWhitespace(ec2InstanceProfileName)

Copy link
Contributor Author

@S-Saranya1 S-Saranya1 Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the StringUtils(software.amazon.awssdk.utils.StringUtils) doesn't have "isWhitespace" method, it only has "isBlank" method.
Apache Commons Lang StringUtils (from org.apache.commons.lang3.StringUtils) does have an isWhitespace method, but it's not available for this module.

throw SdkClientException.builder()
.message("ec2InstanceProfileName cannot be blank")
.build();
}

this.httpCredentialsLoader = HttpCredentialsLoader.create(PROVIDER_NAME);
this.configProvider =
Expand Down Expand Up @@ -173,6 +183,11 @@ private RefreshResult<AwsCredentials> refreshCredentials() {

try {
LoadedCredentials credentials = httpCredentialsLoader.loadCredentials(createEndpointProvider());

if (apiVersion == ApiVersion.UNKNOWN) {
apiVersion = ApiVersion.EXTENDED;
}
Comment on lines +187 to +189
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this change?

Copy link
Contributor Author

@S-Saranya1 S-Saranya1 Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, apiVersion is set to UNKNOWN. When ec2InstanceProfileName is set by the user, we skip the first GET request to IMDS in the getSecurityCredentials() method. In refreshCredentials(), the code attempts to load credentials using the extended API endpoint first. If successful, we update apiVersion to EXTENDED. This serves two purposes - It records that the extended API is supported for future reference so it prevents unnecessary attempts to use the legacy API in subsequent calls, as we now know the extended API works.(This is from IMDS SEP 2.1)


Instant expiration = credentials.getExpiration().orElse(null);
log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration);

Expand All @@ -186,19 +201,29 @@ private RefreshResult<AwsCredentials> refreshCredentials() {
} catch (Ec2MetadataClientException e) {
if (e.statusCode() == 404) {
log.debug(() -> "Resolved profile is no longer available. Resetting it and trying again.");
resolvedProfile = null;

if (apiVersion == ApiVersion.UNKNOWN) {
apiVersion = ApiVersion.LEGACY;
return refreshCredentials();
} else if (resolveProfileName() == null) {
// Resolved profile name is invalid, reset it and try again
resolvedProfile = null;

profileRetryCount++;
if (profileRetryCount <= MAX_PROFILE_RETRIES) {
log.debug(() -> "Profile name not found, retrying fetching the profile name again.");
return refreshCredentials();
}
} else {
String profileName = resolveProfileName();
throw SdkClientException.builder()
.message(String.format("Invalid EC2 instance profile name: '%s'. " +
"Verify that the profile exists and that your instance " +
"has permission to access it. ",
profileName))
.cause(e)
.build();
}

profileRetryCount++;
if (profileRetryCount <= MAX_PROFILE_RETRIES) {
log.debug(() -> "Profile name not found, retrying fetching the profile name again.");
return refreshCredentials();
}
throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e);
}
throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e);
} catch (RuntimeException e) {
Expand Down Expand Up @@ -327,7 +352,18 @@ private boolean isInsecureFallbackDisabled() {
return configProvider.isMetadataV1Disabled();
}

private String resolveProfileName() {
return ec2InstanceProfileName != null ?
ec2InstanceProfileName :
configProvider.ec2InstanceProfileName();
}

private String[] getSecurityCredentials(String imdsHostname, String metadataToken) {
String profileName = resolveProfileName();
if (profileName != null) {
return new String[]{profileName};
}

if (resolvedProfile != null) {
return new String[]{resolvedProfile};
}
Expand Down Expand Up @@ -383,6 +419,16 @@ public Builder toBuilder() {
*/
public interface Builder extends HttpCredentialsProvider.Builder<InstanceProfileCredentialsProvider, Builder>,
CopyableBuilder<Builder, InstanceProfileCredentialsProvider> {
/**
* Configure the EC2 instance profile name to use for retrieving credentials.
*
* <p>When this is set, the provider will skip fetching the list of available instance profiles
* and use this name directly.
*
* @param ec2InstanceProfileName The EC2 instance profile name to use
*/
Builder ec2InstanceProfileName(String ec2InstanceProfileName);

/**
* Configure the profile file used for loading IMDS-related configuration, like the endpoint mode (IPv4 vs IPv6).
*
Expand Down Expand Up @@ -434,6 +480,7 @@ static final class BuilderImpl implements Builder {
private String asyncThreadName;
private Supplier<ProfileFile> profileFile;
private String profileName;
private String ec2InstanceProfileName;
private Duration staleTime;

private BuilderImpl() {
Expand All @@ -447,6 +494,7 @@ private BuilderImpl(InstanceProfileCredentialsProvider provider) {
this.asyncThreadName = provider.asyncThreadName;
this.profileFile = provider.profileFile;
this.profileName = provider.profileName;
this.ec2InstanceProfileName = provider.ec2InstanceProfileName;
this.staleTime = provider.staleTime;
}

Expand Down Expand Up @@ -515,6 +563,17 @@ public Builder profileName(String profileName) {
public void setProfileName(String profileName) {
profileName(profileName);
}

@Override
public Builder ec2InstanceProfileName(String ec2InstanceProfileName) {
this.ec2InstanceProfileName = ec2InstanceProfileName;
return this;
}

public void setEc2InstanceProfileName(String ec2InstanceProfileName) {
ec2InstanceProfileName(ec2InstanceProfileName);
}


@Override
public Builder staleTime(Duration duration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ public final class Ec2MetadataConfigProvider {

private final Lazy<Boolean> metadataV1Disabled;
private final Lazy<Long> serviceTimeout;
private final Lazy<String> ec2InstanceProfileName;

private Ec2MetadataConfigProvider(Builder builder) {
this.profileFile = builder.profileFile;
this.profileName = builder.profileName;
this.metadataV1Disabled = new Lazy<>(this::resolveMetadataV1Disabled);
this.serviceTimeout = new Lazy<>(this::resolveServiceTimeout);
this.ec2InstanceProfileName = new Lazy<>(this::resolveEc2InstanceProfileName);
}

public enum EndpointMode {
Expand Down Expand Up @@ -127,6 +129,14 @@ public boolean isMetadataV1Disabled() {
public long serviceTimeout() {
return serviceTimeout.getValue();
}

/**
* Resolves the EC2 Instance Profile Name to use.
* @return the EC2 Instance Profile Name or null if not specified.
*/
public String ec2InstanceProfileName() {
return ec2InstanceProfileName.getValue();
}

// Internal resolution logic for Metadata V1 disabled
private boolean resolveMetadataV1Disabled() {
Expand All @@ -146,6 +156,14 @@ private long resolveServiceTimeout() {
.orElseGet(() -> parseTimeoutValue(SdkSystemSetting.AWS_METADATA_SERVICE_TIMEOUT.defaultValue()));
}

private String resolveEc2InstanceProfileName() {
return OptionalUtils.firstPresent(
fromSystemSettingsEc2InstanceProfileName(),
() -> fromProfileFileEc2InstanceProfileName(profileFile, profileName)
)
.orElse(null);
}

// System settings resolution for Metadata V1 disabled
private static Optional<Boolean> fromSystemSettingsMetadataV1Disabled() {
return SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.getBooleanValue();
Expand All @@ -171,6 +189,22 @@ private static Optional<Long> fromProfileFileServiceTimeout(Supplier<ProfileFile
.flatMap(p -> p.property(ProfileProperty.METADATA_SERVICE_TIMEOUT))
.map(Ec2MetadataConfigProvider::parseTimeoutValue);
}

// System settings resolution for EC2 Instance Profile Name
private static Optional<String> fromSystemSettingsEc2InstanceProfileName() {
return SdkSystemSetting.AWS_EC2_INSTANCE_PROFILE_NAME.getNonDefaultStringValue();
}

// Profile file resolution for EC2 Instance Profile Name
private static Optional<String> fromProfileFileEc2InstanceProfileName(Supplier<ProfileFile> profileFile, String profileName) {
try {
return profileFile.get()
.profile(profileName)
.flatMap(p -> p.property(ProfileProperty.EC2_INSTANCE_PROFILE_NAME));
} catch (Exception e) {
return Optional.empty();
}
Comment on lines +204 to +206
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What case is this guarding against?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is guarding against exceptions from aggregated profile suppliers, particularly NoSuchElementException like when there are invalid profiles or when suppliers throw exceptions during normal operation.

}

// Parses a timeout value from a string to milliseconds
private static long parseTimeoutValue(String timeoutValue) {
Expand Down
Loading