AnsibleでInSpecを利用する

[InSpec](https://www.inspec.io/docs/)[^1]自体はAnsibleのRoleをTDDで作成するときに[kitchen-inspec](https://github.com/inspec/kitchen-inspec)でよく利用していたものの、実環境に対するテストでは使ったことがなかったのでどう使うかを整理。

# 環境

- Ansible 2.7.10
- InSpec 4.3.2

# InSpecの基本的な使い方

## インストール
Chefと同じくパッケージが提供されているので[Chef Downloads](https://downloads.chef.io/inspec)からダウンロードするのが手っ取り早い。
gem installする場合はCLIで利用するためにinspec-binが必要なので注意。

```shell-session:rpmの場合
$ yum -y localinstall /tmp/inspec-4.3.2-1.el7.x86_64.rpm
```

```shell-session:gemの場合
$ rbenv exec gem install inspec inspec-bin
$ rbenv rehash
```

```shell-session:インストール確認
$ inspec --version
4.3.2

$ inspec help
Commands:
  inspec archive PATH                # archive a profile to tar.gz (default) or zip
...
```

## Profileの作成

`knife`コマンドよろしく`inspec`コマンドにもテンプレートの生成機能があるのでそちらを利用。
`inspec init profile PROFILE_NAME`

```shell-session
$ inspec init profile my-profile
```

```
my-profile
├── README.md
├── controls
│   └── example.rb
├── inspec.yml
└── libraries
```

ProfileのDirectory Layoutの概要は以下の通り。

- inspec.yml (required)
  - このテストに関するメタ情報を記載する
  - 実行可能なOS環境や他のテストとの依存関係などの定義が可能
- controls (required)
  - テストを配置するディレクトリ
  - このディレクトリ配下に配置されたrbファイルがすべて実行対象になる
- libraries (optional)
  - [Custom Resources](https://www.inspec.io/docs/reference/dsl_resource/)を配置するディレクトリ
  - 今のバージョンだと標準の[Resources](https://www.inspec.io/docs/reference/resources/)だけで大体事足りるので使うことはあんまりないかも
- files (optional)
  - `inspec init profile`では作成されない
  - このディレクトリ配下のファイルはテストコードの中で`inspec.profile.file('ファイル名')`とすることでアクセス可能
  - テストで利用するパラメータを定義するために使うが、環境依存で変更されるファイルを置く場所ではないことに注意


## リモートサーバへのテスト実行
サンプルで作成されるexample.rbは「/tmpというディレクトリが作成されていること」のテストになっているのでテスト対象がCentOSとかであれば`inspec init profile`で作成したプロファイルを使ってそのまま試せる。
Ansibleで管理しているのであれば、Ansible実行用のリモートユーザをそのまま使えばOK。
`inspec exec PROFILE_PATH --target=ssh://REMOTE_USER@REMOTE_HOST`

```shell-session:実行例
$ inspec exec my-profile --target=ssh://vagrant@192.168.33.10

Profile: InSpec Profile (my-profile)
Version: 0.1.0
Target:  ssh://vagrant@192.168.33.10:22

  ✔  tmp-1.0: Create /tmp directory
     ✔  File /tmp should be directory

  File /tmp
     ✔  should be directory

Profile Summary: 1 successful control, 0 control failures, 0 controls skipped
Test Summary: 2 successful, 0 failures, 0 skipped
```


# Ansibleとの併用

Serverspecでもちょっと考える必要があるのがサーバ毎のパラメータを読み込む箇所。
Ansibleでは`ansible-inventory`を利用することで実行対象で利用されるvarsをyaml形式で出力できる[^2]。
InSpecでは`--input-file`オプションを使うことで外部のyamlファイルをロードすることができるので、生成したyamlファイルをロードすることでサーバ毎のパラメータを利用したテストが実行可能となる。

## Direcotry Layoutについて

ローカル検証用に[ochiba/inspec-example](https://github.com/ochiba/inspec-example)を用意したのでこちらで解説。

```
├── ansible.cfg
├── inventories
│   └── stg
│       ├── group_vars
│       ├── host_vars
│       └── nodes.yml
├── plays                              # playbookの配置先（roles_pathはansible.cfgで調整)
├── roles
│   └── common
│       ├── defaults
│       ├── tasks
│       ├── templates
│       └── test
│           └── integration
│               └── default            # InSpec Profile
│                   ├── controls
│                   ├── inspec.yml
│                   └── libraries
└── test
    └── attributes                     # ansible-inventoryで出力したvarsの配置先
```

[Alternative Directory Layout](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#alternative-directory-layout)からPlaybookの配置を少し弄った構成。
Profile自体はTDDで作成したものを使い回すのがポイント。


## パラメータの呼び出し方
検証用に作成したテストは以下の通り。

```ruby:roles/common/test/integration/default/controls/myprofile.rb
# encoding: utf-8
# copyright: 2019, Hiroki KUNITAKE

title 'MyProfile section'

control 'myprofile-01' do
  impact 0.7
  title 'Create myprofile.txt'
  desc 'Check contents'
  describe file('/var/tmp/myprofile.txt') do
    it { should be_file }
    its('content') { should match /^My name is #{attribute('my_name')}/ }
    its('content') { should match /^I'm in #{attribute('my_group')}/ }
  end
end
```

`--input-file`オプションで読んだパラメータは`attribute('VARS_NAME')`で呼び出せる。
階層構造になっている場合は親を呼び出して一旦変数に格納してから利用する。


## 実行サンプル

`ansible-inventory`を利用してAttributes File(vars情報が入ったyamlファイル）を作成し、そのファイルを`--input-file`オプションで食わせてテストを実行する。

```shell-session
$ ansible-inventory --yaml --inventory=inventories/stg/nodes.yml --host=192.168.33.10 > test/attributes/192.168.33.10.yml
$ cat test/attributes/192.168.33.10.yml
my_group: Web
my_name: Web-01

$ inspec exec roles/common/test/integration/default --target=ssh://vagrant@192.168.33.10 --input-file=test/attributes/192.168.33.10.yml

Profile: Common baseline (common-baseline)
Version: 0.1.0
Target:  ssh://vagrant@192.168.33.10:22

  ✔  myprofile-01: Create myprofile.txt
     ✔  File /var/tmp/myprofile.txt should be file
     ✔  File /var/tmp/myprofile.txt content should match /^My name is Web-01/
     ✔  File /var/tmp/myprofile.txt content should match /^I'm in Web/


Profile Summary: 1 successful control, 0 control failures, 0 controls skipped
Test Summary: 3 successful, 0 failures, 0 skipped
```

今のところ`inspec exec`そのものには並列実行の機能がなく1ホストずつしか実行できない。
グループ単位でテストを実行したい場合はProfileやAttirbutes Fileの配置パスについてルールを設けてクリプト化する必要がある。


[^1]: [Serverspec](https://serverspec.org/)の拡張として作られているテストツール。サーバの状態をチェックするのにも利用できるが、どちらかといえばある規則に従っているかどうかをスコア付けするためのツール。
[^2]: Factsやvars_filesはその特性上`ansible-inventory`では出力できないので別の手立てが必要。