RDS & Aurora: PostgreSQL của shop-app rời khỏi Docker

5 câu hỏi~50 phútTừ khóa: RDS, Multi-AZ, Read Replica, automated backup, PITR, snapshot, parameter group, subnet group, Secrets Manager, RDS Proxy, Aurora cluster endpoint, Aurora Serverless v2, Global Database · SAA: Domain 2 Resilient

Tiếp đi: RDS. Hiện PostgreSQL của shop-app vẫn chạy Docker trên một EC2 riêng với volume EBS, mình tự pg_dump mỗi đêm bằng cron. Tuần trước EC2 đó reboot do maintenance, app lỗi 10 phút, và mình phát hiện cron backup đã im lặng chết từ 3 tuần. Có nên chuyển sang RDS không, và RDS khác gì "Postgres trên EC2"?

Nên, và đây là một trong những quyết định "mua thay vì tự làm" rõ ràng nhất trên AWS. Amazon RDS (Relational Database Service) là PostgreSQL/MySQL/MariaDB/Oracle/SQL Server/Db2 do AWS vận hành: bạn vẫn dùng JDBC, SQL, Flyway như cũ; AWS lo cài đặt, patch, backup, failover, replica. Aurora là engine riêng của AWS tương thích PostgreSQL/MySQL với tầng storage phân tán. Bài này nằm trọn trong Domain 2 Resilient: Multi-AZ, backup, PITR, replica là chủ đề đề thi hỏi nhiều nhất về database.

1. Bài toán: PostgreSQL tự quản trên EC2

EC2 db-01 (public-a)
  └─ docker run postgres:16 -v /data:/var/lib/postgresql/data
  └─ cron 02:00: pg_dump | gzip > /backup/shop-$(date).sql.gz   (3 tuần nay fail vì đầy đĩa)
shop-app (ASG, 2 instance) ── jdbc:postgresql://10.0.1.50:5432/shop ── password trong application.yml

2. Vấn đề

3. Giải pháp: RDS PostgreSQL trong private subnet, Multi-AZ, secret quản lý

                       shop-vpc 10.0.0.0/16
 ┌────────────────────────────────┬────────────────────────────────┐
 │ ap-southeast-1a                │ ap-southeast-1b                │
 │  shop-private-a 10.0.11.0/24   │  shop-private-b 10.0.12.0/24   │
 │  ┌──────────────┐              │  ┌──────────────┐              │
 │  │ EC2 shop-app │──┐           │  │ EC2 shop-app │──┐           │
 │  └──────────────┘  │ 5432      │  └──────────────┘  │ 5432      │
 │                    ▼           │                    ▼           │
 │  ┌──────────────────────┐ sync │  ┌──────────────────────┐      │
 │  │ shop-db  PRIMARY     │══════│═▶│ shop-db  STANDBY     │      │
 │  │ db.t3.micro, PG 16   │ repl │  │ (Multi-AZ, không đọc)│      │
 │  └──────────────────────┘      │  └──────────────────────┘      │
 │       shop-db-sg: 5432 chỉ từ shop-app-sg                       │
 └────────────────────────────────┴────────────────────────────────┘
 Endpoint DNS: shop-db.abc123.ap-southeast-1.rds.amazonaws.com  (trỏ primary; failover đổi IP)
 Secret: shop/db  (Secrets Manager, rotation tự động)
Thành phầnGiải thíchTừ khóa đề thi
DB instanceMột PostgreSQL chạy trên instance class (db.t3.micro burstable, db.m6g general, db.r6g memory) với EBS storage (gp3/io1)instance class, storage autoscaling, gp3 vs io1
DB subnet groupDanh sách subnet (≥ 2 AZ) RDS được đặt vào; luôn là private subnet"database in private subnet", không public accessible
Security groupshop-db-sg: 5432 chỉ từ shop-app-sg (source là SG, không phải CIDR)SG chaining
Parameter grouppostgresql.conf do AWS quản: max_connections, shared_buffers, log_min_duration_statement; static param cần rebootparameter group vs option group
Multi-AZStandby đồng bộ ở AZ khác, không phục vụ đọc; failover tự động 60–120s, DNS endpoint giữ nguyênhigh availability, synchronous, failover, không phải scaling
Multi-AZ DB clusterBiến thể mới: 1 writer + 2 reader standby đọc được, failover ~35s (PostgreSQL/MySQL)"readable standbys"
Read ReplicaBản sao bất đồng bộ, phục vụ đọc; cùng/khác AZ/khác region; promote thành DB độc lậpread scaling, asynchronous, cross-region, promote
Automated backupSnapshot hàng ngày + transaction log mỗi 5 phút → PITR trong retention 1–35 ngàypoint-in-time recovery, retention, backup window
Manual snapshotGiữ vô hạn, copy sang region/account, sharelong-term retention, before major upgrade
EncryptionKMS at rest (phải chọn lúc tạo; bật sau = snapshot → copy encrypted → restore); TLS in transit"encrypt existing unencrypted DB" → snapshot copy
Secrets Manager integrationRDS tự tạo và xoay master password (--manage-master-user-password)rotate credentials, no hardcode
IAM DB authenticationToken 15 phút thay password, cho user DB đánh dấu rds_iam"use IAM instead of passwords"
RDS ProxyConnection pool quản lý giữa app và DB; giảm connection storm từ Lambda/ASG; failover nhanh hơnLambda + RDS, "too many connections", faster failover
Performance Insights / Enhanced MonitoringTop SQL theo wait event; metric OS 1sidentify slow queries

4. Cấu hình từng bước (CLI + Java)

Bước 1: Subnet group và SG.

export AWS_REGION=ap-southeast-1
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text --profile shop-lab)
VPC_ID=$(aws ec2 describe-vpcs --filters Name=tag:Name,Values=shop-vpc --query 'Vpcs[0].VpcId' --output text --profile shop-lab)
PRIV_A=$(aws ec2 describe-subnets --filters Name=tag:Name,Values=shop-private-a --query 'Subnets[0].SubnetId' --output text --profile shop-lab)
PRIV_B=$(aws ec2 describe-subnets --filters Name=tag:Name,Values=shop-private-b --query 'Subnets[0].SubnetId' --output text --profile shop-lab)
APP_SG=$(aws ec2 describe-security-groups --filters Name=group-name,Values=shop-app-sg --query 'SecurityGroups[0].GroupId' --output text --profile shop-lab)

