基础设施依赖性的模式介绍

265 阅读7分钟

依赖性注入

你必须表达依赖关系,以减少低级模块的变化对高级模块的影响。例如,网络变化不应破坏任何高层资源,如队列、应用程序或数据库。

依赖性注入涉及两个原则:控制反转和依赖反转。

控制的反转

单向依赖意味着一个高层资源依赖于一个低层资源;高层资源调用低层资源的属性。当你在基础设施的依赖关系中执行单向关系时,你自然会应用控制反转。高层资源调用低层资源以获得其需要的属性(图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.依赖关系反转将低层资源元数据的抽象返回给依赖它的资源。


你可以从三种类型的抽象中进行选择:

  1. 资源属性的内插(在模块内)
  2. 模块输出(模块之间)
  3. 基础设施状态(模块之间)

插值处理模块或配置内资源或任务之间的属性传递。该工具为你从基础设施的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例子类似的信息。

依赖注入作为一般原则适用于基础设施的依赖性管理。如果你在编写基础设施配置时应用了依赖注入,你就可以充分地解耦依赖关系,这样你就可以独立地改变它们而不影响其他基础设施。随着模块的增长,你可以继续重构到更具体的模式,并根据资源和模块的类型进一步解耦基础设施。

本节选就讲到这里。