+1

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

Chào bạn đọc, trong 2 phần trước tôi đã chia sẻ với bạn đọc cấu trúc cơ bản của ứng dụng cũng như những thiết lập liên quan đến terraform và k8s.

Trong phần cuối này, tôi xin phép được chia sẻ với bạn đọc:

  • Cách tạo EKS
  • Cách deploy ArgoCD với EKS.
  • Làm cách nào để xây dựng deployment flow cho ứng dụng

Cùng bắt đầu thôi nào !!!

Tạo EKS

EKS là một service Kubernetes được cung cấp bởi AWS cho phép chúng ta có thể chạy Kubernetes mà không cần phải cài đặt cũng như thao tác trên các Kubernetes control plane hoặc nodes

Để tạo và sử dụng EKS, chúng ta cần làm những việc sau:

  1. Tạo EKS cluster.
  2. Tạo EKS node group.

Dưới đây tôi sử dụng terraform để tạo 2 resource trên.

resource "aws_eks_cluster" "main" {
  name = "${var.app_name}-${var.env_name}-eks-cluster"

  role_arn = var.iam_cluster_role_arn

  vpc_config {
    subnet_ids = var.subnet_ids
  }

  tags = {
    Name = "${var.app_name}-${var.env_name}-eks-cluster"
  }
}

resource "aws_eks_node_group" "main" {
  cluster_name = aws_eks_cluster.main.name

  node_group_name = "${var.app_name}-${var.env_name}-eks-node-group"
  node_role_arn   = var.iam_node_role_arn
  subnet_ids      = var.subnet_ids

  scaling_config {
    desired_size = var.eks_node_group_desired_size
    max_size     = var.eks_node_group_max_size
    min_size     = var.eks_node_group_min_size
  }

  tags = {
    Name = "${var.app_name}-${var.env_name}-eks-node-group"
  }
}

Hãy chú ý rằng, với EKS cluster và node group chúng ta PHẢI ĐẶT CHÚNG BÊN TRONG PRIVATE SUBNET

Do đó, với subnet_ids config phải là private subnet ids. Hình dung đơn giản thì mối liên hệ giữa EKS và Node Group sẽ như sau:

Screenshot 2025-12-21 at 12.53.35.png

Với role, chúng ta cần tạo ra:

  • EKS cluster role.
  • EKS node group role.
locals {
  policies = [
    "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
    "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
    "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  ]
}

resource "aws_iam_role" "eks_cluster_role" {
  name = "${var.app_name}-${var.env_name}-eks-cluster-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "eks.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF

  tags = {
    Name = "${var.app_name}-${var.env_name}-eks-role"
  }
}

resource "aws_iam_role_policy_attachment" "eks_cluster_role_attachment" {
  role       = aws_iam_role.eks_cluster_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
}

resource "aws_iam_role" "eks_node_role" {
  name = "${var.app_name}-${var.env_name}-eks-node-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF

  tags = {
    Name = "${var.app_name}-${var.env_name}-eks-node-role"
  }
}

resource "aws_iam_role_policy_attachment" "eks_node_role_attachment" {
  for_each   = toset(local.policies)
  role       = aws_iam_role.eks_node_role.name
  policy_arn = each.value
}

Chúng ta có thể thấy rằng AssumeRole service cho eks_node_role là EC2, do đó chúng ta có thể suy luận rằng, EKS Node Group được xây dựng dựa trên EC2.

EKS cluster sẽ như sau: Screenshot 2025-12-21 at 12.59.14.png

Bằng việc thực thi terraform apply, chúng ta có thể tạo ra EKS cluster trên AWS như sau:

Screenshot 2025-12-21 at 13.01.48.png Screenshot 2025-12-21 at 13.01.54.png

OK, đó là về EKS. Hãy chuyển qua phần về ArgoCD.

Deploy ArgoCD với EKS

Bằng việc thực thi câu lệnh dưới đây, chúng ta có thể kết nối tới Kubernetes cluster vừa tạo trên AWS.

$aws eks --region ap-northeast-1 update-kubeconfig --name restful-app-stg-eks-cluster

Sau đó, apply ArgoCD config Screenshot 2025-12-21 at 13.04.00.png

Mởi load balancer tab, lấy URL của load balancer với một tên "ngẫu nhiên", sau đó mở ArgoCD web UI. Screenshot 2025-12-21 at 13.08.52.png

Screenshot 2025-12-21 at 13.09.01.png

Sử dụng "admin" username với password có được từ câu lệnh sau:

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

Chúng ta sẽ có được:

Screenshot 2025-12-21 at 13.10.42.png

Trước khi tạo ứng dụng, tôi muốn bạn đọc chú ý rằng do chúng ta sử dụng SecretManager để thiết lập biến môi trường, do đó chúng ta phải:

  1. Cài đặt SecretManager Driver cho cluster.
  2. Tạo IAM role cho Service Account để lấy về SecretManager value.

Service Account là một loại account đặc biệt, nó sẽ cung cấp định danh cho tiến trình đang chạy trong Pod

Hiểu một cách đơn giản sẽ như sau:

Screenshot 2025-12-21 at 15.40.41.png

Đầu tiên, chạy các câu lệnh sau để cài đặt SecretManager Driver

helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm repo update
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
  --namespace kube-system \
  --set syncSecret.enabled=true
kubectl apply -f https://raw.githubusercontent.com/aws/secrets-store-csi-driver-provider-aws/main/deployment/aws-provider-installer.yaml

Tiếp theo, chạy câu lệnh dưới đây để tạo IAM policy:

aws iam create-policy --policy-name SecretsManagerAccessPolicy --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": "secret_manager_resource_arn"
        }
    ]
}'

