AWS: bản đồ dịch vụ cho một backend Java

4 câu hỏi~30 phútTừ khóa: VPC, subnet, security group, NAT, EC2, ECS, EKS, Lambda, RDS, ElastiCache, S3, SQS, SNS, MSK, IAM role, IRSA, CloudWatch

Tiếp đi: AWS. Mở console ra thấy hơn 200 dịch vụ, mình không biết shop-app thực sự cần những cái nào.

Một backend Java điển hình chỉ cần khoảng 10–12 dịch vụ. Bài này vẽ bản đồ đó cho shop-app, cái nào thay thế cái gì bạn đã tự chạy trước đây.

1. Bài toán

shop-app cần lên AWS: 4 service Spring Boot, PostgreSQL, Redis, Kafka (hoặc queue), lưu ảnh sản phẩm và hóa đơn PDF, log và metric. Yêu cầu: database không ra Internet, không có access key trong code.

2. Vấn đề khi "cứ thế mà làm"

3. Giải pháp: bản đồ dịch vụ

Nhu cầuDịch vụ AWSThay cho
Mạng riêngVPC, subnet public/private, Security Group, NAT GatewayMạng LAN + firewall
Cửa vàoRoute 53, ALB (L7), WAF, CloudFrontDNS, Nginx
Chạy appEC2 / ECS Fargate / EKS / LambdaServer, Docker host
DatabaseRDS PostgreSQL hoặc AuroraPostgres tự cài
CacheElastiCache (Redis/Valkey)Redis tự cài
FileS3Ổ đĩa, NFS
MessagingSQS (queue), SNS (pub/sub), MSK (Kafka), EventBridgeRabbitMQ, Kafka tự chạy
ImageECRDocker registry
QuyềnIAM role, policyUser/password dịch vụ
SecretSecrets Manager, Parameter Store, KMSFile .env
Quan sátCloudWatch Logs/Metrics/Alarms, X-RayELK, Prometheus

Chọn nơi chạy app

EC2ECS FargateEKSLambda
Bạn quản lýOS, JVM, deployTask definitionCluster K8s, manifestChỉ code
Phù hợpApp legacy, cần kiểm soát máyVài service container, team nhỏNhiều service, đã dùng K8s, cần portableJob sự kiện, traffic thưa
Lưu ý Java——Phải vận hành K8sCold start: dùng SnapStart / GraalVM; timeout 15 phút

shop-app đã có Helm chart và Argo CD (bài 18.3) nên chọn EKS. Team nhỏ chưa biết K8s thì ECS Fargate đơn giản hơn nhiều.

4. Code từng bước

Bước 1: mạng (Terraform, rút gọn)

module "vpc" {
  source          = "terraform-aws-modules/vpc/aws"
  version         = "~> 5.0"
  name            = "shop-prod"
  cidr            = "10.10.0.0/16"
  azs             = ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"]
  public_subnets  = ["10.10.0.0/24", "10.10.1.0/24", "10.10.2.0/24"]     # ALB, NAT
  private_subnets = ["10.10.10.0/24", "10.10.11.0/24", "10.10.12.0/24"]  # EKS node
  database_subnets= ["10.10.20.0/24", "10.10.21.0/24", "10.10.22.0/24"]  # RDS, Redis
  enable_nat_gateway = true
}

Security Group chain: ALB-sg nhận 443 từ Internet → node-sg chỉ nhận từ ALB-sg → db-sg chỉ nhận 5432 từ node-sg. Khái niệm VPC/NAT xem thêm bài 9.7.

Bước 2: IAM role cho pod (IRSA / EKS Pod Identity), không có access key

# ServiceAccount của order-service gắn với role chỉ được ghi vào bucket hóa đơn
apiVersion: v1
kind: ServiceAccount
metadata:
  name: order-service
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/shop-order-invoices
{
  "Effect": "Allow",
  "Action": ["s3:PutObject", "s3:GetObject"],
  "Resource": "arn:aws:s3:::shop-prod-invoices/*"
}

