依赖性注入
你必须表达依赖关系,以减少低级模块的变化对高级模块的影响。例如,网络变化不应破坏任何高层资源,如队列、应用程序或数据库。
依赖性注入涉及两个原则:控制反转和依赖反转。
控制的反转
单向依赖意味着一个高层资源依赖于一个低层资源;高层资源调用低层资源的属性。当你在基础设施的依赖关系中执行单向关系时,你自然会应用控制反转。高层资源调用低层资源以获得其需要的属性(图1)。当你打电话预约医生,而不是医生办公室打电话提醒你时,你就使用了控制倒置。

图1.通过控制倒置,高层资源或模块调用低层模块获取信息,并解析其元数据以获取任何依赖性。
我将应用控制的倒置来展示一个服务器是如何依赖网络的。服务器没有硬编码或参数化子网名称,而是调用网络模块的输出来获取子网名称。网络模块在一个名为 "terraform.tfstate "的JSON文件中返回子网名称,供任何高层资源使用。
清单1 网络模块用子网名称创建一个JSON文件:
{
"version": 4,
"terraform_version": "0.14.8",
"serial": 8,
"lineage": "7feea4f3-631e-24f9-c5e2-0b3aa95d9517",
"outputs": { #A
"name": { #B
"value": "hello-world-subnet", #B
"type": "string"
}
} #C
}
#A 网络模块创建一个带有输出列表的JSON文件。
#B 网络模块包括一个子网名称的输出。
#C 为了清楚起见,省略了JSON文件的其余部分。
服务器使用反转控制,从网络的 "terraform.tfstate "文件中获得输出。然后,服务器模块对其进行解析,找出子网名称。
清单2 应用反转控制在网络上创建一个服务器:
import json
class NetworkModuleOutput: #A
def __init__(self):
with open('network/terraform.tfstate', 'r') as network_state:
network_attributes = json.load(network_state)
self.name = network_attributes['outputs']['name']['value']
class ServerFactoryModule:
def __init__(self, name, zone='us-central1-a'):
self._name = name
self._network = NetworkModuleOutput() #B
self._zone = zone
self.resources = self._build()
def _build(self):
return {
'resource': [{
'google_compute_instance': [{
self._name: [{
'allow_stopping_for_update': True,
'boot_disk': [{
'initialize_params': [{
'image': 'ubuntu-1804-lts'
}]
}],
'machine_type': 'f1-micro',
'name': self._name,
'zone': self._zone,
'network_interface': [{
'subnetwork': self._network.name #C
}]
}]
}]
}]
}
if __name__ == "__main__":
server = ServerFactoryModule(name='hello-world')
with open('main.tf.json', 'w') as outfile:
json.dump(server.resources, outfile, sort_keys=True, indent=4)
#A 网络模块输出创建一个具有子网名称的对象。
#B 服务器调用网络输出并检索该模块的所有属性。
#C 检索子网的名称来创建服务器。
服务器只需要从网络模块中获取子网的名称。它消除了我的服务器模块中的直接引用。我还控制和限制了网络为高层资源使用而返回的信息。当我有新的高层资源依赖于网络时,我可以添加更多他们需要的网络属性。一个使用反转控制的实现消除了明确的依赖关系,并授权高层模块或资源来维持这种依赖关系。反转控制提高了可进化性和可组合性。
如果我需要改变网络IP地址范围会怎样?由于控制的反转,服务器模块会识别新的范围并重新分配一个新的IP地址,但低级模块的改变仍然会扰乱高级模块。
依赖性反转
尽管控制权的反转使高层模块得以进化,但依赖性反转隔离了低层模块的变化,并减轻了对其依赖性的破坏。依赖反转决定了高层和低层资源的依赖关系应该通过抽象来表达。你可以把抽象看成是传达所需属性的翻译器。抽象允许你改变低级模块而不影响高级模块。当你使用翻译程序时,你会使用依赖性反转。翻译器成为你检索文本信息的接口。以我的服务器和网络为例,我需要访问网络的属性,这些属性在我改变网络的时候会更新(图2)。

