目次


ビヘイビア駆動開発者向け Infrastructure as Code 入門

Ansible を使用したサーバー・プロビジョニングに BDD を適用する

Comments

2000 年代のはじめの頃、Kent Beck 氏が「2 つの単純な規則に従うことで、開発者の作業はその目標にぐんと近づくようになる。その規則とは、新しいコードを書く前に失敗する自動テストを書くこと、そして重複を取り除くことである」と宣言しました。その後、Dan North 氏はこのガイダンスを基に、ビヘイビア駆動開発 (BDD) という手法を考案しました。

North 氏が解決しようとしたテスト駆動開発 (TDD) に伴う問題は、開発者が「テストの出発点、テストする対象、テストしない対象、1 回のテストで対象とする量、テストの呼び出し側、テストの失敗理由を理解する方法」を把握していなければならないことです。彼はこの問題を解決するために、シナリオ (受け入れ基準) テンプレートを作成し、ユーザー・ストーリーを分析して、そのストーリーを実行するメソッドを指定できるようにしました (JBehave)。これにより、仕様 (ユーザー・ストーリー) はアプリケーションの振る舞いを駆動する実際のドキュメントという形になります。この記事ではビヘイビア駆動開発手法の延長として、同じ手法で Ansible を使用してアプリケーションのサーバーをプロビジョニングする Infrastructure as Code (IaC) を駆動します。

ストーリーからシナリオへ

ユーザー・ストーリーとは、顧客にとって有益な機能 (「フィーチャー」という言葉を使う人もいます) のチャンクを指します。

Martin Fowler と Kent Beck の共著『XP エクストリーム・プログラミング実行計画』(ピアソンエデュケーション、2001年)

ユーザー・ストーリーは、INVEST (Independent (独立している)、Negotiable (交渉可能である)、Valuable (価値がある)、Estimable (見積り可能である)、Small (小さい)、Testable (テスト可能である)) 基準の枠を超えて、完結された形になるように作成しなければなりません。このことはつまり、ユーザーにとって「有意義な目標を達成して」(Mike Cohn 著『User Stories Applied: For Agile Software Development』) ストーリーを完了する必要があることを意味します。そのために使用される一般的なフォーマットが、以下に示す Connextra ユーザー・ストーリー・テンプレートです。

As a {role}
I want {goal/desire}
so that {benefit}

これらのユーザー・ストーリーから、シナリオあるいは受け入れ基準を生成することができます。多くの場合、ユーザー・ストーリーはアプリケーション全体での「ハッピー・パス」です。そこで、ハッピー・パスに該当するパスとその代替パスを調べなければなりません。Dan North 氏は、以下の given-when-then (前提-条件-結果) テンプレートをシナリオとして使用しています。

Given some initial context (the givens),
When an event occurs,
then ensure some outcomes.

このテンプレートに基づく言語としてよく使われているのが、Gherkin です。ユーザー・ストーリーが取り込まれたテンプレートは、CucumberJBehave などのフレームワークを使用して実行可能ファイルにすることができます。

IT のシナリオ

技術的なインフラストラクチャーは例外なく、ストーリーと結合させて作成し、そのストーリーに必要なものをサポートするように開発しなければなりません。

Martin Fowler と Kent Beck の共著『XP エクストリーム・プログラミング実行計画』(ピアソンエデュケーション、2001年)

通常、プロジェクトに関連する基本的なインフラストラクチャー・タスクは、Zero Feature Release (ZFR、または ziffer) と呼ばれる最初のイテレーションの間に行われます。この最初のイテレーション中のインフラストラクチャー (skinny とも呼ばれます) は、アプリケーションとその開発を辛うじてサポートするだけのものです。一般に、このインフラストラクチャーはシステム上で制約として定義されます。例えば、「アプリケーションのデータベースには couchdb を使用すること」、「開発チームは継続的インテグレーションに Jenkins を使用すること」などといった制約です。すべての制約をユーザー・ストーリー・テンプレートで表現しなければならないわけではありませんが、そうしておくと、後で制約のソースと理由を理解するのに役立ちます。

セキュリティー・ストーリー

デスクトップ、モバイル、ゲーム・コンソールのエンド・ユーザーが、サーバー・アプリケーションを最新バージョンの Apache Web サーバー上で実行するよう具体的に求めることはないかもしれませんが、インタビューやアンケートでのユーザーからのコメントで、最終的な受け入れ基準が明らかになる可能性があります。例えばユーザー・データの損失は、当該ユーザーに大きな被害をもたらしかねません。Sony の PlayStation Network がハッキングされた事件では、Sony はユーザーと政治家たちからサーバーのパッチやファイアウォールの欠如について質問攻めに遭いました。セキュリティー違反、データ損失、ダウンタイム、あるいはその他の IT 関連の事件によってどのような影響があったのかに耳を傾けると、インフラストストラクチャーのシナリオが形になってきます。

