LoginSignup
9
5

More than 1 year has passed since last update.

Goss による高速テストの実装検討

Last updated at Posted at 2022-05-01

はじめに

普段、Serverspec を利用してサーバー構築後のテストを行っているのですが、対象サーバー台数やテスト項目が肥大化して、テスト実行時間が1時間くらい掛かるようになり、代替手段を探したところ、 Goss という Go製のテストフレームワークを見つけました。

そこで、 Goss のインストール方法、使いかたについて検討することにしました。

インストール方法

Goss公式サイトにあるように、 curl | sh で簡単にインストールできます。

curl -fsSL https://goss.rocks/install | sh

環境によってはインターネットからダウンロードができないシーンもあるので、バイナリファイルをあらかじめダウンロードしておいて、運用端末から配布してもよさそうです。

使いかた

ヘルプを見てみます。

$ goss --help
NAME:
   goss - Quick and Easy server validation

USAGE:
   goss [global options] command [command options] [arguments...]

VERSION:
   v0.3.16

COMMANDS:
     validate, v  Validate system
     serve, s     Serve a health endpoint
     render, r    render gossfile after imports
     autoadd, aa  automatically add all matching resource to the test suite
     add, a       add a resource to the test suite
     help, h      Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --gossfile value, -g value  Goss file to read from / write to (default: "./goss.yaml") [$GOSS_FILE]
   --vars value                json/yaml file containing variables for template [$GOSS_VARS]
   --vars-inline value         json/yaml string containing variables for template (overwrites vars) [$GOSS_VARS_INLINE]
   --package value             Package type to use [apk, dpkg, pacman, rpm]
   --help, -h                  show help
   --version, -v               print the version

テストシナリオの作りかた

Serverspec にはない特徴的な機能として、 autoadd, add があります。これらは、今ある状態を正として自動的にテストシナリオを生成するものです。

autoadd

file, group, package, port, process, service, user を探索し、マッチするものをもとにテストシナリオを生成します。

例えば、 sshd を指定します。

$ goss autoadd sshd

goss.yaml には以下のようなテストシナリオが生成されます。(すでにファイルが存在する場合は追記されます)

goss.ayml
port:
  tcp:22:
    listening: true
    ip:
    - 0.0.0.0
  tcp6:22:
    listening: true
    ip:
    - '::'
service:
  sshd:
    enabled: true
    running: true
user:
  sshd:
    exists: true
    uid: 74
    gid: 74
    groups:
    - sshd
    home: /var/empty/sshd
    shell: /sbin/nologin
group:
  sshd:
    exists: true
    gid: 74
process:
  sshd:
    running: true

-g オプションで出力するファイルを指定することができます。

add

autoadd では対応できないテスト(addr, command, dns, http, interface, kernel-param, mount)は add を使って生成します。

$ goss add command "cat /etc/passwd | wc -l"
Adding Command to './goss.yaml':

cat /etc/passwd | wc -l:
  exit-status: 0
  stdout:
  - "32"
  stderr: []
  timeout: 10000

手動編集

YAMLファイルを手動で編集します。使用可能なリソースタイプは以下の通りです。詳しくは goss manual を参照してください。

リソースタイプ 説明
addr リモートの address:port に接続できるかを評価する
command コマンドを実行し、 exit status や出力を評価する
dns アドレスの名前解決ができるかを評価する
file ファイル、ディレクトリ、シンボリックリンクの状態を評価する
gossfile gossfile をインポートする
group グループの状態を評価する
http HTTPレスポンスステータスコードやコンテンツを評価する
interface ネットワークインターフェースの値を評価する
kernel-param カーネルパラメータ(sysctl) の値を評価する
mount マウントポイントの attribute を評価する
matching matcher に対して特定のコンテンツを評価する
package パッケージの状態を評価する
port ローカルポートの状態を評価する
process プロセスが起動しているかを評価する
service サービスの状態を評価する
user ユーザーの状態を評価する

注意点として、同じファイルに同じ内容のテスト項目を書くことはできません。(片方が無視されます)

誤った書きかた
file:
  /etc/ssh/sshd_config:
    exists: true

file:
  /etc/httpd/conf/httpd.conf:
    exists: true
正しい書きかた
file:
  /etc/ssh/sshd_config:
    exists: true
  /etc/httpd/conf/httpd.conf:
    exists: true

同じファイルでなければよいので、別々のファイルに書いておいて、 gossfile でインポートすると気にしなくてもよさそうです。

応用的な書きかた

変数とループ

変数ファイルを用意しておき、ループで呼び出すことができます。

変数 説明
{{.Env}} 環境変数
{{.Vars}} --vars で指定したファイルで定義した変数
例1. Vars
vars.yaml
users:
  - user1
  - user2
