"EC2에는 IAM Role이 붙어 있는데, 왜 컨테이너 안의 Spring 애플리케이션은 AWS 자격증명을 못 가져올까?"
S3 Presigned URL을 생성하는 API에서 AWS 자격증명을 찾지 못하는 문제가 있었습니다.
처음에는 S3 권한 문제라고 생각했습니다. IAM Role에 s3:GetObject, s3:PutObject 같은 권한이 빠졌거나, 애플리케이션에 환경변수가 없어서 생긴 문제처럼 보였거든요.
그런데 EC2 호스트에서는 IAM Role 자격증명을 정상적으로 가져오고 있었습니다. 호스트에서 aws sts get-caller-identity를 실행하면 아래처럼 assumed-role ARN이 반환됐습니다.
{
"UserId": "AROAXXXXXXXXXXXXX:botocore-session-1234567890",
"Account": "<account-id>",
"Arn": "arn:aws:sts::<account-id>:assumed-role/<role-name>/<session-name>"
}
문제는 Docker 컨테이너 내부에서만 발생했습니다.
이번 글에서는 Docker 컨테이너에서 EC2 IAM Role 자격증명을 가져오지 못할 때, 어떤 순서로 확인하면 좋은지 가볍게 정리해봅니다.
결론부터 말하면, 원인은 EC2 Metadata Options의 IMDSv2 hop limit이었습니다.
EC2 호스트에서 직접 IMDS에 접근할 때는 hop limit이 1이어도 문제가 없었습니다. 하지만 애플리케이션은 Docker 컨테이너 안에서 실행되고 있었고, 컨테이너는 호스트와 다른 네트워크 경계를 한 번 더 거칩니다. 이 상태에서 IMDSv2 토큰 응답의 hop limit이 1이면, 호스트에서는 자격증명을 가져오지만 컨테이너 안에서는 토큰 응답을 받지 못할 수 있습니다.
그래서 해결은 단순했습니다. IMDSv2는 유지하되, 컨테이너 환경에 맞게 http_put_response_hop_limit을 2로 올려주는 것이었습니다.
환경
항목 내용
| 실행 환경 | EC2 위에서 Docker 컨테이너로 실행되는 Spring 애플리케이션 |
| AWS 인증 방식 | EC2 Instance Profile, IAM Role |
| 문제 기능 | S3 이미지 URL 생성, Presigned URL 생성 |
| 핵심 에러 | Unable to load AWS credentials from any provider in the chain |
| 실제 원인 | IMDSv2 응답 hop limit이 컨테이너 환경에 맞지 않음 |
| 해결 방향 | http_put_response_hop_limit 값을 1에서 2로 변경 |
문제 상황
애플리케이션에서 S3 Presigned URL을 만들 때 아래와 같은 에러가 발생했습니다.
Unable to load AWS credentials from any provider in the chain
로그를 더 따라가면 마지막 쪽에 이런 메시지도 보였습니다.
EC2ContainerCredentialsProviderWrapper ... Unauthorized (Status Code: 401)
AWS SDK는 자격증명을 찾을 때 여러 후보를 순서대로 확인합니다. 환경변수, 프로파일, 컨테이너 자격증명, EC2 메타데이터 같은 것들을 차례대로 보는 방식입니다.
EC2에 IAM Role을 붙여두면 애플리케이션에 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY를 직접 넣지 않아도 됩니다. 대신 애플리케이션은 EC2의 Instance Metadata Service, 줄여서 IMDS를 통해 임시 자격증명을 가져옵니다.
그래서 이 에러를 보면 질문은 이렇게 바뀝니다.
AWS SDK가 IMDS에서 자격증명을 가져오지 못하는 이유가 뭘까?
처음 의심한 지점
처음에는 S3 권한을 의심했습니다.
Presigned URL 생성 중에 터진 문제였기 때문에 IAM Policy에 S3 권한이 없어서 실패한 것처럼 보였어요. 하지만 이 경우에는 S3 요청까지 가지도 못했습니다.
문제는 더 앞단이었습니다.
AWS 자격증명 조회 실패
↓
S3 클라이언트 생성 또는 요청 준비 실패
↓
Presigned URL 생성 실패
즉, s3:GetObject 권한이 있느냐 없느냐를 보기 전에 애플리케이션이 IAM Role 자격증명 자체를 얻지 못하고 있었습니다.
다음으로는 환경변수를 확인했습니다.
env | grep AWS
컨테이너 안에 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY가 없었습니다. 하지만 이건 이상한 게 아니었습니다. EC2 IAM Role 기반으로 인증하는 구조라면 장기 Access Key를 환경변수에 넣지 않는 것이 오히려 정상입니다.
그래서 호스트와 컨테이너를 나눠서 확인하기로 했습니다.
EC2 호스트에서는 정상인지 확인하기
먼저 EC2 호스트에서 IMDSv2 토큰을 발급받아 봅니다.
TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
그 다음 IAM Role 이름을 조회합니다.
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
여기서 EC2에 연결된 Role 이름이 나오면, 호스트 레벨에서는 IMDS 접근이 정상입니다.
추가로 STS 호출도 확인할 수 있습니다.
aws sts get-caller-identity
호스트에서는 이 명령들이 정상적으로 동작했습니다.
이제 중요한 건 컨테이너 내부입니다.
컨테이너 안에서는 실패했습니다
실행 중인 Spring 컨테이너 안으로 들어갑니다.
docker exec -it <spring-container> sh
그리고 호스트에서 했던 것과 같은 방식으로 IMDSv2 토큰을 요청합니다.
TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
컨테이너 안에서는 이 요청이 멈추거나 실패했습니다.
여기서 원인이 꽤 좁혀졌습니다.
위치 IMDSv2 조회 결과
| EC2 호스트 | 성공 |
| Docker 컨테이너 내부 | 실패 |
IAM Role 자체가 없거나 Policy가 잘못됐다면 호스트에서도 문제가 보여야 합니다. 그런데 호스트는 정상이고 컨테이너만 실패한다면, IAM 권한보다는 컨테이너 네트워크와 IMDSv2 설정 사이의 문제를 의심해야 합니다.
실제 원인
EC2 Metadata Options를 확인해보니 아래처럼 설정되어 있었습니다.
http_tokens = required
http_put_response_hop_limit = 1
http_tokens = required는 IMDSv2만 허용하겠다는 뜻입니다. 이 설정 자체는 좋은 방향입니다. IMDSv1보다 IMDSv2를 강제하는 편이 더 안전하니까요.
문제는 http_put_response_hop_limit = 1이었습니다.
MDSv2는 먼저 토큰을 발급받기 위해 PUT /latest/api/token 요청을 보냅니다. 이때 EC2 메타데이터 서비스가 돌려주는 토큰 응답에는 hop limit이 적용됩니다.
EC2 호스트 프로세스가 직접 IMDS에 접근할 때는 hop limit이 1이어도 보통 문제가 없습니다.
하지만 Docker 컨테이너는 호스트의 네트워크 네임스페이스와 브리지 네트워크를 거칠 수 있습니다. 컨테이너 입장에서는 응답이 한 번 더 네트워크 경계를 지나와야 하는 셈입니다.
그래서 컨테이너 환경에서 IMDSv2를 강제한다면 hop limit을 2로 두는 것이 일반적입니다. AWS 문서에서도 컨테이너 환경에서 IMDSv2가 필요할 때 metadata response hop limit을 2로 설정하는 것을 권장합니다.
정리하면 이 문제는 S3 권한 문제가 아니었습니다.
S3 권한 부족 X
AWS Access Key 환경변수 누락 X
EC2 IAM Role 미연결 X
Docker 컨테이너 내부에서 IMDSv2 토큰 응답을 받지 못한 문제 O
해결 방법
운영 중인 EC2 인스턴스라면 Metadata Options를 바로 수정할 수 있습니다.
AWS_PROFILE=<profile> aws ec2 modify-instance-metadata-options \
--instance-id <instance-id> \
--http-endpoint enabled \
--http-tokens required \
--http-put-response-hop-limit 2 \
--region ap-northeast-2
적용 결과는 아래 명령으로 확인합니다.
AWS_PROFILE=<profile> aws ec2 describe-instances \
--instance-ids <instance-id> \
--region ap-northeast-2 \
--query 'Reservations[0].Instances[0].MetadataOptions'
기대하는 값은 아래와 같습니다.
{
"State": "applied",
"HttpTokens": "required",
"HttpPutResponseHopLimit": 2,
"HttpEndpoint": "enabled"
}
이 명령은 EC2 내부에서 실행하면 권한이 없어서 실패할 수 있습니다.
애플리케이션용 EC2 Role에는 보통 ec2:ModifyInstanceMetadataOptions 권한을 주지 않습니다. 그러니 로컬 개발 환경이나 운영용 관리자 프로파일처럼 EC2 설정을 바꿀 수 있는 권한이 있는 주체에서 실행하는 편이 자연스럽습니다.
Terraform에도 반영하기
수동으로 인스턴스만 고치면 나중에 새 인스턴스가 뜰 때 같은 문제가 다시 생길 수 있습니다.
그래서 Terraform이나 Launch Template에도 같은 설정을 반영해야 합니다.
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 2
}
ASG를 사용하고 있다면 특히 중요합니다.
운영 중인 인스턴스 하나만 고쳐도 당장은 해결됩니다. 하지만 Auto Scaling Group이 새 인스턴스를 만들 때 Launch Template에 hop limit이 1로 남아 있으면, 새로 뜬 컨테이너에서 같은 장애가 다시 발생할 수 있습니다.
즉, 조치는 두 단계로 보는 게 좋습니다.
단계 목적
| 운영 중 인스턴스 수정 | 지금 발생한 장애를 즉시 해결 |
| Terraform 또는 Launch Template 수정 | 새 인스턴스에서도 같은 문제가 재발하지 않게 방지 |
확인한 결과
설정을 바꾼 뒤 컨테이너 안에서 다시 IMDSv2 토큰을 요청했습니다.
docker exec -it <spring-container> sh
TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
이번에는 IAM Role 이름이 정상적으로 반환됐습니다.
그 다음 애플리케이션에서 문제가 발생했던 API를 다시 호출했습니다.
- 이미지 URL 생성 API
- S3 Presigned URL 생성 API
- 사용자 프로필 조회처럼 이미지 URL을 포함하는 API
기존에 보이던 Unable to load AWS credentials 에러가 사라졌고, S3 관련 기능도 정상 동작했습니다.
빠르게 진단하는 순서
비슷한 문제가 생기면 아래 순서로 보면 좋습니다.
1. 에러가 권한 문제인지 자격증명 조회 문제인지 나누기
아래 메시지가 보이면 S3 권한 자체보다 자격증명 조회 실패를 먼저 의심합니다.
Unable to load AWS credentials from any provider in the chain
S3 API가 거부된 것인지, S3를 호출하기 전에 AWS SDK가 자격증명을 못 찾은 것인지 구분해야 합니다.
2. 호스트에서 IMDS 조회하기
TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
호스트에서도 실패하면 IAM Role 연결, IMDS endpoint 활성화 여부, EC2 Metadata Options를 먼저 봅니다.
3. 컨테이너 안에서 같은 명령 실행하기
docker exec -it <container> sh
컨테이너에서만 실패한다면 Docker 네트워크와 IMDSv2 hop limit 쪽을 의심합니다.
4. Metadata Options 확인하기
aws ec2 describe-instances \
--instance-ids <instance-id> \
--query 'Reservations[0].Instances[0].MetadataOptions'
컨테이너 환경에서 HttpTokens가 required인데 HttpPutResponseHopLimit이 1이면 이 글의 케이스와 같은 문제일 가능성이 높습니다.
돌아보며
이번 문제는 에러 메시지만 보면 S3 권한 문제처럼 보였습니다. 하지만 실제로는 S3까지 가기 전, AWS SDK가 EC2 IAM Role 자격증명을 가져오는 단계에서 막힌 문제였습니다.
이런 문제를 볼 때는 "권한이 있냐 없냐"만 보면 오래 헤맬 수 있습니다.
더 빠른 접근은 실행 위치를 나누는 것입니다.
EC2 호스트에서는 되는가?
Docker 컨테이너 안에서도 되는가?
이 두 질문만으로도 IAM Role 문제인지, 컨테이너 네트워크 문제인지 꽤 빠르게 좁힐 수 있었습니다.
컨테이너에서 EC2 IAM Role을 사용하는 구조라면 아래 조합을 기억해두면 좋습니다.
http_tokens = required
http_put_response_hop_limit = 2
IMDSv2를 강제하는 것은 유지하되, 컨테이너가 토큰 응답을 받을 수 있도록 hop limit을 맞춰주는 것이 핵심이었습니다.
참고
- AWS EC2 User Guide, Configure the Instance Metadata Service options
- AWS EC2 User Guide, Configure instance metadata options for new instances
'AWS' 카테고리의 다른 글
| Cloud Map private DNS에 .local을 붙였더니 왜 조회가 실패했을까 (0) | 2026.03.02 |
|---|