GĐ16 — AWS nền tảng cho Backend: EC2, S3, IAM, RDS, Lambda, CloudWatch, VPC

Study note cho FE engineer chuyển sang Backend. Mục tiêu là đọc được sơ đồ AWS nhỏ, triển khai một API có database và upload an toàn, rồi biết tìm lỗi và dọn tài nguyên lab.

Kiểm chứng ngày 2026-10-05: hành vi hết hạn của presigned URL và mặc định của bucket S3 mới đối chiếu với tài liệu presigned URL và bài blog thay đổi bảo mật S3 tháng 04/2023; checksum mặc định của SDK đo lại với @aws-sdk/client-s3 3.1146.0 (credentials giả). Lệnh AWS CLI trong bài chưa chạy trên tài khoản AWS thật trừ chỗ có ghi "Đã chạy".


1. AWS nhìn từ một backend engineer#

AWS có hàng trăm dịch vụ. Giai đoạn này chỉ học bảy khối thường gặp để bạn hiểu app nằm ở đâu, dữ liệu đi qua đâu, quyền được cấp thế nào và lỗi được quan sát ra sao. Đây là nền tảng để đọc job description và tiếp tục học theo dự án; nó không bảo đảm việc làm và không phải mọi production system đều chỉ cần bảy dịch vụ.

Dịch vụVai tròCâu hỏi backend cần trả lời
EC2Máy ảo chạy ứng dụngProcess chạy bằng user nào, mở port nào, restart và cập nhật OS ra sao?
S3Lưu object/fileBucket và object key là gì, ai được upload/download, file lớn đi đường nào?
IAMDanh tính và quyền AWSPrincipal nào được gọi action nào trên resource nào?
RDSDatabase quan hệ được quản lýDB nằm trong subnet nào, app được kết nối ra sao, backup và pool do ai lo?
LambdaChạy code theo event hoặc requestTrigger nào gọi handler, retry có thể lặp thế nào, execution role có quyền gì?
CloudWatchLogs, metrics và alarmsKhi app lỗi, log nào giúp tìm nguyên nhân và alarm nào báo sớm?
VPCMạng riêng của resource AWSTraffic được route qua đâu, resource nào public, rule nào cho phép kết nối?

Thứ tự học: VPC → IAM → EC2 → RDS và S3 → Lambda → CloudWatch. Thứ tự này bắt đầu từ ranh giới mạng và quyền truy cập, sau đó mới đặt app và data vào đó.

Trước khi tạo tài nguyên#

  1. Bảo vệ root user, bật MFA, và không dùng root cho thao tác hằng ngày.
  2. Chọn một Region gần nơi bạn học hoặc người dùng thử nghiệm. Hầu hết resource phải được tạo cùng Region để kết nối thuận tiện.
  3. Tạo AWS Budget hoặc billing alert trước khi mở EC2, RDS, NAT Gateway hay địa chỉ IP tĩnh. Free tier và ưu đãi có thể thay đổi theo tài khoản và thời điểm.
  4. Dùng AWS Console hoặc profile đăng nhập tạm thời cho người học. Không dán access key vào source, Docker image, chat, hay lệnh terminal được lưu lịch sử.
  5. Tạo lab nhỏ, ghi lại tên resource, và xoá tài nguyên sau buổi học. Dừng EC2 không tự xoá RDS, disk snapshot, log, hoặc địa chỉ IP có phí.

2. VPC — vẽ mạng trước khi deploy#

Định nghĩa. VPC là mạng ảo tách biệt trong một AWS Region. Bạn chọn dải IP (CIDR), chia subnet theo Availability Zone, gắn route table, gateway và security controls. VPC không tự làm app private hay public; route, địa chỉ IP và firewall rules cùng quyết định khả năng truy cập.

Các khối cần nhận diện#

KhốiLàm gìMental model
VPC CIDRCấp dải địa chỉ private cho mạngVí dụ 10.20.0.0/16 là không gian IP của lab
SubnetChia dải VPC theo AZ và mục đíchSubnet public thường có route tới Internet Gateway; subnet private không có route trực tiếp đó
Route tableChọn next hop theo địa chỉ đích0.0.0.0/0 → IGW cho đường IPv4 tới Internet Gateway
Internet Gateway (IGW)Kết nối VPC với InternetCó route tới IGW vẫn cần địa chỉ public và security rule phù hợp
Security Group (SG)Allowlist traffic ở resource như EC2/RDSMở port 5432 chỉ từ SG của app, thay vì từ mọi IP
Network ACL (NACL)Lọc traffic ở subnetCó thể dùng thêm, nhưng lab thường bắt đầu bằng route table + SG

Ví dụ CIDR cho lab: VPC 10.20.0.0/16; subnet app 10.20.1.0/24; hai subnet DB 10.20.11.0/24 và 10.20.12.0/24 đặt ở hai AZ. RDS DB subnet group thường trải qua nhiều AZ để hỗ trợ cấu hình sẵn sàng cao.

textReady
Internet   │ HTTPS (lab có thể giới hạn theo IP cá nhân)   ▼VPC 10.20.0.0/16├─ Public subnet 10.20.1.0/24: EC2 Node API└─ Private subnets 10.20.11.0/24, 10.20.12.0/24: RDS PostgreSQLSecurity Group của EC2: inbound chỉ port cần thiếtSecurity Group của RDS: inbound TCP 5432, source = Security Group của EC2

