Infrastructure as Code 行为驱动开发指南

\n

通过 Ansible 将 BDD 应用于服务器配置

\n \n \n

21 世纪初期,Kent Beck 声称:“遵循两条简单规则能让我们最大限度地发挥潜能:在编写任何代码前编写一个失败的自动化测试,并删除重复内容。”后来,Dan North 通过行为驱动开发 (BDD) 扩展了这一指导原则。

\n

Dan 尝试利用测试驱动开发 (TDD) 解决问题,在这一过程中,开发人员需要“知道从何处开始,要测试和不测试哪些内容,一次测试多少内容,如何称呼他们的测试,以及如何理解测试失败的原因”。为此,他创建了一个场景(或验收标准)模板,用于分析用户案例和提供执行用户案例的方法 (JBehave)。这将需求(用户案例)转换成了驱动应用程序行为的动态文档。本文将在此基础上进行扩展,使用驱动 Infrastructure as code (IaC) 的相同技术,为您使用 Ansible 的应用程序配置一个服务器。

\n

从案例到场景

\n

除了 INVEST(独立、可协商、有价值、可评估、小型、可测试)条件之外,用户案例还应采用封闭方式编写,也就是说,在完成编写时,应为用户“实现有意义的目标”(Mike Cohn,“用户案例应用:敏捷软件开发”)。用于捕获此用户案例的典型格式是 Connextra 用户案例模板:

\n
As a {role}\nI want {goal/desire}\nso that {benefit}
\n

利用这些案例,可以生成场景或验收标准。 通常,用户案例是实现应用程序的“愉快路径”。现在需要探索该路径和它的替代方案。Dan North 使用 given-when-then 模板来创建场景:

\n
Given some initial context (the givens),\nWhen an event occurs,\nthen ensure some outcomes.
\n

一种基于此模板的流行语言就是 Gherkin。 捕获模板后,可通过 CucumberJBehave 等框架执行它。

\n

IT 场景

\n

通常,与项目相关的基本的基础架构任务都是在第一次迭代期间完成的,该迭代称为 Zero Feature Release(ZFR 或 ziffer)。有时将其称为 skinny,它是一个足以支持应用程序及其开发的基础架构。通常将此基础架构定义为系统上的约束条件,比如“应用程序应使用 couchdb 作为数据库”或“开发团队将使用 Jenkins 进行持续集成”。尽管不是所有约束条件都应通过用户案例模板来表达,但这有助于在以后理解约束条件的来源和原因。

\n

安全案例

\n

桌面、移动或游戏控制器最终用户可能不会明确要求服务器应用程序在最新版的 Apache Web 服务器上运行。但是,得出该验收标准的评论可能出现在用户访谈或调查问卷中。用户数据丢失可能让用户很苦恼。当 Sony 的 PlayStation Network 受到黑客攻击时,用户和政客们会询问为什么缺乏服务器补丁和防火墙。倾听安全破坏、数据丢失、宕机或其他 IT 相关事件的影响,有助于为您的基础架构生成场景。

\n
清单 1. 初始用户案例
\n
As a user of your service\nI want my credit card information to be secured\nSo that I don't have to cancel and reorder my credit card.
\n

这个案例的问题在于,它不是封闭的,而且难以制作成场景。一种流行的方法是使用恶意用户案例来制作它。以下是从恶意角色角度制作的案例。

\n
清单 2. 黑客恶意用例 1
\n
As a hacker \nI want to port scan the server\nSo that I can see if vulnerable less secure services are running.
\n
清单 3. 黑客恶意用例 2
\n
As a hacker\nI want to leverage vulnerabilities in "out of date" packages\nSo that I can gain access to the system
\n

现在,可以将这些恶意用户案例制作成场景来阻止这类用户,比如:

\n
清单 4. 黑客恶意用例 1:场景 1
\n
Given the server has a firewall installed\nWhen a port scan is performed\nThen only ssl port 443 is open
\n

Gherkin 语言的一个特性是场景大纲概念,此概念将场景应用于数据集。

