goss

Gossでのインフラテスト+小ネタいくつか

0. はじめに

  • Gossというテストツールを紹介します
  • Gossの初歩的な使い方を紹介します
  • Gossを使う上での小ネタをいくつか紹介します

1. Goss?

1.1. Gossとは

テストツールの1つで、Serverspecと競合する。GossはGo言語で書かれており、バイナリ形式でサーバに配布される。

テストはYAML形式で書くが、変数はYAMLでもJsonでも対応している。

1.2. Serverspecに負けているところ

1つめ。Gossはリモートでテストする機能がない点。AnsibleのGossモジュール など、非公式ではあるものの何かと組み合わせる他ない。つまり、あるサーバでテスト結果を集約するのに対して、信頼性が薄い。が、本番環境でもこういうものが使えるよ、という組織にとってはCIサーバで構築もテストも集約して行えるので、あまりServerspecに負けているとは言えない。

2つめは、良し悪しが分かれるが。Serverspecはいくつものモジュールを持っているが、試験したい内容に即したモジュールがなければRuby(Rspec)で記述するという逃げが出来る。これはメリットもデメリットも含んでいるが、こうしてでもテストしたいものがある場合はGossは負ける。

1.3. Serverspecより嬉しいところ

まず1つめ。yaml形式でテストを記述できるところ…もそうだが、変数ファイルをyamlで書けるところが、個人的には大きかった。

特にAnsibleやSaltStackを利用している組織からすると、変数をyaml形式で書いているのでGossだとそのまま利用できる点はアドバンテージかと思う。ServerspecではRubyで書けば出来るが、そのための不要なコードを書かなくて良いことは大きいと思っている。

ChefとかPuppetを使っている組織からすると、どうなんだろう…。

Kubernetes自体もそうだし、Kubernetesの構築ツールとしてAnsibleやSaltStackでの構築スタックが出てきているところからすると、YAML形式で記載するツールが求められているのだろう、だからGossも…と思う。

2つめ。バイナリ形式でのインストールという点。ServerspecはRubyに依存するし、RspecやRubyそのものでServerspecに足りないものを補完するという行為はよく為される。それが落とし穴になることがあり、例えばRubyのバージョンを上げたら今までと動作が違う、とか、複数部署でテスト環境のRubyバージョンや依存関係が違っており、マージしたら依存するコンポーネントが競合してテスト自体が出来ない…とかとか。Gossだと1つのバイナリなので、こうした依存関係の不具合は起こらない。

1.4. 使ってみて

「テストツールをこれから導入する」「AnsibleなどYAML形式で定義するツールを使っていて、有志のAnsible Gossモジュールも使ってOK」という条件に当てはまる組織にとっては導入の価値があると思います。

先述しましたが、AnsibleやSaltStackで構築、そこで使った変数を使って振る舞いテストをGossで行う、という連携ができます。インフラでのTDDにピッタリですね。

2. インストール

参考: 公式Github

本番環境にはcurl | shでインストールしないように、とある。単にバージョンを指定するかしないかの違いなだけで、前者のインストール方法であれば最新版がインストールされる。

インストールパスは、/usr/local/bin/gossで、バイナリ形式。

# 最新版のインストール
curl -fsSL https://goss.rocks/install | sh

# マニュアルインストールはこちら。_VERSION_はバージョンで置き換えること。
curl -L https://github.com/aelsabbahy/goss/releases/download/_VERSION_/goss-linux-amd64 -o /usr/local/bin/goss
chmod +rx /usr/local/bin/goss

goss コマンドを実行して正常に実行できるか確認する。

goss --version
goss version v0.3.5

3. let's test

3.1. autoaddでテストスイートの作成と実行

goss autoadd コマンドによってgoss.yamlというテストを記述したファイルを作成することが出来る。

物は試しにいざ実行。

$ goss autoadd sshd
Adding Process to './goss.yaml':

sshd:
  running: true

()

すると、上記コマンドを実行したディレクトリにgoss.yamlというファイルが作成される。

port:
  tcp:22:
    listening: true
    ip:
    - 0.0.0.0
service:
  sshd:
    enabled: true
    running: true
user:
  sshd:
    exists: true
    uid: 110
    gid: 65534
    groups:
    - nogroup
    home: /var/run/sshd
    shell: /usr/sbin/nologin
(略)

実行したサーバのsshdコンフィグ次第でこのyamlファイルの記述が変わるので、実行すれば必ず成功する。

# 実行してみる
$ goss validate
...........

Total Duration: 0.012s
Count: 11, Failed: 0, Skipped: 0

1つのドットが1回のテスト試行を指し、末尾にテスト結果サマリが表示される。

3.2. 色々なテスト結果表示方法

このテスト結果の表示フォーマットは--formatで変更できるので、好きなフォーマットを選択すると良い。

選択可能な全オプションは コチラ を確認すること。

好みや環境次第だが、よく使うんじゃないかなと思うフォーマットをいくつか記載する。