Cách suy luận đường kết nối EC2 → RDS:

  1. EC2 và RDS có private IP trong cùng VPC, vì vậy VPC local route cho phép tìm đường nội bộ.
  2. Security Group của RDS phải cho phép TCP 5432 từ Security Group gắn với EC2.
  3. Security Group của EC2 phải cho phép outbound tới RDS; rule mặc định thường cho outbound, nhưng cần kiểm tra nếu đã siết rule.
  4. RDS vẫn không cần public IP. Không mở 5432 tới 0.0.0.0/0.

Pitfall. Gắn EC2 vào “public subnet” không tự động cho người ngoài truy cập được. Route tới IGW, public IPv4/EIP và SG inbound đều phải đúng. Ngược lại, bỏ public access của RDS không cản app trong VPC kết nối qua private IP.

Nếu app chuyển vào private subnet để không nhận kết nối Internet trực tiếp, nó vẫn cần một đường outbound để gọi API AWS hoặc tải image/package. Có thể thiết kế VPC endpoint phù hợp (ví dụ S3 Gateway Endpoint cho traffic tới S3) hoặc egress qua NAT; cân nhắc chi phí và không thêm NAT chỉ theo thói quen.

Làm thử. Mở VPC Resource Map, tìm VPC, subnet và route table. Tự trả lời: subnet nào có route tới IGW? SG nào cho phép port 443? SG DB nhận traffic từ CIDR rộng hay từ SG của app? AWS giải thích VPC, route tables và Security Groups.


3. IAM — quyền tối thiểu cho người và workload#

Định nghĩa. IAM quản lý principal (người dùng, role hoặc dịch vụ), policy và quyền truy cập AWS. IAM không thay thế đăng nhập user của ứng dụng: user của SaaS là danh tính trong hệ thống của bạn; IAM role là danh tính để app gọi API AWS.

Role gồm hai câu hỏi khác nhau#

  • Trust policy: ai hoặc dịch vụ nào được assume role? Ví dụ EC2 hoặc Lambda.
  • Permissions policy: sau khi assume role, principal được gọi action nào trên resource nào?
  • Least privilege: chỉ cấp đúng action và resource cần dùng. Tránh policy Action: "*" hoặc Resource: "*" trong workload.
  • Temporary credentials: EC2/Lambda nhận credentials tạm qua role. AWS SDK tự tìm credentials từ môi trường chạy; không cần copy access key vào .env.

Ví dụ permissions policy cho API chỉ được upload object vào prefix raw/ của một bucket:

jsonReady
{  "Version": "2012-10-17",  "Statement": [    {      "Effect": "Allow",      "Action": ["s3:PutObject"],      "Resource": "arn:aws:s3:::backend-guide-lab/raw/*"    }  ]}

Policy này không cho phép liệt kê bucket, đọc object, xoá object, hoặc ghi vào prefix khác. Nếu API cần GetObject, thêm riêng action và resource tương ứng sau khi có use case.

Một trust policy đơn giản cho EC2 có dạng:

jsonReady
{  "Version": "2012-10-17",  "Statement": [{    "Effect": "Allow",    "Principal": { "Service": "ec2.amazonaws.com" },    "Action": "sts:AssumeRole"  }]}

Trong Console: tạo role cho EC2 → gắn permissions policy → gắn role vào instance. Với Lambda, tạo role có trust cho lambda.amazonaws.com, rồi chỉ thêm quyền S3/CloudWatch function cần.

Pitfall. Tạo IAM user rồi lưu access key trong EC2 là cách dễ rò rỉ và khó rotate. Với workload AWS, ưu tiên role. Khi gặp AccessDenied, đọc cả identity policy, resource policy, trust policy và policy boundary; một policy allow không xoá được explicit deny.

Làm thử. Tạo role chỉ có s3:PutObject vào prefix raw/. Từ API, upload thử vào raw/ thành công, rồi thử ghi vào processed/ hoặc bucket khác và xác nhận bị từ chối. Bài này chứng minh policy đang giới hạn quyền thật.

Tài liệu chính thức: IAM là gì và best practices.


4. EC2 — máy ảo chạy Node API#

Định nghĩa. EC2 instance là virtual server. Bạn chọn image/AMI, loại máy, disk, network và quyền; sau đó bạn chịu trách nhiệm OS, runtime, process, cập nhật và log agent. Đây là cách nhìn hạ tầng rõ nhất, nhưng cũng tự quản nhiều nhất. Với container production, phần trước đã giới thiệu ECS Fargate như một lựa chọn managed hơn; cách chạy container trên Fargate và luồng deploy lên ECS nằm ở GĐ15 mục Bổ sung.

