Logo
Featured Image

Triển Khai Hệ Thống CI/CD Với Jenkins, Kubernetes (K3s), Terraform và AWS RDS

Author
Tobi 22/03/2026 7 views

Ở bài viết trước chúng ta đã biết cách để sử dụng Terraform để Xây Dựng Hệ Thống 2-Tier HA (EC2, RDS, S3, ALB) trong hệ sinh thái AWS.

Trong bài viết này, chúng ta sẽ cùng nhau xây dựng một hệ thống CI/CD (Continuous Integration / Continuous Deployment) hoàn toàn tự động cho một ứng dụng Spring Boot (package com.quyenlt). Kiến trúc này áp dụng các tiêu chuẩn thực tế của doanh nghiệp, đảm bảo tính bảo mật, khả năng mở rộng và triển khai không gián đoạn (Zero-Downtime).

🛠 Kiến trúc hệ thống bao gồm:

  • Infrastructure as Code (IaC): Sử dụng Terraform để tự động khởi tạo hạ tầng trên AWS (EC2 & RDS).

  • CI/CD Pipeline: Jenkins kết hợp GitHub Webhook để tự động hóa quy trình build và deploy.

  • Containerization: Docker & Docker Hub để đóng gói ứng dụng.

  • Orchestration: Kubernetes (K3s) để vận hành ứng dụng, quản lý Secrets và tự động cập nhật.

  • Database: AWS RDS (MySQL) tách biệt hoàn toàn khỏi cụm K8s để đảm bảo an toàn dữ liệu.

  • Security & SSL: Cert-Manager tự động cấp phát chứng chỉ HTTPS từ Let's Encrypt cho tên miền.


Bước 1: Khởi tạo hạ tầng tự động với Terraform

Thay vì thao tác thủ công trên giao diện AWS, chúng ta sẽ dùng file cấu hình Terraform để tạo 2 máy chủ EC2 (một cho Jenkins, một cho Kubernetes) và 1 cơ sở dữ liệu RDS.

Tạo file main.tf với nội dung sau:

provider "aws" {
  region = "ap-southeast-1"
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
  }
}

resource "aws_key_pair" "tobi_key" {
  key_name   = "tobi-aws-key"
  public_key = file("~/.ssh/tobi-aws.pub") 
}

