Terraform: Triển Khai Hệ Thống AWS EKS kết hợp RDS, S3, Lambda và CI/CD với Jenkins
Ở bài trước chúng ta đã Triển Khai Hệ Thống CI/CD Với Jenkins, Kubernetes (K3s), Terraform và AWS RDS , tuy nhiên đó là tất cả với hệ sinh thái AWS và chưa thực sự private khóa riêng các tài nguyên với nhau, ở bài này chúng ta cùng triển khai hệ thống mới với Jenkins riêng không thuộc tài nguyên của AWS nhé.
🏗️ TỔNG QUAN KIẾN TRÚC LAB (OriShop on AWS)
Dự án e-commerce OriShop (Spring Boot Java) được triển khai theo chuẩn Cloud-Native trên AWS:
-
Mã nguồn: Quản lý trên GitHub.
-
CI/CD: Dùng Jenkins dựng trên VPS riêng, kích hoạt tự động qua GitHub Webhook.
-
Hạ tầng (IaC): Dùng Terraform để dựng toàn bộ AWS VPC, EKS, RDS (MySQL), S3 và IAM.
-
Lưu trữ: RDS cho Database, S3 để chứa ảnh Media (truy cập Public).
-
Bảo mật: Dùng chuẩn IRSA (IAM Roles for Service Accounts) để Pod trên EKS gọi API vào S3 một cách an toàn nhất, không lộ Access Key.
-
Xử lý sự kiện (Event-Driven): Mỗi khi một tấm ảnh được upload thành công lên S3 từ EKS, S3 sẽ "bắn" một tín hiệu (Event) kích hoạt AWS Lambda.
-
Nhiệm vụ của Lambda: Tự động tối ưu hóa ảnh, tạo ảnh thu nhỏ (Thumbnail) hoặc đóng dấu (Watermark) cho sản phẩm OriShop mà không làm tốn tài nguyên của cụm EKS chính. Điều này giúp hệ thống tiết kiệm RAM/CPU cho các tác vụ bán hàng.
📜 PHẦN 1: Cấu hình Jenkins (VPS) & Phân quyền AWS
Đây là trạm trung chuyển, nơi biến code từ GitHub thành Docker Image và đẩy lên EKS.
-
Tạo IAM User trên AWS:
-
Tạo một user (vd:
jenkins-deployer) với quyền Programmatic Access. -
Cấp quyền đủ để quản lý EKS (hoặc
AdministratorAccesscho môi trường Lab). -
Lấy
Access Key IDvàSecret Access Key.
-
-
Cài đặt máy chủ Jenkins:
-
Đảm bảo VPS đã cài đặt sẵn Java, Docker, AWS CLI, và
kubectl. -
Cấu hình
aws configuretrên VPS Jenkins bằng bộ Key vừa lấy để Jenkins có quyền ra lệnh cho AWS.
-
-
Cấu hình trên giao diện Jenkins:
-
Lưu thông tin đăng nhập Docker Hub (
tobi1008) vào mục Credentials. -
Lưu thông tin
kubeconfighoặc AWS Credentials vào Jenkins để Pipeline có thể kết nối với EKS. -
Cài đặt các Plugin cần thiết: AWS Credentials, Docker Pipeline, Kubernetes CLI.
-
📜 PHẦN 2: Triển khai Hạ tầng với Terraform (IaC)
Biến toàn bộ giao diện click chuột của AWS thành code. Cấu trúc thư mục của chúng ta gồm:
-
providers.tf: Khai báo nhà cung cấp AWS và khu vực (ap-southeast-1). -
vpc.tf: Dựng mạng ảo riêng biệt, chia subnet Public/Private, cấu hình Internet Gateway và NAT Gateway cho EKS và RDS. -
eks.tf: Dựng cụm Kubernetes (EKS Cluster) và Node Group (máy chủt3.micro). Quan trọng: Phải kích hoạt bộ nhận diện OIDC Provider tại đây để dùng cho IRSA. -
rds.tf: Dựng Database MySQL. -
s3_iam.tf: File "hành xác" nhất nhưng xịn nhất:-
Tạo bucket S3 ngẫu nhiên (
com-quyenlt-media-...). -
Dùng block
aws_s3_bucket_public_access_blockvàaws_s3_bucket_policyđể mở khóa Public Access, cho phép thế giới xem ảnh. -
Thiết lập thuật toán IRSA (
sts:AssumeRoleWithWebIdentity) kết nối OIDC của EKS với IAM Role. -
Cấp chính xác quyền
s3:PutObject,GetObjectcho cái Role đó.
-
-
lambda.tf/variables.tf: Khai báo các hàm không máy chủ (nếu có dùng cho các task nền) và các biến môi trường động.
📜 PHẦN 3: Cấu hình K8s & Mã nguồn Java (App Configuration)
Để Spring Boot chạy mượt trên EKS và chơi chung được với S3.
-
File
orishop-eks.yaml:-
Tạo
Deploymentchạy Docker Imagetobi1008/orishop. -
Tạo
Service(thường là LoadBalancer hoặc NodePort) để xuất web ra ngoài. -
Điểm mấu chốt: Tạo
ServiceAccount(vd:orishop-sa) và gắn thẻ Annotation trỏ về cái ARN của IAM Role mà Terraform sinh ra. Phải map đúng cáiserviceAccountNamenày vào trong spec của Pod.
-
-
Sửa mã nguồn Java (
application.properties):-
Nâng giới hạn upload file của Tomcat:
spring.servlet.multipart.max-file-size=10MB. -
Cập nhật URL kết nối đến RDS Endpoint thật.
-
-
Sửa mã nguồn Java (
pom.xml):-
Thêm thư viện
aws-java-sdk-sts. Đây là "kính lúp" bắt buộc phải có để ứng dụng Java đọc được Token mà Kubernetes cấp cho nó.
-
📜 PHẦN 4: CI/CD Pipeline (Tự động hóa)
-
Jenkinsfile: Viết các Stage rõ ràng: Checkout Code -> Build bằng Maven (tạo file .jar) -> Build Docker Image -> Push Image lên Docker Hubtobi1008/orishop:latest(hoặc build ID) -> Chạy lệnhkubectl applyvàkubectl rollout restartđể ép EKS cập nhật bản mới. -
GitHub Webhook: Lên kho code GitHub của dự án OriShop, mục Settings -> Webhooks, trỏ về URL của Jenkins VPS (kèm path
/github-webhook/). Mỗi lần Tobi push code là Jenkins tự động rít lên làm việc.
📜 PHẦN 5: Vận hành & Cập nhật Database
-
Kết nối cụm EKS từ máy Mac:
-
Chạy lệnh:
aws eks update-kubeconfig --region ap-southeast-1 --name <tên-cluster>để lấy file cấu hình, từ đó dùng được lệnhkubectlđể soi Pod, check Log.
-
-
Import Dữ liệu:
-
Dùng DBeaver hoặc MySQL Workbench kết nối thẳng vào Endpoint của RDS.
-
Import file
.sqlban đầu (hoặc để Flyway trong Spring Boot tự động chạy file migration).
-
Thực Hành
1.Cấu hình Jenkins (VPS) & Phân quyền AWS
Bước 1: Cấp "Hộ chiếu AWS" cho Jenkins Như đã bàn, vì VPS này nằm ngoài AWS, bạn cần:
- Đăng nhập AWS Console, vào IAM -> Tạo một User tên là
jenkins-deployer. - Cấp quyền
AdministratorAccess

