S3 암호화 (Encryption)
암호화 방식 비교
S3는 4가지 서버 측 암호화(SSE) 방식을 제공
1. SSE-S3 (기본값, 권장)
특징:
- AWS가 관리하는 키로 암호화
- AES-256 암호화
- 추가 비용 없음
- 기본으로 활성화 (2023년 1월부터)
작동 방식:
객체 업로드
↓
S3가 자동으로 암호화 키 생성
↓
AES-256으로 암호화
↓
암호화된 객체 저장
암호화 키는 별도 암호화하여 저장
설정:
# 업로드 시 암호화 (기본값이므로 명시 불필요)
aws s3 cp myfile.txt s3://my-bucket/
# 명시적으로 지정
aws s3 cp myfile.txt s3://my-bucket/ \
--server-side-encryption AES256
# 버킷 기본 암호화 설정
aws s3api put-bucket-encryption \
--bucket my-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
},
"BucketKeyEnabled": true
}]
}'
장점:
- 설정 간단
- 추가 비용 없음
- 키 관리 불필요
단점:
- 키 회전 제어 불가
- 키 접근 감사 불가
- 키 삭제 불가
2. SSE-KMS (AWS Key Management Service)
특징:
- KMS로 키 관리
- 키 회전 자동화
- 키 사용 감사 가능 (CloudTrail)
- 키 접근 제어 가능 (IAM)
비용:
- KMS 키: $1/월
- API 요청: $0.03/10,000 요청
설정:
# KMS 키 생성
aws kms create-key \
--description "S3 encryption key"
# 별칭 생성
aws kms create-alias \
--alias-name alias/s3-encryption \
--target-key-id 1234abcd-12ab-34cd-56ef-1234567890ab
# 업로드 시 KMS 사용
aws s3 cp myfile.txt s3://my-bucket/ \
--server-side-encryption aws:kms \
--ssekms-key-id arn:aws:kms:region:account:key/1234abcd-12ab-34cd-56ef-1234567890ab
# 버킷 기본 암호화 (KMS)
aws s3api put-bucket-encryption \
--bucket my-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:region:account:key/1234abcd"
},
"BucketKeyEnabled": true
}]
}'
S3 Bucket Key 최적화:
Bucket Key 없음:
- 각 객체마다 KMS API 호출
- 1,000개 객체 = 1,000 KMS 요청 = $0.03
Bucket Key 사용:
- 버킷 레벨 키 생성
- S3가 객체 키 생성
- 1,000개 객체 = 1 KMS 요청 = $0.000003
→ 99% 비용 절감
KMS 키 정책:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow S3 to use the key",
"Effect": "Allow",
"Principal": {
"Service": "s3.amazonaws.com"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*"
},
{
"Sid": "Allow specific users to decrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:user/alice"
},
"Action": [
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}
장점:
- 키 회전 자동화
- 세밀한 접근 제어
- 감사 로그 (CloudTrail)
- 키 비활성화/삭제 가능
단점:
- 추가 비용
- KMS API 제한 (초당 5,500-10,000 요청)
- 복잡한 설정
3. SSE-C (Customer-Provided Keys)
특징:
- 고객이 암호화 키 제공 및 관리
- AWS는 키를 저장하지 않음
- 각 요청마다 키 전송 필요
설정:
# 키 생성 (256-bit)
openssl rand -base64 32 > encryption.key
# 업로드
aws s3api put-object \
--bucket my-bucket \
--key myfile.txt \
--body myfile.txt \
--sse-customer-algorithm AES256 \
--sse-customer-key fileb://encryption.key \
--sse-customer-key-md5 $(openssl dgst -md5 -binary encryption.key | base64)
# 다운로드 (동일한 키 필요)
aws s3api get-object \
--bucket my-bucket \
--key myfile.txt \
--sse-customer-algorithm AES256 \
--sse-customer-key fileb://encryption.key \
--sse-customer-key-md5 $(openssl dgst -md5 -binary encryption.key | base64) \
myfile-downloaded.txt
Python 예시:
import boto3
import os
from base64 import b64encode
# 키 생성
key = os.urandom(32)
key_b64 = b64encode(key).decode()
s3 = boto3.client('s3')
# 업로드
s3.put_object(
Bucket='my-bucket',
Key='myfile.txt',
Body=b'My secret data',
SSECustomerAlgorithm='AES256',
SSECustomerKey=key_b64
)
# 다운로드
response = s3.get_object(
Bucket='my-bucket',
Key='myfile.txt',
SSECustomerAlgorithm='AES256',
SSECustomerKey=key_b64
)
data = response['Body'].read()
장점:
- 완전한 키 제어
- AWS가 키를 저장하지 않음
- 규정 준수 (특정 산업)
단점:
- 키 관리 책임
- 키 분실 시 복구 불가
- HTTPS 필수
- 복잡한 구현
4. DSSE-KMS (Dual-layer Server-Side Encryption)
특징:
- 2중 암호화 레이어
- 규정 준수 요구사항 충족
- 2023년 출시
작동 방식:
객체 ↓ 첫 번째 암호화 (Application Layer) ↓ 두 번째 암호화 (Infrastructure Layer) ↓ 저장
설정:
aws s3 cp myfile.txt s3://my-bucket/ \
--server-side-encryption aws:kms:dsse \
--ssekms-key-id arn:aws:kms:region:account:key/1234abcd
사용 사례:
- 금융 기관
- 의료 데이터
- 정부 기관
클라이언트 측 암호화
애플리케이션에서 암호화 후 S3에 업로드
from cryptography.fernet import Fernet
import boto3
# 키 생성 및 저장
key = Fernet.generate_key()
cipher = Fernet(key)
# 데이터 암호화
data = b"Sensitive information"
encrypted = cipher.encrypt(data)
# S3 업로드
s3 = boto3.client('s3')
s3.put_object(
Bucket='my-bucket',
Key='encrypted-file',
Body=encrypted
)
# 나중에 다운로드 및 복호화
response = s3.get_object(Bucket='my-bucket', Key='encrypted-file')
encrypted_data = response['Body'].read()
decrypted = cipher.decrypt(encrypted_data)
장점:
- 전송 중에도 암호화
- AWS는 암호화된 데이터만 봄
- 완전한 제어
단점:
- 암호화/복호화 오버헤드
- 키 관리 복잡
- S3 기능 일부 사용 불가 (S3 Select 등)
암호화 강제
버킷 정책으로 암호화되지 않은 업로드를 차단
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedObjectUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
},
{
"Sid": "DenyNonKMSUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-secure-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}
CORS (Cross-Origin Resource Sharing)
개념
웹 브라우저가 다른 도메인의 S3 리소스에 접근할 수 있도록 허용
브라우저 (https://myapp.com)
↓ Ajax 요청
S3 (https://my-bucket.s3.amazonaws.com)
↓ CORS 검증
허용 또는 거부
CORS 설정
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>https://myapp.com</AllowedOrigin>
<AllowedOrigin>https://www.myapp.com</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>DELETE</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<ExposeHeader>ETag</ExposeHeader>
</CORSRule>
</CORSConfiguration>
JSON 형식:
[
{
"AllowedOrigins": ["https://myapp.com"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3000,
"ExposeHeaders": ["ETag", "x-amz-meta-custom-header"]
}
]
CLI로 설정:
aws s3api put-bucket-cors \ --bucket my-bucket \ --cors-configuration file://cors.json
실전 예시
프론트엔드에서 직접 업로드
// React 예시
async function uploadToS3(file) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch(
'https://my-bucket.s3.ap-northeast-2.amazonaws.com/uploads/' + file.name,
{
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type
}
}
);
if (response.ok) {
console.log('Upload successful!');
}
} catch (error) {
console.error('Upload failed:', error);
}
}
주의사항:
- CORS는 브라우저 보안 기능
- API 요청에는 적용 안 됨
- AllowedOrigin에 정확한 도메인 지정 (보안)
MFA Delete
개념
Multi-Factor Authentication으로 중요한 작업을 보호
MFA 필요 작업:
- 버전 관리 일시 중지/중단
- 객체 버전 영구 삭제
MFA 불필요:
- 일반 객체 업로드/다운로드
- 삭제 마커 추가 (일반 삭제)
설정 방법
# 1. 버전 관리 활성화 (필수)
aws s3api put-bucket-versioning \
--bucket my-bucket \
--versioning-configuration Status=Enabled
# 2. MFA Delete 활성화 (루트 계정만 가능!)
aws s3api put-bucket-versioning \
--bucket my-bucket \
--versioning-configuration Status=Enabled,MFADelete=Enabled \
--mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device 123456"
⚠️ 중요: MFA Delete는 루트 계정 자격 증명으로만 활성화/비활성화 가능!
사용 예시
# MFA 없이 삭제 시도 (삭제 마커만 추가)
aws s3api delete-object \
--bucket my-bucket \
--key important-file.txt
# → 성공 (삭제 마커)
# 버전 영구 삭제 시도 (MFA 필요)
aws s3api delete-object \
--bucket my-bucket \
--key important-file.txt \
--version-id "abc123" \
--mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device 123456"
# → MFA 코드 검증 후 삭제
모범 사례
중요 데이터 버킷:
├─ 버전 관리: Enabled
├─ MFA Delete: Enabled
├─ 버킷 정책: 관리자만 접근
└─ CloudTrail: 모든 API 로깅
S3 액세스 로그 (Access Logs)
개념
모든 S3 버킷 요청을 로깅합니다.
Source Bucket (my-app-bucket)
↓ 모든 요청 기록
Logging Bucket (my-logs-bucket/logs/)
↓
log1.txt: GET /photo.jpg 200 OK
log2.txt: PUT /data.csv 200 OK
log3.txt: DELETE /old.txt 403 Forbidden
설정
# 로그 버킷 생성
aws s3 mb s3://my-logs-bucket
# 로깅 활성화
aws s3api put-bucket-logging \
--bucket my-app-bucket \
--bucket-logging-status '{
"LoggingEnabled": {
"TargetBucket": "my-logs-bucket",
"TargetPrefix": "app-logs/"
}
}'
로그 형식
79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be my-app-bucket [06/Feb/2024:00:00:38 +0000] 192.0.2.3 79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be 3E57427F3EXAMPLE REST.GET.VERSIONING - "GET /my-app-bucket?versioning HTTP/1.1" 200 - 113 - 7 - "-" "S3Console/0.4" - s9lzHYrFp76ZVxRcpX9+5cjAnEH2ROuNkd2BHfIa6UkFVdtjf5mKR3/eTPFvsiP/XV/VLi31234= SigV2 ECDHE-RSA-AES128-GCM-SHA256 AuthHeader my-app-bucket.s3.amazonaws.com TLSV1.2
주요 필드:
- Bucket Owner
- Bucket
- Time
- Remote IP
- Requester
- Request ID
- Operation (REST.GET.OBJECT, REST.PUT.OBJECT 등)
- Key
- HTTP Status
- Error Code
- Bytes Sent
- Total Time
- User Agent
로그 분석
Athena로 분석
CREATE EXTERNAL TABLE s3_access_logs(
bucket_owner string,
bucket string,
request_datetime string,
remote_ip string,
requester string,
request_id string,
operation string,
key string,
request_uri string,
http_status int,
error_code string,
bytes_sent bigint,
object_size bigint,
total_time int,
turn_around_time int,
referer string,
user_agent string
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe'
WITH SERDEPROPERTIES (
'input.regex' = '([^ ]*) ([^ ]*) \\[(.*?)\\] ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) (-|[0-9]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) ([^ ]*)'
)
LOCATION 's3://my-logs-bucket/app-logs/';
-- 가장 많이 접근된 파일 TOP 10
SELECT key, COUNT(*) as requests
FROM s3_access_logs
WHERE http_status = 200
GROUP BY key
ORDER BY requests DESC
LIMIT 10;
-- 실패한 요청 분석
SELECT key, http_status, error_code, COUNT(*) as errors
FROM s3_access_logs
WHERE http_status >= 400
GROUP BY key, http_status, error_code
ORDER BY errors DESC;
-- IP별 요청 수
SELECT remote_ip, COUNT(*) as requests
FROM s3_access_logs
GROUP BY remote_ip
ORDER BY requests DESC
LIMIT 100;
비용 최적화
로그는 빠르게 쌓이므로 Lifecycle 정책으로 관리
{
"Rules": [
{
"Id": "ArchiveLogs",
"Status": "Enabled",
"Filter": {
"Prefix": "app-logs/"
},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER"
}
],
"Expiration": {
"Days": 365
}
}
]
}
사전 서명된 URL (Pre-Signed URLs)
개념
임시로 S3 객체에 접근할 수 있는 URL을 생성합니다.
사용자 → 애플리케이션 서버
↓
Pre-Signed URL 생성
(유효기간: 1시간)
↓
사용자 → S3 (Pre-Signed URL로 직접 접근)
생성 방법
CLI
# 다운로드용 URL (기본 1시간)
aws s3 presign s3://my-bucket/private-file.pdf
# 유효기간 지정 (최대 7일)
aws s3 presign s3://my-bucket/private-file.pdf \
--expires-in 3600 # 1시간 (초 단위)
# 업로드용 URL
aws s3 presign s3://my-bucket/upload/newfile.txt \
--expires-in 300 \
--http-method PUT
Python SDK
import boto3
from botocore.exceptions import ClientError
s3_client = boto3.client('s3')
# 다운로드 URL
def create_presigned_url(bucket, key, expiration=3600):
try:
url = s3_client.generate_presigned_url(
'get_object',
Params={
'Bucket': bucket,
'Key': key
},
ExpiresIn=expiration
)
return url
except ClientError as e:
print(e)
return None
# 사용
download_url = create_presigned_url('my-bucket', 'private-file.pdf', 3600)
print(f"Download URL: {download_url}")
# 업로드 URL
def create_presigned_upload_url(bucket, key, expiration=3600):
try:
url = s3_client.generate_presigned_url(
'put_object',
Params={
'Bucket': bucket,
'Key': key,
'ContentType': 'application/pdf'
},
ExpiresIn=expiration,
HttpMethod='PUT'
)
return url
except ClientError as e:
print(e)
return None
upload_url = create_presigned_upload_url('my-bucket', 'uploads/newfile.pdf', 300)
JavaScript (Node.js)
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
// 다운로드 URL
const params = {
Bucket: 'my-bucket',
Key: 'private-file.pdf',
Expires: 3600
};
s3.getSignedUrl('getObject', params, (err, url) => {
if (err) {
console.error(err);
} else {
console.log('Download URL:', url);
}
});
// 업로드 URL
const uploadParams = {
Bucket: 'my-bucket',
Key: 'uploads/newfile.pdf',
Expires: 300,
ContentType: 'application/pdf'
};
s3.getSignedUrl('putObject', uploadParams, (err, url) => {
if (err) {
console.error(err);
} else {
console.log('Upload URL:', url);
}
});
실전 활용
1. 안전한 파일 다운로드
# Flask 애플리케이션
from flask import Flask, redirect
import boto3
app = Flask(__name__)
s3 = boto3.client('s3')
@app.route('/download/<file_id>')
def download_file(file_id):
# 권한 확인
if not user_has_permission(file_id):
return "Forbidden", 403
# Pre-Signed URL 생성
url = s3.generate_presigned_url(
'get_object',
Params={
'Bucket': 'my-private-bucket',
'Key': f'files/{file_id}'
},
ExpiresIn=60 # 1분
)
# 리다이렉트
return redirect(url)
2. 브라우저에서 직접 업로드
// 프론트엔드
async function uploadFile(file) {
// 백엔드에서 Pre-Signed URL 요청
const response = await fetch('/api/get-upload-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
filename: file.name,
contentType: file.type
})
});
const { uploadUrl } = await response.json();
// S3에 직접 업로드
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type
}
});
console.log('Upload successful!');
}
// 백엔드 (Node.js/Express)
app.post('/api/get-upload-url', async (req, res) => {
const { filename, contentType } = req.body;
const params = {
Bucket: 'my-uploads-bucket',
Key: `uploads/${Date.now()}-${filename}`,
ContentType: contentType,
Expires: 300 // 5분
};
const uploadUrl = await s3.getSignedUrlPromise('putObject', params);
res.json({ uploadUrl });
});
보안 고려사항
# ❌ 나쁜 예: 긴 유효기간
url = s3.generate_presigned_url(
'get_object',
Params={'Bucket': 'my-bucket', 'Key': 'file.pdf'},
ExpiresIn=604800 # 7일 - 너무 김!
)
# ✅ 좋은 예: 짧은 유효기간
url = s3.generate_presigned_url(
'get_object',
Params={'Bucket': 'my-bucket', 'Key': 'file.pdf'},
ExpiresIn=300 # 5분
)
# ✅ 더 좋은 예: 조건 추가
url = s3.generate_presigned_url(
'get_object',
Params={
'Bucket': 'my-bucket',
'Key': 'file.pdf',
'ResponseContentDisposition': 'attachment; filename="document.pdf"',
'ResponseContentType': 'application/pdf'
},
ExpiresIn=300
)
S3 Glacier Vault Lock & Object Lock
S3 Glacier Vault Lock
개념: Glacier 볼트를 "잠가서" 데이터를 변경 불가능하게 함
사용 사례:
- 규정 준수 (WORM: Write Once Read Many)
- 법적 요구사항
- 감사 로그 보호
# Vault Lock 정책 설정
aws glacier initiate-vault-lock \
--account-id - \
--vault-name my-compliance-vault \
--policy file://vault-lock-policy.json
정책 예시:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "deny-delete-before-7-years",
"Effect": "Deny",
"Principal": "*",
"Action": "glacier:DeleteArchive",
"Resource": "arn:aws:glacier:region:account-id:vaults/my-compliance-vault",
"Condition": {
"NumericLessThan": {
"glacier:ArchiveAgeInDays": "2555"
}
}
}
]
}
S3 Object Lock
개념: S3 객체를 지정된 기간 동안 삭제/수정 불가능하게 만듭니다.
모드:
1. Governance Mode (거버넌스 모드)
- 특별한 권한이 있는 사용자는 삭제 가능
- 보호 설정 수정 가능
- 일반 사용자는 삭제 불가
2. Compliance Mode (규정 준수 모드)
- 누구도 삭제/수정 불가 (루트 계정 포함)
- 보호 기간 단축 불가
- 완전한 불변성
- 가장 강력한 보호
Object Lock 설정
# 1. 버킷 생성 시 Object Lock 활성화 (필수)
aws s3api create-bucket \
--bucket my-locked-bucket \
--object-lock-enabled-for-bucket
# 2. 기본 보존 설정
aws s3api put-object-lock-configuration \
--bucket my-locked-bucket \
--object-lock-configuration '{
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": "GOVERNANCE",
"Days": 365
}
}
}'
# 3. 객체 업로드 (보존 기간 지정)
aws s3api put-object \
--bucket my-locked-bucket \
--key important-doc.pdf \
--body document.pdf \
--object-lock-mode COMPLIANCE \
--object-lock-retain-until-date "2025-12-31T00:00:00Z"
법적 보존 (Legal Hold)
무기한으로 객체를 보호(기간 없음).
# Legal Hold 설정
aws s3api put-object-legal-hold \
--bucket my-bucket \
--key evidence.pdf \
--legal-hold Status=ON
# Legal Hold 해제 (특별 권한 필요)
aws s3api put-object-legal-hold \
--bucket my-bucket \
--key evidence.pdf \
--legal-hold Status=OFF
실전 시나리오
규정 준수 아카이브
import boto3
from datetime import datetime, timedelta
s3 = boto3.client('s3')
def archive_with_lock(bucket, key, file_path, retention_years=7):
# 보존 종료일 계산
retain_until = datetime.now() + timedelta(days=retention_years*365)
# 업로드 with Object Lock
with open(file_path, 'rb') as f:
s3.put_object(
Bucket=bucket,
Key=key,
Body=f,
ObjectLockMode='COMPLIANCE',
ObjectLockRetainUntilDate=retain_until
)
print(f"Archived: {key} (locked until {retain_until})")
# 의료 기록 7년 보관
archive_with_lock('medical-records', 'patient-123/record-2024.pdf', 'record.pdf', 7)
# 금융 기록 10년 보관
archive_with_lock('financial-records', 'audit-2024.pdf', 'audit.pdf', 10)
Governance 모드에서 강제 삭제
# 특별 권한으로 삭제 (s3:BypassGovernanceRetention 필요)
aws s3api delete-object \
--bucket my-bucket \
--key protected-file.pdf \
--version-id "abc123" \
--bypass-governance-retention
IAM 정책:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:BypassGovernanceRetention",
"s3:DeleteObject",
"s3:DeleteObjectVersion"
],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
S3 액세스 포인트 (Access Points)
개념
버킷에 대한 여러 진입점을 생성하여 접근을 단순화합니다.
단일 버킷:
s3://my-data-bucket/
여러 액세스 포인트:
├─ finance-ap → /finance/* (재무팀만)
├─ engineering-ap → /engineering/* (엔지니어링팀만)
└─ analytics-ap → /analytics/* (분석팀만)
생성 및 사용
# 액세스 포인트 생성
aws s3control create-access-point \
--account-id 123456789012 \
--name finance-ap \
--bucket my-data-bucket \
--vpc-configuration VpcId=vpc-12345678
# 액세스 포인트 정책
aws s3control put-access-point-policy \
--account-id 123456789012 \
--name finance-ap \
--policy '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/FinanceRole"
},
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:region:account-id:accesspoint/finance-ap/object/finance/*"
}
]
}'
액세스 포인트 사용
# 일반 버킷 대신 액세스 포인트 사용
aws s3api get-object \
--bucket arn:aws:s3:region:account-id:accesspoint/finance-ap \
--key finance/report.pdf \
report.pdf
# Python SDK
import boto3
s3 = boto3.client('s3')
response = s3.get_object(
Bucket='arn:aws:s3:ap-northeast-2:123456789012:accesspoint/finance-ap',
Key='finance/report.pdf'
)
Multi-Region Access Point
여러 리전의 버킷을 단일 글로벌 엔드포인트로 통합
Global Endpoint: my-app.mrap.accesspoint.s3-global.amazonaws.com
↓
자동 라우팅
├─ ap-northeast-2 bucket (아시아 사용자)
├─ us-east-1 bucket (미국 사용자)
└─ eu-west-1 bucket (유럽 사용자)
장점:
- 단일 엔드포인트
- 자동 장애 조치
- 지연 시간 최소화
S3 Object Lambda
개념
S3에서 객체를 검색할 때 Lambda 함수를 실행하여 데이터를 변환합니다.
사용자 요청: GET /data.csv
↓
Object Lambda Access Point
↓ Lambda 함수 호출
데이터 변환 (예: 개인정보 마스킹)
↓
변환된 데이터 반환
사용 사례
- 개인정보 보호
- 민감한 정보 마스킹
- 부서별 필터링
- 데이터 형식 변환
- JSON → XML
- CSV → JSON
- 이미지 리사이징
- 동적 콘텐츠 생성
- 워터마크 추가
- 압축/압축 해제
설정 예시
1. Lambda 함수 생성
import boto3
import json
s3 = boto3.client('s3')
def lambda_handler(event, context):
# Object Lambda 이벤트 파싱
object_context = event['getObjectContext']
request_route = object_context['outputRoute']
request_token = object_context['outputToken']
s3_url = object_context['inputS3Url']
# 원본 객체 가져오기
response = s3.get_object(
Bucket=event['configuration']['accessPointArn'].split('/')[-1],
Key=event['userRequest']['url'].split('/')[-1]
)
original_data = response['Body'].read().decode('utf-8')
# 데이터 변환 (예: 이메일 마스킹)
import re
masked_data = re.sub(
r'[\w\.-]+@[\w\.-]+',
'***@***.com',
original_data
)
# 변환된 데이터 반환
s3.write_get_object_response(
RequestRoute=request_route,
RequestToken=request_token,
Body=masked_data
)
return {'statusCode': 200}
2. Object Lambda Access Point 생성
# 1. 표준 액세스 포인트 생성
aws s3control create-access-point \
--account-id 123456789012 \
--name my-ap \
--bucket my-bucket
# 2. Object Lambda Access Point 생성
aws s3control create-access-point-for-object-lambda \
--account-id 123456789012 \
--name my-lambda-ap \
--configuration '{
"SupportingAccessPoint": "arn:aws:s3:region:account-id:accesspoint/my-ap",
"TransformationConfigurations": [{
"Actions": ["GetObject"],
"ContentTransformation": {
"AwsLambda": {
"FunctionArn": "arn:aws:lambda:region:account-id:function:my-transform-function"
}
}
}]
}'
3. 사용
import boto3
s3 = boto3.client('s3')
# Object Lambda Access Point로 요청
response = s3.get_object(
Bucket='arn:aws:s3-object-lambda:region:account-id:accesspoint/my-lambda-ap',
Key='sensitive-data.csv'
)
# 마스킹된 데이터 반환
data = response['Body'].read()
print(data) # 이메일이 마스킹됨
실전 예시
이미지 리사이징
from PIL import Image
from io import BytesIO
import boto3
s3 = boto3.client('s3')
def lambda_handler(event, context):
object_context = event['getObjectContext']
s3_url = object_context['inputS3Url']
# 원본 이미지 가져오기
response = s3.get_object(
Bucket=event['configuration']['payload']['bucket'],
Key=event['userRequest']['url'].split('/')[-1]
)
# 이미지 리사이징
image = Image.open(BytesIO(response['Body'].read()))
image.thumbnail((200, 200))
# 버퍼에 저장
buffer = BytesIO()
image.save(buffer, 'JPEG')
buffer.seek(0)
# 변환된 이미지 반환
s3.write_get_object_response(
RequestRoute=object_context['outputRoute'],
RequestToken=object_context['outputToken'],
Body=buffer.read()
)
return {'statusCode': 200}
부서별 데이터 필터링
import json
import boto3
s3 = boto3.client('s3')
def lambda_handler(event, context):
# 사용자 정보 (IAM 태그 등에서 가져옴)
user_department = event['userRequest']['headers'].get('x-department', 'unknown')
# 원본 JSON 데이터
response = s3.get_object(
Bucket=event['configuration']['payload']['bucket'],
Key=event['userRequest']['url'].split('/')[-1]
)
data = json.loads(response['Body'].read())
# 부서별 필터링
filtered_data = [
record for record in data
if record.get('department') == user_department
]
# 필터링된 데이터 반환
s3.write_get_object_response(
RequestRoute=event['getObjectContext']['outputRoute'],
RequestToken=event['getObjectContext']['outputToken'],
Body=json.dumps(filtered_data)
)
return {'statusCode': 200}
보안 체크리스트
필수 보안 설정
✅ 퍼블릭 액세스 차단 (기본 활성화) ✅ 버킷 정책으로 최소 권한 원칙 ✅ 암호화 활성화 (SSE-S3 또는 SSE-KMS) ✅ 버전 관리 활성화 (중요 데이터) ✅ MFA Delete (매우 중요한 데이터) ✅ 액세스 로그 활성화 ✅ CloudTrail로 API 모니터링 ✅ AWS Config로 규정 준수 확인
IAM 정책 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket",
"Condition": {
"StringLike": {
"s3:prefix": ["${aws:username}/*"]
}
}
},
{
"Sid": "AllowUserFolder",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-bucket/${aws:username}/*"
},
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
}
]
}
버킷 정책 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
},
{
"Sid": "AllowOnlyFromVPC",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-1234567890abcdef0"
}
}
},
{
"Sid": "AllowCloudFrontOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::account-id:distribution/E1234567890ABC"
}
}
}
]
}
모니터링 및 감사
CloudWatch 지표
# 버킷 크기 모니터링
aws cloudwatch get-metric-statistics \
--namespace AWS/S3 \
--metric-name BucketSizeBytes \
--dimensions Name=BucketName,Value=my-bucket Name=StorageType,Value=StandardStorage \
--start-time 2024-11-01T00:00:00Z \
--end-time 2024-11-19T23:59:59Z \
--period 86400 \
--statistics Average
# 요청 수 모니터링
aws cloudwatch get-metric-statistics \
--namespace AWS/S3 \
--metric-name NumberOfObjects \
--dimensions Name=BucketName,Value=my-bucket Name=StorageType,Value=AllStorageTypes \
--start-time 2024-11-19T00:00:00Z \
--end-time 2024-11-19T23:59:59Z \
--period 3600 \
--statistics Sum
CloudWatch 알람
# 큰 객체 업로드 알림
aws cloudwatch put-metric-alarm \
--alarm-name large-object-upload \
--alarm-description "Alert when large objects are uploaded" \
--metric-name BytesUploaded \
--namespace AWS/S3 \
--statistic Sum \
--period 300 \
--evaluation-periods 1 \
--threshold 1073741824 \
--comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:region:account-id:my-topic
CloudTrail 로그 분석
-- Athena 쿼리: 삭제 작업 추적
SELECT
useridentity.principalid,
eventtime,
requestparameters.bucketname,
requestparameters.key
FROM cloudtrail_logs
WHERE eventname = 'DeleteObject'
AND eventtime > '2024-11-01'
ORDER BY eventtime DESC;
-- 실패한 접근 시도
SELECT
sourceipaddress,
useridentity.principalid,
eventname,
errorcode,
COUNT(*) as attempts
FROM cloudtrail_logs
WHERE errorcode IS NOT NULL
AND eventtime > '2024-11-01'
GROUP BY sourceipaddress, useridentity.principalid, eventname, errorcode
ORDER BY attempts DESC;
실전 통합 시나리오
시나리오 1: 금융 문서 보관 시스템
요구사항:
- 7년 보관 의무
- 변경 불가
- 접근 감사
- 암호화 필수
구현:
├─ S3 Bucket (버전 관리 + Object Lock)
│ ├─ Encryption: SSE-KMS
│ ├─ Object Lock: COMPLIANCE (7년)
│ └─ MFA Delete: Enabled
├─ 액세스 로그 → Glacier (장기 보관)
├─ CloudTrail → 모든 API 로깅
└─ EventBridge → 이상 활동 알림
시나리오 2: 멀티테넌트 SaaS 플랫폼
요구사항:
- 고객별 데이터 격리
- 고객별 접근 제어
- 암호화
구현:
├─ S3 Bucket: saas-customer-data
├─ 액세스 포인트 (고객별)
│ ├─ customer-a-ap → /customer-a/*
│ ├─ customer-b-ap → /customer-b/*
│ └─ customer-c-ap → /customer-c/*
├─ 각 액세스 포인트에 정책
│ └─ 해당 고객 역할만 접근
└─ SSE-KMS (고객별 키)
시나리오 3: 개인정보 보호 데이터 레이크
요구사항:
- 민감 정보 자동 마스킹
- 부서별 데이터 필터링
- 감사 로그
구현:
├─ S3 Bucket (원본 데이터)
├─ Object Lambda Access Point
│ ├─ Lambda: 개인정보 마스킹
│ └─ Lambda: 부서별 필터링
├─ 부서별 IAM 역할
│ └─ Object Lambda AP 접근
└─ 모든 접근 로깅
비용 최적화 with 보안
암호화 비용
SSE-S3: 무료
SSE-KMS: $1/월 (키) + $0.03/10,000 요청
SSE-KMS + Bucket Key: 99% 절감
SSE-C: 무료 (관리 부담)
액세스 로그 비용 관리
{
"Rules": [
{
"Id": "ManageAccessLogs",
"Status": "Enabled",
"Filter": {"Prefix": "logs/"},
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER"}
],
"Expiration": {"Days": 2555}
}
]
}
S3 보안은 다층 방어(Defense in Depth) 전략이 필요
- 암호화: SSE-S3 (기본) 또는 SSE-KMS (고급)
- CORS: 웹 애플리케이션 통합
- MFA Delete: 중요 데이터 보호
- 액세스 로그: 모든 활동 추적
- Pre-Signed URLs: 임시 접근 제어
- Object Lock: 규정 준수 (WORM)
- 액세스 포인트: 복잡한 접근 제어 단순화
- Object Lambda: 동적 데이터 변환
Share article