
AWS SDK를 쓸 땐 access key랑 secret key를 꼭 넣어야 한다고 알고 있었다.
그래서 S3 모듈 개발시에 빈 주입을 위한 생성자에도 accessKey와 secretKey를 바인딩하도록 구성했다.
그런데 팀장님께서 엑세스 키랑 시크릿 키 없이도 동작한다고 하셔서, 관련 내용을 탐구해봤다.
처음엔 “엥? 진짜요?” 싶었는데, 실제로 테스트해보니 S3 업로드도 잘 되고 모든 게 정상 작동했다.
그래서 그때부터 이 원리를 파기 시작했다.
이 아티클은 그 궁금증의 여정을 간단히 정리한 기록이다.
EC2에 IAM Role만 잘 부여돼 있다면, SDK는 access key, secret key, session token을 자동으로 가져온다.
그것도 아무 설정 없이, 아무 키 없이.
도대체 어떻게?
EC2 인스턴스 안에서 AWS SDK가 실행되면, 이 주소로 HTTP 요청을 보낸다:
http://169.254.169.254/latest/meta-data/iam/security-credentials/{role-name}
여기가 바로 IMDS, 즉 인스턴스 메타데이터 서비스다.
이 주소는 EC2 인스턴스 내부에서만 접근 가능한 로컬 IP다. 외부에선 접근이 불가능하다.
이 엔드포인트는 해당 인스턴스에 부여된 IAM Role의 이름을 키로, 아래와 같은 JSON을 내려준다:
{
"AccessKeyId": "ASIA...",
"SecretAccessKey": "ABC...",
"Token": "IQoJ...",
"Expiration": "2025-07-12T12:34:56Z"
}
이걸 SDK가 받아서 자동으로 서명하고, 갱신하고, 인증까지 다 처리해준다.
덕분에 우리는 코딩할 때 따로 키를 저장할 필요가 없다.