IAM role sẽ như sau:


{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::<account_id>:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/<eks-cluster-oidc>"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "oidc.eks.ap-northeast-1.amazonaws.com/id/<eks-cluster-oidc>:sub": "system:serviceaccount:<env>:<service-account-name>"
                }
            }
        }
    ]
}

Đừng quên chạy câu lệnh dưới đây để kết nối OIDC Identity Provider với EKS cluster

eksctl utils associate-iam-oidc-provider --cluster <cluster-name> --approve

Bây giờ là lúc apply ArgoCD configure:

kubectl apply -f overlays/stg/argocd.yaml

và ta sẽ có được điều như sau:

Screenshot 2025-12-21 at 15.45.01.png

Thế nhưng chúng ta vẫn chưa thể truy cập tới swagger UI, do đó chúng ta cần tạo:

  • Controller
  • Router

Với controller, ta sử dụng lệnh

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.3/deploy/static/provider/cloud/deploy.yaml

Screenshot 2025-12-21 at 15.46.19.png

Với Router, tôi sử dụng Route53 để:

  • Tạo domain
  • Kết nối tới controller

Screenshot 2025-12-21 at 15.47.07.png

Lúc này, tôi có thể truy cập tới Swagger UI tại địa chỉ: http://nestjs.restful-app.com/swagger

Screenshot 2025-12-21 at 15.47.38.png

OK, đó là tất cả về deploy ArgoCD với EKS. Hãy cùng nhau di chuyển đến phần cuối cùng của bài viết lần này.

Xây dựng deployment flow cho ứng dụng và k8s

Screenshot 2025-12-21 at 15.50.46.png

Về cơ bản gồm 2 bước:

  1. Build & push image lên ECR.
  2. Trigger restful-app-k8s repository github action để ghi đè imageTag trong kustomization.

Build & push image to ECR

echo "IMAGE_TAG=${{ github.sha }}" >> $GITHUB_ENV
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f Dockerfile .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

Chúng ta có 3 bước chính ở đây:

  1. Bóc tách github commit sha256 để sử dụng nó như image_tag cho mục đích định danh từng image.
  2. Build docker image.
  3. Push image lên ECR

Đây chính là dockerfile

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

COPY . .

RUN npm run build

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/. .
COPY --from=builder /app/package*.json ./

EXPOSE 3000

CMD ["node", "dist/src/main"]

Trong dockerfile này tôi chia thành 2 pha chính:

  1. Builder: cài đặt các packages cần thiết.
  2. Runner: expose running port và khởi động app.

Trigger restful-app-k8s repository github action để ghi đè imageTag trong kustomization

Với restful-app-k8s repo, tôi tạo một github action gọi là kustomize, flow sẽ như sau:

  1. Cài đặt kustomize CLI.
  2. Update image tag.
  3. Tạo PR tự động.
  4. Merge PR vào nhánh main.

- name: Install kustomize
  run: |
    curl -sLo kustomize.tar.gz https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/v5.3.0/kustomize_v5.3.0_linux_amd64.tar.gz
    tar -xzf kustomize.tar.gz
    sudo mv kustomize /usr/local/bin
- name: Update Image Tag on separated ECR env
  run: |
    cd overlays/$ENV
    kustomize edit set image restful-app=$ECR_REGISTRY/$ECR_REPO:$IMAGE_TAG
    cd ../..
    rm -f kustomize.tar.gz
- name: Create Pull Request
  id: cpr
  uses: peter-evans/create-pull-request@v5
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    title: "Kustomization Update of ${{ env.PRODUCT_NAME }} - ${{ env.ENV }} - ${{ env.IMAGE_TAG }}"
    branch: ci/update-kustomization-${{ env.PRODUCT_NAME }}-${{ env.ENV }}-${{ env.IMAGE_TAG }}
    commit-message: ":hammer: Update kustomization of ${{ env.PRODUCT_NAME }}-${{ env.ENV }}-${{ env.IMAGE_TAG }}"
    base: main
    labels: automerge

- name: Merge the PR
  if: steps.cpr.outputs.pull-request-url != ''
  env:
    GH_TOKEN: ${{ secrets.GH_PAT }}
  run: |
    pr_number=$(echo "${{ steps.cpr.outputs.pull-request-url }}" | sed 's/.*\/pull\///')
    echo "Merging PR #$pr_number"
    gh pr merge "$pr_number" --delete-branch

Đây là ví dụ của một PR cho mục đích update image tag.

Screenshot 2025-12-21 at 15.51.17.png

Sau khi cập nhật image tag, ArgoCD sẽ sync một cách tự động để kéo ECR image mới nhất về và deploy app một lần nữa.

Screenshot 2025-12-21 at 15.51.26.png

Tổng kết

Đó là tất cả các bước mà tôi đã thực hiện khi xây dựng một ứng dụng với k8s, argocd và terraform.

Hi vọng qua series bài viết lần này, bạn đọc có thể:

  • Hiểu được cách xây dựng deployment flow với k8s, ArgoCD.
  • HIểu được cách xây dựng ứng dụng với k8s, ArgoCD và EKS

Cảm ơn bạn đọc đã theo dõi đến đây. Hẹn gặp lại ở các bài viết tiếp theo.

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í