Tạo một EC2 lab#

  1. Chọn Region và VPC. Chọn Linux AMI được hỗ trợ, loại instance nhỏ phù hợp với lab và một disk vừa đủ.
  2. Gắn Security Group. Nếu dùng SSH, chỉ cho TCP 22 từ IP của bạn (x.x.x.x/32), không mở cho toàn Internet. Có thể dùng Systems Manager Session Manager thay SSH; cách đó cần instance role và agent/đường kết nối tương ứng.
  3. Gắn IAM role khi tạo instance, ví dụ role có quyền ghi vào prefix S3 ở phần IAM.
  4. Chọn subnet. Lab công khai cần route, public IP và inbound rule; DB không đặt trong subnet này.
  5. Kết nối vào máy, cài runtime hoặc Docker, deploy image đã build từ CI.
  6. Mở đúng port ứng dụng, kiểm tra /health, log và restart sau khi process lỗi.

Ví dụ lệnh chạy image đã được đẩy tới registry. Thay image/tag và port theo app; secret lấy từ secret store/runtime configuration, không viết vào lệnh hoặc image:

bashReady
docker run --detach \  --name backend-api \  --restart unless-stopped \  --publish 3000:3000 \  --env NODE_ENV=production \  --env PORT=3000 \  registry.example/backend-api:1.0.0

App trong container phải listen trên 0.0.0.0:3000, không chỉ 127.0.0.1, nếu muốn nhận traffic đi qua Docker port mapping.

Tự chịu trách nhiệm: OS patching, disk đầy, process restart, deploy/rollback, security updates và thu thập log. EC2 không tự thay bạn thành PaaS.

Pitfall. Public IP có thể đổi sau khi instance dừng/start; dùng DNS hoặc load balancer khi cần endpoint ổn định. docker run --restart chỉ khởi động lại process/container, không thay health check hay rollout an toàn. Kiểm tra phí cho instance, disk, snapshot và public IPv4; xoá lab khi xong.


5. RDS — PostgreSQL được quản lý#

Định nghĩa. RDS giúp tạo, vận hành và backup database quan hệ mà không phải tự cài OS/database lên EC2. AWS lo một phần patching, backup, phần cứng và khả năng failover; bạn vẫn lo schema, migration, query, connection pool, authorization trong DB và khôi phục dữ liệu theo yêu cầu sản phẩm.

Thiết lập RDS cho API#

  1. Chọn PostgreSQL version, instance class và storage phù hợp với lab.
  2. Chọn VPC và DB subnet group trong private subnets. Không bật public access cho DB.
  3. Tạo RDS Security Group, chỉ inbound TCP 5432 từ Security Group của EC2/API.
  4. Bật automated backups và đặt retention theo bài tập. Dùng Multi-AZ khi cần failover/high availability và chấp nhận chi phí tương ứng.
  5. Tạo user database quyền ứng dụng riêng; không dùng master user trong app.
  6. Lưu connection string trong secret store hoặc biến môi trường được inject an toàn. Không commit password.
  7. Dùng TLS khi app kết nối, pool connection, và chạy migration bằng pipeline có kiểm soát.

Từ máy app nằm trong VPC, psql có thể kết nối endpoint private của RDS:

bashReady
psql "host=mydb.abc123.ap-southeast-1.rds.amazonaws.com port=5432 dbname=app user=app_user sslmode=verify-full"

Lệnh sẽ hỏi password; không truyền password trực tiếp trong shell command. Để verify-full hoạt động, cấu hình CA certificate theo hướng dẫn TLS của database engine.

Multi-AZ không đồng nghĩa read replica. Với RDS Multi-AZ DB instance, standby dùng cho high availability/failover và không phục vụ truy vấn đọc. Multi-AZ DB cluster là mô hình khác, có instances phục vụ đọc theo engine hỗ trợ. Read replica phục vụ read workload riêng và có thể có replication lag. Chọn theo mục tiêu availability hay throughput đọc; đừng thêm replica để chữa query/index kém. Xem GĐ31 — RDS/Aurora và tài liệu Multi-AZ để phân biệt hai deployment types.

Pitfall. “RDS managed” không có nghĩa là không cần backup drill. Backup có thể tồn tại nhưng restore vẫn chậm hoặc app không biết đổi endpoint. Đo thử restore và xác nhận pool reconnect sau failover. RDS cũng không làm SQL query nhanh hơn tự động.

Tài liệu: RDS User Guide, RDS backups, read replicas.


6. S3 — object storage và upload trực tiếp#

Mental model. S3 lưu object trong bucket. Mỗi object được định danh bởi bucket + key (ví dụ raw/user-123/uuid.pdf). “Folder” thường là prefix của key, không phải thư mục POSIX. Dùng S3 cho file/blob; metadata nghiệp vụ và quyền sở hữu vẫn nên được lưu trong database.

