문제 상황
•
어떤 것을 하려다가 문제가 발생했는가?
◦
수요지식회 리뉴얼을 마무리 하고자 DEV 환경에서 일주일간 베타테스트 후, 메인에 베포를 하였는데 갑자기 메인에서만 이미지를 올리면 S3 연결에 실패했다는 메시지와 함께 500 에러 발생..
•
발생한 환경, 프로그램
◦
PROD 에서만…
•
발생한 문제(에러)
◦
500 internal server error
1.
Security Path Error (?)
2025-07-22 00:10:09 ERROR [main] {} CustomAuthenticationEntryPoint:
Authentication Fail: .. Request Uri : /latest/meta-data/iam/security-credentials
2025-07-22 00:10:09 INFO [main] {} LogAspect:
SecurityExceptionHandlerManager .. .. 로그인 정보가 유효하지 않습니다. 다시 로그인해주세요, isRedirect: true
Java
복사
2.
SDK Error - S3 업로드 에러
: S3에 접근하기에 필요한 권한이 없는 상황으로 분석됨
2025-07-22 00:10:09 ERROR [main] {} ThumbnailStorageService:
SDK Error during upload (non-S3): Bucket={실제 버킷 이름}, Key={생성한 파일 이름},
Error=Unable to load credentials from any of the providers in the chain AwsCredentialsProviderChain(credentialsProviders=[SystemPropertyCredentialsProvider(), EnvironmentVariableCredentialsProvider(), WebIdentityTokenCredentialsProvider(), ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])), ContainerCredentialsProvider(), InstanceProfileCredentialsProvider()]) : [SystemPropertyCredentialsProvider():
Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., EnvironmentVariableCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., WebIdentityTokenCredentialsProvider(): Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set., ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])): Profile file contained no credentials for profile 'default': ProfileFile(sections=[]), ContainerCredentialsProvider(): Cannot fetch credentials from container - neither AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables are set., InstanceProfileCredentialsProvider(): Failed to load credentials from IMDS.]
2025-07-22 00:10:09 INFO [main] {} ExceptionController:
[ServiceException] Internal Server Error : S3 업로드 중 에러가 발생했습니다.
Java
복사
원인
•
추정되는 원인
◦
S3 추가한 로직 상에서 dev와 prod에 서로 다르게 적용된 정책이 있을수도..?
◦
S3에 대해 IAM Role에 설정한 정책이 잘못 되었을 수도..?
◦
내가 모르는 dev와 prod의 ec2 환경 설정이 다른 부분이 있을 수도..?
•
실제 원인
◦
두 서버 모두 iptables 설정 우선순위에 따라 요청이 무조건 백엔드 서버로 가로채지고 있었음
◦
prod와 dev 간의 차이가 아닌, iptables에 대한 설정 자체의 문제였음
◦
일단 확실한 이유는 찾았지만, 근본적으로 왜 그렇게 동작했는지 두 서버간의 다른 설정을 찾지는 못함..
⇒ 처음에는 왜 prod 서버만 안되는가가 의문이었는데, 나중엔 왜 dev가 통신이 잘되고 있었지? 로 결론이 남..
해결 과정 시나리오
: 최종 해결을 위한 시행착오(optional)
정말 당황함… 진짜 모르겠다… 종민님이랑 순형님이랑 며칠동안 여러 부분을 확인해봤는데.. 감도 안왔다…
프롬프팅 과정 또한 상세(?)하게 넣었기 때문에 길다면 4번 부분은 스킵해도 OK (feat. gemini의 배신)
1.
S3 연결을 위해 추가한 로직 확인
a.
S3Config.java 파일 상에서 { local }과 { dev, prod }에 따라 설정을 달리한 부분에 대해 dev와 prod 설정이 동일한 것을 확인
b.
application-dev.yml, application-prod.yml 의 credential 관련하여 OIDC 형식으로 인증 방식을 지정해 둔 부분 주석 처리 → 효과 없음
c.
이외에도, dev와 prod에 따라 설정을 달리한 부분이 있는지 확인
→ demo를 제외한 나머지 로직에서 다른 설정이 없는 것을 모두 확인
2.
S3 관련하여 OIDC로의 연결을 위해 role에 추가한 내용 확인
a.
dev와 prod 서버 모두 같은 cabi-codedeploy-role을 사용
b.
해당 role에 S3 관련한 정책을 생성했는데, 다시 확인해보니 내용 문제 없음
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3-object-접근용-정책이름",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::{s3-bucket-name}/*"
},
{
"Sid": "S3-bucket-접근용-정책이름",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::{s3-bucket-name}"
}
]
}
JSON
복사
두개의 정책 설정 중, 전자는 이미지 저장 및 삭제 관련한 내용이고, 후자는 추후 스케쥴러로 버킷 전체 리스트를 순회하는 기능으로 확장하기 위한 내용이다.
3.
인프라 환경 확인 in AWS
a.
dev와 prod의 EC2 설정부터, 보안 그리고 네트워킹을 위주로 확인
•
prod(3개)와 dev(4개)의 보안 그룹이 한가지 다른 것을 확인
•
하지만, dev에 추가적으로 연결된 보안 그룹(인바운드 규칙)은 prod의 db 데이터를 dump 씌우기 위한 접근 권한임을 확인 ⇒ 현재 문제와 관련 없음
b.
S3에 연결된 IAM role인 cabi-codedeploy-role의 기존에 연결되어있는 정책 내용 확인
•
이 부분은 확인이 불가능함… 재단 측에서 권한을 없앴기에 조회나 확인 조차 불가능한 상황
지금 기록하면서 다시 생각해보면.. 여기에 우리가 인지하지 못한 규칙이 추가되었을 수도 있다는.. 의문이…
4.
Google Gemini 무한 프롬프팅 시작 (일주일동안 나를 배신한 이 친구와의 대화를 아주 간단히 정리)
a.
각 서버에서 다음 요청 시, 연결된 IAM 정책 확인 (IMDS 요청 확인)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
Bash
복사
이때, 169.254.169.254 는 AWS의 Internal IP 주소로, EC2에서만 조회가 가능한 주소이다. (IMDS)
prod와 dev 모두 해당 요청 시, cabi-codedeploy-role 가 뜨는 것을 모두 확인
(처음에 prod에서는 확인할 수 없다고 하여, .aws/credentials 의 값을 dev와 동일하게 해주니 다시 해당 정책이 동일하게 떴음. 설정을 건드렸으니, 혹시몰라 prod 서버를 재배포 했으나 문제가 해결되지 않음)
b.
VPC 엔드포인트 확인
⇒ 우리 서버는 VPC 엔트포인트를 사용하고 있지 않으며, 이와 관련된 추가적인 설정이 있는지 확인해보았으나 차이점이 없는것을 확인
c.
PROXY 설정 확인
env | grep -i proxy
Bash
복사
prod와 dev 설정 모두 동일함을 확인
d.
Java 프로세스 확인
# 실행중인 pid 확인 명령
ps aux | grep java
# 찾은 pid 값을 넣어 확인 명령
ps -ww -o cmd -p {java-pid}
Bash
복사
prod와 dev 모두 다음과 같은 값임을 확인하며 실행중인 어플리케이션의 방식이 동일함을 확인
java -jar -Dspring.profiles.active={dev/prod} -javaagent:/pinpoint-agent/pinpoint-bootstrap-2.5.2.jar -Dpinpoint.agentId={각 서버의 ID} -Dpinpoint.applicationName={각 서버의 app 이름} -Dpinpoint.profiler.profiles.active=release {jar 파일 이름} --spring.config.location=file:{각 application yaml 파일 위치}
Bash
복사
e.
각 서버의 application.yml 파일의 설정 확인
# 1. Java 시스템 속성을 이용한 프록시 설정 확인
system:
properties:
https.proxyHost: proxy.example.com # 👈 이런 설정이 있는지?
...
# 2. Spring Cloud AWS의 프록시 설정 확인
cloud:
aws:
http-client:
proxy:
host: proxy.example.com # 👈 또는 이런 설정이 있는지?
...
YAML
복사
확인 결과, 위와 같은 설정이 모두 없는 것을 확인
OIDC 형태로 되어있기 때문에, credentials 부분에 이와 관련된 설정을 해둔 것이 있어 이 부분을 없애고 재배포 후 확인해보았으나 문제 해결되지 않음
f.
Dev와 Prod 서브넷(Subnet)에 연결된 네트워크 ACL의 규칙 확인
•
각 서버의 서브넷의 NACL 확인을 해보았으나, 한개의 목록을 발견, 즉 같은 환경임을 확인
•
g.
curl로 docker 내부로 요청을 쏘아본 결과, 여기서 dev와 prod의 설정이 다른 것을 확인!!
⇒ 이 부분까지 팀원들 각자 프롬프팅을 하며 확인하였으나, 정확한 원인과 이에 대한 설정을 더이상 확인할 방법을 찾지 못함..
이 다른 부분이 결국 진짜 원인이었으며, 이 부분에 대해서는 6번에서 상세히 설명!!
⇒ 길고 긴 프롬프팅과 확인을 하면서 점점 미궁에 빠짐.. 점점 내가 모르는 설정이 꼬여있나.. 내가 해결할 수 있나.. 라는 생각이 들며.. 해당 기능을 없애야 하나.. 이런 생각까지 들었다.
5.
일단 prod에서 발생하는 시큐리티 관련 uri에 대해 에러가 나는 부분을 수정
•
다음과 같은 두개의 uri에서 각각 에러가 나서 임시로 PUBLIC_ENDPOINT에 추가
public static final String[] PUBLIC_ENDPOINTS = {
"another uri"... ,
// 추가한 부분
"/latest/meta-data/iam/security-credentials/**",
"/latest/api/token",
};
Java
복사
왜 이러한 uri로 요청되고 있는지 이해가 되지 않음.. 하지만 일단은 추가해서 해당 에러를 해결
6.
소현언니의 도움을 받아 EC2 자체가 아닌 배포되고 있는 도커 환경에서 IMDS 설정 확인 → 원인 파악
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
Bash
복사
이때, 169.254.169.254 는 AWS의 Internal IP 주소로, EC2에서만 조회가 가능한 주소이다. (IMDS)
docker 내부로 exec 명령어를 통해 들어가서 각 서버의 컨테이너에서 확인을 해본 결과, 요청이 다르게 나온 것을 확인!!
•
DEV 환경 : cabi-codedeploy-role 정상적으로 출력, 내부의 값을 확인해본 결과 key 값들이 정상적으로 출력됨을 확인
•
PROD 환경 : { 404 NOT FOUND, … } 으로 반환됨
다음과 같이 상황을 정리할 수 있다.
a.
먼저, 5번에서 가졌던 시큐리티 uri에 관련한 의문(왜 해당 url로 요청을?)이 풀림
⇒ 도커 내부에서 http로 오는 모든 요청에 대해 백엔드 포트로 가로채지고 있어, 백엔드 내부에서는 해당 uri에 대한 설정이 없으니 404 응답과 함께 에러를 반환하고 있었음
잠깐! 백엔드 포트로 가로채지는걸 어떻게 알았냐고?
⇒ 반환되는 에러 값의 형태가 백엔드에서 응답하는 json 형태와 같아 의심하기 시작했으며, 해당 요청 시, 응답 헤더의 내용과 prod 로그 상에서 시큐리티 에러가 오는 것을 보고 확신하게 되었다.
(응답 헤더에 출력된 내용 중, Apache-Coyote이 Spring Boot에 내장된 Tomcat 웹 서버의 이름)
b.
dev에서는 해당 credential에 대해 정상적으로 요청이 받아들여지고 있었기에 S3와 연결이 정상적으로 진행되고 있었던 것임
하지만, 그렇다면 왜? prod에서는 어떤 설정이 다르길래? dev는 어떠한 이유로 되고 있는걸까?
7.
원인은 확실히 알았으니, 이를 기반으로 확인해보자.
a.
해당 환경에서의 요청 관련한 port 설정을 확인해보자
⇒ 다른 부분을 찾지 못함..
b.
deploy 과정을 다시 한번 확인해보자
⇒ 문제 없음.. prod와 dev의 배포 과정이 서버 이름이랑 각각의 yaml 파일 등을 제외하고 모든 설정이 동일함을 코드에서 모두 확인…
c.
그렇다면 일단 prod 내부에서 인식하지 못하는 credential 부분은 deploy 상에서 env로 인식하도록 설정을 해보면 작동하는지 확인해보자. feat.소현언니
•
배포 시 작동되는 스크립트인 deploy.sh 에서 env로 값을 설정할 수 있도록 변경
•
다만, 이 부분은 절대 공개되서는 안되는 값이며, public한 레포에서 config으로 빼거나 할 수 있는 상황이 아님
•
따라서, EC2 내부에서 exec 명령으로 컨테이너 내부에 들어가 해당 env 값에 대한 코드를 vim으로 주입 후, 컨테이너 상에서 재배포 실행
•
해당 env의 값은 다음 명령을 통해 구한 키 값을 복사해서 직접 넣는 형식으로 진행
# 요청 명령
docker exec -it {컨테이너 이름} curl http://169.254.169.254/latest/meta-data/iam/security-credentials/cabi-codedeploy-role
# 다음과 같은 형식의 응답에서 key 값을 찾아 넣음
{
"Code" : "Success",
"LastUpdated" : "2025-07-26T--time--",
"Type" : "type value~",
"AccessKeyId" : "access key value~", # 복사한 값
"SecretAccessKey" : "secret key value~", # 복사한 값
"Token" : "token value~", # 복사한 값
"Expiration" : "2025-07-27T--time--"
}
Bash
복사
•
성공!! 정상적으로 이미지 기능이 작동한다!!
⇒ 하지만, 이는 임시적인 방법이며 확인용일 뿐, 깃허브에서 재배포라도 하는 경우에는 해당 기능이 다시 작동하지 않는다…
8.
두가지 해결 방법 feat. SOS
해결 방법은 다음과 같이 두가지가 있다.
a.
당장 github secret key 값으로 등록하여 deploy 상에서 해당 값을 env로 인식하도록 설정하도록 코드를 수정한다.
b.
다시 차근차근히.. 근본적인 문제를 찾아 해결한다…
전자는 해당 키 값이 만료된다면 언제 다시 문제가 생길지 모른다. 특히 보안을 위해 OIDC 형태로 구축되어 있는 AWS 환경에서 키 값으로 요청을 주고받는다? 이것은 모순이다.. 그래서 다음과 같은 SOS를…
SOS 요청 메시지,,, to. 귀신잡는 백엔드 채널…
9.
시원이와 함께 다시 확인하며 추가적인 프롬프팅 진행 → 해결 완료!!
a.
이제 확실히 원인을 알았으니, 이를 기반으로 인프라 여왕인 시원이와 함께 다시 접근하였다.
b.
iptables 관련하여 모든 인아웃 바운드 규칙과 내부에서 설정한 포트 관련한 정보를 확인
sudo iptables -t nat -L -n -v
Bash
복사
다음 명령에 따라 prod와 dev에 설정된 iptables의 다른 값들을 찾아보았다.
다른 부분이 존재하여, 이 부분을 설정을 동일하도록 추가설정이 있던 dev 설정을 삭제도 해보고, 확인해보았으나, 해결되지 않았다.
다시 원상복구!
c.
일주일동안 나를 배신했던 Google Gemini에게 상황을 정확한 원인과 함께 설명했다.
다음과 같은 방법을 추천해줬다… 이게 성공?!
i.
현재 PREROUTING 설정되어 있는 iptables를 확인해보았다.
# 다음 명령어로 현재 prod와 dev의 iptables 상황 확인
sudo iptables -t nat -L PREROUTING --line-numbers
# 응답 결과물
Chain PREROUTING (policy ACCEPT)
num target prot opt source destination
1 REDIRECT tcp -- anywhere anywhere tcp dpt:http redir ports {백엔드 포트번호}
2 DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
Bash
복사
여기서 정확한 문제의 원인을 찾을 수 있었다.
DOCKER로 오는 요청 중 하나인 http 80 요청(IMDS 포함)에 대해 1번의 설정 조건이 우선순위로 적용되어 DOCKER에 대한 요청들 모두 백엔드 포트로 가로채지고 있었던 것이었다.
하지만, prod와 dev 서버 설정이 동일했으며, 오히려 dev에서 요청이 정상적으로 진행된 이유를 알수가 없었다…
ii.
일단은 방법을 알았으니 해결을 해야지? 둘의 순서를 바꾸자! ⇒ 해결!!
# 1번 설정을 삭제함으로써 기존 2번의 DOCKER에 대한 요청 설정이 1번이 되도록..
sudo iptables -t nat -D PREROUTING 1
# 삭제한 1번 설정을 다음으로 추가
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port {백엔드 포트번호}
# 해당 설정 영구 저장
sudo service iptables save
Bash
복사
해당 설정의 우선순위를 변경해주고, 야무지게 저장까지 하니.. 드디어 모든 요청이 제대로 간다!!
저 울어도 되죠..? gemni 신뢰도 급상승.. 너 다시 믿어볼게…
이제서야 모든 요청이 정상적으로 작동한다.. 혹시몰라 prod를 재배포(아까 임시 설정 날라가도록) 해봐도 잘 작동한다…!
10.
마무리
배포된 서버에서 막아두었던 이미지 기능을 다시 활성화 시키고,
이 과정에서 추가한 스크립트나, 테스트하기 위해 변경한 값들을 모두 다시 원상복구 시키고,
최종 재배포로 마무리 하였다. 
최종 해결 방법 + 회고
iptable에서 요청 관련한 설정의 우선순위가 뒤바뀌어 있어 기존 요청이 모두 백엔드로 가로채지고 있었다.
prod와 dev 서버에 대해 지금까지 발견한 설정들은 모두 동일했기 때문에 찾기 어려웠다. 두 서버의 차이점이 아닌, 근본적으로 왜 요청이 가로채졌는가에 집중을 했더라면 더욱 빨리 해결할 수 있지 않았을까.. 싶다.
비록.. 아직까지 dev에 어떠한 추가적인 설정으로 인해 해당 요청이 우회되었는지.. 정확한 원인을 찾지는 못하였으나.. dev에서 잘 되는 것이 이상했던 것으로 결론을 내리고 근본적인 문제를 해결하는데 초점을 두었다.
•
기존의 prod와 dev의 iptable 설정
$ sudo iptables -t nat -L PREROUTING --line-numbers
Chain PREROUTING (policy ACCEPT)
num target prot opt source destination
1 REDIRECT tcp -- anywhere anywhere tcp dpt:http redir ports {백엔드 포트번호}
2 DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
Bash
복사
•
바뀐 iptable 설정
$ sudo iptables -t nat -L PREROUTING --line-numbers
Chain PREROUTING (policy ACCEPT)
num target prot opt source destination
1 DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
2 REDIRECT tcp -- anywhere anywhere tcp dpt:http redir ports {백엔드 포트번호}
Bash
복사
추가적인 의문에 대한 해결
그렇다면 잘 몰랐던 개념이나 간과한 내용이 있지 않을까? 과정 중에 생긴 의문과 이에 대한 답이다.
Question 1) 아웃바운드에 대한 리다이렉션 설정이 없는데 그래도 저 설정을 해야할까?
Answer 1)
•
당연히 설정을 변경해야 한다.
•
Docker 네트워크의 특성상, 컨테이너에서 '나가는(outbound)' 요청이 EC2 호스트 입장에서는 '들어오는(inbound)' 것처럼 처리되기 때문이다.
Question 2) 도커 컨테이너 상에서의 요청과 응답인데.. 이 부분이 왜 EC2 설정에서 영향을 받을까? 설정한 각각의 내용대로 작동하는것이 아닐까?
Answer 2)
•
dst-type LOCAL 매칭은 단순히 외부 요청뿐 아니라 컨테이너 내부에서 호스트 IP로 접근한 요청도 DOCKER 체인에 처리 대상이 된다.
•
따라서 컨테이너가 IMDS(169.254.169.254:80)로 요청할 때도 LOCAL으로 인식되어
PREROUTING → DOCKER 흐름에 올라가며, 이 흐름에서 REDIRECT 우선 적용되면 요청이 잘못 라우팅된다.
•
반면 DOCKER 체인이 먼저 실행되면, Docker가 NAT/DNAT을 먼저 수행하므로 REDIRECT가 영향을 미치지 않아 정상적으로 진행되는 것이다.
Question 3) 그렇다면 AWS 상에서 인-아웃 바운드로 설정이 안되는 걸까?
Answer 3)
•
AWS 보안 그룹의 규칙과 iptables의 리디렉션은 역할이 완전히 다르기 때문에 대체할 수 없다.
•
AWS 보안 그룹은 EC2 인스턴스로 들어오고 나가는 트래픽을 허용하거나 차단하는 '방화벽' 역할을 하며,
iptables 리디렉션은 인스턴스 내부로 들어온 트래픽의 경로를 바꿔주는 역할을 한다.
•
현재 필요한 작업은 '내부 경로 변경'이므로 AWS 상에서 설정할 수 없다.
참고자료
Google Gemini
OpenAI ChatGPT