\n
清单 5. 黑客恶意用例 2:场景大纲 1
\n
Scenario outline: Expect security updates to be installed\n   Given the server is Ubuntu 14.04\n   And the package <package>\n   When the version is fetched\n   Then It should be equal or later than version <version>\n\n   Examples: Ubuntu 14.04 nginx packages with security updates\n     | package      | version          |\n     | nginx        | 1.4.6-1ubuntu3.7 |\n     | nginx-common | 1.4.6-1ubuntu3.7 |\n     | nginx-core   | 1.4.6-1ubuntu3.7 |
\n

创建配置项目

\n

有包括 ChefSaltStack 在内的许多 IaC 框架。本文利用了基于 Python 的 Ansible。可从命令行运行 pip install ansible 来安装它。

\n

不幸的是,Ansible 本身没有提供任何单元测试或静态分析功能。命令 ansible-galaxy init <playbook name> 可创建一个框架角色项目,但是它仅提供了一种手动测试机制。ServerSpec 等工具可以提供帮助,但帮助有限。Molecule 为 Ansible 角色开发提供了额外的软件开发最佳实践。

\n

Molecule 提供了以下基本工作流:

\n
    \n
  1. 验证角色的语法 (ansible-lint)。
  2. \n
  3. 通过 Vagrant 包装器创建用于测试的虚拟镜像。另外,它还支持 DockerOpenStack
  4. \n
  5. Converge 运行 Ansible 操作手册来配置镜像。
  6. \n
  7. 再次运行该角色,验证它不会更改任何内容(幂等性)。
  8. \n
  9. 通过静态分析测试并运行它们来实现验证。
  10. \n
\n

静态分析集成到部署脚本开发过程中是一种受欢迎的做法。利用 ansible-lintflake8rubocop,帮助在协作时保持软件一直遵循最佳实践。尽管不使用 Molecule 也能完成所有这些任务,但拥有一个整合了这些工具的平台也很不错。

\n

幂等性有助于确保脚本仅执行必要的更改。运行脚本两次不应更改任何内容。这听起来很简单,但实际操作却很难。要利用命令shell 任务执行外部操作,需要额外的逻辑来防止脚本运行两次。

\n

要安装 Molecule,请运行以下命令:

\n
                    pip install molecule
\n

要使用 Molecule 创建框架角色项目,请运行以下命令:

\n
ansible-galaxy init ansible-role-dw-bdd-example
\ncd ansible-role-dw-bdd-example
\nmolecule init
\nmkdir features
\n

features 目录用于存储 GHERKIN 特性文件。本文使用下面这个安全特性示例:

\n
Feature: Security\n\nStory: User's confidential data\nAs a user of your service\nI want methods for my credit card information to be stolen blocked\nSo that I don't have to cancel and reorder my credit card.\n\n# https://www.symantec.com/security_response/attacksignatures/detail.jsp?asid=20429\nEvil Story: MyDoom Trojan\nAs a hacker\nI want to infect a server with MyDoom\nSo that I can use it as a socks proxy to gain access to a system\n\nEvil Story: Old nginx packages\nAs a hacker\nI want to leverage vulnerabilities in out of date packages\nSo that I can gain access to the system\n\nScenario: Socks proxy is blocked\n  Given the server has a firewall installed\n  When a list of open ports is fetched\n  Then the socks port 1080 is not open\n\nScenario Outline: Expect security updates to be installed\n   Given the server is Ubuntu 14.04\n   And the package <package> is installed\n   When the version is fetched\n   Then It should be equal or later than version <version>\n\n   Examples: Ubuntu 14.04 nginx packages with security updates\n     | package      | version          |\n     | nginx        | 1.4.6-1ubuntu3.7 |\n     | nginx-common | 1.4.6-1ubuntu3.7 |\n     | nginx-core   | 1.4.6-1ubuntu3.7 |
\n

编写步骤

\n

步骤定义是执行 GHERKIN 语法的机制。 由于 GHERKIN 不依赖于特定编程语言,步骤定义是使用 Python、Ruby、JavaScript 等语言编写的。每一步都能接受一个或多个参数,这些参数可在 GERKIN 语句外解析。您的场景中的每条语句都将映射到步骤定义中的方法。每种编程语言都有不同的 Cucumber 实现。

\n