# json形式で表示する
$ goss validate --format json
{
    "results": [
        {
            "duration": 9198,
            "err": null,
            "expected": [
                "true"
            ],
            "found": [
                "true"
            ],
            "human": "",
            "meta": null,
            "property": "listening",
            "resource-id": "tcp:22",
            "resource-type": "Port",
            "result": 0,
            "successful": true,
            "summary-line": "Port: tcp:22: listening: matches expectation: [true]",
            "test-type": 0,
            "title": ""
        },
()

# tap形式で表示する
$ goss validate --format tap
1..11
ok 1 - User: sshd: exists: matches expectation: [true]
ok 2 - User: sshd: uid: matches expectation: [110]
ok 3 - User: sshd: gid: matches expectation: [65534]
ok 4 - User: sshd: home: matches expectation: ["/var/run/sshd"]
ok 5 - User: sshd: groups: matches expectation: [["nogroup"]]
ok 6 - User: sshd: shell: matches expectation: ["/usr/sbin/nologin"]
ok 7 - Port: tcp:22: listening: matches expectation: [true]
ok 8 - Port: tcp:22: ip: matches expectation: [["0.0.0.0"]]
ok 9 - Process: sshd: running: matches expectation: [true]
ok 10 - Service: sshd: enabled: matches expectation: [true]
ok 11 - Service: sshd: running: matches expectation: [true]

# JUnit形式で表示する
$ goss validate --format junit
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="goss" errors="0" tests="11" failures="0" skipped="0" time="0.005" timestamp="2018-01-05T10:15:28Z">
<testcase name="User sshd exists" time="0.000">
<system-out>User: sshd: exists: matches expectation: [true]</system-out>
</testcase>
()

# documentation形式で表示する(本当は成功していると緑色、失敗していると赤色で表示される)
$ goss validate --format documentation
User: sshd: exists: matches expectation: [true]
User: sshd: uid: matches expectation: [110]
User: sshd: gid: matches expectation: [65534]
User: sshd: home: matches expectation: ["/var/run/sshd"]
User: sshd: groups: matches expectation: [["nogroup"]]
User: sshd: shell: matches expectation: ["/usr/sbin/nologin"]
Port: tcp:22: listening: matches expectation: [true]
Port: tcp:22: ip: matches expectation: [["0.0.0.0"]]
Process: sshd: running: matches expectation: [true]
Service: sshd: enabled: matches expectation: [true]
Service: sshd: running: matches expectation: [true]


Total Duration: 0.009s
Count: 11, Failed: 0, Skipped: 0

JUnit形式で出力できるので、CI/CDツールとの組み合わせでテスト結果サマリがいい感じに表示できるのは良いですね。

3.3. 小ネタ集

3.3.1. goss.yaml以外のファイルでテストしたい

デフォルトはgoss.yamlを読み込むが、違うファイル名で記載した場合は--gossfileまたは-gオプションを使う。

goss -g goss_hoge.yaml validate

3.3.2. 外部の変数ファイルを読み込んでテストに使いたい

--varsオプションで指定する。YAML, JSON形式に対応している。

goss --vars variables.yaml validate

key:valueの変数を使う

普通のkey:value形式で変数が定義されている場合のアクセス方法。

$ cat variables.yaml
port: 22
listen: 0.0.0.0
$ cat goss.yaml
port:
  tcp:{{.Vars.port}}:
    listening: true
    ip:
    - {{.Vars.listen}}

上記のように.Varsを使うと変数にアクセスできる。

ループしたい

range を使う。

変数がこんな感じだったとする。

variables.yaml
file_check:
  /etc/passwd:
    contains:
    - "sshd"
    - "dnsmasq"
  /etc/group:
    contains:
    - "ssh"
    - "nogroup"

goss.yamlはこれ。

goss.yaml
file:
  {{- range $file, $value := .Vars.file_check}}
  {{$file}}:
    exists: true
    contains:
    {{- range $value.contains}}
    - {{.}}
    {{- end}}
  {{- end}}

注釈: {{- range ...}}のようにマイナス記号を使っているが、使わなくても正常に実行できる。標準的には括弧{{ }} は行の先頭に書くべきなので、括弧{{ }}以前の空白を無視するオプションであるマイナス記号を記載している。

これは、このテストを行っているのと同じ。

goss.yaml
file:
  /etc/passwd:
    exists: true
    contains:
    - "sshd"
    - "dnsmasq"
  /etc/group:
    exists: true
    contains:
    - "ssh"
    - "nogroup"

上記から分かるように、templateを使うと、ループはrangeで表現することができる。

環境変数でテスト内容を微妙に変えたい

例えば、本番環境/開発環境/ステージング環境など、環境毎にちょっと内容を変えたいということはあると思う。

環境変数を読み込むには.Envを使い、条件節には{{if}}を使い、"本番環境だったら"、"開発環境だったら"…という条件を書くことができる。

goss.yaml
http:
  {{if eq .Env.ENVIRONMENT "production"}}https://www.yahoo.co.jp/{{else if eq .Env.ENVIRONMENT "development"}}https://www.google.co.jp/{{else}}https://www.bing.com/{{end}}:
    status: 200
    allow-insecure: false
    timeout: 10000
    body: []

環境変数"ENVIRONMENT"を指定ながら実行する。

ENVIRONMENT=production goss validate

ループ内部で環境変数を使いたい

ここまで読んだ人は、{{range}}.Env{{if}}を組み合わせればループの中で環境変数を切り替えることが出来るだろうと推測するはず。

が、YAMLは{{ }}ステートメントの内部にドット.始まりの変数を呼ぶことが出来ないので、以下の記述はエラーになってしまう。

{{- range .Vars.domains}}
  {{- if eq .Env.ENVIRONMENT}}
  {{- end}}
{{- end}}

ただし、ダラーマーク\$始まりの変数は{{ }}ステートメント内でもアクセス可能。

そこで、\$変数に .変数を逃がす。

goss.yaml
file:
  # これによって.Envは$variables.Envでアクセスすることができるようになる
  {{- $variables := . -}}
  {{- range $file, $value := .Vars.file_check}}
  {{$file}}:
    exists: {{if eq $variables.Env.ENVIRONMENT "production"}}true{{else}}false{{end}}
    contains:
    {{- range $value.contains}}
    - {{.}}
    {{- end}}
  {{- end}}