aws rds create-db-subnet-group --db-subnet-group-name shop-db-subnets \
  --db-subnet-group-description "shop private subnets 1a/1b" \
  --subnet-ids $PRIV_A $PRIV_B \
  --tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab

DB_SG=$(aws ec2 create-security-group --group-name shop-db-sg --description "PostgreSQL from app" \
  --vpc-id $VPC_ID \
  --tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=shop-db-sg},{Key=Project,Value=shop-app},{Key=Env,Value=lab}]' \
  --query GroupId --output text --profile shop-lab)
aws ec2 authorize-security-group-ingress --group-id $DB_SG \
  --protocol tcp --port 5432 --source-group $APP_SG --profile shop-lab

Bước 2: Parameter group (tùy chọn nhưng nên có, để bật log slow query và buộc TLS).

aws rds create-db-parameter-group --db-parameter-group-name shop-pg16 \
  --db-parameter-group-family postgres16 --description "shop-app PG16" \
  --tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
aws rds modify-db-parameter-group --db-parameter-group-name shop-pg16 --parameters \
  "ParameterName=log_min_duration_statement,ParameterValue=500,ApplyMethod=immediate" \
  "ParameterName=rds.force_ssl,ParameterValue=1,ApplyMethod=immediate" \
  "ParameterName=shared_preload_libraries,ParameterValue=pg_stat_statements,ApplyMethod=pending-reboot" \
  --profile shop-lab

Bước 3: Tạo instance. Password do Secrets Manager sinh và quản; bạn không bao giờ thấy nó trong shell history.

aws rds create-db-instance --db-instance-identifier shop-db \
  --engine postgres --engine-version 16 \
  --db-instance-class db.t3.micro \
  --allocated-storage 20 --storage-type gp3 --max-allocated-storage 100 \
  --db-name shop --master-username shop \
  --manage-master-user-password \
  --db-subnet-group-name shop-db-subnets --vpc-security-group-ids $DB_SG \
  --db-parameter-group-name shop-pg16 \
  --no-publicly-accessible \
  --no-multi-az \
  --backup-retention-period 7 --preferred-backup-window 18:00-19:00 \
  --preferred-maintenance-window sun:19:30-sun:20:30 \
  --storage-encrypted \
  --enable-performance-insights --performance-insights-retention-period 7 \
  --enable-cloudwatch-logs-exports '["postgresql"]' \
  --deletion-protection \
  --tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab

aws rds wait db-instance-available --db-instance-identifier shop-db --profile shop-lab   # ~5–10 phút
DB_HOST=$(aws rds describe-db-instances --db-instance-identifier shop-db \
  --query 'DBInstances[0].Endpoint.Address' --output text --profile shop-lab)
SECRET_ARN=$(aws rds describe-db-instances --db-instance-identifier shop-db \
  --query 'DBInstances[0].MasterUserSecret.SecretArn' --output text --profile shop-lab)

Chú ý: backup window 18:00–19:00 UTC = 1–2 giờ sáng Việt Nam. Secret RDS tạo có tên dạng rds!db-xxxx; để app đọc theo tên cố định shop/db, bước 5 sẽ tạo thêm một secret alias hoặc trỏ app bằng ARN.

Bước 4: Role app đọc secret. Thêm vào identity policy của shop-app-ec2-role:

{
  "Effect": "Allow",
  "Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
  "Resource": "SECRET_ARN"
},
{
  "Effect": "Allow",
  "Action": "kms:Decrypt",
  "Resource": "*",
  "Condition": {"StringEquals": {"kms:ViaService": "secretsmanager.ap-southeast-1.amazonaws.com"}}
}

Bước 5: Spring Boot đọc secret. Cách sạch nhất là Spring Cloud AWS 3 (awspring): import secret thành property source, không viết code gọi SDK.

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.awspring.cloud</groupId>
      <artifactId>spring-cloud-aws-dependencies</artifactId>
      <version>3.2.1</version><type>pom</type><scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependency>
  <groupId>io.awspring.cloud</groupId>
  <artifactId>spring-cloud-aws-starter-secrets-manager</artifactId>
</dependency>
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency>
# application-aws.yml
spring:
  config:
    import: "aws-secretsmanager:${SHOP_DB_SECRET}"     # ARN hoặc tên secret, truyền qua env/user data
  datasource:
    url: jdbc:postgresql://${host}:${port}/${dbname}?sslmode=require
    username: ${username}
    password: ${password}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2
      max-lifetime: 900000        # 15 phút: < thời gian xoay password, kết nối cũ tự thay
      connection-timeout: 5000
      keepalive-time: 60000
  jpa:
    open-in-view: false
  flyway:
    enabled: true
    locations: classpath:db/migration

Secret của RDS là JSON {"username":"shop","password":"...","engine":"postgres","host":"shop-db.abc.ap-southeast-1.rds.amazonaws.com","port":5432,"dbname":"shop"}, nên các placeholder ${host}, ${password} được điền thẳng. Đọc thêm về profile và config ở Profile & Config, pool ở Connection pool.

Nếu không dùng awspring, dùng SDK v2 trực tiếp:

@Configuration
public class DataSourceConfig {

    @Bean
    DataSource dataSource(@Value("${SHOP_DB_SECRET}") String secretId, ObjectMapper om) throws Exception {
        try (SecretsManagerClient sm = SecretsManagerClient.create()) {   // credential từ role EC2
            String json = sm.getSecretValue(r -> r.secretId(secretId)).secretString();
            JsonNode s = om.readTree(json);
            HikariConfig cfg = new HikariConfig();
            cfg.setJdbcUrl("jdbc:postgresql://%s:%d/%s?sslmode=require"
                    .formatted(s.get("host").asText(), s.get("port").asInt(), s.get("dbname").asText()));
            cfg.setUsername(s.get("username").asText());
            cfg.setPassword(s.get("password").asText());
            cfg.setMaximumPoolSize(10);
            cfg.setMaxLifetime(Duration.ofMinutes(15).toMillis());
            return new HikariDataSource(cfg);
        }
    }
}

