0

Xây dựng ứng dụng bằng NestJS, k8s, ArgoCD, Terraform (Phần 2)

Xin chào bạn đọc, ở bài viết lần trước tôi đã chia sẻ với các bạn tổng quan về ứng dụng mà tôi sẽ xây dựng trong series lần này.

Trong bài viết lần này tôi xin phép được đi sâu hơn về cách tôi thiết lập k8s cũng như ArgoCD cho môi trường local. Tôi sử dụng Kustomize cho project của mình nên tôi sẽ chia cấu trúc thư mục ra thành 2 phần:

  • base: đây sẽ là nơi đặt các config "nền tàng" hoặc config chung cho các môi trường (local, stg, prod).
  • overlays: đây là nơi tôi sẽ custom các thiết lập dựa theo môi trường (local, stg, prod).

Giới thiệu qua với bạn đọc thì Kustomize là một công cụ giúp custom cũng như quản lí manifests mà không cần phải chỉnh sửa chúng một cách trực tiếp.

base

1_CbAuXdDWlNOP87Fi9edN6w.jpeg

Thư mục base bao gồm tất cả các thiết lập cơ bản cho:

  • Deployment.
  • Service.
  • Autoscaling.
  • Migration job.

Hãy chú ý tới file kustomization.yaml vì chúng ta sẽ cần file này cho việc thực thi kustomize build để tạo ra các file custom k8s manifests.

Chúng ta cần deployment cho việc quản lí các pods - nơi mà các server containers sẽ chạy. 1_P2D2w04iQrolVX36gaueYQ.jpeg

  1. Deployment sẽ tạo ra ReplicaSet.
  2. ReplicaSet sẽ tạo ra Pods.

Trong thực tế, chúng ta sẽ KHÔNG TẠO POD TRỰC TIẾP mà sẽ tạo pod thông qua deployment.

Deployment

Thiết lập cho deployment sẽ như sau:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: restful-app
  namespace: default
  labels:
    app: restful-app
spec:
  revisionHisotryLimit: 1
  selector:
    matchLabels:
      app: restful-app
  template:
    metadata:
      labels:
        app: restful-app
    spec:
      containers:
        - name: restful-app
          image: restful-app
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          ports:
            - containerPort: 3000

Selector

Đầu tiên, chúng ta hãy cùng nhau nói về selector. Hiểu một cách đơn giản thì

Selector là cách nhóm các resources lại với nhau.

Ở đây selector.matchLabels sẽ chọn ra các pods có label app:restful-app

Template

Đây chính là thiết lập dành cho pod, trong phần này chúng ta sẽ phải chỉ định ra label cho pod, label này "phải khớp" với label được định nghĩa trong selector.matchLabels

template:
    metadata:
      labels:
        app: restful-app
    spec:
      containers:
        - name: restful-app
          image: restful-app
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          ports:
            - containerPort: 3000

Trong phần này, chúng ta sẽ chỉ ra:

  • Container Name
  • Container Image
  • Resources bao gồm memorycpu

Hãy chú ý tới việc chỉ định ra requestslimits resources.

Request resources

Lượng tài nguyên NHỎ NHẤT mà Kubernetes sẽ cung cấp cho container khi container hoạt động.

Limit resources

Lượng tài nguyên TỐI ĐA mà container có thể sử dụng.

Đơn vị của:

  • memory là "Mi" tương đương với "MiB RAM".
  • CPU là "core".

Trong ứng dụng của mình tôi chỉ định:

  • memory: 256Mi ~ 256 MiB RAM.
  • cpu: 250m ~ 250 mili core ~ 0.25core.

Và tôi cũng chỉ định container port3000

Service

Tạo sao chúng ta lại cần service ở đây ? Câu trả lời hết sức đơn giản đó là do Pod KHÔNG CÓ BẤT KÌ địa chỉ IP nào cả nên Service sẽ cung cấp địa chỉ IP để truy cập tới Pod.

Bạn đọc có thể xem hình minh hoạ dưới đây để nắm rõ hơn điều tôi vừa nói.

1_ElU6OCVPaohdYu9xgJSAWg.jpeg

Chúng ta sẽ truy cập tới server thông qua Service thay vì kết nối trực tiếp tới Pod.


apiVersion: v1
kind: Service
metadata:
  name: restful-app
  namespace: default
spec:
  selector:
    app: restful-app
  ports:
    - port: 80
      targetPort: 3000

Rất đơn giản, Service sẽ:

  • Đưa ra ngoài cổng 80 và bản thân nó sẽ kết nối tới cổng 3000 của Pod.
  • Có type là ClusterIP - Public service bằng địa chỉ IP của cluster.

HPA (Horizontal Pod Atuoscaler)

Với HPA chúng ta có thể tự động tăng hoặc giảm số lượng Pods.