图2.依赖关系反转将低层资源元数据的抽象返回给依赖它的资源。
你可以从三种类型的抽象中进行选择:
- 资源属性的内插(在模块内)
- 模块输出(模块之间)
- 基础设施状态(模块之间)
插值处理模块或配置内资源或任务之间的属性传递。该工具为你从基础设施的API中检索信息。例如,我可以使用Terraform的本地插值来获取网络名称,以便创建子网。
清单3 使用内插法将网络传给子网
{
"resource": [
{
"google_compute_network": [
{
"hello-world-network": [
{
"auto_create_subnetworks": false,
"name": "hello-world-network" #B
}
]
}
]
},
{
"google_compute_subnetwork": [
{
"hello-world-network": [
{
"ip_cidr_range": "10.0.0.0/16",
"name": "hello-world-subnet",
"network": "${google_compute_network.hello-world-network.name}", #A
"region": "us-central1"
}
]
}
]
}
]
}
#A 使用Terraform资源插值检索网络名称以创建子网
#B Terraform用网络的名称替换插值
一些工具使用输出来在模块之间传递资源属性。对于像Ansible这样的配置管理工具,该工具通过标准输出在自动化任务之间传递变量。对于像AWS CloudFormation或HashiCorp Terraform这样的配置工具,你为模块或堆栈生成输出,更高级别的模块可以消费。你可以根据你需要的任何模式或参数定制输出。例如,我可以为网络模块创建一个带有子网名称的输出。
清单 4 将子网名称设置为一个模块的输出:
{
"resource": [
{
"google_compute_subnetwork": [ #A
{
"hello-world-network": [
{
"ip_cidr_range": "10.0.0.0/16",
"name": "hello-world-subnet",
"network": "hello-world-network",
"region": "us-central1"
}
]
}
]
}
],
"output": [ #B
{ #B
"name": [ #B
{ #B
"value": "${google_compute_subnetwork.hello-world-network.name}" #B
} #B
] #B
} #B
] #B
}
#A 为网络定义子网
#B Terraform将子网名称作为网络模块的一部分输出
你也可以使用基础设施状态作为状态文件或基础设施提供者的API元数据。回忆一下前面关于控制权倒置的部分。我从一个叫做 "terraform.tfstate "的文件中得到了服务器的子网名称。这个文件是网络状态文件,是我的工具所提供的一个抽象概念。不是所有的工具都提供状态文件,我更喜欢使用基础设施供应商的API。基础设施的API很少变化,提供详细的信息,并说明状态文件可能不包括的带外变化。你可以在图3中找到你可以用来反转控制的不同抽象。

图3.依赖反转的抽象可以使用属性插值、模块输出或基础设施状态,取决于工具和依赖关系。
应用依赖注入
当你把控制反转和依赖反转结合起来,你就得到了依赖注入。控制反转隔离了对高层模块的改变,而依赖反转隔离了对低层模块的改变。依赖注入进一步隔离了变化,减轻了高层和低层模块变化的潜在爆炸半径(图4)。

图4.依赖注入结合了控制反转和依赖反转的原则,以放松基础设施的依赖性,并允许低层和高层资源的隔离演化。
我用Apache Libcloud为服务器和网络的例子实现了依赖注入。Apache Libcloud是谷歌云平台(GCP)API的一个库。我用它来搜索网络。服务器调用GCP API获取子网名称,解析GCP API元数据,并为自己分配网络范围内的第五个IP地址。
清单5 使用依赖性注入在网络上创建一个服务器:
import credentials
import ipaddress
import json
from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver
def get_network(name): #A
ComputeEngine = get_driver(Provider.GCE) #A
driver = ComputeEngine( # A
credentials.GOOGLE_SERVICE_ACCOUNT, #A
credentials.GOOGLE_SERVICE_ACCOUNT_FILE, #A
project=credentials.GOOGLE_PROJECT, #A
datacenter=credentials.GOOGLE_REGION) #A
return driver.ex_get_subnetwork( #A
name, credentials.GOOGLE_REGION) #A
class ServerFactoryModule:
def __init__(self, name, network, zone='us-central1-a'):
self._name = name
gcp_network_object = get_network(network) #B
self._network = gcp_network_object.name #C
self._network_ip = self._allocate_fifth_ip_address_in_range( #C
gcp_network_object.cidr) #C
self._zone = zone
self.resources = self._build()
def _allocate_fifth_ip_address_in_range(self, ip_range): #D
ip = ipaddress.IPv4Network(ip_range) #D
return format(ip[-2]) #D
def _build(self):
return {
'resource': [{
'google_compute_instance': [{
self._name: [{
'allow_stopping_for_update': True,
'boot_disk': [{
'initialize_params': [{
'image': 'ubuntu-1804-lts'
}]
}],
'machine_type': 'f1-micro',
'name': self._name,
'zone': self._zone,
'network_interface': [{
'subnetwork': self._network,
'network_ip': self._network_ip
}]
}]
}]
}]
}
if __name__ == "__main__":
server = ServerFactoryModule(name='hello-world', network='default') #A
with open('main.tf.json', 'w') as outfile:
json.dump(server.resources, outfile, sort_keys=True, indent=4)
#A 通过Apache Libcloud调用谷歌云平台API来获取默认网络的所有属性。
#B 获取一个网络对象,其模式由Apache Libcloud定义。
#C 选择名称和CIDR块参数,在网络的第五个IP地址上创建一个服务器。
#D 该方法计算出CIDR块范围内的第五个IP地址给服务器。
图5所示的例子通过允许服务器调用网络来实现控制的倒置。它使用GCP API作为抽象来检索网络属性,应用依赖性反转。当我改变网络的IP地址范围时,我的服务器会得到更新的地址范围,并在需要时重新分配IP地址。

图5.依赖性注入使我能够改变低级模块,即网络,并自动将变化传播到服务器,即高级模块。
相当于AWS
你可以使用AWS Python SDK来获取AWS VPC信息。该SDK与AWS的API交互,并返回与GCP例子类似的信息。
依赖注入作为一般原则适用于基础设施的依赖性管理。如果你在编写基础设施配置时应用了依赖注入,你就可以充分地解耦依赖关系,这样你就可以独立地改变它们而不影响其他基础设施。随着模块的增长,你可以继续重构到更具体的模式,并根据资源和模块的类型进一步解耦基础设施。
本节选就讲到这里。