Tạo Access Key và Secret Access Key.

Vào giao diện web của Jenkins -> Manage Jenkins -> Credentials -> Thêm cặp khóa này vào dưới dạng Secret text hoặc Username with password để lát nữa Pipeline lấy ra dùng.

Bạn hãy điền nốt 2 ô còn lại như sau trước khi bấm Create nhé:
-
ID: Bạn gõ chính xác chữ
aws-credentials(viết liền, không dấu cách). Lát nữa code của chúng ta sẽ gọi đúng cái tên này ra để mở khóa AWS. -
Description: Ghi chú cho dễ nhớ, ví dụ:
Key AWS để deploy EKS.
Bước 2: Cấp chìa khóa kho Docker Hub Tương tự, Jenkins cần mật khẩu để đẩy Image lên kho tobi1008. Bạn cũng vào Credentials của Jenkins và lưu Username/Password của Docker Hub vào đó nhé.
bạn bấm Add credentials làm thêm y hệt một cái nữa cho Docker Hub nhé:
-
Kind: Vẫn chọn Username with password.
-
Username: Tên đăng nhập Docker Hub của bạn (hôm qua là
tobi1008). -
Password: Mật khẩu Docker Hub của bạn.
-
ID: Gõ chính xác chữ
docker-hub-credentials. -
Description:
Tai khoan Docker Hub.