goss.yaml
user:
{{range .Vars.users}}
  {{.}}:
    exists: true
    groups:
    - {{.}}
    home: /home/{{.}}
    shell: /bin/bash
{{end}}
例2. Vars, Env
vars.yaml
centos:
  packages:
    kernel:
      - "4.9.11-centos"
      - "4.9.11-centos2"
debian:
  packages:
    kernel:
      - "4.9.11-debian"
      - "4.9.11-debian2"
goss.yaml
package:
# Looping over a variables defined in a vars.yaml using $OS environment variable as a lookup key
{{range $name, $vers := index .Vars .Env.OS "packages"}}
  {{$name}}:
    installed: true
    versions:
    {{range $vers}}
      - {{.}}
    {{end}}
{{end}}
例3. if
goss.yaml
package:
{{if eq .Env.OS "centos"}}
  # This test is only when $OS environment variable is set to "centos"
  libselinux:
    installed: true
{{end}}

パターン

file, command, output で使えます。

パターン 説明
"string" string を含むか
"!string" string を含まないか
"\\!string" !string を含むか
"/regex/" 正規表現にマッチするか
"!/regex/" 正規表現にマッチしないか
sshd_config.yaml
file:
  /etc/ssh/sshd_config:
    exists: true
    contains:
    - "/^PermitRootLogin no/"
    - "/^UseDNS no/"

実行例は以下の通りです:

$ goss --gossfile sshd_config.yaml validate --format tap
1..2
ok 1 - File: /etc/ssh/sshd_config: exists: matches expectation: [true]
ok 2 - File: /etc/ssh/sshd_config: contains: all expectations found: [/^PermitRootLogin no/, /^UseDNS no/]

マッチしないと以下のような結果になります。

$ goss --gossfile sshd_config.yaml validate --format tap
1..2
ok 1 - File: /etc/ssh/sshd_config: exists: matches expectation: [true]
not ok 2 - File: /etc/ssh/sshd_config: contains: patterns not found: [/^UseDNS yes/]

command

exit-status, stdout, stderr で評価します。
exit-status は必須のようです。定義しないとエラーになりました。

passwd.yaml
command:
  number_of_users:
    exec: "cat /etc/passwd | wc -l"
    exit-status: 0
    stdout: 
      - 32
$ goss --gossfile passwd.yaml validate --format tap
1..2
ok 1 - Command: number_of_users: exit-status: matches expectation: [0]
ok 2 - Command: number_of_users: stdout: all expectations found: [32]

gossfile

複数のファイルをインクルードできます。

