本文是對(duì)倉庫 https://github.com/patrickchugh/terravision 的源碼分析筆記,重點(diǎn)關(guān)注它是 如何從 Terraform 資源構(gòu)建出“適合畫架構(gòu)圖的節(jié)點(diǎn)與連線” 的??梢宰鳛閷?shí)現(xiàn)類似能力的設(shè)計(jì)參考。
1. 總體流水線概覽
TerraVision 從 Terraform 到最終圖形,大致分成四層:
-
Terraform 執(zhí)行與原始依賴圖(
modules/tfwrapper.py) -
變量 / locals / 模塊輸出解釋 + 元數(shù)據(jù)合并(
modules/interpreter.py+fileparser) -
資源圖結(jié)構(gòu)構(gòu)建與精修(
modules/graphmaker.py+helpers+resource_handlers) -
布局與渲染(Graphviz)(
modules/drawing.py+resource_classes)
頂層入口在 terravision/terravision.py 的 draw():
tfdata = compile_tfdata(source, varfile, workspace, debug, annotate, planfile, graphfile)
if simplified:
graphmaker.simplify_graphdict(tfdata)
drawing.render_diagram(tfdata, show, final_outfile, format, source)
整個(gè)過程就是:先把 Terraform 變成一個(gè)富信息的 graphdict,再交給 Graphviz 做布局渲染。
2. Terraform 執(zhí)行與原始圖構(gòu)建(tfwrapper)
2.1 運(yùn)行 terraform,生成 plan 和 graph
核心函數(shù):tf_initplan() 和 tf_makegraph()。
def tf_initplan(source, varfile, workspace, debug=True) -> Dict[str, Any]:
# 1) 寫 override.tf,強(qiáng)制 local backend,避免 remote state
override_dest = _write_override(codepath)
# 2) terraform init + workspace
subprocess.run(["terraform", "init", "--upgrade", "-reconfigure"], ...)
subprocess.run(["terraform", "workspace", "select", "-or-create=True", workspace], ...)
# 3) terraform plan -out=tfplan.bin
subprocess.run(["terraform", "plan", "-refresh=false", "-out", tfplan_path, ...])
# 4) terraform show -json tfplan.bin > tfplan.json
subprocess.run(["terraform", "show", "-json", tfplan_path], stdout=tfplan_json_path, ...)
# 5) terraform graph > tfgraph.dot
subprocess.run(["terraform", "graph"], stdout=tfgraph_path, ...)
# 6) 用 Graphviz 把 DOT 轉(zhuǎn)成 JSON(xdot_json)
graphdata = convert_dot_to_json(tfgraph_path)
tfdata["plandata"] = plandata
tfdata["tfgraph"] = graphdata
convert_dot_to_json() 通過:
dot -Txdot_json -o graph.json tfgraph.dot
得到包含 _gvid、objects、edges 的 JSON。
2.2 初始化節(jié)點(diǎn)和 meta_data(setup_tfdata)
def setup_tfdata(tfdata: Dict[str, Any]) -> Dict[str, Any]:
cloud_config = load_config(detected_provider)
HIDDEN_NODES = cloud_config.AWS_HIDE_NODES / AZURE_HIDE_NODES / ...
tfdata["graphdict"] = {}
tfdata["meta_data"] = {}
tfdata["node_list"] = []
tfdata["hidden"] = HIDDEN_NODES
# 每一條 resource_changes 都變成一個(gè) node
for obj in tfdata["tf_resources_created"]:
if obj["mode"] == "managed":
node = obj["address"] # e.g. aws_instance.web
if "index" in obj:
# 處理 count/for_each
suffix = f"~{int(obj['index']) + 1}" 或 "[index]"
node = node + suffix
tfdata["graphdict"][node] = []
tfdata["node_list"].append(node)
details = obj["change"]["after"]
# 把 after_unknown 里的字段標(biāo)記為“部署后才知道”
...
tfdata["meta_data"][node] = details
這一步得到的結(jié)果:
-
graphdict: 節(jié)點(diǎn)都有了,但連接還幾乎為空; -
meta_data: 來自 plan 的屬性(不含 HCL 原始變量表達(dá)式); -
node_list: 所有資源節(jié)點(diǎn)。
2.3 從 Terraform graph 填充初始連接(tf_makegraph)
def tf_makegraph(tfdata, debug):
tfdata = setup_tfdata(tfdata)
# 1) 建立 gvid -> name 映射表
gvid_table = ["" for _ in tfdata["tfgraph"]["objects"]]
for item in tfdata["tfgraph"]["objects"]:
gvid = item["_gvid"]
if item.get("name").startswith("module."):
gvid_table[gvid] = item["name"]
else:
gvid_table[gvid] = item["label"]
# 2) 遍歷所有 node,根據(jù) tfgraph.edges 填充 graphdict
for node in dict(tfdata["graphdict"]):
node_id = find_node_in_gvid_table(node, gvid_table)
for connection in tfdata["tfgraph"]["edges"]:
head = connection["head"]
tail = connection["tail"]
if node_id == head and ...:
conn = gvid_table[tail] # 依賴資源名
# 針對(duì) count/for_each 做一次「best match」
...
if conn_type in REVERSE_ARROW_LIST:
# 特定類型:反向箭頭,conn -> node
tfdata["graphdict"][conn].append(node)
else:
tfdata["graphdict"][node].append(conn)
# 3) 特殊補(bǔ)充:VPC -> Subnet(CIDR overlap)
tfdata = add_vpc_implied_relations(tfdata)
tfdata["original_graphdict"] = deepcopy(tfdata["graphdict"])
tfdata["original_metadata"] = deepcopy(tfdata["meta_data"])
這里使用的是 Terraform 自身的 graph 結(jié)果 + provider 配置中的 REVERSE_ARROW_LIST,構(gòu)建出基本的依賴有向圖。
3. 變量、locals、模塊輸出與 HCL 元數(shù)據(jù)(interpreter)
這一層的目標(biāo)是:讓 meta_data 里的字段盡量是“真值”,而不是 var.xxx、local.xxx 之類的表達(dá)式,以便后續(xù)關(guān)系掃描和布局策略使用。
3.1 resolve_all_variables 流程
入口在 terravision.py::_enrich_graph_data:
tfdata = interpreter.prefix_module_names(tfdata)
tfdata = interpreter.resolve_all_variables(tfdata, debug, already_processed)
resolve_all_variables 主要做四件事:
-
get_variable_values:從-
.tf里variable塊的 default, -
--varfile傳入的 tfvars, -
module參數(shù)
里,構(gòu)造:
tfdata["variable_list"] # 所有變量 -> 值 tfdata["variable_map"] # 按 module 組織的變量表 -
-
extract_locals:把all_locals按 module 聚合為:tfdata["all_locals"] = { module_name: { local_name: value, ... } } -
merge_metadata:掃描all_resource里的 HCL 源碼,把原始屬性合并回meta_data,保留了還未展開的 Terraform 表達(dá)式,例如:subnet_id = aws_subnet.private[0].id -
handle_metadata_vars:把meta_data[resource][key]里的值進(jìn)行多輪替換:-
var.xxx→replace_var_values,從variable_map里取; -
local.xxx→replace_local_values,從all_locals; -
data.xxx→replace_data_values,部分有內(nèi)置替換表,其他標(biāo)記為"UNKNOWN"; -
module.xxx→replace_module_vars/handle_module_vars,解析模塊輸出,并支持遞歸替換。
-
最終 meta_data 變成更“接近真實(shí)部署后狀態(tài)”的屬性視圖,同時(shí)仍然保留了一些必要的信息(如 count)。
4. 圖結(jié)構(gòu)的精修(graphmaker)
graphmaker 是 TerraVision 的核心,它在 Terraform 的原始依賴上實(shí)現(xiàn)了一整套“架構(gòu)圖友好”的處理步驟:
4.1 注入 data source 節(jié)點(diǎn)
inject_data_source_nodes(tfdata):
從 plan 的
prior_state中找出mode == "data"的資源;過濾掉只做 lookup 的類型(
EXCLUDED_DATA_SOURCE_TYPES);在
all_resource的字符串里搜"data.<type>.<name>",只有真的被引用的 data source 才變成圖中的節(jié)點(diǎn);-
把它們加入到:
tfdata["graphdict"][node_name] = [] tfdata["node_list"].append(node_name) tfdata["meta_data"][node_name] = { ..., "_data_source": True } 從 Terraform graph 中重建 data source 節(jié)點(diǎn)與其他資源之間的邊;
如果某些 data source 表示子網(wǎng)/ALB,并包含
vpc_id,但圖中沒有 VPC,則通過_synthesize_vpc_from_data_sources()合成一個(gè)aws_vpc.<vpc_id>節(jié)點(diǎn),把它作為容器。
這是把 plan 中“只作為數(shù)據(jù)引用”的資源,提升為圖里的顯式節(jié)點(diǎn)。
4.2 掃描并補(bǔ)充關(guān)系(add_relations)
add_relations(tfdata) 主要調(diào)用 _scan_node_relationships:
for node in tfdata["node_list"]:
nodename = _get_base_node_name(node, tfdata)
if _should_skip_node(node, nodename):
continue
dg = _get_metadata_generator(node, nodename, tfdata) # 遍歷 meta_data 的葉子
for param_item_list in dg:
matching_result = check_relationship(node, param_item_list, tfdata)
if matching_result:
tfdata = _process_connection_pairs(matching_result, tfdata)
check_relationship 的關(guān)鍵邏輯:
- 用
_find_matching_resources解析字符串里的:aws_xxx.yyy${aws_xxx.yyy.id}-
resource[0]/resource[count.index]等;
- 用
_find_implied_connections按關(guān)鍵詞匹配“隱式連接”(例如日志、監(jiān)控等服務(wù)); - 配合
REVERSE_ARROW_LIST決定邊的方向,是resource -> matched還是反過來。
scan_module_relationships 會(huì):
- 分析 module 輸出和 module 參數(shù)中
module.other_module.resource_type.name這樣的引用; - 在 module 粒度上補(bǔ)充 module→module 的關(guān)系。
4.3 節(jié)點(diǎn)合并與多實(shí)例展開
4.3.1 合并節(jié)點(diǎn)(consolidate_nodes)
根據(jù) provider config 中的 CONSOLIDATED_NODES,把一些本質(zhì)上屬于一個(gè)“邏輯服務(wù)”的節(jié)點(diǎn)合并,例如:
-
aws_lb/aws_alb/aws_nlb合成一個(gè) ALB 節(jié)點(diǎn); - EC2 Auto Scaling Group / Launch Template 等合成一個(gè)更高層抽象。
處理方式:
- 如果某個(gè)資源的
consolidated_node_check有結(jié)果,就把它的 metadata 和連接合并到目標(biāo)節(jié)點(diǎn); - 替換原連接中的指向,避免內(nèi)部成環(huán);
- 刪除被合并的原節(jié)點(diǎn)。
4.3.2 count / for_each 的多實(shí)例(create_multiple_resources)
多實(shí)例展開的大致流程:
-
detect_and_set_counts:利用配置*_MULTI_INSTANCE_PATTERNS掃描一些有“多引用”的資源(例如一個(gè) ALB 監(jiān)聽多個(gè) target group),推斷 count,并對(duì)關(guān)聯(lián)資源設(shè)置相同 count。 -
create_multiple_resources:遍歷所有有
count / desired_count / max_capacity / target_size的資源;-
通過
handle_count_resources:for i in range(max_i): new_name = f"{resource}~{i+1}" tfdata["graphdict"][new_name] = ... # 復(fù)制并重寫連接 tfdata["meta_data"][new_name] = deepcopy(meta_data[resource]) add_multiples_to_parents(i, resource, multi_resources, tfdata) 對(duì)連接中的節(jié)點(diǎn),如果也應(yīng)該有多實(shí)例(但原來是單節(jié)點(diǎn)),也生成對(duì)應(yīng)的
~i副本;特殊處理安全組與子網(wǎng),根據(jù)資源在
node_list中的位置,只連接到對(duì)應(yīng) subnet 的那一個(gè)實(shí)例;最后刪除原始未帶
~的資源節(jié)點(diǎn)。
4.3.3 簡化網(wǎng)絡(luò) / 清理多余連線
-
cleanup_cross_subnet_connections:如果多個(gè) numbered 節(jié)點(diǎn)在不同 subnet 中實(shí)例化,且某 un-numbered 資源只存在于一部分 subnet,中間的跨 subnet 連線會(huì)刪掉,減少圖的混亂。 -
extend_sg_groups:安全組跟著多實(shí)例一起擴(kuò)展,保持“安全組 N 對(duì)應(yīng)實(shí)例 N”的結(jié)構(gòu)。
4.4 方向調(diào)整與雙向邊
4.4.1 reverse_relations
根據(jù) provider config 中的:
-
FORCED_DEST: 某些資源總是認(rèn)為是“目的地”(箭頭朝它); -
FORCED_ORIGIN: 某些資源總是“起點(diǎn)”;
再遍歷 graphdict 調(diào)整邊的方向,確保整個(gè)圖符合“從入口 → 業(yè)務(wù) → 數(shù)據(jù)”的流向。
4.4.2 雙向邊
helpers.find_bidirectional_links(tfdata) 識(shí)別 (A -> B) 與 (B -> A) 同時(shí)存在的情況,用一個(gè)集合 bidirectional_edges 記錄,后續(xù)在畫圖時(shí)改用雙向箭頭而不是刪掉其中一條。
5. 布局與渲染(drawing + Graphviz)
TerraVision 自己并不計(jì)算 x/y 坐標(biāo),而是用 Graphviz 的布局引擎(neato)+ cluster + 隱形邊等機(jī)制,讓 Graphviz 做布局,TerraVision 負(fù)責(zé)“給出合適的結(jié)構(gòu)和約束”。
5.1 加載 provider-specific 繪圖配置
render_diagram(tfdata, ...) 中:
constants = _load_provider_constants(tfdata)
CONSOLIDATED_NODES = constants["CONSOLIDATED_NODES"]
GROUP_NODES = constants["GROUP_NODES"]
DRAW_ORDER = constants["DRAW_ORDER"]
OUTER_NODES = constants["OUTER_NODES"]
EDGE_NODES = constants["EDGE_NODES"]
SHARED_SERVICES = constants["SHARED_SERVICES"]
ALWAYS_DRAW_LINE = constants["ALWAYS_DRAW_LINE"]
NEVER_DRAW_LINE = constants["NEVER_DRAW_LINE"]
這些配置定義了:
- 哪些資源是容器/分組(VPC、子網(wǎng)、SG);
- 各種資源的繪制順序;
- 哪些資源在云邊界之外(Region/AZ);
- 哪些資源是“共享服務(wù)”節(jié)點(diǎn),畫線需要謹(jǐn)慎;
- 哪些邊永遠(yuǎn)畫 / 永遠(yuǎn)不畫(用
invisedge 只作為布局約束)。
5.2 構(gòu)建 Graphviz DOT 圖
-
Canvas(engine='neato', direction='TB')作為整體畫布。 - 為 provider 創(chuàng)建大 cluster(
AWSGroup/AzureGroup/GCPGroup)。 - 按
DRAW_ORDER遍歷資源類型:-
draw_objects(node_type_list, ...)→ 對(duì)每種類型調(diào)用:-
handle_group:如果是 GROUP_NODES(VPC、Subnet、SG 等),創(chuàng)建 cluster,遞歸處理子 group 和子節(jié)點(diǎn); -
handle_nodes:畫普通資源節(jié)點(diǎn),使用resource_classes里定義的類(帶 icon / label / 顏色)。
-
- 連接時(shí)通過
ok_to_connect / always_draw_edge決定是否畫邊、邊是否可見(solidorinvis),進(jìn)一步影響 Graphviz 的布局。
-
5.3 Graphviz 布局與后處理
繪制工作只定義了“節(jié)點(diǎn)、cluster 和邊”的結(jié)構(gòu)與樣式,具體坐標(biāo)由 Graphviz 決定:
-
Canvas.pre_render()生成初始 DOT; - 調(diào)用
gvpr -f shiftLabel.gvpr調(diào)整 cluster label、標(biāo)題等的位置; - 使用 Graphviz 引擎(neato)輸出 PNG/SVG 等格式;
- 如果格式是
drawio,則用graphviz2drawio把 dot 轉(zhuǎn)成.drawio。
這一策略的優(yōu)點(diǎn)是:
- TerraVision 專注于“圖結(jié)構(gòu)”和“分組 / 圖例 / 邊可見性”等高層邏輯;
- 布局細(xì)節(jié)(線走向、節(jié)點(diǎn)碰撞等)全部交給 Graphviz。