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.
- Phần 1: https://viblo.asia/p/xay-dung-ung-dung-bang-nestjs-k8s-argocd-terraform-phan-1-wlVmR1e145Z
- Phần 2: https://viblo.asia/p/xay-dung-ung-dung-bang-nestjs-k8s-argocd-terraform-phan-2-XP4WEEQBL7G
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:
- Tạo EKS cluster.
- 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:

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:

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

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

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.


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:

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:
- Cài đặt SecretManager Driver cho cluster.
- 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:

Đầ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:

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

Với Router, tôi sử dụng Route53 để:
- Tạo domain
- Kết nối tới controller

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

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

Về cơ bản gồm 2 bước:
- Build & push image lên ECR.
- 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:
- Bóc tách
github commit sha256để sử dụng nó như image_tag cho mục đích định danh từng image. - Build docker image.
- 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 /app/node_modules ./node_modules
COPY /app/. .
COPY /app/package*.json ./
EXPOSE 3000
CMD ["node", "dist/src/main"]
Trong dockerfile này tôi chia thành 2 pha chính:
- Builder: cài đặt các packages cần thiết.
- 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:
- Cài đặt kustomize CLI.
- Update image tag.
- Tạo PR tự động.
- 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.

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.

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