Bảo vệ bucket#

  • Để bucket private và bật S3 Block Public Access.
  • Bucket tạo mới từ tháng 04/2023 đã mặc định bật Block Public Access (cả bốn cờ) và tắt ACL (AWS blog, đọc 2026-10-05). Bucket cũ hoặc bucket có người từng đổi cài đặt thì không chắc; kiểm bằng aws s3api get-public-access-block --bucket <tên>.
  • Ghi theo key do server tạo, không dùng nguyên filename người dùng làm key.
  • Gán IAM quyền theo bucket/prefix cần thiết; tách input raw/ và output processed/.
  • Bật versioning nếu cần khôi phục overwrite; đặt lifecycle/retention để kiểm soát chi phí.
  • Không proxy file lớn qua Node nếu có thể cấp presigned URL có thời hạn ngắn.
  • Presigned URL là bearer token: ai có URL có thể dùng đúng action cho tới khi URL hết hạn; không ghi URL vào log.
  • URL còn sống không lâu hơn credentials đã ký nó: ký bằng IAM role thì URL hết hạn khi phiên role hết hạn, dù expiresIn đặt dài hơn; credentials role của EC2 thường sống khoảng 6 giờ, còn AssumeRole mặc định 1 giờ. Credentials hết hạn thì URL trả lỗi (mã lỗi cụ thể như ExpiredToken là theo kinh nghiệm, chưa có nguồn và chưa chạy), cho dù expiresIn còn dư. Vì vậy lấy URL mới theo từng yêu cầu thay vì lưu URL lâu (tài liệu AWS: presigned URL, đọc 2026-10-05).

Ví dụ Node.js/TypeScript: API cấp presigned PUT#

Cài AWS SDK for JavaScript v3:

bashReady
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Backend đã xác thực user, lấy userId từ session/JWT đã verify, tự tạo key rồi ký URL. EC2 role hoặc profile local cấp credentials cho SDK:

typescriptReady
import { randomUUID } from 'node:crypto'import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'import { getSignedUrl } from '@aws-sdk/s3-request-presigner'const s3 = new S3Client({ region: process.env.AWS_REGION })export async function createUploadUrl(userId: string) {  const key = `raw/${userId}/${randomUUID()}.pdf`  const command = new PutObjectCommand({    Bucket: process.env.UPLOAD_BUCKET!,    Key: key,    ContentType: 'application/pdf',  })  const url = await getSignedUrl(s3, command, { expiresIn: 60 })  return { url, key, method: 'PUT', contentType: 'application/pdf' as const }}

Client upload đúng method và content type đã ký:

typescriptReady
await fetch(url, {  method: 'PUT',  headers: { 'Content-Type': 'application/pdf' },  body: file,})

Checksum trong URL. Đã chạy lại ngày 2026-10-05 với Node 24 và @aws-sdk/client-s3 3.1146.0 (credentials giả, không gọi S3): S3Client mặc định đưa thêm x-amz-checksum-crc32=AAAAAA%3D%3D và x-amz-sdk-checksum-algorithm=CRC32 vào query của URL PUT; đặt requestChecksumCalculation: 'WHEN_REQUIRED' thì cả hai biến mất. Việc S3 thật từ chối upload body khác rỗng vì giá trị đó là suy luận, chưa gọi S3 thật. Nếu ví dụ trên upload lỗi checksum, thử new S3Client({ region: process.env.AWS_REGION, requestChecksumCalculation: 'WHEN_REQUIRED' }) như lab ở cuối bài.

URL thành công không chứng minh file an toàn hoặc thuộc user đó. Sau upload, backend cần xác minh object tồn tại, kiểm tra size/type bằng server-side checks, rồi mới đánh dấu file sẵn sàng. Xem quy trình đầy đủ tại GĐ12 — File upload.

Pitfall. Tin Content-Type do browser gửi là không đủ; kiểm tra magic bytes ở pipeline xử lý. Không cấp presigned URL cho key tùy ý từ request body, nếu không user có thể ghi đè file của user khác.

Tài liệu: S3 là gì, block public access, ví dụ SDK v3.


7. Lambda — xử lý event không quản server#

Định nghĩa. Lambda chạy handler khi được gọi bằng event hoặc request. Bạn không tự quản OS/instance; đổi lại function có runtime, quyền, memory, concurrency và timeout cần cấu hình. Function nên stateless: mỗi invocation không được giả định sẽ dùng lại cùng một process hay local file.

Dùng S3 event để xử lý file#

Luồng đơn giản: user upload vào raw/ → S3 ObjectCreated notification → Lambda đọc metadata/đẩy job → ghi kết quả vào processed/. Khi cấu hình trigger, S3 cần permission gọi Lambda. Dùng filter prefix raw/; tránh để function output vào đúng prefix kích hoạt nó.

Ví dụ handler Node.js đọc danh sách object từ event và ghi structured log:

typescriptReady
export const handler = async (event) => {  const files = (event.Records ?? []).map((record) => ({    bucket: record.s3.bucket.name,    key: decodeURIComponent(record.s3.object.key.replace(/\+/g, ' ')),    eTag: record.s3.object.eTag,  }))  for (const file of files) {    console.log(JSON.stringify({ level: 'info', event: 'upload.received', ...file }))    // Thực tế: kiểm tra DB theo idempotency key trước khi xử lý tiếp.  }  return { received: files.length }}

S3 event có thể được gửi lại; handler có side effect phải idempotent. Tạo unique job key từ bucket + object key + version ID (nếu bật versioning) hoặc một định danh nghiệp vụ khác. Ghi kết quả vào prefix processed/, không upload file mới vào raw/ trong cùng trigger.

Khi nào Lambda hợp?#

  • Resize/validate file sau upload, gửi notification, consumer nhỏ, webhook nhẹ, tác vụ event-driven.
  • Không cần chạy server liên tục; workload tự tăng/giảm theo invocation.
  • Một invocation có timeout tối đa 900 giây (15 phút). Lambda hỗ trợ response streaming qua những đường invoke nhất định; kiểm tra giới hạn API, Region, memory, timeout và chi phí trước khi chọn cho LLM streaming.
  • Job dài, cần giữ process/connections ổn định, hoặc chạy liên tục thường phù hợp container/VM hơn. Không lưu state bền trong memory hoặc /tmp.

Pitfall. Lambda gọi RDS từ VPC có thể cần ENI/network configuration, subnet capacity, SG rule và connection pooling phù hợp; “gắn vào VPC” không tự làm RDS public. Với tải burst lớn, mỗi execution environment có thể mở connection; giới hạn pool để không làm cạn connection DB.

Tài liệu: cách Lambda hoạt động, S3 trigger tutorial, timeout, response streaming.


8. CloudWatch — log, metric, alarm#

CloudWatch có ba góc nhìn chính:

  • Logs: sự kiện có timestamp từ Lambda, app, agent hoặc AWS service. Lambda có thể ghi log vào CloudWatch Logs khi execution role có quyền; log file của EC2 không tự xuất hiện nếu bạn chưa cấu hình CloudWatch Agent hoặc logging pipeline.
  • Metrics: số theo thời gian như EC2 CPU, RDS DatabaseConnections, Lambda Errors/Duration/Throttles.
  • Alarms/Dashboards: theo dõi ngưỡng hoặc trạng thái và gửi cảnh báo/thực hiện action đã cấu hình.

Log JSON giúp tìm request xuyên qua API và worker:

typescriptReady
console.log(JSON.stringify({  level: 'error',  event: 'db.query_failed',  requestId,  route: '/documents/:id',  errorCode: 'ECONNRESET',}))

Không ghi access token, presigned URL, password, file contents hoặc PII không cần thiết. Đặt retention cho log group; nếu không chọn retention phù hợp, log có thể tồn tại lâu và phát sinh chi phí.

Ví dụ truy vấn CloudWatch Logs Insights để tìm lỗi mới nhất:

textReady
fields @timestamp, level, event, requestId, errorCode| filter level = "error"| sort @timestamp desc| limit 20

Alarm lab có thể bắt Lambda Errors > 0 trong một khoảng thời gian hoặc RDS connections tiến gần giới hạn đã chọn. Một alarm cần threshold, evaluation periods và hành động thông báo; tạo alarm xong phải chủ động kích hoạt thử để xác nhận nó hoạt động.

Pitfall. “Có log” chưa phải monitoring. Khi sự cố xảy ra, cần biết metric nào tăng, log nào cùng request ID, ai nhận alarm, và dashboard mở ở đâu. Metrics và logs có pricing riêng; theo dõi ingestion, retention và query scan volume.

Tài liệu: CloudWatch, Logs Insights query, alarms.


9. Bài lab — nối cả bảy dịch vụ#

Mục tiêu: API Node cho phép user upload file và lưu metadata trong PostgreSQL; Lambda nhận event S3; bạn tìm được log và alarm khi có lỗi.

  1. Vẽ VPC: chọn CIDR, một subnet cho app lab, hai private subnets cho DB subnet group. Đánh dấu route, IGW và SG rules.

    Đáp án

    aws ec2 create-vpc --cidr-block 10.20.0.0/16, ba subnet (app 10.20.1.0/24, DB 10.20.11.0/24 và 10.20.12.0/24 ở hai AZ khác nhau), một IGW gắn vào VPC, route table của subnet app có 0.0.0.0/0 → igw-..., route table của subnet DB không có route đó. Tạo SG sg-api (inbound 443 và, nếu cần SSH, 22 từ x.x.x.x/32) và SG sg-db (inbound TCP 5432, source = sg-api, không phải CIDR). Mở VPC Resource Map để nhìn lại.

  2. Tạo IAM roles: role EC2 chỉ được ký upload vào prefix raw/; role Lambda chỉ được đọc raw/, ghi processed/ và ghi log. Không tạo access key cho app.

    Đáp án

    Role EC2 (trust ec2.amazonaws.com) với policy dưới đây; role Lambda (trust lambda.amazonaws.com) gắn AWSLambdaBasicExecutionRole cho log, thêm s3:GetObject trên raw/* và s3:PutObject trên processed/*. Gắn role EC2 qua instance profile; không tạo access key.

    jsonReady
    {  "Version": "2012-10-17",  "Statement": [    { "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject"],      "Resource": "arn:aws:s3:::backend-guide-lab-<account-id>/raw/*" },    { "Effect": "Allow", "Action": "s3:ListBucket",      "Resource": "arn:aws:s3:::backend-guide-lab-<account-id>" }  ]}

    s3:GetObject và s3:ListBucket có mặt vì bước 6 gọi HeadObject để xác minh object: HeadObject cần quyền s3:GetObject, và nếu thiếu s3:ListBucket thì object không tồn tại trả 403 thay vì 404. Statement ListBucket cố ý không có Condition theo s3:prefix: khoá này thuộc request liệt kê (ListObjectsV2), còn HeadObject không mang nó, nên điều kiện không khớp và quyền ListBucket không được tính, key thiếu vẫn trả 403 (suy từ tài liệu IAM, chưa gọi AWS thật). Đổi lại role liệt kê được tên mọi object trong bucket. Nếu không chấp nhận điều đó, bỏ ListBucket, chịu 403 cho cả "không có" lẫn "không quyền", hoặc chỉ cho phép liệt kê qua một role khác. Nếu muốn chỉ ghi, bỏ hai quyền này và đổi cách xác minh sang S3 event/Lambda.

  3. Tạo S3 bucket private: giữ Block Public Access, bật versioning nếu cần rollback, đặt lifecycle phù hợp lab.

    Đáp án

    aws s3api create-bucket --bucket backend-guide-lab-<account-id> --region <region> --create-bucket-configuration LocationConstraint=<region>. Bucket mới đã bật Block Public Access (từ 04/2023); kiểm bằng aws s3api get-public-access-block --bucket ... phải thấy bốn cờ true. Bật versioning nếu cần; thêm lifecycle xoá raw/ sau vài ngày để lab không tốn tiền.

  4. Tạo RDS PostgreSQL: private subnet, backup, user riêng; chỉ SG của EC2 được vào port 5432.

    Đáp án

    DB subnet group gồm hai subnet DB; instance PostgreSQL nhỏ nhất đủ lab với --no-publicly-accessible --vpc-security-group-ids <sg-db> --backup-retention-period 1. Tạo role DB riêng cho app (CREATE ROLE app_user LOGIN ...), không dùng master user trong app. Password quản lý bằng Secrets Manager hoặc nhập khi psql hỏi.

  5. Triển khai API lên EC2: Docker image immutable, health check, /health, structured log và giới hạn inbound. Với public demo, dùng load balancer + TLS như kiến thức ở GĐ15.

    Đáp án

    Image đã build và đẩy lên registry (ECR hoặc registry bạn dùng); trên instance chạy lệnh docker run ở mục 4. /health trả 200. Nếu mở ra Internet thật, đặt ALB + TLS (GĐ15); ALB idle timeout 60 s nên Node server.keepAliveTimeout đặt 65 s.

  6. Thêm endpoint upload: API xác thực user, tạo object key, ký presigned URL ngắn hạn; client PUT file thẳng lên S3; API xác minh object rồi ghi metadata vào RDS.

    Đáp án

    Dựa trên mục 6, thêm bước xác minh và ghi metadata. Chú ý cấu hình checksum của SDK (xem "Lỗi hay gặp" trong Khung và mã dùng chung).

    typescriptReady
    import { randomUUID } from 'node:crypto'import { HeadObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'import { getSignedUrl } from '@aws-sdk/s3-request-presigner'import { Pool } from 'pg'const s3 = new S3Client({ region: process.env.AWS_REGION, requestChecksumCalculation: 'WHEN_REQUIRED' })const db = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 })const Bucket = process.env.UPLOAD_BUCKET!// POST /uploads  (userId lấy từ session đã verify)export async function createUpload(userId: string) {  const key = `raw/${userId}/${randomUUID()}.pdf`  const url = await getSignedUrl(    s3, new PutObjectCommand({ Bucket, Key: key, ContentType: 'application/pdf' }), { expiresIn: 60 })  return { url, key }}// POST /uploads/complete  { key }export async function completeUpload(userId: string, key: string) {  if (!key.startsWith(`raw/${userId}/`)) throw new Error('forbidden') // không tin key từ client  const head = await s3.send(new HeadObjectCommand({ Bucket, Key: key }))  if (head.ContentType !== 'application/pdf' || (head.ContentLength ?? 0) > 10_000_000) {    throw new Error('invalid object')  }  await db.query(    'INSERT INTO files (owner_id, object_key, byte_size) VALUES ($1, $2, $3) ON CONFLICT (object_key) DO NOTHING',    [userId, key, head.ContentLength],  )}
  7. Tạo Lambda trigger: chỉ prefix raw/; handler idempotent; không để output gọi lặp lại chính nó.

    Đáp án

    Handler ở mục 7, đóng gói zip, memory/timeout nhỏ. Thêm kết quả deterministic để lặp là an toàn: ghi processed/<key>.json (cùng input cho cùng output, ghi lại là vô hại). Cho S3 quyền gọi Lambda rồi gắn notification với filter prefix raw/:

    bashReady
    aws lambda add-permission --function-name on-upload --statement-id s3-invoke \  --action lambda:InvokeFunction --principal s3.amazonaws.com \  --source-arn arn:aws:s3:::backend-guide-lab-<account-id> --source-account <account-id>aws s3api put-bucket-notification-configuration --bucket backend-guide-lab-<account-id> \  --notification-configuration '{"LambdaFunctionConfigurations":[{    "LambdaFunctionArn":"arn:aws:lambda:<region>:<account-id>:function:on-upload",    "Events":["s3:ObjectCreated:*"],    "Filter":{"Key":{"FilterRules":[{"Name":"prefix","Value":"raw/"}]}}}]}'
  8. Bật quan sát: tìm log theo request ID/key đã làm sạch, tạo alarm cho Lambda error hoặc lỗi app, kích hoạt thử alarm.

    Đáp án

    Đặt retention cho log group /aws/lambda/on-upload. Tạo alarm và kích hoạt lỗi thật:

    bashReady
    aws cloudwatch put-metric-alarm --alarm-name on-upload-errors \  --namespace AWS/Lambda --metric-name Errors --dimensions Name=FunctionName,Value=on-upload \  --statistic Sum --period 60 --evaluation-periods 1 --threshold 0 \  --comparison-operator GreaterThanThreshold --treat-missing-data notBreaching \  --alarm-actions arn:aws:sns:<region>:<account-id>:lab-alertsaws lambda invoke --function-name on-upload \  --cli-binary-format raw-in-base64-out --payload '{"Records":[{}]}' out.json

    Payload {"Records":[{}]} làm handler ném TypeError (record.s3 là undefined). Dùng thêm Logs Insights ở mục 8 để tìm dòng lỗi.

  9. Dọn lab: xóa DB/EC2 và resource liên quan sau khi kiểm tra backup cần giữ; kiểm tra bill/budget.

    Đáp án
    1. Xoá notification S3 (put-bucket-notification-configuration với {}), Lambda, alarm, log group.
    2. Terminate EC2; xoá instance profile và role.
    3. Xoá RDS (aws rds delete-db-instance --skip-final-snapshot --delete-automated-backups cho lab; production thì giữ snapshot); xoá DB subnet group.
    4. Làm rỗng S3 gồm mọi version và delete marker nếu đã bật versioning (xoá bằng aws s3api delete-objects theo list-object-versions) rồi xoá bucket.
    5. Xoá SG, subnet, route table phụ, detach + xoá IGW, rồi xoá VPC.
    6. Kiểm tra còn sót gì: aws resourcegroupstaggingapi get-resources --tag-filters Key=project,Values=backend-guide-lab phải trả danh sách rỗng (resource vừa xoá có thể hiện thêm vài phút); xem Billing/Budget sau 1–2 ngày.
Khung và mã dùng chung

Chưa chạy trên AWS thật. Không có tài khoản hay credential AWS trong quá trình soạn lời giải; các bước, lệnh và "kết quả mong đợi" dưới đây là suy ra từ tài liệu AWS (URL ở cuối khối) và từ code. Tự làm trước, rồi mới mở. Mọi lệnh dưới đây tạo tài nguyên có thể tính phí: dùng tài khoản lab, đặt Budget trước (mục 1), gắn tag project=backend-guide-lab cho mọi resource để dọn ở bước cuối. Lệnh aws dùng profile đăng nhập tạm thời (IAM Identity Center hoặc aws login), không có access key trong lệnh.

Sơ đồ

textReady
 Internet ──HTTPS──► [IGW] ─ 0.0.0.0/0 ─► Public subnet 10.20.1.0/24                                           EC2 (SG api: 443/22 từ IP bạn)                                            │ role: PutObject raw/*                   presigned PUT (60 s)     │ TCP 5432 (nguồn = SG api)   Browser ─────────────────────────┐       ▼                                    ▼     Private 10.20.11.0/24 + .12.0/24                              S3 bucket     RDS PostgreSQL (public = no)                               raw/ ──ObjectCreated(prefix raw/)──► Lambda                               processed/ ◄── PutObject ───────────┘   Logs: EC2 (CloudWatch Agent), Lambda ──► CloudWatch Logs                                              ──► Insights, Alarm

Kết quả mong đợi (suy ra, chưa quan sát)

BướcMong đợi
get-public-access-blockBlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, RestrictPublicBuckets đều true
psql từ máy ngoài VPC tới endpoint RDSTreo rồi timeout (không có đường đi, không có public IP); từ EC2 thì hỏi password và vào được
Từ EC2, ghi raw/Thành công; ghi processed/ hoặc bucket khác bằng chính role đó: AccessDenied
PUT presigned URL đúng method + Content-Type200; sai Content-Type hoặc sau 60 s: 403
Upload vào raw/Lambda được gọi một lần; log có dòng upload.received với bucket/key
lambda invoke payload hỏngTrả FunctionError: Unhandled; sau chu kỳ 1 phút, alarm ALARM (hoặc INSUFFICIENT_DATA rồi ALARM ở lần đầu)
Logs InsightsDòng có level = "error"; limit 20 trả tối đa 20 dòng

Kiểm "alarm thật sự kích hoạt" bằng trạng thái ALARM do metric gây ra (aws cloudwatch describe-alarm-history), không phải set-alarm-state (lệnh này chỉ đổi trạng thái thủ công, dùng để thử hành động thông báo).

Lỗi hay gặp

  • Presigned PUT bị từ chối vì checksum. Từ @aws-sdk/client-s3 3.729.0, SDK mặc định thêm x-amz-checksum-crc32 vào URL đã ký (đã quan sát: giá trị AAAAAA== là CRC32 của body rỗng). Việc S3 thật từ chối upload body khác rỗng là suy luận (chưa gọi S3 thật), nhưng cách an toàn là đặt requestChecksumCalculation: 'WHEN_REQUIRED' như code trên rồi thử upload thật.
  • 403 khi HeadObject đáng lẽ 404: thiếu s3:ListBucket (xem bước 2).
  • Lambda gọi lại chính nó: ghi output vào raw/ hoặc notification không có filter prefix. Dấu hiệu: số invocation tăng vô hạn và hoá đơn tăng; ngắt bằng cách xoá notification rồi sửa filter.
  • AccessDenied mà policy đã allow: có explicit deny, thiếu resource ARN /* cho object, hoặc role chưa gắn vào instance (dùng aws sts get-caller-identity trên EC2 để thấy role đang dùng).
  • Lambda Timeout/ENETUNREACH khi gắn vào VPC: Lambda trong VPC cần đường ra (VPC endpoint hoặc NAT) để gọi S3/API khác. Lambda ở lab không cần vào VPC.
  • Quên log retention: log group mặc định giữ vô hạn.

Tài liệu AWS đã dùng: VPC, IAM best practices, S3 presigned URL, HeadObject, S3 event notifications, Lambda với S3, CloudWatch alarms, RDS delete.

Done khi#

  • Giải thích được request từ Internet tới EC2 và vì sao RDS không public.

    Đáp án

    Tham chiếu từ nội dung bài và tài liệu AWS, chưa chạy trên AWS thật. Browser → DNS/public IP → Internet Gateway → route table của subnet (0.0.0.0/0 → IGW) → Security Group inbound (443) → EC2. RDS nằm ở subnet không có route tới IGW, publiclyAccessible = false (không public IP), và SG chỉ cho 5432 từ SG của EC2. Cả bốn điều kiện (route, public IP, SG, subnet) cùng quyết định, không riêng một cái. Xem GĐ16 mục 2.

  • Vẽ được đường EC2 → RDS và nêu SG rule cho port 5432.

    Đáp án

    Cùng VPC nên dùng route local; không cần IGW. sg-db inbound: TCP 5432, source = sg-api (tham chiếu SG, không dùng CIDR rộng). sg-api outbound mặc định cho phép. Sai thường gặp: mở 5432 tới 0.0.0.0/0 "cho dễ thử". Tự kiểm: psql từ ngoài VPC phải timeout, từ EC2 phải vào được. Xem mục 2 và 5.

  • API dùng EC2 role thay access key, và chỉ upload được vào S3 prefix được cấp.

    Đáp án

    Trên EC2, aws sts get-caller-identity trả ARN dạng assumed-role/<tên-role>/... mà không cần .env chứa key. Ghi vào raw/ thành công, ghi vào processed/ hoặc bucket khác bị AccessDenied. Quyền HeadObject cần s3:GetObject (và s3:ListBucket để phân biệt 404/403). Xem mục 3 và lời giải lab.

  • Browser upload file qua presigned URL; server vẫn kiểm tra object sau upload.

    Đáp án

    Browser PUT đúng method và Content-Type đã ký trong thời hạn thì 200; sai một trong ba thì 403. Sau đó server HeadObject kiểm tồn tại, size, type (magic bytes ở pipeline xử lý), rồi mới ghi metadata. URL thành công không chứng minh file an toàn hay thuộc đúng user; key do server sinh, không nhận từ client. Xem mục 6 và GĐ12.

  • S3 event kích hoạt Lambda; handler xử lý lặp an toàn và không tự tạo vòng lặp.

    Đáp án

    Notification có filter prefix raw/; handler chỉ ghi processed/. Lặp an toàn: output deterministic hoặc ghi nhận job theo key (bucket + key + version) trước khi tạo side effect. Tự kiểm: upload một file thì chỉ có đúng một chuỗi log upload.received và số invocation không tăng thêm sau đó. Xem mục 7.

  • Tìm được lỗi bằng Logs Insights và chứng minh alarm đã kích hoạt.

    Đáp án

    Query filter level = "error" trả được dòng lỗi có requestId. Alarm đã kích hoạt khi describe-alarm-history có chuyển trạng thái sang ALARM do metric (không phải set-alarm-state thủ công). Có threshold, evaluation period và action thông báo. Xem mục 8.

  • Xoá tài nguyên lab và kiểm tra resource còn tồn tại.

    Đáp án

    Dọn theo thứ tự phụ thuộc (notification → Lambda → EC2 → RDS → S3 gồm mọi version → mạng → IAM). Kiểm: aws resourcegroupstaggingapi get-resources --tag-filters Key=project,Values=backend-guide-lab rỗng, Budget không tăng sau 1–2 ngày. Sai thường gặp: chỉ dừng EC2 trong khi RDS, snapshot, IP tĩnh, log vẫn tính phí. Xem mục 1 và lời giải lab.


Bước tiếp theo — Cloudflare, rồi Kubernetes#

Khi bạn deploy được container và hiểu quyền, mạng, database, log cùng chi phí trên AWS, học GĐ17 — Cloudflare để so sánh runtime edge, bindings, object storage và queue. Sau đó học GĐ18 — Kubernetes về điều phối nhiều container; K8s không thay thế VPC, IAM, health check, connection pool hay graceful shutdown.

Nếu muốn học kiến trúc AWS sâu hơn hoặc chuẩn bị chứng chỉ, rẽ sang GĐ28 — AWS Solutions Architect – Associate. Nhánh GĐ28–33 có Security/Networking, Compute/Resilience, Storage/Databases, Cost/Migration, 6 lab và câu hỏi tự luyện; không cần hoàn thành GĐ17–27 trước.