☁️

뿌슝빠슝 AWS SDK에서는 키가 필요 없다?

최민석·2025-07-12

뿌슝빠슝 AWS SDK에서는 키가 필요 없다?

main.png


AWS SDK를 쓸 땐 access key랑 secret key를 꼭 넣어야 한다고 알고 있었다.
그래서 S3 모듈 개발시에 빈 주입을 위한 생성자에도 accessKey와 secretKey를 바인딩하도록 구성했다. 그런데 팀장님께서 엑세스 키랑 시크릿 키 없이도 동작한다고 하셔서, 관련 내용을 탐구해봤다.

처음엔 “엥? 진짜요?” 싶었는데, 실제로 테스트해보니 S3 업로드도 잘 되고 모든 게 정상 작동했다.
그래서 그때부터 이 원리를 파기 시작했다.

이 아티클은 그 궁금증의 여정을 간단히 정리한 기록이다.

EC2에선 진짜 키 없이 동작할까?

EC2에 IAM Role만 잘 부여돼 있다면, SDK는 access key, secret key, session token자동으로 가져온다.
그것도 아무 설정 없이, 아무 키 없이.

도대체 어떻게?

핵심은 Instance Metadata Service (IMDS)

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가 받아서 자동으로 서명하고, 갱신하고, 인증까지 다 처리해준다.
덕분에 우리는 코딩할 때 따로 키를 저장할 필요가 없다.

이 흐름을 다이어그램으로 정리하면

aws-sdk-iam-role-flow.png

  1. IAM Role이 STS를 통해 임시 자격증명 발급
  2. SDK가 169.254.169.254 메타데이터로부터 자격증명 조회

이 모든 게 자동으로 이루어진다.

코드 예시

아래는 SesClientS3Client 생성 코드다.
공통점은 모두 DefaultCredentialsProvider를 사용한다는 것.
즉, 키를 명시적으로 넣지 않아도 AWS SDK가 내부에서 알아서 자격증명을 찾아쓴다.

@Bean
public SesClient sesClient() {
    return SesClient.builder()
        .credentialsProvider(DefaultCredentialsProvider.create()) // EC2에서 가져옴
        .region(Region.AP_NORTHEAST_2)
        .build();
}

아래는 내가 만든 AttachmentClient의 두 생성자다.

  • 하나는 로컬 개발 환경 등에서 accessKey/secretKey를 명시적으로 주입하는 버전이고,
  • 하나는 EC2, ECS, Lambda처럼 환경에서 제공하는 자격 증명을 사용하는 버전이다.
// 명시적 자격증명 (로컬용)
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();
    ...
}

SDK가 자격 증명을 찾는 우선순위

DefaultCredentialsProvider는 아래 순서대로 자격증명을 찾는다:

  1. 환경 변수 (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
  2. 자격 증명 파일 (~/.aws/credentials)
  3. EC2 Instance Metadata Service (IMDS) or ECS Task Role
  4. 기타 (Web Identity Token, 프로파일 지정 등)

그래서 로컬 개발 환경에선 .envcredentials 파일을 참고하고,
배포된 EC2에선 알아서 메타데이터에서 키를 가져온다.

이 체계를 잘 이해해두면, 로컬과 서버의 코드가 동일하게 유지되면서도 환경에 따라 다르게 동작할 수 있다.

실제 내부 코드 흐름

코드에서 DefaultCredentialsProvider.create()를 호출하면 실제로 다음과 같은 일들이 벌어진다.

  1. DefaultCredentialsProvider는 내부적으로 LazyAwsCredentialsProvider를 생성한다.
  2. LazyAwsCredentialsProvider는 실제 자격 증명을 제공할 AwsCredentialsProviderChain늦은 시점(lazy) 에 생성한다.
  3. 그 안에는 다음 순서대로 provider가 등록된다:
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 객체로 감싸서 반환하게 된다.


고정 키 사용 시에는 StaticCredentialsProvider

AwsBasicCredentials creds = AwsBasicCredentials.create("AKIA...", "SECRET...");
StaticCredentialsProvider.create(creds);

이 클래스는 resolveCredentials() 호출 시 항상 같은 credentials를 반환한다.
즉, 하드코딩된 키를 사용하는 경우엔 이 경로를 타게 된다.


요약하자면

  • DefaultCredentialsProvider는 내부적으로 AwsCredentialsProviderChain을 만들어,
  • 여러 공급자들을 순차적으로 호출해서 자격 증명을 가져온다.
  • EC2 환경에서는 보통 InstanceProfileCredentialsProvider가 실제 자격 증명을 내려주는 역할을 한다.
  • 하드코딩된 키를 쓰는 경우엔 StaticCredentialsProvider가 정해진 credentials를 리턴한다.

이 흐름을 알면, "SDK가 어떻게 키를 찾는가"를 디버깅하거나 분석하기 훨씬 수월하다.

그래서 뭐가 좋냐

  • 보안이 강해진다
    하드코딩한 키가 없어도 된다. 깃허브에 키 올라갈 일도 전혀 없다.

  • 운영이 편해진다
    키 만료 걱정이나 갱신 주기에 대한 걱정이 필요 없다. SDK가 알아서 해주니까.

  • 코드가 깔끔해진다
    환경변수, 설정파일이 필요 없다.

169.254.169.254는 VIP일까?

많은 사람이 이 주소를 보고 "이거 VIP 아니야?"라고 생각하는데(나도 그럼), 사실 엄밀히 말하면 VIP는 아니다.

  • 이 주소는 RFC 3927에 정의된 Link-Local 주소
  • AWS가 하이퍼바이저 수준에서 내부적으로 라우팅해주기 때문에, 모든 EC2에서 동일하게 접근 가능
  • 하지만 Load Balancer나 Floating IP 같은 VIP 특성은 없다

그러니까, VIP처럼 보이지만 VIP는 아니다 라는 게 결론이다.

마무리

EC2에선 정말 아무 키 없이 AWS SDK를 쓸 수 있다.
그 이유는 인스턴스에 부여된 IAM Role + IMDS 덕분이다.

개인적으로 이걸 알게 된 이후로, 키 없이 바인딩 할 수 있는 생성자들을 모듈에 추가했다. 그리고 실제 배포 환경에선 이런 방식이 가장 권장되는 방법이라고 한다.

의문

개발하면서 한 가지 고민이 들긴 했다.

“그냥 키를 명시적으로 지정해놓는 게 더 나을 수도 있지 않을까?”

물론 IAM Role 기반 자격증명은 보안적으론 좋을 것이다.
근데 이런 구조는 처음 접하는 신입 개발자에겐 보이지 않는 마법처럼 느껴질 수도 있다.
코드엔 credentials가 안 보이는데, API 호출은 잘 되니까...

또, "이 코드가 실제 어떤 자격 증명을 사용 중인지 추적하기 어렵다" 는 피드백도 있었다.
특히 다중 환경(dev/stage/prod)에서 명확하게 어떤 자격이 쓰이고 있는지 파악하려면,
명시적인 설정이 오히려 더 도움이 될 때도 있을 것 같다.

그래서 지금은 온보딩이나 테스트용 코드에는
명시적으로 키를 지정한 버전을 하나 넣어두는 게 더 낫겠다는 결론을 내렸다. 그래서 상기 모듈에도 생성자가 두 개가 있는 것이다. 아마도 AWS 측에서도 그런 이유들 때문에 StaticCredentialsProvider를 만들지 않았을까?

실제 배포 환경에서는 여전히 IMDS + IAM Role 방식을 쓰지만,
초심자를 위한 최소한의 힌트는 남겨두는 게 좋다고 생각한다.