默认情况下,Molecule 使用基于 Python 的 TestInfra 测试框架。不使用单独的运行器来集成 Cucumber 与 Molecule 的最简单方法是,结合使用 TestInfrapytest-bdd。这样做的原因是,TestInfra 和 pytest-bdd 都是 pytest 框架的扩展。behave 等解决方案和其他 Cucumber 实现需要做更多工作才能实现相同的集成。话虽如此,最简单的方法仍需要做一些工作。

\n

要让 TestInfra 兼容 Molecule,需要生成使用 Connection API 的主机对象。因为 Molecule 动态生成了一个清单,所以会将此对象添加到脚本的开头 ("test/test_default.py"):

\n
import testinfra\n\nhost = testinfra.get_host(\n "ansible://all?ansible_inventory=.molecule/ansible_inventory",\n sudo=True)
\n

需要从 pytest-bdd 导入 given、when、then 和 scenarios 包:

\n
from pytest_bdd import (\n    given,\n    scenarios,\n    then,\n    when\n)
\n

对于每个使用场景大纲的场景,都要添加一个示例转换器

\n
@scenario('../features/security.feature',\n 'Expect security updates to be installed',\n example_converters=dict(package=str, version=str))\ndef test_package_scenario(): \n '''\n scenarios with tables that require type mapping must be referenced\n directly before calling "scenarios()"\n '''\n pass
\n

添加所有示例转换器后,调用“scenarios('../features’)”来拉入剩余场景。使用 TestInfra 主机对象的 pytest-bdd 代码。

\n
@given('the package <package> is installed')\ndef package_is_installed(package): \n    assert host.package(package).is_installed\n    return dict(package=package)\n\n\n@given('the server is Ubuntu 14.04')\ndef the_server_is_ubuntu_1404(): \n    """the server is Ubuntu 14.04."""\n    assert host.system_info.type == 'linux'\n    assert host.system_info.distribution == 'ubuntu'\n    assert host.system_info.release == '14.04'\n\n\n@when('the server is running')\ndef the_nginx_server_is_running(): \n    """the ngingx server is running."""\n    assert host.service('nginx').is_running\n\n\n@when('the version is fetched')\ndef the_version_is_fetched(package_is_installed): \n    """the version is fetched."""\n    version = host.package(package_is_installed['package']).version\n    package_is_installed['version'] = version\n\n\n@given('the server has a firewall installed')\ndef the_server_has_a_firewall_installed(): \n    """the server has a firewall installed."""\n    assert host.package('ufw').is_installed\n\n\n@pytest.fixture\n@when('a list of open ports is fetched')\ndef a_list_of_open_ports_is_fetched(): \n    """a list of open ports is fetched."""\n    return host.socket.get_listening_sockets()\n\n\n@then(parsers.parse('the socks port {port:d} is not open'))\ndef the_socks_port_1080_is_not_open(a_port_scan_is_performed, port): \n    """the socks port 1080 is not open."""\n    url = 'tcp://0.0.0.0:%d' % port\n    assert url not in a_port_scan_is_performed\n\n\n@then('It should be equal or later than version <version>')\ndef it_should_be_equal_or_later_than_version_version(package_is_installed,\n                                                     version): \n    """It should be equal or later than version <version>."""\n    assert package_is_installed['version'] == version
\n

通过测试并响应更改

\n

运行 molecule test 将会失败,因为尚未编写 Ansible 脚本。编写 Ansible 任务后,应进行更多测试,直到所有测试都通过为止。此刻,Ansible 滚动才算完成。 下面是一组通过这些测试的样本 Ansible 任务:

\n
---\n- name: Install nginx server\n  apt: \n    name: nginx\n- name: Block all ports\n  ufw: \n    state: enabled\n    policy: reject\n    log: yes\n- name: Allow ssh\n  ufw: \n    rule: allow\n    name: OpenSSH\n- name: Allow 443\n  ufw: \n    rule: allow\n    port: 443
\n

BDD 的关键优势是,只要测试失败,就意味着存在一个缺陷或者您的文档是需要更新的。例如,使用新的安全漏洞来更新特性文档,这意味着您的文档是需要更新的,因为特性文件是单一事实来源。如果测试失败,那么服务器配置中有缺陷需要更改。

\n\n\n \n \n

相关主题

\n \n \n \n \n
{{uname}}

{{meta.replies}} 条回复
写下第一个评论!

-----------到底了-----------