터미널에서 S3 목록은 잘 나오는데, 같은 버킷을 읽는 Node.js 코드에서는 AccessDenied가 뜬다. '방금 됐는데요?'라는 말이 절로 나온다. 하지만 같은 컴퓨터에서 실행했다는 사실만으로 같은 AWS 주체가 요청했다는 뜻은 아니다. 먼저 누가 호출했는지 맞춰 보고, 그다음에 무엇을 요청했는지 비교하면 막힌 곳이 보인다.
AWS CLI와 SDK는 왜 다른 자격 증명을 쓸까?
CLI는 명령마다 --profile을 지정할 수 있다. 반면 Node.js용 AWS SDK v3에서 new S3Client({ region: "ap-northeast-2" })처럼 자격 증명을 직접 지정하지 않으면 기본 자격 증명 탐색 순서를 따른다. 환경 변수, 공유 설정 파일, 실행 환경의 역할 등 여러 출처 중 먼저 찾은 값을 쓴다. 따라서 CLI에만 --profile dev를 붙이고 SDK에는 아무것도 지정하지 않았다면 서로 다른 계정이나 역할을 사용할 수 있다. AWS의 JavaScript SDK 자격 증명 안내는 이 탐색 순서와 여러 출처를 동시에 설정할 때의 위험을 설명한다.
가상의 example-docs-bucket을 두고 보자. 두 도구가 모두 S3의 ListObjectsV2를 호출한다고 가정한다. CLI는 dev 프로필의 Role A로, Node 프로세스는 기본 자격 증명의 Role B로 요청한다. Role A에만 조회 권한이 있다면 CLI 성공과 SDK 거부는 모순이 아니다. 이미지의 200과 403은 이 조건을 설명하기 위한 예시이며 실제 계정 응답이 아니다.
aws s3api list-objects-v2 --bucket example-docs-bucket --max-keys 1 --profile devimport { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "ap-northeast-2" });
const page = await s3.send(new ListObjectsV2Command({
Bucket: "example-docs-bucket",
MaxKeys: 1,
}));
console.log(page.KeyCount);두 번째 코드는 @aws-sdk/client-s3가 설치된 Node.js 프로젝트에서 실행하는 예시다. 버킷 이름과 리전은 자신의 조회 대상으로 바꾸되, 비교하는 동안에는 두 호출의 값을 같게 둔다.
CLI와 SDK가 실제로 누구로 호출하는지 확인하기
먼저 CLI에서 성공한 그 명령과 같은 프로필로 호출 주체를 확인한다. 아래 dev는 예시 프로필 이름이다.
aws sts get-caller-identity --profile dev
aws configure list --profile dev첫 명령의 Account와 Arn은 호출한 계정·주체를 알려 준다. 두 번째 명령은 프로필, 리전, 자격 증명 값의 출처를 보여 준다. 액세스 키는 일부가 마스킹되지만 출력 전체를 공개 이슈나 채팅에 붙이지 말자. 다른 사람에게 확인을 요청할 때는 계정 ID와 ARN도 필요한 범위만 공유한다.
이제 문제의 Node 프로세스에서 같은 STS 호출을 한다. 프로젝트에 @aws-sdk/client-sts가 설치되어 있다는 전제의 독립 예제다.
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
const client = new STSClient({ region: "ap-northeast-2" });
const { Account, Arn } = await client.send(new GetCallerIdentityCommand({}));
console.log({ Account, Arn });두 결과의 Account와 Arn에서 계정·역할을 대조한다. 임시 자격 증명은 역할이 같아도 ARN 끝의 세션 이름이 다를 수 있다. 계정이나 역할이 다르면 S3 정책부터 바꾸지 말고 CLI의 --profile, Node의 AWS_PROFILE, 프로세스에 주입된 자격 증명 환경 변수, 코드의 명시적 credentials 설정을 확인한다. ~/.aws/config와 ~/.aws/credentials는 공유 설정 위치지만, 파일에 dev가 있다고 해서 모든 프로세스가 자동으로 dev를 선택하는 것은 아니다. AWS CLI 설정 파일 안내에서 프로필 이름과 aws configure list의 출처 표시를 확인할 수 있다.
Node도 dev 프로필을 써야 한다면, macOS·Linux 터미널에서는 AWS_PROFILE=dev node list.mjs처럼 그 프로세스에만 프로필을 지정한 뒤 STS 결과를 다시 확인한다. 단, 프로세스에 액세스 키 환경 변수까지 주입돼 있다면 SDK가 이를 먼저 선택할 수 있다. 프로필 이름만 바꾸고 끝내지 말고 실제 호출 주체가 바뀌었는지 확인하자.
로컬 개발에서는 조직이 제공한 로그인 방식이 있다면 그것을 따르고, 새로 구성한다면 IAM Identity Center와 단기 자격 증명을 우선한다. 오래된 튜토리얼의 장기 액세스 키를 코드나 저장소에 복사하거나 AmazonS3FullAccess를 오류 해결용으로 덧붙이지 않는다. 이미 사용 중인 키가 있다면 화면·로그에 값 자체를 출력하지 않고 출처와 권한 범위를 점검한다.
호출 주체가 같아도 AccessDenied가 나는 이유
계정·ARN이 같다면 다음은 요청한 동작이다. 위 CLI 목록 조회가 성공해도 다른 SDK 코드가 PutObjectCommand로 파일을 올리면 필요한 권한이 달라진다. 목록 조회는 버킷의 s3:ListBucket, 객체 업로드는 대상 객체의 s3:PutObject가 핵심이다. 'CLI는 된다'는 말만으로 SDK의 다른 작업까지 허용된다고 볼 수 없다.
비교할 때는 도구 이름이 아니라 같은 계정·역할, 같은 버킷, 같은 리전, 같은 API 동작을 맞춘다. 그래도 한쪽만 거부된다면 실제 오류의 작업·리소스와 해당 역할의 권한, 버킷 정책, 조직 정책 등 적용 가능한 제한을 살핀다. 무작정 권한을 넓히면 원인을 가린 채 접근 범위만 커진다.
| 확인 결과 | 다음에 볼 곳 |
|---|---|
| CLI와 SDK의 계정 또는 역할이 다름 | 프로필 선택, 환경 변수, 명시적 자격 증명 |
| 호출 주체는 같지만 API 동작이 다름 | 각 동작에 필요한 IAM 권한과 리소스 |
| 호출 주체와 동작도 같음 | 리전·버킷, 정책의 명시적 거부와 적용 범위 |
핵심 요약
CLI 성공과 SDK AccessDenied가 함께 보이면 '누가' → '무엇을' 순서로 확인한다. get-caller-identity로 두 실행의 주체를 맞추고, CLI 프로필과 SDK의 자격 증명 출처를 본다. 주체가 같다면 같은 S3 API 동작·버킷·리전인지 비교한다. 권한을 먼저 크게 열기보다, 어긋난 조건 하나를 찾는 편이 빠르고 안전하다.
작성자
기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

