+3

Tôi đã xây dựng một ứng dụng đơn giản bằng NestJS, k8s, ArgoCD và Terraform như thế nào ? (Phần 1)

Thời gian gần đây thông qua công việc tôi có dịp được đào sâu hơn về k8s do đó trong series bài viến lần này tôi muốn chia sẻ với bạn đọc những kiến thức cũng như kinh nghiệm tôi tự đúc rút được về k8s thông qua việc xây dựng một ứng dụng đơn giản.

Tổng quan về ứng dụng

Đây chỉ là một server application đơn giản với giao diện Swagger như hình bên dưới: 1_fWEHLyWOkVcp_YaxnlnAOg.jpeg

Ứng dụng của tôi gồm có:

  • GET user data API.
  • UPDATE user data API.
  • DELETE user data API.

Tổng quan về kiến trúc của ứng dụng sẽ như sau: 1_Vlg8fXbWmSWwWvaJD_LfAg.jpeg

Ở đây tôi sử dụng:

  • Github actions và ArgoCD cho GitOps.
  • NestJS để xây dựng server application.
  • Terraform để tạo các AWS resources.

Đó là về phần tổng quan, giờ chúng ta sẽ cùng nhau đi sâu vào chi tiết hơn.

Về server application

Như đã nói ở trên, tôi sử dụng NestJS để xây dựng server application.

Bạn đọc có thể đọc source code hoàn chỉnh tại đây: https://github.com/tuananhhedspibk/restful-app

Do nội dung chính của series lần này là k8s nên tôi sẽ chỉ nói rất nhanh về phần xây dựng server application. Tôi xây dựng ứng dụng dựa trên nguyên lí DDD (Domain Driven Design), bạn đọc có thể xem hình phía dưới để có thể hiểu tổng quan về nguyên lí này. 1_W27hoU2r-1JRyIpD6tI0Eg.jpeg

Bạn đọc có thể tham khảo bài viết này nếu muốn nắm rõ hơn về DDD

Ứng dụng của tôi gồm 4 tầng (layers):

  1. Tần Domain - Tầng nghiệp vụ.
  2. Tầng Application - Tầng ứng dụng.
  3. Tầng Presentation - Tầng trình diễn.
  4. Tầng Infrastructure - Tầng cơ sở (xin lỗi bạn đọc vì tôi không thể nghĩ ra một cái tên phù hợp hơn 😅).

Mũi tên màu đỏ ở hình trên cho thấy mối quan hệ phụ thuộc giữa các tầng:

Tầng bên ngoài luôn phụ thuộc vào tầng bên trong (điều ngược lại KHÔNG BAO GIỜ ĐƯỢC PHÉP XẢY RA)

Tại tầng nghiệp vụ, tôi sẽ thiết lập cho User Aggregate như sau:

import { Expose } from 'class-transformer';
import { BaseAggregate } from '../base';
import {
  DomainError,
  DomainErrorCode,
  DomainErrorDetailCode,
} from '@libs/exception/domain';

export class UserAggregate extends BaseAggregate {
  @Expose()
  private id?: string;

  @Expose()
  email: string;

  @Expose()
  private password: string;

  @Expose()
  private salt: string;

  @Expose()
  name?: string;

  getId(): string | null {
    return this.id ? this.id : null;
  }

  setId(value: string) {
    if (this.id) {
      throw new DomainError({
        code: DomainErrorCode.BAD_REQUEST,
        message: 'User already has an id',
        info: {
          errorCode: DomainErrorDetailCode.USER_AGGREGATE_ALREADY_HAS_ID,
        },
      });
    }

    this.id = value;
  }

  setEmail(value: string) {
    this.email = value;
  }

  setName(value: string) {
    this.name = value;
  }

  getPassword(): string {
    return this.password;
  }

  setPassword(value: string) {
    this.password = value;
  }

  getSalt() {
    return this.salt;
  }

  setSalt(value: string) {
    this.salt = value;
  }
}

Hiểu một cách đơn giản thì User Aggregate (Kết tập User) sẽ mô phỏng lại đặc điểm chung của các đối tượng User - giống như con người trong thực tế. User Aggregate bao gồm:

  • Thuộc tính.
  • Hành động (Methods).

Với tầng ứng dụng, tôi chia thành 2 phần:

  1. Command: cho việc xử lí CUD dữ liệu, ví dụ như:
  • Tạo user (Signup).
  • Update user.
  • Delete user.
  1. Query: cho việc xử lí lấy về dữ liệu, ví dụ như:
  • Lấy về thông tin của user.

Tầng này sẽ xử lí logic chính cho các APIs của ứng dụng

Bạn đọc có thể xem chi tiết hơn:

Tầng trình diễn là nơi thiết lập các API endpoints:

  • /v1/user/signup
  • /v1/user/signin
  • ...

Bạn đọc có thể xem thêm tại đây

Với tầng dưới cùng là tầng cơ sở.

Đây là tầng xử lí các tương tác với hệ thống ngoài:

  • API của hệ thống khác.
  • Database.
  • ...

Lấy ví dụ: khi tìm user bằng id, chúng ta cần truy cập vào database, repository đảm nhận việc này sẽ thực thiện các hành động:

  1. Kết nối tới database để thu thập dữ liệu.
  2. Tạo aggregate và trả về aggregate đó.

async findById(id: string): Promise<UserAggregate | null> {
    try {
      const repository = this.getRepository(UserEntity);

      const user = await this.getBaseQuery(repository)
        .addSelect(['user.name', 'user.salt'])
        .where({ id })
        .getOne();

      return user ? this.factory.createAggregate({ ...user }) : null;
    } catch (err) {
      console.error(err.stack);

      throw new InfrastructureError({
        code: InfrastructureErrorCode.INTERNAL_SERVER_ERROR,
        message: 'Internal Server Error',
      });
    }
  }

OK, đây chính là phần tổng quan về server application, bây giờ chúng ta sẽ chuyển qua phần infrastructure (AWS).

AWS Infrastructure

Tổng quan

1_7RyBwwMalB7Rb1JE20sqdA.jpeg

Hình phía trên mô tả tổng quan về infrastructure của ứng dụng, tôi sẽ chia nó ra thành 3 phần chính:

  • Networking (mạng).
  • RDB (cơ sở dữ liệu).
  • Server app.

Trong bài viết này, thay vì tập trung vào việc giải thích các khái niệm liên quan đến AWS resources (VPC, Subnets, ...) tôi sẽ chỉ tập trung vào cách tôi sử dụng và thiết lập chúng trong ứng dụng lần này.

Networking

Đây là nơi thiết lập:

  • VPC.
  • Public/ Private subnets.
  • Internet / NAT gateways.

Các resources này sẽ được kết hợp như hình mô tả dưới đây: 1_xFvp85ohI8vS_tIAsdBaag.jpeg

VPC là nơi đặt toàn bộ app và resources vào trong đó.

Public subnet - như cái tên của nó, đây là mạng con sẽ được public ra ngoài internet (có thể truy cập từ bên ngoài vào), với mục đích để:

  • Triển khai kết nối API từ client.
  • Thực hiện kết nối SSH từ dev đến database.

Private Subnet, đây là mạng con KHÔNG ĐƯỢC public ra ngoài (không thể truy cập từ bên ngoài), trong này ta sẽ đặt:

  • API instances.
  • Database instances.
  • ...

nói chung là các resources mang tính "nhạy cảm" cao.

Cuối cùng là NAT Gateway, đay chính là giải pháp giúp

Private subnet có thể kết nối ra bên ngoài nhưng bên ngoài KHÔNG THỂ đi vào Private subnet

RDB

Đây là phần thiết lập cho database. Do tôi sử dụng PostgreSQL cho database ở môi trường local nên tôi sẽ sử dụng aurora-postgresql trên môi trường AWS.

Vì đây là database nên chúng ta PHẢI ĐẶT NÓ VÀO TRONG PRIVATE SUBNET. 1_StcB6k_12spPqvqRBNFtEA.jpeg

Bạn đọc có thể tham khảo code chi tiết cho thiết lập RDB tại đây

Do developers cũng cần phải "đi vào bên trong" database để xem dữ liệu nên chúng ta cần tạo một tunnel cho họ. Cách làm của tôi đó là:

  1. Tạo một SSH Proxy Instance "đứng chắn phía trước" database.
  2. Tạo các SSH users tương ứng với các developers muốn truy cập vào database.
  3. Các developers sẽ sử dụng SSH để kết nối tới database thông qua SSH Proxy.

Cụ thể như sau: 1_ZCReCzemh95i2bnvnKnOGQ.jpeg

Tôi muốn bạn đọc chú ý vào phần tôi khoanh đỏ trong hình trên.

  • Proxy của chúng ta chỉ đơn thuần là một EC2 instance.
  • Security Group của RDS phải chấp nhận kết nối SSH từ Proxy instance.

Config cho security group sẽ như sau:

resource "aws_security_group" "proxy" {
  name        = "${var.app_name}-${var.env_name}-proxy-sg"
  description = "Allow ssh connect to db proxy"
  vpc_id      = var.vpc_id

  ingress {
    from_port   = 22
    to_port     = 22
    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"]
  }

  tags = {
    Name = "${var.app_name}-${var.env_name}-proxy-sg"
  }
}

Trong đoạn code trên, tôi thiết lập ingress (đầu vào) lắng nghe ở cổng 22 cho kết nối SSH.

Với RDS

Bạn đọc có thể tham khảo đoạn code dưới đây


resource "aws_security_group_rule" "proxy" {
  security_group_id = aws_security_group.this.id

  type                     = "ingress"
  from_port                = var.port
  to_port                  = var.port
  protocol                 = "tcp"
  source_security_group_id = var.proxy_security_group
  description              = "Access from proxy"
}

Server App

Trong phần này tôi sẽ trình bày về:

  • Load balancer.
  • EKS.
  • ECR.

Về Load balancer

Đây chính là "cổng" để clients có thể kết nối tới server app của chúng ta.

Tôi sẽ sử dụng Application Load Balancer vì tôi muốn load balancer không chỉ nhận request mà còn hoạt động như một reverse proxy để phân tải cho ứng dụng của mình.

Bạn đọc có thể tham khảo bài viết này để hiểu thêm về khái niệm reverse proxy.

EKS

Kubernetes service được cung cấp bởi AWS giúp quản lý các instances dưới dạng pods.

Cụ thể như sau: 1_SMfM6FVHgBvsotWy3rB8Fg.jpeg

EKS quản lí 2 pods, trong mỗi pod sẽ là Server App Docker Container, các container này sẽ kết nối trực tiếp tới database.

Bạn đọc có thể tham khảo thêm về EKS terraform tại đây

ECR

Là một Container Managed Service được cung cấp ởi AWS

Tôi sử dụng nó để quản lí Docker Image của ứng dụng. 1_ynraSaAJ43nySxyT6yoT-g.jpeg

Source code

Bạn đọc có thể xem full source code tại đây

Nói sơ qua về source code folders:

  • modules: đây là nơi tôi sẽ đặt thiết lập cho các AWS resources (VPC, subnets, RDS, EKS, ...), có thể coi đây như base environment.
  • environments: đúng như tên gọi, đây sẽ là folder tôi phân chia các môi trường khác nhau (staging, production).

1_dskM3CEh3anrxY4t8XBH-g.jpeg

Bạn đọc có thể xem chi tiết hơn về cách chạy và thực thi code tại file README

k8s & ArgoCD

Đây là phần nội dung chính của series. Trong bài viết lần này, tôi xin phép được trình bày với bạn đọc về:

  • Cấu trúc project k8s.
  • Cách tôi thiết lập và sử dụng ArgoCD.

Cấu trúc project k8s

Trong bài viết này, tôi sẽ không giải thích các định nghĩa như Pod hoặc Replica, ... mà thay vào đó, tôi sẽ trình bày với bạn đọc về cách tôi sử dụng các resources này cho dự án của mình.

1_vr2j-sQXgAUJ7ueSq9-osg.jpeg

Ở hình phía trên, các bạn có thể thấy 2 flows:

  • Server app flow.
  • Migration flow.

Server app flow

Với flow này, tôi sử dụng Deployment để quản lí ReplicaSet với 2 Pods (mỗi Pod sẽ có 1 docker container để chạy app).

Vì Deployment không có địa chỉ IP nên để truy cập đến Pod, tôi sử dụng Service.

Lúc này, chúng ta sẽ truy cập tới Pods thông qua Service.

Migration Flow

Với flow này, tôi sử dụng Job - với job, chúng ta có thể thực thi một tác vụ giống như việc migration cho database.

ArgoCD

GitOps

Theo như định nghĩa trong cuốn GitOps Cookbook

GitOps là một pương thức cho phép chúng ta sử dụng Git Repository như một Single Source Of Truth để triển khai infrastructure as code (IaC).

Trong ứng dụng của tôi

1_ahH4Zk1OaPVIQ9ShZ7CTxw.jpeg

ArgoCD sẽ theo dõi trạng thái mới nhất của k8s project và sau đó đồng bộ hoá (tự động hoặc bằng tay) tới ứng dụng để ứng dụng đạt trạng thái mới nhất.

Tôi sẽ trình bày kĩ hơn về vấn đề này trong bài viết tiếp theo.

Tổng kết

Hi vọng sau bài viết này, bạn đọc đã có thể nắm được tổng quan về ứng dụng mà tôi muốn xây dựng. Trong các bài viết tiếp theo tôi sẽ trình bày kĩ hơn với bạn đọc về cách thiết lập k8s và ArgoCD cho ứng dụng của mình.

Xin cảm ơn và hẹn gặp lại bạn đọc.

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í