# --- 1. JENKINS SERVER ---
resource "aws_security_group" "jenkins_sg" {
  name        = "jenkins_security_group"
  ingress { from_port = 22, to_port = 22, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
  ingress { from_port = 8080, to_port = 8080, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
  egress { from_port = 0, to_port = 0, protocol = "-1", cidr_blocks = ["0.0.0.0/0"] }
}

resource "aws_instance" "jenkins_server" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.small" 
  key_name               = aws_key_pair.tobi_key.key_name
  vpc_security_group_ids = [aws_security_group.jenkins_sg.id]
  user_data              = file("install_jenkins.sh")
  tags = { Name = "Jenkins-Server" }
}

# --- 2. KUBERNETES (K3S) SERVER ---
resource "aws_security_group" "k8s_sg" {
  name        = "k8s_security_group"
  ingress { from_port = 22, to_port = 22, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
  ingress { from_port = 80, to_port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
  ingress { from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
  ingress { from_port = 6443, to_port = 6443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
  egress { from_port = 0, to_port = 0, protocol = "-1", cidr_blocks = ["0.0.0.0/0"] }
}

resource "aws_instance" "k8s_server" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.small" 
  key_name               = aws_key_pair.tobi_key.key_name
  vpc_security_group_ids = [aws_security_group.k8s_sg.id]
  user_data              = file("install_k8s.sh")
  tags = { Name = "Kubernetes-Server" }
}

# --- 3. AWS RDS (MYSQL) ---
resource "aws_security_group" "rds_sg" {
  name        = "rds_security_group"
  ingress { from_port = 3306, to_port = 3306, protocol = "tcp", security_groups = [aws_security_group.k8s_sg.id] }
  ingress { from_port = 3306, to_port = 3306, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] } # Mở tạm để import data
  egress { from_port = 0, to_port = 0, protocol = "-1", cidr_blocks = ["0.0.0.0/0"] }
}

resource "aws_db_instance" "orishop_db" {
  allocated_storage    = 20
  engine               = "mysql"
  engine_version       = "8.0"
  instance_class       = "db.t3.micro"
  db_name              = "orishop"
  username             = "admin"
  password             = "Orishop12345!" 
  parameter_group_name = "default.mysql8.0"
  skip_final_snapshot  = true
  publicly_accessible  = true
  vpc_security_group_ids = [aws_security_group.rds_sg.id]
  tags = { Name = "OriShop-RDS-Database" }
}

output "rds_endpoint" { value = aws_db_instance.orishop_db.endpoint }

Tạo kèm file install_k8s.sh cùng thư mục:

#!/bin/bash
apt-get update -y
curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644
PUBLIC_IP=$(curl -s ifconfig.me)
sed -i "s/127.0.0.1/$PUBLIC_IP/g" /etc/rancher/k3s/k3s.yaml

Chạy lệnh terraform initterraform apply -auto-approve để AWS cấp phát toàn bộ hệ thống.


Bước 2: Thiết lập ban đầu cho Jenkins (Plugins & Credentials)

Sau khi hạ tầng tạo xong, truy cập vào http://<IP_JENKINS>:8080 để mở khóa Jenkins (lấy mật khẩu khởi tạo bằng lệnh cat /var/lib/jenkins/secrets/initialAdminPassword trên server Jenkins).

2.1 Cài đặt Plugins cần thiết

Vào Manage Jenkins -> Plugins -> Available plugins và cài đặt:

  1. Docker Pipeline: Để Jenkins có thể chạy lệnh docker.builddocker.push.

  2. Docker Plugin: Hỗ trợ tương tác với Docker daemon trên server Jenkins.

2.2 Tạo Credentials (Chìa khóa bảo mật)

Hệ thống cần 2 chìa khóa: 1 để đẩy Image lên Docker Hub, 1 để điều khiển cụm Kubernetes từ xa. Vào Manage Jenkins -> Credentials -> System -> Global credentials -> Add Credentials:

  • Credential 1: Docker Hub

    • Kind: Username with password

    • Username: Tài khoản Docker Hub của bạn (ví dụ: tobi1008).

    • Password: Mật khẩu Docker Hub.

    • ID: docker-hub-credentials (Phải khớp chính xác với ID gọi trong Jenkinsfile).

  • Credential 2: Kubeconfig (Kết nối K8s)

    • SSH vào máy chủ Kubernetes, gõ lệnh cat /etc/rancher/k3s/k3s.yaml và copy toàn bộ nội dung.

    • Trên Jenkins: Chọn KindSecret file.

    • Tạo một file text trên máy tính, dán nội dung vừa copy vào và lưu lại (ví dụ kubeconfig.yaml). Upload file này lên Jenkins.

    • ID: k8s-kubeconfig (Phải khớp với Jenkinsfile).

Bước 3: Thiết lập Database và Import Dữ Liệu

Sau khi Terraform chạy xong, bạn sẽ nhận được rds_endpoint. Sử dụng terminal hoặc các công cụ như DBeaver để kết nối và bơm dữ liệu vào:

mysql -h <rds_endpoint_cua_ban> -P 3306 -u admin -p orishop < datalast.sql

Lưu ý cực kỳ quan trọng về chuẩn hóa dữ liệu: Để đảm bảo tính toàn vẹn và ứng dụng hoạt động chính xác, khi chuẩn bị file datalast.sql, các khóa (keys) trong cơ sở dữ liệu tuyệt đối không được chứa định dạng ngày tháng. Đồng thời, dữ liệu ở trường created_at chỉ được phép sử dụng các mốc thời gian nằm trong khoảng từ tháng 1/2025 đến tháng 7/2025.


Bước 4: Bảo mật thông tin cấu hình Spring Boot

Để tránh lộ lọt tài khoản AWS RDS, chúng ta tuyệt đối không lưu hard-code mật khẩu trên GitHub. Trong file application.properties của ứng dụng Spring Boot, cấu hình nhận biến môi trường từ Kubernetes:

spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASS}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=none

Tiếp theo, truy cập vào máy chủ Kubernetes và tạo Secret để lưu trữ an toàn:

kubectl create secret generic orishop-db-secret \
  --from-literal=DB_URL='jdbc:mysql://<rds_endpoint>:3306/orishop?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC' \
  --from-literal=DB_USER='admin' \
  --from-literal=DB_PASS='Orishop12345!' \
  --insecure-skip-tls-verify=true


Bước 5: Viết bản vẽ Kubernetes & Tích hợp SSL (HTTPS)

Để ứng dụng có chứng chỉ SSL miễn phí tự động gia hạn, chúng ta cài đặt Cert-Manager lên Server cài K8s:

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.4/cert-manager.yaml --insecure-skip-tls-verify=true

Khai báo ClusterIssuer (tạo file cluster-issuer.yaml và apply):

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
    - http01:
        ingress:
          class: traefik

Cuối cùng, file manifest triển khai ứng dụng orishop-k8s.yaml hoàn chỉnh (gồm Deployment, Service, Ingress và chèn Secret):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orishop-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: orishop
  template:
    metadata:
      labels:
        app: orishop
    spec:
      containers:
      - name: orishop-app
        image: tobi1008/orishop:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8091
        env:
        - name: DB_URL
          valueFrom: { secretKeyRef: { name: orishop-db-secret, key: DB_URL } }
        - name: DB_USER
          valueFrom: { secretKeyRef: { name: orishop-db-secret, key: DB_USER } }
        - name: DB_PASS
          valueFrom: { secretKeyRef: { name: orishop-db-secret, key: DB_PASS } }

---
apiVersion: v1
kind: Service
metadata:
  name: orishop-service
spec:
  selector:
    app: orishop
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8091

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: orishop-ingress
  annotations:
    kubernetes.io/ingress.class: "traefik"
    cert-manager.io/cluster-issuer: "letsencrypt-prod" 
spec:
  tls:
  - hosts:
    - orishop.quyenlt.com
    secretName: orishop-tls-secret
  rules:
  - host: orishop.quyenlt.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: orishop-service
            port:
              number: 80


Bước 6: Tự động hóa hoàn toàn với Jenkinsfile và GitHub Webhook

Trên source code GitHub, tạo file Jenkinsfile để định nghĩa luồng làm việc tự động:

pipeline {
    agent any
    environment {
        DOCKER_HUB_USER = "tobi1008" 
        IMAGE_NAME = "${DOCKER_HUB_USER}/orishop" 
        IMAGE_TAG = "v${env.BUILD_NUMBER}" 
    }
    stages {
        stage('Checkout Code') {
            steps { git branch: 'main', url: 'https://github.com/tobi1008/orishop.git' }
        }
        stage('Build Docker Image') {
            steps {
                script { dockerImage = docker.build("${IMAGE_NAME}:${IMAGE_TAG}") }
            }
        }
        stage('Push to Docker Hub') {
            steps {
                script {
                    docker.withRegistry('', 'docker-hub-credentials') {
                        dockerImage.push()
                        dockerImage.push('latest') 
                    }
                }
            }
        }
        stage('Deploy to Kubernetes') {
            steps {
                withCredentials([file(credentialsId: 'k8s-kubeconfig', variable: 'KUBECONFIG')]) {
                    sh '''
                    export KUBECONFIG=$KUBECONFIG
                    kubectl apply -f orishop-k8s.yaml --insecure-skip-tls-verify=true
                    kubectl rollout restart deployment/orishop-deployment --insecure-skip-tls-verify=true
                    '''
                }
            }
        }
    }
}

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.

  1. Bạn mở giao diện Jenkins, bấm vào Job Deploy-OriShop của chúng ta.

  2. Nhìn sang menu bên trái, chọn Configure (Cấu hình).

  3. Cuộn chuột xuống tìm mục Triggers (Trình kích hoạt bản build).

  4. Bạn tích chọn vào ô: GitHub hook trigger for GITScm polling.

  5. 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.

  1. Bạn vào trang mã nguồn orishop trên GitHub.

  2. Bấm vào tab Settings (biểu tượng bánh răng) ở trên cùng.

  3. 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).

  4. 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.

  5. 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!

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!

Tậ

Tận hưởng thành quả hiện website orishop.quyenlt.com của mình đã hoạt động.


Kết Luận

Nhìn lại chặng đường vừa qua, bạn đã tự tay dựng lên một kiến trúc chuẩn Enterprise thực thụ mà rất nhiều công ty công nghệ đang áp dụng:

  • Infrastructure as Code (IaC): Dùng Terraform điều khiển hạ tầng AWS tạo server rẹt rẹt.

  • CI/CD Pipeline: Tự động hóa hoàn toàn luồng đẩy code với Jenkins và GitHub Webhook.

  • Containerization & Orchestration: Đóng gói app bằng Docker và chạy cập nhật cuốn chiếu (Zero-downtime) trên Kubernetes (K3s).

  • Cloud Database: Tách rời dữ liệu an toàn lên AWS RDS.

  • Security: Quản lý mật khẩu bằng K8s Secrets và cấp chứng chỉ HTTPS tự động với Cert-Manager.

Hy vọng bản hướng dẫn tổng hợp này sẽ là tài liệu tham khảo hữu ích cho các dự án sắp tới của bạn. Chúc bạn thành công!

Bình luận (0)

+ =
Zalo