Về cơ bản, HPA sẽ điều chỉnh số lượng Pods dựa theo:

  • CPU.
  • Memory.
  • Custom Metrics.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: restful-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: restful-app
  minReplicas: 2
  maxReplicas: 5
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Với thiết lập này, chúng ta sẽ có:

  • Ít nhất là 2 Pods.
  • Nhiều nhất là 5 Pods.
  • Số lượng Pods sẽ thay đổi dựa theo CPU khi chỉ số averageUtilization đạt giá trị 70%, HPA sẽ tăng số lượng pods.

Migration

Như tôi đã nói ở bài viết trước Chúng ta có 2 flows chính:

  • Server app flow.
  • Migration flow

Trong phần này, tôi sẽ nói chi tiết hơn về Migration Flow.

apiVersion: batch/v1
kind: Job
metadata:
  name: restful-app-migration
  labels:
    app: restful-app-migration
spec:
  backoffLimit: 1
  template:
    metadata:
      labels:
        app: restful-app-migration
      annotations:
        sidecar.istio.io/inject: "false"
    spec:
      containers:
      - name: restful-app-migration
        image: restful-app
        securityContext:
          allowPrivilegeEscalation: false
      restartPolicy: Never

Migration Flow chỉ đơn thuần là một job, nó đảm bảo việc tác vụ database migrate được thực thi một cách hoàn chỉnh.

backoffLimit: 1 chỉ ra rằng: Job chỉ có thể retry 1 lần duy nhất.

Kustomize (overlays)

Kustomize là tool cho phép chúng ta có thể custom các base Kubernetes manifests.

Ví dụ, chúng ta có base - configure cơ bản, với kustomize chúng ta có thể tạo các "version" khác như cho môi trường staging hoặc môi trường production.

Để sử dụng kustomize, chúng ta phải có file kustomization.yaml cho base và các môi trường custom khác như hình minh hoạ dưới đây:

1_Zgba5ASF4_pVYzF1vxWtVw.jpeg

Base kustomization.yaml file sẽ như sau:

# base kustomization

resources:
- ./deployment.yaml
- ./service.yaml
- ./hpa.yaml
- ./migration.yaml

Bạn đọc có thể thấy rằng, kustomization file sẽ giống như một "kết tập" của các:

  • Resources.
  • Jobs. dùng trong ứng dụng.

Với overlays environment, thay vì từ khoá resources như ở base kustomization, chúng ta sẽ sử dụng từ khoá patches với ý nghĩa:

Chúng ta sẽ "ghi đè" các base configure sẵn có.

# staging kustomization

# import base configure
resources:
- ../../base

# Create "patch" version
patches:
  - path: patch-deployment.yaml
    target:
      kind: Deployment
      name: restful-app
  - path: patch-hpa.yaml
  - path: patch-migration.yaml

Trong ứng dụng lần này, tôi tạo ra 3 môi trường:

  • dev (chạy ở local)
  • stg
  • prod (về cơ bản sẽ giống như stg, chỉ khác biệt ở mỗi cái tên mà thôi =))))

Môi trường dev

1_O7touT8LToHgvnLUrPKmZw.jpeg

patch-*

Nhìn vào hình minh hoạ trên, bạn đọc có thể thấy chúng ta có:

  • patch-deployment.yaml
  • patch-hpa.yaml
  • patch-migartion.yaml

Đó chính là các overwrite versions của:

  • base/deployment.yaml
  • base/hpa.yaml
  • base/migration.yaml

Đây là base/deployment.yaml

template:
    metadata:
      labels:
        app: restful-app
    spec:
      containers:
        - name: restful-app
          image: restful-app
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          ports:
            - containerPort: 3000

Còn đây là patch-deployment.yaml

spec:
  serviceAccountName: restful-app-dev-sa
  automountServiceAccountToken: true
  containers:
    - name: restful-app
      command:
        - node
        - dist/src/main
      resources:
        requests:
          memory: "512Mi"
          cpu: "20m"
        limits:
          memory: "1024Mi"
          cpu: "40m"

Đâu là sự khác biệt ?

Trong "base version", tôi có chỉ định ra:

  • Container Name.
  • Container Image.
  • Resource (request & limit).

Nhưng trong "patch-version", tôi chỉ định:

  • Câu lệnh để chạy container.
  • Resources (request & limit) nhưng NHIỀU HƠN BASE VERSION.

Bạn có thể thấy "patch-version" cung cấp một giải pháp vận hành cho chúng ta thay vì chỉ đơn thuần là skeleton như "base version"

Tương tự với patch-migration.yaml

spec:
  containers:
  - name: restful-app-migration
    image: trantuananh/restful-app:latest
    securityContext:
      allowPrivilegeEscalation: false
    envFrom:
      - configMapRef:
          name: restful-app-config
    command: ["npm", "run", "migration:run"]
  restartPolicy: Never

Ở đây tôi thiết lập câu lệnh ["npm", "run", "migration:run"] để chạy migration job.

db-*

Đây là phần thiết lập cho database, tôi sử dụng StatefulSetPersistentVolumeClaim.

Với PersistentVolumeClaim

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 256Mi

Mục đích chính của PersistentVolumeClaim chính là yêu cầu một phần bộ nhớ để lưu dữ liệu có trong database.

