Dùng khi cần lấy nhanh phiên bản, serial, module của nhiều router IOS XR và xuất ra file YAML/CSV.
Tóm tắt: Khai báo inventory theo group iosxr, dùng network_cli + module cisco.iosxr.iosxr_facts để lấy facts, rồi ghi ra out/inventory.yml. Chạy được trên Ansible ≥ 2.15 với collection cisco.iosxr.
flowchart LR
A[inventory/hosts.yml] --> B[ansible-playbook]
B -->|SSH network_cli| C[(Router IOS XR)]
C -->|show version / inventory| B
B --> D[iosxr_facts]
D --> E[out/inventory.yml]
Dùng khi cần đẩy config lên nhiều router IOS XR bằng iosxr_config hoặc resource module, chạy thử bằng check mode và giữ backup trước khi commit.
Tóm tắt: Thay đổi nhỏ theo dòng lệnh → cisco.iosxr.iosxr_config (lines + parents, hoặc src từ template). Thay đổi có cấu trúc (interface, BGP, static route, ACL) → resource module như cisco.iosxr.iosxr_interfaces với state: merged/replaced/overridden. Luôn chạy --check trên 1 host trước, bật backup: true, và gắn comment cho commit. Inventory và kết nối network_cli xem ở Thu thập inventory IOS XR bằng Ansible.
flowchart LR
A["playbook + template"] --> B["ansible-playbook --check"]
B -->|"xem commands"| C{"Đúng ý?"}
C -->|Không| A
C -->|Có| D["chạy thật, backup: true"]
D -->|"network_cli"| E[("Router IOS XR")]
E -->|"commit + comment"| F["show configuration commit list"]
Thay toàn bộ cấu hình loại đó trên thiết bị (mục không khai báo bị xoá)
deleted
Xoá cấu hình của mục khai báo (để trống config = xoá hết)
gathered
Đọc config hiện tại ra dạng structured (kết quả ở key gathered)
rendered
Sinh lệnh CLI offline, không cần kết nối (kết quả ở key rendered)
parsed
Parse text config có sẵn (running_config) ra structured
state: overridden với iosxr_interfaces hay iosxr_l3_interfaces sẽ dọn mọi interface không có trong config, kể cả interface management. Chỉ dùng khi biến chứa đầy đủ toàn bộ interface.
# 1. Chạy thử trên 1 host, xem lệnh sẽ gửiansible-playbook -i inventory/hosts.yml push_description.yml --check -l pe1-hni -v# 2. Chạy thật, giới hạn số router cùng lúcansible-playbook -i inventory/hosts.yml push_description.yml -f 5# 3. Kiểm tra trên router# show configuration commit list detail# show configuration commit changes last 1
Muốn rollback, đăng nhập router và dùng rollback configuration last 1 (xem IOS XR CLI cheatsheet).
Dùng khi viết script Python thao tác Junos qua PyEZ - kết nối, đọc facts, gọi RPC, load config, commit confirmed và rollback.
Tóm tắt: PyEZ (junos-eznc) chạy trên NETCONF over SSH (port 830). Dùng Device để kết nối và đọc dev.facts, dev.rpc.* để lấy dữ liệu vận hành, và Config để load/diff/commit. Quy trình an toàn: load → diff → commit_check → commit(confirm=N) → kiểm tra → commit() lần hai để xác nhận.
flowchart LR
A["Device.open()"] --> B["Config(dev, mode=exclusive)"]
B --> C["cu.load()"]
C --> D["cu.diff()"]
D --> E["cu.commit_check()"]
E --> F["cu.commit(confirm=5)"]
F --> G{"Kiểm tra OK?"}
G -->|Có| H["cu.commit() xác nhận"]
G -->|Không| I["không làm gì, tự rollback sau 5 phút"]
Dùng SSH key thay mật khẩu: bỏ passwd, thêm ssh_private_key_file="~/.ssh/id_ed25519". Có ~/.ssh/config (ProxyJump...) thì truyền ssh_config="~/.ssh/config", xem SSH config và jumphost.
cu.load("set system ntp server 192.0.2.123", format="set")
Load chuỗi trực tiếp
mode của Config: exclusive (khoá, tương đương configure exclusive), private (candidate riêng), dynamic, batch, ephemeral. Trên router nhiều người cùng thao tác, dùng exclusive hoặc private.
Dùng khi cần hiểu 4 RPC của gNMI, chọn giữa model OpenConfig và native, viết đúng path và chọn encoding trước khi dùng gnmic hay Telegraf.
gNMI (gRPC Network Management Interface) là giao thức của OpenConfig để đọc, ghi cấu hình và stream telemetry qua gRPC (HTTP/2, thường có TLS). Dữ liệu được mô tả bằng YANG model, và mỗi giá trị được định vị bằng một path dẫn theo cây YANG đó.
Có thể khác version giữa các OS release (deviation)
Bám theo OS release
Nên dùng
Dashboard/alert dùng chung nhiều vendor
Tính năng không có trong OpenConfig, counter chi tiết
Model nào được hỗ trợ và deviation của từng release: IOS XR xem repo YangModels/yang thư mục vendor/cisco/xr/<release>. Junos xem Juniper YANG Data Model Explorer. Nhanh nhất vẫn là gnmic capabilities trên chính thiết bị.
gNMI Set khi muốn đẩy config có cấu trúc, nguyên tử, cùng model với telemetry. Nếu thiết bị hay quy trình đã chuẩn hoá NETCONF (candidate, commit confirmed), dùng NETCONF với ncclient.
CLI/SSH vẫn hợp cho lệnh show một lần hoặc thiết bị cũ không có gNMI.
Tra nhanh lệnh gnmic để kiểm tra capabilities, Get, Set, Subscribe trên IOS XR và Junos.
Quy ước: $R là địa chỉ host:port (IOS XR thường 57400, Junos thường 32767, tuỳ cấu hình). Đặt thông tin đăng nhập bằng biến môi trường để khỏi lặp flag: gnmic đọc mọi flag qua biến GNMIC_<FLAG>.
export GNMIC_USERNAME=automation GNMIC_PASSWORD='***'export R=198.51.100.11:57400alias g='gnmic -a $R --skip-verify' # lab: thêm --insecure nếu gRPC không có TLS
g set --update-path '/interfaces/interface[name=ge-0/0/1]/config/description' --update-value 'to-pe1'
Update từ file JSON
g set --update-path '/interfaces/interface[name=ge-0/0/1]/config' --update-file desc.json
Replace cả cây con
g set --replace-path '/system/config/hostname' --replace-value 'pe1-hni'
Xoá path
g set --delete '/interfaces/interface[name=ge-0/0/1]/config/description'
Nhiều thao tác trong 1 SetRequest
Gộp nhiều --update-path/--update-value, --delete trong cùng một lệnh
Set từ file request (YAML/JSON)
g set --request-file set.yaml
Xem request sẽ gửi, không gửi
g set --request-file set.yaml --dry-run
--replace-path thay toàn bộ cây con. Trỏ nhầm lên container cha (ví dụ /interfaces) có thể xoá cấu hình mọi interface. Thử --dry-run và trên lab trước.
Dùng khi cần đọc/ghi config IOS XR hoặc Junos qua NETCONF bằng Python ncclient - get-config có filter, edit-config vào candidate, validate, commit và commit confirmed.
Tóm tắt: Bật NETCONF trên thiết bị (port 830), kết nối bằng ncclient.manager.connect(..., device_params={"name": "iosxr"|"junos"}). Đọc bằng get_config / get với filter subtree (hoặc xpath nếu thiết bị có capability :xpath). Ghi bằng edit_config(target="candidate") → validate → commit. Luôn khoá candidate trong lúc sửa.
import osfrom ncclient import managerdef connect(host: str, platform: str): conn = { "host": host, "port": 830, "username": os.environ["NET_USER"], "password": os.environ["NET_PASS"], "hostkey_verify": False, # lab; production nên dùng known_hosts "device_params": {"name": platform}, # "iosxr" hoặc "junos" "timeout": 60, } return manager.connect(**conn)if __name__ == "__main__": with connect("198.51.100.11", "iosxr") as m: for cap in m.server_capabilities: if "openconfig-interfaces" in cap or ":candidate" in cap or ":xpath" in cap: print(cap)
OC_IF = """<interfaces xmlns="http://openconfig.net/yang/interfaces"> <interface> <name>GigabitEthernet0/0/0/1</name> </interface></interfaces>"""with connect("198.51.100.11", "iosxr") as m: # subtree filter: chỉ lấy interface có name khớp r = m.get_config(source="running", filter=("subtree", OC_IF)) print(r.data_xml) # xpath filter: chỉ khi thiết bị có capability :xpath r = m.get(filter=("xpath", "/interfaces/interface[name='GigabitEthernet0/0/0/1']/state"))
Junos: config native nằm dưới <configuration>:
JUNOS_BGP = "<configuration><protocols><bgp/></protocols></configuration>"with connect("198.51.100.21", "junos") as m: r = m.get_config(source="running", filter=("subtree", JUNOS_BGP)) print(r.data_xml)
from nc_common import connectCFG = """<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <interfaces xmlns="http://openconfig.net/yang/interfaces"> <interface> <name>GigabitEthernet0/0/0/1</name> <config> <name>GigabitEthernet0/0/0/1</name> <description>to-pe2-hcm</description> </config> </interface> </interfaces></config>"""with connect("198.51.100.11", "iosxr") as m: with m.locked("candidate"): m.edit_config(target="candidate", config=CFG, default_operation="merge") m.validate(source="candidate") m.commit(confirmed=True, timeout="300") # tự rollback sau 300s nếu không xác nhận # ... kiểm tra bằng m.get(...) ... m.commit() # xác nhận
Junos còn có RPC riêng qua device_params={"name": "junos"}: m.load_configuration(action="set", config=["set interfaces ge-0/0/1 description to-pe1-hni"]) và m.command("show bgp summary", format="text"). Nếu chỉ dùng Junos, PyEZ gọn hơn.
Tra nhanh lệnh IOS XR hay dùng - show, commit, commit confirmed, rollback, config replace, backup và install.
IOS XR dùng mô hình two-stage commit: sửa trong target config, chỉ có hiệu lực khi commit. Mỗi commit có commit ID, rollback được theo ID hoặc theo số lần gần nhất. Lệnh Junos tương đương: Junos CLI cheatsheet.
show configuration sessions / clear configuration sessions <session-id>
commit replace thay toàn bộ running-config bằng nội dung target. Chỉ dùng sau khi load đúng file config đầy đủ, và nên đi kèm commit confirmed khi làm từ xa.
Platform chạy IOS XR 7 dạng XR7/LNT (Cisco 8000, NCS 540 một số model) dùng bộ lệnh install package add, install apply, install commit thay cho chu trình add/activate ở trên. Kiểm tra release notes của platform trước khi nâng cấp.
Tra nhanh lệnh Junos hay dùng - show, pipe, configure, commit check/confirmed, compare, rollback, load và display set.
Junos cũng dùng candidate config: sửa xong phải commit. Hệ thống giữ tối đa 50 bản rollback (rollback 0 là active hiện tại). Lệnh IOS XR tương đương: IOS XR CLI cheatsheet.
commit at "2026-11-06 23:00:00" / clear system commit (operational)
Commit đồng bộ 2 RE
commit synchronize
Bỏ mọi thay đổi chưa commit
rollback 0
Quay về bản trước
rollback 1 → show | compare → commit
Lưu / nạp rescue config
request system configuration rescue save / rollback rescue
Làm từ xa trên thiết bị sản xuất: commit check → commit confirmed 5 → kiểm tra vẫn SSH được và dịch vụ ổn → commit. Mất kết nối thì 5 phút sau Junos tự quay lại.
load merge terminal relative (đang edit ở đâu thì gộp tại đó)
Gộp từ file
load merge /var/tmp/change.conf
Thay các khối có tag replace:
load replace /var/tmp/change.conf
Thay toàn bộ config
load override /var/tmp/full.conf
Áp các lệnh từ diff đã lưu
load patch /var/tmp/change.patch
load override thay toàn bộ candidate. Sau đó luôn show | compare trước khi commit để chắc không mất cấu hình quản trị (system, interfaces mgmt, firewall lo0).
Dùng khi cần chọn thư viện Python để chạy lệnh qua SSH/CLI trên IOS XR và Junos, kèm ví dụ show và đẩy config với commit.
Netmiko và Scrapli đều là thư viện "screen scraping": mở phiên SSH tương tác, gửi lệnh CLI, đợi prompt rồi trả về text. Dùng khi thiết bị không có (hoặc bạn không muốn dùng) API có cấu trúc như NETCONF/gNMI. Cả hai lo phần khó: nhận diện prompt, tắt phân trang, vào/ra config mode.
flowchart LR
A["Script Python"] --> B{"Thư viện"}
B -->|"Netmiko"| C["Paramiko"]
B -->|"Scrapli"| D["system (OpenSSH) / paramiko / ssh2 / asyncssh"]
C --> E[("Router")]
D --> E
E -->|"text output"| F["TextFSM / TTP / Genie"]
F --> G["dict / list"]
import osfrom scrapli import Scraplidev = { "host": "198.51.100.11", "auth_username": os.environ["NET_USER"], "auth_password": os.environ["NET_PASS"], "auth_strict_key": False, # lab; production nên kiểm tra host key "platform": "cisco_iosxr", # Junos: "juniper_junos"}with Scrapli(**dev) as conn: r = conn.send_command("show ipv4 interface brief") print(r.result, r.elapsed_time) r = conn.send_configs([ "interface GigabitEthernet0/0/0/1", "description to-pe2-hcm", "commit comment CHG-1234", ]) if r.failed: print("Có lệnh lỗi:", [x.channel_input for x in r if x.failed])
Với IOS XR và Junos, nếu thoát config mode khi còn thay đổi chưa commit, thiết bị sẽ hỏi lại (XR: Uncommitted changes found, commit them before exiting(yes/no/cancel)?). Luôn commit hoặc abort/rollback 0 trước khi thoát để script không treo ở prompt đó.
Xử lý lỗi network_cli và netconf khi chạy Ansible với IOS XR/Junos - unable to open shell, command timeout, persistent connection, paramiko/libssh.
Triệu chứng chính: playbook báo unable to open shell, command timeout triggered, persistent connection idle timeout triggered, Authentication failed, hoặc module Junos báo ncclient is not installed / không kết nối được port 830, trong khi ssh tay vào router vẫn bình thường.
Dùng khi cần đẩy config lên nhiều thiết bị Junos bằng junos_config hoặc resource module, có check mode, diff, commit confirmed và rollback.
Tóm tắt: Junos trong Ansible đi qua NETCONF (ansible.netcommon.netconf, port 830). Config dạng set/text/XML → junipernetworks.junos.junos_config (lines hoặc src + src_format). Config có cấu trúc → resource module như junipernetworks.junos.junos_interfaces. Quy trình an toàn: --check --diff → chạy thật với confirm: 5 → kiểm tra → task confirm_commit: true. Bản IOS XR tương ứng: Đẩy cấu hình IOS XR bằng Ansible.
flowchart LR
A["playbook"] --> B["--check --diff"]
B -->|"load, commit check, rollback 0"| C{"Diff đúng?"}
C -->|Không| A
C -->|Có| D["chạy thật, confirm: 5"]
D -->|"commit confirmed 5"| E[("Junos")]
E --> F{"Verify OK?"}
F -->|Có| G["confirm_commit: true"]
F -->|Không| H["không làm gì, tự rollback sau 5 phút"]
junos_command (lệnh show) chạy được cả với network_cli, nhưng junos_config và các resource module cần netconf. Một nhóm host nên dùng chung netconf cho đơn giản.
set protocols bgp group {{ bgp_group }} type externalset protocols bgp group {{ bgp_group }} neighbor {{ peer_ip }} peer-as {{ peer_asn }}set protocols bgp group {{ bgp_group }} neighbor {{ peer_ip }} description "{{ peer_desc }}"set protocols bgp group {{ bgp_group }} neighbor {{ peer_ip }} import {{ policy_in }}set protocols bgp group {{ bgp_group }} neighbor {{ peer_ip }} export {{ policy_out }}
- name: Thêm BGP peer từ template set junipernetworks.junos.junos_config: src: templates/bgp_peer.set.j2 src_format: set comment: "CHG-1235 peer {{ peer_ip }}" confirm: 5
update: override (hoặc src_format: text kèm update: override) thay toàn bộ cấu hình bằng nội dung file. File thiếu system/interfaces fxp0 là mất đường quản lý. Chỉ dùng khi template sinh đủ toàn bộ config, và luôn kèm confirm.
Template viết kiểu text (ngoặc nhọn) cũng được, đặt src_format: text. Cú pháp vòng lặp/điều kiện xem Jinja2 cho template cấu hình mạng.
state có cùng ý nghĩa như bên IOS XR (merged, replaced, overridden, deleted, gathered, rendered, parsed). state: rendered sinh XML/lệnh offline, tiện để review trong merge request.
# 1. Check mode + diff trên 1 host: load, commit check, rồi bỏ thay đổiansible-playbook -i inventory push_set.yml --check --diff -l mx1-hni# 2. Chạy thật, ít host một lúcansible-playbook -i inventory push_set.yml --diff -f 5# 3. Kiểm tra trên thiết bị# show system commit# show configuration | compare rollback 1
Muốn quay lại bản trước bằng Ansible: task junipernetworks.junos.junos_config với rollback: 1 (load rollback 1 rồi commit). Trên CLI xem Junos CLI cheatsheet.
Dùng khi muốn chạy tác vụ song song trên nhiều router bằng Python thuần với Nornir, kết hợp plugin netmiko/napalm và xử lý kết quả.
Tóm tắt: Nornir là framework Python (không phải DSL như Ansible): đọc inventory → nr.filter(...) chọn host → nr.run(task=...) chạy song song bằng thread → nhận AggregatedResult (dict host → list Result). Kết nối thiết bị nằm ở plugin riêng: nornir_netmiko, nornir_napalm, nornir_scrapli. In kết quả bằng print_result của nornir_utils.
flowchart LR
A["config.yaml"] --> B["InitNornir()"]
B --> C["SimpleInventory: hosts/groups/defaults"]
C --> D["nr.filter(F(...))"]
D --> E["nr.run(task)"]
E -->|"thread pool"| F[("Router 1..N")]
F --> G["AggregatedResult"]
G --> H["print_result / failed_hosts"]
platform ở host/group được plugin dùng làm tên driver. Netmiko và NAPALM đặt tên khác nhau (cisco_xr vs iosxr), nên ghi đè trong connection_options.<plugin>.platform.
Không ghi mật khẩu vào defaults.yaml, nạp từ biến môi trường:
from nornir.core.task import Task, Resultfrom nornir_netmiko.tasks import netmiko_send_commandfrom nornir_napalm.plugins.tasks import napalm_get, napalm_configuredef check_bgp(task: Task) -> Result: # Sub-task: kết quả con nằm trong MultiResult của host r = task.run(task=napalm_get, getters=["bgp_neighbors"]) peers = r.result["bgp_neighbors"]["global"]["peers"] down = [ip for ip, p in peers.items() if not p["is_up"]] return Result(host=task.host, result=down, failed=bool(down))def push_desc(task: Task, dry_run: bool = True) -> Result: cfg = task.host["desc_cfg"] # lấy từ data của host r = task.run(task=napalm_configure, configuration=cfg, dry_run=dry_run) return Result(host=task.host, result=r.result, changed=r.changed, diff=r.diff)
res = nr.run(task=check_bgp, name="BGP down?")res = nr.filter(site="hni").run(task=push_desc, dry_run=True) # chỉ xem diff
Tra nhanh getter, quy trình load/compare/commit/rollback và lệnh CLI napalm cho IOS XR và Junos.
NAPALM cho một API chung trên nhiều hệ điều hành: getter trả về dict cùng cấu trúc, config theo quy trình load candidate → compare → commit/discard. Ví dụ dưới dùng NAPALM 5.x.
commit_config(message="CHG-1234") (iosxr_netconf không hỗ trợ message)
Commit confirmed (Junos)
commit_config(revert_in=300) (giây, phải là bội số của 60)
Còn commit confirmed đang chờ? (Junos)
has_pending_commit()
Xác nhận commit confirmed (Junos)
confirm_commit()
Quay về bản trước commit gần nhất
rollback()
push.py
dev.load_merge_candidate(filename="snippets/desc.set") # Junos tự nhận dạng set/text/xmldiff = dev.compare_config()if not diff: dev.discard_config()elif input(diff + "\nCommit? [y/N] ") == "y": dev.commit_config(message="CHG-1234", revert_in=300) # Junos # ... kiểm tra ... dev.confirm_commit()else: dev.discard_config()
Với hai nền tảng này, revert_in chỉ có trên driver junos; iosxr và iosxr_netconf ném NotImplementedError. Trên IOS XR muốn commit confirmed thì dùng CLI/NETCONF trực tiếp hoặc Ansible.
load_replace_candidate thay cả config. File thiếu phần quản lý (mgmt interface, AAA, SSH) là mất kết nối. Luôn đọc kỹ compare_config().
Dùng khi cần tải model YANG cho IOS XR/Junos/OpenConfig, xem cây bằng pyang -f tree, tìm path cho NETCONF/gNMI và validate dữ liệu bằng yanglint.
Tóm tắt: Lấy model từ repo YangModels/yang (vendor + IETF) hoặc openconfig/public → pyang -p <thư mục> -f tree để xem cây → --tree-path / -f flatten để rút đúng path → dùng path đó trong NETCONF filter hoặc gNMI. yanglint (libyang) dùng để validate file JSON/XML theo model trước khi đẩy. Khái niệm path, OpenConfig vs native: gNMI, YANG và cú pháp path.
python3 -m pip install pyangsudo apt install libyang2-tools # Ubuntu 22.04 (gói có thể tên khác trên bản khác: libyang-tools, yanglint)# Chỉ lấy phần cần trong repo YangModels/yang (repo rất lớn)git clone --depth 1 --filter=blob:none --sparse https://github.com/YangModels/yang.gitcd yanggit sparse-checkout set vendor/cisco/xr standard/ietf/RFCls vendor/cisco/xr/ # thư mục theo release, ví dụ 7111 = 7.11.1# OpenConfiggit clone --depth 1 https://github.com/openconfig/public.git openconfig
Model trong repo có thể khác bản chạy trên router. Nguồn chính xác nhất là chính thiết bị: NETCONF <get-schema> (ncclient m.get_schema("Cisco-IOS-XR-um-interface-cfg"), xem NETCONF với ncclient), hoặc trên IOS XR run ls /pkg/yang. Junos có repo riêng Juniper/yang trên GitHub, chia theo release.
XR=yang/vendor/cisco/xr/7111# Cây đầy đủ của một module (-p tìm import/include, mặc định đệ quy vào thư mục con)pyang -p $XR -f tree $XR/Cisco-IOS-XR-um-interface-cfg.yang | less# Giới hạn độ sâupyang -p $XR -f tree --tree-depth 3 $XR/Cisco-IOS-XR-ipv4-bgp-oper.yang# Chỉ in một nhánh (path không cần prefix)pyang -p openconfig/release/models -f tree \ --tree-path /interfaces/interface/state/counters \ openconfig/release/models/interfaces/openconfig-interfaces.yang# Model có augment: đưa cả module augment vào để thấy nhánh được thêmpyang -p ietf -f tree ietf/ietf-interfaces.yang ietf/ietf-ip.yang
Đọc ký hiệu cây (pyang -f tree --tree-help):
Ký hiệu
Ý nghĩa
rw / ro
Config (ghi được) / state (chỉ đọc, dùng cho oper/telemetry)
list* [name]
List, key là name → path gNMI interface[name=Gi0/0/0/0]
leaf?
Leaf tuỳ chọn
container!
Presence container (có mặt đã mang nghĩa)
leaf-list*
Danh sách giá trị
x-- / o--
Deprecated / obsolete
+--:(case)
Nhánh của choice (không xuất hiện trong path dữ liệu)
Kết quả -f flatten có dạng /ietf-interfaces:interfaces/interface/enabled. Với gNMI đổi thành ietf-interfaces:interfaces/interface[name=...]/enabled hoặc bỏ prefix nếu thiết bị chấp nhận.
M=/usr/local/share/yang/modules # nơi chứa ietf/ iana/ ; đổi theo máy bạn# Cây một nhánh (path có prefix module)yanglint -p $M/ietf -f tree -P /ietf-interfaces:interfaces/interface $M/ietf/ietf-interfaces.yang# Validate file JSON config theo các module liên quanyanglint -p $M/ietf -p $M/iana -t config \ $M/ietf/ietf-interfaces.yang $M/ietf/ietf-ip.yang $M/iana/iana-if-type.yang if.jsonecho $? # 0 = hợp lệ
Dùng khi cần đọc/ghi NetBox (device, interface, IP) bằng REST API hoặc pynetbox để làm nguồn dữ liệu cho script automation.
Tóm tắt: NetBox có REST API tại /api/ (ví dụ /api/dcim/devices/), xác thực bằng API token trong header Authorization. pynetbox bọc API thành object: nb.dcim.devices.get(...), .filter(...), .create(...), sửa thuộc tính rồi .save(). Tham số filter chính là query string của REST API, nên thử bằng curl hoặc trang /api/ trên trình duyệt trước. Dùng NetBox làm inventory cho Ansible: Ansible inventory từ NetBox.
flowchart LR
A["Script / pynetbox"] -->|"Authorization: Token ..."| B["NetBox /api/"]
B --> C["dcim: devices, interfaces"]
B --> D["ipam: ip-addresses, prefixes"]
C --> E["Record object"]
D --> E
E -->|"sửa + save()"| B
NetBox 4.5 bổ sung token v2 (dạng nbt_..., gửi bằng Authorization: Bearer <token>); token kiểu cũ vẫn dùng Authorization: Token <token>. Kiểm tra loại token trên giao diện, và dùng pynetbox bản mới nếu dùng token v2.
import osimport pynetboxnb = pynetbox.api(os.environ["NETBOX_URL"], token=os.environ["NETBOX_TOKEN"])# nb.http_session.verify = "/etc/ssl/certs/internal-ca.pem" # CA nội bộdev = nb.dcim.devices.get(name="pe1-hni") # 1 object hoặc None; nhiều kết quả → lỗiprint(dev.id, dev.platform, dev.primary_ip4)for d in nb.dcim.devices.filter(site="hni", status="active"): print(d.name, d.primary_ip4.address if d.primary_ip4 else "-")for i in nb.dcim.interfaces.filter(device="pe1-hni"): print(i.name, i.description, i.enabled)for ip in nb.ipam.ip_addresses.filter(device="pe1-hni"): print(ip.address, ip.assigned_object)print(nb.dcim.devices.count(site="hni"))
.filter() tự đi hết các trang. Thuộc tính liên kết (dev.site, dev.platform) là object con, in ra thấy tên, truy cập .slug/.id khi cần so sánh.
intf = nb.dcim.interfaces.get(device="pe1-hni", name="GigabitEthernet0/0/0/1")intf.description = "to-pe2-hcm"intf.save() # PATCH các field đã đổi, trả True/Falsedev = nb.dcim.devices.get(name="pe1-hni")dev.update({"comments": "CHG-1234"}) # cập nhật nhiều field một lầnip = nb.ipam.ip_addresses.create( address="192.0.2.0/31", status="active", assigned_object_type="dcim.interface", assigned_object_id=intf.id, description="p2p pe1-pe2",)# Cấp IP trống tiếp theo trong prefixpfx = nb.ipam.prefixes.get(prefix="198.51.100.0/24")new_ip = pfx.available_ips.create({"description": "loopback mới"})
Khi đồng bộ dữ liệu từ thiết bị lên NetBox, so sánh trước rồi mới save() để changelog NetBox không đầy bản ghi vô nghĩa. Chạy thử trên NetBox lab, hoặc dùng token chỉ đọc để test phần đọc.
Dùng khi muốn Ansible lấy danh sách router trực tiếp từ NetBox bằng plugin netbox.netbox.nb_inventory, chia group theo site/role/platform và tự gán biến kết nối.
Tóm tắt: Tạo file netbox.yml với plugin: netbox.netbox.nb_inventory, token qua biến NETBOX_TOKEN. Lọc thiết bị bằng query_filters/device_query_filters (chính là filter REST API), chia group bằng group_by hoặc keyed_groups, sinh biến bằng compose. Plugin tự đặt ansible_host từ primary IP. Luôn xem kết quả bằng ansible-inventory --graph trước khi chạy playbook. Inventory tĩnh: Thu thập inventory IOS XR bằng Ansible.
flowchart LR
A["netbox.yml"] --> B["nb_inventory"]
B -->|"GET /api/dcim/devices/?status=active"| C["NetBox"]
C --> B
B --> D["group_by, keyed_groups"]
B --> E["compose: ansible_network_os"]
D --> F["ansible-inventory --graph"]
E --> F
F --> G["ansible-playbook -i netbox.yml"]
Tính năng Constructable chuẩn của Ansible: group theo giá trị, group theo điều kiện, tạo biến
config_context
Kéo config context của NetBox thành biến host
ansible_host_dns_name
Dùng dns_name của primary IP thay cho địa chỉ
fetch_all
false = gọi API theo từng host khi danh sách filter dài (tránh URL quá dài)
cache, cache_plugin, cache_timeout
Cache kết quả, đỡ gọi API mỗi lần chạy
timeout
Timeout HTTP (giây)
Danh sách tên group hợp lệ trong group_by và tên biến host (role hay device_role, dạng đơn hay số nhiều) thay đổi theo phiên bản collection và plurals. Đừng đoán: chạy ansible-inventory --host <tên> để xem biến thật rồi mới viết compose/keyed_groups.
# Cây groupansible-inventory -i inventory/netbox.yml --graph# Toàn bộ biến của 1 host (xem tên biến thật: site, platform, role...)ansible-inventory -i inventory/netbox.yml --host pe1-hni# Xuất ra YAML để so sánh / debugansible-inventory -i inventory/netbox.yml --list --yaml > /tmp/inv.yml# Chạy playbook trên 1 group sinh từ NetBoxansible-playbook -i inventory/netbox.yml collect.yml -l os_iosxr
Có thể gộp NetBox với inventory tĩnh: trỏ -i inventory/ (cả thư mục), Ansible đọc mọi nguồn trong đó.
Tra nhanh cú pháp Jinja2 (vòng lặp, điều kiện, filter, whitespace control, macro) khi viết template config IOS XR/Junos cho Ansible hoặc Python.
Jinja2 dùng chung cho Ansible (template, src: của *_config), NAPALM load_template và script Python. Filter có dấu (A) chỉ có trong Ansible, Python thuần không có.
IOS XR và Junos dạng text quan tâm thụt lề để hiển thị, nhưng *_config so sánh theo dòng: thừa/thiếu khoảng trắng là lần nào cũng changed. Render thử và so với show running-config trước khi chạy.
Dùng khi phải biến text của lệnh show (IOS XR, Junos) thành dữ liệu có cấu trúc và cần chọn giữa TextFSM/ntc-templates, TTP và Genie.
Lệnh show trả về text cho người đọc. Muốn so sánh, lưu, cảnh báo thì phải parse thành list/dict. Ba công cụ phổ biến đều nhận text và trả về dữ liệu có cấu trúc, khác nhau ở cách viết template và độ phủ lệnh có sẵn.
Trước khi parse, hỏi lại: có cách lấy dữ liệu có cấu trúc sẵn không? Junos có | display json / | display xml và RPC qua PyEZ; IOS XR có model oper qua NETCONF/gNMI. Parse text là phương án cuối, vì output đổi theo release.
flowchart LR
A["show ... (text)"] --> B{"Parser"}
B -->|"regex + state machine"| C["TextFSM + ntc-templates"]
B -->|"template giống output"| D["TTP"]
B -->|"parser Python có sẵn"| E["Genie (pyATS)"]
C --> F["list of dict"]
D --> G["dict lồng nhau"]
E --> G
from ntc_templates.parse import parse_outputrows = parse_output(platform="cisco_xr", command="show ip interface brief", data=raw)# [{'interface': 'Loopback0', 'ip_address': '192.0.2.255', 'status': 'Up', 'proto': 'Up', 'vrf': 'default'}, ...]down = [r["interface"] for r in rows if r["status"] == "Up" and r["proto"] != "Up"]
ntc-templates chọn template bằng cách khớp chuỗi lệnh với regex trong file templates/index, không nhìn vào output. Trên ntc-templates 7.0, show ipv4 interface brief khớp nhầm template của show ipv4 interface (bản chi tiết) và ném TextFSMError, trong khi show ip interface brief cho đúng kết quả. Lỗi State Error raised gần như luôn là chọn sai template hoặc output khác release: mở file index kiểm tra.
Dòng nào trong output không khớp template thì bị bỏ qua, nên TTP hợp để rút vài field từ running-config dài. Mặc định {{ var }} chỉ khớp một từ; description có khoảng trắng phải dùng re(".+") hoặc ORPHRASE.
Dùng khi cần backup config IOS XR/Junos định kỳ bằng Oxidized hoặc Ansible, lưu vào Git để xem lịch sử và diff giữa các lần thay đổi.
Tóm tắt: Hai cách phổ biến. Oxidized: daemon chuyên backup, tự poll theo chu kỳ, mỗi thay đổi là một commit Git, có REST/Web UI. Ansible: playbook chạy show running-config / show configuration | display set, ghi mỗi router một file, rồi git commit nếu có thay đổi. Dù cách nào, phải lọc bỏ dòng thay đổi theo thời gian (timestamp, "Last configuration change") để Git chỉ ghi nhận thay đổi thật.
flowchart LR
A["cron / systemd timer"] --> B["Ansible playbook"]
C["Oxidized (interval)"] --> D[("Router IOS XR / Junos")]
B --> D
D -->|"running config"| E["lọc dòng timestamp"]
E --> F["configs/host.cfg"]
F --> G["git commit nếu có diff"]
G --> H["git push lên GitLab/GitHub"]
docker run -d --name oxidized --restart unless-stopped \ -v "$PWD/oxidized:/home/oxidized/.config/oxidized" \ -p 127.0.0.1:8888:8888 oxidized/oxidized:latest# Bắt backup ngay một node qua RESTcurl -s http://127.0.0.1:8888/node/next/pe1-hni# Xem lịch sử trong repo (bare repo, đọc bằng git --git-dir)git --git-dir oxidized/configs.git log --oneline -5git --git-dir oxidized/configs.git show HEAD
Model iosxr và junos của Oxidized đã tự bỏ các dòng thay đổi theo thời gian. Nguồn node có thể đổi sang http để đọc từ NetBox (xem NetBox REST API và pynetbox). Muốn tự push lên GitLab thì cấu hình hooks loại githubrepo.
Running config chứa secret: hash mật khẩu, SNMP community, key TACACS/BGP MD5. Repo backup phải private, giới hạn người đọc. Muốn đẩy lên nơi rộng hơn thì lọc thêm các dòng này (Oxidized: vars: remove_secret: true như ví dụ trên).
Dùng khi muốn đưa thay đổi cấu hình router qua pipeline Git - lint, validate bằng Batfish, dry-run, deploy, verify và rollback - thay vì gõ tay trên CLI.
Ý tưởng: cấu hình mạng (biến, template, playbook) nằm trong Git. Mọi thay đổi đi qua merge request, pipeline tự kiểm tra từ rẻ đến đắt, người review đọc diff thật trên thiết bị chứ không chỉ diff YAML. Chỉ sau khi merge mới deploy, và deploy luôn có bước verify và đường rollback.
flowchart LR
A["merge request"] --> B["lint: yamllint, ansible-lint"]
B --> C["render template"]
C --> D["validate: Batfish"]
D --> E["dry-run: --check --diff"]
E --> F["review + merge"]
F --> G["deploy: commit confirmed"]
G --> H{"verify"}
H -->|"OK"| I["confirm commit"]
H -->|"lỗi"| J["rollback"]
Batfish đọc file config (IOS XR, Junos, EOS...), dựng mô hình mạng và trả lời câu hỏi mà không cần chạm vào thiết bị. Chạy service bằng Docker, gọi từ Python qua pybatfish.
snapshots/candidate/└── configs/ ├── pe1-hni.cfg # config render từ template (bản sẽ đẩy) └── mx1-hcm.cfg
ci/batfish_check.py
import sysfrom pybatfish.client.session import Sessionbf = Session(host="localhost")bf.set_network("backbone")bf.init_snapshot("snapshots/candidate", name="candidate", overwrite=True)checks = { "parse": bf.q.fileParseStatus().answer().frame().query("Status != 'PASSED'"), "undefined": bf.q.undefinedReferences().answer().frame(), "bgp": bf.q.bgpSessionStatus().answer().frame().query("Established_Status != 'ESTABLISHED'"),}failed = Falsefor name, df in checks.items(): if not df.empty: failed = True print(f"--- {name} ---\n{df.to_string()}")sys.exit(1 if failed else 0)
Peer eBGP ra ngoài snapshot (transit, IXP) sẽ không bao giờ ESTABLISHED trong Batfish: lọc theo Remote_IP hoặc thêm file mô tả external BGP announcement, tránh pipeline đỏ giả.
Với GitHub Actions cấu trúc tương tự, xem GitHub Actions cheatsheet. Trong script dùng Session(host="batfish") khi Batfish chạy dưới dạng service có alias.
Runner chạy dry-run/deploy có credential vào thiết bị thật: dùng runner riêng trong mạng quản lý, credential lưu ở biến CI dạng masked/protected hoặc Vault, tài khoản dry-run chỉ có quyền đọc + load (không commit) nếu AAA cho phép.
Dùng khi cần chứng minh một thay đổi trên router IOS XR đã đạt kết quả mong muốn, bằng assert, ansible.utils.validate (JSON Schema) và fact_diff trước/sau.
Tóm tắt: Quy trình post-check gồm 3 bước: (1) thu thập trạng thái trước thay đổi (iosxr_facts hoặc iosxr_command), (2) đẩy config, (3) thu thập lại rồi kiểm tra bằng ansible.builtin.assert (điều kiện cụ thể), ansible.utils.validate (cấu trúc dữ liệu theo JSON Schema) và ansible.utils.fact_diff (xem cái gì đã đổi). Assert fail thì play dừng với host đó, nên đặt post-check ngay sau task đẩy config. Đẩy config xem Đẩy cấu hình IOS XR bằng Ansible.
iosxr_facts chỉ hỗ trợ một số resource (interfaces, l3_interfaces, lag_interfaces, static_routes, acls, ospfv2, lldp_*, lacp*; xem ansible-doc cisco.iosxr.iosxr_facts). Không có BGP session state, nên với trạng thái runtime dùng iosxr_command rồi parse (xem Parse output show).
post_check.yml
- name: Post-check sau thay đổi hosts: iosxr gather_facts: false tasks: - name: Thu thập interface (cấu hình khai báo) cisco.iosxr.iosxr_facts: gather_subset: min gather_network_resources: - interfaces - l3_interfaces - name: Lấy trạng thái BGP (runtime) cisco.iosxr.iosxr_command: commands: - show bgp ipv4 unicast summary register: bgp_summary
Facts resource nằm ở ansible_network_resources.<tên> (danh sách dict), còn facts cơ bản như ansible_net_version, ansible_net_hostname.
Sau thay đổi, BGP/IGP cần vài giây mới lên. Dùng until thay vì pause cố định:
- name: Chờ neighbor 192.0.2.2 Established cisco.iosxr.iosxr_command: commands: - show bgp ipv4 unicast summary register: bgp until: bgp.stdout[0] | regex_search('(?m)^192\.0\.2\.2\s.*\s\d+\s*$') is not none retries: 12 delay: 10
Cột cuối của show bgp ... summary là số prefix khi session Established, hoặc tên state (Idle, Active) khi chưa lên. Regex trên chỉ khớp khi cột cuối là số. Kiểm tra lại trên phiên bản IOS XR của bạn trước khi dùng cho production.
ansible.utils.validate (engine mặc định ansible.utils.jsonschema) hữu ích khi dữ liệu đã được parse thành structured và bạn muốn kiểm tra toàn bộ danh sách theo một hợp đồng. Cần pip install jsonschema trên control node.
Ở đây bgp_neighbors là list dict {ip, state, prefixes} do bạn tự parse từ output. Khi dữ liệu không khớp, task fail và liệt kê từng lỗi (đường dẫn trong data và lý do).
ansible-playbook -i inventory/hosts.yml post_check.yml -l pe1-hni# Mức chi tiết cao hơn để xem giá trị assert đánh giáansible-playbook -i inventory/hosts.yml post_check.yml -l pe1-hni -v
SELECT SrcAddr, formatReadableSize(sum(Bytes)) AS vol FROM flows WHERE TimeReceived > now() - INTERVAL 1 HOUR GROUP BY SrcAddr ORDER BY sum(Bytes) DESC LIMIT 10
Top AS đích
SELECT DstAS, dictGetOrDefault('asns','name',DstAS,'?') AS name, sum(Bytes)*8/3600 AS bps FROM flows WHERE TimeReceived > now() - INTERVAL 1 HOUR GROUP BY DstAS ORDER BY bps DESC LIMIT 10
Top interface vào
SELECT ExporterName, InIfName, sum(Bytes) AS b FROM flows WHERE TimeReceived > now() - INTERVAL 15 MINUTE GROUP BY 1,2 ORDER BY b DESC LIMIT 20
Top cặp src→dst
SELECT SrcAddr, DstAddr, sum(Bytes) b FROM flows WHERE TimeReceived > now() - INTERVAL 5 MINUTE GROUP BY 1,2 ORDER BY b DESC LIMIT 20
Theo protocol/port
SELECT Proto, DstPort, sum(Packets) p FROM flows WHERE TimeReceived > now() - INTERVAL 1 HOUR GROUP BY 1,2 ORDER BY p DESC LIMIT 20
SELECT toStartOfInterval(TimeReceived, INTERVAL 1 MINUTE) AS t, SrcAS, sum(Bytes) * 8 / 60 AS bpsFROM flowsWHERE $__timeFilter(TimeReceived) AND SrcAS IN ( SELECT SrcAS FROM flows WHERE $__timeFilter(TimeReceived) GROUP BY SrcAS ORDER BY sum(Bytes) DESC LIMIT 5 )GROUP BY t, SrcASORDER BY t
Query Flux mẫu cho telemetry mạng trên InfluxDB 2.x: lọc, tính bps từ counter, top-N, pivot, downsampling và lệnh influx CLI.
Giả định dữ liệu do Telegraf đẩy vào (input gnmi hoặc snmp): bucket telemetry, measurement interfaces, tag source (thiết bị) và name (interface), field counter in_octets / out_octets / in_errors. Đổi tên cho khớp schema thật bằng schema.fieldKeys (xem bên dưới).
Flux chỉ dùng cho InfluxDB 1.8+/2.x. InfluxDB 3 đã bỏ Flux, chuyển sang SQL và InfluxQL.
Mọi query đều theo thứ tự from → range → filter → biến đổi. Đặt filter càng sớm càng tốt để InfluxDB đẩy điều kiện xuống tầng storage (pushdown), query sẽ nhanh hơn nhiều.
Dùng khi cần tính toán giữa các field, ví dụ phần trăm sử dụng băng thông so với speed (đơn vị Mbps). Counter cần derivative, còn speed thì không, nên phải tách thành hai stream rồi ghép lại:
Nhiều lệnh trong cùng một query thì mỗi lệnh phải có yield(name: ...) riêng, nếu không sẽ báo lỗi tried to produce more than one result with the name "_result". Dòng schema.tagValues(...) (bỏ yield) dùng được làm query cho biến (variable) trong Grafana, ví dụ để tạo dropdown chọn thiết bị ${device}.
Dùng khi cần lấy counter interface qua gNMI Subscribe bằng Telegraf và đẩy vào InfluxDB 2.x hoặc Prometheus.
Tóm tắt: Bật gRPC/gNMI trên router, khai báo [[inputs.gnmi]] với một [[inputs.gnmi.subscription]] có name = "interfaces" (tên này thành measurement), rồi xuất ra outputs.influxdb_v2 (bucket telemetry) và/hoặc outputs.prometheus_client. Telegraf là bên dial-in: nó chủ động kết nối tới router.
set system services extension-service request-response grpc clear-text port 32767set system services extension-service request-response grpc skip-authentication
no-tls / clear-text chỉ dùng cho lab. Production nên bật TLS và đặt enable_tls = true + tls_ca ở Telegraf. skip-authentication trên Junos bỏ qua xác thực user, đừng để trên thiết bị thật.
Junos dùng một block [[inputs.gnmi]] riêng với addresses = ["198.51.100.21:32767"]. Bản Telegraf mới có thêm vendor_specific = ["juniper_header"] để giải mã header JTI của Junos.
Telegraf tự gắn tag source (địa chỉ thiết bị) và biến key trong path interface[name=...] thành tag name, khớp schema trong InfluxDB Flux.
Với prometheus_client, metric có tên <measurement>_<field>, ví dụ interfaces_in_octets{source="198.51.100.11",name="HundredGigE0/0/0/0"}. Query xem PromQL cheatsheet.
Dùng khi cần cấu hình MDT trên IOS XR (sensor-group, destination-group, subscription) theo kiểu dial-out hoặc dial-in.
Tóm tắt: MDT gồm 3 khối dưới telemetry model-driven: sensor-group (lấy path YANG nào), destination-group (gửi đi đâu, encoding, giao thức) và subscription (ghép hai khối, đặt chu kỳ sample-interval tính bằng ms). Dial-out: router chủ động đẩy tới collector. Dial-in: collector kết nối vào router qua gRPC/gNMI, subscription không cần destination.
flowchart LR
SG["sensor-group SG-IF"] --> SUB["subscription SUB-IF"]
DG["destination-group DG-COLLECTOR"] --> SUB
SUB -->|"dial-out gRPC :57000"| C["Telegraf cisco_telemetry_mdt"]
G["gNMI client (gnmic, Telegraf gnmi)"] -->|"dial-in :57400"| R[grpc server trên router]
R --> SUB
C --> DB[(InfluxDB / Prometheus)]
aliases đổi encoding path dài thành measurement ngắn. Path native dùng key interface-name, path OpenConfig dùng key name: muốn chung schema tag source/name thì nên ưu tiên path OpenConfig.
Dùng khi cần chọn giao thức flow export cho router/switch và hiểu template, sampling ảnh hưởng thế nào tới số liệu.
Flow telemetry trả lời câu hỏi "ai nói chuyện với ai, bao nhiêu" mà counter interface (SNMP, gNMI) không trả lời được. Có hai cách tiếp cận:
NetFlow v9 / IPFIX: thiết bị gom packet thành flow trong cache (theo 5-tuple + interface...), hết timeout thì xuất record. Định dạng dựa trên template.
sFlow: thiết bị không có flow cache, chỉ lấy mẫu 1/N packet, cắt phần header (thường 128 byte) gửi ngay, kèm counter sample định kỳ. Collector tự dựng flow.
flowchart LR
P[Packet trên interface] --> S["Sampler 1:N"]
S --> C["Flow cache (NetFlow/IPFIX)"]
C -->|"active/inactive timeout"| E[Exporter UDP]
S -->|"sFlow: header mẫu gửi ngay"| E
E --> K["Collector (Akvorado, nfacctd, goflow2)"]
K --> D[(ClickHouse)]
Dùng khi cần dựng collector flow Akvorado bằng Docker Compose, cấu hình router export flow và enrichment SNMP/GeoIP.
Tóm tắt: Akvorado nhận NetFlow v9/IPFIX/sFlow, làm giàu dữ liệu (tên interface qua SNMP, ASN/quốc gia qua GeoIP, classifier) rồi ghi vào ClickHouse; giao diện web để vẽ top talkers. Cách nhanh nhất là bản quickstart Docker Compose. Router chỉ cần export flow + mở SNMP cho collector.
flowchart LR
R["Router (NetFlow/IPFIX :2055)"] --> IN[inlet]
SW["Switch (sFlow :6343)"] --> IN
IN --> K[(Kafka)]
K --> OUT["outlet: decode + enrich"]
OUT -->|SNMP poll| R
OUT --> CH[(ClickHouse)]
ORC[orchestrator] -.->|"config, schema, GeoIP"| OUT
CH --> CON["console :8081"]
Thành phần
Vai trò
inlet
Nhận UDP flow, đẩy vào Kafka
outlet
Giải mã flow, enrichment (SNMP, GeoIP, classifier), ghi ClickHouse (bản 2.x; bản 1.x việc này nằm ở inlet)
orchestrator
Phân phối cấu hình, tạo bảng/dictionary trong ClickHouse
mkdir akvorado && cd akvoradocurl -sL https://github.com/akvorado/akvorado/releases/latest/download/docker-compose-quickstart.tar.gz | tar zxvf -docker compose up -ddocker compose ps
Mở http://<host>:8081. Bản quickstart có sẵn demo exporter sinh flow giả: tắt nó trong file .env / compose override trước khi dùng thật (xem hướng dẫn trong tarball). Mẫu compose override xem Docker Compose patterns.
SNMP: Akvorado poll ifName, ifDescr, ifSpeed theo ifIndex có trong flow. Khai báo community theo dải IP exporter trong akvorado.yaml (key cụ thể khác nhau giữa 1.x và 2.x, xem file mẫu trong quickstart).
GeoIP: quickstart tự tải cơ sở dữ liệu ASN/country dạng MMDB. Có thể trỏ sang file MaxMind GeoLite2 nếu có license.
Classifier: gán Boundary/Connectivity/Provider dựa trên description interface. Nên chuẩn hoá description trên router, ví dụ Transit: example-net.
sudo tcpdump -ni any 'udp port 2055 or udp port 6343' -c 20 # flow có tới host khôngdocker compose logs -f akvorado-inlet akvorado-outlet | grep -i -E 'error|template'docker compose exec clickhouse clickhouse-client -q "SELECT ExporterName, count() FROM flows WHERE TimeReceived > now() - INTERVAL 5 MINUTE GROUP BY 1"
Dùng khi muốn khai báo datasource, folder và dashboard Grafana bằng file trong Git rồi mount vào container.
Tóm tắt: Grafana đọc file YAML trong /etc/grafana/provisioning/datasources/ và /etc/grafana/provisioning/dashboards/ lúc khởi động. Provider dashboard trỏ tới một thư mục chứa file JSON; mỗi thư mục con có thể thành một folder. Cố định uid cho datasource và dashboard để link và tham chiếu không bị đổi.
Tra nhanh PromQL tính bps từ counter, top-N, gom nhóm, percentile, phát hiện mất metric và viết recording rule.
Giả định metric do Telegraf prometheus_client xuất (Telegraf gNMI): interfaces_in_octets, interfaces_out_octets, interfaces_in_errors, label source (thiết bị) và name (interface).
Hiểu các thành phần của Grafana Alerting và cách label quyết định cảnh báo đi tới đâu, khi nào bị gom hoặc tắt tiếng.
Grafana Alerting (unified alerting) tách việc phát hiện sự cố khỏi việc gửi thông báo. Alert rule chỉ sinh ra alert instance có label; việc gửi đi đâu, gom thế nào, nhắc lại bao lâu do notification policy quyết định dựa trên label đó. Nhờ vậy đổi kênh nhận (Slack, email, webhook) không cần sửa rule.
Vi phạm liên tục quá pending period, gửi thông báo
No Data
Query không trả dữ liệu, hành vi tuỳ chỉnh được (No Data, Alerting, OK)
Error
Query hoặc datasource lỗi, hành vi tuỳ chỉnh được
Với telemetry, mất dữ liệu thường quan trọng như vượt ngưỡng. Đặt No Data → Alerting cho rule kiểm tra thiết bị còn gửi metric, hoặc dùng absent_over_time (PromQL cheatsheet).
Policy là một cây. Gốc là default policy, các policy con có matcher (=, !=, =~, !~). Alert đi từ trên xuống, dừng ở policy con đầu tiên khớp, trừ khi bật Continue matching.
Thiết lập
Ý nghĩa
Mặc định
Group by
Gom các instance cùng giá trị label thành một thông báo
Đã có dashboard trên Grafana, nhiều datasource (Influx, ClickHouse, Prometheus)
Grafana Alerting
Chỉ dùng Prometheus, muốn rule nằm cạnh metric
Prometheus rule + Alertmanager
Quản lý rule bằng Git
Provisioning alerting (YAML) hoặc Terraform provider, tương tự Grafana provisioning
Bảo trì có lịch cố định
Mute timing; bảo trì đột xuất dùng silence
Không đặt giá trị thay đổi liên tục (bps hiện tại, timestamp) vào label của rule: mỗi giá trị mới thành một instance mới và gây spam. Đặt chúng vào annotation.
Xử lý TOO_MANY_PARTS, vượt memory limit, merge/mutation bị kẹt, đầy disk và query chậm bằng system tables.
Triệu chứng chính: insert bị từ chối với Too many parts (...). Merges are processing significantly slower than inserts (TOO_MANY_PARTS), query báo Memory limit (for query) exceeded ... (MEMORY_LIMIT_EXCEEDED), disk đầy, hoặc dashboard flow (Akvorado) load rất chậm.
OPTIMIZE TABLE ... FINAL ép merge toàn bộ, rất tốn IO và cần nhiều disk trống. Không dùng làm cách sửa TOO_MANY_PARTS trên bảng lớn trong giờ cao điểm.
-- Lỗi gần nhấtSELECT name, value, last_error_time, last_error_messageFROM system.errors ORDER BY last_error_time DESC LIMIT 10;-- Số part active theo partition (cảnh báo khi lên hàng trăm)SELECT database, table, partition, count() AS parts, sum(rows) AS rowsFROM system.parts WHERE activeGROUP BY database, table, partition ORDER BY parts DESC LIMIT 20;-- Merge đang chạySELECT database, table, round(elapsed) AS sec, round(progress * 100) AS pct, num_parts, formatReadableSize(memory_usage) AS memFROM system.merges;-- Mutation chưa xongSELECT database, table, mutation_id, command, latest_fail_reasonFROM system.mutations WHERE NOT is_done;-- DiskSELECT name, path, formatReadableSize(free_space) AS free, formatReadableSize(total_space) AS totalFROM system.disks;-- Dung lượng theo bảng, tỉ lệ nénSELECT table, formatReadableSize(sum(data_compressed_bytes)) AS compressed, formatReadableSize(sum(data_uncompressed_bytes)) AS raw, round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 1) AS ratioFROM system.parts WHERE activeGROUP BY table ORDER BY sum(data_compressed_bytes) DESC;-- 10 query nặng nhất 1 giờ qua (cùng dạng query gom lại)SELECT normalized_query_hash, count() AS n, round(avg(query_duration_ms)) AS avg_ms, formatReadableSize(max(memory_usage)) AS max_mem, formatReadableSize(sum(read_bytes)) AS read, any(query) AS sampleFROM system.query_logWHERE type = 'QueryFinish' AND event_time > now() - INTERVAL 1 HOURGROUP BY normalized_query_hash ORDER BY sum(query_duration_ms) DESC LIMIT 10;-- Query đang chạySELECT query_id, user, round(elapsed) AS sec, formatReadableSize(memory_usage) AS mem, queryFROM system.processes ORDER BY elapsed DESC;-- Query có dùng primary key / partition pruning khôngEXPLAIN indexes = 1SELECT count() FROM flows WHERE TimeReceived > now() - INTERVAL 1 HOUR;
Lệnh xử lý:
KILL QUERY WHERE query_id = '<id>';KILL MUTATION WHERE mutation_id = '<id>';ALTER TABLE flows DROP PARTITION '<partition-id>'; -- lấy tên từ system.parts.partitionALTER TABLE flows MODIFY TTL TimeReceived + INTERVAL 30 DAY;SYSTEM FLUSH LOGS; -- ghi query_log ngaySELECT * FROM flows WHERE TimeReceived > now() - INTERVAL 1 HOURSETTINGS max_memory_usage = 8000000000, max_execution_time = 60;
Dùng khi cần poll counter interface qua SNMP bằng snmp_exporter, tự build snmp.yml bằng generator và scrape bằng Prometheus.
Tóm tắt: snmp_exporter là một proxy: Prometheus gọi /snmp?target=<ip>&module=if_mib&auth=<tên>, exporter đi walk SNMP thiết bị rồi trả metric. File snmp.yml (danh sách OID + cách đặt label) không viết tay mà sinh từ generator.yml bằng generator. Từ v0.24 file tách thành hai khối auths (community/SNMPv3) và modules (OID cần walk). Với SNMPv2c public thì dùng luôn snmp.yml có sẵn trong release, không cần generator.
git clone https://github.com/prometheus/snmp_exporter.gitcd snmp_exporter/generatormake generator mibs # build binary ./generator và tải bộ MIB chuẩn vào ./mibs
Cần gcc, make, unzip và thư viện libsnmp-dev (Debian/Ubuntu) vì generator dùng net-snmp để parse MIB. MIB riêng của hãng (Cisco, Juniper) thì chép thêm vào ./mibs.
- targets: ["198.51.100.11:9273", "198.51.100.12:9273"] labels: site: hni role: pe
Prometheus tự đọc lại khi file đổi (không cần reload). Dạng .json cũng được: [{"targets": [...], "labels": {...}}]. Sinh file này từ inventory (Ansible, NetBox) là cách đơn giản nhất để đồng bộ danh sách thiết bị.
Web UI → Status → Targets (hoặc Service Discovery)
Metric scrape_samples_scraped, scrape_duration_seconds và up có sẵn cho mọi target: topk(10, scrape_samples_scraped) cho biết exporter nào đang đẩy nhiều series nhất.
Tra nhanh LogQL - stream selector, line filter, parser json/logfmt/pattern, label filter và metric query đếm log theo thiết bị.
Giả định log do pipeline Vector đẩy vào Loki: label job="syslog", source (hostname thiết bị), site, severity; nội dung mỗi dòng là JSON có message, appname, mnemonic. Một query LogQL luôn bắt đầu bằng stream selector{...}, sau đó là chuỗi pipeline nối bằng |.
unwrap biến một label thành giá trị mẫu; duration_seconds() / bytes() chuyển 2s, 10KB thành số. Hàm dùng được: sum_over_time, avg_over_time, max_over_time, quantile_over_time, rate...
logcli instant-query 'sum by (source) (count_over_time({job="syslog"}[5m]))'
Không đưa giá trị động (IP peer, interface, nội dung message) vào label của stream. Mỗi tổ hợp label là một stream riêng; nhiều stream nhỏ làm Loki chậm và báo maximum active stream limit exceeded. Giá trị động để trong nội dung dòng và tách bằng parser lúc query.
2.14 Pipeline syslog thiết bị mạng bằng Vector – Loki và ClickHouse
Dùng khi cần gom syslog từ router/switch qua Vector, tách mnemonic, gắn tên thiết bị/site rồi ghi đồng thời vào Loki và ClickHouse.
Tóm tắt: Vector nhận syslog UDP 514 bằng source socket, chương trình VRL trong transform remap parse header syslog (parse_syslog), tách mnemonic kiểu %ROUTING-BGP-5-ADJCHANGE, rồi tra bảng CSV để gắn source (tên thiết bị) và site theo IP gửi. Kết quả đi hai đường: Loki (tìm log nhanh, label ít) và ClickHouse (lưu lâu, thống kê bằng SQL).
flowchart LR
XR["IOS XR (UDP 514)"] --> S["Vector source socket"]
JN["Junos (UDP 514)"] --> S
S --> T["remap: parse_syslog + regex"]
CSV["devices.csv (enrichment)"] -.-> T
T --> L["sink loki"]
T --> C["sink clickhouse"]
L --> G[Grafana]
C --> G
api: enabled: true # cho vector top / vector tap address: 127.0.0.1:8686enrichment_tables: devices: type: file file: path: /etc/vector/devices.csv encoding: type: csv schema: ip: string name: string site: stringsources: syslog_udp: type: socket mode: udp address: 0.0.0.0:514transforms: parse: type: remap inputs: [syslog_udp] source: | .source_ip = del(.host) # IP thiết bị gửi (peer UDP) del(.source_type) del(.port) # Header syslog RFC 3164/5424 -> hostname, appname, severity, facility, timestamp, message parsed, err = parse_syslog(.message) if err == null { . = merge(., parsed) } # Mnemonic Cisco: %ROUTING-BGP-5-ADJCHANGE m, err = parse_regex(.message, r'%(?P<facility>[A-Z0-9_]+(?:-[A-Z0-9_]+)*)-(?P<sev>[0-7])-(?P<mnemonic>[A-Z0-9_]+)') if err == null { .mnemonic = m.mnemonic .cisco_facility = m.facility } # Gắn tên thiết bị / site theo IP gửi row, err = get_enrichment_table_record("devices", {"ip": .source_ip}) if err == null { .source = row.name .site = row.site } else { .source = string(.hostname) ?? string(.source_ip) ?? "unknown" .site = "unknown" } if !exists(.severity) { .severity = "unknown" }sinks: loki: type: loki inputs: [parse] endpoint: http://loki.example.net:3100 encoding: codec: json labels: # giữ label ít và ổn định job: syslog source: "{{ source }}" site: "{{ site }}" severity: "{{ severity }}" clickhouse: type: clickhouse inputs: [parse] endpoint: http://clickhouse.example.net:8123 database: logs table: syslog skip_unknown_fields: true # field thừa (procid, msgid...) bỏ qua date_time_best_effort: true # nhận timestamp ISO 8601 của Vector auth: strategy: basic user: vector password: "${CLICKHOUSE_PASSWORD}" batch: max_events: 10000 timeout_secs: 5
Vector thay ${VAR} bằng biến môi trường lúc nạp config, nên password để trong /etc/default/vector (gói deb/rpm đọc file này) thay vì ghi thẳng vào YAML. Syslog qua TCP thì thêm một source socket với mode: tcp.
SELECT source, mnemonic, count() AS nFROM logs.syslogWHERE timestamp > now() - INTERVAL 1 HOUR AND mnemonic != ''GROUP BY source, mnemonic ORDER BY n DESC LIMIT 20;
Dùng khi cần dropdown chọn thiết bị/interface (biến phụ thuộc nhau), tự nhân panel theo biến và xử lý kết quả query bằng transformations.
Tóm tắt: Tạo biến device (query variable lấy giá trị label source), rồi biến interface lọc theo $device (chained variable). Panel dùng $device / $interface trong query; bật Repeat để mỗi thiết bị một panel hoặc một row. Transformations (Reduce, Join, Organize fields...) xử lý dữ liệu sau khi query về, không cần sửa query.
sum by (source, name) (rate(interfaces_in_octets{source=~"$device", name=~"$interface"}[$__rate_interval])) * 8
Biến nhiều giá trị phải dùng =~ (Grafana nối thành a|b|c). Định dạng khác theo cú pháp ${var:format}:
Cú pháp
Kết quả với pe1, pe2
Dùng cho
${device:regex}
pe1|pe2 (escape ký tự regex)
PromQL, LogQL
${device:pipe}
pe1|pe2
Regex không cần escape
${device:singlequote}
'pe1','pe2'
SQL IN (...)
${device:sqlstring}
'pe1','pe2' (escape ')
SQL
${device:json}
["pe1","pe2"]
Flux contains(set: ...)
${device:csv}
pe1,pe2
API, URL
${device:raw}
không escape
Khi datasource tự xử lý
Biến có sẵn: $__interval, $__rate_interval (Prometheus, nên dùng trong rate()), $__range, $__from / $__to, $__dashboard. ClickHouse/SQL có macro $__timeFilter(cột).
Panel: Edit panel → Panel options → Repeat options → Repeat by variable = device, Repeat direction = Horizontal (kèm Max per row) hoặc Vertical. Trong query dùng source="$device": mỗi bản sao nhận một giá trị.
Row: thêm row, đặt panel vào row, ở Row options chọn Repeat for = device. Cả row (nhiều panel) được nhân theo từng thiết bị.
Biến phải bật Multi-value hoặc Include All, nếu không chỉ có một bản.
Hiểu cách chọn sorting key, partition, TTL và bảng tổng hợp bằng materialized view khi thiết kế bảng telemetry/log trên ClickHouse.
Bảng MergeTree lưu dữ liệu thành các part bất biến: mỗi lần INSERT tạo một part mới, đã sắp xếp theo ORDER BY; nền tảng chạy merge gộp part nhỏ thành part lớn. Bốn quyết định khi tạo bảng ảnh hưởng tới tốc độ query và dung lượng nhiều nhất: ORDER BY, PRIMARY KEY, PARTITION BY, TTL.
flowchart LR
I["INSERT batch"] --> P1["Part mới (đã sort theo ORDER BY)"]
P1 --> M["Merge nền"]
M --> P2["Part lớn trong partition"]
P2 -->|"TTL hết hạn"| D["Xoá dòng / drop part"]
I -->|"trigger"| MV["Materialized view"]
MV --> AGG["Bảng tổng hợp (SummingMergeTree)"]
Q[SELECT] -->|"primary index bỏ qua granule"| P2
Thứ tự lưu trên disk, cũng là primary key mặc định
Cột hay lọc bằng = và cardinality thấp đứng trước, thời gian đứng sau: (source, name, time)
PRIMARY KEY
Index thưa (sparse): mỗi granule (mặc định 8192 dòng) một entry, nằm trong RAM
Bỏ qua nếu giống ORDER BY; chỉ khai báo khi muốn index là tiền tố ngắn hơn của ORDER BY
PARTITION BY
Chia part theo thư mục logic; merge chỉ diễn ra trong cùng partition
Thô: theo ngày toDate(t) hoặc tháng toYYYYMM(t). Không partition theo thiết bị/giờ (gây TOO_MANY_PARTS)
TTL
Tự xoá dòng (hoặc chuyển volume, gộp dòng) khi hết hạn
TTL time + INTERVAL 90 DAY; kết hợp ttl_only_drop_parts = 1 để drop cả part thay vì viết lại
CODEC / kiểu dữ liệu
Nén theo cột
LowCardinality(String) cho tên thiết bị/interface, Delta/DoubleDelta cho counter và thời gian
Partition không phải index. Query nhanh là nhờ điều kiện WHERE khớp tiền tố ORDER BY (primary index) và khoảng thời gian (partition pruning). Kiểm tra bằng EXPLAIN indexes = 1 SELECT ....
Cùng schema với phần còn lại của section (source, name, in_octets...):
CREATE DATABASE IF NOT EXISTS telemetry;CREATE TABLE telemetry.interfaces( time DateTime CODEC(DoubleDelta, ZSTD), source LowCardinality(String), name LowCardinality(String), in_octets UInt64 CODEC(Delta, ZSTD), out_octets UInt64 CODEC(Delta, ZSTD), in_errors UInt64 CODEC(Delta, ZSTD))ENGINE = MergeTreePARTITION BY toYYYYMM(time)ORDER BY (source, name, time)TTL time + INTERVAL 90 DAYSETTINGS ttl_only_drop_parts = 1;
Query WHERE source = 'pe1-hni' AND time > now() - INTERVAL 1 HOUR chỉ đọc vài granule.
Query chỉ lọc theo name (cột thứ hai) vẫn chạy nhưng kém hiệu quả hơn. Cần lọc nhiều theo cột khác thì thêm projection hoặc data skipping index, đừng đổi ORDER BY vội.
ORDER BY không đổi được sau khi tạo (chỉ thêm cột mới vào cuối). Sai thì tạo bảng mới và INSERT INTO ... SELECT.
-- Đổi TTL bảng có sẵn. Mặc định (materialize_ttl_after_modify = 1) lệnh này-- còn chạy MATERIALIZE TTL trên toàn bộ dữ liệu cũ: nặng IO với bảng lớnALTER TABLE telemetry.interfaces MODIFY TTL time + INTERVAL 30 DAY;-- TTL theo cột: xoá nội dung message sau 7 ngày, giữ dòngALTER TABLE logs.syslog MODIFY COLUMN message String TTL toDateTime(timestamp) + INTERVAL 7 DAY;-- Ép áp TTL cho dữ liệu cũ khi đã tắt materialize_ttl_after_modifyALTER TABLE telemetry.interfaces MATERIALIZE TTL;
TTL được xử lý lúc merge, nên dữ liệu hết hạn có thể còn thêm một thời gian (điều khiển bởi merge_with_ttl_timeout). Biểu thức TTL phải trả về Date hoặc DateTime: cột DateTime64 thì bọc toDateTime(...).
Materialized view (MV) trong ClickHouse là trigger lúc INSERT: mỗi batch ghi vào bảng nguồn được chạy qua SELECT của MV, kết quả ghi vào bảng đích. Ví dụ đếm syslog theo phút từ bảng logs.syslog của pipeline Vector:
CREATE TABLE logs.syslog_1m( t DateTime, source LowCardinality(String), mnemonic LowCardinality(String), cnt UInt64)ENGINE = SummingMergeTreeORDER BY (source, mnemonic, t)TTL t + INTERVAL 1 YEAR;CREATE MATERIALIZED VIEW logs.syslog_1m_mv TO logs.syslog_1m ASSELECT toStartOfMinute(timestamp) AS t, source, mnemonic, count() AS cntFROM logs.syslogGROUP BY t, source, mnemonic;-- Backfill dữ liệu đã có trước khi tạo MVINSERT INTO logs.syslog_1mSELECT toStartOfMinute(timestamp) AS t, source, mnemonic, count() AS cntFROM logs.syslogGROUP BY t, source, mnemonic;-- Query: luôn sum() lại vì merge gộp dòng không tức thờiSELECT t, source, sum(cnt) AS nFROM logs.syslog_1mWHERE t > now() - INTERVAL 1 DAYGROUP BY t, source ORDER BY t;
Với giá trị không cộng dồn được (max, last, quantile) dùng AggregatingMergeTree:
CREATE TABLE telemetry.interfaces_5m( t DateTime, source LowCardinality(String), name LowCardinality(String), in_octets_max SimpleAggregateFunction(max, UInt64), out_octets_max SimpleAggregateFunction(max, UInt64))ENGINE = AggregatingMergeTreeORDER BY (source, name, t)TTL t + INTERVAL 2 YEAR;CREATE MATERIALIZED VIEW telemetry.interfaces_5m_mv TO telemetry.interfaces_5m ASSELECT toStartOfFiveMinutes(time) AS t, source, name, max(in_octets) AS in_octets_max, max(out_octets) AS out_octets_maxFROM telemetry.interfacesGROUP BY t, source, name;
Counter tăng dần nên max mỗi 5 phút tương đương giá trị cuối kỳ. Đặt alias khác tên cột nguồn (in_octets_max), vì max(in_octets) AS in_octets có thể bị ClickHouse thay alias vào chính đối số và báo lỗi aggregate lồng nhau; tính bps từ bảng này giống bảng raw (xem ClickHouse SQL cheatsheet). Hàm phức tạp hơn (quantile, uniq) thì dùng kiểu AggregateFunction(...) với -State khi ghi và -Merge khi đọc.
Lưu ý về MV:
Chỉ thấy dữ liệu INSERT mới vào bảng ở FROM; không tự backfill (tránh POPULATE, dễ mất dữ liệu insert trong lúc tạo).
Lỗi trong SELECT của MV làm INSERT vào bảng nguồn thất bại.
Dùng cú pháp TO bảng_đích để có thể sửa/xoá MV mà không mất dữ liệu tổng hợp.
Tra nhanh hàm SQL ClickHouse hay dùng cho telemetry và log - gom theo khoảng thời gian, tính bps từ counter, percentile, mảng, lấp chỗ trống.
Ví dụ dùng bảng telemetry.interfaces (counter in_octets, cột time, source, name) và logs.syslog định nghĩa trong ClickHouse schema và TTL. Query top talkers cho flow xem ClickHouse top talkers.
SELECT time, source, name, (in_octets - prev_octets) * 8 / dateDiff('second', prev_time, time) AS bpsFROM( SELECT time, source, name, in_octets, lagInFrame(in_octets) OVER w AS prev_octets, lagInFrame(time) OVER w AS prev_time FROM telemetry.interfaces WHERE source = 'pe1-hni' AND time > now() - INTERVAL 1 HOUR WINDOW w AS (PARTITION BY source, name ORDER BY time ROWS BETWEEN 1 PRECEDING AND CURRENT ROW))WHERE prev_time > toDateTime(0) -- bỏ dòng đầu mỗi interface (không có mẫu trước) AND in_octets >= prev_octets -- bỏ điểm counter resetORDER BY name, time;
p95 bps theo interface trong 1 ngày: bọc query trên (đổi khoảng thời gian thành INTERVAL 1 DAY) rồi SELECT source, name, quantile(0.95)(bps) ... GROUP BY source, name.
Phút không có log sẽ không có dòng, đồ thị bị nối thẳng. WITH FILL thêm dòng giá trị mặc định (0):
SELECT toStartOfInterval(toDateTime(timestamp), INTERVAL 1 MINUTE) AS t, count() AS nFROM logs.syslogWHERE timestamp > now() - INTERVAL 1 HOUR AND source = 'pe1-hni'GROUP BY tORDER BY t WITH FILL FROM toStartOfInterval(now() - INTERVAL 1 HOUR, INTERVAL 1 MINUTE) TO now() STEP INTERVAL 1 MINUTE;
Kiểu của FROM/TO phải khớp kiểu cột t (ở đây DateTime, nên bọc toDateTime(...) cho cột DateTime64).
SELECT arrayJoin(['pe1-hni', 'pe2-hcm']) AS source
Tách chuỗi thành mảng
splitByChar(',', 'ge-0/0/0,ge-0/0/1')
Lấy mọi chuỗi khớp regex
extractAll(message, '\\d+\\.\\d+\\.\\d+\\.\\d+')
Lọc / biến đổi phần tử
arrayFilter(x -> x > 0, arr), arrayMap(x -> x * 8, arr)
Có chứa phần tử
has(arr, 'ADJCHANGE')
Độ dài, phần tử đầu (index từ 1)
length(arr), arr[1]
IP nào xuất hiện nhiều nhất trong syslog 1 giờ qua (mỗi IP trong một dòng log thành một dòng kết quả):
SELECT arrayJoin(extractAll(message, '\\d+\\.\\d+\\.\\d+\\.\\d+')) AS ip, count() AS nFROM logs.syslogWHERE timestamp > now() - INTERVAL 1 HOURGROUP BY ip ORDER BY n DESC LIMIT 10;
Xử lý khi Telegraf ghi vào InfluxDB 2.x bị lỗi (partial write, type conflict, buffer overflow), series bùng nổ, disk tăng nhanh hoặc dashboard query timeout.
Triệu chứng chính: log Telegraf có partial write: field type conflict hoặc points beyond retention policy, Metric buffer overflow; N metrics have been dropped; RAM/disk của influxd tăng liên tục; panel Grafana báo timeout hoặc context deadline exceeded. Trang này giả định InfluxDB 2.x OSS, bucket telemetry, measurement interfaces, tag source/name như InfluxDB Flux.
partial write: field type conflict: input field "in_octets" on measurement "interfaces" is type float, already exists as type integer
Cùng field nhưng nguồn khác gửi kiểu khác (int vs float, int vs uint, string)
Ép kiểu ở Telegraf (processors.converter), hoặc ghi sang measurement/bucket mới. Kiểu field cố định trong một shard, chỉ đổi được khi shard cũ hết retention
influx pingcurl -s http://localhost:8086/healthjournalctl -u influxdb -n 100 --no-pager # gói deb/rpm; Docker: docker logs influxdbjournalctl -u telegraf --since '15 min ago' | grep -E 'E!|W!'# Ghi tay 1 điểm (204 = thành công)curl -s -i -X POST "http://localhost:8086/api/v2/write?org=netops&bucket=telemetry&precision=s" \ -H "Authorization: Token $INFLUX_TOKEN" \ --data-binary "interfaces,source=lab1,name=Gi0/0/0/0 in_octets=123i $(date +%s)"# Retention và shard group duration của bucketinflux bucket list --name telemetryinflux bucket update -i <bucket-id> -r 30d --shard-group-duration 1d# Dung lượng (gói deb: /var/lib/influxdb; image Docker: /var/lib/influxdb2)du -sh /var/lib/influxdb/engine/data /var/lib/influxdb/engine/waldf -h /var/lib/influxdb
// Tổng số series của bucket trong 30 ngàyimport "influxdata/influxdb"influxdb.cardinality(bucket: "telemetry", start: -30d)
// Số giá trị của từng tag (chạy lần lượt cho các tag nghi ngờ)import "influxdata/influxdb/schema"schema.tagKeys(bucket: "telemetry", start: -1d) |> yield(name: "tag_keys")schema.tagValues(bucket: "telemetry", tag: "name", start: -1d) |> count() |> yield(name: "name_count")
// Telegraf có đang drop metric không (từ inputs.internal)from(bucket: "telemetry") |> range(start: -1h) |> filter(fn: (r) => r._measurement == "internal_write" and (r._field == "metrics_dropped" or r._field == "buffer_size")) |> last()
Với InfluxDB 1.x (InfluxQL): SHOW SERIES CARDINALITY ON telemetry và SHOW TAG VALUES CARDINALITY ON telemetry WITH KEY = "name". Xoá dữ liệu sai bằng influx delete (xem bảng influx CLI trong InfluxDB Flux); disk đầy ở tầng OS xem disk đầy.
flowchart LR
R["Router / switch"] -->|"gNMI dial-in"| T[Telegraf]
R -->|"SNMP poll"| SE[snmp_exporter]
R -->|"syslog UDP"| V[Vector]
R -->|"NetFlow / IPFIX"| AK[Akvorado inlet]
T --> K[(Kafka)]
V --> K
AK --> K
K --> DB["TSDB / ClickHouse / Loki"]
SE -->|"scrape (pull)"| P[Prometheus]
DB --> G["Grafana + Alerting"]
P --> G
Không phải hệ thống nào cũng cần Kafka: với vài chục thiết bị, Telegraf ghi thẳng InfluxDB hoặc Prometheus scrape trực tiếp là đủ.
Số series ≈ số thiết bị × số interface × số field. Ví dụ 200 router × 100 interface × 6 counter = 120.000 series; chu kỳ 10 giây → 12.000 điểm/giây. Con số này quyết định:
TSDB nào đủ (một Prometheus/InfluxDB OSS xử lý được mức này; vài triệu series thì cần sharding, Mimir/VictoriaMetrics, hoặc ClickHouse).
Có cần Kafka để đệm khi kho dữ liệu bảo trì.
Retention và downsampling: giữ raw 15-30 ngày, bản 5 phút giữ 1-2 năm (task Flux, recording rule, hoặc materialized view ClickHouse).
Tag/label có giá trị không giới hạn (IP peer, description, session id) làm series bùng nổ: xem InfluxDB troubleshooting.
Khoanh vùng khi subscribe gNMI không ra dữ liệu - kết nối/TLS, xác thực, path và origin, encoding, sample interval và đồng hồ thiết bị.
Triệu chứng chính: collector (Telegraf inputs.gnmi, gnmic) kết nối lỗi code = Unavailable, x509: ..., Unauthenticated; hoặc subscribe thành công nhưng không có update, thiếu interface, chu kỳ dữ liệu khác cấu hình, hoặc Grafana không thấy điểm nào dù collector đang nhận. Nguyên tắc: thử bằng gnmic từ đúng máy collector trước, rồi mới sửa config Telegraf.
Telegraf với TLS (router dùng cert do CA nội bộ ký):
/etc/telegraf/telegraf.d/gnmi.conf
[[inputs.gnmi]] addresses = ["pe1-hni.example.net:57400"] # hostname khớp SAN trong cert username = "${NET_USER}" password = "${NET_PASS}" encoding = "proto" redial = "10s" enable_tls = true tls_ca = "/etc/telegraf/pki/ca.pem" # insecure_skip_verify = true # chỉ dùng tạm để khoanh vùng [[inputs.gnmi.subscription]] name = "interfaces" origin = "openconfig" path = "/interfaces/interface/state/counters" subscription_mode = "sample" sample_interval = "10s"
IOS XR có thể dùng cert tự ký do router sinh ra khi bật TLS cho gRPC (thường nằm ở /misc/config/grpc/ems.pem); chép file này về collector làm tls_ca và kết nối bằng tên/IP có trong cert. Cấu hình phía router xem MDT trên IOS XR và Telegraf gNMI.
Tra nhanh Vector (observability pipeline) - lệnh CLI validate/top/tap/test, source/transform/sink phổ biến, buffer và các hàm VRL hay dùng.
Vector chạy một đồ thị source → transform → sink khai báo trong YAML/TOML, mỗi component có type và inputs (tên component phía trước). Ví dụ đầy đủ cho syslog thiết bị mạng xem pipeline syslog bằng Vector.
m, err = parse_regex(.message, r'%(?P<mn>[A-Z_-]+)')
Parse key=value
parse_key_value!(.message)
Ép kiểu
.bytes = to_int!(.bytes)
Kiểm tồn tại / null
exists(.a), is_null(.a)
Giá trị mặc định
.host = string(.host) ?? "unknown"
Đổi chuỗi
replace(.message, "a", "b"), downcase(.x)
Format thời gian
format_timestamp!(now(), "%F %T")
Merge object
. = merge(., parsed)
Bỏ event
abort
Biến môi trường
get_env_var!("ENV")
Hàm có hậu tố ! sẽ abort event nếu lỗi, hàm không có ! trả về value, err. VRL buộc xử lý mọi hàm có thể lỗi, nếu không sẽ báo unhandled fallible assignment lúc compile.
Tra nhanh công cụ net-snmp (snmpget/walk/bulkwalk/table/translate), tuỳ chọn hiển thị và các OID system, IF-MIB, BGP4-MIB hay dùng khi debug thiết bị mạng.
Bộ công cụ net-snmp chạy từ máy collector. Ví dụ dùng 192.0.2.1, community $COMM, user SNMPv3 nms01 với mật khẩu lấy từ biến môi trường. Cấu hình SNMPv3 trên thiết bị xem SNMPv3 authPriv; đưa vào Prometheus xem snmp_exporter.
export SNMP_AUTH='***' SNMP_PRIV='***' # lấy từ secret store, không gõ thẳng vào lệnhsnmpwalk -v3 -l authPriv -u nms01 -a SHA -A "$SNMP_AUTH" -x AES -X "$SNMP_PRIV" 192.0.2.1 system
6 = established (BGP4-MIB, một số hãng dùng MIB riêng)
Tính băng thông: lấy ifHCInOctets hai lần cách nhau t giây, bps = (v2 - v1) * 8 / t. ifIndex có thể đổi sau reload, nên map theo ifName thay vì hard-code index.
SNMPv2c gửi community dạng rõ qua UDP 161. Chỉ dùng cho lab hoặc mạng quản trị đã giới hạn bằng ACL, môi trường thật nên dùng v3.
curl -sI https://tools.example.net/app/ | head -1curl -s https://tools.example.net/app/ | grep -o 'src="[^"]*"' # phải bắt đầu bằng /app/nginx -t && systemctl reload nginx
Với Cloudflare Tunnel, cloudflared luôn nói chuyện HTTP với origin. Hãy để Nginx gắn X-Forwarded-Proto https thay vì tin header từ client.
Thứ tự khoanh vùng nên đi: Events → describe → logs --previous → exec/debug. Phần lớn lỗi Pending và CrashLoopBackOff lộ ra ngay trong Events mà không cần vào container.
kubectl get secret -o yaml in ra giá trị base64 (chỉ là mã hoá, không phải bảo mật). Đừng dán output đó vào sổ tay hay ticket.
Tra nhanh lệnh Docker CLI cho container, image, build, volume, network và dọn dẹp ổ đĩa.
Quy ước: <c> là tên hoặc ID container, <img> là image. Các lệnh dùng cú pháp Docker Engine hiện hành (docker compose là plugin v2, không có gạch nối). Compose xem Docker Compose – pattern hay dùng.
docker system prune -a --volumes xoá luôn volume không gắn với container nào, kể cả dữ liệu database của stack đang down. Chạy docker volume ls trước.
Log mặc định (json-file) không giới hạn dung lượng. Đặt "log-opts": {"max-size": "10m", "max-file": "3"} trong /etc/docker/daemon.json rồi restart Docker (chỉ áp dụng cho container tạo mới). Xem thêm Disk đầy.
Dùng khi viết compose.yaml cho stack nhiều service cần healthcheck, thứ tự khởi động, profiles, biến môi trường và override theo môi trường.
Tóm tắt: Viết compose.yaml theo Compose Specification (không còn trường version:), dùng healthcheck + depends_on.condition: service_healthy để chờ DB sẵn sàng, profiles cho service phụ, .env cho biến, và compose.override.yaml cho cấu hình dev. Chạy bằng docker compose (plugin v2).
flowchart LR
A[compose.yaml] --> M[Merge]
B[compose.override.yaml] --> M
E[".env"] -->|interpolation| M
M --> C["docker compose up"]
C --> D[(db)]
D -->|service_healthy| W[api]
W -->|service_started| P[proxy]
Khoanh vùng khi container không ra internet, không resolve DNS, port publish không truy cập được hoặc kết nối treo do MTU.
Triệu chứng chính: trong container ping 1.1.1.1 hoặc curl https://example.com timeout, Temporary failure in name resolution, curl http://<host>:8080 từ máy khác bị Connection refused, hoặc TLS handshake/git clone/apt treo giữa chừng trong khi gói nhỏ vẫn đi được.
net.ipv4.ip_forward=0 hoặc rule MASQUERADE của Docker bị xoá (firewalld/nftables reload, iptables -F)
sysctl -w net.ipv4.ip_forward=1, rồi systemctl restart docker để Docker tạo lại rule
Ping IP được nhưng không resolve tên
Host chỉ có nameserver loopback (dnsmasq local, 127.0.0.1...). Container không dùng được loopback của host nên Docker bỏ đi và fallback về 8.8.8.8/8.8.4.4, bị firewall công ty chặn
Đặt "dns": ["192.0.2.53"] trong daemon.json hoặc --dns khi run
Container A không gọi được container B bằng tên
Cả hai nằm trên bridge mặc định docker0 (không có DNS nội bộ)
Tạo network riêng docker network create appnet, Compose tự làm việc này
Connection refused khi gọi port đã publish
App trong container listen 127.0.0.1
App phải listen 0.0.0.0 (hoặc ::)
Publish port bỏ qua ufw/firewalld
Docker chèn rule NAT/DOCKER chain trước rule của ufw
Publish 127.0.0.1:8080:80 nếu chỉ cần local, hoặc thêm rule vào chain DOCKER-USER
Port chỉ truy cập được từ host, máy khác không vào được
Publish dạng 127.0.0.1:8080:80
Bỏ IP hoặc dùng IP interface ngoài
--network host mà -p không có tác dụng
Chế độ host dùng chung stack mạng host, không có NAT
Bỏ -p, app listen trực tiếp port trên host
HTTPS/git clone/apt treo, request nhỏ vẫn qua
MTU đường ra nhỏ hơn 1500 (VPN, VXLAN, PPPoE), PMTUD bị chặn
Đặt MTU network Docker nhỏ hơn uplink (xem dưới)
Mạng nội bộ công ty không tới được từ container
Subnet bridge Docker (mặc định 172.17.0.0/16, network mới lấy từ 172.18+) trùng subnet thật
Đổi bip và default-address-pools trong daemon.json
Sau khi đổi daemon.json, Docker không start
JSON sai cú pháp hoặc trùng tuỳ chọn với flag trong unit systemd
flowchart TD
A["Container lỗi mạng"] --> B{"ping IP ngoài được?"}
B -->|Không| C["ip_forward, iptables/nft MASQUERADE"]
B -->|Có| D{"Resolve DNS được?"}
D -->|Không| E["resolv.conf trong container, dns trong daemon.json"]
D -->|Có| F{"Gói lớn/TLS treo?"}
F -->|Có| G["MTU"]
F -->|Không| H["Port publish, listen address, firewall"]
Kiểm tra từ trong container bằng image có đủ công cụ: docker run --rm -it --network container:<c> nicolaka/netshoot (dùng chung network namespace với container lỗi).
Kiểm tra trên host: route, NAT, forwarding. Bắt gói trên docker0 hoặc br-<id> bằng tcpdump, xem tcpdump cheatsheet.
So MTU giữa uplink của host và interface trong container.
# Trên hostsysctl net.ipv4.ip_forwardip -br addr show docker0; ip routeiptables -t nat -S POSTROUTING | grep MASQUERADE # hoặc: nft list ruleset | grep -i masqdocker network lsdocker network inspect bridge -f '{{json .IPAM.Config}}'ss -ltnp | grep 8080ip link show eth0 | grep -o 'mtu [0-9]*'# Trong container (netshoot)cat /etc/resolv.confip -br addr; ip routeping -c2 1.1.1.1 && dig example.comping -c2 -M do -s 1472 1.1.1.1 # fail mà -s 1372 qua => MTU đường ra < 1500curl -sv https://example.com -o /dev/null
DNS nội bộ của user-defined network là 127.0.0.11 trong /etc/resolv.conf của container. Thấy địa chỉ này là bình thường, Docker sẽ forward ra DNS của host.
3.6 Cloudflare Tunnel – publish dịch vụ nội bộ không mở port
Dùng khi cần đưa web/SSH nội bộ ra internet qua Cloudflare mà không mở port inbound, không cần IP public.
Tóm tắt:cloudflared chạy trên máy nội bộ, mở kết nối outbound tới Cloudflare edge. Tạo tunnel bằng cloudflared tunnel create, gắn hostname bằng cloudflared tunnel route dns, khai báo ingress trong config.yml, cài thành service systemd, rồi bảo vệ bằng Cloudflare Access. Domain phải đang dùng DNS của Cloudflare.
# Cài trên Debian/Ubuntu: theo hướng dẫn repo pkg.cloudflare.com, rồicloudflared --versioncloudflared tunnel login # mở trình duyệt, chọn zone; tạo ~/.cloudflared/cert.pemcloudflared tunnel create netlab # in ra Tunnel ID, tạo ~/.cloudflared/<UUID>.jsoncloudflared tunnel listcloudflared tunnel route dns netlab grafana.example.com # tạo CNAME -> <UUID>.cfargotunnel.comcloudflared tunnel route dns netlab ssh.example.com
sudo cloudflared service install # đọc /etc/cloudflared/config.yml, tạo unit cloudflared.servicesudo systemctl enable --now cloudflaredjournalctl -u cloudflared -f
Cách khác (tunnel quản lý trên dashboard Zero Trust, không cần config.yml): tạo tunnel trên dashboard, copy token rồi chạy sudo cloudflared service install <TOKEN>. Ingress lúc này cấu hình trên dashboard, file local bị bỏ qua.
Trên dashboard Zero Trust → Access → Applications → Add an application → Self-hosted: nhập hostname (grafana.example.com), tạo policy Allow với rule Emails ending in @example.com hoặc nhóm IdP. Người dùng phải đăng nhập trước khi request tới được tunnel.
Nắm nhanh Pod, Deployment, Service, Ingress, ConfigMap, Secret, PVC và control plane để đọc hiểu manifest và log lỗi.
Kubernetes là hệ thống điều phối container theo mô hình khai báo: bạn gửi trạng thái mong muốn (manifest YAML) lên API server, các controller liên tục so sánh và kéo trạng thái thực tế về khớp với nó (reconcile loop). Không có lệnh "chạy container X trên node Y", chỉ có "muốn 3 bản sao của image X".
flowchart LR
K[kubectl] --> API["kube-apiserver (control plane)"]
API --> ETCD[(etcd)]
SCH[kube-scheduler] --> API
CM[kube-controller-manager] --> API
KL["kubelet (node)"] --> API
KP["kube-proxy (node)"] --> API
KL --> CR["containerd (CRI)"]
Thành phần
Vai trò
kube-apiserver
Cửa vào duy nhất, xác thực, RBAC, ghi/đọc etcd. Mọi thành phần khác chỉ nói chuyện với API server
etcd
Key-value store lưu toàn bộ trạng thái cluster. Mất etcd là mất cluster, cần backup
kube-scheduler
Chọn node cho Pod mới (resource request, taint/toleration, affinity)
kube-controller-manager
Chạy các controller: Deployment, ReplicaSet, Node, Job, EndpointSlice...
kubelet
Agent trên mỗi node, nhận Pod spec và bảo container runtime chạy; chạy probe
kube-proxy
Lập trình iptables/IPVS/nftables để Service IP trỏ tới Pod (một số CNI như Cilium thay thế nó)
CNI plugin
Cấp IP cho Pod và nối mạng giữa các node (Calico, Cilium, Flannel)
Chuỗi liên kết: Service selector khớp labels của Pod template → EndpointSlice chứa IP các Pod Ready → DNS api.<namespace>.svc.cluster.local trả về ClusterIP. Sai label là Service không có endpoint.
Nên: nhiều service, cần tự phục hồi, rolling update, scale ngang, nhiều team chung hạ tầng, đã có người vận hành cluster (hoặc dùng managed: EKS/GKE/AKS).
Tra nhanh lệnh Helm (3.x/4.x) để cài, nâng cấp, rollback release, xem values và debug template chart.
Quy ước: <rel> là tên release, <chart> dạng repo/chart hoặc oci://.... Helm lưu trạng thái release dưới dạng Secret trong namespace của release. Luôn thêm -n <ns>.
--reuse-values gộp values cũ với --set mới nhưng bỏ qua default mới của chart version mới. Khi nâng version chart, truyền lại đầy đủ -f values.yaml thay vì dùng cờ này.
Trong template, dùng {{ .Values.x | default "y" }}, {{- toYaml .Values.resources | nindent 12 }} và required "image.tag là bắt buộc" .Values.image.tag để chart báo lỗi sớm thay vì render YAML sai.
Dùng khi đặt Nginx trước app nội bộ để terminate TLS, bật HTTP/2, proxy WebSocket, gắn header chuẩn và giới hạn tốc độ request.
Tóm tắt: Một server port 80 chỉ redirect sang HTTPS, một server 443 terminate TLS (http2 on;, TLS 1.2/1.3) rồi proxy_pass tới upstream. Thêm map $http_upgrade cho WebSocket, proxy_set_header cho Host/X-Forwarded-*, và limit_req cho endpoint nhạy cảm. Cú pháp theo Nginx ≥ 1.25.1.
flowchart LR
C[Client] -->|"HTTP :80"| R["301 sang HTTPS"]
C -->|"HTTPS :443 TLS + HTTP/2"| N[Nginx]
N -->|"limit_req"| L{"Vượt rate?"}
L -->|Có| E["429"]
L -->|Không| U["upstream app 127.0.0.1:8000"]
N -->|"Upgrade: websocket"| W["ws 127.0.0.1:8001"]
listen 443 ssl http2; đã deprecated từ 1.25.1, dùng directive riêng http2 on;. Nginx cũ hơn vẫn phải dùng cú pháp cũ.
proxy_set_header và add_headerkhông cộng dồn: nếu một location khai báo bất kỳ proxy_set_header nào, nó bỏ hết các proxy_set_header kế thừa từ server. Vì vậy mỗi location ở trên khai báo lại đủ header.
proxy_pass http://host:port/; (có / cuối) thay phần URI khớp location bằng /. Không có / cuối thì URI giữ nguyên.
burst=20 nodelay: cho phép dồn 20 request vượt rate được xử lý ngay, quá nữa trả 429.
Dùng khi cần publish Service qua Ingress với chứng chỉ Let's Encrypt tự cấp và tự gia hạn bằng cert-manager ClusterIssuer.
Tóm tắt: Cài ingress-nginx và cert-manager bằng Helm, tạo ClusterIssuer ACME (staging trước, production sau), rồi gắn annotation cert-manager.io/cluster-issuer vào Ingress. cert-manager tự tạo Certificate, giải challenge HTTP-01 qua chính ingress-nginx, lưu cert vào Secret ở spec.tls[].secretName và gia hạn trước khi hết hạn.
Dự án ingress-nginx (kubernetes/ingress-nginx) đã ngừng phát triển: Kubernetes SIG Network thông báo chỉ bảo trì best-effort tới tháng 3/2026, sau đó không còn bản phát hành hay bản vá bảo mật. Cluster mới nên cân nhắc controller khác hoặc Gateway API (cert-manager cũng hỗ trợ Gateway). Phần cert-manager dưới đây áp dụng được cho các controller khác, chỉ cần đổi ingressClassName.
flowchart LR
I["Ingress + annotation cluster-issuer"] --> C[Certificate]
C --> O["Order / Challenge"]
O -->|"HTTP-01 /.well-known/acme-challenge"| LE["Let's Encrypt"]
LE -->|"GET qua ingress-nginx"| S["solver Pod tạm"]
LE -->|"cấp cert"| K["Secret TLS"]
K --> N["ingress-nginx dùng cert"]
crds.enabled=true là tên value từ cert-manager 1.15 (bản cũ dùng installCRDs=true). Trỏ bản ghi A/AAAA của app.example.com về EXTERNAL-IP của controller trước khi xin cert.
Chạy non-root (dùng UID số để Kubernetes runAsNonRoot kiểm tra được)
USER 10001
File thuộc user app mà không thêm layer chown
COPY --chown=app:app . .
Pin base image theo digest
FROM python:3.12-slim@sha256:<digest>
Secret lúc build, không lưu vào layer
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci + docker build --secret id=npmrc,src=$HOME/.npmrc .
Exec form để app là PID 1 và nhận SIGTERM
CMD ["app", "--flag"] (không dùng CMD app --flag)
App không tự reap zombie / xử lý signal
docker run --init hoặc ENTRYPOINT ["tini", "--"]
Đổi signal dừng
STOPSIGNAL SIGQUIT (vd nginx graceful)
Healthcheck cho Docker/Compose
HEALTHCHECK CMD curl -fsS http://127.0.0.1:8000/healthz || exit 1 (image phải có curl)
Không COPY .env, không ARG TOKEN hay ENV PASSWORD=... cho secret: giá trị ARG/ENV nằm trong metadata, xem được bằng docker history/docker inspect. Xoá file ở layer sau cũng không xoá khỏi layer trước.
Dùng khi image quá lớn, pull chậm hoặc nhiều CVE, cần chọn base slim/alpine/distroless, tách multi-stage và soi layer bằng dive, docker history.
Tóm tắt: Đo trước (docker image ls, docker history, dive), tìm layer to nhất, rồi xử lý theo thứ tự hiệu quả: (1) multi-stage để bỏ toolchain build, (2) đổi base sang -slim hoặc distroless, (3) dọn cache trong cùng RUN, (4) .dockerignore. Quy tắc viết Dockerfile chung xem Dockerfile – best practices.
flowchart LR
A["Đo: image ls, history"] --> B["dive: layer nào to, file thừa"]
B --> C{"Có toolchain build?"}
C -->|Có| D["Multi-stage"]
C -->|Không| E["Đổi base slim/distroless"]
D --> E
E --> F["Dọn cache trong cùng RUN"]
F --> G["Đo lại, CI=true dive"]
docker image ls app # cột SIZE là dung lượng giải néndocker history app:latest # kích thước từng layer, lệnh tạo ra nódocker history --no-trunc --format '{{.Size}}\t{{.CreatedBy}}' app:latest | sort -h | taildocker buildx imagetools inspect python:3.12-slim # manifest, các platform có sẵn# dive: duyệt từng layer, xem file thêm/sửa/xoá và "wasted space"dive app:latestCI=true dive app:latest # chế độ không tương tác, trả exit code
Trong dive: Tab chuyển giữa panel layer và cây file, Ctrl+U ẩn file không đổi để chỉ thấy file do layer đó thêm. Chỉ số Image efficiency thấp nghĩa là có file bị ghi đè hoặc xoá ở layer sau (vẫn chiếm chỗ ở layer trước).
# Sai: rm ở layer sau không làm image nhỏ điRUN apt-get update && apt-get install -y build-essentialRUN rm -rf /var/lib/apt/lists/*# ĐúngRUN apt-get update \ && apt-get install -y --no-install-recommends libpq5 \ && rm -rf /var/lib/apt/lists/*# pip / npm: không giữ cache trong imageRUN pip install --no-cache-dir -r requirements.txtRUN npm ci --omit=dev && npm cache clean --force
Với Python cần compile extension: cài build-essential ở stage build, tạo venv, rồi chỉ copy /opt/venv sang stage -slim (ví dụ ở trang Dockerfile – best practices).
Hiểu requests/limits, QoS class và liveness/readiness/startup probe để Pod được schedule đúng, không bị OOMKill/throttle vô lý và không restart oan.
Hai nhóm field trong container spec quyết định Pod sống ổn định hay không: resources (scheduler đặt Pod ở đâu, kernel giới hạn bao nhiêu) và probes (kubelet khi nào gửi traffic, khi nào restart container).
Scheduler: chỉ đặt Pod lên node còn đủ allocatable trừ tổng requests
Kernel (cgroup) trên node lúc chạy
CPU
Trọng số chia CPU khi tranh chấp
Vượt thì bị throttle (chậm đi, không chết)
Memory
Cơ sở để xếp hạng khi node thiếu RAM
Vượt thì container bị OOMKilled (exit code 137)
Đơn vị
CPU: 500m = 0.5 core; Memory: 128Mi, 1Gi (hệ 2), 128M là hệ 10
Thiếu field
Chỉ có limits thì requests mặc định bằng limits
Không có limit thì dùng tới hết node (hoặc theo LimitRange của namespace)
Thực tế hay dùng: đặt requests CPU và memory sát mức dùng thường ngày (xem kubectl top), đặt limits.memory, cân nhắc bỏ limits.cpu để tránh throttle khi app có burst ngắn (nhiều team chọn cách này; cluster nhiều tenant thì vẫn giữ limit CPU).
Kubernetes tự gán QoS class cho Pod dựa trên resources của mọi container trong Pod:
QoS class
Điều kiện
Khi node thiếu memory
Guaranteed
Mọi container có cả CPU và memory, requests == limits
Bị đuổi (evict) sau cùng
Burstable
Không đạt Guaranteed nhưng ít nhất một container có request hoặc limit
Pod dùng vượt requests bị đuổi trước
BestEffort
Không container nào có requests/limits
Bị đuổi đầu tiên
kubectl get pod <pod> -o jsonpath='{.status.qosClass}'kubectl get pods -o custom-columns=NAME:.metadata.name,QOS:.status.qosClasskubectl describe node <node> | grep -A10 "Allocated resources" # tổng requests/limits trên node
Có hai cơ chế khác nhau: OOMKilled do container vượt limits.memory của chính nó (restart theo restartPolicy), còn Evicted do kubelet giải phóng tài nguyên khi node bị memory/disk pressure (Pod bị xoá, controller tạo Pod mới).
Dùng khi cần tạo ConfigMap/Secret, đưa vào Pod dưới dạng env hoặc file, và làm app nhận cấu hình mới khi giá trị thay đổi.
Tóm tắt: Tạo bằng kubectl create configmap|secret ... --dry-run=client -o yaml | kubectl apply -f - (idempotent). Đưa vào Pod qua env (env/envFrom, chỉ đọc lúc container start) hoặc volume (file, kubelet tự cập nhật sau một khoảng trễ, trừ subPath). Đổi giá trị env thì phải kubectl rollout restart. Secret chỉ là base64, cần RBAC và encryption at rest.
flowchart LR
F["file / literal / .env"] --> C["ConfigMap / Secret"]
C -->|"env, envFrom"| E["Biến môi trường (đọc 1 lần khi start)"]
C -->|"volume"| V["File trong Pod (tự cập nhật, trễ)"]
C -->|"volume + subPath"| S["File (KHÔNG cập nhật)"]
E --> R["rollout restart để nhận giá trị mới"]
--from-env-file không dùng chung được với --from-literal/--from-file trong cùng một lệnh (error: from-env-file cannot be combined...): tách thành hai ConfigMap hoặc gộp hết vào file .env.
# Cách đơn giản: restart rolling sau khi apply ConfigMap/Secretkubectl apply -f api-config.yaml && kubectl rollout restart deploy/api -n appkubectl rollout status deploy/api -n app# Kiểm tra giá trị Pod đang thấykubectl exec -n app deploy/api -- env | grep LOG_LEVELkubectl exec -n app deploy/api -- cat /etc/api/app.toml
Tự động hoá:
KustomizeconfigMapGenerator/secretGenerator: tên có hậu tố hash nội dung, đổi nội dung là đổi tên, Deployment tự rollout.
Helm: annotation checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} trong Pod template.
Controller như Stakater Reloader theo dõi ConfigMap/Secret và tự restart workload có annotation.
Cấu hình không bao giờ đổi lúc chạy: đặt immutable: true để giảm tải watch cho API server (muốn đổi phải xoá rồi tạo lại).
Dùng khi cần cấp quyền tối thiểu cho ServiceAccount (CI, operator, app gọi API) hoặc người dùng, và kiểm tra quyền bằng kubectl auth can-i.
Tóm tắt: Quyền = ai (User, Group, ServiceAccount) + được làm gì (Role/ClusterRole: apiGroups + resources + verbs) + ở đâu (RoleBinding: một namespace; ClusterRoleBinding: toàn cluster). RBAC chỉ có allow, không có deny. Luôn kiểm tra lại bằng kubectl auth can-i --as=....
Quyền của ClusterRole nhưng chỉ trong namespace của RoleBinding (cách tái dùng phổ biến)
ClusterRole + ClusterRoleBinding
Mọi namespace và resource cấp cluster (node, PV, namespace)
ClusterRole có sẵn hay dùng: view (đọc, trừ Secret), edit (đọc/ghi hầu hết, không sửa RBAC), admin (toàn quyền trong namespace, kể cả RBAC), cluster-admin (mọi thứ).
create trên pods/exec (client mới dùng WebSocket có thể cần thêm get)
kubectl port-forward
create trên pods/portforward
kubectl scale
patch/update trên deployments/scale
Đọc Secret
get trên secrets (list/watch cũng đọc được nội dung)
resourceNames: ["api"] giới hạn rule vào object cụ thể. Không giới hạn được create (tên chưa tồn tại lúc kiểm tra); list/watch chỉ khớp khi client lọc theo metadata.name.
Từ Kubernetes 1.24 ServiceAccount không còn tự có Secret token dài hạn. Nếu bắt buộc cần token không hết hạn (hệ thống ngoài không làm mới được), tạo Secret type: kubernetes.io/service-account-token với annotation kubernetes.io/service-account.name: ci-deploy, và coi nó như mật khẩu.
Pod không cần gọi API thì đặt automountServiceAccountToken: false trong Pod spec hoặc ServiceAccount.
Dùng khi cần cô lập namespace bằng NetworkPolicy, chặn mặc định rồi mở theo label Pod/namespace, kèm egress DNS để Pod vẫn phân giải tên.
Tóm tắt: Pod mặc định nhận/gửi mọi traffic. Khi một Pod bị ít nhất một NetworkPolicy chọn (podSelector) cho chiều Ingress hoặc Egress, chiều đó chuyển thành chỉ cho phép những gì được liệt kê; các policy cộng dồn (OR), không có rule deny. Cách làm chuẩn: default deny cho namespace → allow DNS → allow từng luồng theo label. Cần CNI có enforce NetworkPolicy (Calico, Cilium, kube-router trong k3s); Flannel đơn thuần thì policy bị bỏ qua.
Label kubernetes.io/metadata.name được Kubernetes tự gắn cho mọi namespace (từ 1.21), dùng được để chọn namespace theo tên. CoreDNS ở hầu hết distro (kể cả k3s) có label k8s-app=kube-dns; kiểm tra bằng kubectl get pods -n kube-system --show-labels.
# OR: hai phần tử list → Pod app=api trong CÙNG namespace, HOẶC mọi Pod của namespace monitoring- from: - podSelector: { matchLabels: { app: api } } - namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: monitoring } }# AND: một phần tử có cả hai selector → chỉ Pod app=prometheus TRONG namespace monitoring- from: - namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: monitoring } } podSelector: { matchLabels: { app: prometheus } }
Khác nhau đúng một dấu -. Đây là lỗi NetworkPolicy phổ biến nhất.
Dùng khi cần một cluster Kubernetes nhẹ trên một máy (lab, edge, VPS) với k3s, có sẵn Traefik ingress, ServiceLB và local-path storage.
Tóm tắt: Ghi /etc/rancher/k3s/config.yaml trước, rồi curl -sfL https://get.k3s.io | sh -. k3s là một binary chứa control plane, containerd, Flannel, CoreDNS, Traefik (ingress), ServiceLB (LoadBalancer bằng host port), local-path-provisioner và metrics-server. Kubeconfig ở /etc/rancher/k3s/k3s.yaml. Gỡ bằng /usr/local/bin/k3s-uninstall.sh.
write-kubeconfig-mode: "0640"tls-san: - k3s.example.com # tên/IP sẽ dùng để gọi API từ máy khác - 203.0.113.10node-name: lab-01# disable: # bỏ comment nếu muốn tự cài ingress / LB khác# - traefik# - servicelb
sudo mkdir -p /etc/rancher/k3s # rồi tạo config.yaml như trêncurl -sfL https://get.k3s.io | sh -# Ghim phiên bản / kênh (nên làm cho môi trường cần lặp lại được)curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="v1.31.4+k3s1" sh -curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable sh -sudo systemctl status k3ssudo k3s kubectl get nodes -o widesudo k3s check-config # kiểm tra kernel/cgroup khi k3s không lên
Tham số trong config.yaml tương ứng flag của k3s server (vd disable: [traefik] ≡ --disable=traefik). Đổi config thì sudo systemctl restart k3s.
mkdir -p ~/.kubesudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config && sudo chown "$USER": ~/.kube/configchmod 600 ~/.kube/configkubectl get pods -A# Dùng từ máy khác: đổi server 127.0.0.1 thành tên đã có trong tls-sansed -i 's#https://127.0.0.1:6443#https://k3s.example.com:6443#' ~/.kube/config
k3s.yaml chứa client cert quyền cluster-admin. Đừng write-kubeconfig-mode: "0644" trên máy nhiều user.
Traefik được cài bằng HelmChart trong kube-system. Không sửa file trong /var/lib/rancher/k3s/server/manifests/traefik.yaml (k3s ghi đè khi khởi động); tuỳ biến bằng HelmChartConfig:
StorageClass local-path là mặc định, tạo thư mục trên node dưới /var/lib/rancher/k3s/storage. Dung lượng 5Gikhông được giới hạn thật; dữ liệu gắn với node, không có replication. Binding mode WaitForFirstConsumer: PVC Pending tới khi có Pod dùng nó là bình thường.
Dùng khi cần reverse proxy có HTTPS Let's Encrypt tự cấp và tự gia hạn chỉ với vài dòng Caddyfile, thay cho nginx + certbot.
Tóm tắt: Cài Caddy v2, viết /etc/caddy/Caddyfile với một khối cho mỗi domain và reverse_proxy tới upstream. Chỉ cần DNS trỏ đúng và port 80/443 mở, Caddy tự xin và gia hạn chứng chỉ, tự redirect HTTP sang HTTPS, tự gửi X-Forwarded-*, hỗ trợ WebSocket và HTTP/2, HTTP/3 không cần cấu hình. Kiểm tra bằng caddy validate, áp dụng bằng systemctl reload caddy.
Chứng chỉ và account ACME của bản cài bằng package (user caddy) nằm trong /var/lib/caddy/.local/share/caddy. Chạy Docker thì phải mount volume /data (và /config), nếu không mỗi lần tạo lại container sẽ xin cert mới và dễ dính rate limit.
location /img/ { alias /srv/images/; } → /img/a.png đọc /srv/images/a.png
Liệt kê thư mục
autoindex on;
Với alias, dấu / cuối của location và của alias phải khớp nhau. location /img { alias /srv/images/; } (thiếu / ở location) cho phép /img../ đọc ra ngoài thư mục (path traversal).
return 403; / return 444; (đóng kết nối không trả gì)
Redirect một path, giữ query string
location = /old { return 301 /new$is_args$args; }
Rewrite có capture
rewrite ^/blog/(\d+)$ /posts/$1 permanent;
Rewrite nội bộ rồi tìm location mới
rewrite ^/v1/(.*)$ /api/$1 last;
Rewrite trong location hiện tại, không tìm lại
rewrite ^/api/(.*)$ /$1 break;
Cờ rewrite: last (dừng, tìm location lại), break (dừng, ở lại location), redirect (302), permanent (301). Redirect đơn giản thì ưu tiên return, nhanh và dễ đọc hơn.
$request_time: tổng thời gian từ byte đầu tiên của client tới khi gửi xong. $upstream_response_time: chỉ phần chờ upstream. rt lớn mà urt nhỏ thường là client/mạng chậm.
Tránh if trong location cho logic phức tạp ("if is evil"): chỉ return và rewrite ... last trong if là an toàn. Dùng map ở khối http để chọn giá trị theo biến.
3.21 Argo CD – GitOps với Application, sync policy, App of Apps
Dùng khi muốn Git là nguồn sự thật cho manifest Kubernetes, Argo CD tự đồng bộ cluster theo repo, và cần biết cách rollback an toàn.
Tóm tắt: Cài Argo CD vào namespace argocd, khai báo mỗi ứng dụng bằng một CRD Application (repo + path + cluster đích). syncPolicy.automated với prune và selfHeal để cluster luôn khớp Git. Nhiều app thì dùng App of Apps: một Application gốc trỏ tới thư mục chứa các Application con. Rollback đúng kiểu GitOps là git revert; argocd app rollback chỉ là biện pháp tạm.
flowchart LR
G["Git repo (manifest)"] -->|"poll / webhook"| R["repo-server render"]
R --> C["application-controller so sánh"]
C -->|"OutOfSync"| S["Sync (kubectl apply)"]
S --> K["Cluster đích"]
K -->|"live state"| C
C --> H["Synced + Healthy"]
Argo CD so sánh trạng thái mong muốn (render từ Git: plain YAML, Helm, Kustomize) với live state. Hai trạng thái chính: Sync (Synced/OutOfSync) và Health (Healthy/Progressing/Degraded/Missing).
kubectl create namespace argocdkubectl apply -n argocd --server-side --force-conflicts \ -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yamlkubectl get pods -n argocd# mật khẩu admin ban đầu (xoá Secret này sau khi đổi mật khẩu)argocd admin initial-password -n argocdkubectl port-forward svc/argocd-server -n argocd 8080:443argocd login localhost:8080 --username admin --insecure
stable là branch theo bản phát hành mới nhất; môi trường thật nên ghim theo tag phiên bản. --insecure ở đây chỉ vì port-forward dùng cert tự ký; production đặt Ingress + TLS (xem ingress-nginx + cert-manager).
Thư mục trong repo; Argo CD tự nhận Helm chart / Kustomize / YAML thường
targetRevision
Branch, tag hoặc commit. Ghim tag/commit cho prod nếu cần kiểm soát chặt
destination.server
https://kubernetes.default.svc là cluster chứa Argo CD; cluster khác thêm bằng argocd cluster add
automated.prune
Xoá resource không còn trong Git
automated.selfHeal
Tự đưa về như Git khi có người sửa tay trên cluster
CreateNamespace=true
Tự tạo namespace đích
finalizer resources-finalizer...
Xoá Application thì xoá luôn resource con (cascade). Bỏ finalizer nếu chỉ muốn gỡ app khỏi Argo CD
Repo private: argocd repo add https://git.example.com/platform/deploy.git --username ci --password "$GIT_TOKEN" (hoặc dùng SSH key); không commit token vào Git.
Cách đúng:git revert <commit> rồi push; Argo CD sync lại. Git vẫn là nguồn sự thật, có audit trail.
Khẩn cấp:argocd app rollback không chạy khi automated đang bật. Tắt auto-sync, rollback, điều tra, rồi sửa Git và bật lại; nếu quên thì lần sync kế sẽ đè rollback.
Bật prune: true cùng selfHeal: true nghĩa là xoá/sửa tay trên cluster bị hoàn tác ngay. Với resource nhạy cảm (PVC, namespace) cân nhắc annotation argocd.argoproj.io/sync-options: Prune=false trên resource đó.
MCP là gì, gồm những thành phần nào và khi nào nên viết MCP server riêng cho hệ thống mạng.
MCP là giao thức mở chuẩn hoá cách một ứng dụng AI (host) kết nối tới nguồn dữ liệu và công cụ bên ngoài. Thay vì viết integration riêng cho từng model, bạn viết một MCP server và mọi client hỗ trợ MCP (Claude Code, Claude Desktop, IDE...) đều dùng được.
sequenceDiagram
participant U as Người dùng
participant H as Host (Claude Code)
participant S as MCP server
participant R as Router
U->>H: "Interface nào trên pe1 đang down?"
H->>S: tools/list
S-->>H: [get_interfaces, get_bgp]
H->>S: tools/call get_interfaces(host=pe1)
S->>R: gNMI Get /interfaces
R-->>S: JSON state
S-->>H: kết quả đã lọc
H-->>U: Tóm tắt + đề xuất
Dùng khi cần tự viết MCP server Python cung cấp tool/resource chỉ đọc cho agent, test bằng MCP Inspector rồi gắn vào Claude Code.
Tóm tắt: Cài package mcp, khai báo FastMCP("tên"), gắn @mcp.tool() / @mcp.resource("uri") lên hàm Python có type hint + docstring, chạy mcp.run() (mặc định transport stdio). Test bằng MCP Inspector, rồi claude mcp add để Claude Code dùng. Khái niệm MCP xem MCP tổng quan.
flowchart LR
A["server.py (FastMCP)"] --> B["mcp dev / Inspector"]
B -->|OK| C["claude mcp add"]
C --> D[Claude Code]
D -->|"stdio JSON-RPC"| A
A -->|"ping / show"| E[(Router)]
import ipaddressimport subprocessfrom mcp.server.fastmcp import FastMCPmcp = FastMCP("netops")DEVICES = { "pe1-hni": {"mgmt": "198.51.100.11", "role": "pe", "os": "iosxr"}, "pe2-hcm": {"mgmt": "198.51.100.12", "role": "pe", "os": "junos"},}@mcp.tool()def ping(target: str, count: int = 3) -> str: """Ping một địa chỉ IPv4/IPv6 từ máy chạy server. Chỉ nhận IP, không nhận hostname.""" ip = ipaddress.ip_address(target) # ValueError nếu không phải IP -> trả lỗi cho model count = max(1, min(count, 10)) # giới hạn để không treo out = subprocess.run( ["ping", "-c", str(count), "-W", "1", str(ip)], capture_output=True, text=True, timeout=30, ) return out.stdout[-1500:] or out.stderr@mcp.tool()def list_devices(role: str | None = None) -> list[dict]: """Liệt kê thiết bị trong inventory, lọc theo role (pe, p, rr) nếu có.""" return [ {"name": n, **d} for n, d in DEVICES.items() if role is None or d["role"] == role ]@mcp.resource("inventory://device/{name}")def device(name: str) -> dict: """Thông tin một thiết bị theo tên.""" return DEVICES[name]if __name__ == "__main__": mcp.run() # stdio; HTTP: mcp.run(transport="streamable-http")
Điểm cần nhớ:
Tên hàm thành tên tool, docstring thành description, type hint thành JSON Schema inputSchema. Viết docstring cho model đọc (xem Thiết kế tool cho agent).
Raise exception trong tool thì client nhận kết quả lỗi (isError), model có thể tự sửa tham số.
Với stdio, không print() ra stdout: stdout là kênh JSON-RPC. Log ra stderr (logging mặc định ghi stderr).
Tool chạy với quyền của user chạy server. Chỉ expose lệnh đọc (show, get, ping); nếu cần ghi cấu hình, buộc qua commit confirmed và để Claude Code hỏi xác nhận (không cho vào permissions.allow).
Tra nhanh lệnh CLI, slash command, CLAUDE.md, phím tắt và chế độ permission của Claude Code.
Kiểm chứng trên Claude Code 2.1.x. Flag thay đổi theo phiên bản: luôn đối chiếu claude --help trên máy bạn. Cấu hình permission/hook chi tiết xem Claude Code: settings.json và hooks.
# Repo ansible-network- Lint: `ansible-lint` và `yamllint .` trước khi báo xong.- Không chạy playbook không có `--check` trên group `prod`.- IOS XR: luôn `commit confirmed 300`, Junos: `commit confirmed 5`.- Inventory: inventory/hosts.yml, secret qua ansible-vault, không ghi plaintext.
CLAUDE.md nên ngắn: lệnh build/test, quy ước, điều cấm. Thứ model tự đọc được từ code thì không cần ghi.
4.4 Claude Code: settings.json, permissions và hooks
Dùng khi cần cho phép/chặn lệnh trong Claude Code bằng permissions allow/deny và chạy script tự động trước/sau khi tool chạy (PreToolUse/PostToolUse).
Tóm tắt: Rule permissions.allow/ask/deny quyết định tool nào chạy không cần hỏi, phải hỏi, hay bị chặn. Hook là lệnh shell do harness chạy ở các sự kiện (PreToolUse, PostToolUse...), nhận JSON qua stdin; exit 2 ở PreToolUse sẽ chặn tool và gửi stderr cho Claude. Lệnh nhanh xem Claude Code cheatsheet.
flowchart LR
A[Claude muốn gọi tool] --> B{"permissions deny/ask/allow"}
B -->|deny| X[Bị chặn]
B -->|"allow / user đồng ý"| C[Hook PreToolUse]
C -->|"exit 2"| X
C -->|"exit 0"| D[Tool chạy]
D --> E[Hook PostToolUse]
E --> F[Kết quả về Claude]
Đường dẫn theo pattern kiểu gitignore (** = mọi cấp thư mục)
WebFetch(domain:example.com)
Chỉ một domain
mcp__<server>__<tool>
Một tool MCP; mcp__<server> là mọi tool của server
Thứ tự ưu tiên: deny > ask > allow. Rule Bash so khớp theo tiền tố lệnh, có thể bị lách (bash -c ..., biến môi trường); muốn chặn chắc thì thêm hook hoặc chạy trong sandbox.
matcher: tên tool, hỗ trợ regex (Edit|Write, mcp__netops__.*). Bỏ trống hoặc * = mọi tool.
Hook nhận JSON qua stdin: session_id, cwd, hook_event_name, tool_name, tool_input (Bash: tool_input.command; Edit/Write: tool_input.file_path). PostToolUse có thêm tool_response.
Exit code: 0 = tiếp tục; 2 = chặn (PreToolUse) / báo lại cho Claude (PostToolUse), nội dung stderr được gửi cho Claude; mã khác = lỗi không chặn, chỉ hiện cho người dùng.
Dùng khi cần tự viết agent loop gọi tool (ping, show) bằng Messages API và package anthropic, không qua framework.
Tóm tắt: Khai báo tools (name, description, input_schema) trong client.messages.create. Khi stop_reason == "tool_use", đọc các block tool_use, chạy hàm tương ứng, gửi lại một message user chứa block tool_result (cùng tool_use_id), lặp đến khi stop_reason == "end_turn". Đây chính là vòng ReAct mà LangGraph đóng gói sẵn.
sequenceDiagram
participant A as Code của bạn
participant C as Claude API
participant R as Router
A->>C: messages + tools
C-->>A: stop_reason=tool_use (ping 198.51.100.11)
A->>R: chạy ping
R-->>A: kết quả
A->>C: tool_result (tool_use_id)
C-->>A: stop_reason=end_turn, câu trả lời
import ipaddressimport jsonimport subprocessimport anthropicclient = anthropic.Anthropic() # đọc ANTHROPIC_API_KEY từ envMODEL = "claude-sonnet-5-5"MAX_TURNS = 10TOOLS = [ { "name": "ping", "description": "Ping một địa chỉ IP từ máy quản trị. Dùng để kiểm tra reachability. Chỉ nhận IP, không nhận hostname.", "input_schema": { "type": "object", "properties": { "target": {"type": "string", "description": "IPv4/IPv6, vd 198.51.100.11"}, "count": {"type": "integer", "minimum": 1, "maximum": 10, "default": 3}, }, "required": ["target"], }, }, { "name": "get_device", "description": "Tra inventory theo tên thiết bị, trả về IP quản trị, role, OS.", "input_schema": { "type": "object", "properties": {"name": {"type": "string", "description": "vd pe1-hni"}}, "required": ["name"], }, },]INVENTORY = {"pe1-hni": {"mgmt": "198.51.100.11", "role": "pe", "os": "iosxr"}}def ping(target: str, count: int = 3) -> str: ip = ipaddress.ip_address(target) out = subprocess.run(["ping", "-c", str(count), "-W", "1", str(ip)], capture_output=True, text=True, timeout=30) return out.stdout[-1500:] or out.stderrdef get_device(name: str) -> str: return json.dumps(INVENTORY[name])HANDLERS = {"ping": ping, "get_device": get_device}def run(question: str) -> str: messages = [{"role": "user", "content": question}] for _ in range(MAX_TURNS): resp = client.messages.create( model=MODEL, max_tokens=2048, system="Bạn là trợ lý NOC. Chỉ dùng tool để đọc trạng thái, không đoán.", tools=TOOLS, messages=messages, ) # luôn giữ nguyên content của assistant (gồm cả block tool_use) messages.append({"role": "assistant", "content": resp.content}) if resp.stop_reason != "tool_use": return "".join(b.text for b in resp.content if b.type == "text") results = [] for block in resp.content: if block.type != "tool_use": continue try: output = HANDLERS[block.name](**block.input) results.append({"type": "tool_result", "tool_use_id": block.id, "content": output}) except Exception as e: # trả lỗi cho model tự sửa, không crash loop results.append({"type": "tool_result", "tool_use_id": block.id, "content": f"{type(e).__name__}: {e}", "is_error": True}) # mọi tool_result của một lượt nằm chung trong MỘT message user messages.append({"role": "user", "content": results}) raise RuntimeError("Vượt MAX_TURNS, có thể agent đang lặp")if __name__ == "__main__": print(run("pe1-hni có ping được không?"))
Hiểu StateGraph, node, edge, conditional edge, checkpointer và human-in-the-loop trước khi dựng agent bằng LangGraph.
LangGraph mô tả agent như một đồ thị trạng thái: mỗi node là một hàm nhận state và trả về phần state cần cập nhật; edge quyết định node nào chạy tiếp. Khác với loop tự viết (tool use loop), LangGraph cho sẵn lưu trạng thái theo thread, dừng chờ người duyệt, resume, và stream từng bước.
flowchart LR
S([START]) --> A["agent: gọi LLM"]
A -->|"có tool_calls"| H{"cần duyệt?"}
H -->|"read-only"| T[tools]
H -->|"ghi cấu hình"| I["interrupt: chờ người duyệt"]
I --> T
T --> A
A -->|"không có tool_calls"| E([END])
from langgraph.types import Command, interruptdef push_config(state: State) -> dict: decision = interrupt({"question": "Đẩy config lên pe1-hni?", "diff": state["diff"]}) if decision != "approve": return {"messages": [("system", "Người vận hành từ chối")]} ... # commit confirmed# lần 1: graph dừng tại interrupt, trả payload trong kết quảgraph.invoke(inputs, config)# sau khi người duyệt:graph.invoke(Command(resume="approve"), config)
Cách đơn giản hơn: builder.compile(checkpointer=..., interrupt_before=["tools"]) để dừng trước mọi lần chạy tool.
flowchart LR
U[Câu hỏi NOC] --> A["agent: Claude"]
A -->|tool_calls| T[ToolNode]
T -->|ping| R1[("198.51.100.11")]
T -->|show_bgp_summary| R2[("pe1-hni")]
T --> A
A -->|"không còn tool_calls"| O[Câu trả lời]
import ipaddressimport subprocessfrom langchain_core.tools import toolINVENTORY = { "pe1-hni": {"mgmt": "198.51.100.11", "os": "iosxr"}, "pe2-hcm": {"mgmt": "198.51.100.12", "os": "junos"},}SHOW_BGP = {"iosxr": "show bgp summary", "junos": "show bgp summary"}@tooldef ping(target: str, count: int = 3) -> str: """Ping một địa chỉ IP (không nhận hostname). Dùng để kiểm tra reachability.""" ip = ipaddress.ip_address(target) out = subprocess.run(["ping", "-c", str(min(count, 10)), "-W", "1", str(ip)], capture_output=True, text=True, timeout=30) return out.stdout[-1500:] or out.stderr@tooldef show_bgp_summary(device: str) -> str: """Chạy 'show bgp summary' (chỉ đọc) trên thiết bị theo tên inventory, vd pe1-hni.""" d = INVENTORY.get(device) if d is None: return f"Không có thiết bị {device}. Thiết bị hợp lệ: {', '.join(INVENTORY)}" return run_show(d["mgmt"], d["os"], SHOW_BGP[d["os"]])def run_show(host: str, os_name: str, command: str) -> str: # Gọi netmiko/scrapli/PyEZ ở đây; chỉ cho phép lệnh show if not command.startswith("show "): raise ValueError("chỉ cho phép lệnh show") raise NotImplementedError
from langchain_anthropic import ChatAnthropicfrom langgraph.checkpoint.memory import InMemorySaverfrom langgraph.prebuilt import create_react_agentfrom net_tools import ping, show_bgp_summarymodel = ChatAnthropic(model="claude-sonnet-5-5", temperature=0)agent = create_react_agent( model, tools=[ping, show_bgp_summary], prompt="Bạn là trợ lý NOC. Chỉ dùng tool đọc, trả lời ngắn, nêu rõ lệnh đã chạy.", checkpointer=InMemorySaver(),)config = {"configurable": {"thread_id": "noc-1"}, "recursion_limit": 20}result = agent.invoke({"messages": [("user", "pe1-hni có ping được không?")]}, config)print(result["messages"][-1].content)
Ở LangGraph 1.x, create_react_agent được đánh dấu deprecated, hướng thay thế là create_agent trong package langchain. Kiểm tra phiên bản bạn cài; Cách 2 bên dưới không phụ thuộc vào thay đổi này.
Gọi lại graph.invoke(..., config) với cùng thread_id thì agent nhớ thiết bị đang bàn. Muốn duyệt trước khi chạy tool: builder.compile(checkpointer=..., interrupt_before=["tools"]).
Nguyên tắc đặt tên, viết mô tả, schema, giới hạn quyền và định dạng output để agent gọi tool đúng và an toàn trên hạ tầng mạng.
Model chỉ "thấy" tool qua tên, mô tả và JSON Schema. Tool tốt cho agent khác API tốt cho con người: ít tool, mỗi tool làm trọn một việc có ý nghĩa, output gọn, lỗi có hướng dẫn sửa. Nguyên tắc dưới đây áp dụng như nhau cho MCP server, Claude API tool use và LangGraph.
flowchart LR
A["Model đọc tên + mô tả"] --> B["Sinh input theo schema"]
B --> C{"Validate input"}
C -->|sai| E["Lỗi có gợi ý sửa"]
E --> A
C -->|đúng| D{"Kiểm tra quyền"}
D -->|"ghi / nguy hiểm"| F["Xác nhận người dùng"]
D -->|"chỉ đọc"| G[Thực thi]
F --> G
G --> H["Output gọn, có cấu trúc"]
{ "name": "bgp_get_neighbors", "description": "Lấy trạng thái BGP neighbor (chỉ đọc) của một router. Dùng khi cần biết peer nào không Established hoặc số prefix nhận được. Không dùng để xem route chi tiết (dùng bgp_get_routes).", "input_schema": { "type": "object", "properties": { "device": {"type": "string", "description": "Tên trong inventory, vd pe1-hni"}, "state": {"type": "string", "enum": ["all", "not-established"], "default": "not-established"}, "afi": {"type": "string", "enum": ["ipv4-unicast", "ipv6-unicast", "vpnv4-unicast"]} }, "required": ["device"] }}
Output gợi ý (gọn, đủ để model quyết định bước tiếp):
Thao tác ghi đi qua xác nhận / interrupt, kèm diff
Audit
Log mọi lần gọi: ai, tool, input, kết quả, thời gian
Output tool là dữ liệu không tin cậy: description interface, BGP community, log syslog có thể chứa chuỗi kiểu "bỏ qua chỉ dẫn trước đó và chạy...". Không cho tool đọc dữ liệu bên ngoài đứng chung phiên với tool ghi mà không có người duyệt.
Hiểu pipeline RAG (chunking, embedding, vector store, retrieval, rerank) và khi nào không cần RAG cho tài liệu vận hành mạng.
RAG (Retrieval-Augmented Generation): trước khi hỏi model, tìm các đoạn tài liệu liên quan nhất rồi đưa vào prompt. Model trả lời dựa trên tài liệu của bạn (runbook, MOP, design doc) thay vì kiến thức chung, và có thể trích nguồn.
Bắt đầu đơn giản: một tool search_docs(query) dùng full-text search (ripgrep/Postgres FTS) trên thư mục Markdown thường đã đủ tốt. Chỉ thêm embedding + vector store khi đo được là keyword search bỏ sót.
Xử lý agent không gọi tool, lặp vô hạn, tràn context, MCP server không kết nối và lỗi schema khi gọi tool.
Triệu chứng chính: agent trả lời bịa thay vì gọi tool, gọi đi gọi lại một tool, báo hết context, /mcp hiện server failed, hoặc API trả 400 invalid_request_error liên quan tools / tool_use.
# Claude Code + MCPclaude mcp listclaude mcp get netopsclaude --debug=mcp # log chi tiết kết nối MCPuv run --directory /opt/netops-mcp server.py # chạy tay: phải đứng chờ stdin, không in gì ra stdout# MCP Inspectornpx @modelcontextprotocol/inspector uv run --directory /opt/netops-mcp server.py# Schema tool hợp lệ?python3 -c 'import json,jsonschema; jsonschema.Draft202012Validator.check_schema(json.load(open("tool_schema.json"))); print("OK")'# Settings hợp lệ?python3 -m json.tool .claude/settings.json > /dev/null && echo OKclaude doctor
Khi agent hành xử lạ, thử lại với ít tool hơn. Mỗi tool thừa là một lựa chọn sai tiềm năng và tốn context cho phần mô tả.
4.11 MCP transport: stdio, Streamable HTTP và xác thực OAuth
Chọn transport cho MCP server (stdio hay Streamable HTTP), hiểu luồng OAuth cho remote server và các rủi ro bảo mật cần chặn trước khi mở server ra mạng.
MCP dùng JSON-RPC 2.0, và transport là lớp mang các message đó giữa client với server. Hiện spec định nghĩa hai transport chuẩn: stdio cho server chạy cục bộ và Streamable HTTP cho server từ xa. Transport HTTP+SSE cũ (spec 2024-11-05) đã bị thay thế, chỉ còn giữ để tương thích ngược. Khái niệm chung về MCP xem MCP tổng quan.
Theo spec (từ bản 2025-06-18), MCP server là resource server của OAuth: nó chỉ kiểm tra token. Token do một authorization server riêng cấp (Keycloak, Entra ID, Okta...).
sequenceDiagram
participant C as MCP client
participant S as MCP server
participant A as Authorization server
C->>S: POST /mcp (không có token)
S-->>C: "401 + WWW-Authenticate (resource_metadata)"
C->>S: "GET /.well-known/oauth-protected-resource"
S-->>C: authorization_servers
C->>A: Metadata, đăng ký client, login (PKCE)
A-->>C: access token (audience = MCP server)
C->>S: "POST /mcp + Authorization: Bearer"
S-->>C: kết quả JSON-RPC
Thành phần
Chuẩn
Ghi chú
Protected Resource Metadata
RFC 9728
Server công bố authorization server nào cấp token cho nó
Authorization Server Metadata
RFC 8414
Client tự tìm endpoint authorize/token
PKCE
OAuth 2.1
Bắt buộc, kể cả với public client
Resource Indicators
RFC 8707
Client gửi tham số resource để token gắn đúng audience
Dynamic Client Registration
RFC 7591
Không bắt buộc. Nếu không có, cấu hình sẵn client id (Claude Code: --client-id)
Phía server bắt buộc kiểm tra audience của token: chỉ chấp nhận token được cấp cho chính nó. Server không được chuyển tiếp token của client sang API phía sau (spec cấm "token passthrough"). Khi cần gọi NetBox hay router, server dùng credential riêng của nó.
stdio: mặc định cho tool cá nhân và server chỉ đọc chạy ngay trên máy có quyền vào thiết bị. Đơn giản, không phải lo mở cổng.
Streamable HTTP + OAuth: khi nhiều người hoặc nhiều agent cùng dùng một server, cần quản lý quyền theo user, cần audit tập trung, hoặc server phải nằm trong vùng mạng quản trị.
HTTP + API key tĩnh: chỉ dùng trong mạng nội bộ, tạm thời. Phải xoay vòng key và đặt sau TLS.
Dùng khi cần viết MCP server bằng Node.js/TypeScript với @modelcontextprotocol/sdk, khai báo tool bằng zod schema, build rồi gắn vào Claude Code.
Tóm tắt: Cài @modelcontextprotocol/sdk và zod. Tạo new McpServer({ name, version }), gọi server.registerTool(name, { description, inputSchema: { field: z... } }, handler), sau đó server.connect(new StdioServerTransport()). Handler trả về { content: [{ type: "text", text }] }, và thêm isError: true khi lỗi. Bản Python tương đương xem FastMCP.
Ví dụ dưới đây đã chạy thử với @modelcontextprotocol/sdk1.29.0, zod 3.25 và Node 22. Ở bản này server.tool(...) đã bị đánh dấu deprecated, nên dùng registerTool. SDK đang được phát triển nhanh, có thể có major version mới đổi tên package hoặc import path. Nâng cấp thì đọc README/changelog trước.
flowchart LR
A["src/index.ts"] -->|"tsc"| B["build/index.js"]
B --> C["MCP Inspector"]
C -->|OK| D["claude mcp add"]
D --> E[Claude Code]
E -->|"stdio JSON-RPC"| B
B -->|"ping / inventory"| F[(Router)]
import { execFile } from "node:child_process";import { isIP } from "node:net";import { promisify } from "node:util";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { z } from "zod";const run = promisify(execFile);const DEVICES: Record<string, { mgmt: string; role: string; os: string }> = { "pe1-hni": { mgmt: "198.51.100.11", role: "pe", os: "iosxr" }, "pe2-hcm": { mgmt: "198.51.100.12", role: "pe", os: "junos" },};const server = new McpServer({ name: "netops-ts", version: "0.1.0" });server.registerTool( "get_device", { title: "Tra inventory", description: "Tra thiết bị theo tên, trả về IP quản trị, role, OS. Chỉ đọc.", inputSchema: { name: z.string().describe("Tên trong inventory, vd pe1-hni") }, annotations: { readOnlyHint: true }, }, async ({ name }) => { const dev = DEVICES[name]; if (!dev) { const valid = Object.keys(DEVICES).join(", "); return { content: [{ type: "text", text: `Không có ${name}. Hợp lệ: ${valid}` }], isError: true }; } return { content: [{ type: "text", text: JSON.stringify(dev) }] }; },);server.registerTool( "ping", { title: "Ping", description: "Ping một IPv4/IPv6 từ máy chạy server. Chỉ nhận IP, không nhận hostname.", inputSchema: { target: z.string().refine((s) => isIP(s) !== 0, "target phải là địa chỉ IP"), count: z.number().int().min(1).max(10).default(3), }, annotations: { readOnlyHint: true, openWorldHint: true }, }, async ({ target, count }) => { try { // execFile không qua shell, không có nguy cơ chèn lệnh const { stdout } = await run("ping", ["-c", String(count), "-W", "1", target], { timeout: 30_000 }); return { content: [{ type: "text", text: stdout.slice(-1500) }] }; } catch (err: any) { // ping trả exit code khác 0 khi mất gói: vẫn trả output cho model đọc return { content: [{ type: "text", text: String(err.stdout || err.message).slice(-1500) }], isError: true }; } },);const transport = new StdioServerTransport();await server.connect(transport);console.error("netops-ts MCP server đang chạy trên stdio"); // stderr, KHÔNG dùng console.log
Những điểm cần nhớ:
inputSchema là object gồm các field zod (raw shape). SDK tự sinh JSON Schema cho tools/list và validate input trước khi gọi handler. Input sai thì client nhận lỗi Input validation error kèm message của refine.
.describe() trên từng field thành description trong schema. Model dựa vào đó để điền tham số, nên viết rõ (xem thiết kế tool).
annotations.readOnlyHint chỉ là gợi ý cho client, không thay được việc kiểm tra quyền trong code.
Import phải có đuôi .js (.../server/mcp.js) vì project dùng ESM với moduleResolution: Node16.
claude mcp add netops-ts -- node /opt/netops-mcp-ts/build/index.jsclaude mcp add netops-ts -s project -- node build/index.js # ghi vào .mcp.json cho cả teamclaude mcp list
Nếu cần chạy server qua HTTP, SDK có StreamableHTTPServerTransport (@modelcontextprotocol/sdk/server/streamableHttp.js) để gắn vào Express hoặc node:http. Trước khi mở server ra mạng, đọc transport và xác thực.
Dùng khi muốn đóng gói quy trình lặp lại (review playbook, đọc log, kiểm tra BGP) thành subagent, skill hoặc slash command riêng của project trong Claude Code.
Tóm tắt:Subagent là file .claude/agents/<tên>.md gồm frontmatter (name, description, tools, model) và phần thân là system prompt. Subagent chạy trong context riêng và chỉ trả kết quả tóm tắt về phiên chính. Skill là thư mục .claude/skills/<tên>/ có SKILL.md (frontmatter name, description) cùng các file phụ. Model tự nạp skill khi description khớp với việc đang làm, hoặc bạn gọi bằng /<tên>. Slash command là file .claude/commands/<tên>.md và nhận tham số qua $ARGUMENTS. Kiểm chứng trên Claude Code 2.1.x.
flowchart LR
U[Yêu cầu của bạn] --> M[Phiên chính]
M -->|"description khớp"| S["Skill: nạp SKILL.md vào context"]
M -->|"giao việc"| A["Subagent: context riêng, tool giới hạn"]
A -->|"chỉ trả tóm tắt"| M
U -->|"/lint-playbook"| C["Command / skill gọi tay"]
C --> M
Subagent
Skill
Slash command
Vị trí (project)
.claude/agents/*.md
.claude/skills/<tên>/SKILL.md
.claude/commands/*.md
Vị trí (user)
~/.claude/agents/
~/.claude/skills/
~/.claude/commands/
Context
Riêng, không làm đầy phiên chính
Nạp vào phiên chính
Nạp vào phiên chính
Ai kích hoạt
Model tự giao việc, hoặc bạn yêu cầu đích danh
Model tự nạp, hoặc /<tên>
Chỉ bạn, /<tên>
Hợp với
Việc đọc nhiều file/log rồi chỉ cần kết luận, cần giới hạn tool
---name: config-reviewerdescription: Review diff cấu hình IOS XR/Junos và playbook Ansible trước khi đẩy. Dùng chủ động sau khi sửa file trong roles/ hoặc configs/.tools: Read, Grep, Glob, Bashmodel: sonnet---Bạn là reviewer cấu hình mạng. Chỉ đọc, không sửa file.Kiểm tra:1. Thay đổi BGP/route-policy có thể làm rơi session hoặc leak route không.2. Có xoá interface, VRF, ACL đang được tham chiếu không.3. Playbook có `check_mode`/`--check` và `commit confirmed` không.Trả về danh sách: mức độ (block/warn/info), file:dòng, lý do, đề xuất sửa.
Field
Ý nghĩa
name
Định danh, chữ thường và dấu -
description
Quan trọng nhất: model dựa vào đây để quyết định giao việc. Ghi rõ "dùng khi nào".
tools
Danh sách tool cho phép, cách nhau bằng dấu phẩy. Bỏ trống thì subagent thừa hưởng toàn bộ tool của phiên chính, gồm cả MCP.
model
sonnet, opus, haiku, inherit (theo phiên chính)
Cách dùng:
> Dùng subagent config-reviewer để review thay đổi trong roles/bgp/agents # tạo, sửa, xem danh sách subagent bằng giao diện
# định nghĩa subagent tạm thời cho một phiên (CI), không cần fileclaude -p "review diff hiện tại" \ --agents '{"config-reviewer": {"description": "Review diff cấu hình mạng", "prompt": "Bạn là reviewer cấu hình mạng, chỉ đọc."}}'
.claude/skills/bgp-health/├── SKILL.md├── checklist.md # tài liệu phụ, chỉ đọc khi cần└── scripts/ └── parse_bgp.py # script model có thể chạy
.claude/skills/bgp-health/SKILL.md
---name: bgp-healthdescription: Kiểm tra sức khoẻ BGP từ output show bgp summary của IOS XR/Junos. Dùng khi người dùng dán output BGP hoặc hỏi peer nào đang down.allowed-tools: Read, Bash(python3 *)---# BGP health check1. Lưu output người dùng dán vào file tạm.2. Chạy `python3 .claude/skills/bgp-health/scripts/parse_bgp.py <file>`, script in JSON gồm peer, state, prefix.3. Đối chiếu với [checklist.md](checklist.md) và báo cáo peer không Established, prefix tụt bất thường.
Model chỉ thấy name + description của mọi skill. Nội dung SKILL.md chỉ được nạp khi cần, file phụ đọc sau nữa. Nhờ vậy có thể cài nhiều skill mà không tốn context.
allowed-tools: tool được dùng không cần hỏi khi skill đang chạy.
disable-model-invocation: true: model không tự nạp skill, chỉ chạy khi bạn gõ /<tên>. Hợp với thao tác có side-effect.
---description: Lint một playbook Ansible và tóm tắt lỗiargument-hint: <đường dẫn playbook>allowed-tools: Bash(ansible-lint *), Bash(yamllint *)---Chạy `yamllint $ARGUMENTS` và `ansible-lint $ARGUMENTS`.Gom lỗi theo file, giải thích ngắn từng rule, đề xuất bản sửa nhưng KHÔNG tự sửa.
> /lint-playbook playbooks/bgp.yml
$ARGUMENTS là toàn bộ chuỗi sau tên lệnh, còn $1, $2 là từng tham số theo vị trí. Trong các bản Claude Code gần đây, command và skill dùng chung cơ chế /<tên>. Command cũ vẫn chạy, nhưng skill dùng được thêm file phụ.
Nên và không nên ghi gì vào CLAUDE.md, chia theo phạm vi user/project/thư mục con, để Claude Code làm đúng quy ước repo mà không tốn context.
CLAUDE.md là file Markdown mà Claude Code tự nạp vào context ở mỗi phiên. Vì lần nào cũng được đọc, mỗi dòng trong đó tốn token ở mọi lượt và cạnh tranh sự chú ý với yêu cầu thật của bạn. CLAUDE.md tốt thì ngắn, cụ thể và kiểm chứng được. Vị trí file và lệnh /init, /memory đã có trong Claude Code cheatsheet. Trang này bàn về nội dung.
flowchart TD
A["Managed (do admin cài)"] --> D[Context phiên]
B["~/.claude/CLAUDE.md (user)"] --> D
C["./CLAUDE.md (project, commit)"] --> D
E["subdir/CLAUDE.md"] -->|"nạp khi làm việc trong subdir"| D
F["@docs/x.md (import)"] --> C
D --> G[Model]
H["settings.json + hooks"] -->|"cưỡng chế, không phụ thuộc model"| G
CLAUDE.md là chỉ dẫn: model thường làm theo nhưng không có gì bảo đảm. Điều gì bắt buộc (cấm git push --force, cấm commit lên router prod) thì đặt vào permissions.deny hoặc hook (settings và hooks). CLAUDE.md chỉ nên giải thích lý do.
Sở thích cá nhân dùng ở mọi repo: ngôn ngữ trả lời, công cụ quen dùng
Bạn
./CLAUDE.md
Quy ước team, lệnh build/test, điều cấm của repo
Cả team, review qua PR
roles/bgp/CLAUDE.md
Quy tắc chỉ đúng trong thư mục con: "role này hỗ trợ cả IOS XR và Junos, test cả hai"
Người phụ trách thư mục
@path/file.md (import)
Tách tài liệu dài, chỉ import thứ thật sự cần ở mọi phiên
Cả team
Monorepo nhiều mảng (ansible/, terraform/, mcp-servers/) thì giữ CLAUDE.md gốc thật ngắn và đặt CLAUDE.md riêng trong từng thư mục. Model chỉ nạp phần liên quan khi làm việc trong thư mục đó.
# netauto## Lệnh- Lint: `make lint` (yamllint + ansible-lint). Test: `make test`. Chạy cả hai trước khi báo xong.- Python: `uv run ...`, không `pip install` vào system.## Quy ước- Hostname: `<role><n>-<site>`, vd `pe1-hni`. Biến thiết bị trong `host_vars/<hostname>.yml`.- Template Jinja2 cho IOS XR ở `templates/iosxr/`, Junos ở `templates/junos/` (dạng `set`).## Điều cấm- Không chạy playbook lên group `prod` nếu thiếu `--check --diff`. Lý do: không có lab cho mọi platform.- Không sửa `inventory/prod/`: file này sinh tự động từ NetBox.
Ví dụ trên khoảng 15 dòng, và mỗi dòng đều thay đổi hành vi của model.
Dùng khi agent gửi lặp lại system prompt, tool hoặc tài liệu dài (runbook, config mẫu) và muốn giảm chi phí, độ trễ bằng cache_control.
Tóm tắt: Đặt cache_control: {"type": "ephemeral"} lên block cuối cùng của phần prefix không đổi. Thứ tự prefix luôn là tools → system → messages. Lần gọi đầu, phần đó được ghi vào cache (usage.cache_creation_input_tokens). Các lần sau, trong thời gian TTL (mặc định 5 phút, mỗi lần hit lại được làm mới), prefix giống hệt sẽ đọc từ cache (usage.cache_read_input_tokens) với giá rẻ hơn nhiều. Vòng tool use cơ bản xem Claude API tool use.
flowchart LR
A["Request: tools + system + messages"] --> B{"Prefix đến breakpoint đã có trong cache?"}
B -->|"có"| C["cache_read_input_tokens (rẻ)"]
B -->|"không"| D["cache_creation_input_tokens (ghi cache)"]
C --> E["Phần sau breakpoint: input_tokens"]
D --> E
E --> F[Model sinh output]
Cache khớp theo prefix chính xác. Chỉ cần đổi một ký tự trong tools hoặc system thì mọi breakpoint phía sau đều bị miss.
Đặt breakpoint ở block cuối của message mới nhất. Ở lượt sau, toàn bộ lịch sử trước đó sẽ là cache hit. Cách gọn nhất là dùng tham số top-levelcache_control (có trong SDK Python hiện hành, đã kiểm tra với anthropic 0.109). SDK sẽ tự gắn breakpoint vào block cuối có thể cache của request:
def with_cache_on_last(messages): """Gắn cache_control vào block cuối của message cuối, không sửa list gốc.""" msgs = [dict(m) for m in messages] last = msgs[-1] content = last["content"] if isinstance(content, str): content = [{"type": "text", "text": content}] content = [dict(b) if isinstance(b, dict) else b.model_dump(exclude_none=True) for b in content] content[-1]["cache_control"] = {"type": "ephemeral"} last["content"] = content return msgsresp = client.messages.create(model="claude-sonnet-5-5", max_tokens=2048, system=SYSTEM, tools=TOOLS, messages=with_cache_on_last(messages))
Dùng khi cần Claude trả dữ liệu đúng schema (phân loại alarm, trích xuất từ syslog, parse output show) để code xử lý tiếp, có validate bằng pydantic.
Tóm tắt: Định nghĩa model pydantic, rồi lấy Model.model_json_schema() làm input_schema của một tool "giả". Ép model gọi tool đó bằng tool_choice={"type": "tool", "name": ...}, sau đó đọc block.input và kiểm tra lại bằng Model.model_validate(). Validate lỗi thì gửi lỗi về dưới dạng tool_result có is_error: true để model sửa. Cách thứ hai là structured outputs (output_config.format / client.messages.parse). Kiến thức tool use cơ bản xem Claude API tool use.
flowchart LR
A["Pydantic model"] -->|"model_json_schema()"| B["tool.input_schema"]
B --> C["messages.create + tool_choice ép tool"]
C --> D["block tool_use: input (dict)"]
D --> E{"model_validate"}
E -->|"OK"| F["Object Python"]
E -->|"ValidationError"| G["tool_result is_error"]
G --> C
Cách 1: tool use + pydantic (chạy trên mọi model hỗ trợ tool)#
uv add anthropic pydantic
extract.py
from typing import Literalimport anthropicfrom pydantic import BaseModel, Field, ValidationErrorclient = anthropic.Anthropic()MODEL = "claude-sonnet-5-5"class Interface(BaseModel): name: str = Field(description="Tên interface, vd HundredGigE0/0/0/1") oper_status: Literal["up", "down"] description: str | None = Noneclass LinkDownReport(BaseModel): device: str = Field(description="Hostname thiết bị") severity: Literal["critical", "major", "minor"] interfaces: list[Interface] summary: str = Field(description="Một câu tóm tắt cho NOC")TOOL = { "name": "report_link_down", "description": "Ghi nhận kết quả phân tích log link down. Luôn gọi tool này để trả kết quả.", "input_schema": LinkDownReport.model_json_schema(),}def extract(log_text: str, max_attempts: int = 2) -> LinkDownReport: messages = [{"role": "user", "content": f"Phân tích syslog sau:\n\n{log_text}"}] for _ in range(max_attempts): resp = client.messages.create( model=MODEL, max_tokens=1024, tools=[TOOL], tool_choice={"type": "tool", "name": "report_link_down"}, # ép trả qua tool messages=messages, ) block = next(b for b in resp.content if b.type == "tool_use") try: return LinkDownReport.model_validate(block.input) except ValidationError as e: # trả lỗi validate cho model sửa ở lượt sau messages.append({"role": "assistant", "content": resp.content}) messages.append({"role": "user", "content": [{ "type": "tool_result", "tool_use_id": block.id, "is_error": True, "content": f"JSON không hợp lệ, sửa và gọi lại tool:\n{e}", }]}) raise RuntimeError("Model không trả JSON hợp lệ sau nhiều lần thử")if __name__ == "__main__": log = "RP/0/RP0/CPU0:Nov 6 10:01:02 pe1-hni ifmgr: Interface HundredGigE0/0/0/1, changed state to Down" print(extract(log).model_dump_json(indent=2))
Những điểm cần nhớ:
tool_choice kiểu tool bắt buộc model gọi đúng tool đó, nên response không có đoạn văn "Đây là kết quả...". Vì vậy stop_reason sẽ là tool_use.
block.input đã là dict, không cần json.loads.
Schema là gợi ý mạnh, không phải bảo đảm: vẫn phải model_validate. Lỗi hay gặp là sai enum, thiếu field bắt buộc, hoặc kiểu số trả về dạng chuỗi.
Field(description=...) đi vào schema. Mô tả càng cụ thể (đơn vị, ví dụ) thì kết quả càng đúng.
Cách 2: structured outputs (output_config.format)#
SDK Python hiện hành (đã kiểm tra anthropic 0.109) có client.messages.parse(). Hàm này nhận class pydantic, tự chuyển thành output_config={"format": {"type": "json_schema", "schema": ...}}, và trả object đã parse trong parsed_output:
resp = client.messages.parse( model="claude-sonnet-5-5", max_tokens=1024, messages=[{"role": "user", "content": f"Phân tích syslog sau:\n\n{log}"}], output_format=LinkDownReport,)report = resp.parsed_output # LinkDownReport hoặc None
Ngoài ra có thể đặt "strict": True trong định nghĩa tool để input của tool tuân thủ schema chặt hơn. Hai tính năng này hỗ trợ một tập con của JSON Schema và một số model nhất định. Kiểm tra docs trước khi dùng schema phức tạp ($ref lồng sâu, pattern, minLength...).
Tiêu chí
Tool use + validate
output_config.format / parse
Tương thích model/SDK cũ
Cao
Cần model và SDK mới
Bảo đảm đúng schema
Không, phải validate và retry
Chặt hơn, vẫn nên validate
Kết hợp với tool khác trong agent loop
Tự nhiên (thêm một tool "report")
Output cuối là JSON, tool khác vẫn gọi bình thường
Cách dựng bộ eval cho agent vận hành mạng (test case, metric, LLM-as-judge, regression) để đổi prompt, model hay tool mà vẫn biết agent tốt lên hay tệ đi.
Agent không có "unit test pass/fail" rõ ràng như code thường: cùng một input có thể ra output khác nhau, và một thay đổi nhỏ ở prompt hoặc mô tả tool có thể làm hỏng một case trước đó vẫn chạy đúng. Eval là bộ test case cố định kèm cách chấm điểm tự động, chạy lại mỗi khi đổi prompt, model, tool hay phiên bản SDK.
flowchart LR
A["Bộ case (JSONL)"] --> B["Chạy agent (tool mock / lab)"]
B --> C["Transcript: tool calls + output"]
C --> D["Grader code"]
C --> E["LLM-as-judge"]
D --> F["Điểm từng case"]
E --> F
F --> G{"So với baseline"}
G -->|"tụt"| H["Chặn merge, xem case fail"]
G -->|"ổn"| I["Cập nhật baseline"]
Mỗi sự cố thật (agent trả sai, gọi sai tool) thì thêm một case vào bộ eval trước khi sửa prompt.
Chạy eval trong CI khi đổi prompt, mô tả tool, model id, phiên bản SDK. Bộ case nhanh dùng tool mock, bộ case chậm chạy trên lab (containerlab) theo lịch.
Lưu transcript đầy đủ của mỗi lần chạy để so sánh diff khi một case chuyển từ pass sang fail.
So sánh với baseline theo từng case, không chỉ nhìn điểm trung bình: tổng tăng 2% nhưng một case an toàn bị fail thì vẫn là regression.
Lộ trình và các lớp bảo vệ (read-only trước, dry-run, approval gate, commit confirmed, audit log, giới hạn blast radius) trước khi cho agent đẩy cấu hình lên router production.
Agent có thể hiểu sai yêu cầu, bị prompt injection qua dữ liệu (description interface, syslog, ticket), hoặc tự tin đề xuất một lệnh sai. Trên mạng thật, một lệnh sai có thể làm mất kết nối cả site, kể cả kết nối quản trị dùng để sửa lại. Nguyên tắc: model không phải ranh giới an toàn. Mọi giới hạn phải nằm ở tầng tool, tài khoản thiết bị và quy trình, những nơi model không thể vượt qua.
flowchart LR
A["Agent đề xuất intent"] --> B["Tool sinh config + diff"]
B --> C["Dry-run: commit check / check mode"]
C --> D{"Người duyệt xem diff"}
D -->|"từ chối"| X[Dừng]
D -->|"duyệt"| E["Commit confirmed (timer)"]
E --> F["Verify: BGP, interface, ping"]
F -->|"OK"| G["Confirm commit"]
F -->|"lỗi / hết giờ"| H["Tự rollback"]
G --> I["Audit log"]
H --> I
Bất kỳ trường văn bản nào có thể bị người khác sửa đều là đầu vào không tin cậy: interface description, BGP neighbor description, SNMP sysContact, syslog message, nội dung ticket, trang wiki.
Coi output của tool là dữ liệu, không phải lệnh. Nhắc điều này trong system prompt, nhưng không chỉ dựa vào đó.
Tool ghi luôn đi qua approval: dù model bị lừa thì vẫn có người chặn.
Phiên đọc dữ liệu không tin cậy thì không cấp tool ghi trong cùng phiên đó.
Lọc và cắt ngắn trường văn bản tự do trước khi trả cho model.
MAX_DIFF_LINES = 40PROTECTED = ("MgmtEth0/RP0/CPU0/0", "fxp0", "em0")def propose_change(device: str, intent: dict) -> dict: """Chỉ sinh config + diff, không commit. Trả change_id để người duyệt.""" config = render_template(intent) # template, không nhận config tự do từ model if any(p in config for p in PROTECTED): raise PermissionError("Không được thay đổi interface quản trị") diff = device_dry_run(device, config) # commit check / show commit changes diff if len(diff.splitlines()) > MAX_DIFF_LINES: raise ValueError(f"Diff {len(diff.splitlines())} dòng, vượt giới hạn {MAX_DIFF_LINES}") change_id = save_pending(device, config, diff) # lưu, chờ duyệt audit("propose", device=device, change_id=change_id, diff=diff) return {"change_id": change_id, "diff": diff, "status": "pending_approval"}def apply_change(change_id: str, approver: str) -> dict: """Chỉ gọi được từ UI/CLI của người duyệt, KHÔNG expose cho model.""" change = load_pending(change_id) commit_confirmed(change.device, change.config, minutes=5) ok = verify(change.device) (confirm if ok else rollback)(change.device) audit("apply", change_id=change_id, approver=approver, verified=ok) return {"applied": ok}
Các hàm render_template, device_dry_run, commit_confirmed... là phần bạn tự hiện thực bằng NETCONF/PyEZ/Ansible. Điểm chính: apply_changekhông nằm trong danh sách tool của agent.
Tra nhanh lệnh pgvector (extension, kiểu vector, index HNSW/IVFFlat, toán tử khoảng cách) và Qdrant (collection, upsert, search có filter) khi dựng RAG.
Lệnh hay dùng để lưu và truy vấn embedding. Pipeline RAG tổng thể (chunking, embedding, rerank) xem RAG cơ bản. Số chiều trong ví dụ là 1024: thay bằng số chiều của model embedding bạn dùng.
SELECT extversion FROM pg_extension WHERE extname = 'vector';
Chạy nhanh bằng Docker
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=$PGPASSWORD pgvector/pgvector:pg17
Cột embedding
embedding vector(1024)
Tiết kiệm bộ nhớ (float16)
embedding halfvec(1024)
CREATE TABLE doc_chunks ( id bigserial PRIMARY KEY, source text NOT NULL, -- vd runbook/bgp.md vendor text, -- iosxr / junos, dùng để filter content text NOT NULL, embedding vector(1024) NOT NULL);INSERT INTO doc_chunks (source, vendor, content, embedding)VALUES ('runbook/bgp.md', 'iosxr', 'Kiểm tra hold timer...', '[0.012, -0.034, ...]');
-- top 5 chunk gần nhất theo cosine, có filterSELECT id, source, 1 - (embedding <=> $1) AS similarityFROM doc_chunksWHERE vendor = 'iosxr'ORDER BY embedding <=> $1LIMIT 5;
Index chỉ được dùng khi có ORDER BY embedding <op> ... LIMIT n với đúng toán tử khớp operator class của index. Embedding đã chuẩn hoá (norm = 1) thì cosine và inner product cho cùng thứ tự kết quả.
CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops);
HNSW chỉnh tham số
... USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
Tăng recall khi query HNSW
SET hnsw.ef_search = 100; (mặc định 40)
IVFFlat (build nhanh, nên tạo sau khi đã có dữ liệu)
CREATE INDEX ON doc_chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
Tăng recall khi query IVFFlat
SET ivfflat.probes = 10; (mặc định 1)
Tăng bộ nhớ khi build index
SET maintenance_work_mem = '2GB';
Xem query có dùng index
EXPLAIN ANALYZE SELECT ... ORDER BY embedding <=> $1 LIMIT 5;
Gợi ý lists cho IVFFlat: khoảng rows / 1000 nếu dưới 1 triệu dòng. Index HNSW/IVFFlat cho kiểu vector giới hạn tối đa 2000 chiều. Model nhiều chiều hơn thì dùng halfvec (index được tới 4000 chiều).
Sửa request (thiếu tool_result, sai schema, sai thứ tự message)
401
authentication_error
AuthenticationError
Kiểm tra ANTHROPIC_API_KEY
403
permission_error
PermissionDeniedError
Key không có quyền với model/tính năng
404
not_found_error
NotFoundError
Sai model id hoặc endpoint
413
request_too_large
RequestTooLargeError
Request quá lớn: cắt bớt tài liệu, chia nhỏ
429
rate_limit_error
RateLimitError
Vượt RPM/ITPM/OTPM: đợi theo retry-after, giảm song song
500
api_error
InternalServerError
Lỗi phía server: retry có backoff
529
overloaded_error
OverloadedError
API quá tải tạm thời: retry có backoff, có thể chuyển model
SDK tự retry (mặc định 2 lần, backoff) cho lỗi kết nối, 408, 409, 429 và lỗi 5xx. Mọi lỗi HTTP đều kế thừa anthropic.APIStatusError (có .status_code, .response). Lỗi mạng là anthropic.APIConnectionError.
raw = client.messages.with_raw_response.create( model="claude-haiku-5-5", max_tokens=64, messages=[{"role": "user", "content": "ping"}],)print(raw.headers.get("anthropic-ratelimit-requests-remaining"))msg = raw.parse() # Message như bình thường
Dùng khi đã viết MCP server và cần test tool ở 3 tầng - unit test hàm, test giao thức qua stdio bằng pytest, rồi thử tay bằng MCP Inspector và Claude Code.
Tóm tắt: Test theo 3 tầng. (1) Unit test gọi thẳng hàm Python của tool, không cần MCP. (2) Test giao thức: pytest khởi chạy server qua stdio_client, list_tools rồi call_tool như một client thật. (3) Thử tay: MCP Inspector, sau đó claude mcp add và /mcp trong Claude Code. Cách viết server xem Viết MCP server bằng Python.
uv add "mcp[cli]<2" # code dưới đây dùng API 1.x (mcp.server.fastmcp)uv add --dev pytest anyio
Gói mcp bản 2.x đổi API: FastMCP thành MCPServer (from mcp.server.mcpserver import MCPServer), CallToolResult.isError thành is_error, và có thêm mcp.Client để test in-memory. Trang này viết cho 1.x; nếu dùng 2.x hãy đọc migration guide của SDK. uv add "mcp[cli]" không ghim sẽ kéo bản mới nhất.
Decorator @mcp.tool() trả lại chính hàm gốc, nên gọi trực tiếp được:
test_unit.py
import pytestfrom server import list_devices, pingdef test_list_devices_filter(): names = {d["name"] for d in list_devices(role="pe")} assert names == {"pe1-hni", "pe2-hcm"}def test_ping_rejects_hostname(): with pytest.raises(ValueError): ping("router.example.com") # chỉ nhận IP
Với tool gọi thiết bị/API ngoài, mock tầng I/O (monkeypatchsubprocess.run, client HTTP), đừng để unit test đụng thiết bị thật.
Khởi chạy server như tiến trình con và nói chuyện bằng JSON-RPC thật, nên bắt được lỗi schema, lỗi print() ra stdout, lỗi import:
test_e2e.py
import sysimport pytestfrom mcp import ClientSession, StdioServerParameters, stdio_client@pytest.fixturedef anyio_backend(): return "asyncio"@pytest.mark.anyioasync def test_tools_over_stdio(): params = StdioServerParameters(command=sys.executable, args=["server.py"]) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() assert "ping" in {t.name for t in tools.tools} ok = await session.call_tool("list_devices", {"role": "pe"}) assert not ok.isError bad = await session.call_tool("ping", {"target": "not-an-ip"}) assert bad.isError # lỗi trả về cho model, server không chết
uv run pytest -q
Điểm cần nhớ:
Plugin anyio có sẵn trong pytest khi cài anyio; @pytest.mark.anyio + fixture anyio_backend là đủ, không cần pytest-asyncio.
Exception trong tool không làm call_tool raise: kết quả có isError=True và content chứa thông báo (dạng Error executing tool <tên>: <lỗi>). Assert trên isError, đừng pytest.raises.
Muốn kiểm tra schema model nhìn thấy: assert trên t.inputSchema của từng tool (kiểu, required) để bắt hồi quy khi đổi chữ ký hàm.
Kết quả dạng list/dict trả về trong content (mỗi phần tử là một TextContent chứa JSON); tool trả str thì đọc content[0].text. Có thể parse bằng json.loads để assert.
uv run mcp dev server.py # Inspector qua CLI của mcpnpx @modelcontextprotocol/inspector uv run server.py # hoặc chạy Inspector trực tiếp
Trong Inspector: Connect → tab Tools → List Tools → chọn tool, nhập tham số → Run Tool. Xem cả tab Notifications (log server) khi tool lỗi. Trường hợp cần thử: tham số thiếu, sai kiểu, giá trị biên, tool chạy lâu.
claude mcp add netops-test -- uv run --directory /opt/netops-mcp server.pyclaude mcp listclaude mcp get netops-test
Trong phiên Claude Code gõ /mcp xem trạng thái và danh sách tool, rồi hỏi một câu buộc model phải gọi tool (vd "liệt kê thiết bị role pe"). Kiểm tra: model có chọn đúng tool không, tham số có đúng không, kết quả lỗi có đủ gợi ý để tự sửa không. Nếu model chọn sai, sửa docstring/tên tool trước khi sửa code (xem Thiết kế tool cho agent và Đánh giá agent).
flowchart LR
A[git commit] --> B{commit.gpgsign?}
B -- true --> C[gpg-agent ký bằng private key]
C --> D[commit có chữ ký]
D --> E[git push]
E --> F[GitHub so với public key] --> G[Verified]
Dùng khi cần làm gọn lịch sử feature branch trước khi mở hoặc cập nhật PR, gồm rebase lên main, gộp commit fixup, giải conflict và push an toàn.
Tóm tắt:git fetch → git rebase origin/main → sửa commit cũ bằng git commit --fixup=<sha> + git rebase -i --autosquash → giải conflict (git add + git rebase --continue) → git push --force-with-lease. Chỉ rebase branch của riêng bạn, không rebase main hay branch dùng chung.
flowchart TD
A[git fetch origin] --> B["git rebase origin/main"]
B --> C{Conflict?}
C -- có --> D["sửa file, git add"]
D --> E[git rebase --continue]
E --> C
C -- không --> F["git commit --fixup / rebase -i --autosquash"]
F --> G[chạy test]
G --> H[git push --force-with-lease]
git config --global pull.rebase true # git pull = fetch + rebasegit config --global rebase.autoSquash true # rebase -i tự xếp fixup!/squash!git config --global rebase.autoStash true # tự stash/unstash khi working tree bẩngit config --global rebase.updateRefs true # Git >= 2.38: cập nhật luôn các branch xếp chồnggit config --global rerere.enabled true # nhớ cách giải conflict, lần sau tự áp dụng
git log --oneline origin/main..HEAD# a1b2c3d add route-policy PASS-ALL# e4f5a6b add BGP neighbor template# sửa file, rồi gắn thay đổi vào commit e4f5a6bgit add -pgit commit --fixup=e4f5a6b # tạo commit "fixup! add BGP neighbor template"git rebase -i --autosquash origin/main # todo list đã xếp sẵn, chỉ cần lưu và thoát
Không muốn mở editor (dùng trong script):
GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash origin/main
Các lệnh trong todo list của rebase -i:
Lệnh
Tác dụng
pick
giữ commit
reword
giữ commit, sửa message
edit
dừng lại để amend hoặc tách commit
squash
gộp vào commit trước, ghép cả message
fixup
gộp vào commit trước, bỏ message
drop
bỏ commit
exec <cmd>
chạy lệnh (vd test) sau commit trước đó
Chạy test sau mỗi commit để đảm bảo commit nào cũng build được:
git status # file "both modified"git diff # xem marker <<<<<<< ======= >>>>>>># sửa file, hoặc chọn hẳn một phía:git restore --ours path/file # khi rebase: "ours" = base mới (main)git restore --theirs path/file # khi rebase: "theirs" = commit của bạn đang được áp lạigit add path/filegit rebase --continue# bỏ qua commit hiện tại / huỷ toàn bộ rebasegit rebase --skipgit rebase --abort
Khi rebase, ours/theirs bị đảo so với khi merge. --ours là phía branch bạn đang rebase lên (main), --theirs là commit của bạn.
git push --force-with-lease # từ chối nếu remote có commit bạn chưa fetchgit push --force-with-lease --force-if-includes # Git >= 2.30, an toàn hơn khi IDE tự fetch nền
Không dùng git push --force trần: nó ghi đè cả commit người khác vừa đẩy lên.
# feat-b được tách ra từ feat-a; rebase feat-b và tự dời luôn feat-agit switch feat-bgit rebase --update-refs origin/maingit push --force-with-lease origin feat-a feat-b
Dùng khi Git ở trạng thái lạ, lỡ commit nhầm branch, reset mất commit hoặc push bị từ chối, kèm cách cứu bằng reflog.
Triệu chứng chính:HEAD detached at ..., commit lên main thay vì feature branch, git reset --hard xong mất commit, ! [rejected] ... (fetch first) hoặc (non-fast-forward) khi push.
# Đang ở main, có 2 commit lẽ ra thuộc featuregit branch feature/fix-acl # giữ 2 commit ở branch mớigit reset --hard origin/main # đưa main về như remotegit switch feature/fix-acl
Nếu feature branch đã tồn tại sẵn thì cherry-pick sang:
git reflog -20# e4f5a6b HEAD@{0}: reset: moving to HEAD~3# 9c8d7e6 HEAD@{1}: commit: add prefix-listgit branch recover 9c8d7e6 # an toàn: tạo branch trỏ vào commit cũ# hoặc đưa branch hiện tại về đógit reset --hard HEAD@{1}
Reflog chỉ có trên máy local và mặc định giữ khoảng 90 ngày (commit không còn ref giữ khoảng 30 ngày) trước khi git gc dọn.
Dùng khi cần dựng nhanh một REST API nhỏ (vd inventory thiết bị mạng) bằng FastAPI + Pydantic v2, chạy bằng uvicorn và có OpenAPI tự sinh.
Tóm tắt:uv add "fastapi[standard]" → khai báo FastAPI(), route bằng decorator, body/response bằng Pydantic BaseModel, kiểm tra quyền bằng Depends → fastapi dev main.py → mở /docs. Ví dụ dưới đã test với FastAPI 0.115 và Pydantic 2.11.
flowchart LR
C[Client] -->|HTTP JSON| U[uvicorn]
U --> F[FastAPI router]
F --> D["Depends (auth, settings)"]
F --> V["Pydantic validate body/query"]
V -->|lỗi| E[422 Unprocessable Entity]
V --> H[Handler]
H --> R["response_model lọc output"]
R --> C
uv run fastapi dev main.py # dev, auto reload, cổng 8000uv run fastapi run main.py --port 8000 # production mode, listen 0.0.0.0uv run uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 # gọi uvicorn trực tiếp
Dùng khi API FastAPI vượt quá một file main.py và cần chia router, quản lý cấu hình bằng pydantic-settings, dependency injection và viết test.
Khi app còn dưới khoảng 200 dòng, một file main.py là đủ (xem FastAPI cơ bản). Lớn hơn thì chia theo router (mỗi nhóm endpoint một file), tách config, dependency và logic nghiệp vụ ra khỏi handler để test được.
Tra nhanh lệnh uv để tạo project, thêm dependency, lock/sync, cài Python, chạy tool và thay pip/venv.
uv (Astral) thay cho pip, pip-tools, virtualenv, pyenv, pipx bằng một binary. Lệnh dưới đã kiểm tra với uv 0.11. Project dùng pyproject.toml + uv.lock, venv nằm ở .venv/.
Tra nhanh cú pháp workflow GitHub Actions, gồm trigger, matrix, cache, secrets, artifact, concurrency, output và reusable workflow.
Workflow đặt ở .github/workflows/*.yml. Biểu thức dùng ${{ ... }}. Tag version action trong ví dụ là major mới nhất tại thời điểm viết, nên chạy Dependabot để tự nâng.
Workflow chạy bởi pull_request từ fork không đọc được secrets. Đừng chuyển sang pull_request_target rồi checkout code của PR, vì code lạ sẽ chạy với quyền và secret của repo.
5.9 pre-commit – chạy ruff, gitleaks trước mỗi commit và trong CI
Dùng khi muốn tự động lint/format Python bằng ruff và chặn commit chứa secret bằng gitleaks, cả trên máy dev lẫn trong GitHub Actions.
Tóm tắt: cài pre-commit → viết .pre-commit-config.yaml (pre-commit-hooks, ruff-pre-commit, gitleaks) → pre-commit install để gắn vào .git/hooks/pre-commit → pre-commit run --all-files lần đầu → chạy lại trong CI để chặn người quên cài hook.
flowchart LR
A[git commit] --> B[".git/hooks/pre-commit"]
B --> C["pre-commit đọc .pre-commit-config.yaml"]
C --> D["hook chạy trên file đã stage"]
D -->|pass| E[commit được tạo]
D -->|fail hoặc sửa file| F[commit bị chặn]
F --> G["git add, commit lại"]
E --> H[git push]
H --> I["CI: pre-commit run --all-files"]
[extend]useDefault = true[[allowlists]] # gitleaks >= 8.25; bản cũ hơn dùng [allowlist]description = "file mẫu và test"paths = ['''tests/fixtures/.*''', '''\.env\.example$''']
Hoặc thêm comment # gitleaks:allow ở cuối dòng bị báo nhầm.
Tra nhanh regex hay dùng để lọc IP, prefix, MAC, interface IOS XR/Junos, timestamp và sự kiện BGP trong log/config, kèm cú pháp grep -E, grep -P và Python re.
Mọi pattern dưới đây đã test bằng Python re và GNU grep 3.7. Lưu ý: grep -E (ERE) không hiểu \d, (?:...), lookahead, nên viết [0-9] và (...). Cần \d, \K, lookaround thì dùng grep -P. Muốn kiểm tra IP hợp lệ thì dùng module ipaddress thay vì regex.
Dùng khi đang code dở mà phải sửa gấp branch khác, review PR hoặc chạy test dài trên branch khác mà không muốn mất trạng thái làm việc.
Tóm tắt: Việc ngắn (vài phút) thì git stash push -m → chuyển branch → quay lại git stash pop. Việc dài hoặc cần chạy song song (hotfix, review, build) thì git worktree add ../repo-hotfix hotfix/1.2 để có thư mục làm việc thứ hai dùng chung .git, xong thì git worktree remove. Lệnh dưới đã chạy thử với Git 2.34; tuỳ chọn cần bản mới hơn có ghi chú.
Mỗi worktree là một thư mục checkout riêng, dùng chung object, branch và remote với repo chính. Một branch chỉ được checkout ở một worktree tại một thời điểm.
# Hotfix gấp khi đang code dở featuregit fetch origingit worktree add -b hotfix/1.2.1 ../netinv-hotfix origin/release/1.2cd ../netinv-hotfix# ... sửa, test ...git commit -am "fix: timeout SSH khi backup"git push -u origin HEADcd - && git worktree remove ../netinv-hotfix
Đặt worktree ngoài thư mục repo chính (../tên) để tool như ruff, pytest, IDE không quét lồng. Mỗi worktree cần venv riêng (uv sync trong thư mục đó), xem uv cheatsheet.
Lệnh stash cơ bản (push -u -m, list, pop, apply, branch) đã có trong Git cheatsheet. Phần dưới là các dạng hay quên.
Mục đích
Lệnh
Chỉ stash một số file
git stash push -m "wip f" -- path/a.py path/b.py
Chọn từng hunk để stash
git stash push -p -m "một phần"
Stash nhưng giữ nguyên phần đã add
git stash push --keep-index -m "test phần đã stage"
Chỉ stash phần đã stage (Git ≥ 2.35)
git stash push --staged -m "staged"
Xem diff một stash, kể cả file untracked (Git ≥ 2.32)
git stash show -p -u stash@{1}
Pop một stash cụ thể
git stash pop stash@{1}
Khôi phục cả trạng thái stage như lúc stash
git stash pop --index
Xoá một / tất cả stash
git stash drop stash@{1} / git stash clear
Lấy một file từ stash, không pop
git restore --source=stash@{0} -- path/a.py
git stash clear và drop không hỏi lại. Stash đã xoá chỉ còn tìm lại được qua git fsck --unreachable (dòng unreachable commit) cho tới khi bị gc, đừng trông vào đó.
Dùng khi cần đưa một repo khác (thư viện template, bộ YANG model, role Ansible dùng chung) vào project và phải chọn giữa submodule và subtree.
Có hai cách chính để nhúng repo B vào thư mục con của repo A:
Submodule: A chỉ lưu con trỏ (URL trong .gitmodules + SHA commit của B). Code B không nằm trong lịch sử A; người clone phải kéo thêm.
Subtree: A chứa bản copy thật của code B (thường squash thành 1 commit), merge vào thư mục con. Clone A là đủ, không cần lệnh đặc biệt.
Trước khi chọn, hỏi: có thật cần nhúng không? Nếu B là package Python/collection Ansible thì cài qua package manager (uv add, ansible-galaxy) thường đơn giản hơn cả hai.
git submodule deinit -f vendor/lib && git rm -f vendor/lib, sau đó xoá .git/modules/vendor/lib
Submodule sau update ở trạng thái detached HEAD. Muốn sửa code trong đó: cd vendor/lib && git switch main trước, nếu không commit dễ bị "lạc" khi update lần sau.
GitHub Actions: actions/checkout cần with: submodules: recursive (repo con private thì cần thêm token có quyền đọc), xem GitHub Actions cheatsheet.
Luôn dùng cùng một kiểu (--squash hoặc không) cho mọi lần add/pull của một prefix. Đặt remote cho gọn: git remote add lib https://github.com/org/lib.git rồi dùng lib main thay cho URL.
Submodule: cần ghim chính xác version, repo con lớn hoặc cập nhật độc lập, nhiều repo cha cùng dùng và team quen lệnh submodule. Ví dụ: bộ YANG model của vendor, firmware SDK.
Subtree: muốn người clone không phải biết gì thêm, code nhúng nhỏ và ít khi đẩy ngược. Ví dụ: bộ template Jinja2 dùng chung, script tiện ích.
Không dùng cả hai: khi B có thể phát hành thành package (PyPI, Ansible collection, Helm chart); khi chỉ cần một vài file thì copy và ghi rõ nguồn.
Tách một thư mục trong monorepo ra repo riêng (giữ lịch sử): git subtree split là cách nhanh không cần tool ngoài.
Tra nhanh cú pháp type hint hiện đại (Python 3.12+), TypedDict, Protocol và cách khai báo model, field, validator, serialize trong Pydantic v2.
Cú pháp dưới cho Python ≥ 3.12 và Pydantic 2.x (đã chạy thử với Python 3.12, Pydantic 2.14, mypy --strict). Bảng đổi tên API v1 → v2 xem ở FastAPI cơ bản; cấu hình mypy xem ruff và mypy.
from typing import NotRequired, Protocol, TypedDict, runtime_checkableclass Iface(TypedDict): # dict có key cố định, ví dụ JSON từ API name: str mtu: int descr: NotRequired[str] # key có thể vắng@runtime_checkableclass Driver(Protocol): # "duck typing" có kiểm tra: class nào có get_facts() là hợp lệ def get_facts(self) -> dict[str, str]: ...def collect(drv: Driver) -> dict[str, str]: return drv.get_facts()
Dùng
Khi
TypedDict
Dữ liệu vẫn là dict (JSON, kết quả thư viện), chỉ cần type checker hiểu key
dataclass
Object nội bộ, không cần validate input
Pydantic BaseModel
Input không tin cậy: API, file YAML, env; cần validate và ép kiểu
from pydantic import ValidationErrortry: Device(hostname="x", platform="eos", mgmt_ip="1.2.3", ssh_port=0)except ValidationError as e: for err in e.errors(): # 3 lỗi được gom trong một lần print(err["loc"], err["type"], err["msg"])# ('platform',) literal_error ...# ('mgmt_ip',) ip_v4_address ...# ('ssh_port',) value_error ...
Mặc định Pydantic chạy lax mode: "9000" thành 9000, "true" thành True. Điều này tiện khi đọc YAML/env, nhưng nếu cần bắt lỗi kiểu chặt (vd input từ API công khai) thì bật strict=True cho model hoặc từng field Field(strict=True).
mypy báo Decorators on top of @property are not supported ở @computed_field: đây là hạn chế đã biết, thêm # type: ignore[prop-decorator] như ví dụ trên.
Tra nhanh cách chạy chọn lọc test, viết fixture, parametrize, mark, dùng tmp_path/monkeypatch và đo coverage với pytest.
Đã chạy thử với pytest 9.0, pytest-cov và pytest-xdist. Cài vào project: uv add --dev pytest pytest-cov, chạy bằng uv run pytest (xem uv cheatsheet). Test FastAPI bằng TestClient xem cấu trúc project FastAPI.
import pytest@pytest.fixture(scope="session") # tạo 1 lần cho cả phiên testdef inventory(): return {"pe1": "198.51.100.11"}@pytest.fixture # mặc định scope="function"def conn(): c = {"open": True} # setup yield c # giá trị trả cho test c["open"] = False # teardown, chạy cả khi test fail
Mục đích
Cách làm
Dùng fixture
Khai báo tên làm tham số: def test_x(inventory, conn):
Dùng chung cho nhiều file
Đặt trong conftest.py (cùng thư mục hoặc thư mục cha)
[tool.pytest.ini_options]testpaths = ["tests"]addopts = "-ra --strict-markers" # mark gõ sai tên -> lỗi thay vì bị bỏ quamarkers = [ "slow: test chạy lâu", "lab: cần thiết bị lab",]
Mục đích
Lệnh
Coverage cho package app, hiện dòng chưa test
pytest --cov=app --cov-report=term-missing
Xuất HTML / XML cho CI
--cov-report=html / --cov-report=xml
Fail nếu coverage dưới ngưỡng
pytest --cov=app --cov-fail-under=80
Đo cả nhánh if/else
pytest --cov=app --cov-branch
Test chạm thiết bị thật thì gắn @pytest.mark.lab và chạy CI bằng -m "not lab". Mock kết nối (netmiko, ncclient) bằng monkeypatch.setattr để test logic parse mà không cần SSH.
Dùng khi dựng chuẩn code cho project Python, gồm ruff để lint và format (thay flake8, isort, black) và mypy để kiểm tra kiểu, tất cả cấu hình trong pyproject.toml.
Tóm tắt:uv add --dev ruff mypy → khai báo [tool.ruff], [tool.ruff.lint], [tool.ruff.format], [tool.mypy] trong pyproject.toml → chạy ruff check --fix, ruff format, mypy → đưa vào pre-commit và CI. Đã chạy thử với ruff 0.17 và mypy 2.4. Gắn vào hook git xem pre-commit.
flowchart LR
A["Sửa code"] --> B["ruff check --fix"]
B --> C["ruff format"]
C --> D["mypy"]
D -->|"lỗi kiểu"| A
D --> E["pre-commit / CI"]
E -->|"--check, không sửa"| F["Merge"]
[tool.ruff]line-length = 100target-version = "py312" # bỏ được nếu đã có requires-python trong [project]extend-exclude = ["migrations"][tool.ruff.lint]# E/W pycodestyle, F pyflakes, I isort, B bugbear, UP pyupgrade,# SIM simplify, RUF luật riêng của ruff, S bandit (bảo mật)select = ["E", "W", "F", "I", "B", "UP", "SIM", "RUF", "S"]ignore = ["E501"] # để formatter lo độ dài dòng[tool.ruff.lint.per-file-ignores]"tests/**" = ["S101"] # cho phép assert trong test[tool.ruff.lint.isort]known-first-party = ["netinv"][tool.ruff.format]quote-style = "double"docstring-code-format = true # format cả code mẫu trong docstring[tool.mypy]python_version = "3.12"strict = truewarn_unreachable = truemypy_path = "src" # project dạng src/ layoutfiles = ["src", "tests"]plugins = ["pydantic.mypy"] # chỉ khi dùng Pydantic[[tool.mypy.overrides]]module = ["ncclient.*", "textfsm.*"] # thư viện không có type hintignore_missing_imports = true[[tool.mypy.overrides]]module = ["tests.*"] # cần tests/__init__.py để khớp tên moduledisallow_untyped_defs = false
Project cũ: bắt đầu với select = ["E", "F", "I"] và mypy không strict, sửa hết lỗi rồi mới thêm nhóm luật. Bật hết một lúc sẽ ra hàng trăm lỗi và không ai sửa.
uv add "fastapi[standard]" pyjwt "pwdlib[argon2]"export JWT_SECRET="$(openssl rand -hex 32)" # production: lấy từ secret managerexport DEMO_PASSWORD='***' # chỉ cho ví dụ dưới
Hướng dẫn cũ dùng python-jose và passlib: cả hai gần như không còn bảo trì. FastAPI docs hiện dùng PyJWT và pwdlib. Gói PyPI tên pyjwt nhưng import là jwt; đừng cài nhầm gói jwt khác tên.
Đọc form username, password, scope (content-type application/x-www-form-urlencoded)
OAuth2PasswordBearer(tokenUrl="token")
Lấy token từ header Authorization: Bearer, thiếu thì trả 401; tokenUrl chỉ để Swagger UI biết chỗ login
PasswordHash.recommended()
Hash Argon2, verify() so mật khẩu với hash
jwt.encode / jwt.decode
Ký / kiểm chữ ký, tự kiểm exp và báo ExpiredSignatureError (lớp con của InvalidTokenError)
Claim sub
Định danh user; iat, exp dùng datetime có timezone UTC
Phân quyền theo vai trò: thêm claim (vd "scopes": ["read"]) khi tạo token rồi viết dependency require_scope("write") kiểm tra trong get_current_user. Với nhiều service cần xác minh token mà không giữ secret chung, dùng RS256/ES256 (private key ký, public key verify; cần pyjwt[crypto]) hoặc để IdP (Keycloak, Authentik) phát token.
Dùng khi đưa API FastAPI lên server thật, gồm chọn số worker, đóng gói Docker bằng uv, đặt sau Nginx với proxy headers và chạy dưới URL prefix bằng root_path.
Tóm tắt: Chạy fastapi run (là uvicorn) với --workers N trong container non-root → Nginx terminate TLS và proxy_pass tới container → bật tin cậy header X-Forwarded-* bằng --forwarded-allow-ips để app biết scheme/IP thật → nếu public dưới /netinv/ thì đặt root_path. Đã chạy thử với FastAPI 0.162, uvicorn 0.54, gunicorn 26 và Docker 27.
Quen quản lý bằng gunicorn (--timeout, --max-requests, reload bằng HUP)
1 worker/container
fastapi run app/main.py --port 8000
Kubernetes: scale bằng số replica, không bằng worker
Số worker khởi điểm: bằng số CPU core cấp cho container, sau đó đo tải. Mỗi worker là một process riêng: RAM nhân lên, biến global/cache không chia sẻ giữa các worker.
Gunicorn: class uvicorn.workers.UvicornWorker cũ đã deprecated, dùng gói uvicorn-worker (uv add gunicorn uvicorn-worker).
--forwarded-allow-ips "*" nghĩa là tin header X-Forwarded-* từ bất kỳ ai gửi tới. Chỉ dùng khi container không publish port ra ngoài, chỉ Nginx gọi được (publish vào 127.0.0.1 như trên hoặc chung Docker network nội bộ). Nếu không thì liệt kê IP/subnet của proxy. Thêm về Dockerfile xem Dockerfile best practices.
Dùng khi muốn code trực tiếp trên jump host, server lab hoặc trong container có sẵn toolchain, với VS Code chạy trên laptop.
Tóm tắt:Remote-SSH (ms-vscode-remote.remote-ssh): VS Code đọc ~/.ssh/config, SSH vào host, tự cài VS Code Server vào ~/.vscode-server rồi mở thư mục từ xa; extension chạy phía server. Dev Containers (ms-vscode-remote.remote-containers): khai báo .devcontainer/devcontainer.json trong repo, VS Code build/chạy container và mở project bên trong, mọi người trong team có cùng môi trường. Hai cái kết hợp được: SSH vào server có Docker rồi mở Dev Container trên đó.
flowchart LR
L["VS Code (laptop)"] -->|"SSH, theo ~/.ssh/config"| H["Host từ xa"]
H --> S["VS Code Server (~/.vscode-server)"]
S --> E["Extension, terminal, debugger"]
L -->|"Reopen in Container"| D["Docker (local hoặc trên host)"]
D --> C["Container từ devcontainer.json"]
C --> E2["Extension, uv, ruff trong container"]
VS Code Server chạy với quyền user SSH và extension có thể chạy code trên host. Không mở Remote-SSH bằng tài khoản có quyền cao trên máy production; dùng jump host/lab riêng.
Dùng image có sẵn, hoặc build từ Dockerfile trong .devcontainer/
features
Gói cài thêm dạng plug-in (Docker CLI, Node, kubectl...), tham số trong {}
containerEnv / remoteEnv
Env cho cả container / chỉ cho process VS Code mở (terminal, debugger); ${localEnv:VAR} lấy từ máy laptop
mounts
Bind mount thêm (SSH key, cache)
forwardPorts
Port tự forward về laptop
postCreateCommand
Chạy một lần sau khi tạo container: cài dependency
customizations.vscode
Extension và setting chỉ áp trong container
remoteUser
User VS Code dùng trong container (image devcontainers/* có sẵn vscode)
Thao tác
Cách làm
Mở project trong container
F1 → Dev Containers: Reopen in Container
Build lại sau khi sửa devcontainer.json
Dev Containers: Rebuild Container
Quay về máy local
Dev Containers: Reopen Folder Locally
Tạo file mẫu
Dev Containers: Add Dev Container Configuration Files...
Dev Container trên server từ xa
Remote-SSH vào server có Docker → mở thư mục → Reopen in Container
Mount ~/.ssh dạng readonly như trên để Ansible/Netmiko trong container SSH được tới thiết bị lab. Nếu dùng ssh-agent thì không cần mount: Dev Containers tự chuyển tiếp agent của máy local vào container.
Tra nhanh cú pháp Makefile (.PHONY, biến, tab, target phụ thuộc) và just để gom lệnh hay chạy của project thành make test, just deploy.
Mục tiêu: gõ make test / just test thay vì nhớ chuỗi lệnh dài; CI và người mới chạy giống nhau. Đã chạy thử với GNU Make 4.3 và just 1.59 (uvx --from rust-just just nếu chưa cài). make có sẵn trên hầu hết Linux; just cú pháp gọn hơn, truyền tham số dễ hơn nhưng phải cài thêm.
Dòng lệnh trong recipe phải bắt đầu bằng ký tự TAB, không phải dấu cách. Sai sẽ báo Makefile:N: *** missing separator. Stop. Kiểm tra bằng cat -A Makefile: TAB hiện thành ^I.
Phải cài (uv tool install rust-just, apt/brew, binary)
Tab bắt buộc
Có
Không (thụt đầu dòng bất kỳ, nhất quán)
Tham số dòng lệnh
Qua biến VAR=...
Tham số vị trí như hàm
Build theo timestamp file
Có (thế mạnh của make)
Không, mọi recipe luôn chạy
Mỗi dòng một shell riêng
Có
Có (trừ recipe shebang)
Chỉ dùng như task runner thì liệt kê mọi target trong .PHONY; nếu không, khi lỡ có file/thư mục tên test thì make test báo 'test' is up to date và không chạy gì.
5.20 jq và yq – lọc, biến đổi JSON và YAML trên dòng lệnh
Tra nhanh filter jq (select, map, to_entries, @tsv, --arg) để xử lý output API/JSON và yq bản mikefarah để đọc, sửa file YAML như inventory Ansible hay manifest Kubernetes.
Ví dụ dùng file devices.json dạng {"devices": [{"hostname", "platform", "mgmt_ip", "status", "interfaces": [{"name", "oper", "mtu"}]}]}. Đã chạy thử với jq 1.6 và yq v4.54 (mikefarah). Có hai công cụ cùng tên yq: bản mikefarah (Go, một binary, yq --version có chữ mikefarah) và bản kislyuk (Python, bọc jq). Trang này dùng bản mikefarah, cú pháp hai bản khác nhau.
yq -i 'select(.kind == "Deployment") | .spec.replicas = 2' (không bọc ngoặc vế trái) chỉ in ra document được chọn, và với -i sẽ xoá các document còn lại khỏi file. Luôn viết dạng (select(...) | .path) = value. yq -i giữ comment nhưng có thể đổi khoảng trắng/định dạng flow map, xem git diff trước khi commit.
5.21 Dev trong container (compose watch, bind mount, debug Python)
Dùng khi muốn sửa code trên máy và chạy trong container với auto-sync bằng compose watch hoặc bind mount, kèm gắn debugger debugpy từ VS Code.
Tóm tắt: Có 2 cách đưa code vào container khi dev: bind mount (volumes: ./app:/srv/app) hoặc docker compose watch với khối develop.watch (action sync, sync+restart, rebuild). Debug Python bằng debugpy mở cổng 5678 trong container rồi attach từ VS Code. Stack nhiều service xem Docker Compose – pattern hay dùng.
flowchart LR
A["Sửa code trên máy"] --> B{"Cách đưa code vào"}
B -->|"bind mount"| C["Container thấy file ngay"]
B -->|"compose watch"| D["sync / sync+restart / rebuild"]
C --> E["App reload"]
D --> E
E --> F["debugpy :5678"]
F --> G["VS Code attach"]
Chỉ mount thư mục code, không mount cả thư mục project (tránh đè .venv hoặc node_modules đã cài trong image). compose.override.yaml được docker compose up tự đọc, xem phần override ở Docker Compose – pattern hay dùng.
Ôn nhanh eBGP/iBGP, các attribute, thứ tự chọn best path (Cisco và khác biệt Junos) và route reflector.
BGP là giao thức path-vector chạy trên TCP/179, trao đổi prefix giữa các AS (eBGP) và phân phối prefix trong một AS (iBGP). BGP không tự tìm đường. Next-hop của mỗi path phải resolve được qua IGP, static route hoặc route có label (inet.3 trên Junos).
Chỉ xét path có next-hop reachable và hợp lệ (không bị policy loại, không loop AS_PATH).
Bước
Tiêu chí (Cisco IOS XR)
Ghi chú Junos
1
Weight cao nhất
Không có weight. Junos so route preference trước (BGP mặc định 170, thấp hơn thắng)
2
LOCAL_PREF cao nhất
Như nhau
3
Route do chính router sinh ra (network, aggregate-address, redistribute)
Không có bước riêng. Route static/aggregate trong bảng đã thắng route BGP nhờ preference thấp hơn
4
AIGP thấp nhất (nếu bật)
Có, khi bật aigp
5
AS_PATH ngắn nhất
Như nhau (as-path-ignore để bỏ qua)
6
ORIGIN thấp nhất: IGP < EGP < incomplete
Như nhau
7
MED thấp nhất (mặc định chỉ so giữa path từ cùng AS láng giềng)
Như nhau. Junos mặc định nhóm theo AS láng giềng như deterministic MED. path-selection always-compare-med để so mọi path
8
eBGP hơn iBGP
Như nhau
9
IGP metric tới next-hop thấp nhất
Như nhau
10
Xác định multipath (nếu bật maximum-paths)
multipath
11
Hai path eBGP: giữ path cũ nhất (ổn định)
Junos giữ path đang active. Tắt bằng path-selection external-router-id
12
Router ID thấp nhất (ORIGINATOR_ID nếu qua RR)
Như nhau
13
CLUSTER_LIST ngắn nhất
Như nhau
14
Địa chỉ neighbor thấp nhất
Như nhau
Trên IOS XR, show bgp ipv4 unicast 203.0.113.0/24 bestpath-compare in lý do từng path thua. Trên Junos, show route 203.0.113.0/24 detail có dòng Inactive reason: cho các path không active.
Thay vì full-mesh iBGP (n(n-1)/2 session), client chỉ peer với RR. RR phản xạ route giữa các client và non-client, đồng thời gắn ORIGINATOR_ID và CLUSTER_LIST để chống loop.
set protocols bgp group RR-CLIENTS cluster 192.0.2.1 (mọi neighbor trong group là client)
RR chỉ quảng bá một best path cho mỗi prefix. Muốn PE thấy nhiều path để làm multipath hoặc fast convergence thì cần BGP ADD-PATH, hoặc mỗi PE dùng RD riêng (với VPN).
Hard reset làm rơi toàn bộ route từ neighbor đó. Trên peering có full table, luôn thử soft trước. Junos tự áp lại import policy sau commit nên thường không cần clear.
Khoanh vùng khi BGP neighbor kẹt Idle/Active, Established nhưng 0 prefix, route bị policy chặn hoặc session down vì max-prefix.
Triệu chứng chính:show bgp summary thấy neighbor ở Idle, Active, Connect hoặc nhảy OpenSent liên tục; hoặc session Established nhưng nhận/gửi 0 prefix; hoặc session tự down kèm log maximum prefix.
flowchart TD
A["show bgp summary"] --> B{"State?"}
B -->|Idle/Active/Connect| C["Ping peer từ source IP, kiểm ACL/TTL/update-source"]
B -->|OpenSent lặp| D["Xem NOTIFICATION: AS, auth, capability"]
B -->|Established| E{"Có prefix?"}
E -->|Không| F["Kiểm route-policy in/out, max-prefix"]
E -->|Có nhưng không active| G["Kiểm next-hop, hidden routes, best path"]
Xem state và lý do reset gần nhất: XR show bgp neighbor 198.51.100.2 (dòng Last reset), Junos show bgp neighbor 198.51.100.2 (dòng Last error, Last State).
Session chưa lên: kiểm tra lớp IP/TCP trước (route, ping từ đúng source, filter), sau đó mới tới cấu hình BGP.
Session lên nhưng thiếu route: so route nhận trước và sau policy, rồi đọc policy. Lệnh đầy đủ ở BGP CLI cheatsheet.
eBGP tới upstream có policy hai chiều và max-prefix.
! IOS XRprefix-set OUR-PREFIXES 203.0.113.0/24end-set!route-policy ISP-A-IN if destination in (0.0.0.0/0 le 7, 0.0.0.0/0 ge 25) then drop endif passend-policy!route-policy ISP-A-OUT if destination in OUR-PREFIXES then pass else drop endifend-policy!router bgp 64500 bgp router-id 192.0.2.1 address-family ipv4 unicast network 203.0.113.0/24 ! neighbor 198.51.100.2 remote-as 64510 description ISP-A address-family ipv4 unicast route-policy ISP-A-IN in route-policy ISP-A-OUT out maximum-prefix 1000 80 restart 15 ! !!router static address-family ipv4 unicast 203.0.113.0/24 Null0
# Junosset routing-options autonomous-system 64500set routing-options router-id 192.0.2.1set routing-options static route 203.0.113.0/24 discardset policy-options prefix-list OUR-PREFIXES 203.0.113.0/24set policy-options policy-statement ISP-A-IN term BOGON-LEN from route-filter 0.0.0.0/0 upto /7set policy-options policy-statement ISP-A-IN term BOGON-LEN from route-filter 0.0.0.0/0 prefix-length-range /25-/32set policy-options policy-statement ISP-A-IN term BOGON-LEN then rejectset policy-options policy-statement ISP-A-IN term ACCEPT then acceptset policy-options policy-statement ISP-A-OUT term OURS from prefix-list OUR-PREFIXESset policy-options policy-statement ISP-A-OUT term OURS then acceptset policy-options policy-statement ISP-A-OUT term REST then rejectset protocols bgp group ISP-A type externalset protocols bgp group ISP-A peer-as 64510set protocols bgp group ISP-A import ISP-A-INset protocols bgp group ISP-A export ISP-A-OUTset protocols bgp group ISP-A neighbor 198.51.100.2 family inet unicast prefix-limit maximum 1000set protocols bgp group ISP-A neighbor 198.51.100.2 family inet unicast prefix-limit teardown 80 idle-timeout 15
restart 15 (XR) và idle-timeout 15 (Junos) tính bằng phút: session tự thử lại sau 15 phút. Bỏ đi thì phải clear bằng tay. Chỉ muốn cảnh báo mà không cắt session: XR maximum-prefix 1000 80 warning-only, Junos bỏ teardown (chỉ log khi vượt maximum).
Nắm nguyên lý link-state, cách chia area (OSPF) và level (IS-IS), và vì sao backbone ISP thường chọn IS-IS.
OSPF và IS-IS đều là link-state IGP: mỗi router quảng bá trạng thái link của mình (LSA/LSP), mọi router trong cùng vùng flood để có chung một LSDB, rồi tự chạy SPF (Dijkstra) để tính cây đường ngắn nhất. Trong backbone nhà mạng, IGP chỉ mang loopback và link nội bộ; prefix khách hàng và Internet đi bằng BGP.
flowchart LR
A["Hello: tìm neighbor"] --> B["Adjacency Up"]
B --> C["Đồng bộ LSDB"]
C --> D["Flood LSA/LSP khi topo đổi"]
D --> E["Chạy SPF"]
E --> F["Cài route vào RIB/FIB"]
Router L1/L2. Ranh giới area nằm trên link, không nằm trong router
Đi ra khỏi vùng
Route summary (LSA type 3) từ ABR
Router L1 đi theo default route nhờ ATT bit của router L1/L2
Định danh router
Router ID 32-bit
System ID 6 byte trong NET, ví dụ 49.0001.1920.0000.2001.00
Backbone ISP thường chạy IS-IS một level duy nhất (L2-only) cho toàn mạng để đơn giản: không có route leaking, không có ATT bit, thêm PoP mới không phải thiết kế lại area.
Dựng IS-IS L2-only, wide metric, point-to-point giữa IOS XR và Junos rồi kiểm tra adjacency, LSDB và route.
Tóm tắt: Một tiến trình IS-IS level-2-only, NET suy từ loopback, metric-style wide, link router-router dạng point-to-point, loopback passive. Junos phải bật family iso trên interface và đặt NET ở lo0. Lý thuyết xem OSPF và IS-IS.
Topo ví dụ: R1 (IOS XR, lo 192.0.2.1) nối R2 (Junos, lo 192.0.2.2) qua link 198.51.100.0/31, area 49.0001.
Thêm address-family ipv6 unicast (XR, cả dưới router isis và từng interface) hoặc family inet6 trên interface (Junos). Chú ý kiểu topology: IOS XR mặc định multi-topology cho IPv6 (single-topology dưới address-family ipv6 unicast để đổi), Junos mặc định single-topology (set protocols isis topologies ipv6-unicast để bật MT). Hai đầu phải cùng kiểu, và với single-topology mọi link phải chạy cả IPv4 lẫn IPv6.
Kết quả mong đợi: adjacency Up ở level 2, LSDB hai bên có cùng danh sách LSP, show route thấy loopback đối diện với metric 100 (link) + metric loopback.
Hiểu SR-MPLS (SRGB, prefix SID, adjacency SID, TI-LFA) và so sánh với LDP, RSVP-TE trước khi thay thế LDP trong backbone.
Segment Routing (SR) là source routing: router đầu vào ghi sẵn danh sách segment (SID) vào gói, các router giữa đường chỉ làm theo. Với SR-MPLS, mỗi SID là một MPLS label, được IGP (IS-IS/OSPF) quảng bá luôn trong LSP/LSA. Không cần LDP hay RSVP-TE, không có trạng thái per-tunnel ở router giữa đường.
Dải label dành cho SID toàn cục, nên giống nhau trên mọi router
16000-23999 (mặc định của Cisco)
Prefix SID
SID gắn với một prefix, thường là loopback. Quảng bá dạng index
index 2 → label 16000 + 2 = 16002
Node SID
Prefix SID của loopback, đại diện cho router
R2 = 16002
Adjacency SID
SID cục bộ cho một link, ép gói đi đúng link đó
Cấp động từ dải local, ví dụ 24005
SRLB
Dải label local cho adjacency SID tĩnh, BGP peer SID...
Cisco mặc định 15000-15999
Label stack
Danh sách SID router đầu vào đẩy vào gói
[16005, 24007, 16009]
Prefix SID được quảng bá bằng index, label thực = SRGB base của router kế tiếp + index. SRGB khác nhau giữa các router vẫn chạy được nhưng label đổi qua từng hop, rất khó debug. Hãy giữ một SRGB thống nhất cho cả domain.
Gói đi theo đường ngắn nhất của IGP (ECMP nếu có), label giữ nguyên 16004 trên mọi hop vì SRGB giống nhau. Hop áp chót pop label (PHP), trừ khi cấu hình explicit-null.
Topology-Independent Loop-Free Alternate tính sẵn đường backup cho mỗi prefix, theo đúng đường sau hội tụ (post-convergence path). Khi link hoặc node hỏng, router cục bộ chuyển sang backup thường dưới 50 ms, trước cả khi IGP hội tụ.
Đường backup được mã hoá bằng label stack (node SID + adjacency SID), nên che được mọi topo, không bị giới hạn như LFA/rLFA cổ điển.
Bảo vệ link (mặc định), node hoặc SRLG tuỳ cấu hình.
Thay LDP trong backbone để bớt một giao thức và có TI-LFA. Cấu hình xem SR-MPLS trên IS-IS.
Cần TE đơn giản (tránh link, ép theo màu/latency) mà không muốn duy trì hàng nghìn LSP RSVP.
Không nên vội bỏ LDP khi còn thiết bị không hỗ trợ SR trên đường đi, hoặc khi transport còn phụ thuộc LDP. Lưu ý pseudowire/VPLS signaling bằng targeted LDP vẫn cần LDP cho phần signaling, dù transport đã là SR.
Dịch vụ L3VPN chạy trên SR không đổi gì ở tầng BGP: xem MPLS L3VPN.
Bật Segment Routing MPLS trên IS-IS có sẵn, gán node SID, bật TI-LFA và kiểm tra label trên IOS XR và Junos.
Tóm tắt: Khai SRGB 16000-23999 giống nhau trên mọi router, bật SR trong IS-IS, gán prefix SID (index) cho loopback, bật TI-LFA trên link. IS-IS phải đang chạy wide metric như trong cấu hình IS-IS. Khái niệm xem Segment Routing MPLS.
Quy ước index: router có loopback 192.0.2.N dùng index N. R1 (XR) = 16001, R2 (Junos) = 16002.
flowchart LR
A["Khai SRGB 16000-23999"] --> B["Bật SR trong IS-IS"]
B --> C["Gán prefix SID cho loopback"]
C --> D["IS-IS quảng bá SID trong LSP"]
D --> E["Cài label vào LFIB"]
E --> F["Bật TI-LFA trên link"]
F --> G["Kiểm tra label và backup path"]
Junos cần family mpls trên unit và interface trong protocols mpls. Thiếu một trong hai thì adjacency IS-IS vẫn lên, SID vẫn quảng bá, nhưng label không được cài để chuyển tiếp qua link đó.
show isis database verbose | include "SID|Router Cap"
show isis database extensive | match "SID|SRGB"
Adjacency SID
show isis adjacency detail
show isis adjacency extensive
LFIB theo node SID
show mpls forwarding labels 16002
show route label 16001
Route có label tới loopback
show cef 192.0.2.2/32
show route table inet.3 192.0.2.1/32 extensive
TI-LFA tổng quan
show isis fast-reroute summary
show isis backup coverage
Backup path của một prefix
show isis fast-reroute 192.0.2.2/32 detail
show route 192.0.2.1/32 extensive (next-hop có weight 0xf000 là backup)
Đi thử theo label
traceroute sr-mpls 192.0.2.2/32
traceroute 192.0.2.1 (hop hỗ trợ RFC 4950 sẽ in label MPLS)
Kết quả mong đợi: trên R1, show mpls forwarding labels 16002 có outgoing label Pop (PHP) qua link trực tiếp sang R2. Khi có router ở giữa, outgoing label giữ nguyên 16002.
Hiểu EVPN route type 1-5, cách VXLAN VNI ánh xạ VLAN/VRF và khác biệt symmetric với asymmetric IRB khi thiết kế fabric DC.
EVPN (RFC 7432) dùng BGP address-family l2vpn evpn làm control plane để quảng bá MAC/IP, thay cho học MAC bằng flood-and-learn. VXLAN (RFC 7348) là data plane: đóng gói frame Ethernet trong UDP/4789 kèm VNI 24-bit (khoảng 16 triệu segment). Mỗi switch đầu cuối là một VTEP, thường dùng loopback làm địa chỉ nguồn tunnel. EVPN cũng chạy trên MPLS (EVPN-MPLS) trong mạng nhà mạng. Phần route type dưới đây dùng chung cho cả hai.
Underlay (IS-IS/OSPF hoặc eBGP) chỉ cần mang loopback VTEP. Overlay là BGP EVPN giữa leaf, qua spine làm route reflector (iBGP) hoặc qua eBGP (mỗi leaf một AS private, ví dụ 65001, 65002).
IRB (RFC 9135) là route giữa các subnet ngay trên leaf.
Tiêu chí
Asymmetric IRB
Symmetric IRB
Leaf vào
Route rồi bridge sang VNI đích
Route, đóng gói bằng L3VNI
Leaf ra
Chỉ bridge
Route lần nữa từ L3VNI xuống VLAN đích
VNI cần có trên leaf
Mọi L2VNI của mọi subnet trong VRF
Chỉ L2VNI có host local + một L3VNI mỗi VRF
Bảng ARP/MAC
Lớn, mọi leaf học mọi host
Nhỏ hơn, mở rộng tốt
Route type 5
Không dùng được cho prefix ngoài
Dùng được
Hay gặp
Fabric nhỏ, cấu hình đơn giản
Mặc định ở hầu hết thiết kế hiện nay
sequenceDiagram
participant A as Host A VLAN 10
participant L1 as Leaf1
participant L2 as Leaf2
participant B as Host B VLAN 20
A->>L1: Frame tới anycast gateway
L1->>L1: Route trong VRF
L1->>L2: VXLAN với L3VNI, MAC đích là Router MAC của Leaf2
L2->>L2: Route trong VRF xuống VLAN 20
L2->>B: Frame tới Host B
Cấu hình VRF, RD/RT, VPNv4 iBGP và PE-CE eBGP cho một khách hàng L3VPN trên IOS XR và Junos, kèm lệnh kiểm tra.
Tóm tắt: Mỗi khách hàng một VRF (XR vrf, Junos routing-instances ... instance-type vrf). RD làm route VPN duy nhất, RT quyết định import/export. PE trao đổi route VPNv4 qua iBGP (thường qua RR), next-hop là loopback PE và phải có LSP (LDP hoặc SR-MPLS) tới đó.
Topo ví dụ: PE1 (IOS XR, lo 192.0.2.1) và PE2 (Junos, lo 192.0.2.2) peer VPNv4 với RR 192.0.2.10, AS 64500. Khách hàng CUST-A, RD/RT 64500:100, hai site CE dùng AS 65001.
set interfaces ge-0/0/1 unit 0 family inet address 198.51.100.5/30set routing-instances CUST-A instance-type vrfset routing-instances CUST-A interface ge-0/0/1.0set routing-instances CUST-A route-distinguisher 64500:100set routing-instances CUST-A vrf-target target:64500:100set routing-instances CUST-A vrf-table-labelset routing-instances CUST-A protocols bgp group CE type externalset routing-instances CUST-A protocols bgp group CE peer-as 65001set routing-instances CUST-A protocols bgp group CE as-overrideset routing-instances CUST-A protocols bgp group CE neighbor 198.51.100.6set protocols bgp group RR type internalset protocols bgp group RR local-address 192.0.2.2set protocols bgp group RR family inet unicastset protocols bgp group RR family inet-vpn unicastset protocols bgp group RR neighbor 192.0.2.10
PE trong lab dùng chung RD cho mọi site là đủ. Trong production nên dùng RD riêng cho mỗi PE (ví dụ 192.0.2.1:100) để RR không gộp path, CE multihomed có đường dự phòng sẵn trên PE xa.
! IOS XRinterface Bundle-Ether10 mtu 9216 ipv4 address 198.51.100.1 255.255.255.254 bundle minimum-active links 1!interface HundredGigE0/0/0/1 bundle id 10 mode active lacp period short!interface HundredGigE0/0/0/2 bundle id 10 mode active lacp period short
MTU trên cả IOS XR (mtu) và Junos (mtu ở interface vật lý) là MTU lớp 2, đã gồm 14 byte header Ethernet: mtu 9216 cho IP MTU 9202. Lệch giữa hai hãng thường do một bên đặt ipv4 mtu / family inet mtu riêng.
Kích thước ping khác nhau giữa hai hãng: IOS XR size là cả gói IP, Junos size là payload ICMP (cộng thêm 28 byte header IP + ICMP). 9202 trên XR tương đương 9174 trên Junos. Bắt gói BFD/LACP trên máy Linux nối vào: xem tcpdump cheatsheet.
6.11 BGP route policy – RPL trên IOS XR và policy-statement trên Junos
Viết policy lọc prefix khách hàng, gắn community, đặt local-preference và chỉ quảng bá route khách lên upstream, bằng RPL và Junos policy-options.
Tóm tắt: IOS XR dùng RPL: khai báo prefix-set, community-set, as-path-set rồi viết logic if ... then ... endif trong route-policy ... end-policy. Junos dùng policy-options: prefix-list, community, as-path và policy-statement gồm nhiều term với from (điều kiện) và then (hành động). Khái niệm attribute xem BGP – khái niệm cốt lõi, danh sách community xem BGP communities.
Ví dụ: ISP AS 64500, khách hàng AS 65001 (neighbor 198.51.100.6) chỉ được quảng bá 203.0.113.0/24, upstream AS 64510 (neighbor 198.51.100.2). Route khách được gắn 64500:100 và local-preference 200; ra upstream chỉ gửi route có 64500:100.
prefix-set CUST-65001 203.0.113.0/24end-set!community-set CUSTOMER-ROUTES 64500:100end-set!community-set OUR-INTERNAL 64500:*end-set!as-path-set AS65001-ONLY ios-regex '^65001(_65001)*$'end-set!route-policy CUST-65001-IN if not destination in CUST-65001 then drop endif if not as-path in AS65001-ONLY then drop endif delete community in OUR-INTERNAL set community CUSTOMER-ROUTES additive set local-preference 200 doneend-policy!route-policy UPSTREAM-OUT if community matches-any CUSTOMER-ROUTES then delete community in OUR-INTERNAL pass else drop endifend-policy!router bgp 64500 neighbor 198.51.100.6 remote-as 65001 address-family ipv4 unicast route-policy CUST-65001-IN in route-policy PASS out ! ! neighbor 198.51.100.2 remote-as 64510 address-family ipv4 unicast route-policy UPSTREAM-OUT out ! !!
PASS là policy chỉ có pass (xem L3VPN). Bỏ additive thì set communitythay thế toàn bộ community cũ.
Policy có tham số, dùng lại cho nhiều neighbor:
route-policy SET-LP($lp) set local-preference $lpend-policy!route-policy UPSTREAM-B-IN apply SET-LP(80) passend-policy!route-policy UPSTREAM-B-OUT if community matches-any CUSTOMER-ROUTES then prepend as-path 64500 2 pass endifend-policy
set policy-options prefix-list CUST-65001 203.0.113.0/24set policy-options community CUSTOMER-ROUTES members 64500:100set policy-options community OUR-INTERNAL members "^64500:.*$"set policy-options as-path AS65001-ONLY "65001+"set policy-options policy-statement CUST-65001-IN term SCRUB then community delete OUR-INTERNALset policy-options policy-statement CUST-65001-IN term OK from prefix-list-filter CUST-65001 exactset policy-options policy-statement CUST-65001-IN term OK from as-path AS65001-ONLYset policy-options policy-statement CUST-65001-IN term OK then community add CUSTOMER-ROUTESset policy-options policy-statement CUST-65001-IN term OK then local-preference 200set policy-options policy-statement CUST-65001-IN term OK then acceptset policy-options policy-statement CUST-65001-IN term REJECT then rejectset policy-options policy-statement UPSTREAM-OUT term CUST from community CUSTOMER-ROUTESset policy-options policy-statement UPSTREAM-OUT term CUST then community delete OUR-INTERNALset policy-options policy-statement UPSTREAM-OUT term CUST then acceptset policy-options policy-statement UPSTREAM-OUT term REJECT then rejectset protocols bgp group CUST-65001 type externalset protocols bgp group CUST-65001 peer-as 65001set protocols bgp group CUST-65001 import CUST-65001-INset protocols bgp group CUST-65001 neighbor 198.51.100.6set protocols bgp group ISP-A export UPSTREAM-OUT
Quy tắc khớp trong một term: các điều kiện fromkhác loại là AND (prefix và AS_PATH), nhiều giá trị cùng loại là OR. Term SCRUB không có from nên khớp mọi route, không có terminating action nên đi tiếp sang OK.
Prepend và local-preference trên Junos:
set policy-options policy-statement UPSTREAM-B-IN term ALL then local-preference 80set policy-options policy-statement UPSTREAM-B-IN term ALL then acceptset policy-options policy-statement UPSTREAM-B-OUT term CUST from community CUSTOMER-ROUTESset policy-options policy-statement UPSTREAM-B-OUT term CUST then as-path-prepend "64500 64500"set policy-options policy-statement UPSTREAM-B-OUT term CUST then acceptset policy-options policy-statement UPSTREAM-B-OUT term REJECT then reject
Mặc định không gửi: send-community-ebgp dưới address-family của neighbor
Mặc định gửi
Gửi extended community cho eBGP
send-extended-community-ebgp
Mặc định gửi
Route mang một community
show bgp ipv4 unicast community 64500:100
show route community 64500:100
Route khớp community có tên
—
show route community-name C-CUST
Community của một prefix
show bgp ipv4 unicast 203.0.113.0/24 (dòng Community:)
show route 203.0.113.0/24 detail | match Communities
Mỗi ISP công bố bảng community riêng cho khách (ví dụ 64500:80 = LP 80, 64500:666 = blackhole, 64500:2xxx = prepend tới peer nào). Viết bảng này trên trang web/IRR và luôn xoá community nội bộ 64500:* khách tự gắn trước khi gắn lại theo policy.
6.13 RPKI Route Origin Validation với Routinator, IOS XR và Junos
Dựng Routinator làm RTR server, cấu hình router IOS XR và Junos lấy VRP, rồi drop route RPKI invalid trong import policy.
Tóm tắt: Validator (Routinator) tải ROA từ 5 RIR, kiểm chữ ký, xuất danh sách VRP (prefix, maxLength, origin AS) qua giao thức RTR (RFC 8210). Router chỉ so origin AS và độ dài prefix của route nhận được với VRP để gán trạng thái valid / invalid / not-found. Việc loại route invalid là do import policy, không tự động.
Ví dụ: hai validator 192.0.2.50 và 192.0.2.51 (RTR TCP 3323), router XR lo 192.0.2.1, Junos lo 192.0.2.2, upstream 198.51.100.2 AS 64510.
Cài từ repo của NLnet Labs (gói routinator cho Debian/Ubuntu/RHEL) hoặc Docker. Chạy tối thiểu hai validator ở hai nơi khác nhau.
# chạy tay để thử: RTR cho router, HTTP cho metrics/APIroutinator server --rtr 192.0.2.50:3323 --http 192.0.2.50:8323# kiểm một route: origin AS64510 có hợp lệ cho 203.0.113.0/24?routinator validate --asn AS64510 --prefix 203.0.113.0/24# xuất toàn bộ VRP ra CSVroutinator vrps --format csv | head
Khi chạy bằng service, đặt địa chỉ listen trong /etc/routinator/routinator.conf:
sudo systemctl enable --now routinatorcurl -s http://192.0.2.50:8323/status | head
RTR không mã hoá. Chỉ mở cổng 3323 cho loopback/management của router (ACL, firewall). Lần đầu khởi động Routinator mất vài phút để tải repository, trong lúc đó router chưa có VRP.
router bgp 64500 rpki server 192.0.2.50 transport tcp port 3323 refresh-time 600 ! rpki server 192.0.2.51 transport tcp port 3323 refresh-time 600 ! address-family ipv4 unicast bgp origin-as validation enable !!route-policy UPSTREAM-IN if validation-state is invalid then drop endif if validation-state is not-found then set local-preference 90 endif passend-policy!router bgp 64500 neighbor 198.51.100.2 address-family ipv4 unicast route-policy UPSTREAM-IN in ! !!
bgp origin-as validation enable cần cho bản XR mới (từ 6.5.x validation tắt mặc định theo address-family). Bản cũ bật mặc định, khi đó lệnh có thể không tồn tại.
set routing-options validation group RPKI session 192.0.2.50 port 3323set routing-options validation group RPKI session 192.0.2.50 local-address 192.0.2.2set routing-options validation group RPKI session 192.0.2.50 refresh-time 600set routing-options validation group RPKI session 192.0.2.51 port 3323set routing-options validation group RPKI session 192.0.2.51 local-address 192.0.2.2set routing-options validation group RPKI session 192.0.2.51 refresh-time 600set policy-options policy-statement RPKI term VALID from validation-database validset policy-options policy-statement RPKI term VALID then validation-state validset policy-options policy-statement RPKI term VALID then next policyset policy-options policy-statement RPKI term INVALID from validation-database invalidset policy-options policy-statement RPKI term INVALID then validation-state invalidset policy-options policy-statement RPKI term INVALID then rejectset policy-options policy-statement RPKI term UNKNOWN from validation-database unknownset policy-options policy-statement RPKI term UNKNOWN then validation-state unknownset policy-options policy-statement RPKI term UNKNOWN then local-preference 90set policy-options policy-statement RPKI term UNKNOWN then next policyset protocols bgp group ISP-A import [ RPKI ISP-A-IN ]
Junos chỉ gán validation-state cho route khi import policy có then validation-state. Policy RPKI đặt đầu chuỗi import, policy lọc bogon/prefix (ISP-A-IN) theo sau.
Route của chính mình hoặc khách bị invalid: kiểm ROA trên portal RIR (origin AS, maxLength). Lỗi hay gặp là ROA /22 maxLength 22 nhưng lại quảng bá /24.
Khách mới có prefix mà chưa tạo ROA: kết quả là not-found, không bị drop. Chỉ khi có ROA sai mới thành invalid.
Cần cho qua tạm một prefix invalid (sự cố của bên thứ ba): Routinator hỗ trợ file ngoại lệ SLURM (RFC 8416) qua tuỳ chọn exceptions, áp tập trung thay vì sửa policy trên từng router.
Không đặt local-pref cho valid cao hơn not-found một cách tuỳ tiện: dễ làm đổi traffic engineering sẵn có.
Hiểu khi nào dùng RTBH (bỏ toàn bộ traffic tới một IP) và khi nào dùng BGP FlowSpec (lọc theo port/protocol), kèm cấu hình mẫu ngắn trên IOS XR và Junos.
Khi một IP bị DDoS làm nghẽn link upstream, cần chặn traffic càng xa càng tốt: trên edge router của mình, hoặc nhờ upstream chặn hộ. BGP có hai cơ chế:
RTBH (Remotely Triggered Black Hole): quảng bá /32 (/128) của nạn nhân kèm community blackhole, router nhận đặt next-hop về discard. Đơn giản, mọi thiết bị đều hỗ trợ, nhưng chặn luôn cả traffic hợp lệ: kẻ tấn công "thắng" vì IP đã ngừng phục vụ.
BGP FlowSpec (RFC 8955, IPv6 ở RFC 8956): quảng bá rule lọc (đích, nguồn, protocol, port, packet length...) kèm hành động (drop, rate-limit, redirect, mark). Router cài rule vào phần cứng như ACL. Chỉ chặn đúng loại traffic tấn công.
Hành động mang trong extended community: traffic-rate (rate 0 = drop), traffic-action (sample/terminal), redirect (sang VRF theo RT), traffic-marking (đặt DSCP).
Mặc định router chỉ nhận FlowSpec rule nếu đích của rule khớp best-path unicast từ cùng peer (validation theo RFC 8955). Rule do mình tự sinh hoặc từ controller nội bộ thì thường cần tắt validation có kiểm soát. Luôn giới hạn số rule nhận được và chỉ nhận từ nguồn tin cậy.
Khách AS 65001 gửi /32 thuộc 203.0.113.0/24 kèm 65535:666 (hoặc 64500:666 theo bảng community của ISP). Edge chấp nhận /32 chỉ khi có community, đặt next-hop discard và gắn no-export để không lọt ra ngoài.
! IOS XRprefix-set CUST-65001-HOST 203.0.113.0/24 ge 32 le 32end-set!community-set BLACKHOLE 65535:666, 64500:666end-set!route-policy CUST-65001-IN if destination in CUST-65001-HOST and community matches-any BLACKHOLE then set next-hop discard set community (65535:65281) additive done endif # ... phần lọc prefix thường như trang BGP route policyend-policy
# Junosset policy-options community BLACKHOLE members [ 65535:666 ]set policy-options community BLACKHOLE-ISP members [ 64500:666 ]set policy-options community NO-EXPORT members no-exportset policy-options policy-statement CUST-65001-IN term RTBH from community [ BLACKHOLE BLACKHOLE-ISP ]set policy-options policy-statement CUST-65001-IN term RTBH from route-filter 203.0.113.0/24 prefix-length-range /32-/32set policy-options policy-statement CUST-65001-IN term RTBH then community add NO-EXPORTset policy-options policy-statement CUST-65001-IN term RTBH then next-hop discardset policy-options policy-statement CUST-65001-IN term RTBH then accept
Term RTBH phải nằm trước term lọc prefix thông thường (vốn chỉ nhận /24). Cách viết phần còn lại xem BGP route policy.
Junos: rule tĩnh chặn NTP amplification tới một IP, quảng bá qua iBGP family inet flow:
set routing-options flow route NTP-AMP match destination 203.0.113.10/32set routing-options flow route NTP-AMP match protocol udpset routing-options flow route NTP-AMP match source-port 123set routing-options flow route NTP-AMP then discardset protocols bgp group IBGP family inet flow
IOS XR: nhận FlowSpec qua BGP và cài rule xuống mọi interface:
RTBH đích: link upstream đã nghẽn, cần hạ ngay; chấp nhận hy sinh một IP để cứu cả PoP. Phối hợp với community blackhole của upstream để chặn từ phía họ.
FlowSpec: tấn công có đặc trưng rõ (amplification theo source port, SYN flood tới một port), link còn chịu được, muốn giữ dịch vụ cho người dùng thật.
Scrubbing center: tấn công lớn hoặc tầng ứng dụng, dùng FlowSpec redirect hoặc BGP diversion sang hệ thống lọc.
Dựng OSPFv2 area 0 với link point-to-point, loopback passive, reference bandwidth, area stub/NSSA, rồi kiểm tra neighbor, LSDB và route trên IOS XR và Junos.
Tóm tắt: IOS XR cấu hình OSPF theo router ospf <tên> → area → interface. Junos cấu hình dưới protocols ospf area 0.0.0.0 interface. Link router-router đặt point-to-point (XR network point-to-point, Junos interface-type p2p), loopback passive, router-id cố định bằng loopback. Lý thuyết và so sánh với IS-IS xem OSPF và IS-IS.
Topo ví dụ: R1 (IOS XR, lo 192.0.2.1) nối R2 (Junos, lo 192.0.2.2) qua 198.51.100.0/31, area 0. R2 có thêm area 1 (stub) phía dưới qua ge-0/0/2.
! IOS XRroute-policy STATIC-TO-OSPF if destination in (203.0.113.0/24) then pass endifend-policy!router ospf CORE redistribute static route-policy STATIC-TO-OSPF!
# Junosset policy-options policy-statement STATIC-TO-OSPF term S from protocol staticset policy-options policy-statement STATIC-TO-OSPF term S from route-filter 203.0.113.0/24 exactset policy-options policy-statement STATIC-TO-OSPF term S then acceptset protocols ospf export STATIC-TO-OSPF
Backbone ISP không nên redistribute prefix khách hàng vào IGP. IGP chỉ mang loopback và link, prefix khách đi bằng BGP. Muốn chỉnh chuyển tiếp nhanh, bật BFD: XR bfd fast-detect dưới interface OSPF, Junos bfd-liveness-detection minimum-interval 100 (xem BFD, LAG, MTU).
Nắm cách MPLS chuyển tiếp bằng label, LDP và RSVP-TE phân phối label ra sao, và khi nào chọn cái nào trong backbone ISP.
MPLS chuyển tiếp gói dựa trên label 20 bit thay vì tra IP. Router đầu vào (ingress LER) gán gói vào một FEC (Forwarding Equivalence Class, thường là một loopback PE /32), đẩy label; router giữa đường (LSR) chỉ swap label; router áp chót thường pop (PHP) để egress chỉ cần tra label dịch vụ hoặc IP. Chuỗi label từ ingress tới egress gọi là LSP.
Label được phân phối bởi một control plane: LDP, RSVP-TE, hoặc IGP với Segment Routing. Dịch vụ (L3VPN, L2VPN) chạy bên trên dùng LSP làm transport, xem L3VPN.
LDP: transport đơn giản cho L3VPN/L2VPN khi chỉ cần đi theo IGP. Ít cấu hình, nhưng phải bật LDP-IGP sync và cân nhắc chuyển sang SR-MPLS (cấu hình SR-MPLS).
RSVP-TE: cần đặt trước băng thông, ép đường theo ràng buộc (tránh link, affinity) hoặc FRR có bảo đảm trên topo mà LFA không phủ hết. Mạng mới thường dùng SR-TE thay thế.
Cả hai song song: LDP cho transport mặc định, RSVP-TE chỉ cho vài LSP quan trọng; hoặc LDP-over-RSVP ở core.
6.17 QoS – classification, marking, queuing, policing và shaping
Ôn nhanh các khối của QoS (phân loại, đánh dấu DSCP/EXP, hàng đợi, policer, shaper) và mô hình DiffServ trong backbone MPLS trước khi cấu hình.
QoS không tạo thêm băng thông. Nó chỉ quyết định gói nào bị trễ hoặc bị bỏ trước khi link nghẽn. Mô hình phổ biến là DiffServ: phân loại và đánh dấu ở biên mạng, core chỉ đọc nhãn (DSCP hoặc MPLS TC/EXP) rồi xếp hàng đợi tương ứng. Cấu hình thực tế xem QoS trên IOS XR và Junos.
EXP copy từ DSCP, khi pop thì đánh lại DSCP theo EXP
Một miền QoS thống nhất
Pipe
DSCP khách giữ nguyên; PE egress xếp hàng theo EXP của ISP
Phổ biến cho L3VPN
Short pipe
DSCP khách giữ nguyên; PE egress xếp hàng theo DSCP khách
Khi khách tự quản lý QoS
Với PHP, label ngoài bị pop trước PE egress nên EXP ở label ngoài mất. Mã EXP vẫn nằm trong label VPN; muốn giữ EXP trên label transport tới tận egress thì dùng explicit null (xem MPLS LDP và RSVP-TE).
Link biên khách hàng: policing ingress theo hợp đồng, shaping egress khi tốc độ bán nhỏ hơn tốc độ port.
Backbone: thường dư băng thông, chỉ cần vài lớp (network control, voice, business, best-effort) để bảo vệ BGP/IGP và thoại khi có sự cố làm dồn traffic.
Không nên tạo quá nhiều lớp: mỗi platform chỉ có 8 hàng đợi/port và vận hành càng nhiều lớp càng khó kiểm tra.
6.18 Cấu hình QoS trên IOS XR (MQC) và Junos class-of-service
Cấu hình policing ingress cho khách, marking EXP, hàng đợi priority/bandwidth trên link core và shaping cho khách bằng class-map/policy-map trên IOS XR và class-of-service trên Junos.
Tóm tắt: IOS XR dùng MQC: class-map (match) → policy-map (hành động theo class) → service-policy input|output trên interface. Junos tách thành nhiều khối trong class-of-service: classifier (gói vào forwarding-class nào), scheduler + scheduler-map (hàng đợi ra), rewrite-rules (đánh dấu khi ra), còn policer nằm ở firewall policer. Khái niệm xem QoS concepts.
Ví dụ: PE nhận khách tại GigabitEthernet0/0/0/1 (XR) / ge-0/0/1 (Junos), khách mua 500 Mbps, thoại EF tối đa 50 Mbps. Link core HundredGigE0/0/0/0 / et-0/0/0 có 3 lớp: voice (EXP 5), network control (EXP 6), best-effort.
class-map match-any VOICE match dscp ef end-class-map!class-map match-any EXP5 match mpls experimental topmost 5 end-class-map!class-map match-any EXP6 match mpls experimental topmost 6 end-class-map!policy-map CUST-IN class VOICE police rate 50 mbps ! set mpls experimental imposition 5 ! class class-default police rate 500 mbps ! set mpls experimental imposition 0 ! end-policy-map!policy-map CORE-OUT class EXP5 priority level 1 police rate percent 20 ! ! class EXP6 bandwidth remaining percent 10 ! class class-default bandwidth remaining percent 90 ! end-policy-map!policy-map CUST-SHAPE-OUT class class-default shape average 500 mbps ! end-policy-map!interface GigabitEthernet0/0/0/1 service-policy input CUST-IN service-policy output CUST-SHAPE-OUT!interface HundredGigE0/0/0/0 service-policy output CORE-OUT!
Mặc định police rate có conform transmit, exceed drop. Muốn remark thay vì drop, khai báo trong submode police: exceed-action set mpls experimental imposition 1.
Khả năng QoS khác nhau nhiều giữa các dòng IOS XR. ASR 9000 hỗ trợ đầy đủ mẫu trên. NCS 5500/5700 dùng mô hình riêng: ingress phân loại và set traffic-class, egress queuing match theo traffic-class, một số hành động (vd marking egress) bị giới hạn. Kiểm tra QoS configuration guide cho đúng platform và release trước khi áp.
set firewall policer VOICE-50M if-exceeding bandwidth-limit 50mset firewall policer VOICE-50M if-exceeding burst-size-limit 62500set firewall policer VOICE-50M then discardset firewall policer CUST-500M if-exceeding bandwidth-limit 500mset firewall policer CUST-500M if-exceeding burst-size-limit 625kset firewall policer CUST-500M then discardset firewall family inet filter CUST-IN term VOICE from dscp efset firewall family inet filter CUST-IN term VOICE then policer VOICE-50Mset firewall family inet filter CUST-IN term VOICE then forwarding-class expedited-forwardingset firewall family inet filter CUST-IN term VOICE then acceptset firewall family inet filter CUST-IN term REST then policer CUST-500Mset firewall family inet filter CUST-IN term REST then forwarding-class best-effortset firewall family inet filter CUST-IN term REST then acceptset interfaces ge-0/0/1 unit 0 family inet filter input CUST-INset class-of-service classifiers exp CORE-EXP import defaultset class-of-service rewrite-rules exp CORE-EXP-RW forwarding-class expedited-forwarding loss-priority low code-point 101set class-of-service rewrite-rules exp CORE-EXP-RW forwarding-class network-control loss-priority low code-point 110set class-of-service rewrite-rules exp CORE-EXP-RW forwarding-class best-effort loss-priority low code-point 000set class-of-service schedulers SCH-EF transmit-rate percent 20set class-of-service schedulers SCH-EF priority strict-highset class-of-service schedulers SCH-NC transmit-rate percent 10set class-of-service schedulers SCH-NC priority lowset class-of-service schedulers SCH-BE transmit-rate remainderset class-of-service schedulers SCH-BE priority lowset class-of-service scheduler-maps CORE-MAP forwarding-class expedited-forwarding scheduler SCH-EFset class-of-service scheduler-maps CORE-MAP forwarding-class network-control scheduler SCH-NCset class-of-service scheduler-maps CORE-MAP forwarding-class best-effort scheduler SCH-BEset class-of-service interfaces et-0/0/0 scheduler-map CORE-MAPset class-of-service interfaces et-0/0/0 unit 0 classifiers exp CORE-EXPset class-of-service interfaces et-0/0/0 unit 0 rewrite-rules exp CORE-EXP-RWset class-of-service interfaces ge-0/0/1 shaping-rate 500m
Forwarding-class mặc định trên MX: best-effort (queue 0), expedited-forwarding (1), assured-forwarding (2), network-control (3). Code point EXP viết dạng 3 bit (101 = 5), DSCP dạng 6 bit hoặc alias (ef, af41). burst-size-limit tính bằng byte, thường chọn khoảng 10 ms ở tốc độ policer (500 Mbps × 10 ms = 625 KB).
Junos: strict-high không tự giới hạn theo transmit-rate. Giới hạn hàng thoại bằng policer ở ingress (như trên) để nó không chiếm hết link khi có lỗi marking.
Không chặn toàn bộ ICMPv6 trên firewall/ACL. ND (133-137) và Packet Too Big (type 2) là bắt buộc: IPv6 không fragment ở router, thiếu PTB thì PMTUD hỏng và kết nối treo với gói lớn.
Hiểu cách receiver báo tham gia nhóm (IGMP/MLD), cách PIM-SM và PIM-SSM dựng cây phân phối, vai trò của RP và kiểm tra RPF khi IPTV hoặc multicast VPN không chạy.
Multicast gửi một luồng tới nhiều receiver, router nhân bản gói tại điểm rẽ nhánh. Ba câu hỏi cần trả lời: receiver nào muốn nhận (IGMP/MLD), cây phân phối dựng thế nào (PIM), và gói đến có đúng hướng không (RPF). Ứng dụng điển hình trong ISP: IPTV, phân phối dữ liệu thị trường, multicast VPN.
Router chỉ nhận gói multicast trên interface mà chính nó dùng để đi unicast về source (hoặc về RP với cây shared). Gói đến interface khác bị bỏ để chống loop. Hầu hết sự cố "có join mà không có traffic" là RPF fail: route unicast về source đi một đường, PIM neighbor hoặc luồng thực tế đi đường khác.
Hiểu Add-Path (quảng bá nhiều path cho một prefix) và PIC edge/core (backup path dựng sẵn trong FIB) để giảm thời gian hội tụ BGP, kèm cấu hình cơ bản XR và Junos.
Mặc định BGP chỉ quảng bá một best path cho mỗi prefix. Route reflector (RR) chọn best path theo góc nhìn của nó rồi chỉ reflect path đó, nên client mất thông tin về path thứ hai. Hệ quả: khi PE đang là next-hop hỏng, client phải chờ RR chọn và gửi lại path mới, rồi mới cập nhật lại FIB cho hàng trăm nghìn prefix. Add-Path giải quyết phần "thiếu path", PIC (Prefix Independent Convergence) giải quyết phần "cập nhật FIB chậm".
flowchart LR
PE1["PE1 (next-hop chính)"] -.->|"down"| R["PE-xa"]
PE2["PE2 (next-hop backup)"] --> R
R --> F["FIB: primary PE1, backup PE2"]
F -->|"IGP báo PE1 mất"| S["Đổi 1 pointer sang PE2"]
Khi IGP báo loopback của PE1 mất, router chỉ cần đổi next-hop đã dựng sẵn (một thao tác theo BGP path-list / pointer), không phải ghi lại từng prefix. Thời gian hội tụ vì thế gần như không phụ thuộc số prefix.
Có: Add-Path đưa path backup tới PE, PIC dựng backup trong FIB
Hai khái niệm PIC:
PIC core: next-hop BGP (loopback PE) vẫn còn nhưng đường IGP tới nó đổi hoặc mất một nhánh ECMP. FIB có sẵn đường IGP backup (ví dụ nhờ TI-LFA), tất cả prefix dùng chung next-hop nên đổi cùng lúc.
PIC edge: mất hẳn PE/next-hop BGP chính (hoặc link eBGP). Router đổi sang path BGP backup đã có sẵn. Cần path backup trong BGP table, thường nhờ Add-Path, best-external hoặc RD khác nhau mỗi PE trong L3VPN.
Mỗi PE dùng RD khác nhau nên RR coi là prefix khác, reflect cả hai
Tốn bộ nhớ RR theo số PE, nhưng không cần capability mới
Add-Path
RR/ASBR quảng bá thêm path (best + backup hoặc tất cả)
Cần cả hai phía hỗ trợ, tăng số route trong BGP table
Best-external
PE có path eBGP tốt nhất nhưng đang chọn path iBGP vẫn quảng bá path eBGP
Chỉ phủ trường hợp PE ở biên
Multipath
Cả hai path active (ECMP), mất một thì còn một
Chỉ khi attribute đủ giống nhau
Add-Path làm số path trong BGP table nhân lên. Đừng bật all paths cho toàn bộ bảng định tuyến Internet. Giới hạn theo số path (path-count hoặc policy chỉ chọn một backup) và theo prefix cần bảo vệ.
Policy set path-selection có các kiểu thường gặp: backup N, all, group-best; kèm từ khoá advertise (quảng bá path đó cho peer) và install (cài path vào RIB/FIB làm backup). Kiểm tra bằng show bgp ipv4 unicast 203.0.113.0/24 (xem nhiều path và nhãn backup) và show bgp neighbors 192.0.2.2 (capability Additional-paths đã thoả thuận).
set protocols bgp group IBGP type internalset protocols bgp group IBGP local-address 192.0.2.1set protocols bgp group IBGP family inet unicast add-path receiveset protocols bgp group IBGP family inet unicast add-path send path-count 2
path-count là số path tối đa gửi cho mỗi prefix. Ngoài ra Junos có path-selection-mode (all-paths hoặc equal-cost-paths) và prefix-policy để chỉ áp cho một số prefix. Kiểm tra: show bgp neighbor 192.0.2.2 (dòng Addpath), show route 203.0.113.0/24 extensive.
PIC chủ yếu là hành vi của forwarding plane và phụ thuộc phần cứng, phiên bản:
IOS XR: BGP PIC edge/core thường có sẵn trên các nền tảng hỗ trợ (CEF hierarchical, BGP path-list), điều kiện chính là phải có path backup trong BGP. Kiểm tra bằng show cef 203.0.113.0/24 detail (có entry backup) và show bgp 203.0.113.0/24 (path đánh dấu backup).
Junos: bật protection bằng set routing-options protect core (và tương ứng trong routing-instance cho VPN), cần thiết khi chạy PIC. Xem show route 203.0.113.0/24 extensive (next-hop Indirect và Next hop có backup).
Kiểm tra kết quả bằng cách ngắt thử next-hop chính trong lab (shutdown loopback PE1 hoặc link), chạy ping/iperf liên tục tới prefix, đếm số gói mất.
Backbone có RR, mỗi prefix có từ hai PE trở lên nhận (multi-homing, dual-PE L3VPN, ASBR kép): bật Add-Path giới hạn (backup 1 hoặc path-count 2) để client biết path thứ hai.
Bảng route lớn (hàng trăm nghìn prefix trở lên) và yêu cầu hội tụ dưới 1 giây: dùng PIC kết hợp BFD để phát hiện mất next-hop nhanh.
Không cần khi mạng nhỏ, hội tụ vài giây vẫn chấp nhận được, hoặc RR/PE chưa hỗ trợ capability (Add-Path cần hai phía). Hãy kiểm tra phiên bản phần mềm và license trước khi bật.
Dùng khi cần gateway dự phòng cho một subnet - cấu hình VRRP có priority, preempt, track interface trên IOS XR và Junos, kiểm tra master và so nhanh với HSRP.
Tóm tắt: Hai router chia sẻ một virtual IP (VIP) và virtual MAC 00:00:5e:00:01:<vrid>. Router priority cao nhất là master (gửi advertisement tới 224.0.0.18, IP protocol 112), router còn lại là backup. Host trỏ default gateway về VIP. Dùng track để hạ priority khi uplink hỏng, nếu không master vẫn giữ vai trò dù đã mất đường ra ngoài. HSRP là giao thức độc quyền của Cisco, cùng ý tưởng nhưng khác cú pháp (xem cuối trang).
Khi R1 mất uplink, priority của R1 giảm dưới R2, hoặc R2 không nhận advertisement trong master-down-interval (khoảng 3 x advertisement interval + skew) thì R2 lên master.
set interfaces ge-0/0/1 unit 0 family inet address 192.0.2.3/24 vrrp-group 10 virtual-address 192.0.2.1set interfaces ge-0/0/1 unit 0 family inet address 192.0.2.3/24 vrrp-group 10 priority 100set interfaces ge-0/0/1 unit 0 family inet address 192.0.2.3/24 vrrp-group 10 accept-dataset interfaces ge-0/0/1 unit 0 family inet address 192.0.2.3/24 vrrp-group 10 track interface xe-0/0/0 priority-cost 30
Junos mặc định preempt bật (dùng no-preempt để tắt, preempt hold-time <giây> để trì hoãn).
accept-data cho phép VIP trả lời ping/traffic gửi tới VIP khi đang là master (mặc định Junos bỏ qua).
track route <prefix> routing-instance <ri> priority-cost <n> theo dõi route thay cho interface.
Advertisement mặc định 1 giây nên failover khoảng 3 giây. Muốn nhanh hơn thì chạy BFD hoặc giảm interval (Junos: fast-interval <ms>), nhưng hai đầu phải khớp và phải test CPU load trước khi áp rộng.
Hai đầu phải cùng VRID, cùng VIP và cùng version VRRP (v2 hay v3) trên interface. Chỉ ghép Junos với IOS XR sau khi đã thử trong lab, vì giá trị mặc định khác nhau giữa các phiên bản.
Tra nhanh lệnh xem và sửa tạm IP, route, link, ARP/NDP, socket và DNS trên Linux hiện đại.
Dùng iproute2 (ip, ss) thay cho ifconfig/netstat/route (gói net-tools đã cũ). Thay đổi bằng ipmất sau reboot; muốn bền thì cấu hình qua netplan hoặc NetworkManager.
Bắt trên link tải lớn mà không có filter sẽ làm đầy đĩa rất nhanh và tốn CPU. Luôn có filter, -c hoặc -C/-W giới hạn dung lượng.
Session BGP đứng ở Active: nếu thấy SYN đi ra mà nhận về RST thì peer không listen hoặc ACL/neighbor chưa khai báo; không thấy gì quay về thì nghi đường đi/firewall. Xem thêm BGP troubleshooting.
Dùng khi cần chạy một app/script như service tự khởi động lại, đọc env từ file, hoặc lập lịch bằng timer thay cron.
Tóm tắt: Đặt unit tự viết ở /etc/systemd/system/, chạy dưới user riêng, Restart=on-failure, env đọc từ EnvironmentFile=. Việc định kỳ dùng cặp .service (Type=oneshot) + .timer (OnCalendar=, Persistent=true). Sau mỗi lần sửa file: systemctl daemon-reload.
[Unit]Description=Net collector (gNMI -> InfluxDB)Documentation=https://example.com/netcollector# Chờ mạng thật sự có IP, không chỉ "network.target"Wants=network-online.targetAfter=network-online.target# Restart quá 5 lần trong 5 phút thì dừng hẳn (state failed)StartLimitIntervalSec=300StartLimitBurst=5[Service]Type=execUser=netcollectorGroup=netcollectorWorkingDirectory=/opt/netcollector# Dấu "-" = file không tồn tại thì bỏ qua, không lỗiEnvironmentFile=-/etc/default/netcollectorEnvironment=LOG_LEVEL=infoExecStart=/opt/netcollector/.venv/bin/python -m netcollector --config /etc/netcollector/config.ymlExecReload=/bin/kill -HUP $MAINPIDRestart=on-failureRestartSec=5# Giới hạn tài nguyên (cgroups v2)MemoryMax=512MCPUQuota=50%# Hardening cơ bảnNoNewPrivileges=yesProtectSystem=strictProtectHome=yesPrivateTmp=yesStateDirectory=netcollectorLogsDirectory=netcollector[Install]WantedBy=multi-user.target
/etc/default/netcollector
# KEY=VALUE, không có "export"; quyền 0600, owner rootINFLUX_URL=http://127.0.0.1:8086INFLUX_TOKEN=***
Directive
Ý nghĩa
Wants= / Requires=
Kéo unit khác lên cùng; Requires thì unit kia fail sẽ kéo theo unit này. Không quy định thứ tự
After= / Before=
Chỉ quy định thứ tự khởi động, thường dùng kèm Wants=
Type=simple / exec
Process chính chính là service; exec báo lỗi ngay nếu binary không chạy được
Type=forking
Daemon kiểu cũ tự fork, nên kèm PIDFile=
Type=notify
App tự báo sẵn sàng qua sd_notify
Type=oneshot
Chạy xong thoát, dùng cho job/timer
Restart=
no, on-failure, on-abnormal, always... on-failure hợp lý cho đa số app
StateDirectory=
Tạo /var/lib/<tên> thuộc User=, ghi được dù ProtectSystem=strict
sudo systemctl edit nginx # tạo /etc/systemd/system/nginx.service.d/override.confsystemctl cat nginx # xem unit gốc + mọi drop-in
/etc/systemd/system/nginx.service.d/override.conf
[Service]Restart=on-failureRestartSec=3# Với list như ExecStart= phải reset bằng dòng rỗng trước khi gán lạiExecStart=ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
[Unit]Description=Backup config thiết bị mạngWants=network-online.targetAfter=network-online.target[Service]Type=oneshotUser=netbackupEnvironmentFile=/etc/default/config-backupExecStart=/opt/netbackup/run.sh
/etc/systemd/system/config-backup.timer
[Unit]Description=Chạy config-backup 02:00 thứ Hai đến thứ Sáu[Timer]OnCalendar=Mon..Fri *-*-* 02:00:00# Máy tắt lúc đến giờ thì chạy bù khi bật lạiPersistent=trueRandomizedDelaySec=5min[Install]WantedBy=timers.target
Cron
OnCalendar=
*/15 * * * *
*:0/15
0 * * * *
hourly
0 0 * * *
daily
0 2 * * 1
Mon *-*-* 02:00:00
30 8 1 * *
*-*-01 08:30:00
Sau boot 5 phút, rồi mỗi giờ
OnBootSec=5min + OnUnitActiveSec=1h (không dùng OnCalendar)
Tra nhanh lệnh journalctl để lọc log theo unit, thời gian, mức độ, boot, xuất JSON và dọn dung lượng journal.
Journal chỉ bền qua reboot khi có thư mục /var/log/journal (mặc định trên Ubuntu). User thường cần nằm trong group systemd-journal hoặc adm để đọc log hệ thống. Các flag lọc kết hợp được với nhau.
Giới hạn cố định đặt trong /etc/systemd/journald.conf (hoặc drop-in /etc/systemd/journald.conf.d/*.conf):
/etc/systemd/journald.conf.d/size.conf
[Journal]SystemMaxUse=1GMaxRetentionSec=1month
sudo systemctl restart systemd-journald
Service fail mà không rõ lý do: systemctl status <unit> chỉ hiện vài dòng; dùng journalctl -u <unit> -b -n 200 --no-pager để thấy đủ. Xem thêm systemd service.
7.5 SSH config – jump host, ControlMaster, agent, port forward
Dùng khi phải SSH qua bastion tới router/server, muốn tái sử dụng kết nối cho Ansible, hoặc cần tunnel port.
Tóm tắt: Khai báo mọi host trong ~/.ssh/config: bastion dùng ProxyJump, bật ControlMaster auto + ControlPersist để các lần SSH sau dùng lại socket (nhanh hơn nhiều với Ansible/scp). Không bật ForwardAgent bừa bãi; qua bastion thì ProxyJump đã đủ. Cần OpenSSH ≥ 7.3 cho ProxyJump.
flowchart LR
L["Laptop (key trong ssh-agent)"] -->|"SSH 1: tới bastion"| B["bastion.example.com"]
B -->|"TCP forward, không giải mã"| R1["pe1 198.51.100.11"]
B -->|"TCP forward"| R2["pe2 198.51.100.12"]
L -.->|"SSH 2: end-to-end qua đường hầm"| R1
L --> S["ControlPath socket"]
S -.->|"lần sau dùng lại"| L
Với ProxyJump, phiên SSH tới router được mã hoá end-to-end từ laptop; bastion chỉ chuyển tiếp TCP, private key không rời laptop.
# Rule đầu tiên khớp sẽ thắng cho từng option: đặt Host cụ thể lên trên, "Host *" ở cuốiHost bastion HostName bastion.example.com User ops Port 22 IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes# Router/thiết bị sau bastionHost pe1 HostName 198.51.100.11Host pe2 HostName 198.51.100.12Host pe* 198.51.100.* User automation ProxyJump bastion# Thiết bị cũ chỉ hỗ trợ thuật toán yếu: chỉ mở cho đúng nhóm nàyHost legacy-* KexAlgorithms +diffie-hellman-group14-sha1 HostKeyAlgorithms +ssh-rsa PubkeyAcceptedAlgorithms +ssh-rsaHost * ServerAliveInterval 30 ServerAliveCountMax 3 ControlMaster auto ControlPath ~/.ssh/cm-%C ControlPersist 10m AddKeysToAgent yes HashKnownHosts yes
Option
Ý nghĩa
ProxyJump bastion
Tương đương ssh -J bastion. Nhiều tầng: ProxyJump jump1,jump2
ControlMaster auto
Có socket sẵn thì dùng lại, chưa có thì tạo master mới
ControlPath ~/.ssh/cm-%C
%C là hash của host/port/user, đường dẫn ngắn, tránh lỗi socket path quá dài
ControlPersist 10m
Master chạy nền thêm 10 phút sau khi phiên cuối đóng
IdentitiesOnly yes
Chỉ thử key chỉ định, tránh Too many authentication failures khi agent có nhiều key
ServerAliveInterval 30
Gửi keepalive mỗi 30s, giữ phiên qua NAT/firewall
AddKeysToAgent yes
Tự nạp key vào agent sau lần nhập passphrase đầu
chmod 600 ~/.ssh/config và chmod 700 ~/.ssh, nếu không ssh có thể từ chối đọc file.
eval "$(ssh-agent -s)"ssh-add ~/.ssh/id_ed25519ssh-add -l # key đang có trong agentssh-add -t 4h ~/.ssh/id_ed25519 # key tự hết hạn sau 4 giờ
ForwardAgent yes cho phép root trên máy đích dùng agent của bạn để SSH đi nơi khác trong lúc bạn đang kết nối. Chỉ bật cho host thật sự tin cậy và cần (vd git pull trên server). Đi qua bastion thì dùng ProxyJump, không cần agent forwarding.
ssh -O check pe1 # master còn sống khôngssh -O exit pe1 # đóng master (khi đổi config, đổi key)ssh -G pe1 | grep -Ei '^(hostname|user|proxyjump|controlpath) ' # config cuối cùng áp cho hostssh -vvv pe1 # debug thuật toán, key được thử
Dùng khi cấu hình mạng bền vững trên Ubuntu Server (IP tĩnh, LACP bond, VLAN, bridge cho VM) và áp dụng an toàn qua SSH.
Tóm tắt: Netplan đọc /etc/netplan/*.yaml (theo thứ tự tên file), sinh cấu hình cho backend networkd (server) hoặc NetworkManager (desktop). Khi làm qua SSH luôn dùng sudo netplan try (tự rollback sau 120s nếu không xác nhận) trước khi netplan apply. Ví dụ dưới dùng cú pháp routes: - to: default; gateway4 đã bị deprecate.
flowchart LR
A["/etc/netplan/*.yaml"] --> B["netplan generate"]
B --> C["/run/systemd/network/*"]
B --> D["NetworkManager profiles"]
C --> E["systemd-networkd"]
A --> F{"netplan try"}
F -->|"Enter trong timeout"| G["giữ cấu hình"]
F -->|"không xác nhận"| H["rollback"]
G --> I["netplan apply"]
sudo chmod 600 /etc/netplan/*.yamlsudo netplan generate # kiểm cú pháp, sinh file backend, chưa áp dụngsudo netplan try --timeout 120 # áp dụng thử, Enter để giữ, không thì tự rollbacksudo netplan apply # áp dụng chính thứcsudo netplan get # cấu hình hợp nhất từ mọi filesudo netplan --debug apply # xem chi tiết khi lỗinetworkctl status bond0 # trạng thái theo networkdcat /proc/net/bonding/bond0 # LACP partner, slave nào upip -d link show bond0.100 # xác nhận VLAN id
netplan apply không xoá thiết bị ảo (bond, bridge, VLAN) đã bỏ khỏi YAML; phải ip link del <dev> hoặc reboot. netplan try cũng có giới hạn với thiết bị ảo và đôi khi rollback không trọn: hãy có đường console/IPMI dự phòng khi đổi bond/bridge trên máy ở xa.
Luôn đặt biến trong nháy kép ("$var", "${arr[@]}") để tránh tách từ và glob. Script dài hơn ~150 dòng hoặc cần parse JSON phức tạp thì chuyển sang Python. Xem thêm regex cheatsheet.
7.8 Linux namespaces và cgroups v2 – nền tảng của container
Hiểu namespace (netns, veth) và cgroups v2 để debug mạng container, giới hạn tài nguyên và đọc đúng các lệnh docker/systemd.
Container không phải VM: đó là process bình thường của kernel host, được cô lập bằng namespace (thấy gì) và bị giới hạn bằng cgroups (dùng được bao nhiêu). Docker, containerd, Kubernetes, systemd đều chỉ là lớp điều khiển hai cơ chế này.
flowchart LR
E1["ctr1 eth0 (netns ctr1)"] --- V1["vethA trên host"]
E2["ctr2 eth0 (netns ctr2)"] --- V2["vethB trên host"]
V1 --- BR["bridge docker0"]
V2 --- BR
BR -->|"NAT MASQUERADE"| UP["eno1 ra Internet"]
veth là cặp interface nối như sợi cáp: một đầu trong netns của container, đầu kia gắn vào bridge trên host. Ra ngoài thì đi qua NAT trên host (Docker mặc định bridge docker0 dải 172.17.0.0/16).
Tự dựng lại để hiểu (chạy trên máy lab, cần root):
netns-lab.sh
ip netns add redip link add veth-red type veth peer name veth-hostip link set veth-red netns redip addr add 198.51.100.1/24 dev veth-host && ip link set veth-host upip -n red addr add 198.51.100.2/24 dev veth-redip -n red link set veth-red up && ip -n red link set lo upip netns exec red ping -c2 198.51.100.1ip netns del red # xoá netns, veth đi kèm cũng mất
Mục đích
Lệnh
Liệt kê namespace đang có
lsns / lsns -t net
netns tạo bằng ip netns
ip netns list
Vào netns của container Docker (dùng tool của host)
nsenter -t $(docker inspect -f '{{.State.Pid}}' <ctr>) -n ip addr
tcpdump bên trong container không có tcpdump
nsenter -t <pid> -n tcpdump -i eth0 -nn
Đầu veth trên host ứng với container
ip -n <netns> link xem eth0@ifN, rồi ip link | grep '^N:' trên host
cgroups v2 (unified hierarchy) là một cây duy nhất tại /sys/fs/cgroup, mặc định trên Ubuntu 22.04+, Debian 11+, RHEL 9. systemd quản lý cây này: mỗi service là một cgroup dưới system.slice, container Docker nằm dưới system.slice/docker-<id>.scope (với cgroup driver systemd).
Dùng khi gặp lỗi No space left on device: tìm thư mục/file lớn, hết inode, file đã xoá vẫn bị giữ, journal và Docker chiếm chỗ.
Triệu chứng chính: app báo No space left on device, df -h thấy 100%, hoặc df còn trống mà vẫn không ghi được file (hết inode), hoặc đã xoá file lớn mà df không giảm.
flowchart TD
A["No space left on device"] --> B{"df -h đầy?"}
B -->|Không| C["df -i: hết inode"]
B -->|Có| D["du -xh --max-depth=1 / hoặc ncdu -x /"]
D --> E{"du khớp df?"}
E -->|Không| F["lsof +L1: file đã xoá còn mở"]
E -->|Có| G["Xử lý thư mục lớn: log, journal, docker"]
sudo lsof -nP +L1 # cột NLINK = 0, SIZE lớn# Giải phóng mà không restart: truncate qua file descriptorsudo truncate -s 0 /proc/<pid>/fd/<fd># Hoặc restart service đang giữ filesudo systemctl restart <service>
# journaljournalctl --disk-usagesudo journalctl --vacuum-size=500M# aptsudo apt-get cleansudo apt-get autoremove --purge# Docker: xem trước rồi mới xoádocker system dfdocker container prune # container đã dừngdocker image prune -a # image không có container dùngdocker builder prune # build cachedocker system prune # gộp: container dừng, network thừa, dangling image, build cachesudo du -h /var/lib/docker/containers/*/*-json.log | sort -h | tail
docker system prune -a --volumes xoá luôn volume không có container nào gắn, tức có thể là dữ liệu database. Chỉ thêm --volumes khi đã chắc chắn. Không rm trực tiếp trong /var/lib/docker.
Dùng khi server chậm hoặc load average cao, để xác định nghẽn CPU, RAM/swap, đĩa hay process bị OOM killer giết.
Triệu chứng chính:uptime báo load average cao hơn nhiều so với số CPU, SSH vào chậm, app timeout, hoặc process tự biến mất (log kernel có Out of memory: Killed process).
flowchart TD
A["uptime: load cao"] --> B["vmstat 1"]
B -->|"r lớn hơn số CPU, us/sy cao"| C["CPU: top, pidstat -u"]
B -->|"b lớn, wa cao"| D["I/O: iostat -xz, pidstat -d"]
B -->|"si/so khác 0"| E["RAM: free -h, ps theo RSS"]
E --> F["OOM? journalctl -k"]
B -->|"st cao"| G["VM bị steal CPU"]
Quy trình 60 giây (theo thứ tự):
uptime # load 1/5/15 phút; so với nprocdmesg -T | tail -20 # OOM, lỗi đĩa, NICvmstat 1 5 # r, b, si, so, us, sy, wa, stmpstat -P ALL 1 3 # có CPU nào 100% một mình? (app đơn luồng, IRQ)pidstat 1 3 # CPU theo processiostat -xz 1 3 # %util, r_await/w_await, aqu-sz theo đĩafree -h # nhìn cột availablesar -n DEV 1 3 # throughput NICtop # nhấn 1: từng CPU, M: sort RAM, P: sort CPU
mpstat, pidstat, iostat, sar nằm trong gói sysstat.
ps -eo pid,ppid,user,%cpu,%mem,rss,stat,cmd --sort=-%cpu | headps -eo pid,user,rss,cmd --sort=-rss | head # RSS theo KBps -eo stat,pid,cmd | awk '$1 ~ /^D/' # process bị kẹt I/Ocat /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io # PSI: % thời gian bị nghẽnsystemd-cgtop # theo service/container
Dùng khi máy có nhiều uplink/ISP cần trả lời đúng cổng theo source IP, hoặc cần tách mạng management bằng VRF trên Linux.
Tóm tắt: Kernel duyệt ip rule theo pref tăng dần; rule nào khớp thì tra route trong table của rule đó, không thấy route thì đi tiếp rule sau. Mặc định có 3 rule: 0 local, 32766 main, 32767 default. Policy routing = thêm rule (from/to/fwmark/iif...) trỏ tới table riêng. VRF = interface ảo type vrf gắn với một table, các interface "enslave" vào VRF chỉ dùng table đó. Lệnh ip dưới đây mất sau reboot; bản bền xem phần netplan.
Ví dụ: eth0 198.51.100.10/24 (gw .1, ISP1, default trong main), eth1 203.0.113.10/24 (gw .1, ISP2). Không có policy routing thì gói trả lời cho traffic vào eth1 vẫn đi ra eth0 → bị ISP2 / rp_filter drop.
# Table riêng cho ISP2: connected + defaultsudo ip route add 203.0.113.0/24 dev eth1 src 203.0.113.10 table isp2sudo ip route add default via 203.0.113.1 dev eth1 table isp2# Rule: gói có source 203.0.113.10 → tra table isp2sudo ip rule add from 203.0.113.10 table isp2 pref 100# Kiểm traip rule showip route show table isp2ip route get 192.0.2.50 from 203.0.113.10 # phải ra dev eth1
# Tạo VRF mgmt dùng table 10, gán eth0 vàosudo ip link add mgmt type vrf table 10sudo ip link set mgmt upsudo ip link set eth0 master mgmt # IP của eth0 giữ nguyên, route connected chuyển sang table 10sudo ip route add default via 198.51.100.1 table 10# Xemip vrf showip -br link show master mgmtip route show vrf mgmtip rule show # kernel tự thêm rule "l3mdev" (pref 1000)# Chạy lệnh trong VRFsudo ip vrf exec mgmt ping -c3 198.51.100.1sudo ip vrf exec mgmt ssh admin@198.51.100.20
Socket mở ở VRF mặc định không nhận kết nối đến từ interface trong VRF. Để sshd (chạy ở default VRF) nhận được SSH vào cổng mgmt:
Netplan từ 0.105 (Ubuntu 22.04 có 0.106) hỗ trợ khoá vrfs: (table, interfaces, routes, routing-policy); xem man netplan. Áp dụng bằng netplan try như trong Netplan.
Dùng khi cần thử app/automation trong điều kiện mạng xấu (WAN chậm, mất gói, băng thông thấp) ngay trên một máy Linux, và gỡ bỏ sạch sau khi thử.
Tóm tắt:tc gắn qdisc vào interface. netem thêm delay/jitter/loss/duplicate/reorder/corrupt, tbf giới hạn băng thông. Qdisc chỉ tác động chiều ra (egress) của interface đó; muốn tác động chiều vào thì chuyển traffic sang interface ifb. Tất cả là runtime, mất sau reboot, gỡ bằng tc qdisc del dev <if> root.
Đặt netem lên interface đang SSH vào có thể làm mất phiên. Hẹn giờ tự gỡ trước khi thử: sudo systemd-run --on-active=5m /usr/sbin/tc qdisc del dev eth0 root.
tc qdisc show dev eth0tc -s qdisc show dev eth0 # kèm counter: sent, dropped, overlimitssudo tc qdisc del dev eth0 root # gỡ toàn bộ, về mặc định (fq_codel / mq)
sudo modprobe ifbsudo ip link add ifb0 type ifbsudo ip link set ifb0 upsudo tc qdisc add dev eth0 handle ffff: ingresssudo tc filter add dev eth0 parent ffff: matchall action mirred egress redirect dev ifb0sudo tc qdisc add dev ifb0 root netem delay 100ms# Gỡsudo tc qdisc del dev eth0 ingresssudo tc qdisc del dev ifb0 rootsudo ip link del ifb0
Tra nhanh lệnh đo băng thông, loss theo hop, kiểm tra MTU, port mở và thời gian từng pha của một request HTTP.
Bộ lệnh để trả lời "mạng chậm/đứt ở đâu". Gói trên Ubuntu: iperf3, mtr-tiny (hoặc mtr), iputils-ping, iputils-tracepath, netcat-openbsd, curl. Lệnh xem IP/route/socket nằm ở Linux network cheatsheet.
Dùng khi gặp Temporary failure in name resolution, SERVFAIL, tên nội bộ không phân giải được, hoặc kết quả DNS cũ trên Ubuntu dùng systemd-resolved.
Triệu chứng chính:Temporary failure in name resolution, Could not resolve host, dig ra SERVFAIL/NXDOMAIN, tên nội bộ (split DNS) không phân giải được trong khi tên public thì được, hoặc đã đổi record mà máy vẫn trả IP cũ.
Trên Ubuntu, app gọi glibc → /etc/nsswitch.conf (hosts: files ... dns) → /etc/hosts rồi /etc/resolv.conf → stub 127.0.0.53 (systemd-resolved) → DNS server thật theo từng link. Khoanh vùng = hỏi từng tầng.
flowchart TD
A["App lỗi resolve"] --> B{"getent hosts name OK?"}
B -->|Có| C["Lỗi ở app: cache, proxy, container"]
B -->|Không| D{"resolvectl query name OK?"}
D -->|Có| E["Kiểm tra nsswitch, /etc/resolv.conf"]
D -->|Không| F{"dig @server-thật name OK?"}
F -->|Có| G["resolved: link, routing domain, DNSSEC"]
F -->|Không| H["Lỗi ở DNS server / zone / firewall"]
ls -l /etc/resolv.conf # nên là -> ../run/systemd/resolve/stub-resolv.confcat /run/systemd/resolve/resolv.conf # DNS server thật mà resolved đang dùngresolvectl status # server + domain theo từng link, Current DNS Servergrep ^hosts /etc/nsswitch.conf
getent hosts app.example.com # đúng đường app đi (gồm /etc/hosts)resolvectl query app.example.com # qua resolved, cho biết link nào trả lờiresolvectl query --cache=no app.example.com # bỏ qua cachedig app.example.com # qua 127.0.0.53dig @198.51.100.53 app.example.com # hỏi thẳng server, bỏ qua resolveddig +tcp @198.51.100.53 app.example.com # nghi UDP 53 bị chặn / response lớn
Đặt DNS bền qua netplan (nameservers:). Cấu hình chung cho resolved đặt ở drop-in:
/etc/systemd/resolved.conf.d/10-local.conf
[Resolve]# Split DNS: chỉ gửi *.corp.example.com tới server nội bộDNS=198.51.100.53Domains=~corp.example.com# Tắt stub 127.0.0.53 khi chạy DNS server riêng trên port 53#DNSStubListener=no
sudo systemctl restart systemd-resolved# Nếu tắt stub: trỏ resolv.conf sang file chứa server thậtsudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
resolvectl dns / resolvectl domain chỉ là runtime, mất khi link up/down hoặc reboot. Dùng để thử, sau đó đưa vào netplan hoặc resolved.conf.d.
Dùng khi cần đặt job định kỳ bằng crontab, debug job cron không chạy, hoặc cấu hình logrotate cho log ứng dụng và thử bằng logrotate -d.
Tóm tắt: Cron chạy job với môi trường tối giản (SHELL=/bin/sh, PATH=/usr/bin:/bin, không đọc .bashrc) nên lỗi phổ biến nhất là "chạy tay được, cron thì không". Logrotate trên Ubuntu 22.04 được gọi mỗi ngày bởi logrotate.timer (systemd), đọc /etc/logrotate.conf + /etc/logrotate.d/*. Luôn thử config mới bằng logrotate -d (không thay đổi gì). Job mới cần log, retry, phụ thuộc service thì cân nhắc systemd timer.
flowchart LR
A["crontab -e (user)"] --> C["cron daemon"]
B["/etc/cron.d/* (có cột user)"] --> C
C -->|"mỗi phút so lịch"| D["/bin/sh -c 'lệnh'"]
D --> E["output: mail hoặc file log"]
T["logrotate.timer (daily)"] --> L["logrotate /etc/logrotate.conf"]
L --> R["/etc/logrotate.d/myapp"]
R --> S["rotate, compress, postrotate"]
SHELL=/bin/bashPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binMAILTO=""# m h dom mon dow user command15 1 * * * netops flock -n /tmp/netbackup.lock /opt/netbackup/run.sh >> /var/log/netbackup/run.log 2>&1
flock -n: lần chạy trước chưa xong thì bỏ qua, tránh chồng job.
Redirect >> ... 2>&1: không có thì output gửi mail (thường mất nếu không có MTA).
Ký tự % trong lệnh cron là xuống dòng; phải viết \%, ví dụ date +\%F.
File trong /etc/cron.d/ và /etc/cron.daily/: tên chỉ dùng chữ, số, -, _ (có dấu . như backup.sh sẽ bị run-parts bỏ qua), file cron.d phải thuộc root và không cho group/other ghi.
Biến từ .env thì load trong script, không dựa vào môi trường shell của bạn.
sudo logrotate -d /etc/logrotate.d/myapp # dry-run: in ra sẽ làm gì, không đổi filesudo logrotate -v -f /etc/logrotate.d/myapp # ép xoay ngay, in chi tiếtcat /var/lib/logrotate/status | grep myapp # lần xoay cuốisystemctl list-timers logrotate.timer # lần chạy kế tiếp
-d cũng không cập nhật state, nên kết quả kiểu log does not need rotating là do state hoặc điều kiện lịch, không phải lỗi cú pháp.
Tra nhanh lệnh tạo/sửa user và group, cấp sudo giới hạn qua sudoers.d, chmod/chown và ACL cho thư mục dùng chung.
Lệnh dạng Ubuntu/Debian, cần sudo. useradd là tool cấp thấp (không tự tạo home nếu thiếu -m), adduser là wrapper hỏi tương tác. Hardening SSH xem SSH hardening.
# Group netops: chỉ restart/xem trạng thái 2 service, không cần mật khẩu%netops ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx# User deploy: chạy mọi lệnh nhưng phải nhập mật khẩudeploy ALL=(ALL:ALL) ALL
Luôn sửa bằng visudo: file sudoers lỗi cú pháp làm mọi lệnh sudo hỏng. File trong /etc/sudoers.d/ có dấu . hoặc kết thúc bằng ~ bị bỏ qua; quyền nên là 0440. Cấp NOPASSWD cho lệnh có thể mở shell (vim, less, find, tar...) là cấp root.
Tra nhanh lệnh xem ổ đĩa, mở rộng LVM (thêm disk hoặc disk ảo được tăng size), tạo volume mới và mount bền bằng fstab theo UUID.
Mô hình: disk/partition → PV (pvcreate) → gom vào VG (vgcreate/vgextend) → cắt thành LV (lvcreate/lvextend) → filesystem → mount. Ubuntu Server cài mặc định thường để LV ubuntu-vg/ubuntu-lv chỉ dùng một phần VG: kiểm tra vgs trước khi thêm disk. Ổ đầy cần dọn trước: Ổ đĩa đầy.
growpart /dev/sda 3 (gói cloud-guest-utils, có khoảng trắng giữa disk và số partition)
Báo LVM PV đã lớn hơn
pvresize /dev/sda3
Resize filesystem thủ công (nếu quên -r)
ext4: resize2fs /dev/ubuntu-vg/ubuntu-lv; XFS: xfs_growfs / (theo mount point)
# Quy trình hay gặp: disk VM tăng từ 50G lên 100G, LV root nằm trên /dev/sda3lsblk # xác nhận sda đã 100G, sda3 vẫn nhỏsudo growpart /dev/sda 3sudo pvresize /dev/sda3sudo lvextend -r -l +100%FREE /dev/ubuntu-vg/ubuntu-lvdf -h /
Mở rộng ext4/XFS làm online được. Thu nhỏ (lvreduce) thì XFS không hỗ trợ, ext4 phải umount và resize filesystem trước LV; sai thứ tự là mất dữ liệu. Backup trước khi làm.
sudo blkid -s UUID -o value /dev/data-vg/data # lấy UUIDsudo findmnt --verify # kiểm tra fstab trước khi rebootsudo systemctl daemon-reload # systemd sinh lại mount unit từ fstabsudo mount -a && findmnt /srv/data
Disk dữ liệu (không phải root) nên có nofail: disk hỏng/thiếu thì máy vẫn boot thay vì rơi vào emergency mode. Fstab sai làm máy không boot: xem Linux không boot.
7.18 rsync, scp, sftp – copy file giữa máy và qua jump host
Tra nhanh lệnh rsync (đồng bộ, xoá thừa, exclude, dry-run, resume, giới hạn băng thông), scp/sftp và cách đi qua bastion.
rsync chỉ gửi phần khác biệt, resume được, có dry-run: dùng cho mọi thứ lớn hơn vài file. scp cho copy nhanh một lần. Cả hai đi qua SSH nên đọc ~/.ssh/config (alias, ProxyJump): xem SSH config & jump host.
Dấu / cuối nguồn quyết định kết quả: rsync -a src/ dst/ copy nội dungsrc vào dst; rsync -a src dst/ tạo dst/src/. Đích không ảnh hưởng. Luôn chạy -n (dry-run) trước khi thêm --delete.
Tra nhanh one-liner lọc, đếm, tính tổng, sửa và định dạng log hoặc output show của thiết bị mạng bằng công cụ có sẵn trên Linux.
Ví dụ chạy trên GNU coreutils/gawk/GNU sed (Ubuntu). Cột trong output thiết bị cách nhau bằng nhiều khoảng trắng: awk mặc định tách theo khoảng trắng liên tiếp nên hợp hơn cut. JSON/YAML thì dùng jq/yq, cú pháp regex xem Regex cheatsheet.
Dùng khi máy Ubuntu rơi vào emergency mode, dừng ở prompt initramfs hoặc grub, hoặc không boot sau khi sửa fstab, đổi disk, nâng kernel.
Triệu chứng chính: màn hình You are in emergency mode. After logging in, type "journalctl -xb"..., prompt (initramfs), prompt grub> / grub rescue>, hoặc kernel panic VFS: Unable to mount root fs. Cần console (iDRAC/iLO, console VM, cloud serial console); SSH thường không có.
journalctl -xb -p err # lỗi của lần boot nàysystemctl --failed # unit mount nào hỏngmount -o remount,rw / # root đang ro thì remount rw để sửalsblk -f # UUID thật của các deviceblkidnano /etc/fstab # sửa UUID, thêm nofail hoặc comment dòng lỗifindmnt --verify # kiểm tra fstabsystemctl daemon-reload && mount -a # thử mount, không lỗi mới rebootreboot
# Root trên /dev/sda2 (LVM: vgchange -ay rồi dùng /dev/mapper/...), EFI trên /dev/sda1sudo mount /dev/sda2 /mntsudo mount /dev/sda1 /mnt/boot/efi # chỉ với UEFIfor d in dev proc sys run; do sudo mount --bind /$d /mnt/$d; donesudo chroot /mntgrub-install /dev/sda # BIOS; UEFI: grub-install --target=x86_64-efi --efi-directory=/boot/efiupdate-initramfs -u -k allupdate-grubexitsudo umount -R /mnt && sudo reboot
Tra nhanh lệnh strace, lsof, ltrace và /proc để biết process đang mở file nào, treo ở syscall nào, kết nối tới đâu.
Dùng khi process treo, báo lỗi mơ hồ (permission denied, connection refused không rõ file/địa chỉ nào) hoặc nghi rò file descriptor. Cần quyền root (hoặc cùng user) để attach process khác. Xem thêm Load cao, máy chậm và tcpdump.
strace -p làm process chậm đi đáng kể vì dừng ở mỗi syscall. Trên dịch vụ production đang tải cao, hạn chế bằng -e trace= và -c, rồi detach (Ctrl+C) sớm. Trace có thể lộ dữ liệu nhạy cảm (password, token) trong tham số read/write, đừng dán nguyên file trace lên nơi công khai.
Đọc kết quả: dòng như openat(AT_FDCWD, "/etc/app.conf", O_RDONLY) = -1 ENOENT (No such file or directory) cho biết chính xác file nào thiếu. Thứ tự hay gặp: ENOENT (không có file), EACCES (không đủ quyền), ECONNREFUSED (cổng không lắng nghe), EAGAIN (non-blocking, thường vô hại).
Luôn thêm -nP (không resolve tên host/port) cho nhanh.
Mục đích
Lệnh
File đang mở bởi process
lsof -nP -p <pid>
Theo tên lệnh / theo user
lsof -nP -c nginx / lsof -nP -u www-data
Ai đang lắng nghe TCP
lsof -nP -iTCP -sTCP:LISTEN
Ai dùng cổng 443
lsof -nP -i :443
Kết nối tới một host
lsof -nP -i @203.0.113.5
Chỉ IPv4 / IPv6
lsof -nP -i4 / lsof -nP -i6
Ai đang mở file này
lsof /var/log/app.log
Ai đang giữ filesystem (khó umount)
lsof /mnt/data (đường dẫn là mount point)
Mọi file dưới thư mục
lsof +D /var/log (chậm trên cây lớn)
File đã xoá nhưng còn mở (chiếm đĩa)
lsof -nP +L1
Unix socket
lsof -nP -U
Chỉ in PID (dùng trong script)
lsof -t -i :8080
Kết hợp điều kiện bằng AND
lsof -nP -a -u root -i TCP
Lặp lại mỗi 2 giây
lsof -nP -r 2 -i :443
Ổ đĩa đầy mà du không thấy file nào: chạy lsof -nP +L1, cột SIZE/OFF lớn là file đã bị xoá nhưng process còn giữ. Restart process hoặc truncate qua : > /proc/<pid>/fd/<fd>. Xem thêm Disk đầy.
Cột FD của lsof: cwd (thư mục hiện hành), txt (binary), mem (thư viện mmap), số kèm r/w/u (đọc/ghi/cả hai).
ltrace theo dõi lời gọi thư viện (libc...) thay vì syscall. Không phải máy nào cũng cài (apt install ltrace), và có thể không hoạt động với binary PIE/static hoặc Go.
Mục đích
Lệnh
Trace lời gọi thư viện
ltrace <lệnh>
Kèm cả syscall
ltrace -S <lệnh>
Thống kê
ltrace -c <lệnh>
Attach process
ltrace -p <pid>
8. Security
8.1 TLS và PKI – chain of trust, CA, CSR, SAN, mTLS
Nắm nhanh cách TLS xác thực server/client qua chuỗi chứng chỉ, trước khi xin cert, cấu hình mTLS hay debug lỗi handshake.
TLS mã hoá kết nối và xác thực danh tính bên kia bằng chứng chỉ X.509. Chứng chỉ chỉ có giá trị khi client dựng được một chain of trust từ cert của server lên tới một root CA nằm sẵn trong trust store của client (OS, trình duyệt, ca-certificates, JVM...).
flowchart TD
R["Root CA (self-signed, trong trust store)"] -->|ký| I["Intermediate CA"]
I -->|ký| L["Leaf cert: api.example.com"]
S[Server] -->|gửi leaf + intermediate| C[Client]
C -->|tra root trong trust store| R
Server phải gửi leaf + intermediate (file fullchain.pem). Root không cần gửi, client đã có sẵn.
Thiếu intermediate là lỗi kinh điển: trình duyệt đôi khi vẫn chạy (do cache/AIA fetching) nhưng curl, Python, Go, thiết bị mạng thì báo unable to get local issuer certificate.
Root CA nội bộ (lab, gNMI, Vault PKI) phải được thêm vào trust store của từng client.
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello (SNI, cipher suites, key share)
S->>C: ServerHello + Certificate + CertificateVerify + Finished
C->>C: Kiểm tra chain, thời hạn, SAN khớp SNI
C->>S: Finished
C->>S: Dữ liệu ứng dụng (mã hoá)
SNI cho phép một IP phục vụ nhiều domain: client gửi tên host trong ClientHello, server chọn cert tương ứng. Kết nối bằng IP trần hoặc client cũ không gửi SNI sẽ nhận cert mặc định, dẫn tới lỗi SAN không khớp.
-copy_extensions copy chép mọi extension trong CSR, kể cả CA:TRUE nếu người gửi cố ý thêm. Chỉ dùng với CSR bạn tự tạo; CA thật nên dùng Vault PKI hoặc step-ca.
Dùng khi cần cert TLS miễn phí cho Nginx, cert wildcard qua DNS, và đảm bảo cert tự gia hạn không ai phải nhớ.
Tóm tắt: Cài certbot (snap hoặc gói distro), chọn challenge: http-01 (cổng 80 mở ra internet, plugin --nginx hoặc --webroot) hoặc dns-01 (bắt buộc cho wildcard, dùng plugin DNS như dns-cloudflare). Certbot tạo timer systemd tự renew; thêm --deploy-hook để reload Nginx. Luôn thử với --dry-run / --test-cert trước.
flowchart LR
A[certbot] -->|"tạo order"| B[Let's Encrypt ACME]
B -->|"challenge token"| A
A -->|"http-01: ghi file"| C["/.well-known/acme-challenge/"]
A -->|"dns-01: tạo TXT"| D["_acme-challenge.example.com"]
B -->|"kiểm tra"| C
B -->|"kiểm tra"| D
B -->|"cấp cert"| E["/etc/letsencrypt/live/example.com/"]
E --> F["nginx reload (deploy-hook)"]
Challenge
Yêu cầu
Wildcard
Khi nào dùng
http-01
Cổng 80 của domain trỏ về server, truy cập được từ internet
Không
Web server public thông thường
dns-01
API token sửa DNS zone
Có
Wildcard, server nội bộ không mở cổng 80, nhiều server dùng chung cert
# Plugin nginx: tự sửa server block, thêm redirect HTTPSsudo certbot --nginx -d example.com -d www.example.com \ -m ops@example.com --agree-tos --non-interactive# Chỉ lấy cert, tự cấu hình Nginx (an toàn hơn khi config Nginx được quản lý bằng Ansible/Git)sudo certbot certonly --webroot -w /var/www/letsencrypt \ -d example.com -d www.example.com --deploy-hook "systemctl reload nginx"
Với --webroot, Nginx phải phục vụ thư mục challenge qua HTTP:
Luôn dùng fullchain.pem (leaf + intermediate), không dùng cert.pem. Dùng cert.pem là nguyên nhân phổ biến của lỗi thiếu chain, xem TLS troubleshooting.
http2 on; cần Nginx ≥ 1.25.1; bản cũ hơn viết listen 443 ssl http2;. Cấu hình reverse proxy đầy đủ xem Nginx reverse proxy + TLS.
DNS provider không có plugin: --manual --preferred-challenges dns cho phép tạo TXT bằng tay, nhưng không tự renew được (trừ khi viết --manual-auth-hook).
Khoanh vùng lỗi handshake TLS khi curl, trình duyệt, gNMI client hoặc thiết bị mạng không chịu kết nối HTTPS.
Triệu chứng chính:curl: (60) SSL certificate problem: unable to get local issuer certificate, certificate has expired, no alternative certificate subject name matches target host name, tlsv1 alert protocol version, handshake failure, Python CERTIFICATE_VERIFY_FAILED, Go x509: certificate signed by unknown authority.
# Debian/Ubuntu (file phải có đuôi .crt, định dạng PEM)sudo cp lab-root-ca.crt /usr/local/share/ca-certificates/sudo update-ca-certificates# RHEL/Rockysudo cp lab-root-ca.crt /etc/pki/ca-trust/source/anchors/sudo update-ca-trust
Đừng "sửa" bằng curl -k, verify=False hay insecure: true trong tool automation. Lỗi sẽ biến mất cùng với toàn bộ lợi ích của TLS. Trỏ đúng CA bundle (REQUESTS_CA_BUNDLE, SSL_CERT_FILE, --cacert).
Dùng khi dựng server Linux mới (jump host, collector, runner) và cần khoá SSH về key-only, giới hạn user, chặn brute-force mà không tự khoá mình ra ngoài.
Tóm tắt: Đưa key lên trước, giữ một phiên SSH đang mở, đặt cấu hình trong /etc/ssh/sshd_config.d/ (tắt password, tắt root, AllowGroups), kiểm tra bằng sshd -t / sshd -T, reload, mở phiên mới để xác nhận. Thêm fail2ban hoặc chỉ mở cổng 22 cho dải quản trị bằng nftables.
flowchart TD
A["ssh-copy-id: đưa public key lên"] --> B["Giữ 1 phiên SSH đang mở"]
B --> C["Ghi sshd_config.d/10-hardening.conf"]
C --> D{"sshd -t OK?"}
D -->|không| C
D -->|có| E["systemctl reload ssh"]
E --> F["Mở phiên mới để test"]
F -->|lỗi| G["Sửa từ phiên cũ"]
F -->|OK| H["fail2ban / firewall"]
Trên Ubuntu/Debian, sshd_config có Include /etc/ssh/sshd_config.d/*.conf ở đầu file, và sshd lấy giá trị xuất hiện đầu tiên của mỗi directive. Vì vậy file drop-in có tên sắp xếp trước sẽ thắng, kể cả file do cloud-init sinh ra (vd 50-cloud-init.conf bật PasswordAuthentication yes). Đặt tên 10-... để chắc chắn.
/etc/ssh/sshd_config.d/10-hardening.conf
# Xác thựcPermitRootLogin noPubkeyAuthentication yesPasswordAuthentication noKbdInteractiveAuthentication noPermitEmptyPasswords noAuthenticationMethods publickey# Giới hạn ai được vào (tạo group trước: groupadd ssh-users)AllowGroups ssh-users# Chống brute-force / phiên treoMaxAuthTries 3LoginGraceTime 30MaxStartups 10:30:60ClientAliveInterval 300ClientAliveCountMax 2# Tắt tính năng không dùng (jump host thì giữ AllowTcpForwarding)X11Forwarding noAllowAgentForwarding noAllowTcpForwarding noLogLevel VERBOSE
Jump host cần ProxyJump thì phải để AllowTcpForwarding yes (ít nhất cho group dùng jump). Có thể nới riêng bằng Match Group netops ở cuối file. Cấu hình phía client xem SSH config và jump host.
Chỉ cho phép thuật toán hiện đại (tuỳ chọn, kiểm tra client cũ và thiết bị mạng trước khi áp):
sudo groupadd -f ssh-users && sudo usermod -aG ssh-users opssudo sshd -t # kiểm tra cú pháp, không in gì = OKsudo sshd -T | grep -Ei 'passwordauth|permitroot|allowgroups|authenticationmethods'sudo systemctl reload ssh # RHEL: sshd# Mở terminal MỚI và ssh vào, chỉ đóng phiên cũ khi phiên mới vào được
Ubuntu 24.04 dùng socket activation (ssh.socket): đổi Port/ListenAddress trong sshd_config cần systemctl daemon-reload && systemctl restart ssh.socket, không chỉ reload service.
sudo systemctl enable --now fail2bansudo fail2ban-client status sshdsudo fail2ban-client set sshd unbanip 203.0.113.50
ignoreip là dải mạng quản trị, tránh tự ban chính mình. OpenSSH ≥ 9.8 có thêm PerSourcePenalties tích hợp sẵn, nhưng fail2ban vẫn hữu ích cho bản cũ và để chặn ở firewall.
Chọn cách lưu và phân phối mật khẩu thiết bị, API token, key cho pipeline automation, và hiểu đánh đổi giữa các công cụ.
Secret là mọi thứ cho phép truy cập: mật khẩu thiết bị, TACACS+ key, API token NetBox/Cloudflare, private key TLS/SSH, chuỗi kết nối DB. Mục tiêu không chỉ là "mã hoá" mà là trả lời được: ai đọc được, đọc ở đâu, xoay vòng thế nào, lộ thì biết không.
flowchart LR
K["age key / KMS / vault password"] --> A["File mã hoá trong Git (SOPS, ansible-vault)"]
A -->|"giải mã lúc chạy"| B["Env / file tmpfs"]
V["Vault / secret manager"] -->|"API + auth, lease"| B
B --> P["Playbook, app, CI job"]
Hai trường phái:
Encrypted-in-Git (SOPS, ansible-vault): secret mã hoá nằm cùng code, review qua PR, không cần server. Bài toán chuyển thành bảo vệ một key giải mã.
Secret store tập trung (HashiCorp Vault/OpenBao, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault): app lấy secret qua API lúc chạy, có audit log, có thể cấp dynamic secret (credential sinh ra theo yêu cầu, tự hết hạn).
Biến môi trường là kênh truyền, không phải nơi lưu: chúng lộ qua /proc/<pid>/environ, crash dump, log debug, docker inspect. Ưu tiên file mount 0600 hoặc tmpfs cho secret nhạy cảm.
Một secret, một mục đích: token riêng cho từng tool/môi trường, quyền tối thiểu, để khi lộ chỉ cần thu hồi đúng một cái.
Có quy trình xoay vòng trước khi cần đến nó. Secret không xoay được là rủi ro tồn đọng.
Key giải mã (age private key, vault password, KMS IAM) là secret quan trọng nhất: để trong password manager / CI secret, không nằm cạnh repo.
Log và output của automation phải che giá trị bí mật (Ansible no_log: true, CI masked variables).
Dùng khi cần commit mật khẩu thiết bị, token API vào repo automation ở dạng mã hoá, chia quyền giải mã theo môi trường và dùng trực tiếp trong playbook.
Tóm tắt:SOPS + age: mỗi người/CI có một cặp key age, .sops.yaml khai báo file nào mã hoá cho public key nào, SOPS chỉ mã hoá giá trị nên diff vẫn đọc được. Ansible đọc trực tiếp qua collection community.sops. ansible-vault: không cần tool ngoài, mã hoá cả file hoặc từng biến, phân môi trường bằng --vault-id. So sánh tổng quan ở Quản lý secret.
flowchart LR
A["age-keygen"] -->|"public key"| B[".sops.yaml creation_rules"]
B --> C["sops -e -i secrets.sops.yaml"]
C --> D["git commit (đã mã hoá)"]
D --> E["CI / máy chạy ansible"]
F["SOPS_AGE_KEY_FILE (private key)"] --> E
E -->|"community.sops giải mã"| G["Playbook"]
# Cài: tải binary sops và age từ GitHub releases, hoặc apt install age (Ubuntu 22.04+)mkdir -p ~/.config/sops/ageage-keygen -o ~/.config/sops/age/keys.txt# In ra: Public key: age1... (public key, được phép commit)chmod 600 ~/.config/sops/age/keys.txt
SOPS tự tìm private key ở ~/.config/sops/age/keys.txt trên Linux; nơi khác thì đặt SOPS_AGE_KEY_FILE. Trong CI, đưa nội dung key vào CI secret và expose qua biến SOPS_AGE_KEY.
File keys.txt là private key. Backup nó vào password manager; mất key = mất khả năng giải mã. Không commit, không để trong thư mục repo.
creation_rules: # prod: chỉ team lead + CI prod giải mã được - path_regex: inventories/prod/.*\.sops\.ya?ml$ encrypted_regex: ^(ansible_password|ansible_become_password|.*_token|.*_key|tacacs_.*)$ age: >- age1<PUBKEY_LEAD>, age1<PUBKEY_CI_PROD> # lab và mọi file .sops.* còn lại - path_regex: .*\.sops\.(ya?ml|json|env)$ age: >- age1<PUBKEY_LEAD>, age1<PUBKEY_DEV1>, age1<PUBKEY_CI_LAB>
SOPS dùng rule đầu tiên khớppath_regex, nên đặt rule cụ thể lên trước.
encrypted_regex chỉ mã hoá key khớp regex, các key khác để plaintext (dễ review). Bỏ đi thì mọi giá trị đều bị mã hoá.
Thay age1<...> bằng public key thật (chuỗi bắt đầu bằng age1).
Với vars plugin trên, file group_vars/<group>/*.sops.yaml và host_vars/<host>/*.sops.yaml được giải mã tự động như biến thường. Đọc file ngoài group_vars:
ANSIBLE_VAULT_PASSWORD_FILE=~/.ansible/vault-prod.pass hoặc vault_identity_list trong ansible.cfg
Quy ước dễ grep: biến bí mật đặt trong vault.yml với tiền tố vault_ (vd vault_tacacs_key), file vars.yml cạnh đó tham chiếu tacacs_key: "{{ vault_tacacs_key }}". Tìm biến không cần giải mã.
Dùng khi cần viết firewall host cho server Linux (jump host, collector, gateway lab) bằng nftables, kèm NAT và cách lưu cấu hình qua reboot.
Tóm tắt: Viết toàn bộ ruleset vào /etc/nftables.conf (bảng inet xử lý cả IPv4/IPv6), mặc định policy drop ở input, cho phép established,related, SSH chỉ từ dải quản trị qua set. Kiểm tra cú pháp bằng nft -c -f, áp bằng nft -f, bật nftables.service để tự nạp khi boot.
flowchart LR
P[Packet vào] --> PR["prerouting (dstnat)"]
PR --> R{"Đích là host?"}
R -->|có| IN["input (filter)"]
R -->|không| FW["forward (filter)"]
IN --> L["Process local"]
L --> OUT["output (filter)"]
FW --> PO["postrouting (srcnat)"]
OUT --> PO
PO --> X[Packet ra]
#!/usr/sbin/nft -fflush rulesetdefine MGMT_V4 = { 198.51.100.0/24 }define MGMT_V6 = { 2001:db8:100::/48 }table inet filter { set mgmt4 { type ipv4_addr flags interval elements = $MGMT_V4 } set blocklist4 { type ipv4_addr flags dynamic, timeout timeout 1h } chain input { type filter hook input priority filter; policy drop; ct state established,related accept ct state invalid drop iifname "lo" accept ip saddr @blocklist4 drop # ICMP/ICMPv6 cần cho PMTUD và IPv6 ND ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } accept meta l4proto ipv6-icmp accept # SSH chỉ từ dải quản trị, giới hạn tốc độ kết nối mới tcp dport 22 ip saddr @mgmt4 ct state new limit rate 10/minute accept tcp dport 22 ip6 saddr $MGMT_V6 accept # Web public tcp dport { 80, 443 } accept # gNMI dial-in từ router tcp dport 57400 ip saddr 203.0.113.0/24 accept limit rate 5/second log prefix "nft-drop: " level info } chain forward { type filter hook forward priority filter; policy drop; ct state established,related accept ct status dnat accept iifname "eth1" oifname "eth0" accept } chain output { type filter hook output priority filter; policy accept; }}table ip nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept; iifname "eth0" tcp dport 8443 dnat to 192.0.2.20:443 } chain postrouting { type nat hook postrouting priority srcnat; policy accept; oifname "eth0" ip saddr 192.0.2.0/24 masquerade }}
Ghi chú:
flush ruleset ở đầu làm file idempotent: chạy lại bao nhiêu lần cũng ra cùng kết quả. Lưu ý nó xoá cả rule do Docker/libvirt/fail2ban tạo; host chạy Docker thì thay flush ruleset bằng hai dòng table inet filter + delete table inet filter để chỉ làm mới bảng của mình (và bỏ bảng ip nat nếu không cần).
policy drop ở forward chỉ cần khi máy làm router/NAT. Muốn forward phải bật net.ipv4.ip_forward=1.
192.0.2.0/24 và eth0/eth1 là ví dụ, thay bằng mạng thật.
Làm qua SSH thì hẹn giờ tự gỡ trước khi áp: sudo systemd-run --on-active=5m nft flush ruleset, áp ruleset, mở phiên SSH mới thử, rồi huỷ timer bằng systemctl stop run-<id>.timer (tên in ra khi chạy systemd-run).
Công cụ cũ (backend iptables-nft trên distro mới đã dùng nftables bên dưới)
Framework hiện hành của kernel
Frontend, sinh rule nftables/iptables
IPv4 + IPv6
Hai bộ lệnh iptables/ip6tables
Một bảng inet
Có
Set / map
Cần ipset riêng
Có sẵn, cập nhật atomic
Hạn chế
Áp ruleset
Từng rule, không atomic
Cả file, atomic
Theo lệnh
Hợp với
Hệ thống cũ, script có sẵn
Server, gateway, cấu hình bằng Ansible
Desktop, server đơn giản
Chuyển rule cũ: iptables-save > old.txt && iptables-restore-translate -f old.txt in ra cú pháp nft tương đương. Không chạy song song ufw/firewalld với file nftables tự viết: chúng ghi đè lẫn nhau.
Dùng khi đưa router IOS XR hoặc Junos vào vận hành và cần chuẩn hoá đăng nhập qua TACACS+, chỉ cho SSH/NETCONF từ dải quản trị, và lọc lưu lượng tới routing engine.
Tóm tắt: (1) AAA qua TACACS+, luôn có fallback local và một user local khẩn cấp; (2) chỉ SSH v2 + NETCONF, tắt telnet, giới hạn phiên; (3) chỉ nhận management từ dải quản trị: control-plane management-plane trên IOS XR, filter trên lo0 trên Junos; (4) giới hạn tốc độ lưu lượng tới CPU (LPTS / policer). Mọi thay đổi trên thiết bị từ xa đều làm với commit confirmed.
Ví dụ dùng: dải quản trị 198.51.100.0/24, TACACS+ 198.51.100.10-11, BGP peer trong 203.0.113.0/24. Secret viết <...>, khi đẩy bằng Ansible thì lấy từ SOPS/ansible-vault.
tacacs-server host 198.51.100.10 port 49 key 7 <ENCRYPTED_KEY>!tacacs-server host 198.51.100.11 port 49 key 7 <ENCRYPTED_KEY>!aaa group server tacacs+ TACACS-GRP vrf MGMT server 198.51.100.10 server 198.51.100.11!aaa authentication login default group TACACS-GRP localaaa authorization exec default group TACACS-GRP localaaa authorization commands default group TACACS-GRP noneaaa accounting exec default start-stop group TACACS-GRPaaa accounting commands default stop-only group TACACS-GRP!username breakglass group root-lr secret 10 <SHA512_HASH>!
aaa authorization commands ... none nghĩa là khi TACACS+ mất hết thì lệnh được phép (không bị khoá ngoài). Ngược lại thiếu local ở authentication login thì mất TACACS+ = mất quyền vào thiết bị. Kiểm tra user breakglass trước khi đóng phiên.
ssh server v2ssh server vrf MGMTssh server netconf vrf MGMTssh server rate-limit 30ssh server session-limit 10ssh timeout 60netconf-yang agent ssh!line default exec-timeout 15 0!
Key host: chạy lệnh exec crypto key generate ed25519 (hoặc crypto key generate rsa, chọn 2048 trở lên). Telnet không bật mặc định trên XR; đảm bảo không có dòng telnet ... server trong running-config.
Có management-plane thì giao thức không được liệt kê trên interface đó sẽ bị chặn, nên khai báo đủ SSH/NETCONF/SNMP trước khi commit.
LPTS (Local Packet Transport Services) đã có policer mặc định cho từng loại flow tới CPU (BGP, OSPF, SSH, ICMP...). Chỉ chỉnh khi có lý do, cú pháp lpts pifib hardware police → flow <type> rate <pps> và giá trị mặc định khác nhau theo dòng máy (ASR 9000, NCS 5500, 8000): tra tài liệu platform trước.
set system authentication-order [ tacplus password ]set system tacplus-server 198.51.100.10 secret "<TACACS_KEY>"set system tacplus-server 198.51.100.10 source-address 192.0.2.1set system tacplus-server 198.51.100.11 secret "<TACACS_KEY>"set system tacplus-server 198.51.100.11 source-address 192.0.2.1set system accounting events [ login change-log interactive-commands ]set system accounting destination tacplusset system login user remote class read-onlyset system login user breakglass class super-userset system login user breakglass authentication encrypted-password "<SHA512_HASH>"
User remote là template: user TACACS+ không có tài khoản local sẽ được map vào class này (server TACACS+ có thể trả về local-user-name để map vào template khác, vd netops class super-user).
password trong authentication-order cho phép rơi về user local khi TACACS+ không phản hồi. Bị TACACS+ từ chối thì không thử local.
delete system services telnetdelete system services ftpset system services ssh root-login denyset system services ssh protocol-version v2set system services ssh connection-limit 10set system services ssh rate-limit 5set system services netconf sshset system login retry-options tries-before-disconnect 3set system login retry-options backoff-threshold 2
Filter đặt ở lo0 áp cho mọi lưu lượng tới RE qua các interface revenue (trên nhiều platform cả fxp0/em0). Thiếu term cho giao thức nào thì giao thức đó chết, nên đi theo thứ tự: liệt kê đủ, commit confirmed, xem counter.
Junos - lo0 filter
set policy-options prefix-list MGMT-NETS 198.51.100.0/24set policy-options prefix-list TACACS-SERVERS 198.51.100.10/32set policy-options prefix-list TACACS-SERVERS 198.51.100.11/32set policy-options prefix-list BGP-PEERS apply-path "protocols bgp group <*> neighbor <*>"set firewall policer ICMP-1M if-exceeding bandwidth-limit 1mset firewall policer ICMP-1M if-exceeding burst-size-limit 15kset firewall policer ICMP-1M then discardset firewall family inet filter PROTECT-RE term MGMT from source-prefix-list MGMT-NETSset firewall family inet filter PROTECT-RE term MGMT from protocol tcpset firewall family inet filter PROTECT-RE term MGMT from destination-port [ ssh 830 ]set firewall family inet filter PROTECT-RE term MGMT then acceptset firewall family inet filter PROTECT-RE term SNMP from source-prefix-list MGMT-NETSset firewall family inet filter PROTECT-RE term SNMP from protocol udpset firewall family inet filter PROTECT-RE term SNMP from destination-port snmpset firewall family inet filter PROTECT-RE term SNMP then acceptset firewall family inet filter PROTECT-RE term TACACS from source-prefix-list TACACS-SERVERSset firewall family inet filter PROTECT-RE term TACACS from protocol tcpset firewall family inet filter PROTECT-RE term TACACS from source-port 49set firewall family inet filter PROTECT-RE term TACACS then acceptset firewall family inet filter PROTECT-RE term BGP from source-prefix-list BGP-PEERSset firewall family inet filter PROTECT-RE term BGP from protocol tcpset firewall family inet filter PROTECT-RE term BGP from port bgpset firewall family inet filter PROTECT-RE term BGP then acceptset firewall family inet filter PROTECT-RE term OSPF from protocol ospfset firewall family inet filter PROTECT-RE term OSPF then acceptset firewall family inet filter PROTECT-RE term BFD from protocol udpset firewall family inet filter PROTECT-RE term BFD from destination-port [ 3784 4784 ]set firewall family inet filter PROTECT-RE term BFD then acceptset firewall family inet filter PROTECT-RE term NTP-DNS from protocol udpset firewall family inet filter PROTECT-RE term NTP-DNS from source-port [ ntp domain ]set firewall family inet filter PROTECT-RE term NTP-DNS then acceptset firewall family inet filter PROTECT-RE term ICMP from protocol icmpset firewall family inet filter PROTECT-RE term ICMP then policer ICMP-1Mset firewall family inet filter PROTECT-RE term ICMP then acceptset firewall family inet filter PROTECT-RE term TRACEROUTE from protocol udpset firewall family inet filter PROTECT-RE term TRACEROUTE from destination-port 33434-33534set firewall family inet filter PROTECT-RE term TRACEROUTE then acceptset firewall family inet filter PROTECT-RE term DENY then count RE-DENYset firewall family inet filter PROTECT-RE term DENY then syslogset firewall family inet filter PROTECT-RE term DENY then discardset interfaces lo0 unit 0 family inet filter input PROTECT-RE
IS-IS không phải IP nên không bị filter family inet chặn. IPv6 cần filter family inet6 riêng (cho phép ICMPv6 ND, BGP v6).
Term NTP-DNS theo source-port là cách đơn giản; muốn chặt hơn thì giới hạn thêm source-prefix-list server NTP/DNS.
Lưu lượng trả về từ kết nối do chính router mở (vd TACACS+, BGP chiều active) cũng đi qua filter, vì vậy mới có term TACACS theo source-port 49.
Làm gì ngay khi phát hiện token, mật khẩu hay private key đã bị commit (dù đã push hay chưa), và cách chặn để không lặp lại.
Triệu chứng chính: GitHub gửi alert secret scanning, push bị chặn bởi push protection, gitleaks báo trong CI, hoặc bạn tự thấy .env, keys.txt, id_ed25519, token API trong git log -p.
Rotate trước, dọn lịch sử sau. Secret đã lên remote thì coi như đã lộ: có thể đã bị clone, fork, cache, hoặc bot quét repo public bắt được trong vài phút. Viết lại lịch sử Git không thu hồi được secret; chỉ thu hồi/đổi secret mới có tác dụng.
Xác định secret là gì, cấp bởi ai: token GitHub, Cloudflare, API cloud, mật khẩu thiết bị, age key, TLS key.
Rotate / revoke tại nơi cấp. Với private key TLS: cấp lại cert với key mới và thu hồi cert cũ. Với age key SOPS: sinh key mới, sops updatekeys + sops -r mọi file, và đổi luôn các secret bên trong (người có key cũ đã đọc được chúng).
Kiểm tra đã bị dùng chưa: audit log của nhà cung cấp, log đăng nhập TACACS+/SSH, CloudTrail...
Tìm mọi chỗ secret xuất hiện: gitleaks trên toàn bộ lịch sử, mọi nhánh.
Rewrite lịch sử bằng git filter-repo (sau khi đã rotate).
Force push, báo team xoá clone cũ và clone lại; đóng/rebase các PR đang mở.
Chặn tái diễn: .gitignore, gitleaks pre-commit + CI, bật push protection, chuyển secret sang SOPS/ansible-vault.
# gitleaks v8.19+ (lệnh mới)gitleaks git -v --redact # quét toàn bộ lịch sử repo hiện tạigitleaks dir -v --redact . # quét file trong thư mục (không cần git)gitleaks git --pre-commit --staged -v --redact # chỉ phần đã stage# gitleaks bản cũ hơn: detect / protectgitleaks detect --source . -v --redactgitleaks protect --staged -v --redact# Xuất báo cáo cho CIgitleaks git --redact --report-format json --report-path gitleaks.json
False positive: thêm fingerprint (cột Fingerprint trong output) vào file .gitleaksignore ở gốc repo, hoặc tuỳ biến rule trong .gitleaks.toml. Tích hợp pre-commit xem pre-commit hooks.
pip install git-filter-repo # hoặc apt install git-filter-repo# Làm trên clone MỚI (filter-repo từ chối chạy trên clone đang dùng nếu không có --force)git clone --mirror git@github.com:example/netops.git netops-cleancd netops-clean
Cách 1: thay chuỗi secret trong mọi file, mọi commit:
Force push viết lại toàn bộ hash commit. Tạm tắt branch protection nếu cần, báo mọi người clone lại (đừng pull, bản cũ sẽ merge secret trở lại), và tag/release cũng đổi hash.
Dựng VPN WireGuard site-to-site hoặc cho laptop quản trị vào mạng lab, hiểu đúng AllowedIPs để định tuyến và lọc peer.
Tóm tắt: Mỗi máy sinh một cặp key (wg genkey | wg pubkey), chỉ trao đổi public key. Viết /etc/wireguard/wg0.conf gồm [Interface] (private key, địa chỉ tunnel, port) và một [Peer] cho mỗi bên kia (public key, AllowedIPs, Endpoint). Bật bằng wg-quick up wg0 hoặc systemctl enable --now wg-quick@wg0, kiểm tra bằng wg show. Mở UDP port trên firewall (nftables).
flowchart LR
A["Laptop (peer)"] -->|"UDP 51820, mã hoá"| B["Server wg0 203.0.113.10"]
B -->|"AllowedIPs khớp src?"| C{"Nhận gói"}
C -->|có| D["Chuyển vào LAN 192.0.2.0/24"]
C -->|không| E[Drop]
D -->|"Trả lời: dst khớp AllowedIPs của peer nào"| B
AllowedIPs có hai vai trò cùng lúc:
Chiều
Ý nghĩa
Gửi đi
Bảng định tuyến nội bộ của wg: gói có dst thuộc AllowedIPs của peer nào thì gửi cho peer đó (cryptokey routing)
Nhận về
Gói giải mã từ peer chỉ được nhận nếu src thuộc AllowedIPs của chính peer đó
wg-quick còn tự thêm route hệ thống cho mọi AllowedIPs (trừ khi đặt Table = off).
[Interface]Address = 198.51.100.2/32PrivateKey = <private-key-của-laptop># DNS = 192.0.2.53 # wg-quick đặt DNS qua resolvconf, cần gói resolvconf/systemd-resolved[Peer]PublicKey = <public-key-của-server>PresharedKey = <psk-của-cặp-này>Endpoint = 203.0.113.10:51820AllowedIPs = 198.51.100.0/24, 192.0.2.0/24 # split tunnel: chỉ mạng lab đi qua VPNPersistentKeepalive = 25 # giữ mapping NAT khi client sau NAT
AllowedIPs = 0.0.0.0/0, ::/0 = full tunnel (mọi traffic đi qua VPN); wg-quick tự xử lý policy routing để không mất route tới chính Endpoint.
sudo chmod 600 /etc/wireguard/wg0.confsudo wg-quick up wg0 # tắt: wg-quick down wg0sudo systemctl enable --now wg-quick@wg0 # chạy khi bootsudo wg show # peer, endpoint, latest handshake, transfersudo wg show wg0 latest-handshakesping -c3 198.51.100.1ip route get 192.0.2.20 # phải đi dev wg0
# Sửa wg0.conf rồi áp phần cấu hình wg (không đụng Address/route):sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'# Hoặc thêm nóng một peer (nhớ ghi lại vào wg0.conf):sudo wg set wg0 peer <public-key-của-peer-mới> allowed-ips 198.51.100.3/32
wg syncconf không thêm route mới cho AllowedIPs mới; với subnet mới phía sau peer, thêm route thủ công (ip route add ... dev wg0) hoặc systemctl restart wg-quick@wg0.
Dùng khi cần lưu credential thiết bị/DB trong Vault, cấp quyền đọc theo path cho người và cho automation (Ansible, CI) qua AppRole.
Tóm tắt: Bật secrets engine KV v2 (vault secrets enable -path=secret kv-v2), ghi/đọc bằng vault kv put/get -mount=secret. Viết policy HCL cấp quyền theo path (với KV v2 path API có data/ và metadata/). Automation đăng nhập bằng AppRole (role_id + secret_id) để nhận token ngắn hạn gắn policy. So sánh Vault với SOPS/ansible-vault xem Quản lý secret. Lệnh dưới cũng chạy với OpenBao (thay vault bằng bao).
flowchart LR
A["Ansible / CI"] -->|"role_id + secret_id"| B["auth/approle/login"]
B -->|"token (TTL 1h, policy netops-ro)"| A
A -->|"GET secret/data/netops/router"| C["KV v2"]
C --> D{"Policy cho phép?"}
D -->|có| E["Trả secret"]
D -->|không| F["403 permission denied"]
export VAULT_ADDR=https://vault.example.com:8200export VAULT_CACERT=/etc/ssl/certs/internal-ca.pem # CA nội bộ ký cert của Vaultvault status # Sealed: false là OKvault login -method=oidc # hoặc -method=userpass username=alicevault token lookup # policy, TTL của token hiện tại
Lab nhanh: vault server -dev (in-memory, tự unseal, root token in ra màn hình). Chỉ dùng để thử lệnh, không lưu dữ liệu thật.
vault policy write netops-ro netops-ro.hclvault policy read netops-rovault token capabilities secret/data/netops/router # kiểm tra token hiện tại có quyền gì
vault auth enable approlevault write auth/approle/role/ansible \ token_policies=netops-ro token_ttl=1h token_max_ttl=4h \ secret_id_ttl=24h secret_id_num_uses=0 token_bound_cidrs=198.51.100.0/24vault read -field=role_id auth/approle/role/ansible/role-id # ít nhạy cảm, có thể để trong configvault write -f -field=secret_id auth/approle/role/ansible/secret-id # nhạy cảm, giao qua CI secret# Phía automationexport VAULT_TOKEN=$(vault write -field=token auth/approle/login \ role_id="$VAULT_ROLE_ID" secret_id="$VAULT_SECRET_ID")vault kv get -mount=secret -field=password netops/router
Trong Ansible, collection community.hashi_vault đọc trực tiếp (lookup community.hashi_vault.vault_kv2_get, auth approle hoặc token), không cần ghi secret ra file.
Nắm các lớp bảo vệ container (image, runtime, secret) và cấu hình tương ứng trong Docker/Kubernetes trước khi đưa service lên production.
Container không phải máy ảo: mọi container chia sẻ kernel với host. Bảo mật container là thu nhỏ những gì một tiến trình bị chiếm quyền có thể làm: ít phần mềm trong image, không chạy root, không ghi được filesystem, bỏ capability thừa, không có secret nằm sẵn trong image. Cơ chế nền (namespace, cgroup) xem Linux namespaces và cgroups.
flowchart LR
A["Build: base image nhỏ, multi-stage"] --> B["Scan: trivy image"]
B --> C["Registry: tag bất biến / digest"]
C --> D["Runtime: non-root, read-only, cap_drop ALL"]
D --> E["Secret: mount lúc chạy"]
E --> F["Network: chỉ mở port cần thiết"]
Mọi service mới: non-root, cap_drop: ALL, no-new-privileges, scan trong CI. Chi phí gần như bằng 0.
Read-only rootfs: hầu hết app stateless; cần liệt kê thư mục app phải ghi (/tmp, cache) để mount tmpfs/emptyDir.
Ngoại lệ có kiểm soát: container mạng (VPN, routing daemon, collector cần raw socket) mới thêm NET_ADMIN/NET_RAW, ghi lý do. --privileged và mount docker.sock coi như trao quyền root host.
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image <image>
.github/workflows/scan.yml (trích)
- uses: aquasecurity/trivy-action@<version> # pin tag hoặc SHA cụ thể with: image-ref: registry.example.com/api:${{ github.sha }} severity: CRITICAL,HIGH ignore-unfixed: true exit-code: "1"
Hai lệnh hay dùng: chạy --exit-code 1 --severity CRITICAL để chặn merge, và một lệnh đầy đủ xuất SARIF/JSON để theo dõi. Chặn ở mức HIGH ngay từ đầu thường làm pipeline đỏ liên tục và bị tắt luôn.
Mount docker.sock vào container trivy là trao quyền root host cho container đó; chỉ làm trên runner tin cậy, hoặc quét file tar bằng --input.
Dùng khi thay community SNMPv2c bằng SNMPv3 có xác thực và mã hoá (authPriv) cho collector giám sát, và kiểm tra bằng snmpwalk.
Tóm tắt: Tạo view (OID được đọc), group ở mức priv gắn view, rồi user với auth SHA + priv AES thuộc group đó. Giới hạn nguồn bằng ACL (IOS XR) hoặc filter lo0 (Junos, xem Hardening thiết bị mạng). Kiểm tra từ collector bằng snmpwalk -v3 -l authPriv. Gỡ community v2c sau khi collector đã chạy ổn với v3. Phía Prometheus xem snmp_exporter.
flowchart LR
A["Collector 198.51.100.20"] -->|"GET, user nms01, SHA + AES"| B{"ACL / filter lo0"}
B -->|"không thuộc dải quản trị"| X[Drop]
B --> C{"User tồn tại, key đúng?"}
C -->|sai| Y["Report: authFailure / unknownUser"]
C --> D["Group NMS-RO (priv)"]
D --> E["View: OID được phép"]
E --> F[Response mã hoá]
set snmp view NMS-VIEW oid 1.3.6.1.2.1 includeset snmp view NMS-VIEW oid 1.3.6.1.4.1 includeset snmp v3 usm local-engine user nms01 authentication-sha authentication-password "<auth-pass>"set snmp v3 usm local-engine user nms01 privacy-aes128 privacy-password "<priv-pass>"set snmp v3 vacm security-to-group security-model usm security-name nms01 group NMS-ROset snmp v3 vacm access group NMS-RO default-context-prefix security-model usm security-level privacy read-view NMS-VIEWcommit confirmed 5
show snmp v3show snmp statisticsshow configuration snmp | display set
Junos lưu password dưới dạng key đã băm (authentication-key), không đọc lại được bản gốc. Để chặn theo nguồn, dùng term SNMP trong filter lo0 với source-prefix-list dải quản trị.
Nhiều bản phần mềm mới hỗ trợ SHA-2 (vd auth sha256 / authentication-sha256) và AES-256. Chỉ dùng khi cả thiết bị lẫn collector cùng hỗ trợ; dò bằng ? trên CLI thiết bị.
Key SNMPv3 được "localize" theo engine ID của thiết bị. Đổi engine ID (đổi cấu hình engine-id, đôi khi sau RMA/thay RE) làm user cũ không xác thực được nữa: phải tạo lại user. Ghi lại engine ID bằng show snmp engineid (XR) / show snmp v3 (Junos).
# Từ collector. Đặt password trong biến môi trường, không gõ thẳng trên dòng lệnh dùng chungexport SNMP_AUTH='***' SNMP_PRIV='***'snmpget -v3 -l authPriv -u nms01 -a SHA -A "$SNMP_AUTH" -x AES -X "$SNMP_PRIV" 192.0.2.1 1.3.6.1.2.1.1.5.0snmpwalk -v3 -l authPriv -u nms01 -a SHA -A "$SNMP_AUTH" -x AES -X "$SNMP_PRIV" 192.0.2.1 1.3.6.1.2.1.2.2.1.2snmpbulkwalk -v3 -l authPriv -u nms01 -a SHA -A "$SNMP_AUTH" -x AES -X "$SNMP_PRIV" -Cr25 192.0.2.1 IF-MIB::ifHCInOctets# Kiểm tra v2c đã bị gỡ (phải timeout)snmpget -v2c -c public -t 2 -r 0 192.0.2.1 1.3.6.1.2.1.1.5.0
-x AES trong Net-SNMP là AES-128, tương ứng priv aes 128 / privacy-aes128. Có thể lưu tham số vào ~/.snmp/snmp.conf (defSecurityName, defAuthType, defPrivType...) với quyền 0600.
Dùng khi số server/người dùng lớn và không muốn phát tán authorized_keys hay known_hosts nữa, chuyển sang SSH certificate ngắn hạn do CA nội bộ ký.
Tóm tắt: Tạo hai CA key (user CA và host CA). Ký public key của user bằng ssh-keygen -s user_ca -I <id> -n <principals> -V +8h; server tin user CA qua TrustedUserCAKeys và giới hạn principal bằng AuthorizedPrincipalsFile. Ký host key bằng -h; client tin host CA bằng dòng @cert-authority trong known_hosts. Thu hồi bằng KRL (RevokedKeys). Cấu hình sshd nền tảng xem SSH hardening.
flowchart LR
U["User: id_ed25519.pub"] -->|"gửi public key"| CA["User CA (ssh-keygen -s)"]
CA -->|"id_ed25519-cert.pub (8h, principals)"| U
U -->|"ssh: key + cert"| S["sshd"]
S --> T{"Ký bởi TrustedUserCAKeys?"}
T -->|"có, còn hạn, không bị revoke"| P{"Principal có trong file của user đích?"}
P -->|có| OK[Đăng nhập]
P -->|không| NO[Từ chối]
T -->|không| NO
So với authorized_keys:
authorized_keys
SSH certificate
Thêm người mới
Sửa file trên mọi server
Ký cert, server không đổi
Hết quyền
Xoá key khỏi mọi server
Cert tự hết hạn (TTL ngắn)
Host key
TOFU, cảnh báo khi đổi host key
Client tin host CA, không hỏi fingerprint
Ràng buộc
from=, command= trong từng dòng
Trong cert: principal, thời hạn, source-address, force-command
Ai có user_ca (private) thì đăng nhập được mọi server tin CA đó. Giữ offline/HSM hoặc dùng CA có quản lý (Vault SSH secrets engine, step-ca). Chỉ user_ca.pub và host_ca.pub được phát đi.
sudo install -d -m 755 /etc/ssh/auth_principalsecho netops | sudo tee /etc/ssh/auth_principals/deploy # ai có principal "netops" được vào user deployecho alice | sudo tee /etc/ssh/auth_principals/alicesudo ssh-keygen -k -f /etc/ssh/revoked.krl # KRL rỗng, tránh sshd từ chối mọi key vì thiếu filesudo sshd -t && sudo systemctl reload ssh
Không có AuthorizedPrincipalsFile thì sshd yêu cầu tên user đích nằm trong principals của cert. Có file thì chỉ principal liệt kê trong file mới hợp lệ, nhờ đó map vai trò → user.
# Ký host key của server (chạy trên máy CA, sau khi lấy ssh_host_ed25519_key.pub về)ssh-keygen -s host_ca -I bastion01 -h -n bastion01.example.com,203.0.113.30 -V +52w ssh_host_ed25519_key.pub
~/.ssh/known_hosts (client) hoặc /etc/ssh/ssh_known_hosts
Dùng khi muốn chỉ client có cert do CA nội bộ cấp (collector, agent, script automation) mới gọi được API sau nginx.
Tóm tắt: Tạo CA nội bộ dành riêng cho client, ký cert client có extendedKeyUsage=clientAuth. Trong nginx: ssl_client_certificate trỏ tới CA đó, ssl_verify_client on, chuyển danh tính client ($ssl_client_s_dn, $ssl_client_serial) xuống upstream. Test bằng curl --cert client.crt --key client.key. Khái niệm mTLS xem TLS và PKI; TLS phía server và reverse proxy xem Nginx reverse proxy + TLS.
sequenceDiagram
participant C as Client (collector01)
participant N as nginx
participant U as Upstream API
C->>N: ClientHello
N->>C: Certificate (server) + CertificateRequest
C->>N: Certificate (client.crt) + CertificateVerify
N->>N: Verify với client-ca.crt, CRL, thời hạn
alt hợp lệ
N->>U: Request + X-Client-DN, X-Client-Serial
U->>C: 200
else không có cert hoặc cert sai
N->>C: 400 Bad Request
end
Phân quyền theo client cụ thể: map $ssl_client_s_dn $mtls_allowed { ... } hoặc để app đọc header X-Client-DN. Upstream phải chỉ nghe localhost/mạng nội bộ và ghi đè các header này, nếu không client có thể tự giả header.
Khi bật ssl_crl, CRL hết hạn (nextUpdate qua) thì nginx từ chối mọi client: phải sinh lại CRL định kỳ rồi reload nginx. Cert client ngắn hạn (vài ngày tới 90 ngày) thường đơn giản hơn duy trì CRL.
Tra nhanh ý nghĩa và giá trị an toàn của các security header, cách thêm trong nginx và kiểm tra bằng curl.
Header bảo vệ người dùng trình duyệt (XSS, clickjacking, downgrade HTTPS). Với API chỉ phục vụ máy, HSTS và X-Content-Type-Options vẫn nên có; CSP chủ yếu cho trang HTML. Cấu hình reverse proxy đầy đủ xem Nginx reverse proxy + TLS.
HSTS với includeSubDomains (và nhất là preload) áp lên mọi subdomain: subdomain nào chưa có HTTPS sẽ không vào được cho tới khi hết max-age. Bắt đầu bằng max-age=300, tăng dần.
server { listen 443 ssl; server_name app.example.com; include snippets/security-headers.conf; location /static/ { add_header Cache-Control "public, max-age=86400"; include snippets/security-headers.conf; # bắt buộc include lại, xem lưu ý dưới }}
Lưu ý
Giải thích
Kế thừa add_header
Block con (location) có bất kỳadd_header nào thì không kế thừa add_header của block cha. Include lại snippet trong mọi block có add_header riêng
always
Không có always thì header chỉ gắn vào response 2xx/3xx, trang lỗi 4xx/5xx thiếu header
Header trùng
Upstream đã gửi header rồi thì thêm proxy_hide_header <Tên>; trước khi tự add_header
HSTS trên HTTP
Chỉ đặt trong server 443; trình duyệt bỏ qua HSTS gửi qua HTTP
Dùng trong giờ đầu khi nghi một server Linux bị xâm nhập, để cô lập, giữ bằng chứng và kiểm tra user, cron, process, network trước khi dọn dẹp.
Triệu chứng chính: CPU cao bất thường (thường là coin miner), process/user lạ, kết nối ra ngoài tới IP lạ, file trong /tmp hoặc /dev/shm có quyền chạy, authorized_keys có key không rõ nguồn gốc, cảnh báo từ IDS/cloud provider, log bị xoá ngắt quãng.
Thứ tự ưu tiên: cô lập → thu thập bằng chứng → phân tích → khôi phục. Không reboot, không xoá file độc, không kill ngay: mất bằng chứng trong RAM và attacker có thể có cơ chế phá huỷ. Báo cho người phụ trách bảo mật trước khi hành động lớn. Máy đã bị chiếm root thì cài lại từ image sạch, không "dọn dẹp" rồi dùng tiếp.
Dùng khi cần ghi lại ai sửa file cấu hình, ai chạy lệnh với quyền root, đăng nhập thành công/thất bại, và đẩy log đó về server tập trung.
Tóm tắt: Cài auditd, đặt rule trong /etc/audit/rules.d/*.rules, nạp bằng augenrules --load, kiểm tra auditctl -l. Mỗi rule gắn -k <key> để tra bằng ausearch -k <key> -i; báo cáo tổng hợp bằng aureport. Log đăng nhập/sudo ngoài auditd còn nằm trong /var/log/auth.log hoặc journal (journalctl cheatsheet). Đẩy về máy tập trung để attacker có root không xoá được dấu vết.
flowchart LR
K["Kernel audit"] -->|"event khớp rule"| A[auditd]
A --> F["/var/log/audit/audit.log"]
A -->|"audisp plugin"| R["audisp-remote / syslog"]
J["sshd, sudo, PAM"] --> JR["journald / auth.log"]
JR --> RS["rsyslog / Vector"]
R --> C["Log server tập trung"]
RS --> C
## Thay đổi user, group, sudo-w /etc/passwd -p wa -k identity-w /etc/group -p wa -k identity-w /etc/shadow -p wa -k identity-w /etc/sudoers -p wa -k sudoers-w /etc/sudoers.d/ -p wa -k sudoers## Cấu hình SSH và key-w /etc/ssh/sshd_config -p wa -k sshd-w /etc/ssh/sshd_config.d/ -p wa -k sshd-w /root/.ssh/ -p wa -k rootkey## Persistence-w /etc/cron.d/ -p wa -k cron-w /etc/crontab -p wa -k cron-w /etc/systemd/system/ -p wa -k systemd## Lệnh chạy với quyền root bởi người đã đăng nhập (auid là user gốc, kể cả sau sudo)-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k root-cmd-a always,exit -F arch=b32 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k root-cmd## Nạp/gỡ kernel module-a always,exit -F arch=b64 -S init_module,finit_module,delete_module -k modules
Thành phần
Ý nghĩa
-w <path> -p wa
Theo dõi file/thư mục: r đọc, w ghi, x chạy, a đổi thuộc tính
-a always,exit
Rule syscall, ghi khi syscall kết thúc
-F arch=b64
Bắt buộc với rule syscall; thêm bản b32 để không bị lách qua binary 32-bit
-F auid>=1000 -F auid!=4294967295
Chỉ user thật đã login (4294967295 = auid chưa đặt, tiến trình hệ thống)
-k <key>
Nhãn để tra cứu
-e 2
Khoá cấu hình, chỉ đổi được sau reboot (đặt ở file cuối cùng, vd 99-finalize.rules)
Rule execve cho mọi user sinh rất nhiều log trên máy bận (CI runner, container host). Bắt đầu với euid=0 như trên, theo dõi dung lượng và lost trong auditctl -s.
sudo augenrules --check # có thay đổi chưa nạp?sudo augenrules --load # gộp rules.d/*.rules -> /etc/audit/audit.rules và nạpsudo auditctl -l # rule đang hiệu lực
Dung lượng log trong /etc/audit/auditd.conf: max_log_file (MB), num_logs, max_log_file_action = ROTATE, space_left_action. Đổi xong: sudo service auditd restart (unit auditd thường từ chối systemctl restart).
# auth/authpriv (ssh, sudo, PAM) qua TCP, có hàng đợi khi server tạm mấtauthpriv.*;auth.* action(type="omfwd" target="logs.example.com" port="514" protocol="tcp" queue.type="LinkedList" queue.filename="fwd_auth" action.resumeRetryCount="-1")
auditd có hai cách: plugin audisp-remote (gói audispd-plugins, cấu hình /etc/audit/audisp-remote.conf với remote_server, bật active = yes trong /etc/audit/plugins.d/au-remote.conf), hoặc plugin syslog (/etc/audit/plugins.d/syslog.conf, active = yes) để rsyslog/Vector chuyển tiếp. Pipeline phía server xem syslog pipeline với Vector. Production nên dùng TLS cho đường truyền log.
Dùng khi cần tự động ban IP brute-force trên server Linux (sshd, nginx), viết filter riêng, và cân nhắc CrowdSec thay thế.
Tóm tắt: fail2ban đọc log, đếm lần thất bại theo IP bằng regex trong filter, vượt maxretry trong findtime thì chạy action (thường thêm rule firewall) trong bantime. Cấu hình đặt trong /etc/fail2ban/jail.d/*.local, không sửa jail.conf. CrowdSec cùng ý tưởng nhưng tách phần phát hiện (agent) khỏi phần chặn (bouncer) và chia sẻ danh sách IP xấu theo cộng đồng. Jail sshd cơ bản: xem SSH hardening.
nginx-http-auth và nginx-botsearch là filter có sẵn trong gói fail2ban; banaction phải khớp firewall đang dùng (nftables-multiport hay iptables-multiport, xem ls /etc/fail2ban/action.d/). Giới thiệu firewall: nftables.
sudo fail2ban-client -t # kiểm tra cú pháp cấu hìnhsudo systemctl restart fail2bansudo fail2ban-client status # danh sách jailsudo fail2ban-client status nginx-http-auth # IP đang bị bansudo fail2ban-client set nginx-http-auth banip 203.0.113.50sudo fail2ban-client set nginx-http-auth unbanip 203.0.113.50
CrowdSec cài theo hướng dẫn chính thức của dự án (repo + gói + bouncer); lệnh quản lý chính là cscli (vd cscli decisions list, cscli metrics). Chọn fail2ban cho 1-2 máy cần đơn giản; chọn CrowdSec khi có nhiều máy hoặc muốn dùng blocklist cộng đồng. Không chạy cả hai cùng ban một dịch vụ nếu chưa cần.