小馬智行基礎設施自動化(IaC)實踐
背景
小馬智行(pony.ai)成立于2016年,在硅谷、廣州、北京、上海、深圳設立研發中心,并獲得中美多地自動駕駛測試、運營資質與牌照。憑借人工智能技術領域的最新突破,已與豐田、現代、一汽、廣汽等車廠建立合作。目前估值85億美元。 小馬智行中國內地業務使用阿里云作為基礎架構的重要部分,承載了包括小馬智行自研的Data Labeling、Robotaxi、Robotruck等業務。這些業務使用了包括ECS、RDS、SLB、堡壘機、云安全中心在內的眾多產品。如何管理好這些基礎組件給DevOps團隊帶來了不小的挑戰。從業務需求出發,我們定義了三個目標:
「組件部署可評審」:保障小馬智行整體運維活動從需求收集、 架構設計、 代碼編寫最終到部署不會出現任何偏差。同時也能夠保證代碼編寫符合我們的要求。
「組件部署版本化」:保障小馬智行任何基礎設施生產的迭代都有跡可循。當出現極端情況的時候, 可以快速恢復到指定版本, 避免影響到業務。
「組件部署多環境一致」:不同環境的一致性則能夠保證小馬智行的基礎設施部署不會因為環境部署差異導致故障。
技術選型
從業內角度來看, 我們看到主要有三種主流公有云組件部署和管理方案:
云服務商的控制臺管理能力
使用管理系統(可能自研或者購買)調用公有云API做操作
基礎設施即代碼(IaC)框架
IaC方案,在國際的主流社區已經成為基礎設施自動化的既定標準,也是最廣泛使用的多云管理框架。 但是中國內地相對來說比較滯后,知道并使用的人還是比較少。 在這方面,擁有海外背景的小馬智行具備技術的先發優勢。我們發現,結合Git等代碼管理工具,可以很好的解決部署可追溯以及版本化的問題。 所有運維團隊部署的過程和變更細節都可以通過代碼很好的管理起來。 當需要回退的時候,可通過Git的分支對基礎設施進行回滾。
在社區生態方面,Terraform作為最優秀的、開源的IaC工具之一,已經被大量企業應用于生產環境。作為DevOps管理團隊,我們則把主要精力投入到對應的基礎架構邏輯代碼的編寫中去。
使用Terraform比調用各個云廠商API做二次開發不論是從開發量、復雜程度和運維難度都是具有優勢的;幫助我們做到更好、更敏捷的部署。
最后,考慮到多云戰略以及小馬智行混合云的現狀,結合Terraform的標準性、便捷性、易用性以及社區繁榮的特點,最終我們選擇使用Terraform作為企業IaC落地的工具。
架構設計
小馬智行團隊選用以Terraform為技術核心的IaC解決方案,并通過如下所示的架構圖最終落地到業務生產中:
在Terraform配置文件格式上,技術團隊綜合考慮小馬智行已經使用眾多的JSON應用的現狀,為了保持一致以及方便代碼Review,沒有選擇直接使用HCL格式, 而是選擇了JSON格式。
在代碼組織方面,小馬智行選擇了按業務維度來組織Terraform代碼。比如:業務1使用了SLB、證書、ECS三種資源,那么在代碼編寫的時候會把三種資源都統一定義在一個Terraform文件上,類似如下:
{
"output": {
"ecs_instance_1-private-ip": {
"value": "${alicloud_instance.ecs_instance_1.private_ip}"
},
"ecs_instance_2-private-ip": {
"value": "${alicloud_instance.ecs_instance_2.private_ip}"
},
"ponyai_business_1-slb-address": {
"value": "${alicloud_slb.ponyai_business_1-slb.address}"
}
},
"provider": {
"alicloud": {
"region": "alicloud_region"
}
},
"resource": {
"alicloud_instance": {
"ecs_instance_1": {
"availability_zone": "availability_zone_1",
"data_disks": [
{
"category": "cloud_essd",
"name": "data_volume",
"size": "xx"
}
],
"host_name": "ecs_instance_1",
"image_id": "image_id_1",
"instance_name": "ecs_instance_1",
"instance_type": "ecs_instance_type",
"internet_charge_type": "PayByTraffic",
"internet_max_bandwidth_out": 10,
"key_name": "key_name_1",
"security_groups": [
"security_groups_1"
],
"system_disk_category": "cloud_essd",
"system_disk_size": "xx",
"tags": {
"host_name": "ecs_instance_1"
},
"vswitch_id": "vswitch_id_1"
},
"ecs_instance_2": {
"availability_zone": "availability_zone_2",
"data_disks": [
{
"category": "cloud_essd",
"name": "data_volume",
"size": "xx"
}
],
"host_name": "availability_zone_2",
"image_id": "image_id_1",
"instance_name": "availability_zone_2",
"instance_type": "ecs_instance_type",
"internet_charge_type": "PayByTraffic",
"internet_max_bandwidth_out": 10,
"key_name": "key_name_1",
"security_groups": [
"security_groups_1"
],
"system_disk_category": "cloud_essd",
"system_disk_size": "xx",
"tags": {
"host_name": "availability_zone_2"
},
"vswitch_id": "vswitch_id_2"
}
},
"alicloud_slb": {
"slb-1": {
"address_type": "internet",
"internet_charge_type": "PayByTraffic",
"name": "slb_name",
"specification": "slb_specification"
}
},
"alicloud_slb_listener": {
"slb-listener-1": {
"backend_port": "xx",
"bandwidth": -1,
"frontend_port": "xx",
"health_check": "on",
"health_check_connect_port": "xx",
"health_check_domain": "domain_name",
"health_check_type": "check_type",
"health_check_uri": "uri_1",
"load_balancer_id": "${alicloud_slb.slb-1.id}",
"protocol": "protocol_1",
"scheduler": "scheduler_1",
"server_certificate_id":
"${alicloud_slb_server_certificate.slb-certificate-1.id}",
"server_group_id":
"${alicloud_slb_server_group.slb-server-group-1.id}"
}
},
"alicloud_slb_server_certificate": {
"slb-certificate-1": {
"alicloud_certificate_id": "xx",
"alicloud_certificate_name": "xx",
"name": "certificate_1"
}
},
"alicloud_slb_server_group": {
"slb-server-group-1": {
"load_balancer_id": "${alicloud_slb.slb-1.id}",
"name": "slb-server-group",
"servers": {
"port": "xx",
"server_ids": [
"${alicloud_instance.ecs_instance_1.id}",
"${alicloud_instance.ecs_instance_2.id}"
]
}
}
}
},
"terraform": {
"backend": {
"s3": {
"bucket": "bucket_name",
"dynamodb_table": "table",
"key": "key_1",
"profile": "profile_1",
"region": "region_1"
}
},
"required_providers": {
"alicloud": {
"source": "aliyun/alicloud",
"version": "xx"
}
}
}
}
業務挑戰
在Terraform代碼實際編寫中,我們逐步發現,對一些資源的需求,我們更關心其中的一些參數和特性。比如:
阿里云的ECS,我們更多的是關心ECS創建時的instance_type,instance_name, availability_zone等。
同一個業務在不同的部署環境僅僅只是一些資源的規格不太一樣, 比如業務1在正式的生產環境中的SLB使用的規格為slb.s2.medium,在測試環境中的規格為slb.s1.small,如果每次部署僅僅只是不同參數的相同組件時,代碼上需要重新寫一遍,無疑代碼的可讀性和重用性會非常差。
解決方案
考慮到Terraform使用的是JSON格式的文件, 為了解決其代碼重用和可讀性問題, 我們引入開源的JSONnet模板語言來生成Terraform使用的JSON文件,同時封裝了豐富的Utils。比如:我們對于創建ECS,封裝了如下的Function:
generateEcs(instance_name,
availability_zone,
vswitch_id,
security_groups,
instance_type,
host_name,
data_volume_size=null,
system_disk_size=null,
internet_charge_type="PayByTraffic",
image_id="ubuntu_18_04_x64_20G_alibase_20200914.vhd",
key_name="bootstrap-bot",
system_disk_category="cloud_essd",
internet_max_bandwidth_out=10,
data_disk_category="cloud_essd"): {
instance_name: instance_name,
availability_zone: availability_zone,
vswitch_id: vswitch_id,
security_groups: security_groups,
instance_type: instance_type,
internet_charge_type: internet_charge_type,
image_id: image_id,
system_disk_category: system_disk_category,
[if system_disk_size != null then "system_disk_size"]:
system_disk_size,
key_name: key_name,
internet_max_bandwidth_out: internet_max_bandwidth_out,
host_name: host_name,
data_disks: if data_volume_size != null then [
{
name: "data_volume",
size: data_volume_size,
category: data_disk_category,
},
] else [],
tags: {
host_name: host_name,
},
}
這樣上層在使用的時候直接調用該function就能生成對應的JSON部分。如下所示:
alicloud_instance: {
[host_config.host_name]:
ecsUtils.generateEcs(
instance_name=host_config.host_name,
availability_zone=host_config.az,
security_groups=$.ecs_security_groups,
host_name=host_config.host_name,
instance_type=$.ecs_instance_type,
vswitch_id=VPC_output["vswitch-public-" + host_config.az].value,
data_volume_size=$.ecs_data_volume_size,
system_disk_size=$.ecs_system_disk_size
)
for host_config in host_configs
},
同時如果要調整的話直接調整對應的Utils函數即可, 不需要每個基礎架構組件去調整。
對于不同的環境(正式, 預發布, 測試)只有少量組件參數不同的情況,我們也僅僅先定義好一個基礎的模板,然后不同的環境import之后對需要調整的參數賦予不同的值即可。
通過這樣的方式,可以做到對于小馬智行的某項業務在不同環境的部署,我們僅需要遵循如下的代碼路徑即可。
generated/main.tf.JSON
是文件main.tf.JSON.JSONnet
用JSONnet工具生成出來的JSON文件,是Terraform 最終執行的文件,main.tf.JSON.JSONnet.output
則是Terraform生效之后產生的一些字段和字段值輸出。
?
├── alicloud-region
│ ├── dev
│ │ ├── generated
│ │ │ └── main.tf.JSON
│ │ ├── main.tf.JSON.JSONnet
│ │ └── main.tf.JSON.JSONnet.output
│ ├── prod
│ │ ├── generated
│ │ │ └── main.tf.JSON
│ │ ├── main.tf.JSON.JSONnet
│ │ └── main.tf.JSON.JSONnet.output
│ └── staging
│ ├── generated
│ │ └── main.tf.JSON
│ ├── main.tf.JSON.JSONnet
│ └── main.tf.JSON.JSONnet.output
└── ponyai_business_1_base.libsonnet
以不同環境的SLB規格為例,在正式環境, 測試環境中, 我們僅僅需要把基礎模板引入之后, 調整不同的參數即可。比如正式環境我們用如下的代碼:
local base = import "../../ponyai_business_1_base.libsonnet";
base {
name: "ponyai_business_1_prod",
environment: "prod",
region: "alicloud_region",
slb_specification: "slb.s2.medium"
}
測試環境我們僅僅需要用如下的代碼:
local base = import "../../ponyai_business_1_base.libsonnet";
base {
name: "ponyai_business_1_dev",
environment: "dev",
region: "alicloud_region",
slb_specification: "slb.s1.small"
}
這樣可以實現較好的代碼可讀性和重用性。JSONnet也能夠很方便的解決基礎組件互相依賴導致在編寫Terraform代碼需要互相引用的問題。 比如阿里云上創建ECS, 需要提供VPC的ID,但是VPC一般是單獨寫在一個獨立的Terraform文件中并單獨生效,這個時候在代碼層面創建ECS的Terrform代碼則需要引用創建VPC生成出來的VPC id。以小馬智行為例子, 我們用“main.tf.JSON”文件創建了一個VPC, 其ID按照我們的要求輸出到了“main.tf.JSON.JSONnet.output”文件中:
├── ali-cloud-region
│ ├── dev
│ │ ├── generated
│ │ │ └── main.tf.JSON
│ │ ├── main.tf.JSON.JSONnet
│ │ └── main.tf.JSON.JSONnet.output
│ └── prod
│ ├── generated
│ │ └── main.tf.JSON
│ ├── main.tf.JSON.JSONnet
│ └── main.tf.JSON.JSONnet.output
此時其"main.tf.JSON.JSONnet.output"文件輸出如下:
{
"VPC_id": {
"sensitive": false,
"type": "string",
"value": "VPC_id_for_ponyai"
},
"vswitch-id": {
"sensitive": false,
"type": "string",
"value": "vswitch_public_id_for_ponyai"
}
}
我們在其他需要引用的地方用如下的語法就能很方便的進行引用,從而避免我們在代碼庫里面直接放入生成出來的值:
{
"ali-cloud-region": {
prod: import "./ali-cloud-region/prod/main.tf.JSON.JSONnet.output",
}
}
通過上述一層層的技術封裝,小馬智行DevOps團隊既使用了Terraform的技術及生態能力,也解決了業務調用的復雜性問題。讓運維效率整體得到很大提升。
業務收益
通過使用IaC的方式, 我們對各個業務使用的各個阿里云的組件的參數都定義得非常明確,比如某業務使用了兩臺ECS(每一臺的規格都是s6-c1m1.small且帶有一塊80G的系統盤和20G的數據盤)。結合Git管理,可以非常方便的進行Code Review;從而對整個部署過程進行把控。
如下圖所示,我們團隊在每次實際部署之前,都會通過Git進行代碼Review整個PR,并在PR內進行討論,最終確認好部署各個細節。并且,在未來的某一天,我們都有能力回溯到今天來看云基礎設施發生了什么變更。
正是由于能夠做到參數級別的Review, 我們可以保證最終的部署和最初的設計不會出現偏差。如果在最終部署的時候發現和原先設計的時候會有偏差, 我們也能夠在Terraform代碼編寫的時候發現并及時調整原先設計。 同時我們在提交PR進行Review前, 都要求進行自測, 盡量避免出現Review多次, 結果發現連運行都運行不了的情況出現。
回顧整個使用阿里云Terraform落地IaC的過程,我們總結了四個我們認為的主要業務收益:
「更快」:同樣的基礎設施生產不需要再去控制臺重復低效操作,生產周期大大縮短。為企業的業務決策和市場機遇響應提供了強有力的基礎支撐。
「更可控」:基礎設施代碼化,歷史任何時間點都可以回溯。將組織從控制臺操作升級至更優雅、可控、可信、可回溯的體系化管理階段。
「更高效」:多團隊、跨團隊同尤其是跨國協同效率提高。解決了時差、工作模式帶來的效率降低的問題。
「更安全」:大大減少了人為誤操作導致的生產事故。針對不同的環境采取不同的審批鏈路,做到技術+人的雙重結合。為小馬智行的業務保駕護航。
總結
管理模式升級
小馬智行整體建設都是基于IaC的整體理念之下。目前企業內部已經將眾多的阿里云組件抽象成內部自己的函數, 包括ECS,VPC, OSS, PUBLIC DNS, RAM, SLS, USERS等。
業務模式升級
DevOps團隊或者業務團隊需要使用阿里云組件的時候都會使用這些封裝好的函數去拉取對應的阿里云組件,DevOps團隊則專心維護并迭代好這些函數即可。運維效率大大提升。
運維模式升級
小馬智行目前內部20+業務都是按照IaC思路通過Terraform100%落地,我們能夠非常清晰的看到各個業務使用的阿里云組件的迭代史;也能夠很好的進行Review,及時拒絕不合理的部署,保證線上環境的干凈、整潔、高可靠的同時兼顧優秀的可擴展性。
作者介紹
小馬智行DevOps團隊