Amazon S3: ảnh sản phẩm của shop-app rời khỏi ổ đĩa EC2
Tiếp đi: S3. Bài trước shop-app chạy 2 instance trong ASG, và mình phát hiện ảnh sản phẩm người bán upload đang lưu vào /opt/shop-app/uploads trên từng máy. Máy A có ảnh, máy B không; ASG thay máy là mất luôn ảnh. Giờ mình phải lưu ở đâu?
Bạn vừa đụng đúng bài học kinh điển: app stateless thì file phải ra ngoài. Trên AWS, nơi đó là Amazon S3 (Simple Storage Service): object storage bền (durability 99.999999999%, "11 số 9"), rẻ, không giới hạn dung lượng, truy cập qua HTTP. S3 xuất hiện ở gần như mọi câu đề SAA nên bài này đi khá kỹ.
1. Bài toán: ảnh sản phẩm trên đĩa EC2
POST /api/products/42/images (multipart/form-data, 2 MB)
→ MultipartFile.transferTo(new File("/opt/shop-app/uploads/42/front.jpg"))
→ DB lưu path "/uploads/42/front.jpg"
GET /uploads/42/front.jpg → Spring serve static từ đĩa
Chạy một máy thì ổn. Sau bài 22.5:
ALB ─┬─→ EC2-A /opt/shop-app/uploads/42/front.jpg (có)
└─→ EC2-B /opt/shop-app/uploads/42/ (trống) → 404
2. Vấn đề
- Không chia sẻ: mỗi EC2 một đĩa EBS riêng. Request upload và request đọc có thể rơi vào máy khác nhau.
- Mất dữ liệu: ASG terminate instance là EBS root cũng đi theo (mặc định
DeleteOnTermination=true). - Tốn tài nguyên app: Tomcat thread ngồi stream ảnh 2 MB cho mỗi request thay vì làm nghiệp vụ. Băng thông EC2 trả tiền, không có cache.
- Backup, phân quyền, dọn ảnh cũ: tất cả phải tự viết.
- Lựa chọn khác: EFS (NFS chia sẻ) làm được nhưng đắt hơn 10 lần/GB và không cần thiết cho file tĩnh phục vụ qua web. So sánh chi tiết ở bài 22.16.
3. Giải pháp: S3 và các khái niệm đề thi
Người bán ──(1) POST /api/products/42/images (metadata) ──→ shop-app
←─(2) presigned PUT URL (hạn 5 phút) ────────────┘
──(3) PUT ảnh thẳng lên S3 ──→ s3://shop-app-assets-$ACCOUNT_ID/products/42/front.jpg
│
Khách ←──(4) GET qua CloudFront (bài 22.10) ──┘ (hoặc presigned GET nếu ảnh riêng tư)
│
(5) S3 event → SQS → Lambda shop-image-resize (bài 22.11/22.12)
| Khái niệm | Giải thích ngắn | Từ khóa đề thi |
|---|---|---|
| Bucket | Thùng chứa, tên duy nhất toàn cầu, nằm trong một region; tối đa 100 mặc định (xin tăng được) | globally unique, regional, flat namespace |
| Object | Key (đường dẫn, không có folder thật) + data (0 B – 5 TB) + metadata + version ID | key prefix, 5 GB/PUT đơn → multipart ≥ 100 MB |
| Storage class | Mức giá/độ sẵn sàng khác nhau cho cùng durability 11 số 9 (trừ One Zone) | Standard, Intelligent-Tiering, Standard-IA, One Zone-IA, Glacier Instant/Flexible/Deep Archive, Express One Zone |
| Lifecycle rule | Tự chuyển class hoặc xóa theo tuổi object / theo prefix / tag | transition, expiration, noncurrent version, abort incomplete multipart |
| Versioning | Giữ mọi phiên bản; xóa chỉ tạo delete marker | MFA Delete, protect from accidental deletion |
| Encryption | SSE-S3 (mặc định, AES-256), SSE-KMS (key của bạn, audit CloudTrail), DSSE-KMS, SSE-C (bạn gửi key), client-side | encrypt at rest, audit key usage → SSE-KMS; bucket key giảm phí KMS |
| Block Public Access | 4 công tắc chặn ACL/policy public ở cấp bucket và account; mặc định bật | "accidentally made public" → BPA |
| Bucket policy | Resource-based policy JSON; phân quyền cross-account, buộc TLS, buộc mã hóa | aws:SecureTransport, Principal cross-account, Deny ưu tiên |
| ACL | Cơ chế cũ; mặc định tắt (Object Ownership = Bucket owner enforced) | hiếm khi là đáp án đúng |
| Presigned URL | URL ký bằng credential của app, ai có URL dùng được trong N giây, cho GET/PUT | temporary access, upload directly without exposing credentials |
| Event notification | Object created/removed → SQS / SNS / Lambda / EventBridge | process after upload, EventBridge cho filter phức tạp |
| Replication | CRR (khác region) / SRR (cùng region), cần versioning hai đầu, chỉ object mới (Batch Replication cho cũ) | compliance, DR, lower latency other region |
| Transfer Acceleration | Upload tới edge location gần user rồi đi backbone AWS về bucket | global users uploading large files |
| Object Lock | WORM: Governance / Compliance mode, legal hold | regulatory, cannot be deleted even by root |
| Consistency | Strong read-after-write cho mọi thao tác (từ 12/2020) | không còn "eventual consistency for overwrite PUT" |
S3 là dịch vụ regional nhưng tên bucket toàn cầu, và endpoint có dạng https://shop-app-assets-123.s3.ap-southeast-1.amazonaws.com/products/42/front.jpg. Nếu bị khóa vì "bản đồ dịch vụ" thì xem lại bài tổng quan; bài này chỉ đi sâu S3.
4. Cấu hình từng bước (CLI + Java)
Bước 1: Bucket với mọi khóa an toàn bật sẵn.
export AWS_REGION=ap-southeast-1
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text --profile shop-lab)
BUCKET=shop-app-assets-$ACCOUNT_ID
# Ngoài us-east-1 phải có LocationConstraint
aws s3api create-bucket --bucket $BUCKET --region $AWS_REGION \
--create-bucket-configuration LocationConstraint=$AWS_REGION --profile shop-lab
aws s3api put-bucket-tagging --bucket $BUCKET \
--tagging 'TagSet=[{Key=Project,Value=shop-app},{Key=Env,Value=lab}]' --profile shop-lab
# Block Public Access: cả 4 công tắc
aws s3api put-public-access-block --bucket $BUCKET --public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true --profile shop-lab
# Versioning
aws s3api put-bucket-versioning --bucket $BUCKET \
--versioning-configuration Status=Enabled --profile shop-lab
# Mã hóa mặc định SSE-KMS với key của shop (alias/shop-key, bài 22.14); chưa có key thì bỏ qua, S3 đã SSE-S3 mặc định
aws s3api put-bucket-encryption --bucket $BUCKET --server-side-encryption-configuration '{
"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "aws:kms", "KMSMasterKeyID": "alias/shop-key"},
"BucketKeyEnabled": true}]}' --profile shop-lab
Bước 2: Lifecycle. Ảnh sản phẩm bán được thì xem nhiều, hết mùa thì ít; ảnh gốc độ phân giải cao chỉ cần giữ để resize lại.
cat > lifecycle.json <<'EOF'
{
"Rules": [
{
"ID": "originals-to-cold",
"Filter": {"Prefix": "originals/"},
"Status": "Enabled",
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 180, "StorageClass": "GLACIER_IR"}
],
"NoncurrentVersionTransitions": [{"NoncurrentDays": 7, "StorageClass": "GLACIER"}],
"NoncurrentVersionExpiration": {"NoncurrentDays": 90}
},
{
"ID": "tmp-uploads-expire",
"Filter": {"Prefix": "tmp/"},
"Status": "Enabled",
"Expiration": {"Days": 1}
},
{
"ID": "abort-multipart",
"Filter": {"Prefix": ""},
"Status": "Enabled",
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 3}
}
]
}
EOF
aws s3api put-bucket-lifecycle-configuration --bucket $BUCKET --lifecycle-configuration file://lifecycle.json --profile shop-lab
Hai lưu ý thực tế: object phải ở Standard ≥ 30 ngày trước khi sang IA; object < 128 KB chuyển sang IA không tiết kiệm (IA tính tối thiểu 128 KB). Rule abort-multipart luôn nên có: upload đứt giữa chừng để lại part "vô hình" vẫn tính tiền.
Bước 3: Bucket policy chỉ cho role của app, buộc TLS. Role shop-app-ec2-role đã có identity policy (bài 22.4); thêm bucket policy là lớp thứ hai: với cùng account, chỉ cần một trong hai Allow và không có Deny; với khác account phải cả hai.
cat > bucket-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowShopAppRole",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::$ACCOUNT_ID:role/shop-app-ec2-role"},
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::$BUCKET", "arn:aws:s3:::$BUCKET/*"]
},
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::$BUCKET", "arn:aws:s3:::$BUCKET/*"],
"Condition": {"Bool": {"aws:SecureTransport": "false"}}
},
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::$BUCKET/*",
"Condition": {"StringNotEquals": {"s3:x-amz-server-side-encryption": "aws:kms"}}
}
]
}
EOF
aws s3api put-bucket-policy --bucket $BUCKET --policy file://bucket-policy.json --profile shop-lab
Chú ý ListBucket áp trên ARN bucket (không có /*), còn GetObject áp trên /*. Viết nhầm là lỗi 403 kinh điển.
Bước 4: Java SDK v2 trong Spring Boot.
<!-- pom.xml -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.29.0</version> <!-- dùng bản mới nhất khi bạn đọc -->
<type>pom</type><scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency><groupId>software.amazon.awssdk</groupId><artifactId>s3</artifactId></dependency>
<dependency><groupId>software.amazon.awssdk</groupId><artifactId>s3-transfer-manager</artifactId></dependency>
<dependency><groupId>software.amazon.awssdk.crt</groupId><artifactId>aws-crt</artifactId><version>0.31.3</version></dependency>
</dependencies>
# application-aws.yml
shop:
s3:
bucket: shop-app-assets-${AWS_ACCOUNT_ID}
region: ap-southeast-1
presign-ttl: 5m
@Configuration
@EnableConfigurationProperties(S3Props.class)
public class S3Config {
@Bean
S3Client s3Client(S3Props props) {
// Không truyền credential: DefaultCredentialsProvider lấy từ role EC2 (IMDSv2),
// ở local lấy từ profile shop-lab trong ~/.aws/credentials
return S3Client.builder()
.region(Region.of(props.region()))
.build();
}
@Bean
S3Presigner s3Presigner(S3Props props) {
return S3Presigner.builder().region(Region.of(props.region())).build();
}
@Bean
S3TransferManager transferManager(S3Props props) {
// CRT client: multipart tự động, song song, dùng cho file lớn
S3AsyncClient async = S3AsyncClient.crtBuilder()
.region(Region.of(props.region()))
.targetThroughputInGbps(5.0)
.minimumPartSizeInBytes(8L * 1024 * 1024)
.build();
return S3TransferManager.builder().s3Client(async).build();
}
}
@ConfigurationProperties("shop.s3")
public record S3Props(String bucket, String region, Duration presignTtl) {}
@Service
public class ProductImageService {
private final S3Client s3;
private final S3Presigner presigner;
private final S3Props props;
public ProductImageService(S3Client s3, S3Presigner presigner, S3Props props) {
this.s3 = s3; this.presigner = presigner; this.props = props;
}
/** Cách 1: server upload (file nhỏ, đã qua validate ở app) */
public String upload(long productId, MultipartFile file) throws IOException {
String key = "products/%d/%s.%s".formatted(productId, UUID.randomUUID(),
StringUtils.getFilenameExtension(file.getOriginalFilename()));
s3.putObject(PutObjectRequest.builder()
.bucket(props.bucket()).key(key)
.contentType(file.getContentType())
.serverSideEncryption(ServerSideEncryption.AWS_KMS) // khớp bucket policy
.metadata(Map.of("product-id", String.valueOf(productId)))
.build(),
RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
return key; // DB lưu key, không lưu URL
}
/** Cách 2: presigned PUT, client upload thẳng lên S3 */
public PresignedUpload presignUpload(long productId, String contentType) {
String key = "tmp/%d/%s".formatted(productId, UUID.randomUUID());
PutObjectRequest put = PutObjectRequest.builder()
.bucket(props.bucket()).key(key)
.contentType(contentType)
.serverSideEncryption(ServerSideEncryption.AWS_KMS)
.build();
PresignedPutObjectRequest signed = presigner.presignPutObject(
PutObjectPresignRequest.builder()
.signatureDuration(props.presignTtl())
.putObjectRequest(put).build());
// client phải gửi đúng Content-Type và header x-amz-server-side-encryption đã ký
return new PresignedUpload(key, signed.url().toString(), signed.signedHeaders());
}
/** Presigned GET cho ảnh riêng tư (hóa đơn, CMND người bán...) */
public URL presignDownload(String key) {
return presigner.presignGetObject(GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(2))
.getObjectRequest(GetObjectRequest.builder()
.bucket(props.bucket()).key(key)
.responseContentDisposition("inline").build())
.build()).url();
}
public record PresignedUpload(String key, String url, Map<String, List<String>> headers) {}
}
@RestController
@RequestMapping("/api/products/{id}/images")
public class ProductImageController {
private final ProductImageService images;
private final ProductService products;
// constructor...
@PostMapping("/presign")
@PreAuthorize("@productAuth.isOwner(#id)")
public ProductImageService.PresignedUpload presign(@PathVariable long id,
@RequestParam String contentType) {
if (!Set.of("image/jpeg", "image/png", "image/webp").contains(contentType))
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "unsupported type");
return images.presignUpload(id, contentType);
}
@PostMapping("/confirm")
@PreAuthorize("@productAuth.isOwner(#id)")
public void confirm(@PathVariable long id, @RequestParam String key) {
// client báo đã PUT xong; app HEAD object, kiểm tra size/type rồi mới ghi DB
products.attachImage(id, key);
}
}
Vì sao có bước confirm: S3 không biết nghiệp vụ của bạn. Presigned URL cho phép client PUT, nhưng client có thể không PUT, hoặc PUT file 50 MB rác. App chỉ ghi DB sau khi headObject xác nhận; rác trong tmp/ được lifecycle xóa sau 1 ngày; contentLength giới hạn bằng presigned POST policy nếu cần chặt hơn.
Bước 5: Test bằng CLI.
# Upload với CLI (SDK dưới mũ, multipart tự động > 8 MB)
aws s3 cp ./front.jpg s3://$BUCKET/products/42/front.jpg --sse aws:kms --sse-kms-key-id alias/shop-key --profile shop-lab
aws s3 ls s3://$BUCKET/products/42/ --profile shop-lab
aws s3api head-object --bucket $BUCKET --key products/42/front.jpg --profile shop-lab
# "ServerSideEncryption": "aws:kms", "VersionId": "..."
# Presign bằng CLI để thử nhanh (GET, 300s)
aws s3 presign s3://$BUCKET/products/42/front.jpg --expires-in 300 --profile shop-lab
curl -s -o /tmp/x.jpg -w "%{http_code}\n" "<url vừa in>" # 200; sau 5 phút → 403
5. Cách hoạt động
Presigned URL không gọi về AWS khi tạo. Nó chỉ là chữ ký SigV4 tính bằng credential hiện tại của app:
app (role shop-app-ec2-role, credential tạm từ IMDS, hết hạn sau ~6h)
│ HMAC-SHA256(secret, canonical request: PUT /tmp/42/uuid, host, x-amz-date, expires=300, content-type...)
▼
https://shop-app-assets-123.s3.ap-southeast-1.amazonaws.com/tmp/42/uuid
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=ASIA.../20250101/ap-southeast-1/s3/aws4_request
&X-Amz-Date=...&X-Amz-Expires=300&X-Amz-SignedHeaders=content-type;host;x-amz-server-side-encryption
&X-Amz-Security-Token=... ← token của role tạm
&X-Amz-Signature=...
client PUT → S3 tính lại chữ ký với secret của credential đó
→ đúng, chưa hết hạn, credential còn sống, IAM/bucket policy cho phép → 200
→ sai header đã ký (content-type khác) → 403 SignatureDoesNotMatch
Hệ quả quan trọng: presigned URL không sống lâu hơn credential ký nó. Role EC2 có credential xoay vòng ~6 giờ, nên presign "7 ngày" bằng role sẽ chết sớm hơn. Cần link dài ngày (email hóa đơn) thì dùng IAM user key chuyên dụng, hoặc tốt hơn là CloudFront signed URL (bài 22.10).
Quyền truy cập được quyết định thế nào (cùng account):
Request → có Deny ở bất kỳ đâu (SCP, identity, bucket policy, BPA)? → DENY
→ có Allow ở identity policy HOẶC bucket policy? → ALLOW
→ không → DENY (mặc định)
Public? → Block Public Access bật → mọi policy/ACL public bị vô hiệu, kể cả khi bạn cố ý
Versioning và delete marker:
PUT front.jpg → v1
PUT front.jpg → v2 (v1 vẫn còn, tính tiền)
DELETE front.jpg → tạo delete marker (v3); GET → 404; v1, v2 vẫn còn
"Khôi phục" = DELETE delete marker (cần quyền DeleteObjectVersion)
Xóa thật = DELETE ?versionId=v1 (MFA Delete bật thì phải có MFA, chỉ root/CLI làm được)
Multipart upload cho file lớn (ảnh gốc 4K, video):
CreateMultipartUpload → UploadId
UploadPart #1 (8 MB) ─┐
UploadPart #2 (8 MB) ─┼─ song song, retry từng part
UploadPart #3 (3 MB) ─┘
CompleteMultipartUpload (danh sách ETag) → object xuất hiện (atomic)
(hoặc AbortMultipartUpload / lifecycle abort sau 3 ngày)
SDK v2 S3TransferManager làm việc này tự động; bạn chỉ gọi uploadFile.
6. So sánh
Storage class — bảng này đáng học thuộc:
| Class | AZ | Phí lấy (retrieval) | Thời gian lấy | Tối thiểu | Dùng cho |
|---|---|---|---|---|---|
| Standard | ≥3 | Không | ms | Không | Dữ liệu truy cập thường xuyên, ảnh sản phẩm đang bán |
| Intelligent-Tiering | ≥3 | Không; phí giám sát/object | ms (tier Archive opt-in: phút–giờ) | Không | Không đoán được access pattern, object > 128 KB |
| Standard-IA | ≥3 | Có (per GB) | ms | 30 ngày, 128 KB | Backup, ảnh gốc ít xem nhưng cần ngay |
| One Zone-IA | 1 | Có | ms | 30 ngày | Dữ liệu tái tạo được (thumbnail), rẻ hơn IA ~20% |
| Glacier Instant Retrieval | ≥3 | Cao hơn IA | ms | 90 ngày | Archive truy cập quý một lần nhưng cần ngay (ảnh y tế) |
| Glacier Flexible Retrieval | ≥3 | Có; Expedited 1–5 phút, Standard 3–5 giờ, Bulk 5–12 giờ (free) | phút–giờ | 90 ngày | Backup năm, chấp nhận chờ |
| Glacier Deep Archive | ≥3 | Có; Standard 12 giờ, Bulk 48 giờ | giờ | 180 ngày | Lưu trữ pháp lý 7–10 năm, rẻ nhất |
| Express One Zone | 1 (directory bucket) | Không | < ms | Không | ML training, latency một chữ số ms, đắt |
Đề thi hay bẫy: "cần lấy trong vài phút" → Glacier Flexible Expedited (không phải Deep Archive); "cần ngay lập tức nhưng hiếm khi" → Glacier Instant; "không biết pattern" → Intelligent-Tiering; "tái tạo được / không quan trọng mất khi AZ hỏng" → One Zone-IA.
Cách cấp quyền truy cập object
| Bucket policy / IAM | Presigned URL | CloudFront + OAC | Public bucket | |
|---|---|---|---|---|
| Ai dùng | Principal AWS (role, user, account) | Bất kỳ ai có URL, trong N giây | Public qua CDN, bucket vẫn private | Mọi người |
| Dùng cho shop-app | App EC2/Lambda đọc ghi | Upload từ browser, tải file riêng tư | Ảnh sản phẩm public (bài 22.10) | Không khuyến nghị; website tĩnh thì qua CloudFront |
| Cache / tốc độ | — | Không cache được (URL thay đổi) | Edge cache, HTTPS, custom domain | Không cache |
S3 vs EBS vs EFS (tóm; chi tiết bài 22.16)
| S3 | EBS | EFS | |
|---|---|---|---|
| Loại | Object, HTTP API | Block, gắn 1 EC2 (io2 multi-attach cùng AZ) | File (NFS), nhiều EC2, nhiều AZ |
| Dùng khi | File tĩnh, backup, data lake, log | Root disk, DB tự dựng | Shared home, CMS legacy cần filesystem |
| Giá tương đối | Rẻ nhất/GB | Trung bình, trả theo provisioned | Đắt nhất/GB (có IA) |
7. Khi nào dùng / không
- Dùng S3: mọi file người dùng upload, static asset, backup DB, log, data lake, artifact (jar) cho deploy, website tĩnh (sau CloudFront).
- Không dùng S3 như filesystem: không có append, không rename (copy + delete), không lock; app cần POSIX → EFS. Không dùng làm DB: không query theo nội dung (Athena query log/CSV thì được).
- Không lưu URL S3 vào DB — lưu key, URL sinh lúc đọc (presigned hoặc CloudFront), để sau đổi bucket/CDN không phải migrate.
- Không để app stream ảnh public qua Tomcat: dùng CloudFront đọc thẳng S3 (bài 22.10). App chỉ giữ metadata.
Team frontend báo ảnh sản phẩm bị 403 và bảo mình "cứ mở public bucket cho nhanh, ảnh sản phẩm có gì mà giấu". Mình thấy cũng có lý, ảnh public thật mà. Mở ACL public-read được không?
Bạn đúng ở một nửa: ảnh sản phẩm là public về nội dung. Nhưng "public bucket" là chuyện khác: nó là cách phục vụ, và cách này kém ở cả ba mặt bảo mật, hiệu năng, chi phí.
Vì sao không mở public bucket:
- Bucket chứa cả thứ không public:
originals/,tmp/, sau nàyinvoices/. Chỉ cần một dev lỡ để policyResource: "arn:aws:s3:::bucket/*"là lộ hết. Hầu hết vụ rò rỉ dữ liệu "S3 leak" trên báo đều bắt đầu từ "mở tạm cho nhanh". Block Public Access tồn tại chính vì lý do này và AWS Config rules3-bucket-public-read-prohibitedsẽ đỏ ngay. - Không có cache, không có custom domain, không có HTTPS với domain của bạn: URL dạng
bucket.s3.ap-southeast-1.amazonaws.com, mỗi request đi thẳng vào S3, tốn phí request + data transfer out (S3 → Internet đắt hơn CloudFront → Internet). - Không hạn chế được: hotlink từ web khác, bot cào toàn bộ ảnh, không có WAF.
- ACL là cơ chế cũ: bucket mới mặc định
BucketOwnerEnforced, ACL bị vô hiệu; muốn dùng phải cố ý bật lại. Đề SAA: đáp án có chữ "ACL" gần như luôn sai.
Cách đúng cho ảnh public: CloudFront + Origin Access Control (OAC)
Khách ──HTTPS──→ cdn.shop.example.com (CloudFront, cache ở edge Singapore/HCM)
│ miss → ký request SigV4 bằng OAC
▼
s3://shop-app-assets-… (bucket vẫn private, BPA vẫn bật)
bucket policy: Allow s3:GetObject cho Principal cloudfront.amazonaws.com
Condition AWS:SourceArn = arn của distribution, prefix products/*
Bucket policy thêm statement này (chỉ prefix public):
{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": {"Service": "cloudfront.amazonaws.com"},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::shop-app-assets-ACCOUNT_ID/products/*",
"Condition": {"StringEquals": {"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/EDFDVBD6EXAMPLE"}}
}
Frontend nhận URL https://cdn.shop.example.com/products/42/front.jpg, nhanh hơn, rẻ hơn, và bucket không public. Bài 22.10 dựng phần này. Trong lúc chờ, cho lab hôm nay, dùng presigned GET với TTL dài (1 giờ) để frontend hiển thị được; đó là "mở tạm" an toàn.
Còn 403 của frontend hôm nay có thể do: (1) role app thiếu s3:GetObject trên đúng prefix; (2) object được upload bằng SSE-KMS nhưng role không có kms:Decrypt trên alias/shop-key — lỗi rất hay gặp khi bật SSE-KMS; (3) presigned URL gửi lên thiếu header đã ký. Kiểm tra bằng:
aws s3api get-object --bucket $BUCKET --key products/42/front.jpg /tmp/x.jpg --profile shop-lab # lỗi gì?
aws kms describe-key --key-id alias/shop-key --profile shop-lab # key policy có role chưa?
Mình tưởng S3 rẻ, nhưng tháng này hóa đơn S3 cao hơn EC2. Bucket chỉ có khoảng 20 GB ảnh. Mình bật versioning và có một job đồng bộ chạy aws s3 sync mỗi phút. Tiền đi đâu vậy?
Chính xác hai thứ bạn vừa nhắc là hai thủ phạm điển hình. Giá lưu trữ S3 (khoảng vài cent/GB/tháng, 20 GB gần như miễn phí) không phải vấn đề. Hóa đơn S3 gồm 5 nhóm:
| Nhóm | Tính theo | Hay gây bất ngờ khi |
|---|---|---|
| Storage | GB-tháng, theo class | Versioning giữ mọi bản cũ; multipart dở dang; log bucket không lifecycle |
| Requests | Số PUT/COPY/POST/LIST (đắt hơn) và GET/SELECT (rẻ hơn), tính trên 1000 request | aws s3 sync / ls mỗi phút → hàng triệu LIST; app poll bucket; nhiều file nhỏ |
| Data transfer out | GB ra Internet hoặc sang region khác (vào S3 miễn phí; sang EC2 cùng region miễn phí; sang CloudFront miễn phí) | Phục vụ ảnh thẳng từ S3 ra Internet không qua CloudFront |
| Retrieval | GB lấy ra từ IA/Glacier, phí sớm (xóa trước 30/90/180 ngày) | Lifecycle đẩy sang IA nhưng app vẫn đọc thường xuyên |
| Management / KMS | Inventory, Analytics, Intelligent-Tiering monitoring; mỗi GET/PUT SSE-KMS = 1 KMS request | SSE-KMS không bật Bucket Key → phí KMS gấp nhiều lần phí S3 |
Chẩn đoán:
# Dung lượng thật gồm cả noncurrent version (CloudWatch BucketSizeBytes, metric hàng ngày)
aws cloudwatch get-metric-statistics --namespace AWS/S3 --metric-name BucketSizeBytes \
--dimensions Name=BucketName,Value=$BUCKET Name=StorageType,Value=StandardStorage \
--start-time $(date -u -d '-3 day' +%FT%TZ) --end-time $(date -u +%FT%TZ) --period 86400 --statistics Average --profile shop-lab
# Bao nhiêu version cũ?
aws s3api list-object-versions --bucket $BUCKET --query 'length(Versions[?IsLatest==`false`])' --profile shop-lab
# Multipart dở
aws s3api list-multipart-uploads --bucket $BUCKET --profile shop-lab
# Cost Explorer: lọc Service = S3, group by Usage Type → thấy "Requests-Tier1" (PUT/LIST) hay "TimedStorage" lớn hơn
Sửa:
aws s3 syncmỗi phút: mỗi lần là một loạt LIST trên toàn prefix. Thay bằng S3 event → SQS: S3 báo khi có object mới, consumer chỉ xử lý đúng object đó (bài 22.11 làm sâu; mục Lab dưới có bước bật event). Đây cũng là câu đề thi: "application polls S3 every minute, high cost" → event notification.- Versioning: giữ, nhưng thêm
NoncurrentVersionExpiration(đã có trong lifecycle ở Bước 2). Với ảnh sản phẩm thay thế là ghi key mới (UUID) thay vì overwrite cùng key, versioning thành ra chỉ chống xóa nhầm. - SSE-KMS: bật
BucketKeyEnabled(đã có ở Bước 1); giảm ~99% KMS request. - Serve ảnh qua CloudFront; data transfer S3 → CloudFront miễn phí.
- Bật S3 Storage Lens (dashboard mặc định miễn phí) để thấy phân bố class, version, multipart trên mọi bucket.
Số liệu giá thay đổi theo region, luôn đối chiếu pricing page; nhưng cấu trúc 5 nhóm thì không đổi và là thứ phỏng vấn muốn nghe.
Cho mình bài lab để tự tay làm phần này nhé.
Lab 22.6: Bucket shop-app-assets với BPA, versioning, lifecycle, bucket policy và presigned URL từ Spring Boot
Mục tiêu
- Bucket
shop-app-assets-$ACCOUNT_IDcó đủ 4 công tắc BPA, versioning Enabled, mã hóa mặc định, 3 lifecycle rule, bucket policy chỉ choshop-app-ec2-rolevà buộc TLS. - Từ EC2 trong ASG (hoặc local với profile), Spring Boot upload được ảnh và sinh presigned GET/PUT URL; curl dùng URL đó thành công rồi hết hạn trả 403.
- Từ profile
shop-lab(không phải role app), thao tác bị chặn đúng chỗ bucket policy nói chặn; HTTP không TLS bị từ chối. - Object mới tạo trong
products/sinh message vào SQSshop-order-eventstạm (chỉ để thấy event; bài 22.11 làm đúng queue).
Chi phí & thời gian
- S3: Free Tier 5 GB Standard, 20.000 GET, 2.000 PUT/tháng trong 12 tháng đầu. Lab này dùng vài MB và vài trăm request: gần 0 USD.
- KMS: key tùy chỉnh khoảng 1 USD/tháng (tính theo ngày) + request. Nếu chưa làm bài 22.14, dùng SSE-S3 (bỏ phần KMS) để không phát sinh.
- EC2 trong ASG từ bài 22.5 nếu còn chạy: như bài đó; không bắt buộc, có thể chạy Spring Boot ở local với profile
shop-lab. - SQS: Free Tier 1 triệu request/tháng.
- Thời gian: 60–75 phút.
Chuẩn bị
- Profile
shop-lab, biếnAWS_REGION,ACCOUNT_ID. Roleshop-app-ec2-roletồn tại (bài 22.4). - Tùy chọn: KMS key
alias/shop-key(bài 22.14) với key policy cho phép role appkms:Encrypt/Decrypt/GenerateDataKey. Không có thì thayaws:kmsbằngAES256ở mọi chỗ. - Source shop-app với code ở mục 4; chạy local bằng
AWS_PROFILE=shop-lab SPRING_PROFILES_ACTIVE=aws ./mvnw spring-boot:run. - Một file ảnh
front.jpgđể test.
Các bước
Bước 1 – Tạo bucket, BPA, versioning, mã hóa: chạy Bước 1 ở mục 4.
Console: S3 → Create bucket → Region ap-southeast-1 → Object Ownership: ACLs disabled → Block all public access (giữ tick) → Bucket Versioning: Enable → Default encryption: SSE-KMS, chọn alias/shop-key, Bucket Key: Enable → Tags.
aws s3api get-public-access-block --bucket $BUCKET --profile shop-lab
aws s3api get-bucket-versioning --bucket $BUCKET --profile shop-lab # "Status": "Enabled"
aws s3api get-bucket-encryption --bucket $BUCKET --profile shop-lab
Bước 2 – Lifecycle: chạy Bước 2 ở mục 4.
Console: bucket → Management → Create lifecycle rule → Prefix originals/ → Transition current versions 30 ngày Standard-IA, 180 ngày Glacier Instant → Noncurrent 7 ngày Glacier Flexible, expire 90 ngày; rule thứ hai prefix tmp/ expire 1 ngày; rule thứ ba toàn bucket, Delete incomplete multipart 3 ngày.
aws s3api get-bucket-lifecycle-configuration --bucket $BUCKET --query 'Rules[].[ID,Status]' --output table --profile shop-lab
Bước 3 – Bucket policy: chạy Bước 3 ở mục 4. Tạm thêm vào statement AllowShopAppRole một principal nữa là ShopAdminRole (role bạn assume ở profile shop-lab) để chính bạn có thể upload file test; sẽ gỡ ở Thử thêm.
Console: bucket → Permissions → Bucket policy → Edit → dán JSON.
aws s3api get-bucket-policy --bucket $BUCKET --query Policy --output text --profile shop-lab | python -m json.tool
Bước 4 – Upload và presign bằng CLI: chạy Bước 5 ở mục 4.
Bước 5 – Spring Boot: chạy app, gọi:
# presigned PUT
curl -s -X POST "http://localhost:8080/api/products/42/images/presign?contentType=image/jpeg" \
-H "Authorization: Bearer $TOKEN" | tee presign.json
URL=$(jq -r .url presign.json); KEY=$(jq -r .key presign.json)
# upload thẳng lên S3 bằng URL đó (phải gửi đúng header đã ký)
curl -s -o /dev/null -w "%{http_code}\n" -X PUT "$URL" \
-H "Content-Type: image/jpeg" -H "x-amz-server-side-encryption: aws:kms" \
--data-binary @front.jpg
# 200
# confirm → app HEAD object, ghi DB
curl -s -X POST "http://localhost:8080/api/products/42/images/confirm?key=$KEY" -H "Authorization: Bearer $TOKEN"
Nếu TOKEN chưa có vì bạn chưa cấu hình auth cho lab, tạm permitAll hai endpoint này trong profile lab.
Bước 6 – Event notification → SQS (nhìn trước bài 22.11):
QUEUE_URL=$(aws sqs create-queue --queue-name shop-s3-events-lab \
--tags Project=shop-app,Env=lab --query QueueUrl --output text --profile shop-lab)
QUEUE_ARN=$(aws sqs get-queue-attributes --queue-url $QUEUE_URL --attribute-names QueueArn \
--query Attributes.QueueArn --output text --profile shop-lab)
# Queue policy: cho S3 (từ đúng bucket) gửi message
aws sqs set-queue-attributes --queue-url $QUEUE_URL --attributes "{\"Policy\": \"$(cat <<EOF | jq -c . | sed 's/"/\\\\"/g'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"s3.amazonaws.com"},
"Action":"sqs:SendMessage","Resource":"$QUEUE_ARN",
"Condition":{"ArnLike":{"aws:SourceArn":"arn:aws:s3:::$BUCKET"}}}]}
EOF
)\"}" --profile shop-lab
aws s3api put-bucket-notification-configuration --bucket $BUCKET --notification-configuration "{
\"QueueConfigurations\": [{\"QueueArn\": \"$QUEUE_ARN\", \"Events\": [\"s3:ObjectCreated:*\"],
\"Filter\": {\"Key\": {\"FilterRules\": [{\"Name\": \"prefix\", \"Value\": \"products/\"}]}}}]}" --profile shop-lab
Console: bucket → Properties → Event notifications → Create → prefix products/ → All object create events → Destination SQS queue. (Nếu lệnh set policy khó gõ, dán policy trong Console SQS → Access policy.)
Kiểm tra kết quả
# 1. BPA chặn thật: cố đặt ACL public → lỗi AccessControlListNotSupported hoặc AccessDenied
aws s3api put-object-acl --bucket $BUCKET --key products/42/front.jpg --acl public-read --profile shop-lab
# 2. HTTP không TLS bị Deny (endpoint http)
curl -s -o /dev/null -w "%{http_code}\n" "http://$BUCKET.s3.ap-southeast-1.amazonaws.com/products/42/front.jpg" # 403
# 3. Upload không khai SSE-KMS bị Deny
aws s3 cp front.jpg s3://$BUCKET/products/42/plain.jpg --profile shop-lab # AccessDenied (bucket policy DenyUnencryptedUploads)
# 4. Versioning: overwrite rồi xem 2 version
aws s3 cp front.jpg s3://$BUCKET/products/42/front.jpg --sse aws:kms --sse-kms-key-id alias/shop-key --profile shop-lab
aws s3api list-object-versions --bucket $BUCKET --prefix products/42/front.jpg \
--query 'Versions[].[VersionId,IsLatest,LastModified]' --output table --profile shop-lab
# 5. Presigned GET hết hạn
U=$(aws s3 presign s3://$BUCKET/products/42/front.jpg --expires-in 15 --profile shop-lab)
curl -s -o /dev/null -w "%{http_code}\n" "$U"; sleep 20; curl -s -o /dev/null -w "%{http_code}\n" "$U" # 200 rồi 403
# 6. Event đã vào SQS
aws sqs receive-message --queue-url $QUEUE_URL --max-number-of-messages 5 --profile shop-lab \
| jq -r '.Messages[].Body | fromjson | .Records[]? | [.eventName, .s3.object.key, .s3.object.size] | @tsv'
# ObjectCreated:Put products/42/front.jpg 204812
Thử thêm
A. Xóa nhầm và khôi phục.
aws s3 rm s3://$BUCKET/products/42/front.jpg --profile shop-lab # tạo delete marker
aws s3 ls s3://$BUCKET/products/42/ --profile shop-lab # không thấy nữa
DM=$(aws s3api list-object-versions --bucket $BUCKET --prefix products/42/front.jpg \
--query 'DeleteMarkers[?IsLatest].VersionId' --output text --profile shop-lab)
aws s3api delete-object --bucket $BUCKET --key products/42/front.jpg --version-id $DM --profile shop-lab
aws s3 ls s3://$BUCKET/products/42/ --profile shop-lab # trở lại
B. Gỡ ShopAdminRole khỏi bucket policy, chạy Spring Boot trên EC2 của ASG (role shop-app-ec2-role): upload vẫn được; chạy local với profile shop-lab thì 403. Đây là minh họa rõ nhất cho "resource policy quyết định principal nào vào được".
C. Lifecycle thử nhanh: không chờ 30 ngày được, nhưng có thể upload thẳng vào class khác và xem phí lấy ra:
aws s3 cp front.jpg s3://$BUCKET/originals/42/raw.jpg --storage-class GLACIER_IR --sse AES256 --profile shop-lab
aws s3api head-object --bucket $BUCKET --key originals/42/raw.jpg --query StorageClass --profile shop-lab
aws s3 cp front.jpg s3://$BUCKET/originals/42/deep.jpg --storage-class DEEP_ARCHIVE --sse AES256 --profile shop-lab
aws s3 cp s3://$BUCKET/originals/42/deep.jpg /tmp/ --profile shop-lab
# InvalidObjectState: phải restore-object trước (Tier Bulk/Standard), chờ giờ
D. Storage Lens: Console S3 → Storage Lens → default dashboard (dữ liệu xuất hiện sau 24–48h) để thấy noncurrent version và incomplete multipart toàn account.
Dọn tài nguyên
Bucket có versioning không xóa được khi còn version/delete marker; aws s3 rb --force chỉ xóa version hiện tại. Thứ tự:
# 1. Tắt event notification, xóa queue tạm
aws s3api put-bucket-notification-configuration --bucket $BUCKET --notification-configuration '{}' --profile shop-lab
aws sqs delete-queue --queue-url $QUEUE_URL --profile shop-lab
# 2. Xóa mọi version và delete marker (python one-liner qua boto3 nếu nhiều; ít thì CLI)
aws s3api list-object-versions --bucket $BUCKET --output json --profile shop-lab \
--query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}' > /tmp/v.json
[ "$(jq '.Objects|length' /tmp/v.json)" != "0" ] && aws s3api delete-objects --bucket $BUCKET --delete file:///tmp/v.json --profile shop-lab
aws s3api list-object-versions --bucket $BUCKET --output json --profile shop-lab \
--query '{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}' > /tmp/dm.json
[ "$(jq '.Objects|length' /tmp/dm.json)" != "0" ] && aws s3api delete-objects --bucket $BUCKET --delete file:///tmp/dm.json --profile shop-lab
# 3. Multipart dở
aws s3api list-multipart-uploads --bucket $BUCKET --profile shop-lab # nếu có: abort-multipart-upload từng cái
# 4. Xóa bucket (giữ lại nếu bạn làm tiếp 22.7–22.13: các bài sau dùng bucket này cho jar và ảnh)
aws s3api delete-bucket --bucket $BUCKET --profile shop-lab
Thứ tốn tiền nếu quên: KMS key (khoảng 1 USD/tháng, chỉ xóa ở bài 22.14 khi xong chương), object ở Glacier/Deep Archive xóa sớm bị tính phí tối thiểu 90/180 ngày (vài cent với file test). S3 Standard vài MB thì không đáng kể.
Phỏng vấn và đề thi SAA hỏi về S3 thì sao?
Q1. Thiết kế upload ảnh từ browser lên S3 trong Spring Boot thế nào?
🇻🇳 Tiếng Việt:
Em không cho file đi qua server. Browser gọi API của em xin presigned PUT URL; app kiểm tra quyền và content-type, dùng S3Presigner ký URL có hạn 5 phút với credential của role EC2, trả về cùng key. Browser PUT thẳng lên S3. Sau đó browser gọi confirm, app HEAD object để xác nhận kích thước và loại rồi mới lưu key vào DB. Object vào prefix tmp có lifecycle xóa sau 1 ngày để dọn upload bỏ dở. Bucket bật Block Public Access, mã hóa SSE-KMS, policy chỉ cho role app. Ảnh public phục vụ qua CloudFront với OAC, không mở bucket.
🇬🇧 English:
I keep the file off the application server. The browser asks my API for a presigned PUT URL; the app checks authorization and content type, signs a 5-minute URL with S3Presigner using the EC2 role credentials, and returns it with the object key. The browser PUTs directly to S3. Then it calls a confirm endpoint, where the app does a HEAD on the object to verify size and type before storing the key in the database. Objects land under a tmp prefix with a one-day lifecycle expiration to clean up abandoned uploads. The bucket has Block Public Access on, SSE-KMS encryption and a policy allowing only the app role. Public images are served through CloudFront with OAC, never from a public bucket.
Q2. Presigned URL có bảo mật không? Hạn tối đa bao lâu?
🇻🇳 Tiếng Việt:
Presigned URL là chữ ký SigV4 trên một request cụ thể: bucket, key, method, header đã ký, thời hạn. Ai có URL dùng được, nên nó giống bearer token: chỉ trả qua HTTPS cho người đã xác thực, TTL ngắn, không log URL đầy đủ. Hạn tối đa 7 ngày với IAM user key, nhưng URL không sống lâu hơn credential ký nó: role EC2 xoay vòng khoảng 6 giờ nên URL chết theo. Cần link dài ngày em dùng CloudFront signed URL hoặc user key chuyên dụng. Quyền thật vẫn do IAM và bucket policy quyết định, presign không vượt quá quyền của người ký.
🇬🇧 English:
A presigned URL is a SigV4 signature over one specific request: bucket, key, method, signed headers and expiry. Anyone holding it can use it, so it behaves like a bearer token: return it only over HTTPS to an authenticated user, keep the TTL short, and never log the full URL. The maximum is 7 days with IAM user keys, but a URL never outlives the credentials that signed it; EC2 role credentials rotate roughly every 6 hours, so URLs signed with them expire with them. For long-lived links I use CloudFront signed URLs or a dedicated user key. The actual permission still comes from IAM and the bucket policy; presigning cannot grant more than the signer has.
Q3. Chọn storage class và lifecycle cho dữ liệu của shop-app ra sao?
🇻🇳 Tiếng Việt:
Ảnh đang bán để Standard vì đọc nhiều. Ảnh gốc độ phân giải cao chuyển Standard-IA sau 30 ngày rồi Glacier Instant sau 180 ngày vì hiếm đọc nhưng khi cần phải có ngay. Log và backup DB sau 90 ngày sang Glacier Flexible, sau 1 năm Deep Archive, giữ 7 năm theo yêu cầu kế toán. Dữ liệu em không đoán được pattern thì dùng Intelligent-Tiering. Luôn có rule xóa noncurrent version và abort multipart dở. Em tránh chuyển object nhỏ hơn 128 KB sang IA vì không tiết kiệm.
🇬🇧 English:
Images currently on sale stay in Standard because they are read often. High-resolution originals move to Standard-IA after 30 days and Glacier Instant Retrieval after 180 days: rarely read, but needed immediately when they are. Logs and database backups go to Glacier Flexible Retrieval after 90 days and Deep Archive after a year, retained for 7 years for accounting compliance. For data whose access pattern I cannot predict I use Intelligent-Tiering. I always add rules to expire noncurrent versions and abort incomplete multipart uploads, and I avoid transitioning objects under 128 KB to IA because it does not save money.
Q4. Cross-account: team Data ở account khác cần đọc bucket log của bạn. Làm thế nào?
🇻🇳 Tiếng Việt:
Khác account thì cần cả hai phía cho phép: bucket policy bên em Allow principal là role của account Data trên prefix cần thiết, và identity policy của role đó cũng Allow s3:GetObject. Nếu bucket dùng SSE-KMS, key policy cũng phải cho role đó kms:Decrypt. Object Ownership để Bucket owner enforced để object bên kia ghi vào vẫn thuộc em. Phương án khác là em tạo role trong account mình cho họ AssumeRole, hoặc dùng S3 Access Point để quản lý nhiều consumer.
🇬🇧 English:
Across accounts both sides must allow it: my bucket policy allows the Data account's role on the needed prefix, and that role's identity policy also allows s3:GetObject. If the bucket uses SSE-KMS, the key policy must grant that role kms:Decrypt as well. I keep Object Ownership at Bucket owner enforced so anything they write still belongs to me. Alternatives are creating a role in my account for them to assume, or using S3 Access Points when there are many consumers.
Góc thi SAA-C03
Câu 1: Công ty lưu 50 TB hồ sơ kiểm toán trên S3, theo quy định phải giữ 7 năm, mỗi năm chỉ truy xuất 1–2 lần và có thể chờ tới 12 giờ. Giải pháp MOST cost-effective?
A. S3 Standard-IA
B. S3 Glacier Instant Retrieval
C. S3 Glacier Deep Archive
D. S3 Intelligent-Tiering
Đáp án: C. Hiếm truy cập, chấp nhận chờ 12 giờ, giữ dài hạn → Deep Archive rẻ nhất. A đắt gấp nhiều lần cho dữ liệu không đọc. B dành cho cần lấy tức thì (ms). D phí giám sát trên 50 TB và vẫn không rẻ bằng Deep Archive khi pattern đã biết rõ. Nếu đề nói "retrieve within minutes" thì chọn Glacier Flexible với Expedited.
Câu 2: Ứng dụng web cho người dùng toàn cầu upload video 2 GB lên một bucket ở ap-southeast-1. Người dùng ở châu Âu báo upload chậm và hay đứt. Cách cải thiện với LEAST change to the application?
A. Tạo bucket ở mỗi region và sửa app chọn bucket gần nhất
B. Bật S3 Transfer Acceleration và dùng multipart upload
C. Chuyển bucket sang us-east-1
D. Dùng EFS thay S3
Đáp án: B. Transfer Acceleration đưa upload tới edge gần user rồi đi backbone AWS; multipart chia nhỏ, retry từng part, chống đứt. A nhiều thay đổi app và dữ liệu phân mảnh. C chỉ chuyển vấn đề sang user châu Á. D EFS không phải object storage qua Internet.
Câu 3: Một bucket chứa dữ liệu nhạy cảm. Công ty yêu cầu: mọi object phải mã hóa bằng key do công ty quản lý và có log việc dùng key; không object nào được truy cập qua HTTP không mã hóa; dữ liệu không được public dù cấu hình nhầm. Tập hợp nào thỏa mãn? (Chọn 1)
A. SSE-S3 + bucket policy Deny aws:SecureTransport=false + ACL private
B. SSE-KMS với customer managed key + bucket policy Deny aws:SecureTransport=false + bật S3 Block Public Access ở cấp account
C. SSE-C + CloudFront + ACL private
D. Client-side encryption + bucket versioning + MFA Delete
Đáp án: B. "Key do công ty quản lý + audit dùng key" → SSE-KMS CMK (CloudTrail ghi mỗi lần dùng key). "Không HTTP" → điều kiện aws:SecureTransport. "Không public dù nhầm" → Block Public Access. A SSE-S3 không cho audit key; ACL không chống nhầm policy. C SSE-C bạn phải gửi key mỗi request, không audit, vận hành nặng. D versioning/MFA Delete chống xóa, không liên quan 3 yêu cầu.
Cách nhớ ngắn gọn
- Bucket private mặc định: BPA bật, ACL tắt, SSE-S3 sẵn. Public → CloudFront + OAC, không mở bucket.
- Presigned URL = chữ ký tạm trên một request; TTL ngắn; không sống quá credential ký.
- Class: nóng Standard → ấm IA (30 ngày, 128 KB) → lạnh Glacier Instant (ms) / Flexible (phút–giờ) / Deep (12–48 giờ); mù pattern → Intelligent-Tiering; tái tạo được → One Zone.
- Lifecycle 3 rule luôn có: transition, expire noncurrent, abort multipart.
- Hóa đơn: storage + requests + transfer out + retrieval + KMS. Poll → event.
| Từ khóa đề | Nghĩ đến |
|---|---|
| accidentally public, prevent public access | S3 Block Public Access (account level) |
| audit encryption key usage, company-managed key | SSE-KMS với customer managed key |
| enforce encryption in transit | Bucket policy Deny aws:SecureTransport=false |
| temporary access, upload without AWS credentials | Presigned URL |
| unknown access pattern | Intelligent-Tiering |
| retrieve within minutes / hours / 12+ hours | Glacier Flexible Expedited / Flexible Standard / Deep Archive |
| recreatable data, lowest cost, infrequent | One Zone-IA |
| protect against accidental deletion | Versioning (+ MFA Delete) |
| cannot be deleted by anyone for N years, WORM | Object Lock Compliance mode |
| global users upload large files | Transfer Acceleration + multipart |
| copy to another region for DR / compliance | Cross-Region Replication |
| process file after upload, stop polling | S3 Event Notification → SQS/Lambda/EventBridge |
| query CSV/JSON in S3 with SQL | Athena (bài 22.17) |
Tự kiểm tra
Vì sao lưu key object vào DB thay vì URL?
URL phụ thuộc bucket, region, CDN, chữ ký. Lưu key, sinh URL lúc đọc (presigned hoặc CloudFront) để đổi hạ tầng không phải migrate dữ liệu.
Presigned URL ký bằng role EC2 có thể sống 7 ngày không?
Không. URL hết hiệu lực khi credential ký nó hết hạn; role EC2 xoay vòng ~6 giờ. Link dài ngày dùng CloudFront signed URL hoặc IAM user key riêng.
Bật SSE-KMS xong app bị 403 khi GET dù bucket policy đã Allow. Thiếu gì?
Role app cần kms:Decrypt trên key (trong key policy hoặc identity policy). Và nhớ bật Bucket Key để không bị phí KMS theo từng request.
ListBucket và GetObject áp trên ARN nào?
ListBucket trên arn:aws:s3:::bucket; GetObject/PutObject trên arn:aws:s3:::bucket/*. Viết chung một ARN là 403.
Bucket có versioning, xóa thế nào cho sạch khi dọn lab?
Phải xóa mọi Versions và DeleteMarkers (list-object-versions + delete-objects), abort multipart dở, tắt notification, rồi mới delete-bucket.
🧪 Thực hành bài này: Intermediate: Ứng dụng HA: ALB + ASG + RDS + S3 + SQS (chưa triển khai) · lab labs/aws