goss.yaml
gossfile:
  include/common/*.yaml: {}
  include/target/*.yaml: {}

テストシナリオ作成時の Tips

render は すべての gossfile をインポートしたあとの gossfile を生成する機能ですが、バリデーションチェックとしても使えます。
エラー発生時の実行例は以下の通りです:

$ goss --gossfile ss.yaml --vars vars.yaml render
could not unmarshal "command:\n  \n  - version:\n    exec: \"ss -tuna | grep ':22 ' | grep LISTEN\"\n    exit-status: 0\n  \n  - version:\n    exec: \"ss -tuna | grep ':80 ' | grep LISTEN\"\n    exit-status: 0\n  \n  - version:\n    exec: \"ss -tuna | grep ':443 ' | grep LISTEN\"\n    exit-status: 0\n  \n" as YAML data: yaml: unmarshal errors:
  line 3: cannot unmarshal !!seq into map[string]map[string]interface {}

実践投入に向けて

Ansible で以下の手順で実行することにします。

  1. goss およびテストコードをターゲットノードに送る
  2. テストを実行する
  3. 結果をファイルに出力する

※必要に応じてターゲットノードの資材を削除してもよいでしょう

PoC

以下のようなディレクトリ・ファイル構成とします。

.
├── files
│   └── goss ............ ターゲットノードに送るテスト環境一式
│       ├── bin
│       │   └── goss .... goss本体
│       └── goss.yaml ... テストシナリオ
├── goss_local.yml ...... Ansible Playbook
├── library
│   └── goss.py ......... goss 呼び出し用ライブラリ
└── log ................. テスト実行結果格納用

goss.pyansible-goss を利用させていただきました。

Ansible Playbook は以下の通りです。 PoC なので、ローカル実行かつ1ファイルなのはご容赦ください。また、実行環境が少々古いので、モジュール名がFQCNではありません。適宜 ansible.builtin.copy などと読み替えてください。

goss_local.yml
---
- hosts: localhost
  connection: local
  become: yes
  gather_facts: no
  tasks:
  - name: copy goss files
    copy:
      src: goss
      dest: /tmp
  
  - name: delegate the right to execute
    file:
      path: /tmp/goss/bin/goss
      owner: root
      group: root
      mode: 0755
  
  - name: execute goss
    goss:
      path: /tmp/goss/goss.yaml
      format: tap
      output_file: "/tmp/goss/goss_{{ inventory_hostname }}.log"
      goss_path: /tmp/goss/bin/goss
    ignore_errors: yes
  
  - name: fetch log
    fetch:
      src: "/tmp/goss/goss_{{ inventory_hostname }}.log"
      dest: "./log/goss_{{ inventory_hostname }}.log"
      flat: yes

実行します。

$ ansible-playbook goss_local.yml --diff -v
No config file found; using defaults
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] ****************************************************************************************************************************************************************************************************************************

TASK [copy goss files] **********************************************************************************************************************************************************************************************************************
changed: [localhost] => {"changed": true, "dest": "/tmp/", "src": "/opt/ansible/goss/files/goss"}

TASK [delegate the right to execute] ********************************************************************************************************************************************************************************************************
ok: [localhost] => {"changed": false, "gid": 0, "group": "root", "mode": "0755", "owner": "root", "path": "/tmp/goss/bin/goss", "secontext": "unconfined_u:object_r:user_home_t:s0", "size": 12414976, "state": "file", "uid": 0}

TASK [execute goss] *************************************************************************************************************************************************************************************************************************
ok: [localhost] => {"changed": false, "stdout": "1..11\nok 1 - Process: sshd: running: matches expectation: [true]\nok 2 - User: sshd: exists: matches expectation: [true]\nok 3 - User: sshd: uid: matches expectation: [74]\nok 4 - User: sshd: gid: matches expectation: [74]\nok 5 - User: sshd: home: matches expectation: [\"/var/empty/sshd\"]\nok 6 - User: sshd: groups: matches expectation: [[\"sshd\"]]\nok 7 - User: sshd: shell: matches expectation: [\"/sbin/nologin\"]\nok 8 - Group: sshd: exists: matches expectation: [true]\nok 9 - Group: sshd: gid: matches expectation: [74]\nok 10 - Service: sshd: enabled: matches expectation: [true]\nok 11 - Service: sshd: running: matches expectation: [true]\n", "stdout_lines": ["1..11", "ok 1 - Process: sshd: running: matches expectation: [true]", "ok 2 - User: sshd: exists: matches expectation: [true]", "ok 3 - User: sshd: uid: matches expectation: [74]", "ok 4 - User: sshd: gid: matches expectation: [74]", "ok 5 - User: sshd: home: matches expectation: [\"/var/empty/sshd\"]", "ok 6 - User: sshd: groups: matches expectation: [[\"sshd\"]]", "ok 7 - User: sshd: shell: matches expectation: [\"/sbin/nologin\"]", "ok 8 - Group: sshd: exists: matches expectation: [true]", "ok 9 - Group: sshd: gid: matches expectation: [74]", "ok 10 - Service: sshd: enabled: matches expectation: [true]", "ok 11 - Service: sshd: running: matches expectation: [true]"]}

TASK [fetch log] ****************************************************************************************************************************************************************************************************************************
changed: [localhost] => {"changed": true, "checksum": "1592662d878a81087e426b4bf250e155d3485674", "dest": "/opt/ansible/goss/log/goss_localhost.log", "md5sum": "663ccd7670bc02ce7da6910c6a2383bc", "remote_checksum": "1592662d878a81087e426b4bf250e155d3485674", "remote_md5sum": null}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
localhost                  : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

$ cat log/goss_localhost.log 
1..11
ok 1 - Process: sshd: running: matches expectation: [true]
ok 2 - User: sshd: exists: matches expectation: [true]
ok 3 - User: sshd: uid: matches expectation: [74]
ok 4 - User: sshd: gid: matches expectation: [74]
ok 5 - User: sshd: home: matches expectation: ["/var/empty/sshd"]
ok 6 - User: sshd: groups: matches expectation: [["sshd"]]
ok 7 - User: sshd: shell: matches expectation: ["/sbin/nologin"]
ok 8 - Group: sshd: exists: matches expectation: [true]
ok 9 - Group: sshd: gid: matches expectation: [74]
ok 10 - Service: sshd: enabled: matches expectation: [true]
ok 11 - Service: sshd: running: matches expectation: [true]

無事に実行結果をファイルに出力することができました。

Ansible から実行することで、ターゲットノード数が増えても並列実行ができるのではないかと思います。

感想

最後に、ここまで検討した感想を述べます:

  1. インストールは簡単。12MBの単一実行ファイルだけで実行できる
  2. validate は爆速。Ansible の力を借りれば並列実行できるのでさらに実行速度が上がる
  3. validate のフォーマットは個人的にはtapがシンプルで読みやすい
  4. goss.yaml に書いた順番通りにテストしてくれないので、テスト項目ごとにファイルを分けたほうがよさそう
  5. リソースタイプは Serverspec と比較してとても少ないので、できる範囲で移行する、という割り切りが必要(とはいえ、ほとんどのものは移行できそう)

参考

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5