Trong thiết lập của mình, tôi "yêu cầu" 256Mi cho bộ nhớ.

Với StatefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:17.4
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_DB
          value: rest_app
        - name: POSTGRES_USER
          value: user
        - name: POSTGRES_PASSWORD
          value: password
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc

StatefulSet là

Workload API object có thể được sử dụng cho stable storage.

Ở đây tôi chỉ định ra image namepostgres:17.4

  • Database Name.
  • Database User.
  • Database Password.

và tôi cũng mount StatefulSet với PersistentVolumeClaim, bạn đọc có thể xem hình minh hoạ dưới đây để hiểu rõ hơn.

1__KVMDjNroZrezsU5NB0E6w.jpeg

kustomization.yaml

Trong kustomization.yaml file, tôi không chỉ thiết lập resources và patches mà còn thiết lập configMapGenerator như sau:

configMapGenerator:
  - name: restful-app-config
    behavior: replace
    envs:
      - config.env
generatorOptions:
  disableNameSuffixHash: true

Nói đơn giản thì configMapGenerator:

Được sử dụng để tạo ra ConfigMap từ literal key-value pairs hoặc từ file.

Trong ứng dụng của mình, tôi có tạo ra config.env file như sau:

DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_USERNAME=user
DATABASE_PASSWORD=password
NODE_ENV=dev
DATABASE_NAME=rest_app
SERVER_HOST=0.0.0.0

Bức tranh tổng quan trông sẽ như sau: 1_RNNcn5gI-1b4pn5pvxJgbw.jpeg

Và bạn đọc có thể thấy trong patch-migration, chúng ta cũng có thể sử dụng configMap cho việc thiết lập biến môi trường của ứng dụng với envFromconfigMapRef

spec:
 containers:
 - name: restful-app-migration
   image: trantuananh/restful-app:latest
   securityContext:
     allowPrivilegeEscalation: false
   envFrom:
     - configMapRef:
         name: restful-app-config
   command: ["npm", "run", "migration:run"]
 restartPolicy: Never

argocd.yaml

ArgoCD là gì ?

Là một công cụ cho phép continuous delivery với Kubernetes.

Với ArgoCD, chúng ta có thể sử dụng Github repository giống như source of truth cho việc định nghĩa desired state của ứng dụng.

Application deployment có thể được tracking bởi:

  • Branches update.
  • Tags update.
  • Git commit.

1_HLCFTXD5HESekKWbrW9NEg.jpeg

Cho những bạn đọc chưa biết về GitOps

GitOps là một DevOps practice sử dụng Git như single source of truth để quản lí infrastructure và application configuration.

Thiết lập cho argocd sẽ như sau:

# File name: argocd.yaml

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: restful-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/tuananhhedspibk/restful-app-k8s
    targetRevision: HEAD
    path: overlays/dev
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Điều quan trọng nhất ở đây chính là việc thiết lập sourcerepoURL vì khi đó chúng ta có thể chỉ định single source of truth cho ArgoCD để định nghĩa desired state của ứng dụng.

Để chạy các thiết lập k8s và argocd phía trên trong môi trường local, đầu tiên bạn đọc cần cài đặt:

  • kubectl (k8s command)
  • argocd

Khi đã cài đặt kubectl và argocd, bạn đọc có thể tham khảo các bước dưới đây của tôi để chạy các thiết lập k8s và argocd.

Bước 1: Chạy câu lệnh sau để lấy về argocd password

$kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

Bước 2: Đăng nhập vào argocd bằng cli với password lấy được ở bước 1

$argocd login localhost:8080 - username admin - password <password>

Bước 3: Tạo ứng dụng bằng câu lệnh sau

$argocd app create restful-app \
--repo <your_github_repo_url>\
--path <path_to_overlays> \
--dest-server https://kubernetes.default.svc \
--dest-namespace default

Bước 4: Đừng quên việc forward ports của

  • Argocd Service.
  • Database Service.
  • Application Service.

tới host port như sau:

# App Service
$kubectl port-forward svc/restful-app <host_port>:<svc_port>

# StatefulSet (DB) Service
$kubectl port-forward svc/postgres <host_port>:<svc_port>

# ArgoCD Service
$kubectl port-forward svc/argocd-server -n argocd <host_port>:443

Đây chính là thành quả mà bạn đọc có thể thu nhận được.

ArgoCD UI

1_03JTQxVFAW73Fv9XVamvpQ.jpeg

Swagger UI của ứng dụng

1_GO81FZ5J_L9SoNNMp1aq_Q.jpeg

Tổng kết

Vậy là trong phần 2 của series tôi đã trình bày với bạn đọc về:

  • Cấu trúc của ứng dụng gồm baseoverlays.
  • Thiết lập cơ bản gồm deployment, service, hpajob.
  • Deployment overlays và cách chạy.

Hi vọng rằng thông qua bài viết lần này, bạn đọc đã có thể hiểu hơn về các thiết lập cơ bản có trong k8s cũng như argocd.

Hẹn gặp lại bạn đọc ở các bài viết tiếp theo trong series. Happy coding 😃


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí