Giới thiệu
Ở bài này chúng ta sẽ tìm hiểu cách làm thế nào để triển khai hệ thống Microservices trên môi trường Kubernetes.
Về hệ thống thì ta sẽ sử dụng Nodejs Molecular, đây là một Framework dùng để xây dựng ứng dụng với kiến trúc Microservices.
Kiến trúc của ứng dụng
API Gateway: đóng vai trò là ngõ vào của ứng dụng, nó sẽ tạo một HTTP Endpoint và hứng yêu cầu từ người dùng.
NATS: đóng vai trò là một trạm chuyển tiếp để cho các Service có thể giao tiếp được với nhau.
Categories Service và News Service: đây là Service thực hiện công việc CRUD cho các resource liên quan.
Redis: dùng để Cache kết quả lấy ra từ Database, giúp giảm số lần thực hiện truy vấn vào DB và tăng tốc độ của ứng dụng.
Database: nơi lưu trữ dữ liệu, ta sẽ xài PostgreSQL.
Ta đã xem sơ qua về kiến trúc của hệ thống Microservices, tiếp theo ta sẽ tiến hành viết cấu hình cho từng thành phần trên và triển khai nó lên Kubernetes.
Xây dựng Container Image
Các bạn có thể sử dụng Image mình đã xây dựng sẵn tên là 080196/microservice
, hoặc nếu các bạn thích tự xây dựng Image riêng thì các bạn tải source code từ đây xuống NodeJS Microservices.
Sau đó các bạn nhảy vào thư mục microservices/code
và thực hiện xây dựng Image với tên Image sẽ là <docker-hub-username>/microservice
, tiếp theo các bạn đẩy Image lên Docker Hub, ví dụ:
git clone https://github.com/hoalongnatsu/microservices.git && cd microservices/code
docker build . -t 080196/microservice
docker push 080196/microservice
Tiếp theo ta tiến hành viết tệp tin cấu hình cho ứng dụng.
Triển khai API Gateway
Đầu tiên ta sẽ viết tệp tin cấu hình cho API Gateway, ta dùng Deployment để triển khai API Gateway, tạo tệp tin tên là api-gateway-deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
labels:
component: api-gateway
spec:
revisionHistoryLimit: 1
selector:
matchLabels:
component: api-gateway
template:
metadata:
labels:
component: api-gateway
spec:
containers:
- name: api-gateway
image: 080196/microservice
ports:
- name: http
containerPort: 3000
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
env:
- name: NODE_ENV
value: testing
- name: SERVICEDIR
value: dist/services
- name: SERVICES
value: api
- name: PORT
value: "3000"
- name: CACHER
value: redis://redis:6379
Trong Image 080196/microservice
chúng ta đã xây dựng ở trên, có 3 Service ở trong Image đó là api
, categories
, news
. Ta chọn Service ta cần chạy bằng cách truyền tên của Service thông qua biến môi trường SERVICES
, ở tệp tin cấu hình trên ta chạy API Gateway nên ta truyền vào giá trị là api
.
Các bạn nhìn vào phần code ở tệp tin code/services/api.service.ts
, ta thấy ở chỗ cấu hình cho API Gateway ở dòng 15.
...
settings: {
port: process.env.PORT || 3001,
...
Biến môi trường PORT dùng để chọn port
của API Gateway, ở trên ta truyền vào giá trị là 3000. Biến môi trường CACHER
dùng để khai báo Redis Host.
Ta tạo Deployment.
$ kubectl apply -f api-gateway-deployment.yaml
deployment.apps/api-gateway created
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
api-gateway 0/1 1 0 100s
Ta đã tạo được API Gateway, nhưng khi bạn liệt kê Pod thì bạn sẽ thấy nó chạy không thành công.
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
api-gateway-79688cf6f5-g88f2 0/1 Running 2 93s
Ta kiểm tra Pod để xem lý do.
$ kubectl logs api-gateway-79688cf6f5-g88f2
...
[2021-11-07T14:53:37.449Z] ERROR api-gateway-79688cf6f5-g88f2-28/CACHER: Error: getaddrinfo EAI_AGAIN redis
at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:60:26) {
errno: 'EAI_AGAIN',
code: 'EAI_AGAIN',
syscall: 'getaddrinfo',
hostname: 'redis'
}
Lỗi được hiển thị ở đây là do ta không thể kết nối được tới Redis, vì ta chưa tạo Redis.
Triển khai Redis
Tiếp theo ta sẽ tạo Redis, tạo một tệp tin tên là redis-deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
labels:
component: redis
spec:
strategy:
type: Recreate
selector:
matchLabels:
component: redis
template:
metadata:
labels:
component: redis
spec:
containers:
- name: redis
image: redis
ports:
- containerPort: 6379
$ kubectl apply -f redis-deployment.yaml
deployment.apps/redis created
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
api-gateway 0/1 1 0 16m
redis 1/1 1 1 14s
Vậy là ta đã tạo được Redis Pod, tiếp theo nếu muốn kết nối được tới Redis Pod này thì ta cần phải tạo Kubernetes Service cho nó. Tạo tệp tin tên là redis-service.yaml
:
apiVersion: v1
kind: Service
metadata:
name: redis
labels:
component: redis
spec:
selector:
component: redis
ports:
- port: 6379
Ta tạo Kubernetes Service:
$ kubectl apply -f redis-service.yaml
service/redis created
Khởi động lại API Gateway Deployment.
$ kubectl rollout restart deploy api-gateway
deployment.apps/api-gateway restarted
Bây giờ khi ta liệt kê Pod, ta vẫn thấy nó vẫn chưa chạy được thành công.
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
api-gateway-79688cf6f5-g88f2 0/1 CrashLoopBackOff 8 13m
api-gateway-7f4d5f54f-lzgkd 0/1 Running 2 82s
redis-58c4799ccc-qhv2z 1/1 Running 0 5m41s
kiểm tra để xem tại sao.
$ kubectl logs api-gateway-7f4d5f54f-lzgkd
...
[2021-11-07T15:05:10.388Z] INFO api-gateway-7f4d5f54f-lzgkd-28/CACHER: Redis cacher connected.
Sequelize CLI [Node: 12.13.0, CLI: 6.2.0, ORM: 6.6.5]
Loaded configuration file "migrate/config.js".
Using environment "testing".
ERROR: connect ECONNREFUSED 127.0.0.1:5432
Error: Command failed: sequelize-cli db:migrate
ERROR: connect ECONNREFUSED 127.0.0.1:5432
at ChildProcess.exithandler (child_process.js:295:12)
at ChildProcess.emit (events.js:210:5)
at maybeClose (internal/child_process.js:1021:16)
at Process.ChildProcess._handle.onexit (internal/child_process.js:283:5) {
killed: false,
code: 1,
signal: null,
cmd: 'sequelize-cli db:migrate'
}
Sequelize CLI [Node: 12.13.0, CLI: 6.2.0, ORM: 6.6.5]
Loaded configuration file "migrate/config.js".
Using environment "testing".
ERROR: connect ECONNREFUSED 127.0.0.1:5432
...
API Gateway đã kết nối tới Redis thành công, tuy nhiên bây giờ thì API Gateway bị lỗi là do không thể kết nối được tới Database.
Triển khai Database
Bước tiếp theo là ta sẽ tạo Database, để triển khai Database ta sẽ không dùng Deployment mà sẽ dùng StatefulSet. Ta tạo tệp tin tên là postgres-statefulset.yaml
:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
labels:
component: postgres
spec:
selector:
matchLabels:
component: postgres
serviceName: postgres
template:
metadata:
labels:
component: postgres
spec:
containers:
- name: postgres
image: postgres:11
ports:
- containerPort: 5432
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres-data
env:
- name: POSTGRES_DB
value: postgres
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_PASSWORD
value: postgres
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: hostpath
resources:
requests:
storage: 5Gi
Lưu ý tên của storageClassName
tùy thuộc vào Kubernetes Cluster của ta, để kiểm tra StorageClass thì ta gõ câu lệnh kubectl get sc
.
Ta tạo StatefulSet:
$ kubectl apply -f postgres-statefulset.yaml
statefulset.apps/postgres created
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
api-gateway-79688cf6f5-g88f2 0/1 Running 16 32m
api-gateway-7f4d5f54f-lzgkd 0/1 CrashLoopBackOff 11 20m
postgres-0 1/1 Running 0 55s
redis-58c4799ccc-qhv2z 1/1 Running 0 25m
Để kết nối được tới DB thì ta cần tạo Kubernetes Service cho nó, tạo tệp tin tên là postgres-service.yaml
:
apiVersion: v1
kind: Service
metadata:
name: postgres
labels:
component: postgres
spec:
selector:
component: postgres
ports:
- port: 5432
Ta tạo Kubernetes Service:
$ kubectl apply -f postgres-service.yaml
service/postgres created
Ta cần cập lại cấu hình của API Gateway Deployment để nó có thể kết nối được tới DB. Các bạn xem ở trong tệp tin code/src/db/connect.ts
thì sẽ thấy các biến môi trường mà API Gateway dùng để kết nối tới DB.
import { Op, Sequelize } from "sequelize";
export const connect = () => new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
port: +process.env.DB_PORT,
operatorsAliases: {
$like: Op.like,
$nlike: Op.notLike,
$eq: Op.eq,
$ne: Op.ne,
$in: Op.in,
$nin: Op.notIn,
$gt: Op.gt,
$gte: Op.gte,
$lt: Op.lt,
$lte: Op.lte,
$bet: Op.between,
$contains: Op.contains,
},
dialect: "postgres",
logging: (process.env.ENABLE_LOG_QUERY === "true"),
}
);
Cập nhật các biến env
của tệp tin api-gateway-deployment.yaml
và tạo lại API Gateway.
...
env:
- name: NODE_ENV
value: testing
- name: SERVICEDIR
value: dist/services
- name: SERVICES
value: api
- name: PORT
value: "3000"
- name: CACHER
value: redis://redis:6379
- name: DB_HOST
value: postgres
- name: DB_PORT
value: "5432"
- name: DB_NAME
value: postgres
- name: DB_USER
value: postgres
- name: DB_PASSWORD
value: postgres
$ kubectl apply -f api-gateway-deployment.yaml
deployment.apps/api-gateway configured
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
api-gateway-544c7f84-6hv7z 1/1 Running 0 2m4s
nats-65687968fc-2drwp 1/1 Running 0 4m23s
postgres-0 1/1 Running 0 31m
redis-58c4799ccc-qhv2z 1/1 Running 0 56m
Bây giờ khi ta liệt kê Pod thì ta thấy API Gateway đã chạy thành công.
Triển khai Categories Service và News Service
Tiếp theo ta sẽ triển khai hai Services còn lại của Micoservices, tạo tệp tin tên là categories-news-deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: categories-service
labels:
component: categories-service
spec:
revisionHistoryLimit: 1
selector:
matchLabels:
component: categories-service
template:
metadata:
labels:
component: categories-service
spec:
containers:
- name: categories-service
image: 080196/microservice
env:
- name: NODE_ENV
value: testing
- name: SERVICEDIR
value: dist/services
- name: SERVICES
value: categories
- name: CACHER
value: redis://redis:6379
- name: DB_HOST
value: postgres
- name: DB_PORT
value: "5432"
- name: DB_NAME
value: postgres
- name: DB_USER
value: postgres
- name: DB_PASSWORD
value: postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: news-service
labels:
component: news-service
spec:
revisionHistoryLimit: 1
selector:
matchLabels:
component: news-service
template:
metadata:
labels:
component: news-service
spec:
containers:
- name: news-service
image: 080196/microservice
env:
- name: NODE_ENV
value: testing
- name: SERVICEDIR
value: dist/services
- name: SERVICES
value: news
- name: CACHER
value: redis://redis:6379
- name: DB_HOST
value: postgres
- name: DB_PORT
value: "5432"
- name: DB_NAME
value: postgres
- name: DB_USER
value: postgres
- name: DB_PASSWORD
value: postgres
$ kubectl apply -f categories-news-deployment.yaml
deployment.apps/categories-service created
deployment.apps/news-service created
Sau khi tạo hai Service này xong thì để chúng và API Gateway có thể giao tiếp với nhau, ta cần tạo NATS.
Triển khai NATS
Tạo một tệp tin tên là nats-deployment.yaml
với cấu hình như sau:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nats
labels:
component: nats
spec:
strategy:
type: Recreate
selector:
matchLabels:
component: nats
template:
metadata:
labels:
component: nats
spec:
containers:
- name: nats
image: nats
ports:
- containerPort: 4222
$ kubectl apply -f nats-deployment.yaml
deployment/nats created
Tiếp theo ta tạo Kubernetes Service cho NATS, tạo tệp tin nats-service.yaml
:
apiVersion: v1
kind: Service
metadata:
name: nats
labels:
component: nats
spec:
selector:
component: nats
ports:
- port: 4222
$ kubectl apply -f nats-service.yaml
service/nats created
Bước cuối cùng ta cập nhật lại env
của API Gateway và Categories Service với News Service. Thêm vào env
các giá trị sau để chỉ định TRANSPORTER
cho các Service:
...
env:
...
- name: TRANSPORTER
value: nats://nats:4222
Cập nhật lại toàn bộ Deployment.
$ kubectl apply -f kubectl apply -f api-gateway-deployment.yaml
deployment.apps/api-gateway configured
$ kubectl apply -f categories-news-deployment.yaml
deployment.apps/categories-service configured
deployment.apps/news-service configured
Ta liệt kê Pod và kiểm tra xem các Service đã có thể giao tiếp với nhau được chưa:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
api-gateway-6cb4c6b657-tlkzq 1/1 Running 0 36s
categories-service-689cdb6c6d-gqtlb 1/1 Running 0 20s
nats-65687968fc-2drwp 1/1 Running 0 20m
news-service-6b85f99987-dcplv 1/1 Running 0 20s
postgres-0 1/1 Running 0 48m
redis-58c4799ccc-qhv2z 1/1 Running 0 72m
$ kubectl logs api-gateway-6cb4c6b657-tlkzq
...
[2021-11-07T16:14:54.181Z] INFO api-gateway-6cb4c6b657-tlkzq-28/REGISTRY: Node 'news-service-6b85f99987-vcjzn-28' connected.
...
[2021-11-07T16:14:57.357Z] INFO api-gateway-6cb4c6b657-tlkzq-28/REGISTRY: Node 'categories-service-689cdb6c6d-gjjjr-29' connected.
...
Bạn sẽ thấy logs
là News Service và Categories Service đã được kết nối tới API Gateway. Vậy là ứng dụng của ta đã chạy thành công.
Nhưng ta để ý thấy giá trị env
ta khai báo có hơi dài và lập lại ở các tệp tin Deployment không? Ta có thể dùng ConfigMap để khai báo cấu hình ở chỗ và sử dụng lại cho nhiều nơi, giúp tệp tin cấu hình của ta gọn hơn.
Khai báo cấu hình chung
Tạo một tệp tin tên là microservice-cm.yaml
với cấu hình như sau:
apiVersion: v1
kind: ConfigMap
metadata:
name: microservice-cm
labels:
component: microservice-cm
data:
NODE_ENV: testing
SERVICEDIR: dist/services
TRANSPORTER: nats://nats:4222
CACHER: redis://redis:6379
DB_NAME: postgres
DB_HOST: postgres
DB_USER: postgres
DB_PASSWORD: postgres
DB_PORT: "5432"
$ kubectl apply -f microservice-cm.yaml
configmap/microservice-cm created
Ta cập nhật lại cấu hình của các tệp Deployment như sau, tệp tin api-gateway-deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
labels:
component: api-gateway
spec:
revisionHistoryLimit: 1
selector:
matchLabels:
component: api-gateway
template:
metadata:
labels:
component: api-gateway
spec:
containers:
- name: api-gateway
image: 080196/microservice
ports:
- name: http
containerPort: 3000
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
env:
- name: SERVICES
value: api
- name: PORT
value: "3000"
envFrom:
- configMapRef:
name: microservice-cm
$ kubectl apply -f api-gateway-deployment.yaml
deployment.apps/api-gateway configured
Tệp tin categories-news-deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: categories-service
labels:
component: categories-service
spec:
revisionHistoryLimit: 1
selector:
matchLabels:
component: categories-service
template:
metadata:
labels:
component: categories-service
spec:
containers:
- name: categories-service
image: 080196/microservice
env:
- name: SERVICES
value: categories
envFrom:
- configMapRef:
name: microservice-cm
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: news-service
labels:
component: news-service
spec:
revisionHistoryLimit: 1
selector:
matchLabels:
component: news-service
template:
metadata:
labels:
component: news-service
spec:
containers:
- name: news-service
image: 080196/microservice
env:
- name: SERVICES
value: news
envFrom:
- configMapRef:
name: microservice-cm
$ kubectl apply -f categories-news-deployment.yaml
deployment.apps/categories-service configured
deployment.apps/news-service configured
Ta liệt kê Pod và kiểm tra mọi thứ có ổn không:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
api-gateway-86b67895fd-cphmv 1/1 Running 0 79s
categories-service-84c74cd87c-zjtd2 1/1 Running 0 53s
nats-65687968fc-2drwp 1/1 Running 0 41m
news-service-69f45b8668-kv9dm 1/1 Running 0 52s
postgres-0 1/1 Running 0 69m
redis-58c4799ccc-qhv2z 1/1 Running 0 93m
Tất cả các thành phần của ứng dụng ta vẫn chạy bình thường. Ta sử dụng ConfigMap để giúp tệp tin cấu hình của ta nhìn gọn hơn và dễ dàng cập nhật.
Kết luận
Vậy là ta đã triển khai được hệ thống Microservice lên môi trường Kubernetes thành công, như các bạn thấy thì không khó lắm, chỉ cần ta triển khai những thành phần trong hệ thống theo từng bước là ta sẽ triển khai được.
Tác giả @Quân Huỳnh
Nếu bài viết có gì sai hoặc cần cập nhật thì liên hệ Admin.
Tham gia nhóm chat của DevOps VN tại Telegram.
Kém tiếng Anh và cần nâng cao trình độ giao tiếp: Tại sao bạn học không hiệu quả?