이 모든 게 자동으로 이루어진다.
아래는 SesClient와 S3Client 생성 코드다.
공통점은 모두 DefaultCredentialsProvider를 사용한다는 것.
즉, 키를 명시적으로 넣지 않아도 AWS SDK가 내부에서 알아서 자격증명을 찾아쓴다.
@Bean
public SesClient sesClient() {
return SesClient.builder()
.credentialsProvider(DefaultCredentialsProvider.create()) // EC2에서 가져옴
.region(Region.AP_NORTHEAST_2)
.build();
}
아래는 내가 만든 AttachmentClient의 두 생성자다.
// 명시적 자격증명 (로컬용)
public AttachmentClient(
Region s3Region,
String s3AccessKey,
String s3SecretKey,
String s3BucketName,
String s3BaseUrl
) {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(s3AccessKey, s3SecretKey);
StaticCredentialsProvider staticCredentialsProvider = StaticCredentialsProvider.create(awsCreds);
...
this.s3Client = S3Client.builder()
.region(this.s3Region)
.credentialsProvider(staticCredentialsProvider)
.build();
...
}
// 기본 자격 증명 체인 (EC2/ECS/Lambda)
public AttachmentClient(
Region s3Region,
String s3BucketName,
String s3BaseUrl
) {
DefaultCredentialsProvider defaultCredentialsProvider = DefaultCredentialsProvider.create();
...
this.s3Client = S3Client.builder()
.region(this.s3Region)
.credentialsProvider(defaultCredentialsProvider)
.build();
...
}
DefaultCredentialsProvider는 아래 순서대로 자격증명을 찾는다:
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)~/.aws/credentials)그래서 로컬 개발 환경에선 .env나 credentials 파일을 참고하고,
배포된 EC2에선 알아서 메타데이터에서 키를 가져온다.
이 체계를 잘 이해해두면, 로컬과 서버의 코드가 동일하게 유지되면서도 환경에 따라 다르게 동작할 수 있다.
코드에서 DefaultCredentialsProvider.create()를 호출하면 실제로 다음과 같은 일들이 벌어진다.
DefaultCredentialsProvider는 내부적으로 LazyAwsCredentialsProvider를 생성한다.LazyAwsCredentialsProvider는 실제 자격 증명을 제공할 AwsCredentialsProviderChain을 늦은 시점(lazy) 에 생성한다.new AwsCredentialsProvider[] {
SystemPropertyCredentialsProvider.create(),
EnvironmentVariableCredentialsProvider.create(),
WebIdentityTokenFileCredentialsProvider.builder().build(),
ProfileCredentialsProvider.builder().build(),
ContainerCredentialsProvider.builder().build(),
InstanceProfileCredentialsProvider.builder().build()
}
이 순서대로 순회하며, 첫 번째로 성공적으로 자격 증명을 제공하는 provider가 선택된다.
AwsCredentialsProviderChain이 클래스의 핵심 메서드는 다음과 같다:
@Override
public AwsCredentials resolveCredentials() {
for (IdentityProvider provider : credentialsProviders) {
try {
AwsCredentialsIdentity credentials = provider.resolveIdentity().join();
return CredentialUtils.toCredentials(credentials);
} catch (RuntimeException e) {
// 실패하면 다음 provider로 넘어간다
}
}
throw new SdkClientException("자격 증명을 찾을 수 없습니다.");
}
즉, 실제 자격 증명을 바인딩하는 순간은 provider.resolveIdentity()를 통해 이루어지고,
그 결과를 AwsCredentials 객체로 감싸서 반환하게 된다.
StaticCredentialsProviderAwsBasicCredentials creds = AwsBasicCredentials.create("AKIA...", "SECRET...");
StaticCredentialsProvider.create(creds);
이 클래스는 resolveCredentials() 호출 시 항상 같은 credentials를 반환한다.
즉, 하드코딩된 키를 사용하는 경우엔 이 경로를 타게 된다.
DefaultCredentialsProvider는 내부적으로 AwsCredentialsProviderChain을 만들어,InstanceProfileCredentialsProvider가 실제 자격 증명을 내려주는 역할을 한다.StaticCredentialsProvider가 정해진 credentials를 리턴한다.이 흐름을 알면, "SDK가 어떻게 키를 찾는가"를 디버깅하거나 분석하기 훨씬 수월하다.
보안이 강해진다
하드코딩한 키가 없어도 된다. 깃허브에 키 올라갈 일도 전혀 없다.
운영이 편해진다
키 만료 걱정이나 갱신 주기에 대한 걱정이 필요 없다. SDK가 알아서 해주니까.
코드가 깔끔해진다
환경변수, 설정파일이 필요 없다.
169.254.169.254는 VIP일까?많은 사람이 이 주소를 보고 "이거 VIP 아니야?"라고 생각하는데(나도 그럼), 사실 엄밀히 말하면 VIP는 아니다.
그러니까, VIP처럼 보이지만 VIP는 아니다 라는 게 결론이다.
EC2에선 정말 아무 키 없이 AWS SDK를 쓸 수 있다.
그 이유는 인스턴스에 부여된 IAM Role + IMDS 덕분이다.
개인적으로 이걸 알게 된 이후로, 키 없이 바인딩 할 수 있는 생성자들을 모듈에 추가했다. 그리고 실제 배포 환경에선 이런 방식이 가장 권장되는 방법이라고 한다.
개발하면서 한 가지 고민이 들긴 했다.
“그냥 키를 명시적으로 지정해놓는 게 더 나을 수도 있지 않을까?”
물론 IAM Role 기반 자격증명은 보안적으론 좋을 것이다.
근데 이런 구조는 처음 접하는 신입 개발자에겐 보이지 않는 마법처럼 느껴질 수도 있다.
코드엔 credentials가 안 보이는데, API 호출은 잘 되니까...
또, "이 코드가 실제 어떤 자격 증명을 사용 중인지 추적하기 어렵다" 는 피드백도 있었다.
특히 다중 환경(dev/stage/prod)에서 명확하게 어떤 자격이 쓰이고 있는지 파악하려면,
명시적인 설정이 오히려 더 도움이 될 때도 있을 것 같다.
그래서 지금은 온보딩이나 테스트용 코드에는
명시적으로 키를 지정한 버전을 하나 넣어두는 게 더 낫겠다는 결론을 내렸다.
그래서 상기 모듈에도 생성자가 두 개가 있는 것이다.
아마도 AWS 측에서도 그런 이유들 때문에 StaticCredentialsProvider를 만들지 않았을까?
실제 배포 환경에서는 여전히 IMDS + IAM Role 방식을 쓰지만,
초심자를 위한 최소한의 힌트는 남겨두는 게 좋다고 생각한다.