Hạn chế của cách này: password chỉ đọc một lần lúc start; khi secret xoay, kết nối mới sẽ fail cho tới khi restart. Vì vậy production dùng AWS Secrets Manager JDBC driver (software.amazon.awssdk.secretsmanager:aws-secretsmanager-jdbc, URL jdbc-secretsmanager:postgresql://host/shop, username = secret id) tự refresh khi gặp lỗi xác thực, hoặc RDS Proxy giữ password ở proxy, app chỉ cần IAM token.

Bước 6: Deploy user data truyền secret ARN (bổ sung vào launch template bài 22.5):

Environment=SPRING_PROFILES_ACTIVE=aws
Environment=SHOP_DB_SECRET=arn:aws:secretsmanager:ap-southeast-1:ACCOUNT_ID:secret:rds!db-xxxx

Hoặc tạo secret shop/db của riêng bạn (bài 22.14) và cho RDS dùng nó bằng cách tắt managed password rồi đặt lại; trong lab giữ cái RDS tạo cho đơn giản.

5. Cách hoạt động

Multi-AZ failover:

bình thường:  app ── DNS shop-db.xxx.rds.amazonaws.com → 10.0.11.80 (primary, 1a)
              mỗi commit: ghi EBS primary + ghi EBS standby (sync) → ack   (latency +1–2 ms)

sự cố 1a:     RDS phát hiện primary mất (health check nội bộ)
  t+0s        bắt đầu failover: promote standby 1b
  t+~30s      đổi DNS record shop-db.xxx → 10.0.12.90
  t+60–120s   standby thành primary, nhận kết nối
              app: HikariCP thấy connection hỏng → tạo mới → resolve DNS lại → kết nối 1b
              RDS tạo standby mới ở 1a (nền)

ghi chú:      - DNS TTL của RDS endpoint ~5s; JVM phải không cache DNS vô hạn
                (networkaddress.cache.ttl mặc định 30s khi không có SecurityManager → OK)
              - Transaction đang bay lúc failover: lỗi; app retry ở tầng service (idempotent)
              - Reboot "with failover" dùng để tập dượt

Backup và PITR:

00:00 ─────────────────────────────────── 7 ngày ───────────────────────────────────▶ now
  │ snapshot tự động hàng ngày (backup window)  │ snapshot │ snapshot │ ...
  └─ WAL/transaction log upload S3 mỗi 5 phút ──────────────────────────────────────┘
Restore to time 2025-03-10 14:37:00:
  = snapshot gần nhất trước đó + replay log tới 14:37 → TẠO INSTANCE MỚI shop-db-restored
  (không ghi đè instance cũ; app đổi endpoint hoặc rename)
RPO ≈ 5 phút (latest restorable time), RTO = thời gian restore (phút → giờ tùy dung lượng)

Read Replica:

primary (1a) ──WAL streaming async──▶ replica-1 (1b)  ──▶ báo cáo, export
                                 └──▶ replica-2 (us-west-2, cross-region) ──▶ DR / user Mỹ
replica lag: ms → giây (xem metric ReplicaLag); đọc sau ghi có thể chưa thấy
promote replica → DB độc lập, mất liên kết replication (dùng khi DR hoặc tách hệ thống)

Spring: định tuyến read/write bằng hai DataSource và AbstractRoutingDataSource theo @Transactional(readOnly = true) — bài Replication đã mô tả; trên RDS chỉ đổi URL replica endpoint.

Aurora khác RDS ở kiến trúc storage:

RDS:    mỗi instance có EBS riêng; Multi-AZ = copy block sang EBS standby; replica = replay WAL

Aurora: cluster volume chung, 6 copy trên 3 AZ, tự heal, tăng 10 GB/lần tới 128 TB
         ┌──────────┐   ┌──────────┐ ┌──────────┐
         │  writer  │   │ reader 1 │ │ reader 2 │  ... tới 15 reader, lag thường < 20 ms
         └────┬─────┘   └────┬─────┘ └────┬─────┘
              └──────────────┴────────────┘
                 shared storage (log-structured, ghi redo log, không ghi page)
endpoints: cluster (writer) · reader (LB giữa reader) · custom · instance
failover: promote reader ≈ 30s (có reader) · tạo instance mới ≈ phút (không reader)
backtrack (MySQL): quay lui cluster tới thời điểm mà không restore
Serverless v2: ACU 0.5 → 256, scale theo giây, tính tiền theo ACU-giờ; v1 đã ngừng
Global Database: 1 primary region + tới 5 secondary, lag < 1s, promote < 1 phút → DR RPO≈1s

6. So sánh

Multi-AZ vs Read Replica — câu đề thi hỏi nhiều nhất về RDS:

Multi-AZ (instance)Multi-AZ DB clusterRead Replica
Mục đíchAvailability (HA)HA + chút read scalingRead scaling, DR, báo cáo
ReplicationĐồng bộ (sync)Bán đồng bộ (1 trong 2 standby ack)Bất đồng bộ (async)
Đọc từ bản sao?KhôngCó (2 reader)Có
FailoverTự động, 60–120s, cùng endpointTự động, ~35sThủ công: promote (hoặc tự động hóa bằng script/Lambda)
Vị tríAZ khác, cùng region3 AZ, cùng regionCùng AZ, khác AZ, khác region
Số lượng1 standby2 standbyTới 15 (PostgreSQL), 5 (MySQL cũ)
BackupLấy từ standby, không ảnh hưởng primaryTừ readerReplica có backup riêng nếu bật
Chi phí~2× instance + storage~3×+1× mỗi replica; cross-region thêm data transfer
Free TierKhông (Single-AZ db.t3.micro 750 giờ thì có)KhôngKhông

Hai thứ kết hợp được: primary Multi-AZ + read replica (replica cũng có thể Multi-AZ). Đề nói "improve availability" → Multi-AZ; "improve read performance / offload reporting" → Read Replica; "disaster recovery in another region" → cross-region Read Replica hoặc Aurora Global Database.

RDS PostgreSQL vs Aurora PostgreSQL vs tự dựng trên EC2

PostgreSQL trên EC2RDS PostgreSQLAurora PostgreSQL
Bạn quảnMọi thứ: OS, patch, backup, HA, replicaSchema, query, parameter groupSchema, query
Tương thích100%, extension tùy ý, superuserHầu hết extension phổ biến, không superuserTương thích PG nhưng chậm theo major version mới hơn RDS
HATự làm (Patroni...)Multi-AZ, 60–120s6 copy/3 AZ, failover ~30s
Read scalingTự làmTới 15 replica, lag giây15 reader, lag ms, reader endpoint tự LB
StorageEBS, bạn resizeEBS gp3/io1, autoscaling tới giới hạn bạn đặtTự tăng tới 128 TB, trả theo dùng
Chi phíRẻ nhất về hóa đơn, đắt nhất về ngườiTrung bình; có Free Tier db.t3.microĐắt hơn RDS ~20% cùng instance + phí I/O (hoặc I/O-Optimized); Serverless v2 cho tải thưa; không Free Tier
Chọn khiCần extension/kernel đặc thù, license riêngHầu hết app, ngân sách vừaCần HA cao, nhiều reader, tải biến động mạnh, multi-region

Cách quản credential

Password trong yml/envSecrets ManagerIAM DB authRDS Proxy + IAM
RotationTay, phải deployTự động (RDS managed hoặc Lambda rotation)Không có password; token 15 phútProxy giữ secret; app dùng IAM
Thay đổi codeKhôngProperty import / JDBC wrapperSinh token mỗi connection (Hikari cần hook)Đổi host + IAM token
Giới hạnLộ trong Git/logPhí nhỏ/secret/tháng + API call< 200 kết nối mới/giây, không hợp pool lớnPhí theo vCPU của DB

7. Khi nào dùng / không

Mình bật Multi-AZ rồi và trỏ job báo cáo cuối tháng vào cùng endpoint, nghĩ RDS sẽ tự chia SELECT sang standby. Nhưng primary vẫn 100% CPU còn standby hình như không làm gì. Mình hiểu sai chỗ nào?

Bạn hiểu gần đúng về "có hai máy", nhưng nhầm vai trò của máy thứ hai. Standby trong Multi-AZ không nhận bất kỳ kết nối nào: nó chỉ nhận block ghi đồng bộ từ primary và ngồi chờ để lên thay. Nó là bảo hiểm, không phải công suất. Đây là điểm đề SAA đánh rất thường xuyên bằng câu "the standby instance can be used for read traffic" như một lựa chọn sai.

Multi-AZ:        app ──▶ primary (xử lý tất cả) ══sync══▶ standby (không có endpoint)
Read Replica:    app ──▶ primary ──async──▶ replica (có endpoint riêng, nhận SELECT)

Giải pháp cho báo cáo: tạo Read Replica và trỏ job vào endpoint của nó.

aws rds create-db-instance-read-replica --db-instance-identifier shop-db-report \
  --source-db-instance-identifier shop-db --db-instance-class db.t3.micro \
  --availability-zone ap-southeast-1b --no-publicly-accessible \
  --tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
# endpoint: shop-db-report.xxx.ap-southeast-1.rds.amazonaws.com

Phía Spring Boot, job báo cáo dùng DataSource thứ hai:

spring:
  datasource:
    url: jdbc:postgresql://${host}:5432/shop?sslmode=require
  report-datasource:
    url: jdbc:postgresql://${REPORT_DB_HOST}:5432/shop?sslmode=require&readOnly=true
@Configuration
public class ReportDataSourceConfig {
    @Bean @ConfigurationProperties("spring.report-datasource")
    DataSourceProperties reportProps() { return new DataSourceProperties(); }

    @Bean(name = "reportDataSource")
    DataSource reportDataSource(DataSourceProperties reportProps,
                                @Value("${username}") String user, @Value("${password}") String pw) {
        return reportProps.initializeDataSourceBuilder().username(user).password(pw)
                .type(HikariDataSource.class).build();   // cùng user/password từ secret
    }

    @Bean(name = "reportJdbc")
    JdbcTemplate reportJdbc(@Qualifier("reportDataSource") DataSource ds) { return new JdbcTemplate(ds); }
}

Ba điều cần nhớ khi đọc từ replica:

Còn Multi-AZ bạn đã bật thì giữ: đó là thứ giúp shop-db sống qua sự cố AZ mà bài mở đầu than phiền. Multi-AZ giải "mất dịch vụ", Read Replica giải "chậm vì đọc nhiều". Hai bệnh, hai thuốc.

Flash sale hôm qua ASG lên 8 instance, mỗi instance HikariCP max 10, và Postgres bắt đầu ném FATAL: remaining connection slots are reserved. db.t3.micro có max_connections bao nhiêu vậy? Mình tăng tham số đó lên 500 là xong chứ?

Tăng được, nhưng bạn sẽ chuyển lỗi từ "hết slot" sang "hết RAM và chậm toàn bộ". Hãy nhìn con số trước.

RDS đặt max_connections theo công thức dựa trên RAM của instance class: LEAST({DBInstanceClassMemory/9531392}, 5000). Với db.t3.micro (1 GB) ra khoảng ~100; db.t3.small (2 GB) ~200; db.r6g.large (16 GB) ~1700. Mỗi connection PostgreSQL là một process tốn vài MB RAM (work_mem, buffer) dù đang idle. 500 connection trên 1 GB RAM là OOM hoặc swap.

8 instance × 10 pool = 80 kết nối app
+ 3 (rds superuser reserved) + Performance Insights + replica + psql của bạn ≈ 90–100 → đụng trần
Khi ASG lên max 4 → 40, ổn; cấu hình pool đúng theo max của ASG, không theo 1 instance.

Ba hướng xử lý, từ rẻ đến đúng:

HướngLàm gìKhi nào đủ
1. Co poolHikariCP maximum-pool-size = (max_connections − 10) / ASG max. Với 100 slot, ASG max 4 → 20; max 8 → 10 (đúng con số đang có, nên lỗi xuất hiện khi lên 8 là do những kết nối "ngoài pool").Pool nhỏ hơn bạn nghĩ thường đủ: PostgreSQL phục vụ tốt nhất với ít connection bận hơn nhiều connection chờ (xem Connection pool).
2. Đổi instance classdb.t3.small/db.t4g.medium: gấp đôi RAM, max_connections gấp đôi, cũng là giải pháp cho CPU credit của t3 cạn khi flash sale (burstable!).Lab/dev; production nhỏ. Có downtime vài phút (Multi-AZ: failover thay vì downtime).
3. RDS ProxyProxy đứng giữa, giữ pool tới DB (ví dụ 90), nhận hàng nghìn kết nối từ app/Lambda, multiplex: một connection DB phục vụ nhiều client xen kẽ khi họ không trong transaction. Pinning xảy ra khi session có state (prepared statement, SET, advisory lock).Lambda gọi RDS, ASG lớn, muốn failover nhanh (proxy giữ kết nối app, chỉ đổi phía DB).
aws rds create-db-proxy --db-proxy-name shop-db-proxy --engine-family POSTGRESQL \
  --auth "AuthScheme=SECRETS,SecretArn=$SECRET_ARN,IAMAuth=REQUIRED" \
  --role-arn arn:aws:iam::$ACCOUNT_ID:role/shop-rds-proxy-role \
  --vpc-subnet-ids $PRIV_A $PRIV_B --vpc-security-group-ids $DB_SG \
  --require-tls --idle-client-timeout 1800 --tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
aws rds register-db-proxy-targets --db-proxy-name shop-db-proxy --db-instance-identifiers shop-db --profile shop-lab
# app: jdbc:postgresql://shop-db-proxy.proxy-xxx.ap-southeast-1.rds.amazonaws.com:5432/shop
# role shop-rds-proxy-role cần secretsmanager:GetSecretValue trên secret; app dùng IAM token hoặc password

RDS Proxy tính phí theo vCPU của instance phía sau (có mức tối thiểu), không Free Tier; với db.t3.micro nó có thể đắt hơn cả DB, nên trong lab chỉ cần biết lệnh, không bắt buộc tạo. Trong đề SAA, "Lambda functions open many connections and exhaust the database" → RDS Proxy gần như luôn là đáp án.

Cuối cùng, kiểm tra ai đang chiếm slot:

SELECT usename, application_name, state, count(*)
FROM pg_stat_activity GROUP BY 1,2,3 ORDER BY 4 DESC;
-- idle nhiều → pool để minimum-idle cao hoặc app nào quên đóng; idle in transaction → bug @Transactional dài

Cho mình bài lab để tự tay làm phần này nhé.

Lab 22.7: shop-db PostgreSQL 16 trong private subnet, secret quản lý, Multi-AZ failover, snapshot và PITR

Mục tiêu

Chi phí & thời gian

Chuẩn bị

Các bước

Bước 1 – Subnet group + SG: chạy Bước 1 mục 4.
Console: RDS → Subnet groups → Create (VPC shop-vpc, AZ 1a/1b, chọn 2 private subnet). EC2 → Security Groups → Create shop-db-sg, inbound PostgreSQL 5432, Source = shop-app-sg.

Bước 2 – Parameter group: chạy Bước 2 mục 4.
Console: RDS → Parameter groups → Create (postgres16) → Edit: log_min_duration_statement=500, rds.force_ssl=1.

Bước 3 – Tạo instance: chạy Bước 3 mục 4; trong lúc chờ ~8 phút đọc tiếp Bước 4.
Console: RDS → Create database → Standard create → PostgreSQL 16 → Templates: Free tier → identifier shop-db → master username shop → Credentials management: Managed in AWS Secrets Manager → db.t3.micro → gp3 20 GB, autoscaling 100 → Connectivity: VPC shop-vpc, subnet group shop-db-subnets, Public access No, SG shop-db-sg → Additional: initial database shop, parameter group shop-pg16, backup 7 ngày, encryption, Performance Insights, deletion protection → Create.

aws rds describe-db-instances --db-instance-identifier shop-db \
  --query 'DBInstances[0].[DBInstanceStatus,Endpoint.Address,AvailabilityZone,MultiAZ,PubliclyAccessible,StorageEncrypted]' \
  --output table --profile shop-lab

Bước 4 – Cho role app đọc secret: chạy Bước 4 mục 4 với SECRET_ARN thật:

cat > secret-policy.json <<EOF
{"Version":"2012-10-17","Statement":[
 {"Effect":"Allow","Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Resource":"$SECRET_ARN"},
 {"Effect":"Allow","Action":"kms:Decrypt","Resource":"*",
  "Condition":{"StringEquals":{"kms:ViaService":"secretsmanager.$AWS_REGION.amazonaws.com"}}}]}
EOF
aws iam put-role-policy --role-name shop-app-ec2-role --policy-name shop-db-secret-read \
  --policy-document file://secret-policy.json --profile shop-lab

Console: IAM → Roles → shop-app-ec2-role → Add permissions → Create inline policy → JSON.

Bước 5 – Kết nối từ EC2 bằng psql:

INSTANCE_ID=$(aws ec2 describe-instances --filters Name=tag:Project,Values=shop-app Name=instance-state-name,Values=running \
  --query 'Reservations[0].Instances[0].InstanceId' --output text --profile shop-lab)
aws ssm start-session --target $INSTANCE_ID --profile shop-lab

# ----- trong EC2 (role shop-app-ec2-role) -----
sudo dnf install -y postgresql16 jq
SECRET_ARN=<dán ARN>
S=$(aws secretsmanager get-secret-value --secret-id $SECRET_ARN --query SecretString --output text)
export PGHOST=$(echo $S | jq -r .host) PGUSER=$(echo $S | jq -r .username) PGDATABASE=$(echo $S | jq -r .dbname)
export PGPASSWORD=$(echo $S | jq -r .password) PGSSLMODE=require
psql -c "select version(), inet_server_addr();"
psql -c "create table orders(id bigserial primary key, sku text, qty int, created_at timestamptz default now());"
psql -c "insert into orders(sku, qty) select 'SKU-'||g, (random()*5)::int from generate_series(1,1000) g;"
psql -c "select count(*) from orders;"      # 1000
unset PGPASSWORD

Console: Secrets Manager → secret rds!db-… → Retrieve secret value (chỉ để xem cấu trúc JSON; không copy password ra ngoài).

Bước 6 – Spring Boot trên EC2 đọc secret (nếu ASG đang chạy): thêm SHOP_DB_SECRET vào user data của launch template version mới và instance refresh (bài 22.5), hoặc tạm sửa /etc/systemd/system/shop-app.service trên một instance và systemctl restart shop-app. Kiểm tra curl localhost:8080/actuator/health có "db":{"status":"UP"}. Flyway migrate chạy khi start.

Kiểm tra kết quả

# 1. Không public, không kết nối được từ máy bạn
nslookup $DB_HOST                 # trả IP 10.0.11.x hoặc 10.0.12.x (private)
psql "host=$DB_HOST user=shop dbname=shop sslmode=require" -c 'select 1'   # timeout (đúng)

# 2. SG chỉ cho shop-app-sg
aws ec2 describe-security-groups --group-ids $DB_SG \
  --query 'SecurityGroups[0].IpPermissions[].[FromPort,UserIdGroupPairs[0].GroupId]' --output text --profile shop-lab

# 3. Secret do RDS quản, rotation tự động 7 ngày
aws secretsmanager describe-secret --secret-id $SECRET_ARN \
  --query '[Name,RotationEnabled,RotationRules.AutomaticallyAfterDays,OwningService]' --output text --profile shop-lab
# rds!db-...  True  7  rds

# 4. Backup đang bật, có latest restorable time
aws rds describe-db-instances --db-instance-identifier shop-db \
  --query 'DBInstances[0].[BackupRetentionPeriod,LatestRestorableTime]' --output text --profile shop-lab

# 5. TLS bắt buộc: psql không sslmode=require (từ EC2) phải bị từ chối
# PGSSLMODE=disable psql -c 'select 1'  → "no pg_hba.conf entry ... no encryption"

# 6. Log slow query lên CloudWatch
aws logs describe-log-streams --log-group-name /aws/rds/instance/shop-db/postgresql --max-items 1 --profile shop-lab

Thử thêm

A. Multi-AZ và failover có kiểm chứng. (Không Free Tier; bật ~30 phút rồi tắt.)

aws rds modify-db-instance --db-instance-identifier shop-db --multi-az --apply-immediately --profile shop-lab
aws rds wait db-instance-available --db-instance-identifier shop-db --profile shop-lab   # 10–15 phút
aws rds describe-db-instances --db-instance-identifier shop-db \
  --query 'DBInstances[0].[MultiAZ,AvailabilityZone,SecondaryAvailabilityZone]' --output text --profile shop-lab
# True  ap-southeast-1a  ap-southeast-1b

# Trong EC2: vòng lặp đo mất kết nối (mỗi giây in AZ server qua IP)
while true; do date +%T; psql -tAc "select inet_server_addr()" 2>&1 | head -1; sleep 1; done

# Từ máy bạn: ép failover
aws rds reboot-db-instance --db-instance-identifier shop-db --force-failover --profile shop-lab

Quan sát: IP đổi từ 10.0.11.x sang 10.0.12.x; khoảng 30–90 giây lỗi "could not connect" rồi tự nối lại; describe-db-instances cho thấy AvailabilityZone giờ là 1b. Xem thêm event:

aws rds describe-events --source-identifier shop-db --source-type db-instance --duration 60 \
  --query 'Events[].[Date,Message]' --output table --profile shop-lab
# "Multi-AZ instance failover started" → "Multi-AZ instance failover completed"

Nếu app Spring Boot đang chạy: HikariCP log vài dòng Connection is not available rồi phục hồi; không cần restart. Tắt Multi-AZ khi xong: modify-db-instance --no-multi-az --apply-immediately.

B. Xóa nhầm và PITR.

# Trong EC2: ghi nhớ giờ, rồi "xóa nhầm"
psql -c "select now();"                      # ví dụ 2025-03-10 07:42:10+00
sleep 60
psql -c "delete from orders where qty > 2;"  # mất ~60% dữ liệu
psql -c "select count(*) from orders;"

# Từ máy bạn: restore về trước lúc xóa 30s → instance MỚI
aws rds restore-db-instance-to-point-in-time --source-db-instance-identifier shop-db \
  --target-db-instance-identifier shop-db-pitr --restore-time 2025-03-10T07:42:40Z \
  --db-instance-class db.t3.micro --db-subnet-group-name shop-db-subnets --vpc-security-group-ids $DB_SG \
  --no-publicly-accessible --tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
aws rds wait db-instance-available --db-instance-identifier shop-db-pitr --profile shop-lab   # ~10 phút

# Secret của RDS chỉ gắn với shop-db; instance restore dùng cùng master password tại thời điểm restore
PITR_HOST=$(aws rds describe-db-instances --db-instance-identifier shop-db-pitr --query 'DBInstances[0].Endpoint.Address' --output text --profile shop-lab)
# trong EC2: PGHOST=$PITR_HOST psql -c "select count(*) from orders;"   → 1000

Thực tế khi khôi phục production: pg_dump bảng từ instance PITR và nạp lại vào primary, hoặc đổi endpoint app sang instance mới (rename identifier). PITR không ghi đè instance gốc, đó là điểm đề thi hay hỏi.

C. Manual snapshot, share và copy encrypted.

aws rds create-db-snapshot --db-instance-identifier shop-db --db-snapshot-identifier shop-db-before-upgrade \
  --tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
aws rds wait db-snapshot-available --db-snapshot-identifier shop-db-before-upgrade --profile shop-lab
# Copy sang region khác (DR) — snapshot encrypted phải chỉ định KMS key ở region đích
aws rds copy-db-snapshot --source-db-snapshot-identifier arn:aws:rds:$AWS_REGION:$ACCOUNT_ID:snapshot:shop-db-before-upgrade \
  --target-db-snapshot-identifier shop-db-before-upgrade-dr --kms-key-id alias/aws/rds \
  --source-region $AWS_REGION --region ap-northeast-1 --profile shop-lab

D. Xem Performance Insights: Console RDS → shop-db → Monitoring → Performance Insights; chạy trong EC2 psql -c "select pg_sleep(0.6), count(*) from orders a, orders b limit 1;" vài lần rồi xem top SQL theo wait event, và log slow query xuất hiện trong CloudWatch /aws/rds/instance/shop-db/postgresql.

Dọn tài nguyên

Thứ tự: instance phụ (PITR, replica, proxy) → tắt deletion protection → xóa instance (có/không snapshot cuối) → snapshot thủ công → parameter/subnet group → SG → policy/secret. Multi-AZ, RDS Proxy, replica, snapshot copy ở region khác là những thứ tốn tiền nếu quên.

# 1. Instance phụ
aws rds delete-db-instance --db-instance-identifier shop-db-pitr --skip-final-snapshot --profile shop-lab
aws rds delete-db-instance --db-instance-identifier shop-db-report --skip-final-snapshot --profile shop-lab 2>/dev/null
aws rds delete-db-proxy --db-proxy-name shop-db-proxy --profile shop-lab 2>/dev/null

# 2. Tắt Multi-AZ (nếu còn) và deletion protection, rồi xóa shop-db
aws rds modify-db-instance --db-instance-identifier shop-db --no-multi-az --no-deletion-protection --apply-immediately --profile shop-lab
aws rds wait db-instance-available --db-instance-identifier shop-db --profile shop-lab
# giữ lại DB cho các bài sau? → bỏ qua lệnh dưới. Xóa hẳn:
aws rds delete-db-instance --db-instance-identifier shop-db --skip-final-snapshot --delete-automated-backups --profile shop-lab
aws rds wait db-instance-deleted --db-instance-identifier shop-db --profile shop-lab
# Secret rds!db-... bị xóa theo instance (sau thời gian chờ); kiểm tra:
aws secretsmanager list-secrets --filters Key=owning-service,Values=rds --query 'SecretList[].Name' --profile shop-lab

# 3. Snapshot thủ công (cả region DR)
aws rds delete-db-snapshot --db-snapshot-identifier shop-db-before-upgrade --profile shop-lab
aws rds delete-db-snapshot --db-snapshot-identifier shop-db-before-upgrade-dr --region ap-northeast-1 --profile shop-lab

# 4. Group và SG
aws rds delete-db-parameter-group --db-parameter-group-name shop-pg16 --profile shop-lab
aws rds delete-db-subnet-group --db-subnet-group-name shop-db-subnets --profile shop-lab
aws ec2 delete-security-group --group-id $DB_SG --profile shop-lab

# 5. Policy inline trên role
aws iam delete-role-policy --role-name shop-app-ec2-role --policy-name shop-db-secret-read --profile shop-lab

# 6. Kiểm tra
aws rds describe-db-instances --query 'DBInstances[].DBInstanceIdentifier' --profile shop-lab
aws rds describe-db-snapshots --snapshot-type manual --query 'DBSnapshots[].DBSnapshotIdentifier' --profile shop-lab

Nếu bạn tiếp tục bài 22.8–22.13 ngay, hãy giữ shop-db Single-AZ (Free Tier) và chỉ xóa instance PITR/replica. Xem thêm quy trình backup/restore tổng quát ở Backup & Restore.

Phỏng vấn và đề thi SAA hỏi về RDS và Aurora thì sao?

Q1. Multi-AZ và Read Replica khác nhau thế nào? Khi nào dùng cái nào?

🇻🇳 Tiếng Việt:

Multi-AZ là bài toán sẵn sàng: một standby ở AZ khác nhận ghi đồng bộ, không phục vụ đọc, khi primary hỏng RDS tự failover trong một hai phút và endpoint DNS giữ nguyên nên app chỉ cần pool tạo lại kết nối. Read Replica là bài toán hiệu năng đọc: bản sao bất đồng bộ có endpoint riêng, có thể ở region khác, app trỏ query báo cáo vào đó và chấp nhận lag. Production em luôn bật Multi-AZ; thêm replica khi đọc nặng hoặc cần DR khác region. Hai thứ dùng chung được.

🇬🇧 English:

Multi-AZ is about availability: a standby in another AZ receives synchronous writes, serves no reads, and when the primary fails RDS fails over within a minute or two while the DNS endpoint stays the same, so the app only needs its pool to reconnect. A Read Replica is about read performance: an asynchronous copy with its own endpoint, possibly in another region, where I point reporting queries and accept some lag. In production I always enable Multi-AZ and add replicas when reads are heavy or I need cross-region DR. They can be combined.

Q2. Bạn quản password DB trong Spring Boot trên AWS như thế nào?

🇻🇳 Tiếng Việt:

Em không để password trong yml hay biến môi trường. RDS tạo với managed master password nên secret nằm trong Secrets Manager và tự xoay 7 ngày. Role EC2 của app chỉ có quyền GetSecretValue trên đúng secret đó. Trong Spring Boot em dùng Spring Cloud AWS import secret thành property source, datasource đọc host, username, password từ đó; Hikari max-lifetime đặt 15 phút để kết nối cũ tự thay sau khi xoay. Khi cần không gián đoạn tuyệt đối em dùng Secrets Manager JDBC driver tự refresh hoặc RDS Proxy với IAM auth, app không còn giữ password.

🇬🇧 English:

I never keep the password in YAML or environment variables. The RDS instance is created with a managed master password, so the secret lives in Secrets Manager and rotates every 7 days. The app's EC2 role only has GetSecretValue on that one secret. In Spring Boot I use Spring Cloud AWS to import the secret as a property source, and the datasource reads host, username and password from it; Hikari max-lifetime is 15 minutes so old connections are recycled after rotation. When I need zero interruption I use the Secrets Manager JDBC driver, which refreshes on auth failure, or RDS Proxy with IAM authentication so the app holds no password at all.

Q3. Khôi phục dữ liệu xóa nhầm trên RDS ra sao? RPO/RTO là gì?

🇻🇳 Tiếng Việt:

Automated backup gồm snapshot hàng ngày và transaction log mỗi 5 phút, cho phép point-in-time recovery tới bất kỳ giây nào trong retention tối đa 35 ngày, nên RPO khoảng 5 phút. Restore luôn tạo instance mới, không ghi đè, nên em restore về trước thời điểm xóa, pg_dump bảng bị ảnh hưởng và nạp lại, hoặc đổi endpoint. RTO là thời gian restore cộng thời gian chuyển app, thường vài chục phút. Trước thay đổi lớn em chụp manual snapshot vì nó giữ vô hạn; snapshot còn copy được sang region khác cho DR.

🇬🇧 English:

Automated backups consist of a daily snapshot plus transaction logs every 5 minutes, enabling point-in-time recovery to any second within the retention period of up to 35 days, so the RPO is about 5 minutes. A restore always creates a new instance rather than overwriting, so I restore to just before the deletion, dump the affected tables and load them back, or switch the endpoint. RTO is the restore time plus the application switch, typically tens of minutes. Before major changes I take a manual snapshot because it is kept indefinitely, and snapshots can be copied to another region for DR.

Q4. Khi nào bạn chọn Aurora thay RDS? Aurora Serverless v2 hợp với gì?

🇻🇳 Tiếng Việt:

Aurora khi cần failover nhanh hơn, nhiều reader với lag mili giây qua reader endpoint, storage tự lớn tới 128 TB, hay multi-region với Global Database lag dưới một giây. Nó đắt hơn RDS cùng cỡ và không có Free Tier nên app nhỏ em dùng RDS. Serverless v2 hợp với tải thưa hoặc biến động mạnh như môi trường dev, test, SaaS nhiều tenant nhỏ: scale theo ACU từng giây, trả theo dùng, có thể trộn với instance provisioned trong cùng cluster.

🇬🇧 English:

I choose Aurora when I need faster failover, many readers with millisecond lag behind a reader endpoint, storage that grows automatically up to 128 TB, or multi-region with Global Database and sub-second lag. It costs more than RDS of the same size and has no Free Tier, so small apps stay on RDS. Serverless v2 fits sparse or highly variable workloads such as dev and test environments or multi-tenant SaaS with many small tenants: it scales in ACUs per second, bills for what is used, and can be mixed with provisioned instances in the same cluster.

Góc thi SAA-C03

Câu 1: Một ứng dụng thương mại điện tử dùng RDS PostgreSQL Single-AZ. Công ty yêu cầu database vẫn hoạt động nếu một Availability Zone bị sự cố, với thay đổi ít nhất cho ứng dụng. Giải pháp nào đáp ứng?
A. Tạo Read Replica ở AZ khác và cấu hình ứng dụng chuyển sang replica khi lỗi
B. Chuyển instance sang Multi-AZ deployment
C. Tăng backup retention lên 35 ngày và bật PITR
D. Tạo snapshot hàng giờ và copy sang AZ khác

Đáp án: B. Multi-AZ: standby đồng bộ, failover tự động, endpoint giữ nguyên → không đổi app. A replica async có thể mất dữ liệu, promote thủ công, phải đổi endpoint. C backup phục hồi dữ liệu, không giữ dịch vụ chạy. D snapshot là regional, không có khái niệm "copy sang AZ", và restore mất hàng chục phút.

Câu 2: Một ứng dụng serverless dùng hàng trăm Lambda function kết nối tới RDS MySQL. Khi tải tăng, database báo "Too many connections" và failover mất vài phút khiến ứng dụng lỗi. Giải pháp với LEAST operational overhead?
A. Tăng max_connections trong parameter group và dùng instance class lớn hơn
B. Triển khai RDS Proxy và trỏ Lambda vào proxy endpoint
C. Viết connection pool trong mỗi Lambda và giữ kết nối trong biến global
D. Chuyển sang DynamoDB

Đáp án: B. RDS Proxy pool và multiplex kết nối, giữ kết nối phía client khi DB failover nên app gần như không thấy gián đoạn. A chỉ đẩy giới hạn, tốn tiền, không giải quyết failover. C mỗi Lambda execution environment vẫn giữ một kết nối, hàng trăm environment vẫn là hàng trăm kết nối. D thay đổi toàn bộ data model, không phải "least overhead".

Câu 3: Công ty vận hành ứng dụng toàn cầu trên Aurora PostgreSQL ở eu-west-1. Yêu cầu: người dùng châu Á đọc với latency thấp, và nếu region chính sự cố phải khôi phục trong vòng 1 phút với mất dữ liệu dưới 1 giây. Chọn gì?
A. Cross-region Read Replica của RDS PostgreSQL và promote khi sự cố
B. Aurora Global Database với secondary cluster ở ap-southeast-1
C. Sao chép snapshot sang ap-southeast-1 mỗi giờ
D. Aurora Multi-AZ với 3 reader trong eu-west-1

Đáp án: B. Global Database: replication storage-level lag thường < 1 giây, secondary phục vụ đọc tại chỗ, promote < 1 phút → RPO ~1s, RTO ~1 phút. A replica RDS lag giây đến phút, promote tay lâu hơn. C RPO 1 giờ. D không rời khỏi region nên không chống sự cố region và không giúp latency châu Á.

Cách nhớ ngắn gọn

Từ khóa đềNghĩ đến
highly available, survive AZ failure, minimal app changeMulti-AZ
offload reporting / read-heavy / read latency other regionRead Replica (cross-region)
point-in-time, accidental deletion, within last N daysAutomated backup + PITR (retention ≤ 35)
retain backup for years, before upgradeManual snapshot (copy cross-region)
too many connections, Lambda + RDS, faster failoverRDS Proxy
rotate credentials automaticallySecrets Manager (RDS managed password)
encrypt an existing unencrypted databaseSnapshot → copy with KMS → restore
unpredictable / intermittent workload, dev-testAurora Serverless v2
RPO < 1 s, RTO < 1 min, multi-regionAurora Global Database
undo bad change without restore (MySQL)Aurora Backtrack
identify slow SQL, wait eventsPerformance Insights
database in private subnet, only app can connectDB subnet group private + SG source = app SG

Tự kiểm tra

Standby của Multi-AZ có nhận query SELECT không?

Không. Nó chỉ nhận ghi đồng bộ và chờ failover. Muốn đọc từ bản sao thì tạo Read Replica (hoặc dùng Multi-AZ DB cluster có 2 reader).

PITR có ghi đè instance đang chạy không? RPO xấp xỉ bao nhiêu?

Không, PITR luôn tạo instance mới. Log upload mỗi 5 phút nên RPO ≈ 5 phút; retention tối đa 35 ngày.

Vì sao Hikari max-lifetime nên nhỏ hơn chu kỳ xoay password?

Password xoay thì kết nối đang mở vẫn sống, nhưng kết nối mới dùng password cũ sẽ fail. Đặt max-lifetime ngắn để pool thay kết nối đều, và dùng JDBC wrapper/awspring refresh hoặc RDS Proxy để không gián đoạn.

DB hiện chưa mã hóa, muốn bật encryption at rest thì làm gì?

Không bật trực tiếp được. Snapshot → copy snapshot với KMS key → restore thành instance mới → đổi endpoint app.

Lab xong, thứ gì trên RDS tốn tiền nếu quên tắt?

Multi-AZ (gấp đôi), instance PITR/replica, RDS Proxy, snapshot copy ở region khác, và secret trong Secrets Manager; Single-AZ db.t3.micro còn trong Free Tier 12 tháng đầu.

🧪 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