최근에 AWS에서 동작하는 서버에서 다음과 같은 오류가 발생하기 시작했습니다.
com.amazonaws.services.s3.model.AmazonS3Exception: The provided token has expired. (Service: Amazon S3; Status Code: 400; Error Code: ExpiredToken; Request ID: 0NV9YTKYVBCEPCB1; S3 Extended Request ID: TYjnvp4WwoLNm/Eytu+qXleNLFYDYbc7jyr7yt2x8jSGkGG5w0/f8D2TJGgQWNmRCRIwB3ahiZI=; Proxy: null)
그런데 이상한 것은 해당 서버는 Instance Profile 이 연결되어 있어서, 제대로 서버내에서 터미널에서는 S3에 계속 접근이 잘 되는 상황이었습니다. 그런데 계속 TokenExpire 가 발생해서 이상하다고 생각하고 검색을 하다보니, 아래와 같이 도움되는 블로그를 찾게되었습니다.
https://kim-jong-hyun.tistory.com/136?fbclid=IwAR3R4rmTexgu3CuQOHOgidDpZeVHqfGp8A9O4vZLjpz4T1_CN4NDse_0HHE
굉장히 도움이 되었던 내용은 InstanceProfileCredentialsProvider를 이용하는데, 이게 Expire 가 설정되어 있어서 중간에 만료가 될 수 있다는 부분이었습니다. 그리고 거기에 보면 직접 Instance를 넘기면 토큰이 자동으로 Refresh 가 된다라는 얘기였습니다.
그런데 그러면 언제 토큰이 Refresh 가 될까요? InstanceProfileCredentialsProvider 안에는 refresh 함수가 있습니다. 그런데 이 refresh는 기본적으로는 handleError 가 발생하면 호출이됩니다.
public class InstanceProfileCredentialsProvider implements AWSCredentialsProvider, Closeable {
......
private void handleError(Throwable t) {
refresh();
LOG.error(t.getMessage(), t);
}
......
@Override
public void refresh() {
if (credentialsFetcher != null) {
credentialsFetcher.refresh();
}
}
......
}
그리고 이 refresh 함수는 BaseCredentialsFetcher 안에서 영향을 줍니다. 아래 코드와 같이 refresh 를 하면 credentials 가 null이 되어서 needsToLoadCredentials 에서 true를 리턴하게 되므로, getCredentials를 하면 fetchCredentials() 를 호출해서 credential을 다시 가져오게 됩니다.
@SdkInternalApi
abstract class BaseCredentialsFetcher {
public AWSCredentials getCredentials() {
if (needsToLoadCredentials())
fetchCredentials();
if (expired()) {
throw new SdkClientException(
"The credentials received have been expired");
}
return credentials;
}
boolean needsToLoadCredentials() {
if (credentials == null) return true;
if (credentialsExpiration != null) {
if (isWithinExpirationThreshold()) return true;
}
if (lastInstanceProfileCheck != null) {
if (isPastRefreshThreshold()) return true;
}
return false;
}
public void refresh() {
credentials = null;
}
}
그런데 위의 getCredentials() 가 호출이 되어야만 하는데, 저게 언제 되는지는 알 수가 없습니다.
그래서 단순히 해당 블로그의 InstanceProfileCredentialsProvider.getInstance()를 호출하면 좀 이슈가 있을 수 있습니다. 그런데 InstanceProfileCredentialsProvider는 하나의 생성 method를 제공하는데 다음과 같습니다.
public class InstanceProfileCredentialsProvider implements AWSCredentialsProvider, Closeable {
public InstanceProfileCredentialsProvider(boolean refreshCredentialsAsync) {
this(refreshCredentialsAsync, true);
}
private InstanceProfileCredentialsProvider(boolean refreshCredentialsAsync, final boolean eagerlyRefreshCredentialsAsync) {
credentialsFetcher = new InstanceMetadataServiceCredentialsFetcher();
if (!SDKGlobalConfiguration.isEc2MetadataDisabled()) {
if (refreshCredentialsAsync) {
executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setName("instance-profile-credentials-refresh");
t.setDaemon(true);
return t;
}
});
executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
if (shouldRefresh) credentialsFetcher.getCredentials();
} catch (AmazonClientException ace) {
handleError(ace);
} catch (RuntimeException re) {
handleError(re);
}
}
}, 0, ASYNC_REFRESH_INTERVAL_TIME_MINUTES, TimeUnit.MINUTES);
}
}
}
}
refreshCredentialsAsync 가 true 가 넘어가면 Async 스레드가 생성되고 해당 비동기 스레드에서 ASYNC_REFRESH_INTERVAL_TIME_MINUTES 마다 getCredentials 를 호출해서 계속 Token을 재설정하게 됩니다.