Bước 3: Java dùng default credentials chain

<dependency>
  <groupId>software.amazon.awssdk</groupId>
  <artifactId>s3</artifactId>
</dependency>   <!-- version qua BOM software.amazon.awssdk:bom -->
@Configuration
class AwsConfig {
    @Bean
    S3Client s3Client(@Value("${aws.region}") String region) {
        // Không truyền key: SDK tự lấy từ env → web identity token (IRSA) → instance profile
        return S3Client.builder().region(Region.of(region)).build();
    }
}

@Service
@RequiredArgsConstructor
class InvoiceStorage {
    private final S3Client s3;
    @Value("${shop.invoice-bucket}") private String bucket;

    public String save(long orderId, byte[] pdf) {
        String key = "invoices/%d.pdf".formatted(orderId);
        s3.putObject(b -> b.bucket(bucket).key(key).contentType("application/pdf"),
                     RequestBody.fromBytes(pdf));
        return key;
    }
}
# application.yml
aws.region: ${AWS_REGION}
shop.invoice-bucket: ${INVOICE_BUCKET}
spring.datasource:
  url: jdbc:postgresql://${DB_HOST}:5432/shop
  username: ${DB_USER}
  password: ${DB_PASSWORD}     # bơm từ Secrets Manager qua External Secrets Operator

Bước 4: kiểm tra

kubectl exec deploy/order-service -- env | grep AWS_     # thấy AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE
curl -X POST https://api.shop.example/orders/1001/invoice
aws s3 ls s3://shop-prod-invoices/invoices/              # có 1001.pdf
aws logs tail /aws/containerinsights/shop-prod/application --follow

5. Kiến trúc tham chiếu

Internet ─► Route 53 ─► CloudFront + WAF ─► ALB (public subnet, 3 AZ)
                                               │
              ┌────────────── VPC 10.10.0.0/16 ┼─────────────────────────┐
              │  private subnet (3 AZ)         ▼                         │
              │   EKS nodes: gateway ─► order / payment / inventory      │
              │        │ IRSA role          │            │               │
              │        ▼                    ▼            ▼               │
              │   database subnet: RDS Postgres Multi-AZ, ElastiCache    │
              │   NAT GW ─► Internet (gọi payment provider, pull ECR)    │
              └─────────────────────────────────────────────────────────┘
   Ngoài VPC (qua VPC endpoint): S3, SQS/SNS hoặc MSK, ECR, Secrets Manager, CloudWatch

6. So sánh messaging trên AWS

SQSSNSMSK
Mô hìnhQueue, 1 message → 1 consumerPub/sub fan-outKafka log, replay được
Vận hànhServerless, gần như 0ServerlessQuản lý broker (có bản Serverless)
Dùng khiJob nền, gửi email, hóa đơnMột event nhiều nơi nhận (SNS → nhiều SQS)Event streaming, outbox/CDC, cần thứ tự theo key

Lý thuyết queue vs log đã có ở bài 8.10.

7. Khi nào nên / không nên

Ở local mình vẫn cần gọi S3, không có IRSA thì chẳng lẽ lại phải dùng access key?

Đúng là local không có IRSA, nhưng vẫn không cần key dài hạn trong code. Vì SDK dùng default credentials chain, code không đổi, chỉ nguồn credential đổi:

Cùng một dòng S3Client.builder().build() chạy được cả ba nơi. Đó là lý do không bao giờ gọi StaticCredentialsProvider với key thật.

Security Group và Network ACL khác nhau thế nào? Mình thấy cả hai đều chặn port.

Bạn hiểu đúng là cả hai lọc traffic, chỉ khác tầng và cách nhớ trạng thái:

Security GroupNetwork ACL
Gắn vàoENI (instance, pod, RDS)Subnet
Stateful?Có: cho vào thì chiều về tự choKhông: phải mở cả chiều về (ephemeral port)
RuleChỉ allowAllow và deny, theo thứ tự số
Tham chiếuĐược tham chiếu SG khác (db-sg cho phép node-sg)Chỉ CIDR