リスト 1. 初期のユーザー・ストーリー
As a user of your service
I want my credit card information to be secured
So that I don't have to cancel and reorder my credit card.

上記のユーザー・ストーリーには問題があります。それは、このユーザー・ストーリーは完結されてはおらず、詳細なシナリオとして説明するのが難しいことです。よく使われている方法の 1 つは、悪いパターンのユーザー・ストーリーを使用して詳細を説明することです。以下に、不正なアクターの観点から見た、詳細なストーリーを記載します。

リスト 2. ハッカーによる不正な使用例 1
As a hacker 
I want to port scan the server
So that I can see if vulnerable less secure services are running.
リスト 3. ハッカーによる不正な使用例 2
As a hacker
I want to leverage vulnerabilities in "out of date" packages
So that I can gain access to the system

上記のユーザー・ストーリーであれば、これらの使用例を防ぐためのシナリオとして詳細に説明することができます。

リスト 4. ハッカーによる不正な使用例 1: シナリオ 1
Given the server has a firewall installed
When a port scan is performed
Then only ssl port 443 is open

Gherkin 言語の特徴は、シナリオをデータ・セットに適用するという、シナリオ・アウトラインの概念です。

リスト 5. ハッカーによる不正な使用例 2: シナリオ・アウトライン 1
Scenario outline: Expect security updates to be installed
   Given the server is Ubuntu 14.04
   And the package <package>
   When the version is fetched
   Then It should be equal or later than version <version>

   Examples: Ubuntu 14.04 nginx packages with security updates
     | package      | version          |
     | nginx        | 1.4.6-1ubuntu3.7 |
     | nginx-common | 1.4.6-1ubuntu3.7 |
     | nginx-core   | 1.4.6-1ubuntu3.7 |

プロビジョニング・プロジェクトを作成する

使用できる IaC フレームワークには、Chef から SaltStack に至るまで、さまざまなものがあります。そのうち、この記事では Python べースの Ansible を使用します。Ansible をダウンロードするには、コマンド・ラインから pip install ansible を実行します。

あいにく、Ansible 単独では、単体テストや lint 機能に対応することができません。ansible-galaxy init <プレイブック名> コマンドを実行するとスケルトン・ロール・プロジェクトが作成されますが、これは手作業でテストするためのメカニズムに過ぎません。ServerSpec などのツールは役立つとは言っても、それだけでは不十分です。Molecule は、ソフトウェア開発のベスト・プラクティスを Ansible のロール開発に追加します。

Molecule が提供する基本的なワークフローは以下のとおりです。

  1. ロールの構文を確認します (ansible-lint)。
  2. Vagrant ラッパーを介したテストに使用する仮想イメージを作成します。あるいは、DockerOpenStack もサポートされています。
  3. converge で Ansible プレイブックを実行してイメージをプロビジョニングします。
  4. ロールを再実行して、ロールによって何も変更されないことを確認します (べき等性)。
  5. 検証のためにテストを lint して実行します。

lint 機能をデプロイメント・スクリプトに統合した開発プロセスは歓迎されます。コラボレーションの際には、ansible-lintflake8、および rubocop を利用することで、ソフトウェアをベスト・プラクティスに近い状態に維持できるようになります。Molecule を使わなくても、このすべての目標を達成することは可能ですが、これらのツールによる機能を組み込んでおくと便利です。

べき等性は、変更が必要なものだけをスクリプトが変更することを確実にする上で役立ちます。スクリプトを 2 回実行することで、何かが変更されるようであってはなりません。変更されないようにするのは簡単なことに聞こえるかもしれませんが、実際にはかなり難しいことです。コマンドシェル・タスクを使用して外部アクションを実行するには、そのアクションが 2 回実行されないようにするためのロジックが追加で必要になります。

Molecule をインストールするには、以下のコマンドを実行します。

                    pip install molecule

Molecule を使用してスケルトン・ロール・プロジェクトを作成するには、以下のコマンドを実行します。

ansible-galaxy init ansible-role-dw-bdd-example
cd ansible-role-dw-bdd-example
molecule init
mkdir features

features ディレクトリーは、GHERKIN フィーチャー・ファイルを格納するためのものです。この記事では、以下のサンプル・セキュリティー・フィーチャーを使用します。

Feature: Security

Story: User's confidential data
As a user of your service
I want methods for my credit card information to be stolen blocked
So that I don't have to cancel and reorder my credit card.

# https://www.symantec.com/security_response/attacksignatures/detail.jsp?asid=20429
Evil Story: MyDoom Trojan
As a hacker
I want to infect a server with MyDoom
So that I can use it as a socks proxy to gain access to a system

Evil Story: Old nginx packages
As a hacker
I want to leverage vulnerabilities in out of date packages
So that I can gain access to the system

Scenario: Socks proxy is blocked
  Given the server has a firewall installed
  When a list of open ports is fetched
  Then the socks port 1080 is not open

