AWS S3에 파일을 업로드하려다 갑자기 403 Forbidden 에러가 뜬다. 프론트에서 S3로 직접 업로드하려니 CORS 에러가 발생한다. 대부분의 개발자는 AWS 콘솔에서 권한 설정을 만지작거리다가 결국 공개 액세스를 허용해버린다. 하지만 정확히 어떤 권한이 필요한지, 어디서 뭘 설정해야 하는지 모른 채 추측으로 진행하는 경우가 많다.
이번에는 AWS S3 업로드 실패의 가장 흔한 원인인 IAM 정책 오류와 CORS 설정 문제를 정확히 진단하고, PHP에서 AWS SDK를 사용한 안전한 파일 업로드 방법까지 완벽하게 정리해서 소개하겠다.
S3에 파일을 업로드하려면 3가지 권한 계층을 통과해야 한다. 첫 번째는 IAM 사용자 권한이다. AWS 계정에서 생성한 IAM 사용자가 S3 버킷에 대한 기본 권한(s3:PutObject)을 가져야 한다. 이 권한이 없으면 아무리 버킷 정책을 열어도 업로드할 수 없다.
두 번째는 S3 버킷 정책이다. 버킷 자체에 설정된 리소스 기반 정책으로, 특정 IAM 사용자나 역할이 어떤 작업을 할 수 있는지 제어한다. 세 번째는 CORS 설정이다. 웹 브라우저에서 다른 도메인의 S3로 직접 업로드하려면 CORS 규칙을 설정해야 한다.
AWS 콘솔에서 IAM 대시보드로 이동해 S3 업로드에 사용할 사용자를 선택한다. "인라인 정책 추가" 또는 "권한 추가"를 클릭하고 아래 JSON 정책을 붙여넣는다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your-bucket-name",
"arn:aws:s3:::your-bucket-name/*"
]
}
]
}
여기서 중요한 건 Resource 부분이다. 첫 번째 항목은 버킷 자체에 대한 권한(ListBucket)이고, 두 번째는 버킷 안의 모든 객체에 대한 권한이다. 한쪽만 설정하면 업로드가 실패한다.
IAM 사용자 권한만으로는 부족하다. S3 버킷 자체의 정책도 설정해야 한다. AWS 콘솔에서 해당 S3 버킷을 선택하고 "권한" 탭으로 이동한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowIAMUserUpload",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:user/your-iam-user"
},
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
여기서 Principal에 IAM 사용자의 ARN을 정확히 입력해야 한다. "123456789012"는 AWS 계정 ID이고, "your-iam-user"는 실제 IAM 사용자명이다. 이 두 가지를 정확히 입력하지 않으면 권한 거부 에러가 계속 난다.
Composer를 사용해 AWS SDK를 설치한다.
composer require aws/aws-sdk-php
PHP 코드에서 S3 클라이언트를 초기화하고 파일을 업로드한다.
<?php
require 'vendor/autoload.php';
use Aws\S3\S3Client;
use Aws\Exception\AwsException;
$s3Client = new S3Client([
'version' => 'latest',
'region' => 'ap-northeast-2',
'credentials' => [
'key' => 'YOUR_AWS_ACCESS_KEY',
'secret' => 'YOUR_AWS_SECRET_KEY',
]
]);
try {
$result = $s3Client->putObject([
'Bucket' => 'your-bucket-name',
'Key' => 'uploads/example.jpg',
'Body' => fopen('/path/to/file.jpg', 'r'),
'ContentType' => 'image/jpeg',
'ACL' => 'private'
]);
echo "업로드 성공: " . $result['ObjectURL'];
} catch (AwsException $e) {
echo "업로드 실패: " . $e->getMessage();
}
?>
여기서 중요한 부분을 강조하자. 첫 번째, credentials는 환경 변수나 IAM 역할을 사용하는 게 좋지만, 테스트 목적으로는 직접 입력해도 된다. 두 번째, ACL을 'private'으로 설정하면 누구도 직접 접근할 수 없다. 'public-read'로 설정하면 누구나 읽을 수 있지만, 쓰기는 불가능하다. 세 번째, Key 값이 객체의 경로가 되므로 중복되지 않도록 주의해야 한다.
프론트엔드에서 S3로 직접 업로드하려면 CORS를 설정해야 한다. S3 버킷 "권한" 탭에서 "CORS"를 선택하고 다음 설정을 추가한다.
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["https://yourdomain.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
AllowedOrigins에 정확한 도메인을 입력해야 한다. "*"로 설정하면 모든 도메인에서 접근 가능하지만, 보안상 권장하지 않는다. 프로덕션 환경에서는 반드시 자신의 도메인만 허용하자.
✗ 잘못된 방법: IAM 사용자 권한은 설정했지만 버킷 정책에 Principal을 명시하지 않음. 이 경우 IAM 사용자가 버킷에 접근할 수 없다.
✓ 올바른 방법: IAM 사용자 권한과 버킷 정책 둘 다 설정. 버킷 정책에 명시적으로 해당 IAM 사용자의 ARN을 Principal로 지정.
✗ 잘못된 방법: CORS 설정에서 AllowedOrigins를 "*"로 설정해 모든 도메인에서 업로드 가능하게 함. 악의적인 사용자가 당신의 버킷에 대량의 쓰레기 파일을 올릴 수 있다.
✓ 올바른 방법: AllowedOrigins에 자신의 도메인만 명시. AllowedMethods도 필요한 것만(보통 PUT) 선택.
✗ 잘못된 방법: Resource를 "arn:aws:s3:::bucket-name"만 설정하고 "arn:aws:s3:::bucket-name/*"를 빼먹음. 이 경우 ListBucket은 되지만 실제 객체 업로드는 실패한다.
✓ 올바른 방법: Resource 배열에 버킷 자체와 그 안의 모든 객체 둘 다 포함.
IAM 사용자의 액세스 키와 시크릿 키로 위의 PHP 코드를 실행해보자. 성공하면 다음처럼 출력된다.
업로드 성공: https://your-bucket-name.s3.ap-northeast-2.amazonaws.com/uploads/example.jpg
만약 403 에러가 여전히 난다면, CloudTrail에서 실제 실패 로그를 확인해야 한다. AWS 콘솔에서 CloudTrail을 선택하고 최근 이벤트를 검색하면, 어느 권한이 부족한지 정확히 알 수 있다. 예를 들어 "User: arn:aws:iam::123456789012:user/test is not authorized to perform: s3:PutObject"라는 메시지가 나오면 IAM 사용자 권한을 다시 확인해야 한다.
credentials를 코드에 직접 입력하는 건 보안 위험이다. 대신 환경 변수를 사용하자.
<?php
require 'vendor/autoload.php';
use Aws\S3\S3Client;
$s3Client = new S3Client([
'version' => 'latest',
'region' => 'ap-northeast-2'
// 환경 변수에서 자동으로 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY를 읽음
]);
$result = $s3Client->putObject([
'Bucket' => getenv('AWS_S3_BUCKET'),
'Key' => 'uploads/' . uniqid() . '.jpg',
'Body' => fopen($_FILES['file']['tmp_name'], 'r'),
'ContentType' => $_FILES['file']['type'],
'ACL' => 'private'
]);
echo json_encode(['url' => $result['ObjectURL']]);
?>
.env 파일에 다음과 같이 환경 변수를 설정한다.
AWS_ACCESS_KEY_ID=YOUR_KEY
AWS_SECRET_ACCESS_KEY=YOUR_SECRET
AWS_S3_BUCKET=your-bucket-name
AWS_DEFAULT_REGION=ap-northeast-2
AWS SDK는 자동으로 이 환경 변수들을 읽고 credentials를 설정한다. 이 방식이 안전하고 프로덕션 환경에서도 권장된다.
S3 업로드는 올바른 권한 설정과 CORS 이해가 핵심이다. IAM 권한, 버킷 정책, CORS 세 계층을 모두 확인하고 설정해야만 예상대로 동작한다는 점을 기억하자. 위 글의 5단계 체크리스트를 참고해 각 설정을 검증하면, 403 에러 없이 안전하게 S3에 파일을 업로드할 수 있을 것이다. 특히 환경 변수 설정으로 보안까지 챙기면, 실제 서비스 환경에서도 걱정 없이 S3를 활용할 수 있다.