Thực tế: dùng Security Group là chính, để NACL mặc định. NACL chỉ thêm khi cần chặn cứng một dải IP ở cả subnet.

Phỏng vấn hỏi về AWS thì trả lời sao?

Q1. Mô tả kiến trúc AWS cho một hệ thống Spring Boot microservices.

🇻🇳 Tiếng Việt:

VPC trải 3 AZ: public subnet cho ALB và NAT, private subnet cho EKS node, database subnet cho RDS PostgreSQL Multi-AZ và ElastiCache. Traffic vào qua Route 53, CloudFront, WAF, ALB. Image ở ECR, file ở S3, messaging bằng SQS/SNS hoặc MSK. Pod dùng IAM role qua IRSA, secret trong Secrets Manager, log và metric vào CloudWatch. Hạ tầng dựng bằng Terraform.

🇬🇧 English:

A VPC across three AZs: public subnets for the ALB and NAT, private subnets for EKS nodes, database subnets for RDS PostgreSQL Multi-AZ and ElastiCache. Traffic enters through Route 53, CloudFront, WAF and the ALB. Images live in ECR, files in S3, messaging uses SQS/SNS or MSK. Pods get IAM roles through IRSA, secrets come from Secrets Manager, and logs and metrics go to CloudWatch. Everything is provisioned with Terraform.

Q2. Ứng dụng lấy quyền gọi S3 như thế nào mà không có access key?

🇻🇳 Tiếng Việt:

Gắn IAM role cho workload: instance profile cho EC2, task role cho ECS, IRSA hoặc Pod Identity cho EKS. SDK dùng default credentials chain để lấy credential tạm thời, tự refresh. Role theo least privilege, mỗi service một role, chỉ đúng bucket và action cần.

🇬🇧 English:

We attach an IAM role to the workload: an instance profile on EC2, a task role on ECS, IRSA or Pod Identity on EKS. The SDK's default credentials chain fetches temporary credentials and refreshes them automatically. Roles follow least privilege: one role per service, scoped to the exact bucket and actions.

Q3. Khi nào chọn ECS, EKS hay Lambda?

🇻🇳 Tiếng Việt:

Lambda cho xử lý theo sự kiện, traffic thưa, chạy ngắn; với Java phải xử lý cold start. ECS Fargate cho vài service container khi team không muốn vận hành Kubernetes. EKS khi có nhiều service, đã có hệ sinh thái K8s như Helm, Argo CD, hoặc cần chạy được trên nhiều cloud.

🇬🇧 English:

Lambda for event-driven, spiky, short-running work; with Java we must handle cold starts. ECS Fargate for a handful of container services when the team doesn't want to operate Kubernetes. EKS when we have many services, already use the Kubernetes ecosystem like Helm and Argo CD, or need portability across clouds.

Cách nhớ ngắn gọn


Tự kiểm tra

Pod trong private subnet ra Internet bằng gì?

NAT Gateway nằm ở public subnet. Với dịch vụ AWS như S3/ECR nên dùng VPC endpoint để khỏi đi qua NAT.

Security Group stateful nghĩa là gì?

Đã cho phép kết nối vào thì response đi ra tự động được phép, không cần rule chiều ngược lại.

IRSA hoạt động dựa trên gì?

ServiceAccount có annotation role ARN; pod nhận web identity token (OIDC), SDK đổi token đó lấy credential tạm qua STS.

SQS và SNS khác nhau chỗ nào?

SQS là queue, mỗi message một consumer xử lý. SNS là pub/sub, một message đẩy tới nhiều subscriber (thường là nhiều SQS).

Vì sao nên chọn RDS thay vì Postgres chạy trong EKS?

RDS lo backup, Multi-AZ failover, patch, point-in-time recovery; giảm việc vận hành stateful trong K8s.