Scenario Outline: Expect security updates to be installed
   Given the server is Ubuntu 14.04
   And the package <package> is installed
   When the version is fetched
   Then It should be equal or later than version <version>

   Examples: Ubuntu 14.04 nginx packages with security updates
     | package      | version          |
     | nginx        | 1.4.6-1ubuntu3.7 |
     | nginx-common | 1.4.6-1ubuntu3.7 |
     | nginx-core   | 1.4.6-1ubuntu3.7 |

ステップを作成する

ステップ定義は、GHERKIN 構文を実行するためのメカニズムです。GHERKIN はプログラミング言語に依存しませんが、ステップ定義は Python、Ruby、JavaScript などの特定の言語で作成します。各ステップでは、GERKIN ステートメントに解析できる 1 つ以上の引数を取ることができます。シナリオに含まれるステートメントのそれぞれが、ステップ定義内のメソッドにマッピングされます。プログラミング言語ごとに異なる Cucumber 実装があります。

デフォルトでは、Molecule は Python ベースの TestInfra テスト・フレームワークを使用します。別個のランナーを使わずに Cucumber を Molecule に統合するには、TestInfrapytest-bdd を使用するのが最も簡単な方法になります。なぜなら、TestInfra と pytest-bdd はどちらも pytest フレームワークの拡張機能であるためです。behave などのソリューションやその他の Cucumber 実装には、同じ統合を達成するためにもう少し作業が必要になってきます。とは言え、どの方法を取るにしても、ある程度の作業が必要になることに変わりはありません。

TestInfra を Molecule と連動させるには、Connection API を使用するホスト・オブジェクトを生成する必要があります。Molecule は臨機応変にインベントリーを生成するため、スクリプト (「test/test_default.py」) の先頭に以下のコードを追加してください。

import testinfra

host = testinfra.get_host(
 "ansible://all?ansible_inventory=.molecule/ansible_inventory",
 sudo=True)

pytest-bdd から、given、when、then、および scenarios パッケージをインポートする必要があります。

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

シナリオ・アウトラインを使用するシナリオごとに、サンプル・コンバーターを追加します。

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

すべてのサンプル・コンバーターを追加した後、“scenarios('../features’)” を呼び出して残りのシナリオを取り込みます。TestInfra ホスト・オブジェクトを使用した pytest-bdd コードは以下のとおりです。

@given('the package <package> is installed')
def package_is_installed(package):
    assert host.package(package).is_installed
    return dict(package=package)


@given('the server is Ubuntu 14.04')
def the_server_is_ubuntu_1404():
    """the server is Ubuntu 14.04."""
    assert host.system_info.type == 'linux'
    assert host.system_info.distribution == 'ubuntu'
    assert host.system_info.release == '14.04'


@when('the server is running')
def the_nginx_server_is_running():
    """the ngingx server is running."""
    assert host.service('nginx').is_running


@when('the version is fetched')
def the_version_is_fetched(package_is_installed):
    """the version is fetched."""
    version = host.package(package_is_installed['package']).version
    package_is_installed['version'] = version


@given('the server has a firewall installed')
def the_server_has_a_firewall_installed():
    """the server has a firewall installed."""
    assert host.package('ufw').is_installed


@pytest.fixture
@when('a list of open ports is fetched')
def a_list_of_open_ports_is_fetched():
    """a list of open ports is fetched."""
    return host.socket.get_listening_sockets()


@then(parsers.parse('the socks port {port:d} is not open'))
def the_socks_port_1080_is_not_open(a_port_scan_is_performed, port):
    """the socks port 1080 is not open."""
    url = 'tcp://0.0.0.0:%d' % port
    assert url not in a_port_scan_is_performed


@then('It should be equal or later than version <version>')
def it_should_be_equal_or_later_than_version_version(package_is_installed,
                                                     version):
    """It should be equal or later than version <version>."""
    assert package_is_installed['version'] == version

テストを合格させて、変更に対応する

Ansible スクリプトはまだ作成されていないため、molecule テストを実行すると失敗するはずです。Ansible タスクが作成されるにつれ、合格しなければならないテストの数も増えてきます。この作業は、すべてのテストに合格するまで続ける必要があります。すべてのテストに合格した時点で、Ansible ロールが完成します。以下に、テストに合格する Ansible タスクのサンプル・セットを記載します。

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

BDD の主なメリットは、テストが失敗すると、それによって常に、コードに欠陥があるか、ドキュメントが最新の状態でないことがわかるという点にあります。フィーチャー・ファイルが真実を語る唯一の情報源であるため、フィーチャー・ドキュメントを更新すると、それが最新のドキュメントになります。例えば更新によって新しいセキュリティーの脆弱性がもたらされたとすると、最新のフィーチャー・ドキュメントに対するテストは失敗し、それによってサーバー構成に変更が必要な欠陥があることがわかるというわけです。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=DevOps, Open source
ArticleID=1056761
ArticleTitle=ビヘイビア駆動開発者向け Infrastructure as Code 入門
publish-date=01182018