Bước 3 : Kịch bản Jenkinsfile
Bạn hãy tạo một file có tên là Jenkinsfile (chữ J viết hoa, không có đuôi mở rộng) nằm ngay thư mục gốc trong source code dự án của bạn, và dán toàn bộ nội dung này vào:
pipeline {agent {kubernetes {yaml '''apiVersion: v1kind: Podspec:containers:- name: docker-cliimage: docker:24.0.5command: ["cat"]tty: true- name: dindimage: docker:24.0.5-dindsecurityContext:privileged: trueenv:- name: DOCKER_TLS_CERTDIRvalue: ""- name: k8s-toolsimage: alpine/k8s:1.28.2command: ["cat"]tty: true'''}}environment {DOCKER_HOST = 'tcp://localhost:2375'IMAGE_NAME = "tobi1008/orishop:latest"DOCKER_CREDS = credentials('docker-hub-credentials')AWS_CREDS = credentials('aws-credentials')AWS_REGION = "ap-southeast-1"EKS_CLUSTER_NAME = "quyenlt-eks-cluster"}stages {stage('1. Kéo mã nguồn') {steps {checkout scmecho "--- Đã lấy mã nguồn com.quyenlt mới nhất ---"}}stage('2. Đóng gói & Đẩy Image') {steps {container('docker-cli') {script {echo "--- Đang đợi Docker Daemon nổ máy (waitUntil) ---"// Jenkins sẽ lặp lại đoạn này cho đến khi return truewaitUntil {def status = sh(script: "docker version", returnStatus: true)if (status == 0) {return true} else {echo "Docker chưa sẵn sàng, đang thử lại sau 5s..."sleep 5return false}}echo "--- Docker đã sẵn sàng! Bắt đầu Build Image ---"sh "docker build -t ${IMAGE_NAME} ."sh "echo ${DOCKER_CREDS_PSW} | docker login -u ${DOCKER_CREDS_USR} --password-stdin"sh "docker push ${IMAGE_NAME}"}}}}stage('3. Triển khai lên AWS EKS') {steps {container('k8s-tools') {script {echo "--- Đang kết nối tới cụm EKS: ${EKS_CLUSTER_NAME} ---"withEnv(["AWS_ACCESS_KEY_ID=${AWS_CREDS_USR}", "AWS_SECRET_ACCESS_KEY=${AWS_CREDS_PSW}"]) {sh "aws eks update-kubeconfig --region ${AWS_REGION} --name ${EKS_CLUSTER_NAME}"echo "--- Đang nạp file cấu hình vào EKS (Phòng default) ---"sh "kubectl apply -f orishop-eks.yaml -n default"sh "kubectl rollout restart deployment/orishop-app -n default"echo "--- Kiểm tra trạng thái các Pod ---"sh "kubectl get pods -n default"echo "--- Triển khai lên AWS EKS thành công rực rỡ! ---"}}}}}}}
Commit file này lên github, nhớ là file Jenkinsfile đặt trong mã nguồn code nhé.
Bước 4: Kích hoạt Jenkins
Vào giao diện web của Jenkins, tạo một New Item -> Chọn Pipeline -> Trỏ đường dẫn GitHub của bạn vào để Jenkins đọc cái Jenkinsfile này.
1. Ở phần General (Đang mở)
-
Bạn nên tích vào ô Discard old builds (Xóa các bản build cũ).
-
Trong mục Max # of builds to keep (Số bản build tối đa giữ lại), bạn điền số
5hoặc10. Điều này giúp ổ cứng VPS của bạn không bị rác sau hàng chục lần chạy test. -
Phần Triggers (Tự động kích hoạt): Tạm thời để trống. Lần đầu tiên chúng ta sẽ tự bấm nút chạy (Build Now) để kiểm soát lỗi. (Sau này chạy mượt rồi, mình sẽ hướng dẫn bạn móc nối Webhook để cứ gõ
git pushlà Jenkins tự chạy).
2. Bước Quyết Định: Chuyển sang mục "Pipeline"
Bạn nhìn sang cột menu bên trái, click vào chữ Pipeline (ngay dưới chữ Triggers). Đây mới là phần linh hồn để móc nối với GitHub của bạn:
-
Definition: Bấm vào menu thả xuống, đổi từ Pipeline script thành Pipeline script from SCM (Lấy kịch bản từ Source Control).
-
SCM: Chọn Git.
-
Repository URL: Dán đường dẫn link GitHub chứa source code
com.quyenltcủa bạn vào đây (ví dụ:https://github.com/tobi1008/orishop.git). -
Branch Specifier: Chỗ này cực kỳ quan trọng! Jenkins mặc định để là
*/master. Nếu nhánh trên GitHub của bạn tên làmain, bạn phải sửa lại thành*/mainnhé. -
Script Path: Giữ nguyên chữ
Jenkinsfile(đây là lý do lúc nãy mình dặn viết đúng chữ J hoa).
Cấu hình xong phần Pipeline này, bạn tự tin bấm nút Save màu xanh dương ở góc dưới cùng nhé!

2.Triển khai Hạ tầng với Terraform (IaC)
📁 Cấu trúc thư mục Terraform:
-
providers.tf: Khai báo nhà cung cấp (AWS). -
variables.tf: Chứa các biến dùng chung (Tên cụm, Region) để dễ đổi sau này. -
vpc.tf: Hạ tầng mạng (Đường xá, cầu cống). -
eks.tf: Cụm Kubernetes (Khu công nghiệp). -
rds.tf: Database (Kho dữ liệu). -
s3_iam.tf: Chỗ lưu ảnh và Phân quyền IRSA.
1. File providers.tf (Kết nối với AWS)
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
2. File variables.tf (Trạm điều khiển trung tâm)
variable "aws_region" {
description = "Region triển khai"
default = "ap-southeast-1"
}
variable "cluster_name" {
description = "Tên cụm EKS khớp với Jenkinsfile"
default = "quyenlt-eks-cluster"
}
3. File vpc.tf (Xây dựng mạng lưới Network) Lưu ý: Mình dùng module chuẩn của AWS để code ngắn gọn và an toàn nhất.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "quyenlt-vpc"
cidr = "10.0.0.0/16"
azs = ["${var.aws_region}a", "${var.aws_region}b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
# Bật NAT Gateway để Node ẩn danh vẫn ra được Internet tải Docker Image
enable_nat_gateway = true
single_nat_gateway = true # Dùng 1 cái cho rẻ
enable_dns_hostnames = true
# Tag bắt buộc để EKS biết đường gắn Load Balancer
public_subnet_tags = {
"kubernetes.io/role/elb" = 1
}
private_subnet_tags = {
"kubernetes.io/role/internal-elb" = 1
}
}
4. File eks.tf (Triệu hồi K8s)
module "eks" {source = "terraform-aws-modules/eks/aws"version = "~> 20.0"cluster_name = "quyenlt-eks-cluster"cluster_version = "1.29"# Cho phép người tạo Cluster (là máy Mac của bạn) có quyền Admin mặc địnhenable_cluster_creator_admin_permissions = truecluster_endpoint_public_access = truevpc_id = module.vpc.vpc_idsubnet_ids = module.vpc.private_subnets# Cấu hình Node Group (Các máy chủ chạy App)eks_managed_node_groups = {orishop_nodes = {min_size = 1max_size = 3desired_size = 2# Đã sửa thành t3.micro để vượt qua rào cản Free Tier của AWSinstance_types = ["t3.micro"]capacity_type = "ON_DEMAND"}}# 🔐 CẤP QUYỀN CHO JENKINSaccess_entries = {jenkins_admin = {kubernetes_groups = []principal_arn = "arn:aws:iam::808999395759:user/jenkins-deployer"policy_associations = {admin_policy = {# Đã sửa lỗi chính tả từ aws:iam thành aws:ekspolicy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"access_scope = {type = "cluster"}}}}}tags = {Environment = "dev"Project = "com.quyenlt"}enable_irsa = true}
Hãy tạo xong 4 file này trong cùng 1 thư mục trên Mac. Mở Terminal tại thư mục đó và gõ lệnh "làm phép" đầu tiên để tải các module về:
terraform init
Nếu lệnh này chạy mượt và hiện ra dòng chữ Terraform has been successfully initialized! màu xanh lá cây thì chứng tỏ code của bạn hoàn toàn đúng
hãy mở Terminal lên, gõ ngay câu thần chú này và nhấn Enter:
terraform apply -auto-approve
Bây giờ bạn đợi cho hệ thông AWS khởi tạo thôi.
Thiết kế Kho Dữ Liệu (File rds.tf)
Bạn hãy tạo một file mới tên là rds.tf nằm cùng thư mục với các file lúc nãy, và dán đoạn code siêu mượt này vào.
Đoạn code này sẽ làm 3 việc:
-
Tạo một bức tường lửa (Security Group) chỉ cho phép mạng nội bộ EKS được chọc vào cổng 3306.
-
Gom các Private Subnet lại thành một cái "giường" êm ái cho Database nằm.
-
Tạo ra con MySQL 8.0 với tài khoản và mật khẩu chuẩn khớp với code Java của
com.quyenlt.
# 1. Bức tường lửa bảo vệ Database (Chỉ nhận Traffic từ trong VPC)resource "aws_security_group" "rds_sg" {name = "quyenlt-rds-sg"description = "Allow MySQL traffic from EKS nodes"vpc_id = module.vpc.vpc_idingress {description = "MySQL from VPC"from_port = 3306to_port = 3306protocol = "tcp"# Chỉ cho phép các IP nằm trong dải mạng của VPC được kết nốicidr_blocks = [module.vpc.vpc_cidr_block]}egress {from_port = 0to_port = 0protocol = "-1"cidr_blocks = ["0.0.0.0/0"]}}# 2. Nhóm các Private Subnet để giấu Databaseresource "aws_db_subnet_group" "quyenlt_db_subnet" {name = "quyenlt-db-subnet-group"subnet_ids = module.vpc.private_subnetsdescription = "Subnet group for com.quyenlt RDS"}# 3. Tạo máy chủ MySQL 8.0resource "aws_db_instance" "orishop_db" {identifier = "orishop-db"engine = "mysql"engine_version = "8.0"instance_class = "db.t3.micro" # Dùng gói rẻ nhất cho Laballocated_storage = 20storage_type = "gp2"db_name = "orishop" # Tên Database tạo sẵn để Spring Boot kết nốiusername = "admin"password = "OriShop12345!" # Mật khẩu giống cấu hình hôm trướcdb_subnet_group_name = aws_db_subnet_group.quyenlt_db_subnet.namevpc_security_group_ids = [aws_security_group.rds_sg.id]publicly_accessible = false # Tuyệt đối không đưa ra Internetskip_final_snapshot = true # Bắt buộc True để lúc gõ destroy không bị kẹt báo lỗi}output "RDS_ENDPOINT" {value = aws_db_instance.orishop_db.endpoint}
Bạn hãy lưu file rds.tf này lại nhé! Vì bạn đang chạy lệnh apply cho EKS rồi, nên file này hiện tại chưa được AWS biết đến đâu. Lát nữa EKS xây xong, chúng ta chỉ việc gõ terraform apply thêm một lần nữa (tốn đúng 3 phút) là con Database này sẽ xuất hiện ngay lập tức.
Mảnh ghép cuối cùng và cũng là mảnh ghép mang đậm tính "ma thuật" nhất của hệ sinh thái AWS.Trong môi trường thực tế, nếu bạn hardcode (gõ cứng) cặp Access Key / Secret Key vào thẳng code Java để truy cập S3 thì sẽ bị các chuyên gia bảo mật "gõ đầu" ngay lập tức. Giải pháp chuẩn Enterprise là dùng IRSA (IAM Roles for Service Accounts).
Cơ chế này giống như việc bạn cấp cho con Pod chạy app com.quyenlt một cái "Thẻ nhân viên" vô hình. Khi Pod cần đẩy ảnh lên S3, nó chỉ việc giơ thẻ ra, AWS quét thẻ (OIDC), thấy hợp lệ là cho qua mà không cần bất kỳ mật khẩu nào!
Phép màu Phân quyền (File s3_iam.tf)
Bạn hãy tạo file s3_iam.tf nằm chung thư mục với các file kia và dán đoạn code này vào.
(Lưu ý: Tên bucket S3 trên toàn cầu không được trùng nhau, nên mình dùng một hàm random_string sinh ra vài chữ số ngẫu nhiên gắn vào đuôi tên bucket cho chắc cú).
# 1. Tạo đuôi ngẫu nhiên chống trùng lặp tên S3resource "random_string" "suffix" {length = 6special = falseupper = false}# 2. Xây kho chứa ảnh (S3 Bucket)resource "aws_s3_bucket" "orishop_media" {bucket = "com-quyenlt-media-${random_string.suffix.result}"# Cực kỳ quan trọng khi làm Lab: Cho phép Terraform xóa Bucket ngay cả khi có ảnh bên trongforce_destroy = true}# 3. Viết giấy phép (Policy): Chỉ cho phép Đọc/Ghi vào đúng Bucket nàyresource "aws_iam_policy" "s3_access_policy" {name = "quyenlt-s3-access-policy"description = "Cho phep EKS Pods doc/ghi vao S3 cua orishop"policy = jsonencode({Version = "2012-10-17"Statement = [{Effect = "Allow"Action = ["s3:PutObject","s3:GetObject","s3:DeleteObject","s3:ListBucket"]Resource = [aws_s3_bucket.orishop_media.arn,"${aws_s3_bucket.orishop_media.arn}/*"]}]})}# 4. Thuật toán IRSA (Biến Service Account của K8s thành Thẻ nhân viên AWS)data "aws_iam_policy_document" "irsa_trust_policy" {statement {actions = ["sts:AssumeRoleWithWebIdentity"]effect = "Allow"condition {test = "StringEquals"# Kết nối với hệ thống nhận diện OIDC của cụm EKSvariable = "${replace(module.eks.cluster_oidc_issuer_url, "https://", "")}:sub"# Quy định: Chỉ có Pod nào dùng Service Account tên là 'orishop-sa' mới được xài thẻ nàyvalues = ["system:serviceaccount:default:orishop-sa"]}principals {identifiers = [module.eks.oidc_provider_arn]type = "Federated"}}}# Tạo cái Thẻ (Role) từ thuật toán trênresource "aws_iam_role" "orishop_s3_role" {name = "quyenlt-orishop-s3-role"assume_role_policy = data.aws_iam_policy_document.irsa_trust_policy.json}# Ép Giấy phép (Policy) vào cái Thẻ (Role)resource "aws_iam_role_policy_attachment" "orishop_s3_attach" {role = aws_iam_role.orishop_s3_role.namepolicy_arn = aws_iam_policy.s3_access_policy.arn}# 5. IN RA KẾT QUẢ ĐỂ MANG ĐI CẤU HÌNH JAVA VÀ K8Soutput "S3_BUCKET_NAME" {value = aws_s3_bucket.orishop_media.bucket}output "IRSA_ROLE_ARN" {value = aws_iam_role.orishop_s3_role.arn}# --- THÊM 3 BLOCK NÀY VÀO DƯỚI CÙNG FILE s3_iam.tf ---# 1. Tắt tính năng chặn Public Access mặc định của AWS S3resource "aws_s3_bucket_public_access_block" "orishop_media_public" {bucket = aws_s3_bucket.orishop_media.idblock_public_acls = falseblock_public_policy = falseignore_public_acls = falserestrict_public_buckets = false}# 2. Bật lại quyền kiểm soát ACL (Cần thiết để Spring Boot nhét ảnh vào)resource "aws_s3_bucket_ownership_controls" "orishop_media_ownership" {bucket = aws_s3_bucket.orishop_media.idrule {object_ownership = "BucketOwnerPreferred"}}# 3. Viết giấy phép cho phép CẢ THẾ GIỚI được xem ảnh trên Webresource "aws_s3_bucket_policy" "public_read_policy" {depends_on = [aws_s3_bucket_public_access_block.orishop_media_public]bucket = aws_s3_bucket.orishop_media.idpolicy = jsonencode({Version = "2012-10-17"Statement = [{Sid = "PublicReadGetObject"Effect = "Allow"Principal = "*"Action = "s3:GetObject"Resource = "${aws_s3_bucket.orishop_media.arn}/*"}]})}
Trong kiến trúc Cloud-Native của com.quyenlt, Lambda sẽ đóng vai trò là một "nhân viên mẫn cán" chạy ngầm (Event-Driven Architecture).
Kịch bản hoàn hảo cho Lambda lúc này là: Mỗi khi ai đó dùng app OriShop tải một bức ảnh sản phẩm mới lên cái kho S3 (com-quyenlt-media-...), S3 sẽ ngay lập tức "đá lông nheo" gọi con Lambda dậy. Lambda có thể tự động nén ảnh cho nhẹ web, hoặc ghi log thông báo.
Món tráng miệng Serverless (File lambda.tf)
Bạn tạo thêm file lambda.tf nằm chung với các file cũ, và dán toàn bộ đoạn code này vào.
# 1. Tạo "Thẻ nhân viên" (IAM Role) cho Lambda
resource "aws_iam_role" "lambda_role" {
name = "quyenlt-lambda-s3-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}
# Cấp quyền cơ bản: Cho phép Lambda ghi Log ra CloudWatch để Tobi đọc
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# Cấp quyền nâng cao: Cho phép Lambda đọc ảnh từ S3
resource "aws_iam_role_policy" "lambda_s3_read" {
name = "quyenlt-lambda-s3-read-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = ["s3:GetObject"]
Effect = "Allow"
Resource = "${aws_s3_bucket.orishop_media.arn}/*"
}]
})
}
# 2. Đóng gói đoạn code Python xử lý ảnh thành file ZIP ngay tại chỗ
data "archive_file" "lambda_zip" {
type = "zip"
output_path = "lambda_function.zip"
source {
filename = "index.py"
content = <<-EOF
import json
import urllib.parse
def lambda_handler(event, context):
# Lấy thông tin từ sự kiện S3 báo về
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
# In ra log (sau này có thể viết code nén ảnh thumbnail ở đây)
print(f"OriShop Auto-Bot: Có ảnh sản phẩm mới được upload lên kho {bucket}! Tên file là: {key}")
return {
'statusCode': 200,
'body': json.dumps('Da ghi log thanh cong!')
}
EOF
}
}
# 3. Khởi tạo AWS Lambda Function
resource "aws_lambda_function" "s3_trigger_lambda" {
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
function_name = "quyenlt-s3-image-processor"
role = aws_iam_role.lambda_role.arn
handler = "index.lambda_handler"
runtime = "python3.12"
}
# 4. Cấp quyền cho cái Bucket S3 được phép "gọi điện" đánh thức Lambda
resource "aws_lambda_permission" "allow_s3" {
statement_id = "AllowExecutionFromS3Bucket"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.s3_trigger_lambda.arn
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.orishop_media.arn
}
# 5. Gắn Cò (Trigger): Khi có ảnh mới (ObjectCreated) -> Kích hoạt Lambda
resource "aws_s3_bucket_notification" "bucket_notification" {
bucket = aws_s3_bucket.orishop_media.id
lambda_function {
lambda_function_arn = aws_lambda_function.s3_trigger_lambda.arn
events = ["s3:ObjectCreated:*"]
}
depends_on = [aws_lambda_permission.allow_s3]
}
🚀 Cú búng tay cuối cùng
Bây giờ bạn đã có trọn bộ 7 file trong tay (providers.tf, variables.tf, vpc.tf, eks.tf, rds.tf và s3_iam.tflambda.tf).
Vì lệnh apply lúc nãy của bạn chắc đã tạo xong (hoặc gần xong) EKS rồi. Tobi chỉ cần mở Terminal lên và gõ lại lần nữa:
terraform init -upgrade
terraform apply -auto-approve
Lần này Terraform sẽ phân tích và thấy: "À, EKS và VPC có rồi, thiếu mỗi RDS và S3. Tôi sẽ tạo nốt 2 thằng này!". Quá trình này rất nhanh, chỉ tốn khoảng 3-5 phút cho RDS khởi động.
Khi chạy xong, bạn hãy nhìn xuống dưới cùng của Terminal. Nó sẽ in ra 2 dòng chữ màu xanh lá cây cực kỳ quan trọng ở phần Outputs:
-
S3_BUCKET_NAME = "com-quyenlt-media-xxxxxx" -
IRSA_ROLE_ARN = "arn:aws:iam::xxxxxx:role/quyenlt-orishop-s3-role"

Bạn đã thấy 2 thông số quý giá này hiện ra chưa? Sao chép chúng lại nhé, vì trạm dừng chân tiếp theo của chúng ta chính là nhét chúng vào file orishop-eks.yaml và test thử cái Pipeline Jenkins "siêu to khổng lồ" đấy!
3.Cấu hình K8s & Mã nguồn Java (App Configuration)
Chốt sổ file orishop-eks.yaml
Bạn hãy mở file orishop-eks.yaml trong thư mục source code dự án (nơi chứa code Java và Jenkinsfile) và cập nhật nội dung của nó thành như sau:
---# 1. TẠO "THẺ NHÂN VIÊN" (SERVICE ACCOUNT) ĐỂ APP CÓ QUYỀN VÀO S3apiVersion: v1kind: ServiceAccountmetadata:name: orishop-saannotations:eks.amazonaws.com/role-arn: arn:aws:iam::808999395759:role/quyenlt-orishop-s3-role---# 2. TRIỂN KHAI ỨNG DỤNG LÊN CLUSTER (DEPLOYMENT)apiVersion: apps/v1kind: Deploymentmetadata:name: orishop-appspec:replicas: 2 # Chạy 2 Pod để đảm bảo không bao giờ sập webselector:matchLabels:app: orishoptemplate:metadata:labels:app: orishopspec:serviceAccountName: orishop-sa # Giao thẻ nhân viên cho Podcontainers:- name: orishopimage: tobi1008/orishop:latest # Image sẽ được Jenkins đẩy lênports:- containerPort: 8091env:# --- KẾT NỐI DATABASE (RDS MYSQL) ---- name: SPRING_DATASOURCE_URLvalue: "jdbc:mysql://orishop-db.ch8mw2wuqs14.ap-southeast-1.rds.amazonaws.com:3306/orishop"- name: SPRING_DATASOURCE_USERNAMEvalue: "admin"- name: SPRING_DATASOURCE_PASSWORDvalue: "OriShop12345!"# --- KẾT NỐI KHO ẢNH (AWS S3) ---- name: AWS_S3_BUCKET_NAMEvalue: "com-quyenlt-media-dgecje"- name: AWS_REGIONvalue: "ap-southeast-1"---# 3. MỞ CỬA CHO KHÁCH HÀNG TRUY CẬP (LOAD BALANCER SERVICE)apiVersion: v1kind: Servicemetadata:name: orishop-servicespec:type: LoadBalancer # Báo AWS cấp cho 1 cái tên miền (URL) Publicselector:app: orishopports:- protocol: TCPport: 80targetPort: 8091
4.Vận hành & Cập nhật Database
🛠️ Bước 1: Cập nhật lại Kubeconfig
Mở Terminal trên Mac và gõ lệnh này để AWS nạp lại địa chỉ cụm EKS mới vào máy:
aws eks update-kubeconfig --region ap-southeast-1 --name quyenlt-eks-cluster
(Nó sẽ báo Updated context... là thành công).
🛠️ Bước 2: Tạo lại con Pod "Đặc vụ"
Vì cụm EKS hiện tại là cụm mới tinh tình tình, chưa có cái Pod nào bên trong cả, nên bro phải thả con tmp-mysql-client vào lại:
kubectl run tmp-mysql-client --image=mysql:8.0 --command -- sleep 3600
Gõ tiếp kubectl get pods và đợi chừng vài giây cho đến khi nó hiện trạng thái Running nhé. Lệnh dưới đây anh chạy tại đường dẫn thư mục chứ file sql của mã nguồn là file datalast.sql nhé.
kubectl exec -i tmp-mysql-client -- mysql -h orishop-db.ch8mw2wuqs14.ap-southeast-1.rds.amazonaws.com -u admin -pOriShop12345! orishop < datalast.sql
🚀 Sau khi xong, hãy làm sạch hiện trường!
Xong việc thì "giết" con đặc vụ đi cho đỡ tốn tài nguyên của cụm(Do chúng ta chỉ mượn EKS để import database vào RDS thôi, nên xong rồi xóa thoải mái nhé ):
kubectl delete pod tmp-mysql-client
5.CI/CD Pipeline
quay lại giao diện Jenkins trên trình duyệt, vào project orishop-eks-pipeline và thực hiện cú búng tay quyết định:
-
Bấm Build Now.
-
Mở Console Output (nhấp vào cái vòng tròn đang xoay) để theo dõi:
-
Xem nó có build Docker Image và push lên
tobi1008/orishop:latestthành công không. -
Xem bước cuối cùng nó có báo
Triển khai lên AWS EKS thành công rực rỡ!không.
-

Cách lấy link truy cập :
Sau khi Jenkins hiện màu xanh lá cây (Success), bạn quay lại Terminal trên máy Mac và gõ lệnh này để lấy địa chỉ website:
kubectl get svc orishop-service
Bạn hãy nhìn vào cột EXTERNAL-IP. Nó sẽ là một cái link dài dằng dặc của AWS (dạng ...ap-southeast-1.elb.amazonaws.com).

Cấu hình GitHub Webhook để kích hoạt Build tự động:
Bản chất của Webhook giống như một cái chuông cửa. Khi bạn đẩy code mới lên GitHub, GitHub sẽ tự động "bấm chuông" gọi Jenkins dậy làm việc. Để nối dây chuông này, chúng ta cần làm 2 đầu:
Mở cửa đón khách ở phía Jenkins
Jenkins mặc định hơi "lười", nó sẽ không tự động làm nếu bạn không giao việc.
-
Bạn mở giao diện Jenkins, bấm vào Job Deploy-OriShop của chúng ta.
-
Nhìn sang menu bên trái, chọn Configure (Cấu hình).
-
Cuộn chuột xuống tìm mục Triggers (Trình kích hoạt bản build).
-
Bạn tích chọn vào ô: GitHub hook trigger for GITScm polling.
-
Kéo xuống dưới cùng và bấm Save.

Nối dây chuông từ phía GitHub
Bây giờ chúng ta sang nhà GitHub để bảo nó biết địa chỉ của Jenkins mà gọi.
-
Bạn vào trang mã nguồn
orishoptrên GitHub. -
Bấm vào tab Settings (biểu tượng bánh răng) ở trên cùng.
-
Nhìn sang menu bên trái, chọn Webhooks, rồi bấm nút Add webhook (có thể GitHub sẽ yêu cầu bạn nhập lại mật khẩu để xác nhận).
-
Cấu hình bảng hiện ra chính xác như sau:
-
Payload URL: Bạn điền chính xác đường dẫn này vào:
http://<IP_PUBLIC_CỦA_MÁY_JENKINS>:8080/github-webhook/(Lưu ý cực kỳ quan trọng: Bắt buộc phải có dấu gạch chéo/ở cuối cùng, nếu thiếu dấu này Jenkins sẽ từ chối nhận thông báo). -
Content type: Chọn
application/json. -
Secret: Để trống.
-
Which events would you like to trigger this webhook?: Giữ nguyên mặc định là
Just the push event(Chỉ báo khi có code mới được push lên). -
Đảm bảo ô Active đang được tích.
-
-
Bấm nút Add webhook màu xanh lá cây ở dưới cùng.

Ngay sau khi bạn bấm Add, GitHub sẽ gửi thử một tin nhắn "chào hỏi" sang Jenkins. Nếu bạn thấy cái webhook vừa tạo có hiện lên biểu tượng dấu tích màu xanh lá cây (✅) ở bên cạnh, nghĩa là 2 bên đã thông đường mạng với nhau!

5: Điều chỉnh lại mã nguồn để phù hợp với tài nguyên AWS
Để code Java nói chuyện được với cấu hình AWS ở trên, cần bắt buộc sửa 2 file sau:
1. Thêm bộ công cụ giải mã Token (pom.xml)
Kubernetes cấp thẻ VIP, nhưng Java mặc định bị "cận thị". Phải thêm thư viện STS để nó đọc được Token:
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sts</artifactId>
</dependency>
2. Nới lỏng giới hạn Upload (src/main/resources/application.properties)
Mặc định Spring Boot chặn file > 1MB (Gây lỗi kẹt upload không hiện log).
# Sửa lỗi Maximum upload size exceeded
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
# Update đúng endpoint của RDS Terraform tạo ra
spring.datasource.url=jdbc:mysql://orishop-db...rds.amazonaws.com:3306/orishop
Kết Quả
Tận hưởng phép thuật "Không chạm"
Để test xem cỗ máy hoạt động chưa, bạn hãy mở máy tính cá nhân lên, vào mã nguồn của ứng dụng. Bạn có thể mở một file bất kỳ (ví dụ file README.md hoặc thêm một dòng comment nhỏ vào trong các class thuộc package com.quyenlt), sau đó gõ 3 lệnh thần thánh:
git add .
git commit -m "Test tự động hóa Webhook"
git push origin main
Ngay khoảnh khắc bạn ấn Enter xong lệnh push, hãy nhanh tay chuyển sang tab trình duyệt mở Jenkins. Bạn sẽ thấy bản build mới tự động nhảy số và bắt đầu chạy mà bạn không hề động vào một ngón tay nào!

Truy cập website và kiểm tra xem đã kết nối với S3 thành công chưa bằng cách upload 1 ảnh lên thử nhé .

Kết thúc bài Lab này, chúng ta không chỉ dừng lại ở việc có một website com.quyenlt chạy được trên mạng. Thành quả lớn nhất mà Tobi đạt được chính là việc làm chủ sự phối hợp nhịp nhàng giữa Hạ tầng (Terraform), Tự động hóa (Jenkins) và Bảo mật (EKS IRSA).
Việc giải quyết thành công con bug "vân tay OIDC" hay "thư viện STS" chính là những trải nghiệm thực chiến quý giá nhất, giúp chúng ta hiểu rằng trong thế giới Cloud, quyền hạn (Permission) và danh tính (Identity) mới là những chìa khóa quan trọng nhất. Dự án OriShop giờ đây đã sẵn sàng để chịu tải, dễ dàng mở rộng và cực kỳ an toàn.