Mở Cloud Manager, bấm từng nút tạo cluster, tạo máy NFS, rồi SSH vào cài phần còn lại: làm một lần thì nhanh, làm lần thứ hai thì bắt đầu sai. Tài liệu này hướng dẫn viết lại quy trình đó thành pipeline GitLab CI gọi Linode API: một lần chạy là có cluster LKE, ingress, Prometheus, máy NFS và MySQL. Bài trình bày cách Linode API hoạt động, kiến trúc parent/child pipeline, các đoạn script mẫu cho từng bước, và những lỗi bảo mật, lỗi script hay gặp khi tự động hoá hạ tầng bằng curl.
1. Vì sao dựng hạ tầng bằng Linode API thay vì bấm tay
Làm tay lần thứ hai là lúc quên tick private IP, chọn nhầm phiên bản Kubernetes, quên cài Prometheus, vì cấu hình lần trước chỉ nằm trong trí nhớ người bấm. Chuyển sang Linode API giải quyết ba việc:
- Lặp lại được: cùng một payload JSON cho ra cùng một loại hạ tầng.
- Có lịch sử: payload nằm trong Git, ai đổi region hay loại máy đều để lại commit.
- Nối được vào quy trình sẵn có: nếu kho pipeline vận hành đã theo mô hình mỗi branch một thao tác (bài Mỗi branch một pipeline vận hành: tự động hoá bằng GitLab CI), dựng hạ tầng chỉ là thêm vài branch nữa.
2. Linode API hoạt động thế nào
Linode API là REST API phiên bản 4, xác thực bằng personal access token gửi trong header Authorization: Bearer. Token được cấp quyền theo từng nhóm tài nguyên (LKE, Linodes, Volumes…) và từng mức đọc hoặc ghi. Bốn endpoint đủ cho bài toán:
| Endpoint | Việc | Lưu ý |
|---|---|---|
POST /v4/lke/clusters | Tạo cluster LKE với region, phiên bản Kubernetes và node pool | Trả về id ngay, cluster chưa dùng được |
GET /v4/lke/clusters/{id}/kubeconfig | Lấy kubeconfig dạng base64 | Trả lỗi cho tới khi control plane sẵn sàng |
POST /v4/linode/instances | Tạo một Linode (VM) từ image có sẵn | Trạng thái ban đầu là provisioning |
GET /v4/linode/instances/{id} | Đọc trạng thái và địa chỉ IP | Chờ tới khi status là running |
Mọi lệnh tạo trên Linode API đều bất đồng bộ: API trả id trong khi tài nguyên thật vẫn đang được dựng, nên script phải có vòng chờ kèm timeout. Danh sách đầy đủ tham số nằm trong tài liệu Linode API cho LKE.
3. Kiến trúc: một parent pipeline, nhiều child pipeline
Branch cha không tự gọi Linode API. Nó lấy file .gitlab-ci.yml của từng branch con làm artifact, rồi lần lượt kích hoạt mỗi file thành child pipeline theo cơ chế parent-child pipeline của GitLab:
build ─┬─ get_script_cluster (checkout branch con, cp .gitlab-ci.yml → artifact)
├─ get_script_addons
├─ get_script_nfs
└─ get_script_mysql
setup ─── cluster LKE ──► ingress + Prometheus ──► NFS server ──► MySQL
(mỗi mũi tên là needs:, mỗi khối là một child pipeline, strategy: depend)Một job kích hoạt child pipeline (lược bớt):
trigger_cluster:
stage: setup
trigger:
include:
- artifact: setup-cluster.yml
job: get_script_cluster
strategy: depend # cha chờ con, con lỗi thì cha lỗi
variables:
CLUSTER_LABEL: "${CLUSTER_NAME}"
KUBERNETES_VERSION: "<phiên-bản>"
KUBERNETES_REGION: "<region>"
# LINODE_TOKEN là biến CI masked/protected, không truyền trong fileMỗi branch con vẫn chạy độc lập được (rule cho phép web, api và parent_pipeline): muốn thêm một máy NFS cho cluster đang chạy, chỉ cần chạy riêng branch NFS.
4. Bước 1: tạo cluster LKE qua Linode API
Child pipeline đầu tiên gửi payload tạo cluster tới Linode API:
RESPONSE=$(curl -sS --fail -H "Content-Type: application/json" \
-H "Authorization: Bearer $LINODE_TOKEN" \
-X POST -d '{
"label": "'"$CLUSTER_LABEL"'",
"region": "'"$KUBERNETES_REGION"'",
"k8s_version": "'"$KUBERNETES_VERSION"'",
"tags": ["'"$CLUSTER_LABEL"'"],
"node_pools": [ { "type": "g6-standard-2", "count": 1 } ]
}' https://api.linode.com/v4/lke/clusters)
CLUSTER_ID=$(echo "$RESPONSE" | jq -r '.id')
echo "CLUSTER_ID=$CLUSTER_ID" >> created.env # để rollback biết cần xoá gìSau đó là vòng chờ kubeconfig: cứ vài giây gọi lại Linode API ở endpoint /kubeconfig, nếu phản hồi còn chứa errors thì đợi tiếp, quá thời gian giới hạn (ví dụ 120 giây) thì dừng pipeline với mã lỗi. Khi có kubeconfig, chạy thử kubectl get namespace để chắc chắn control plane đã trả lời.
Truyền kubeconfig sang child pipeline kế tiếp
Kubeconfig chỉ tồn tại trong job vừa chạy; các child pipeline có thể chạy trên runner khác nhau, không dùng chung ổ đĩa. Cách đơn giản là ghi kubeconfig vào một biến CI của project qua GitLab API, để child pipeline sau đọc lại:
curl --request PUT --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
"https://gitlab.com/api/v4/projects/<project-id>/variables/SETUP_KUBECONFIG_BASE64" \
--form "value=${KUBECONFIG_BASE64}" --form "masked=true"Cái giá: biến project là trạng thái dùng chung, hai lần dựng song song sẽ ghi đè nhau (đặt tên biến theo cluster hoặc dùng resource_group), và token GitLab ghi biến cần quyền api rất rộng. Phương án gọn hơn là truyền kubeconfig qua artifact có expire_in ngắn.
5. Bước 2: ingress-nginx và Prometheus
Child pipeline thứ hai cài ingress-nginx bằng Helm. Service LoadBalancer của controller khiến Linode tự tạo một NodeBalancer; job đọc IP của nó từ status.loadBalancer, như bài Ingress và NodeBalancer: đưa traffic vào cluster Kubernetes. Tiếp theo, job chờ node đầu tiên chuyển Ready rồi cài kube-prometheus-stack với values tối giản:
alertmanager:
enabled: false
grafana:
enabled: false
prometheus:
service:
type: NodePort
nodePort: 30090
prometheusSpec:
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: linode-block-storage-retain
resources:
requests:
storage: 10GiPrometheus mở qua NodePort để một Grafana bên ngoài cluster đọc dữ liệu. NodePort mở trên mọi node, kể cả IP public, và Prometheus không có xác thực: bắt buộc chặn cổng đó bằng firewall (ví dụ Cloud Firewall của Linode), chỉ cho máy Grafana đi vào. Cuối job, ghi tên cluster, IP NodeBalancer vào nơi lưu cấu hình chung (repo values hoặc hệ thống quản trị) để các pipeline tạo website dùng về sau.
6. Bước 3: máy NFS tạo bằng Linode API
Website lưu public, themes, plugins trên NFS để pod nào cũng đọc được. Child pipeline thứ ba gọi Linode API để tạo một Linode Debian cỡ nhỏ, bật private IP, mật khẩu root sinh ngẫu nhiên ngay trong job và đăng nhập bằng SSH key:
ROOT_PASSWORD=$(openssl rand -base64 24)
JSON_PAYLOAD=$(jq -n --arg pw "$ROOT_PASSWORD" --arg label "$NFS_LABEL" '{
label: $label, region: "<region>", type: "g6-nanode-1",
image: "linode/debian12", authorized_keys: ["<ssh-public-key>"],
root_pass: $pw, private_ip: true }')
# POST /v4/linode/instances → LINODE_ID, ghi vào created.env
# vòng chờ GET /v4/linode/instances/$LINODE_ID tới khi status = running
# lấy IP qua GET /v4/linode/instances/$LINODE_ID/ips, lọc theo public/privateLinode API chỉ lo phần máy. Khi máy chạy, job SSH vào cài nfs-kernel-server, khai báo export cho dải private và mở firewall cho các cổng 111, 2049 cùng dải cổng động:
ssh root@$NFS_IP 'echo "/export 10.0.0.0/16(rw,sync,no_subtree_check,no_root_squash)" >> /etc/exports && exportfs -rav'
ssh root@$NFS_IP 'ufw allow OpenSSH && ufw allow from 10.0.0.0/16 to any port 2049 proto tcp && ufw --force enable' # mở SSH trước khi bật ufw, nếu không sẽ mất kết nốiThay 10.0.0.0/16 bằng dải private thực tế, rồi ghi IP private và thư mục export vào nơi lưu cấu hình cho pipeline tạo site.
7. Bước 4: MySQL
Child pipeline cuối sinh mật khẩu root, tạo Secret trực tiếp trên cluster, cài MySQL (StatefulSet với PVC trên Block Storage, hoặc một VPS riêng tạo bằng Linode API), rồi xoá các biến CI tạm. Đánh đổi giữa MySQL trong và ngoài cluster được bàn ở một bài sau.
8. Lỗi thường gặp khi viết script gọi Linode API
Token viết cứng trong file pipeline
Khai báo token Linode API ngay trong khối variables của .gitlab-ci.yml là lỗi rất dễ mắc ở bản thử. Chuyển ra biến CI sau đó là chưa đủ: xoá khỏi file không xoá khỏi lịch sử Git, ai clone repo đều đọc lại được commit cũ.
- Token từng lọt vào Git coi như đã lộ: thu hồi ngay trong Cloud Manager, tạo token mới với phạm vi hẹp nhất (chỉ LKE và Linodes, đúng mức đọc hoặc ghi cần thiết) và có thời hạn.
- Token chỉ nằm trong biến CI/CD của GitLab, bật Masked để GitLab che giá trị trong log và Protected để chỉ pipeline của branch được bảo vệ mới đọc được.
- Biến Protected không hiện ra trên branch thường. Với kho pipeline mỗi branch một thao tác, phải đánh dấu protected cho các branch dựng hạ tầng, nếu không job chạy với token rỗng.
Tự in bí mật ra log
echo $LINODE_TOKEN, echo $KUBECONFIG_BASE64 hay echo $ROOT_PASSWORD để debug sẽ nằm lại trong log job. Masked chỉ che giá trị gốc; kubeconfig hay mật khẩu sinh trong job thì không được che. Đừng in chúng ra.
Chuyển hướng output qua SSH chạy nhầm máy
ssh root@$IP echo "ALL: 10.0." >> /etc/hosts.allow trông đúng nhưng sai: dấu >> do shell của runner xử lý, nên file bị ghi trên runner chứ không phải trên máy đích. Bọc cả lệnh trong dấu nháy đơn để nó chạy trọn vẹn ở máy đích. Tương tự, ufw allow chỉ thêm rule; firewall chưa ufw enable thì rule chưa có tác dụng.
Đoán thứ tự IP
Lấy ipv4[0] làm IP public và ipv4[1] làm IP private là dựa vào thứ tự phản hồi, không phải hợp đồng của API. Gọi endpoint /ips và lọc theo trường public, hoặc lọc theo dải địa chỉ private.
Rollback không dọn tài nguyên
Pipeline lỗi giữa chừng để lại cluster hoặc VM vẫn tính tiền. Ghi CLUSTER_ID, LINODE_ID ra artifact ngay khi tạo, để job when: on_failure gọi DELETE của Linode API đúng tài nguyên đó.
Bí mật trong values commit lên Git
Điền mật khẩu root vào values.yaml rồi đẩy lên repo Helm là đưa bí mật cho mọi người đọc được repo. Mật khẩu nên nằm trong Secret tạo trực tiếp trên cluster, values chỉ giữ tên Secret.
9. Đánh đổi: curl và jq hay công cụ IaC
| Được | Mất |
|---|---|
| Không thêm công cụ mới: chỉ cần curl, jq, kubectl, helm trong image CI sẵn có | Không có "state": script không biết tài nguyên nào đã tồn tại, chạy lại là tạo thêm bản mới |
| Đọc script là hiểu từng lệnh gọi Linode API | Không có lệnh xem trước thay đổi hay xoá đồng loạt như các công cụ IaC có state |
| Nối thẳng vào mô hình mỗi branch một pipeline | Logic chờ, timeout, xử lý lỗi phải tự viết và tự kiểm thử |
Dựng thêm cluster hoặc máy NFS vài lần mỗi năm thì curl cùng Linode API là đủ, miễn token được giữ đúng chỗ. Nếu số lần dựng và gỡ tăng lên, công cụ có state (Terraform với provider Linode chẳng hạn) đáng để đầu tư. Kubeconfig lấy về cũng cần được bảo vệ như kubeconfig tải từ Cloud Manager ở bài Dựng cluster LKE đầu tiên: node pool, kubeconfig và lựa chọn ban đầu.
Bài tiếp theo
Chart dùng chung: tái sử dụng Helm cho Laravel, MySQL và Redis. Từ chart riêng của một sản phẩm, tách ra bộ chart tham số hoá để dùng cho các dự án Laravel khác.
