👋👋👋 Hello hello, xin chào tất cả anh em. Anh em nào đã lỡ vào đây rồi thì comment chào nhau một cái nhé cho đông vui nhé!
Trong quá trình thực hiện GitOps, có những dữ liệu nhạy cảm như: TLS Certificate, Private Key, API Key... được lưu trữ dưới dạng Kubernetes Secret và bắt buộc phải có khi deploy ứng dụng. Việc lưu trữ các dữ liệu nhạy cảm ngay trong Git Repository khi thực hiện GitOps là một hành động CỰC KỲ NGUY HIỂM VÀ RỦI RO. Chúng ta không bao giờ commit trực tiếp các dữ liệu nhạy cảm vào trong Git. Đó là bài học vỡ lòng khi dùng Git mà ai cũng biết!
Vậy phải làm sao để lưu trữ các dữ liệu nhạy cảm này một cách an toàn khi tự động hóa với ArgoCD? 🤔 Vấn đề này quay về bài toán làm sao để quản lý Secrets khi thực hiện GitOps. Bài viết này sẽ tập trung vào vấn đề này nhé mọi người!
Secret Management
Thực sự thì việc quản lý các dữ liệu nhay cảm trên là vấn đề khá đau đầu vì vừa phải làm sao để đảm ảo an toàn, vừa phải dễ dàng sử dụng cho cả người và máy khi triển khai GitOps. Mình cũng từng mất khá nhiều thời gian để tìm hiểu về các giải pháp khi sử dụng ArgoCD, FluxCD.
Đối với người em FluxCD thì encryption là tính năng được tích hợp sẵn, việc sử dụng dễ dàng và đơn giản hơn so với ArgoCD. Còn với ArgoCD, sau khi tìm hiểu mình thấy rằng sẽ có hai hướng để thực hiện:
- Dữ liệu nhạy cảm được cung cấp sẵn trong K8s cluster dưới dạng Kubernetes Secret hoặc CRDs thông qua một số tools như: Sealed Secrets, External Secrets Operator để dùng với HashCorp Vault, Azure Key Vault, .etc. Đây có lẽ các cách an toàn hơn cả và được recommend vì trách nghiệm quản lý Secret được chuyển ra một thành phần bên ngoài ArgoCD. ArgoCD không quản lý và cũng không được đọc các dữ liệu nhạy cảm này.
- Dữ liệu nhạy cảm được mã hóa rồi commit vào Git. ArgoCD được cấp thêm quyền và chịu trách nhiệm giải mã thông qua các công cụ hoặc plugins cài thêm. Thêm trách nhiệm đồng nghĩa là tăng thêm mức độ nghiêm trọng nếu ArgoCD bị xâm phạm.
Các công cụ có thể sử dụng để triển khai có thể kể đến:
- Bitnami Sealed Secrets: Một controller của nhà Bitnami, cho phép mã hóa dữ liệu nhạy cảm và lưu thành CRD loại SealedSecret. Controller sẽ tự động decrypt và generate ra Secret tương ứng. Quản nó Secret độc lập với ArgoCD.
- External Secrets Operator: Đây là một operator, cho phép kéo dữ liệu từ nhiều nguồn khác nhau về k8s cluster. Hoạt động cũng gần giống với SealedSecrets.
- ArgoCD Vault Plugin: Đây là plugin được viết dành cho ArgoCD. Nó cũng hỗ trợ nhiều loại backend khác nhau.
- Helm Secrets Plugin: Plugin dành cho Helm, nó sử dụng SOPS để giải mã file values đã bị mã hóa.
- KSOPS Plugin (Kustomize plugin for SOPS): Plugin dành cho Kustomize, nó sử dụng SOPS để giải mã các file đã bị mã hóa. Chủ yếu là dùng với tính năng
secretGenerator
.
Sử dụng SOPS với ArgoCD
Nhu cầu sử dụng
Do chưa có nhiều thời gian để ngồi viết hết về các công cụ kể trên nên trong bài viết này mình sẽ chỉ chia sẻ về case-study dưới đây. Nó đáp ứng được các nhu cầu cơ bản sau của mình:
- Hầu hết ứng dụng được deploy bằng Helm nên mình cần một giải pháp để có thể mã hóa các file values chứa dữ liệu nhạy cảm.
- Một số ứng dụng cần deploy bằng Kustomize nên mình cũng cần mã hóa các file cấu hình chứa dữ liệu nhạy cảm khi dùng
secretGenerator
. - Mình sử dụng VSCode nên cũng cần giải pháp để thuận tiện trong quá trình xem nội dung các file đã mã hóa.
- Các công cụ thích hợp cần đơn giản, dễ dùng, vẫn có thể tích hợp với các dịch vụ phổ biến gồm: HashiCorp Vault, Azure Key Vault, AWS Secret Manager.
Cài đặt các công cụ
Các tool cần thiết để cài dưới local gồm:
- SOPS v3.10.2: https://github.com/getsops/sops
- age v1.2.1: https://github.com/FiloSottile/age
- SOPS easy edit: https://marketplace.visualstudio.com/items?itemName=ShipitSmarter.sops-edit
- Helm Secrets Plugin v4.6.5: https://github.com/jkroepke/helm-secrets (optional)
- KSOPS plugin v4.3.3: https://github.com/viaduct-ai/kustomize-sops (optional)
Cách thức triển khai
SOPS là công cụ mã hóa mới, linh hoạt và được sử dụng nhiều trong thời gian gần đây. Trong bài viết này mình sẽ sử dụng combo gồm SOPS
và age
.
Nó cho phép mã hóa file bằng nhiều public-key. Với bất kỳ private-key khớp với một trong số public-key khi mã hóa đều có thể giải mã dữ liệu. Điều này giúp mỗi thành viên trong team đều có thể giải mã bằng SOPS với private-key của chính họ chứ không phải dùng chung key.
Cụ thể mình sẽ cài thêm Helm Secrets Plugin + KSOPS Plugin vào container chạy ArgoCD. Cả hai plugin này đều sử dụng SOPS để mã hóa và giải mã nên có thể dùng cả với các dịch vụ cloud kể trên. Sửa lại config ArgoCD để nó dùng hai plugin mới cài. Qua đó, các file đã mã hóa trong Git sẽ được ArgoCD giải mã ra dữ liệu ban đầu.
Để việc mã hóa và giải mã được thuận tiện khi dùng VSCode, mình sử dụng thêm extension SOPS easy edit giúp tự động decrypt file và mã hóa file.
Setup SOPS + age
Bước số 1: Cài đặt tools
Việc mã hóa các file nhạy cảm cần phải do con người thực hiện nền chúng ta cần cài đặt sops
và age
trên máy local thông qua Homebrew:
brew install sops age
Bước số 2: Tạo encryption keys
Tiếp theo, mình sẽ generate ra hai cặp encryption key gồm:
argocd.agekey
: Sẽ được lưu trên K8s để cho ArgoCD sử dụng (máy dùng).human.agekey
: Sẽ được lưu dưới local để cho mình dùng (người dùng).
Sử dụng age
, mỗi cặp gồm 1 public key và 1 private key. Dùng lệnh age-keygen
với flag -o
để output ra file:
age-keygen -o argocd.agekey
age-keygen -o human.agekey
Tên file các bạn tự đặt nhé
human.agekey
hayargocd.agekey
hay gì cũng được.
Output trên terminal sẽ hiển ra public key, còn file *.agekey
sẽ chứa cả private key và public key:
Public key: age1ly94f4ya36ed6ep2llg4s8pfjzwjr2t06hgyl907hh696kvmx5rqhmh7jc
# created: 2025-06-12T05:06:30+07:00
# public key: age1ly94f4ya36ed6ep2llg4s8pfjzwjr2t06hgyl907hh696kvmx5rqhmh7jc
AGE-SECRET-KEY-132L68Y2FVM3V7KDVJRPSGW9C3N4PCZVHVVUMXMAWL8HEMV7EPNQS0CX56Z
Bước số 3: Lưu encryption key cho local
Tiếp theo, cần copy lại nội dung trong file human.agekey
bạn dùng dưới local và lưu lại vào file theo đúng đường dẫn sau để cho sops
sử dụng:
- Linux:
~/.config/sops/age/keys.txt
- MacOS:
~/Library/Application\ Support/sops/age/keys.txt
Bước số 4: Lưu encryption key cho server
Lưu key trong file argocd.agekey
lên K8s cluster dưới dạng Secret để cho ArgoCD sử dụng:
kubectl -n argocd create secret generic sops-age \ --from-file age.agekey=argocd.agekey
NAME TYPE DATA AGE
sops-age Opaque 1 19d
apiVersion: v1
data: age.agekey: xxxx
kind: Secret
metadata: name: sops-age namespace: argocd
type: Opaque
Bước số 5: Tạo file .sops.yaml
Tạo file cấu hình .sops.yaml
, chúng ta cần lưu trong GitOps repository với nội dung theo theo mẫu sau:
## Recipients
##
## argocd1: age1ly94f4ya36ed6ep2llg4s8pfjzwjr2t06hgyl907hh696kvmx5rqhmh7jc
## argocd2: age1ly94f4ya36ed6ep2llg4s8pfjzwjr2t06hgyl907hh696kvmx5rqhmh7j2
## kimnh1: age1edhywhcjjnuy5yvfzy6lgf2x763ntv0m9thp3qjqr0dln07qfshqsh8hn1
## kimnh2: age1edhywhcjjnuy5yvfzy6lgf2x763ntv0m9thp3qjqr0dln07qfshqsh8hn2
creation_rules: - path_regex: 'production.*\.enc\.yaml$' age: >- age1ly94f4ya36ed6ep2llg4s8pfjzwjr2t06hgyl907hh696kvmx5rqhmh7jc, age1edhywhcjjnuy5yvfzy6lgf2x763ntv0m9thp3qjqr0dln07qfshqsh8hn1 - path_regex: 'staging.*\.enc\.yaml$' age: >- age1ly94f4ya36ed6ep2llg4s8pfjzwjr2t06hgyl907hh696kvmx5rqhmh7j2, age1edhywhcjjnuy5yvfzy6lgf2x763ntv0m9thp3qjqr0dln07qfshqsh8hn2
File cấu hình trên chỉ dẫn cho sops
:
- Chỉ các file có đuôi
.enc.yaml
sẽ áp dụngsops
- Nếu file
.enc.yaml
nằm trong thư mụcproduction
hoặc tên file có từ khóaproduction
thì dùng hai public keyargocd1
vàkimnh1
. - Nếu file
.enc.yaml
nằm trong thư mụcstaging
hoặc tên file có từ khóastaging
thì dùng hai public keyargocd2
vàkimnh2
.
Chú ý thay thế key tương ứng vào nhé. Nếu bạn gặp khó khăn gì trong việc thiết lập cấu hình
.sops.yaml
thì comment ở cuối bài viết nhé.
Bước số 6: Cài đặt VSCode extension
SOPS easy edit: https://marketplace.visualstudio.com/items?itemName=ShipitSmarter.sops-edit
Bước số 7: Thử nghiệm mã hóa file
Mình tạo thử một file có tên production.values.enc.yaml
, cấu hình file .sops.yaml
đúng và khớp nên vscode extension xuất hiện nút Encrypt để mình mã hóa. Khi mở file thì cũng tự giải mã luôn.
Nếu thấy nó hoạt động đúng theo flow trên là phần cấu hình SOPS cơ bản đã hoàn tất rồi. Giờ chỉ tích hợp để hoạt động trên ArgoCD server nữa thôi.
Bước số 8: Setup .gitignore
Các bạn cần sửa file .gitignore
để tránh việc thêm nhầm các file raw vào trong Git. Do VSCode extension sẽ tự động decrypt và tạo file có suffix là tmp
nên chúng ta không tracking những file đó nha:
charts
*tmp*
*.agekey
Bước số 9: Git Differ
Khi bạn sử dụng SOPS để encrypt thì các file trong Git đều đã được mã hóa. Việc review code và khi xem Git commits sẽ gặp khó khăn. Làm sao để bạn biết commit đó đã sửa đổi chỗ nào trong các file bị mã hóa.
Với thâm niên nhiều năm sử dụng GitOps với SOPS, mình cũng đã từng phải xử lý vấn đề này và dưới đây là cách khắc phục. Bạn chỉ cần chạy thêm 1 lệnh duy nhất sau:
git config diff.sopsdiffer.textconv "sops -d --config /dev/null"
Lệnh này có tác dụng cấu hình Git để sử dụng sops
như một công cụ textconv
cho các file được xử lý bằng sops
khi thực hiện các lệnh git diff
.
Kết quả là các bạn có thể dùng git diff
với các file đã encrypt qua đó theo dõi được chi tiết các vị trí sửa đổi như thế này:
Cài đặt plugins cho ArgoCD
Bước 1: Custom Docker Image
Có hai cách để cài đặt Helm Secrets plugin và KSOPS plugin cho ArgoCD đó là dùng sidecar container hoặc Custom Docker image.
Trong bài viết này, mình sẽ sử dụng cách thứ hai - dùng Custom Docker Image. Các bạn có thể dùng image được mình build sẵn sau:
ghcr.io/teguru-labs/argocd:v3.0.2-build.7
Hoặc tự build lại theo Dockerfile
của mình dưới đây:
ARG ARGOCD_VERSION="3.0.2"
ARG KSOPS_VERSION="4.3.3" FROM viaductoss/ksops:v$KSOPS_VERSION AS ksops-builder FROM quay.io/argoproj/argocd:v$ARGOCD_VERSION ARG SOPS_VERSION=3.10.2
ARG KUBECTL_VERSION=1.33.0
ARG VALS_VERSION=0.41.1
ARG HELM_SECRETS_VERSION=4.6.5 USER root
RUN apt-get update && \ apt-get install -y \ wget && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ mkdir -p /gitops-tools/helm-plugins # Override the default kustomize executable with the Go built version
COPY /usr/local/bin/kustomize /usr/local/bin/kustomize
COPY /usr/local/bin/ksops /usr/local/bin/ksops RUN \ GO_ARCH=$(uname -m | sed -e 's/x86_64/amd64/') && \ wget -qO "/gitops-tools/curl" "https://github.com/moparisthebest/static-curl/releases/latest/download/curl-${GO_ARCH}" && \ true RUN \ GO_ARCH=$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/') && \ wget -qO "/gitops-tools/kubectl" "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/${GO_ARCH}/kubectl" && \ true # sops backend installation (optional)
RUN \ GO_ARCH=$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/') && \ wget -qO "/gitops-tools/sops" "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.${GO_ARCH}" && \ true # vals backend installation (optional)
RUN \ GO_ARCH=$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/') && \ wget -qO- "https://github.com/helmfile/vals/releases/download/v${VALS_VERSION}/vals_${VALS_VERSION}_linux_${GO_ARCH}.tar.gz" | tar zxv -C /gitops-tools vals && \ true # helm secert installation
RUN \ wget -qO- "https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/helm-secrets.tar.gz" | tar -C /gitops-tools/helm-plugins -xzf- && \ true RUN chmod +x /gitops-tools/* && ln -sf /gitops-tools/helm-plugins/helm-secrets/scripts/wrapper/helm.sh /usr/local/sbin/helm # Set the environment variables for helm secrets
ENV HELM_PLUGINS=/gitops-tools/helm-plugins/ \ HELM_SECRETS_CURL_PATH=/gitops-tools/curl \ HELM_SECRETS_SOPS_PATH=/gitops-tools/sops \ HELM_SECRETS_VALS_PATH=/gitops-tools/vals \ HELM_SECRETS_KUBECTL_PATH=/gitops-tools/kubectl \ HELM_SECRETS_BACKEND="sops" \ HELM_SECRETS_VALUES_ALLOW_SYMLINKS=false \ HELM_SECRETS_VALUES_ALLOW_ABSOLUTE_PATH=true \ HELM_SECRETS_VALUES_ALLOW_PATH_TRAVERSAL=false \ HELM_SECRETS_WRAPPER_ENABLED=true \ HELM_SECRETS_DECRYPT_SECRETS_IN_TMP_DIR=true \ HELM_SECRETS_HELM_PATH=/usr/local/bin/helm \ PATH="$PATH:/gitops-tools" USER argocd
Image trên cũng chỉ đơn giản là tải thêm các tool gồm sops, helm-secrets plugin và ksops plugin vào mà thôi. Cũng không có gì quá đặc biệt.
Bước 2: Deploy ArgoCD Repo Server với custom image
Mình sẽ sửa lại Helm values của ArgoCD cho service repo-server
như dưới đây:
- Đổi container image sang dùng custom image
- Mount file
age.agekey
trong Secretsops-age
đã tạo bên trên vào trong container - Chỉ định uid thông qua
runAsUser: 999
## Repo Server
repoServer: ## Repo server image image: repository: ghcr.io/teguru-labs/argocd tag: v3.0.2-build.7 # -- Environment variables to pass to repo server env: - name: SOPS_AGE_KEY_FILE value: /sops/age.agekey # -- Additional volumeMounts to the repo server main container volumeMounts: - mountPath: /sops name: sops-age # -- Additional volumes to the repo server pod volumes: # age-keygen -o age.agekey # kubectl -n argocd create secret generic sops-age --from-file=age.agekey - name: sops-age secret: secretName: sops-age # -- Repo server container-level security context containerSecurityContext: runAsNonRoot: true runAsUser: 999
Tới đây deploy lại ArgoCD là có thể dụng được sops
với helm
rồi. Còn với ksops
thì để dùng được, chúng ta phải sửa thêm config xíu.
Bước 3: Tích hợp ksops với ArgoCD
Mình sẽ sửa helm values như sau để bố sung một số build options cho kustomize thì ksops mới dùng được:
## Argo Configs
configs: # General Argo CD configuration. Any values you put under `.configs.cm` are passed to argocd-cm ConfigMap. ## Ref: https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-cm.yaml cm: # Build options/parameters to use with `kustomize build` (optional) kustomize.buildOptions: --enable-alpha-plugins --enable-exec
Như vậy là xong rồi! Mình sẽ deploy thử với cả Kustomize và Helm nhé.
Deploy dùng Helm + SOPS như nào?
Đối với Helm, nếu trong file values.yaml
có dữ liệu nhạy cảm thì các bạn chỉ cần đặt lại tên theo đúng quy tắc trong .sops.yaml
là được nhé. Ví dụ: production.values.enc.yaml
thì trong YAML của Application cũng chỉ định values file từ values.yaml
thành production.values.enc.yaml
. Khi deploy Helm Secrets Plugin sẽ hoạt động và tự xử lý tiếp.
project: shope
destination: namespace: shope name: in-cluster
syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true - ApplyOutOfSyncOnly=true - ServerSideApply=true
ignoreDifferences: - group: argoproj.io kind: Application jsonPointers: - /status
sources: - repoURL: https://charts.teguru.com targetRevision: 1.0.1 helm: valueFiles: - $values/apps/shope/shope-api/production.values.enc.yaml releaseName: shope-api chart: instant-chart - repoURL: https://github.com/sun-devops/atlas-demo-infra targetRevision: main ref: values
Deploy dùng Kustomize + SOPS như nào?
Bước 1: Chuyển sang dùng secretGenerator
Đối với Kustomize thì sẽ có chút khác biệt. Thay vì bạn tạo file manifest cho Secret trực tiếp thì bây giờ sẽ cần chuyển sang dùng tính năng secretGenerator
để kustomize tự tạo ra Secret.
Ví dụ, để tạo ra Secret với nội dung:
apiVersion: v1
kind: Secret
metadata: name: app-config namespace: my-app
data: config.yaml: xxxx
Mình sẽ tạo file config.enc.yaml
với nội dung đã được mã hóa bằng sops
. Sau đó tạo kustomization như sau:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: my-app resources: - ../base secretGenerator: - name: app-config files: - config.yaml=config.enc.yaml
Bước 2: Xử lý tên Secret có suffix ngẫu nhiên khi dùng Kustomize
Sau khi chạy kustomize build
một resource Secret tương ứng sẽ được generate ra, bạn có thể thấy tên của Secret được sinh ra có thêm một hậu tố (suffix) ngẫu nhiên, ví dụ:
apiVersion: v1
kind: Secret
metadata: name: app-config-13s4s7jx-kwp namespace: my-app
Hậu tố này được Kustomize thêm vào theo mặc định khi bạn sử dụng các transformer như configMapGenerator
hoặc secretGenerator
. Mục đích của nó là tạo ra tên duy nhất, giúp Kubernetes có thể phân biệt các phiên bản khác nhau của cùng một ConfigMap/Secret khi nội dung của chúng thay đổi (ví dụ: khi bạn cập nhật một giá trị trong Secret, Kustomize sẽ tạo ra một Secret mới với tên khác để đảm bảo tính bất biến).
Tuy nhiên, trong một số trường hợp, việc có hậu tố ngẫu nhiên này có thể gây khó khăn, đặc biệt khi các resource khác cần tham chiếu đến Secret này (ví dụ: một Deployment cần mount Secret vào pod). Bạn không thể biết trước tên Secret sẽ là gì để hardcode vào cấu hình của Deployment.
Có hai hướng xử lý chính cho vấn đề này:
- Tắt suffix (hậu tố ngẫu nhiên)
- Cấu hình
nameReference
để Kustomize tự động thay thế
Cách 1: Tắt suffix:
Bạn có thể yêu cầu Kustomize không thêm hậu tố ngẫu nhiên vào tên Secret bằng cách sử dụng trường disableNameSuffixHash
trong kustomization.yaml
.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: my-app resources: - ../base secretGenerator: - name: app-config files: - config.yaml=config.enc.yaml # disable suffix:
generatorOptions: disableNameSuffixHash: true labels: type: generated annotations: note: generated
Hướng này phù hợp khi bạn không thay đổi nội dung của Secret thường xuyên hoặc khi bạn có một quy trình triển khai đảm bảo rằng việc cập nhật Secret sẽ được xử lý cẩn thận (ví dụ: xóa Secret cũ và tạo Secret mới với cùng tên).
Cách 2: Cấu hình nameReference
để Kustomize tự động thay thế
Đây là cách tiếp cận mình khuyến nghị và mạnh mẽ hơn, đặc biệt khi bạn cần tham chiếu đến Secret từ các resource khác (như Deployment
, StatefulSet
, CronJob
, v.v.) và vẫn muốn tận dụng lợi ích của việc tạo tên duy nhất khi nội dung Secret
thay đổi.
Kustomize cung cấp chức năng nameReference
để tự động cập nhật các tham chiếu tên Secret trong các resource khác. Bạn chỉ cần khai báo cho Kustomize biết trường nào trong resource của bạn đang tham chiếu đến Secret
(hoặc ConfigMap
).
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: my-app resources: - ../base secretGenerator: - name: app-config files: - config.yaml=config.enc.yaml # setup name reference:
configurations:
- name-reference.yaml
nameReference: - kind: Secret version: v1 fieldSpecs: - kind: Deployment path: spec/template/spec/volumes/secret/secretName
Với cấu hình name-reference.yaml
như trên, khi chạy build, kustomize sẽ tự động thay thế giá trị của trường spec/template/spec/volumes/secret/secretName
trong Deployment. Còn trong manifest của Deployment, khi khai báo bạn chỉ cần dùng giống generator là được, ở đây là app-config
.
Tổng kết
Như vậy, trong bài viết này mình cũng đã chia sẻ tới các bạn về cách quản lý Secret khi dùng với ArgoCD, các công cụ có thể áp dụng và cuối cùng đó là cách triển khai theo phương án sử dụng SOPS + Plugins để có thể mã hóa và giải mã các file chứa dữ liệu nhạy cảm khi triển khai GitOps CD với ArgoCD. Nếu các bạn có thắc mắc nào khác có thể comment xuống phía dưới để mình cùng thảo luận nha. Cảm ơn các bạn đã đọc bài!